@scp-dev/cli 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +1 -0
  2. package/dist/{chunk-GH7AXFUL.js → chunk-25ZVL5LD.js} +8 -10
  3. package/dist/{chunk-RXKBND3T.js → chunk-HF4KP6AR.js} +229 -13
  4. package/dist/{chunk-5DYFYLAQ.js → chunk-KC5AV5NQ.js} +3 -5
  5. package/dist/{chunk-M2DCR2J4.js → chunk-MNTIEWKF.js} +13 -3
  6. package/dist/{chunk-U2RSSTNW.js → chunk-MXQ7B3CQ.js} +3 -5
  7. package/dist/{chunk-WV5FINTW.js → chunk-NJLFHBSS.js} +20 -10
  8. package/dist/chunk-ZLL5EGI2.js +805 -0
  9. package/dist/{claude-YJLGFQ2P.js → claude-WTCPUYU3.js} +21 -11
  10. package/dist/cli.js +1 -1
  11. package/dist/codex-YZZNH6IQ.js +565 -0
  12. package/dist/{daemon-SUTLTY5U.js → daemon-OMJY7T2F.js} +1 -1
  13. package/dist/{hooks-EI7M3YKY.js → hooks-4B5NOH7X.js} +19 -3
  14. package/dist/install-2ZM3IYXG.js +14 -0
  15. package/dist/join-BH5CS6SL.js +45 -0
  16. package/dist/{login-V2WT7QX5.js → login-UB72MU7W.js} +4 -7
  17. package/dist/{main-HPBTO3JK.js → main-I4F53MMA.js} +79 -31
  18. package/dist/{mcp-DGAQNFKO.js → mcp-RXAVQBMP.js} +11 -11
  19. package/dist/{peek-WLG3ZROU.js → peek-M2HQVO4O.js} +2 -3
  20. package/dist/{pi-HMESQA64.js → pi-IAVFVOOO.js} +2 -3
  21. package/dist/{relay-5Z7IEXKD.js → relay-QDX2BNZH.js} +6 -4
  22. package/dist/{remote-FYN5ZXE4.js → remote-2OWBWLJH.js} +3 -5
  23. package/dist/{setup-LXG346FJ.js → setup-HPH7HE6O.js} +5 -6
  24. package/dist/{spawn-DLPMAS5M.js → spawn-V66GYUNC.js} +6 -7
  25. package/dist/{up-GPM4LWY3.js → up-2YGEDA5J.js} +6 -9
  26. package/package.json +7 -5
  27. package/dist/chunk-3RRPUYTE.js +0 -147
  28. package/dist/chunk-3V3DRXB2.js +0 -340
  29. package/dist/chunk-E53YHYEP.js +0 -305
  30. package/dist/install-MYHVFKT4.js +0 -15
package/README.md CHANGED
@@ -11,6 +11,7 @@ npm i -g @scp-dev/cli
11
11
  ```sh
12
12
  swarm up # browser login, links this machine to the org hub, installs hooks and MCP
13
13
  swarm claude --name lead # start a named orchestrator that wakes on peer messages
14
+ swarm codex --name worker # start a session that wakes on peer messages (--headless for an ambient worker)
14
15
  ```
15
16
 
16
17
  From the orchestrator's shell:
@@ -3,28 +3,26 @@ import {
3
3
  agySettingsFile,
4
4
  mcpFile,
5
5
  mountedEntry
6
- } from "./chunk-WV5FINTW.js";
6
+ } from "./chunk-NJLFHBSS.js";
7
+ import {
8
+ steerPrompt
9
+ } from "./chunk-HF4KP6AR.js";
7
10
  import {
8
11
  OPTIONS,
9
12
  RelayUsage,
10
13
  SPAWN_OPTIONS,
14
+ detectHarness,
11
15
  grantParams,
12
16
  localClient,
13
17
  open,
14
18
  selectorList,
19
+ spawnerKey,
15
20
  walkNow
16
- } from "./chunk-3V3DRXB2.js";
17
- import {
18
- steerPrompt
19
- } from "./chunk-RXKBND3T.js";
20
- import {
21
- detectHarness,
22
- spawnerKey
23
- } from "./chunk-E53YHYEP.js";
21
+ } from "./chunk-ZLL5EGI2.js";
24
22
  import {
25
23
  SELF,
26
24
  note
27
- } from "./chunk-M2DCR2J4.js";
25
+ } from "./chunk-MNTIEWKF.js";
28
26
 
29
27
  // src/spawn.ts
30
28
  import { spawn, spawnSync } from "child_process";
@@ -6,12 +6,17 @@ import {
6
6
  import {
7
7
  hookHarness,
8
8
  hookHarnessPid,
9
- relayIdentity
10
- } from "./chunk-E53YHYEP.js";
9
+ membership,
10
+ normalizeHarness,
11
+ processKey,
12
+ psStart,
13
+ relayIdentity,
14
+ sessionKey
15
+ } from "./chunk-ZLL5EGI2.js";
11
16
 
12
17
  // src/hooks.ts
13
18
  import { Readable } from "stream";
14
- import { readFileSync, writeSync } from "fs";
19
+ import { closeSync, fstatSync, openSync, readFileSync, readSync, writeSync } from "fs";
15
20
  import { homedir } from "os";
16
21
  import { basename, join } from "path";
17
22
  import {
@@ -24,6 +29,7 @@ import {
24
29
  redactSecrets,
25
30
  shortId,
26
31
  stripControl,
32
+ TITLE_CHARS,
27
33
  validHandle,
28
34
  wrapMessage
29
35
  } from "@scp-dev/core";
@@ -33,6 +39,8 @@ var HOOK_COMMAND = `${CLI_NAME} hook`;
33
39
  var BLOCK_STOP = "--block-stop-on-unread";
34
40
  var NO_INJECT = "--no-inject";
35
41
  var PUSH_WAIT = "--push-wait";
42
+ var WAKE = "--wake";
43
+ var OPENCODE_WAKE = "opencode-plugin";
36
44
  var PUSH_WAIT_DEFAULT_S = 600;
37
45
  var PUSH_TIMEOUT_MARGIN_S = 30;
38
46
  var PUSH_PRINT_MS = 1e3;
@@ -286,6 +294,57 @@ function claudeVersion(payload, env) {
286
294
  return void 0;
287
295
  }
288
296
  }
297
+ var TITLE_TAIL_BYTES = 128 * 1024;
298
+ var TITLE_LINE = /^\{"type":"(custom-title|ai-title|summary)"/;
299
+ var OPENCODE_UNNAMED = /^(New|Child) session - \d{4}-/;
300
+ function titleText(text) {
301
+ if (!text) return void 0;
302
+ const line = redactSecrets(stripControl(text.slice(0, 4096)).replace(/\s+/g, " ").trim()).slice(0, TITLE_CHARS).trim();
303
+ return line || void 0;
304
+ }
305
+ function promptTitle(prompt) {
306
+ return titleText(prompt?.split("\n").find((line) => line.trim()));
307
+ }
308
+ function transcriptTitle(path, session) {
309
+ if (!path || !session || !path.startsWith("/") || !path.endsWith(`/${session}.jsonl`)) return void 0;
310
+ let fd;
311
+ try {
312
+ fd = openSync(path, "r");
313
+ const size = fstatSync(fd).size;
314
+ const start = Math.max(0, size - TITLE_TAIL_BYTES);
315
+ const buffer = Buffer.alloc(size - start);
316
+ const read = readSync(fd, buffer, 0, buffer.length, start);
317
+ const lines = buffer.subarray(0, read).toString("utf8").split("\n");
318
+ if (start > 0) lines.shift();
319
+ const found = {};
320
+ for (const line of lines) {
321
+ const kind = TITLE_LINE.exec(line)?.[1];
322
+ if (!kind) continue;
323
+ try {
324
+ const record = JSON.parse(line);
325
+ const text = asString(record.customTitle ?? record.aiTitle ?? record.summary);
326
+ if (text) found[kind] = text;
327
+ } catch {
328
+ }
329
+ }
330
+ return titleText(found["custom-title"] ?? found["ai-title"] ?? found.summary);
331
+ } catch {
332
+ return void 0;
333
+ } finally {
334
+ if (fd !== void 0) closeSync(fd);
335
+ }
336
+ }
337
+ function inferredTitle(harness, event, named, payload) {
338
+ if (event.agent) return void 0;
339
+ if (event.kind === "prompt" && named === "userpromptsubmit") {
340
+ const text2 = promptTitle(asString(payload.prompt));
341
+ return text2 ? { text: text2, from: "prompt" } : void 0;
342
+ }
343
+ if (event.kind !== "stop" || named !== "stop") return void 0;
344
+ const own = harness === "opencode" ? asString(payload.session_title) : void 0;
345
+ const text = harness === "claude-code" ? transcriptTitle(asString(field(payload, "transcript_path", "transcriptPath")), asString(field(payload, "session_id", "sessionId"))) : own && !OPENCODE_UNNAMED.test(own) ? titleText(own) : void 0;
346
+ return text ? { text, from: "summary" } : void 0;
347
+ }
289
348
  function hookReply(harness, kind, text, unread = 0, continues = true) {
290
349
  if (kind === "stop" && harness === "claude-code" && continues)
291
350
  return JSON.stringify({ hookSpecificOutput: { hookEventName: "Stop", additionalContext: text } });
@@ -298,6 +357,12 @@ function hookReply(harness, kind, text, unread = 0, continues = true) {
298
357
  return JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext: text } });
299
358
  }
300
359
  var DEFERS = /* @__PURE__ */ new Set(["claude-code", "codex"]);
360
+ var TLDR_EVERY_DEFAULT = 10;
361
+ var TLDR = `SCP: update your tl;dr with \`${CLI_NAME} set-now "..."\` or relay_set_now (what you are doing now).`;
362
+ function tldrEvery(env) {
363
+ const raw = env.SCP_TLDR_EVERY?.trim();
364
+ return raw && /^\d{1,6}$/.test(raw) ? Number(raw) : TLDR_EVERY_DEFAULT;
365
+ }
301
366
  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.`;
302
367
  async function runHook(args, options) {
303
368
  const began = Date.now();
@@ -305,8 +370,10 @@ async function runHook(args, options) {
305
370
  const takes = !args.includes(NO_INJECT);
306
371
  const subagents = args.includes(SUBAGENT);
307
372
  const waits = pushWait(args, options.env);
373
+ const wakeAt = args.indexOf(WAKE);
374
+ const wakeS = wakeAt < 0 ? 0 : Number(args[wakeAt + 1]);
308
375
  const [harnessArg, eventArg] = args.filter(
309
- (arg, i) => arg !== BLOCK_STOP && arg !== NO_INJECT && arg !== SUBAGENT && arg !== PUSH_WAIT && args[i - 1] !== PUSH_WAIT
376
+ (arg, i) => arg !== BLOCK_STOP && arg !== NO_INJECT && arg !== SUBAGENT && arg !== PUSH_WAIT && args[i - 1] !== PUSH_WAIT && arg !== WAKE && args[i - 1] !== WAKE
310
377
  );
311
378
  let payload;
312
379
  try {
@@ -315,18 +382,32 @@ async function runHook(args, options) {
315
382
  } catch {
316
383
  payload = {};
317
384
  }
385
+ if (Number.isFinite(wakeS) && wakeS > 0) return wakeHook(harnessArg, payload, wakeS, options);
318
386
  const event = mapHookPayload(payload, eventArg);
319
387
  if (!event || options.env.SCP_TRANSPORT === "cloud") return;
320
388
  const sub = subagents && (event.kind === "tool" || event.kind === "stop") ? subagentOf(payload) : void 0;
321
389
  if (subagents && !sub) return;
322
390
  if (event.kind === "session_end") options.deadline?.(1e3);
323
391
  if (event.kind === "session_start") options.deadline?.(1500);
392
+ const harness = normalizeHarness(hookHarness(harnessArg, options.env), "hook");
393
+ const pid = (options.harnessPid ?? ((h) => hookHarnessPid(options.env, h)))(harness);
394
+ const started = psStart(pid);
395
+ const joined = membership(
396
+ options.env,
397
+ sessionKey(
398
+ options.env,
399
+ processKey(pid, () => started)
400
+ )
401
+ );
402
+ if ("dark" in joined) return;
324
403
  const claim = relayIdentity({
325
- harness: hookHarness(harnessArg, options.env),
404
+ ...joined.handle ? { as: joined.handle } : {},
405
+ harness,
326
406
  fallbackHarness: "hook",
327
407
  cwd: event.cwd ?? options.cwd,
328
408
  env: options.env,
329
- harnessPid: options.harnessPid ?? ((harness) => hookHarnessPid(options.env, harness))
409
+ harnessPid: () => pid,
410
+ startOf: () => started
330
411
  });
331
412
  if (!validHandle(claim.handle)) return;
332
413
  const named = eventName(asString(field(payload, "hook_event_name", "hookEventName")) ?? eventArg);
@@ -345,7 +426,9 @@ async function runHook(args, options) {
345
426
  const inject = takes && !event.agent && (stop || injectsAt(claim.harness, event.kind)) && (options.remaining?.() ?? Number.POSITIVE_INFINITY) >= INJECT_MIN_MS;
346
427
  try {
347
428
  const posted = [client.hello({ transient: true, claim })];
348
- if (event.kind === "prompt" && named === "userpromptsubmit") posted.push(client.request("prompted", {}));
429
+ const every = tldrEvery(options.env);
430
+ const prompted = event.kind === "prompt" && named === "userpromptsubmit" ? client.request("prompted", every ? { tldr_every: every } : {}) : void 0;
431
+ if (prompted) posted.push(prompted);
349
432
  if (event.kind !== "prompt") {
350
433
  const session = event.agent ? void 0 : asString(field(payload, "session_id", "sessionId"));
351
434
  const transcript = session && claim.harness === "claude-code" ? asString(field(payload, "transcript_path", "transcriptPath")) : void 0;
@@ -363,6 +446,8 @@ async function runHook(args, options) {
363
446
  if (event.status) posted.push(client.request("status", { state: event.status }));
364
447
  const turn = peekTurn(event, named, payload);
365
448
  if (turn) posted.push(client.request("peek_turn", turn));
449
+ const title = inferredTitle(claim.harness, event, named, payload);
450
+ if (title) posted.push(client.request("title_infer", title));
366
451
  const injected = inject ? client.request(
367
452
  "inject",
368
453
  counts ? { count: true, ...stop ? { stop: true, directives: true, ...blockStop ? { hold_unread: true } : {} } : {} } : {}
@@ -372,7 +457,8 @@ async function runHook(args, options) {
372
457
  const untitled = hello?.status === "fulfilled" && hello.value.endpoint?.title === null;
373
458
  return untitled && (claim.harness === "claude-code" || claim.harness === "codex") ? hookReply(claim.harness, event.kind, NUDGE) : void 0;
374
459
  }
375
- if (result.status !== "fulfilled" || !result.value) return;
460
+ const tldr = (await prompted?.catch(() => void 0))?.tldr === true ? TLDR : void 0;
461
+ if (result.status !== "fulfilled" || !result.value) return tldr ? hookReply(claim.harness, event.kind, tldr) : void 0;
376
462
  const continues = !stop || claim.harness !== "claude-code" || stopContinues(claudeVersion(payload, options.env));
377
463
  if (stop && !result.value.block) {
378
464
  if (waits === void 0 || !PUSHES.has(claim.harness) || !(result.value.workers || result.value.worker_unread) || result.value.pushes_left === 0) return;
@@ -384,7 +470,7 @@ async function runHook(args, options) {
384
470
  const nudged = stop ? result.value.nudge ?? [] : [];
385
471
  const intro = !stop && result.value.intro ? introText(result.value.intro, result.value.intro_key) : void 0;
386
472
  const shown = stop || counts ? void 0 : injectionText({ ...result.value, more: await moreNow(client, result.value) }, claim.harness !== "agy");
387
- const text = stop ? [directive?.text, [pointer, nudgeText(nudged, result.value.nudge_more)].filter(Boolean).join("\n")].filter(Boolean).join("\n\n") : [intro, counts ? unreadPointer(result.value.unread, result.value.awaiting) : shown].filter(Boolean).join("\n\n");
473
+ const text = stop ? [directive?.text, [pointer, nudgeText(nudged, result.value.nudge_more)].filter(Boolean).join("\n")].filter(Boolean).join("\n\n") : [intro, counts ? unreadPointer(result.value.unread, result.value.awaiting) : shown, tldr].filter(Boolean).join("\n\n");
388
474
  if (!text || (options.remaining?.() ?? Number.POSITIVE_INFINITY) < CONFIRM_MIN_MS) return;
389
475
  const reply = hookReply(claim.harness, event.kind, text, result.value.unread, continues);
390
476
  const offered = result.value.offered ?? [];
@@ -409,6 +495,45 @@ async function runHook(args, options) {
409
495
  client.close();
410
496
  }
411
497
  }
498
+ async function wakeHook(harnessArg, payload, waitS, options) {
499
+ if (options.env.SCP_TRANSPORT === "cloud" || field(payload, "agent_id", "agentId", "agent_type", "agentType", "subagentType")) return;
500
+ const claim = relayIdentity({
501
+ harness: hookHarness(harnessArg, options.env),
502
+ fallbackHarness: "hook",
503
+ cwd: asString(payload.cwd) ?? options.cwd,
504
+ env: options.env,
505
+ harnessPid: options.harnessPid ?? ((harness) => hookHarnessPid(options.env, harness))
506
+ });
507
+ if (!validHandle(claim.handle)) return;
508
+ const waitMs = Math.min(waitS, LIMITS.waitMaxS) * 1e3;
509
+ options.deadline?.(waitMs + PUSH_PRINT_MS + 1e3);
510
+ const client = await options.connect(false);
511
+ try {
512
+ await client.hello({ transient: true, claim });
513
+ const result = await client.request("inject", { wait_s: waitMs / 1e3 });
514
+ if (!result.offered.length) return;
515
+ const intro = result.intro ? introText(result.intro, result.intro_key) : void 0;
516
+ const shown = injectionText({ ...result, more: await moreNow(client, result) });
517
+ const text = [intro, shown].filter(Boolean).join("\n\n");
518
+ if (!text) return;
519
+ const reply = hookReply(claim.harness, "prompt", text);
520
+ if (options.stopping?.()) return;
521
+ if (options.print?.(reply) ?? true) {
522
+ const read = result.messages.flatMap((message) => message.seq === void 0 ? [] : [message.seq]);
523
+ await client.request("injected", {
524
+ seqs: result.offered,
525
+ hold: result.hold,
526
+ ...read.length ? { read } : {},
527
+ deferred: true,
528
+ ...shown && !result.brief && result.messages.length ? { preamble: true } : {},
529
+ ...intro ? { intro: true } : {}
530
+ });
531
+ }
532
+ return reply;
533
+ } finally {
534
+ client.close();
535
+ }
536
+ }
412
537
  var SUBAGENT_HARNESSES = /* @__PURE__ */ new Set(["claude-code"]);
413
538
  function subagentOf(payload) {
414
539
  const id = asString(field(payload, "agent_id", "agentId"));
@@ -481,6 +606,7 @@ async function relayHookCommand(args, startDaemon) {
481
606
  const started = Date.now();
482
607
  const agy = args[0] === "agy";
483
608
  let printed = false;
609
+ let stopping = false;
484
610
  const print = (text) => {
485
611
  if (printed) return false;
486
612
  printed = true;
@@ -497,6 +623,11 @@ async function relayHookCommand(args, startDaemon) {
497
623
  print(void 0);
498
624
  process.exit(0);
499
625
  };
626
+ const terminate = () => {
627
+ stopping = true;
628
+ if (!printed) quit();
629
+ };
630
+ if (args.includes(WAKE)) process.on("SIGTERM", terminate);
500
631
  let ends = started + (agy ? 900 : 1500);
501
632
  let timer = setTimeout(quit, ends - Date.now());
502
633
  const deadline = (ms) => {
@@ -515,6 +646,7 @@ async function relayHookCommand(args, startDaemon) {
515
646
  deadline,
516
647
  remaining: () => ends - Date.now(),
517
648
  print,
649
+ stopping: () => stopping,
518
650
  connect: (start) => start ? connectBroker(dir, () => startDaemon(dir), 1e3) : connectBroker(dir)
519
651
  })
520
652
  );
@@ -522,6 +654,7 @@ async function relayHookCommand(args, startDaemon) {
522
654
  }
523
655
  print(void 0);
524
656
  clearTimeout(timer);
657
+ if (args.includes(WAKE)) process.off("SIGTERM", terminate);
525
658
  return 0;
526
659
  }
527
660
  function stopArgs(options) {
@@ -626,6 +759,54 @@ const partId = (parts: Record<string, unknown>[]) => {
626
759
  }
627
760
 
628
761
  export const Scp = async ({ client, directory }: { client: any; directory: string }) => {
762
+ const WAKE = process.env.SCP_WAKE !== "0"
763
+ const busy = new Set<string>()
764
+ let target: string | undefined
765
+ let waiter: ReturnType<typeof spawn> | undefined
766
+ let looping = false
767
+ let armed = 0
768
+ const cancel = () => { waiter?.kill("SIGTERM") }
769
+ const wake = async () => {
770
+ if (looping) return
771
+ looping = true
772
+ let version = armed
773
+ try {
774
+ while (WAKE && target && busy.size === 0) {
775
+ const id = target
776
+ const began = Date.now()
777
+ let text: string | undefined
778
+ try {
779
+ const args = ["${CLI_NAME}", "hook", "opencode", "${WAKE}", "50"]
780
+ const child = spawn(args[0]!, args.slice(1), { env: { ...process.env }, stdio: ["pipe", "pipe", "ignore"] })
781
+ waiter = child
782
+ const ended = new Promise<void>((resolve, reject) => {
783
+ child.once("error", reject)
784
+ child.once("close", () => resolve())
785
+ })
786
+ child.stdin!.on("error", () => {})
787
+ child.stdin!.end(JSON.stringify({ hook_event_name: "Wake", session_id: id, cwd: directory }))
788
+ const [output] = await Promise.all([readAll(child.stdout!), ended])
789
+ text = output.trim() ? context(JSON.parse(output) as Reply) : undefined
790
+ } catch {} finally {
791
+ waiter = undefined
792
+ }
793
+ if (text) {
794
+ version = armed
795
+ await client.session.promptAsync({ path: { id }, body: { parts: [{ type: "text", text }] } }).catch(() => {})
796
+ break
797
+ }
798
+ if (Date.now() - began < 2000) await new Promise((resolve) => setTimeout(resolve, 5000))
799
+ }
800
+ } catch {} finally {
801
+ looping = false
802
+ if (armed !== version) void wake()
803
+ }
804
+ }
805
+ const arm = (id: string) => {
806
+ target = id
807
+ armed++
808
+ void wake()
809
+ }
629
810
  const children = new Map<string, Promise<boolean | undefined>>()
630
811
  const failed = new Set<string>()
631
812
  const isChild = async (id: string) => {
@@ -653,11 +834,14 @@ export const Scp = async ({ client, directory }: { client: any; directory: strin
653
834
  },
654
835
  "chat.message": async (input: { sessionID: string }, output: { message?: { id?: string }; parts: Record<string, unknown>[] }) => {
655
836
  const messageID = output.message?.id
656
- if (!messageID || (await isChild(input.sessionID))) return
837
+ if (await isChild(input.sessionID)) return
838
+ busy.add(input.sessionID)
839
+ cancel()
840
+ if (!messageID) return
657
841
  const text = context(await hook({ hook_event_name: "UserPromptSubmit", session_id: input.sessionID, cwd: directory }, true))
658
842
  if (text) output.parts.push({ id: partId(output.parts), sessionID: input.sessionID, messageID, type: "text", text, synthetic: true })
659
843
  },
660
- event: async ({ event }: { event: { type: string; properties?: { sessionID?: string; info?: { id?: string; parentID?: string; sessionID?: string; role?: string; error?: unknown } } } }) => {
844
+ event: async ({ event }: { event: { type: string; properties?: { sessionID?: string; status?: { type?: string }; info?: { id?: string; parentID?: string; sessionID?: string; role?: string; error?: unknown } } } }) => {
661
845
  const info = event.properties?.info
662
846
  if (event.type === "message.updated") {
663
847
  if (info?.role === "assistant" && info.sessionID && info.error) failed.add(info.sessionID)
@@ -669,15 +853,39 @@ export const Scp = async ({ client, directory }: { client: any; directory: strin
669
853
  if (!sessionID) return
670
854
  if (event.type === "session.error") failed.add(sessionID)
671
855
  if (event.type === "session.deleted") {
856
+ const child = await isChild(sessionID)
672
857
  children.delete(sessionID)
673
858
  failed.delete(sessionID)
859
+ if (!child) {
860
+ busy.delete(sessionID)
861
+ if (target === sessionID) target = undefined
862
+ cancel()
863
+ }
864
+ return
674
865
  }
675
866
  if (event.type === "session.created" && info) children.set(sessionID, Promise.resolve(Boolean(info.parentID)))
676
- if (!name || (await isChild(sessionID))) return
867
+ if (await isChild(sessionID)) return
868
+ if (event.type === "session.status") {
869
+ const status = event.properties?.status?.type
870
+ if (status === "busy" || status === "retry") {
871
+ busy.add(sessionID)
872
+ cancel()
873
+ } else if (status === "idle") busy.delete(sessionID)
874
+ return
875
+ }
876
+ if (event.type === "session.idle") busy.delete(sessionID)
877
+ if (!name) return
677
878
  const stop = name === "Stop" && !failed.has(sessionID)
678
- const reply = await hook({ hook_event_name: name, session_id: sessionID, cwd: directory, ...(name === "SessionStart" ? { source: "startup" } : {}) }, stop, name !== "Stop" || stop, stop && BLOCK_STOP)
879
+ const title = name === "Stop" ? await client.session.get({ path: { id: sessionID } }).then((found: any) => found?.data?.title).catch(() => undefined) : undefined
880
+ const reply = await hook(
881
+ { hook_event_name: name, session_id: sessionID, cwd: directory, ...(name === "SessionStart" ? { source: "startup" } : {}), ...(typeof title === "string" ? { session_title: title } : {}) },
882
+ stop,
883
+ name !== "Stop" || stop,
884
+ stop && BLOCK_STOP,
885
+ )
679
886
  const text = reply.decision === "block" && typeof reply.reason === "string" && !reply.reason.includes("<relay-message") ? reply.reason : undefined
680
887
  if (text) await client.session.promptAsync({ path: { id: sessionID }, body: { parts: [{ type: "text", text }] } }).catch(() => {})
888
+ else arm(sessionID)
681
889
  },
682
890
  }
683
891
  }
@@ -727,6 +935,8 @@ export {
727
935
  BLOCK_STOP,
728
936
  NO_INJECT,
729
937
  PUSH_WAIT,
938
+ WAKE,
939
+ OPENCODE_WAKE,
730
940
  PUSH_WAIT_DEFAULT_S,
731
941
  AGY_UNVERIFIED,
732
942
  summarize,
@@ -743,7 +953,13 @@ export {
743
953
  nudgeText,
744
954
  claudeVersionFile,
745
955
  stopContinues,
956
+ titleText,
957
+ promptTitle,
958
+ transcriptTitle,
746
959
  hookReply,
960
+ TLDR_EVERY_DEFAULT,
961
+ TLDR,
962
+ tldrEvery,
747
963
  runHook,
748
964
  subagentIntro,
749
965
  relayHookCommand,
@@ -1,6 +1,3 @@
1
- import {
2
- RelayUsage
3
- } from "./chunk-3V3DRXB2.js";
4
1
  import {
5
2
  AGY_HOOK_SET,
6
3
  AGY_UNVERIFIED,
@@ -13,10 +10,11 @@ import {
13
10
  mergeHooks,
14
11
  musePlugin,
15
12
  opencodePlugin
16
- } from "./chunk-RXKBND3T.js";
13
+ } from "./chunk-HF4KP6AR.js";
17
14
  import {
15
+ RelayUsage,
18
16
  gitToplevel
19
- } from "./chunk-E53YHYEP.js";
17
+ } from "./chunk-ZLL5EGI2.js";
20
18
 
21
19
  // src/install.ts
22
20
  import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
@@ -1,5 +1,6 @@
1
1
  // src/term.ts
2
2
  import { spawnSync } from "child_process";
3
+ import { fstatSync } from "fs";
3
4
  import { CLI_NAME } from "@scp-dev/core";
4
5
  var FAILED = 125;
5
6
  var SELF = [process.execPath, ...process.versions.bun ? ["--no-env-file", "--config=/dev/null"] : [], process.argv[1]];
@@ -11,16 +12,25 @@ function note(message) {
11
12
  process.stderr.write(`${message.startsWith(`${CLI_NAME} `) ? "" : `${CLI_NAME}: `}${message}
12
13
  `);
13
14
  }
15
+ function isSocket(fd) {
16
+ try {
17
+ return fstatSync(fd).isSocket();
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
14
22
  async function exitFlushed(code, waitMs = 1e4) {
15
- const ended = (stream) => new Promise((resolve) => {
23
+ const ended = (stream, fd) => new Promise((resolve) => {
16
24
  try {
17
- stream.end(() => resolve());
25
+ if (!isSocket(fd)) stream.end(() => resolve());
26
+ else if (stream.writableNeedDrain) stream.once("drain", () => resolve());
27
+ else stream.write("", () => resolve());
18
28
  } catch {
19
29
  resolve();
20
30
  }
21
31
  });
22
32
  let timer;
23
- await Promise.race([Promise.all([ended(process.stdout), ended(process.stderr)]), new Promise((resolve) => timer = setTimeout(resolve, waitMs))]);
33
+ await Promise.race([Promise.all([ended(process.stdout, 1), ended(process.stderr, 2)]), new Promise((resolve) => timer = setTimeout(resolve, waitMs))]);
24
34
  clearTimeout(timer);
25
35
  process.exit(code);
26
36
  }
@@ -1,17 +1,15 @@
1
1
  import {
2
+ RelayUsage,
2
3
  activeRelay,
3
4
  checkRelayUrl,
4
5
  configDir,
5
6
  relayCredential,
6
7
  writePrivateJson
7
- } from "./chunk-3RRPUYTE.js";
8
- import {
9
- RelayUsage
10
- } from "./chunk-3V3DRXB2.js";
8
+ } from "./chunk-ZLL5EGI2.js";
11
9
  import {
12
10
  note,
13
11
  printTable
14
- } from "./chunk-M2DCR2J4.js";
12
+ } from "./chunk-MNTIEWKF.js";
15
13
 
16
14
  // src/remote.ts
17
15
  import { existsSync, readFileSync, rmSync } from "fs";
@@ -1,9 +1,6 @@
1
1
  import {
2
2
  hooksTarget
3
- } from "./chunk-5DYFYLAQ.js";
4
- import {
5
- RelayUsage
6
- } from "./chunk-3V3DRXB2.js";
3
+ } from "./chunk-KC5AV5NQ.js";
7
4
  import {
8
5
  AGY_HOOK_SET,
9
6
  BLOCK_STOP,
@@ -15,10 +12,12 @@ import {
15
12
  mergeHooks,
16
13
  musePlugin,
17
14
  opencodePlugin
18
- } from "./chunk-RXKBND3T.js";
15
+ } from "./chunk-HF4KP6AR.js";
19
16
  import {
20
- gitToplevel
21
- } from "./chunk-E53YHYEP.js";
17
+ RelayUsage,
18
+ gitToplevel,
19
+ setJoinMode
20
+ } from "./chunk-ZLL5EGI2.js";
22
21
 
23
22
  // src/setup.ts
24
23
  import { spawnSync } from "child_process";
@@ -50,7 +49,7 @@ var SETUP_HARNESSES = ["claude", "codex", "opencode", "grok", "muse", "agy"];
50
49
  var SERVER = "scp";
51
50
  var HOOKED = /* @__PURE__ */ new Set(["claude", "codex", "opencode", "agy", "muse"]);
52
51
  var VERIFY_TIMEOUT_MS = 6e4;
53
- var SETUP_HELP = `${CLI_NAME} setup [--write] [--no-hooks] [--block-stop-on-unread] [--undo] [--bin PATH] [--harness H]
52
+ var SETUP_HELP = `${CLI_NAME} setup [--write] [--no-hooks] [--block-stop-on-unread] [--join auto|manual] [--undo] [--bin PATH] [--harness H]
54
53
  mounts the relay MCP shim in every installed harness (claude, codex, opencode, grok, muse, agy) at user scope, running
55
54
  the ${CLI_NAME} PATH finds (by that path) or --bin, and installs the relay hooks at user scope (Muse's as a plugin). Prints
56
55
  the plan; --write applies it (each file backed up next to itself as <file>.bak-<time>, its mode kept), then checks
@@ -60,6 +59,8 @@ var SETUP_HELP = `${CLI_NAME} setup [--write] [--no-hooks] [--block-stop-on-unre
60
59
  messages are unread; without it a hooks file keeps the stop hook it has. ${CLI_NAME} spawn returns at once from an
61
60
  agent's shell; while the workers it started run, a stop waits up to 600 s for their next message or handback (hooks
62
61
  install --push-wait), so results arrive by themselves. --no-hooks mounts the MCP shim only.
62
+ --join auto (the default) puts every hooked session on the relay when it starts; --join manual keeps a session off it
63
+ until it runs ${CLI_NAME} join <name> (or calls relay_join); saved in ~/.config/scp/config.json with --write.
63
64
  --undo removes only what setup added (it keeps a list in ~/.config/scp/setup.json), its hooks too unless --no-hooks.`;
64
65
  var KNOWN = {
65
66
  claude: [".local/bin/claude", ".claude/local/claude"],
@@ -954,6 +955,7 @@ function relaySetup(options, deps) {
954
955
  throw new RelayUsage(`--harness is one of ${SETUP_HARNESSES.join(", ")}`);
955
956
  }
956
957
  if (options.blockStop && (options.hooks === false || options.undo)) throw new RelayUsage(`${BLOCK_STOP} goes with the hooks: not with --no-hooks or --undo`);
958
+ if (options.join !== void 0 && options.undo) throw new RelayUsage("--join sets how sessions join: not with --undo");
957
959
  let added;
958
960
  try {
959
961
  added = readManifest(env);
@@ -976,6 +978,12 @@ function relaySetup(options, deps) {
976
978
  out(`${CLI_NAME} setup${options.undo ? " --undo" : ""}: ${options.write ? "applying" : "dry run, nothing is written (add --write to apply)"}`);
977
979
  let hooks = options.hooks !== false;
978
980
  let unresolved = 0;
981
+ if (options.join !== void 0) {
982
+ if (options.write) setJoinMode(env, options.join);
983
+ out(
984
+ `join: ${options.join}${options.join === "manual" ? ` (a session stays off the relay until it runs ${CLI_NAME} join <name>)` : " (every hooked session joins when it starts)"}${options.write ? "" : ", saved with --write"}`
985
+ );
986
+ }
979
987
  if (options.hooks === false) out(`hooks: ${options.undo ? "kept (--no-hooks)" : "not installed (--no-hooks); only the MCP shim is mounted"}`);
980
988
  if (!options.undo) {
981
989
  if (options.bin !== void 0) out(`mounts run: ${bin} (from --bin; they break if that file moves)`);
@@ -1136,6 +1144,7 @@ function parseSetupArgs(args) {
1136
1144
  "block-stop-on-unread": { type: "boolean" },
1137
1145
  bin: { type: "string" },
1138
1146
  harness: { type: "string" },
1147
+ join: { type: "string" },
1139
1148
  help: { type: "boolean", short: "h" }
1140
1149
  };
1141
1150
  let parsed;
@@ -1149,10 +1158,11 @@ function parseSetupArgs(args) {
1149
1158
  } catch (error) {
1150
1159
  throw new RelayUsage(message(error));
1151
1160
  }
1152
- const { help, hooks, "no-hooks": noHooks, "block-stop-on-unread": blockStop, ...rest } = parsed.values;
1161
+ const { help, hooks, "no-hooks": noHooks, "block-stop-on-unread": blockStop, join: joins, ...rest } = parsed.values;
1153
1162
  if (help) return "help";
1163
+ if (joins !== void 0 && joins !== "auto" && joins !== "manual") throw new RelayUsage("--join takes auto or manual");
1154
1164
  if (hooks && noHooks) throw new RelayUsage("--hooks and --no-hooks contradict each other; the hooks are installed unless --no-hooks");
1155
- return { ...rest, ...noHooks ? { hooks: false } : {}, ...blockStop ? { blockStop } : {} };
1165
+ return { ...rest, ...noHooks ? { hooks: false } : {}, ...blockStop ? { blockStop } : {}, ...joins !== void 0 ? { join: joins } : {} };
1156
1166
  }
1157
1167
  function relaySetupCommand(args, cwd, out) {
1158
1168
  const options = parseSetupArgs(args);