feat(v2): migrate sidecar from raw CLI spawning to @anthropic-ai/claude-agent-sdk
Claude CLI v2.1.69 hangs silently when spawned via child_process.spawn() with piped stdio (known bug github.com/anthropics/claude-code/issues/6775). Replace raw CLI spawning in both sidecar runners with the SDK's query() function, which handles subprocess management internally. SDK message format matches CLI stream-json, so the sdk-messages.ts adapter is unchanged. - agent-runner.ts: use SDK query() with AbortController for stop - agent-runner-deno.ts: use npm:@anthropic-ai/claude-agent-sdk import - sidecar.rs: add --allow-write and --allow-net Deno permissions - package.json: add @anthropic-ai/claude-agent-sdk ^0.2.70, build:sidecar script
This commit is contained in:
parent
fdd1884015
commit
323703caba
5 changed files with 457 additions and 151 deletions
|
|
@ -220,6 +220,8 @@ impl SidecarManager {
|
|||
"--allow-run".to_string(),
|
||||
"--allow-env".to_string(),
|
||||
"--allow-read".to_string(),
|
||||
"--allow-write".to_string(),
|
||||
"--allow-net".to_string(),
|
||||
deno_path.to_string_lossy().to_string(),
|
||||
],
|
||||
});
|
||||
|
|
|
|||
328
v2/package-lock.json
generated
328
v2/package-lock.json
generated
|
|
@ -8,6 +8,7 @@
|
|||
"name": "bterminal-v2",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.70",
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
|
|
@ -27,6 +28,29 @@
|
|||
"vitest": "^4.0.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/claude-agent-sdk": {
|
||||
"version": "0.2.70",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.70.tgz",
|
||||
"integrity": "sha512-ABaB37jWt7dZXfIDHebHv99jX9GIyqc0aSjcz9nxq79eauOpa+64Cah5hx/yzhsWz7m5GEtjbMIZCClTfnRRhg==",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "^0.34.2",
|
||||
"@img/sharp-darwin-x64": "^0.34.2",
|
||||
"@img/sharp-linux-arm": "^0.34.2",
|
||||
"@img/sharp-linux-arm64": "^0.34.2",
|
||||
"@img/sharp-linux-x64": "^0.34.2",
|
||||
"@img/sharp-linuxmusl-arm64": "^0.34.2",
|
||||
"@img/sharp-linuxmusl-x64": "^0.34.2",
|
||||
"@img/sharp-win32-arm64": "^0.34.2",
|
||||
"@img/sharp-win32-x64": "^0.34.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||
|
|
@ -469,6 +493,310 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@
|
|||
"tauri": "cargo tauri",
|
||||
"tauri:dev": "cargo tauri dev",
|
||||
"tauri:build": "cargo tauri build",
|
||||
"test": "vitest run"
|
||||
"test": "vitest run",
|
||||
"build:sidecar": "esbuild sidecar/agent-runner.ts --bundle --platform=node --format=esm --outfile=sidecar/dist/agent-runner.mjs --external:@anthropic-ai/claude-agent-sdk"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
|
|
@ -24,6 +25,7 @@
|
|||
"vitest": "^4.0.18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.70",
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
// Agent Runner — Deno sidecar entry point (experimental)
|
||||
// Agent Runner — Deno sidecar entry point
|
||||
// Drop-in replacement for agent-runner.ts using Deno APIs
|
||||
// Build: deno compile --allow-run --allow-env --allow-read agent-runner-deno.ts -o dist/agent-runner
|
||||
// Run: deno run --allow-run --allow-env --allow-read agent-runner-deno.ts
|
||||
// Uses @anthropic-ai/claude-agent-sdk via npm: specifier
|
||||
// Run: deno run --allow-run --allow-env --allow-read --allow-write --allow-net agent-runner-deno.ts
|
||||
|
||||
import { TextLineStream } from "https://deno.land/std@0.224.0/streams/text_line_stream.ts";
|
||||
import { query } from "npm:@anthropic-ai/claude-agent-sdk";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
// Active agent sessions keyed by session ID
|
||||
const sessions = new Map<string, Deno.ChildProcess>();
|
||||
// Active sessions with abort controllers
|
||||
const sessions = new Map<string, AbortController>();
|
||||
|
||||
function send(msg: Record<string, unknown>) {
|
||||
Deno.stdout.writeSync(encoder.encode(JSON.stringify(msg) + "\n"));
|
||||
|
|
@ -49,7 +50,7 @@ function handleMessage(msg: Record<string, unknown>) {
|
|||
}
|
||||
}
|
||||
|
||||
function handleQuery(msg: QueryMessage) {
|
||||
async function handleQuery(msg: QueryMessage) {
|
||||
const { sessionId, prompt, cwd, maxTurns, maxBudgetUsd, resumeSessionId } = msg;
|
||||
|
||||
if (sessions.has(sessionId)) {
|
||||
|
|
@ -57,93 +58,89 @@ function handleQuery(msg: QueryMessage) {
|
|||
return;
|
||||
}
|
||||
|
||||
const args = ["-p", "--output-format", "stream-json", "--verbose"];
|
||||
log(`Starting agent session ${sessionId} via SDK`);
|
||||
|
||||
if (maxTurns) args.push("--max-turns", String(maxTurns));
|
||||
if (maxBudgetUsd) args.push("--max-budget-usd", String(maxBudgetUsd));
|
||||
if (resumeSessionId) args.push("--resume", resumeSessionId);
|
||||
args.push(prompt);
|
||||
const controller = new AbortController();
|
||||
|
||||
log(`Starting agent session ${sessionId}: claude ${args.join(" ")}`);
|
||||
|
||||
// Strip all CLAUDE* env vars to prevent nesting detection by claude CLI
|
||||
const env: Record<string, string> = {};
|
||||
// Strip CLAUDE* env vars to prevent nesting detection
|
||||
const cleanEnv: Record<string, string | undefined> = {};
|
||||
for (const [key, value] of Object.entries(Deno.env.toObject())) {
|
||||
if (!key.startsWith("CLAUDE")) {
|
||||
env[key] = value;
|
||||
cleanEnv[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const command = new Deno.Command("claude", {
|
||||
args,
|
||||
try {
|
||||
const q = query({
|
||||
prompt,
|
||||
options: {
|
||||
abortController: controller,
|
||||
cwd: cwd || Deno.cwd(),
|
||||
env,
|
||||
stdin: "piped",
|
||||
stdout: "piped",
|
||||
stderr: "piped",
|
||||
env: cleanEnv,
|
||||
maxTurns: maxTurns ?? undefined,
|
||||
maxBudgetUsd: maxBudgetUsd ?? undefined,
|
||||
resume: resumeSessionId ?? undefined,
|
||||
allowedTools: [
|
||||
"Bash", "Read", "Write", "Edit", "Glob", "Grep",
|
||||
"WebSearch", "WebFetch", "TodoWrite", "NotebookEdit",
|
||||
],
|
||||
permissionMode: "bypassPermissions",
|
||||
allowDangerouslySkipPermissions: true,
|
||||
},
|
||||
});
|
||||
|
||||
const child = command.spawn();
|
||||
sessions.set(sessionId, child);
|
||||
sessions.set(sessionId, controller);
|
||||
send({ type: "agent_started", sessionId });
|
||||
|
||||
// Parse NDJSON from claude's stdout
|
||||
(async () => {
|
||||
const lines = child.stdout
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
.pipeThrough(new TextLineStream());
|
||||
for await (const line of lines) {
|
||||
try {
|
||||
const sdkMsg = JSON.parse(line);
|
||||
send({ type: "agent_event", sessionId, event: sdkMsg });
|
||||
} catch {
|
||||
log(`Non-JSON from claude stdout: ${line}`);
|
||||
for await (const message of q) {
|
||||
const sdkMsg = message as Record<string, unknown>;
|
||||
send({
|
||||
type: "agent_event",
|
||||
sessionId,
|
||||
event: sdkMsg,
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// Capture stderr
|
||||
(async () => {
|
||||
const lines = child.stderr
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
.pipeThrough(new TextLineStream());
|
||||
for await (const line of lines) {
|
||||
log(`[claude:${sessionId}] ${line}`);
|
||||
send({ type: "agent_log", sessionId, message: line });
|
||||
}
|
||||
})();
|
||||
|
||||
// Wait for exit
|
||||
child.status.then((status) => {
|
||||
log(`Claude process exited for ${sessionId}: code=${status.code} signal=${status.signal}`);
|
||||
sessions.delete(sessionId);
|
||||
send({
|
||||
type: "agent_stopped",
|
||||
sessionId,
|
||||
exitCode: status.code,
|
||||
signal: status.signal,
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
sessions.delete(sessionId);
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
|
||||
if (errMsg.includes("aborted") || errMsg.includes("AbortError")) {
|
||||
log(`Agent session ${sessionId} aborted`);
|
||||
send({
|
||||
type: "agent_stopped",
|
||||
sessionId,
|
||||
exitCode: null,
|
||||
signal: "SIGTERM",
|
||||
});
|
||||
} else {
|
||||
log(`Agent session ${sessionId} error: ${errMsg}`);
|
||||
send({
|
||||
type: "agent_error",
|
||||
sessionId,
|
||||
message: errMsg,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleStop(msg: StopMessage) {
|
||||
const { sessionId } = msg;
|
||||
const child = sessions.get(sessionId);
|
||||
if (!child) {
|
||||
const controller = sessions.get(sessionId);
|
||||
if (!controller) {
|
||||
send({ type: "error", sessionId, message: "Session not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
log(`Stopping agent session ${sessionId}`);
|
||||
child.kill("SIGTERM");
|
||||
|
||||
// Force kill after 5s
|
||||
setTimeout(() => {
|
||||
if (sessions.has(sessionId)) {
|
||||
log(`Force killing agent session ${sessionId}`);
|
||||
child.kill("SIGKILL");
|
||||
}
|
||||
}, 5000);
|
||||
controller.abort();
|
||||
}
|
||||
|
||||
// Main: read NDJSON from stdin
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
// Agent Runner — Node.js sidecar entry point
|
||||
// Spawned by Rust backend, communicates via stdio NDJSON
|
||||
// Manages claude CLI subprocess with --output-format stream-json
|
||||
// Uses @anthropic-ai/claude-agent-sdk for proper Claude session management
|
||||
|
||||
import { stdin, stdout, stderr } from 'process';
|
||||
import { createInterface } from 'readline';
|
||||
import { spawn, type ChildProcess } from 'child_process';
|
||||
import { query, type Query } from '@anthropic-ai/claude-agent-sdk';
|
||||
|
||||
const rl = createInterface({ input: stdin });
|
||||
|
||||
// Active agent sessions keyed by session ID
|
||||
const sessions = new Map<string, ChildProcess>();
|
||||
const sessions = new Map<string, { query: Query; controller: AbortController }>();
|
||||
|
||||
function send(msg: Record<string, unknown>) {
|
||||
stdout.write(JSON.stringify(msg) + '\n');
|
||||
|
|
@ -59,7 +59,7 @@ function handleMessage(msg: Record<string, unknown>) {
|
|||
}
|
||||
}
|
||||
|
||||
function handleQuery(msg: QueryMessage) {
|
||||
async function handleQuery(msg: QueryMessage) {
|
||||
const { sessionId, prompt, cwd, maxTurns, maxBudgetUsd, resumeSessionId } = msg;
|
||||
|
||||
if (sessions.has(sessionId)) {
|
||||
|
|
@ -67,114 +67,91 @@ function handleQuery(msg: QueryMessage) {
|
|||
return;
|
||||
}
|
||||
|
||||
const args = [
|
||||
'-p',
|
||||
'--output-format', 'stream-json',
|
||||
'--verbose',
|
||||
];
|
||||
log(`Starting agent session ${sessionId} via SDK`);
|
||||
|
||||
if (maxTurns) {
|
||||
args.push('--max-turns', String(maxTurns));
|
||||
}
|
||||
const controller = new AbortController();
|
||||
|
||||
if (maxBudgetUsd) {
|
||||
args.push('--max-budget-usd', String(maxBudgetUsd));
|
||||
}
|
||||
|
||||
if (resumeSessionId) {
|
||||
args.push('--resume', resumeSessionId);
|
||||
}
|
||||
|
||||
args.push(prompt);
|
||||
|
||||
log(`Starting agent session ${sessionId}: claude ${args.join(' ')}`);
|
||||
|
||||
// Strip all CLAUDE* env vars to prevent nesting detection by claude CLI.
|
||||
// When BTerminal is launched from a Claude Code terminal, these leak in.
|
||||
const cleanEnv: Record<string, string> = {};
|
||||
// Strip CLAUDE* env vars to prevent nesting detection by the spawned CLI
|
||||
const cleanEnv: Record<string, string | undefined> = {};
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (!key.startsWith('CLAUDE') && value !== undefined) {
|
||||
if (!key.startsWith('CLAUDE')) {
|
||||
cleanEnv[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const child = spawn('claude', args, {
|
||||
try {
|
||||
const q = query({
|
||||
prompt,
|
||||
options: {
|
||||
abortController: controller,
|
||||
cwd: cwd || process.cwd(),
|
||||
env: cleanEnv,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
maxTurns: maxTurns ?? undefined,
|
||||
maxBudgetUsd: maxBudgetUsd ?? undefined,
|
||||
resume: resumeSessionId ?? undefined,
|
||||
allowedTools: [
|
||||
'Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep',
|
||||
'WebSearch', 'WebFetch', 'TodoWrite', 'NotebookEdit',
|
||||
],
|
||||
permissionMode: 'bypassPermissions',
|
||||
allowDangerouslySkipPermissions: true,
|
||||
},
|
||||
});
|
||||
|
||||
sessions.set(sessionId, child);
|
||||
|
||||
sessions.set(sessionId, { query: q, controller });
|
||||
send({ type: 'agent_started', sessionId });
|
||||
|
||||
// Parse NDJSON from claude's stdout
|
||||
const childRl = createInterface({ input: child.stdout! });
|
||||
childRl.on('line', (line: string) => {
|
||||
try {
|
||||
const sdkMsg = JSON.parse(line);
|
||||
for await (const message of q) {
|
||||
// Forward SDK messages as-is — they use the same format as CLI stream-json
|
||||
const sdkMsg = message as Record<string, unknown>;
|
||||
send({
|
||||
type: 'agent_event',
|
||||
sessionId,
|
||||
event: sdkMsg,
|
||||
});
|
||||
} catch {
|
||||
// Non-JSON output from claude (shouldn't happen with stream-json)
|
||||
log(`Non-JSON from claude stdout: ${line}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Capture stderr for debugging
|
||||
const stderrRl = createInterface({ input: child.stderr! });
|
||||
stderrRl.on('line', (line: string) => {
|
||||
log(`[claude:${sessionId}] ${line}`);
|
||||
send({
|
||||
type: 'agent_log',
|
||||
sessionId,
|
||||
message: line,
|
||||
});
|
||||
});
|
||||
|
||||
child.on('error', (err: Error) => {
|
||||
log(`Claude process error for ${sessionId}: ${err.message}`);
|
||||
sessions.delete(sessionId);
|
||||
send({
|
||||
type: 'agent_error',
|
||||
sessionId,
|
||||
message: err.message,
|
||||
});
|
||||
});
|
||||
|
||||
child.on('exit', (code: number | null, signal: string | null) => {
|
||||
log(`Claude process exited for ${sessionId}: code=${code} signal=${signal}`);
|
||||
// Session completed normally
|
||||
sessions.delete(sessionId);
|
||||
send({
|
||||
type: 'agent_stopped',
|
||||
sessionId,
|
||||
exitCode: code,
|
||||
signal,
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
sessions.delete(sessionId);
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
|
||||
if (errMsg.includes('aborted') || errMsg.includes('AbortError')) {
|
||||
log(`Agent session ${sessionId} aborted`);
|
||||
send({
|
||||
type: 'agent_stopped',
|
||||
sessionId,
|
||||
exitCode: null,
|
||||
signal: 'SIGTERM',
|
||||
});
|
||||
} else {
|
||||
log(`Agent session ${sessionId} error: ${errMsg}`);
|
||||
send({
|
||||
type: 'agent_error',
|
||||
sessionId,
|
||||
message: errMsg,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleStop(msg: StopMessage) {
|
||||
const { sessionId } = msg;
|
||||
const child = sessions.get(sessionId);
|
||||
if (!child) {
|
||||
const session = sessions.get(sessionId);
|
||||
if (!session) {
|
||||
send({ type: 'error', sessionId, message: 'Session not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
log(`Stopping agent session ${sessionId}`);
|
||||
child.kill('SIGTERM');
|
||||
|
||||
// Force kill after 5s if still running
|
||||
setTimeout(() => {
|
||||
if (sessions.has(sessionId)) {
|
||||
log(`Force killing agent session ${sessionId}`);
|
||||
child.kill('SIGKILL');
|
||||
}
|
||||
}, 5000);
|
||||
session.controller.abort();
|
||||
}
|
||||
|
||||
log('Sidecar started');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue