@studio-foundation/runner 0.4.0-beta → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/__tests__/script-executor.test.js +57 -3
- package/dist/__tests__/script-executor.test.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/anonymization.d.ts +17 -2
- package/dist/middleware/anonymization.d.ts.map +1 -1
- package/dist/middleware/anonymization.js +37 -1
- package/dist/middleware/anonymization.js.map +1 -1
- package/dist/prompt-builder.d.ts +10 -6
- package/dist/prompt-builder.d.ts.map +1 -1
- package/dist/prompt-builder.js +27 -2
- package/dist/prompt-builder.js.map +1 -1
- package/dist/providers/claude-code-mcp-server.d.ts +15 -0
- package/dist/providers/claude-code-mcp-server.d.ts.map +1 -0
- package/dist/providers/claude-code-mcp-server.js +126 -0
- package/dist/providers/claude-code-mcp-server.js.map +1 -0
- package/dist/providers/claude-code-mcp-server.test.d.ts +2 -0
- package/dist/providers/claude-code-mcp-server.test.d.ts.map +1 -0
- package/dist/providers/claude-code-mcp-server.test.js +123 -0
- package/dist/providers/claude-code-mcp-server.test.js.map +1 -0
- package/dist/providers/claude-code.d.ts +23 -0
- package/dist/providers/claude-code.d.ts.map +1 -0
- package/dist/providers/claude-code.js +187 -0
- package/dist/providers/claude-code.js.map +1 -0
- package/dist/providers/claude-code.test.d.ts +2 -0
- package/dist/providers/claude-code.test.d.ts.map +1 -0
- package/dist/providers/claude-code.test.js +259 -0
- package/dist/providers/claude-code.test.js.map +1 -0
- package/dist/providers/registry.d.ts +3 -0
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +4 -0
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/registry.test.d.ts +2 -0
- package/dist/providers/registry.test.d.ts.map +1 -0
- package/dist/providers/registry.test.js +56 -0
- package/dist/providers/registry.test.js.map +1 -0
- package/dist/runner.d.ts +2 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +55 -8
- package/dist/runner.js.map +1 -1
- package/dist/runner.test.js +5 -2
- package/dist/runner.test.js.map +1 -1
- package/dist/script-executor.d.ts.map +1 -1
- package/dist/script-executor.js +16 -2
- package/dist/script-executor.js.map +1 -1
- package/dist/tools/tool-registry.d.ts +21 -4
- package/dist/tools/tool-registry.d.ts.map +1 -1
- package/dist/tools/tool-registry.js +55 -17
- package/dist/tools/tool-registry.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const claude_code_mcp_server_js_1 = require("./claude-code-mcp-server.js");
|
|
5
|
+
const TOOLS = [
|
|
6
|
+
{
|
|
7
|
+
name: 'repo_manager-read_file',
|
|
8
|
+
description: 'Read a file',
|
|
9
|
+
parameters: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: { path: { type: 'string' } },
|
|
12
|
+
required: ['path'],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
async function jsonRpc(port, method, params, id = 1) {
|
|
18
|
+
const body = { jsonrpc: '2.0', method, params };
|
|
19
|
+
if (id !== undefined)
|
|
20
|
+
body.id = id;
|
|
21
|
+
const res = await fetch(`http://127.0.0.1:${port}`, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: { 'Content-Type': 'application/json' },
|
|
24
|
+
body: JSON.stringify(body),
|
|
25
|
+
});
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
27
|
+
return res.json();
|
|
28
|
+
}
|
|
29
|
+
(0, vitest_1.describe)('ClaudeCodeMcpServer', () => {
|
|
30
|
+
let server;
|
|
31
|
+
let executeTool;
|
|
32
|
+
(0, vitest_1.beforeEach)(() => {
|
|
33
|
+
executeTool = vitest_1.vi.fn();
|
|
34
|
+
server = new claude_code_mcp_server_js_1.ClaudeCodeMcpServer(TOOLS, executeTool);
|
|
35
|
+
});
|
|
36
|
+
(0, vitest_1.afterEach)(async () => {
|
|
37
|
+
await server.stop();
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.it)('starts and returns a port number', async () => {
|
|
40
|
+
const port = await server.start();
|
|
41
|
+
(0, vitest_1.expect)(port).toBeGreaterThan(1024);
|
|
42
|
+
(0, vitest_1.expect)(port).toBeLessThan(65536);
|
|
43
|
+
});
|
|
44
|
+
(0, vitest_1.it)('responds to initialize with server capabilities', async () => {
|
|
45
|
+
const port = await server.start();
|
|
46
|
+
const res = await jsonRpc(port, 'initialize', {
|
|
47
|
+
protocolVersion: '2024-11-05',
|
|
48
|
+
capabilities: {},
|
|
49
|
+
clientInfo: { name: 'claude', version: '1.0' },
|
|
50
|
+
});
|
|
51
|
+
(0, vitest_1.expect)(res.result.capabilities).toBeDefined();
|
|
52
|
+
(0, vitest_1.expect)(res.result.serverInfo.name).toBe('studio');
|
|
53
|
+
});
|
|
54
|
+
(0, vitest_1.it)('responds to notifications/initialized (no id) with null', async () => {
|
|
55
|
+
const port = await server.start();
|
|
56
|
+
const res = await jsonRpc(port, 'notifications/initialized', {}, undefined);
|
|
57
|
+
(0, vitest_1.expect)(res).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
(0, vitest_1.it)('lists tools in MCP format', async () => {
|
|
60
|
+
const port = await server.start();
|
|
61
|
+
const res = await jsonRpc(port, 'tools/list', {});
|
|
62
|
+
(0, vitest_1.expect)(res.result.tools).toHaveLength(1);
|
|
63
|
+
(0, vitest_1.expect)(res.result.tools[0].name).toBe('repo_manager-read_file');
|
|
64
|
+
(0, vitest_1.expect)(res.result.tools[0].inputSchema).toBeDefined();
|
|
65
|
+
});
|
|
66
|
+
(0, vitest_1.it)('calls executeTool and returns result on tools/call', async () => {
|
|
67
|
+
const outcome = { result: 'file contents' };
|
|
68
|
+
executeTool.mockResolvedValueOnce(outcome);
|
|
69
|
+
const port = await server.start();
|
|
70
|
+
const res = await jsonRpc(port, 'tools/call', {
|
|
71
|
+
name: 'repo_manager-read_file',
|
|
72
|
+
arguments: { path: 'src/foo.ts' },
|
|
73
|
+
});
|
|
74
|
+
(0, vitest_1.expect)(executeTool).toHaveBeenCalledWith('repo_manager-read_file', { path: 'src/foo.ts' }, vitest_1.expect.any(String));
|
|
75
|
+
(0, vitest_1.expect)(res.result.content[0].text).toBe(JSON.stringify('file contents'));
|
|
76
|
+
});
|
|
77
|
+
(0, vitest_1.it)('returns MCP error content when executeTool returns an error', async () => {
|
|
78
|
+
const outcome = { error: 'file not found' };
|
|
79
|
+
executeTool.mockResolvedValueOnce(outcome);
|
|
80
|
+
const port = await server.start();
|
|
81
|
+
const res = await jsonRpc(port, 'tools/call', {
|
|
82
|
+
name: 'repo_manager-read_file',
|
|
83
|
+
arguments: { path: 'missing.ts' },
|
|
84
|
+
});
|
|
85
|
+
(0, vitest_1.expect)(res.result.content[0].text).toContain('file not found');
|
|
86
|
+
(0, vitest_1.expect)(res.result.isError).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
(0, vitest_1.it)('returns JSON-RPC error for unknown method', async () => {
|
|
89
|
+
const port = await server.start();
|
|
90
|
+
const res = await jsonRpc(port, 'unknown/method', {});
|
|
91
|
+
(0, vitest_1.expect)(res.error).toBeDefined();
|
|
92
|
+
(0, vitest_1.expect)(res.error.code).toBe(-32601);
|
|
93
|
+
});
|
|
94
|
+
(0, vitest_1.it)('returns JSON-RPC error -32602 when tools/call params are missing', async () => {
|
|
95
|
+
const port = await server.start();
|
|
96
|
+
const res = await jsonRpc(port, 'tools/call', { name: 'repo_manager-read_file' });
|
|
97
|
+
(0, vitest_1.expect)(res.error).toBeDefined();
|
|
98
|
+
(0, vitest_1.expect)(res.error.code).toBe(-32602);
|
|
99
|
+
});
|
|
100
|
+
(0, vitest_1.it)('records tool call with error when executeTool throws', async () => {
|
|
101
|
+
executeTool.mockRejectedValueOnce(new Error('unexpected crash'));
|
|
102
|
+
const port = await server.start();
|
|
103
|
+
const res = await jsonRpc(port, 'tools/call', {
|
|
104
|
+
name: 'repo_manager-read_file',
|
|
105
|
+
arguments: { path: 'f.ts' },
|
|
106
|
+
});
|
|
107
|
+
(0, vitest_1.expect)(res.result.isError).toBe(true);
|
|
108
|
+
(0, vitest_1.expect)(res.result.content[0].text).toContain('unexpected crash');
|
|
109
|
+
const calls = server.getToolCalls();
|
|
110
|
+
(0, vitest_1.expect)(calls).toHaveLength(1);
|
|
111
|
+
(0, vitest_1.expect)(calls[0].error).toBe('unexpected crash');
|
|
112
|
+
});
|
|
113
|
+
(0, vitest_1.it)('accumulates tool calls in getToolCalls()', async () => {
|
|
114
|
+
executeTool.mockResolvedValueOnce({ result: 'ok' });
|
|
115
|
+
const port = await server.start();
|
|
116
|
+
await jsonRpc(port, 'tools/call', { name: 'repo_manager-read_file', arguments: { path: 'f.ts' } });
|
|
117
|
+
const calls = server.getToolCalls();
|
|
118
|
+
(0, vitest_1.expect)(calls).toHaveLength(1);
|
|
119
|
+
(0, vitest_1.expect)(calls[0].name).toBe('repo_manager-read_file');
|
|
120
|
+
(0, vitest_1.expect)(calls[0].result).toBe('ok');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
//# sourceMappingURL=claude-code-mcp-server.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-code-mcp-server.test.js","sourceRoot":"","sources":["../../src/providers/claude-code-mcp-server.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { LLMRequest, LLMResponse } from '@studio-foundation/contracts';
|
|
2
|
+
import type { AgentLoopProvider, AgentLoopResult, ToolCallOutcome } from './provider.js';
|
|
3
|
+
export interface ClaudeCodeConfig {
|
|
4
|
+
model?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class ClaudeCodeProvider implements AgentLoopProvider {
|
|
7
|
+
readonly name = "claude-code";
|
|
8
|
+
private readonly model;
|
|
9
|
+
constructor(config?: ClaudeCodeConfig);
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the model to spawn `claude` with. The per-agent model — resolved by
|
|
12
|
+
* the engine into request.model (STU-429) — takes precedence. The
|
|
13
|
+
* construction-time model (config.claudeCode.model, i.e. defaults.model) is
|
|
14
|
+
* only a FALLBACK, for direct callers that omit or blank out request.model.
|
|
15
|
+
* `||` (not `??`) so an empty string also falls back, never reaching the CLI
|
|
16
|
+
* as a broken `--model ""`.
|
|
17
|
+
*/
|
|
18
|
+
private resolveModel;
|
|
19
|
+
call(request: LLMRequest, onToken?: (token: string) => void, signal?: AbortSignal): Promise<LLMResponse>;
|
|
20
|
+
runAgentLoop(request: LLMRequest, executeTool: (name: string, args: Record<string, unknown>, callId: string) => Promise<ToolCallOutcome>, onToken?: (token: string) => void, signal?: AbortSignal): Promise<AgentLoopResult>;
|
|
21
|
+
private spawnClaude;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=claude-code.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-code.d.ts","sourceRoot":"","sources":["../../src/providers/claude-code.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC5E,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGzF,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,kBAAmB,YAAW,iBAAiB;IAC1D,QAAQ,CAAC,IAAI,iBAAiB;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;gBAEnB,MAAM,GAAE,gBAAqB;IAIzC;;;;;;;OAOG;IACH,OAAO,CAAC,YAAY;IAId,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAUxG,YAAY,CAChB,OAAO,EAAE,UAAU,EACnB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,eAAe,CAAC,EACtG,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,EACjC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,eAAe,CAAC;IA6B3B,OAAO,CAAC,WAAW;CA8GpB"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ClaudeCodeProvider = void 0;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const promises_1 = require("node:fs/promises");
|
|
6
|
+
const node_os_1 = require("node:os");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const claude_code_mcp_server_js_1 = require("./claude-code-mcp-server.js");
|
|
10
|
+
class ClaudeCodeProvider {
|
|
11
|
+
name = 'claude-code';
|
|
12
|
+
model;
|
|
13
|
+
constructor(config = {}) {
|
|
14
|
+
this.model = config.model ?? 'claude-sonnet-4-5';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the model to spawn `claude` with. The per-agent model — resolved by
|
|
18
|
+
* the engine into request.model (STU-429) — takes precedence. The
|
|
19
|
+
* construction-time model (config.claudeCode.model, i.e. defaults.model) is
|
|
20
|
+
* only a FALLBACK, for direct callers that omit or blank out request.model.
|
|
21
|
+
* `||` (not `??`) so an empty string also falls back, never reaching the CLI
|
|
22
|
+
* as a broken `--model ""`.
|
|
23
|
+
*/
|
|
24
|
+
resolveModel(request) {
|
|
25
|
+
return request.model || this.model;
|
|
26
|
+
}
|
|
27
|
+
async call(request, onToken, signal) {
|
|
28
|
+
const result = await this.runAgentLoop(request, async () => ({ result: null }), onToken, signal);
|
|
29
|
+
return {
|
|
30
|
+
content: result.content,
|
|
31
|
+
tool_calls: result.tool_calls,
|
|
32
|
+
finish_reason: result.finish_reason,
|
|
33
|
+
usage: result.usage,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async runAgentLoop(request, executeTool, onToken, signal) {
|
|
37
|
+
const tools = request.tools ?? [];
|
|
38
|
+
const prompt = buildPrompt(request);
|
|
39
|
+
const model = this.resolveModel(request);
|
|
40
|
+
// No tools → no MCP server. Attaching the HTTP MCP server makes the claude CLI
|
|
41
|
+
// hang on the streamable-http handshake, and a tool-less agent has nothing to
|
|
42
|
+
// call anyway, so run a plain --print invocation with no --mcp-config.
|
|
43
|
+
if (tools.length === 0) {
|
|
44
|
+
const result = await this.spawnClaude(model, prompt, undefined, onToken, signal);
|
|
45
|
+
return { ...result, tool_calls: [] };
|
|
46
|
+
}
|
|
47
|
+
const mcpServer = new claude_code_mcp_server_js_1.ClaudeCodeMcpServer(tools, executeTool);
|
|
48
|
+
const port = await mcpServer.start();
|
|
49
|
+
const mcpConfig = { mcpServers: { studio: { type: 'http', url: `http://127.0.0.1:${port}` } } };
|
|
50
|
+
const mcpConfigPath = (0, node_path_1.join)((0, node_os_1.tmpdir)(), `studio-mcp-${(0, node_crypto_1.randomUUID)()}.json`);
|
|
51
|
+
await (0, promises_1.writeFile)(mcpConfigPath, JSON.stringify(mcpConfig), 'utf-8');
|
|
52
|
+
try {
|
|
53
|
+
const result = await this.spawnClaude(model, prompt, mcpConfigPath, onToken, signal);
|
|
54
|
+
return { ...result, tool_calls: mcpServer.getToolCalls() };
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
await mcpServer.stop();
|
|
58
|
+
await (0, promises_1.unlink)(mcpConfigPath).catch(() => { });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
spawnClaude(model, prompt, mcpConfigPath, onToken, signal) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
if (signal?.aborted)
|
|
64
|
+
return reject(new DOMException('Aborted', 'AbortError'));
|
|
65
|
+
const args = [
|
|
66
|
+
'--print',
|
|
67
|
+
'--output-format', 'stream-json',
|
|
68
|
+
'--model', model,
|
|
69
|
+
// With tools: expose them via the MCP server. Without tools: disable the
|
|
70
|
+
// CLI's built-in tools (`--tools ""`) so this is a single-turn pure
|
|
71
|
+
// completion — otherwise claude would run an unbounded agentic loop
|
|
72
|
+
// (reading files, running commands) instead of just answering.
|
|
73
|
+
// `--tools` is variadic, so it must be followed by another flag (--verbose),
|
|
74
|
+
// never by the positional prompt.
|
|
75
|
+
...(mcpConfigPath ? ['--mcp-config', mcpConfigPath] : ['--tools', '']),
|
|
76
|
+
// --strict-mcp-config: use ONLY the MCP servers we pass (the studio server,
|
|
77
|
+
// or none for tool-less agents) and ignore the user's GLOBAL MCP servers
|
|
78
|
+
// (claude.ai Gmail/Drive/Linear/Notion/Figma/…). Without this, the spawned
|
|
79
|
+
// `claude` loads those at startup; their streamable-http handshake can hang
|
|
80
|
+
// the whole --print subprocess (→ Studio cancels with 0 tool calls / 0
|
|
81
|
+
// tokens) and injects ~88k tokens of tool defs into every call (~15x cost).
|
|
82
|
+
'--strict-mcp-config',
|
|
83
|
+
// stream-json output with --print REQUIRES --verbose. (The old
|
|
84
|
+
// --no-verbose flag was removed from the claude CLI and now errors.)
|
|
85
|
+
'--verbose',
|
|
86
|
+
'--dangerously-skip-permissions',
|
|
87
|
+
];
|
|
88
|
+
const startedAt = Date.now();
|
|
89
|
+
logCC('spawn', { model, hasMcp: !!mcpConfigPath, flags: args, promptChars: prompt.length });
|
|
90
|
+
// The prompt goes on stdin, never in argv. Linux caps ONE argv entry at
|
|
91
|
+
// MAX_ARG_STRLEN (32 pages = 131072 bytes — not the ARG_MAX getconf reports),
|
|
92
|
+
// so a positional prompt makes spawn throw E2BIG for every agent whose payload
|
|
93
|
+
// outgrows 128 KiB, before the process exists (STU-561).
|
|
94
|
+
// What stdin must never be is an open pipe nobody closes: claude 2.1.37 blocks
|
|
95
|
+
// waiting for its EOF and never emits output. `end()` below is what delivers it.
|
|
96
|
+
const proc = (0, node_child_process_1.spawn)('claude', args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
97
|
+
logCC('spawned', { pid: proc.pid });
|
|
98
|
+
proc.stdin.on('error', () => {
|
|
99
|
+
// The CLI can exit before reading it all; `close` already reports that.
|
|
100
|
+
});
|
|
101
|
+
proc.stdin.end(prompt);
|
|
102
|
+
if (signal) {
|
|
103
|
+
signal.addEventListener('abort', () => {
|
|
104
|
+
proc.kill('SIGTERM');
|
|
105
|
+
reject(new DOMException('Aborted', 'AbortError'));
|
|
106
|
+
}, { once: true });
|
|
107
|
+
}
|
|
108
|
+
let resultContent;
|
|
109
|
+
let stderrContent = '';
|
|
110
|
+
let buffer = '';
|
|
111
|
+
proc.stdout.on('data', (chunk) => {
|
|
112
|
+
buffer += chunk.toString('utf-8');
|
|
113
|
+
const lines = buffer.split('\n');
|
|
114
|
+
buffer = lines.pop() ?? '';
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
if (!line.trim())
|
|
117
|
+
continue;
|
|
118
|
+
try {
|
|
119
|
+
const event = JSON.parse(line);
|
|
120
|
+
logCC('event', { type: event.type, subtype: event.subtype });
|
|
121
|
+
if (event.type === 'assistant') {
|
|
122
|
+
const msg = event.message;
|
|
123
|
+
for (const block of msg.content ?? []) {
|
|
124
|
+
if (block.type === 'text' && block.text) {
|
|
125
|
+
onToken?.(block.text);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (event.type === 'result' && event.subtype === 'success') {
|
|
130
|
+
resultContent = String(event.result ?? '');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// ignore non-JSON lines
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
proc.stderr.on('data', (chunk) => {
|
|
139
|
+
const s = chunk.toString('utf-8');
|
|
140
|
+
stderrContent += s;
|
|
141
|
+
logCC('stderr', s.trim());
|
|
142
|
+
});
|
|
143
|
+
proc.on('close', (code) => {
|
|
144
|
+
logCC('close', { code, ms: Date.now() - startedAt, gotResult: resultContent !== undefined });
|
|
145
|
+
if (signal?.aborted)
|
|
146
|
+
return;
|
|
147
|
+
if (resultContent !== undefined) {
|
|
148
|
+
resolve({ content: resultContent, finish_reason: 'stop', usage: undefined });
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const errDetail = stderrContent.trim() ? `: ${stderrContent.trim()}` : '';
|
|
152
|
+
reject(new Error(`claude -p exited with code ${code}${errDetail}`));
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
proc.on('error', (err) => {
|
|
156
|
+
reject(new Error(`Failed to spawn claude: ${err.message}. Is Claude Code installed?`));
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
exports.ClaudeCodeProvider = ClaudeCodeProvider;
|
|
162
|
+
/**
|
|
163
|
+
* Diagnostic logging for the claude-code provider, gated by the
|
|
164
|
+
* STUDIO_LOG_CLAUDE_CODE env var (set it to any non-empty value to enable).
|
|
165
|
+
* Writes to STDERR only — stdout carries the stream-json/result payload that
|
|
166
|
+
* callers (e.g. `studio run --json`) parse, so it must never be polluted.
|
|
167
|
+
* Lets you see, during a hang, exactly which lifecycle step stalls: spawn →
|
|
168
|
+
* spawned(pid) → event(type)… → close(code, ms, gotResult).
|
|
169
|
+
*/
|
|
170
|
+
function logCC(stage, detail) {
|
|
171
|
+
if (!process.env.STUDIO_LOG_CLAUDE_CODE)
|
|
172
|
+
return;
|
|
173
|
+
let rendered;
|
|
174
|
+
try {
|
|
175
|
+
rendered = typeof detail === 'string' ? detail : JSON.stringify(detail);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
rendered = String(detail);
|
|
179
|
+
}
|
|
180
|
+
process.stderr.write(`[claude-code] ${stage} ${rendered}\n`);
|
|
181
|
+
}
|
|
182
|
+
function buildPrompt(request) {
|
|
183
|
+
const system = request.messages.filter(m => m.role === 'system').map(m => m.content).join('\n\n');
|
|
184
|
+
const user = request.messages.filter(m => m.role !== 'system').map(m => m.content).join('\n\n');
|
|
185
|
+
return system ? `<system>\n${system}\n</system>\n\n${user}` : user;
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=claude-code.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-code.js","sourceRoot":"","sources":["../../src/providers/claude-code.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-code.test.d.ts","sourceRoot":"","sources":["../../src/providers/claude-code.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const claude_code_js_1 = require("./claude-code.js");
|
|
5
|
+
const node_events_1 = require("node:events");
|
|
6
|
+
const node_stream_1 = require("node:stream");
|
|
7
|
+
const { mockSpawn, MockClaudeCodeMcpServer } = vitest_1.vi.hoisted(() => {
|
|
8
|
+
const mockSpawn = vitest_1.vi.fn();
|
|
9
|
+
function MockClaudeCodeMcpServer() {
|
|
10
|
+
return {
|
|
11
|
+
start: vitest_1.vi.fn().mockResolvedValue(9999),
|
|
12
|
+
stop: vitest_1.vi.fn().mockResolvedValue(undefined),
|
|
13
|
+
getToolCalls: vitest_1.vi.fn().mockReturnValue([]),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return { mockSpawn, MockClaudeCodeMcpServer };
|
|
17
|
+
});
|
|
18
|
+
vitest_1.vi.mock('node:child_process', () => ({ spawn: mockSpawn }));
|
|
19
|
+
vitest_1.vi.mock('./claude-code-mcp-server.js', () => ({ ClaudeCodeMcpServer: MockClaudeCodeMcpServer }));
|
|
20
|
+
vitest_1.vi.mock('node:fs/promises', () => {
|
|
21
|
+
return {
|
|
22
|
+
writeFile: vitest_1.vi.fn().mockResolvedValue(undefined),
|
|
23
|
+
unlink: vitest_1.vi.fn().mockResolvedValue(undefined),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
function makeFakeProcess(lines, exitCode = 0) {
|
|
27
|
+
const proc = new node_events_1.EventEmitter();
|
|
28
|
+
proc.stdout = new node_stream_1.Readable({ read() { } });
|
|
29
|
+
proc.stderr = new node_stream_1.Readable({ read() { } });
|
|
30
|
+
proc.stdinWritten = '';
|
|
31
|
+
proc.stdin = new node_stream_1.Writable({ write(chunk, _enc, cb) { proc.stdinWritten += String(chunk); cb(); } });
|
|
32
|
+
proc.kill = vitest_1.vi.fn();
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
proc.stdout.push(line + '\n');
|
|
36
|
+
}
|
|
37
|
+
proc.stdout.push(null);
|
|
38
|
+
proc.emit('close', exitCode);
|
|
39
|
+
}, 0);
|
|
40
|
+
return proc;
|
|
41
|
+
}
|
|
42
|
+
const BASE_REQUEST = {
|
|
43
|
+
model: 'claude-sonnet-4-5',
|
|
44
|
+
messages: [
|
|
45
|
+
{ role: 'system', content: 'You are helpful.' },
|
|
46
|
+
{ role: 'user', content: 'Hello' },
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
(0, vitest_1.describe)('ClaudeCodeProvider', () => {
|
|
50
|
+
(0, vitest_1.beforeEach)(() => {
|
|
51
|
+
vitest_1.vi.clearAllMocks();
|
|
52
|
+
});
|
|
53
|
+
(0, vitest_1.it)('has name "claude-code"', () => {
|
|
54
|
+
const provider = new claude_code_js_1.ClaudeCodeProvider();
|
|
55
|
+
(0, vitest_1.expect)(provider.name).toBe('claude-code');
|
|
56
|
+
});
|
|
57
|
+
(0, vitest_1.it)('runAgentLoop returns content from result event', async () => {
|
|
58
|
+
const lines = [
|
|
59
|
+
JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: '{"summary":"done"}' }] } }),
|
|
60
|
+
JSON.stringify({ type: 'result', subtype: 'success', result: '{"summary":"done"}' }),
|
|
61
|
+
];
|
|
62
|
+
mockSpawn.mockReturnValueOnce(makeFakeProcess(lines));
|
|
63
|
+
const provider = new claude_code_js_1.ClaudeCodeProvider({ model: 'claude-sonnet-4-5' });
|
|
64
|
+
const result = await provider.runAgentLoop(BASE_REQUEST, vitest_1.vi.fn());
|
|
65
|
+
(0, vitest_1.expect)(result.content).toBe('{"summary":"done"}');
|
|
66
|
+
(0, vitest_1.expect)(result.finish_reason).toBe('stop');
|
|
67
|
+
});
|
|
68
|
+
(0, vitest_1.it)('streams tokens via onToken from assistant events', async () => {
|
|
69
|
+
const lines = [
|
|
70
|
+
JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'Hello' }] } }),
|
|
71
|
+
JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: ' world' }] } }),
|
|
72
|
+
JSON.stringify({ type: 'result', subtype: 'success', result: 'Hello world' }),
|
|
73
|
+
];
|
|
74
|
+
mockSpawn.mockReturnValueOnce(makeFakeProcess(lines));
|
|
75
|
+
const provider = new claude_code_js_1.ClaudeCodeProvider({ model: 'claude-sonnet-4-5' });
|
|
76
|
+
const tokens = [];
|
|
77
|
+
await provider.runAgentLoop(BASE_REQUEST, vitest_1.vi.fn(), t => tokens.push(t));
|
|
78
|
+
(0, vitest_1.expect)(tokens).toEqual(['Hello', ' world']);
|
|
79
|
+
});
|
|
80
|
+
(0, vitest_1.it)('spawns claude with --output-format stream-json, --model, and --mcp-config when tools are present', async () => {
|
|
81
|
+
const lines = [JSON.stringify({ type: 'result', subtype: 'success', result: 'ok' })];
|
|
82
|
+
mockSpawn.mockReturnValueOnce(makeFakeProcess(lines));
|
|
83
|
+
const provider = new claude_code_js_1.ClaudeCodeProvider({ model: 'claude-sonnet-4-5' });
|
|
84
|
+
const requestWithTools = {
|
|
85
|
+
...BASE_REQUEST,
|
|
86
|
+
tools: [{ name: 'echo', description: 'echo', parameters: { type: 'object', properties: {} } }],
|
|
87
|
+
};
|
|
88
|
+
await provider.runAgentLoop(requestWithTools, vitest_1.vi.fn());
|
|
89
|
+
const [cmd, args] = mockSpawn.mock.calls[0];
|
|
90
|
+
(0, vitest_1.expect)(cmd).toBe('claude');
|
|
91
|
+
(0, vitest_1.expect)(args).toContain('--output-format');
|
|
92
|
+
(0, vitest_1.expect)(args).toContain('stream-json');
|
|
93
|
+
(0, vitest_1.expect)(args).toContain('--model');
|
|
94
|
+
(0, vitest_1.expect)(args).toContain('claude-sonnet-4-5');
|
|
95
|
+
(0, vitest_1.expect)(args).toContain('--mcp-config');
|
|
96
|
+
// --strict-mcp-config pins the spawned claude to ONLY the studio MCP server,
|
|
97
|
+
// ignoring the user's global MCP servers (claude.ai Gmail/Drive/Linear/etc.)
|
|
98
|
+
// whose startup handshake otherwise hangs the subprocess and balloons cost.
|
|
99
|
+
(0, vitest_1.expect)(args).toContain('--strict-mcp-config');
|
|
100
|
+
// --output-format stream-json with --print REQUIRES --verbose; the old
|
|
101
|
+
// --no-verbose flag was removed from the claude CLI and now errors out.
|
|
102
|
+
(0, vitest_1.expect)(args).toContain('--verbose');
|
|
103
|
+
(0, vitest_1.expect)(args).not.toContain('--no-verbose');
|
|
104
|
+
});
|
|
105
|
+
(0, vitest_1.it)('sends the prompt on stdin and closes it, never in argv', async () => {
|
|
106
|
+
const lines = [JSON.stringify({ type: 'result', subtype: 'success', result: 'ok' })];
|
|
107
|
+
const proc = makeFakeProcess(lines);
|
|
108
|
+
mockSpawn.mockReturnValueOnce(proc);
|
|
109
|
+
const provider = new claude_code_js_1.ClaudeCodeProvider({ model: 'claude-sonnet-4-5' });
|
|
110
|
+
await provider.runAgentLoop(BASE_REQUEST, vitest_1.vi.fn());
|
|
111
|
+
const [, args, options] = mockSpawn.mock.calls[0];
|
|
112
|
+
// A positional prompt caps every agent at MAX_ARG_STRLEN and fails with E2BIG
|
|
113
|
+
// before the process exists (STU-561) — the last arg must stay a flag.
|
|
114
|
+
(0, vitest_1.expect)(args.some(arg => arg.includes('Hello'))).toBe(false);
|
|
115
|
+
(0, vitest_1.expect)(args.at(-1)).toBe('--dangerously-skip-permissions');
|
|
116
|
+
(0, vitest_1.expect)(options.stdio[0]).toBe('pipe');
|
|
117
|
+
(0, vitest_1.expect)(proc.stdinWritten).toContain('Hello');
|
|
118
|
+
(0, vitest_1.expect)(proc.stdinWritten).toContain('You are helpful.');
|
|
119
|
+
// claude blocks forever on a pipe nobody closes; end() is what delivers the EOF.
|
|
120
|
+
(0, vitest_1.expect)(proc.stdin.writableEnded).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
(0, vitest_1.it)('a prompt past MAX_ARG_STRLEN reaches a real process on stdin and cannot on argv', async () => {
|
|
123
|
+
// The reason the test above exists, against the real kernel rather than a mock:
|
|
124
|
+
// Linux caps one argv entry at 32 pages, so the old positional prompt made
|
|
125
|
+
// spawn throw E2BIG for any agent whose payload outgrew it (STU-561). This is
|
|
126
|
+
// not a Claude limit and no CLI flag lifts it — stdin is the only way through.
|
|
127
|
+
const { spawn: realSpawn } = await vitest_1.vi.importActual('node:child_process');
|
|
128
|
+
const huge = 'x'.repeat(512 * 1024);
|
|
129
|
+
(0, vitest_1.expect)(() => realSpawn('/bin/cat', [huge], { stdio: 'ignore' })).toThrow(/E2BIG/);
|
|
130
|
+
const received = await new Promise((resolve, reject) => {
|
|
131
|
+
const proc = realSpawn('/bin/cat', [], { stdio: ['pipe', 'pipe', 'ignore'] });
|
|
132
|
+
let out = 0;
|
|
133
|
+
proc.stdout.on('data', (chunk) => { out += chunk.length; });
|
|
134
|
+
proc.on('close', () => resolve(out));
|
|
135
|
+
proc.on('error', reject);
|
|
136
|
+
proc.stdin.end(huge);
|
|
137
|
+
});
|
|
138
|
+
(0, vitest_1.expect)(received).toBe(huge.length);
|
|
139
|
+
});
|
|
140
|
+
(0, vitest_1.it)('honors the per-agent model from request.model over the construction-time default', async () => {
|
|
141
|
+
// STU-429: the construction-time model (from config.claudeCode.model, i.e.
|
|
142
|
+
// defaults.model) is only a FALLBACK. When a stage's agent declares its own
|
|
143
|
+
// model, the engine resolves it into request.model — the provider must spawn
|
|
144
|
+
// claude with THAT model, not the default it was constructed with.
|
|
145
|
+
const lines = [JSON.stringify({ type: 'result', subtype: 'success', result: 'ok' })];
|
|
146
|
+
mockSpawn.mockReturnValueOnce(makeFakeProcess(lines));
|
|
147
|
+
const provider = new claude_code_js_1.ClaudeCodeProvider({ model: 'claude-sonnet-4-5' }); // default
|
|
148
|
+
const requestWithModel = { ...BASE_REQUEST, model: 'claude-opus-4-1' }; // per-agent override
|
|
149
|
+
await provider.runAgentLoop(requestWithModel, vitest_1.vi.fn());
|
|
150
|
+
const [, args] = mockSpawn.mock.calls[0];
|
|
151
|
+
const modelIdx = args.indexOf('--model');
|
|
152
|
+
(0, vitest_1.expect)(modelIdx).toBeGreaterThanOrEqual(0);
|
|
153
|
+
(0, vitest_1.expect)(args[modelIdx + 1]).toBe('claude-opus-4-1');
|
|
154
|
+
});
|
|
155
|
+
(0, vitest_1.it)('falls back to the construction-time default when request.model is absent', async () => {
|
|
156
|
+
// Direct callers (not the runner) may omit model; the construction default
|
|
157
|
+
// still applies as the fallback.
|
|
158
|
+
const lines = [JSON.stringify({ type: 'result', subtype: 'success', result: 'ok' })];
|
|
159
|
+
mockSpawn.mockReturnValueOnce(makeFakeProcess(lines));
|
|
160
|
+
const provider = new claude_code_js_1.ClaudeCodeProvider({ model: 'claude-sonnet-4-5' });
|
|
161
|
+
const requestNoModel = { ...BASE_REQUEST, model: undefined };
|
|
162
|
+
await provider.runAgentLoop(requestNoModel, vitest_1.vi.fn());
|
|
163
|
+
const [, args] = mockSpawn.mock.calls[0];
|
|
164
|
+
const modelIdx = args.indexOf('--model');
|
|
165
|
+
(0, vitest_1.expect)(args[modelIdx + 1]).toBe('claude-sonnet-4-5');
|
|
166
|
+
});
|
|
167
|
+
(0, vitest_1.it)('skips the MCP server and --mcp-config when there are no tools', async () => {
|
|
168
|
+
// Attaching the HTTP MCP server makes the claude CLI hang on the streamable-http
|
|
169
|
+
// handshake; a tool-less agent (e.g. a pure classifier) needs no MCP server.
|
|
170
|
+
const lines = [JSON.stringify({ type: 'result', subtype: 'success', result: 'ok' })];
|
|
171
|
+
mockSpawn.mockReturnValueOnce(makeFakeProcess(lines));
|
|
172
|
+
const provider = new claude_code_js_1.ClaudeCodeProvider({ model: 'claude-sonnet-4-5' });
|
|
173
|
+
const result = await provider.runAgentLoop(BASE_REQUEST, vitest_1.vi.fn()); // BASE_REQUEST has no tools
|
|
174
|
+
const [, args] = mockSpawn.mock.calls[0];
|
|
175
|
+
(0, vitest_1.expect)(args).not.toContain('--mcp-config');
|
|
176
|
+
// No --mcp-config here, but --strict-mcp-config still matters: it stops the
|
|
177
|
+
// CLI from loading the user's global MCP servers, which otherwise inject ~88k
|
|
178
|
+
// tokens of tool defs per call (~15x cost) and can hang on their handshake.
|
|
179
|
+
(0, vitest_1.expect)(args).toContain('--strict-mcp-config');
|
|
180
|
+
(0, vitest_1.expect)(args).toContain('--verbose');
|
|
181
|
+
// built-in tools disabled for a single-turn pure completion (no agentic roaming),
|
|
182
|
+
// and the empty value must sit before a flag so the variadic doesn't eat the prompt.
|
|
183
|
+
const toolsIdx = args.indexOf('--tools');
|
|
184
|
+
(0, vitest_1.expect)(toolsIdx).toBeGreaterThanOrEqual(0);
|
|
185
|
+
(0, vitest_1.expect)(args[toolsIdx + 1]).toBe('');
|
|
186
|
+
(0, vitest_1.expect)(args[toolsIdx + 2]).toMatch(/^--/);
|
|
187
|
+
(0, vitest_1.expect)(result.tool_calls).toEqual([]);
|
|
188
|
+
});
|
|
189
|
+
(0, vitest_1.it)('never leaves claude an open stdin pipe (else --print hangs waiting for EOF)', async () => {
|
|
190
|
+
// ROOT CAUSE of the studio classify hang: an open, non-TTY stdin nobody closes.
|
|
191
|
+
// claude 2.1.37 blocks waiting for its EOF and never emits output, so Studio
|
|
192
|
+
// cancels it (0 tool calls / 0 tokens). The prompt now travels on that pipe, so
|
|
193
|
+
// the guard is no longer stdio[0]='ignore' — it is that the pipe always ends.
|
|
194
|
+
const lines = [JSON.stringify({ type: 'result', subtype: 'success', result: 'ok' })];
|
|
195
|
+
const proc = makeFakeProcess(lines);
|
|
196
|
+
mockSpawn.mockReturnValueOnce(proc);
|
|
197
|
+
await new claude_code_js_1.ClaudeCodeProvider().runAgentLoop(BASE_REQUEST, vitest_1.vi.fn());
|
|
198
|
+
(0, vitest_1.expect)(proc.stdin.writableEnded).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
(0, vitest_1.it)('logs lifecycle to stderr only when STUDIO_LOG_CLAUDE_CODE is set', async () => {
|
|
201
|
+
const lines = [JSON.stringify({ type: 'result', subtype: 'success', result: 'ok' })];
|
|
202
|
+
const writes = [];
|
|
203
|
+
const spy = vitest_1.vi.spyOn(process.stderr, 'write').mockImplementation(((s) => {
|
|
204
|
+
writes.push(String(s));
|
|
205
|
+
return true;
|
|
206
|
+
}));
|
|
207
|
+
try {
|
|
208
|
+
// Off by default: no diagnostic lines.
|
|
209
|
+
delete process.env.STUDIO_LOG_CLAUDE_CODE;
|
|
210
|
+
mockSpawn.mockReturnValueOnce(makeFakeProcess(lines));
|
|
211
|
+
await new claude_code_js_1.ClaudeCodeProvider().runAgentLoop(BASE_REQUEST, vitest_1.vi.fn());
|
|
212
|
+
(0, vitest_1.expect)(writes.some(w => w.includes('[claude-code]'))).toBe(false);
|
|
213
|
+
// On when the env var is set: spawn + close lifecycle lines on stderr.
|
|
214
|
+
process.env.STUDIO_LOG_CLAUDE_CODE = '1';
|
|
215
|
+
mockSpawn.mockReturnValueOnce(makeFakeProcess(lines));
|
|
216
|
+
await new claude_code_js_1.ClaudeCodeProvider().runAgentLoop(BASE_REQUEST, vitest_1.vi.fn());
|
|
217
|
+
(0, vitest_1.expect)(writes.some(w => w.includes('[claude-code] spawn'))).toBe(true);
|
|
218
|
+
(0, vitest_1.expect)(writes.some(w => w.includes('[claude-code] close'))).toBe(true);
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
delete process.env.STUDIO_LOG_CLAUDE_CODE;
|
|
222
|
+
spy.mockRestore();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
(0, vitest_1.it)('throws when claude exits non-zero with no result event', async () => {
|
|
226
|
+
mockSpawn.mockReturnValueOnce(makeFakeProcess([], 1));
|
|
227
|
+
const provider = new claude_code_js_1.ClaudeCodeProvider();
|
|
228
|
+
await (0, vitest_1.expect)(provider.runAgentLoop(BASE_REQUEST, vitest_1.vi.fn())).rejects.toThrow(/claude -p exited/i);
|
|
229
|
+
});
|
|
230
|
+
(0, vitest_1.it)('aborts child process when signal fires', async () => {
|
|
231
|
+
const controller = new AbortController();
|
|
232
|
+
// Long-running proc that never closes on its own
|
|
233
|
+
const proc = new node_events_1.EventEmitter();
|
|
234
|
+
proc.stdout = new node_stream_1.Readable({ read() { } });
|
|
235
|
+
proc.stderr = new node_stream_1.Readable({ read() { } });
|
|
236
|
+
proc.stdin = new node_stream_1.Writable({ write(_c, _e, cb) { cb(); } });
|
|
237
|
+
proc.kill = vitest_1.vi.fn();
|
|
238
|
+
mockSpawn.mockReturnValueOnce(proc);
|
|
239
|
+
const provider = new claude_code_js_1.ClaudeCodeProvider();
|
|
240
|
+
const promise = provider.runAgentLoop(BASE_REQUEST, vitest_1.vi.fn(), undefined, controller.signal);
|
|
241
|
+
// Wait for spawn to be called and listeners to be set up
|
|
242
|
+
await Promise.resolve();
|
|
243
|
+
await Promise.resolve();
|
|
244
|
+
controller.abort();
|
|
245
|
+
// Simulate OS sending signal back as close event
|
|
246
|
+
proc.emit('close', 130);
|
|
247
|
+
await (0, vitest_1.expect)(promise).rejects.toThrow(/aborted/i);
|
|
248
|
+
(0, vitest_1.expect)(proc.kill).toHaveBeenCalled();
|
|
249
|
+
});
|
|
250
|
+
(0, vitest_1.it)('call() delegates to runAgentLoop and returns LLMResponse shape', async () => {
|
|
251
|
+
const lines = [JSON.stringify({ type: 'result', subtype: 'success', result: '{"answer":42}' })];
|
|
252
|
+
mockSpawn.mockReturnValueOnce(makeFakeProcess(lines));
|
|
253
|
+
const provider = new claude_code_js_1.ClaudeCodeProvider({ model: 'claude-sonnet-4-5' });
|
|
254
|
+
const result = await provider.call(BASE_REQUEST);
|
|
255
|
+
(0, vitest_1.expect)(result.content).toBe('{"answer":42}');
|
|
256
|
+
(0, vitest_1.expect)(result.finish_reason).toBe('stop');
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
//# sourceMappingURL=claude-code.test.js.map
|