mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2b66d9d2c | ||
|
|
d70e7f7f05 | ||
|
|
72a2093cd6 | ||
|
|
b5c299f5d2 | ||
|
|
ac42ce2d67 | ||
|
|
c659912517 | ||
|
|
a14b7f352b | ||
|
|
c5ab83a3fc | ||
|
|
03b7f9b453 | ||
|
|
bddd146f25 | ||
|
|
c8d08d235b | ||
|
|
a02dc0bded | ||
|
|
3cb1e50b25 | ||
|
|
cfab161e28 | ||
|
|
90027279e6 | ||
|
|
3470783ced | ||
|
|
8169b96250 | ||
|
|
fe08cacf8d | ||
|
|
5a4c820e1d | ||
|
|
1a4e9702c2 | ||
|
|
2273a0bcfe | ||
|
|
b80483c23e | ||
|
|
8442aaadd2 | ||
|
|
dad3ad2b8d | ||
|
|
b5b81dfe49 | ||
|
|
ecbd5a93e4 | ||
|
|
be80da4ce0 | ||
|
|
fce40fed1f | ||
|
|
a7e03a5b30 | ||
|
|
05cabbbd73 | ||
|
|
d4b30d32c3 | ||
|
|
e0484e2817 | ||
|
|
2fb1c9dcd8 | ||
|
|
bbb8b613a9 | ||
|
|
c63b875ae3 | ||
|
|
9b8503d13d | ||
|
|
3cf94fbda0 | ||
|
|
9a3081dff6 | ||
|
|
bd6448ecda | ||
|
|
1071270ce3 | ||
|
|
711440474c | ||
|
|
8399d9ed20 | ||
|
|
513ccc3003 |
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: macos-latest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -23,8 +23,16 @@ jobs:
|
||||
- name: Type check
|
||||
run: bunx tsc --noEmit
|
||||
|
||||
- name: Test
|
||||
run: bun test
|
||||
- name: Test with Coverage
|
||||
run: |
|
||||
set -o pipefail
|
||||
bun test --coverage --coverage-reporter=lcov 2>&1 | grep -vE '^\s*(\(pass\)|\(skip\))' | sed '/^.*\/__tests__\/.*:$/d' | cat -s
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
file: ./coverage/lcov.info
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Build
|
||||
run: bun run build
|
||||
run: bun run build:vite
|
||||
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -13,9 +13,8 @@ src/utils/vendor/
|
||||
# AI tool runtime directories
|
||||
.agents/
|
||||
.claude/
|
||||
.codex/
|
||||
.omx/
|
||||
|
||||
.docs/task/
|
||||
# Binary / screenshot files (root only)
|
||||
/*.png
|
||||
*.bmp
|
||||
@@ -30,3 +29,12 @@ __pycache__/
|
||||
logs
|
||||
|
||||
data
|
||||
.omc
|
||||
.codex/*
|
||||
!.codex/agents/
|
||||
!.codex/agents/**
|
||||
!.codex/skills/
|
||||
!.codex/skills/**
|
||||
.codex/skills/.system/**
|
||||
!.codex/prompts/
|
||||
!.codex/prompts/**
|
||||
|
||||
15
CLAUDE.md
15
CLAUDE.md
@@ -55,6 +55,8 @@ bun run health
|
||||
# Check unused exports
|
||||
bun run check:unused
|
||||
|
||||
bun run typecheck
|
||||
|
||||
# Remote Control Server
|
||||
bun run rcs
|
||||
|
||||
@@ -245,14 +247,23 @@ Feature flags control which functionality is enabled at runtime. 代码中统一
|
||||
## Testing
|
||||
|
||||
- **框架**: `bun:test`(内置断言 + mock)
|
||||
- **当前状态**: 2472 tests / 138 files / 0 fail
|
||||
- **当前状态**: 2992 tests / 188 files / 0 fail
|
||||
- **单元测试**: 就近放置于 `src/**/__tests__/`,文件名 `<module>.test.ts`
|
||||
- **集成测试**: `tests/integration/` — 4 个文件(cli-arguments, context-build, message-pipeline, tool-chain)
|
||||
- **共享 mock/fixture**: `tests/mocks/`(api-responses, file-system, fixtures/)
|
||||
- **命名**: `describe("functionName")` + `test("behavior description")`,英文
|
||||
- **Mock 模式**: 对重依赖模块使用 `mock.module()` + `await import()` 解锁(必须内联在测试文件中,不能从共享 helper 导入)
|
||||
- **包测试**: `packages/` 下各包也有独立测试(如 `color-diff-napi` 11 tests)
|
||||
|
||||
### Mock 使用规范
|
||||
|
||||
**只 mock 有副作用的依赖链,不 mock 纯函数/纯数据模块。**
|
||||
|
||||
被迫 mock 的根源:`log.ts` / `debug.ts` → `bootstrap/state.ts`(模块级 `realpathSync` / `randomUUID` 副作用)。必须 mock 的模块:`log.ts`、`debug.ts`、`bun:bundle`、`settings/settings.js`、`config.ts`、`auth.ts`、第三方网络库。
|
||||
|
||||
不要 mock:纯函数模块(`errors.ts`、`stringUtils.js`)、mock 值与真实实现相同的模块、mock 路径与实际 import 不匹配的模块。
|
||||
|
||||
路径规则:统一用 `.ts` 扩展名 + `src/*` 别名路径,禁止双重 mock 同一模块。
|
||||
|
||||
### 类型检查
|
||||
|
||||
项目使用 TypeScript strict 模式,**tsc 必须零错误**。每次修改后运行:
|
||||
|
||||
29
README.md
29
README.md
@@ -10,25 +10,25 @@
|
||||
|
||||
> Which Claude do you like? The open source one is the best.
|
||||
|
||||
牢 A (Anthropic) 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 工具的源码反编译/逆向还原项目。目标是将 Claude Code 大部分功能及工程化能力复现 (问就是老佛爷已经付过钱了)。虽然很难绷, 但是它叫做 CCB(踩踩背)...
|
||||
牢 A (Anthropic) 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 工具的源码反编译/逆向还原项目。目标是将 Claude Code 大部分功能及工程化能力复现 (问就是老佛爷已经付过钱了)。虽然很难绷, 但是它叫做 CCB(踩踩背)... 而且, 我们实现了企业版或者需要登陆 Claude 账号才能使用的特性, 实现技术普惠
|
||||
|
||||
[文档在这里, 支持投稿 PR](https://ccb.agent-aura.top/) | [留影文档在这里](./Friends.md) | [Discord 群组](https://discord.gg/qZU6zS7Q)
|
||||
|
||||
| 特性 | 说明 | 文档 |
|
||||
|------|------|------|
|
||||
| **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) |
|
||||
| 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) |
|
||||
| **ACP 协议一等一支持** | 支持接入 Zed、Cursor 等 IDE,支持会话恢复、Skills、权限桥接 | [文档](https://ccb.agent-aura.top/docs/features/acp-zed) |
|
||||
| **Remote Control 私有部署** | Docker 自托管远程界面, 可以手机上看 CC | [文档](https://ccb.agent-aura.top/docs/features/remote-control-self-hosting) |
|
||||
| **Langfuse 监控** | 企业级 Agent 监控, 可以清晰看到每次 agent loop 细节, 可以一键转化为数据集 | [文档](https://ccb.agent-aura.top/docs/features/langfuse-monitoring) |
|
||||
| **Web Search** | 内置网页搜索工具, 支持 bing 和 brave 搜索 | [文档](https://ccb.agent-aura.top/docs/features/web-browser-tool) |
|
||||
| **Poor Mode** | 穷鬼模式,关闭记忆提取和键入建议,大幅度减少并发请求 | /poor 可以开关 |
|
||||
| **自定义模型供应商** | 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) |
|
||||
| Langfuse 监控 | LLM 调用/工具执行/多 Agent 全链路追踪 | [文档](https://ccb.agent-aura.top/docs/features/langfuse-monitoring) |
|
||||
| Poor Mode | 穷鬼模式,关闭记忆提取和键入建议 | /poor 可以开关 |
|
||||
|
||||
|
||||
- 🔮 [ ] V6 — 大规模重构石山代码,全面模块分包(全新分支,main 封存为历史版本)
|
||||
| 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) |
|
||||
| /dream 记忆整理 | 自动整理和优化记忆文件 | [文档](https://ccb.agent-aura.top/docs/features/auto-dream) |
|
||||
|
||||
- 🚀 [想要启动项目](#快速开始源码版)
|
||||
- 🐛 [想要调试项目](#vs-code-调试)
|
||||
@@ -42,8 +42,9 @@
|
||||
```sh
|
||||
bun i -g claude-code-best
|
||||
bun pm -g trust claude-code-best
|
||||
ccb # 直接打开 claude code
|
||||
CLAUDE_BRIDGE_BASE_URL=https://remote-control.claude-code-best.win/ CLAUDE_BRIDGE_OAUTH_TOKEN=test-my-key bun run dev --remote-control # 我们有自部署的远程控制
|
||||
ccb # 以 nodejs 打开 claude code
|
||||
ccb-bun # 以 bun 形态打开
|
||||
CLAUDE_BRIDGE_BASE_URL=https://remote-control.claude-code-best.win/ CLAUDE_BRIDGE_OAUTH_TOKEN=test-my-key ccb --remote-control # 我们有自部署的远程控制
|
||||
```
|
||||
|
||||
## ⚡ 快速开始(源码版)
|
||||
|
||||
42
build.ts
42
build.ts
@@ -30,16 +30,20 @@ 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',
|
||||
'CONTEXT_COLLAPSE',
|
||||
'MONITOR_TOOL',
|
||||
'FORK_SUBAGENT',
|
||||
'UDS_INBOX',
|
||||
// 'UDS_INBOX',
|
||||
'KAIROS',
|
||||
'COORDINATOR_MODE',
|
||||
'LAN_PIPES',
|
||||
'BG_SESSIONS',
|
||||
'TEMPLATES',
|
||||
// 'REVIEW_ARTIFACT', // API 请求无响应,需进一步排查 schema 兼容性
|
||||
// P3: poor mode (disable extract_memories + prompt_suggestion)
|
||||
'POOR',
|
||||
@@ -88,8 +92,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)
|
||||
@@ -112,3 +135,18 @@ if (!rgScript.success) {
|
||||
} else {
|
||||
console.log(`Bundled download-ripgrep script to ${outdir}/`)
|
||||
}
|
||||
|
||||
// Step 6: Generate cli-bun and cli-node executable entry points
|
||||
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
|
||||
const { chmodSync } = await import('fs')
|
||||
chmodSync(cliBun, 0o755)
|
||||
chmodSync(cliNode, 0o755)
|
||||
|
||||
console.log(`Generated ${cliBun} (shebang: bun) and ${cliNode} (shebang: node)`)
|
||||
|
||||
220
bun.lock
220
bun.lock
@@ -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.8",
|
||||
"ws": "^8.20.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.3.0",
|
||||
@@ -13,6 +15,7 @@
|
||||
"@ant/computer-use-input": "workspace:*",
|
||||
"@ant/computer-use-mcp": "workspace:*",
|
||||
"@ant/computer-use-swift": "workspace:*",
|
||||
"@ant/model-provider": "workspace:*",
|
||||
"@anthropic-ai/bedrock-sdk": "^0.26.4",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.87",
|
||||
"@anthropic-ai/foundry-sdk": "^0.2.3",
|
||||
@@ -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/@ant/model-provider": {
|
||||
"name": "@ant/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"],
|
||||
@@ -234,6 +278,8 @@
|
||||
|
||||
"@ant/computer-use-swift": ["@ant/computer-use-swift@workspace:packages/@ant/computer-use-swift"],
|
||||
|
||||
"@ant/model-provider": ["@ant/model-provider@workspace:packages/@ant/model-provider"],
|
||||
|
||||
"@anthropic-ai/bedrock-sdk": ["@anthropic-ai/bedrock-sdk@0.26.4", "https://registry.npmmirror.com/@anthropic-ai/bedrock-sdk/-/bedrock-sdk-0.26.4.tgz", { "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1", "@aws-crypto/sha256-js": "^4.0.0", "@aws-sdk/client-bedrock-runtime": "^3.797.0", "@aws-sdk/credential-providers": "^3.796.0", "@smithy/eventstream-serde-node": "^2.0.10", "@smithy/fetch-http-handler": "^5.0.4", "@smithy/protocol-http": "^3.0.6", "@smithy/signature-v4": "^3.1.1", "@smithy/smithy-client": "^2.1.9", "@smithy/types": "^2.3.4", "@smithy/util-base64": "^2.0.0" } }, "sha512-0Z2NY3T2wnzT9esRit6BiWpQXvL+F2b3z3Z9in3mXh7MDf122rVi2bcPowQHmo9ITXAPJmv/3H3t0V1z3Fugfw=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.104", "https://registry.npmmirror.com/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.104.tgz", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-lVm+nS79r6WWlDnv5AgRzTtAlbP8O6M6kkWmDZAWE3nt9agmngxls9frJFvH55uzws2+6l0yyup/JYspfijkzw=="],
|
||||
@@ -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.8", "https://registry.npmmirror.com/@claude-code-best/mcp-chrome-bridge/-/mcp-chrome-bridge-2.0.8.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-f7J1e4PQ6qxXzdHwL7QRrMZ4lPfD/L1MWxWDbyHmHY7jaW2GL6WcArKpk/fApg3V/q0racqUWzXHQdpE/HJZqg=="],
|
||||
|
||||
"@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 |
17
docs/diagrams/agent-loop-simple.mmd
Normal file
17
docs/diagrams/agent-loop-simple.mmd
Normal 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
|
||||
40
docs/diagrams/agent-loop.mmd
Normal file
40
docs/diagrams/agent-loop.mmd
Normal 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
|
||||
346
docs/extensibility/mcp-configuration.mdx
Normal file
346
docs/extensibility/mcp-configuration.mdx
Normal 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`,否则会报错。
|
||||
|
||||
### SSE(Server-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 缓存避免重复提示。
|
||||
|
||||
### HTTP(Streamable 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:` + 原始 URL(unwrap 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 双向通知 |
|
||||
@@ -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)
|
||||
├── 创建 Transport(stdio/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) |
|
||||
|
||||
@@ -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
189
docs/features/acp-zed.md
Normal 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` | ✅ | 配置更新通知 |
|
||||
30
docs/features/chrome-use-mcp.md
Normal file
30
docs/features/chrome-use-mcp.md
Normal 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
|
||||
318
docs/features/daemon-restructure-design.md
Normal file
318
docs/features/daemon-restructure-design.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# Daemon 重构设计方案
|
||||
|
||||
> 分支: `feat/integrate-5-branches`
|
||||
> 基于: `f41745cb` (= main `11bb3f62` 内容)
|
||||
> 日期: 2026-04-13
|
||||
|
||||
## 一、问题概述
|
||||
|
||||
### 1.1 命令结构散乱
|
||||
|
||||
当前后台进程相关的命令分布在三个不同的位置,没有统一的命名空间:
|
||||
|
||||
| 命令 | 注册位置 | 入口 |
|
||||
|------|---------|------|
|
||||
| `claude daemon start/status/stop` | `cli.tsx` 快速路径 L203 | `daemon/main.ts` |
|
||||
| `claude ps` | `cli.tsx` 快速路径 L220 | `cli/bg.ts` |
|
||||
| `claude logs <x>` | `cli.tsx` 快速路径 L232 | `cli/bg.ts` |
|
||||
| `claude attach <x>` | `cli.tsx` 快速路径 L236 | `cli/bg.ts` |
|
||||
| `claude kill <x>` | `cli.tsx` 快速路径 L238 | `cli/bg.ts` |
|
||||
| `claude --bg` | `cli.tsx` 快速路径 L244 | `cli/bg.ts` |
|
||||
| `claude new/list/reply` | `cli.tsx` 快速路径 L250 | `cli/handlers/templateJobs.ts` |
|
||||
| `claude rollback` | `main.tsx` Commander.js L6525 | `cli/rollback.ts` |
|
||||
| `claude up` | `main.tsx` Commander.js L6511 | `cli/up.ts` |
|
||||
|
||||
**问题**:
|
||||
- `ps/logs/attach/kill` 与 `daemon` 逻辑上都是后台进程管理,但互不关联
|
||||
- 这些命令都**只有 CLI 入口**,REPL 里输入 `/daemon` 或 `/ps` 不存在
|
||||
- `new/list/reply` 是模板任务系统的顶级命令,容易与其他命令冲突(特别是 `list`)
|
||||
|
||||
### 1.2 Windows 不支持
|
||||
|
||||
`--bg` 和 `attach` 硬依赖 tmux:
|
||||
- `bg.ts:handleBgFlag()` 第一步就检查 tmux,不可用直接报错退出
|
||||
- `bg.ts:attachHandler()` 用 `tmux attach-session`,无 tmux 替代方案
|
||||
- Windows (包括 VS Code 终端) 完全无法使用后台会话功能
|
||||
|
||||
### 1.3 无 REPL 入口
|
||||
|
||||
对比 `/mcp` 的双注册模式:
|
||||
- **CLI**: `claude mcp serve/add/remove/list` (Commander.js, `main.tsx:5760`)
|
||||
- **REPL**: `/mcp enable/disable/reconnect` (slash command, `commands/mcp/index.ts`)
|
||||
|
||||
`daemon`/`bg`/`job` 系列只有 CLI 快速路径,REPL 中完全不可用。
|
||||
|
||||
## 二、目标
|
||||
|
||||
1. **层级化命令结构**: 参照 `/mcp` 模式,将后台管理收归 `/daemon`,模板任务收归 `/job`
|
||||
2. **跨平台后台会话**: Windows / macOS / Linux 都能启动、附着、终止后台会话
|
||||
3. **双注册**: CLI (`claude daemon ...`) + REPL (`/daemon ...`) 同时可用
|
||||
4. **向后兼容**: 旧命令保留但输出 deprecation 提示
|
||||
|
||||
## 三、命令结构设计
|
||||
|
||||
### 3.1 `/daemon` — 后台进程管理
|
||||
|
||||
合并 daemon supervisor + bg sessions 为统一命名空间:
|
||||
|
||||
```
|
||||
claude daemon <subcommand> ← CLI 入口 (cli.tsx 快速路径)
|
||||
/daemon <subcommand> ← REPL 入口 (slash command, local-jsx)
|
||||
|
||||
子命令:
|
||||
status 综合状态面板 (daemon + 所有会话)
|
||||
start [--dir <path>] 启动 daemon supervisor
|
||||
stop 停止 daemon
|
||||
bg [args...] 启动后台会话
|
||||
attach [target] 附着到后台会话
|
||||
logs [target] 查看会话日志
|
||||
kill [target] 终止会话
|
||||
(无参数) 等同于 status
|
||||
```
|
||||
|
||||
**CLI 快速路径路由** (`cli.tsx`):
|
||||
```typescript
|
||||
// 新: 统一入口
|
||||
if (feature('DAEMON') && args[0] === 'daemon') {
|
||||
const sub = args[1] || 'status'
|
||||
switch (sub) {
|
||||
case 'start': case 'stop': case 'status':
|
||||
await daemonMain([sub, ...args.slice(2)])
|
||||
break
|
||||
case 'bg':
|
||||
await bg.handleBgStart(args.slice(2))
|
||||
break
|
||||
case 'attach': case 'logs': case 'kill':
|
||||
await bg[`${sub}Handler`](args[2])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 向后兼容 (deprecated)
|
||||
if (feature('BG_SESSIONS') && ['ps','logs','attach','kill'].includes(args[0])) {
|
||||
console.warn(`[deprecated] Use: claude daemon ${args[0] === 'ps' ? 'status' : args[0]}`)
|
||||
// ... delegate to daemon subcommand
|
||||
}
|
||||
```
|
||||
|
||||
**REPL 斜杠命令** (`commands/daemon/index.ts`):
|
||||
```typescript
|
||||
const daemon = {
|
||||
type: 'local-jsx',
|
||||
name: 'daemon',
|
||||
description: 'Manage background sessions and daemon',
|
||||
argumentHint: '[status|start|stop|bg|attach|logs|kill]',
|
||||
isEnabled: () => feature('DAEMON') || feature('BG_SESSIONS'),
|
||||
load: () => import('./daemon.js'),
|
||||
} satisfies Command
|
||||
```
|
||||
|
||||
### 3.2 `/job` — 模板任务管理
|
||||
|
||||
```
|
||||
claude job <subcommand> ← CLI 入口
|
||||
/job <subcommand> ← REPL 入口
|
||||
|
||||
子命令:
|
||||
list 列出模板和活跃任务
|
||||
new <template> [args] 从模板创建任务
|
||||
reply <id> <text> 回复任务
|
||||
status <id> 查看任务状态
|
||||
(无参数) 等同于 list
|
||||
```
|
||||
|
||||
### 3.3 独立命令 (不变)
|
||||
|
||||
```
|
||||
claude up 保持顶级 (简短的 bootstrap 命令)
|
||||
claude rollback [target] 保持顶级 (低频运维命令)
|
||||
```
|
||||
|
||||
## 四、跨平台后台引擎
|
||||
|
||||
### 4.1 引擎抽象
|
||||
|
||||
```typescript
|
||||
// src/cli/bg/engine.ts
|
||||
export interface BgEngine {
|
||||
readonly name: string
|
||||
|
||||
/** 当前平台是否可用 */
|
||||
available(): Promise<boolean>
|
||||
|
||||
/** 启动后台会话 */
|
||||
start(opts: BgStartOptions): Promise<BgStartResult>
|
||||
|
||||
/** 附着到后台会话(blocking) */
|
||||
attach(session: SessionEntry): Promise<void>
|
||||
}
|
||||
|
||||
export interface BgStartOptions {
|
||||
sessionName: string
|
||||
args: string[]
|
||||
env: Record<string, string | undefined>
|
||||
logPath: string
|
||||
cwd: string
|
||||
}
|
||||
|
||||
export interface BgStartResult {
|
||||
pid: number
|
||||
sessionName: string
|
||||
logPath: string
|
||||
engineUsed: string
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 三种引擎实现
|
||||
|
||||
| 引擎 | 平台 | 启动方式 | attach 方式 |
|
||||
|------|------|---------|------------|
|
||||
| TmuxEngine | macOS/Linux (有 tmux) | `tmux new-session -d` | `tmux attach-session` |
|
||||
| DetachedEngine | Windows / 无 tmux 的 macOS/Linux | `spawn({ detached, stdio→logFile })` | `tail -f` 日志文件 |
|
||||
|
||||
#### DetachedEngine 详细设计
|
||||
|
||||
**启动 (`start`)**:
|
||||
```typescript
|
||||
// 1. 打开日志文件 fd
|
||||
const logFd = fs.openSync(logPath, 'a')
|
||||
// 2. detached spawn, stdout/stderr 重定向到日志
|
||||
const child = spawn(process.execPath, execArgs, {
|
||||
detached: true,
|
||||
stdio: ['ignore', logFd, logFd],
|
||||
env,
|
||||
cwd,
|
||||
})
|
||||
child.unref()
|
||||
fs.closeSync(logFd)
|
||||
// 3. 写 sessions/<PID>.json
|
||||
```
|
||||
|
||||
**附着 (`attach`)**:
|
||||
```typescript
|
||||
// 跨平台 tail -f 实现
|
||||
// 1. 读取已有日志内容输出到 stdout
|
||||
// 2. fs.watch(logPath) 监听变化
|
||||
// 3. 每次变化读取新增内容
|
||||
// 4. Ctrl+C 退出 tail(不杀后台进程)
|
||||
```
|
||||
|
||||
#### 引擎选择逻辑
|
||||
|
||||
```typescript
|
||||
// src/cli/bg/engines/index.ts
|
||||
export async function selectEngine(): Promise<BgEngine> {
|
||||
if (process.platform === 'win32') {
|
||||
return new DetachedEngine()
|
||||
}
|
||||
|
||||
const tmux = new TmuxEngine()
|
||||
if (await tmux.available()) {
|
||||
return tmux
|
||||
}
|
||||
|
||||
return new DetachedEngine()
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 SessionEntry 扩展
|
||||
|
||||
```typescript
|
||||
interface SessionEntry {
|
||||
// ... 现有字段
|
||||
engine: 'tmux' | 'detached' // 新增: 记录使用的引擎
|
||||
tmuxSessionName?: string // tmux 引擎才有
|
||||
logPath?: string // 两种引擎都有
|
||||
}
|
||||
```
|
||||
|
||||
`attach` 时根据 `session.engine` 选择对应的 attach 策略。
|
||||
|
||||
## 五、文件变更清单
|
||||
|
||||
### 新增文件 (10 个)
|
||||
|
||||
```
|
||||
src/cli/bg/engine.ts BgEngine 接口定义
|
||||
src/cli/bg/engines/tmux.ts TmuxEngine (从 bg.ts 提取)
|
||||
src/cli/bg/engines/detached.ts DetachedEngine (新实现)
|
||||
src/cli/bg/engines/index.ts 引擎选择 + re-export
|
||||
src/cli/bg/tail.ts 跨平台日志 tail (用于 detached attach)
|
||||
src/commands/daemon/index.ts /daemon REPL 斜杠命令注册
|
||||
src/commands/daemon/daemon.tsx /daemon 子命令路由 + status UI
|
||||
src/commands/job/index.ts /job REPL 斜杠命令注册
|
||||
src/commands/job/job.tsx /job 子命令路由 + UI
|
||||
docs/features/daemon-restructure-design.md 本设计文档
|
||||
```
|
||||
|
||||
### 修改文件 (6 个)
|
||||
|
||||
```
|
||||
src/cli/bg.ts 重构: handler 函数改为调用 BgEngine
|
||||
src/entrypoints/cli.tsx 快速路径: daemon 统一入口 + 向后兼容
|
||||
src/commands.ts 注册 /daemon 和 /job 斜杠命令
|
||||
src/daemon/main.ts daemonMain() 增加 bg/ps/logs 子命令分发
|
||||
src/main.tsx Commander.js: 可选注册 daemon/job 子命令
|
||||
src/cli/handlers/templateJobs.ts 适配 /job 入口 (可能不需改)
|
||||
```
|
||||
|
||||
### 不动的文件
|
||||
|
||||
```
|
||||
src/daemon/state.ts daemon PID 状态管理 (无需改)
|
||||
src/jobs/state.ts job 状态管理 (无需改)
|
||||
src/jobs/templates.ts 模板发现 (无需改)
|
||||
src/jobs/classifier.ts 任务分类器 (无需改)
|
||||
src/cli/rollback.ts 保持顶级命令 (无需改)
|
||||
src/cli/up.ts 保持顶级命令 (无需改)
|
||||
```
|
||||
|
||||
## 六、可行性分析
|
||||
|
||||
### 6.1 风险评估
|
||||
|
||||
| 风险 | 级别 | 缓解措施 |
|
||||
|------|------|---------|
|
||||
| cli.tsx 快速路径修改影响启动性能 | 低 | 仅改路由逻辑,import 仍然 lazy |
|
||||
| DetachedEngine 的 attach 在 Windows 上 fs.watch 不可靠 | 中 | 使用轮询 fallback (setInterval + fs.stat) |
|
||||
| 向后兼容的 deprecation 可能破坏脚本 | 低 | 旧命令保持可用,仅输出 stderr 警告 |
|
||||
| REPL 中 /daemon bg 需要 spawn 子进程 | 中 | 参考 /assistant 的 NewInstallWizard (已有 spawn 先例) |
|
||||
| tsc 类型兼容 | 低 | 接口定义清晰,不引入 any |
|
||||
|
||||
### 6.2 工作量估计
|
||||
|
||||
| Task | 文件数 | 复杂度 |
|
||||
|------|--------|--------|
|
||||
| Task 013: BgEngine 抽象 + 引擎实现 | 5 新增 + 1 修改 | 中 |
|
||||
| Task 014: /daemon 命令层级化 | 3 新增 + 3 修改 | 中 |
|
||||
| Task 015: /job 命令层级化 | 2 新增 + 2 修改 | 低 |
|
||||
| Task 016: 向后兼容 + 测试 | 0 新增 + 2 修改 | 低 |
|
||||
|
||||
### 6.3 依赖关系
|
||||
|
||||
```
|
||||
Task 013 (BgEngine) ← 无依赖,可独立开发
|
||||
Task 014 (/daemon) ← 依赖 Task 013 (引擎选择)
|
||||
Task 015 (/job) ← 无依赖,可与 013 并行
|
||||
Task 016 (兼容) ← 依赖 Task 014 + 015
|
||||
```
|
||||
|
||||
## 七、设计决策记录
|
||||
|
||||
### D1: 为什么 daemon + bg sessions 合为一个命名空间?
|
||||
|
||||
用户视角:都是"后台运行的东西"。分开会导致 `claude daemon status` 看 supervisor + `claude ps` 看会话,割裂感强。合并后 `claude daemon status` 一次性展示 supervisor 状态 + 所有会话列表。
|
||||
|
||||
### D2: 为什么 rollback/up 不收入 daemon?
|
||||
|
||||
它们本质是**版本管理/环境初始化**,不是后台进程管理。`claude up` 是同步阻塞的 setup 脚本,不涉及 daemon 或后台会话。保持顶级更直观。
|
||||
|
||||
### D3: 为什么 DetachedEngine 的 attach 用 tail 而不是 IPC?
|
||||
|
||||
1. 日志文件是最简单的跨平台方案,无需额外依赖
|
||||
2. UDS Pipe IPC 系统 (usePipeIpc) 设计用于实例间通信,不是终端附着
|
||||
3. tmux attach 的体验(完整 PTY)无法在纯 detached 模式下复制,tail 是最诚实的替代
|
||||
|
||||
### D4: 为什么不用 Windows Terminal 的 tab/pane API?
|
||||
|
||||
Windows Terminal 的 `wt.exe` 新窗口/标签功能不够通用——用户可能在 VS Code、ConEmu、cmder 等终端中。detached + log 是唯一跨终端方案。
|
||||
@@ -1,7 +1,7 @@
|
||||
# KAIROS — 常驻助手模式
|
||||
|
||||
> Feature Flag: `FEATURE_KAIROS=1`(及子 Feature)
|
||||
> 实现状态:核心框架完整,部分子模块为 stub
|
||||
> 实现状态:核心框架完整,部分子模块为 stub;proactive/sleep 节奏控制已可用
|
||||
> 引用数:154(全库最大)
|
||||
|
||||
## 一、功能概述
|
||||
@@ -74,8 +74,9 @@ KAIROS 在系统提示中注入两大段落:
|
||||
|
||||
SleepTool 是 KAIROS/Proactive 的节奏控制核心。工具描述让模型理解"休眠"概念:
|
||||
- 工具名:`Sleep`
|
||||
- 功能:等待指定时间后响应 tick prompt
|
||||
- 功能:等待指定时间后响应 tick prompt;若队列出现新工作或 proactive 被关闭,会提前唤醒
|
||||
- 与 `<tick_tag>` 配合实现心跳式自主工作
|
||||
- 远程控制 surfaces 可通过 `automation_state` 看到 `standby` / `sleeping` 两种状态
|
||||
|
||||
### 3.3 Bridge 集成
|
||||
|
||||
@@ -172,8 +173,10 @@ FEATURE_KAIROS=1 FEATURE_TOKEN_BUDGET=1 bun run dev
|
||||
| `src/assistant/AssistantSessionChooser.ts` | — | Session 选择 UI(stub) |
|
||||
| `src/tools/BriefTool/` | — | BriefTool 实现(stub) |
|
||||
| `src/tools/SleepTool/prompt.ts` | ~30 | SleepTool 工具提示 |
|
||||
| `src/tools/SleepTool/SleepTool.ts` | ~200 | 休眠/唤醒与 automation metadata |
|
||||
| `src/services/mcp/channelNotification.ts` | 5 | 频道消息接入(stub) |
|
||||
| `src/memdir/memdir.ts` | — | 记忆目录管理(stub) |
|
||||
| `src/constants/prompts.ts:552-554,843-914` | 72 | 系统提示注入 |
|
||||
| `src/components/tasks/src/tasks/DreamTask/` | 3 | Dream 任务(stub) |
|
||||
| `src/proactive/index.ts` | — | Proactive 核心(stub,KAIROS 共享) |
|
||||
| `src/proactive/index.ts` | — | Proactive 核心(KAIROS 共享) |
|
||||
| `src/utils/sessionState.ts` | — | 向 bridge/CCR 暴露 automation 状态 |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# PROACTIVE — 主动模式
|
||||
|
||||
> Feature Flag: `FEATURE_PROACTIVE=1`(与 `FEATURE_KAIROS=1` 共享功能)
|
||||
> 实现状态:核心模块全部 Stub,布线完整
|
||||
> 实现状态:核心循环与 SleepTool 已落地,部分外围文档仍在补齐
|
||||
> 引用数:37
|
||||
|
||||
## 一、功能概述
|
||||
@@ -21,13 +21,13 @@ PROACTIVE 实现 Tick 驱动的自主代理。CLI 在用户不输入时也能持
|
||||
|
||||
| 模块 | 文件 | 状态 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 核心逻辑 | `src/proactive/index.ts` | **Stub** | `activateProactive()`、`deactivateProactive()`、`isProactiveActive() => false` |
|
||||
| 核心逻辑 | `src/proactive/index.ts` | **已实现** | `activateProactive()`、`deactivateProactive()`、`pause/resume`、`nextTickAt` 调度状态 |
|
||||
| SleepTool 提示 | `src/tools/SleepTool/prompt.ts` | **完整** | 工具提示定义(工具名:`Sleep`) |
|
||||
| 命令注册 | `src/commands.ts:62-65` | **布线** | 动态加载 `./commands/proactive.js` |
|
||||
| 工具注册 | `src/tools.ts:26-28` | **布线** | SleepTool 动态加载 |
|
||||
| REPL 集成 | `src/screens/REPL.tsx` | **布线** | tick 驱动逻辑、占位符、页脚 UI |
|
||||
| REPL 集成 | `src/screens/REPL.tsx` | **已实现** | tick 驱动、standby/sleeping 状态、页脚与 bridge automation metadata 上报 |
|
||||
| 系统提示 | `src/constants/prompts.ts:860-914` | **完整** | 自主工作行为指令(~55 行详细 prompt) |
|
||||
| 会话存储 | `src/utils/sessionStorage.ts:4892-4912` | **布线** | tick 消息注入对话流 |
|
||||
| 远控状态镜像 | `src/utils/sessionState.ts` | **已实现** | 向 remote-control/CCR 暴露 `automation_state` 元数据 |
|
||||
|
||||
### 2.2 系统提示内容
|
||||
|
||||
@@ -46,7 +46,7 @@ PROACTIVE 实现 Tick 驱动的自主代理。CLI 在用户不输入时也能持
|
||||
### 2.3 数据流
|
||||
|
||||
```
|
||||
activateProactive() [需要实现]
|
||||
activateProactive()
|
||||
│
|
||||
▼
|
||||
Tick 调度器启动
|
||||
@@ -62,20 +62,22 @@ Tick 调度器启动
|
||||
└── 无事可做 → 必须调用 SleepTool
|
||||
│
|
||||
▼
|
||||
SleepTool 等待 [需要实现]
|
||||
SleepTool 等待
|
||||
│
|
||||
├── 用户插入新工作 / 队列中有命令 → 立即唤醒
|
||||
├── proactive 被关闭 → 立即中断
|
||||
└── 进入休眠时向远端 surfaces 上报 `automation_state = sleeping`
|
||||
│
|
||||
▼
|
||||
下一个 tick 到达
|
||||
```
|
||||
|
||||
## 三、需要补全的内容
|
||||
## 三、当前行为补充
|
||||
|
||||
| 优先级 | 模块 | 工作量 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| 1 | `src/proactive/index.ts` | 中 | Tick 调度器、activate/deactivate 状态机、pause/resume |
|
||||
| 2 | `src/tools/SleepTool/SleepTool.ts` | 小 | 工具执行(等待指定时间后触发 tick) |
|
||||
| 3 | `src/commands/proactive.js` | 小 | `/proactive` 斜杠命令处理器 |
|
||||
| 4 | `src/hooks/useProactive.ts` | 中 | React hook(REPL 引用但不存在) |
|
||||
- `standby`:proactive 已开启,当前没有执行中的 turn,且已调度下一个 tick。
|
||||
- `sleeping`:模型显式调用 `SleepTool` 进入等待窗口。
|
||||
- remote-control/CCR 通过 `external_metadata.automation_state` 接收这两个状态,用于 Web UI 的 Autopilot 状态显示。
|
||||
- `SleepTool` 现在不是纯定时器;它会在共享命令队列出现新工作时提前醒来。
|
||||
|
||||
## 四、关键设计决策
|
||||
|
||||
@@ -101,9 +103,11 @@ FEATURE_PROACTIVE=1 FEATURE_KAIROS=1 FEATURE_KAIROS_BRIEF=1 bun run dev
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/proactive/index.ts` | 核心逻辑(stub) |
|
||||
| `src/proactive/index.ts` | 核心逻辑与 next-tick 状态 |
|
||||
| `src/tools/SleepTool/prompt.ts` | SleepTool 工具提示 |
|
||||
| `src/tools/SleepTool/SleepTool.ts` | 休眠/唤醒执行逻辑 |
|
||||
| `src/constants/prompts.ts:860-914` | 自主工作系统提示 |
|
||||
| `src/screens/REPL.tsx` | REPL tick 集成 |
|
||||
| `src/screens/REPL.tsx` | REPL tick 集成与 automation 状态上报 |
|
||||
| `src/utils/sessionStorage.ts:4892-4912` | Tick 消息注入 |
|
||||
| `src/utils/sessionState.ts` | bridge/CCR metadata 镜像 |
|
||||
| `src/components/PromptInput/PromptInputFooterLeftSide.tsx` | 页脚 UI 状态 |
|
||||
|
||||
@@ -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,9 +171,11 @@ claude bridge
|
||||
|
||||
通过 `/remote-control` 命令获取 URL 后,在浏览器打开即可使用。功能:
|
||||
|
||||
- 查看已注册的运行环境
|
||||
- 查看已注册的运行环境(environment 模式)
|
||||
- 创建和管理会话
|
||||
- 实时查看对话消息和工具调用
|
||||
- 查看 Autopilot 状态(`standby` / `sleeping`)和自动运行指示
|
||||
- 查看 authoritative task snapshots 驱动的 Tasks 面板
|
||||
- 审批 Claude Code 的工具权限请求
|
||||
|
||||
Web UI 使用 UUID 认证(无需用户账户),适合受信任网络环境。
|
||||
@@ -209,6 +217,7 @@ Web UI 使用 UUID 认证(无需用户账户),适合受信任网络环境
|
||||
9. 双向通信
|
||||
CLI ──消息/工具调用结果──► RCS ──► Browser
|
||||
CLI ◄──权限审批/指令───── RCS ◄──── Browser
|
||||
CLI ──automation_state / task_state──► RCS ──► Browser
|
||||
|
||||
10. 心跳保活(每 20 秒)
|
||||
CLI ──POST /v1/environments/:id/work/:workId/heartbeat──► RCS
|
||||
@@ -218,6 +227,13 @@ Web UI 使用 UUID 认证(无需用户账户),适合受信任网络环境
|
||||
|
||||
## 故障排查
|
||||
|
||||
### Web UI 看不到当前 Autopilot 状态
|
||||
|
||||
- `standby`:proactive 已开启,正在等待下一个 tick
|
||||
- `sleeping`:模型正在 `SleepTool` 等待窗口中
|
||||
|
||||
这两个状态通过 worker `external_metadata.automation_state` 上报。如果页面只显示普通 working spinner,优先检查 CLI 和 RCS 之间的 worker metadata PUT 是否成功。
|
||||
|
||||
### CLI 无法连接
|
||||
|
||||
```
|
||||
@@ -275,4 +291,3 @@ curl https://rcs.example.com/health
|
||||
| 依赖 | claude.ai 订阅 + OAuth | 仅需 API Key |
|
||||
|
||||
自托管模式的核心优势是:设置 `CLAUDE_BRIDGE_BASE_URL` 后,代码自动调用 `isSelfHostedBridge()` 返回 `true`,跳过所有 GrowthBook 和订阅检查,无需 claude.ai 账户即可使用。
|
||||
|
||||
|
||||
310
docs/features/stub-recovery-design-1-4.md
Normal file
310
docs/features/stub-recovery-design-1-4.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# Stub 恢复设计 1-4
|
||||
|
||||
> 日期:2026-04-12
|
||||
> 目标:基于当前代码边界,为下一阶段 4 个 stub/半 stub 命令面给出可实施的设计方案。
|
||||
> 排序原则:按建议实施顺序排序,不按问题严重性排序。
|
||||
|
||||
## 设计原则
|
||||
|
||||
- 先做能独立闭环、收益明确、改动边界清晰的项。
|
||||
- 大项拆成 `MVP` 和 `Phase 2+`,避免一次性掉进大范围恢复。
|
||||
- 优先复用已有状态、传输层、日志与配置能力,不重造协议。
|
||||
- 设计以当前仓库实际代码为准,不以旧文档的理想状态为准。
|
||||
|
||||
## 1. `claude daemon status` / `claude daemon stop`
|
||||
|
||||
### 现状
|
||||
|
||||
- `start` 路径已有完整 supervisor + worker 生命周期:
|
||||
[src/daemon/main.ts](</e:/Source_code/Claude-code-bast/src/daemon/main.ts:1>)
|
||||
[src/daemon/workerRegistry.ts](</e:/Source_code/Claude-code-bast/src/daemon/workerRegistry.ts:1>)
|
||||
- `status` / `stop` 目前只是占位输出:
|
||||
[src/daemon/main.ts](</e:/Source_code/Claude-code-bast/src/daemon/main.ts:49>)
|
||||
- `/remote-control-server` 有自己的命令内 UI 状态,但只维护当前进程内的 `daemonProcess`,并不适合作为跨进程 CLI 管理基础:
|
||||
[src/commands/remoteControlServer/remoteControlServer.tsx](</e:/Source_code/Claude-code-bast/src/commands/remoteControlServer/remoteControlServer.tsx:32>)
|
||||
|
||||
### 目标
|
||||
|
||||
- 让 `claude daemon status` 和 `claude daemon stop` 在另一个 CLI 进程中也能正确工作。
|
||||
- 不依赖 TUI 内存态,不要求当前命令进程就是启动 daemon 的那个进程。
|
||||
|
||||
### MVP 方案
|
||||
|
||||
- 新增 daemon 状态文件,例如:
|
||||
`~/.claude/daemon/remote-control.json`
|
||||
- `start` 时写入:
|
||||
- supervisor pid
|
||||
- cwd
|
||||
- startedAt
|
||||
- worker kinds
|
||||
- 最近状态
|
||||
- `status`:
|
||||
- 读取状态文件
|
||||
- 用现有进程探测能力验证 pid 是否存活
|
||||
- 输出 `running / stopped / stale`
|
||||
- stale 时自动清理状态文件
|
||||
- `stop`:
|
||||
- 读取 pid
|
||||
- 发送 `SIGTERM`
|
||||
- 等待退出
|
||||
- 超时后 `SIGKILL`
|
||||
- 清理状态文件
|
||||
|
||||
### 代码范围
|
||||
|
||||
- 新增 `src/daemon/state.ts`
|
||||
- 修改 [src/daemon/main.ts](</e:/Source_code/Claude-code-bast/src/daemon/main.ts:1>)
|
||||
- 轻量修改 [src/commands/remoteControlServer/remoteControlServer.tsx](</e:/Source_code/Claude-code-bast/src/commands/remoteControlServer/remoteControlServer.tsx:32>),让 UI 尽量读取同一份状态文件
|
||||
|
||||
### 验证
|
||||
|
||||
1. `claude daemon start`
|
||||
2. 新开终端执行 `claude daemon status`
|
||||
3. 执行 `claude daemon stop`
|
||||
4. 再次执行 `claude daemon status`,确认返回 `stopped` 或清晰的 `stale cleaned`
|
||||
|
||||
### 风险
|
||||
|
||||
- Windows 信号模型和 Unix 不同,`stop` 需要超时兜底。
|
||||
- 当前设计默认单 supervisor,不处理多实例并发。
|
||||
|
||||
### 工作量判断
|
||||
|
||||
- 小
|
||||
- 适合作为下一步的首选实现项
|
||||
|
||||
## 2. `BG_SESSIONS`
|
||||
|
||||
### 现状
|
||||
|
||||
- fast-path 已接好:
|
||||
[src/entrypoints/cli.tsx](</e:/Source_code/Claude-code-bast/src/entrypoints/cli.tsx:218>)
|
||||
- session registry 已有真实实现:
|
||||
[src/utils/concurrentSessions.ts](</e:/Source_code/Claude-code-bast/src/utils/concurrentSessions.ts:1>)
|
||||
- `exit` 在 bg session 内已会 `tmux detach-client`:
|
||||
[src/commands/exit/exit.tsx](</e:/Source_code/Claude-code-bast/src/commands/exit/exit.tsx:20>)
|
||||
- 但 CLI handler 仍全空:
|
||||
[src/cli/bg.ts](</e:/Source_code/Claude-code-bast/src/cli/bg.ts:1>)
|
||||
- task summary 仍然是 stub:
|
||||
[src/utils/taskSummary.ts](</e:/Source_code/Claude-code-bast/src/utils/taskSummary.ts:1>)
|
||||
|
||||
### 目标
|
||||
|
||||
- 先把 `ps` / `logs` / `kill` 做成真正有用的 session 管理命令。
|
||||
- 不在第一阶段就强行补完 `attach` / `--bg`。
|
||||
|
||||
### Phase 2A:MVP
|
||||
|
||||
- 实现 `ps`
|
||||
- 从 registry 读取 live sessions
|
||||
- 展示 pid、kind、sessionId、cwd、name、startedAt、bridgeSessionId
|
||||
- 如果有 activity/status,则一并展示
|
||||
- 实现 `logs`
|
||||
- 支持按 `sessionId / pid / name` 查找
|
||||
- 优先复用本地 transcript/log 读取能力
|
||||
- 如果 registry 里存在 `logPath`,支持 tail 文件
|
||||
- 实现 `kill`
|
||||
- 解析目标 session
|
||||
- 发退出信号
|
||||
- 清理 stale registry
|
||||
|
||||
### Phase 2B:后续
|
||||
|
||||
- 实现 `attach`
|
||||
- 实现 `--bg`
|
||||
- 实现 `taskSummary` 的中途状态更新
|
||||
|
||||
### 为什么要拆
|
||||
|
||||
- 现有 registry 记录了 `pid / sessionId / name / logPath`
|
||||
- 但没有可靠的 tmux attach target
|
||||
- 所以 `attach` 和 `--bg` 不是简单补 handler,而是需要补启动/附着元数据设计
|
||||
|
||||
### 代码范围
|
||||
|
||||
- 修改 [src/cli/bg.ts](</e:/Source_code/Claude-code-bast/src/cli/bg.ts:1>)
|
||||
- 修改 [src/utils/concurrentSessions.ts](</e:/Source_code/Claude-code-bast/src/utils/concurrentSessions.ts:1>) 以便后续 attach/--bg 扩展
|
||||
- 修改 [src/utils/taskSummary.ts](</e:/Source_code/Claude-code-bast/src/utils/taskSummary.ts:1>)
|
||||
- 复用:
|
||||
[src/utils/sessionStorage.ts](</e:/Source_code/Claude-code-bast/src/utils/sessionStorage.ts:3870>)
|
||||
[src/utils/udsClient.ts](</e:/Source_code/Claude-code-bast/src/utils/udsClient.ts:1>)
|
||||
|
||||
### 验证
|
||||
|
||||
1. `ps` 能列出 live sessions
|
||||
2. `logs <sessionId|pid|name>` 能输出对应日志
|
||||
3. `kill <sessionId|pid|name>` 能结束目标 session
|
||||
|
||||
### 风险
|
||||
|
||||
- `attach` / `--bg` 第二阶段需要 tmux 元数据设计
|
||||
- Windows 下 tmux 路径需要明确降级策略
|
||||
|
||||
### 工作量判断
|
||||
|
||||
- `ps/logs/kill` 中等
|
||||
- `attach/--bg` 明显更大,应分阶段
|
||||
|
||||
## 3. `TEMPLATES`
|
||||
|
||||
### 现状
|
||||
|
||||
- 命令入口只有 fast-path:
|
||||
[src/entrypoints/cli.tsx](</e:/Source_code/Claude-code-bast/src/entrypoints/cli.tsx:249>)
|
||||
- handler 是空的:
|
||||
[src/cli/handlers/templateJobs.ts](</e:/Source_code/Claude-code-bast/src/cli/handlers/templateJobs.ts:1>)
|
||||
- `markdownConfigLoader` 已把 `templates` 纳入配置目录:
|
||||
[src/utils/markdownConfigLoader.ts](</e:/Source_code/Claude-code-bast/src/utils/markdownConfigLoader.ts:29>)
|
||||
- `query / stopHooks` 已预留 job classifier 链路:
|
||||
[src/query/stopHooks.ts](</e:/Source_code/Claude-code-bast/src/query/stopHooks.ts:103>)
|
||||
- `jobs/classifier.ts` 仍是 stub:
|
||||
[src/jobs/classifier.ts](</e:/Source_code/Claude-code-bast/src/jobs/classifier.ts:1>)
|
||||
|
||||
### 目标
|
||||
|
||||
- 把 `new / list / reply` 做成可用的模板任务系统。
|
||||
- 第一阶段不碰复杂的自动分类与自动执行。
|
||||
|
||||
### MVP 方案
|
||||
|
||||
- 模板来源:
|
||||
`.claude/templates/*.md`
|
||||
- 模板格式:
|
||||
复用现有 markdown + frontmatter 解析,不另外设计 DSL
|
||||
- `list`
|
||||
- 列出所有模板
|
||||
- 显示模板名、description、路径
|
||||
- `new <template> [args...]`
|
||||
- 解析模板
|
||||
- 在 `~/.claude/jobs/<job-id>/` 下创建 job 目录
|
||||
- 写入 `template.md`、`input.txt`、`state.json`
|
||||
- 返回 job id 与目录
|
||||
- `reply <job-id> <text>`
|
||||
- 将回复写入 `replies.jsonl` 或 `input.txt`
|
||||
- 更新 `state.json`
|
||||
|
||||
### Phase 2
|
||||
|
||||
- 恢复 [src/jobs/classifier.ts](</e:/Source_code/Claude-code-bast/src/jobs/classifier.ts:1>)
|
||||
- 让带 `CLAUDE_JOB_DIR` 的 job session 在 turn 完成后自动更新 `state.json`
|
||||
- 再决定是否补自动 job runner
|
||||
|
||||
### 为什么要拆
|
||||
|
||||
- 当前证据表明这是“template job commands”,不是单纯模板列表
|
||||
- 但自动 job 运行链路没有足够现成实现,先做文件系统 job lifecycle 更稳
|
||||
|
||||
### 代码范围
|
||||
|
||||
- 修改 [src/cli/handlers/templateJobs.ts](</e:/Source_code/Claude-code-bast/src/cli/handlers/templateJobs.ts:1>)
|
||||
- 新增 `src/jobs/state.ts`
|
||||
- 新增 `src/jobs/templates.ts`
|
||||
- Phase 2 再改 [src/jobs/classifier.ts](</e:/Source_code/Claude-code-bast/src/jobs/classifier.ts:1>)
|
||||
|
||||
### 验证
|
||||
|
||||
1. `list` 能列出 `.claude/templates`
|
||||
2. `new` 能创建 job 目录和状态文件
|
||||
3. `reply` 能更新 job 内容和状态
|
||||
4. Phase 2 再验证 classifier 写状态
|
||||
|
||||
### 风险
|
||||
|
||||
- frontmatter schema 需要先定义最小字段集
|
||||
- 一旦扩展到“自动运行 job”,范围会明显膨胀
|
||||
|
||||
### 工作量判断
|
||||
|
||||
- MVP 中等
|
||||
- 完整 job 系统偏大
|
||||
|
||||
## 4. `assistant [sessionId]`
|
||||
|
||||
### 现状
|
||||
|
||||
- attach 主流程其实已经存在:
|
||||
[src/main.tsx](</e:/Source_code/Claude-code-bast/src/main.tsx:4708>)
|
||||
- 远端 viewer 所需基础模块已存在:
|
||||
[src/remote/RemoteSessionManager.ts](</e:/Source_code/Claude-code-bast/src/remote/RemoteSessionManager.ts:1>)
|
||||
[src/hooks/useAssistantHistory.ts](</e:/Source_code/Claude-code-bast/src/hooks/useAssistantHistory.ts:1>)
|
||||
[src/assistant/sessionHistory.ts](</e:/Source_code/Claude-code-bast/src/assistant/sessionHistory.ts:1>)
|
||||
- 真正 stub 的主要是:
|
||||
[src/assistant/sessionDiscovery.ts](</e:/Source_code/Claude-code-bast/src/assistant/sessionDiscovery.ts:1>)
|
||||
[src/assistant/AssistantSessionChooser.ts](</e:/Source_code/Claude-code-bast/src/assistant/AssistantSessionChooser.ts:1>)
|
||||
[src/commands/assistant/assistant.ts](</e:/Source_code/Claude-code-bast/src/commands/assistant/assistant.ts:7>)
|
||||
[src/assistant/index.ts](</e:/Source_code/Claude-code-bast/src/assistant/index.ts:1>)
|
||||
|
||||
### 目标
|
||||
|
||||
- 不一次性恢复整个 KAIROS 助手系统。
|
||||
- 先做“明确 sessionId 的 viewer attach 可用”,再逐步补 discovery / chooser / install。
|
||||
|
||||
### Phase 4A:MVP
|
||||
|
||||
- 只支持 `claude assistant <sessionId>`
|
||||
- 对 `claude assistant` 无参数模式,先返回明确提示:
|
||||
- 当前版本需要显式 `sessionId`
|
||||
- discovery 尚未启用
|
||||
- 这样可以直接复用现有 attach 分支,不必先恢复 chooser/install wizard
|
||||
|
||||
### Phase 4B
|
||||
|
||||
- 恢复 `discoverAssistantSessions()`
|
||||
- 数据来源优先复用现有 sessions / bridge / teleport API,而不是新协议
|
||||
- 让 `claude assistant` 无参数时能拿到候选 session 列表
|
||||
|
||||
### Phase 4C
|
||||
|
||||
- 恢复 `AssistantSessionChooser`
|
||||
- 多 session 时可交互选择
|
||||
|
||||
### Phase 4D
|
||||
|
||||
- 最后考虑 install wizard 辅助函数
|
||||
- 这部分属于“没有 session 时如何引导”,不是 attach 核心路径
|
||||
|
||||
### 为什么要拆
|
||||
|
||||
- attach 渲染层与远端消息通道大部分已经在
|
||||
- 真正缺的是“如何发现目标 session”和“如何交互选择”
|
||||
- 如果把 `src/assistant/index.ts` 的整套 KAIROS 正常模式也一起拉进来,范围会失控
|
||||
|
||||
### 代码范围
|
||||
|
||||
- Phase 4A:
|
||||
- [src/main.tsx](</e:/Source_code/Claude-code-bast/src/main.tsx:4708>)
|
||||
- [src/commands/assistant/index.ts](</e:/Source_code/Claude-code-bast/src/commands/assistant/index.ts:1>)
|
||||
- Phase 4B:
|
||||
- [src/assistant/sessionDiscovery.ts](</e:/Source_code/Claude-code-bast/src/assistant/sessionDiscovery.ts:1>)
|
||||
- Phase 4C:
|
||||
- [src/assistant/AssistantSessionChooser.ts](</e:/Source_code/Claude-code-bast/src/assistant/AssistantSessionChooser.ts:1>)
|
||||
- Phase 4D:
|
||||
- [src/commands/assistant/assistant.ts](</e:/Source_code/Claude-code-bast/src/commands/assistant/assistant.ts:7>)
|
||||
|
||||
### 验证
|
||||
|
||||
1. `claude assistant <sessionId>` 能进入 remote viewer
|
||||
2. 历史懒加载工作正常
|
||||
3. 无参数模式先给出明确提示
|
||||
4. 后续阶段再分别验证 discovery / chooser / install
|
||||
|
||||
### 风险
|
||||
|
||||
- 这是四项里范围最大的
|
||||
- 一旦把 KAIROS 正常模式整体拉入,会从“viewer attach”膨胀成“完整 assistant mode 恢复”
|
||||
|
||||
### 工作量判断
|
||||
|
||||
- Phase 4A 中等
|
||||
- 4A-4D 全做完很大
|
||||
|
||||
## 建议执行顺序
|
||||
|
||||
1. `claude daemon status` / `claude daemon stop`
|
||||
2. `BG_SESSIONS` 先做 `ps/logs/kill`
|
||||
3. `TEMPLATES` 先做 job 文件系统 MVP
|
||||
4. `assistant [sessionId]` 先做显式 sessionId attach,再补 discovery/chooser/install
|
||||
|
||||
## 简短结论
|
||||
|
||||
这四项里,最适合立刻实现的是 `daemon status/stop`。`BG_SESSIONS` 和 `TEMPLATES` 适合按 MVP 先补 handler 与文件系统闭环。`assistant [sessionId]` 不能整块硬上,应该按“attach → discovery → chooser → install”拆开恢复。
|
||||
@@ -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`。
|
||||
|
||||
## 五、接口定义
|
||||
|
||||
|
||||
77
docs/task/task-001-daemon-status-stop.md
Normal file
77
docs/task/task-001-daemon-status-stop.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Task 001: daemon status / stop
|
||||
|
||||
> 来源: [stub-recovery-design-1-4.md](../features/stub-recovery-design-1-4.md) 第 1 项
|
||||
> 优先级: P0 (首选实现项)
|
||||
> 工作量: 小
|
||||
> 状态: DONE
|
||||
|
||||
## 目标
|
||||
|
||||
让 `claude daemon status` 和 `claude daemon stop` 在任意 CLI 进程中都能正确工作,不依赖 TUI 内存态。
|
||||
|
||||
## 背景
|
||||
|
||||
- `start` 路径已有完整 supervisor + worker 生命周期 (`src/daemon/main.ts`, `src/daemon/workerRegistry.ts`)
|
||||
- `status` / `stop` 目前只是占位输出 (`src/daemon/main.ts:49`)
|
||||
- `/remote-control-server` 有自己的命令内 UI 状态,但只维护当前进程内的 `daemonProcess`,不适合跨进程管理
|
||||
|
||||
## 实现方案
|
||||
|
||||
### 新增文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `src/daemon/state.ts` | daemon 状态文件读写模块 |
|
||||
|
||||
### 修改文件
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `src/daemon/main.ts` | `start` 写入状态文件;`status`/`stop` 调用 state 模块 |
|
||||
| `src/commands/remoteControlServer/remoteControlServer.tsx` | 读取同一份状态文件(轻量改动) |
|
||||
|
||||
### 状态文件
|
||||
|
||||
路径: `~/.claude/daemon/remote-control.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"pid": 12345,
|
||||
"cwd": "/path/to/project",
|
||||
"startedAt": "2026-04-12T10:00:00Z",
|
||||
"workerKinds": ["bridge", "rcs"],
|
||||
"lastStatus": "running"
|
||||
}
|
||||
```
|
||||
|
||||
### status 逻辑
|
||||
|
||||
1. 读取状态文件
|
||||
2. 用进程探测验证 pid 是否存活
|
||||
3. 输出 `running` / `stopped` / `stale`
|
||||
4. stale 时自动清理状态文件
|
||||
|
||||
### stop 逻辑
|
||||
|
||||
1. 读取 pid
|
||||
2. 发送 `SIGTERM`
|
||||
3. 等待退出(超时兜底)
|
||||
4. 超时后 `SIGKILL`
|
||||
5. 清理状态文件
|
||||
|
||||
## 验证步骤
|
||||
|
||||
- [ ] `claude daemon start` 正常启动并写入状态文件
|
||||
- [ ] 新开终端执行 `claude daemon status`,显示 `running`
|
||||
- [ ] 执行 `claude daemon stop`,daemon 正常退出
|
||||
- [ ] 再次执行 `claude daemon status`,返回 `stopped` 或 `stale cleaned`
|
||||
- [ ] Windows 下 stop 超时兜底正常工作
|
||||
|
||||
## 风险
|
||||
|
||||
- Windows 信号模型和 Unix 不同,`stop` 需要超时兜底
|
||||
- 当前设计默认单 supervisor,不处理多实例并发
|
||||
|
||||
## 依赖
|
||||
|
||||
无外部依赖,可独立实施。
|
||||
80
docs/task/task-002-bg-sessions-ps-logs-kill.md
Normal file
80
docs/task/task-002-bg-sessions-ps-logs-kill.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Task 002: BG_SESSIONS — ps / logs / kill
|
||||
|
||||
> 来源: [stub-recovery-design-1-4.md](../features/stub-recovery-design-1-4.md) 第 2 项
|
||||
> 优先级: P1
|
||||
> 工作量: 中等
|
||||
> 状态: DONE
|
||||
> 阶段: Phase 2A (MVP)
|
||||
|
||||
## 目标
|
||||
|
||||
把 `ps` / `logs` / `kill` 做成真正有用的 session 管理命令。不在第一阶段补完 `attach` / `--bg`。
|
||||
|
||||
## 背景
|
||||
|
||||
- fast-path 已接好 (`src/entrypoints/cli.tsx:218`)
|
||||
- session registry 已有真实实现 (`src/utils/concurrentSessions.ts`)
|
||||
- `exit` 在 bg session 内已会 `tmux detach-client` (`src/commands/exit/exit.tsx:20`)
|
||||
- CLI handler 仍全空 (`src/cli/bg.ts`)
|
||||
- task summary 仍然是 stub (`src/utils/taskSummary.ts`)
|
||||
|
||||
## 实现方案
|
||||
|
||||
### 修改文件
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `src/cli/bg.ts` | 实现 `ps` / `logs` / `kill` handler |
|
||||
| `src/utils/concurrentSessions.ts` | 扩展以便后续 attach/--bg 使用 |
|
||||
| `src/utils/taskSummary.ts` | 补充基础实现 |
|
||||
|
||||
### 复用模块
|
||||
|
||||
- `src/utils/sessionStorage.ts` — session 存储
|
||||
- `src/utils/udsClient.ts` — UDS 通信
|
||||
|
||||
### ps 命令
|
||||
|
||||
- 从 registry 读取 live sessions
|
||||
- 展示: pid, kind, sessionId, cwd, name, startedAt, bridgeSessionId
|
||||
- 如果有 activity/status,一并展示
|
||||
|
||||
### logs 命令
|
||||
|
||||
- 支持按 `sessionId` / `pid` / `name` 查找
|
||||
- 优先复用本地 transcript/log 读取能力
|
||||
- 如果 registry 里存在 `logPath`,支持 tail 文件
|
||||
|
||||
### kill 命令
|
||||
|
||||
- 解析目标 session
|
||||
- 发退出信号
|
||||
- 清理 stale registry
|
||||
|
||||
## 验证步骤
|
||||
|
||||
- [ ] `ps` 能列出当前 live sessions
|
||||
- [ ] `logs <sessionId|pid|name>` 能输出对应日志
|
||||
- [ ] `kill <sessionId|pid|name>` 能结束目标 session 并清理 registry
|
||||
- [ ] 无 live session 时各命令有明确提示
|
||||
|
||||
## Phase 2B (后续)
|
||||
|
||||
- [ ] 实现 `attach`
|
||||
- [ ] 实现 `--bg`
|
||||
- [ ] 实现 `taskSummary` 的中途状态更新
|
||||
|
||||
### 为什么拆分
|
||||
|
||||
- 现有 registry 记录了 `pid / sessionId / name / logPath`
|
||||
- 但没有可靠的 tmux attach target
|
||||
- `attach` 和 `--bg` 需要补启动/附着元数据设计,不是简单补 handler
|
||||
|
||||
## 风险
|
||||
|
||||
- `attach` / `--bg` 第二阶段需要 tmux 元数据设计
|
||||
- Windows 下 tmux 路径需要明确降级策略
|
||||
|
||||
## 依赖
|
||||
|
||||
- Task 001 (daemon 状态管理可复用模式,但非硬性依赖)
|
||||
87
docs/task/task-003-templates-job-mvp.md
Normal file
87
docs/task/task-003-templates-job-mvp.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Task 003: TEMPLATES — job 文件系统 MVP
|
||||
|
||||
> 来源: [stub-recovery-design-1-4.md](../features/stub-recovery-design-1-4.md) 第 3 项
|
||||
> 优先级: P2
|
||||
> 工作量: 中等
|
||||
> 状态: DONE
|
||||
> 阶段: MVP
|
||||
|
||||
## 目标
|
||||
|
||||
把 `new` / `list` / `reply` 做成可用的模板任务系统。第一阶段不碰复杂的自动分类与自动执行。
|
||||
|
||||
## 背景
|
||||
|
||||
- 命令入口只有 fast-path (`src/entrypoints/cli.tsx:249`)
|
||||
- handler 是空的 (`src/cli/handlers/templateJobs.ts`)
|
||||
- `markdownConfigLoader` 已把 `templates` 纳入配置目录 (`src/utils/markdownConfigLoader.ts:29`)
|
||||
- `query/stopHooks` 已预留 job classifier 链路 (`src/query/stopHooks.ts:103`)
|
||||
- `jobs/classifier.ts` 仍是 stub (`src/jobs/classifier.ts`)
|
||||
|
||||
## 实现方案
|
||||
|
||||
### 新增文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `src/jobs/state.ts` | job 状态管理 |
|
||||
| `src/jobs/templates.ts` | 模板解析与列表 |
|
||||
|
||||
### 修改文件
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `src/cli/handlers/templateJobs.ts` | 实现 `new` / `list` / `reply` handler |
|
||||
|
||||
### 模板来源
|
||||
|
||||
`.claude/templates/*.md`
|
||||
|
||||
### 模板格式
|
||||
|
||||
复用现有 markdown + frontmatter 解析,不另外设计 DSL。
|
||||
|
||||
### list 命令
|
||||
|
||||
- 列出所有模板
|
||||
- 显示: 模板名, description, 路径
|
||||
|
||||
### new 命令
|
||||
|
||||
- 解析模板
|
||||
- 在 `~/.claude/jobs/<job-id>/` 下创建 job 目录
|
||||
- 写入 `template.md`, `input.txt`, `state.json`
|
||||
- 返回 job id 与目录路径
|
||||
|
||||
### reply 命令
|
||||
|
||||
- 将回复写入 `replies.jsonl` 或 `input.txt`
|
||||
- 更新 `state.json`
|
||||
|
||||
## 验证步骤
|
||||
|
||||
- [ ] `list` 能列出 `.claude/templates` 下的所有模板
|
||||
- [ ] `new <template> [args...]` 能创建 job 目录和状态文件
|
||||
- [ ] `reply <job-id> <text>` 能更新 job 内容和状态
|
||||
- [ ] frontmatter schema 最小字段集已定义
|
||||
|
||||
## Phase 2 (后续)
|
||||
|
||||
- [ ] 恢复 `src/jobs/classifier.ts`
|
||||
- [ ] 让带 `CLAUDE_JOB_DIR` 的 job session 在 turn 完成后自动更新 `state.json`
|
||||
- [ ] 再决定是否补自动 job runner
|
||||
|
||||
### 为什么拆分
|
||||
|
||||
- 当前是 "template job commands",不是单纯模板列表
|
||||
- 自动 job 运行链路没有足够现成实现
|
||||
- 先做文件系统 job lifecycle 更稳
|
||||
|
||||
## 风险
|
||||
|
||||
- frontmatter schema 需要先定义最小字段集
|
||||
- 一旦扩展到"自动运行 job",范围会明显膨胀
|
||||
|
||||
## 依赖
|
||||
|
||||
无硬性依赖,可独立实施。
|
||||
103
docs/task/task-004-assistant-session-attach.md
Normal file
103
docs/task/task-004-assistant-session-attach.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Task 004: assistant [sessionId] — 分阶段恢复
|
||||
|
||||
> 来源: [stub-recovery-design-1-4.md](../features/stub-recovery-design-1-4.md) 第 4 项
|
||||
> 优先级: P3
|
||||
> 工作量: Phase 4A 中等,4A-4D 全做完很大
|
||||
> 状态: Phase 4A DONE, 4B-4D TODO
|
||||
|
||||
## 目标
|
||||
|
||||
不一次性恢复整个 KAIROS 助手系统。先做"明确 sessionId 的 viewer attach 可用",再逐步补 discovery / chooser / install。
|
||||
|
||||
## 背景
|
||||
|
||||
- attach 主流程已存在 (`src/main.tsx:4708`)
|
||||
- 远端 viewer 所需基础模块已存在:
|
||||
- `src/remote/RemoteSessionManager.ts`
|
||||
- `src/hooks/useAssistantHistory.ts`
|
||||
- `src/assistant/sessionHistory.ts`
|
||||
- 真正 stub 的主要是:
|
||||
- `src/assistant/sessionDiscovery.ts`
|
||||
- `src/assistant/AssistantSessionChooser.ts`
|
||||
- `src/commands/assistant/assistant.ts:7`
|
||||
- `src/assistant/index.ts`
|
||||
|
||||
## 分阶段实现
|
||||
|
||||
### Phase 4A: MVP — 显式 sessionId attach
|
||||
|
||||
**修改文件:**
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `src/main.tsx` | 确保 attach 分支可用 |
|
||||
| `src/commands/assistant/index.ts` | 实现显式 sessionId 参数入口 |
|
||||
|
||||
**行为:**
|
||||
- `claude assistant <sessionId>` — 进入 remote viewer
|
||||
- `claude assistant` (无参数) — 返回明确提示: 当前版本需要显式 sessionId,discovery 尚未启用
|
||||
|
||||
**验证:**
|
||||
- [ ] `claude assistant <sessionId>` 能进入 remote viewer
|
||||
- [ ] 历史懒加载工作正常
|
||||
- [ ] 无参数模式给出明确提示
|
||||
|
||||
### Phase 4B: session discovery
|
||||
|
||||
**修改文件:**
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `src/assistant/sessionDiscovery.ts` | 恢复 `discoverAssistantSessions()` |
|
||||
|
||||
**行为:**
|
||||
- 数据来源优先复用现有 sessions / bridge / teleport API,不新增协议
|
||||
- `claude assistant` 无参数时能拿到候选 session 列表
|
||||
|
||||
**验证:**
|
||||
- [ ] 无参数调用能列出可用 sessions
|
||||
- [ ] 数据来源复用现有通道
|
||||
|
||||
### Phase 4C: session chooser
|
||||
|
||||
**修改文件:**
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `src/assistant/AssistantSessionChooser.ts` | 恢复交互式选择器 |
|
||||
|
||||
**行为:**
|
||||
- 多 session 时可交互选择
|
||||
|
||||
**验证:**
|
||||
- [ ] 多个 session 时弹出选择器
|
||||
- [ ] 选择后正确 attach
|
||||
|
||||
### Phase 4D: install wizard
|
||||
|
||||
**修改文件:**
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `src/commands/assistant/assistant.ts` | 恢复 install wizard 辅助函数 |
|
||||
|
||||
**行为:**
|
||||
- 没有 session 时如何引导用户
|
||||
|
||||
**验证:**
|
||||
- [ ] 无可用 session 时引导用户创建/连接
|
||||
|
||||
## 为什么拆分
|
||||
|
||||
- attach 渲染层与远端消息通道大部分已在
|
||||
- 真正缺的是"如何发现目标 session"和"如何交互选择"
|
||||
- 如果把 `src/assistant/index.ts` 的整套 KAIROS 正常模式也一起拉进来,范围会失控
|
||||
|
||||
## 风险
|
||||
|
||||
- 这是四项里范围最大的
|
||||
- 一旦把 KAIROS 正常模式整体拉入,会从"viewer attach"膨胀成"完整 assistant mode 恢复"
|
||||
|
||||
## 依赖
|
||||
|
||||
- Task 002 的 session registry 模式可复用
|
||||
196
docs/task/task-013-bg-engine-abstraction.md
Normal file
196
docs/task/task-013-bg-engine-abstraction.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# Task 013: BgEngine 跨平台后台引擎抽象
|
||||
|
||||
> 设计文档: [daemon-restructure-design.md](../features/daemon-restructure-design.md) § 四
|
||||
> 依赖: 无
|
||||
> 分支: `feat/integrate-5-branches`
|
||||
|
||||
## 目标
|
||||
|
||||
将 `src/cli/bg.ts` 中硬编码的 tmux 逻辑提取为引擎抽象层,实现 TmuxEngine + DetachedEngine,使后台会话功能在 Windows / macOS / Linux 上都能工作。
|
||||
|
||||
## 背景
|
||||
|
||||
当前 `bg.ts` 中 `handleBgFlag()` 和 `attachHandler()` 直接调用 tmux 命令。Windows 上 `--bg` 直接报错退出。需要一个引擎抽象层,根据平台和可用工具自动选择最佳方案。
|
||||
|
||||
## 文件清单
|
||||
|
||||
### 新增
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `src/cli/bg/engine.ts` | BgEngine 接口 + BgStartOptions/BgStartResult 类型 |
|
||||
| `src/cli/bg/engines/tmux.ts` | TmuxEngine: 从 `bg.ts` 提取 tmux 相关逻辑 |
|
||||
| `src/cli/bg/engines/detached.ts` | DetachedEngine: spawn({ detached }) + logFile 重定向 |
|
||||
| `src/cli/bg/engines/index.ts` | selectEngine() 自动选择 + re-export |
|
||||
| `src/cli/bg/tail.ts` | 跨平台日志 tail: fs.watch + 轮询 fallback |
|
||||
|
||||
### 修改
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `src/cli/bg.ts` | `handleBgFlag()` 改为调用 `selectEngine().start()`;`attachHandler()` 改为调用 `engine.attach()` |
|
||||
|
||||
## 实现方案
|
||||
|
||||
### 1. BgEngine 接口 (`src/cli/bg/engine.ts`)
|
||||
|
||||
```typescript
|
||||
export interface BgEngine {
|
||||
readonly name: string
|
||||
available(): Promise<boolean>
|
||||
start(opts: BgStartOptions): Promise<BgStartResult>
|
||||
attach(session: SessionEntry): Promise<void>
|
||||
}
|
||||
|
||||
export interface BgStartOptions {
|
||||
sessionName: string
|
||||
args: string[] // CLI args (去除 --bg)
|
||||
env: Record<string, string | undefined>
|
||||
logPath: string
|
||||
cwd: string
|
||||
}
|
||||
|
||||
export interface BgStartResult {
|
||||
pid: number
|
||||
sessionName: string
|
||||
logPath: string
|
||||
engineUsed: 'tmux' | 'detached'
|
||||
}
|
||||
```
|
||||
|
||||
### 2. TmuxEngine (`src/cli/bg/engines/tmux.ts`)
|
||||
|
||||
从 `bg.ts:handleBgFlag()` 和 `bg.ts:attachHandler()` 提取:
|
||||
- `available()`: `execFileNoThrow('tmux', ['-V'])` 返回 code === 0
|
||||
- `start()`: `tmux new-session -d -s <name> <cmd>`
|
||||
- `attach()`: `tmux attach-session -t <session.tmuxSessionName>`
|
||||
|
||||
### 3. DetachedEngine (`src/cli/bg/engines/detached.ts`)
|
||||
|
||||
```typescript
|
||||
export class DetachedEngine implements BgEngine {
|
||||
readonly name = 'detached'
|
||||
|
||||
async available(): Promise<boolean> {
|
||||
return true // 总是可用
|
||||
}
|
||||
|
||||
async start(opts: BgStartOptions): Promise<BgStartResult> {
|
||||
const logFd = openSync(opts.logPath, 'a')
|
||||
const child = spawn(process.execPath, [process.argv[1]!, ...opts.args], {
|
||||
detached: true,
|
||||
stdio: ['ignore', logFd, logFd],
|
||||
env: opts.env,
|
||||
cwd: opts.cwd,
|
||||
})
|
||||
child.unref()
|
||||
closeSync(logFd)
|
||||
|
||||
return {
|
||||
pid: child.pid!,
|
||||
sessionName: opts.sessionName,
|
||||
logPath: opts.logPath,
|
||||
engineUsed: 'detached',
|
||||
}
|
||||
}
|
||||
|
||||
async attach(session: SessionEntry): Promise<void> {
|
||||
// 委托给 tail.ts
|
||||
await tailLog(session.logPath!)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 日志 Tail (`src/cli/bg/tail.ts`)
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 跨平台实时日志输出。Ctrl+C 退出,不杀后台进程。
|
||||
*
|
||||
* 策略:
|
||||
* 1. 读取已有内容输出
|
||||
* 2. fs.watch() 监听文件变化 (主方案)
|
||||
* 3. 如果 fs.watch 不可靠 (某些 Windows 网络驱动器),fallback 到 500ms 轮询
|
||||
*/
|
||||
export async function tailLog(logPath: string): Promise<void>
|
||||
```
|
||||
|
||||
### 5. 引擎选择 (`src/cli/bg/engines/index.ts`)
|
||||
|
||||
```typescript
|
||||
export async function selectEngine(): Promise<BgEngine> {
|
||||
if (process.platform === 'win32') {
|
||||
return new DetachedEngine()
|
||||
}
|
||||
const tmux = new TmuxEngine()
|
||||
if (await tmux.available()) {
|
||||
return tmux
|
||||
}
|
||||
return new DetachedEngine()
|
||||
}
|
||||
```
|
||||
|
||||
### 6. bg.ts 重构
|
||||
|
||||
`handleBgFlag()` 改名为 `handleBgStart()`,内部逻辑:
|
||||
```typescript
|
||||
export async function handleBgStart(args: string[]): Promise<void> {
|
||||
const engine = await selectEngine()
|
||||
const sessionName = `claude-bg-${randomUUID().slice(0, 8)}`
|
||||
const logPath = join(getClaudeConfigHomeDir(), 'sessions', 'logs', `${sessionName}.log`)
|
||||
|
||||
const result = await engine.start({
|
||||
sessionName,
|
||||
args: filteredArgs,
|
||||
env: { ...process.env, CLAUDE_CODE_SESSION_KIND: 'bg', ... },
|
||||
logPath,
|
||||
cwd: process.cwd(),
|
||||
})
|
||||
|
||||
console.log(`Background session started: ${result.sessionName}`)
|
||||
console.log(` Engine: ${result.engineUsed}`)
|
||||
console.log(` Log: ${result.logPath}`)
|
||||
console.log(` Use \`claude daemon attach ${result.sessionName}\` to reconnect.`)
|
||||
}
|
||||
```
|
||||
|
||||
`attachHandler()` 根据 `session.engine` 字段选择引擎:
|
||||
```typescript
|
||||
export async function attachHandler(target: string | undefined): Promise<void> {
|
||||
// ... 找到 session
|
||||
if (session.engine === 'tmux' && session.tmuxSessionName) {
|
||||
const tmux = new TmuxEngine()
|
||||
await tmux.attach(session)
|
||||
} else {
|
||||
const detached = new DetachedEngine()
|
||||
await detached.attach(session)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## SessionEntry 扩展
|
||||
|
||||
`sessions/<PID>.json` 新增 `engine` 字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"pid": 12345,
|
||||
"engine": "detached",
|
||||
"logPath": "~/.claude/sessions/logs/claude-bg-a1b2c3d4.log",
|
||||
"sessionId": "...",
|
||||
"cwd": "..."
|
||||
}
|
||||
```
|
||||
|
||||
兼容旧格式: 如果 `engine` 字段缺失,检查 `tmuxSessionName` 存在则为 `tmux`,否则为 `detached`。
|
||||
|
||||
## 验证清单
|
||||
|
||||
- [ ] Windows: `claude daemon bg` 启动后台会话,无 tmux 依赖
|
||||
- [ ] Windows: `claude daemon attach <name>` 以 tail 模式附着,Ctrl+C 退出不杀进程
|
||||
- [ ] macOS/Linux (有 tmux): 行为与当前一致
|
||||
- [ ] macOS/Linux (无 tmux): 自动 fallback 到 detached 引擎
|
||||
- [ ] `claude daemon status` 正确显示 engine 类型
|
||||
- [ ] 旧格式 session JSON (无 engine 字段) 兼容
|
||||
- [ ] tsc --noEmit 零错误
|
||||
- [ ] bun test 通过
|
||||
275
docs/task/task-014-daemon-command-hierarchy.md
Normal file
275
docs/task/task-014-daemon-command-hierarchy.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# Task 014: /daemon 命令层级化
|
||||
|
||||
> 设计文档: [daemon-restructure-design.md](../features/daemon-restructure-design.md) § 三.1
|
||||
> 依赖: Task 013 (BgEngine 抽象)
|
||||
> 分支: `feat/integrate-5-branches`
|
||||
|
||||
## 目标
|
||||
|
||||
将散落的 `daemon start/stop/status` + `ps/logs/attach/kill` + `--bg` 统一收归 `/daemon` 命名空间,实现 CLI + REPL 双注册。
|
||||
|
||||
## 背景
|
||||
|
||||
当前这些命令注册在两个互不关联的位置:
|
||||
- `cli.tsx:203-212`: `daemon [start|status|stop]` → `daemon/main.ts`
|
||||
- `cli.tsx:217-246`: `ps|logs|attach|kill|--bg` → `cli/bg.ts`
|
||||
|
||||
需要合并为统一的 `claude daemon <subcommand>` 入口,并新增 REPL `/daemon` 斜杠命令。
|
||||
|
||||
## 文件清单
|
||||
|
||||
### 新增
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `src/commands/daemon/index.ts` | `/daemon` REPL 斜杠命令注册 (type: local-jsx) |
|
||||
| `src/commands/daemon/daemon.tsx` | `/daemon` 子命令路由 + status UI 组件 |
|
||||
|
||||
### 修改
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `src/entrypoints/cli.tsx` | 统一 daemon 快速路径: `daemon <sub>` 路由到对应 handler。旧命令 `ps/logs/attach/kill` 保留但输出 deprecation 警告后代理 |
|
||||
| `src/commands.ts` | 注册 `/daemon` 斜杠命令 (feature-gated: DAEMON \|\| BG_SESSIONS) |
|
||||
| `src/daemon/main.ts` | `daemonMain()` 扩展: 支持 `bg/attach/logs/kill/ps` 子命令 (委托给 bg.ts handlers) |
|
||||
|
||||
## 实现方案
|
||||
|
||||
### 1. CLI 快速路径统一 (`cli.tsx`)
|
||||
|
||||
**改前** (两段独立路由):
|
||||
```typescript
|
||||
// 段 1: daemon
|
||||
if (feature('DAEMON') && args[0] === 'daemon') {
|
||||
await daemonMain(args.slice(1))
|
||||
}
|
||||
// 段 2: bg sessions
|
||||
if (feature('BG_SESSIONS') && ['ps','logs','attach','kill'].includes(args[0])) {
|
||||
// ...switch/case
|
||||
}
|
||||
```
|
||||
|
||||
**改后** (统一入口):
|
||||
```typescript
|
||||
// 统一 daemon 入口 — 合并 daemon supervisor + bg sessions
|
||||
if (
|
||||
(feature('DAEMON') || feature('BG_SESSIONS')) &&
|
||||
args[0] === 'daemon'
|
||||
) {
|
||||
profileCheckpoint('cli_daemon_path')
|
||||
const { enableConfigs } = await import('../utils/config.js')
|
||||
enableConfigs()
|
||||
const { initSinks } = await import('../utils/sinks.js')
|
||||
initSinks()
|
||||
const { daemonMain } = await import('../daemon/main.js')
|
||||
await daemonMain(args.slice(1))
|
||||
return
|
||||
}
|
||||
|
||||
// --bg 快捷方式 → daemon bg
|
||||
if (
|
||||
feature('BG_SESSIONS') &&
|
||||
(args.includes('--bg') || args.includes('--background'))
|
||||
) {
|
||||
profileCheckpoint('cli_daemon_path')
|
||||
const { enableConfigs } = await import('../utils/config.js')
|
||||
enableConfigs()
|
||||
const bg = await import('../cli/bg.js')
|
||||
await bg.handleBgStart(args.filter(a => a !== '--bg' && a !== '--background'))
|
||||
return
|
||||
}
|
||||
|
||||
// 向后兼容: ps/logs/attach/kill → daemon <sub> (deprecated)
|
||||
if (
|
||||
feature('BG_SESSIONS') &&
|
||||
['ps', 'logs', 'attach', 'kill'].includes(args[0] ?? '')
|
||||
) {
|
||||
const mapped = args[0] === 'ps' ? 'status' : args[0]
|
||||
console.error(`[deprecated] Use: claude daemon ${mapped} ${args.slice(1).join(' ')}`.trim())
|
||||
const { enableConfigs } = await import('../utils/config.js')
|
||||
enableConfigs()
|
||||
const { daemonMain } = await import('../daemon/main.js')
|
||||
await daemonMain([args[0]!, ...args.slice(1)])
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
### 2. daemonMain 扩展 (`daemon/main.ts`)
|
||||
|
||||
```typescript
|
||||
export async function daemonMain(args: string[]): Promise<void> {
|
||||
const subcommand = args[0] || 'status'
|
||||
|
||||
switch (subcommand) {
|
||||
// --- Supervisor 管理 ---
|
||||
case 'start':
|
||||
await runSupervisor(args.slice(1))
|
||||
break
|
||||
case 'stop':
|
||||
await handleDaemonStop()
|
||||
break
|
||||
|
||||
// --- 会话管理 (委托给 bg.ts) ---
|
||||
case 'status':
|
||||
case 'ps':
|
||||
await showUnifiedStatus() // 新: daemon 状态 + 会话列表
|
||||
break
|
||||
case 'bg':
|
||||
const bg = await import('../cli/bg.js')
|
||||
await bg.handleBgStart(args.slice(1))
|
||||
break
|
||||
case 'attach':
|
||||
const bg2 = await import('../cli/bg.js')
|
||||
await bg2.attachHandler(args[1])
|
||||
break
|
||||
case 'logs':
|
||||
const bg3 = await import('../cli/bg.js')
|
||||
await bg3.logsHandler(args[1])
|
||||
break
|
||||
case 'kill':
|
||||
const bg4 = await import('../cli/bg.js')
|
||||
await bg4.killHandler(args[1])
|
||||
break
|
||||
|
||||
case '--help': case '-h': case 'help':
|
||||
printHelp()
|
||||
break
|
||||
default:
|
||||
console.error(`Unknown daemon subcommand: ${subcommand}`)
|
||||
printHelp()
|
||||
process.exitCode = 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 统一状态面板 (`showUnifiedStatus`)
|
||||
|
||||
```typescript
|
||||
async function showUnifiedStatus(): Promise<void> {
|
||||
// 1. Daemon supervisor 状态
|
||||
const daemonResult = queryDaemonStatus()
|
||||
console.log('=== Daemon Supervisor ===')
|
||||
switch (daemonResult.status) {
|
||||
case 'running':
|
||||
console.log(` Status: running (PID: ${daemonResult.state!.pid})`)
|
||||
console.log(` Workers: ${daemonResult.state!.workerKinds.join(', ')}`)
|
||||
break
|
||||
case 'stopped':
|
||||
console.log(' Status: stopped')
|
||||
break
|
||||
case 'stale':
|
||||
console.log(' Status: stale (cleaned up)')
|
||||
break
|
||||
}
|
||||
|
||||
// 2. 后台会话列表
|
||||
console.log('\n=== Background Sessions ===')
|
||||
const bg = await import('../cli/bg.js')
|
||||
await bg.psHandler([])
|
||||
}
|
||||
```
|
||||
|
||||
### 4. REPL 斜杠命令注册
|
||||
|
||||
**`src/commands/daemon/index.ts`**:
|
||||
```typescript
|
||||
import type { Command } from '../../commands.js'
|
||||
import { feature } from 'bun:bundle'
|
||||
|
||||
const daemon = {
|
||||
type: 'local-jsx',
|
||||
name: 'daemon',
|
||||
description: 'Manage background sessions and daemon',
|
||||
argumentHint: '[status|start|stop|bg|attach|logs|kill]',
|
||||
isEnabled: () => {
|
||||
if (feature('DAEMON')) return true
|
||||
if (feature('BG_SESSIONS')) return true
|
||||
return false
|
||||
},
|
||||
load: () => import('./daemon.js'),
|
||||
} satisfies Command
|
||||
|
||||
export default daemon
|
||||
```
|
||||
|
||||
**`src/commands/daemon/daemon.tsx`**:
|
||||
```typescript
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
args: string,
|
||||
): Promise<React.ReactNode> {
|
||||
const parts = args.trim().split(/\s+/)
|
||||
const sub = parts[0] || 'status'
|
||||
|
||||
switch (sub) {
|
||||
case 'status':
|
||||
case 'ps':
|
||||
// 调用 showUnifiedStatus,捕获输出
|
||||
// 返回文本结果
|
||||
break
|
||||
case 'bg':
|
||||
// REPL 中启动后台会话
|
||||
break
|
||||
case 'start':
|
||||
case 'stop':
|
||||
case 'attach':
|
||||
case 'logs':
|
||||
case 'kill':
|
||||
// 委托给对应 handler
|
||||
break
|
||||
default:
|
||||
onDone(`Unknown: ${sub}. Use: status|start|stop|bg|attach|logs|kill`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`src/commands.ts`** 添加:
|
||||
```typescript
|
||||
// 条件导入
|
||||
const daemonCmd =
|
||||
feature('DAEMON') || feature('BG_SESSIONS')
|
||||
? require('./commands/daemon/index.js').default
|
||||
: null
|
||||
|
||||
// COMMANDS 数组中添加
|
||||
...(daemonCmd ? [daemonCmd] : []),
|
||||
```
|
||||
|
||||
### 5. 更新 help 文本 (`daemon/main.ts`)
|
||||
|
||||
```
|
||||
Claude Code Daemon — background process management
|
||||
|
||||
USAGE
|
||||
claude daemon [subcommand]
|
||||
|
||||
SUBCOMMANDS
|
||||
status Show daemon and session status (default)
|
||||
start Start the daemon supervisor
|
||||
stop Stop the daemon
|
||||
bg Start a background session
|
||||
attach Attach to a background session
|
||||
logs Show session logs
|
||||
kill Kill a session
|
||||
help Show this help
|
||||
|
||||
REPL
|
||||
/daemon [subcommand] Same commands available in interactive mode
|
||||
```
|
||||
|
||||
## 验证清单
|
||||
|
||||
- [ ] `claude daemon` (无参数) 显示统一状态面板
|
||||
- [ ] `claude daemon status` 显示 supervisor + 会话列表
|
||||
- [ ] `claude daemon start/stop` 与当前行为一致
|
||||
- [ ] `claude daemon bg` 启动后台会话 (调用 BgEngine)
|
||||
- [ ] `claude daemon attach/logs/kill <target>` 功能正常
|
||||
- [ ] `claude ps` 输出 deprecation 警告 + 正常工作
|
||||
- [ ] `claude logs/attach/kill` 同上
|
||||
- [ ] `claude --bg` 快捷方式正常
|
||||
- [ ] REPL 中 `/daemon` 可用,tab 补全显示
|
||||
- [ ] REPL 中 `/daemon status` 显示状态信息
|
||||
- [ ] tsc --noEmit 零错误
|
||||
- [ ] bun test 通过
|
||||
177
docs/task/task-015-job-command-hierarchy.md
Normal file
177
docs/task/task-015-job-command-hierarchy.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Task 015: /job 命令层级化
|
||||
|
||||
> 设计文档: [daemon-restructure-design.md](../features/daemon-restructure-design.md) § 三.2
|
||||
> 依赖: 无 (可与 Task 013 并行)
|
||||
> 分支: `feat/integrate-5-branches`
|
||||
|
||||
## 目标
|
||||
|
||||
将 `claude new/list/reply` 收归 `/job` 命名空间,实现 CLI + REPL 双注册。
|
||||
|
||||
## 背景
|
||||
|
||||
当前 `new`, `list`, `reply` 是顶级 CLI 命令 (`cli.tsx:250-261`),容易与其他命令冲突(特别是 `list` 这种通用词)。需要收归 `claude job <subcommand>` 并新增 REPL `/job` 入口。
|
||||
|
||||
## 文件清单
|
||||
|
||||
### 新增
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `src/commands/job/index.ts` | `/job` REPL 斜杠命令注册 |
|
||||
| `src/commands/job/job.tsx` | `/job` 子命令路由 |
|
||||
|
||||
### 修改
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `src/entrypoints/cli.tsx` | 新增 `job` 快速路径 + 旧 `new/list/reply` deprecation 代理 |
|
||||
| `src/commands.ts` | 注册 `/job` 斜杠命令 |
|
||||
|
||||
### 不动
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `src/cli/handlers/templateJobs.ts` | 内部 handler 不变,只是被调用方式变了 |
|
||||
| `src/jobs/state.ts` | job 状态管理不变 |
|
||||
| `src/jobs/templates.ts` | 模板发现不变 |
|
||||
| `src/jobs/classifier.ts` | 任务分类器不变 |
|
||||
|
||||
## 实现方案
|
||||
|
||||
### 1. CLI 快速路径 (`cli.tsx`)
|
||||
|
||||
**改后**:
|
||||
```typescript
|
||||
// 新: claude job <subcommand>
|
||||
if (
|
||||
feature('TEMPLATES') &&
|
||||
args[0] === 'job'
|
||||
) {
|
||||
profileCheckpoint('cli_templates_path')
|
||||
const { templatesMain } = await import('../cli/handlers/templateJobs.js')
|
||||
await templatesMain(args.slice(1))
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// 向后兼容 (deprecated)
|
||||
if (
|
||||
feature('TEMPLATES') &&
|
||||
(args[0] === 'new' || args[0] === 'list' || args[0] === 'reply')
|
||||
) {
|
||||
console.error(`[deprecated] Use: claude job ${args[0]} ${args.slice(1).join(' ')}`.trim())
|
||||
profileCheckpoint('cli_templates_path')
|
||||
const { templatesMain } = await import('../cli/handlers/templateJobs.js')
|
||||
await templatesMain(args)
|
||||
process.exit(0)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. templateJobs.ts 新增 status 子命令
|
||||
|
||||
在现有 `switch` 中增加:
|
||||
```typescript
|
||||
case 'status':
|
||||
handleStatus(args.slice(1))
|
||||
break
|
||||
```
|
||||
|
||||
```typescript
|
||||
function handleStatus(args: string[]): void {
|
||||
const jobId = args[0]
|
||||
if (!jobId) {
|
||||
console.error('Usage: claude job status <job-id>')
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
const state = readJobState(jobId)
|
||||
if (!state) {
|
||||
console.error(`Job not found: ${jobId}`)
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
console.log(`Job: ${state.jobId}`)
|
||||
console.log(` Template: ${state.templateName}`)
|
||||
console.log(` Status: ${state.status}`)
|
||||
console.log(` Created: ${state.createdAt}`)
|
||||
console.log(` Updated: ${state.updatedAt}`)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. REPL 斜杠命令
|
||||
|
||||
**`src/commands/job/index.ts`**:
|
||||
```typescript
|
||||
import type { Command } from '../../commands.js'
|
||||
import { feature } from 'bun:bundle'
|
||||
|
||||
const job = {
|
||||
type: 'local-jsx',
|
||||
name: 'job',
|
||||
description: 'Manage template jobs',
|
||||
argumentHint: '[list|new|reply|status]',
|
||||
isEnabled: () => {
|
||||
if (feature('TEMPLATES')) return true
|
||||
return false
|
||||
},
|
||||
load: () => import('./job.js'),
|
||||
} satisfies Command
|
||||
|
||||
export default job
|
||||
```
|
||||
|
||||
**`src/commands/job/job.tsx`**:
|
||||
```typescript
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
_context: LocalJSXCommandContext,
|
||||
args: string,
|
||||
): Promise<React.ReactNode> {
|
||||
const parts = args.trim().split(/\s+/)
|
||||
const sub = parts[0] || 'list'
|
||||
|
||||
// 委托给 templatesMain
|
||||
const { templatesMain } = await import('../../cli/handlers/templateJobs.js')
|
||||
|
||||
// 捕获 console.log 输出作为结果返回给 REPL
|
||||
const lines: string[] = []
|
||||
const origLog = console.log
|
||||
const origError = console.error
|
||||
console.log = (...a: unknown[]) => lines.push(a.join(' '))
|
||||
console.error = (...a: unknown[]) => lines.push(a.join(' '))
|
||||
|
||||
try {
|
||||
await templatesMain([sub, ...parts.slice(1)])
|
||||
} finally {
|
||||
console.log = origLog
|
||||
console.error = origError
|
||||
}
|
||||
|
||||
onDone(lines.join('\n') || 'Done.', { display: 'system' })
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
### 4. commands.ts 注册
|
||||
|
||||
```typescript
|
||||
const jobCmd = feature('TEMPLATES')
|
||||
? require('./commands/job/index.js').default
|
||||
: null
|
||||
|
||||
// COMMANDS 数组:
|
||||
...(jobCmd ? [jobCmd] : []),
|
||||
```
|
||||
|
||||
## 验证清单
|
||||
|
||||
- [ ] `claude job list` 列出模板
|
||||
- [ ] `claude job new <template>` 创建任务
|
||||
- [ ] `claude job reply <id> <text>` 回复任务
|
||||
- [ ] `claude job status <id>` 显示任务状态
|
||||
- [ ] `claude job` (无参数) 等同于 `claude job list`
|
||||
- [ ] `claude new/list/reply` 输出 deprecation 警告 + 正常工作
|
||||
- [ ] REPL 中 `/job` 可用
|
||||
- [ ] REPL 中 `/job list` 显示模板列表
|
||||
- [ ] tsc --noEmit 零错误
|
||||
- [ ] bun test 通过
|
||||
123
docs/task/task-016-backward-compat-tests.md
Normal file
123
docs/task/task-016-backward-compat-tests.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Task 016: 向后兼容 + 测试
|
||||
|
||||
> 设计文档: [daemon-restructure-design.md](../features/daemon-restructure-design.md) § 五
|
||||
> 依赖: Task 014, Task 015
|
||||
> 分支: `feat/integrate-5-branches`
|
||||
|
||||
## 目标
|
||||
|
||||
确保旧命令向后兼容 (deprecation 警告 + 正常代理),并为重构后的命令结构编写测试。
|
||||
|
||||
## 文件清单
|
||||
|
||||
### 新增
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `src/daemon/__tests__/daemonMain.test.ts` | daemonMain 子命令路由测试 |
|
||||
| `src/cli/bg/__tests__/engine.test.ts` | BgEngine 选择逻辑测试 |
|
||||
| `src/cli/bg/__tests__/detached.test.ts` | DetachedEngine 启动/停止测试 |
|
||||
| `src/cli/bg/__tests__/tail.test.ts` | 日志 tail 功能测试 |
|
||||
|
||||
### 修改
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `src/entrypoints/cli.tsx` | 确认 deprecation 路径正确代理 |
|
||||
|
||||
## 实现方案
|
||||
|
||||
### 1. 向后兼容矩阵
|
||||
|
||||
| 旧命令 | 新命令 | 处理方式 |
|
||||
|--------|--------|---------|
|
||||
| `claude ps` | `claude daemon status` | stderr 输出 `[deprecated] Use: claude daemon status`,然后执行 |
|
||||
| `claude logs <x>` | `claude daemon logs <x>` | 同上 |
|
||||
| `claude attach <x>` | `claude daemon attach <x>` | 同上 |
|
||||
| `claude kill <x>` | `claude daemon kill <x>` | 同上 |
|
||||
| `claude --bg` | `claude daemon bg` | 保留为快捷方式,**不** deprecate (太常用) |
|
||||
| `claude new <t>` | `claude job new <t>` | stderr deprecation + 执行 |
|
||||
| `claude list` | `claude job list` | stderr deprecation + 执行 |
|
||||
| `claude reply <id>` | `claude job reply <id>` | stderr deprecation + 执行 |
|
||||
|
||||
**关键**: deprecation 输出到 stderr 而非 stdout,不影响脚本管道。
|
||||
|
||||
### 2. 测试计划
|
||||
|
||||
#### 2.1 daemonMain 路由测试
|
||||
|
||||
```typescript
|
||||
describe('daemonMain', () => {
|
||||
test('无参数默认 status', async () => { ... })
|
||||
test('start 调用 runSupervisor', async () => { ... })
|
||||
test('stop 调用 handleDaemonStop', async () => { ... })
|
||||
test('bg 委托给 bg.handleBgStart', async () => { ... })
|
||||
test('attach 委托给 bg.attachHandler', async () => { ... })
|
||||
test('logs 委托给 bg.logsHandler', async () => { ... })
|
||||
test('kill 委托给 bg.killHandler', async () => { ... })
|
||||
test('未知子命令设置 exitCode=1', async () => { ... })
|
||||
})
|
||||
```
|
||||
|
||||
#### 2.2 引擎选择测试
|
||||
|
||||
```typescript
|
||||
describe('selectEngine', () => {
|
||||
test('win32 返回 DetachedEngine', async () => { ... })
|
||||
test('darwin + tmux 可用返回 TmuxEngine', async () => { ... })
|
||||
test('darwin + tmux 不可用返回 DetachedEngine', async () => { ... })
|
||||
test('linux + tmux 可用返回 TmuxEngine', async () => { ... })
|
||||
})
|
||||
```
|
||||
|
||||
#### 2.3 DetachedEngine 测试
|
||||
|
||||
```typescript
|
||||
describe('DetachedEngine', () => {
|
||||
test('available 始终返回 true', async () => { ... })
|
||||
test('start 创建 detached 子进程并写入日志', async () => { ... })
|
||||
test('start 返回的 PID 文件存在', async () => { ... })
|
||||
})
|
||||
```
|
||||
|
||||
#### 2.4 Tail 测试
|
||||
|
||||
```typescript
|
||||
describe('tailLog', () => {
|
||||
test('输出已有日志内容', async () => { ... })
|
||||
test('追加内容时实时输出', async () => { ... })
|
||||
test('SIGINT 退出 tail', async () => { ... })
|
||||
})
|
||||
```
|
||||
|
||||
### 3. 集成验证脚本
|
||||
|
||||
可选: 在 `scripts/` 下添加一个手动验证脚本:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/verify-daemon-restructure.sh
|
||||
echo "=== 1. claude daemon status ==="
|
||||
bun run dev -- daemon status
|
||||
|
||||
echo "=== 2. claude daemon bg (should start) ==="
|
||||
bun run dev -- daemon bg --help
|
||||
|
||||
echo "=== 3. claude ps (deprecated) ==="
|
||||
bun run dev -- ps 2>&1 | head -1
|
||||
|
||||
echo "=== 4. claude job list ==="
|
||||
bun run dev -- job list
|
||||
|
||||
echo "=== 5. claude list (deprecated) ==="
|
||||
bun run dev -- list 2>&1 | head -1
|
||||
```
|
||||
|
||||
## 验证清单
|
||||
|
||||
- [ ] 旧命令全部正常工作 (仅多一行 stderr 警告)
|
||||
- [ ] `--bg` 保持无警告
|
||||
- [ ] 所有新增测试通过
|
||||
- [ ] 现有 2695 个测试无回归
|
||||
- [ ] tsc --noEmit 零错误
|
||||
- [ ] 手动在 Windows + macOS/Linux 上验证关键路径
|
||||
88
docs/test-plans/openclaw-autonomy-baseline.md
Normal file
88
docs/test-plans/openclaw-autonomy-baseline.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# OpenClaw Autonomy Baseline Test Spec
|
||||
|
||||
## Purpose
|
||||
|
||||
This test spec locks the current behavior of the existing trigger and context layers before any formal autonomy-subsystem implementation begins.
|
||||
|
||||
At this stage, production code is read-only. Only test files, fixtures, and planning documents may change.
|
||||
|
||||
## Goal
|
||||
|
||||
Establish a stable baseline around the parts of `Claude-code-bast` that later autonomy work is most likely to touch:
|
||||
|
||||
- proactive state handling
|
||||
- cron task storage semantics
|
||||
- cron scheduler helper semantics
|
||||
- user-context cache and `CLAUDE.md` injection behavior
|
||||
|
||||
## Out of Scope for This Baseline Round
|
||||
|
||||
- New authority behavior (`AGENTS.md` / `HEARTBEAT.md`)
|
||||
- New detached-run ledger behavior
|
||||
- New flow behavior
|
||||
- UI redesign
|
||||
|
||||
## Files Under Baseline Protection
|
||||
|
||||
- `src/proactive/index.ts`
|
||||
- `src/utils/cronTasks.ts`
|
||||
- `src/utils/cronScheduler.ts`
|
||||
- `src/context.ts`
|
||||
|
||||
## Test Files Added In This Round
|
||||
|
||||
- `src/proactive/__tests__/state.baseline.test.ts`
|
||||
- `src/commands/__tests__/proactive.baseline.test.ts`
|
||||
- `src/utils/__tests__/cronTasks.baseline.test.ts`
|
||||
- `src/utils/__tests__/cronScheduler.baseline.test.ts`
|
||||
- `src/__tests__/context.baseline.test.ts`
|
||||
|
||||
## Baseline Assertions
|
||||
|
||||
### Proactive state
|
||||
|
||||
1. Activating proactive mode sets active state and activation source.
|
||||
2. Pausing proactive mode suppresses `shouldTick()` and clears `nextTickAt`.
|
||||
3. Blocking context suppresses `shouldTick()` and clears `nextTickAt`.
|
||||
4. Subscribers are notified on state transitions.
|
||||
5. The `/proactive` command enables proactive mode and emits the expected hidden reminder.
|
||||
6. The `/proactive` command disables proactive mode on the second invocation.
|
||||
|
||||
### Cron task storage
|
||||
|
||||
1. Session-only cron tasks remain in memory only.
|
||||
2. Durable cron tasks are persisted to `.claude/scheduled_tasks.json`.
|
||||
3. Daemon-style `dir`-scoped reads exclude session-only cron tasks.
|
||||
4. `removeCronTasks()` without `dir` can remove session-only tasks.
|
||||
5. `removeCronTasks()` with `dir` does not mutate session-only task storage.
|
||||
|
||||
### Cron scheduler helpers
|
||||
|
||||
1. `isRecurringTaskAged()` preserves current aging semantics.
|
||||
2. `buildMissedTaskNotification()` preserves the current AskUserQuestion safety wording.
|
||||
3. `buildMissedTaskNotification()` preserves code-fence hardening for prompt bodies that contain backticks.
|
||||
|
||||
### User context caching
|
||||
|
||||
1. `getUserContext()` includes `currentDate`.
|
||||
2. `getUserContext()` includes mocked `claudeMd` content when memory loading is enabled.
|
||||
3. `CLAUDE_CODE_DISABLE_CLAUDE_MDS` suppresses `claudeMd`.
|
||||
4. `setSystemPromptInjection()` clears the memoized user-context cache.
|
||||
5. `getSystemContext()` reflects the injection after cache invalidation.
|
||||
|
||||
## Remaining Baseline Gaps
|
||||
|
||||
The following areas are intentionally deferred because they require higher-cost harnessing and should still avoid production-code changes:
|
||||
|
||||
1. `useScheduledTasks.ts` hook-level runtime behavior
|
||||
2. `src/cli/print.ts` full headless scheduler loop behavior
|
||||
3. `useProactive.ts` hook timer behavior
|
||||
4. end-to-end queue interaction between proactive ticks and `SleepTool`
|
||||
|
||||
## Acceptance
|
||||
|
||||
This baseline round is complete when:
|
||||
|
||||
1. The four new test files pass.
|
||||
2. No production source files are modified.
|
||||
3. The tests are stable enough to serve as a pre-implementation guardrail.
|
||||
@@ -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` |
|
||||
|
||||
适配器是无状态的,同一会话内缓存复用。
|
||||
|
||||
|
||||
17
mint.json
17
mint.json
@@ -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"
|
||||
}
|
||||
|
||||
42
package.json
42
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-code-best",
|
||||
"version": "1.2.1",
|
||||
"version": "1.4.1",
|
||||
"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>",
|
||||
@@ -25,20 +25,25 @@
|
||||
"bun": ">=1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"ccb": "dist/cli.js",
|
||||
"claude-code-best": "dist/cli.js"
|
||||
"ccb": "dist/cli-node.js",
|
||||
"ccb-bun": "dist/cli-bun.js",
|
||||
"claude-code-best": "dist/cli-node.js"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"packages/@ant/*"
|
||||
"packages/@ant/*",
|
||||
"packages/@anthropic-ai/*"
|
||||
],
|
||||
"files": [
|
||||
"dist",
|
||||
"scripts/download-ripgrep.ts",
|
||||
"scripts/postinstall.cjs"
|
||||
"scripts/postinstall.cjs",
|
||||
"scripts/setup-chrome-mcp.mjs"
|
||||
],
|
||||
"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",
|
||||
@@ -49,19 +54,19 @@
|
||||
"test": "bun test",
|
||||
"check:unused": "knip-bun",
|
||||
"health": "bun run scripts/health-check.ts",
|
||||
"postinstall": "node scripts/postinstall.cjs && node scripts/setup-chrome-mcp.mjs",
|
||||
"postinstall": "node scripts/run-parallel.mjs scripts/postinstall.cjs 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.8",
|
||||
"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",
|
||||
"@ant/model-provider": "workspace:*",
|
||||
"@ant/claude-for-chrome-mcp": "workspace:*",
|
||||
"@ant/computer-use-input": "workspace:*",
|
||||
"@ant/computer-use-mcp": "workspace:*",
|
||||
@@ -81,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",
|
||||
@@ -105,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",
|
||||
@@ -162,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",
|
||||
@@ -176,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"
|
||||
|
||||
5
packages/@ant/claude-for-chrome-mcp/tsconfig.json
Normal file
5
packages/@ant/claude-for-chrome-mcp/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -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! }
|
||||
|
||||
5
packages/@ant/computer-use-input/tsconfig.json
Normal file
5
packages/@ant/computer-use-input/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -37,16 +37,21 @@
|
||||
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
/** Detect actual image MIME type from base64 data using magic bytes. */
|
||||
/** Detect actual image MIME type from base64 data by decoding the magic bytes. */
|
||||
function detectMimeFromBase64(b64: string): string {
|
||||
// First byte is enough to distinguish PNG (0x89) from JPEG (0xFF)
|
||||
const c = b64.charCodeAt(0);
|
||||
if (c === 0x89) return "image/png";
|
||||
if (c === 0xFF) return "image/jpeg";
|
||||
// RIFF = WebP
|
||||
if (c === 0x52) return "image/webp";
|
||||
// GIF
|
||||
if (c === 0x47) return "image/gif";
|
||||
// Decode first 12 raw bytes (16 base64 chars is enough) and check standard magic bytes.
|
||||
// PNG: 89 50 4E 47
|
||||
// JPEG: FF D8 FF
|
||||
// RIFF+WEBP: "RIFF" at 0..3 + "WEBP" at 8..11
|
||||
// GIF: "GIF" at 0..2
|
||||
const raw = Buffer.from(b64.slice(0, 16), "base64");
|
||||
if (raw[0] === 0x89 && raw[1] === 0x50 && raw[2] === 0x4e && raw[3] === 0x47) return "image/png";
|
||||
if (raw[0] === 0xff && raw[1] === 0xd8 && raw[2] === 0xff) return "image/jpeg";
|
||||
if (
|
||||
raw[0] === 0x52 && raw[1] === 0x49 && raw[2] === 0x46 && raw[3] === 0x46 && // RIFF
|
||||
raw[8] === 0x57 && raw[9] === 0x45 && raw[10] === 0x42 && raw[11] === 0x50 // WEBP
|
||||
) return "image/webp";
|
||||
if (raw[0] === 0x47 && raw[1] === 0x49 && raw[2] === 0x46) return "image/gif";
|
||||
return "image/png";
|
||||
}
|
||||
|
||||
|
||||
5
packages/@ant/computer-use-mcp/tsconfig.json
Normal file
5
packages/@ant/computer-use-mcp/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
5
packages/@ant/computer-use-swift/tsconfig.json
Normal file
5
packages/@ant/computer-use-swift/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
176
packages/@ant/ink/docs/01-getting-started.md
Normal file
176
packages/@ant/ink/docs/01-getting-started.md
Normal 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.
|
||||
348
packages/@ant/ink/docs/02-layout.md
Normal file
348
packages/@ant/ink/docs/02-layout.md
Normal 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" />
|
||||
```
|
||||
238
packages/@ant/ink/docs/03-text-and-styling.md
Normal file
238
packages/@ant/ink/docs/03-text-and-styling.md
Normal 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 `─|++++`
|
||||
213
packages/@ant/ink/docs/04-theme-system.md
Normal file
213
packages/@ant/ink/docs/04-theme-system.md
Normal 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
|
||||
390
packages/@ant/ink/docs/05-design-system.md
Normal file
390
packages/@ant/ink/docs/05-design-system.md
Normal 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 |
|
||||
189
packages/@ant/ink/docs/06-scrolling.md
Normal file
189
packages/@ant/ink/docs/06-scrolling.md
Normal 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`.
|
||||
267
packages/@ant/ink/docs/07-user-input.md
Normal file
267
packages/@ant/ink/docs/07-user-input.md
Normal 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.
|
||||
302
packages/@ant/ink/docs/08-keybindings.md
Normal file
302
packages/@ant/ink/docs/08-keybindings.md
Normal 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'
|
||||
```
|
||||
407
packages/@ant/ink/docs/09-hooks-reference.md
Normal file
407
packages/@ant/ink/docs/09-hooks-reference.md
Normal 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.
|
||||
232
packages/@ant/ink/docs/10-events-and-focus.md
Normal file
232
packages/@ant/ink/docs/10-events-and-focus.md
Normal 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.
|
||||
301
packages/@ant/ink/docs/11-core-architecture.md
Normal file
301
packages/@ant/ink/docs/11-core-architecture.md
Normal 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`)
|
||||
}
|
||||
})
|
||||
```
|
||||
381
packages/@ant/ink/docs/12-terminal-integration.md
Normal file
381
packages/@ant/ink/docs/12-terminal-integration.md
Normal 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
|
||||
46
packages/@ant/ink/docs/README.md
Normal file
46
packages/@ant/ink/docs/README.md
Normal 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 |
|
||||
5
packages/@ant/ink/tsconfig.json
Normal file
5
packages/@ant/ink/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
18
packages/@ant/model-provider/package.json
Normal file
18
packages/@ant/model-provider/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@ant/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"
|
||||
}
|
||||
}
|
||||
27
packages/@ant/model-provider/src/client/index.ts
Normal file
27
packages/@ant/model-provider/src/client/index.ts
Normal 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 }
|
||||
35
packages/@ant/model-provider/src/client/types.ts
Normal file
35
packages/@ant/model-provider/src/client/types.ts
Normal 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
|
||||
}
|
||||
238
packages/@ant/model-provider/src/errorUtils.ts
Normal file
238
packages/@ant/model-provider/src/errorUtils.ts
Normal 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
|
||||
}
|
||||
27
packages/@ant/model-provider/src/hooks/index.ts
Normal file
27
packages/@ant/model-provider/src/hooks/index.ts
Normal 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 }
|
||||
48
packages/@ant/model-provider/src/hooks/types.ts
Normal file
48
packages/@ant/model-provider/src/hooks/types.ts
Normal 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
|
||||
}
|
||||
63
packages/@ant/model-provider/src/index.ts
Normal file
63
packages/@ant/model-provider/src/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
// @ant/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'
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test'
|
||||
import type {
|
||||
AssistantMessage,
|
||||
UserMessage,
|
||||
} from '../../../../types/message.js'
|
||||
} from '../../../types/message.js'
|
||||
import { anthropicMessagesToGemini } from '../convertMessages.js'
|
||||
|
||||
function makeUserMsg(content: string | any[]): UserMessage {
|
||||
@@ -2,9 +2,8 @@ import type {
|
||||
BetaToolResultBlockParam,
|
||||
BetaToolUseBlock,
|
||||
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||
import type { AssistantMessage, UserMessage } from '../../../types/message.js'
|
||||
import { safeParseJSON } from '../../../utils/json.js'
|
||||
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
|
||||
import type { AssistantMessage, UserMessage } from '../../types/message.js'
|
||||
import type { SystemPrompt } from '../../types/systemPrompt.js'
|
||||
import {
|
||||
GEMINI_THOUGHT_SIGNATURE_FIELD,
|
||||
type GeminiContent,
|
||||
@@ -12,6 +11,16 @@ import {
|
||||
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,
|
||||
@@ -113,7 +122,7 @@ function convertUserContentBlockToGeminiParts(
|
||||
]
|
||||
}
|
||||
|
||||
// 将 Anthropic image 块转换为 Gemini inlineData
|
||||
// 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') {
|
||||
@@ -127,7 +136,7 @@ function convertUserContentBlockToGeminiParts(
|
||||
},
|
||||
]
|
||||
}
|
||||
// url 类型的图片,Gemini 不直接支持,转为文本描述
|
||||
// URL images not directly supported by Gemini, convert to text description
|
||||
if (source?.type === 'url' && typeof source.url === 'string') {
|
||||
return createTextGeminiParts(`[image: ${source.url}]`)
|
||||
}
|
||||
@@ -17,14 +17,12 @@ export function resolveGeminiModel(anthropicModel: string): string {
|
||||
return cleanModel
|
||||
}
|
||||
|
||||
// First, try Gemini-specific DEFAULT variables (separated from Anthropic)
|
||||
const geminiEnvVar = `GEMINI_DEFAULT_${family.toUpperCase()}_MODEL`
|
||||
const geminiModel = process.env[geminiEnvVar]
|
||||
if (geminiModel) {
|
||||
return geminiModel
|
||||
}
|
||||
|
||||
// Fallback to Anthropic DEFAULT variables for backward compatibility
|
||||
const sharedEnvVar = `ANTHROPIC_DEFAULT_${family.toUpperCase()}_MODEL`
|
||||
const resolvedModel = process.env[sharedEnvVar]
|
||||
if (resolvedModel) {
|
||||
@@ -2,8 +2,7 @@
|
||||
* 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):
|
||||
* GROK_MODEL_MAP='{"opus":"grok-4","sonnet":"grok-3","haiku":"grok-3-mini-fast"}'
|
||||
* 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',
|
||||
@@ -19,9 +18,6 @@ const DEFAULT_MODEL_MAP: Record<string, string> = {
|
||||
'claude-3-5-sonnet-20241022': 'grok-3-mini-fast',
|
||||
}
|
||||
|
||||
/**
|
||||
* Family-level mapping defaults (used by GROK_MODEL_MAP).
|
||||
*/
|
||||
const DEFAULT_FAMILY_MAP: Record<string, string> = {
|
||||
opus: 'grok-4.20-reasoning',
|
||||
sonnet: 'grok-3-mini-fast',
|
||||
@@ -35,10 +31,6 @@ function getModelFamily(model: string): 'haiku' | 'sonnet' | 'opus' | null {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse user-provided model map from GROK_MODEL_MAP env var.
|
||||
* Accepts JSON like: {"opus":"grok-4","sonnet":"grok-3","haiku":"grok-3-mini-fast"}
|
||||
*/
|
||||
function getUserModelMap(): Record<string, string> | null {
|
||||
const raw = process.env.GROK_MODEL_MAP
|
||||
if (!raw) return null
|
||||
@@ -55,18 +47,8 @@ function getUserModelMap(): Record<string, string> | null {
|
||||
|
||||
/**
|
||||
* Resolve the Grok model name for a given Anthropic model.
|
||||
*
|
||||
* Priority:
|
||||
* 1. GROK_MODEL env var (override all)
|
||||
* 2. GROK_MODEL_MAP env var — JSON family map (e.g. {"opus":"grok-4"})
|
||||
* 3. GROK_DEFAULT_{FAMILY}_MODEL env var (e.g. GROK_DEFAULT_OPUS_MODEL)
|
||||
* 4. ANTHROPIC_DEFAULT_{FAMILY}_MODEL env var (backward compat)
|
||||
* 5. DEFAULT_MODEL_MAP lookup
|
||||
* 6. Family-level default
|
||||
* 7. Pass through original model name
|
||||
*/
|
||||
export function resolveGrokModel(anthropicModel: string): string {
|
||||
// 1. Global override
|
||||
if (process.env.GROK_MODEL) {
|
||||
return process.env.GROK_MODEL
|
||||
}
|
||||
@@ -74,34 +56,28 @@ export function resolveGrokModel(anthropicModel: string): string {
|
||||
const cleanModel = anthropicModel.replace(/\[1m\]$/, '')
|
||||
const family = getModelFamily(cleanModel)
|
||||
|
||||
// 2. User-provided model map
|
||||
const userMap = getUserModelMap()
|
||||
if (userMap && family && userMap[family]) {
|
||||
return userMap[family]
|
||||
}
|
||||
|
||||
if (family) {
|
||||
// 3. Grok-specific family override
|
||||
const grokEnvVar = `GROK_DEFAULT_${family.toUpperCase()}_MODEL`
|
||||
const grokOverride = process.env[grokEnvVar]
|
||||
if (grokOverride) return grokOverride
|
||||
|
||||
// 4. Anthropic env var (backward compat)
|
||||
const anthropicEnvVar = `ANTHROPIC_DEFAULT_${family.toUpperCase()}_MODEL`
|
||||
const anthropicOverride = process.env[anthropicEnvVar]
|
||||
if (anthropicOverride) return anthropicOverride
|
||||
}
|
||||
|
||||
// 5. Exact model name lookup
|
||||
if (DEFAULT_MODEL_MAP[cleanModel]) {
|
||||
return DEFAULT_MODEL_MAP[cleanModel]
|
||||
}
|
||||
|
||||
// 6. Family-level default
|
||||
if (family && DEFAULT_FAMILY_MAP[family]) {
|
||||
return DEFAULT_FAMILY_MAP[family]
|
||||
}
|
||||
|
||||
// 7. Pass through
|
||||
return cleanModel
|
||||
}
|
||||
@@ -16,9 +16,6 @@ const DEFAULT_MODEL_MAP: Record<string, string> = {
|
||||
'claude-3-5-sonnet-20241022': 'gpt-4o',
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the model family (haiku / sonnet / opus) from an Anthropic model ID.
|
||||
*/
|
||||
function getModelFamily(model: string): 'haiku' | 'sonnet' | 'opus' | null {
|
||||
if (/haiku/i.test(model)) return 'haiku'
|
||||
if (/opus/i.test(model)) return 'opus'
|
||||
@@ -37,23 +34,18 @@ function getModelFamily(model: string): 'haiku' | 'sonnet' | 'opus' | null {
|
||||
* 5. Pass through original model name
|
||||
*/
|
||||
export function resolveOpenAIModel(anthropicModel: string): string {
|
||||
// Highest priority: explicit override
|
||||
if (process.env.OPENAI_MODEL) {
|
||||
return process.env.OPENAI_MODEL
|
||||
}
|
||||
|
||||
// Strip [1m] suffix if present (Claude-specific modifier)
|
||||
const cleanModel = anthropicModel.replace(/\[1m\]$/, '')
|
||||
|
||||
// Check family-specific overrides
|
||||
const family = getModelFamily(cleanModel)
|
||||
if (family) {
|
||||
// OpenAI-specific family override (preferred for openai provider)
|
||||
const openaiEnvVar = `OPENAI_DEFAULT_${family.toUpperCase()}_MODEL`
|
||||
const openaiOverride = process.env[openaiEnvVar]
|
||||
if (openaiOverride) return openaiOverride
|
||||
|
||||
// Anthropic env var (backward compatibility)
|
||||
const anthropicEnvVar = `ANTHROPIC_DEFAULT_${family.toUpperCase()}_MODEL`
|
||||
const anthropicOverride = process.env[anthropicEnvVar]
|
||||
if (anthropicOverride) return anthropicOverride
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { anthropicMessagesToOpenAI } from '../convertMessages.js'
|
||||
import type { UserMessage, AssistantMessage } from '../../../../types/message.js'
|
||||
import { anthropicMessagesToOpenAI } from '../openaiConvertMessages.js'
|
||||
import type { UserMessage, AssistantMessage } from '../../types/message.js'
|
||||
|
||||
// Helpers to create internal-format messages
|
||||
function makeUserMsg(content: string | any[]): UserMessage {
|
||||
@@ -396,10 +396,6 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
||||
{ enableThinking: true },
|
||||
)
|
||||
|
||||
// All 3 assistant messages are in the current turn (after last user msg is the last tool_result,
|
||||
// but the "last user message" boundary logic finds the last user-typed message).
|
||||
// Actually, tool_result messages are also UserMessage type, so the last user message
|
||||
// is the one with tool_result for toolu_002. All assistant messages after that should have reasoning.
|
||||
const assistants = result.filter(m => m.role === 'assistant')
|
||||
expect(assistants.length).toBe(3)
|
||||
// All iterations within the same turn preserve reasoning
|
||||
@@ -435,6 +431,54 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
||||
expect(assistant.reasoning_content).toBeUndefined()
|
||||
})
|
||||
|
||||
// ── fix: reorder tool and user messages for OpenAI API compatibility (#168) ──
|
||||
|
||||
test('tool messages come BEFORE user text when mixed in same turn', () => {
|
||||
// OpenAI requires: assistant(tool_calls) → tool → user
|
||||
// Bug: previously user text was emitted before tool messages
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[
|
||||
makeUserMsg('run ls'),
|
||||
makeAssistantMsg([
|
||||
{ type: 'tool_use' as const, id: 'toolu_1', name: 'bash', input: { command: 'ls' } },
|
||||
]),
|
||||
makeUserMsg([
|
||||
{ type: 'tool_result' as const, tool_use_id: 'toolu_1', content: 'file.txt' },
|
||||
{ type: 'text' as const, text: 'looks good' },
|
||||
]),
|
||||
],
|
||||
[] as any,
|
||||
)
|
||||
// Find the tool message and the user text message
|
||||
const toolIdx = result.findIndex(m => m.role === 'tool')
|
||||
const userTextIdx = result.findIndex(
|
||||
m => m.role === 'user' && typeof m.content === 'string' && m.content.includes('looks good'),
|
||||
)
|
||||
expect(toolIdx).toBeGreaterThanOrEqual(0)
|
||||
expect(userTextIdx).toBeGreaterThanOrEqual(0)
|
||||
// Tool MUST come before user text
|
||||
expect(toolIdx).toBeLessThan(userTextIdx)
|
||||
})
|
||||
|
||||
test('tool message immediately follows assistant tool_calls (no user message in between)', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[
|
||||
makeUserMsg('do something'),
|
||||
makeAssistantMsg([
|
||||
{ type: 'tool_use' as const, id: 'toolu_2', name: 'bash', input: { command: 'pwd' } },
|
||||
]),
|
||||
makeUserMsg([
|
||||
{ type: 'tool_result' as const, tool_use_id: 'toolu_2', content: '/home/user' },
|
||||
]),
|
||||
],
|
||||
[] as any,
|
||||
)
|
||||
const assistantIdx = result.findIndex(m => m.role === 'assistant' && (m as any).tool_calls)
|
||||
const toolIdx = result.findIndex(m => m.role === 'tool')
|
||||
expect(assistantIdx).toBeGreaterThanOrEqual(0)
|
||||
expect(toolIdx).toBe(assistantIdx + 1)
|
||||
})
|
||||
|
||||
test('sets content to null when only thinking and tool_calls present', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeUserMsg('question'), makeAssistantMsg([
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from '../convertTools.js'
|
||||
import { anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from '../openaiConvertTools.js'
|
||||
|
||||
describe('anthropicToolsToOpenAI', () => {
|
||||
test('converts basic tool', () => {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { adaptOpenAIStreamToAnthropic } from '../streamAdapter.js'
|
||||
import type { ChatCompletionChunk } from 'openai/resources/chat/completions/completions.mjs'
|
||||
import { adaptOpenAIStreamToAnthropic } from '../openaiStreamAdapter.js'
|
||||
|
||||
/** Helper to create a mock async iterable from chunk array */
|
||||
function mockStream(chunks: ChatCompletionChunk[]): AsyncIterable<ChatCompletionChunk> {
|
||||
@@ -10,8 +10,8 @@ import type {
|
||||
ChatCompletionToolMessageParam,
|
||||
ChatCompletionUserMessageParam,
|
||||
} from 'openai/resources/chat/completions/completions.mjs'
|
||||
import type { AssistantMessage, UserMessage } from '../../../types/message.js'
|
||||
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
|
||||
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
|
||||
@@ -152,7 +152,6 @@ function convertInternalUserMessage(
|
||||
// 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".
|
||||
// See: https://github.com/anthropics/claude-code/issues/xxx
|
||||
for (const tr of toolResults) {
|
||||
result.push(convertToolResult(tr))
|
||||
}
|
||||
@@ -51,10 +51,6 @@ export async function* adaptOpenAIStreamToAnthropic(
|
||||
let textBlockOpen = false
|
||||
|
||||
// Track usage — all four Anthropic fields, populated from OpenAI usage fields:
|
||||
// prompt_tokens → input_tokens
|
||||
// completion_tokens → output_tokens
|
||||
// prompt_tokens_details.cached_tokens → cache_read_input_tokens
|
||||
// (no standard OpenAI equivalent) → cache_creation_input_tokens (always 0)
|
||||
let inputTokens = 0
|
||||
let outputTokens = 0
|
||||
let cachedReadTokens = 0
|
||||
@@ -62,10 +58,7 @@ export async function* adaptOpenAIStreamToAnthropic(
|
||||
// Track all open content block indices (for cleanup)
|
||||
const openBlockIndices = new Set<number>()
|
||||
|
||||
// Deferred finish state: populated when finish_reason is encountered so that
|
||||
// message_delta / message_stop are emitted AFTER the stream loop ends.
|
||||
// This ensures usage chunks that arrive after the finish_reason chunk are
|
||||
// captured before we emit the final token counts.
|
||||
// Deferred finish state
|
||||
let pendingFinishReason: string | null = null
|
||||
let pendingHasToolCalls = false
|
||||
|
||||
@@ -74,16 +67,9 @@ export async function* adaptOpenAIStreamToAnthropic(
|
||||
const delta = choice?.delta
|
||||
|
||||
// Extract usage from any chunk that carries it.
|
||||
// Many OpenAI-compatible endpoints (e.g. DeepSeek) send usage in a separate
|
||||
// final chunk that arrives AFTER the finish_reason chunk. Reading it here
|
||||
// (before emitting message_delta) ensures the token counts are available
|
||||
// when we later emit message_delta.
|
||||
if (chunk.usage) {
|
||||
inputTokens = chunk.usage.prompt_tokens ?? inputTokens
|
||||
outputTokens = chunk.usage.completion_tokens ?? outputTokens
|
||||
// OpenAI prompt caching: prompt_tokens_details.cached_tokens
|
||||
// → Anthropic cache_read_input_tokens
|
||||
// Note: OpenAI has no equivalent for cache_creation_input_tokens.
|
||||
const details = (chunk.usage as any).prompt_tokens_details
|
||||
if (details?.cached_tokens != null) {
|
||||
cachedReadTokens = details.cached_tokens
|
||||
@@ -118,7 +104,6 @@ export async function* adaptOpenAIStreamToAnthropic(
|
||||
if (!delta) continue
|
||||
|
||||
// Handle reasoning_content → Anthropic thinking block
|
||||
// DeepSeek and compatible providers send delta.reasoning_content
|
||||
const reasoningContent = (delta as any).reasoning_content
|
||||
if (reasoningContent != null && reasoningContent !== '') {
|
||||
if (!thinkingBlockOpen) {
|
||||
@@ -150,7 +135,7 @@ export async function* adaptOpenAIStreamToAnthropic(
|
||||
// Handle text content
|
||||
if (delta.content != null && delta.content !== '') {
|
||||
if (!textBlockOpen) {
|
||||
// Close thinking block if still open (reasoning done, now generating answer)
|
||||
// Close thinking block if still open
|
||||
if (thinkingBlockOpen) {
|
||||
yield {
|
||||
type: 'content_block_stop',
|
||||
@@ -251,12 +236,8 @@ export async function* adaptOpenAIStreamToAnthropic(
|
||||
}
|
||||
}
|
||||
|
||||
// Handle finish: close all open content blocks and record the finish_reason.
|
||||
// message_delta + message_stop are emitted AFTER the stream loop so that any
|
||||
// trailing usage chunk (sent after the finish chunk by some endpoints)
|
||||
// is captured first — ensuring token counts are non-zero.
|
||||
// Handle finish
|
||||
if (choice?.finish_reason) {
|
||||
// Close thinking block if still open
|
||||
if (thinkingBlockOpen) {
|
||||
yield {
|
||||
type: 'content_block_stop',
|
||||
@@ -266,7 +247,6 @@ export async function* adaptOpenAIStreamToAnthropic(
|
||||
thinkingBlockOpen = false
|
||||
}
|
||||
|
||||
// Close text block if still open
|
||||
if (textBlockOpen) {
|
||||
yield {
|
||||
type: 'content_block_stop',
|
||||
@@ -276,7 +256,6 @@ export async function* adaptOpenAIStreamToAnthropic(
|
||||
textBlockOpen = false
|
||||
}
|
||||
|
||||
// Close all tool blocks that haven't been closed yet
|
||||
for (const [, block] of toolBlocks) {
|
||||
if (openBlockIndices.has(block.contentIndex)) {
|
||||
yield {
|
||||
@@ -287,14 +266,12 @@ export async function* adaptOpenAIStreamToAnthropic(
|
||||
}
|
||||
}
|
||||
|
||||
// Defer message_delta / message_stop until after the loop so that any
|
||||
// trailing usage chunk is processed before we emit the final token counts.
|
||||
pendingFinishReason = choice.finish_reason
|
||||
pendingHasToolCalls = toolBlocks.size > 0
|
||||
}
|
||||
}
|
||||
|
||||
// Safety: close any remaining open blocks if stream ended without finish_reason
|
||||
// Safety: close any remaining open blocks
|
||||
for (const idx of openBlockIndices) {
|
||||
yield {
|
||||
type: 'content_block_stop',
|
||||
@@ -302,15 +279,8 @@ export async function* adaptOpenAIStreamToAnthropic(
|
||||
} as BetaRawMessageStreamEvent
|
||||
}
|
||||
|
||||
// Emit message_delta + message_stop now that the stream is fully consumed.
|
||||
// Usage values (inputTokens / outputTokens) reflect all chunks including any
|
||||
// trailing usage-only chunk sent after the finish_reason chunk.
|
||||
// Emit message_delta + message_stop
|
||||
if (pendingFinishReason !== null) {
|
||||
// Map finish_reason to Anthropic stop_reason.
|
||||
// CRITICAL: When finish_reason is 'length' (token budget exhausted), always
|
||||
// report 'max_tokens' regardless of whether partial tool calls were received.
|
||||
// Otherwise the query loop would try to execute tool calls with incomplete
|
||||
// JSON arguments instead of triggering the max_tokens retry/recovery path.
|
||||
const stopReason =
|
||||
pendingFinishReason === 'length'
|
||||
? 'max_tokens'
|
||||
@@ -324,19 +294,6 @@ export async function* adaptOpenAIStreamToAnthropic(
|
||||
stop_reason: stopReason,
|
||||
stop_sequence: null,
|
||||
},
|
||||
// Carry all four Anthropic usage fields so queryModelOpenAI's message_delta
|
||||
// handler (which spreads this into the accumulated usage object) can override
|
||||
// every field that message_start emitted as 0. For endpoints that send usage
|
||||
// in a trailing chunk (e.g. DeepSeek), message_start is emitted on the first
|
||||
// content chunk before the trailing usage chunk arrives, so all four fields
|
||||
// start at 0. By the time we reach here (post-loop) the trailing chunk has
|
||||
// been processed and all values reflect the real counts.
|
||||
//
|
||||
// OpenAI → Anthropic field mapping:
|
||||
// prompt_tokens → input_tokens
|
||||
// completion_tokens → output_tokens
|
||||
// prompt_tokens_details.cached_tokens → cache_read_input_tokens
|
||||
// (no OpenAI equivalent) → cache_creation_input_tokens (stays 0)
|
||||
usage: {
|
||||
input_tokens: inputTokens,
|
||||
output_tokens: outputTokens,
|
||||
@@ -353,11 +310,6 @@ export async function* adaptOpenAIStreamToAnthropic(
|
||||
|
||||
/**
|
||||
* Map OpenAI finish_reason to Anthropic stop_reason.
|
||||
*
|
||||
* stop → end_turn
|
||||
* tool_calls → tool_use
|
||||
* length → max_tokens
|
||||
* content_filter → end_turn
|
||||
*/
|
||||
function mapFinishReason(reason: string): string {
|
||||
switch (reason) {
|
||||
54
packages/@ant/model-provider/src/types/errors.ts
Normal file
54
packages/@ant/model-provider/src/types/errors.ts
Normal 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'
|
||||
6
packages/@ant/model-provider/src/types/index.ts
Normal file
6
packages/@ant/model-provider/src/types/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// Type definitions for @ant/model-provider
|
||||
|
||||
export * from './message.js'
|
||||
export * from './usage.js'
|
||||
export * from './errors.js'
|
||||
export * from './systemPrompt.js'
|
||||
129
packages/@ant/model-provider/src/types/message.ts
Normal file
129
packages/@ant/model-provider/src/types/message.ts
Normal 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' }
|
||||
10
packages/@ant/model-provider/src/types/systemPrompt.ts
Normal file
10
packages/@ant/model-provider/src/types/systemPrompt.ts
Normal 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
|
||||
}
|
||||
49
packages/@ant/model-provider/src/types/usage.ts
Normal file
49
packages/@ant/model-provider/src/types/usage.ts
Normal 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',
|
||||
}
|
||||
7
packages/@ant/model-provider/tsconfig.json
Normal file
7
packages/@ant/model-provider/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||
}
|
||||
11
packages/agent-tools/package.json
Normal file
11
packages/agent-tools/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
34
packages/agent-tools/src/__tests__/compat.test.ts
Normal file
34
packages/agent-tools/src/__tests__/compat.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
63
packages/agent-tools/src/__tests__/registry.test.ts
Normal file
63
packages/agent-tools/src/__tests__/registry.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
18
packages/agent-tools/src/index.ts
Normal file
18
packages/agent-tools/src/index.ts
Normal 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'
|
||||
21
packages/agent-tools/src/registry.ts
Normal file
21
packages/agent-tools/src/registry.ts
Normal 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))
|
||||
}
|
||||
221
packages/agent-tools/src/types.ts
Normal file
221
packages/agent-tools/src/types.ts
Normal 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[]
|
||||
5
packages/agent-tools/tsconfig.json
Normal file
5
packages/agent-tools/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
5
packages/audio-capture-napi/tsconfig.json
Normal file
5
packages/audio-capture-napi/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
16
packages/builtin-tools/package.json
Normal file
16
packages/builtin-tools/package.json
Normal 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:*"
|
||||
}
|
||||
}
|
||||
70
packages/builtin-tools/src/index.ts
Normal file
70
packages/builtin-tools/src/index.ts
Normal 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'
|
||||
@@ -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
|
||||
@@ -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,
|
||||
@@ -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 {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user