From f512bd58ec31c03335103f1c18a89a37bb88ac44 Mon Sep 17 00:00:00 2001 From: leefer Date: Mon, 24 Aug 2026 17:14:08 +0800 Subject: [PATCH] chore: archive AgentDock v1 implementation --- .dockerignore | 13 + .env.example | 40 + .gitattributes | 7 + .gitignore | 13 + Dockerfile | 123 + README.md | 80 + compose.yaml | 142 + console/.dockerignore | 4 + console/Dockerfile | 26 + console/index.html | 13 + console/package-lock.json | 2754 +++++++++++++++++ console/package.json | 30 + console/scripts/extract-design-css.mjs | 11 + console/server/auth.js | 73 + console/server/index.js | 291 ++ console/server/multica-oauth.js | 36 + console/server/multica-oauth.test.js | 24 + console/server/settings-store.js | 116 + console/server/settings-store.test.js | 38 + console/src/App.jsx | 555 ++++ console/src/api.js | 36 + console/src/app.css | 339 ++ .../src/components/AuthorizationTerminal.jsx | 77 + console/src/components/ChatConversation.jsx | 206 ++ console/src/components/IntegrationsView.jsx | 140 + console/src/components/LoginPage.jsx | 54 + console/src/components/SettingsView.jsx | 129 + console/src/components/TerminalPane.jsx | 132 + console/src/design/agent-console-v4.html | 1255 ++++++++ console/src/design/prototype.css | 401 +++ console/src/main.jsx | 15 + console/src/tooling.js | 64 + console/src/visual-polish.css | 329 ++ console/tests/mock-runner.mjs | 80 + console/vite.config.js | 23 + docs/CONSOLE-DEPLOYMENT.md | 117 + docs/DEPLOYMENT.md | 161 + docs/MAINTENANCE.md | 72 + docs/SERVER-192.168.200.36.md | 124 + docs/SOURCES.md | 17 + docs/TROUBLESHOOTING.md | 101 + docs/USAGE.md | 113 + proxy/Caddyfile | 14 + runner/index.js | 365 +++ runner/integrations.js | 178 ++ runner/integrations.test.js | 35 + runner/multica-callback.js | 37 + runner/multica-callback.test.js | 32 + runner/package-lock.json | 924 ++++++ runner/package.json | 15 + runner/path-policy.js | 26 + runner/path-policy.test.js | 21 + runner/registry.js | 168 + runner/registry.test.js | 74 + runner/session-store.js | 103 + scripts/ai | 60 + scripts/container-check.sh | 23 + scripts/generate-console-certificate.sh | 27 + scripts/multica-runtime.sh | 18 + scripts/multica-setup.sh | 8 + scripts/prepare-console-env.sh | 50 + scripts/remote-ai.ps1 | 34 + scripts/runner-smoke.sh | 26 + scripts/supervisord.conf | 31 + server/leefer.bashrc | 9 + server/leefer.profile | 11 + 66 files changed, 10663 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 compose.yaml create mode 100644 console/.dockerignore create mode 100644 console/Dockerfile create mode 100644 console/index.html create mode 100644 console/package-lock.json create mode 100644 console/package.json create mode 100644 console/scripts/extract-design-css.mjs create mode 100644 console/server/auth.js create mode 100644 console/server/index.js create mode 100644 console/server/multica-oauth.js create mode 100644 console/server/multica-oauth.test.js create mode 100644 console/server/settings-store.js create mode 100644 console/server/settings-store.test.js create mode 100644 console/src/App.jsx create mode 100644 console/src/api.js create mode 100644 console/src/app.css create mode 100644 console/src/components/AuthorizationTerminal.jsx create mode 100644 console/src/components/ChatConversation.jsx create mode 100644 console/src/components/IntegrationsView.jsx create mode 100644 console/src/components/LoginPage.jsx create mode 100644 console/src/components/SettingsView.jsx create mode 100644 console/src/components/TerminalPane.jsx create mode 100644 console/src/design/agent-console-v4.html create mode 100644 console/src/design/prototype.css create mode 100644 console/src/main.jsx create mode 100644 console/src/tooling.js create mode 100644 console/src/visual-polish.css create mode 100644 console/tests/mock-runner.mjs create mode 100644 console/vite.config.js create mode 100644 docs/CONSOLE-DEPLOYMENT.md create mode 100644 docs/DEPLOYMENT.md create mode 100644 docs/MAINTENANCE.md create mode 100644 docs/SERVER-192.168.200.36.md create mode 100644 docs/SOURCES.md create mode 100644 docs/TROUBLESHOOTING.md create mode 100644 docs/USAGE.md create mode 100644 proxy/Caddyfile create mode 100644 runner/index.js create mode 100644 runner/integrations.js create mode 100644 runner/integrations.test.js create mode 100644 runner/multica-callback.js create mode 100644 runner/multica-callback.test.js create mode 100644 runner/package-lock.json create mode 100644 runner/package.json create mode 100644 runner/path-policy.js create mode 100644 runner/path-policy.test.js create mode 100644 runner/registry.js create mode 100644 runner/registry.test.js create mode 100644 runner/session-store.js create mode 100644 scripts/ai create mode 100644 scripts/container-check.sh create mode 100644 scripts/generate-console-certificate.sh create mode 100644 scripts/multica-runtime.sh create mode 100644 scripts/multica-setup.sh create mode 100644 scripts/prepare-console-env.sh create mode 100644 scripts/remote-ai.ps1 create mode 100644 scripts/runner-smoke.sh create mode 100644 scripts/supervisord.conf create mode 100644 server/leefer.bashrc create mode 100644 server/leefer.profile 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 +