From cdfb892cbada5ddcda1b9db9981d2739eefafdff Mon Sep 17 00:00:00 2001 From: eleven_qy <64311644+alph-cmky@users.noreply.github.com> Date: Thu, 28 May 2026 17:07:39 +0800 Subject: [PATCH 01/13] chore: scaffold monorepo --- .gitignore | 3 +++ apps/daemon/package.json | 14 ++++++++++ apps/daemon/src/index.ts | 1 + apps/daemon/tsconfig.json | 9 +++++++ apps/server/package.json | 14 ++++++++++ apps/server/src/index.ts | 1 + apps/server/tsconfig.json | 9 +++++++ apps/web/package.json | 14 ++++++++++ apps/web/src/index.ts | 1 + apps/web/tsconfig.json | 9 +++++++ docs/development/task-backlog.md | 10 +++---- package.json | 20 ++++++++++++++ packages/mcp-server/package.json | 20 ++++++++++++++ packages/mcp-server/src/index.ts | 1 + packages/mcp-server/tsconfig.json | 9 +++++++ packages/runtime-adapters/package.json | 20 ++++++++++++++ packages/runtime-adapters/src/index.ts | 1 + packages/runtime-adapters/tsconfig.json | 9 +++++++ packages/workflow-core/package.json | 20 ++++++++++++++ packages/workflow-core/src/index.ts | 1 + packages/workflow-core/tsconfig.json | 9 +++++++ pnpm-lock.yaml | 36 +++++++++++++++++++++++++ pnpm-workspace.yaml | 3 +++ tsconfig.base.json | 17 ++++++++++++ 24 files changed, 246 insertions(+), 5 deletions(-) create mode 100644 apps/daemon/package.json create mode 100644 apps/daemon/src/index.ts create mode 100644 apps/daemon/tsconfig.json create mode 100644 apps/server/package.json create mode 100644 apps/server/src/index.ts create mode 100644 apps/server/tsconfig.json create mode 100644 apps/web/package.json create mode 100644 apps/web/src/index.ts create mode 100644 apps/web/tsconfig.json create mode 100644 package.json create mode 100644 packages/mcp-server/package.json create mode 100644 packages/mcp-server/src/index.ts create mode 100644 packages/mcp-server/tsconfig.json create mode 100644 packages/runtime-adapters/package.json create mode 100644 packages/runtime-adapters/src/index.ts create mode 100644 packages/runtime-adapters/tsconfig.json create mode 100644 packages/workflow-core/package.json create mode 100644 packages/workflow-core/src/index.ts create mode 100644 packages/workflow-core/tsconfig.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 tsconfig.base.json diff --git a/.gitignore b/.gitignore index aa1ec1e..2c92883 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ *.tgz +node_modules/ +dist/ +.worktrees/ diff --git a/apps/daemon/package.json b/apps/daemon/package.json new file mode 100644 index 0000000..7ed5dfd --- /dev/null +++ b/apps/daemon/package.json @@ -0,0 +1,14 @@ +{ + "name": "@agentdeck/daemon", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "echo \"@agentdeck/daemon dev server is not scaffolded yet\"", + "build": "tsc -p tsconfig.json", + "test": "echo \"@agentdeck/daemon tests are not scaffolded yet\"", + "lint": "echo \"@agentdeck/daemon lint is not scaffolded yet\"", + "typecheck": "tsc -p tsconfig.json --noEmit", + "format": "echo \"@agentdeck/daemon format is not scaffolded yet\"" + } +} diff --git a/apps/daemon/src/index.ts b/apps/daemon/src/index.ts new file mode 100644 index 0000000..02b56ea --- /dev/null +++ b/apps/daemon/src/index.ts @@ -0,0 +1 @@ +export const daemonAppName = "@agentdeck/daemon"; diff --git a/apps/daemon/tsconfig.json b/apps/daemon/tsconfig.json new file mode 100644 index 0000000..1155e6e --- /dev/null +++ b/apps/daemon/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/apps/server/package.json b/apps/server/package.json new file mode 100644 index 0000000..12a6a17 --- /dev/null +++ b/apps/server/package.json @@ -0,0 +1,14 @@ +{ + "name": "@agentdeck/server", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "echo \"@agentdeck/server dev server is not scaffolded yet\"", + "build": "tsc -p tsconfig.json", + "test": "echo \"@agentdeck/server tests are not scaffolded yet\"", + "lint": "echo \"@agentdeck/server lint is not scaffolded yet\"", + "typecheck": "tsc -p tsconfig.json --noEmit", + "format": "echo \"@agentdeck/server format is not scaffolded yet\"" + } +} diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts new file mode 100644 index 0000000..0feeded --- /dev/null +++ b/apps/server/src/index.ts @@ -0,0 +1 @@ +export const serverAppName = "@agentdeck/server"; diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json new file mode 100644 index 0000000..1155e6e --- /dev/null +++ b/apps/server/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..ea23393 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,14 @@ +{ + "name": "@agentdeck/web", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "echo \"@agentdeck/web dev server is not scaffolded yet\"", + "build": "tsc -p tsconfig.json", + "test": "echo \"@agentdeck/web tests are not scaffolded yet\"", + "lint": "echo \"@agentdeck/web lint is not scaffolded yet\"", + "typecheck": "tsc -p tsconfig.json --noEmit", + "format": "echo \"@agentdeck/web format is not scaffolded yet\"" + } +} diff --git a/apps/web/src/index.ts b/apps/web/src/index.ts new file mode 100644 index 0000000..81e1b23 --- /dev/null +++ b/apps/web/src/index.ts @@ -0,0 +1 @@ +export const webAppName = "@agentdeck/web"; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..1155e6e --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/docs/development/task-backlog.md b/docs/development/task-backlog.md index 8d85264..e7e4992 100644 --- a/docs/development/task-backlog.md +++ b/docs/development/task-backlog.md @@ -28,11 +28,11 @@ Status markers: **Tasks:** -- [ ] Add workspace scripts for `dev`, `build`, `test`, `lint`, `typecheck`, and `format`. -- [ ] Add package names using the `@agentdeck/*` namespace. -- [ ] Configure TypeScript strict mode in the shared base config. -- [ ] Verify `pnpm install` succeeds from the repo root. -- [ ] Commit with message `chore: scaffold monorepo`. +- [x] Add workspace scripts for `dev`, `build`, `test`, `lint`, `typecheck`, and `format`. +- [x] Add package names using the `@agentdeck/*` namespace. +- [x] Configure TypeScript strict mode in the shared base config. +- [x] Verify `pnpm install` succeeds from the repo root. +- [x] Commit with message `chore: scaffold monorepo`. **Acceptance Criteria:** diff --git a/package.json b/package.json new file mode 100644 index 0000000..da03c80 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "agentdeck", + "version": "0.0.0", + "private": true, + "description": "Local-first workspace for multi-agent coding and low-code orchestration.", + "license": "MIT", + "type": "module", + "packageManager": "pnpm@10.33.0", + "scripts": { + "dev": "pnpm -r --parallel dev", + "build": "pnpm -r build", + "test": "pnpm -r test", + "lint": "pnpm -r lint", + "typecheck": "pnpm -r typecheck", + "format": "pnpm -r format" + }, + "devDependencies": { + "typescript": "^5.9.3" + } +} diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json new file mode 100644 index 0000000..e314212 --- /dev/null +++ b/packages/mcp-server/package.json @@ -0,0 +1,20 @@ +{ + "name": "@agentdeck/mcp-server", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "dev": "tsc -p tsconfig.json --watch", + "build": "tsc -p tsconfig.json", + "test": "echo \"@agentdeck/mcp-server tests are not scaffolded yet\"", + "lint": "echo \"@agentdeck/mcp-server lint is not scaffolded yet\"", + "typecheck": "tsc -p tsconfig.json --noEmit", + "format": "echo \"@agentdeck/mcp-server format is not scaffolded yet\"" + } +} diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts new file mode 100644 index 0000000..b077d62 --- /dev/null +++ b/packages/mcp-server/src/index.ts @@ -0,0 +1 @@ +export const mcpServerPackageName = "@agentdeck/mcp-server"; diff --git a/packages/mcp-server/tsconfig.json b/packages/mcp-server/tsconfig.json new file mode 100644 index 0000000..1155e6e --- /dev/null +++ b/packages/mcp-server/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/runtime-adapters/package.json b/packages/runtime-adapters/package.json new file mode 100644 index 0000000..7f9019f --- /dev/null +++ b/packages/runtime-adapters/package.json @@ -0,0 +1,20 @@ +{ + "name": "@agentdeck/runtime-adapters", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "dev": "tsc -p tsconfig.json --watch", + "build": "tsc -p tsconfig.json", + "test": "echo \"@agentdeck/runtime-adapters tests are not scaffolded yet\"", + "lint": "echo \"@agentdeck/runtime-adapters lint is not scaffolded yet\"", + "typecheck": "tsc -p tsconfig.json --noEmit", + "format": "echo \"@agentdeck/runtime-adapters format is not scaffolded yet\"" + } +} diff --git a/packages/runtime-adapters/src/index.ts b/packages/runtime-adapters/src/index.ts new file mode 100644 index 0000000..7cf79dd --- /dev/null +++ b/packages/runtime-adapters/src/index.ts @@ -0,0 +1 @@ +export const runtimeAdaptersPackageName = "@agentdeck/runtime-adapters"; diff --git a/packages/runtime-adapters/tsconfig.json b/packages/runtime-adapters/tsconfig.json new file mode 100644 index 0000000..1155e6e --- /dev/null +++ b/packages/runtime-adapters/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/workflow-core/package.json b/packages/workflow-core/package.json new file mode 100644 index 0000000..60782b6 --- /dev/null +++ b/packages/workflow-core/package.json @@ -0,0 +1,20 @@ +{ + "name": "@agentdeck/workflow-core", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "dev": "tsc -p tsconfig.json --watch", + "build": "tsc -p tsconfig.json", + "test": "echo \"@agentdeck/workflow-core tests are not scaffolded yet\"", + "lint": "echo \"@agentdeck/workflow-core lint is not scaffolded yet\"", + "typecheck": "tsc -p tsconfig.json --noEmit", + "format": "echo \"@agentdeck/workflow-core format is not scaffolded yet\"" + } +} diff --git a/packages/workflow-core/src/index.ts b/packages/workflow-core/src/index.ts new file mode 100644 index 0000000..6db5932 --- /dev/null +++ b/packages/workflow-core/src/index.ts @@ -0,0 +1 @@ +export const workflowCorePackageName = "@agentdeck/workflow-core"; diff --git a/packages/workflow-core/tsconfig.json b/packages/workflow-core/tsconfig.json new file mode 100644 index 0000000..1155e6e --- /dev/null +++ b/packages/workflow-core/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..3ea1e10 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,36 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + apps/daemon: {} + + apps/server: {} + + apps/web: {} + + packages/mcp-server: {} + + packages/runtime-adapters: {} + + packages/workflow-core: {} + +packages: + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + +snapshots: + + typescript@5.9.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..3ff5faa --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "apps/*" + - "packages/*" diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..350b823 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "isolatedModules": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + } +} From cfe4bc0c6c856ca750e36135a55637d4a788423b Mon Sep 17 00:00:00 2001 From: eleven_qy <64311644+alph-cmky@users.noreply.github.com> Date: Thu, 28 May 2026 19:28:27 +0800 Subject: [PATCH 02/13] chore: add code quality tooling --- .editorconfig | 12 + .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/pull_request_template.md | 6 +- .prettierignore | 5 + .prettierrc | 6 + AGENTDECK_PRODUCT_DESIGN.md | 40 +- agentdeck-project-brief.md | 19 +- docs/development/README.md | 26 +- docs/development/task-backlog.md | 12 +- eslint.config.js | 39 + package.json | 17 +- pnpm-lock.yaml | 3178 ++++++++++++++++++++++++- vitest.config.ts | 9 + 13 files changed, 3313 insertions(+), 58 deletions(-) create mode 100644 .editorconfig create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 eslint.config.js create mode 100644 vitest.config.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8c52ff9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 34496b6..98af607 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -22,7 +22,7 @@ body: placeholder: | 1. 2. - 3. + 3. validations: required: true - type: textarea diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8898f75..0805b9c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,10 +1,10 @@ ## Summary -- +- ## Related Task or Issue -- +- ## Verification @@ -21,7 +21,7 @@ Commands run: Notes: -- +- ## UI Impact diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..79d809f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +node_modules +dist +coverage +.worktrees +pnpm-lock.yaml diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..90abee2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 100, + "semi": true, + "singleQuote": false, + "trailingComma": "all" +} diff --git a/AGENTDECK_PRODUCT_DESIGN.md b/AGENTDECK_PRODUCT_DESIGN.md index 221de67..10ba8cf 100644 --- a/AGENTDECK_PRODUCT_DESIGN.md +++ b/AGENTDECK_PRODUCT_DESIGN.md @@ -68,15 +68,15 @@ Slock-like 多 Agent 协作空间 截至 2026-05-27,社区里有很多可借鉴组件,但没有一个成熟开源方案完整覆盖 AgentDeck 的设想。 -| 方向 | 代表方案 | 可借鉴点 | 不足 | -| --- | --- | --- | --- | -| 多 Agent 协作空间 | Slock | Channel / DM / thread、多 Agent、local daemon、持久记忆 | 低代码编排弱,完整开源生态不明确 | -| 低代码 Agent 平台 | Coze Studio | Agent、Workflow、Plugin、RAG、可视化低代码 | 架构较重,偏平台,不是本地 coding-agent workspace | -| LLM App 平台 | Dify | 应用、Agent、Workflow、RAG、插件、API 发布 | 更偏 AI 应用发布,不偏本地代码协作 | -| 可视化 Agent 工作流 | Flowise / Langflow | Chatflow、Agentflow、可视化搭建、API/SDK/CLI | 本地 coding agent 与安全改图能力不足 | -| 多 Agent 框架 | CrewAI / AutoGen Studio / LangGraph | 多 Agent 协作、任务分发、状态图、调试 | 更偏框架或实验台,不是完整产品 | -| coding agent runtime | OpenHands / Aider / Claude Code / Codex CLI | 代码修改、命令执行、仓库任务、沙箱能力 | 缺少 Coze-like 低代码工作流产品体验 | -| 协议层 | MCP | 标准化 tools/resources/prompts,可做 Agent 修改系统的安全工具层 | 本身不是应用框架 | +| 方向 | 代表方案 | 可借鉴点 | 不足 | +| -------------------- | ------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------- | +| 多 Agent 协作空间 | Slock | Channel / DM / thread、多 Agent、local daemon、持久记忆 | 低代码编排弱,完整开源生态不明确 | +| 低代码 Agent 平台 | Coze Studio | Agent、Workflow、Plugin、RAG、可视化低代码 | 架构较重,偏平台,不是本地 coding-agent workspace | +| LLM App 平台 | Dify | 应用、Agent、Workflow、RAG、插件、API 发布 | 更偏 AI 应用发布,不偏本地代码协作 | +| 可视化 Agent 工作流 | Flowise / Langflow | Chatflow、Agentflow、可视化搭建、API/SDK/CLI | 本地 coding agent 与安全改图能力不足 | +| 多 Agent 框架 | CrewAI / AutoGen Studio / LangGraph | 多 Agent 协作、任务分发、状态图、调试 | 更偏框架或实验台,不是完整产品 | +| coding agent runtime | OpenHands / Aider / Claude Code / Codex CLI | 代码修改、命令执行、仓库任务、沙箱能力 | 缺少 Coze-like 低代码工作流产品体验 | +| 协议层 | MCP | 标准化 tools/resources/prompts,可做 Agent 修改系统的安全工具层 | 本身不是应用框架 | ### Slock 是否开源 @@ -423,17 +423,17 @@ sequenceDiagram ## 13. 用户路径梳理 -| 阶段 | 用户动作 | 系统响应 | 关键产物 | -| --- | --- | --- | --- | -| 初始化 | 打开 AgentDeck | 探测 Codex / Claude / Ollama / Node / Git | Runtime 状态 | -| 建立工作区 | 选择项目目录 | 绑定 workspaceRoot,读取项目 marker | Workspace | -| 创建 Agent | 设置名称、prompt、模型、权限 | 保存 AgentDefinition | Agent Registry | -| 发起任务 | 在 Chat 中 `@agent` | 创建 task,启动本地 runtime | Task Stream | -| 查看执行 | 看日志、状态、diff、产物 | 实时事件流回传 | Execution Trace | -| 编排流程 | 打开 Workflow Canvas | 创建 Start / Agent / Tool / Condition / Human Approval / End | WorkflowDefinition | -| 自然语言改流程 | 在 Chat 中描述修改 | Coding agent 生成 patch proposal | PatchProposal | -| 审批发布 | 查看 diff 并确认 | 校验、apply、生成版本 | 新 workflow version | -| 回滚审计 | 查看历史修改 | 展示谁改了什么、何时改、为何改 | Audit Log / Rollback | +| 阶段 | 用户动作 | 系统响应 | 关键产物 | +| -------------- | ---------------------------- | ------------------------------------------------------------ | -------------------- | +| 初始化 | 打开 AgentDeck | 探测 Codex / Claude / Ollama / Node / Git | Runtime 状态 | +| 建立工作区 | 选择项目目录 | 绑定 workspaceRoot,读取项目 marker | Workspace | +| 创建 Agent | 设置名称、prompt、模型、权限 | 保存 AgentDefinition | Agent Registry | +| 发起任务 | 在 Chat 中 `@agent` | 创建 task,启动本地 runtime | Task Stream | +| 查看执行 | 看日志、状态、diff、产物 | 实时事件流回传 | Execution Trace | +| 编排流程 | 打开 Workflow Canvas | 创建 Start / Agent / Tool / Condition / Human Approval / End | WorkflowDefinition | +| 自然语言改流程 | 在 Chat 中描述修改 | Coding agent 生成 patch proposal | PatchProposal | +| 审批发布 | 查看 diff 并确认 | 校验、apply、生成版本 | 新 workflow version | +| 回滚审计 | 查看历史修改 | 展示谁改了什么、何时改、为何改 | Audit Log / Rollback | ## 14. MVP 用户故事 diff --git a/agentdeck-project-brief.md b/agentdeck-project-brief.md index 81173d9..1b82a01 100644 --- a/agentdeck-project-brief.md +++ b/agentdeck-project-brief.md @@ -59,15 +59,15 @@ Slock-like 多 Agent 协作空间 截至 2026-05-27,社区里有很多可借鉴组件,但没有一个完整覆盖本项目设想的成熟开源方案。 -| 方向 | 代表方案 | 可借鉴点 | 不足 | -| --- | --- | --- | --- | -| 多 Agent 协作空间 | Slock | Channel / DM / thread、多 Agent、local daemon、持久记忆 | 低代码编排弱,完整开源生态不明确 | -| 低代码 Agent 平台 | Coze Studio | Agent、Workflow、Plugin、RAG、可视化低代码 | 架构较重,偏平台,不是本地 coding-agent workspace | -| LLM App 平台 | Dify | 应用、Agent、Workflow、RAG、插件、API 发布 | 更偏 AI 应用发布,不偏本地代码协作 | -| 可视化 Agent 工作流 | Flowise / Langflow | Chatflow、Agentflow、可视化搭建、API/SDK/CLI | 本地 coding agent 与安全改图能力不足 | -| 多 Agent 框架 | CrewAI / AutoGen Studio / LangGraph | 多 Agent 协作、任务分发、状态图、调试 | 更偏框架或实验台,不是完整产品 | -| coding agent runtime | OpenHands / Aider / Claude Code / Codex CLI | 代码修改、命令执行、仓库任务、沙箱能力 | 缺少 Coze-like 低代码工作流产品体验 | -| 协议层 | MCP | 标准化 tools/resources/prompts,可做 Agent 修改系统的安全工具层 | 本身不是应用框架 | +| 方向 | 代表方案 | 可借鉴点 | 不足 | +| -------------------- | ------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------- | +| 多 Agent 协作空间 | Slock | Channel / DM / thread、多 Agent、local daemon、持久记忆 | 低代码编排弱,完整开源生态不明确 | +| 低代码 Agent 平台 | Coze Studio | Agent、Workflow、Plugin、RAG、可视化低代码 | 架构较重,偏平台,不是本地 coding-agent workspace | +| LLM App 平台 | Dify | 应用、Agent、Workflow、RAG、插件、API 发布 | 更偏 AI 应用发布,不偏本地代码协作 | +| 可视化 Agent 工作流 | Flowise / Langflow | Chatflow、Agentflow、可视化搭建、API/SDK/CLI | 本地 coding agent 与安全改图能力不足 | +| 多 Agent 框架 | CrewAI / AutoGen Studio / LangGraph | 多 Agent 协作、任务分发、状态图、调试 | 更偏框架或实验台,不是完整产品 | +| coding agent runtime | OpenHands / Aider / Claude Code / Codex CLI | 代码修改、命令执行、仓库任务、沙箱能力 | 缺少 Coze-like 低代码工作流产品体验 | +| 协议层 | MCP | 标准化 tools/resources/prompts,可做 Agent 修改系统的安全工具层 | 本身不是应用框架 | 结论:最优策略不是直接 fork Coze/Dify,而是借鉴它们的模型,自研本地 daemon、Chat 协作层和 workflow patch 安全层。 @@ -529,4 +529,3 @@ Phase 5: Memory、Audit、Plugin/MCP 市场 - OpenAI Codex CLI: https://developers.openai.com/codex/cli - Ollama Codex integration: https://docs.ollama.com/integrations/codex - Model Context Protocol: https://modelcontextprotocol.wiki/en/introduction - diff --git a/docs/development/README.md b/docs/development/README.md index cf2b6cc..671c964 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -27,19 +27,19 @@ ## Phase Map -| Phase | Name | Outcome | -| --- | --- | --- | -| 0 | Repository Foundation | Monorepo scaffold, tooling, docs, CI-quality local commands | -| 1 | Runtime Detector | Detect Codex, Claude, Node, Git, Ollama, LM Studio with safe metadata collection | -| 2 | Local Daemon | Run local detection and stream events to the app/server | -| 3 | Agent Registry | Create, edit, validate, and persist agent definitions with permissions | -| 4 | Chat Workspace | Channels, messages, `@agent` mentions, task creation, task status stream | -| 5 | Runtime Adapters | Codex/Claude/shell adapter contracts and first executable task path | -| 6 | Workflow Core | DAG schema, node types, validation, versioning, execution trace model | -| 7 | Workflow Canvas | Visual DAG editor with node inspector and validation feedback | -| 8 | Patch Approval | Chat-generated workflow/agent patch proposals, diff preview, approval, rollback | -| 9 | Security & Audit | Permission enforcement, redaction, audit log, safe command policy | -| 10 | Packaging & Examples | Local dev setup, sample agents/workflows, contributor docs | +| Phase | Name | Outcome | +| ----- | --------------------- | -------------------------------------------------------------------------------- | +| 0 | Repository Foundation | Monorepo scaffold, tooling, docs, CI-quality local commands | +| 1 | Runtime Detector | Detect Codex, Claude, Node, Git, Ollama, LM Studio with safe metadata collection | +| 2 | Local Daemon | Run local detection and stream events to the app/server | +| 3 | Agent Registry | Create, edit, validate, and persist agent definitions with permissions | +| 4 | Chat Workspace | Channels, messages, `@agent` mentions, task creation, task status stream | +| 5 | Runtime Adapters | Codex/Claude/shell adapter contracts and first executable task path | +| 6 | Workflow Core | DAG schema, node types, validation, versioning, execution trace model | +| 7 | Workflow Canvas | Visual DAG editor with node inspector and validation feedback | +| 8 | Patch Approval | Chat-generated workflow/agent patch proposals, diff preview, approval, rollback | +| 9 | Security & Audit | Permission enforcement, redaction, audit log, safe command policy | +| 10 | Packaging & Examples | Local dev setup, sample agents/workflows, contributor docs | ## Recommended Build Order diff --git a/docs/development/task-backlog.md b/docs/development/task-backlog.md index e7e4992..8444757 100644 --- a/docs/development/task-backlog.md +++ b/docs/development/task-backlog.md @@ -53,12 +53,12 @@ Status markers: **Tasks:** -- [ ] Configure Prettier for Markdown, TypeScript, JSON, and CSS. -- [ ] Configure ESLint for TypeScript and React. -- [ ] Configure Vitest for package-level unit tests. -- [ ] Add root scripts for `lint`, `format`, `test`, and `typecheck`. -- [ ] Verify all scripts run successfully on the scaffold. -- [ ] Commit with message `chore: add code quality tooling`. +- [x] Configure Prettier for Markdown, TypeScript, JSON, and CSS. +- [x] Configure ESLint for TypeScript and React. +- [x] Configure Vitest for package-level unit tests. +- [x] Add root scripts for `lint`, `format`, `test`, and `typecheck`. +- [x] Verify all scripts run successfully on the scaffold. +- [x] Commit with message `chore: add code quality tooling`. **Acceptance Criteria:** diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..01e86e1 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,39 @@ +import js from "@eslint/js"; +import react from "eslint-plugin-react"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { + ignores: ["**/dist/**", "**/node_modules/**", "**/coverage/**", ".worktrees/**"], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + }, + }, + { + files: ["apps/web/**/*.{ts,tsx}"], + plugins: { + react, + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + settings: { + react: { + version: "19.0", + }, + }, + rules: { + ...react.configs.recommended.rules, + ...reactHooks.configs.recommended.rules, + "react/react-in-jsx-scope": "off", + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + }, + }, +); diff --git a/package.json b/package.json index da03c80..e5d37f4 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,21 @@ "scripts": { "dev": "pnpm -r --parallel dev", "build": "pnpm -r build", - "test": "pnpm -r test", - "lint": "pnpm -r lint", + "test": "vitest run --passWithNoTests", + "lint": "eslint .", "typecheck": "pnpm -r typecheck", - "format": "pnpm -r format" + "format": "prettier --check .", + "format:write": "prettier --write ." }, "devDependencies": { - "typescript": "^5.9.3" + "@eslint/js": "^9.39.4", + "eslint": "^9.39.4", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "prettier": "^3.8.3", + "typescript": "^5.9.3", + "typescript-eslint": "^8.60.0", + "vitest": "^4.1.7" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ea1e10..e246de2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,33 @@ importers: .: devDependencies: + '@eslint/js': + specifier: ^9.39.4 + version: 9.39.4 + eslint: + specifier: ^9.39.4 + version: 9.39.4 + eslint-plugin-react: + specifier: ^7.37.5 + version: 7.37.5(eslint@9.39.4) + eslint-plugin-react-hooks: + specifier: ^7.1.1 + version: 7.1.1(eslint@9.39.4) + eslint-plugin-react-refresh: + specifier: ^0.5.2 + version: 0.5.2(eslint@9.39.4) + prettier: + specifier: ^3.8.3 + version: 3.8.3 typescript: specifier: ^5.9.3 version: 5.9.3 + typescript-eslint: + specifier: ^8.60.0 + version: 8.60.0(eslint@9.39.4)(typescript@5.9.3) + vitest: + specifier: ^4.1.7 + version: 4.1.7(vite@8.0.14) apps/daemon: {} @@ -26,11 +50,3163 @@ importers: packages: + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.132.0': + resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} + + '@rolldown/binding-android-arm64@1.0.2': + resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.2': + resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.2': + resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.2': + resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.2': + resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.2': + resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.2': + resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.2': + resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.2': + resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.2': + resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.2': + resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.2': + resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.2': + resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.2': + resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@typescript-eslint/eslint-plugin@8.60.0': + resolution: {integrity: sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.60.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.60.0': + resolution: {integrity: sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.60.0': + resolution: {integrity: sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.60.0': + resolution: {integrity: sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.60.0': + resolution: {integrity: sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.60.0': + resolution: {integrity: sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.60.0': + resolution: {integrity: sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.60.0': + resolution: {integrity: sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.60.0': + resolution: {integrity: sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.60.0': + resolution: {integrity: sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitest/expect@4.1.7': + resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} + + '@vitest/mocker@4.1.7': + resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.7': + resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} + + '@vitest/runner@4.1.7': + resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} + + '@vitest/snapshot@4.1.7': + resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} + + '@vitest/spy@4.1.7': + resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} + + '@vitest/utils@4.1.7': + resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.32: + resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==} + engines: {node: '>=6.0.0'} + hasBin: true + + brace-expansion@1.1.15: + resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.363: + resolution: {integrity: sha512-VjUKPyWzGnT1fujlkEGC/BvN70Hh70KXtAqcmniXviYlJC/ivcT+BWGPyxWVbJZLfvtKR6dqg1L7T7pgAMBtWA==} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.3.2: + resolution: {integrity: sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react-refresh@0.5.2: + resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} + peerDependencies: + eslint: ^9 || ^10 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-exports-info@1.6.0: + resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + engines: {node: '>= 0.4'} + + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} + engines: {node: '>= 0.4'} + hasBin: true + + rolldown@1.0.2: + resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.2: + resolution: {integrity: sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript-eslint@8.60.0: + resolution: {integrity: sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true -snapshots: + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@8.0.14: + resolution: {integrity: sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.7: + resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.7 + '@vitest/browser-preview': 4.1.7 + '@vitest/browser-webdriverio': 4.1.7 + '@vitest/coverage-istanbul': 4.1.7 + '@vitest/coverage-v8': 4.1.7 + '@vitest/ui': 4.1.7 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.21: + resolution: {integrity: sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + 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/core@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.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + dependencies: + eslint: 9.39.4 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@oxc-project/types@0.132.0': {} + + '@rolldown/binding-android-arm64@1.0.2': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.2': + optional: true + + '@rolldown/binding-darwin-x64@1.0.2': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.2': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.2': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.2': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.2': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.2': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.2': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.2': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.60.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/type-utils': 8.60.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.60.0 + eslint: 9.39.4 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.60.0(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.60.0 + debug: 4.4.3 + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.60.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@5.9.3) + '@typescript-eslint/types': 8.60.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.60.0': + dependencies: + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/visitor-keys': 8.60.0 + + '@typescript-eslint/tsconfig-utils@8.60.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.60.0(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.0(eslint@9.39.4)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.60.0': {} + + '@typescript-eslint/typescript-estree@8.60.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.60.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@5.9.3) + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/visitor-keys': 8.60.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.1 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.60.0(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.60.0': + dependencies: + '@typescript-eslint/types': 8.60.0 + eslint-visitor-keys: 5.0.1 + + '@vitest/expect@4.1.7': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.7(vite@8.0.14)': + dependencies: + '@vitest/spy': 4.1.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.14 + + '@vitest/pretty-format@4.1.7': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.7': + dependencies: + '@vitest/utils': 4.1.7 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.7': + dependencies: + '@vitest/pretty-format': 4.1.7 + '@vitest/utils': 4.1.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.7': {} + + '@vitest/utils@4.1.7': + dependencies: + '@vitest/pretty-format': 4.1.7 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + assertion-error@2.0.1: {} + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.32: {} + + brace-expansion@1.1.15: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.32 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.363 + node-releases: 2.0.46 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001793: {} + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + detect-libc@2.1.2: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.363: {} + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.21 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.3.2: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 + + es-module-lexer@2.1.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.3 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@7.1.1(eslint@9.39.4): + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + eslint: 9.39.4 + hermes-parser: 0.25.1 + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.5.2(eslint@9.39.4): + dependencies: + eslint: 9.39.4 + + eslint-plugin-react@7.37.5(eslint@9.39.4): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.3.2 + eslint: 9.39.4 + estraverse: 5.3.0 + hasown: 2.0.3 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.7 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + expect-type@1.3.0: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.3 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.3 + side-channel: 1.1.0 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.3 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.21 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.15 + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + natural-compare@1.4.0: {} + + node-exports-info@1.6.0: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + node-releases@2.0.46: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + obug@2.1.1: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + possible-typed-array-names@1.1.0: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@3.8.3: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + react-is@16.13.1: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + resolve-from@4.0.0: {} + + resolve@2.0.0-next.7: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + node-exports-info: 1.6.0 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rolldown@1.0.2: + dependencies: + '@oxc-project/types': 0.132.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.2 + '@rolldown/binding-darwin-arm64': 1.0.2 + '@rolldown/binding-darwin-x64': 1.0.2 + '@rolldown/binding-freebsd-x64': 1.0.2 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.2 + '@rolldown/binding-linux-arm64-gnu': 1.0.2 + '@rolldown/binding-linux-arm64-musl': 1.0.2 + '@rolldown/binding-linux-ppc64-gnu': 1.0.2 + '@rolldown/binding-linux-s390x-gnu': 1.0.2 + '@rolldown/binding-linux-x64-gnu': 1.0.2 + '@rolldown/binding-linux-x64-musl': 1.0.2 + '@rolldown/binding-openharmony-arm64': 1.0.2 + '@rolldown/binding-wasm32-wasi': 1.0.2 + '@rolldown/binding-win32-arm64-msvc': 1.0.2 + '@rolldown/binding-win32-x64-msvc': 1.0.2 + + safe-array-concat@1.1.4: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + semver@6.3.1: {} + + semver@7.8.1: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.1.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.2.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tslib@2.8.1: + optional: true + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.60.0(eslint@9.39.4)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 8.60.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.0(eslint@9.39.4)(typescript@5.9.3) + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color typescript@5.9.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@8.0.14: + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.0.2 + tinyglobby: 0.2.16 + optionalDependencies: + fsevents: 2.3.3 + + vitest@4.1.7(vite@8.0.14): + dependencies: + '@vitest/expect': 4.1.7 + '@vitest/mocker': 4.1.7(vite@8.0.14) + '@vitest/pretty-format': 4.1.7 + '@vitest/runner': 4.1.7 + '@vitest/snapshot': 4.1.7 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.14 + why-is-node-running: 2.3.0 + transitivePeerDependencies: + - msw + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.21 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.21: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..1f78b7f --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["**/*.test.ts", "**/*.test.tsx"], + exclude: ["**/node_modules/**", "**/dist/**", ".worktrees/**"], + passWithNoTests: true, + }, +}); From 6b93ab38d2e36883ff8fcc21ae445108ba50df1f Mon Sep 17 00:00:00 2001 From: eleven_qy <64311644+alph-cmky@users.noreply.github.com> Date: Thu, 28 May 2026 19:31:49 +0800 Subject: [PATCH 03/13] docs: add project documentation skeleton --- README.md | 20 +++++++- docs/architecture.md | 66 ++++++++++++++++++++++++++ docs/contributing.md | 48 +++++++++++++++++++ docs/development/task-backlog.md | 10 ++-- docs/runtime-adapters.md | 71 ++++++++++++++++++++++++++++ docs/security-model.md | 80 ++++++++++++++++++++++++++++++++ docs/workflow-schema.md | 75 ++++++++++++++++++++++++++++++ 7 files changed, 364 insertions(+), 6 deletions(-) create mode 100644 docs/architecture.md create mode 100644 docs/contributing.md create mode 100644 docs/runtime-adapters.md create mode 100644 docs/security-model.md create mode 100644 docs/workflow-schema.md diff --git a/README.md b/README.md index 886d2aa..9831194 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ AgentDeck is a local-first workspace for multi-agent coding and low-code orchestration. -The project is in early design and planning. Current work is focused on defining the product, architecture, security model, UI direction, and development backlog before implementation begins. +The project is in early development. Current work is focused on turning the product design into a small, testable monorepo: runtime detection, local daemon, agent registry, chat workspace, workflow core, workflow canvas, and safe patch approval. ## What AgentDeck Is @@ -13,12 +13,30 @@ AgentDeck is designed to combine: - Low-code Agent and Workflow DAG orchestration. - Safe chat-to-workflow editing through patch proposals, validation, diff preview, approval, and rollback. +## MVP Scope + +The MVP focuses on: + +- Runtime Detector +- Agent Registry +- Chat Workspace +- Local Daemon +- Workflow Canvas +- Workflow Patch Approval + +The MVP intentionally excludes multi-tenant enterprise permissions, cloud-hosted execution, billing, plugin marketplace, full RAG, custom model training, and full Dify/Coze-style app publishing. + ## Project Documents - Product design: [`AGENTDECK_PRODUCT_DESIGN.md`](AGENTDECK_PRODUCT_DESIGN.md) - Original project brief: [`agentdeck-project-brief.md`](agentdeck-project-brief.md) - Development plan: [`docs/development/README.md`](docs/development/README.md) - Task backlog: [`docs/development/task-backlog.md`](docs/development/task-backlog.md) +- Architecture: [`docs/architecture.md`](docs/architecture.md) +- Runtime adapters: [`docs/runtime-adapters.md`](docs/runtime-adapters.md) +- Workflow schema: [`docs/workflow-schema.md`](docs/workflow-schema.md) +- Security model: [`docs/security-model.md`](docs/security-model.md) +- Contributing guide: [`docs/contributing.md`](docs/contributing.md) - Agent instructions: [`AGENTS.md`](AGENTS.md) - Claude Code instructions: [`CLAUDE.md`](CLAUDE.md) diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..d0548aa --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,66 @@ +# AgentDeck Architecture + +AgentDeck is planned as a local-first monorepo with clear package boundaries between UI, control-plane services, local runtime execution, workflow domain logic, runtime adapters, and MCP-facing tools. + +## Monorepo Layout + +```text +apps/web + Next.js / React / React Flow UI + +apps/server + Control plane for agents, workflows, chat, tasks, permissions, audit, and patch proposals + +apps/daemon + Local runtime detection, process execution, workspace adapter, and event stream + +packages/workflow-core + DAG schema, validator, executor, versioning, and patch engine + +packages/runtime-adapters + Runtime detection and execution adapters for Codex, Claude, shell, Ollama, LM Studio, Node, Git, and related tools + +packages/mcp-server + Controlled MCP tools for reading definitions, proposing patches, validating patches, previewing diffs, and applying approved changes +``` + +## Primary Data Flow + +```text +User + -> Web Chat Workspace + -> Server Orchestrator + -> Agent / Workflow / Permission Services + -> Local Daemon + -> Runtime Adapter + -> Codex / Claude / shell / local model provider + -> Event Stream + -> Web Task Inspector +``` + +## Design Boundaries + +- UI components should not contain workflow validation, runtime detection, permission policy, or patch application logic. +- Server routes should delegate domain behavior to focused services. +- The daemon should expose only local runtime and process capabilities needed by the control plane. +- Workflow schema and validation must live in `packages/workflow-core`. +- Runtime-specific behavior must live in `packages/runtime-adapters`. +- Coding agents must use controlled tools and patch proposals instead of direct database or workflow mutation. + +## MVP Persistence + +The MVP can use SQLite or local file-backed persistence. The persistence layer should be hidden behind repositories/services so later storage changes do not affect UI or domain packages. + +## Eventing + +The daemon and server should stream task lifecycle events: + +- `runtime.detected` +- `task.started` +- `task.output` +- `task.diff` +- `task.approvalRequested` +- `task.completed` +- `task.failed` + +These events power the Chat Workspace and Task Inspector. diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..06156d1 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,48 @@ +# Contributing + +This document supplements the root `CONTRIBUTING.md` with development-specific guidance. + +## Before Starting + +Read: + +- `AGENTDECK_PRODUCT_DESIGN.md` +- `docs/development/README.md` +- `docs/development/task-backlog.md` +- `AGENTS.md` + +## Task Flow + +1. Pick an open `AD-*` issue from GitHub or `docs/development/task-backlog.md`. +2. Create a focused branch. +3. Keep changes scoped to the selected task. +4. Update the task checklist when the task is complete. +5. Run the strongest available verification commands. +6. Open a pull request that links the issue. + +## Verification Commands + +For the current scaffold: + +```bash +pnpm install +pnpm lint +pnpm format +pnpm typecheck +pnpm test +pnpm build +``` + +## Pull Request Notes + +Mention: + +- Related `AD-*` issue. +- Verification commands and results. +- Security impact. +- UI impact, with screenshots when relevant. +- Documentation updates. + +## Security-Sensitive Changes + +Changes involving daemon execution, runtime adapters, workflow patches, permission policy, audit logs, or secret redaction require extra scrutiny and tests. diff --git a/docs/development/task-backlog.md b/docs/development/task-backlog.md index 8444757..4881754 100644 --- a/docs/development/task-backlog.md +++ b/docs/development/task-backlog.md @@ -80,11 +80,11 @@ Status markers: **Tasks:** -- [ ] Summarize product positioning in `README.md`. -- [ ] Link `AGENTDECK_PRODUCT_DESIGN.md` from `README.md`. -- [ ] Document the monorepo package responsibilities in `docs/architecture.md`. -- [ ] Capture initial security rules in `docs/security-model.md`. -- [ ] Commit with message `docs: add project documentation skeleton`. +- [x] Summarize product positioning in `README.md`. +- [x] Link `AGENTDECK_PRODUCT_DESIGN.md` from `README.md`. +- [x] Document the monorepo package responsibilities in `docs/architecture.md`. +- [x] Capture initial security rules in `docs/security-model.md`. +- [x] Commit with message `docs: add project documentation skeleton`. **Acceptance Criteria:** diff --git a/docs/runtime-adapters.md b/docs/runtime-adapters.md new file mode 100644 index 0000000..5a9f816 --- /dev/null +++ b/docs/runtime-adapters.md @@ -0,0 +1,71 @@ +# Runtime Adapters + +Runtime adapters isolate AgentDeck from the details of individual CLIs, local model providers, and command runners. + +## Goals + +- Detect available runtimes without exposing secrets. +- Normalize runtime metadata for the Runtime Dashboard. +- Provide a common execution interface for task orchestration. +- Keep Codex, Claude, shell, Ollama, LM Studio, Node, Git, and future integrations decoupled. + +## Detection Model + +Each detector should return normalized metadata: + +```text +RuntimeDetectionResult + id + name + type + detected + path + version + scope + capabilities + warnings + lastDetectedAt +``` + +Runtime status levels: + +```text +missing +ready +configured +projectActive +localProviderReady +``` + +## Detection Rules + +- Use read-only probes. +- Prefer login-shell probing on macOS because GUI apps often have incomplete `PATH`. +- Do not read or display raw auth files. +- Redact token-like values in warnings and logs. +- Detect project markers such as `.codex/` and `AGENTS.md` where relevant. + +## Execution Interface + +Runtime execution should eventually share one adapter contract: + +```text +executeTask(task, context) -> event stream +cancelTask(taskId) +getCapabilities() +``` + +Execution events should include stdout, stderr, tool events, diffs, completion, and failure. + +## Initial Runtime Targets + +- Codex +- Claude +- Safe shell +- Ollama +- LM Studio +- Node +- Git +- VS Code CLI + +The safe shell adapter must reject disallowed commands before execution. diff --git a/docs/security-model.md b/docs/security-model.md new file mode 100644 index 0000000..4b63411 --- /dev/null +++ b/docs/security-model.md @@ -0,0 +1,80 @@ +# Security Model + +AgentDeck executes local commands and gives agents access to workspace context, so security is a core product capability. + +## Core Principles + +- New agents default to read-only workspace access. +- Dangerous actions require explicit permission or approval. +- Coding agents may propose patches but cannot apply them directly. +- The platform owns schema validation, DAG validation, permission checks, versioning, audit, and rollback. +- Secrets must never be read, displayed, logged, snapshotted, or persisted. + +## Permission Levels + +```text +read_workspace + Read files under an approved workspace root. + +write_workspace + Write files under an approved workspace root. + +run_safe_commands + Run allowlisted commands such as test, typecheck, and lint. + +run_arbitrary_commands + Run arbitrary commands. Requires approval by default. + +network_access + Access the network. + +install_dependencies + Install dependencies through npm, pnpm, brew, pip, or similar tools. + +git_commit + Create git commits. + +git_push + Push to remotes. Requires strong approval by default. +``` + +## Secret Handling + +Never read or display raw auth files such as: + +```text +~/.codex/auth.json +``` + +Redact values matching or labeled as: + +- token +- secret +- api key +- password +- credential +- authorization header + +## Local Daemon Boundary + +The daemon should: + +- Bind to localhost by default. +- Reject non-local browser origins. +- Expose minimal runtime detection and execution APIs. +- Stream summarized events instead of exposing sensitive raw data. +- Record command summaries and outputs in an audit-friendly format. + +## Workflow Patch Safety + +Patch application requires: + +- Target version match. +- Schema validation. +- DAG validation. +- Input/output compatibility checks. +- Permission boundary checks. +- Diff preview. +- User approval. +- Audit log. +- Rollback version. diff --git a/docs/workflow-schema.md b/docs/workflow-schema.md new file mode 100644 index 0000000..715ef79 --- /dev/null +++ b/docs/workflow-schema.md @@ -0,0 +1,75 @@ +# Workflow Schema + +AgentDeck workflows are low-code DAGs that connect agents, tools, conditions, human approvals, and final outputs. + +## MVP Node Types + +```text +Start + Workflow entry. Receives user input and context variables. + +Agent + Calls an AgentDefinition. + +Tool + Calls an MCP tool or built-in platform tool. + +Condition + Branches based on structured fields or expressions. + +Human Approval + Pauses execution until the user approves or rejects. + +End + Produces final output. +``` + +## Workflow Definition + +```text +WorkflowDefinition + id + version + nodes + edges + variables + permissions + status + createdAt + updatedAt +``` + +## Validation Requirements + +The workflow validator must reject: + +- Missing start node. +- Missing end node. +- Multiple start nodes when the schema requires exactly one. +- Cycles. +- Edges pointing to missing nodes. +- Node input/output mismatches. +- References to missing agents or tools. +- Permission requests that exceed workflow or agent policy. + +## Versioning + +Workflow changes must produce new versions. Every applied patch should keep a rollback point and audit record. + +## Patch Model + +Coding agents must not directly mutate workflow state. They can only propose patches: + +```text +PatchProposal + id + targetType + targetId + baseVersion + jsonPatch + validationResult + approvalState + diffPreview +``` + +The platform validates, previews, and applies only approved patches. From ffeecc26790e5d67d41f1b382f542031df38c854 Mon Sep 17 00:00:00 2001 From: eleven_qy <64311644+alph-cmky@users.noreply.github.com> Date: Thu, 28 May 2026 19:50:30 +0800 Subject: [PATCH 04/13] feat: define runtime detection types --- docs/development/task-backlog.md | 10 +-- packages/runtime-adapters/src/index.ts | 12 ++- .../src/runtime-types.test.ts | 88 +++++++++++++++++++ .../runtime-adapters/src/runtime-types.ts | 74 ++++++++++++++++ 4 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 packages/runtime-adapters/src/runtime-types.test.ts create mode 100644 packages/runtime-adapters/src/runtime-types.ts diff --git a/docs/development/task-backlog.md b/docs/development/task-backlog.md index 4881754..7de5b5a 100644 --- a/docs/development/task-backlog.md +++ b/docs/development/task-backlog.md @@ -104,11 +104,11 @@ Status markers: **Tasks:** -- [ ] Define `RuntimeType` for `codex`, `claude`, `ollama`, `lmstudio`, `node`, `git`, and `code`. -- [ ] Define `RuntimeStatus` for `missing`, `ready`, `configured`, `projectActive`, and `localProviderReady`. -- [ ] Define `RuntimeDetectionResult` with `id`, `name`, `type`, `detected`, `path`, `version`, `scope`, `capabilities`, `warnings`, and `lastDetectedAt`. -- [ ] Add tests for status ordering and required runtime fields. -- [ ] Commit with message `feat: define runtime detection types`. +- [x] Define `RuntimeType` for `codex`, `claude`, `ollama`, `lmstudio`, `node`, `git`, and `code`. +- [x] Define `RuntimeStatus` for `missing`, `ready`, `configured`, `projectActive`, and `localProviderReady`. +- [x] Define `RuntimeDetectionResult` with `id`, `name`, `type`, `detected`, `path`, `version`, `scope`, `capabilities`, `warnings`, and `lastDetectedAt`. +- [x] Add tests for status ordering and required runtime fields. +- [x] Commit with message `feat: define runtime detection types`. **Acceptance Criteria:** diff --git a/packages/runtime-adapters/src/index.ts b/packages/runtime-adapters/src/index.ts index 7cf79dd..8dc1e3b 100644 --- a/packages/runtime-adapters/src/index.ts +++ b/packages/runtime-adapters/src/index.ts @@ -1 +1,11 @@ -export const runtimeAdaptersPackageName = "@agentdeck/runtime-adapters"; +export { + RUNTIME_STATUS_RANK, + RUNTIME_TYPES, + createMissingRuntimeResult, + isRuntimeAtLeastStatus, + type RuntimeCapabilities, + type RuntimeDetectionResult, + type RuntimeScope, + type RuntimeStatus, + type RuntimeType, +} from "./runtime-types.js"; diff --git a/packages/runtime-adapters/src/runtime-types.test.ts b/packages/runtime-adapters/src/runtime-types.test.ts new file mode 100644 index 0000000..23a2b35 --- /dev/null +++ b/packages/runtime-adapters/src/runtime-types.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from "vitest"; + +import { + RUNTIME_STATUS_RANK, + RUNTIME_TYPES, + createMissingRuntimeResult, + isRuntimeAtLeastStatus, + type RuntimeDetectionResult, + type RuntimeStatus, + type RuntimeType, +} from "./runtime-types.js"; + +describe("runtime type contracts", () => { + it("defines the MVP runtime types without provider-specific branching", () => { + const expectedRuntimeTypes: RuntimeType[] = [ + "codex", + "claude", + "ollama", + "lmstudio", + "node", + "git", + "code", + ]; + + expect(RUNTIME_TYPES).toEqual(expectedRuntimeTypes); + }); + + it("orders runtime statuses by readiness level", () => { + const expectedStatuses: RuntimeStatus[] = [ + "missing", + "ready", + "configured", + "projectActive", + "localProviderReady", + ]; + + expect(Object.keys(RUNTIME_STATUS_RANK)).toEqual(expectedStatuses); + expect(isRuntimeAtLeastStatus("projectActive", "configured")).toBe(true); + expect(isRuntimeAtLeastStatus("ready", "projectActive")).toBe(false); + }); + + it("represents a complete detected runtime result", () => { + const result: RuntimeDetectionResult = { + id: "codex", + name: "OpenAI Codex CLI", + type: "codex", + status: "projectActive", + detected: true, + path: "/usr/local/bin/codex", + version: "codex-cli 0.128.0", + scope: ["global", "project"], + configPath: "/Users/example/.codex/config.toml", + projectMarkers: ["/workspace/.codex", "/workspace/AGENTS.md"], + capabilities: { + exec: true, + mcp: true, + localProviders: ["ollama"], + }, + warnings: [], + lastDetectedAt: "2026-05-28T00:00:00.000Z", + }; + + expect(result.type).toBe("codex"); + expect(result.capabilities.localProviders).toEqual(["ollama"]); + }); + + it("creates a normalized missing runtime result", () => { + expect( + createMissingRuntimeResult({ + id: "ollama", + name: "Ollama", + type: "ollama", + lastDetectedAt: "2026-05-28T00:00:00.000Z", + warnings: ["ollama was not found on PATH"], + }), + ).toEqual({ + id: "ollama", + name: "Ollama", + type: "ollama", + status: "missing", + detected: false, + scope: [], + capabilities: {}, + warnings: ["ollama was not found on PATH"], + lastDetectedAt: "2026-05-28T00:00:00.000Z", + }); + }); +}); diff --git a/packages/runtime-adapters/src/runtime-types.ts b/packages/runtime-adapters/src/runtime-types.ts new file mode 100644 index 0000000..735b73e --- /dev/null +++ b/packages/runtime-adapters/src/runtime-types.ts @@ -0,0 +1,74 @@ +export const RUNTIME_TYPES = [ + "codex", + "claude", + "ollama", + "lmstudio", + "node", + "git", + "code", +] as const; + +export type RuntimeType = (typeof RUNTIME_TYPES)[number]; + +export const RUNTIME_STATUS_RANK = { + missing: 0, + ready: 1, + configured: 2, + projectActive: 3, + localProviderReady: 4, +} as const; + +export type RuntimeStatus = keyof typeof RUNTIME_STATUS_RANK; + +export type RuntimeScope = "global" | "project" | "localProvider"; + +export interface RuntimeCapabilities { + readonly exec?: boolean; + readonly mcp?: boolean; + readonly chat?: boolean; + readonly versionCommand?: boolean; + readonly localProviders?: readonly RuntimeType[]; +} + +export interface RuntimeDetectionResult { + readonly id: string; + readonly name: string; + readonly type: RuntimeType; + readonly status: RuntimeStatus; + readonly detected: boolean; + readonly path?: string; + readonly version?: string; + readonly scope: readonly RuntimeScope[]; + readonly configPath?: string; + readonly projectMarkers?: readonly string[]; + readonly capabilities: RuntimeCapabilities; + readonly warnings: readonly string[]; + readonly lastDetectedAt: string; +} + +export function isRuntimeAtLeastStatus( + actualStatus: RuntimeStatus, + minimumStatus: RuntimeStatus, +): boolean { + return RUNTIME_STATUS_RANK[actualStatus] >= RUNTIME_STATUS_RANK[minimumStatus]; +} + +export function createMissingRuntimeResult(input: { + readonly id: string; + readonly name: string; + readonly type: RuntimeType; + readonly lastDetectedAt: string; + readonly warnings?: readonly string[]; +}): RuntimeDetectionResult { + return { + id: input.id, + name: input.name, + type: input.type, + status: "missing", + detected: false, + scope: [], + capabilities: {}, + warnings: input.warnings ?? [], + lastDetectedAt: input.lastDetectedAt, + }; +} From 00435784257a6bc8339a908553fbf7337bfcb3fa Mon Sep 17 00:00:00 2001 From: eleven_qy <64311644+alph-cmky@users.noreply.github.com> Date: Thu, 28 May 2026 20:02:33 +0800 Subject: [PATCH 05/13] feat: add runtime probe command helper --- docs/development/task-backlog.md | 10 +- package.json | 1 + packages/runtime-adapters/src/index.ts | 9 ++ .../src/probe/run-probe.test.ts | 55 +++++++ .../runtime-adapters/src/probe/run-probe.ts | 134 ++++++++++++++++++ pnpm-lock.yaml | 32 ++++- 6 files changed, 229 insertions(+), 12 deletions(-) create mode 100644 packages/runtime-adapters/src/probe/run-probe.test.ts create mode 100644 packages/runtime-adapters/src/probe/run-probe.ts diff --git a/docs/development/task-backlog.md b/docs/development/task-backlog.md index 7de5b5a..41e658c 100644 --- a/docs/development/task-backlog.md +++ b/docs/development/task-backlog.md @@ -125,11 +125,11 @@ Status markers: **Tasks:** -- [ ] Implement a helper that runs `/bin/zsh -lc ''` on macOS and a regular shell command elsewhere. -- [ ] Capture `stdout`, `stderr`, `exitCode`, and elapsed time. -- [ ] Add a timeout option with a short default. -- [ ] Prevent probe helpers from accepting write-oriented commands in tests by documenting allowed usage. -- [ ] Commit with message `feat: add runtime probe command helper`. +- [x] Implement a helper that runs `/bin/zsh -lc ''` on macOS and a regular shell command elsewhere. +- [x] Capture `stdout`, `stderr`, `exitCode`, and elapsed time. +- [x] Add a timeout option with a short default. +- [x] Prevent probe helpers from accepting write-oriented commands in tests by documenting allowed usage. +- [x] Commit with message `feat: add runtime probe command helper`. **Acceptance Criteria:** diff --git a/package.json b/package.json index e5d37f4..80f8049 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.4", + "@types/node": "^25.9.1", "eslint": "^9.39.4", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.1.1", diff --git a/packages/runtime-adapters/src/index.ts b/packages/runtime-adapters/src/index.ts index 8dc1e3b..706f4ff 100644 --- a/packages/runtime-adapters/src/index.ts +++ b/packages/runtime-adapters/src/index.ts @@ -9,3 +9,12 @@ export { type RuntimeStatus, type RuntimeType, } from "./runtime-types.js"; + +export { + getProbeShellInvocation, + runProbe, + type ProbeShellInvocation, + type ProbeShellOptions, + type RunProbeOptions, + type RunProbeResult, +} from "./probe/run-probe.js"; diff --git a/packages/runtime-adapters/src/probe/run-probe.test.ts b/packages/runtime-adapters/src/probe/run-probe.test.ts new file mode 100644 index 0000000..1b2ab13 --- /dev/null +++ b/packages/runtime-adapters/src/probe/run-probe.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; + +import { getProbeShellInvocation, runProbe } from "./run-probe.js"; + +describe("getProbeShellInvocation", () => { + it("uses zsh login shell on macOS so GUI-launched apps get user PATH", () => { + expect(getProbeShellInvocation("command -v codex", { platform: "darwin" })).toEqual({ + command: "/bin/zsh", + args: ["-lc", "command -v codex"], + }); + }); + + it("uses a regular login shell on non-macOS platforms", () => { + expect( + getProbeShellInvocation("command -v node", { + platform: "linux", + shell: "/bin/bash", + }), + ).toEqual({ + command: "/bin/bash", + args: ["-lc", "command -v node"], + }); + }); +}); + +describe("runProbe", () => { + it("captures stdout, exit code, and elapsed time for successful read-only probes", async () => { + const result = await runProbe("printf 'codex-cli 0.128.0'"); + + expect(result.stdout).toBe("codex-cli 0.128.0"); + expect(result.stderr).toBe(""); + expect(result.exitCode).toBe(0); + expect(result.timedOut).toBe(false); + expect(result.elapsedMs).toBeGreaterThanOrEqual(0); + }); + + it("captures stderr and non-zero exit code without throwing", async () => { + const result = await runProbe("printf 'missing runtime' >&2; exit 7"); + + expect(result.stdout).toBe(""); + expect(result.stderr).toBe("missing runtime"); + expect(result.exitCode).toBe(7); + expect(result.timedOut).toBe(false); + }); + + it("returns a normalized timeout result", async () => { + const result = await runProbe('node -e "setTimeout(() => {}, 500)"', { + timeoutMs: 50, + }); + + expect(result.exitCode).toBeNull(); + expect(result.timedOut).toBe(true); + expect(result.elapsedMs).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/packages/runtime-adapters/src/probe/run-probe.ts b/packages/runtime-adapters/src/probe/run-probe.ts new file mode 100644 index 0000000..b47b84d --- /dev/null +++ b/packages/runtime-adapters/src/probe/run-probe.ts @@ -0,0 +1,134 @@ +import { spawn } from "node:child_process"; +import { performance } from "node:perf_hooks"; + +export interface ProbeShellInvocation { + readonly command: string; + readonly args: readonly string[]; +} + +export interface ProbeShellOptions { + readonly platform?: NodeJS.Platform; + readonly shell?: string; +} + +export interface RunProbeOptions extends ProbeShellOptions { + readonly timeoutMs?: number; + readonly cwd?: string; + readonly env?: NodeJS.ProcessEnv; +} + +export interface RunProbeResult { + readonly stdout: string; + readonly stderr: string; + readonly exitCode: number | null; + readonly signal: NodeJS.Signals | null; + readonly timedOut: boolean; + readonly elapsedMs: number; +} + +const DEFAULT_TIMEOUT_MS = 5_000; + +export function getProbeShellInvocation( + probeCommand: string, + options: ProbeShellOptions = {}, +): ProbeShellInvocation { + const platform = options.platform ?? process.platform; + + if (platform === "darwin") { + return { + command: "/bin/zsh", + args: ["-lc", probeCommand], + }; + } + + if (platform === "win32") { + return { + command: options.shell ?? "cmd.exe", + args: ["/d", "/s", "/c", probeCommand], + }; + } + + return { + command: options.shell ?? process.env.SHELL ?? "/bin/sh", + args: ["-lc", probeCommand], + }; +} + +/** + * Runs a read-only runtime detection probe. + * + * This helper is intentionally for metadata probes such as `command -v`, + * `tool --version`, and `tool --help`. Do not use it for write-oriented + * commands, installs, file mutation, git mutation, or long-running tasks. + */ +export async function runProbe( + probeCommand: string, + options: RunProbeOptions = {}, +): Promise { + const startedAt = performance.now(); + const invocation = getProbeShellInvocation(probeCommand, options); + const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; + + return await new Promise((resolve) => { + let stdout = ""; + let stderr = ""; + let timedOut = false; + let settled = false; + + const child = spawn(invocation.command, [...invocation.args], { + cwd: options.cwd, + env: options.env ?? process.env, + stdio: ["ignore", "pipe", "pipe"], + }); + + const timeout = setTimeout(() => { + timedOut = true; + child.kill("SIGTERM"); + }, timeoutMs); + + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + + child.stdout.on("data", (chunk: string) => { + stdout += chunk; + }); + + child.stderr.on("data", (chunk: string) => { + stderr += chunk; + }); + + child.on("error", (error) => { + if (settled) { + return; + } + + settled = true; + clearTimeout(timeout); + resolve({ + stdout: stdout.trimEnd(), + stderr: (stderr + error.message).trimEnd(), + exitCode: null, + signal: null, + timedOut, + elapsedMs: performance.now() - startedAt, + }); + }); + + child.on("close", (exitCode, signal) => { + if (settled) { + return; + } + + settled = true; + clearTimeout(timeout); + resolve({ + stdout: stdout.trimEnd(), + stderr: stderr.trimEnd(), + exitCode: timedOut ? null : exitCode, + signal, + timedOut, + elapsedMs: performance.now() - startedAt, + }); + }); + }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e246de2..9c18970 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@eslint/js': specifier: ^9.39.4 version: 9.39.4 + '@types/node': + specifier: ^25.9.1 + version: 25.9.1 eslint: specifier: ^9.39.4 version: 9.39.4 @@ -34,7 +37,7 @@ importers: version: 8.60.0(eslint@9.39.4)(typescript@5.9.3) vitest: specifier: ^4.1.7 - version: 4.1.7(vite@8.0.14) + version: 4.1.7(@types/node@25.9.1)(vite@8.0.14(@types/node@25.9.1)) apps/daemon: {} @@ -325,6 +328,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@25.9.1': + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + '@typescript-eslint/eslint-plugin@8.60.0': resolution: {integrity: sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1392,6 +1398,9 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -1808,6 +1817,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/node@25.9.1': + dependencies: + undici-types: 7.24.6 + '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -1908,13 +1921,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.7(vite@8.0.14)': + '@vitest/mocker@4.1.7(vite@8.0.14(@types/node@25.9.1))': dependencies: '@vitest/spy': 4.1.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.14 + vite: 8.0.14(@types/node@25.9.1) '@vitest/pretty-format@4.1.7': dependencies: @@ -3104,6 +3117,8 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + undici-types@7.24.6: {} + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 @@ -3114,7 +3129,7 @@ snapshots: dependencies: punycode: 2.3.1 - vite@8.0.14: + vite@8.0.14(@types/node@25.9.1): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -3122,12 +3137,13 @@ snapshots: rolldown: 1.0.2 tinyglobby: 0.2.16 optionalDependencies: + '@types/node': 25.9.1 fsevents: 2.3.3 - vitest@4.1.7(vite@8.0.14): + vitest@4.1.7(@types/node@25.9.1)(vite@8.0.14(@types/node@25.9.1)): dependencies: '@vitest/expect': 4.1.7 - '@vitest/mocker': 4.1.7(vite@8.0.14) + '@vitest/mocker': 4.1.7(vite@8.0.14(@types/node@25.9.1)) '@vitest/pretty-format': 4.1.7 '@vitest/runner': 4.1.7 '@vitest/snapshot': 4.1.7 @@ -3144,8 +3160,10 @@ snapshots: tinyexec: 1.2.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.14 + vite: 8.0.14(@types/node@25.9.1) why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.9.1 transitivePeerDependencies: - msw From f918d24abb1246843a4fe6a825ecdd9262e74408 Mon Sep 17 00:00:00 2001 From: eleven_qy <64311644+alph-cmky@users.noreply.github.com> Date: Thu, 28 May 2026 20:21:11 +0800 Subject: [PATCH 06/13] feat: detect codex runtime --- docs/development/task-backlog.md | 14 +- .../src/detectors/codex.test.ts | 178 +++++++++++++++++ .../runtime-adapters/src/detectors/codex.ts | 185 ++++++++++++++++++ packages/runtime-adapters/src/index.ts | 2 + 4 files changed, 372 insertions(+), 7 deletions(-) create mode 100644 packages/runtime-adapters/src/detectors/codex.test.ts create mode 100644 packages/runtime-adapters/src/detectors/codex.ts diff --git a/docs/development/task-backlog.md b/docs/development/task-backlog.md index 41e658c..ffd9037 100644 --- a/docs/development/task-backlog.md +++ b/docs/development/task-backlog.md @@ -147,13 +147,13 @@ Status markers: **Tasks:** -- [ ] Probe `command -v codex`. -- [ ] Probe `codex --version`. -- [ ] Detect `~/.codex/config.toml` existence without reading auth secrets. -- [ ] Detect project markers `.codex/` and `AGENTS.md` under the selected workspace root. -- [ ] Probe help text for `exec`, `mcp`, and local provider support. -- [ ] Redact paths or warnings that include token-like values. -- [ ] Commit with message `feat: detect codex runtime`. +- [x] Probe `command -v codex`. +- [x] Probe `codex --version`. +- [x] Detect `~/.codex/config.toml` existence without reading auth secrets. +- [x] Detect project markers `.codex/` and `AGENTS.md` under the selected workspace root. +- [x] Probe help text for `exec`, `mcp`, and local provider support. +- [x] Redact paths or warnings that include token-like values. +- [x] Commit with message `feat: detect codex runtime`. **Acceptance Criteria:** diff --git a/packages/runtime-adapters/src/detectors/codex.test.ts b/packages/runtime-adapters/src/detectors/codex.test.ts new file mode 100644 index 0000000..aec1042 --- /dev/null +++ b/packages/runtime-adapters/src/detectors/codex.test.ts @@ -0,0 +1,178 @@ +import { mkdtemp, mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +import { describe, expect, it } from "vitest"; + +import { detectCodexRuntime } from "./codex.js"; +import type { RunProbeResult } from "../probe/run-probe.js"; + +function probeResult(overrides: Partial): RunProbeResult { + return { + stdout: "", + stderr: "", + exitCode: 0, + signal: null, + timedOut: false, + elapsedMs: 1, + ...overrides, + }; +} + +async function createTempDir(prefix: string): Promise { + return await mkdtemp(join(tmpdir(), prefix)); +} + +describe("detectCodexRuntime", () => { + it("returns a missing result when the codex binary cannot be found", async () => { + const result = await detectCodexRuntime({ + lastDetectedAt: "2026-05-28T00:00:00.000Z", + runProbe: async (command) => { + expect(command).toBe("command -v codex"); + return probeResult({ + exitCode: 1, + stderr: "codex not found", + }); + }, + }); + + expect(result).toEqual({ + id: "codex", + name: "OpenAI Codex CLI", + type: "codex", + status: "missing", + detected: false, + scope: [], + capabilities: {}, + warnings: ["codex not found"], + lastDetectedAt: "2026-05-28T00:00:00.000Z", + }); + }); + + it("returns ready when binary and version probes succeed", async () => { + const commands: string[] = []; + const result = await detectCodexRuntime({ + lastDetectedAt: "2026-05-28T00:00:00.000Z", + runProbe: async (command) => { + commands.push(command); + if (command === "command -v codex") { + return probeResult({ stdout: "/usr/local/bin/codex" }); + } + if (command === "codex --version") { + return probeResult({ stdout: "codex-cli 0.128.0" }); + } + return probeResult({ stdout: "Usage: codex\nCommands:\n exec\n mcp" }); + }, + }); + + expect(commands).toEqual(["command -v codex", "codex --version", "codex --help"]); + expect(result.status).toBe("ready"); + expect(result.detected).toBe(true); + expect(result.path).toBe("/usr/local/bin/codex"); + expect(result.version).toBe("codex-cli 0.128.0"); + expect(result.capabilities).toEqual({ + exec: true, + mcp: true, + versionCommand: true, + localProviders: [], + }); + }); + + it("returns configured when ~/.codex/config.toml exists without reading auth files", async () => { + const homeDir = await createTempDir("agentdeck-codex-home-"); + await mkdir(join(homeDir, ".codex"), { recursive: true }); + await writeFile(join(homeDir, ".codex", "config.toml"), 'model = "gpt-5"\n'); + await writeFile(join(homeDir, ".codex", "auth.json"), '{"token":"super-secret-token"}\n'); + + const result = await detectCodexRuntime({ + homeDir, + lastDetectedAt: "2026-05-28T00:00:00.000Z", + runProbe: async (command) => { + if (command === "command -v codex") { + return probeResult({ stdout: "/usr/local/bin/codex" }); + } + if (command === "codex --version") { + return probeResult({ stdout: "codex-cli 0.128.0" }); + } + return probeResult({ stdout: "Usage: codex\nCommands:\n exec" }); + }, + }); + + expect(result.status).toBe("configured"); + expect(result.configPath).toBe(join(homeDir, ".codex", "config.toml")); + expect(JSON.stringify(result)).not.toContain("super-secret-token"); + expect(JSON.stringify(result)).not.toContain("auth.json"); + }); + + it("returns projectActive when project markers exist", async () => { + const workspaceRoot = await createTempDir("agentdeck-codex-workspace-"); + await mkdir(join(workspaceRoot, ".codex"), { recursive: true }); + await writeFile(join(workspaceRoot, "AGENTS.md"), "# instructions\n"); + + const result = await detectCodexRuntime({ + workspaceRoot, + lastDetectedAt: "2026-05-28T00:00:00.000Z", + runProbe: async (command) => { + if (command === "command -v codex") { + return probeResult({ stdout: "/usr/local/bin/codex" }); + } + if (command === "codex --version") { + return probeResult({ stdout: "codex-cli 0.128.0" }); + } + return probeResult({ stdout: "Usage: codex\nCommands:\n exec\n mcp" }); + }, + }); + + expect(result.status).toBe("projectActive"); + expect(result.scope).toEqual(["global", "project"]); + expect(result.projectMarkers).toEqual([ + join(workspaceRoot, ".codex"), + join(workspaceRoot, "AGENTS.md"), + ]); + }); + + it("returns localProviderReady when Codex help exposes local provider support", async () => { + const result = await detectCodexRuntime({ + lastDetectedAt: "2026-05-28T00:00:00.000Z", + runProbe: async (command) => { + if (command === "command -v codex") { + return probeResult({ stdout: "/usr/local/bin/codex" }); + } + if (command === "codex --version") { + return probeResult({ stdout: "codex-cli 0.128.0" }); + } + return probeResult({ + stdout: + "Usage: codex --oss --local-provider ollama --local-provider lmstudio\nCommands:\n exec\n mcp", + }); + }, + }); + + expect(result.status).toBe("localProviderReady"); + expect(result.scope).toEqual(["global", "localProvider"]); + expect(result.capabilities.localProviders).toEqual(["ollama", "lmstudio"]); + }); + + it("redacts token-like values from warnings", async () => { + const result = await detectCodexRuntime({ + lastDetectedAt: "2026-05-28T00:00:00.000Z", + runProbe: async (command) => { + if (command === "command -v codex") { + return probeResult({ stdout: "/usr/local/bin/codex" }); + } + if (command === "codex --version") { + return probeResult({ + exitCode: 1, + stderr: "failed with api_key=abc123 and token: xyz789", + }); + } + return probeResult({ stdout: "" }); + }, + }); + + expect(result.warnings.join("\n")).toContain("api_key=[REDACTED]"); + expect(result.warnings.join("\n")).toContain("token: [REDACTED]"); + expect(result.warnings.join("\n")).not.toContain("abc123"); + expect(result.warnings.join("\n")).not.toContain("xyz789"); + }); +}); diff --git a/packages/runtime-adapters/src/detectors/codex.ts b/packages/runtime-adapters/src/detectors/codex.ts new file mode 100644 index 0000000..1fd0d32 --- /dev/null +++ b/packages/runtime-adapters/src/detectors/codex.ts @@ -0,0 +1,185 @@ +import { access } from "node:fs/promises"; +import { constants } from "node:fs"; +import { join } from "node:path"; + +import { runProbe, type RunProbeResult } from "../probe/run-probe.js"; +import { + createMissingRuntimeResult, + type RuntimeCapabilities, + type RuntimeDetectionResult, + type RuntimeScope, + type RuntimeStatus, + type RuntimeType, +} from "../runtime-types.js"; + +export interface DetectCodexRuntimeOptions { + readonly workspaceRoot?: string; + readonly homeDir?: string; + readonly lastDetectedAt?: string; + readonly runProbe?: (command: string) => Promise; +} + +const CODEX_RUNTIME_ID = "codex"; +const CODEX_RUNTIME_NAME = "OpenAI Codex CLI"; + +export async function detectCodexRuntime( + options: DetectCodexRuntimeOptions = {}, +): Promise { + const probe = options.runProbe ?? runProbe; + const lastDetectedAt = options.lastDetectedAt ?? new Date().toISOString(); + const binaryProbe = await probe("command -v codex"); + + if (binaryProbe.exitCode !== 0 || binaryProbe.stdout.length === 0) { + return createMissingRuntimeResult({ + id: CODEX_RUNTIME_ID, + name: CODEX_RUNTIME_NAME, + type: "codex", + lastDetectedAt, + warnings: collectWarnings(binaryProbe), + }); + } + + const versionProbe = await probe("codex --version"); + const helpProbe = await probe("codex --help"); + const configPath = options.homeDir ? await findCodexConfigPath(options.homeDir) : undefined; + const projectMarkers = await findProjectMarkers(options.workspaceRoot); + const version = + versionProbe.exitCode === 0 && versionProbe.stdout.length > 0 + ? versionProbe.stdout.trim() + : undefined; + const capabilities = parseCodexCapabilities(helpProbe.stdout, Boolean(version)); + const scope = getRuntimeScope(projectMarkers, capabilities.localProviders ?? []); + const warnings = [...collectWarnings(versionProbe), ...collectWarnings(helpProbe)]; + + const result: RuntimeDetectionResult = { + id: CODEX_RUNTIME_ID, + name: CODEX_RUNTIME_NAME, + type: "codex", + status: getCodexStatus({ + configPath, + projectMarkers, + localProviders: capabilities.localProviders ?? [], + }), + detected: true, + path: binaryProbe.stdout.trim(), + scope, + capabilities, + warnings, + lastDetectedAt, + }; + + return { + ...result, + ...(version ? { version } : {}), + ...(configPath ? { configPath } : {}), + ...(projectMarkers ? { projectMarkers } : {}), + }; +} + +async function findCodexConfigPath(homeDir: string): Promise { + const configPath = join(homeDir, ".codex", "config.toml"); + + try { + await access(configPath, constants.R_OK); + return configPath; + } catch { + return undefined; + } +} + +async function findProjectMarkers( + workspaceRoot: string | undefined, +): Promise { + if (!workspaceRoot) { + return undefined; + } + + const markerCandidates = [join(workspaceRoot, ".codex"), join(workspaceRoot, "AGENTS.md")]; + const markers: string[] = []; + + for (const marker of markerCandidates) { + try { + await access(marker, constants.F_OK); + markers.push(marker); + } catch { + // Missing project markers are normal for workspaces that do not use Codex yet. + } + } + + return markers.length > 0 ? markers : undefined; +} + +function parseCodexCapabilities(helpText: string, versionCommand: boolean): RuntimeCapabilities { + const normalizedHelp = helpText.toLowerCase(); + const localProviders: RuntimeType[] = []; + + if (normalizedHelp.includes("ollama")) { + localProviders.push("ollama"); + } + + if (normalizedHelp.includes("lmstudio") || normalizedHelp.includes("lm studio")) { + localProviders.push("lmstudio"); + } + + return { + exec: /\bexec\b/.test(normalizedHelp), + mcp: /\bmcp\b/.test(normalizedHelp), + versionCommand, + localProviders, + }; +} + +function getRuntimeScope( + projectMarkers: readonly string[] | undefined, + localProviders: readonly RuntimeType[], +): RuntimeScope[] { + const scope: RuntimeScope[] = ["global"]; + + if (projectMarkers && projectMarkers.length > 0) { + scope.push("project"); + } + + if (localProviders.length > 0) { + scope.push("localProvider"); + } + + return scope; +} + +function getCodexStatus(input: { + readonly configPath: string | undefined; + readonly projectMarkers: readonly string[] | undefined; + readonly localProviders: readonly RuntimeType[]; +}): RuntimeStatus { + if (input.localProviders.length > 0) { + return "localProviderReady"; + } + + if (input.projectMarkers && input.projectMarkers.length > 0) { + return "projectActive"; + } + + if (input.configPath) { + return "configured"; + } + + return "ready"; +} + +function collectWarnings(result: RunProbeResult): string[] { + if (result.exitCode === 0 && !result.timedOut) { + return []; + } + + const warning = + result.stderr || result.stdout || (result.timedOut ? "probe timed out" : "probe failed"); + return [redactSensitiveValues(warning)]; +} + +function redactSensitiveValues(value: string): string { + return value + .replace(/\b(api[_-]?key\s*[=:]\s*)([^\s]+)/gi, "$1[REDACTED]") + .replace(/\b(token\s*[=:]\s*)([^\s]+)/gi, "$1[REDACTED]") + .replace(/\b(password\s*[=:]\s*)([^\s]+)/gi, "$1[REDACTED]") + .replace(/\b(secret\s*[=:]\s*)([^\s]+)/gi, "$1[REDACTED]"); +} diff --git a/packages/runtime-adapters/src/index.ts b/packages/runtime-adapters/src/index.ts index 706f4ff..6379722 100644 --- a/packages/runtime-adapters/src/index.ts +++ b/packages/runtime-adapters/src/index.ts @@ -18,3 +18,5 @@ export { type RunProbeOptions, type RunProbeResult, } from "./probe/run-probe.js"; + +export { detectCodexRuntime, type DetectCodexRuntimeOptions } from "./detectors/codex.js"; From ea5bc1994a39f67eb03c98d707ec3e6164a925e0 Mon Sep 17 00:00:00 2001 From: eleven_qy <64311644+alph-cmky@users.noreply.github.com> Date: Thu, 28 May 2026 20:29:21 +0800 Subject: [PATCH 07/13] feat: add core runtime detectors --- docs/development/task-backlog.md | 8 +- .../runtime-adapters/src/detectors/claude.ts | 16 ++ .../src/detectors/cli-runtime.ts | 138 +++++++++++++ .../runtime-adapters/src/detectors/code.ts | 16 ++ .../src/detectors/core-detectors.test.ts | 184 ++++++++++++++++++ .../src/detectors/core-runtimes.ts | 52 +++++ .../runtime-adapters/src/detectors/git.ts | 16 ++ .../src/detectors/lmstudio.ts | 19 ++ .../runtime-adapters/src/detectors/node.ts | 16 ++ .../runtime-adapters/src/detectors/ollama.ts | 19 ++ packages/runtime-adapters/src/index.ts | 8 + 11 files changed, 488 insertions(+), 4 deletions(-) create mode 100644 packages/runtime-adapters/src/detectors/claude.ts create mode 100644 packages/runtime-adapters/src/detectors/cli-runtime.ts create mode 100644 packages/runtime-adapters/src/detectors/code.ts create mode 100644 packages/runtime-adapters/src/detectors/core-detectors.test.ts create mode 100644 packages/runtime-adapters/src/detectors/core-runtimes.ts create mode 100644 packages/runtime-adapters/src/detectors/git.ts create mode 100644 packages/runtime-adapters/src/detectors/lmstudio.ts create mode 100644 packages/runtime-adapters/src/detectors/node.ts create mode 100644 packages/runtime-adapters/src/detectors/ollama.ts diff --git a/docs/development/task-backlog.md b/docs/development/task-backlog.md index ffd9037..9c283d2 100644 --- a/docs/development/task-backlog.md +++ b/docs/development/task-backlog.md @@ -176,10 +176,10 @@ Status markers: **Tasks:** -- [ ] Use read-only version/path probes for each CLI-based runtime. -- [ ] Use local HTTP health checks only for Ollama and LM Studio when configured. -- [ ] Normalize missing runtime warnings for UI display. -- [ ] Commit with message `feat: add core runtime detectors`. +- [x] Use read-only version/path probes for each CLI-based runtime. +- [x] Use local HTTP health checks only for Ollama and LM Studio when configured. +- [x] Normalize missing runtime warnings for UI display. +- [x] Commit with message `feat: add core runtime detectors`. **Acceptance Criteria:** diff --git a/packages/runtime-adapters/src/detectors/claude.ts b/packages/runtime-adapters/src/detectors/claude.ts new file mode 100644 index 0000000..7331d2e --- /dev/null +++ b/packages/runtime-adapters/src/detectors/claude.ts @@ -0,0 +1,16 @@ +import { detectCliRuntime, type DetectCliRuntimeOptions } from "./cli-runtime.js"; + +export type DetectClaudeRuntimeOptions = DetectCliRuntimeOptions; + +export async function detectClaudeRuntime(options: DetectClaudeRuntimeOptions = {}) { + return await detectCliRuntime( + { + id: "claude", + name: "Claude Code", + binary: "claude", + versionCommand: "claude --version", + capabilities: { exec: true }, + }, + options, + ); +} diff --git a/packages/runtime-adapters/src/detectors/cli-runtime.ts b/packages/runtime-adapters/src/detectors/cli-runtime.ts new file mode 100644 index 0000000..4bd90d0 --- /dev/null +++ b/packages/runtime-adapters/src/detectors/cli-runtime.ts @@ -0,0 +1,138 @@ +import { runProbe, type RunProbeResult } from "../probe/run-probe.js"; +import { + createMissingRuntimeResult, + type RuntimeCapabilities, + type RuntimeDetectionResult, + type RuntimeScope, + type RuntimeStatus, + type RuntimeType, +} from "../runtime-types.js"; + +export interface DetectCliRuntimeOptions { + readonly lastDetectedAt?: string; + readonly runProbe?: (command: string) => Promise; +} + +export interface DetectLocalProviderRuntimeOptions extends DetectCliRuntimeOptions { + readonly healthUrl?: string; + readonly fetchHealth?: ( + url: string, + ) => Promise<{ readonly ok: boolean; readonly status: number }>; +} + +export interface CliRuntimeDefinition { + readonly id: RuntimeType; + readonly name: string; + readonly binary: string; + readonly versionCommand: string; + readonly capabilities: RuntimeCapabilities; +} + +export interface LocalProviderRuntimeDefinition extends CliRuntimeDefinition { + readonly defaultHealthUrl?: string; +} + +export async function detectCliRuntime( + definition: CliRuntimeDefinition, + options: DetectCliRuntimeOptions = {}, +): Promise { + const probe = options.runProbe ?? runProbe; + const lastDetectedAt = options.lastDetectedAt ?? new Date().toISOString(); + const pathProbe = await probe(`command -v ${definition.binary}`); + + if (pathProbe.exitCode !== 0 || pathProbe.stdout.trim().length === 0) { + return createMissingRuntimeResult({ + id: definition.id, + name: definition.name, + type: definition.id, + lastDetectedAt, + warnings: [`${definition.name} was not found on PATH.`], + }); + } + + const versionProbe = await probe(definition.versionCommand); + const version = versionProbe.exitCode === 0 ? firstOutputLine(versionProbe.stdout) : undefined; + const warnings = collectProbeWarnings(versionProbe); + const capabilities = { + ...definition.capabilities, + versionCommand: Boolean(version), + }; + + return { + id: definition.id, + name: definition.name, + type: definition.id, + status: "ready", + detected: true, + path: pathProbe.stdout.trim(), + scope: ["global"], + capabilities, + warnings, + lastDetectedAt, + ...(version ? { version } : {}), + }; +} + +export async function detectLocalProviderRuntime( + definition: LocalProviderRuntimeDefinition, + options: DetectLocalProviderRuntimeOptions = {}, +): Promise { + const cliResult = await detectCliRuntime(definition, options); + + if (!cliResult.detected) { + return cliResult; + } + + const healthUrl = options.healthUrl ?? definition.defaultHealthUrl; + if (!healthUrl) { + return cliResult; + } + + const fetchHealth = options.fetchHealth ?? defaultFetchHealth; + const healthResult = await fetchHealth(healthUrl); + + if (!healthResult.ok) { + return { + ...cliResult, + warnings: [ + ...cliResult.warnings, + `${definition.name} health check failed with HTTP ${healthResult.status}.`, + ], + }; + } + + return { + ...cliResult, + status: "localProviderReady" satisfies RuntimeStatus, + scope: ["global", "localProvider"] satisfies RuntimeScope[], + }; +} + +function firstOutputLine(output: string): string | undefined { + const firstLine = output + .split(/\r?\n/) + .map((line) => line.trim()) + .find((line) => line.length > 0); + + return firstLine; +} + +function collectProbeWarnings(result: RunProbeResult): string[] { + if (result.exitCode === 0 && !result.timedOut) { + return []; + } + + if (result.timedOut) { + return ["Version probe timed out."]; + } + + const warning = result.stderr.trim() || result.stdout.trim(); + return warning ? [warning] : ["Version probe failed."]; +} + +async function defaultFetchHealth( + url: string, +): Promise<{ readonly ok: boolean; readonly status: number }> { + const response = await fetch(url); + return { ok: response.ok, status: response.status }; +} diff --git a/packages/runtime-adapters/src/detectors/code.ts b/packages/runtime-adapters/src/detectors/code.ts new file mode 100644 index 0000000..436833f --- /dev/null +++ b/packages/runtime-adapters/src/detectors/code.ts @@ -0,0 +1,16 @@ +import { detectCliRuntime, type DetectCliRuntimeOptions } from "./cli-runtime.js"; + +export type DetectCodeRuntimeOptions = DetectCliRuntimeOptions; + +export async function detectCodeRuntime(options: DetectCodeRuntimeOptions = {}) { + return await detectCliRuntime( + { + id: "code", + name: "Visual Studio Code CLI", + binary: "code", + versionCommand: "code --version", + capabilities: { exec: true }, + }, + options, + ); +} diff --git a/packages/runtime-adapters/src/detectors/core-detectors.test.ts b/packages/runtime-adapters/src/detectors/core-detectors.test.ts new file mode 100644 index 0000000..e7aadc9 --- /dev/null +++ b/packages/runtime-adapters/src/detectors/core-detectors.test.ts @@ -0,0 +1,184 @@ +import { describe, expect, it } from "vitest"; + +import { detectClaudeRuntime } from "./claude.js"; +import { detectCodeRuntime } from "./code.js"; +import { detectCoreRuntimes } from "./core-runtimes.js"; +import { detectGitRuntime } from "./git.js"; +import { detectLmStudioRuntime } from "./lmstudio.js"; +import { detectNodeRuntime } from "./node.js"; +import { detectOllamaRuntime } from "./ollama.js"; +import type { RunProbeResult } from "../probe/run-probe.js"; + +function probeResult(overrides: Partial): RunProbeResult { + return { + stdout: "", + stderr: "", + exitCode: 0, + signal: null, + timedOut: false, + elapsedMs: 1, + ...overrides, + }; +} + +describe("core CLI runtime detectors", () => { + it("detects Claude from path and version probes", async () => { + const commands: string[] = []; + const result = await detectClaudeRuntime({ + lastDetectedAt: "2026-05-28T00:00:00.000Z", + runProbe: async (command) => { + commands.push(command); + if (command === "command -v claude") { + return probeResult({ stdout: "/opt/homebrew/bin/claude" }); + } + return probeResult({ stdout: "1.2.3" }); + }, + }); + + expect(commands).toEqual(["command -v claude", "claude --version"]); + expect(result).toMatchObject({ + id: "claude", + name: "Claude Code", + type: "claude", + status: "ready", + detected: true, + path: "/opt/homebrew/bin/claude", + version: "1.2.3", + scope: ["global"], + capabilities: { exec: true, versionCommand: true }, + warnings: [], + lastDetectedAt: "2026-05-28T00:00:00.000Z", + }); + }); + + it("normalizes missing CLI runtime warnings for UI display", async () => { + const result = await detectCodeRuntime({ + lastDetectedAt: "2026-05-28T00:00:00.000Z", + runProbe: async () => probeResult({ exitCode: 1, stderr: "" }), + }); + + expect(result).toEqual({ + id: "code", + name: "Visual Studio Code CLI", + type: "code", + status: "missing", + detected: false, + scope: [], + capabilities: {}, + warnings: ["Visual Studio Code CLI was not found on PATH."], + lastDetectedAt: "2026-05-28T00:00:00.000Z", + }); + }); + + it("detects Node, Git, and VS Code with normalized result rows", async () => { + const detectors = [ + { + detect: detectNodeRuntime, + pathCommand: "command -v node", + versionCommand: "node --version", + version: "v24.1.0", + id: "node", + }, + { + detect: detectGitRuntime, + pathCommand: "command -v git", + versionCommand: "git --version", + version: "git version 2.50.0", + id: "git", + }, + { + detect: detectCodeRuntime, + pathCommand: "command -v code", + versionCommand: "code --version", + version: "1.101.0\nabcdef", + id: "code", + }, + ] as const; + + const results = await Promise.all( + detectors.map((runtime) => + runtime.detect({ + lastDetectedAt: "2026-05-28T00:00:00.000Z", + runProbe: async (command) => { + if (command === runtime.pathCommand) { + return probeResult({ stdout: `/usr/local/bin/${runtime.id}` }); + } + expect(command).toBe(runtime.versionCommand); + return probeResult({ stdout: runtime.version }); + }, + }), + ), + ); + + expect(results.map((result) => result.id)).toEqual(["node", "git", "code"]); + expect(results.every((result) => result.detected)).toBe(true); + expect(results.every((result) => result.status === "ready")).toBe(true); + expect(results.every((result) => result.scope.join(",") === "global")).toBe(true); + }); +}); + +describe("local provider runtime detectors", () => { + it("detects Ollama local provider readiness from CLI and configured HTTP health check", async () => { + const result = await detectOllamaRuntime({ + healthUrl: "http://127.0.0.1:11434/api/tags", + lastDetectedAt: "2026-05-28T00:00:00.000Z", + runProbe: async (command) => { + if (command === "command -v ollama") { + return probeResult({ stdout: "/usr/local/bin/ollama" }); + } + return probeResult({ stdout: "ollama version is 0.9.0" }); + }, + fetchHealth: async (url) => { + expect(url).toBe("http://127.0.0.1:11434/api/tags"); + return { ok: true, status: 200 }; + }, + }); + + expect(result.status).toBe("localProviderReady"); + expect(result.scope).toEqual(["global", "localProvider"]); + expect(result.capabilities).toEqual({ chat: true, versionCommand: true }); + }); + + it("keeps LM Studio ready when no HTTP health check is configured", async () => { + let healthCalled = false; + const result = await detectLmStudioRuntime({ + lastDetectedAt: "2026-05-28T00:00:00.000Z", + runProbe: async (command) => { + if (command === "command -v lms") { + return probeResult({ stdout: "/usr/local/bin/lms" }); + } + return probeResult({ stdout: "0.3.0" }); + }, + fetchHealth: async () => { + healthCalled = true; + return { ok: true, status: 200 }; + }, + }); + + expect(healthCalled).toBe(false); + expect(result.status).toBe("ready"); + expect(result.scope).toEqual(["global"]); + }); +}); + +describe("detectCoreRuntimes", () => { + it("returns one normalized result list for all MVP runtime rows", async () => { + const result = await detectCoreRuntimes({ + lastDetectedAt: "2026-05-28T00:00:00.000Z", + runProbe: async () => probeResult({ exitCode: 1 }), + }); + + expect(result.map((runtime) => runtime.id)).toEqual([ + "codex", + "claude", + "ollama", + "lmstudio", + "node", + "git", + "code", + ]); + expect(result.every((runtime) => runtime.lastDetectedAt === "2026-05-28T00:00:00.000Z")).toBe( + true, + ); + }); +}); diff --git a/packages/runtime-adapters/src/detectors/core-runtimes.ts b/packages/runtime-adapters/src/detectors/core-runtimes.ts new file mode 100644 index 0000000..b046c89 --- /dev/null +++ b/packages/runtime-adapters/src/detectors/core-runtimes.ts @@ -0,0 +1,52 @@ +import { detectClaudeRuntime } from "./claude.js"; +import { detectCodeRuntime } from "./code.js"; +import { detectCodexRuntime } from "./codex.js"; +import { detectGitRuntime } from "./git.js"; +import { detectLmStudioRuntime } from "./lmstudio.js"; +import { detectNodeRuntime } from "./node.js"; +import { detectOllamaRuntime } from "./ollama.js"; +import type { RunProbeResult } from "../probe/run-probe.js"; +import type { RuntimeDetectionResult } from "../runtime-types.js"; + +export interface DetectCoreRuntimesOptions { + readonly workspaceRoot?: string; + readonly homeDir?: string; + readonly ollamaHealthUrl?: string; + readonly lmStudioHealthUrl?: string; + readonly lastDetectedAt?: string; + readonly runProbe?: (command: string) => Promise; + readonly fetchHealth?: ( + url: string, + ) => Promise<{ readonly ok: boolean; readonly status: number }>; +} + +export async function detectCoreRuntimes( + options: DetectCoreRuntimesOptions = {}, +): Promise { + const commonOptions = { + ...(options.lastDetectedAt ? { lastDetectedAt: options.lastDetectedAt } : {}), + ...(options.runProbe ? { runProbe: options.runProbe } : {}), + }; + + return await Promise.all([ + detectCodexRuntime({ + ...commonOptions, + ...(options.workspaceRoot ? { workspaceRoot: options.workspaceRoot } : {}), + ...(options.homeDir ? { homeDir: options.homeDir } : {}), + }), + detectClaudeRuntime(commonOptions), + detectOllamaRuntime({ + ...commonOptions, + ...(options.ollamaHealthUrl ? { healthUrl: options.ollamaHealthUrl } : {}), + ...(options.fetchHealth ? { fetchHealth: options.fetchHealth } : {}), + }), + detectLmStudioRuntime({ + ...commonOptions, + ...(options.lmStudioHealthUrl ? { healthUrl: options.lmStudioHealthUrl } : {}), + ...(options.fetchHealth ? { fetchHealth: options.fetchHealth } : {}), + }), + detectNodeRuntime(commonOptions), + detectGitRuntime(commonOptions), + detectCodeRuntime(commonOptions), + ]); +} diff --git a/packages/runtime-adapters/src/detectors/git.ts b/packages/runtime-adapters/src/detectors/git.ts new file mode 100644 index 0000000..cd43905 --- /dev/null +++ b/packages/runtime-adapters/src/detectors/git.ts @@ -0,0 +1,16 @@ +import { detectCliRuntime, type DetectCliRuntimeOptions } from "./cli-runtime.js"; + +export type DetectGitRuntimeOptions = DetectCliRuntimeOptions; + +export async function detectGitRuntime(options: DetectGitRuntimeOptions = {}) { + return await detectCliRuntime( + { + id: "git", + name: "Git", + binary: "git", + versionCommand: "git --version", + capabilities: {}, + }, + options, + ); +} diff --git a/packages/runtime-adapters/src/detectors/lmstudio.ts b/packages/runtime-adapters/src/detectors/lmstudio.ts new file mode 100644 index 0000000..8238c26 --- /dev/null +++ b/packages/runtime-adapters/src/detectors/lmstudio.ts @@ -0,0 +1,19 @@ +import { + detectLocalProviderRuntime, + type DetectLocalProviderRuntimeOptions, +} from "./cli-runtime.js"; + +export type DetectLmStudioRuntimeOptions = DetectLocalProviderRuntimeOptions; + +export async function detectLmStudioRuntime(options: DetectLmStudioRuntimeOptions = {}) { + return await detectLocalProviderRuntime( + { + id: "lmstudio", + name: "LM Studio", + binary: "lms", + versionCommand: "lms --version", + capabilities: { chat: true }, + }, + options, + ); +} diff --git a/packages/runtime-adapters/src/detectors/node.ts b/packages/runtime-adapters/src/detectors/node.ts new file mode 100644 index 0000000..2bc04e3 --- /dev/null +++ b/packages/runtime-adapters/src/detectors/node.ts @@ -0,0 +1,16 @@ +import { detectCliRuntime, type DetectCliRuntimeOptions } from "./cli-runtime.js"; + +export type DetectNodeRuntimeOptions = DetectCliRuntimeOptions; + +export async function detectNodeRuntime(options: DetectNodeRuntimeOptions = {}) { + return await detectCliRuntime( + { + id: "node", + name: "Node.js", + binary: "node", + versionCommand: "node --version", + capabilities: { exec: true }, + }, + options, + ); +} diff --git a/packages/runtime-adapters/src/detectors/ollama.ts b/packages/runtime-adapters/src/detectors/ollama.ts new file mode 100644 index 0000000..6ac85d9 --- /dev/null +++ b/packages/runtime-adapters/src/detectors/ollama.ts @@ -0,0 +1,19 @@ +import { + detectLocalProviderRuntime, + type DetectLocalProviderRuntimeOptions, +} from "./cli-runtime.js"; + +export type DetectOllamaRuntimeOptions = DetectLocalProviderRuntimeOptions; + +export async function detectOllamaRuntime(options: DetectOllamaRuntimeOptions = {}) { + return await detectLocalProviderRuntime( + { + id: "ollama", + name: "Ollama", + binary: "ollama", + versionCommand: "ollama --version", + capabilities: { chat: true }, + }, + options, + ); +} diff --git a/packages/runtime-adapters/src/index.ts b/packages/runtime-adapters/src/index.ts index 6379722..b3b2579 100644 --- a/packages/runtime-adapters/src/index.ts +++ b/packages/runtime-adapters/src/index.ts @@ -20,3 +20,11 @@ export { } from "./probe/run-probe.js"; export { detectCodexRuntime, type DetectCodexRuntimeOptions } from "./detectors/codex.js"; +export { detectCoreRuntimes, type DetectCoreRuntimesOptions } from "./detectors/core-runtimes.js"; + +export { detectClaudeRuntime, type DetectClaudeRuntimeOptions } from "./detectors/claude.js"; +export { detectCodeRuntime, type DetectCodeRuntimeOptions } from "./detectors/code.js"; +export { detectGitRuntime, type DetectGitRuntimeOptions } from "./detectors/git.js"; +export { detectLmStudioRuntime, type DetectLmStudioRuntimeOptions } from "./detectors/lmstudio.js"; +export { detectNodeRuntime, type DetectNodeRuntimeOptions } from "./detectors/node.js"; +export { detectOllamaRuntime, type DetectOllamaRuntimeOptions } from "./detectors/ollama.js"; From 3e5e74676977ef70c397af2d7ecccfc0fc9676fa Mon Sep 17 00:00:00 2001 From: eleven_qy <64311644+alph-cmky@users.noreply.github.com> Date: Thu, 28 May 2026 21:07:39 +0800 Subject: [PATCH 08/13] feat: expose runtime detection daemon api --- apps/daemon/package.json | 3 + apps/daemon/src/index.ts | 18 +++++ apps/daemon/src/routes/runtimes.ts | 41 ++++++++++ apps/daemon/src/server.test.ts | 120 +++++++++++++++++++++++++++++ apps/daemon/src/server.ts | 87 +++++++++++++++++++++ docs/development/task-backlog.md | 12 +-- pnpm-lock.yaml | 6 +- 7 files changed, 280 insertions(+), 7 deletions(-) create mode 100644 apps/daemon/src/routes/runtimes.ts create mode 100644 apps/daemon/src/server.test.ts create mode 100644 apps/daemon/src/server.ts diff --git a/apps/daemon/package.json b/apps/daemon/package.json index 7ed5dfd..60b325c 100644 --- a/apps/daemon/package.json +++ b/apps/daemon/package.json @@ -10,5 +10,8 @@ "lint": "echo \"@agentdeck/daemon lint is not scaffolded yet\"", "typecheck": "tsc -p tsconfig.json --noEmit", "format": "echo \"@agentdeck/daemon format is not scaffolded yet\"" + }, + "dependencies": { + "@agentdeck/runtime-adapters": "workspace:*" } } diff --git a/apps/daemon/src/index.ts b/apps/daemon/src/index.ts index 02b56ea..c070b0d 100644 --- a/apps/daemon/src/index.ts +++ b/apps/daemon/src/index.ts @@ -1 +1,19 @@ +import { pathToFileURL } from "node:url"; + +import { createDaemonServer, listenOnLocalhost } from "./server.js"; + +export { createDaemonServer, listenOnLocalhost } from "./server.js"; +export { getRuntimesResponse } from "./routes/runtimes.js"; +export type { + RuntimeDetectionRequest, + RuntimeDetector, + RuntimesResponse, +} from "./routes/runtimes.js"; + export const daemonAppName = "@agentdeck/daemon"; + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + const port = Number(process.env.AGENTDECK_DAEMON_PORT ?? "17345"); + const server = createDaemonServer(); + listenOnLocalhost(server, port); +} diff --git a/apps/daemon/src/routes/runtimes.ts b/apps/daemon/src/routes/runtimes.ts new file mode 100644 index 0000000..c9f1532 --- /dev/null +++ b/apps/daemon/src/routes/runtimes.ts @@ -0,0 +1,41 @@ +import type { RuntimeDetectionResult } from "@agentdeck/runtime-adapters"; + +export interface RuntimeDetectionRequest { + readonly workspaceRoot?: string; +} + +export interface RuntimesResponse { + readonly runtimes: readonly RuntimeDetectionResult[]; +} + +export type RuntimeDetector = ( + request: RuntimeDetectionRequest, +) => Promise; + +export async function getRuntimesResponse(input: { + readonly workspaceRoot?: string; + readonly detectRuntimes: RuntimeDetector; +}): Promise { + const runtimes = await input.detectRuntimes( + input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}, + ); + + return { + runtimes: runtimes.map(sanitizeRuntimeResult), + }; +} + +function sanitizeRuntimeResult(result: RuntimeDetectionResult): RuntimeDetectionResult { + return { + ...result, + warnings: result.warnings.map(redactSensitiveValues), + }; +} + +function redactSensitiveValues(value: string): string { + return value + .replace(/\b(api[_-]?key\s*[=:]\s*)([^\s]+)/gi, "$1[REDACTED]") + .replace(/\b(token\s*[=:]\s*)([^\s]+)/gi, "$1[REDACTED]") + .replace(/\b(password\s*[=:]\s*)([^\s]+)/gi, "$1[REDACTED]") + .replace(/\b(secret\s*[=:]\s*)([^\s]+)/gi, "$1[REDACTED]"); +} diff --git a/apps/daemon/src/server.test.ts b/apps/daemon/src/server.test.ts new file mode 100644 index 0000000..ab23a08 --- /dev/null +++ b/apps/daemon/src/server.test.ts @@ -0,0 +1,120 @@ +import { once } from "node:events"; +import type { Server } from "node:http"; +import type { AddressInfo } from "node:net"; + +import { afterEach, describe, expect, it } from "vitest"; + +import { createDaemonServer } from "./server.js"; +import type { RuntimeDetectionResult } from "@agentdeck/runtime-adapters"; + +const servers: Server[] = []; + +async function startTestServer( + options: Parameters[0] = {}, +): Promise { + const server = createDaemonServer(options); + servers.push(server); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + const address = server.address() as AddressInfo; + return `http://127.0.0.1:${address.port}`; +} + +function runtimeResult(overrides: Partial = {}): RuntimeDetectionResult { + return { + id: "node", + name: "Node.js", + type: "node", + status: "ready", + detected: true, + path: "/usr/local/bin/node", + version: "v24.1.0", + scope: ["global"], + capabilities: { exec: true, versionCommand: true }, + warnings: [], + lastDetectedAt: "2026-05-28T00:00:00.000Z", + ...overrides, + }; +} + +afterEach(async () => { + await Promise.all( + servers.splice(0).map(async (server) => { + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + return; + } + + resolve(); + }); + }); + }), + ); +}); + +describe("daemon HTTP API", () => { + it("returns a stable health payload from a localhost-bound server", async () => { + const baseUrl = await startTestServer(); + + const response = await fetch(`${baseUrl}/health`); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + ok: true, + service: "agentdeck-daemon", + version: "0.0.0", + }); + }); + + it("returns normalized runtime detection results for a workspace root", async () => { + const calls: string[] = []; + const baseUrl = await startTestServer({ + detectRuntimes: async (options) => { + calls.push(options.workspaceRoot ?? ""); + return [runtimeResult()]; + }, + }); + + const response = await fetch(`${baseUrl}/runtimes?workspaceRoot=/tmp/agentdeck`); + + expect(response.status).toBe(200); + expect(calls).toEqual(["/tmp/agentdeck"]); + expect(await response.json()).toEqual({ + runtimes: [runtimeResult()], + }); + }); + + it("rejects non-localhost browser origins", async () => { + const baseUrl = await startTestServer(); + + const response = await fetch(`${baseUrl}/health`, { + headers: { Origin: "https://example.com" }, + }); + + expect(response.status).toBe(403); + expect(await response.json()).toEqual({ + error: "origin_not_allowed", + message: "Only localhost origins can access the AgentDeck daemon.", + }); + }); + + it("redacts token-like values from runtime responses", async () => { + const baseUrl = await startTestServer({ + detectRuntimes: async () => [ + runtimeResult({ + warnings: ["failed with api_key=abc123 and token: xyz789"], + }), + ], + }); + + const response = await fetch(`${baseUrl}/runtimes`); + const body = JSON.stringify(await response.json()); + + expect(body).toContain("api_key=[REDACTED]"); + expect(body).toContain("token: [REDACTED]"); + expect(body).not.toContain("abc123"); + expect(body).not.toContain("xyz789"); + }); +}); diff --git a/apps/daemon/src/server.ts b/apps/daemon/src/server.ts new file mode 100644 index 0000000..a806aa5 --- /dev/null +++ b/apps/daemon/src/server.ts @@ -0,0 +1,87 @@ +import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; + +import { detectCoreRuntimes } from "@agentdeck/runtime-adapters"; + +import { getRuntimesResponse, type RuntimeDetector } from "./routes/runtimes.js"; + +export interface CreateDaemonServerOptions { + readonly detectRuntimes?: RuntimeDetector; +} + +const HEALTH_RESPONSE = { + ok: true, + service: "agentdeck-daemon", + version: "0.0.0", +} as const; + +export function createDaemonServer(options: CreateDaemonServerOptions = {}): Server { + const detectRuntimes = options.detectRuntimes ?? detectCoreRuntimes; + + return createServer(async (request, response) => { + try { + if (!isAllowedOrigin(request.headers.origin)) { + writeJson(response, 403, { + error: "origin_not_allowed", + message: "Only localhost origins can access the AgentDeck daemon.", + }); + return; + } + + const requestUrl = getRequestUrl(request); + + if (request.method === "GET" && requestUrl.pathname === "/health") { + writeJson(response, 200, HEALTH_RESPONSE); + return; + } + + if (request.method === "GET" && requestUrl.pathname === "/runtimes") { + const workspaceRoot = requestUrl.searchParams.get("workspaceRoot") ?? undefined; + const body = await getRuntimesResponse({ + ...(workspaceRoot ? { workspaceRoot } : {}), + detectRuntimes, + }); + writeJson(response, 200, body); + return; + } + + writeJson(response, 404, { + error: "not_found", + message: "Route not found.", + }); + } catch { + writeJson(response, 500, { + error: "internal_error", + message: "AgentDeck daemon request failed.", + }); + } + }); +} + +export function listenOnLocalhost(server: Server, port: number): void { + server.listen(port, "127.0.0.1"); +} + +function getRequestUrl(request: IncomingMessage): URL { + return new URL(request.url ?? "/", "http://127.0.0.1"); +} + +function isAllowedOrigin(origin: string | undefined): boolean { + if (!origin) { + return true; + } + + try { + const originUrl = new URL(origin); + return originUrl.hostname === "localhost" || originUrl.hostname === "127.0.0.1"; + } catch { + return false; + } +} + +function writeJson(response: ServerResponse, statusCode: number, body: unknown): void { + response.writeHead(statusCode, { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + }); + response.end(JSON.stringify(body)); +} diff --git a/docs/development/task-backlog.md b/docs/development/task-backlog.md index 9c283d2..ae0fb37 100644 --- a/docs/development/task-backlog.md +++ b/docs/development/task-backlog.md @@ -200,12 +200,12 @@ Status markers: **Tasks:** -- [ ] Start a local server bound to `127.0.0.1`. -- [ ] Add `GET /health`. -- [ ] Add `GET /runtimes?workspaceRoot=`. -- [ ] Return normalized runtime detection results. -- [ ] Reject non-localhost origins for browser requests. -- [ ] Commit with message `feat: expose runtime detection daemon api`. +- [x] Start a local server bound to `127.0.0.1`. +- [x] Add `GET /health`. +- [x] Add `GET /runtimes?workspaceRoot=`. +- [x] Return normalized runtime detection results. +- [x] Reject non-localhost origins for browser requests. +- [x] Commit with message `feat: expose runtime detection daemon api`. **Acceptance Criteria:** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c18970..5bf347b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,7 +39,11 @@ importers: specifier: ^4.1.7 version: 4.1.7(@types/node@25.9.1)(vite@8.0.14(@types/node@25.9.1)) - apps/daemon: {} + apps/daemon: + dependencies: + '@agentdeck/runtime-adapters': + specifier: workspace:* + version: link:../../packages/runtime-adapters apps/server: {} From 7c7fc5c59758a1cd2a7125775cc7794caeddbff0 Mon Sep 17 00:00:00 2001 From: eleven_qy <64311644+alph-cmky@users.noreply.github.com> Date: Thu, 28 May 2026 21:41:06 +0800 Subject: [PATCH 09/13] feat: add daemon event stream --- apps/daemon/src/events/event-bus.test.ts | 46 +++++++ apps/daemon/src/events/event-bus.ts | 35 +++++ apps/daemon/src/events/event-types.test.ts | 17 +++ apps/daemon/src/events/event-types.ts | 86 ++++++++++++ apps/daemon/src/index.ts | 5 + apps/daemon/src/routes/events.test.ts | 149 +++++++++++++++++++++ apps/daemon/src/routes/events.ts | 25 ++++ apps/daemon/src/server.ts | 20 +++ docs/development/task-backlog.md | 8 +- 9 files changed, 387 insertions(+), 4 deletions(-) create mode 100644 apps/daemon/src/events/event-bus.test.ts create mode 100644 apps/daemon/src/events/event-bus.ts create mode 100644 apps/daemon/src/events/event-types.test.ts create mode 100644 apps/daemon/src/events/event-types.ts create mode 100644 apps/daemon/src/routes/events.test.ts create mode 100644 apps/daemon/src/routes/events.ts diff --git a/apps/daemon/src/events/event-bus.test.ts b/apps/daemon/src/events/event-bus.test.ts new file mode 100644 index 0000000..6049318 --- /dev/null +++ b/apps/daemon/src/events/event-bus.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; + +import { createEventBus } from "./event-bus.js"; +import type { DaemonEvent } from "./event-types.js"; + +function taskStartedEvent(id: string): DaemonEvent { + return { + id, + type: "task.started", + createdAt: "2026-05-28T00:00:00.000Z", + payload: { + taskId: "task-1", + title: "Run workflow", + }, + }; +} + +describe("createEventBus", () => { + it("broadcasts events to multiple subscribers", () => { + const bus = createEventBus(); + const first: DaemonEvent[] = []; + const second: DaemonEvent[] = []; + + bus.subscribe((event) => first.push(event)); + bus.subscribe((event) => second.push(event)); + bus.publish(taskStartedEvent("event-1")); + + expect(first.map((event) => event.id)).toEqual(["event-1"]); + expect(second.map((event) => event.id)).toEqual(["event-1"]); + expect(bus.subscriberCount()).toBe(2); + }); + + it("removes subscribers when they unsubscribe", () => { + const bus = createEventBus(); + const received: DaemonEvent[] = []; + + const subscription = bus.subscribe((event) => received.push(event)); + expect(bus.subscriberCount()).toBe(1); + + subscription.unsubscribe(); + bus.publish(taskStartedEvent("event-2")); + + expect(received).toEqual([]); + expect(bus.subscriberCount()).toBe(0); + }); +}); diff --git a/apps/daemon/src/events/event-bus.ts b/apps/daemon/src/events/event-bus.ts new file mode 100644 index 0000000..93a1595 --- /dev/null +++ b/apps/daemon/src/events/event-bus.ts @@ -0,0 +1,35 @@ +import type { DaemonEvent } from "./event-types.js"; + +export interface EventSubscription { + readonly unsubscribe: () => void; +} + +export interface EventBus { + readonly publish: (event: DaemonEvent) => void; + readonly subscribe: (listener: (event: DaemonEvent) => void) => EventSubscription; + readonly subscriberCount: () => number; +} + +export function createEventBus(): EventBus { + const listeners = new Set<(event: DaemonEvent) => void>(); + + return { + publish(event) { + for (const listener of listeners) { + listener(event); + } + }, + subscribe(listener) { + listeners.add(listener); + + return { + unsubscribe() { + listeners.delete(listener); + }, + }; + }, + subscriberCount() { + return listeners.size; + }, + }; +} diff --git a/apps/daemon/src/events/event-types.test.ts b/apps/daemon/src/events/event-types.test.ts new file mode 100644 index 0000000..69ede42 --- /dev/null +++ b/apps/daemon/src/events/event-types.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; + +import { DAEMON_EVENT_TYPES } from "./event-types.js"; + +describe("DAEMON_EVENT_TYPES", () => { + it("defines all MVP daemon event types", () => { + expect(DAEMON_EVENT_TYPES).toEqual([ + "runtime.detected", + "task.started", + "task.output", + "task.diff", + "task.approvalRequested", + "task.completed", + "task.failed", + ]); + }); +}); diff --git a/apps/daemon/src/events/event-types.ts b/apps/daemon/src/events/event-types.ts new file mode 100644 index 0000000..1f33c82 --- /dev/null +++ b/apps/daemon/src/events/event-types.ts @@ -0,0 +1,86 @@ +import type { RuntimeStatus } from "@agentdeck/runtime-adapters"; + +export const DAEMON_EVENT_TYPES = [ + "runtime.detected", + "task.started", + "task.output", + "task.diff", + "task.approvalRequested", + "task.completed", + "task.failed", +] as const; + +export type DaemonEventType = (typeof DAEMON_EVENT_TYPES)[number]; + +export interface DaemonEventBase { + readonly id: string; + readonly type: TType; + readonly createdAt: string; + readonly payload: TPayload; +} + +export type RuntimeDetectedEvent = DaemonEventBase< + "runtime.detected", + { + readonly runtimeId: string; + readonly status: RuntimeStatus; + } +>; + +export type TaskStartedEvent = DaemonEventBase< + "task.started", + { + readonly taskId: string; + readonly title: string; + } +>; + +export type TaskOutputEvent = DaemonEventBase< + "task.output", + { + readonly taskId: string; + readonly stream: "stdout" | "stderr"; + readonly text: string; + } +>; + +export type TaskDiffEvent = DaemonEventBase< + "task.diff", + { + readonly taskId: string; + readonly diff: string; + } +>; + +export type TaskApprovalRequestedEvent = DaemonEventBase< + "task.approvalRequested", + { + readonly taskId: string; + readonly reason: string; + } +>; + +export type TaskCompletedEvent = DaemonEventBase< + "task.completed", + { + readonly taskId: string; + readonly exitCode: number; + } +>; + +export type TaskFailedEvent = DaemonEventBase< + "task.failed", + { + readonly taskId: string; + readonly message: string; + } +>; + +export type DaemonEvent = + | RuntimeDetectedEvent + | TaskStartedEvent + | TaskOutputEvent + | TaskDiffEvent + | TaskApprovalRequestedEvent + | TaskCompletedEvent + | TaskFailedEvent; diff --git a/apps/daemon/src/index.ts b/apps/daemon/src/index.ts index c070b0d..f43c533 100644 --- a/apps/daemon/src/index.ts +++ b/apps/daemon/src/index.ts @@ -3,6 +3,11 @@ import { pathToFileURL } from "node:url"; import { createDaemonServer, listenOnLocalhost } from "./server.js"; export { createDaemonServer, listenOnLocalhost } from "./server.js"; +export { createEventBus } from "./events/event-bus.js"; +export { DAEMON_EVENT_TYPES } from "./events/event-types.js"; +export { handleEventsStream } from "./routes/events.js"; +export type { DaemonEvent, DaemonEventType } from "./events/event-types.js"; +export type { EventBus, EventSubscription } from "./events/event-bus.js"; export { getRuntimesResponse } from "./routes/runtimes.js"; export type { RuntimeDetectionRequest, diff --git a/apps/daemon/src/routes/events.test.ts b/apps/daemon/src/routes/events.test.ts new file mode 100644 index 0000000..a5076c3 --- /dev/null +++ b/apps/daemon/src/routes/events.test.ts @@ -0,0 +1,149 @@ +import { once } from "node:events"; +import type { Server } from "node:http"; +import type { AddressInfo } from "node:net"; + +import { afterEach, describe, expect, it } from "vitest"; + +import { createEventBus } from "../events/event-bus.js"; +import type { DaemonEvent } from "../events/event-types.js"; +import { createDaemonServer } from "../server.js"; + +const servers: Server[] = []; +const abortControllers: AbortController[] = []; + +async function startTestServer(options: Parameters[0]): Promise { + const server = createDaemonServer(options); + servers.push(server); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + const address = server.address() as AddressInfo; + return `http://127.0.0.1:${address.port}`; +} + +async function closeServer(server: Server): Promise { + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + return; + } + + resolve(); + }); + }); +} + +function runtimeDetectedEvent(): DaemonEvent { + return { + id: "event-1", + type: "runtime.detected", + createdAt: "2026-05-28T00:00:00.000Z", + payload: { + runtimeId: "node", + status: "ready", + }, + }; +} + +afterEach(async () => { + for (const abortController of abortControllers.splice(0)) { + abortController.abort(); + } + + await Promise.all(servers.splice(0).map(closeServer)); +}); + +describe("GET /events", () => { + it("streams daemon events over SSE and removes disconnected clients", async () => { + const eventBus = createEventBus(); + const baseUrl = await startTestServer({ eventBus }); + const abortController = new AbortController(); + abortControllers.push(abortController); + + const response = await fetch(`${baseUrl}/events`, { + signal: abortController.signal, + }); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toContain("text/event-stream"); + expect(eventBus.subscriberCount()).toBe(1); + + eventBus.publish(runtimeDetectedEvent()); + + const reader = response.body?.getReader(); + expect(reader).toBeDefined(); + const text = await readUntil(reader, "event: runtime.detected"); + + expect(text).toContain("event: runtime.detected"); + expect(text).toContain('"runtimeId":"node"'); + + try { + abortController.abort(); + await reader?.cancel(); + } catch (error) { + if (!(error instanceof DOMException && error.name === "AbortError")) { + throw error; + } + } + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(eventBus.subscriberCount()).toBe(0); + }); + + it("broadcasts runtime detection events when runtimes are requested", async () => { + const eventBus = createEventBus(); + const received: DaemonEvent[] = []; + eventBus.subscribe((event) => received.push(event)); + const baseUrl = await startTestServer({ + eventBus, + detectRuntimes: async () => [ + { + id: "node", + name: "Node.js", + type: "node", + status: "ready", + detected: true, + scope: ["global"], + capabilities: {}, + warnings: [], + lastDetectedAt: "2026-05-28T00:00:00.000Z", + }, + ], + }); + + const response = await fetch(`${baseUrl}/runtimes`); + + expect(response.status).toBe(200); + expect(received).toHaveLength(1); + expect(received[0]).toMatchObject({ + type: "runtime.detected", + payload: { + runtimeId: "node", + status: "ready", + }, + }); + }); +}); + +async function readUntil( + reader: ReadableStreamDefaultReader | undefined, + expectedText: string, +): Promise { + if (!reader) { + throw new Error("Response body reader is not available."); + } + + const decoder = new TextDecoder(); + let text = ""; + + for (let attempt = 0; attempt < 5; attempt += 1) { + const chunk = await reader.read(); + text += decoder.decode(chunk.value, { stream: !chunk.done }); + + if (text.includes(expectedText) || chunk.done) { + return text; + } + } + + return text; +} diff --git a/apps/daemon/src/routes/events.ts b/apps/daemon/src/routes/events.ts new file mode 100644 index 0000000..43c33b0 --- /dev/null +++ b/apps/daemon/src/routes/events.ts @@ -0,0 +1,25 @@ +import type { ServerResponse } from "node:http"; + +import type { EventBus } from "../events/event-bus.js"; +import type { DaemonEvent } from "../events/event-types.js"; + +export function handleEventsStream(response: ServerResponse, eventBus: EventBus): void { + response.writeHead(200, { + "Content-Type": "text/event-stream; charset=utf-8", + "Cache-Control": "no-store", + Connection: "keep-alive", + }); + response.write(": connected\n\n"); + + const subscription = eventBus.subscribe((event) => { + response.write(formatServerSentEvent(event)); + }); + + response.on("close", () => { + subscription.unsubscribe(); + }); +} + +function formatServerSentEvent(event: DaemonEvent): string { + return `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`; +} diff --git a/apps/daemon/src/server.ts b/apps/daemon/src/server.ts index a806aa5..1ea9060 100644 --- a/apps/daemon/src/server.ts +++ b/apps/daemon/src/server.ts @@ -2,10 +2,13 @@ import { createServer, type IncomingMessage, type Server, type ServerResponse } import { detectCoreRuntimes } from "@agentdeck/runtime-adapters"; +import { createEventBus, type EventBus } from "./events/event-bus.js"; +import { handleEventsStream } from "./routes/events.js"; import { getRuntimesResponse, type RuntimeDetector } from "./routes/runtimes.js"; export interface CreateDaemonServerOptions { readonly detectRuntimes?: RuntimeDetector; + readonly eventBus?: EventBus; } const HEALTH_RESPONSE = { @@ -16,6 +19,7 @@ const HEALTH_RESPONSE = { export function createDaemonServer(options: CreateDaemonServerOptions = {}): Server { const detectRuntimes = options.detectRuntimes ?? detectCoreRuntimes; + const eventBus = options.eventBus ?? createEventBus(); return createServer(async (request, response) => { try { @@ -40,10 +44,26 @@ export function createDaemonServer(options: CreateDaemonServerOptions = {}): Ser ...(workspaceRoot ? { workspaceRoot } : {}), detectRuntimes, }); + for (const runtime of body.runtimes) { + eventBus.publish({ + id: `runtime.${runtime.id}.${Date.now()}`, + type: "runtime.detected", + createdAt: new Date().toISOString(), + payload: { + runtimeId: runtime.id, + status: runtime.status, + }, + }); + } writeJson(response, 200, body); return; } + if (request.method === "GET" && requestUrl.pathname === "/events") { + handleEventsStream(response, eventBus); + return; + } + writeJson(response, 404, { error: "not_found", message: "Route not found.", diff --git a/docs/development/task-backlog.md b/docs/development/task-backlog.md index ae0fb37..9974014 100644 --- a/docs/development/task-backlog.md +++ b/docs/development/task-backlog.md @@ -225,10 +225,10 @@ Status markers: **Tasks:** -- [ ] Define event types for `runtime.detected`, `task.started`, `task.output`, `task.diff`, `task.approvalRequested`, `task.completed`, and `task.failed`. -- [ ] Add SSE endpoint `GET /events`. -- [ ] Broadcast runtime detection lifecycle events. -- [ ] Commit with message `feat: add daemon event stream`. +- [x] Define event types for `runtime.detected`, `task.started`, `task.output`, `task.diff`, `task.approvalRequested`, `task.completed`, and `task.failed`. +- [x] Add SSE endpoint `GET /events`. +- [x] Broadcast runtime detection lifecycle events. +- [x] Commit with message `feat: add daemon event stream`. **Acceptance Criteria:** From 9298458915e77069b03fbb928dc8309d39df9e00 Mon Sep 17 00:00:00 2001 From: eleven_qy <64311644+alph-cmky@users.noreply.github.com> Date: Thu, 28 May 2026 23:28:42 +0800 Subject: [PATCH 10/13] feat: define agent schema --- apps/server/src/agents/agent-schema.test.ts | 106 +++++++++++++ apps/server/src/agents/agent-schema.ts | 158 ++++++++++++++++++++ apps/server/src/index.ts | 17 +++ docs/development/task-backlog.md | 8 +- 4 files changed, 285 insertions(+), 4 deletions(-) create mode 100644 apps/server/src/agents/agent-schema.test.ts create mode 100644 apps/server/src/agents/agent-schema.ts diff --git a/apps/server/src/agents/agent-schema.test.ts b/apps/server/src/agents/agent-schema.test.ts new file mode 100644 index 0000000..a63d151 --- /dev/null +++ b/apps/server/src/agents/agent-schema.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, it } from "vitest"; + +import { + AGENT_MEMORY_SCOPES, + AGENT_RUNTIME_PREFERENCES, + createAgentDefinition, + validateAgentDefinition, +} from "./agent-schema.js"; + +const baseAgentInput = { + id: "agent-reviewer", + name: "Reviewer", + description: "Reviews code changes", + prompt: "Review the current diff and report risks.", + model: "gpt-5", + tools: ["git.diff", "repo.read"], + memoryScope: "workspace", + runtimePreference: "codex", + workspaceRoot: "/tmp/agentdeck", +} as const; + +describe("agent schema constants", () => { + it("defines MVP memory scopes and runtime preferences", () => { + expect(AGENT_MEMORY_SCOPES).toEqual(["none", "workspace", "project"]); + expect(AGENT_RUNTIME_PREFERENCES).toEqual(["codex", "claude", "auto"]); + }); +}); + +describe("createAgentDefinition", () => { + it("normalizes all MVP agent fields and defaults permissions to read-only", () => { + const result = createAgentDefinition(baseAgentInput); + + expect(result.success).toBe(true); + if (!result.success) { + throw new Error(result.errors.join("\n")); + } + + expect(result.value).toEqual({ + ...baseAgentInput, + permissions: { + read: true, + write: false, + install: false, + arbitraryCommands: false, + commit: false, + push: false, + }, + }); + }); + + it("rejects missing prompts and non-explicit workspace roots", () => { + const result = createAgentDefinition({ + ...baseAgentInput, + prompt: " ", + workspaceRoot: "", + }); + + expect(result).toEqual({ + success: false, + errors: ["prompt is required.", "workspaceRoot must be an explicit absolute path."], + }); + }); + + it("rejects invalid permissions that allow writes, installs, commands, commits, or pushes", () => { + const result = createAgentDefinition({ + ...baseAgentInput, + permissions: { + read: true, + write: true, + install: true, + arbitraryCommands: true, + commit: true, + push: true, + }, + }); + + expect(result).toEqual({ + success: false, + errors: [ + "permissions.write must default to false for new agents.", + "permissions.install must default to false for new agents.", + "permissions.arbitraryCommands must default to false for new agents.", + "permissions.commit must default to false for new agents.", + "permissions.push must default to false for new agents.", + ], + }); + }); +}); + +describe("validateAgentDefinition", () => { + it("accepts an already normalized read-only agent definition", () => { + const result = validateAgentDefinition({ + ...baseAgentInput, + permissions: { + read: true, + write: false, + install: false, + arbitraryCommands: false, + commit: false, + push: false, + }, + }); + + expect(result).toEqual({ success: true, errors: [] }); + }); +}); diff --git a/apps/server/src/agents/agent-schema.ts b/apps/server/src/agents/agent-schema.ts new file mode 100644 index 0000000..03056c4 --- /dev/null +++ b/apps/server/src/agents/agent-schema.ts @@ -0,0 +1,158 @@ +import { isAbsolute } from "node:path"; + +export const AGENT_MEMORY_SCOPES = ["none", "workspace", "project"] as const; +export const AGENT_RUNTIME_PREFERENCES = ["codex", "claude", "auto"] as const; + +export type AgentMemoryScope = (typeof AGENT_MEMORY_SCOPES)[number]; +export type AgentRuntimePreference = (typeof AGENT_RUNTIME_PREFERENCES)[number]; + +export interface AgentPermissions { + readonly read: boolean; + readonly write: boolean; + readonly install: boolean; + readonly arbitraryCommands: boolean; + readonly commit: boolean; + readonly push: boolean; +} + +export interface AgentDefinition { + readonly id: string; + readonly name: string; + readonly description: string; + readonly prompt: string; + readonly model: string; + readonly tools: readonly string[]; + readonly permissions: AgentPermissions; + readonly memoryScope: AgentMemoryScope; + readonly runtimePreference: AgentRuntimePreference; + readonly workspaceRoot: string; +} + +export type AgentDefinitionInput = Omit & { + readonly permissions?: Partial; +}; + +export type AgentSchemaResult = + | { + readonly success: true; + readonly value: T; + } + | { + readonly success: false; + readonly errors: readonly string[]; + }; + +export interface AgentValidationResult { + readonly success: boolean; + readonly errors: readonly string[]; +} + +const READ_ONLY_PERMISSIONS: AgentPermissions = { + read: true, + write: false, + install: false, + arbitraryCommands: false, + commit: false, + push: false, +}; + +export function createAgentDefinition( + input: AgentDefinitionInput, +): AgentSchemaResult { + const permissions = { + ...READ_ONLY_PERMISSIONS, + ...input.permissions, + }; + const agent: AgentDefinition = { + id: input.id.trim(), + name: input.name.trim(), + description: input.description.trim(), + prompt: input.prompt.trim(), + model: input.model.trim(), + tools: input.tools.map((tool) => tool.trim()).filter((tool) => tool.length > 0), + permissions, + memoryScope: input.memoryScope, + runtimePreference: input.runtimePreference, + workspaceRoot: input.workspaceRoot.trim(), + }; + const validation = validateAgentDefinition(agent); + + if (!validation.success) { + return { + success: false, + errors: validation.errors, + }; + } + + return { + success: true, + value: agent, + }; +} + +export function validateAgentDefinition(agent: AgentDefinition): AgentValidationResult { + const errors: string[] = []; + + if (!agent.id.trim()) { + errors.push("id is required."); + } + + if (!agent.name.trim()) { + errors.push("name is required."); + } + + if (!agent.prompt.trim()) { + errors.push("prompt is required."); + } + + if (!agent.model.trim()) { + errors.push("model is required."); + } + + if (!isExplicitWorkspaceRoot(agent.workspaceRoot)) { + errors.push("workspaceRoot must be an explicit absolute path."); + } + + if (!isAgentMemoryScope(agent.memoryScope)) { + errors.push("memoryScope is invalid."); + } + + if (!isAgentRuntimePreference(agent.runtimePreference)) { + errors.push("runtimePreference is invalid."); + } + + validateReadOnlyPermissions(agent.permissions, errors); + + return { + success: errors.length === 0, + errors, + }; +} + +export function getReadOnlyAgentPermissions(): AgentPermissions { + return { ...READ_ONLY_PERMISSIONS }; +} + +function validateReadOnlyPermissions(permissions: AgentPermissions, errors: string[]): void { + if (!permissions.read) { + errors.push("permissions.read must be true."); + } + + for (const permission of ["write", "install", "arbitraryCommands", "commit", "push"] as const) { + if (permissions[permission]) { + errors.push(`permissions.${permission} must default to false for new agents.`); + } + } +} + +function isExplicitWorkspaceRoot(value: string): boolean { + return value.trim().length > 0 && isAbsolute(value); +} + +function isAgentMemoryScope(value: string): value is AgentMemoryScope { + return AGENT_MEMORY_SCOPES.includes(value as AgentMemoryScope); +} + +function isAgentRuntimePreference(value: string): value is AgentRuntimePreference { + return AGENT_RUNTIME_PREFERENCES.includes(value as AgentRuntimePreference); +} diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 0feeded..ad1af84 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -1 +1,18 @@ export const serverAppName = "@agentdeck/server"; + +export { + AGENT_MEMORY_SCOPES, + AGENT_RUNTIME_PREFERENCES, + createAgentDefinition, + getReadOnlyAgentPermissions, + validateAgentDefinition, +} from "./agents/agent-schema.js"; +export type { + AgentDefinition, + AgentDefinitionInput, + AgentMemoryScope, + AgentPermissions, + AgentRuntimePreference, + AgentSchemaResult, + AgentValidationResult, +} from "./agents/agent-schema.js"; diff --git a/docs/development/task-backlog.md b/docs/development/task-backlog.md index 9974014..344b8c7 100644 --- a/docs/development/task-backlog.md +++ b/docs/development/task-backlog.md @@ -248,10 +248,10 @@ Status markers: **Tasks:** -- [ ] Define schema fields for `id`, `name`, `description`, `prompt`, `model`, `tools`, `permissions`, `memoryScope`, `runtimePreference`, and `workspaceRoot`. -- [ ] Require new agents to default to read-only permissions. -- [ ] Validate workspace root is explicit. -- [ ] Commit with message `feat: define agent schema`. +- [x] Define schema fields for `id`, `name`, `description`, `prompt`, `model`, `tools`, `permissions`, `memoryScope`, `runtimePreference`, and `workspaceRoot`. +- [x] Require new agents to default to read-only permissions. +- [x] Validate workspace root is explicit. +- [x] Commit with message `feat: define agent schema`. **Acceptance Criteria:** From 3eb41780e1f314b563949887c7f218588d948007 Mon Sep 17 00:00:00 2001 From: eleven_qy <64311644+alph-cmky@users.noreply.github.com> Date: Thu, 28 May 2026 23:34:05 +0800 Subject: [PATCH 11/13] feat: add agent registry service --- apps/server/src/agents/agent-repository.ts | 76 +++++++++++++++ apps/server/src/agents/agent-schema.ts | 5 + apps/server/src/agents/agent-service.test.ts | 94 +++++++++++++++++++ apps/server/src/agents/agent-service.ts | 99 ++++++++++++++++++++ apps/server/src/index.ts | 9 ++ docs/development/task-backlog.md | 8 +- 6 files changed, 287 insertions(+), 4 deletions(-) create mode 100644 apps/server/src/agents/agent-repository.ts create mode 100644 apps/server/src/agents/agent-service.test.ts create mode 100644 apps/server/src/agents/agent-service.ts diff --git a/apps/server/src/agents/agent-repository.ts b/apps/server/src/agents/agent-repository.ts new file mode 100644 index 0000000..c9d0046 --- /dev/null +++ b/apps/server/src/agents/agent-repository.ts @@ -0,0 +1,76 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname } from "node:path"; + +import type { PersistedAgentDefinition } from "./agent-schema.js"; + +export interface AgentRepository { + readonly list: () => Promise; + readonly get: (id: string) => Promise; + readonly save: (agent: PersistedAgentDefinition) => Promise; + readonly delete: (id: string) => Promise; +} + +interface AgentStoreFile { + readonly agents: readonly PersistedAgentDefinition[]; +} + +export function createFileAgentRepository(storePath: string): AgentRepository { + return { + async list() { + return await readAgents(storePath); + }, + async get(id) { + const agents = await readAgents(storePath); + return agents.find((agent) => agent.id === id); + }, + async save(agent) { + const agents = await readAgents(storePath); + const nextAgents = [...agents.filter((existing) => existing.id !== agent.id), agent].sort( + (left, right) => left.id.localeCompare(right.id), + ); + await writeAgents(storePath, nextAgents); + }, + async delete(id) { + const agents = await readAgents(storePath); + const nextAgents = agents.filter((agent) => agent.id !== id); + + if (nextAgents.length === agents.length) { + return false; + } + + await writeAgents(storePath, nextAgents); + return true; + }, + }; +} + +async function readAgents(storePath: string): Promise { + try { + const contents = await readFile(storePath, "utf8"); + const parsed = JSON.parse(contents) as AgentStoreFile; + return [...(parsed.agents ?? [])]; + } catch (error) { + if (error instanceof Error && "code" in error && error.code === "ENOENT") { + return []; + } + + throw error; + } +} + +async function writeAgents( + storePath: string, + agents: readonly PersistedAgentDefinition[], +): Promise { + await mkdir(dirname(storePath), { recursive: true }); + await writeFile( + storePath, + `${JSON.stringify( + { + agents, + } satisfies AgentStoreFile, + null, + 2, + )}\n`, + ); +} diff --git a/apps/server/src/agents/agent-schema.ts b/apps/server/src/agents/agent-schema.ts index 03056c4..1e4fd3b 100644 --- a/apps/server/src/agents/agent-schema.ts +++ b/apps/server/src/agents/agent-schema.ts @@ -28,6 +28,11 @@ export interface AgentDefinition { readonly workspaceRoot: string; } +export interface PersistedAgentDefinition extends AgentDefinition { + readonly createdAt: string; + readonly updatedAt: string; +} + export type AgentDefinitionInput = Omit & { readonly permissions?: Partial; }; diff --git a/apps/server/src/agents/agent-service.test.ts b/apps/server/src/agents/agent-service.test.ts new file mode 100644 index 0000000..eb364be --- /dev/null +++ b/apps/server/src/agents/agent-service.test.ts @@ -0,0 +1,94 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +import { afterEach, describe, expect, it } from "vitest"; + +import { createAgentRegistryService } from "./agent-service.js"; + +const tempDirs: string[] = []; + +async function createTempStorePath(): Promise { + const tempDir = await mkdtemp(join(tmpdir(), "agentdeck-agent-registry-")); + tempDirs.push(tempDir); + return join(tempDir, "agents.json"); +} + +const baseInput = { + id: "reviewer", + name: "Reviewer", + description: "Reviews code", + prompt: "Review the current diff.", + model: "gpt-5", + tools: ["repo.read", "git.diff"], + memoryScope: "workspace", + runtimePreference: "codex", + workspaceRoot: "/tmp/agentdeck", +} as const; + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +describe("createAgentRegistryService", () => { + it("creates, lists, gets, updates, and deletes agents", async () => { + const service = createAgentRegistryService({ + storePath: await createTempStorePath(), + now: () => "2026-05-28T00:00:00.000Z", + }); + + const created = await service.create(baseInput); + + expect(created).toMatchObject({ + ...baseInput, + createdAt: "2026-05-28T00:00:00.000Z", + updatedAt: "2026-05-28T00:00:00.000Z", + }); + expect(await service.list()).toEqual([created]); + expect(await service.get("reviewer")).toEqual(created); + + const updated = await service.update("reviewer", { + name: "Senior Reviewer", + prompt: "Review the current diff and focus on regressions.", + now: () => "2026-05-28T01:00:00.000Z", + }); + + expect(updated.name).toBe("Senior Reviewer"); + expect(updated.prompt).toBe("Review the current diff and focus on regressions."); + expect(updated.createdAt).toBe("2026-05-28T00:00:00.000Z"); + expect(updated.updatedAt).toBe("2026-05-28T01:00:00.000Z"); + + expect(await service.delete("reviewer")).toBe(true); + expect(await service.get("reviewer")).toBeUndefined(); + }); + + it("rejects invalid agents before persistence", async () => { + const service = createAgentRegistryService({ + storePath: await createTempStorePath(), + }); + + await expect( + service.create({ + ...baseInput, + prompt: "", + permissions: { write: true }, + }), + ).rejects.toThrow("prompt is required."); + + expect(await service.list()).toEqual([]); + }); + + it("persists agent definitions across service instances", async () => { + const storePath = await createTempStorePath(); + const firstService = createAgentRegistryService({ + storePath, + now: () => "2026-05-28T00:00:00.000Z", + }); + const created = await firstService.create(baseInput); + + const secondService = createAgentRegistryService({ storePath }); + + expect(await secondService.get("reviewer")).toEqual(created); + expect(await secondService.list()).toEqual([created]); + }); +}); diff --git a/apps/server/src/agents/agent-service.ts b/apps/server/src/agents/agent-service.ts new file mode 100644 index 0000000..96e3d4d --- /dev/null +++ b/apps/server/src/agents/agent-service.ts @@ -0,0 +1,99 @@ +import { + createAgentDefinition, + type AgentDefinitionInput, + type PersistedAgentDefinition, +} from "./agent-schema.js"; +import { createFileAgentRepository, type AgentRepository } from "./agent-repository.js"; + +export type AgentUpdateInput = Partial> & { + readonly now?: () => string; +}; + +export interface AgentRegistryService { + readonly create: (input: AgentDefinitionInput) => Promise; + readonly list: () => Promise; + readonly get: (id: string) => Promise; + readonly update: (id: string, input: AgentUpdateInput) => Promise; + readonly delete: (id: string) => Promise; +} + +export interface CreateAgentRegistryServiceOptions { + readonly storePath?: string; + readonly repository?: AgentRepository; + readonly now?: () => string; +} + +export function createAgentRegistryService( + options: CreateAgentRegistryServiceOptions = {}, +): AgentRegistryService { + const repository = + options.repository ?? createFileAgentRepository(options.storePath ?? ".agentdeck/agents.json"); + const now = options.now ?? (() => new Date().toISOString()); + + return { + async create(input) { + const result = createAgentDefinition(input); + if (!result.success) { + throw new AgentValidationError(result.errors); + } + + const timestamp = now(); + const persisted = { + ...result.value, + createdAt: timestamp, + updatedAt: timestamp, + }; + await repository.save(persisted); + return persisted; + }, + async list() { + return await repository.list(); + }, + async get(id) { + return await repository.get(id); + }, + async update(id, input) { + const existing = await repository.get(id); + if (!existing) { + throw new Error(`Agent "${id}" was not found.`); + } + + const result = createAgentDefinition({ + id: existing.id, + name: input.name ?? existing.name, + description: input.description ?? existing.description, + prompt: input.prompt ?? existing.prompt, + model: input.model ?? existing.model, + tools: input.tools ?? existing.tools, + permissions: input.permissions ?? existing.permissions, + memoryScope: input.memoryScope ?? existing.memoryScope, + runtimePreference: input.runtimePreference ?? existing.runtimePreference, + workspaceRoot: input.workspaceRoot ?? existing.workspaceRoot, + }); + if (!result.success) { + throw new AgentValidationError(result.errors); + } + + const persisted = { + ...result.value, + createdAt: existing.createdAt, + updatedAt: (input.now ?? now)(), + }; + await repository.save(persisted); + return persisted; + }, + async delete(id) { + return await repository.delete(id); + }, + }; +} + +export class AgentValidationError extends Error { + readonly errors: readonly string[]; + + constructor(errors: readonly string[]) { + super(errors.join(" ")); + this.name = "AgentValidationError"; + this.errors = errors; + } +} diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index ad1af84..5191b47 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -15,4 +15,13 @@ export type { AgentRuntimePreference, AgentSchemaResult, AgentValidationResult, + PersistedAgentDefinition, } from "./agents/agent-schema.js"; +export { createFileAgentRepository } from "./agents/agent-repository.js"; +export type { AgentRepository } from "./agents/agent-repository.js"; +export { AgentValidationError, createAgentRegistryService } from "./agents/agent-service.js"; +export type { + AgentRegistryService, + AgentUpdateInput, + CreateAgentRegistryServiceOptions, +} from "./agents/agent-service.js"; diff --git a/docs/development/task-backlog.md b/docs/development/task-backlog.md index 344b8c7..24f72b1 100644 --- a/docs/development/task-backlog.md +++ b/docs/development/task-backlog.md @@ -270,10 +270,10 @@ Status markers: **Tasks:** -- [ ] Implement create, list, get, update, and delete operations. -- [ ] Enforce schema validation before persistence. -- [ ] Preserve created/updated timestamps. -- [ ] Commit with message `feat: add agent registry service`. +- [x] Implement create, list, get, update, and delete operations. +- [x] Enforce schema validation before persistence. +- [x] Preserve created/updated timestamps. +- [x] Commit with message `feat: add agent registry service`. **Acceptance Criteria:** From 45f418acc4458c81ccc68a96f0e813520a8ca96a Mon Sep 17 00:00:00 2001 From: eleven_qy <64311644+alph-cmky@users.noreply.github.com> Date: Fri, 29 May 2026 12:06:19 +0800 Subject: [PATCH 12/13] feat: define chat and task schemas --- apps/server/src/chat/chat-schema.test.ts | 72 +++++++++++++++ apps/server/src/chat/chat-schema.ts | 102 ++++++++++++++++++++++ apps/server/src/index.ts | 19 ++++ apps/server/src/tasks/task-schema.test.ts | 76 ++++++++++++++++ apps/server/src/tasks/task-schema.ts | 81 +++++++++++++++++ docs/development/task-backlog.md | 8 +- 6 files changed, 354 insertions(+), 4 deletions(-) create mode 100644 apps/server/src/chat/chat-schema.test.ts create mode 100644 apps/server/src/chat/chat-schema.ts create mode 100644 apps/server/src/tasks/task-schema.test.ts create mode 100644 apps/server/src/tasks/task-schema.ts diff --git a/apps/server/src/chat/chat-schema.test.ts b/apps/server/src/chat/chat-schema.test.ts new file mode 100644 index 0000000..84ec175 --- /dev/null +++ b/apps/server/src/chat/chat-schema.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from "vitest"; + +import { + createChannel, + createConversation, + createMessage, + createThread, + parseAgentMentions, +} from "./chat-schema.js"; + +describe("chat schema", () => { + it("creates normalized conversation, channel, thread, and message models", () => { + const channel = createChannel({ + id: "channel-main", + name: " Main ", + createdAt: "2026-05-29T00:00:00.000Z", + }); + const thread = createThread({ + id: "thread-1", + channelId: "channel-main", + title: " Review ", + createdAt: "2026-05-29T00:01:00.000Z", + }); + const message = createMessage({ + id: "message-1", + threadId: "thread-1", + authorId: "user-1", + body: " @reviewer please check this ", + createdAt: "2026-05-29T00:02:00.000Z", + }); + const conversation = createConversation({ + id: "conversation-1", + channel, + threads: [thread], + messages: [message], + }); + + expect(conversation).toEqual({ + id: "conversation-1", + channel: { + id: "channel-main", + name: "Main", + createdAt: "2026-05-29T00:00:00.000Z", + }, + threads: [ + { + id: "thread-1", + channelId: "channel-main", + title: "Review", + createdAt: "2026-05-29T00:01:00.000Z", + }, + ], + messages: [ + { + id: "message-1", + threadId: "thread-1", + authorId: "user-1", + body: "@reviewer please check this", + mentions: [{ agentId: "reviewer", label: "@reviewer", start: 0, end: 9 }], + createdAt: "2026-05-29T00:02:00.000Z", + }, + ], + }); + }); + + it("parses unique @agent mentions into structured references", () => { + expect(parseAgentMentions("@reviewer please ask @builder and @reviewer again")).toEqual([ + { agentId: "reviewer", label: "@reviewer", start: 0, end: 9 }, + { agentId: "builder", label: "@builder", start: 21, end: 29 }, + ]); + }); +}); diff --git a/apps/server/src/chat/chat-schema.ts b/apps/server/src/chat/chat-schema.ts new file mode 100644 index 0000000..ce11191 --- /dev/null +++ b/apps/server/src/chat/chat-schema.ts @@ -0,0 +1,102 @@ +export interface AgentMention { + readonly agentId: string; + readonly label: string; + readonly start: number; + readonly end: number; +} + +export interface Channel { + readonly id: string; + readonly name: string; + readonly createdAt: string; +} + +export interface Thread { + readonly id: string; + readonly channelId: string; + readonly title: string; + readonly createdAt: string; +} + +export interface Message { + readonly id: string; + readonly threadId: string; + readonly authorId: string; + readonly body: string; + readonly mentions: readonly AgentMention[]; + readonly createdAt: string; +} + +export interface Conversation { + readonly id: string; + readonly channel: Channel; + readonly threads: readonly Thread[]; + readonly messages: readonly Message[]; +} + +export function createChannel(input: Channel): Channel { + return { + id: input.id.trim(), + name: input.name.trim(), + createdAt: input.createdAt, + }; +} + +export function createThread(input: Thread): Thread { + return { + id: input.id.trim(), + channelId: input.channelId.trim(), + title: input.title.trim(), + createdAt: input.createdAt, + }; +} + +export function createMessage( + input: Omit & { readonly mentions?: readonly AgentMention[] }, +): Message { + const body = input.body.trim(); + + return { + id: input.id.trim(), + threadId: input.threadId.trim(), + authorId: input.authorId.trim(), + body, + mentions: input.mentions ?? parseAgentMentions(body), + createdAt: input.createdAt, + }; +} + +export function createConversation(input: Conversation): Conversation { + return { + id: input.id.trim(), + channel: input.channel, + threads: input.threads, + messages: input.messages, + }; +} + +export function parseAgentMentions(body: string): AgentMention[] { + const mentions: AgentMention[] = []; + const seenAgentIds = new Set(); + const mentionPattern = /(^|[^\w])@([a-zA-Z][a-zA-Z0-9_-]*)\b/g; + + for (const match of body.matchAll(mentionPattern)) { + const prefix = match[1] ?? ""; + const agentId = match[2]?.toLowerCase(); + if (!agentId || seenAgentIds.has(agentId)) { + continue; + } + + const start = match.index + prefix.length; + const label = `@${agentId}`; + mentions.push({ + agentId, + label, + start, + end: start + label.length, + }); + seenAgentIds.add(agentId); + } + + return mentions; +} diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 5191b47..84becc9 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -25,3 +25,22 @@ export type { AgentUpdateInput, CreateAgentRegistryServiceOptions, } from "./agents/agent-service.js"; +export { + createChannel, + createConversation, + createMessage, + createThread, + parseAgentMentions, +} from "./chat/chat-schema.js"; +export type { AgentMention, Channel, Conversation, Message, Thread } from "./chat/chat-schema.js"; +export { + TASK_STATUSES, + createTaskRefsForMessage, + validateMentionedAgentsExist, +} from "./tasks/task-schema.js"; +export type { + TaskRef, + TaskSchemaResult, + TaskStatus, + TaskValidationResult, +} from "./tasks/task-schema.js"; diff --git a/apps/server/src/tasks/task-schema.test.ts b/apps/server/src/tasks/task-schema.test.ts new file mode 100644 index 0000000..be4f3eb --- /dev/null +++ b/apps/server/src/tasks/task-schema.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from "vitest"; + +import { createMessage } from "../chat/chat-schema.js"; +import { createTaskRefsForMessage, validateMentionedAgentsExist } from "./task-schema.js"; +import type { PersistedAgentDefinition } from "../agents/agent-schema.js"; + +function agent(id: string): PersistedAgentDefinition { + return { + id, + name: id, + description: "Agent", + prompt: "Do the work.", + model: "gpt-5", + tools: [], + permissions: { + read: true, + write: false, + install: false, + arbitraryCommands: false, + commit: false, + push: false, + }, + memoryScope: "workspace", + runtimePreference: "codex", + workspaceRoot: "/tmp/agentdeck", + createdAt: "2026-05-29T00:00:00.000Z", + updatedAt: "2026-05-29T00:00:00.000Z", + }; +} + +describe("task schema", () => { + it("turns a chat message with @reviewer into a pending task linked to an agent", () => { + const message = createMessage({ + id: "message-1", + threadId: "thread-1", + authorId: "user-1", + body: "@reviewer please review this diff", + createdAt: "2026-05-29T00:01:00.000Z", + }); + + const result = createTaskRefsForMessage({ + message, + agents: [agent("reviewer")], + now: () => "2026-05-29T00:02:00.000Z", + }); + + expect(result).toEqual({ + success: true, + value: [ + { + id: "task-message-1-reviewer", + agentId: "reviewer", + messageId: "message-1", + threadId: "thread-1", + status: "pending", + createdAt: "2026-05-29T00:02:00.000Z", + }, + ], + }); + }); + + it("rejects task creation when mentioned agents do not exist", () => { + const message = createMessage({ + id: "message-1", + threadId: "thread-1", + authorId: "user-1", + body: "@reviewer and @missing please help", + createdAt: "2026-05-29T00:01:00.000Z", + }); + + expect(validateMentionedAgentsExist(message.mentions, [agent("reviewer")])).toEqual({ + success: false, + errors: ['Mentioned agent "missing" does not exist.'], + }); + }); +}); diff --git a/apps/server/src/tasks/task-schema.ts b/apps/server/src/tasks/task-schema.ts new file mode 100644 index 0000000..f99e3b7 --- /dev/null +++ b/apps/server/src/tasks/task-schema.ts @@ -0,0 +1,81 @@ +import type { PersistedAgentDefinition } from "../agents/agent-schema.js"; +import type { AgentMention, Message } from "../chat/chat-schema.js"; + +export const TASK_STATUSES = ["pending", "running", "completed", "failed", "cancelled"] as const; + +export type TaskStatus = (typeof TASK_STATUSES)[number]; + +export interface TaskRef { + readonly id: string; + readonly agentId: string; + readonly messageId: string; + readonly threadId: string; + readonly status: TaskStatus; + readonly createdAt: string; +} + +export type TaskSchemaResult = + | { + readonly success: true; + readonly value: T; + } + | { + readonly success: false; + readonly errors: readonly string[]; + }; + +export type TaskValidationResult = + | { + readonly success: true; + readonly errors: readonly []; + } + | { + readonly success: false; + readonly errors: readonly string[]; + }; + +export function createTaskRefsForMessage(input: { + readonly message: Message; + readonly agents: readonly PersistedAgentDefinition[]; + readonly now?: () => string; +}): TaskSchemaResult { + const validation = validateMentionedAgentsExist(input.message.mentions, input.agents); + if (!validation.success) { + return validation; + } + + const createdAt = (input.now ?? (() => new Date().toISOString()))(); + return { + success: true, + value: input.message.mentions.map((mention) => ({ + id: `task-${input.message.id}-${mention.agentId}`, + agentId: mention.agentId, + messageId: input.message.id, + threadId: input.message.threadId, + status: "pending", + createdAt, + })), + }; +} + +export function validateMentionedAgentsExist( + mentions: readonly AgentMention[], + agents: readonly PersistedAgentDefinition[], +): TaskValidationResult { + const knownAgentIds = new Set(agents.map((agent) => agent.id)); + const errors = mentions + .filter((mention) => !knownAgentIds.has(mention.agentId)) + .map((mention) => `Mentioned agent "${mention.agentId}" does not exist.`); + + if (errors.length > 0) { + return { + success: false, + errors, + }; + } + + return { + success: true, + errors: [], + }; +} diff --git a/docs/development/task-backlog.md b/docs/development/task-backlog.md index 24f72b1..b3e3b50 100644 --- a/docs/development/task-backlog.md +++ b/docs/development/task-backlog.md @@ -293,10 +293,10 @@ Status markers: **Tasks:** -- [ ] Define `Conversation`, `Channel`, `Thread`, `Message`, and `TaskRef`. -- [ ] Parse `@agent` mentions into structured references. -- [ ] Validate that mentioned agents exist before task creation. -- [ ] Commit with message `feat: define chat and task schemas`. +- [x] Define `Conversation`, `Channel`, `Thread`, `Message`, and `TaskRef`. +- [x] Parse `@agent` mentions into structured references. +- [x] Validate that mentioned agents exist before task creation. +- [x] Commit with message `feat: define chat and task schemas`. **Acceptance Criteria:** From b881ea1b2d86fb75915510e30f0d07d7a965e678 Mon Sep 17 00:00:00 2001 From: eleven_qy <64311644+alph-cmky@users.noreply.github.com> Date: Fri, 29 May 2026 12:38:39 +0800 Subject: [PATCH 13/13] feat: add chat workspace api --- apps/server/src/chat/chat-routes.test.ts | 26 ++++ apps/server/src/chat/chat-routes.ts | 39 ++++++ apps/server/src/chat/chat-service.test.ts | 111 ++++++++++++++++ apps/server/src/chat/chat-service.ts | 146 ++++++++++++++++++++++ apps/server/src/index.ts | 10 ++ docs/development/task-backlog.md | 8 +- 6 files changed, 336 insertions(+), 4 deletions(-) create mode 100644 apps/server/src/chat/chat-routes.test.ts create mode 100644 apps/server/src/chat/chat-routes.ts create mode 100644 apps/server/src/chat/chat-service.test.ts create mode 100644 apps/server/src/chat/chat-service.ts diff --git a/apps/server/src/chat/chat-routes.test.ts b/apps/server/src/chat/chat-routes.test.ts new file mode 100644 index 0000000..cbf25aa --- /dev/null +++ b/apps/server/src/chat/chat-routes.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; + +import { createChatRoutes } from "./chat-routes.js"; +import { createChatWorkspaceService } from "./chat-service.js"; + +describe("createChatRoutes", () => { + it("exposes route handlers for channels and messages", () => { + const service = createChatWorkspaceService({ + now: () => "2026-05-29T00:00:00.000Z", + }); + const routes = createChatRoutes({ service }); + + const channelResponse = routes.createChannel({ name: "Main" }); + const messageResponse = routes.createMessage({ + channelId: channelResponse.channel.id, + authorId: "user-1", + body: "Hello", + }); + + expect(routes.listChannels()).toEqual({ channels: [channelResponse.channel] }); + expect(messageResponse.tasks).toEqual([]); + expect(routes.listMessages({ threadId: messageResponse.message.threadId })).toEqual({ + messages: [messageResponse.message], + }); + }); +}); diff --git a/apps/server/src/chat/chat-routes.ts b/apps/server/src/chat/chat-routes.ts new file mode 100644 index 0000000..c1a8932 --- /dev/null +++ b/apps/server/src/chat/chat-routes.ts @@ -0,0 +1,39 @@ +import type { Channel, Message } from "./chat-schema.js"; +import type { + ChatWorkspaceService, + CreateChannelInput, + CreateMessageInput, + CreateMessageResult, +} from "./chat-service.js"; + +export interface ChatRoutes { + readonly createChannel: (input: CreateChannelInput) => { readonly channel: Channel }; + readonly listChannels: () => { readonly channels: readonly Channel[] }; + readonly createMessage: (input: CreateMessageInput) => CreateMessageResult; + readonly listMessages: (input: { readonly threadId: string }) => { + readonly messages: readonly Message[]; + }; +} + +export function createChatRoutes(input: { readonly service: ChatWorkspaceService }): ChatRoutes { + return { + createChannel(channelInput) { + return { + channel: input.service.createChannel(channelInput), + }; + }, + listChannels() { + return { + channels: input.service.listChannels(), + }; + }, + createMessage(messageInput) { + return input.service.createMessage(messageInput); + }, + listMessages(messageInput) { + return { + messages: input.service.listMessages(messageInput.threadId), + }; + }, + }; +} diff --git a/apps/server/src/chat/chat-service.test.ts b/apps/server/src/chat/chat-service.test.ts new file mode 100644 index 0000000..87df4ad --- /dev/null +++ b/apps/server/src/chat/chat-service.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, it } from "vitest"; + +import { createChatWorkspaceService } from "./chat-service.js"; +import type { PersistedAgentDefinition } from "../agents/agent-schema.js"; + +function agent(id: string): PersistedAgentDefinition { + return { + id, + name: id, + description: "Agent", + prompt: "Do the work.", + model: "gpt-5", + tools: [], + permissions: { + read: true, + write: false, + install: false, + arbitraryCommands: false, + commit: false, + push: false, + }, + memoryScope: "workspace", + runtimePreference: "codex", + workspaceRoot: "/tmp/agentdeck", + createdAt: "2026-05-29T00:00:00.000Z", + updatedAt: "2026-05-29T00:00:00.000Z", + }; +} + +describe("createChatWorkspaceService", () => { + it("creates and lists channels", () => { + const service = createChatWorkspaceService({ + now: () => "2026-05-29T00:00:00.000Z", + }); + + const channel = service.createChannel({ name: " Main " }); + + expect(channel).toEqual({ + id: "channel-main", + name: "Main", + createdAt: "2026-05-29T00:00:00.000Z", + }); + expect(service.listChannels()).toEqual([channel]); + }); + + it("creates and lists messages under an auto-created thread", () => { + const service = createChatWorkspaceService({ + now: () => "2026-05-29T00:00:00.000Z", + }); + const channel = service.createChannel({ name: "Main" }); + + const result = service.createMessage({ + channelId: channel.id, + authorId: "user-1", + body: "Hello team", + }); + + expect(result.tasks).toEqual([]); + expect(result.message).toMatchObject({ + id: "message-1", + threadId: "thread-1", + authorId: "user-1", + body: "Hello team", + mentions: [], + createdAt: "2026-05-29T00:00:00.000Z", + }); + expect(service.listMessages(result.message.threadId)).toEqual([result.message]); + }); + + it("creates pending task refs when a message contains valid @agent mentions", () => { + const service = createChatWorkspaceService({ + agents: [agent("reviewer")], + now: () => "2026-05-29T00:00:00.000Z", + }); + const channel = service.createChannel({ name: "Main" }); + + const result = service.createMessage({ + channelId: channel.id, + authorId: "user-1", + body: "@reviewer please review this", + }); + + expect(result.tasks).toEqual([ + { + id: "task-message-1-reviewer", + agentId: "reviewer", + messageId: "message-1", + threadId: "thread-1", + status: "pending", + createdAt: "2026-05-29T00:00:00.000Z", + }, + ]); + }); + + it("rejects messages that mention unknown agents before storing them", () => { + const service = createChatWorkspaceService({ + agents: [agent("reviewer")], + now: () => "2026-05-29T00:00:00.000Z", + }); + const channel = service.createChannel({ name: "Main" }); + + expect(() => + service.createMessage({ + channelId: channel.id, + authorId: "user-1", + body: "@missing please help", + }), + ).toThrow('Mentioned agent "missing" does not exist.'); + expect(service.listMessages("thread-1")).toEqual([]); + }); +}); diff --git a/apps/server/src/chat/chat-service.ts b/apps/server/src/chat/chat-service.ts new file mode 100644 index 0000000..02311a7 --- /dev/null +++ b/apps/server/src/chat/chat-service.ts @@ -0,0 +1,146 @@ +import type { PersistedAgentDefinition } from "../agents/agent-schema.js"; +import { createTaskRefsForMessage, type TaskRef } from "../tasks/task-schema.js"; +import { + createChannel as createChannelModel, + createMessage as createMessageModel, + createThread as createThreadModel, + type Channel, + type Message, + type Thread, +} from "./chat-schema.js"; + +export interface CreateChatWorkspaceServiceOptions { + readonly agents?: readonly PersistedAgentDefinition[]; + readonly now?: () => string; +} + +export interface CreateChannelInput { + readonly name: string; +} + +export interface CreateMessageInput { + readonly channelId: string; + readonly authorId: string; + readonly body: string; + readonly threadId?: string; +} + +export interface CreateMessageResult { + readonly message: Message; + readonly tasks: readonly TaskRef[]; +} + +export interface ChatWorkspaceService { + readonly createChannel: (input: CreateChannelInput) => Channel; + readonly listChannels: () => readonly Channel[]; + readonly createMessage: (input: CreateMessageInput) => CreateMessageResult; + readonly listMessages: (threadId: string) => readonly Message[]; + readonly listThreads: (channelId: string) => readonly Thread[]; +} + +export function createChatWorkspaceService( + options: CreateChatWorkspaceServiceOptions = {}, +): ChatWorkspaceService { + const channels: Channel[] = []; + const threads: Thread[] = []; + const messages: Message[] = []; + const tasks: TaskRef[] = []; + const now = options.now ?? (() => new Date().toISOString()); + let nextMessageNumber = 1; + let nextThreadNumber = 1; + + return { + createChannel(input) { + const channel = createChannelModel({ + id: `channel-${slugify(input.name)}`, + name: input.name, + createdAt: now(), + }); + channels.push(channel); + return channel; + }, + listChannels() { + return [...channels]; + }, + createMessage(input) { + const channel = channels.find((candidate) => candidate.id === input.channelId); + if (!channel) { + throw new Error(`Channel "${input.channelId}" was not found.`); + } + + const thread = getOrCreateThread({ + channel, + ...(input.threadId ? { requestedThreadId: input.threadId } : {}), + threads, + nextThreadId: () => `thread-${nextThreadNumber++}`, + now, + }); + const message = createMessageModel({ + id: `message-${nextMessageNumber++}`, + threadId: thread.id, + authorId: input.authorId, + body: input.body, + createdAt: now(), + }); + const taskResult = createTaskRefsForMessage({ + message, + agents: options.agents ?? [], + now, + }); + + if (!taskResult.success) { + throw new Error(taskResult.errors.join(" ")); + } + + messages.push(message); + tasks.push(...taskResult.value); + + return { + message, + tasks: taskResult.value, + }; + }, + listMessages(threadId) { + return messages.filter((message) => message.threadId === threadId); + }, + listThreads(channelId) { + return threads.filter((thread) => thread.channelId === channelId); + }, + }; +} + +function getOrCreateThread(input: { + readonly channel: Channel; + readonly requestedThreadId?: string; + readonly threads: Thread[]; + readonly nextThreadId: () => string; + readonly now: () => string; +}): Thread { + if (input.requestedThreadId) { + const existing = input.threads.find((thread) => thread.id === input.requestedThreadId); + if (!existing) { + throw new Error(`Thread "${input.requestedThreadId}" was not found.`); + } + + return existing; + } + + const thread = createThreadModel({ + id: input.nextThreadId(), + channelId: input.channel.id, + title: input.channel.name, + createdAt: input.now(), + }); + input.threads.push(thread); + return thread; +} + +function slugify(value: string): string { + const slug = value + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, ""); + + return slug || "channel"; +} diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 84becc9..724e162 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -33,6 +33,16 @@ export { parseAgentMentions, } from "./chat/chat-schema.js"; export type { AgentMention, Channel, Conversation, Message, Thread } from "./chat/chat-schema.js"; +export { createChatRoutes } from "./chat/chat-routes.js"; +export { createChatWorkspaceService } from "./chat/chat-service.js"; +export type { ChatRoutes } from "./chat/chat-routes.js"; +export type { + ChatWorkspaceService, + CreateChannelInput, + CreateMessageInput, + CreateMessageResult, + CreateChatWorkspaceServiceOptions, +} from "./chat/chat-service.js"; export { TASK_STATUSES, createTaskRefsForMessage, diff --git a/docs/development/task-backlog.md b/docs/development/task-backlog.md index b3e3b50..3077b6b 100644 --- a/docs/development/task-backlog.md +++ b/docs/development/task-backlog.md @@ -314,10 +314,10 @@ Status markers: **Tasks:** -- [ ] Add channel creation and listing. -- [ ] Add message creation and listing. -- [ ] Create task refs when messages contain valid `@agent` mentions. -- [ ] Commit with message `feat: add chat workspace api`. +- [x] Add channel creation and listing. +- [x] Add message creation and listing. +- [x] Create task refs when messages contain valid `@agent` mentions. +- [x] Commit with message `feat: add chat workspace api`. **Acceptance Criteria:**