@scp-dev/cli 0.0.0-stage → 0.1.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.
@@ -0,0 +1,948 @@
1
+ import {
2
+ agyRules,
3
+ agySettingsFile,
4
+ mcpFile,
5
+ mountedEntry
6
+ } from "./chunk-N3ZROLFA.js";
7
+ import {
8
+ OPTIONS,
9
+ RelayUsage,
10
+ SPAWN_OPTIONS,
11
+ grantParams,
12
+ localClient,
13
+ open,
14
+ selectorList,
15
+ walkNow
16
+ } from "./chunk-7KP6TAJW.js";
17
+ import {
18
+ steerPrompt
19
+ } from "./chunk-XWLF7ON2.js";
20
+ import {
21
+ SELF,
22
+ detectHarness,
23
+ note,
24
+ spawnerKey
25
+ } from "./chunk-DA3EB2YF.js";
26
+
27
+ // src/spawn.ts
28
+ import { spawn, spawnSync } from "child_process";
29
+ import { closeSync, existsSync, constants as files, fstatSync, openSync, readFileSync, readSync, realpathSync, writeSync } from "fs";
30
+ import { constants, homedir } from "os";
31
+ import { basename, dirname, isAbsolute, join, resolve, sep } from "path";
32
+ import { parseArgs } from "util";
33
+ import {
34
+ CLI_NAME,
35
+ PEEK,
36
+ findSecret,
37
+ HANDBACK_CUT,
38
+ handbackOutput,
39
+ keepLast,
40
+ LIMITS,
41
+ redactSecrets,
42
+ steerNote,
43
+ stripControl
44
+ } from "@scp-dev/core";
45
+ var SERVER = "scp";
46
+ var SPAWN_RUNNER = "SCP_SPAWN_RUNNER";
47
+ var SPAWNER_IDENTITY = ["SCP_NAME", "SCP_SESSION", "SCP_GRANT", "SCP_PARENT", "SCP_HARNESS", SPAWN_RUNNER];
48
+ var SHIM_ENV = ["SCP_GRANT", "SCP_NAME", "SCP_LABELS", "SCP_DIR", "XDG_STATE_HOME"];
49
+ function spawnEnvironment(parent, grant, labels) {
50
+ const env = {};
51
+ for (const [key, value] of Object.entries(parent)) if (value !== void 0 && !SPAWNER_IDENTITY.includes(key)) env[key] = value;
52
+ return { ...env, ...grant, ...labels !== void 0 ? { SCP_LABELS: labels } : {} };
53
+ }
54
+ function codexMount(env) {
55
+ try {
56
+ return mountedEntry("codex", mcpFile("codex", env));
57
+ } catch {
58
+ return void 0;
59
+ }
60
+ }
61
+ function toml(value) {
62
+ if (typeof value === "string") return JSON.stringify(value).replaceAll("\x7F", "\\u007f");
63
+ if (Array.isArray(value)) return `[${value.map(toml).join(", ")}]`;
64
+ if (value && typeof value === "object")
65
+ return `{ ${Object.entries(value).map(([k, v]) => `${JSON.stringify(k)} = ${toml(v)}`).join(", ")} }`;
66
+ return JSON.stringify(value);
67
+ }
68
+ function codexOverrides(env, mount, self) {
69
+ const kept = Array.isArray(mount?.env_vars) ? mount.env_vars : [];
70
+ const names = SHIM_ENV.filter((name) => env[name] !== void 0 && !kept.includes(name));
71
+ const key = (field) => `mcp_servers.${SERVER}.${field}`;
72
+ const set = (field, value) => ["-c", `${key(field)}=${toml(value)}`];
73
+ return [
74
+ ...mount ? [] : [...set("command", self[0]), ...set("args", [...self.slice(1), "mcp", "--harness", "codex"])],
75
+ ...set("env_vars", [...kept, ...names]),
76
+ ...set("enabled", true),
77
+ ...mount && "default_tools_approval_mode" in mount ? [] : set("default_tools_approval_mode", "approve")
78
+ ];
79
+ }
80
+ var CODEX_VALUED = /* @__PURE__ */ new Set([
81
+ "-c",
82
+ "--config",
83
+ "--enable",
84
+ "--disable",
85
+ "--remote",
86
+ "--remote-auth-token-env",
87
+ "-i",
88
+ "--image",
89
+ "-m",
90
+ "--model",
91
+ "--local-provider",
92
+ "-p",
93
+ "--profile",
94
+ "-s",
95
+ "--sandbox",
96
+ "-C",
97
+ "--cd",
98
+ "--add-dir",
99
+ "-a",
100
+ "--ask-for-approval"
101
+ ]);
102
+ var CODEX_SESSIONS = /* @__PURE__ */ new Set(["exec", "e", "review", "resume", "fork"]);
103
+ function codexOverrideAt(args) {
104
+ let i = 0;
105
+ while (i < args.length && args[i].startsWith("-") && args[i] !== "--") i += CODEX_VALUED.has(args[i]) ? 2 : 1;
106
+ if (i >= args.length || !CODEX_SESSIONS.has(args[i])) return [0];
107
+ const at = [0, i + 1];
108
+ if (args[i] === "exec" || args[i] === "e") {
109
+ let j = i + 1;
110
+ while (j < args.length && args[j].startsWith("-") && args[j] !== "--") j += CODEX_EXEC_VALUED.has(args[j]) ? 2 : 1;
111
+ if (args[j] === "resume" || args[j] === "review") at.push(j + 1);
112
+ }
113
+ return at;
114
+ }
115
+ var ENV_VALUED = /* @__PURE__ */ new Set(["-u", "--unset", "-C", "--chdir", "-S", "--split-string"]);
116
+ var TIMEOUT_VALUED = /* @__PURE__ */ new Set(["-s", "--signal", "-k", "--kill-after"]);
117
+ function programAt(command) {
118
+ let i = 0;
119
+ const skipFlags = (valued) => {
120
+ while (i < command.length && command[i].startsWith("-") && command[i] !== "--") i += valued.has(command[i]) ? 2 : 1;
121
+ if (command[i] === "--") i++;
122
+ };
123
+ while (i < command.length) {
124
+ const word = command[i];
125
+ const name = basename(word);
126
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(word)) i++;
127
+ else if (name === "env") {
128
+ i++;
129
+ skipFlags(ENV_VALUED);
130
+ } else if (name === "nohup") i++;
131
+ else if (name === "nice") {
132
+ i++;
133
+ skipFlags(/* @__PURE__ */ new Set(["-n", "--adjustment"]));
134
+ } else if (name === "timeout") {
135
+ i++;
136
+ skipFlags(TIMEOUT_VALUED);
137
+ i++;
138
+ } else break;
139
+ }
140
+ return i;
141
+ }
142
+ var MESSAGING = [
143
+ "relay_send",
144
+ "relay_ask",
145
+ "relay_ask_wait",
146
+ "relay_reply",
147
+ "relay_inbox",
148
+ "relay_list",
149
+ "relay_peers",
150
+ "relay_whoami",
151
+ "relay_set_status",
152
+ "relay_set_now",
153
+ "relay_set_title"
154
+ ].map((tool) => `mcp__${SERVER}__${tool}`);
155
+ var PRESETS = {
156
+ claude: {
157
+ headless: ["-p", "--print"],
158
+ own: ["--allowedTools", "--allowed-tools", "--dangerously-skip-permissions", "--permission-mode"],
159
+ allow: ["--allowedTools", ...MESSAGING]
160
+ },
161
+ grok: {
162
+ headless: ["-p", "--single", "--prompt-file", "--prompt-json"],
163
+ own: ["--allow", "--allowedTools", "--allowed-tools", "--always-approve", "--permission-mode"],
164
+ allow: MESSAGING.flatMap((tool) => ["--allow", tool])
165
+ }
166
+ };
167
+ var MUSE_VALUED = /* @__PURE__ */ new Set([
168
+ "--agents",
169
+ "--provider",
170
+ "--preset",
171
+ "--model",
172
+ "--contributor-mode",
173
+ "-c",
174
+ "--config",
175
+ "--reasoning-effort",
176
+ "--effort",
177
+ "--base-url",
178
+ "--image",
179
+ "--workspace",
180
+ "--worktree-base",
181
+ "--worktree-existing",
182
+ "--approval-mode",
183
+ "--permission-profile",
184
+ "--approval-judge",
185
+ "--sandbox-network",
186
+ "--echo-delay-ms"
187
+ ]);
188
+ var MUSE_MODE = "--contributor-mode";
189
+ var MUSE_OPTIONAL = { "-w": /* @__PURE__ */ new Set(["off", "create", "existing"]), "--worktree": /* @__PURE__ */ new Set(["off", "create", "existing"]) };
190
+ var MUSE_CONFIG = /* @__PURE__ */ new Set(["-c", "--config"]);
191
+ var MUSE_MODES_ALLOWED = /* @__PURE__ */ new Set(["off", "on"]);
192
+ function museOptionsEnd(args, start = 0) {
193
+ let i = start;
194
+ while (i < args.length && args[i].startsWith("-") && args[i] !== "--") {
195
+ const word = args[i];
196
+ i += word.includes("=") ? 1 : MUSE_VALUED.has(word) || MUSE_OPTIONAL[word]?.has(args[i + 1] ?? "") ? 2 : 1;
197
+ }
198
+ return i;
199
+ }
200
+ function museModeAt(args) {
201
+ const i = museOptionsEnd(args);
202
+ return args[i] === "exec" ? i + 1 : 0;
203
+ }
204
+ function configMode(value) {
205
+ try {
206
+ const parsed = JSON.parse(value);
207
+ return parsed === false ? "off" : parsed === true ? "on" : typeof parsed === "string" ? parsed : value;
208
+ } catch {
209
+ return value;
210
+ }
211
+ }
212
+ function museModes(args) {
213
+ const end = args.indexOf("--");
214
+ const words = end < 0 ? args : args.slice(0, end);
215
+ const modes = [];
216
+ for (const [i, word] of words.entries()) {
217
+ if (word === MUSE_MODE) modes.push(words[i + 1]);
218
+ else if (word.startsWith(`${MUSE_MODE}=`)) modes.push(word.slice(MUSE_MODE.length + 1));
219
+ const eq = word.indexOf("=");
220
+ const setting = MUSE_CONFIG.has(word) ? words[i + 1] : eq > 0 && MUSE_CONFIG.has(word.slice(0, eq)) ? word.slice(eq + 1) : void 0;
221
+ const split = setting?.indexOf("=") ?? -1;
222
+ if (setting !== void 0 && split > 0 && /contributor/i.test(setting.slice(0, split))) modes.push(configMode(setting.slice(split + 1)));
223
+ }
224
+ return modes;
225
+ }
226
+ function contributorRefusal(command) {
227
+ const at = programAt(command);
228
+ if (command[at] === void 0 || basename(command[at]) !== "muse") return null;
229
+ for (const mode of museModes(command.slice(at + 1))) {
230
+ if (mode === void 0 || !MUSE_MODES_ALLOWED.has(mode))
231
+ return `${CLI_NAME} spawn: muse runs with ${MUSE_MODE} off or on, not ${mode === void 0 ? "a bare flag" : JSON.stringify(mode)}`;
232
+ }
233
+ return null;
234
+ }
235
+ function hasFlag(args, flags) {
236
+ const end = args.indexOf("--");
237
+ return (end < 0 ? args : args.slice(0, end)).some((arg) => flags.some((flag) => arg === flag || flag.startsWith("--") && arg.startsWith(`${flag}=`)));
238
+ }
239
+ var SKIP_GIT = "--skip-git-repo-check";
240
+ var PROBE_MS = 3e3;
241
+ var probes = /* @__PURE__ */ new Map();
242
+ function codexSkipsGitCheck(program, env) {
243
+ let takes = probes.get(program);
244
+ if (takes === void 0) {
245
+ const help = spawnSync(program, ["exec", "--help"], { env, encoding: "utf8", timeout: PROBE_MS, stdio: ["ignore", "pipe", "ignore"] });
246
+ takes = help.status === 0 && help.stdout.includes(SKIP_GIT);
247
+ probes.set(program, takes);
248
+ }
249
+ return takes;
250
+ }
251
+ function inGit(dir) {
252
+ for (let at = dir; ; at = dirname(at)) {
253
+ if (existsSync(join(at, ".git"))) return true;
254
+ if (dirname(at) === at) return false;
255
+ }
256
+ }
257
+ function codexExec(args) {
258
+ let cd;
259
+ const options = (from, valued) => {
260
+ let i2 = from;
261
+ while (i2 < args.length && args[i2].startsWith("-") && args[i2] !== "--" && args[i2] !== "-") {
262
+ const word2 = args[i2];
263
+ if (word2 === "-C" || word2 === "--cd") cd = args[i2 + 1];
264
+ else if (word2.startsWith("--cd=")) cd = word2.slice(5);
265
+ i2 += valued.has(word2) ? 2 : 1;
266
+ }
267
+ return i2;
268
+ };
269
+ const exec = options(0, CODEX_VALUED);
270
+ if (args[exec] !== "exec" && args[exec] !== "e") return void 0;
271
+ let i = options(exec + 1, CODEX_EXEC_VALUED);
272
+ const word = args[i];
273
+ const sub = word === "resume" || word === "review" ? { name: word, at: i } : void 0;
274
+ const operands = [];
275
+ for (i = sub ? i + 1 : i; i < args.length; i++) {
276
+ i = options(i, CODEX_EXEC_VALUED);
277
+ if (i >= args.length) break;
278
+ if (args[i] === "--") {
279
+ operands.push(...args.slice(i + 1));
280
+ break;
281
+ }
282
+ operands.push(args[i]);
283
+ }
284
+ return { exec, ...sub ? { sub } : {}, operands, ...cd !== void 0 ? { cd } : {} };
285
+ }
286
+ function closedStdin(command) {
287
+ const at = programAt(command);
288
+ if (command[at] === void 0 || basename(command[at]) !== "codex") return false;
289
+ const args = command.slice(at + 1);
290
+ const run = codexExec(args);
291
+ if (!run || run.operands.includes("-")) return false;
292
+ if (run.sub?.name === "resume") return run.operands.length >= (hasFlag(args, ["--last"]) ? 1 : 2);
293
+ return run.sub !== void 0 || run.operands.length > 0;
294
+ }
295
+ var STDIN_WAIT_MS = 300;
296
+ async function harnessStdin(waitMs) {
297
+ if (process.stdin.isTTY === true) return "ignore";
298
+ let stat;
299
+ try {
300
+ stat = fstatSync(0);
301
+ } catch {
302
+ return "ignore";
303
+ }
304
+ if (stat.isFile()) return stat.size > 0 ? "inherit" : "ignore";
305
+ if (!stat.isFIFO() && !stat.isSocket()) return "ignore";
306
+ const input = process.stdin;
307
+ return new Promise((resolve2) => {
308
+ const settle = (result) => {
309
+ clearTimeout(timer);
310
+ input.off("data", data);
311
+ input.off("end", ended);
312
+ input.off("error", ended);
313
+ input.pause();
314
+ resolve2(result);
315
+ };
316
+ const data = (chunk) => settle({ first: chunk, rest: input });
317
+ const ended = () => settle("ignore");
318
+ const timer = waitMs === void 0 ? void 0 : setTimeout(ended, waitMs);
319
+ input.on("data", data);
320
+ input.once("end", ended);
321
+ input.once("error", ended);
322
+ });
323
+ }
324
+ function harnessArgv(command, env, self = SELF, mount = codexMount, cwd = process.cwd(), probe = codexSkipsGitCheck) {
325
+ const at = programAt(command);
326
+ const program = command[at];
327
+ const name = program === void 0 ? void 0 : basename(program);
328
+ const args = command.slice(at + 1);
329
+ const preset = name === void 0 ? void 0 : PRESETS[name];
330
+ if (preset) {
331
+ if (!hasFlag(args, preset.headless) || hasFlag(args, preset.own)) return command;
332
+ const end = args.indexOf("--");
333
+ return end < 0 ? [...command, ...preset.allow] : [...command.slice(0, at + 1 + end), ...preset.allow, ...command.slice(at + 1 + end)];
334
+ }
335
+ if (name === "muse") {
336
+ if (museModes(args).length) return command;
337
+ const mode = at + 1 + museModeAt(args);
338
+ return [...command.slice(0, mode), MUSE_MODE, "off", ...command.slice(mode)];
339
+ }
340
+ if (name !== "codex") return command;
341
+ const run = codexExec(args);
342
+ const skip = run && run.sub?.name !== "review" && !hasFlag(args, [SKIP_GIT]) && !inGit(resolve(cwd, run.cd ?? ".")) && probe(program, env);
343
+ const after = run && (run.sub?.at ?? run.exec) + 1;
344
+ const words = skip && after ? [...args.slice(0, after), SKIP_GIT, ...args.slice(after)] : args;
345
+ const overrides = codexOverrides(env, mount(env), self);
346
+ const points = codexOverrideAt(words);
347
+ return [
348
+ ...command.slice(0, at + 1),
349
+ ...words.flatMap((arg, i) => points.includes(i) ? [...overrides, arg] : [arg]),
350
+ ...points.includes(words.length) ? overrides : []
351
+ ];
352
+ }
353
+ var CLAUDE_VALUED = /* @__PURE__ */ new Set([
354
+ "--model",
355
+ "--fallback-model",
356
+ "--output-format",
357
+ "--input-format",
358
+ "--permission-mode",
359
+ "--settings",
360
+ "--setting-sources",
361
+ "--mcp-config",
362
+ "--allowedTools",
363
+ "--allowed-tools",
364
+ "--disallowedTools",
365
+ "--disallowed-tools",
366
+ "--tools",
367
+ "--add-dir",
368
+ "--append-system-prompt",
369
+ "--system-prompt",
370
+ "--session-id",
371
+ "--resume",
372
+ "-r",
373
+ "--max-turns",
374
+ "--agents",
375
+ "--agent",
376
+ "--plugin-dir",
377
+ "--permission-prompt-tool",
378
+ "--effort",
379
+ "--betas"
380
+ ]);
381
+ var CODEX_EXEC_VALUED = /* @__PURE__ */ new Set([...CODEX_VALUED, "--output-schema", "--color", "-o", "--output-last-message"]);
382
+ function promptAt(command) {
383
+ const at = programAt(command);
384
+ const name = command[at] === void 0 ? void 0 : basename(command[at]);
385
+ const args = command.slice(at + 1);
386
+ const end = args.indexOf("--");
387
+ if (name === "claude") {
388
+ if (!hasFlag(args, PRESETS.claude.headless)) return void 0;
389
+ if (end >= 0) return end + 1 < args.length ? { at: at + args.length, from: at + end + 2 } : void 0;
390
+ for (let i2 = args.length - 1; i2 >= 0; i2--)
391
+ if (!args[i2].startsWith("-") && !CLAUDE_VALUED.has(args[i2 - 1] ?? "")) return args[i2] ? { at: at + 1 + i2 } : void 0;
392
+ return void 0;
393
+ }
394
+ if (name !== "codex") return void 0;
395
+ let i = 0;
396
+ while (i < args.length && args[i].startsWith("-")) i += CODEX_VALUED.has(args[i]) ? 2 : 1;
397
+ if (args[i] !== "exec" && args[i] !== "e") return void 0;
398
+ for (i++; i < args.length; i++) {
399
+ const word = args[i];
400
+ if (word === "--") return args[i + 1] === "-" || !args[i + 1] ? void 0 : { at: at + 2 + i };
401
+ if (word.startsWith("-")) {
402
+ if (CODEX_EXEC_VALUED.has(word)) i++;
403
+ continue;
404
+ }
405
+ return word === "-" || word === "resume" || word === "review" || !word ? void 0 : { at: at + 1 + i };
406
+ }
407
+ return void 0;
408
+ }
409
+ var RESUME_TAKES = /* @__PURE__ */ new Set([
410
+ "-c",
411
+ "--config",
412
+ "--enable",
413
+ "--disable",
414
+ "--strict-config",
415
+ "-m",
416
+ "--model",
417
+ "--dangerously-bypass-approvals-and-sandbox",
418
+ "--dangerously-bypass-hook-trust",
419
+ "--skip-git-repo-check",
420
+ "--ignore-user-config",
421
+ "--ignore-rules",
422
+ "--output-schema",
423
+ "--json",
424
+ "-o",
425
+ "--output-last-message"
426
+ ]);
427
+ var RESUME_DROPS = /* @__PURE__ */ new Set(["-i", "--image", "--worktree", "--thread-source"]);
428
+ function resumeArgv(command, session, prompt) {
429
+ const found = promptAt(command);
430
+ const at = programAt(command);
431
+ if (!found || command[at] === void 0 || basename(command[at]) !== "codex") return void 0;
432
+ let exec = at + 1;
433
+ while (exec < command.length && command[exec].startsWith("-")) exec += CODEX_VALUED.has(command[exec]) ? 2 : 1;
434
+ const before = [];
435
+ const after = [];
436
+ for (let i = exec + 1; i < command.length; i++) {
437
+ const word = command[i];
438
+ if (i === found.at || word === "--") continue;
439
+ const eq = word.startsWith("--") ? word.indexOf("=") : -1;
440
+ const flag = eq > 0 ? word.slice(0, eq) : word;
441
+ const valued = eq < 0 && CODEX_EXEC_VALUED.has(flag);
442
+ const value = eq > 0 ? word.slice(eq + 1) : valued ? command[i + 1] : void 0;
443
+ const words = valued ? command.slice(i, i + 2) : [word];
444
+ if (valued) i++;
445
+ if (flag === "--ephemeral") return void 0;
446
+ if (flag === "-s" || flag === "--sandbox") after.push("-c", `sandbox_mode=${JSON.stringify(value ?? "")}`);
447
+ else if (!RESUME_DROPS.has(flag)) (RESUME_TAKES.has(flag) ? after : before).push(...words);
448
+ }
449
+ return [...command.slice(0, exec + 1), ...before, "resume", ...after, session, prompt];
450
+ }
451
+ function spawnPrompt(command) {
452
+ const found = promptAt(command);
453
+ return found ? command.slice(found.from ?? found.at, found.at + 1).join(" ") || void 0 : void 0;
454
+ }
455
+ var STEER_KEY = /^[0-9a-z]{6}$/;
456
+ function steerArgv(command, steer) {
457
+ const found = promptAt(command);
458
+ if (!found || typeof steer?.key !== "string" || !STEER_KEY.test(steer.key) || typeof steer.parent !== "string" || !/^[^\s\]]+$/.test(steer.parent))
459
+ return command;
460
+ return command.map((word, i) => i === found.at ? `${word}${steerNote(steer.parent, steer.key)}` : word);
461
+ }
462
+ var TITLE_CHARS = 64;
463
+ function spawnTitle(command) {
464
+ const first = spawnPrompt(command)?.split("\n").map((text) => stripControl(text).replace(/\s+/g, " ").trim()).find(Boolean);
465
+ if (!first) return void 0;
466
+ const line = redactSecrets(first);
467
+ const end = line.length > TITLE_CHARS && /[\ud800-\udbff]/.test(line[TITLE_CHARS - 1]) ? TITLE_CHARS - 1 : TITLE_CHARS;
468
+ const title = line.slice(0, end).trim();
469
+ return findSecret(title) ? void 0 : title;
470
+ }
471
+ var NAME_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
472
+ function nameSuffix() {
473
+ return Array.from(crypto.getRandomValues(new Uint8Array(5)), (byte) => NAME_ALPHABET[byte % NAME_ALPHABET.length]).join("");
474
+ }
475
+ function workerName(command, random = nameSuffix) {
476
+ const program = command[programAt(command)];
477
+ const harness = (program === void 0 ? "" : basename(program)).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, "").slice(0, 57);
478
+ return `${harness || "worker"}-${random()}`;
479
+ }
480
+ function agyNote(command, env) {
481
+ const at = programAt(command);
482
+ const args = command.slice(at + 1);
483
+ if (command[at] === void 0 || basename(command[at]) !== "agy" || !hasFlag(args, ["-p", "--print", "--prompt"]) || hasFlag(args, ["--dangerously-skip-permissions"]))
484
+ return null;
485
+ let allow;
486
+ try {
487
+ allow = JSON.parse(readFileSync(agySettingsFile(env), "utf8"))?.permissions?.allow;
488
+ } catch {
489
+ }
490
+ if (Array.isArray(allow) && agyRules().every((rule) => allow.includes(rule))) return null;
491
+ return `${CLI_NAME} spawn: headless agy denies the relay tools without the permission rules ${CLI_NAME} setup writes to ${agySettingsFile(env)}; run ${CLI_NAME} setup`;
492
+ }
493
+ var FORWARDED = ["SIGINT", "SIGQUIT", "SIGTERM", "SIGHUP"];
494
+ var TAIL_BYTES = 16 * 1024;
495
+ var DRAIN_MS = 2e3;
496
+ function tee(from, to) {
497
+ let tail = Buffer.alloc(0);
498
+ let cut = false;
499
+ from.on("data", (chunk) => {
500
+ tail = Buffer.concat([tail, chunk]);
501
+ if (tail.length > TAIL_BYTES) {
502
+ tail = tail.subarray(tail.length - TAIL_BYTES);
503
+ cut = true;
504
+ }
505
+ if (!to.write(chunk)) {
506
+ from.pause();
507
+ to.once("drain", () => from.resume());
508
+ }
509
+ });
510
+ return { bytes: () => new Uint8Array(tail), cut: () => cut };
511
+ }
512
+ function runHarness(argv, env, options = {}) {
513
+ const capture = options.capture ?? process.stdout.isTTY !== true;
514
+ const captureErr = options.captureErr ?? process.stderr.isTTY !== true;
515
+ const out = options.out ?? process.stdout;
516
+ return new Promise((resolve2) => {
517
+ const tty = process.stdin.isTTY === true;
518
+ const group = options.group === true && !tty && process.stdout.isTTY !== true && process.stderr.isTTY !== true;
519
+ const input = options.stdin ?? "inherit";
520
+ const piped = typeof input === "object" ? input : void 0;
521
+ const child = spawn(argv[0], argv.slice(1), {
522
+ stdio: [typeof input === "object" ? "pipe" : input, capture ? "pipe" : "inherit", captureErr ? "pipe" : "inherit"],
523
+ env,
524
+ detached: group
525
+ });
526
+ if (piped && child.stdin) {
527
+ child.stdin.on("error", () => {
528
+ });
529
+ child.stdin.write(piped.first);
530
+ piped.rest.pipe(child.stdin);
531
+ }
532
+ const pipes = [child.stdout, child.stderr].filter((pipe) => pipe !== null);
533
+ const stdout = child.stdout ? tee(child.stdout, out) : void 0;
534
+ const stderr = child.stderr ? tee(child.stderr, options.err ?? process.stderr) : void 0;
535
+ let open2 = pipes.length;
536
+ let settled = false;
537
+ let started = false;
538
+ const drained = [];
539
+ for (const pipe of pipes) {
540
+ pipe.once("close", () => {
541
+ if (--open2 === 0) for (const fn of drained.splice(0)) fn();
542
+ });
543
+ }
544
+ const handlers = FORWARDED.map((signal) => {
545
+ const handler = () => {
546
+ if (tty && (signal === "SIGINT" || signal === "SIGQUIT")) return;
547
+ if (child.exitCode !== null || child.signalCode !== null) return;
548
+ try {
549
+ if (group && child.pid) process.kill(-child.pid, signal);
550
+ else child.kill(signal);
551
+ } catch {
552
+ }
553
+ };
554
+ process.on(signal, handler);
555
+ return [signal, handler];
556
+ });
557
+ const done = async (code) => {
558
+ if (settled) return;
559
+ settled = true;
560
+ if (open2 > 0) {
561
+ const timer = setTimeout(() => {
562
+ for (const pipe of pipes) pipe.destroy();
563
+ }, DRAIN_MS);
564
+ await new Promise((next) => open2 === 0 ? next() : drained.push(next));
565
+ clearTimeout(timer);
566
+ }
567
+ if (capture) await new Promise((next) => out.write("", () => next()));
568
+ for (const [signal, handler] of handlers) process.off(signal, handler);
569
+ resolve2({
570
+ code,
571
+ tail: stdout?.bytes() ?? new Uint8Array(),
572
+ cut: stdout?.cut() ?? false,
573
+ err: stderr?.bytes() ?? new Uint8Array(),
574
+ errCut: stderr?.cut() ?? false,
575
+ started
576
+ });
577
+ };
578
+ child.once("spawn", () => {
579
+ started = true;
580
+ options.started?.();
581
+ });
582
+ child.once("error", (error) => {
583
+ const failed = {
584
+ message: `${CLI_NAME} spawn: ${argv[0]}: ${error.code === "ENOENT" ? "command not found" : error.message}`,
585
+ code: error.code === "ENOENT" ? 127 : 126
586
+ };
587
+ if (options.started) options.started(failed);
588
+ else note(failed.message);
589
+ void done(failed.code);
590
+ });
591
+ child.once("exit", (code, signal) => void done(code ?? 128 + (signal ? constants.signals[signal] ?? 0 : 0)));
592
+ });
593
+ }
594
+ function harnessOutput(run, bytes) {
595
+ let start = 0;
596
+ if (run.cut) while (start < run.tail.length && (run.tail[start] & 192) === 128) start++;
597
+ return handbackOutput(new TextDecoder().decode(run.tail.subarray(start)), run.cut, bytes);
598
+ }
599
+ var EARLIER_STDERR = "[earlier stderr cut]";
600
+ function stderrOutput(run, bytes = LIMITS.stderrBytes) {
601
+ let start = 0;
602
+ if (run.errCut) while (start < run.err.length && (run.err[start] & 192) === 128) start++;
603
+ const text = new TextDecoder().decode(run.err.subarray(start));
604
+ const clean = handbackOutput(run.errCut ? text.slice(text.indexOf("\n") + 1) : text, false, Number.MAX_SAFE_INTEGER);
605
+ const encoded = new TextEncoder().encode(clean);
606
+ if (encoded.length <= bytes) return run.errCut && clean ? `${EARLIER_STDERR}
607
+ ${clean}` : clean;
608
+ let from = encoded.length - bytes;
609
+ const line = encoded.indexOf(10, from - 1);
610
+ if (line >= 0 && line + 1 < encoded.length) from = line + 1;
611
+ else while (from < encoded.length && (encoded[from] & 192) === 128) from++;
612
+ return `${EARLIER_STDERR}
613
+ ${new TextDecoder().decode(encoded.subarray(from))}`;
614
+ }
615
+ var TRANSCRIPT_BYTES = 4 * 1024 * 1024;
616
+ var LIMIT_CUT = "[reply cut at the model's output limit]";
617
+ function prompted(entry, content) {
618
+ if (entry.isMeta === true || entry.isCompactSummary === true || entry.toolUseResult !== void 0) return false;
619
+ const blocks = Array.isArray(content) ? content : [content];
620
+ if (blocks.some((block) => block?.type === "tool_result")) return false;
621
+ const texts = blocks.map((block) => typeof block === "string" ? block : block?.type === "text" ? block.text : void 0);
622
+ return texts.some((text) => typeof text === "string" && text.trim() !== "" && !text.startsWith("Stop hook feedback"));
623
+ }
624
+ function claudeTranscripts(env) {
625
+ return join(env.CLAUDE_CONFIG_DIR || join(env.HOME || homedir(), ".claude"), "projects");
626
+ }
627
+ function transcriptReplies(path, session, root, bytes = TRANSCRIPT_BYTES) {
628
+ if (!isAbsolute(path) || basename(path) !== `${session}.jsonl`) return [];
629
+ try {
630
+ if (!realpathSync(path).startsWith(realpathSync(root) + sep)) return [];
631
+ } catch {
632
+ return [];
633
+ }
634
+ let text;
635
+ let start;
636
+ let fd;
637
+ try {
638
+ fd = openSync(path, files.O_RDONLY | files.O_NOFOLLOW | files.O_NONBLOCK);
639
+ const stat = fstatSync(fd);
640
+ if (!stat.isFile()) return [];
641
+ start = Math.max(0, stat.size - bytes);
642
+ const buffer = Buffer.alloc(stat.size - start);
643
+ text = buffer.subarray(0, readSync(fd, buffer, 0, buffer.length, start)).toString("utf8");
644
+ } catch {
645
+ return [];
646
+ } finally {
647
+ if (fd !== void 0) closeSync(fd);
648
+ }
649
+ let replies = start > 0 ? [HANDBACK_CUT] : [];
650
+ for (const line of text.split("\n").slice(start > 0 ? 1 : 0)) {
651
+ let entry;
652
+ try {
653
+ entry = JSON.parse(line);
654
+ } catch {
655
+ continue;
656
+ }
657
+ if (!entry || typeof entry !== "object" || entry.isSidechain === true || entry.sessionId !== session) continue;
658
+ const message = entry.message;
659
+ const content = message && typeof message === "object" ? message.content : void 0;
660
+ if (entry.type === "user" && prompted(entry, content)) replies = [];
661
+ if (entry.type !== "assistant") continue;
662
+ const before = replies.length;
663
+ for (const block of Array.isArray(content) ? content : [content]) {
664
+ const reply = typeof block === "string" ? block : block?.type === "text" ? block.text : void 0;
665
+ if (typeof reply === "string" && reply.trim()) replies.push(reply);
666
+ }
667
+ if (replies.length > before && message.stop_reason === "max_tokens") replies.push(`${replies.pop()}
668
+ ${LIMIT_CUT}`);
669
+ }
670
+ return replies;
671
+ }
672
+ function repliesOutput(replies, bytes = LIMITS.handbackBytes) {
673
+ const lines = (text) => text.split("\n").map((line) => line.trim()).filter(Boolean);
674
+ const all = replies.map((reply) => handbackOutput(reply.replace(/\r\n?/g, "\n"), false, Number.MAX_SAFE_INTEGER)).filter(Boolean);
675
+ const restated = (text, later) => lines(text).length >= 2 && lines(text).every((line) => lines(later).includes(line));
676
+ const clean = all.filter((text, i) => text === HANDBACK_CUT || !all.slice(i + 1).some((later) => restated(text, later)));
677
+ if (clean.every((text) => text === HANDBACK_CUT)) return "";
678
+ const size = (text) => new TextEncoder().encode(text).length;
679
+ const framed = (from) => {
680
+ const text = clean.slice(from).join("\n\n");
681
+ return from > 0 ? `${HANDBACK_CUT}
682
+
683
+ ${text}` : text;
684
+ };
685
+ let fit;
686
+ for (let from = clean.length - 1; from >= 0 && size(framed(from)) <= bytes; from--) fit = framed(from);
687
+ if (fit !== void 0) return fit;
688
+ return keepLast(clean.at(-1), bytes, clean.length > 1);
689
+ }
690
+ function splitSpawn(args) {
691
+ let i = 0;
692
+ while (i < args.length && args[i].startsWith("-") && args[i] !== "--") {
693
+ const flag = args[i];
694
+ const option = flag.startsWith("--") && !flag.includes("=") ? OPTIONS[flag.slice(2)] : void 0;
695
+ i += option?.type === "string" ? 2 : 1;
696
+ }
697
+ return { flags: args.slice(0, i), command: args.slice(args[i] === "--" ? i + 1 : i) };
698
+ }
699
+ function spawnDefaults(values, command, random, spawner) {
700
+ const scoped = ["scope", "rules", "in", "out", "allow"].some((key) => values[key] !== void 0);
701
+ const under = typeof values.under === "string" ? values.under : void 0;
702
+ const reach = [
703
+ ...spawner && under && under.replace(/^me\//, "") !== spawner ? [`ep:${spawner}`] : [],
704
+ ...(selectorList(values.group) ?? []).map((group) => `group:${group}`)
705
+ ];
706
+ const title = values.title === void 0 ? spawnTitle(command) : void 0;
707
+ return {
708
+ ...values,
709
+ ...scoped ? {} : { scope: "parent", ...reach.length ? { allow: reach } : {} },
710
+ ...title ? { title } : {},
711
+ ...values.name === void 0 ? { name: workerName(command, random) } : {}
712
+ };
713
+ }
714
+ function handbackBytes(value) {
715
+ const bytes = /^\d+$/.test(value) ? Number(value) : 0;
716
+ if (bytes < 1 || bytes > LIMITS.handbackBytes) throw new RelayUsage(`--handback-bytes takes whole bytes, 1 to ${LIMITS.handbackBytes}`);
717
+ return bytes;
718
+ }
719
+ var STEER_RESUMES = 2;
720
+ async function spawnCommand(args, cwd) {
721
+ const parsed = parseSpawn(args);
722
+ let values = parsed.values;
723
+ const { command } = parsed;
724
+ const bytes = values["handback-bytes"] === void 0 ? void 0 : handbackBytes(values["handback-bytes"]);
725
+ const runner = process.env[SPAWN_RUNNER];
726
+ delete process.env[SPAWN_RUNNER];
727
+ if (runner === void 0 && (values.bg === true || values.fg !== true && (process.stdout.isTTY !== true || process.stdin.isTTY !== true && agentShell(process.env))))
728
+ return detachSpawn(args, cwd);
729
+ const channel = runner === void 0 ? void 0 : handshakeChannel();
730
+ const handshake = channel === void 0 ? void 0 : once((reply) => {
731
+ try {
732
+ writeSync(channel, `${JSON.stringify(reply)}
733
+ `);
734
+ closeSync(channel);
735
+ } catch {
736
+ }
737
+ });
738
+ const bound = spawnerKey();
739
+ const prompt = values.peek === void 0 ? void 0 : spawnPrompt(command)?.slice(0, PEEK.rawChars);
740
+ const walk = runner === void 0 ? walkNow() : JSON.parse(runner);
741
+ let grant;
742
+ let steer;
743
+ try {
744
+ const opened = await open("spawn", values, cwd, false, walk);
745
+ const { me } = opened;
746
+ const client = localClient(opened.client, opened.broker, "spawn");
747
+ try {
748
+ values = spawnDefaults(values, command, void 0, me?.kind === "agent" && me.address.startsWith("me/") ? me.address.slice(3) : void 0);
749
+ const params = { ttl_s: SPAWN_GRANT_TTL_S, ...grantParams(values), ...bound ? { bind_process: bound } : {}, spawn: true, ...prompt ? { prompt } : {} };
750
+ const granted = await client.grant(params);
751
+ grant = granted.env;
752
+ steer = granted.steer;
753
+ } finally {
754
+ client.close();
755
+ }
756
+ } catch (error) {
757
+ handshake?.({ error: error instanceof Error ? error.message : String(error), ...error instanceof RelayUsage ? { usage: true } : {} });
758
+ throw error;
759
+ }
760
+ const name = values.name;
761
+ const env = spawnEnvironment(process.env, grant, values.labels);
762
+ const agy = agyNote(command, process.env);
763
+ if (agy) note(agy);
764
+ const started = handshake && ((failed) => handshake(failed ? { error: failed.message, code: failed.code } : { name, pid: process.pid }));
765
+ const launch = (argv, extra = {}) => runHarness(harnessArgv(argv, env), env, { group: runner !== void 0, ...closedStdin(argv) ? { stdin: "ignore" } : {}, ...extra });
766
+ const first = steerArgv(command, steer);
767
+ const stdin = closedStdin(first) ? await harnessStdin(agentShell(process.env) ? STDIN_WAIT_MS : void 0) : void 0;
768
+ let run = await launch(first, { ...started ? { started } : {}, ...stdin ? { stdin } : {} });
769
+ if (!run.started) {
770
+ await spawnExit(values, cwd, grant.SCP_GRANT, run.code, name, "", walk, [], [], false);
771
+ return run.code;
772
+ }
773
+ const exits = [Date.now()];
774
+ let unapplied = [];
775
+ for (let round = 0; round < STEER_RESUMES && run.code === 0 && resumeArgv(command, "", ""); round++) {
776
+ const pending = await spawnSteers(values, cwd, grant.SCP_GRANT, walk, true);
777
+ if (!pending?.session || !pending.steers.length) break;
778
+ const resumed = await launch(resumeArgv(command, pending.session, steerPrompt(pending.steers)));
779
+ exits.push(Date.now());
780
+ if (resumed.code !== 0) {
781
+ unapplied = pending.steers.map((message) => message.id);
782
+ break;
783
+ }
784
+ run = resumed;
785
+ }
786
+ const worker = await spawnSteers(values, cwd, grant.SCP_GRANT, walk, false);
787
+ const replies = worker?.session && worker.transcript ? repliesOutput(transcriptReplies(worker.transcript, worker.session, claudeTranscripts(process.env)), bytes) : "";
788
+ await spawnExit(values, cwd, grant.SCP_GRANT, run.code, name, replies || harnessOutput(run, bytes), walk, unapplied, exits, true, stderrOutput(run));
789
+ return run.code;
790
+ }
791
+ function parseSpawn(args) {
792
+ const split = splitSpawn(args);
793
+ let values;
794
+ try {
795
+ values = parseArgs({ args: split.flags, options: OPTIONS, strict: true }).values;
796
+ } catch (error) {
797
+ throw new RelayUsage(error instanceof Error ? error.message : String(error));
798
+ }
799
+ if (values.help) return { values, command: split.command };
800
+ for (const key of Object.keys(values)) if (!SPAWN_OPTIONS.includes(key)) throw new RelayUsage(`${CLI_NAME} spawn does not take --${key}`);
801
+ if (values.bg && values.fg) throw new RelayUsage("--bg and --fg: pick one");
802
+ const command = split.command;
803
+ if (!command.length)
804
+ throw new RelayUsage(`${CLI_NAME} spawn runs a harness command: ${CLI_NAME} spawn [flags] [--] <harness cmd...>, e.g. ${CLI_NAME} spawn codex exec "..."`);
805
+ const refusal = contributorRefusal(command);
806
+ if (refusal) throw new RelayUsage(refusal);
807
+ if (values["handback-bytes"] !== void 0) handbackBytes(values["handback-bytes"]);
808
+ return { values, command };
809
+ }
810
+ async function spawnDetached(args, cwd) {
811
+ const { values } = parseSpawn(args);
812
+ if (values.help || values.fg) throw new RelayUsage("a detached spawn takes no --help or --fg");
813
+ const reply = await detachedRunner(args, cwd);
814
+ if ("error" in reply) throw new Error(reply.error);
815
+ return reply;
816
+ }
817
+ function agentShell(env) {
818
+ return detectHarness(env) !== void 0 || ["SCP_NAME", "SCP_GRANT", "SCP_SESSION"].some((key) => env[key]);
819
+ }
820
+ function handshakeChannel() {
821
+ try {
822
+ const fd = openSync("/dev/fd/3", "w");
823
+ closeSync(3);
824
+ return fd;
825
+ } catch {
826
+ return 3;
827
+ }
828
+ }
829
+ function once(fn) {
830
+ let done = false;
831
+ return (...args) => {
832
+ if (done) return;
833
+ done = true;
834
+ fn(...args);
835
+ };
836
+ }
837
+ async function detachSpawn(args, cwd) {
838
+ const reply = await detachedRunner(args, cwd);
839
+ if ("error" in reply) {
840
+ note(reply.error);
841
+ return reply.code;
842
+ }
843
+ console.log(
844
+ `${reply.name} started (pid ${reply.pid}) and runs in the background: its result arrives as a relay message by itself; steer it with ${CLI_NAME} send ${reply.name} <text>`
845
+ );
846
+ return 0;
847
+ }
848
+ async function detachedRunner(args, cwd) {
849
+ const walk = walkNow();
850
+ const child = spawn(SELF[0], [...SELF.slice(1), "spawn", ...args], {
851
+ cwd,
852
+ detached: true,
853
+ stdio: ["ignore", "ignore", "ignore", "pipe"],
854
+ env: { ...process.env, [SPAWN_RUNNER]: JSON.stringify(walk) }
855
+ });
856
+ const channel = child.stdio[3];
857
+ const line = await new Promise((resolve2) => {
858
+ let text = "";
859
+ channel.on("data", (chunk) => {
860
+ text += chunk.toString();
861
+ if (text.includes("\n")) resolve2(text.slice(0, text.indexOf("\n")));
862
+ });
863
+ channel.once("end", () => resolve2(text));
864
+ child.once("error", () => resolve2(""));
865
+ });
866
+ channel.destroy();
867
+ child.unref();
868
+ let reply;
869
+ try {
870
+ reply = JSON.parse(line);
871
+ } catch {
872
+ }
873
+ if (!reply) throw new Error(`${CLI_NAME} spawn: the detached runner ended before the harness started`);
874
+ if ("error" in reply) {
875
+ if (reply.usage) throw new RelayUsage(reply.error);
876
+ if (reply.code === void 0) throw new Error(reply.error);
877
+ return { error: reply.error, code: reply.code };
878
+ }
879
+ return reply;
880
+ }
881
+ async function spawnSteers(values, cwd, token, walk, take) {
882
+ try {
883
+ const opened = await open("spawn", values, cwd, false, walk);
884
+ const client = localClient(opened.client, opened.broker, "spawn");
885
+ try {
886
+ return await client.spawnSteers({ grant: token, take });
887
+ } finally {
888
+ client.close();
889
+ }
890
+ } catch {
891
+ return void 0;
892
+ }
893
+ }
894
+ async function spawnExit(values, cwd, token, code, name, output, walk, unapplied = [], exits = [], started = true, stderr = "") {
895
+ try {
896
+ const opened = await open("spawn", values, cwd, false, walk);
897
+ const client = localClient(opened.client, opened.broker, "spawn");
898
+ try {
899
+ await client.spawnExit({
900
+ grant: token,
901
+ code: Math.min(code, 255),
902
+ ...output ? { output } : {},
903
+ ...stderr ? { stderr } : {},
904
+ ...unapplied.length ? { unapplied } : {},
905
+ ...exits.length ? { exits } : {},
906
+ ...started ? {} : { started: false }
907
+ });
908
+ } finally {
909
+ client.close();
910
+ }
911
+ } catch (error) {
912
+ note(`${CLI_NAME} spawn: could not mark ${name} as exited: ${error instanceof Error ? error.message : String(error)}`);
913
+ }
914
+ }
915
+ var SPAWN_GRANT_TTL_S = 3600;
916
+
917
+ export {
918
+ SPAWN_RUNNER,
919
+ spawnEnvironment,
920
+ codexMount,
921
+ codexOverrides,
922
+ codexOverrideAt,
923
+ programAt,
924
+ MESSAGING,
925
+ contributorRefusal,
926
+ codexSkipsGitCheck,
927
+ closedStdin,
928
+ STDIN_WAIT_MS,
929
+ harnessStdin,
930
+ harnessArgv,
931
+ resumeArgv,
932
+ spawnPrompt,
933
+ steerArgv,
934
+ spawnTitle,
935
+ workerName,
936
+ agyNote,
937
+ runHarness,
938
+ harnessOutput,
939
+ stderrOutput,
940
+ claudeTranscripts,
941
+ transcriptReplies,
942
+ repliesOutput,
943
+ splitSpawn,
944
+ spawnDefaults,
945
+ spawnCommand,
946
+ spawnDetached,
947
+ agentShell
948
+ };