agent-yes 1.85.0 → 1.87.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-CawnsTw2.js → SUPPORTED_CLIS-DfGbBLZy.js} +3 -3
- package/dist/cli.js +5 -5
- package/dist/index.js +2 -2
- package/dist/remotes-CFrho898.js +131 -0
- package/dist/remotes-kfUzk-JT.js +3 -0
- package/dist/serve-8dWQHSBu.js +303 -0
- package/dist/subcommands-B2lxwpQn.js +6 -0
- package/dist/{subcommands-BwWcA9uo.js → subcommands-BltyYaEs.js} +332 -7
- package/dist/{tray-CH_G7aXM.js → tray-DHuD0nEk.js} +1 -1
- package/dist/{ts-D0ddYVke.js → ts-CSMhtgoi.js} +2 -2
- package/dist/{versionChecker-ftOiNICT.js → versionChecker-gyE00XVe.js} +2 -2
- package/package.json +1 -1
- package/ts/remotes.ts +161 -0
- package/ts/serve.ts +373 -0
- package/ts/subcommands.spec.ts +103 -0
- package/ts/subcommands.ts +421 -10
package/ts/subcommands.ts
CHANGED
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { appendFile, mkdir, open, readFile, stat, writeFile } from "fs/promises";
|
|
15
|
+
import ms from "ms";
|
|
15
16
|
import { homedir } from "os";
|
|
16
17
|
import path from "path";
|
|
17
18
|
import { type GlobalPidRecord, readGlobalPids } from "./globalPidIndex.ts";
|
|
18
19
|
import yargs from "yargs";
|
|
20
|
+
import { type ResolvedRemote, readRemotes, resolveRemoteSpec } from "./remotes.ts";
|
|
19
21
|
|
|
20
22
|
// ---------------------------------------------------------------------------
|
|
21
23
|
// notes store (~/.agent-yes/notes.jsonl)
|
|
@@ -26,7 +28,7 @@ function notesPath(): string {
|
|
|
26
28
|
return path.join(dir, "notes.jsonl");
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
async function readNotes(): Promise<Map<number, string>> {
|
|
31
|
+
export async function readNotes(): Promise<Map<number, string>> {
|
|
30
32
|
let raw: string;
|
|
31
33
|
try {
|
|
32
34
|
raw = await readFile(notesPath(), "utf-8");
|
|
@@ -136,6 +138,8 @@ const SUBCOMMANDS = new Set([
|
|
|
136
138
|
"send",
|
|
137
139
|
"restart",
|
|
138
140
|
"note",
|
|
141
|
+
"serve",
|
|
142
|
+
"remote",
|
|
139
143
|
]);
|
|
140
144
|
|
|
141
145
|
const IDLE_THRESHOLD_MS = 60 * 1000;
|
|
@@ -175,6 +179,14 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
|
|
|
175
179
|
return await cmdRestart(rest);
|
|
176
180
|
case "note":
|
|
177
181
|
return await cmdNote(rest);
|
|
182
|
+
case "serve": {
|
|
183
|
+
const { cmdServe } = await import("./serve.ts");
|
|
184
|
+
return cmdServe(rest);
|
|
185
|
+
}
|
|
186
|
+
case "remote": {
|
|
187
|
+
const { cmdRemote } = await import("./remotes.ts");
|
|
188
|
+
return cmdRemote(rest);
|
|
189
|
+
}
|
|
178
190
|
default:
|
|
179
191
|
return null;
|
|
180
192
|
}
|
|
@@ -189,7 +201,7 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
|
|
|
189
201
|
// shared helpers
|
|
190
202
|
// ---------------------------------------------------------------------------
|
|
191
203
|
|
|
192
|
-
interface CommonOpts {
|
|
204
|
+
export interface CommonOpts {
|
|
193
205
|
all: boolean;
|
|
194
206
|
active: boolean;
|
|
195
207
|
cwdScope: string | null;
|
|
@@ -211,7 +223,7 @@ export function matchKeyword(record: GlobalPidRecord, keyword: string): boolean
|
|
|
211
223
|
return false;
|
|
212
224
|
}
|
|
213
225
|
|
|
214
|
-
async function listRecords(
|
|
226
|
+
export async function listRecords(
|
|
215
227
|
keyword: string | undefined,
|
|
216
228
|
opts: CommonOpts,
|
|
217
229
|
): Promise<GlobalPidRecord[]> {
|
|
@@ -240,7 +252,7 @@ async function listRecords(
|
|
|
240
252
|
return records;
|
|
241
253
|
}
|
|
242
254
|
|
|
243
|
-
function isPidAlive(pid: number): boolean {
|
|
255
|
+
export function isPidAlive(pid: number): boolean {
|
|
244
256
|
try {
|
|
245
257
|
process.kill(pid, 0);
|
|
246
258
|
return true;
|
|
@@ -249,7 +261,10 @@ function isPidAlive(pid: number): boolean {
|
|
|
249
261
|
}
|
|
250
262
|
}
|
|
251
263
|
|
|
252
|
-
async function resolveOne(
|
|
264
|
+
export async function resolveOne(
|
|
265
|
+
keyword: string | undefined,
|
|
266
|
+
opts: CommonOpts,
|
|
267
|
+
): Promise<GlobalPidRecord> {
|
|
253
268
|
if (!keyword) {
|
|
254
269
|
throw new Error("keyword required (pid, cwd substring, cli name, or prompt substring)");
|
|
255
270
|
}
|
|
@@ -268,6 +283,319 @@ async function resolveOne(keyword: string | undefined, opts: CommonOpts): Promis
|
|
|
268
283
|
);
|
|
269
284
|
}
|
|
270
285
|
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// remote routing helpers
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
async function remoteGet(remote: ResolvedRemote, pathname: string): Promise<Response> {
|
|
291
|
+
return fetch(`${remote.url}${pathname}`, {
|
|
292
|
+
headers: { Authorization: `Bearer ${remote.token}` },
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function remotePost(
|
|
297
|
+
remote: ResolvedRemote,
|
|
298
|
+
pathname: string,
|
|
299
|
+
body: unknown,
|
|
300
|
+
): Promise<Response> {
|
|
301
|
+
return fetch(`${remote.url}${pathname}`, {
|
|
302
|
+
method: "POST",
|
|
303
|
+
headers: { Authorization: `Bearer ${remote.token}`, "Content-Type": "application/json" },
|
|
304
|
+
body: JSON.stringify(body),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function runRemoteLs(
|
|
309
|
+
remote: ResolvedRemote,
|
|
310
|
+
opts: { all: boolean; active: boolean },
|
|
311
|
+
): Promise<number> {
|
|
312
|
+
const params = new URLSearchParams();
|
|
313
|
+
if (remote.keyword) params.set("keyword", remote.keyword);
|
|
314
|
+
if (opts.all) params.set("all", "1");
|
|
315
|
+
if (opts.active) params.set("active", "1");
|
|
316
|
+
const res = await remoteGet(remote, `/api/ls?${params}`);
|
|
317
|
+
if (!res.ok) {
|
|
318
|
+
process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
|
|
319
|
+
return 1;
|
|
320
|
+
}
|
|
321
|
+
const records = (await res.json()) as any[];
|
|
322
|
+
if (records.length === 0) {
|
|
323
|
+
process.stderr.write(
|
|
324
|
+
remote.keyword
|
|
325
|
+
? `no agents matched "${remote.keyword}" on ${remote.url}\n`
|
|
326
|
+
: `no running agents on ${remote.url}\n`,
|
|
327
|
+
);
|
|
328
|
+
return 0;
|
|
329
|
+
}
|
|
330
|
+
process.stderr.write(`[remote ${remote.url}]\n`);
|
|
331
|
+
const termWidth = (process.stdout as any).columns ?? 120;
|
|
332
|
+
const widths = {
|
|
333
|
+
pid: Math.max(3, ...records.map((r: any) => String(r.pid).length)),
|
|
334
|
+
cli: Math.max(3, ...records.map((r: any) => String(r.cli).length)),
|
|
335
|
+
status: Math.max(6, ...records.map((r: any) => String(r.status).length)),
|
|
336
|
+
cwd: Math.max(3, ...records.map((r: any) => String(r.cwd).length)),
|
|
337
|
+
};
|
|
338
|
+
const fixedWidth = widths.pid + widths.cli + widths.status + widths.cwd + 4 * 2;
|
|
339
|
+
const promptBudget = Math.max(20, termWidth - fixedWidth - 1);
|
|
340
|
+
const header =
|
|
341
|
+
[
|
|
342
|
+
"PID".padEnd(widths.pid),
|
|
343
|
+
"CLI".padEnd(widths.cli),
|
|
344
|
+
"STATUS".padEnd(widths.status),
|
|
345
|
+
"CWD".padEnd(widths.cwd),
|
|
346
|
+
"PROMPT",
|
|
347
|
+
].join(" ") + "\n";
|
|
348
|
+
process.stdout.write(header);
|
|
349
|
+
for (const r of records) {
|
|
350
|
+
const label = r.prompt ? truncate(`→ ${r.prompt}`, promptBudget) : "";
|
|
351
|
+
process.stdout.write(
|
|
352
|
+
[
|
|
353
|
+
String(r.pid).padEnd(widths.pid),
|
|
354
|
+
String(r.cli).padEnd(widths.cli),
|
|
355
|
+
String(r.status).padEnd(widths.status),
|
|
356
|
+
String(r.cwd).padEnd(widths.cwd),
|
|
357
|
+
label,
|
|
358
|
+
].join(" ") + "\n",
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function runRemoteRead(
|
|
365
|
+
remote: ResolvedRemote,
|
|
366
|
+
mode: "cat" | "tail" | "head",
|
|
367
|
+
follow: boolean,
|
|
368
|
+
n: number,
|
|
369
|
+
reconnectTimeoutMs = 120_000,
|
|
370
|
+
): Promise<number> {
|
|
371
|
+
const keyword = remote.keyword ?? "";
|
|
372
|
+
if (!keyword) {
|
|
373
|
+
process.stderr.write(
|
|
374
|
+
"remote tail/cat/head requires a keyword (e.g. token@host:port:keyword)\n",
|
|
375
|
+
);
|
|
376
|
+
return 1;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (mode === "tail" && follow) {
|
|
380
|
+
const ac = new AbortController();
|
|
381
|
+
process.on("SIGINT", () => ac.abort());
|
|
382
|
+
const deadline = Date.now() + reconnectTimeoutMs;
|
|
383
|
+
let delay = 1_000;
|
|
384
|
+
let attempt = 0;
|
|
385
|
+
|
|
386
|
+
process.stderr.write(
|
|
387
|
+
`[remote ${remote.url} ${keyword}]\nfollowing... (Ctrl-C to stop, timeout: ${Math.round(reconnectTimeoutMs / 1000)}s)\n`,
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
while (!ac.signal.aborted) {
|
|
391
|
+
try {
|
|
392
|
+
const res = await fetch(`${remote.url}/api/tail/${encodeURIComponent(keyword)}`, {
|
|
393
|
+
headers: { Authorization: `Bearer ${remote.token}`, Accept: "text/event-stream" },
|
|
394
|
+
signal: ac.signal,
|
|
395
|
+
});
|
|
396
|
+
if (!res.ok) {
|
|
397
|
+
// 401/404 are permanent failures — no point retrying
|
|
398
|
+
if (res.status === 401 || res.status === 404) {
|
|
399
|
+
process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
|
|
400
|
+
return 1;
|
|
401
|
+
}
|
|
402
|
+
throw new Error(`HTTP ${res.status}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (attempt > 0) process.stderr.write("remote: reconnected\n");
|
|
406
|
+
delay = 1_000; // reset backoff on successful connect
|
|
407
|
+
|
|
408
|
+
const reader = res.body!.getReader();
|
|
409
|
+
const dec = new TextDecoder();
|
|
410
|
+
let buf = "";
|
|
411
|
+
while (true) {
|
|
412
|
+
const { done, value } = await reader.read();
|
|
413
|
+
if (done) break;
|
|
414
|
+
buf += dec.decode(value, { stream: true });
|
|
415
|
+
const lines = buf.split("\n");
|
|
416
|
+
buf = lines.pop() ?? "";
|
|
417
|
+
for (const line of lines) {
|
|
418
|
+
if (!line.startsWith("data: ")) continue;
|
|
419
|
+
try {
|
|
420
|
+
const text = JSON.parse(line.slice(6)) as string;
|
|
421
|
+
process.stdout.write(text);
|
|
422
|
+
if (!text.endsWith("\n")) process.stdout.write("\n");
|
|
423
|
+
} catch {
|
|
424
|
+
/* skip non-JSON */
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
break; // stream ended cleanly
|
|
429
|
+
} catch (e: any) {
|
|
430
|
+
if (e.name === "AbortError" || ac.signal.aborted) return 0;
|
|
431
|
+
if (Date.now() >= deadline) {
|
|
432
|
+
process.stderr.write(
|
|
433
|
+
`remote: timeout after ${Math.round(reconnectTimeoutMs / 1000)}s, giving up\n`,
|
|
434
|
+
);
|
|
435
|
+
return 1;
|
|
436
|
+
}
|
|
437
|
+
process.stderr.write(
|
|
438
|
+
`remote: disconnected (${e.message}), retrying in ${delay / 1000}s…\n`,
|
|
439
|
+
);
|
|
440
|
+
await new Promise<void>((resolve, reject) => {
|
|
441
|
+
const t = setTimeout(resolve, delay);
|
|
442
|
+
ac.signal.addEventListener("abort", () => {
|
|
443
|
+
clearTimeout(t);
|
|
444
|
+
reject(new Error("abort"));
|
|
445
|
+
});
|
|
446
|
+
}).catch(() => {});
|
|
447
|
+
if (ac.signal.aborted) return 0;
|
|
448
|
+
delay = Math.min(delay * 2, 30_000);
|
|
449
|
+
attempt++;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return 0;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Static read (cat/head/tail without -f)
|
|
456
|
+
const params = new URLSearchParams({ mode, n: String(n) });
|
|
457
|
+
const res = await remoteGet(remote, `/api/read/${encodeURIComponent(keyword)}?${params}`);
|
|
458
|
+
if (!res.ok) {
|
|
459
|
+
process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
|
|
460
|
+
return 1;
|
|
461
|
+
}
|
|
462
|
+
const text = await res.text();
|
|
463
|
+
process.stderr.write(`[remote ${remote.url} ${keyword}]\n`);
|
|
464
|
+
process.stdout.write(text);
|
|
465
|
+
if (!text.endsWith("\n")) process.stdout.write("\n");
|
|
466
|
+
return 0;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function runRemoteSend(remote: ResolvedRemote, msg: string, code: string): Promise<number> {
|
|
470
|
+
const keyword = remote.keyword ?? "";
|
|
471
|
+
if (!keyword) {
|
|
472
|
+
process.stderr.write("remote send requires a keyword (e.g. token@host:port:keyword)\n");
|
|
473
|
+
return 1;
|
|
474
|
+
}
|
|
475
|
+
const res = await remotePost(remote, "/api/send", { keyword, msg, code });
|
|
476
|
+
if (!res.ok) {
|
|
477
|
+
process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
|
|
478
|
+
return 1;
|
|
479
|
+
}
|
|
480
|
+
const data = (await res.json()) as any;
|
|
481
|
+
process.stdout.write(`sent to remote pid ${data.pid} (${remote.url} ${keyword})\n`);
|
|
482
|
+
return 0;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function runRemoteStatus(remote: ResolvedRemote): Promise<number> {
|
|
486
|
+
const keyword = remote.keyword ?? "";
|
|
487
|
+
if (!keyword) {
|
|
488
|
+
process.stderr.write("remote status requires a keyword (e.g. token@host:port:keyword)\n");
|
|
489
|
+
return 1;
|
|
490
|
+
}
|
|
491
|
+
const res = await remoteGet(remote, `/api/status/${encodeURIComponent(keyword)}`);
|
|
492
|
+
if (!res.ok) {
|
|
493
|
+
process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
|
|
494
|
+
return 1;
|
|
495
|
+
}
|
|
496
|
+
process.stdout.write(JSON.stringify(await res.json(), null, 2) + "\n");
|
|
497
|
+
return 0;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
// --all-remotes helpers
|
|
502
|
+
// ---------------------------------------------------------------------------
|
|
503
|
+
|
|
504
|
+
async function fetchRemoteRecordsRaw(
|
|
505
|
+
url: string,
|
|
506
|
+
token: string,
|
|
507
|
+
opts: { all: boolean; active: boolean; keyword?: string },
|
|
508
|
+
): Promise<any[]> {
|
|
509
|
+
const params = new URLSearchParams();
|
|
510
|
+
if (opts.all) params.set("all", "1");
|
|
511
|
+
if (opts.active) params.set("active", "1");
|
|
512
|
+
if (opts.keyword) params.set("keyword", opts.keyword);
|
|
513
|
+
try {
|
|
514
|
+
const res = await fetch(`${url}/api/ls?${params}`, {
|
|
515
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
516
|
+
signal: AbortSignal.timeout(5000),
|
|
517
|
+
});
|
|
518
|
+
if (!res.ok) return [];
|
|
519
|
+
return (await res.json()) as any[];
|
|
520
|
+
} catch {
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function runAllRemotesLs(opts: {
|
|
526
|
+
all: boolean;
|
|
527
|
+
active: boolean;
|
|
528
|
+
keyword?: string;
|
|
529
|
+
}): Promise<number> {
|
|
530
|
+
const remotes = await readRemotes();
|
|
531
|
+
const localOpts: CommonOpts = {
|
|
532
|
+
all: opts.all,
|
|
533
|
+
active: opts.active,
|
|
534
|
+
json: true,
|
|
535
|
+
latest: false,
|
|
536
|
+
cwdScope: null,
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const [localResult, ...remoteResults] = await Promise.allSettled([
|
|
540
|
+
listRecords(opts.keyword, localOpts).then((recs) => ({
|
|
541
|
+
host: "local",
|
|
542
|
+
records: recs as any[],
|
|
543
|
+
})),
|
|
544
|
+
...Array.from(remotes.entries()).map(([alias, cfg]) =>
|
|
545
|
+
fetchRemoteRecordsRaw(cfg.url, cfg.token, opts).then((records) => ({ host: alias, records })),
|
|
546
|
+
),
|
|
547
|
+
]);
|
|
548
|
+
|
|
549
|
+
type HostedRow = { host: string; rec: any };
|
|
550
|
+
const rows: HostedRow[] = [];
|
|
551
|
+
if (localResult.status === "fulfilled") {
|
|
552
|
+
for (const r of localResult.value.records) rows.push({ host: "local", rec: r });
|
|
553
|
+
}
|
|
554
|
+
for (const res of remoteResults) {
|
|
555
|
+
if (res.status === "fulfilled") {
|
|
556
|
+
for (const r of res.value.records) rows.push({ host: res.value.host, rec: r });
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (rows.length === 0) {
|
|
561
|
+
process.stderr.write("no running agents\n");
|
|
562
|
+
return 0;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const termWidth = (process.stdout as any).columns ?? 120;
|
|
566
|
+
const hostW = Math.max(4, ...rows.map((r) => r.host.length));
|
|
567
|
+
const pidW = Math.max(3, ...rows.map((r) => String(r.rec.pid).length));
|
|
568
|
+
const cliW = Math.max(3, ...rows.map((r) => String(r.rec.cli).length));
|
|
569
|
+
const statusW = Math.max(6, ...rows.map((r) => String(r.rec.status).length));
|
|
570
|
+
const cwdW = Math.max(3, ...rows.map((r) => shortenPath(String(r.rec.cwd)).length));
|
|
571
|
+
const promptBudget = Math.max(20, termWidth - hostW - pidW - cliW - statusW - cwdW - 5 * 2 - 1);
|
|
572
|
+
|
|
573
|
+
process.stdout.write(
|
|
574
|
+
[
|
|
575
|
+
"HOST".padEnd(hostW),
|
|
576
|
+
"PID".padEnd(pidW),
|
|
577
|
+
"CLI".padEnd(cliW),
|
|
578
|
+
"STATUS".padEnd(statusW),
|
|
579
|
+
"CWD".padEnd(cwdW),
|
|
580
|
+
"PROMPT",
|
|
581
|
+
].join(" ") + "\n",
|
|
582
|
+
);
|
|
583
|
+
for (const { host, rec } of rows) {
|
|
584
|
+
const label = rec.prompt ? truncate(`→ ${rec.prompt}`, promptBudget) : "";
|
|
585
|
+
process.stdout.write(
|
|
586
|
+
[
|
|
587
|
+
host.padEnd(hostW),
|
|
588
|
+
String(rec.pid).padEnd(pidW),
|
|
589
|
+
String(rec.cli).padEnd(cliW),
|
|
590
|
+
String(rec.status).padEnd(statusW),
|
|
591
|
+
shortenPath(String(rec.cwd)).padEnd(cwdW),
|
|
592
|
+
label,
|
|
593
|
+
].join(" ") + "\n",
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
return 0;
|
|
597
|
+
}
|
|
598
|
+
|
|
271
599
|
// ---------------------------------------------------------------------------
|
|
272
600
|
// ay ls
|
|
273
601
|
// ---------------------------------------------------------------------------
|
|
@@ -297,8 +625,14 @@ async function cmdLs(rest: string[]): Promise<number> {
|
|
|
297
625
|
description: "Show only the most recent agent",
|
|
298
626
|
})
|
|
299
627
|
.option("cwd", { type: "string", description: "Restrict to agents whose cwd starts with dir" })
|
|
628
|
+
.option("all-remotes", {
|
|
629
|
+
type: "boolean",
|
|
630
|
+
default: false,
|
|
631
|
+
description: "Include agents from all configured remotes (remotes.yaml)",
|
|
632
|
+
})
|
|
300
633
|
.option("help", { alias: "h", type: "boolean", default: false, description: "Show this help" })
|
|
301
634
|
.example("ay ls", "list running agents")
|
|
635
|
+
.example("ay ls --all-remotes", "include all configured remote machines")
|
|
302
636
|
.example("ay ls --all", "include exited agents")
|
|
303
637
|
.example("ay ls --json", "machine-readable output")
|
|
304
638
|
.example("ay ls symval", "filter by cwd/prompt keyword")
|
|
@@ -313,7 +647,19 @@ async function cmdLs(rest: string[]): Promise<number> {
|
|
|
313
647
|
return 0;
|
|
314
648
|
}
|
|
315
649
|
|
|
650
|
+
if (argv["all-remotes"]) {
|
|
651
|
+
return runAllRemotesLs({
|
|
652
|
+
all: argv.all,
|
|
653
|
+
active: argv.active,
|
|
654
|
+
keyword: argv._[0] !== undefined ? String(argv._[0]) : undefined,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
316
658
|
const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
|
|
659
|
+
if (keyword) {
|
|
660
|
+
const remote = await resolveRemoteSpec(keyword);
|
|
661
|
+
if (remote) return runRemoteLs(remote, { all: argv.all, active: argv.active });
|
|
662
|
+
}
|
|
317
663
|
const opts: CommonOpts = {
|
|
318
664
|
all: argv.all,
|
|
319
665
|
active: argv.active,
|
|
@@ -422,6 +768,7 @@ async function cmdLs(rest: string[]): Promise<number> {
|
|
|
422
768
|
if (alive) {
|
|
423
769
|
hints.push(` ay status ${alive.pid} # JSON status snapshot\n`);
|
|
424
770
|
hints.push(` ay status ${alive.pid} --watch # stream changes as JSON\n`);
|
|
771
|
+
hints.push(` ay status ${alive.pid} --wait-idle # block until state == idle\n`);
|
|
425
772
|
hints.push(` ay tail ${alive.pid} # view latest output\n`);
|
|
426
773
|
hints.push(` ay tail -f ${alive.pid} # follow live output\n`);
|
|
427
774
|
hints.push(
|
|
@@ -491,6 +838,11 @@ async function cmdRead(rest: string[], { mode }: ReadOpts): Promise<number> {
|
|
|
491
838
|
description: "Use most recent match when multiple match",
|
|
492
839
|
})
|
|
493
840
|
.option("cwd", { type: "string", description: "Restrict to agents under this dir" })
|
|
841
|
+
.option("reconnect-timeout", {
|
|
842
|
+
type: "number",
|
|
843
|
+
default: 120,
|
|
844
|
+
description: "Seconds before giving up reconnecting remote SSE (default: 120)",
|
|
845
|
+
})
|
|
494
846
|
.help(false)
|
|
495
847
|
.version(false)
|
|
496
848
|
.exitProcess(false);
|
|
@@ -504,6 +856,18 @@ async function cmdRead(rest: string[], { mode }: ReadOpts): Promise<number> {
|
|
|
504
856
|
cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null,
|
|
505
857
|
};
|
|
506
858
|
const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
|
|
859
|
+
if (keyword) {
|
|
860
|
+
const remote = await resolveRemoteSpec(keyword);
|
|
861
|
+
const nFlag2 = argv.n;
|
|
862
|
+
const n2 =
|
|
863
|
+
nFlag2 !== undefined && Number.isFinite(nFlag2) && nFlag2 > 0
|
|
864
|
+
? Math.floor(nFlag2)
|
|
865
|
+
: mode === "cat"
|
|
866
|
+
? 0
|
|
867
|
+
: 96;
|
|
868
|
+
const reconnectTimeoutMs = ((argv["reconnect-timeout"] as number) ?? 120) * 1000;
|
|
869
|
+
if (remote) return runRemoteRead(remote, mode, argv.follow, n2, reconnectTimeoutMs);
|
|
870
|
+
}
|
|
507
871
|
const follow = argv.follow;
|
|
508
872
|
const nFlag = argv.n;
|
|
509
873
|
const n =
|
|
@@ -579,7 +943,7 @@ async function cmdRead(rest: string[], { mode }: ReadOpts): Promise<number> {
|
|
|
579
943
|
* Feed the raw PTY bytes through @xterm/headless and emit plain text.
|
|
580
944
|
* Same approach as koho's renderTerminalBuffer + agent-yes's XtermProxy.
|
|
581
945
|
*/
|
|
582
|
-
async function renderRawLog(
|
|
946
|
+
export async function renderRawLog(
|
|
583
947
|
buf: Uint8Array,
|
|
584
948
|
{ mode, n }: { mode: "cat" | "tail" | "head"; n: number },
|
|
585
949
|
): Promise<string> {
|
|
@@ -773,6 +1137,10 @@ async function cmdSend(rest: string[]): Promise<number> {
|
|
|
773
1137
|
throw new Error("usage: ay send <keyword> <msg|-> [--code=enter|esc|ctrl-c|ctrl-y|tab|none]");
|
|
774
1138
|
|
|
775
1139
|
const codeName = argv.code.toLowerCase();
|
|
1140
|
+
{
|
|
1141
|
+
const remote = await resolveRemoteSpec(keyword);
|
|
1142
|
+
if (remote) return runRemoteSend(remote, rawMessage, codeName);
|
|
1143
|
+
}
|
|
776
1144
|
const trailing = controlCodeFromName(codeName);
|
|
777
1145
|
|
|
778
1146
|
const record = await resolveOne(keyword, opts);
|
|
@@ -851,7 +1219,7 @@ export function controlCodeFromName(name: string): string {
|
|
|
851
1219
|
}
|
|
852
1220
|
}
|
|
853
1221
|
|
|
854
|
-
async function writeToIpc(ipcPath: string, payload: string): Promise<void> {
|
|
1222
|
+
export async function writeToIpc(ipcPath: string, payload: string): Promise<void> {
|
|
855
1223
|
if (process.platform === "win32") {
|
|
856
1224
|
const { connect } = await import("net");
|
|
857
1225
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -974,7 +1342,7 @@ async function cmdNote(rest: string[]): Promise<number> {
|
|
|
974
1342
|
// ay status
|
|
975
1343
|
// ---------------------------------------------------------------------------
|
|
976
1344
|
|
|
977
|
-
interface StatusSnapshot {
|
|
1345
|
+
export interface StatusSnapshot {
|
|
978
1346
|
pid: number;
|
|
979
1347
|
cli: string;
|
|
980
1348
|
cwd: string;
|
|
@@ -989,7 +1357,7 @@ interface StatusSnapshot {
|
|
|
989
1357
|
log_file: string | null;
|
|
990
1358
|
}
|
|
991
1359
|
|
|
992
|
-
async function snapshotStatus(record: GlobalPidRecord): Promise<StatusSnapshot> {
|
|
1360
|
+
export async function snapshotStatus(record: GlobalPidRecord): Promise<StatusSnapshot> {
|
|
993
1361
|
const alive = isPidAlive(record.pid);
|
|
994
1362
|
let state: "active" | "idle" | "stopped";
|
|
995
1363
|
let logMtimeMs: number | null = null;
|
|
@@ -1032,6 +1400,15 @@ async function cmdStatus(rest: string[]): Promise<number> {
|
|
|
1032
1400
|
default: false,
|
|
1033
1401
|
description: "Stream changes as JSON",
|
|
1034
1402
|
})
|
|
1403
|
+
.option("wait-idle", {
|
|
1404
|
+
type: "boolean",
|
|
1405
|
+
default: false,
|
|
1406
|
+
description: "Block until state == idle. Exit 0 idle, 1 stopped, 2 timeout",
|
|
1407
|
+
})
|
|
1408
|
+
.option("timeout", {
|
|
1409
|
+
type: "string",
|
|
1410
|
+
description: "Timeout for --wait-idle (e.g. 30s, 5m). Default: no timeout",
|
|
1411
|
+
})
|
|
1035
1412
|
.option("interval", { type: "number", default: 2, description: "Poll interval in seconds" })
|
|
1036
1413
|
.option("latest", { type: "boolean", default: false, description: "Use most recent match" })
|
|
1037
1414
|
.option("cwd", { type: "string", description: "Restrict to agents under this dir" })
|
|
@@ -1049,11 +1426,25 @@ async function cmdStatus(rest: string[]): Promise<number> {
|
|
|
1049
1426
|
};
|
|
1050
1427
|
const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
|
|
1051
1428
|
|
|
1052
|
-
if (!keyword)
|
|
1429
|
+
if (!keyword)
|
|
1430
|
+
throw new Error("usage: ay status <keyword> [--watch | --wait-idle] [--timeout=Ns]");
|
|
1431
|
+
|
|
1432
|
+
{
|
|
1433
|
+
const remote = await resolveRemoteSpec(keyword);
|
|
1434
|
+
if (remote) return runRemoteStatus(remote);
|
|
1435
|
+
}
|
|
1053
1436
|
|
|
1054
1437
|
const watch = argv.watch;
|
|
1438
|
+
const waitIdle = argv["wait-idle"];
|
|
1055
1439
|
const intervalFlag = argv.interval;
|
|
1056
1440
|
const intervalMs = Math.max(500, (Number.isFinite(intervalFlag) ? intervalFlag : 2) * 1000);
|
|
1441
|
+
const timeoutMs =
|
|
1442
|
+
typeof argv.timeout === "string" && argv.timeout.length > 0
|
|
1443
|
+
? (ms(argv.timeout) ?? Number.NaN)
|
|
1444
|
+
: null;
|
|
1445
|
+
if (timeoutMs !== null && !Number.isFinite(timeoutMs)) {
|
|
1446
|
+
throw new Error(`invalid --timeout value: ${argv.timeout}`);
|
|
1447
|
+
}
|
|
1057
1448
|
|
|
1058
1449
|
const record = await resolveOne(keyword, opts);
|
|
1059
1450
|
|
|
@@ -1062,6 +1453,26 @@ async function cmdStatus(rest: string[]): Promise<number> {
|
|
|
1062
1453
|
process.stdout.write(JSON.stringify(out) + "\n");
|
|
1063
1454
|
};
|
|
1064
1455
|
|
|
1456
|
+
if (waitIdle) {
|
|
1457
|
+
const startedAt = Date.now();
|
|
1458
|
+
for (;;) {
|
|
1459
|
+
const snap = await snapshotStatus(record);
|
|
1460
|
+
if (snap.state === "idle") {
|
|
1461
|
+
emit(snap);
|
|
1462
|
+
return 0;
|
|
1463
|
+
}
|
|
1464
|
+
if (snap.state === "stopped") {
|
|
1465
|
+
emit(snap);
|
|
1466
|
+
return 1;
|
|
1467
|
+
}
|
|
1468
|
+
if (timeoutMs !== null && Date.now() - startedAt >= timeoutMs) {
|
|
1469
|
+
emit(snap);
|
|
1470
|
+
return 2;
|
|
1471
|
+
}
|
|
1472
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1065
1476
|
if (!watch) {
|
|
1066
1477
|
emit(await snapshotStatus(record));
|
|
1067
1478
|
return 0;
|