agent-yes 1.83.0 → 1.85.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.
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
@@ -15,6 +15,7 @@ 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";
18
+ import yargs from "yargs";
18
19
 
19
20
  // ---------------------------------------------------------------------------
20
21
  // notes store (~/.agent-yes/notes.jsonl)
@@ -66,7 +67,7 @@ async function compactNotes(): Promise<void> {
66
67
  /**
67
68
  * Read the per-cwd TS PidStore JSONL and convert to the global record shape,
68
69
  * 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`.
70
+ * shipped still show up in `ay ls`. Merging is done in `mergeRecords`.
70
71
  */
71
72
  async function readLocalTsPids(cwd: string): Promise<GlobalPidRecord[]> {
72
73
  const jsonlPath = path.join(cwd, ".agent-yes", "pid-records.jsonl");
@@ -127,6 +128,7 @@ const SUBCOMMANDS = new Set([
127
128
  "ls",
128
129
  "list",
129
130
  "ps",
131
+ "status",
130
132
  "read",
131
133
  "cat",
132
134
  "tail",
@@ -136,6 +138,8 @@ const SUBCOMMANDS = new Set([
136
138
  "note",
137
139
  ]);
138
140
 
141
+ const IDLE_THRESHOLD_MS = 60 * 1000;
142
+
139
143
  export function isSubcommand(name: string | undefined): boolean {
140
144
  return !!name && SUBCOMMANDS.has(name);
141
145
  }
@@ -156,6 +160,8 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
156
160
  case "list":
157
161
  case "ps":
158
162
  return await cmdLs(rest);
163
+ case "status":
164
+ return await cmdStatus(rest);
159
165
  case "read":
160
166
  case "cat":
161
167
  return await cmdRead(rest, { mode: "cat" });
@@ -174,7 +180,7 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
174
180
  }
175
181
  } catch (err) {
176
182
  const msg = err instanceof Error ? err.message : String(err);
177
- process.stderr.write(`cy ${sub}: ${msg}\n`);
183
+ process.stderr.write(`ay ${sub}: ${msg}\n`);
178
184
  return 1;
179
185
  }
180
186
  }
@@ -191,65 +197,6 @@ interface CommonOpts {
191
197
  json: boolean;
192
198
  }
193
199
 
194
- interface ParsedArgs {
195
- flags: Record<string, string | boolean>;
196
- positional: string[];
197
- }
198
-
199
- export function parseArgs(rest: string[]): ParsedArgs {
200
- const flags: Record<string, string | boolean> = {};
201
- const positional: string[] = [];
202
- for (let i = 0; i < rest.length; i++) {
203
- const arg = rest[i]!;
204
- if (arg.startsWith("--")) {
205
- const eq = arg.indexOf("=");
206
- if (eq >= 0) {
207
- flags[arg.slice(2, eq)] = arg.slice(eq + 1);
208
- } else {
209
- const key = arg.slice(2);
210
- const next = rest[i + 1];
211
- // Boolean flags: --all, --json, --latest
212
- if (
213
- ["all", "active", "follow", "json", "latest"].includes(key) ||
214
- !next ||
215
- next.startsWith("-")
216
- ) {
217
- flags[key] = true;
218
- } else {
219
- flags[key] = next;
220
- i++;
221
- }
222
- }
223
- } else if (arg.startsWith("-") && arg.length > 1) {
224
- // -n N short flag
225
- if (arg === "-n") {
226
- flags["n"] = rest[i + 1] ?? "";
227
- i++;
228
- } else {
229
- flags[arg.slice(1)] = true;
230
- }
231
- } else {
232
- positional.push(arg);
233
- }
234
- }
235
- return { flags, positional };
236
- }
237
-
238
- function commonOpts(flags: Record<string, string | boolean>): CommonOpts {
239
- return {
240
- all: !!flags.all,
241
- active: !!flags.active,
242
- cwdScope:
243
- typeof flags.cwd === "string"
244
- ? path.resolve(flags.cwd)
245
- : flags.cwd === true
246
- ? process.cwd()
247
- : null,
248
- latest: !!flags.latest,
249
- json: !!flags.json,
250
- };
251
- }
252
-
253
200
  export function matchKeyword(record: GlobalPidRecord, keyword: string): boolean {
254
201
  if (!keyword) return true;
255
202
  const kw = keyword.toLowerCase();
@@ -322,13 +269,58 @@ async function resolveOne(keyword: string | undefined, opts: CommonOpts): Promis
322
269
  }
323
270
 
324
271
  // ---------------------------------------------------------------------------
325
- // cy ls
272
+ // ay ls
326
273
  // ---------------------------------------------------------------------------
327
274
 
328
275
  async function cmdLs(rest: string[]): Promise<number> {
329
- const { flags, positional } = parseArgs(rest);
330
- const opts = commonOpts(flags);
331
- const keyword = positional[0];
276
+ const y = yargs(rest)
277
+ .usage(
278
+ "Usage: ay ls [keyword] [options]\n" +
279
+ " ay list [keyword] [options]\n" +
280
+ " ay ps [keyword] [options]\n\n" +
281
+ "List running agents. Optionally filter by keyword (pid, cwd substring, or prompt substring).",
282
+ )
283
+ .option("all", {
284
+ type: "boolean",
285
+ default: false,
286
+ description: "Show all agents including exited ones",
287
+ })
288
+ .option("active", {
289
+ type: "boolean",
290
+ default: false,
291
+ description: "Only show agents with an alive process",
292
+ })
293
+ .option("json", { type: "boolean", default: false, description: "Output as JSON array" })
294
+ .option("latest", {
295
+ type: "boolean",
296
+ default: false,
297
+ description: "Show only the most recent agent",
298
+ })
299
+ .option("cwd", { type: "string", description: "Restrict to agents whose cwd starts with dir" })
300
+ .option("help", { alias: "h", type: "boolean", default: false, description: "Show this help" })
301
+ .example("ay ls", "list running agents")
302
+ .example("ay ls --all", "include exited agents")
303
+ .example("ay ls --json", "machine-readable output")
304
+ .example("ay ls symval", "filter by cwd/prompt keyword")
305
+ .help(false)
306
+ .version(false)
307
+ .exitProcess(false);
308
+
309
+ const argv = await y.parseAsync();
310
+
311
+ if (argv.help || argv.h) {
312
+ process.stdout.write((await y.getHelp()) + "\n");
313
+ return 0;
314
+ }
315
+
316
+ const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
317
+ const opts: CommonOpts = {
318
+ all: argv.all,
319
+ active: argv.active,
320
+ json: argv.json,
321
+ latest: argv.latest,
322
+ cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null,
323
+ };
332
324
  const records = await listRecords(keyword, opts);
333
325
 
334
326
  if (opts.json) {
@@ -359,7 +351,6 @@ async function cmdLs(rest: string[]): Promise<number> {
359
351
  const fixedWidth = widths.pid + widths.cli + widths.status + widths.age + widths.cwd + 5 * 2; // 5 separators of " "
360
352
  const promptBudget = Math.max(20, termWidth - fixedWidth - 1);
361
353
 
362
- const IDLE_THRESHOLD_MS = 60 * 1000;
363
354
  const notes = await readNotes();
364
355
  const rows = await Promise.all(
365
356
  records.map(async (r) => {
@@ -383,9 +374,9 @@ async function cmdLs(rest: string[]): Promise<number> {
383
374
  hasNote = true;
384
375
  } else if (r.log_file && displayStatus !== "stopped") {
385
376
  const activity = await extractActivity(r.log_file);
386
- label = truncate(activity ?? r.prompt ?? "", promptBudget);
377
+ label = truncate(activity ?? (r.prompt ? `→ ${r.prompt}` : ""), promptBudget);
387
378
  } else {
388
- label = truncate(r.prompt ?? "", promptBudget);
379
+ label = truncate(r.prompt ? `→ ${r.prompt}` : "", promptBudget);
389
380
  }
390
381
  return {
391
382
  pid: String(r.pid),
@@ -429,17 +420,24 @@ async function cmdLs(rest: string[]): Promise<number> {
429
420
  const stopped = rows.find((r) => !r._alive);
430
421
  const hints: string[] = ["\n"];
431
422
  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`);
423
+ hints.push(` ay status ${alive.pid} # JSON status snapshot\n`);
424
+ hints.push(` ay status ${alive.pid} --watch # stream changes as JSON\n`);
425
+ hints.push(` ay tail ${alive.pid} # view latest output\n`);
426
+ hints.push(` ay tail -f ${alive.pid} # follow live output\n`);
427
+ hints.push(
428
+ ` ay send ${alive.pid} "next: ..." # send a prompt (keyword: pid, cwd, or prompt substring)\n`,
429
+ );
430
+ hints.push(` ay send ${alive.pid} "" --code=ctrl-c # interrupt\n`);
431
+ hints.push(` ay note ${alive.pid} "what it's doing" # set a note\n`);
432
+ hints.push(
433
+ ` ay ls --json # machine-readable list for scripts/agents\n`,
434
+ );
437
435
  }
438
436
  if (stopped) {
439
- hints.push(` cy restart ${stopped.pid} # restart stopped agent\n`);
437
+ hints.push(` ay restart ${stopped.pid} # restart stopped agent\n`);
440
438
  }
441
439
  if (!alive && !stopped)
442
- hints.push(` cy ls --all # show exited agents\n`);
440
+ hints.push(` ay ls --all # show exited agents\n`);
443
441
  process.stderr.write(hints.join(""));
444
442
  }
445
443
 
@@ -469,7 +467,7 @@ function truncate(s: string, n: number): string {
469
467
  }
470
468
 
471
469
  // ---------------------------------------------------------------------------
472
- // cy read / cat / tail / head
470
+ // ay read / cat / tail / head
473
471
  // ---------------------------------------------------------------------------
474
472
 
475
473
  interface ReadOpts {
@@ -477,12 +475,37 @@ interface ReadOpts {
477
475
  }
478
476
 
479
477
  async function cmdRead(rest: string[], { mode }: ReadOpts): Promise<number> {
480
- const { flags, positional } = parseArgs(rest);
481
- const opts = commonOpts(flags);
482
- const keyword = positional[0];
483
- const follow = !!(flags.f || flags.follow);
484
-
485
- const nFlag = typeof flags.n === "string" ? Number(flags.n) : undefined;
478
+ const y = yargs(rest)
479
+ .usage("Usage: ay read/cat/tail/head <keyword> [options]")
480
+ .option("follow", {
481
+ alias: "f",
482
+ type: "boolean",
483
+ default: false,
484
+ description: "Follow log output (Ctrl-C to stop)",
485
+ })
486
+ .option("n", { type: "number", description: "Number of lines (default: 96 for tail/head)" })
487
+ .option("all", { type: "boolean", default: false, description: "Include exited agents" })
488
+ .option("latest", {
489
+ type: "boolean",
490
+ default: false,
491
+ description: "Use most recent match when multiple match",
492
+ })
493
+ .option("cwd", { type: "string", description: "Restrict to agents under this dir" })
494
+ .help(false)
495
+ .version(false)
496
+ .exitProcess(false);
497
+
498
+ const argv = await y.parseAsync();
499
+ const opts: CommonOpts = {
500
+ all: argv.all,
501
+ active: false,
502
+ json: false,
503
+ latest: argv.latest,
504
+ cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null,
505
+ };
506
+ const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
507
+ const follow = argv.follow;
508
+ const nFlag = argv.n;
486
509
  const n =
487
510
  nFlag !== undefined && Number.isFinite(nFlag) && nFlag > 0
488
511
  ? Math.floor(nFlag)
@@ -544,10 +567,10 @@ async function cmdRead(rest: string[], { mode }: ReadOpts): Promise<number> {
544
567
 
545
568
  process.stderr.write(
546
569
  `\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`,
570
+ ` ay ls # list all agents\n` +
571
+ ` ay tail -f ${record.pid} # follow live output\n` +
572
+ ` ay send ${record.pid} "next: ..." # send a prompt\n` +
573
+ ` ay send ${record.pid} "" --code=ctrl-c # interrupt\n`,
551
574
  );
552
575
  return 0;
553
576
  }
@@ -656,26 +679,38 @@ function extractActivityFromLines(lines: string[]): string | null {
656
679
 
657
680
  const clean = lines.filter((l) => !isChrome(l));
658
681
 
682
+ const isSpinnerLine = (l: string) =>
683
+ /^[^\w\s❯>⎿✓✗]\s+[A-Z]\w+[….]/u.test(l.trim()) || /still thinking/i.test(l);
684
+
685
+ // Find positions of the last ❯ prompt and last spinner in the rendered output.
686
+ // If ❯ comes after the last spinner, the agent finished and is waiting — show
687
+ // idle state rather than the stale spinner description.
688
+ let lastPromptIdx = -1;
689
+ let lastSpinnerIdx = -1;
690
+ for (let i = clean.length - 1; i >= 0; i--) {
691
+ const l = clean[i]!.trim();
692
+ if (lastPromptIdx === -1 && l.startsWith("❯")) lastPromptIdx = i;
693
+ if (lastSpinnerIdx === -1 && isSpinnerLine(l)) lastSpinnerIdx = i;
694
+ if (lastPromptIdx !== -1 && lastSpinnerIdx !== -1) break;
695
+ }
696
+
697
+ // ❯ appears after (or without) any spinner → agent is idle/waiting for input
698
+ if (lastPromptIdx > lastSpinnerIdx) {
699
+ const text = clean[lastPromptIdx]!.trim()
700
+ .replace(/^❯\s*/, "")
701
+ .trim();
702
+ return text ? `» ${text}` : null;
703
+ }
704
+
659
705
  // Priority 1: thinking/composing spinner active
660
706
  // Claude Code cycles through various Unicode dingbats for its spinner (✢✳✶✻✷…).
661
707
  // The format is always: SPINNER_CHAR Verb… (timing…)
662
708
  // Require ellipsis after the verb so we don't false-positive on normal text
663
709
  // 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
- );
710
+ const thinkingLine = clean.find((l) => isSpinnerLine(l));
667
711
  if (thinkingLine) {
668
712
  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}`;
713
+ return m?.[1] ? `✳ ${m[1].trim()}` : "thinking…";
679
714
  }
680
715
 
681
716
  // Priority 3: ✻ spinner just finished — show nearby context
@@ -705,19 +740,39 @@ function extractActivityFromLines(lines: string[]): string | null {
705
740
  }
706
741
 
707
742
  // ---------------------------------------------------------------------------
708
- // cy send
743
+ // ay send
709
744
  // ---------------------------------------------------------------------------
710
745
 
711
746
  async function cmdSend(rest: string[]): Promise<number> {
712
- const { flags, positional } = parseArgs(rest);
713
- const opts = commonOpts(flags);
714
- const keyword = positional[0];
715
- const rawMessage = positional.slice(1).join(" ");
747
+ const y = yargs(rest)
748
+ .usage("Usage: ay send <keyword> <msg|-> [options]")
749
+ .option("code", {
750
+ type: "string",
751
+ default: "enter",
752
+ description: "Trailing control code (enter|esc|ctrl-c|ctrl-y|tab|none)",
753
+ })
754
+ .option("all", { type: "boolean", default: false, description: "Include exited agents" })
755
+ .option("latest", { type: "boolean", default: false, description: "Use most recent match" })
756
+ .option("cwd", { type: "string", description: "Restrict to agents under this dir" })
757
+ .help(false)
758
+ .version(false)
759
+ .exitProcess(false);
760
+
761
+ const argv = await y.parseAsync();
762
+ const opts: CommonOpts = {
763
+ all: argv.all,
764
+ active: false,
765
+ json: false,
766
+ latest: argv.latest,
767
+ cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null,
768
+ };
769
+ const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
770
+ const rawMessage = argv._.slice(1).map(String).join(" ");
716
771
 
717
772
  if (!keyword)
718
- throw new Error("usage: cy send <keyword> <msg|-> [--code=enter|esc|ctrl-c|ctrl-y|tab|none]");
773
+ throw new Error("usage: ay send <keyword> <msg|-> [--code=enter|esc|ctrl-c|ctrl-y|tab|none]");
719
774
 
720
- const codeName = typeof flags.code === "string" ? flags.code.toLowerCase() : "enter";
775
+ const codeName = argv.code.toLowerCase();
721
776
  const trailing = controlCodeFromName(codeName);
722
777
 
723
778
  const record = await resolveOne(keyword, opts);
@@ -737,20 +792,30 @@ async function cmdSend(rest: string[]): Promise<number> {
737
792
  body = rawMessage;
738
793
  }
739
794
 
740
- if (body && trailing) {
741
- await writeToIpc(fifoPath, body);
795
+ const sourcePid = process.env.AGENT_YES_PID ? Number(process.env.AGENT_YES_PID) : null;
796
+ const talkBack = sourcePid
797
+ ? `\n(from AGENT_YES_PID=${sourcePid} — reply: ay send ${sourcePid} "...")`
798
+ : "";
799
+
800
+ const fullBody = body + talkBack;
801
+ if (fullBody && trailing) {
802
+ await writeToIpc(fifoPath, fullBody);
742
803
  await new Promise((r) => setTimeout(r, 200));
743
804
  await writeToIpc(fifoPath, trailing);
744
805
  } else {
745
- await writeToIpc(fifoPath, body + trailing);
806
+ await writeToIpc(fifoPath, fullBody + trailing);
746
807
  }
747
808
  const payload = body + trailing;
748
809
  process.stdout.write(`sent to pid ${record.pid} (${record.cli}): ${truncate(payload, 80)}\n`);
749
810
 
811
+ const replyHint = sourcePid
812
+ ? ` ay send ${sourcePid} "..." # reply to sender\n`
813
+ : "";
750
814
  process.stderr.write(
751
815
  `\n` +
752
- ` cy tail ${record.pid} # watch output\n` +
753
- ` cy ls # list all agents\n`,
816
+ replyHint +
817
+ ` ay tail ${record.pid} # watch output\n` +
818
+ ` ay ls # list all agents\n`,
754
819
  );
755
820
  return 0;
756
821
  }
@@ -818,17 +883,31 @@ async function writeToIpc(ipcPath: string, payload: string): Promise<void> {
818
883
  }
819
884
 
820
885
  // ---------------------------------------------------------------------------
821
- // cy restart
886
+ // ay restart
822
887
  // ---------------------------------------------------------------------------
823
888
 
824
889
  async function cmdRestart(rest: string[]): Promise<number> {
825
- const { flags, positional } = parseArgs(rest);
826
- const opts = { ...commonOpts(flags), all: true }; // search stopped agents too
827
- const keyword = positional[0];
890
+ const y = yargs(rest)
891
+ .usage("Usage: ay restart <keyword>")
892
+ .option("latest", { type: "boolean", default: false, description: "Use most recent match" })
893
+ .option("cwd", { type: "string", description: "Restrict to agents under this dir" })
894
+ .help(false)
895
+ .version(false)
896
+ .exitProcess(false);
897
+
898
+ const argv = await y.parseAsync();
899
+ const opts: CommonOpts = {
900
+ all: true,
901
+ active: false,
902
+ json: false,
903
+ latest: argv.latest,
904
+ cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null,
905
+ };
906
+ const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
828
907
  const record = await resolveOne(keyword, opts);
829
908
 
830
909
  if (isPidAlive(record.pid)) {
831
- process.stderr.write(`pid ${record.pid} is still running — stop it first or use cy send\n`);
910
+ process.stderr.write(`pid ${record.pid} is still running — stop it first or use ay send\n`);
832
911
  return 1;
833
912
  }
834
913
 
@@ -846,25 +925,36 @@ async function cmdRestart(rest: string[]): Promise<number> {
846
925
  );
847
926
  process.stderr.write(
848
927
  `\n` +
849
- ` cy tail ${proc.pid} # watch output\n` +
850
- ` cy ls # list all agents\n`,
928
+ ` ay tail ${proc.pid} # watch output\n` +
929
+ ` ay ls # list all agents\n`,
851
930
  );
852
931
  return 0;
853
932
  }
854
933
 
855
934
  // ---------------------------------------------------------------------------
856
- // cy note
935
+ // ay note
857
936
  // ---------------------------------------------------------------------------
858
937
 
859
938
  async function cmdNote(rest: string[]): Promise<number> {
860
- const { flags, positional } = parseArgs(rest);
861
- const opts = commonOpts(flags);
862
- const keyword = positional[0];
863
- const note = positional.slice(1).join(" ");
864
-
865
- if (!keyword) throw new Error('usage: cy note <keyword> ["note text"] (omit text to clear)');
866
-
867
- const record = await resolveOne(keyword, { ...opts, all: true });
939
+ const y = yargs(rest)
940
+ .usage('Usage: ay note <keyword> ["note text"]')
941
+ .help(false)
942
+ .version(false)
943
+ .exitProcess(false);
944
+
945
+ const argv = await y.parseAsync();
946
+ const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
947
+ const note = argv._.slice(1).map(String).join(" ");
948
+
949
+ if (!keyword) throw new Error('usage: ay note <keyword> ["note text"] (omit text to clear)');
950
+
951
+ const record = await resolveOne(keyword, {
952
+ all: true,
953
+ active: false,
954
+ json: false,
955
+ latest: false,
956
+ cwdScope: null,
957
+ });
868
958
 
869
959
  if (!note) {
870
960
  // clear
@@ -876,6 +966,135 @@ async function cmdNote(rest: string[]): Promise<number> {
876
966
 
877
967
  await writeNote(record.pid, note);
878
968
  process.stdout.write(`note set for pid ${record.pid}: ${note}\n`);
879
- process.stderr.write(`\n cy ls # see updated note in list\n`);
969
+ process.stderr.write(`\n ay ls # see updated note in list\n`);
970
+ return 0;
971
+ }
972
+
973
+ // ---------------------------------------------------------------------------
974
+ // ay status
975
+ // ---------------------------------------------------------------------------
976
+
977
+ interface StatusSnapshot {
978
+ pid: number;
979
+ cli: string;
980
+ cwd: string;
981
+ state: "active" | "idle" | "stopped";
982
+ activity: string | null;
983
+ note: string | null;
984
+ log_mtime_ms: number | null;
985
+ started_at: number;
986
+ age_ms: number;
987
+ exit_code: number | null;
988
+ exit_reason: string | null;
989
+ log_file: string | null;
990
+ }
991
+
992
+ async function snapshotStatus(record: GlobalPidRecord): Promise<StatusSnapshot> {
993
+ const alive = isPidAlive(record.pid);
994
+ let state: "active" | "idle" | "stopped";
995
+ let logMtimeMs: number | null = null;
996
+ if (!alive) {
997
+ state = "stopped";
998
+ } else if (record.log_file) {
999
+ logMtimeMs = await stat(record.log_file)
1000
+ .then((s) => s.mtimeMs)
1001
+ .catch(() => null);
1002
+ state = logMtimeMs !== null && Date.now() - logMtimeMs > IDLE_THRESHOLD_MS ? "idle" : "active";
1003
+ } else {
1004
+ state = "active";
1005
+ }
1006
+ const activity =
1007
+ state !== "stopped" && record.log_file ? await extractActivity(record.log_file) : null;
1008
+ const notes = await readNotes();
1009
+ const note = notes.get(record.pid) ?? null;
1010
+ return {
1011
+ pid: record.pid,
1012
+ cli: record.cli,
1013
+ cwd: record.cwd,
1014
+ state,
1015
+ activity,
1016
+ note,
1017
+ log_mtime_ms: logMtimeMs,
1018
+ started_at: record.started_at,
1019
+ age_ms: Date.now() - record.started_at,
1020
+ exit_code: record.exit_code,
1021
+ exit_reason: record.exit_reason,
1022
+ log_file: record.log_file ?? null,
1023
+ };
1024
+ }
1025
+
1026
+ async function cmdStatus(rest: string[]): Promise<number> {
1027
+ const y = yargs(rest)
1028
+ .usage("Usage: ay status <keyword> [options]")
1029
+ .option("watch", {
1030
+ alias: "w",
1031
+ type: "boolean",
1032
+ default: false,
1033
+ description: "Stream changes as JSON",
1034
+ })
1035
+ .option("interval", { type: "number", default: 2, description: "Poll interval in seconds" })
1036
+ .option("latest", { type: "boolean", default: false, description: "Use most recent match" })
1037
+ .option("cwd", { type: "string", description: "Restrict to agents under this dir" })
1038
+ .help(false)
1039
+ .version(false)
1040
+ .exitProcess(false);
1041
+
1042
+ const argv = await y.parseAsync();
1043
+ const opts: CommonOpts = {
1044
+ all: true,
1045
+ active: false,
1046
+ json: false,
1047
+ latest: argv.latest,
1048
+ cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null,
1049
+ };
1050
+ const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
1051
+
1052
+ if (!keyword) throw new Error("usage: ay status <keyword> [--watch] [--interval=N]");
1053
+
1054
+ const watch = argv.watch;
1055
+ const intervalFlag = argv.interval;
1056
+ const intervalMs = Math.max(500, (Number.isFinite(intervalFlag) ? intervalFlag : 2) * 1000);
1057
+
1058
+ const record = await resolveOne(keyword, opts);
1059
+
1060
+ const emit = (snap: StatusSnapshot, ts?: number): void => {
1061
+ const out = ts !== undefined ? { ts, ...snap } : snap;
1062
+ process.stdout.write(JSON.stringify(out) + "\n");
1063
+ };
1064
+
1065
+ if (!watch) {
1066
+ emit(await snapshotStatus(record));
1067
+ return 0;
1068
+ }
1069
+
1070
+ process.stderr.write(
1071
+ `watching pid ${record.pid} every ${intervalMs / 1000}s… (Ctrl-C to stop)\n`,
1072
+ );
1073
+
1074
+ let prev: { state: string; activity: string | null; exit_code: number | null } | null = null;
1075
+
1076
+ const tick = async (): Promise<void> => {
1077
+ const snap = await snapshotStatus(record);
1078
+ if (
1079
+ prev === null ||
1080
+ snap.state !== prev.state ||
1081
+ snap.activity !== prev.activity ||
1082
+ snap.exit_code !== prev.exit_code
1083
+ ) {
1084
+ emit(snap, Date.now());
1085
+ prev = { state: snap.state, activity: snap.activity, exit_code: snap.exit_code };
1086
+ }
1087
+ };
1088
+
1089
+ await tick();
1090
+
1091
+ await new Promise<void>((resolve) => {
1092
+ const timer = setInterval(tick, intervalMs);
1093
+ process.on("SIGINT", () => {
1094
+ clearInterval(timer);
1095
+ resolve();
1096
+ });
1097
+ });
1098
+
880
1099
  return 0;
881
1100
  }