diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 56e28b578..4a584773d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -65,6 +65,14 @@ jobs:
with:
go-version-file: go.mod
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: Setup pnpm
+ run: corepack enable && corepack prepare pnpm@latest --activate
+
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
diff --git a/.gitignore b/.gitignore
index a52b8d25a..61fe494ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,6 +47,12 @@ docs/plans/
# Added by goreleaser init:
dist/
+*.vite/
# Windows Application Icon/Resource
*.syso
+
+# Keep embedded backend dist directory placeholder in VCS
+!web/backend/dist/
+web/backend/dist/*
+!web/backend/dist/.gitkeep
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index fe208ebd4..70ea67323 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -6,8 +6,9 @@ before:
hooks:
- go mod tidy
- go generate ./...
+ - sh -c 'cd web/frontend && pnpm install && pnpm build:backend'
- go install github.com/tc-hib/go-winres@latest
- - go-winres make --in cmd/picoclaw-launcher/winres/winres.json --out cmd/picoclaw-launcher/rsrc --product-version={{ .Version }} --file-version={{ .Version }}
+ - go-winres make --in web/backend/winres/winres.json --out web/backend/rsrc --product-version={{ .Version }} --file-version={{ .Version }}
builds:
- id: picoclaw
@@ -70,7 +71,7 @@ builds:
- "7"
gomips:
- softfloat
- main: ./cmd/picoclaw-launcher
+ main: ./web/backend
ignore:
- goos: windows
goarch: arm
@@ -178,6 +179,11 @@ nfpms:
- rpm
- deb
bindir: /usr/bin
+ contents:
+ - src: web/picoclaw-launcher.desktop
+ dst: /usr/share/applications/picoclaw-launcher.desktop
+ - src: web/picoclaw-launcher.png
+ dst: /usr/share/icons/hicolor/512x512/apps/picoclaw-launcher.png
changelog:
sort: asc
diff --git a/Makefile b/Makefile
index 8de98e984..955c1c966 100644
--- a/Makefile
+++ b/Makefile
@@ -111,6 +111,18 @@ build: generate
@echo "Build complete: $(BINARY_PATH)"
@ln -sf $(BINARY_NAME)-$(PLATFORM)-$(ARCH) $(BUILD_DIR)/$(BINARY_NAME)
+## build-launcher: Build the picoclaw-launcher (web console) binary
+build-launcher:
+ @echo "Building picoclaw-launcher for $(PLATFORM)/$(ARCH)..."
+ @mkdir -p $(BUILD_DIR)
+ @if [ ! -f web/backend/dist/index.html ]; then \
+ echo "Building frontend..."; \
+ cd web/frontend && pnpm install && pnpm build:backend; \
+ fi
+ @$(GO) build $(GOFLAGS) -o $(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH) ./web/backend
+ @ln -sf picoclaw-launcher-$(PLATFORM)-$(ARCH) $(BUILD_DIR)/picoclaw-launcher
+ @echo "Build complete: $(BUILD_DIR)/picoclaw-launcher"
+
## build-whatsapp-native: Build with WhatsApp native (whatsmeow) support; larger binary
build-whatsapp-native: generate
## @echo "Building $(BINARY_NAME) with WhatsApp native for $(PLATFORM)/$(ARCH)..."
diff --git a/cmd/picoclaw-launcher/README.md b/cmd/picoclaw-launcher/README.md
deleted file mode 100644
index 0872a5f65..000000000
--- a/cmd/picoclaw-launcher/README.md
+++ /dev/null
@@ -1,290 +0,0 @@
-# PicoClaw Launcher
-
-> [!WARNING]
-> This project is a temporary solution and will be refactored in the future to provide a complete web service. Therefore, the APIs in this directory are not stable.
-
-A standalone launcher for PicoClaw, providing visual JSON editing and OAuth provider authentication management.
-
-## Features
-
-- 📝 **Config Editor** — Sidebar-based settings UI with model management, channel configuration forms, and a raw JSON editor
-- 🤖 **Model Management** — Model card grid with availability status (grayed out without API key), primary model selection, add/edit/delete with required/optional field separation
-- 📡 **Channel Configuration** — Form-based settings for 13 channel types (Telegram, Discord, Slack, Matrix, WeCom, DingTalk, Feishu, LINE, WhatsApp, QQ, OneBot, MaixCAM, etc.) with documentation links
-- 🔐 **Provider Auth** — Login to OpenAI (Device Code), Anthropic (API Token), Google Antigravity (Browser OAuth)
-- 🌐 **Embedded Frontend** — Compiles to a single binary with no external dependencies
-- 🌍 **i18n** — Chinese/English language switching with browser auto-detection
-- 🎨 **Theme** — Light / Dark / System theme toggle with localStorage persistence
-
-## Quick Start
-
-```bash
-# Build
-go build -o picoclaw-launcher ./cmd/picoclaw-launcher/
-
-# Run with default config path (~/.picoclaw/config.json)
-./picoclaw-launcher
-
-# Specify a config file
-./picoclaw-launcher ./config.json
-
-# Allow LAN access
-./picoclaw-launcher -public
-```
-
-Open `http://localhost:18800` in your browser.
-
-## CLI Options
-
-```
-Usage: picoclaw-config [options] [config.json]
-
-Arguments:
- config.json Path to the configuration file (default: ~/.picoclaw/config.json)
-
-Options:
- -public Listen on all interfaces (0.0.0.0), allowing access from other devices
-```
-
-## API Reference
-
-Base URL: `http://localhost:18800`
-
----
-
-### Static Files
-
-#### GET /
-
-Serves the embedded frontend (`index.html`).
-
----
-
-### Config API
-
-#### GET /api/config
-
-Reads the current configuration file.
-
-**Response** `200 OK`
-
-```json
-{
- "config": { ... },
- "path": "/Users/xiao/.picoclaw/config.json"
-}
-```
-
----
-
-#### PUT /api/config
-
-Saves the configuration. The request body must be a complete Config JSON object.
-
-**Request Body** — `application/json`
-
-```json
-{
- "agents": { "defaults": { "model_name": "gpt-5.2" } },
- "model_list": [
- {
- "model_name": "gpt-5.2",
- "model": "openai/gpt-5.2",
- "auth_method": "oauth"
- }
- ]
-}
-```
-
-**Response** `200 OK`
-
-```json
-{ "status": "ok" }
-```
-
-**Error** `400 Bad Request` — Invalid JSON
-
----
-
-### Auth API
-
-#### GET /api/auth/status
-
-Returns the authentication status of all providers and any in-progress device code login.
-
-**Response** `200 OK`
-
-```json
-{
- "providers": [
- {
- "provider": "openai",
- "auth_method": "oauth",
- "status": "active",
- "account_id": "user-xxx",
- "expires_at": "2026-03-01T00:00:00Z"
- }
- ],
- "pending_device": {
- "provider": "openai",
- "status": "pending",
- "device_url": "https://auth.openai.com/activate",
- "user_code": "ABCD-1234"
- }
-}
-```
-
-`status` values: `active` | `expired` | `needs_refresh`
-
-`pending_device` is only present when a device code login is in progress.
-
----
-
-#### POST /api/auth/login
-
-Initiates a provider login.
-
-**Request Body** — `application/json`
-
-```json
-{ "provider": "openai" }
-```
-
-Supported `provider` values: `openai` | `anthropic` | `google-antigravity`
-
-##### OpenAI (Device Code Flow)
-
-Returns device code info. The server polls for completion in the background.
-
-```json
-{
- "status": "pending",
- "device_url": "https://auth.openai.com/activate",
- "user_code": "ABCD-1234",
- "message": "Open the URL and enter the code to authenticate."
-}
-```
-
-The user opens `device_url` in a browser and enters `user_code`. Once authenticated, `GET /api/auth/status` will show `pending_device.status` as `success`.
-
-##### Anthropic (API Token)
-
-Requires a `token` field in the request:
-
-```json
-{ "provider": "anthropic", "token": "sk-ant-xxx" }
-```
-
-**Response:**
-
-```json
-{ "status": "success", "message": "Anthropic token saved" }
-```
-
-##### Google Antigravity (Browser OAuth)
-
-Returns an authorization URL for the frontend to open in a new tab:
-
-```json
-{
- "status": "redirect",
- "auth_url": "https://accounts.google.com/o/oauth2/auth?...",
- "message": "Open the URL to authenticate with Google."
-}
-```
-
-After authentication, Google redirects to `GET /auth/callback`, which saves the credentials and redirects back to the picoclaw-config UI.
-
----
-
-#### POST /api/auth/logout
-
-Logs out from a provider.
-
-**Request Body** — `application/json`
-
-```json
-{ "provider": "openai" }
-```
-
-Omit or leave `provider` empty to log out from all providers.
-
-**Response** `200 OK`
-
-```json
-{ "status": "ok" }
-```
-
----
-
-#### GET /auth/callback
-
-OAuth browser callback endpoint (used by Google Antigravity). Called by the OAuth provider's redirect — **not invoked directly by the frontend**.
-
-**Query Parameters:**
-- `state` — OAuth state for CSRF validation
-- `code` — Authorization code
-
-On success, redirects to `/#auth`.
-
-
-### Process API
-
-#### GET /api/process/status
-
-Gets the running status of the `picoclaw gateway` process.
-
-**Response** `200 OK` (Running)
-
-```json
-{
- "process_status": "running",
- "status": "ok",
- "uptime": "1.010814s"
-}
-```
-
-**Response** `200 OK` (Stopped)
-
-```json
-{
- "process_status": "stopped",
- "error": "Get \"http://localhost:18790/health\": dial tcp [::1]:18790: connect: connection refused"
-}
-```
-
----
-
-#### POST /api/process/start
-
-Starts the `picoclaw gateway` process in the background.
-
-**Response** `200 OK`
-
-```json
-{
- "status": "ok",
- "pid": 12345
-}
-```
-
----
-
-#### POST /api/process/stop
-
-Stops the running `picoclaw gateway` process.
-
-**Response** `200 OK`
-
-```json
-{
- "status": "ok"
-}
-```
-
----
-
-## Testing
-
-```bash
-go test -v ./cmd/picoclaw-launcher/
-```
diff --git a/cmd/picoclaw-launcher/README.zh.md b/cmd/picoclaw-launcher/README.zh.md
deleted file mode 100644
index 320de75a5..000000000
--- a/cmd/picoclaw-launcher/README.zh.md
+++ /dev/null
@@ -1,287 +0,0 @@
-# PicoClaw Launcher
-
-> [!WARNING]
-> 该项目属于临时解决方案,后续会重构并提供完整的 Web 服务,因此该目录下的接口并不稳定。
-
-PicoClaw 的独立启动器,提供可视化 JSON 配置编辑和 OAuth Provider 认证管理。
-
-## 功能
-
-- 📝 **配置编辑** — 侧边栏式设置 UI,支持模型管理、通道配置表单和原始 JSON 编辑器
-- 🤖 **模型管理** — 模型卡片网格,可用性状态显示(无 API Key 时灰色),主模型选择,增删改查,必填/选填字段分离
-- 📡 **通道配置** — 12 种通道类型(Telegram、Discord、Slack、企业微信、钉钉、飞书、LINE、WhatsApp、QQ、OneBot、MaixCAM 等)的表单化配置,附带文档链接
-- 🔐 **Provider 认证** — 支持 OpenAI (Device Code)、Anthropic (API Token)、Google Antigravity (Browser OAuth) 登录
-- 🌐 **嵌入式前端** — 编译为单一二进制文件,无需额外依赖
-- 🌍 **国际化** — 中英文切换,首次访问自动检测浏览器语言
-- 🎨 **主题** — 亮色 / 暗色 / 跟随系统,偏好保存在 localStorage
-
-## 快速开始
-
-```bash
-# 编译
-go build -o picoclaw-launcher ./cmd/picoclaw-launcher/
-
-# 运行(使用默认配置路径 ~/.picoclaw/config.json)
-./picoclaw-launcher
-
-# 指定配置文件
-./picoclaw-launcher ./config.json
-
-# 允许局域网访问
-./picoclaw-launcher -public
-```
-
-启动后在浏览器中打开 `http://localhost:18800`。
-
-## 命令行参数
-
-```
-Usage: picoclaw-launcher [options] [config.json]
-
-Arguments:
- config.json 配置文件路径(默认: ~/.picoclaw/config.json)
-
-Options:
- -public 监听所有网络接口(0.0.0.0),允许局域网设备访问
-```
-
-## API 文档
-
-Base URL: `http://localhost:18800`
-
-### 静态文件
-
-#### GET /
-
-提供嵌入式前端页面(`index.html`)。
-
----
-
-### Config API
-
-#### GET /api/config
-
-读取当前配置文件内容。
-
-**Response** `200 OK`
-
-```json
-{
- "config": { ... },
- "path": "/Users/xiao/.picoclaw/config.json"
-}
-```
-
----
-
-#### PUT /api/config
-
-保存配置。请求体为完整的 Config JSON。
-
-**Request Body** — `application/json`
-
-```json
-{
- "agents": { "defaults": { "model_name": "gpt-5.2" } },
- "model_list": [
- {
- "model_name": "gpt-5.2",
- "model": "openai/gpt-5.2",
- "auth_method": "oauth"
- }
- ]
-}
-```
-
-**Response** `200 OK`
-
-```json
-{ "status": "ok" }
-```
-
-**Error** `400 Bad Request` — 无效 JSON
-
----
-
-### Auth API
-
-#### GET /api/auth/status
-
-获取所有 Provider 的认证状态和进行中的 Device Code 登录信息。
-
-**Response** `200 OK`
-
-```json
-{
- "providers": [
- {
- "provider": "openai",
- "auth_method": "oauth",
- "status": "active",
- "account_id": "user-xxx",
- "expires_at": "2026-03-01T00:00:00Z"
- }
- ],
- "pending_device": {
- "provider": "openai",
- "status": "pending",
- "device_url": "https://auth.openai.com/activate",
- "user_code": "ABCD-1234"
- }
-}
-```
-
-`status` 可选值: `active` | `expired` | `needs_refresh`
-
-`pending_device` 仅在有进行中的 Device Code 登录时返回。
-
----
-
-#### POST /api/auth/login
-
-发起 Provider 登录。
-
-**Request Body** — `application/json`
-
-```json
-{ "provider": "openai" }
-```
-
-支持的 `provider` 值: `openai` | `anthropic` | `google-antigravity`
-
-##### OpenAI (Device Code Flow)
-
-返回 Device Code 信息,后台自动轮询认证结果:
-
-```json
-{
- "status": "pending",
- "device_url": "https://auth.openai.com/activate",
- "user_code": "ABCD-1234",
- "message": "Open the URL and enter the code to authenticate."
-}
-```
-
-用户在浏览器中打开 `device_url` 并输入 `user_code`。认证完成后通过 `GET /api/auth/status` 的 `pending_device.status` 变为 `success` 通知前端。
-
-##### Anthropic (API Token)
-
-需在请求中附带 token:
-
-```json
-{ "provider": "anthropic", "token": "sk-ant-xxx" }
-```
-
-**Response:**
-
-```json
-{ "status": "success", "message": "Anthropic token saved" }
-```
-
-##### Google Antigravity (Browser OAuth)
-
-返回授权 URL,前端打开新标签页:
-
-```json
-{
- "status": "redirect",
- "auth_url": "https://accounts.google.com/o/oauth2/auth?...",
- "message": "Open the URL to authenticate with Google."
-}
-```
-
-认证完成后 Google 回调至 `GET /auth/callback`,自动保存凭据并重定向回 picoclaw-config 页面。
-
----
-
-#### POST /api/auth/logout
-
-登出 Provider。
-
-**Request Body** — `application/json`
-
-```json
-{ "provider": "openai" }
-```
-
-传空字符串或省略 `provider` 则登出所有 Provider。
-
-**Response** `200 OK`
-
-```json
-{ "status": "ok" }
-```
-
----
-
-#### GET /auth/callback
-
-OAuth Browser 回调端点(Google Antigravity 专用),由 OAuth Provider 重定向调用,**非前端直接使用**。
-
-**Query Parameters:**
-- `state` — OAuth state 校验
-- `code` — 授权码
-
-认证成功后重定向到 `/#auth`。
-
-### Process API
-
-#### GET /api/process/status
-
-获取 `picoclaw gateway` 进程的运行状态。
-
-**Response** `200 OK` (运行中)
-
-```json
-{
- "process_status": "running",
- "status": "ok",
- "uptime": "1.010814s"
-}
-```
-
-**Response** `200 OK` (未运行)
-
-```json
-{
- "process_status": "stopped",
- "error": "Get \"http://localhost:18790/health\": dial tcp [::1]:18790: connect: connection refused"
-}
-```
-
----
-
-#### POST /api/process/start
-
-在后台启动 `picoclaw gateway` 进程。
-
-**Response** `200 OK`
-
-```json
-{
- "status": "ok",
- "pid": 12345
-}
-```
-
----
-
-#### POST /api/process/stop
-
-停止正在运行的 `picoclaw gateway` 进程。
-
-**Response** `200 OK`
-
-```json
-{
- "status": "ok"
-}
-```
-
----
-
-## 测试
-
-```bash
-go test -v ./cmd/picoclaw-launcher/
-```
diff --git a/cmd/picoclaw-launcher/internal/server/auth_config.go b/cmd/picoclaw-launcher/internal/server/auth_config.go
deleted file mode 100644
index f75e8fff0..000000000
--- a/cmd/picoclaw-launcher/internal/server/auth_config.go
+++ /dev/null
@@ -1,147 +0,0 @@
-package server
-
-import (
- "log"
- "strings"
-
- "github.com/sipeed/picoclaw/pkg/auth"
- "github.com/sipeed/picoclaw/pkg/config"
-)
-
-// updateConfigAfterLogin updates config.json after a successful provider login.
-func updateConfigAfterLogin(configPath, provider string, cred *auth.AuthCredential) {
- cfg, err := config.LoadConfig(configPath)
- if err != nil {
- log.Printf("Warning: could not load config to update auth_method: %v", err)
- return
- }
-
- switch provider {
- case "openai":
- cfg.Providers.OpenAI.AuthMethod = "oauth"
- found := false
- for i := range cfg.ModelList {
- if isOpenAIModel(cfg.ModelList[i].Model) {
- cfg.ModelList[i].AuthMethod = "oauth"
- found = true
- break
- }
- }
- if !found {
- cfg.ModelList = append(cfg.ModelList, config.ModelConfig{
- ModelName: "gpt-5.2",
- Model: "openai/gpt-5.2",
- AuthMethod: "oauth",
- })
- }
- cfg.Agents.Defaults.ModelName = "gpt-5.2"
-
- case "anthropic":
- cfg.Providers.Anthropic.AuthMethod = "token"
- found := false
- for i := range cfg.ModelList {
- if isAnthropicModel(cfg.ModelList[i].Model) {
- cfg.ModelList[i].AuthMethod = "token"
- found = true
- break
- }
- }
- if !found {
- cfg.ModelList = append(cfg.ModelList, config.ModelConfig{
- ModelName: "claude-sonnet-4.6",
- Model: "anthropic/claude-sonnet-4.6",
- AuthMethod: "token",
- })
- }
- cfg.Agents.Defaults.ModelName = "claude-sonnet-4.6"
-
- case "google-antigravity":
- cfg.Providers.Antigravity.AuthMethod = "oauth"
- found := false
- for i := range cfg.ModelList {
- if isAntigravityModel(cfg.ModelList[i].Model) {
- cfg.ModelList[i].AuthMethod = "oauth"
- found = true
- break
- }
- }
- if !found {
- cfg.ModelList = append(cfg.ModelList, config.ModelConfig{
- ModelName: "gemini-flash",
- Model: "antigravity/gemini-3-flash",
- AuthMethod: "oauth",
- })
- }
- cfg.Agents.Defaults.ModelName = "gemini-flash"
- }
-
- if err := config.SaveConfig(configPath, cfg); err != nil {
- log.Printf("Warning: could not update config: %v", err)
- }
-}
-
-// clearAuthMethodInConfig clears auth_method for a specific provider in config.json.
-func clearAuthMethodInConfig(configPath, provider string) {
- cfg, err := config.LoadConfig(configPath)
- if err != nil {
- return
- }
-
- for i := range cfg.ModelList {
- switch provider {
- case "openai":
- if isOpenAIModel(cfg.ModelList[i].Model) {
- cfg.ModelList[i].AuthMethod = ""
- }
- case "anthropic":
- if isAnthropicModel(cfg.ModelList[i].Model) {
- cfg.ModelList[i].AuthMethod = ""
- }
- case "google-antigravity", "antigravity":
- if isAntigravityModel(cfg.ModelList[i].Model) {
- cfg.ModelList[i].AuthMethod = ""
- }
- }
- }
-
- switch provider {
- case "openai":
- cfg.Providers.OpenAI.AuthMethod = ""
- case "anthropic":
- cfg.Providers.Anthropic.AuthMethod = ""
- case "google-antigravity", "antigravity":
- cfg.Providers.Antigravity.AuthMethod = ""
- }
-
- config.SaveConfig(configPath, cfg)
-}
-
-// clearAllAuthMethodsInConfig clears auth_method for all providers in config.json.
-func clearAllAuthMethodsInConfig(configPath string) {
- cfg, err := config.LoadConfig(configPath)
- if err != nil {
- return
- }
- for i := range cfg.ModelList {
- cfg.ModelList[i].AuthMethod = ""
- }
- cfg.Providers.OpenAI.AuthMethod = ""
- cfg.Providers.Anthropic.AuthMethod = ""
- cfg.Providers.Antigravity.AuthMethod = ""
- config.SaveConfig(configPath, cfg)
-}
-
-// ── Model identification helpers ─────────────────────────────────
-
-func isOpenAIModel(model string) bool {
- return model == "openai" || strings.HasPrefix(model, "openai/")
-}
-
-func isAnthropicModel(model string) bool {
- return model == "anthropic" || strings.HasPrefix(model, "anthropic/")
-}
-
-func isAntigravityModel(model string) bool {
- return model == "antigravity" || model == "google-antigravity" ||
- strings.HasPrefix(model, "antigravity/") || strings.HasPrefix(model, "google-antigravity/")
-}
diff --git a/cmd/picoclaw-launcher/internal/server/auth_config_test.go b/cmd/picoclaw-launcher/internal/server/auth_config_test.go
deleted file mode 100644
index 92158d011..000000000
--- a/cmd/picoclaw-launcher/internal/server/auth_config_test.go
+++ /dev/null
@@ -1,222 +0,0 @@
-package server
-
-import (
- "path/filepath"
- "testing"
-
- "github.com/sipeed/picoclaw/pkg/auth"
- "github.com/sipeed/picoclaw/pkg/config"
-)
-
-// ── Model identification helpers ─────────────────────────────────
-
-func TestIsOpenAIModel(t *testing.T) {
- tests := []struct {
- model string
- want bool
- }{
- {"openai", true},
- {"openai/gpt-4o", true},
- {"openai/gpt-5.2", true},
- {"anthropic", false},
- {"anthropic/claude-sonnet-4.6", false},
- {"openai-compatible", false},
- {"", false},
- }
- for _, tt := range tests {
- if got := isOpenAIModel(tt.model); got != tt.want {
- t.Errorf("isOpenAIModel(%q) = %v, want %v", tt.model, got, tt.want)
- }
- }
-}
-
-func TestIsAnthropicModel(t *testing.T) {
- tests := []struct {
- model string
- want bool
- }{
- {"anthropic", true},
- {"anthropic/claude-sonnet-4.6", true},
- {"openai", false},
- {"openai/gpt-4o", false},
- {"", false},
- }
- for _, tt := range tests {
- if got := isAnthropicModel(tt.model); got != tt.want {
- t.Errorf("isAnthropicModel(%q) = %v, want %v", tt.model, got, tt.want)
- }
- }
-}
-
-func TestIsAntigravityModel(t *testing.T) {
- tests := []struct {
- model string
- want bool
- }{
- {"antigravity", true},
- {"google-antigravity", true},
- {"antigravity/gemini-3-flash", true},
- {"google-antigravity/gemini-3-flash", true},
- {"openai", false},
- {"antigravity-custom", false},
- {"", false},
- }
- for _, tt := range tests {
- if got := isAntigravityModel(tt.model); got != tt.want {
- t.Errorf("isAntigravityModel(%q) = %v, want %v", tt.model, got, tt.want)
- }
- }
-}
-
-// ── Config update helpers ────────────────────────────────────────
-
-func writeTempConfigViaSave(t *testing.T, cfg *config.Config) string {
- t.Helper()
- dir := t.TempDir()
- path := filepath.Join(dir, "config.json")
- if err := config.SaveConfig(path, cfg); err != nil {
- t.Fatalf("save config: %v", err)
- }
- return path
-}
-
-func loadTempConfig(t *testing.T, path string) *config.Config {
- t.Helper()
- cfg, err := config.LoadConfig(path)
- if err != nil {
- t.Fatalf("load config: %v", err)
- }
- return cfg
-}
-
-func TestUpdateConfigAfterLogin_OpenAI_ExistingModel(t *testing.T) {
- cfg := &config.Config{
- ModelList: []config.ModelConfig{
- {ModelName: "gpt-4o", Model: "openai/gpt-4o"},
- },
- }
- path := writeTempConfigViaSave(t, cfg)
-
- cred := &auth.AuthCredential{AuthMethod: "oauth"}
- updateConfigAfterLogin(path, "openai", cred)
-
- result := loadTempConfig(t, path)
-
- // Model-level auth_method persists through serialization
- if len(result.ModelList) != 1 {
- t.Fatalf("expected 1 model, got %d", len(result.ModelList))
- }
- if result.ModelList[0].AuthMethod != "oauth" {
- t.Errorf("expected model auth_method=oauth, got %q", result.ModelList[0].AuthMethod)
- }
-}
-
-func TestUpdateConfigAfterLogin_OpenAI_NoExistingModel(t *testing.T) {
- cfg := &config.Config{
- ModelList: []config.ModelConfig{
- {ModelName: "claude", Model: "anthropic/claude-sonnet-4.6"},
- },
- }
- path := writeTempConfigViaSave(t, cfg)
-
- cred := &auth.AuthCredential{AuthMethod: "oauth"}
- updateConfigAfterLogin(path, "openai", cred)
-
- result := loadTempConfig(t, path)
-
- if len(result.ModelList) != 2 {
- t.Fatalf("expected 2 models (original + added), got %d", len(result.ModelList))
- }
- if result.ModelList[1].Model != "openai/gpt-5.2" {
- t.Errorf("expected added model openai/gpt-5.2, got %q", result.ModelList[1].Model)
- }
- if result.Agents.Defaults.ModelName != "gpt-5.2" {
- t.Errorf("expected default model_name=gpt-5.2, got %q", result.Agents.Defaults.ModelName)
- }
-}
-
-func TestUpdateConfigAfterLogin_Anthropic(t *testing.T) {
- cfg := &config.Config{}
- path := writeTempConfigViaSave(t, cfg)
-
- cred := &auth.AuthCredential{AuthMethod: "token"}
- updateConfigAfterLogin(path, "anthropic", cred)
-
- result := loadTempConfig(t, path)
-
- // Model should be added with correct auth_method
- if len(result.ModelList) != 1 {
- t.Fatalf("expected 1 model added, got %d", len(result.ModelList))
- }
- if result.ModelList[0].Model != "anthropic/claude-sonnet-4.6" {
- t.Errorf("expected model anthropic/claude-sonnet-4.6, got %q", result.ModelList[0].Model)
- }
- if result.ModelList[0].AuthMethod != "token" {
- t.Errorf("expected model auth_method=token, got %q", result.ModelList[0].AuthMethod)
- }
-}
-
-func TestUpdateConfigAfterLogin_GoogleAntigravity(t *testing.T) {
- cfg := &config.Config{}
- path := writeTempConfigViaSave(t, cfg)
-
- cred := &auth.AuthCredential{AuthMethod: "oauth"}
- updateConfigAfterLogin(path, "google-antigravity", cred)
-
- result := loadTempConfig(t, path)
-
- // Model should be added with correct auth_method
- if len(result.ModelList) != 1 {
- t.Fatalf("expected 1 model added, got %d", len(result.ModelList))
- }
- if result.ModelList[0].Model != "antigravity/gemini-3-flash" {
- t.Errorf("expected model antigravity/gemini-3-flash, got %q", result.ModelList[0].Model)
- }
- if result.ModelList[0].AuthMethod != "oauth" {
- t.Errorf("expected model auth_method=oauth, got %q", result.ModelList[0].AuthMethod)
- }
-}
-
-func TestClearAuthMethodInConfig(t *testing.T) {
- cfg := &config.Config{
- ModelList: []config.ModelConfig{
- {ModelName: "gpt-4o", Model: "openai/gpt-4o", AuthMethod: "oauth"},
- {ModelName: "claude", Model: "anthropic/claude-sonnet-4.6", AuthMethod: "token"},
- },
- }
- path := writeTempConfigViaSave(t, cfg)
-
- clearAuthMethodInConfig(path, "openai")
-
- result := loadTempConfig(t, path)
-
- // Openai model auth_method should be cleared
- if result.ModelList[0].AuthMethod != "" {
- t.Errorf("expected openai model auth_method cleared, got %q", result.ModelList[0].AuthMethod)
- }
- // Anthropic model should be unchanged
- if result.ModelList[1].AuthMethod != "token" {
- t.Errorf("expected anthropic model auth_method unchanged, got %q", result.ModelList[1].AuthMethod)
- }
-}
-
-func TestClearAllAuthMethodsInConfig(t *testing.T) {
- cfg := &config.Config{
- ModelList: []config.ModelConfig{
- {ModelName: "gpt-4o", Model: "openai/gpt-4o", AuthMethod: "oauth"},
- {ModelName: "claude", Model: "anthropic/claude-sonnet-4.6", AuthMethod: "token"},
- {ModelName: "gemini", Model: "antigravity/gemini-3-flash", AuthMethod: "oauth"},
- },
- }
- path := writeTempConfigViaSave(t, cfg)
-
- clearAllAuthMethodsInConfig(path)
-
- result := loadTempConfig(t, path)
-
- for i, m := range result.ModelList {
- if m.AuthMethod != "" {
- t.Errorf("model[%d] auth_method not cleared, got %q", i, m.AuthMethod)
- }
- }
-}
diff --git a/cmd/picoclaw-launcher/internal/server/auth_handlers.go b/cmd/picoclaw-launcher/internal/server/auth_handlers.go
deleted file mode 100644
index 3b48f9739..000000000
--- a/cmd/picoclaw-launcher/internal/server/auth_handlers.go
+++ /dev/null
@@ -1,315 +0,0 @@
-package server
-
-import (
- "encoding/json"
- "fmt"
- "io"
- "log"
- "net/http"
- "sync"
- "time"
-
- "github.com/sipeed/picoclaw/pkg/auth"
- "github.com/sipeed/picoclaw/pkg/providers"
-)
-
-// oauthSession stores in-flight OAuth state for browser-based flows.
-type oauthSession struct {
- Provider string
- PKCE auth.PKCECodes
- State string
- RedirectURI string
- OAuthCfg auth.OAuthProviderConfig
- ConfigPath string
-}
-
-// deviceCodeSession stores in-flight device code flow state.
-type deviceCodeSession struct {
- mu sync.Mutex
- Provider string
- Info *auth.DeviceCodeInfo
- OAuthCfg auth.OAuthProviderConfig
- ConfigPath string
- Status string // "pending", "success", "error"
- Error string
- Done bool
-}
-
-var (
- oauthSessions = map[string]*oauthSession{} // keyed by state
- oauthSessionsMu sync.Mutex
-
- activeDeviceSession *deviceCodeSession
- activeDeviceSessionMu sync.Mutex
-)
-
-// handleOpenAILogin starts the OpenAI device code flow and returns device code info to the frontend.
-func handleOpenAILogin(w http.ResponseWriter, configPath string) {
- // Check if there's already a pending device code session
- activeDeviceSessionMu.Lock()
- if activeDeviceSession != nil {
- activeDeviceSession.mu.Lock()
- if !activeDeviceSession.Done {
- resp := map[string]any{
- "status": "pending",
- "device_url": activeDeviceSession.Info.VerifyURL,
- "user_code": activeDeviceSession.Info.UserCode,
- "message": "Device code flow already in progress. Enter the code in your browser.",
- }
- activeDeviceSession.mu.Unlock()
- activeDeviceSessionMu.Unlock()
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(resp)
- return
- }
- activeDeviceSession.mu.Unlock()
- }
- activeDeviceSessionMu.Unlock()
-
- // Request a device code
- oauthCfg := auth.OpenAIOAuthConfig()
- info, err := auth.RequestDeviceCode(oauthCfg)
- if err != nil {
- http.Error(w, fmt.Sprintf("Failed to request device code: %v", err), http.StatusInternalServerError)
- return
- }
-
- session := &deviceCodeSession{
- Provider: "openai",
- Info: info,
- OAuthCfg: oauthCfg,
- ConfigPath: configPath,
- Status: "pending",
- }
-
- activeDeviceSessionMu.Lock()
- activeDeviceSession = session
- activeDeviceSessionMu.Unlock()
-
- // Start background polling
- go func() {
- deadline := time.After(15 * time.Minute)
- ticker := time.NewTicker(time.Duration(info.Interval) * time.Second)
- defer ticker.Stop()
-
- for {
- select {
- case <-deadline:
- session.mu.Lock()
- session.Status = "error"
- session.Error = "Authentication timed out after 15 minutes"
- session.Done = true
- session.mu.Unlock()
- return
- case <-ticker.C:
- cred, err := auth.PollDeviceCodeOnce(oauthCfg, info.DeviceAuthID, info.UserCode)
- if err != nil {
- continue // Still pending
- }
- if cred != nil {
- if saveErr := auth.SetCredential("openai", cred); saveErr != nil {
- session.mu.Lock()
- session.Status = "error"
- session.Error = saveErr.Error()
- session.Done = true
- session.mu.Unlock()
- return
- }
- updateConfigAfterLogin(configPath, "openai", cred)
- session.mu.Lock()
- session.Status = "success"
- session.Done = true
- session.mu.Unlock()
- log.Printf("OpenAI device code login successful (account: %s)", cred.AccountID)
- return
- }
- }
- }
- }()
-
- // Return device code info to frontend
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(map[string]any{
- "status": "pending",
- "device_url": info.VerifyURL,
- "user_code": info.UserCode,
- "message": "Open the URL and enter the code to authenticate.",
- })
-}
-
-// handleAnthropicLogin saves a pasted API token for Anthropic.
-func handleAnthropicLogin(w http.ResponseWriter, token, configPath string) {
- if token == "" {
- http.Error(w, "Token is required for Anthropic login", http.StatusBadRequest)
- return
- }
-
- cred := &auth.AuthCredential{
- AccessToken: token,
- Provider: "anthropic",
- AuthMethod: "token",
- }
-
- if err := auth.SetCredential("anthropic", cred); err != nil {
- http.Error(w, fmt.Sprintf("Failed to save credentials: %v", err), http.StatusInternalServerError)
- return
- }
-
- updateConfigAfterLogin(configPath, "anthropic", cred)
-
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(map[string]string{
- "status": "success",
- "message": "Anthropic token saved",
- })
-}
-
-// handleGoogleAntigravityLogin generates a PKCE + auth URL and returns it to the frontend.
-func handleGoogleAntigravityLogin(w http.ResponseWriter, r *http.Request, configPath string) {
- oauthCfg := auth.GoogleAntigravityOAuthConfig()
-
- pkce, err := auth.GeneratePKCE()
- if err != nil {
- http.Error(w, fmt.Sprintf("Failed to generate PKCE: %v", err), http.StatusInternalServerError)
- return
- }
-
- state, err := auth.GenerateState()
- if err != nil {
- http.Error(w, fmt.Sprintf("Failed to generate state: %v", err), http.StatusInternalServerError)
- return
- }
-
- // Build redirect URI pointing to picoclaw-launcher's own callback
- scheme := "http"
- redirectURI := fmt.Sprintf("%s://%s/auth/callback", scheme, r.Host)
-
- authURL := auth.BuildAuthorizeURL(oauthCfg, pkce, state, redirectURI)
-
- // Store session for callback
- oauthSessionsMu.Lock()
- oauthSessions[state] = &oauthSession{
- Provider: "google-antigravity",
- PKCE: pkce,
- State: state,
- RedirectURI: redirectURI,
- OAuthCfg: oauthCfg,
- ConfigPath: configPath,
- }
- oauthSessionsMu.Unlock()
-
- // Clean up stale sessions after 10 minutes
- go func() {
- time.Sleep(10 * time.Minute)
- oauthSessionsMu.Lock()
- delete(oauthSessions, state)
- oauthSessionsMu.Unlock()
- }()
-
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(map[string]string{
- "status": "redirect",
- "auth_url": authURL,
- "message": "Open the URL to authenticate with Google.",
- })
-}
-
-// handleOAuthCallback processes the OAuth callback from Google Antigravity.
-func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
- state := r.URL.Query().Get("state")
- code := r.URL.Query().Get("code")
-
- oauthSessionsMu.Lock()
- session, ok := oauthSessions[state]
- if ok {
- delete(oauthSessions, state)
- }
- oauthSessionsMu.Unlock()
-
- if !ok {
- http.Error(w, "Invalid or expired OAuth state", http.StatusBadRequest)
- return
- }
-
- if code == "" {
- errMsg := r.URL.Query().Get("error")
- w.Header().Set("Content-Type", "text/html")
- fmt.Fprintf(
- w,
- `
Authentication failed
%s
You can close this window.
`,
- errMsg,
- )
- return
- }
-
- cred, err := auth.ExchangeCodeForTokens(session.OAuthCfg, code, session.PKCE.CodeVerifier, session.RedirectURI)
- if err != nil {
- w.Header().Set("Content-Type", "text/html")
- fmt.Fprintf(
- w,
- `Authentication failed
%s
You can close this window.
`,
- err.Error(),
- )
- return
- }
-
- cred.Provider = session.Provider
-
- // Fetch user info for Google Antigravity
- if session.Provider == "google-antigravity" {
- if email, err := fetchGoogleUserEmail(cred.AccessToken); err == nil {
- cred.Email = email
- }
- if projectID, err := providers.FetchAntigravityProjectID(cred.AccessToken); err == nil {
- cred.ProjectID = projectID
- }
- }
-
- if err := auth.SetCredential(session.Provider, cred); err != nil {
- w.Header().Set("Content-Type", "text/html")
- fmt.Fprintf(w, `Failed to save credentials
%s
`, err.Error())
- return
- }
-
- updateConfigAfterLogin(session.ConfigPath, session.Provider, cred)
-
- // Redirect back to picoclaw-launcher UI
- w.Header().Set("Content-Type", "text/html")
- fmt.Fprintf(w, `
- Authentication successful!
- Redirecting back to Config Editor...
-
- `)
-}
-
-// fetchGoogleUserEmail retrieves the user's email from Google's userinfo endpoint.
-func fetchGoogleUserEmail(accessToken string) (string, error) {
- req, err := http.NewRequest("GET", "https://www.googleapis.com/oauth2/v2/userinfo", nil)
- if err != nil {
- return "", err
- }
- req.Header.Set("Authorization", "Bearer "+accessToken)
-
- client := &http.Client{Timeout: 10 * time.Second}
- resp, err := client.Do(req)
- if err != nil {
- return "", err
- }
- defer resp.Body.Close()
-
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return "", fmt.Errorf("reading userinfo response: %w", err)
- }
- if resp.StatusCode != http.StatusOK {
- return "", fmt.Errorf("userinfo request failed: %s", string(body))
- }
-
- var userInfo struct {
- Email string `json:"email"`
- }
- if err := json.Unmarshal(body, &userInfo); err != nil {
- return "", err
- }
- return userInfo.Email, nil
-}
diff --git a/cmd/picoclaw-launcher/internal/server/logbuffer_test.go b/cmd/picoclaw-launcher/internal/server/logbuffer_test.go
deleted file mode 100644
index dc525be16..000000000
--- a/cmd/picoclaw-launcher/internal/server/logbuffer_test.go
+++ /dev/null
@@ -1,116 +0,0 @@
-package server
-
-import (
- "fmt"
- "sync"
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestLogBuffer_Basic(t *testing.T) {
- buf := NewLogBuffer(5)
-
- // Empty buffer
- lines, total, runID := buf.LinesSince(0)
- assert.Nil(t, lines)
- assert.Equal(t, 0, total)
- assert.Equal(t, 0, runID)
-
- // Append some lines
- buf.Append("line1")
- buf.Append("line2")
- buf.Append("line3")
-
- lines, total, runID = buf.LinesSince(0)
- assert.Equal(t, []string{"line1", "line2", "line3"}, lines)
- assert.Equal(t, 3, total)
- assert.Equal(t, 0, runID)
-
- // Incremental read
- lines, total, _ = buf.LinesSince(2)
- assert.Equal(t, []string{"line3"}, lines)
- assert.Equal(t, 3, total)
-
- // No new lines
- lines, total, _ = buf.LinesSince(3)
- assert.Nil(t, lines)
- assert.Equal(t, 3, total)
-}
-
-func TestLogBuffer_Wrap(t *testing.T) {
- buf := NewLogBuffer(3)
-
- buf.Append("a")
- buf.Append("b")
- buf.Append("c")
- buf.Append("d") // evicts "a"
- buf.Append("e") // evicts "b"
-
- lines, total, _ := buf.LinesSince(0)
- assert.Equal(t, []string{"c", "d", "e"}, lines)
- assert.Equal(t, 5, total)
-
- // Incremental after wrap
- lines, total, _ = buf.LinesSince(3)
- assert.Equal(t, []string{"d", "e"}, lines)
- assert.Equal(t, 5, total)
-
- // Offset too old (before buffer start), get all buffered
- lines, total, _ = buf.LinesSince(1)
- assert.Equal(t, []string{"c", "d", "e"}, lines)
- assert.Equal(t, 5, total)
-}
-
-func TestLogBuffer_Reset(t *testing.T) {
- buf := NewLogBuffer(5)
-
- buf.Append("before")
- assert.Equal(t, 0, buf.RunID())
-
- buf.Reset()
- assert.Equal(t, 1, buf.RunID())
- assert.Equal(t, 0, buf.Total())
-
- lines, total, runID := buf.LinesSince(0)
- assert.Nil(t, lines)
- assert.Equal(t, 0, total)
- assert.Equal(t, 1, runID)
-
- buf.Append("after")
- lines, total, runID = buf.LinesSince(0)
- assert.Equal(t, []string{"after"}, lines)
- assert.Equal(t, 1, total)
- assert.Equal(t, 1, runID)
-}
-
-func TestLogBuffer_Concurrent(t *testing.T) {
- buf := NewLogBuffer(100)
- var wg sync.WaitGroup
-
- // 10 writers
- for i := range 10 {
- wg.Add(1)
- go func(id int) {
- defer wg.Done()
- for j := range 50 {
- buf.Append(fmt.Sprintf("writer-%d-line-%d", id, j))
- }
- }(i)
- }
-
- // 5 readers
- for range 5 {
- wg.Add(1)
- go func() {
- defer wg.Done()
- for range 100 {
- buf.LinesSince(0)
- }
- }()
- }
-
- wg.Wait()
-
- assert.Equal(t, 500, buf.Total())
-}
diff --git a/cmd/picoclaw-launcher/internal/server/process.go b/cmd/picoclaw-launcher/internal/server/process.go
deleted file mode 100644
index bc2129bf5..000000000
--- a/cmd/picoclaw-launcher/internal/server/process.go
+++ /dev/null
@@ -1,232 +0,0 @@
-package server
-
-import (
- "bufio"
- "encoding/json"
- "fmt"
- "io"
- "log"
- "net"
- "net/http"
- "os"
- "os/exec"
- "path/filepath"
- "runtime"
- "strconv"
- "time"
-
- "github.com/sipeed/picoclaw/pkg/config"
-)
-
-// gatewayLogs stores captured stdout/stderr from the gateway process launched by the launcher.
-var gatewayLogs = NewLogBuffer(200)
-
-// RegisterProcessAPI registers endpoints to start, stop and check status of the picoclaw gateway.
-func RegisterProcessAPI(mux *http.ServeMux, absPath string) {
- mux.HandleFunc("GET /api/process/status", func(w http.ResponseWriter, r *http.Request) {
- handleStatusGateway(w, r, absPath)
- })
- mux.HandleFunc("POST /api/process/start", handleStartGateway)
- mux.HandleFunc("POST /api/process/stop", handleStopGateway)
-}
-
-func handleStartGateway(w http.ResponseWriter, r *http.Request) {
- // Locate picoclaw executable:
- // 1. Try same directory as current executable
- // 2. Fallback to just "picoclaw" (relies on $PATH)
- execPath := "picoclaw"
-
- if exe, err := os.Executable(); err == nil {
- dir := filepath.Dir(exe)
- candidate := filepath.Join(dir, "picoclaw")
- if runtime.GOOS == "windows" {
- candidate += ".exe"
- }
-
- if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
- execPath = candidate
- }
- }
-
- cmd := exec.Command(execPath, "gateway")
-
- stdoutPipe, err := cmd.StdoutPipe()
- if err != nil {
- log.Printf("Failed to create stdout pipe: %v\n", err)
- http.Error(w, fmt.Sprintf("Failed to start gateway: %v", err), http.StatusInternalServerError)
- return
- }
-
- stderrPipe, err := cmd.StderrPipe()
- if err != nil {
- log.Printf("Failed to create stderr pipe: %v\n", err)
- http.Error(w, fmt.Sprintf("Failed to start gateway: %v", err), http.StatusInternalServerError)
- return
- }
-
- // Clear old logs and increment runID before starting
- gatewayLogs.Reset()
-
- if err := cmd.Start(); err != nil {
- log.Printf("Failed to start picoclaw gateway: %v\n", err)
- http.Error(w, fmt.Sprintf("Failed to start gateway: %v", err), http.StatusInternalServerError)
- return
- }
-
- // Read stdout and stderr into the log buffer
- go scanPipe(stdoutPipe, gatewayLogs)
- go scanPipe(stderrPipe, gatewayLogs)
-
- // Wait for the process to exit in the background to avoid zombies
- go func() {
- if err := cmd.Wait(); err != nil {
- log.Printf("Gateway process exited: %v\n", err)
- }
- }()
-
- log.Printf("Started picoclaw gateway (PID: %d) from %s\n", cmd.Process.Pid, execPath)
-
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(map[string]any{
- "status": "ok",
- "pid": cmd.Process.Pid,
- })
-}
-
-// scanPipe reads lines from r and appends them to buf. It returns when r reaches EOF.
-func scanPipe(r io.Reader, buf *LogBuffer) {
- scanner := bufio.NewScanner(r)
- scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) // up to 1MB per line
-
- for scanner.Scan() {
- buf.Append(scanner.Text())
- }
-}
-
-func handleStopGateway(w http.ResponseWriter, r *http.Request) {
- var err error
- if runtime.GOOS == "windows" {
- // Kill via taskkill finding picoclaw.exe (though it might kill this config tool if it's named picoclaw-launcher.exe...? No, /IM does exact match usually, but just to be safe let's stop exactly picoclaw.exe)
- // Alternatively, we use powershell to kill processes with commandline containing 'gateway'
- psCmd := `Get-WmiObject Win32_Process | Where-Object { $_.CommandLine -match 'picoclaw.*gateway' } | ForEach-Object { Stop-Process $_.ProcessId -Force }`
- err = exec.Command("powershell", "-Command", psCmd).Run()
- } else {
- // Linux/macOS
- err = exec.Command("pkill", "-f", "picoclaw gateway").Run()
- }
-
- if err != nil {
- log.Printf("Warning: Failed to stop gateway (perhaps not running?): %v\n", err)
- // We still return 200 OK because pkill returns an error if no process was found
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(map[string]any{
- "status": "ok", // or "not_found"
- "msg": "Stop command executed, but returned error (process might not be running).",
- "error": err.Error(),
- })
- return
- }
-
- log.Printf("Stopped picoclaw gateway processes.\n")
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(map[string]string{
- "status": "ok",
- })
-}
-
-func handleStatusGateway(w http.ResponseWriter, r *http.Request, absPath string) {
- cfg, cfgErr := config.LoadConfig(absPath)
- host := "127.0.0.1"
- port := 18790
- if cfgErr == nil && cfg != nil {
- if cfg.Gateway.Host != "" && cfg.Gateway.Host != "0.0.0.0" {
- host = cfg.Gateway.Host
- }
- if cfg.Gateway.Port != 0 {
- port = cfg.Gateway.Port
- }
- }
-
- url := fmt.Sprintf("http://%s/health", net.JoinHostPort(host, strconv.Itoa(port)))
- client := http.Client{Timeout: 2 * time.Second}
- resp, err := client.Get(url)
-
- // Build the response data map
- data := map[string]any{}
-
- if err != nil {
- data["process_status"] = "stopped"
- data["error"] = err.Error()
- } else {
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- data["process_status"] = "error"
- data["status_code"] = resp.StatusCode
- } else {
- var healthData map[string]any
- if decErr := json.NewDecoder(resp.Body).Decode(&healthData); decErr != nil {
- data["process_status"] = "error"
- data["error"] = "invalid response from gateway"
- } else {
- // Gateway is running and responded properly — merge health data
- for k, v := range healthData {
- data[k] = v
- }
- data["process_status"] = "running"
- }
- }
- }
-
- // Append log data from the buffer
- appendLogData(r, data)
-
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(data)
-}
-
-// appendLogData reads log_offset and log_run_id query params from the request and
-// populates the response data map with incremental log lines.
-func appendLogData(r *http.Request, data map[string]any) {
- clientOffset := 0
- clientRunID := -1
-
- if v := r.URL.Query().Get("log_offset"); v != "" {
- if n, err := strconv.Atoi(v); err == nil {
- clientOffset = n
- }
- }
-
- if v := r.URL.Query().Get("log_run_id"); v != "" {
- if n, err := strconv.Atoi(v); err == nil {
- clientRunID = n
- }
- }
-
- runID := gatewayLogs.RunID()
-
- // If runID is 0 (never reset = never launched from this launcher), report no source
- if runID == 0 {
- data["logs"] = []string{}
- data["log_total"] = 0
- data["log_run_id"] = 0
- data["log_source"] = "none"
- return
- }
-
- // If the client's runID doesn't match, send all buffered lines (gateway restarted)
- offset := clientOffset
- if clientRunID != runID {
- offset = 0
- }
-
- lines, total, runID := gatewayLogs.LinesSince(offset)
- if lines == nil {
- lines = []string{}
- }
-
- data["logs"] = lines
- data["log_total"] = total
- data["log_run_id"] = runID
- data["log_source"] = "launcher"
-}
diff --git a/cmd/picoclaw-launcher/internal/server/server.go b/cmd/picoclaw-launcher/internal/server/server.go
deleted file mode 100644
index 4fc68f04c..000000000
--- a/cmd/picoclaw-launcher/internal/server/server.go
+++ /dev/null
@@ -1,196 +0,0 @@
-package server
-
-import (
- "encoding/json"
- "fmt"
- "io"
- "log"
- "net/http"
- "time"
-
- "github.com/sipeed/picoclaw/pkg/auth"
- "github.com/sipeed/picoclaw/pkg/config"
-)
-
-const DefaultPort = "18800"
-
-// providerStatus represents the auth status of a single provider in API responses.
-type providerStatus struct {
- Provider string `json:"provider"`
- AuthMethod string `json:"auth_method"`
- Status string `json:"status"`
- AccountID string `json:"account_id,omitempty"`
- Email string `json:"email,omitempty"`
- ProjectID string `json:"project_id,omitempty"`
- ExpiresAt string `json:"expires_at,omitempty"`
-}
-
-// ── Route registration ───────────────────────────────────────────
-
-func RegisterConfigAPI(mux *http.ServeMux, absPath string) {
- // GET /api/config — read config
- mux.HandleFunc("GET /api/config", func(w http.ResponseWriter, r *http.Request) {
- cfg, err := config.LoadConfig(absPath)
- if err != nil {
- http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- resp := map[string]any{
- "config": cfg,
- "path": absPath,
- }
- enc := json.NewEncoder(w)
- enc.SetIndent("", " ")
- if err := enc.Encode(resp); err != nil {
- log.Printf("Failed to encode response: %v", err)
- }
- })
-
- // PUT /api/config — save config
- mux.HandleFunc("PUT /api/config", func(w http.ResponseWriter, r *http.Request) {
- body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
- if err != nil {
- http.Error(w, "Failed to read request body", http.StatusBadRequest)
- return
- }
- defer r.Body.Close()
-
- var cfg config.Config
- if err := json.Unmarshal(body, &cfg); err != nil {
- http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
- return
- }
-
- if err := config.SaveConfig(absPath, &cfg); err != nil {
- http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError)
- return
- }
-
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
- })
-}
-
-func RegisterAuthAPI(mux *http.ServeMux, absPath string) {
- // GET /api/auth/status — all authenticated providers + pending login state
- mux.HandleFunc("GET /api/auth/status", func(w http.ResponseWriter, r *http.Request) {
- store, err := auth.LoadStore()
- if err != nil {
- http.Error(w, fmt.Sprintf("Failed to load auth store: %v", err), http.StatusInternalServerError)
- return
- }
-
- result := []providerStatus{}
- for name, cred := range store.Credentials {
- status := "active"
- if cred.IsExpired() {
- status = "expired"
- } else if cred.NeedsRefresh() {
- status = "needs_refresh"
- }
- ps := providerStatus{
- Provider: name,
- AuthMethod: cred.AuthMethod,
- Status: status,
- AccountID: cred.AccountID,
- Email: cred.Email,
- ProjectID: cred.ProjectID,
- }
- if !cred.ExpiresAt.IsZero() {
- ps.ExpiresAt = cred.ExpiresAt.Format(time.RFC3339)
- }
- result = append(result, ps)
- }
-
- // Include pending device code state
- var pendingDevice map[string]any
- activeDeviceSessionMu.Lock()
- if activeDeviceSession != nil {
- activeDeviceSession.mu.Lock()
- pendingDevice = map[string]any{
- "provider": activeDeviceSession.Provider,
- "status": activeDeviceSession.Status,
- "device_url": activeDeviceSession.Info.VerifyURL,
- "user_code": activeDeviceSession.Info.UserCode,
- }
- if activeDeviceSession.Error != "" {
- pendingDevice["error"] = activeDeviceSession.Error
- }
- if activeDeviceSession.Done {
- activeDeviceSession.mu.Unlock()
- activeDeviceSession = nil
- } else {
- activeDeviceSession.mu.Unlock()
- }
- }
- activeDeviceSessionMu.Unlock()
-
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(map[string]any{
- "providers": result,
- "pending_device": pendingDevice,
- })
- })
-
- // POST /api/auth/login — initiate provider login
- mux.HandleFunc("POST /api/auth/login", func(w http.ResponseWriter, r *http.Request) {
- var req struct {
- Provider string `json:"provider"`
- Token string `json:"token,omitempty"`
- }
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, "Invalid request body", http.StatusBadRequest)
- return
- }
-
- switch req.Provider {
- case "openai":
- handleOpenAILogin(w, absPath)
- case "anthropic":
- handleAnthropicLogin(w, req.Token, absPath)
- case "google-antigravity", "antigravity":
- handleGoogleAntigravityLogin(w, r, absPath)
- default:
- http.Error(
- w,
- fmt.Sprintf(
- "Unsupported provider: %s (supported: openai, anthropic, google-antigravity)",
- req.Provider,
- ),
- http.StatusBadRequest,
- )
- }
- })
-
- // POST /api/auth/logout — logout a provider
- mux.HandleFunc("POST /api/auth/logout", func(w http.ResponseWriter, r *http.Request) {
- var req struct {
- Provider string `json:"provider"`
- }
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, "Invalid request body", http.StatusBadRequest)
- return
- }
-
- if req.Provider == "" {
- if err := auth.DeleteAllCredentials(); err != nil {
- http.Error(w, fmt.Sprintf("Failed to logout: %v", err), http.StatusInternalServerError)
- return
- }
- clearAllAuthMethodsInConfig(absPath)
- } else {
- if err := auth.DeleteCredential(req.Provider); err != nil {
- http.Error(w, fmt.Sprintf("Failed to logout: %v", err), http.StatusInternalServerError)
- return
- }
- clearAuthMethodInConfig(absPath, req.Provider)
- }
-
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
- })
-
- // GET /auth/callback — OAuth browser callback for Google Antigravity
- mux.HandleFunc("GET /auth/callback", handleOAuthCallback)
-}
diff --git a/cmd/picoclaw-launcher/internal/server/server_test.go b/cmd/picoclaw-launcher/internal/server/server_test.go
deleted file mode 100644
index c87e93d8c..000000000
--- a/cmd/picoclaw-launcher/internal/server/server_test.go
+++ /dev/null
@@ -1,247 +0,0 @@
-package server
-
-import (
- "encoding/json"
- "net/http"
- "net/http/httptest"
- "os"
- "path/filepath"
- "strings"
- "testing"
-
- "github.com/sipeed/picoclaw/pkg/config"
-)
-
-// ── Config API tests ─────────────────────────────────────────────
-
-func setupConfigMux(t *testing.T, cfg *config.Config) (*http.ServeMux, string) {
- t.Helper()
- dir := t.TempDir()
- path := filepath.Join(dir, "config.json")
- data, err := json.MarshalIndent(cfg, "", " ")
- if err != nil {
- t.Fatalf("marshal config: %v", err)
- }
- if err := os.WriteFile(path, data, 0o600); err != nil {
- t.Fatalf("write config: %v", err)
- }
-
- mux := http.NewServeMux()
- RegisterConfigAPI(mux, path)
- RegisterAuthAPI(mux, path)
- return mux, path
-}
-
-func TestGetConfig(t *testing.T) {
- cfg := &config.Config{
- ModelList: []config.ModelConfig{
- {ModelName: "gpt-4o", Model: "openai/gpt-4o"},
- },
- }
- mux, path := setupConfigMux(t, cfg)
-
- req := httptest.NewRequest("GET", "/api/config", nil)
- w := httptest.NewRecorder()
- mux.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Fatalf("GET /api/config: expected 200, got %d: %s", w.Code, w.Body.String())
- }
-
- var resp struct {
- Config config.Config `json:"config"`
- Path string `json:"path"`
- }
- if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
- t.Fatalf("decode response: %v", err)
- }
-
- if resp.Path != path {
- t.Errorf("expected path %q, got %q", path, resp.Path)
- }
- if len(resp.Config.ModelList) != 1 {
- t.Errorf("expected 1 model, got %d", len(resp.Config.ModelList))
- }
-}
-
-func TestGetConfig_MissingFile_ReturnsDefault(t *testing.T) {
- mux := http.NewServeMux()
- RegisterConfigAPI(mux, "/tmp/nonexistent-picoclaw-launcher-test/config.json")
-
- req := httptest.NewRequest("GET", "/api/config", nil)
- w := httptest.NewRecorder()
- mux.ServeHTTP(w, req)
-
- // LoadConfig returns a default empty config when file is missing
- if w.Code != http.StatusOK {
- t.Errorf("expected 200 for missing file (default config), got %d", w.Code)
- }
-}
-
-func TestPutConfig(t *testing.T) {
- cfg := &config.Config{}
- mux, path := setupConfigMux(t, cfg)
-
- newCfg := config.Config{
- ModelList: []config.ModelConfig{
- {ModelName: "claude", Model: "anthropic/claude-sonnet-4.6", AuthMethod: "token"},
- },
- }
- body, _ := json.Marshal(newCfg)
-
- req := httptest.NewRequest("PUT", "/api/config", strings.NewReader(string(body)))
- req.Header.Set("Content-Type", "application/json")
- w := httptest.NewRecorder()
- mux.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Fatalf("PUT /api/config: expected 200, got %d: %s", w.Code, w.Body.String())
- }
-
- saved, err := config.LoadConfig(path)
- if err != nil {
- t.Fatalf("load saved config: %v", err)
- }
- if len(saved.ModelList) != 1 {
- t.Fatalf("expected 1 model saved, got %d", len(saved.ModelList))
- }
- if saved.ModelList[0].Model != "anthropic/claude-sonnet-4.6" {
- t.Errorf("expected model anthropic/claude-sonnet-4.6, got %q", saved.ModelList[0].Model)
- }
-}
-
-func TestPutConfig_InvalidJSON(t *testing.T) {
- cfg := &config.Config{}
- mux, _ := setupConfigMux(t, cfg)
-
- req := httptest.NewRequest("PUT", "/api/config", strings.NewReader("{invalid"))
- req.Header.Set("Content-Type", "application/json")
- w := httptest.NewRecorder()
- mux.ServeHTTP(w, req)
-
- if w.Code != http.StatusBadRequest {
- t.Errorf("expected 400 for invalid JSON, got %d", w.Code)
- }
-}
-
-// ── Auth API tests ───────────────────────────────────────────────
-
-func TestAuthStatus(t *testing.T) {
- cfg := &config.Config{}
- mux, _ := setupConfigMux(t, cfg)
-
- req := httptest.NewRequest("GET", "/api/auth/status", nil)
- w := httptest.NewRecorder()
- mux.ServeHTTP(w, req)
-
- if w.Code != http.StatusOK {
- t.Fatalf("GET /api/auth/status: expected 200, got %d: %s", w.Code, w.Body.String())
- }
-
- var resp struct {
- Providers []providerStatus `json:"providers"`
- PendingDevice map[string]any `json:"pending_device"`
- }
- if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
- t.Fatalf("decode response: %v", err)
- }
-
- // providers should be a non-nil list (could be empty)
- if resp.Providers == nil {
- t.Error("providers should not be nil")
- }
-}
-
-func TestAuthLogin_UnsupportedProvider(t *testing.T) {
- cfg := &config.Config{}
- mux, _ := setupConfigMux(t, cfg)
-
- body := `{"provider": "unsupported"}`
- req := httptest.NewRequest("POST", "/api/auth/login", strings.NewReader(body))
- req.Header.Set("Content-Type", "application/json")
- w := httptest.NewRecorder()
- mux.ServeHTTP(w, req)
-
- if w.Code != http.StatusBadRequest {
- t.Errorf("expected 400 for unsupported provider, got %d", w.Code)
- }
-}
-
-func TestAuthLogin_AnthropicNoToken(t *testing.T) {
- cfg := &config.Config{}
- mux, _ := setupConfigMux(t, cfg)
-
- body := `{"provider": "anthropic"}`
- req := httptest.NewRequest("POST", "/api/auth/login", strings.NewReader(body))
- req.Header.Set("Content-Type", "application/json")
- w := httptest.NewRecorder()
- mux.ServeHTTP(w, req)
-
- if w.Code != http.StatusBadRequest {
- t.Errorf("expected 400 for anthropic without token, got %d", w.Code)
- }
-}
-
-func TestAuthLogin_InvalidBody(t *testing.T) {
- cfg := &config.Config{}
- mux, _ := setupConfigMux(t, cfg)
-
- req := httptest.NewRequest("POST", "/api/auth/login", strings.NewReader("{bad"))
- req.Header.Set("Content-Type", "application/json")
- w := httptest.NewRecorder()
- mux.ServeHTTP(w, req)
-
- if w.Code != http.StatusBadRequest {
- t.Errorf("expected 400 for invalid JSON body, got %d", w.Code)
- }
-}
-
-func TestAuthLogout_InvalidBody(t *testing.T) {
- cfg := &config.Config{}
- mux, _ := setupConfigMux(t, cfg)
-
- req := httptest.NewRequest("POST", "/api/auth/logout", strings.NewReader("{bad"))
- req.Header.Set("Content-Type", "application/json")
- w := httptest.NewRecorder()
- mux.ServeHTTP(w, req)
-
- if w.Code != http.StatusBadRequest {
- t.Errorf("expected 400 for invalid body, got %d", w.Code)
- }
-}
-
-func TestOAuthCallback_InvalidState(t *testing.T) {
- cfg := &config.Config{}
- mux, _ := setupConfigMux(t, cfg)
-
- req := httptest.NewRequest("GET", "/auth/callback?state=invalid&code=test", nil)
- w := httptest.NewRecorder()
- mux.ServeHTTP(w, req)
-
- if w.Code != http.StatusBadRequest {
- t.Errorf("expected 400 for invalid state, got %d", w.Code)
- }
-}
-
-// ── Utility tests ────────────────────────────────────────────────
-
-func TestDefaultConfigPath(t *testing.T) {
- path := DefaultConfigPath()
- if path == "" {
- t.Error("defaultConfigPath should not return empty")
- }
- if !strings.HasSuffix(path, filepath.Join(".picoclaw", "config.json")) {
- t.Errorf("expected path ending with .picoclaw/config.json, got %q", path)
- }
-}
-
-func TestGetLocalIP(t *testing.T) {
- // Just ensure it doesn't panic; IP may or may not be available
- ip := GetLocalIP()
- if ip != "" {
- // If returned, should look like an IP
- if !strings.Contains(ip, ".") {
- t.Errorf("getLocalIP returned non-IPv4 looking string: %q", ip)
- }
- }
-}
diff --git a/cmd/picoclaw-launcher/internal/server/utils.go b/cmd/picoclaw-launcher/internal/server/utils.go
deleted file mode 100644
index a46adbece..000000000
--- a/cmd/picoclaw-launcher/internal/server/utils.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package server
-
-import (
- "net"
- "os"
- "path/filepath"
-)
-
-func DefaultConfigPath() string {
- home, err := os.UserHomeDir()
- if err != nil {
- return "config.json"
- }
- return filepath.Join(home, ".picoclaw", "config.json")
-}
-
-func GetLocalIP() string {
- addrs, err := net.InterfaceAddrs()
- if err != nil {
- return ""
- }
- for _, a := range addrs {
- if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil {
- return ipnet.IP.String()
- }
- }
- return ""
-}
diff --git a/cmd/picoclaw-launcher/internal/ui/index.html b/cmd/picoclaw-launcher/internal/ui/index.html
deleted file mode 100644
index e77ef4fea..000000000
--- a/cmd/picoclaw-launcher/internal/ui/index.html
+++ /dev/null
@@ -1,2009 +0,0 @@
-
-
-
-
-
-
-
- PicoClaw Config
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Models
-
Manage LLM model configurations. Models without an API key are grayed out. Only available models can be set as primary.
-
-
-
-
-
-
-
-
Provider Authentication
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Gateway Logs
-
Real-time output from the gateway process.
-
-
-
No logs available. Start the gateway to see output here.
-
-
-
-
-
-
Raw JSON
-
Directly edit the configuration file.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/cmd/picoclaw-launcher/main.go b/cmd/picoclaw-launcher/main.go
deleted file mode 100644
index 3323c31a8..000000000
--- a/cmd/picoclaw-launcher/main.go
+++ /dev/null
@@ -1,127 +0,0 @@
-// PicoClaw Launcher - Standalone HTTP service
-//
-// Provides a web-based JSON editor for picoclaw config files,
-// with OAuth provider authentication support.
-//
-// Usage:
-//
-// go build -o picoclaw-launcher ./cmd/picoclaw-launcher/
-// ./picoclaw-launcher [config.json]
-// ./picoclaw-launcher -public config.json
-
-package main
-
-import (
- "embed"
- "flag"
- "fmt"
- "io/fs"
- "log"
- "net/http"
- "os"
- "os/exec"
- "path/filepath"
- "runtime"
- "time"
-
- "github.com/sipeed/picoclaw/cmd/picoclaw-launcher/internal/server"
-)
-
-//go:embed internal/ui/index.html
-var staticFiles embed.FS
-
-func main() {
- public := flag.Bool("public", false, "Listen on all interfaces (0.0.0.0) instead of localhost only")
- flag.Usage = func() {
- fmt.Fprintf(os.Stderr, "PicoClaw Launcher - A web-based configuration editor\n\n")
- fmt.Fprintf(os.Stderr, "Usage: %s [options] [config.json]\n\n", os.Args[0])
- fmt.Fprintf(os.Stderr, "Arguments:\n")
- fmt.Fprintf(os.Stderr, " config.json Path to the configuration file (default: ~/.picoclaw/config.json)\n\n")
- fmt.Fprintf(os.Stderr, "Options:\n")
- flag.PrintDefaults()
- fmt.Fprintf(os.Stderr, "\nExamples:\n")
- fmt.Fprintf(os.Stderr, " %s Use default config path\n", os.Args[0])
- fmt.Fprintf(os.Stderr, " %s ./config.json Specify a config file\n", os.Args[0])
- fmt.Fprintf(
- os.Stderr,
- " %s -public ./config.json Allow access from other devices on the network\n",
- os.Args[0],
- )
- }
- flag.Parse()
-
- configPath := server.DefaultConfigPath()
- if flag.NArg() > 0 {
- configPath = flag.Arg(0)
- }
-
- absPath, err := filepath.Abs(configPath)
- if err != nil {
- log.Fatalf("Failed to resolve config path: %v", err)
- }
-
- var addr string
- if *public {
- addr = "0.0.0.0:" + server.DefaultPort
- } else {
- addr = "127.0.0.1:" + server.DefaultPort
- }
-
- mux := http.NewServeMux()
- server.RegisterConfigAPI(mux, absPath)
- server.RegisterAuthAPI(mux, absPath)
- server.RegisterProcessAPI(mux, absPath)
-
- staticFS, err := fs.Sub(staticFiles, "internal/ui")
- if err != nil {
- log.Fatalf("Failed to create sub filesystem: %v", err)
- }
- mux.Handle("/", http.FileServer(http.FS(staticFS)))
-
- // Print startup banner
- fmt.Println("=============================================")
- fmt.Println(" PicoClaw Launcher")
- fmt.Println("=============================================")
- fmt.Printf(" Config file : %s\n", absPath)
- fmt.Printf(" Listen addr : %s\n\n", addr)
- fmt.Println(" Open the following URL in your browser")
- fmt.Println(" to view and edit the configuration:")
- fmt.Println()
- fmt.Printf(" >> http://localhost:%s <<\n", server.DefaultPort)
- if *public {
- if ip := server.GetLocalIP(); ip != "" {
- fmt.Printf(" >> http://%s:%s <<\n", ip, server.DefaultPort)
- }
- }
- fmt.Println()
- // fmt.Println("=============================================")
-
- go func() {
- // Wait briefly to ensure the server is ready before opening the browser
- time.Sleep(500 * time.Millisecond)
- url := "http://localhost:" + server.DefaultPort
- if err := openBrowser(url); err != nil {
- log.Printf("Warning: Failed to auto-open browser: %v\n", err)
- }
- }()
-
- if err := http.ListenAndServe(addr, mux); err != nil {
- log.Fatalf("Server failed: %v", err)
- }
-}
-
-// openBrowser automatically opens the given URL in the default browser.
-func openBrowser(url string) error {
- var err error
- switch runtime.GOOS {
- case "linux":
- err = exec.Command("xdg-open", url).Start()
- case "windows":
- err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
- case "darwin":
- err = exec.Command("open", url).Start()
- default:
- err = fmt.Errorf("unsupported platform")
- }
- return err
-}
diff --git a/web/Makefile b/web/Makefile
new file mode 100644
index 000000000..559005956
--- /dev/null
+++ b/web/Makefile
@@ -0,0 +1,38 @@
+.PHONY: dev dev-frontend dev-backend build test lint clean
+
+# Run both frontend and backend dev servers
+dev:
+ @if [ ! -f backend/picoclaw-web ] || [ ! -d backend/dist ]; then \
+ echo "Build artifacts not found, building..."; \
+ $(MAKE) build; \
+ fi
+ @echo "Starting backend and frontend dev servers..."
+ @$(MAKE) dev-backend & $(MAKE) dev-frontend
+
+# Start frontend dev server (Vite, with proxy to backend)
+dev-frontend:
+ cd frontend && pnpm dev
+
+# Start backend dev server
+dev-backend:
+ cd backend && go run .
+
+# Build frontend and embed into Go binary
+build:
+ cd frontend && pnpm build:backend
+ cd backend && go build -o picoclaw-web .
+
+# Run all tests
+test:
+ cd backend && go test ./...
+ cd frontend && pnpm lint
+
+# Lint and format
+lint:
+ cd backend && go vet ./...
+ cd frontend && pnpm check
+
+# Clean build artifacts
+clean:
+ rm -rf frontend/dist backend/dist backend/picoclaw-web
+ mkdir -p backend/dist && touch backend/dist/.gitkeep
diff --git a/web/README.md b/web/README.md
new file mode 100644
index 000000000..6ec247bae
--- /dev/null
+++ b/web/README.md
@@ -0,0 +1,51 @@
+# Picoclaw Web
+
+This directory contains the standalone web service for `picoclaw`.
+It provides a complete unified web interface, acting as a dashboard, configuration center, and interactive console (channel client) for the core `picoclaw` engine.
+
+## Architecture
+
+The service is structured as a monorepo containing both the backend and frontend code to ensure high cohesion and simplify deployment.
+
+* **`backend/`**: The Go-based web server. It provides RESTful APIs, manages WebSocket connections for chat, and handles the lifecycle of the `picoclaw` process. It eventually embeds the compiled frontend assets into a single executable.
+* **`frontend/`**: The Vite + React + TanStack Router single-page application (SPA). It provides the interactive user interface.
+
+## Getting Started
+
+### Prerequisites
+
+* Go 1.25+
+* Node.js 20+ with pnpm
+
+### Development
+
+Run both the frontend dev server and the Go backend simultaneously:
+
+```bash
+make dev
+```
+
+Or run them separately:
+
+```bash
+make dev-frontend # Vite dev server
+make dev-backend # Go backend
+```
+
+### Build
+
+Build the frontend and embed it into a single Go binary:
+
+```bash
+make build
+```
+
+The output binary is `backend/picoclaw-web`.
+
+### Other Commands
+
+```bash
+make test # Run backend tests and frontend lint
+make lint # Run go vet and prettier/eslint
+make clean # Remove all build artifacts
+```
diff --git a/web/backend/.gitignore b/web/backend/.gitignore
new file mode 100644
index 000000000..509042171
--- /dev/null
+++ b/web/backend/.gitignore
@@ -0,0 +1,19 @@
+# Go build output
+*.exe
+*.dll
+*.so
+*.dylib
+*.test
+*.out
+picoclaw-web
+
+# Frontend build artifacts (embedded by Go)
+dist/*
+!dist/.gitkeep
+
+# OS
+.DS_Store
+
+# Editors
+.vscode/
+.idea/
\ No newline at end of file
diff --git a/web/backend/api/channels.go b/web/backend/api/channels.go
new file mode 100644
index 000000000..507882823
--- /dev/null
+++ b/web/backend/api/channels.go
@@ -0,0 +1,47 @@
+package api
+
+import (
+ "encoding/json"
+ "net/http"
+)
+
+type channelCatalogItem struct {
+ Name string `json:"name"`
+ ConfigKey string `json:"config_key"`
+ Variant string `json:"variant,omitempty"`
+}
+
+var channelCatalog = []channelCatalogItem{
+ {Name: "telegram", ConfigKey: "telegram"},
+ {Name: "discord", ConfigKey: "discord"},
+ {Name: "slack", ConfigKey: "slack"},
+ {Name: "feishu", ConfigKey: "feishu"},
+ {Name: "dingtalk", ConfigKey: "dingtalk"},
+ {Name: "line", ConfigKey: "line"},
+ {Name: "qq", ConfigKey: "qq"},
+ {Name: "onebot", ConfigKey: "onebot"},
+ {Name: "wecom", ConfigKey: "wecom"},
+ {Name: "wecom_app", ConfigKey: "wecom_app"},
+ {Name: "wecom_aibot", ConfigKey: "wecom_aibot"},
+ {Name: "whatsapp", ConfigKey: "whatsapp", Variant: "bridge"},
+ {Name: "whatsapp_native", ConfigKey: "whatsapp", Variant: "native"},
+ {Name: "pico", ConfigKey: "pico"},
+ {Name: "maixcam", ConfigKey: "maixcam"},
+ {Name: "matrix", ConfigKey: "matrix"},
+ {Name: "irc", ConfigKey: "irc"},
+}
+
+// registerChannelRoutes binds read-only channel catalog endpoints to the ServeMux.
+func (h *Handler) registerChannelRoutes(mux *http.ServeMux) {
+ mux.HandleFunc("GET /api/channels/catalog", h.handleListChannelCatalog)
+}
+
+// handleListChannelCatalog returns the channels supported by backend.
+//
+// GET /api/channels/catalog
+func (h *Handler) handleListChannelCatalog(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]any{
+ "channels": channelCatalog,
+ })
+}
diff --git a/web/backend/api/config.go b/web/backend/api/config.go
new file mode 100644
index 000000000..f160b42b6
--- /dev/null
+++ b/web/backend/api/config.go
@@ -0,0 +1,221 @@
+package api
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+
+ "github.com/sipeed/picoclaw/pkg/config"
+)
+
+// registerConfigRoutes binds configuration management endpoints to the ServeMux.
+func (h *Handler) registerConfigRoutes(mux *http.ServeMux) {
+ mux.HandleFunc("GET /api/config", h.handleGetConfig)
+ mux.HandleFunc("PUT /api/config", h.handleUpdateConfig)
+ mux.HandleFunc("PATCH /api/config", h.handlePatchConfig)
+}
+
+// loadFilteredConfig loads the configuration and filters out default placeholder credentials
+// (like API limits/keys) if the configuration file has not been created yet by the user.
+func (h *Handler) loadFilteredConfig() (*config.Config, error) {
+ cfg, err := config.LoadConfig(h.configPath)
+ if err != nil {
+ return nil, err
+ }
+
+ configExists := false
+ if h.configPath != "" {
+ if _, err := os.Stat(h.configPath); err == nil {
+ configExists = true
+ }
+ }
+
+ if !configExists {
+ for i := range cfg.ModelList {
+ cfg.ModelList[i].APIKey = ""
+ cfg.ModelList[i].AuthMethod = ""
+ }
+ }
+
+ return cfg, nil
+}
+
+// handleGetConfig returns the complete system configuration.
+//
+// GET /api/config
+func (h *Handler) handleGetConfig(w http.ResponseWriter, r *http.Request) {
+ cfg, err := h.loadFilteredConfig()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ if err := json.NewEncoder(w).Encode(cfg); err != nil {
+ http.Error(w, "Failed to encode response", http.StatusInternalServerError)
+ }
+}
+
+// handleUpdateConfig updates the complete system configuration.
+//
+// PUT /api/config
+func (h *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
+ body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
+ if err != nil {
+ http.Error(w, "Failed to read request body", http.StatusBadRequest)
+ return
+ }
+ defer r.Body.Close()
+
+ var cfg config.Config
+ if err := json.Unmarshal(body, &cfg); err != nil {
+ http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
+ return
+ }
+
+ if errs := validateConfig(&cfg); len(errs) > 0 {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusBadRequest)
+ json.NewEncoder(w).Encode(map[string]any{
+ "status": "validation_error",
+ "errors": errs,
+ })
+ return
+ }
+
+ if err := config.SaveConfig(h.configPath, &cfg); err != nil {
+ http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
+}
+
+// handlePatchConfig partially updates the system configuration using JSON Merge Patch (RFC 7396).
+// Only the fields present in the request body will be updated; all other fields remain unchanged.
+//
+// PATCH /api/config
+func (h *Handler) handlePatchConfig(w http.ResponseWriter, r *http.Request) {
+ patchBody, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
+ if err != nil {
+ http.Error(w, "Failed to read request body", http.StatusBadRequest)
+ return
+ }
+ defer r.Body.Close()
+
+ // Validate the patch is valid JSON
+ var patch map[string]any
+ if err = json.Unmarshal(patchBody, &patch); err != nil {
+ http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
+ return
+ }
+
+ // Load existing config and marshal to a map for merging
+ cfg, err := config.LoadConfig(h.configPath)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ existing, err := json.Marshal(cfg)
+ if err != nil {
+ http.Error(w, "Failed to serialize current config", http.StatusInternalServerError)
+ return
+ }
+
+ var base map[string]any
+ if err = json.Unmarshal(existing, &base); err != nil {
+ http.Error(w, "Failed to parse current config", http.StatusInternalServerError)
+ return
+ }
+
+ // Recursively merge patch into base
+ mergeMap(base, patch)
+
+ // Convert merged map back to Config struct
+ merged, err := json.Marshal(base)
+ if err != nil {
+ http.Error(w, "Failed to serialize merged config", http.StatusInternalServerError)
+ return
+ }
+
+ var newCfg config.Config
+ if err := json.Unmarshal(merged, &newCfg); err != nil {
+ http.Error(w, fmt.Sprintf("Merged config is invalid: %v", err), http.StatusBadRequest)
+ return
+ }
+
+ if errs := validateConfig(&newCfg); len(errs) > 0 {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusBadRequest)
+ json.NewEncoder(w).Encode(map[string]any{
+ "status": "validation_error",
+ "errors": errs,
+ })
+ return
+ }
+
+ if err := config.SaveConfig(h.configPath, &newCfg); err != nil {
+ http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
+}
+
+// validateConfig checks the config for common errors before saving.
+// Returns a list of human-readable error strings; empty means valid.
+func validateConfig(cfg *config.Config) []string {
+ var errs []string
+
+ // Validate model_list entries
+ if err := cfg.ValidateModelList(); err != nil {
+ errs = append(errs, err.Error())
+ }
+
+ // Gateway port range
+ if cfg.Gateway.Port != 0 && (cfg.Gateway.Port < 1 || cfg.Gateway.Port > 65535) {
+ errs = append(errs, fmt.Sprintf("gateway.port %d is out of valid range (1-65535)", cfg.Gateway.Port))
+ }
+
+ // Pico channel: token required when enabled
+ if cfg.Channels.Pico.Enabled && cfg.Channels.Pico.Token == "" {
+ errs = append(errs, "channels.pico.token is required when pico channel is enabled")
+ }
+
+ // Telegram: token required when enabled
+ if cfg.Channels.Telegram.Enabled && cfg.Channels.Telegram.Token == "" {
+ errs = append(errs, "channels.telegram.token is required when telegram channel is enabled")
+ }
+
+ // Discord: token required when enabled
+ if cfg.Channels.Discord.Enabled && cfg.Channels.Discord.Token == "" {
+ errs = append(errs, "channels.discord.token is required when discord channel is enabled")
+ }
+
+ return errs
+}
+
+// mergeMap recursively merges src into dst (JSON Merge Patch semantics).
+// - If a key in src has a null value, it is deleted from dst.
+// - If both dst and src have a nested object for the same key, merge recursively.
+// - Otherwise the value from src overwrites dst.
+func mergeMap(dst, src map[string]any) {
+ for key, srcVal := range src {
+ if srcVal == nil {
+ delete(dst, key)
+ continue
+ }
+ srcMap, srcIsMap := srcVal.(map[string]any)
+ dstMap, dstIsMap := dst[key].(map[string]any)
+ if srcIsMap && dstIsMap {
+ mergeMap(dstMap, srcMap)
+ } else {
+ dst[key] = srcVal
+ }
+ }
+}
diff --git a/web/backend/api/events.go b/web/backend/api/events.go
new file mode 100644
index 000000000..0a8d4a9bb
--- /dev/null
+++ b/web/backend/api/events.go
@@ -0,0 +1,62 @@
+package api
+
+import (
+ "encoding/json"
+ "sync"
+)
+
+// GatewayEvent represents a state change event for the gateway process.
+type GatewayEvent struct {
+ Status string `json:"gateway_status"` // "running", "starting", "stopped", "error"
+ PID int `json:"pid,omitempty"`
+}
+
+// EventBroadcaster manages SSE client subscriptions and broadcasts events.
+type EventBroadcaster struct {
+ mu sync.RWMutex
+ clients map[chan string]struct{}
+}
+
+// NewEventBroadcaster creates a new broadcaster.
+func NewEventBroadcaster() *EventBroadcaster {
+ return &EventBroadcaster{
+ clients: make(map[chan string]struct{}),
+ }
+}
+
+// Subscribe adds a new listener channel and returns it.
+// The caller must call Unsubscribe when done.
+func (b *EventBroadcaster) Subscribe() chan string {
+ ch := make(chan string, 8)
+ b.mu.Lock()
+ b.clients[ch] = struct{}{}
+ b.mu.Unlock()
+ return ch
+}
+
+// Unsubscribe removes a listener channel and closes it.
+func (b *EventBroadcaster) Unsubscribe(ch chan string) {
+ b.mu.Lock()
+ delete(b.clients, ch)
+ b.mu.Unlock()
+ close(ch)
+}
+
+// Broadcast sends a GatewayEvent to all connected SSE clients.
+func (b *EventBroadcaster) Broadcast(event GatewayEvent) {
+ data, err := json.Marshal(event)
+ if err != nil {
+ return
+ }
+
+ b.mu.RLock()
+ defer b.mu.RUnlock()
+
+ for ch := range b.clients {
+ // Non-blocking send; drop event if client is slow
+ select {
+ case ch <- string(data):
+ default:
+ }
+ }
+}
diff --git a/web/backend/api/gateway.go b/web/backend/api/gateway.go
new file mode 100644
index 000000000..1aea1c801
--- /dev/null
+++ b/web/backend/api/gateway.go
@@ -0,0 +1,555 @@
+package api
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net"
+ "net/http"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "strings"
+ "sync"
+ "syscall"
+ "time"
+
+ "github.com/sipeed/picoclaw/pkg/config"
+)
+
+// gateway holds the state for the managed gateway process.
+var gateway = struct {
+ mu sync.Mutex
+ cmd *exec.Cmd
+ logs *LogBuffer
+ events *EventBroadcaster
+}{
+ logs: NewLogBuffer(200),
+ events: NewEventBroadcaster(),
+}
+
+// registerGatewayRoutes binds gateway lifecycle endpoints to the ServeMux.
+func (h *Handler) registerGatewayRoutes(mux *http.ServeMux) {
+ mux.HandleFunc("GET /api/gateway/status", h.handleGatewayStatus)
+ mux.HandleFunc("GET /api/gateway/events", h.handleGatewayEvents)
+ mux.HandleFunc("POST /api/gateway/start", h.handleGatewayStart)
+ mux.HandleFunc("POST /api/gateway/stop", h.handleGatewayStop)
+ mux.HandleFunc("POST /api/gateway/restart", h.handleGatewayRestart)
+}
+
+// TryAutoStartGateway checks whether gateway start preconditions are met and
+// starts it when possible. Intended to be called by the backend at startup.
+func (h *Handler) TryAutoStartGateway() {
+ gateway.mu.Lock()
+ defer gateway.mu.Unlock()
+
+ if isGatewayProcessAliveLocked() {
+ return
+ }
+ if gateway.cmd != nil && gateway.cmd.Process != nil {
+ gateway.cmd = nil
+ }
+
+ ready, reason, err := h.gatewayStartReady()
+ if err != nil {
+ log.Printf("Skip auto-starting gateway: %v", err)
+ return
+ }
+ if !ready {
+ log.Printf("Skip auto-starting gateway: %s", reason)
+ return
+ }
+
+ pid, err := h.startGatewayLocked()
+ if err != nil {
+ log.Printf("Failed to auto-start gateway: %v", err)
+ return
+ }
+ log.Printf("Gateway auto-started (PID: %d)", pid)
+}
+
+// gatewayStartReady validates whether current config can start the gateway.
+func (h *Handler) gatewayStartReady() (bool, string, error) {
+ cfg, err := config.LoadConfig(h.configPath)
+ if err != nil {
+ return false, "", fmt.Errorf("failed to load config: %w", err)
+ }
+
+ modelName := strings.TrimSpace(cfg.Agents.Defaults.GetModelName())
+ if modelName == "" {
+ return false, "no default model configured", nil
+ }
+
+ modelCfg := lookupModelConfig(cfg, modelName)
+ if modelCfg == nil {
+ return false, fmt.Sprintf("default model %q is invalid", modelName), nil
+ }
+
+ hasCredential := strings.TrimSpace(modelCfg.APIKey) != "" ||
+ strings.TrimSpace(modelCfg.AuthMethod) != ""
+ if !hasCredential {
+ return false, fmt.Sprintf("default model %q has no credentials configured", modelName), nil
+ }
+
+ return true, "", nil
+}
+
+func lookupModelConfig(cfg *config.Config, modelName string) *config.ModelConfig {
+ modelCfg, err := cfg.GetModelConfig(modelName)
+ if err != nil {
+ return nil
+ }
+ return modelCfg
+}
+
+func isGatewayProcessAliveLocked() bool {
+ return isCmdProcessAliveLocked(gateway.cmd)
+}
+
+func isCmdProcessAliveLocked(cmd *exec.Cmd) bool {
+ if cmd == nil || cmd.Process == nil {
+ return false
+ }
+
+ // Wait() sets ProcessState when the process exits; use it when available.
+ if cmd.ProcessState != nil && cmd.ProcessState.Exited() {
+ return false
+ }
+
+ // Windows does not support Signal(0) probing. If we still own cmd and it
+ // has not reported exit, treat it as alive.
+ if runtime.GOOS == "windows" {
+ return true
+ }
+
+ return cmd.Process.Signal(syscall.Signal(0)) == nil
+}
+
+func (h *Handler) startGatewayLocked() (int, error) {
+ // Locate the picoclaw executable
+ execPath := findPicoclawBinary()
+
+ cmd := exec.Command(execPath, "gateway")
+
+ stdoutPipe, err := cmd.StdoutPipe()
+ if err != nil {
+ return 0, fmt.Errorf("failed to create stdout pipe: %w", err)
+ }
+
+ stderrPipe, err := cmd.StderrPipe()
+ if err != nil {
+ return 0, fmt.Errorf("failed to create stderr pipe: %w", err)
+ }
+
+ // Clear old logs for this new run
+ gateway.logs.Reset()
+
+ // Ensure Pico Channel is configured before starting gateway
+ if _, err := h.ensurePicoChannel(); err != nil {
+ log.Printf("Warning: failed to ensure pico channel: %v", err)
+ // Non-fatal: gateway can still start without pico channel
+ }
+
+ if err := cmd.Start(); err != nil {
+ return 0, fmt.Errorf("failed to start gateway: %w", err)
+ }
+
+ gateway.cmd = cmd
+ pid := cmd.Process.Pid
+ log.Printf("Started picoclaw gateway (PID: %d) from %s", pid, execPath)
+
+ // Broadcast starting event
+ gateway.events.Broadcast(GatewayEvent{Status: "starting", PID: pid})
+
+ // Capture stdout/stderr in background
+ go scanPipe(stdoutPipe, gateway.logs)
+ go scanPipe(stderrPipe, gateway.logs)
+
+ // Wait for exit in background and clean up
+ go func() {
+ if err := cmd.Wait(); err != nil {
+ log.Printf("Gateway process exited: %v", err)
+ } else {
+ log.Printf("Gateway process exited normally")
+ }
+
+ gateway.mu.Lock()
+ if gateway.cmd == cmd {
+ gateway.cmd = nil
+ }
+ gateway.mu.Unlock()
+
+ // Broadcast stopped event
+ gateway.events.Broadcast(GatewayEvent{Status: "stopped"})
+ }()
+
+ // Start a goroutine to probe health and broadcast "running" once ready
+ go func() {
+ for i := 0; i < 30; i++ { // try for up to 15 seconds
+ time.Sleep(500 * time.Millisecond)
+ gateway.mu.Lock()
+ stillOurs := gateway.cmd == cmd
+ gateway.mu.Unlock()
+ if !stillOurs {
+ return
+ }
+ cfg, err := config.LoadConfig(h.configPath)
+ if err != nil {
+ continue
+ }
+ healthHost := "127.0.0.1"
+ if cfg.Gateway.Host != "" && cfg.Gateway.Host != "0.0.0.0" {
+ healthHost = cfg.Gateway.Host
+ }
+ healthPort := cfg.Gateway.Port
+ if healthPort == 0 {
+ healthPort = 18790
+ }
+ healthURL := fmt.Sprintf("http://%s/health", net.JoinHostPort(healthHost, strconv.Itoa(healthPort)))
+ client := http.Client{Timeout: 1 * time.Second}
+ resp, err := client.Get(healthURL)
+ if err == nil {
+ resp.Body.Close()
+ if resp.StatusCode == http.StatusOK {
+ gateway.events.Broadcast(GatewayEvent{Status: "running", PID: pid})
+ return
+ }
+ }
+ }
+ }()
+
+ return pid, nil
+}
+
+// handleGatewayStart starts the picoclaw gateway subprocess.
+//
+// POST /api/gateway/start
+func (h *Handler) handleGatewayStart(w http.ResponseWriter, r *http.Request) {
+ gateway.mu.Lock()
+ defer gateway.mu.Unlock()
+
+ // Prevent duplicate starts
+ if isGatewayProcessAliveLocked() {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusConflict)
+ json.NewEncoder(w).Encode(map[string]any{
+ "status": "already_running",
+ "pid": gateway.cmd.Process.Pid,
+ })
+ return
+ }
+ if gateway.cmd != nil && gateway.cmd.Process != nil {
+ gateway.cmd = nil
+ }
+
+ ready, reason, err := h.gatewayStartReady()
+ if err != nil {
+ http.Error(
+ w,
+ fmt.Sprintf("Failed to validate gateway start conditions: %v", err),
+ http.StatusInternalServerError,
+ )
+ return
+ }
+ if !ready {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusBadRequest)
+ json.NewEncoder(w).Encode(map[string]any{
+ "status": "precondition_failed",
+ "message": reason,
+ })
+ return
+ }
+
+ pid, err := h.startGatewayLocked()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Failed to start gateway: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]any{
+ "status": "ok",
+ "pid": pid,
+ })
+}
+
+// handleGatewayStop stops the running gateway subprocess gracefully.
+//
+// POST /api/gateway/stop
+func (h *Handler) handleGatewayStop(w http.ResponseWriter, r *http.Request) {
+ gateway.mu.Lock()
+ defer gateway.mu.Unlock()
+
+ if gateway.cmd == nil || gateway.cmd.Process == nil {
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]any{
+ "status": "not_running",
+ })
+ return
+ }
+
+ pid := gateway.cmd.Process.Pid
+
+ // Send SIGTERM for graceful shutdown (SIGKILL on Windows)
+ var sigErr error
+ if runtime.GOOS == "windows" {
+ sigErr = gateway.cmd.Process.Kill()
+ } else {
+ sigErr = gateway.cmd.Process.Signal(syscall.SIGTERM)
+ }
+
+ if sigErr != nil {
+ http.Error(w, fmt.Sprintf("Failed to stop gateway (PID %d): %v", pid, sigErr), http.StatusInternalServerError)
+ return
+ }
+
+ log.Printf("Sent stop signal to gateway (PID: %d)", pid)
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]any{
+ "status": "ok",
+ "pid": pid,
+ })
+}
+
+// handleGatewayRestart stops the gateway (if running) and starts a new instance.
+//
+// POST /api/gateway/restart
+func (h *Handler) handleGatewayRestart(w http.ResponseWriter, r *http.Request) {
+ gateway.mu.Lock()
+
+ // Stop existing process if running
+ if gateway.cmd != nil && gateway.cmd.Process != nil {
+ if isCmdProcessAliveLocked(gateway.cmd) {
+ // Process is alive, send SIGTERM
+ if runtime.GOOS == "windows" {
+ gateway.cmd.Process.Kill()
+ } else {
+ gateway.cmd.Process.Signal(syscall.SIGTERM)
+ }
+
+ // Wait briefly for it to exit
+ gateway.mu.Unlock()
+ time.Sleep(2 * time.Second)
+ gateway.mu.Lock()
+ }
+ gateway.cmd = nil
+ }
+
+ gateway.mu.Unlock()
+
+ // Start fresh via the existing handler
+ h.handleGatewayStart(w, r)
+}
+
+// handleGatewayStatus returns the gateway run status, health info, and logs.
+//
+// GET /api/gateway/status
+func (h *Handler) handleGatewayStatus(w http.ResponseWriter, r *http.Request) {
+ data := map[string]any{}
+
+ // Check process state
+ gateway.mu.Lock()
+ processAlive := isGatewayProcessAliveLocked()
+ if processAlive {
+ data["pid"] = gateway.cmd.Process.Pid
+ }
+ gateway.mu.Unlock()
+
+ if !processAlive {
+ data["gateway_status"] = "stopped"
+ } else {
+ // Process is alive — probe its health endpoint
+ cfg, err := config.LoadConfig(h.configPath)
+ host := "127.0.0.1"
+ port := 18790
+ if err == nil && cfg != nil {
+ if cfg.Gateway.Host != "" && cfg.Gateway.Host != "0.0.0.0" {
+ host = cfg.Gateway.Host
+ }
+ if cfg.Gateway.Port != 0 {
+ port = cfg.Gateway.Port
+ }
+ }
+
+ url := fmt.Sprintf("http://%s/health", net.JoinHostPort(host, strconv.Itoa(port)))
+ client := http.Client{Timeout: 2 * time.Second}
+ resp, err := client.Get(url)
+
+ if err != nil {
+ data["gateway_status"] = "starting"
+ } else {
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ data["gateway_status"] = "error"
+ data["status_code"] = resp.StatusCode
+ } else {
+ var healthData map[string]any
+ if decErr := json.NewDecoder(resp.Body).Decode(&healthData); decErr != nil {
+ data["gateway_status"] = "error"
+ } else {
+ for k, v := range healthData {
+ data[k] = v
+ }
+ data["gateway_status"] = "running"
+ }
+ }
+ }
+ }
+
+ ready, reason, readyErr := h.gatewayStartReady()
+ if readyErr != nil {
+ data["gateway_start_allowed"] = false
+ data["gateway_start_reason"] = readyErr.Error()
+ } else {
+ data["gateway_start_allowed"] = ready
+ if !ready {
+ data["gateway_start_reason"] = reason
+ }
+ }
+
+ // Append incremental log data
+ appendGatewayLogs(r, data)
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(data)
+}
+
+// appendGatewayLogs reads log_offset and log_run_id query params from the request
+// and populates the response data map with incremental log lines.
+func appendGatewayLogs(r *http.Request, data map[string]any) {
+ clientOffset := 0
+ clientRunID := -1
+
+ if v := r.URL.Query().Get("log_offset"); v != "" {
+ if n, err := strconv.Atoi(v); err == nil {
+ clientOffset = n
+ }
+ }
+
+ if v := r.URL.Query().Get("log_run_id"); v != "" {
+ if n, err := strconv.Atoi(v); err == nil {
+ clientRunID = n
+ }
+ }
+
+ runID := gateway.logs.RunID()
+
+ if runID == 0 {
+ data["logs"] = []string{}
+ data["log_total"] = 0
+ data["log_run_id"] = 0
+ return
+ }
+
+ // If runID changed, reset offset to get all logs from new run
+ offset := clientOffset
+ if clientRunID != runID {
+ offset = 0
+ }
+
+ lines, total, runID := gateway.logs.LinesSince(offset)
+ if lines == nil {
+ lines = []string{}
+ }
+
+ data["logs"] = lines
+ data["log_total"] = total
+ data["log_run_id"] = runID
+}
+
+// handleGatewayEvents serves an SSE stream of gateway state change events.
+//
+// GET /api/gateway/events
+func (h *Handler) handleGatewayEvents(w http.ResponseWriter, r *http.Request) {
+ flusher, ok := w.(http.Flusher)
+ if !ok {
+ http.Error(w, "SSE not supported", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/event-stream")
+ w.Header().Set("Cache-Control", "no-cache")
+ w.Header().Set("Connection", "keep-alive")
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+
+ // Subscribe to gateway events
+ ch := gateway.events.Subscribe()
+ defer gateway.events.Unsubscribe(ch)
+
+ // Send initial status so the client doesn't start blank
+ initial := h.currentGatewayStatus()
+ fmt.Fprintf(w, "data: %s\n\n", initial)
+ flusher.Flush()
+
+ for {
+ select {
+ case <-r.Context().Done():
+ return
+ case data, ok := <-ch:
+ if !ok {
+ return
+ }
+ fmt.Fprintf(w, "data: %s\n\n", data)
+ flusher.Flush()
+ }
+ }
+}
+
+// currentGatewayStatus returns the current gateway status as a JSON string.
+func (h *Handler) currentGatewayStatus() string {
+ gateway.mu.Lock()
+ defer gateway.mu.Unlock()
+
+ data := map[string]any{
+ "gateway_status": "stopped",
+ }
+ if isGatewayProcessAliveLocked() {
+ data["gateway_status"] = "running"
+ data["pid"] = gateway.cmd.Process.Pid
+ }
+
+ ready, reason, readyErr := h.gatewayStartReady()
+ if readyErr != nil {
+ data["gateway_start_allowed"] = false
+ data["gateway_start_reason"] = readyErr.Error()
+ } else {
+ data["gateway_start_allowed"] = ready
+ if !ready {
+ data["gateway_start_reason"] = reason
+ }
+ }
+
+ encoded, _ := json.Marshal(data)
+ return string(encoded)
+}
+
+// findPicoclawBinary locates the picoclaw executable.
+// Tries the same directory as the current executable first, then falls back to $PATH.
+func findPicoclawBinary() string {
+ if exe, err := os.Executable(); err == nil {
+ dir := filepath.Dir(exe)
+ candidate := filepath.Join(dir, "picoclaw")
+ if runtime.GOOS == "windows" {
+ candidate += ".exe"
+ }
+ if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
+ return candidate
+ }
+ }
+ return "picoclaw"
+}
+
+// scanPipe reads lines from r and appends them to buf. Returns when r reaches EOF.
+func scanPipe(r io.Reader, buf *LogBuffer) {
+ scanner := bufio.NewScanner(r)
+ scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
+ for scanner.Scan() {
+ buf.Append(scanner.Text())
+ }
+}
diff --git a/web/backend/api/gateway_test.go b/web/backend/api/gateway_test.go
new file mode 100644
index 000000000..336bb6a0c
--- /dev/null
+++ b/web/backend/api/gateway_test.go
@@ -0,0 +1,122 @@
+package api
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/sipeed/picoclaw/pkg/config"
+)
+
+func TestGatewayStartReady_NoDefaultModel(t *testing.T) {
+ configPath := filepath.Join(t.TempDir(), "config.json")
+ h := NewHandler(configPath)
+
+ ready, reason, err := h.gatewayStartReady()
+ if err != nil {
+ t.Fatalf("gatewayStartReady() error = %v", err)
+ }
+ if ready {
+ t.Fatalf("gatewayStartReady() ready = true, want false")
+ }
+ if reason != "no default model configured" {
+ t.Fatalf("gatewayStartReady() reason = %q, want %q", reason, "no default model configured")
+ }
+}
+
+func TestGatewayStartReady_InvalidDefaultModel(t *testing.T) {
+ configPath := filepath.Join(t.TempDir(), "config.json")
+ cfg := config.DefaultConfig()
+ cfg.Agents.Defaults.Model = "missing-model"
+ if err := config.SaveConfig(configPath, cfg); err != nil {
+ t.Fatalf("SaveConfig() error = %v", err)
+ }
+
+ h := NewHandler(configPath)
+ ready, reason, err := h.gatewayStartReady()
+ if err != nil {
+ t.Fatalf("gatewayStartReady() error = %v", err)
+ }
+ if ready {
+ t.Fatalf("gatewayStartReady() ready = true, want false")
+ }
+ if reason == "" {
+ t.Fatalf("gatewayStartReady() reason is empty")
+ }
+}
+
+func TestGatewayStartReady_ValidDefaultModel(t *testing.T) {
+ configPath := filepath.Join(t.TempDir(), "config.json")
+ cfg := config.DefaultConfig()
+ cfg.Agents.Defaults.ModelName = cfg.ModelList[0].ModelName
+ cfg.ModelList[0].APIKey = "test-key"
+ if err := config.SaveConfig(configPath, cfg); err != nil {
+ t.Fatalf("SaveConfig() error = %v", err)
+ }
+
+ h := NewHandler(configPath)
+ ready, reason, err := h.gatewayStartReady()
+ if err != nil {
+ t.Fatalf("gatewayStartReady() error = %v", err)
+ }
+ if !ready {
+ t.Fatalf("gatewayStartReady() ready = false, want true (reason=%q)", reason)
+ }
+}
+
+func TestGatewayStartReady_DefaultModelWithoutCredential(t *testing.T) {
+ configPath := filepath.Join(t.TempDir(), "config.json")
+ cfg := config.DefaultConfig()
+ cfg.Agents.Defaults.ModelName = cfg.ModelList[0].ModelName
+ cfg.ModelList[0].APIKey = ""
+ cfg.ModelList[0].AuthMethod = ""
+ if err := config.SaveConfig(configPath, cfg); err != nil {
+ t.Fatalf("SaveConfig() error = %v", err)
+ }
+
+ h := NewHandler(configPath)
+ ready, reason, err := h.gatewayStartReady()
+ if err != nil {
+ t.Fatalf("gatewayStartReady() error = %v", err)
+ }
+ if ready {
+ t.Fatalf("gatewayStartReady() ready = true, want false")
+ }
+ if !strings.Contains(reason, "no credentials configured") {
+ t.Fatalf("gatewayStartReady() reason = %q, want contains %q", reason, "no credentials configured")
+ }
+}
+
+func TestGatewayStatusIncludesStartConditionWhenNotReady(t *testing.T) {
+ configPath := filepath.Join(t.TempDir(), "config.json")
+ h := NewHandler(configPath)
+ mux := http.NewServeMux()
+ h.RegisterRoutes(mux)
+
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest(http.MethodGet, "/api/gateway/status", nil)
+ mux.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
+ }
+
+ var body map[string]any
+ if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
+ t.Fatalf("unmarshal response: %v", err)
+ }
+
+ allowed, ok := body["gateway_start_allowed"].(bool)
+ if !ok {
+ t.Fatalf("gateway_start_allowed missing or not bool: %#v", body["gateway_start_allowed"])
+ }
+ if allowed {
+ t.Fatalf("gateway_start_allowed = true, want false")
+ }
+ if _, ok := body["gateway_start_reason"].(string); !ok {
+ t.Fatalf("gateway_start_reason missing or not string: %#v", body["gateway_start_reason"])
+ }
+}
diff --git a/web/backend/api/launcher_config.go b/web/backend/api/launcher_config.go
new file mode 100644
index 000000000..e149d5671
--- /dev/null
+++ b/web/backend/api/launcher_config.go
@@ -0,0 +1,85 @@
+package api
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+
+ "github.com/sipeed/picoclaw/web/backend/launcherconfig"
+)
+
+type launcherConfigPayload struct {
+ Port int `json:"port"`
+ Public bool `json:"public"`
+ AllowedCIDRs []string `json:"allowed_cidrs"`
+}
+
+func (h *Handler) registerLauncherConfigRoutes(mux *http.ServeMux) {
+ mux.HandleFunc("GET /api/system/launcher-config", h.handleGetLauncherConfig)
+ mux.HandleFunc("PUT /api/system/launcher-config", h.handleUpdateLauncherConfig)
+}
+
+func (h *Handler) launcherConfigPath() string {
+ return launcherconfig.PathForAppConfig(h.configPath)
+}
+
+func (h *Handler) launcherFallbackConfig() launcherconfig.Config {
+ port := h.serverPort
+ if port <= 0 {
+ port = launcherconfig.DefaultPort
+ }
+ return launcherconfig.Config{
+ Port: port,
+ Public: h.serverPublic,
+ AllowedCIDRs: append([]string(nil), h.serverCIDRs...),
+ }
+}
+
+func (h *Handler) loadLauncherConfig() (launcherconfig.Config, error) {
+ return launcherconfig.Load(h.launcherConfigPath(), h.launcherFallbackConfig())
+}
+
+func (h *Handler) handleGetLauncherConfig(w http.ResponseWriter, r *http.Request) {
+ cfg, err := h.loadLauncherConfig()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Failed to load launcher config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(launcherConfigPayload{
+ Port: cfg.Port,
+ Public: cfg.Public,
+ AllowedCIDRs: append([]string(nil), cfg.AllowedCIDRs...),
+ })
+}
+
+func (h *Handler) handleUpdateLauncherConfig(w http.ResponseWriter, r *http.Request) {
+ var payload launcherConfigPayload
+ if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
+ http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
+ return
+ }
+
+ cfg := launcherconfig.Config{
+ Port: payload.Port,
+ Public: payload.Public,
+ AllowedCIDRs: append([]string(nil), payload.AllowedCIDRs...),
+ }
+ if err := launcherconfig.Validate(cfg); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ if err := launcherconfig.Save(h.launcherConfigPath(), cfg); err != nil {
+ http.Error(w, fmt.Sprintf("Failed to save launcher config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(launcherConfigPayload{
+ Port: cfg.Port,
+ Public: cfg.Public,
+ AllowedCIDRs: append([]string(nil), cfg.AllowedCIDRs...),
+ })
+}
diff --git a/web/backend/api/launcher_config_test.go b/web/backend/api/launcher_config_test.go
new file mode 100644
index 000000000..5049dd88f
--- /dev/null
+++ b/web/backend/api/launcher_config_test.go
@@ -0,0 +1,115 @@
+package api
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/sipeed/picoclaw/web/backend/launcherconfig"
+)
+
+func TestGetLauncherConfigUsesRuntimeFallback(t *testing.T) {
+ configPath := filepath.Join(t.TempDir(), "config.json")
+ h := NewHandler(configPath)
+ h.SetServerOptions(19999, true, []string{"192.168.1.0/24"})
+
+ mux := http.NewServeMux()
+ h.RegisterRoutes(mux)
+
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest(http.MethodGet, "/api/system/launcher-config", nil)
+ mux.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
+ }
+
+ var got launcherConfigPayload
+ if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
+ t.Fatalf("unmarshal response: %v", err)
+ }
+ if got.Port != 19999 || !got.Public {
+ t.Fatalf("response = %+v, want port=19999 public=true", got)
+ }
+ if len(got.AllowedCIDRs) != 1 || got.AllowedCIDRs[0] != "192.168.1.0/24" {
+ t.Fatalf("response allowed_cidrs = %v, want [192.168.1.0/24]", got.AllowedCIDRs)
+ }
+}
+
+func TestPutLauncherConfigPersists(t *testing.T) {
+ configPath := filepath.Join(t.TempDir(), "config.json")
+ h := NewHandler(configPath)
+
+ mux := http.NewServeMux()
+ h.RegisterRoutes(mux)
+
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest(
+ http.MethodPut,
+ "/api/system/launcher-config",
+ strings.NewReader(`{"port":18080,"public":true,"allowed_cidrs":["192.168.1.0/24"]}`),
+ )
+ req.Header.Set("Content-Type", "application/json")
+ mux.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
+ }
+
+ path := launcherconfig.PathForAppConfig(configPath)
+ cfg, err := launcherconfig.Load(path, launcherconfig.Default())
+ if err != nil {
+ t.Fatalf("launcherconfig.Load() error = %v", err)
+ }
+ if cfg.Port != 18080 || !cfg.Public {
+ t.Fatalf("saved config = %+v, want port=18080 public=true", cfg)
+ }
+ if len(cfg.AllowedCIDRs) != 1 || cfg.AllowedCIDRs[0] != "192.168.1.0/24" {
+ t.Fatalf("saved config allowed_cidrs = %v, want [192.168.1.0/24]", cfg.AllowedCIDRs)
+ }
+}
+
+func TestPutLauncherConfigRejectsInvalidPort(t *testing.T) {
+ configPath := filepath.Join(t.TempDir(), "config.json")
+ h := NewHandler(configPath)
+
+ mux := http.NewServeMux()
+ h.RegisterRoutes(mux)
+
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest(
+ http.MethodPut,
+ "/api/system/launcher-config",
+ strings.NewReader(`{"port":70000,"public":false}`),
+ )
+ req.Header.Set("Content-Type", "application/json")
+ mux.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusBadRequest {
+ t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusBadRequest, rec.Body.String())
+ }
+}
+
+func TestPutLauncherConfigRejectsInvalidCIDR(t *testing.T) {
+ configPath := filepath.Join(t.TempDir(), "config.json")
+ h := NewHandler(configPath)
+
+ mux := http.NewServeMux()
+ h.RegisterRoutes(mux)
+
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest(
+ http.MethodPut,
+ "/api/system/launcher-config",
+ strings.NewReader(`{"port":18080,"public":false,"allowed_cidrs":["bad-cidr"]}`),
+ )
+ req.Header.Set("Content-Type", "application/json")
+ mux.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusBadRequest {
+ t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusBadRequest, rec.Body.String())
+ }
+}
diff --git a/cmd/picoclaw-launcher/internal/server/logbuffer.go b/web/backend/api/log.go
similarity index 92%
rename from cmd/picoclaw-launcher/internal/server/logbuffer.go
rename to web/backend/api/log.go
index 4d70f6466..ecf7d422f 100644
--- a/cmd/picoclaw-launcher/internal/server/logbuffer.go
+++ b/web/backend/api/log.go
@@ -1,4 +1,4 @@
-package server
+package api
import "sync"
@@ -89,11 +89,3 @@ func (b *LogBuffer) RunID() int {
return b.runID
}
-
-// Total returns the total number of lines appended in the current run.
-func (b *LogBuffer) Total() int {
- b.mu.RLock()
- defer b.mu.RUnlock()
-
- return b.total
-}
diff --git a/web/backend/api/models.go b/web/backend/api/models.go
new file mode 100644
index 000000000..cb57d6f2e
--- /dev/null
+++ b/web/backend/api/models.go
@@ -0,0 +1,298 @@
+package api
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strconv"
+
+ "github.com/sipeed/picoclaw/pkg/config"
+)
+
+// registerModelRoutes binds model list management endpoints to the ServeMux.
+func (h *Handler) registerModelRoutes(mux *http.ServeMux) {
+ mux.HandleFunc("GET /api/models", h.handleListModels)
+ mux.HandleFunc("POST /api/models", h.handleAddModel)
+ mux.HandleFunc("POST /api/models/default", h.handleSetDefaultModel)
+ mux.HandleFunc("PUT /api/models/{index}", h.handleUpdateModel)
+ mux.HandleFunc("DELETE /api/models/{index}", h.handleDeleteModel)
+}
+
+// modelResponse is the JSON structure returned for each model in the list.
+// All ModelConfig fields are included so the frontend can display and edit them.
+type modelResponse struct {
+ Index int `json:"index"`
+ ModelName string `json:"model_name"`
+ Model string `json:"model"`
+ APIBase string `json:"api_base,omitempty"`
+ APIKey string `json:"api_key"`
+ Proxy string `json:"proxy,omitempty"`
+ AuthMethod string `json:"auth_method,omitempty"`
+ // Advanced fields
+ ConnectMode string `json:"connect_mode,omitempty"`
+ Workspace string `json:"workspace,omitempty"`
+ RPM int `json:"rpm,omitempty"`
+ MaxTokensField string `json:"max_tokens_field,omitempty"`
+ RequestTimeout int `json:"request_timeout,omitempty"`
+ ThinkingLevel string `json:"thinking_level,omitempty"`
+ // Meta
+ Configured bool `json:"configured"`
+ IsDefault bool `json:"is_default"`
+}
+
+// handleListModels returns all model_list entries with masked API keys.
+//
+// GET /api/models
+func (h *Handler) handleListModels(w http.ResponseWriter, r *http.Request) {
+ cfg, err := h.loadFilteredConfig()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ defaultModel := cfg.Agents.Defaults.GetModelName()
+
+ models := make([]modelResponse, 0, len(cfg.ModelList))
+ for i, m := range cfg.ModelList {
+ models = append(models, modelResponse{
+ Index: i,
+ ModelName: m.ModelName,
+ Model: m.Model,
+ APIBase: m.APIBase,
+ APIKey: maskAPIKey(m.APIKey),
+ Proxy: m.Proxy,
+ AuthMethod: m.AuthMethod,
+ ConnectMode: m.ConnectMode,
+ Workspace: m.Workspace,
+ RPM: m.RPM,
+ MaxTokensField: m.MaxTokensField,
+ RequestTimeout: m.RequestTimeout,
+ ThinkingLevel: m.ThinkingLevel,
+ Configured: m.APIKey != "" || m.AuthMethod != "",
+ IsDefault: m.ModelName == defaultModel,
+ })
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]any{
+ "models": models,
+ "total": len(models),
+ "default_model": defaultModel,
+ })
+}
+
+// handleAddModel appends a new model configuration entry.
+//
+// POST /api/models
+func (h *Handler) handleAddModel(w http.ResponseWriter, r *http.Request) {
+ body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
+ if err != nil {
+ http.Error(w, "Failed to read request body", http.StatusBadRequest)
+ return
+ }
+ defer r.Body.Close()
+
+ var mc config.ModelConfig
+ if err = json.Unmarshal(body, &mc); err != nil {
+ http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
+ return
+ }
+
+ if err = mc.Validate(); err != nil {
+ http.Error(w, fmt.Sprintf("Validation error: %v", err), http.StatusBadRequest)
+ return
+ }
+
+ cfg, err := config.LoadConfig(h.configPath)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ cfg.ModelList = append(cfg.ModelList, mc)
+
+ if err := config.SaveConfig(h.configPath, cfg); err != nil {
+ http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]any{
+ "status": "ok",
+ "index": len(cfg.ModelList) - 1,
+ })
+}
+
+// handleUpdateModel replaces a model configuration entry at the given index.
+// If the request body omits api_key (or sends an empty string), the existing
+// stored key is preserved so callers can update only api_base / proxy without
+// exposing or clearing the secret.
+//
+// PUT /api/models/{index}
+func (h *Handler) handleUpdateModel(w http.ResponseWriter, r *http.Request) {
+ idx, err := strconv.Atoi(r.PathValue("index"))
+ if err != nil {
+ http.Error(w, "Invalid index", http.StatusBadRequest)
+ return
+ }
+
+ body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
+ if err != nil {
+ http.Error(w, "Failed to read request body", http.StatusBadRequest)
+ return
+ }
+ defer r.Body.Close()
+
+ var mc config.ModelConfig
+ if err = json.Unmarshal(body, &mc); err != nil {
+ http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
+ return
+ }
+
+ if err = mc.Validate(); err != nil {
+ http.Error(w, fmt.Sprintf("Validation error: %v", err), http.StatusBadRequest)
+ return
+ }
+
+ cfg, err := config.LoadConfig(h.configPath)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ if idx < 0 || idx >= len(cfg.ModelList) {
+ http.Error(w, fmt.Sprintf("Index %d out of range (0-%d)", idx, len(cfg.ModelList)-1), http.StatusNotFound)
+ return
+ }
+
+ // Preserve the existing API key when the caller omits it (empty string).
+ // This lets the UI update api_base / proxy without clearing the stored secret.
+ if mc.APIKey == "" {
+ mc.APIKey = cfg.ModelList[idx].APIKey
+ }
+
+ cfg.ModelList[idx] = mc
+
+ if err := config.SaveConfig(h.configPath, cfg); err != nil {
+ http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
+}
+
+// handleDeleteModel removes a model configuration entry at the given index.
+//
+// DELETE /api/models/{index}
+func (h *Handler) handleDeleteModel(w http.ResponseWriter, r *http.Request) {
+ idx, err := strconv.Atoi(r.PathValue("index"))
+ if err != nil {
+ http.Error(w, "Invalid index", http.StatusBadRequest)
+ return
+ }
+
+ cfg, err := config.LoadConfig(h.configPath)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ if idx < 0 || idx >= len(cfg.ModelList) {
+ http.Error(w, fmt.Sprintf("Index %d out of range (0-%d)", idx, len(cfg.ModelList)-1), http.StatusNotFound)
+ return
+ }
+
+ deletedModelName := cfg.ModelList[idx].ModelName
+
+ cfg.ModelList = append(cfg.ModelList[:idx], cfg.ModelList[idx+1:]...)
+
+ // If the deleted model was the default, clear it.
+ if cfg.Agents.Defaults.ModelName == deletedModelName {
+ cfg.Agents.Defaults.ModelName = ""
+ }
+ if cfg.Agents.Defaults.Model == deletedModelName {
+ cfg.Agents.Defaults.Model = ""
+ }
+
+ if err := config.SaveConfig(h.configPath, cfg); err != nil {
+ http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
+}
+
+// handleSetDefaultModel sets the default model for all agents.
+//
+// POST /api/models/default
+func (h *Handler) handleSetDefaultModel(w http.ResponseWriter, r *http.Request) {
+ body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
+ if err != nil {
+ http.Error(w, "Failed to read request body", http.StatusBadRequest)
+ return
+ }
+ defer r.Body.Close()
+
+ var req struct {
+ ModelName string `json:"model_name"`
+ }
+ if err = json.Unmarshal(body, &req); err != nil {
+ http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
+ return
+ }
+
+ if req.ModelName == "" {
+ http.Error(w, "model_name is required", http.StatusBadRequest)
+ return
+ }
+
+ cfg, err := config.LoadConfig(h.configPath)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ // Verify the model_name exists in model_list
+ found := false
+ for _, m := range cfg.ModelList {
+ if m.ModelName == req.ModelName {
+ found = true
+ break
+ }
+ }
+ if !found {
+ http.Error(w, fmt.Sprintf("Model %q not found in model_list", req.ModelName), http.StatusNotFound)
+ return
+ }
+
+ cfg.Agents.Defaults.ModelName = req.ModelName
+
+ if err := config.SaveConfig(h.configPath, cfg); err != nil {
+ http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{
+ "status": "ok",
+ "default_model": req.ModelName,
+ })
+}
+
+// maskAPIKey returns a masked version of an API key for safe display.
+// Keys longer than 8 chars show prefix + last 4 chars: "sk-****abcd"
+// Shorter keys are fully masked as "****".
+// Empty keys return empty string.
+func maskAPIKey(key string) string {
+ if key == "" {
+ return ""
+ }
+ if len(key) <= 8 {
+ return "****"
+ }
+ // Show first 3 chars and last 4 chars
+ return key[:3] + "****" + key[len(key)-4:]
+}
diff --git a/web/backend/api/oauth.go b/web/backend/api/oauth.go
new file mode 100644
index 000000000..04cd595f2
--- /dev/null
+++ b/web/backend/api/oauth.go
@@ -0,0 +1,844 @@
+package api
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "html"
+ "io"
+ "log"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/sipeed/picoclaw/pkg/auth"
+ "github.com/sipeed/picoclaw/pkg/config"
+ "github.com/sipeed/picoclaw/pkg/providers"
+)
+
+const (
+ oauthProviderOpenAI = "openai"
+ oauthProviderAnthropic = "anthropic"
+ oauthProviderGoogleAntigravity = "google-antigravity"
+
+ oauthMethodBrowser = "browser"
+ oauthMethodDeviceCode = "device_code"
+ oauthMethodToken = "token"
+
+ oauthFlowPending = "pending"
+ oauthFlowSuccess = "success"
+ oauthFlowError = "error"
+ oauthFlowExpired = "expired"
+)
+
+const (
+ oauthBrowserFlowTTL = 10 * time.Minute
+ oauthDeviceCodeFlowTTL = 15 * time.Minute
+ oauthTerminalFlowGC = 30 * time.Minute
+)
+
+var oauthProviderOrder = []string{
+ oauthProviderOpenAI,
+ oauthProviderAnthropic,
+ oauthProviderGoogleAntigravity,
+}
+
+var oauthProviderMethods = map[string][]string{
+ oauthProviderOpenAI: {oauthMethodBrowser, oauthMethodDeviceCode, oauthMethodToken},
+ oauthProviderAnthropic: {oauthMethodToken},
+ oauthProviderGoogleAntigravity: {oauthMethodBrowser},
+}
+
+var oauthProviderLabels = map[string]string{
+ oauthProviderOpenAI: "OpenAI",
+ oauthProviderAnthropic: "Anthropic",
+ oauthProviderGoogleAntigravity: "Google Antigravity",
+}
+
+var (
+ oauthNow = time.Now
+ oauthGeneratePKCE = auth.GeneratePKCE
+ oauthGenerateState = auth.GenerateState
+ oauthBuildAuthorizeURL = auth.BuildAuthorizeURL
+ oauthRequestDeviceCode = auth.RequestDeviceCode
+ oauthPollDeviceCodeOnce = auth.PollDeviceCodeOnce
+ oauthExchangeCodeForTokens = auth.ExchangeCodeForTokens
+ oauthGetCredential = auth.GetCredential
+ oauthSetCredential = auth.SetCredential
+ oauthDeleteCredential = auth.DeleteCredential
+ oauthLoadConfig = config.LoadConfig
+ oauthSaveConfig = config.SaveConfig
+ oauthFetchAntigravityProject = providers.FetchAntigravityProjectID
+ oauthFetchGoogleUserEmailFunc = fetchGoogleUserEmail
+)
+
+type oauthFlow struct {
+ ID string
+ Provider string
+ Method string
+ Status string
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ ExpiresAt time.Time
+ Error string
+ CodeVerifier string
+ OAuthState string
+ RedirectURI string
+ DeviceAuthID string
+ UserCode string
+ VerifyURL string
+ Interval int
+}
+
+type oauthProviderStatus struct {
+ Provider string `json:"provider"`
+ DisplayName string `json:"display_name"`
+ Methods []string `json:"methods"`
+ LoggedIn bool `json:"logged_in"`
+ Status string `json:"status"`
+ AuthMethod string `json:"auth_method,omitempty"`
+ ExpiresAt string `json:"expires_at,omitempty"`
+ AccountID string `json:"account_id,omitempty"`
+ Email string `json:"email,omitempty"`
+ ProjectID string `json:"project_id,omitempty"`
+}
+
+type oauthFlowResponse struct {
+ FlowID string `json:"flow_id"`
+ Provider string `json:"provider"`
+ Method string `json:"method"`
+ Status string `json:"status"`
+ ExpiresAt string `json:"expires_at,omitempty"`
+ Error string `json:"error,omitempty"`
+ UserCode string `json:"user_code,omitempty"`
+ VerifyURL string `json:"verify_url,omitempty"`
+ Interval int `json:"interval,omitempty"`
+}
+
+// registerOAuthRoutes binds OAuth login/logout endpoints to the ServeMux.
+func (h *Handler) registerOAuthRoutes(mux *http.ServeMux) {
+ mux.HandleFunc("GET /api/oauth/providers", h.handleListOAuthProviders)
+ mux.HandleFunc("POST /api/oauth/login", h.handleOAuthLogin)
+ mux.HandleFunc("GET /api/oauth/flows/{id}", h.handleGetOAuthFlow)
+ mux.HandleFunc("POST /api/oauth/flows/{id}/poll", h.handlePollOAuthFlow)
+ mux.HandleFunc("POST /api/oauth/logout", h.handleOAuthLogout)
+ mux.HandleFunc("GET /oauth/callback", h.handleOAuthCallback)
+}
+
+func (h *Handler) handleListOAuthProviders(w http.ResponseWriter, r *http.Request) {
+ providersResp := make([]oauthProviderStatus, 0, len(oauthProviderOrder))
+
+ for _, provider := range oauthProviderOrder {
+ cred, err := oauthGetCredential(provider)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("failed to load credentials: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ item := oauthProviderStatus{
+ Provider: provider,
+ DisplayName: oauthProviderLabels[provider],
+ Methods: oauthProviderMethods[provider],
+ Status: "not_logged_in",
+ }
+ if cred != nil {
+ item.LoggedIn = true
+ item.AuthMethod = cred.AuthMethod
+ item.AccountID = cred.AccountID
+ item.Email = cred.Email
+ item.ProjectID = cred.ProjectID
+ if !cred.ExpiresAt.IsZero() {
+ item.ExpiresAt = cred.ExpiresAt.Format(time.RFC3339)
+ }
+ switch {
+ case cred.IsExpired():
+ item.Status = "expired"
+ case cred.NeedsRefresh():
+ item.Status = "needs_refresh"
+ default:
+ item.Status = "connected"
+ }
+ }
+
+ providersResp = append(providersResp, item)
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(map[string]any{
+ "providers": providersResp,
+ })
+}
+
+func (h *Handler) handleOAuthLogin(w http.ResponseWriter, r *http.Request) {
+ body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
+ if err != nil {
+ http.Error(w, "failed to read request body", http.StatusBadRequest)
+ return
+ }
+ defer r.Body.Close()
+
+ var req struct {
+ Provider string `json:"provider"`
+ Method string `json:"method"`
+ Token string `json:"token"`
+ }
+ if err = json.Unmarshal(body, &req); err != nil {
+ http.Error(w, fmt.Sprintf("invalid JSON: %v", err), http.StatusBadRequest)
+ return
+ }
+
+ provider, err := normalizeOAuthProvider(req.Provider)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ method := strings.ToLower(strings.TrimSpace(req.Method))
+ if !isOAuthMethodSupported(provider, method) {
+ http.Error(
+ w,
+ fmt.Sprintf("unsupported login method %q for provider %q", method, provider),
+ http.StatusBadRequest,
+ )
+ return
+ }
+
+ switch method {
+ case oauthMethodToken:
+ token := strings.TrimSpace(req.Token)
+ if token == "" {
+ http.Error(w, "token is required", http.StatusBadRequest)
+ return
+ }
+
+ cred := &auth.AuthCredential{
+ AccessToken: token,
+ Provider: provider,
+ AuthMethod: oauthMethodToken,
+ }
+ if err := h.persistCredentialAndConfig(provider, oauthMethodToken, cred); err != nil {
+ http.Error(w, fmt.Sprintf("token login failed: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(map[string]any{
+ "status": "ok",
+ "provider": provider,
+ "method": method,
+ })
+ return
+
+ case oauthMethodDeviceCode:
+ cfg := auth.OpenAIOAuthConfig()
+ info, err := oauthRequestDeviceCode(cfg)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("failed to request device code: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ now := oauthNow()
+ flow := &oauthFlow{
+ ID: newOAuthFlowID(),
+ Provider: provider,
+ Method: method,
+ Status: oauthFlowPending,
+ CreatedAt: now,
+ UpdatedAt: now,
+ ExpiresAt: now.Add(oauthDeviceCodeFlowTTL),
+ DeviceAuthID: info.DeviceAuthID,
+ UserCode: info.UserCode,
+ VerifyURL: info.VerifyURL,
+ Interval: info.Interval,
+ }
+ h.storeOAuthFlow(flow)
+
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(map[string]any{
+ "status": "ok",
+ "provider": provider,
+ "method": method,
+ "flow_id": flow.ID,
+ "user_code": flow.UserCode,
+ "verify_url": flow.VerifyURL,
+ "interval": flow.Interval,
+ "expires_at": flow.ExpiresAt.Format(time.RFC3339),
+ })
+ return
+
+ case oauthMethodBrowser:
+ cfg, err := oauthConfigForProvider(provider)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ pkce, err := oauthGeneratePKCE()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("failed to generate PKCE: %v", err), http.StatusInternalServerError)
+ return
+ }
+ state, err := oauthGenerateState()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("failed to generate state: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ redirectURI := buildOAuthRedirectURI(r)
+ authURL := oauthBuildAuthorizeURL(cfg, pkce, state, redirectURI)
+
+ now := oauthNow()
+ flow := &oauthFlow{
+ ID: newOAuthFlowID(),
+ Provider: provider,
+ Method: method,
+ Status: oauthFlowPending,
+ CreatedAt: now,
+ UpdatedAt: now,
+ ExpiresAt: now.Add(oauthBrowserFlowTTL),
+ CodeVerifier: pkce.CodeVerifier,
+ OAuthState: state,
+ RedirectURI: redirectURI,
+ }
+ h.storeOAuthFlow(flow)
+
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(map[string]any{
+ "status": "ok",
+ "provider": provider,
+ "method": method,
+ "flow_id": flow.ID,
+ "auth_url": authURL,
+ "expires_at": flow.ExpiresAt.Format(time.RFC3339),
+ })
+ return
+ default:
+ http.Error(w, "unsupported login method", http.StatusBadRequest)
+ }
+}
+
+func (h *Handler) handleGetOAuthFlow(w http.ResponseWriter, r *http.Request) {
+ flowID := strings.TrimSpace(r.PathValue("id"))
+ if flowID == "" {
+ http.Error(w, "missing flow id", http.StatusBadRequest)
+ return
+ }
+
+ flow, ok := h.getOAuthFlow(flowID)
+ if !ok {
+ http.Error(w, "flow not found", http.StatusNotFound)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(flowToResponse(flow))
+}
+
+func (h *Handler) handlePollOAuthFlow(w http.ResponseWriter, r *http.Request) {
+ flowID := strings.TrimSpace(r.PathValue("id"))
+ if flowID == "" {
+ http.Error(w, "missing flow id", http.StatusBadRequest)
+ return
+ }
+
+ flow, ok := h.getOAuthFlow(flowID)
+ if !ok {
+ http.Error(w, "flow not found", http.StatusNotFound)
+ return
+ }
+
+ if flow.Method != oauthMethodDeviceCode {
+ http.Error(w, "flow does not support polling", http.StatusBadRequest)
+ return
+ }
+ if flow.Status != oauthFlowPending {
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(flowToResponse(flow))
+ return
+ }
+
+ cfg := auth.OpenAIOAuthConfig()
+ cred, err := oauthPollDeviceCodeOnce(cfg, flow.DeviceAuthID, flow.UserCode)
+ if err != nil {
+ if strings.Contains(strings.ToLower(err.Error()), "pending") {
+ updated, _ := h.getOAuthFlow(flowID)
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(flowToResponse(updated))
+ return
+ }
+ h.setOAuthFlowError(flowID, fmt.Sprintf("device code poll failed: %v", err))
+ updated, _ := h.getOAuthFlow(flowID)
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(flowToResponse(updated))
+ return
+ }
+ if cred == nil {
+ updated, _ := h.getOAuthFlow(flowID)
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(flowToResponse(updated))
+ return
+ }
+
+ if err := h.persistCredentialAndConfig(flow.Provider, oauthMethodTokenOrOAuth(flow.Method), cred); err != nil {
+ h.setOAuthFlowError(flowID, fmt.Sprintf("failed to save credential: %v", err))
+ updated, _ := h.getOAuthFlow(flowID)
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(flowToResponse(updated))
+ return
+ }
+
+ h.setOAuthFlowSuccess(flowID)
+ updated, _ := h.getOAuthFlow(flowID)
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(flowToResponse(updated))
+}
+
+func (h *Handler) handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
+ state := strings.TrimSpace(r.URL.Query().Get("state"))
+ if state == "" {
+ renderOAuthCallbackPage(w, "", oauthFlowError, "Missing state", "missing_state")
+ return
+ }
+
+ flow, ok := h.getOAuthFlowByState(state)
+ if !ok {
+ renderOAuthCallbackPage(w, "", oauthFlowError, "OAuth flow not found", "flow_not_found")
+ return
+ }
+
+ if flow.Status != oauthFlowPending {
+ renderOAuthCallbackPage(w, flow.ID, flow.Status, "Flow already completed", flow.Error)
+ return
+ }
+
+ if errMsg := strings.TrimSpace(r.URL.Query().Get("error")); errMsg != "" {
+ if desc := strings.TrimSpace(r.URL.Query().Get("error_description")); desc != "" {
+ errMsg += ": " + desc
+ }
+ h.setOAuthFlowError(flow.ID, errMsg)
+ renderOAuthCallbackPage(w, flow.ID, oauthFlowError, "Authorization failed", errMsg)
+ return
+ }
+
+ code := strings.TrimSpace(r.URL.Query().Get("code"))
+ if code == "" {
+ h.setOAuthFlowError(flow.ID, "missing authorization code")
+ renderOAuthCallbackPage(w, flow.ID, oauthFlowError, "Missing authorization code", "missing_code")
+ return
+ }
+
+ cfg, err := oauthConfigForProvider(flow.Provider)
+ if err != nil {
+ h.setOAuthFlowError(flow.ID, err.Error())
+ renderOAuthCallbackPage(w, flow.ID, oauthFlowError, "Unsupported provider", err.Error())
+ return
+ }
+
+ cred, err := oauthExchangeCodeForTokens(cfg, code, flow.CodeVerifier, flow.RedirectURI)
+ if err != nil {
+ h.setOAuthFlowError(flow.ID, fmt.Sprintf("token exchange failed: %v", err))
+ renderOAuthCallbackPage(w, flow.ID, oauthFlowError, "Token exchange failed", err.Error())
+ return
+ }
+
+ if err := h.persistCredentialAndConfig(flow.Provider, oauthMethodTokenOrOAuth(flow.Method), cred); err != nil {
+ h.setOAuthFlowError(flow.ID, fmt.Sprintf("failed to save credential: %v", err))
+ renderOAuthCallbackPage(w, flow.ID, oauthFlowError, "Failed to save credential", err.Error())
+ return
+ }
+
+ h.setOAuthFlowSuccess(flow.ID)
+ renderOAuthCallbackPage(w, flow.ID, oauthFlowSuccess, "Authentication successful", "")
+}
+
+func (h *Handler) handleOAuthLogout(w http.ResponseWriter, r *http.Request) {
+ body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
+ if err != nil {
+ http.Error(w, "failed to read request body", http.StatusBadRequest)
+ return
+ }
+ defer r.Body.Close()
+
+ var req struct {
+ Provider string `json:"provider"`
+ }
+ if err = json.Unmarshal(body, &req); err != nil {
+ http.Error(w, fmt.Sprintf("invalid JSON: %v", err), http.StatusBadRequest)
+ return
+ }
+
+ provider, err := normalizeOAuthProvider(req.Provider)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ if err := oauthDeleteCredential(provider); err != nil {
+ http.Error(w, fmt.Sprintf("failed to delete credential: %v", err), http.StatusInternalServerError)
+ return
+ }
+ if err := h.syncProviderAuthMethod(provider, ""); err != nil {
+ http.Error(w, fmt.Sprintf("failed to update config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(map[string]any{
+ "status": "ok",
+ "provider": provider,
+ })
+}
+
+func renderOAuthCallbackPage(w http.ResponseWriter, flowID, status, title, errMsg string) {
+ payload := map[string]string{
+ "type": "picoclaw-oauth-result",
+ "flowId": flowID,
+ "status": status,
+ }
+ if errMsg != "" {
+ payload["error"] = errMsg
+ }
+ payloadJSON, _ := json.Marshal(payload)
+
+ message := title
+ if errMsg != "" {
+ message = fmt.Sprintf("%s: %s", title, errMsg)
+ }
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ if status == oauthFlowSuccess {
+ w.WriteHeader(http.StatusOK)
+ } else {
+ w.WriteHeader(http.StatusBadRequest)
+ }
+
+ _, _ = fmt.Fprintf(
+ w,
+ "PicoClaw OAuth%s
%s
You can close this window.
",
+ string(payloadJSON),
+ html.EscapeString(title),
+ html.EscapeString(message),
+ )
+}
+
+func normalizeOAuthProvider(raw string) (string, error) {
+ provider := strings.ToLower(strings.TrimSpace(raw))
+ switch provider {
+ case "antigravity":
+ return oauthProviderGoogleAntigravity, nil
+ case oauthProviderOpenAI, oauthProviderAnthropic, oauthProviderGoogleAntigravity:
+ return provider, nil
+ default:
+ return "", fmt.Errorf("unsupported provider %q", raw)
+ }
+}
+
+func isOAuthMethodSupported(provider, method string) bool {
+ methods := oauthProviderMethods[provider]
+ for _, m := range methods {
+ if m == method {
+ return true
+ }
+ }
+ return false
+}
+
+func oauthConfigForProvider(provider string) (auth.OAuthProviderConfig, error) {
+ switch provider {
+ case oauthProviderOpenAI:
+ return auth.OpenAIOAuthConfig(), nil
+ case oauthProviderGoogleAntigravity:
+ return auth.GoogleAntigravityOAuthConfig(), nil
+ default:
+ return auth.OAuthProviderConfig{}, fmt.Errorf("provider %q does not support browser oauth", provider)
+ }
+}
+
+func oauthMethodTokenOrOAuth(method string) string {
+ if method == oauthMethodToken {
+ return oauthMethodToken
+ }
+ return "oauth"
+}
+
+func buildOAuthRedirectURI(r *http.Request) string {
+ scheme := "http"
+ if r.TLS != nil {
+ scheme = "https"
+ }
+ if forwarded := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto")); forwarded != "" {
+ scheme = strings.Split(forwarded, ",")[0]
+ }
+ return fmt.Sprintf("%s://%s/oauth/callback", scheme, r.Host)
+}
+
+func flowToResponse(flow *oauthFlow) oauthFlowResponse {
+ resp := oauthFlowResponse{
+ FlowID: flow.ID,
+ Provider: flow.Provider,
+ Method: flow.Method,
+ Status: flow.Status,
+ Error: flow.Error,
+ }
+ if !flow.ExpiresAt.IsZero() {
+ resp.ExpiresAt = flow.ExpiresAt.Format(time.RFC3339)
+ }
+ if flow.Method == oauthMethodDeviceCode {
+ resp.UserCode = flow.UserCode
+ resp.VerifyURL = flow.VerifyURL
+ resp.Interval = flow.Interval
+ }
+ return resp
+}
+
+func newOAuthFlowID() string {
+ buf := make([]byte, 16)
+ if _, err := rand.Read(buf); err != nil {
+ return fmt.Sprintf("oauth_%d", time.Now().UnixNano())
+ }
+ return hex.EncodeToString(buf)
+}
+
+func (h *Handler) storeOAuthFlow(flow *oauthFlow) {
+ now := oauthNow()
+ h.oauthMu.Lock()
+ defer h.oauthMu.Unlock()
+
+ h.gcOAuthFlowsLocked(now)
+ h.oauthFlows[flow.ID] = flow
+ if flow.OAuthState != "" {
+ h.oauthState[flow.OAuthState] = flow.ID
+ }
+}
+
+func (h *Handler) getOAuthFlow(flowID string) (*oauthFlow, bool) {
+ now := oauthNow()
+ h.oauthMu.Lock()
+ defer h.oauthMu.Unlock()
+
+ h.gcOAuthFlowsLocked(now)
+ flow, ok := h.oauthFlows[flowID]
+ if !ok {
+ return nil, false
+ }
+ cp := *flow
+ return &cp, true
+}
+
+func (h *Handler) getOAuthFlowByState(state string) (*oauthFlow, bool) {
+ now := oauthNow()
+ h.oauthMu.Lock()
+ defer h.oauthMu.Unlock()
+
+ h.gcOAuthFlowsLocked(now)
+ flowID, ok := h.oauthState[state]
+ if !ok {
+ return nil, false
+ }
+ flow, ok := h.oauthFlows[flowID]
+ if !ok {
+ delete(h.oauthState, state)
+ return nil, false
+ }
+ cp := *flow
+ return &cp, true
+}
+
+func (h *Handler) setOAuthFlowSuccess(flowID string) {
+ now := oauthNow()
+ h.oauthMu.Lock()
+ defer h.oauthMu.Unlock()
+
+ flow, ok := h.oauthFlows[flowID]
+ if !ok {
+ return
+ }
+ flow.Status = oauthFlowSuccess
+ flow.Error = ""
+ flow.UpdatedAt = now
+ if flow.OAuthState != "" {
+ delete(h.oauthState, flow.OAuthState)
+ }
+}
+
+func (h *Handler) setOAuthFlowError(flowID, errMsg string) {
+ now := oauthNow()
+ h.oauthMu.Lock()
+ defer h.oauthMu.Unlock()
+
+ flow, ok := h.oauthFlows[flowID]
+ if !ok {
+ return
+ }
+ flow.Status = oauthFlowError
+ flow.Error = errMsg
+ flow.UpdatedAt = now
+ if flow.OAuthState != "" {
+ delete(h.oauthState, flow.OAuthState)
+ }
+}
+
+func (h *Handler) gcOAuthFlowsLocked(now time.Time) {
+ for id, flow := range h.oauthFlows {
+ if flow.Status == oauthFlowPending && !flow.ExpiresAt.IsZero() && now.After(flow.ExpiresAt) {
+ flow.Status = oauthFlowExpired
+ flow.Error = "flow expired"
+ flow.UpdatedAt = now
+ if flow.OAuthState != "" {
+ delete(h.oauthState, flow.OAuthState)
+ }
+ }
+
+ if flow.Status != oauthFlowPending && now.Sub(flow.UpdatedAt) > oauthTerminalFlowGC {
+ if flow.OAuthState != "" {
+ delete(h.oauthState, flow.OAuthState)
+ }
+ delete(h.oauthFlows, id)
+ }
+ }
+}
+
+func (h *Handler) persistCredentialAndConfig(provider, authMethod string, cred *auth.AuthCredential) error {
+ if cred == nil {
+ return fmt.Errorf("empty credential")
+ }
+
+ cp := *cred
+ cp.Provider = provider
+ if cp.AuthMethod == "" {
+ cp.AuthMethod = authMethod
+ }
+
+ if provider == oauthProviderGoogleAntigravity {
+ if cp.Email == "" {
+ email, err := oauthFetchGoogleUserEmailFunc(cp.AccessToken)
+ if err != nil {
+ log.Printf("oauth warning: could not fetch google email: %v", err)
+ } else {
+ cp.Email = email
+ }
+ }
+ if cp.ProjectID == "" {
+ projectID, err := oauthFetchAntigravityProject(cp.AccessToken)
+ if err != nil {
+ log.Printf("oauth warning: could not fetch antigravity project id: %v", err)
+ } else {
+ cp.ProjectID = projectID
+ }
+ }
+ }
+
+ if err := oauthSetCredential(provider, &cp); err != nil {
+ return fmt.Errorf("saving credential: %w", err)
+ }
+ if err := h.syncProviderAuthMethod(provider, authMethod); err != nil {
+ return fmt.Errorf("syncing provider auth config: %w", err)
+ }
+ return nil
+}
+
+func (h *Handler) syncProviderAuthMethod(provider, authMethod string) error {
+ cfg, err := oauthLoadConfig(h.configPath)
+ if err != nil {
+ return err
+ }
+
+ switch provider {
+ case oauthProviderOpenAI:
+ cfg.Providers.OpenAI.AuthMethod = authMethod
+ case oauthProviderAnthropic:
+ cfg.Providers.Anthropic.AuthMethod = authMethod
+ case oauthProviderGoogleAntigravity:
+ cfg.Providers.Antigravity.AuthMethod = authMethod
+ default:
+ return fmt.Errorf("unsupported provider %q", provider)
+ }
+
+ found := false
+ for i := range cfg.ModelList {
+ if modelBelongsToProvider(provider, cfg.ModelList[i].Model) {
+ cfg.ModelList[i].AuthMethod = authMethod
+ found = true
+ }
+ }
+
+ if !found && authMethod != "" {
+ cfg.ModelList = append(cfg.ModelList, defaultModelConfigForProvider(provider, authMethod))
+ }
+
+ return oauthSaveConfig(h.configPath, cfg)
+}
+
+func modelBelongsToProvider(provider, model string) bool {
+ lower := strings.ToLower(strings.TrimSpace(model))
+ switch provider {
+ case oauthProviderOpenAI:
+ return lower == "openai" || strings.HasPrefix(lower, "openai/")
+ case oauthProviderAnthropic:
+ return lower == "anthropic" || strings.HasPrefix(lower, "anthropic/")
+ case oauthProviderGoogleAntigravity:
+ return lower == "antigravity" ||
+ lower == "google-antigravity" ||
+ strings.HasPrefix(lower, "antigravity/") ||
+ strings.HasPrefix(lower, "google-antigravity/")
+ default:
+ return false
+ }
+}
+
+func defaultModelConfigForProvider(provider, authMethod string) config.ModelConfig {
+ switch provider {
+ case oauthProviderOpenAI:
+ return config.ModelConfig{
+ ModelName: "gpt-5.2",
+ Model: "openai/gpt-5.2",
+ AuthMethod: authMethod,
+ }
+ case oauthProviderAnthropic:
+ return config.ModelConfig{
+ ModelName: "claude-sonnet-4.6",
+ Model: "anthropic/claude-sonnet-4.6",
+ AuthMethod: authMethod,
+ }
+ case oauthProviderGoogleAntigravity:
+ return config.ModelConfig{
+ ModelName: "gemini-flash",
+ Model: "antigravity/gemini-3-flash",
+ AuthMethod: authMethod,
+ }
+ default:
+ return config.ModelConfig{}
+ }
+}
+
+func fetchGoogleUserEmail(accessToken string) (string, error) {
+ req, err := http.NewRequest(http.MethodGet, "https://www.googleapis.com/oauth2/v2/userinfo", nil)
+ if err != nil {
+ return "", err
+ }
+ req.Header.Set("Authorization", "Bearer "+accessToken)
+
+ client := &http.Client{Timeout: 10 * time.Second}
+ resp, err := client.Do(req)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+
+ body, _ := io.ReadAll(resp.Body)
+ if resp.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("userinfo request failed: %s", string(body))
+ }
+
+ var userInfo struct {
+ Email string `json:"email"`
+ }
+ if err := json.Unmarshal(body, &userInfo); err != nil {
+ return "", err
+ }
+ if userInfo.Email == "" {
+ return "", fmt.Errorf("empty email in userinfo response")
+ }
+ return userInfo.Email, nil
+}
diff --git a/web/backend/api/oauth_test.go b/web/backend/api/oauth_test.go
new file mode 100644
index 000000000..2103e1efc
--- /dev/null
+++ b/web/backend/api/oauth_test.go
@@ -0,0 +1,293 @@
+package api
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/sipeed/picoclaw/pkg/auth"
+ "github.com/sipeed/picoclaw/pkg/config"
+)
+
+func TestOAuthLoginRejectsUnsupportedMethod(t *testing.T) {
+ configPath, cleanup := setupOAuthTestEnv(t)
+ defer cleanup()
+ resetOAuthHooks(t)
+
+ h := NewHandler(configPath)
+ mux := http.NewServeMux()
+ h.RegisterRoutes(mux)
+
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest(
+ http.MethodPost,
+ "/api/oauth/login",
+ strings.NewReader(`{"provider":"anthropic","method":"browser"}`),
+ )
+ req.Header.Set("Content-Type", "application/json")
+ mux.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusBadRequest {
+ t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusBadRequest, rec.Body.String())
+ }
+}
+
+func TestOAuthBrowserFlowCreatedAndQueried(t *testing.T) {
+ configPath, cleanup := setupOAuthTestEnv(t)
+ defer cleanup()
+ resetOAuthHooks(t)
+
+ oauthGeneratePKCE = func() (auth.PKCECodes, error) {
+ return auth.PKCECodes{CodeVerifier: "verifier-1", CodeChallenge: "challenge-1"}, nil
+ }
+ oauthGenerateState = func() (string, error) { return "state-1", nil }
+ oauthBuildAuthorizeURL = func(cfg auth.OAuthProviderConfig, pkce auth.PKCECodes, state, redirectURI string) string {
+ return "https://example.com/authorize?state=" + state
+ }
+
+ h := NewHandler(configPath)
+ mux := http.NewServeMux()
+ h.RegisterRoutes(mux)
+
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest(
+ http.MethodPost,
+ "/api/oauth/login",
+ strings.NewReader(`{"provider":"openai","method":"browser"}`),
+ )
+ req.Host = "localhost:18800"
+ req.Header.Set("Content-Type", "application/json")
+ mux.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
+ }
+
+ var loginResp map[string]any
+ if err := json.Unmarshal(rec.Body.Bytes(), &loginResp); err != nil {
+ t.Fatalf("unmarshal login response: %v", err)
+ }
+ flowID, _ := loginResp["flow_id"].(string)
+ if flowID == "" {
+ t.Fatalf("flow_id is empty: %v", loginResp)
+ }
+ if loginResp["auth_url"] != "https://example.com/authorize?state=state-1" {
+ t.Fatalf("unexpected auth_url: %v", loginResp["auth_url"])
+ }
+
+ rec2 := httptest.NewRecorder()
+ req2 := httptest.NewRequest(http.MethodGet, "/api/oauth/flows/"+flowID, nil)
+ mux.ServeHTTP(rec2, req2)
+ if rec2.Code != http.StatusOK {
+ t.Fatalf("flow status code = %d, want %d, body=%s", rec2.Code, http.StatusOK, rec2.Body.String())
+ }
+ var flowResp oauthFlowResponse
+ if err := json.Unmarshal(rec2.Body.Bytes(), &flowResp); err != nil {
+ t.Fatalf("unmarshal flow response: %v", err)
+ }
+ if flowResp.Status != oauthFlowPending {
+ t.Fatalf("flow status = %q, want %q", flowResp.Status, oauthFlowPending)
+ }
+ if flowResp.Method != oauthMethodBrowser {
+ t.Fatalf("flow method = %q, want %q", flowResp.Method, oauthMethodBrowser)
+ }
+}
+
+func TestOAuthFlowExpiresWhenQueried(t *testing.T) {
+ configPath, cleanup := setupOAuthTestEnv(t)
+ defer cleanup()
+ resetOAuthHooks(t)
+
+ now := time.Date(2026, 3, 6, 12, 0, 0, 0, time.UTC)
+ oauthNow = func() time.Time { return now }
+
+ h := NewHandler(configPath)
+ h.storeOAuthFlow(&oauthFlow{
+ ID: "expired-flow",
+ Provider: oauthProviderOpenAI,
+ Method: oauthMethodBrowser,
+ Status: oauthFlowPending,
+ CreatedAt: now.Add(-20 * time.Minute),
+ UpdatedAt: now.Add(-20 * time.Minute),
+ ExpiresAt: now.Add(-1 * time.Minute),
+ })
+
+ mux := http.NewServeMux()
+ h.RegisterRoutes(mux)
+
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest(http.MethodGet, "/api/oauth/flows/expired-flow", nil)
+ mux.ServeHTTP(rec, req)
+ if rec.Code != http.StatusOK {
+ t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
+ }
+ var flowResp oauthFlowResponse
+ if err := json.Unmarshal(rec.Body.Bytes(), &flowResp); err != nil {
+ t.Fatalf("unmarshal flow response: %v", err)
+ }
+ if flowResp.Status != oauthFlowExpired {
+ t.Fatalf("flow status = %q, want %q", flowResp.Status, oauthFlowExpired)
+ }
+}
+
+func TestOAuthCallbackUnknownState(t *testing.T) {
+ configPath, cleanup := setupOAuthTestEnv(t)
+ defer cleanup()
+ resetOAuthHooks(t)
+
+ h := NewHandler(configPath)
+ mux := http.NewServeMux()
+ h.RegisterRoutes(mux)
+
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest(http.MethodGet, "/oauth/callback?state=unknown&code=abc", nil)
+ mux.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusBadRequest {
+ t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
+ }
+ if !strings.Contains(rec.Body.String(), "OAuth flow not found") {
+ t.Fatalf("unexpected body: %s", rec.Body.String())
+ }
+}
+
+func TestOAuthLogoutClearsCredentialAndConfig(t *testing.T) {
+ configPath, cleanup := setupOAuthTestEnv(t)
+ defer cleanup()
+ resetOAuthHooks(t)
+
+ cfg, err := config.LoadConfig(configPath)
+ if err != nil {
+ t.Fatalf("LoadConfig error: %v", err)
+ }
+ cfg.Providers.OpenAI.AuthMethod = "oauth"
+ cfg.ModelList = append(cfg.ModelList, config.ModelConfig{
+ ModelName: "gpt-5.2",
+ Model: "openai/gpt-5.2",
+ AuthMethod: "oauth",
+ })
+ if err = config.SaveConfig(configPath, cfg); err != nil {
+ t.Fatalf("SaveConfig error: %v", err)
+ }
+ if err = auth.SetCredential(oauthProviderOpenAI, &auth.AuthCredential{
+ AccessToken: "token-before-logout",
+ Provider: oauthProviderOpenAI,
+ AuthMethod: "oauth",
+ }); err != nil {
+ t.Fatalf("SetCredential error: %v", err)
+ }
+
+ h := NewHandler(configPath)
+ mux := http.NewServeMux()
+ h.RegisterRoutes(mux)
+
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest(http.MethodPost, "/api/oauth/logout", bytes.NewBufferString(`{"provider":"openai"}`))
+ req.Header.Set("Content-Type", "application/json")
+ mux.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
+ }
+
+ cred, err := auth.GetCredential(oauthProviderOpenAI)
+ if err != nil {
+ t.Fatalf("GetCredential error: %v", err)
+ }
+ if cred != nil {
+ t.Fatalf("expected credential deleted, got %#v", cred)
+ }
+
+ updated, err := config.LoadConfig(configPath)
+ if err != nil {
+ t.Fatalf("LoadConfig error: %v", err)
+ }
+ if updated.Providers.OpenAI.AuthMethod != "" {
+ t.Fatalf("providers.openai.auth_method = %q, want empty", updated.Providers.OpenAI.AuthMethod)
+ }
+ for _, m := range updated.ModelList {
+ if strings.HasPrefix(m.Model, "openai/") && m.AuthMethod != "" {
+ t.Fatalf("openai model auth_method = %q, want empty", m.AuthMethod)
+ }
+ }
+}
+
+func setupOAuthTestEnv(t *testing.T) (string, func()) {
+ t.Helper()
+
+ tmp := t.TempDir()
+ oldHome := os.Getenv("HOME")
+ oldPicoHome := os.Getenv("PICOCLAW_HOME")
+
+ if err := os.Setenv("HOME", tmp); err != nil {
+ t.Fatalf("set HOME: %v", err)
+ }
+ if err := os.Setenv("PICOCLAW_HOME", filepath.Join(tmp, ".picoclaw")); err != nil {
+ t.Fatalf("set PICOCLAW_HOME: %v", err)
+ }
+
+ cfg := config.DefaultConfig()
+ cfg.ModelList = []config.ModelConfig{{
+ ModelName: "custom-default",
+ Model: "openai/gpt-4o",
+ APIKey: "sk-default",
+ }}
+ cfg.Agents.Defaults.ModelName = "custom-default"
+
+ configPath := filepath.Join(tmp, "config.json")
+ if err := config.SaveConfig(configPath, cfg); err != nil {
+ t.Fatalf("SaveConfig error: %v", err)
+ }
+
+ cleanup := func() {
+ _ = os.Setenv("HOME", oldHome)
+ if oldPicoHome == "" {
+ _ = os.Unsetenv("PICOCLAW_HOME")
+ } else {
+ _ = os.Setenv("PICOCLAW_HOME", oldPicoHome)
+ }
+ }
+ return configPath, cleanup
+}
+
+func resetOAuthHooks(t *testing.T) {
+ t.Helper()
+
+ origNow := oauthNow
+ origGeneratePKCE := oauthGeneratePKCE
+ origGenerateState := oauthGenerateState
+ origBuildAuthorizeURL := oauthBuildAuthorizeURL
+ origRequestDeviceCode := oauthRequestDeviceCode
+ origPollDeviceCodeOnce := oauthPollDeviceCodeOnce
+ origExchangeCodeForTokens := oauthExchangeCodeForTokens
+ origGetCredential := oauthGetCredential
+ origSetCredential := oauthSetCredential
+ origDeleteCredential := oauthDeleteCredential
+ origLoadConfig := oauthLoadConfig
+ origSaveConfig := oauthSaveConfig
+ origFetchProject := oauthFetchAntigravityProject
+ origFetchGoogleEmail := oauthFetchGoogleUserEmailFunc
+
+ t.Cleanup(func() {
+ oauthNow = origNow
+ oauthGeneratePKCE = origGeneratePKCE
+ oauthGenerateState = origGenerateState
+ oauthBuildAuthorizeURL = origBuildAuthorizeURL
+ oauthRequestDeviceCode = origRequestDeviceCode
+ oauthPollDeviceCodeOnce = origPollDeviceCodeOnce
+ oauthExchangeCodeForTokens = origExchangeCodeForTokens
+ oauthGetCredential = origGetCredential
+ oauthSetCredential = origSetCredential
+ oauthDeleteCredential = origDeleteCredential
+ oauthLoadConfig = origLoadConfig
+ oauthSaveConfig = origSaveConfig
+ oauthFetchAntigravityProject = origFetchProject
+ oauthFetchGoogleUserEmailFunc = origFetchGoogleEmail
+ })
+}
diff --git a/web/backend/api/pico.go b/web/backend/api/pico.go
new file mode 100644
index 000000000..fc942d51c
--- /dev/null
+++ b/web/backend/api/pico.go
@@ -0,0 +1,161 @@
+package api
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "net"
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/sipeed/picoclaw/pkg/config"
+)
+
+// registerPicoRoutes binds Pico Channel management endpoints to the ServeMux.
+func (h *Handler) registerPicoRoutes(mux *http.ServeMux) {
+ mux.HandleFunc("GET /api/pico/token", h.handleGetPicoToken)
+ mux.HandleFunc("POST /api/pico/token", h.handleRegenPicoToken)
+ mux.HandleFunc("POST /api/pico/setup", h.handlePicoSetup)
+}
+
+// handleGetPicoToken returns the current WS token and URL for the frontend.
+//
+// GET /api/pico/token
+func (h *Handler) handleGetPicoToken(w http.ResponseWriter, r *http.Request) {
+ cfg, err := config.LoadConfig(h.configPath)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ wsURL := buildWsURL(r, cfg)
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]any{
+ "token": cfg.Channels.Pico.Token,
+ "ws_url": wsURL,
+ "enabled": cfg.Channels.Pico.Enabled,
+ })
+}
+
+// handleRegenPicoToken generates a new Pico WebSocket token and saves it.
+//
+// POST /api/pico/token
+func (h *Handler) handleRegenPicoToken(w http.ResponseWriter, r *http.Request) {
+ cfg, err := config.LoadConfig(h.configPath)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ token := generateSecureToken()
+ cfg.Channels.Pico.Token = token
+
+ if err := config.SaveConfig(h.configPath, cfg); err != nil {
+ http.Error(w, fmt.Sprintf("Failed to save config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ wsURL := fmt.Sprintf("ws://%s/pico/ws", net.JoinHostPort(cfg.Gateway.Host, strconv.Itoa(cfg.Gateway.Port)))
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]any{
+ "token": token,
+ "ws_url": wsURL,
+ })
+}
+
+// ensurePicoChannel checks if the Pico Channel is properly configured and
+// enables it with sensible defaults if not. Returns true if config was changed.
+func (h *Handler) ensurePicoChannel() (bool, error) {
+ cfg, err := config.LoadConfig(h.configPath)
+ if err != nil {
+ return false, fmt.Errorf("failed to load config: %w", err)
+ }
+
+ changed := false
+
+ if !cfg.Channels.Pico.Enabled {
+ cfg.Channels.Pico.Enabled = true
+ changed = true
+ }
+
+ if cfg.Channels.Pico.Token == "" {
+ cfg.Channels.Pico.Token = generateSecureToken()
+ changed = true
+ }
+
+ if !cfg.Channels.Pico.AllowTokenQuery {
+ cfg.Channels.Pico.AllowTokenQuery = true
+ changed = true
+ }
+
+ // Make sure origins are allowed (frontend might be running on a different port like 5173 during dev)
+ if len(cfg.Channels.Pico.AllowOrigins) == 0 {
+ cfg.Channels.Pico.AllowOrigins = []string{"*"}
+ changed = true
+ }
+
+ if changed {
+ if err := config.SaveConfig(h.configPath, cfg); err != nil {
+ return false, fmt.Errorf("failed to save config: %w", err)
+ }
+ }
+
+ return changed, nil
+}
+
+// handlePicoSetup automatically configures everything needed for the Pico Channel to work.
+//
+// POST /api/pico/setup
+func (h *Handler) handlePicoSetup(w http.ResponseWriter, r *http.Request) {
+ changed, err := h.ensurePicoChannel()
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ cfg, err := config.LoadConfig(h.configPath)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Failed to load config: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ wsURL := buildWsURL(r, cfg)
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]any{
+ "token": cfg.Channels.Pico.Token,
+ "ws_url": wsURL,
+ "enabled": true,
+ "changed": changed,
+ })
+}
+
+// buildWsURL creates a WebSocket URL for the Pico Channel.
+// When the gateway host is "0.0.0.0" or empty, it uses the hostname from the
+// incoming HTTP request so the browser gets a connectable address.
+func buildWsURL(r *http.Request, cfg *config.Config) string {
+ host := cfg.Gateway.Host
+ if host == "" || host == "0.0.0.0" {
+ // Use the hostname the browser used to reach this backend
+ reqHost, _, err := net.SplitHostPort(r.Host)
+ if err != nil {
+ reqHost = r.Host // r.Host might not have a port
+ }
+ host = reqHost
+ }
+ return "ws://" + net.JoinHostPort(host, strconv.Itoa(cfg.Gateway.Port)) + "/pico/ws"
+}
+
+// generateSecureToken creates a random 32-character hex string.
+func generateSecureToken() string {
+ b := make([]byte, 16)
+ if _, err := rand.Read(b); err != nil {
+ // Fallback to something pseudo-random if crypto/rand fails
+ return fmt.Sprintf("pico_%x", time.Now().UnixNano())
+ }
+ return hex.EncodeToString(b)
+}
diff --git a/web/backend/api/router.go b/web/backend/api/router.go
new file mode 100644
index 000000000..c250724d1
--- /dev/null
+++ b/web/backend/api/router.go
@@ -0,0 +1,66 @@
+package api
+
+import (
+ "net/http"
+ "sync"
+
+ "github.com/sipeed/picoclaw/web/backend/launcherconfig"
+)
+
+// Handler serves HTTP API requests.
+type Handler struct {
+ configPath string
+ serverPort int
+ serverPublic bool
+ serverCIDRs []string
+ oauthMu sync.Mutex
+ oauthFlows map[string]*oauthFlow
+ oauthState map[string]string
+}
+
+// NewHandler creates an instance of the API handler.
+func NewHandler(configPath string) *Handler {
+ return &Handler{
+ configPath: configPath,
+ serverPort: launcherconfig.DefaultPort,
+ oauthFlows: make(map[string]*oauthFlow),
+ oauthState: make(map[string]string),
+ }
+}
+
+// SetServerOptions stores current backend listen options for fallback behavior.
+func (h *Handler) SetServerOptions(port int, public bool, allowedCIDRs []string) {
+ h.serverPort = port
+ h.serverPublic = public
+ h.serverCIDRs = append([]string(nil), allowedCIDRs...)
+}
+
+// RegisterRoutes binds all API endpoint handlers to the ServeMux.
+func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
+ // Config CRUD
+ h.registerConfigRoutes(mux)
+
+ // Pico Channel (WebSocket chat)
+ h.registerPicoRoutes(mux)
+
+ // Gateway process lifecycle
+ h.registerGatewayRoutes(mux)
+
+ // Session history
+ h.registerSessionRoutes(mux)
+
+ // OAuth login and credential management
+ h.registerOAuthRoutes(mux)
+
+ // Model list management
+ h.registerModelRoutes(mux)
+
+ // Channel catalog (for frontend navigation/config pages)
+ h.registerChannelRoutes(mux)
+
+ // OS startup / launch-at-login
+ h.registerStartupRoutes(mux)
+
+ // Launcher service parameters (port/public)
+ h.registerLauncherConfigRoutes(mux)
+}
diff --git a/web/backend/api/session.go b/web/backend/api/session.go
new file mode 100644
index 000000000..e3cf674fc
--- /dev/null
+++ b/web/backend/api/session.go
@@ -0,0 +1,286 @@
+package api
+
+import (
+ "encoding/json"
+ "net/http"
+ "os"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/sipeed/picoclaw/pkg/config"
+ "github.com/sipeed/picoclaw/pkg/providers"
+)
+
+// registerSessionRoutes binds session list and detail endpoints to the ServeMux.
+func (h *Handler) registerSessionRoutes(mux *http.ServeMux) {
+ mux.HandleFunc("GET /api/sessions", h.handleListSessions)
+ mux.HandleFunc("GET /api/sessions/{id}", h.handleGetSession)
+ mux.HandleFunc("DELETE /api/sessions/{id}", h.handleDeleteSession)
+}
+
+// sessionFile mirrors the on-disk session JSON structure from pkg/session.
+type sessionFile struct {
+ Key string `json:"key"`
+ Messages []providers.Message `json:"messages"`
+ Summary string `json:"summary,omitempty"`
+ Created time.Time `json:"created"`
+ Updated time.Time `json:"updated"`
+}
+
+// sessionListItem is a lightweight summary returned by GET /api/sessions.
+type sessionListItem struct {
+ ID string `json:"id"`
+ Preview string `json:"preview"`
+ MessageCount int `json:"message_count"`
+ Created string `json:"created"`
+ Updated string `json:"updated"`
+}
+
+// picoSessionPrefix is the key prefix used by the gateway's routing for Pico
+// channel sessions. The full key format is:
+//
+// agent:main:pico:direct:pico:
+//
+// The sanitized filename replaces ':' with '_', so on disk it becomes:
+//
+// agent_main_pico_direct_pico_.json
+const picoSessionPrefix = "agent:main:pico:direct:pico:"
+
+// extractPicoSessionID extracts the session UUID from a full session key.
+// Returns the UUID and true if the key matches the Pico session pattern.
+func extractPicoSessionID(key string) (string, bool) {
+ if strings.HasPrefix(key, picoSessionPrefix) {
+ return strings.TrimPrefix(key, picoSessionPrefix), true
+ }
+ return "", false
+}
+
+// sessionsDir resolves the path to the gateway's session storage directory.
+// It reads the workspace from config, falling back to ~/.picoclaw/workspace.
+func (h *Handler) sessionsDir() (string, error) {
+ cfg, err := config.LoadConfig(h.configPath)
+ if err != nil {
+ return "", err
+ }
+
+ workspace := cfg.Agents.Defaults.Workspace
+ if workspace == "" {
+ home, _ := os.UserHomeDir()
+ workspace = filepath.Join(home, ".picoclaw", "workspace")
+ }
+
+ // Expand ~ prefix
+ if len(workspace) > 0 && workspace[0] == '~' {
+ home, _ := os.UserHomeDir()
+ if len(workspace) > 1 && workspace[1] == '/' {
+ workspace = home + workspace[1:]
+ } else {
+ workspace = home
+ }
+ }
+
+ return filepath.Join(workspace, "sessions"), nil
+}
+
+// handleListSessions returns a list of Pico session summaries.
+//
+// GET /api/sessions
+func (h *Handler) handleListSessions(w http.ResponseWriter, r *http.Request) {
+ dir, err := h.sessionsDir()
+ if err != nil {
+ http.Error(w, "failed to resolve sessions directory", http.StatusInternalServerError)
+ return
+ }
+
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ // Directory doesn't exist yet = no sessions
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode([]sessionListItem{})
+ return
+ }
+
+ items := []sessionListItem{}
+
+ for _, entry := range entries {
+ if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
+ continue
+ }
+
+ data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
+ if err != nil {
+ continue
+ }
+
+ var sess sessionFile
+ if err := json.Unmarshal(data, &sess); err != nil {
+ continue
+ }
+
+ // Only include Pico channel sessions
+ sessionID, ok := extractPicoSessionID(sess.Key)
+ if !ok {
+ continue
+ }
+
+ // Build a preview from the first user message
+ preview := ""
+ for _, msg := range sess.Messages {
+ if msg.Role == "user" && strings.TrimSpace(msg.Content) != "" {
+ preview = msg.Content
+ break
+ }
+ }
+ if len([]rune(preview)) > 60 {
+ preview = string([]rune(preview)[:60]) + "..."
+ }
+ if preview == "" {
+ preview = "(empty)"
+ }
+
+ // Only count non-empty user and assistant messages
+ validMessageCount := 0
+ for _, msg := range sess.Messages {
+ if (msg.Role == "user" || msg.Role == "assistant") && strings.TrimSpace(msg.Content) != "" {
+ validMessageCount++
+ }
+ }
+
+ items = append(items, sessionListItem{
+ ID: sessionID,
+ Preview: preview,
+ MessageCount: validMessageCount,
+ Created: sess.Created.Format(time.RFC3339),
+ Updated: sess.Updated.Format(time.RFC3339),
+ })
+ }
+
+ // Sort by updated descending (most recent first)
+ sort.Slice(items, func(i, j int) bool {
+ return items[i].Updated > items[j].Updated
+ })
+
+ // Pagination parameters
+ offsetStr := r.URL.Query().Get("offset")
+ limitStr := r.URL.Query().Get("limit")
+
+ offset := 0
+ limit := 20 // Default limit
+
+ if val, err := strconv.Atoi(offsetStr); err == nil && val >= 0 {
+ offset = val
+ }
+ if val, err := strconv.Atoi(limitStr); err == nil && val > 0 {
+ limit = val
+ }
+
+ totalItems := len(items)
+
+ end := offset + limit
+ if offset >= totalItems {
+ items = []sessionListItem{} // Out of bounds, return empty
+ } else {
+ if end > totalItems {
+ end = totalItems
+ }
+ items = items[offset:end]
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(items)
+}
+
+// handleGetSession returns the full message history for a specific session.
+//
+// GET /api/sessions/{id}
+func (h *Handler) handleGetSession(w http.ResponseWriter, r *http.Request) {
+ sessionID := r.PathValue("id")
+ if sessionID == "" {
+ http.Error(w, "missing session id", http.StatusBadRequest)
+ return
+ }
+
+ dir, err := h.sessionsDir()
+ if err != nil {
+ http.Error(w, "failed to resolve sessions directory", http.StatusInternalServerError)
+ return
+ }
+
+ // The sanitized filename replaces ':' with '_':
+ // agent:main:pico:direct:pico: -> agent_main_pico_direct_pico_.json
+ filename := strings.ReplaceAll(picoSessionPrefix+sessionID, ":", "_") + ".json"
+
+ data, err := os.ReadFile(filepath.Join(dir, filename))
+ if err != nil {
+ http.Error(w, "session not found", http.StatusNotFound)
+ return
+ }
+
+ var sess sessionFile
+ if err := json.Unmarshal(data, &sess); err != nil {
+ http.Error(w, "failed to parse session", http.StatusInternalServerError)
+ return
+ }
+
+ // Convert to a simpler format for the frontend
+ type chatMessage struct {
+ Role string `json:"role"`
+ Content string `json:"content"`
+ }
+
+ messages := make([]chatMessage, 0, len(sess.Messages))
+ for _, msg := range sess.Messages {
+ // Only include user and assistant messages that have actual content
+ if (msg.Role == "user" || msg.Role == "assistant") && strings.TrimSpace(msg.Content) != "" {
+ messages = append(messages, chatMessage{
+ Role: msg.Role,
+ Content: msg.Content,
+ })
+ }
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]any{
+ "id": sessionID,
+ "messages": messages,
+ "summary": sess.Summary,
+ "created": sess.Created.Format(time.RFC3339),
+ "updated": sess.Updated.Format(time.RFC3339),
+ })
+}
+
+// handleDeleteSession deletes a specific session.
+//
+// DELETE /api/sessions/{id}
+func (h *Handler) handleDeleteSession(w http.ResponseWriter, r *http.Request) {
+ sessionID := r.PathValue("id")
+ if sessionID == "" {
+ http.Error(w, "missing session id", http.StatusBadRequest)
+ return
+ }
+
+ dir, err := h.sessionsDir()
+ if err != nil {
+ http.Error(w, "failed to resolve sessions directory", http.StatusInternalServerError)
+ return
+ }
+
+ // The sanitized filename replaces ':' with '_':
+ // agent:main:pico:direct:pico: -> agent_main_pico_direct_pico_.json
+ filename := strings.ReplaceAll(picoSessionPrefix+sessionID, ":", "_") + ".json"
+ filePath := filepath.Join(dir, filename)
+
+ if err := os.Remove(filePath); err != nil {
+ if os.IsNotExist(err) {
+ http.Error(w, "session not found", http.StatusNotFound)
+ } else {
+ http.Error(w, "failed to delete session", http.StatusInternalServerError)
+ }
+ return
+ }
+
+ w.WriteHeader(http.StatusNoContent)
+}
diff --git a/web/backend/api/startup.go b/web/backend/api/startup.go
new file mode 100644
index 000000000..1c685bc90
--- /dev/null
+++ b/web/backend/api/startup.go
@@ -0,0 +1,305 @@
+package api
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+)
+
+const (
+ autoStartEntryName = "PicoClawLauncher"
+ launchAgentLabel = "io.picoclaw.launcher"
+)
+
+type autoStartRequest struct {
+ Enabled bool `json:"enabled"`
+}
+
+type autoStartResponse struct {
+ Enabled bool `json:"enabled"`
+ Supported bool `json:"supported"`
+ Platform string `json:"platform"`
+ Message string `json:"message,omitempty"`
+}
+
+var errAutoStartUnsupported = errors.New("autostart is not supported on this platform")
+
+func (h *Handler) registerStartupRoutes(mux *http.ServeMux) {
+ mux.HandleFunc("GET /api/system/autostart", h.handleGetAutoStart)
+ mux.HandleFunc("PUT /api/system/autostart", h.handleSetAutoStart)
+}
+
+func (h *Handler) handleGetAutoStart(w http.ResponseWriter, r *http.Request) {
+ enabled, supported, message, err := h.getAutoStartStatus()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Failed to read startup setting: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(autoStartResponse{
+ Enabled: enabled,
+ Supported: supported,
+ Platform: runtime.GOOS,
+ Message: message,
+ })
+}
+
+func (h *Handler) handleSetAutoStart(w http.ResponseWriter, r *http.Request) {
+ var req autoStartRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
+ return
+ }
+
+ if err := h.setAutoStart(req.Enabled); err != nil {
+ if errors.Is(err, errAutoStartUnsupported) {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ http.Error(w, fmt.Sprintf("Failed to update startup setting: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ enabled, supported, message, err := h.getAutoStartStatus()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Failed to verify startup setting: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(autoStartResponse{
+ Enabled: enabled,
+ Supported: supported,
+ Platform: runtime.GOOS,
+ Message: message,
+ })
+}
+
+func (h *Handler) resolveLaunchCommand() (string, []string, error) {
+ exePath, err := os.Executable()
+ if err != nil {
+ return "", nil, err
+ }
+
+ args := []string{"-no-browser"}
+ if h.configPath != "" {
+ args = append(args, h.configPath)
+ }
+
+ return exePath, args, nil
+}
+
+func (h *Handler) getAutoStartStatus() (enabled bool, supported bool, message string, err error) {
+ switch runtime.GOOS {
+ case "darwin":
+ exists, err := fileExists(macLaunchAgentPath())
+ return exists, true, "Changes apply on next login.", err
+ case "linux":
+ exists, err := fileExists(linuxAutoStartPath())
+ return exists, true, "Changes apply on next login.", err
+ case "windows":
+ exists, err := windowsRunKeyExists()
+ return exists, true, "Changes apply on next login.", err
+ default:
+ return false, false, "Current platform does not support launch at login.", nil
+ }
+}
+
+func (h *Handler) setAutoStart(enabled bool) error {
+ exePath, args, err := h.resolveLaunchCommand()
+ if err != nil {
+ return err
+ }
+
+ switch runtime.GOOS {
+ case "darwin":
+ return setDarwinAutoStart(enabled, exePath, args)
+ case "linux":
+ return setLinuxAutoStart(enabled, exePath, args)
+ case "windows":
+ return setWindowsAutoStart(enabled, exePath, args)
+ default:
+ return errAutoStartUnsupported
+ }
+}
+
+func fileExists(path string) (bool, error) {
+ _, err := os.Stat(path)
+ if err == nil {
+ return true, nil
+ }
+ if os.IsNotExist(err) {
+ return false, nil
+ }
+ return false, err
+}
+
+func macLaunchAgentPath() string {
+ home, _ := os.UserHomeDir()
+ return filepath.Join(home, "Library", "LaunchAgents", launchAgentLabel+".plist")
+}
+
+func setDarwinAutoStart(enabled bool, exePath string, args []string) error {
+ plistPath := macLaunchAgentPath()
+ if enabled {
+ if err := os.MkdirAll(filepath.Dir(plistPath), 0o755); err != nil {
+ return err
+ }
+ content := buildDarwinPlist(exePath, args)
+ return os.WriteFile(plistPath, []byte(content), 0o644)
+ }
+
+ if err := os.Remove(plistPath); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ return nil
+}
+
+func xmlEscape(s string) string {
+ var b bytes.Buffer
+ for _, r := range s {
+ switch r {
+ case '&':
+ b.WriteString("&")
+ case '<':
+ b.WriteString("<")
+ case '>':
+ b.WriteString(">")
+ case '"':
+ b.WriteString(""")
+ case '\'':
+ b.WriteString("'")
+ default:
+ b.WriteRune(r)
+ }
+ }
+ return b.String()
+}
+
+func buildDarwinPlist(exePath string, args []string) string {
+ programArgs := make([]string, 0, len(args)+1)
+ programArgs = append(programArgs, exePath)
+ programArgs = append(programArgs, args...)
+
+ var b strings.Builder
+ b.WriteString(`` + "\n")
+ b.WriteString(
+ `` + "\n",
+ )
+ b.WriteString(`` + "\n")
+ b.WriteString(`` + "\n")
+ b.WriteString(` Label` + "\n")
+ b.WriteString(` ` + launchAgentLabel + `` + "\n")
+ b.WriteString(` ProgramArguments` + "\n")
+ b.WriteString(` ` + "\n")
+ for _, arg := range programArgs {
+ b.WriteString(` ` + xmlEscape(arg) + `` + "\n")
+ }
+ b.WriteString(` ` + "\n")
+ b.WriteString(` RunAtLoad` + "\n")
+ b.WriteString(` ` + "\n")
+ b.WriteString(` ProcessType` + "\n")
+ b.WriteString(` Background` + "\n")
+ b.WriteString(`` + "\n")
+ b.WriteString(`` + "\n")
+ return b.String()
+}
+
+func linuxAutoStartPath() string {
+ home, _ := os.UserHomeDir()
+ return filepath.Join(home, ".config", "autostart", "picoclaw-web.desktop")
+}
+
+func shellQuote(s string) string {
+ if s == "" {
+ return "''"
+ }
+ if !strings.ContainsAny(s, " \t\n'\"\\$`") {
+ return s
+ }
+ return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'"
+}
+
+func buildLinuxExecLine(exePath string, args []string) string {
+ parts := make([]string, 0, len(args)+1)
+ parts = append(parts, shellQuote(exePath))
+ for _, arg := range args {
+ parts = append(parts, shellQuote(arg))
+ }
+ return strings.Join(parts, " ")
+}
+
+func setLinuxAutoStart(enabled bool, exePath string, args []string) error {
+ desktopPath := linuxAutoStartPath()
+ if enabled {
+ if err := os.MkdirAll(filepath.Dir(desktopPath), 0o755); err != nil {
+ return err
+ }
+ content := strings.Join([]string{
+ "[Desktop Entry]",
+ "Type=Application",
+ "Version=1.0",
+ "Name=PicoClaw Web",
+ "Comment=Start PicoClaw Web on login",
+ "Exec=" + buildLinuxExecLine(exePath, args),
+ "Terminal=false",
+ "X-GNOME-Autostart-enabled=true",
+ "NoDisplay=true",
+ "",
+ }, "\n")
+ return os.WriteFile(desktopPath, []byte(content), 0o644)
+ }
+
+ if err := os.Remove(desktopPath); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ return nil
+}
+
+func windowsCommandLine(exePath string, args []string) string {
+ parts := make([]string, 0, len(args)+1)
+ parts = append(parts, fmt.Sprintf("%q", exePath))
+ for _, arg := range args {
+ parts = append(parts, fmt.Sprintf("%q", arg))
+ }
+ return strings.Join(parts, " ")
+}
+
+func windowsRunKeyExists() (bool, error) {
+ cmd := exec.Command("reg", "query", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", autoStartEntryName)
+ if err := cmd.Run(); err != nil {
+ var exitErr *exec.ExitError
+ if errors.As(err, &exitErr) {
+ return false, nil
+ }
+ return false, err
+ }
+ return true, nil
+}
+
+func setWindowsAutoStart(enabled bool, exePath string, args []string) error {
+ key := `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`
+ if enabled {
+ commandLine := windowsCommandLine(exePath, args)
+ cmd := exec.Command("reg", "add", key, "/v", autoStartEntryName, "/t", "REG_SZ", "/d", commandLine, "/f")
+ return cmd.Run()
+ }
+
+ cmd := exec.Command("reg", "delete", key, "/v", autoStartEntryName, "/f")
+ if err := cmd.Run(); err != nil {
+ var exitErr *exec.ExitError
+ if errors.As(err, &exitErr) {
+ return nil
+ }
+ return err
+ }
+ return nil
+}
diff --git a/web/backend/api/startup_test.go b/web/backend/api/startup_test.go
new file mode 100644
index 000000000..cfa9b4c53
--- /dev/null
+++ b/web/backend/api/startup_test.go
@@ -0,0 +1,56 @@
+package api
+
+import (
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/sipeed/picoclaw/web/backend/launcherconfig"
+)
+
+func TestResolveLaunchCommandUsesConfigFileDefaults(t *testing.T) {
+ configPath := filepath.Join(t.TempDir(), "config.json")
+ h := NewHandler(configPath)
+
+ // Persist non-default launcher options to ensure resolveLaunchCommand does not
+ // pin them into autostart args.
+ launcherPath := launcherconfig.PathForAppConfig(configPath)
+ if err := launcherconfig.Save(launcherPath, launcherconfig.Config{
+ Port: 19999,
+ Public: true,
+ }); err != nil {
+ t.Fatalf("launcherconfig.Save() error = %v", err)
+ }
+
+ exePath, args, err := h.resolveLaunchCommand()
+ if err != nil {
+ t.Fatalf("resolveLaunchCommand() error = %v", err)
+ }
+ if exePath == "" {
+ t.Fatal("resolveLaunchCommand() returned empty executable path")
+ }
+ if len(args) != 2 {
+ t.Fatalf("args len = %d, want 2 (got %v)", len(args), args)
+ }
+ if args[0] != "-no-browser" {
+ t.Fatalf("args[0] = %q, want %q", args[0], "-no-browser")
+ }
+ if args[1] != configPath {
+ t.Fatalf("args[1] = %q, want %q", args[1], configPath)
+ }
+ for _, arg := range args {
+ if arg == "-port" || arg == "-public" {
+ t.Fatalf("autostart args should not pin network flags, got %v", args)
+ }
+ }
+}
+
+func TestBuildDarwinPlistIncludesRunAtLoad(t *testing.T) {
+ plist := buildDarwinPlist("/tmp/picoclaw-web", []string{"-no-browser", "/tmp/config.json"})
+ if !strings.Contains(plist, "RunAtLoad") {
+ t.Fatalf("plist missing RunAtLoad key:\n%s", plist)
+ }
+ if !strings.Contains(plist, "") {
+ t.Fatalf("plist missing RunAtLoad true value:\n%s", plist)
+ }
+}
diff --git a/web/backend/dist/.gitkeep b/web/backend/dist/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/web/backend/embed.go b/web/backend/embed.go
new file mode 100644
index 000000000..556fb7384
--- /dev/null
+++ b/web/backend/embed.go
@@ -0,0 +1,69 @@
+package main
+
+import (
+ "embed"
+ "io/fs"
+ "log"
+ "net/http"
+ "path"
+ "strings"
+)
+
+//go:embed all:dist
+var frontendFS embed.FS
+
+// registerEmbedRoutes sets up the HTTP handler to serve the embedded frontend files
+func registerEmbedRoutes(mux *http.ServeMux) {
+ // Attempt to get the subdirectory 'dist' where Vite usually builds
+ subFS, err := fs.Sub(frontendFS, "dist")
+ if err != nil {
+ // Log a warning if dist doesn't exist yet (e.g., during development before a frontend build)
+ log.Printf(
+ "Warning: no 'dist' folder found in embedded frontend. " +
+ "Ensure you run `pnpm build:backend` in the frontend directory " +
+ "before building the Go backend.",
+ )
+ return
+ }
+
+ fileServer := http.FileServer(http.FS(subFS))
+
+ // Serve static assets and fallback to index.html for SPA routes.
+ mux.Handle(
+ "/",
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet && r.Method != http.MethodHead {
+ http.NotFound(w, r)
+ return
+ }
+
+ // Keep unknown API paths as 404 instead of falling back to SPA entry.
+ if r.URL.Path == "/api" || strings.HasPrefix(r.URL.Path, "/api/") {
+ http.NotFound(w, r)
+ return
+ }
+
+ cleanPath := path.Clean(strings.TrimPrefix(r.URL.Path, "/"))
+ if cleanPath == "." {
+ cleanPath = ""
+ }
+
+ // Existing static files/directories should be served directly.
+ if cleanPath != "" {
+ if _, statErr := fs.Stat(subFS, cleanPath); statErr == nil {
+ fileServer.ServeHTTP(w, r)
+ return
+ }
+ // Missing asset-like paths should remain 404.
+ if strings.Contains(path.Base(cleanPath), ".") {
+ fileServer.ServeHTTP(w, r)
+ return
+ }
+ }
+
+ indexReq := r.Clone(r.Context())
+ indexReq.URL.Path = "/"
+ fileServer.ServeHTTP(w, indexReq)
+ }),
+ )
+}
diff --git a/web/backend/embed_test.go b/web/backend/embed_test.go
new file mode 100644
index 000000000..c0365488e
--- /dev/null
+++ b/web/backend/embed_test.go
@@ -0,0 +1,33 @@
+package main
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestUnknownAPIPathStays404(t *testing.T) {
+ mux := http.NewServeMux()
+ registerEmbedRoutes(mux)
+
+ req := httptest.NewRequest(http.MethodGet, "/api/not-found", nil)
+ rr := httptest.NewRecorder()
+ mux.ServeHTTP(rr, req)
+
+ if rr.Code != http.StatusNotFound {
+ t.Fatalf("status = %d, want %d", rr.Code, http.StatusNotFound)
+ }
+}
+
+func TestMissingAssetStays404(t *testing.T) {
+ mux := http.NewServeMux()
+ registerEmbedRoutes(mux)
+
+ req := httptest.NewRequest(http.MethodGet, "/assets/not-found.js", nil)
+ rr := httptest.NewRecorder()
+ mux.ServeHTTP(rr, req)
+
+ if rr.Code != http.StatusNotFound {
+ t.Fatalf("status = %d, want %d", rr.Code, http.StatusNotFound)
+ }
+}
diff --git a/cmd/picoclaw-launcher/icon.ico b/web/backend/icon.ico
similarity index 100%
rename from cmd/picoclaw-launcher/icon.ico
rename to web/backend/icon.ico
diff --git a/web/backend/launcherconfig/config.go b/web/backend/launcherconfig/config.go
new file mode 100644
index 000000000..4dca45b0e
--- /dev/null
+++ b/web/backend/launcherconfig/config.go
@@ -0,0 +1,113 @@
+package launcherconfig
+
+import (
+ "encoding/json"
+ "fmt"
+ "net"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+const (
+ // FileName is the launcher-specific settings file name.
+ FileName = "launcher-config.json"
+ // DefaultPort is the default port for the web launcher.
+ DefaultPort = 18800
+)
+
+// Config stores launch parameters for the web backend service.
+type Config struct {
+ Port int `json:"port"`
+ Public bool `json:"public"`
+ AllowedCIDRs []string `json:"allowed_cidrs,omitempty"`
+}
+
+// Default returns default launcher settings.
+func Default() Config {
+ return Config{Port: DefaultPort, Public: false}
+}
+
+// Validate checks if launcher settings are valid.
+func Validate(cfg Config) error {
+ if cfg.Port < 1 || cfg.Port > 65535 {
+ return fmt.Errorf("port %d is out of range (1-65535)", cfg.Port)
+ }
+ for _, cidr := range cfg.AllowedCIDRs {
+ if _, _, err := net.ParseCIDR(cidr); err != nil {
+ return fmt.Errorf("invalid CIDR %q", cidr)
+ }
+ }
+ return nil
+}
+
+// NormalizeCIDRs trims entries, removes empty values, and deduplicates CIDRs.
+func NormalizeCIDRs(cidrs []string) []string {
+ if len(cidrs) == 0 {
+ return nil
+ }
+ out := make([]string, 0, len(cidrs))
+ seen := make(map[string]struct{}, len(cidrs))
+ for _, raw := range cidrs {
+ trimmed := strings.TrimSpace(raw)
+ if trimmed == "" {
+ continue
+ }
+ if _, ok := seen[trimmed]; ok {
+ continue
+ }
+ seen[trimmed] = struct{}{}
+ out = append(out, trimmed)
+ }
+ if len(out) == 0 {
+ return nil
+ }
+ return out
+}
+
+// PathForAppConfig returns launcher-config path near the app config file.
+func PathForAppConfig(appConfigPath string) string {
+ dir := filepath.Dir(appConfigPath)
+ if dir == "" || dir == "." {
+ dir = "."
+ }
+ return filepath.Join(dir, FileName)
+}
+
+// Load reads launcher settings; fallback is returned when file does not exist.
+func Load(path string, fallback Config) (Config, error) {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return fallback, nil
+ }
+ return Config{}, err
+ }
+
+ cfg := fallback
+ if err := json.Unmarshal(data, &cfg); err != nil {
+ return Config{}, err
+ }
+ cfg.AllowedCIDRs = NormalizeCIDRs(cfg.AllowedCIDRs)
+ if err := Validate(cfg); err != nil {
+ return Config{}, err
+ }
+ return cfg, nil
+}
+
+// Save writes launcher settings to disk.
+func Save(path string, cfg Config) error {
+ cfg.AllowedCIDRs = NormalizeCIDRs(cfg.AllowedCIDRs)
+ if err := Validate(cfg); err != nil {
+ return err
+ }
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ return err
+ }
+ data, err := json.MarshalIndent(cfg, "", " ")
+ if err != nil {
+ return err
+ }
+ data = append(data, '\n')
+ return os.WriteFile(path, data, 0o600)
+}
diff --git a/web/backend/launcherconfig/config_test.go b/web/backend/launcherconfig/config_test.go
new file mode 100644
index 000000000..c63bee09a
--- /dev/null
+++ b/web/backend/launcherconfig/config_test.go
@@ -0,0 +1,89 @@
+package launcherconfig
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func TestLoadReturnsFallbackWhenMissing(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "launcher-config.json")
+ fallback := Config{Port: 19999, Public: true}
+
+ got, err := Load(path, fallback)
+ if err != nil {
+ t.Fatalf("Load() error = %v", err)
+ }
+ if got.Port != fallback.Port || got.Public != fallback.Public {
+ t.Fatalf("Load() = %+v, want %+v", got, fallback)
+ }
+}
+
+func TestSaveAndLoadRoundTrip(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "launcher-config.json")
+ want := Config{
+ Port: 18080,
+ Public: true,
+ AllowedCIDRs: []string{"192.168.1.0/24", "10.0.0.0/8"},
+ }
+
+ if err := Save(path, want); err != nil {
+ t.Fatalf("Save() error = %v", err)
+ }
+ got, err := Load(path, Default())
+ if err != nil {
+ t.Fatalf("Load() error = %v", err)
+ }
+ if got.Port != want.Port || got.Public != want.Public {
+ t.Fatalf("Load() = %+v, want %+v", got, want)
+ }
+ if len(got.AllowedCIDRs) != len(want.AllowedCIDRs) {
+ t.Fatalf("allowed_cidrs len = %d, want %d", len(got.AllowedCIDRs), len(want.AllowedCIDRs))
+ }
+ for i := range want.AllowedCIDRs {
+ if got.AllowedCIDRs[i] != want.AllowedCIDRs[i] {
+ t.Fatalf("allowed_cidrs[%d] = %q, want %q", i, got.AllowedCIDRs[i], want.AllowedCIDRs[i])
+ }
+ }
+
+ stat, err := os.Stat(path)
+ if err != nil {
+ t.Fatalf("Stat() error = %v", err)
+ }
+ if perm := stat.Mode().Perm(); perm != 0o600 {
+ t.Fatalf("file perm = %o, want 600", perm)
+ }
+}
+
+func TestValidateRejectsInvalidPort(t *testing.T) {
+ if err := Validate(Config{Port: 0, Public: false}); err == nil {
+ t.Fatal("Validate() expected error for port 0")
+ }
+ if err := Validate(Config{Port: 65536, Public: false}); err == nil {
+ t.Fatal("Validate() expected error for port 65536")
+ }
+}
+
+func TestValidateRejectsInvalidCIDR(t *testing.T) {
+ err := Validate(Config{
+ Port: 18800,
+ AllowedCIDRs: []string{"192.168.1.0/24", "not-a-cidr"},
+ })
+ if err == nil {
+ t.Fatal("Validate() expected error for invalid CIDR")
+ }
+}
+
+func TestNormalizeCIDRs(t *testing.T) {
+ got := NormalizeCIDRs([]string{" 192.168.1.0/24 ", "", "10.0.0.0/8", "192.168.1.0/24"})
+ want := []string{"192.168.1.0/24", "10.0.0.0/8"}
+ if len(got) != len(want) {
+ t.Fatalf("len(got) = %d, want %d", len(got), len(want))
+ }
+ for i := range want {
+ if got[i] != want[i] {
+ t.Fatalf("got[%d] = %q, want %q", i, got[i], want[i])
+ }
+ }
+}
diff --git a/web/backend/main.go b/web/backend/main.go
new file mode 100644
index 000000000..b8c4dc2bb
--- /dev/null
+++ b/web/backend/main.go
@@ -0,0 +1,164 @@
+// PicoClaw Web Console - Web-based chat and management interface
+//
+// Provides a web UI for chatting with PicoClaw via the Pico Channel WebSocket,
+// with configuration management and gateway process control.
+//
+// Usage:
+//
+// go build -o picoclaw-web ./web/backend/
+// ./picoclaw-web [config.json]
+// ./picoclaw-web -public config.json
+
+package main
+
+import (
+ "errors"
+ "flag"
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strconv"
+ "time"
+
+ "github.com/sipeed/picoclaw/web/backend/api"
+ "github.com/sipeed/picoclaw/web/backend/launcherconfig"
+ "github.com/sipeed/picoclaw/web/backend/middleware"
+)
+
+func main() {
+ port := flag.String("port", "18800", "Port to listen on")
+ public := flag.Bool("public", false, "Listen on all interfaces (0.0.0.0) instead of localhost only")
+ noBrowser := flag.Bool("no-browser", false, "Do not auto-open browser on startup")
+
+ flag.Usage = func() {
+ fmt.Fprintf(os.Stderr, "PicoClaw Launcher - A web-based configuration editor\n\n")
+ fmt.Fprintf(os.Stderr, "Usage: %s [options] [config.json]\n\n", os.Args[0])
+ fmt.Fprintf(os.Stderr, "Arguments:\n")
+ fmt.Fprintf(os.Stderr, " config.json Path to the configuration file (default: ~/.picoclaw/config.json)\n\n")
+ fmt.Fprintf(os.Stderr, "Options:\n")
+ flag.PrintDefaults()
+ fmt.Fprintf(os.Stderr, "\nExamples:\n")
+ fmt.Fprintf(os.Stderr, " %s Use default config path\n", os.Args[0])
+ fmt.Fprintf(os.Stderr, " %s ./config.json Specify a config file\n", os.Args[0])
+ fmt.Fprintf(
+ os.Stderr,
+ " %s -public ./config.json Allow access from other devices on the network\n",
+ os.Args[0],
+ )
+ }
+ flag.Parse()
+
+ // Resolve config path
+ configPath := getDefaultConfigPath()
+ if flag.NArg() > 0 {
+ configPath = flag.Arg(0)
+ }
+
+ absPath, err := filepath.Abs(configPath)
+ if err != nil {
+ log.Fatalf("Failed to resolve config path: %v", err)
+ }
+
+ var explicitPort bool
+ var explicitPublic bool
+ flag.Visit(func(f *flag.Flag) {
+ switch f.Name {
+ case "port":
+ explicitPort = true
+ case "public":
+ explicitPublic = true
+ }
+ })
+
+ launcherPath := launcherconfig.PathForAppConfig(absPath)
+ launcherCfg, err := launcherconfig.Load(launcherPath, launcherconfig.Default())
+ if err != nil {
+ log.Printf("Warning: Failed to load %s: %v", launcherPath, err)
+ launcherCfg = launcherconfig.Default()
+ }
+
+ effectivePort := *port
+ effectivePublic := *public
+ if !explicitPort {
+ effectivePort = strconv.Itoa(launcherCfg.Port)
+ }
+ if !explicitPublic {
+ effectivePublic = launcherCfg.Public
+ }
+
+ portNum, err := strconv.Atoi(effectivePort)
+ if err != nil || portNum < 1 || portNum > 65535 {
+ if err == nil {
+ err = errors.New("must be in range 1-65535")
+ }
+ log.Fatalf("Invalid port %q: %v", effectivePort, err)
+ }
+
+ // Determine listen address
+ var addr string
+ if effectivePublic {
+ addr = "0.0.0.0:" + effectivePort
+ } else {
+ addr = "127.0.0.1:" + effectivePort
+ }
+
+ // Initialize Server components
+ mux := http.NewServeMux()
+
+ // API Routes (e.g. /api/status)
+ apiHandler := api.NewHandler(absPath)
+ apiHandler.SetServerOptions(portNum, effectivePublic, launcherCfg.AllowedCIDRs)
+ apiHandler.RegisterRoutes(mux)
+
+ // Frontend Embedded Assets
+ registerEmbedRoutes(mux)
+
+ accessControlledMux, err := middleware.IPAllowlist(launcherCfg.AllowedCIDRs, mux)
+ if err != nil {
+ log.Fatalf("Invalid allowed CIDR configuration: %v", err)
+ }
+
+ // Apply middleware stack
+ handler := middleware.Recoverer(
+ middleware.Logger(
+ middleware.JSONContentType(accessControlledMux),
+ ),
+ )
+
+ // Print startup banner
+ fmt.Print(banner)
+ fmt.Println()
+ fmt.Println(" Open the following URL in your browser:")
+ fmt.Println()
+ fmt.Printf(" >> http://localhost:%s <<\n", effectivePort)
+ if effectivePublic {
+ if ip := getLocalIP(); ip != "" {
+ fmt.Printf(" >> http://%s:%s <<\n", ip, effectivePort)
+ }
+ }
+ fmt.Println()
+
+ // Auto-open browser
+ if !*noBrowser {
+ go func() {
+ time.Sleep(500 * time.Millisecond)
+ url := "http://localhost:" + effectivePort
+ if err := openBrowser(url); err != nil {
+ log.Printf("Warning: Failed to auto-open browser: %v", err)
+ }
+ }()
+ }
+
+ // Auto-start gateway after backend starts listening.
+ go func() {
+ time.Sleep(1 * time.Second)
+ apiHandler.TryAutoStartGateway()
+ }()
+
+ // Start the Server
+ if err := http.ListenAndServe(addr, handler); err != nil {
+ log.Fatalf("Server failed to start: %v", err)
+ }
+}
diff --git a/web/backend/middleware/access_control.go b/web/backend/middleware/access_control.go
new file mode 100644
index 000000000..159d60c3e
--- /dev/null
+++ b/web/backend/middleware/access_control.go
@@ -0,0 +1,64 @@
+package middleware
+
+import (
+ "fmt"
+ "net"
+ "net/http"
+ "strings"
+)
+
+// IPAllowlist restricts access to requests from configured CIDR ranges.
+// Loopback addresses are always allowed for local administration.
+// Empty CIDR list means no restriction.
+func IPAllowlist(allowedCIDRs []string, next http.Handler) (http.Handler, error) {
+ if len(allowedCIDRs) == 0 {
+ return next, nil
+ }
+
+ nets := make([]*net.IPNet, 0, len(allowedCIDRs))
+ for _, cidr := range allowedCIDRs {
+ _, ipNet, err := net.ParseCIDR(cidr)
+ if err != nil {
+ return nil, fmt.Errorf("invalid CIDR %q: %w", cidr, err)
+ }
+ nets = append(nets, ipNet)
+ }
+
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ ip := clientIPFromRemoteAddr(r.RemoteAddr)
+ if ip == nil {
+ rejectByPolicy(w, r)
+ return
+ }
+ if ip.IsLoopback() {
+ next.ServeHTTP(w, r)
+ return
+ }
+ for _, ipNet := range nets {
+ if ipNet.Contains(ip) {
+ next.ServeHTTP(w, r)
+ return
+ }
+ }
+
+ rejectByPolicy(w, r)
+ }), nil
+}
+
+func clientIPFromRemoteAddr(remoteAddr string) net.IP {
+ host := remoteAddr
+ if h, _, err := net.SplitHostPort(remoteAddr); err == nil {
+ host = h
+ }
+ return net.ParseIP(host)
+}
+
+func rejectByPolicy(w http.ResponseWriter, r *http.Request) {
+ if strings.HasPrefix(r.URL.Path, "/api/") {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusForbidden)
+ _, _ = w.Write([]byte(`{"error":"access denied by network policy"}`))
+ return
+ }
+ http.Error(w, "Forbidden", http.StatusForbidden)
+}
diff --git a/web/backend/middleware/access_control_test.go b/web/backend/middleware/access_control_test.go
new file mode 100644
index 000000000..259fd4a4c
--- /dev/null
+++ b/web/backend/middleware/access_control_test.go
@@ -0,0 +1,86 @@
+package middleware
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestIPAllowlist_EmptyCIDRsAllowsAll(t *testing.T) {
+ h, err := IPAllowlist(nil, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ }))
+ if err != nil {
+ t.Fatalf("IPAllowlist() error = %v", err)
+ }
+
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest(http.MethodGet, "/", nil)
+ req.RemoteAddr = "203.0.113.5:1234"
+ h.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
+ }
+}
+
+func TestIPAllowlist_RejectsOutsideCIDR(t *testing.T) {
+ h, err := IPAllowlist([]string{"192.168.1.0/24"}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ }))
+ if err != nil {
+ t.Fatalf("IPAllowlist() error = %v", err)
+ }
+
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest(http.MethodGet, "/api/config", nil)
+ req.RemoteAddr = "10.0.0.8:1234"
+ h.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusForbidden {
+ t.Fatalf("status = %d, want %d", rec.Code, http.StatusForbidden)
+ }
+}
+
+func TestIPAllowlist_AllowsInsideCIDR(t *testing.T) {
+ h, err := IPAllowlist([]string{"192.168.1.0/24"}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ }))
+ if err != nil {
+ t.Fatalf("IPAllowlist() error = %v", err)
+ }
+
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest(http.MethodGet, "/", nil)
+ req.RemoteAddr = "192.168.1.88:1234"
+ h.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
+ }
+}
+
+func TestIPAllowlist_AlwaysAllowsLoopback(t *testing.T) {
+ h, err := IPAllowlist([]string{"192.168.1.0/24"}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ }))
+ if err != nil {
+ t.Fatalf("IPAllowlist() error = %v", err)
+ }
+
+ rec := httptest.NewRecorder()
+ req := httptest.NewRequest(http.MethodGet, "/", nil)
+ req.RemoteAddr = "127.0.0.1:1234"
+ h.ServeHTTP(rec, req)
+
+ if rec.Code != http.StatusOK {
+ t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
+ }
+}
+
+func TestIPAllowlist_InvalidCIDR(t *testing.T) {
+ _, err := IPAllowlist([]string{"bad-cidr"}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
+ if err == nil {
+ t.Fatal("IPAllowlist() expected error for invalid CIDR")
+ }
+}
diff --git a/web/backend/middleware/middleware.go b/web/backend/middleware/middleware.go
new file mode 100644
index 000000000..de9e6d870
--- /dev/null
+++ b/web/backend/middleware/middleware.go
@@ -0,0 +1,70 @@
+package middleware
+
+import (
+ "log"
+ "net/http"
+ "runtime/debug"
+ "strings"
+ "time"
+)
+
+// JSONContentType sets the Content-Type header to application/json for
+// API requests handled by the wrapped handler.
+// SSE endpoints (text/event-stream) are excluded.
+func JSONContentType(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if strings.HasPrefix(r.URL.Path, "/api/") && !strings.HasSuffix(r.URL.Path, "/events") {
+ w.Header().Set("Content-Type", "application/json")
+ }
+ next.ServeHTTP(w, r)
+ })
+}
+
+// responseRecorder wraps http.ResponseWriter to capture the status code.
+type responseRecorder struct {
+ http.ResponseWriter
+ statusCode int
+}
+
+func (rr *responseRecorder) WriteHeader(code int) {
+ rr.statusCode = code
+ rr.ResponseWriter.WriteHeader(code)
+}
+
+// Flush delegates to the underlying ResponseWriter if it implements http.Flusher.
+// This is required for SSE (Server-Sent Events) to work through the middleware.
+func (rr *responseRecorder) Flush() {
+ if f, ok := rr.ResponseWriter.(http.Flusher); ok {
+ f.Flush()
+ }
+}
+
+// Unwrap returns the underlying ResponseWriter so that http.ResponseController
+// and interface checks (like http.Flusher) can see through the wrapper.
+func (rr *responseRecorder) Unwrap() http.ResponseWriter {
+ return rr.ResponseWriter
+}
+
+// Logger logs each HTTP request with method, path, status code, and duration.
+func Logger(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ start := time.Now()
+ rec := &responseRecorder{ResponseWriter: w, statusCode: http.StatusOK}
+ next.ServeHTTP(rec, r)
+ log.Printf("%s %s %d %s", r.Method, r.URL.Path, rec.statusCode, time.Since(start))
+ })
+}
+
+// Recoverer recovers from panics in downstream handlers and returns a 500
+// Internal Server Error response.
+func Recoverer(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ defer func() {
+ if err := recover(); err != nil {
+ log.Printf("panic recovered: %v\n%s", err, debug.Stack())
+ http.Error(w, `{"error":"internal server error"}`, http.StatusInternalServerError)
+ }
+ }()
+ next.ServeHTTP(w, r)
+ })
+}
diff --git a/web/backend/model/status.go b/web/backend/model/status.go
new file mode 100644
index 000000000..325981502
--- /dev/null
+++ b/web/backend/model/status.go
@@ -0,0 +1,8 @@
+package model
+
+// StatusResponse represents the response payload for the GET /api/status endpoint.
+type StatusResponse struct {
+ Status string `json:"status"`
+ Version string `json:"version"`
+ Uptime string `json:"uptime"`
+}
diff --git a/web/backend/utils.go b/web/backend/utils.go
new file mode 100644
index 000000000..6fa734aeb
--- /dev/null
+++ b/web/backend/utils.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+ "fmt"
+ "net"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+)
+
+const (
+ colorBlue = "\x1b[38;2;62;93;185m"
+ colorRed = "\x1b[38;2;213;70;70m"
+ colorReset = "\x1b[0m"
+ banner = "\r\n" +
+ colorBlue + "██████╗ ██╗ ██████╗ ██████╗ " + colorRed + " ██████╗██╗ █████╗ ██╗ ██╗\n" +
+ colorBlue + "██╔══██╗██║██╔════╝██╔═══██╗" + colorRed + "██╔════╝██║ ██╔══██╗██║ ██║\n" +
+ colorBlue + "██████╔╝██║██║ ██║ ██║" + colorRed + "██║ ██║ ███████║██║ █╗ ██║\n" +
+ colorBlue + "██╔═══╝ ██║██║ ██║ ██║" + colorRed + "██║ ██║ ██╔══██║██║███╗██║\n" +
+ colorBlue + "██║ ██║╚██████╗╚██████╔╝" + colorRed + "╚██████╗███████╗██║ ██║╚███╔███╔╝\n" +
+ colorBlue + "╚═╝ ╚═╝ ╚═════╝ ╚═════╝ " + colorRed + " ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝\n" +
+ colorReset
+)
+
+// getDefaultConfigPath returns the default path to the picoclaw config file.
+func getDefaultConfigPath() string {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return "config.json"
+ }
+ return filepath.Join(home, ".picoclaw", "config.json")
+}
+
+// getLocalIP returns the local IP address of the machine.
+func getLocalIP() string {
+ addrs, err := net.InterfaceAddrs()
+ if err != nil {
+ return ""
+ }
+ for _, a := range addrs {
+ if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil {
+ return ipnet.IP.String()
+ }
+ }
+ return ""
+}
+
+// openBrowser automatically opens the given URL in the default browser.
+func openBrowser(url string) error {
+ switch runtime.GOOS {
+ case "linux":
+ return exec.Command("xdg-open", url).Start()
+ case "windows":
+ return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
+ case "darwin":
+ return exec.Command("open", url).Start()
+ default:
+ return fmt.Errorf("unsupported platform")
+ }
+}
diff --git a/cmd/picoclaw-launcher/winres/winres.json b/web/backend/winres/winres.json
similarity index 100%
rename from cmd/picoclaw-launcher/winres/winres.json
rename to web/backend/winres/winres.json
diff --git a/web/frontend/.editorconfig b/web/frontend/.editorconfig
new file mode 100644
index 000000000..a8c0f1ecf
--- /dev/null
+++ b/web/frontend/.editorconfig
@@ -0,0 +1,7 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
\ No newline at end of file
diff --git a/web/frontend/.gitignore b/web/frontend/.gitignore
new file mode 100644
index 000000000..4811cdd9b
--- /dev/null
+++ b/web/frontend/.gitignore
@@ -0,0 +1,26 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+.tanstack
\ No newline at end of file
diff --git a/web/frontend/.prettierignore b/web/frontend/.prettierignore
new file mode 100644
index 000000000..7040bf59e
--- /dev/null
+++ b/web/frontend/.prettierignore
@@ -0,0 +1,5 @@
+package-lock.json
+pnpm-lock.yaml
+yarn.lock
+routeTree.gen.ts
+src/components/ui
\ No newline at end of file
diff --git a/web/frontend/components.json b/web/frontend/components.json
new file mode 100644
index 000000000..9d5329694
--- /dev/null
+++ b/web/frontend/components.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "radix-vega",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/index.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "tabler",
+ "rtl": false,
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "menuColor": "default",
+ "menuAccent": "subtle",
+ "registries": {}
+}
diff --git a/web/frontend/eslint.config.js b/web/frontend/eslint.config.js
new file mode 100644
index 000000000..bc9c64344
--- /dev/null
+++ b/web/frontend/eslint.config.js
@@ -0,0 +1,31 @@
+import js from "@eslint/js"
+import eslintConfigPrettier from "eslint-config-prettier"
+import reactHooks from "eslint-plugin-react-hooks"
+import reactRefresh from "eslint-plugin-react-refresh"
+import { defineConfig, globalIgnores } from "eslint/config"
+import globals from "globals"
+import tseslint from "typescript-eslint"
+
+export default defineConfig([
+ globalIgnores(["dist", "src/components/ui", "src/routeTree.gen.ts"]),
+ {
+ files: ["**/*.{ts,tsx}"],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ eslintConfigPrettier,
+ ],
+ languageOptions: {
+ ecmaVersion: "latest",
+ globals: globals.browser,
+ },
+ rules: {
+ "react-refresh/only-export-components": [
+ "warn",
+ { allowConstantExport: true },
+ ],
+ },
+ },
+])
diff --git a/web/frontend/index.html b/web/frontend/index.html
new file mode 100644
index 000000000..d3bdd90f8
--- /dev/null
+++ b/web/frontend/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+ PicoClaw
+
+
+
+
+
+
+
diff --git a/web/frontend/package.json b/web/frontend/package.json
new file mode 100644
index 000000000..ee46cdcda
--- /dev/null
+++ b/web/frontend/package.json
@@ -0,0 +1,62 @@
+{
+ "name": "picoclaw-web",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "build:backend": "tsc -b && vite build --outDir ../backend/dist --emptyOutDir",
+ "lint": "eslint .",
+ "preview": "vite preview",
+ "format": "prettier --check .",
+ "check": "prettier --write . && eslint --fix"
+ },
+ "dependencies": {
+ "@fontsource-variable/inter": "^5.2.8",
+ "@tabler/icons-react": "^3.38.0",
+ "@tailwindcss/vite": "^4.2.1",
+ "@tanstack/react-query": "^5.90.21",
+ "@tanstack/react-router": "^1.163.3",
+ "@tanstack/react-router-devtools": "^1.163.3",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "dayjs": "^1.11.19",
+ "i18next": "^25.8.14",
+ "i18next-browser-languagedetector": "^8.2.1",
+ "jotai": "^2.18.0",
+ "radix-ui": "^1.4.3",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "react-i18next": "^16.5.4",
+ "react-markdown": "^10.1.0",
+ "react-textarea-autosize": "^8.5.9",
+ "remark-gfm": "^4.0.1",
+ "shadcn": "^3.8.5",
+ "sonner": "^2.0.7",
+ "tailwind-merge": "^3.5.0",
+ "tailwindcss": "^4.2.1",
+ "tw-animate-css": "^1.4.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@tailwindcss/typography": "^0.5.19",
+ "@tanstack/router-plugin": "^1.164.0",
+ "@trivago/prettier-plugin-sort-imports": "^6.0.2",
+ "@types/node": "^24.10.1",
+ "@types/react": "^19.2.7",
+ "@types/react-dom": "^19.2.3",
+ "@typescript-eslint/eslint-plugin": "^8.56.1",
+ "@vitejs/plugin-react": "^5.1.1",
+ "eslint": "^9.39.1",
+ "eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "prettier": "^3.8.1",
+ "prettier-plugin-tailwindcss": "^0.7.2",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.48.0",
+ "vite": "^7.3.1"
+ }
+}
diff --git a/web/frontend/pnpm-lock.yaml b/web/frontend/pnpm-lock.yaml
new file mode 100644
index 000000000..8e89cbbe5
--- /dev/null
+++ b/web/frontend/pnpm-lock.yaml
@@ -0,0 +1,7981 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@fontsource-variable/inter':
+ specifier: ^5.2.8
+ version: 5.2.8
+ '@tabler/icons-react':
+ specifier: ^3.38.0
+ version: 3.38.0(react@19.2.4)
+ '@tailwindcss/vite':
+ specifier: ^4.2.1
+ version: 4.2.1(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))
+ '@tanstack/react-query':
+ specifier: ^5.90.21
+ version: 5.90.21(react@19.2.4)
+ '@tanstack/react-router':
+ specifier: ^1.163.3
+ version: 1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@tanstack/react-router-devtools':
+ specifier: ^1.163.3
+ version: 1.163.3(@tanstack/react-router@1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.163.3)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ class-variance-authority:
+ specifier: ^0.7.1
+ version: 0.7.1
+ clsx:
+ specifier: ^2.1.1
+ version: 2.1.1
+ dayjs:
+ specifier: ^1.11.19
+ version: 1.11.19
+ i18next:
+ specifier: ^25.8.14
+ version: 25.8.14(typescript@5.9.3)
+ i18next-browser-languagedetector:
+ specifier: ^8.2.1
+ version: 8.2.1
+ jotai:
+ specifier: ^2.18.0
+ version: 2.18.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4)
+ radix-ui:
+ specifier: ^1.4.3
+ version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react:
+ specifier: ^19.2.0
+ version: 19.2.4
+ react-dom:
+ specifier: ^19.2.0
+ version: 19.2.4(react@19.2.4)
+ react-i18next:
+ specifier: ^16.5.4
+ version: 16.5.4(i18next@25.8.14(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
+ react-markdown:
+ specifier: ^10.1.0
+ version: 10.1.0(@types/react@19.2.14)(react@19.2.4)
+ react-textarea-autosize:
+ specifier: ^8.5.9
+ version: 8.5.9(@types/react@19.2.14)(react@19.2.4)
+ remark-gfm:
+ specifier: ^4.0.1
+ version: 4.0.1
+ shadcn:
+ specifier: ^3.8.5
+ version: 3.8.5(@types/node@24.11.0)(typescript@5.9.3)
+ sonner:
+ specifier: ^2.0.7
+ version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ tailwind-merge:
+ specifier: ^3.5.0
+ version: 3.5.0
+ tailwindcss:
+ specifier: ^4.2.1
+ version: 4.2.1
+ tw-animate-css:
+ specifier: ^1.4.0
+ version: 1.4.0
+ devDependencies:
+ '@eslint/js':
+ specifier: ^9.39.1
+ version: 9.39.3
+ '@tailwindcss/typography':
+ specifier: ^0.5.19
+ version: 0.5.19(tailwindcss@4.2.1)
+ '@tanstack/router-plugin':
+ specifier: ^1.164.0
+ version: 1.164.0(@tanstack/react-router@1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))
+ '@trivago/prettier-plugin-sort-imports':
+ specifier: ^6.0.2
+ version: 6.0.2(prettier@3.8.1)
+ '@types/node':
+ specifier: ^24.10.1
+ version: 24.11.0
+ '@types/react':
+ specifier: ^19.2.7
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.3(@types/react@19.2.14)
+ '@typescript-eslint/eslint-plugin':
+ specifier: ^8.56.1
+ version: 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ '@vitejs/plugin-react':
+ specifier: ^5.1.1
+ version: 5.1.4(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))
+ eslint:
+ specifier: ^9.39.1
+ version: 9.39.3(jiti@2.6.1)
+ eslint-config-prettier:
+ specifier: ^10.1.8
+ version: 10.1.8(eslint@9.39.3(jiti@2.6.1))
+ eslint-plugin-react-hooks:
+ specifier: ^7.0.1
+ version: 7.0.1(eslint@9.39.3(jiti@2.6.1))
+ eslint-plugin-react-refresh:
+ specifier: ^0.4.24
+ version: 0.4.26(eslint@9.39.3(jiti@2.6.1))
+ globals:
+ specifier: ^16.5.0
+ version: 16.5.0
+ prettier:
+ specifier: ^3.8.1
+ version: 3.8.1
+ prettier-plugin-tailwindcss:
+ specifier: ^0.7.2
+ version: 0.7.2(@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.1))(prettier@3.8.1)
+ typescript:
+ specifier: ~5.9.3
+ version: 5.9.3
+ typescript-eslint:
+ specifier: ^8.48.0
+ version: 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ vite:
+ specifier: ^7.3.1
+ version: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)
+
+packages:
+
+ '@antfu/ni@25.0.0':
+ resolution: {integrity: sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA==}
+ hasBin: true
+
+ '@babel/code-frame@7.29.0':
+ resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.29.0':
+ resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.29.0':
+ resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.29.1':
+ resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-annotate-as-pure@7.27.3':
+ resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.28.6':
+ resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-create-class-features-plugin@7.28.6':
+ resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-globals@7.28.0':
+ resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-member-expression-to-functions@7.28.5':
+ resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.28.6':
+ resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.28.6':
+ resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-optimise-call-expression@7.27.1':
+ resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-plugin-utils@7.28.6':
+ resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-replace-supers@7.28.6':
+ resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-skip-transparent-expression-wrappers@7.27.1':
+ resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.27.1':
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.28.6':
+ resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.0':
+ resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-syntax-jsx@7.28.6':
+ resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-typescript@7.28.6':
+ resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-modules-commonjs@7.28.6':
+ resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-jsx-self@7.27.1':
+ resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-jsx-source@7.27.1':
+ resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-typescript@7.28.6':
+ resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/preset-typescript@7.28.5':
+ resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/runtime@7.28.6':
+ resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/template@7.28.6':
+ resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.29.0':
+ resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.29.0':
+ resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
+ engines: {node: '>=6.9.0'}
+
+ '@dotenvx/dotenvx@1.52.0':
+ resolution: {integrity: sha512-CaQcc8JvtzQhUSm9877b6V4Tb7HCotkcyud9X2YwdqtQKwgljkMRwU96fVYKnzN3V0Hj74oP7Es+vZ0mS+Aa1w==}
+ hasBin: true
+
+ '@ecies/ciphers@0.2.5':
+ resolution: {integrity: sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A==}
+ engines: {bun: '>=1', deno: '>=2', node: '>=16'}
+ peerDependencies:
+ '@noble/ciphers': ^1.0.0
+
+ '@esbuild/aix-ppc64@0.27.3':
+ resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.27.3':
+ resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.27.3':
+ resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.27.3':
+ resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.27.3':
+ resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.27.3':
+ resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.27.3':
+ resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.27.3':
+ resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.27.3':
+ resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.27.3':
+ resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.27.3':
+ resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.27.3':
+ resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.27.3':
+ resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.27.3':
+ resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.27.3':
+ resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.27.3':
+ resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.27.3':
+ resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.27.3':
+ resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.27.3':
+ resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.27.3':
+ resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.27.3':
+ resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.27.3':
+ resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.27.3':
+ resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.27.3':
+ resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.27.3':
+ resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.27.3':
+ resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@eslint-community/eslint-utils@4.9.1':
+ resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+ '@eslint-community/regexpp@4.12.2':
+ resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ '@eslint/config-array@0.21.1':
+ resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/config-helpers@0.4.2':
+ resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/core@0.17.0':
+ resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/eslintrc@3.3.4':
+ resolution: {integrity: sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/js@9.39.3':
+ resolution: {integrity: sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/object-schema@2.1.7':
+ resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/plugin-kit@0.4.1':
+ resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@floating-ui/core@1.7.4':
+ resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==}
+
+ '@floating-ui/dom@1.7.5':
+ resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==}
+
+ '@floating-ui/react-dom@2.1.7':
+ resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
+ '@floating-ui/utils@0.2.10':
+ resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
+
+ '@fontsource-variable/inter@5.2.8':
+ resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==}
+
+ '@hono/node-server@1.19.9':
+ resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==}
+ engines: {node: '>=18.14.1'}
+ peerDependencies:
+ hono: ^4
+
+ '@humanfs/core@0.19.1':
+ resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/node@0.16.7':
+ resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanwhocodes/module-importer@1.0.1':
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+
+ '@humanwhocodes/retry@0.4.3':
+ resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
+ engines: {node: '>=18.18'}
+
+ '@inquirer/ansi@1.0.2':
+ resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==}
+ engines: {node: '>=18'}
+
+ '@inquirer/confirm@5.1.21':
+ resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@inquirer/core@10.3.2':
+ resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@inquirer/figures@1.0.15':
+ resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==}
+ engines: {node: '>=18'}
+
+ '@inquirer/type@3.0.10':
+ resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@types/node': '>=18'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@modelcontextprotocol/sdk@1.27.1':
+ resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@cfworker/json-schema': ^4.1.1
+ zod: ^3.25 || ^4.0
+ peerDependenciesMeta:
+ '@cfworker/json-schema':
+ optional: true
+
+ '@mswjs/interceptors@0.41.3':
+ resolution: {integrity: sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==}
+ engines: {node: '>=18'}
+
+ '@noble/ciphers@1.3.0':
+ resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==}
+ engines: {node: ^14.21.3 || >=16}
+
+ '@noble/curves@1.9.7':
+ resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==}
+ engines: {node: ^14.21.3 || >=16}
+
+ '@noble/hashes@1.8.0':
+ resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
+ engines: {node: ^14.21.3 || >=16}
+
+ '@nodelib/fs.scandir@2.1.5':
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.stat@2.0.5':
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.walk@1.2.8':
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+ engines: {node: '>= 8'}
+
+ '@open-draft/deferred-promise@2.2.0':
+ resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==}
+
+ '@open-draft/logger@0.3.0':
+ resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==}
+
+ '@open-draft/until@2.1.0':
+ resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
+
+ '@radix-ui/number@1.1.1':
+ resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
+
+ '@radix-ui/primitive@1.1.3':
+ resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
+
+ '@radix-ui/react-accessible-icon@1.1.7':
+ resolution: {integrity: sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-accordion@1.2.12':
+ resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-alert-dialog@1.1.15':
+ resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-arrow@1.1.7':
+ resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-aspect-ratio@1.1.7':
+ resolution: {integrity: sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-avatar@1.1.10':
+ resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-checkbox@1.3.3':
+ resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-collapsible@1.1.12':
+ resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-collection@1.1.7':
+ resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-compose-refs@1.1.2':
+ resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-context-menu@2.2.16':
+ resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-context@1.1.2':
+ resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-dialog@1.1.15':
+ resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-direction@1.1.1':
+ resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-dismissable-layer@1.1.11':
+ resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-dropdown-menu@2.1.16':
+ resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-focus-guards@1.1.3':
+ resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-focus-scope@1.1.7':
+ resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-form@0.1.8':
+ resolution: {integrity: sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-hover-card@1.1.15':
+ resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-id@1.1.1':
+ resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-label@2.1.7':
+ resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-menu@2.1.16':
+ resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-menubar@1.1.16':
+ resolution: {integrity: sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-navigation-menu@1.2.14':
+ resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-one-time-password-field@0.1.8':
+ resolution: {integrity: sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-password-toggle-field@0.1.3':
+ resolution: {integrity: sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-popover@1.1.15':
+ resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-popper@1.2.8':
+ resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-portal@1.1.9':
+ resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-presence@1.1.5':
+ resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-primitive@2.1.3':
+ resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-progress@1.1.7':
+ resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-radio-group@1.3.8':
+ resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-roving-focus@1.1.11':
+ resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-scroll-area@1.2.10':
+ resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-select@2.2.6':
+ resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-separator@1.1.7':
+ resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-slider@1.3.6':
+ resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-slot@1.2.3':
+ resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-switch@1.2.6':
+ resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-tabs@1.1.13':
+ resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-toast@1.2.15':
+ resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-toggle-group@1.1.11':
+ resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-toggle@1.1.10':
+ resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-toolbar@1.1.11':
+ resolution: {integrity: sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-tooltip@1.2.8':
+ resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-use-callback-ref@1.1.1':
+ resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-controllable-state@1.2.2':
+ resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-effect-event@0.0.2':
+ resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-escape-keydown@1.1.1':
+ resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-is-hydrated@0.1.0':
+ resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-layout-effect@1.1.1':
+ resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-previous@1.1.1':
+ resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-rect@1.1.1':
+ resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-size@1.1.1':
+ resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-visually-hidden@1.2.3':
+ resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/rect@1.1.1':
+ resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
+
+ '@rolldown/pluginutils@1.0.0-rc.3':
+ resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==}
+
+ '@rollup/rollup-android-arm-eabi@4.59.0':
+ resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.59.0':
+ resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.59.0':
+ resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.59.0':
+ resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.59.0':
+ resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.59.0':
+ resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.59.0':
+ resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.59.0':
+ resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
+ cpu: [arm]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-arm64-gnu@4.59.0':
+ resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-arm64-musl@4.59.0':
+ resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-loong64-gnu@4.59.0':
+ resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
+ cpu: [loong64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-loong64-musl@4.59.0':
+ resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
+ cpu: [loong64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.59.0':
+ resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-ppc64-musl@4.59.0':
+ resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.59.0':
+ resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-riscv64-musl@4.59.0':
+ resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-s390x-gnu@4.59.0':
+ resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-x64-gnu@4.59.0':
+ resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-x64-musl@4.59.0':
+ resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-openbsd-x64@4.59.0':
+ resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@rollup/rollup-openharmony-arm64@4.59.0':
+ resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rollup/rollup-win32-arm64-msvc@4.59.0':
+ resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.59.0':
+ resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-gnu@4.59.0':
+ resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.59.0':
+ resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==}
+ cpu: [x64]
+ os: [win32]
+
+ '@sec-ant/readable-stream@0.4.1':
+ resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
+
+ '@sindresorhus/merge-streams@4.0.0':
+ resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
+ engines: {node: '>=18'}
+
+ '@tabler/icons-react@3.38.0':
+ resolution: {integrity: sha512-kR5wv+m4+GgmnSszg3rQd6SrTFAQ/XnQC/yTwIfuRJSfqB12KoIC7fPbIijFgOHTFlBN5DARnN0IVrR7KYG6/A==}
+ peerDependencies:
+ react: '>= 16'
+
+ '@tabler/icons@3.38.0':
+ resolution: {integrity: sha512-FdETQSpQ3lN7BEjEUzjKhsfTDCamrvMDops4HEMphTm3DmkIFpThoODn8XXZ8Q9MhjshIvphIYVHHB7zpq167w==}
+
+ '@tailwindcss/node@4.2.1':
+ resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==}
+
+ '@tailwindcss/oxide-android-arm64@4.2.1':
+ resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [android]
+
+ '@tailwindcss/oxide-darwin-arm64@4.2.1':
+ resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-darwin-x64@4.2.1':
+ resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-freebsd-x64@4.2.1':
+ resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1':
+ resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==}
+ engines: {node: '>= 20'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.2.1':
+ resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.2.1':
+ resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.2.1':
+ resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.2.1':
+ resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@tailwindcss/oxide-wasm32-wasi@4.2.1':
+ resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+ bundledDependencies:
+ - '@napi-rs/wasm-runtime'
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ - '@tybys/wasm-util'
+ - '@emnapi/wasi-threads'
+ - tslib
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.2.1':
+ resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.2.1':
+ resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.2.1':
+ resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==}
+ engines: {node: '>= 20'}
+
+ '@tailwindcss/typography@0.5.19':
+ resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==}
+ peerDependencies:
+ tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
+
+ '@tailwindcss/vite@4.2.1':
+ resolution: {integrity: sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==}
+ peerDependencies:
+ vite: ^5.2.0 || ^6 || ^7
+
+ '@tanstack/history@1.161.4':
+ resolution: {integrity: sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww==}
+ engines: {node: '>=20.19'}
+
+ '@tanstack/query-core@5.90.20':
+ resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==}
+
+ '@tanstack/react-query@5.90.21':
+ resolution: {integrity: sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==}
+ peerDependencies:
+ react: ^18 || ^19
+
+ '@tanstack/react-router-devtools@1.163.3':
+ resolution: {integrity: sha512-42VMkV/2Z8ro7xzblPBRNZIEmCNXMzm2jD68G52p2qhjXm38wGpg46qneAESN9FtTQeVWk5aSXs47/jt7lkzmw==}
+ engines: {node: '>=20.19'}
+ peerDependencies:
+ '@tanstack/react-router': ^1.163.3
+ '@tanstack/router-core': ^1.163.3
+ react: '>=18.0.0 || >=19.0.0'
+ react-dom: '>=18.0.0 || >=19.0.0'
+ peerDependenciesMeta:
+ '@tanstack/router-core':
+ optional: true
+
+ '@tanstack/react-router@1.163.3':
+ resolution: {integrity: sha512-hheBbFVb+PbxtrWp8iy6+TTRTbhx3Pn6hKo8Tv/sWlG89ZMcD1xpQWzx8ukHN9K8YWbh5rdzt4kv6u8X4kB28Q==}
+ engines: {node: '>=20.19'}
+ peerDependencies:
+ react: '>=18.0.0 || >=19.0.0'
+ react-dom: '>=18.0.0 || >=19.0.0'
+
+ '@tanstack/react-store@0.9.1':
+ resolution: {integrity: sha512-YzJLnRvy5lIEFTLWBAZmcOjK3+2AepnBv/sr6NZmiqJvq7zTQggyK99Gw8fqYdMdHPQWXjz0epFKJXC+9V2xDA==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ '@tanstack/router-core@1.163.3':
+ resolution: {integrity: sha512-jPptiGq/w3nuPzcMC7RNa79aU+b6OjaDzWJnBcV2UAwL4ThJamRS4h42TdhJE+oF5yH9IEnCOGQdfnbw45LbfA==}
+ engines: {node: '>=20.19'}
+
+ '@tanstack/router-devtools-core@1.163.3':
+ resolution: {integrity: sha512-FPi64IP0PT1IkoeyGmsD6JoOVOYAb85VCH0mUbSdD90yV0+1UB6oT+D7K27GXkp7SXMJN3mBEjU5rKnNnmSCIw==}
+ engines: {node: '>=20.19'}
+ peerDependencies:
+ '@tanstack/router-core': ^1.163.3
+ csstype: ^3.0.10
+ peerDependenciesMeta:
+ csstype:
+ optional: true
+
+ '@tanstack/router-generator@1.164.0':
+ resolution: {integrity: sha512-Uiyj+RtW0kdeqEd8NEd3Np1Z2nhJ2xgLS8U+5mTvFrm/s3xkM2LYjJHoLzc6am7sKPDsmeF9a4/NYq3R7ZJP0Q==}
+ engines: {node: '>=20.19'}
+
+ '@tanstack/router-plugin@1.164.0':
+ resolution: {integrity: sha512-cZPsEMhqzyzmuPuDbsTAzBZaT+cj0pGjwdhjxJfPCM06Ax8v4tFR7n/Ug0UCwnNAUEmKZWN3lA9uT+TxXnk9PQ==}
+ engines: {node: '>=20.19'}
+ peerDependencies:
+ '@rsbuild/core': '>=1.0.2'
+ '@tanstack/react-router': ^1.163.3
+ vite: '>=5.0.0 || >=6.0.0 || >=7.0.0'
+ vite-plugin-solid: ^2.11.10
+ webpack: '>=5.92.0'
+ peerDependenciesMeta:
+ '@rsbuild/core':
+ optional: true
+ '@tanstack/react-router':
+ optional: true
+ vite:
+ optional: true
+ vite-plugin-solid:
+ optional: true
+ webpack:
+ optional: true
+
+ '@tanstack/router-utils@1.161.4':
+ resolution: {integrity: sha512-r8TpjyIZoqrXXaf2DDyjd44gjGBoyE+/oEaaH68yLI9ySPO1gUWmQENZ1MZnmBnpUGN24NOZxdjDLc8npK0SAw==}
+ engines: {node: '>=20.19'}
+
+ '@tanstack/store@0.9.1':
+ resolution: {integrity: sha512-+qcNkOy0N1qSGsP7omVCW0SDrXtaDcycPqBDE726yryiA5eTDFpjBReaYjghVJwNf1pcPMyzIwTGlYjCSQR0Fg==}
+
+ '@tanstack/virtual-file-routes@1.161.4':
+ resolution: {integrity: sha512-42WoRePf8v690qG8yGRe/YOh+oHni9vUaUUfoqlS91U2scd3a5rkLtVsc6b7z60w3RogH0I00vdrC5AaeiZ18w==}
+ engines: {node: '>=20.19'}
+
+ '@trivago/prettier-plugin-sort-imports@6.0.2':
+ resolution: {integrity: sha512-3DgfkukFyC/sE/VuYjaUUWoFfuVjPK55vOFDsxD56XXynFMCZDYFogH2l/hDfOsQAm1myoU/1xByJ3tWqtulXA==}
+ engines: {node: '>= 20'}
+ peerDependencies:
+ '@vue/compiler-sfc': 3.x
+ prettier: 2.x - 3.x
+ prettier-plugin-ember-template-tag: '>= 2.0.0'
+ prettier-plugin-svelte: 3.x
+ svelte: 4.x || 5.x
+ peerDependenciesMeta:
+ '@vue/compiler-sfc':
+ optional: true
+ prettier-plugin-ember-template-tag:
+ optional: true
+ prettier-plugin-svelte:
+ optional: true
+ svelte:
+ optional: true
+
+ '@ts-morph/common@0.27.0':
+ resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==}
+
+ '@types/babel__core@7.20.5':
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+
+ '@types/babel__generator@7.27.0':
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
+
+ '@types/babel__template@7.4.4':
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
+
+ '@types/babel__traverse@7.28.0':
+ resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
+
+ '@types/debug@4.1.12':
+ resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
+
+ '@types/estree-jsx@1.0.5':
+ resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
+
+ '@types/estree@1.0.8':
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+
+ '@types/hast@3.0.4':
+ resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
+
+ '@types/json-schema@7.0.15':
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+ '@types/mdast@4.0.4':
+ resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
+
+ '@types/ms@2.1.0':
+ resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
+
+ '@types/node@24.11.0':
+ resolution: {integrity: sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==}
+
+ '@types/react-dom@19.2.3':
+ resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
+ peerDependencies:
+ '@types/react': ^19.2.0
+
+ '@types/react@19.2.14':
+ resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
+
+ '@types/statuses@2.0.6':
+ resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==}
+
+ '@types/unist@2.0.11':
+ resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
+
+ '@types/unist@3.0.3':
+ resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
+
+ '@types/validate-npm-package-name@4.0.2':
+ resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==}
+
+ '@typescript-eslint/eslint-plugin@8.56.1':
+ resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^8.56.1
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/parser@8.56.1':
+ resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/project-service@8.56.1':
+ resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/scope-manager@8.56.1':
+ resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/tsconfig-utils@8.56.1':
+ resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/type-utils@8.56.1':
+ resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/types@8.56.1':
+ resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@8.56.1':
+ resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/utils@8.56.1':
+ resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/visitor-keys@8.56.1':
+ resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@ungap/structured-clone@1.3.0':
+ resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
+
+ '@vitejs/plugin-react@5.1.4':
+ resolution: {integrity: sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
+
+ accepts@2.0.0:
+ resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
+ engines: {node: '>= 0.6'}
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.16.0:
+ resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ agent-base@7.1.4:
+ resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
+ engines: {node: '>= 14'}
+
+ ajv-formats@3.0.1:
+ resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
+ peerDependencies:
+ ajv: ^8.0.0
+ peerDependenciesMeta:
+ ajv:
+ optional: true
+
+ ajv@6.14.0:
+ resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
+
+ ajv@8.18.0:
+ resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-regex@6.2.2:
+ resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
+ engines: {node: '>=12'}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ ansis@4.2.0:
+ resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
+ engines: {node: '>=14'}
+
+ anymatch@3.1.3:
+ resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+ engines: {node: '>= 8'}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ aria-hidden@1.2.6:
+ resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
+ engines: {node: '>=10'}
+
+ ast-types@0.16.1:
+ resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==}
+ engines: {node: '>=4'}
+
+ babel-dead-code-elimination@1.0.12:
+ resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==}
+
+ bail@2.0.2:
+ resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
+
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ balanced-match@4.0.4:
+ resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
+ engines: {node: 18 || 20 || >=22}
+
+ baseline-browser-mapping@2.10.0:
+ resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ binary-extensions@2.3.0:
+ resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
+ engines: {node: '>=8'}
+
+ body-parser@2.2.2:
+ resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
+ engines: {node: '>=18'}
+
+ brace-expansion@1.1.12:
+ resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
+
+ brace-expansion@2.0.2:
+ resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
+
+ brace-expansion@5.0.4:
+ resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==}
+ engines: {node: 18 || 20 || >=22}
+
+ braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+
+ browserslist@4.28.1:
+ resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ bundle-name@4.1.0:
+ resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
+ engines: {node: '>=18'}
+
+ bytes@3.1.2:
+ resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
+ engines: {node: '>= 0.8'}
+
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ call-bound@1.0.4:
+ resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
+ engines: {node: '>= 0.4'}
+
+ callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+
+ caniuse-lite@1.0.30001775:
+ resolution: {integrity: sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==}
+
+ ccount@2.0.1:
+ resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
+
+ chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+
+ chalk@5.6.2:
+ resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
+ engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+
+ character-entities-html4@2.1.0:
+ resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
+
+ character-entities-legacy@3.0.0:
+ resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
+
+ character-entities@2.0.2:
+ resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
+
+ character-reference-invalid@2.0.1:
+ resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
+
+ chokidar@3.6.0:
+ resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
+ engines: {node: '>= 8.10.0'}
+
+ class-variance-authority@0.7.1:
+ resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
+
+ cli-cursor@5.0.0:
+ resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
+ engines: {node: '>=18'}
+
+ cli-spinners@2.9.2:
+ resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
+ engines: {node: '>=6'}
+
+ cli-width@4.1.0:
+ resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
+ engines: {node: '>= 12'}
+
+ cliui@8.0.1:
+ resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
+ engines: {node: '>=12'}
+
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
+ code-block-writer@13.0.3:
+ resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ comma-separated-tokens@2.0.3:
+ resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
+
+ commander@11.1.0:
+ resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
+ engines: {node: '>=16'}
+
+ commander@14.0.3:
+ resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
+ engines: {node: '>=20'}
+
+ concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+ content-disposition@1.0.1:
+ resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==}
+ engines: {node: '>=18'}
+
+ content-type@1.0.5:
+ resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
+ engines: {node: '>= 0.6'}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ cookie-es@2.0.0:
+ resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==}
+
+ cookie-signature@1.2.2:
+ resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
+ engines: {node: '>=6.6.0'}
+
+ cookie@0.7.2:
+ resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
+ engines: {node: '>= 0.6'}
+
+ cookie@1.1.1:
+ resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
+ engines: {node: '>=18'}
+
+ cors@2.8.6:
+ resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
+ engines: {node: '>= 0.10'}
+
+ cosmiconfig@9.0.0:
+ resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ typescript: '>=4.9.5'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ cssesc@3.0.0:
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ data-uri-to-buffer@4.0.1:
+ resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
+ engines: {node: '>= 12'}
+
+ dayjs@1.11.19:
+ resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ decode-named-character-reference@1.3.0:
+ resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
+
+ dedent@1.7.2:
+ resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==}
+ peerDependencies:
+ babel-plugin-macros: ^3.1.0
+ peerDependenciesMeta:
+ babel-plugin-macros:
+ optional: true
+
+ deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+ deepmerge@4.3.1:
+ resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
+ engines: {node: '>=0.10.0'}
+
+ default-browser-id@5.0.1:
+ resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==}
+ engines: {node: '>=18'}
+
+ default-browser@5.5.0:
+ resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==}
+ engines: {node: '>=18'}
+
+ define-lazy-prop@3.0.0:
+ resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
+ engines: {node: '>=12'}
+
+ depd@2.0.0:
+ resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
+ engines: {node: '>= 0.8'}
+
+ dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ detect-node-es@1.1.0:
+ resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+
+ devlop@1.1.0:
+ resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
+
+ diff@8.0.3:
+ resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==}
+ engines: {node: '>=0.3.1'}
+
+ dotenv@17.3.1:
+ resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==}
+ engines: {node: '>=12'}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ eciesjs@0.4.17:
+ resolution: {integrity: sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w==}
+ engines: {bun: '>=1', deno: '>=2', node: '>=16'}
+
+ ee-first@1.1.1:
+ resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
+
+ electron-to-chromium@1.5.302:
+ resolution: {integrity: sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==}
+
+ emoji-regex@10.6.0:
+ resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
+
+ emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+ encodeurl@2.0.0:
+ resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
+ engines: {node: '>= 0.8'}
+
+ enhanced-resolve@5.20.0:
+ resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==}
+ engines: {node: '>=10.13.0'}
+
+ env-paths@2.2.1:
+ resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
+ engines: {node: '>=6'}
+
+ error-ex@1.3.4:
+ resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
+
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-object-atoms@1.1.1:
+ resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+ engines: {node: '>= 0.4'}
+
+ esbuild@0.27.3:
+ resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ escape-html@1.0.3:
+ resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ escape-string-regexp@5.0.0:
+ resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
+ engines: {node: '>=12'}
+
+ eslint-config-prettier@10.1.8:
+ resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==}
+ hasBin: true
+ peerDependencies:
+ eslint: '>=7.0.0'
+
+ eslint-plugin-react-hooks@7.0.1:
+ resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
+
+ eslint-plugin-react-refresh@0.4.26:
+ resolution: {integrity: sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==}
+ peerDependencies:
+ eslint: '>=8.40'
+
+ eslint-scope@8.4.0:
+ resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint-visitor-keys@4.2.1:
+ resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint-visitor-keys@5.0.1:
+ resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ eslint@9.39.3:
+ resolution: {integrity: sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ hasBin: true
+ peerDependencies:
+ jiti: '*'
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+
+ espree@10.4.0:
+ resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ esprima@4.0.1:
+ resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ esquery@1.7.0:
+ resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
+ engines: {node: '>=0.10'}
+
+ esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ estree-util-is-identifier-name@3.0.0:
+ resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ etag@1.8.1:
+ resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
+ engines: {node: '>= 0.6'}
+
+ eventsource-parser@3.0.6:
+ resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
+ engines: {node: '>=18.0.0'}
+
+ eventsource@3.0.7:
+ resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
+ engines: {node: '>=18.0.0'}
+
+ execa@5.1.1:
+ resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
+ engines: {node: '>=10'}
+
+ execa@9.6.1:
+ resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
+ engines: {node: ^18.19.0 || >=20.5.0}
+
+ express-rate-limit@8.2.1:
+ resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==}
+ engines: {node: '>= 16'}
+ peerDependencies:
+ express: '>= 4.11'
+
+ express@5.2.1:
+ resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
+ engines: {node: '>= 18'}
+
+ extend@3.0.2:
+ resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-glob@3.3.3:
+ resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
+ engines: {node: '>=8.6.0'}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+ fast-uri@3.1.0:
+ resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
+
+ fastq@1.20.1:
+ resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ fetch-blob@3.2.0:
+ resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
+ engines: {node: ^12.20 || >= 14.13}
+
+ figures@6.1.0:
+ resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
+ engines: {node: '>=18'}
+
+ file-entry-cache@8.0.0:
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+ engines: {node: '>=16.0.0'}
+
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
+ finalhandler@2.1.1:
+ resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
+ engines: {node: '>= 18.0.0'}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ flat-cache@4.0.1:
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+ engines: {node: '>=16'}
+
+ flatted@3.3.3:
+ resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
+
+ formdata-polyfill@4.0.10:
+ resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
+ engines: {node: '>=12.20.0'}
+
+ forwarded@0.2.0:
+ resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
+ engines: {node: '>= 0.6'}
+
+ fresh@2.0.0:
+ resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
+ engines: {node: '>= 0.8'}
+
+ fs-extra@11.3.3:
+ resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==}
+ engines: {node: '>=14.14'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ fuzzysort@3.1.0:
+ resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==}
+
+ fzf@0.5.2:
+ resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==}
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+
+ get-east-asian-width@1.5.0:
+ resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==}
+ engines: {node: '>=18'}
+
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-nonce@1.0.1:
+ resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
+ engines: {node: '>=6'}
+
+ get-own-enumerable-keys@1.0.0:
+ resolution: {integrity: sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==}
+ engines: {node: '>=14.16'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
+ get-stream@6.0.1:
+ resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
+ engines: {node: '>=10'}
+
+ get-stream@9.0.1:
+ resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
+ engines: {node: '>=18'}
+
+ get-tsconfig@4.13.6:
+ resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==}
+
+ glob-parent@5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ globals@14.0.0:
+ resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+ engines: {node: '>=18'}
+
+ globals@16.5.0:
+ resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==}
+ engines: {node: '>=18'}
+
+ goober@2.1.18:
+ resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==}
+ peerDependencies:
+ csstype: ^3.0.10
+
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ graphql@16.13.0:
+ resolution: {integrity: sha512-uSisMYERbaB9bkA9M4/4dnqyktaEkf1kMHNKq/7DHyxVeWqHQ2mBmVqm5u6/FVHwF3iCNalKcg82Zfl+tffWoA==}
+ engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ has-symbols@1.1.0:
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+ engines: {node: '>= 0.4'}
+
+ hasown@2.0.2:
+ resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+ engines: {node: '>= 0.4'}
+
+ hast-util-to-jsx-runtime@2.3.6:
+ resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
+
+ hast-util-whitespace@3.0.0:
+ resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
+
+ headers-polyfill@4.0.3:
+ resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==}
+
+ hermes-estree@0.25.1:
+ resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
+
+ hermes-parser@0.25.1:
+ resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
+
+ hono@4.12.3:
+ resolution: {integrity: sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==}
+ engines: {node: '>=16.9.0'}
+
+ html-parse-stringify@3.0.1:
+ resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
+
+ html-url-attributes@3.0.1:
+ resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
+
+ http-errors@2.0.1:
+ resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
+ engines: {node: '>= 0.8'}
+
+ https-proxy-agent@7.0.6:
+ resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
+ engines: {node: '>= 14'}
+
+ human-signals@2.1.0:
+ resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
+ engines: {node: '>=10.17.0'}
+
+ human-signals@8.0.1:
+ resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
+ engines: {node: '>=18.18.0'}
+
+ i18next-browser-languagedetector@8.2.1:
+ resolution: {integrity: sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==}
+
+ i18next@25.8.14:
+ resolution: {integrity: sha512-paMUYkfWJMsWPeE/Hejcw+XLhHrQPehem+4wMo+uELnvIwvCG019L9sAIljwjCmEMtFQQO3YeitJY8Kctei3iA==}
+ peerDependencies:
+ typescript: ^5
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ iconv-lite@0.7.2:
+ resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
+ engines: {node: '>=0.10.0'}
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ ignore@7.0.5:
+ resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
+ engines: {node: '>= 4'}
+
+ import-fresh@3.3.1:
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+ engines: {node: '>=6'}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+ inline-style-parser@0.2.7:
+ resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
+
+ ip-address@10.0.1:
+ resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==}
+ engines: {node: '>= 12'}
+
+ ipaddr.js@1.9.1:
+ resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
+ engines: {node: '>= 0.10'}
+
+ is-alphabetical@2.0.1:
+ resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
+
+ is-alphanumerical@2.0.1:
+ resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
+
+ is-arrayish@0.2.1:
+ resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
+
+ is-binary-path@2.1.0:
+ resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+ engines: {node: '>=8'}
+
+ is-decimal@2.0.1:
+ resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
+
+ is-docker@3.0.0:
+ resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ hasBin: true
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-hexadecimal@2.0.1:
+ resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
+
+ is-in-ssh@1.0.0:
+ resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==}
+ engines: {node: '>=20'}
+
+ is-inside-container@1.0.0:
+ resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
+ engines: {node: '>=14.16'}
+ hasBin: true
+
+ is-interactive@2.0.0:
+ resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==}
+ engines: {node: '>=12'}
+
+ is-node-process@1.2.0:
+ resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==}
+
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ is-obj@3.0.0:
+ resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==}
+ engines: {node: '>=12'}
+
+ is-plain-obj@4.1.0:
+ resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
+ engines: {node: '>=12'}
+
+ is-promise@4.0.0:
+ resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
+
+ is-regexp@3.1.0:
+ resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==}
+ engines: {node: '>=12'}
+
+ is-stream@2.0.1:
+ resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
+ engines: {node: '>=8'}
+
+ is-stream@4.0.1:
+ resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
+ engines: {node: '>=18'}
+
+ is-unicode-supported@1.3.0:
+ resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==}
+ engines: {node: '>=12'}
+
+ is-unicode-supported@2.1.0:
+ resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
+ engines: {node: '>=18'}
+
+ is-wsl@3.1.1:
+ resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==}
+ engines: {node: '>=16'}
+
+ isbot@5.1.35:
+ resolution: {integrity: sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg==}
+ engines: {node: '>=18'}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ isexe@3.1.5:
+ resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==}
+ engines: {node: '>=18'}
+
+ javascript-natural-sort@0.7.1:
+ resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==}
+
+ jiti@2.6.1:
+ resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
+ hasBin: true
+
+ jose@6.1.3:
+ resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==}
+
+ jotai@2.18.0:
+ resolution: {integrity: sha512-XI38kGWAvtxAZ+cwHcTgJsd+kJOJGf3OfL4XYaXWZMZ7IIY8e53abpIHvtVn1eAgJ5dlgwlGFnP4psrZ/vZbtA==}
+ engines: {node: '>=12.20.0'}
+ peerDependencies:
+ '@babel/core': '>=7.0.0'
+ '@babel/template': '>=7.0.0'
+ '@types/react': '>=17.0.0'
+ react: '>=17.0.0'
+ peerDependenciesMeta:
+ '@babel/core':
+ optional: true
+ '@babel/template':
+ optional: true
+ '@types/react':
+ optional: true
+ react:
+ optional: true
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ js-yaml@4.1.1:
+ resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
+ hasBin: true
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+ json-parse-even-better-errors@2.3.1:
+ resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+
+ json-schema-typed@8.0.2:
+ resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==}
+
+ json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ jsonfile@6.2.0:
+ resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
+
+ keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+ kleur@3.0.3:
+ resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
+ engines: {node: '>=6'}
+
+ kleur@4.1.5:
+ resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
+ engines: {node: '>=6'}
+
+ levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+
+ lightningcss-android-arm64@1.31.1:
+ resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.31.1:
+ resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.31.1:
+ resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.31.1:
+ resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.31.1:
+ resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.31.1:
+ resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-arm64-musl@1.31.1:
+ resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-x64-gnu@1.31.1:
+ resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-x64-musl@1.31.1:
+ resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-win32-arm64-msvc@1.31.1:
+ resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.31.1:
+ resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.31.1:
+ resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==}
+ engines: {node: '>= 12.0.0'}
+
+ lines-and-columns@1.2.4:
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ lodash-es@4.17.23:
+ resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==}
+
+ lodash.merge@4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+ log-symbols@6.0.0:
+ resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==}
+ engines: {node: '>=18'}
+
+ longest-streak@3.1.0:
+ resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ markdown-table@3.0.4:
+ resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
+
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ mdast-util-find-and-replace@3.0.2:
+ resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==}
+
+ mdast-util-from-markdown@2.0.3:
+ resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==}
+
+ mdast-util-gfm-autolink-literal@2.0.1:
+ resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==}
+
+ mdast-util-gfm-footnote@2.1.0:
+ resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==}
+
+ mdast-util-gfm-strikethrough@2.0.0:
+ resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==}
+
+ mdast-util-gfm-table@2.0.0:
+ resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==}
+
+ mdast-util-gfm-task-list-item@2.0.0:
+ resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==}
+
+ mdast-util-gfm@3.1.0:
+ resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
+
+ mdast-util-mdx-expression@2.0.1:
+ resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==}
+
+ mdast-util-mdx-jsx@3.2.0:
+ resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==}
+
+ mdast-util-mdxjs-esm@2.0.1:
+ resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==}
+
+ mdast-util-phrasing@4.1.0:
+ resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
+
+ mdast-util-to-hast@13.2.1:
+ resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
+
+ mdast-util-to-markdown@2.1.2:
+ resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==}
+
+ mdast-util-to-string@4.0.0:
+ resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
+
+ media-typer@1.1.0:
+ resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
+ engines: {node: '>= 0.8'}
+
+ merge-descriptors@2.0.0:
+ resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
+ engines: {node: '>=18'}
+
+ merge-stream@2.0.0:
+ resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
+
+ merge2@1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+
+ micromark-core-commonmark@2.0.3:
+ resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
+
+ micromark-extension-gfm-autolink-literal@2.1.0:
+ resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==}
+
+ micromark-extension-gfm-footnote@2.1.0:
+ resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==}
+
+ micromark-extension-gfm-strikethrough@2.1.0:
+ resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==}
+
+ micromark-extension-gfm-table@2.1.1:
+ resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==}
+
+ micromark-extension-gfm-tagfilter@2.0.0:
+ resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==}
+
+ micromark-extension-gfm-task-list-item@2.1.0:
+ resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==}
+
+ micromark-extension-gfm@3.0.0:
+ resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==}
+
+ micromark-factory-destination@2.0.1:
+ resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
+
+ micromark-factory-label@2.0.1:
+ resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==}
+
+ micromark-factory-space@2.0.1:
+ resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==}
+
+ micromark-factory-title@2.0.1:
+ resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==}
+
+ micromark-factory-whitespace@2.0.1:
+ resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==}
+
+ micromark-util-character@2.1.1:
+ resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==}
+
+ micromark-util-chunked@2.0.1:
+ resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==}
+
+ micromark-util-classify-character@2.0.1:
+ resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==}
+
+ micromark-util-combine-extensions@2.0.1:
+ resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==}
+
+ micromark-util-decode-numeric-character-reference@2.0.2:
+ resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==}
+
+ micromark-util-decode-string@2.0.1:
+ resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==}
+
+ micromark-util-encode@2.0.1:
+ resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==}
+
+ micromark-util-html-tag-name@2.0.1:
+ resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==}
+
+ micromark-util-normalize-identifier@2.0.1:
+ resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==}
+
+ micromark-util-resolve-all@2.0.1:
+ resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==}
+
+ micromark-util-sanitize-uri@2.0.1:
+ resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==}
+
+ micromark-util-subtokenize@2.1.0:
+ resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==}
+
+ micromark-util-symbol@2.0.1:
+ resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==}
+
+ micromark-util-types@2.0.2:
+ resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==}
+
+ micromark@4.0.2:
+ resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==}
+
+ micromatch@4.0.8:
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+ engines: {node: '>=8.6'}
+
+ mime-db@1.54.0:
+ resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
+ engines: {node: '>= 0.6'}
+
+ mime-types@3.0.2:
+ resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
+ engines: {node: '>=18'}
+
+ mimic-fn@2.1.0:
+ resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
+ engines: {node: '>=6'}
+
+ mimic-function@5.0.1:
+ resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
+ engines: {node: '>=18'}
+
+ minimatch@10.2.4:
+ resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
+ engines: {node: 18 || 20 || >=22}
+
+ minimatch@3.1.5:
+ resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
+
+ minimatch@9.0.9:
+ resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ msw@2.12.10:
+ resolution: {integrity: sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw==}
+ engines: {node: '>=18'}
+ hasBin: true
+ peerDependencies:
+ typescript: '>= 4.8.x'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ mute-stream@2.0.0:
+ resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
+ engines: {node: ^18.17.0 || >=20.5.0}
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+ negotiator@1.0.0:
+ resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
+ engines: {node: '>= 0.6'}
+
+ node-domexception@1.0.0:
+ resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
+ engines: {node: '>=10.5.0'}
+ deprecated: Use your platform's native DOMException instead
+
+ node-fetch@3.3.2:
+ resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
+ node-releases@2.0.27:
+ resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
+
+ normalize-path@3.0.0:
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+ engines: {node: '>=0.10.0'}
+
+ npm-run-path@4.0.1:
+ resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
+ engines: {node: '>=8'}
+
+ npm-run-path@6.0.0:
+ resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
+ engines: {node: '>=18'}
+
+ object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+
+ object-inspect@1.13.4:
+ resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
+ engines: {node: '>= 0.4'}
+
+ object-treeify@1.1.33:
+ resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==}
+ engines: {node: '>= 10'}
+
+ on-finished@2.4.1:
+ resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
+ engines: {node: '>= 0.8'}
+
+ once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
+ onetime@5.1.2:
+ resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
+ engines: {node: '>=6'}
+
+ onetime@7.0.0:
+ resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
+ engines: {node: '>=18'}
+
+ open@11.0.0:
+ resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==}
+ engines: {node: '>=20'}
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ ora@8.2.0:
+ resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==}
+ engines: {node: '>=18'}
+
+ outvariant@1.4.3:
+ resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ package-manager-detector@1.6.0:
+ resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
+
+ parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+
+ parse-entities@4.0.2:
+ resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
+
+ parse-imports-exports@0.2.4:
+ resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==}
+
+ parse-json@5.2.0:
+ resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
+ engines: {node: '>=8'}
+
+ parse-ms@4.0.0:
+ resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
+ engines: {node: '>=18'}
+
+ parse-statements@1.0.11:
+ resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==}
+
+ parseurl@1.3.3:
+ resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
+ engines: {node: '>= 0.8'}
+
+ path-browserify@1.0.1:
+ resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ path-key@4.0.0:
+ resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
+ engines: {node: '>=12'}
+
+ path-to-regexp@6.3.0:
+ resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
+
+ path-to-regexp@8.3.0:
+ resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+
+ picomatch@4.0.3:
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+ engines: {node: '>=12'}
+
+ pkce-challenge@5.0.1:
+ resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
+ engines: {node: '>=16.20.0'}
+
+ postcss-selector-parser@6.0.10:
+ resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
+ engines: {node: '>=4'}
+
+ postcss-selector-parser@7.1.1:
+ resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
+ engines: {node: '>=4'}
+
+ postcss@8.5.6:
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ powershell-utils@0.1.0:
+ resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
+ engines: {node: '>=20'}
+
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
+ prettier-plugin-tailwindcss@0.7.2:
+ resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==}
+ engines: {node: '>=20.19'}
+ peerDependencies:
+ '@ianvs/prettier-plugin-sort-imports': '*'
+ '@prettier/plugin-hermes': '*'
+ '@prettier/plugin-oxc': '*'
+ '@prettier/plugin-pug': '*'
+ '@shopify/prettier-plugin-liquid': '*'
+ '@trivago/prettier-plugin-sort-imports': '*'
+ '@zackad/prettier-plugin-twig': '*'
+ prettier: ^3.0
+ prettier-plugin-astro: '*'
+ prettier-plugin-css-order: '*'
+ prettier-plugin-jsdoc: '*'
+ prettier-plugin-marko: '*'
+ prettier-plugin-multiline-arrays: '*'
+ prettier-plugin-organize-attributes: '*'
+ prettier-plugin-organize-imports: '*'
+ prettier-plugin-sort-imports: '*'
+ prettier-plugin-svelte: '*'
+ peerDependenciesMeta:
+ '@ianvs/prettier-plugin-sort-imports':
+ optional: true
+ '@prettier/plugin-hermes':
+ optional: true
+ '@prettier/plugin-oxc':
+ optional: true
+ '@prettier/plugin-pug':
+ optional: true
+ '@shopify/prettier-plugin-liquid':
+ optional: true
+ '@trivago/prettier-plugin-sort-imports':
+ optional: true
+ '@zackad/prettier-plugin-twig':
+ optional: true
+ prettier-plugin-astro:
+ optional: true
+ prettier-plugin-css-order:
+ optional: true
+ prettier-plugin-jsdoc:
+ optional: true
+ prettier-plugin-marko:
+ optional: true
+ prettier-plugin-multiline-arrays:
+ optional: true
+ prettier-plugin-organize-attributes:
+ optional: true
+ prettier-plugin-organize-imports:
+ optional: true
+ prettier-plugin-sort-imports:
+ optional: true
+ prettier-plugin-svelte:
+ optional: true
+
+ prettier@3.8.1:
+ resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==}
+ engines: {node: '>=14'}
+ hasBin: true
+
+ pretty-ms@9.3.0:
+ resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
+ engines: {node: '>=18'}
+
+ prompts@2.4.2:
+ resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
+ engines: {node: '>= 6'}
+
+ property-information@7.1.0:
+ resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
+
+ proxy-addr@2.0.7:
+ resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
+ engines: {node: '>= 0.10'}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ qs@6.15.0:
+ resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==}
+ engines: {node: '>=0.6'}
+
+ queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+ radix-ui@1.4.3:
+ resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ range-parser@1.2.1:
+ resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
+ engines: {node: '>= 0.6'}
+
+ raw-body@3.0.2:
+ resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
+ engines: {node: '>= 0.10'}
+
+ react-dom@19.2.4:
+ resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
+ peerDependencies:
+ react: ^19.2.4
+
+ react-i18next@16.5.4:
+ resolution: {integrity: sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g==}
+ peerDependencies:
+ i18next: '>= 25.6.2'
+ react: '>= 16.8.0'
+ react-dom: '*'
+ react-native: '*'
+ typescript: ^5
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+ typescript:
+ optional: true
+
+ react-markdown@10.1.0:
+ resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==}
+ peerDependencies:
+ '@types/react': '>=18'
+ react: '>=18'
+
+ react-refresh@0.18.0:
+ resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
+ engines: {node: '>=0.10.0'}
+
+ react-remove-scroll-bar@2.3.8:
+ resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ react-remove-scroll@2.7.2:
+ resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ react-style-singleton@2.2.3:
+ resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ react-textarea-autosize@8.5.9:
+ resolution: {integrity: sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ react@19.2.4:
+ resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
+ engines: {node: '>=0.10.0'}
+
+ readdirp@3.6.0:
+ resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+ engines: {node: '>=8.10.0'}
+
+ recast@0.23.11:
+ resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
+ engines: {node: '>= 4'}
+
+ remark-gfm@4.0.1:
+ resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
+
+ remark-parse@11.0.0:
+ resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
+
+ remark-rehype@11.1.2:
+ resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==}
+
+ remark-stringify@11.0.0:
+ resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
+
+ require-directory@2.1.1:
+ resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+ engines: {node: '>=0.10.0'}
+
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
+ resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+
+ resolve-pkg-maps@1.0.0:
+ resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+
+ restore-cursor@5.1.0:
+ resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
+ engines: {node: '>=18'}
+
+ rettime@0.10.1:
+ resolution: {integrity: sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==}
+
+ reusify@1.1.0:
+ resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+ rollup@4.59.0:
+ resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ router@2.2.0:
+ resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
+ engines: {node: '>= 18'}
+
+ run-applescript@7.1.0:
+ resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
+ engines: {node: '>=18'}
+
+ run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+ safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+ scheduler@0.27.0:
+ resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ semver@7.7.4:
+ resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ send@1.2.1:
+ resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
+ engines: {node: '>= 18'}
+
+ seroval-plugins@1.5.0:
+ resolution: {integrity: sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ seroval: ^1.0
+
+ seroval@1.5.0:
+ resolution: {integrity: sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==}
+ engines: {node: '>=10'}
+
+ serve-static@2.2.1:
+ resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
+ engines: {node: '>= 18'}
+
+ setprototypeof@1.2.0:
+ resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
+
+ shadcn@3.8.5:
+ resolution: {integrity: sha512-jPRx44e+eyeV7xwY3BLJXcfrks00+M0h5BGB9l6DdcBW4BpAj4x3lVmVy0TXPEs2iHEisxejr62sZAAw6B1EVA==}
+ hasBin: true
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ side-channel-list@1.0.0:
+ resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-map@1.0.1:
+ resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-weakmap@1.0.2:
+ resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
+ engines: {node: '>= 0.4'}
+
+ side-channel@1.1.0:
+ resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
+ engines: {node: '>= 0.4'}
+
+ signal-exit@3.0.7:
+ resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+
+ signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+
+ sisteransi@1.0.5:
+ resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
+
+ sonner@2.0.7:
+ resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
+ peerDependencies:
+ react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ source-map@0.6.1:
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+ engines: {node: '>=0.10.0'}
+
+ source-map@0.7.6:
+ resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
+ engines: {node: '>= 12'}
+
+ space-separated-tokens@2.0.2:
+ resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
+
+ statuses@2.0.2:
+ resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
+ engines: {node: '>= 0.8'}
+
+ stdin-discarder@0.2.2:
+ resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==}
+ engines: {node: '>=18'}
+
+ strict-event-emitter@0.5.1:
+ resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==}
+
+ string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+
+ string-width@7.2.0:
+ resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
+ engines: {node: '>=18'}
+
+ stringify-entities@4.0.4:
+ resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
+
+ stringify-object@5.0.0:
+ resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==}
+ engines: {node: '>=14.16'}
+
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
+ strip-ansi@7.2.0:
+ resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
+ engines: {node: '>=12'}
+
+ strip-bom@3.0.0:
+ resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
+ engines: {node: '>=4'}
+
+ strip-final-newline@2.0.0:
+ resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
+ engines: {node: '>=6'}
+
+ strip-final-newline@4.0.0:
+ resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==}
+ engines: {node: '>=18'}
+
+ strip-json-comments@3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+
+ style-to-js@1.1.21:
+ resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
+
+ style-to-object@1.0.14:
+ resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ tagged-tag@1.0.0:
+ resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
+ engines: {node: '>=20'}
+
+ tailwind-merge@3.5.0:
+ resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
+
+ tailwindcss@4.2.1:
+ resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==}
+
+ tapable@2.3.0:
+ resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
+ engines: {node: '>=6'}
+
+ tiny-invariant@1.3.3:
+ resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
+
+ tiny-warning@1.0.3:
+ resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
+
+ tinyexec@1.0.2:
+ resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
+ engines: {node: '>=18'}
+
+ tinyglobby@0.2.15:
+ resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
+ engines: {node: '>=12.0.0'}
+
+ tldts-core@7.0.23:
+ resolution: {integrity: sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==}
+
+ tldts@7.0.23:
+ resolution: {integrity: sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==}
+ hasBin: true
+
+ to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+
+ toidentifier@1.0.1:
+ resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
+ engines: {node: '>=0.6'}
+
+ tough-cookie@6.0.0:
+ resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
+ engines: {node: '>=16'}
+
+ trim-lines@3.0.1:
+ resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
+
+ trough@2.2.0:
+ resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
+
+ ts-api-utils@2.4.0:
+ resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
+ engines: {node: '>=18.12'}
+ peerDependencies:
+ typescript: '>=4.8.4'
+
+ ts-morph@26.0.0:
+ resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==}
+
+ tsconfig-paths@4.2.0:
+ resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
+ engines: {node: '>=6'}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ tsx@4.21.0:
+ resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
+ engines: {node: '>=18.0.0'}
+ hasBin: true
+
+ tw-animate-css@1.4.0:
+ resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
+
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ type-fest@5.4.4:
+ resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==}
+ engines: {node: '>=20'}
+
+ type-is@2.0.1:
+ resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
+ engines: {node: '>= 0.6'}
+
+ typescript-eslint@8.56.1:
+ resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici-types@7.16.0:
+ resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
+
+ unicorn-magic@0.3.0:
+ resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
+ engines: {node: '>=18'}
+
+ unified@11.0.5:
+ resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
+
+ unist-util-is@6.0.1:
+ resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
+
+ unist-util-position@5.0.0:
+ resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
+
+ unist-util-stringify-position@4.0.0:
+ resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
+
+ unist-util-visit-parents@6.0.2:
+ resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==}
+
+ unist-util-visit@5.1.0:
+ resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
+
+ universalify@2.0.1:
+ resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
+ engines: {node: '>= 10.0.0'}
+
+ unpipe@1.0.0:
+ resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
+ engines: {node: '>= 0.8'}
+
+ unplugin@2.3.11:
+ resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==}
+ engines: {node: '>=18.12.0'}
+
+ until-async@3.0.2:
+ resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==}
+
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ use-callback-ref@1.3.3:
+ resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ use-composed-ref@1.4.0:
+ resolution: {integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ use-isomorphic-layout-effect@1.2.1:
+ resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ use-latest@1.3.0:
+ resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ use-sidecar@1.1.3:
+ resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ use-sync-external-store@1.6.0:
+ resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+ validate-npm-package-name@7.0.2:
+ resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==}
+ engines: {node: ^20.17.0 || >=22.9.0}
+
+ vary@1.1.2:
+ resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
+ engines: {node: '>= 0.8'}
+
+ vfile-message@4.0.3:
+ resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
+
+ vfile@6.0.3:
+ resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+
+ vite@7.3.1:
+ resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ lightningcss: ^1.21.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ void-elements@3.1.0:
+ resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
+ engines: {node: '>=0.10.0'}
+
+ web-streams-polyfill@3.3.3:
+ resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
+ engines: {node: '>= 8'}
+
+ webpack-virtual-modules@0.6.2:
+ resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ which@4.0.0:
+ resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==}
+ engines: {node: ^16.13.0 || >=18.0.0}
+ hasBin: true
+
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
+ wrap-ansi@6.2.0:
+ resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
+ engines: {node: '>=8'}
+
+ wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+
+ wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
+ wsl-utils@0.3.1:
+ resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==}
+ engines: {node: '>=20'}
+
+ y18n@5.0.8:
+ resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+ engines: {node: '>=10'}
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+ yargs-parser@21.1.1:
+ resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
+ engines: {node: '>=12'}
+
+ yargs@17.7.2:
+ resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
+ engines: {node: '>=12'}
+
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
+ yoctocolors-cjs@2.1.3:
+ resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}
+ engines: {node: '>=18'}
+
+ yoctocolors@2.1.2:
+ resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
+ engines: {node: '>=18'}
+
+ zod-to-json-schema@3.25.1:
+ resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
+ peerDependencies:
+ zod: ^3.25 || ^4
+
+ zod-validation-error@4.0.2:
+ resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ zod: ^3.25.0 || ^4.0.0
+
+ zod@3.25.76:
+ resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
+
+ zod@4.3.6:
+ resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
+
+ zwitch@2.0.4:
+ resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
+
+snapshots:
+
+ '@antfu/ni@25.0.0':
+ dependencies:
+ ansis: 4.2.0
+ fzf: 0.5.2
+ package-manager-detector: 1.6.0
+ tinyexec: 1.0.2
+
+ '@babel/code-frame@7.29.0':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.28.5
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.29.0': {}
+
+ '@babel/core@7.29.0':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helpers': 7.28.6
+ '@babel/parser': 7.29.0
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ '@jridgewell/remapping': 2.3.5
+ convert-source-map: 2.0.0
+ debug: 4.4.3
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.29.1':
+ dependencies:
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-annotate-as-pure@7.27.3':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/helper-compilation-targets@7.28.6':
+ dependencies:
+ '@babel/compat-data': 7.29.0
+ '@babel/helper-validator-option': 7.27.1
+ browserslist: 4.28.1
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-member-expression-to-functions': 7.28.5
+ '@babel/helper-optimise-call-expression': 7.27.1
+ '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ '@babel/traverse': 7.29.0
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-globals@7.28.0': {}
+
+ '@babel/helper-member-expression-to-functions@7.28.5':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-imports@7.28.6':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-validator-identifier': 7.28.5
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-optimise-call-expression@7.27.1':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/helper-plugin-utils@7.28.6': {}
+
+ '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-member-expression-to-functions': 7.28.5
+ '@babel/helper-optimise-call-expression': 7.27.1
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-skip-transparent-expression-wrappers@7.27.1':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-validator-identifier@7.28.5': {}
+
+ '@babel/helper-validator-option@7.27.1': {}
+
+ '@babel/helpers@7.28.6':
+ dependencies:
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+
+ '@babel/parser@7.29.0':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0)
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-validator-option': 7.27.1
+ '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/runtime@7.28.6': {}
+
+ '@babel/template@7.28.6':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
+
+ '@babel/traverse@7.29.0':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/helper-globals': 7.28.0
+ '@babel/parser': 7.29.0
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.29.0':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
+
+ '@dotenvx/dotenvx@1.52.0':
+ dependencies:
+ commander: 11.1.0
+ dotenv: 17.3.1
+ eciesjs: 0.4.17
+ execa: 5.1.1
+ fdir: 6.5.0(picomatch@4.0.3)
+ ignore: 5.3.2
+ object-treeify: 1.1.33
+ picomatch: 4.0.3
+ which: 4.0.0
+
+ '@ecies/ciphers@0.2.5(@noble/ciphers@1.3.0)':
+ dependencies:
+ '@noble/ciphers': 1.3.0
+
+ '@esbuild/aix-ppc64@0.27.3':
+ optional: true
+
+ '@esbuild/android-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/android-arm@0.27.3':
+ optional: true
+
+ '@esbuild/android-x64@0.27.3':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/darwin-x64@0.27.3':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.27.3':
+ optional: true
+
+ '@esbuild/linux-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/linux-arm@0.27.3':
+ optional: true
+
+ '@esbuild/linux-ia32@0.27.3':
+ optional: true
+
+ '@esbuild/linux-loong64@0.27.3':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.27.3':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.27.3':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.27.3':
+ optional: true
+
+ '@esbuild/linux-s390x@0.27.3':
+ optional: true
+
+ '@esbuild/linux-x64@0.27.3':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.27.3':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.27.3':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/sunos-x64@0.27.3':
+ optional: true
+
+ '@esbuild/win32-arm64@0.27.3':
+ optional: true
+
+ '@esbuild/win32-ia32@0.27.3':
+ optional: true
+
+ '@esbuild/win32-x64@0.27.3':
+ optional: true
+
+ '@eslint-community/eslint-utils@4.9.1(eslint@9.39.3(jiti@2.6.1))':
+ dependencies:
+ eslint: 9.39.3(jiti@2.6.1)
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/regexpp@4.12.2': {}
+
+ '@eslint/config-array@0.21.1':
+ dependencies:
+ '@eslint/object-schema': 2.1.7
+ debug: 4.4.3
+ minimatch: 3.1.5
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/config-helpers@0.4.2':
+ dependencies:
+ '@eslint/core': 0.17.0
+
+ '@eslint/core@0.17.0':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/eslintrc@3.3.4':
+ dependencies:
+ ajv: 6.14.0
+ debug: 4.4.3
+ espree: 10.4.0
+ globals: 14.0.0
+ ignore: 5.3.2
+ import-fresh: 3.3.1
+ js-yaml: 4.1.1
+ minimatch: 3.1.5
+ strip-json-comments: 3.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/js@9.39.3': {}
+
+ '@eslint/object-schema@2.1.7': {}
+
+ '@eslint/plugin-kit@0.4.1':
+ dependencies:
+ '@eslint/core': 0.17.0
+ levn: 0.4.1
+
+ '@floating-ui/core@1.7.4':
+ dependencies:
+ '@floating-ui/utils': 0.2.10
+
+ '@floating-ui/dom@1.7.5':
+ dependencies:
+ '@floating-ui/core': 1.7.4
+ '@floating-ui/utils': 0.2.10
+
+ '@floating-ui/react-dom@2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@floating-ui/dom': 1.7.5
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+
+ '@floating-ui/utils@0.2.10': {}
+
+ '@fontsource-variable/inter@5.2.8': {}
+
+ '@hono/node-server@1.19.9(hono@4.12.3)':
+ dependencies:
+ hono: 4.12.3
+
+ '@humanfs/core@0.19.1': {}
+
+ '@humanfs/node@0.16.7':
+ dependencies:
+ '@humanfs/core': 0.19.1
+ '@humanwhocodes/retry': 0.4.3
+
+ '@humanwhocodes/module-importer@1.0.1': {}
+
+ '@humanwhocodes/retry@0.4.3': {}
+
+ '@inquirer/ansi@1.0.2': {}
+
+ '@inquirer/confirm@5.1.21(@types/node@24.11.0)':
+ dependencies:
+ '@inquirer/core': 10.3.2(@types/node@24.11.0)
+ '@inquirer/type': 3.0.10(@types/node@24.11.0)
+ optionalDependencies:
+ '@types/node': 24.11.0
+
+ '@inquirer/core@10.3.2(@types/node@24.11.0)':
+ dependencies:
+ '@inquirer/ansi': 1.0.2
+ '@inquirer/figures': 1.0.15
+ '@inquirer/type': 3.0.10(@types/node@24.11.0)
+ cli-width: 4.1.0
+ mute-stream: 2.0.0
+ signal-exit: 4.1.0
+ wrap-ansi: 6.2.0
+ yoctocolors-cjs: 2.1.3
+ optionalDependencies:
+ '@types/node': 24.11.0
+
+ '@inquirer/figures@1.0.15': {}
+
+ '@inquirer/type@3.0.10(@types/node@24.11.0)':
+ optionalDependencies:
+ '@types/node': 24.11.0
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@modelcontextprotocol/sdk@1.27.1(zod@3.25.76)':
+ dependencies:
+ '@hono/node-server': 1.19.9(hono@4.12.3)
+ ajv: 8.18.0
+ ajv-formats: 3.0.1(ajv@8.18.0)
+ content-type: 1.0.5
+ cors: 2.8.6
+ cross-spawn: 7.0.6
+ eventsource: 3.0.7
+ eventsource-parser: 3.0.6
+ express: 5.2.1
+ express-rate-limit: 8.2.1(express@5.2.1)
+ hono: 4.12.3
+ jose: 6.1.3
+ json-schema-typed: 8.0.2
+ pkce-challenge: 5.0.1
+ raw-body: 3.0.2
+ zod: 3.25.76
+ zod-to-json-schema: 3.25.1(zod@3.25.76)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@mswjs/interceptors@0.41.3':
+ dependencies:
+ '@open-draft/deferred-promise': 2.2.0
+ '@open-draft/logger': 0.3.0
+ '@open-draft/until': 2.1.0
+ is-node-process: 1.2.0
+ outvariant: 1.4.3
+ strict-event-emitter: 0.5.1
+
+ '@noble/ciphers@1.3.0': {}
+
+ '@noble/curves@1.9.7':
+ dependencies:
+ '@noble/hashes': 1.8.0
+
+ '@noble/hashes@1.8.0': {}
+
+ '@nodelib/fs.scandir@2.1.5':
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+
+ '@nodelib/fs.stat@2.0.5': {}
+
+ '@nodelib/fs.walk@1.2.8':
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.20.1
+
+ '@open-draft/deferred-promise@2.2.0': {}
+
+ '@open-draft/logger@0.3.0':
+ dependencies:
+ is-node-process: 1.2.0
+ outvariant: 1.4.3
+
+ '@open-draft/until@2.1.0': {}
+
+ '@radix-ui/number@1.1.1': {}
+
+ '@radix-ui/primitive@1.1.3': {}
+
+ '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ aria-hidden: 1.2.6
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-form@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ aria-hidden: 1.2.6
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/number': 1.1.1
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ aria-hidden: 1.2.6
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@floating-ui/react-dom': 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/rect': 1.1.1
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/number': 1.1.1
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/number': 1.1.1
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ aria-hidden: 1.2.6
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/number': 1.1.1
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ use-sync-external-store: 1.6.0(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/rect': 1.1.1
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/rect@1.1.1': {}
+
+ '@rolldown/pluginutils@1.0.0-rc.3': {}
+
+ '@rollup/rollup-android-arm-eabi@4.59.0':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-musl@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-musl@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.59.0':
+ optional: true
+
+ '@rollup/rollup-openbsd-x64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-openharmony-arm64@4.59.0':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.59.0':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.59.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-gnu@4.59.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.59.0':
+ optional: true
+
+ '@sec-ant/readable-stream@0.4.1': {}
+
+ '@sindresorhus/merge-streams@4.0.0': {}
+
+ '@tabler/icons-react@3.38.0(react@19.2.4)':
+ dependencies:
+ '@tabler/icons': 3.38.0
+ react: 19.2.4
+
+ '@tabler/icons@3.38.0': {}
+
+ '@tailwindcss/node@4.2.1':
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ enhanced-resolve: 5.20.0
+ jiti: 2.6.1
+ lightningcss: 1.31.1
+ magic-string: 0.30.21
+ source-map-js: 1.2.1
+ tailwindcss: 4.2.1
+
+ '@tailwindcss/oxide-android-arm64@4.2.1':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.2.1':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.2.1':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.2.1':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.2.1':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.2.1':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.2.1':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.2.1':
+ optional: true
+
+ '@tailwindcss/oxide-wasm32-wasi@4.2.1':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.2.1':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.2.1':
+ optional: true
+
+ '@tailwindcss/oxide@4.2.1':
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.2.1
+ '@tailwindcss/oxide-darwin-arm64': 4.2.1
+ '@tailwindcss/oxide-darwin-x64': 4.2.1
+ '@tailwindcss/oxide-freebsd-x64': 4.2.1
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1
+ '@tailwindcss/oxide-linux-arm64-musl': 4.2.1
+ '@tailwindcss/oxide-linux-x64-gnu': 4.2.1
+ '@tailwindcss/oxide-linux-x64-musl': 4.2.1
+ '@tailwindcss/oxide-wasm32-wasi': 4.2.1
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1
+ '@tailwindcss/oxide-win32-x64-msvc': 4.2.1
+
+ '@tailwindcss/typography@0.5.19(tailwindcss@4.2.1)':
+ dependencies:
+ postcss-selector-parser: 6.0.10
+ tailwindcss: 4.2.1
+
+ '@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))':
+ dependencies:
+ '@tailwindcss/node': 4.2.1
+ '@tailwindcss/oxide': 4.2.1
+ tailwindcss: 4.2.1
+ vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)
+
+ '@tanstack/history@1.161.4': {}
+
+ '@tanstack/query-core@5.90.20': {}
+
+ '@tanstack/react-query@5.90.21(react@19.2.4)':
+ dependencies:
+ '@tanstack/query-core': 5.90.20
+ react: 19.2.4
+
+ '@tanstack/react-router-devtools@1.163.3(@tanstack/react-router@1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.163.3)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@tanstack/react-router': 1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@tanstack/router-devtools-core': 1.163.3(@tanstack/router-core@1.163.3)(csstype@3.2.3)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@tanstack/router-core': 1.163.3
+ transitivePeerDependencies:
+ - csstype
+
+ '@tanstack/react-router@1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@tanstack/history': 1.161.4
+ '@tanstack/react-store': 0.9.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@tanstack/router-core': 1.163.3
+ isbot: 5.1.35
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ tiny-invariant: 1.3.3
+ tiny-warning: 1.0.3
+
+ '@tanstack/react-store@0.9.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@tanstack/store': 0.9.1
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ use-sync-external-store: 1.6.0(react@19.2.4)
+
+ '@tanstack/router-core@1.163.3':
+ dependencies:
+ '@tanstack/history': 1.161.4
+ '@tanstack/store': 0.9.1
+ cookie-es: 2.0.0
+ seroval: 1.5.0
+ seroval-plugins: 1.5.0(seroval@1.5.0)
+ tiny-invariant: 1.3.3
+ tiny-warning: 1.0.3
+
+ '@tanstack/router-devtools-core@1.163.3(@tanstack/router-core@1.163.3)(csstype@3.2.3)':
+ dependencies:
+ '@tanstack/router-core': 1.163.3
+ clsx: 2.1.1
+ goober: 2.1.18(csstype@3.2.3)
+ tiny-invariant: 1.3.3
+ optionalDependencies:
+ csstype: 3.2.3
+
+ '@tanstack/router-generator@1.164.0':
+ dependencies:
+ '@tanstack/router-core': 1.163.3
+ '@tanstack/router-utils': 1.161.4
+ '@tanstack/virtual-file-routes': 1.161.4
+ prettier: 3.8.1
+ recast: 0.23.11
+ source-map: 0.7.6
+ tsx: 4.21.0
+ zod: 3.25.76
+ transitivePeerDependencies:
+ - supports-color
+
+ '@tanstack/router-plugin@1.164.0(@tanstack/react-router@1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ '@tanstack/router-core': 1.163.3
+ '@tanstack/router-generator': 1.164.0
+ '@tanstack/router-utils': 1.161.4
+ '@tanstack/virtual-file-routes': 1.161.4
+ chokidar: 3.6.0
+ unplugin: 2.3.11
+ zod: 3.25.76
+ optionalDependencies:
+ '@tanstack/react-router': 1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@tanstack/router-utils@1.161.4':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
+ ansis: 4.2.0
+ babel-dead-code-elimination: 1.0.12
+ diff: 8.0.3
+ pathe: 2.0.3
+ tinyglobby: 0.2.15
+ transitivePeerDependencies:
+ - supports-color
+
+ '@tanstack/store@0.9.1': {}
+
+ '@tanstack/virtual-file-routes@1.161.4': {}
+
+ '@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.1)':
+ dependencies:
+ '@babel/generator': 7.29.1
+ '@babel/parser': 7.29.0
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ javascript-natural-sort: 0.7.1
+ lodash-es: 4.17.23
+ minimatch: 9.0.9
+ parse-imports-exports: 0.2.4
+ prettier: 3.8.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@ts-morph/common@0.27.0':
+ dependencies:
+ fast-glob: 3.3.3
+ minimatch: 10.2.4
+ path-browserify: 1.0.1
+
+ '@types/babel__core@7.20.5':
+ dependencies:
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
+ '@types/babel__generator': 7.27.0
+ '@types/babel__template': 7.4.4
+ '@types/babel__traverse': 7.28.0
+
+ '@types/babel__generator@7.27.0':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@types/babel__template@7.4.4':
+ dependencies:
+ '@babel/parser': 7.29.0
+ '@babel/types': 7.29.0
+
+ '@types/babel__traverse@7.28.0':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@types/debug@4.1.12':
+ dependencies:
+ '@types/ms': 2.1.0
+
+ '@types/estree-jsx@1.0.5':
+ dependencies:
+ '@types/estree': 1.0.8
+
+ '@types/estree@1.0.8': {}
+
+ '@types/hast@3.0.4':
+ dependencies:
+ '@types/unist': 3.0.3
+
+ '@types/json-schema@7.0.15': {}
+
+ '@types/mdast@4.0.4':
+ dependencies:
+ '@types/unist': 3.0.3
+
+ '@types/ms@2.1.0': {}
+
+ '@types/node@24.11.0':
+ dependencies:
+ undici-types: 7.16.0
+
+ '@types/react-dom@19.2.3(@types/react@19.2.14)':
+ dependencies:
+ '@types/react': 19.2.14
+
+ '@types/react@19.2.14':
+ dependencies:
+ csstype: 3.2.3
+
+ '@types/statuses@2.0.6': {}
+
+ '@types/unist@2.0.11': {}
+
+ '@types/unist@3.0.3': {}
+
+ '@types/validate-npm-package-name@4.0.2': {}
+
+ '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.2
+ '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/scope-manager': 8.56.1
+ '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/visitor-keys': 8.56.1
+ eslint: 9.39.3(jiti@2.6.1)
+ ignore: 7.0.5
+ natural-compare: 1.4.0
+ ts-api-utils: 2.4.0(typescript@5.9.3)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 8.56.1
+ '@typescript-eslint/types': 8.56.1
+ '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
+ '@typescript-eslint/visitor-keys': 8.56.1
+ debug: 4.4.3
+ eslint: 9.39.3(jiti@2.6.1)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3)
+ '@typescript-eslint/types': 8.56.1
+ debug: 4.4.3
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@8.56.1':
+ dependencies:
+ '@typescript-eslint/types': 8.56.1
+ '@typescript-eslint/visitor-keys': 8.56.1
+
+ '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)':
+ dependencies:
+ typescript: 5.9.3
+
+ '@typescript-eslint/type-utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/types': 8.56.1
+ '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ debug: 4.4.3
+ eslint: 9.39.3(jiti@2.6.1)
+ ts-api-utils: 2.4.0(typescript@5.9.3)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/types@8.56.1': {}
+
+ '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3)
+ '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3)
+ '@typescript-eslint/types': 8.56.1
+ '@typescript-eslint/visitor-keys': 8.56.1
+ debug: 4.4.3
+ minimatch: 10.2.4
+ semver: 7.7.4
+ tinyglobby: 0.2.15
+ ts-api-utils: 2.4.0(typescript@5.9.3)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/utils@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1))
+ '@typescript-eslint/scope-manager': 8.56.1
+ '@typescript-eslint/types': 8.56.1
+ '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
+ eslint: 9.39.3(jiti@2.6.1)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/visitor-keys@8.56.1':
+ dependencies:
+ '@typescript-eslint/types': 8.56.1
+ eslint-visitor-keys: 5.0.1
+
+ '@ungap/structured-clone@1.3.0': {}
+
+ '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0)
+ '@rolldown/pluginutils': 1.0.0-rc.3
+ '@types/babel__core': 7.20.5
+ react-refresh: 0.18.0
+ vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ accepts@2.0.0:
+ dependencies:
+ mime-types: 3.0.2
+ negotiator: 1.0.0
+
+ acorn-jsx@5.3.2(acorn@8.16.0):
+ dependencies:
+ acorn: 8.16.0
+
+ acorn@8.16.0: {}
+
+ agent-base@7.1.4: {}
+
+ ajv-formats@3.0.1(ajv@8.18.0):
+ optionalDependencies:
+ ajv: 8.18.0
+
+ ajv@6.14.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ ajv@8.18.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-uri: 3.1.0
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+
+ ansi-regex@5.0.1: {}
+
+ ansi-regex@6.2.2: {}
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ ansis@4.2.0: {}
+
+ anymatch@3.1.3:
+ dependencies:
+ normalize-path: 3.0.0
+ picomatch: 2.3.1
+
+ argparse@2.0.1: {}
+
+ aria-hidden@1.2.6:
+ dependencies:
+ tslib: 2.8.1
+
+ ast-types@0.16.1:
+ dependencies:
+ tslib: 2.8.1
+
+ babel-dead-code-elimination@1.0.12:
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/parser': 7.29.0
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ bail@2.0.2: {}
+
+ balanced-match@1.0.2: {}
+
+ balanced-match@4.0.4: {}
+
+ baseline-browser-mapping@2.10.0: {}
+
+ binary-extensions@2.3.0: {}
+
+ body-parser@2.2.2:
+ dependencies:
+ bytes: 3.1.2
+ content-type: 1.0.5
+ debug: 4.4.3
+ http-errors: 2.0.1
+ iconv-lite: 0.7.2
+ on-finished: 2.4.1
+ qs: 6.15.0
+ raw-body: 3.0.2
+ type-is: 2.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ brace-expansion@1.1.12:
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
+ brace-expansion@2.0.2:
+ dependencies:
+ balanced-match: 1.0.2
+
+ brace-expansion@5.0.4:
+ dependencies:
+ balanced-match: 4.0.4
+
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+
+ browserslist@4.28.1:
+ dependencies:
+ baseline-browser-mapping: 2.10.0
+ caniuse-lite: 1.0.30001775
+ electron-to-chromium: 1.5.302
+ node-releases: 2.0.27
+ update-browserslist-db: 1.2.3(browserslist@4.28.1)
+
+ bundle-name@4.1.0:
+ dependencies:
+ run-applescript: 7.1.0
+
+ bytes@3.1.2: {}
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ call-bound@1.0.4:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ get-intrinsic: 1.3.0
+
+ callsites@3.1.0: {}
+
+ caniuse-lite@1.0.30001775: {}
+
+ ccount@2.0.1: {}
+
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
+ chalk@5.6.2: {}
+
+ character-entities-html4@2.1.0: {}
+
+ character-entities-legacy@3.0.0: {}
+
+ character-entities@2.0.2: {}
+
+ character-reference-invalid@2.0.1: {}
+
+ chokidar@3.6.0:
+ dependencies:
+ anymatch: 3.1.3
+ braces: 3.0.3
+ glob-parent: 5.1.2
+ is-binary-path: 2.1.0
+ is-glob: 4.0.3
+ normalize-path: 3.0.0
+ readdirp: 3.6.0
+ optionalDependencies:
+ fsevents: 2.3.3
+
+ class-variance-authority@0.7.1:
+ dependencies:
+ clsx: 2.1.1
+
+ cli-cursor@5.0.0:
+ dependencies:
+ restore-cursor: 5.1.0
+
+ cli-spinners@2.9.2: {}
+
+ cli-width@4.1.0: {}
+
+ cliui@8.0.1:
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+
+ clsx@2.1.1: {}
+
+ code-block-writer@13.0.3: {}
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ comma-separated-tokens@2.0.3: {}
+
+ commander@11.1.0: {}
+
+ commander@14.0.3: {}
+
+ concat-map@0.0.1: {}
+
+ content-disposition@1.0.1: {}
+
+ content-type@1.0.5: {}
+
+ convert-source-map@2.0.0: {}
+
+ cookie-es@2.0.0: {}
+
+ cookie-signature@1.2.2: {}
+
+ cookie@0.7.2: {}
+
+ cookie@1.1.1: {}
+
+ cors@2.8.6:
+ dependencies:
+ object-assign: 4.1.1
+ vary: 1.1.2
+
+ cosmiconfig@9.0.0(typescript@5.9.3):
+ dependencies:
+ env-paths: 2.2.1
+ import-fresh: 3.3.1
+ js-yaml: 4.1.1
+ parse-json: 5.2.0
+ optionalDependencies:
+ typescript: 5.9.3
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ cssesc@3.0.0: {}
+
+ csstype@3.2.3: {}
+
+ data-uri-to-buffer@4.0.1: {}
+
+ dayjs@1.11.19: {}
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ decode-named-character-reference@1.3.0:
+ dependencies:
+ character-entities: 2.0.2
+
+ dedent@1.7.2: {}
+
+ deep-is@0.1.4: {}
+
+ deepmerge@4.3.1: {}
+
+ default-browser-id@5.0.1: {}
+
+ default-browser@5.5.0:
+ dependencies:
+ bundle-name: 4.1.0
+ default-browser-id: 5.0.1
+
+ define-lazy-prop@3.0.0: {}
+
+ depd@2.0.0: {}
+
+ dequal@2.0.3: {}
+
+ detect-libc@2.1.2: {}
+
+ detect-node-es@1.1.0: {}
+
+ devlop@1.1.0:
+ dependencies:
+ dequal: 2.0.3
+
+ diff@8.0.3: {}
+
+ dotenv@17.3.1: {}
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ eciesjs@0.4.17:
+ dependencies:
+ '@ecies/ciphers': 0.2.5(@noble/ciphers@1.3.0)
+ '@noble/ciphers': 1.3.0
+ '@noble/curves': 1.9.7
+ '@noble/hashes': 1.8.0
+
+ ee-first@1.1.1: {}
+
+ electron-to-chromium@1.5.302: {}
+
+ emoji-regex@10.6.0: {}
+
+ emoji-regex@8.0.0: {}
+
+ encodeurl@2.0.0: {}
+
+ enhanced-resolve@5.20.0:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.3.0
+
+ env-paths@2.2.1: {}
+
+ error-ex@1.3.4:
+ dependencies:
+ is-arrayish: 0.2.1
+
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-object-atoms@1.1.1:
+ dependencies:
+ es-errors: 1.3.0
+
+ esbuild@0.27.3:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.27.3
+ '@esbuild/android-arm': 0.27.3
+ '@esbuild/android-arm64': 0.27.3
+ '@esbuild/android-x64': 0.27.3
+ '@esbuild/darwin-arm64': 0.27.3
+ '@esbuild/darwin-x64': 0.27.3
+ '@esbuild/freebsd-arm64': 0.27.3
+ '@esbuild/freebsd-x64': 0.27.3
+ '@esbuild/linux-arm': 0.27.3
+ '@esbuild/linux-arm64': 0.27.3
+ '@esbuild/linux-ia32': 0.27.3
+ '@esbuild/linux-loong64': 0.27.3
+ '@esbuild/linux-mips64el': 0.27.3
+ '@esbuild/linux-ppc64': 0.27.3
+ '@esbuild/linux-riscv64': 0.27.3
+ '@esbuild/linux-s390x': 0.27.3
+ '@esbuild/linux-x64': 0.27.3
+ '@esbuild/netbsd-arm64': 0.27.3
+ '@esbuild/netbsd-x64': 0.27.3
+ '@esbuild/openbsd-arm64': 0.27.3
+ '@esbuild/openbsd-x64': 0.27.3
+ '@esbuild/openharmony-arm64': 0.27.3
+ '@esbuild/sunos-x64': 0.27.3
+ '@esbuild/win32-arm64': 0.27.3
+ '@esbuild/win32-ia32': 0.27.3
+ '@esbuild/win32-x64': 0.27.3
+
+ escalade@3.2.0: {}
+
+ escape-html@1.0.3: {}
+
+ escape-string-regexp@4.0.0: {}
+
+ escape-string-regexp@5.0.0: {}
+
+ eslint-config-prettier@10.1.8(eslint@9.39.3(jiti@2.6.1)):
+ dependencies:
+ eslint: 9.39.3(jiti@2.6.1)
+
+ eslint-plugin-react-hooks@7.0.1(eslint@9.39.3(jiti@2.6.1)):
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/parser': 7.29.0
+ eslint: 9.39.3(jiti@2.6.1)
+ hermes-parser: 0.25.1
+ zod: 4.3.6
+ zod-validation-error: 4.0.2(zod@4.3.6)
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-plugin-react-refresh@0.4.26(eslint@9.39.3(jiti@2.6.1)):
+ dependencies:
+ eslint: 9.39.3(jiti@2.6.1)
+
+ eslint-scope@8.4.0:
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-visitor-keys@3.4.3: {}
+
+ eslint-visitor-keys@4.2.1: {}
+
+ eslint-visitor-keys@5.0.1: {}
+
+ eslint@9.39.3(jiti@2.6.1):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1))
+ '@eslint-community/regexpp': 4.12.2
+ '@eslint/config-array': 0.21.1
+ '@eslint/config-helpers': 0.4.2
+ '@eslint/core': 0.17.0
+ '@eslint/eslintrc': 3.3.4
+ '@eslint/js': 9.39.3
+ '@eslint/plugin-kit': 0.4.1
+ '@humanfs/node': 0.16.7
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.4.3
+ '@types/estree': 1.0.8
+ ajv: 6.14.0
+ chalk: 4.1.2
+ cross-spawn: 7.0.6
+ debug: 4.4.3
+ escape-string-regexp: 4.0.0
+ eslint-scope: 8.4.0
+ eslint-visitor-keys: 4.2.1
+ espree: 10.4.0
+ esquery: 1.7.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 8.0.0
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ ignore: 5.3.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ json-stable-stringify-without-jsonify: 1.0.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.5
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ optionalDependencies:
+ jiti: 2.6.1
+ transitivePeerDependencies:
+ - supports-color
+
+ espree@10.4.0:
+ dependencies:
+ acorn: 8.16.0
+ acorn-jsx: 5.3.2(acorn@8.16.0)
+ eslint-visitor-keys: 4.2.1
+
+ esprima@4.0.1: {}
+
+ esquery@1.7.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ esrecurse@4.3.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ estraverse@5.3.0: {}
+
+ estree-util-is-identifier-name@3.0.0: {}
+
+ esutils@2.0.3: {}
+
+ etag@1.8.1: {}
+
+ eventsource-parser@3.0.6: {}
+
+ eventsource@3.0.7:
+ dependencies:
+ eventsource-parser: 3.0.6
+
+ execa@5.1.1:
+ dependencies:
+ cross-spawn: 7.0.6
+ get-stream: 6.0.1
+ human-signals: 2.1.0
+ is-stream: 2.0.1
+ merge-stream: 2.0.0
+ npm-run-path: 4.0.1
+ onetime: 5.1.2
+ signal-exit: 3.0.7
+ strip-final-newline: 2.0.0
+
+ execa@9.6.1:
+ dependencies:
+ '@sindresorhus/merge-streams': 4.0.0
+ cross-spawn: 7.0.6
+ figures: 6.1.0
+ get-stream: 9.0.1
+ human-signals: 8.0.1
+ is-plain-obj: 4.1.0
+ is-stream: 4.0.1
+ npm-run-path: 6.0.0
+ pretty-ms: 9.3.0
+ signal-exit: 4.1.0
+ strip-final-newline: 4.0.0
+ yoctocolors: 2.1.2
+
+ express-rate-limit@8.2.1(express@5.2.1):
+ dependencies:
+ express: 5.2.1
+ ip-address: 10.0.1
+
+ express@5.2.1:
+ dependencies:
+ accepts: 2.0.0
+ body-parser: 2.2.2
+ content-disposition: 1.0.1
+ content-type: 1.0.5
+ cookie: 0.7.2
+ cookie-signature: 1.2.2
+ debug: 4.4.3
+ depd: 2.0.0
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ etag: 1.8.1
+ finalhandler: 2.1.1
+ fresh: 2.0.0
+ http-errors: 2.0.1
+ merge-descriptors: 2.0.0
+ mime-types: 3.0.2
+ on-finished: 2.4.1
+ once: 1.4.0
+ parseurl: 1.3.3
+ proxy-addr: 2.0.7
+ qs: 6.15.0
+ range-parser: 1.2.1
+ router: 2.2.0
+ send: 1.2.1
+ serve-static: 2.2.1
+ statuses: 2.0.2
+ type-is: 2.0.1
+ vary: 1.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ extend@3.0.2: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-glob@3.3.3:
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.8
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
+ fast-uri@3.1.0: {}
+
+ fastq@1.20.1:
+ dependencies:
+ reusify: 1.1.0
+
+ fdir@6.5.0(picomatch@4.0.3):
+ optionalDependencies:
+ picomatch: 4.0.3
+
+ fetch-blob@3.2.0:
+ dependencies:
+ node-domexception: 1.0.0
+ web-streams-polyfill: 3.3.3
+
+ figures@6.1.0:
+ dependencies:
+ is-unicode-supported: 2.1.0
+
+ file-entry-cache@8.0.0:
+ dependencies:
+ flat-cache: 4.0.1
+
+ fill-range@7.1.1:
+ dependencies:
+ to-regex-range: 5.0.1
+
+ finalhandler@2.1.1:
+ dependencies:
+ debug: 4.4.3
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ on-finished: 2.4.1
+ parseurl: 1.3.3
+ statuses: 2.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ flat-cache@4.0.1:
+ dependencies:
+ flatted: 3.3.3
+ keyv: 4.5.4
+
+ flatted@3.3.3: {}
+
+ formdata-polyfill@4.0.10:
+ dependencies:
+ fetch-blob: 3.2.0
+
+ forwarded@0.2.0: {}
+
+ fresh@2.0.0: {}
+
+ fs-extra@11.3.3:
+ dependencies:
+ graceful-fs: 4.2.11
+ jsonfile: 6.2.0
+ universalify: 2.0.1
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ fuzzysort@3.1.0: {}
+
+ fzf@0.5.2: {}
+
+ gensync@1.0.0-beta.2: {}
+
+ get-caller-file@2.0.5: {}
+
+ get-east-asian-width@1.5.0: {}
+
+ get-intrinsic@1.3.0:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.2
+ math-intrinsics: 1.1.0
+
+ get-nonce@1.0.1: {}
+
+ get-own-enumerable-keys@1.0.0: {}
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.1
+
+ get-stream@6.0.1: {}
+
+ get-stream@9.0.1:
+ dependencies:
+ '@sec-ant/readable-stream': 0.4.1
+ is-stream: 4.0.1
+
+ get-tsconfig@4.13.6:
+ dependencies:
+ resolve-pkg-maps: 1.0.0
+
+ glob-parent@5.1.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ globals@14.0.0: {}
+
+ globals@16.5.0: {}
+
+ goober@2.1.18(csstype@3.2.3):
+ dependencies:
+ csstype: 3.2.3
+
+ gopd@1.2.0: {}
+
+ graceful-fs@4.2.11: {}
+
+ graphql@16.13.0: {}
+
+ has-flag@4.0.0: {}
+
+ has-symbols@1.1.0: {}
+
+ hasown@2.0.2:
+ dependencies:
+ function-bind: 1.1.2
+
+ hast-util-to-jsx-runtime@2.3.6:
+ dependencies:
+ '@types/estree': 1.0.8
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.3
+ comma-separated-tokens: 2.0.3
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ hast-util-whitespace: 3.0.0
+ mdast-util-mdx-expression: 2.0.1
+ mdast-util-mdx-jsx: 3.2.0
+ mdast-util-mdxjs-esm: 2.0.1
+ property-information: 7.1.0
+ space-separated-tokens: 2.0.2
+ style-to-js: 1.1.21
+ unist-util-position: 5.0.0
+ vfile-message: 4.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ hast-util-whitespace@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+
+ headers-polyfill@4.0.3: {}
+
+ hermes-estree@0.25.1: {}
+
+ hermes-parser@0.25.1:
+ dependencies:
+ hermes-estree: 0.25.1
+
+ hono@4.12.3: {}
+
+ html-parse-stringify@3.0.1:
+ dependencies:
+ void-elements: 3.1.0
+
+ html-url-attributes@3.0.1: {}
+
+ http-errors@2.0.1:
+ dependencies:
+ depd: 2.0.0
+ inherits: 2.0.4
+ setprototypeof: 1.2.0
+ statuses: 2.0.2
+ toidentifier: 1.0.1
+
+ https-proxy-agent@7.0.6:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ human-signals@2.1.0: {}
+
+ human-signals@8.0.1: {}
+
+ i18next-browser-languagedetector@8.2.1:
+ dependencies:
+ '@babel/runtime': 7.28.6
+
+ i18next@25.8.14(typescript@5.9.3):
+ dependencies:
+ '@babel/runtime': 7.28.6
+ optionalDependencies:
+ typescript: 5.9.3
+
+ iconv-lite@0.7.2:
+ dependencies:
+ safer-buffer: 2.1.2
+
+ ignore@5.3.2: {}
+
+ ignore@7.0.5: {}
+
+ import-fresh@3.3.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ imurmurhash@0.1.4: {}
+
+ inherits@2.0.4: {}
+
+ inline-style-parser@0.2.7: {}
+
+ ip-address@10.0.1: {}
+
+ ipaddr.js@1.9.1: {}
+
+ is-alphabetical@2.0.1: {}
+
+ is-alphanumerical@2.0.1:
+ dependencies:
+ is-alphabetical: 2.0.1
+ is-decimal: 2.0.1
+
+ is-arrayish@0.2.1: {}
+
+ is-binary-path@2.1.0:
+ dependencies:
+ binary-extensions: 2.3.0
+
+ is-decimal@2.0.1: {}
+
+ is-docker@3.0.0: {}
+
+ is-extglob@2.1.1: {}
+
+ is-fullwidth-code-point@3.0.0: {}
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-hexadecimal@2.0.1: {}
+
+ is-in-ssh@1.0.0: {}
+
+ is-inside-container@1.0.0:
+ dependencies:
+ is-docker: 3.0.0
+
+ is-interactive@2.0.0: {}
+
+ is-node-process@1.2.0: {}
+
+ is-number@7.0.0: {}
+
+ is-obj@3.0.0: {}
+
+ is-plain-obj@4.1.0: {}
+
+ is-promise@4.0.0: {}
+
+ is-regexp@3.1.0: {}
+
+ is-stream@2.0.1: {}
+
+ is-stream@4.0.1: {}
+
+ is-unicode-supported@1.3.0: {}
+
+ is-unicode-supported@2.1.0: {}
+
+ is-wsl@3.1.1:
+ dependencies:
+ is-inside-container: 1.0.0
+
+ isbot@5.1.35: {}
+
+ isexe@2.0.0: {}
+
+ isexe@3.1.5: {}
+
+ javascript-natural-sort@0.7.1: {}
+
+ jiti@2.6.1: {}
+
+ jose@6.1.3: {}
+
+ jotai@2.18.0(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4):
+ optionalDependencies:
+ '@babel/core': 7.29.0
+ '@babel/template': 7.28.6
+ '@types/react': 19.2.14
+ react: 19.2.4
+
+ js-tokens@4.0.0: {}
+
+ js-yaml@4.1.1:
+ dependencies:
+ argparse: 2.0.1
+
+ jsesc@3.1.0: {}
+
+ json-buffer@3.0.1: {}
+
+ json-parse-even-better-errors@2.3.1: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-schema-traverse@1.0.0: {}
+
+ json-schema-typed@8.0.2: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ json5@2.2.3: {}
+
+ jsonfile@6.2.0:
+ dependencies:
+ universalify: 2.0.1
+ optionalDependencies:
+ graceful-fs: 4.2.11
+
+ keyv@4.5.4:
+ dependencies:
+ json-buffer: 3.0.1
+
+ kleur@3.0.3: {}
+
+ kleur@4.1.5: {}
+
+ levn@0.4.1:
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+
+ lightningcss-android-arm64@1.31.1:
+ optional: true
+
+ lightningcss-darwin-arm64@1.31.1:
+ optional: true
+
+ lightningcss-darwin-x64@1.31.1:
+ optional: true
+
+ lightningcss-freebsd-x64@1.31.1:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.31.1:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.31.1:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.31.1:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.31.1:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.31.1:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.31.1:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.31.1:
+ optional: true
+
+ lightningcss@1.31.1:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.31.1
+ lightningcss-darwin-arm64: 1.31.1
+ lightningcss-darwin-x64: 1.31.1
+ lightningcss-freebsd-x64: 1.31.1
+ lightningcss-linux-arm-gnueabihf: 1.31.1
+ lightningcss-linux-arm64-gnu: 1.31.1
+ lightningcss-linux-arm64-musl: 1.31.1
+ lightningcss-linux-x64-gnu: 1.31.1
+ lightningcss-linux-x64-musl: 1.31.1
+ lightningcss-win32-arm64-msvc: 1.31.1
+ lightningcss-win32-x64-msvc: 1.31.1
+
+ lines-and-columns@1.2.4: {}
+
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ lodash-es@4.17.23: {}
+
+ lodash.merge@4.6.2: {}
+
+ log-symbols@6.0.0:
+ dependencies:
+ chalk: 5.6.2
+ is-unicode-supported: 1.3.0
+
+ longest-streak@3.1.0: {}
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ markdown-table@3.0.4: {}
+
+ math-intrinsics@1.1.0: {}
+
+ mdast-util-find-and-replace@3.0.2:
+ dependencies:
+ '@types/mdast': 4.0.4
+ escape-string-regexp: 5.0.0
+ unist-util-is: 6.0.1
+ unist-util-visit-parents: 6.0.2
+
+ mdast-util-from-markdown@2.0.3:
+ dependencies:
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ mdast-util-to-string: 4.0.0
+ micromark: 4.0.2
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-decode-string: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ unist-util-stringify-position: 4.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-gfm-autolink-literal@2.0.1:
+ dependencies:
+ '@types/mdast': 4.0.4
+ ccount: 2.0.1
+ devlop: 1.1.0
+ mdast-util-find-and-replace: 3.0.2
+ micromark-util-character: 2.1.1
+
+ mdast-util-gfm-footnote@2.1.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ micromark-util-normalize-identifier: 2.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-gfm-strikethrough@2.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-gfm-table@2.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ markdown-table: 3.0.4
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-gfm-task-list-item@2.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-gfm@3.1.0:
+ dependencies:
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-gfm-autolink-literal: 2.0.1
+ mdast-util-gfm-footnote: 2.1.0
+ mdast-util-gfm-strikethrough: 2.0.0
+ mdast-util-gfm-table: 2.0.0
+ mdast-util-gfm-task-list-item: 2.0.0
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdx-expression@2.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdx-jsx@3.2.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ ccount: 2.0.1
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ parse-entities: 4.0.2
+ stringify-entities: 4.0.4
+ unist-util-stringify-position: 4.0.0
+ vfile-message: 4.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdxjs-esm@2.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-phrasing@4.1.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ unist-util-is: 6.0.1
+
+ mdast-util-to-hast@13.2.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ '@ungap/structured-clone': 1.3.0
+ devlop: 1.1.0
+ micromark-util-sanitize-uri: 2.0.1
+ trim-lines: 3.0.1
+ unist-util-position: 5.0.0
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+
+ mdast-util-to-markdown@2.1.2:
+ dependencies:
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ longest-streak: 3.1.0
+ mdast-util-phrasing: 4.1.0
+ mdast-util-to-string: 4.0.0
+ micromark-util-classify-character: 2.0.1
+ micromark-util-decode-string: 2.0.1
+ unist-util-visit: 5.1.0
+ zwitch: 2.0.4
+
+ mdast-util-to-string@4.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+
+ media-typer@1.1.0: {}
+
+ merge-descriptors@2.0.0: {}
+
+ merge-stream@2.0.0: {}
+
+ merge2@1.4.1: {}
+
+ micromark-core-commonmark@2.0.3:
+ dependencies:
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ micromark-factory-destination: 2.0.1
+ micromark-factory-label: 2.0.1
+ micromark-factory-space: 2.0.1
+ micromark-factory-title: 2.0.1
+ micromark-factory-whitespace: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-chunked: 2.0.1
+ micromark-util-classify-character: 2.0.1
+ micromark-util-html-tag-name: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-subtokenize: 2.1.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-autolink-literal@2.1.0:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-sanitize-uri: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-footnote@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-core-commonmark: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-sanitize-uri: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-strikethrough@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-chunked: 2.0.1
+ micromark-util-classify-character: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-table@2.1.1:
+ dependencies:
+ devlop: 1.1.0
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-tagfilter@2.0.0:
+ dependencies:
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-task-list-item@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm@3.0.0:
+ dependencies:
+ micromark-extension-gfm-autolink-literal: 2.1.0
+ micromark-extension-gfm-footnote: 2.1.0
+ micromark-extension-gfm-strikethrough: 2.1.0
+ micromark-extension-gfm-table: 2.1.1
+ micromark-extension-gfm-tagfilter: 2.0.0
+ micromark-extension-gfm-task-list-item: 2.1.0
+ micromark-util-combine-extensions: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-destination@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-label@2.0.1:
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-space@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-title@2.0.1:
+ dependencies:
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-whitespace@2.0.1:
+ dependencies:
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-character@2.1.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-chunked@2.0.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-classify-character@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-combine-extensions@2.0.1:
+ dependencies:
+ micromark-util-chunked: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-decode-numeric-character-reference@2.0.2:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-decode-string@2.0.1:
+ dependencies:
+ decode-named-character-reference: 1.3.0
+ micromark-util-character: 2.1.1
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-encode@2.0.1: {}
+
+ micromark-util-html-tag-name@2.0.1: {}
+
+ micromark-util-normalize-identifier@2.0.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-resolve-all@2.0.1:
+ dependencies:
+ micromark-util-types: 2.0.2
+
+ micromark-util-sanitize-uri@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-encode: 2.0.1
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-subtokenize@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-chunked: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-symbol@2.0.1: {}
+
+ micromark-util-types@2.0.2: {}
+
+ micromark@4.0.2:
+ dependencies:
+ '@types/debug': 4.1.12
+ debug: 4.4.3
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ micromark-core-commonmark: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-chunked: 2.0.1
+ micromark-util-combine-extensions: 2.0.1
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-encode: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-sanitize-uri: 2.0.1
+ micromark-util-subtokenize: 2.1.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ micromatch@4.0.8:
+ dependencies:
+ braces: 3.0.3
+ picomatch: 2.3.1
+
+ mime-db@1.54.0: {}
+
+ mime-types@3.0.2:
+ dependencies:
+ mime-db: 1.54.0
+
+ mimic-fn@2.1.0: {}
+
+ mimic-function@5.0.1: {}
+
+ minimatch@10.2.4:
+ dependencies:
+ brace-expansion: 5.0.4
+
+ minimatch@3.1.5:
+ dependencies:
+ brace-expansion: 1.1.12
+
+ minimatch@9.0.9:
+ dependencies:
+ brace-expansion: 2.0.2
+
+ minimist@1.2.8: {}
+
+ ms@2.1.3: {}
+
+ msw@2.12.10(@types/node@24.11.0)(typescript@5.9.3):
+ dependencies:
+ '@inquirer/confirm': 5.1.21(@types/node@24.11.0)
+ '@mswjs/interceptors': 0.41.3
+ '@open-draft/deferred-promise': 2.2.0
+ '@types/statuses': 2.0.6
+ cookie: 1.1.1
+ graphql: 16.13.0
+ headers-polyfill: 4.0.3
+ is-node-process: 1.2.0
+ outvariant: 1.4.3
+ path-to-regexp: 6.3.0
+ picocolors: 1.1.1
+ rettime: 0.10.1
+ statuses: 2.0.2
+ strict-event-emitter: 0.5.1
+ tough-cookie: 6.0.0
+ type-fest: 5.4.4
+ until-async: 3.0.2
+ yargs: 17.7.2
+ optionalDependencies:
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - '@types/node'
+
+ mute-stream@2.0.0: {}
+
+ nanoid@3.3.11: {}
+
+ natural-compare@1.4.0: {}
+
+ negotiator@1.0.0: {}
+
+ node-domexception@1.0.0: {}
+
+ node-fetch@3.3.2:
+ dependencies:
+ data-uri-to-buffer: 4.0.1
+ fetch-blob: 3.2.0
+ formdata-polyfill: 4.0.10
+
+ node-releases@2.0.27: {}
+
+ normalize-path@3.0.0: {}
+
+ npm-run-path@4.0.1:
+ dependencies:
+ path-key: 3.1.1
+
+ npm-run-path@6.0.0:
+ dependencies:
+ path-key: 4.0.0
+ unicorn-magic: 0.3.0
+
+ object-assign@4.1.1: {}
+
+ object-inspect@1.13.4: {}
+
+ object-treeify@1.1.33: {}
+
+ on-finished@2.4.1:
+ dependencies:
+ ee-first: 1.1.1
+
+ once@1.4.0:
+ dependencies:
+ wrappy: 1.0.2
+
+ onetime@5.1.2:
+ dependencies:
+ mimic-fn: 2.1.0
+
+ onetime@7.0.0:
+ dependencies:
+ mimic-function: 5.0.1
+
+ open@11.0.0:
+ dependencies:
+ default-browser: 5.5.0
+ define-lazy-prop: 3.0.0
+ is-in-ssh: 1.0.0
+ is-inside-container: 1.0.0
+ powershell-utils: 0.1.0
+ wsl-utils: 0.3.1
+
+ optionator@0.9.4:
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+
+ ora@8.2.0:
+ dependencies:
+ chalk: 5.6.2
+ cli-cursor: 5.0.0
+ cli-spinners: 2.9.2
+ is-interactive: 2.0.0
+ is-unicode-supported: 2.1.0
+ log-symbols: 6.0.0
+ stdin-discarder: 0.2.2
+ string-width: 7.2.0
+ strip-ansi: 7.2.0
+
+ outvariant@1.4.3: {}
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ package-manager-detector@1.6.0: {}
+
+ parent-module@1.0.1:
+ dependencies:
+ callsites: 3.1.0
+
+ parse-entities@4.0.2:
+ dependencies:
+ '@types/unist': 2.0.11
+ character-entities-legacy: 3.0.0
+ character-reference-invalid: 2.0.1
+ decode-named-character-reference: 1.3.0
+ is-alphanumerical: 2.0.1
+ is-decimal: 2.0.1
+ is-hexadecimal: 2.0.1
+
+ parse-imports-exports@0.2.4:
+ dependencies:
+ parse-statements: 1.0.11
+
+ parse-json@5.2.0:
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ error-ex: 1.3.4
+ json-parse-even-better-errors: 2.3.1
+ lines-and-columns: 1.2.4
+
+ parse-ms@4.0.0: {}
+
+ parse-statements@1.0.11: {}
+
+ parseurl@1.3.3: {}
+
+ path-browserify@1.0.1: {}
+
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ path-key@4.0.0: {}
+
+ path-to-regexp@6.3.0: {}
+
+ path-to-regexp@8.3.0: {}
+
+ pathe@2.0.3: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@2.3.1: {}
+
+ picomatch@4.0.3: {}
+
+ pkce-challenge@5.0.1: {}
+
+ postcss-selector-parser@6.0.10:
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+
+ postcss-selector-parser@7.1.1:
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+
+ postcss@8.5.6:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ powershell-utils@0.1.0: {}
+
+ prelude-ls@1.2.1: {}
+
+ prettier-plugin-tailwindcss@0.7.2(@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.1))(prettier@3.8.1):
+ dependencies:
+ prettier: 3.8.1
+ optionalDependencies:
+ '@trivago/prettier-plugin-sort-imports': 6.0.2(prettier@3.8.1)
+
+ prettier@3.8.1: {}
+
+ pretty-ms@9.3.0:
+ dependencies:
+ parse-ms: 4.0.0
+
+ prompts@2.4.2:
+ dependencies:
+ kleur: 3.0.3
+ sisteransi: 1.0.5
+
+ property-information@7.1.0: {}
+
+ proxy-addr@2.0.7:
+ dependencies:
+ forwarded: 0.2.0
+ ipaddr.js: 1.9.1
+
+ punycode@2.3.1: {}
+
+ qs@6.15.0:
+ dependencies:
+ side-channel: 1.1.0
+
+ queue-microtask@1.2.3: {}
+
+ radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-form': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ range-parser@1.2.1: {}
+
+ raw-body@3.0.2:
+ dependencies:
+ bytes: 3.1.2
+ http-errors: 2.0.1
+ iconv-lite: 0.7.2
+ unpipe: 1.0.0
+
+ react-dom@19.2.4(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ scheduler: 0.27.0
+
+ react-i18next@16.5.4(i18next@25.8.14(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3):
+ dependencies:
+ '@babel/runtime': 7.28.6
+ html-parse-stringify: 3.0.1
+ i18next: 25.8.14(typescript@5.9.3)
+ react: 19.2.4
+ use-sync-external-store: 1.6.0(react@19.2.4)
+ optionalDependencies:
+ react-dom: 19.2.4(react@19.2.4)
+ typescript: 5.9.3
+
+ react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ '@types/react': 19.2.14
+ devlop: 1.1.0
+ hast-util-to-jsx-runtime: 2.3.6
+ html-url-attributes: 3.0.1
+ mdast-util-to-hast: 13.2.1
+ react: 19.2.4
+ remark-parse: 11.0.0
+ remark-rehype: 11.1.2
+ unified: 11.0.5
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ react-refresh@0.18.0: {}
+
+ react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4)
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.4)
+ react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4)
+ tslib: 2.8.1
+ use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.4)
+ use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ get-nonce: 1.0.1
+ react: 19.2.4
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ react-textarea-autosize@8.5.9(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ '@babel/runtime': 7.28.6
+ react: 19.2.4
+ use-composed-ref: 1.4.0(@types/react@19.2.14)(react@19.2.4)
+ use-latest: 1.3.0(@types/react@19.2.14)(react@19.2.4)
+ transitivePeerDependencies:
+ - '@types/react'
+
+ react@19.2.4: {}
+
+ readdirp@3.6.0:
+ dependencies:
+ picomatch: 2.3.1
+
+ recast@0.23.11:
+ dependencies:
+ ast-types: 0.16.1
+ esprima: 4.0.1
+ source-map: 0.6.1
+ tiny-invariant: 1.3.3
+ tslib: 2.8.1
+
+ remark-gfm@4.0.1:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-gfm: 3.1.0
+ micromark-extension-gfm: 3.0.0
+ remark-parse: 11.0.0
+ remark-stringify: 11.0.0
+ unified: 11.0.5
+ transitivePeerDependencies:
+ - supports-color
+
+ remark-parse@11.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-from-markdown: 2.0.3
+ micromark-util-types: 2.0.2
+ unified: 11.0.5
+ transitivePeerDependencies:
+ - supports-color
+
+ remark-rehype@11.1.2:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ mdast-util-to-hast: 13.2.1
+ unified: 11.0.5
+ vfile: 6.0.3
+
+ remark-stringify@11.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-to-markdown: 2.1.2
+ unified: 11.0.5
+
+ require-directory@2.1.1: {}
+
+ require-from-string@2.0.2: {}
+
+ resolve-from@4.0.0: {}
+
+ resolve-pkg-maps@1.0.0: {}
+
+ restore-cursor@5.1.0:
+ dependencies:
+ onetime: 7.0.0
+ signal-exit: 4.1.0
+
+ rettime@0.10.1: {}
+
+ reusify@1.1.0: {}
+
+ rollup@4.59.0:
+ dependencies:
+ '@types/estree': 1.0.8
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.59.0
+ '@rollup/rollup-android-arm64': 4.59.0
+ '@rollup/rollup-darwin-arm64': 4.59.0
+ '@rollup/rollup-darwin-x64': 4.59.0
+ '@rollup/rollup-freebsd-arm64': 4.59.0
+ '@rollup/rollup-freebsd-x64': 4.59.0
+ '@rollup/rollup-linux-arm-gnueabihf': 4.59.0
+ '@rollup/rollup-linux-arm-musleabihf': 4.59.0
+ '@rollup/rollup-linux-arm64-gnu': 4.59.0
+ '@rollup/rollup-linux-arm64-musl': 4.59.0
+ '@rollup/rollup-linux-loong64-gnu': 4.59.0
+ '@rollup/rollup-linux-loong64-musl': 4.59.0
+ '@rollup/rollup-linux-ppc64-gnu': 4.59.0
+ '@rollup/rollup-linux-ppc64-musl': 4.59.0
+ '@rollup/rollup-linux-riscv64-gnu': 4.59.0
+ '@rollup/rollup-linux-riscv64-musl': 4.59.0
+ '@rollup/rollup-linux-s390x-gnu': 4.59.0
+ '@rollup/rollup-linux-x64-gnu': 4.59.0
+ '@rollup/rollup-linux-x64-musl': 4.59.0
+ '@rollup/rollup-openbsd-x64': 4.59.0
+ '@rollup/rollup-openharmony-arm64': 4.59.0
+ '@rollup/rollup-win32-arm64-msvc': 4.59.0
+ '@rollup/rollup-win32-ia32-msvc': 4.59.0
+ '@rollup/rollup-win32-x64-gnu': 4.59.0
+ '@rollup/rollup-win32-x64-msvc': 4.59.0
+ fsevents: 2.3.3
+
+ router@2.2.0:
+ dependencies:
+ debug: 4.4.3
+ depd: 2.0.0
+ is-promise: 4.0.0
+ parseurl: 1.3.3
+ path-to-regexp: 8.3.0
+ transitivePeerDependencies:
+ - supports-color
+
+ run-applescript@7.1.0: {}
+
+ run-parallel@1.2.0:
+ dependencies:
+ queue-microtask: 1.2.3
+
+ safer-buffer@2.1.2: {}
+
+ scheduler@0.27.0: {}
+
+ semver@6.3.1: {}
+
+ semver@7.7.4: {}
+
+ send@1.2.1:
+ dependencies:
+ debug: 4.4.3
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ etag: 1.8.1
+ fresh: 2.0.0
+ http-errors: 2.0.1
+ mime-types: 3.0.2
+ ms: 2.1.3
+ on-finished: 2.4.1
+ range-parser: 1.2.1
+ statuses: 2.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ seroval-plugins@1.5.0(seroval@1.5.0):
+ dependencies:
+ seroval: 1.5.0
+
+ seroval@1.5.0: {}
+
+ serve-static@2.2.1:
+ dependencies:
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ parseurl: 1.3.3
+ send: 1.2.1
+ transitivePeerDependencies:
+ - supports-color
+
+ setprototypeof@1.2.0: {}
+
+ shadcn@3.8.5(@types/node@24.11.0)(typescript@5.9.3):
+ dependencies:
+ '@antfu/ni': 25.0.0
+ '@babel/core': 7.29.0
+ '@babel/parser': 7.29.0
+ '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
+ '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0)
+ '@dotenvx/dotenvx': 1.52.0
+ '@modelcontextprotocol/sdk': 1.27.1(zod@3.25.76)
+ '@types/validate-npm-package-name': 4.0.2
+ browserslist: 4.28.1
+ commander: 14.0.3
+ cosmiconfig: 9.0.0(typescript@5.9.3)
+ dedent: 1.7.2
+ deepmerge: 4.3.1
+ diff: 8.0.3
+ execa: 9.6.1
+ fast-glob: 3.3.3
+ fs-extra: 11.3.3
+ fuzzysort: 3.1.0
+ https-proxy-agent: 7.0.6
+ kleur: 4.1.5
+ msw: 2.12.10(@types/node@24.11.0)(typescript@5.9.3)
+ node-fetch: 3.3.2
+ open: 11.0.0
+ ora: 8.2.0
+ postcss: 8.5.6
+ postcss-selector-parser: 7.1.1
+ prompts: 2.4.2
+ recast: 0.23.11
+ stringify-object: 5.0.0
+ tailwind-merge: 3.5.0
+ ts-morph: 26.0.0
+ tsconfig-paths: 4.2.0
+ validate-npm-package-name: 7.0.2
+ zod: 3.25.76
+ zod-to-json-schema: 3.25.1(zod@3.25.76)
+ transitivePeerDependencies:
+ - '@cfworker/json-schema'
+ - '@types/node'
+ - babel-plugin-macros
+ - supports-color
+ - typescript
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ side-channel-list@1.0.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-map@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-weakmap@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-map: 1.0.1
+
+ side-channel@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-list: 1.0.0
+ side-channel-map: 1.0.1
+ side-channel-weakmap: 1.0.2
+
+ signal-exit@3.0.7: {}
+
+ signal-exit@4.1.0: {}
+
+ sisteransi@1.0.5: {}
+
+ sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+
+ source-map-js@1.2.1: {}
+
+ source-map@0.6.1: {}
+
+ source-map@0.7.6: {}
+
+ space-separated-tokens@2.0.2: {}
+
+ statuses@2.0.2: {}
+
+ stdin-discarder@0.2.2: {}
+
+ strict-event-emitter@0.5.1: {}
+
+ string-width@4.2.3:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+
+ string-width@7.2.0:
+ dependencies:
+ emoji-regex: 10.6.0
+ get-east-asian-width: 1.5.0
+ strip-ansi: 7.2.0
+
+ stringify-entities@4.0.4:
+ dependencies:
+ character-entities-html4: 2.1.0
+ character-entities-legacy: 3.0.0
+
+ stringify-object@5.0.0:
+ dependencies:
+ get-own-enumerable-keys: 1.0.0
+ is-obj: 3.0.0
+ is-regexp: 3.1.0
+
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
+
+ strip-ansi@7.2.0:
+ dependencies:
+ ansi-regex: 6.2.2
+
+ strip-bom@3.0.0: {}
+
+ strip-final-newline@2.0.0: {}
+
+ strip-final-newline@4.0.0: {}
+
+ strip-json-comments@3.1.1: {}
+
+ style-to-js@1.1.21:
+ dependencies:
+ style-to-object: 1.0.14
+
+ style-to-object@1.0.14:
+ dependencies:
+ inline-style-parser: 0.2.7
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ tagged-tag@1.0.0: {}
+
+ tailwind-merge@3.5.0: {}
+
+ tailwindcss@4.2.1: {}
+
+ tapable@2.3.0: {}
+
+ tiny-invariant@1.3.3: {}
+
+ tiny-warning@1.0.3: {}
+
+ tinyexec@1.0.2: {}
+
+ tinyglobby@0.2.15:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+
+ tldts-core@7.0.23: {}
+
+ tldts@7.0.23:
+ dependencies:
+ tldts-core: 7.0.23
+
+ to-regex-range@5.0.1:
+ dependencies:
+ is-number: 7.0.0
+
+ toidentifier@1.0.1: {}
+
+ tough-cookie@6.0.0:
+ dependencies:
+ tldts: 7.0.23
+
+ trim-lines@3.0.1: {}
+
+ trough@2.2.0: {}
+
+ ts-api-utils@2.4.0(typescript@5.9.3):
+ dependencies:
+ typescript: 5.9.3
+
+ ts-morph@26.0.0:
+ dependencies:
+ '@ts-morph/common': 0.27.0
+ code-block-writer: 13.0.3
+
+ tsconfig-paths@4.2.0:
+ dependencies:
+ json5: 2.2.3
+ minimist: 1.2.8
+ strip-bom: 3.0.0
+
+ tslib@2.8.1: {}
+
+ tsx@4.21.0:
+ dependencies:
+ esbuild: 0.27.3
+ get-tsconfig: 4.13.6
+ optionalDependencies:
+ fsevents: 2.3.3
+
+ tw-animate-css@1.4.0: {}
+
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ type-fest@5.4.4:
+ dependencies:
+ tagged-tag: 1.0.0
+
+ type-is@2.0.1:
+ dependencies:
+ content-type: 1.0.5
+ media-typer: 1.1.0
+ mime-types: 3.0.2
+
+ typescript-eslint@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3):
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/parser': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)
+ eslint: 9.39.3(jiti@2.6.1)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ typescript@5.9.3: {}
+
+ undici-types@7.16.0: {}
+
+ unicorn-magic@0.3.0: {}
+
+ unified@11.0.5:
+ dependencies:
+ '@types/unist': 3.0.3
+ bail: 2.0.2
+ devlop: 1.1.0
+ extend: 3.0.2
+ is-plain-obj: 4.1.0
+ trough: 2.2.0
+ vfile: 6.0.3
+
+ unist-util-is@6.0.1:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-position@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-stringify-position@4.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-visit-parents@6.0.2:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+
+ unist-util-visit@5.1.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+ unist-util-visit-parents: 6.0.2
+
+ universalify@2.0.1: {}
+
+ unpipe@1.0.0: {}
+
+ unplugin@2.3.11:
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ acorn: 8.16.0
+ picomatch: 4.0.3
+ webpack-virtual-modules: 0.6.2
+
+ until-async@3.0.2: {}
+
+ update-browserslist-db@1.2.3(browserslist@4.28.1):
+ dependencies:
+ browserslist: 4.28.1
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ use-composed-ref@1.4.0(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ use-isomorphic-layout-effect@1.2.1(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ use-latest@1.3.0(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.14)(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ detect-node-es: 1.1.0
+ react: 19.2.4
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ use-sync-external-store@1.6.0(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+
+ util-deprecate@1.0.2: {}
+
+ validate-npm-package-name@7.0.2: {}
+
+ vary@1.1.2: {}
+
+ vfile-message@4.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-stringify-position: 4.0.0
+
+ vfile@6.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ vfile-message: 4.0.3
+
+ vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0):
+ dependencies:
+ esbuild: 0.27.3
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+ postcss: 8.5.6
+ rollup: 4.59.0
+ tinyglobby: 0.2.15
+ optionalDependencies:
+ '@types/node': 24.11.0
+ fsevents: 2.3.3
+ jiti: 2.6.1
+ lightningcss: 1.31.1
+ tsx: 4.21.0
+
+ void-elements@3.1.0: {}
+
+ web-streams-polyfill@3.3.3: {}
+
+ webpack-virtual-modules@0.6.2: {}
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ which@4.0.0:
+ dependencies:
+ isexe: 3.1.5
+
+ word-wrap@1.2.5: {}
+
+ wrap-ansi@6.2.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ wrap-ansi@7.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ wrappy@1.0.2: {}
+
+ wsl-utils@0.3.1:
+ dependencies:
+ is-wsl: 3.1.1
+ powershell-utils: 0.1.0
+
+ y18n@5.0.8: {}
+
+ yallist@3.1.1: {}
+
+ yargs-parser@21.1.1: {}
+
+ yargs@17.7.2:
+ dependencies:
+ cliui: 8.0.1
+ escalade: 3.2.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ string-width: 4.2.3
+ y18n: 5.0.8
+ yargs-parser: 21.1.1
+
+ yocto-queue@0.1.0: {}
+
+ yoctocolors-cjs@2.1.3: {}
+
+ yoctocolors@2.1.2: {}
+
+ zod-to-json-schema@3.25.1(zod@3.25.76):
+ dependencies:
+ zod: 3.25.76
+
+ zod-validation-error@4.0.2(zod@4.3.6):
+ dependencies:
+ zod: 4.3.6
+
+ zod@3.25.76: {}
+
+ zod@4.3.6: {}
+
+ zwitch@2.0.4: {}
diff --git a/web/frontend/prettier.config.js b/web/frontend/prettier.config.js
new file mode 100644
index 000000000..492ef1dd7
--- /dev/null
+++ b/web/frontend/prettier.config.js
@@ -0,0 +1,17 @@
+// @ts-check
+
+/** @type {import('prettier').Config} */
+const config = {
+ semi: false,
+ printWidth: 80,
+ tabWidth: 2,
+ importOrder: ["", "", "^@/", "^[./]"],
+ importOrderSeparation: true,
+ importOrderSortSpecifiers: true,
+ plugins: [
+ "@trivago/prettier-plugin-sort-imports",
+ "prettier-plugin-tailwindcss",
+ ],
+}
+
+export default config
diff --git a/web/frontend/public/apple-touch-icon.png b/web/frontend/public/apple-touch-icon.png
new file mode 100644
index 000000000..d881c64af
Binary files /dev/null and b/web/frontend/public/apple-touch-icon.png differ
diff --git a/web/frontend/public/favicon-96x96.png b/web/frontend/public/favicon-96x96.png
new file mode 100644
index 000000000..5bdeccea5
Binary files /dev/null and b/web/frontend/public/favicon-96x96.png differ
diff --git a/web/frontend/public/favicon.ico b/web/frontend/public/favicon.ico
new file mode 100644
index 000000000..8b46b4b26
Binary files /dev/null and b/web/frontend/public/favicon.ico differ
diff --git a/web/frontend/public/favicon.svg b/web/frontend/public/favicon.svg
new file mode 100644
index 000000000..e2f412b70
--- /dev/null
+++ b/web/frontend/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/frontend/public/lark.svg b/web/frontend/public/lark.svg
new file mode 100644
index 000000000..0761f278f
--- /dev/null
+++ b/web/frontend/public/lark.svg
@@ -0,0 +1 @@
+
diff --git a/web/frontend/public/logo_with_text.png b/web/frontend/public/logo_with_text.png
new file mode 100644
index 000000000..70f26788c
Binary files /dev/null and b/web/frontend/public/logo_with_text.png differ
diff --git a/web/frontend/public/site.webmanifest b/web/frontend/public/site.webmanifest
new file mode 100644
index 000000000..981d97f15
--- /dev/null
+++ b/web/frontend/public/site.webmanifest
@@ -0,0 +1,21 @@
+{
+ "name": "MyWebSite",
+ "short_name": "MySite",
+ "icons": [
+ {
+ "src": "/web-app-manifest-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/web-app-manifest-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
diff --git a/web/frontend/public/web-app-manifest-192x192.png b/web/frontend/public/web-app-manifest-192x192.png
new file mode 100644
index 000000000..01933339b
Binary files /dev/null and b/web/frontend/public/web-app-manifest-192x192.png differ
diff --git a/web/frontend/public/web-app-manifest-512x512.png b/web/frontend/public/web-app-manifest-512x512.png
new file mode 100644
index 000000000..e0b4aab9c
Binary files /dev/null and b/web/frontend/public/web-app-manifest-512x512.png differ
diff --git a/web/frontend/src/api/channels.ts b/web/frontend/src/api/channels.ts
new file mode 100644
index 000000000..ecd77632c
--- /dev/null
+++ b/web/frontend/src/api/channels.ts
@@ -0,0 +1,65 @@
+// API client for channels navigation and channel-specific config flows.
+
+export type ChannelConfig = Record
+export type AppConfig = Record
+
+export interface SupportedChannel {
+ name: string
+ display_name?: string
+ config_key: string
+ variant?: string
+}
+
+interface ChannelsCatalogResponse {
+ channels: SupportedChannel[]
+}
+
+interface ConfigActionResponse {
+ status: string
+ errors?: string[]
+}
+
+const BASE_URL = ""
+
+async function request(path: string, options?: RequestInit): Promise {
+ const res = await fetch(`${BASE_URL}${path}`, options)
+ if (!res.ok) {
+ let message = `API error: ${res.status} ${res.statusText}`
+ try {
+ const body = (await res.json()) as {
+ error?: string
+ errors?: string[]
+ status?: string
+ }
+ if (Array.isArray(body.errors) && body.errors.length > 0) {
+ message = body.errors.join("; ")
+ } else if (typeof body.error === "string" && body.error.trim() !== "") {
+ message = body.error
+ }
+ } catch {
+ // Keep default fallback message if response body is not JSON.
+ }
+ throw new Error(message)
+ }
+ return res.json() as Promise
+}
+
+export async function getChannelsCatalog(): Promise {
+ return request("/api/channels/catalog")
+}
+
+export async function getAppConfig(): Promise {
+ return request("/api/config")
+}
+
+export async function patchAppConfig(
+ patch: Record,
+): Promise {
+ return request("/api/config", {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(patch),
+ })
+}
+
+export type { ChannelsCatalogResponse, ConfigActionResponse }
diff --git a/web/frontend/src/api/gateway.ts b/web/frontend/src/api/gateway.ts
new file mode 100644
index 000000000..5a58d48f0
--- /dev/null
+++ b/web/frontend/src/api/gateway.ts
@@ -0,0 +1,62 @@
+// API client for gateway process management.
+
+interface GatewayStatusResponse {
+ gateway_status: "running" | "starting" | "stopped" | "error"
+ gateway_start_allowed?: boolean
+ gateway_start_reason?: string
+ pid?: number
+ logs?: string[]
+ log_total?: number
+ log_run_id?: number
+ [key: string]: unknown
+}
+
+interface GatewayActionResponse {
+ status: string
+ pid?: number
+}
+
+const BASE_URL = ""
+
+async function request(path: string, options?: RequestInit): Promise {
+ const res = await fetch(`${BASE_URL}${path}`, options)
+ if (!res.ok) {
+ throw new Error(`API error: ${res.status} ${res.statusText}`)
+ }
+ return res.json() as Promise
+}
+
+export async function getGatewayStatus(options?: {
+ log_offset?: number
+ log_run_id?: number
+}): Promise {
+ const params = new URLSearchParams()
+ if (options?.log_offset !== undefined) {
+ params.set("log_offset", options.log_offset.toString())
+ }
+ if (options?.log_run_id !== undefined) {
+ params.set("log_run_id", options.log_run_id.toString())
+ }
+ const queryString = params.toString() ? `?${params.toString()}` : ""
+ return request(`/api/gateway/status${queryString}`)
+}
+
+export async function startGateway(): Promise {
+ return request("/api/gateway/start", {
+ method: "POST",
+ })
+}
+
+export async function stopGateway(): Promise {
+ return request("/api/gateway/stop", {
+ method: "POST",
+ })
+}
+
+export async function restartGateway(): Promise {
+ return request("/api/gateway/restart", {
+ method: "POST",
+ })
+}
+
+export type { GatewayStatusResponse, GatewayActionResponse }
diff --git a/web/frontend/src/api/models.ts b/web/frontend/src/api/models.ts
new file mode 100644
index 000000000..6a4544c65
--- /dev/null
+++ b/web/frontend/src/api/models.ts
@@ -0,0 +1,91 @@
+import { refreshGatewayState } from "@/store/gateway"
+
+// API client for model list management.
+
+export interface ModelInfo {
+ index: number
+ model_name: string
+ model: string
+ api_base?: string
+ api_key: string
+ proxy?: string
+ auth_method?: string
+ // Advanced fields
+ connect_mode?: string
+ workspace?: string
+ rpm?: number
+ max_tokens_field?: string
+ request_timeout?: number
+ thinking_level?: string
+ // Meta
+ configured: boolean
+ is_default: boolean
+}
+
+interface ModelsListResponse {
+ models: ModelInfo[]
+ total: number
+ default_model: string
+}
+
+interface ModelActionResponse {
+ status: string
+ index?: number
+ default_model?: string
+}
+
+const BASE_URL = ""
+
+async function request(path: string, options?: RequestInit): Promise {
+ const res = await fetch(`${BASE_URL}${path}`, options)
+ if (!res.ok) {
+ throw new Error(`API error: ${res.status} ${res.statusText}`)
+ }
+ return res.json() as Promise
+}
+
+export async function getModels(): Promise {
+ return request("/api/models")
+}
+
+export async function addModel(
+ model: Partial,
+): Promise {
+ return request("/api/models", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(model),
+ })
+}
+
+export async function updateModel(
+ index: number,
+ model: Partial,
+): Promise {
+ return request(`/api/models/${index}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(model),
+ })
+}
+
+export async function deleteModel(index: number): Promise {
+ return request(`/api/models/${index}`, {
+ method: "DELETE",
+ })
+}
+
+export async function setDefaultModel(
+ modelName: string,
+): Promise {
+ const response = await request("/api/models/default", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ model_name: modelName }),
+ })
+
+ void refreshGatewayState()
+ return response
+}
+
+export type { ModelsListResponse, ModelActionResponse }
diff --git a/web/frontend/src/api/oauth.ts b/web/frontend/src/api/oauth.ts
new file mode 100644
index 000000000..a1ed1afcb
--- /dev/null
+++ b/web/frontend/src/api/oauth.ts
@@ -0,0 +1,102 @@
+export type OAuthProvider = "openai" | "anthropic" | "google-antigravity"
+export type OAuthMethod = "browser" | "device_code" | "token"
+
+export interface OAuthProviderStatus {
+ provider: OAuthProvider
+ display_name: string
+ methods: OAuthMethod[]
+ logged_in: boolean
+ status: "connected" | "expired" | "needs_refresh" | "not_logged_in"
+ auth_method?: string
+ expires_at?: string
+ account_id?: string
+ email?: string
+ project_id?: string
+}
+
+export interface OAuthFlowState {
+ flow_id: string
+ provider: OAuthProvider
+ method: OAuthMethod
+ status: "pending" | "success" | "error" | "expired"
+ expires_at?: string
+ error?: string
+ user_code?: string
+ verify_url?: string
+ interval?: number
+}
+
+export interface OAuthLoginRequest {
+ provider: OAuthProvider
+ method: OAuthMethod
+ token?: string
+}
+
+export interface OAuthLoginResponse {
+ status: string
+ provider: OAuthProvider
+ method: OAuthMethod
+ flow_id?: string
+ auth_url?: string
+ user_code?: string
+ verify_url?: string
+ interval?: number
+ expires_at?: string
+}
+
+interface OAuthProvidersResponse {
+ providers: OAuthProviderStatus[]
+}
+
+const BASE_URL = ""
+
+async function request(path: string, options?: RequestInit): Promise {
+ const res = await fetch(`${BASE_URL}${path}`, options)
+ if (!res.ok) {
+ const message = await res.text()
+ throw new Error(message || `API error: ${res.status} ${res.statusText}`)
+ }
+ return res.json() as Promise
+}
+
+export async function getOAuthProviders(): Promise {
+ return request("/api/oauth/providers")
+}
+
+export async function loginOAuth(
+ payload: OAuthLoginRequest,
+): Promise {
+ return request("/api/oauth/login", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ })
+}
+
+export async function getOAuthFlow(flowID: string): Promise {
+ return request(
+ `/api/oauth/flows/${encodeURIComponent(flowID)}`,
+ )
+}
+
+export async function pollOAuthFlow(flowID: string): Promise {
+ return request(
+ `/api/oauth/flows/${encodeURIComponent(flowID)}/poll`,
+ {
+ method: "POST",
+ },
+ )
+}
+
+export async function logoutOAuth(
+ provider: OAuthProvider,
+): Promise<{ status: string; provider: OAuthProvider }> {
+ return request<{ status: string; provider: OAuthProvider }>(
+ "/api/oauth/logout",
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ provider }),
+ },
+ )
+}
diff --git a/web/frontend/src/api/pico.ts b/web/frontend/src/api/pico.ts
new file mode 100644
index 000000000..9a1a553d5
--- /dev/null
+++ b/web/frontend/src/api/pico.ts
@@ -0,0 +1,38 @@
+// API client for Pico Channel configuration.
+
+interface PicoTokenResponse {
+ token: string
+ ws_url: string
+ enabled: boolean
+}
+
+interface PicoSetupResponse {
+ token: string
+ ws_url: string
+ enabled: boolean
+ changed: boolean
+}
+
+const BASE_URL = ""
+
+async function request(path: string, options?: RequestInit): Promise {
+ const res = await fetch(`${BASE_URL}${path}`, options)
+ if (!res.ok) {
+ throw new Error(`API error: ${res.status} ${res.statusText}`)
+ }
+ return res.json() as Promise
+}
+
+export async function getPicoToken(): Promise {
+ return request("/api/pico/token")
+}
+
+export async function regenPicoToken(): Promise {
+ return request("/api/pico/token", { method: "POST" })
+}
+
+export async function setupPico(): Promise {
+ return request("/api/pico/setup", { method: "POST" })
+}
+
+export type { PicoTokenResponse, PicoSetupResponse }
diff --git a/web/frontend/src/api/sessions.ts b/web/frontend/src/api/sessions.ts
new file mode 100644
index 000000000..56ef148db
--- /dev/null
+++ b/web/frontend/src/api/sessions.ts
@@ -0,0 +1,50 @@
+// Sessions API — list and retrieve chat session history
+
+export interface SessionSummary {
+ id: string
+ preview: string
+ message_count: number
+ created: string
+ updated: string
+}
+
+export interface SessionDetail {
+ id: string
+ messages: { role: "user" | "assistant"; content: string }[]
+ summary: string
+ created: string
+ updated: string
+}
+
+export async function getSessions(
+ offset: number = 0,
+ limit: number = 20,
+): Promise {
+ const params = new URLSearchParams({
+ offset: offset.toString(),
+ limit: limit.toString(),
+ })
+
+ const res = await fetch(`/api/sessions?${params.toString()}`)
+ if (!res.ok) {
+ throw new Error(`Failed to fetch sessions: ${res.status}`)
+ }
+ return res.json()
+}
+
+export async function getSessionHistory(id: string): Promise {
+ const res = await fetch(`/api/sessions/${encodeURIComponent(id)}`)
+ if (!res.ok) {
+ throw new Error(`Failed to fetch session ${id}: ${res.status}`)
+ }
+ return res.json()
+}
+
+export async function deleteSession(id: string): Promise {
+ const res = await fetch(`/api/sessions/${encodeURIComponent(id)}`, {
+ method: "DELETE",
+ })
+ if (!res.ok) {
+ throw new Error(`Failed to delete session ${id}: ${res.status}`)
+ }
+}
diff --git a/web/frontend/src/api/system.ts b/web/frontend/src/api/system.ts
new file mode 100644
index 000000000..543c8694d
--- /dev/null
+++ b/web/frontend/src/api/system.ts
@@ -0,0 +1,62 @@
+export interface AutoStartStatus {
+ enabled: boolean
+ supported: boolean
+ platform: string
+ message?: string
+}
+
+export interface LauncherConfig {
+ port: number
+ public: boolean
+ allowed_cidrs: string[]
+}
+
+async function request(path: string, options?: RequestInit): Promise {
+ const res = await fetch(path, options)
+ if (!res.ok) {
+ let message = `API error: ${res.status} ${res.statusText}`
+ try {
+ const body = (await res.json()) as {
+ error?: string
+ errors?: string[]
+ }
+ if (Array.isArray(body.errors) && body.errors.length > 0) {
+ message = body.errors.join("; ")
+ } else if (typeof body.error === "string" && body.error.trim() !== "") {
+ message = body.error
+ }
+ } catch {
+ // Keep fallback error message when response body is not JSON.
+ }
+ throw new Error(message)
+ }
+ return res.json() as Promise
+}
+
+export async function getAutoStartStatus(): Promise {
+ return request("/api/system/autostart")
+}
+
+export async function setAutoStartEnabled(
+ enabled: boolean,
+): Promise {
+ return request("/api/system/autostart", {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ enabled }),
+ })
+}
+
+export async function getLauncherConfig(): Promise {
+ return request("/api/system/launcher-config")
+}
+
+export async function setLauncherConfig(
+ payload: LauncherConfig,
+): Promise {
+ return request("/api/system/launcher-config", {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ })
+}
diff --git a/web/frontend/src/components/app-header.tsx b/web/frontend/src/components/app-header.tsx
new file mode 100644
index 000000000..7a50fe0fb
--- /dev/null
+++ b/web/frontend/src/components/app-header.tsx
@@ -0,0 +1,193 @@
+import {
+ IconBook,
+ IconLanguage,
+ IconLoader2,
+ IconMenu2,
+ IconMoon,
+ IconPlayerPlay,
+ IconPower,
+ IconSun,
+} from "@tabler/icons-react"
+import { Link } from "@tanstack/react-router"
+import * as React from "react"
+import { useTranslation } from "react-i18next"
+
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/components/ui/alert-dialog.tsx"
+import { Button } from "@/components/ui/button.tsx"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu.tsx"
+import { Separator } from "@/components/ui/separator.tsx"
+import { SidebarTrigger } from "@/components/ui/sidebar"
+import { useGateway } from "@/hooks/use-gateway.ts"
+import { useTheme } from "@/hooks/use-theme.ts"
+
+export function AppHeader() {
+ const { i18n, t } = useTranslation()
+ const { theme, toggleTheme } = useTheme()
+ const {
+ state: gwState,
+ loading: gwLoading,
+ canStart,
+ start,
+ stop,
+ } = useGateway()
+
+ const isRunning = gwState === "running"
+ const isStarting = gwState === "starting"
+ const isStopped = gwState === "stopped" || gwState === "unknown"
+ const showNotConnectedHint =
+ canStart && (gwState === "stopped" || gwState === "error")
+
+ const [showStopDialog, setShowStopDialog] = React.useState(false)
+
+ const handleGatewayToggle = () => {
+ if (gwLoading || (!isRunning && !canStart)) return
+ if (isRunning) {
+ setShowStopDialog(true)
+ } else {
+ start()
+ }
+ }
+
+ const confirmStop = () => {
+ setShowStopDialog(false)
+ stop()
+ }
+
+ return (
+
+
+
+
+
+
+
+

+
+
+
+
+ {/* Center prominent connection status */}
+
+ {showNotConnectedHint && (
+
+
+
+
+ {t("chat.notConnected")}
+
+ )}
+
+
+
+
+
+
+ {t("header.gateway.stopDialog.title")}
+
+
+ {t("header.gateway.stopDialog.description")}
+
+
+
+ {t("common.cancel")}
+
+ {t("header.gateway.stopDialog.confirm")}
+
+
+
+
+
+
+ {/* Gateway Start/Stop */}
+
+
+
+
+ {/* Docs Link */}
+
+
+ {/* Language Switcher */}
+
+
+
+
+
+ i18n.changeLanguage("en")}>
+ English
+
+ i18n.changeLanguage("zh")}>
+ 简体中文
+
+
+
+
+ {/* Theme Toggle */}
+
+
+
+ )
+}
diff --git a/web/frontend/src/components/app-layout.tsx b/web/frontend/src/components/app-layout.tsx
new file mode 100644
index 000000000..ff9877bae
--- /dev/null
+++ b/web/frontend/src/components/app-layout.tsx
@@ -0,0 +1,27 @@
+import type { ReactNode } from "react"
+import { Toaster } from "sonner"
+
+import { AppHeader } from "@/components/app-header"
+import { AppSidebar } from "@/components/app-sidebar"
+import { SidebarProvider } from "@/components/ui/sidebar"
+import { TooltipProvider } from "@/components/ui/tooltip"
+
+export function AppLayout({ children }: { children: ReactNode }) {
+ return (
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/web/frontend/src/components/app-sidebar.tsx b/web/frontend/src/components/app-sidebar.tsx
new file mode 100644
index 000000000..dc24f8781
--- /dev/null
+++ b/web/frontend/src/components/app-sidebar.tsx
@@ -0,0 +1,215 @@
+import { IconChevronRight } from "@tabler/icons-react"
+import {
+ IconAtom,
+ IconChevronsDown,
+ IconChevronsUp,
+ IconKey,
+ IconListDetails,
+ IconMessageCircle,
+ IconSettings,
+} from "@tabler/icons-react"
+import { Link, useRouterState } from "@tanstack/react-router"
+import * as React from "react"
+import { useTranslation } from "react-i18next"
+
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/ui/collapsible"
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarRail,
+} from "@/components/ui/sidebar"
+import { useSidebarChannels } from "@/hooks/use-sidebar-channels"
+
+interface NavItem {
+ title: string
+ url: string
+ icon: React.ComponentType<{ className?: string }>
+ translateTitle?: boolean
+}
+
+interface NavGroup {
+ label: string
+ defaultOpen: boolean
+ items: NavItem[]
+ isChannelsGroup?: boolean
+}
+
+const baseNavGroups: Omit[] = [
+ {
+ label: "navigation.chat",
+ defaultOpen: true,
+ },
+ {
+ label: "navigation.model_group",
+ defaultOpen: true,
+ },
+ {
+ label: "navigation.services",
+ defaultOpen: true,
+ },
+]
+
+export function AppSidebar({ ...props }: React.ComponentProps) {
+ const routerState = useRouterState()
+ const { t } = useTranslation()
+ const currentPath = routerState.location.pathname
+ const {
+ channelItems,
+ hasMoreChannels,
+ showAllChannels,
+ toggleShowAllChannels,
+ } = useSidebarChannels({ t })
+
+ const navGroups: NavGroup[] = React.useMemo(() => {
+ return [
+ {
+ ...baseNavGroups[0],
+ items: [
+ {
+ title: "navigation.chat",
+ url: "/",
+ icon: IconMessageCircle,
+ translateTitle: true,
+ },
+ ],
+ },
+ {
+ ...baseNavGroups[1],
+ items: [
+ {
+ title: "navigation.models",
+ url: "/models",
+ icon: IconAtom,
+ translateTitle: true,
+ },
+ {
+ title: "navigation.credentials",
+ url: "/credentials",
+ icon: IconKey,
+ translateTitle: true,
+ },
+ ],
+ },
+ {
+ label: "navigation.channels_group",
+ defaultOpen: true,
+ items: channelItems.map((item) => ({
+ title: item.title,
+ url: item.url,
+ icon: item.icon,
+ translateTitle: false,
+ })),
+ isChannelsGroup: true,
+ },
+ {
+ ...baseNavGroups[2],
+ items: [
+ {
+ title: "navigation.config",
+ url: "/config",
+ icon: IconSettings,
+ translateTitle: true,
+ },
+ {
+ title: "navigation.logs",
+ url: "/logs",
+ icon: IconListDetails,
+ translateTitle: true,
+ },
+ ],
+ },
+ ]
+ }, [channelItems])
+
+ return (
+
+
+ {navGroups.map((group) => (
+
+
+
+
+ {t(group.label)}
+
+
+
+
+
+
+ {group.items.map((item) => {
+ const isActive =
+ currentPath === item.url ||
+ (item.url !== "/" &&
+ currentPath.startsWith(`${item.url}/`))
+ return (
+
+
+
+
+
+ {item.translateTitle === false
+ ? item.title
+ : t(item.title)}
+
+
+
+
+ )
+ })}
+ {group.isChannelsGroup && hasMoreChannels && (
+
+
+ {showAllChannels ? (
+
+ ) : (
+
+ )}
+
+ {showAllChannels
+ ? t("navigation.show_less_channels")
+ : t("navigation.show_more_channels")}
+
+
+
+ )}
+
+
+
+
+
+ ))}
+
+
+
+ )
+}
diff --git a/web/frontend/src/components/channels/channel-config-page.tsx b/web/frontend/src/components/channels/channel-config-page.tsx
new file mode 100644
index 000000000..b19d11e6a
--- /dev/null
+++ b/web/frontend/src/components/channels/channel-config-page.tsx
@@ -0,0 +1,539 @@
+import { IconLoader2 } from "@tabler/icons-react"
+import { useAtomValue } from "jotai"
+import { useCallback, useEffect, useMemo, useRef, useState } from "react"
+import { useTranslation } from "react-i18next"
+import { toast } from "sonner"
+
+import {
+ type ChannelConfig,
+ type SupportedChannel,
+ getAppConfig,
+ getChannelsCatalog,
+ patchAppConfig,
+} from "@/api/channels"
+import { getChannelDisplayName } from "@/components/channels/channel-display-name"
+import { DiscordForm } from "@/components/channels/channel-forms/discord-form"
+import { FeishuForm } from "@/components/channels/channel-forms/feishu-form"
+import { GenericForm } from "@/components/channels/channel-forms/generic-form"
+import { SlackForm } from "@/components/channels/channel-forms/slack-form"
+import { TelegramForm } from "@/components/channels/channel-forms/telegram-form"
+import { PageHeader } from "@/components/page-header"
+import { Button } from "@/components/ui/button"
+import { Switch } from "@/components/ui/switch"
+import { gatewayAtom } from "@/store/gateway"
+
+interface ChannelConfigPageProps {
+ channelName: string
+}
+
+const SECRET_FIELD_MAP: Record = {
+ token: "_token",
+ app_secret: "_app_secret",
+ client_secret: "_client_secret",
+ corp_secret: "_corp_secret",
+ channel_secret: "_channel_secret",
+ channel_access_token: "_channel_access_token",
+ access_token: "_access_token",
+ bot_token: "_bot_token",
+ app_token: "_app_token",
+ encoding_aes_key: "_encoding_aes_key",
+ encrypt_key: "_encrypt_key",
+ verification_token: "_verification_token",
+ password: "_password",
+ nickserv_password: "_nickserv_password",
+ sasl_password: "_sasl_password",
+}
+
+function asRecord(value: unknown): Record {
+ if (value && typeof value === "object" && !Array.isArray(value)) {
+ return value as Record
+ }
+ return {}
+}
+
+function asString(value: unknown): string {
+ return typeof value === "string" ? value : ""
+}
+
+function asBool(value: unknown): boolean {
+ return value === true
+}
+
+function buildEditConfig(config: ChannelConfig): ChannelConfig {
+ const edit: ChannelConfig = { ...config }
+ for (const secretKey of Object.keys(SECRET_FIELD_MAP)) {
+ if (secretKey in config) {
+ edit[SECRET_FIELD_MAP[secretKey]] = ""
+ }
+ }
+ return edit
+}
+
+function normalizeConfig(
+ channel: SupportedChannel,
+ rawConfig: ChannelConfig,
+): ChannelConfig {
+ const config = { ...rawConfig }
+ if (channel.name === "whatsapp_native") {
+ config.use_native = true
+ }
+ if (channel.name === "whatsapp") {
+ config.use_native = false
+ }
+ return config
+}
+
+function buildSavePayload(
+ channel: SupportedChannel,
+ editConfig: ChannelConfig,
+ enabled: boolean,
+): ChannelConfig {
+ const payload: ChannelConfig = { enabled }
+
+ for (const [key, value] of Object.entries(editConfig)) {
+ if (key.startsWith("_")) continue
+ if (key === "enabled") continue
+
+ if (key in SECRET_FIELD_MAP) {
+ const editKey = SECRET_FIELD_MAP[key]
+ const incoming = asString(editConfig[editKey])
+ payload[key] = incoming !== "" ? incoming : value
+ continue
+ }
+
+ payload[key] = value
+ }
+
+ if (channel.name === "whatsapp_native") {
+ payload.use_native = true
+ }
+ if (channel.name === "whatsapp") {
+ payload.use_native = false
+ }
+
+ return payload
+}
+
+function isConfigured(
+ channel: SupportedChannel,
+ config: ChannelConfig,
+): boolean {
+ switch (channel.name) {
+ case "telegram":
+ return asString(config.token) !== ""
+ case "discord":
+ return asString(config.token) !== ""
+ case "slack":
+ return asString(config.bot_token) !== ""
+ case "feishu":
+ return (
+ asString(config.app_id) !== "" && asString(config.app_secret) !== ""
+ )
+ case "dingtalk":
+ return (
+ asString(config.client_id) !== "" &&
+ asString(config.client_secret) !== ""
+ )
+ case "line":
+ return asString(config.channel_access_token) !== ""
+ case "qq":
+ return (
+ asString(config.app_id) !== "" && asString(config.app_secret) !== ""
+ )
+ case "onebot":
+ return asString(config.ws_url) !== ""
+ case "wecom":
+ return asString(config.token) !== ""
+ case "wecom_app":
+ return (
+ asString(config.corp_id) !== "" && asString(config.corp_secret) !== ""
+ )
+ case "wecom_aibot":
+ return asString(config.token) !== ""
+ case "whatsapp":
+ return asString(config.bridge_url) !== ""
+ case "whatsapp_native":
+ return asBool(config.use_native)
+ case "pico":
+ return asString(config.token) !== ""
+ case "maixcam":
+ return asString(config.host) !== ""
+ case "matrix":
+ return (
+ asString(config.homeserver) !== "" &&
+ asString(config.user_id) !== "" &&
+ asString(config.access_token) !== ""
+ )
+ case "irc":
+ return asString(config.server) !== ""
+ default:
+ return false
+ }
+}
+
+function getRequiredFieldKeys(channelName: string): string[] {
+ switch (channelName) {
+ case "telegram":
+ return ["token"]
+ case "discord":
+ return ["token"]
+ case "slack":
+ return ["bot_token"]
+ case "feishu":
+ return ["app_id", "app_secret"]
+ case "dingtalk":
+ return ["client_id", "client_secret"]
+ case "line":
+ return ["channel_secret", "channel_access_token"]
+ case "qq":
+ return ["app_id", "app_secret"]
+ case "onebot":
+ return ["ws_url"]
+ case "wecom":
+ return ["token"]
+ case "wecom_app":
+ return ["corp_id", "corp_secret"]
+ case "wecom_aibot":
+ return ["token"]
+ case "whatsapp":
+ return ["bridge_url"]
+ case "pico":
+ return ["token"]
+ case "maixcam":
+ return ["host"]
+ case "matrix":
+ return ["homeserver", "user_id", "access_token"]
+ case "irc":
+ return ["server"]
+ default:
+ return []
+ }
+}
+
+function isMissingRequiredValue(value: unknown): boolean {
+ if (value === null || value === undefined) {
+ return true
+ }
+ if (typeof value === "string") {
+ return value.trim() === ""
+ }
+ if (Array.isArray(value)) {
+ return value.length === 0
+ }
+ return false
+}
+
+function getChannelDocSlug(channelName: string): string {
+ return channelName.replaceAll("_", "-")
+}
+
+const CHANNELS_WITHOUT_DOCS = new Set([
+ "pico",
+ "wecom",
+ "matrix",
+ "irc",
+ "whatsapp",
+ "whatsapp_native",
+])
+
+export function ChannelConfigPage({ channelName }: ChannelConfigPageProps) {
+ const { t, i18n } = useTranslation()
+ const gateway = useAtomValue(gatewayAtom)
+
+ const [loading, setLoading] = useState(true)
+ const [saving, setSaving] = useState(false)
+ const [fetchError, setFetchError] = useState("")
+ const [serverError, setServerError] = useState("")
+ const [fieldErrors, setFieldErrors] = useState>({})
+
+ const [channel, setChannel] = useState(null)
+ const [baseConfig, setBaseConfig] = useState({})
+ const [editConfig, setEditConfig] = useState({})
+ const [enabled, setEnabled] = useState(false)
+
+ const loadData = useCallback(async () => {
+ setLoading(true)
+ try {
+ const [catalog, appConfig] = await Promise.all([
+ getChannelsCatalog(),
+ getAppConfig(),
+ ])
+ const matched =
+ catalog.channels.find((item) => item.name === channelName) ?? null
+
+ if (!matched) {
+ setChannel(null)
+ setFetchError(
+ t("channels.page.notFound", {
+ name: channelName,
+ }),
+ )
+ return
+ }
+
+ const channelsConfig = asRecord(asRecord(appConfig).channels)
+ const raw = asRecord(channelsConfig[matched.config_key])
+ const normalized = normalizeConfig(matched, raw)
+
+ setChannel(matched)
+ setBaseConfig(normalized)
+ setEditConfig(buildEditConfig(normalized))
+ setEnabled(asBool(normalized.enabled))
+ setFetchError("")
+ setServerError("")
+ setFieldErrors({})
+ } catch (e) {
+ setFetchError(e instanceof Error ? e.message : t("channels.loadError"))
+ } finally {
+ setLoading(false)
+ }
+ }, [channelName, t])
+
+ useEffect(() => {
+ loadData()
+ }, [loadData])
+
+ const previousGatewayStatusRef = useRef(gateway.status)
+ useEffect(() => {
+ const previousStatus = previousGatewayStatusRef.current
+ if (previousStatus !== "running" && gateway.status === "running") {
+ void loadData()
+ }
+ previousGatewayStatusRef.current = gateway.status
+ }, [gateway.status, loadData])
+
+ const savePayload = useMemo(() => {
+ if (!channel) return null
+ return buildSavePayload(channel, editConfig, enabled)
+ }, [channel, editConfig, enabled])
+
+ const configured = useMemo(() => {
+ if (!channel || !savePayload) return false
+ return isConfigured(channel, savePayload)
+ }, [channel, savePayload])
+
+ const docsUrl = useMemo(() => {
+ if (!channel) return ""
+ if (CHANNELS_WITHOUT_DOCS.has(channel.name)) return ""
+ const language = (
+ i18n.resolvedLanguage ??
+ i18n.language ??
+ ""
+ ).toLowerCase()
+ const base = language.startsWith("zh")
+ ? "https://docs.picoclaw.io/zh-Hans/docs/channels"
+ : "https://docs.picoclaw.io/docs/channels"
+ return `${base}/${getChannelDocSlug(channel.name)}`
+ }, [channel, i18n.language, i18n.resolvedLanguage])
+
+ const channelDisplayName = useMemo(() => {
+ if (!channel) return channelName
+ return getChannelDisplayName(channel, t)
+ }, [channel, channelName, t])
+
+ const hiddenKeys = useMemo(() => {
+ if (!channel) return []
+ if (channel.name === "whatsapp") {
+ return ["use_native"]
+ }
+ if (channel.name === "whatsapp_native") {
+ return ["use_native", "bridge_url"]
+ }
+ return []
+ }, [channel])
+ const requiredKeys = useMemo(
+ () => getRequiredFieldKeys(channelName),
+ [channelName],
+ )
+
+ const handleChange = useCallback((key: string, value: unknown) => {
+ const normalizedKey = key.startsWith("_") ? key.slice(1) : key
+ setEditConfig((prev) => ({ ...prev, [key]: value }))
+ setFieldErrors((prev) => {
+ if (!(key in prev) && !(normalizedKey in prev)) {
+ return prev
+ }
+ const next = { ...prev }
+ delete next[key]
+ delete next[normalizedKey]
+ return next
+ })
+ }, [])
+
+ const handleReset = () => {
+ setEditConfig(buildEditConfig(baseConfig))
+ setEnabled(asBool(baseConfig.enabled))
+ setServerError("")
+ setFieldErrors({})
+ }
+
+ const handleSave = async () => {
+ if (!channel || !savePayload) return
+
+ const missingRequiredFields = requiredKeys.filter((key) =>
+ isMissingRequiredValue(savePayload[key]),
+ )
+ if (missingRequiredFields.length > 0) {
+ const requiredFieldError = t("channels.validation.requiredField")
+ const nextFieldErrors: Record = {}
+ for (const key of missingRequiredFields) {
+ nextFieldErrors[key] = requiredFieldError
+ }
+ setFieldErrors(nextFieldErrors)
+ setServerError("")
+ return
+ }
+
+ setSaving(true)
+ setServerError("")
+ setFieldErrors({})
+ try {
+ await patchAppConfig({
+ channels: {
+ [channel.config_key]: savePayload,
+ },
+ })
+ toast.success(t("channels.page.saveSuccess"))
+ await loadData()
+ } catch (e) {
+ const message =
+ e instanceof Error ? e.message : t("channels.page.saveError")
+ setServerError(message)
+ toast.error(message)
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const renderForm = () => {
+ if (!channel) return null
+ const isEdit = configured
+
+ switch (channel.name) {
+ case "telegram":
+ return (
+
+ )
+ case "discord":
+ return (
+
+ )
+ case "slack":
+ return (
+
+ )
+ case "feishu":
+ return (
+
+ )
+ default:
+ return (
+
+ )
+ }
+ }
+
+ return (
+
+
+ {enabled ? (
+
+ {t("channels.page.enabled")}
+
+ ) : configured ? (
+
+ {t("channels.status.configured")}
+
+ ) : null}
+
+ ) : undefined
+ }
+ />
+
+
+ {loading ? (
+
+
+
+ ) : fetchError ? (
+
+ {fetchError}
+
+ ) : (
+
+
+
+
+
+ {t("channels.page.enableLabel")}
+
+
+
+
+ {renderForm()}
+
+ {serverError && (
+
{serverError}
+ )}
+
+
+
+
+
+
+ )}
+
+
+ )
+}
diff --git a/web/frontend/src/components/channels/channel-display-name.ts b/web/frontend/src/components/channels/channel-display-name.ts
new file mode 100644
index 000000000..fe70f5f5e
--- /dev/null
+++ b/web/frontend/src/components/channels/channel-display-name.ts
@@ -0,0 +1,23 @@
+import type { TFunction } from "i18next"
+
+import type { SupportedChannel } from "@/api/channels"
+
+export function getChannelDisplayName(
+ channel: Pick,
+ t: TFunction,
+): string {
+ const key = `channels.name.${channel.name}`
+ const translated = t(key)
+ if (translated !== key) {
+ return translated
+ }
+
+ if (channel.display_name && channel.display_name.trim() !== "") {
+ return channel.display_name
+ }
+
+ return channel.name
+ .split("_")
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
+ .join(" ")
+}
diff --git a/web/frontend/src/components/channels/channel-forms/discord-form.tsx b/web/frontend/src/components/channels/channel-forms/discord-form.tsx
new file mode 100644
index 000000000..300175e20
--- /dev/null
+++ b/web/frontend/src/components/channels/channel-forms/discord-form.tsx
@@ -0,0 +1,109 @@
+import { useTranslation } from "react-i18next"
+
+import type { ChannelConfig } from "@/api/channels"
+import { maskedSecretPlaceholder } from "@/components/secret-placeholder"
+import { Field, KeyInput, SwitchCardField } from "@/components/shared-form"
+import { Input } from "@/components/ui/input"
+
+interface DiscordFormProps {
+ config: ChannelConfig
+ onChange: (key: string, value: unknown) => void
+ isEdit: boolean
+ fieldErrors?: Record
+}
+
+function asString(value: unknown): string {
+ return typeof value === "string" ? value : ""
+}
+
+function asStringArray(value: unknown): string[] {
+ if (!Array.isArray(value)) return []
+ return value.filter((item): item is string => typeof item === "string")
+}
+
+function asBool(value: unknown): boolean {
+ return value === true
+}
+
+function asRecord(value: unknown): Record {
+ if (value && typeof value === "object" && !Array.isArray(value)) {
+ return value as Record
+ }
+ return {}
+}
+
+export function DiscordForm({
+ config,
+ onChange,
+ isEdit,
+ fieldErrors = {},
+}: DiscordFormProps) {
+ const { t } = useTranslation()
+ const groupTriggerConfig = asRecord(config.group_trigger)
+ const tokenExtraHint =
+ isEdit && asString(config.token)
+ ? ` ${t("channels.field.secretHintSet")}`
+ : ""
+
+ return (
+
+
+ onChange("_token", v)}
+ placeholder={maskedSecretPlaceholder(
+ config.token,
+ t("channels.field.tokenPlaceholder"),
+ )}
+ />
+
+
+
+ onChange("proxy", e.target.value)}
+ placeholder="http://127.0.0.1:7890"
+ />
+
+
+
+ onChange(
+ "allow_from",
+ e.target.value
+ .split(",")
+ .map((s: string) => s.trim())
+ .filter(Boolean),
+ )
+ }
+ placeholder={t("channels.field.allowFromPlaceholder")}
+ />
+
+
+ {
+ onChange("group_trigger", {
+ ...groupTriggerConfig,
+ mention_only: checked,
+ })
+ }}
+ ariaLabel={t("channels.field.mentionOnly")}
+ />
+
+ )
+}
diff --git a/web/frontend/src/components/channels/channel-forms/feishu-form.tsx b/web/frontend/src/components/channels/channel-forms/feishu-form.tsx
new file mode 100644
index 000000000..a834a65f9
--- /dev/null
+++ b/web/frontend/src/components/channels/channel-forms/feishu-form.tsx
@@ -0,0 +1,121 @@
+import { useTranslation } from "react-i18next"
+
+import type { ChannelConfig } from "@/api/channels"
+import { maskedSecretPlaceholder } from "@/components/secret-placeholder"
+import { Field, KeyInput } from "@/components/shared-form"
+import { Input } from "@/components/ui/input"
+
+interface FeishuFormProps {
+ config: ChannelConfig
+ onChange: (key: string, value: unknown) => void
+ isEdit: boolean
+ fieldErrors?: Record
+}
+
+function asString(value: unknown): string {
+ return typeof value === "string" ? value : ""
+}
+
+function asStringArray(value: unknown): string[] {
+ if (!Array.isArray(value)) return []
+ return value.filter((item): item is string => typeof item === "string")
+}
+
+export function FeishuForm({
+ config,
+ onChange,
+ isEdit,
+ fieldErrors = {},
+}: FeishuFormProps) {
+ const { t } = useTranslation()
+ const appSecretExtraHint =
+ isEdit && asString(config.app_secret)
+ ? ` ${t("channels.field.secretHintSet")}`
+ : ""
+ const verificationExtraHint =
+ isEdit && asString(config.verification_token)
+ ? ` ${t("channels.field.secretHintSet")}`
+ : ""
+ const encryptExtraHint =
+ isEdit && asString(config.encrypt_key)
+ ? ` ${t("channels.field.secretHintSet")}`
+ : ""
+
+ return (
+
+
+ onChange("app_id", e.target.value)}
+ placeholder="cli_xxxx"
+ />
+
+
+
+ onChange("_app_secret", v)}
+ placeholder={maskedSecretPlaceholder(
+ config.app_secret,
+ t("channels.field.secretPlaceholder"),
+ )}
+ />
+
+
+
+ onChange("_verification_token", v)}
+ placeholder={maskedSecretPlaceholder(
+ config.verification_token,
+ t("channels.field.secretPlaceholder"),
+ )}
+ />
+
+
+ onChange("_encrypt_key", v)}
+ placeholder={maskedSecretPlaceholder(
+ config.encrypt_key,
+ t("channels.field.secretPlaceholder"),
+ )}
+ />
+
+
+
+ onChange(
+ "allow_from",
+ e.target.value
+ .split(",")
+ .map((s: string) => s.trim())
+ .filter(Boolean),
+ )
+ }
+ placeholder={t("channels.field.allowFromPlaceholder")}
+ />
+
+
+ )
+}
diff --git a/web/frontend/src/components/channels/channel-forms/generic-form.tsx b/web/frontend/src/components/channels/channel-forms/generic-form.tsx
new file mode 100644
index 000000000..fc5a0a7fd
--- /dev/null
+++ b/web/frontend/src/components/channels/channel-forms/generic-form.tsx
@@ -0,0 +1,377 @@
+import { useTranslation } from "react-i18next"
+
+import type { ChannelConfig } from "@/api/channels"
+import { maskedSecretPlaceholder } from "@/components/secret-placeholder"
+import { Field, KeyInput, SwitchCardField } from "@/components/shared-form"
+import { Input } from "@/components/ui/input"
+
+interface GenericFormProps {
+ config: ChannelConfig
+ onChange: (key: string, value: unknown) => void
+ isEdit: boolean
+ hiddenKeys?: string[]
+ requiredKeys?: string[]
+ fieldErrors?: Record
+}
+
+// Secret field names that should use masked input.
+const SECRET_FIELDS = new Set([
+ "token",
+ "app_secret",
+ "client_secret",
+ "corp_secret",
+ "channel_secret",
+ "channel_access_token",
+ "access_token",
+ "bot_token",
+ "app_token",
+ "encoding_aes_key",
+ "encrypt_key",
+ "verification_token",
+ "password",
+ "nickserv_password",
+ "sasl_password",
+])
+
+// Fields to skip in the generic form (handled by enabled toggle or internal).
+const SKIP_FIELDS = new Set(["enabled", "reasoning_channel_id"])
+
+// Fields that are objects/nested — show as JSON or skip.
+const OBJECT_FIELDS = new Set([
+ "group_trigger",
+ "typing",
+ "placeholder",
+ "allow_token_query",
+ "allow_from",
+ "allow_origins",
+])
+
+function formatLabel(key: string): string {
+ return key
+ .split("_")
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
+ .join(" ")
+}
+
+function formatSentenceFieldName(key: string): string {
+ const label = formatLabel(key)
+ return label.charAt(0).toLowerCase() + label.slice(1)
+}
+
+function asString(value: unknown): string {
+ return typeof value === "string" ? value : ""
+}
+
+function asStringArray(value: unknown): string[] {
+ if (!Array.isArray(value)) return []
+ return value.filter((item): item is string => typeof item === "string")
+}
+
+function asRecord(value: unknown): Record {
+ if (value && typeof value === "object" && !Array.isArray(value)) {
+ return value as Record
+ }
+ return {}
+}
+
+function asBool(value: unknown): boolean {
+ return value === true
+}
+
+export function GenericForm({
+ config,
+ onChange,
+ isEdit,
+ hiddenKeys = [],
+ requiredKeys = [],
+ fieldErrors = {},
+}: GenericFormProps) {
+ const { t } = useTranslation()
+ const hiddenFieldSet = new Set(hiddenKeys)
+ const requiredFieldSet = new Set(requiredKeys)
+ const groupTriggerConfig = asRecord(config.group_trigger)
+ const typingConfig = asRecord(config.typing)
+ const placeholderConfig = asRecord(config.placeholder)
+ const placeholderEnabled = asBool(placeholderConfig.enabled)
+
+ const fields = Object.keys(config).filter(
+ (k) =>
+ !k.startsWith("_") &&
+ !SKIP_FIELDS.has(k) &&
+ !OBJECT_FIELDS.has(k) &&
+ !hiddenFieldSet.has(k),
+ )
+
+ const buildHint = (key: string): string => {
+ const descriptions: Record = {
+ ws_url: t("channels.form.desc.wsUrl"),
+ reconnect_interval: t("channels.form.desc.reconnectInterval"),
+ bridge_url: t("channels.form.desc.bridgeUrl"),
+ session_store_path: t("channels.form.desc.sessionStorePath"),
+ use_native: t("channels.form.desc.useNative"),
+ host: t("channels.form.desc.host"),
+ port: t("channels.form.desc.port"),
+ homeserver: t("channels.form.desc.homeserver"),
+ user_id: t("channels.form.desc.userId"),
+ device_id: t("channels.form.desc.deviceId"),
+ join_on_invite: t("channels.form.desc.joinOnInvite"),
+ app_id: t("channels.form.desc.appId"),
+ client_id: t("channels.form.desc.clientId"),
+ corp_id: t("channels.form.desc.corpId"),
+ agent_id: t("channels.form.desc.agentId"),
+ webhook_url: t("channels.form.desc.webhookUrl"),
+ webhook_host: t("channels.form.desc.webhookHost"),
+ webhook_port: t("channels.form.desc.webhookPort"),
+ webhook_path: t("channels.form.desc.webhookPath"),
+ reply_timeout: t("channels.form.desc.replyTimeout"),
+ max_steps: t("channels.form.desc.maxSteps"),
+ welcome_message: t("channels.form.desc.welcomeMessage"),
+ allow_token_query: t("channels.form.desc.allowTokenQuery"),
+ ping_interval: t("channels.form.desc.pingInterval"),
+ read_timeout: t("channels.form.desc.readTimeout"),
+ write_timeout: t("channels.form.desc.writeTimeout"),
+ max_connections: t("channels.form.desc.maxConnections"),
+ server: t("channels.form.desc.server"),
+ tls: t("channels.form.desc.tls"),
+ nick: t("channels.form.desc.nick"),
+ user: t("channels.form.desc.user"),
+ real_name: t("channels.form.desc.realName"),
+ channels: t("channels.form.desc.channels"),
+ request_caps: t("channels.form.desc.requestCaps"),
+ }
+ return (
+ descriptions[key] ??
+ t("channels.form.desc.genericField", {
+ field: formatSentenceFieldName(key),
+ })
+ )
+ }
+
+ return (
+
+ {fields.map((key) => {
+ const isRequired = requiredFieldSet.has(key)
+ if (SECRET_FIELDS.has(key)) {
+ const editKey = `_${key}`
+ const extraHint =
+ isEdit && config[key] ? ` ${t("channels.field.secretHintSet")}` : ""
+ return (
+
+ onChange(editKey, v)}
+ placeholder={maskedSecretPlaceholder(config[key])}
+ />
+
+ )
+ }
+
+ const value = config[key]
+ if (typeof value === "boolean") {
+ return (
+
onChange(key, checked)}
+ ariaLabel={formatLabel(key)}
+ />
+ )
+ }
+
+ if (Array.isArray(value)) {
+ return (
+
+
+ onChange(
+ key,
+ e.target.value
+ .split(",")
+ .map((s: string) => s.trim())
+ .filter(Boolean),
+ )
+ }
+ />
+
+ )
+ }
+
+ return (
+
+ {
+ // Attempt to preserve number types
+ const v = e.target.value
+ if (typeof config[key] === "number") {
+ onChange(key, v === "" ? 0 : Number(v))
+ } else {
+ onChange(key, v)
+ }
+ }}
+ />
+
+ )
+ })}
+
+ {/* Allow From field */}
+ {config.allow_from !== undefined && !hiddenFieldSet.has("allow_from") && (
+
+
+ onChange(
+ "allow_from",
+ e.target.value
+ .split(",")
+ .map((s: string) => s.trim())
+ .filter(Boolean),
+ )
+ }
+ placeholder={t("channels.field.allowFromPlaceholder")}
+ />
+
+ )}
+
+ {config.allow_origins !== undefined &&
+ !hiddenFieldSet.has("allow_origins") && (
+
+
+ onChange(
+ "allow_origins",
+ e.target.value
+ .split(",")
+ .map((s: string) => s.trim())
+ .filter(Boolean),
+ )
+ }
+ placeholder={t("channels.field.allowOriginsPlaceholder")}
+ />
+
+ )}
+
+ {config.allow_token_query !== undefined &&
+ !hiddenFieldSet.has("allow_token_query") && (
+
+ onChange("allow_token_query", checked)
+ }
+ ariaLabel={formatLabel("allow_token_query")}
+ />
+ )}
+
+ {config.group_trigger !== undefined &&
+ !hiddenFieldSet.has("group_trigger") && (
+ <>
+
+ onChange("group_trigger", {
+ ...groupTriggerConfig,
+ mention_only: checked,
+ })
+ }
+ ariaLabel={t("channels.field.groupTriggerMentionOnly")}
+ />
+
+
+ onChange("group_trigger", {
+ ...groupTriggerConfig,
+ prefixes: e.target.value
+ .split(",")
+ .map((s: string) => s.trim())
+ .filter(Boolean),
+ })
+ }
+ placeholder={t("channels.field.groupTriggerPrefixes")}
+ />
+
+ >
+ )}
+
+ {config.typing !== undefined && !hiddenFieldSet.has("typing") && (
+
+ onChange("typing", { ...typingConfig, enabled: checked })
+ }
+ ariaLabel={t("channels.field.typingEnabled")}
+ />
+ )}
+
+ {config.placeholder !== undefined &&
+ !hiddenFieldSet.has("placeholder") && (
+
+ onChange("placeholder", {
+ ...placeholderConfig,
+ enabled: checked,
+ })
+ }
+ ariaLabel={t("channels.field.placeholderEnabled")}
+ >
+ {placeholderEnabled && (
+
+
+ onChange("placeholder", {
+ ...placeholderConfig,
+ text: e.target.value,
+ })
+ }
+ placeholder={t("channels.field.placeholderText")}
+ aria-label={t("channels.field.placeholderText")}
+ />
+
+ )}
+
+ )}
+
+ )
+}
diff --git a/web/frontend/src/components/channels/channel-forms/slack-form.tsx b/web/frontend/src/components/channels/channel-forms/slack-form.tsx
new file mode 100644
index 000000000..54650e842
--- /dev/null
+++ b/web/frontend/src/components/channels/channel-forms/slack-form.tsx
@@ -0,0 +1,86 @@
+import { useTranslation } from "react-i18next"
+
+import type { ChannelConfig } from "@/api/channels"
+import { maskedSecretPlaceholder } from "@/components/secret-placeholder"
+import { Field, KeyInput } from "@/components/shared-form"
+import { Input } from "@/components/ui/input"
+
+interface SlackFormProps {
+ config: ChannelConfig
+ onChange: (key: string, value: unknown) => void
+ isEdit: boolean
+ fieldErrors?: Record
+}
+
+function asString(value: unknown): string {
+ return typeof value === "string" ? value : ""
+}
+
+function asStringArray(value: unknown): string[] {
+ if (!Array.isArray(value)) return []
+ return value.filter((item): item is string => typeof item === "string")
+}
+
+export function SlackForm({
+ config,
+ onChange,
+ isEdit,
+ fieldErrors = {},
+}: SlackFormProps) {
+ const { t } = useTranslation()
+ const botTokenExtraHint =
+ isEdit && asString(config.bot_token)
+ ? ` ${t("channels.field.secretHintSet")}`
+ : ""
+ const appTokenExtraHint =
+ isEdit && asString(config.app_token)
+ ? ` ${t("channels.field.secretHintSet")}`
+ : ""
+
+ return (
+
+
+ onChange("_bot_token", v)}
+ placeholder={maskedSecretPlaceholder(config.bot_token, "xoxb-xxxx")}
+ />
+
+
+
+ onChange("_app_token", v)}
+ placeholder={maskedSecretPlaceholder(config.app_token, "xapp-xxxx")}
+ />
+
+
+
+
+ onChange(
+ "allow_from",
+ e.target.value
+ .split(",")
+ .map((s: string) => s.trim())
+ .filter(Boolean),
+ )
+ }
+ placeholder={t("channels.field.allowFromPlaceholder")}
+ />
+
+
+ )
+}
diff --git a/web/frontend/src/components/channels/channel-forms/telegram-form.tsx b/web/frontend/src/components/channels/channel-forms/telegram-form.tsx
new file mode 100644
index 000000000..169ddec63
--- /dev/null
+++ b/web/frontend/src/components/channels/channel-forms/telegram-form.tsx
@@ -0,0 +1,147 @@
+import { useTranslation } from "react-i18next"
+
+import type { ChannelConfig } from "@/api/channels"
+import { maskedSecretPlaceholder } from "@/components/secret-placeholder"
+import { Field, KeyInput, SwitchCardField } from "@/components/shared-form"
+import { Input } from "@/components/ui/input"
+
+interface TelegramFormProps {
+ config: ChannelConfig
+ onChange: (key: string, value: unknown) => void
+ isEdit: boolean
+ fieldErrors?: Record
+}
+
+function asString(value: unknown): string {
+ return typeof value === "string" ? value : ""
+}
+
+function asStringArray(value: unknown): string[] {
+ if (!Array.isArray(value)) return []
+ return value.filter((item): item is string => typeof item === "string")
+}
+
+function asRecord(value: unknown): Record {
+ if (value && typeof value === "object" && !Array.isArray(value)) {
+ return value as Record
+ }
+ return {}
+}
+
+function asBool(value: unknown): boolean {
+ return value === true
+}
+
+export function TelegramForm({
+ config,
+ onChange,
+ isEdit,
+ fieldErrors = {},
+}: TelegramFormProps) {
+ const { t } = useTranslation()
+ const typingConfig = asRecord(config.typing)
+ const placeholderConfig = asRecord(config.placeholder)
+ const placeholderEnabled = asBool(placeholderConfig.enabled)
+ const tokenExtraHint =
+ isEdit && asString(config.token)
+ ? ` ${t("channels.field.secretHintSet")}`
+ : ""
+
+ return (
+
+
+ onChange("_token", v)}
+ placeholder={maskedSecretPlaceholder(
+ config.token,
+ t("channels.field.tokenPlaceholder"),
+ )}
+ />
+
+
+
+ onChange("base_url", e.target.value)}
+ placeholder="https://api.telegram.org"
+ />
+
+
+ onChange("proxy", e.target.value)}
+ placeholder="http://127.0.0.1:7890"
+ />
+
+
+
+ onChange(
+ "allow_from",
+ e.target.value
+ .split(",")
+ .map((s: string) => s.trim())
+ .filter(Boolean),
+ )
+ }
+ placeholder={t("channels.field.allowFromPlaceholder")}
+ />
+
+
+
+ onChange("typing", { ...typingConfig, enabled: checked })
+ }
+ ariaLabel={t("channels.field.typingEnabled")}
+ />
+
+
+ onChange("placeholder", {
+ ...placeholderConfig,
+ enabled: checked,
+ })
+ }
+ ariaLabel={t("channels.field.placeholderEnabled")}
+ >
+ {placeholderEnabled && (
+
+
+ onChange("placeholder", {
+ ...placeholderConfig,
+ text: e.target.value,
+ })
+ }
+ placeholder={t("channels.field.placeholderText")}
+ aria-label={t("channels.field.placeholderText")}
+ />
+
+ )}
+
+
+ )
+}
diff --git a/web/frontend/src/components/chat/assistant-message.tsx b/web/frontend/src/components/chat/assistant-message.tsx
new file mode 100644
index 000000000..150f2f87d
--- /dev/null
+++ b/web/frontend/src/components/chat/assistant-message.tsx
@@ -0,0 +1,62 @@
+import { IconCheck, IconCopy } from "@tabler/icons-react"
+import { useState } from "react"
+import ReactMarkdown from "react-markdown"
+import remarkGfm from "remark-gfm"
+
+import { Button } from "@/components/ui/button"
+import { formatMessageTime } from "@/hooks/use-pico-chat"
+
+interface AssistantMessageProps {
+ content: string
+ timestamp?: string | number
+}
+
+export function AssistantMessage({
+ content,
+ timestamp = "",
+}: AssistantMessageProps) {
+ const [isCopied, setIsCopied] = useState(false)
+ const formattedTimestamp =
+ timestamp !== "" ? formatMessageTime(timestamp) : ""
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(content).then(() => {
+ setIsCopied(true)
+ setTimeout(() => setIsCopied(false), 2000)
+ })
+ }
+
+ return (
+
+
+
+ PicoClaw
+ {formattedTimestamp && (
+ <>
+ •
+ {formattedTimestamp}
+ >
+ )}
+
+
+
+
+
+ {content}
+
+
+
+
+ )
+}
diff --git a/web/frontend/src/components/chat/chat-composer.tsx b/web/frontend/src/components/chat/chat-composer.tsx
new file mode 100644
index 000000000..e8bae89b8
--- /dev/null
+++ b/web/frontend/src/components/chat/chat-composer.tsx
@@ -0,0 +1,67 @@
+import { IconArrowUp } from "@tabler/icons-react"
+import type { KeyboardEvent } from "react"
+import { useTranslation } from "react-i18next"
+import TextareaAutosize from "react-textarea-autosize"
+
+import { Button } from "@/components/ui/button"
+import { cn } from "@/lib/utils"
+
+interface ChatComposerProps {
+ input: string
+ onInputChange: (value: string) => void
+ onSend: () => void
+ isConnected: boolean
+ hasDefaultModel: boolean
+}
+
+export function ChatComposer({
+ input,
+ onInputChange,
+ onSend,
+ isConnected,
+ hasDefaultModel,
+}: ChatComposerProps) {
+ const { t } = useTranslation()
+ const canInput = isConnected && hasDefaultModel
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.nativeEvent.isComposing) return
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault()
+ onSend()
+ }
+ }
+
+ return (
+
+
+
onInputChange(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder={t("chat.placeholder")}
+ disabled={!canInput}
+ className={cn(
+ "max-h-[200px] min-h-[60px] resize-none border-0 bg-transparent px-2 py-1 text-[15px] shadow-none transition-colors focus-visible:ring-0 focus-visible:outline-none dark:bg-transparent",
+ !canInput && "cursor-not-allowed",
+ )}
+ minRows={1}
+ maxRows={8}
+ />
+
+
+
{/* action buttons */}
+
+
+
+
+
+ )
+}
diff --git a/web/frontend/src/components/chat/chat-empty-state.tsx b/web/frontend/src/components/chat/chat-empty-state.tsx
new file mode 100644
index 000000000..624ff9c59
--- /dev/null
+++ b/web/frontend/src/components/chat/chat-empty-state.tsx
@@ -0,0 +1,87 @@
+import {
+ IconPlugConnectedX,
+ IconRobot,
+ IconRobotOff,
+ IconStar,
+} from "@tabler/icons-react"
+import { Link } from "@tanstack/react-router"
+import { useTranslation } from "react-i18next"
+
+import { Button } from "@/components/ui/button"
+
+interface ChatEmptyStateProps {
+ hasConfiguredModels: boolean
+ defaultModelName: string
+ isConnected: boolean
+}
+
+export function ChatEmptyState({
+ hasConfiguredModels,
+ defaultModelName,
+ isConnected,
+}: ChatEmptyStateProps) {
+ const { t } = useTranslation()
+
+ if (!hasConfiguredModels) {
+ return (
+
+
+
+
+
+ {t("chat.empty.noConfiguredModel")}
+
+
+ {t("chat.empty.noConfiguredModelDescription")}
+
+
+
+ )
+ }
+
+ if (!defaultModelName) {
+ return (
+
+
+
+
+
+ {t("chat.empty.noSelectedModel")}
+
+
+ {t("chat.empty.noSelectedModelDescription")}
+
+
+ )
+ }
+
+ if (!isConnected) {
+ return (
+
+
+
+
+
+ {t("chat.empty.notRunning")}
+
+
+ {t("chat.empty.notRunningDescription")}
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
{t("chat.welcome")}
+
+ {t("chat.welcomeDesc")}
+
+
+ )
+}
diff --git a/web/frontend/src/components/chat/chat-page.tsx b/web/frontend/src/components/chat/chat-page.tsx
new file mode 100644
index 000000000..0fd23a6a5
--- /dev/null
+++ b/web/frontend/src/components/chat/chat-page.tsx
@@ -0,0 +1,150 @@
+import { IconPlus } from "@tabler/icons-react"
+import { useEffect, useRef, useState } from "react"
+import { useTranslation } from "react-i18next"
+
+import { AssistantMessage } from "@/components/chat/assistant-message"
+import { ChatComposer } from "@/components/chat/chat-composer"
+import { ChatEmptyState } from "@/components/chat/chat-empty-state"
+import { ModelSelector } from "@/components/chat/model-selector"
+import { SessionHistoryMenu } from "@/components/chat/session-history-menu"
+import { TypingIndicator } from "@/components/chat/typing-indicator"
+import { UserMessage } from "@/components/chat/user-message"
+import { PageHeader } from "@/components/page-header"
+import { Button } from "@/components/ui/button"
+import { useChatModels } from "@/hooks/use-chat-models"
+import { useGateway } from "@/hooks/use-gateway"
+import { usePicoChat } from "@/hooks/use-pico-chat"
+import { useSessionHistory } from "@/hooks/use-session-history"
+
+export function ChatPage() {
+ const { t } = useTranslation()
+ const scrollRef = useRef(null)
+ const [isAtBottom, setIsAtBottom] = useState(true)
+ const [input, setInput] = useState("")
+
+ const {
+ messages,
+ isTyping,
+ activeSessionId,
+ sendMessage,
+ switchSession,
+ newChat,
+ } = usePicoChat()
+
+ const { state: gwState } = useGateway()
+ const isConnected = gwState === "running"
+
+ const {
+ defaultModelName,
+ hasConfiguredModels,
+ apiKeyModels,
+ oauthModels,
+ localModels,
+ handleSetDefault,
+ } = useChatModels({ isConnected })
+
+ const { sessions, hasMore, observerRef, loadSessions, handleDeleteSession } =
+ useSessionHistory({
+ activeSessionId,
+ onDeletedActiveSession: newChat,
+ })
+
+ const handleScroll = (e: React.UIEvent) => {
+ const { scrollTop, scrollHeight, clientHeight } = e.currentTarget
+ setIsAtBottom(scrollHeight - scrollTop <= clientHeight + 10)
+ }
+
+ useEffect(() => {
+ if (isAtBottom && scrollRef.current) {
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight
+ }
+ }, [messages, isTyping, isAtBottom])
+
+ const handleSend = () => {
+ if (!input.trim() || !isConnected) return
+ sendMessage(input.trim())
+ setInput("")
+ }
+
+ return (
+
+
+ )
+ }
+ >
+
+
+
{
+ if (open) {
+ void loadSessions(true)
+ }
+ }}
+ onSwitchSession={switchSession}
+ onDeleteSession={handleDeleteSession}
+ />
+
+
+
+
+ {messages.length === 0 && !isTyping && (
+
+ )}
+
+ {messages.map((msg) => (
+
+ {msg.role === "assistant" ? (
+
+ ) : (
+
+ )}
+
+ ))}
+
+ {isTyping &&
}
+
+
+
+
+
+ )
+}
diff --git a/web/frontend/src/components/chat/model-selector.tsx b/web/frontend/src/components/chat/model-selector.tsx
new file mode 100644
index 000000000..30afc5d04
--- /dev/null
+++ b/web/frontend/src/components/chat/model-selector.tsx
@@ -0,0 +1,84 @@
+import { useTranslation } from "react-i18next"
+
+import type { ModelInfo } from "@/api/models"
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectSeparator,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+
+interface ModelSelectorProps {
+ defaultModelName: string
+ apiKeyModels: ModelInfo[]
+ oauthModels: ModelInfo[]
+ localModels: ModelInfo[]
+ onValueChange: (modelName: string) => void
+}
+
+export function ModelSelector({
+ defaultModelName,
+ apiKeyModels,
+ oauthModels,
+ localModels,
+ onValueChange,
+}: ModelSelectorProps) {
+ const { t } = useTranslation()
+
+ return (
+
+ )
+}
diff --git a/web/frontend/src/components/chat/session-history-menu.tsx b/web/frontend/src/components/chat/session-history-menu.tsx
new file mode 100644
index 000000000..f2e93295c
--- /dev/null
+++ b/web/frontend/src/components/chat/session-history-menu.tsx
@@ -0,0 +1,98 @@
+import { IconHistory, IconTrash } from "@tabler/icons-react"
+import dayjs from "dayjs"
+import type { RefObject } from "react"
+import { useTranslation } from "react-i18next"
+
+import type { SessionSummary } from "@/api/sessions"
+import { Button } from "@/components/ui/button"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import { ScrollArea } from "@/components/ui/scroll-area"
+
+interface SessionHistoryMenuProps {
+ sessions: SessionSummary[]
+ activeSessionId: string
+ hasMore: boolean
+ observerRef: RefObject
+ onOpenChange: (open: boolean) => void
+ onSwitchSession: (sessionId: string) => void
+ onDeleteSession: (sessionId: string) => void
+}
+
+export function SessionHistoryMenu({
+ sessions,
+ activeSessionId,
+ hasMore,
+ observerRef,
+ onOpenChange,
+ onSwitchSession,
+ onDeleteSession,
+}: SessionHistoryMenuProps) {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+
+
+
+ {sessions.length === 0 ? (
+
+
+ {t("chat.noHistory")}
+
+
+ ) : (
+ sessions.map((session) => (
+ onSwitchSession(session.id)}
+ >
+
+ {session.preview}
+
+
+ {t("chat.messagesCount", {
+ count: session.message_count,
+ })}{" "}
+ · {dayjs(session.updated).fromNow()}
+
+
+
+ ))
+ )}
+ {hasMore && sessions.length > 0 && (
+
+
+ {t("chat.loadingMore")}
+
+
+ )}
+
+
+
+ )
+}
diff --git a/web/frontend/src/components/chat/typing-indicator.tsx b/web/frontend/src/components/chat/typing-indicator.tsx
new file mode 100644
index 000000000..98580963d
--- /dev/null
+++ b/web/frontend/src/components/chat/typing-indicator.tsx
@@ -0,0 +1,47 @@
+import { useEffect, useState } from "react"
+import { useTranslation } from "react-i18next"
+
+export function TypingIndicator() {
+ const { t } = useTranslation()
+ const thinkingSteps = [
+ t("chat.thinking.step1"),
+ t("chat.thinking.step2"),
+ t("chat.thinking.step3"),
+ t("chat.thinking.step4"),
+ ]
+ const [stepIndex, setStepIndex] = useState(0)
+
+ useEffect(() => {
+ const stepsCount = thinkingSteps.length
+ const interval = setInterval(() => {
+ setStepIndex((prev) => (prev + 1) % stepsCount)
+ }, 3000)
+ return () => clearInterval(interval)
+ }, [thinkingSteps.length])
+
+ return (
+
+
+ PicoClaw
+
+
+
+
+
+
+
+
+
+
+
+ {thinkingSteps[stepIndex]}
+
+
+
+ )
+}
diff --git a/web/frontend/src/components/chat/user-message.tsx b/web/frontend/src/components/chat/user-message.tsx
new file mode 100644
index 000000000..b47806f49
--- /dev/null
+++ b/web/frontend/src/components/chat/user-message.tsx
@@ -0,0 +1,13 @@
+interface UserMessageProps {
+ content: string
+}
+
+export function UserMessage({ content }: UserMessageProps) {
+ return (
+
+ )
+}
diff --git a/web/frontend/src/components/config/config-page.tsx b/web/frontend/src/components/config/config-page.tsx
new file mode 100644
index 000000000..c2d502079
--- /dev/null
+++ b/web/frontend/src/components/config/config-page.tsx
@@ -0,0 +1,337 @@
+import { IconCode, IconDeviceFloppy } from "@tabler/icons-react"
+import { useQuery, useQueryClient } from "@tanstack/react-query"
+import { Link } from "@tanstack/react-router"
+import { useEffect, useState } from "react"
+import { useTranslation } from "react-i18next"
+import { toast } from "sonner"
+
+import { patchAppConfig } from "@/api/channels"
+import {
+ getAutoStartStatus,
+ getLauncherConfig,
+ setAutoStartEnabled as updateAutoStartEnabled,
+ setLauncherConfig as updateLauncherConfig,
+} from "@/api/system"
+import {
+ AdvancedSection,
+ AgentDefaultsSection,
+ DevicesSection,
+ LauncherSection,
+ RuntimeSection,
+} from "@/components/config/config-sections"
+import {
+ type CoreConfigForm,
+ EMPTY_FORM,
+ EMPTY_LAUNCHER_FORM,
+ type LauncherForm,
+ buildFormFromConfig,
+ parseCIDRText,
+ parseIntField,
+} from "@/components/config/form-model"
+import { PageHeader } from "@/components/page-header"
+import { Button } from "@/components/ui/button"
+import { Separator } from "@/components/ui/separator"
+
+export function ConfigPage() {
+ const { t } = useTranslation()
+ const queryClient = useQueryClient()
+ const [form, setForm] = useState(EMPTY_FORM)
+ const [baseline, setBaseline] = useState(EMPTY_FORM)
+ const [launcherForm, setLauncherForm] =
+ useState(EMPTY_LAUNCHER_FORM)
+ const [launcherBaseline, setLauncherBaseline] =
+ useState(EMPTY_LAUNCHER_FORM)
+ const [autoStartEnabled, setAutoStartEnabled] = useState(false)
+ const [autoStartBaseline, setAutoStartBaseline] = useState(false)
+ const [saving, setSaving] = useState(false)
+
+ const { data, isLoading, error } = useQuery({
+ queryKey: ["config"],
+ queryFn: async () => {
+ const res = await fetch("/api/config")
+ if (!res.ok) {
+ throw new Error("Failed to load config")
+ }
+ return res.json()
+ },
+ })
+
+ const {
+ data: launcherConfig,
+ isLoading: isLauncherLoading,
+ error: launcherError,
+ } = useQuery({
+ queryKey: ["system", "launcher-config"],
+ queryFn: getLauncherConfig,
+ })
+
+ const {
+ data: autoStartStatus,
+ isLoading: isAutoStartLoading,
+ error: autoStartError,
+ } = useQuery({
+ queryKey: ["system", "autostart"],
+ queryFn: getAutoStartStatus,
+ })
+
+ useEffect(() => {
+ if (!data) return
+ const parsed = buildFormFromConfig(data)
+ setForm(parsed)
+ setBaseline(parsed)
+ }, [data])
+
+ useEffect(() => {
+ if (!launcherConfig) return
+ const parsed: LauncherForm = {
+ port: String(launcherConfig.port),
+ publicAccess: launcherConfig.public,
+ allowedCIDRsText: (launcherConfig.allowed_cidrs ?? []).join("\n"),
+ }
+ setLauncherForm(parsed)
+ setLauncherBaseline(parsed)
+ }, [launcherConfig])
+
+ useEffect(() => {
+ if (!autoStartStatus) return
+ setAutoStartEnabled(autoStartStatus.enabled)
+ setAutoStartBaseline(autoStartStatus.enabled)
+ }, [autoStartStatus])
+
+ const configDirty = JSON.stringify(form) !== JSON.stringify(baseline)
+ const launcherDirty =
+ JSON.stringify(launcherForm) !== JSON.stringify(launcherBaseline)
+ const autoStartDirty = autoStartEnabled !== autoStartBaseline
+ const isDirty = configDirty || launcherDirty || autoStartDirty
+
+ const autoStartSupported = autoStartStatus?.supported !== false
+ const autoStartHint = autoStartError
+ ? t("pages.config.autostart_load_error")
+ : !autoStartSupported
+ ? t("pages.config.autostart_unsupported")
+ : t("pages.config.autostart_hint")
+
+ const launcherHint = launcherError
+ ? t("pages.config.launcher_load_error")
+ : t("pages.config.launcher_restart_hint")
+
+ const updateField = (
+ key: K,
+ value: CoreConfigForm[K],
+ ) => {
+ setForm((prev) => ({ ...prev, [key]: value }))
+ }
+
+ const updateLauncherField = (
+ key: K,
+ value: LauncherForm[K],
+ ) => {
+ setLauncherForm((prev) => ({ ...prev, [key]: value }))
+ }
+
+ const handleReset = () => {
+ setForm(baseline)
+ setLauncherForm(launcherBaseline)
+ setAutoStartEnabled(autoStartBaseline)
+ toast.info(t("pages.config.reset_success"))
+ }
+
+ const handleSave = async () => {
+ try {
+ setSaving(true)
+
+ if (configDirty) {
+ const workspace = form.workspace.trim()
+ const dmScope = form.dmScope.trim()
+
+ if (!workspace) {
+ throw new Error("Workspace path is required.")
+ }
+ if (!dmScope) {
+ throw new Error("Session scope is required.")
+ }
+
+ const maxTokens = parseIntField(form.maxTokens, "Max tokens", {
+ min: 1,
+ })
+ const maxToolIterations = parseIntField(
+ form.maxToolIterations,
+ "Max tool iterations",
+ { min: 1 },
+ )
+ const summarizeMessageThreshold = parseIntField(
+ form.summarizeMessageThreshold,
+ "Summarize message threshold",
+ { min: 1 },
+ )
+ const summarizeTokenPercent = parseIntField(
+ form.summarizeTokenPercent,
+ "Summarize token percent",
+ { min: 1, max: 100 },
+ )
+ const heartbeatInterval = parseIntField(
+ form.heartbeatInterval,
+ "Heartbeat interval",
+ { min: 1 },
+ )
+
+ await patchAppConfig({
+ agents: {
+ defaults: {
+ workspace,
+ restrict_to_workspace: form.restrictToWorkspace,
+ max_tokens: maxTokens,
+ max_tool_iterations: maxToolIterations,
+ summarize_message_threshold: summarizeMessageThreshold,
+ summarize_token_percent: summarizeTokenPercent,
+ },
+ },
+ session: {
+ dm_scope: dmScope,
+ },
+ heartbeat: {
+ enabled: form.heartbeatEnabled,
+ interval: heartbeatInterval,
+ },
+ devices: {
+ enabled: form.devicesEnabled,
+ monitor_usb: form.monitorUSB,
+ },
+ })
+
+ setBaseline(form)
+ queryClient.invalidateQueries({ queryKey: ["config"] })
+ }
+
+ if (launcherDirty) {
+ const port = parseIntField(launcherForm.port, "Service port", {
+ min: 1,
+ max: 65535,
+ })
+ const allowedCIDRs = parseCIDRText(launcherForm.allowedCIDRsText)
+ const savedLauncherConfig = await updateLauncherConfig({
+ port,
+ public: launcherForm.publicAccess,
+ allowed_cidrs: allowedCIDRs,
+ })
+ const parsedLauncher: LauncherForm = {
+ port: String(savedLauncherConfig.port),
+ publicAccess: savedLauncherConfig.public,
+ allowedCIDRsText: (savedLauncherConfig.allowed_cidrs ?? []).join(
+ "\n",
+ ),
+ }
+ setLauncherForm(parsedLauncher)
+ setLauncherBaseline(parsedLauncher)
+ queryClient.setQueryData(
+ ["system", "launcher-config"],
+ savedLauncherConfig,
+ )
+ }
+
+ if (autoStartDirty) {
+ if (!autoStartSupported) {
+ throw new Error(t("pages.config.autostart_unsupported"))
+ }
+ const status = await updateAutoStartEnabled(autoStartEnabled)
+ setAutoStartEnabled(status.enabled)
+ setAutoStartBaseline(status.enabled)
+ queryClient.setQueryData(["system", "autostart"], status)
+ }
+
+ toast.success(t("pages.config.save_success"))
+ } catch (err) {
+ toast.error(
+ err instanceof Error ? err.message : t("pages.config.save_error"),
+ )
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ return (
+
+
+
+
+ {t("pages.config.open_raw")}
+
+
+ }
+ />
+
+
+ {isLoading ? (
+
+ {t("labels.loading")}
+
+ ) : error ? (
+
+ {t("pages.config.load_error")}
+
+ ) : (
+
+ {isDirty && (
+
+ {t("pages.config.unsaved_changes")}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ )
+}
diff --git a/web/frontend/src/components/config/config-sections.tsx b/web/frontend/src/components/config/config-sections.tsx
new file mode 100644
index 000000000..340ece333
--- /dev/null
+++ b/web/frontend/src/components/config/config-sections.tsx
@@ -0,0 +1,326 @@
+import { IconCode } from "@tabler/icons-react"
+import { Link } from "@tanstack/react-router"
+import { useTranslation } from "react-i18next"
+
+import {
+ type CoreConfigForm,
+ DM_SCOPE_OPTIONS,
+ type LauncherForm,
+} from "@/components/config/form-model"
+import { Field, SwitchCardField } from "@/components/shared-form"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { Textarea } from "@/components/ui/textarea"
+
+type UpdateCoreField = (
+ key: K,
+ value: CoreConfigForm[K],
+) => void
+
+type UpdateLauncherField = (
+ key: K,
+ value: LauncherForm[K],
+) => void
+
+interface AgentDefaultsSectionProps {
+ form: CoreConfigForm
+ onFieldChange: UpdateCoreField
+}
+
+export function AgentDefaultsSection({
+ form,
+ onFieldChange,
+}: AgentDefaultsSectionProps) {
+ const { t } = useTranslation()
+
+ return (
+
+ )
+}
+
+interface RuntimeSectionProps {
+ form: CoreConfigForm
+ onFieldChange: UpdateCoreField
+}
+
+export function RuntimeSection({ form, onFieldChange }: RuntimeSectionProps) {
+ const { t } = useTranslation()
+ const selectedDmScopeOption = DM_SCOPE_OPTIONS.find(
+ (scope) => scope.value === form.dmScope,
+ )
+
+ return (
+
+
+
+
+
+
+
+ onFieldChange("heartbeatEnabled", checked)
+ }
+ />
+
+ {form.heartbeatEnabled && (
+
+
+ onFieldChange("heartbeatInterval", e.target.value)
+ }
+ />
+
+ )}
+
+
+ )
+}
+
+interface LauncherSectionProps {
+ launcherForm: LauncherForm
+ onFieldChange: UpdateLauncherField
+ launcherHint: string
+ disabled: boolean
+}
+
+export function LauncherSection({
+ launcherForm,
+ onFieldChange,
+ launcherHint,
+ disabled,
+}: LauncherSectionProps) {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+ onFieldChange("port", e.target.value)}
+ />
+
+
+
onFieldChange("publicAccess", checked)}
+ />
+
+
+
+
+ {launcherHint}
+
+
+ )
+}
+
+interface DevicesSectionProps {
+ form: CoreConfigForm
+ onFieldChange: UpdateCoreField
+ autoStartEnabled: boolean
+ autoStartHint: string
+ autoStartDisabled: boolean
+ onAutoStartChange: (checked: boolean) => void
+}
+
+export function DevicesSection({
+ form,
+ onFieldChange,
+ autoStartEnabled,
+ autoStartHint,
+ autoStartDisabled,
+ onAutoStartChange,
+}: DevicesSectionProps) {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+ onFieldChange("devicesEnabled", checked)
+ }
+ />
+
+ onFieldChange("monitorUSB", checked)}
+ />
+
+
+
+
+ )
+}
+
+export function AdvancedSection() {
+ const { t } = useTranslation()
+
+ return (
+
+
+ {t("pages.config.advanced_desc")}
+
+
+
+
+
+ )
+}
diff --git a/web/frontend/src/components/config/form-model.ts b/web/frontend/src/components/config/form-model.ts
new file mode 100644
index 000000000..0768d3e7f
--- /dev/null
+++ b/web/frontend/src/components/config/form-model.ts
@@ -0,0 +1,172 @@
+export type JsonRecord = Record
+
+export interface CoreConfigForm {
+ workspace: string
+ restrictToWorkspace: boolean
+ maxTokens: string
+ maxToolIterations: string
+ summarizeMessageThreshold: string
+ summarizeTokenPercent: string
+ dmScope: string
+ heartbeatEnabled: boolean
+ heartbeatInterval: string
+ devicesEnabled: boolean
+ monitorUSB: boolean
+}
+
+export interface LauncherForm {
+ port: string
+ publicAccess: boolean
+ allowedCIDRsText: string
+}
+
+export const DM_SCOPE_OPTIONS = [
+ {
+ value: "per-channel-peer",
+ labelKey: "pages.config.session_scope_per_channel_peer",
+ labelDefault: "Per Channel + Peer",
+ descKey: "pages.config.session_scope_per_channel_peer_desc",
+ descDefault: "Separate context for each user in each channel.",
+ },
+ {
+ value: "per-channel",
+ labelKey: "pages.config.session_scope_per_channel",
+ labelDefault: "Per Channel",
+ descKey: "pages.config.session_scope_per_channel_desc",
+ descDefault: "One shared context per channel.",
+ },
+ {
+ value: "per-peer",
+ labelKey: "pages.config.session_scope_per_peer",
+ labelDefault: "Per Peer",
+ descKey: "pages.config.session_scope_per_peer_desc",
+ descDefault: "One context per user across channels.",
+ },
+ {
+ value: "global",
+ labelKey: "pages.config.session_scope_global",
+ labelDefault: "Global",
+ descKey: "pages.config.session_scope_global_desc",
+ descDefault: "All messages share one global context.",
+ },
+] as const
+
+export const EMPTY_FORM: CoreConfigForm = {
+ workspace: "",
+ restrictToWorkspace: true,
+ maxTokens: "32768",
+ maxToolIterations: "50",
+ summarizeMessageThreshold: "20",
+ summarizeTokenPercent: "75",
+ dmScope: "per-channel-peer",
+ heartbeatEnabled: true,
+ heartbeatInterval: "30",
+ devicesEnabled: false,
+ monitorUSB: true,
+}
+
+export const EMPTY_LAUNCHER_FORM: LauncherForm = {
+ port: "18800",
+ publicAccess: false,
+ allowedCIDRsText: "",
+}
+
+function asRecord(value: unknown): JsonRecord {
+ if (value && typeof value === "object" && !Array.isArray(value)) {
+ return value as JsonRecord
+ }
+ return {}
+}
+
+function asString(value: unknown): string {
+ return typeof value === "string" ? value : ""
+}
+
+function asBool(value: unknown): boolean {
+ return value === true
+}
+
+function asNumberString(value: unknown, fallback: string): string {
+ if (typeof value === "number" && Number.isFinite(value)) {
+ return String(value)
+ }
+ if (typeof value === "string" && value.trim() !== "") {
+ return value
+ }
+ return fallback
+}
+
+export function buildFormFromConfig(config: unknown): CoreConfigForm {
+ const root = asRecord(config)
+ const agents = asRecord(root.agents)
+ const defaults = asRecord(agents.defaults)
+ const session = asRecord(root.session)
+ const heartbeat = asRecord(root.heartbeat)
+ const devices = asRecord(root.devices)
+
+ return {
+ workspace: asString(defaults.workspace) || EMPTY_FORM.workspace,
+ restrictToWorkspace:
+ defaults.restrict_to_workspace === undefined
+ ? EMPTY_FORM.restrictToWorkspace
+ : asBool(defaults.restrict_to_workspace),
+ maxTokens: asNumberString(defaults.max_tokens, EMPTY_FORM.maxTokens),
+ maxToolIterations: asNumberString(
+ defaults.max_tool_iterations,
+ EMPTY_FORM.maxToolIterations,
+ ),
+ summarizeMessageThreshold: asNumberString(
+ defaults.summarize_message_threshold,
+ EMPTY_FORM.summarizeMessageThreshold,
+ ),
+ summarizeTokenPercent: asNumberString(
+ defaults.summarize_token_percent,
+ EMPTY_FORM.summarizeTokenPercent,
+ ),
+ dmScope: asString(session.dm_scope) || EMPTY_FORM.dmScope,
+ heartbeatEnabled:
+ heartbeat.enabled === undefined
+ ? EMPTY_FORM.heartbeatEnabled
+ : asBool(heartbeat.enabled),
+ heartbeatInterval: asNumberString(
+ heartbeat.interval,
+ EMPTY_FORM.heartbeatInterval,
+ ),
+ devicesEnabled:
+ devices.enabled === undefined
+ ? EMPTY_FORM.devicesEnabled
+ : asBool(devices.enabled),
+ monitorUSB:
+ devices.monitor_usb === undefined
+ ? EMPTY_FORM.monitorUSB
+ : asBool(devices.monitor_usb),
+ }
+}
+
+export function parseIntField(
+ rawValue: string,
+ label: string,
+ options: { min?: number; max?: number } = {},
+): number {
+ const value = Number(rawValue)
+ if (!Number.isInteger(value)) {
+ throw new Error(`${label} must be an integer.`)
+ }
+ if (options.min !== undefined && value < options.min) {
+ throw new Error(`${label} must be >= ${options.min}.`)
+ }
+ if (options.max !== undefined && value > options.max) {
+ throw new Error(`${label} must be <= ${options.max}.`)
+ }
+ return value
+}
+
+export function parseCIDRText(raw: string): string[] {
+ if (!raw.trim()) {
+ return []
+ }
+ return raw
+ .split(/[\n,]/)
+ .map((v) => v.trim())
+ .filter((v) => v.length > 0)
+}
diff --git a/web/frontend/src/components/config/raw-json-panel.tsx b/web/frontend/src/components/config/raw-json-panel.tsx
new file mode 100644
index 000000000..f67bd89f5
--- /dev/null
+++ b/web/frontend/src/components/config/raw-json-panel.tsx
@@ -0,0 +1,204 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
+import { useState } from "react"
+import { useTranslation } from "react-i18next"
+import { toast } from "sonner"
+
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from "@/components/ui/alert-dialog"
+import { Button } from "@/components/ui/button"
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { Textarea } from "@/components/ui/textarea"
+
+export function RawJsonPanel() {
+ const { t } = useTranslation()
+ const queryClient = useQueryClient()
+
+ const { data: config, isLoading } = useQuery({
+ queryKey: ["config"],
+ queryFn: async () => {
+ const res = await fetch("/api/config")
+ if (!res.ok) {
+ throw new Error("Failed to fetch config")
+ }
+ return res.json()
+ },
+ })
+
+ const mutation = useMutation({
+ mutationFn: async (newConfig: string) => {
+ const res = await fetch("/api/config", {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: newConfig,
+ })
+ if (!res.ok) {
+ throw new Error("Failed to save config")
+ }
+ },
+ onSuccess: (_, submittedConfig) => {
+ toast.success(t("pages.config.save_success"))
+ try {
+ const savedConfig = JSON.parse(submittedConfig)
+ setLastSavedConfig(savedConfig)
+ setIsDirty(false)
+ queryClient.invalidateQueries({ queryKey: ["config"] })
+ } catch {
+ queryClient.invalidateQueries({ queryKey: ["config"] })
+ }
+ },
+ onError: () => {
+ toast.error(t("pages.config.save_error"))
+ },
+ })
+
+ const [editorValue, setEditorValue] = useState("")
+ const [isDirty, setIsDirty] = useState(false)
+ const [lastSavedConfig, setLastSavedConfig] = useState | null>(null)
+
+ const effectiveEditorValue =
+ editorValue || (config ? JSON.stringify(config, null, 2) : "")
+
+ const handleSave = () => {
+ try {
+ JSON.parse(effectiveEditorValue)
+ mutation.mutate(effectiveEditorValue)
+ } catch (error) {
+ toast.error(
+ t(
+ "pages.config.invalid_json",
+ error instanceof Error ? error.message : "Invalid JSON format.",
+ ),
+ )
+ }
+ }
+
+ const handleFormat = () => {
+ try {
+ const formatted = JSON.stringify(
+ JSON.parse(effectiveEditorValue),
+ null,
+ 2,
+ )
+ setEditorValue(formatted)
+ toast.success(t("pages.config.format_success"))
+ } catch (error) {
+ toast.error(
+ t(
+ "pages.config.format_error",
+ error instanceof Error ? error.message : "Invalid JSON format.",
+ ),
+ )
+ }
+ }
+
+ const [showResetDialog, setShowResetDialog] = useState(false)
+
+ const confirmReset = () => {
+ if (lastSavedConfig) {
+ setEditorValue(JSON.stringify(lastSavedConfig, null, 2))
+ } else if (config) {
+ setEditorValue(JSON.stringify(config, null, 2))
+ }
+ setIsDirty(false)
+ toast.info(t("pages.config.reset_success"))
+ setShowResetDialog(false)
+ }
+
+ return (
+
+
+ {t("pages.config.raw_json_title")}
+ {t("pages.config.raw_json_desc")}
+
+
+ {isLoading ? (
+
+
{t("labels.loading")}
+
+ ) : (
+
+ {isDirty && (
+
+ {t("pages.config.unsaved_changes")}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t("pages.config.reset_confirm_title")}
+
+
+ {t("pages.config.reset_confirm_desc")}
+
+
+
+ {t("common.cancel")}
+
+ {t("common.confirm")}
+
+
+
+
+
+
+
+ )}
+
+
+ )
+}
diff --git a/web/frontend/src/components/credentials/anthropic-credential-card.tsx b/web/frontend/src/components/credentials/anthropic-credential-card.tsx
new file mode 100644
index 000000000..c7e968646
--- /dev/null
+++ b/web/frontend/src/components/credentials/anthropic-credential-card.tsx
@@ -0,0 +1,108 @@
+import {
+ IconKey,
+ IconLoader2,
+ IconPlayerStopFilled,
+ IconSparkles,
+} from "@tabler/icons-react"
+import { useTranslation } from "react-i18next"
+
+import type { OAuthProviderStatus } from "@/api/oauth"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+
+import { CredentialCard } from "./credential-card"
+
+interface AnthropicCredentialCardProps {
+ status?: OAuthProviderStatus
+ activeAction: string
+ token: string
+ onTokenChange: (value: string) => void
+ onStopLoading: () => void
+ onSaveToken: () => void
+ onAskLogout: () => void
+}
+
+export function AnthropicCredentialCard({
+ status,
+ activeAction,
+ token,
+ onTokenChange,
+ onStopLoading,
+ onSaveToken,
+ onAskLogout,
+}: AnthropicCredentialCardProps) {
+ const { t } = useTranslation()
+ const actionBusy = activeAction !== ""
+ const tokenLoading = activeAction === "anthropic:token"
+ const stopLabel = t("credentials.actions.stopLoading")
+
+ return (
+
+
+
+
+ Anthropic
+
+ }
+ description={t("credentials.providers.anthropic.description")}
+ status={status?.status ?? "not_logged_in"}
+ authMethod={status?.auth_method}
+ actions={
+
+
+
+ onTokenChange(e.target.value)}
+ type="password"
+ placeholder={t("credentials.fields.anthropicToken")}
+ />
+
+ {tokenLoading && (
+
+ )}
+
+
+
+ }
+ footer={
+ status?.logged_in ? (
+
+ ) : null
+ }
+ />
+ )
+}
diff --git a/web/frontend/src/components/credentials/antigravity-credential-card.tsx b/web/frontend/src/components/credentials/antigravity-credential-card.tsx
new file mode 100644
index 000000000..62a19145b
--- /dev/null
+++ b/web/frontend/src/components/credentials/antigravity-credential-card.tsx
@@ -0,0 +1,106 @@
+import {
+ IconBrandGoogle,
+ IconLoader2,
+ IconLockOpen,
+ IconPlayerStopFilled,
+} from "@tabler/icons-react"
+import { useTranslation } from "react-i18next"
+
+import type { OAuthProviderStatus } from "@/api/oauth"
+import { Button } from "@/components/ui/button"
+
+import { CredentialCard } from "./credential-card"
+
+interface AntigravityCredentialCardProps {
+ status?: OAuthProviderStatus
+ activeAction: string
+ onStopLoading: () => void
+ onStartBrowserOAuth: () => void
+ onAskLogout: () => void
+}
+
+export function AntigravityCredentialCard({
+ status,
+ activeAction,
+ onStopLoading,
+ onStartBrowserOAuth,
+ onAskLogout,
+}: AntigravityCredentialCardProps) {
+ const { t } = useTranslation()
+ const actionBusy = activeAction !== ""
+ const browserLoading = activeAction === "google-antigravity:browser"
+
+ return (
+
+
+
+
+ Google Antigravity
+
+ }
+ description={t("credentials.providers.antigravity.description")}
+ status={status?.status ?? "not_logged_in"}
+ authMethod={status?.auth_method}
+ details={
+
+ {status?.email && (
+
+ {t("credentials.labels.email")}: {status.email}
+
+ )}
+ {status?.project_id && (
+
+ {t("credentials.labels.project")}: {status.project_id}
+
+ )}
+
+ }
+ actions={
+
+
+
+ {browserLoading && (
+
+ )}
+
+
+ }
+ footer={
+ status?.logged_in ? (
+
+ ) : null
+ }
+ />
+ )
+}
diff --git a/web/frontend/src/components/credentials/credential-card.tsx b/web/frontend/src/components/credentials/credential-card.tsx
new file mode 100644
index 000000000..60e849972
--- /dev/null
+++ b/web/frontend/src/components/credentials/credential-card.tsx
@@ -0,0 +1,44 @@
+import type { ReactNode } from "react"
+
+import type { OAuthProviderStatus } from "@/api/oauth"
+
+import { ProviderStatusLine } from "./provider-status-line"
+
+interface CredentialCardProps {
+ title: ReactNode
+ description: string
+ status: OAuthProviderStatus["status"]
+ authMethod?: string
+ details?: ReactNode
+ actions: ReactNode
+ footer?: ReactNode
+}
+
+export function CredentialCard({
+ title,
+ description,
+ status,
+ authMethod,
+ details,
+ actions,
+ footer,
+}: CredentialCardProps) {
+ return (
+
+
+
{title}
+
{description}
+
+
+
+
+ {details}
+
+
+
+
+ )
+}
diff --git a/web/frontend/src/components/credentials/credentials-page.tsx b/web/frontend/src/components/credentials/credentials-page.tsx
new file mode 100644
index 000000000..04aceb002
--- /dev/null
+++ b/web/frontend/src/components/credentials/credentials-page.tsx
@@ -0,0 +1,127 @@
+import { IconLoader2 } from "@tabler/icons-react"
+import { useTranslation } from "react-i18next"
+
+import { PageHeader } from "@/components/page-header"
+import { useCredentialsPage } from "@/hooks/use-credentials-page"
+
+import { AnthropicCredentialCard } from "./anthropic-credential-card"
+import { AntigravityCredentialCard } from "./antigravity-credential-card"
+import { DeviceCodeSheet } from "./device-code-sheet"
+import { LogoutConfirmDialog } from "./logout-confirm-dialog"
+import { OpenAICredentialCard } from "./openai-credential-card"
+
+export function CredentialsPage() {
+ const { t } = useTranslation()
+ const {
+ loading,
+ error,
+ activeAction,
+ activeFlow,
+ flowHint,
+ openAIToken,
+ anthropicToken,
+ openaiStatus,
+ anthropicStatus,
+ antigravityStatus,
+ logoutDialogOpen,
+ logoutConfirmProvider,
+ logoutProviderLabel,
+ deviceSheetOpen,
+ deviceFlow,
+ setOpenAIToken,
+ setAnthropicToken,
+ startBrowserOAuth,
+ startOpenAIDeviceCode,
+ stopLoading,
+ saveToken,
+ askLogout,
+ handleConfirmLogout,
+ handleLogoutDialogOpenChange,
+ handleDeviceSheetOpenChange,
+ } = useCredentialsPage()
+
+ return (
+
+
+
+
+
+
+ {t("credentials.description")}
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {activeFlow && (
+
+
{t("credentials.flow.current")}
+
{flowHint}
+
+ )}
+
+ {loading ? (
+
+
+ {t("credentials.loading")}
+
+ ) : (
+
+
void startBrowserOAuth("openai")}
+ onStartDeviceCode={() => void startOpenAIDeviceCode()}
+ onStopLoading={stopLoading}
+ onSaveToken={() => void saveToken("openai", openAIToken.trim())}
+ onAskLogout={() => askLogout("openai")}
+ />
+
+
+ void saveToken("anthropic", anthropicToken.trim())
+ }
+ onAskLogout={() => askLogout("anthropic")}
+ />
+
+
+ void startBrowserOAuth("google-antigravity")
+ }
+ onAskLogout={() => askLogout("google-antigravity")}
+ />
+
+ )}
+
+
+
+
+
+
+ )
+}
diff --git a/web/frontend/src/components/credentials/device-code-sheet.tsx b/web/frontend/src/components/credentials/device-code-sheet.tsx
new file mode 100644
index 000000000..c6d2a754f
--- /dev/null
+++ b/web/frontend/src/components/credentials/device-code-sheet.tsx
@@ -0,0 +1,92 @@
+import { IconRefresh } from "@tabler/icons-react"
+import { useTranslation } from "react-i18next"
+
+import type { OAuthFlowState } from "@/api/oauth"
+import { Button } from "@/components/ui/button"
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet"
+
+interface DeviceCodeSheetProps {
+ open: boolean
+ flow: OAuthFlowState | null
+ flowHint: string
+ onOpenChange: (open: boolean) => void
+}
+
+export function DeviceCodeSheet({
+ open,
+ flow,
+ flowHint,
+ onOpenChange,
+}: DeviceCodeSheetProps) {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+ {t("credentials.device.title")}
+
+ {t("credentials.device.description")}
+
+
+
+
+
+
+ {t("credentials.device.code")}
+
+
+ {flow?.user_code || "-"}
+
+
+
+
+
+
+
+ {t("credentials.device.polling")}
+
+
+ {flow && (
+
+ {flowHint}
+
+ )}
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/web/frontend/src/components/credentials/logout-confirm-dialog.tsx b/web/frontend/src/components/credentials/logout-confirm-dialog.tsx
new file mode 100644
index 000000000..1a9eebffd
--- /dev/null
+++ b/web/frontend/src/components/credentials/logout-confirm-dialog.tsx
@@ -0,0 +1,57 @@
+import { IconLoader2 } from "@tabler/icons-react"
+import { useTranslation } from "react-i18next"
+
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/components/ui/alert-dialog"
+
+interface LogoutConfirmDialogProps {
+ open: boolean
+ providerLabel: string
+ isSubmitting: boolean
+ onOpenChange: (open: boolean) => void
+ onConfirm: () => void | Promise
+}
+
+export function LogoutConfirmDialog({
+ open,
+ providerLabel,
+ isSubmitting,
+ onOpenChange,
+ onConfirm,
+}: LogoutConfirmDialogProps) {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+
+ {t("credentials.logoutDialog.title")}
+
+
+ {t(
+ "credentials.logoutDialog.description",
+ "This will remove your saved credential for {{provider}}.",
+ { provider: providerLabel },
+ )}
+
+
+
+ {t("common.cancel")}
+
+ {isSubmitting && }
+ {t("credentials.actions.logout")}
+
+
+
+
+ )
+}
diff --git a/web/frontend/src/components/credentials/openai-credential-card.tsx b/web/frontend/src/components/credentials/openai-credential-card.tsx
new file mode 100644
index 000000000..925508191
--- /dev/null
+++ b/web/frontend/src/components/credentials/openai-credential-card.tsx
@@ -0,0 +1,162 @@
+import {
+ IconBrandOpenai,
+ IconClockHour4,
+ IconKey,
+ IconLoader2,
+ IconPlayerStopFilled,
+} from "@tabler/icons-react"
+import { useTranslation } from "react-i18next"
+
+import type { OAuthProviderStatus } from "@/api/oauth"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+
+import { CredentialCard } from "./credential-card"
+
+interface OpenAICredentialCardProps {
+ status?: OAuthProviderStatus
+ activeAction: string
+ token: string
+ onTokenChange: (value: string) => void
+ onStartBrowserOAuth: () => void
+ onStartDeviceCode: () => void
+ onStopLoading: () => void
+ onSaveToken: () => void
+ onAskLogout: () => void
+}
+
+export function OpenAICredentialCard({
+ status,
+ activeAction,
+ token,
+ onTokenChange,
+ onStartBrowserOAuth,
+ onStartDeviceCode,
+ onStopLoading,
+ onSaveToken,
+ onAskLogout,
+}: OpenAICredentialCardProps) {
+ const { t } = useTranslation()
+ const actionBusy = activeAction !== ""
+ const browserLoading = activeAction === "openai:browser"
+ const deviceLoading = activeAction === "openai:device"
+ const oauthLoading = browserLoading || deviceLoading
+ const tokenLoading = activeAction === "openai:token"
+
+ return (
+
+
+
+
+ OpenAI
+
+ }
+ description={t("credentials.providers.openai.description")}
+ status={status?.status ?? "not_logged_in"}
+ authMethod={status?.auth_method}
+ details={
+ status?.account_id ? (
+
+ {t("credentials.labels.account")}: {status.account_id}
+
+ ) : null
+ }
+ actions={
+
+
+
+
+
+
+ {oauthLoading && !deviceLoading && (
+
+ )}
+
+
+
+
+
+
+
+ onTokenChange(e.target.value)}
+ type="password"
+ placeholder={t("credentials.fields.openaiToken")}
+ />
+
+ {tokenLoading && (
+
+ )}
+
+
+
+
+ }
+ footer={
+ status?.logged_in ? (
+
+ ) : null
+ }
+ />
+ )
+}
diff --git a/web/frontend/src/components/credentials/provider-status-line.tsx b/web/frontend/src/components/credentials/provider-status-line.tsx
new file mode 100644
index 000000000..d891d18df
--- /dev/null
+++ b/web/frontend/src/components/credentials/provider-status-line.tsx
@@ -0,0 +1,43 @@
+import { useTranslation } from "react-i18next"
+
+import type { OAuthProviderStatus } from "@/api/oauth"
+
+interface ProviderStatusLineProps {
+ status: OAuthProviderStatus["status"]
+ authMethod?: string
+}
+
+export function ProviderStatusLine({
+ status,
+ authMethod,
+}: ProviderStatusLineProps) {
+ const { t } = useTranslation()
+
+ const style =
+ status === "connected"
+ ? "bg-green-500/10 text-green-700 dark:text-green-300"
+ : status === "needs_refresh"
+ ? "bg-amber-500/10 text-amber-700 dark:text-amber-300"
+ : status === "expired"
+ ? "bg-red-500/10 text-red-700 dark:text-red-300"
+ : "bg-muted text-muted-foreground"
+
+ return (
+
+
+ {status === "connected"
+ ? t("credentials.status.connected")
+ : status === "needs_refresh"
+ ? t("credentials.status.needsRefresh")
+ : status === "expired"
+ ? t("credentials.status.expired")
+ : t("credentials.status.notLoggedIn")}
+
+ {authMethod && (
+
+ {authMethod}
+
+ )}
+
+ )
+}
diff --git a/web/frontend/src/components/models/add-model-sheet.tsx b/web/frontend/src/components/models/add-model-sheet.tsx
new file mode 100644
index 000000000..c760bc672
--- /dev/null
+++ b/web/frontend/src/components/models/add-model-sheet.tsx
@@ -0,0 +1,330 @@
+import { IconLoader2 } from "@tabler/icons-react"
+import { useEffect, useState } from "react"
+import { useTranslation } from "react-i18next"
+
+import { addModel, setDefaultModel } from "@/api/models"
+import { maskedSecretPlaceholder } from "@/components/secret-placeholder"
+import {
+ AdvancedSection,
+ Field,
+ KeyInput,
+ SwitchCardField,
+} from "@/components/shared-form"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet"
+
+interface AddForm {
+ modelName: string
+ model: string
+ apiBase: string
+ apiKey: string
+ proxy: string
+ authMethod: string
+ connectMode: string
+ workspace: string
+ rpm: string
+ maxTokensField: string
+ requestTimeout: string
+ thinkingLevel: string
+}
+
+const EMPTY_ADD_FORM: AddForm = {
+ modelName: "",
+ model: "",
+ apiBase: "",
+ apiKey: "",
+ proxy: "",
+ authMethod: "",
+ connectMode: "",
+ workspace: "",
+ rpm: "",
+ maxTokensField: "",
+ requestTimeout: "",
+ thinkingLevel: "",
+}
+
+interface AddModelSheetProps {
+ open: boolean
+ onClose: () => void
+ onSaved: () => void
+ existingModelNames: string[]
+}
+
+export function AddModelSheet({
+ open,
+ onClose,
+ onSaved,
+ existingModelNames,
+}: AddModelSheetProps) {
+ const { t } = useTranslation()
+ const [form, setForm] = useState(EMPTY_ADD_FORM)
+ const [saving, setSaving] = useState(false)
+ const [setAsDefault, setSetAsDefault] = useState(false)
+ const [fieldErrors, setFieldErrors] = useState<
+ Partial>
+ >({})
+ const [serverError, setServerError] = useState("")
+ const apiKeyPlaceholder = maskedSecretPlaceholder(
+ form.apiKey,
+ t("models.field.apiKeyPlaceholder"),
+ )
+
+ useEffect(() => {
+ if (open) {
+ setForm(EMPTY_ADD_FORM)
+ setSetAsDefault(false)
+ setFieldErrors({})
+ setServerError("")
+ }
+ }, [open])
+
+ const validate = (): boolean => {
+ const errors: Partial> = {}
+ const modelName = form.modelName.trim()
+ if (!modelName) {
+ errors.modelName = t("models.add.errorRequired")
+ } else if (existingModelNames.some((name) => name.trim() === modelName)) {
+ errors.modelName = t("models.add.errorDuplicateModelName")
+ }
+ if (!form.model.trim()) errors.model = t("models.add.errorRequired")
+ setFieldErrors(errors)
+ return Object.keys(errors).length === 0
+ }
+
+ const setField =
+ (key: keyof AddForm) => (e: React.ChangeEvent) => {
+ setForm((f) => ({ ...f, [key]: e.target.value }))
+ if (fieldErrors[key]) {
+ setFieldErrors((prev) => ({ ...prev, [key]: undefined }))
+ }
+ }
+
+ const handleSave = async () => {
+ if (!validate()) return
+ setSaving(true)
+ setServerError("")
+ try {
+ const modelName = form.modelName.trim()
+ const modelId = form.model.trim()
+ await addModel({
+ model_name: modelName,
+ model: modelId,
+ api_base: form.apiBase.trim() || undefined,
+ api_key: form.apiKey.trim() || undefined,
+ proxy: form.proxy.trim() || undefined,
+ auth_method: form.authMethod.trim() || undefined,
+ connect_mode: form.connectMode.trim() || undefined,
+ workspace: form.workspace.trim() || undefined,
+ rpm: form.rpm ? Number(form.rpm) : undefined,
+ max_tokens_field: form.maxTokensField.trim() || undefined,
+ request_timeout: form.requestTimeout
+ ? Number(form.requestTimeout)
+ : undefined,
+ thinking_level: form.thinkingLevel.trim() || undefined,
+ })
+ if (setAsDefault) {
+ await setDefaultModel(modelName)
+ }
+ onSaved()
+ onClose()
+ } catch (e) {
+ setServerError(e instanceof Error ? e.message : t("models.add.saveError"))
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ return (
+ !v && onClose()}>
+
+
+ {t("models.add.title")}
+
+ {t("models.add.description")}
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/web/frontend/src/components/models/delete-model-dialog.tsx b/web/frontend/src/components/models/delete-model-dialog.tsx
new file mode 100644
index 000000000..72b8df002
--- /dev/null
+++ b/web/frontend/src/components/models/delete-model-dialog.tsx
@@ -0,0 +1,74 @@
+import { IconLoader2 } from "@tabler/icons-react"
+import { useState } from "react"
+import { useTranslation } from "react-i18next"
+
+import { type ModelInfo, deleteModel } from "@/api/models"
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/components/ui/alert-dialog"
+
+interface DeleteModelDialogProps {
+ model: ModelInfo | null
+ onClose: () => void
+ onDeleted: () => void
+}
+
+export function DeleteModelDialog({
+ model,
+ onClose,
+ onDeleted,
+}: DeleteModelDialogProps) {
+ const { t } = useTranslation()
+ const [deleting, setDeleting] = useState(false)
+
+ const handleConfirm = async () => {
+ if (!model) return
+ if (model.is_default) {
+ onClose()
+ return
+ }
+ setDeleting(true)
+ try {
+ await deleteModel(model.index)
+ onDeleted()
+ } catch {
+ // ignore, user can retry from list
+ } finally {
+ setDeleting(false)
+ onClose()
+ }
+ }
+
+ return (
+ !v && onClose()}>
+
+
+ {t("models.delete.title")}
+
+ {t("models.delete.description", { name: model?.model_name })}
+
+
+
+
+ {t("common.cancel")}
+
+
+ {deleting && }
+ {t("models.delete.confirm")}
+
+
+
+
+ )
+}
diff --git a/web/frontend/src/components/models/edit-model-sheet.tsx b/web/frontend/src/components/models/edit-model-sheet.tsx
new file mode 100644
index 000000000..4c77944a9
--- /dev/null
+++ b/web/frontend/src/components/models/edit-model-sheet.tsx
@@ -0,0 +1,298 @@
+import { IconLoader2 } from "@tabler/icons-react"
+import { useEffect, useState } from "react"
+import { useTranslation } from "react-i18next"
+
+import { type ModelInfo, setDefaultModel, updateModel } from "@/api/models"
+import { maskedSecretPlaceholder } from "@/components/secret-placeholder"
+import {
+ AdvancedSection,
+ Field,
+ KeyInput,
+ SwitchCardField,
+} from "@/components/shared-form"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet"
+
+interface EditForm {
+ apiKey: string
+ apiBase: string
+ proxy: string
+ authMethod: string
+ connectMode: string
+ workspace: string
+ rpm: string
+ maxTokensField: string
+ requestTimeout: string
+ thinkingLevel: string
+}
+
+interface EditModelSheetProps {
+ model: ModelInfo | null
+ open: boolean
+ onClose: () => void
+ onSaved: () => void
+}
+
+export function EditModelSheet({
+ model,
+ open,
+ onClose,
+ onSaved,
+}: EditModelSheetProps) {
+ const { t } = useTranslation()
+ const [form, setForm] = useState({
+ apiKey: "",
+ apiBase: "",
+ proxy: "",
+ authMethod: "",
+ connectMode: "",
+ workspace: "",
+ rpm: "",
+ maxTokensField: "",
+ requestTimeout: "",
+ thinkingLevel: "",
+ })
+ const [saving, setSaving] = useState(false)
+ const [setAsDefault, setSetAsDefault] = useState(false)
+ const [error, setError] = useState("")
+
+ useEffect(() => {
+ if (model) {
+ setForm({
+ apiKey: "",
+ apiBase: model.api_base ?? "",
+ proxy: model.proxy ?? "",
+ authMethod: model.auth_method ?? "",
+ connectMode: model.connect_mode ?? "",
+ workspace: model.workspace ?? "",
+ rpm: model.rpm ? String(model.rpm) : "",
+ maxTokensField: model.max_tokens_field ?? "",
+ requestTimeout: model.request_timeout
+ ? String(model.request_timeout)
+ : "",
+ thinkingLevel: model.thinking_level ?? "",
+ })
+ setSetAsDefault(model.is_default)
+ setError("")
+ }
+ }, [model])
+
+ const setField =
+ (key: keyof EditForm) => (e: React.ChangeEvent) =>
+ setForm((f) => ({ ...f, [key]: e.target.value }))
+
+ const handleSave = async () => {
+ if (!model) return
+ setSaving(true)
+ setError("")
+ try {
+ await updateModel(model.index, {
+ model_name: model.model_name,
+ model: model.model,
+ api_base: form.apiBase || undefined,
+ api_key: form.apiKey || undefined,
+ proxy: form.proxy || undefined,
+ auth_method: form.authMethod || undefined,
+ connect_mode: form.connectMode || undefined,
+ workspace: form.workspace || undefined,
+ rpm: form.rpm ? Number(form.rpm) : undefined,
+ max_tokens_field: form.maxTokensField || undefined,
+ request_timeout: form.requestTimeout
+ ? Number(form.requestTimeout)
+ : undefined,
+ thinking_level: form.thinkingLevel || undefined,
+ })
+ if (setAsDefault) {
+ await setDefaultModel(model.model_name)
+ }
+ onSaved()
+ onClose()
+ } catch (e) {
+ setError(e instanceof Error ? e.message : t("models.edit.saveError"))
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const isOAuth = model?.auth_method === "oauth"
+ const apiKeyPlaceholder = model?.configured
+ ? maskedSecretPlaceholder(
+ model.api_key,
+ t("models.field.apiKeyPlaceholderSet"),
+ )
+ : t("models.field.apiKeyPlaceholder")
+
+ return (
+ !v && onClose()}>
+
+
+
+ {t("models.edit.title", { name: model?.model_name })}
+
+
+ {model?.model}
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/web/frontend/src/components/models/model-card.tsx b/web/frontend/src/components/models/model-card.tsx
new file mode 100644
index 000000000..316e05e4d
--- /dev/null
+++ b/web/frontend/src/components/models/model-card.tsx
@@ -0,0 +1,137 @@
+import {
+ IconEdit,
+ IconKey,
+ IconLoader2,
+ IconStar,
+ IconStarFilled,
+ IconTrash,
+} from "@tabler/icons-react"
+import { useTranslation } from "react-i18next"
+
+import type { ModelInfo } from "@/api/models"
+import { Button } from "@/components/ui/button"
+
+interface ModelCardProps {
+ model: ModelInfo
+ onEdit: (model: ModelInfo) => void
+ onSetDefault: (model: ModelInfo) => void
+ onDelete: (model: ModelInfo) => void
+ settingDefault: boolean
+}
+
+export function ModelCard({
+ model,
+ onEdit,
+ onSetDefault,
+ onDelete,
+ settingDefault,
+}: ModelCardProps) {
+ const { t } = useTranslation()
+ const isOAuth = model.auth_method === "oauth"
+ const canSetDefault = model.configured && !model.is_default
+
+ return (
+
+
+
+
+
+ {model.model_name}
+
+ {model.is_default && (
+
+ {t("models.badge.default")}
+
+ )}
+
+
+
+ {model.is_default ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+ {model.model}
+
+
+
+ {isOAuth ? (
+
+ OAuth
+
+ ) : model.configured && model.api_key ? (
+
+
+ {model.api_key}
+
+ ) : (
+
+ {t("models.status.unconfigured")}
+
+ )}
+
+
+ )
+}
diff --git a/web/frontend/src/components/models/models-page.tsx b/web/frontend/src/components/models/models-page.tsx
new file mode 100644
index 000000000..b8e80e709
--- /dev/null
+++ b/web/frontend/src/components/models/models-page.tsx
@@ -0,0 +1,213 @@
+import { IconLoader2, IconPlus, IconStar } from "@tabler/icons-react"
+import { useCallback, useEffect, useState } from "react"
+import { useTranslation } from "react-i18next"
+
+import { type ModelInfo, getModels, setDefaultModel } from "@/api/models"
+import { PageHeader } from "@/components/page-header"
+import { Button } from "@/components/ui/button"
+
+import { AddModelSheet } from "./add-model-sheet"
+import { DeleteModelDialog } from "./delete-model-dialog"
+import { EditModelSheet } from "./edit-model-sheet"
+import { getProviderKey, getProviderLabel } from "./provider-label"
+import { ProviderSection } from "./provider-section"
+
+const PROVIDER_PRIORITY: Record = {
+ volcengine: 0,
+ openai: 1,
+ gemini: 2,
+ anthropic: 3,
+ zhipu: 4,
+ deepseek: 5,
+ openrouter: 6,
+ qwen: 7,
+ moonshot: 8,
+ groq: 9,
+ "github-copilot": 10,
+ antigravity: 11,
+ nvidia: 12,
+ cerebras: 13,
+ shengsuanyun: 14,
+ ollama: 15,
+ vllm: 16,
+ mistral: 17,
+ avian: 18,
+}
+
+interface ProviderGroup {
+ key: string
+ label: string
+ models: ModelInfo[]
+ hasDefault: boolean
+ configuredCount: number
+}
+
+export function ModelsPage() {
+ const { t } = useTranslation()
+ const [models, setModels] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [fetchError, setFetchError] = useState("")
+
+ const [editingModel, setEditingModel] = useState(null)
+ const [deletingModel, setDeletingModel] = useState(null)
+ const [addOpen, setAddOpen] = useState(false)
+ const [settingDefaultIndex, setSettingDefaultIndex] = useState(
+ null,
+ )
+
+ const fetchModels = useCallback(async () => {
+ try {
+ const data = await getModels()
+ const sorted = [...data.models].sort((a, b) => {
+ if (a.is_default && !b.is_default) return -1
+ if (!a.is_default && b.is_default) return 1
+ if (a.configured && !b.configured) return -1
+ if (!a.configured && b.configured) return 1
+ return a.model_name.localeCompare(b.model_name)
+ })
+ setModels(sorted)
+ setFetchError("")
+ } catch (e) {
+ setFetchError(e instanceof Error ? e.message : t("models.loadError"))
+ } finally {
+ setLoading(false)
+ }
+ }, [t])
+
+ useEffect(() => {
+ fetchModels()
+ }, [fetchModels])
+
+ const handleSetDefault = async (model: ModelInfo) => {
+ setSettingDefaultIndex(model.index)
+ try {
+ await setDefaultModel(model.model_name)
+ await fetchModels()
+ } catch {
+ // ignore
+ } finally {
+ setSettingDefaultIndex(null)
+ }
+ }
+
+ const grouped: Record = {}
+ for (const model of models) {
+ const providerKey = getProviderKey(model.model)
+ if (!grouped[providerKey]) {
+ grouped[providerKey] = {
+ label: getProviderLabel(model.model),
+ models: [],
+ }
+ }
+ grouped[providerKey].models.push(model)
+ }
+
+ const providerGroups: ProviderGroup[] = Object.entries(grouped)
+ .map(([key, group]) => {
+ const configuredCount = group.models.filter(
+ (model) => model.configured,
+ ).length
+ return {
+ key,
+ label: group.label,
+ models: group.models,
+ hasDefault: group.models.some((model) => model.is_default),
+ configuredCount,
+ }
+ })
+ .sort((a, b) => {
+ if (a.hasDefault && !b.hasDefault) return -1
+ if (!a.hasDefault && b.hasDefault) return 1
+
+ if (a.configuredCount !== b.configuredCount) {
+ return b.configuredCount - a.configuredCount
+ }
+
+ const aPriority = PROVIDER_PRIORITY[a.key] ?? Number.MAX_SAFE_INTEGER
+ const bPriority = PROVIDER_PRIORITY[b.key] ?? Number.MAX_SAFE_INTEGER
+ if (aPriority !== bPriority) {
+ return aPriority - bPriority
+ }
+
+ return a.label.localeCompare(b.label)
+ })
+
+ const defaultModel = models.find((model) => model.is_default)
+
+ return (
+
+
+
+
+
+
+
+
+
+ {!defaultModel && (
+
+ {t("models.noDefaultHintPrefix")}
+
+ {t("models.noDefaultHintSuffix")}
+
+ )}
+
+ {t("models.description")}
+
+
+
+ {loading && (
+
+
+
+ )}
+
+ {fetchError && (
+
+ {fetchError}
+
+ )}
+
+ {!loading && !fetchError && (
+
+ {providerGroups.map((providerGroup) => (
+
+ ))}
+
+ )}
+
+
+
setEditingModel(null)}
+ onSaved={fetchModels}
+ />
+
+ setAddOpen(false)}
+ onSaved={fetchModels}
+ existingModelNames={models.map((model) => model.model_name)}
+ />
+
+ setDeletingModel(null)}
+ onDeleted={fetchModels}
+ />
+
+ )
+}
diff --git a/web/frontend/src/components/models/provider-icon.tsx b/web/frontend/src/components/models/provider-icon.tsx
new file mode 100644
index 000000000..5e2151e2d
--- /dev/null
+++ b/web/frontend/src/components/models/provider-icon.tsx
@@ -0,0 +1,95 @@
+import { useMemo, useState } from "react"
+
+const PROVIDER_ICON_SLUGS: Record = {
+ openai: "openai",
+ anthropic: "anthropic",
+ gemini: "googlegemini",
+ deepseek: "deepseek",
+ qwen: "alibabacloud",
+ groq: "groq",
+ openrouter: "openrouter",
+ nvidia: "nvidia",
+ cerebras: "cerebras",
+ volcengine: "bytedance",
+ "github-copilot": "githubcopilot",
+ ollama: "ollama",
+ mistral: "mistralai",
+ zhipu: "zhipu",
+}
+
+const PROVIDER_DOMAINS: Record = {
+ openai: "openai.com",
+ anthropic: "anthropic.com",
+ gemini: "gemini.google.com",
+ deepseek: "deepseek.com",
+ qwen: "qwenlm.ai",
+ moonshot: "moonshot.ai",
+ groq: "groq.com",
+ openrouter: "openrouter.ai",
+ nvidia: "nvidia.com",
+ cerebras: "cerebras.ai",
+ volcengine: "volcengine.com",
+ shengsuanyun: "shengsuanyun.com",
+ antigravity: "antigravity.google",
+ "github-copilot": "github.com",
+ ollama: "ollama.com",
+ mistral: "mistral.ai",
+ avian: "avian.io",
+ vllm: "vllm.ai",
+ zhipu: "zhipuai.cn",
+}
+
+interface ProviderIconProps {
+ providerKey: string
+ providerLabel: string
+}
+
+export function ProviderIcon({
+ providerKey,
+ providerLabel,
+}: ProviderIconProps) {
+ const [sourceIndex, setSourceIndex] = useState(0)
+ const [loadFailed, setLoadFailed] = useState(false)
+ const initial = providerLabel.trim().charAt(0).toUpperCase() || "?"
+ const iconUrls = useMemo(() => {
+ const slug = PROVIDER_ICON_SLUGS[providerKey]
+ const domain = PROVIDER_DOMAINS[providerKey]
+ const urls: string[] = []
+ if (slug) {
+ urls.push(`https://cdn.simpleicons.org/${slug}`)
+ }
+ if (domain) {
+ urls.push(`https://www.google.com/s2/favicons?domain=${domain}&sz=64`)
+ }
+ return urls
+ }, [providerKey])
+
+ const iconUrl = iconUrls[sourceIndex]
+
+ if (!iconUrl || loadFailed) {
+ return (
+
+ {initial}
+
+ )
+ }
+
+ return (
+
+
{
+ if (sourceIndex < iconUrls.length - 1) {
+ setSourceIndex((idx) => idx + 1)
+ return
+ }
+ setLoadFailed(true)
+ }}
+ />
+
+ )
+}
diff --git a/web/frontend/src/components/models/provider-label.ts b/web/frontend/src/components/models/provider-label.ts
new file mode 100644
index 000000000..923cd9506
--- /dev/null
+++ b/web/frontend/src/components/models/provider-label.ts
@@ -0,0 +1,33 @@
+const PROVIDER_LABELS: Record = {
+ openai: "OpenAI",
+ anthropic: "Anthropic",
+ gemini: "Google Gemini",
+ deepseek: "DeepSeek",
+ qwen: "Qwen (阿里云)",
+ moonshot: "Moonshot (月之暗面)",
+ groq: "Groq",
+ openrouter: "OpenRouter",
+ nvidia: "NVIDIA",
+ cerebras: "Cerebras",
+ volcengine: "Volcengine (火山引擎)",
+ shengsuanyun: "ShengsuanYun (神算云)",
+ antigravity: "Google Code Assist",
+ "github-copilot": "GitHub Copilot",
+ ollama: "Ollama (local)",
+ mistral: "Mistral AI",
+ avian: "Avian",
+ vllm: "VLLM (local)",
+ zhipu: "Zhipu AI (智谱)",
+}
+
+export function getProviderKey(model: string): string {
+ return model.split("/")[0]
+}
+
+export function getProviderLabel(model: string): string {
+ const prefix = getProviderKey(model)
+ const labels: Record = {
+ ...PROVIDER_LABELS,
+ }
+ return labels[prefix] ?? prefix
+}
diff --git a/web/frontend/src/components/models/provider-section.tsx b/web/frontend/src/components/models/provider-section.tsx
new file mode 100644
index 000000000..613364f0f
--- /dev/null
+++ b/web/frontend/src/components/models/provider-section.tsx
@@ -0,0 +1,72 @@
+import { IconChevronDown } from "@tabler/icons-react"
+import { useState } from "react"
+
+import type { ModelInfo } from "@/api/models"
+
+import { ModelCard } from "./model-card"
+import { ProviderIcon } from "./provider-icon"
+
+interface ProviderSectionProps {
+ provider: string
+ providerKey: string
+ models: ModelInfo[]
+ onEdit: (model: ModelInfo) => void
+ onSetDefault: (model: ModelInfo) => void
+ onDelete: (model: ModelInfo) => void
+ settingDefaultIndex: number | null
+}
+
+export function ProviderSection({
+ provider,
+ providerKey,
+ models,
+ onEdit,
+ onSetDefault,
+ onDelete,
+ settingDefaultIndex,
+}: ProviderSectionProps) {
+ const [open, setOpen] = useState(true)
+
+ return (
+
+
+
+ {open && (
+
+ {models.map((model) => (
+
+ ))}
+
+ )}
+
+ )
+}
diff --git a/web/frontend/src/components/page-header.tsx b/web/frontend/src/components/page-header.tsx
new file mode 100644
index 000000000..9d4aa6975
--- /dev/null
+++ b/web/frontend/src/components/page-header.tsx
@@ -0,0 +1,27 @@
+import { IconMenu2 } from "@tabler/icons-react"
+import type { ReactNode } from "react"
+
+import { SidebarTrigger } from "@/components/ui/sidebar"
+
+interface PageHeaderProps {
+ title: string
+ titleExtra?: ReactNode
+ children?: ReactNode
+}
+
+export function PageHeader({ title, titleExtra, children }: PageHeaderProps) {
+ return (
+
+
+
+
+
+
+ {title}
+
+ {titleExtra}
+
+ {children &&
{children}
}
+
+ )
+}
diff --git a/web/frontend/src/components/secret-placeholder.ts b/web/frontend/src/components/secret-placeholder.ts
new file mode 100644
index 000000000..c6167d78e
--- /dev/null
+++ b/web/frontend/src/components/secret-placeholder.ts
@@ -0,0 +1,16 @@
+export function maskedSecretPlaceholder(value: unknown, fallback = ""): string {
+ const secret = typeof value === "string" ? value.trim() : ""
+ if (!secret) {
+ return fallback
+ }
+
+ if (secret.length < 7) {
+ const first = secret[0]
+ const last = secret[secret.length - 1]
+ return `${first}***${last}`
+ }
+
+ const prefix = secret.slice(0, Math.min(3, secret.length))
+ const suffix = secret.slice(-Math.min(4, secret.length))
+ return `${prefix}***${suffix}`
+}
diff --git a/web/frontend/src/components/shared-form.tsx b/web/frontend/src/components/shared-form.tsx
new file mode 100644
index 000000000..a0d82cf15
--- /dev/null
+++ b/web/frontend/src/components/shared-form.tsx
@@ -0,0 +1,158 @@
+import { IconChevronDown, IconEye, IconEyeOff } from "@tabler/icons-react"
+import { type ReactNode, useState } from "react"
+import { useTranslation } from "react-i18next"
+
+import {
+ FieldDescription,
+ FieldLabel,
+ Field as UiField,
+} from "@/components/ui/field"
+import { Input } from "@/components/ui/input"
+import { Switch } from "@/components/ui/switch"
+
+interface FieldProps {
+ label: string
+ hint?: string
+ error?: string
+ required?: boolean
+ children: ReactNode
+}
+
+export function Field({ label, hint, error, required, children }: FieldProps) {
+ return (
+
+
+
+ {label}
+ {required && *}
+
+ {hint && (
+
+ {hint}
+
+ )}
+
+ {children}
+ {error && (
+
+ {error}
+
+ )}
+
+ )
+}
+
+interface KeyInputProps {
+ value: string
+ onChange: (v: string) => void
+ placeholder?: string
+}
+
+export function KeyInput({ value, onChange, placeholder }: KeyInputProps) {
+ const [show, setShow] = useState(false)
+
+ return (
+
+ onChange(e.target.value)}
+ placeholder={placeholder}
+ className="pr-10"
+ />
+
+
+ )
+}
+
+interface SwitchCardFieldProps {
+ label: string
+ hint?: string
+ error?: string
+ checked: boolean
+ onCheckedChange: (checked: boolean) => void
+ ariaLabel?: string
+ disabled?: boolean
+ children?: ReactNode
+}
+
+export function SwitchCardField({
+ label,
+ hint,
+ error,
+ checked,
+ onCheckedChange,
+ ariaLabel,
+ disabled,
+ children,
+}: SwitchCardFieldProps) {
+ return (
+
+
+
+
{label}
+ {hint && (
+
+ {hint}
+
+ )}
+
+
+
+ {children &&
{children}
}
+ {error && (
+
{error}
+ )}
+
+ )
+}
+
+interface AdvancedSectionProps {
+ children: ReactNode
+}
+
+export function AdvancedSection({ children }: AdvancedSectionProps) {
+ const { t } = useTranslation()
+ const [open, setOpen] = useState(false)
+
+ return (
+
+
+ {open && (
+
+ {children}
+
+ )}
+
+ )
+}
diff --git a/web/frontend/src/components/ui/alert-dialog.tsx b/web/frontend/src/components/ui/alert-dialog.tsx
new file mode 100644
index 000000000..d20de09a3
--- /dev/null
+++ b/web/frontend/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,197 @@
+import * as React from "react"
+import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+function AlertDialog({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function AlertDialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogContent({
+ className,
+ size = "default",
+ ...props
+}: React.ComponentProps & {
+ size?: "default" | "sm"
+}) {
+ return (
+
+
+
+
+ )
+}
+
+function AlertDialogHeader({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDialogFooter({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDialogMedia({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogAction({
+ className,
+ variant = "default",
+ size = "default",
+ ...props
+}: React.ComponentProps &
+ Pick, "variant" | "size">) {
+ return (
+
+ )
+}
+
+function AlertDialogCancel({
+ className,
+ variant = "outline",
+ size = "default",
+ ...props
+}: React.ComponentProps &
+ Pick, "variant" | "size">) {
+ return (
+
+ )
+}
+
+export {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogMedia,
+ AlertDialogOverlay,
+ AlertDialogPortal,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+}
diff --git a/web/frontend/src/components/ui/button.tsx b/web/frontend/src/components/ui/button.tsx
new file mode 100644
index 000000000..b9b702292
--- /dev/null
+++ b/web/frontend/src/components/ui/button.tsx
@@ -0,0 +1,67 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+import { Slot } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/80",
+ outline:
+ "border-border bg-background shadow-xs hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
+ ghost:
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
+ destructive:
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default:
+ "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
+ sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
+ lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
+ icon: "size-9",
+ "icon-xs":
+ "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
+ "icon-sm":
+ "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md",
+ "icon-lg": "size-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function Button({
+ className,
+ variant = "default",
+ size = "default",
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot.Root : "button"
+
+ return (
+
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/web/frontend/src/components/ui/card.tsx b/web/frontend/src/components/ui/card.tsx
new file mode 100644
index 000000000..13610b23d
--- /dev/null
+++ b/web/frontend/src/components/ui/card.tsx
@@ -0,0 +1,103 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Card({
+ className,
+ size = "default",
+ ...props
+}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
+ return (
+ img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/web/frontend/src/components/ui/collapsible.tsx b/web/frontend/src/components/ui/collapsible.tsx
new file mode 100644
index 000000000..63fc8eff3
--- /dev/null
+++ b/web/frontend/src/components/ui/collapsible.tsx
@@ -0,0 +1,31 @@
+import { Collapsible as CollapsiblePrimitive } from "radix-ui"
+
+function Collapsible({
+ ...props
+}: React.ComponentProps
) {
+ return
+}
+
+function CollapsibleTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CollapsibleContent({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent }
diff --git a/web/frontend/src/components/ui/dropdown-menu.tsx b/web/frontend/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 000000000..566e2c861
--- /dev/null
+++ b/web/frontend/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,269 @@
+"use client"
+
+import * as React from "react"
+import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+import { IconCheck, IconChevronRight } from "@tabler/icons-react"
+
+function DropdownMenu({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DropdownMenuPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuContent({
+ className,
+ align = "start",
+ sideOffset = 4,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function DropdownMenuGroup({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuItem({
+ className,
+ inset,
+ variant = "default",
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+ variant?: "default" | "destructive"
+}) {
+ return (
+
+ )
+}
+
+function DropdownMenuCheckboxItem({
+ className,
+ children,
+ checked,
+ inset,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function DropdownMenuRadioGroup({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuRadioItem({
+ className,
+ children,
+ inset,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function DropdownMenuLabel({
+ className,
+ inset,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ )
+}
+
+function DropdownMenuSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuShortcut({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+function DropdownMenuSub({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DropdownMenuSubTrigger({
+ className,
+ inset,
+ children,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ {children}
+
+
+ )
+}
+
+function DropdownMenuSubContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ DropdownMenu,
+ DropdownMenuPortal,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuLabel,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
+ DropdownMenuSubTrigger,
+ DropdownMenuSubContent,
+}
diff --git a/web/frontend/src/components/ui/field.tsx b/web/frontend/src/components/ui/field.tsx
new file mode 100644
index 000000000..7cf58a8b4
--- /dev/null
+++ b/web/frontend/src/components/ui/field.tsx
@@ -0,0 +1,236 @@
+import { useMemo } from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+import { Label } from "@/components/ui/label"
+import { Separator } from "@/components/ui/separator"
+
+function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
+ return (
+