claude-yes 1.82.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-WV8trEPX.js";
1
+ import { t as CLIS_CONFIG } from "./ts-Bw6gQKyU.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import "./versionChecker-BVt2a2mL.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-WKiG4dyQ.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-BVt2a2mL.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-eMrWDGMq.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-WKiG4dyQ.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-WV8trEPX.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-BVt2a2mL.js";
3
+ import "./versionChecker-CspuhOwO.js";
4
4
  import "./pidStore-C1JXxoPi.js";
5
5
  import "./globalPidIndex-Cr-g75QF.js";
6
6
 
@@ -1,12 +1,12 @@
1
1
  import "./logger-B9h0djqx.js";
2
2
  import { r as readGlobalPids } from "./globalPidIndex-Cr-g75QF.js";
3
- import { appendFile, mkdir, readFile, stat, writeFile } from "fs/promises";
3
+ import { appendFile, mkdir, open, readFile, stat, writeFile } from "fs/promises";
4
4
  import { homedir } from "os";
5
5
  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;
@@ -271,8 +274,14 @@ async function cmdLs(rest) {
271
274
  const mtime = await stat(r.log_file).then((s) => s.mtimeMs).catch(() => null);
272
275
  displayStatus = mtime !== null && Date.now() - mtime > IDLE_THRESHOLD_MS ? "idle" : "active";
273
276
  } else displayStatus = "active";
274
- const label = truncate(notes.get(r.pid) ?? r.prompt ?? "", promptBudget);
275
- const hasNote = notes.has(r.pid);
277
+ const note = notes.get(r.pid);
278
+ let label;
279
+ let hasNote = false;
280
+ if (note) {
281
+ label = truncate(note, promptBudget);
282
+ hasNote = true;
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);
276
285
  return {
277
286
  pid: String(r.pid),
278
287
  cli: r.cli,
@@ -306,14 +315,17 @@ async function cmdLs(rest) {
306
315
  const stopped = rows.find((r) => !r._alive);
307
316
  const hints = ["\n"];
308
317
  if (alive) {
309
- hints.push(` cy tail ${alive.pid} # view latest output\n`);
310
- hints.push(` cy tail -f ${alive.pid} # follow live output\n`);
311
- hints.push(` cy send ${alive.pid} "next: ..." # send a prompt\n`);
312
- hints.push(` cy send ${alive.pid} "" --code=ctrl-c # interrupt\n`);
313
- 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`);
314
326
  }
315
- if (stopped) hints.push(` cy restart ${stopped.pid} # restart stopped agent\n`);
316
- 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`);
317
329
  process.stderr.write(hints.join(""));
318
330
  }
319
331
  return 0;
@@ -386,8 +398,8 @@ async function cmdRead(rest, { mode }) {
386
398
  return 0;
387
399
  }
388
400
  process.stderr.write(`
389
- cy ls # list all agents
390
- 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`);
391
403
  return 0;
392
404
  }
393
405
  /**
@@ -426,12 +438,85 @@ async function renderRawLog(buf, { mode, n }) {
426
438
  return lines.slice(0, n).join("\n");
427
439
  }
428
440
  }
441
+ /**
442
+ * Extract a one-line activity summary from a raw log file.
443
+ * Reads only the last 32 KB for speed, renders via xterm for clean output.
444
+ */
445
+ async function extractActivity(logPath) {
446
+ const TAIL_BYTES = 32 * 1024;
447
+ let buf;
448
+ try {
449
+ const fh = await open(logPath, "r");
450
+ try {
451
+ const { size } = await fh.stat();
452
+ if (size === 0) return null;
453
+ if (size <= TAIL_BYTES) {
454
+ const data = await fh.readFile();
455
+ buf = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
456
+ } else {
457
+ const tmp = Buffer.alloc(TAIL_BYTES);
458
+ const { bytesRead } = await fh.read(tmp, 0, TAIL_BYTES, size - TAIL_BYTES);
459
+ buf = new Uint8Array(tmp.buffer, 0, bytesRead);
460
+ }
461
+ } finally {
462
+ await fh.close();
463
+ }
464
+ } catch {
465
+ return null;
466
+ }
467
+ try {
468
+ return extractActivityFromLines((await renderRawLog(buf, {
469
+ mode: "tail",
470
+ n: 40
471
+ })).split("\n"));
472
+ } catch {
473
+ return null;
474
+ }
475
+ }
476
+ function extractActivityFromLines(lines) {
477
+ const isChrome = (l) => {
478
+ const s = l.trim();
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);
480
+ };
481
+ const clean = lines.filter((l) => !isChrome(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));
496
+ if (thinkingLine) {
497
+ const m = /^.\s+(\w+[^(]*)(?:\s*\(|$)/u.exec(thinkingLine.trim());
498
+ return m?.[1] ? `✳ ${m[1].trim()}` : "thinking…";
499
+ }
500
+ const cookIdx = clean.findIndex((l) => /^✻\s+/.test(l.trim()));
501
+ if (cookIdx >= 0) {
502
+ const window = clean.slice(Math.max(0, cookIdx - 8), cookIdx);
503
+ for (let i = window.length - 1; i >= 0; i--) {
504
+ const l = window[i].trim();
505
+ if (l && !/^[✻✢⧉❯]/.test(l) && !isChrome(l)) return l.length > 80 ? l.slice(0, 79) + "…" : l;
506
+ }
507
+ }
508
+ for (let i = clean.length - 1; i >= 0; i--) {
509
+ const l = clean[i].trim();
510
+ if (l && !/^[─●○◉⧉]/.test(l) && !/^[^\w\s❯>]\s+[A-Z]\w+[….]/u.test(l)) return l.length > 80 ? l.slice(0, 79) + "…" : l;
511
+ }
512
+ return null;
513
+ }
429
514
  async function cmdSend(rest) {
430
515
  const { flags, positional } = parseArgs(rest);
431
516
  const opts = commonOpts(flags);
432
517
  const keyword = positional[0];
433
518
  const rawMessage = positional.slice(1).join(" ");
434
- 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]");
435
520
  const trailing = controlCodeFromName(typeof flags.code === "string" ? flags.code.toLowerCase() : "enter");
436
521
  const record = await resolveOne(keyword, opts);
437
522
  const fifoPath = record.fifo_file;
@@ -442,14 +527,18 @@ async function cmdSend(rest) {
442
527
  for await (const chunk of process.stdin) chunks.push(chunk);
443
528
  body = Buffer.concat(chunks).toString("utf-8").trimEnd();
444
529
  } else body = rawMessage;
445
- if (body && trailing) {
446
- 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);
447
535
  await new Promise((r) => setTimeout(r, 200));
448
536
  await writeToIpc(fifoPath, trailing);
449
- } else await writeToIpc(fifoPath, body + trailing);
537
+ } else await writeToIpc(fifoPath, fullBody + trailing);
450
538
  const payload = body + trailing;
451
539
  process.stdout.write(`sent to pid ${record.pid} (${record.cli}): ${truncate(payload, 80)}\n`);
452
- 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`);
453
542
  return 0;
454
543
  }
455
544
  function controlCodeFromName(name) {
@@ -513,7 +602,7 @@ async function cmdRestart(rest) {
513
602
  const keyword = positional[0];
514
603
  const record = await resolveOne(keyword, opts);
515
604
  if (isPidAlive(record.pid)) {
516
- 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`);
517
606
  return 1;
518
607
  }
519
608
  const args = ["--cli=" + record.cli];
@@ -528,7 +617,7 @@ async function cmdRestart(rest) {
528
617
  ]
529
618
  });
530
619
  process.stdout.write(`restarted ${record.cli} in ${shortenPath(record.cwd)} (new pid: ${proc.pid})\n`);
531
- 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`);
532
621
  return 0;
533
622
  }
534
623
  async function cmdNote(rest) {
@@ -536,7 +625,7 @@ async function cmdNote(rest) {
536
625
  const opts = commonOpts(flags);
537
626
  const keyword = positional[0];
538
627
  const note = positional.slice(1).join(" ");
539
- 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)");
540
629
  const record = await resolveOne(keyword, {
541
630
  ...opts,
542
631
  all: true
@@ -549,10 +638,82 @@ async function cmdNote(rest) {
549
638
  }
550
639
  await writeNote(record.pid, note);
551
640
  process.stdout.write(`note set for pid ${record.pid}: ${note}\n`);
552
- 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
+ });
553
714
  return 0;
554
715
  }
555
716
 
556
717
  //#endregion
557
718
  export { isSubcommand, runSubcommand };
558
- //# sourceMappingURL=subcommands-eMrWDGMq.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-BVt2a2mL.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-WV8trEPX.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.82.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-BVt2a2mL.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.82.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
@@ -11,7 +11,7 @@
11
11
  * to the normal agent-spawning flow.
12
12
  */
13
13
 
14
- import { appendFile, mkdir, readFile, stat, writeFile } from "fs/promises";
14
+ import { appendFile, mkdir, open, readFile, stat, writeFile } from "fs/promises";
15
15
  import { homedir } from "os";
16
16
  import path from "path";
17
17
  import { type GlobalPidRecord, readGlobalPids } from "./globalPidIndex.ts";
@@ -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) => {
@@ -375,8 +379,18 @@ async function cmdLs(rest: string[]): Promise<number> {
375
379
  } else {
376
380
  displayStatus = "active";
377
381
  }
378
- const label = truncate(notes.get(r.pid) ?? r.prompt ?? "", promptBudget);
379
- const hasNote = notes.has(r.pid);
382
+ const note = notes.get(r.pid);
383
+ let label: string;
384
+ let hasNote = false;
385
+ if (note) {
386
+ label = truncate(note, promptBudget);
387
+ hasNote = true;
388
+ } else if (r.log_file && displayStatus !== "stopped") {
389
+ const activity = await extractActivity(r.log_file);
390
+ label = truncate(activity ?? (r.prompt ? `→ ${r.prompt}` : ""), promptBudget);
391
+ } else {
392
+ label = truncate(r.prompt ? `→ ${r.prompt}` : "", promptBudget);
393
+ }
380
394
  return {
381
395
  pid: String(r.pid),
382
396
  cli: r.cli,
@@ -419,17 +433,24 @@ async function cmdLs(rest: string[]): Promise<number> {
419
433
  const stopped = rows.find((r) => !r._alive);
420
434
  const hints: string[] = ["\n"];
421
435
  if (alive) {
422
- hints.push(` cy tail ${alive.pid} # view latest output\n`);
423
- hints.push(` cy tail -f ${alive.pid} # follow live output\n`);
424
- hints.push(` cy send ${alive.pid} "next: ..." # send a prompt\n`);
425
- hints.push(` cy send ${alive.pid} "" --code=ctrl-c # interrupt\n`);
426
- 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
+ );
427
448
  }
428
449
  if (stopped) {
429
- hints.push(` cy restart ${stopped.pid} # restart stopped agent\n`);
450
+ hints.push(` ay restart ${stopped.pid} # restart stopped agent\n`);
430
451
  }
431
452
  if (!alive && !stopped)
432
- hints.push(` cy ls --all # show exited agents\n`);
453
+ hints.push(` ay ls --all # show exited agents\n`);
433
454
  process.stderr.write(hints.join(""));
434
455
  }
435
456
 
@@ -459,7 +480,7 @@ function truncate(s: string, n: number): string {
459
480
  }
460
481
 
461
482
  // ---------------------------------------------------------------------------
462
- // cy read / cat / tail / head
483
+ // ay read / cat / tail / head
463
484
  // ---------------------------------------------------------------------------
464
485
 
465
486
  interface ReadOpts {
@@ -534,10 +555,10 @@ async function cmdRead(rest: string[], { mode }: ReadOpts): Promise<number> {
534
555
 
535
556
  process.stderr.write(
536
557
  `\n` +
537
- ` cy ls # list all agents\n` +
538
- ` cy tail -f ${record.pid} # follow live output\n` +
539
- ` cy send ${record.pid} "next: ..." # send a prompt\n` +
540
- ` 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`,
541
562
  );
542
563
  return 0;
543
564
  }
@@ -589,7 +610,125 @@ async function renderRawLog(
589
610
  }
590
611
 
591
612
  // ---------------------------------------------------------------------------
592
- // cy send
613
+ // activity extraction
614
+ // ---------------------------------------------------------------------------
615
+
616
+ /**
617
+ * Extract a one-line activity summary from a raw log file.
618
+ * Reads only the last 32 KB for speed, renders via xterm for clean output.
619
+ */
620
+ async function extractActivity(logPath: string): Promise<string | null> {
621
+ const TAIL_BYTES = 32 * 1024;
622
+ let buf: Uint8Array;
623
+ try {
624
+ const fh = await open(logPath, "r");
625
+ try {
626
+ const { size } = await fh.stat();
627
+ if (size === 0) return null;
628
+ if (size <= TAIL_BYTES) {
629
+ const data = await fh.readFile();
630
+ buf = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
631
+ } else {
632
+ const tmp = Buffer.alloc(TAIL_BYTES);
633
+ const { bytesRead } = await fh.read(tmp, 0, TAIL_BYTES, size - TAIL_BYTES);
634
+ buf = new Uint8Array(tmp.buffer, 0, bytesRead);
635
+ }
636
+ } finally {
637
+ await fh.close();
638
+ }
639
+ } catch {
640
+ return null;
641
+ }
642
+
643
+ try {
644
+ const rendered = await renderRawLog(buf, { mode: "tail", n: 40 });
645
+ return extractActivityFromLines(rendered.split("\n"));
646
+ } catch {
647
+ return null;
648
+ }
649
+ }
650
+
651
+ function extractActivityFromLines(lines: string[]): string | null {
652
+ // Claude Code UI chrome: these lines carry no meaningful activity info
653
+ const isChrome = (l: string): boolean => {
654
+ const s = l.trim();
655
+ return (
656
+ !s ||
657
+ /^─+$/.test(s) ||
658
+ s.startsWith("? for shortcuts") ||
659
+ /^esc to interrupt/i.test(s) ||
660
+ /\d+%\s*until auto-compact/i.test(s) ||
661
+ /^\/model\s+/i.test(s) ||
662
+ /^⧉\s+In\s+/i.test(s) ||
663
+ /^●\s+(high|medium|low)\s*[·•]/i.test(s) ||
664
+ /^[·•]\s*\d+\s+(left|request)/i.test(s)
665
+ );
666
+ };
667
+
668
+ const clean = lines.filter((l) => !isChrome(l));
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
+
693
+ // Priority 1: thinking/composing spinner active
694
+ // Claude Code cycles through various Unicode dingbats for its spinner (✢✳✶✻✷…).
695
+ // The format is always: SPINNER_CHAR Verb… (timing…)
696
+ // Require ellipsis after the verb so we don't false-positive on normal text
697
+ // that happens to contain one of these chars mid-sentence.
698
+ const thinkingLine = clean.find((l) => isSpinnerLine(l));
699
+ if (thinkingLine) {
700
+ const m = /^.\s+(\w+[^(]*)(?:\s*\(|$)/u.exec(thinkingLine.trim());
701
+ return m?.[1] ? `✳ ${m[1].trim()}` : "thinking…";
702
+ }
703
+
704
+ // Priority 3: ✻ spinner just finished — show nearby context
705
+ const cookIdx = clean.findIndex((l) => /^✻\s+/.test(l.trim()));
706
+ if (cookIdx >= 0) {
707
+ const window = clean.slice(Math.max(0, cookIdx - 8), cookIdx);
708
+ for (let i = window.length - 1; i >= 0; i--) {
709
+ const l = window[i]!.trim();
710
+ if (l && !/^[✻✢⧉❯]/.test(l) && !isChrome(l)) {
711
+ return l.length > 80 ? l.slice(0, 79) + "…" : l;
712
+ }
713
+ }
714
+ }
715
+
716
+ // Priority 4: last meaningful non-icon line
717
+ for (let i = clean.length - 1; i >= 0; i--) {
718
+ const l = clean[i]!.trim();
719
+ // Skip lines that look like spinner patterns (caught by priority 1 above)
720
+ // and status dots/separators; everything else (including ⎿ tool sub-output
721
+ // and non-ASCII text like Japanese) is fair game as meaningful content.
722
+ if (l && !/^[─●○◉⧉]/.test(l) && !/^[^\w\s❯>]\s+[A-Z]\w+[….]/u.test(l)) {
723
+ return l.length > 80 ? l.slice(0, 79) + "…" : l;
724
+ }
725
+ }
726
+
727
+ return null;
728
+ }
729
+
730
+ // ---------------------------------------------------------------------------
731
+ // ay send
593
732
  // ---------------------------------------------------------------------------
594
733
 
595
734
  async function cmdSend(rest: string[]): Promise<number> {
@@ -599,7 +738,7 @@ async function cmdSend(rest: string[]): Promise<number> {
599
738
  const rawMessage = positional.slice(1).join(" ");
600
739
 
601
740
  if (!keyword)
602
- 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]");
603
742
 
604
743
  const codeName = typeof flags.code === "string" ? flags.code.toLowerCase() : "enter";
605
744
  const trailing = controlCodeFromName(codeName);
@@ -621,20 +760,30 @@ async function cmdSend(rest: string[]): Promise<number> {
621
760
  body = rawMessage;
622
761
  }
623
762
 
624
- if (body && trailing) {
625
- 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);
626
771
  await new Promise((r) => setTimeout(r, 200));
627
772
  await writeToIpc(fifoPath, trailing);
628
773
  } else {
629
- await writeToIpc(fifoPath, body + trailing);
774
+ await writeToIpc(fifoPath, fullBody + trailing);
630
775
  }
631
776
  const payload = body + trailing;
632
777
  process.stdout.write(`sent to pid ${record.pid} (${record.cli}): ${truncate(payload, 80)}\n`);
633
778
 
779
+ const replyHint = sourcePid
780
+ ? ` ay send ${sourcePid} "..." # reply to sender\n`
781
+ : "";
634
782
  process.stderr.write(
635
783
  `\n` +
636
- ` cy tail ${record.pid} # watch output\n` +
637
- ` cy ls # list all agents\n`,
784
+ replyHint +
785
+ ` ay tail ${record.pid} # watch output\n` +
786
+ ` ay ls # list all agents\n`,
638
787
  );
639
788
  return 0;
640
789
  }
@@ -702,7 +851,7 @@ async function writeToIpc(ipcPath: string, payload: string): Promise<void> {
702
851
  }
703
852
 
704
853
  // ---------------------------------------------------------------------------
705
- // cy restart
854
+ // ay restart
706
855
  // ---------------------------------------------------------------------------
707
856
 
708
857
  async function cmdRestart(rest: string[]): Promise<number> {
@@ -712,7 +861,7 @@ async function cmdRestart(rest: string[]): Promise<number> {
712
861
  const record = await resolveOne(keyword, opts);
713
862
 
714
863
  if (isPidAlive(record.pid)) {
715
- 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`);
716
865
  return 1;
717
866
  }
718
867
 
@@ -730,14 +879,14 @@ async function cmdRestart(rest: string[]): Promise<number> {
730
879
  );
731
880
  process.stderr.write(
732
881
  `\n` +
733
- ` cy tail ${proc.pid} # watch output\n` +
734
- ` cy ls # list all agents\n`,
882
+ ` ay tail ${proc.pid} # watch output\n` +
883
+ ` ay ls # list all agents\n`,
735
884
  );
736
885
  return 0;
737
886
  }
738
887
 
739
888
  // ---------------------------------------------------------------------------
740
- // cy note
889
+ // ay note
741
890
  // ---------------------------------------------------------------------------
742
891
 
743
892
  async function cmdNote(rest: string[]): Promise<number> {
@@ -746,7 +895,7 @@ async function cmdNote(rest: string[]): Promise<number> {
746
895
  const keyword = positional[0];
747
896
  const note = positional.slice(1).join(" ");
748
897
 
749
- 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)');
750
899
 
751
900
  const record = await resolveOne(keyword, { ...opts, all: true });
752
901
 
@@ -760,6 +909,114 @@ async function cmdNote(rest: string[]): Promise<number> {
760
909
 
761
910
  await writeNote(record.pid, note);
762
911
  process.stdout.write(`note set for pid ${record.pid}: ${note}\n`);
763
- 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
+
764
1021
  return 0;
765
1022
  }