@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.
- package/CHANGELOG.md +10 -0
- package/bin/minions.js +5 -3
- package/dashboard/js/settings.js +216 -22
- package/dashboard.js +136 -8
- package/docs/copilot-cli-schema.md +637 -0
- package/docs/copilot-output-sample-claude.jsonl +72 -0
- package/docs/copilot-output-sample-default.jsonl +26 -0
- package/docs/copilot-output-sample-gpt4o.jsonl +23 -0
- package/engine/cli.js +250 -18
- package/engine/lifecycle.js +14 -9
- package/engine/llm.js +346 -94
- package/engine/model-discovery.js +167 -0
- package/engine/preflight.js +247 -19
- package/engine/runtimes/claude.js +413 -0
- package/engine/runtimes/copilot.js +566 -0
- package/engine/runtimes/index.js +61 -0
- package/engine/shared.js +299 -63
- package/engine/spawn-agent.js +265 -181
- package/engine.js +118 -31
- package/package.json +1 -1
package/engine/spawn-agent.js
CHANGED
|
@@ -1,209 +1,293 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* spawn-agent.js —
|
|
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
|
-
*
|
|
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 {
|
|
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
|
|
51
|
+
const promptFile = args[0];
|
|
52
|
+
const sysPromptFile = args[1];
|
|
53
|
+
let runtimeName = 'claude';
|
|
54
|
+
const opts = {};
|
|
55
|
+
const passthrough = [];
|
|
15
56
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
function
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
//
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
fs.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
fs.
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
165
|
-
if (!isResume) setTimeout(() => { try { fs.unlinkSync(sysTmpPath); } catch { /* cleanup */ } }, 5000);
|
|
291
|
+
module.exports = { parseSpawnArgs, buildSpawnInvocation };
|
|
166
292
|
|
|
167
|
-
|
|
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();
|