@yemi33/minions 0.1.1587 → 0.1.1589

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.
@@ -1,209 +1,293 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * spawn-agent.js — Wrapper to spawn claude CLI safely
4
- * Reads prompt and system prompt from files, avoiding shell metacharacter issues.
3
+ * spawn-agent.js — Runtime-agnostic wrapper for spawning a CLI runtime.
5
4
  *
6
- * Usage: node spawn-agent.js <prompt-file> <sysprompt-file> [claude-args...]
5
+ * As of P-9c4f2d6a this script knows nothing CLI-specific. All binary
6
+ * resolution, arg construction, and prompt prep flows through the runtime
7
+ * adapter resolved from `--runtime <name>` (defaults to 'claude').
8
+ *
9
+ * Usage:
10
+ * node spawn-agent.js <prompt-file> <sysprompt-file> [--runtime <name>] [adapter opts...] [unknown args...]
11
+ *
12
+ * Recognized adapter opts (parsed and forwarded to `runtime.buildArgs(opts)`):
13
+ * --model <m> → opts.model
14
+ * --max-turns <n> → opts.maxTurns
15
+ * --allowedTools <list> → opts.allowedTools
16
+ * --effort <level> → opts.effort
17
+ * --resume <sessionId> → opts.sessionId
18
+ * --max-budget-usd <n> → opts.maxBudget
19
+ * --bare → opts.bare = true
20
+ * --fallback-model <m> → opts.fallbackModel
21
+ * --output-format <f> → opts.outputFormat
22
+ * --verbose / --no-verbose → opts.verbose
23
+ * --stream <on|off> → opts.stream (Copilot)
24
+ * --disable-builtin-mcps → opts.disableBuiltinMcps (Copilot)
25
+ * --no-custom-instructions → opts.suppressAgentsMd (Copilot)
26
+ * --enable-reasoning-summaries→ opts.reasoningSummaries (Copilot)
27
+ *
28
+ * Legacy --permission-mode <X> is dropped: the new Claude adapter emits
29
+ * `--dangerously-skip-permissions` itself, so passing the legacy flag from a
30
+ * pre-P-2a6d9c4f engine.js would only produce a duplicate flag. Other unknown
31
+ * args are forwarded verbatim to the runtime binary as a defensive escape
32
+ * hatch.
7
33
  */
8
34
 
9
35
  const fs = require('fs');
10
36
  const os = require('os');
11
37
  const path = require('path');
12
- const { exec, runFile, cleanChildEnv, killGracefully, killImmediate, ts, safeJson, safeWrite } = require('./shared');
38
+ const { runFile, cleanChildEnv, killGracefully, killImmediate, ts } = require('./shared');
39
+ const { resolveRuntime } = require('./runtimes');
40
+
41
+ // ─── Pure helpers (exported for tests) ──────────────────────────────────────
42
+
43
+ /**
44
+ * Parse argv into { promptFile, sysPromptFile, runtimeName, opts, passthrough }.
45
+ * Returns null when the two positional args are missing.
46
+ */
47
+ function parseSpawnArgs(argv) {
48
+ const args = (argv || []).slice(2);
49
+ if (args.length < 2) return null;
13
50
 
14
- const [,, promptFile, sysPromptFile, ...extraArgs] = process.argv;
51
+ const promptFile = args[0];
52
+ const sysPromptFile = args[1];
53
+ let runtimeName = 'claude';
54
+ const opts = {};
55
+ const passthrough = [];
15
56
 
16
- if (!promptFile || !sysPromptFile) {
17
- console.error('Usage: node spawn-agent.js <prompt-file> <sysprompt-file> [args...]');
18
- process.exit(1);
57
+ for (let i = 2; i < args.length; i++) {
58
+ const a = args[i];
59
+ const peek = () => args[++i];
60
+
61
+ switch (a) {
62
+ case '--runtime': runtimeName = peek(); break;
63
+ case '--model': opts.model = peek(); break;
64
+ case '--max-turns': opts.maxTurns = peek(); break;
65
+ case '--allowedTools': opts.allowedTools = peek(); break;
66
+ case '--effort': opts.effort = peek(); break;
67
+ case '--resume': opts.sessionId = peek(); break;
68
+ case '--max-budget-usd': opts.maxBudget = peek(); break;
69
+ case '--bare': opts.bare = true; break;
70
+ case '--fallback-model': opts.fallbackModel = peek(); break;
71
+ case '--output-format': opts.outputFormat = peek(); break;
72
+ case '--verbose': opts.verbose = true; break;
73
+ case '--no-verbose': opts.verbose = false; break;
74
+ case '--stream': opts.stream = peek(); break;
75
+ case '--disable-builtin-mcps': opts.disableBuiltinMcps = true; break;
76
+ case '--no-custom-instructions': opts.suppressAgentsMd = true; break;
77
+ case '--enable-reasoning-summaries': opts.reasoningSummaries = true; break;
78
+ // LEGACY: dropped — the runtime adapter emits its own permission flag.
79
+ // Pre-P-2a6d9c4f engine.js still passes `--permission-mode bypassPermissions`;
80
+ // letting it through would duplicate the permission flag for Claude.
81
+ case '--permission-mode': i++; break;
82
+ default: passthrough.push(a); break;
83
+ }
84
+ }
85
+
86
+ return { promptFile, sysPromptFile, runtimeName, opts, passthrough };
19
87
  }
20
88
 
21
- const prompt = fs.readFileSync(promptFile, 'utf8');
22
- const sysPrompt = fs.readFileSync(sysPromptFile, 'utf8');
23
-
24
- const env = cleanChildEnv();
25
-
26
- // Resolve claude binary — supports both npm install (cli.js) and native installer (binary on PATH)
27
- let claudeBin;
28
- let claudeIsNative = false; // true = native binary, false = node cli.js
29
- const capsCachePath = path.join(__dirname, 'claude-caps.json');
30
- let _cacheHit = false;
31
- const isWin = process.platform === 'win32';
32
-
33
- // Probe a @anthropic-ai/claude-code package dir for the best binary: native exe > cli.js
34
- function _probeClaudePackage(pkgDir) {
35
- const nativeBin = path.join(pkgDir, 'bin', isWin ? 'claude.exe' : 'claude');
36
- if (fs.existsSync(nativeBin)) return { bin: nativeBin, native: true };
37
- const cliJs = path.join(pkgDir, 'cli.js');
38
- if (fs.existsSync(cliJs)) return { bin: cliJs, native: false };
39
- return null;
89
+ /**
90
+ * Compose the final spawn invocation. Pure: no FS, no spawn. Caller is
91
+ * responsible for writing the sysprompt tmp file (when `sysPromptFile` is
92
+ * supplied via opts) and for cleanup.
93
+ *
94
+ * Returns:
95
+ * {
96
+ * bin, leadingArgs, args, // spawn(execPath OR bin, [bin?, ...leadingArgs, ...args])
97
+ * deliveryMode, // 'stdin' | 'arg' (per runtime.capabilities.promptViaArg)
98
+ * finalPrompt, // adapter-built prompt text (may include sysprompt for some runtimes)
99
+ * usingNodeShim, // true runtime returned a non-native binary (Claude cli.js)
100
+ * }
101
+ */
102
+ function buildSpawnInvocation({ runtime, resolved, promptText, sysPromptText, opts, passthrough, addDirs }) {
103
+ const finalPrompt = runtime.buildPrompt(promptText, sysPromptText);
104
+ const adapterOpts = { ...opts };
105
+ if (Array.isArray(addDirs) && addDirs.length) adapterOpts.addDirs = addDirs;
106
+ // When the adapter delivers the prompt via argv, hand it the prompt so it
107
+ // can splice `--prompt <text>` into its args.
108
+ if (runtime.capabilities && runtime.capabilities.promptViaArg) {
109
+ adapterOpts.prompt = finalPrompt;
110
+ }
111
+ const adapterArgs = runtime.buildArgs(adapterOpts);
112
+ const { bin, native, leadingArgs = [] } = resolved;
113
+ return {
114
+ bin,
115
+ native,
116
+ leadingArgs,
117
+ args: [...adapterArgs, ...(passthrough || [])],
118
+ deliveryMode: runtime.capabilities && runtime.capabilities.promptViaArg ? 'arg' : 'stdin',
119
+ finalPrompt,
120
+ usingNodeShim: !native,
121
+ };
40
122
  }
41
123
 
42
- // Fast path: use cached binary path if it still exists on disk
43
- const caps = safeJson(capsCachePath);
44
- if (caps?.claudeBin && fs.existsSync(caps.claudeBin)) {
45
- claudeBin = caps.claudeBin;
46
- claudeIsNative = !!caps.claudeIsNative;
47
- _cacheHit = true;
124
+ // ─── Main script execution ──────────────────────────────────────────────────
125
+
126
+ function _installHint(name, runtime) {
127
+ // Adapters expose `installHint` as the canonical message; fall back to a
128
+ // generic line when an adapter without one is registered (defensive — every
129
+ // bundled adapter sets it, but custom registrations may not).
130
+ if (runtime && typeof runtime.installHint === 'string' && runtime.installHint) {
131
+ return runtime.installHint;
132
+ }
133
+ return `${name} CLI binary not found on PATH`;
48
134
  }
49
135
 
50
- // Strategy 1: Find `claude` on PATH, then probe known binary locations relative to the wrapper's directory
51
- if (!claudeBin) try {
52
- const cmd = isWin ? 'where claude 2>NUL' : 'which claude 2>/dev/null';
53
- const which = exec(cmd, { encoding: 'utf8', env, timeout: 10000 }).trim().split('\n')[0].trim();
54
- if (which) {
55
- // On non-Windows under Git Bash/MSYS, `which` returns POSIX paths (/c/Users/...) — normalize
56
- const whichNative = isWin ? which : which.replace(/^\/([a-zA-Z])\//, (_, d) => d.toUpperCase() + ':/').replace(/\//g, path.sep);
57
- const ccPkg = path.join(path.dirname(whichNative), 'node_modules', '@anthropic-ai', 'claude-code');
58
- const found = _probeClaudePackage(ccPkg);
59
- if (found) {
60
- claudeBin = found.bin;
61
- claudeIsNative = found.native;
62
- } else {
63
- // Not an npm wrapper — on Windows, only trust .exe files (shell scripts can't be spawned directly)
64
- if (!isWin || path.extname(whichNative).toLowerCase() === '.exe') {
65
- claudeBin = whichNative;
66
- claudeIsNative = true;
67
- }
68
- }
136
+ function main() {
137
+ const parsed = parseSpawnArgs(process.argv);
138
+ if (!parsed) {
139
+ console.error('Usage: node spawn-agent.js <prompt-file> <sysprompt-file> [--runtime <name>] [args...]');
140
+ process.exit(1);
141
+ }
142
+ const { promptFile, sysPromptFile, runtimeName, opts, passthrough } = parsed;
143
+
144
+ const env = cleanChildEnv();
145
+
146
+ let runtime;
147
+ try { runtime = resolveRuntime(runtimeName); }
148
+ catch (err) {
149
+ console.error(`FATAL: ${err.message}`);
150
+ process.exit(78);
151
+ }
152
+
153
+ const promptText = fs.readFileSync(promptFile, 'utf8');
154
+ const sysPromptText = fs.readFileSync(sysPromptFile, 'utf8');
155
+
156
+ // Sys prompt tmp file — only when (a) NOT resuming and (b) the adapter
157
+ // accepts a system prompt as a separate file. For runtimes that bake the
158
+ // system prompt into the user prompt (e.g. Copilot), sysPromptText is
159
+ // already merged inside `runtime.buildPrompt(prompt, sys)`.
160
+ const isResume = opts.sessionId != null;
161
+ const sysTmpPath = sysPromptFile + '.tmp';
162
+ const wantsSystemPromptFile = !isResume && runtime.capabilities && runtime.capabilities.systemPromptFile;
163
+ if (wantsSystemPromptFile) {
164
+ fs.writeFileSync(sysTmpPath, sysPromptText);
165
+ opts.sysPromptFile = sysTmpPath;
69
166
  }
70
- } catch { /* optional */ }
71
-
72
- // Strategy 2: Known node_modules locations (npm global installs)
73
- if (!claudeBin) {
74
- const prefixes = [
75
- process.env.npm_config_prefix ? path.join(process.env.npm_config_prefix, 'node_modules', '@anthropic-ai', 'claude-code') : '',
76
- process.env.APPDATA ? path.join(process.env.APPDATA, 'npm', 'node_modules', '@anthropic-ai', 'claude-code') : '',
77
- '/usr/local/lib/node_modules/@anthropic-ai/claude-code',
78
- '/usr/lib/node_modules/@anthropic-ai/claude-code',
79
- '/opt/homebrew/lib/node_modules/@anthropic-ai/claude-code',
80
- path.join(path.dirname(process.execPath), '..', 'lib', 'node_modules', '@anthropic-ai', 'claude-code'),
81
- path.join(path.dirname(process.execPath), 'node_modules', '@anthropic-ai', 'claude-code'),
82
- path.join(__dirname, '..', 'node_modules', '@anthropic-ai', 'claude-code'),
83
- ].filter(Boolean);
84
- for (const pkg of prefixes) {
167
+
168
+ // Skill discovery dirs — agents run with CWD set to an external repo
169
+ // worktree, so skills in the minions repo and the user's global ~/.claude
170
+ // dir are otherwise invisible. The adapter decides how to surface them
171
+ // (Claude `--add-dir <path>`; Copilot → ignored).
172
+ const minionsDir = path.resolve(__dirname, '..');
173
+ const userClaudeDir = path.join(os.homedir(), '.claude');
174
+ const addDirs = [minionsDir];
175
+ if (fs.existsSync(userClaudeDir) && path.resolve(userClaudeDir) !== path.resolve(minionsDir)) {
176
+ addDirs.push(userClaudeDir);
177
+ }
178
+
179
+ let resolved;
180
+ try { resolved = runtime.resolveBinary({ env }); }
181
+ catch (err) {
182
+ console.error(`FATAL: ${runtimeName} runtime resolveBinary failed: ${err.message}`);
183
+ process.exit(78);
184
+ }
185
+ if (!resolved) {
186
+ console.error(`FATAL: Cannot find ${runtimeName} CLI — ${_installHint(runtimeName, runtime)}`);
187
+ process.exit(78);
188
+ }
189
+
190
+ const invocation = buildSpawnInvocation({
191
+ runtime, resolved,
192
+ promptText, sysPromptText,
193
+ opts, passthrough, addDirs,
194
+ });
195
+
196
+ // Debug log (async — not on critical path)
197
+ const tmpDir = path.join(__dirname, 'tmp');
198
+ if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
199
+ const debugPath = path.join(tmpDir, 'spawn-debug.log');
200
+ fs.writeFile(
201
+ debugPath,
202
+ `spawn-agent.js at ${ts()}\nruntime=${runtimeName}\nbin=${invocation.bin}\nnative=${invocation.native}\n` +
203
+ `leadingArgs=${invocation.leadingArgs.join(' ')}\nprompt=${promptFile}\nsysPrompt=${sysPromptFile}\n` +
204
+ `delivery=${invocation.deliveryMode}\nargs=${invocation.args.join(' ').slice(0, 800)}\n`,
205
+ () => {},
206
+ );
207
+
208
+ // Build the actual exec form. If the runtime returns a non-native binary
209
+ // (e.g. Claude's cli.js), shim it under the current node process.
210
+ const execBin = invocation.native ? invocation.bin : process.execPath;
211
+ const execArgs = invocation.native
212
+ ? [...invocation.leadingArgs, ...invocation.args]
213
+ : [invocation.bin, ...invocation.leadingArgs, ...invocation.args];
214
+
215
+ const proc = runFile(execBin, execArgs, { stdio: ['pipe', 'pipe', 'pipe'], env });
216
+
217
+ fs.appendFile(debugPath, `PID=${proc.pid || 'none'}\n`, () => {});
218
+
219
+ // Write PID file for parent engine to verify spawn (async — engine checks after 5s)
220
+ const pidFile = promptFile.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid');
221
+ fs.writeFile(pidFile, String(proc.pid || ''), () => {});
222
+
223
+ // Deliver the prompt — stdin (Claude default) vs argv (handled by adapter).
224
+ if (invocation.deliveryMode === 'stdin') {
85
225
  try {
86
- const found = _probeClaudePackage(pkg);
87
- if (found) { claudeBin = found.bin; claudeIsNative = found.native; break; }
88
- } catch {}
226
+ proc.stdin.write(invocation.finalPrompt);
227
+ proc.stdin.end();
228
+ } catch (err) {
229
+ console.error(`FATAL: stdin write failed (broken pipe): ${err.message}`);
230
+ fs.appendFileSync(debugPath, `STDIN ERROR: ${err.message}\n`);
231
+ killImmediate(proc);
232
+ process.exit(1);
233
+ }
234
+ } else {
235
+ // Adapter has already spliced the prompt into argv (--prompt <text>).
236
+ // Close stdin so the runtime doesn't wait on it.
237
+ try { proc.stdin.end(); } catch { /* may already be closed */ }
89
238
  }
90
- }
91
239
 
92
- // Strategy 3: npm root -g
93
- if (!claudeBin) {
94
- try {
95
- const globalRoot = exec('npm root -g', { encoding: 'utf8', env, timeout: 10000 }).trim();
96
- const found = _probeClaudePackage(path.join(globalRoot, '@anthropic-ai', 'claude-code'));
97
- if (found) { claudeBin = found.bin; claudeIsNative = found.native; }
98
- } catch { /* optional */ }
99
- }
240
+ // Clean up sys tmp (only created for non-resume sessions on adapters that
241
+ // use --system-prompt-file).
242
+ if (wantsSystemPromptFile) {
243
+ setTimeout(() => { try { fs.unlinkSync(sysTmpPath); } catch { /* cleanup */ } }, 5000);
244
+ }
100
245
 
101
- // Debug log (async not on critical path)
102
- const tmpDir = path.join(__dirname, 'tmp');
103
- if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
104
- const debugPath = path.join(tmpDir, 'spawn-debug.log');
105
- fs.writeFile(debugPath, `spawn-agent.js at ${ts()}\nclaudeBin=${claudeBin || 'not found'}\nnative=${claudeIsNative}\nprompt=${promptFile}\nsysPrompt=${sysPromptFile}\nextraArgs=${extraArgs.join(' ')}\n`, () => {});
106
-
107
- // Skill discovery dirs — agents run with CWD set to an external repo worktree,
108
- // so skills in the minions repo and the user's global ~/.claude dir are otherwise
109
- // invisible. Pass them via --add-dir on every invocation (resume included, since
110
- // each resume spawns a fresh CLI process with the same CWD). See issue #1231.
111
- const minionsDir = path.resolve(__dirname, '..');
112
- const userClaudeDir = path.join(os.homedir(), '.claude');
113
- const addDirArgs = ['--add-dir', minionsDir];
114
- if (fs.existsSync(userClaudeDir) && path.resolve(userClaudeDir) !== path.resolve(minionsDir)) {
115
- addDirArgs.push('--add-dir', userClaudeDir);
116
- }
246
+ // Register exit handler to clean up orphaned temp files
247
+ function _cleanupSpawnTempFiles() {
248
+ if (wantsSystemPromptFile) {
249
+ try { fs.unlinkSync(sysTmpPath); } catch { /* may already be cleaned */ }
250
+ }
251
+ }
252
+ process.on('exit', _cleanupSpawnTempFiles);
253
+ process.on('SIGTERM', () => { _cleanupSpawnTempFiles(); process.exit(143); });
117
254
 
118
- // When resuming a session, skip system prompt (it's baked into the session)
119
- const isResume = extraArgs.includes('--resume');
120
- const sysTmpPath = sysPromptFile + '.tmp';
121
- let cliArgs;
122
- if (isResume) {
123
- cliArgs = ['-p', ...addDirArgs, ...extraArgs];
124
- } else {
125
- // Pass system prompt via file to avoid ENAMETOOLONG on Windows (32KB arg limit)
126
- fs.writeFileSync(sysTmpPath, sysPrompt);
127
- cliArgs = ['-p', '--system-prompt-file', sysTmpPath, ...addDirArgs, ...extraArgs];
128
- }
255
+ // Capture stderr separately for debugging
256
+ let stderrBuf = '';
257
+ proc.stderr.on('data', (chunk) => {
258
+ stderrBuf += chunk.toString();
259
+ process.stderr.write(chunk);
260
+ });
129
261
 
130
- if (!claudeBin) {
131
- const msg = 'FATAL: Cannot find Claude Code CLI — install from https://claude.ai/download or: npm install -g @anthropic-ai/claude-code';
132
- fs.appendFileSync(debugPath, msg + '\n');
133
- console.error(msg);
134
- process.exit(78); // 78 = configuration error (distinct from runtime failures)
135
- }
262
+ // Pipe stdout to parent
263
+ proc.stdout.pipe(process.stdout);
136
264
 
137
- // Save binary path cache on first resolution (subsequent spawns use fast path)
138
- let actualArgs = cliArgs;
139
- if (!_cacheHit) {
140
- try { safeWrite(capsCachePath, { claudeBin, claudeIsNative }); } catch {}
141
- }
265
+ // MCP startup timeout: kill if no stdout within 3 minutes
266
+ const MCP_STARTUP_TIMEOUT = 180000; // 3 minutes
267
+ let gotFirstOutput = false;
268
+ const startupTimer = setTimeout(() => {
269
+ if (!gotFirstOutput) {
270
+ const msg = `TIMEOUT: ${runtimeName} CLI produced no output after ${MCP_STARTUP_TIMEOUT / 1000}s (likely MCP server startup stall)`;
271
+ console.error(msg);
272
+ fs.appendFileSync(debugPath, msg + '\n');
273
+ killGracefully(proc);
274
+ }
275
+ }, MCP_STARTUP_TIMEOUT);
276
+ proc.stdout.once('data', () => { gotFirstOutput = true; clearTimeout(startupTimer); });
142
277
 
143
- const proc = claudeIsNative
144
- ? runFile(claudeBin, actualArgs, { stdio: ['pipe', 'pipe', 'pipe'], env })
145
- : runFile(process.execPath, [claudeBin, ...actualArgs], { stdio: ['pipe', 'pipe', 'pipe'], env });
146
-
147
- fs.appendFile(debugPath, `PID=${proc.pid || 'none'}\nargs=${actualArgs.join(' ').slice(0, 500)}\n`, () => {});
148
-
149
- // Write PID file for parent engine to verify spawn (async — engine checks after 5s)
150
- const pidFile = promptFile.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid');
151
- fs.writeFile(pidFile, String(proc.pid || ''), () => {});
152
-
153
- // Send prompt via stdin
154
- try {
155
- proc.stdin.write(prompt);
156
- proc.stdin.end();
157
- } catch (err) {
158
- console.error(`FATAL: stdin write failed (broken pipe): ${err.message}`);
159
- fs.appendFileSync(debugPath, `STDIN ERROR: ${err.message}\n`);
160
- killImmediate(proc);
161
- process.exit(1);
278
+ proc.on('close', (code) => {
279
+ clearTimeout(startupTimer);
280
+ // Write process-exit sentinel to stdout so the engine can detect completion (#716).
281
+ try { process.stdout.write(`\n[process-exit] code=${code}\n`); } catch { /* stdout may be closed */ }
282
+ fs.appendFileSync(debugPath, `EXIT: code=${code}\nSTDERR: ${stderrBuf.slice(0, 500)}\n`);
283
+ process.exit(code || 0);
284
+ });
285
+ proc.on('error', (err) => {
286
+ fs.appendFileSync(debugPath, `ERROR: ${err.message}\n`);
287
+ process.exit(1);
288
+ });
162
289
  }
163
290
 
164
- // Clean up temp file (only created for non-resume sessions)
165
- if (!isResume) setTimeout(() => { try { fs.unlinkSync(sysTmpPath); } catch { /* cleanup */ } }, 5000);
291
+ module.exports = { parseSpawnArgs, buildSpawnInvocation };
166
292
 
167
- // Register exit handler to clean up orphaned temp files (system prompt tmp)
168
- function _cleanupSpawnTempFiles() {
169
- try { fs.unlinkSync(sysTmpPath); } catch { /* may already be cleaned */ }
170
- }
171
- process.on('exit', _cleanupSpawnTempFiles);
172
- process.on('SIGTERM', () => { _cleanupSpawnTempFiles(); process.exit(143); });
173
-
174
- // Capture stderr separately for debugging
175
- let stderrBuf = '';
176
- proc.stderr.on('data', (chunk) => {
177
- stderrBuf += chunk.toString();
178
- process.stderr.write(chunk);
179
- });
180
-
181
- // Pipe stdout to parent
182
- proc.stdout.pipe(process.stdout);
183
-
184
- // MCP startup timeout: kill if no stdout within 3 minutes (MCP servers downloading/starting)
185
- const MCP_STARTUP_TIMEOUT = 180000; // 3 minutes
186
- let gotFirstOutput = false;
187
- const startupTimer = setTimeout(() => {
188
- if (!gotFirstOutput) {
189
- const msg = `TIMEOUT: Claude CLI produced no output after ${MCP_STARTUP_TIMEOUT / 1000}s (likely MCP server startup stall)`;
190
- console.error(msg);
191
- fs.appendFileSync(debugPath, msg + '\n');
192
- killGracefully(proc);
193
- }
194
- }, MCP_STARTUP_TIMEOUT);
195
- proc.stdout.once('data', () => { gotFirstOutput = true; clearTimeout(startupTimer); });
196
-
197
- proc.on('close', (code) => {
198
- clearTimeout(startupTimer);
199
- // Write process-exit sentinel to stdout so the engine can detect completion (#716).
200
- // This is a backup for cases where Claude CLI crashes without writing a result line.
201
- // process.stdout.write is synchronous for pipes, so it will be captured by the parent.
202
- try { process.stdout.write(`\n[process-exit] code=${code}\n`); } catch { /* stdout may be closed */ }
203
- fs.appendFileSync(debugPath, `EXIT: code=${code}\nSTDERR: ${stderrBuf.slice(0, 500)}\n`);
204
- process.exit(code || 0);
205
- });
206
- proc.on('error', (err) => {
207
- fs.appendFileSync(debugPath, `ERROR: ${err.message}\n`);
208
- process.exit(1);
209
- });
293
+ if (require.main === module) main();