agent-yes 1.84.0 → 1.86.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-BdYQ3v93.js → SUPPORTED_CLIS-BlWg00sP.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-D0NnTXRD.js +303 -0
- package/dist/subcommands-BDiS305D.js +6 -0
- package/dist/{subcommands-DjO8lthH.js → subcommands-BpGEGOQM.js} +440 -78
- package/dist/{tray-CH_G7aXM.js → tray-DHuD0nEk.js} +1 -1
- package/dist/{ts-DP0dIeoe.js → ts-DWuvdSWr.js} +2 -2
- package/dist/{versionChecker-DtDqoy3L.js → versionChecker-BCrJk4Zj.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 +478 -35
- package/ts/subcommands.ts +544 -95
package/ts/subcommands.ts
CHANGED
|
@@ -15,6 +15,8 @@ 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";
|
|
19
|
+
import { type ResolvedRemote, readRemotes, resolveRemoteSpec } from "./remotes.ts";
|
|
18
20
|
|
|
19
21
|
// ---------------------------------------------------------------------------
|
|
20
22
|
// notes store (~/.agent-yes/notes.jsonl)
|
|
@@ -25,7 +27,7 @@ function notesPath(): string {
|
|
|
25
27
|
return path.join(dir, "notes.jsonl");
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
async function readNotes(): Promise<Map<number, string>> {
|
|
30
|
+
export async function readNotes(): Promise<Map<number, string>> {
|
|
29
31
|
let raw: string;
|
|
30
32
|
try {
|
|
31
33
|
raw = await readFile(notesPath(), "utf-8");
|
|
@@ -135,6 +137,8 @@ const SUBCOMMANDS = new Set([
|
|
|
135
137
|
"send",
|
|
136
138
|
"restart",
|
|
137
139
|
"note",
|
|
140
|
+
"serve",
|
|
141
|
+
"remote",
|
|
138
142
|
]);
|
|
139
143
|
|
|
140
144
|
const IDLE_THRESHOLD_MS = 60 * 1000;
|
|
@@ -174,6 +178,14 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
|
|
|
174
178
|
return await cmdRestart(rest);
|
|
175
179
|
case "note":
|
|
176
180
|
return await cmdNote(rest);
|
|
181
|
+
case "serve": {
|
|
182
|
+
const { cmdServe } = await import("./serve.ts");
|
|
183
|
+
return cmdServe(rest);
|
|
184
|
+
}
|
|
185
|
+
case "remote": {
|
|
186
|
+
const { cmdRemote } = await import("./remotes.ts");
|
|
187
|
+
return cmdRemote(rest);
|
|
188
|
+
}
|
|
177
189
|
default:
|
|
178
190
|
return null;
|
|
179
191
|
}
|
|
@@ -188,7 +200,7 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
|
|
|
188
200
|
// shared helpers
|
|
189
201
|
// ---------------------------------------------------------------------------
|
|
190
202
|
|
|
191
|
-
interface CommonOpts {
|
|
203
|
+
export interface CommonOpts {
|
|
192
204
|
all: boolean;
|
|
193
205
|
active: boolean;
|
|
194
206
|
cwdScope: string | null;
|
|
@@ -196,65 +208,6 @@ interface CommonOpts {
|
|
|
196
208
|
json: boolean;
|
|
197
209
|
}
|
|
198
210
|
|
|
199
|
-
interface ParsedArgs {
|
|
200
|
-
flags: Record<string, string | boolean>;
|
|
201
|
-
positional: string[];
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export function parseArgs(rest: string[]): ParsedArgs {
|
|
205
|
-
const flags: Record<string, string | boolean> = {};
|
|
206
|
-
const positional: string[] = [];
|
|
207
|
-
for (let i = 0; i < rest.length; i++) {
|
|
208
|
-
const arg = rest[i]!;
|
|
209
|
-
if (arg.startsWith("--")) {
|
|
210
|
-
const eq = arg.indexOf("=");
|
|
211
|
-
if (eq >= 0) {
|
|
212
|
-
flags[arg.slice(2, eq)] = arg.slice(eq + 1);
|
|
213
|
-
} else {
|
|
214
|
-
const key = arg.slice(2);
|
|
215
|
-
const next = rest[i + 1];
|
|
216
|
-
// Boolean flags: --all, --json, --latest
|
|
217
|
-
if (
|
|
218
|
-
["all", "active", "follow", "json", "latest", "watch"].includes(key) ||
|
|
219
|
-
!next ||
|
|
220
|
-
next.startsWith("-")
|
|
221
|
-
) {
|
|
222
|
-
flags[key] = true;
|
|
223
|
-
} else {
|
|
224
|
-
flags[key] = next;
|
|
225
|
-
i++;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
} else if (arg.startsWith("-") && arg.length > 1) {
|
|
229
|
-
// -n N short flag
|
|
230
|
-
if (arg === "-n") {
|
|
231
|
-
flags["n"] = rest[i + 1] ?? "";
|
|
232
|
-
i++;
|
|
233
|
-
} else {
|
|
234
|
-
flags[arg.slice(1)] = true;
|
|
235
|
-
}
|
|
236
|
-
} else {
|
|
237
|
-
positional.push(arg);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return { flags, positional };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function commonOpts(flags: Record<string, string | boolean>): CommonOpts {
|
|
244
|
-
return {
|
|
245
|
-
all: !!flags.all,
|
|
246
|
-
active: !!flags.active,
|
|
247
|
-
cwdScope:
|
|
248
|
-
typeof flags.cwd === "string"
|
|
249
|
-
? path.resolve(flags.cwd)
|
|
250
|
-
: flags.cwd === true
|
|
251
|
-
? process.cwd()
|
|
252
|
-
: null,
|
|
253
|
-
latest: !!flags.latest,
|
|
254
|
-
json: !!flags.json,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
211
|
export function matchKeyword(record: GlobalPidRecord, keyword: string): boolean {
|
|
259
212
|
if (!keyword) return true;
|
|
260
213
|
const kw = keyword.toLowerCase();
|
|
@@ -269,7 +222,7 @@ export function matchKeyword(record: GlobalPidRecord, keyword: string): boolean
|
|
|
269
222
|
return false;
|
|
270
223
|
}
|
|
271
224
|
|
|
272
|
-
async function listRecords(
|
|
225
|
+
export async function listRecords(
|
|
273
226
|
keyword: string | undefined,
|
|
274
227
|
opts: CommonOpts,
|
|
275
228
|
): Promise<GlobalPidRecord[]> {
|
|
@@ -298,7 +251,7 @@ async function listRecords(
|
|
|
298
251
|
return records;
|
|
299
252
|
}
|
|
300
253
|
|
|
301
|
-
function isPidAlive(pid: number): boolean {
|
|
254
|
+
export function isPidAlive(pid: number): boolean {
|
|
302
255
|
try {
|
|
303
256
|
process.kill(pid, 0);
|
|
304
257
|
return true;
|
|
@@ -307,7 +260,10 @@ function isPidAlive(pid: number): boolean {
|
|
|
307
260
|
}
|
|
308
261
|
}
|
|
309
262
|
|
|
310
|
-
async function resolveOne(
|
|
263
|
+
export async function resolveOne(
|
|
264
|
+
keyword: string | undefined,
|
|
265
|
+
opts: CommonOpts,
|
|
266
|
+
): Promise<GlobalPidRecord> {
|
|
311
267
|
if (!keyword) {
|
|
312
268
|
throw new Error("keyword required (pid, cwd substring, cli name, or prompt substring)");
|
|
313
269
|
}
|
|
@@ -326,14 +282,390 @@ async function resolveOne(keyword: string | undefined, opts: CommonOpts): Promis
|
|
|
326
282
|
);
|
|
327
283
|
}
|
|
328
284
|
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// remote routing helpers
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
async function remoteGet(remote: ResolvedRemote, pathname: string): Promise<Response> {
|
|
290
|
+
return fetch(`${remote.url}${pathname}`, {
|
|
291
|
+
headers: { Authorization: `Bearer ${remote.token}` },
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function remotePost(
|
|
296
|
+
remote: ResolvedRemote,
|
|
297
|
+
pathname: string,
|
|
298
|
+
body: unknown,
|
|
299
|
+
): Promise<Response> {
|
|
300
|
+
return fetch(`${remote.url}${pathname}`, {
|
|
301
|
+
method: "POST",
|
|
302
|
+
headers: { Authorization: `Bearer ${remote.token}`, "Content-Type": "application/json" },
|
|
303
|
+
body: JSON.stringify(body),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function runRemoteLs(
|
|
308
|
+
remote: ResolvedRemote,
|
|
309
|
+
opts: { all: boolean; active: boolean },
|
|
310
|
+
): Promise<number> {
|
|
311
|
+
const params = new URLSearchParams();
|
|
312
|
+
if (remote.keyword) params.set("keyword", remote.keyword);
|
|
313
|
+
if (opts.all) params.set("all", "1");
|
|
314
|
+
if (opts.active) params.set("active", "1");
|
|
315
|
+
const res = await remoteGet(remote, `/api/ls?${params}`);
|
|
316
|
+
if (!res.ok) {
|
|
317
|
+
process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
|
|
318
|
+
return 1;
|
|
319
|
+
}
|
|
320
|
+
const records = (await res.json()) as any[];
|
|
321
|
+
if (records.length === 0) {
|
|
322
|
+
process.stderr.write(
|
|
323
|
+
remote.keyword
|
|
324
|
+
? `no agents matched "${remote.keyword}" on ${remote.url}\n`
|
|
325
|
+
: `no running agents on ${remote.url}\n`,
|
|
326
|
+
);
|
|
327
|
+
return 0;
|
|
328
|
+
}
|
|
329
|
+
process.stderr.write(`[remote ${remote.url}]\n`);
|
|
330
|
+
const termWidth = (process.stdout as any).columns ?? 120;
|
|
331
|
+
const widths = {
|
|
332
|
+
pid: Math.max(3, ...records.map((r: any) => String(r.pid).length)),
|
|
333
|
+
cli: Math.max(3, ...records.map((r: any) => String(r.cli).length)),
|
|
334
|
+
status: Math.max(6, ...records.map((r: any) => String(r.status).length)),
|
|
335
|
+
cwd: Math.max(3, ...records.map((r: any) => String(r.cwd).length)),
|
|
336
|
+
};
|
|
337
|
+
const fixedWidth = widths.pid + widths.cli + widths.status + widths.cwd + 4 * 2;
|
|
338
|
+
const promptBudget = Math.max(20, termWidth - fixedWidth - 1);
|
|
339
|
+
const header =
|
|
340
|
+
[
|
|
341
|
+
"PID".padEnd(widths.pid),
|
|
342
|
+
"CLI".padEnd(widths.cli),
|
|
343
|
+
"STATUS".padEnd(widths.status),
|
|
344
|
+
"CWD".padEnd(widths.cwd),
|
|
345
|
+
"PROMPT",
|
|
346
|
+
].join(" ") + "\n";
|
|
347
|
+
process.stdout.write(header);
|
|
348
|
+
for (const r of records) {
|
|
349
|
+
const label = r.prompt ? truncate(`→ ${r.prompt}`, promptBudget) : "";
|
|
350
|
+
process.stdout.write(
|
|
351
|
+
[
|
|
352
|
+
String(r.pid).padEnd(widths.pid),
|
|
353
|
+
String(r.cli).padEnd(widths.cli),
|
|
354
|
+
String(r.status).padEnd(widths.status),
|
|
355
|
+
String(r.cwd).padEnd(widths.cwd),
|
|
356
|
+
label,
|
|
357
|
+
].join(" ") + "\n",
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function runRemoteRead(
|
|
364
|
+
remote: ResolvedRemote,
|
|
365
|
+
mode: "cat" | "tail" | "head",
|
|
366
|
+
follow: boolean,
|
|
367
|
+
n: number,
|
|
368
|
+
reconnectTimeoutMs = 120_000,
|
|
369
|
+
): Promise<number> {
|
|
370
|
+
const keyword = remote.keyword ?? "";
|
|
371
|
+
if (!keyword) {
|
|
372
|
+
process.stderr.write(
|
|
373
|
+
"remote tail/cat/head requires a keyword (e.g. token@host:port:keyword)\n",
|
|
374
|
+
);
|
|
375
|
+
return 1;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (mode === "tail" && follow) {
|
|
379
|
+
const ac = new AbortController();
|
|
380
|
+
process.on("SIGINT", () => ac.abort());
|
|
381
|
+
const deadline = Date.now() + reconnectTimeoutMs;
|
|
382
|
+
let delay = 1_000;
|
|
383
|
+
let attempt = 0;
|
|
384
|
+
|
|
385
|
+
process.stderr.write(
|
|
386
|
+
`[remote ${remote.url} ${keyword}]\nfollowing... (Ctrl-C to stop, timeout: ${Math.round(reconnectTimeoutMs / 1000)}s)\n`,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
while (!ac.signal.aborted) {
|
|
390
|
+
try {
|
|
391
|
+
const res = await fetch(`${remote.url}/api/tail/${encodeURIComponent(keyword)}`, {
|
|
392
|
+
headers: { Authorization: `Bearer ${remote.token}`, Accept: "text/event-stream" },
|
|
393
|
+
signal: ac.signal,
|
|
394
|
+
});
|
|
395
|
+
if (!res.ok) {
|
|
396
|
+
// 401/404 are permanent failures — no point retrying
|
|
397
|
+
if (res.status === 401 || res.status === 404) {
|
|
398
|
+
process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
|
|
399
|
+
return 1;
|
|
400
|
+
}
|
|
401
|
+
throw new Error(`HTTP ${res.status}`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (attempt > 0) process.stderr.write("remote: reconnected\n");
|
|
405
|
+
delay = 1_000; // reset backoff on successful connect
|
|
406
|
+
|
|
407
|
+
const reader = res.body!.getReader();
|
|
408
|
+
const dec = new TextDecoder();
|
|
409
|
+
let buf = "";
|
|
410
|
+
while (true) {
|
|
411
|
+
const { done, value } = await reader.read();
|
|
412
|
+
if (done) break;
|
|
413
|
+
buf += dec.decode(value, { stream: true });
|
|
414
|
+
const lines = buf.split("\n");
|
|
415
|
+
buf = lines.pop() ?? "";
|
|
416
|
+
for (const line of lines) {
|
|
417
|
+
if (!line.startsWith("data: ")) continue;
|
|
418
|
+
try {
|
|
419
|
+
const text = JSON.parse(line.slice(6)) as string;
|
|
420
|
+
process.stdout.write(text);
|
|
421
|
+
if (!text.endsWith("\n")) process.stdout.write("\n");
|
|
422
|
+
} catch {
|
|
423
|
+
/* skip non-JSON */
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
break; // stream ended cleanly
|
|
428
|
+
} catch (e: any) {
|
|
429
|
+
if (e.name === "AbortError" || ac.signal.aborted) return 0;
|
|
430
|
+
if (Date.now() >= deadline) {
|
|
431
|
+
process.stderr.write(
|
|
432
|
+
`remote: timeout after ${Math.round(reconnectTimeoutMs / 1000)}s, giving up\n`,
|
|
433
|
+
);
|
|
434
|
+
return 1;
|
|
435
|
+
}
|
|
436
|
+
process.stderr.write(
|
|
437
|
+
`remote: disconnected (${e.message}), retrying in ${delay / 1000}s…\n`,
|
|
438
|
+
);
|
|
439
|
+
await new Promise<void>((resolve, reject) => {
|
|
440
|
+
const t = setTimeout(resolve, delay);
|
|
441
|
+
ac.signal.addEventListener("abort", () => {
|
|
442
|
+
clearTimeout(t);
|
|
443
|
+
reject(new Error("abort"));
|
|
444
|
+
});
|
|
445
|
+
}).catch(() => {});
|
|
446
|
+
if (ac.signal.aborted) return 0;
|
|
447
|
+
delay = Math.min(delay * 2, 30_000);
|
|
448
|
+
attempt++;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return 0;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Static read (cat/head/tail without -f)
|
|
455
|
+
const params = new URLSearchParams({ mode, n: String(n) });
|
|
456
|
+
const res = await remoteGet(remote, `/api/read/${encodeURIComponent(keyword)}?${params}`);
|
|
457
|
+
if (!res.ok) {
|
|
458
|
+
process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
|
|
459
|
+
return 1;
|
|
460
|
+
}
|
|
461
|
+
const text = await res.text();
|
|
462
|
+
process.stderr.write(`[remote ${remote.url} ${keyword}]\n`);
|
|
463
|
+
process.stdout.write(text);
|
|
464
|
+
if (!text.endsWith("\n")) process.stdout.write("\n");
|
|
465
|
+
return 0;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function runRemoteSend(remote: ResolvedRemote, msg: string, code: string): Promise<number> {
|
|
469
|
+
const keyword = remote.keyword ?? "";
|
|
470
|
+
if (!keyword) {
|
|
471
|
+
process.stderr.write("remote send requires a keyword (e.g. token@host:port:keyword)\n");
|
|
472
|
+
return 1;
|
|
473
|
+
}
|
|
474
|
+
const res = await remotePost(remote, "/api/send", { keyword, msg, code });
|
|
475
|
+
if (!res.ok) {
|
|
476
|
+
process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
|
|
477
|
+
return 1;
|
|
478
|
+
}
|
|
479
|
+
const data = (await res.json()) as any;
|
|
480
|
+
process.stdout.write(`sent to remote pid ${data.pid} (${remote.url} ${keyword})\n`);
|
|
481
|
+
return 0;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function runRemoteStatus(remote: ResolvedRemote): Promise<number> {
|
|
485
|
+
const keyword = remote.keyword ?? "";
|
|
486
|
+
if (!keyword) {
|
|
487
|
+
process.stderr.write("remote status requires a keyword (e.g. token@host:port:keyword)\n");
|
|
488
|
+
return 1;
|
|
489
|
+
}
|
|
490
|
+
const res = await remoteGet(remote, `/api/status/${encodeURIComponent(keyword)}`);
|
|
491
|
+
if (!res.ok) {
|
|
492
|
+
process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
|
|
493
|
+
return 1;
|
|
494
|
+
}
|
|
495
|
+
process.stdout.write(JSON.stringify(await res.json(), null, 2) + "\n");
|
|
496
|
+
return 0;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
// --all-remotes helpers
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
|
|
503
|
+
async function fetchRemoteRecordsRaw(
|
|
504
|
+
url: string,
|
|
505
|
+
token: string,
|
|
506
|
+
opts: { all: boolean; active: boolean; keyword?: string },
|
|
507
|
+
): Promise<any[]> {
|
|
508
|
+
const params = new URLSearchParams();
|
|
509
|
+
if (opts.all) params.set("all", "1");
|
|
510
|
+
if (opts.active) params.set("active", "1");
|
|
511
|
+
if (opts.keyword) params.set("keyword", opts.keyword);
|
|
512
|
+
try {
|
|
513
|
+
const res = await fetch(`${url}/api/ls?${params}`, {
|
|
514
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
515
|
+
signal: AbortSignal.timeout(5000),
|
|
516
|
+
});
|
|
517
|
+
if (!res.ok) return [];
|
|
518
|
+
return (await res.json()) as any[];
|
|
519
|
+
} catch {
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function runAllRemotesLs(opts: {
|
|
525
|
+
all: boolean;
|
|
526
|
+
active: boolean;
|
|
527
|
+
keyword?: string;
|
|
528
|
+
}): Promise<number> {
|
|
529
|
+
const remotes = await readRemotes();
|
|
530
|
+
const localOpts: CommonOpts = {
|
|
531
|
+
all: opts.all,
|
|
532
|
+
active: opts.active,
|
|
533
|
+
json: true,
|
|
534
|
+
latest: false,
|
|
535
|
+
cwdScope: null,
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const [localResult, ...remoteResults] = await Promise.allSettled([
|
|
539
|
+
listRecords(opts.keyword, localOpts).then((recs) => ({
|
|
540
|
+
host: "local",
|
|
541
|
+
records: recs as any[],
|
|
542
|
+
})),
|
|
543
|
+
...Array.from(remotes.entries()).map(([alias, cfg]) =>
|
|
544
|
+
fetchRemoteRecordsRaw(cfg.url, cfg.token, opts).then((records) => ({ host: alias, records })),
|
|
545
|
+
),
|
|
546
|
+
]);
|
|
547
|
+
|
|
548
|
+
type HostedRow = { host: string; rec: any };
|
|
549
|
+
const rows: HostedRow[] = [];
|
|
550
|
+
if (localResult.status === "fulfilled") {
|
|
551
|
+
for (const r of localResult.value.records) rows.push({ host: "local", rec: r });
|
|
552
|
+
}
|
|
553
|
+
for (const res of remoteResults) {
|
|
554
|
+
if (res.status === "fulfilled") {
|
|
555
|
+
for (const r of res.value.records) rows.push({ host: res.value.host, rec: r });
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (rows.length === 0) {
|
|
560
|
+
process.stderr.write("no running agents\n");
|
|
561
|
+
return 0;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const termWidth = (process.stdout as any).columns ?? 120;
|
|
565
|
+
const hostW = Math.max(4, ...rows.map((r) => r.host.length));
|
|
566
|
+
const pidW = Math.max(3, ...rows.map((r) => String(r.rec.pid).length));
|
|
567
|
+
const cliW = Math.max(3, ...rows.map((r) => String(r.rec.cli).length));
|
|
568
|
+
const statusW = Math.max(6, ...rows.map((r) => String(r.rec.status).length));
|
|
569
|
+
const cwdW = Math.max(3, ...rows.map((r) => shortenPath(String(r.rec.cwd)).length));
|
|
570
|
+
const promptBudget = Math.max(20, termWidth - hostW - pidW - cliW - statusW - cwdW - 5 * 2 - 1);
|
|
571
|
+
|
|
572
|
+
process.stdout.write(
|
|
573
|
+
[
|
|
574
|
+
"HOST".padEnd(hostW),
|
|
575
|
+
"PID".padEnd(pidW),
|
|
576
|
+
"CLI".padEnd(cliW),
|
|
577
|
+
"STATUS".padEnd(statusW),
|
|
578
|
+
"CWD".padEnd(cwdW),
|
|
579
|
+
"PROMPT",
|
|
580
|
+
].join(" ") + "\n",
|
|
581
|
+
);
|
|
582
|
+
for (const { host, rec } of rows) {
|
|
583
|
+
const label = rec.prompt ? truncate(`→ ${rec.prompt}`, promptBudget) : "";
|
|
584
|
+
process.stdout.write(
|
|
585
|
+
[
|
|
586
|
+
host.padEnd(hostW),
|
|
587
|
+
String(rec.pid).padEnd(pidW),
|
|
588
|
+
String(rec.cli).padEnd(cliW),
|
|
589
|
+
String(rec.status).padEnd(statusW),
|
|
590
|
+
shortenPath(String(rec.cwd)).padEnd(cwdW),
|
|
591
|
+
label,
|
|
592
|
+
].join(" ") + "\n",
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
return 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
329
598
|
// ---------------------------------------------------------------------------
|
|
330
599
|
// ay ls
|
|
331
600
|
// ---------------------------------------------------------------------------
|
|
332
601
|
|
|
333
602
|
async function cmdLs(rest: string[]): Promise<number> {
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
603
|
+
const y = yargs(rest)
|
|
604
|
+
.usage(
|
|
605
|
+
"Usage: ay ls [keyword] [options]\n" +
|
|
606
|
+
" ay list [keyword] [options]\n" +
|
|
607
|
+
" ay ps [keyword] [options]\n\n" +
|
|
608
|
+
"List running agents. Optionally filter by keyword (pid, cwd substring, or prompt substring).",
|
|
609
|
+
)
|
|
610
|
+
.option("all", {
|
|
611
|
+
type: "boolean",
|
|
612
|
+
default: false,
|
|
613
|
+
description: "Show all agents including exited ones",
|
|
614
|
+
})
|
|
615
|
+
.option("active", {
|
|
616
|
+
type: "boolean",
|
|
617
|
+
default: false,
|
|
618
|
+
description: "Only show agents with an alive process",
|
|
619
|
+
})
|
|
620
|
+
.option("json", { type: "boolean", default: false, description: "Output as JSON array" })
|
|
621
|
+
.option("latest", {
|
|
622
|
+
type: "boolean",
|
|
623
|
+
default: false,
|
|
624
|
+
description: "Show only the most recent agent",
|
|
625
|
+
})
|
|
626
|
+
.option("cwd", { type: "string", description: "Restrict to agents whose cwd starts with dir" })
|
|
627
|
+
.option("all-remotes", {
|
|
628
|
+
type: "boolean",
|
|
629
|
+
default: false,
|
|
630
|
+
description: "Include agents from all configured remotes (remotes.yaml)",
|
|
631
|
+
})
|
|
632
|
+
.option("help", { alias: "h", type: "boolean", default: false, description: "Show this help" })
|
|
633
|
+
.example("ay ls", "list running agents")
|
|
634
|
+
.example("ay ls --all-remotes", "include all configured remote machines")
|
|
635
|
+
.example("ay ls --all", "include exited agents")
|
|
636
|
+
.example("ay ls --json", "machine-readable output")
|
|
637
|
+
.example("ay ls symval", "filter by cwd/prompt keyword")
|
|
638
|
+
.help(false)
|
|
639
|
+
.version(false)
|
|
640
|
+
.exitProcess(false);
|
|
641
|
+
|
|
642
|
+
const argv = await y.parseAsync();
|
|
643
|
+
|
|
644
|
+
if (argv.help || argv.h) {
|
|
645
|
+
process.stdout.write((await y.getHelp()) + "\n");
|
|
646
|
+
return 0;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (argv["all-remotes"]) {
|
|
650
|
+
return runAllRemotesLs({
|
|
651
|
+
all: argv.all,
|
|
652
|
+
active: argv.active,
|
|
653
|
+
keyword: argv._[0] !== undefined ? String(argv._[0]) : undefined,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
|
|
658
|
+
if (keyword) {
|
|
659
|
+
const remote = await resolveRemoteSpec(keyword);
|
|
660
|
+
if (remote) return runRemoteLs(remote, { all: argv.all, active: argv.active });
|
|
661
|
+
}
|
|
662
|
+
const opts: CommonOpts = {
|
|
663
|
+
all: argv.all,
|
|
664
|
+
active: argv.active,
|
|
665
|
+
json: argv.json,
|
|
666
|
+
latest: argv.latest,
|
|
667
|
+
cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null,
|
|
668
|
+
};
|
|
337
669
|
const records = await listRecords(keyword, opts);
|
|
338
670
|
|
|
339
671
|
if (opts.json) {
|
|
@@ -488,12 +820,54 @@ interface ReadOpts {
|
|
|
488
820
|
}
|
|
489
821
|
|
|
490
822
|
async function cmdRead(rest: string[], { mode }: ReadOpts): Promise<number> {
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
823
|
+
const y = yargs(rest)
|
|
824
|
+
.usage("Usage: ay read/cat/tail/head <keyword> [options]")
|
|
825
|
+
.option("follow", {
|
|
826
|
+
alias: "f",
|
|
827
|
+
type: "boolean",
|
|
828
|
+
default: false,
|
|
829
|
+
description: "Follow log output (Ctrl-C to stop)",
|
|
830
|
+
})
|
|
831
|
+
.option("n", { type: "number", description: "Number of lines (default: 96 for tail/head)" })
|
|
832
|
+
.option("all", { type: "boolean", default: false, description: "Include exited agents" })
|
|
833
|
+
.option("latest", {
|
|
834
|
+
type: "boolean",
|
|
835
|
+
default: false,
|
|
836
|
+
description: "Use most recent match when multiple match",
|
|
837
|
+
})
|
|
838
|
+
.option("cwd", { type: "string", description: "Restrict to agents under this dir" })
|
|
839
|
+
.option("reconnect-timeout", {
|
|
840
|
+
type: "number",
|
|
841
|
+
default: 120,
|
|
842
|
+
description: "Seconds before giving up reconnecting remote SSE (default: 120)",
|
|
843
|
+
})
|
|
844
|
+
.help(false)
|
|
845
|
+
.version(false)
|
|
846
|
+
.exitProcess(false);
|
|
847
|
+
|
|
848
|
+
const argv = await y.parseAsync();
|
|
849
|
+
const opts: CommonOpts = {
|
|
850
|
+
all: argv.all,
|
|
851
|
+
active: false,
|
|
852
|
+
json: false,
|
|
853
|
+
latest: argv.latest,
|
|
854
|
+
cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null,
|
|
855
|
+
};
|
|
856
|
+
const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
|
|
857
|
+
if (keyword) {
|
|
858
|
+
const remote = await resolveRemoteSpec(keyword);
|
|
859
|
+
const nFlag2 = argv.n;
|
|
860
|
+
const n2 =
|
|
861
|
+
nFlag2 !== undefined && Number.isFinite(nFlag2) && nFlag2 > 0
|
|
862
|
+
? Math.floor(nFlag2)
|
|
863
|
+
: mode === "cat"
|
|
864
|
+
? 0
|
|
865
|
+
: 96;
|
|
866
|
+
const reconnectTimeoutMs = ((argv["reconnect-timeout"] as number) ?? 120) * 1000;
|
|
867
|
+
if (remote) return runRemoteRead(remote, mode, argv.follow, n2, reconnectTimeoutMs);
|
|
868
|
+
}
|
|
869
|
+
const follow = argv.follow;
|
|
870
|
+
const nFlag = argv.n;
|
|
497
871
|
const n =
|
|
498
872
|
nFlag !== undefined && Number.isFinite(nFlag) && nFlag > 0
|
|
499
873
|
? Math.floor(nFlag)
|
|
@@ -567,7 +941,7 @@ async function cmdRead(rest: string[], { mode }: ReadOpts): Promise<number> {
|
|
|
567
941
|
* Feed the raw PTY bytes through @xterm/headless and emit plain text.
|
|
568
942
|
* Same approach as koho's renderTerminalBuffer + agent-yes's XtermProxy.
|
|
569
943
|
*/
|
|
570
|
-
async function renderRawLog(
|
|
944
|
+
export async function renderRawLog(
|
|
571
945
|
buf: Uint8Array,
|
|
572
946
|
{ mode, n }: { mode: "cat" | "tail" | "head"; n: number },
|
|
573
947
|
): Promise<string> {
|
|
@@ -732,15 +1106,39 @@ function extractActivityFromLines(lines: string[]): string | null {
|
|
|
732
1106
|
// ---------------------------------------------------------------------------
|
|
733
1107
|
|
|
734
1108
|
async function cmdSend(rest: string[]): Promise<number> {
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
1109
|
+
const y = yargs(rest)
|
|
1110
|
+
.usage("Usage: ay send <keyword> <msg|-> [options]")
|
|
1111
|
+
.option("code", {
|
|
1112
|
+
type: "string",
|
|
1113
|
+
default: "enter",
|
|
1114
|
+
description: "Trailing control code (enter|esc|ctrl-c|ctrl-y|tab|none)",
|
|
1115
|
+
})
|
|
1116
|
+
.option("all", { type: "boolean", default: false, description: "Include exited agents" })
|
|
1117
|
+
.option("latest", { type: "boolean", default: false, description: "Use most recent match" })
|
|
1118
|
+
.option("cwd", { type: "string", description: "Restrict to agents under this dir" })
|
|
1119
|
+
.help(false)
|
|
1120
|
+
.version(false)
|
|
1121
|
+
.exitProcess(false);
|
|
1122
|
+
|
|
1123
|
+
const argv = await y.parseAsync();
|
|
1124
|
+
const opts: CommonOpts = {
|
|
1125
|
+
all: argv.all,
|
|
1126
|
+
active: false,
|
|
1127
|
+
json: false,
|
|
1128
|
+
latest: argv.latest,
|
|
1129
|
+
cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null,
|
|
1130
|
+
};
|
|
1131
|
+
const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
|
|
1132
|
+
const rawMessage = argv._.slice(1).map(String).join(" ");
|
|
739
1133
|
|
|
740
1134
|
if (!keyword)
|
|
741
1135
|
throw new Error("usage: ay send <keyword> <msg|-> [--code=enter|esc|ctrl-c|ctrl-y|tab|none]");
|
|
742
1136
|
|
|
743
|
-
const codeName =
|
|
1137
|
+
const codeName = argv.code.toLowerCase();
|
|
1138
|
+
{
|
|
1139
|
+
const remote = await resolveRemoteSpec(keyword);
|
|
1140
|
+
if (remote) return runRemoteSend(remote, rawMessage, codeName);
|
|
1141
|
+
}
|
|
744
1142
|
const trailing = controlCodeFromName(codeName);
|
|
745
1143
|
|
|
746
1144
|
const record = await resolveOne(keyword, opts);
|
|
@@ -819,7 +1217,7 @@ export function controlCodeFromName(name: string): string {
|
|
|
819
1217
|
}
|
|
820
1218
|
}
|
|
821
1219
|
|
|
822
|
-
async function writeToIpc(ipcPath: string, payload: string): Promise<void> {
|
|
1220
|
+
export async function writeToIpc(ipcPath: string, payload: string): Promise<void> {
|
|
823
1221
|
if (process.platform === "win32") {
|
|
824
1222
|
const { connect } = await import("net");
|
|
825
1223
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -855,9 +1253,23 @@ async function writeToIpc(ipcPath: string, payload: string): Promise<void> {
|
|
|
855
1253
|
// ---------------------------------------------------------------------------
|
|
856
1254
|
|
|
857
1255
|
async function cmdRestart(rest: string[]): Promise<number> {
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
1256
|
+
const y = yargs(rest)
|
|
1257
|
+
.usage("Usage: ay restart <keyword>")
|
|
1258
|
+
.option("latest", { type: "boolean", default: false, description: "Use most recent match" })
|
|
1259
|
+
.option("cwd", { type: "string", description: "Restrict to agents under this dir" })
|
|
1260
|
+
.help(false)
|
|
1261
|
+
.version(false)
|
|
1262
|
+
.exitProcess(false);
|
|
1263
|
+
|
|
1264
|
+
const argv = await y.parseAsync();
|
|
1265
|
+
const opts: CommonOpts = {
|
|
1266
|
+
all: true,
|
|
1267
|
+
active: false,
|
|
1268
|
+
json: false,
|
|
1269
|
+
latest: argv.latest,
|
|
1270
|
+
cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null,
|
|
1271
|
+
};
|
|
1272
|
+
const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
|
|
861
1273
|
const record = await resolveOne(keyword, opts);
|
|
862
1274
|
|
|
863
1275
|
if (isPidAlive(record.pid)) {
|
|
@@ -890,14 +1302,25 @@ async function cmdRestart(rest: string[]): Promise<number> {
|
|
|
890
1302
|
// ---------------------------------------------------------------------------
|
|
891
1303
|
|
|
892
1304
|
async function cmdNote(rest: string[]): Promise<number> {
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1305
|
+
const y = yargs(rest)
|
|
1306
|
+
.usage('Usage: ay note <keyword> ["note text"]')
|
|
1307
|
+
.help(false)
|
|
1308
|
+
.version(false)
|
|
1309
|
+
.exitProcess(false);
|
|
1310
|
+
|
|
1311
|
+
const argv = await y.parseAsync();
|
|
1312
|
+
const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
|
|
1313
|
+
const note = argv._.slice(1).map(String).join(" ");
|
|
897
1314
|
|
|
898
1315
|
if (!keyword) throw new Error('usage: ay note <keyword> ["note text"] (omit text to clear)');
|
|
899
1316
|
|
|
900
|
-
const record = await resolveOne(keyword, {
|
|
1317
|
+
const record = await resolveOne(keyword, {
|
|
1318
|
+
all: true,
|
|
1319
|
+
active: false,
|
|
1320
|
+
json: false,
|
|
1321
|
+
latest: false,
|
|
1322
|
+
cwdScope: null,
|
|
1323
|
+
});
|
|
901
1324
|
|
|
902
1325
|
if (!note) {
|
|
903
1326
|
// clear
|
|
@@ -917,7 +1340,7 @@ async function cmdNote(rest: string[]): Promise<number> {
|
|
|
917
1340
|
// ay status
|
|
918
1341
|
// ---------------------------------------------------------------------------
|
|
919
1342
|
|
|
920
|
-
interface StatusSnapshot {
|
|
1343
|
+
export interface StatusSnapshot {
|
|
921
1344
|
pid: number;
|
|
922
1345
|
cli: string;
|
|
923
1346
|
cwd: string;
|
|
@@ -932,7 +1355,7 @@ interface StatusSnapshot {
|
|
|
932
1355
|
log_file: string | null;
|
|
933
1356
|
}
|
|
934
1357
|
|
|
935
|
-
async function snapshotStatus(record: GlobalPidRecord): Promise<StatusSnapshot> {
|
|
1358
|
+
export async function snapshotStatus(record: GlobalPidRecord): Promise<StatusSnapshot> {
|
|
936
1359
|
const alive = isPidAlive(record.pid);
|
|
937
1360
|
let state: "active" | "idle" | "stopped";
|
|
938
1361
|
let logMtimeMs: number | null = null;
|
|
@@ -967,14 +1390,40 @@ async function snapshotStatus(record: GlobalPidRecord): Promise<StatusSnapshot>
|
|
|
967
1390
|
}
|
|
968
1391
|
|
|
969
1392
|
async function cmdStatus(rest: string[]): Promise<number> {
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
|
|
1393
|
+
const y = yargs(rest)
|
|
1394
|
+
.usage("Usage: ay status <keyword> [options]")
|
|
1395
|
+
.option("watch", {
|
|
1396
|
+
alias: "w",
|
|
1397
|
+
type: "boolean",
|
|
1398
|
+
default: false,
|
|
1399
|
+
description: "Stream changes as JSON",
|
|
1400
|
+
})
|
|
1401
|
+
.option("interval", { type: "number", default: 2, description: "Poll interval in seconds" })
|
|
1402
|
+
.option("latest", { type: "boolean", default: false, description: "Use most recent match" })
|
|
1403
|
+
.option("cwd", { type: "string", description: "Restrict to agents under this dir" })
|
|
1404
|
+
.help(false)
|
|
1405
|
+
.version(false)
|
|
1406
|
+
.exitProcess(false);
|
|
1407
|
+
|
|
1408
|
+
const argv = await y.parseAsync();
|
|
1409
|
+
const opts: CommonOpts = {
|
|
1410
|
+
all: true,
|
|
1411
|
+
active: false,
|
|
1412
|
+
json: false,
|
|
1413
|
+
latest: argv.latest,
|
|
1414
|
+
cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null,
|
|
1415
|
+
};
|
|
1416
|
+
const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
|
|
973
1417
|
|
|
974
1418
|
if (!keyword) throw new Error("usage: ay status <keyword> [--watch] [--interval=N]");
|
|
975
1419
|
|
|
976
|
-
|
|
977
|
-
|
|
1420
|
+
{
|
|
1421
|
+
const remote = await resolveRemoteSpec(keyword);
|
|
1422
|
+
if (remote) return runRemoteStatus(remote);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const watch = argv.watch;
|
|
1426
|
+
const intervalFlag = argv.interval;
|
|
978
1427
|
const intervalMs = Math.max(500, (Number.isFinite(intervalFlag) ? intervalFlag : 2) * 1000);
|
|
979
1428
|
|
|
980
1429
|
const record = await resolveOne(keyword, opts);
|