Compare commits

..

37 Commits

Author SHA1 Message Date
claude-code-best
9c1db0e543 feat: 添加对 ACP 协议的支持 (#284)
* feat: 适配 zed acp 协议

* docs: 完善 acp 文档
2026-04-16 20:50:03 +08:00
claude-code-best
1171f487ca feat(langfuse): LLM generation 记录工具定义
将 Anthropic 格式的工具定义转换为 Langfuse 兼容的 OpenAI 格式,
并在 generation 的 input 中以 { messages, tools } 结构传入,
以便在 Langfuse UI 中查看完整的工具定义信息。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 20:50:03 +08:00
claude-code-best
49869ffa3e feat: 添加环境变量支持以覆盖 max_tokens 设置 2026-04-16 20:50:03 +08:00
claude-code-best
40b5e4452d build: 新增 vite 构建流程 2026-04-16 20:50:03 +08:00
claude-code-best
6fb36390b1 docs: update contributors 2026-04-16 20:50:03 +08:00
Cheng Zi Feng
962ed75f4b fix(remote-control): harden self-hosted session flows (#278)
Co-authored-by: chengzifeng <chengzifeng@meituan.com>
2026-04-16 20:50:03 +08:00
claude-code-best
920a7ffd9d test: 修复类型校验 (#279)
* fix: 修复 Bun 的 polyfill 问题

* fix: 类型修复完成

* feat: 统一所有包的类型文件

* fix: 修复构建问题
2026-04-16 20:50:03 +08:00
claude-code-best
aa0f868790 fix: 修复类型问题(#267) (#271)
* fix: 修复 Bun 的 polyfill 问题

* fix: 类型修复完成

* feat: 统一所有包的类型文件

* fix: 修复构建问题
2026-04-16 20:50:03 +08:00
claude-code-best
3c4fa38b19 test: 添加测试支持 2026-04-14 22:31:44 +08:00
claude-code-best
e601557716 docs: 修复链接 2026-04-14 22:31:44 +08:00
claude-code-best
a36ab55ff9 fix: 修复 node 下 ws 没打包问题 2026-04-14 22:31:44 +08:00
claude-code-best
46593d952a fix: 修复 n 快捷键导致关闭的问题 2026-04-14 22:31:44 +08:00
claude-code-best
3c9112f969 docs: 添加简化版 agent loop 2026-04-14 15:50:04 +08:00
claude-code-best
67a77ba327 Revert "feat: compact 模型降级为 -1 模式(Opus→Sonnet, Sonnet→Haiku)"
This reverts commit e458d6391d.
2026-04-14 14:32:34 +08:00
claude-code-best
befcd2bafa docs: 添加 agent-loop 绘图 2026-04-14 10:07:49 +08:00
claude-code-best
e458d6391d feat: compact 模型降级为 -1 模式(Opus→Sonnet, Sonnet→Haiku)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 09:56:22 +08:00
claude-code-best
ac0ca4a481 refactor: 搬入 errorUtils 并迁移消费者导入到 model-provider
- 搬入 formatAPIError、extractConnectionErrorDetails 等 errorUtils
- 迁移 10 个消费者文件直接从 @anthropic-ai/model-provider 导入
- 更新 emptyUsage、sdkUtilityTypes、systemPromptType 为 re-export proxy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 23:11:18 +08:00
claude-code-best
a99375b03d refactor: 搬入 Gemini 兼容层到 model-provider 包
- 搬入 Gemini 类型定义、消息转换、工具转换、流适配、模型映射
- 主项目 gemini/ 目录下文件改为 thin re-export proxy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 23:10:37 +08:00
claude-code-best
670cad66ad refactor: 提升 OpenAI 转换器和模型映射到 model-provider 包
- 搬入 OpenAI 消息转换(convertMessages)、工具转换(convertTools)、流适配(streamAdapter)
- 搬入 OpenAI 和 Grok 模型映射(resolveOpenAIModel、resolveGrokModel)
- 主项目文件改为 thin re-export proxy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 23:10:23 +08:00
claude-code-best
a3fbcb31c0 refactor: 创建 @anthropic-ai/model-provider 包骨架与类型定义
- 新建 workspace 包 packages/@anthropic-ai/model-provider
- 定义 ModelProviderHooks 接口(依赖注入:分析、成本、日志等)
- 定义 ClientFactories 接口(Anthropic/OpenAI/Gemini/Grok 客户端工厂)
- 搬入核心类型:Message 体系、NonNullableUsage、EMPTY_USAGE、SystemPrompt、错误常量
- 主项目 src/types/message.ts 等改为 re-export,保持向后兼容

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 23:10:00 +08:00
claude-code-best
dad3ad2b8d docs: 添加浏览器说明支持 2026-04-13 21:22:41 +08:00
claude-code-best
b5b81dfe49 chore: 更改 chrome 的依赖 2026-04-13 20:55:04 +08:00
claude-code-best
ecbd5a93e4 fix: 修复 Bun.hash 不存在的问题 2026-04-13 20:21:14 +08:00
claude-code-best
be80da4ce0 fix: 修复缓存 2026-04-13 20:09:23 +08:00
claude-code-best
fce40fed1f feat: 加上 userId 的传递 2026-04-13 19:04:51 +08:00
claude-code-best
a7e03a5b30 fix: 修复 interrupt 日志不上传 2026-04-13 18:12:23 +08:00
claude-code-best
05cabbbd73 feat: langfuse 工具调用显示为嵌套结构 2026-04-13 18:09:12 +08:00
claude-code-best
d4b30d32c3 fix: 修复 chrome 链接版本 2026-04-13 17:30:47 +08:00
claude-code-best
e0484e2817 fix: 使用简化版本的 chrome 桥接器 2026-04-13 16:03:47 +08:00
claude-code-best
2fb1c9dcd8 feat: 工具层及 mcp 大重构 (#252)
* feat: 第一版大重构

* fix: 修复类型问题

* chore: 更新版本到 1.3.2

* Add brave as alternative WebSearchTool

* fix: 修正顺序

* fix: 修复对穷鬼模式的 auto dream 和 session memory 越过

* feat: 穷鬼模式去除 session-summary

* feat: 创建 builtin-tools 包,搬运所有工具实现

将 src/tools/ 下的全部 60 个工具目录迁移至 packages/builtin-tools/src/tools/,
内部导入路径已更新为 src/ alias 模式。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 更新 src/ 中所有工具引用至 builtin-tools 包,删除 src/tools/

- src/tools.ts 及 178 个 src/ 文件的 import 路径从 ./tools/ 改为 builtin-tools/tools/
- 删除 src/tools/ 整个目录(已迁移至 packages/builtin-tools/)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: 添加 builtin-tools 路径别名至 tsconfig,更新 bun.lock

- tsconfig.json 新增 builtin-tools/* 和 builtin-tools 路径映射
- 新增 packages/builtin-tools/src 至 include

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 为 builtin-tools、mcp-client、agent-tools 添加 @claude-code-best 作用域前缀

所有包名及 import 路径统一添加 @claude-code-best/ 前缀:
- builtin-tools → @claude-code-best/builtin-tools
- mcp-client → @claude-code-best/mcp-client
- agent-tools → @claude-code-best/agent-tools

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复 node 环境没有 bun 的问题

---------

Co-authored-by: Eric-Guo <eric.guocz@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 09:52:05 +08:00
claude-code-best
bbb8b613a9 docs: update contributors 2026-04-13 01:51:00 +00:00
claude-code-best
c63b875ae3 chore: 1.3.3 版本 2026-04-13 09:50:38 +08:00
claude-code-best
9b8503d13d fix: 修复 node 环境没有 bun 的问题 2026-04-13 09:47:33 +08:00
claude-code-best
3cf94fbda0 fix: 修复对穷鬼模式的 auto dream 和 session memory 越过 2026-04-12 23:24:12 +08:00
claude-code-best
9a3081dff6 Merge branch 'pr/Eric-Guo/245' 2026-04-12 23:12:23 +08:00
claude-code-best
bd6448ecda fix: 修正顺序 2026-04-12 23:12:09 +08:00
Eric-Guo
711440474c Add brave as alternative WebSearchTool 2026-04-12 22:23:11 +08:00
692 changed files with 20126 additions and 4655 deletions

View File

@@ -27,4 +27,4 @@ jobs:
run: bun test
- name: Build
run: bun run build
run: bun run build:vite

View File

@@ -55,6 +55,8 @@ bun run health
# Check unused exports
bun run check:unused
bun run typecheck
# Remote Control Server
bun run rcs

View File

@@ -17,13 +17,16 @@
| 特性 | 说明 | 文档 |
|------|------|------|
| **Claude 群控技术** | Pipe IPC 多实例协作:同机 main/sub 自动编排 + LAN 跨机器零配置发现与通讯,`/pipes` 选择面板 + `Shift+↓` 交互 + 消息广播路由 | [Pipe IPC](https://ccb.agent-aura.top/docs/features/pipes-and-lan) / [LAN](https://ccb.agent-aura.top/docs/features/lan-pipes) |
| ACP 协议一等一支持 | 支持接入 Zed、Cursor 等 IDE支持会话恢复、Skills、权限桥接 | [文档](https://ccb.agent-aura.top/docs/features/acp-zed) |
| Remote Control 私有部署 | Docker 自托管 RCS + Web UI | [文档](https://ccb.agent-aura.top/docs/features/remote-control-self-hosting) |
| /dream 记忆整理 | 自动整理和优化记忆文件 | [文档](https://ccb.agent-aura.top/docs/features/auto-dream) |
| Web Search | 内置网页搜索工具 | [文档](https://ccb.agent-aura.top/docs/features/web-browser-tool) |
| 自定义模型供应商 | OpenAI/Anthropic/Gemini/Grok 兼容 | [文档](https://ccb.agent-aura.top/docs/features/custom-platform-login) |
| Voice Mode | Push-to-Talk 语音输入 | [文档](https://ccb.agent-aura.top/docs/features/voice-mode) |
| Computer Use / Chrome Use | 截图、键鼠控制、浏览器操控 | [Computer Use](https://ccb.agent-aura.top/docs/features/computer-use)<br>[Chrome Use](https://ccb.agent-aura.top/docs/features/claude-in-chrome-mcp) |
| Sentry / GrowthBook 企业监控 | 企业级错误追踪与特性开关 | [Sentry](https://ccb.agent-aura.top/docs/internals/sentry-setup)<br>[GrowthBook](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) |
| Computer Use | 屏幕截图、键鼠控制 | [文档](https://ccb.agent-aura.top/docs/features/computer-use) |
| Chrome Use | 浏览器自动化、表单填写、数据抓取 | [自托管](https://ccb.agent-aura.top/docs/features/chrome-use-mcp) [原生版](https://ccb.agent-aura.top/docs/features/claude-in-chrome-mcp) |
| Sentry | 企业级错误追踪 | [文档](https://ccb.agent-aura.top/docs/internals/sentry-setup) |
| GrowthBook | 企业级特性开关 | [文档](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) |
| Langfuse 监控 | LLM 调用/工具执行/多 Agent 全链路追踪 | [文档](https://ccb.agent-aura.top/docs/features/langfuse-monitoring) |
| Poor Mode | 穷鬼模式,关闭记忆提取和键入建议 | /poor 可以开关 |

View File

@@ -30,6 +30,8 @@ const DEFAULT_BUILD_FEATURES = [
'ULTRAPLAN',
// P2: daemon + remote control server
'DAEMON',
// ACP (Agent Client Protocol) agent mode
'ACP',
// PR-package restored features
'WORKFLOW_SCRIPTS',
'HISTORY_SNIP',
@@ -88,8 +90,27 @@ for (const file of files) {
}
}
// Also patch unguarded globalThis.Bun destructuring from third-party deps
// (e.g. @anthropic-ai/sandbox-runtime) so Node.js doesn't crash at import time.
let bunPatched = 0
const BUN_DESTRUCTURE = /var \{([^}]+)\} = globalThis\.Bun;?/g
const BUN_DESTRUCTURE_SAFE = 'var {$1} = typeof globalThis.Bun !== "undefined" ? globalThis.Bun : {};'
for (const file of files) {
if (!file.endsWith('.js')) continue
const filePath = join(outdir, file)
const content = await readFile(filePath, 'utf-8')
if (BUN_DESTRUCTURE.test(content)) {
await writeFile(
filePath,
content.replace(BUN_DESTRUCTURE, BUN_DESTRUCTURE_SAFE),
)
bunPatched++
}
}
BUN_DESTRUCTURE.lastIndex = 0
console.log(
`Bundled ${result.outputs.length} files to ${outdir}/ (patched ${patched} for Node.js compat)`,
`Bundled ${result.outputs.length} files to ${outdir}/ (patched ${patched} for import.meta.require, ${bunPatched} for Bun destructure)`,
)
// Step 4: Copy native .node addon files (audio-capture)
@@ -118,6 +139,7 @@ const cliBun = join(outdir, 'cli-bun.js')
const cliNode = join(outdir, 'cli-node.js')
await writeFile(cliBun, '#!/usr/bin/env bun\nimport "./cli.js"\n')
await writeFile(cliNode, '#!/usr/bin/env node\nimport "./cli.js"\n')
// Make both executable

220
bun.lock
View File

@@ -5,7 +5,9 @@
"": {
"name": "claude-code-best",
"dependencies": {
"mcp-chrome-bridge": "^1.0.31",
"@agentclientprotocol/sdk": "^0.19.0",
"@claude-code-best/mcp-chrome-bridge": "^2.0.7",
"ws": "^8.20.0",
},
"devDependencies": {
"@alcalzone/ansi-tokenize": "^0.3.0",
@@ -17,6 +19,7 @@
"@anthropic-ai/claude-agent-sdk": "^0.2.87",
"@anthropic-ai/foundry-sdk": "^0.2.3",
"@anthropic-ai/mcpb": "^2.1.2",
"@anthropic-ai/model-provider": "workspace:*",
"@anthropic-ai/sandbox-runtime": "^0.0.44",
"@anthropic-ai/sdk": "^0.80.0",
"@anthropic-ai/vertex-sdk": "^0.14.4",
@@ -28,6 +31,9 @@
"@aws-sdk/credential-providers": "^3.1020.0",
"@azure/identity": "^4.13.1",
"@biomejs/biome": "^2.4.10",
"@claude-code-best/agent-tools": "workspace:*",
"@claude-code-best/builtin-tools": "workspace:*",
"@claude-code-best/mcp-client": "workspace:*",
"@commander-js/extra-typings": "^14.0.0",
"@growthbook/growthbook": "^1.6.5",
"@langfuse/otel": "^5.1.0",
@@ -54,10 +60,11 @@
"@sentry/node": "^10.47.0",
"@smithy/core": "^3.23.13",
"@smithy/node-http-handler": "^4.5.1",
"@types/bun": "^1.3.11",
"@types/bun": "^1.3.12",
"@types/cacache": "^20.0.1",
"@types/he": "^1.2.3",
"@types/lodash-es": "^4.17.12",
"@types/node": "^25.6.0",
"@types/picomatch": "^4.0.3",
"@types/plist": "^3.0.5",
"@types/proper-lockfile": "^4.1.4",
@@ -113,6 +120,7 @@
"react": "^19.2.4",
"react-compiler-runtime": "^1.0.0",
"react-reconciler": "^0.33.0",
"rollup": "^4.60.1",
"semver": "^7.7.4",
"sharp": "^0.34.5",
"shell-quote": "^1.8.3",
@@ -127,11 +135,11 @@
"undici": "^7.24.6",
"url-handler-napi": "workspace:*",
"usehooks-ts": "^3.1.1",
"vite": "^8.0.8",
"vscode-jsonrpc": "^8.2.1",
"vscode-languageserver-protocol": "^3.17.5",
"vscode-languageserver-types": "^3.17.5",
"wrap-ansi": "^10.0.0",
"ws": "^8.20.0",
"xss": "^1.0.15",
"yaml": "^2.8.3",
"zod": "^4.3.6",
@@ -176,10 +184,32 @@
"wrap-ansi": "^10.0.0",
},
},
"packages/@anthropic-ai/model-provider": {
"name": "@anthropic-ai/model-provider",
"version": "1.0.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.80.0",
"openai": "^6.33.0",
},
},
"packages/agent-tools": {
"name": "@claude-code-best/agent-tools",
"version": "1.0.0",
"dependencies": {
"zod": "^3.25.0",
},
},
"packages/audio-capture-napi": {
"name": "audio-capture-napi",
"version": "1.0.0",
},
"packages/builtin-tools": {
"name": "@claude-code-best/builtin-tools",
"version": "1.0.0",
"dependencies": {
"@claude-code-best/agent-tools": "workspace:*",
},
},
"packages/color-diff-napi": {
"name": "color-diff-napi",
"version": "1.0.0",
@@ -194,6 +224,18 @@
"sharp": "^0.33.5",
},
},
"packages/mcp-client": {
"name": "@claude-code-best/mcp-client",
"version": "1.0.0",
"dependencies": {
"@claude-code-best/agent-tools": "workspace:*",
"@modelcontextprotocol/sdk": "^1.29.0",
"lodash-es": "^4.17.21",
"lru-cache": "^10.0.0",
"p-map": "^4.0.0",
"zod": "^3.25.0",
},
},
"packages/modifiers-napi": {
"name": "modifiers-napi",
"version": "1.0.0",
@@ -224,6 +266,8 @@
},
},
"packages": {
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.19.0", "https://registry.npmmirror.com/@agentclientprotocol/sdk/-/sdk-0.19.0.tgz", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-U9I8ws9WTOk6jCBAWpXefGSDgVXn14/kV6HFzwWGcstQ02mOQgClMAROHmoIn9GqZbDBDEOkdIbP4P4TEMQdug=="],
"@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.3.0", "https://registry.npmmirror.com/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.0.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA=="],
"@ant/claude-for-chrome-mcp": ["@ant/claude-for-chrome-mcp@workspace:packages/@ant/claude-for-chrome-mcp"],
@@ -242,6 +286,8 @@
"@anthropic-ai/mcpb": ["@anthropic-ai/mcpb@2.1.2", "https://registry.npmmirror.com/@anthropic-ai/mcpb/-/mcpb-2.1.2.tgz", { "dependencies": { "@inquirer/prompts": "^6.0.1", "commander": "^13.1.0", "fflate": "^0.8.2", "galactus": "^1.0.0", "ignore": "^7.0.5", "node-forge": "^1.3.2", "pretty-bytes": "^5.6.0", "zod": "^3.25.67", "zod-to-json-schema": "^3.24.6" }, "bin": { "mcpb": "dist/cli/cli.js" } }, "sha512-goRbBC8ySo7SWb7tRzr+tL6FxDc4JPTRCdgfD2omba7freofvjq5rom1lBnYHZHo6Mizs1jAHJeN53aZbDoy8A=="],
"@anthropic-ai/model-provider": ["@anthropic-ai/model-provider@workspace:packages/@anthropic-ai/model-provider"],
"@anthropic-ai/sandbox-runtime": ["@anthropic-ai/sandbox-runtime@0.0.44", "https://registry.npmmirror.com/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.44.tgz", { "dependencies": { "@pondwader/socks5-server": "^1.0.10", "@types/lodash-es": "^4.17.12", "commander": "^12.1.0", "lodash-es": "^4.17.23", "shell-quote": "^1.8.3", "zod": "^3.24.1" }, "bin": { "srt": "dist/cli.js" } }, "sha512-mmyjq0mzsHnQZyiU+FGYyaiJcPckuQpP78VB8iqFi2IOu8rcb9i5SmaOKyJENJNfY8l/1grzLMQgWq4Apvmozw=="],
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.80.0", "https://registry.npmmirror.com/@anthropic-ai/sdk/-/sdk-0.80.0.tgz", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-WeXLn7zNVk3yjeshn+xZHvld6AoFUOR3Sep6pSoHho5YbSi6HwcirqgPA5ccFuW8QTVJAAU7N8uQQC6Wa9TG+g=="],
@@ -410,6 +456,14 @@
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.11", "https://registry.npmmirror.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.11.tgz", { "os": "win32", "cpu": "x64" }, "sha512-A8D3JM/00C2KQgUV3oj8Ba15EHEYwebAGCy5Sf9GAjr5Y3+kJIYOiESoqRDeuRZueuMdCsbLZIUqmPhpYXJE9A=="],
"@claude-code-best/agent-tools": ["@claude-code-best/agent-tools@workspace:packages/agent-tools"],
"@claude-code-best/builtin-tools": ["@claude-code-best/builtin-tools@workspace:packages/builtin-tools"],
"@claude-code-best/mcp-chrome-bridge": ["@claude-code-best/mcp-chrome-bridge@2.0.7", "https://registry.npmmirror.com/@claude-code-best/mcp-chrome-bridge/-/mcp-chrome-bridge-2.0.7.tgz", { "dependencies": { "@fastify/cors": "^11.0.1", "@modelcontextprotocol/sdk": "^1.11.0", "chalk": "^5.4.1", "chrome-mcp-shared": "^1.0.2", "commander": "^13.1.0", "fastify": "^5.3.2", "is-admin": "^4.0.0", "pino": "^9.6.0", "uuid": "^11.1.0" }, "bin": { "mcp-chrome-bridge": "dist/cli.js", "mcp-chrome-stdio": "dist/mcp/mcp-server-stdio.js" } }, "sha512-gb64+Ga6li3A8Ll9NKV+ePBn5/U0fccCdrH43tGYveLKZIZxURz8cbY+Z3BdbTdYSPVdFXtfUlp3TMxu4OT5gg=="],
"@claude-code-best/mcp-client": ["@claude-code-best/mcp-client@workspace:packages/mcp-client"],
"@commander-js/extra-typings": ["@commander-js/extra-typings@14.0.0", "https://registry.npmmirror.com/@commander-js/extra-typings/-/extra-typings-14.0.0.tgz", { "peerDependencies": { "commander": "~14.0.0" } }, "sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg=="],
"@emnapi/core": ["@emnapi/core@1.9.2", "https://registry.npmmirror.com/@emnapi/core/-/core-1.9.2.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
@@ -804,6 +858,36 @@
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.0.tgz", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "arm" }, "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "x64" }, "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", { "os": "linux", "cpu": "x64" }, "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", { "os": "none", "cpu": "arm64" }, "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", { "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", "@napi-rs/wasm-runtime": "^1.1.3" }, "cpu": "none" }, "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", { "os": "win32", "cpu": "x64" }, "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="],
@@ -1018,8 +1102,6 @@
"@types/node": ["@types/node@25.6.0", "https://registry.npmmirror.com/@types/node/-/node-25.6.0.tgz", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
"@types/node-fetch": ["@types/node-fetch@2.6.13", "https://registry.npmmirror.com/@types/node-fetch/-/node-fetch-2.6.13.tgz", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
"@types/pg": ["@types/pg@8.15.6", "https://registry.npmmirror.com/@types/pg/-/pg-8.15.6.tgz", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="],
"@types/pg-pool": ["@types/pg-pool@2.0.7", "https://registry.npmmirror.com/@types/pg-pool/-/pg-pool-2.0.7.tgz", { "dependencies": { "@types/pg": "*" } }, "sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng=="],
@@ -1074,6 +1156,8 @@
"agent-base": ["agent-base@8.0.0", "https://registry.npmmirror.com/agent-base/-/agent-base-8.0.0.tgz", {}, "sha512-QT8i0hCz6C/KQ+KTAbSNwCHDGdmUJl2tp2ZpNlGSWCfhUNVbYG2WLE3MdZGBAgXPV4GAvjGMxo+C1hroyxmZEg=="],
"aggregate-error": ["aggregate-error@3.1.0", "https://registry.npmmirror.com/aggregate-error/-/aggregate-error-3.1.0.tgz", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="],
"ajv": ["ajv@8.18.0", "https://registry.npmmirror.com/ajv/-/ajv-8.18.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
"ajv-formats": ["ajv-formats@3.0.1", "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-3.0.1.tgz", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
@@ -1106,16 +1190,10 @@
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.18", "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A=="],
"better-sqlite3": ["better-sqlite3@11.10.0", "https://registry.npmmirror.com/better-sqlite3/-/better-sqlite3-11.10.0.tgz", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ=="],
"bidi-js": ["bidi-js@1.0.3", "https://registry.npmmirror.com/bidi-js/-/bidi-js-1.0.3.tgz", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
"bignumber.js": ["bignumber.js@9.3.1", "https://registry.npmmirror.com/bignumber.js/-/bignumber.js-9.3.1.tgz", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
"bindings": ["bindings@1.5.0", "https://registry.npmmirror.com/bindings/-/bindings-1.5.0.tgz", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
"bl": ["bl@4.1.0", "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
"body-parser": ["body-parser@2.2.2", "https://registry.npmmirror.com/body-parser/-/body-parser-2.2.2.tgz", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
"bowser": ["bowser@2.14.1", "https://registry.npmmirror.com/bowser/-/bowser-2.14.1.tgz", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="],
@@ -1126,8 +1204,6 @@
"browserslist": ["browserslist@4.28.2", "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
"buffer": ["buffer@5.7.1", "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
"bun-types": ["bun-types@1.3.12", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.12.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
@@ -1152,14 +1228,12 @@
"chokidar": ["chokidar@5.0.0", "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"chownr": ["chownr@1.1.4", "https://registry.npmmirror.com/chownr/-/chownr-1.1.4.tgz", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
"chrome-devtools-frontend": ["chrome-devtools-frontend@1.0.1611825", "https://registry.npmmirror.com/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.1611825.tgz", {}, "sha512-xp7EQPurkgJgYiSjIyLc3d7+BMevetrVeXHm5zEK0Zbr99/XjOlUzMnj18twLsrb/fYXYnMD4g5SjzcJkYATfQ=="],
"chrome-mcp-shared": ["chrome-mcp-shared@1.0.2", "https://registry.npmmirror.com/chrome-mcp-shared/-/chrome-mcp-shared-1.0.2.tgz", { "dependencies": { "@modelcontextprotocol/sdk": "^1.11.0", "zod": "^3.24.4" } }, "sha512-v+6HBmcgXrIfyVbkkrVgfFDzqOfDutI8yZM0yA8k7SiicqL1MfBoqnsOy5idYNvxyQymxCxXNuTmajn8xaGsgQ=="],
"cjs-module-lexer": ["cjs-module-lexer@2.2.0", "https://registry.npmmirror.com/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="],
"clean-stack": ["clean-stack@2.2.0", "https://registry.npmmirror.com/clean-stack/-/clean-stack-2.2.0.tgz", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="],
"cli-boxes": ["cli-boxes@4.0.1", "https://registry.npmmirror.com/cli-boxes/-/cli-boxes-4.0.1.tgz", {}, "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw=="],
"cli-highlight": ["cli-highlight@2.1.11", "https://registry.npmmirror.com/cli-highlight/-/cli-highlight-2.1.11.tgz", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="],
@@ -1210,10 +1284,6 @@
"decamelize": ["decamelize@1.2.0", "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
"decompress-response": ["decompress-response@6.0.0", "https://registry.npmmirror.com/decompress-response/-/decompress-response-6.0.0.tgz", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
"deep-extend": ["deep-extend@0.6.0", "https://registry.npmmirror.com/deep-extend/-/deep-extend-0.6.0.tgz", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
"default-browser": ["default-browser@5.5.0", "https://registry.npmmirror.com/default-browser/-/default-browser-5.5.0.tgz", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="],
"default-browser-id": ["default-browser-id@5.0.1", "https://registry.npmmirror.com/default-browser-id/-/default-browser-id-5.0.1.tgz", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
@@ -1234,8 +1304,6 @@
"dom-mutator": ["dom-mutator@0.6.0", "https://registry.npmmirror.com/dom-mutator/-/dom-mutator-0.6.0.tgz", {}, "sha512-iCt9o0aYfXMUkz/43ZOAUFQYotjGB+GNbYJiJdz4TgXkyToXbbRy5S6FbTp72lRBtfpUMwEc1KmpFEU4CZeoNg=="],
"drizzle-orm": ["drizzle-orm@0.38.4", "https://registry.npmmirror.com/drizzle-orm/-/drizzle-orm-0.38.4.tgz", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/react": ">=18", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "react": ">=18", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/react", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "knex", "kysely", "mysql2", "pg", "postgres", "react", "sql.js", "sqlite3"] }, "sha512-s7/5BpLKO+WJRHspvpqTydxFob8i1vo2rEx4pY6TGY7QSMuUfWUuzaY0DIpXCkgHOo37BaFC+SJQb99dDUXT3Q=="],
"dunder-proto": ["dunder-proto@1.0.1", "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
@@ -1248,8 +1316,6 @@
"encodeurl": ["encodeurl@2.0.0", "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"end-of-stream": ["end-of-stream@1.4.5", "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"enhanced-resolve": ["enhanced-resolve@5.20.1", "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
"env-paths": ["env-paths@4.0.0", "https://registry.npmmirror.com/env-paths/-/env-paths-4.0.0.tgz", { "dependencies": { "is-safe-filename": "^0.1.0" } }, "sha512-pxP8eL2SwwaTRi/KHYwLYXinDs7gL3jxFcBYmEdYfZmZXbaVDvdppd0XBU8qVz03rDfKZMXg1omHCbsJjZrMsw=="],
@@ -1278,8 +1344,6 @@
"execa": ["execa@9.6.1", "https://registry.npmmirror.com/execa/-/execa-9.6.1.tgz", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],
"expand-template": ["expand-template@2.0.3", "https://registry.npmmirror.com/expand-template/-/expand-template-2.0.3.tgz", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
"express": ["express@5.2.1", "https://registry.npmmirror.com/express/-/express-5.2.1.tgz", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"express-rate-limit": ["express-rate-limit@8.3.2", "https://registry.npmmirror.com/express-rate-limit/-/express-rate-limit-8.3.2.tgz", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg=="],
@@ -1320,8 +1384,6 @@
"figures": ["figures@6.1.0", "https://registry.npmmirror.com/figures/-/figures-6.1.0.tgz", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
"file-uri-to-path": ["file-uri-to-path@1.0.0", "https://registry.npmmirror.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
"fill-range": ["fill-range@7.1.1", "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"finalhandler": ["finalhandler@2.1.1", "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.1.tgz", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
@@ -1346,8 +1408,6 @@
"fresh": ["fresh@2.0.0", "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"fs-constants": ["fs-constants@1.0.0", "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
"fs-extra": ["fs-extra@10.1.0", "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
"fs-minipass": ["fs-minipass@3.0.3", "https://registry.npmmirror.com/fs-minipass/-/fs-minipass-3.0.3.tgz", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="],
@@ -1378,8 +1438,6 @@
"get-tsconfig": ["get-tsconfig@4.13.7", "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.13.7.tgz", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="],
"github-from-package": ["github-from-package@0.0.0", "https://registry.npmmirror.com/github-from-package/-/github-from-package-0.0.0.tgz", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
"glob": ["glob@13.0.6", "https://registry.npmmirror.com/glob/-/glob-13.0.6.tgz", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"glob-parent": ["glob-parent@5.1.2", "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@@ -1418,8 +1476,6 @@
"iconv-lite": ["iconv-lite@0.7.2", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"ieee754": ["ieee754@1.2.1", "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ignore": ["ignore@7.0.5", "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"image-processor-napi": ["image-processor-napi@workspace:packages/image-processor-napi"],
@@ -1430,8 +1486,6 @@
"inherits": ["inherits@2.0.4", "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ini": ["ini@1.3.8", "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"ip-address": ["ip-address@10.1.0", "https://registry.npmmirror.com/ip-address/-/ip-address-10.1.0.tgz", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
@@ -1556,8 +1610,6 @@
"math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mcp-chrome-bridge": ["mcp-chrome-bridge@1.0.31", "https://registry.npmmirror.com/mcp-chrome-bridge/-/mcp-chrome-bridge-1.0.31.tgz", { "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.1.69", "@fastify/cors": "^11.0.1", "@modelcontextprotocol/sdk": "^1.11.0", "@types/node-fetch": "2", "better-sqlite3": "^11.6.0", "chalk": "^5.4.1", "chrome-devtools-frontend": "^1.0.1299282", "chrome-mcp-shared": "1.0.2", "commander": "^13.1.0", "drizzle-orm": "^0.38.2", "fastify": "^5.3.2", "is-admin": "^4.0.0", "node-fetch": "2", "pino": "^9.6.0", "uuid": "^11.1.0" }, "bin": { "mcp-chrome-bridge": "dist/cli.js", "chrome-mcp-bridge": "dist/cli.js", "mcp-chrome-stdio": "dist/mcp/mcp-server-stdio.js" } }, "sha512-bcl4POvdXhf9PX0+EIJ9guR+n6oVPNfbSBnhwf0LVg9MWwMJYpdvLszUT77NG2gBJCJF+JV/+CNz5xHnt9GwFg=="],
"media-typer": ["media-typer@1.1.0", "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.0.tgz", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
@@ -1574,8 +1626,6 @@
"mimic-fn": ["mimic-fn@2.1.0", "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"mimic-response": ["mimic-response@3.1.0", "https://registry.npmmirror.com/mimic-response/-/mimic-response-3.1.0.tgz", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
"minimatch": ["minimatch@10.2.5", "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"minimist": ["minimist@1.2.8", "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
@@ -1588,8 +1638,6 @@
"minipass-pipeline": ["minipass-pipeline@1.2.4", "https://registry.npmmirror.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="],
"mkdirp-classic": ["mkdirp-classic@0.5.3", "https://registry.npmmirror.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
"modifiers-napi": ["modifiers-napi@workspace:packages/modifiers-napi"],
"module-details-from-path": ["module-details-from-path@1.0.4", "https://registry.npmmirror.com/module-details-from-path/-/module-details-from-path-1.0.4.tgz", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="],
@@ -1602,15 +1650,11 @@
"nanoid": ["nanoid@3.3.11", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"napi-build-utils": ["napi-build-utils@2.0.0", "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
"negotiator": ["negotiator@1.0.0", "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"node-abi": ["node-abi@3.89.0", "https://registry.npmmirror.com/node-abi/-/node-abi-3.89.0.tgz", { "dependencies": { "semver": "^7.3.5" } }, "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA=="],
"node-domexception": ["node-domexception@1.0.0", "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
"node-fetch": ["node-fetch@2.7.0", "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"node-fetch": ["node-fetch@3.3.2", "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"node-forge": ["node-forge@1.4.0", "https://registry.npmmirror.com/node-forge/-/node-forge-1.4.0.tgz", {}, "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ=="],
@@ -1698,8 +1742,6 @@
"postgres-interval": ["postgres-interval@1.2.0", "https://registry.npmmirror.com/postgres-interval/-/postgres-interval-1.2.0.tgz", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"prebuild-install": ["prebuild-install@7.1.3", "https://registry.npmmirror.com/prebuild-install/-/prebuild-install-7.1.3.tgz", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
"pretty-bytes": ["pretty-bytes@5.6.0", "https://registry.npmmirror.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="],
"pretty-ms": ["pretty-ms@9.3.0", "https://registry.npmmirror.com/pretty-ms/-/pretty-ms-9.3.0.tgz", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="],
@@ -1714,8 +1756,6 @@
"proxy-from-env": ["proxy-from-env@2.1.0", "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
"pump": ["pump@3.0.4", "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
"qrcode": ["qrcode@1.5.4", "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
"qs": ["qs@6.15.1", "https://registry.npmmirror.com/qs/-/qs-6.15.1.tgz", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="],
@@ -1728,8 +1768,6 @@
"raw-body": ["raw-body@3.0.2", "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.2.tgz", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"rc": ["rc@1.2.8", "https://registry.npmmirror.com/rc/-/rc-1.2.8.tgz", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
"react": ["react@19.2.5", "https://registry.npmmirror.com/react/-/react-19.2.5.tgz", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="],
"react-compiler-runtime": ["react-compiler-runtime@1.0.0", "https://registry.npmmirror.com/react-compiler-runtime/-/react-compiler-runtime-1.0.0.tgz", { "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental" } }, "sha512-rRfjYv66HlG8896yPUDONgKzG5BxZD1nV9U6rkm+7VCuvQc903C4MjcoZR4zPw53IKSOX9wMQVpA1IAbRtzQ7w=="],
@@ -1740,8 +1778,6 @@
"react-refresh": ["react-refresh@0.17.0", "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
"readable-stream": ["readable-stream@3.6.2", "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"readdirp": ["readdirp@5.0.0", "https://registry.npmmirror.com/readdirp/-/readdirp-5.0.0.tgz", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
"real-require": ["real-require@0.2.0", "https://registry.npmmirror.com/real-require/-/real-require-0.2.0.tgz", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
@@ -1764,6 +1800,8 @@
"rfdc": ["rfdc@1.4.1", "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"rolldown": ["rolldown@1.0.0-rc.15", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.15.tgz", { "dependencies": { "@oxc-project/types": "=0.124.0", "@rolldown/pluginutils": "1.0.0-rc.15" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-x64": "1.0.0-rc.15", "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g=="],
"rollup": ["rollup@4.60.1", "https://registry.npmmirror.com/rollup/-/rollup-4.60.1.tgz", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="],
"router": ["router@2.2.0", "https://registry.npmmirror.com/router/-/router-2.2.0.tgz", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
@@ -1814,10 +1852,6 @@
"signal-exit": ["signal-exit@4.1.0", "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"simple-concat": ["simple-concat@1.0.1", "https://registry.npmmirror.com/simple-concat/-/simple-concat-1.0.1.tgz", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
"simple-get": ["simple-get@4.0.1", "https://registry.npmmirror.com/simple-get/-/simple-get-4.0.1.tgz", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
"simple-swizzle": ["simple-swizzle@0.2.4", "https://registry.npmmirror.com/simple-swizzle/-/simple-swizzle-0.2.4.tgz", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="],
"smol-toml": ["smol-toml@1.6.1", "https://registry.npmmirror.com/smol-toml/-/smol-toml-1.6.1.tgz", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="],
@@ -1836,8 +1870,6 @@
"string-width": ["string-width@8.2.0", "https://registry.npmmirror.com/string-width/-/string-width-8.2.0.tgz", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="],
"string_decoder": ["string_decoder@1.3.0", "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"strip-ansi": ["strip-ansi@7.2.0", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"strip-final-newline": ["strip-final-newline@4.0.0", "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
@@ -1856,10 +1888,6 @@
"tapable": ["tapable@2.3.2", "https://registry.npmmirror.com/tapable/-/tapable-2.3.2.tgz", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="],
"tar-fs": ["tar-fs@2.1.4", "https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.4.tgz", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
"tar-stream": ["tar-stream@2.2.0", "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
"thenify": ["thenify@3.3.1", "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
"thenify-all": ["thenify-all@1.6.0", "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
@@ -1884,8 +1912,6 @@
"tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tunnel-agent": ["tunnel-agent@0.6.0", "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
"turndown": ["turndown@7.2.4", "https://registry.npmmirror.com/turndown/-/turndown-7.2.4.tgz", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ=="],
"type-fest": ["type-fest@5.5.0", "https://registry.npmmirror.com/type-fest/-/type-fest-5.5.0.tgz", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g=="],
@@ -1912,13 +1938,11 @@
"usehooks-ts": ["usehooks-ts@3.1.1", "https://registry.npmmirror.com/usehooks-ts/-/usehooks-ts-3.1.1.tgz", { "dependencies": { "lodash.debounce": "^4.0.8" }, "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA=="],
"util-deprecate": ["util-deprecate@1.0.2", "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"uuid": ["uuid@11.1.0", "https://registry.npmmirror.com/uuid/-/uuid-11.1.0.tgz", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
"vary": ["vary@1.1.2", "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"vite": ["vite@6.4.2", "https://registry.npmmirror.com/vite/-/vite-6.4.2.tgz", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="],
"vite": ["vite@8.0.8", "https://registry.npmmirror.com/vite/-/vite-8.0.8.tgz", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "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" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw=="],
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="],
@@ -1986,6 +2010,8 @@
"@anthropic/remote-control-server/typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"@anthropic/remote-control-server/vite": ["vite@6.4.2", "https://registry.npmmirror.com/vite/-/vite-6.4.2.tgz", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="],
"@aws-crypto/crc32/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "https://registry.npmmirror.com/@aws-crypto/util/-/util-5.2.0.tgz", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
"@aws-crypto/sha256-browser/@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "https://registry.npmmirror.com/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="],
@@ -2142,6 +2168,14 @@
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@claude-code-best/agent-tools/zod": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@claude-code-best/mcp-client/lru-cache": ["lru-cache@10.4.3", "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"@claude-code-best/mcp-client/p-map": ["p-map@4.0.0", "https://registry.npmmirror.com/p-map/-/p-map-4.0.0.tgz", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="],
"@claude-code-best/mcp-client/zod": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@fastify/otel/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.212.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation/-/instrumentation-0.212.0.tgz", { "dependencies": { "@opentelemetry/api-logs": "0.212.0", "import-in-the-middle": "^2.0.6", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg=="],
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-2.3.0.tgz", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
@@ -2282,6 +2316,8 @@
"@typespec/ts-http-runtime/https-proxy-agent": ["https-proxy-agent@7.0.6", "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"aggregate-error/indent-string": ["indent-string@4.0.0", "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
"ansi-escapes/type-fest": ["type-fest@0.21.3", "https://registry.npmmirror.com/type-fest/-/type-fest-0.21.3.tgz", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
"chrome-mcp-shared/zod": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
@@ -2302,8 +2338,6 @@
"gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"gaxios/node-fetch": ["node-fetch@3.3.2", "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"gtoken/gaxios": ["gaxios@6.7.1", "https://registry.npmmirror.com/gaxios/-/gaxios-6.7.1.tgz", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="],
"http-proxy-agent/agent-base": ["agent-base@7.1.4", "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
@@ -2316,8 +2350,6 @@
"light-my-request/process-warning": ["process-warning@4.0.1", "https://registry.npmmirror.com/process-warning/-/process-warning-4.0.1.tgz", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
"mcp-chrome-bridge/@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.1.77", "https://registry.npmmirror.com/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.77.tgz", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-linuxmusl-arm64": "^0.33.5", "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-ZEjWQtkoB2MEY6K16DWMmF+8OhywAynH0m08V265cerbZ8xPD/2Ng2jPzbbO40mPeFSsMDJboShL+a3aObP0Jg=="],
"micromatch/picomatch": ["picomatch@2.3.2", "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"minipass-flush/minipass": ["minipass@3.3.6", "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
@@ -2332,7 +2364,9 @@
"qrcode/yargs": ["yargs@15.4.1", "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
"rolldown/@oxc-project/types": ["@oxc-project/types@0.124.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.124.0.tgz", {}, "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg=="],
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", {}, "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g=="],
"vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
@@ -2466,6 +2500,8 @@
"gtoken/gaxios/is-stream": ["is-stream@2.0.1", "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"gtoken/gaxios/node-fetch": ["node-fetch@2.7.0", "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"gtoken/gaxios/uuid": ["uuid@9.0.1", "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"image-processor-napi/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
@@ -2518,22 +2554,6 @@
"is-admin/execa/strip-final-newline": ["strip-final-newline@2.0.0", "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
"mcp-chrome-bridge/@anthropic-ai/claude-agent-sdk/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
"mcp-chrome-bridge/@anthropic-ai/claude-agent-sdk/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
"mcp-chrome-bridge/@anthropic-ai/claude-agent-sdk/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="],
"mcp-chrome-bridge/@anthropic-ai/claude-agent-sdk/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="],
"mcp-chrome-bridge/@anthropic-ai/claude-agent-sdk/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="],
"mcp-chrome-bridge/@anthropic-ai/claude-agent-sdk/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="],
"mcp-chrome-bridge/@anthropic-ai/claude-agent-sdk/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="],
"mcp-chrome-bridge/@anthropic-ai/claude-agent-sdk/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
"qrcode/yargs/cliui": ["cliui@6.0.0", "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
"qrcode/yargs/string-width": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@@ -2552,6 +2572,8 @@
"@anthropic-ai/vertex-sdk/google-auth-library/gaxios/is-stream": ["is-stream@2.0.1", "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"@anthropic-ai/vertex-sdk/google-auth-library/gaxios/node-fetch": ["node-fetch@2.7.0", "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"@anthropic-ai/vertex-sdk/google-auth-library/gaxios/uuid": ["uuid@9.0.1", "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"@anthropic-ai/vertex-sdk/google-auth-library/gcp-metadata/google-logging-utils": ["google-logging-utils@0.0.2", "https://registry.npmmirror.com/google-logging-utils/-/google-logging-utils-0.0.2.tgz", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
@@ -2614,20 +2636,6 @@
"gtoken/gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"mcp-chrome-bridge/@anthropic-ai/claude-agent-sdk/@img/sharp-darwin-arm64/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="],
"mcp-chrome-bridge/@anthropic-ai/claude-agent-sdk/@img/sharp-darwin-x64/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="],
"mcp-chrome-bridge/@anthropic-ai/claude-agent-sdk/@img/sharp-linux-arm/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="],
"mcp-chrome-bridge/@anthropic-ai/claude-agent-sdk/@img/sharp-linux-arm64/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="],
"mcp-chrome-bridge/@anthropic-ai/claude-agent-sdk/@img/sharp-linux-x64/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="],
"mcp-chrome-bridge/@anthropic-ai/claude-agent-sdk/@img/sharp-linuxmusl-arm64/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="],
"mcp-chrome-bridge/@anthropic-ai/claude-agent-sdk/@img/sharp-linuxmusl-x64/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="],
"qrcode/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -0,0 +1,17 @@
flowchart TB
START((输入)) --> CTX["Context 管理"]
CTX --> LLM["LLM 流式输出"]
LLM --> TC{tool_use?}
TC --> |是| EXEC["执行工具"]
EXEC --> CTX
TC --> |否| DONE((完成))
classDef proc fill:#eef,stroke:#66c,color:#224
classDef decision fill:#fee,stroke:#c66,color:#422
classDef io fill:#eff,stroke:#6cc,color:#244
class CTX,LLM,EXEC proc
class TC decision
class START,DONE io

View File

@@ -0,0 +1,40 @@
flowchart TB
START((输入)) --> CTX["Context 管理"]
CTX --> PRE["Pre-sampling Hook"]
PRE --> LLM["LLM 流式输出"]
LLM --> TC{tool_use?}
TC --> |是| PERM{需权限?}
PERM --> |是| USER["👤 用户审批"]
USER --> |allow| TOOL_PRE
USER --> |deny| DENIED["拒绝"]
PERM --> |否| TOOL_PRE["Pre-tool Hook"]
TOOL_PRE --> EXEC["并发执行工具"]
EXEC --> TOOL_POST["Post-tool Hook"]
TOOL_POST --> CTX
DENIED --> CTX
TC --> |否| POST["Post-sampling Hook"]
POST --> STOP{"Stop Hook"}
STOP --> |不通过| CTX
STOP --> |通过| BUDGET{"Token Budget"}
BUDGET --> |继续| CTX
BUDGET --> |完成| DONE((完成))
subgraph SUB["子 Agent"]
FORK["AgentTool"] --> RECURSE["递归调用"]
end
EXEC -.-> FORK
classDef proc fill:#eef,stroke:#66c,color:#224
classDef decision fill:#fee,stroke:#c66,color:#422
classDef hook fill:#ffe,stroke:#cc6,color:#442
classDef io fill:#eff,stroke:#6cc,color:#244
classDef sub fill:#efe,stroke:#6a6,color:#242
class CTX,LLM,EXEC proc
class TC,PERM,STOP,BUDGET decision
class PRE,TOOL_PRE,TOOL_POST,POST hook
class START,DONE,USER,DENIED io
class FORK,RECURSE sub

View File

@@ -0,0 +1,346 @@
---
title: "MCP 配置 - 多来源合并、作用域与策略管控"
description: "详细说明 Claude Code MCP 配置的来源层次、合并优先级、传输类型、企业策略管控、插件集成和保留名称机制。"
keywords: ["MCP", "配置", "settings.json", ".mcp.json", "企业策略", "插件"]
---
## 配置来源与作用域
Claude Code 的 MCP 配置来自多个来源,每个来源对应一个 `scope`(作用域)。配置按优先级合并,高优先级来源的同名配置覆盖低优先级。
### 来源列表
| 来源 | Scope | 文件/接口 | 说明 |
|------|-------|----------|------|
| 企业管控 | `enterprise` | 系统管理路径 `managed-mcp.json` | **排他模式**:存在时忽略所有其他来源 |
| 本地项目 | `local` | `<project>/.claude/settings.local.json` | 项目级私有配置(不提交到 VCS |
| 项目配置 | `project` | `<project>/.mcp.json` | 项目级共享配置(可提交到 VCS |
| 用户全局 | `user` | `~/.claude/settings.json` | 用户级配置,所有项目共享 |
| 插件 | `dynamic` | 插件 manifest 中 `.mcp.json` / `.mcpb` | 插件提供的 MCP 服务器 |
| claude.ai | `claudeai` | 通过 API 获取 | claude.ai 网页端配置的连接器 |
| 内置动态 | `dynamic` | 代码中注册 | Computer Use / Chrome 等内置服务器 |
| IDE SDK | `sdk` | IDE 传入 | VS Code / JetBrains 嵌入模式 |
### 合并优先级(从低到高)
```
claude.ai 连接器 ← 最低优先级
↓ 去重
插件服务器
↓ 去重
用户全局配置
项目配置(.mcp.json ← 需要用户审批
本地项目配置
动态配置(内置 MCP ← 最高优先级
```
`Object.assign({}, dedupedPluginServers, userServers, approvedProjectServers, localServers)` 实现合并——后出现的同名键覆盖前者。
## 企业管控模式
当 `managed-mcp.json` 文件存在时,进入 **排他模式**
```typescript
// config.ts:1084
if (doesEnterpriseMcpConfigExist()) {
// 只返回企业配置,忽略所有用户/项目/插件/claude.ai 配置
return { servers: filtered, errors: [] }
}
```
特性:
- 路径由系统管理决定(`getManagedFilePath()` + `managed-mcp.json`
- 覆盖所有用户级、项目级、插件和 claude.ai 配置
- 仍然应用策略过滤allowlist/denylist
- 无法通过 CLI 添加新服务器(`addMcpConfig` 会拒绝)
## 传输类型与配置 Schema
### stdio默认
启动子进程,通过 stdin/stdout JSON-RPC 通信。
```json
{
"my-server": {
"command": "npx",
"args": ["-y", "@my-org/mcp-server"],
"env": { "API_KEY": "..." }
}
}
```
`type` 字段可省略(默认为 `stdio`)。环境变量通过 `env` 传递给子进程,会与当前进程环境合并。
**Windows 注意**:使用 `npx` 需要包装为 `cmd /c npx`,否则会报错。
### SSEServer-Sent Events
通过 HTTP SSE 连接远程 MCP 服务器。
```json
{
"my-remote": {
"type": "sse",
"url": "https://mcp.example.com/sse",
"headers": { "Authorization": "Bearer ..." },
"oauth": {
"clientId": "...",
"authServerMetadataUrl": "https://auth.example.com/.well-known/oauth-authorization-server"
}
}
}
```
支持 OAuth 认证流程。认证失败时进入 `needs-auth` 状态15 分钟 TTL 缓存避免重复提示。
### HTTPStreamable HTTP
HTTP 流式传输。
```json
{
"my-http": {
"type": "http",
"url": "https://mcp.example.com/mcp",
"headers": { "X-API-Key": "..." }
}
}
```
支持与 SSE 相同的 OAuth 配置。
### WebSocket
```json
{
"my-ws": {
"type": "ws",
"url": "wss://mcp.example.com/ws"
}
}
```
### IDE 专用类型(内部)
`sse-ide` 和 `ws-ide` 是 IDE 扩展专用类型,不由用户直接配置。
- `sse-ide`:使用 lockfile token 认证
- `ws-ide`:使用 `X-Claude-Code-Ide-Authorization` header
### SDK 类型(内部)
`type: "sdk"` 由 IDE 嵌入模式传入,不经过保留名称检查和企业管控排他限制。
### claude.ai 代理类型(内部)
`type: "claudeai-proxy"` 由 claude.ai 网页端配置的连接器使用,通过 OAuth bearer token 认证并支持 401 重试。
## 配置操作
### 添加 MCP 服务器
通过 CLI 命令 `claude mcp add` 或 API 调用 `addMcpConfig()`
```bash
# 添加到用户配置
claude mcp add my-server -s user -- npx @my-org/mcp-server
# 添加到项目配置
claude mcp add my-server -s project -- npx @my-org/mcp-server
# 添加 HTTP 类型
claude mcp add my-remote -s user -t http -u https://mcp.example.com/mcp
```
添加时的验证流程:
1. **名称校验**:只允许字母、数字、连字符和下划线
2. **保留名检查**`claude-in-chrome` 和 `computer-use` 被保留
3. **企业管控检查**:企业模式下拒绝添加
4. **Schema 验证**Zod 校验配置格式
5. **策略检查**denylist 拒绝、allowlist 验证
### 移除 MCP 服务器
```bash
claude mcp remove my-server -s user
```
### 列出 MCP 服务器
```bash
claude mcp list
```
## 项目配置审批
`.mcp.json` 中的项目配置需要用户显式审批才能生效:
```typescript
// config.ts:1166
const approvedProjectServers: Record<string, ScopedMcpServerConfig> = {}
for (const [name, config] of Object.entries(projectServers)) {
if (getProjectMcpServerStatus(name) === 'approved') {
approvedProjectServers[name] = config
}
}
```
首次打开项目时Claude Code 会提示用户审批 `.mcp.json` 中的每个服务器。审批状态持久化在本地配置中。
## 插件 MCP 集成
插件通过 manifest 中的 `.mcp.json` 或 `.mcpb` 文件声明 MCP 服务器:
```typescript
// 插件 MCP 加载流程
const pluginResult = await loadAllPluginsCacheOnly()
const pluginServerResults = await Promise.all(
pluginResult.enabled.map(plugin => getPluginMcpServers(plugin, mcpErrors))
)
```
### 插件命名空间
插件 MCP 服务器名格式为 `plugin:<pluginName>:<serverName>`,不会与手动配置的名称冲突。
### 去重机制
插件服务器通过内容签名去重(`dedupPluginMcpServers`
- **stdio 类型**:签名 = `stdio:` + JSON.stringify([command, ...args])
- **URL 类型**:签名 = `url:` + 原始 URLunwrap CCR proxy URL
- **sdk 类型**:签名为 null不去重
去重规则:
1. 手动配置优先于插件配置
2. 先加载的插件优先于后加载的
3. 被抑制的插件服务器在 `/plugin` UI 中显示提示
### claude.ai 连接器去重
claude.ai 连接器使用相同的内容签名机制去重(`dedupClaudeAiMcpServers`
- 仅启用的手动配置参与去重(禁用的手动配置不应抑制连接器)
- 连接器名格式为 `claude.ai <DisplayName>`
## 策略管控
### Allowlist / Denylist
企业策略通过 allowlist 和 denylist 控制可用的 MCP 服务器:
```typescript
// config.ts:1243 - 最终策略过滤
for (const [name, serverConfig] of Object.entries(configs)) {
if (!isMcpServerAllowedByPolicy(name, serverConfig)) {
continue // 跳过策略禁止的服务器
}
filtered[name] = serverConfig
}
```
策略检查考虑:
- 服务器名称匹配
- stdio 类型的 command + args 匹配
- URL 类型的 URL 模式匹配(支持通配符)
### 插件专用模式
`isRestrictedToPluginOnly('mcp')` 启用时,只允许插件提供的 MCP 服务器——用户/项目级配置被忽略。
## 环境变量展开
MCP 配置中的环境变量支持 `$VAR` 和 `${VAR}` 语法展开:
```json
{
"my-server": {
"command": "npx",
"args": ["@my-org/mcp-server"],
"env": {
"API_KEY": "$MY_API_KEY",
"DB_URL": "${DATABASE_URL}"
}
}
}
```
展开时缺失的变量会生成警告信息,但不阻止配置加载。
## 内置 MCP 动态注册
内置 MCP 服务器在 `main.tsx` 启动流程中动态注入配置:
### Computer Use MCP
```typescript
// src/utils/computerUse/setup.ts
export function setupComputerUseMCP(): {
mcpConfig: Record<string, ScopedMcpServerConfig>
allowedTools: string[]
} {
return {
mcpConfig: {
"computer-use": {
type: "stdio",
command: process.execPath,
args: ["--computer-use-mcp"],
scope: "dynamic",
}
},
allowedTools: ["mcp__computer-use__screenshot", ...]
}
}
```
启用条件:
- Feature flag `CHICAGO_MCP` 开启
- `getPlatform() !== "unknown"`macOS/Windows/Linux
- 非非交互式会话
- GrowthBook gate `getChicagoEnabled()` 返回 true
### Claude in Chrome MCP
```typescript
// 类似 Computer Use在 main.tsx 中注册
const { mcpConfig, allowedTools, systemPrompt } = setupClaudeInChrome()
dynamicMcpConfig = { ...dynamicMcpConfig, ...mcpConfig }
```
启用条件:
- `--chrome` 参数或 `claudeInChromeDefaultEnabled` 配置
- Chrome 扩展已安装
### VSCode SDK MCP
IDE 嵌入模式通过初始化消息传入 `type:'sdk'` 的配置,由 `setupVscodeSdkMcp()` 设置双向通知。
## 保留名称
以下 MCP 服务器名称被保留,用户无法手动配置同名服务器:
| 名称 | 用途 | 检查条件 |
|------|------|---------|
| `claude-in-chrome` | Chrome 浏览器控制 | 始终检查 |
| `computer-use` | 桌面自动化 | `CHICAGO_MCP` feature flag 开启时检查 |
| `claude-vscode` | VSCode IDE 集成 | 由 SDK 传入,不经过名称检查 |
保留名检查在两个位置:
1. `addMcpConfig()``config.ts:636-648`)— 运行时拒绝
2. `main.tsx` 启动检查(`main.tsx:2351-2368`)— 启动时退出
## 关键源文件索引
| 文件 | 职责 |
|------|------|
| `src/services/mcp/config.ts` | 配置管理核心:合并、去重、策略、添加/删除 |
| `src/services/mcp/types.ts` | Zod Schema 定义、类型声明 |
| `src/services/mcp/client.ts` | 连接管理、传输层选择 |
| `src/utils/plugins/mcpPluginIntegration.ts` | 插件 MCP 配置加载 |
| `src/utils/computerUse/setup.ts` | Computer Use 动态注册 |
| `src/utils/claudeInChrome/common.ts` | Chrome MCP 保留名与工具名 |
| `src/services/mcp/vscodeSdkMcp.ts` | VSCode SDK 双向通知 |

View File

@@ -1,25 +1,32 @@
---
title: "MCP 协议 - 连接管理、工具发现与执行链路"
description: "从源码角度解析 Claude Code 的 MCP 集成7 种传输层实现、connectToServer 的 memoize 缓存、工具发现的 LRU 策略、认证状态机、以及 MCP 工具如何进入权限检查链路。"
keywords: ["MCP", "Model Context Protocol", "工具扩展", "MCP 客户端", "工具发现"]
description: "从源码角度解析 Claude Code 的 MCP 集成:内置 MCP 与外部 MCP 的区别、7 种传输层实现、connectToServer 的 memoize 缓存、工具发现的 LRU 策略、认证状态机、以及 MCP 工具如何进入权限检查链路。"
keywords: ["MCP", "Model Context Protocol", "工具扩展", "MCP 客户端", "工具发现", "内置 MCP", "外部 MCP"]
---
{/* 本章目标:从源码角度揭示 MCP 客户端的连接管理、工具发现协议和执行链路 */}
{/* 本章目标:从源码角度揭示 MCP 客户端的两种运行模式(内置/外部)、连接管理、工具发现协议和执行链路 */}
## 架构总览:从配置到可用工具
```
settings.json: { mcpServers: { "my-db": { command: "npx", args: [...] } } }
配置层(多来源合并)
├── settings.json: { mcpServers: { "my-db": { command: "npx", args: [...] } } } ← 外部
├── .mcp.json: 项目级 MCP 配置 ← 外部
├── 插件 manifest (.mcp.json / .mcpb) ← 外部(插件)
├── claude.ai connectors ← 外部(远程)
├── enterprise managed-mcp.json ← 外部(企业管控)
├── setupComputerUseMCP() / setupClaudeInChrome() ← 内置(动态注册)
└── SDK 传入 (type:'sdk') ← 内置IDE 嵌入)
getAllMcpConfigs() ← enterprise 独占合并 user/project/local + plugin + claude.ai
getAllMcpConfigs() ← enterprise 独占合并 user/project/local + plugin + claude.ai
useManageMCPConnections() ← React Hook 管理连接生命周期
connectToServer(name, config) ← memoize 缓存lodash memoize
├── 创建 Transportstdio/sse/http/...
├── new Client() ← @modelcontextprotocol/sdk
├── client.connect(transport) ← 超时控制MCP_TIMEOUT, 默认 30s
└── 返回 MCPServerConnection ← { connected | failed | needs-auth | pending }
├── 判断:内置 MCP → InProcessTransport同进程
├── 判断:外部 stdio → StdioClientTransport子进程
├── 判断:远程 SSE/HTTP/WS → 网络传输
└── 返回 MCPServerConnection ← { connected | failed | needs-auth | pending | disabled }
fetchToolsForClient(client) ← LRU(20) 缓存
├── client.request({ method: 'tools/list' })
@@ -30,19 +37,208 @@ assembleToolPool() ← 合并内置工具 + MCP 工具
工具名格式: mcp__<serverName>__<toolName> ← buildMcpToolName()
```
## 两种 MCP 模式:内置 vs 外部
Claude Code 的 MCP 实现区分 **内置 MCP 服务器** 和 **外部 MCP 服务器**。两者使用相同的客户端协议和工具发现机制,但在连接方式、生命周期管理和配置来源上完全不同。
### 内置 MCP 服务器
内置 MCP 服务器由 Claude Code 自身提供,无需用户手动配置。它们在启动时自动注册为 `dynamic` scope 的配置,并在同进程内运行。
| 服务器 | 名称 | 包路径 | Feature Flag | 启用方式 |
|--------|------|--------|-------------|---------|
| Computer Use | `computer-use` | `@ant/computer-use-mcp` | `CHICAGO_MCP` | GrowthBook gate + macOS + interactive |
| Claude in Chrome | `claude-in-chrome` | `@ant/claude-for-chrome-mcp` | — | `--chrome` 参数或 `claudeInChromeDefaultEnabled` 配置 |
| VSCode SDK | `claude-vscode` | — | — | IDE 嵌入模式 (type:`sdk`) |
#### InProcessTransport零开销同进程通信
内置服务器通过 `InProcessTransport``src/services/mcp/InProcessTransport.ts`)运行,**不启动子进程**
```typescript
// 创建一对 linked transport —— 消息在两端之间直接传递
const [clientTransport, serverTransport] = createLinkedTransportPair()
// server 端连接到 serverTransport
inProcessServer = createComputerUseMcpServerForCli()
await inProcessServer.connect(serverTransport)
// client 端使用 clientTransport与外部 MCP 的 Client 相同接口)
transport = clientTransport
```
`InProcessTransport` 的核心设计:
- `send()` 通过 `queueMicrotask()` 异步投递消息到对端,避免同步请求/响应的栈深度问题
- `close()` 双向关闭,任一端关闭都会触发两端的 `onclose` 回调
- 无网络开销、无 IPC 序列化、无进程启动时间
#### 动态注册流程
内置服务器在 `main.tsx` 的启动流程中注册,注入 `dynamicMcpConfig`
```typescript
// main.tsx: Computer Use MCP 动态注册
if (feature("CHICAGO_MCP") && getPlatform() !== "unknown" && !getIsNonInteractiveSession()) {
const { getChicagoEnabled } = await import("src/utils/computerUse/gates.js")
if (getChicagoEnabled()) {
const { setupComputerUseMCP } = await import("src/utils/computerUse/setup.js")
const { mcpConfig, allowedTools } = setupComputerUseMCP()
dynamicMcpConfig = { ...dynamicMcpConfig, ...mcpConfig }
allowedTools.push(...cuTools)
}
}
```
`setupComputerUseMCP()` 返回的配置(`src/utils/computerUse/setup.ts`
```typescript
{
"computer-use": {
type: "stdio", // 类型标记为 stdio但 client.ts 会拦截为 InProcessTransport
command: process.execPath,
args: ["--computer-use-mcp"],
scope: "dynamic", // 动态作用域,不持久化
}
}
```
#### 连接时拦截
`connectToServer()` 在 `client.ts:906-944` 中根据服务器名拦截内置服务器:
```typescript
// Chrome MCP — 在 process 内运行,避免 ~325MB 子进程
if (isClaudeInChromeMCPServer(name)) {
const { createChromeContext } = await import('../../utils/claudeInChrome/mcpServer.js')
const { createClaudeForChromeMcpServer } = await import('@ant/claude-for-chrome-mcp')
const { createLinkedTransportPair } = await import('./InProcessTransport.js')
const context = createChromeContext(config.env)
inProcessServer = createClaudeForChromeMcpServer(context)
const [clientTransport, serverTransport] = createLinkedTransportPair()
await inProcessServer.connect(serverTransport)
transport = clientTransport
}
// Computer Use MCP — 同理
if (feature('CHICAGO_MCP') && isComputerUseMCPServer(name)) {
const { createComputerUseMcpServerForCli } = await import('../../utils/computerUse/mcpServer.js')
const { createLinkedTransportPair } = await import('./InProcessTransport.js')
inProcessServer = await createComputerUseMcpServerForCli()
const [clientTransport, serverTransport] = createLinkedTransportPair()
await inProcessServer.connect(serverTransport)
transport = clientTransport
}
```
#### 保留名称保护
内置服务器的名称被保留,用户无法手动添加同名配置(`config.ts:636-648`
```typescript
// 添加 MCP 配置时检查保留名
if (isClaudeInChromeMCPServer(name)) {
throw new Error(`Cannot add MCP server "${name}": this name is reserved.`)
}
if (feature('CHICAGO_MCP') && isComputerUseMCPServer(name)) {
throw new Error(`Cannot add MCP server "${name}": this name is reserved.`)
}
```
启动时也有全局检查(`main.tsx:2351-2368`):如果用户配置中包含保留名(非 `type:'sdk'`),直接 `process.exit(1)`。
#### VSCode SDK MCP
VSCode SDK MCP 是特殊的内置模式。IDE如 VS Code、JetBrains通过嵌入方式启动 Claude Code并传入 `type:'sdk'` 的 MCP 配置。这类配置:
- 不经过保留名称检查IDE 可以使用任意名称)
- 不参与 enterprise MCP 的排他控制
- 通过 VSCode SDK transport 连接
- 支持双向通知(如 `file_updated`、`experiment_gates`
```typescript
// src/services/mcp/vscodeSdkMcp.ts
export function setupVscodeSdkMcp(sdkClients: MCPServerConnection[]): void {
const client = sdkClients.find(client => client.name === 'claude-vscode')
if (client && client.type === 'connected') {
// 注册 log_event 通知处理器
client.client.setNotificationHandler(LogEventNotificationSchema(), ...)
// 发送实验门控到 VSCode
client.client.notification({ method: 'experiment_gates', params: { gates } })
}
}
```
### 外部 MCP 服务器
外部 MCP 服务器由用户在配置文件中声明,通过子进程或网络连接运行。
#### 配置来源
| 来源 | Scope | 文件位置 | 优先级 |
|------|-------|---------|--------|
| 项目配置 | `project` | `<project>/.mcp.json` | 最高(同名覆盖) |
| 本地配置 | `local` | `<project>/.claude/settings.local.json` | 高 |
| 用户配置 | `user` | `~/.claude/settings.json` | 中 |
| 插件 | `dynamic` | 插件 manifest 中 `.mcp.json` | 中 |
| claude.ai | `claudeai` | 通过 API 获取 | 低 |
| 企业管控 | `enterprise` | 系统管理路径 `managed-mcp.json` | 排他(存在时覆盖全部) |
#### 配置示例
```json
// settings.json / .mcp.json 中的 MCP 配置
{
"mcpServers": {
// stdio 类型 — 启动子进程
"my-database": {
"command": "npx",
"args": ["@my-org/db-mcp-server"],
"env": { "DB_URL": "postgres://..." }
},
// HTTP 流类型 — 远程服务器
"remote-api": {
"type": "http",
"url": "https://api.example.com/mcp"
},
// SSE 类型 — Server-Sent Events
"realtime-feed": {
"type": "sse",
"url": "https://feed.example.com/sse"
},
// WebSocket 类型
"ws-service": {
"type": "ws",
"url": "wss://ws.example.com/mcp"
}
}
}
```
#### 配置合并与去重
`getAllMcpConfigs()``config.ts`)按优先级合并多个来源的配置:
1. 企业管控配置存在时,**独占返回**(忽略所有其他来源)
2. 否则合并user → project → local → plugin → claude.ai
3. 插件与手动配置去重:通过 `getMcpServerSignature()` 生成内容签名(基于 command/args/url插件配置被同名手动配置抑制
4. `addScopeToServers()` 为每个配置项标注来源 scope
## 7 种传输层实现
`connectToServer()``client.ts:596-1643`)根据 `config.type` 分发到不同的 Transport 实现:
| 传输类型 | Transport 类 | 适用场景 | 认证方式 |
|----------|-------------|---------|---------|
| `stdio`(默认) | `StdioClientTransport` | 本地子进程 | 无 |
| `stdio`(默认) | `StdioClientTransport` | 外部本地子进程 | 无 |
| `sse` | `SSEClientTransport` | 远程 SSE 服务 | `ClaudeAuthProvider` + OAuth |
| `http` | `StreamableHTTPClientTransport` | HTTP 流 | `ClaudeAuthProvider` + OAuth |
| `sse-ide` | `SSEClientTransport` | IDE 集成 | lockfile token |
| `ws-ide` | `WebSocketTransport` | IDE WebSocket | `X-Claude-Code-Ide-Authorization` |
| `ws` | `WebSocketTransport` | WebSocket 服务 | session ingress token |
| `claudeai-proxy` | `StreamableHTTPClientTransport` | claude.ai 代理 | OAuth bearer + 401 重试 |
| InProcess内置 | `InProcessTransport` | Computer Use / Chrome | 无(同进程) |
### stdio 传输的进程管理
@@ -112,9 +308,17 @@ timer.unref?.() // 不阻止进程退出
```typescript
const fullyQualifiedName = buildMcpToolName(client.name, tool.name)
// 结果: "mcp__my-db__query"
// 结果: "mcp__my-database__query"
```
### 内置 MCP 的工具发现
内置 MCP 服务器虽然使用 InProcessTransport但工具发现流程与外部服务器完全一致
- **Computer Use**`createComputerUseMcpServerForCli()` 在 `src/utils/computerUse/mcpServer.ts` 中构建 MCP Server 对象,注册 `ListToolsRequestSchema` handler。工具描述包含平台特定的已安装应用列表1s 超时枚举)。
- **Claude in Chrome**`createClaudeForChromeMcpServer()` 在 `@ant/claude-for-chrome-mcp` 包中构建 Server提供 17+ 个浏览器控制工具。
- **VSCode SDK**:由 IDE 端提供工具列表,通过 SDK transport 传递。
### 工具描述截断
MCP 工具描述上限 2048 字符(`MAX_MCP_DESCRIPTION_LENGTH`。OpenAPI 生成的 MCP 服务器曾观察到 15-60KB 的描述文档。
@@ -134,6 +338,8 @@ MCP 工具描述上限 2048 字符(`MAX_MCP_DESCRIPTION_LENGTH`。OpenAPI
MCP 工具默认返回 `{ behavior: 'passthrough' }``client.ts:1816-1834`),意味着它们始终进入权限确认流程。工具名使用 `mcp__` 前缀精确匹配权限规则。
内置 MCP 服务器的工具通过 `allowedTools` 列表自动授权——在 `main.tsx` 启动时加入,绕过普通权限提示。例如 Computer Use 工具的 `request_access` 自行处理会话级审批。
## MCP 工具的执行链路
```
@@ -169,23 +375,33 @@ getRemoteMcpServerConnectionBatchSize() // 默认 20
本地 MCP 服务器stdio是重量级的子进程默认限制 3 个并发连接。远程服务器是轻量级 HTTP 请求,允许 20 个并发。
## 实际配置示例
## 内置 vs 外部 MCP 对比总结
```json
// settings.json 中的 MCP 配置
{
"mcpServers": {
"my-database": {
"command": "npx",
"args": ["@my-org/db-mcp-server"],
"env": { "DB_URL": "postgres://..." }
},
"remote-api": {
"type": "http",
"url": "https://api.example.com/mcp"
}
}
}
```
| 维度 | 内置 MCP | 外部 MCP |
|------|---------|---------|
| **Transport** | `InProcessTransport`(同进程) | stdio / SSE / HTTP / WebSocket |
| **配置来源** | `setupComputerUseMCP()` / `setupClaudeInChrome()` 等动态注册 | settings.json / .mcp.json / 插件 / claude.ai |
| **Scope** | `dynamic` | `user` / `project` / `local` / `enterprise` / `claudeai` |
| **进程模型** | 同进程,零开销 | 子进程stdio或网络连接 |
| **名称保护** | 保留名,用户不可添加同名 | 自由命名(字母数字 + `-_` |
| **生命周期** | 随 CLI 启停 | 连接缓存 + 按需重连 |
| **权限** | `allowedTools` 自动授权 | `passthrough` 进入权限确认 |
| **Feature Flag** | `CHICAGO_MCP`Computer Use等 | 无(始终可用) |
| **工具发现** | 与外部相同MCP 协议) | 标准 MCP `tools/list` |
| **清理** | `inProcessServer.close()` | 信号升级策略 SIGINT→SIGTERM→SIGKILL |
配置后AI 的工具列表中会出现 `mcp__my-database__query` 和 `mcp__remote-api__*` 工具——与内置工具使用相同的权限检查链路和 UI 渲染。
## 关键源文件索引
| 文件 | 职责 |
|------|------|
| `src/services/mcp/client.ts` | 核心客户端connectToServer、fetchToolsForClient、MCPTool.call |
| `src/services/mcp/config.ts` | 配置管理getAllMcpConfigs、addMcpConfig、removeMcpConfig |
| `src/services/mcp/types.ts` | 类型定义:配置 Schema、连接状态类型 |
| `src/services/mcp/InProcessTransport.ts` | 内置 MCP 传输层linked transport pair |
| `src/services/mcp/vscodeSdkMcp.ts` | VSCode SDK MCP双向通知、实验门控 |
| `src/services/mcp/useManageMCPConnections.ts` | React Hook连接生命周期、重连 |
| `src/utils/computerUse/mcpServer.ts` | Computer Use MCP Server 构建 |
| `src/utils/computerUse/setup.ts` | Computer Use 动态注册 |
| `src/utils/claudeInChrome/mcpServer.ts` | Chrome MCP Server 构建 + Bridge 配置 |
| `src/tools/MCPTool/MCPTool.ts` | MCP 工具包装:统一 Tool 接口 |
| `src/entrypoints/mcp.ts` | MCP server 入口Claude Code 作为 MCP server |

View File

@@ -19,7 +19,7 @@
| 11 | BigQuery Metrics | `api.anthropic.com/api/claude_code/metrics` | HTTPS | 默认启用 |
| 12 | MCP Proxy | `mcp-proxy.anthropic.com` | HTTPS+WS | 使用 MCP 工具时 |
| 13 | MCP Registry | `api.anthropic.com/mcp-registry` | HTTPS | 查询 MCP 服务器时 |
| 14 | Bing Search | `www.bing.com` | HTTPS | WebSearch 工具 |
| 14 | Web Search Pages | `www.bing.com`, `search.brave.com` | HTTPS | WebSearch 工具,可通过 `WEB_SEARCH_ADAPTER=bing|brave` 切换 |
| 15 | Google Cloud Storage (更新) | `storage.googleapis.com` | HTTPS | 版本检查 |
| 16 | GitHub Raw (Changelog/Stats) | `raw.githubusercontent.com` | HTTPS | 更新提示 |
| 17 | Claude in Chrome Bridge | `bridge.claudeusercontent.com` | WSS | Chrome 集成 |
@@ -121,12 +121,16 @@ Anthropic 托管的 MCP 服务器代理。
- **端点**: `https://api.anthropic.com/mcp-registry/v0/servers?version=latest&visibility=commercial`
- **文件**: `src/services/mcp/officialRegistry.ts`
### 14. Bing Search
### 14. Web Search Pages
WebSearch 工具的默认适配器,抓取 Bing 搜索结果
WebSearch 工具支持直接抓取 Bing 搜索结果页面,也支持通过 Brave 的 LLM Context API
获取搜索上下文;可通过 `WEB_SEARCH_ADAPTER=bing|brave` 显式切换后端。
- **端点**: `https://www.bing.com/search?q={query}&setmkt=en-US`
- **文件**: `src/tools/WebSearchTool/adapters/bingAdapter.ts`
- **Bing 端点**: `https://www.bing.com/search?q={query}&setmkt=en-US`
- **Brave 端点**: `https://api.search.brave.com/res/v1/llm/context?q={query}`
- **文件**:
- `src/tools/WebSearchTool/adapters/bingAdapter.ts`
- `src/tools/WebSearchTool/adapters/braveAdapter.ts`
另外还有 Domain Blocklist 查询:
- **端点**: `https://api.anthropic.com/api/web/domain_info?domain={domain}`
@@ -201,6 +205,7 @@ WebSearch 工具的默认适配器,抓取 Bing 搜索结果。
| `{region}-aiplatform.googleapis.com` | Google Vertex AI | HTTPS |
| `{resource}.services.ai.azure.com` | Azure Foundry | HTTPS |
| `www.bing.com` | Bing 搜索 | HTTPS |
| `search.brave.com` | Brave 搜索 | HTTPS |
| `storage.googleapis.com` | 自动更新 | HTTPS |
| `raw.githubusercontent.com` | Changelog / 插件统计 | HTTPS |
| `bridge.claudeusercontent.com` | Chrome Bridge | WSS |

189
docs/features/acp-zed.md Normal file
View File

@@ -0,0 +1,189 @@
# ACP (Agent Client Protocol) — Zed / IDE 集成
> Feature Flag: `FEATURE_ACP=1`build 和 dev 模式默认启用)
> 实现状态:可用(支持 Zed、Cursor 等 ACP 客户端)
> 源码目录:`src/services/acp/`
## 一、功能概述
ACP (Agent Client Protocol) 是一种标准化的 stdio 协议,允许 IDE 和编辑器通过 stdin/stdout 的 NDJSON 流驱动 AI Agent。CCB 实现了完整的 ACP agent 端,可以被 Zed、Cursor 等支持 ACP 的客户端直接调用。
### 核心特性
- **会话管理**:新建 / 恢复 / 加载 / 分叉 / 关闭会话
- **历史回放**:恢复会话时自动加载并回放对话历史
- **权限桥接**ACP 客户端的权限决策映射到 CCB 的工具权限系统
- **斜杠命令 & Skills**:加载真实命令列表,支持 `/commit``/review` 等 prompt 型 skill
- **Context Window 跟踪**:精确的 usage_update含 model prefix matching
- **Prompt 排队**:支持连续发送多条 prompt自动排队处理
- **模式切换**auto / default / acceptEdits / plan / dontAsk / bypassPermissions
- **模型切换**:运行时切换 AI 模型
## 二、架构
```
┌──────────────┐ NDJSON/stdio ┌──────────────────┐
│ Zed / IDE │ ◄────────────────► │ CCB ACP Agent │
│ (Client) │ stdin / stdout │ (Agent) │
└──────────────┘ │ │
│ entry.ts │ ← stdio → NDJSON stream
│ agent.ts │ ← ACP protocol handler
│ bridge.ts │ ← SDKMessage → ACP SessionUpdate
│ permissions.ts │ ← 权限桥接
│ utils.ts │ ← 通用工具
│ │
│ QueryEngine │ ← 内部查询引擎
└──────────────────┘
```
### 文件职责
| 文件 | 职责 |
|------|------|
| `entry.ts` | 入口,创建 stdio → NDJSON stream启动 `AgentSideConnection` |
| `agent.ts` | 实现 ACP `Agent` 接口:会话 CRUD、prompt、cancel、模式/模型切换 |
| `bridge.ts` | `SDKMessage` → ACP `SessionUpdate` 转换:文本/思考/工具/用量/编辑 diff |
| `permissions.ts` | ACP `requestPermission()` → CCB `CanUseToolFn` 桥接 |
| `utils.ts` | Pushable、流转换、权限模式解析、session fingerprint、路径显示 |
## 三、配置 Zed 编辑器
### 3.1 Zed settings.json 配置
打开 Zed 的 `settings.json``Cmd+,` → Open Settings添加 `agent_servers` 配置:
```json
{
"agent_servers": {
"ccb": {
"type": "custom",
"command": "ccb",
"args": ["--acp"]
}
}
}
```
### 3.3 API 认证配置
CCB 的 ACP agent 在启动时会自动加载 `settings.json` 中的环境变量(`ANTHROPIC_BASE_URL``ANTHROPIC_AUTH_TOKEN` 等)。确保已通过 `/login` 配置好 API 供应商。
也可通过环境变量传入:
```json
{
"agent_servers": {
"claude-code": {
"command": "ccb",
"args": ["--acp"],
"env": {
"ANTHROPIC_BASE_URL": "https://api.example.com/v1",
"ANTHROPIC_AUTH_TOKEN": "sk-xxx"
}
}
}
}
```
### 3.4 在 Zed 中使用
1. 配置完成后重启 Zed
2. 打开任意项目目录
3.`Cmd+'`macOS`Ctrl+'`Linux打开 Agent Panel
4. 在 Agent Panel 顶部的下拉菜单中选择 **claude-code**
5. 开始对话
### 3.5 功能说明
| 功能 | 操作 |
|------|------|
| 对话 | 在 Agent Panel 中直接输入消息 |
| 斜杠命令 | 输入 `/` 查看可用 skills 列表(如 `/commit``/review` |
| 工具权限 | 弹出权限请求时选择 Allow / Reject / Always Allow |
| 模式切换 | 通过 Agent Panel 的设置菜单切换 auto/default/plan 等模式 |
| 模型切换 | 通过 Agent Panel 的设置菜单切换 AI 模型 |
| 会话恢复 | 关闭重开 Zed 后,之前的会话可自动恢复(含历史消息) |
## 四、配置其他 ACP 客户端
ACP 是开放协议,任何支持 ACP 的客户端都可以连接 CCB。通用配置模式
```
命令: ccb --acp
参数: ["--acp"]
通信: stdin/stdout NDJSON
协议版本: ACP v1
```
### 4.1 Cursor
在 Cursor 的设置中配置 MCP / Agent Server使用同样的 `ccb --acp` 命令。
### 4.2 自定义客户端
使用 `@agentclientprotocol/sdk` 可以快速构建 ACP 客户端:
```typescript
import { ClientSideConnection, ndJsonStream } from '@agentclientprotocol/sdk'
// 创建连接(将 ccb --acp 作为子进程启动)
const child = spawn('ccb', ['--acp'])
const stream = ndJsonStream(
Writable.toWeb(child.stdin),
Readable.toWeb(child.stdout),
)
const client = new ClientSideConnection(stream)
// 初始化
await client.initialize({ clientCapabilities: {} })
// 创建会话
const { sessionId } = await client.newSession({
cwd: '/path/to/project',
})
// 发送 prompt
const response = await client.prompt({
sessionId,
prompt: [{ type: 'text', text: 'Hello, explain this project' }],
})
// 监听 session 更新
client.on('sessionUpdate', (update) => {
console.log('Update:', update)
})
```
## 五、ACP 协议支持矩阵
| 方法 | 状态 | 说明 |
|------|------|------|
| `initialize` | ✅ | 返回 agent 信息和能力 |
| `authenticate` | ✅ | 无需认证(自托管) |
| `newSession` | ✅ | 创建新会话 |
| `resumeSession` | ✅ | 恢复已有会话(含历史回放) |
| `loadSession` | ✅ | 加载指定会话(含历史回放) |
| `listSessions` | ✅ | 列出可用会话 |
| `forkSession` | ✅ | 分叉会话 |
| `closeSession` | ✅ | 关闭会话 |
| `prompt` | ✅ | 发送消息,支持排队 |
| `cancel` | ✅ | 取消当前/排队的 prompt |
| `setSessionMode` | ✅ | 切换权限模式 |
| `setSessionModel` | ✅ | 切换 AI 模型 |
| `setSessionConfigOption` | ✅ | 动态修改配置 |
### SessionUpdate 类型
| 类型 | 状态 | 说明 |
|------|------|------|
| `agent_message_chunk` | ✅ | 助手文本消息 |
| `agent_thought_chunk` | ✅ | 思考/推理内容 |
| `user_message_chunk` | ✅ | 用户消息(历史回放) |
| `tool_call` | ✅ | 工具调用开始 |
| `tool_call_update` | ✅ | 工具调用结果/状态更新 |
| `usage_update` | ✅ | token 用量 + context window |
| `plan` | ✅ | TodoWrite → plan entries |
| `available_commands_update` | ✅ | 斜杠命令 & skills 列表 |
| `current_mode_update` | ✅ | 模式切换通知 |
| `config_option_update` | ✅ | 配置更新通知 |

View File

@@ -0,0 +1,30 @@
# Chrome Use — 浏览器自动化快速指南
让 Claude Code 直接控制你的 Chrome 浏览器,用自然语言完成网页操作。
## 快速开始3 分钟)
### 第一步:安装 Chrome 扩展
1. 下载扩展https://github.com/hangwin/mcp-chrome/releases
2. 解压 zip 文件
3. 打开 Chrome 访问 `chrome://extensions/`
4. 开启右上角「开发者模式」
5. 点击「加载已解压的扩展程序」,选择解压后的文件夹
### 第二步:启动 Claude Code
```bash
bun run dev
ccb # 或者 ccb 安装版也行
```
### 第三步:启用 Chrome MCP
1. 在 REPL 中输入 `/mcp` 打开 MCP 面板
2. 找到 `mcp-chrome`,按空格键启用
3. 按 Enter 确认
## 相关文档
- GitHub 仓库https://github.com/hangwin/mcp-chrome

View File

@@ -138,13 +138,19 @@ bun run dist/cli.js
/remote-control
```
CLI 会向 RCS 注册环境,注册成功后在终端显示连接 URL
环境型 Remote Control例如 `claude remote-control` 子命令)会向 RCS 注册环境,注册成功后在终端显示连接 URL
```
https://rcs.example.com/code?bridge=<environmentId>
```
同时支持 QR 码扫码打开。该 URL 即 Web UI 控制面板入口,在浏览器中打开即可远程操控当前会话。
交互式 REPL 方式(`--remote-control``/remote-control`)在某些桥接模式下也可能直接给出会话 URL
```
https://rcs.example.com/code/session_<id>
```
两种 URL 都可以直接在浏览器打开并远程操控当前会话;只有 environment 模式才会出现在 Web UI 的环境列表中。
若已连接,再次执行 `/remote-control` 会显示对话框,包含以下选项:
- **Disconnect this session** — 断开远程连接
@@ -165,7 +171,7 @@ claude bridge
通过 `/remote-control` 命令获取 URL 后,在浏览器打开即可使用。功能:
- 查看已注册的运行环境
- 查看已注册的运行环境environment 模式)
- 创建和管理会话
- 实时查看对话消息和工具调用
- 审批 Claude Code 的工具权限请求
@@ -275,4 +281,3 @@ curl https://rcs.example.com/health
| 依赖 | claude.ai 订阅 + OAuth | 仅需 API Key |
自托管模式的核心优势是:设置 `CLAUDE_BRIDGE_BASE_URL` 后,代码自动调用 `isSelfHostedBridge()` 返回 `true`,跳过所有 GrowthBook 和订阅检查,无需 claude.ai 账户即可使用。

View File

@@ -1,11 +1,11 @@
# WEB_SEARCH_TOOL — 网页搜索工具
> 实现状态:适配器架构完成,Bing 适配器为当前默认后端
> 实现状态:适配器架构完成,支持 API / Bing / Brave 三种后端
> 引用数:核心工具,无 feature flag 门控(始终启用)
## 一、功能概述
WebSearchTool 让模型可以搜索互联网获取最新信息。原始实现仅支持 Anthropic API 服务端搜索(`web_search_20250305` server tool在第三方代理端点下不可用。现已重构为适配器架构新增 Bing 搜索页面解析作为 fallback,确保任何 API 端点都能使用搜索功能。
WebSearchTool 让模型可以搜索互联网获取最新信息。原始实现仅支持 Anthropic API 服务端搜索(`web_search_20250305` server tool在第三方代理端点下不可用。现已重构为适配器架构支持 API 服务端搜索,以及 Bing / Brave 两个 HTML 解析后端,确保任何 API 端点都能使用搜索功能。
## 二、实现架构
@@ -21,9 +21,13 @@ WebSearchTool.call()
│ └── 使用 web_search_20250305 server tool
│ 通过 queryModelWithStreaming 二次调用 API
── BingSearchAdapter — Bing HTML 抓取 + 正则提取(当前默认)
└── 直接抓取 Bing 搜索页 HTML
正则提取 b_algo 块中的标题/URL/摘要
── BingSearchAdapter — Bing HTML 抓取 + 正则提取
└── 直接抓取 Bing 搜索页 HTML
正则提取 b_algo 块中的标题/URL/摘要
└── BraveSearchAdapter — Brave LLM Context API
└── 调用 Brave HTTPS GET 接口
将 grounding payload 映射为标题/URL/摘要
```
### 2.2 模块结构
@@ -37,8 +41,9 @@ WebSearchTool.call()
| 适配器工厂 | `src/tools/WebSearchTool/adapters/index.ts` | `createAdapter()` 工厂函数,选择后端 |
| API 适配器 | `src/tools/WebSearchTool/adapters/apiAdapter.ts` | 封装原有 `queryModelWithStreaming` 逻辑,使用 server tool |
| Bing 适配器 | `src/tools/WebSearchTool/adapters/bingAdapter.ts` | Bing HTML 抓取 + 正则解析 |
| 单元测试 | `src/tools/WebSearchTool/__tests__/bingAdapter.test.ts` | 32 个测试用例 |
| 集成测试 | `src/tools/WebSearchTool/__tests__/bingAdapter.integration.ts` | 真实网络请求验证 |
| Brave 适配器 | `src/tools/WebSearchTool/adapters/braveAdapter.ts` | Brave LLM Context API 适配与结果映射 |
| 单元测试 | `src/tools/WebSearchTool/__tests__/bingAdapter.test.ts`, `src/tools/WebSearchTool/__tests__/braveAdapter*.test.ts`, `src/tools/WebSearchTool/__tests__/adapterFactory.test.ts` | Bing / Brave 解析与工厂逻辑测试 |
| 集成测试 | `src/tools/WebSearchTool/__tests__/bingAdapter.integration.ts`, `src/tools/WebSearchTool/__tests__/braveAdapter.integration.ts` | 真实网络请求验证 |
### 2.3 数据流
@@ -49,20 +54,18 @@ WebSearchTool.call()
validateInput() — 校验 query 非空、allowed/block 不共存
createAdapter() → BingSearchAdapter当前硬编码
createAdapter() → ApiSearchAdapter | BingSearchAdapter | BraveSearchAdapter
adapter.search(query, { allowedDomains, blockedDomains, signal, onProgress })
├── onProgress({ type: 'query_update', query })
├── axios.get(bing.com/search?q=...&setmkt=en-US)
│ └── 13 个 Edge 浏览器请求头
├── axios.get(search-engine-url)
│ └── API 鉴权请求头
├── extractBingResults(html) — 正则提取 <li class="b_algo"> 块
── resolveBingUrl() — 解码 base64 重定向 URL
│ ├── extractSnippet() — 三级降级摘要提取
│ └── decodeHtmlEntities() — he.decode
├── extractResults(payload) — 按后端提取结果
── grounding → SearchResult[] 映射
├── 客户端域名过滤 (allowedDomains / blockedDomains)
@@ -117,19 +120,18 @@ Bing 返回的重定向 URL 格式:`bing.com/ck/a?...&u=a1aHR0cHM6Ly9...`
## 四、适配器选择逻辑
当前 `createAdapter()` 硬编码返回 `BingSearchAdapter`,原逻辑已注释保留
`createAdapter()` 按以下优先级选择后端,并按选中的后端 key 缓存适配器实例
```typescript
export function createAdapter(): WebSearchAdapter {
return new BingSearchAdapter()
// 注释保留的选择逻辑:
// 1. WEB_SEARCH_ADAPTER 环境变量强制指定 api|bing
// 2. isFirstPartyAnthropicBaseUrl() → API 适配器
// 3. 第三方端点 → Bing 适配器
// 1. WEB_SEARCH_ADAPTER=api|bing|brave 显式指定
// 2. Anthropic 官方 API Base URL → ApiSearchAdapter
// 3. 第三方代理 / 非官方端点 → BingSearchAdapter
}
```
恢复自动选择:取消 `index.ts` 中的注释即可。
显式指定 `WEB_SEARCH_ADAPTER=brave` 时,会改用 Brave LLM Context API 后端,并要求
`BRAVE_SEARCH_API_KEY``BRAVE_API_KEY`
## 五、接口定义

View File

@@ -146,14 +146,15 @@ AI 的信息获取不局限于本地代码:
### WebSearch 实现机制
WebSearch 通过适配器模式支持种搜索后端,由 `src/tools/WebSearchTool/adapters/` 中的工厂函数 `createAdapter()` 选择:
WebSearch 通过适配器模式支持种搜索后端,由 `src/tools/WebSearchTool/adapters/` 中的工厂函数 `createAdapter()` 选择:
```
适配器架构:
WebSearchTool.call()
→ createAdapter() 选择后端
├─ ApiSearchAdapter — Anthropic API 服务端搜索(需官方 API 密钥)
─ BingSearchAdapter — 直接抓取 Bing 搜索页面解析(无需 API 密钥)
─ BingSearchAdapter — 直接抓取 Bing 搜索页面解析(无需 API 密钥)
└─ BraveSearchAdapter — 调用 Brave LLM Context API 解析(需 Brave API 密钥)
→ adapter.search(query, options)
→ 转换为统一 SearchResult[] 格式返回
```
@@ -166,8 +167,9 @@ WebSearch 通过适配器模式支持两种搜索后端,由 `src/tools/WebSear
|--------|------|--------|
| 1 | 环境变量 `WEB_SEARCH_ADAPTER=api` | `ApiSearchAdapter` |
| 2 | 环境变量 `WEB_SEARCH_ADAPTER=bing` | `BingSearchAdapter` |
| 3 | API Base URL 指向 Anthropic 官方 | `ApiSearchAdapter` |
| 4 | 第三方代理 / 非官方端点 | `BingSearchAdapter` |
| 3 | 环境变量 `WEB_SEARCH_ADAPTER=brave` | `BraveSearchAdapter` |
| 4 | API Base URL 指向 Anthropic 官方 | `ApiSearchAdapter` |
| 5 | 第三方代理 / 非官方端点 | `BingSearchAdapter` |
适配器是无状态的,同一会话内缓存复用。

View File

@@ -86,6 +86,7 @@
"group": "可扩展性",
"pages": [
"docs/extensibility/mcp-protocol",
"docs/extensibility/mcp-configuration",
"docs/extensibility/hooks",
"docs/extensibility/skills",
"docs/extensibility/custom-agents"
@@ -177,21 +178,7 @@
]
}
],
"excludes": [
"docs/test-plans/**",
"docs/testing-spec.md",
"docs/REVISION-PLAN.md",
"docs/feature-exploration-plan.md",
"docs/ultraplan-implementation.md",
"docs/features/feature-flags-audit-complete.md",
"docs/features/feature-flags-codex-review.md",
"docs/features/growthbook-enablement-plan.md",
"docs/features/computer-use-architecture-v2.md",
"docs/features/computer-use-mcp-test-report.md",
"docs/features/computer-use-tools-reference.md",
"docs/features/computer-use-windows-enhancement.md",
"docs/features/lan-pipes-implementation.md"
],
"excludes": [],
"footerSocials": {
"github": "https://github.com/anthropics/claude-code"
}

View File

@@ -1,6 +1,6 @@
{
"name": "claude-code-best",
"version": "1.3.2",
"version": "1.3.7",
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
"type": "module",
"author": "claude-code-best <claude-code-best@proton.me>",
@@ -31,7 +31,8 @@
},
"workspaces": [
"packages/*",
"packages/@ant/*"
"packages/@ant/*",
"packages/@anthropic-ai/*"
],
"files": [
"dist",
@@ -40,6 +41,9 @@
],
"scripts": {
"build": "bun run build.ts",
"build:vite": "vite build && bun run scripts/post-build.ts",
"build:vite:only": "vite build",
"build:bun": "bun run build.ts",
"dev": "bun run scripts/dev.ts",
"dev:inspect": "bun run scripts/dev-debug.ts",
"prepublishOnly": "bun run build",
@@ -52,17 +56,17 @@
"health": "bun run scripts/health-check.ts",
"postinstall": "node scripts/postinstall.cjs && node scripts/setup-chrome-mcp.mjs",
"docs:dev": "npx mintlify dev",
"typecheck": "tsc --noEmit",
"rcs": "bun run scripts/rcs.ts"
},
"dependencies": {
"mcp-chrome-bridge": "^1.0.31"
"@agentclientprotocol/sdk": "^0.19.0",
"@claude-code-best/mcp-chrome-bridge": "^2.0.7",
"ws": "^8.20.0"
},
"devDependencies": {
"@types/he": "^1.2.3",
"@langfuse/otel": "^5.1.0",
"@langfuse/tracing": "^5.1.0",
"@types/lodash-es": "^4.17.12",
"@alcalzone/ansi-tokenize": "^0.3.0",
"@anthropic-ai/model-provider": "workspace:*",
"@ant/claude-for-chrome-mcp": "workspace:*",
"@ant/computer-use-input": "workspace:*",
"@ant/computer-use-mcp": "workspace:*",
@@ -82,8 +86,13 @@
"@aws-sdk/credential-providers": "^3.1020.0",
"@azure/identity": "^4.13.1",
"@biomejs/biome": "^2.4.10",
"@claude-code-best/agent-tools": "workspace:*",
"@claude-code-best/builtin-tools": "workspace:*",
"@claude-code-best/mcp-client": "workspace:*",
"@commander-js/extra-typings": "^14.0.0",
"@growthbook/growthbook": "^1.6.5",
"@langfuse/otel": "^5.1.0",
"@langfuse/tracing": "^5.1.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/api-logs": "^0.214.0",
@@ -106,8 +115,11 @@
"@sentry/node": "^10.47.0",
"@smithy/core": "^3.23.13",
"@smithy/node-http-handler": "^4.5.1",
"@types/bun": "^1.3.11",
"@types/bun": "^1.3.12",
"@types/cacache": "^20.0.1",
"@types/he": "^1.2.3",
"@types/lodash-es": "^4.17.12",
"@types/node": "^25.6.0",
"@types/picomatch": "^4.0.3",
"@types/plist": "^3.0.5",
"@types/proper-lockfile": "^4.1.4",
@@ -163,6 +175,7 @@
"react": "^19.2.4",
"react-compiler-runtime": "^1.0.0",
"react-reconciler": "^0.33.0",
"rollup": "^4.60.1",
"semver": "^7.7.4",
"sharp": "^0.34.5",
"shell-quote": "^1.8.3",
@@ -177,11 +190,11 @@
"undici": "^7.24.6",
"url-handler-napi": "workspace:*",
"usehooks-ts": "^3.1.1",
"vite": "^8.0.8",
"vscode-jsonrpc": "^8.2.1",
"vscode-languageserver-protocol": "^3.17.5",
"vscode-languageserver-types": "^3.17.5",
"wrap-ansi": "^10.0.0",
"ws": "^8.20.0",
"xss": "^1.0.15",
"yaml": "^2.8.3",
"zod": "^4.3.6"

View File

@@ -0,0 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -5,9 +5,12 @@
* mouse and keyboard via CoreGraphics events and System Events.
*/
import { $ } from 'bun'
import { execFile, execFileSync } from 'child_process'
import { promisify } from 'util'
import type { FrontmostAppInfo, InputBackend } from '../types.js'
const execFileAsync = promisify(execFile)
const KEY_MAP: Record<string, number> = {
return: 36, enter: 36, tab: 48, space: 49, delete: 51, backspace: 51,
escape: 53, esc: 53,
@@ -25,13 +28,17 @@ const MODIFIER_MAP: Record<string, string> = {
}
async function osascript(script: string): Promise<string> {
const result = await $`osascript -e ${script}`.quiet().nothrow().text()
return result.trim()
const { stdout } = await execFileAsync('osascript', ['-e', script], {
encoding: 'utf-8',
})
return stdout.trim()
}
async function jxa(script: string): Promise<string> {
const result = await $`osascript -l JavaScript -e ${script}`.quiet().nothrow().text()
return result.trim()
const { stdout } = await execFileAsync('osascript', ['-l', 'JavaScript', '-e', script], {
encoding: 'utf-8',
})
return stdout.trim()
}
function buildMouseJxa(eventType: string, x: number, y: number, btn: number, clickState?: number): string {
@@ -115,19 +122,14 @@ export const typeText: InputBackend['typeText'] = async (text) => {
export const getFrontmostAppInfo: InputBackend['getFrontmostAppInfo'] = () => {
try {
const result = Bun.spawnSync({
cmd: ['osascript', '-e', `
tell application "System Events"
set frontApp to first application process whose frontmost is true
set appName to name of frontApp
set bundleId to bundle identifier of frontApp
return bundleId & "|" & appName
end tell
`],
stdout: 'pipe',
stderr: 'pipe',
})
const output = new TextDecoder().decode(result.stdout).trim()
const output = execFileSync('osascript', ['-e', `
tell application "System Events"
set frontApp to first application process whose frontmost is true
set appName to name of frontApp
set bundleId to bundle identifier of frontApp
return bundleId & "|" & appName
end tell
`], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim()
if (!output || !output.includes('|')) return null
const [bundleId, appName] = output.split('|', 2)
return { bundleId: bundleId!, appName: appName! }

View File

@@ -0,0 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -274,4 +274,9 @@ export const screenshot: ScreenshotAPI = {
if (displayId !== undefined) args.push('-D', String(displayId))
return captureScreenToBase64(args)
},
captureWindowTarget(_titleOrHwnd: string | number): ScreenshotResult | null {
// Window capture not supported on macOS via this backend
return null
},
}

View File

@@ -275,4 +275,9 @@ export const screenshot: ScreenshotAPI = {
return { base64: '', width: 0, height: 0 }
}
},
captureWindowTarget(_titleOrHwnd: string | number): ScreenshotResult | null {
// Window capture not supported on Linux via this backend
return null
},
}

View File

@@ -76,6 +76,7 @@ export interface ScreenshotAPI {
x: number, y: number, w: number, h: number,
outW: number, outH: number, quality: number, displayId?: number,
): Promise<ScreenshotResult>
captureWindowTarget(titleOrHwnd: string | number): ScreenshotResult | null
}
export interface SwiftBackend {

View File

@@ -0,0 +1,5 @@
{
"extends": "../../../tsconfig.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,176 @@
# Chapter 1: Getting Started
## Installation
`@anthropic/ink` is a workspace package. It is consumed internally and not published to npm.
```json
{
"dependencies": {
"@anthropic/ink": "workspace:*"
}
}
```
### Peer Dependencies
- `react` ^19.2.4
- `react-reconciler` ^0.33.0
### Key Dependencies
| Package | Purpose |
|---------|---------|
| `chalk` | ANSI color generation |
| `cli-boxes` | Border style definitions |
| `get-east-asian-width` | CJK character width measurement |
| `wrap-ansi` | ANSI-aware word wrapping |
| `bidi-js` | Bidirectional text support |
| `lodash-es` | Utility functions (throttle, noop) |
| `signal-exit` | Process exit handler cleanup |
| `emoji-regex` | Emoji width handling |
## Basic Rendering
### `render(node, options?)`
The primary entry point. Renders a React element tree to the terminal.
```tsx
import { render } from '@anthropic/ink'
import { Box, Text } from '@anthropic/ink'
const { unmount, rerender, waitUntilExit } = await render(
<Box>
<Text>Hello, World!</Text>
</Box>
)
```
**Parameters:**
- `node` -- `ReactNode` to render
- `options` -- `RenderOptions | NodeJS.WriteStream` (optional)
**Returns:** `Promise<Instance>` with:
- `rerender(node)` -- Replace the root node
- `unmount()` -- Unmount and clean up
- `waitUntilExit()` -- `Promise<void>` that resolves on unmount
- `cleanup()` -- Remove from instance registry
### `renderSync(node, options?)`
Synchronous version of render. Same API, returns `Instance` directly (no Promise).
```tsx
import { renderSync } from '@anthropic/ink'
const instance = renderSync(<App />)
// instance.rerender, instance.unmount, etc.
```
### `createRoot(options?)`
Creates a managed Ink root without immediately rendering. Similar to `react-dom`'s `createRoot`.
```tsx
import { createRoot } from '@anthropic/ink'
const root = await createRoot({ exitOnCtrlC: false })
// Later, render into it
root.render(<App />)
// You can re-render into the same root
root.render(<DifferentApp />)
// Clean up
root.unmount()
```
**Returns:** `Promise<Root>` with:
- `render(node)` -- Mount or update the tree
- `unmount()` -- Unmount
- `waitUntilExit()` -- `Promise<void>`
## RenderOptions
```ts
type RenderOptions = {
/** Output stream. Default: process.stdout */
stdout?: NodeJS.WriteStream
/** Input stream. Default: process.stdin */
stdin?: NodeJS.ReadStream
/** Error stream. Default: process.stderr */
stderr?: NodeJS.WriteStream
/** Handle Ctrl+C to exit. Default: true */
exitOnCtrlC?: boolean
/** Patch console methods to prevent Ink output mixing. Default: true */
patchConsole?: boolean
/** Called after each frame render with timing info. */
onFrame?: (event: FrameEvent) => void
}
```
## Basic Concepts
### Component Tree
Ink renders React components to a terminal using a custom reconciler. The tree structure maps to terminal output:
```tsx
<Box flexDirection="column">
<Text bold color="green">Header</Text>
<Box flexDirection="row" gap={1}>
<Text>Left</Text>
<Text>Right</Text>
</Box>
</Box>
```
This produces terminal output with Flexbox layout (via Yoga).
### Rendering Pipeline
1. **React Reconciler** -- Standard React reconciliation; diffs virtual tree
2. **Yoga Layout** -- Computes Flexbox positions/ sizes for every node
3. **Render to Output** -- Walks the DOM tree, emits styled text into an `Output` buffer
4. **Screen Diff** -- Compares new frame against previous frame in a screen buffer
5. **Terminal Write** -- Emits minimal ANSI escape sequences to update only changed cells
### Module System
Import everything from the package root:
```tsx
// Core rendering
import { render, createRoot, renderSync } from '@anthropic/ink'
// Components (base, no theme)
import { BaseBox, BaseText, ScrollBox, Button, Link, Newline, Spacer } from '@anthropic/ink'
// Theme-aware components (recommended)
import { Box, Text } from '@anthropic/ink'
// Hooks
import { useApp, useInput, useTerminalSize, useInterval } from '@anthropic/ink'
// Theme
import { ThemeProvider, useTheme, color } from '@anthropic/ink'
// Keybindings
import { useKeybinding, KeybindingProvider } from '@anthropic/ink'
```
### Naming Convention: Base vs Theme-aware
The package exports both raw and theme-aware versions of core components:
- **`BaseBox`** / **`BaseText`** -- Raw components that only accept raw color values (`rgb(...)`, `#hex`, `ansi:...`, `ansi256(...)`)
- **`Box`** / **`Text`** -- Theme-aware wrappers that accept both theme keys (`'claude'`, `'success'`, `'error'`) and raw color values
Always prefer the theme-aware versions unless you have a specific reason to use raw components.

View File

@@ -0,0 +1,348 @@
# Chapter 2: Layout System
Ink uses [Yoga](https://yogalayout.com/) (Facebook's cross-platform layout engine) to implement CSS Flexbox in the terminal. Every layout is flexbox-based -- there is no CSS Grid or flow layout.
## Box Component
`Box` is the fundamental layout primitive. It is the terminal equivalent of `<div style="display: flex">`.
```tsx
import { Box, Text } from '@anthropic/ink'
<Box flexDirection="row" gap={1}>
<Text>Left</Text>
<Text>Right</Text>
</Box>
```
### Box Props (Styles)
All layout props are passed directly as JSX props (no `style={}` wrapper needed):
#### Flex Direction
Controls the main axis direction.
```tsx
<Box flexDirection="row">...</Box> // Left to right (default)
<Box flexDirection="column">...</Box> // Top to bottom
<Box flexDirection="row-reverse">...</Box> // Right to left
<Box flexDirection="column-reverse">...</Box> // Bottom to top
```
#### Flex Grow / Shrink / Basis
```tsx
<Box flexGrow={1}>...</Box> // Grow to fill available space
<Box flexShrink={0}>...</Box> // Don't shrink below intrinsic size
<Box flexBasis={20}>...</Box> // Initial size before flex distribution
<Box flexBasis="50%">...</Box> // Percentage basis
```
Default values: `flexGrow={0}`, `flexShrink={1}`, `flexBasis=auto`.
#### Flex Wrap
```tsx
<Box flexWrap="nowrap">...</Box> // Single line (default)
<Box flexWrap="wrap">...</Box> // Multiple lines
<Box flexWrap="wrap-reverse">...</Box> // Reverse cross-axis stacking
```
#### Alignment
```tsx
<Box alignItems="flex-start">...</Box> // Cross-axis start
<Box alignItems="center">...</Box> // Cross-axis center
<Box alignItems="flex-end">...</Box> // Cross-axis end
<Box alignItems="stretch">...</Box> // Stretch to fill (default)
<Box alignSelf="flex-start">...</Box> // Override parent's alignItems
<Box alignSelf="center">...</Box>
<Box alignSelf="flex-end">...</Box>
<Box alignSelf="auto">...</Box> // Inherit from parent
```
#### Justify Content
```tsx
<Box justifyContent="flex-start">...</Box> // Main-axis start (default)
<Box justifyContent="flex-end">...</Box> // Main-axis end
<Box justifyContent="center">...</Box> // Center
<Box justifyContent="space-between">...</Box> // Equal gaps, no edges
<Box justifyContent="space-around">...</Box> // Equal gaps with edges
<Box justifyContent="space-evenly">...</Box> // Evenly distributed
```
#### Gap
Spacing between children (only accepts integers):
```tsx
<Box gap={1}>...</Box> // Both row and column gap
<Box columnGap={2}>...</Box> // Gap between columns only
<Box rowGap={1}>...</Box> // Gap between rows only
```
#### Padding
Inner spacing (only accepts integers):
```tsx
<Box padding={1}>...</Box> // All sides
<Box paddingX={2}>...</Box> // Left and right
<Box paddingY={1}>...</Box> // Top and bottom
<Box paddingLeft={2}>...</Box> // Left only
<Box paddingRight={2}>...</Box> // Right only
<Box paddingTop={1}>...</Box> // Top only
<Box paddingBottom={1}>...</Box> // Bottom only
```
#### Margin
Outer spacing (only accepts integers):
```tsx
<Box margin={1}>...</Box> // All sides
<Box marginX={2}>...</Box> // Left and right
<Box marginY={1}>...</Box> // Top and bottom
<Box marginLeft={2}>...</Box> // Left only
<Box marginRight={2}>...</Box> // Right only
<Box marginTop={1}>...</Box> // Top only
<Box marginBottom={1}>...</Box> // Bottom only
```
> **Note:** Fractional values for padding, margin, and gap are not supported. Ink will emit warnings if non-integer values are used.
#### Width & Height
```tsx
<Box width={40}>...</Box> // Fixed 40 characters wide
<Box height={10}>...</Box> // Fixed 10 rows tall
<Box width="50%">...</Box> // 50% of parent's width
<Box width="100%">...</Box> // Full parent width
```
#### Min/Max Dimensions
```tsx
<Box minWidth={20}>...</Box>
<Box maxWidth={80}>...</Box>
<Box minHeight={5}>...</Box>
<Box maxHeight={20}>...</Box>
```
Percentage values are supported: `minWidth="30%"`.
#### Position
```tsx
<Box position="absolute" top={0} right={0}>...</Box>
<Box position="absolute" top="10%" left="20%">...</Box>
<Box position="relative">...</Box> // Default
```
Position `absolute` removes the element from normal flow and positions it relative to its nearest positioned ancestor. Useful for overlays.
#### Display
```tsx
<Box display="flex">...</Box> // Visible (default)
<Box display="none">...</Box> // Hidden (removed from layout)
```
#### Border
```tsx
<Box borderStyle="single">...</Box> // Thin border
<Box borderStyle="double">...</Box> // Double-line border
<Box borderStyle="round">...</Box> // Rounded corners
<Box borderStyle="bold">...</Box> // Bold border
<Box borderStyle="singleDouble">...</Box> // Mixed
<Box borderStyle="doubleSingle">...</Box> // Mixed
<Box borderStyle="classic">...</Box> // ASCII art border
```
Control individual sides and colors:
```tsx
<Box
borderStyle="single"
borderTop={false} // Hide top border
borderBottom={true} // Show bottom border
borderColor="rgb(255,0,0)" // Red border
borderDimColor={true} // Dim the border
>
...
</Box>
```
Per-side colors:
```tsx
<Box
borderStyle="single"
borderTopColor="rgb(255,0,0)"
borderBottomColor="ansi:green"
borderLeftColor="#0000FF"
borderRightColor="ansi256(200)"
/>
```
Border text (labels in the border):
```tsx
<Box
borderStyle="round"
borderText={{ title: "My Panel", align: "left" }}
/>
```
#### Background
```tsx
<Box backgroundColor="rgb(40,40,40)">...</Box>
```
#### Overflow
```tsx
<Box overflow="visible">...</Box> // Content expands container (default)
<Box overflow="hidden">...</Box> // Clip without scrolling
<Box overflow="scroll">...</Box> // Enable scrolling (use ScrollBox)
```
`overflowX` and `overflowY` control each axis independently.
#### Opaque
```tsx
<Box opaque={true}>...</Box>
```
Fills the box interior with spaces (using terminal's default background) before rendering children. Useful for absolute-positioned overlays where gaps would otherwise be transparent.
#### NoSelect
```tsx
<Box noSelect={true}>...</Box> // Exclude from text selection
<Box noSelect="from-left-edge">...</Box> // Exclude from column 0 to box edge
```
Only affects alt-screen text selection. Useful for gutters (line numbers, diff markers).
## Spacer
`Spacer` fills all available space along the main axis (equivalent to `flexGrow: 1`).
```tsx
<Box flexDirection="row">
<Text>Left</Text>
<Spacer />
<Text>Right</Text>
</Box>
```
## Newline
Inserts line breaks.
```tsx
<Text>
Line 1
<Newline />
Line 2
<Newline count={2} />
Line 4 (after double break)
</Text>
```
## Layout Examples
### Two-column layout
```tsx
<Box flexDirection="row" width={80}>
<Box width="50%" padding={1}>
<Text>Left column</Text>
</Box>
<Box width="50%" padding={1}>
<Text>Right column</Text>
</Box>
</Box>
```
### Centered content
```tsx
<Box justifyContent="center" alignItems="center" height={20}>
<Text>Centered!</Text>
</Box>
```
### Sticky footer
```tsx
<Box flexDirection="column" height={24}>
<Box flexGrow={1}>
<Text>Scrollable content area</Text>
</Box>
<Box>
<Text>Status bar at bottom</Text>
</Box>
</Box>
```
### Bordered panel with title
```tsx
<Box
flexDirection="column"
borderStyle="round"
borderColor="rgb(87,105,247)"
padding={1}
width={60}
>
<Text bold>Panel Title</Text>
<Text>Panel content goes here.</Text>
</Box>
```
## NoSelect
Wraps a region to exclude it from text selection in alt-screen mode. A convenience wrapper around `Box` with `noSelect` set.
```tsx
import { NoSelect } from '@anthropic/ink'
<Box flexDirection="row">
<NoSelect>
<Text dimColor>1 </Text>
</NoSelect>
<Text>selectable code here</Text>
</Box>
```
### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | - | Content |
| `fromLeftEdge` | `boolean` | `false` | Extend exclusion from column 0 to box's right edge |
Accepts all `BoxProps` except `noSelect`.
## BaseBox vs ThemedBox
Two versions of Box are exported:
- **`BaseBox`** (imported as `BaseBox`) -- Raw box, color props accept only raw `Color` values
- **`Box`** (themed, imported as `Box`) -- Theme-aware, color props accept `keyof Theme | Color`
```tsx
// Raw
<BaseBox borderStyle="single" borderColor="rgb(255,0,0)" />
// Theme-aware (resolves 'permission' to the current theme's blue)
<Box borderStyle="single" borderColor="permission" />
```

View File

@@ -0,0 +1,238 @@
# Chapter 3: Text & Styling
## Text Component
`Text` renders styled text content. It supports colors, emphasis, and text wrapping.
```tsx
import { Text } from '@anthropic/ink'
<Text bold color="success">Operation complete</Text>
```
### Text Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `color` | `keyof Theme \| Color` | - | Foreground color |
| `backgroundColor` | `keyof Theme` | - | Background color (theme-aware) |
| `bold` | `boolean` | `false` | Bold text |
| `dimColor` | `boolean` | `false` | Dim text (uses theme's `inactive` color) |
| `italic` | `boolean` | `false` | Italic text |
| `underline` | `boolean` | `false` | Underlined text |
| `strikethrough` | `boolean` | `false` | Strikethrough text |
| `inverse` | `boolean` | `false` | Swap foreground/background |
| `wrap` | `TextWrap` | `'wrap'` | Wrapping/truncation mode |
| `children` | `ReactNode` | - | Text content |
> **Note:** `bold` and `dimColor` are mutually exclusive (ANSI terminals cannot render both simultaneously).
### BaseText vs ThemedText
- **`BaseText`** -- Accepts raw `Color` values only
- **`Text`** (default export) -- Theme-aware, accepts `keyof Theme | Color` for `color`, and `keyof Theme` for `backgroundColor`
```tsx
// Raw color
<BaseText color="rgb(255,0,0)">Red text</BaseText>
// Theme key (resolved to current theme palette)
<Text color="error">Error message</Text>
// Mixed
<Text color="#FF0000">Custom red</Text>
```
### Text Wrap Modes
```tsx
<Text wrap="wrap">...</Text> // Word-wrap at container width (default)
<Text wrap="wrap-trim">...</Text> // Wrap + trim trailing whitespace
<Text wrap="end">...</Text> // Truncate with "..." at end
<Text wrap="truncate-end">...</Text> // Same as "end"
<Text wrap="truncate">...</Text> // Truncate (no ellipsis)
<Text wrap="middle">...</Text> // "start...end"
<Text wrap="truncate-middle">...</Text> // Same as "middle"
<Text wrap="truncate-start">...</Text> // "...text"
```
### TextHoverColorContext
Uncolored `Text` children inherit a hover color from context:
```tsx
import { TextHoverColorContext } from '@anthropic/ink'
<TextHoverColorContext.Provider value="suggestion">
<Text>Uncolored text gets the suggestion color</Text>
<Text color="error">This stays red</Text>
</TextHoverColorContext.Provider>
```
Precedence: explicit `color` > `TextHoverColorContext` > `dimColor`.
## Color System
### Raw Color Formats
Four formats are supported for raw color values:
```tsx
// RGB
<Text color="rgb(255,107,128)">Bright red</Text>
// Hex
<Text color="#FF6B80">Bright red</Text>
// ANSI 256-color
<Text color="ansi256(196)">Red from 256-color palette</Text>
// Named ANSI 16-color
<Text color="ansi:red">Red</Text>
<Text color="ansi:greenBright">Bright green</Text>
```
### ANSI Named Colors
Full list of `ansi:` prefixed names:
| Name | Color |
|------|-------|
| `ansi:black` | Black |
| `ansi:red` | Red |
| `ansi:green` | Green |
| `ansi:yellow` | Yellow |
| `ansi:blue` | Blue |
| `ansi:magenta` | Magenta |
| `ansi:cyan` | Cyan |
| `ansi:white` | White |
| `ansi:blackBright` | Dark gray |
| `ansi:redBright` | Bright red |
| `ansi:greenBright` | Bright green |
| `ansi:yellowBright` | Bright yellow |
| `ansi:blueBright` | Bright blue |
| `ansi:magentaBright` | Bright magenta |
| `ansi:cyanBright` | Bright cyan |
| `ansi:whiteBright` | Bright white |
## Utility Functions
### `color(colorValue, themeName, type?)`
Curried theme-aware color function. Resolves theme keys to raw color values.
```tsx
import { color } from '@anthropic/ink'
const paint = color('error', 'dark') // Returns (text: string) => string
console.log(paint('failed')) // 'failed' wrapped in ANSI red codes
const paintFg = color('rgb(255,0,0)', 'dark', 'foreground')
const paintBg = color('success', 'dark', 'background')
```
Parameters:
- `c` -- `keyof Theme | Color | undefined` -- Theme key or raw color
- `theme` -- `ThemeName` -- Current theme
- `type` -- `'foreground' | 'background'` (default `'foreground'`)
### `stringWidth(text)`
Measures the visual width of a string in terminal columns, accounting for:
- CJK characters (2 columns each)
- Emoji (2 columns each)
- ANSI escape sequences (0 columns)
```tsx
import { stringWidth } from '@anthropic/ink'
stringWidth('hello') // 5
stringWidth('你好') // 4
stringWidth('\x1b[31mhi') // 2 (ANSI codes ignored)
```
### `wrapText(text, width, textWrap)`
Wraps text to a given width with the specified wrapping mode.
```tsx
import { wrapText } from '@anthropic/ink'
wrapText('Hello World', 5, 'wrap') // 'Hello\nWorld'
wrapText('Hello World', 8, 'end') // 'Hello...'
```
### `wrapAnsi(text, width)`
Wraps text containing ANSI escape codes while preserving styling.
```tsx
import { wrapAnsi } from '@anthropic/ink'
wrapAnsi('\x1b[31mHello World\x1b[0m', 5)
// Wraps at word boundaries, keeps color codes intact
```
### `measureElement(node)`
Measures a rendered DOM element's dimensions.
```tsx
import { measureElement } from '@anthropic/ink'
const { width, height } = measureElement(domElement)
```
## Link Component
Renders an OSC 8 terminal hyperlink (clickable URL in supported terminals).
```tsx
import { Link } from '@anthropic/ink'
<Link url="https://example.com">
<Text underline color="suggestion">example.com</Text>
</Link>
```
Props:
- `url` -- `string` (required) -- Target URL
- `children` -- `ReactNode` -- Display content
- `fallback` -- `ReactNode` -- Shown when hyperlinks are unsupported
## RawAnsi Component
Renders pre-formatted ANSI strings directly into the layout.
```tsx
import { RawAnsi } from '@anthropic/ink'
<RawAnsi
lines={['\x1b[31mRed line 1\x1b[0m', '\x1b[32mGreen line 2\x1b[0m']}
width={40}
/>
```
Props:
- `lines` -- `string[]` -- Pre-rendered ANSI lines (one terminal row each)
- `width` -- `number` -- Column width the producer wrapped to
## Border Rendering
### `renderBorder(box, output, options?)`
Low-level border rendering function used internally by Box.
```tsx
import { renderBorder } from '@anthropic/ink'
import type { BorderTextOptions } from '@anthropic/ink'
```
Border styles available (from `cli-boxes`):
- `single` -- Thin lines `─│┌┐└┘`
- `double` -- Double lines `═║╔╗╚╝`
- `round` -- Rounded corners `─│╭╮╰╯`
- `bold` -- Bold lines `━┃┏┓┗┛`
- `singleDouble` -- Single horizontal, double vertical
- `doubleSingle` -- Double horizontal, single vertical
- `classic` -- ASCII `─|++++`

View File

@@ -0,0 +1,213 @@
# Chapter 4: Theme System
The theme system provides consistent, accessible color palettes across the application. It supports dark mode, light mode, ANSI-only terminals, and colorblind-accessible variants.
## ThemeProvider
Wraps the application to provide theme context.
```tsx
import { ThemeProvider } from '@anthropic/ink'
function App() {
return (
<ThemeProvider initialState="dark" onThemeSave={(setting) => saveConfig(setting)}>
<MyComponent />
</ThemeProvider>
)
}
```
### Props
| Prop | Type | Description |
|------|------|-------------|
| `children` | `ReactNode` | Child components |
| `initialState` | `ThemeSetting` | Initial theme (default: loads from config) |
| `onThemeSave` | `(setting: ThemeSetting) => void` | Called when theme is saved |
### Theme Configuration Injection
Before mounting, inject config persistence callbacks:
```tsx
import { setThemeConfigCallbacks } from '@anthropic/ink'
setThemeConfigCallbacks({
loadTheme: () => configStore.get('theme', 'dark'),
saveTheme: (setting) => configStore.set('theme', setting),
})
```
## Theme Settings
```ts
type ThemeSetting = 'auto' | 'dark' | 'light' | 'light-daltonized' | 'dark-daltonized' | 'light-ansi' | 'dark-ansi'
type ThemeName = 'dark' | 'light' | 'light-daltonized' | 'dark-daltonized' | 'light-ansi' | 'dark-ansi'
```
| Theme | Description |
|-------|-------------|
| `dark` | Dark theme with RGB colors (default) |
| `light` | Light theme with RGB colors |
| `dark-daltonized` | Colorblind-accessible dark theme |
| `light-daltonized` | Colorblind-accessible light theme |
| `dark-ansi` | Dark theme using only 16 ANSI colors |
| `light-ansi` | Light theme using only 16 ANSI colors |
| `auto` | Follows terminal's dark/light mode (resolved at runtime) |
## Theme Hooks
### `useTheme()`
Returns the resolved theme name and setter.
```tsx
const [currentTheme, setTheme] = useTheme()
// currentTheme: ThemeName (never 'auto')
// setTheme: (setting: ThemeSetting) => void
```
### `useThemeSetting()`
Returns the raw setting (may be `'auto'`).
```tsx
const setting = useThemeSetting() // 'auto' | 'dark' | ...
```
### `usePreviewTheme()`
Returns preview controls for a theme picker UI.
```tsx
const { setPreviewTheme, savePreview, cancelPreview } = usePreviewTheme()
// Show preview
setPreviewTheme('light')
// User confirms
savePreview()
// User cancels
cancelPreview()
```
## Theme Color Palette
Every theme defines these semantic color keys:
### Brand & Identity
| Key | Purpose |
|-----|---------|
| `claude` | Brand orange |
| `claudeShimmer` | Lighter brand orange (animated) |
| `permission` | Permission/blue |
| `permissionShimmer` | Lighter permission blue |
| `autoAccept` | Electric violet |
| `planMode` | Teal/sage |
| `ide` | Muted blue |
### Semantic Colors
| Key | Purpose |
|-----|---------|
| `text` | Primary text color |
| `inverseText` | Text on inverse backgrounds |
| `inactive` | Dimmed/disabled elements |
| `inactiveShimmer` | Lighter inactive |
| `subtle` | Very subtle text |
| `suggestion` | Interactive/accent |
| `background` | General background accent |
| `success` | Positive/success |
| `error` | Negative/error |
| `warning` | Caution/warning |
| `warningShimmer` | Lighter warning |
| `merged` | Merged state |
### Diff Colors
| Key | Purpose |
|-----|---------|
| `diffAdded` | Added lines background |
| `diffRemoved` | Removed lines background |
| `diffAddedDimmed` | Dimmed added |
| `diffRemovedDimmed` | Dimmed removed |
| `diffAddedWord` | Word-level added |
| `diffRemovedWord` | Word-level removed |
### UI Colors
| Key | Purpose |
|-----|---------|
| `promptBorder` | Input prompt border |
| `promptBorderShimmer` | Lighter prompt border |
| `bashBorder` | Shell block border |
| `selectionBg` | Text selection highlight background |
| `userMessageBackground` | User message background |
| `userMessageBackgroundHover` | User message hover |
| `messageActionsBackground` | Action buttons background |
### Agent Colors
| Key | Purpose |
|-----|---------|
| `red_FOR_SUBAGENTS_ONLY` | Agent color assignment |
| `blue_FOR_SUBAGENTS_ONLY` | Agent color assignment |
| `green_FOR_SUBAGENTS_ONLY` | Agent color assignment |
| `yellow_FOR_SUBAGENTS_ONLY` | Agent color assignment |
| `purple_FOR_SUBAGENTS_ONLY` | Agent color assignment |
| `orange_FOR_SUBAGENTS_ONLY` | Agent color assignment |
| `pink_FOR_SUBAGENTS_ONLY` | Agent color assignment |
| `cyan_FOR_SUBAGENTS_ONLY` | Agent color assignment |
## Using Theme Colors in Components
### ThemedText
```tsx
<Text color="success">Operation complete</Text>
<Text color="error" bold>Failed!</Text>
<Text color="claude">Claude says...</Text>
<Text dimColor>Secondary info</Text>
<Text backgroundColor="userMessageBackground">Highlighted</Text>
```
### ThemedBox
```tsx
<Box borderStyle="single" borderColor="permission" backgroundColor="userMessageBackground">
<Text>Themed content</Text>
</Box>
```
### color() Utility
```tsx
import { color, useTheme } from '@anthropic/ink'
function MyComponent() {
const [themeName] = useTheme()
const paint = color('success', themeName)
// paint('text') returns ANSI-colored string
}
```
## Daltonized Themes
The daltonized themes (`light-daltonized`, `dark-daltonized`) are designed for users with protanopia/deuteranopia:
- Green/red diffs replaced with blue/red
- Status colors use blue instead of green
- Warning colors adjusted for better distinction
- All color pairs verified for sufficient contrast
## System Theme Detection
When `ThemeSetting` is `'auto'`:
1. Seeds from `$COLORFGBG` environment variable
2. Queries terminal via OSC 11 for live background color
3. Watches for changes (terminal theme switch) in real-time
4. Resolves to `'dark'` or `'light'` based on detected brightness

View File

@@ -0,0 +1,390 @@
# Chapter 5: Design System Components
Pre-built theme-aware UI components for common terminal interface patterns.
## Dialog
Modal dialog with border, title, and keyboard navigation.
```tsx
import { Dialog } from '@anthropic/ink'
<Dialog
title="Confirm Action"
subtitle="This cannot be undone"
onCancel={() => setShowDialog(false)}
color="warning"
>
<Text>Are you sure you want to proceed?</Text>
</Dialog>
```
### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `title` | `ReactNode` | - | Dialog title (required) |
| `subtitle` | `ReactNode` | - | Optional subtitle |
| `children` | `ReactNode` | - | Dialog body content |
| `onCancel` | `() => void` | - | Called on Esc/n (required) |
| `color` | `keyof Theme` | `'permission'` | Title and border color |
| `hideInputGuide` | `boolean` | `false` | Hide the keyboard hint footer |
| `hideBorder` | `boolean` | `false` | Render without Pane border |
| `inputGuide` | `(exitState) => ReactNode` | - | Custom input guide footer |
| `isCancelActive` | `boolean` | `true` | Enable/disable cancel keybindings |
### Keyboard Shortcuts
- **Enter** -- Confirm (consumer handles this)
- **Esc / n** -- Cancel (calls `onCancel`)
- **Ctrl+C / Ctrl+D** -- Double-press to exit
### Custom Input Guide
```tsx
<Dialog
title="Save file?"
onCancel={handleCancel}
inputGuide={(exitState) => (
exitState.pending
? <Text>Press {exitState.keyName} again to exit</Text>
: <Text>Press Enter to save, Esc to cancel</Text>
)}
>
...
</Dialog>
```
## Pane
Bordered container with themed top border.
```tsx
import { Pane } from '@anthropic/ink'
<Pane color="permission">
<Text>Content inside a bordered pane</Text>
</Pane>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | - | Content |
| `color` | `keyof Theme` | `'permission'` | Top border color |
## ProgressBar
Visual progress indicator.
```tsx
import { ProgressBar } from '@anthropic/ink'
<ProgressBar
ratio={0.65}
width={40}
fillColor="rate_limit_fill"
emptyColor="rate_limit_empty"
/>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `ratio` | `number` | - | Progress 0..1 (required) |
| `width` | `number` | - | Character width (required) |
| `fillColor` | `keyof Theme` | - | Filled portion color |
| `emptyColor` | `keyof Theme` | - | Empty portion color |
## Spinner
Animated loading spinner. No props.
```tsx
import { Spinner } from '@anthropic/ink'
<Box gap={1}>
<Spinner />
<Text>Loading...</Text>
</Box>
```
## LoadingState
Loading message with spinner and optional subtitle.
```tsx
import { LoadingState } from '@anthropic/ink'
<LoadingState
message="Installing dependencies"
subtitle="This may take a moment"
bold={true}
/>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `message` | `string` | - | Loading message (required) |
| `bold` | `boolean` | `false` | Bold message |
| `dimColor` | `boolean` | `false` | Dimmed message |
| `subtitle` | `string` | - | Secondary text below |
## StatusIcon
Semantic status indicator with icon and color.
```tsx
import { StatusIcon } from '@anthropic/ink'
<StatusIcon status="success" withSpace />
<Text>Build complete</Text>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `status` | `'success' \| 'error' \| 'warning' \| 'info' \| 'pending' \| 'loading'` | - | Status type (required) |
| `withSpace` | `boolean` | `false` | Add trailing space |
Status icons:
- `success` -- Green checkmark
- `error` -- Red cross
- `warning` -- Yellow warning
- `info` -- Blue info
- `pending` -- Dimmed circle
- `loading` -- Dimmed ellipsis
## FuzzyPicker
Full-featured fuzzy search selector with preview support.
```tsx
import { FuzzyPicker } from '@anthropic/ink'
<FuzzyPicker
title="Select a file"
items={files}
getKey={(f) => f.path}
renderItem={(f, focused) => <Text>{f.name}</Text>}
onQueryChange={(q) => setFilteredFiles(filterFiles(q))}
onSelect={(f) => openFile(f)}
onCancel={() => setShowPicker(false)}
/>
```
### Props
| Prop | Type | Description |
|------|------|-------------|
| `title` | `string` | Picker title (required) |
| `items` | `readonly T[]` | Items to display (required) |
| `getKey` | `(item: T) => string` | Unique key extractor (required) |
| `renderItem` | `(item: T, isFocused: boolean) => ReactNode` | Item renderer (required) |
| `onQueryChange` | `(query: string) => void` | Filter callback (required) |
| `onSelect` | `(item: T) => void` | Enter key handler (required) |
| `onCancel` | `() => void` | Esc handler (required) |
| `renderPreview` | `(item: T) => ReactNode` | Preview panel renderer |
| `previewPosition` | `'bottom' \| 'right'` | Preview placement |
| `visibleCount` | `number` | Max visible items |
| `direction` | `'down' \| 'up'` | Item ordering |
| `onTab` | `PickerAction<T>` | Tab key handler |
| `onShiftTab` | `PickerAction<T>` | Shift+Tab handler |
| `onFocus` | `(item: T \| undefined) => void` | Focus change callback |
| `emptyMessage` | `string \| ((query: string) => string)` | Empty state message |
| `matchLabel` | `string` | Status line below list |
| `placeholder` | `string` | Input placeholder |
| `initialQuery` | `string` | Initial search query |
| `selectAction` | `string` | Action label for byline |
| `extraHints` | `ReactNode` | Additional keyboard hints |
## Tabs / Tab
Tabbed interface with keyboard navigation.
```tsx
import { Tabs, Tab } from '@anthropic/ink'
<Tabs title="Settings" color="claude">
<Tab title="General" id="general">
<GeneralSettings />
</Tab>
<Tab title="Advanced" id="advanced">
<AdvancedSettings />
</Tab>
</Tabs>
```
### Tabs Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactElement<TabProps>[]` | - | Tab elements |
| `title` | `string` | - | Header title |
| `color` | `keyof Theme` | - | Active tab indicator color |
| `defaultTab` | `string` | - | Initial tab id |
| `selectedTab` | `string` | - | Controlled selected tab |
| `onTabChange` | `(tabId: string) => void` | - | Tab change callback |
| `hidden` | `boolean` | `false` | Hide tab headers |
| `useFullWidth` | `boolean` | `false` | Use full terminal width |
| `banner` | `ReactNode` | - | Banner below tab headers |
| `disableNavigation` | `boolean` | `false` | Disable keyboard nav |
| `initialHeaderFocused` | `boolean` | `true` | Start with header focused |
| `contentHeight` | `number` | - | Fixed content height |
| `navFromContent` | `boolean` | `false` | Allow Tab/Arrow from content |
### Tab Props
| Prop | Type | Description |
|------|------|-------------|
| `title` | `string` | Tab label (required) |
| `id` | `string` | Tab identifier |
| `children` | `ReactNode` | Tab content |
### Tab Hooks
```tsx
import { useTabsWidth, useTabHeaderFocus } from '@anthropic/ink'
const width = useTabsWidth() // Available content width
const focused = useTabHeaderFocus() // Whether tab header is focused
```
## ListItem
Selectable list item with focus/selection indicators.
```tsx
import { ListItem } from '@anthropic/ink'
<ListItem isFocused={index === focusedIndex} isSelected={item.checked}>
<Text>{item.label}</Text>
</ListItem>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `isFocused` | `boolean` | - | Keyboard focus (required) |
| `isSelected` | `boolean` | `false` | Checked/active state |
| `children` | `ReactNode` | - | Content |
| `description` | `string` | - | Secondary text below |
| `styled` | `boolean` | `true` | Auto-style based on state |
| `disabled` | `boolean` | `false` | Dimmed, non-interactive |
| `showScrollDown` | `boolean` | `false` | Scroll-down hint arrow |
| `showScrollUp` | `boolean` | `false` | Scroll-up hint arrow |
| `declareCursor` | `boolean` | `true` | Declare terminal cursor |
## SearchBox
Search input with theme-aware styling.
```tsx
import { SearchBox } from '@anthropic/ink'
<SearchBox
query={searchQuery}
placeholder="Search..."
isFocused={true}
isTerminalFocused={true}
width="100%"
/>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `query` | `string` | - | Current search text |
| `placeholder` | `string` | - | Placeholder text |
| `isFocused` | `boolean` | - | Focus state |
| `isTerminalFocused` | `boolean` | - | Terminal focus state |
| `prefix` | `string` | - | Input prefix label |
| `width` | `number \| string` | - | Input width |
| `cursorOffset` | `number` | - | Cursor position offset |
| `borderless` | `boolean` | `false` | Remove border |
## Divider
Horizontal/vertical divider line.
```tsx
import { Divider } from '@anthropic/ink'
<Divider width={60} color="subtle" />
<Divider title="Section Title" />
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `width` | `number` | Terminal width | Divider width |
| `color` | `keyof Theme` | Dimmed | Line color |
| `char` | `string` | `'─'` | Line character |
| `padding` | `number` | `0` | Width reduction |
| `title` | `string` | - | Centered title text |
## Byline
Footer with middot-separated items.
```tsx
import { Byline } from '@anthropic/ink'
<Byline>
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
<KeyboardShortcutHint shortcut="Esc" action="cancel" />
</Byline>
```
## KeyboardShortcutHint
Display a keyboard shortcut with its action.
```tsx
import { KeyboardShortcutHint } from '@anthropic/ink'
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
<KeyboardShortcutHint shortcut="↑/↓" action="navigate" parens />
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `shortcut` | `string` | - | Key or chord to display |
| `action` | `string` | - | Action description |
| `parens` | `boolean` | `false` | Wrap in parentheses |
| `bold` | `boolean` | `false` | Bold shortcut text |
## ConfigurableShortcutHint
Displays a shortcut hint that reads the actual keybinding from config.
```tsx
import { ConfigurableShortcutHint } from '@anthropic/ink'
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="cancel"
/>
```
| Prop | Type | Description |
|------|------|-------------|
| `action` | `string` | Keybinding action name |
| `context` | `string` | Keybinding context |
| `fallback` | `string` | Default shortcut if unbound |
| `description` | `string` | Action description |
| `parens` | `boolean` | Wrap in parentheses |
| `bold` | `boolean` | Bold shortcut text |
## Ratchet
Animated counter component that prevents layout jumps.
```tsx
import { Ratchet } from '@anthropic/ink'
<Ratchet lock="always">
<Text>{count}</Text>
</Ratchet>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | - | Content |
| `lock` | `'always' \| 'offscreen'` | `'always'` | Width locking strategy. `'always'` locks always; `'offscreen'` only locks when the element is scrolled off-screen |

View File

@@ -0,0 +1,189 @@
# Chapter 6: Scrolling
## ScrollBox
A scrollable container with imperative scroll API, viewport culling, and sticky scroll support.
```tsx
import { ScrollBox } from '@anthropic/ink'
import type { ScrollBoxHandle } from '@anthropic/ink'
function MessageList({ messages }) {
const scrollRef = useRef<ScrollBoxHandle>(null)
// Auto-scroll to bottom on new messages
useEffect(() => {
scrollRef.current?.scrollToBottom()
}, [messages.length])
return (
<ScrollBox ref={scrollRef} stickyScroll flexDirection="column" height={20}>
{messages.map(msg => (
<Text key={msg.id}>{msg.text}</Text>
))}
</ScrollBox>
)
}
```
### Props
ScrollBox accepts all Box layout props except `textWrap`, `overflow`, `overflowX`, `overflowY` (these are managed internally):
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `ref` | `Ref<ScrollBoxHandle>` | - | Imperative handle |
| `stickyScroll` | `boolean` | `false` | Auto-follow new content |
| *(layout props)* | `Styles` | - | Width, height, padding, etc. |
### ScrollBoxHandle (Imperative API)
```ts
interface ScrollBoxHandle {
// Absolute positioning
scrollTo(y: number): void
scrollToElement(el: DOMElement, offset?: number): void
scrollToBottom(): void
// Relative positioning
scrollBy(dy: number): void
// Query state
getScrollTop(): number
getPendingDelta(): number
getScrollHeight(): number
getFreshScrollHeight(): number
getViewportHeight(): number
getViewportTop(): number
isSticky(): boolean
// Events
subscribe(listener: () => void): () => void
// Virtual scroll support
setClampBounds(min?: number, max?: number): void
}
```
### Method Details
#### `scrollTo(y)`
Jump to an absolute position. Breaks sticky scroll.
```tsx
scrollRef.current?.scrollTo(0) // Scroll to top
```
#### `scrollBy(dy)`
Scroll by a relative amount. Accumulates deltas for smooth scrolling.
```tsx
scrollRef.current?.scrollBy(3) // Scroll down 3 rows
scrollRef.current?.scrollBy(-5) // Scroll up 5 rows
```
#### `scrollToElement(el, offset?)`
Scroll so a specific DOM element is at the viewport top. More reliable than `scrollTo` because it reads the element's position at render time (avoids stale layout values).
```tsx
const elementRef = useRef<DOMElement>(null)
scrollRef.current?.scrollToElement(elementRef.current!, 2)
```
#### `scrollToBottom()`
Pin scroll to bottom. Enables sticky mode.
```tsx
scrollRef.current?.scrollToBottom()
```
#### `isSticky()`
Returns `true` when scroll is pinned to the bottom.
```tsx
if (scrollRef.current?.isSticky()) {
// User hasn't scrolled up
}
```
#### `subscribe(listener)`
Subscribe to imperative scroll changes. Returns unsubscribe function.
```tsx
useEffect(() => {
return scrollRef.current?.subscribe(() => {
console.log('Scroll position changed')
})
}, [])
```
### Sticky Scroll
When `stickyScroll` is enabled:
1. Scroll automatically follows new content at the bottom
2. User scroll (via `scrollBy`/`scrollTo`) breaks stickiness
3. `scrollToBottom()` re-enables stickiness
4. Content growth at the bottom is detected and followed automatically
```tsx
<ScrollBox stickyScroll height={20}>
{/* New items auto-scroll to bottom */}
{items.map(renderItem)}
</ScrollBox>
```
### Viewport Culling
ScrollBox only renders children that intersect the visible viewport. Children outside the viewport are still mounted in React but skipped during terminal rendering. This makes large lists performant.
### Virtual Scrolling
For very large lists, use `setClampBounds` in combination with a virtual scrolling hook:
```tsx
const scrollRef = useRef<ScrollBoxHandle>(null)
// After computing visible range
scrollRef.current?.setClampBounds(firstVisibleRow, lastVisibleRow)
```
This prevents burst `scrollTo` calls from showing blank space beyond mounted content.
### Scroll Events
ScrollBox bypasses React state for scroll operations. Instead:
1. `scrollTo`/`scrollBy` mutate `scrollTop` directly on the DOM node
2. The node is marked dirty
3. A microtask-deferred render fires to coalesce multiple scroll events
4. The Ink renderer reads `scrollTop` during layout
This avoids React reconciler overhead per wheel event.
### Integration with Mouse Wheel
In alt-screen mode, mouse wheel events are captured by the `App` component and forwarded to the focused ScrollBox:
```
Wheel event → App.handleMouseEvent → ScrollBox.scrollBy(delta)
```
### Layout Structure
ScrollBox creates a two-level DOM structure:
```
ink-box (overflow: scroll, constrained height)
└── Box (flexGrow: 1, flexShrink: 0, width: 100%)
├── Child 1
├── Child 2
└── ...
```
The outer `ink-box` is the viewport with constrained size. The inner `Box` grows to fit all content. The renderer computes `scrollHeight` from the inner box and translates content by `-scrollTop`.

View File

@@ -0,0 +1,267 @@
# Chapter 7: User Input
## useInput
The primary hook for handling keyboard input.
```tsx
import { useInput } from '@anthropic/ink'
function MyComponent() {
useInput((input, key, event) => {
if (input === 'q') {
// 'q' key pressed
}
if (key.leftArrow) {
// Left arrow
}
if (key.ctrl && input === 'c') {
// Ctrl+C (only if exitOnCtrlC is false)
}
if (key.meta && input === 'b') {
// Alt+B (Option+B on Mac)
}
if (key.shift && input === 'Tab') {
// Shift+Tab
}
})
return <Text>Press keys...</Text>
}
```
### Signature
```ts
function useInput(
handler: (input: string, key: Key, event: InputEvent) => void,
options?: { isActive?: boolean }
): void
```
### Parameters
- **`input`** (`string`) -- The character entered. Empty string for non-printable keys (arrows, function keys). For paste events, the entire pasted text.
- **`key`** (`Key`) -- Parsed key metadata (see below)
- **`event`** (`InputEvent`) -- Raw event with `stopImmediatePropagation()`
### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `isActive` | `boolean` | `true` | Enable/disable input handling |
### Key Object
```ts
type Key = {
upArrow: boolean
downArrow: boolean
leftArrow: boolean
rightArrow: boolean
pageDown: boolean
pageUp: boolean
wheelUp: boolean // Mouse wheel in alt-screen
wheelDown: boolean // Mouse wheel in alt-screen
home: boolean
end: boolean
return: boolean
escape: boolean
ctrl: boolean
shift: boolean
fn: boolean
tab: boolean
backspace: boolean
delete: boolean
meta: boolean // Alt / Option
super: boolean // Cmd (macOS) / Win key
}
```
### Event Propagation
Multiple `useInput` handlers form a chain. Call `event.stopImmediatePropagation()` to prevent downstream handlers from receiving the event:
```tsx
useInput((input, key, event) => {
if (input === 'j') {
// Consumed by this handler
event.stopImmediatePropagation()
}
// Other handlers won't see 'j'
})
useInput((input, key) => {
// This won't fire for 'j'
})
```
### Raw Mode
`useInput` automatically enables raw mode on stdin when active. Raw mode is reference-counted -- it stays enabled as long as any hook has `isActive: true`.
In raw mode:
- Keystrokes don't echo
- Ctrl+C is not sent as signal (app must handle it)
- Line buffering is disabled
## InputEvent
```ts
class InputEvent extends Event {
readonly input: string
readonly key: Key
readonly keypress: ParsedKey // Raw parsed keypress data
}
```
## KeyboardEvent
DOM-like keyboard event dispatched to focused elements:
```ts
class KeyboardEvent extends Event {
readonly key: Key
}
```
Used with `Box`'s `onKeyDown` and `onKeyDownCapture` props:
```tsx
<Box
tabIndex={0}
autoFocus
onKeyDown={(event) => {
if (event.key.return) {
handleSubmit()
}
}}
>
<Text>Press Enter to submit</Text>
</Box>
```
## Key Parsing
Ink supports multiple keyboard protocols:
### Standard Escape Sequences
- Arrow keys, function keys, Home/End, Page Up/Down
- Ctrl+letter combinations
- Shift, Alt, Meta modifiers
### Kitty Keyboard Protocol (CSI u)
Extended key reporting with full modifier support:
- Distinguishes Ctrl+Shift+A from Ctrl+A
- Reports Super (Cmd/Win) key
- Sends key release events
### xterm modifyOtherKeys
Alternative extended key reporting for xterm-compatible terminals.
### Application Keypad Mode
Numpad keys mapped to their digit characters.
## Paste Detection
When `Bracketed Paste` mode is enabled (DECSET 2004), pasted text is delivered as a single `InputEvent` with the full text in `input`. This distinguishes paste from rapid typing:
```tsx
useInput((input, key, event) => {
if (event.keypress.paste) {
// User pasted text -- handle as a batch
handlePaste(input)
} else {
// Regular keypress
handleKey(input, key)
}
})
```
## Mouse Events (Alt-Screen Only)
In alternate screen mode, mouse events are parsed and dispatched:
### Click Events
```tsx
<Box
onClick={(event) => {
console.log(`Clicked at (${event.x}, ${event.y})`)
event.stopImmediatePropagation()
}}
>
<Text>Click me</Text>
</Box>
```
### Hover Events
```tsx
<Box
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<Text>{hovered ? 'Hovered!' : 'Hover me'}</Text>
</Box>
```
Hover events use `mouseenter`/`mouseleave` semantics (no bubbling between children).
### Wheel Events
Mouse wheel events arrive as `Key.wheelUp`/`Key.wheelDown`:
```tsx
useInput((input, key) => {
if (key.wheelUp) scrollUp()
if (key.wheelDown) scrollDown()
})
```
## useStdin
Lower-level access to the stdin stream.
```tsx
import { useStdin } from '@anthropic/ink'
const {
stdin, // Raw stdin stream
setRawMode, // (enabled: boolean) => void
isRawModeSupported, // boolean
internal_exitOnCtrlC, // boolean
internal_eventEmitter, // EventEmitter | undefined
internal_querier, // Terminal querier
} = useStdin()
```
> **Prefer `useInput` for keyboard handling.** `useStdin` is for advanced use cases like terminal querying or custom event handling.
## Button Component
Interactive button that responds to keyboard and mouse:
```tsx
import { Button } from '@anthropic/ink'
<Button onAction={() => handleClick()} tabIndex={0} autoFocus>
{(state) => (
<Text bold={state.focused} color={state.focused ? 'claude' : 'text'}>
{state.focused ? '> Click Me' : ' Click Me'}
</Text>
)}
</Button>
```
Button receives a render prop with state:
```ts
type ButtonState = {
focused: boolean // Has keyboard focus
hovered: boolean // Mouse is over it (alt-screen)
active: boolean // True for 100ms after activation (flash effect)
}
```
Activation triggers: Enter key, Space key, or mouse click.

View File

@@ -0,0 +1,302 @@
# Chapter 8: Keybinding System
The keybinding system provides configurable, context-aware keyboard shortcuts with chord sequence support.
## Architecture
```
KeybindingSetup (loads config)
└── KeybindingProvider (provides context)
├── useKeybinding(action, handler)
├── useKeybindings({ action: handler })
├── useKeybindingContext()
└── useRegisterKeybindingContext(name, isActive)
```
## KeybindingSetup
Loads and validates keybinding configuration at app startup.
```tsx
import { KeybindingSetup } from '@anthropic/ink'
<KeybindingSetup
loadBindings={() => parseUserKeybindings(configFile)}
subscribeToChanges={(cb) => watchConfigFile(cb)}
onWarnings={(warnings, isReload) => {
warnings.forEach(w => console.warn(w.message))
}}
>
<App />
</KeybindingSetup>
```
### Props
| Prop | Type | Description |
|------|------|-------------|
| `children` | `ReactNode` | App tree |
| `loadBindings` | `() => KeybindingsLoadResult` | Load bindings from config |
| `subscribeToChanges` | `(cb) => unsubscribe` | Watch for config changes |
| `initWatcher` | `() => void \| Promise<void>` | One-time setup (optional) |
| `onWarnings` | `(warnings, isReload) => void` | Validation warnings (optional) |
| `onDebugLog` | `(message) => void` | Debug logging (optional) |
### KeybindingsLoadResult
```ts
type KeybindingsLoadResult = {
bindings: ParsedBinding[]
warnings: KeybindingWarning[]
}
```
### KeybindingWarning
```ts
type KeybindingWarning = {
type: 'parse_error' | 'duplicate' | 'reserved' | 'invalid_context' | 'invalid_action'
severity: 'error' | 'warning'
message: string
key?: string
context?: string
action?: string
suggestion?: string
}
```
## KeybindingProvider
Context provider that holds binding state and resolution logic. Automatically provided by `KeybindingSetup`.
## useKeybinding
Register a handler for a keybinding action.
```tsx
import { useKeybinding } from '@anthropic/ink'
function MyComponent() {
useKeybinding('app:toggleTodos', () => {
setShowTodos(prev => !prev)
}, { context: 'Global' })
// Return false to NOT consume the event (allow propagation)
useKeybinding('scroll:lineDown', () => {
if (!hasContent) return false // Don't consume
scrollBy(1)
})
}
```
### Signature
```ts
function useKeybinding(
action: string,
handler: () => void | false | Promise<void>,
options?: { context?: string; isActive?: boolean }
): void
```
### Handler Return Values
| Return | Effect |
|--------|--------|
| `undefined` / `void` | Event consumed, stop propagation |
| `false` | Event NOT consumed, propagate to other handlers |
| `Promise<void>` | Async handler, treated as consumed |
## useKeybindings
Register multiple handlers in one hook (reduces `useInput` overhead).
```tsx
import { useKeybindings } from '@anthropic/ink'
useKeybindings({
'chat:submit': () => handleSubmit(),
'chat:cancel': () => handleCancel(),
'scroll:pageDown': () => {
scrollBy(viewportHeight)
},
'scroll:lineDown': () => {
if (!hasContent) return false
scrollBy(1)
},
}, { context: 'Chat' })
```
## Keybinding Contexts
Contexts allow the same key to perform different actions depending on what's active.
```tsx
// Register a context as active
import { useRegisterKeybindingContext } from '@anthropic/ink'
function ThemePicker({ isOpen }) {
useRegisterKeybindingContext('ThemePicker', isOpen)
// While open, 'ThemePicker' context bindings take precedence
useKeybinding('picker:select', handleSelect, { context: 'ThemePicker' })
return isOpen ? <PickerUI /> : null
}
```
Context resolution order:
1. Registered active contexts (most recent first)
2. The hook's own `context` parameter
3. `'Global'` (always checked last)
## Chord Sequences
Keybindings support multi-key sequences (chords):
```
"ctrl+k ctrl+s" → Save (press Ctrl+K, then Ctrl+S)
"ctrl+k ctrl+c" → Close (press Ctrl+K, then Ctrl+C)
```
When a chord prefix is pressed:
- `result.type === 'chord_started'` -- Show "Ctrl+K ..." pending indicator
- Next key completes or cancels the chord
- `result.type === 'chord_cancelled'` -- Invalid key, reset
## KeybindingContext Hook
```tsx
import { useKeybindingContext, useOptionalKeybindingContext } from '@anthropic/ink'
const ctx = useKeybindingContext()
// ctx.resolve(input, key, contexts) → ResolveResult
// ctx.bindings → ParsedBinding[]
// ctx.pendingChord → ParsedKeystroke[] | null
// ctx.activeContexts → Set<string>
// ctx.getDisplayText(action, context) → string | undefined
// ctx.invokeAction(action) → boolean
// ctx.registerHandler(registration) → () => void (unsubscribe)
// Returns null outside provider (no throw)
const optionalCtx = useOptionalKeybindingContext()
```
## Parser Functions
Parse and format keybinding strings:
```tsx
import {
parseKeystroke,
parseChord,
keystrokeToString,
chordToString,
keystrokeToDisplayString,
chordToDisplayString,
parseBindings,
} from '@anthropic/ink'
```
### `parseKeystroke(str)`
Parse a single keystroke string:
```ts
parseKeystroke('ctrl+shift+enter')
// → { key: 'enter', ctrl: true, alt: false, shift: true, meta: false, super: false }
```
### `parseChord(str)`
Parse a chord (space-separated keystrokes):
```ts
parseChord('ctrl+k ctrl+s')
// → [{ key: 'k', ctrl: true, ... }, { key: 's', ctrl: true, ... }]
```
### `keystrokeToString(ks)` / `chordToString(chord)`
Convert parsed keystroke/chord back to string.
### `keystrokeToDisplayString(ks)` / `chordToDisplayString(chord)`
Convert to human-readable display string (platform-aware).
### `parseBindings(blocks)`
Parse a keybinding configuration:
```ts
parseBindings([
{
context: 'Global',
bindings: {
'ctrl+s': 'app:save',
'ctrl+k ctrl+s': 'app:saveAs',
}
}
])
// → ParsedBinding[]
```
## Match Functions
```tsx
import { getKeyName, matchesKeystroke, matchesBinding } from '@anthropic/ink'
```
### `getKeyName(input, key)`
Get the canonical key name from raw input:
```ts
getKeyName('\x1b[A', { upArrow: true }) // 'up'
```
### `matchesKeystroke(input, key, target)`
Check if raw input matches a parsed keystroke:
```ts
matchesKeystroke('s', { ctrl: true, shift: false }, { key: 's', ctrl: true })
```
### `matchesBinding(input, key, binding)`
Check if raw input matches any keystroke in a binding's chord.
## Resolver Functions
```tsx
import { resolveKey, resolveKeyWithChordState, getBindingDisplayText } from '@anthropic/ink'
```
### `resolveKey(input, key, contexts, bindings)`
Resolve input to a binding action:
```ts
const result = resolveKey('s', { ctrl: true, shift: false }, ['Global'], bindings)
// result.type: 'match' | 'none' | 'unbound'
// result.action: string (when type === 'match')
```
### `resolveKeyWithChordState(input, key, contexts, bindings, pendingChord)`
Resolve with chord state:
```ts
const result = resolveKeyWithChordState('k', key, ['Global'], bindings, null)
// result.type: 'match' | 'none' | 'unbound' | 'chord_started' | 'chord_cancelled'
// result.pending: ParsedKeystroke[] (when type === 'chord_started')
```
### `getBindingDisplayText(action, context, bindings)`
Get the display string for a binding:
```ts
getBindingDisplayText('app:save', 'Global', bindings) // 'Ctrl+S'
```

View File

@@ -0,0 +1,407 @@
# Chapter 9: Hooks Reference
Complete API reference for all hooks exported by `@anthropic/ink`.
---
## Application Hooks
### `useApp()`
Access app-level operations.
```ts
function useApp(): {
exit: (error?: Error) => void
}
```
Example:
```tsx
const { exit } = useApp()
// Gracefully unmount and exit
exit()
```
### `useStdin()`
Access the stdin stream and raw mode control.
```ts
function useStdin(): {
stdin: NodeJS.ReadStream
isRawModeSupported: boolean
setRawMode: (enabled: boolean) => void
internal_exitOnCtrlC: boolean
internal_eventEmitter: EventEmitter | undefined
internal_querier: TerminalQuerier | null
}
```
> Prefer `useInput` for keyboard handling.
---
## Input Hooks
### `useInput(handler, options?)`
Handle keyboard input. See [Chapter 7](./07-user-input.md) for full details.
```ts
function useInput(
handler: (input: string, key: Key, event: InputEvent) => void,
options?: { isActive?: boolean }
): void
```
---
## Terminal Hooks
### `useTerminalSize()`
Get current terminal dimensions.
```ts
function useTerminalSize(): {
columns: number
rows: number
}
```
Throws if used outside `<App>`.
### `useTerminalFocus()`
Track whether the terminal window is focused.
```ts
function useTerminalFocus(): boolean
```
Uses DECSET 1004 focus reporting. Returns `true` when focused.
### `useTerminalTitle(title)`
Set the terminal window title.
```ts
function useTerminalTitle(title: string | null): void
```
Pass `null` to clear the title.
### `useTerminalViewport()`
Track element visibility in the terminal viewport.
```ts
function useTerminalViewport(): [
ref: (element: DOMElement | null) => void,
entry: { isVisible: boolean }
]
```
Example:
```tsx
const [viewportRef, { isVisible }] = useTerminalViewport()
<Box ref={viewportRef}>
<Text>{isVisible ? 'Visible' : 'Scrolled off'}</Text>
</Box>
```
### `useTabStatus(kind)`
Set tab status indicator in terminal tab bar (OSC 21337).
```ts
type TabStatusKind = 'idle' | 'busy' | 'waiting'
function useTabStatus(kind: TabStatusKind | null): void
```
### `useTerminalNotification()`
Send terminal notifications (iTerm2, Kitty, Ghostty, bell).
```ts
function useTerminalNotification(): {
notifyITerm2: (opts: { message: string; title?: string }) => void
notifyKitty: (opts: { message: string; title: string; id: number }) => void
notifyGhostty: (opts: { message: string; title: string }) => void
notifyBell: () => void
progress: (state: Progress['state'] | null, percentage?: number) => void
}
```
Requires `TerminalWriteProvider` in the tree.
Progress states: `'running'`, `'completed'`, `'error'`, `'indeterminate'`, `null` (clear).
---
## Animation & Timing Hooks
### `useInterval(callback, intervalMs)`
Clock-backed interval timer.
```ts
function useInterval(callback: () => void, intervalMs: number | null): void
```
Pass `null` to pause. Shares the application clock for efficient batching.
### `useAnimationTimer(intervalMs)`
Returns the current clock time, updating at the given interval.
```ts
function useAnimationTimer(intervalMs: number): number
```
Subscribes as non-keepAlive -- won't keep the clock running on its own.
### `useAnimationFrame(intervalMs?)`
Synchronized animation hook that pauses when offscreen.
```ts
function useAnimationFrame(
intervalMs?: number | null, // default 16
): [ref: (element: DOMElement | null) => void, time: number]
```
Returns a ref callback (attach to animated element) and the current animation time. All instances share the same clock. Pass `null` to pause.
```tsx
const [ref, time] = useAnimationFrame(120)
const frame = Math.floor(time / 120) % FRAMES.length
return <Box ref={ref}>{FRAMES[frame]}</Box>
```
### `useTimeout(delayMs, resetTrigger?)`
One-shot timer.
```ts
function useTimeout(delay: number, resetTrigger?: number): boolean
```
Returns `true` when the timeout has elapsed. Change `resetTrigger` to restart.
### `useMinDisplayTime(value, minMs)`
Ensure a value is displayed for at least `minMs` milliseconds.
```ts
function useMinDisplayTime<T>(value: T, minMs: number): T
```
Holds the previous value until `minMs` has elapsed, then switches to the new value.
Example:
```tsx
// Keep showing "Loading" for at least 300ms to prevent flash
const displayValue = useMinDisplayTime(isLoading ? 'loading' : 'done', 300)
```
---
## Interaction Hooks
### `useDoublePress(setPending, onDoublePress, onFirstPress?)`
Detect double-press (double-click equivalent for keyboard).
```ts
export const DOUBLE_PRESS_TIMEOUT_MS = 800
function useDoublePress(
setPending: (pending: boolean) => void,
onDoublePress: () => void,
onFirstPress?: () => void
): () => void // Returns the press handler
```
Example:
```tsx
const [pendingExit, setPendingExit] = useState(false)
const handlePress = useDoublePress(
setPendingExit,
() => exit(), // Double press
() => {}, // First press
)
useInput((input, key) => {
if (key.escape) handlePress()
})
```
### `useExitOnCtrlCD(options?)`
Handle Ctrl+C / Ctrl+D with double-press confirmation.
```ts
type ExitState = {
pending: boolean
keyName: 'Ctrl-C' | 'Ctrl-D' | null
}
function useExitOnCtrlCDWithKeybindings(
onExit?: () => void,
onInterrupt?: () => boolean,
isActive?: boolean
): ExitState
```
Example:
```tsx
const exitState = useExitOnCtrlCDWithKeybindings(
() => exit(),
() => { /* return true to prevent exit */ }
)
if (exitState.pending) {
return <Text>Press {exitState.keyName} again to exit</Text>
}
```
---
## Selection Hooks (Alt-Screen Only)
### `useSelection()`
Text selection operations.
```ts
function useSelection(): {
copySelection: () => string
copySelectionNoClear: () => string
clearSelection: () => void
hasSelection: () => boolean
getState: () => SelectionState | null
subscribe: (cb: () => void) => () => void
shiftAnchor: (dRow: number, minRow: number, maxRow: number) => void
shiftSelection: (dRow: number, minRow: number, maxRow: number) => void
moveFocus: (move: FocusMove) => void
captureScrolledRows: (firstRow: number, lastRow: number, side: 'above' | 'below') => void
setSelectionBgColor: (color: string) => void
}
```
### `useHasSelection()`
Reactive boolean for selection state.
```ts
function useHasSelection(): boolean
```
Re-renders when selection is created or cleared.
---
## Search Hooks
### `useSearchHighlight()`
Set and manage search highlighting.
```ts
function useSearchHighlight(): {
setQuery: (query: string) => void
scanElement: (el: DOMElement) => MatchPosition[]
setPositions: (state: { positions: MatchPosition[]; rowOffset: number; currentIdx: number } | null) => void
}
```
### `useSearchInput(options)`
Search input handler with cursor management.
```ts
type UseSearchInputOptions = {
isActive: boolean
onExit: () => void
onCancel?: () => void
onExitUp?: () => void
columns?: number
passthroughCtrlKeys?: string[]
initialQuery?: string
backspaceExitsOnEmpty?: boolean
}
type UseSearchInputReturn = {
query: string
setQuery: (q: string) => void
cursorOffset: number
handleKeyDown: (e: KeyboardEvent) => void
}
function useSearchInput(options: UseSearchInputOptions): UseSearchInputReturn
```
---
## Cursor Hooks
### `useDeclaredCursor(options)`
Park the terminal cursor at a specific position for IME and accessibility.
```ts
function useDeclaredCursor({
line: number,
column: number,
active: boolean
}): (element: DOMElement | null) => void
```
Returns a ref callback. Position is relative to the ref'd element.
Example:
```tsx
const cursorRef = useDeclaredCursor({
line: 0,
column: cursorPosition,
active: isFocused,
})
return <Box ref={cursorRef}>...</Box>
```
---
## Tab Status Hooks
### `useTabStatus(kind)`
Set tab status indicator (OSC 21337) for terminal tab bars.
```ts
type TabStatusKind = 'idle' | 'busy' | 'waiting'
function useTabStatus(kind: TabStatusKind | null): void
```
Pass `null` to clear.
---
## Viewport Hooks
### `useTerminalViewport()`
Track element visibility within the terminal viewport.
```ts
function useTerminalViewport(): [
ref: (element: DOMElement | null) => void,
entry: { isVisible: boolean }
]
```
Returns a ref callback and visibility state.

View File

@@ -0,0 +1,232 @@
# Chapter 10: Events & Focus
## Event System
Ink implements a DOM-like event system with capture/bubble phases, propagation control, and prioritized dispatch.
### Event Classes
All events extend the base `Event` class:
```ts
class Event {
stopImmediatePropagation(): void
}
```
### InputEvent
Emitted for every keystroke or input action.
```ts
class InputEvent extends Event {
readonly input: string // Character(s) entered
readonly key: Key // Parsed key metadata
readonly keypress: ParsedKey // Raw keypress data
}
```
### KeyboardEvent
DOM-like keyboard event for focused elements.
```ts
class KeyboardEvent extends Event {
readonly key: Key
}
```
Dispatched via `onKeyDown` / `onKeyDownCapture` on `Box`.
### ClickEvent
Mouse click event (alt-screen only).
```ts
class ClickEvent extends Event {
readonly x: number // Column (0-indexed)
readonly y: number // Row (0-indexed)
}
```
Clicks bubble from the deepest hit Box up through ancestors.
### FocusEvent
Focus change event.
```ts
class FocusEvent extends Event {
readonly relatedTarget: DOMElement | null
}
```
### TerminalFocusEvent
Terminal window focus change.
```ts
class TerminalFocusEvent extends Event {
readonly type: 'terminalfocus' | 'terminalblur'
}
```
### ResizeEvent
Terminal resize event (internal).
### PasteEvent
Pasted text event (bracketed paste mode).
## Event Dispatch Flow
```
stdin data → parse-keypress → InputEvent
App.handleInput (useInput handlers)
Box.onKeyDown (focused element, bubble)
```
### Capture and Bubble Phases
```tsx
<Box
onKeyDownCapture={(e) => {
// Capture phase: fires top-down
console.log('Parent captures key')
}}
onKeyDown={(e) => {
// Bubble phase: fires bottom-up
console.log('Parent receives bubbled key')
}}
>
<Box
onKeyDown={(e) => {
// Target: fires first in bubble phase
console.log('Child handles key')
e.stopImmediatePropagation() // Stop here
}}
>
<Text>Focus here</Text>
</Box>
</Box>
```
### Event Propagation Methods
| Method | Effect |
|--------|--------|
| `event.stopImmediatePropagation()` | Stop all subsequent handlers |
| `event.preventDefault()` | Not supported in terminal context |
## FocusManager
DOM-like focus management system.
### How Focus Works
1. Elements with `tabIndex >= 0` participate in Tab/Shift+Tab cycling
2. Elements with `tabIndex === -1` are programmatically focusable only
3. Elements with `autoFocus` receive focus on mount
4. Clicking a focusable element focuses it
### Focus API
```ts
class FocusManager {
activeElement: DOMElement | null
focus(node: DOMElement): void
blur(): void
focusNext(root: DOMElement): void // Tab
focusPrevious(root: DOMElement): void // Shift+Tab
handleNodeRemoved(node: DOMElement, root: DOMElement): void
handleAutoFocus(node: DOMElement): void
handleClickFocus(node: DOMElement): void
enable(): void
disable(): void
}
```
### Tab Navigation
```tsx
<Box flexDirection="column">
<Button tabIndex={0} onAction={handleSave}>
{(s) => <Text>{s.focused ? '> Save' : ' Save'}</Text>}
</Button>
<Button tabIndex={0} onAction={handleCancel}>
{(s) => <Text>{s.focused ? '> Cancel' : ' Cancel'}</Text>}
</Button>
<Button tabIndex={-1} onAction={handleSecret}>
{/* Not reachable via Tab */}
{(s) => <Text>Secret</Text>}
</Button>
</Box>
```
### Auto Focus
```tsx
<Box tabIndex={0} autoFocus onKeyDown={handleKey}>
<Text>Receives focus immediately on mount</Text>
</Box>
```
### Focus Events
```tsx
<Box
tabIndex={0}
onFocus={(e) => console.log('Got focus')}
onBlur={(e) => console.log('Lost focus')}
onFocusCapture={(e) => console.log('Capture: focus in')}
onBlurCapture={(e) => console.log('Capture: focus out')}
>
<Text>Focusable element</Text>
</Box>
```
## Hit Testing
Mouse click/hover resolution:
1. Screen coordinates are mapped to DOM elements via Yoga layout
2. The deepest element at the click position is the target
3. Click events bubble upward through ancestors
4. Hover events use `mouseenter`/`mouseleave` semantics (no bubbling between children)
### Click Hit Testing
```ts
dispatchClick(rootNode, col, row): void
```
Walks the DOM tree, finds the deepest Box at (col, row), fires `onClick`, then bubbles to ancestors.
### Hover Hit Testing
```ts
dispatchHover(rootNode, col, row, hoveredNodes): void
```
Tracks which nodes are under the pointer. Fires `onMouseEnter`/`onMouseLeave` as the pointer moves between elements.
## EventEmitter
Custom event emitter for internal use:
```ts
class EventEmitter {
on(event: string, handler: Function): void
off(event: string, handler: Function): void
emit(event: string, ...args: any[]): void
removeListener(event: string, handler: Function): void
}
```
Used internally by the Ink instance for `input` events.

View File

@@ -0,0 +1,301 @@
# Chapter 11: Core Architecture
This chapter covers the internal rendering pipeline, DOM model, and screen buffer system. This is advanced material -- most users only need the component and hooks APIs.
## Rendering Pipeline
```
React Component Tree
↓ (React reconciler)
Ink DOM Tree (virtual terminal DOM)
↓ (Yoga layout)
Positioned DOM Tree (computed x, y, width, height)
↓ (renderNodeToOutput)
Output Buffer (styled characters)
↓ (renderer → Screen)
Screen Buffer (Int32Array of cells)
↓ (diffEach)
ANSI Diff Patches (minimal escape sequences)
↓ (writeDiffToTerminal)
Terminal stdout
```
### Frame Lifecycle
Each render cycle (`onRender`) follows these phases:
1. **React Commit** -- React reconciles the virtual tree; host config updates Ink DOM
2. **Yoga Layout** -- All dirty nodes have their styles applied and layout computed
3. **Renderer** -- Creates Output buffer, calls `renderNodeToOutput` for the full tree
4. **Screen Diff** -- New frame is compared against previous frame cell-by-cell
5. **Optimize** -- Patches are merged and ordered for minimal cursor movement
6. **Write** -- ANSI escape sequences are written to stdout
### Frame Timing
```ts
const FRAME_INTERVAL_MS = 16 // ~60fps cap
```
Renders are throttled. Multiple state updates in one frame are batched.
### Double Buffering
Two frames are maintained:
- **`frontFrame`** -- The currently displayed frame
- **`backFrame`** -- The frame being rendered
After rendering, they are swapped. This prevents partial updates from being visible.
## Ink DOM
### Node Types
```ts
type ElementNames =
| 'ink-root' // Root container
| 'ink-box' // Box component
| 'ink-text' // Text component
| 'ink-virtual-text' // Intermediate text wrapper
| 'ink-link' // Link component
| 'ink-raw-ansi' // Raw ANSI content
```
### DOMElement
```ts
type DOMElement = {
nodeName: ElementNames
attributes: Record<string, unknown>
childNodes: DOMNode[] // DOMElement | TextNode
yogaNode?: LayoutNode // Yoga layout node
textStyles?: TextStyles // Inherited text styles
// Scroll state
scrollTop?: number
scrollHeight?: number
scrollViewportHeight?: number
scrollViewportTop?: number
stickyScroll?: boolean
pendingScrollDelta?: number
scrollAnchor?: { el: DOMElement; offset: number }
// Dirty tracking
dirty: boolean
// Event handlers (stored separately)
onClick?: (event: ClickEvent) => void
onFocus?: (event: FocusEvent) => void
onBlur?: (event: FocusEvent) => void
onKeyDown?: (event: KeyboardEvent) => void
onMouseEnter?: () => void
onMouseLeave?: () => void
}
```
### TextNode
```ts
type TextNode = {
nodeName: '#text'
nodeValue: string
yogaNode?: LayoutNode
}
```
### DOM Operations
```ts
// Node creation
createNode(nodeName: string): DOMElement
createTextNode(text: string): TextNode
// Tree manipulation
appendChildNode(parent: DOMElement, child: DOMNode): void
insertBeforeNode(parent: DOMElement, child: DOMNode, before: DOMNode): void
removeChildNode(parent: DOMElement, child: DOMNode): void
// Attribute manipulation
setAttribute(node: DOMElement, key: string, value: unknown): void
setStyle(node: DOMElement, style: Styles): void
setTextStyles(node: DOMElement, styles: TextStyles): void
// Dirty tracking
markDirty(node: DOMElement): void
scheduleRenderFrom(node: DOMElement): void
```
## Screen Buffer
### Cell Storage
The screen buffer uses packed `Int32Array` storage for memory efficiency:
```ts
type Screen = {
width: number
height: number
cells: Int32Array // 2 Int32s per cell: [charId, packed_style_hyperlink_width]
cells64: BigInt64Array // For bulk fill operations
charPool: CharPool // String interning
stylePool: StylePool // ANSI code interning
hyperlinkPool: HyperlinkPool
emptyStyleId: number
damage: Rectangle | undefined // Bounding box of changed cells
noSelect: Uint8Array // Per-cell no-select bitmap
softWrap: Int32Array // Per-row soft-wrap markers
}
```
### Cell Width
```ts
enum CellWidth {
Narrow = 0, // Regular character (1 column)
Wide = 1, // CJK/emoji (2 columns)
SpacerTail = 2, // Right half of wide character
SpacerHead = 3, // Soft-wrapped wide character
}
```
### Style Pool
ANSI style codes are interned for efficiency:
```ts
class StylePool {
intern(codes: AnsiCode[]): number // Returns compact ID
get(id: number): AnsiCode[]
transition(from: number, to: number): string // Cached ANSI transition
withInverse(id: number): number // Selection overlay
setSelectionBg(bg: AnsiCode): void // Theme-aware selection bg
}
```
### Diff Algorithm
```ts
diffEach(prev: Screen, next: Screen, callback: (x, y, oldCell, newCell) => void): void
```
Only iterates cells within the damage bounding box. Unchanged regions are skipped entirely.
### Screen Operations
```ts
createScreen(width, height, stylePool, charPool, hyperlinkPool): Screen
setCellAt(screen, x, y, cell): void
cellAt(screen, x, y): Cell
clearRegion(screen, x, y, width, height): void
blitRegion(dst, src, x, y, maxX, maxY): void
shiftRows(screen, top, bottom, n): void
```
## Layout Engine
### Yoga Integration
Ink wraps Facebook's Yoga layout engine for Flexbox computation:
```ts
// Layout node types
enum LayoutDisplay { Flex, None }
enum LayoutPositionType { Absolute, Relative }
enum LayoutOverflow { Visible, Hidden, Scroll }
enum LayoutFlexDirection { Row, Column, RowReverse, ColumnReverse }
enum LayoutWrap { NoWrap, Wrap, WrapReverse }
enum LayoutAlign { FlexStart, Center, FlexEnd, Stretch }
enum LayoutJustify { FlexStart, Center, FlexEnd, SpaceBetween, SpaceAround, SpaceEvenly }
enum LayoutEdge { Top, Bottom, Left, Right, Start, End, Horizontal, Vertical, All }
enum LayoutGutter { Column, Row, All }
```
### Style Application
Styles from React props are applied to Yoga nodes during the commit phase:
```ts
function styles(node: LayoutNode, style: Styles, resolvedStyle?: Styles): void
```
This function maps each CSS-like prop to the corresponding Yoga setter.
## Output Buffer
Intermediate rendering target before screen diff:
```ts
class Output {
write(text: string, x: number, y: number, styles: TextStyles): void
wrap(width: number, textWrap: TextWrap): void
}
```
`renderNodeToOutput` walks the DOM tree and writes styled characters into this buffer.
## Reconciler
Custom React reconciler that bridges React and the Ink DOM:
- **Host config** -- Defines how React operations map to Ink DOM mutations
- **Concurrent mode** -- Supports `ConcurrentRoot` for React 19 features
- **Yoga integration** -- Applies styles during commit phase
- **DevTools** -- Connected in development mode
### Host Config Methods
| Method | Purpose |
|--------|---------|
| `createInstance` | Create `ink-box`, `ink-text`, etc. |
| `createTextInstance` | Create `#text` node |
| `appendChildNode` | Add child to parent |
| `removeChildNode` | Remove child from parent |
| `insertBefore` | Insert child before sibling |
| `commitUpdate` | Update element attributes/styles |
| `commitTextUpdate` | Update text content |
| `getPublicInstance` | Return DOMElement for refs |
## Performance Optimizations
1. **String Interning** -- CharPool deduplicates character strings across frames
2. **Style Caching** -- StylePool caches ANSI transition strings
3. **Damage Tracking** -- Only diff cells within the changed bounding box
4. **Bulk Operations** -- `Int32Array.set()` for fast region blit
5. **Throttled Rendering** -- Frame rate capped at ~60fps
6. **Viewport Culling** -- ScrollBox only renders visible children
7. **Microtask Coalescing** -- Multiple scroll deltas merged into one render
## Frame Events
Debug instrumentation for render performance:
```ts
type FrameEvent = {
durationMs: number
phases: {
renderer: number // Yoga + renderNodeToOutput
diff: number // Screen diff
optimize: number // Patch optimization
write: number // Terminal write
patches: number // Number of ANSI patches
yoga: number // Yoga layout time
commit: number // React commit time
yogaVisited: number // Yoga nodes visited
yogaMeasured: number // Yoga nodes measured
yogaCacheHits: number // Cached measurements
yogaLive: number // Active Yoga nodes
}
flickers: FlickerReason[]
}
```
Enable with `onFrame` in RenderOptions:
```tsx
render(<App />, {
onFrame: (event) => {
console.log(`Frame: ${event.durationMs}ms`)
}
})
```

View File

@@ -0,0 +1,381 @@
# Chapter 12: Terminal Integration
This chapter covers terminal-specific features: alternate screen, mouse tracking, clipboard, notifications, and terminal querying.
## Alternate Screen
Enter a fullscreen alternate screen buffer (like vim, less, htop).
```tsx
import { AlternateScreen } from '@anthropic/ink'
<AlternateScreen mouseTracking={true}>
<Box flexDirection="column" height="100%">
<Text>Fullscreen content</Text>
</Box>
</AlternateScreen>
```
### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | - | Content |
| `mouseTracking` | `boolean` | `true` | Enable SGR mouse tracking |
### Behavior
On mount:
1. Enters DEC 1049 alternate screen buffer
2. Hides cursor
3. Enables mouse tracking (if `mouseTracking=true`)
4. Constrains rendering height to terminal rows
On unmount:
1. Exits alternate screen buffer
2. Shows cursor
3. Disables mouse tracking
4. Restores original terminal content
### Mouse Tracking Modes
When enabled:
- **Mode 1003** -- Button press/release + motion (hover)
- **Mode 1006** -- SGR extended mouse format (coordinates > 223)
- **Wheel events** -- Scroll up/down
### External Editor Handoff
The Ink instance supports pausing for an external editor:
```ts
// Pause Ink, run external command, resume
ink.enterAlternateScreen() // Save state
// ... external editor runs ...
ink.reassertTerminalModes() // Restore on resume
```
This is triggered by Ctrl+Z (SIGTSTP) and SIGCONT.
## Mouse Events
### Click Events
```tsx
<Box onClick={(event) => {
console.log(`Clicked at col=${event.x}, row=${event.y}`)
event.stopImmediatePropagation()
}}>
<Text>Clickable area</Text>
</Box>
```
### Multi-Click
Double-click selects a word, triple-click selects a line. Handled by the App component:
```ts
// App prop
onMultiClick: (col: number, row: number, count: 2 | 3) => void
```
### Hover Events
```tsx
<Box
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<Text>{hovered ? 'Hovered!' : 'Hover me'}</Text>
</Box>
```
Hover uses `mouseenter`/`mouseleave` semantics (no bubbling between children).
### Drag-to-Select
In alt-screen mode, click-drag creates a text selection:
```ts
// App prop
onSelectionDrag: (col: number, row: number) => void
```
## Clipboard
### OSC 52 Clipboard
```tsx
import { setClipboard } from '@anthropic/ink'
await setClipboard('Copied text')
```
### Copy Selection
```tsx
const { copySelection } = useSelection()
const text = copySelection() // Copies to clipboard and clears highlight
```
### Copy Without Clear
```tsx
const { copySelectionNoClear } = useSelection()
const text = copySelectionNoClear() // Copies but keeps highlight
```
## Terminal Notifications
Send desktop notifications from the terminal.
```tsx
import { useTerminalNotification } from '@anthropic/ink'
function MyComponent() {
const { notifyBell, progress } = useTerminalNotification()
// Terminal bell (audible/system notification)
notifyBell()
// Progress bar in terminal title/tab
progress('running', 65) // 65% complete
progress('completed') // Done
progress('error') // Error state
progress('indeterminate') // Unknown progress
progress(null) // Clear
}
```
### Terminal-Specific Notifications
```tsx
const { notifyITerm2, notifyKitty, notifyGhostty } = useTerminalNotification()
// iTerm2
notifyITerm2({ message: 'Build complete', title: 'My App' })
// Kitty
notifyKitty({ message: 'Build complete', title: 'My App', id: 1 })
// Ghostty
notifyGhostty({ message: 'Build complete', title: 'My App' })
```
## Terminal Queries
### Background Color (OSC 11)
Used for auto-theme detection:
```ts
import { getTerminalBackground } from '@anthropic/ink'
const bg = await getTerminalBackground()
// e.g., 'rgb:0000/0000/0000' (dark) or 'rgb:ffff/ffff/ffff' (light)
```
### Terminal Version (XTVERSION)
```ts
import { isXtermJs, setXtversionName, getXtversionName } from '@anthropic/ink'
```
### Feature Detection
```ts
import { supportsHyperlinks } from '@anthropic/ink'
if (supportsHyperlinks()) {
// OSC 8 hyperlinks supported
}
import { supportsExtendedKeys } from '@anthropic/ink'
if (supportsExtendedKeys()) {
// Kitty keyboard protocol / modifyOtherKeys available
}
```
## Terminal Focus
Track terminal window focus/unfocus:
```tsx
import { useTerminalFocus } from '@anthropic/ink'
const isFocused = useTerminalFocus()
```
Low-level API:
```ts
import { getTerminalFocused, subscribeTerminalFocus } from '@anthropic/ink'
getTerminalFocused() // boolean
subscribeTerminalFocus((focused: boolean) => {
// Called on focus change
})
```
Uses DECSET 1004 focus reporting.
## Terminal Title
Set the terminal window title:
```tsx
import { useTerminalTitle } from '@anthropic/ink'
useTerminalTitle('My App - Dashboard')
```
Clear:
```tsx
useTerminalTitle(null)
```
## Terminal I/O Sequences
Low-level ANSI sequence constants for advanced use.
### Cursor Control
```ts
import {
SHOW_CURSOR,
HIDE_CURSOR,
CURSOR_HOME,
} from '@anthropic/ink'
// cursorPosition(row, col) -- Move cursor to absolute position
// cursorMove(dx, dy) -- Move cursor relative
```
### Screen Control
```ts
import {
ENTER_ALT_SCREEN,
EXIT_ALT_SCREEN,
ERASE_SCREEN,
} from '@anthropic/ink'
```
### Mouse Control
```ts
import {
ENABLE_MOUSE_TRACKING,
DISABLE_MOUSE_TRACKING,
} from '@anthropic/ink'
```
### Keyboard Protocols
```ts
import {
ENABLE_KITTY_KEYBOARD,
DISABLE_KITTY_KEYBOARD,
ENABLE_MODIFY_OTHER_KEYS,
DISABLE_MODIFY_OTHER_KEYS,
} from '@anthropic/ink'
```
### Clipboard & Tab Status
```ts
import {
CLEAR_ITERM2_PROGRESS,
CLEAR_TAB_STATUS,
CLEAR_TERMINAL_TITLE,
wrapForMultiplexer,
} from '@anthropic/ink'
```
`wrapForMultiplexer` wraps OSC sequences for tmux compatibility.
## Terminal Compatibility
### Supported Terminals
| Terminal | Features |
|----------|----------|
| iTerm2 | Full support (hyperlinks, notifications, progress) |
| Kitty | Full support (keyboard protocol, notifications) |
| Ghostty | Full support |
| WezTerm | Full support |
| Alacritty | Most features |
| Windows Terminal | Most features |
| Apple Terminal | 256-color fallback |
| xterm.js (VS Code) | Detected and special-cased |
| tmux | Wrapped sequences via `wrapForMultiplexer` |
| Screen | Basic support |
### Feature Degradation
The framework gracefully degrades:
- No true color → Falls back to ANSI 16-color themes
- No OSC 52 → Clipboard operations silently fail
- No mouse tracking → Click/hover events are no-ops
- No extended keys → Standard escape sequences used
- No bracketed paste → Paste detected by timing heuristic
### Synchronized Output
```ts
import { isSynchronizedOutputSupported } from '@anthropic/ink'
if (isSynchronizedOutputSupported()) {
// BSU/ESU for tear-free rendering
}
```
Uses DECSET 2026 synchronized output to prevent partial frame display.
### Bracketed Paste
Uses DECSET 2004 to distinguish paste events from rapid typing. Automatically enabled by the App component.
## Text Selection (Alt-Screen)
### Selection State
```ts
type SelectionState = {
anchor: Point | null // Drag start
focus: Point | null // Current position
isDragging: boolean
anchorSpan: { lo: Point; hi: Point; kind: 'word' | 'line' } | null
scrolledOffAbove: string[] // Text scrolled out above
scrolledOffBelow: string[] // Text scrolled out below
}
```
### Selection Operations
- **Click-drag** -- Free-form selection
- **Double-click** -- Word selection
- **Triple-click** -- Line selection
- **Shift+Arrow** -- Extend selection from keyboard
- **Drag-to-scroll** -- Auto-scroll when dragging near edges
### noSelect Regions
Exclude areas from selection (gutters, line numbers):
```tsx
<Box noSelect={true}>
<Text>1 </Text>
</Box>
<Box>
<Text>code here</Text> {/* Only this is selectable */}
</Box>
```
### Soft-Wrap Awareness
Selection correctly handles text that was wrapped across multiple rows:
- Wrapped lines are joined when copied
- Trailing whitespace is trimmed
- The `softWrap` bitmap tracks which rows are continuations

View File

@@ -0,0 +1,46 @@
# @anthropic/ink Documentation
A terminal React rendering framework for building rich command-line interfaces.
## Architecture Overview
`@anthropic/ink` is a forked/internal Ink framework that renders React components directly to the terminal using ANSI escape sequences. It uses Yoga (via a custom layout engine) for Flexbox layout, a custom React reconciler for terminal DOM, and a screen-buffer differ for efficient updates.
### Three-Layer Architecture
```
┌─────────────────────────────────────────┐
│ Layer 3: Theme │
│ ThemeProvider, ThemedBox, ThemedText, │
│ Dialog, FuzzyPicker, ProgressBar, etc. │
├─────────────────────────────────────────┤
│ Layer 2: Components │
│ Box, Text, ScrollBox, Button, Link, │
│ Newline, Spacer, AlternateScreen │
├─────────────────────────────────────────┤
│ Layer 1: Core │
│ Reconciler, Layout (Yoga), Terminal │
│ I/O, Screen Buffer, Event System │
└─────────────────────────────────────────┘
```
- **Core** (`src/core/`) -- Rendering engine: React reconciler, Yoga flexbox layout, terminal I/O, screen buffer with diff-based updates, event system (keyboard, mouse, focus, click).
- **Components** (`src/components/`) -- UI primitives: `Box`, `Text`, `ScrollBox`, `Button`, `Link`, `Newline`, `Spacer`, etc. Plus context providers (`App`, `StdinContext`).
- **Theme** (`src/theme/`) -- Theme system: `ThemeProvider`, theme-aware `Box`/`Text` wrappers, and design-system components (`Dialog`, `FuzzyPicker`, `ProgressBar`, `Tabs`, etc.).
### Documentation
| Chapter | File | Contents |
|---------|------|----------|
| 1 | [Getting Started](./01-getting-started.md) | Installation, rendering, basic concepts |
| 2 | [Layout System](./02-layout.md) | Box, Flexbox, Yoga, positioning, dimensions |
| 3 | [Text & Styling](./03-text-and-styling.md) | Text component, colors, text wrapping, ANSI styling |
| 4 | [Theme System](./04-theme-system.md) | ThemeProvider, themes, ThemedBox, ThemedText, color() |
| 5 | [Design System Components](./05-design-system.md) | Dialog, ProgressBar, FuzzyPicker, Tabs, Spinner, etc. |
| 6 | [Scrolling](./06-scrolling.md) | ScrollBox, sticky scroll, imperative scroll API |
| 7 | [User Input](./07-user-input.md) | useInput, Key types, raw mode, mouse events |
| 8 | [Keybinding System](./08-keybindings.md) | KeybindingProvider, useKeybinding, chord sequences, parser |
| 9 | [Hooks Reference](./09-hooks-reference.md) | All hooks with full API signatures |
| 10 | [Events & Focus](./10-events-and-focus.md) | Event system, FocusManager, click/hover, tab navigation |
| 11 | [Core Architecture](./11-core-architecture.md) | Reconciler, screen buffer, terminal I/O, rendering pipeline |
| 12 | [Terminal Integration](./12-terminal-integration.md) | Alternate screen, mouse tracking, clipboard, notifications |

View File

@@ -0,0 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,18 @@
{
"name": "@anthropic-ai/model-provider",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./types": "./src/types/index.ts",
"./hooks": "./src/hooks/index.ts",
"./client": "./src/client/index.ts"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.80.0",
"openai": "^6.33.0"
}
}

View File

@@ -0,0 +1,27 @@
import type { ClientFactories } from './types.js'
let registeredFactories: ClientFactories | null = null
/**
* Register client factories from the main project.
* Call this during application initialization.
*/
export function registerClientFactories(factories: ClientFactories): void {
registeredFactories = factories
}
/**
* Get registered client factories.
* Throws if not registered (fail-fast).
*/
export function getClientFactories(): ClientFactories {
if (!registeredFactories) {
throw new Error(
'Client factories not registered. ' +
'Call registerClientFactories() during app initialization.',
)
}
return registeredFactories
}
export type { ClientFactories }

View File

@@ -0,0 +1,35 @@
/**
* Client factory interfaces.
* Authentication is handled externally — main project provides factory implementations.
*/
export interface ClientFactories {
/** Get Anthropic client (1st party, Bedrock, Foundry, Vertex) */
getAnthropicClient: (params: {
model?: string
maxRetries: number
fetchOverride?: unknown
source?: string
}) => Promise<unknown>
/** Get OpenAI-compatible client */
getOpenAIClient: (params: {
maxRetries: number
fetchOverride?: unknown
source?: string
}) => unknown
/** Stream Gemini generate content */
streamGeminiGenerateContent: (params: {
model: string
signal?: AbortSignal
fetchOverride?: unknown
body: Record<string, unknown>
}) => AsyncIterable<unknown>
/** Get Grok client (OpenAI-compatible) */
getGrokClient: (params: {
maxRetries: number
fetchOverride?: unknown
source?: string
}) => unknown
}

View File

@@ -0,0 +1,238 @@
import type { APIError } from '@anthropic-ai/sdk'
// SSL/TLS error codes from OpenSSL (used by both Node.js and Bun)
// See: https://www.openssl.org/docs/man3.1/man3/X509_STORE_CTX_get_error.html
const SSL_ERROR_CODES = new Set([
// Certificate verification errors
'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
'UNABLE_TO_GET_ISSUER_CERT',
'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
'CERT_SIGNATURE_FAILURE',
'CERT_NOT_YET_VALID',
'CERT_HAS_EXPIRED',
'CERT_REVOKED',
'CERT_REJECTED',
'CERT_UNTRUSTED',
// Self-signed certificate errors
'DEPTH_ZERO_SELF_SIGNED_CERT',
'SELF_SIGNED_CERT_IN_CHAIN',
// Chain errors
'CERT_CHAIN_TOO_LONG',
'PATH_LENGTH_EXCEEDED',
// Hostname/altname errors
'ERR_TLS_CERT_ALTNAME_INVALID',
'HOSTNAME_MISMATCH',
// TLS handshake errors
'ERR_TLS_HANDSHAKE_TIMEOUT',
'ERR_SSL_WRONG_VERSION_NUMBER',
'ERR_SSL_DECRYPTION_FAILED_OR_BAD_RECORD_MAC',
])
export type ConnectionErrorDetails = {
code: string
message: string
isSSLError: boolean
}
/**
* Extracts connection error details from the error cause chain.
* The Anthropic SDK wraps underlying errors in the `cause` property.
* This function walks the cause chain to find the root error code/message.
*/
export function extractConnectionErrorDetails(
error: unknown,
): ConnectionErrorDetails | null {
if (!error || typeof error !== 'object') {
return null
}
// Walk the cause chain to find the root error with a code
let current: unknown = error
const maxDepth = 5 // Prevent infinite loops
let depth = 0
while (current && depth < maxDepth) {
if (
current instanceof Error &&
'code' in current &&
typeof current.code === 'string'
) {
const code = current.code
const isSSLError = SSL_ERROR_CODES.has(code)
return {
code,
message: current.message,
isSSLError,
}
}
// Move to the next cause in the chain
if (
current instanceof Error &&
'cause' in current &&
current.cause !== current
) {
current = current.cause
depth++
} else {
break
}
}
return null
}
/**
* Returns an actionable hint for SSL/TLS errors, intended for contexts outside
* the main API client (OAuth token exchange, preflight connectivity checks)
* where `formatAPIError` doesn't apply.
*/
export function getSSLErrorHint(error: unknown): string | null {
const details = extractConnectionErrorDetails(error)
if (!details?.isSSLError) {
return null
}
return `SSL certificate error (${details.code}). If you are behind a corporate proxy or TLS-intercepting firewall, set NODE_EXTRA_CA_CERTS to your CA bundle path, or ask IT to allowlist *.anthropic.com. Run /doctor for details.`
}
/**
* Strips HTML content (e.g., CloudFlare error pages) from a message string,
* returning a user-friendly title or empty string if HTML is detected.
* Returns the original message unchanged if no HTML is found.
*/
function sanitizeMessageHTML(message: string): string {
if (message.includes('<!DOCTYPE html') || message.includes('<html')) {
const titleMatch = message.match(/<title>([^<]+)<\/title>/)
if (titleMatch && titleMatch[1]) {
return titleMatch[1].trim()
}
return ''
}
return message
}
/**
* Detects if an error message contains HTML content (e.g., CloudFlare error pages)
* and returns a user-friendly message instead
*/
export function sanitizeAPIError(apiError: APIError): string {
const message = apiError.message
if (!message) {
return ''
}
return sanitizeMessageHTML(message)
}
/**
* Shapes of deserialized API errors from session JSONL.
*/
type NestedAPIError = {
error?: {
message?: string
error?: { message?: string }
}
}
function hasNestedError(value: unknown): value is NestedAPIError {
return (
typeof value === 'object' &&
value !== null &&
'error' in value &&
typeof value.error === 'object' &&
value.error !== null
)
}
/**
* Extract a human-readable message from a deserialized API error that lacks
* a top-level `.message`.
*/
function extractNestedErrorMessage(error: APIError): string | null {
if (!hasNestedError(error)) {
return null
}
const narrowed: NestedAPIError = error
const nested = narrowed.error
// Standard Anthropic API shape: { error: { error: { message } } }
const deepMsg = nested?.error?.message
if (typeof deepMsg === 'string' && deepMsg.length > 0) {
const sanitized = sanitizeMessageHTML(deepMsg)
if (sanitized.length > 0) {
return sanitized
}
}
// Bedrock shape: { error: { message } }
const msg = nested?.message
if (typeof msg === 'string' && msg.length > 0) {
const sanitized = sanitizeMessageHTML(msg)
if (sanitized.length > 0) {
return sanitized
}
}
return null
}
export function formatAPIError(error: APIError): string {
// Extract connection error details from the cause chain
const connectionDetails = extractConnectionErrorDetails(error)
if (connectionDetails) {
const { code, isSSLError } = connectionDetails
// Handle timeout errors
if (code === 'ETIMEDOUT') {
return 'Request timed out. Check your internet connection and proxy settings'
}
// Handle SSL/TLS errors with specific messages
if (isSSLError) {
switch (code) {
case 'UNABLE_TO_VERIFY_LEAF_SIGNATURE':
case 'UNABLE_TO_GET_ISSUER_CERT':
case 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY':
return 'Unable to connect to API: SSL certificate verification failed. Check your proxy or corporate SSL certificates'
case 'CERT_HAS_EXPIRED':
return 'Unable to connect to API: SSL certificate has expired'
case 'CERT_REVOKED':
return 'Unable to connect to API: SSL certificate has been revoked'
case 'DEPTH_ZERO_SELF_SIGNED_CERT':
case 'SELF_SIGNED_CERT_IN_CHAIN':
return 'Unable to connect to API: Self-signed certificate detected. Check your proxy or corporate SSL certificates'
case 'ERR_TLS_CERT_ALTNAME_INVALID':
case 'HOSTNAME_MISMATCH':
return 'Unable to connect to API: SSL certificate hostname mismatch'
case 'CERT_NOT_YET_VALID':
return 'Unable to connect to API: SSL certificate is not yet valid'
default:
return `Unable to connect to API: SSL error (${code})`
}
}
}
if (error.message === 'Connection error.') {
// If we have a code but it's not SSL, include it for debugging
if (connectionDetails?.code) {
return `Unable to connect to API (${connectionDetails.code})`
}
return 'Unable to connect to API. Check your internet connection'
}
// Guard: when deserialized from JSONL (e.g. --resume), the error object may
// be a plain object without a `.message` property.
if (!error.message) {
return (
extractNestedErrorMessage(error) ??
`API error (status ${error.status ?? 'unknown'})`
)
}
const sanitizedMessage = sanitizeAPIError(error)
// Use sanitized message if it's different from the original (i.e., HTML was sanitized)
return sanitizedMessage !== error.message && sanitizedMessage.length > 0
? sanitizedMessage
: error.message
}

View File

@@ -0,0 +1,27 @@
import type { ModelProviderHooks } from './types.js'
let registeredHooks: ModelProviderHooks | null = null
/**
* Register hooks from the main project.
* Call this during application initialization.
*/
export function registerHooks(hooks: ModelProviderHooks): void {
registeredHooks = hooks
}
/**
* Get registered hooks.
* Throws if hooks not registered (fail-fast).
*/
export function getHooks(): ModelProviderHooks {
if (!registeredHooks) {
throw new Error(
'ModelProvider hooks not registered. ' +
'Call registerHooks() during app initialization.',
)
}
return registeredHooks
}
export type { ModelProviderHooks }

View File

@@ -0,0 +1,48 @@
/**
* Hooks for dependency injection.
* Main project provides implementations; model-provider calls them.
*
* This decouples the model-provider from main project specifics like
* analytics, cost tracking, feature flags, etc.
*/
export interface ModelProviderHooks {
/** Log an analytics event (replaces direct logEvent calls) */
logEvent: (eventName: string, metadata?: Record<string, unknown>) => void
/** Report API cost after each response */
reportCost: (params: {
costUSD: number
usage: Record<string, unknown>
model: string
}) => void
/** Get tool permission context */
getToolPermissionContext?: () => Promise<Record<string, unknown>>
/** Debug logging */
logForDebugging: (msg: string, opts?: { level?: string }) => void
/** Error logging */
logError: (error: Error) => void
/** Get feature flag value */
getFeatureFlag?: (flagName: string) => unknown
/** Get session ID */
getSessionId: () => string
/** Add a notification */
addNotification?: (notification: Record<string, unknown>) => void
/** Get API provider name */
getAPIProvider: () => string
/** Get user ID */
getOrCreateUserID: () => string
/** Check if non-interactive session */
isNonInteractiveSession: () => boolean
/** Get OAuth account info */
getOauthAccountInfo?: () => Record<string, unknown> | undefined
}

View File

@@ -0,0 +1,63 @@
// @anthropic-ai/model-provider
// Model provider abstraction layer for Claude Code
//
// This package owns the model calling logic and provides:
// - Core query functions (queryModelWithStreaming, etc.)
// - Provider implementations (Anthropic, OpenAI, Gemini, Grok)
// - Type definitions (Message, Tool, Usage, etc.)
// - Dependency injection hooks (analytics, cost tracking, etc.)
//
// Initialization:
// registerClientFactories({ ... }) // inject auth clients
// registerHooks({ ... }) // inject analytics/cost/logging
// Hooks (dependency injection)
export { registerHooks, getHooks } from './hooks/index.js'
export type { ModelProviderHooks } from './hooks/types.js'
// Client factories
export { registerClientFactories, getClientFactories } from './client/index.js'
export type { ClientFactories } from './client/types.js'
// Types
export * from './types/index.js'
// Provider model mappings
export { resolveOpenAIModel } from './providers/openai/modelMapping.js'
export { resolveGrokModel } from './providers/grok/modelMapping.js'
export { resolveGeminiModel } from './providers/gemini/modelMapping.js'
// Gemini provider utilities
export { anthropicMessagesToGemini } from './providers/gemini/convertMessages.js'
export { anthropicToolsToGemini, anthropicToolChoiceToGemini } from './providers/gemini/convertTools.js'
export { adaptGeminiStreamToAnthropic } from './providers/gemini/streamAdapter.js'
export {
GEMINI_THOUGHT_SIGNATURE_FIELD,
type GeminiContent,
type GeminiGenerateContentRequest,
type GeminiPart,
type GeminiStreamChunk,
type GeminiTool,
type GeminiFunctionCallingConfig,
type GeminiFunctionDeclaration,
type GeminiFunctionCall,
type GeminiFunctionResponse,
type GeminiInlineData,
type GeminiUsageMetadata,
type GeminiCandidate,
} from './providers/gemini/types.js'
// Error utilities
export {
formatAPIError,
extractConnectionErrorDetails,
sanitizeAPIError,
getSSLErrorHint,
type ConnectionErrorDetails,
} from './errorUtils.js'
// Shared OpenAI conversion utilities
export { anthropicMessagesToOpenAI } from './shared/openaiConvertMessages.js'
export type { ConvertMessagesOptions } from './shared/openaiConvertMessages.js'
export { anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from './shared/openaiConvertTools.js'
export { adaptOpenAIStreamToAnthropic } from './shared/openaiStreamAdapter.js'

View File

@@ -0,0 +1,307 @@
import type {
BetaToolResultBlockParam,
BetaToolUseBlock,
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
import type { AssistantMessage, UserMessage } from '../../types/message.js'
import type { SystemPrompt } from '../../types/systemPrompt.js'
import {
GEMINI_THOUGHT_SIGNATURE_FIELD,
type GeminiContent,
type GeminiGenerateContentRequest,
type GeminiPart,
} from './types.js'
// Simple JSON parse utility (replaces safeParseJSON from main project)
function safeParseJSON(json: string | null | undefined): unknown {
if (!json) return null
try {
return JSON.parse(json)
} catch {
return null
}
}
export function anthropicMessagesToGemini(
messages: (UserMessage | AssistantMessage)[],
systemPrompt: SystemPrompt,
): Pick<GeminiGenerateContentRequest, 'contents' | 'systemInstruction'> {
const contents: GeminiContent[] = []
const toolNamesById = new Map<string, string>()
for (const msg of messages) {
if (msg.type === 'assistant') {
const content = convertInternalAssistantMessage(msg)
if (content.parts.length > 0) {
contents.push(content)
}
const assistantContent = msg.message.content
if (Array.isArray(assistantContent)) {
for (const block of assistantContent) {
if (typeof block !== 'string' && block.type === 'tool_use') {
toolNamesById.set(block.id, block.name)
}
}
}
continue
}
if (msg.type === 'user') {
const content = convertInternalUserMessage(msg, toolNamesById)
if (content.parts.length > 0) {
contents.push(content)
}
}
}
const systemText = systemPromptToText(systemPrompt)
return {
contents,
...(systemText
? {
systemInstruction: {
parts: [{ text: systemText }],
},
}
: {}),
}
}
function systemPromptToText(systemPrompt: SystemPrompt): string {
if (!systemPrompt || systemPrompt.length === 0) return ''
return systemPrompt.filter(Boolean).join('\n\n')
}
function convertInternalUserMessage(
msg: UserMessage,
toolNamesById: ReadonlyMap<string, string>,
): GeminiContent {
const content = msg.message.content
if (typeof content === 'string') {
return {
role: 'user',
parts: createTextGeminiParts(content),
}
}
if (!Array.isArray(content)) {
return { role: 'user', parts: [] }
}
return {
role: 'user',
parts: content.flatMap(block =>
convertUserContentBlockToGeminiParts(block as unknown as string | Record<string, unknown>, toolNamesById),
),
}
}
function convertUserContentBlockToGeminiParts(
block: string | Record<string, unknown>,
toolNamesById: ReadonlyMap<string, string>,
): GeminiPart[] {
if (typeof block === 'string') {
return createTextGeminiParts(block)
}
if (block.type === 'text') {
return createTextGeminiParts(block.text)
}
if (block.type === 'tool_result') {
const toolResult = block as unknown as BetaToolResultBlockParam
return [
{
functionResponse: {
name: toolNamesById.get(toolResult.tool_use_id) ?? toolResult.tool_use_id,
response: toolResultToResponseObject(toolResult),
},
},
]
}
// Convert Anthropic image blocks to Gemini inlineData
if (block.type === 'image') {
const source = block.source as Record<string, unknown> | undefined
if (source?.type === 'base64' && typeof source.data === 'string') {
const mediaType = (source.media_type as string) || 'image/png'
return [
{
inlineData: {
mimeType: mediaType,
data: source.data,
},
},
]
}
// URL images not directly supported by Gemini, convert to text description
if (source?.type === 'url' && typeof source.url === 'string') {
return createTextGeminiParts(`[image: ${source.url}]`)
}
}
return []
}
function convertInternalAssistantMessage(msg: AssistantMessage): GeminiContent {
const content = msg.message.content
if (typeof content === 'string') {
return {
role: 'model',
parts: createTextGeminiParts(content),
}
}
if (!Array.isArray(content)) {
return { role: 'model', parts: [] }
}
const parts: GeminiPart[] = []
for (const block of content) {
if (typeof block === 'string') {
parts.push(...createTextGeminiParts(block))
continue
}
if (block.type === 'text') {
parts.push(
...createTextGeminiParts(
block.text,
getGeminiThoughtSignature(block as unknown as Record<string, unknown>),
),
)
continue
}
if (block.type === 'thinking') {
const thinkingPart = createThinkingGeminiPart(
block.thinking,
block.signature,
)
if (thinkingPart) {
parts.push(thinkingPart)
}
continue
}
if (block.type === 'tool_use') {
const toolUse = block as unknown as BetaToolUseBlock
parts.push({
functionCall: {
name: toolUse.name,
args: normalizeToolUseInput(toolUse.input),
},
...(getGeminiThoughtSignature(block as unknown as Record<string, unknown>) && {
thoughtSignature: getGeminiThoughtSignature(block as unknown as Record<string, unknown>),
}),
})
}
}
return { role: 'model', parts }
}
function createTextGeminiParts(
value: unknown,
thoughtSignature?: string,
): GeminiPart[] {
if (typeof value !== 'string' || value.length === 0) {
return []
}
return [
{
text: value,
...(thoughtSignature && { thoughtSignature }),
},
]
}
function createThinkingGeminiPart(
value: unknown,
thoughtSignature?: string,
): GeminiPart | undefined {
if (typeof value !== 'string' || value.length === 0) {
return undefined
}
return {
text: value,
thought: true,
...(thoughtSignature && { thoughtSignature }),
}
}
function normalizeToolUseInput(input: unknown): Record<string, unknown> {
if (typeof input === 'string') {
const parsed = safeParseJSON(input)
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed as Record<string, unknown>
}
return parsed === null ? {} : { value: parsed }
}
if (input && typeof input === 'object' && !Array.isArray(input)) {
return input as Record<string, unknown>
}
return input === undefined ? {} : { value: input }
}
function toolResultToResponseObject(
block: BetaToolResultBlockParam,
): Record<string, unknown> {
const result = normalizeToolResultContent(block.content)
if (
result &&
typeof result === 'object' &&
!Array.isArray(result)
) {
return block.is_error ? { ...(result as Record<string, unknown>), is_error: true } : result as Record<string, unknown>
}
return {
result,
...(block.is_error ? { is_error: true } : {}),
}
}
function normalizeToolResultContent(content: unknown): unknown {
if (typeof content === 'string') {
const parsed = safeParseJSON(content)
return parsed ?? content
}
if (Array.isArray(content)) {
const text = content
.map(part => {
if (typeof part === 'string') return part
if (
part &&
typeof part === 'object' &&
'text' in part &&
typeof part.text === 'string'
) {
return part.text
}
return ''
})
.filter(Boolean)
.join('\n')
const parsed = safeParseJSON(text)
return parsed ?? text
}
return content ?? ''
}
function getGeminiThoughtSignature(block: Record<string, unknown>): string | undefined {
const signature = block[GEMINI_THOUGHT_SIGNATURE_FIELD]
return typeof signature === 'string' && signature.length > 0
? signature
: undefined
}

View File

@@ -0,0 +1,285 @@
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
import type {
GeminiFunctionCallingConfig,
GeminiTool,
} from './types.js'
const GEMINI_JSON_SCHEMA_TYPES = new Set([
'string',
'number',
'integer',
'boolean',
'object',
'array',
'null',
])
function normalizeGeminiJsonSchemaType(
value: unknown,
): string | string[] | undefined {
if (typeof value === 'string') {
return GEMINI_JSON_SCHEMA_TYPES.has(value) ? value : undefined
}
if (Array.isArray(value)) {
const normalized = value.filter(
(item): item is string =>
typeof item === 'string' && GEMINI_JSON_SCHEMA_TYPES.has(item),
)
const unique = Array.from(new Set(normalized))
if (unique.length === 0) return undefined
return unique.length === 1 ? unique[0] : unique
}
return undefined
}
function inferGeminiJsonSchemaTypeFromValue(value: unknown): string | undefined {
if (value === null) return 'null'
if (Array.isArray(value)) return 'array'
if (typeof value === 'string') return 'string'
if (typeof value === 'boolean') return 'boolean'
if (typeof value === 'number') {
return Number.isInteger(value) ? 'integer' : 'number'
}
if (typeof value === 'object') return 'object'
return undefined
}
function inferGeminiJsonSchemaTypeFromEnum(
values: unknown[],
): string | string[] | undefined {
const inferred = values
.map(inferGeminiJsonSchemaTypeFromValue)
.filter((value): value is string => value !== undefined)
const unique = Array.from(new Set(inferred))
if (unique.length === 0) return undefined
return unique.length === 1 ? unique[0] : unique
}
function addNullToGeminiJsonSchemaType(
value: string | string[] | undefined,
): string | string[] | undefined {
if (value === undefined) return ['null']
if (Array.isArray(value)) {
return value.includes('null') ? value : [...value, 'null']
}
return value === 'null' ? value : [value, 'null']
}
function sanitizeGeminiJsonSchemaProperties(
value: unknown,
): Record<string, Record<string, unknown>> | undefined {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return undefined
}
const sanitizedEntries = Object.entries(value as Record<string, unknown>)
.map(([key, schema]) => [key, sanitizeGeminiJsonSchema(schema)] as const)
.filter(([, schema]) => Object.keys(schema).length > 0)
if (sanitizedEntries.length === 0) {
return undefined
}
return Object.fromEntries(sanitizedEntries)
}
function sanitizeGeminiJsonSchemaArray(
value: unknown,
): Record<string, unknown>[] | undefined {
if (!Array.isArray(value)) return undefined
const sanitized = value
.map(item => sanitizeGeminiJsonSchema(item))
.filter(item => Object.keys(item).length > 0)
return sanitized.length > 0 ? sanitized : undefined
}
function sanitizeGeminiJsonSchema(
schema: unknown,
): Record<string, unknown> {
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
return {}
}
const source = schema as Record<string, unknown>
const result: Record<string, unknown> = {}
let type = normalizeGeminiJsonSchemaType(source.type)
if (source.const !== undefined) {
result.enum = [source.const]
type = type ?? inferGeminiJsonSchemaTypeFromValue(source.const)
} else if (Array.isArray(source.enum) && source.enum.length > 0) {
result.enum = source.enum
type = type ?? inferGeminiJsonSchemaTypeFromEnum(source.enum)
}
if (!type) {
if (source.properties && typeof source.properties === 'object') {
type = 'object'
} else if (source.items !== undefined || source.prefixItems !== undefined) {
type = 'array'
}
}
if (source.nullable === true) {
type = addNullToGeminiJsonSchemaType(type)
}
if (type) {
result.type = type
}
if (typeof source.title === 'string') {
result.title = source.title
}
if (typeof source.description === 'string') {
result.description = source.description
}
if (typeof source.format === 'string') {
result.format = source.format
}
if (typeof source.pattern === 'string') {
result.pattern = source.pattern
}
if (typeof source.minimum === 'number') {
result.minimum = source.minimum
} else if (typeof source.exclusiveMinimum === 'number') {
result.minimum = source.exclusiveMinimum
}
if (typeof source.maximum === 'number') {
result.maximum = source.maximum
} else if (typeof source.exclusiveMaximum === 'number') {
result.maximum = source.exclusiveMaximum
}
if (typeof source.minItems === 'number') {
result.minItems = source.minItems
}
if (typeof source.maxItems === 'number') {
result.maxItems = source.maxItems
}
if (typeof source.minLength === 'number') {
result.minLength = source.minLength
}
if (typeof source.maxLength === 'number') {
result.maxLength = source.maxLength
}
if (typeof source.minProperties === 'number') {
result.minProperties = source.minProperties
}
if (typeof source.maxProperties === 'number') {
result.maxProperties = source.maxProperties
}
const properties = sanitizeGeminiJsonSchemaProperties(source.properties)
if (properties) {
result.properties = properties
result.propertyOrdering = Object.keys(properties)
}
if (Array.isArray(source.required)) {
const required = source.required.filter(
(item): item is string => typeof item === 'string',
)
if (required.length > 0) {
result.required = required
}
}
if (typeof source.additionalProperties === 'boolean') {
result.additionalProperties = source.additionalProperties
} else {
const additionalProperties = sanitizeGeminiJsonSchema(
source.additionalProperties,
)
if (Object.keys(additionalProperties).length > 0) {
result.additionalProperties = additionalProperties
}
}
const items = sanitizeGeminiJsonSchema(source.items)
if (Object.keys(items).length > 0) {
result.items = items
}
const prefixItems = sanitizeGeminiJsonSchemaArray(source.prefixItems)
if (prefixItems) {
result.prefixItems = prefixItems
}
const anyOf = sanitizeGeminiJsonSchemaArray(source.anyOf ?? source.oneOf)
if (anyOf) {
result.anyOf = anyOf
}
return result
}
function sanitizeGeminiFunctionParameters(
schema: unknown,
): Record<string, unknown> {
const sanitized = sanitizeGeminiJsonSchema(schema)
if (Object.keys(sanitized).length > 0) {
return sanitized
}
return {
type: 'object',
properties: {},
}
}
export function anthropicToolsToGemini(tools: BetaToolUnion[]): GeminiTool[] {
const functionDeclarations = tools
.filter(tool => {
const toolType = (tool as unknown as { type?: string }).type
return tool.type === 'custom' || !('type' in tool) || toolType !== 'server'
})
.map(tool => {
const anyTool = tool as unknown as Record<string, unknown>
const name = (anyTool.name as string) || ''
const description = (anyTool.description as string) || ''
const inputSchema =
(anyTool.input_schema as Record<string, unknown> | undefined) ?? {
type: 'object',
properties: {},
}
return {
name,
description,
parametersJsonSchema: sanitizeGeminiFunctionParameters(inputSchema),
}
})
return functionDeclarations.length > 0
? [{ functionDeclarations }]
: []
}
export function anthropicToolChoiceToGemini(
toolChoice: unknown,
): GeminiFunctionCallingConfig | undefined {
if (!toolChoice || typeof toolChoice !== 'object') return undefined
const tc = toolChoice as Record<string, unknown>
const type = tc.type as string
switch (type) {
case 'auto':
return { mode: 'AUTO' }
case 'any':
return { mode: 'ANY' }
case 'tool':
return {
mode: 'ANY',
allowedFunctionNames:
typeof tc.name === 'string' ? [tc.name] : undefined,
}
default:
return undefined
}
}

View File

@@ -0,0 +1,35 @@
function getModelFamily(model: string): 'haiku' | 'sonnet' | 'opus' | null {
if (/haiku/i.test(model)) return 'haiku'
if (/opus/i.test(model)) return 'opus'
if (/sonnet/i.test(model)) return 'sonnet'
return null
}
export function resolveGeminiModel(anthropicModel: string): string {
if (process.env.GEMINI_MODEL) {
return process.env.GEMINI_MODEL
}
const cleanModel = anthropicModel.replace(/\[1m\]$/i, '')
const family = getModelFamily(cleanModel)
if (!family) {
return cleanModel
}
const geminiEnvVar = `GEMINI_DEFAULT_${family.toUpperCase()}_MODEL`
const geminiModel = process.env[geminiEnvVar]
if (geminiModel) {
return geminiModel
}
const sharedEnvVar = `ANTHROPIC_DEFAULT_${family.toUpperCase()}_MODEL`
const resolvedModel = process.env[sharedEnvVar]
if (resolvedModel) {
return resolvedModel
}
throw new Error(
`Gemini provider requires GEMINI_MODEL or ${geminiEnvVar} (or ${sharedEnvVar} for backward compatibility) to be configured.`,
)
}

View File

@@ -0,0 +1,243 @@
import type { BetaRawMessageStreamEvent } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
import { randomUUID } from 'crypto'
import type { GeminiPart, GeminiStreamChunk } from './types.js'
export async function* adaptGeminiStreamToAnthropic(
stream: AsyncIterable<GeminiStreamChunk>,
model: string,
): AsyncGenerator<BetaRawMessageStreamEvent, void> {
const messageId = `msg_${randomUUID().replace(/-/g, '').slice(0, 24)}`
let started = false
let stopped = false
let nextContentIndex = 0
let openTextLikeBlock:
| { index: number; type: 'text' | 'thinking' }
| null = null
let sawToolUse = false
let finishReason: string | undefined
let inputTokens = 0
let outputTokens = 0
for await (const chunk of stream) {
const usage = chunk.usageMetadata
if (usage) {
inputTokens = usage.promptTokenCount ?? inputTokens
outputTokens =
(usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0)
}
if (!started) {
started = true
yield {
type: 'message_start',
message: {
id: messageId,
type: 'message',
role: 'assistant',
content: [],
model,
stop_reason: null,
stop_sequence: null,
usage: {
input_tokens: inputTokens,
output_tokens: 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
},
},
} as unknown as BetaRawMessageStreamEvent
}
const candidate = chunk.candidates?.[0]
const parts = candidate?.content?.parts ?? []
for (const part of parts) {
if (part.functionCall) {
if (openTextLikeBlock) {
yield {
type: 'content_block_stop',
index: openTextLikeBlock.index,
} as BetaRawMessageStreamEvent
openTextLikeBlock = null
}
sawToolUse = true
const toolIndex = nextContentIndex++
const toolId = `toolu_${randomUUID().replace(/-/g, '').slice(0, 24)}`
yield {
type: 'content_block_start',
index: toolIndex,
content_block: {
type: 'tool_use',
id: toolId,
name: part.functionCall.name || '',
input: {},
},
} as BetaRawMessageStreamEvent
if (part.thoughtSignature) {
yield {
type: 'content_block_delta',
index: toolIndex,
delta: {
type: 'signature_delta',
signature: part.thoughtSignature,
},
} as BetaRawMessageStreamEvent
}
if (part.functionCall.args && Object.keys(part.functionCall.args).length > 0) {
yield {
type: 'content_block_delta',
index: toolIndex,
delta: {
type: 'input_json_delta',
partial_json: JSON.stringify(part.functionCall.args),
},
} as BetaRawMessageStreamEvent
}
yield {
type: 'content_block_stop',
index: toolIndex,
} as BetaRawMessageStreamEvent
continue
}
const textLikeType = getTextLikeBlockType(part)
if (textLikeType) {
if (!openTextLikeBlock || openTextLikeBlock.type !== textLikeType) {
if (openTextLikeBlock) {
yield {
type: 'content_block_stop',
index: openTextLikeBlock.index,
} as BetaRawMessageStreamEvent
}
openTextLikeBlock = {
index: nextContentIndex++,
type: textLikeType,
}
yield {
type: 'content_block_start',
index: openTextLikeBlock.index,
content_block:
textLikeType === 'thinking'
? {
type: 'thinking',
thinking: '',
signature: '',
}
: {
type: 'text',
text: '',
},
} as BetaRawMessageStreamEvent
}
if (part.text) {
yield {
type: 'content_block_delta',
index: openTextLikeBlock.index,
delta:
textLikeType === 'thinking'
? {
type: 'thinking_delta',
thinking: part.text,
}
: {
type: 'text_delta',
text: part.text,
},
} as BetaRawMessageStreamEvent
}
if (part.thoughtSignature) {
yield {
type: 'content_block_delta',
index: openTextLikeBlock.index,
delta: {
type: 'signature_delta',
signature: part.thoughtSignature,
},
} as BetaRawMessageStreamEvent
}
continue
}
if (part.thoughtSignature && openTextLikeBlock) {
yield {
type: 'content_block_delta',
index: openTextLikeBlock.index,
delta: {
type: 'signature_delta',
signature: part.thoughtSignature,
},
} as BetaRawMessageStreamEvent
}
}
if (candidate?.finishReason) {
finishReason = candidate.finishReason
}
}
if (!started) {
return
}
if (openTextLikeBlock) {
yield {
type: 'content_block_stop',
index: openTextLikeBlock.index,
} as BetaRawMessageStreamEvent
}
if (!stopped) {
yield {
type: 'message_delta',
delta: {
stop_reason: mapGeminiFinishReason(finishReason, sawToolUse),
stop_sequence: null,
},
usage: {
output_tokens: outputTokens,
},
} as BetaRawMessageStreamEvent
yield {
type: 'message_stop',
} as BetaRawMessageStreamEvent
stopped = true
}
}
function getTextLikeBlockType(
part: GeminiPart,
): 'text' | 'thinking' | null {
if (typeof part.text !== 'string') {
return null
}
return part.thought ? 'thinking' : 'text'
}
function mapGeminiFinishReason(
reason: string | undefined,
sawToolUse: boolean,
): string {
switch (reason) {
case 'MAX_TOKENS':
return 'max_tokens'
case 'STOP':
case 'FINISH_REASON_UNSPECIFIED':
case 'SAFETY':
case 'RECITATION':
case 'BLOCKLIST':
case 'PROHIBITED_CONTENT':
case 'SPII':
case 'MALFORMED_FUNCTION_CALL':
default:
return sawToolUse ? 'tool_use' : 'end_turn'
}
}

View File

@@ -0,0 +1,86 @@
export const GEMINI_THOUGHT_SIGNATURE_FIELD = '_geminiThoughtSignature'
export type GeminiFunctionCall = {
name?: string
args?: Record<string, unknown>
}
export type GeminiFunctionResponse = {
name?: string
response?: Record<string, unknown>
}
export type GeminiInlineData = {
mimeType: string
data: string
}
export type GeminiPart = {
text?: string
thought?: boolean
thoughtSignature?: string
functionCall?: GeminiFunctionCall
functionResponse?: GeminiFunctionResponse
inlineData?: GeminiInlineData
}
export type GeminiContent = {
role: 'user' | 'model'
parts: GeminiPart[]
}
export type GeminiFunctionDeclaration = {
name: string
description?: string
parameters?: Record<string, unknown>
parametersJsonSchema?: Record<string, unknown>
}
export type GeminiTool = {
functionDeclarations: GeminiFunctionDeclaration[]
}
export type GeminiFunctionCallingConfig = {
mode: 'AUTO' | 'ANY' | 'NONE'
allowedFunctionNames?: string[]
}
export type GeminiGenerateContentRequest = {
contents: GeminiContent[]
systemInstruction?: {
parts: Array<{ text: string }>
}
tools?: GeminiTool[]
toolConfig?: {
functionCallingConfig: GeminiFunctionCallingConfig
}
generationConfig?: {
temperature?: number
thinkingConfig?: {
includeThoughts?: boolean
thinkingBudget?: number
}
}
}
export type GeminiUsageMetadata = {
promptTokenCount?: number
candidatesTokenCount?: number
thoughtsTokenCount?: number
totalTokenCount?: number
}
export type GeminiCandidate = {
content?: {
role?: string
parts?: GeminiPart[]
}
finishReason?: string
index?: number
}
export type GeminiStreamChunk = {
candidates?: GeminiCandidate[]
usageMetadata?: GeminiUsageMetadata
modelVersion?: string
}

View File

@@ -0,0 +1,83 @@
/**
* Default mapping from Anthropic model names to Grok model names.
*
* Users can override per-family via GROK_DEFAULT_{FAMILY}_MODEL env vars,
* or override the entire mapping via GROK_MODEL_MAP env var (JSON string).
*/
const DEFAULT_MODEL_MAP: Record<string, string> = {
'claude-sonnet-4-20250514': 'grok-3-mini-fast',
'claude-sonnet-4-5-20250929': 'grok-3-mini-fast',
'claude-sonnet-4-6': 'grok-3-mini-fast',
'claude-opus-4-20250514': 'grok-4.20-reasoning',
'claude-opus-4-1-20250805': 'grok-4.20-reasoning',
'claude-opus-4-5-20251101': 'grok-4.20-reasoning',
'claude-opus-4-6': 'grok-4.20-reasoning',
'claude-haiku-4-5-20251001': 'grok-3-mini-fast',
'claude-3-5-haiku-20241022': 'grok-3-mini-fast',
'claude-3-7-sonnet-20250219': 'grok-3-mini-fast',
'claude-3-5-sonnet-20241022': 'grok-3-mini-fast',
}
const DEFAULT_FAMILY_MAP: Record<string, string> = {
opus: 'grok-4.20-reasoning',
sonnet: 'grok-3-mini-fast',
haiku: 'grok-3-mini-fast',
}
function getModelFamily(model: string): 'haiku' | 'sonnet' | 'opus' | null {
if (/haiku/i.test(model)) return 'haiku'
if (/opus/i.test(model)) return 'opus'
if (/sonnet/i.test(model)) return 'sonnet'
return null
}
function getUserModelMap(): Record<string, string> | null {
const raw = process.env.GROK_MODEL_MAP
if (!raw) return null
try {
const parsed = JSON.parse(raw)
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed as Record<string, string>
}
} catch {
// ignore invalid JSON
}
return null
}
/**
* Resolve the Grok model name for a given Anthropic model.
*/
export function resolveGrokModel(anthropicModel: string): string {
if (process.env.GROK_MODEL) {
return process.env.GROK_MODEL
}
const cleanModel = anthropicModel.replace(/\[1m\]$/, '')
const family = getModelFamily(cleanModel)
const userMap = getUserModelMap()
if (userMap && family && userMap[family]) {
return userMap[family]
}
if (family) {
const grokEnvVar = `GROK_DEFAULT_${family.toUpperCase()}_MODEL`
const grokOverride = process.env[grokEnvVar]
if (grokOverride) return grokOverride
const anthropicEnvVar = `ANTHROPIC_DEFAULT_${family.toUpperCase()}_MODEL`
const anthropicOverride = process.env[anthropicEnvVar]
if (anthropicOverride) return anthropicOverride
}
if (DEFAULT_MODEL_MAP[cleanModel]) {
return DEFAULT_MODEL_MAP[cleanModel]
}
if (family && DEFAULT_FAMILY_MAP[family]) {
return DEFAULT_FAMILY_MAP[family]
}
return cleanModel
}

View File

@@ -0,0 +1,55 @@
/**
* Default mapping from Anthropic model names to OpenAI model names.
* Used only when ANTHROPIC_DEFAULT_*_MODEL env vars are not set.
*/
const DEFAULT_MODEL_MAP: Record<string, string> = {
'claude-sonnet-4-20250514': 'gpt-4o',
'claude-sonnet-4-5-20250929': 'gpt-4o',
'claude-sonnet-4-6': 'gpt-4o',
'claude-opus-4-20250514': 'o3',
'claude-opus-4-1-20250805': 'o3',
'claude-opus-4-5-20251101': 'o3',
'claude-opus-4-6': 'o3',
'claude-haiku-4-5-20251001': 'gpt-4o-mini',
'claude-3-5-haiku-20241022': 'gpt-4o-mini',
'claude-3-7-sonnet-20250219': 'gpt-4o',
'claude-3-5-sonnet-20241022': 'gpt-4o',
}
function getModelFamily(model: string): 'haiku' | 'sonnet' | 'opus' | null {
if (/haiku/i.test(model)) return 'haiku'
if (/opus/i.test(model)) return 'opus'
if (/sonnet/i.test(model)) return 'sonnet'
return null
}
/**
* Resolve the OpenAI model name for a given Anthropic model.
*
* Priority:
* 1. OPENAI_MODEL env var (override all)
* 2. OPENAI_DEFAULT_{FAMILY}_MODEL env var (e.g. OPENAI_DEFAULT_SONNET_MODEL)
* 3. ANTHROPIC_DEFAULT_{FAMILY}_MODEL env var (backward compatibility)
* 4. DEFAULT_MODEL_MAP lookup
* 5. Pass through original model name
*/
export function resolveOpenAIModel(anthropicModel: string): string {
if (process.env.OPENAI_MODEL) {
return process.env.OPENAI_MODEL
}
const cleanModel = anthropicModel.replace(/\[1m\]$/, '')
const family = getModelFamily(cleanModel)
if (family) {
const openaiEnvVar = `OPENAI_DEFAULT_${family.toUpperCase()}_MODEL`
const openaiOverride = process.env[openaiEnvVar]
if (openaiOverride) return openaiOverride
const anthropicEnvVar = `ANTHROPIC_DEFAULT_${family.toUpperCase()}_MODEL`
const anthropicOverride = process.env[anthropicEnvVar]
if (anthropicOverride) return anthropicOverride
}
return DEFAULT_MODEL_MAP[cleanModel] ?? cleanModel
}

View File

@@ -0,0 +1,304 @@
import type {
BetaContentBlockParam,
BetaToolResultBlockParam,
BetaToolUseBlock,
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
import type {
ChatCompletionAssistantMessageParam,
ChatCompletionMessageParam,
ChatCompletionSystemMessageParam,
ChatCompletionToolMessageParam,
ChatCompletionUserMessageParam,
} from 'openai/resources/chat/completions/completions.mjs'
import type { AssistantMessage, UserMessage } from '../types/message.js'
import type { SystemPrompt } from '../types/systemPrompt.js'
export interface ConvertMessagesOptions {
/** When true, preserve thinking blocks as reasoning_content on assistant messages
* (required for DeepSeek thinking mode with tool calls). */
enableThinking?: boolean
}
/**
* Convert internal (UserMessage | AssistantMessage)[] to OpenAI-format messages.
*
* Key conversions:
* - system prompt → role: "system" message prepended
* - tool_use blocks → tool_calls[] on assistant message
* - tool_result blocks → role: "tool" messages
* - thinking blocks → silently dropped (or preserved as reasoning_content when enableThinking=true)
* - cache_control → stripped
*/
export function anthropicMessagesToOpenAI(
messages: (UserMessage | AssistantMessage)[],
systemPrompt: SystemPrompt,
options?: ConvertMessagesOptions,
): ChatCompletionMessageParam[] {
const result: ChatCompletionMessageParam[] = []
const enableThinking = options?.enableThinking ?? false
// Prepend system prompt as system message
const systemText = systemPromptToText(systemPrompt)
if (systemText) {
result.push({
role: 'system',
content: systemText,
} satisfies ChatCompletionSystemMessageParam)
}
// When thinking mode is on, detect turn boundaries so that reasoning_content
// from *previous* user turns is stripped (saves bandwidth; DeepSeek ignores it).
// A "new turn" starts when a user text message appears after at least one assistant response.
const turnBoundaries = new Set<number>()
if (enableThinking) {
let hasSeenAssistant = false
for (let i = 0; i < messages.length; i++) {
const msg = messages[i]
if (msg.type === 'assistant') {
hasSeenAssistant = true
}
if (msg.type === 'user' && hasSeenAssistant) {
const content = msg.message.content
// A user message starts a new turn if it contains any non-tool_result content
// (text, image, or other media). Tool results alone do NOT start a new turn
// because they are continuations of the previous assistant tool call.
const startsNewUserTurn = typeof content === 'string'
? content.length > 0
: Array.isArray(content) && content.some(
(b: any) =>
typeof b === 'string' ||
(b &&
typeof b === 'object' &&
'type' in b &&
b.type !== 'tool_result'),
)
if (startsNewUserTurn) {
turnBoundaries.add(i)
}
}
}
}
for (let i = 0; i < messages.length; i++) {
const msg = messages[i]
switch (msg.type) {
case 'user':
result.push(...convertInternalUserMessage(msg))
break
case 'assistant':
// Preserve reasoning_content unless we're before a turn boundary
// (i.e., from a previous user Q&A round)
const preserveReasoning = enableThinking && !isBeforeAnyTurnBoundary(i, turnBoundaries)
result.push(...convertInternalAssistantMessage(msg, preserveReasoning))
break
default:
break
}
}
return result
}
function systemPromptToText(systemPrompt: SystemPrompt): string {
if (!systemPrompt || systemPrompt.length === 0) return ''
return systemPrompt
.filter(Boolean)
.join('\n\n')
}
/**
* Check if index `i` falls before any turn boundary (i.e. it belongs to a previous turn).
* A message at index i is "before" a boundary if there exists a boundary j where i < j.
*/
function isBeforeAnyTurnBoundary(i: number, boundaries: Set<number>): boolean {
for (const b of boundaries) {
if (i < b) return true
}
return false
}
function convertInternalUserMessage(
msg: UserMessage,
): ChatCompletionMessageParam[] {
const result: ChatCompletionMessageParam[] = []
const content = msg.message.content
if (typeof content === 'string') {
result.push({
role: 'user',
content,
} satisfies ChatCompletionUserMessageParam)
} else if (Array.isArray(content)) {
const textParts: string[] = []
const toolResults: BetaToolResultBlockParam[] = []
const imageParts: Array<{ type: 'image_url'; image_url: { url: string } }> = []
for (const block of content) {
if (typeof block === 'string') {
textParts.push(block)
} else if (block.type === 'text') {
textParts.push(block.text)
} else if (block.type === 'tool_result') {
toolResults.push(block as BetaToolResultBlockParam)
} else if (block.type === 'image') {
const imagePart = convertImageBlockToOpenAI(block as unknown as Record<string, unknown>)
if (imagePart) {
imageParts.push(imagePart)
}
}
}
// CRITICAL: tool messages must come BEFORE any user message in the result.
// OpenAI API requires that a tool message immediately follows the assistant
// message with tool_calls. If we emit a user message first, the API will
// reject the request with "insufficient tool messages following tool_calls".
for (const tr of toolResults) {
result.push(convertToolResult(tr))
}
// 如果有图片,构建多模态 content 数组
if (imageParts.length > 0) {
const multiContent: Array<{ type: 'text'; text: string } | { type: 'image_url'; image_url: { url: string } }> = []
if (textParts.length > 0) {
multiContent.push({ type: 'text', text: textParts.join('\n') })
}
multiContent.push(...imageParts)
result.push({
role: 'user',
content: multiContent,
} satisfies ChatCompletionUserMessageParam)
} else if (textParts.length > 0) {
result.push({
role: 'user',
content: textParts.join('\n'),
} satisfies ChatCompletionUserMessageParam)
}
}
return result
}
function convertToolResult(
block: BetaToolResultBlockParam,
): ChatCompletionToolMessageParam {
let content: string
if (typeof block.content === 'string') {
content = block.content
} else if (Array.isArray(block.content)) {
content = block.content
.map(c => {
if (typeof c === 'string') return c
if ('text' in c) return c.text
return ''
})
.filter(Boolean)
.join('\n')
} else {
content = ''
}
return {
role: 'tool',
tool_call_id: block.tool_use_id,
content,
} satisfies ChatCompletionToolMessageParam
}
function convertInternalAssistantMessage(
msg: AssistantMessage,
preserveReasoning = false,
): ChatCompletionMessageParam[] {
const content = msg.message.content
if (typeof content === 'string') {
return [
{
role: 'assistant',
content,
} satisfies ChatCompletionAssistantMessageParam,
]
}
if (!Array.isArray(content)) {
return [
{
role: 'assistant',
content: '',
} satisfies ChatCompletionAssistantMessageParam,
]
}
const textParts: string[] = []
const toolCalls: NonNullable<ChatCompletionAssistantMessageParam['tool_calls']> = []
const reasoningParts: string[] = []
for (const block of content) {
if (typeof block === 'string') {
textParts.push(block)
} else if (block.type === 'text') {
textParts.push(block.text)
} else if (block.type === 'tool_use') {
const tu = block as BetaToolUseBlock
toolCalls.push({
id: tu.id,
type: 'function',
function: {
name: tu.name,
arguments:
typeof tu.input === 'string' ? tu.input : JSON.stringify(tu.input),
},
})
} else if (block.type === 'thinking' && preserveReasoning) {
// DeepSeek thinking mode: preserve reasoning_content for tool call iterations
const thinkingText = (block as unknown as Record<string, unknown>).thinking
if (typeof thinkingText === 'string' && thinkingText) {
reasoningParts.push(thinkingText)
}
}
// Skip redacted_thinking, server_tool_use, etc.
}
const result: ChatCompletionAssistantMessageParam = {
role: 'assistant',
content: textParts.length > 0 ? textParts.join('\n') : null,
...(toolCalls.length > 0 && { tool_calls: toolCalls }),
...(reasoningParts.length > 0 && { reasoning_content: reasoningParts.join('\n') }),
}
return [result]
}
/**
* 将 Anthropic image 块转换为 OpenAI image_url 格式。
*
* Anthropic 格式: { type: "image", source: { type: "base64", media_type: "image/png", data: "..." } }
* OpenAI 格式: { type: "image_url", image_url: { url: "data:image/png;base64,..." } }
*/
function convertImageBlockToOpenAI(
block: Record<string, unknown>,
): { type: 'image_url'; image_url: { url: string } } | null {
const source = block.source as Record<string, unknown> | undefined
if (!source) return null
if (source.type === 'base64' && typeof source.data === 'string') {
const mediaType = (source.media_type as string) || 'image/png'
return {
type: 'image_url',
image_url: {
url: `data:${mediaType};base64,${source.data}`,
},
}
}
// url 类型的图片直接传递
if (source.type === 'url' && typeof source.url === 'string') {
return {
type: 'image_url',
image_url: {
url: source.url,
},
}
}
return null
}

View File

@@ -0,0 +1,123 @@
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
import type { ChatCompletionTool } from 'openai/resources/chat/completions/completions.mjs'
/**
* Convert Anthropic tool schemas to OpenAI function calling format.
*
* Anthropic: { name, description, input_schema }
* OpenAI: { type: "function", function: { name, description, parameters } }
*
* Anthropic-specific fields (cache_control, defer_loading, etc.) are stripped.
*/
export function anthropicToolsToOpenAI(
tools: BetaToolUnion[],
): ChatCompletionTool[] {
return tools
.filter(tool => {
// Only convert standard tools (skip server tools like computer_use, etc.)
const toolType = (tool as unknown as { type?: string }).type
return tool.type === 'custom' || !('type' in tool) || toolType !== 'server'
})
.map(tool => {
// Handle the various tool shapes from Anthropic SDK
const anyTool = tool as unknown as Record<string, unknown>
const name = (anyTool.name as string) || ''
const description = (anyTool.description as string) || ''
const inputSchema = anyTool.input_schema as Record<string, unknown> | undefined
return {
type: 'function' as const,
function: {
name,
description,
parameters: sanitizeJsonSchema(inputSchema || { type: 'object', properties: {} }),
},
} satisfies ChatCompletionTool
})
}
/**
* Recursively sanitize a JSON Schema for OpenAI-compatible providers.
*
* Many OpenAI-compatible endpoints (Ollama, DeepSeek, vLLM, etc.) do not
* support the `const` keyword in JSON Schema. Convert it to `enum` with a
* single-element array, which is semantically equivalent.
*/
function sanitizeJsonSchema(schema: Record<string, unknown>): Record<string, unknown> {
if (!schema || typeof schema !== 'object') return schema
const result = { ...schema }
// Convert `const` → `enum: [value]`
if ('const' in result) {
result.enum = [result.const]
delete result.const
}
// Recursively process nested schemas
const objectKeys = ['properties', 'definitions', '$defs', 'patternProperties'] as const
for (const key of objectKeys) {
const nested = result[key]
if (nested && typeof nested === 'object') {
const sanitized: Record<string, unknown> = {}
for (const [k, v] of Object.entries(nested as Record<string, unknown>)) {
sanitized[k] = v && typeof v === 'object' ? sanitizeJsonSchema(v as Record<string, unknown>) : v
}
result[key] = sanitized
}
}
// Recursively process single-schema keys
const singleKeys = ['items', 'additionalProperties', 'not', 'if', 'then', 'else', 'contains', 'propertyNames'] as const
for (const key of singleKeys) {
const nested = result[key]
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
result[key] = sanitizeJsonSchema(nested as Record<string, unknown>)
}
}
// Recursively process array-of-schemas keys
const arrayKeys = ['anyOf', 'oneOf', 'allOf'] as const
for (const key of arrayKeys) {
const nested = result[key]
if (Array.isArray(nested)) {
result[key] = nested.map(item =>
item && typeof item === 'object' ? sanitizeJsonSchema(item as Record<string, unknown>) : item
)
}
}
return result
}
/**
* Map Anthropic tool_choice to OpenAI tool_choice format.
*
* Anthropic → OpenAI:
* - { type: "auto" } → "auto"
* - { type: "any" } → "required"
* - { type: "tool", name } → { type: "function", function: { name } }
* - undefined → undefined (use provider default)
*/
export function anthropicToolChoiceToOpenAI(
toolChoice: unknown,
): string | { type: 'function'; function: { name: string } } | undefined {
if (!toolChoice || typeof toolChoice !== 'object') return undefined
const tc = toolChoice as Record<string, unknown>
const type = tc.type as string
switch (type) {
case 'auto':
return 'auto'
case 'any':
return 'required'
case 'tool':
return {
type: 'function',
function: { name: tc.name as string },
}
default:
return undefined
}
}

View File

@@ -0,0 +1,327 @@
import type { BetaRawMessageStreamEvent } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
import type { ChatCompletionChunk } from 'openai/resources/chat/completions/completions.mjs'
import { randomUUID } from 'crypto'
/**
* Adapt an OpenAI streaming response into Anthropic BetaRawMessageStreamEvent.
*
* Mapping:
* First chunk → message_start
* delta.reasoning_content → content_block_start(thinking) + thinking_delta + content_block_stop
* delta.content → content_block_start(text) + text_delta + content_block_stop
* delta.tool_calls → content_block_start(tool_use) + input_json_delta + content_block_stop
* finish_reason → message_delta(stop_reason) + message_stop
*
* Usage field mapping (OpenAI → Anthropic):
* prompt_tokens → input_tokens
* completion_tokens → output_tokens
* prompt_tokens_details.cached_tokens → cache_read_input_tokens
* (no OpenAI equivalent) → cache_creation_input_tokens (always 0)
*
* All four fields are emitted in the post-loop message_delta (not message_start)
* so that trailing usage chunks (sent after finish_reason by some
* OpenAI-compatible endpoints) are fully captured before the final counts are reported.
*
* Thinking support:
* DeepSeek and compatible providers send `delta.reasoning_content` for chain-of-thought.
* This is mapped to Anthropic's `thinking` content blocks:
* content_block_start: { type: 'thinking', thinking: '', signature: '' }
* content_block_delta: { type: 'thinking_delta', thinking: '...' }
*
* Prompt caching:
* OpenAI reports cached tokens in usage.prompt_tokens_details.cached_tokens.
* This is mapped to Anthropic's cache_read_input_tokens.
*/
export async function* adaptOpenAIStreamToAnthropic(
stream: AsyncIterable<ChatCompletionChunk>,
model: string,
): AsyncGenerator<BetaRawMessageStreamEvent, void> {
const messageId = `msg_${randomUUID().replace(/-/g, '').slice(0, 24)}`
let started = false
let currentContentIndex = -1
// Track tool_use blocks: tool_calls index → { contentIndex, id, name, arguments }
const toolBlocks = new Map<number, { contentIndex: number; id: string; name: string; arguments: string }>()
// Track thinking block state
let thinkingBlockOpen = false
// Track text block state
let textBlockOpen = false
// Track usage — all four Anthropic fields, populated from OpenAI usage fields:
let inputTokens = 0
let outputTokens = 0
let cachedReadTokens = 0
// Track all open content block indices (for cleanup)
const openBlockIndices = new Set<number>()
// Deferred finish state
let pendingFinishReason: string | null = null
let pendingHasToolCalls = false
for await (const chunk of stream) {
const choice = chunk.choices?.[0]
const delta = choice?.delta
// Extract usage from any chunk that carries it.
if (chunk.usage) {
inputTokens = chunk.usage.prompt_tokens ?? inputTokens
outputTokens = chunk.usage.completion_tokens ?? outputTokens
const details = (chunk.usage as any).prompt_tokens_details
if (details?.cached_tokens != null) {
cachedReadTokens = details.cached_tokens
}
}
// Emit message_start on first chunk
if (!started) {
started = true
yield {
type: 'message_start',
message: {
id: messageId,
type: 'message',
role: 'assistant',
content: [],
model,
stop_reason: null,
stop_sequence: null,
usage: {
input_tokens: inputTokens,
output_tokens: 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: cachedReadTokens,
},
},
} as unknown as BetaRawMessageStreamEvent
}
// Skip chunks that carry only usage data (no delta content)
if (!delta) continue
// Handle reasoning_content → Anthropic thinking block
const reasoningContent = (delta as any).reasoning_content
if (reasoningContent != null && reasoningContent !== '') {
if (!thinkingBlockOpen) {
currentContentIndex++
thinkingBlockOpen = true
openBlockIndices.add(currentContentIndex)
yield {
type: 'content_block_start',
index: currentContentIndex,
content_block: {
type: 'thinking',
thinking: '',
signature: '',
},
} as BetaRawMessageStreamEvent
}
yield {
type: 'content_block_delta',
index: currentContentIndex,
delta: {
type: 'thinking_delta',
thinking: reasoningContent,
},
} as BetaRawMessageStreamEvent
}
// Handle text content
if (delta.content != null && delta.content !== '') {
if (!textBlockOpen) {
// Close thinking block if still open
if (thinkingBlockOpen) {
yield {
type: 'content_block_stop',
index: currentContentIndex,
} as BetaRawMessageStreamEvent
openBlockIndices.delete(currentContentIndex)
thinkingBlockOpen = false
}
currentContentIndex++
textBlockOpen = true
openBlockIndices.add(currentContentIndex)
yield {
type: 'content_block_start',
index: currentContentIndex,
content_block: {
type: 'text',
text: '',
},
} as BetaRawMessageStreamEvent
}
yield {
type: 'content_block_delta',
index: currentContentIndex,
delta: {
type: 'text_delta',
text: delta.content,
},
} as BetaRawMessageStreamEvent
}
// Handle tool calls
if (delta.tool_calls) {
for (const tc of delta.tool_calls) {
const tcIndex = tc.index
if (!toolBlocks.has(tcIndex)) {
// Close thinking block if open
if (thinkingBlockOpen) {
yield {
type: 'content_block_stop',
index: currentContentIndex,
} as BetaRawMessageStreamEvent
openBlockIndices.delete(currentContentIndex)
thinkingBlockOpen = false
}
// Close text block if open
if (textBlockOpen) {
yield {
type: 'content_block_stop',
index: currentContentIndex,
} as BetaRawMessageStreamEvent
openBlockIndices.delete(currentContentIndex)
textBlockOpen = false
}
// Start new tool_use block
currentContentIndex++
const toolId = tc.id || `toolu_${randomUUID().replace(/-/g, '').slice(0, 24)}`
const toolName = tc.function?.name || ''
toolBlocks.set(tcIndex, {
contentIndex: currentContentIndex,
id: toolId,
name: toolName,
arguments: '',
})
openBlockIndices.add(currentContentIndex)
yield {
type: 'content_block_start',
index: currentContentIndex,
content_block: {
type: 'tool_use',
id: toolId,
name: toolName,
input: {},
},
} as BetaRawMessageStreamEvent
}
// Stream argument fragments
const argFragment = tc.function?.arguments
if (argFragment) {
toolBlocks.get(tcIndex)!.arguments += argFragment
yield {
type: 'content_block_delta',
index: toolBlocks.get(tcIndex)!.contentIndex,
delta: {
type: 'input_json_delta',
partial_json: argFragment,
},
} as BetaRawMessageStreamEvent
}
}
}
// Handle finish
if (choice?.finish_reason) {
if (thinkingBlockOpen) {
yield {
type: 'content_block_stop',
index: currentContentIndex,
} as BetaRawMessageStreamEvent
openBlockIndices.delete(currentContentIndex)
thinkingBlockOpen = false
}
if (textBlockOpen) {
yield {
type: 'content_block_stop',
index: currentContentIndex,
} as BetaRawMessageStreamEvent
openBlockIndices.delete(currentContentIndex)
textBlockOpen = false
}
for (const [, block] of toolBlocks) {
if (openBlockIndices.has(block.contentIndex)) {
yield {
type: 'content_block_stop',
index: block.contentIndex,
} as BetaRawMessageStreamEvent
openBlockIndices.delete(block.contentIndex)
}
}
pendingFinishReason = choice.finish_reason
pendingHasToolCalls = toolBlocks.size > 0
}
}
// Safety: close any remaining open blocks
for (const idx of openBlockIndices) {
yield {
type: 'content_block_stop',
index: idx,
} as BetaRawMessageStreamEvent
}
// Emit message_delta + message_stop
if (pendingFinishReason !== null) {
const stopReason =
pendingFinishReason === 'length'
? 'max_tokens'
: pendingHasToolCalls
? 'tool_use'
: mapFinishReason(pendingFinishReason)
yield {
type: 'message_delta',
delta: {
stop_reason: stopReason,
stop_sequence: null,
},
usage: {
input_tokens: inputTokens,
output_tokens: outputTokens,
cache_read_input_tokens: cachedReadTokens,
cache_creation_input_tokens: 0,
},
} as BetaRawMessageStreamEvent
yield {
type: 'message_stop',
} as BetaRawMessageStreamEvent
}
}
/**
* Map OpenAI finish_reason to Anthropic stop_reason.
*/
function mapFinishReason(reason: string): string {
switch (reason) {
case 'stop':
return 'end_turn'
case 'tool_calls':
return 'tool_use'
case 'length':
return 'max_tokens'
case 'content_filter':
return 'end_turn'
default:
return 'end_turn'
}
}

View File

@@ -0,0 +1,54 @@
// Error type constants for the model provider package.
// Error string constants extracted from src/services/api/errors.ts.
// The full error handling functions remain in the main project (Phase 4).
export const API_ERROR_MESSAGE_PREFIX = 'API Error'
export const PROMPT_TOO_LONG_ERROR_MESSAGE = 'Prompt is too long'
export const CREDIT_BALANCE_TOO_LOW_ERROR_MESSAGE = 'Credit balance is too low'
export const INVALID_API_KEY_ERROR_MESSAGE = 'Not logged in · Please run /login'
export const INVALID_API_KEY_ERROR_MESSAGE_EXTERNAL =
'Invalid API key · Fix external API key'
export const ORG_DISABLED_ERROR_MESSAGE_ENV_KEY_WITH_OAUTH =
'Your ANTHROPIC_API_KEY belongs to a disabled organization · Unset the environment variable to use your subscription instead'
export const ORG_DISABLED_ERROR_MESSAGE_ENV_KEY =
'Your ANTHROPIC_API_KEY belongs to a disabled organization · Update or unset the environment variable'
export const TOKEN_REVOKED_ERROR_MESSAGE =
'OAuth token revoked · Please run /login'
export const CCR_AUTH_ERROR_MESSAGE =
'Authentication error · This may be a temporary network issue, please try again'
export const REPEATED_529_ERROR_MESSAGE = 'Repeated 529 Overloaded errors'
export const CUSTOM_OFF_SWITCH_MESSAGE =
'Opus is experiencing high load, please use /model to switch to Sonnet'
export const API_TIMEOUT_ERROR_MESSAGE = 'Request timed out'
export const OAUTH_ORG_NOT_ALLOWED_ERROR_MESSAGE =
'Your account does not have access to Claude Code. Please run /login.'
/** Error classification types returned by classifyAPIError */
export type APIErrorClassification =
| 'aborted'
| 'api_timeout'
| 'repeated_529'
| 'capacity_off_switch'
| 'rate_limit'
| 'server_overload'
| 'prompt_too_long'
| 'pdf_too_large'
| 'pdf_password_protected'
| 'image_too_large'
| 'tool_use_mismatch'
| 'unexpected_tool_result'
| 'duplicate_tool_use_id'
| 'invalid_model'
| 'credit_balance_low'
| 'invalid_api_key'
| 'token_revoked'
| 'oauth_org_not_allowed'
| 'auth_error'
| 'bedrock_model_access'
| 'server_error'
| 'client_error'
| 'ssl_cert_error'
| 'connection_error'
| 'unknown'

View File

@@ -0,0 +1,6 @@
// Type definitions for @anthropic-ai/model-provider
export * from './message.js'
export * from './usage.js'
export * from './errors.js'
export * from './systemPrompt.js'

View File

@@ -0,0 +1,129 @@
// Core message types for the model provider package.
// Moved from src/types/message.ts to decouple the API layer from the main project.
import type { UUID } from 'crypto'
import type {
ContentBlockParam,
ContentBlock,
} from '@anthropic-ai/sdk/resources/index.mjs'
import type { BetaUsage } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
/**
* Base message type with discriminant `type` field and common properties.
* Individual message subtypes (UserMessage, AssistantMessage, etc.) extend
* this with narrower `type` literals and additional fields.
*/
export type MessageType = 'user' | 'assistant' | 'system' | 'attachment' | 'progress' | 'grouped_tool_use' | 'collapsed_read_search'
/** A single content element inside message.content arrays. */
export type ContentItem = ContentBlockParam | ContentBlock
export type MessageContent = string | ContentBlockParam[] | ContentBlock[]
/**
* Typed content array — used in narrowed message subtypes so that
* `message.content[0]` resolves to `ContentItem` instead of
* `string | ContentBlockParam | ContentBlock`.
*/
export type TypedMessageContent = ContentItem[]
export type Message = {
type: MessageType
uuid: UUID
isMeta?: boolean
isCompactSummary?: boolean
toolUseResult?: unknown
isVisibleInTranscriptOnly?: boolean
attachment?: { type: string; toolUseID?: string; [key: string]: unknown; addedNames: string[]; addedLines: string[]; removedNames: string[] }
message?: {
role?: string
id?: string
content?: MessageContent
usage?: BetaUsage | Record<string, unknown>
[key: string]: unknown
}
[key: string]: unknown
}
export type AssistantMessage = Message & {
type: 'assistant'
message: NonNullable<Message['message']>
}
export type AttachmentMessage<T = { type: string; [key: string]: unknown }> = Message & { type: 'attachment'; attachment: T }
export type ProgressMessage<T = unknown> = Message & { type: 'progress'; data: T }
export type SystemLocalCommandMessage = Message & { type: 'system' }
export type SystemMessage = Message & { type: 'system' }
export type UserMessage = Message & {
type: 'user'
message: NonNullable<Message['message']>
imagePasteIds?: number[]
}
export type NormalizedUserMessage = UserMessage
export type RequestStartEvent = { type: string; [key: string]: unknown }
export type StreamEvent = { type: string; [key: string]: unknown }
export type SystemCompactBoundaryMessage = Message & {
type: 'system'
compactMetadata: {
preservedSegment?: {
headUuid: UUID
tailUuid: UUID
anchorUuid: UUID
[key: string]: unknown
}
[key: string]: unknown
}
}
export type TombstoneMessage = Message
export type ToolUseSummaryMessage = Message
export type MessageOrigin = string
export type CompactMetadata = Record<string, unknown>
export type SystemAPIErrorMessage = Message & { type: 'system' }
export type SystemFileSnapshotMessage = Message & { type: 'system' }
export type NormalizedAssistantMessage<T = unknown> = AssistantMessage
export type NormalizedMessage = Message
export type PartialCompactDirection = string
export type StopHookInfo = {
command?: string
durationMs?: number
[key: string]: unknown
}
export type SystemAgentsKilledMessage = Message & { type: 'system' }
export type SystemApiMetricsMessage = Message & { type: 'system' }
export type SystemAwaySummaryMessage = Message & { type: 'system' }
export type SystemBridgeStatusMessage = Message & { type: 'system' }
export type SystemInformationalMessage = Message & { type: 'system' }
export type SystemMemorySavedMessage = Message & { type: 'system' }
export type SystemMessageLevel = string
export type SystemMicrocompactBoundaryMessage = Message & { type: 'system' }
export type SystemPermissionRetryMessage = Message & { type: 'system' }
export type SystemScheduledTaskFireMessage = Message & { type: 'system' }
export type SystemStopHookSummaryMessage = Message & {
type: 'system'
subtype: string
hookLabel: string
hookCount: number
totalDurationMs?: number
hookInfos: StopHookInfo[]
}
export type SystemTurnDurationMessage = Message & { type: 'system' }
export type GroupedToolUseMessage = Message & {
type: 'grouped_tool_use'
toolName: string
messages: NormalizedAssistantMessage[]
results: NormalizedUserMessage[]
displayMessage: NormalizedAssistantMessage | NormalizedUserMessage
}
// CollapsibleMessage is used by the main project's CollapsedReadSearchGroup
export type CollapsibleMessage =
| AssistantMessage
| UserMessage
| GroupedToolUseMessage
export type HookResultMessage = Message
export type SystemThinkingMessage = Message & { type: 'system' }

View File

@@ -0,0 +1,10 @@
// System prompt branded type.
// Dependency-free so it can be imported from anywhere without circular imports.
export type SystemPrompt = readonly string[] & {
readonly __brand: 'SystemPrompt'
}
export function asSystemPrompt(value: readonly string[]): SystemPrompt {
return value as SystemPrompt
}

View File

@@ -0,0 +1,49 @@
// Usage types for the model provider package.
// Moved from src/entrypoints/sdk/sdkUtilityTypes.ts and src/services/api/emptyUsage.ts
/**
* Non-nullable usage object representing token consumption from an API response.
* Moved from src/entrypoints/sdk/sdkUtilityTypes.ts
*/
export type NonNullableUsage = {
inputTokens?: number
outputTokens?: number
cacheReadInputTokens?: number
cacheCreationInputTokens?: number
input_tokens: number
cache_creation_input_tokens: number
cache_read_input_tokens: number
output_tokens: number
server_tool_use: { web_search_requests: number; web_fetch_requests: number }
service_tier: string
cache_creation: {
ephemeral_1h_input_tokens: number
ephemeral_5m_input_tokens: number
}
inference_geo: string
iterations: unknown[]
speed: string
cache_deleted_input_tokens?: number
[key: string]: unknown
}
/**
* Zero-initialized usage object. Extracted from logging.ts so that
* bridge/replBridge.ts can import it without transitively pulling in
* api/errors.ts → utils/messages.ts → BashTool.tsx → the world.
*/
export const EMPTY_USAGE: Readonly<NonNullableUsage> = {
input_tokens: 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
output_tokens: 0,
server_tool_use: { web_search_requests: 0, web_fetch_requests: 0 },
service_tier: 'standard',
cache_creation: {
ephemeral_1h_input_tokens: 0,
ephemeral_5m_input_tokens: 0,
},
inference_geo: '',
iterations: [],
speed: 'standard',
}

View File

@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true
},
"include": ["src/**/*.ts", "src/**/*.tsx"]
}

View File

@@ -0,0 +1,11 @@
{
"name": "@claude-code-best/agent-tools",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"dependencies": {
"zod": "^3.25.0"
}
}

View File

@@ -0,0 +1,34 @@
import { describe, expect, test } from 'bun:test'
import type { CoreTool, Tool, Tools, AnyObject, ToolResult, ValidationResult, PermissionResult } from '@claude-code-best/agent-tools'
import type { Tool as HostTool } from '../../../../src/Tool.js'
describe('agent-tools compatibility', () => {
test('CoreTool structural compatibility with host Tool', () => {
// The host's Tool should structurally satisfy CoreTool
// because it has all required fields (name, call, description, etc.)
// This test verifies the type-level compatibility at runtime
const mockHostTool: HostTool = {
name: 'test',
aliases: [],
searchHint: 'test tool',
inputSchema: {} as any,
async call() { return { data: 'ok' } as any },
async description() { return 'test' },
async prompt() { return 'test prompt' },
isConcurrencySafe: () => false,
isEnabled: () => true,
isReadOnly: () => false,
async checkPermissions() { return { behavior: 'allow' as const, updatedInput: {} } },
toAutoClassifierInput: () => '',
userFacingName: () => 'test',
maxResultSizeChars: 100000,
mapToolResultToToolResultBlockParam: () => ({ type: 'tool_result', tool_use_id: '1', content: 'ok' }),
renderToolUseMessage: () => null,
}
// This assignment should work if HostTool structurally extends CoreTool
const coreTool: CoreTool = mockHostTool as unknown as CoreTool
expect(coreTool.name).toBe('test')
expect(coreTool.isEnabled()).toBe(true)
})
})

View File

@@ -0,0 +1,63 @@
import { describe, expect, test } from 'bun:test'
import { findToolByName, toolMatchesName } from '../registry.js'
import type { CoreTool, Tools } from '../types.js'
describe('toolMatchesName', () => {
test('matches primary name', () => {
expect(toolMatchesName({ name: 'bash' }, 'bash')).toBe(true)
})
test('does not match different name', () => {
expect(toolMatchesName({ name: 'bash' }, 'read')).toBe(false)
})
test('matches alias', () => {
expect(toolMatchesName({ name: 'bash', aliases: ['shell', 'sh'] }, 'shell')).toBe(true)
expect(toolMatchesName({ name: 'bash', aliases: ['shell', 'sh'] }, 'sh')).toBe(true)
})
test('handles empty aliases', () => {
expect(toolMatchesName({ name: 'bash', aliases: [] }, 'bash')).toBe(true)
expect(toolMatchesName({ name: 'bash', aliases: [] }, 'shell')).toBe(false)
})
test('handles undefined aliases', () => {
expect(toolMatchesName({ name: 'bash' }, 'bash')).toBe(true)
expect(toolMatchesName({ name: 'bash' }, 'shell')).toBe(false)
})
})
describe('findToolByName', () => {
const tools: Tools = [
{ name: 'bash' } as CoreTool,
{ name: 'read', aliases: ['cat'] } as CoreTool,
{ name: 'write', aliases: ['edit'] } as CoreTool,
]
test('finds tool by primary name', () => {
expect(findToolByName(tools, 'bash')?.name).toBe('bash')
})
test('finds tool by alias', () => {
expect(findToolByName(tools, 'cat')?.name).toBe('read')
expect(findToolByName(tools, 'edit')?.name).toBe('write')
})
test('returns undefined for unknown name', () => {
expect(findToolByName(tools, 'unknown')).toBeUndefined()
})
test('handles empty tools array', () => {
expect(findToolByName([], 'bash')).toBeUndefined()
})
test('returns first match for duplicate names', () => {
const dupTools: Tools = [
{ name: 'tool', aliases: ['a'] } as CoreTool,
{ name: 'tool', aliases: ['b'] } as CoreTool,
]
const found = findToolByName(dupTools, 'tool')
expect(found).toBeDefined()
expect(found!.aliases).toContain('a')
})
})

View File

@@ -0,0 +1,18 @@
// agent-tools — Tool interface definitions and registry utilities
// Pure types + pure functions, zero runtime dependencies
export type {
AnyObject,
ToolInputJSONSchema,
ToolProgressData,
ToolProgress,
ToolCallProgress,
ToolResult,
ValidationResult,
PermissionResult,
CoreTool,
Tool,
Tools,
} from './types.js'
export { findToolByName, toolMatchesName } from './registry.js'

View File

@@ -0,0 +1,21 @@
import type { CoreTool, Tools } from './types.js'
/**
* Checks if a tool matches the given name (primary name or alias).
*/
export function toolMatchesName(
tool: { name: string; aliases?: string[] },
name: string,
): boolean {
return tool.name === name || (tool.aliases?.includes(name) ?? false)
}
/**
* Finds a tool by name or alias from a list of tools.
*/
export function findToolByName(
tools: Tools,
name: string,
): CoreTool | undefined {
return tools.find(t => toolMatchesName(t, name))
}

View File

@@ -0,0 +1,221 @@
// agent-tools — Core Tool interface definitions
// Protocol-level types, independent of any host framework
import type { z } from 'zod/v4'
// ============================================================================
// Schema types
// ============================================================================
/**
* Zod schema type for any object with string keys.
* Used as the Input generic constraint for Tool.
*/
export type AnyObject = z.ZodType<{ [key: string]: unknown }>
/**
* JSON Schema format for MCP tool input schemas.
* MCP servers provide this directly instead of Zod schemas.
*/
export type ToolInputJSONSchema = {
[x: string]: unknown
type: 'object'
properties?: {
[x: string]: unknown
}
}
// ============================================================================
// Progress types
// ============================================================================
/**
* Progress data from a running tool. Host defines concrete subtypes.
* Typed as `any` at the protocol level — the host assigns real shapes.
*/
export type ToolProgressData = any
/**
* A progress event from a tool execution.
*/
export type ToolProgress<P extends ToolProgressData = ToolProgressData> = {
toolUseID: string
data: P
}
/**
* Callback for receiving progress updates during tool execution.
*/
export type ToolCallProgress<P extends ToolProgressData = ToolProgressData> = (
progress: ToolProgress<P>,
) => void
// ============================================================================
// Result types
// ============================================================================
/**
* Result returned by a tool's call() method.
* @template T - The output data type
* @template Message - The message type (host-specific, defaults to unknown)
*/
export type ToolResult<T, Message = unknown> = {
data: T
newMessages?: Message[]
contextModifier?: (context: any) => any
/** MCP protocol metadata (structuredContent, _meta) */
mcpMeta?: {
_meta?: Record<string, unknown>
structuredContent?: Record<string, unknown>
}
}
// ============================================================================
// Validation & Permission types
// ============================================================================
/**
* Result of tool input validation.
*/
export type ValidationResult =
| { result: true }
| { result: false; message: string; errorCode: number }
/**
* Result of a permission check for a tool invocation.
*/
export type PermissionResult =
| { behavior: 'allow'; updatedInput: Record<string, unknown> }
| { behavior: 'deny'; message: string }
| { behavior: 'passthrough' }
// ============================================================================
// Core Tool interface
// ============================================================================
/**
* The host-agnostic core Tool interface.
*
* This defines the protocol-level contract for any tool — independent of
* React rendering, specific context types, or host infrastructure.
*
* The host (Claude Code) extends this with render methods, richer context
* types, and other host-specific features. Host tools structurally satisfy
* this interface because they implement all required fields.
*
* @template Input - Zod schema type for tool input
* @template Output - Tool output data type
* @template P - Tool progress data type
* @template Context - Tool execution context type (host-specific)
*/
export interface CoreTool<
Input extends AnyObject = AnyObject,
Output = unknown,
P extends ToolProgressData = ToolProgressData,
Context = unknown,
> {
// ── Identity ──
readonly name: string
aliases?: string[]
searchHint?: string
// ── Schema ──
readonly inputSchema: Input
readonly inputJSONSchema?: ToolInputJSONSchema
outputSchema?: z.ZodType<unknown>
// ── Execution ──
call(
args: z.infer<Input>,
context: Context,
canUseTool: (...args: any[]) => Promise<any>,
parentMessage: any,
onProgress?: ToolCallProgress<P>,
): Promise<ToolResult<Output>>
// ── Description ──
description(
input: z.infer<Input>,
options: {
isNonInteractiveSession: boolean
toolPermissionContext: any
tools: readonly CoreTool[]
},
): Promise<string>
prompt(options: {
getToolPermissionContext: () => Promise<any>
tools: readonly CoreTool[]
agents: any[]
allowedAgentTypes?: string[]
}): Promise<string>
// ── Behavioral properties ──
isConcurrencySafe(input: z.infer<Input>): boolean
isEnabled(): boolean
isReadOnly(input: z.infer<Input>): boolean
isDestructive?(input: z.infer<Input>): boolean
isOpenWorld?(input: z.infer<Input>): boolean
interruptBehavior?(): 'cancel' | 'block'
requiresUserInteraction?(): boolean
// ── MCP markers ──
isMcp?: boolean
isLsp?: boolean
readonly shouldDefer?: boolean
readonly alwaysLoad?: boolean
mcpInfo?: { serverName: string; toolName: string }
// ── Permissions ──
validateInput?(
input: z.infer<Input>,
context: Context,
): Promise<ValidationResult>
checkPermissions(
input: z.infer<Input>,
context: Context,
): Promise<PermissionResult>
// ── Utility ──
inputsEquivalent?(a: z.infer<Input>, b: z.infer<Input>): boolean
getPath?(input: z.infer<Input>): string
toAutoClassifierInput(input: z.infer<Input>): unknown
backfillObservableInput?(input: Record<string, unknown>): void
// ── Output ──
maxResultSizeChars: number
userFacingName(input: Partial<z.infer<Input>> | undefined): string
mapToolResultToToolResultBlockParam(
content: Output,
toolUseID: string,
): any
// ── Optional output helpers ──
isResultTruncated?(output: Output): boolean
getToolUseSummary?(input: Partial<z.infer<Input>> | undefined): string | null
getActivityDescription?(
input: Partial<z.infer<Input>> | undefined,
): string | null
isTransparentWrapper?(): boolean
isSearchOrReadCommand?(input: z.infer<Input>): {
isSearch: boolean
isRead: boolean
isList?: boolean
}
}
/**
* A tool with a generic context type.
* This is the default export — hosts can specify their own Context type.
*/
export type Tool<
Input extends AnyObject = AnyObject,
Output = unknown,
P extends ToolProgressData = ToolProgressData,
> = CoreTool<Input, Output, P>
/**
* A collection of tools.
*/
export type Tools = readonly CoreTool[]

View File

@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,16 @@
{
"name": "@claude-code-best/builtin-tools",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./tools/*": "./src/tools/*",
"./utils": "./src/utils.ts"
},
"dependencies": {
"@claude-code-best/agent-tools": "workspace:*"
}
}

View File

@@ -0,0 +1,70 @@
// builtin-tools — All tool implementations for Claude Code
// This barrel file re-exports the main tool constants and utilities.
// For specific submodules, use deep imports: 'builtin-tools/tools/XTool/XTool.js'
// =============================================================================
// Main tool exports (used by src/tools.ts)
// =============================================================================
// Core tools
export { AgentTool } from './tools/AgentTool/AgentTool.js'
export { AskUserQuestionTool } from './tools/AskUserQuestionTool/AskUserQuestionTool.js'
export { BashTool } from './tools/BashTool/BashTool.js'
export { BriefTool } from './tools/BriefTool/BriefTool.js'
export { ConfigTool } from './tools/ConfigTool/ConfigTool.js'
export { EnterPlanModeTool } from './tools/EnterPlanModeTool/EnterPlanModeTool.js'
export { EnterWorktreeTool } from './tools/EnterWorktreeTool/EnterWorktreeTool.js'
export { ExitPlanModeV2Tool } from './tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'
export { ExitWorktreeTool } from './tools/ExitWorktreeTool/ExitWorktreeTool.js'
export { FileEditTool } from './tools/FileEditTool/FileEditTool.js'
export { FileReadTool } from './tools/FileReadTool/FileReadTool.js'
export { FileWriteTool } from './tools/FileWriteTool/FileWriteTool.js'
export { GlobTool } from './tools/GlobTool/GlobTool.js'
export { GrepTool } from './tools/GrepTool/GrepTool.js'
export { LSPTool } from './tools/LSPTool/LSPTool.js'
export { ListMcpResourcesTool } from './tools/ListMcpResourcesTool/ListMcpResourcesTool.js'
export { ReadMcpResourceTool } from './tools/ReadMcpResourceTool/ReadMcpResourceTool.js'
export { NotebookEditTool } from './tools/NotebookEditTool/NotebookEditTool.js'
export { SkillTool } from './tools/SkillTool/SkillTool.js'
export { TaskOutputTool } from './tools/TaskOutputTool/TaskOutputTool.js'
export { TaskStopTool } from './tools/TaskStopTool/TaskStopTool.js'
export { TodoWriteTool } from './tools/TodoWriteTool/TodoWriteTool.js'
export { ToolSearchTool } from './tools/ToolSearchTool/ToolSearchTool.js'
export { TungstenTool } from './tools/TungstenTool/TungstenTool.js'
export { WebFetchTool } from './tools/WebFetchTool/WebFetchTool.js'
export { WebSearchTool } from './tools/WebSearchTool/WebSearchTool.js'
export { TestingPermissionTool } from './tools/testing/TestingPermissionTool.js'
// Feature-gated tools
export { OVERFLOW_TEST_TOOL_NAME } from './tools/OverflowTestTool/OverflowTestTool.js'
export { CtxInspectTool } from './tools/CtxInspectTool/CtxInspectTool.js'
export { ListPeersTool } from './tools/ListPeersTool/ListPeersTool.js'
export { MonitorTool } from './tools/MonitorTool/MonitorTool.js'
export { PowerShellTool } from './tools/PowerShellTool/PowerShellTool.js'
export { PushNotificationTool } from './tools/PushNotificationTool/PushNotificationTool.js'
export { REPLTool } from './tools/REPLTool/REPLTool.js'
export { RemoteTriggerTool } from './tools/RemoteTriggerTool/RemoteTriggerTool.js'
export { ReviewArtifactTool } from './tools/ReviewArtifactTool/ReviewArtifactTool.js'
export { CronCreateTool } from './tools/ScheduleCronTool/CronCreateTool.js'
export { CronDeleteTool } from './tools/ScheduleCronTool/CronDeleteTool.js'
export { CronListTool } from './tools/ScheduleCronTool/CronListTool.js'
export { SendMessageTool } from './tools/SendMessageTool/SendMessageTool.js'
export { SendUserFileTool } from './tools/SendUserFileTool/SendUserFileTool.js'
export { SleepTool } from './tools/SleepTool/SleepTool.js'
export { SnipTool } from './tools/SnipTool/SnipTool.js'
export { SubscribePRTool } from './tools/SubscribePRTool/SubscribePRTool.js'
export { SuggestBackgroundPRTool } from './tools/SuggestBackgroundPRTool/SuggestBackgroundPRTool.js'
export { TeamCreateTool } from './tools/TeamCreateTool/TeamCreateTool.js'
export { TeamDeleteTool } from './tools/TeamDeleteTool/TeamDeleteTool.js'
export { TerminalCaptureTool } from './tools/TerminalCaptureTool/TerminalCaptureTool.js'
export { VerifyPlanExecutionTool } from './tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.js'
export { WebBrowserTool } from './tools/WebBrowserTool/WebBrowserTool.js'
export { WorkflowTool } from './tools/WorkflowTool/WorkflowTool.js'
export { initBundledWorkflows } from './tools/WorkflowTool/bundled/index.js'
export { getWorkflowCommands } from './tools/WorkflowTool/createWorkflowCommand.js'
// Constants
export { SYNTHETIC_OUTPUT_TOOL_NAME, createSyntheticOutputTool } from './tools/SyntheticOutputTool/SyntheticOutputTool.js'
// Shared utilities
export { tagMessagesWithToolUseID, getToolUseIDFromParentMessage } from './tools/utils.js'

View File

@@ -11,19 +11,19 @@ import { z } from 'zod/v4'
import {
clearInvokedSkillsForAgent,
getSdkAgentProgressSummariesEnabled,
} from '../../bootstrap/state.js'
} from 'src/bootstrap/state.js'
import {
enhanceSystemPromptWithEnvDetails,
getSystemPrompt,
} from '../../constants/prompts.js'
import { isCoordinatorMode } from '../../coordinator/coordinatorMode.js'
import { startAgentSummarization } from '../../services/AgentSummary/agentSummary.js'
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
} from 'src/constants/prompts.js'
import { isCoordinatorMode } from 'src/coordinator/coordinatorMode.js'
import { startAgentSummarization } from 'src/services/AgentSummary/agentSummary.js'
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../../services/analytics/index.js'
import { clearDumpState } from '../../services/api/dumpPrompts.js'
} from 'src/services/analytics/index.js'
import { clearDumpState } from 'src/services/api/dumpPrompts.js'
import {
completeAgentTask as completeAsyncAgent,
createActivityDescriptionResolver,
@@ -39,53 +39,53 @@ import {
unregisterAgentForeground,
updateAgentProgress as updateAsyncAgentProgress,
updateProgressFromMessage,
} from '../../tasks/LocalAgentTask/LocalAgentTask.js'
} from 'src/tasks/LocalAgentTask/LocalAgentTask.js'
import {
checkRemoteAgentEligibility,
formatPreconditionError,
getRemoteTaskSessionUrl,
registerRemoteAgentTask,
type BackgroundRemoteSessionPrecondition,
} from '../../tasks/RemoteAgentTask/RemoteAgentTask.js'
import { assembleToolPool } from '../../tools.js'
import { asAgentId } from '../../types/ids.js'
import { runWithAgentContext, type SubagentContext } from '../../utils/agentContext.js'
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
import { getCwd, runWithCwdOverride } from '../../utils/cwd.js'
import { logForDebugging } from '../../utils/debug.js'
import { isEnvTruthy } from '../../utils/envUtils.js'
import { AbortError, errorMessage, toError } from '../../utils/errors.js'
import type { CacheSafeParams } from '../../utils/forkedAgent.js'
import { lazySchema } from '../../utils/lazySchema.js'
} from 'src/tasks/RemoteAgentTask/RemoteAgentTask.js'
import { assembleToolPool } from 'src/tools.js'
import { asAgentId } from 'src/types/ids.js'
import { runWithAgentContext, type SubagentContext } from 'src/utils/agentContext.js'
import { isAgentSwarmsEnabled } from 'src/utils/agentSwarmsEnabled.js'
import { getCwd, runWithCwdOverride } from 'src/utils/cwd.js'
import { logForDebugging } from 'src/utils/debug.js'
import { isEnvTruthy } from 'src/utils/envUtils.js'
import { AbortError, errorMessage, toError } from 'src/utils/errors.js'
import type { CacheSafeParams } from 'src/utils/forkedAgent.js'
import { lazySchema } from 'src/utils/lazySchema.js'
import {
createUserMessage,
extractTextContent,
isSyntheticMessage,
normalizeMessages,
} from '../../utils/messages.js'
import { getAgentModel } from '../../utils/model/agent.js'
import { permissionModeSchema } from '../../utils/permissions/PermissionMode.js'
import type { PermissionResult } from '../../utils/permissions/PermissionResult.js'
} from 'src/utils/messages.js'
import { getAgentModel } from 'src/utils/model/agent.js'
import { permissionModeSchema } from 'src/utils/permissions/PermissionMode.js'
import type { PermissionResult } from 'src/utils/permissions/PermissionResult.js'
import {
filterDeniedAgents,
getDenyRuleForAgent,
} from '../../utils/permissions/permissions.js'
import { enqueueSdkEvent } from '../../utils/sdkEventQueue.js'
import { writeAgentMetadata } from '../../utils/sessionStorage.js'
import { sleep } from '../../utils/sleep.js'
import { buildEffectiveSystemPrompt } from '../../utils/systemPrompt.js'
import { asSystemPrompt } from '../../utils/systemPromptType.js'
import { getTaskOutputPath } from '../../utils/task/diskOutput.js'
import { getParentSessionId, isTeammate } from '../../utils/teammate.js'
import { isInProcessTeammate } from '../../utils/teammateContext.js'
import { teleportToRemote } from '../../utils/teleport.js'
import { getAssistantMessageContentLength } from '../../utils/tokens.js'
import { createAgentId } from '../../utils/uuid.js'
} from 'src/utils/permissions/permissions.js'
import { enqueueSdkEvent } from 'src/utils/sdkEventQueue.js'
import { writeAgentMetadata } from 'src/utils/sessionStorage.js'
import { sleep } from 'src/utils/sleep.js'
import { buildEffectiveSystemPrompt } from 'src/utils/systemPrompt.js'
import { asSystemPrompt } from 'src/utils/systemPromptType.js'
import { getTaskOutputPath } from 'src/utils/task/diskOutput.js'
import { getParentSessionId, isTeammate } from 'src/utils/teammate.js'
import { isInProcessTeammate } from 'src/utils/teammateContext.js'
import { teleportToRemote } from 'src/utils/teleport.js'
import { getAssistantMessageContentLength } from 'src/utils/tokens.js'
import { createAgentId } from 'src/utils/uuid.js'
import {
createAgentWorktree,
hasWorktreeChanges,
removeAgentWorktree,
} from '../../utils/worktree.js'
} from 'src/utils/worktree.js'
import { BASH_TOOL_NAME } from '../BashTool/toolName.js'
import { BackgroundHint } from '../BashTool/UI.js'
import { FILE_READ_TOOL_NAME } from '../FileReadTool/prompt.js'
@@ -136,7 +136,7 @@ import {
/* eslint-disable @typescript-eslint/no-require-imports */
const proactiveModule =
feature('PROACTIVE') || feature('KAIROS')
? (require('../../proactive/index.js') as typeof import('../../proactive/index.js'))
? (require('src/proactive/index.js') as typeof import('src/proactive/index.js'))
: null
/* eslint-enable @typescript-eslint/no-require-imports */
@@ -332,7 +332,7 @@ export type RemoteLaunchedOutput = {
type InternalOutput = Output | TeammateSpawnedOutput | RemoteLaunchedOutput
import type { AgentToolProgress, ShellProgress } from '../../types/tools.js'
import type { AgentToolProgress, ShellProgress } from 'src/types/tools.js'
// AgentTool forwards both its own progress events and shell progress
// events from the sub-agent so the SDK receives tool_progress updates during bash/powershell runs.
export type Progress = AgentToolProgress | ShellProgress

View File

@@ -12,37 +12,37 @@ import {
} from 'src/components/CtrlOToExpand.js'
import { Byline, KeyboardShortcutHint } from '@anthropic/ink'
import type { z } from 'zod/v4'
import { AgentProgressLine } from '../../components/AgentProgressLine.js'
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js'
import { FallbackToolUseRejectedMessage } from '../../components/FallbackToolUseRejectedMessage.js'
import { Markdown } from '../../components/Markdown.js'
import { Message as MessageComponent } from '../../components/Message.js'
import { MessageResponse } from '../../components/MessageResponse.js'
import { ToolUseLoader } from '../../components/ToolUseLoader.js'
import { AgentProgressLine } from 'src/components/AgentProgressLine.js'
import { FallbackToolUseErrorMessage } from 'src/components/FallbackToolUseErrorMessage.js'
import { FallbackToolUseRejectedMessage } from 'src/components/FallbackToolUseRejectedMessage.js'
import { Markdown } from 'src/components/Markdown.js'
import { Message as MessageComponent } from 'src/components/Message.js'
import { MessageResponse } from 'src/components/MessageResponse.js'
import { ToolUseLoader } from 'src/components/ToolUseLoader.js'
import { Box, Text } from '@anthropic/ink'
import { getDumpPromptsPath } from '../../services/api/dumpPrompts.js'
import { findToolByName, type Tools } from '../../Tool.js'
import type { Message, ProgressMessage } from '../../types/message.js'
import type { AgentToolProgress } from '../../types/tools.js'
import { count } from '../../utils/array.js'
import { getDumpPromptsPath } from 'src/services/api/dumpPrompts.js'
import { findToolByName, type Tools } from 'src/Tool.js'
import type { Message, ProgressMessage } from 'src/types/message.js'
import type { AgentToolProgress } from 'src/types/tools.js'
import { count } from 'src/utils/array.js'
import {
getSearchOrReadFromContent,
getSearchReadSummaryText,
} from '../../utils/collapseReadSearch.js'
import { getDisplayPath } from '../../utils/file.js'
import { formatDuration, formatNumber } from '../../utils/format.js'
} from 'src/utils/collapseReadSearch.js'
import { getDisplayPath } from 'src/utils/file.js'
import { formatDuration, formatNumber } from 'src/utils/format.js'
import {
buildSubagentLookups,
createAssistantMessage,
EMPTY_LOOKUPS,
} from '../../utils/messages.js'
import type { ModelAlias } from '../../utils/model/aliases.js'
} from 'src/utils/messages.js'
import type { ModelAlias } from 'src/utils/model/aliases.js'
import {
getMainLoopModel,
parseUserSpecifiedModel,
renderModelName,
} from '../../utils/model/model.js'
import type { Theme, ThemeName } from '../../utils/theme.js'
} from 'src/utils/model/model.js'
import type { Theme, ThemeName } from 'src/utils/theme.js'
import type {
outputSchema,
Progress,

View File

@@ -1,12 +1,23 @@
import { mock, describe, expect, test } from "bun:test";
// Mock heavy deps
mock.module("../../utils/model/agent.js", () => ({
mock.module("src/utils/model/agent.js", () => ({
getDefaultSubagentModel: () => undefined,
}));
mock.module("../../utils/settings/constants.js", () => ({
mock.module("src/utils/settings/constants.js", () => ({
getSourceDisplayName: (source: string) => source,
getSourceDisplayNameLowercase: (source: string) => source,
getSourceDisplayNameCapitalized: (source: string) => source,
getSettingSourceName: (source: string) => source,
getSettingSourceDisplayNameLowercase: (source: string) => source,
getSettingSourceDisplayNameCapitalized: (source: string) => source,
parseSettingSourcesFlag: () => [],
getEnabledSettingSources: () => [],
isSettingSourceEnabled: () => true,
SETTING_SOURCES: ["localSettings", "userSettings", "projectSettings"],
SOURCES: ["localSettings", "userSettings", "projectSettings"],
CLAUDE_CODE_SETTINGS_SCHEMA_URL: "https://json.schemastore.org/claude-code-settings.json",
}));
const {

View File

@@ -1,5 +1,5 @@
import { getAgentColorMap } from '../../bootstrap/state.js'
import type { Theme } from '../../utils/theme.js'
import { getAgentColorMap } from 'src/bootstrap/state.js'
import type { Theme } from 'src/utils/theme.js'
export type AgentColorName =
| 'red'

View File

@@ -3,11 +3,11 @@
* Used by both the CLI `claude agents` handler and the interactive `/agents` command.
*/
import { getDefaultSubagentModel } from '../../utils/model/agent.js'
import { getDefaultSubagentModel } from 'src/utils/model/agent.js'
import {
getSourceDisplayName,
type SettingSource,
} from '../../utils/settings/constants.js'
} from 'src/utils/settings/constants.js'
import type { AgentDefinition } from './loadAgentsDir.js'
type AgentSource = SettingSource | 'built-in' | 'plugin'

View File

@@ -1,13 +1,13 @@
import { join, normalize, sep } from 'path'
import { getProjectRoot } from '../../bootstrap/state.js'
import { getProjectRoot } from 'src/bootstrap/state.js'
import {
buildMemoryPrompt,
ensureMemoryDirExists,
} from '../../memdir/memdir.js'
import { getMemoryBaseDir } from '../../memdir/paths.js'
import { getCwd } from '../../utils/cwd.js'
import { findCanonicalGitRoot } from '../../utils/git.js'
import { sanitizePath } from '../../utils/path.js'
} from 'src/memdir/memdir.js'
import { getMemoryBaseDir } from 'src/memdir/paths.js'
import { getCwd } from 'src/utils/cwd.js'
import { findCanonicalGitRoot } from 'src/utils/git.js'
import { sanitizePath } from 'src/utils/path.js'
// Persistent agent memory scope: 'user' (~/.claude/agent-memory/), 'project' (.claude/agent-memory/), or 'local' (.claude/agent-memory-local/)
export type AgentMemoryScope = 'user' | 'project' | 'local'

View File

@@ -1,10 +1,10 @@
import { mkdir, readdir, readFile, unlink, writeFile } from 'fs/promises'
import { join } from 'path'
import { z } from 'zod/v4'
import { getCwd } from '../../utils/cwd.js'
import { logForDebugging } from '../../utils/debug.js'
import { lazySchema } from '../../utils/lazySchema.js'
import { jsonParse, jsonStringify } from '../../utils/slowOperations.js'
import { getCwd } from 'src/utils/cwd.js'
import { logForDebugging } from 'src/utils/debug.js'
import { lazySchema } from 'src/utils/lazySchema.js'
import { jsonParse, jsonStringify } from 'src/utils/slowOperations.js'
import { type AgentMemoryScope, getAgentMemoryDir } from './agentMemory.js'
const SNAPSHOT_BASE = 'agent-memory-snapshots'

View File

@@ -1,26 +1,26 @@
import { feature } from 'bun:bundle'
import { z } from 'zod/v4'
import { clearInvokedSkillsForAgent } from '../../bootstrap/state.js'
import { clearInvokedSkillsForAgent } from 'src/bootstrap/state.js'
import {
ALL_AGENT_DISALLOWED_TOOLS,
ASYNC_AGENT_ALLOWED_TOOLS,
CUSTOM_AGENT_DISALLOWED_TOOLS,
IN_PROCESS_TEAMMATE_ALLOWED_TOOLS,
} from '../../constants/tools.js'
import { startAgentSummarization } from '../../services/AgentSummary/agentSummary.js'
} from 'src/constants/tools.js'
import { startAgentSummarization } from 'src/services/AgentSummary/agentSummary.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../../services/analytics/index.js'
import { clearDumpState } from '../../services/api/dumpPrompts.js'
import type { AppState } from '../../state/AppState.js'
} from 'src/services/analytics/index.js'
import { clearDumpState } from 'src/services/api/dumpPrompts.js'
import type { AppState } from 'src/state/AppState.js'
import type {
Tool,
ToolPermissionContext,
Tools,
ToolUseContext,
} from '../../Tool.js'
import { toolMatchesName } from '../../Tool.js'
} from 'src/Tool.js'
import { toolMatchesName } from 'src/Tool.js'
import {
completeAgentTask as completeAsyncAgent,
createActivityDescriptionResolver,
@@ -34,28 +34,28 @@ import {
type ProgressTracker,
updateAgentProgress as updateAsyncAgentProgress,
updateProgressFromMessage,
} from '../../tasks/LocalAgentTask/LocalAgentTask.js'
import { asAgentId } from '../../types/ids.js'
import type { Message as MessageType, ContentItem } from '../../types/message.js'
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
import { logForDebugging } from '../../utils/debug.js'
import { isInProtectedNamespace } from '../../utils/envUtils.js'
import { AbortError, errorMessage } from '../../utils/errors.js'
import type { CacheSafeParams } from '../../utils/forkedAgent.js'
import { lazySchema } from '../../utils/lazySchema.js'
} from 'src/tasks/LocalAgentTask/LocalAgentTask.js'
import { asAgentId } from 'src/types/ids.js'
import type { Message as MessageType, ContentItem } from 'src/types/message.js'
import { isAgentSwarmsEnabled } from 'src/utils/agentSwarmsEnabled.js'
import { logForDebugging } from 'src/utils/debug.js'
import { isInProtectedNamespace } from 'src/utils/envUtils.js'
import { AbortError, errorMessage } from 'src/utils/errors.js'
import type { CacheSafeParams } from 'src/utils/forkedAgent.js'
import { lazySchema } from 'src/utils/lazySchema.js'
import {
extractTextContent,
getLastAssistantMessage,
} from '../../utils/messages.js'
import type { PermissionMode } from '../../utils/permissions/PermissionMode.js'
import { permissionRuleValueFromString } from '../../utils/permissions/permissionRuleParser.js'
} from 'src/utils/messages.js'
import type { PermissionMode } from 'src/utils/permissions/PermissionMode.js'
import { permissionRuleValueFromString } from 'src/utils/permissions/permissionRuleParser.js'
import {
buildTranscriptForClassifier,
classifyYoloAction,
} from '../../utils/permissions/yoloClassifier.js'
import { emitTaskProgress as emitTaskProgressEvent } from '../../utils/task/sdkProgress.js'
import { isInProcessTeammate } from '../../utils/teammateContext.js'
import { getTokenCountFromUsage } from '../../utils/tokens.js'
} from 'src/utils/permissions/yoloClassifier.js'
import { emitTaskProgress as emitTaskProgressEvent } from 'src/utils/task/sdkProgress.js'
import { isInProcessTeammate } from 'src/utils/teammateContext.js'
import { getTokenCountFromUsage } from 'src/utils/tokens.js'
import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '../ExitPlanModeTool/constants.js'
import { AGENT_TOOL_NAME, LEGACY_AGENT_TOOL_NAME } from './constants.js'
import type { AgentDefinition } from './loadAgentsDir.js'

View File

@@ -1,14 +1,14 @@
import { BASH_TOOL_NAME } from 'src/tools/BashTool/toolName.js'
import { FILE_READ_TOOL_NAME } from 'src/tools/FileReadTool/prompt.js'
import { GLOB_TOOL_NAME } from 'src/tools/GlobTool/prompt.js'
import { GREP_TOOL_NAME } from 'src/tools/GrepTool/prompt.js'
import { SEND_MESSAGE_TOOL_NAME } from 'src/tools/SendMessageTool/constants.js'
import { WEB_FETCH_TOOL_NAME } from 'src/tools/WebFetchTool/prompt.js'
import { WEB_SEARCH_TOOL_NAME } from 'src/tools/WebSearchTool/prompt.js'
import { BASH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/BashTool/toolName.js'
import { FILE_READ_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/FileReadTool/prompt.js'
import { GLOB_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/GlobTool/prompt.js'
import { GREP_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/GrepTool/prompt.js'
import { SEND_MESSAGE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/SendMessageTool/constants.js'
import { WEB_FETCH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/WebFetchTool/prompt.js'
import { WEB_SEARCH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/WebSearchTool/prompt.js'
import { isUsing3PServices } from 'src/utils/auth.js'
import { hasEmbeddedSearchTools } from 'src/utils/embeddedTools.js'
import { getSettings_DEPRECATED } from 'src/utils/settings/settings.js'
import { jsonStringify } from '../../../utils/slowOperations.js'
import { jsonStringify } from 'src/utils/slowOperations.js'
import type {
AgentDefinition,
BuiltInAgentDefinition,

View File

@@ -1,11 +1,11 @@
import { BASH_TOOL_NAME } from 'src/tools/BashTool/toolName.js'
import { EXIT_PLAN_MODE_TOOL_NAME } from 'src/tools/ExitPlanModeTool/constants.js'
import { FILE_EDIT_TOOL_NAME } from 'src/tools/FileEditTool/constants.js'
import { FILE_READ_TOOL_NAME } from 'src/tools/FileReadTool/prompt.js'
import { FILE_WRITE_TOOL_NAME } from 'src/tools/FileWriteTool/prompt.js'
import { GLOB_TOOL_NAME } from 'src/tools/GlobTool/prompt.js'
import { GREP_TOOL_NAME } from 'src/tools/GrepTool/prompt.js'
import { NOTEBOOK_EDIT_TOOL_NAME } from 'src/tools/NotebookEditTool/constants.js'
import { BASH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/BashTool/toolName.js'
import { EXIT_PLAN_MODE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/ExitPlanModeTool/constants.js'
import { FILE_EDIT_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/FileEditTool/constants.js'
import { FILE_READ_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/FileReadTool/prompt.js'
import { FILE_WRITE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/FileWriteTool/prompt.js'
import { GLOB_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/GlobTool/prompt.js'
import { GREP_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/GrepTool/prompt.js'
import { NOTEBOOK_EDIT_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/NotebookEditTool/constants.js'
import { hasEmbeddedSearchTools } from 'src/utils/embeddedTools.js'
import { AGENT_TOOL_NAME } from '../constants.js'
import type { BuiltInAgentDefinition } from '../loadAgentsDir.js'

View File

@@ -1,11 +1,11 @@
import { BASH_TOOL_NAME } from 'src/tools/BashTool/toolName.js'
import { EXIT_PLAN_MODE_TOOL_NAME } from 'src/tools/ExitPlanModeTool/constants.js'
import { FILE_EDIT_TOOL_NAME } from 'src/tools/FileEditTool/constants.js'
import { FILE_READ_TOOL_NAME } from 'src/tools/FileReadTool/prompt.js'
import { FILE_WRITE_TOOL_NAME } from 'src/tools/FileWriteTool/prompt.js'
import { GLOB_TOOL_NAME } from 'src/tools/GlobTool/prompt.js'
import { GREP_TOOL_NAME } from 'src/tools/GrepTool/prompt.js'
import { NOTEBOOK_EDIT_TOOL_NAME } from 'src/tools/NotebookEditTool/constants.js'
import { BASH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/BashTool/toolName.js'
import { EXIT_PLAN_MODE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/ExitPlanModeTool/constants.js'
import { FILE_EDIT_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/FileEditTool/constants.js'
import { FILE_READ_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/FileReadTool/prompt.js'
import { FILE_WRITE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/FileWriteTool/prompt.js'
import { GLOB_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/GlobTool/prompt.js'
import { GREP_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/GrepTool/prompt.js'
import { NOTEBOOK_EDIT_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/NotebookEditTool/constants.js'
import { hasEmbeddedSearchTools } from 'src/utils/embeddedTools.js'
import { AGENT_TOOL_NAME } from '../constants.js'
import type { BuiltInAgentDefinition } from '../loadAgentsDir.js'

Some files were not shown because too many files have changed in this diff Show More