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/dist/{SUPPORTED_CLIS-Dzx7za7u.js → SUPPORTED_CLIS-CawnsTw2.js} +3 -3
- package/dist/cli.js +8 -3
- package/dist/index.js +2 -2
- package/dist/{subcommands-Vt_yQiEZ.js → subcommands-BwWcA9uo.js} +257 -99
- package/dist/{ts-WFsbtrbl.js → ts-D0ddYVke.js} +17 -3
- package/dist/{versionChecker-DdnBAwJe.js → versionChecker-ftOiNICT.js} +2 -2
- package/package.json +1 -1
- package/ts/index.ts +21 -0
- package/ts/parseCliArgs.ts +7 -0
- package/ts/subcommands.spec.ts +478 -35
- package/ts/subcommands.ts +348 -129
package/ts/subcommands.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `
|
|
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 `
|
|
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(`
|
|
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
|
-
//
|
|
272
|
+
// ay ls
|
|
326
273
|
// ---------------------------------------------------------------------------
|
|
327
274
|
|
|
328
275
|
async function cmdLs(rest: string[]): Promise<number> {
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
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
|
|
377
|
+
label = truncate(activity ?? (r.prompt ? `→ ${r.prompt}` : ""), promptBudget);
|
|
387
378
|
} else {
|
|
388
|
-
label = truncate(r.prompt
|
|
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(`
|
|
433
|
-
hints.push(`
|
|
434
|
-
hints.push(`
|
|
435
|
-
hints.push(`
|
|
436
|
-
hints.push(
|
|
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(`
|
|
437
|
+
hints.push(` ay restart ${stopped.pid} # restart stopped agent\n`);
|
|
440
438
|
}
|
|
441
439
|
if (!alive && !stopped)
|
|
442
|
-
hints.push(`
|
|
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
|
-
//
|
|
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
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
`
|
|
548
|
-
`
|
|
549
|
-
`
|
|
550
|
-
`
|
|
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
|
-
//
|
|
743
|
+
// ay send
|
|
709
744
|
// ---------------------------------------------------------------------------
|
|
710
745
|
|
|
711
746
|
async function cmdSend(rest: string[]): Promise<number> {
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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:
|
|
773
|
+
throw new Error("usage: ay send <keyword> <msg|-> [--code=enter|esc|ctrl-c|ctrl-y|tab|none]");
|
|
719
774
|
|
|
720
|
-
const codeName =
|
|
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
|
-
|
|
741
|
-
|
|
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,
|
|
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
|
-
|
|
753
|
-
`
|
|
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
|
-
//
|
|
886
|
+
// ay restart
|
|
822
887
|
// ---------------------------------------------------------------------------
|
|
823
888
|
|
|
824
889
|
async function cmdRestart(rest: string[]): Promise<number> {
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
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
|
|
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
|
-
`
|
|
850
|
-
`
|
|
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
|
-
//
|
|
935
|
+
// ay note
|
|
857
936
|
// ---------------------------------------------------------------------------
|
|
858
937
|
|
|
859
938
|
async function cmdNote(rest: string[]): Promise<number> {
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
const
|
|
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
|
|
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
|
}
|