claude-yes 1.83.0 → 1.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- import { t as CLIS_CONFIG } from "./ts-DbdWuoGq.js";
1
+ import { t as CLIS_CONFIG } from "./ts-Bw6gQKyU.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import "./versionChecker-Ct-4UPeG.js";
3
+ import "./versionChecker-CspuhOwO.js";
4
4
  import "./pidStore-C1JXxoPi.js";
5
5
  import "./globalPidIndex-Cr-g75QF.js";
6
6
 
@@ -9,4 +9,4 @@ const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
9
9
 
10
10
  //#endregion
11
11
  export { SUPPORTED_CLIS };
12
- //# sourceMappingURL=SUPPORTED_CLIS-DkXclUge.js.map
12
+ //# sourceMappingURL=SUPPORTED_CLIS-DM0fJTMR.js.map
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { n as logger } from "./logger-B9h0djqx.js";
3
- import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-Ct-4UPeG.js";
3
+ import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-CspuhOwO.js";
4
4
  import { argv } from "process";
5
5
  import { execFileSync, spawn } from "child_process";
6
6
  import ms from "ms";
@@ -42,6 +42,10 @@ function parseCliArgs(argv, supportedClis) {
42
42
  type: "boolean",
43
43
  description: "Prepend SKILL.md header from current directory to the prompt (helpful for non-Claude agents)",
44
44
  default: false
45
+ }).option("swarm-hint", {
46
+ type: "boolean",
47
+ description: "Inject peer discovery hint into agent system prompt when other agents are running (use --no-swarm-hint to opt out)",
48
+ default: true
45
49
  }).option("timeout", {
46
50
  type: "string",
47
51
  description: "Exit after a period of inactivity, e.g., \"5s\" or \"1m\"",
@@ -221,6 +225,7 @@ function parseCliArgs(argv, supportedClis) {
221
225
  verbose: parsedArgv.verbose,
222
226
  resume: parsedArgv.continue,
223
227
  useSkills: parsedArgv.useSkills,
228
+ swarmHint: parsedArgv.swarmHint,
224
229
  appendPrompt: parsedArgv.appendPrompt,
225
230
  useStdinAppend: Boolean(parsedArgv.stdpush || parsedArgv.ipc || parsedArgv.fifo),
226
231
  showVersion: parsedArgv.version,
@@ -475,7 +480,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
475
480
  }
476
481
  }
477
482
  {
478
- const { isSubcommand, runSubcommand } = await import("./subcommands-Vt_yQiEZ.js");
483
+ const { isSubcommand, runSubcommand } = await import("./subcommands-DjO8lthH.js");
479
484
  if (isSubcommand(process.argv[2])) {
480
485
  const code = await runSubcommand(process.argv);
481
486
  process.exit(code ?? 0);
@@ -504,7 +509,7 @@ if (config.useRust) {
504
509
  }
505
510
  }
506
511
  if (rustBinary) {
507
- const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-DkXclUge.js");
512
+ const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-DM0fJTMR.js");
508
513
  const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
509
514
  if (config.verbose) {
510
515
  console.log(`[rust] Using binary: ${rustBinary}`);
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-DbdWuoGq.js";
1
+ import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-Bw6gQKyU.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import "./versionChecker-Ct-4UPeG.js";
3
+ import "./versionChecker-CspuhOwO.js";
4
4
  import "./pidStore-C1JXxoPi.js";
5
5
  import "./globalPidIndex-Cr-g75QF.js";
6
6
 
@@ -6,7 +6,7 @@ import path from "path";
6
6
 
7
7
  //#region ts/subcommands.ts
8
8
  /**
9
- * `cy ls / read / cat / tail / head / send` subcommand implementations.
9
+ * `ay ls / read / cat / tail / head / send` subcommand implementations.
10
10
  *
11
11
  * Mirrors the principles of koho's `terminal-ws-lib.ts` (session list, render
12
12
  * via @xterm/headless, keyword-keyed input) — but file-based instead of via
@@ -61,7 +61,7 @@ async function compactNotes() {
61
61
  /**
62
62
  * Read the per-cwd TS PidStore JSONL and convert to the global record shape,
63
63
  * so pre-existing TS agents that were spawned before the global-index mirror
64
- * shipped still show up in `cy ls`. Merging is done in `mergeRecords`.
64
+ * shipped still show up in `ay ls`. Merging is done in `mergeRecords`.
65
65
  */
66
66
  async function readLocalTsPids(cwd) {
67
67
  const jsonlPath = path.join(cwd, ".agent-yes", "pid-records.jsonl");
@@ -118,6 +118,7 @@ const SUBCOMMANDS = new Set([
118
118
  "ls",
119
119
  "list",
120
120
  "ps",
121
+ "status",
121
122
  "read",
122
123
  "cat",
123
124
  "tail",
@@ -126,6 +127,7 @@ const SUBCOMMANDS = new Set([
126
127
  "restart",
127
128
  "note"
128
129
  ]);
130
+ const IDLE_THRESHOLD_MS = 60 * 1e3;
129
131
  function isSubcommand(name) {
130
132
  return !!name && SUBCOMMANDS.has(name);
131
133
  }
@@ -142,6 +144,7 @@ async function runSubcommand(argv) {
142
144
  case "ls":
143
145
  case "list":
144
146
  case "ps": return await cmdLs(rest);
147
+ case "status": return await cmdStatus(rest);
145
148
  case "read":
146
149
  case "cat": return await cmdRead(rest, { mode: "cat" });
147
150
  case "tail": return await cmdRead(rest, { mode: "tail" });
@@ -153,7 +156,7 @@ async function runSubcommand(argv) {
153
156
  }
154
157
  } catch (err) {
155
158
  const msg = err instanceof Error ? err.message : String(err);
156
- process.stderr.write(`cy ${sub}: ${msg}\n`);
159
+ process.stderr.write(`ay ${sub}: ${msg}\n`);
157
160
  return 1;
158
161
  }
159
162
  }
@@ -173,7 +176,8 @@ function parseArgs(rest) {
173
176
  "active",
174
177
  "follow",
175
178
  "json",
176
- "latest"
179
+ "latest",
180
+ "watch"
177
181
  ].includes(key) || !next || next.startsWith("-")) flags[key] = true;
178
182
  else {
179
183
  flags[key] = next;
@@ -262,7 +266,6 @@ async function cmdLs(rest) {
262
266
  };
263
267
  const fixedWidth = widths.pid + widths.cli + widths.status + widths.age + widths.cwd + 10;
264
268
  const promptBudget = Math.max(20, termWidth - fixedWidth - 1);
265
- const IDLE_THRESHOLD_MS = 60 * 1e3;
266
269
  const notes = await readNotes();
267
270
  const rows = await Promise.all(records.map(async (r) => {
268
271
  let displayStatus;
@@ -277,8 +280,8 @@ async function cmdLs(rest) {
277
280
  if (note) {
278
281
  label = truncate(note, promptBudget);
279
282
  hasNote = true;
280
- } else if (r.log_file && displayStatus !== "stopped") label = truncate(await extractActivity(r.log_file) ?? r.prompt ?? "", promptBudget);
281
- else label = truncate(r.prompt ?? "", promptBudget);
283
+ } else if (r.log_file && displayStatus !== "stopped") label = truncate(await extractActivity(r.log_file) ?? (r.prompt ? `→ ${r.prompt}` : ""), promptBudget);
284
+ else label = truncate(r.prompt ? `→ ${r.prompt}` : "", promptBudget);
282
285
  return {
283
286
  pid: String(r.pid),
284
287
  cli: r.cli,
@@ -312,14 +315,17 @@ async function cmdLs(rest) {
312
315
  const stopped = rows.find((r) => !r._alive);
313
316
  const hints = ["\n"];
314
317
  if (alive) {
315
- hints.push(` cy tail ${alive.pid} # view latest output\n`);
316
- hints.push(` cy tail -f ${alive.pid} # follow live output\n`);
317
- hints.push(` cy send ${alive.pid} "next: ..." # send a prompt\n`);
318
- hints.push(` cy send ${alive.pid} "" --code=ctrl-c # interrupt\n`);
319
- hints.push(` cy note ${alive.pid} "what it's doing" # set a note\n`);
318
+ hints.push(` ay status ${alive.pid} # JSON status snapshot\n`);
319
+ hints.push(` ay status ${alive.pid} --watch # stream changes as JSON\n`);
320
+ hints.push(` ay tail ${alive.pid} # view latest output\n`);
321
+ hints.push(` ay tail -f ${alive.pid} # follow live output\n`);
322
+ hints.push(` ay send ${alive.pid} "next: ..." # send a prompt (keyword: pid, cwd, or prompt substring)\n`);
323
+ hints.push(` ay send ${alive.pid} "" --code=ctrl-c # interrupt\n`);
324
+ hints.push(` ay note ${alive.pid} "what it's doing" # set a note\n`);
325
+ hints.push(` ay ls --json # machine-readable list for scripts/agents\n`);
320
326
  }
321
- if (stopped) hints.push(` cy restart ${stopped.pid} # restart stopped agent\n`);
322
- if (!alive && !stopped) hints.push(` cy ls --all # show exited agents\n`);
327
+ if (stopped) hints.push(` ay restart ${stopped.pid} # restart stopped agent\n`);
328
+ if (!alive && !stopped) hints.push(` ay ls --all # show exited agents\n`);
323
329
  process.stderr.write(hints.join(""));
324
330
  }
325
331
  return 0;
@@ -392,8 +398,8 @@ async function cmdRead(rest, { mode }) {
392
398
  return 0;
393
399
  }
394
400
  process.stderr.write(`
395
- cy ls # list all agents
396
- cy tail -f ${record.pid} # follow live output\n cy send ${record.pid} "next: ..." # send a prompt\n cy send ${record.pid} "" --code=ctrl-c # interrupt\n`);
401
+ ay ls # list all agents
402
+ ay tail -f ${record.pid} # follow live output\n ay send ${record.pid} "next: ..." # send a prompt\n ay send ${record.pid} "" --code=ctrl-c # interrupt\n`);
397
403
  return 0;
398
404
  }
399
405
  /**
@@ -473,15 +479,23 @@ function extractActivityFromLines(lines) {
473
479
  return !s || /^─+$/.test(s) || s.startsWith("? for shortcuts") || /^esc to interrupt/i.test(s) || /\d+%\s*until auto-compact/i.test(s) || /^\/model\s+/i.test(s) || /^⧉\s+In\s+/i.test(s) || /^●\s+(high|medium|low)\s*[·•]/i.test(s) || /^[·•]\s*\d+\s+(left|request)/i.test(s);
474
480
  };
475
481
  const clean = lines.filter((l) => !isChrome(l));
476
- const thinkingLine = clean.find((l) => /^[^\w\s❯>⎿✓✗]\s+[A-Z]\w+[….]/u.test(l.trim()) || /still thinking/i.test(l));
482
+ const isSpinnerLine = (l) => /^[^\w\s❯>⎿✓✗]\s+[A-Z]\w+[….]/u.test(l.trim()) || /still thinking/i.test(l);
483
+ let lastPromptIdx = -1;
484
+ let lastSpinnerIdx = -1;
485
+ for (let i = clean.length - 1; i >= 0; i--) {
486
+ const l = clean[i].trim();
487
+ if (lastPromptIdx === -1 && l.startsWith("❯")) lastPromptIdx = i;
488
+ if (lastSpinnerIdx === -1 && isSpinnerLine(l)) lastSpinnerIdx = i;
489
+ if (lastPromptIdx !== -1 && lastSpinnerIdx !== -1) break;
490
+ }
491
+ if (lastPromptIdx > lastSpinnerIdx) {
492
+ const text = clean[lastPromptIdx].trim().replace(/^❯\s*/, "").trim();
493
+ return text ? `» ${text}` : null;
494
+ }
495
+ const thinkingLine = clean.find((l) => isSpinnerLine(l));
477
496
  if (thinkingLine) {
478
497
  const m = /^.\s+(\w+[^(]*)(?:\s*\(|$)/u.exec(thinkingLine.trim());
479
- return m ? `✳ ${m[1].trim()}` : "thinking…";
480
- }
481
- const promptLines = clean.filter((l) => /^❯\s+/.test(l.trim()));
482
- if (promptLines.length > 0) {
483
- const text = promptLines[promptLines.length - 1].trim().replace(/^❯\s+/, "").trim();
484
- if (text) return `» ${text}`;
498
+ return m?.[1] ? `✳ ${m[1].trim()}` : "thinking…";
485
499
  }
486
500
  const cookIdx = clean.findIndex((l) => /^✻\s+/.test(l.trim()));
487
501
  if (cookIdx >= 0) {
@@ -502,7 +516,7 @@ async function cmdSend(rest) {
502
516
  const opts = commonOpts(flags);
503
517
  const keyword = positional[0];
504
518
  const rawMessage = positional.slice(1).join(" ");
505
- if (!keyword) throw new Error("usage: cy send <keyword> <msg|-> [--code=enter|esc|ctrl-c|ctrl-y|tab|none]");
519
+ if (!keyword) throw new Error("usage: ay send <keyword> <msg|-> [--code=enter|esc|ctrl-c|ctrl-y|tab|none]");
506
520
  const trailing = controlCodeFromName(typeof flags.code === "string" ? flags.code.toLowerCase() : "enter");
507
521
  const record = await resolveOne(keyword, opts);
508
522
  const fifoPath = record.fifo_file;
@@ -513,14 +527,18 @@ async function cmdSend(rest) {
513
527
  for await (const chunk of process.stdin) chunks.push(chunk);
514
528
  body = Buffer.concat(chunks).toString("utf-8").trimEnd();
515
529
  } else body = rawMessage;
516
- if (body && trailing) {
517
- await writeToIpc(fifoPath, body);
530
+ const sourcePid = process.env.AGENT_YES_PID ? Number(process.env.AGENT_YES_PID) : null;
531
+ const talkBack = sourcePid ? `\n(from AGENT_YES_PID=${sourcePid} — reply: ay send ${sourcePid} "...")` : "";
532
+ const fullBody = body + talkBack;
533
+ if (fullBody && trailing) {
534
+ await writeToIpc(fifoPath, fullBody);
518
535
  await new Promise((r) => setTimeout(r, 200));
519
536
  await writeToIpc(fifoPath, trailing);
520
- } else await writeToIpc(fifoPath, body + trailing);
537
+ } else await writeToIpc(fifoPath, fullBody + trailing);
521
538
  const payload = body + trailing;
522
539
  process.stdout.write(`sent to pid ${record.pid} (${record.cli}): ${truncate(payload, 80)}\n`);
523
- process.stderr.write(`\n cy tail ${record.pid} # watch output\n cy ls # list all agents\n`);
540
+ const replyHint = sourcePid ? ` ay send ${sourcePid} "..." # reply to sender\n` : "";
541
+ process.stderr.write(`\n` + replyHint + ` ay tail ${record.pid} # watch output\n ay ls # list all agents\n`);
524
542
  return 0;
525
543
  }
526
544
  function controlCodeFromName(name) {
@@ -584,7 +602,7 @@ async function cmdRestart(rest) {
584
602
  const keyword = positional[0];
585
603
  const record = await resolveOne(keyword, opts);
586
604
  if (isPidAlive(record.pid)) {
587
- process.stderr.write(`pid ${record.pid} is still running — stop it first or use cy send\n`);
605
+ process.stderr.write(`pid ${record.pid} is still running — stop it first or use ay send\n`);
588
606
  return 1;
589
607
  }
590
608
  const args = ["--cli=" + record.cli];
@@ -599,7 +617,7 @@ async function cmdRestart(rest) {
599
617
  ]
600
618
  });
601
619
  process.stdout.write(`restarted ${record.cli} in ${shortenPath(record.cwd)} (new pid: ${proc.pid})\n`);
602
- process.stderr.write(`\n cy tail ${proc.pid} # watch output\n cy ls # list all agents\n`);
620
+ process.stderr.write(`\n ay tail ${proc.pid} # watch output\n ay ls # list all agents\n`);
603
621
  return 0;
604
622
  }
605
623
  async function cmdNote(rest) {
@@ -607,7 +625,7 @@ async function cmdNote(rest) {
607
625
  const opts = commonOpts(flags);
608
626
  const keyword = positional[0];
609
627
  const note = positional.slice(1).join(" ");
610
- if (!keyword) throw new Error("usage: cy note <keyword> [\"note text\"] (omit text to clear)");
628
+ if (!keyword) throw new Error("usage: ay note <keyword> [\"note text\"] (omit text to clear)");
611
629
  const record = await resolveOne(keyword, {
612
630
  ...opts,
613
631
  all: true
@@ -620,10 +638,82 @@ async function cmdNote(rest) {
620
638
  }
621
639
  await writeNote(record.pid, note);
622
640
  process.stdout.write(`note set for pid ${record.pid}: ${note}\n`);
623
- process.stderr.write(`\n cy ls # see updated note in list\n`);
641
+ process.stderr.write(`\n ay ls # see updated note in list\n`);
642
+ return 0;
643
+ }
644
+ async function snapshotStatus(record) {
645
+ const alive = isPidAlive(record.pid);
646
+ let state;
647
+ let logMtimeMs = null;
648
+ if (!alive) state = "stopped";
649
+ else if (record.log_file) {
650
+ logMtimeMs = await stat(record.log_file).then((s) => s.mtimeMs).catch(() => null);
651
+ state = logMtimeMs !== null && Date.now() - logMtimeMs > IDLE_THRESHOLD_MS ? "idle" : "active";
652
+ } else state = "active";
653
+ const activity = state !== "stopped" && record.log_file ? await extractActivity(record.log_file) : null;
654
+ const note = (await readNotes()).get(record.pid) ?? null;
655
+ return {
656
+ pid: record.pid,
657
+ cli: record.cli,
658
+ cwd: record.cwd,
659
+ state,
660
+ activity,
661
+ note,
662
+ log_mtime_ms: logMtimeMs,
663
+ started_at: record.started_at,
664
+ age_ms: Date.now() - record.started_at,
665
+ exit_code: record.exit_code,
666
+ exit_reason: record.exit_reason,
667
+ log_file: record.log_file ?? null
668
+ };
669
+ }
670
+ async function cmdStatus(rest) {
671
+ const { flags, positional } = parseArgs(rest);
672
+ const opts = {
673
+ ...commonOpts(flags),
674
+ all: true
675
+ };
676
+ const keyword = positional[0];
677
+ if (!keyword) throw new Error("usage: ay status <keyword> [--watch] [--interval=N]");
678
+ const watch = !!(flags.watch || flags.w);
679
+ const intervalFlag = typeof flags.interval === "string" ? Number(flags.interval) : 2;
680
+ const intervalMs = Math.max(500, (Number.isFinite(intervalFlag) ? intervalFlag : 2) * 1e3);
681
+ const record = await resolveOne(keyword, opts);
682
+ const emit = (snap, ts) => {
683
+ const out = ts !== void 0 ? {
684
+ ts,
685
+ ...snap
686
+ } : snap;
687
+ process.stdout.write(JSON.stringify(out) + "\n");
688
+ };
689
+ if (!watch) {
690
+ emit(await snapshotStatus(record));
691
+ return 0;
692
+ }
693
+ process.stderr.write(`watching pid ${record.pid} every ${intervalMs / 1e3}s… (Ctrl-C to stop)\n`);
694
+ let prev = null;
695
+ const tick = async () => {
696
+ const snap = await snapshotStatus(record);
697
+ if (prev === null || snap.state !== prev.state || snap.activity !== prev.activity || snap.exit_code !== prev.exit_code) {
698
+ emit(snap, Date.now());
699
+ prev = {
700
+ state: snap.state,
701
+ activity: snap.activity,
702
+ exit_code: snap.exit_code
703
+ };
704
+ }
705
+ };
706
+ await tick();
707
+ await new Promise((resolve) => {
708
+ const timer = setInterval(tick, intervalMs);
709
+ process.on("SIGINT", () => {
710
+ clearInterval(timer);
711
+ resolve();
712
+ });
713
+ });
624
714
  return 0;
625
715
  }
626
716
 
627
717
  //#endregion
628
718
  export { isSubcommand, runSubcommand };
629
- //# sourceMappingURL=subcommands-Vt_yQiEZ.js.map
719
+ //# sourceMappingURL=subcommands-DjO8lthH.js.map
@@ -1,7 +1,8 @@
1
1
  import { n as logger, t as addTransport } from "./logger-B9h0djqx.js";
2
- import { r as getInstalledPackage } from "./versionChecker-Ct-4UPeG.js";
2
+ import { r as getInstalledPackage } from "./versionChecker-CspuhOwO.js";
3
3
  import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-C22d9SRJ.js";
4
4
  import { t as PidStore } from "./pidStore-C1JXxoPi.js";
5
+ import { r as readGlobalPids } from "./globalPidIndex-Cr-g75QF.js";
5
6
  import { arch, platform } from "process";
6
7
  import { execSync } from "child_process";
7
8
  import { closeSync, constants, createReadStream, existsSync, mkdirSync, openSync } from "fs";
@@ -1062,7 +1063,7 @@ const CLIS_CONFIG = config.clis;
1062
1063
  * });
1063
1064
  * ```
1064
1065
  */
1065
- async function agentYes({ cli, cliArgs = [], prompt, robust = true, cwd, env, exitOnIdle, logFile, removeControlCharactersFromStdout = false, verbose = false, queue = false, install = false, resume = false, useSkills = false, useStdinAppend = false, autoYes = true, idleAction }) {
1066
+ async function agentYes({ cli, cliArgs = [], prompt, robust = true, cwd, env, exitOnIdle, logFile, removeControlCharactersFromStdout = false, verbose = false, queue = false, install = false, resume = false, useSkills = false, useStdinAppend = false, autoYes = true, idleAction, swarmHint = true }) {
1066
1067
  if (!cli) throw new Error(`cli is required`);
1067
1068
  const conf = CLIS_CONFIG[cli] || DIE(`Unsupported cli tool: ${cli}, current process.argv: ${process.argv.join(" ")}`);
1068
1069
  const workingDir = cwd ?? process.cwd();
@@ -1139,6 +1140,18 @@ async function agentYes({ cli, cliArgs = [], prompt, robust = true, cwd, env, ex
1139
1140
  } catch (error) {
1140
1141
  if (verbose) logger.warn("[skills] Failed to inject SKILL.md header:", { error });
1141
1142
  }
1143
+ if (swarmHint) try {
1144
+ const peers = await readGlobalPids({ liveOnly: true });
1145
+ if (peers.length > 0) {
1146
+ const hint = `${peers.length} peer agent${peers.length > 1 ? "s are" : " is"} running. Before asking the user for input on any domain-specific topic (design, testing, architecture, etc.), check for relevant peers first: \`ay ls --json\` (see \`prompt\` field for their role). Ask one: \`ay send <pid> <question>\`. Read reply: \`ay tail <pid>\`. Do not use interactive forms or user prompts when a peer can answer.`;
1147
+ if (cli === "claude") cliArgs = [
1148
+ "--append-system-prompt",
1149
+ hint,
1150
+ ...cliArgs
1151
+ ];
1152
+ prompt = prompt ? `[${hint}]\n\n${prompt}` : hint;
1153
+ }
1154
+ } catch {}
1142
1155
  if (resume) if (cli === "codex" && resume) {
1143
1156
  const storedSessionId = await getSessionForCwd(workingDir);
1144
1157
  if (storedSessionId) {
@@ -1171,6 +1184,7 @@ async function agentYes({ cli, cliArgs = [], prompt, robust = true, cwd, env, ex
1171
1184
  prompt = void 0;
1172
1185
  } else logger.warn(`Unknown promptArg format: ${cliConf.promptArg}`);
1173
1186
  const ptyEnv = { ...env ?? process.env };
1187
+ ptyEnv.AGENT_YES_PID = String(process.pid);
1174
1188
  const ptyOptions = {
1175
1189
  name: "xterm-color",
1176
1190
  ...getTerminalDimensions(),
@@ -1679,4 +1693,4 @@ function sleep(ms) {
1679
1693
 
1680
1694
  //#endregion
1681
1695
  export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
1682
- //# sourceMappingURL=ts-DbdWuoGq.js.map
1696
+ //# sourceMappingURL=ts-Bw6gQKyU.js.map
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
7
7
 
8
8
  //#region package.json
9
9
  var name = "claude-yes";
10
- var version = "1.83.0";
10
+ var version = "1.84.0";
11
11
 
12
12
  //#endregion
13
13
  //#region ts/versionChecker.ts
@@ -221,4 +221,4 @@ async function displayVersion() {
221
221
 
222
222
  //#endregion
223
223
  export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
224
- //# sourceMappingURL=versionChecker-Ct-4UPeG.js.map
224
+ //# sourceMappingURL=versionChecker-CspuhOwO.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-yes",
3
- "version": "1.83.0",
3
+ "version": "1.84.0",
4
4
  "description": "A wrapper tool that automates interactions with various AI CLI tools by automatically handling common prompts and responses.",
5
5
  "keywords": [
6
6
  "ai",
package/ts/index.ts CHANGED
@@ -28,6 +28,7 @@ import { AgentContext } from "./core/context.ts";
28
28
  import { createTerminatorStream } from "./core/streamHelpers.ts";
29
29
  import { globalAgentRegistry } from "./agentRegistry.ts";
30
30
  import { notifyWebhook } from "./webhookNotifier.ts";
31
+ import { readGlobalPids } from "./globalPidIndex.ts";
31
32
 
32
33
  export { removeControlCharacters };
33
34
  export { AgentContext };
@@ -124,6 +125,7 @@ export default async function agentYes({
124
125
  useStdinAppend = false,
125
126
  autoYes = true,
126
127
  idleAction,
128
+ swarmHint = true,
127
129
  }: {
128
130
  cli: keyof typeof CLIS_CONFIG;
129
131
  cliArgs?: string[];
@@ -142,6 +144,7 @@ export default async function agentYes({
142
144
  useStdinAppend?: boolean; // if true, enable FIFO input stream on Linux, for additional stdin input
143
145
  autoYes?: boolean; // if true, auto-yes is enabled (default), toggle with Ctrl+Y during session
144
146
  idleAction?: string; // if set, type this message when idle instead of exiting
147
+ swarmHint?: boolean; // if true (default), inject peer discovery hint when other agents are running; --no-swarm-hint to opt out
145
148
  }) {
146
149
  if (!cli) throw new Error(`cli is required`);
147
150
  const conf =
@@ -274,6 +277,23 @@ export default async function agentYes({
274
277
  if (verbose) logger.warn("[skills] Failed to inject SKILL.md header:", { error });
275
278
  }
276
279
 
280
+ // Inject peer discovery hint when other agents are running
281
+ if (swarmHint) {
282
+ try {
283
+ const peers = await readGlobalPids({ liveOnly: true });
284
+ if (peers.length > 0) {
285
+ const hint = `${peers.length} peer agent${peers.length > 1 ? "s are" : " is"} running. Before asking the user for input on any domain-specific topic (design, testing, architecture, etc.), check for relevant peers first: \`ay ls --json\` (see \`prompt\` field for their role). Ask one: \`ay send <pid> <question>\`. Read reply: \`ay tail <pid>\`. Do not use interactive forms or user prompts when a peer can answer.`;
286
+ if (cli === "claude") {
287
+ cliArgs = ["--append-system-prompt", hint, ...cliArgs];
288
+ }
289
+ // Prepend to prompt for all CLIs (including claude) so it's read before the task
290
+ prompt = prompt ? `[${hint}]\n\n${prompt}` : hint;
291
+ }
292
+ } catch {
293
+ // Non-fatal
294
+ }
295
+ }
296
+
277
297
  // Handle --continue flag for codex session restoration
278
298
  if (resume) {
279
299
  if (cli === "codex" && resume) {
@@ -322,6 +342,7 @@ export default async function agentYes({
322
342
 
323
343
  // Spawn the agent CLI process
324
344
  const ptyEnv = { ...(env ?? (process.env as Record<string, string>)) };
345
+ ptyEnv.AGENT_YES_PID = String(process.pid);
325
346
  const ptyOptions = {
326
347
  name: "xterm-color",
327
348
  ...getTerminalDimensions(),
@@ -63,6 +63,12 @@ export function parseCliArgs(argv: string[], supportedClis?: readonly string[])
63
63
  "Prepend SKILL.md header from current directory to the prompt (helpful for non-Claude agents)",
64
64
  default: false,
65
65
  })
66
+ .option("swarm-hint", {
67
+ type: "boolean",
68
+ description:
69
+ "Inject peer discovery hint into agent system prompt when other agents are running (use --no-swarm-hint to opt out)",
70
+ default: true,
71
+ })
66
72
  .option("timeout", {
67
73
  type: "string",
68
74
  description: 'Exit after a period of inactivity, e.g., "5s" or "1m"',
@@ -313,6 +319,7 @@ export function parseCliArgs(argv: string[], supportedClis?: readonly string[])
313
319
  verbose: parsedArgv.verbose,
314
320
  resume: parsedArgv.continue, // Note: intentional use resume here to avoid preserved keyword (continue)
315
321
  useSkills: parsedArgv.useSkills,
322
+ swarmHint: parsedArgv.swarmHint,
316
323
  appendPrompt: parsedArgv.appendPrompt,
317
324
  useStdinAppend: Boolean(parsedArgv.stdpush || parsedArgv.ipc || parsedArgv.fifo), // Support --stdpush, --ipc, and --fifo (backward compatibility)
318
325
  showVersion: parsedArgv.version,
package/ts/subcommands.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `cy ls / read / cat / tail / head / send` subcommand implementations.
2
+ * `ay ls / read / cat / tail / head / send` subcommand implementations.
3
3
  *
4
4
  * Mirrors the principles of koho's `terminal-ws-lib.ts` (session list, render
5
5
  * via @xterm/headless, keyword-keyed input) — but file-based instead of via
@@ -66,7 +66,7 @@ async function compactNotes(): Promise<void> {
66
66
  /**
67
67
  * Read the per-cwd TS PidStore JSONL and convert to the global record shape,
68
68
  * so pre-existing TS agents that were spawned before the global-index mirror
69
- * shipped still show up in `cy ls`. Merging is done in `mergeRecords`.
69
+ * shipped still show up in `ay ls`. Merging is done in `mergeRecords`.
70
70
  */
71
71
  async function readLocalTsPids(cwd: string): Promise<GlobalPidRecord[]> {
72
72
  const jsonlPath = path.join(cwd, ".agent-yes", "pid-records.jsonl");
@@ -127,6 +127,7 @@ const SUBCOMMANDS = new Set([
127
127
  "ls",
128
128
  "list",
129
129
  "ps",
130
+ "status",
130
131
  "read",
131
132
  "cat",
132
133
  "tail",
@@ -136,6 +137,8 @@ const SUBCOMMANDS = new Set([
136
137
  "note",
137
138
  ]);
138
139
 
140
+ const IDLE_THRESHOLD_MS = 60 * 1000;
141
+
139
142
  export function isSubcommand(name: string | undefined): boolean {
140
143
  return !!name && SUBCOMMANDS.has(name);
141
144
  }
@@ -156,6 +159,8 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
156
159
  case "list":
157
160
  case "ps":
158
161
  return await cmdLs(rest);
162
+ case "status":
163
+ return await cmdStatus(rest);
159
164
  case "read":
160
165
  case "cat":
161
166
  return await cmdRead(rest, { mode: "cat" });
@@ -174,7 +179,7 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
174
179
  }
175
180
  } catch (err) {
176
181
  const msg = err instanceof Error ? err.message : String(err);
177
- process.stderr.write(`cy ${sub}: ${msg}\n`);
182
+ process.stderr.write(`ay ${sub}: ${msg}\n`);
178
183
  return 1;
179
184
  }
180
185
  }
@@ -210,7 +215,7 @@ export function parseArgs(rest: string[]): ParsedArgs {
210
215
  const next = rest[i + 1];
211
216
  // Boolean flags: --all, --json, --latest
212
217
  if (
213
- ["all", "active", "follow", "json", "latest"].includes(key) ||
218
+ ["all", "active", "follow", "json", "latest", "watch"].includes(key) ||
214
219
  !next ||
215
220
  next.startsWith("-")
216
221
  ) {
@@ -322,7 +327,7 @@ async function resolveOne(keyword: string | undefined, opts: CommonOpts): Promis
322
327
  }
323
328
 
324
329
  // ---------------------------------------------------------------------------
325
- // cy ls
330
+ // ay ls
326
331
  // ---------------------------------------------------------------------------
327
332
 
328
333
  async function cmdLs(rest: string[]): Promise<number> {
@@ -359,7 +364,6 @@ async function cmdLs(rest: string[]): Promise<number> {
359
364
  const fixedWidth = widths.pid + widths.cli + widths.status + widths.age + widths.cwd + 5 * 2; // 5 separators of " "
360
365
  const promptBudget = Math.max(20, termWidth - fixedWidth - 1);
361
366
 
362
- const IDLE_THRESHOLD_MS = 60 * 1000;
363
367
  const notes = await readNotes();
364
368
  const rows = await Promise.all(
365
369
  records.map(async (r) => {
@@ -383,9 +387,9 @@ async function cmdLs(rest: string[]): Promise<number> {
383
387
  hasNote = true;
384
388
  } else if (r.log_file && displayStatus !== "stopped") {
385
389
  const activity = await extractActivity(r.log_file);
386
- label = truncate(activity ?? r.prompt ?? "", promptBudget);
390
+ label = truncate(activity ?? (r.prompt ? `→ ${r.prompt}` : ""), promptBudget);
387
391
  } else {
388
- label = truncate(r.prompt ?? "", promptBudget);
392
+ label = truncate(r.prompt ? `→ ${r.prompt}` : "", promptBudget);
389
393
  }
390
394
  return {
391
395
  pid: String(r.pid),
@@ -429,17 +433,24 @@ async function cmdLs(rest: string[]): Promise<number> {
429
433
  const stopped = rows.find((r) => !r._alive);
430
434
  const hints: string[] = ["\n"];
431
435
  if (alive) {
432
- hints.push(` cy tail ${alive.pid} # view latest output\n`);
433
- hints.push(` cy tail -f ${alive.pid} # follow live output\n`);
434
- hints.push(` cy send ${alive.pid} "next: ..." # send a prompt\n`);
435
- hints.push(` cy send ${alive.pid} "" --code=ctrl-c # interrupt\n`);
436
- hints.push(` cy note ${alive.pid} "what it's doing" # set a note\n`);
436
+ hints.push(` ay status ${alive.pid} # JSON status snapshot\n`);
437
+ hints.push(` ay status ${alive.pid} --watch # stream changes as JSON\n`);
438
+ hints.push(` ay tail ${alive.pid} # view latest output\n`);
439
+ hints.push(` ay tail -f ${alive.pid} # follow live output\n`);
440
+ hints.push(
441
+ ` ay send ${alive.pid} "next: ..." # send a prompt (keyword: pid, cwd, or prompt substring)\n`,
442
+ );
443
+ hints.push(` ay send ${alive.pid} "" --code=ctrl-c # interrupt\n`);
444
+ hints.push(` ay note ${alive.pid} "what it's doing" # set a note\n`);
445
+ hints.push(
446
+ ` ay ls --json # machine-readable list for scripts/agents\n`,
447
+ );
437
448
  }
438
449
  if (stopped) {
439
- hints.push(` cy restart ${stopped.pid} # restart stopped agent\n`);
450
+ hints.push(` ay restart ${stopped.pid} # restart stopped agent\n`);
440
451
  }
441
452
  if (!alive && !stopped)
442
- hints.push(` cy ls --all # show exited agents\n`);
453
+ hints.push(` ay ls --all # show exited agents\n`);
443
454
  process.stderr.write(hints.join(""));
444
455
  }
445
456
 
@@ -469,7 +480,7 @@ function truncate(s: string, n: number): string {
469
480
  }
470
481
 
471
482
  // ---------------------------------------------------------------------------
472
- // cy read / cat / tail / head
483
+ // ay read / cat / tail / head
473
484
  // ---------------------------------------------------------------------------
474
485
 
475
486
  interface ReadOpts {
@@ -544,10 +555,10 @@ async function cmdRead(rest: string[], { mode }: ReadOpts): Promise<number> {
544
555
 
545
556
  process.stderr.write(
546
557
  `\n` +
547
- ` cy ls # list all agents\n` +
548
- ` cy tail -f ${record.pid} # follow live output\n` +
549
- ` cy send ${record.pid} "next: ..." # send a prompt\n` +
550
- ` cy send ${record.pid} "" --code=ctrl-c # interrupt\n`,
558
+ ` ay ls # list all agents\n` +
559
+ ` ay tail -f ${record.pid} # follow live output\n` +
560
+ ` ay send ${record.pid} "next: ..." # send a prompt\n` +
561
+ ` ay send ${record.pid} "" --code=ctrl-c # interrupt\n`,
551
562
  );
552
563
  return 0;
553
564
  }
@@ -656,26 +667,38 @@ function extractActivityFromLines(lines: string[]): string | null {
656
667
 
657
668
  const clean = lines.filter((l) => !isChrome(l));
658
669
 
670
+ const isSpinnerLine = (l: string) =>
671
+ /^[^\w\s❯>⎿✓✗]\s+[A-Z]\w+[….]/u.test(l.trim()) || /still thinking/i.test(l);
672
+
673
+ // Find positions of the last ❯ prompt and last spinner in the rendered output.
674
+ // If ❯ comes after the last spinner, the agent finished and is waiting — show
675
+ // idle state rather than the stale spinner description.
676
+ let lastPromptIdx = -1;
677
+ let lastSpinnerIdx = -1;
678
+ for (let i = clean.length - 1; i >= 0; i--) {
679
+ const l = clean[i]!.trim();
680
+ if (lastPromptIdx === -1 && l.startsWith("❯")) lastPromptIdx = i;
681
+ if (lastSpinnerIdx === -1 && isSpinnerLine(l)) lastSpinnerIdx = i;
682
+ if (lastPromptIdx !== -1 && lastSpinnerIdx !== -1) break;
683
+ }
684
+
685
+ // ❯ appears after (or without) any spinner → agent is idle/waiting for input
686
+ if (lastPromptIdx > lastSpinnerIdx) {
687
+ const text = clean[lastPromptIdx]!.trim()
688
+ .replace(/^❯\s*/, "")
689
+ .trim();
690
+ return text ? `» ${text}` : null;
691
+ }
692
+
659
693
  // Priority 1: thinking/composing spinner active
660
694
  // Claude Code cycles through various Unicode dingbats for its spinner (✢✳✶✻✷…).
661
695
  // The format is always: SPINNER_CHAR Verb… (timing…)
662
696
  // Require ellipsis after the verb so we don't false-positive on normal text
663
697
  // that happens to contain one of these chars mid-sentence.
664
- const thinkingLine = clean.find(
665
- (l) => /^[^\w\s❯>⎿✓✗]\s+[A-Z]\w+[….]/u.test(l.trim()) || /still thinking/i.test(l),
666
- );
698
+ const thinkingLine = clean.find((l) => isSpinnerLine(l));
667
699
  if (thinkingLine) {
668
700
  const m = /^.\s+(\w+[^(]*)(?:\s*\(|$)/u.exec(thinkingLine.trim());
669
- return m ? `✳ ${m[1].trim()}` : "thinking…";
670
- }
671
-
672
- // Priority 2: last ❯ prompt line means agent is idle, waiting for next input
673
- const promptLines = clean.filter((l) => /^❯\s+/.test(l.trim()));
674
- if (promptLines.length > 0) {
675
- const text = promptLines[promptLines.length - 1]!.trim()
676
- .replace(/^❯\s+/, "")
677
- .trim();
678
- if (text) return `» ${text}`;
701
+ return m?.[1] ? `✳ ${m[1].trim()}` : "thinking…";
679
702
  }
680
703
 
681
704
  // Priority 3: ✻ spinner just finished — show nearby context
@@ -705,7 +728,7 @@ function extractActivityFromLines(lines: string[]): string | null {
705
728
  }
706
729
 
707
730
  // ---------------------------------------------------------------------------
708
- // cy send
731
+ // ay send
709
732
  // ---------------------------------------------------------------------------
710
733
 
711
734
  async function cmdSend(rest: string[]): Promise<number> {
@@ -715,7 +738,7 @@ async function cmdSend(rest: string[]): Promise<number> {
715
738
  const rawMessage = positional.slice(1).join(" ");
716
739
 
717
740
  if (!keyword)
718
- throw new Error("usage: cy send <keyword> <msg|-> [--code=enter|esc|ctrl-c|ctrl-y|tab|none]");
741
+ throw new Error("usage: ay send <keyword> <msg|-> [--code=enter|esc|ctrl-c|ctrl-y|tab|none]");
719
742
 
720
743
  const codeName = typeof flags.code === "string" ? flags.code.toLowerCase() : "enter";
721
744
  const trailing = controlCodeFromName(codeName);
@@ -737,20 +760,30 @@ async function cmdSend(rest: string[]): Promise<number> {
737
760
  body = rawMessage;
738
761
  }
739
762
 
740
- if (body && trailing) {
741
- await writeToIpc(fifoPath, body);
763
+ const sourcePid = process.env.AGENT_YES_PID ? Number(process.env.AGENT_YES_PID) : null;
764
+ const talkBack = sourcePid
765
+ ? `\n(from AGENT_YES_PID=${sourcePid} — reply: ay send ${sourcePid} "...")`
766
+ : "";
767
+
768
+ const fullBody = body + talkBack;
769
+ if (fullBody && trailing) {
770
+ await writeToIpc(fifoPath, fullBody);
742
771
  await new Promise((r) => setTimeout(r, 200));
743
772
  await writeToIpc(fifoPath, trailing);
744
773
  } else {
745
- await writeToIpc(fifoPath, body + trailing);
774
+ await writeToIpc(fifoPath, fullBody + trailing);
746
775
  }
747
776
  const payload = body + trailing;
748
777
  process.stdout.write(`sent to pid ${record.pid} (${record.cli}): ${truncate(payload, 80)}\n`);
749
778
 
779
+ const replyHint = sourcePid
780
+ ? ` ay send ${sourcePid} "..." # reply to sender\n`
781
+ : "";
750
782
  process.stderr.write(
751
783
  `\n` +
752
- ` cy tail ${record.pid} # watch output\n` +
753
- ` cy ls # list all agents\n`,
784
+ replyHint +
785
+ ` ay tail ${record.pid} # watch output\n` +
786
+ ` ay ls # list all agents\n`,
754
787
  );
755
788
  return 0;
756
789
  }
@@ -818,7 +851,7 @@ async function writeToIpc(ipcPath: string, payload: string): Promise<void> {
818
851
  }
819
852
 
820
853
  // ---------------------------------------------------------------------------
821
- // cy restart
854
+ // ay restart
822
855
  // ---------------------------------------------------------------------------
823
856
 
824
857
  async function cmdRestart(rest: string[]): Promise<number> {
@@ -828,7 +861,7 @@ async function cmdRestart(rest: string[]): Promise<number> {
828
861
  const record = await resolveOne(keyword, opts);
829
862
 
830
863
  if (isPidAlive(record.pid)) {
831
- process.stderr.write(`pid ${record.pid} is still running — stop it first or use cy send\n`);
864
+ process.stderr.write(`pid ${record.pid} is still running — stop it first or use ay send\n`);
832
865
  return 1;
833
866
  }
834
867
 
@@ -846,14 +879,14 @@ async function cmdRestart(rest: string[]): Promise<number> {
846
879
  );
847
880
  process.stderr.write(
848
881
  `\n` +
849
- ` cy tail ${proc.pid} # watch output\n` +
850
- ` cy ls # list all agents\n`,
882
+ ` ay tail ${proc.pid} # watch output\n` +
883
+ ` ay ls # list all agents\n`,
851
884
  );
852
885
  return 0;
853
886
  }
854
887
 
855
888
  // ---------------------------------------------------------------------------
856
- // cy note
889
+ // ay note
857
890
  // ---------------------------------------------------------------------------
858
891
 
859
892
  async function cmdNote(rest: string[]): Promise<number> {
@@ -862,7 +895,7 @@ async function cmdNote(rest: string[]): Promise<number> {
862
895
  const keyword = positional[0];
863
896
  const note = positional.slice(1).join(" ");
864
897
 
865
- if (!keyword) throw new Error('usage: cy note <keyword> ["note text"] (omit text to clear)');
898
+ if (!keyword) throw new Error('usage: ay note <keyword> ["note text"] (omit text to clear)');
866
899
 
867
900
  const record = await resolveOne(keyword, { ...opts, all: true });
868
901
 
@@ -876,6 +909,114 @@ async function cmdNote(rest: string[]): Promise<number> {
876
909
 
877
910
  await writeNote(record.pid, note);
878
911
  process.stdout.write(`note set for pid ${record.pid}: ${note}\n`);
879
- process.stderr.write(`\n cy ls # see updated note in list\n`);
912
+ process.stderr.write(`\n ay ls # see updated note in list\n`);
913
+ return 0;
914
+ }
915
+
916
+ // ---------------------------------------------------------------------------
917
+ // ay status
918
+ // ---------------------------------------------------------------------------
919
+
920
+ interface StatusSnapshot {
921
+ pid: number;
922
+ cli: string;
923
+ cwd: string;
924
+ state: "active" | "idle" | "stopped";
925
+ activity: string | null;
926
+ note: string | null;
927
+ log_mtime_ms: number | null;
928
+ started_at: number;
929
+ age_ms: number;
930
+ exit_code: number | null;
931
+ exit_reason: string | null;
932
+ log_file: string | null;
933
+ }
934
+
935
+ async function snapshotStatus(record: GlobalPidRecord): Promise<StatusSnapshot> {
936
+ const alive = isPidAlive(record.pid);
937
+ let state: "active" | "idle" | "stopped";
938
+ let logMtimeMs: number | null = null;
939
+ if (!alive) {
940
+ state = "stopped";
941
+ } else if (record.log_file) {
942
+ logMtimeMs = await stat(record.log_file)
943
+ .then((s) => s.mtimeMs)
944
+ .catch(() => null);
945
+ state = logMtimeMs !== null && Date.now() - logMtimeMs > IDLE_THRESHOLD_MS ? "idle" : "active";
946
+ } else {
947
+ state = "active";
948
+ }
949
+ const activity =
950
+ state !== "stopped" && record.log_file ? await extractActivity(record.log_file) : null;
951
+ const notes = await readNotes();
952
+ const note = notes.get(record.pid) ?? null;
953
+ return {
954
+ pid: record.pid,
955
+ cli: record.cli,
956
+ cwd: record.cwd,
957
+ state,
958
+ activity,
959
+ note,
960
+ log_mtime_ms: logMtimeMs,
961
+ started_at: record.started_at,
962
+ age_ms: Date.now() - record.started_at,
963
+ exit_code: record.exit_code,
964
+ exit_reason: record.exit_reason,
965
+ log_file: record.log_file ?? null,
966
+ };
967
+ }
968
+
969
+ async function cmdStatus(rest: string[]): Promise<number> {
970
+ const { flags, positional } = parseArgs(rest);
971
+ const opts = { ...commonOpts(flags), all: true };
972
+ const keyword = positional[0];
973
+
974
+ if (!keyword) throw new Error("usage: ay status <keyword> [--watch] [--interval=N]");
975
+
976
+ const watch = !!(flags.watch || flags.w);
977
+ const intervalFlag = typeof flags.interval === "string" ? Number(flags.interval) : 2;
978
+ const intervalMs = Math.max(500, (Number.isFinite(intervalFlag) ? intervalFlag : 2) * 1000);
979
+
980
+ const record = await resolveOne(keyword, opts);
981
+
982
+ const emit = (snap: StatusSnapshot, ts?: number): void => {
983
+ const out = ts !== undefined ? { ts, ...snap } : snap;
984
+ process.stdout.write(JSON.stringify(out) + "\n");
985
+ };
986
+
987
+ if (!watch) {
988
+ emit(await snapshotStatus(record));
989
+ return 0;
990
+ }
991
+
992
+ process.stderr.write(
993
+ `watching pid ${record.pid} every ${intervalMs / 1000}s… (Ctrl-C to stop)\n`,
994
+ );
995
+
996
+ let prev: { state: string; activity: string | null; exit_code: number | null } | null = null;
997
+
998
+ const tick = async (): Promise<void> => {
999
+ const snap = await snapshotStatus(record);
1000
+ if (
1001
+ prev === null ||
1002
+ snap.state !== prev.state ||
1003
+ snap.activity !== prev.activity ||
1004
+ snap.exit_code !== prev.exit_code
1005
+ ) {
1006
+ emit(snap, Date.now());
1007
+ prev = { state: snap.state, activity: snap.activity, exit_code: snap.exit_code };
1008
+ }
1009
+ };
1010
+
1011
+ await tick();
1012
+
1013
+ await new Promise<void>((resolve) => {
1014
+ const timer = setInterval(tick, intervalMs);
1015
+ process.on("SIGINT", () => {
1016
+ clearInterval(timer);
1017
+ resolve();
1018
+ });
1019
+ });
1020
+
880
1021
  return 0;
881
1022
  }