@studio-foundation/runner 0.4.0-beta → 0.5.2

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.
Files changed (53) hide show
  1. package/README.md +1 -1
  2. package/dist/__tests__/script-executor.test.js +57 -3
  3. package/dist/__tests__/script-executor.test.js.map +1 -1
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +3 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/middleware/anonymization.d.ts +17 -2
  9. package/dist/middleware/anonymization.d.ts.map +1 -1
  10. package/dist/middleware/anonymization.js +37 -1
  11. package/dist/middleware/anonymization.js.map +1 -1
  12. package/dist/prompt-builder.d.ts +10 -6
  13. package/dist/prompt-builder.d.ts.map +1 -1
  14. package/dist/prompt-builder.js +27 -2
  15. package/dist/prompt-builder.js.map +1 -1
  16. package/dist/providers/claude-code-mcp-server.d.ts +15 -0
  17. package/dist/providers/claude-code-mcp-server.d.ts.map +1 -0
  18. package/dist/providers/claude-code-mcp-server.js +126 -0
  19. package/dist/providers/claude-code-mcp-server.js.map +1 -0
  20. package/dist/providers/claude-code-mcp-server.test.d.ts +2 -0
  21. package/dist/providers/claude-code-mcp-server.test.d.ts.map +1 -0
  22. package/dist/providers/claude-code-mcp-server.test.js +123 -0
  23. package/dist/providers/claude-code-mcp-server.test.js.map +1 -0
  24. package/dist/providers/claude-code.d.ts +23 -0
  25. package/dist/providers/claude-code.d.ts.map +1 -0
  26. package/dist/providers/claude-code.js +187 -0
  27. package/dist/providers/claude-code.js.map +1 -0
  28. package/dist/providers/claude-code.test.d.ts +2 -0
  29. package/dist/providers/claude-code.test.d.ts.map +1 -0
  30. package/dist/providers/claude-code.test.js +259 -0
  31. package/dist/providers/claude-code.test.js.map +1 -0
  32. package/dist/providers/registry.d.ts +3 -0
  33. package/dist/providers/registry.d.ts.map +1 -1
  34. package/dist/providers/registry.js +4 -0
  35. package/dist/providers/registry.js.map +1 -1
  36. package/dist/providers/registry.test.d.ts +2 -0
  37. package/dist/providers/registry.test.d.ts.map +1 -0
  38. package/dist/providers/registry.test.js +56 -0
  39. package/dist/providers/registry.test.js.map +1 -0
  40. package/dist/runner.d.ts +2 -0
  41. package/dist/runner.d.ts.map +1 -1
  42. package/dist/runner.js +55 -8
  43. package/dist/runner.js.map +1 -1
  44. package/dist/runner.test.js +5 -2
  45. package/dist/runner.test.js.map +1 -1
  46. package/dist/script-executor.d.ts.map +1 -1
  47. package/dist/script-executor.js +16 -2
  48. package/dist/script-executor.js.map +1 -1
  49. package/dist/tools/tool-registry.d.ts +21 -4
  50. package/dist/tools/tool-registry.d.ts.map +1 -1
  51. package/dist/tools/tool-registry.js +55 -17
  52. package/dist/tools/tool-registry.js.map +1 -1
  53. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=claude-code.test.d.ts.map
@@ -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