commit f512bd58ec31c03335103f1c18a89a37bb88ac44 Author: leefer Date: Mon Aug 24 17:14:08 2026 +0800 chore: archive AgentDock v1 implementation diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1fde221 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.git +.env +.env.* +console/ +console-data/ +data/ +workspace/ +docs/ +proxy/ +tests/ +**/node_modules/ +**/dist/ +*.log diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1cf55eb --- /dev/null +++ b/.env.example @@ -0,0 +1,40 @@ +# Ubuntu host directory that contains all projects. +WORKSPACE_PATH=/vol1/1000/docker/ai-toools/workspace +AI_HOME_PATH=/vol1/1000/docker/ai-toools/data + +# Use `id -u` and `id -g` on the Ubuntu host. +AI_UID=1000 +AI_GID=1001 + +AI_CONTAINER_NAME=ai-tools +TZ=Asia/Shanghai + +# Web console. Generate long random values; do not reuse passwords. +CONSOLE_HTTPS_PORT=8443 +CONSOLE_DATA_PATH=/vol1/1000/docker/ai-toools/console-data +ADMIN_PASSWORD=replace-with-a-long-admin-password +RUNNER_TOKEN=replace-with-at-least-32-random-characters +CONFIG_ENCRYPTION_KEY=replace-with-at-least-32-random-characters + +AI_TOOLS_IMAGE_TAG=0.2.0 +CONSOLE_IMAGE_TAG=0.1.0 +AI_TOOLS_MEMORY_LIMIT=6g +AI_TOOLS_CPU_LIMIT=4.0 +CONSOLE_MEMORY_LIMIT=512m +CONSOLE_CPU_LIMIT=1.0 + +# Use `latest` for the first deployment. Pin exact versions after verification. +CODEX_VERSION=latest +CLAUDE_VERSION=latest +CODEBUDDY_VERSION=latest +KIMI_VERSION=latest +OPENCODE_VERSION=latest +QWEN_VERSION=latest +DSH_VERSION=latest +CC_SWITCH_VERSION=v5.10.2 +MULTICA_VERSION=v0.1.53 + +# Multica daemon starts automatically after `multica setup self-host` succeeds. +MULTICA_ENABLED=true +MULTICA_DAEMON_MAX_CONCURRENT_TASKS=2 +MULTICA_AGENT_RUNTIME_NAME=AgentDock Ubuntu Runtime diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..240f5db --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +* text=auto eol=lf +*.ps1 text eol=crlf +*.png binary +*.jpg binary +*.jpeg binary +*.woff binary +*.woff2 binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a7d8e5d --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.env +.env.server +data/ +workspace/ +console-data/ +**/node_modules/ +**/dist/ +*.log +*.crt +.agentdock-initial-password +.qa-data/ +proxy/certs/ +references/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..905e1b9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,123 @@ +FROM ubuntu:24.04 + +ARG DEBIAN_FRONTEND=noninteractive +ARG AI_UID=1000 +ARG AI_GID=1000 +ARG CODEX_VERSION=latest +ARG CLAUDE_VERSION=latest +ARG CODEBUDDY_VERSION=latest +ARG KIMI_VERSION=latest +ARG OPENCODE_VERSION=latest +ARG QWEN_VERSION=latest +ARG DSH_VERSION=latest +ARG CC_SWITCH_VERSION=v5.10.2 +ARG CC_SWITCH_SHA256_AMD64=8065c5bae9eda270747c1766cefbb2091d9625655dbf409ad7764eb47c0a8635 +ARG CC_SWITCH_SHA256_ARM64=b25c77f7eebbe3968c53022e1b5e703e324203e94e5c6379320bcd1bbe268e63 +ARG MULTICA_VERSION=v0.1.53 +ARG MULTICA_SHA256_AMD64=cfe60ff5cfee07e8147eb64fbaca46dac28162d84d03f1e07e4f87858d0fabb6 +ARG MULTICA_SHA256_ARM64=afb77e15d585c1d60a67a9583c20b5ff1ef4a1bacdcdd73c8e318fc9d254ad02 +ARG TARGETARCH + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash-completion \ + build-essential \ + ca-certificates \ + curl \ + git \ + gnupg \ + jq \ + less \ + locales \ + nano \ + openssh-client \ + pipx \ + procps \ + python3 \ + python3-pip \ + python3-venv \ + ripgrep \ + rsync \ + supervisor \ + tini \ + unzip \ + vim \ + wget \ + zip \ + && rm -rf /var/lib/apt/lists/* + +# Kimi Code requires Node.js 22.19.0 or later. NodeSource 22.x supplies it. +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get update \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* \ + && node -e 'const [major, minor] = process.versions.node.split(".").map(Number); if (major < 22 || (major === 22 && minor < 19)) process.exit(1)' + +RUN npm config set update-notifier false \ + && npm install --global \ + "@openai/codex@${CODEX_VERSION}" \ + "@anthropic-ai/claude-code@${CLAUDE_VERSION}" \ + "@tencent-ai/codebuddy-code@${CODEBUDDY_VERSION}" \ + "@moonshot-ai/kimi-code@${KIMI_VERSION}" \ + "opencode-ai@${OPENCODE_VERSION}" \ + "@qwen-code/qwen-code@${QWEN_VERSION}" \ + "@deepseek-ai/dsh@${DSH_VERSION}" \ + && npm cache clean --force + +RUN set -eux; \ + case "${TARGETARCH:-amd64}" in \ + amd64) cc_arch=x64; multica_arch=amd64; cc_sha="${CC_SWITCH_SHA256_AMD64}"; multica_sha="${MULTICA_SHA256_AMD64}" ;; \ + arm64) cc_arch=arm64; multica_arch=arm64; cc_sha="${CC_SWITCH_SHA256_ARM64}"; multica_sha="${MULTICA_SHA256_ARM64}" ;; \ + *) echo "Unsupported target architecture: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + cc_archive="/tmp/cc-switch.tar.gz"; \ + multica_archive="/tmp/multica.tar.gz"; \ + curl -fsSL -o "${cc_archive}" "https://github.com/SaladDay/cc-switch-cli/releases/download/${CC_SWITCH_VERSION}/cc-switch-cli-linux-${cc_arch}-musl.tar.gz"; \ + echo "${cc_sha} ${cc_archive}" | sha256sum -c -; \ + tar -xzf "${cc_archive}" -C /tmp cc-switch; \ + install -m 0755 /tmp/cc-switch /usr/local/bin/cc-switch; \ + curl -fsSL -o "${multica_archive}" "https://github.com/zimplemedia/multica-cli/releases/download/${MULTICA_VERSION}/multica_linux_${multica_arch}.tar.gz"; \ + echo "${multica_sha} ${multica_archive}" | sha256sum -c -; \ + tar -xzf "${multica_archive}" -C /tmp multica; \ + install -m 0755 /tmp/multica /usr/local/bin/multica; \ + rm -f "${cc_archive}" "${multica_archive}" /tmp/cc-switch /tmp/multica + +COPY runner/package.json runner/package-lock.json /opt/runner/ +RUN cd /opt/runner \ + && npm ci --omit=dev \ + && npm cache clean --force +COPY runner/*.js /opt/runner/ + +RUN existing_group="$(getent group "${AI_GID}" 2>/dev/null | cut -d: -f1 || true)" \ + && if [ -z "${existing_group}" ]; then groupadd --gid "${AI_GID}" ai; existing_group=ai; fi \ + && existing_user="$(getent passwd "${AI_UID}" 2>/dev/null | cut -d: -f1 || true)" \ + && if [ -n "${existing_user}" ]; then \ + usermod --login ai --home /home/ai --move-home --gid "${existing_group}" --shell /bin/bash "${existing_user}"; \ + else \ + useradd --uid "${AI_UID}" --gid "${existing_group}" --create-home --home-dir /home/ai --shell /bin/bash ai; \ + fi \ + && mkdir -p /workspace /home/ai/.ai-console/sessions \ + && chown -R "${AI_UID}:${AI_GID}" /workspace /home/ai /opt/runner + +COPY --chmod=755 scripts/container-check.sh /usr/local/bin/ai-tools-check +COPY --chmod=755 scripts/multica-runtime.sh /usr/local/bin/multica-runtime +COPY --chmod=755 scripts/multica-setup.sh /usr/local/bin/multica-setup +COPY scripts/supervisord.conf /etc/supervisor/conf.d/ai-runtime.conf + +ENV HOME=/home/ai \ + USER=ai \ + LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 \ + PATH=/home/ai/.local/bin:/home/ai/bin:${PATH} \ + PIPX_HOME=/home/ai/.local/pipx \ + PIPX_BIN_DIR=/home/ai/.local/bin + +USER ai +WORKDIR /workspace + +EXPOSE 4174 + +ENTRYPOINT ["/usr/bin/tini", "--"] +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/ai-runtime.conf"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..edc0822 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# Ubuntu AI CLI 工具箱 + +这套部署包在一台独立 Ubuntu 主机的 Docker 中统一运行以下工具: + +- Codex CLI +- Claude Code +- Tencent CodeBuddy Code +- Kimi Code CLI +- OpenCode +- Qwen Code +- DeepSeek Harness (`dsh`) +- CC Switch CLI +- Multica 客户端 Runtime 与 daemon + +OpenClaw 不在这个容器中安装,继续使用 Ubuntu 主机上现有的 OpenClaw。Cursor 桌面版继续运行在 Windows,通过 Remote SSH 编辑 Ubuntu 上的项目。 + +## AgentDock 网页控制台 + +本项目现在还提供一个仅限局域网使用的网页控制台: + +- 保留 OpenDesign `agent-console-v4.html` 的视觉、布局和设计 Token +- 单管理员登录,无公开注册 +- 浏览器内真实 PTY 终端,支持关闭浏览器后重新连接 +- 实时显示 CLI 版本、运行实例、CPU、内存、会话日志和 Git Diff +- 在设置页管理加密 API Key、默认模型和 DeepSeek Harness profile +- 使用 CC Switch 管理 Claude、Codex、OpenCode 的 Provider、MCP 和 Skills +- 连接现有 Multica 自托管服务端,让 Runtime 调度容器内 CLI +- Runner 只允许 7 个登记过的 CLI,项目目录只能位于 `/workspace` +- 不挂载 Docker Socket + +服务关系如下: + +```text +浏览器 -> ai-proxy (HTTPS + LAN 限制) -> ai-console -> ai-tools (Runner + Multica daemon + CLI) +``` + +## 最终效果 + +Windows 上可以直接运行: + +```powershell +.\scripts\remote-ai.ps1 -Server nas -Tool codex -Project my-project +``` + +AI CLI 实际在 Ubuntu 的 Docker 容器中运行,Windows Terminal 只负责显示和输入。项目文件位于 NAS 的 `/vol1/1000/docker/ai-toools/workspace`,在容器内对应 `/workspace`。 + +## 文档入口 + +1. [部署说明](docs/DEPLOYMENT.md) +2. [日常使用](docs/USAGE.md) +3. [升级和备份](docs/MAINTENANCE.md) +4. [常见故障](docs/TROUBLESHOOTING.md) +5. [官方资料来源](docs/SOURCES.md) +6. [192.168.200.36 实际部署记录](docs/SERVER-192.168.200.36.md) + +## 最短部署步骤 + +在 Ubuntu 主机上进入本目录,然后执行: + +```bash +cp .env.example .env +chmod +x scripts/ai +mkdir -p /vol1/1000/docker/ai-toools/workspace +docker compose build +docker compose up -d +docker compose exec ai-tools ai-tools-check +``` + +网页控制台还需要在 `.env` 设置 `ADMIN_PASSWORD`、`RUNNER_TOKEN` 和 +`CONFIG_ENCRYPTION_KEY`。完整步骤见 [网页控制台部署说明](docs/CONSOLE-DEPLOYMENT.md)。 + +首次使用某个工具时,需要在容器内完成它自己的登录: + +```bash +./scripts/ai codex . +./scripts/ai claude . +./scripts/ai kimi . +``` + +登录信息保存在部署目录的 `data` 中。重启或重建容器不会清除它,请不要把这个目录公开或提交到 Git。 diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..7b9093a --- /dev/null +++ b/compose.yaml @@ -0,0 +1,142 @@ +name: ai-toolbox + +services: + ai-tools: + image: local/ai-toolbox:${AI_TOOLS_IMAGE_TAG:-0.2.0} + container_name: ${AI_CONTAINER_NAME:-ai-tools} + build: + context: . + args: + AI_UID: ${AI_UID:-1000} + AI_GID: ${AI_GID:-1000} + CODEX_VERSION: ${CODEX_VERSION:-latest} + CLAUDE_VERSION: ${CLAUDE_VERSION:-latest} + CODEBUDDY_VERSION: ${CODEBUDDY_VERSION:-latest} + KIMI_VERSION: ${KIMI_VERSION:-latest} + OPENCODE_VERSION: ${OPENCODE_VERSION:-latest} + QWEN_VERSION: ${QWEN_VERSION:-latest} + DSH_VERSION: ${DSH_VERSION:-latest} + CC_SWITCH_VERSION: ${CC_SWITCH_VERSION:-v5.10.2} + MULTICA_VERSION: ${MULTICA_VERSION:-v0.1.53} + tty: true + stdin_open: true + restart: unless-stopped + working_dir: /workspace + environment: + TZ: ${TZ:-Asia/Shanghai} + TERM: xterm-256color + COLORTERM: truecolor + RUNNER_PORT: 4174 + RUNNER_TOKEN: ${RUNNER_TOKEN:?RUNNER_TOKEN is required} + WORKSPACE_ROOT: /workspace + SESSION_DIRECTORY: /home/ai/.ai-console/sessions + MULTICA_ENABLED: ${MULTICA_ENABLED:-true} + MULTICA_DAEMON_MAX_CONCURRENT_TASKS: ${MULTICA_DAEMON_MAX_CONCURRENT_TASKS:-2} + MULTICA_AGENT_RUNTIME_NAME: ${MULTICA_AGENT_RUNTIME_NAME:-AgentDock Ubuntu Runtime} + volumes: + - type: bind + source: ${WORKSPACE_PATH:-/srv/projects} + target: /workspace + - type: bind + source: ${AI_HOME_PATH:-./data} + target: /home/ai + networks: + - runner + - egress + security_opt: + - no-new-privileges:true + pids_limit: 2048 + mem_limit: ${AI_TOOLS_MEMORY_LIMIT:-6g} + cpus: ${AI_TOOLS_CPU_LIMIT:-4.0} + stop_grace_period: 30s + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:4174/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + + ai-console: + image: local/agentdock-console:${CONSOLE_IMAGE_TAG:-0.1.0} + build: + context: ./console + dockerfile: Dockerfile + restart: unless-stopped + environment: + TZ: ${TZ:-Asia/Shanghai} + CONSOLE_PORT: 4173 + RUNNER_URL: http://ai-tools:4174 + RUNNER_TOKEN: ${RUNNER_TOKEN:?RUNNER_TOKEN is required} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD is required} + CONFIG_ENCRYPTION_KEY: ${CONFIG_ENCRYPTION_KEY:?CONFIG_ENCRYPTION_KEY is required} + CONSOLE_DATA_DIRECTORY: /data + volumes: + - type: bind + source: ${CONSOLE_DATA_PATH:-./console-data} + target: /data + networks: + - frontend + - runner + depends_on: + ai-tools: + condition: service_healthy + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + pids_limit: 256 + mem_limit: ${CONSOLE_MEMORY_LIMIT:-512m} + cpus: ${CONSOLE_CPU_LIMIT:-1.0} + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:4173/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + + ai-proxy: + image: caddy:2.10.2-alpine + restart: unless-stopped + ports: + - "192.168.200.36:${CONSOLE_HTTPS_PORT:-8443}:443" + volumes: + - ./proxy/Caddyfile:/etc/caddy/Caddyfile:ro + - ./proxy/certs:/etc/caddy/certs:ro + - caddy_data:/data + - caddy_config:/config + networks: + - frontend + - edge + depends_on: + ai-console: + condition: service_healthy + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + cap_add: + - NET_BIND_SERVICE + read_only: true + tmpfs: + - /tmp + - /config/caddy + mem_limit: 256m + cpus: 0.5 + healthcheck: + test: ["CMD", "wget", "--no-check-certificate", "-qO-", "https://127.0.0.1/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + +networks: + frontend: + internal: true + runner: + internal: true + egress: + edge: + +volumes: + caddy_data: + caddy_config: diff --git a/console/.dockerignore b/console/.dockerignore new file mode 100644 index 0000000..3910563 --- /dev/null +++ b/console/.dockerignore @@ -0,0 +1,4 @@ +node_modules +dist +*.log +.env* diff --git a/console/Dockerfile b/console/Dockerfile new file mode 100644 index 0000000..b6bf4e6 --- /dev/null +++ b/console/Dockerfile @@ -0,0 +1,26 @@ +FROM node:22.22.0-alpine3.22 AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +FROM node:22.22.0-alpine3.22 AS build +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npm run build \ + && npm prune --omit=dev + +FROM node:22.22.0-alpine3.22 AS production +WORKDIR /app +ENV NODE_ENV=production +RUN addgroup -g 1001 -S agentdock \ + && adduser -S agentdock -u 1001 -G agentdock +COPY --from=build --chown=agentdock:agentdock /app/package.json ./package.json +COPY --from=build --chown=agentdock:agentdock /app/node_modules ./node_modules +COPY --from=build --chown=agentdock:agentdock /app/dist ./dist +COPY --from=build --chown=agentdock:agentdock /app/server ./server +USER agentdock +EXPOSE 4173 +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ + CMD wget -qO- http://127.0.0.1:4173/health >/dev/null || exit 1 +CMD ["node", "server/index.js"] diff --git a/console/index.html b/console/index.html new file mode 100644 index 0000000..3c22a06 --- /dev/null +++ b/console/index.html @@ -0,0 +1,13 @@ + + + + + + + AgentDock - AI CLI 控制台 + + +
+ + + diff --git a/console/package-lock.json b/console/package-lock.json new file mode 100644 index 0000000..4a781bd --- /dev/null +++ b/console/package-lock.json @@ -0,0 +1,2754 @@ +{ + "name": "agentdock-console", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agentdock-console", + "version": "0.1.0", + "dependencies": { + "@fontsource/inter": "^5.2.6", + "@fontsource/jetbrains-mono": "^5.2.6", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", + "express": "^5.1.0", + "lucide-react": "^0.468.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "ws": "^8.18.3" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.4", + "vite": "^6.0.7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fontsource/inter": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-RofMylZmjlJEfELXeNHFWBRcSs75rGU/6bV2S2jfnvv/3rPXPGe0LgUJTklcHZ9lM4OZmAVFhcJPnACfb91A3g==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/jetbrains-mono": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz", + "integrity": "sha512-fqDfB5I9f1p1TV486aUgB9t8zP84P0O1FtQR5Ol9vjwPy+S+EIGlVYm1cvj2W5shcZMTg2nZFdVMoH5wFu8a1A==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.5.tgz", + "integrity": "sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.5.tgz", + "integrity": "sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.5.tgz", + "integrity": "sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.5.tgz", + "integrity": "sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.5.tgz", + "integrity": "sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.5.tgz", + "integrity": "sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.5.tgz", + "integrity": "sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.5.tgz", + "integrity": "sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.5.tgz", + "integrity": "sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.5.tgz", + "integrity": "sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.5.tgz", + "integrity": "sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.5.tgz", + "integrity": "sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.5.tgz", + "integrity": "sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.5.tgz", + "integrity": "sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.5.tgz", + "integrity": "sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.5.tgz", + "integrity": "sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.5.tgz", + "integrity": "sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.5.tgz", + "integrity": "sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.5.tgz", + "integrity": "sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.5.tgz", + "integrity": "sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.5.tgz", + "integrity": "sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.5.tgz", + "integrity": "sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.5.tgz", + "integrity": "sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.5.tgz", + "integrity": "sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.5.tgz", + "integrity": "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@xterm/addon-fit": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", + "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.18.tgz", + "integrity": "sha512-1iEmLEYSiE1SeBoAfPo/Mnx3PzfzHUkDK61ASkCpuk3YXugYLH5DYK1SzqV55F8FMI6s0F+/tCP7Polz1QRjxw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.412", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.412.tgz", + "integrity": "sha512-z4rMe3esBzlzovKHj4gxJnsCGZRK5l4baUvm+gCGJBPE+gsyUMKsuU9tnEUtI1dOebXz1ytAPGjvXhmQ7rIPwA==", + "dev": true, + "license": "ISC" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.468.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.5.tgz", + "integrity": "sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.5", + "@rollup/rollup-android-arm64": "4.62.5", + "@rollup/rollup-darwin-arm64": "4.62.5", + "@rollup/rollup-darwin-x64": "4.62.5", + "@rollup/rollup-freebsd-arm64": "4.62.5", + "@rollup/rollup-freebsd-x64": "4.62.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.5", + "@rollup/rollup-linux-arm-musleabihf": "4.62.5", + "@rollup/rollup-linux-arm64-gnu": "4.62.5", + "@rollup/rollup-linux-arm64-musl": "4.62.5", + "@rollup/rollup-linux-loong64-gnu": "4.62.5", + "@rollup/rollup-linux-loong64-musl": "4.62.5", + "@rollup/rollup-linux-ppc64-gnu": "4.62.5", + "@rollup/rollup-linux-ppc64-musl": "4.62.5", + "@rollup/rollup-linux-riscv64-gnu": "4.62.5", + "@rollup/rollup-linux-riscv64-musl": "4.62.5", + "@rollup/rollup-linux-s390x-gnu": "4.62.5", + "@rollup/rollup-linux-x64-gnu": "4.62.5", + "@rollup/rollup-linux-x64-musl": "4.62.5", + "@rollup/rollup-openbsd-x64": "4.62.5", + "@rollup/rollup-openharmony-arm64": "4.62.5", + "@rollup/rollup-win32-arm64-msvc": "4.62.5", + "@rollup/rollup-win32-ia32-msvc": "4.62.5", + "@rollup/rollup-win32-x64-gnu": "4.62.5", + "@rollup/rollup-win32-x64-msvc": "4.62.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "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 + } + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/console/package.json b/console/package.json new file mode 100644 index 0000000..e6a2eb1 --- /dev/null +++ b/console/package.json @@ -0,0 +1,30 @@ +{ + "name": "agentdock-console", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "predev": "node scripts/extract-design-css.mjs", + "dev": "vite --host 0.0.0.0", + "dev:server": "node --watch server/index.js", + "prebuild": "node scripts/extract-design-css.mjs", + "build": "vite build --configLoader runner", + "start": "node server/index.js", + "test": "node --test" + }, + "dependencies": { + "@fontsource/inter": "^5.2.6", + "@fontsource/jetbrains-mono": "^5.2.6", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", + "express": "^5.1.0", + "lucide-react": "^0.468.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "ws": "^8.18.3" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.4", + "vite": "^6.0.7" + } +} diff --git a/console/scripts/extract-design-css.mjs b/console/scripts/extract-design-css.mjs new file mode 100644 index 0000000..beceb61 --- /dev/null +++ b/console/scripts/extract-design-css.mjs @@ -0,0 +1,11 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const directory = path.dirname(fileURLToPath(import.meta.url)); +const sourcePath = path.resolve(directory, "../src/design/agent-console-v4.html"); +const targetPath = path.resolve(directory, "../src/design/prototype.css"); +const source = await fs.readFile(sourcePath, "utf8"); +const match = source.match(/ + + +
+ + + + +
+
+

工作台

+ agentdock / 焦点会话 + + + + + +
+ +
+ +
+
+
+
+
+
+ + +
+
+
+ + + + + + + +
+
+
+
+
+ + +
+
+ +
+
+
+ + + + +
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+ +
+
+ + +
+
+
+
+

终端行为

+
+ 自动跟随钉底新输出到达时保持滚动到底部 + +
+
+ 模拟实时流向焦点终端追加演示日志行 + +
+
+
+

界面效果

+
+ 扫描线纹理在主区叠加极弱扫描线(彩蛋,默认关闭) + +
+
+
+

快捷键

+ + + + + + +
命令面板⌘K / Ctrl K
会话 Tab 紧凑 / 舒适⌘B / Ctrl B
快捷键速查?
切换视图14
关闭浮层Esc
+
+
+

关于

+
版本v4 高保真原型 · 会话 Tab 布局0.4.0
+
数据全部为本页内置演示数据未连接容器
+
目标架构SPA ↔ WebSocket 网关 ↔ docker exec / PTY
+
+
+
+
+
+ +
+ + +
+ 已连接 docker + —/7 运行 + + ⌘K 命令 + ⌘B 密度 + ? 快捷键 + +
+
+ + +
+ + +
+ + + + diff --git a/console/src/design/prototype.css b/console/src/design/prototype.css new file mode 100644 index 0000000..07fc772 --- /dev/null +++ b/console/src/design/prototype.css @@ -0,0 +1,401 @@ +/* ─── tokens ────────────────────────────────────────────────────── */ + :root { + color-scheme: dark; + /* 三层表面明度递进(Tokyo Night 低饱和蓝黑,不用纯黑) */ + --bg: #0d1117; /* 底 */ + --surface: #141a24; /* 面板 */ + --overlay: #1b2330; /* 浮层 */ + --fg: #e2e8f2; + --muted: #8b95a9; + --border: rgb(255 255 255 / .06); /* hairline */ + --accent: oklch(0.8 0.13 195); /* 电光青:唯一品牌强调色 */ + + /* 状态语义色:只用于 ~10px 圆点与呼吸边框 */ + --ok: oklch(0.76 0.15 150); + --warn: oklch(0.8 0.13 85); + --err: oklch(0.7 0.17 25); + + /* 派生 */ + --accent-ink: oklch(0.16 0.02 240); + --accent-soft: color-mix(in oklch, var(--accent) 26%, transparent); + --accent-faint: color-mix(in oklch, var(--accent) 10%, transparent); + --hover: rgb(255 255 255 / .05); + --hover-strong: rgb(255 255 255 / .09); + --term-bg: #0a0e14; + --glass: color-mix(in srgb, var(--overlay) 78%, transparent); + + /* 光影分层:柔和多层投影 + 顶部 1px 高光描边(材质厚度感) */ + --shadow-pop: 0 1px 2px rgb(0 0 0 / .32), 0 10px 28px rgb(0 0 0 / .42); + --top-hi: inset 0 1px 0 rgb(255 255 255 / .04); + + --font-display: "Inter", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; + --font-body: "Inter", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; + --font-mono: "JetBrains Mono", "SF Mono", ui-monospace, Menlo, monospace; + + /* 动效 token:三档时长 + 缓动(spring 只给低频仪式时刻) */ + --dur-fast: 100ms; /* hover */ + --dur-med: 200ms; /* 面板 / Tab / 视图切换 */ + --dur-slow: 400ms; /* 高亮淡出 / 数字跳变 / toast 入场 */ + --ease-in: cubic-bezier(0, 0, 0.2, 1); + --ease-out: cubic-bezier(0.4, 0, 1, 1); + --ease-move: cubic-bezier(0.4, 0, 0.2, 1); + --ease-spring: cubic-bezier(.34, 1.32, .42, 1); /* overshoot 克制 */ + + --rail-w: 48px; --status-h: 28px; --top-h: 44px; + --radius: 8px; --radius-lg: 12px; + } + + /* ─── reset & base ──────────────────────────────────────────────── */ + *, *::before, *::after { box-sizing: border-box; } + html, body { height: 100%; } + body { + margin: 0; background: var(--bg); color: var(--fg); + font-family: var(--font-body); font-size: 14px; line-height: 1.55; + text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; + overflow: hidden; + } + svg { display: block; } + button { font: inherit; cursor: pointer; color: inherit; background: none; border: 0; padding: 0; } + [hidden] { display: none !important; } + h1, h2, h3 { font-family: var(--font-display); margin: 0; text-wrap: balance; } + :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; } + ::selection { background: var(--accent-soft); } + .num, .mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; } + .meta { font-family: var(--font-mono); font-size: 12px; color: var(--muted); } + /* 键盘触发的动作零动画(选中高亮瞬时切换) */ + html.kbd *, html.kbd *::before, html.kbd *::after { transition-duration: 0s !important; animation-duration: 0s !important; } + + /* ─── app shell:活动栏 / 主区 + 底部状态栏(v4 无侧栏)───────── */ + .app { display: grid; height: 100vh; grid-template-rows: 1fr var(--status-h); grid-template-columns: var(--rail-w) 1fr; } + .statusbar { grid-column: 1 / -1; } + + /* 活动栏 */ + .rail { background: var(--bg); border-right: 1px solid var(--border); display: flex; flex-direction: column; align-items: center; padding: 8px 0; gap: 4px; z-index: 5; } + .rail .logo { width: 30px; height: 30px; margin-bottom: 10px; display: grid; place-items: center; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); box-shadow: var(--top-hi); color: var(--fg); } + .rail-btn { position: relative; width: 36px; height: 36px; display: grid; place-items: center; border-radius: 8px; color: var(--muted); transition: background var(--dur-fast) var(--ease-move), color var(--dur-fast) var(--ease-move); } + .rail-btn:hover { background: var(--hover); color: var(--fg); } + .rail-btn[aria-current="true"] { color: var(--fg); background: var(--hover); } + .rail-btn[aria-current="true"]::before { content: ''; position: absolute; left: -6px; top: 8px; bottom: 8px; width: 2px; border-radius: 2px; background: var(--accent); } + .rail-btn::after { + content: attr(data-tip); position: absolute; left: calc(100% + 12px); top: 50%; translate: 0 -50%; + background: var(--glass); backdrop-filter: blur(12px); border: 1px solid var(--border); box-shadow: var(--shadow-pop); + color: var(--fg); font-size: 12px; white-space: nowrap; padding: 5px 10px; border-radius: 6px; + opacity: 0; pointer-events: none; transition: opacity var(--dur-fast) var(--ease-in); z-index: 60; + } + .rail-btn:hover::after { opacity: 1; } + .rail .spacer { flex: 1; } + /* 「等待输入」注意力角标:挂在工作台图标上 */ + .rail-badge { position: absolute; top: 1px; right: -1px; min-width: 15px; height: 15px; padding: 0 4px; border-radius: 999px; background: var(--warn); color: var(--accent-ink); font-family: var(--font-mono); font-size: 9.5px; font-weight: 600; display: grid; place-items: center; } + + /* 状态点:颜色 + 形状双编码;running 呼吸点带辉光(特效三处之一) */ + .dot { width: 9px; height: 9px; border-radius: 50%; flex: none; background: var(--muted); } + .dot.run { background: var(--ok); animation: breathe-dot 2s cubic-bezier(.4, 0, .6, 1) infinite; } + .dot.wait { background: var(--warn); border-radius: 2px; } + .dot.err { background: var(--err); border-radius: 2px; transform: rotate(45deg); } + .dot.off { background: transparent; border: 1.5px solid var(--muted); } + @keyframes breathe-dot { + 0%, 100% { box-shadow: 0 0 8px color-mix(in oklch, var(--ok) 45%, transparent), 0 0 0 0 color-mix(in oklch, var(--ok) 40%, transparent); opacity: 1; } + 50% { box-shadow: 0 0 4px color-mix(in oklch, var(--ok) 28%, transparent), 0 0 0 4px transparent; opacity: .72; } + } + + /* waiting:唯一脉冲元素 = claude 会话 Tab 的 1px 呼吸边框;其余等待者降级为常亮琥珀徽标 */ + .sess-tab.waiting { border-color: color-mix(in oklch, var(--warn) 45%, transparent); animation: breathe-border 2s cubic-bezier(.4, 0, .6, 1) infinite; } + @keyframes breathe-border { 0%, 100% { border-color: color-mix(in oklch, var(--warn) 45%, transparent); } 50% { border-color: color-mix(in oklch, var(--warn) 12%, transparent); } } + .sess-tab.shake { animation: shake .4s var(--ease-move) 1; } + @keyframes shake { 25% { transform: translateX(-3px); } 50% { transform: translateX(3px); } 75% { transform: translateX(-2px); } } + + /* 主区:环境纹理(网格 ≤4%)+ 顶部单道青色环境光晕(全页唯一大面积光效) */ + .main { position: relative; min-width: 0; min-height: 0; display: flex; flex-direction: column; background: var(--bg); } + .main::before { content: ''; position: absolute; inset: 0 0 auto; height: 320px; pointer-events: none; background: radial-gradient(60% 100% at 50% 0%, color-mix(in oklch, var(--accent) 6%, transparent), transparent 75%); } + .main::after { + content: ''; position: absolute; inset: 0; pointer-events: none; + background-image: linear-gradient(rgb(255 255 255 / .028) 1px, transparent 1px), linear-gradient(90deg, rgb(255 255 255 / .028) 1px, transparent 1px); + background-size: 30px 30px; + mask-image: linear-gradient(180deg, black, transparent 62%); + } + + /* 顶部上下文栏 */ + .topbar { position: relative; height: var(--top-h); flex: none; display: flex; align-items: center; gap: 14px; padding: 0 16px; border-bottom: 1px solid var(--border); background: var(--surface); box-shadow: var(--top-hi); z-index: 4; } + .topbar h1 { font-size: 14px; font-weight: 600; letter-spacing: -0.01em; } + .topbar .crumb { font-family: var(--font-mono); font-size: 11.5px; color: var(--muted); } + .env-sel { position: relative; margin-left: auto; } + .env-sel select { appearance: none; background: var(--overlay); color: var(--fg); border: 1px solid var(--border); border-radius: 7px; font-family: var(--font-mono); font-size: 12px; padding: 5px 28px 5px 10px; transition: border-color var(--dur-fast) var(--ease-move); } + .env-sel select:hover { border-color: var(--hover-strong); } + .env-sel::after { content: '▾'; position: absolute; right: 10px; top: 50%; translate: 0 -50%; color: var(--muted); font-size: 11px; pointer-events: none; } + /* 注意力药丸:点击跳到第一个等待中的智能体 */ + .attn-pill { display: inline-flex; align-items: center; gap: 7px; padding: 5px 12px; border-radius: 999px; border: 1px solid color-mix(in oklch, var(--warn) 42%, transparent); background: var(--glass); backdrop-filter: blur(12px); color: var(--warn); font-family: var(--font-mono); font-size: 12px; transition: background var(--dur-fast) var(--ease-move), transform var(--dur-fast) var(--ease-move); } + .attn-pill:hover { background: color-mix(in oklch, var(--warn) 10%, transparent); transform: translateY(-1px); } + .kbtn { display: inline-flex; align-items: center; gap: 8px; background: var(--overlay); border: 1px solid var(--border); border-radius: 7px; padding: 5px 10px; color: var(--muted); font-size: 12.5px; transition: background var(--dur-fast) var(--ease-move); } + .kbtn:hover { background: var(--hover); color: var(--fg); } + kbd { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px; background: var(--bg); } + + /* 视图容器(叠在环境纹理之上) */ + .views { position: relative; z-index: 1; flex: 1; min-height: 0; } + .view { display: none; height: 100%; min-height: 0; } + .view.active { display: flex; flex-direction: column; } + /* 入场 fade + 6px 上移,瞬态类:稳态下视图不带 animation,任何截图时机都能捕获到内容 */ + .view.entering { animation: view-in var(--dur-med) var(--ease-in); } + @keyframes view-in { from { opacity: 0; transform: translateY(6px); } } + + /* 列表/表格行入场:交错淡入上移(瞬态类,只首次进入视图时挂) */ + .stag-in { animation: stag-in var(--dur-slow) var(--ease-in) both; animation-delay: calc(var(--si, 0) * 36ms); } + @keyframes stag-in { from { opacity: 0; transform: translateY(8px); } } + + /* ─── 总览 ──────────────────────────────────────────────────────── */ + .ov-scroll { flex: 1; overflow-y: auto; padding: 22px 24px 40px; scrollbar-width: thin; scrollbar-color: var(--border) transparent; } + .stat-strip { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; } + .stat-tile { background: var(--surface); box-shadow: var(--top-hi); padding: 15px 18px 13px; } + .stat-tile .v { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 25px; font-weight: 600; line-height: 1.15; letter-spacing: -0.02em; } + /* 数字入场:400ms easeOut 轻微上浮(booted 后触发,防截图捕获空白首帧) */ + html.booted .stat-tile .v { animation: num-in var(--dur-slow) var(--ease-out) both; } + @keyframes num-in { from { opacity: .3; transform: translateY(4px); } } + .stat-tile .l { font-size: 12.5px; color: var(--muted); margin-top: 2px; } + .stat-tile .sub { font-family: var(--font-mono); font-size: 11px; color: var(--muted); margin-top: 3px; opacity: .85; } + .ov-grid { display: grid; gap: 12px; margin-top: 16px; grid-template-columns: repeat(auto-fill, minmax(min(288px, 100%), 1fr)); } + .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--top-hi); padding: 15px 16px; cursor: pointer; display: flex; flex-direction: column; gap: 11px; transition: background var(--dur-fast) var(--ease-move), border-color var(--dur-fast) var(--ease-move), transform var(--dur-fast) var(--ease-move); } + .card:hover { background: var(--overlay); border-color: var(--hover-strong); transform: translateY(-2px); } + .card:active { transform: scale(.97); } + .card-head { display: flex; align-items: center; gap: 10px; } + .mono-badge { width: 32px; height: 32px; flex: none; display: grid; place-items: center; border: 1px solid var(--border); border-radius: 7px; background: var(--bg); font-family: var(--font-mono); font-size: 11px; font-weight: 600; } + .card-head strong { font-size: 14px; display: block; line-height: 1.25; } + .card-head .meta { font-size: 11px; } + .card-status { margin-left: auto; display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-mono); font-size: 11px; color: var(--muted); } + .st-badge { flex: none; font-size: 10.5px; font-family: var(--font-mono); color: var(--warn); border: 1px solid color-mix(in oklch, var(--warn) 40%, transparent); border-radius: 999px; padding: 1px 7px; } + .kv { display: grid; grid-template-columns: 52px minmax(0, 1fr); gap: 4px 12px; margin: 0; } + .kv dt { color: var(--muted); font-family: var(--font-mono); font-size: 10.5px; padding-top: 1.5px; } + .kv dd { margin: 0; font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 12px; color: var(--fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + + /* ─── 工作台 ────────────────────────────────────────────────────── */ + /* 会话 Tab 条:编辑器式并行会话隐喻,横向滚动,毛玻璃粘在顶部 */ + .sess-tabs { flex: none; display: flex; align-items: stretch; gap: 4px; padding: 8px 12px 0; overflow-x: auto; scrollbar-width: none; background: var(--glass); backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); } + .sess-tabs::-webkit-scrollbar { display: none; } + .sess-tab { position: relative; display: flex; align-items: center; gap: 8px; padding: 7px 12px; margin-bottom: -1px; min-width: 0; flex: none; border: 1px solid transparent; border-radius: 9px 9px 0 0; color: var(--muted); transition: background var(--dur-fast) var(--ease-move), border-color var(--dur-fast) var(--ease-move), color var(--dur-fast) var(--ease-move); } + .sess-tab:hover { background: var(--hover); color: var(--fg); } + /* 当前会话 Tab:高亮 + 辉光(特效三处之二,blur 12px / opacity ≤.5) */ + .sess-tab[aria-current="true"] { background: var(--overlay); border-color: var(--border); border-bottom-color: var(--overlay); box-shadow: var(--top-hi), 0 0 12px color-mix(in oklch, var(--accent) 22%, transparent); color: var(--fg); } + .st-main { min-width: 0; text-align: left; } + .st-name { display: block; font-size: 12.5px; font-weight: 500; line-height: 1.25; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .st-sub { display: block; font-family: var(--font-mono); font-size: 10px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + /* ⌘B:紧凑 / 舒适密度 */ + .sess-tabs.compact { padding-top: 5px; } + .sess-tabs.compact .sess-tab { padding: 5px 10px; gap: 6px; } + .sess-tabs.compact .st-sub { display: none; } + + .wb-bar { flex: none; display: flex; align-items: center; gap: 6px; padding: 10px 16px 0; } + .wb-tab { padding: 7px 13px; border-radius: 7px; font-size: 13px; color: var(--muted); border: 1px solid transparent; transition: background var(--dur-fast) var(--ease-move), color var(--dur-fast) var(--ease-move); } + .wb-tab:hover { background: var(--hover); color: var(--fg); } + .wb-tab[aria-current="true"] { color: var(--fg); background: var(--overlay); border-color: var(--border); box-shadow: var(--top-hi); font-weight: 500; } + .seg { margin-left: auto; display: inline-flex; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 2px; } + .seg button { display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px; border-radius: 6px; font-size: 12.5px; color: var(--muted); transition: background var(--dur-fast) var(--ease-move), color var(--dur-fast) var(--ease-move); } + .seg button:hover { color: var(--fg); } + .seg button[aria-current="true"] { background: var(--overlay); color: var(--fg); box-shadow: var(--top-hi); } + .wb-body { flex: 1; min-height: 0; padding: 12px 16px 16px; display: flex; flex-direction: column; } + .pane { display: none; flex: 1; min-height: 0; flex-direction: column; } + .pane.active { display: flex; } + + /* 终端面板:浮层材质 = 多层投影 + 顶部高光 */ + .term-panel { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; background: var(--term-bg); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-pop), var(--top-hi); overflow: hidden; } + .term-head { flex: none; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 14px; border-bottom: 1px solid var(--border); font-family: var(--font-mono); font-size: 11.5px; color: var(--muted); background: var(--surface); box-shadow: var(--top-hi); } + .term-head .l { display: inline-flex; align-items: center; gap: 8px; min-width: 0; } + .term-head .l span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .term-head .r { color: var(--fg); flex: none; } + .term { flex: 1; overflow-y: auto; padding: 14px 16px 16px; scrollbar-width: thin; scrollbar-color: var(--border) transparent; } + .tl { font-family: var(--font-mono); font-size: 12.5px; line-height: 1.75; white-space: pre-wrap; word-break: break-word; color: var(--fg); } + .tl.dim { color: var(--muted); } + .tl.ev { color: var(--muted); } .tl.ev .c { color: var(--fg); } + .tl.ok { color: color-mix(in oklch, var(--ok) 70%, var(--fg)); } + .tl.err { color: color-mix(in oklch, var(--err) 70%, var(--fg)); } + .tl.user { font-weight: 600; } .tl.user .mark { color: var(--ok); margin-right: 8px; } + /* 事件级变化:单次 1.8s yellow-fade 背景高亮淡出(不循环,只给最新一行) */ + .tl.flash { animation: flash 1.8s var(--ease-out) 1; } + @keyframes flash { 0% { background: color-mix(in oklch, var(--warn) 15%, transparent); } 100% { background: transparent; } } + .caret { display: inline-block; width: 8px; height: 14px; vertical-align: -2px; background: var(--accent); animation: blink 1.1s steps(1) infinite; } + @keyframes blink { 50% { opacity: 0; } } + .wait-box { margin: 6px 0; padding: 10px 14px; border: 1px solid color-mix(in oklch, var(--warn) 40%, transparent); border-radius: var(--radius); background: color-mix(in oklch, var(--warn) 7%, transparent); font-family: var(--font-mono); font-size: 12.5px; color: var(--fg); } + .wait-box .meta { display: block; margin-top: 3px; } + /* 「↓ N 条新输出」毛玻璃药丸 */ + .new-pill { position: absolute; right: 16px; bottom: 14px; display: inline-flex; align-items: center; gap: 7px; padding: 7px 13px; border-radius: 999px; border: 1px solid var(--border); background: var(--glass); backdrop-filter: blur(12px); box-shadow: var(--shadow-pop); color: var(--fg); font-family: var(--font-mono); font-size: 12px; opacity: 0; pointer-events: none; transform: translateY(6px); transition: opacity var(--dur-med) var(--ease-in), transform var(--dur-med) var(--ease-in); } + .new-pill.show { opacity: 1; pointer-events: auto; transform: none; } + .new-pill:hover { background: var(--overlay); } + + /* composer */ + .chips { flex: none; display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0 8px; } + .chip { border: 1px solid var(--border); border-radius: 999px; padding: 6px 13px; font-size: 12px; color: var(--muted); background: var(--surface); transition: background var(--dur-fast) var(--ease-move), color var(--dur-fast) var(--ease-move); } + .chip:hover { background: var(--hover); color: var(--fg); } + .composer { flex: none; display: flex; gap: 10px; align-items: stretch; } + .input { min-width: 0; min-height: 40px; padding: 9px 13px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--fg); font: inherit; font-size: 13.5px; transition: border-color var(--dur-fast) var(--ease-move); } + .input::placeholder { color: var(--muted); } + .input:focus { outline: none; border-color: color-mix(in oklch, var(--accent) 55%, var(--border)); } + .composer .input[type="text"] { flex: 1; } + .sel { position: relative; flex: none; } + .sel::after { content: '▾'; position: absolute; right: 11px; top: 50%; translate: 0 -50%; pointer-events: none; color: var(--muted); font-size: 11px; } + .sel select { appearance: none; padding-right: 30px; height: 100%; font-family: var(--font-mono); font-size: 12px; } + .sel select option { background: var(--overlay); color: var(--fg); } + .btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 9px 16px; min-height: 40px; border-radius: var(--radius); border: 1px solid transparent; font-size: 13.5px; font-weight: 500; transition: transform var(--dur-fast) var(--ease-move), background var(--dur-fast) var(--ease-move), border-color var(--dur-fast) var(--ease-move); } + .btn:active { transform: scale(.97); } + .btn[disabled] { opacity: .5; cursor: not-allowed; } + /* 主按钮辉光(特效三处之三) */ + .btn-primary { background: var(--accent); color: var(--accent-ink); box-shadow: 0 0 12px color-mix(in oklch, var(--accent) 35%, transparent); } + .btn-primary:hover { background: color-mix(in oklch, var(--accent) 88%, white); } + .btn-secondary { background: var(--overlay); color: var(--fg); border-color: var(--border); box-shadow: var(--top-hi); } + .btn-secondary:hover { background: var(--hover-strong); } + .btn-danger { background: transparent; color: color-mix(in oklch, var(--err) 70%, var(--fg)); border-color: color-mix(in oklch, var(--err) 40%, var(--border)); } + .btn-danger:hover { background: color-mix(in oklch, var(--err) 10%, transparent); } + .btn-sm { padding: 6px 12px; min-height: 32px; font-size: 12.5px; } + + /* Diff 视图:绿/红只给 diff 行内小面积 */ + .diff-panel { flex: 1; min-height: 0; overflow-y: auto; background: var(--term-bg); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-pop), var(--top-hi); scrollbar-width: thin; scrollbar-color: var(--border) transparent; } + .diff-file { padding: 9px 16px; border-bottom: 1px solid var(--border); font-family: var(--font-mono); font-size: 12px; color: var(--fg); background: var(--surface); box-shadow: var(--top-hi); display: flex; justify-content: space-between; } + .dl { display: flex; font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 12px; line-height: 1.7; white-space: pre; } + .dl .ln { flex: none; width: 76px; text-align: right; padding-right: 14px; color: var(--muted); opacity: .6; user-select: none; } + .dl .lc { flex: 1; padding-right: 16px; overflow: hidden; text-overflow: ellipsis; } + .dl.add { background: color-mix(in oklch, var(--ok) 8%, transparent); } .dl.add .lc { color: color-mix(in oklch, var(--ok) 65%, var(--fg)); } + .dl.del { background: color-mix(in oklch, var(--err) 8%, transparent); } .dl.del .lc { color: color-mix(in oklch, var(--err) 65%, var(--fg)); } + .dl.hunk .lc { color: var(--muted); } + .dl.add .ln, .dl.del .ln { opacity: .9; } + + /* 信息视图 */ + .info-scroll { flex: 1; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--border) transparent; } + .info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding-bottom: 8px; } + .info-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--top-hi); padding: 15px 16px; } + .info-card h3 { font-size: 11px; font-family: var(--font-mono); letter-spacing: .08em; text-transform: uppercase; color: var(--muted); margin-bottom: 10px; font-weight: 600; } + .tag { display: inline-flex; align-items: center; padding: 3px 10px; color: var(--muted); border: 1px solid var(--border); border-radius: 999px; font-size: 11.5px; font-family: var(--font-mono); } + .d-session { display: flex; gap: 12px; padding: 7px 0; border-bottom: 1px solid var(--border); font-size: 12.5px; align-items: baseline; } + .d-session:last-child { border-bottom: 0; } + + /* 网格监控模式 2×2 */ + .wb-grid { flex: 1; min-height: 0; display: none; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 12px; } + .wb-grid.active { display: grid; } + .mini { position: relative; min-height: 0; display: flex; flex-direction: column; background: var(--term-bg); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--top-hi); overflow: hidden; cursor: pointer; transition: border-color var(--dur-fast) var(--ease-move); } + .mini:hover { border-color: var(--hover-strong); } + .mini .term-head { padding: 7px 12px; } + .mini .term { padding: 10px 12px; overflow: hidden; } + .mini .tl { font-size: 11px; line-height: 1.65; } + + /* ─── 日志视图:左时间轴 + 右详情抽屉 ───────────────────────────── */ + .logs-layout { flex: 1; min-height: 0; display: grid; grid-template-columns: minmax(340px, 5fr) minmax(380px, 6fr); } + .log-list { overflow-y: auto; border-right: 1px solid var(--border); padding: 8px; scrollbar-width: thin; scrollbar-color: var(--border) transparent; } + .log-row { width: 100%; display: grid; grid-template-columns: 52px 9px 1fr; gap: 10px; align-items: baseline; padding: 10px 12px; border-radius: var(--radius); border: 1px solid transparent; text-align: left; transition: background var(--dur-fast) var(--ease-move), border-color var(--dur-fast) var(--ease-move); } + .log-row:hover { background: var(--hover); border-color: var(--border); } + .log-row[aria-current="true"] { background: var(--overlay); border-color: var(--border); box-shadow: var(--top-hi); } + .log-row .t { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 11px; color: var(--muted); } + .log-row .task { display: block; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .log-row .who { display: block; font-family: var(--font-mono); font-size: 11px; color: var(--muted); margin-top: 1px; } + .log-detail { min-width: 0; display: flex; flex-direction: column; background: var(--surface); box-shadow: var(--top-hi); } + .ld-head { flex: none; padding: 14px 18px 0; border-bottom: 1px solid var(--border); } + .ld-head h2 { font-size: 15px; font-weight: 600; line-height: 1.3; } + .ld-meta { display: flex; gap: 14px; flex-wrap: wrap; margin: 6px 0 10px; } + .ld-tabs { display: flex; gap: 4px; } + .ld-tab { padding: 7px 12px; font-size: 12.5px; color: var(--muted); border-bottom: 2px solid transparent; margin-bottom: -1px; transition: color var(--dur-fast) var(--ease-move); } + .ld-tab:hover { color: var(--fg); } + .ld-tab[aria-current="true"] { color: var(--fg); border-color: var(--accent); font-weight: 500; } + .ld-body { flex: 1; min-height: 0; overflow-y: auto; padding: 16px 18px 24px; scrollbar-width: thin; scrollbar-color: var(--border) transparent; } + .bubble { max-width: 88%; padding: 10px 14px; border-radius: 12px; font-size: 13px; line-height: 1.6; margin-bottom: 12px; } + .bubble.user { margin-left: auto; background: var(--accent-faint); border: 1px solid color-mix(in oklch, var(--accent) 22%, transparent); border-bottom-right-radius: 4px; } + .bubble.bot { background: var(--overlay); border: 1px solid var(--border); box-shadow: var(--top-hi); border-bottom-left-radius: 4px; } + .bubble .steps { margin: 8px 0 0; padding: 0; list-style: none; } + .bubble .steps li { font-family: var(--font-mono); font-size: 11.5px; color: var(--muted); padding: 2px 0; } + .bubble .steps li b { color: var(--fg); font-weight: 500; } + .json-pre { margin: 0; font-family: var(--font-mono); font-size: 11.5px; line-height: 1.7; color: var(--muted); white-space: pre-wrap; word-break: break-word; } + + /* ─── 设置视图 ──────────────────────────────────────────────────── */ + .set-scroll { flex: 1; overflow-y: auto; padding: 22px 24px 40px; scrollbar-width: thin; scrollbar-color: var(--border) transparent; } + .set-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr)); gap: 12px; max-width: 900px; } + .set-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 9px 0; border-bottom: 1px solid var(--border); font-size: 13px; } + .set-row:last-child { border-bottom: 0; } + .set-row .meta { font-size: 11px; display: block; margin-top: 1px; } + .switch { position: relative; width: 34px; height: 20px; flex: none; border-radius: 999px; background: var(--overlay); border: 1px solid var(--border); transition: background var(--dur-fast) var(--ease-move); } + .switch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: var(--muted); transition: transform var(--dur-med) var(--ease-move), background var(--dur-fast) var(--ease-move); } + .switch[aria-checked="true"] { background: var(--accent-faint); border-color: color-mix(in oklch, var(--accent) 40%, transparent); } + .switch[aria-checked="true"]::after { transform: translateX(14px); background: var(--accent); } + .kbd-table { width: 100%; border-collapse: collapse; font-size: 12.5px; } + .kbd-table td { padding: 7px 0; border-bottom: 1px solid var(--border); } + .kbd-table tr:last-child td { border-bottom: 0; } + .kbd-table td:last-child { text-align: right; } + + /* ─── 底部状态栏 ────────────────────────────────────────────────── */ + .statusbar { display: flex; align-items: center; gap: 16px; padding: 0 14px; background: var(--surface); border-top: 1px solid var(--border); box-shadow: var(--top-hi); font-family: var(--font-mono); font-size: 11px; color: var(--muted); z-index: 5; } + .statusbar .grp { display: inline-flex; align-items: center; gap: 6px; } + .statusbar .right { margin-left: auto; display: inline-flex; gap: 14px; } + .statusbar .right span { display: inline-flex; align-items: center; gap: 5px; } + + /* ─── 浮层:命令面板 / 快捷键 / Toast(毛玻璃)─────────────────── */ + .veil { position: fixed; inset: 0; z-index: 40; background: color-mix(in srgb, var(--bg) 60%, transparent); opacity: 0; pointer-events: none; transition: opacity var(--dur-med) var(--ease-out); } + .veil.open { opacity: 1; pointer-events: auto; transition-timing-function: var(--ease-in); } + .palette { position: fixed; z-index: 50; left: 50%; top: 16vh; translate: -50% 0; width: min(560px, calc(100vw - 48px)); background: var(--glass); backdrop-filter: blur(12px); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-pop), var(--top-hi); overflow: hidden; opacity: 0; pointer-events: none; transform: translateY(8px); transition: opacity var(--dur-med) var(--ease-in), transform var(--dur-med) var(--ease-in); } + .palette.open { opacity: 1; pointer-events: auto; transform: none; } + .palette input { width: 100%; background: transparent; border: 0; border-bottom: 1px solid var(--border); color: var(--fg); font: inherit; font-size: 14.5px; padding: 14px 16px; } + .palette input:focus { outline: none; } + .palette input::placeholder { color: var(--muted); } + .pal-list { max-height: 320px; overflow-y: auto; padding: 6px; scrollbar-width: thin; scrollbar-color: var(--border) transparent; } + .pal-item { width: 100%; display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: var(--radius); text-align: left; } + .pal-item .meta { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .pal-item .kind { flex: none; font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); border: 1px solid var(--border); border-radius: 4px; padding: 1px 6px; } + .pal-item.active { background: var(--hover-strong); } + .pal-empty { padding: 20px 16px; color: var(--muted); font-size: 13px; text-align: center; } + + .modal { position: fixed; z-index: 50; left: 50%; top: 50%; translate: -50% -50%; width: min(440px, calc(100vw - 48px)); background: var(--glass); backdrop-filter: blur(12px); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-pop), var(--top-hi); padding: 18px 20px; opacity: 0; pointer-events: none; transform: translateY(8px); transition: opacity var(--dur-med) var(--ease-in), transform var(--dur-med) var(--ease-in); } + .modal.open { opacity: 1; pointer-events: auto; transform: none; } + .modal h2 { font-size: 14px; font-weight: 600; margin-bottom: 10px; } + + .toasts { position: fixed; z-index: 55; right: 16px; bottom: calc(var(--status-h) + 12px); display: flex; flex-direction: column; gap: 8px; align-items: flex-end; } + /* toast:低频仪式时刻,位移+淡入用克制的 spring 曲线 */ + .toast { display: flex; align-items: center; gap: 10px; background: var(--glass); backdrop-filter: blur(12px); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-pop), var(--top-hi); padding: 10px 14px; font-size: 12.5px; cursor: pointer; opacity: 0; transform: translateY(10px); transition: opacity var(--dur-slow) var(--ease-in), transform var(--dur-slow) var(--ease-spring); } + .toast.in { opacity: 1; transform: none; } + .toast.out { opacity: 0; transform: translateY(6px); transition-duration: 320ms; transition-timing-function: var(--ease-out); } + + /* ─── 彩蛋:极弱扫描线(设置页开关,默认关)────────────────────── */ + .scanline { position: absolute; inset: 0; z-index: 2; pointer-events: none; display: none; background: repeating-linear-gradient(0deg, rgb(255 255 255 / .022) 0 1px, transparent 1px 3px); } + .scanline.on { display: block; } + .scanline.on::after { content: ''; position: absolute; left: 0; right: 0; height: 120px; background: linear-gradient(180deg, transparent, color-mix(in oklch, var(--accent) 5%, transparent), transparent); animation: scan-drift 9s linear infinite; } + @keyframes scan-drift { from { top: -120px; } to { top: 100%; } } + + /* ─── reduced motion:脉冲改常亮、位移改淡入、纹理静止 ─────────── */ + @media (prefers-reduced-motion: reduce) { + .dot.run, .sess-tab.waiting { animation: none; } + .dot.run { box-shadow: 0 0 8px color-mix(in oklch, var(--ok) 30%, transparent), 0 0 0 3px color-mix(in oklch, var(--ok) 25%, transparent); } + .sess-tab.waiting { border-color: color-mix(in oklch, var(--warn) 45%, transparent); } + .caret { animation: none; } + .toast, .palette, .modal, .new-pill { transform: none !important; transition-property: opacity !important; } + .tl.flash, .stag-in, .view.entering, html.booted .stat-tile .v, .scanline.on::after, .log-detail.open { animation: none; } + .card:hover, .attn-pill:hover { transform: none; } + } + + /* ─── 响应式:移动端专用控件桌面端隐藏 ─────────────────────────── */ + .log-back { display: none; } + + /* ─── 平板(≤1080px):触控目标 ≥44px、会话 Tab 条横滚 ────────── */ + @media (max-width: 1080px) { + .rail-btn { width: 44px; height: 44px; } /* 触摸目标 ≥44px */ + .rail-btn::after { display: none; } /* 触屏无 hover,关掉 tooltip */ + .sess-tab { min-height: 44px; } + } + + /* ─── 手机(≤720px):单栏布局 + 日志详情全屏覆盖 ────────────── */ + @media (max-width: 720px) { + .topbar { padding: 0 10px; gap: 8px; } + .topbar .crumb { display: none; } + .attn-pill { padding: 5px 9px; } + .kbtn { padding: 5px 8px; } + .kbtn kbd { display: none; } + .ov-scroll, .set-scroll { padding: 16px 16px 32px; } + .stat-strip { grid-template-columns: 1fr 1fr; } + .sess-tabs { padding: 6px 8px 0; } + .wb-bar { flex-wrap: wrap; row-gap: 8px; padding: 8px 12px 0; } + .wb-body { padding: 10px 12px 12px; } + .wb-grid { grid-template-columns: 1fr; grid-template-rows: repeat(4, 240px); overflow-y: auto; } + .composer { flex-wrap: wrap; } + .composer .sel { max-width: 148px; } + .composer .sel select { width: 100%; } + .composer .input[type="text"] { flex: 1 1 140px; } + .diff-panel { overflow-x: auto; } /* .dl 为 pre,行内容横向滚动兜底 */ + .logs-layout { grid-template-columns: 1fr; } + .log-list { border-right: 0; } + .log-detail { display: none; } + .log-detail.open { display: flex; position: fixed; inset: 0 0 var(--status-h) 0; z-index: 25; animation: view-in var(--dur-med) var(--ease-in); } + .log-back { display: inline-flex; align-items: center; gap: 6px; min-height: 44px; margin: -6px 0 4px -12px; padding: 0 12px; border-radius: 8px; color: var(--muted); font-size: 13px; } + .log-back:hover { background: var(--hover); color: var(--fg); } + .statusbar { gap: 12px; padding: 0 12px; } + .statusbar .right { display: none; } /* 隐藏快捷键提示,保留连接状态与计数 */ + .toasts { right: 12px; left: 12px; align-items: stretch; } + } diff --git a/console/src/main.jsx b/console/src/main.jsx new file mode 100644 index 0000000..0f548cb --- /dev/null +++ b/console/src/main.jsx @@ -0,0 +1,15 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "@fontsource/inter/latin-400.css"; +import "@fontsource/inter/latin-500.css"; +import "@fontsource/inter/latin-600.css"; +import "@fontsource/jetbrains-mono/latin-400.css"; +import "@fontsource/jetbrains-mono/latin-600.css"; +import "@xterm/xterm/css/xterm.css"; +import App from "./App.jsx"; +import "./design/prototype.css"; +import "./app.css"; + +ReactDOM.createRoot(document.getElementById("root")).render( + , +); diff --git a/console/src/tooling.js b/console/src/tooling.js new file mode 100644 index 0000000..0dd75c9 --- /dev/null +++ b/console/src/tooling.js @@ -0,0 +1,64 @@ +export const TOOL_DETAILS = Object.freeze({ + codex: { + description: "OpenAI 官方编码智能体,适合仓库分析、修改、测试和代码审查。", + tags: ["沙箱执行", "代码修改", "任务编排"], + }, + claude: { + description: "Anthropic 官方 CLI,适合复杂工程任务、长链路分析和工具调用。", + tags: ["复杂任务", "MCP", "长上下文"], + }, + codebuddy: { + description: "腾讯 CodeBuddy CLI,面向中文代码任务和工程协作。", + tags: ["中文任务", "工程协作", "代码分析"], + }, + kimi: { + description: "Moonshot AI 的终端编码工具,适合大上下文仓库阅读与文档任务。", + tags: ["长上下文", "仓库阅读", "中文任务"], + }, + opencode: { + description: "可切换多家模型提供方的开源终端编码智能体。", + tags: ["多模型", "开源", "TUI"], + }, + qwen: { + description: "基于 Qwen Coder 的终端智能体,适合中文代码和仓库级任务。", + tags: ["中文代码", "仓库级任务", "智能体模式"], + }, + dsh: { + description: "DeepSeek Harness 以 headless 模式执行单次任务,完成后自动退出。", + tags: ["DeepSeek", "headless", "批量任务"], + }, +}); + +export const STATUS = Object.freeze({ + waiting: { dot: "wait", label: "等待输入" }, + running: { dot: "run", label: "运行中" }, + exited: { dot: "off", label: "已结束" }, + stopped: { dot: "off", label: "未运行" }, + interrupted: { dot: "err", label: "已中断" }, + failed: { dot: "err", label: "错误" }, + error: { dot: "err", label: "不可用" }, +}); + +export function statusFor(value) { + return STATUS[value] || STATUS.stopped; +} + +export function formatTime(value) { + if (!value) return "-"; + return new Intl.DateTimeFormat("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false }).format(new Date(value)); +} + +export function formatDuration(start, end = Date.now()) { + if (!start) return "-"; + const seconds = Math.max(0, Math.floor((new Date(end).getTime() - new Date(start).getTime()) / 1000)); + if (seconds < 60) return `${seconds}s`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ${seconds % 60}s`; + return `${Math.floor(minutes / 60)}h ${minutes % 60}m`; +} + +export function formatBytes(value) { + if (!Number.isFinite(value) || value < 0) return "-"; + if (value < 1024 * 1024) return `${Math.round(value / 1024)} KB`; + return `${(value / (1024 * 1024)).toFixed(value >= 100 * 1024 * 1024 ? 0 : 1)} MB`; +} diff --git a/console/src/visual-polish.css b/console/src/visual-polish.css new file mode 100644 index 0000000..f3889fa --- /dev/null +++ b/console/src/visual-polish.css @@ -0,0 +1,329 @@ +.chat-conversation { + position: relative; + flex: 1; + min-height: 0; + overflow: hidden; + background: var(--term-bg); +} + +.chat-scroll { + height: 100%; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-width: thin; + scrollbar-color: var(--border) transparent; +} + +.chat-thread { + width: min(860px, 100%); + min-height: 100%; + margin: 0 auto; + padding: 28px 28px 44px; +} + +.chat-message { + animation: chat-message-in var(--dur-med) var(--ease-in) both; +} + +.chat-message-user { + display: flex; + justify-content: flex-end; + margin-bottom: 30px; +} + +.chat-user-bubble { + width: fit-content; + max-width: min(78%, 680px); + padding: 10px 14px; + border: 1px solid color-mix(in oklch, var(--accent) 18%, var(--border)); + border-radius: var(--radius-lg) var(--radius-lg) 4px var(--radius-lg); + background: var(--accent-faint); + color: var(--fg); + font-size: 13.5px; + line-height: 1.65; + overflow-wrap: anywhere; + white-space: pre-wrap; +} + +.chat-message-assistant { + min-width: 0; + animation-delay: 36ms; +} + +.chat-turn + .chat-turn { + margin-top: 34px; + padding-top: 30px; + border-top: 1px solid color-mix(in srgb, var(--border) 72%, transparent); +} + +.chat-assistant-head { + display: flex; + align-items: center; + min-width: 0; + gap: 8px; + margin-bottom: 10px; +} + +.chat-assistant-mark, +.chat-empty-mark { + width: 24px; + height: 24px; + flex: none; + display: grid; + place-items: center; + border: 1px solid var(--border); + border-radius: 7px; + background: var(--surface); + box-shadow: var(--top-hi); + color: var(--accent); + font-family: var(--font-mono); + font-size: 8.5px; + font-weight: 600; +} + +.chat-assistant-name { + min-width: 0; + overflow: hidden; + color: var(--fg); + font-size: 12.5px; + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chat-run-state { + display: inline-flex; + align-items: center; + gap: 6px; + margin-left: auto; + color: var(--muted); + font-family: var(--font-mono); + font-size: 10.5px; + white-space: nowrap; +} + +.chat-state-dot { + width: 7px; + height: 7px; + flex: none; + border: 1px solid var(--muted); + border-radius: 50%; +} + +.chat-run-state.is-working .chat-state-dot { + border-color: transparent; + background: var(--ok); + animation: chat-status-breathe 2s cubic-bezier(.4, 0, .6, 1) infinite; +} + +.chat-run-state.is-done .chat-state-dot { + border-color: transparent; + background: var(--ok); +} + +.chat-run-state.is-error { + color: color-mix(in oklch, var(--err) 70%, var(--fg)); +} + +.chat-run-state.is-error .chat-state-dot { + border-color: transparent; + border-radius: 2px; + background: var(--err); + transform: rotate(45deg); +} + +.chat-run-state.is-stopped .chat-state-dot { + border-radius: 2px; +} + +.chat-response { + width: 100%; + min-height: 24px; + margin: 0; + color: color-mix(in srgb, var(--fg) 92%, var(--muted)); + font-family: var(--font-body); + font-size: 13.5px; + line-height: 1.72; + overflow-wrap: anywhere; + tab-size: 2; + white-space: pre-wrap; +} + +.chat-response.is-placeholder { + color: var(--muted); +} + +.chat-response.is-placeholder.is-working::after { + content: "..."; +} + +.chat-jump { + position: absolute; + right: 18px; + bottom: 16px; + display: inline-flex; + align-items: center; + gap: 7px; + min-height: 32px; + padding: 6px 11px; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--glass); + backdrop-filter: blur(12px); + box-shadow: var(--shadow-pop), var(--top-hi); + color: var(--fg); + font-family: var(--font-mono); + font-size: 11px; + opacity: 0; + pointer-events: none; + transform: translateY(6px); + transition: + opacity var(--dur-med) var(--ease-in), + transform var(--dur-med) var(--ease-in), + background var(--dur-fast) var(--ease-move); +} + +.chat-jump.show { + opacity: 1; + pointer-events: auto; + transform: none; +} + +.chat-jump:hover { + background: var(--overlay); +} + +.chat-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6px; + padding: 24px; + text-align: center; +} + +.chat-empty-mark { + width: 32px; + height: 32px; + margin-bottom: 5px; + font-size: 10px; +} + +.chat-empty strong { + color: var(--fg); + font-size: 13px; + font-weight: 600; +} + +.chat-empty > span { + max-width: 360px; + color: var(--muted); + font-size: 12px; + line-height: 1.6; +} + +/* The existing composer keeps its DOM and controls, but reads as one input surface. */ +.app .composer { + gap: 6px; + padding: 6px; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--surface); + box-shadow: var(--top-hi), 0 8px 24px rgb(0 0 0 / .18); + transition: border-color var(--dur-fast) var(--ease-move), box-shadow var(--dur-fast) var(--ease-move); +} + +.app .composer:focus-within { + border-color: color-mix(in oklch, var(--accent) 34%, var(--border)); + box-shadow: var(--top-hi), 0 8px 24px rgb(0 0 0 / .22); +} + +.app .composer > .chat-input { + flex: 1 1 240px; + min-height: 36px; + max-height: 112px; + padding-block: 8px; + border-color: transparent; + background: transparent; + line-height: 1.5; + resize: none; +} + +.app .composer > .chat-input:focus { + border-color: transparent; +} + +.app .composer .sel select, +.app .composer .btn { + min-height: 36px; +} + +@keyframes chat-message-in { + from { + opacity: 0; + transform: translateY(6px); + } +} + +@keyframes chat-status-breathe { + 0%, 100% { + box-shadow: 0 0 6px color-mix(in oklch, var(--ok) 38%, transparent); + opacity: 1; + } + 50% { + box-shadow: 0 0 3px color-mix(in oklch, var(--ok) 22%, transparent); + opacity: .66; + } +} + +@media (max-width: 720px) { + .chat-thread { + padding: 20px 16px 36px; + } + + .chat-message-user { + margin-bottom: 24px; + } + + .chat-user-bubble { + max-width: 92%; + } + + .chat-run-state { + max-width: 42%; + overflow: hidden; + text-overflow: ellipsis; + } + + .chat-jump { + right: 12px; + bottom: 12px; + } + + .app .composer { + gap: 5px; + } + + .app .composer > .chat-input { + order: -1; + flex-basis: 100%; + } +} + +@media (prefers-reduced-motion: reduce) { + .chat-message, + .chat-run-state.is-working .chat-state-dot { + animation: none; + } + + .chat-jump { + transform: none; + transition-property: opacity, background; + } + + .chat-scroll { + scroll-behavior: auto; + } +} diff --git a/console/tests/mock-runner.mjs b/console/tests/mock-runner.mjs new file mode 100644 index 0000000..9d5ab33 --- /dev/null +++ b/console/tests/mock-runner.mjs @@ -0,0 +1,80 @@ +import crypto from "node:crypto"; +import http from "node:http"; +import express from "express"; +import { WebSocketServer } from "ws"; + +const token = process.env.RUNNER_TOKEN || "test-runner-token-that-is-at-least-32-characters"; +const tools = [ + ["codex", "Codex", "OpenAI", "CX", "codex", "0.149.0"], + ["claude", "Claude Code", "Anthropic", "CC", "claude", "2.1.241"], + ["codebuddy", "CodeBuddy", "Tencent", "CB", "codebuddy", "2.137.1"], + ["kimi", "Kimi CLI", "Moonshot AI", "KM", "kimi", "0.38.0"], + ["opencode", "OpenCode", "SST / Open source", "OC", "opencode", "1.18.21"], + ["qwen", "Qwen Code", "Alibaba", "QW", "qwen", "0.22.0"], + ["dsh", "DeepSeek Harness", "DeepSeek", "DS", "dsh", "0.1.1-rc.2"], +].map(([id, name, vendor, mono, command, version]) => ({ id, name, vendor, mono, command, version, installed: true, status: "stopped", profiles: id === "dsh" ? ["headless"] : [] })); +const sessions = []; +const integrations = { + ccSwitch: { + installed: true, + version: "cc-switch 5.10.2", + initialized: true, + apps: [ + { id: "claude", toolId: "claude", name: "Claude Code", currentProviderId: "anthropic", providers: [{ id: "anthropic", name: "Anthropic", endpoint: "https://api.anthropic.com", active: true }, { id: "relay", name: "Team Relay", endpoint: "https://relay.example", active: false }] }, + { id: "codex", toolId: "codex", name: "Codex", currentProviderId: "codex-official", providers: [{ id: "codex-official", name: "OpenAI Official", endpoint: null, active: true }] }, + { id: "open-code", toolId: "opencode", name: "OpenCode", currentProviderId: "openrouter", providers: [{ id: "openrouter", name: "OpenRouter", endpoint: "https://openrouter.ai", active: true }] }, + ], + unsupportedToolIds: ["codebuddy", "kimi", "qwen", "dsh"], + }, + multica: { + installed: true, + version: "multica 0.1.53", + daemon: { status: "stopped", supervision: { mode: "unsupervised" } }, + config: { configured: false, serverUrl: null, appUrl: null, workspaceId: null }, + supportedToolIds: ["claude", "codex", "codebuddy", "kimi", "opencode"], + }, +}; + +const app = express(); +app.use(express.json()); +app.use((req, res, next) => req.headers.authorization === `Bearer ${token}` ? next() : res.status(401).json({ error: "Unauthorized" })); +app.get("/health", (_req, res) => res.json({ ok: true })); +app.get("/tools", (_req, res) => res.json({ tools: tools.map((tool) => ({ ...tool, status: sessions.some((session) => session.tool === tool.id && session.status === "running") ? "running" : "stopped" })) })); +app.get("/projects", (_req, res) => res.json({ projects: ["demo-app", "website"] })); +app.get("/sessions", (_req, res) => res.json({ sessions })); +app.get("/integrations", (_req, res) => res.json({ integrations })); +app.get("/sessions/:id/output", (_req, res) => res.type("text/plain").send("AgentDock local QA session\r\n")); +app.get("/sessions/:id/diff", (_req, res) => res.type("text/plain").send("diff --git a/src/app.js b/src/app.js\n--- a/src/app.js\n+++ b/src/app.js\n@@ -1 +1 @@\n-old\n+new\n")); +app.post("/sessions", (req, res) => { + const tool = tools.find((item) => item.id === req.body.tool); + const session = { id: crypto.randomUUID(), tool: tool.id, toolName: tool.name, project: req.body.project || ".", purpose: req.body.purpose || "task", initialInput: req.body.initialInput || "", profile: req.body.profile || null, model: req.body.model || null, status: "running", pid: 4242, createdAt: new Date().toISOString(), endedAt: null, exitCode: null, signal: null }; + sessions.unshift(session); + res.status(201).json({ session }); +}); +app.post("/sessions/:id/input", (_req, res) => res.status(204).end()); +app.post("/sessions/:id/resize", (_req, res) => res.status(204).end()); +app.post("/sessions/:id/stop", (req, res) => { const session = sessions.find((item) => item.id === req.params.id); if (session) { session.status = "stopped"; session.endedAt = new Date().toISOString(); } res.status(204).end(); }); +app.post("/integrations/cc-switch/:app/switch", (req, res) => { + const appConfig = integrations.ccSwitch.apps.find((item) => item.id === req.params.app); + if (!appConfig) return res.status(400).json({ error: "Unsupported CC Switch application" }); + appConfig.currentProviderId = req.body.providerId; + appConfig.providers.forEach((provider) => { provider.active = provider.id === req.body.providerId; }); + res.json({ integrations }); +}); +app.post("/integrations/cc-switch/:app/import-live", (_req, res) => res.json({ integrations })); + +const server = http.createServer(app); +const sockets = new WebSocketServer({ noServer: true }); +server.on("upgrade", (request, socket, head) => { + const url = new URL(request.url, "http://localhost"); + const match = url.pathname.match(/^\/ws\/sessions\/([a-f0-9-]+)$/); + if (!match || url.searchParams.get("token") !== token) return socket.destroy(); + sockets.handleUpgrade(request, socket, head, (client) => { + client.send(JSON.stringify({ type: "snapshot", data: "\u001b[36mAgentDock local QA session\u001b[0m\r\n$ CLI ready\r\n" })); + client.on("message", (raw) => { + const message = JSON.parse(raw.toString()); + if (message.type === "input" && message.data.trim()) client.send(JSON.stringify({ type: "output", data: `\r\nreceived: ${message.data}` })); + }); + }); +}); +server.listen(4174, "127.0.0.1", () => console.log("mock runner listening on 4174")); diff --git a/console/vite.config.js b/console/vite.config.js new file mode 100644 index 0000000..3600906 --- /dev/null +++ b/console/vite.config.js @@ -0,0 +1,23 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + "/api": "http://127.0.0.1:4173", + }, + }, + build: { + sourcemap: true, + rollupOptions: { + output: { + manualChunks(id) { + if (id.includes("@xterm")) return "terminal"; + if (id.includes("react") || id.includes("lucide-react")) return "ui-vendor"; + }, + }, + }, + }, +}); diff --git a/docs/CONSOLE-DEPLOYMENT.md b/docs/CONSOLE-DEPLOYMENT.md new file mode 100644 index 0000000..83305b7 --- /dev/null +++ b/docs/CONSOLE-DEPLOYMENT.md @@ -0,0 +1,117 @@ +# AgentDock 网页控制台部署说明 + +## 访问范围 + +控制台固定绑定服务器地址 `192.168.200.36`,默认端口为 `8443`: + +```text +https://192.168.200.36:8443 +``` + +Caddy 同时检查来源 IP,只接受 `192.168.200.0/24`。Compose 不映射 +Runner 和 Console 的内部端口,局域网只能接触 HTTPS 代理。 + +## 准备 `.env` + +在服务器执行: + +```bash +cd /vol1/1000/docker/ai-toools +cp -n .env.example .env +chmod 600 .env +``` + +必须设置三个独立的机密值: + +```dotenv +ADMIN_PASSWORD=一个至少12位的管理员密码 +RUNNER_TOKEN=至少32位随机字符串 +CONFIG_ENCRYPTION_KEY=至少32位随机字符串 +``` + +可以用 OpenSSL 生成随机值: + +```bash +openssl rand -base64 36 +``` + +不要把三个值写进 `compose.yaml`、Dockerfile 或 Git。`CONFIG_ENCRYPTION_KEY` +丢失后,设置页中保存的 API Key 将无法解密。 + +## 构建和启动 + +```bash +cd /vol1/1000/docker/ai-toools +mkdir -p workspace data console-data +chmod +x scripts/generate-console-certificate.sh +./scripts/generate-console-certificate.sh /vol1/1000/docker/ai-toools +docker compose build +docker compose up -d +docker compose ps +docker compose logs --tail=100 ai-tools ai-console ai-proxy +``` + +健康状态应为 `healthy`。检查内部服务: + +```bash +docker compose exec ai-tools curl -fsS http://127.0.0.1:4174/health +docker compose exec ai-console wget -qO- http://127.0.0.1:4173/health +``` + +## 信任局域网证书 + +Caddy 使用部署目录中为服务器 IP 生成的自签名证书。把证书复制到 Windows: + +```bash +cd /vol1/1000/docker/ai-toools +scp nas:/vol1/1000/docker/ai-toools/proxy/certs/agentdock.crt . +``` + +复制到 Windows 后,在当前用户的受信任根证书存储中安装: + +```powershell +certutil -user -addstore Root .\agentdock.crt +``` + +只应信任从自己的 `192.168.200.36` 服务器导出的证书。安装后重新打开浏览器, +访问 `https://192.168.200.36:8443`。 + +## 第一次使用 + +1. 使用 `.env` 中的 `ADMIN_PASSWORD` 登录。 +2. 在顶部选择 `/workspace` 下的项目。 +3. 在设置页填写需要的 API Key,或点击“打开终端”进入独立授权终端完成设备授权。 +4. 回到工作台,选择 CLI 后直接发送消息。 +5. DeepSeek Harness 必须先填写任务再发送,使用 `headless` 执行一次后自动退出。 + +工作台的“会话”页使用非交互 CLI 输出和单一多行输入框。每条消息对应一次任务, +回复按时间连续显示。按 Enter 发送,按 Shift+Enter 换行。初始化授权使用独立的 +可交互终端;“原始输出”页只用于查看本次非交互任务的底层输出,不接收键盘输入。 + +当前 `@deepseek-ai/dsh@0.1.1-rc.2` 没有发布可直接安装的 TUI profile bundle。 +CLI 帮助中的 `tui` 只是自定义 profile 示例,不能直接执行。AgentDock 因此只开放 +官方内置并适合本控制台的 `headless` profile,不开放 Harness 自带 Web 服务。 +DeepSeek Harness 不使用其他 CLI 的网页登录状态,必须在设置页配置 +`DEEPSEEK_API_KEY` 才能运行。 + +API Key 使用 AES-256-GCM 加密后存放于 `console-data/settings.enc.json`,接口只返回 +掩码。原有 CLI 自己的登录状态仍保存在 `data` 目录。 + +## 会话生命周期 + +- 浏览器关闭:CLI 进程继续运行,重新登录后可继续连接。 +- `ai-console` 重启:CLI 进程不受影响,但管理员需要重新登录。 +- `ai-tools` 重启:其中的所有 CLI 进程会停止;历史输出仍保留。 +- `docker compose down`:所有 CLI 进程停止,`data` 和 `console-data` 不删除。 + +## 安全检查 + +```bash +# 不应出现 docker.sock +docker inspect ai-tools --format '{{json .Mounts}}' | grep docker.sock + +# 主机只应监听 HTTPS 端口,不应监听 4173/4174 +ss -lnt | grep -E '(:8443|:4173|:4174)' +``` + +第一条命令没有输出是正确结果。第二条只应看到 `192.168.200.36:8443`。 diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..0a9385f --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,161 @@ +# 部署说明 + +## 1. 部署前提 + +Ubuntu 主机需要满足: + +- Ubuntu 22.04 或 24.04,x86_64 或 ARM64 +- 已安装 Docker Engine 和 Docker Compose Plugin +- 能访问各 CLI 的官网、npm Registry 和模型接口 +- 当前用户有运行 Docker 的权限 +- 建议至少 4 核 CPU、8 GB 内存和 15 GB 可用磁盘 + +这些 CLI 通常调用云端模型,本容器不在本机加载大模型权重,因此一般不需要 GPU。 + +## 2. 放置部署文件 + +建议把本目录放到 Ubuntu: + +```text +/opt/ai-toolbox +``` + +项目统一放到: + +```text +/srv/projects +``` + +创建项目目录: + +```bash +sudo mkdir -p /srv/projects +sudo chown -R "$(id -u):$(id -g)" /srv/projects +``` + +## 3. 准备配置 + +```bash +cd /opt/ai-toolbox +cp .env.example .env +chmod +x scripts/ai +id -u +id -g +``` + +把 `id -u` 和 `id -g` 的结果填写进 `.env`: + +```dotenv +WORKSPACE_PATH=/srv/projects +AI_HOME_PATH=/opt/ai-toolbox/data +AI_UID=1000 +AI_GID=1000 +AI_CONTAINER_NAME=ai-tools +TZ=Asia/Shanghai +``` + +不要把 API Key 写进 Dockerfile。`.env` 也不要提交到 Git。 + +## 4. 构建并启动 + +```bash +docker compose build +docker compose up -d +docker compose ps +docker compose exec ai-tools ai-tools-check +``` + +检查结果中以下命令都应显示 `OK`: + +```text +codex claude codebuddy kimi opencode qwen dsh cc-switch multica +``` + +## 5. 首次登录 + +每个工具需要分别登录一次: + +```bash +./scripts/ai codex . +./scripts/ai claude . +./scripts/ai codebuddy . +./scripts/ai kimi . +./scripts/ai opencode . +./scripts/ai qwen . +``` + +按照终端显示的链接或验证码,在 Windows 浏览器中完成登录。Kimi 进入界面后使用 `/login`。 + +远程服务器上,浏览器自动打开通常不会成功,这是正常的。优先使用工具提供的设备验证码登录;如果某个工具只支持浏览器回调,可以改用它支持的 API Key,或者为回调端口建立 SSH 隧道。 + +登录信息和会话文件保存在 `.env` 的 `AI_HOME_PATH` 目录中,不会因为重新构建镜像而丢失。 + +## 6. 初始化 CC Switch + +打开网页的“设置 -> Provider”,点击“打开 CC Switch TUI”。先为 Claude、Codex 和 OpenCode 导入或创建 Provider,并确认三个 CLI 都有当前 Provider。 + +也可以在 SSH 中操作: + +```bash +ai cc-switch +ai cc-switch --app codex provider list +``` + +配置完成后,在网页启用“CC Switch 配置接管”。启用后,AgentDock 不再向 Claude、Codex 和 OpenCode 会话注入网页账号页保存的旧 API Key,避免环境变量覆盖 CC Switch 配置。CodeBuddy、Kimi、Qwen 和 DeepSeek 仍由 AgentDock 管理。 + +## 7. 初始化 Multica Runtime + +打开网页的“设置 -> Runtime”,填写现有自托管 Multica 的 Server URL 和 App URL,然后点击“初始化 Runtime”。授权只在独立终端中进行,完成后 Supervisor 会自动启动前台 daemon。 + +也可以在 SSH 中检查: + +```bash +ai multica config show +ai multica daemon status --output json +docker compose logs --tail=100 ai-tools +``` + +Multica 当前公开支持 Claude、Codex、CodeBuddy、Kimi 和 OpenCode。Qwen 与 DeepSeek 会显示为“已安装,待验证支持”。本部署仅安装 Multica 客户端 Runtime,不包含也不替换用户现有的自托管服务端。 + +如果网页终端中的 OAuth 回调无法从 Windows 返回容器,可在 NAS SSH 中运行一次临时初始化。将两个示例 URL 替换为实际地址: + +```bash +docker run --rm -it \ + --network host \ + --user 1000:1001 \ + -e HOME=/home/ai \ + -v /vol1/1000/docker/ai-toools/data:/home/ai \ + local/ai-toolbox:0.2.0 \ + multica-setup \ + --server-url https://multica-api.example.com \ + --app-url https://multica.example.com \ + --callback-host 192.168.200.36 +``` + +该容器只在授权期间使用宿主机网络,完成后自动退出。正式 Multica daemon 仍由 `ai-tools` 内的 Supervisor 管理,不使用 host 网络。 + +## 8. DeepSeek Harness + +DeepSeek Harness 的命令是 `dsh`,当前属于 developer preview,官方提示未来可能出现不兼容更新。 + +无界面执行一次任务: + +```bash +./scripts/ai dsh my-project --profile headless "检查项目并运行测试" +``` + +查看当前版本支持的参数: + +```bash +docker compose exec ai-tools dsh --help +``` + +当前 npm 包只提供 `headless` 与 `web` profile,没有可安装的原生 TUI。AgentDock 使用 `headless`,每次提交一个任务,完成后进程自动退出。`web` profile 不纳入 Compose,也不额外暴露端口。 + +## 9. OpenClaw 和 Cursor + +- 不修改 Ubuntu 主机现有的 OpenClaw。 +- 不占用 OpenClaw 的 Gateway 端口。 +- Cursor 桌面版保留在 Windows。 +- Cursor 使用 Remote SSH 打开 `/srv/projects/项目名`。 +- AI 容器通过 `/workspace/项目名` 访问同一批文件。 diff --git a/docs/MAINTENANCE.md b/docs/MAINTENANCE.md new file mode 100644 index 0000000..c2fad2c --- /dev/null +++ b/docs/MAINTENANCE.md @@ -0,0 +1,72 @@ +# 升级和备份 + +## 升级 CLI + +默认版本是 `latest`。升级时重新构建镜像: + +```bash +cd /opt/ai-toolbox +docker compose build --pull --no-cache +docker compose up -d +docker compose exec ai-tools ai-tools-check +``` + +为了避免上游突然更新造成故障,首次部署成功后建议记录版本: + +```bash +docker compose exec ai-tools npm list -g --depth=0 +``` + +然后把 `.env` 中的 `latest` 改成确定的版本号,再重新构建。例如: + +```dotenv +CODEX_VERSION=具体版本号 +CLAUDE_VERSION=具体版本号 +``` + +DeepSeek Harness 目前是 developer preview,尤其建议锁定版本。 + +CC Switch 与 Multica 使用固定原生二进制版本和 SHA256 校验。升级时同时修改 Dockerfile 中的版本号和对应架构校验值,不要只修改 `.env` 版本而保留旧校验值。 + +## 备份账号和会话 + +所有 CLI 的用户配置位于 `.env` 的 `AI_HOME_PATH`。默认示例为: + +```text +/opt/ai-toolbox/data +``` + +这个目录也包含 `~/.cc-switch`、`~/.multica`、各 CLI 登录信息以及 Multica Runtime 身份,必须整体备份。 + +创建备份: + +```bash +mkdir -p "$HOME/ai-toolbox-backups" +docker run --rm \ + -v /opt/ai-toolbox/data:/source:ro \ + -v "$HOME/ai-toolbox-backups:/backup" \ + ubuntu:24.04 \ + tar -C /source -czf /backup/ai-toolbox-home.tar.gz . +``` + +备份中包含登录凭证,应当像密码一样保护。 + +网页控制台还需要备份: + +```text +/vol1/1000/docker/ai-toools/console-data +/vol1/1000/docker/ai-toools/.env +``` + +`.env` 内的 `CONFIG_ENCRYPTION_KEY` 必须与 `console-data` 成对保存,否则无法解密 +网页中保存的 API Key。备份中还包含管理员密码,也应按密码文件保护。 + +## 不要执行的命令 + +停止使用: + +```bash +docker compose down +``` + +`docker compose down` 不会删除 `AI_HOME_PATH` 中的配置。不要手动删除该目录,除非明确要清除全部登录状态和会话。 diff --git a/docs/SERVER-192.168.200.36.md b/docs/SERVER-192.168.200.36.md new file mode 100644 index 0000000..7c40a16 --- /dev/null +++ b/docs/SERVER-192.168.200.36.md @@ -0,0 +1,124 @@ +# 192.168.200.36 实际部署记录 + +部署时间:2026-08-23 + +## 已部署状态 + +- SSH 主机:`192.168.200.36:22` +- 实际可用账户:`leefer` +- Windows SSH 别名:`nas` +- 部署目录:`/vol1/1000/docker/ai-toools` +- 项目目录:`/vol1/1000/docker/ai-toools/workspace` +- CLI 账号数据:`/vol1/1000/docker/ai-toools/data` +- 容器名称:`ai-tools` +- 镜像:`local/ai-toolbox:latest` + +## 网页控制台 + +- 局域网 URL:`https://192.168.200.36:8443` +- 局域网域名:`https://agent.solsum.cn` +- 代理:`ai-proxy` +- 控制服务:`ai-console` +- CLI Runner:`ai-tools:4174`,仅 Docker 内部网络可达 +- Runtime:Runner 与 Multica daemon 同处 `ai-tools`,共享 `/home/ai`、`/workspace` 和 CLI PATH +- 控制台数据:`/vol1/1000/docker/ai-toools/console-data` +- 允许来源:`192.168.200.0/24` +- Docker Socket:未挂载 + +注意:本机 SSH 配置把直接使用 IP `192.168.200.36` 指向了 Gitea 的 `222` 端口。日常系统 SSH 应使用已有别名 `nas`: + +```powershell +ssh nas +``` + +`root` 账户没有接受本机现有私钥,因此本次部署使用已验证且属于 Docker 组的 `leefer` 账户。没有修改服务器的 root SSH 设置。 + +## 已锁定版本 + +```text +Codex CLI 0.149.0 +Claude Code 2.1.241 +CodeBuddy Code 2.137.1 +Kimi Code CLI 0.38.0 +OpenCode 1.18.21 +Qwen Code 0.22.0 +DeepSeek Harness 0.1.1-rc.2 +CC Switch CLI 5.10.2 +Multica CLI 0.1.53 +Node.js 22.23.2 +``` + +## 第一次登录 + +从 Windows 进入服务器: + +```powershell +ssh nas +``` + +然后逐个启动并完成登录: + +```bash +ai codex . +ai claude . +ai codebuddy . +ai kimi . +ai opencode . +ai qwen . +ai cc-switch +``` + +Kimi 进入界面后输入 `/login`。网页登录在 Windows 浏览器完成即可。DeepSeek Harness 按实际模型提供方配置使用,不需要作为常驻服务启动。 + +## 日常调用 + +在 Ubuntu SSH 终端调用: + +```bash +ai codex 项目名 +ai claude 项目名 +ai kimi 项目名 +ai qwen 项目名 +ai dsh 项目名 --profile headless "运行测试" +ai multica daemon status --output json +``` + +这里的 `项目名` 是项目目录相对于 `workspace` 的路径。例如: + +```text +/vol1/1000/docker/ai-toools/workspace/my-app +``` + +对应: + +```bash +ai codex my-app +``` + +从 Windows 直接调用: + +```powershell +.\scripts\remote-ai.ps1 -Server nas -Tool codex -Project my-app +.\scripts\remote-ai.ps1 -Server nas -Tool claude -Project my-app +.\scripts\remote-ai.ps1 -Server nas -Tool kimi -Project my-app +``` + +## 管理命令 + +```bash +cd /vol1/1000/docker/ai-toools + +docker compose ps +docker compose exec ai-tools ai-tools-check +docker compose logs --tail=100 ai-tools +docker compose restart +docker compose down +docker compose up -d +``` + +`docker compose down` 不会删除登录数据。不要手动删除 `data` 目录。 + +## Cursor 和 OpenClaw + +- Windows Cursor 使用 Remote SSH 的 `nas` 连接,打开 `workspace` 下的项目。 +- OpenClaw 没有安装进 `ai-tools`,现有 OpenClaw 配置和端口均未修改。 diff --git a/docs/SOURCES.md b/docs/SOURCES.md new file mode 100644 index 0000000..2355ee7 --- /dev/null +++ b/docs/SOURCES.md @@ -0,0 +1,17 @@ +# 官方资料来源 + +以下资料在 2026-08-23 核对。上游安装方式可能继续变化。 + +- Codex CLI:[官方 CLI 文档](https://developers.openai.com/codex/cli/);官方当前提供 Linux 安装方式,并支持 ChatGPT 或 API Key 登录。 +- Codex 登录:[官方认证文档](https://learn.chatgpt.com/docs/auth);凭证可能保存在 `~/.codex/auth.json` 或系统凭证存储中。 +- Claude Code:[官方安装文档](https://code.claude.com/docs/en/setup);支持 Ubuntu 20.04+,官方推荐原生安装,也保留 npm 安装方式。 +- Kimi Code CLI:[官方入门文档](https://www.kimi.com/code/docs/en/kimi-code-cli/guides/getting-started.html);npm 包为 `@moonshot-ai/kimi-code`,要求 Node.js 22.19.0+。 +- DeepSeek Harness:[官方仓库](https://github.com/deepseek-ai/deepseek-harness);npm 包为 `@deepseek-ai/dsh`,命令为 `dsh`,当前处于 developer preview。 +- DeepSeek Harness CLI:[官方 CLI 说明](https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/README.md);支持 `web` 和 `headless` profile。 +- CodeBuddy Code:[npm 包](https://www.npmjs.com/package/@tencent-ai/codebuddy-code) +- OpenCode:[官方站点](https://opencode.ai/) +- Qwen Code:[官方仓库](https://github.com/QwenLM/qwen-code) +- CC Switch CLI:[社区 CLI 仓库](https://github.com/SaladDay/cc-switch-cli);本部署锁定 `v5.10.2`,管理 Claude、Codex 和 OpenCode。 +- Multica CLI:[公开二进制分发仓库](https://github.com/zimplemedia/multica-cli);本部署锁定 `v0.1.53`,仅运行连接现有自托管服务端的客户端 daemon。 + +本部署选择 npm 统一安装,是为了让所有工具共用同一套 Node.js 运行环境并简化 Docker 镜像维护。对于提供原生安装器的工具,这不代表 npm 是其唯一安装方式。 diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md new file mode 100644 index 0000000..e756dfd --- /dev/null +++ b/docs/TROUBLESHOOTING.md @@ -0,0 +1,101 @@ +# 常见故障 + +## 1. 命令不存在 + +```bash +docker compose exec ai-tools ai-tools-check +docker compose build --no-cache +docker compose up -d +``` + +如果只缺少一个命令,查看构建日志中对应 npm 包的安装错误。 + +## 2. 项目目录没有权限 + +在 Ubuntu 主机执行: + +```bash +grep -E '^(AI_UID|AI_GID)=' .env +id -u +id -g +sudo chown -R "$(id -u):$(id -g)" /srv/projects +``` + +`.env` 中的 UID/GID 应与操作项目文件的 Ubuntu 用户一致。修改后重新构建容器。 + +## 3. 登录完成后重建容器又要求登录 + +检查持久化目录: + +```bash +docker compose config +grep '^AI_HOME_PATH=' .env +ls -ld "$(grep '^AI_HOME_PATH=' .env | cut -d= -f2-)" +``` + +确认 Compose 仍把 `AI_HOME_PATH` 挂载到了 `/home/ai`,并且目录没有被手动删除。 + +## 4. Windows 运行脚本后界面显示异常 + +使用 Windows Terminal,并确保 SSH 分配终端: + +```powershell +ssh -tt mobai@192.168.1.100 "docker exec -it ai-tools codex" +``` + +本部署包的 `remote-ai.ps1` 已经使用 `ssh -tt`。 + +## 5. 登录链接跳回 localhost 后失败 + +原因是网页在 Windows 打开,但登录回调服务在 Ubuntu 容器里。优先选择设备验证码登录或 API Key。只有在工具明确显示回调端口时,才建立对应的 SSH 端口转发。 + +不要直接把随机回调端口开放到公网。 + +## 6. 无法访问模型接口或 npm + +在容器内检查 DNS 和 HTTPS: + +```bash +docker compose exec ai-tools getent hosts registry.npmjs.org +docker compose exec ai-tools curl -I https://registry.npmjs.org/ +``` + +如果 Ubuntu 主机需要代理,应把代理地址配置到 Docker daemon 或 Compose 环境中。代理凭证不要写入会提交的文件。 + +## 7. DeepSeek Harness 更新后参数变化 + +它目前是 developer preview。先查看当前帮助: + +```bash +docker compose exec ai-tools dsh --help +docker compose exec ai-tools dsh web --help +``` + +确认工作后,把 `.env` 的 `DSH_VERSION` 从 `latest` 改为当前版本号并重新构建。 + +## 8. OpenClaw 受到影响 + +本 Compose 文件没有安装 OpenClaw、没有映射 OpenClaw 端口,也没有挂载 OpenClaw 配置目录。若现有 OpenClaw 出现问题,应单独检查它原来的服务,不要删除 AI 工具箱的 `data` 目录试图修复。 + +## 9. CC Switch 切换后没有生效 + +先检查冲突环境变量和当前 Provider: + +```bash +ai cc-switch env check --app codex +ai cc-switch --app codex provider current +``` + +确认网页“设置 -> Provider”中的“CC Switch 配置接管”已启用,并重新创建 CLI 会话。已经运行中的会话不会自动切换 Provider。 + +## 10. Multica daemon 离线 + +```bash +ai multica config show +ai multica daemon status --output json +docker compose logs --tail=150 ai-tools +``` + +若 `workspace_id` 未设置,回到网页 Runtime 页面重新初始化。若配置完整但 daemon 未上线,执行 `docker compose restart ai-tools`,Supervisor 会重新拉起它。不要在容器内额外运行后台 daemon,否则会与 Supervisor 管理的前台进程重复。 + +若授权页面最终跳回无法访问的本地回调地址,使用部署文档“初始化 Multica Runtime”中的一次性 host 网络命令。它只负责写入 `/home/ai/.multica`,不会改变正式 Compose 的网络边界。 diff --git a/docs/USAGE.md b/docs/USAGE.md new file mode 100644 index 0000000..bea51e6 --- /dev/null +++ b/docs/USAGE.md @@ -0,0 +1,113 @@ +# 日常使用 + +## 使用网页控制台 + +局域网浏览器访问: + +```text +https://192.168.200.36:8443 +``` + +登录后可以选择项目、发送消息、查看文字回复、Git Diff 和历史会话。 +关闭浏览器不会停止 CLI;停止 `ai-tools` 容器才会停止其中全部 CLI 进程。 + +日常任务使用“会话”页的单一多行输入框;Enter 发送,Shift+Enter 换行。首次登录时从“设置 -> CLI 账号”打开独立终端。 +“原始输出”只用于故障排查。日常任务使用非交互模式,不会弹出原生确认提示。 +DeepSeek Harness 没有空会话,必须先输入完整任务再发送。 + +Provider 切换在“设置 -> Provider”完成,只影响切换后新启动的 CLI 会话。MCP 与 Skills 的完整编辑仍在 CC Switch TUI 中完成。Multica 的连接、Workspace 和 daemon 状态在“设置 -> Runtime”查看。 + +网页中的“运行中实例”表示当前真实 CLI 进程数量,不是已安装工具数量。 + +## 在 Ubuntu 上使用 + +先登录 Ubuntu: + +```powershell +ssh nas +``` + +进入部署目录后调用工具: + +```bash +ai codex my-project +ai claude my-project +ai codebuddy my-project +ai kimi my-project +ai opencode my-project +ai qwen my-project +ai cc-switch +ai multica daemon status --output json +``` + +`my-project` 对应 NAS 的 `/vol1/1000/docker/ai-toools/workspace/my-project`。`cc-switch` 和 `multica` 是管理命令,不接项目参数。 + +如果不使用辅助脚本,原始命令是: + +```bash +docker compose exec --workdir /workspace/my-project ai-tools codex +``` + +## 从 Windows 一条命令调用 + +在包含本部署包的 Windows 目录执行: + +```powershell +.\scripts\remote-ai.ps1 ` + -Server mobai@192.168.1.100 ` + -Tool codex ` + -Project my-project +``` + +调用其他工具只需要修改 `-Tool`: + +```powershell +.\scripts\remote-ai.ps1 -Server mobai@192.168.1.100 -Tool claude -Project my-project +.\scripts\remote-ai.ps1 -Server mobai@192.168.1.100 -Tool kimi -Project my-project +.\scripts\remote-ai.ps1 -Server mobai@192.168.1.100 -Tool qwen -Project my-project +``` + +传递额外参数: + +```powershell +.\scripts\remote-ai.ps1 ` + -Server mobai@192.168.1.100 ` + -Tool kimi ` + -Project my-project ` + -ToolArguments @("-p", "解释这个项目") +``` + +DeepSeek Harness 无界面任务: + +```powershell +.\scripts\remote-ai.ps1 ` + -Server mobai@192.168.1.100 ` + -Tool dsh ` + -Project my-project ` + -ToolArguments @("--profile", "headless", "运行测试") +``` + +## 推荐的工作方式 + +1. Windows Cursor 使用 Remote SSH 打开 Ubuntu 的 `/srv/projects/my-project`。 +2. Windows Terminal 使用 `remote-ai.ps1` 启动需要的 CLI。 +3. Cursor 和容器看到的是同一份 Ubuntu 项目文件。 +4. OpenClaw继续按现有方式运行,与本容器互不影响。 + +## 查看容器状态 + +```bash +cd /opt/ai-toolbox +docker compose ps +docker compose logs --tail=100 ai-tools +docker compose exec ai-tools ai-tools-check +``` + +## 停止和启动 + +```bash +docker compose stop +docker compose start +``` + +Codex、Claude 等任务 CLI 只在会话期间运行。容器常驻 Runner、Supervisor 和 Multica daemon;停止 `ai-tools` 容器会同时停止 daemon 和全部会话。配置保存在宿主机 `data` 目录,重新启动或重建容器后仍在。 diff --git a/proxy/Caddyfile b/proxy/Caddyfile new file mode 100644 index 0000000..8dac8be --- /dev/null +++ b/proxy/Caddyfile @@ -0,0 +1,14 @@ +:443 { + tls /etc/caddy/certs/agentdock.crt /etc/caddy/certs/agentdock.key + + @outside_lan not remote_ip 192.168.200.0/24 127.0.0.1/32 + respond @outside_lan "Forbidden" 403 + + encode zstd gzip + reverse_proxy ai-console:4173 + + header { + Strict-Transport-Security "max-age=31536000" + -Server + } +} diff --git a/runner/index.js b/runner/index.js new file mode 100644 index 0000000..04e0a79 --- /dev/null +++ b/runner/index.js @@ -0,0 +1,365 @@ +import crypto from "node:crypto"; +import { execFile, spawn } from "node:child_process"; +import { promisify } from "node:util"; +import fs from "node:fs/promises"; +import http from "node:http"; +import path from "node:path"; +import express from "express"; +import pty from "node-pty"; +import { WebSocketServer, WebSocket } from "ws"; +import { buildCommand, SECRET_ENV_ALLOWLIST, TOOLS, toolById } from "./registry.js"; +import { importCcLiveConfig, integrationSnapshot, switchCcProvider } from "./integrations.js"; +import { relayMulticaCallback } from "./multica-callback.js"; +import { listProjects, resolveWorkspacePath } from "./path-policy.js"; +import { SessionStore } from "./session-store.js"; + +const execFileAsync = promisify(execFile); +const PORT = Number(process.env.RUNNER_PORT || 4174); +const TOKEN = process.env.RUNNER_TOKEN || ""; +const WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || "/workspace"; +const SESSION_DIRECTORY = process.env.SESSION_DIRECTORY || "/home/ai/.ai-console/sessions"; + +if (TOKEN.length < 32) throw new Error("RUNNER_TOKEN must contain at least 32 characters"); + +const store = new SessionStore(SESSION_DIRECTORY); +await store.init(); + +function tokenMatches(candidate = "") { + const left = Buffer.from(candidate); + const right = Buffer.from(TOKEN); + return left.length === right.length && crypto.timingSafeEqual(left, right); +} + +function requireRunnerToken(req, res, next) { + const candidate = req.headers.authorization?.replace(/^Bearer\s+/i, "") || ""; + if (!tokenMatches(candidate)) return res.status(401).json({ error: "Unauthorized" }); + next(); +} + +function jsonError(res, error, status = 400) { + const message = error instanceof Error ? error.message : "Request failed"; + return res.status(status).json({ error: message }); +} + +async function commandVersion(command) { + try { + const { stdout, stderr } = await execFileAsync(command, ["--version"], { + timeout: 8000, + maxBuffer: 256 * 1024, + env: process.env, + }); + return { installed: true, version: (stdout || stderr).trim().split(/\r?\n/)[0] || "installed" }; + } catch (error) { + if (error.code === "ENOENT") return { installed: false, version: null }; + return { installed: true, version: "unknown", error: error.message }; + } +} + +async function processResources(pid) { + if (!pid) return { cpu: null, memoryBytes: null }; + try { + const { stdout } = await execFileAsync("ps", ["-p", String(pid), "-o", "%cpu=,rss="], { + timeout: 3000, + maxBuffer: 64 * 1024, + }); + const [cpu, rssKb] = stdout.trim().split(/\s+/); + return { + cpu: Number.isFinite(Number(cpu)) ? Number(cpu) : null, + memoryBytes: Number.isFinite(Number(rssKb)) ? Number(rssKb) * 1024 : null, + }; + } catch { + return { cpu: null, memoryBytes: null }; + } +} + +function activeSessionForTool(toolId) { + return store.list().find((session) => session.tool === toolId && session.status === "running"); +} + +function broadcast(session, message) { + const payload = JSON.stringify(message); + for (const client of session.clients) { + if (client.readyState === WebSocket.OPEN) client.send(payload); + } +} + +function appendOutput(session, data) { + if (!data) return; + store.append(session, data); + broadcast(session, { type: "output", data }); +} + +const app = express(); +app.disable("x-powered-by"); +app.use(express.json({ limit: "128kb" })); +app.get("/health", (_req, res) => res.json({ ok: true, service: "agentdock-runner" })); +app.use(requireRunnerToken); + +app.get("/tools", async (_req, res) => { + const records = await Promise.all(Object.values(TOOLS).filter((tool) => tool.visible !== false).map(async (tool) => { + const version = await commandVersion(tool.command); + const active = activeSessionForTool(tool.id); + const resources = active ? await processResources(active.pid) : { cpu: null, memoryBytes: null }; + return { + id: tool.id, + name: tool.name, + vendor: tool.vendor, + mono: tool.mono, + command: tool.command, + profiles: tool.profiles || [], + ...version, + status: active ? "running" : version.installed ? "stopped" : "error", + sessionId: active?.id || null, + project: active?.project || null, + resources, + }; + })); + res.json({ tools: records }); +}); + +app.get("/projects", async (_req, res) => { + try { + res.json({ projects: await listProjects(WORKSPACE_ROOT) }); + } catch (error) { + jsonError(res, error, 500); + } +}); + +app.get("/sessions", (_req, res) => res.json({ sessions: store.list() })); + +app.get("/integrations", async (req, res) => { + try { + res.json({ integrations: await integrationSnapshot({ force: req.query.force === "1" }) }); + } catch (error) { + jsonError(res, error, 500); + } +}); + +app.post("/integrations/cc-switch/:app/switch", async (req, res) => { + try { + await switchCcProvider(req.params.app, req.body?.providerId); + res.json({ integrations: await integrationSnapshot({ force: true }) }); + } catch (error) { + jsonError(res, error); + } +}); + +app.post("/integrations/cc-switch/:app/import-live", async (req, res) => { + try { + await importCcLiveConfig(req.params.app); + res.json({ integrations: await integrationSnapshot({ force: true }) }); + } catch (error) { + jsonError(res, error); + } +}); + +app.post("/integrations/multica/callback", async (req, res) => { + try { + res.json(await relayMulticaCallback(req.body?.callbackUrl)); + } catch (error) { + jsonError(res, error); + } +}); + +app.get("/sessions/:id/output", async (req, res) => { + const session = store.get(req.params.id); + if (!session) return res.status(404).json({ error: "Session not found" }); + res.type("text/plain").send(await store.readOutput(session)); +}); + +app.get("/sessions/:id/diff", async (req, res) => { + const session = store.get(req.params.id); + if (!session) return res.status(404).json({ error: "Session not found" }); + try { + const cwd = await resolveWorkspacePath(WORKSPACE_ROOT, session.project); + const { stdout } = await execFileAsync("git", ["diff", "--no-ext-diff", "--no-color"], { + cwd, + timeout: 10000, + maxBuffer: 2 * 1024 * 1024, + }); + res.type("text/plain").send(stdout || ""); + } catch (error) { + if (error.code === 1 && typeof error.stdout === "string") return res.type("text/plain").send(error.stdout); + jsonError(res, error, 500); + } +}); + +app.post("/sessions", async (req, res) => { + const tool = toolById(req.body?.tool); + if (!tool) return res.status(400).json({ error: "Unsupported CLI tool" }); + try { + const cwd = await resolveWorkspacePath(WORKSPACE_ROOT, req.body.project || "."); + const request = { + purpose: req.body.purpose === "authorization" ? "authorization" : "task", + profile: req.body.profile, + model: req.body.model, + initialInput: typeof req.body.initialInput === "string" ? req.body.initialInput.slice(0, 32000) : "", + serverUrl: typeof req.body.serverUrl === "string" ? req.body.serverUrl.trim().slice(0, 2048) : "", + appUrl: typeof req.body.appUrl === "string" ? req.body.appUrl.trim().slice(0, 2048) : "", + }; + const built = buildCommand(tool, request); + const environment = {}; + for (const [name, value] of Object.entries(req.body.environment || {})) { + if (SECRET_ENV_ALLOWLIST.has(name) && tool.secretNames.includes(name) && typeof value === "string") { + environment[name] = value; + } + } + const id = crypto.randomUUID(); + const processEnvironment = { + ...process.env, + ...environment, + TERM: built.interactive ? "xterm-256color" : "dumb", + COLORTERM: built.interactive ? "truecolor" : "", + NO_COLOR: built.interactive ? (process.env.NO_COLOR || "") : "1", + CI: built.interactive ? (process.env.CI || "") : "1", + }; + const baseRecord = { + id, + tool: tool.id, + toolName: tool.name, + project: path.relative(await fs.realpath(WORKSPACE_ROOT), cwd) || ".", + purpose: request.purpose, + initialInput: request.initialInput, + profile: request.profile || null, + model: typeof req.body.model === "string" ? req.body.model.slice(0, 128) : null, + status: "running", + createdAt: new Date().toISOString(), + endedAt: null, + exitCode: null, + signal: null, + interactive: built.interactive, + environment, + }; + let session; + + if (built.interactive) { + const processHandle = pty.spawn(built.command, built.args, { + name: "xterm-256color", + cols: Math.max(40, Math.min(300, Number(req.body.cols) || 120)), + rows: Math.max(12, Math.min(120, Number(req.body.rows) || 32)), + cwd, + env: processEnvironment, + }); + session = store.add({ ...baseRecord, pid: processHandle.pid, pty: processHandle }); + processHandle.onData((data) => appendOutput(session, data)); + processHandle.onExit(({ exitCode, signal }) => { + session.processExited = true; + const status = session.status === "stopped" ? "stopped" : exitCode === 0 ? "exited" : "failed"; + store.finish(session, status, exitCode, signal); + broadcast(session, { type: "exit", exitCode, signal, status }); + }); + } else { + const childProcess = spawn(built.command, built.args, { + cwd, + env: processEnvironment, + stdio: ["ignore", "pipe", "pipe"], + }); + session = store.add({ ...baseRecord, pid: childProcess.pid, childProcess }); + childProcess.stdout.setEncoding("utf8"); + childProcess.stderr.setEncoding("utf8"); + childProcess.stdout.on("data", (data) => appendOutput(session, data)); + childProcess.stderr.on("data", (data) => { + session.errorBuffer = `${session.errorBuffer}${data}`.slice(-256 * 1024); + broadcast(session, { type: "activity", label: "CLI 正在处理任务" }); + }); + childProcess.on("error", (error) => { + session.errorBuffer = error.message; + }); + childProcess.on("close", (exitCode, signal) => { + session.processExited = true; + const status = session.status === "stopped" ? "stopped" : exitCode === 0 ? "exited" : "failed"; + if (!session.buffer.trim()) { + const fallback = status === "failed" + ? session.errorBuffer.trim() || `CLI 退出,代码 ${exitCode ?? "unknown"}` + : "任务已完成,但 CLI 没有返回文本。"; + appendOutput(session, fallback); + } + store.finish(session, status, exitCode, signal); + broadcast(session, { type: "exit", exitCode, signal, status }); + }); + } + + res.status(201).json({ session: store.publicRecord(session) }); + } catch (error) { + jsonError(res, error); + } +}); + +app.post("/sessions/:id/input", (req, res) => { + const session = store.get(req.params.id); + if (!session) return res.status(404).json({ error: "Session not found" }); + if (session.status !== "running" || !session.pty || !session.interactive) return res.status(409).json({ error: "Only interactive sessions accept terminal input" }); + const data = typeof req.body?.data === "string" ? req.body.data.slice(0, 65536) : ""; + session.pty.write(data); + res.status(204).end(); +}); + +app.post("/sessions/:id/resize", (req, res) => { + const session = store.get(req.params.id); + if (!session) return res.status(404).json({ error: "Session not found" }); + if (session.status !== "running" || !session.pty || !session.interactive) return res.status(409).json({ error: "Only interactive sessions can be resized" }); + const cols = Math.max(40, Math.min(300, Number(req.body?.cols) || 120)); + const rows = Math.max(12, Math.min(120, Number(req.body?.rows) || 32)); + session.pty.resize(cols, rows); + res.status(204).end(); +}); + +app.post("/sessions/:id/stop", (req, res) => { + const session = store.get(req.params.id); + if (!session) return res.status(404).json({ error: "Session not found" }); + if (session.status === "running" && (session.pty || session.childProcess)) { + store.finish(session, "stopped"); + const handle = session.pty || session.childProcess; + handle.kill("SIGTERM"); + setTimeout(() => { + if (!session.processExited) handle.kill("SIGKILL"); + }, 5000).unref(); + } + res.status(204).end(); +}); + +const server = http.createServer(app); +const sockets = new WebSocketServer({ noServer: true }); +const socketHeartbeat = setInterval(() => { + for (const client of sockets.clients) { + if (client.readyState === WebSocket.OPEN) client.ping(); + } +}, 25_000); +socketHeartbeat.unref(); + +server.on("upgrade", (request, socket, head) => { + const url = new URL(request.url, "http://runner.internal"); + const match = url.pathname.match(/^\/ws\/sessions\/([a-f0-9-]+)$/); + if (!match || !tokenMatches(url.searchParams.get("token") || "")) { + socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n"); + return socket.destroy(); + } + const session = store.get(match[1]); + if (!session) { + socket.write("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n"); + return socket.destroy(); + } + sockets.handleUpgrade(request, socket, head, async (client) => { + session.clients.add(client); + client.send(JSON.stringify({ type: "snapshot", data: await store.readOutput(session), session: store.publicRecord(session) })); + client.on("message", (raw) => { + if (session.status !== "running" || !session.pty || !session.interactive) return; + try { + const message = JSON.parse(raw.toString()); + if (message.type === "input" && typeof message.data === "string") session.pty.write(message.data.slice(0, 65536)); + if (message.type === "resize") { + const cols = Math.max(40, Math.min(300, Number(message.cols) || 120)); + const rows = Math.max(12, Math.min(120, Number(message.rows) || 32)); + session.pty.resize(cols, rows); + } + } catch { + client.send(JSON.stringify({ type: "error", error: "Invalid WebSocket message" })); + } + }); + client.on("close", () => session.clients.delete(client)); + }); +}); + +server.listen(PORT, "0.0.0.0", () => { + console.log(`agentdock runner listening on ${PORT}`); +}); diff --git a/runner/integrations.js b/runner/integrations.js new file mode 100644 index 0000000..797733a --- /dev/null +++ b/runner/integrations.js @@ -0,0 +1,178 @@ +import { execFile } from "node:child_process"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); +const HOME = process.env.HOME || "/home/ai"; +const CC_SWITCH_DATABASE = path.join(HOME, ".cc-switch", "cc-switch.db"); +const ANSI = /\u001b\[[0-?]*[ -/]*[@-~]/g; +const SAFE_ID = /^[a-zA-Z0-9._-]{1,128}$/; + +export const CC_SWITCH_APPS = Object.freeze([ + { id: "claude", toolId: "claude", name: "Claude Code" }, + { id: "codex", toolId: "codex", name: "Codex" }, + { id: "open-code", toolId: "opencode", name: "OpenCode" }, +]); + +export const MULTICA_TOOL_IDS = Object.freeze(["claude", "codex", "codebuddy", "kimi", "opencode"]); + +function clean(value) { + return String(value || "").replace(ANSI, "").trim(); +} + +function publicEndpoint(value) { + const candidate = clean(value); + if (!candidate || candidate === "N/A") return null; + try { + const url = new URL(candidate); + return url.origin; + } catch { + return "configured"; + } +} + +export function parseCcProviderList(output) { + const providers = []; + let currentProviderId = null; + for (const rawLine of clean(output).split(/\r?\n/)) { + const line = rawLine.trim(); + const current = line.match(/^→\s*Current:\s*(\S+)/); + if (current) currentProviderId = current[1]; + if (!line.startsWith("│") || !line.includes("┆")) continue; + const columns = line.slice(1, line.endsWith("│") ? -1 : undefined).split("┆").map(clean); + if (columns.length < 4 || columns[1] === "ID") continue; + const [marker, id, name, endpoint] = columns; + if (!SAFE_ID.test(id)) continue; + providers.push({ + id, + name: name || id, + endpoint: publicEndpoint(endpoint), + active: marker.includes("✓"), + }); + } + if (!currentProviderId) currentProviderId = providers.find((provider) => provider.active)?.id || null; + return { currentProviderId, providers }; +} + +export function parseMulticaConfig(output) { + const values = {}; + for (const line of clean(output).split(/\r?\n/)) { + const match = line.match(/^([a-z_]+):\s*(.*)$/); + if (match) values[match[1]] = match[2] === "(not set)" ? null : match[2]; + } + return { + serverUrl: values.server_url || null, + appUrl: values.app_url || null, + workspaceId: values.workspace_id || null, + configured: Boolean(values.server_url && values.workspace_id), + }; +} + +async function run(command, args, options = {}) { + const { stdout, stderr } = await execFileAsync(command, args, { + timeout: options.timeout || 10000, + maxBuffer: options.maxBuffer || 1024 * 1024, + env: { ...process.env, NO_COLOR: "1", TERM: "dumb" }, + }); + return clean(stdout || stderr); +} + +async function versionOf(command, args = ["--version"]) { + try { + const output = await run(command, args, { timeout: 8000 }); + return { installed: true, version: output.split(/\r?\n/)[0] || "installed" }; + } catch (error) { + if (error.code === "ENOENT") return { installed: false, version: null }; + return { installed: true, version: "unknown", error: error.message }; + } +} + +async function ccSwitchInitialized() { + try { + await fs.access(CC_SWITCH_DATABASE); + return true; + } catch { + return false; + } +} + +async function ccSwitchSnapshot() { + const availability = await versionOf("cc-switch"); + const initialized = availability.installed && await ccSwitchInitialized(); + const apps = await Promise.all(CC_SWITCH_APPS.map(async (app) => { + if (!initialized) return { ...app, currentProviderId: null, providers: [] }; + try { + return { ...app, ...parseCcProviderList(await run("cc-switch", ["--app", app.id, "provider", "list"])) }; + } catch (error) { + return { ...app, currentProviderId: null, providers: [], error: error.message }; + } + })); + return { + ...availability, + initialized, + apps, + unsupportedToolIds: ["codebuddy", "kimi", "qwen", "dsh"], + }; +} + +async function multicaSnapshot() { + const availability = await versionOf("multica", ["version"]); + if (!availability.installed) { + return { ...availability, daemon: { status: "unavailable" }, config: { configured: false }, supportedToolIds: MULTICA_TOOL_IDS }; + } + let daemon = { status: "unknown" }; + let config = { configured: false, serverUrl: null, appUrl: null, workspaceId: null }; + try { + daemon = JSON.parse(await run("multica", ["daemon", "status", "--output", "json"])); + } catch (error) { + daemon = { status: "unknown", error: error.message }; + } + try { + config = parseMulticaConfig(await run("multica", ["config", "show"])); + } catch (error) { + config = { ...config, error: error.message }; + } + return { ...availability, daemon, config, supportedToolIds: MULTICA_TOOL_IDS }; +} + +let cachedSnapshot = null; +let cachedAt = 0; + +export async function integrationSnapshot({ force = false } = {}) { + if (!force && cachedSnapshot && Date.now() - cachedAt < 5000) return cachedSnapshot; + const [ccSwitch, multica] = await Promise.all([ccSwitchSnapshot(), multicaSnapshot()]); + cachedSnapshot = { ccSwitch, multica }; + cachedAt = Date.now(); + return cachedSnapshot; +} + +function validateCcSwitchApp(appId) { + if (!CC_SWITCH_APPS.some((app) => app.id === appId)) throw new Error("Unsupported CC Switch application"); +} + +let mutationQueue = Promise.resolve(); +function serializeMutation(operation) { + const result = mutationQueue.then(operation, operation); + mutationQueue = result.catch(() => {}); + return result; +} + +export async function switchCcProvider(appId, providerId) { + validateCcSwitchApp(appId); + if (!SAFE_ID.test(providerId || "")) throw new Error("Invalid provider ID"); + return serializeMutation(async () => { + const output = await run("cc-switch", ["--app", appId, "provider", "switch", providerId], { timeout: 30000 }); + cachedSnapshot = null; + return { ok: true, output: output.split(/\r?\n/).slice(0, 4).join("\n") }; + }); +} + +export async function importCcLiveConfig(appId) { + validateCcSwitchApp(appId); + return serializeMutation(async () => { + const output = await run("cc-switch", ["--app", appId, "provider", "import-live"], { timeout: 30000 }); + cachedSnapshot = null; + return { ok: true, output: output.split(/\r?\n/).slice(0, 6).join("\n") }; + }); +} diff --git a/runner/integrations.test.js b/runner/integrations.test.js new file mode 100644 index 0000000..fe90a22 --- /dev/null +++ b/runner/integrations.test.js @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { parseCcProviderList, parseMulticaConfig } from "./integrations.js"; + +test("parses CC Switch provider table without exposing URL paths", () => { + const result = parseCcProviderList(` +┌───┬────────────────┬─────────────────┬────────────────────────────┐ +│ ┆ ID ┆ Name ┆ API URL │ +╞═══╪════════════════╪═════════════════╪════════════════════════════╡ +│ ✓ ┆ codex-official ┆ OpenAI Official ┆ N/A │ +│ ┆ relay-one ┆ Relay One ┆ https://api.example/a/key │ +└───┴────────────────┴─────────────────┴────────────────────────────┘ +→ Current: codex-official +`); + assert.equal(result.currentProviderId, "codex-official"); + assert.deepEqual(result.providers, [ + { id: "codex-official", name: "OpenAI Official", endpoint: null, active: true }, + { id: "relay-one", name: "Relay One", endpoint: "https://api.example", active: false }, + ]); +}); + +test("parses Multica config and marks an authenticated workspace configured", () => { + const result = parseMulticaConfig(` +Config file: /home/ai/.multica/config.json +server_url: https://multica-api.example +app_url: https://multica.example +workspace_id: ws-123 +`); + assert.deepEqual(result, { + serverUrl: "https://multica-api.example", + appUrl: "https://multica.example", + workspaceId: "ws-123", + configured: true, + }); +}); diff --git a/runner/multica-callback.js b/runner/multica-callback.js new file mode 100644 index 0000000..26e42f5 --- /dev/null +++ b/runner/multica-callback.js @@ -0,0 +1,37 @@ +const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]); + +export function parseMulticaCallbackUrl(value) { + if (typeof value !== "string" || !value || value.length > 12_000) { + throw new Error("请粘贴完整的 Multica 回调链接"); + } + + let url; + try { + url = new URL(value); + } catch { + throw new Error("回调链接格式不正确"); + } + + const port = Number(url.port); + if (url.protocol !== "http:" || !LOOPBACK_HOSTS.has(url.hostname) || url.pathname !== "/callback") { + throw new Error("只接受 Multica 生成的 localhost 回调链接"); + } + if (!Number.isInteger(port) || port < 1024 || port > 65535) { + throw new Error("回调链接缺少有效端口"); + } + if (!url.searchParams.get("token") || !url.searchParams.get("state")) { + throw new Error("回调链接缺少 token 或 state"); + } + return url; +} + +export async function relayMulticaCallback(value, fetchImpl = fetch) { + const url = parseMulticaCallbackUrl(value); + const response = await fetchImpl(url, { + method: "GET", + redirect: "manual", + signal: AbortSignal.timeout(8_000), + }); + if (!response.ok) throw new Error(`Multica 回调未被接受 (${response.status})`); + return { ok: true }; +} diff --git a/runner/multica-callback.test.js b/runner/multica-callback.test.js new file mode 100644 index 0000000..349b712 --- /dev/null +++ b/runner/multica-callback.test.js @@ -0,0 +1,32 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { parseMulticaCallbackUrl, relayMulticaCallback } from "./multica-callback.js"; + +test("accepts a complete Multica loopback callback", () => { + const url = parseMulticaCallbackUrl("http://localhost:42957/callback?token=secret&state=nonce"); + assert.equal(url.port, "42957"); +}); + +test("rejects non-loopback callback targets", () => { + assert.throws( + () => parseMulticaCallbackUrl("https://example.com/callback?token=secret&state=nonce"), + /localhost/, + ); +}); + +test("rejects callbacks without OAuth parameters", () => { + assert.throws(() => parseMulticaCallbackUrl("http://localhost:42957/callback"), /token/); +}); + +test("relays the callback without exposing its response", async () => { + let receivedUrl; + const result = await relayMulticaCallback( + "http://127.0.0.1:42957/callback?token=secret&state=nonce", + async (url) => { + receivedUrl = url; + return { ok: true, status: 200 }; + }, + ); + assert.equal(receivedUrl.hostname, "127.0.0.1"); + assert.deepEqual(result, { ok: true }); +}); diff --git a/runner/package-lock.json b/runner/package-lock.json new file mode 100644 index 0000000..72946d5 --- /dev/null +++ b/runner/package-lock.json @@ -0,0 +1,924 @@ +{ + "name": "agentdock-runner", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agentdock-runner", + "version": "0.1.0", + "dependencies": { + "express": "^5.1.0", + "node-pty": "^1.1.0", + "ws": "^8.18.3" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-pty": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz", + "integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.1.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/runner/package.json b/runner/package.json new file mode 100644 index 0000000..67aafb6 --- /dev/null +++ b/runner/package.json @@ -0,0 +1,15 @@ +{ + "name": "agentdock-runner", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "start": "node index.js", + "test": "node --test" + }, + "dependencies": { + "express": "^5.1.0", + "node-pty": "^1.1.0", + "ws": "^8.18.3" + } +} diff --git a/runner/path-policy.js b/runner/path-policy.js new file mode 100644 index 0000000..ec79031 --- /dev/null +++ b/runner/path-policy.js @@ -0,0 +1,26 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +export async function resolveWorkspacePath(root, project = ".") { + if (typeof project !== "string" || project.includes("\0")) { + throw new Error("Invalid project path"); + } + const rootReal = await fs.realpath(root); + const candidate = path.resolve(rootReal, project || "."); + const candidateReal = await fs.realpath(candidate); + if (candidateReal !== rootReal && !candidateReal.startsWith(`${rootReal}${path.sep}`)) { + throw new Error("Project must be below /workspace"); + } + const stat = await fs.stat(candidateReal); + if (!stat.isDirectory()) throw new Error("Project is not a directory"); + return candidateReal; +} + +export async function listProjects(root) { + const rootReal = await fs.realpath(root); + const entries = await fs.readdir(rootReal, { withFileTypes: true }); + return entries + .filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")) + .map((entry) => entry.name) + .sort((a, b) => a.localeCompare(b)); +} diff --git a/runner/path-policy.test.js b/runner/path-policy.test.js new file mode 100644 index 0000000..dd8169f --- /dev/null +++ b/runner/path-policy.test.js @@ -0,0 +1,21 @@ +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { listProjects, resolveWorkspacePath } from "./path-policy.js"; + +test("workspace policy accepts child directories and rejects traversal", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "agentdock-workspace-")); + await fs.mkdir(path.join(root, "demo")); + assert.equal(await resolveWorkspacePath(root, "demo"), path.join(root, "demo")); + await assert.rejects(() => resolveWorkspacePath(root, "..")); +}); + +test("project listing hides dot directories", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "agentdock-projects-")); + await fs.mkdir(path.join(root, "zeta")); + await fs.mkdir(path.join(root, "alpha")); + await fs.mkdir(path.join(root, ".cache")); + assert.deepEqual(await listProjects(root), ["alpha", "zeta"]); +}); diff --git a/runner/registry.js b/runner/registry.js new file mode 100644 index 0000000..5ead908 --- /dev/null +++ b/runner/registry.js @@ -0,0 +1,168 @@ +export const TOOLS = Object.freeze({ + codex: { + id: "codex", + command: "codex", + name: "Codex", + vendor: "OpenAI", + mono: "CX", + modelFlag: "--model", + secretNames: ["OPENAI_API_KEY"], + }, + claude: { + id: "claude", + command: "claude", + name: "Claude Code", + vendor: "Anthropic", + mono: "CC", + modelFlag: "--model", + secretNames: ["ANTHROPIC_API_KEY"], + }, + codebuddy: { + id: "codebuddy", + command: "codebuddy", + name: "CodeBuddy", + vendor: "Tencent", + mono: "CB", + modelFlag: "--model", + secretNames: ["CODEBUDDY_API_KEY"], + }, + kimi: { + id: "kimi", + command: "kimi", + name: "Kimi CLI", + vendor: "Moonshot AI", + mono: "KM", + modelFlag: "--model", + secretNames: ["MOONSHOT_API_KEY"], + }, + opencode: { + id: "opencode", + command: "opencode", + name: "OpenCode", + vendor: "SST / Open source", + mono: "OC", + modelFlag: "--model", + secretNames: [ + "OPENAI_API_KEY", + "ANTHROPIC_API_KEY", + "DEEPSEEK_API_KEY", + "GOOGLE_GENERATIVE_AI_API_KEY", + ], + }, + qwen: { + id: "qwen", + command: "qwen", + name: "Qwen Code", + vendor: "Alibaba", + mono: "QW", + modelFlag: "--model", + secretNames: ["DASHSCOPE_API_KEY"], + }, + dsh: { + id: "dsh", + command: "dsh", + name: "DeepSeek Harness", + vendor: "DeepSeek", + mono: "DS", + secretNames: ["DEEPSEEK_API_KEY"], + profiles: ["headless"], + }, + ccswitch: { + id: "ccswitch", + command: "cc-switch", + name: "CC Switch", + vendor: "Community CLI", + mono: "CS", + secretNames: [], + visible: false, + integration: true, + }, + multica: { + id: "multica", + command: "multica-setup", + name: "Multica Runtime", + vendor: "Multica", + mono: "MU", + secretNames: [], + visible: false, + integration: true, + }, +}); + +export const SECRET_ENV_ALLOWLIST = new Set( + Object.values(TOOLS).flatMap((tool) => tool.secretNames), +); + +export function toolById(id) { + return typeof id === "string" ? TOOLS[id] : undefined; +} + +export function buildCommand(tool, request = {}) { + const args = []; + const input = request.initialInput?.trim() || ""; + + if (tool.integration) { + if (request.purpose !== "authorization") throw new Error("Integration tools only support setup sessions"); + if (tool.id === "multica") { + for (const [flag, value] of [["--server-url", request.serverUrl], ["--app-url", request.appUrl]]) { + if (!value) continue; + let url; + try { url = new URL(value); } catch { throw new Error(`${flag} must be a valid URL`); } + if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new Error(`${flag} must be an HTTP(S) URL without embedded credentials`); + args.push(flag, url.toString().replace(/\/$/, "")); + } + } + return { command: tool.command, args, interactive: true }; + } + + if (request.purpose === "task" && !input) { + throw new Error("A task message is required"); + } + + if (request.purpose === "task") { + switch (tool.id) { + case "codex": + args.push("exec", "--color", "never", "--skip-git-repo-check"); + break; + case "claude": + case "codebuddy": + args.push("--print", "--output-format", "text"); + break; + case "kimi": + args.push("--prompt", input, "--output-format", "text"); + break; + case "opencode": + args.push("run", "--format", "default"); + break; + case "qwen": + args.push("--prompt", input, "--output-format", "text"); + break; + case "dsh": + args.push("--profile", "headless", input); + return { command: tool.command, args, interactive: false }; + default: + throw new Error("Unsupported CLI task mode"); + } + + if (tool.modelFlag && typeof request.model === "string" && request.model.trim()) { + args.push(tool.modelFlag, request.model.trim().slice(0, 128)); + } + if (!["kimi", "qwen"].includes(tool.id)) args.push(input); + return { command: tool.command, args, interactive: false }; + } + + if (tool.id === "dsh") { + const profile = request.profile || "headless"; + if (!tool.profiles.includes(profile)) { + throw new Error("DeepSeek Harness profile must be headless or web"); + } + if (profile === "headless" && !request.initialInput?.trim()) { + throw new Error("DeepSeek Harness requires a task before starting"); + } + args.push("--profile", profile); + if (profile === "headless" && request.initialInput) args.push(request.initialInput); + } else if (tool.modelFlag && typeof request.model === "string" && request.model.trim()) { + args.push(tool.modelFlag, request.model.trim().slice(0, 128)); + } + return { command: tool.command, args, interactive: true }; +} diff --git a/runner/registry.test.js b/runner/registry.test.js new file mode 100644 index 0000000..15f2ca1 --- /dev/null +++ b/runner/registry.test.js @@ -0,0 +1,74 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { buildCommand, toolById } from "./registry.js"; + +test("DeepSeek Harness requires a headless task", () => { + const tool = toolById("dsh"); + assert.throws(() => buildCommand(tool, { profile: "headless", initialInput: "" }), /requires a task/); + assert.deepEqual( + buildCommand(tool, { profile: "headless", initialInput: "run tests" }), + { command: "dsh", args: ["--profile", "headless", "run tests"], interactive: true }, + ); +}); + +test("DeepSeek Harness rejects unavailable profiles", () => { + assert.throws(() => buildCommand(toolById("dsh"), { profile: "tui", initialInput: "run tests" }), /must be headless/); +}); + +test("task commands use non-interactive CLI modes", () => { + const task = { purpose: "task", initialInput: "review this", model: "test-model" }; + assert.deepEqual(buildCommand(toolById("codex"), task), { + command: "codex", + args: ["exec", "--color", "never", "--skip-git-repo-check", "--model", "test-model", "review this"], + interactive: false, + }); + assert.deepEqual(buildCommand(toolById("claude"), task), { + command: "claude", + args: ["--print", "--output-format", "text", "--model", "test-model", "review this"], + interactive: false, + }); + assert.deepEqual(buildCommand(toolById("kimi"), task), { + command: "kimi", + args: ["--prompt", "review this", "--output-format", "text", "--model", "test-model"], + interactive: false, + }); + assert.deepEqual(buildCommand(toolById("opencode"), task), { + command: "opencode", + args: ["run", "--format", "default", "--model", "test-model", "review this"], + interactive: false, + }); + assert.deepEqual(buildCommand(toolById("qwen"), task), { + command: "qwen", + args: ["--prompt", "review this", "--output-format", "text", "--model", "test-model"], + interactive: false, + }); + assert.deepEqual(buildCommand(toolById("dsh"), task), { + command: "dsh", + args: ["--profile", "headless", "review this"], + interactive: false, + }); +}); + +test("task commands require a message", () => { + assert.throws( + () => buildCommand(toolById("codex"), { purpose: "task", initialInput: "" }), + /task message is required/i, + ); +}); + +test("Multica setup accepts only HTTP URLs and stays interactive", () => { + const built = buildCommand(toolById("multica"), { + purpose: "authorization", + serverUrl: "https://api.example.test/", + appUrl: "https://app.example.test/", + }); + assert.deepEqual(built, { + command: "multica-setup", + args: ["--server-url", "https://api.example.test", "--app-url", "https://app.example.test"], + interactive: true, + }); + assert.throws(() => buildCommand(toolById("multica"), { + purpose: "authorization", + serverUrl: "file:///etc/passwd", + }), /HTTP\(S\)/); +}); diff --git a/runner/session-store.js b/runner/session-store.js new file mode 100644 index 0000000..def6409 --- /dev/null +++ b/runner/session-store.js @@ -0,0 +1,103 @@ +import fs from "node:fs"; +import fsp from "node:fs/promises"; +import path from "node:path"; + +const MAX_BUFFER_BYTES = 2 * 1024 * 1024; + +export class SessionStore { + constructor(directory) { + this.directory = directory; + this.sessions = new Map(); + } + + async init() { + await fsp.mkdir(this.directory, { recursive: true, mode: 0o700 }); + const files = await fsp.readdir(this.directory); + for (const file of files.filter((name) => name.endsWith(".json"))) { + try { + const record = JSON.parse(await fsp.readFile(path.join(this.directory, file), "utf8")); + if (record.status === "running") { + record.status = "interrupted"; + record.endedAt = new Date().toISOString(); + } + this.sessions.set(record.id, { ...record, archived: true, clients: new Set(), buffer: "" }); + } catch { + // A partial metadata file must not prevent the runner from starting. + } + } + } + + add(record) { + const session = { ...record, clients: new Set(), buffer: "", errorBuffer: "", archived: false }; + this.sessions.set(session.id, session); + this.persist(session); + return session; + } + + get(id) { + return this.sessions.get(id); + } + + publicRecord(session) { + const { + clients, + pty, + childProcess, + buffer, + errorBuffer, + logStream, + environment, + archived, + ...record + } = session; + return { ...record, connectedClients: clients?.size || 0, archived: Boolean(archived) }; + } + + list() { + return [...this.sessions.values()] + .map((session) => this.publicRecord(session)) + .sort((a, b) => b.createdAt.localeCompare(a.createdAt)); + } + + append(session, data) { + session.buffer += data; + if (Buffer.byteLength(session.buffer) > MAX_BUFFER_BYTES) { + session.buffer = session.buffer.slice(-MAX_BUFFER_BYTES); + } + if (!session.logStream) { + session.logStream = fs.createWriteStream(path.join(this.directory, `${session.id}.log`), { + flags: "a", + mode: 0o600, + }); + } + session.logStream.write(data); + } + + async readOutput(session) { + if (session.buffer) return session.buffer; + try { + return await fsp.readFile(path.join(this.directory, `${session.id}.log`), "utf8"); + } catch { + return ""; + } + } + + finish(session, status, exitCode = null, signal = null) { + session.status = status; + session.exitCode = exitCode; + session.signal = signal; + session.endedAt = new Date().toISOString(); + session.logStream?.end(); + session.logStream = undefined; + this.persist(session); + } + + persist(session) { + const target = path.join(this.directory, `${session.id}.json`); + const temp = `${target}.tmp`; + const body = `${JSON.stringify(this.publicRecord(session), null, 2)}\n`; + fsp.writeFile(temp, body, { mode: 0o600 }) + .then(() => fsp.rename(temp, target)) + .catch(() => {}); + } +} diff --git a/scripts/ai b/scripts/ai new file mode 100644 index 0000000..99a9ad0 --- /dev/null +++ b/scripts/ai @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_path="$(readlink -f -- "${BASH_SOURCE[0]}")" +script_dir="$(cd -- "$(dirname -- "${script_path}")" && pwd)" +deploy_dir="$(cd -- "${script_dir}/.." && pwd)" + +usage() { + cat <<'EOF' +Usage: + ./scripts/ai [project] [tool arguments...] + +Tools: + codex, claude, codebuddy, kimi, opencode, qwen, dsh + cc-switch, multica (management commands; no project argument) + +Examples: + ./scripts/ai codex my-project + ./scripts/ai claude my-project + ./scripts/ai kimi my-project -p "explain this project" + ./scripts/ai dsh my-project --profile headless "run the tests" + ./scripts/ai cc-switch + ./scripts/ai multica daemon status +EOF +} + +if [[ $# -lt 1 ]]; then + usage + exit 2 +fi + +tool="$1" +shift + +case "${tool}" in + codex|claude|codebuddy|kimi|opencode|qwen|dsh|cc-switch|multica) ;; + *) + printf 'Unsupported tool: %s\n\n' "${tool}" >&2 + usage >&2 + exit 2 + ;; +esac + +if [[ "${tool}" == "cc-switch" || "${tool}" == "multica" ]]; then + exec docker compose --project-directory "${deploy_dir}" exec ai-tools "${tool}" "$@" +fi + +project="." +if [[ $# -gt 0 && "$1" != -* ]]; then + project="$1" + shift +fi + +if [[ "${project}" == /* || "${project}" == *".."* ]]; then + printf 'Project must be a path below /workspace.\n' >&2 + exit 2 +fi + +exec docker compose --project-directory "${deploy_dir}" exec \ + --workdir "/workspace/${project}" ai-tools "${tool}" "$@" diff --git a/scripts/container-check.sh b/scripts/container-check.sh new file mode 100644 index 0000000..f4e8fe5 --- /dev/null +++ b/scripts/container-check.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -uo pipefail + +tools=(codex claude codebuddy kimi opencode qwen dsh cc-switch multica) +failed=0 + +printf '%-12s %s\n' "TOOL" "STATUS" +printf '%-12s %s\n' "------------" "------------------------------" + +for tool in "${tools[@]}"; do + if command -v "${tool}" >/dev/null 2>&1; then + printf '%-12s %s\n' "${tool}" "OK: $(command -v "${tool}")" + else + printf '%-12s %s\n' "${tool}" "MISSING" + failed=1 + fi +done + +printf '\nNode.js: %s\n' "$(node --version 2>/dev/null || printf 'MISSING')" +printf 'npm: %s\n' "$(npm --version 2>/dev/null || printf 'MISSING')" +printf 'Git: %s\n' "$(git --version 2>/dev/null || printf 'MISSING')" + +exit "${failed}" diff --git a/scripts/generate-console-certificate.sh b/scripts/generate-console-certificate.sh new file mode 100644 index 0000000..9789674 --- /dev/null +++ b/scripts/generate-console-certificate.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +deploy_dir="${1:-$(pwd)}" +certificate_dir="${deploy_dir}/proxy/certs" +certificate_file="${certificate_dir}/agentdock.crt" +key_file="${certificate_dir}/agentdock.key" + +mkdir -p "${certificate_dir}" +chmod 755 "${certificate_dir}" + +if [[ -f "${certificate_file}" && -f "${key_file}" ]]; then + printf 'Existing AgentDock certificate kept: %s\n' "${certificate_file}" + exit 0 +fi + +openssl req -x509 -nodes -newkey rsa:2048 -sha256 -days 3650 \ + -keyout "${key_file}" \ + -out "${certificate_file}" \ + -subj "/CN=192.168.200.36/O=AgentDock LAN" \ + -addext "subjectAltName=IP:192.168.200.36" \ + -addext "keyUsage=digitalSignature,keyEncipherment" \ + -addext "extendedKeyUsage=serverAuth" + +chmod 644 "${key_file}" +chmod 644 "${certificate_file}" +printf 'AgentDock certificate created: %s\n' "${certificate_file}" diff --git a/scripts/multica-runtime.sh b/scripts/multica-runtime.sh new file mode 100644 index 0000000..df0d89b --- /dev/null +++ b/scripts/multica-runtime.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ "${MULTICA_ENABLED:-true}" != "true" ]]; then + echo "multica runtime disabled by MULTICA_ENABLED" + exec sleep infinity +fi + +config_file="${HOME}/.multica/config.json" +while ! jq -e '.server_url and .workspace_id' "${config_file}" >/dev/null 2>&1; do + sleep 10 +done + +exec multica daemon start \ + --foreground \ + --no-auto-update \ + --max-concurrent-tasks "${MULTICA_DAEMON_MAX_CONCURRENT_TASKS:-2}" \ + --runtime-name "${MULTICA_AGENT_RUNTIME_NAME:-AgentDock Ubuntu Runtime}" diff --git a/scripts/multica-setup.sh b/scripts/multica-setup.sh new file mode 100644 index 0000000..7d42633 --- /dev/null +++ b/scripts/multica-setup.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +multica setup self-host "$@" + +# setup starts a detached daemon. Supervisor will immediately replace it with +# the foreground process used for container lifecycle and clean shutdown. +multica daemon stop >/dev/null 2>&1 || true diff --git a/scripts/prepare-console-env.sh b/scripts/prepare-console-env.sh new file mode 100644 index 0000000..222bf91 --- /dev/null +++ b/scripts/prepare-console-env.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +deploy_dir="${1:-$(pwd)}" +env_file="${deploy_dir}/.env" +password_file="${deploy_dir}/.agentdock-initial-password" + +if [[ ! -f "${env_file}" ]]; then + cp "${deploy_dir}/.env.example" "${env_file}" +fi + +chmod 600 "${env_file}" + +has_key() { + grep -q "^${1}=" "${env_file}" +} + +append_value() { + local key="$1" + local value="$2" + if ! has_key "${key}"; then + printf '\n%s=%s\n' "${key}" "${value}" >> "${env_file}" + fi +} + +if ! has_key "ADMIN_PASSWORD"; then + admin_password="AgentDock-$(openssl rand -hex 12)" + append_value "ADMIN_PASSWORD" "${admin_password}" + printf '%s\n' "${admin_password}" > "${password_file}" + chmod 600 "${password_file}" +fi + +append_value "RUNNER_TOKEN" "$(openssl rand -base64 48 | tr -d '\n')" +append_value "CONFIG_ENCRYPTION_KEY" "$(openssl rand -base64 48 | tr -d '\n')" +append_value "CONSOLE_HTTPS_PORT" "8443" +append_value "CONSOLE_DATA_PATH" "${deploy_dir}/console-data" +append_value "AI_TOOLS_IMAGE_TAG" "0.2.0" +append_value "CONSOLE_IMAGE_TAG" "0.1.0" +append_value "AI_TOOLS_MEMORY_LIMIT" "6g" +append_value "AI_TOOLS_CPU_LIMIT" "4.0" +append_value "CONSOLE_MEMORY_LIMIT" "512m" +append_value "CONSOLE_CPU_LIMIT" "1.0" + +mkdir -p "${deploy_dir}/console-data" +chmod 700 "${deploy_dir}/console-data" + +printf 'AgentDock console environment is ready.\n' +if [[ -f "${password_file}" ]]; then + printf 'Initial password file: %s\n' "${password_file}" +fi diff --git a/scripts/remote-ai.ps1 b/scripts/remote-ai.ps1 new file mode 100644 index 0000000..45becb4 --- /dev/null +++ b/scripts/remote-ai.ps1 @@ -0,0 +1,34 @@ +param( + [Parameter(Mandatory = $true)] + [string]$Server, + + [Parameter(Mandatory = $true)] + [ValidateSet("codex", "claude", "codebuddy", "kimi", "opencode", "qwen", "dsh")] + [string]$Tool, + + [string]$Project = ".", + + [string]$Container = "ai-tools", + + [string[]]$ToolArguments +) + +function ConvertTo-PosixArgument { + param([string]$Value) + return "'" + $Value.Replace("'", "'`"'`"'") + "'" +} + +if ($Project.StartsWith("/") -or $Project -match "(^|[\\/])\.\.([\\/]|$)") { + throw "Project must be a path below /workspace." +} + +$remoteArguments = @( + "docker", "exec", "-it", + "--workdir", "/workspace/$Project", + $Container, + $Tool +) + $ToolArguments + +$remoteCommand = ($remoteArguments | ForEach-Object { ConvertTo-PosixArgument $_ }) -join " " +& ssh -tt $Server $remoteCommand +exit $LASTEXITCODE diff --git a/scripts/runner-smoke.sh b/scripts/runner-smoke.sh new file mode 100644 index 0000000..5827a91 --- /dev/null +++ b/scripts/runner-smoke.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +base_url="http://127.0.0.1:${RUNNER_PORT:-4174}" +authorization="Authorization: Bearer ${RUNNER_TOKEN:?RUNNER_TOKEN is required}" + +response="$(curl -fsS \ + -H "${authorization}" \ + -H "Content-Type: application/json" \ + -d '{"tool":"codex","project":".","cols":100,"rows":30}' \ + "${base_url}/sessions")" +session_id="$(printf '%s' "${response}" | jq -r '.session.id')" +test -n "${session_id}" + +cleanup() { + curl -fsS -X POST \ + -H "${authorization}" \ + -H "Content-Type: application/json" \ + -d '{}' \ + "${base_url}/sessions/${session_id}/stop" >/dev/null || true +} +trap cleanup EXIT + +sleep 3 +curl -fsS -H "${authorization}" "${base_url}/sessions/${session_id}/output" | tail -c 500 +printf '\nSESSION=%s PTY_OK\n' "${session_id}" diff --git a/scripts/supervisord.conf b/scripts/supervisord.conf new file mode 100644 index 0000000..3f3e8bc --- /dev/null +++ b/scripts/supervisord.conf @@ -0,0 +1,31 @@ +[supervisord] +nodaemon=true +pidfile=/tmp/supervisord.pid +logfile=/dev/null +logfile_maxbytes=0 + +[program:runner] +command=node /opt/runner/index.js +priority=10 +autostart=true +autorestart=true +startsecs=2 +stopasgroup=true +killasgroup=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/fd/2 +stderr_logfile_maxbytes=0 + +[program:multica] +command=/usr/local/bin/multica-runtime +priority=20 +autostart=true +autorestart=true +startsecs=2 +stopasgroup=true +killasgroup=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/fd/2 +stderr_logfile_maxbytes=0 diff --git a/server/leefer.bashrc b/server/leefer.bashrc new file mode 100644 index 0000000..d1b24d2 --- /dev/null +++ b/server/leefer.bashrc @@ -0,0 +1,9 @@ +# Add user-installed commands to interactive non-login shells. +if [ -d "$HOME/bin" ]; then + case ":$PATH:" in + *":$HOME/bin:"*) ;; + *) PATH="$HOME/bin:$PATH" ;; + esac +fi + +export PATH diff --git a/server/leefer.profile b/server/leefer.profile new file mode 100644 index 0000000..3821fe9 --- /dev/null +++ b/server/leefer.profile @@ -0,0 +1,11 @@ +# Add user-installed commands to login shells. +if [ -d "$HOME/bin" ]; then + PATH="$HOME/bin:$PATH" +fi + +export PATH + +if [ -f "$HOME/.bashrc" ]; then + . "$HOME/.bashrc" +fi +