mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 06:15:51 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cb1e50b25 | ||
|
|
cfab161e28 | ||
|
|
90027279e6 | ||
|
|
3470783ced | ||
|
|
8169b96250 | ||
|
|
fe08cacf8d | ||
|
|
5a4c820e1d | ||
|
|
1a4e9702c2 | ||
|
|
2273a0bcfe | ||
|
|
b80483c23e | ||
|
|
8442aaadd2 | ||
|
|
dad3ad2b8d | ||
|
|
b5b81dfe49 | ||
|
|
ecbd5a93e4 | ||
|
|
be80da4ce0 | ||
|
|
fce40fed1f | ||
|
|
a7e03a5b30 | ||
|
|
05cabbbd73 | ||
|
|
d4b30d32c3 | ||
|
|
e0484e2817 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -27,4 +27,4 @@ jobs:
|
|||||||
run: bun test
|
run: bun test
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: bun run build
|
run: bun run build:vite
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ bun run health
|
|||||||
# Check unused exports
|
# Check unused exports
|
||||||
bun run check:unused
|
bun run check:unused
|
||||||
|
|
||||||
|
bun run typecheck
|
||||||
|
|
||||||
# Remote Control Server
|
# Remote Control Server
|
||||||
bun run rcs
|
bun run rcs
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,16 @@
|
|||||||
| 特性 | 说明 | 文档 |
|
| 特性 | 说明 | 文档 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| **Claude 群控技术** | Pipe IPC 多实例协作:同机 main/sub 自动编排 + LAN 跨机器零配置发现与通讯,`/pipes` 选择面板 + `Shift+↓` 交互 + 消息广播路由 | [Pipe IPC](https://ccb.agent-aura.top/docs/features/pipes-and-lan) / [LAN](https://ccb.agent-aura.top/docs/features/lan-pipes) |
|
| **Claude 群控技术** | Pipe IPC 多实例协作:同机 main/sub 自动编排 + LAN 跨机器零配置发现与通讯,`/pipes` 选择面板 + `Shift+↓` 交互 + 消息广播路由 | [Pipe IPC](https://ccb.agent-aura.top/docs/features/pipes-and-lan) / [LAN](https://ccb.agent-aura.top/docs/features/lan-pipes) |
|
||||||
|
| ACP 协议一等一支持 | 支持接入 Zed、Cursor 等 IDE,支持会话恢复、Skills、权限桥接 | [文档](https://ccb.agent-aura.top/docs/features/acp-zed) |
|
||||||
| Remote Control 私有部署 | Docker 自托管 RCS + Web UI | [文档](https://ccb.agent-aura.top/docs/features/remote-control-self-hosting) |
|
| 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) |
|
| /dream 记忆整理 | 自动整理和优化记忆文件 | [文档](https://ccb.agent-aura.top/docs/features/auto-dream) |
|
||||||
| Web Search | 内置网页搜索工具 | [文档](https://ccb.agent-aura.top/docs/features/web-browser-tool) |
|
| 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) |
|
| 自定义模型供应商 | 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) |
|
| 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) |
|
| Computer Use | 屏幕截图、键鼠控制 | [文档](https://ccb.agent-aura.top/docs/features/computer-use) |
|
||||||
| Sentry / GrowthBook 企业监控 | 企业级错误追踪与特性开关 | [Sentry](https://ccb.agent-aura.top/docs/internals/sentry-setup)<br>[GrowthBook](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) |
|
| Chrome Use | 浏览器自动化、表单填写、数据抓取 | [自托管](https://ccb.agent-aura.top/docs/features/chrome-use-mcp) [原生版](https://ccb.agent-aura.top/docs/features/claude-in-chrome-mcp) |
|
||||||
|
| Sentry | 企业级错误追踪 | [文档](https://ccb.agent-aura.top/docs/internals/sentry-setup) |
|
||||||
|
| GrowthBook | 企业级特性开关 | [文档](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) |
|
||||||
| Langfuse 监控 | LLM 调用/工具执行/多 Agent 全链路追踪 | [文档](https://ccb.agent-aura.top/docs/features/langfuse-monitoring) |
|
| Langfuse 监控 | LLM 调用/工具执行/多 Agent 全链路追踪 | [文档](https://ccb.agent-aura.top/docs/features/langfuse-monitoring) |
|
||||||
| Poor Mode | 穷鬼模式,关闭记忆提取和键入建议 | /poor 可以开关 |
|
| Poor Mode | 穷鬼模式,关闭记忆提取和键入建议 | /poor 可以开关 |
|
||||||
|
|
||||||
|
|||||||
56
build.ts
56
build.ts
@@ -30,6 +30,8 @@ const DEFAULT_BUILD_FEATURES = [
|
|||||||
'ULTRAPLAN',
|
'ULTRAPLAN',
|
||||||
// P2: daemon + remote control server
|
// P2: daemon + remote control server
|
||||||
'DAEMON',
|
'DAEMON',
|
||||||
|
// ACP (Agent Client Protocol) agent mode
|
||||||
|
'ACP',
|
||||||
// PR-package restored features
|
// PR-package restored features
|
||||||
'WORKFLOW_SCRIPTS',
|
'WORKFLOW_SCRIPTS',
|
||||||
'HISTORY_SNIP',
|
'HISTORY_SNIP',
|
||||||
@@ -88,8 +90,27 @@ for (const file of files) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also patch unguarded globalThis.Bun destructuring from third-party deps
|
||||||
|
// (e.g. @anthropic-ai/sandbox-runtime) so Node.js doesn't crash at import time.
|
||||||
|
let bunPatched = 0
|
||||||
|
const BUN_DESTRUCTURE = /var \{([^}]+)\} = globalThis\.Bun;?/g
|
||||||
|
const BUN_DESTRUCTURE_SAFE = 'var {$1} = typeof globalThis.Bun !== "undefined" ? globalThis.Bun : {};'
|
||||||
|
for (const file of files) {
|
||||||
|
if (!file.endsWith('.js')) continue
|
||||||
|
const filePath = join(outdir, file)
|
||||||
|
const content = await readFile(filePath, 'utf-8')
|
||||||
|
if (BUN_DESTRUCTURE.test(content)) {
|
||||||
|
await writeFile(
|
||||||
|
filePath,
|
||||||
|
content.replace(BUN_DESTRUCTURE, BUN_DESTRUCTURE_SAFE),
|
||||||
|
)
|
||||||
|
bunPatched++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BUN_DESTRUCTURE.lastIndex = 0
|
||||||
|
|
||||||
console.log(
|
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)
|
// Step 4: Copy native .node addon files (audio-capture)
|
||||||
@@ -119,38 +140,7 @@ const cliNode = join(outdir, 'cli-node.js')
|
|||||||
|
|
||||||
await writeFile(cliBun, '#!/usr/bin/env bun\nimport "./cli.js"\n')
|
await writeFile(cliBun, '#!/usr/bin/env bun\nimport "./cli.js"\n')
|
||||||
|
|
||||||
// Node.js entry needs a Bun API polyfill because Bun.build({ target: 'bun' })
|
await writeFile(cliNode, '#!/usr/bin/env node\nimport "./cli.js"\n')
|
||||||
// emits globalThis.Bun references (e.g. Bun.$ shell tag in computer-use-input,
|
|
||||||
// Bun.which in chunk-ys6smqg9) that crash at import time under plain Node.js.
|
|
||||||
const NODE_BUN_POLYFILL = `#!/usr/bin/env node
|
|
||||||
// Bun API polyfill for Node.js runtime
|
|
||||||
if (typeof globalThis.Bun === "undefined") {
|
|
||||||
const { execFileSync } = await import("child_process");
|
|
||||||
const { resolve, delimiter } = await import("path");
|
|
||||||
const { accessSync, constants: { X_OK } } = await import("fs");
|
|
||||||
function which(bin) {
|
|
||||||
const isWin = process.platform === "win32";
|
|
||||||
const pathExt = isWin ? (process.env.PATHEXT || ".EXE").split(";") : [""];
|
|
||||||
for (const dir of (process.env.PATH || "").split(delimiter)) {
|
|
||||||
for (const ext of pathExt) {
|
|
||||||
const candidate = resolve(dir, bin + ext);
|
|
||||||
try { accessSync(candidate, X_OK); return candidate; } catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// Bun.$ is the shell template tag (e.g. $\`osascript ...\`). Only used by
|
|
||||||
// computer-use-input/darwin — stub it so the top-level destructuring
|
|
||||||
// \`var { $ } = globalThis.Bun\` doesn't crash.
|
|
||||||
function $(parts, ...args) {
|
|
||||||
throw new Error("Bun.$ shell API is not available in Node.js. Use Bun runtime for this feature.");
|
|
||||||
}
|
|
||||||
globalThis.Bun = { which, $ };
|
|
||||||
}
|
|
||||||
import "./cli.js"
|
|
||||||
`
|
|
||||||
await writeFile(cliNode, NODE_BUN_POLYFILL)
|
|
||||||
// NOTE: when new Bun-specific globals appear in bundled output, add them here.
|
|
||||||
|
|
||||||
// Make both executable
|
// Make both executable
|
||||||
const { chmodSync } = await import('fs')
|
const { chmodSync } = await import('fs')
|
||||||
|
|||||||
160
bun.lock
160
bun.lock
@@ -5,7 +5,9 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "claude-code-best",
|
"name": "claude-code-best",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mcp-chrome-bridge": "^1.0.31",
|
"@agentclientprotocol/sdk": "^0.19.0",
|
||||||
|
"@claude-code-best/mcp-chrome-bridge": "^2.0.7",
|
||||||
|
"ws": "^8.20.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@alcalzone/ansi-tokenize": "^0.3.0",
|
"@alcalzone/ansi-tokenize": "^0.3.0",
|
||||||
@@ -57,10 +59,11 @@
|
|||||||
"@sentry/node": "^10.47.0",
|
"@sentry/node": "^10.47.0",
|
||||||
"@smithy/core": "^3.23.13",
|
"@smithy/core": "^3.23.13",
|
||||||
"@smithy/node-http-handler": "^4.5.1",
|
"@smithy/node-http-handler": "^4.5.1",
|
||||||
"@types/bun": "^1.3.11",
|
"@types/bun": "^1.3.12",
|
||||||
"@types/cacache": "^20.0.1",
|
"@types/cacache": "^20.0.1",
|
||||||
"@types/he": "^1.2.3",
|
"@types/he": "^1.2.3",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
"@types/node": "^25.6.0",
|
||||||
"@types/picomatch": "^4.0.3",
|
"@types/picomatch": "^4.0.3",
|
||||||
"@types/plist": "^3.0.5",
|
"@types/plist": "^3.0.5",
|
||||||
"@types/proper-lockfile": "^4.1.4",
|
"@types/proper-lockfile": "^4.1.4",
|
||||||
@@ -116,6 +119,7 @@
|
|||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-compiler-runtime": "^1.0.0",
|
"react-compiler-runtime": "^1.0.0",
|
||||||
"react-reconciler": "^0.33.0",
|
"react-reconciler": "^0.33.0",
|
||||||
|
"rollup": "^4.60.1",
|
||||||
"semver": "^7.7.4",
|
"semver": "^7.7.4",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"shell-quote": "^1.8.3",
|
"shell-quote": "^1.8.3",
|
||||||
@@ -130,11 +134,11 @@
|
|||||||
"undici": "^7.24.6",
|
"undici": "^7.24.6",
|
||||||
"url-handler-napi": "workspace:*",
|
"url-handler-napi": "workspace:*",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
|
"vite": "^8.0.8",
|
||||||
"vscode-jsonrpc": "^8.2.1",
|
"vscode-jsonrpc": "^8.2.1",
|
||||||
"vscode-languageserver-protocol": "^3.17.5",
|
"vscode-languageserver-protocol": "^3.17.5",
|
||||||
"vscode-languageserver-types": "^3.17.5",
|
"vscode-languageserver-types": "^3.17.5",
|
||||||
"wrap-ansi": "^10.0.0",
|
"wrap-ansi": "^10.0.0",
|
||||||
"ws": "^8.20.0",
|
|
||||||
"xss": "^1.0.15",
|
"xss": "^1.0.15",
|
||||||
"yaml": "^2.8.3",
|
"yaml": "^2.8.3",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
@@ -253,6 +257,8 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"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=="],
|
"@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"],
|
"@ant/claude-for-chrome-mcp": ["@ant/claude-for-chrome-mcp@workspace:packages/@ant/claude-for-chrome-mcp"],
|
||||||
@@ -443,6 +449,8 @@
|
|||||||
|
|
||||||
"@claude-code-best/builtin-tools": ["@claude-code-best/builtin-tools@workspace:packages/builtin-tools"],
|
"@claude-code-best/builtin-tools": ["@claude-code-best/builtin-tools@workspace:packages/builtin-tools"],
|
||||||
|
|
||||||
|
"@claude-code-best/mcp-chrome-bridge": ["@claude-code-best/mcp-chrome-bridge@2.0.7", "https://registry.npmmirror.com/@claude-code-best/mcp-chrome-bridge/-/mcp-chrome-bridge-2.0.7.tgz", { "dependencies": { "@fastify/cors": "^11.0.1", "@modelcontextprotocol/sdk": "^1.11.0", "chalk": "^5.4.1", "chrome-mcp-shared": "^1.0.2", "commander": "^13.1.0", "fastify": "^5.3.2", "is-admin": "^4.0.0", "pino": "^9.6.0", "uuid": "^11.1.0" }, "bin": { "mcp-chrome-bridge": "dist/cli.js", "mcp-chrome-stdio": "dist/mcp/mcp-server-stdio.js" } }, "sha512-gb64+Ga6li3A8Ll9NKV+ePBn5/U0fccCdrH43tGYveLKZIZxURz8cbY+Z3BdbTdYSPVdFXtfUlp3TMxu4OT5gg=="],
|
||||||
|
|
||||||
"@claude-code-best/mcp-client": ["@claude-code-best/mcp-client@workspace:packages/mcp-client"],
|
"@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=="],
|
"@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=="],
|
||||||
@@ -839,6 +847,36 @@
|
|||||||
|
|
||||||
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.0.tgz", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
|
"@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=="],
|
"@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=="],
|
"@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=="],
|
||||||
@@ -1053,8 +1091,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": ["@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": ["@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=="],
|
"@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=="],
|
||||||
@@ -1143,16 +1179,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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"bowser": ["bowser@2.14.1", "https://registry.npmmirror.com/bowser/-/bowser-2.14.1.tgz", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="],
|
||||||
@@ -1163,8 +1193,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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
||||||
@@ -1189,10 +1217,6 @@
|
|||||||
|
|
||||||
"chokidar": ["chokidar@5.0.0", "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
||||||
@@ -1249,10 +1273,6 @@
|
|||||||
|
|
||||||
"decamelize": ["decamelize@1.2.0", "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
|
"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": ["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=="],
|
"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=="],
|
||||||
@@ -1273,8 +1293,6 @@
|
|||||||
|
|
||||||
"dom-mutator": ["dom-mutator@0.6.0", "https://registry.npmmirror.com/dom-mutator/-/dom-mutator-0.6.0.tgz", {}, "sha512-iCt9o0aYfXMUkz/43ZOAUFQYotjGB+GNbYJiJdz4TgXkyToXbbRy5S6FbTp72lRBtfpUMwEc1KmpFEU4CZeoNg=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
||||||
@@ -1287,8 +1305,6 @@
|
|||||||
|
|
||||||
"encodeurl": ["encodeurl@2.0.0", "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
||||||
@@ -1317,8 +1333,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=="],
|
"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": ["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=="],
|
"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=="],
|
||||||
@@ -1359,8 +1373,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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
||||||
@@ -1385,8 +1397,6 @@
|
|||||||
|
|
||||||
"fresh": ["fresh@2.0.0", "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
"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-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=="],
|
"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=="],
|
||||||
@@ -1417,8 +1427,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=="],
|
"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": ["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=="],
|
"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=="],
|
||||||
@@ -1457,8 +1465,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=="],
|
"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=="],
|
"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"],
|
"image-processor-napi": ["image-processor-napi@workspace:packages/image-processor-napi"],
|
||||||
@@ -1469,8 +1475,6 @@
|
|||||||
|
|
||||||
"inherits": ["inherits@2.0.4", "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
||||||
@@ -1595,8 +1599,6 @@
|
|||||||
|
|
||||||
"math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
"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=="],
|
"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=="],
|
"merge-descriptors": ["merge-descriptors@2.0.0", "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||||
@@ -1613,8 +1615,6 @@
|
|||||||
|
|
||||||
"mimic-fn": ["mimic-fn@2.1.0", "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
"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=="],
|
"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=="],
|
"minimist": ["minimist@1.2.8", "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||||
@@ -1627,8 +1627,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=="],
|
"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"],
|
"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=="],
|
"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=="],
|
||||||
@@ -1641,15 +1639,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=="],
|
"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=="],
|
"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-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=="],
|
"node-forge": ["node-forge@1.4.0", "https://registry.npmmirror.com/node-forge/-/node-forge-1.4.0.tgz", {}, "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ=="],
|
||||||
|
|
||||||
@@ -1737,8 +1731,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=="],
|
"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-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=="],
|
"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=="],
|
||||||
@@ -1753,8 +1745,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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
||||||
@@ -1767,8 +1757,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=="],
|
"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": ["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=="],
|
"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=="],
|
||||||
@@ -1779,8 +1767,6 @@
|
|||||||
|
|
||||||
"react-refresh": ["react-refresh@0.17.0", "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
"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=="],
|
"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=="],
|
"real-require": ["real-require@0.2.0", "https://registry.npmmirror.com/real-require/-/real-require-0.2.0.tgz", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
|
||||||
@@ -1803,6 +1789,8 @@
|
|||||||
|
|
||||||
"rfdc": ["rfdc@1.4.1", "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
||||||
@@ -1853,10 +1841,6 @@
|
|||||||
|
|
||||||
"signal-exit": ["signal-exit@4.1.0", "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
"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=="],
|
"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=="],
|
"smol-toml": ["smol-toml@1.6.1", "https://registry.npmmirror.com/smol-toml/-/smol-toml-1.6.1.tgz", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="],
|
||||||
@@ -1875,8 +1859,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-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-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=="],
|
"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=="],
|
||||||
@@ -1895,10 +1877,6 @@
|
|||||||
|
|
||||||
"tapable": ["tapable@2.3.2", "https://registry.npmmirror.com/tapable/-/tapable-2.3.2.tgz", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="],
|
"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": ["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=="],
|
"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=="],
|
||||||
@@ -1923,8 +1901,6 @@
|
|||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
||||||
@@ -1951,13 +1927,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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="],
|
||||||
|
|
||||||
@@ -2025,6 +1999,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/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/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=="],
|
"@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=="],
|
||||||
@@ -2351,8 +2327,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/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=="],
|
"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=="],
|
"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=="],
|
||||||
@@ -2365,8 +2339,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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
||||||
@@ -2381,7 +2353,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=="],
|
"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=="],
|
"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=="],
|
||||||
|
|
||||||
@@ -2515,6 +2489,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/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=="],
|
"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=="],
|
"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=="],
|
||||||
@@ -2567,22 +2543,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=="],
|
"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/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=="],
|
"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=="],
|
||||||
@@ -2601,6 +2561,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/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/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=="],
|
"@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=="],
|
||||||
@@ -2663,20 +2625,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=="],
|
"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/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=="],
|
"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.6 MiB After Width: | Height: | Size: 1.6 MiB |
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
|
||||||
@@ -138,13 +138,19 @@ bun run dist/cli.js
|
|||||||
/remote-control
|
/remote-control
|
||||||
```
|
```
|
||||||
|
|
||||||
CLI 会向 RCS 注册环境,注册成功后在终端显示连接 URL:
|
环境型 Remote Control(例如 `claude remote-control` 子命令)会向 RCS 注册环境,注册成功后在终端显示连接 URL:
|
||||||
|
|
||||||
```
|
```
|
||||||
https://rcs.example.com/code?bridge=<environmentId>
|
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` 会显示对话框,包含以下选项:
|
若已连接,再次执行 `/remote-control` 会显示对话框,包含以下选项:
|
||||||
- **Disconnect this session** — 断开远程连接
|
- **Disconnect this session** — 断开远程连接
|
||||||
@@ -165,7 +171,7 @@ claude bridge
|
|||||||
|
|
||||||
通过 `/remote-control` 命令获取 URL 后,在浏览器打开即可使用。功能:
|
通过 `/remote-control` 命令获取 URL 后,在浏览器打开即可使用。功能:
|
||||||
|
|
||||||
- 查看已注册的运行环境
|
- 查看已注册的运行环境(environment 模式)
|
||||||
- 创建和管理会话
|
- 创建和管理会话
|
||||||
- 实时查看对话消息和工具调用
|
- 实时查看对话消息和工具调用
|
||||||
- 审批 Claude Code 的工具权限请求
|
- 审批 Claude Code 的工具权限请求
|
||||||
@@ -275,4 +281,3 @@ curl https://rcs.example.com/health
|
|||||||
| 依赖 | claude.ai 订阅 + OAuth | 仅需 API Key |
|
| 依赖 | claude.ai 订阅 + OAuth | 仅需 API Key |
|
||||||
|
|
||||||
自托管模式的核心优势是:设置 `CLAUDE_BRIDGE_BASE_URL` 后,代码自动调用 `isSelfHostedBridge()` 返回 `true`,跳过所有 GrowthBook 和订阅检查,无需 claude.ai 账户即可使用。
|
自托管模式的核心优势是:设置 `CLAUDE_BRIDGE_BASE_URL` 后,代码自动调用 `isSelfHostedBridge()` 返回 `true`,跳过所有 GrowthBook 和订阅检查,无需 claude.ai 账户即可使用。
|
||||||
|
|
||||||
|
|||||||
30
package.json
30
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-code-best",
|
"name": "claude-code-best",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "claude-code-best <claude-code-best@proton.me>",
|
"author": "claude-code-best <claude-code-best@proton.me>",
|
||||||
@@ -40,6 +40,9 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bun run build.ts",
|
"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": "bun run scripts/dev.ts",
|
||||||
"dev:inspect": "bun run scripts/dev-debug.ts",
|
"dev:inspect": "bun run scripts/dev-debug.ts",
|
||||||
"prepublishOnly": "bun run build",
|
"prepublishOnly": "bun run build",
|
||||||
@@ -52,16 +55,15 @@
|
|||||||
"health": "bun run scripts/health-check.ts",
|
"health": "bun run scripts/health-check.ts",
|
||||||
"postinstall": "node scripts/postinstall.cjs && node scripts/setup-chrome-mcp.mjs",
|
"postinstall": "node scripts/postinstall.cjs && node scripts/setup-chrome-mcp.mjs",
|
||||||
"docs:dev": "npx mintlify dev",
|
"docs:dev": "npx mintlify dev",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
"rcs": "bun run scripts/rcs.ts"
|
"rcs": "bun run scripts/rcs.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mcp-chrome-bridge": "^1.0.31"
|
"@agentclientprotocol/sdk": "^0.19.0",
|
||||||
|
"@claude-code-best/mcp-chrome-bridge": "^2.0.7",
|
||||||
|
"ws": "^8.20.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
"@alcalzone/ansi-tokenize": "^0.3.0",
|
||||||
"@ant/claude-for-chrome-mcp": "workspace:*",
|
"@ant/claude-for-chrome-mcp": "workspace:*",
|
||||||
"@ant/computer-use-input": "workspace:*",
|
"@ant/computer-use-input": "workspace:*",
|
||||||
@@ -75,9 +77,6 @@
|
|||||||
"@anthropic-ai/sdk": "^0.80.0",
|
"@anthropic-ai/sdk": "^0.80.0",
|
||||||
"@anthropic-ai/vertex-sdk": "^0.14.4",
|
"@anthropic-ai/vertex-sdk": "^0.14.4",
|
||||||
"@anthropic/ink": "workspace:*",
|
"@anthropic/ink": "workspace:*",
|
||||||
"@claude-code-best/builtin-tools": "workspace:*",
|
|
||||||
"@claude-code-best/agent-tools": "workspace:*",
|
|
||||||
"@claude-code-best/mcp-client": "workspace:*",
|
|
||||||
"@aws-sdk/client-bedrock": "^3.1020.0",
|
"@aws-sdk/client-bedrock": "^3.1020.0",
|
||||||
"@aws-sdk/client-bedrock-runtime": "^3.1020.0",
|
"@aws-sdk/client-bedrock-runtime": "^3.1020.0",
|
||||||
"@aws-sdk/client-sts": "^3.1020.0",
|
"@aws-sdk/client-sts": "^3.1020.0",
|
||||||
@@ -85,8 +84,13 @@
|
|||||||
"@aws-sdk/credential-providers": "^3.1020.0",
|
"@aws-sdk/credential-providers": "^3.1020.0",
|
||||||
"@azure/identity": "^4.13.1",
|
"@azure/identity": "^4.13.1",
|
||||||
"@biomejs/biome": "^2.4.10",
|
"@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",
|
"@commander-js/extra-typings": "^14.0.0",
|
||||||
"@growthbook/growthbook": "^1.6.5",
|
"@growthbook/growthbook": "^1.6.5",
|
||||||
|
"@langfuse/otel": "^5.1.0",
|
||||||
|
"@langfuse/tracing": "^5.1.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
"@opentelemetry/api": "^1.9.1",
|
"@opentelemetry/api": "^1.9.1",
|
||||||
"@opentelemetry/api-logs": "^0.214.0",
|
"@opentelemetry/api-logs": "^0.214.0",
|
||||||
@@ -109,8 +113,11 @@
|
|||||||
"@sentry/node": "^10.47.0",
|
"@sentry/node": "^10.47.0",
|
||||||
"@smithy/core": "^3.23.13",
|
"@smithy/core": "^3.23.13",
|
||||||
"@smithy/node-http-handler": "^4.5.1",
|
"@smithy/node-http-handler": "^4.5.1",
|
||||||
"@types/bun": "^1.3.11",
|
"@types/bun": "^1.3.12",
|
||||||
"@types/cacache": "^20.0.1",
|
"@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/picomatch": "^4.0.3",
|
||||||
"@types/plist": "^3.0.5",
|
"@types/plist": "^3.0.5",
|
||||||
"@types/proper-lockfile": "^4.1.4",
|
"@types/proper-lockfile": "^4.1.4",
|
||||||
@@ -166,6 +173,7 @@
|
|||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-compiler-runtime": "^1.0.0",
|
"react-compiler-runtime": "^1.0.0",
|
||||||
"react-reconciler": "^0.33.0",
|
"react-reconciler": "^0.33.0",
|
||||||
|
"rollup": "^4.60.1",
|
||||||
"semver": "^7.7.4",
|
"semver": "^7.7.4",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"shell-quote": "^1.8.3",
|
"shell-quote": "^1.8.3",
|
||||||
@@ -180,11 +188,11 @@
|
|||||||
"undici": "^7.24.6",
|
"undici": "^7.24.6",
|
||||||
"url-handler-napi": "workspace:*",
|
"url-handler-napi": "workspace:*",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
|
"vite": "^8.0.8",
|
||||||
"vscode-jsonrpc": "^8.2.1",
|
"vscode-jsonrpc": "^8.2.1",
|
||||||
"vscode-languageserver-protocol": "^3.17.5",
|
"vscode-languageserver-protocol": "^3.17.5",
|
||||||
"vscode-languageserver-types": "^3.17.5",
|
"vscode-languageserver-types": "^3.17.5",
|
||||||
"wrap-ansi": "^10.0.0",
|
"wrap-ansi": "^10.0.0",
|
||||||
"ws": "^8.20.0",
|
|
||||||
"xss": "^1.0.15",
|
"xss": "^1.0.15",
|
||||||
"yaml": "^2.8.3",
|
"yaml": "^2.8.3",
|
||||||
"zod": "^4.3.6"
|
"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.
|
* 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'
|
import type { FrontmostAppInfo, InputBackend } from '../types.js'
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile)
|
||||||
|
|
||||||
const KEY_MAP: Record<string, number> = {
|
const KEY_MAP: Record<string, number> = {
|
||||||
return: 36, enter: 36, tab: 48, space: 49, delete: 51, backspace: 51,
|
return: 36, enter: 36, tab: 48, space: 49, delete: 51, backspace: 51,
|
||||||
escape: 53, esc: 53,
|
escape: 53, esc: 53,
|
||||||
@@ -25,13 +28,17 @@ const MODIFIER_MAP: Record<string, string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function osascript(script: string): Promise<string> {
|
async function osascript(script: string): Promise<string> {
|
||||||
const result = await $`osascript -e ${script}`.quiet().nothrow().text()
|
const { stdout } = await execFileAsync('osascript', ['-e', script], {
|
||||||
return result.trim()
|
encoding: 'utf-8',
|
||||||
|
})
|
||||||
|
return stdout.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function jxa(script: string): Promise<string> {
|
async function jxa(script: string): Promise<string> {
|
||||||
const result = await $`osascript -l JavaScript -e ${script}`.quiet().nothrow().text()
|
const { stdout } = await execFileAsync('osascript', ['-l', 'JavaScript', '-e', script], {
|
||||||
return result.trim()
|
encoding: 'utf-8',
|
||||||
|
})
|
||||||
|
return stdout.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMouseJxa(eventType: string, x: number, y: number, btn: number, clickState?: number): string {
|
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'] = () => {
|
export const getFrontmostAppInfo: InputBackend['getFrontmostAppInfo'] = () => {
|
||||||
try {
|
try {
|
||||||
const result = Bun.spawnSync({
|
const output = execFileSync('osascript', ['-e', `
|
||||||
cmd: ['osascript', '-e', `
|
tell application "System Events"
|
||||||
tell application "System Events"
|
set frontApp to first application process whose frontmost is true
|
||||||
set frontApp to first application process whose frontmost is true
|
set appName to name of frontApp
|
||||||
set appName to name of frontApp
|
set bundleId to bundle identifier of frontApp
|
||||||
set bundleId to bundle identifier of frontApp
|
return bundleId & "|" & appName
|
||||||
return bundleId & "|" & appName
|
end tell
|
||||||
end tell
|
`], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim()
|
||||||
`],
|
|
||||||
stdout: 'pipe',
|
|
||||||
stderr: 'pipe',
|
|
||||||
})
|
|
||||||
const output = new TextDecoder().decode(result.stdout).trim()
|
|
||||||
if (!output || !output.includes('|')) return null
|
if (!output || !output.includes('|')) return null
|
||||||
const [bundleId, appName] = output.split('|', 2)
|
const [bundleId, appName] = output.split('|', 2)
|
||||||
return { bundleId: bundleId!, appName: appName! }
|
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"]
|
||||||
|
}
|
||||||
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))
|
if (displayId !== undefined) args.push('-D', String(displayId))
|
||||||
return captureScreenToBase64(args)
|
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 }
|
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,
|
x: number, y: number, w: number, h: number,
|
||||||
outW: number, outH: number, quality: number, displayId?: number,
|
outW: number, outH: number, quality: number, displayId?: number,
|
||||||
): Promise<ScreenshotResult>
|
): Promise<ScreenshotResult>
|
||||||
|
captureWindowTarget(titleOrHwnd: string | number): ScreenshotResult | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SwiftBackend {
|
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"]
|
||||||
|
}
|
||||||
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"]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from 'bun:test'
|
import { describe, expect, test } from 'bun:test'
|
||||||
import type { CoreTool, Tool, Tools, AnyObject, ToolResult, ValidationResult, PermissionResult } from '@claude-code-best/agent-tools'
|
import type { CoreTool, Tool, Tools, AnyObject, ToolResult, ValidationResult, PermissionResult } from '@claude-code-best/agent-tools'
|
||||||
import type { Tool as HostTool } from '../../src/Tool.js'
|
import type { Tool as HostTool } from '../../../../src/Tool.js'
|
||||||
|
|
||||||
describe('agent-tools compatibility', () => {
|
describe('agent-tools compatibility', () => {
|
||||||
test('CoreTool structural compatibility with host Tool', () => {
|
test('CoreTool structural compatibility with host Tool', () => {
|
||||||
@@ -27,7 +27,7 @@ describe('agent-tools compatibility', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This assignment should work if HostTool structurally extends CoreTool
|
// This assignment should work if HostTool structurally extends CoreTool
|
||||||
const coreTool: CoreTool = mockHostTool as CoreTool
|
const coreTool: CoreTool = mockHostTool as unknown as CoreTool
|
||||||
expect(coreTool.name).toBe('test')
|
expect(coreTool.name).toBe('test')
|
||||||
expect(coreTool.isEnabled()).toBe(true)
|
expect(coreTool.isEnabled()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|||||||
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"]
|
||||||
|
}
|
||||||
5
packages/builtin-tools/tsconfig.json
Normal file
5
packages/builtin-tools/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -72,18 +72,18 @@ describe("detectColorMode", () => {
|
|||||||
|
|
||||||
describe("detectLanguage", () => {
|
describe("detectLanguage", () => {
|
||||||
test("detects language from file extension", () => {
|
test("detects language from file extension", () => {
|
||||||
expect(detectLanguage("index.ts")).toBe("ts");
|
expect(detectLanguage("index.ts", null)).toBe("ts");
|
||||||
expect(detectLanguage("main.py")).toBe("py");
|
expect(detectLanguage("main.py", null)).toBe("py");
|
||||||
expect(detectLanguage("style.css")).toBe("css");
|
expect(detectLanguage("style.css", null)).toBe("css");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("detects language from known filenames", () => {
|
test("detects language from known filenames", () => {
|
||||||
expect(detectLanguage("Makefile")).toBe("makefile");
|
expect(detectLanguage("Makefile", null)).toBe("makefile");
|
||||||
expect(detectLanguage("Dockerfile")).toBe("dockerfile");
|
expect(detectLanguage("Dockerfile", null)).toBe("dockerfile");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns null for unknown extensions", () => {
|
test("returns null for unknown extensions", () => {
|
||||||
expect(detectLanguage("file.xyz123")).toBeNull();
|
expect(detectLanguage("file.xyz123", null)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
5
packages/color-diff-napi/tsconfig.json
Normal file
5
packages/color-diff-napi/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
5
packages/image-processor-napi/tsconfig.json
Normal file
5
packages/image-processor-napi/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ describe('InProcessTransport', () => {
|
|||||||
let received: JSONRPCMessage | null = null
|
let received: JSONRPCMessage | null = null
|
||||||
client.onmessage = (msg) => { received = msg }
|
client.onmessage = (msg) => { received = msg }
|
||||||
|
|
||||||
await server.send({ jsonrpc: '2.0', result: 42, id: 1 })
|
await server.send({ jsonrpc: '2.0', result: 42, id: 1 } as any)
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 10))
|
await new Promise(resolve => setTimeout(resolve, 10))
|
||||||
|
|
||||||
|
|||||||
@@ -57,9 +57,9 @@ describe('discoverTools', () => {
|
|||||||
expect(tool.name).toBe('mcp__my-server__search')
|
expect(tool.name).toBe('mcp__my-server__search')
|
||||||
expect(tool.mcpInfo).toEqual({ serverName: 'my-server', toolName: 'search' })
|
expect(tool.mcpInfo).toEqual({ serverName: 'my-server', toolName: 'search' })
|
||||||
expect(tool.isMcp).toBe(true)
|
expect(tool.isMcp).toBe(true)
|
||||||
expect(tool.isReadOnly()).toBe(true)
|
expect(tool.isReadOnly({} as any)).toBe(true)
|
||||||
expect(tool.userFacingName()).toBe('Search Items')
|
expect(tool.userFacingName(undefined)).toBe('Search Items')
|
||||||
expect(await tool.description()).toBe('Search for items')
|
expect(await tool.description({} as any, { isNonInteractiveSession: false, toolPermissionContext: {}, tools: [] })).toBe('Search for items')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('respects skipPrefix option', async () => {
|
test('respects skipPrefix option', async () => {
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ describe('createMcpManager', () => {
|
|||||||
|
|
||||||
const result = await manager.connect('test-server', { command: 'npx', args: [] })
|
const result = await manager.connect('test-server', { command: 'npx', args: [] })
|
||||||
expect(result.type).toBe('connected')
|
expect(result.type).toBe('connected')
|
||||||
expect(connectedEvent).toBe('test-server')
|
expect(connectedEvent as unknown as string).toBe('test-server')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('disconnect calls cleanup and emits disconnected', async () => {
|
test('disconnect calls cleanup and emits disconnected', async () => {
|
||||||
|
|||||||
5
packages/mcp-client/tsconfig.json
Normal file
5
packages/mcp-client/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
5
packages/modifiers-napi/tsconfig.json
Normal file
5
packages/modifiers-napi/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -25,17 +25,18 @@ import {
|
|||||||
storeUpdateSession,
|
storeUpdateSession,
|
||||||
storeGetEnvironment,
|
storeGetEnvironment,
|
||||||
storeGetSession,
|
storeGetSession,
|
||||||
storeListActiveEnvironments,
|
|
||||||
} from "../store";
|
} from "../store";
|
||||||
|
import { getEventBus, getAllEventBuses, removeEventBus } from "../transport/event-bus";
|
||||||
|
import { runDisconnectMonitorSweep } from "../services/disconnect-monitor";
|
||||||
|
|
||||||
describe("Disconnect Monitor Logic", () => {
|
describe("Disconnect Monitor Logic", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
storeReset();
|
storeReset();
|
||||||
|
for (const [key] of getAllEventBuses()) {
|
||||||
|
removeEventBus(key);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test the logic directly rather than the interval-based monitor
|
|
||||||
// to avoid long-running tests with timers
|
|
||||||
|
|
||||||
test("environment times out when lastPollAt is too old", () => {
|
test("environment times out when lastPollAt is too old", () => {
|
||||||
const env = storeCreateEnvironment({ secret: "s" });
|
const env = storeCreateEnvironment({ secret: "s" });
|
||||||
const timeoutMs = 300 * 1000; // 5 minutes
|
const timeoutMs = 300 * 1000; // 5 minutes
|
||||||
@@ -44,14 +45,7 @@ describe("Disconnect Monitor Logic", () => {
|
|||||||
const oldDate = new Date(Date.now() - timeoutMs - 60000);
|
const oldDate = new Date(Date.now() - timeoutMs - 60000);
|
||||||
storeUpdateEnvironment(env.id, { lastPollAt: oldDate });
|
storeUpdateEnvironment(env.id, { lastPollAt: oldDate });
|
||||||
|
|
||||||
// Check the timeout logic (same as in disconnect-monitor.ts)
|
runDisconnectMonitorSweep();
|
||||||
const now = Date.now();
|
|
||||||
const envs = storeListActiveEnvironments();
|
|
||||||
for (const e of envs) {
|
|
||||||
if (e.lastPollAt && now - e.lastPollAt.getTime() > timeoutMs) {
|
|
||||||
storeUpdateEnvironment(e.id, { status: "disconnected" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated = storeGetEnvironment(env.id);
|
const updated = storeGetEnvironment(env.id);
|
||||||
expect(updated?.status).toBe("disconnected");
|
expect(updated?.status).toBe("disconnected");
|
||||||
@@ -59,43 +53,56 @@ describe("Disconnect Monitor Logic", () => {
|
|||||||
|
|
||||||
test("environment stays active when lastPollAt is recent", () => {
|
test("environment stays active when lastPollAt is recent", () => {
|
||||||
const env = storeCreateEnvironment({ secret: "s" });
|
const env = storeCreateEnvironment({ secret: "s" });
|
||||||
const timeoutMs = 300 * 1000;
|
runDisconnectMonitorSweep();
|
||||||
|
|
||||||
// lastPollAt is recent (just created)
|
|
||||||
const now = Date.now();
|
|
||||||
const envs = storeListActiveEnvironments();
|
|
||||||
for (const e of envs) {
|
|
||||||
if (e.lastPollAt && now - e.lastPollAt.getTime() > timeoutMs) {
|
|
||||||
storeUpdateEnvironment(e.id, { status: "disconnected" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated = storeGetEnvironment(env.id);
|
const updated = storeGetEnvironment(env.id);
|
||||||
expect(updated?.status).toBe("active");
|
expect(updated?.status).toBe("active");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("session becomes inactive when updatedAt is too old", () => {
|
test("session becomes inactive when updatedAt is too old", () => {
|
||||||
const session = storeCreateSession({ status: "idle" });
|
const session = storeCreateSession({});
|
||||||
storeUpdateSession(session.id, { status: "running" });
|
storeUpdateSession(session.id, { status: "running" });
|
||||||
const timeoutMs = 300 * 1000 * 2; // 2x disconnect timeout
|
|
||||||
|
|
||||||
// Simulate updatedAt being older than 2x timeout
|
|
||||||
// We can't directly set updatedAt, but we can verify the logic
|
|
||||||
// by checking that recently updated sessions are not marked inactive
|
|
||||||
const now = Date.now();
|
|
||||||
const rec = storeGetSession(session.id);
|
const rec = storeGetSession(session.id);
|
||||||
// Session was just updated, should not be inactive
|
expect(rec).toBeTruthy();
|
||||||
expect(rec?.status).toBe("running");
|
if (!rec) return;
|
||||||
expect(now - rec!.updatedAt.getTime()).toBeLessThan(timeoutMs);
|
|
||||||
|
rec.updatedAt = new Date(Date.now() - 300 * 1000 * 2 - 60000);
|
||||||
|
|
||||||
|
runDisconnectMonitorSweep();
|
||||||
|
|
||||||
|
const updated = storeGetSession(session.id);
|
||||||
|
expect(updated?.status).toBe("inactive");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("session stays running when recently updated", () => {
|
test("session stays running when recently updated", () => {
|
||||||
const session = storeCreateSession({});
|
const session = storeCreateSession({});
|
||||||
storeUpdateSession(session.id, { status: "running" });
|
storeUpdateSession(session.id, { status: "running" });
|
||||||
|
|
||||||
const timeoutMs = 300 * 1000 * 2;
|
runDisconnectMonitorSweep();
|
||||||
|
|
||||||
|
const updated = storeGetSession(session.id);
|
||||||
|
expect(updated?.status).toBe("running");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("session timeout publishes an inactive session_status event", () => {
|
||||||
|
const session = storeCreateSession({});
|
||||||
|
storeUpdateSession(session.id, { status: "idle" });
|
||||||
const rec = storeGetSession(session.id);
|
const rec = storeGetSession(session.id);
|
||||||
expect(rec?.status).toBe("running");
|
expect(rec).toBeTruthy();
|
||||||
expect(Date.now() - rec!.updatedAt.getTime()).toBeLessThan(timeoutMs);
|
if (!rec) return;
|
||||||
|
rec.updatedAt = new Date(Date.now() - 300 * 1000 * 2 - 60000);
|
||||||
|
|
||||||
|
const bus = getEventBus(session.id);
|
||||||
|
const events: Array<{ type: string; payload: { status?: string } }> = [];
|
||||||
|
bus.subscribe((event) => {
|
||||||
|
events.push({ type: event.type, payload: event.payload as { status?: string } });
|
||||||
|
});
|
||||||
|
|
||||||
|
runDisconnectMonitorSweep();
|
||||||
|
|
||||||
|
expect(events).toContainEqual({
|
||||||
|
type: "session_status",
|
||||||
|
payload: { status: "inactive" },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,16 +19,18 @@ mock.module("../config", () => ({
|
|||||||
|
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { storeReset, storeCreateSession, storeCreateEnvironment, storeBindSession } from "../store";
|
import { storeReset, storeCreateSession, storeCreateEnvironment, storeBindSession } from "../store";
|
||||||
import { removeEventBus, getAllEventBuses } from "../transport/event-bus";
|
import { removeEventBus, getAllEventBuses, getEventBus } from "../transport/event-bus";
|
||||||
import { issueToken } from "../auth/token";
|
import { issueToken } from "../auth/token";
|
||||||
|
import { publishSessionEvent } from "../services/transport";
|
||||||
|
|
||||||
// Import route modules
|
// Import route modules
|
||||||
import v1Sessions from "../routes/v1/sessions";
|
import v1Sessions from "../routes/v1/sessions";
|
||||||
import v1Environments from "../routes/v1/environments";
|
import v1Environments from "../routes/v1/environments";
|
||||||
import v1EnvironmentsWork from "../routes/v1/environments.work";
|
import v1EnvironmentsWork from "../routes/v1/environments.work";
|
||||||
import v1SessionIngress from "../routes/v1/session-ingress";
|
import v1SessionIngress, { websocket as sessionIngressWebsocket } from "../routes/v1/session-ingress";
|
||||||
import v2CodeSessions from "../routes/v2/code-sessions";
|
import v2CodeSessions from "../routes/v2/code-sessions";
|
||||||
import v2Worker from "../routes/v2/worker";
|
import v2Worker from "../routes/v2/worker";
|
||||||
|
import v2WorkerEventsStream from "../routes/v2/worker-events-stream";
|
||||||
import v2WorkerEvents from "../routes/v2/worker-events";
|
import v2WorkerEvents from "../routes/v2/worker-events";
|
||||||
import webAuth from "../routes/web/auth";
|
import webAuth from "../routes/web/auth";
|
||||||
import webSessions from "../routes/web/sessions";
|
import webSessions from "../routes/web/sessions";
|
||||||
@@ -43,6 +45,7 @@ function createApp() {
|
|||||||
app.route("/v2/session_ingress", v1SessionIngress);
|
app.route("/v2/session_ingress", v1SessionIngress);
|
||||||
app.route("/v1/code/sessions", v2CodeSessions);
|
app.route("/v1/code/sessions", v2CodeSessions);
|
||||||
app.route("/v1/code/sessions", v2Worker);
|
app.route("/v1/code/sessions", v2Worker);
|
||||||
|
app.route("/v1/code/sessions", v2WorkerEventsStream);
|
||||||
app.route("/v1/code/sessions", v2WorkerEvents);
|
app.route("/v1/code/sessions", v2WorkerEvents);
|
||||||
app.route("/web", webAuth);
|
app.route("/web", webAuth);
|
||||||
app.route("/web", webSessions);
|
app.route("/web", webSessions);
|
||||||
@@ -53,6 +56,11 @@ function createApp() {
|
|||||||
|
|
||||||
const AUTH_HEADERS = { Authorization: "Bearer test-api-key", "X-Username": "testuser" };
|
const AUTH_HEADERS = { Authorization: "Bearer test-api-key", "X-Username": "testuser" };
|
||||||
|
|
||||||
|
function toWebSessionId(sessionId: string): string {
|
||||||
|
if (!sessionId.startsWith("cse_")) return sessionId;
|
||||||
|
return `session_${sessionId.slice("cse_".length)}`;
|
||||||
|
}
|
||||||
|
|
||||||
describe("V1 Session Routes", () => {
|
describe("V1 Session Routes", () => {
|
||||||
let app: Hono;
|
let app: Hono;
|
||||||
|
|
||||||
@@ -109,6 +117,24 @@ describe("V1 Session Routes", () => {
|
|||||||
expect(res.status).toBe(404);
|
expect(res.status).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("GET /v1/sessions/:id — resolves compat code session IDs", async () => {
|
||||||
|
const createRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
session: { id },
|
||||||
|
} = await createRes.json();
|
||||||
|
|
||||||
|
const getRes = await app.request(`/v1/sessions/${toWebSessionId(id)}`, {
|
||||||
|
headers: AUTH_HEADERS,
|
||||||
|
});
|
||||||
|
expect(getRes.status).toBe(200);
|
||||||
|
const body = await getRes.json();
|
||||||
|
expect(body.id).toBe(id);
|
||||||
|
});
|
||||||
|
|
||||||
test("PATCH /v1/sessions/:id — updates title", async () => {
|
test("PATCH /v1/sessions/:id — updates title", async () => {
|
||||||
const createRes = await app.request("/v1/sessions", {
|
const createRes = await app.request("/v1/sessions", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -142,6 +168,32 @@ describe("V1 Session Routes", () => {
|
|||||||
expect(archiveRes.status).toBe(200);
|
expect(archiveRes.status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("POST /v1/sessions/:id/archive — archives compat code session IDs", async () => {
|
||||||
|
const createRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
session: { id },
|
||||||
|
} = await createRes.json();
|
||||||
|
const compatId = toWebSessionId(id);
|
||||||
|
|
||||||
|
const archiveRes = await app.request(`/v1/sessions/${compatId}/archive`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: AUTH_HEADERS,
|
||||||
|
});
|
||||||
|
expect(archiveRes.status).toBe(200);
|
||||||
|
|
||||||
|
const getRes = await app.request(`/v1/sessions/${compatId}`, {
|
||||||
|
headers: AUTH_HEADERS,
|
||||||
|
});
|
||||||
|
expect(getRes.status).toBe(200);
|
||||||
|
const body = await getRes.json();
|
||||||
|
expect(body.id).toBe(id);
|
||||||
|
expect(body.status).toBe("archived");
|
||||||
|
});
|
||||||
|
|
||||||
test("POST /v1/sessions/:id/events — publishes events", async () => {
|
test("POST /v1/sessions/:id/events — publishes events", async () => {
|
||||||
const createRes = await app.request("/v1/sessions", {
|
const createRes = await app.request("/v1/sessions", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -160,6 +212,30 @@ describe("V1 Session Routes", () => {
|
|||||||
expect(body.events).toBe(1);
|
expect(body.events).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("POST /v1/sessions/:id/events — resolves compat code session IDs", async () => {
|
||||||
|
const createRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
session: { id },
|
||||||
|
} = await createRes.json();
|
||||||
|
const compatId = toWebSessionId(id);
|
||||||
|
|
||||||
|
const eventsRes = await app.request(`/v1/sessions/${compatId}/events`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ events: [{ type: "user", content: "hello from compat" }] }),
|
||||||
|
});
|
||||||
|
expect(eventsRes.status).toBe(200);
|
||||||
|
|
||||||
|
const events = getEventBus(id).getEventsSince(0);
|
||||||
|
expect(events).toHaveLength(1);
|
||||||
|
expect(events[0]?.type).toBe("user");
|
||||||
|
expect((events[0]?.payload as { content?: string }).content).toBe("hello from compat");
|
||||||
|
});
|
||||||
|
|
||||||
test("POST /v1/sessions with environment_id creates work item", async () => {
|
test("POST /v1/sessions with environment_id creates work item", async () => {
|
||||||
// First register an environment
|
// First register an environment
|
||||||
const envRes = await app.request("/v1/environments/bridge", {
|
const envRes = await app.request("/v1/environments/bridge", {
|
||||||
@@ -443,6 +519,26 @@ describe("Web Auth Routes", () => {
|
|||||||
expect(body.ok).toBe(true);
|
expect(body.ok).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("POST /web/bind — binds compat code session ID to UUID", async () => {
|
||||||
|
const sessRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const body = await sessRes.json();
|
||||||
|
const compatId = toWebSessionId(body.session.id);
|
||||||
|
|
||||||
|
const bindRes = await app.request("/web/bind?uuid=test-uuid", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ sessionId: compatId }),
|
||||||
|
});
|
||||||
|
expect(bindRes.status).toBe(200);
|
||||||
|
const bindBody = await bindRes.json();
|
||||||
|
expect(bindBody.ok).toBe(true);
|
||||||
|
expect(bindBody.sessionId).toBe(compatId);
|
||||||
|
});
|
||||||
|
|
||||||
test("POST /web/bind — 404 for unknown session", async () => {
|
test("POST /web/bind — 404 for unknown session", async () => {
|
||||||
const res = await app.request("/web/bind?uuid=test-uuid", {
|
const res = await app.request("/web/bind?uuid=test-uuid", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -501,6 +597,24 @@ describe("Web Session Routes", () => {
|
|||||||
expect(sessions[0].id).toBe(id);
|
expect(sessions[0].id).toBe(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("GET /web/sessions and /all — serialize owned code sessions as compat IDs", async () => {
|
||||||
|
const codeSession = storeCreateSession({ idPrefix: "cse_" });
|
||||||
|
storeBindSession(codeSession.id, "user-1");
|
||||||
|
const compatId = toWebSessionId(codeSession.id);
|
||||||
|
|
||||||
|
const listRes = await app.request("/web/sessions?uuid=user-1");
|
||||||
|
expect(listRes.status).toBe(200);
|
||||||
|
const sessions = await listRes.json();
|
||||||
|
expect(sessions).toHaveLength(1);
|
||||||
|
expect(sessions[0].id).toBe(compatId);
|
||||||
|
|
||||||
|
const allRes = await app.request("/web/sessions/all?uuid=user-1");
|
||||||
|
expect(allRes.status).toBe(200);
|
||||||
|
const summaries = await allRes.json();
|
||||||
|
expect(summaries).toHaveLength(1);
|
||||||
|
expect(summaries[0].id).toBe(compatId);
|
||||||
|
});
|
||||||
|
|
||||||
test("GET /web/sessions — requires UUID", async () => {
|
test("GET /web/sessions — requires UUID", async () => {
|
||||||
const res = await app.request("/web/sessions");
|
const res = await app.request("/web/sessions");
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
@@ -525,6 +639,33 @@ describe("Web Session Routes", () => {
|
|||||||
expect(sessions).toHaveLength(1); // only user-1's session, not user-2's
|
expect(sessions).toHaveLength(1); // only user-1's session, not user-2's
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("GET /web/sessions and /all — hides archived and inactive sessions", async () => {
|
||||||
|
const archived = storeCreateSession({});
|
||||||
|
const inactive = storeCreateSession({});
|
||||||
|
const open = storeCreateSession({});
|
||||||
|
storeBindSession(archived.id, "user-1");
|
||||||
|
storeBindSession(inactive.id, "user-1");
|
||||||
|
storeBindSession(open.id, "user-1");
|
||||||
|
|
||||||
|
await app.request(`/v1/sessions/${archived.id}/archive`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: AUTH_HEADERS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { storeUpdateSession } = await import("../store");
|
||||||
|
storeUpdateSession(inactive.id, { status: "inactive" });
|
||||||
|
|
||||||
|
const listRes = await app.request("/web/sessions?uuid=user-1");
|
||||||
|
expect(listRes.status).toBe(200);
|
||||||
|
const sessions = await listRes.json();
|
||||||
|
expect(sessions.map((session: { id: string }) => session.id)).toEqual([open.id]);
|
||||||
|
|
||||||
|
const allRes = await app.request("/web/sessions/all?uuid=user-1");
|
||||||
|
expect(allRes.status).toBe(200);
|
||||||
|
const summaries = await allRes.json();
|
||||||
|
expect(summaries.map((session: { id: string }) => session.id)).toEqual([open.id]);
|
||||||
|
});
|
||||||
|
|
||||||
test("GET /web/sessions/:id — returns owned session", async () => {
|
test("GET /web/sessions/:id — returns owned session", async () => {
|
||||||
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -563,6 +704,22 @@ describe("Web Session Routes", () => {
|
|||||||
expect(body.events).toEqual([]);
|
expect(body.events).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("GET /web/sessions/:id and history — supports compat code session IDs", async () => {
|
||||||
|
const codeSession = storeCreateSession({ idPrefix: "cse_" });
|
||||||
|
storeBindSession(codeSession.id, "user-1");
|
||||||
|
const compatId = toWebSessionId(codeSession.id);
|
||||||
|
|
||||||
|
const getRes = await app.request(`/web/sessions/${compatId}?uuid=user-1`);
|
||||||
|
expect(getRes.status).toBe(200);
|
||||||
|
const session = await getRes.json();
|
||||||
|
expect(session.id).toBe(compatId);
|
||||||
|
|
||||||
|
const histRes = await app.request(`/web/sessions/${compatId}/history?uuid=user-1`);
|
||||||
|
expect(histRes.status).toBe(200);
|
||||||
|
const history = await histRes.json();
|
||||||
|
expect(history.events).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
test("GET /web/sessions/:id/history — 403 for non-owner", async () => {
|
test("GET /web/sessions/:id/history — 403 for non-owner", async () => {
|
||||||
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -647,6 +804,24 @@ describe("Web Session Routes", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("GET /web/sessions/:id/events — supports compat code session IDs", async () => {
|
||||||
|
const codeSession = storeCreateSession({ idPrefix: "cse_" });
|
||||||
|
storeBindSession(codeSession.id, "user-1");
|
||||||
|
const compatId = toWebSessionId(codeSession.id);
|
||||||
|
|
||||||
|
const eventsRes = await app.request(`/web/sessions/${compatId}/events?uuid=user-1`);
|
||||||
|
expect(eventsRes.status).toBe(200);
|
||||||
|
expect(eventsRes.headers.get("Content-Type")).toBe("text/event-stream");
|
||||||
|
|
||||||
|
const reader = eventsRes.body?.getReader();
|
||||||
|
if (reader) {
|
||||||
|
const { value } = await reader.read();
|
||||||
|
const text = new TextDecoder().decode(value!);
|
||||||
|
expect(text).toContain(": keepalive");
|
||||||
|
reader.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("GET /web/sessions/:id/events — 403 for non-owner", async () => {
|
test("GET /web/sessions/:id/events — 403 for non-owner", async () => {
|
||||||
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -658,6 +833,25 @@ describe("Web Session Routes", () => {
|
|||||||
const eventsRes = await app.request(`/web/sessions/${id}/events?uuid=user-2`);
|
const eventsRes = await app.request(`/web/sessions/${id}/events?uuid=user-2`);
|
||||||
expect(eventsRes.status).toBe(403);
|
expect(eventsRes.status).toBe(403);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("GET /web/sessions/:id/events — 409 for archived session", async () => {
|
||||||
|
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const { id } = await createRes.json();
|
||||||
|
|
||||||
|
await app.request(`/v1/sessions/${id}/archive`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: AUTH_HEADERS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await app.request(`/web/sessions/${id}/events?uuid=user-1`);
|
||||||
|
expect(res.status).toBe(409);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.error.type).toBe("session_closed");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Web Control Routes", () => {
|
describe("Web Control Routes", () => {
|
||||||
@@ -692,6 +886,32 @@ describe("Web Control Routes", () => {
|
|||||||
expect(body.event).toBeTruthy();
|
expect(body.event).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("POST /web/sessions/:id/events/control/interrupt — supports compat code session IDs", async () => {
|
||||||
|
const rawSessionId = storeCreateSession({ idPrefix: "cse_" }).id;
|
||||||
|
storeBindSession(rawSessionId, "user-1");
|
||||||
|
const compatId = toWebSessionId(rawSessionId);
|
||||||
|
|
||||||
|
const eventsRes = await app.request(`/web/sessions/${compatId}/events?uuid=user-1`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ type: "user", content: "hello" }),
|
||||||
|
});
|
||||||
|
expect(eventsRes.status).toBe(200);
|
||||||
|
|
||||||
|
const controlRes = await app.request(`/web/sessions/${compatId}/control?uuid=user-1`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ type: "permission_response", approved: true, request_id: "r1" }),
|
||||||
|
});
|
||||||
|
expect(controlRes.status).toBe(200);
|
||||||
|
|
||||||
|
const interruptRes = await app.request(`/web/sessions/${compatId}/interrupt?uuid=user-1`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
expect(interruptRes.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
test("POST /web/sessions/:id/events — 403 for non-owner", async () => {
|
test("POST /web/sessions/:id/events — 403 for non-owner", async () => {
|
||||||
const res = await app.request(`/web/sessions/${sessionId}/events?uuid=user-2`, {
|
const res = await app.request(`/web/sessions/${sessionId}/events?uuid=user-2`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -743,6 +963,33 @@ describe("Web Control Routes", () => {
|
|||||||
});
|
});
|
||||||
expect(res.status).toBe(403);
|
expect(res.status).toBe(403);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("POST /web/sessions/:id/events/control/interrupt — 409 for archived session", async () => {
|
||||||
|
await app.request(`/v1/sessions/${sessionId}/archive`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: AUTH_HEADERS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventsRes = await app.request(`/web/sessions/${sessionId}/events?uuid=user-1`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ type: "user", content: "hello" }),
|
||||||
|
});
|
||||||
|
expect(eventsRes.status).toBe(409);
|
||||||
|
|
||||||
|
const controlRes = await app.request(`/web/sessions/${sessionId}/control?uuid=user-1`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ type: "permission_response", approved: true, request_id: "r1" }),
|
||||||
|
});
|
||||||
|
expect(controlRes.status).toBe(409);
|
||||||
|
|
||||||
|
const interruptRes = await app.request(`/web/sessions/${sessionId}/interrupt?uuid=user-1`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
expect(interruptRes.status).toBe(409);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Web Environment Routes", () => {
|
describe("Web Environment Routes", () => {
|
||||||
@@ -822,6 +1069,81 @@ describe("V1 Session Ingress Routes (HTTP)", () => {
|
|||||||
});
|
});
|
||||||
expect(res.status).toBe(404);
|
expect(res.status).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("POST /v2/session_ingress/session/:sessionId/events — resolves compat code session IDs", async () => {
|
||||||
|
const sessRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
session: { id },
|
||||||
|
} = await sessRes.json();
|
||||||
|
const compatId = toWebSessionId(id);
|
||||||
|
|
||||||
|
const res = await app.request(`/v2/session_ingress/session/${compatId}/events`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ events: [{ type: "assistant", message: { role: "assistant", content: "compat ok" } }] }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
const events = getEventBus(id).getEventsSince(0);
|
||||||
|
expect(events).toHaveLength(1);
|
||||||
|
expect(events[0]?.type).toBe("assistant");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("GET /v2/session_ingress/ws/:sessionId — resolves compat code session IDs", async () => {
|
||||||
|
const sessRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
session: { id },
|
||||||
|
} = await sessRes.json();
|
||||||
|
const compatId = toWebSessionId(id);
|
||||||
|
|
||||||
|
publishSessionEvent(id, "user", { content: "compat ws replay" }, "outbound");
|
||||||
|
|
||||||
|
const server = Bun.serve({
|
||||||
|
port: 0,
|
||||||
|
fetch: app.fetch,
|
||||||
|
websocket: {
|
||||||
|
...sessionIngressWebsocket,
|
||||||
|
idleTimeout: 30,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = await new Promise<string>((resolve, reject) => {
|
||||||
|
const ws = new WebSocket(`ws://127.0.0.1:${server.port}/v2/session_ingress/ws/${compatId}?token=test-api-key`);
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
ws.close();
|
||||||
|
reject(new Error("Timed out waiting for compat WebSocket replay"));
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = typeof event.data === "string" ? event.data : String(event.data);
|
||||||
|
if (data.includes("\"type\":\"user\"")) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
ws.close();
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.onerror = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
reject(new Error("Compat WebSocket connection failed"));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(message).toContain("\"type\":\"user\"");
|
||||||
|
expect(message).toContain(`\"session_id\":\"${id}\"`);
|
||||||
|
expect(message).toContain("compat ws replay");
|
||||||
|
} finally {
|
||||||
|
await server.stop(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("V2 Worker Events Routes", () => {
|
describe("V2 Worker Events Routes", () => {
|
||||||
@@ -856,6 +1178,112 @@ describe("V2 Worker Events Routes", () => {
|
|||||||
expect(body.count).toBe(1);
|
expect(body.count).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("POST /v1/code/sessions/:id/worker/events — unwraps CCR batch payloads", async () => {
|
||||||
|
const sessRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const { session: { id } } = await sessRes.json();
|
||||||
|
|
||||||
|
const res = await app.request(`/v1/code/sessions/${id}/worker/events`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
worker_epoch: 1,
|
||||||
|
events: [{ payload: { type: "assistant", content: "response" } }],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.count).toBe(1);
|
||||||
|
|
||||||
|
const events = getEventBus(id).getEventsSince(0);
|
||||||
|
expect(events).toHaveLength(1);
|
||||||
|
expect(events[0]?.type).toBe("assistant");
|
||||||
|
expect((events[0]?.payload as { content?: string }).content).toBe("response");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("GET/PUT /v1/code/sessions/:id/worker — stores worker state", async () => {
|
||||||
|
const sessRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const { session: { id } } = await sessRes.json();
|
||||||
|
|
||||||
|
const putRes = await app.request(`/v1/code/sessions/${id}/worker`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
worker_epoch: 1,
|
||||||
|
worker_status: "running",
|
||||||
|
external_metadata: { permission_mode: "default" },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(putRes.status).toBe(200);
|
||||||
|
|
||||||
|
const getRes = await app.request(`/v1/code/sessions/${id}/worker`, {
|
||||||
|
headers: AUTH_HEADERS,
|
||||||
|
});
|
||||||
|
expect(getRes.status).toBe(200);
|
||||||
|
const body = await getRes.json();
|
||||||
|
expect(body.worker.worker_status).toBe("running");
|
||||||
|
expect(body.worker.external_metadata.permission_mode).toBe("default");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("POST /v1/code/sessions/:id/worker/heartbeat — updates heartbeat", async () => {
|
||||||
|
const sessRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const { session: { id } } = await sessRes.json();
|
||||||
|
|
||||||
|
const heartbeatRes = await app.request(`/v1/code/sessions/${id}/worker/heartbeat`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ worker_epoch: 1 }),
|
||||||
|
});
|
||||||
|
expect(heartbeatRes.status).toBe(200);
|
||||||
|
|
||||||
|
const getRes = await app.request(`/v1/code/sessions/${id}/worker`, {
|
||||||
|
headers: AUTH_HEADERS,
|
||||||
|
});
|
||||||
|
const body = await getRes.json();
|
||||||
|
expect(body.worker.last_heartbeat_at).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("GET /v1/code/sessions/:id/worker/events/stream — emits CCR client_event frames", async () => {
|
||||||
|
const sessRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const { session: { id } } = await sessRes.json();
|
||||||
|
|
||||||
|
const streamRes = await app.request(`/v1/code/sessions/${id}/worker/events/stream`, {
|
||||||
|
headers: AUTH_HEADERS,
|
||||||
|
});
|
||||||
|
expect(streamRes.status).toBe(200);
|
||||||
|
|
||||||
|
const reader = streamRes.body?.getReader();
|
||||||
|
expect(reader).toBeTruthy();
|
||||||
|
if (!reader) return;
|
||||||
|
|
||||||
|
const firstChunk = await reader.read();
|
||||||
|
const keepalive = new TextDecoder().decode(firstChunk.value!);
|
||||||
|
expect(keepalive).toContain(": keepalive");
|
||||||
|
|
||||||
|
publishSessionEvent(id, "user", { type: "user", content: "hello" }, "outbound");
|
||||||
|
|
||||||
|
const secondChunk = await reader.read();
|
||||||
|
const frame = new TextDecoder().decode(secondChunk.value!);
|
||||||
|
expect(frame).toContain("event: client_event");
|
||||||
|
expect(frame).toContain("\"payload\":{\"type\":\"user\",\"content\":\"hello\",\"message\":{\"content\":\"hello\"}}");
|
||||||
|
reader.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
test("PUT /v1/code/sessions/:id/worker/state — updates session status", async () => {
|
test("PUT /v1/code/sessions/:id/worker/state — updates session status", async () => {
|
||||||
const sessRes = await app.request("/v1/sessions", {
|
const sessRes = await app.request("/v1/sessions", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -903,4 +1331,20 @@ describe("V2 Worker Events Routes", () => {
|
|||||||
});
|
});
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("POST /v1/code/sessions/:id/worker/events/delivery — batch no-op", async () => {
|
||||||
|
const sessRes = await app.request("/v1/code/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
const { session: { id } } = await sessRes.json();
|
||||||
|
|
||||||
|
const res = await app.request(`/v1/code/sessions/${id}/worker/events/delivery`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ worker_epoch: 1, updates: [{ event_id: "evt123", status: "received" }] }),
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -345,6 +345,14 @@ describe("Transport Service", () => {
|
|||||||
expect(result.message).toEqual(msg);
|
expect(result.message).toEqual(msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("preserves uuid field", () => {
|
||||||
|
const result = normalizePayload("user", {
|
||||||
|
uuid: "msg_123",
|
||||||
|
content: "hi",
|
||||||
|
});
|
||||||
|
expect(result.uuid).toBe("msg_123");
|
||||||
|
});
|
||||||
|
|
||||||
test("uses name as tool_name fallback", () => {
|
test("uses name as tool_name fallback", () => {
|
||||||
const result = normalizePayload("tool", { name: "Read" });
|
const result = normalizePayload("tool", { name: "Read" });
|
||||||
expect(result.tool_name).toBe("Read");
|
expect(result.tool_name).toBe("Read");
|
||||||
|
|||||||
@@ -336,6 +336,26 @@ describe("ws-handler", () => {
|
|||||||
expect(lastMsg.message.content).toBe("hello world");
|
expect(lastMsg.message.content).toBe("hello world");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("preserves payload uuid for outbound user events", () => {
|
||||||
|
const bus = getEventBus("um2");
|
||||||
|
const ws = createMockWs();
|
||||||
|
handleWebSocketOpen(ws, "um2");
|
||||||
|
|
||||||
|
bus.publish({
|
||||||
|
id: "internal-event-id",
|
||||||
|
sessionId: "um2",
|
||||||
|
type: "user",
|
||||||
|
payload: { uuid: "web-message-uuid", content: "hello from web" },
|
||||||
|
direction: "outbound",
|
||||||
|
});
|
||||||
|
|
||||||
|
const sent = ws.getSentData();
|
||||||
|
const lastMsg = JSON.parse(sent[sent.length - 1]);
|
||||||
|
expect(lastMsg.type).toBe("user");
|
||||||
|
expect(lastMsg.uuid).toBe("web-message-uuid");
|
||||||
|
expect(lastMsg.message.content).toBe("hello from web");
|
||||||
|
});
|
||||||
|
|
||||||
test("converts generic event type", () => {
|
test("converts generic event type", () => {
|
||||||
const bus = getEventBus("gen1");
|
const bus = getEventBus("gen1");
|
||||||
const ws = createMockWs();
|
const ws = createMockWs();
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ app.post("/bridge", acceptCliHeaders, apiKeyAuth, async (c) => {
|
|||||||
|
|
||||||
/** DELETE /v1/environments/bridge/:id — Deregister */
|
/** DELETE /v1/environments/bridge/:id — Deregister */
|
||||||
app.delete("/bridge/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.delete("/bridge/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const envId = c.req.param("id");
|
const envId = c.req.param("id")!;
|
||||||
deregisterEnvironment(envId);
|
deregisterEnvironment(envId);
|
||||||
return c.json({ status: "ok" }, 200);
|
return c.json({ status: "ok" }, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** POST /v1/environments/:id/bridge/reconnect — Reconnect */
|
/** POST /v1/environments/:id/bridge/reconnect — Reconnect */
|
||||||
app.post("/:id/bridge/reconnect", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.post("/:id/bridge/reconnect", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const envId = c.req.param("id");
|
const envId = c.req.param("id")!;
|
||||||
reconnectEnvironment(envId);
|
reconnectEnvironment(envId);
|
||||||
const { reconnectWorkForEnvironment } = await import("../../services/work-dispatch");
|
const { reconnectWorkForEnvironment } = await import("../../services/work-dispatch");
|
||||||
await reconnectWorkForEnvironment(envId);
|
await reconnectWorkForEnvironment(envId);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const app = new Hono();
|
|||||||
|
|
||||||
/** GET /v1/environments/:id/work/poll — Long-poll for work */
|
/** GET /v1/environments/:id/work/poll — Long-poll for work */
|
||||||
app.get("/:id/work/poll", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.get("/:id/work/poll", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const envId = c.req.param("id");
|
const envId = c.req.param("id")!;
|
||||||
updatePollTime(envId);
|
updatePollTime(envId);
|
||||||
const result = await pollWork(envId);
|
const result = await pollWork(envId);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
@@ -19,21 +19,21 @@ app.get("/:id/work/poll", acceptCliHeaders, apiKeyAuth, async (c) => {
|
|||||||
|
|
||||||
/** POST /v1/environments/:id/work/:workId/ack — Acknowledge work */
|
/** POST /v1/environments/:id/work/:workId/ack — Acknowledge work */
|
||||||
app.post("/:id/work/:workId/ack", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.post("/:id/work/:workId/ack", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const workId = c.req.param("workId");
|
const workId = c.req.param("workId")!;
|
||||||
ackWork(workId);
|
ackWork(workId);
|
||||||
return c.json({ status: "ok" }, 200);
|
return c.json({ status: "ok" }, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** POST /v1/environments/:id/work/:workId/stop — Stop work */
|
/** POST /v1/environments/:id/work/:workId/stop — Stop work */
|
||||||
app.post("/:id/work/:workId/stop", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.post("/:id/work/:workId/stop", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const workId = c.req.param("workId");
|
const workId = c.req.param("workId")!;
|
||||||
stopWork(workId);
|
stopWork(workId);
|
||||||
return c.json({ status: "ok" }, 200);
|
return c.json({ status: "ok" }, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** POST /v1/environments/:id/work/:workId/heartbeat — Heartbeat */
|
/** POST /v1/environments/:id/work/:workId/heartbeat — Heartbeat */
|
||||||
app.post("/:id/work/:workId/heartbeat", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.post("/:id/work/:workId/heartbeat", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const workId = c.req.param("workId");
|
const workId = c.req.param("workId")!;
|
||||||
const result = heartbeatWork(workId);
|
const result = heartbeatWork(workId);
|
||||||
return c.json(result, 200);
|
return c.json(result, 200);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
handleWebSocketClose,
|
handleWebSocketClose,
|
||||||
ingestBridgeMessage,
|
ingestBridgeMessage,
|
||||||
} from "../../transport/ws-handler";
|
} from "../../transport/ws-handler";
|
||||||
import { getSession } from "../../services/session";
|
import { getSession, resolveExistingSessionId } from "../../services/session";
|
||||||
|
|
||||||
const { upgradeWebSocket, websocket } = createBunWebSocket();
|
const { upgradeWebSocket, websocket } = createBunWebSocket();
|
||||||
|
|
||||||
@@ -43,7 +43,8 @@ function authenticateRequest(c: any, label: string, expectedSessionId?: string):
|
|||||||
|
|
||||||
/** POST /v2/session_ingress/session/:sessionId/events — HTTP POST (HybridTransport writes) */
|
/** POST /v2/session_ingress/session/:sessionId/events — HTTP POST (HybridTransport writes) */
|
||||||
app.post("/session/:sessionId/events", async (c) => {
|
app.post("/session/:sessionId/events", async (c) => {
|
||||||
const sessionId = c.req.param("sessionId")!;
|
const requestedSessionId = c.req.param("sessionId")!;
|
||||||
|
const sessionId = resolveExistingSessionId(requestedSessionId) ?? requestedSessionId;
|
||||||
|
|
||||||
if (!authenticateRequest(c, `POST session/${sessionId}`, sessionId)) {
|
if (!authenticateRequest(c, `POST session/${sessionId}`, sessionId)) {
|
||||||
return c.json({ error: { type: "unauthorized", message: "Invalid auth" } }, 401);
|
return c.json({ error: { type: "unauthorized", message: "Invalid auth" } }, 401);
|
||||||
@@ -71,7 +72,8 @@ app.post("/session/:sessionId/events", async (c) => {
|
|||||||
app.get(
|
app.get(
|
||||||
"/ws/:sessionId",
|
"/ws/:sessionId",
|
||||||
upgradeWebSocket(async (c) => {
|
upgradeWebSocket(async (c) => {
|
||||||
const sessionId = c.req.param("sessionId")!;
|
const requestedSessionId = c.req.param("sessionId")!;
|
||||||
|
const sessionId = resolveExistingSessionId(requestedSessionId) ?? requestedSessionId;
|
||||||
|
|
||||||
if (!authenticateRequest(c, `WS ${sessionId}`, sessionId)) {
|
if (!authenticateRequest(c, `WS ${sessionId}`, sessionId)) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
getSession,
|
getSession,
|
||||||
updateSessionTitle,
|
updateSessionTitle,
|
||||||
archiveSession,
|
archiveSession,
|
||||||
|
resolveExistingSessionId,
|
||||||
} from "../../services/session";
|
} from "../../services/session";
|
||||||
import { createWorkItem } from "../../services/work-dispatch";
|
import { createWorkItem } from "../../services/work-dispatch";
|
||||||
import { apiKeyAuth, acceptCliHeaders } from "../../auth/middleware";
|
import { apiKeyAuth, acceptCliHeaders } from "../../auth/middleware";
|
||||||
@@ -38,7 +39,8 @@ app.post("/", acceptCliHeaders, apiKeyAuth, async (c) => {
|
|||||||
|
|
||||||
/** GET /v1/sessions/:id — Get session */
|
/** GET /v1/sessions/:id — Get session */
|
||||||
app.get("/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.get("/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const session = getSession(c.req.param("id"));
|
const sessionId = resolveExistingSessionId(c.req.param("id")!) ?? c.req.param("id")!;
|
||||||
|
const session = getSession(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
}
|
}
|
||||||
@@ -47,27 +49,43 @@ app.get("/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
|
|||||||
|
|
||||||
/** PATCH /v1/sessions/:id — Update session title */
|
/** PATCH /v1/sessions/:id — Update session title */
|
||||||
app.patch("/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.patch("/:id", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
|
const sessionId = resolveExistingSessionId(c.req.param("id")!) ?? c.req.param("id")!;
|
||||||
|
const existing = getSession(sessionId);
|
||||||
|
if (!existing) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
if (body.title) {
|
if (body.title) {
|
||||||
updateSessionTitle(c.req.param("id"), body.title);
|
updateSessionTitle(sessionId, body.title);
|
||||||
}
|
}
|
||||||
const session = getSession(c.req.param("id"));
|
const session = getSession(sessionId);
|
||||||
return c.json(session, 200);
|
return c.json(session, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** POST /v1/sessions/:id/archive — Archive session */
|
/** POST /v1/sessions/:id/archive — Archive session */
|
||||||
app.post("/:id/archive", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.post("/:id/archive", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
|
const sessionId = resolveExistingSessionId(c.req.param("id")!) ?? c.req.param("id")!;
|
||||||
|
const session = getSession(sessionId);
|
||||||
|
if (!session) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
archiveSession(c.req.param("id"));
|
archiveSession(sessionId);
|
||||||
} catch {
|
} catch {
|
||||||
return c.json({ status: "ok" }, 409);
|
return c.json({ status: "ok" }, 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json({ status: "ok" }, 200);
|
return c.json({ status: "ok" }, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** POST /v1/sessions/:id/events — Send event to session */
|
/** POST /v1/sessions/:id/events — Send event to session */
|
||||||
app.post("/:id/events", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.post("/:id/events", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const sessionId = c.req.param("id");
|
const sessionId = resolveExistingSessionId(c.req.param("id")!) ?? c.req.param("id")!;
|
||||||
|
const session = getSession(sessionId);
|
||||||
|
if (!session) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
|
|
||||||
const events = body.events
|
const events = body.events
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ app.post("/", acceptCliHeaders, apiKeyAuth, async (c) => {
|
|||||||
|
|
||||||
/** POST /v1/code/sessions/:id/bridge — Get connection info + worker JWT */
|
/** POST /v1/code/sessions/:id/bridge — Get connection info + worker JWT */
|
||||||
app.post("/:id/bridge", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.post("/:id/bridge", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const sessionId = c.req.param("id");
|
const sessionId = c.req.param("id")!;
|
||||||
const session = getSession(sessionId);
|
const session = getSession(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { sessionIngressAuth, acceptCliHeaders } from "../../auth/middleware";
|
import { sessionIngressAuth, acceptCliHeaders } from "../../auth/middleware";
|
||||||
import { createSSEStream } from "../../transport/sse-writer";
|
import { createWorkerEventStream } from "../../transport/sse-writer";
|
||||||
import { getSession } from "../../services/session";
|
import { getSession } from "../../services/session";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
/** SSE /v1/code/sessions/:id/worker/events/stream — SSE event stream */
|
/** SSE /v1/code/sessions/:id/worker/events/stream — SSE event stream */
|
||||||
app.get("/:id/worker/events/stream", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
app.get("/:id/worker/events/stream", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||||
const sessionId = c.req.param("id");
|
const sessionId = c.req.param("id")!;
|
||||||
const session = getSession(sessionId);
|
const session = getSession(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
@@ -18,7 +18,7 @@ app.get("/:id/worker/events/stream", acceptCliHeaders, sessionIngressAuth, async
|
|||||||
const fromSeq = c.req.query("from_sequence_num");
|
const fromSeq = c.req.query("from_sequence_num");
|
||||||
const fromSeqNum = fromSeq ? parseInt(fromSeq) : lastEventId ? parseInt(lastEventId) : 0;
|
const fromSeqNum = fromSeq ? parseInt(fromSeq) : lastEventId ? parseInt(lastEventId) : 0;
|
||||||
|
|
||||||
return createSSEStream(c, sessionId, fromSeqNum);
|
return createWorkerEventStream(c, sessionId, fromSeqNum);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -1,32 +1,66 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { sessionIngressAuth, acceptCliHeaders } from "../../auth/middleware";
|
import { sessionIngressAuth, acceptCliHeaders } from "../../auth/middleware";
|
||||||
import { publishSessionEvent } from "../../services/transport";
|
import { publishSessionEvent } from "../../services/transport";
|
||||||
import { getSession, updateSessionStatus } from "../../services/session";
|
import { getSession, touchSession, updateSessionStatus } from "../../services/session";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
|
function extractWorkerEvents(body: unknown): Array<Record<string, unknown>> {
|
||||||
|
if (!body || typeof body !== "object") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = body as Record<string, unknown>;
|
||||||
|
const rawEvents = Array.isArray(payload.events)
|
||||||
|
? payload.events
|
||||||
|
: Array.isArray(body)
|
||||||
|
? body
|
||||||
|
: [body];
|
||||||
|
|
||||||
|
return rawEvents
|
||||||
|
.filter((evt): evt is Record<string, unknown> => !!evt && typeof evt === "object")
|
||||||
|
.map((evt) => {
|
||||||
|
const wrappedPayload = evt.payload;
|
||||||
|
if (wrappedPayload && typeof wrappedPayload === "object" && !Array.isArray(wrappedPayload)) {
|
||||||
|
return wrappedPayload as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
return evt;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** POST /v1/code/sessions/:id/worker/events — Write events */
|
/** POST /v1/code/sessions/:id/worker/events — Write events */
|
||||||
app.post("/:id/worker/events", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
app.post("/:id/worker/events", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||||
const sessionId = c.req.param("id");
|
const sessionId = c.req.param("id")!;
|
||||||
|
if (!getSession(sessionId)) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
|
|
||||||
const events = Array.isArray(body) ? body : [body];
|
const events = extractWorkerEvents(body);
|
||||||
const published = [];
|
const published = [];
|
||||||
for (const evt of events) {
|
for (const evt of events) {
|
||||||
const result = publishSessionEvent(sessionId, evt.type || "message", evt, "inbound");
|
const eventType = typeof evt.type === "string" ? evt.type : "message";
|
||||||
|
const result = publishSessionEvent(sessionId, eventType, evt, "inbound");
|
||||||
published.push(result);
|
published.push(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
touchSession(sessionId);
|
||||||
|
|
||||||
return c.json({ status: "ok", count: published.length }, 200);
|
return c.json({ status: "ok", count: published.length }, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** PUT /v1/code/sessions/:id/worker/state — Report worker state */
|
/** PUT /v1/code/sessions/:id/worker/state — Report worker state */
|
||||||
app.put("/:id/worker/state", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
app.put("/:id/worker/state", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||||
const sessionId = c.req.param("id");
|
const sessionId = c.req.param("id")!;
|
||||||
|
if (!getSession(sessionId)) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
|
|
||||||
if (body.status) {
|
if (body.status) {
|
||||||
updateSessionStatus(sessionId, body.status);
|
updateSessionStatus(sessionId, body.status);
|
||||||
|
} else {
|
||||||
|
touchSession(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json({ status: "ok" }, 200);
|
return c.json({ status: "ok" }, 200);
|
||||||
@@ -34,12 +68,29 @@ app.put("/:id/worker/state", acceptCliHeaders, sessionIngressAuth, async (c) =>
|
|||||||
|
|
||||||
/** PUT /v1/code/sessions/:id/worker/external_metadata — Report worker metadata (no-op) */
|
/** PUT /v1/code/sessions/:id/worker/external_metadata — Report worker metadata (no-op) */
|
||||||
app.put("/:id/worker/external_metadata", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
app.put("/:id/worker/external_metadata", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||||
|
const sessionId = c.req.param("id")!;
|
||||||
|
if (!getSession(sessionId)) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
// TUI's CCRClient calls this for metadata reporting. Accept and discard.
|
// TUI's CCRClient calls this for metadata reporting. Accept and discard.
|
||||||
return c.json({ status: "ok" }, 200);
|
return c.json({ status: "ok" }, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** POST /v1/code/sessions/:id/worker/events/delivery — Batch delivery tracking (no-op) */
|
||||||
|
app.post("/:id/worker/events/delivery", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||||
|
const sessionId = c.req.param("id")!;
|
||||||
|
if (!getSession(sessionId)) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
|
return c.json({ status: "ok" }, 200);
|
||||||
|
});
|
||||||
|
|
||||||
/** POST /v1/code/sessions/:id/worker/events/:eventId/delivery — Delivery tracking (no-op) */
|
/** POST /v1/code/sessions/:id/worker/events/:eventId/delivery — Delivery tracking (no-op) */
|
||||||
app.post("/:id/worker/events/:eventId/delivery", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
app.post("/:id/worker/events/:eventId/delivery", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||||
|
const sessionId = c.req.param("id")!;
|
||||||
|
if (!getSession(sessionId)) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
// TUI's CCRClient reports event delivery status (received/processing/processed).
|
// TUI's CCRClient reports event delivery status (received/processing/processed).
|
||||||
// Accept and discard — event bus doesn't track per-event delivery.
|
// Accept and discard — event bus doesn't track per-event delivery.
|
||||||
return c.json({ status: "ok" }, 200);
|
return c.json({ status: "ok" }, 200);
|
||||||
|
|||||||
@@ -1,12 +1,78 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { getSession, incrementEpoch } from "../../services/session";
|
import { getSession, incrementEpoch, touchSession, updateSessionStatus } from "../../services/session";
|
||||||
import { apiKeyAuth, acceptCliHeaders } from "../../auth/middleware";
|
import { apiKeyAuth, acceptCliHeaders, sessionIngressAuth } from "../../auth/middleware";
|
||||||
|
import { storeGetSessionWorker, storeUpsertSessionWorker } from "../../store";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
|
/** GET /v1/code/sessions/:id/worker — Read worker state */
|
||||||
|
app.get("/:id/worker", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||||
|
const sessionId = c.req.param("id")!;
|
||||||
|
const session = getSession(sessionId);
|
||||||
|
if (!session) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const worker = storeGetSessionWorker(sessionId);
|
||||||
|
return c.json({
|
||||||
|
worker: {
|
||||||
|
worker_status: worker?.workerStatus ?? session.status,
|
||||||
|
external_metadata: worker?.externalMetadata ?? null,
|
||||||
|
requires_action_details: worker?.requiresActionDetails ?? null,
|
||||||
|
last_heartbeat_at: worker?.lastHeartbeatAt?.toISOString() ?? null,
|
||||||
|
},
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** PUT /v1/code/sessions/:id/worker — Update worker state */
|
||||||
|
app.put("/:id/worker", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||||
|
const sessionId = c.req.param("id")!;
|
||||||
|
const session = getSession(sessionId);
|
||||||
|
if (!session) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await c.req.json();
|
||||||
|
if (body.worker_status) {
|
||||||
|
updateSessionStatus(sessionId, body.worker_status);
|
||||||
|
} else {
|
||||||
|
touchSession(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const worker = storeUpsertSessionWorker(sessionId, {
|
||||||
|
workerStatus: body.worker_status,
|
||||||
|
externalMetadata: body.external_metadata,
|
||||||
|
requiresActionDetails: body.requires_action_details,
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
status: "ok",
|
||||||
|
worker: {
|
||||||
|
worker_status: worker.workerStatus ?? session.status,
|
||||||
|
external_metadata: worker.externalMetadata,
|
||||||
|
requires_action_details: worker.requiresActionDetails,
|
||||||
|
last_heartbeat_at: worker.lastHeartbeatAt?.toISOString() ?? null,
|
||||||
|
},
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** POST /v1/code/sessions/:id/worker/heartbeat — Keep worker alive */
|
||||||
|
app.post("/:id/worker/heartbeat", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||||
|
const sessionId = c.req.param("id")!;
|
||||||
|
const session = getSession(sessionId);
|
||||||
|
if (!session) {
|
||||||
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
storeUpsertSessionWorker(sessionId, { lastHeartbeatAt: now });
|
||||||
|
touchSession(sessionId);
|
||||||
|
return c.json({ status: "ok", last_heartbeat_at: now.toISOString() }, 200);
|
||||||
|
});
|
||||||
|
|
||||||
/** POST /v1/code/sessions/:id/worker/register — Register worker */
|
/** POST /v1/code/sessions/:id/worker/register — Register worker */
|
||||||
app.post("/:id/worker/register", acceptCliHeaders, apiKeyAuth, async (c) => {
|
app.post("/:id/worker/register", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||||
const sessionId = c.req.param("id");
|
const sessionId = c.req.param("id")!;
|
||||||
const session = getSession(sessionId);
|
const session = getSession(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { storeGetSession, storeBindSession } from "../../store";
|
import { storeBindSession } from "../../store";
|
||||||
|
import { resolveExistingWebSessionId, toWebSessionId } from "../../services/session";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
@@ -14,13 +15,13 @@ app.post("/bind", async (c) => {
|
|||||||
return c.json({ error: "sessionId and uuid are required" }, 400);
|
return c.json({ error: "sessionId and uuid are required" }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = storeGetSession(sessionId);
|
const resolvedSessionId = resolveExistingWebSessionId(sessionId);
|
||||||
if (!session) {
|
if (!resolvedSessionId) {
|
||||||
return c.json({ error: "Session not found" }, 404);
|
return c.json({ error: "Session not found" }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
storeBindSession(sessionId, uuid);
|
storeBindSession(resolvedSessionId, uuid);
|
||||||
return c.json({ ok: true, sessionId });
|
return c.json({ ok: true, sessionId: toWebSessionId(resolvedSessionId) });
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -1,31 +1,46 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { uuidAuth } from "../../auth/middleware";
|
import { uuidAuth } from "../../auth/middleware";
|
||||||
import { getSession, updateSessionStatus } from "../../services/session";
|
import { getSession, isSessionClosedStatus, resolveOwnedWebSessionId, updateSessionStatus } from "../../services/session";
|
||||||
import { publishSessionEvent } from "../../services/transport";
|
import { publishSessionEvent } from "../../services/transport";
|
||||||
import { getEventBus } from "../../transport/event-bus";
|
import { getEventBus } from "../../transport/event-bus";
|
||||||
import { storeIsSessionOwner } from "../../store";
|
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
|
|
||||||
function checkOwnership(c: { get: (key: string) => string | undefined }, sessionId: string) {
|
type OwnershipCheckResult =
|
||||||
const uuid = c.get("uuid");
|
| { error: true }
|
||||||
if (!storeIsSessionOwner(sessionId, uuid)) {
|
| { error: true; reason: string }
|
||||||
return { error: true, session: null };
|
| { error: false; session: NonNullable<ReturnType<typeof getSession>>; sessionId: string };
|
||||||
|
|
||||||
|
function checkOwnership(c: { get: (key: string) => string | undefined }, sessionId: string): OwnershipCheckResult {
|
||||||
|
const uuid = c.get("uuid")!;
|
||||||
|
const resolvedSessionId = resolveOwnedWebSessionId(sessionId, uuid);
|
||||||
|
if (!resolvedSessionId) {
|
||||||
|
return { error: true };
|
||||||
}
|
}
|
||||||
const session = getSession(sessionId);
|
const session = getSession(resolvedSessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return { error: true, session: null };
|
return { error: true };
|
||||||
}
|
}
|
||||||
return { error: false, session };
|
if (isSessionClosedStatus(session.status)) {
|
||||||
|
return { error: true, reason: `Session is ${session.status}` };
|
||||||
|
}
|
||||||
|
return { error: false, session, sessionId: resolvedSessionId };
|
||||||
|
}
|
||||||
|
|
||||||
|
function closedSessionResponse(message: string) {
|
||||||
|
return { error: { type: "session_closed", message } };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** POST /web/sessions/:id/events — Send user message to session */
|
/** POST /web/sessions/:id/events — Send user message to session */
|
||||||
app.post("/sessions/:id/events", uuidAuth, async (c) => {
|
app.post("/sessions/:id/events", uuidAuth, async (c) => {
|
||||||
const sessionId = c.req.param("id")!;
|
const requestedSessionId = c.req.param("id")!;
|
||||||
const { error } = checkOwnership(c, sessionId);
|
const ownership = checkOwnership(c, requestedSessionId);
|
||||||
if (error) {
|
if (ownership.error) {
|
||||||
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
|
const message = "reason" in ownership ? ownership.reason : "Not your session";
|
||||||
|
const status = "reason" in ownership ? 409 : 403;
|
||||||
|
return c.json("reason" in ownership ? closedSessionResponse(message) : { error: { type: "forbidden", message } }, status);
|
||||||
}
|
}
|
||||||
|
const { sessionId } = ownership;
|
||||||
|
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
const eventType = body.type || "user";
|
const eventType = body.type || "user";
|
||||||
@@ -37,11 +52,14 @@ app.post("/sessions/:id/events", uuidAuth, async (c) => {
|
|||||||
|
|
||||||
/** POST /web/sessions/:id/control — Send control request (permission approval etc) */
|
/** POST /web/sessions/:id/control — Send control request (permission approval etc) */
|
||||||
app.post("/sessions/:id/control", uuidAuth, async (c) => {
|
app.post("/sessions/:id/control", uuidAuth, async (c) => {
|
||||||
const sessionId = c.req.param("id")!;
|
const requestedSessionId = c.req.param("id")!;
|
||||||
const { error } = checkOwnership(c, sessionId);
|
const ownership = checkOwnership(c, requestedSessionId);
|
||||||
if (error) {
|
if (ownership.error) {
|
||||||
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
|
const message = "reason" in ownership ? ownership.reason : "Not your session";
|
||||||
|
const status = "reason" in ownership ? 409 : 403;
|
||||||
|
return c.json("reason" in ownership ? closedSessionResponse(message) : { error: { type: "forbidden", message } }, status);
|
||||||
}
|
}
|
||||||
|
const { sessionId } = ownership;
|
||||||
|
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
const event = publishSessionEvent(sessionId, body.type || "control_request", body, "outbound");
|
const event = publishSessionEvent(sessionId, body.type || "control_request", body, "outbound");
|
||||||
@@ -50,11 +68,14 @@ app.post("/sessions/:id/control", uuidAuth, async (c) => {
|
|||||||
|
|
||||||
/** POST /web/sessions/:id/interrupt — Interrupt session */
|
/** POST /web/sessions/:id/interrupt — Interrupt session */
|
||||||
app.post("/sessions/:id/interrupt", uuidAuth, async (c) => {
|
app.post("/sessions/:id/interrupt", uuidAuth, async (c) => {
|
||||||
const sessionId = c.req.param("id")!;
|
const requestedSessionId = c.req.param("id")!;
|
||||||
const { error } = checkOwnership(c, sessionId);
|
const ownership = checkOwnership(c, requestedSessionId);
|
||||||
if (error) {
|
if (ownership.error) {
|
||||||
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
|
const message = "reason" in ownership ? ownership.reason : "Not your session";
|
||||||
|
const status = "reason" in ownership ? 409 : 403;
|
||||||
|
return c.json("reason" in ownership ? closedSessionResponse(message) : { error: { type: "forbidden", message } }, status);
|
||||||
}
|
}
|
||||||
|
const { sessionId } = ownership;
|
||||||
|
|
||||||
publishSessionEvent(sessionId, "interrupt", { action: "interrupt" }, "outbound");
|
publishSessionEvent(sessionId, "interrupt", { action: "interrupt" }, "outbound");
|
||||||
updateSessionStatus(sessionId, "idle");
|
updateSessionStatus(sessionId, "idle");
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { uuidAuth } from "../../auth/middleware";
|
import { uuidAuth } from "../../auth/middleware";
|
||||||
import { getSession, createSession } from "../../services/session";
|
import {
|
||||||
import { storeListSessionsByOwnerUuid, storeIsSessionOwner, storeBindSession } from "../../store";
|
createSession,
|
||||||
|
getSession,
|
||||||
|
isSessionClosedStatus,
|
||||||
|
listWebSessionSummariesByOwnerUuid,
|
||||||
|
listWebSessionsByOwnerUuid,
|
||||||
|
resolveOwnedWebSessionId,
|
||||||
|
toWebSessionResponse,
|
||||||
|
} from "../../services/session";
|
||||||
|
import { storeBindSession } from "../../store";
|
||||||
import { createWorkItem } from "../../services/work-dispatch";
|
import { createWorkItem } from "../../services/work-dispatch";
|
||||||
import { listSessionSummariesByOwnerUuid } from "../../services/session";
|
|
||||||
import { createSSEStream } from "../../transport/sse-writer";
|
import { createSSEStream } from "../../transport/sse-writer";
|
||||||
import { getEventBus } from "../../transport/event-bus";
|
import { getEventBus } from "../../transport/event-bus";
|
||||||
|
|
||||||
@@ -11,7 +18,7 @@ const app = new Hono();
|
|||||||
|
|
||||||
/** POST /web/sessions — Create a session from web UI */
|
/** POST /web/sessions — Create a session from web UI */
|
||||||
app.post("/sessions", uuidAuth, async (c) => {
|
app.post("/sessions", uuidAuth, async (c) => {
|
||||||
const uuid = c.get("uuid");
|
const uuid = c.get("uuid")!;
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
const session = createSession({
|
const session = createSession({
|
||||||
environment_id: body.environment_id || null,
|
environment_id: body.environment_id || null,
|
||||||
@@ -37,37 +44,37 @@ app.post("/sessions", uuidAuth, async (c) => {
|
|||||||
|
|
||||||
/** GET /web/sessions — List sessions owned by the requesting UUID */
|
/** GET /web/sessions — List sessions owned by the requesting UUID */
|
||||||
app.get("/sessions", uuidAuth, async (c) => {
|
app.get("/sessions", uuidAuth, async (c) => {
|
||||||
const uuid = c.get("uuid");
|
const uuid = c.get("uuid")!;
|
||||||
const sessions = storeListSessionsByOwnerUuid(uuid);
|
const sessions = listWebSessionsByOwnerUuid(uuid);
|
||||||
return c.json(sessions, 200);
|
return c.json(sessions, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** GET /web/sessions/all — List sessions owned by the requesting UUID (unowned sessions excluded) */
|
/** GET /web/sessions/all — List sessions owned by the requesting UUID (unowned sessions excluded) */
|
||||||
app.get("/sessions/all", uuidAuth, async (c) => {
|
app.get("/sessions/all", uuidAuth, async (c) => {
|
||||||
const uuid = c.get("uuid");
|
const uuid = c.get("uuid")!;
|
||||||
const sessions = listSessionSummariesByOwnerUuid(uuid);
|
const sessions = listWebSessionSummariesByOwnerUuid(uuid);
|
||||||
return c.json(sessions, 200);
|
return c.json(sessions, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** GET /web/sessions/:id — Session detail */
|
/** GET /web/sessions/:id — Session detail */
|
||||||
app.get("/sessions/:id", uuidAuth, async (c) => {
|
app.get("/sessions/:id", uuidAuth, async (c) => {
|
||||||
const uuid = c.get("uuid");
|
const uuid = c.get("uuid")!;
|
||||||
const sessionId = c.req.param("id")!;
|
const sessionId = resolveOwnedWebSessionId(c.req.param("id")!, uuid);
|
||||||
if (!storeIsSessionOwner(sessionId, uuid)) {
|
if (!sessionId) {
|
||||||
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
|
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
|
||||||
}
|
}
|
||||||
const session = getSession(sessionId);
|
const session = getSession(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
}
|
}
|
||||||
return c.json(session, 200);
|
return c.json(toWebSessionResponse(session), 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** GET /web/sessions/:id/history — Historical events for session */
|
/** GET /web/sessions/:id/history — Historical events for session */
|
||||||
app.get("/sessions/:id/history", uuidAuth, async (c) => {
|
app.get("/sessions/:id/history", uuidAuth, async (c) => {
|
||||||
const uuid = c.get("uuid");
|
const uuid = c.get("uuid")!;
|
||||||
const sessionId = c.req.param("id")!;
|
const sessionId = resolveOwnedWebSessionId(c.req.param("id")!, uuid);
|
||||||
if (!storeIsSessionOwner(sessionId, uuid)) {
|
if (!sessionId) {
|
||||||
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
|
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
|
||||||
}
|
}
|
||||||
const session = getSession(sessionId);
|
const session = getSession(sessionId);
|
||||||
@@ -82,15 +89,18 @@ app.get("/sessions/:id/history", uuidAuth, async (c) => {
|
|||||||
|
|
||||||
/** SSE /web/sessions/:id/events — Real-time event stream */
|
/** SSE /web/sessions/:id/events — Real-time event stream */
|
||||||
app.get("/sessions/:id/events", uuidAuth, async (c) => {
|
app.get("/sessions/:id/events", uuidAuth, async (c) => {
|
||||||
const uuid = c.get("uuid");
|
const uuid = c.get("uuid")!;
|
||||||
const sessionId = c.req.param("id")!;
|
const sessionId = resolveOwnedWebSessionId(c.req.param("id")!, uuid);
|
||||||
if (!storeIsSessionOwner(sessionId, uuid)) {
|
if (!sessionId) {
|
||||||
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
|
return c.json({ error: { type: "forbidden", message: "Not your session" } }, 403);
|
||||||
}
|
}
|
||||||
const session = getSession(sessionId);
|
const session = getSession(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||||
}
|
}
|
||||||
|
if (isSessionClosedStatus(session.status)) {
|
||||||
|
return c.json({ error: { type: "session_closed", message: `Session is ${session.status}` } }, 409);
|
||||||
|
}
|
||||||
|
|
||||||
const lastEventId = c.req.header("Last-Event-ID");
|
const lastEventId = c.req.header("Last-Event-ID");
|
||||||
const fromSeqNum = lastEventId ? parseInt(lastEventId) : 0;
|
const fromSeqNum = lastEventId ? parseInt(lastEventId) : 0;
|
||||||
|
|||||||
@@ -1,32 +1,35 @@
|
|||||||
import { storeListActiveEnvironments, storeUpdateEnvironment } from "../store";
|
import { storeListActiveEnvironments, storeUpdateEnvironment } from "../store";
|
||||||
import { storeListSessions, storeUpdateSession } from "../store";
|
import { storeListSessions } from "../store";
|
||||||
import { config } from "../config";
|
import { config } from "../config";
|
||||||
|
import { updateSessionStatus } from "./session";
|
||||||
|
|
||||||
export function startDisconnectMonitor() {
|
export function runDisconnectMonitorSweep(now = Date.now()) {
|
||||||
const timeoutMs = config.disconnectTimeout * 1000;
|
const timeoutMs = config.disconnectTimeout * 1000;
|
||||||
|
|
||||||
|
// Check environment heartbeat timeout
|
||||||
|
const envs = storeListActiveEnvironments();
|
||||||
|
for (const env of envs) {
|
||||||
|
if (env.lastPollAt && now - env.lastPollAt.getTime() > timeoutMs) {
|
||||||
|
console.log(`[RCS] Environment ${env.id} timed out (no poll for ${Math.round((now - env.lastPollAt.getTime()) / 1000)}s)`);
|
||||||
|
storeUpdateEnvironment(env.id, { status: "disconnected" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check session timeout (2x disconnect timeout with no update)
|
||||||
|
const sessions = storeListSessions();
|
||||||
|
for (const session of sessions) {
|
||||||
|
if (session.status === "running" || session.status === "idle") {
|
||||||
|
const elapsed = now - session.updatedAt.getTime();
|
||||||
|
if (elapsed > timeoutMs * 2) {
|
||||||
|
console.log(`[RCS] Session ${session.id} marked inactive (no update for ${Math.round(elapsed / 1000)}s)`);
|
||||||
|
updateSessionStatus(session.id, "inactive");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startDisconnectMonitor() {
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const now = Date.now();
|
runDisconnectMonitorSweep();
|
||||||
|
|
||||||
// Check environment heartbeat timeout
|
|
||||||
const envs = storeListActiveEnvironments();
|
|
||||||
for (const env of envs) {
|
|
||||||
if (env.lastPollAt && now - env.lastPollAt.getTime() > timeoutMs) {
|
|
||||||
console.log(`[RCS] Environment ${env.id} timed out (no poll for ${Math.round((now - env.lastPollAt.getTime()) / 1000)}s)`);
|
|
||||||
storeUpdateEnvironment(env.id, { status: "disconnected" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check session timeout (2x disconnect timeout with no update)
|
|
||||||
const sessions = storeListSessions();
|
|
||||||
for (const session of sessions) {
|
|
||||||
if (session.status === "running" || session.status === "idle") {
|
|
||||||
const elapsed = now - session.updatedAt.getTime();
|
|
||||||
if (elapsed > timeoutMs * 2) {
|
|
||||||
console.log(`[RCS] Session ${session.id} marked inactive (no update for ${Math.round(elapsed / 1000)}s)`);
|
|
||||||
storeUpdateSession(session.id, { status: "inactive" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 60_000); // Check every minute
|
}, 60_000); // Check every minute
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
import {
|
import {
|
||||||
storeCreateSession,
|
storeCreateSession,
|
||||||
storeGetSession,
|
storeGetSession,
|
||||||
|
storeIsSessionOwner,
|
||||||
storeUpdateSession,
|
storeUpdateSession,
|
||||||
storeListSessions,
|
storeListSessions,
|
||||||
storeListSessionsByUsername,
|
storeListSessionsByUsername,
|
||||||
storeListSessionsByEnvironment,
|
storeListSessionsByEnvironment,
|
||||||
storeListSessionsByOwnerUuid,
|
storeListSessionsByOwnerUuid,
|
||||||
} from "../store";
|
} from "../store";
|
||||||
import { removeEventBus } from "../transport/event-bus";
|
import { getAllEventBuses, removeEventBus } from "../transport/event-bus";
|
||||||
import type { CreateSessionRequest, CreateCodeSessionRequest, SessionResponse, SessionSummaryResponse } from "../types/api";
|
import type { CreateSessionRequest, CreateCodeSessionRequest, SessionResponse, SessionSummaryResponse } from "../types/api";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
|
const CODE_SESSION_PREFIX = "cse_";
|
||||||
|
const WEB_SESSION_PREFIX = "session_";
|
||||||
|
const CLOSED_SESSION_STATUSES = new Set(["archived", "inactive"]);
|
||||||
|
|
||||||
function toResponse(row: { id: string; environmentId: string | null; title: string | null; status: string; source: string; permissionMode: string | null; workerEpoch: number; username: string | null; createdAt: Date; updatedAt: Date }): SessionResponse {
|
function toResponse(row: { id: string; environmentId: string | null; title: string | null; status: string; source: string; permissionMode: string | null; workerEpoch: number; username: string | null; createdAt: Date; updatedAt: Date }): SessionResponse {
|
||||||
return {
|
return {
|
||||||
@@ -25,6 +31,24 @@ function toResponse(row: { id: string; environmentId: string | null; title: stri
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toWebSessionId(sessionId: string): string {
|
||||||
|
if (!sessionId.startsWith(CODE_SESSION_PREFIX)) return sessionId;
|
||||||
|
return `${WEB_SESSION_PREFIX}${sessionId.slice(CODE_SESSION_PREFIX.length)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toCompatibleCodeSessionId(sessionId: string): string | null {
|
||||||
|
if (!sessionId.startsWith(WEB_SESSION_PREFIX)) return null;
|
||||||
|
return `${CODE_SESSION_PREFIX}${sessionId.slice(WEB_SESSION_PREFIX.length)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toWebSessionResponse(session: SessionResponse): SessionResponse {
|
||||||
|
return { ...session, id: toWebSessionId(session.id) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function toWebSessionSummaryResponse(session: SessionSummaryResponse): SessionSummaryResponse {
|
||||||
|
return { ...session, id: toWebSessionId(session.id) };
|
||||||
|
}
|
||||||
|
|
||||||
export function createSession(req: CreateSessionRequest & { username?: string }): SessionResponse {
|
export function createSession(req: CreateSessionRequest & { username?: string }): SessionResponse {
|
||||||
const record = storeCreateSession({
|
const record = storeCreateSession({
|
||||||
environmentId: req.environment_id,
|
environmentId: req.environment_id,
|
||||||
@@ -51,16 +75,78 @@ export function getSession(sessionId: string): SessionResponse | null {
|
|||||||
return record ? toResponse(record) : null;
|
return record ? toResponse(record) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSessionClosedStatus(status: string | null | undefined): boolean {
|
||||||
|
return !!status && CLOSED_SESSION_STATUSES.has(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveExistingSessionId(sessionId: string): string | null {
|
||||||
|
if (storeGetSession(sessionId)) {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const compatibleCodeSessionId = toCompatibleCodeSessionId(sessionId);
|
||||||
|
if (compatibleCodeSessionId && storeGetSession(compatibleCodeSessionId)) {
|
||||||
|
return compatibleCodeSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveExistingWebSessionId(sessionId: string): string | null {
|
||||||
|
return resolveExistingSessionId(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveOwnedWebSessionId(sessionId: string, uuid: string): string | null {
|
||||||
|
if (storeIsSessionOwner(sessionId, uuid)) {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const compatibleCodeSessionId = toCompatibleCodeSessionId(sessionId);
|
||||||
|
if (compatibleCodeSessionId && storeIsSessionOwner(compatibleCodeSessionId, uuid)) {
|
||||||
|
return compatibleCodeSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listWebSessionsByOwnerUuid(uuid: string): SessionResponse[] {
|
||||||
|
return storeListSessionsByOwnerUuid(uuid)
|
||||||
|
.filter((session) => !isSessionClosedStatus(session.status))
|
||||||
|
.map(toResponse)
|
||||||
|
.map(toWebSessionResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listWebSessionSummariesByOwnerUuid(uuid: string): SessionSummaryResponse[] {
|
||||||
|
return storeListSessionsByOwnerUuid(uuid)
|
||||||
|
.filter((session) => !isSessionClosedStatus(session.status))
|
||||||
|
.map(toSummaryResponse)
|
||||||
|
.map(toWebSessionSummaryResponse);
|
||||||
|
}
|
||||||
|
|
||||||
export function updateSessionTitle(sessionId: string, title: string) {
|
export function updateSessionTitle(sessionId: string, title: string) {
|
||||||
storeUpdateSession(sessionId, { title });
|
storeUpdateSession(sessionId, { title });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateSessionStatus(sessionId: string, status: string) {
|
export function updateSessionStatus(sessionId: string, status: string) {
|
||||||
storeUpdateSession(sessionId, { status });
|
storeUpdateSession(sessionId, { status });
|
||||||
|
const bus = getAllEventBuses().get(sessionId);
|
||||||
|
if (!bus) return;
|
||||||
|
|
||||||
|
bus.publish({
|
||||||
|
id: uuid(),
|
||||||
|
sessionId,
|
||||||
|
type: "session_status",
|
||||||
|
payload: { status },
|
||||||
|
direction: "inbound",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function touchSession(sessionId: string) {
|
||||||
|
storeUpdateSession(sessionId, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function archiveSession(sessionId: string) {
|
export function archiveSession(sessionId: string) {
|
||||||
storeUpdateSession(sessionId, { status: "archived" });
|
updateSessionStatus(sessionId, "archived");
|
||||||
removeEventBus(sessionId);
|
removeEventBus(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export function normalizePayload(type: string, payload: unknown): Record<string,
|
|||||||
raw: payload,
|
raw: payload,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (typeof p.uuid === "string" && p.uuid) normalized.uuid = p.uuid;
|
||||||
|
|
||||||
// Preserve tool fields
|
// Preserve tool fields
|
||||||
if (p.tool_name) normalized.tool_name = p.tool_name;
|
if (p.tool_name) normalized.tool_name = p.tool_name;
|
||||||
if (p.name) normalized.tool_name = p.name;
|
if (p.name) normalized.tool_name = p.name;
|
||||||
|
|||||||
@@ -47,6 +47,16 @@ export interface WorkItemRecord {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SessionWorkerRecord {
|
||||||
|
sessionId: string;
|
||||||
|
workerStatus: string | null;
|
||||||
|
externalMetadata: Record<string, unknown> | null;
|
||||||
|
requiresActionDetails: Record<string, unknown> | null;
|
||||||
|
lastHeartbeatAt: Date | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Stores (in-memory Maps) ----------
|
// ---------- Stores (in-memory Maps) ----------
|
||||||
|
|
||||||
const users = new Map<string, UserRecord>();
|
const users = new Map<string, UserRecord>();
|
||||||
@@ -54,6 +64,7 @@ const tokenToUser = new Map<string, { username: string; createdAt: Date }>();
|
|||||||
const environments = new Map<string, EnvironmentRecord>();
|
const environments = new Map<string, EnvironmentRecord>();
|
||||||
const sessions = new Map<string, SessionRecord>();
|
const sessions = new Map<string, SessionRecord>();
|
||||||
const workItems = new Map<string, WorkItemRecord>();
|
const workItems = new Map<string, WorkItemRecord>();
|
||||||
|
const sessionWorkers = new Map<string, SessionWorkerRecord>();
|
||||||
|
|
||||||
// UUID → session ownership: sessionId → Set of UUIDs
|
// UUID → session ownership: sessionId → Set of UUIDs
|
||||||
const sessionOwners = new Map<string, Set<string>>();
|
const sessionOwners = new Map<string, Set<string>>();
|
||||||
@@ -190,9 +201,59 @@ export function storeListSessionsByEnvironment(envId: string): SessionRecord[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function storeDeleteSession(id: string): boolean {
|
export function storeDeleteSession(id: string): boolean {
|
||||||
|
sessionWorkers.delete(id);
|
||||||
return sessions.delete(id);
|
return sessions.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Session Worker ----------
|
||||||
|
|
||||||
|
export function storeGetSessionWorker(sessionId: string): SessionWorkerRecord | undefined {
|
||||||
|
return sessionWorkers.get(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function storeUpsertSessionWorker(sessionId: string, patch: {
|
||||||
|
workerStatus?: string | null;
|
||||||
|
externalMetadata?: Record<string, unknown> | null;
|
||||||
|
requiresActionDetails?: Record<string, unknown> | null;
|
||||||
|
lastHeartbeatAt?: Date | null;
|
||||||
|
}): SessionWorkerRecord {
|
||||||
|
const now = new Date();
|
||||||
|
const existing = sessionWorkers.get(sessionId);
|
||||||
|
const record: SessionWorkerRecord = existing ?? {
|
||||||
|
sessionId,
|
||||||
|
workerStatus: null,
|
||||||
|
externalMetadata: null,
|
||||||
|
requiresActionDetails: null,
|
||||||
|
lastHeartbeatAt: null,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (patch.workerStatus !== undefined) {
|
||||||
|
record.workerStatus = patch.workerStatus;
|
||||||
|
}
|
||||||
|
if (patch.externalMetadata !== undefined) {
|
||||||
|
if (patch.externalMetadata === null) {
|
||||||
|
record.externalMetadata = null;
|
||||||
|
} else {
|
||||||
|
record.externalMetadata = {
|
||||||
|
...(record.externalMetadata ?? {}),
|
||||||
|
...patch.externalMetadata,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (patch.requiresActionDetails !== undefined) {
|
||||||
|
record.requiresActionDetails = patch.requiresActionDetails;
|
||||||
|
}
|
||||||
|
if (patch.lastHeartbeatAt !== undefined) {
|
||||||
|
record.lastHeartbeatAt = patch.lastHeartbeatAt;
|
||||||
|
}
|
||||||
|
record.updatedAt = now;
|
||||||
|
|
||||||
|
sessionWorkers.set(sessionId, record);
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Work Items ----------
|
// ---------- Work Items ----------
|
||||||
|
|
||||||
// ---------- Session Ownership (UUID-based) ----------
|
// ---------- Session Ownership (UUID-based) ----------
|
||||||
@@ -272,5 +333,6 @@ export function storeReset() {
|
|||||||
environments.clear();
|
environments.clear();
|
||||||
sessions.clear();
|
sessions.clear();
|
||||||
workItems.clear();
|
workItems.clear();
|
||||||
|
sessionWorkers.clear();
|
||||||
sessionOwners.clear();
|
sessionOwners.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,3 +115,109 @@ export function createSSEStream(c: Context, sessionId: string, fromSeqNum = 0) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toWorkerClientPayload(event: SessionEvent): Record<string, unknown> {
|
||||||
|
const normalized =
|
||||||
|
event.payload && typeof event.payload === "object"
|
||||||
|
? (event.payload as Record<string, unknown>)
|
||||||
|
: undefined;
|
||||||
|
const raw =
|
||||||
|
normalized?.raw && typeof normalized.raw === "object" && !Array.isArray(normalized.raw)
|
||||||
|
? (normalized.raw as Record<string, unknown>)
|
||||||
|
: undefined;
|
||||||
|
const payload: Record<string, unknown> = {
|
||||||
|
...(raw ?? normalized ?? {}),
|
||||||
|
type: event.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (event.type === "user") {
|
||||||
|
const message = payload.message;
|
||||||
|
if (!message || typeof message !== "object" || !("content" in message)) {
|
||||||
|
const content =
|
||||||
|
typeof normalized?.content === "string"
|
||||||
|
? normalized.content
|
||||||
|
: typeof payload.content === "string"
|
||||||
|
? payload.content
|
||||||
|
: typeof event.payload === "string"
|
||||||
|
? event.payload
|
||||||
|
: "";
|
||||||
|
payload.content = content;
|
||||||
|
payload.message = { content };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toWorkerClientFrame(event: SessionEvent): string {
|
||||||
|
const data = JSON.stringify({
|
||||||
|
event_id: event.id,
|
||||||
|
sequence_num: event.seqNum,
|
||||||
|
event_type: event.type,
|
||||||
|
source: "client",
|
||||||
|
payload: toWorkerClientPayload(event),
|
||||||
|
created_at: new Date(event.createdAt).toISOString(),
|
||||||
|
});
|
||||||
|
return `id: ${event.seqNum}\nevent: client_event\ndata: ${data}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create CCR worker SSE stream (client_event frames, outbound events only). */
|
||||||
|
export function createWorkerEventStream(c: Context, sessionId: string, fromSeqNum = 0) {
|
||||||
|
const bus = getEventBus(sessionId);
|
||||||
|
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
if (fromSeqNum > 0) {
|
||||||
|
const missed = bus
|
||||||
|
.getEventsSince(fromSeqNum)
|
||||||
|
.filter((event) => event.direction === "outbound");
|
||||||
|
for (const event of missed) {
|
||||||
|
controller.enqueue(encoder.encode(toWorkerClientFrame(event)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.enqueue(encoder.encode(": keepalive\n\n"));
|
||||||
|
|
||||||
|
const unsub = bus.subscribe((event) => {
|
||||||
|
if (event.direction !== "outbound") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
controller.enqueue(encoder.encode(toWorkerClientFrame(event)));
|
||||||
|
} catch {
|
||||||
|
unsub();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const keepalive = setInterval(() => {
|
||||||
|
try {
|
||||||
|
controller.enqueue(encoder.encode(": keepalive\n\n"));
|
||||||
|
} catch {
|
||||||
|
clearInterval(keepalive);
|
||||||
|
unsub();
|
||||||
|
}
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
c.req.raw.signal.addEventListener("abort", () => {
|
||||||
|
unsub();
|
||||||
|
clearInterval(keepalive);
|
||||||
|
try {
|
||||||
|
controller.close();
|
||||||
|
} catch {
|
||||||
|
// already closed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/event-stream",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
Connection: "keep-alive",
|
||||||
|
"X-Accel-Buffering": "no",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,13 +24,14 @@ const SERVER_KEEPALIVE_INTERVAL_MS = 60_000;
|
|||||||
*/
|
*/
|
||||||
function toSDKMessage(event: SessionEvent): string {
|
function toSDKMessage(event: SessionEvent): string {
|
||||||
const payload = event.payload as Record<string, unknown> | null;
|
const payload = event.payload as Record<string, unknown> | null;
|
||||||
|
const messageUuid = typeof payload?.uuid === "string" && payload.uuid ? payload.uuid : event.id;
|
||||||
|
|
||||||
let msg: Record<string, unknown>;
|
let msg: Record<string, unknown>;
|
||||||
|
|
||||||
if (event.type === "user" || event.type === "user_message") {
|
if (event.type === "user" || event.type === "user_message") {
|
||||||
msg = {
|
msg = {
|
||||||
type: "user",
|
type: "user",
|
||||||
uuid: event.id,
|
uuid: messageUuid,
|
||||||
session_id: event.sessionId,
|
session_id: event.sessionId,
|
||||||
message: {
|
message: {
|
||||||
role: "user",
|
role: "user",
|
||||||
@@ -82,7 +83,7 @@ function toSDKMessage(event: SessionEvent): string {
|
|||||||
} else {
|
} else {
|
||||||
msg = {
|
msg = {
|
||||||
type: event.type,
|
type: event.type,
|
||||||
uuid: event.id,
|
uuid: messageUuid,
|
||||||
session_id: event.sessionId,
|
session_id: event.sessionId,
|
||||||
message: payload,
|
message: payload,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"extends": "../../tsconfig.base.json",
|
||||||
"target": "ES2022",
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"strict": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"outDir": "dist",
|
|
||||||
"rootDir": ".",
|
|
||||||
"declaration": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"types": ["bun-types"]
|
|
||||||
},
|
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts"],
|
||||||
"exclude": ["node_modules", "dist", "web"]
|
"exclude": ["node_modules", "dist", "web"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,26 @@
|
|||||||
*/
|
*/
|
||||||
import { getUuid, setUuid, apiBind, apiFetchSessions, apiFetchAllSessions, apiFetchEnvironments, apiFetchSession, apiFetchSessionHistory, apiSendEvent, apiSendControl, apiInterrupt, apiCreateSession } from "./api.js";
|
import { getUuid, setUuid, apiBind, apiFetchSessions, apiFetchAllSessions, apiFetchEnvironments, apiFetchSession, apiFetchSessionHistory, apiSendEvent, apiSendControl, apiInterrupt, apiCreateSession } from "./api.js";
|
||||||
import { connectSSE, disconnectSSE } from "./sse.js";
|
import { connectSSE, disconnectSSE } from "./sse.js";
|
||||||
import { appendEvent, renderPermissionRequest, showLoading, isLoading, resetReplayState, renderReplayPendingRequests } from "./render.js";
|
import { appendEvent, showLoading, isLoading, removeLoading, resetReplayState, renderReplayPendingRequests } from "./render.js";
|
||||||
import { initTaskPanel, toggleTaskPanel, resetTaskState } from "./task-panel.js";
|
import { initTaskPanel, toggleTaskPanel, resetTaskState } from "./task-panel.js";
|
||||||
import { esc, formatTime, statusClass } from "./utils.js";
|
import { esc, formatTime, statusClass, isClosedSessionStatus } from "./utils.js";
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// State
|
// State
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
let currentSessionId = null;
|
let currentSessionId = null;
|
||||||
|
let currentSessionStatus = null;
|
||||||
let dashboardInterval = null;
|
let dashboardInterval = null;
|
||||||
let cachedEnvs = [];
|
let cachedEnvs = [];
|
||||||
|
|
||||||
|
function generateMessageUuid() {
|
||||||
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}
|
||||||
|
return `msg_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Router
|
// Router
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -43,6 +51,69 @@ function navigate(path) {
|
|||||||
}
|
}
|
||||||
window.navigate = navigate;
|
window.navigate = navigate;
|
||||||
|
|
||||||
|
function applySessionStatus(status) {
|
||||||
|
currentSessionStatus = status || null;
|
||||||
|
|
||||||
|
const badge = document.getElementById("session-status");
|
||||||
|
if (badge) {
|
||||||
|
badge.textContent = status || "";
|
||||||
|
badge.className = `status-badge status-${statusClass(status)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const closed = isClosedSessionStatus(status);
|
||||||
|
const input = document.getElementById("msg-input");
|
||||||
|
if (input) {
|
||||||
|
input.disabled = closed;
|
||||||
|
input.placeholder = closed ? "Session is closed" : "Type a message...";
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionBtn = document.getElementById("action-btn");
|
||||||
|
if (actionBtn) {
|
||||||
|
actionBtn.disabled = closed;
|
||||||
|
actionBtn.title = closed ? "Session is closed" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closed) {
|
||||||
|
removeLoading();
|
||||||
|
window.__updateActionBtn?.(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSessionEvent(event) {
|
||||||
|
if (event?.type === "session_status" && typeof event.payload?.status === "string") {
|
||||||
|
applySessionStatus(event.payload.status);
|
||||||
|
if (isClosedSessionStatus(event.payload.status)) {
|
||||||
|
disconnectSSE();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appendEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncClosedSessionState(err, actionLabel) {
|
||||||
|
if (!(err instanceof Error)) {
|
||||||
|
alert(`${actionLabel}: unknown error`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSessionId || !/session is /i.test(err.message)) {
|
||||||
|
alert(`${actionLabel}: ${err.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await apiFetchSession(currentSessionId);
|
||||||
|
applySessionStatus(session.status);
|
||||||
|
if (isClosedSessionStatus(session.status)) {
|
||||||
|
appendEvent({ type: "session_status", payload: { status: session.status } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall back to the original error if the refresh also fails.
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(`${actionLabel}: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
async function handleRoute() {
|
async function handleRoute() {
|
||||||
// Ensure we have a UUID
|
// Ensure we have a UUID
|
||||||
getUuid();
|
getUuid();
|
||||||
@@ -86,6 +157,8 @@ async function handleRoute() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default: /code → dashboard
|
// Default: /code → dashboard
|
||||||
|
currentSessionId = null;
|
||||||
|
currentSessionStatus = null;
|
||||||
showPage("dashboard");
|
showPage("dashboard");
|
||||||
disconnectSSE();
|
disconnectSSE();
|
||||||
renderDashboard();
|
renderDashboard();
|
||||||
@@ -172,9 +245,7 @@ async function renderSessionDetail(id) {
|
|||||||
document.getElementById("session-id").textContent = session.id;
|
document.getElementById("session-id").textContent = session.id;
|
||||||
document.getElementById("session-env").textContent = session.environment_id || "";
|
document.getElementById("session-env").textContent = session.environment_id || "";
|
||||||
document.getElementById("session-time").textContent = formatTime(session.created_at);
|
document.getElementById("session-time").textContent = formatTime(session.created_at);
|
||||||
const badge = document.getElementById("session-status");
|
applySessionStatus(session.status);
|
||||||
badge.textContent = session.status;
|
|
||||||
badge.className = `status-badge status-${statusClass(session.status)}`;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Failed to load session: " + err.message);
|
alert("Failed to load session: " + err.message);
|
||||||
navigate("/code/");
|
navigate("/code/");
|
||||||
@@ -201,7 +272,13 @@ async function renderSessionDetail(id) {
|
|||||||
// Re-render any still-unresolved permission prompts from history
|
// Re-render any still-unresolved permission prompts from history
|
||||||
renderReplayPendingRequests();
|
renderReplayPendingRequests();
|
||||||
|
|
||||||
connectSSE(id, appendEvent, lastSeqNum);
|
if (isClosedSessionStatus(currentSessionStatus)) {
|
||||||
|
appendEvent({ type: "session_status", payload: { status: currentSessionStatus } });
|
||||||
|
disconnectSSE();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectSSE(id, handleSessionEvent, lastSeqNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -237,28 +314,35 @@ function setupControlBar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function doInterrupt() {
|
async function doInterrupt() {
|
||||||
if (!currentSessionId) return;
|
if (!currentSessionId || isClosedSessionStatus(currentSessionStatus)) return;
|
||||||
const btn = document.getElementById("action-btn");
|
const btn = document.getElementById("action-btn");
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
try {
|
try {
|
||||||
await apiInterrupt(currentSessionId);
|
await apiInterrupt(currentSessionId);
|
||||||
appendEvent({ type: "interrupt", payload: { message: "Session interrupted" } });
|
appendEvent({ type: "interrupt", payload: { message: "Session interrupted" } });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Interrupt failed: " + err.message);
|
await syncClosedSessionState(err, "Interrupt failed");
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = isClosedSessionStatus(currentSessionStatus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
const input = document.getElementById("msg-input");
|
const input = document.getElementById("msg-input");
|
||||||
const text = input.value.trim();
|
const text = input.value.trim();
|
||||||
if (!text || !currentSessionId) return;
|
if (!text || !currentSessionId || isClosedSessionStatus(currentSessionStatus)) return;
|
||||||
input.value = "";
|
input.value = "";
|
||||||
|
const uuid = generateMessageUuid();
|
||||||
try {
|
try {
|
||||||
await apiSendEvent(currentSessionId, { type: "user", content: text });
|
await apiSendEvent(currentSessionId, {
|
||||||
|
type: "user",
|
||||||
|
uuid,
|
||||||
|
content: text,
|
||||||
|
message: { content: text },
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert("Failed to send: " + err.message);
|
input.value = text;
|
||||||
|
await syncClosedSessionState(err, "Failed to send");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ nav {
|
|||||||
|
|
||||||
.status-active, .status-running { background: var(--green-bg); color: var(--green); }
|
.status-active, .status-running { background: var(--green-bg); color: var(--green); }
|
||||||
.status-idle { background: var(--yellow-bg); color: var(--yellow); }
|
.status-idle { background: var(--yellow-bg); color: var(--yellow); }
|
||||||
|
.status-inactive { background: #F0ECE7; color: var(--text-secondary); }
|
||||||
.status-requires_action { background: var(--orange-bg); color: var(--orange); }
|
.status-requires_action { background: var(--orange-bg); color: var(--orange); }
|
||||||
.status-archived { background: #F0ECE7; color: var(--text-secondary); }
|
.status-archived { background: #F0ECE7; color: var(--text-secondary); }
|
||||||
.status-error { background: var(--red-bg); color: var(--red); }
|
.status-error { background: var(--red-bg); color: var(--red); }
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,500;12..96,600;12..96,700&family=Figtree:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&display=swap" />
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,500;12..96,600;12..96,700&family=Figtree:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&display=swap" />
|
||||||
<link rel="stylesheet" href="./style.css" />
|
<link rel="stylesheet" href="/code/style.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Nav Bar -->
|
<!-- Nav Bar -->
|
||||||
@@ -146,6 +146,6 @@
|
|||||||
<!-- QR Libraries -->
|
<!-- QR Libraries -->
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js"></script>
|
||||||
<script type="module" src="./app.js"></script>
|
<script type="module" src="/code/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ import { processAssistantEvent } from "./task-panel.js";
|
|||||||
|
|
||||||
const replayPendingRequests = new Map(); // request_id → event data (unresolved)
|
const replayPendingRequests = new Map(); // request_id → event data (unresolved)
|
||||||
const replayRespondedRequests = new Set(); // request_ids that have a response
|
const replayRespondedRequests = new Set(); // request_ids that have a response
|
||||||
|
const renderedUserUuids = new Set();
|
||||||
|
|
||||||
/** Clear replay tracking state (call before each history load) */
|
/** Clear replay tracking state (call before each history load) */
|
||||||
export function resetReplayState() {
|
export function resetReplayState() {
|
||||||
replayPendingRequests.clear();
|
replayPendingRequests.clear();
|
||||||
replayRespondedRequests.clear();
|
replayRespondedRequests.clear();
|
||||||
|
renderedUserUuids.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** After replay finishes, render any still-unresolved permission prompts */
|
/** After replay finishes, render any still-unresolved permission prompts */
|
||||||
@@ -84,6 +86,59 @@ function formatAssistantContent(content) {
|
|||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getUserUuid(payload) {
|
||||||
|
if (!payload || typeof payload !== "object") return null;
|
||||||
|
if (typeof payload.uuid === "string" && payload.uuid) return payload.uuid;
|
||||||
|
if (payload.raw && typeof payload.raw === "object" && typeof payload.raw.uuid === "string" && payload.raw.uuid) {
|
||||||
|
return payload.raw.uuid;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldRenderUserEvent(payload, direction, replay) {
|
||||||
|
const uuid = getUserUuid(payload);
|
||||||
|
if (uuid) {
|
||||||
|
if (renderedUserUuids.has(uuid)) return false;
|
||||||
|
renderedUserUuids.add(uuid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy fallback with no uuid: keep the previous no-duplicate behavior.
|
||||||
|
// Live inbound user events without a uuid are most likely echoes of a web-
|
||||||
|
// sent message; replay keeps the prior "outbound only" rule as well.
|
||||||
|
return direction === "outbound";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMessageContentBlocks(payload) {
|
||||||
|
if (!payload || typeof payload !== "object") return [];
|
||||||
|
const msg = payload.message;
|
||||||
|
if (!msg || typeof msg !== "object" || !Array.isArray(msg.content)) return [];
|
||||||
|
return msg.content.filter((block) => block && typeof block === "object");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEmbeddedToolUseBlocks(payload) {
|
||||||
|
return getMessageContentBlocks(payload)
|
||||||
|
.filter((block) => block.type === "tool_use")
|
||||||
|
.map((block) =>
|
||||||
|
renderToolUse({
|
||||||
|
tool_name: block.name || "tool",
|
||||||
|
tool_input: block.input || {},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEmbeddedToolResultBlocks(payload) {
|
||||||
|
return getMessageContentBlocks(payload)
|
||||||
|
.filter((block) => block.type === "tool_result")
|
||||||
|
.map((block) =>
|
||||||
|
renderToolResult({
|
||||||
|
content: block.content || "",
|
||||||
|
output: block.content || "",
|
||||||
|
is_error: !!block.is_error,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Event Router
|
// Event Router
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -103,26 +158,42 @@ export function appendEvent(data, { replay = false } = {}) {
|
|||||||
// During history replay, only render messages & tools — skip interactive/stateful events
|
// During history replay, only render messages & tools — skip interactive/stateful events
|
||||||
// Exception: unresolved permission/control requests are re-shown as pending prompts.
|
// Exception: unresolved permission/control requests are re-shown as pending prompts.
|
||||||
if (replay) {
|
if (replay) {
|
||||||
let histEl;
|
const histEls = [];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "user":
|
case "user":
|
||||||
if (direction === "outbound") histEl = renderUserMessage(payload, direction);
|
{
|
||||||
|
const toolResultEls = renderEmbeddedToolResultBlocks(payload);
|
||||||
|
if (toolResultEls.length > 0) {
|
||||||
|
histEls.push(...toolResultEls);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (shouldRenderUserEvent(payload, direction, true)) {
|
||||||
|
histEls.push(renderUserMessage(payload, direction));
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "assistant":
|
case "assistant":
|
||||||
{
|
{
|
||||||
|
const toolUseEls = renderEmbeddedToolUseBlocks(payload);
|
||||||
const text = extractText(payload);
|
const text = extractText(payload);
|
||||||
if (text && text.trim()) histEl = renderAssistantMessage(payload);
|
if (text && text.trim()) histEls.push(renderAssistantMessage(payload));
|
||||||
|
if (toolUseEls.length > 0) histEls.push(...toolUseEls);
|
||||||
processAssistantEvent(payload);
|
processAssistantEvent(payload);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "tool_use":
|
case "tool_use":
|
||||||
histEl = renderToolUse(payload);
|
histEls.push(renderToolUse(payload));
|
||||||
break;
|
break;
|
||||||
case "tool_result":
|
case "tool_result":
|
||||||
histEl = renderToolResult(payload);
|
histEls.push(renderToolResult(payload));
|
||||||
break;
|
break;
|
||||||
case "error":
|
case "error":
|
||||||
histEl = renderSystemMessage(`Error: ${payload.message || payload.content || "Unknown error"}`);
|
histEls.push(renderSystemMessage(`Error: ${payload.message || payload.content || "Unknown error"}`));
|
||||||
|
break;
|
||||||
|
case "session_status":
|
||||||
|
if (payload.status === "archived" || payload.status === "inactive") {
|
||||||
|
histEls.push(renderSystemMessage(`Session ${payload.status}`));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "control_request":
|
case "control_request":
|
||||||
case "permission_request":
|
case "permission_request":
|
||||||
@@ -149,32 +220,42 @@ export function appendEvent(data, { replay = false } = {}) {
|
|||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (histEl) {
|
for (const histEl of histEls) {
|
||||||
stream.appendChild(histEl);
|
stream.appendChild(histEl);
|
||||||
stream.scrollTop = stream.scrollHeight;
|
stream.scrollTop = stream.scrollHeight;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let el;
|
const els = [];
|
||||||
let needLoading = false;
|
let needLoading = false;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "user":
|
case "user":
|
||||||
// Skip inbound user messages — they're echoes of what we already sent
|
{
|
||||||
if (direction === "inbound") return;
|
const toolResultEls = renderEmbeddedToolResultBlocks(payload);
|
||||||
el = renderUserMessage(payload, direction);
|
if (toolResultEls.length > 0) {
|
||||||
needLoading = true;
|
els.push(...toolResultEls);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!shouldRenderUserEvent(payload, direction, false)) return;
|
||||||
|
els.push(renderUserMessage(payload, direction));
|
||||||
|
needLoading = true;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "partial_assistant":
|
case "partial_assistant":
|
||||||
// Skip partial assistant — wait for the final "assistant" event
|
// Skip partial assistant — wait for the final "assistant" event
|
||||||
// to avoid blank/duplicate messages during streaming
|
// to avoid blank/duplicate messages during streaming
|
||||||
return;
|
return;
|
||||||
case "assistant":
|
case "assistant":
|
||||||
removeLoading();
|
|
||||||
{
|
{
|
||||||
|
const toolUseEls = renderEmbeddedToolUseBlocks(payload);
|
||||||
const text = extractText(payload);
|
const text = extractText(payload);
|
||||||
if (text && text.trim()) el = renderAssistantMessage(payload);
|
if (text && text.trim()) {
|
||||||
|
removeLoading();
|
||||||
|
els.push(renderAssistantMessage(payload));
|
||||||
|
}
|
||||||
|
if (toolUseEls.length > 0) els.push(...toolUseEls);
|
||||||
processAssistantEvent(payload);
|
processAssistantEvent(payload);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -184,10 +265,10 @@ export function appendEvent(data, { replay = false } = {}) {
|
|||||||
// Skip result — it just repeats the assistant message content
|
// Skip result — it just repeats the assistant message content
|
||||||
return;
|
return;
|
||||||
case "tool_use":
|
case "tool_use":
|
||||||
el = renderToolUse(payload);
|
els.push(renderToolUse(payload));
|
||||||
break;
|
break;
|
||||||
case "tool_result":
|
case "tool_result":
|
||||||
el = renderToolResult(payload);
|
els.push(renderToolResult(payload));
|
||||||
break;
|
break;
|
||||||
case "control_request":
|
case "control_request":
|
||||||
case "permission_request":
|
case "permission_request":
|
||||||
@@ -195,27 +276,27 @@ export function appendEvent(data, { replay = false } = {}) {
|
|||||||
const toolName = payload.request.tool_name || "unknown";
|
const toolName = payload.request.tool_name || "unknown";
|
||||||
const toolInput = payload.request.input || payload.request.tool_input || {};
|
const toolInput = payload.request.input || payload.request.tool_input || {};
|
||||||
if (toolName === "AskUserQuestion") {
|
if (toolName === "AskUserQuestion") {
|
||||||
el = renderAskUserQuestion({
|
els.push(renderAskUserQuestion({
|
||||||
request_id: payload.request_id || data.id,
|
request_id: payload.request_id || data.id,
|
||||||
tool_input: toolInput,
|
tool_input: toolInput,
|
||||||
description: payload.request.description || "",
|
description: payload.request.description || "",
|
||||||
});
|
}));
|
||||||
} else if (toolName === "ExitPlanMode") {
|
} else if (toolName === "ExitPlanMode") {
|
||||||
el = renderExitPlanMode({
|
els.push(renderExitPlanMode({
|
||||||
request_id: payload.request_id || data.id,
|
request_id: payload.request_id || data.id,
|
||||||
tool_input: toolInput,
|
tool_input: toolInput,
|
||||||
description: payload.request.description || "",
|
description: payload.request.description || "",
|
||||||
});
|
}));
|
||||||
} else {
|
} else {
|
||||||
el = renderPermissionRequest({
|
els.push(renderPermissionRequest({
|
||||||
request_id: payload.request_id || data.id,
|
request_id: payload.request_id || data.id,
|
||||||
tool_name: toolName,
|
tool_name: toolName,
|
||||||
tool_input: toolInput,
|
tool_input: toolInput,
|
||||||
description: payload.request.description || "",
|
description: payload.request.description || "",
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
el = renderSystemMessage(`Control: ${payload.request?.subtype || "unknown"}`);
|
els.push(renderSystemMessage(`Control: ${payload.request?.subtype || "unknown"}`));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "control_response":
|
case "control_response":
|
||||||
@@ -229,16 +310,22 @@ export function appendEvent(data, { replay = false } = {}) {
|
|||||||
const fullText = typeof payload === "string" ? payload : JSON.stringify(payload);
|
const fullText = typeof payload === "string" ? payload : JSON.stringify(payload);
|
||||||
if (/connecting|waiting|initializing|Remote Control/i.test(msg + " " + fullText)) return;
|
if (/connecting|waiting|initializing|Remote Control/i.test(msg + " " + fullText)) return;
|
||||||
if (!msg.trim()) return;
|
if (!msg.trim()) return;
|
||||||
el = renderSystemMessage(msg);
|
els.push(renderSystemMessage(msg));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "error":
|
case "error":
|
||||||
removeLoading();
|
removeLoading();
|
||||||
el = renderSystemMessage(`Error: ${payload.message || payload.content || "Unknown error"}`);
|
els.push(renderSystemMessage(`Error: ${payload.message || payload.content || "Unknown error"}`));
|
||||||
|
break;
|
||||||
|
case "session_status":
|
||||||
|
if (payload.status === "archived" || payload.status === "inactive") {
|
||||||
|
removeLoading();
|
||||||
|
els.push(renderSystemMessage(`Session ${payload.status}`));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "interrupt":
|
case "interrupt":
|
||||||
removeLoading();
|
removeLoading();
|
||||||
el = renderSystemMessage("Session interrupted");
|
els.push(renderSystemMessage("Session interrupted"));
|
||||||
break;
|
break;
|
||||||
case "system":
|
case "system":
|
||||||
// Skip raw system/init messages — they're noise
|
// Skip raw system/init messages — they're noise
|
||||||
@@ -247,11 +334,11 @@ export function appendEvent(data, { replay = false } = {}) {
|
|||||||
// Skip noise from bridge init
|
// Skip noise from bridge init
|
||||||
const raw = JSON.stringify(payload);
|
const raw = JSON.stringify(payload);
|
||||||
if (/Remote Control connecting/i.test(raw)) return;
|
if (/Remote Control connecting/i.test(raw)) return;
|
||||||
el = renderSystemMessage(`${type}: ${truncate(raw, 200)}`);
|
els.push(renderSystemMessage(`${type}: ${truncate(raw, 200)}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el) {
|
for (const el of els) {
|
||||||
stream.appendChild(el);
|
stream.appendChild(el);
|
||||||
stream.scrollTop = stream.scrollHeight;
|
stream.scrollTop = stream.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,14 @@ export function statusClass(status) {
|
|||||||
active: "active",
|
active: "active",
|
||||||
running: "running",
|
running: "running",
|
||||||
idle: "idle",
|
idle: "idle",
|
||||||
|
inactive: "inactive",
|
||||||
requires_action: "requires_action",
|
requires_action: "requires_action",
|
||||||
archived: "archived",
|
archived: "archived",
|
||||||
error: "error",
|
error: "error",
|
||||||
};
|
};
|
||||||
return map[status] || "default";
|
return map[status] || "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isClosedSessionStatus(status) {
|
||||||
|
return status === "archived" || status === "inactive";
|
||||||
|
}
|
||||||
|
|||||||
5
packages/url-handler-napi/tsconfig.json
Normal file
5
packages/url-handler-napi/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -37,6 +37,8 @@ const DEFAULT_FEATURES = [
|
|||||||
"KAIROS_BRIEF", "AWAY_SUMMARY", "ULTRAPLAN",
|
"KAIROS_BRIEF", "AWAY_SUMMARY", "ULTRAPLAN",
|
||||||
// P2: daemon + remote control server
|
// P2: daemon + remote control server
|
||||||
"DAEMON",
|
"DAEMON",
|
||||||
|
// ACP (Agent Client Protocol) agent mode
|
||||||
|
"ACP",
|
||||||
// PR-package restored features
|
// PR-package restored features
|
||||||
"WORKFLOW_SCRIPTS",
|
"WORKFLOW_SCRIPTS",
|
||||||
"HISTORY_SNIP",
|
"HISTORY_SNIP",
|
||||||
|
|||||||
90
scripts/post-build.ts
Normal file
90
scripts/post-build.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* Post-build processing for Vite build output.
|
||||||
|
*
|
||||||
|
* 1. Patch globalThis.Bun destructuring in third-party deps for Node.js compat
|
||||||
|
* 2. Copy native addon files
|
||||||
|
* 3. Bundle standalone scripts (download-ripgrep)
|
||||||
|
* 4. Generate dual entry points (cli-bun.js, cli-node.js)
|
||||||
|
*/
|
||||||
|
import { readdir, readFile, writeFile, cp } from "node:fs/promises";
|
||||||
|
import { chmodSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
|
||||||
|
const outdir = "dist";
|
||||||
|
|
||||||
|
async function postBuild() {
|
||||||
|
// Step 1: Patch globalThis.Bun destructuring from third-party deps
|
||||||
|
const files = await readdir(outdir, { recursive: true });
|
||||||
|
const BUN_DESTRUCTURE = /var \{([^}]+)\} = globalThis\.Bun;?/g;
|
||||||
|
const BUN_DESTRUCTURE_SAFE =
|
||||||
|
'var {$1} = typeof globalThis.Bun !== "undefined" ? globalThis.Bun : {};';
|
||||||
|
|
||||||
|
let bunPatched = 0;
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = join(outdir, file);
|
||||||
|
if (typeof file !== "string" || !file.endsWith(".js")) continue;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Copy native addon files
|
||||||
|
const vendorDir = join(outdir, "vendor", "audio-capture");
|
||||||
|
await cp("vendor/audio-capture", vendorDir, { recursive: true } as never);
|
||||||
|
console.log(`Copied vendor/audio-capture/ → ${vendorDir}/`);
|
||||||
|
|
||||||
|
// Step 3: Bundle standalone scripts via Bun.build (kept for simplicity)
|
||||||
|
try {
|
||||||
|
const { default: Bun } = await import("bun");
|
||||||
|
const rgScript = await Bun.build({
|
||||||
|
entrypoints: ["scripts/download-ripgrep.ts"],
|
||||||
|
outdir,
|
||||||
|
target: "node",
|
||||||
|
});
|
||||||
|
if (rgScript.success) {
|
||||||
|
console.log(`Bundled download-ripgrep script to ${outdir}/`);
|
||||||
|
} else {
|
||||||
|
console.warn("Failed to bundle download-ripgrep script (non-fatal)");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Bun not available — try esbuild fallback
|
||||||
|
try {
|
||||||
|
execSync(
|
||||||
|
`npx esbuild scripts/download-ripgrep.ts --bundle --platform=node --outfile=${outdir}/download-ripgrep.js --format=esm`,
|
||||||
|
{ stdio: "inherit" },
|
||||||
|
);
|
||||||
|
console.log(`Bundled download-ripgrep script via esbuild to ${outdir}/`);
|
||||||
|
} catch {
|
||||||
|
console.warn(
|
||||||
|
"Failed to bundle download-ripgrep script — skipping (non-fatal)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Generate dual 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');
|
||||||
|
|
||||||
|
chmodSync(cliBun, 0o755);
|
||||||
|
chmodSync(cliNode, 0o755);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Post-build complete: patched ${bunPatched} Bun destructure, generated entry points`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
postBuild().catch((err) => {
|
||||||
|
console.error("Post-build failed:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -13,7 +13,7 @@ import { createRequire } from "node:module";
|
|||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
const cliPath = require.resolve("mcp-chrome-bridge/dist/cli.js");
|
const cliPath = require.resolve("@claude-code-best/mcp-chrome-bridge/dist/cli.js");
|
||||||
|
|
||||||
const userArgs = process.argv.slice(2);
|
const userArgs = process.argv.slice(2);
|
||||||
|
|
||||||
|
|||||||
118
scripts/vite-plugin-feature-flags.ts
Normal file
118
scripts/vite-plugin-feature-flags.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import type { Plugin } from "rollup";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default features that match the official CLI build.
|
||||||
|
* Additional features can be enabled via FEATURE_<NAME>=1 env vars.
|
||||||
|
*/
|
||||||
|
const DEFAULT_BUILD_FEATURES = [
|
||||||
|
"AGENT_TRIGGERS_REMOTE",
|
||||||
|
"CHICAGO_MCP",
|
||||||
|
"VOICE_MODE",
|
||||||
|
"SHOT_STATS",
|
||||||
|
"PROMPT_CACHE_BREAK_DETECTION",
|
||||||
|
"TOKEN_BUDGET",
|
||||||
|
// P0: local features
|
||||||
|
"AGENT_TRIGGERS",
|
||||||
|
"ULTRATHINK",
|
||||||
|
"BUILTIN_EXPLORE_PLAN_AGENTS",
|
||||||
|
"LODESTONE",
|
||||||
|
// P1: API-dependent features
|
||||||
|
"EXTRACT_MEMORIES",
|
||||||
|
"VERIFICATION_AGENT",
|
||||||
|
"KAIROS_BRIEF",
|
||||||
|
"AWAY_SUMMARY",
|
||||||
|
"ULTRAPLAN",
|
||||||
|
// P2: daemon + remote control server
|
||||||
|
"DAEMON",
|
||||||
|
// PR-package restored features
|
||||||
|
"WORKFLOW_SCRIPTS",
|
||||||
|
"HISTORY_SNIP",
|
||||||
|
"CONTEXT_COLLAPSE",
|
||||||
|
"MONITOR_TOOL",
|
||||||
|
"FORK_SUBAGENT",
|
||||||
|
"KAIROS",
|
||||||
|
"COORDINATOR_MODE",
|
||||||
|
"LAN_PIPES",
|
||||||
|
// P3: poor mode
|
||||||
|
"POOR",
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect enabled feature flags from defaults + env vars.
|
||||||
|
*/
|
||||||
|
export function getEnabledFeatures(): Set<string> {
|
||||||
|
const envFeatures = Object.keys(process.env)
|
||||||
|
.filter((k) => k.startsWith("FEATURE_"))
|
||||||
|
.map((k) => k.replace("FEATURE_", ""));
|
||||||
|
return new Set([...DEFAULT_BUILD_FEATURES, ...envFeatures]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regex to match feature('FLAG_NAME') calls with string literal arguments
|
||||||
|
const FEATURE_CALL_RE = /feature\s*\(\s*['"]([\w]+)['"]\s*\)/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vite/Rollup plugin that replaces `feature('X')` calls with boolean literals
|
||||||
|
* at the transform stage, BEFORE the bundler resolves imports.
|
||||||
|
*
|
||||||
|
* This approach is necessary because some feature-gated code blocks contain
|
||||||
|
* require() calls to files that don't exist (e.g. hunter.js inside
|
||||||
|
* feature('REVIEW_ARTIFACT')). The bundler must see these as dead code
|
||||||
|
* (`if (false) { ... }`) before attempting import resolution.
|
||||||
|
*
|
||||||
|
* Also resolves `import { feature } from 'bun:bundle'` as a virtual module
|
||||||
|
* to prevent "module not found" errors.
|
||||||
|
*/
|
||||||
|
export default function featureFlagsPlugin(): Plugin {
|
||||||
|
const features = getEnabledFeatures();
|
||||||
|
|
||||||
|
const virtualModuleId = "bun:bundle";
|
||||||
|
const resolvedVirtualModuleId = "\0" + virtualModuleId;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "feature-flags",
|
||||||
|
|
||||||
|
// Resolve bun:bundle as a virtual module (prevents "module not found")
|
||||||
|
resolveId(id) {
|
||||||
|
if (id === virtualModuleId) {
|
||||||
|
return resolvedVirtualModuleId;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Provide a stub export for bun:bundle (unused at runtime after transform)
|
||||||
|
load(id) {
|
||||||
|
if (id === resolvedVirtualModuleId) {
|
||||||
|
return "export function feature(name) { return false; }";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Replace feature('X') calls with true/false literals at transform time,
|
||||||
|
// and transpile `using` declarations for Node.js compatibility.
|
||||||
|
transform(code, id) {
|
||||||
|
// Skip node_modules
|
||||||
|
if (id.includes("node_modules")) return null;
|
||||||
|
|
||||||
|
let modified = false;
|
||||||
|
|
||||||
|
// 1. Replace feature('X') calls with boolean literals
|
||||||
|
let matchCount = 0;
|
||||||
|
let transformed = code.replace(FEATURE_CALL_RE, (match, flagName) => {
|
||||||
|
matchCount++;
|
||||||
|
return features.has(flagName) ? "true" : "false";
|
||||||
|
});
|
||||||
|
if (matchCount > 0) modified = true;
|
||||||
|
|
||||||
|
// 2. Transpile `using _ = expr;` to `const _ = expr;` for Node.js compat.
|
||||||
|
// Node.js v22 does not support `using` declarations (Explicit Resource Management).
|
||||||
|
// Safe because: SLOW_OPERATION_LOGGING is not enabled, so slowLogging returns
|
||||||
|
// a no-op disposable whose [Symbol.dispose]() is empty.
|
||||||
|
if (transformed.includes("using _")) {
|
||||||
|
transformed = transformed.replace(/\busing\s+(_\w*)\s*=/g, "const $1 =");
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!modified) return null;
|
||||||
|
|
||||||
|
return { code: transformed, map: null };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
25
scripts/vite-plugin-import-meta-require.ts
Normal file
25
scripts/vite-plugin-import-meta-require.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { Plugin } from "rollup";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rollup plugin that replaces `var __require = import.meta.require;`
|
||||||
|
* with a Node.js compatible version that falls back to createRequire
|
||||||
|
* when import.meta.require is not available (e.g. in Node.js runtime).
|
||||||
|
*
|
||||||
|
* This replicates the post-processing done in the original build.ts.
|
||||||
|
*/
|
||||||
|
export default function importMetaRequirePlugin(): Plugin {
|
||||||
|
return {
|
||||||
|
name: "import-meta-require",
|
||||||
|
|
||||||
|
renderChunk(code) {
|
||||||
|
const pattern = "var __require = import.meta.require;";
|
||||||
|
const replacement =
|
||||||
|
'var __require = typeof import.meta.require === "function" ? import.meta.require : (await import("module")).createRequire(import.meta.url);';
|
||||||
|
|
||||||
|
if (code.includes(pattern)) {
|
||||||
|
return code.replace(pattern, replacement);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1184,6 +1184,17 @@ export class QueryEngine {
|
|||||||
this.abortController.abort()
|
this.abortController.abort()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Reset the abort controller so the next submitMessage() call can start
|
||||||
|
* with a fresh, non-aborted signal. Must be called after interrupt(). */
|
||||||
|
resetAbortController(): void {
|
||||||
|
this.abortController = createAbortController()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Expose the current abort signal for external consumers (e.g. ACP bridge). */
|
||||||
|
getAbortSignal(): AbortSignal {
|
||||||
|
return this.abortController.signal
|
||||||
|
}
|
||||||
|
|
||||||
getMessages(): readonly Message[] {
|
getMessages(): readonly Message[] {
|
||||||
return this.mutableMessages
|
return this.mutableMessages
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,6 +277,8 @@ export type ToolUseContext = {
|
|||||||
criticalSystemReminder_EXPERIMENTAL?: string
|
criticalSystemReminder_EXPERIMENTAL?: string
|
||||||
/** Langfuse root trace span for this query turn. Passed down to tool execution for observability. */
|
/** Langfuse root trace span for this query turn. Passed down to tool execution for observability. */
|
||||||
langfuseTrace?: LangfuseSpan | null
|
langfuseTrace?: LangfuseSpan | null
|
||||||
|
/** Langfuse batch span wrapping a concurrent tool group. When set, tool observations are nested under it. */
|
||||||
|
langfuseBatchSpan?: LangfuseSpan | null
|
||||||
/** When true, preserve toolUseResult on messages even for subagents.
|
/** When true, preserve toolUseResult on messages even for subagents.
|
||||||
* Used by in-process teammates whose transcripts are viewable by the user. */
|
* Used by in-process teammates whose transcripts are viewable by the user. */
|
||||||
preserveToolUseResults?: boolean
|
preserveToolUseResults?: boolean
|
||||||
|
|||||||
@@ -132,6 +132,14 @@ async function main(): Promise<void> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fast-path for `--acp` — ACP (Agent Client Protocol) agent mode over stdio.
|
||||||
|
if (feature('ACP') && process.argv[2] === '--acp') {
|
||||||
|
profileCheckpoint('cli_acp_path')
|
||||||
|
const { runAcpAgent } = await import('../services/acp/entry.js')
|
||||||
|
await runAcpAgent()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Fast-path for `--daemon-worker=<kind>` (internal — supervisor spawns this).
|
// Fast-path for `--daemon-worker=<kind>` (internal — supervisor spawns this).
|
||||||
// Must come before the daemon subcommand check: spawned per-worker, so
|
// Must come before the daemon subcommand check: spawned per-worker, so
|
||||||
// perf-sensitive. No enableConfigs(), no analytics sinks at this layer —
|
// perf-sensitive. No enableConfigs(), no analytics sinks at this layer —
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import { isBetaTracingEnabled } from '../utils/telemetry/betaSessionTracing.js'
|
|||||||
import { getTelemetryAttributes } from '../utils/telemetryAttributes.js'
|
import { getTelemetryAttributes } from '../utils/telemetryAttributes.js'
|
||||||
import { setShellIfWindows } from '../utils/windowsPaths.js'
|
import { setShellIfWindows } from '../utils/windowsPaths.js'
|
||||||
import { initSentry } from '../utils/sentry.js'
|
import { initSentry } from '../utils/sentry.js'
|
||||||
|
import { initUser } from '../utils/user.js'
|
||||||
import { initLangfuse, shutdownLangfuse } from '../services/langfuse/index.js'
|
import { initLangfuse, shutdownLangfuse } from '../services/langfuse/index.js'
|
||||||
|
|
||||||
// initialize1PEventLogging is dynamically imported to defer OpenTelemetry sdk-logs/resources
|
// initialize1PEventLogging is dynamically imported to defer OpenTelemetry sdk-logs/resources
|
||||||
@@ -156,6 +157,8 @@ export const init = memoize(async (): Promise<void> => {
|
|||||||
initSentry()
|
initSentry()
|
||||||
|
|
||||||
// Initialize Langfuse tracing (no-op if keys not configured)
|
// Initialize Langfuse tracing (no-op if keys not configured)
|
||||||
|
// Pre-warm user email cache so Langfuse traces include userId
|
||||||
|
await initUser()
|
||||||
initLangfuse()
|
initLangfuse()
|
||||||
registerCleanup(shutdownLangfuse)
|
registerCleanup(shutdownLangfuse)
|
||||||
|
|
||||||
|
|||||||
@@ -130,8 +130,6 @@ export const DEFAULT_BINDINGS: KeybindingBlock[] = [
|
|||||||
{
|
{
|
||||||
context: 'Confirmation',
|
context: 'Confirmation',
|
||||||
bindings: {
|
bindings: {
|
||||||
y: 'confirm:yes',
|
|
||||||
n: 'confirm:no',
|
|
||||||
enter: 'confirm:yes',
|
enter: 'confirm:yes',
|
||||||
escape: 'confirm:no',
|
escape: 'confirm:no',
|
||||||
// Navigation for dialogs with lists
|
// Navigation for dialogs with lists
|
||||||
|
|||||||
12
src/query.ts
12
src/query.ts
@@ -254,12 +254,17 @@ export async function* query(
|
|||||||
}
|
}
|
||||||
: params
|
: params
|
||||||
|
|
||||||
let terminal: Terminal
|
let terminal: Terminal | undefined
|
||||||
try {
|
try {
|
||||||
terminal = yield* queryLoop(paramsWithTrace, consumedCommandUuids)
|
terminal = yield* queryLoop(paramsWithTrace, consumedCommandUuids)
|
||||||
} finally {
|
} finally {
|
||||||
// Only end the trace if we created it — sub-agents own their traces
|
// Only end the trace if we created it — sub-agents own their traces
|
||||||
if (ownsTrace) endTrace(langfuseTrace)
|
if (ownsTrace) {
|
||||||
|
const isAborted =
|
||||||
|
terminal?.reason === 'aborted_streaming' ||
|
||||||
|
terminal?.reason === 'aborted_tools'
|
||||||
|
endTrace(langfuseTrace, undefined, isAborted ? 'interrupted' : undefined)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only reached if queryLoop returned normally. Skipped on throw (error
|
// Only reached if queryLoop returned normally. Skipped on throw (error
|
||||||
@@ -269,7 +274,8 @@ export async function* query(
|
|||||||
for (const uuid of consumedCommandUuids) {
|
for (const uuid of consumedCommandUuids) {
|
||||||
notifyCommandLifecycle(uuid, 'completed')
|
notifyCommandLifecycle(uuid, 'completed')
|
||||||
}
|
}
|
||||||
return terminal
|
// biome-ignore lint/style/noNonNullAssertion: terminal is always assigned when queryLoop returns normally
|
||||||
|
return terminal!
|
||||||
}
|
}
|
||||||
|
|
||||||
async function* queryLoop(
|
async function* queryLoop(
|
||||||
|
|||||||
735
src/services/acp/__tests__/agent.test.ts
Normal file
735
src/services/acp/__tests__/agent.test.ts
Normal file
@@ -0,0 +1,735 @@
|
|||||||
|
import { describe, expect, test, mock, beforeEach } from 'bun:test'
|
||||||
|
|
||||||
|
// ── Heavy module mocks (must be before any import of the module under test) ──
|
||||||
|
|
||||||
|
const mockSetModel = mock(() => {})
|
||||||
|
|
||||||
|
mock.module('../../../QueryEngine.js', () => ({
|
||||||
|
QueryEngine: class MockQueryEngine {
|
||||||
|
submitMessage = mock(async function* () {})
|
||||||
|
interrupt = mock(() => {})
|
||||||
|
resetAbortController = mock(() => {})
|
||||||
|
getAbortSignal = mock(() => new AbortController().signal)
|
||||||
|
setModel = mockSetModel
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module('../../../tools.js', () => ({
|
||||||
|
getTools: mock(() => []),
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module('../../../Tool.js', () => ({
|
||||||
|
getEmptyToolPermissionContext: mock(() => ({})),
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module('../../../utils/config.js', () => ({
|
||||||
|
enableConfigs: mock(() => {}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module('../../../bootstrap/state.js', () => ({
|
||||||
|
setOriginalCwd: mock(() => {}),
|
||||||
|
addSlowOperation: mock(() => {}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockGetDefaultAppState = mock(() => ({
|
||||||
|
toolPermissionContext: {
|
||||||
|
mode: 'default',
|
||||||
|
additionalWorkingDirectories: new Map(),
|
||||||
|
alwaysAllowRules: { user: [], project: [], local: [] },
|
||||||
|
alwaysDenyRules: { user: [], project: [], local: [] },
|
||||||
|
alwaysAskRules: { user: [], project: [], local: [] },
|
||||||
|
isBypassPermissionsModeAvailable: false,
|
||||||
|
},
|
||||||
|
fastMode: false,
|
||||||
|
settings: {},
|
||||||
|
tasks: {},
|
||||||
|
verbose: false,
|
||||||
|
mainLoopModel: null,
|
||||||
|
mainLoopModelForSession: null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module('../../../state/AppStateStore.js', () => ({
|
||||||
|
getDefaultAppState: mockGetDefaultAppState,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module('../../../utils/fileStateCache.js', () => ({
|
||||||
|
FileStateCache: class MockFileStateCache {
|
||||||
|
constructor() {}
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module('../permissions.js', () => ({
|
||||||
|
createAcpCanUseTool: mock(() => mock(async () => ({ behavior: 'allow', updatedInput: {} }))),
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module('../bridge.js', () => ({
|
||||||
|
forwardSessionUpdates: mock(async () => ({ stopReason: 'end_turn' as const })),
|
||||||
|
replayHistoryMessages: mock(async () => {}),
|
||||||
|
toolInfoFromToolUse: mock(() => ({ title: 'Test', kind: 'other', content: [], locations: [] })),
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module('../utils.js', () => ({
|
||||||
|
resolvePermissionMode: mock(() => 'default'),
|
||||||
|
computeSessionFingerprint: mock(() => '{}'),
|
||||||
|
sanitizeTitle: mock((s: string) => s),
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module('../../../utils/listSessionsImpl.js', () => ({
|
||||||
|
listSessionsImpl: mock(async () => []),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockGetMainLoopModel = mock(() => 'claude-sonnet-4-6')
|
||||||
|
|
||||||
|
mock.module('../../../utils/model/model.js', () => ({
|
||||||
|
getMainLoopModel: mockGetMainLoopModel,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module('../../../utils/model/modelOptions.ts', () => ({
|
||||||
|
getModelOptions: mock(() => []),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockApplySafeEnvVars = mock(() => {})
|
||||||
|
mock.module('../../../utils/managedEnv.js', () => ({
|
||||||
|
applySafeConfigEnvironmentVariables: mockApplySafeEnvVars,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockDeserializeMessages = mock((msgs: unknown[]) => msgs)
|
||||||
|
const mockGetLastSessionLog = mock(async () => null)
|
||||||
|
const mockSessionIdExists = mock(() => false)
|
||||||
|
|
||||||
|
mock.module('../../../utils/conversationRecovery.js', () => ({
|
||||||
|
deserializeMessages: mockDeserializeMessages,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module('../../../utils/sessionStorage.js', () => ({
|
||||||
|
getLastSessionLog: mockGetLastSessionLog,
|
||||||
|
sessionIdExists: mockSessionIdExists,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockGetCommands = mock(async () => [
|
||||||
|
{
|
||||||
|
name: 'commit',
|
||||||
|
description: 'Create a git commit',
|
||||||
|
type: 'prompt',
|
||||||
|
userInvocable: true,
|
||||||
|
isHidden: false,
|
||||||
|
argumentHint: '[message]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'compact',
|
||||||
|
description: 'Compact conversation',
|
||||||
|
type: 'local',
|
||||||
|
userInvocable: true,
|
||||||
|
isHidden: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hidden-skill',
|
||||||
|
description: 'Hidden skill',
|
||||||
|
type: 'prompt',
|
||||||
|
userInvocable: false,
|
||||||
|
isHidden: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
mock.module('../../../commands.js', () => ({
|
||||||
|
getCommands: mockGetCommands,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ── Import after mocks ────────────────────────────────────────────
|
||||||
|
|
||||||
|
const { AcpAgent } = await import('../agent.js')
|
||||||
|
const { forwardSessionUpdates } = await import('../bridge.js')
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function makeConn() {
|
||||||
|
return {
|
||||||
|
sessionUpdate: mock(async () => {}),
|
||||||
|
requestPermission: mock(async () => ({ outcome: { outcome: 'cancelled' } })),
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('AcpAgent', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockSetModel.mockClear()
|
||||||
|
mockGetMainLoopModel.mockClear()
|
||||||
|
mockGetDefaultAppState.mockClear()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('initialize', () => {
|
||||||
|
test('returns protocol version and agent info', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const res = await agent.initialize({} as any)
|
||||||
|
expect(res.protocolVersion).toBeDefined()
|
||||||
|
expect(res.agentInfo?.name).toBe('claude-code')
|
||||||
|
expect(typeof res.agentInfo?.version).toBe('string')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('advertises image and embeddedContext capability', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const res = await agent.initialize({} as any)
|
||||||
|
expect(res.agentCapabilities?.promptCapabilities?.image).toBe(true)
|
||||||
|
expect(res.agentCapabilities?.promptCapabilities?.embeddedContext).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('loadSession capability is true', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const res = await agent.initialize({} as any)
|
||||||
|
expect(res.agentCapabilities?.loadSession).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('session capabilities include fork, list, resume, close', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const res = await agent.initialize({} as any)
|
||||||
|
expect(res.agentCapabilities?.sessionCapabilities).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticate', () => {
|
||||||
|
test('returns empty object (no auth required)', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const res = await agent.authenticate({} as any)
|
||||||
|
expect(res).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('newSession', () => {
|
||||||
|
test('returns a sessionId string', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const res = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
expect(typeof res.sessionId).toBe('string')
|
||||||
|
expect(res.sessionId.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns modes and models', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const res = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
expect(res.modes).toBeDefined()
|
||||||
|
expect(res.models).toBeDefined()
|
||||||
|
expect(res.configOptions).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('each call returns a unique sessionId', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const r1 = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
const r2 = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
expect(r1.sessionId).not.toBe(r2.sessionId)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('calls getDefaultAppState to build session appState', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
expect(mockGetDefaultAppState).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('calls getMainLoopModel to resolve current model', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const res = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
expect(mockGetMainLoopModel).toHaveBeenCalled()
|
||||||
|
// The model reported to ACP client should match what getMainLoopModel returns
|
||||||
|
expect(res.models?.currentModelId).toBe('claude-sonnet-4-6')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('calls queryEngine.setModel with resolved model', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
expect(mockSetModel).toHaveBeenCalledWith('claude-sonnet-4-6')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('respects model alias resolution via getMainLoopModel', async () => {
|
||||||
|
// Simulate a mapped model (e.g., "opus" → "glm-5.1" via ANTHROPIC_DEFAULT_OPUS_MODEL)
|
||||||
|
mockGetMainLoopModel.mockReturnValueOnce('glm-5.1')
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const res = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
expect(res.models?.currentModelId).toBe('glm-5.1')
|
||||||
|
expect(mockSetModel).toHaveBeenCalledWith('glm-5.1')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('stores clientCapabilities from initialize', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
await agent.initialize({ clientCapabilities: { _meta: { terminal_output: true } } } as any)
|
||||||
|
const res = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
// Should not throw — clientCapabilities stored internally
|
||||||
|
expect(res.sessionId).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('prompt', () => {
|
||||||
|
test('throws when session not found', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
await expect(
|
||||||
|
agent.prompt({ sessionId: 'nonexistent', prompt: [] } as any)
|
||||||
|
).rejects.toThrow('nonexistent')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns end_turn for empty prompt text', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
const res = await agent.prompt({ sessionId, prompt: [] } as any)
|
||||||
|
expect(res.stopReason).toBe('end_turn')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns end_turn for whitespace-only prompt', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
const res = await agent.prompt({
|
||||||
|
sessionId,
|
||||||
|
prompt: [{ type: 'text', text: ' ' }],
|
||||||
|
} as any)
|
||||||
|
expect(res.stopReason).toBe('end_turn')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('calls forwardSessionUpdates for valid prompt', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockResolvedValueOnce({ stopReason: 'end_turn' })
|
||||||
|
const res = await agent.prompt({
|
||||||
|
sessionId,
|
||||||
|
prompt: [{ type: 'text', text: 'hello' }],
|
||||||
|
} as any)
|
||||||
|
expect(res.stopReason).toBe('end_turn')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('cancel before prompt does not block next prompt', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
// Cancel when nothing is running is a no-op
|
||||||
|
await agent.cancel({ sessionId } as any)
|
||||||
|
// The next prompt should work normally
|
||||||
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockResolvedValueOnce({ stopReason: 'end_turn' })
|
||||||
|
const res = await agent.prompt({
|
||||||
|
sessionId,
|
||||||
|
prompt: [{ type: 'text', text: 'hello' }],
|
||||||
|
} as any)
|
||||||
|
expect(res.stopReason).toBe('end_turn')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('cancel during prompt returns cancelled', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
// Start a prompt that hangs, then cancel it
|
||||||
|
let resolveStream!: () => void
|
||||||
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockImplementationOnce(
|
||||||
|
() => new Promise<{ stopReason: string }>((resolve) => {
|
||||||
|
resolveStream = () => resolve({ stopReason: 'cancelled' })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const promptPromise = agent.prompt({
|
||||||
|
sessionId,
|
||||||
|
prompt: [{ type: 'text', text: 'hello' }],
|
||||||
|
} as any)
|
||||||
|
// Cancel the running prompt
|
||||||
|
await agent.cancel({ sessionId } as any)
|
||||||
|
resolveStream()
|
||||||
|
const res = await promptPromise
|
||||||
|
// After fix, forwardSessionUpdates mock controls the result
|
||||||
|
expect(res.stopReason).toBe('cancelled')
|
||||||
|
|
||||||
|
// Next prompt should work normally
|
||||||
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockResolvedValueOnce({ stopReason: 'end_turn' })
|
||||||
|
const res2 = await agent.prompt({
|
||||||
|
sessionId,
|
||||||
|
prompt: [{ type: 'text', text: 'world' }],
|
||||||
|
} as any)
|
||||||
|
expect(res2.stopReason).toBe('end_turn')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns end_turn on unexpected error', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockImplementationOnce(async () => {
|
||||||
|
throw new Error('unexpected')
|
||||||
|
})
|
||||||
|
const res = await agent.prompt({
|
||||||
|
sessionId,
|
||||||
|
prompt: [{ type: 'text', text: 'hello' }],
|
||||||
|
} as any)
|
||||||
|
expect(res.stopReason).toBe('end_turn')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns usage from forwardSessionUpdates', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockResolvedValueOnce({
|
||||||
|
stopReason: 'end_turn',
|
||||||
|
usage: {
|
||||||
|
inputTokens: 100,
|
||||||
|
outputTokens: 50,
|
||||||
|
cachedReadTokens: 10,
|
||||||
|
cachedWriteTokens: 5,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const res = await agent.prompt({
|
||||||
|
sessionId,
|
||||||
|
prompt: [{ type: 'text', text: 'hello' }],
|
||||||
|
} as any)
|
||||||
|
expect(res.usage).toBeDefined()
|
||||||
|
expect(res.usage!.inputTokens).toBe(100)
|
||||||
|
expect(res.usage!.outputTokens).toBe(50)
|
||||||
|
expect(res.usage!.totalTokens).toBe(165)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('cancel', () => {
|
||||||
|
test('does not throw for unknown session', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
await expect(agent.cancel({ sessionId: 'ghost' } as any)).resolves.toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('closeSession', () => {
|
||||||
|
test('throws for unknown session', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
await expect(agent.unstable_closeSession({ sessionId: 'ghost' } as any)).rejects.toThrow('Session not found')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('removes session after close', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
await agent.unstable_closeSession({ sessionId } as any)
|
||||||
|
expect(agent.sessions.has(sessionId)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('setSessionModel', () => {
|
||||||
|
test('updates model on queryEngine', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
mockSetModel.mockClear()
|
||||||
|
await agent.unstable_setSessionModel({ sessionId, modelId: 'glm-5.1' } as any)
|
||||||
|
expect(mockSetModel).toHaveBeenCalledWith('glm-5.1')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('passes alias modelId to queryEngine as-is for later resolution', async () => {
|
||||||
|
// "sonnet[1m]" is stored raw — QueryEngine.submitMessage() calls
|
||||||
|
// parseUserSpecifiedModel() which resolves aliases via env vars
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
mockSetModel.mockClear()
|
||||||
|
await agent.unstable_setSessionModel({ sessionId, modelId: 'sonnet[1m]' } as any)
|
||||||
|
expect(mockSetModel).toHaveBeenCalledWith('sonnet[1m]')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('entry.ts initialization contract', () => {
|
||||||
|
test('entry.ts imports applySafeConfigEnvironmentVariables from managedEnv', async () => {
|
||||||
|
// Verify the module import exists — this catches if entry.ts forgets
|
||||||
|
// to import applySafeConfigEnvironmentVariables
|
||||||
|
const entrySource = await Bun.file(
|
||||||
|
new URL('../entry.ts', import.meta.url),
|
||||||
|
).text()
|
||||||
|
expect(entrySource).toContain('applySafeConfigEnvironmentVariables')
|
||||||
|
expect(entrySource).toContain('enableConfigs')
|
||||||
|
|
||||||
|
// Verify applySafe is called after enableConfigs in the source
|
||||||
|
const enableIdx = entrySource.indexOf('enableConfigs()')
|
||||||
|
const applyIdx = entrySource.indexOf('applySafeConfigEnvironmentVariables()')
|
||||||
|
expect(enableIdx).toBeGreaterThan(-1)
|
||||||
|
expect(applyIdx).toBeGreaterThan(-1)
|
||||||
|
expect(enableIdx).toBeLessThan(applyIdx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('prompt usage tracking', () => {
|
||||||
|
test('returns totalTokens as sum of all token types', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockResolvedValueOnce({
|
||||||
|
stopReason: 'end_turn',
|
||||||
|
usage: {
|
||||||
|
inputTokens: 100,
|
||||||
|
outputTokens: 50,
|
||||||
|
cachedReadTokens: 10,
|
||||||
|
cachedWriteTokens: 5,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const res = await agent.prompt({
|
||||||
|
sessionId,
|
||||||
|
prompt: [{ type: 'text', text: 'hello' }],
|
||||||
|
} as any)
|
||||||
|
expect(res.usage).toBeDefined()
|
||||||
|
expect(res.usage!.totalTokens).toBe(165)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns undefined usage when forwardSessionUpdates returns none', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockResolvedValueOnce({
|
||||||
|
stopReason: 'end_turn',
|
||||||
|
})
|
||||||
|
const res = await agent.prompt({
|
||||||
|
sessionId,
|
||||||
|
prompt: [{ type: 'text', text: 'hello' }],
|
||||||
|
} as any)
|
||||||
|
expect(res.usage).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('prompt error handling', () => {
|
||||||
|
test('returns cancelled when session was cancelled during prompt', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockImplementationOnce(async () => {
|
||||||
|
// Simulate cancel happening during forward
|
||||||
|
const session = agent.sessions.get(sessionId)
|
||||||
|
if (session) session.cancelled = true
|
||||||
|
return { stopReason: 'end_turn' }
|
||||||
|
})
|
||||||
|
const res = await agent.prompt({
|
||||||
|
sessionId,
|
||||||
|
prompt: [{ type: 'text', text: 'hello' }],
|
||||||
|
} as any)
|
||||||
|
expect(res.stopReason).toBe('cancelled')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns cancelled on cancel after error', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockImplementationOnce(async () => {
|
||||||
|
const session = agent.sessions.get(sessionId)
|
||||||
|
if (session) session.cancelled = true
|
||||||
|
throw new Error('unexpected')
|
||||||
|
})
|
||||||
|
const res = await agent.prompt({
|
||||||
|
sessionId,
|
||||||
|
prompt: [{ type: 'text', text: 'hello' }],
|
||||||
|
} as any)
|
||||||
|
expect(res.stopReason).toBe('cancelled')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('resumeSession', () => {
|
||||||
|
test('creates new session with the requested sessionId when not in memory', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const requestedId = 'e73e9b66-9637-4477-b512-af45357b1dcb'
|
||||||
|
const res = await agent.unstable_resumeSession({
|
||||||
|
sessionId: requestedId,
|
||||||
|
cwd: '/tmp',
|
||||||
|
mcpServers: [],
|
||||||
|
} as any)
|
||||||
|
// The session must be stored under the requested ID
|
||||||
|
expect(agent.sessions.has(requestedId)).toBe(true)
|
||||||
|
// Response should have modes/models/configOptions
|
||||||
|
expect(res.modes).toBeDefined()
|
||||||
|
expect(res.models).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('reuses existing session when sessionId matches and fingerprint unchanged', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const res1 = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
const sid = res1.sessionId
|
||||||
|
const originalSession = agent.sessions.get(sid)
|
||||||
|
// Resume with same params
|
||||||
|
const res2 = await agent.unstable_resumeSession({
|
||||||
|
sessionId: sid,
|
||||||
|
cwd: '/tmp',
|
||||||
|
mcpServers: [],
|
||||||
|
} as any)
|
||||||
|
// Same session object — not recreated
|
||||||
|
expect(agent.sessions.get(sid)).toBe(originalSession)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('can prompt after resumeSession with previously unknown sessionId', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const sid = 'restored-session-id-1234'
|
||||||
|
await agent.unstable_resumeSession({
|
||||||
|
sessionId: sid,
|
||||||
|
cwd: '/tmp',
|
||||||
|
mcpServers: [],
|
||||||
|
} as any)
|
||||||
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockResolvedValueOnce({ stopReason: 'end_turn' })
|
||||||
|
const res = await agent.prompt({
|
||||||
|
sessionId: sid,
|
||||||
|
prompt: [{ type: 'text', text: 'hello after restore' }],
|
||||||
|
} as any)
|
||||||
|
expect(res.stopReason).toBe('end_turn')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('loadSession', () => {
|
||||||
|
test('creates new session with the requested sessionId', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const requestedId = 'aaaa-bbbb-cccc'
|
||||||
|
await agent.loadSession({
|
||||||
|
sessionId: requestedId,
|
||||||
|
cwd: '/tmp',
|
||||||
|
mcpServers: [],
|
||||||
|
} as any)
|
||||||
|
expect(agent.sessions.has(requestedId)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('can prompt after loadSession', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const sid = 'loaded-session-id'
|
||||||
|
await agent.loadSession({
|
||||||
|
sessionId: sid,
|
||||||
|
cwd: '/tmp',
|
||||||
|
mcpServers: [],
|
||||||
|
} as any)
|
||||||
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockResolvedValueOnce({ stopReason: 'end_turn' })
|
||||||
|
const res = await agent.prompt({
|
||||||
|
sessionId: sid,
|
||||||
|
prompt: [{ type: 'text', text: 'hello after load' }],
|
||||||
|
} as any)
|
||||||
|
expect(res.stopReason).toBe('end_turn')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('forkSession', () => {
|
||||||
|
test('returns a different sessionId from any existing', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const original = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
const forked = await agent.unstable_forkSession({
|
||||||
|
cwd: '/tmp',
|
||||||
|
mcpServers: [],
|
||||||
|
} as any)
|
||||||
|
expect(forked.sessionId).not.toBe(original.sessionId)
|
||||||
|
expect(agent.sessions.has(forked.sessionId)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('setSessionMode', () => {
|
||||||
|
test('updates current mode on the session', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
await agent.setSessionMode({ sessionId, modeId: 'auto' } as any)
|
||||||
|
const session = agent.sessions.get(sessionId)
|
||||||
|
expect(session?.modes.currentModeId).toBe('auto')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('throws for invalid mode', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
await expect(
|
||||||
|
agent.setSessionMode({ sessionId, modeId: 'invalid_mode' } as any),
|
||||||
|
).rejects.toThrow('Invalid mode')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('throws for unknown session', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
await expect(
|
||||||
|
agent.setSessionMode({ sessionId: 'ghost', modeId: 'auto' } as any),
|
||||||
|
).rejects.toThrow('Session not found')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('setSessionConfigOption', () => {
|
||||||
|
test('throws for unknown config option', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
await expect(
|
||||||
|
agent.setSessionConfigOption({
|
||||||
|
sessionId,
|
||||||
|
configId: 'nonexistent',
|
||||||
|
value: 'x',
|
||||||
|
} as any),
|
||||||
|
).rejects.toThrow('Unknown config option')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('throws for non-string value', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
await expect(
|
||||||
|
agent.setSessionConfigOption({
|
||||||
|
sessionId,
|
||||||
|
configId: 'mode',
|
||||||
|
value: 42,
|
||||||
|
} as any),
|
||||||
|
).rejects.toThrow('Invalid value')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('prompt queueing', () => {
|
||||||
|
test('queued prompts execute in order after current prompt finishes', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
|
||||||
|
// First prompt hangs
|
||||||
|
let resolveFirst!: () => void
|
||||||
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockImplementationOnce(
|
||||||
|
() => new Promise<{ stopReason: string }>((resolve) => {
|
||||||
|
resolveFirst = () => resolve({ stopReason: 'end_turn' })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
// Second prompt resolves normally
|
||||||
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockResolvedValueOnce({ stopReason: 'end_turn' })
|
||||||
|
|
||||||
|
const p1 = agent.prompt({ sessionId, prompt: [{ type: 'text', text: 'first' }] } as any)
|
||||||
|
const p2 = agent.prompt({ sessionId, prompt: [{ type: 'text', text: 'second' }] } as any)
|
||||||
|
|
||||||
|
// Resolve the first prompt to unblock the second
|
||||||
|
resolveFirst()
|
||||||
|
const [r1, r2] = await Promise.all([p1, p2])
|
||||||
|
expect(r1.stopReason).toBe('end_turn')
|
||||||
|
expect(r2.stopReason).toBe('end_turn')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('queued prompts return cancelled when session is cancelled', async () => {
|
||||||
|
const agent = new AcpAgent(makeConn())
|
||||||
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
|
||||||
|
// First prompt hangs
|
||||||
|
let resolveFirst!: () => void
|
||||||
|
;(forwardSessionUpdates as ReturnType<typeof mock>).mockImplementationOnce(
|
||||||
|
() => new Promise<{ stopReason: string }>((resolve) => {
|
||||||
|
resolveFirst = () => resolve({ stopReason: 'end_turn' })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const p1 = agent.prompt({ sessionId, prompt: [{ type: 'text', text: 'first' }] } as any)
|
||||||
|
const p2 = agent.prompt({ sessionId, prompt: [{ type: 'text', text: 'second' }] } as any)
|
||||||
|
|
||||||
|
// Cancel while first is running — both should be cancelled
|
||||||
|
await agent.cancel({ sessionId } as any)
|
||||||
|
resolveFirst()
|
||||||
|
const [r1, r2] = await Promise.all([p1, p2])
|
||||||
|
expect(r1.stopReason).toBe('cancelled')
|
||||||
|
expect(r2.stopReason).toBe('cancelled')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('commands', () => {
|
||||||
|
test('sends filtered prompt-type commands to client', async () => {
|
||||||
|
const conn = makeConn()
|
||||||
|
const agent = new AcpAgent(conn)
|
||||||
|
await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
|
||||||
|
// Wait for setTimeout-based sendAvailableCommandsUpdate
|
||||||
|
await new Promise(r => setTimeout(r, 10))
|
||||||
|
|
||||||
|
const calls = (conn.sessionUpdate as ReturnType<typeof mock>).mock.calls
|
||||||
|
const cmdUpdate = calls.find((c: any[]) => {
|
||||||
|
const update = c[0]?.update
|
||||||
|
return update?.sessionUpdate === 'available_commands_update'
|
||||||
|
})
|
||||||
|
expect(cmdUpdate).toBeDefined()
|
||||||
|
|
||||||
|
const cmds = (cmdUpdate as any[])[0].update.availableCommands
|
||||||
|
// Only prompt-type, non-hidden, userInvocable commands
|
||||||
|
const names = cmds.map((c: any) => c.name)
|
||||||
|
expect(names).toContain('commit')
|
||||||
|
expect(names).not.toContain('compact') // type: 'local'
|
||||||
|
expect(names).not.toContain('hidden-skill') // isHidden: true, userInvocable: false
|
||||||
|
})
|
||||||
|
|
||||||
|
test('maps argumentHint to input.hint', async () => {
|
||||||
|
const conn = makeConn()
|
||||||
|
const agent = new AcpAgent(conn)
|
||||||
|
await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
|
|
||||||
|
await new Promise(r => setTimeout(r, 10))
|
||||||
|
|
||||||
|
const calls = (conn.sessionUpdate as ReturnType<typeof mock>).mock.calls
|
||||||
|
const cmdUpdate = calls.find((c: any[]) => {
|
||||||
|
const update = c[0]?.update
|
||||||
|
return update?.sessionUpdate === 'available_commands_update'
|
||||||
|
})
|
||||||
|
const commit = (cmdUpdate as any[])[0].update.availableCommands.find(
|
||||||
|
(c: any) => c.name === 'commit',
|
||||||
|
)
|
||||||
|
expect(commit.input).toEqual({ hint: '[message]' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
677
src/services/acp/__tests__/bridge.test.ts
Normal file
677
src/services/acp/__tests__/bridge.test.ts
Normal file
@@ -0,0 +1,677 @@
|
|||||||
|
import { describe, expect, test, mock } from 'bun:test'
|
||||||
|
import {
|
||||||
|
toolInfoFromToolUse,
|
||||||
|
toolUpdateFromToolResult,
|
||||||
|
toolUpdateFromEditToolResponse,
|
||||||
|
forwardSessionUpdates,
|
||||||
|
} from '../bridge.js'
|
||||||
|
import { markdownEscape, toDisplayPath } from '../utils.js'
|
||||||
|
import type { AgentSideConnection, ToolKind } from '@agentclientprotocol/sdk'
|
||||||
|
import type { SDKMessage } from '../../../entrypoints/sdk/coreTypes.js'
|
||||||
|
|
||||||
|
// ── Helpers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function makeConn(overrides: Partial<AgentSideConnection> = {}): AgentSideConnection {
|
||||||
|
return {
|
||||||
|
sessionUpdate: mock(async () => {}),
|
||||||
|
requestPermission: mock(async () => ({ outcome: { outcome: 'cancelled' } }) as any),
|
||||||
|
...overrides,
|
||||||
|
} as unknown as AgentSideConnection
|
||||||
|
}
|
||||||
|
|
||||||
|
async function* makeStream(msgs: SDKMessage[]): AsyncGenerator<SDKMessage, void, unknown> {
|
||||||
|
for (const m of msgs) yield m
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── toolInfoFromToolUse ────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('toolInfoFromToolUse', () => {
|
||||||
|
const kindCases: Array<[string, ToolKind]> = [
|
||||||
|
['Read', 'read'],
|
||||||
|
['Edit', 'edit'],
|
||||||
|
['Write', 'edit'],
|
||||||
|
['Bash', 'execute'],
|
||||||
|
['Glob', 'search'],
|
||||||
|
['Grep', 'search'],
|
||||||
|
['WebFetch', 'fetch'],
|
||||||
|
['WebSearch', 'fetch'],
|
||||||
|
['Agent', 'think'],
|
||||||
|
['Task', 'think'],
|
||||||
|
['TodoWrite', 'think'],
|
||||||
|
['ExitPlanMode', 'switch_mode'],
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const [name, expected] of kindCases) {
|
||||||
|
test(`${name} → ${expected}`, () => {
|
||||||
|
const info = toolInfoFromToolUse({ name, id: 'test', input: {} })
|
||||||
|
expect(info.kind).toBe(expected)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
test('unknown tool name → other', () => {
|
||||||
|
expect(toolInfoFromToolUse({ name: 'SomeFancyTool', id: 'x', input: {} }).kind).toBe('other' as ToolKind)
|
||||||
|
expect(toolInfoFromToolUse({ name: '', id: 'x', input: {} }).kind).toBe('other' as ToolKind)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Bash ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test('Bash with command → title shows command', () => {
|
||||||
|
const info = toolInfoFromToolUse({ name: 'Bash', id: 'x', input: { command: 'ls -la', description: 'List files' } })
|
||||||
|
expect(info.title).toBe('ls -la')
|
||||||
|
expect(info.content).toEqual([
|
||||||
|
{ type: 'content', content: { type: 'text', text: 'List files' } },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Bash with terminalOutput → returns terminalId content', () => {
|
||||||
|
const info = toolInfoFromToolUse(
|
||||||
|
{ name: 'Bash', id: 'tu_123', input: { command: 'ls' } },
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
expect(info.kind).toBe('execute')
|
||||||
|
expect(info.content).toEqual([{ type: 'terminal', terminalId: 'tu_123' }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Bash without description → empty content', () => {
|
||||||
|
const info = toolInfoFromToolUse({ name: 'Bash', id: 'x', input: { command: 'ls' } })
|
||||||
|
expect(info.content).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Glob ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test('Glob with pattern → title shows Find', () => {
|
||||||
|
const info = toolInfoFromToolUse({ name: 'Glob', id: 'x', input: { pattern: '*/**.ts' } })
|
||||||
|
expect(info.title).toBe('Find `*/**.ts`')
|
||||||
|
expect(info.locations).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Glob with path → locations include path', () => {
|
||||||
|
const info = toolInfoFromToolUse({ name: 'Glob', id: 'x', input: { pattern: '*.ts', path: '/src' } })
|
||||||
|
expect(info.title).toBe('Find `/src` `*.ts`')
|
||||||
|
expect(info.locations).toEqual([{ path: '/src' }])
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Task/Agent ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test('Task with description and prompt → content has prompt text', () => {
|
||||||
|
const info = toolInfoFromToolUse({
|
||||||
|
name: 'Task',
|
||||||
|
id: 'x',
|
||||||
|
input: { description: 'Handle task', prompt: 'Do the work' },
|
||||||
|
})
|
||||||
|
expect(info.title).toBe('Handle task')
|
||||||
|
expect(info.content).toEqual([
|
||||||
|
{ type: 'content', content: { type: 'text', text: 'Do the work' } },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Grep ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test('Grep with full flags', () => {
|
||||||
|
const info = toolInfoFromToolUse({
|
||||||
|
name: 'Grep',
|
||||||
|
id: 'x',
|
||||||
|
input: {
|
||||||
|
pattern: 'todo',
|
||||||
|
path: '/src',
|
||||||
|
'-i': true,
|
||||||
|
'-n': true,
|
||||||
|
'-A': 3,
|
||||||
|
'-B': 2,
|
||||||
|
'-C': 5,
|
||||||
|
head_limit: 10,
|
||||||
|
glob: '*.ts',
|
||||||
|
type: 'js',
|
||||||
|
multiline: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(info.title).toContain('-i')
|
||||||
|
expect(info.title).toContain('-n')
|
||||||
|
expect(info.title).toContain('-A 3')
|
||||||
|
expect(info.title).toContain('-B 2')
|
||||||
|
expect(info.title).toContain('-C 5')
|
||||||
|
expect(info.title).toContain('| head -10')
|
||||||
|
expect(info.title).toContain('--include="*.ts"')
|
||||||
|
expect(info.title).toContain('--type=js')
|
||||||
|
expect(info.title).toContain('-P')
|
||||||
|
expect(info.title).toContain('"todo"')
|
||||||
|
expect(info.title).toContain('/src')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Grep with files_with_matches → -l', () => {
|
||||||
|
const info = toolInfoFromToolUse({
|
||||||
|
name: 'Grep',
|
||||||
|
id: 'x',
|
||||||
|
input: { pattern: 'foo', output_mode: 'files_with_matches' },
|
||||||
|
})
|
||||||
|
expect(info.title).toContain('-l')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Grep with count → -c', () => {
|
||||||
|
const info = toolInfoFromToolUse({
|
||||||
|
name: 'Grep',
|
||||||
|
id: 'x',
|
||||||
|
input: { pattern: 'foo', output_mode: 'count' },
|
||||||
|
})
|
||||||
|
expect(info.title).toContain('-c')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Write ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test('Write with file_path and content → diff content', () => {
|
||||||
|
const info = toolInfoFromToolUse({
|
||||||
|
name: 'Write',
|
||||||
|
id: 'x',
|
||||||
|
input: { file_path: '/Users/test/project/example.txt', content: 'Hello, World!\nThis is test content.' },
|
||||||
|
})
|
||||||
|
expect(info.kind).toBe('edit')
|
||||||
|
expect(info.title).toBe('Write /Users/test/project/example.txt')
|
||||||
|
expect(info.content).toEqual([
|
||||||
|
{
|
||||||
|
type: 'diff',
|
||||||
|
path: '/Users/test/project/example.txt',
|
||||||
|
oldText: null,
|
||||||
|
newText: 'Hello, World!\nThis is test content.',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
expect(info.locations).toEqual([{ path: '/Users/test/project/example.txt' }])
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Edit ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test('Edit with file_path → diff content', () => {
|
||||||
|
const info = toolInfoFromToolUse({
|
||||||
|
name: 'Edit',
|
||||||
|
id: 'x',
|
||||||
|
input: { file_path: '/Users/test/project/test.txt', old_string: 'old text', new_string: 'new text' },
|
||||||
|
})
|
||||||
|
expect(info.kind).toBe('edit')
|
||||||
|
expect(info.title).toBe('Edit /Users/test/project/test.txt')
|
||||||
|
expect(info.content).toEqual([
|
||||||
|
{
|
||||||
|
type: 'diff',
|
||||||
|
path: '/Users/test/project/test.txt',
|
||||||
|
oldText: 'old text',
|
||||||
|
newText: 'new text',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Edit without file_path → empty content', () => {
|
||||||
|
const info = toolInfoFromToolUse({ name: 'Edit', id: 'x', input: {} })
|
||||||
|
expect(info.title).toBe('Edit')
|
||||||
|
expect(info.content).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Read ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test('Read with file_path → locations include path and line 1', () => {
|
||||||
|
const info = toolInfoFromToolUse({ name: 'Read', id: 'x', input: { file_path: '/src/foo.ts' } })
|
||||||
|
expect(info.locations).toEqual([{ path: '/src/foo.ts', line: 1 }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Read with limit', () => {
|
||||||
|
const info = toolInfoFromToolUse({ name: 'Read', id: 'x', input: { file_path: '/large.txt', limit: 100 } })
|
||||||
|
expect(info.title).toContain('(1 - 100)')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Read with offset and limit', () => {
|
||||||
|
const info = toolInfoFromToolUse({ name: 'Read', id: 'x', input: { file_path: '/large.txt', offset: 50, limit: 100 } })
|
||||||
|
expect(info.title).toContain('(50 - 149)')
|
||||||
|
expect(info.locations).toEqual([{ path: '/large.txt', line: 50 }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Read with only offset', () => {
|
||||||
|
const info = toolInfoFromToolUse({ name: 'Read', id: 'x', input: { file_path: '/large.txt', offset: 200 } })
|
||||||
|
expect(info.title).toContain('(from line 200)')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Read with cwd → relative path in title, absolute in locations', () => {
|
||||||
|
const info = toolInfoFromToolUse(
|
||||||
|
{ name: 'Read', id: 'x', input: { file_path: '/Users/test/project/src/main.ts' } },
|
||||||
|
false,
|
||||||
|
'/Users/test/project',
|
||||||
|
)
|
||||||
|
expect(info.title).toBe('Read src/main.ts')
|
||||||
|
expect(info.locations).toEqual([{ path: '/Users/test/project/src/main.ts', line: 1 }])
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── WebSearch ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test('WebSearch with allowed/blocked domains', () => {
|
||||||
|
const info = toolInfoFromToolUse({
|
||||||
|
name: 'WebSearch',
|
||||||
|
id: 'x',
|
||||||
|
input: { query: 'test', allowed_domains: ['a.com'], blocked_domains: ['b.com'] },
|
||||||
|
})
|
||||||
|
expect(info.title).toContain('allowed: a.com')
|
||||||
|
expect(info.title).toContain('blocked: b.com')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── TodoWrite ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test('TodoWrite with todos array → title shows content', () => {
|
||||||
|
const info = toolInfoFromToolUse({
|
||||||
|
name: 'TodoWrite',
|
||||||
|
id: 'x',
|
||||||
|
input: { todos: [{ content: 'Task 1' }, { content: 'Task 2' }] },
|
||||||
|
})
|
||||||
|
expect(info.title).toContain('Task 1')
|
||||||
|
expect(info.title).toContain('Task 2')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── ExitPlanMode ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
test('ExitPlanMode with plan → content has plan text', () => {
|
||||||
|
const info = toolInfoFromToolUse({
|
||||||
|
name: 'ExitPlanMode',
|
||||||
|
id: 'x',
|
||||||
|
input: { plan: 'Do the thing' },
|
||||||
|
})
|
||||||
|
expect(info.title).toBe('Ready to code?')
|
||||||
|
expect(info.content).toEqual([
|
||||||
|
{ type: 'content', content: { type: 'text', text: 'Do the thing' } },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── toolUpdateFromToolResult ───────────────────────────────────────
|
||||||
|
|
||||||
|
describe('toolUpdateFromToolResult', () => {
|
||||||
|
test('returns empty for Edit success', () => {
|
||||||
|
const result = toolUpdateFromToolResult(
|
||||||
|
{ content: [{ type: 'text', text: 'The file has been edited' }], is_error: false, tool_use_id: 't1' },
|
||||||
|
{ name: 'Edit', id: 't1' },
|
||||||
|
)
|
||||||
|
expect(result).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns error content for Edit failure', () => {
|
||||||
|
const result = toolUpdateFromToolResult(
|
||||||
|
{ content: [{ type: 'text', text: 'Failed to find `old_string`' }], is_error: true, tool_use_id: 't1' },
|
||||||
|
{ name: 'Edit', id: 't1' },
|
||||||
|
)
|
||||||
|
expect(result.content).toEqual([
|
||||||
|
{ type: 'content', content: { type: 'text', text: '```\nFailed to find `old_string`\n```' } },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns markdown-escaped content for Read', () => {
|
||||||
|
const result = toolUpdateFromToolResult(
|
||||||
|
{ content: 'let x = 1', is_error: false, tool_use_id: 't1' },
|
||||||
|
{ name: 'Read', id: 't1' },
|
||||||
|
)
|
||||||
|
expect(result.content).toBeDefined()
|
||||||
|
expect(result.content![0].type).toBe('content')
|
||||||
|
// Should be wrapped in markdown code fence
|
||||||
|
const text = (result.content![0] as { type: string; content: { type: string; text: string } }).content.text
|
||||||
|
expect(text).toContain('```')
|
||||||
|
expect(text).toContain('let x = 1')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns console block for Bash output', () => {
|
||||||
|
const result = toolUpdateFromToolResult(
|
||||||
|
{ content: [{ type: 'text', text: 'hello world' }], is_error: false, tool_use_id: 't1' },
|
||||||
|
{ name: 'Bash', id: 't1' },
|
||||||
|
)
|
||||||
|
expect(result.content).toEqual([
|
||||||
|
{ type: 'content', content: { type: 'text', text: '```console\nhello world\n```' } },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns terminal metadata for Bash with terminalOutput', () => {
|
||||||
|
const result = toolUpdateFromToolResult(
|
||||||
|
{ content: [{ type: 'text', text: 'output' }], is_error: false, tool_use_id: 't1' },
|
||||||
|
{ name: 'Bash', id: 't1' },
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
expect(result.content).toEqual([{ type: 'terminal', terminalId: 't1' }])
|
||||||
|
expect(result._meta).toBeDefined()
|
||||||
|
expect((result._meta as Record<string, unknown>).terminal_info).toEqual({ terminal_id: 't1' })
|
||||||
|
expect((result._meta as Record<string, unknown>).terminal_output).toEqual({ terminal_id: 't1', data: 'output' })
|
||||||
|
expect((result._meta as Record<string, unknown>).terminal_exit).toEqual({ terminal_id: 't1', exit_code: 0, signal: null })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('handles bash_code_execution_result format', () => {
|
||||||
|
const result = toolUpdateFromToolResult(
|
||||||
|
{ content: { type: 'bash_code_execution_result', stdout: 'out', stderr: 'err', return_code: 0 }, is_error: false, tool_use_id: 't1' },
|
||||||
|
{ name: 'Bash', id: 't1' },
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
const meta = result._meta as Record<string, unknown>
|
||||||
|
const termOutput = meta.terminal_output as { data: string }
|
||||||
|
expect(termOutput.data).toBe('out\nerr')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns empty when no toolUse', () => {
|
||||||
|
const result = toolUpdateFromToolResult(
|
||||||
|
{ content: 'text', is_error: false },
|
||||||
|
undefined,
|
||||||
|
)
|
||||||
|
expect(result).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('transforms tool_reference content', () => {
|
||||||
|
const result = toolUpdateFromToolResult(
|
||||||
|
{ content: [{ type: 'tool_reference', tool_name: 'some_tool' }], is_error: false, tool_use_id: 't1' },
|
||||||
|
{ name: 'ToolSearch', id: 't1' },
|
||||||
|
)
|
||||||
|
expect(result.content).toEqual([
|
||||||
|
{ type: 'content', content: { type: 'text', text: 'Tool: some_tool' } },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('transforms web_search_result content', () => {
|
||||||
|
const result = toolUpdateFromToolResult(
|
||||||
|
{ content: [{ type: 'web_search_result', title: 'Test Result', url: 'https://example.com' }], is_error: false, tool_use_id: 't1' },
|
||||||
|
{ name: 'WebSearch', id: 't1' },
|
||||||
|
)
|
||||||
|
expect(result.content).toEqual([
|
||||||
|
{ type: 'content', content: { type: 'text', text: 'Test Result (https://example.com)' } },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('transforms code_execution_result content', () => {
|
||||||
|
const result = toolUpdateFromToolResult(
|
||||||
|
{ content: [{ type: 'code_execution_result', stdout: 'Hello World', stderr: '' }], is_error: false, tool_use_id: 't1' },
|
||||||
|
{ name: 'CodeExecution', id: 't1' },
|
||||||
|
)
|
||||||
|
expect(result.content).toEqual([
|
||||||
|
{ type: 'content', content: { type: 'text', text: 'Output: Hello World' } },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns title for ExitPlanMode', () => {
|
||||||
|
const result = toolUpdateFromToolResult(
|
||||||
|
{ content: 'ok', is_error: false, tool_use_id: 't1' },
|
||||||
|
{ name: 'ExitPlanMode', id: 't1' },
|
||||||
|
)
|
||||||
|
expect(result.title).toBe('Exited Plan Mode')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── toolUpdateFromEditToolResponse ─────────────────────────────────
|
||||||
|
|
||||||
|
describe('toolUpdateFromEditToolResponse', () => {
|
||||||
|
test('returns empty for null/undefined/string', () => {
|
||||||
|
expect(toolUpdateFromEditToolResponse(null)).toEqual({})
|
||||||
|
expect(toolUpdateFromEditToolResponse(undefined)).toEqual({})
|
||||||
|
expect(toolUpdateFromEditToolResponse('string')).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns empty when filePath or structuredPatch missing', () => {
|
||||||
|
expect(toolUpdateFromEditToolResponse({})).toEqual({})
|
||||||
|
expect(toolUpdateFromEditToolResponse({ filePath: '/foo.ts' })).toEqual({})
|
||||||
|
expect(toolUpdateFromEditToolResponse({ structuredPatch: [] })).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('builds diff content from single hunk', () => {
|
||||||
|
const result = toolUpdateFromEditToolResponse({
|
||||||
|
filePath: '/Users/test/project/test.txt',
|
||||||
|
structuredPatch: [
|
||||||
|
{
|
||||||
|
oldStart: 1,
|
||||||
|
oldLines: 3,
|
||||||
|
newStart: 1,
|
||||||
|
newLines: 3,
|
||||||
|
lines: [' context before', '-old line', '+new line', ' context after'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
expect(result).toEqual({
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'diff',
|
||||||
|
path: '/Users/test/project/test.txt',
|
||||||
|
oldText: 'context before\nold line\ncontext after',
|
||||||
|
newText: 'context before\nnew line\ncontext after',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
locations: [{ path: '/Users/test/project/test.txt', line: 1 }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('builds multiple diff blocks for replaceAll with multiple hunks', () => {
|
||||||
|
const result = toolUpdateFromEditToolResponse({
|
||||||
|
filePath: '/Users/test/project/file.ts',
|
||||||
|
structuredPatch: [
|
||||||
|
{ oldStart: 5, oldLines: 1, newStart: 5, newLines: 1, lines: ['-oldValue', '+newValue'] },
|
||||||
|
{ oldStart: 20, oldLines: 1, newStart: 20, newLines: 1, lines: ['-oldValue', '+newValue'] },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
expect(result.content).toHaveLength(2)
|
||||||
|
expect(result.locations).toHaveLength(2)
|
||||||
|
expect(result.locations).toEqual([
|
||||||
|
{ path: '/Users/test/project/file.ts', line: 5 },
|
||||||
|
{ path: '/Users/test/project/file.ts', line: 20 },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('handles deletion (newText becomes empty string)', () => {
|
||||||
|
const result = toolUpdateFromEditToolResponse({
|
||||||
|
filePath: '/Users/test/project/file.ts',
|
||||||
|
structuredPatch: [
|
||||||
|
{ oldStart: 10, oldLines: 2, newStart: 10, newLines: 1, lines: [' context', '-removed line'] },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
expect(result.content).toEqual([
|
||||||
|
{
|
||||||
|
type: 'diff',
|
||||||
|
path: '/Users/test/project/file.ts',
|
||||||
|
oldText: 'context\nremoved line',
|
||||||
|
newText: 'context',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns empty for empty structuredPatch array', () => {
|
||||||
|
expect(
|
||||||
|
toolUpdateFromEditToolResponse({ filePath: '/foo.ts', structuredPatch: [] }),
|
||||||
|
).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── markdownEscape ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('markdownEscape', () => {
|
||||||
|
test('wraps basic text in code fence', () => {
|
||||||
|
expect(markdownEscape('Hello *world*!')).toBe('```\nHello *world*!\n```')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('extends fence for text containing backtick fences', () => {
|
||||||
|
const text = 'for example:\n```markdown\nHello *world*!\n```\n'
|
||||||
|
expect(markdownEscape(text)).toBe('````\nfor example:\n```markdown\nHello *world*!\n```\n````')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── toDisplayPath ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('toDisplayPath', () => {
|
||||||
|
test('relativizes paths inside cwd', () => {
|
||||||
|
expect(toDisplayPath('/Users/test/project/src/main.ts', '/Users/test/project')).toBe('src/main.ts')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('keeps absolute paths outside cwd', () => {
|
||||||
|
expect(toDisplayPath('/etc/hosts', '/Users/test/project')).toBe('/etc/hosts')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns original when no cwd', () => {
|
||||||
|
expect(toDisplayPath('/Users/test/project/src/main.ts')).toBe('/Users/test/project/src/main.ts')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('partial directory name match does not relativize', () => {
|
||||||
|
expect(toDisplayPath('/Users/test/project-other/file.ts', '/Users/test/project')).toBe('/Users/test/project-other/file.ts')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── forwardSessionUpdates ─────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('forwardSessionUpdates', () => {
|
||||||
|
test('returns end_turn when stream is empty', async () => {
|
||||||
|
const conn = makeConn()
|
||||||
|
const result = await forwardSessionUpdates('s1', makeStream([]), conn, new AbortController().signal, {})
|
||||||
|
expect(result.stopReason).toBe('end_turn')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns cancelled when aborted before iteration', async () => {
|
||||||
|
const ac = new AbortController()
|
||||||
|
ac.abort()
|
||||||
|
const conn = makeConn()
|
||||||
|
const result = await forwardSessionUpdates('s1', makeStream([
|
||||||
|
{ type: 'assistant', message: { content: [{ type: 'text', text: 'hi' }] } } as unknown as SDKMessage,
|
||||||
|
]), conn, ac.signal, {})
|
||||||
|
expect(result.stopReason).toBe('cancelled')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('forwards assistant text message as agent_message_chunk', async () => {
|
||||||
|
const conn = makeConn()
|
||||||
|
const msgs: SDKMessage[] = [
|
||||||
|
{ type: 'assistant', message: { content: [{ type: 'text', text: 'Hello!' }], role: 'assistant' } } as unknown as SDKMessage,
|
||||||
|
]
|
||||||
|
const result = await forwardSessionUpdates('s1', makeStream(msgs), conn, new AbortController().signal, {})
|
||||||
|
const calls = (conn.sessionUpdate as ReturnType<typeof mock>).mock.calls
|
||||||
|
expect(calls.length).toBeGreaterThanOrEqual(1)
|
||||||
|
expect(calls[0][0]).toMatchObject({
|
||||||
|
sessionId: 's1',
|
||||||
|
update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: 'Hello!' } },
|
||||||
|
})
|
||||||
|
expect(result.stopReason).toBe('end_turn')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('forwards thinking block as agent_thought_chunk', async () => {
|
||||||
|
const conn = makeConn()
|
||||||
|
const msgs: SDKMessage[] = [
|
||||||
|
{ type: 'assistant', message: { content: [{ type: 'thinking', thinking: 'reasoning...' }], role: 'assistant' } } as unknown as SDKMessage,
|
||||||
|
]
|
||||||
|
await forwardSessionUpdates('s1', makeStream(msgs), conn, new AbortController().signal, {})
|
||||||
|
const calls = (conn.sessionUpdate as ReturnType<typeof mock>).mock.calls
|
||||||
|
expect(calls[0][0].update).toMatchObject({ sessionUpdate: 'agent_thought_chunk' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('forwards tool_use block as tool_call', async () => {
|
||||||
|
const conn = makeConn()
|
||||||
|
const msgs: SDKMessage[] = [
|
||||||
|
{
|
||||||
|
type: 'assistant',
|
||||||
|
message: {
|
||||||
|
content: [{
|
||||||
|
type: 'tool_use',
|
||||||
|
id: 'tu_1',
|
||||||
|
name: 'Bash',
|
||||||
|
input: { command: 'ls' },
|
||||||
|
}],
|
||||||
|
role: 'assistant',
|
||||||
|
},
|
||||||
|
} as unknown as SDKMessage,
|
||||||
|
]
|
||||||
|
await forwardSessionUpdates('s1', makeStream(msgs), conn, new AbortController().signal, {})
|
||||||
|
const update = (conn.sessionUpdate as ReturnType<typeof mock>).mock.calls[0][0].update as Record<string, unknown>
|
||||||
|
expect(update.sessionUpdate).toBe('tool_call')
|
||||||
|
expect(update.toolCallId).toBe('tu_1')
|
||||||
|
expect(update.kind).toBe('execute' as ToolKind)
|
||||||
|
expect(update.status).toBe('pending')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('sends usage_update on result message with correct tokens', async () => {
|
||||||
|
const conn = makeConn()
|
||||||
|
const msgs: SDKMessage[] = [
|
||||||
|
{
|
||||||
|
type: 'result',
|
||||||
|
subtype: 'success',
|
||||||
|
is_error: false,
|
||||||
|
result: '',
|
||||||
|
usage: { input_tokens: 100, output_tokens: 50, cache_read_input_tokens: 10, cache_creation_input_tokens: 5 },
|
||||||
|
total_cost_usd: 0.01,
|
||||||
|
} as unknown as SDKMessage,
|
||||||
|
]
|
||||||
|
const result = await forwardSessionUpdates('s1', makeStream(msgs), conn, new AbortController().signal, {})
|
||||||
|
expect(result.stopReason).toBe('end_turn')
|
||||||
|
expect(result.usage).toBeDefined()
|
||||||
|
expect(result.usage!.inputTokens).toBe(100)
|
||||||
|
expect(result.usage!.outputTokens).toBe(50)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('sends usage_update with context window from modelUsage', async () => {
|
||||||
|
const conn = makeConn()
|
||||||
|
const msgs: SDKMessage[] = [
|
||||||
|
{
|
||||||
|
type: 'assistant',
|
||||||
|
message: {
|
||||||
|
content: [{ type: 'text', text: 'hi' }],
|
||||||
|
role: 'assistant',
|
||||||
|
model: 'claude-opus-4-20250514',
|
||||||
|
usage: { input_tokens: 100, output_tokens: 50, cache_read_input_tokens: 10, cache_creation_input_tokens: 5 },
|
||||||
|
},
|
||||||
|
parent_tool_use_id: null,
|
||||||
|
} as unknown as SDKMessage,
|
||||||
|
{
|
||||||
|
type: 'result',
|
||||||
|
subtype: 'success',
|
||||||
|
is_error: false,
|
||||||
|
result: '',
|
||||||
|
usage: { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 },
|
||||||
|
modelUsage: {
|
||||||
|
'claude-opus-4-20250514': { contextWindow: 1000000 },
|
||||||
|
},
|
||||||
|
} as unknown as SDKMessage,
|
||||||
|
]
|
||||||
|
await forwardSessionUpdates('s1', makeStream(msgs), conn, new AbortController().signal, {})
|
||||||
|
const calls = (conn.sessionUpdate as ReturnType<typeof mock>).mock.calls
|
||||||
|
const usageUpdate = calls.find((c: unknown[]) => ((c[0] as Record<string, Record<string, unknown>>).update ?? {})['sessionUpdate'] === 'usage_update')
|
||||||
|
expect(usageUpdate).toBeDefined()
|
||||||
|
expect(((usageUpdate![0] as Record<string, unknown>).update as Record<string, unknown>).size).toBe(1000000)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('sends usage_update with prefix-matched modelUsage', async () => {
|
||||||
|
const conn = makeConn()
|
||||||
|
const msgs: SDKMessage[] = [
|
||||||
|
{
|
||||||
|
type: 'assistant',
|
||||||
|
message: {
|
||||||
|
content: [{ type: 'text', text: 'hi' }],
|
||||||
|
role: 'assistant',
|
||||||
|
model: 'claude-opus-4-6-20250514',
|
||||||
|
usage: { input_tokens: 100, output_tokens: 50, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 },
|
||||||
|
},
|
||||||
|
parent_tool_use_id: null,
|
||||||
|
} as unknown as SDKMessage,
|
||||||
|
{
|
||||||
|
type: 'result',
|
||||||
|
subtype: 'success',
|
||||||
|
is_error: false,
|
||||||
|
result: '',
|
||||||
|
usage: { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 },
|
||||||
|
modelUsage: {
|
||||||
|
'claude-opus-4-6': { contextWindow: 2000000 },
|
||||||
|
},
|
||||||
|
} as unknown as SDKMessage,
|
||||||
|
]
|
||||||
|
await forwardSessionUpdates('s1', makeStream(msgs), conn, new AbortController().signal, {})
|
||||||
|
const calls = (conn.sessionUpdate as ReturnType<typeof mock>).mock.calls
|
||||||
|
const usageUpdate = calls.find((c: unknown[]) => ((c[0] as Record<string, Record<string, unknown>>).update ?? {})['sessionUpdate'] === 'usage_update')
|
||||||
|
expect(usageUpdate).toBeDefined()
|
||||||
|
expect(((usageUpdate![0] as Record<string, unknown>).update as Record<string, unknown>).size).toBe(2000000)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('resets usage on compact_boundary', async () => {
|
||||||
|
const conn = makeConn()
|
||||||
|
const msgs: SDKMessage[] = [
|
||||||
|
{ type: 'system', subtype: 'compact_boundary' } as unknown as SDKMessage,
|
||||||
|
]
|
||||||
|
await forwardSessionUpdates('s1', makeStream(msgs), conn, new AbortController().signal, {})
|
||||||
|
const calls = (conn.sessionUpdate as ReturnType<typeof mock>).mock.calls
|
||||||
|
const usageCall = calls.find((c: unknown[]) => ((c[0] as Record<string, Record<string, unknown>>).update ?? {})['sessionUpdate'] === 'usage_update')
|
||||||
|
expect(usageCall).toBeDefined()
|
||||||
|
expect(((usageCall![0] as Record<string, unknown>).update as Record<string, unknown>).used).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('re-throws unexpected errors from stream', async () => {
|
||||||
|
const conn = makeConn()
|
||||||
|
async function* errorStream(): AsyncGenerator<SDKMessage, void, unknown> {
|
||||||
|
throw new Error('stream exploded')
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
forwardSessionUpdates('s1', errorStream(), conn, new AbortController().signal, {}),
|
||||||
|
).rejects.toThrow('stream exploded')
|
||||||
|
})
|
||||||
|
})
|
||||||
144
src/services/acp/__tests__/permissions.test.ts
Normal file
144
src/services/acp/__tests__/permissions.test.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import { describe, expect, test, mock } from 'bun:test'
|
||||||
|
import type { AgentSideConnection } from '@agentclientprotocol/sdk'
|
||||||
|
import type { Tool as ToolType } from '../../../Tool.js'
|
||||||
|
|
||||||
|
// ── Inline re-implementation of createAcpCanUseTool for isolated testing ──
|
||||||
|
// We cannot import the real permissions.js because agent.test.ts mocks it globally.
|
||||||
|
// Instead we re-implement the core logic here, using our own mocked bridge.js.
|
||||||
|
|
||||||
|
function createAcpCanUseTool(
|
||||||
|
conn: AgentSideConnection,
|
||||||
|
sessionId: string,
|
||||||
|
getCurrentMode: () => string,
|
||||||
|
): any {
|
||||||
|
return async (
|
||||||
|
tool: { name: string },
|
||||||
|
input: Record<string, unknown>,
|
||||||
|
_context: any,
|
||||||
|
_assistantMessage: any,
|
||||||
|
toolUseID: string,
|
||||||
|
): Promise<{ behavior: string; message?: string; updatedInput?: Record<string, unknown> }> => {
|
||||||
|
if (getCurrentMode() === 'bypassPermissions') {
|
||||||
|
return { behavior: 'allow', updatedInput: input }
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOOL_KIND_MAP: Record<string, string> = {
|
||||||
|
Read: 'read', Edit: 'edit', Write: 'edit',
|
||||||
|
Bash: 'execute', Glob: 'search', Grep: 'search',
|
||||||
|
WebFetch: 'fetch', WebSearch: 'fetch',
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolCall = {
|
||||||
|
toolCallId: toolUseID,
|
||||||
|
title: tool.name,
|
||||||
|
kind: TOOL_KIND_MAP[tool.name] ?? 'other',
|
||||||
|
status: 'pending',
|
||||||
|
rawInput: input,
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{ kind: 'allow_always', name: 'Always Allow', optionId: 'allow_always' },
|
||||||
|
{ kind: 'allow_once', name: 'Allow', optionId: 'allow' },
|
||||||
|
{ kind: 'reject_once', name: 'Reject', optionId: 'reject' },
|
||||||
|
]
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await (conn as any).requestPermission({ sessionId, toolCall, options })
|
||||||
|
|
||||||
|
if (response.outcome.outcome === 'cancelled') {
|
||||||
|
return { behavior: 'deny', message: 'Permission request cancelled by client' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.outcome.outcome === 'selected' && response.outcome.optionId !== undefined) {
|
||||||
|
const optionId = response.outcome.optionId
|
||||||
|
if (optionId === 'allow' || optionId === 'allow_always') {
|
||||||
|
return { behavior: 'allow', updatedInput: input }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { behavior: 'deny', message: 'Permission denied by client' }
|
||||||
|
} catch {
|
||||||
|
return { behavior: 'deny', message: 'Permission request failed' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeConn(permissionResponse: Record<string, unknown>) {
|
||||||
|
return {
|
||||||
|
requestPermission: mock(async () => permissionResponse),
|
||||||
|
sessionUpdate: mock(async () => {}),
|
||||||
|
} as unknown as AgentSideConnection
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeTool(name: string) {
|
||||||
|
return { name } as unknown as ToolType
|
||||||
|
}
|
||||||
|
|
||||||
|
const dummyContext = {} as Record<string, unknown>
|
||||||
|
const dummyMsg = {} as Record<string, unknown>
|
||||||
|
|
||||||
|
describe('createAcpCanUseTool', () => {
|
||||||
|
test('returns allow when client selects allow option', async () => {
|
||||||
|
const conn = makeConn({ outcome: { outcome: 'selected', optionId: 'allow' } })
|
||||||
|
const canUseTool = createAcpCanUseTool(conn, 'sess-1', () => 'default')
|
||||||
|
const result = await canUseTool(makeTool('Bash'), { command: 'ls' }, dummyContext as any, dummyMsg as any, 'tu_1')
|
||||||
|
expect(result.behavior).toBe('allow')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns deny when client selects reject option', async () => {
|
||||||
|
const conn = makeConn({ outcome: { outcome: 'selected', optionId: 'reject' } })
|
||||||
|
const canUseTool = createAcpCanUseTool(conn, 'sess-1', () => 'default')
|
||||||
|
const result = await canUseTool(makeTool('Bash'), {}, dummyContext as any, dummyMsg as any, 'tu_2')
|
||||||
|
expect(result.behavior).toBe('deny')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns deny when client cancels', async () => {
|
||||||
|
const conn = makeConn({ outcome: { outcome: 'cancelled' } })
|
||||||
|
const canUseTool = createAcpCanUseTool(conn, 'sess-1', () => 'default')
|
||||||
|
const result = await canUseTool(makeTool('Read'), { file_path: '/tmp/x' }, dummyContext as any, dummyMsg as any, 'tu_3')
|
||||||
|
expect(result.behavior).toBe('deny')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns deny when requestPermission throws', async () => {
|
||||||
|
const conn = {
|
||||||
|
requestPermission: mock(async () => { throw new Error('connection lost') }),
|
||||||
|
sessionUpdate: mock(async () => {}),
|
||||||
|
} as unknown as AgentSideConnection
|
||||||
|
const canUseTool = createAcpCanUseTool(conn, 'sess-1', () => 'default')
|
||||||
|
const result = await canUseTool(makeTool('Edit'), {}, dummyContext as any, dummyMsg as any, 'tu_4')
|
||||||
|
expect(result.behavior).toBe('deny')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('passes correct sessionId and toolCallId to requestPermission', async () => {
|
||||||
|
const conn = makeConn({ outcome: { outcome: 'selected', optionId: 'allow' } })
|
||||||
|
const canUseTool = createAcpCanUseTool(conn, 'my-session', () => 'default')
|
||||||
|
await canUseTool(makeTool('Glob'), { pattern: '**/*.ts' }, dummyContext as any, dummyMsg as any, 'tu_99')
|
||||||
|
const rpMock = conn.requestPermission as ReturnType<typeof mock>
|
||||||
|
expect(rpMock.mock.calls.length).toBeGreaterThan(0)
|
||||||
|
const callArgs = rpMock.mock.calls[0][0] as Record<string, unknown>
|
||||||
|
expect(callArgs.sessionId).toBe('my-session')
|
||||||
|
expect((callArgs.toolCall as Record<string, unknown>).toolCallId).toBe('tu_99')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns allow in bypassPermissions mode without calling requestPermission', async () => {
|
||||||
|
const conn = makeConn({ outcome: { outcome: 'selected', optionId: 'allow' } })
|
||||||
|
const canUseTool = createAcpCanUseTool(conn, 'sess-bypass', () => 'bypassPermissions')
|
||||||
|
const result = await canUseTool(makeTool('Bash'), { command: 'rm -rf /' }, dummyContext as any, dummyMsg as any, 'tu_bp')
|
||||||
|
expect(result.behavior).toBe('allow')
|
||||||
|
const rpMock = conn.requestPermission as ReturnType<typeof mock>
|
||||||
|
expect(rpMock.mock.calls).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('options include allow_always, allow_once and reject_once', async () => {
|
||||||
|
const conn = makeConn({ outcome: { outcome: 'cancelled' } })
|
||||||
|
const canUseTool = createAcpCanUseTool(conn, 'sess-3', () => 'default')
|
||||||
|
await canUseTool(makeTool('Write'), {}, dummyContext as any, dummyMsg as any, 'tu_6')
|
||||||
|
const rpMock = conn.requestPermission as ReturnType<typeof mock>
|
||||||
|
expect(rpMock.mock.calls.length).toBeGreaterThan(0)
|
||||||
|
const { options } = rpMock.mock.calls[0][0] as Record<string, unknown>
|
||||||
|
const opts = options as Array<Record<string, unknown>>
|
||||||
|
expect(opts.find((o) => o.kind === 'allow_always')).toBeTruthy()
|
||||||
|
expect(opts.find((o) => o.kind === 'allow_once')).toBeTruthy()
|
||||||
|
expect(opts.find((o) => o.kind === 'reject_once')).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
801
src/services/acp/agent.ts
Normal file
801
src/services/acp/agent.ts
Normal file
@@ -0,0 +1,801 @@
|
|||||||
|
/**
|
||||||
|
* ACP Agent implementation — bridges ACP protocol methods to Claude Code's
|
||||||
|
* internal QueryEngine / query() pipeline.
|
||||||
|
*
|
||||||
|
* Architecture: Uses internal QueryEngine (not @anthropic-ai/claude-agent-sdk)
|
||||||
|
* to directly run queries, with a bridge layer converting SDKMessage → ACP SessionUpdate.
|
||||||
|
*/
|
||||||
|
import type {
|
||||||
|
Agent,
|
||||||
|
AgentSideConnection,
|
||||||
|
InitializeRequest,
|
||||||
|
InitializeResponse,
|
||||||
|
AuthenticateRequest,
|
||||||
|
AuthenticateResponse,
|
||||||
|
NewSessionRequest,
|
||||||
|
NewSessionResponse,
|
||||||
|
PromptRequest,
|
||||||
|
PromptResponse,
|
||||||
|
CancelNotification,
|
||||||
|
LoadSessionRequest,
|
||||||
|
LoadSessionResponse,
|
||||||
|
ListSessionsRequest,
|
||||||
|
ListSessionsResponse,
|
||||||
|
ResumeSessionRequest,
|
||||||
|
ResumeSessionResponse,
|
||||||
|
ForkSessionRequest,
|
||||||
|
ForkSessionResponse,
|
||||||
|
CloseSessionRequest,
|
||||||
|
CloseSessionResponse,
|
||||||
|
SetSessionModeRequest,
|
||||||
|
SetSessionModeResponse,
|
||||||
|
SetSessionModelRequest,
|
||||||
|
SetSessionModelResponse,
|
||||||
|
SetSessionConfigOptionRequest,
|
||||||
|
SetSessionConfigOptionResponse,
|
||||||
|
ContentBlock,
|
||||||
|
ClientCapabilities,
|
||||||
|
SessionModeState,
|
||||||
|
SessionModelState,
|
||||||
|
SessionConfigOption,
|
||||||
|
} from '@agentclientprotocol/sdk'
|
||||||
|
import { randomUUID, type UUID } from 'node:crypto'
|
||||||
|
import type { Message } from '../../types/message.js'
|
||||||
|
import { deserializeMessages } from '../../utils/conversationRecovery.js'
|
||||||
|
import { getLastSessionLog, sessionIdExists } from '../../utils/sessionStorage.js'
|
||||||
|
import { QueryEngine } from '../../QueryEngine.js'
|
||||||
|
import type { QueryEngineConfig } from '../../QueryEngine.js'
|
||||||
|
import type { Tools } from '../../Tool.js'
|
||||||
|
import { getTools } from '../../tools.js'
|
||||||
|
import { getEmptyToolPermissionContext } from '../../Tool.js'
|
||||||
|
import type { PermissionMode } from '../../types/permissions.js'
|
||||||
|
import type { Command } from '../../types/command.js'
|
||||||
|
import { getCommands } from '../../commands.js'
|
||||||
|
import { setOriginalCwd } from '../../bootstrap/state.js'
|
||||||
|
import { enableConfigs } from '../../utils/config.js'
|
||||||
|
import { FileStateCache } from '../../utils/fileStateCache.js'
|
||||||
|
import { getDefaultAppState } from '../../state/AppStateStore.js'
|
||||||
|
import type { AppState } from '../../state/AppStateStore.js'
|
||||||
|
import { createAcpCanUseTool } from './permissions.js'
|
||||||
|
import { forwardSessionUpdates, replayHistoryMessages, type ToolUseCache } from './bridge.js'
|
||||||
|
import {
|
||||||
|
resolvePermissionMode,
|
||||||
|
computeSessionFingerprint,
|
||||||
|
sanitizeTitle,
|
||||||
|
} from './utils.js'
|
||||||
|
import {
|
||||||
|
listSessionsImpl,
|
||||||
|
} from '../../utils/listSessionsImpl.js'
|
||||||
|
import { getMainLoopModel } from '../../utils/model/model.js'
|
||||||
|
import { getModelOptions } from '../../utils/model/modelOptions.js'
|
||||||
|
|
||||||
|
// ── Session state ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type AcpSession = {
|
||||||
|
queryEngine: QueryEngine
|
||||||
|
cancelled: boolean
|
||||||
|
cwd: string
|
||||||
|
sessionFingerprint: string
|
||||||
|
modes: SessionModeState
|
||||||
|
models: SessionModelState
|
||||||
|
configOptions: SessionConfigOption[]
|
||||||
|
promptRunning: boolean
|
||||||
|
pendingMessages: Map<string, { resolve: (cancelled: boolean) => void; order: number }>
|
||||||
|
nextPendingOrder: number
|
||||||
|
toolUseCache: ToolUseCache
|
||||||
|
clientCapabilities?: ClientCapabilities
|
||||||
|
appState: AppState
|
||||||
|
commands: Command[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Agent class ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export class AcpAgent implements Agent {
|
||||||
|
private conn: AgentSideConnection
|
||||||
|
sessions = new Map<string, AcpSession>()
|
||||||
|
private clientCapabilities?: ClientCapabilities
|
||||||
|
|
||||||
|
constructor(conn: AgentSideConnection) {
|
||||||
|
this.conn = conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── initialize ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async initialize(params: InitializeRequest): Promise<InitializeResponse> {
|
||||||
|
this.clientCapabilities = params.clientCapabilities
|
||||||
|
|
||||||
|
return {
|
||||||
|
protocolVersion: 1,
|
||||||
|
agentInfo: {
|
||||||
|
name: 'claude-code',
|
||||||
|
title: 'Claude Code',
|
||||||
|
version:
|
||||||
|
typeof (globalThis as unknown as Record<string, unknown>).MACRO ===
|
||||||
|
'object' &&
|
||||||
|
(globalThis as unknown as Record<string, Record<string, unknown>>)
|
||||||
|
.MACRO !== null
|
||||||
|
? String(
|
||||||
|
(
|
||||||
|
(globalThis as unknown as Record<string, Record<string, unknown>>)
|
||||||
|
.MACRO as Record<string, unknown>
|
||||||
|
).VERSION ?? '0.0.0',
|
||||||
|
)
|
||||||
|
: '0.0.0',
|
||||||
|
},
|
||||||
|
agentCapabilities: {
|
||||||
|
_meta: {
|
||||||
|
claudeCode: {
|
||||||
|
promptQueueing: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
promptCapabilities: {
|
||||||
|
image: true,
|
||||||
|
embeddedContext: true,
|
||||||
|
},
|
||||||
|
mcpCapabilities: {
|
||||||
|
http: true,
|
||||||
|
sse: true,
|
||||||
|
},
|
||||||
|
loadSession: true,
|
||||||
|
sessionCapabilities: {
|
||||||
|
fork: {},
|
||||||
|
list: {},
|
||||||
|
resume: {},
|
||||||
|
close: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── authenticate ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
async authenticate(_params: AuthenticateRequest): Promise<AuthenticateResponse> {
|
||||||
|
// No authentication required — this is a self-hosted/custom deployment
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── newSession ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
|
||||||
|
return this.createSession(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── resumeSession ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
async unstable_resumeSession(
|
||||||
|
params: ResumeSessionRequest,
|
||||||
|
): Promise<ResumeSessionResponse> {
|
||||||
|
const result = await this.getOrCreateSession(params)
|
||||||
|
setTimeout(() => {
|
||||||
|
this.sendAvailableCommandsUpdate(params.sessionId)
|
||||||
|
}, 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── loadSession ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
|
||||||
|
const result = await this.getOrCreateSession(params)
|
||||||
|
setTimeout(() => {
|
||||||
|
this.sendAvailableCommandsUpdate(params.sessionId)
|
||||||
|
}, 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── listSessions ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
async listSessions(params: ListSessionsRequest): Promise<ListSessionsResponse> {
|
||||||
|
const candidates = await listSessionsImpl({
|
||||||
|
dir: params.cwd ?? undefined,
|
||||||
|
limit: 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
const sessions = []
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (!candidate.cwd) continue
|
||||||
|
sessions.push({
|
||||||
|
sessionId: candidate.sessionId,
|
||||||
|
cwd: candidate.cwd,
|
||||||
|
title: sanitizeTitle(candidate.summary ?? ''),
|
||||||
|
updatedAt: new Date(candidate.lastModified).toISOString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sessions }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── forkSession ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async unstable_forkSession(
|
||||||
|
params: ForkSessionRequest,
|
||||||
|
): Promise<ForkSessionResponse> {
|
||||||
|
const response = await this.createSession(
|
||||||
|
{
|
||||||
|
cwd: params.cwd,
|
||||||
|
mcpServers: params.mcpServers ?? [],
|
||||||
|
_meta: params._meta,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
setTimeout(() => {
|
||||||
|
this.sendAvailableCommandsUpdate(response.sessionId)
|
||||||
|
}, 0)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── closeSession ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
async unstable_closeSession(
|
||||||
|
params: CloseSessionRequest,
|
||||||
|
): Promise<CloseSessionResponse> {
|
||||||
|
const session = this.sessions.get(params.sessionId)
|
||||||
|
if (!session) {
|
||||||
|
throw new Error('Session not found')
|
||||||
|
}
|
||||||
|
await this.teardownSession(params.sessionId)
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── prompt ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async prompt(params: PromptRequest): Promise<PromptResponse> {
|
||||||
|
const session = this.sessions.get(params.sessionId)
|
||||||
|
if (!session) {
|
||||||
|
throw new Error(`Session ${params.sessionId} not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset cancelled state at the start of each prompt (matches official impl)
|
||||||
|
session.cancelled = false
|
||||||
|
|
||||||
|
// Extract text/image content from the prompt
|
||||||
|
const promptInput = promptToQueryInput(params.prompt)
|
||||||
|
|
||||||
|
if (!promptInput.trim()) {
|
||||||
|
return { stopReason: 'end_turn' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle prompt queuing — if a prompt is already running, queue this one
|
||||||
|
if (session.promptRunning) {
|
||||||
|
const order = session.nextPendingOrder++
|
||||||
|
const promptUuid = randomUUID()
|
||||||
|
const cancelled = await new Promise<boolean>((resolve) => {
|
||||||
|
session.pendingMessages.set(promptUuid, { resolve, order })
|
||||||
|
})
|
||||||
|
if (cancelled) {
|
||||||
|
return { stopReason: 'cancelled' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session.promptRunning = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Reset the query engine's abort controller for a fresh query.
|
||||||
|
// After a previous interrupt(), the internal controller is stuck in
|
||||||
|
// aborted state — without this, submitMessage() fails immediately.
|
||||||
|
session.queryEngine.resetAbortController()
|
||||||
|
|
||||||
|
const sdkMessages = session.queryEngine.submitMessage(promptInput)
|
||||||
|
|
||||||
|
const { stopReason, usage } = await forwardSessionUpdates(
|
||||||
|
params.sessionId,
|
||||||
|
sdkMessages,
|
||||||
|
this.conn,
|
||||||
|
session.queryEngine.getAbortSignal(),
|
||||||
|
session.toolUseCache,
|
||||||
|
this.clientCapabilities,
|
||||||
|
session.cwd,
|
||||||
|
() => session.cancelled,
|
||||||
|
)
|
||||||
|
|
||||||
|
// If the session was cancelled during processing, return cancelled
|
||||||
|
if (session.cancelled) {
|
||||||
|
return { stopReason: 'cancelled' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
stopReason,
|
||||||
|
usage: usage
|
||||||
|
? {
|
||||||
|
inputTokens: usage.inputTokens,
|
||||||
|
outputTokens: usage.outputTokens,
|
||||||
|
cachedReadTokens: usage.cachedReadTokens,
|
||||||
|
cachedWriteTokens: usage.cachedWriteTokens,
|
||||||
|
totalTokens:
|
||||||
|
usage.inputTokens +
|
||||||
|
usage.outputTokens +
|
||||||
|
usage.cachedReadTokens +
|
||||||
|
usage.cachedWriteTokens,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (session.cancelled) {
|
||||||
|
return { stopReason: 'cancelled' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for process death errors
|
||||||
|
if (
|
||||||
|
err instanceof Error &&
|
||||||
|
(err.message.includes('terminated') ||
|
||||||
|
err.message.includes('process exited'))
|
||||||
|
) {
|
||||||
|
this.teardownSession(params.sessionId)
|
||||||
|
throw new Error(
|
||||||
|
'The Claude Agent process exited unexpectedly. Please start a new session.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[ACP] prompt error:', err)
|
||||||
|
return { stopReason: 'end_turn' }
|
||||||
|
} finally {
|
||||||
|
session.promptRunning = false
|
||||||
|
// Resolve next pending prompt if any
|
||||||
|
if (session.pendingMessages.size > 0) {
|
||||||
|
const next = [...session.pendingMessages.entries()].sort(
|
||||||
|
(a, b) => a[1].order - b[1].order,
|
||||||
|
)[0]
|
||||||
|
if (next) {
|
||||||
|
next[1].resolve(false)
|
||||||
|
session.pendingMessages.delete(next[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── cancel ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async cancel(params: CancelNotification): Promise<void> {
|
||||||
|
const session = this.sessions.get(params.sessionId)
|
||||||
|
if (!session) return
|
||||||
|
|
||||||
|
// Set cancelled flag — checked by prompt() loop to break out
|
||||||
|
session.cancelled = true
|
||||||
|
|
||||||
|
// Cancel any queued prompts
|
||||||
|
for (const [, pending] of session.pendingMessages) {
|
||||||
|
pending.resolve(true)
|
||||||
|
}
|
||||||
|
session.pendingMessages.clear()
|
||||||
|
|
||||||
|
// Interrupt the query engine to abort the current API call
|
||||||
|
session.queryEngine.interrupt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── setSessionMode ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
async setSessionMode(
|
||||||
|
params: SetSessionModeRequest,
|
||||||
|
): Promise<SetSessionModeResponse> {
|
||||||
|
const session = this.sessions.get(params.sessionId)
|
||||||
|
if (!session) {
|
||||||
|
throw new Error('Session not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.applySessionMode(params.sessionId, params.modeId)
|
||||||
|
await this.updateConfigOption(params.sessionId, 'mode', params.modeId)
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── setSessionModel ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
async unstable_setSessionModel(
|
||||||
|
params: SetSessionModelRequest,
|
||||||
|
): Promise<SetSessionModelResponse | void> {
|
||||||
|
const session = this.sessions.get(params.sessionId)
|
||||||
|
if (!session) {
|
||||||
|
throw new Error('Session not found')
|
||||||
|
}
|
||||||
|
// Store the raw value — QueryEngine.submitMessage() calls
|
||||||
|
// parseUserSpecifiedModel() to resolve aliases (e.g. "sonnet" → "glm-5.1-turbo")
|
||||||
|
session.queryEngine.setModel(params.modelId)
|
||||||
|
await this.updateConfigOption(params.sessionId, 'model', params.modelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── setSessionConfigOption ──────────────────────────────────────
|
||||||
|
|
||||||
|
async setSessionConfigOption(
|
||||||
|
params: SetSessionConfigOptionRequest,
|
||||||
|
): Promise<SetSessionConfigOptionResponse> {
|
||||||
|
const session = this.sessions.get(params.sessionId)
|
||||||
|
if (!session) {
|
||||||
|
throw new Error('Session not found')
|
||||||
|
}
|
||||||
|
if (typeof params.value !== 'string') {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid value for config option ${params.configId}: ${String(params.value)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const option = session.configOptions.find((o) => o.id === params.configId)
|
||||||
|
if (!option) {
|
||||||
|
throw new Error(`Unknown config option: ${params.configId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = params.value
|
||||||
|
|
||||||
|
if (params.configId === 'mode') {
|
||||||
|
this.applySessionMode(params.sessionId, value)
|
||||||
|
await this.conn.sessionUpdate({
|
||||||
|
sessionId: params.sessionId,
|
||||||
|
update: {
|
||||||
|
sessionUpdate: 'current_mode_update',
|
||||||
|
currentModeId: value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else if (params.configId === 'model') {
|
||||||
|
session.queryEngine.setModel(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.syncSessionConfigState(session, params.configId, value)
|
||||||
|
|
||||||
|
session.configOptions = session.configOptions.map((o) =>
|
||||||
|
o.id === params.configId && typeof o.currentValue === 'string'
|
||||||
|
? { ...o, currentValue: value }
|
||||||
|
: o,
|
||||||
|
)
|
||||||
|
|
||||||
|
return { configOptions: session.configOptions }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Private helpers ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
private async createSession(
|
||||||
|
params: NewSessionRequest,
|
||||||
|
opts: { forceNewId?: boolean; sessionId?: string; initialMessages?: Message[] } = {},
|
||||||
|
): Promise<NewSessionResponse> {
|
||||||
|
enableConfigs()
|
||||||
|
|
||||||
|
const sessionId = opts.sessionId ?? randomUUID()
|
||||||
|
const cwd = params.cwd
|
||||||
|
|
||||||
|
// Set CWD for the session
|
||||||
|
setOriginalCwd(cwd)
|
||||||
|
try {
|
||||||
|
process.chdir(cwd)
|
||||||
|
} catch {
|
||||||
|
// CWD may not exist yet; best-effort
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build tools with a permissive permission context.
|
||||||
|
const permissionContext = getEmptyToolPermissionContext()
|
||||||
|
const tools: Tools = getTools(permissionContext)
|
||||||
|
|
||||||
|
// Parse permission mode from settings
|
||||||
|
const permissionMode = resolvePermissionMode(
|
||||||
|
this.getSetting<string>('permissions.defaultMode'),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create the permission bridge canUseTool function
|
||||||
|
const canUseTool = createAcpCanUseTool(
|
||||||
|
this.conn,
|
||||||
|
sessionId,
|
||||||
|
() => this.sessions.get(sessionId)?.modes.currentModeId ?? 'default',
|
||||||
|
this.clientCapabilities,
|
||||||
|
cwd,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse MCP servers from ACP params
|
||||||
|
// MCP server config is handled separately in the tools system
|
||||||
|
|
||||||
|
// Create a mutable AppState for the session
|
||||||
|
const appState: AppState = {
|
||||||
|
...getDefaultAppState(),
|
||||||
|
toolPermissionContext: {
|
||||||
|
...permissionContext,
|
||||||
|
mode: permissionMode as PermissionMode,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load commands for slash command and skill support
|
||||||
|
const commands = await getCommands(cwd)
|
||||||
|
|
||||||
|
// Build QueryEngine config
|
||||||
|
const engineConfig: QueryEngineConfig = {
|
||||||
|
cwd,
|
||||||
|
tools,
|
||||||
|
commands,
|
||||||
|
mcpClients: [],
|
||||||
|
agents: [],
|
||||||
|
canUseTool,
|
||||||
|
getAppState: () => appState,
|
||||||
|
setAppState: (updater: (prev: AppState) => AppState) => {
|
||||||
|
const updated = updater(appState)
|
||||||
|
Object.assign(appState, updated)
|
||||||
|
},
|
||||||
|
readFileCache: new FileStateCache(500, 50 * 1024 * 1024),
|
||||||
|
includePartialMessages: true,
|
||||||
|
replayUserMessages: true,
|
||||||
|
initialMessages: opts.initialMessages,
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryEngine = new QueryEngine(engineConfig)
|
||||||
|
|
||||||
|
// Build modes
|
||||||
|
const availableModes = [
|
||||||
|
{ id: 'auto', name: 'Auto', description: 'Use a model classifier to approve/deny permission prompts.' },
|
||||||
|
{ id: 'default', name: 'Default', description: 'Standard behavior, prompts for dangerous operations' },
|
||||||
|
{ id: 'acceptEdits', name: 'Accept Edits', description: 'Auto-accept file edit operations' },
|
||||||
|
{ id: 'plan', name: 'Plan Mode', description: 'Planning mode, no actual tool execution' },
|
||||||
|
{ id: 'dontAsk', name: "Don't Ask", description: "Don't prompt for permissions, deny if not pre-approved" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const modes: SessionModeState = {
|
||||||
|
currentModeId: permissionMode,
|
||||||
|
availableModes,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build models
|
||||||
|
const modelOptions = getModelOptions()
|
||||||
|
const currentModel = getMainLoopModel()
|
||||||
|
const models: SessionModelState = {
|
||||||
|
availableModels: modelOptions.map((m) => ({
|
||||||
|
modelId: String(m.value ?? ''),
|
||||||
|
name: m.label ?? String(m.value ?? ''),
|
||||||
|
description: m.description ?? undefined,
|
||||||
|
})),
|
||||||
|
currentModelId: currentModel,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the model on the engine
|
||||||
|
queryEngine.setModel(currentModel)
|
||||||
|
|
||||||
|
// Build config options
|
||||||
|
const configOptions = buildConfigOptions(modes, models)
|
||||||
|
|
||||||
|
const session: AcpSession = {
|
||||||
|
queryEngine,
|
||||||
|
cancelled: false,
|
||||||
|
cwd,
|
||||||
|
modes,
|
||||||
|
models,
|
||||||
|
configOptions,
|
||||||
|
promptRunning: false,
|
||||||
|
pendingMessages: new Map(),
|
||||||
|
nextPendingOrder: 0,
|
||||||
|
toolUseCache: {},
|
||||||
|
clientCapabilities: this.clientCapabilities,
|
||||||
|
appState,
|
||||||
|
commands,
|
||||||
|
sessionFingerprint: computeSessionFingerprint({
|
||||||
|
cwd,
|
||||||
|
mcpServers: params.mcpServers as Array<{ name: string; [key: string]: unknown }> | undefined,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sessions.set(sessionId, session)
|
||||||
|
|
||||||
|
// Send available commands after session creation
|
||||||
|
setTimeout(() => {
|
||||||
|
this.sendAvailableCommandsUpdate(sessionId)
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionId,
|
||||||
|
models,
|
||||||
|
modes,
|
||||||
|
configOptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getOrCreateSession(params: {
|
||||||
|
sessionId: string
|
||||||
|
cwd: string
|
||||||
|
mcpServers?: NewSessionRequest['mcpServers']
|
||||||
|
_meta?: NewSessionRequest['_meta']
|
||||||
|
}): Promise<NewSessionResponse> {
|
||||||
|
const existingSession = this.sessions.get(params.sessionId)
|
||||||
|
if (existingSession) {
|
||||||
|
const fingerprint = computeSessionFingerprint({
|
||||||
|
cwd: params.cwd,
|
||||||
|
mcpServers:
|
||||||
|
params.mcpServers as Array<{ name: string; [key: string]: unknown }> | undefined,
|
||||||
|
})
|
||||||
|
if (fingerprint === existingSession.sessionFingerprint) {
|
||||||
|
return {
|
||||||
|
sessionId: params.sessionId,
|
||||||
|
modes: existingSession.modes,
|
||||||
|
models: existingSession.models,
|
||||||
|
configOptions: existingSession.configOptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session-defining params changed — tear down and recreate
|
||||||
|
await this.teardownSession(params.sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set CWD early so session file lookup can find the right project directory
|
||||||
|
setOriginalCwd(params.cwd)
|
||||||
|
|
||||||
|
// Try to load session history for resume/load
|
||||||
|
let initialMessages: Message[] | undefined
|
||||||
|
if (sessionIdExists(params.sessionId)) {
|
||||||
|
try {
|
||||||
|
const log = await getLastSessionLog(params.sessionId as UUID)
|
||||||
|
if (log && log.messages.length > 0) {
|
||||||
|
initialMessages = deserializeMessages(log.messages)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[ACP] Failed to load session history:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.createSession(
|
||||||
|
{
|
||||||
|
cwd: params.cwd,
|
||||||
|
mcpServers: params.mcpServers ?? [],
|
||||||
|
_meta: params._meta,
|
||||||
|
},
|
||||||
|
{ sessionId: params.sessionId, initialMessages },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Replay history to client if loaded
|
||||||
|
if (initialMessages && initialMessages.length > 0) {
|
||||||
|
const session = this.sessions.get(params.sessionId)
|
||||||
|
if (session) {
|
||||||
|
await replayHistoryMessages(
|
||||||
|
params.sessionId,
|
||||||
|
initialMessages as unknown as Array<Record<string, unknown>>,
|
||||||
|
this.conn,
|
||||||
|
session.toolUseCache,
|
||||||
|
this.clientCapabilities,
|
||||||
|
session.cwd,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionId: response.sessionId,
|
||||||
|
modes: response.modes,
|
||||||
|
models: response.models,
|
||||||
|
configOptions: response.configOptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async teardownSession(sessionId: string): Promise<void> {
|
||||||
|
const session = this.sessions.get(sessionId)
|
||||||
|
if (!session) return
|
||||||
|
|
||||||
|
await this.cancel({ sessionId })
|
||||||
|
this.sessions.delete(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private applySessionMode(sessionId: string, modeId: string): void {
|
||||||
|
const validModes = ['auto', 'default', 'acceptEdits', 'bypassPermissions', 'dontAsk', 'plan']
|
||||||
|
if (!validModes.includes(modeId)) {
|
||||||
|
throw new Error(`Invalid mode: ${modeId}`)
|
||||||
|
}
|
||||||
|
const session = this.sessions.get(sessionId)
|
||||||
|
if (session) {
|
||||||
|
session.modes = { ...session.modes, currentModeId: modeId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateConfigOption(
|
||||||
|
sessionId: string,
|
||||||
|
configId: string,
|
||||||
|
value: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const session = this.sessions.get(sessionId)
|
||||||
|
if (!session) return
|
||||||
|
|
||||||
|
this.syncSessionConfigState(session, configId, value)
|
||||||
|
|
||||||
|
session.configOptions = session.configOptions.map((o) =>
|
||||||
|
o.id === configId && typeof o.currentValue === 'string'
|
||||||
|
? { ...o, currentValue: value }
|
||||||
|
: o,
|
||||||
|
)
|
||||||
|
|
||||||
|
await this.conn.sessionUpdate({
|
||||||
|
sessionId,
|
||||||
|
update: {
|
||||||
|
sessionUpdate: 'config_option_update',
|
||||||
|
configOptions: session.configOptions,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncSessionConfigState(
|
||||||
|
session: AcpSession,
|
||||||
|
configId: string,
|
||||||
|
value: string,
|
||||||
|
): void {
|
||||||
|
if (configId === 'mode') {
|
||||||
|
session.modes = { ...session.modes, currentModeId: value }
|
||||||
|
} else if (configId === 'model') {
|
||||||
|
session.models = { ...session.models, currentModelId: value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendAvailableCommandsUpdate(sessionId: string): Promise<void> {
|
||||||
|
const session = this.sessions.get(sessionId)
|
||||||
|
if (!session) return
|
||||||
|
|
||||||
|
const availableCommands = session.commands
|
||||||
|
.filter(
|
||||||
|
cmd =>
|
||||||
|
cmd.type === 'prompt' &&
|
||||||
|
!cmd.isHidden &&
|
||||||
|
cmd.userInvocable !== false,
|
||||||
|
)
|
||||||
|
.map(cmd => ({
|
||||||
|
name: cmd.name,
|
||||||
|
description: cmd.description,
|
||||||
|
input: cmd.argumentHint ? { hint: cmd.argumentHint } : undefined,
|
||||||
|
}))
|
||||||
|
|
||||||
|
await this.conn.sessionUpdate({
|
||||||
|
sessionId,
|
||||||
|
update: {
|
||||||
|
sessionUpdate: 'available_commands_update',
|
||||||
|
availableCommands,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read a setting from Claude config (simplified — no file watching) */
|
||||||
|
private getSetting<T>(key: string): T | undefined {
|
||||||
|
// Simplified: read from environment or return undefined
|
||||||
|
// In a full implementation, this would read from settings.json
|
||||||
|
return undefined as T | undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Extract prompt text from ACP ContentBlock array for QueryEngine input */
|
||||||
|
function promptToQueryInput(
|
||||||
|
prompt: Array<ContentBlock> | undefined,
|
||||||
|
): string {
|
||||||
|
if (!prompt || prompt.length === 0) return ''
|
||||||
|
|
||||||
|
const parts: string[] = []
|
||||||
|
for (const block of prompt) {
|
||||||
|
const b = block as Record<string, unknown>
|
||||||
|
if (b.type === 'text') {
|
||||||
|
parts.push(b.text as string)
|
||||||
|
} else if (b.type === 'resource_link') {
|
||||||
|
parts.push(`[${b.name ?? ''}](${b.uri as string})`)
|
||||||
|
} else if (b.type === 'resource') {
|
||||||
|
const resource = b.resource as Record<string, unknown> | undefined
|
||||||
|
if (resource && 'text' in resource) {
|
||||||
|
parts.push(resource.text as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ignore image and other types for text-based prompt
|
||||||
|
}
|
||||||
|
return parts.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildConfigOptions(
|
||||||
|
modes: SessionModeState,
|
||||||
|
models: SessionModelState,
|
||||||
|
): SessionConfigOption[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'mode',
|
||||||
|
name: 'Mode',
|
||||||
|
description: 'Session permission mode',
|
||||||
|
category: 'mode',
|
||||||
|
type: 'select' as const,
|
||||||
|
currentValue: modes.currentModeId,
|
||||||
|
options: modes.availableModes.map((m: SessionModeState['availableModes'][number]) => ({
|
||||||
|
value: m.id,
|
||||||
|
name: m.name,
|
||||||
|
description: m.description,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'model',
|
||||||
|
name: 'Model',
|
||||||
|
description: 'AI model to use',
|
||||||
|
category: 'model',
|
||||||
|
type: 'select' as const,
|
||||||
|
currentValue: models.currentModelId,
|
||||||
|
options: models.availableModels.map((m: SessionModelState['availableModels'][number]) => ({
|
||||||
|
value: m.modelId,
|
||||||
|
name: m.name,
|
||||||
|
description: m.description ?? undefined,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
] as SessionConfigOption[]
|
||||||
|
}
|
||||||
1254
src/services/acp/bridge.ts
Normal file
1254
src/services/acp/bridge.ts
Normal file
File diff suppressed because it is too large
Load Diff
77
src/services/acp/entry.ts
Normal file
77
src/services/acp/entry.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import {
|
||||||
|
AgentSideConnection,
|
||||||
|
ndJsonStream,
|
||||||
|
} from '@agentclientprotocol/sdk'
|
||||||
|
import type { Stream } from '@agentclientprotocol/sdk'
|
||||||
|
import { Readable, Writable } from 'node:stream'
|
||||||
|
import { AcpAgent } from './agent.js'
|
||||||
|
import { enableConfigs } from '../../utils/config.js'
|
||||||
|
import { applySafeConfigEnvironmentVariables } from '../../utils/managedEnv.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an ACP Stream from a pair of Node.js streams.
|
||||||
|
*/
|
||||||
|
export function createAcpStream(
|
||||||
|
nodeReadable: NodeJS.ReadableStream,
|
||||||
|
nodeWritable: NodeJS.WritableStream,
|
||||||
|
): Stream {
|
||||||
|
const readableFromClient = Readable.toWeb(
|
||||||
|
nodeReadable as typeof process.stdin,
|
||||||
|
) as unknown as ReadableStream<Uint8Array>
|
||||||
|
const writableToClient = Writable.toWeb(
|
||||||
|
nodeWritable as typeof process.stdout,
|
||||||
|
) as unknown as WritableStream<Uint8Array>
|
||||||
|
return ndJsonStream(writableToClient, readableFromClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for the ACP (Agent Client Protocol) agent mode.
|
||||||
|
*/
|
||||||
|
export async function runAcpAgent(): Promise<void> {
|
||||||
|
enableConfigs()
|
||||||
|
|
||||||
|
// Apply environment variables from settings.json (ANTHROPIC_BASE_URL,
|
||||||
|
// ANTHROPIC_AUTH_TOKEN, model overrides, etc.) so the API client can
|
||||||
|
// authenticate. Without this, Zed-launched processes won't have these
|
||||||
|
// env vars in process.env.
|
||||||
|
applySafeConfigEnvironmentVariables()
|
||||||
|
|
||||||
|
const stream = createAcpStream(process.stdin, process.stdout)
|
||||||
|
|
||||||
|
let agent!: AcpAgent
|
||||||
|
const connection = new AgentSideConnection((conn) => {
|
||||||
|
agent = new AcpAgent(conn)
|
||||||
|
return agent
|
||||||
|
}, stream)
|
||||||
|
|
||||||
|
// stdout is used for ACP messages — redirect console to stderr
|
||||||
|
console.log = console.error
|
||||||
|
console.info = console.error
|
||||||
|
console.warn = console.error
|
||||||
|
console.debug = console.error
|
||||||
|
|
||||||
|
async function shutdown(): Promise<void> {
|
||||||
|
// Clean up all active sessions
|
||||||
|
for (const [sessionId] of agent.sessions) {
|
||||||
|
try {
|
||||||
|
await agent.unstable_closeSession({ sessionId })
|
||||||
|
} catch {
|
||||||
|
// Best-effort cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit cleanly when the ACP connection closes
|
||||||
|
connection.closed.then(shutdown).catch(shutdown)
|
||||||
|
|
||||||
|
process.on('SIGTERM', shutdown)
|
||||||
|
process.on('SIGINT', shutdown)
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
console.error('Unhandled Rejection at:', promise, 'reason:', reason)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Keep process alive while connection is open
|
||||||
|
process.stdin.resume()
|
||||||
|
}
|
||||||
224
src/services/acp/permissions.ts
Normal file
224
src/services/acp/permissions.ts
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
/**
|
||||||
|
* Permission bridge: maps Claude Code's canUseTool / PermissionDecision
|
||||||
|
* system to ACP's requestPermission() flow.
|
||||||
|
*
|
||||||
|
* Supports:
|
||||||
|
* - bypassPermissions mode (auto-allow all tools)
|
||||||
|
* - ExitPlanMode special handling (multi-option: Yes+auto/acceptEdits/default/No)
|
||||||
|
* - Always Allow
|
||||||
|
* - Standard allow_once/allow_always/reject_once
|
||||||
|
*/
|
||||||
|
import type {
|
||||||
|
AgentSideConnection,
|
||||||
|
PermissionOption,
|
||||||
|
ToolCallUpdate,
|
||||||
|
ClientCapabilities,
|
||||||
|
} from '@agentclientprotocol/sdk'
|
||||||
|
import type { CanUseToolFn } from '../../hooks/useCanUseTool.js'
|
||||||
|
import type {
|
||||||
|
PermissionAllowDecision,
|
||||||
|
PermissionAskDecision,
|
||||||
|
PermissionDenyDecision,
|
||||||
|
} from '../../types/permissions.js'
|
||||||
|
import type { Tool as ToolType, ToolUseContext } from '../../Tool.js'
|
||||||
|
import type { AssistantMessage } from '../../types/message.js'
|
||||||
|
import { toolInfoFromToolUse } from './bridge.js'
|
||||||
|
|
||||||
|
const IS_ROOT =
|
||||||
|
typeof process.geteuid === 'function'
|
||||||
|
? process.geteuid() === 0
|
||||||
|
: typeof process.getuid === 'function'
|
||||||
|
? process.getuid() === 0
|
||||||
|
: false
|
||||||
|
const ALLOW_BYPASS = !IS_ROOT || !!process.env.IS_SANDBOX
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a CanUseToolFn that delegates permission decisions to the
|
||||||
|
* ACP client via requestPermission().
|
||||||
|
*/
|
||||||
|
export function createAcpCanUseTool(
|
||||||
|
conn: AgentSideConnection,
|
||||||
|
sessionId: string,
|
||||||
|
getCurrentMode: () => string,
|
||||||
|
clientCapabilities?: ClientCapabilities,
|
||||||
|
cwd?: string,
|
||||||
|
): CanUseToolFn {
|
||||||
|
return async (
|
||||||
|
tool: ToolType,
|
||||||
|
input: Record<string, unknown>,
|
||||||
|
_context: ToolUseContext,
|
||||||
|
_assistantMessage: AssistantMessage,
|
||||||
|
toolUseID: string,
|
||||||
|
_forceDecision?: PermissionAllowDecision | PermissionAskDecision | PermissionDenyDecision,
|
||||||
|
): Promise<PermissionAllowDecision | PermissionAskDecision | PermissionDenyDecision> => {
|
||||||
|
const supportsTerminalOutput = checkTerminalOutput(clientCapabilities)
|
||||||
|
|
||||||
|
// ── ExitPlanMode special handling ────────────────────────────
|
||||||
|
if (tool.name === 'ExitPlanMode') {
|
||||||
|
return handleExitPlanMode(conn, sessionId, toolUseID, input, supportsTerminalOutput, cwd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── bypassPermissions mode ───────────────────────────────────
|
||||||
|
if (getCurrentMode() === 'bypassPermissions') {
|
||||||
|
return {
|
||||||
|
behavior: 'allow',
|
||||||
|
updatedInput: input,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Standard tool permission ─────────────────────────────────
|
||||||
|
const info = toolInfoFromToolUse(
|
||||||
|
{ name: tool.name, id: toolUseID, input },
|
||||||
|
supportsTerminalOutput,
|
||||||
|
cwd,
|
||||||
|
)
|
||||||
|
|
||||||
|
const toolCall: ToolCallUpdate = {
|
||||||
|
toolCallId: toolUseID,
|
||||||
|
title: info.title,
|
||||||
|
kind: info.kind,
|
||||||
|
status: 'pending',
|
||||||
|
rawInput: input,
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: Array<PermissionOption> = [
|
||||||
|
{ kind: 'allow_always', name: 'Always Allow', optionId: 'allow_always' },
|
||||||
|
{ kind: 'allow_once', name: 'Allow', optionId: 'allow' },
|
||||||
|
{ kind: 'reject_once', name: 'Reject', optionId: 'reject' },
|
||||||
|
]
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await conn.requestPermission({
|
||||||
|
sessionId,
|
||||||
|
toolCall,
|
||||||
|
options,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.outcome.outcome === 'cancelled') {
|
||||||
|
return {
|
||||||
|
behavior: 'deny',
|
||||||
|
message: 'Permission request cancelled by client',
|
||||||
|
decisionReason: { type: 'mode', mode: 'default' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
response.outcome.outcome === 'selected' &&
|
||||||
|
'optionId' in response.outcome &&
|
||||||
|
response.outcome.optionId !== undefined
|
||||||
|
) {
|
||||||
|
const optionId = response.outcome.optionId
|
||||||
|
if (optionId === 'allow' || optionId === 'allow_always') {
|
||||||
|
return {
|
||||||
|
behavior: 'allow',
|
||||||
|
updatedInput: input,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: deny
|
||||||
|
return {
|
||||||
|
behavior: 'deny',
|
||||||
|
message: 'Permission denied by client',
|
||||||
|
decisionReason: { type: 'mode', mode: 'default' },
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
behavior: 'deny',
|
||||||
|
message: 'Permission request failed',
|
||||||
|
decisionReason: { type: 'mode', mode: 'default' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExitPlanMode(
|
||||||
|
conn: AgentSideConnection,
|
||||||
|
sessionId: string,
|
||||||
|
toolUseID: string,
|
||||||
|
input: Record<string, unknown>,
|
||||||
|
supportsTerminalOutput: boolean,
|
||||||
|
cwd?: string,
|
||||||
|
): Promise<PermissionAllowDecision | PermissionDenyDecision> {
|
||||||
|
const options: Array<PermissionOption> = [
|
||||||
|
{ kind: 'allow_always', name: 'Yes, and use "auto" mode', optionId: 'auto' },
|
||||||
|
{ kind: 'allow_always', name: 'Yes, and auto-accept edits', optionId: 'acceptEdits' },
|
||||||
|
{ kind: 'allow_once', name: 'Yes, and manually approve edits', optionId: 'default' },
|
||||||
|
{ kind: 'reject_once', name: 'No, keep planning', optionId: 'plan' },
|
||||||
|
]
|
||||||
|
if (ALLOW_BYPASS) {
|
||||||
|
options.unshift({
|
||||||
|
kind: 'allow_always',
|
||||||
|
name: 'Yes, and bypass permissions',
|
||||||
|
optionId: 'bypassPermissions',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = toolInfoFromToolUse(
|
||||||
|
{ name: 'ExitPlanMode', id: toolUseID, input },
|
||||||
|
supportsTerminalOutput,
|
||||||
|
cwd,
|
||||||
|
)
|
||||||
|
|
||||||
|
const toolCall: ToolCallUpdate = {
|
||||||
|
toolCallId: toolUseID,
|
||||||
|
title: info.title,
|
||||||
|
kind: info.kind,
|
||||||
|
status: 'pending',
|
||||||
|
rawInput: input,
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await conn.requestPermission({
|
||||||
|
sessionId,
|
||||||
|
toolCall,
|
||||||
|
options,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.outcome.outcome === 'cancelled') {
|
||||||
|
return {
|
||||||
|
behavior: 'deny',
|
||||||
|
message: 'Tool use aborted',
|
||||||
|
decisionReason: { type: 'mode', mode: 'default' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
response.outcome.outcome === 'selected' &&
|
||||||
|
'optionId' in response.outcome &&
|
||||||
|
response.outcome.optionId !== undefined
|
||||||
|
) {
|
||||||
|
const selectedOption = response.outcome.optionId
|
||||||
|
if (
|
||||||
|
selectedOption === 'default' ||
|
||||||
|
selectedOption === 'acceptEdits' ||
|
||||||
|
selectedOption === 'auto' ||
|
||||||
|
selectedOption === 'bypassPermissions'
|
||||||
|
) {
|
||||||
|
await conn.sessionUpdate({
|
||||||
|
sessionId,
|
||||||
|
update: {
|
||||||
|
sessionUpdate: 'current_mode_update',
|
||||||
|
currentModeId: selectedOption,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
behavior: 'allow',
|
||||||
|
updatedInput: input,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
behavior: 'deny',
|
||||||
|
message: 'User rejected request to exit plan mode.',
|
||||||
|
decisionReason: { type: 'mode', mode: 'plan' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkTerminalOutput(clientCapabilities?: ClientCapabilities): boolean {
|
||||||
|
if (!clientCapabilities) return false
|
||||||
|
const meta = (clientCapabilities as unknown as Record<string, unknown>)._meta
|
||||||
|
if (!meta || typeof meta !== 'object') return false
|
||||||
|
return (meta as Record<string, unknown>)['terminal_output'] === true
|
||||||
|
}
|
||||||
208
src/services/acp/utils.ts
Normal file
208
src/services/acp/utils.ts
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
/**
|
||||||
|
* Shared utilities for the ACP service.
|
||||||
|
* Ported from claude-agent-acp-main/src/utils.ts and acp-agent.ts helpers.
|
||||||
|
*/
|
||||||
|
import { Readable, Writable } from 'node:stream'
|
||||||
|
import type { PermissionMode } from '../../entrypoints/sdk/coreTypes.generated.js'
|
||||||
|
|
||||||
|
// ── Pushable ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pushable async iterable: allows you to push items and consume them
|
||||||
|
* with for-await. Useful for bridging push-based and async-iterator-based code.
|
||||||
|
*/
|
||||||
|
export class Pushable<T> implements AsyncIterable<T> {
|
||||||
|
private queue: T[] = []
|
||||||
|
private resolvers: ((value: IteratorResult<T>) => void)[] = []
|
||||||
|
private done = false
|
||||||
|
|
||||||
|
push(item: T) {
|
||||||
|
if (this.resolvers.length > 0) {
|
||||||
|
const resolve = this.resolvers.shift()!
|
||||||
|
resolve({ value: item, done: false })
|
||||||
|
} else {
|
||||||
|
this.queue.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
end() {
|
||||||
|
this.done = true
|
||||||
|
while (this.resolvers.length > 0) {
|
||||||
|
const resolve = this.resolvers.shift()!
|
||||||
|
resolve({ value: undefined as unknown as T, done: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.asyncIterator](): AsyncIterator<T> {
|
||||||
|
return {
|
||||||
|
next: (): Promise<IteratorResult<T>> => {
|
||||||
|
if (this.queue.length > 0) {
|
||||||
|
const value = this.queue.shift()!
|
||||||
|
return Promise.resolve({ value, done: false })
|
||||||
|
}
|
||||||
|
if (this.done) {
|
||||||
|
return Promise.resolve({ value: undefined as unknown as T, done: true })
|
||||||
|
}
|
||||||
|
return new Promise<IteratorResult<T>>((resolve) => {
|
||||||
|
this.resolvers.push(resolve)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Stream helpers ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function nodeToWebWritable(nodeStream: Writable): WritableStream<Uint8Array> {
|
||||||
|
return new WritableStream<Uint8Array>({
|
||||||
|
write(chunk) {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
nodeStream.write(Buffer.from(chunk), (err) => {
|
||||||
|
if (err) reject(err)
|
||||||
|
else resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nodeToWebReadable(nodeStream: Readable): ReadableStream<Uint8Array> {
|
||||||
|
return new ReadableStream<Uint8Array>({
|
||||||
|
start(controller) {
|
||||||
|
nodeStream.on('data', (chunk: Buffer) => {
|
||||||
|
controller.enqueue(new Uint8Array(chunk))
|
||||||
|
})
|
||||||
|
nodeStream.on('end', () => controller.close())
|
||||||
|
nodeStream.on('error', (err) => controller.error(err))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── unreachable ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function unreachable(
|
||||||
|
value: never,
|
||||||
|
logger: { error: (...args: unknown[]) => void } = console,
|
||||||
|
): void {
|
||||||
|
let valueAsString: unknown
|
||||||
|
try {
|
||||||
|
valueAsString = JSON.stringify(value)
|
||||||
|
} catch {
|
||||||
|
valueAsString = value
|
||||||
|
}
|
||||||
|
logger.error(`Unexpected case: ${valueAsString}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Permission mode resolution ────────────────────────────────────
|
||||||
|
|
||||||
|
// Bypass Permissions doesn't work if we are a root/sudo user
|
||||||
|
const IS_ROOT =
|
||||||
|
typeof process.geteuid === 'function'
|
||||||
|
? process.geteuid() === 0
|
||||||
|
: typeof process.getuid === 'function'
|
||||||
|
? process.getuid() === 0
|
||||||
|
: false
|
||||||
|
const ALLOW_BYPASS = !IS_ROOT || !!process.env.IS_SANDBOX
|
||||||
|
|
||||||
|
const PERMISSION_MODE_ALIASES: Record<string, PermissionMode> = {
|
||||||
|
auto: 'auto',
|
||||||
|
default: 'default',
|
||||||
|
acceptedits: 'acceptEdits',
|
||||||
|
dontask: 'dontAsk',
|
||||||
|
plan: 'plan',
|
||||||
|
bypasspermissions: 'bypassPermissions',
|
||||||
|
bypass: 'bypassPermissions',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePermissionMode(defaultMode?: unknown): PermissionMode {
|
||||||
|
if (defaultMode === undefined) {
|
||||||
|
return 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof defaultMode !== 'string') {
|
||||||
|
throw new Error('Invalid permissions.defaultMode: expected a string.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = defaultMode.trim().toLowerCase()
|
||||||
|
if (normalized === '') {
|
||||||
|
throw new Error('Invalid permissions.defaultMode: expected a non-empty string.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapped = PERMISSION_MODE_ALIASES[normalized]
|
||||||
|
if (!mapped) {
|
||||||
|
throw new Error(`Invalid permissions.defaultMode: ${defaultMode}.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapped === 'bypassPermissions' && !ALLOW_BYPASS) {
|
||||||
|
throw new Error(
|
||||||
|
'Invalid permissions.defaultMode: bypassPermissions is not available when running as root.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Session fingerprint ───────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a stable fingerprint of the session-defining params so we can
|
||||||
|
* detect when a loadSession/resumeSession call requires tearing down and
|
||||||
|
* recreating the underlying QueryEngine.
|
||||||
|
*/
|
||||||
|
export function computeSessionFingerprint(params: {
|
||||||
|
cwd: string
|
||||||
|
mcpServers?: Array<{ name: string; [key: string]: unknown }>
|
||||||
|
}): string {
|
||||||
|
const servers = [...(params.mcpServers ?? [])].sort((a, b) =>
|
||||||
|
a.name.localeCompare(b.name),
|
||||||
|
)
|
||||||
|
return JSON.stringify({ cwd: params.cwd, mcpServers: servers })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Title sanitization ────────────────────────────────────────────
|
||||||
|
|
||||||
|
const MAX_TITLE_LENGTH = 256
|
||||||
|
|
||||||
|
export function sanitizeTitle(text: string): string {
|
||||||
|
const sanitized = text
|
||||||
|
.replace(/[\r\n]+/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
if (sanitized.length <= MAX_TITLE_LENGTH) {
|
||||||
|
return sanitized
|
||||||
|
}
|
||||||
|
return sanitized.slice(0, MAX_TITLE_LENGTH - 1) + '…'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Path display helpers ──────────────────────────────────────────
|
||||||
|
|
||||||
|
import * as path from 'node:path'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an absolute file path to a project-relative path for display.
|
||||||
|
* Returns the original path if it's outside the project directory or if no cwd is provided.
|
||||||
|
*/
|
||||||
|
export function toDisplayPath(filePath: string, cwd?: string): string {
|
||||||
|
if (!cwd) return filePath
|
||||||
|
const resolvedCwd = path.resolve(cwd)
|
||||||
|
const resolvedFile = path.resolve(filePath)
|
||||||
|
if (
|
||||||
|
resolvedFile.startsWith(resolvedCwd + path.sep) ||
|
||||||
|
resolvedFile === resolvedCwd
|
||||||
|
) {
|
||||||
|
return path.relative(resolvedCwd, resolvedFile)
|
||||||
|
}
|
||||||
|
return filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Markdown helpers ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function markdownEscape(text: string): string {
|
||||||
|
let escape = '```'
|
||||||
|
for (const m of text.matchAll(/^```+/gm) ?? []) {
|
||||||
|
while (m[0].length >= escape.length) {
|
||||||
|
escape += '`'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return escape + '\n' + text + (text.endsWith('\n') ? '' : '\n') + escape
|
||||||
|
}
|
||||||
@@ -230,7 +230,7 @@ import { getInitializationStatus } from '../lsp/manager.js'
|
|||||||
import { isToolFromMcpServer } from '../mcp/utils.js'
|
import { isToolFromMcpServer } from '../mcp/utils.js'
|
||||||
import { recordLLMObservation } from '../langfuse/index.js'
|
import { recordLLMObservation } from '../langfuse/index.js'
|
||||||
import type { LangfuseSpan } from '../langfuse/index.js'
|
import type { LangfuseSpan } from '../langfuse/index.js'
|
||||||
import { convertMessagesToLangfuse, convertOutputToLangfuse } from '../langfuse/convert.js'
|
import { convertMessagesToLangfuse, convertOutputToLangfuse, convertToolsToLangfuse } from '../langfuse/convert.js'
|
||||||
import { withStreamingVCR, withVCR } from '../vcr.js'
|
import { withStreamingVCR, withVCR } from '../vcr.js'
|
||||||
import { CLIENT_REQUEST_ID_HEADER, getAnthropicClient } from './client.js'
|
import { CLIENT_REQUEST_ID_HEADER, getAnthropicClient } from './client.js'
|
||||||
import {
|
import {
|
||||||
@@ -2907,10 +2907,16 @@ async function* queryModel(
|
|||||||
provider: getAPIProvider(),
|
provider: getAPIProvider(),
|
||||||
input: convertMessagesToLangfuse(messagesForAPI, systemPrompt),
|
input: convertMessagesToLangfuse(messagesForAPI, systemPrompt),
|
||||||
output: convertOutputToLangfuse(newMessages),
|
output: convertOutputToLangfuse(newMessages),
|
||||||
usage: { input_tokens: usage.input_tokens, output_tokens: usage.output_tokens },
|
usage: {
|
||||||
|
input_tokens: usage.input_tokens,
|
||||||
|
output_tokens: usage.output_tokens,
|
||||||
|
cache_creation_input_tokens: usage.cache_creation_input_tokens,
|
||||||
|
cache_read_input_tokens: usage.cache_read_input_tokens,
|
||||||
|
},
|
||||||
startTime: new Date(startIncludingRetries),
|
startTime: new Date(startIncludingRetries),
|
||||||
endTime: new Date(),
|
endTime: new Date(),
|
||||||
completionStartTime: ttftMs > 0 ? new Date(start + ttftMs) : undefined,
|
completionStartTime: ttftMs > 0 ? new Date(start + ttftMs) : undefined,
|
||||||
|
tools: convertToolsToLangfuse(toolSchemas as unknown[]),
|
||||||
})
|
})
|
||||||
|
|
||||||
void options.getToolPermissionContext().then(permissionContext => {
|
void options.getToolPermissionContext().then(permissionContext => {
|
||||||
|
|||||||
@@ -194,6 +194,16 @@ mock.module('../convertTools.js', () => ({
|
|||||||
mock.module('../../../../utils/context.js', () => ({
|
mock.module('../../../../utils/context.js', () => ({
|
||||||
getModelMaxOutputTokens: () => ({ upperLimit: 8192, default: 8192 }),
|
getModelMaxOutputTokens: () => ({ upperLimit: 8192, default: 8192 }),
|
||||||
getContextWindowForModel: () => 200_000,
|
getContextWindowForModel: () => 200_000,
|
||||||
|
modelSupports1M: () => false,
|
||||||
|
has1mContext: () => false,
|
||||||
|
is1mContextDisabled: () => false,
|
||||||
|
getSonnet1mExpTreatmentEnabled: () => false,
|
||||||
|
MODEL_CONTEXT_WINDOW_DEFAULT: 200_000,
|
||||||
|
COMPACT_MAX_OUTPUT_TOKENS: 20_000,
|
||||||
|
CAPPED_DEFAULT_MAX_TOKENS: 8_000,
|
||||||
|
ESCALATED_MAX_TOKENS: 64_000,
|
||||||
|
calculateContextPercentages: () => ({ used: null, remaining: null }),
|
||||||
|
getMaxThinkingTokensForModel: () => 8191,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
mock.module('../../../../utils/messages.js', () => ({
|
mock.module('../../../../utils/messages.js', () => ({
|
||||||
@@ -211,6 +221,22 @@ mock.module('../../../../utils/api.js', () => ({
|
|||||||
toolToAPISchema: async (t: any) => t,
|
toolToAPISchema: async (t: any) => t,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
mock.module('../../../../Tool.js', () => ({
|
||||||
|
getEmptyToolPermissionContext: () => ({
|
||||||
|
alwaysAllow: [],
|
||||||
|
alwaysDeny: [],
|
||||||
|
needsPermission: [],
|
||||||
|
mode: 'default',
|
||||||
|
isBypassingPermissions: false,
|
||||||
|
}),
|
||||||
|
toolMatchesName: () => false,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module('../../../../utils/envUtils.js', () => ({
|
||||||
|
isEnvTruthy: (v: string | undefined) => v === '1' || v === 'true',
|
||||||
|
isEnvDefinedFalsy: (v: string | undefined) => v === '0' || v === 'false' || v === 'no' || v === 'off',
|
||||||
|
}))
|
||||||
|
|
||||||
mock.module('../../../../utils/toolSearch.js', () => ({
|
mock.module('../../../../utils/toolSearch.js', () => ({
|
||||||
isToolSearchEnabled: async () => false,
|
isToolSearchEnabled: async () => false,
|
||||||
extractDiscoveredToolNames: () => new Set(),
|
extractDiscoveredToolNames: () => new Set(),
|
||||||
@@ -451,4 +477,83 @@ describe('queryModelOpenAI — max_tokens forwarded to request', () => {
|
|||||||
expect(_lastCreateArgs).not.toBeNull()
|
expect(_lastCreateArgs).not.toBeNull()
|
||||||
expect(_lastCreateArgs!.max_tokens).toBe(8192)
|
expect(_lastCreateArgs!.max_tokens).toBe(8192)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('OPENAI_MAX_TOKENS env var overrides max_tokens', async () => {
|
||||||
|
const original = process.env.OPENAI_MAX_TOKENS
|
||||||
|
process.env.OPENAI_MAX_TOKENS = '4096'
|
||||||
|
try {
|
||||||
|
_nextEvents = [
|
||||||
|
makeMessageStart(),
|
||||||
|
makeContentBlockStart(0, 'text'),
|
||||||
|
makeTextDelta(0, 'hi'),
|
||||||
|
makeContentBlockStop(0),
|
||||||
|
makeMessageDelta('end_turn', 5),
|
||||||
|
makeMessageStop(),
|
||||||
|
]
|
||||||
|
|
||||||
|
await runQueryModel(_nextEvents)
|
||||||
|
|
||||||
|
expect(_lastCreateArgs).not.toBeNull()
|
||||||
|
expect(_lastCreateArgs!.max_tokens).toBe(4096)
|
||||||
|
} finally {
|
||||||
|
if (original === undefined) {
|
||||||
|
delete process.env.OPENAI_MAX_TOKENS
|
||||||
|
} else {
|
||||||
|
process.env.OPENAI_MAX_TOKENS = original
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('CLAUDE_CODE_MAX_OUTPUT_TOKENS env var overrides max_tokens', async () => {
|
||||||
|
const original = process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
|
||||||
|
process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = '2048'
|
||||||
|
try {
|
||||||
|
_nextEvents = [
|
||||||
|
makeMessageStart(),
|
||||||
|
makeContentBlockStart(0, 'text'),
|
||||||
|
makeTextDelta(0, 'hi'),
|
||||||
|
makeContentBlockStop(0),
|
||||||
|
makeMessageDelta('end_turn', 5),
|
||||||
|
makeMessageStop(),
|
||||||
|
]
|
||||||
|
|
||||||
|
await runQueryModel(_nextEvents)
|
||||||
|
|
||||||
|
expect(_lastCreateArgs).not.toBeNull()
|
||||||
|
expect(_lastCreateArgs!.max_tokens).toBe(2048)
|
||||||
|
} finally {
|
||||||
|
if (original === undefined) {
|
||||||
|
delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
|
||||||
|
} else {
|
||||||
|
process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = original
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('OPENAI_MAX_TOKENS takes priority over CLAUDE_CODE_MAX_OUTPUT_TOKENS', async () => {
|
||||||
|
const origOpenai = process.env.OPENAI_MAX_TOKENS
|
||||||
|
const origClaude = process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
|
||||||
|
process.env.OPENAI_MAX_TOKENS = '4096'
|
||||||
|
process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = '2048'
|
||||||
|
try {
|
||||||
|
_nextEvents = [
|
||||||
|
makeMessageStart(),
|
||||||
|
makeContentBlockStart(0, 'text'),
|
||||||
|
makeTextDelta(0, 'hi'),
|
||||||
|
makeContentBlockStop(0),
|
||||||
|
makeMessageDelta('end_turn', 5),
|
||||||
|
makeMessageStop(),
|
||||||
|
]
|
||||||
|
|
||||||
|
await runQueryModel(_nextEvents)
|
||||||
|
|
||||||
|
expect(_lastCreateArgs).not.toBeNull()
|
||||||
|
expect(_lastCreateArgs!.max_tokens).toBe(4096)
|
||||||
|
} finally {
|
||||||
|
if (origOpenai === undefined) delete process.env.OPENAI_MAX_TOKENS
|
||||||
|
else process.env.OPENAI_MAX_TOKENS = origOpenai
|
||||||
|
if (origClaude === undefined) delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
|
||||||
|
else process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = origClaude
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -71,6 +71,28 @@ export function isOpenAIThinkingEnabled(model: string): boolean {
|
|||||||
return modelLower.includes('deepseek-reasoner') || modelLower.includes('deepseek-v3.2')
|
return modelLower.includes('deepseek-reasoner') || modelLower.includes('deepseek-v3.2')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve max output tokens for the OpenAI-compatible path.
|
||||||
|
*
|
||||||
|
* Override priority:
|
||||||
|
* 1. maxOutputTokensOverride (programmatic, from query pipeline)
|
||||||
|
* 2. OPENAI_MAX_TOKENS env var (OpenAI-specific, useful for local models
|
||||||
|
* with small context windows, e.g. RTX 3060 12GB running 65536-token models)
|
||||||
|
* 3. CLAUDE_CODE_MAX_OUTPUT_TOKENS env var (generic override)
|
||||||
|
* 4. upperLimit default (64000)
|
||||||
|
*
|
||||||
|
* @internal Exported for testing purposes only
|
||||||
|
*/
|
||||||
|
export function resolveOpenAIMaxTokens(
|
||||||
|
upperLimit: number,
|
||||||
|
maxOutputTokensOverride?: number,
|
||||||
|
): number {
|
||||||
|
return maxOutputTokensOverride
|
||||||
|
?? (process.env.OPENAI_MAX_TOKENS ? parseInt(process.env.OPENAI_MAX_TOKENS, 10) || undefined : undefined)
|
||||||
|
?? (process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS ? parseInt(process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS, 10) || undefined : undefined)
|
||||||
|
?? upperLimit
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the request body for OpenAI chat.completions.create().
|
* Build the request body for OpenAI chat.completions.create().
|
||||||
* Extracted for testability — the thinking mode params are injected here.
|
* Extracted for testability — the thinking mode params are injected here.
|
||||||
@@ -165,7 +187,7 @@ function assembleFinalAssistantOutputs(params: {
|
|||||||
if (stopReason === 'max_tokens') {
|
if (stopReason === 'max_tokens') {
|
||||||
outputs.push(createAssistantAPIErrorMessage({
|
outputs.push(createAssistantAPIErrorMessage({
|
||||||
content: `Output truncated: response exceeded the ${maxTokens} token limit. ` +
|
content: `Output truncated: response exceeded the ${maxTokens} token limit. ` +
|
||||||
`Set CLAUDE_CODE_MAX_OUTPUT_TOKENS to override.`,
|
`Set OPENAI_MAX_TOKENS or CLAUDE_CODE_MAX_OUTPUT_TOKENS to override.`,
|
||||||
apiError: 'max_output_tokens',
|
apiError: 'max_output_tokens',
|
||||||
error: 'max_output_tokens',
|
error: 'max_output_tokens',
|
||||||
}))
|
}))
|
||||||
@@ -286,8 +308,15 @@ export async function* queryModelOpenAI(
|
|||||||
// auto-retry at 64k in query.ts. The OpenAI path has no such retry, so
|
// auto-retry at 64k in query.ts. The OpenAI path has no such retry, so
|
||||||
// using the capped 8k default would silently truncate responses in
|
// using the capped 8k default would silently truncate responses in
|
||||||
// multi-turn conversations where thinking consumes most of the budget.
|
// multi-turn conversations where thinking consumes most of the budget.
|
||||||
|
//
|
||||||
|
// Override priority:
|
||||||
|
// 1. options.maxOutputTokensOverride (programmatic)
|
||||||
|
// 2. OPENAI_MAX_TOKENS env var (OpenAI-specific, useful for local models
|
||||||
|
// with small context windows, e.g. RTX 3060 12GB running 65536-token models)
|
||||||
|
// 3. CLAUDE_CODE_MAX_OUTPUT_TOKENS env var (generic override)
|
||||||
|
// 4. upperLimit default (64000)
|
||||||
const { upperLimit } = getModelMaxOutputTokens(openaiModel)
|
const { upperLimit } = getModelMaxOutputTokens(openaiModel)
|
||||||
const maxTokens = options.maxOutputTokensOverride ?? upperLimit
|
const maxTokens = resolveOpenAIMaxTokens(upperLimit, options.maxOutputTokensOverride)
|
||||||
|
|
||||||
// 11. Get client
|
// 11. Get client
|
||||||
const client = getOpenAIClient({
|
const client = getOpenAIClient({
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const mockRootEnd = mock(() => {})
|
|||||||
// Mock LangfuseOtelSpanAttributes (re-exported from @langfuse/core)
|
// Mock LangfuseOtelSpanAttributes (re-exported from @langfuse/core)
|
||||||
const mockLangfuseOtelSpanAttributes: Record<string, string> = {
|
const mockLangfuseOtelSpanAttributes: Record<string, string> = {
|
||||||
TRACE_SESSION_ID: 'session.id',
|
TRACE_SESSION_ID: 'session.id',
|
||||||
|
TRACE_USER_ID: 'user.id',
|
||||||
OBSERVATION_TYPE: 'observation.type',
|
OBSERVATION_TYPE: 'observation.type',
|
||||||
OBSERVATION_INPUT: 'observation.input',
|
OBSERVATION_INPUT: 'observation.input',
|
||||||
OBSERVATION_OUTPUT: 'observation.output',
|
OBSERVATION_OUTPUT: 'observation.output',
|
||||||
@@ -74,6 +75,14 @@ mock.module('src/utils/debug.js', () => ({
|
|||||||
logForDebugging: mock(() => {}),
|
logForDebugging: mock(() => {}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Mock user data — resolveLangfuseUserId uses getCoreUserData().email and .deviceId
|
||||||
|
mock.module('src/utils/user.js', () => ({
|
||||||
|
getCoreUserData: mock(() => ({
|
||||||
|
email: 'test-device-id',
|
||||||
|
deviceId: 'test-device-id',
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
|
||||||
describe('Langfuse integration', () => {
|
describe('Langfuse integration', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Reset env
|
// Reset env
|
||||||
@@ -275,6 +284,48 @@ describe('Langfuse integration', () => {
|
|||||||
}))
|
}))
|
||||||
expect(mockRootEnd).toHaveBeenCalled()
|
expect(mockRootEnd).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('includes cache tokens in usageDetails when provided', async () => {
|
||||||
|
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||||
|
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||||
|
const { createTrace, recordLLMObservation } = await import('../tracing.js')
|
||||||
|
const span = createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
|
||||||
|
mockStartObservation.mockClear()
|
||||||
|
mockRootUpdate.mockClear()
|
||||||
|
recordLLMObservation(span, {
|
||||||
|
model: 'claude-3',
|
||||||
|
provider: 'firstParty',
|
||||||
|
input: [],
|
||||||
|
output: [],
|
||||||
|
usage: { input_tokens: 10000, output_tokens: 50, cache_creation_input_tokens: 2000, cache_read_input_tokens: 7000 },
|
||||||
|
})
|
||||||
|
expect(mockRootUpdate).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
usageDetails: {
|
||||||
|
input: 19000, // 10000 + 2000 + 7000
|
||||||
|
output: 50,
|
||||||
|
cache_read: 7000,
|
||||||
|
cache_creation: 2000,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('omits cache fields when not provided', async () => {
|
||||||
|
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||||
|
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||||
|
const { createTrace, recordLLMObservation } = await import('../tracing.js')
|
||||||
|
const span = createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
|
||||||
|
mockRootUpdate.mockClear()
|
||||||
|
recordLLMObservation(span, {
|
||||||
|
model: 'claude-3',
|
||||||
|
provider: 'firstParty',
|
||||||
|
input: [],
|
||||||
|
output: [],
|
||||||
|
usage: { input_tokens: 100, output_tokens: 20 },
|
||||||
|
})
|
||||||
|
expect(mockRootUpdate).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
usageDetails: { input: 100, output: 20 },
|
||||||
|
}))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('recordToolObservation', () => {
|
describe('recordToolObservation', () => {
|
||||||
@@ -477,6 +528,70 @@ describe('Langfuse integration', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('createTrace with username', () => {
|
||||||
|
test('sets user.id attribute when username is provided', async () => {
|
||||||
|
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||||
|
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||||
|
mockSetAttribute.mockClear()
|
||||||
|
const { createTrace } = await import('../tracing.js')
|
||||||
|
const span = createTrace({
|
||||||
|
sessionId: 's1',
|
||||||
|
model: 'claude-3',
|
||||||
|
provider: 'firstParty',
|
||||||
|
username: 'user@example.com',
|
||||||
|
})
|
||||||
|
expect(span).not.toBeNull()
|
||||||
|
expect(mockSetAttribute).toHaveBeenCalledWith('user.id', 'user@example.com')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('falls back to LANGFUSE_USER_ID env when username not provided', async () => {
|
||||||
|
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||||
|
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||||
|
process.env.LANGFUSE_USER_ID = 'env-user@test.com'
|
||||||
|
mockSetAttribute.mockClear()
|
||||||
|
const { createTrace } = await import('../tracing.js')
|
||||||
|
const span = createTrace({
|
||||||
|
sessionId: 's1',
|
||||||
|
model: 'claude-3',
|
||||||
|
provider: 'firstParty',
|
||||||
|
})
|
||||||
|
expect(span).not.toBeNull()
|
||||||
|
expect(mockSetAttribute).toHaveBeenCalledWith('user.id', 'env-user@test.com')
|
||||||
|
delete process.env.LANGFUSE_USER_ID
|
||||||
|
})
|
||||||
|
|
||||||
|
test('falls back to deviceId when neither username nor env is provided', async () => {
|
||||||
|
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||||
|
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||||
|
delete process.env.LANGFUSE_USER_ID
|
||||||
|
mockSetAttribute.mockClear()
|
||||||
|
const { createTrace } = await import('../tracing.js')
|
||||||
|
createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
|
||||||
|
// Falls back to getCoreUserData().deviceId (mocked as 'test-device-id')
|
||||||
|
expect(mockSetAttribute).toHaveBeenCalledWith('user.id', 'test-device-id')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('username takes precedence over LANGFUSE_USER_ID env', async () => {
|
||||||
|
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||||
|
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||||
|
process.env.LANGFUSE_USER_ID = 'env-user@test.com'
|
||||||
|
mockSetAttribute.mockClear()
|
||||||
|
const { createTrace } = await import('../tracing.js')
|
||||||
|
createTrace({
|
||||||
|
sessionId: 's1',
|
||||||
|
model: 'claude-3',
|
||||||
|
provider: 'firstParty',
|
||||||
|
username: 'param-user@test.com',
|
||||||
|
})
|
||||||
|
const userIdCalls = mockSetAttribute.mock.calls.filter(
|
||||||
|
(call: unknown[]) => Array.isArray(call) && call[0] === 'user.id',
|
||||||
|
)
|
||||||
|
expect(userIdCalls.length).toBe(1)
|
||||||
|
expect((userIdCalls[0] as unknown[])[1]).toBe('param-user@test.com')
|
||||||
|
delete process.env.LANGFUSE_USER_ID
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('nested agent scenario', () => {
|
describe('nested agent scenario', () => {
|
||||||
test('sub-agent trace shares sessionId with parent', async () => {
|
test('sub-agent trace shares sessionId with parent', async () => {
|
||||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||||
@@ -538,6 +653,117 @@ describe('Langfuse integration', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('convertToolsToLangfuse', () => {
|
||||||
|
test('converts Anthropic tool schema to OpenAI-style format', async () => {
|
||||||
|
const { convertToolsToLangfuse } = await import('../convert.js')
|
||||||
|
const tools = [
|
||||||
|
{
|
||||||
|
name: 'BashTool',
|
||||||
|
description: 'Execute a bash command',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: { command: { type: 'string' } },
|
||||||
|
required: ['command'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const result = convertToolsToLangfuse(tools) as Array<Record<string, unknown>>
|
||||||
|
expect(result).toHaveLength(1)
|
||||||
|
expect(result[0]).toEqual({
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'BashTool',
|
||||||
|
description: 'Execute a bash command',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: { command: { type: 'string' } },
|
||||||
|
required: ['command'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('converts multiple tools', async () => {
|
||||||
|
const { convertToolsToLangfuse } = await import('../convert.js')
|
||||||
|
const tools = [
|
||||||
|
{ name: 'ReadTool', description: 'Read a file', input_schema: { type: 'object' } },
|
||||||
|
{ name: 'WriteTool', description: 'Write a file', input_schema: { type: 'object' } },
|
||||||
|
]
|
||||||
|
const result = convertToolsToLangfuse(tools) as Array<Record<string, unknown>>
|
||||||
|
expect(result).toHaveLength(2)
|
||||||
|
expect((result[0]!.function as Record<string, unknown>).name).toBe('ReadTool')
|
||||||
|
expect((result[1]!.function as Record<string, unknown>).name).toBe('WriteTool')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('falls back to parameters when input_schema is missing', async () => {
|
||||||
|
const { convertToolsToLangfuse } = await import('../convert.js')
|
||||||
|
const tools = [
|
||||||
|
{ name: 'Tool1', description: 'desc', parameters: { type: 'object', properties: { a: { type: 'string' } } } },
|
||||||
|
]
|
||||||
|
const result = convertToolsToLangfuse(tools) as Array<Record<string, unknown>>
|
||||||
|
expect((result[0]!.function as Record<string, unknown>).parameters).toEqual({
|
||||||
|
type: 'object',
|
||||||
|
properties: { a: { type: 'string' } },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('uses empty object when neither input_schema nor parameters exist', async () => {
|
||||||
|
const { convertToolsToLangfuse } = await import('../convert.js')
|
||||||
|
const tools = [{ name: 'Tool1', description: 'desc' }]
|
||||||
|
const result = convertToolsToLangfuse(tools) as Array<Record<string, unknown>>
|
||||||
|
expect((result[0]!.function as Record<string, unknown>).parameters).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns empty array for empty input', async () => {
|
||||||
|
const { convertToolsToLangfuse } = await import('../convert.js')
|
||||||
|
expect(convertToolsToLangfuse([])).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('recordLLMObservation with tools', () => {
|
||||||
|
test('wraps input into { messages, tools } when tools provided', async () => {
|
||||||
|
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||||
|
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||||
|
const { createTrace, recordLLMObservation } = await import('../tracing.js')
|
||||||
|
const span = createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
|
||||||
|
mockStartObservation.mockClear()
|
||||||
|
const messages = [{ role: 'user', content: 'hello' }]
|
||||||
|
const tools = [{ type: 'function', function: { name: 'Bash', description: 'Run', parameters: {} } }]
|
||||||
|
recordLLMObservation(span, {
|
||||||
|
model: 'claude-3',
|
||||||
|
provider: 'firstParty',
|
||||||
|
input: messages,
|
||||||
|
output: [],
|
||||||
|
usage: { input_tokens: 10, output_tokens: 5 },
|
||||||
|
tools,
|
||||||
|
})
|
||||||
|
expect(mockStartObservation).toHaveBeenCalledWith('ChatAnthropic', expect.objectContaining({
|
||||||
|
input: { messages, tools },
|
||||||
|
}), expect.objectContaining({
|
||||||
|
asType: 'generation',
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('keeps input as-is when tools not provided', async () => {
|
||||||
|
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||||
|
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||||
|
const { createTrace, recordLLMObservation } = await import('../tracing.js')
|
||||||
|
const span = createTrace({ sessionId: 's1', model: 'claude-3', provider: 'firstParty' })
|
||||||
|
mockStartObservation.mockClear()
|
||||||
|
const messages = [{ role: 'user', content: 'hello' }]
|
||||||
|
recordLLMObservation(span, {
|
||||||
|
model: 'claude-3',
|
||||||
|
provider: 'firstParty',
|
||||||
|
input: messages,
|
||||||
|
output: [],
|
||||||
|
usage: { input_tokens: 10, output_tokens: 5 },
|
||||||
|
})
|
||||||
|
expect(mockStartObservation).toHaveBeenCalledWith('ChatAnthropic', expect.objectContaining({
|
||||||
|
input: messages,
|
||||||
|
}), expect.any(Object))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('SDK exceptions do not affect main flow', () => {
|
describe('SDK exceptions do not affect main flow', () => {
|
||||||
test('createTrace returns null on SDK error', async () => {
|
test('createTrace returns null on SDK error', async () => {
|
||||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||||
|
|||||||
@@ -101,6 +101,21 @@ export function convertMessagesToLangfuse(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Convert Anthropic-style tool schemas to Langfuse-compatible OpenAI-style tool format */
|
||||||
|
export function convertToolsToLangfuse(tools: unknown[]): unknown[] {
|
||||||
|
return tools.map(tool => {
|
||||||
|
const t = tool as Record<string, unknown>
|
||||||
|
return {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: t.name,
|
||||||
|
description: t.description,
|
||||||
|
parameters: t.input_schema ?? t.parameters ?? {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/** Convert AssistantMessage[] (newMessages) → Langfuse output format (last assistant turn) */
|
/** Convert AssistantMessage[] (newMessages) → Langfuse output format (last assistant turn) */
|
||||||
export function convertOutputToLangfuse(
|
export function convertOutputToLangfuse(
|
||||||
messages: AssistantMessage[],
|
messages: AssistantMessage[],
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export { initLangfuse, shutdownLangfuse, isLangfuseEnabled, getLangfuseProcessor } from './client.js'
|
export { initLangfuse, shutdownLangfuse, isLangfuseEnabled, getLangfuseProcessor } from './client.js'
|
||||||
export { createTrace, createSubagentTrace, recordLLMObservation, recordToolObservation, endTrace } from './tracing.js'
|
export { createTrace, createSubagentTrace, recordLLMObservation, recordToolObservation, endTrace, createToolBatchSpan, endToolBatchSpan } from './tracing.js'
|
||||||
export type { LangfuseSpan } from './tracing.js'
|
export type { LangfuseSpan } from './tracing.js'
|
||||||
export { sanitizeToolInput, sanitizeToolOutput, sanitizeGlobal } from './sanitize.js'
|
export { sanitizeToolInput, sanitizeToolOutput, sanitizeGlobal } from './sanitize.js'
|
||||||
|
|||||||
@@ -3,11 +3,17 @@ import type { LangfuseSpan, LangfuseGeneration, LangfuseAgent } from '@langfuse/
|
|||||||
import { isLangfuseEnabled } from './client.js'
|
import { isLangfuseEnabled } from './client.js'
|
||||||
import { sanitizeToolInput, sanitizeToolOutput } from './sanitize.js'
|
import { sanitizeToolInput, sanitizeToolOutput } from './sanitize.js'
|
||||||
import { logForDebugging } from 'src/utils/debug.js'
|
import { logForDebugging } from 'src/utils/debug.js'
|
||||||
|
import { getCoreUserData } from 'src/utils/user.js'
|
||||||
|
|
||||||
export type { LangfuseSpan }
|
export type { LangfuseSpan }
|
||||||
|
|
||||||
// Root trace is an agent observation — represents one full agentic turn/session
|
// Root trace is an agent observation — represents one full agentic turn/session
|
||||||
type RootTrace = LangfuseAgent & { _sessionId?: string }
|
type RootTrace = LangfuseAgent & { _sessionId?: string; _userId?: string }
|
||||||
|
|
||||||
|
/** Resolve the user ID for Langfuse traces: explicit param > env var > email > deviceId */
|
||||||
|
function resolveLangfuseUserId(username?: string): string | undefined {
|
||||||
|
return username ?? process.env.LANGFUSE_USER_ID ?? getCoreUserData().email ?? getCoreUserData().deviceId
|
||||||
|
}
|
||||||
|
|
||||||
export function createTrace(params: {
|
export function createTrace(params: {
|
||||||
sessionId: string
|
sessionId: string
|
||||||
@@ -16,6 +22,7 @@ export function createTrace(params: {
|
|||||||
input?: unknown
|
input?: unknown
|
||||||
name?: string
|
name?: string
|
||||||
querySource?: string
|
querySource?: string
|
||||||
|
username?: string
|
||||||
}): LangfuseSpan | null {
|
}): LangfuseSpan | null {
|
||||||
if (!isLangfuseEnabled()) return null
|
if (!isLangfuseEnabled()) return null
|
||||||
try {
|
try {
|
||||||
@@ -31,6 +38,11 @@ export function createTrace(params: {
|
|||||||
}, { asType: 'agent' }) as RootTrace
|
}, { asType: 'agent' }) as RootTrace
|
||||||
rootSpan.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, params.sessionId)
|
rootSpan.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, params.sessionId)
|
||||||
rootSpan._sessionId = params.sessionId
|
rootSpan._sessionId = params.sessionId
|
||||||
|
const userId = resolveLangfuseUserId(params.username)
|
||||||
|
if (userId) {
|
||||||
|
rootSpan.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_USER_ID, userId)
|
||||||
|
rootSpan._userId = userId
|
||||||
|
}
|
||||||
logForDebugging(`[langfuse] Trace created: ${rootSpan.id}`)
|
logForDebugging(`[langfuse] Trace created: ${rootSpan.id}`)
|
||||||
return rootSpan as unknown as LangfuseSpan
|
return rootSpan as unknown as LangfuseSpan
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -56,10 +68,16 @@ export function recordLLMObservation(
|
|||||||
provider: string
|
provider: string
|
||||||
input: unknown
|
input: unknown
|
||||||
output: unknown
|
output: unknown
|
||||||
usage: { input_tokens: number; output_tokens: number }
|
usage: {
|
||||||
|
input_tokens: number
|
||||||
|
output_tokens: number
|
||||||
|
cache_creation_input_tokens?: number
|
||||||
|
cache_read_input_tokens?: number
|
||||||
|
}
|
||||||
startTime?: Date
|
startTime?: Date
|
||||||
endTime?: Date
|
endTime?: Date
|
||||||
completionStartTime?: Date
|
completionStartTime?: Date
|
||||||
|
tools?: unknown
|
||||||
},
|
},
|
||||||
): void {
|
): void {
|
||||||
if (!rootSpan || !isLangfuseEnabled()) return
|
if (!rootSpan || !isLangfuseEnabled()) return
|
||||||
@@ -73,7 +91,9 @@ export function recordLLMObservation(
|
|||||||
genName,
|
genName,
|
||||||
{
|
{
|
||||||
model: params.model,
|
model: params.model,
|
||||||
input: params.input,
|
input: params.tools
|
||||||
|
? { messages: params.input, tools: params.tools }
|
||||||
|
: params.input,
|
||||||
metadata: {
|
metadata: {
|
||||||
provider: params.provider,
|
provider: params.provider,
|
||||||
model: params.model,
|
model: params.model,
|
||||||
@@ -87,17 +107,27 @@ export function recordLLMObservation(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Propagate session ID to generation span so Langfuse links it correctly
|
// Propagate session ID and user ID to generation span so Langfuse links it correctly
|
||||||
const sessionId = (rootSpan as unknown as RootTrace)._sessionId
|
const sessionId = (rootSpan as unknown as RootTrace)._sessionId
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
gen.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId)
|
gen.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId)
|
||||||
}
|
}
|
||||||
|
const userId = (rootSpan as unknown as RootTrace)._userId
|
||||||
|
if (userId) {
|
||||||
|
gen.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_USER_ID, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anthropic splits input into uncached + cache_read + cache_creation.
|
||||||
|
// Langfuse's "input" should be the total prompt tokens so cost calc is correct.
|
||||||
|
const cacheRead = params.usage.cache_read_input_tokens ?? 0
|
||||||
|
const cacheCreation = params.usage.cache_creation_input_tokens ?? 0
|
||||||
gen.update({
|
gen.update({
|
||||||
output: params.output,
|
output: params.output,
|
||||||
usageDetails: {
|
usageDetails: {
|
||||||
input: params.usage.input_tokens,
|
input: params.usage.input_tokens + cacheCreation + cacheRead,
|
||||||
output: params.usage.output_tokens,
|
output: params.usage.output_tokens,
|
||||||
|
...(cacheRead > 0 && { cache_read: cacheRead }),
|
||||||
|
...(cacheCreation > 0 && { cache_creation: cacheCreation }),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -117,6 +147,7 @@ export function recordToolObservation(
|
|||||||
output: string
|
output: string
|
||||||
startTime?: Date
|
startTime?: Date
|
||||||
isError?: boolean
|
isError?: boolean
|
||||||
|
parentBatchSpan?: LangfuseSpan | null
|
||||||
},
|
},
|
||||||
): void {
|
): void {
|
||||||
if (!rootSpan || !isLangfuseEnabled()) return
|
if (!rootSpan || !isLangfuseEnabled()) return
|
||||||
@@ -124,6 +155,7 @@ export function recordToolObservation(
|
|||||||
// Use the global startObservation directly instead of rootSpan.startObservation().
|
// Use the global startObservation directly instead of rootSpan.startObservation().
|
||||||
// The instance method only forwards asType and drops startTime,
|
// The instance method only forwards asType and drops startTime,
|
||||||
// causing tool execution duration to be 0.
|
// causing tool execution duration to be 0.
|
||||||
|
const parentSpan = params.parentBatchSpan ?? rootSpan
|
||||||
const toolObs = startObservation(
|
const toolObs = startObservation(
|
||||||
params.toolName,
|
params.toolName,
|
||||||
{
|
{
|
||||||
@@ -136,15 +168,19 @@ export function recordToolObservation(
|
|||||||
{
|
{
|
||||||
asType: 'tool',
|
asType: 'tool',
|
||||||
...(params.startTime && { startTime: params.startTime }),
|
...(params.startTime && { startTime: params.startTime }),
|
||||||
parentSpanContext: rootSpan.otelSpan.spanContext(),
|
parentSpanContext: parentSpan.otelSpan.spanContext(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Propagate session ID to tool span so Langfuse links it correctly
|
// Propagate session ID and user ID to tool span so Langfuse links it correctly
|
||||||
const sessionId = (rootSpan as unknown as RootTrace)._sessionId
|
const sessionId = (rootSpan as unknown as RootTrace)._sessionId
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
toolObs.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId)
|
toolObs.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId)
|
||||||
}
|
}
|
||||||
|
const userId = (rootSpan as unknown as RootTrace)._userId
|
||||||
|
if (userId) {
|
||||||
|
toolObs.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_USER_ID, userId)
|
||||||
|
}
|
||||||
|
|
||||||
toolObs.update({
|
toolObs.update({
|
||||||
output: sanitizeToolOutput(params.toolName, params.output),
|
output: sanitizeToolOutput(params.toolName, params.output),
|
||||||
@@ -158,6 +194,59 @@ export function recordToolObservation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a span that wraps a batch of concurrent tool calls.
|
||||||
|
* Returns the batch span (to be passed as parentBatchSpan to recordToolObservation)
|
||||||
|
* and must be ended with endToolBatchSpan() after all tools complete.
|
||||||
|
*/
|
||||||
|
export function createToolBatchSpan(
|
||||||
|
rootSpan: LangfuseSpan | null,
|
||||||
|
params: { toolNames: string[]; batchIndex: number },
|
||||||
|
): LangfuseSpan | null {
|
||||||
|
if (!rootSpan || !isLangfuseEnabled()) return null
|
||||||
|
try {
|
||||||
|
const batchSpan = startObservation(
|
||||||
|
`tools`,
|
||||||
|
{
|
||||||
|
metadata: {
|
||||||
|
toolNames: params.toolNames.join(', '),
|
||||||
|
toolCount: String(params.toolNames.length),
|
||||||
|
batchIndex: String(params.batchIndex),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
asType: 'span',
|
||||||
|
parentSpanContext: rootSpan.otelSpan.spanContext(),
|
||||||
|
},
|
||||||
|
) as LangfuseSpan
|
||||||
|
|
||||||
|
const sessionId = (rootSpan as unknown as RootTrace)._sessionId
|
||||||
|
if (sessionId) {
|
||||||
|
batchSpan.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId)
|
||||||
|
}
|
||||||
|
const userId = (rootSpan as unknown as RootTrace)._userId
|
||||||
|
if (userId) {
|
||||||
|
batchSpan.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_USER_ID, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
logForDebugging(`[langfuse] Tool batch span created: ${batchSpan.id} (tools=${params.toolNames.join(',')})`)
|
||||||
|
return batchSpan
|
||||||
|
} catch (e) {
|
||||||
|
logForDebugging(`[langfuse] createToolBatchSpan failed: ${e}`, { level: 'error' })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function endToolBatchSpan(batchSpan: LangfuseSpan | null): void {
|
||||||
|
if (!batchSpan) return
|
||||||
|
try {
|
||||||
|
batchSpan.end()
|
||||||
|
logForDebugging(`[langfuse] Tool batch span ended: ${batchSpan.id}`)
|
||||||
|
} catch (e) {
|
||||||
|
logForDebugging(`[langfuse] endToolBatchSpan failed: ${e}`, { level: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function createSubagentTrace(params: {
|
export function createSubagentTrace(params: {
|
||||||
sessionId: string
|
sessionId: string
|
||||||
agentType: string
|
agentType: string
|
||||||
@@ -165,6 +254,7 @@ export function createSubagentTrace(params: {
|
|||||||
model: string
|
model: string
|
||||||
provider: string
|
provider: string
|
||||||
input?: unknown
|
input?: unknown
|
||||||
|
username?: string
|
||||||
}): LangfuseSpan | null {
|
}): LangfuseSpan | null {
|
||||||
if (!isLangfuseEnabled()) return null
|
if (!isLangfuseEnabled()) return null
|
||||||
try {
|
try {
|
||||||
@@ -179,6 +269,11 @@ export function createSubagentTrace(params: {
|
|||||||
}, { asType: 'agent' }) as RootTrace
|
}, { asType: 'agent' }) as RootTrace
|
||||||
rootSpan.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, params.sessionId)
|
rootSpan.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, params.sessionId)
|
||||||
rootSpan._sessionId = params.sessionId
|
rootSpan._sessionId = params.sessionId
|
||||||
|
const userId = resolveLangfuseUserId(params.username)
|
||||||
|
if (userId) {
|
||||||
|
rootSpan.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_USER_ID, userId)
|
||||||
|
rootSpan._userId = userId
|
||||||
|
}
|
||||||
logForDebugging(`[langfuse] Sub-agent trace created: ${rootSpan.id} (type=${params.agentType})`)
|
logForDebugging(`[langfuse] Sub-agent trace created: ${rootSpan.id} (type=${params.agentType})`)
|
||||||
return rootSpan as unknown as LangfuseSpan
|
return rootSpan as unknown as LangfuseSpan
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -187,14 +282,20 @@ export function createSubagentTrace(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function endTrace(rootSpan: LangfuseSpan | null, output?: unknown): void {
|
export function endTrace(
|
||||||
|
rootSpan: LangfuseSpan | null,
|
||||||
|
output?: unknown,
|
||||||
|
status?: 'interrupted' | 'error',
|
||||||
|
): void {
|
||||||
if (!rootSpan) return
|
if (!rootSpan) return
|
||||||
try {
|
try {
|
||||||
if (output !== undefined) {
|
const updatePayload: Record<string, unknown> = {}
|
||||||
rootSpan.update({ output })
|
if (output !== undefined) updatePayload.output = output
|
||||||
}
|
if (status === 'interrupted') updatePayload.level = 'WARNING'
|
||||||
|
else if (status === 'error') updatePayload.level = 'ERROR'
|
||||||
|
if (Object.keys(updatePayload).length > 0) rootSpan.update(updatePayload)
|
||||||
rootSpan.end()
|
rootSpan.end()
|
||||||
logForDebugging(`[langfuse] Trace ended: ${rootSpan.id}`)
|
logForDebugging(`[langfuse] Trace ended: ${rootSpan.id}${status ? ` (${status})` : ''}`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logForDebugging(`[langfuse] endTrace failed: ${e}`, { level: 'error' })
|
logForDebugging(`[langfuse] endTrace failed: ${e}`, { level: 'error' })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { BASH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/BashTool/t
|
|||||||
import type { AssistantMessage, Message } from '../../types/message.js'
|
import type { AssistantMessage, Message } from '../../types/message.js'
|
||||||
import { createChildAbortController } from '../../utils/abortController.js'
|
import { createChildAbortController } from '../../utils/abortController.js'
|
||||||
import { runToolUse } from './toolExecution.js'
|
import { runToolUse } from './toolExecution.js'
|
||||||
|
import { createToolBatchSpan, endToolBatchSpan } from '../langfuse/index.js'
|
||||||
|
import type { LangfuseSpan } from '../langfuse/index.js'
|
||||||
|
|
||||||
type MessageUpdate = {
|
type MessageUpdate = {
|
||||||
message?: Message
|
message?: Message
|
||||||
@@ -42,13 +44,10 @@ export class StreamingToolExecutor {
|
|||||||
private toolUseContext: ToolUseContext
|
private toolUseContext: ToolUseContext
|
||||||
private hasErrored = false
|
private hasErrored = false
|
||||||
private erroredToolDescription = ''
|
private erroredToolDescription = ''
|
||||||
// Child of toolUseContext.abortController. Fires when a Bash tool errors
|
|
||||||
// so sibling subprocesses die immediately instead of running to completion.
|
|
||||||
// Aborting this does NOT abort the parent — query.ts won't end the turn.
|
|
||||||
private siblingAbortController: AbortController
|
private siblingAbortController: AbortController
|
||||||
private discarded = false
|
private discarded = false
|
||||||
// Signal to wake up getRemainingResults when progress is available
|
|
||||||
private progressAvailableResolve?: () => void
|
private progressAvailableResolve?: () => void
|
||||||
|
private turnSpan: LangfuseSpan | null = null
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly toolDefinitions: Tools,
|
private readonly toolDefinitions: Tools,
|
||||||
@@ -74,6 +73,16 @@ export class StreamingToolExecutor {
|
|||||||
* Add a tool to the execution queue. Will start executing immediately if conditions allow.
|
* Add a tool to the execution queue. Will start executing immediately if conditions allow.
|
||||||
*/
|
*/
|
||||||
addTool(block: ToolUseBlock, assistantMessage: AssistantMessage): void {
|
addTool(block: ToolUseBlock, assistantMessage: AssistantMessage): void {
|
||||||
|
// Create turn span on first tool — will be ended in getRemainingResults
|
||||||
|
if (this.tools.length === 0 && this.turnSpan === null) {
|
||||||
|
this.turnSpan = createToolBatchSpan(
|
||||||
|
this.toolUseContext.langfuseTrace ?? null,
|
||||||
|
{ toolNames: [block.name], batchIndex: 0 },
|
||||||
|
)
|
||||||
|
if (this.turnSpan) {
|
||||||
|
this.toolUseContext = { ...this.toolUseContext, langfuseBatchSpan: this.turnSpan }
|
||||||
|
}
|
||||||
|
}
|
||||||
const toolDefinition = findToolByName(this.toolDefinitions, block.name)
|
const toolDefinition = findToolByName(this.toolDefinitions, block.name)
|
||||||
if (!toolDefinition) {
|
if (!toolDefinition) {
|
||||||
this.tools.push({
|
this.tools.push({
|
||||||
@@ -487,6 +496,9 @@ export class StreamingToolExecutor {
|
|||||||
for (const result of this.getCompletedResults()) {
|
for (const result of this.getCompletedResults()) {
|
||||||
yield result
|
yield result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
endToolBatchSpan(this.turnSpan)
|
||||||
|
this.turnSpan = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1309,6 +1309,7 @@ async function checkPermissionsAndCallTool(
|
|||||||
output: toolResultStr,
|
output: toolResultStr,
|
||||||
startTime: new Date(startTime),
|
startTime: new Date(startTime),
|
||||||
isError: false,
|
isError: false,
|
||||||
|
parentBatchSpan: toolUseContext.langfuseBatchSpan,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Map the tool result to API format once and cache it. This block is reused
|
// Map the tool result to API format once and cache it. This block is reused
|
||||||
@@ -1628,6 +1629,7 @@ async function checkPermissionsAndCallTool(
|
|||||||
output: errorMessage(error),
|
output: errorMessage(error),
|
||||||
startTime: new Date(startTime),
|
startTime: new Date(startTime),
|
||||||
isError: true,
|
isError: true,
|
||||||
|
parentBatchSpan: toolUseContext.langfuseBatchSpan,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Handle MCP auth errors by updating the client status to 'needs-auth'
|
// Handle MCP auth errors by updating the client status to 'needs-auth'
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { findToolByName, type ToolUseContext } from '../../Tool.js'
|
|||||||
import type { AssistantMessage, Message } from '../../types/message.js'
|
import type { AssistantMessage, Message } from '../../types/message.js'
|
||||||
import { all } from '../../utils/generators.js'
|
import { all } from '../../utils/generators.js'
|
||||||
import { type MessageUpdateLazy, runToolUse } from './toolExecution.js'
|
import { type MessageUpdateLazy, runToolUse } from './toolExecution.js'
|
||||||
|
import { createToolBatchSpan, endToolBatchSpan } from '../langfuse/index.js'
|
||||||
|
|
||||||
function getMaxToolUseConcurrency(): number {
|
function getMaxToolUseConcurrency(): number {
|
||||||
return (
|
return (
|
||||||
@@ -22,7 +23,18 @@ export async function* runTools(
|
|||||||
canUseTool: CanUseToolFn,
|
canUseTool: CanUseToolFn,
|
||||||
toolUseContext: ToolUseContext,
|
toolUseContext: ToolUseContext,
|
||||||
): AsyncGenerator<MessageUpdate, void> {
|
): AsyncGenerator<MessageUpdate, void> {
|
||||||
let currentContext = toolUseContext
|
// Wrap all tool calls in this turn under a single Langfuse turn span
|
||||||
|
const turnSpan = toolUseMessages.length > 0
|
||||||
|
? createToolBatchSpan(toolUseContext.langfuseTrace ?? null, {
|
||||||
|
toolNames: toolUseMessages.map(b => b.name),
|
||||||
|
batchIndex: 0,
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
const contextWithTurn = turnSpan
|
||||||
|
? { ...toolUseContext, langfuseBatchSpan: turnSpan }
|
||||||
|
: toolUseContext
|
||||||
|
|
||||||
|
let currentContext = contextWithTurn
|
||||||
for (const { isConcurrencySafe, blocks } of partitionToolCalls(
|
for (const { isConcurrencySafe, blocks } of partitionToolCalls(
|
||||||
toolUseMessages,
|
toolUseMessages,
|
||||||
currentContext,
|
currentContext,
|
||||||
@@ -79,6 +91,8 @@ export async function* runTools(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
endToolBatchSpan(turnSpan)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Batch = { isConcurrencySafe: boolean; blocks: ToolUseBlock[] }
|
type Batch = { isConcurrencySafe: boolean; blocks: ToolUseBlock[] }
|
||||||
|
|||||||
15
tsconfig.base.json
Normal file
15
tsconfig.base.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"types": ["bun", "@types/node"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "./tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"strict": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"types": ["bun"],
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"src/*": ["./src/*"],
|
"src/*": ["./src/*"],
|
||||||
"@claude-code-best/builtin-tools/*": ["./packages/builtin-tools/src/*"],
|
"@claude-code-best/builtin-tools/*": ["./packages/builtin-tools/src/*"],
|
||||||
"@claude-code-best/builtin-tools": ["./packages/builtin-tools/src/index.ts"]
|
"@claude-code-best/builtin-tools": ["./packages/builtin-tools/src/index.ts"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "packages/builtin-tools/src/**/*.ts", "packages/builtin-tools/src/**/*.tsx"],
|
"include": ["src/**/*.ts", "src/**/*.tsx", "packages/**/*.ts", "packages/**/*.tsx"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
100
vite.config.ts
Normal file
100
vite.config.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { defineConfig, type Plugin } from "vite";
|
||||||
|
import { resolve, dirname } from "path";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import { getMacroDefines } from "./scripts/defines";
|
||||||
|
import featureFlagsPlugin from "./scripts/vite-plugin-feature-flags";
|
||||||
|
import importMetaRequirePlugin from "./scripts/vite-plugin-import-meta-require";
|
||||||
|
|
||||||
|
const projectRoot = dirname(new URL(import.meta.url).pathname);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin to import .md files as raw strings (Bun's text loader behavior).
|
||||||
|
*/
|
||||||
|
function rawAssetPlugin(extensions: string[]): Plugin {
|
||||||
|
return {
|
||||||
|
name: "raw-asset",
|
||||||
|
enforce: "pre",
|
||||||
|
resolveId(id, importer) {
|
||||||
|
if (extensions.some((ext) => id.endsWith(ext))) {
|
||||||
|
// Resolve to actual file path
|
||||||
|
return this.resolve(id, importer, { skipSelf: true });
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
load(id) {
|
||||||
|
if (extensions.some((ext) => id.endsWith(ext))) {
|
||||||
|
const content = readFileSync(id, "utf-8");
|
||||||
|
return `export default ${JSON.stringify(content)}`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
// CLI tool — no browser features needed
|
||||||
|
appType: "custom",
|
||||||
|
|
||||||
|
// Tell Vite this is a Node.js build, not browser.
|
||||||
|
// Prevents externalization of Node.js builtins (fs, path, etc.)
|
||||||
|
ssr: {
|
||||||
|
target: "node",
|
||||||
|
noExternal: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
build: {
|
||||||
|
emptyOutDir: true,
|
||||||
|
outDir: "dist",
|
||||||
|
target: "esnext",
|
||||||
|
copyPublicDir: false,
|
||||||
|
sourcemap: false,
|
||||||
|
minify: false,
|
||||||
|
|
||||||
|
// SSR build mode — uses Rollup with Node.js target
|
||||||
|
ssr: true,
|
||||||
|
|
||||||
|
rollupOptions: {
|
||||||
|
input: resolve(projectRoot, "src/entrypoints/cli.tsx"),
|
||||||
|
|
||||||
|
output: {
|
||||||
|
format: "es",
|
||||||
|
dir: "dist",
|
||||||
|
entryFileNames: "cli.js",
|
||||||
|
chunkFileNames: "chunks/[name]-[hash].js",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Externalize native addon packages (they contain .node binaries)
|
||||||
|
external: [
|
||||||
|
/audio-capture-napi/,
|
||||||
|
/color-diff-napi/,
|
||||||
|
/image-processor-napi/,
|
||||||
|
/modifiers-napi/,
|
||||||
|
/url-handler-napi/,
|
||||||
|
],
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
rawAssetPlugin([".md", ".txt", ".html", ".css"]),
|
||||||
|
featureFlagsPlugin(),
|
||||||
|
importMetaRequirePlugin(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
cssCodeSplit: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Compile-time constant replacement (MACRO.* defines)
|
||||||
|
define: {
|
||||||
|
...getMacroDefines(),
|
||||||
|
},
|
||||||
|
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
// src/* path alias (mirrors tsconfig paths)
|
||||||
|
"src/": resolve(projectRoot, "src/"),
|
||||||
|
},
|
||||||
|
// Ensure workspace packages share a single copy of these
|
||||||
|
dedupe: ["react", "react-reconciler", "react-compiler-runtime"],
|
||||||
|
// Resolve .js imports to .ts files (Bun does this automatically)
|
||||||
|
extensions: [".ts", ".tsx", ".js", ".jsx", ".json"],
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user