@scp-dev/cli 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,21 +3,25 @@
3
3
  The `swarm` command line tool, the reference binding of SCP for shells, hooks and MCP.
4
4
 
5
5
  ```sh
6
- npm install -g @scp-dev/cli
6
+ npm i -g @scp-dev/cli
7
7
  ```
8
8
 
9
9
  ## Usage
10
10
 
11
11
  ```sh
12
- swarm login
13
- swarm setup --write
14
- swarm spawn --name parser -- claude "Fix the parser tests"
12
+ swarm up # browser login, links this machine to the org hub, installs hooks and MCP
13
+ swarm claude --name lead # start a named orchestrator that wakes on peer messages
14
+ ```
15
+
16
+ From the orchestrator's shell:
17
+
18
+ ```sh
19
+ swarm spawn --name parser -- claude -p "Fix the parser tests"
15
20
  swarm send parser "Also cover the empty-input case."
16
21
  swarm list --tree
17
- swarm inbox --wait 30
18
22
  ```
19
23
 
20
- Headless (CI, cron, cloud runners): `swarm login --token scp_t_...` or `SCP_TOKEN`. `swarm up --token ...` logs in, links the broker to the hub and runs setup in one step. The default relay is https://app.scp.dev; `swarm relay use <url>` picks another. Runs on Node 22.13 or later.
24
+ Headless (CI, cron, cloud runners): `swarm up --token scp_t_... [--machine NAME]`. The default relay is https://app.scp.dev; `swarm relay use <url>` picks another. Runs on Node 22.13 or later.
21
25
 
22
26
  ## Provides
23
27
 
@@ -1,56 +1,6 @@
1
- // src/term.ts
2
- import { spawnSync } from "child_process";
3
- import { CLI_NAME } from "@scp-dev/core";
4
- var FAILED = 125;
5
- var SELF = [process.execPath, ...process.versions.bun ? ["--no-env-file", "--config=/dev/null"] : [], process.argv[1]];
6
- function capture(argv, env) {
7
- const result = spawnSync(argv[0], argv.slice(1), { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], ...env ? { env } : {} });
8
- return result.status === 0 ? result.stdout : void 0;
9
- }
10
- function note(message) {
11
- process.stderr.write(`${message.startsWith(`${CLI_NAME} `) ? "" : `${CLI_NAME}: `}${message}
12
- `);
13
- }
14
- async function exitFlushed(code, waitMs = 1e4) {
15
- const ended = (stream) => new Promise((resolve2) => {
16
- try {
17
- stream.end(() => resolve2());
18
- } catch {
19
- resolve2();
20
- }
21
- });
22
- let timer;
23
- await Promise.race([Promise.all([ended(process.stdout), ended(process.stderr)]), new Promise((resolve2) => timer = setTimeout(resolve2, waitMs))]);
24
- clearTimeout(timer);
25
- process.exit(code);
26
- }
27
- function printTable(rows) {
28
- const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column].length)));
29
- for (const row of rows)
30
- console.log(
31
- row.map((cell, column) => cell.padEnd(widths[column])).join(" ").trimEnd()
32
- );
33
- }
34
- var DURATION_PART = /(\d+(?:\.\d+)?)(ms|s|m|h|d)/g;
35
- var DURATION_UNITS = { ms: 1e-3, s: 1, m: 60, h: 3600, d: 86400 };
36
- function parseSeconds(text, flag) {
37
- const trimmed = text.trim();
38
- let seconds;
39
- if (/^\d+(?:\.\d+)?$/.test(trimmed)) seconds = Number(trimmed);
40
- else {
41
- seconds = 0;
42
- let consumed = "";
43
- for (const match of trimmed.matchAll(DURATION_PART)) {
44
- seconds += Number(match[1]) * DURATION_UNITS[match[2]];
45
- consumed += match[0];
46
- }
47
- if (consumed !== trimmed || consumed === "") {
48
- throw new Error(`${flag} ${JSON.stringify(text)} is not a duration like 90s, 30m or 1h30m`);
49
- }
50
- }
51
- if (!(seconds > 0)) throw new Error(`${flag} must be more than zero`);
52
- return Math.max(1, Math.ceil(seconds));
53
- }
1
+ import {
2
+ capture
3
+ } from "./chunk-M2DCR2J4.js";
54
4
 
55
5
  // src/identity.ts
56
6
  import { existsSync, realpathSync } from "fs";
@@ -337,12 +287,6 @@ function relayIdentity(input) {
337
287
  }
338
288
 
339
289
  export {
340
- FAILED,
341
- SELF,
342
- note,
343
- exitFlushed,
344
- printTable,
345
- parseSeconds,
346
290
  shortHostname,
347
291
  detectHarness,
348
292
  hookHarness,
@@ -353,6 +297,7 @@ export {
353
297
  hiddenHarness,
354
298
  lineage,
355
299
  spawnerKey,
300
+ psParent,
356
301
  harnessPid,
357
302
  hookHarnessPid,
358
303
  relayIdentity
@@ -4,14 +4,14 @@ import {
4
4
  configDir,
5
5
  relayCredential,
6
6
  writePrivateJson
7
- } from "./chunk-4QYUPPGV.js";
7
+ } from "./chunk-BUC5G5PY.js";
8
8
  import {
9
9
  RelayUsage
10
- } from "./chunk-7KP6TAJW.js";
10
+ } from "./chunk-URSBFDR6.js";
11
11
  import {
12
12
  note,
13
13
  printTable
14
- } from "./chunk-DA3EB2YF.js";
14
+ } from "./chunk-M2DCR2J4.js";
15
15
 
16
16
  // src/remote.ts
17
17
  import { existsSync, readFileSync, rmSync } from "fs";
@@ -202,7 +202,7 @@ async function cloudMain(positionals, values, brokerStatus) {
202
202
  writePrivateJson(path, file);
203
203
  if (values.json) json({ file: path, ...file, token: problem ? "missing" : "found" });
204
204
  else {
205
- out(`the relay broker links to ${url} as machine ${machine} the next time it starts (${path})`);
205
+ out(`the relay broker links to ${url} as machine ${machine} (${path})`);
206
206
  if (problem) note(problem);
207
207
  }
208
208
  return 0;
@@ -1,13 +1,9 @@
1
- import {
2
- SPAWN_HELP
3
- } from "./chunk-IE5VI25P.js";
4
1
  import {
5
2
  agyRules,
6
3
  agySettingsFile,
7
4
  mcpFile,
8
5
  mountedEntry
9
- } from "./chunk-O4J5G2X7.js";
10
- import "./chunk-WSLKRAB7.js";
6
+ } from "./chunk-IH2RJRIW.js";
11
7
  import {
12
8
  OPTIONS,
13
9
  RelayUsage,
@@ -17,17 +13,18 @@ import {
17
13
  open,
18
14
  selectorList,
19
15
  walkNow
20
- } from "./chunk-7KP6TAJW.js";
16
+ } from "./chunk-URSBFDR6.js";
21
17
  import {
22
18
  steerPrompt
23
- } from "./chunk-RO7CUOOH.js";
24
- import "./chunk-YYSLYVNZ.js";
19
+ } from "./chunk-ABR5UABL.js";
25
20
  import {
26
- SELF,
27
21
  detectHarness,
28
- note,
29
22
  spawnerKey
30
- } from "./chunk-DA3EB2YF.js";
23
+ } from "./chunk-3PGDKPRN.js";
24
+ import {
25
+ SELF,
26
+ note
27
+ } from "./chunk-M2DCR2J4.js";
31
28
 
32
29
  // src/spawn.ts
33
30
  import { spawn, spawnSync } from "child_process";
@@ -297,6 +294,35 @@ function closedStdin(command) {
297
294
  if (run.sub?.name === "resume") return run.operands.length >= (hasFlag(args, ["--last"]) ? 1 : 2);
298
295
  return run.sub !== void 0 || run.operands.length > 0;
299
296
  }
297
+ var STDIN_WAIT_MS = 300;
298
+ async function harnessStdin(waitMs) {
299
+ if (process.stdin.isTTY === true) return "ignore";
300
+ let stat;
301
+ try {
302
+ stat = fstatSync(0);
303
+ } catch {
304
+ return "ignore";
305
+ }
306
+ if (stat.isFile()) return stat.size > 0 ? "inherit" : "ignore";
307
+ if (!stat.isFIFO() && !stat.isSocket()) return "ignore";
308
+ const input = process.stdin;
309
+ return new Promise((resolve2) => {
310
+ const settle = (result) => {
311
+ clearTimeout(timer);
312
+ input.off("data", data);
313
+ input.off("end", ended);
314
+ input.off("error", ended);
315
+ input.pause();
316
+ resolve2(result);
317
+ };
318
+ const data = (chunk) => settle({ first: chunk, rest: input });
319
+ const ended = () => settle("ignore");
320
+ const timer = waitMs === void 0 ? void 0 : setTimeout(ended, waitMs);
321
+ input.on("data", data);
322
+ input.once("end", ended);
323
+ input.once("error", ended);
324
+ });
325
+ }
300
326
  function harnessArgv(command, env, self = SELF, mount = codexMount, cwd = process.cwd(), probe = codexSkipsGitCheck) {
301
327
  const at = programAt(command);
302
328
  const program = command[at];
@@ -492,11 +518,19 @@ function runHarness(argv, env, options = {}) {
492
518
  return new Promise((resolve2) => {
493
519
  const tty = process.stdin.isTTY === true;
494
520
  const group = options.group === true && !tty && process.stdout.isTTY !== true && process.stderr.isTTY !== true;
521
+ const input = options.stdin ?? "inherit";
522
+ const piped = typeof input === "object" ? input : void 0;
495
523
  const child = spawn(argv[0], argv.slice(1), {
496
- stdio: [options.stdin ?? "inherit", capture ? "pipe" : "inherit", captureErr ? "pipe" : "inherit"],
524
+ stdio: [typeof input === "object" ? "pipe" : input, capture ? "pipe" : "inherit", captureErr ? "pipe" : "inherit"],
497
525
  env,
498
526
  detached: group
499
527
  });
528
+ if (piped && child.stdin) {
529
+ child.stdin.on("error", () => {
530
+ });
531
+ child.stdin.write(piped.first);
532
+ piped.rest.pipe(child.stdin);
533
+ }
500
534
  const pipes = [child.stdout, child.stderr].filter((pipe) => pipe !== null);
501
535
  const stdout = child.stdout ? tee(child.stdout, out) : void 0;
502
536
  const stderr = child.stderr ? tee(child.stderr, options.err ?? process.stderr) : void 0;
@@ -638,7 +672,11 @@ ${LIMIT_CUT}`);
638
672
  return replies;
639
673
  }
640
674
  function repliesOutput(replies, bytes = LIMITS.handbackBytes) {
641
- const clean = replies.map((reply) => handbackOutput(reply.replace(/\r\n?/g, "\n"), false, Number.MAX_SAFE_INTEGER)).filter(Boolean);
675
+ const lines = (text) => text.split("\n").map((line) => line.trim()).filter(Boolean);
676
+ const all = replies.map((reply) => handbackOutput(reply.replace(/\r\n?/g, "\n"), false, Number.MAX_SAFE_INTEGER)).filter(Boolean);
677
+ const restated = (text, later) => lines(text).length >= 2 && lines(text).every((line) => lines(later).includes(line));
678
+ const clean = all.filter((text, i) => text === HANDBACK_CUT || !all.slice(i + 1).some((later) => restated(text, later)));
679
+ if (clean.every((text) => text === HANDBACK_CUT)) return "";
642
680
  const size = (text) => new TextEncoder().encode(text).length;
643
681
  const framed = (from) => {
644
682
  const text = clean.slice(from).join("\n\n");
@@ -648,7 +686,7 @@ ${text}` : text;
648
686
  };
649
687
  let fit;
650
688
  for (let from = clean.length - 1; from >= 0 && size(framed(from)) <= bytes; from--) fit = framed(from);
651
- if (fit !== void 0 || !clean.length) return fit ?? "";
689
+ if (fit !== void 0) return fit;
652
690
  return keepLast(clean.at(-1), bytes, clean.length > 1);
653
691
  }
654
692
  function splitSpawn(args) {
@@ -682,20 +720,9 @@ function handbackBytes(value) {
682
720
  }
683
721
  var STEER_RESUMES = 2;
684
722
  async function spawnCommand(args, cwd) {
685
- const split = splitSpawn(args);
686
- let values;
687
- try {
688
- values = parseArgs({ args: split.flags, options: OPTIONS, strict: true }).values;
689
- } catch (error) {
690
- throw new RelayUsage(error instanceof Error ? error.message : String(error));
691
- }
692
- for (const key of Object.keys(values)) if (!SPAWN_OPTIONS.includes(key)) throw new RelayUsage(`${CLI_NAME} spawn does not take --${key}`);
693
- if (values.bg && values.fg) throw new RelayUsage("--bg and --fg: pick one");
694
- const command = split.command;
695
- if (!command.length)
696
- throw new RelayUsage(`${CLI_NAME} spawn runs a harness command: ${CLI_NAME} spawn [flags] [--] <harness cmd...>, e.g. ${CLI_NAME} spawn codex exec "..."`);
697
- const refusal = contributorRefusal(command);
698
- if (refusal) throw new RelayUsage(refusal);
723
+ const parsed = parseSpawn(args);
724
+ let values = parsed.values;
725
+ const { command } = parsed;
699
726
  const bytes = values["handback-bytes"] === void 0 ? void 0 : handbackBytes(values["handback-bytes"]);
700
727
  const runner = process.env[SPAWN_RUNNER];
701
728
  delete process.env[SPAWN_RUNNER];
@@ -737,8 +764,10 @@ async function spawnCommand(args, cwd) {
737
764
  const agy = agyNote(command, process.env);
738
765
  if (agy) note(agy);
739
766
  const started = handshake && ((failed) => handshake(failed ? { error: failed.message, code: failed.code } : { name, pid: process.pid }));
740
- const launch = (argv, extra = {}) => runHarness(harnessArgv(argv, env), env, { ...extra, group: runner !== void 0, ...closedStdin(argv) ? { stdin: "ignore" } : {} });
741
- let run = await launch(steerArgv(command, steer), started ? { started } : {});
767
+ const launch = (argv, extra = {}) => runHarness(harnessArgv(argv, env), env, { group: runner !== void 0, ...closedStdin(argv) ? { stdin: "ignore" } : {}, ...extra });
768
+ const first = steerArgv(command, steer);
769
+ const stdin = closedStdin(first) ? await harnessStdin(agentShell(process.env) ? STDIN_WAIT_MS : void 0) : void 0;
770
+ let run = await launch(first, { ...started ? { started } : {}, ...stdin ? { stdin } : {} });
742
771
  if (!run.started) {
743
772
  await spawnExit(values, cwd, grant.SCP_GRANT, run.code, name, "", walk, [], [], false);
744
773
  return run.code;
@@ -761,6 +790,32 @@ async function spawnCommand(args, cwd) {
761
790
  await spawnExit(values, cwd, grant.SCP_GRANT, run.code, name, replies || harnessOutput(run, bytes), walk, unapplied, exits, true, stderrOutput(run));
762
791
  return run.code;
763
792
  }
793
+ function parseSpawn(args) {
794
+ const split = splitSpawn(args);
795
+ let values;
796
+ try {
797
+ values = parseArgs({ args: split.flags, options: OPTIONS, strict: true }).values;
798
+ } catch (error) {
799
+ throw new RelayUsage(error instanceof Error ? error.message : String(error));
800
+ }
801
+ if (values.help) return { values, command: split.command };
802
+ for (const key of Object.keys(values)) if (!SPAWN_OPTIONS.includes(key)) throw new RelayUsage(`${CLI_NAME} spawn does not take --${key}`);
803
+ if (values.bg && values.fg) throw new RelayUsage("--bg and --fg: pick one");
804
+ const command = split.command;
805
+ if (!command.length)
806
+ throw new RelayUsage(`${CLI_NAME} spawn runs a harness command: ${CLI_NAME} spawn [flags] [--] <harness cmd...>, e.g. ${CLI_NAME} spawn codex exec "..."`);
807
+ const refusal = contributorRefusal(command);
808
+ if (refusal) throw new RelayUsage(refusal);
809
+ if (values["handback-bytes"] !== void 0) handbackBytes(values["handback-bytes"]);
810
+ return { values, command };
811
+ }
812
+ async function spawnDetached(args, cwd) {
813
+ const { values } = parseSpawn(args);
814
+ if (values.help || values.fg) throw new RelayUsage("a detached spawn takes no --help or --fg");
815
+ const reply = await detachedRunner(args, cwd);
816
+ if ("error" in reply) throw new Error(reply.error);
817
+ return reply;
818
+ }
764
819
  function agentShell(env) {
765
820
  return detectHarness(env) !== void 0 || ["SCP_NAME", "SCP_GRANT", "SCP_SESSION"].some((key) => env[key]);
766
821
  }
@@ -782,6 +837,17 @@ function once(fn) {
782
837
  };
783
838
  }
784
839
  async function detachSpawn(args, cwd) {
840
+ const reply = await detachedRunner(args, cwd);
841
+ if ("error" in reply) {
842
+ note(reply.error);
843
+ return reply.code;
844
+ }
845
+ console.log(
846
+ `${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>`
847
+ );
848
+ return 0;
849
+ }
850
+ async function detachedRunner(args, cwd) {
785
851
  const walk = walkNow();
786
852
  const child = spawn(SELF[0], [...SELF.slice(1), "spawn", ...args], {
787
853
  cwd,
@@ -810,13 +876,9 @@ async function detachSpawn(args, cwd) {
810
876
  if ("error" in reply) {
811
877
  if (reply.usage) throw new RelayUsage(reply.error);
812
878
  if (reply.code === void 0) throw new Error(reply.error);
813
- note(reply.error);
814
- return reply.code;
879
+ return { error: reply.error, code: reply.code };
815
880
  }
816
- console.log(
817
- `${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>`
818
- );
819
- return 0;
881
+ return reply;
820
882
  }
821
883
  async function spawnSteers(values, cwd, token, walk, take) {
822
884
  try {
@@ -853,33 +915,36 @@ async function spawnExit(values, cwd, token, code, name, output, walk, unapplied
853
915
  }
854
916
  }
855
917
  var SPAWN_GRANT_TTL_S = 3600;
918
+
856
919
  export {
857
- MESSAGING,
858
- SPAWN_HELP,
859
920
  SPAWN_RUNNER,
860
- agentShell,
861
- agyNote,
862
- claudeTranscripts,
863
- closedStdin,
921
+ spawnEnvironment,
864
922
  codexMount,
865
- codexOverrideAt,
866
923
  codexOverrides,
867
- codexSkipsGitCheck,
924
+ codexOverrideAt,
925
+ programAt,
926
+ MESSAGING,
868
927
  contributorRefusal,
928
+ codexSkipsGitCheck,
929
+ closedStdin,
930
+ STDIN_WAIT_MS,
931
+ harnessStdin,
869
932
  harnessArgv,
870
- harnessOutput,
871
- programAt,
872
- repliesOutput,
873
933
  resumeArgv,
874
- runHarness,
875
- spawnCommand,
876
- spawnDefaults,
877
- spawnEnvironment,
878
934
  spawnPrompt,
935
+ steerArgv,
879
936
  spawnTitle,
880
- splitSpawn,
937
+ workerName,
938
+ agyNote,
939
+ runHarness,
940
+ harnessOutput,
881
941
  stderrOutput,
882
- steerArgv,
942
+ claudeTranscripts,
883
943
  transcriptReplies,
884
- workerName
944
+ repliesOutput,
945
+ splitSpawn,
946
+ spawnDefaults,
947
+ spawnCommand,
948
+ spawnDetached,
949
+ agentShell
885
950
  };
@@ -7,7 +7,7 @@ import {
7
7
  hookHarness,
8
8
  hookHarnessPid,
9
9
  relayIdentity
10
- } from "./chunk-DA3EB2YF.js";
10
+ } from "./chunk-3PGDKPRN.js";
11
11
 
12
12
  // src/hooks.ts
13
13
  import { Readable } from "stream";
@@ -238,9 +238,16 @@ function pushText(result) {
238
238
  `Push budget used up for this prompt; read the rest with ${CLI_NAME} inbox.${running.length ? ` Workers you spawned still running: ${running.join(", ")}.` : ""}`
239
239
  );
240
240
  } else {
241
- parts.push(
242
- running.length ? `Workers you spawned still running: ${running.join(", ")}; their next message reaches you when you stop.` : "No worker you spawned is still running."
243
- );
241
+ const background = result.background ?? [];
242
+ if (running.length) {
243
+ parts.push(
244
+ `Workers you spawned still running: ${running.join(", ")}. End your turn to wait for them; no reply needed.${background.length ? ` Also running with --no-push: ${background.join(", ")}.` : ""}`
245
+ );
246
+ } else if (background.length) {
247
+ parts.push(
248
+ `No worker your stop waits on is still running; ${background.join(", ")} (--no-push) still ${background.length === 1 ? "runs" : "run"} in the background.`
249
+ );
250
+ } else parts.push("No worker you spawned is still running.");
244
251
  }
245
252
  return parts.join("\n\n");
246
253
  }
@@ -290,7 +297,8 @@ function hookReply(harness, kind, text, unread = 0, continues = true) {
290
297
  const hookEventName = kind === "tool" ? "PostToolUse" : kind === "session_start" ? "SessionStart" : "UserPromptSubmit";
291
298
  return JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext: text } });
292
299
  }
293
- var NUDGE = `${CLI_NAME}: set your title and a one-line now with relay_set_title / relay_set_now so peers can find you.`;
300
+ var DEFERS = /* @__PURE__ */ new Set(["claude-code", "codex"]);
301
+ var NUDGE = `${CLI_NAME} (optional): a title and a one-line now (relay_set_title / relay_set_now) help peers find you; skip it when you only run workers.`;
294
302
  async function runHook(args, options) {
295
303
  const began = Date.now();
296
304
  const blockStop = args.includes(BLOCK_STOP);
@@ -391,7 +399,8 @@ async function runHook(args, options) {
391
399
  ...nudged.length ? { nudged: nudged.map((n) => n.seq) } : {},
392
400
  ...directive ? { restated: directive.restated } : {},
393
401
  ...full ? { preamble: true } : {},
394
- ...intro ? { intro: true } : {}
402
+ ...intro ? { intro: true } : {},
403
+ ...event.kind === "tool" && DEFERS.has(claim.harness) ? { deferred: true } : {}
395
404
  }).catch(() => {
396
405
  });
397
406
  }
@@ -442,7 +451,7 @@ async function subagentHook(client, claim, sub, event, takes, options) {
442
451
  }
443
452
  async function push(client, harness, waitMs, began, prompts, options, continues = true) {
444
453
  const end = Date.now() + waitMs;
445
- if (waitMs > 0) options.deadline?.(end - began + PUSH_PRINT_MS);
454
+ if (waitMs > 0) options.deadline?.(end - began + LIMITS.pushGraceS * 1e3 + PUSH_PRINT_MS);
446
455
  let result;
447
456
  for (; ; ) {
448
457
  result = await client.request("stop_wait", {
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  RelayUsage
3
- } from "./chunk-7KP6TAJW.js";
3
+ } from "./chunk-URSBFDR6.js";
4
4
  import {
5
5
  note
6
- } from "./chunk-DA3EB2YF.js";
6
+ } from "./chunk-M2DCR2J4.js";
7
7
 
8
8
  // src/relay.ts
9
9
  import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
@@ -119,7 +119,7 @@ function relayVerb(positionals, asJson, env = process.env) {
119
119
  }
120
120
  async function loginMain(positionals, values) {
121
121
  if (positionals.length) throw new RelayUsage(`${CLI_NAME} login takes only flags`);
122
- return (await import("./login-JTRUWG6Q.js")).login(values);
122
+ return (await import("./login-5Y3HPSVC.js")).login(values);
123
123
  }
124
124
  function logoutMain(positionals, asJson, env = process.env) {
125
125
  if (positionals.length) throw new RelayUsage(`${CLI_NAME} logout takes no arguments`);
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  hooksTarget
3
- } from "./chunk-WSLKRAB7.js";
3
+ } from "./chunk-QQZF5TQD.js";
4
4
  import {
5
5
  RelayUsage
6
- } from "./chunk-7KP6TAJW.js";
6
+ } from "./chunk-URSBFDR6.js";
7
7
  import {
8
8
  AGY_HOOK_SET,
9
9
  BLOCK_STOP,
@@ -15,10 +15,10 @@ import {
15
15
  mergeHooks,
16
16
  musePlugin,
17
17
  opencodePlugin
18
- } from "./chunk-RO7CUOOH.js";
18
+ } from "./chunk-ABR5UABL.js";
19
19
  import {
20
20
  gitToplevel
21
- } from "./chunk-DA3EB2YF.js";
21
+ } from "./chunk-3PGDKPRN.js";
22
22
 
23
23
  // src/setup.ts
24
24
  import { spawnSync } from "child_process";
@@ -179,7 +179,7 @@ function entryRight(harness, entry, argv) {
179
179
  }
180
180
  var withoutRemote = (entry) => Object.fromEntries(Object.entries(entry).filter(([key]) => !REMOTE_KEYS.includes(key)));
181
181
  function mountArgv(harness, bin) {
182
- return [bin, "mcp", "--harness", harness];
182
+ return [bin, "mcp", "--harness", harness, ...harness === "claude" ? ["--channels"] : []];
183
183
  }
184
184
  function agyRules() {
185
185
  return TOOLS.map((tool) => `mcp(${SERVER}/${tool.name})`);
@@ -1172,6 +1172,7 @@ export {
1172
1172
  agySettingsFile,
1173
1173
  manifestFile,
1174
1174
  mountedEntry,
1175
+ entryArgv,
1175
1176
  mountArgv,
1176
1177
  agyRules,
1177
1178
  verifyMount,
@@ -29,7 +29,7 @@ Steer: ${CLI_NAME} send <name> <text>; a claude -p or codex exec prompt gets a n
29
29
  A codex exec worker that exited before a late steer is resumed with it (codex exec resume, at most twice).
30
30
  Harnesses: codex gets -c overrides that hand it the grant (also behind env, timeout, nohup or nice); a ${CLI_NAME}
31
31
  command from its workspace-write sandbox needs -c sandbox_workspace_write.network_access=true. codex exec
32
- gets --skip-git-repo-check outside a git repo and a closed stdin unless its prompt is - (read from stdin). Workers
32
+ gets --skip-git-repo-check outside a git repo, and with its prompt in argv piped stdin only under --fg. Workers
33
33
  message with the relay MCP tools (relay_send, relay_inbox, relay_reply). claude -p and grok -p get those tools allowed
34
34
  unless you pass a permission flag. muse runs with --contributor-mode off unless you pass on. agy needs the allow rules ${CLI_NAME}
35
35
  setup writes.
@@ -0,0 +1,63 @@
1
+ // src/term.ts
2
+ import { spawnSync } from "child_process";
3
+ import { CLI_NAME } from "@scp-dev/core";
4
+ var FAILED = 125;
5
+ var SELF = [process.execPath, ...process.versions.bun ? ["--no-env-file", "--config=/dev/null"] : [], process.argv[1]];
6
+ function capture(argv, env) {
7
+ const result = spawnSync(argv[0], argv.slice(1), { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], ...env ? { env } : {} });
8
+ return result.status === 0 ? result.stdout : void 0;
9
+ }
10
+ function note(message) {
11
+ process.stderr.write(`${message.startsWith(`${CLI_NAME} `) ? "" : `${CLI_NAME}: `}${message}
12
+ `);
13
+ }
14
+ async function exitFlushed(code, waitMs = 1e4) {
15
+ const ended = (stream) => new Promise((resolve) => {
16
+ try {
17
+ stream.end(() => resolve());
18
+ } catch {
19
+ resolve();
20
+ }
21
+ });
22
+ let timer;
23
+ await Promise.race([Promise.all([ended(process.stdout), ended(process.stderr)]), new Promise((resolve) => timer = setTimeout(resolve, waitMs))]);
24
+ clearTimeout(timer);
25
+ process.exit(code);
26
+ }
27
+ function printTable(rows) {
28
+ const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column].length)));
29
+ for (const row of rows)
30
+ console.log(
31
+ row.map((cell, column) => cell.padEnd(widths[column])).join(" ").trimEnd()
32
+ );
33
+ }
34
+ var DURATION_PART = /(\d+(?:\.\d+)?)(ms|s|m|h|d)/g;
35
+ var DURATION_UNITS = { ms: 1e-3, s: 1, m: 60, h: 3600, d: 86400 };
36
+ function parseSeconds(text, flag) {
37
+ const trimmed = text.trim();
38
+ let seconds;
39
+ if (/^\d+(?:\.\d+)?$/.test(trimmed)) seconds = Number(trimmed);
40
+ else {
41
+ seconds = 0;
42
+ let consumed = "";
43
+ for (const match of trimmed.matchAll(DURATION_PART)) {
44
+ seconds += Number(match[1]) * DURATION_UNITS[match[2]];
45
+ consumed += match[0];
46
+ }
47
+ if (consumed !== trimmed || consumed === "") {
48
+ throw new Error(`${flag} ${JSON.stringify(text)} is not a duration like 90s, 30m or 1h30m`);
49
+ }
50
+ }
51
+ if (!(seconds > 0)) throw new Error(`${flag} must be more than zero`);
52
+ return Math.max(1, Math.ceil(seconds));
53
+ }
54
+
55
+ export {
56
+ FAILED,
57
+ SELF,
58
+ capture,
59
+ note,
60
+ exitFlushed,
61
+ printTable,
62
+ parseSeconds
63
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  RelayUsage
3
- } from "./chunk-7KP6TAJW.js";
3
+ } from "./chunk-URSBFDR6.js";
4
4
  import {
5
5
  AGY_HOOK_SET,
6
6
  AGY_UNVERIFIED,
@@ -13,10 +13,10 @@ import {
13
13
  mergeHooks,
14
14
  musePlugin,
15
15
  opencodePlugin
16
- } from "./chunk-RO7CUOOH.js";
16
+ } from "./chunk-ABR5UABL.js";
17
17
  import {
18
18
  gitToplevel
19
- } from "./chunk-DA3EB2YF.js";
19
+ } from "./chunk-3PGDKPRN.js";
20
20
 
21
21
  // src/install.ts
22
22
  import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
@@ -185,7 +185,7 @@ function mcpInstall(harness, as, out) {
185
185
  const line = (cli, extra = []) => out([...cli, CLI_NAME, ...args(extra)].map(shellWord).join(" "));
186
186
  switch (harness) {
187
187
  case "claude":
188
- line(["claude", "mcp", "add", "scp", "--"]);
188
+ line(["claude", "mcp", "add", "scp", "--"], ["--harness", "claude", "--channels"]);
189
189
  return 0;
190
190
  case "codex":
191
191
  line(["codex", "mcp", "add", "scp", "--"], ["--harness", "codex"]);
@@ -1,12 +1,14 @@
1
1
  import {
2
- SELF,
3
2
  harnessLineage,
4
3
  hiddenHarness,
5
4
  labelsFor,
6
5
  lineage,
7
- parseSeconds,
8
6
  readPsTable
9
- } from "./chunk-DA3EB2YF.js";
7
+ } from "./chunk-3PGDKPRN.js";
8
+ import {
9
+ SELF,
10
+ parseSeconds
11
+ } from "./chunk-M2DCR2J4.js";
10
12
 
11
13
  // src/common.ts
12
14
  import { spawn } from "child_process";
@@ -184,7 +186,7 @@ async function open(verb, values, cwd, bodyFromStdin, walk) {
184
186
  `${CLI_NAME} ${verb} needs an identity: --as HANDLE (or $SCP_NAME) for an agent, or --human for your own inbox me/ (no terminal is attached, so me/ is not assumed)`
185
187
  );
186
188
  }
187
- const { machineName, openCloud } = await import("./remote-3BWFISXJ.js");
189
+ const { machineName, openCloud } = await import("./remote-GC4XN44U.js");
188
190
  const handle2 = named ? localHandle(named) : void 0;
189
191
  const claim = handle2 ? {
190
192
  handle: handle2,
@@ -317,6 +319,7 @@ function stamp(iso) {
317
319
  const at = new Date(iso);
318
320
  return Number.isNaN(at.getTime()) ? iso : `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())} ${time(iso)}`;
319
321
  }
322
+ var CHANNELS_ENV = "SCP_CHANNELS";
320
323
 
321
324
  export {
322
325
  RelayUsage,
@@ -332,5 +335,6 @@ export {
332
335
  peekGrant,
333
336
  grantParams,
334
337
  time,
335
- stamp
338
+ stamp,
339
+ CHANNELS_ENV
336
340
  };