agent-yes 1.297.0 → 1.298.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-ByHMsXAS.js → SUPPORTED_CLIS-BuCUpIsD.js} +3 -3
- package/dist/{SUPPORTED_CLIS-BFmQL5we.js → SUPPORTED_CLIS-CghmN9y4.js} +2 -2
- package/dist/{agentShare-5ZOUUzqH.js → agentShare-C2z05Xu1.js} +2 -2
- package/dist/{callback-B5gNxIT_.js → callback-Blg3DkI3.js} +2 -2
- package/dist/{callback-DP-7Y8py.js → callback-DULGSaDT.js} +3 -3
- package/dist/cli.js +5 -5
- package/dist/{cmdPs-CROJ_bxk.js → cmdPs-CNue6rFE.js} +2 -2
- package/dist/index.js +2 -2
- package/dist/{notifyDaemon-849g16zO.js → notifyDaemon-BPpVmmj0.js} +2 -2
- package/dist/{parentPingLoop-EBXvSmoB.js → parentPingLoop-BPcqwhCS.js} +2 -2
- package/dist/{rustBinary-BN5vA22v.js → rustBinary-B_KR43x9.js} +2 -2
- package/dist/{rustBinary-DC-BbqOQ.js → rustBinary-D59v2mmy.js} +2 -2
- package/dist/{schedule-BZReJzie.js → schedule-2xXpYxCv.js} +4 -4
- package/dist/{serve-GoY2ofRT.js → serve-cvUh5Oab.js} +18 -18
- package/dist/{setup-DwQ-pEQQ.js → setup-Ba2SPIhs.js} +2 -2
- package/dist/{subcommands-BFcfYykF.js → subcommands-DoKUSvaF.js} +577 -49
- package/dist/{subcommands-CY_xRQwx.js → subcommands-btyolh0m.js} +1 -1
- package/dist/{terminal-Cdx9yEVr.js → terminal-DOY_IN5y.js} +2 -2
- package/dist/{trayApp-CvBKYwE0.js → trayApp-7pq0tDGE.js} +2 -2
- package/dist/trayApp-DAs4YVeV.js +5 -0
- package/dist/{ts-BGei8qPL.js → ts-Cet9EhtO.js} +3 -3
- package/dist/{versionChecker-DRTopFkA.js → versionChecker-BtYHFqjF.js} +2 -2
- package/dist/{widget-a48Lqs0Z.js → widget-DKt2rH9F.js} +3 -3
- package/dist/{ws-BuLkRFmu.js → ws-Bs9AVLZR.js} +2 -2
- package/dist/{ws-DeDxbSUZ.js → ws-DgZVBJje.js} +2 -2
- package/package.json +1 -1
- package/ts/subcommands.ts +660 -31
- package/ts/untilMatch.bun.spec.ts +173 -0
- package/ts/untilMatch.spec.ts +173 -0
- package/ts/untilMatch.ts +230 -0
- package/dist/trayApp-Dxs6p7R2.js +0 -5
|
@@ -317,6 +317,140 @@ function toEvent(a, ts) {
|
|
|
317
317
|
};
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
//#endregion
|
|
321
|
+
//#region ts/untilMatch.ts
|
|
322
|
+
/**
|
|
323
|
+
* Compile an `--until` spec into a line predicate.
|
|
324
|
+
*
|
|
325
|
+
* Throws on an empty pattern (a match-everything predicate would return on the
|
|
326
|
+
* agent's next line of output, which is never what the caller meant) and on an
|
|
327
|
+
* invalid regex, so the failure surfaces at argument-parse time rather than as
|
|
328
|
+
* an instant bogus exit 0.
|
|
329
|
+
*/
|
|
330
|
+
function compileUntil(spec) {
|
|
331
|
+
const { pattern, regex, ignoreCase } = spec;
|
|
332
|
+
if (pattern.length === 0) throw new Error("--until requires a non-empty pattern");
|
|
333
|
+
if (!regex) {
|
|
334
|
+
if (!ignoreCase) return (line) => line.includes(pattern);
|
|
335
|
+
const needle = pattern.toLowerCase();
|
|
336
|
+
return (line) => line.toLowerCase().includes(needle);
|
|
337
|
+
}
|
|
338
|
+
let re;
|
|
339
|
+
try {
|
|
340
|
+
re = new RegExp(pattern, ignoreCase ? "i" : "");
|
|
341
|
+
} catch (e) {
|
|
342
|
+
throw new Error(`invalid --until regex: ${e.message}`);
|
|
343
|
+
}
|
|
344
|
+
return (line) => re.test(line);
|
|
345
|
+
}
|
|
346
|
+
function makeTally(match, needed = 1) {
|
|
347
|
+
if (!Number.isInteger(needed) || needed < 1) throw new Error(`--count must be a positive integer (got ${needed})`);
|
|
348
|
+
let hits = 0;
|
|
349
|
+
let last = null;
|
|
350
|
+
const tally = {
|
|
351
|
+
get hits() {
|
|
352
|
+
return hits;
|
|
353
|
+
},
|
|
354
|
+
needed,
|
|
355
|
+
get last() {
|
|
356
|
+
return last;
|
|
357
|
+
},
|
|
358
|
+
get done() {
|
|
359
|
+
return hits >= needed;
|
|
360
|
+
},
|
|
361
|
+
feed(line) {
|
|
362
|
+
if (hits < needed && match(line)) {
|
|
363
|
+
hits++;
|
|
364
|
+
last = line;
|
|
365
|
+
}
|
|
366
|
+
return hits >= needed;
|
|
367
|
+
},
|
|
368
|
+
feedAll(lines) {
|
|
369
|
+
for (const line of lines) if (tally.feed(line)) return true;
|
|
370
|
+
return hits >= needed;
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
return tally;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* The lines strictly after the LAST occurrence of `anchor` — the tail of a log
|
|
377
|
+
* that a follower has not seen yet, given the newest line it had seen.
|
|
378
|
+
*
|
|
379
|
+
* Needed because an exiting agent's log is not the file the follower was reading:
|
|
380
|
+
* the runtime renders the scrollback to a new path and unlinks the raw log, so the
|
|
381
|
+
* final lines must be recovered from a file with unknown overlap. Aligning on the
|
|
382
|
+
* newest already-seen line — from the END, so a repeated shell prompt resolves to
|
|
383
|
+
* the most recent one — bounds that overlap without byte offsets.
|
|
384
|
+
*
|
|
385
|
+
* Returns null when there is no anchor or it isn't present: the caller must then
|
|
386
|
+
* skip the drain rather than fall back to scanning everything, which could report
|
|
387
|
+
* a match printed before the wait even began.
|
|
388
|
+
*/
|
|
389
|
+
function linesAfterAnchor(lines, anchor) {
|
|
390
|
+
if (anchor === null) return null;
|
|
391
|
+
const at = lines.lastIndexOf(anchor);
|
|
392
|
+
if (at < 0) return null;
|
|
393
|
+
return lines.slice(at + 1);
|
|
394
|
+
}
|
|
395
|
+
function makeJudge(tally, failTally) {
|
|
396
|
+
let outcome = null;
|
|
397
|
+
let matched = null;
|
|
398
|
+
const judge = {
|
|
399
|
+
get settled() {
|
|
400
|
+
return outcome !== null;
|
|
401
|
+
},
|
|
402
|
+
get outcome() {
|
|
403
|
+
return outcome;
|
|
404
|
+
},
|
|
405
|
+
get matched() {
|
|
406
|
+
return matched;
|
|
407
|
+
},
|
|
408
|
+
test(line) {
|
|
409
|
+
if (outcome !== null) return true;
|
|
410
|
+
if (failTally?.feed(line)) {
|
|
411
|
+
outcome = "failed";
|
|
412
|
+
matched = failTally.last;
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
if (tally.feed(line)) {
|
|
416
|
+
outcome = "match";
|
|
417
|
+
matched = tally.last;
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
return false;
|
|
421
|
+
},
|
|
422
|
+
testAll(lines) {
|
|
423
|
+
for (const line of lines) if (judge.test(line)) return true;
|
|
424
|
+
return outcome !== null;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
return judge;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Exit code for an `--until` run. Mirrors the code family already established by
|
|
431
|
+
* `ay status --wait` / `ay result --wait`, so an orchestrator can branch on the
|
|
432
|
+
* status without parsing stdout:
|
|
433
|
+
*
|
|
434
|
+
* 0 the pattern appeared
|
|
435
|
+
* 1 the agent exited without ever printing it (it's done; it never will)
|
|
436
|
+
* 2 `--timeout` elapsed, or we were signalled away, with no match
|
|
437
|
+
* 3 a `--fail-on` pattern appeared first — the thing went wrong, and waiting
|
|
438
|
+
* out the timeout would only delay finding out
|
|
439
|
+
*
|
|
440
|
+
* `stopped` (Ctrl-C / SIGTERM, e.g. an outer `timeout(1)`) deliberately shares
|
|
441
|
+
* exit 2 with a timeout rather than the plain-follow 0: for a caller using this
|
|
442
|
+
* as a predicate, "I was killed" must never read as "the pattern matched".
|
|
443
|
+
*/
|
|
444
|
+
function untilExitCode(outcome) {
|
|
445
|
+
switch (outcome) {
|
|
446
|
+
case "match": return 0;
|
|
447
|
+
case "exited": return 1;
|
|
448
|
+
case "timeout":
|
|
449
|
+
case "stopped": return 2;
|
|
450
|
+
case "failed": return 3;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
320
454
|
//#endregion
|
|
321
455
|
//#region ts/resultEnvelope.ts
|
|
322
456
|
/**
|
|
@@ -1546,7 +1680,7 @@ async function runSubcommand(argv) {
|
|
|
1546
1680
|
switch (sub) {
|
|
1547
1681
|
case "ls":
|
|
1548
1682
|
case "list": return await cmdLs(rest);
|
|
1549
|
-
case "ps": return await (await import("./cmdPs-
|
|
1683
|
+
case "ps": return await (await import("./cmdPs-CNue6rFE.js")).cmdPs(rest);
|
|
1550
1684
|
case "status": return await cmdStatus(rest);
|
|
1551
1685
|
case "whoami": return await cmdWhoami(rest);
|
|
1552
1686
|
case "result": return await cmdResult(rest);
|
|
@@ -1593,35 +1727,35 @@ async function runSubcommand(argv) {
|
|
|
1593
1727
|
return cmdCh(rest);
|
|
1594
1728
|
}
|
|
1595
1729
|
case "term": {
|
|
1596
|
-
const { cmdTerm } = await import("./terminal-
|
|
1730
|
+
const { cmdTerm } = await import("./terminal-DOY_IN5y.js");
|
|
1597
1731
|
return cmdTerm(rest);
|
|
1598
1732
|
}
|
|
1599
1733
|
case "widget": {
|
|
1600
|
-
const { cmdWidget } = await import("./widget-
|
|
1734
|
+
const { cmdWidget } = await import("./widget-DKt2rH9F.js");
|
|
1601
1735
|
return cmdWidget(rest);
|
|
1602
1736
|
}
|
|
1603
1737
|
case "mint": {
|
|
1604
|
-
const { cmdMint } = await import("./widget-
|
|
1738
|
+
const { cmdMint } = await import("./widget-DKt2rH9F.js");
|
|
1605
1739
|
return cmdMint(rest);
|
|
1606
1740
|
}
|
|
1607
1741
|
case "serve": {
|
|
1608
|
-
const { cmdServe } = await import("./serve-
|
|
1742
|
+
const { cmdServe } = await import("./serve-cvUh5Oab.js");
|
|
1609
1743
|
return cmdServe(rest);
|
|
1610
1744
|
}
|
|
1611
1745
|
case "tray": {
|
|
1612
|
-
const { cmdTray } = await import("./trayApp-
|
|
1746
|
+
const { cmdTray } = await import("./trayApp-DAs4YVeV.js");
|
|
1613
1747
|
return cmdTray(rest);
|
|
1614
1748
|
}
|
|
1615
1749
|
case "setup": {
|
|
1616
|
-
const { cmdSetup } = await import("./setup-
|
|
1750
|
+
const { cmdSetup } = await import("./setup-Ba2SPIhs.js");
|
|
1617
1751
|
return cmdSetup(rest);
|
|
1618
1752
|
}
|
|
1619
1753
|
case "ws": {
|
|
1620
|
-
const { cmdWs } = await import("./ws-
|
|
1754
|
+
const { cmdWs } = await import("./ws-Bs9AVLZR.js");
|
|
1621
1755
|
return cmdWs(rest);
|
|
1622
1756
|
}
|
|
1623
1757
|
case "schedule": {
|
|
1624
|
-
const { cmdSchedule } = await import("./schedule-
|
|
1758
|
+
const { cmdSchedule } = await import("./schedule-2xXpYxCv.js");
|
|
1625
1759
|
return cmdSchedule(rest);
|
|
1626
1760
|
}
|
|
1627
1761
|
case "remote": {
|
|
@@ -1633,7 +1767,7 @@ async function runSubcommand(argv) {
|
|
|
1633
1767
|
return cmdExpose(rest);
|
|
1634
1768
|
}
|
|
1635
1769
|
case "callback": {
|
|
1636
|
-
const { cmdCallback } = await import("./callback-
|
|
1770
|
+
const { cmdCallback } = await import("./callback-Blg3DkI3.js");
|
|
1637
1771
|
return cmdCallback(rest);
|
|
1638
1772
|
}
|
|
1639
1773
|
case "reap":
|
|
@@ -1644,7 +1778,7 @@ async function runSubcommand(argv) {
|
|
|
1644
1778
|
return await cmdDsh(rest);
|
|
1645
1779
|
}
|
|
1646
1780
|
case "gc": {
|
|
1647
|
-
const { gcOldBinaryDirs } = await import("./rustBinary-
|
|
1781
|
+
const { gcOldBinaryDirs } = await import("./rustBinary-D59v2mmy.js");
|
|
1648
1782
|
const { gcLogs } = await import("./globalPidIndex-DC93JhHQ.js");
|
|
1649
1783
|
const bins = gcOldBinaryDirs();
|
|
1650
1784
|
const logs = await gcLogs();
|
|
@@ -1705,7 +1839,7 @@ async function cmdHelp(managerCommands = true) {
|
|
|
1705
1839
|
const wsLines = managerCommands ? " ay ws ls [--status] list <owner>/<repo>/tree/<branch> workspaces\n ay ws new <owner>/<repo>[@branch] clone/refresh a workspace (ay ws help for more)\n" : ``;
|
|
1706
1840
|
const self = process.env.AGENT_YES_PID ? await resolveSender() : null;
|
|
1707
1841
|
const agentSection = self ? await buildAgentContextSection(self) : "";
|
|
1708
|
-
process.stdout.write(agentSection + "ay - agent-yes CLI\n\nManagement:\n ay ls [keyword] list running agents\n ay ps [keyword] per-agent CPU/RSS, rolled up over each\n agent's whole process tree, + box vitals\n ay tail [-f] [-n N] <keyword> last N lines (96), -f to follow\n ay read <keyword> [page opts] paginate: --last/--head N, --range A:B,\n --before-line L [--limit N]\n ay cat <keyword> full log\n ay head <keyword> first N lines\n ay hist [-n 6] [--all] [--json] past agent conversations (claude/codex\n transcripts, incl. exited sessions);\n this cwd unless --all\n ay send <keyword> <msg> send a message (keyword '.' = agent in this cwd)\n ay msgs [keyword] [--in|--out] inter-agent message log (sent + received)\n ay ch mk|join|send|read|tail <topic> local-first E2E channels: AI ↔ humans on a topic (ay ch help)\n ay term embed <pid> <script> to embed a live read-only agent terminal in a page (ay term help)\n ay widget ls | read selection|dom read an opted-in page widget's context (selection/DOM) (ay widget help)\n ay mint <target> --caps ... mint a scoped, short-TTL capability token (embeddable in a page)\n ay key <keyword> <key...> send raw keystrokes (down/up/enter/esc/…) — drives menus\n ay select <keyword> <N> pick option N of a needs_input selection menu\n ay attach <keyword> interactive attach (detach: Ctrl-\\)\n ay stop <keyword> graceful shutdown (/exit for claude/codex)\n ay exit <keyword> [reason] graceful shutdown, recording who/why (= 'ay send <kw> exit')\n ay restart <keyword> [--fresh] stop (if live) + relaunch resuming the session; --fresh replays the prompt\n ay status <keyword> agent status snapshot\n ay notify watch --unread get notified when sub-agents finish/stuck/crash (writes to inbox)\n ay whoami [--json] (inside an agent) your own registry identity + reply address\n ay result <keyword> [--wait] pull an agent's structured result envelope\n ay result set '<json>' (inside an agent) deposit your result envelope\n ay reap kill process groups leaked by dead agents\n ay gc reclaim old-version binary cache dirs + stale session logs\n ay dsh-legacy [args...] launch the DeepSeek Harness terminal client (dsh-tui)\n" + wsLines + "\nRemote:\n" + setupLine + " ay schedule <when> <cli> -- <msg> run an agent on a schedule (HH:MM or cron)\n ay serve [--port N] start HTTP API server (prints token)\n ay serve status show serve daemon/server status\n ay remote add <alias> http://<token>@<host>:<port>\n ay remote ls / rm <alias> manage saved remotes\n ay expose <port> share localhost:<port> at https://<id>.agent-yes.com (private link)\n ay callback --expires 7d mint an embeddable message-me widget for one agent\n ay ls <token>@<host>:<port> connect inline (no alias needed)\n ay send <token>@<host>:<port>:<kw> <msg>\n\nRun an agent (naming a CLI is what launches one — bare 'ay' shows this help):\n ay <claude|codex|...> [options] -- [prompt]\n ay claude -- \"fix the bug in auth.ts\"\n cy [options] -- [prompt] shortcut for 'ay claude' (bare 'cy' starts claude)\n ay claude --help full agent-runner options\n\nLabs (examples at https://github.com/snomiao/agent-yes/tree/main/lab):\n local-role-play/ designer + builder on one machine\n http-remote/ ay serve remote access demo\n p2p-pairing/ libp2p P2P (needs: cargo build --features swarm)\n");
|
|
1842
|
+
process.stdout.write(agentSection + "ay - agent-yes CLI\n\nManagement:\n ay ls [keyword] list running agents\n ay ps [keyword] per-agent CPU/RSS, rolled up over each\n agent's whole process tree, + box vitals\n ay tail [-f] [-n N] <keyword> last N lines (96), -f to follow\n ay tail <keyword> --until TEXT block until TEXT is printed, then exit 0\n [--timeout 10m] [-q] [--regex] (1 = agent exited without it, 2 = no match,\n [--fail-on TEXT] [--count N] 3 = --fail-on hit first)\n ay read <keyword> [page opts] paginate: --last/--head N, --range A:B,\n --before-line L [--limit N]\n ay cat <keyword> full log\n ay head <keyword> first N lines\n ay hist [-n 6] [--all] [--json] past agent conversations (claude/codex\n transcripts, incl. exited sessions);\n this cwd unless --all\n ay send <keyword> <msg> send a message (keyword '.' = agent in this cwd)\n ay msgs [keyword] [--in|--out] inter-agent message log (sent + received)\n ay ch mk|join|send|read|tail <topic> local-first E2E channels: AI ↔ humans on a topic (ay ch help)\n ay term embed <pid> <script> to embed a live read-only agent terminal in a page (ay term help)\n ay widget ls | read selection|dom read an opted-in page widget's context (selection/DOM) (ay widget help)\n ay mint <target> --caps ... mint a scoped, short-TTL capability token (embeddable in a page)\n ay key <keyword> <key...> send raw keystrokes (down/up/enter/esc/…) — drives menus\n ay select <keyword> <N> pick option N of a needs_input selection menu\n ay attach <keyword> interactive attach (detach: Ctrl-\\)\n ay stop <keyword> graceful shutdown (/exit for claude/codex)\n ay exit <keyword> [reason] graceful shutdown, recording who/why (= 'ay send <kw> exit')\n ay restart <keyword> [--fresh] stop (if live) + relaunch resuming the session; --fresh replays the prompt\n ay status <keyword> agent status snapshot\n ay notify watch --unread get notified when sub-agents finish/stuck/crash (writes to inbox)\n ay whoami [--json] (inside an agent) your own registry identity + reply address\n ay result <keyword> [--wait] pull an agent's structured result envelope\n ay result set '<json>' (inside an agent) deposit your result envelope\n ay reap kill process groups leaked by dead agents\n ay gc reclaim old-version binary cache dirs + stale session logs\n ay dsh-legacy [args...] launch the DeepSeek Harness terminal client (dsh-tui)\n" + wsLines + "\nRemote:\n" + setupLine + " ay schedule <when> <cli> -- <msg> run an agent on a schedule (HH:MM or cron)\n ay serve [--port N] start HTTP API server (prints token)\n ay serve status show serve daemon/server status\n ay remote add <alias> http://<token>@<host>:<port>\n ay remote ls / rm <alias> manage saved remotes\n ay expose <port> share localhost:<port> at https://<id>.agent-yes.com (private link)\n ay callback --expires 7d mint an embeddable message-me widget for one agent\n ay ls <token>@<host>:<port> connect inline (no alias needed)\n ay send <token>@<host>:<port>:<kw> <msg>\n\nRun an agent (naming a CLI is what launches one — bare 'ay' shows this help):\n ay <claude|codex|...> [options] -- [prompt]\n ay claude -- \"fix the bug in auth.ts\"\n cy [options] -- [prompt] shortcut for 'ay claude' (bare 'cy' starts claude)\n ay claude --help full agent-runner options\n\nLabs (examples at https://github.com/snomiao/agent-yes/tree/main/lab):\n local-role-play/ designer + builder on one machine\n http-remote/ ay serve remote access demo\n p2p-pairing/ libp2p P2P (needs: cargo build --features swarm)\n");
|
|
1709
1843
|
return 0;
|
|
1710
1844
|
}
|
|
1711
1845
|
function matchKeyword(record, keyword) {
|
|
@@ -1895,7 +2029,7 @@ async function runRemoteLs(remote, opts) {
|
|
|
1895
2029
|
}
|
|
1896
2030
|
return 0;
|
|
1897
2031
|
}
|
|
1898
|
-
async function runRemoteRead(remote, mode, follow, n, reconnectTimeoutMs = 12e4, _plain = false) {
|
|
2032
|
+
async function runRemoteRead(remote, mode, follow, n, reconnectTimeoutMs = 12e4, _plain = false, until) {
|
|
1899
2033
|
const keyword = remote.keyword ?? "";
|
|
1900
2034
|
if (!keyword) {
|
|
1901
2035
|
process.stderr.write("remote tail/cat/head requires a keyword (e.g. token@host:port:keyword)\n");
|
|
@@ -1903,12 +2037,67 @@ async function runRemoteRead(remote, mode, follow, n, reconnectTimeoutMs = 12e4,
|
|
|
1903
2037
|
}
|
|
1904
2038
|
if (mode === "tail" && follow) {
|
|
1905
2039
|
const ac = new AbortController();
|
|
2040
|
+
const judge = until ? makeJudge(until.tally, until.failTally) : null;
|
|
2041
|
+
let outcome = "stopped";
|
|
2042
|
+
const settle = (o) => {
|
|
2043
|
+
outcome = o;
|
|
2044
|
+
ac.abort();
|
|
2045
|
+
};
|
|
2046
|
+
const untilTimer = until && until.timeoutMs !== null ? setTimeout(() => settle("timeout"), until.timeoutMs) : null;
|
|
2047
|
+
let remoteDeadSince = null;
|
|
2048
|
+
let sawRemoteLive = false;
|
|
2049
|
+
const statusPoll = until ? setInterval(() => {
|
|
2050
|
+
(async () => {
|
|
2051
|
+
const liveness = await remoteAgentLiveness(remote, keyword);
|
|
2052
|
+
if (liveness === null) return;
|
|
2053
|
+
if (liveness === "live") {
|
|
2054
|
+
sawRemoteLive = true;
|
|
2055
|
+
remoteDeadSince = null;
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
if (!sawRemoteLive) return;
|
|
2059
|
+
if (remoteDeadSince === null) {
|
|
2060
|
+
remoteDeadSince = Date.now();
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2063
|
+
if (!judge?.settled) settle("exited");
|
|
2064
|
+
})();
|
|
2065
|
+
}, UNTIL_REMOTE_STATUS_POLL_MS) : null;
|
|
2066
|
+
statusPoll?.unref?.();
|
|
2067
|
+
/** Reassembles frames into lines; `flush` tests a trailing partial at the end. */
|
|
2068
|
+
let lineBuf = "";
|
|
2069
|
+
const consume = (text, test) => {
|
|
2070
|
+
if (!until?.quiet) {
|
|
2071
|
+
process.stdout.write(text);
|
|
2072
|
+
if (!text.endsWith("\n")) process.stdout.write("\n");
|
|
2073
|
+
}
|
|
2074
|
+
if (!judge) return;
|
|
2075
|
+
lineBuf += text;
|
|
2076
|
+
const lines = lineBuf.split("\n");
|
|
2077
|
+
lineBuf = lines.pop() ?? "";
|
|
2078
|
+
if (!test) return;
|
|
2079
|
+
if (judge.testAll(lines.map((l) => l.trimEnd()))) settle(judge.outcome ?? "match");
|
|
2080
|
+
};
|
|
2081
|
+
const flushLineBuf = () => {
|
|
2082
|
+
if (!judge || lineBuf.length === 0) return;
|
|
2083
|
+
const last = lineBuf.trimEnd();
|
|
2084
|
+
lineBuf = "";
|
|
2085
|
+
if (last && judge.test(last)) settle(judge.outcome ?? "match");
|
|
2086
|
+
};
|
|
2087
|
+
const finishUntil = () => {
|
|
2088
|
+
if (untilTimer) clearTimeout(untilTimer);
|
|
2089
|
+
if (statusPoll) clearInterval(statusPoll);
|
|
2090
|
+
return reportUntil(outcome, judge?.matched ?? null, until.quiet, until.failPattern, {
|
|
2091
|
+
hits: until.tally.hits,
|
|
2092
|
+
needed: until.tally.needed
|
|
2093
|
+
});
|
|
2094
|
+
};
|
|
1906
2095
|
const disposeSignals = installStreamSignals(() => ac.abort());
|
|
1907
2096
|
ac.signal.addEventListener("abort", disposeSignals, { once: true });
|
|
1908
2097
|
const deadline = Date.now() + reconnectTimeoutMs;
|
|
1909
2098
|
let delay = 1e3;
|
|
1910
2099
|
let attempt = 0;
|
|
1911
|
-
process.stderr.write(`[remote ${remote.label} ${keyword}]\nfollowing... (Ctrl-C to stop, timeout: ${Math.round(reconnectTimeoutMs / 1e3)}s)\n`);
|
|
2100
|
+
process.stderr.write(until ? `[remote ${remote.label} ${keyword}]\n${untilBanner(until)}` : `[remote ${remote.label} ${keyword}]\nfollowing... (Ctrl-C to stop, timeout: ${Math.round(reconnectTimeoutMs / 1e3)}s)\n`);
|
|
1912
2101
|
while (!ac.signal.aborted) try {
|
|
1913
2102
|
const res = await fetch(`${remote.url}/api/tail/${encodeURIComponent(keyword)}`, {
|
|
1914
2103
|
headers: {
|
|
@@ -1924,12 +2113,16 @@ async function runRemoteRead(remote, mode, follow, n, reconnectTimeoutMs = 12e4,
|
|
|
1924
2113
|
}
|
|
1925
2114
|
throw new Error(`HTTP ${res.status}`);
|
|
1926
2115
|
}
|
|
1927
|
-
if (attempt > 0)
|
|
2116
|
+
if (attempt > 0) {
|
|
2117
|
+
process.stderr.write("remote: reconnected\n");
|
|
2118
|
+
if (until) process.stderr.write("remote: output during the gap was not tested for --until\n");
|
|
2119
|
+
}
|
|
1928
2120
|
delay = 1e3;
|
|
1929
2121
|
const reader = res.body.getReader();
|
|
1930
2122
|
const dec = new TextDecoder();
|
|
1931
2123
|
let buf = "";
|
|
1932
|
-
|
|
2124
|
+
let firstFrame = true;
|
|
2125
|
+
while (!ac.signal.aborted) {
|
|
1933
2126
|
const { done, value } = await reader.read();
|
|
1934
2127
|
if (done) break;
|
|
1935
2128
|
buf += dec.decode(value, { stream: true });
|
|
@@ -1939,16 +2132,39 @@ async function runRemoteRead(remote, mode, follow, n, reconnectTimeoutMs = 12e4,
|
|
|
1939
2132
|
if (!line.startsWith("data: ")) continue;
|
|
1940
2133
|
try {
|
|
1941
2134
|
const text = JSON.parse(line.slice(6));
|
|
1942
|
-
|
|
1943
|
-
|
|
2135
|
+
const isContext = firstFrame;
|
|
2136
|
+
firstFrame = false;
|
|
2137
|
+
consume(text, !isContext || Boolean(until?.matchBacklog));
|
|
2138
|
+
if (ac.signal.aborted) break;
|
|
1944
2139
|
} catch {}
|
|
1945
2140
|
}
|
|
2141
|
+
if (ac.signal.aborted) break;
|
|
2142
|
+
}
|
|
2143
|
+
if (until) {
|
|
2144
|
+
if (judge?.settled) return finishUntil();
|
|
2145
|
+
if (ac.signal.aborted) {
|
|
2146
|
+
flushLineBuf();
|
|
2147
|
+
return finishUntil();
|
|
2148
|
+
}
|
|
2149
|
+
flushLineBuf();
|
|
2150
|
+
if (!judge?.settled) outcome = "exited";
|
|
2151
|
+
return finishUntil();
|
|
1946
2152
|
}
|
|
1947
2153
|
break;
|
|
1948
2154
|
} catch (e) {
|
|
1949
|
-
if (e.name === "AbortError" || ac.signal.aborted)
|
|
2155
|
+
if (e.name === "AbortError" || ac.signal.aborted) {
|
|
2156
|
+
if (until) {
|
|
2157
|
+
flushLineBuf();
|
|
2158
|
+
return finishUntil();
|
|
2159
|
+
}
|
|
2160
|
+
return 0;
|
|
2161
|
+
}
|
|
1950
2162
|
if (Date.now() >= deadline) {
|
|
1951
2163
|
process.stderr.write(`remote: timeout after ${Math.round(reconnectTimeoutMs / 1e3)}s, giving up\n`);
|
|
2164
|
+
if (until) {
|
|
2165
|
+
outcome = "timeout";
|
|
2166
|
+
return finishUntil();
|
|
2167
|
+
}
|
|
1952
2168
|
return 1;
|
|
1953
2169
|
}
|
|
1954
2170
|
process.stderr.write(`remote: disconnected (${e.message}), retrying in ${delay / 1e3}s…\n`);
|
|
@@ -1959,10 +2175,20 @@ async function runRemoteRead(remote, mode, follow, n, reconnectTimeoutMs = 12e4,
|
|
|
1959
2175
|
reject(/* @__PURE__ */ new Error("abort"));
|
|
1960
2176
|
});
|
|
1961
2177
|
}).catch(() => {});
|
|
1962
|
-
if (ac.signal.aborted)
|
|
2178
|
+
if (ac.signal.aborted) {
|
|
2179
|
+
if (until) {
|
|
2180
|
+
flushLineBuf();
|
|
2181
|
+
return finishUntil();
|
|
2182
|
+
}
|
|
2183
|
+
return 0;
|
|
2184
|
+
}
|
|
1963
2185
|
delay = Math.min(delay * 2, 3e4);
|
|
1964
2186
|
attempt++;
|
|
1965
2187
|
}
|
|
2188
|
+
if (until) {
|
|
2189
|
+
flushLineBuf();
|
|
2190
|
+
return finishUntil();
|
|
2191
|
+
}
|
|
1966
2192
|
return 0;
|
|
1967
2193
|
}
|
|
1968
2194
|
const params = new URLSearchParams({
|
|
@@ -2778,12 +3004,54 @@ function truncate(s, n) {
|
|
|
2778
3004
|
if (s.length <= n) return s;
|
|
2779
3005
|
return s.slice(0, n - 1) + "…";
|
|
2780
3006
|
}
|
|
3007
|
+
/**
|
|
3008
|
+
* Parse a human duration (`30s`, `10m`) to ms, or null when it isn't one.
|
|
3009
|
+
*
|
|
3010
|
+
* `ms()`'s parse overload is typed to a template-literal `StringValue`, which a
|
|
3011
|
+
* CLI flag (a plain `string`) never satisfies, and it returns `undefined` — not a
|
|
3012
|
+
* number — for unparseable input. Both are handled here once so the call sites
|
|
3013
|
+
* just branch on null.
|
|
3014
|
+
*/
|
|
3015
|
+
function parseDurationMs(value) {
|
|
3016
|
+
const parsed = ms(value);
|
|
3017
|
+
return typeof parsed === "number" && Number.isFinite(parsed) ? parsed : null;
|
|
3018
|
+
}
|
|
2781
3019
|
async function cmdRead(rest, { mode }) {
|
|
2782
|
-
const argv = await yargs(rest).usage("Usage: ay read/cat/tail/head <keyword> [options]\n\nPagination (static read; render the log once, window the rendered lines):\n --last N | --head N last / first N lines\n --range A:B lines A..B (1-indexed, inclusive)\n --before-line L [--limit N] the page of N lines ending just above line L").option("follow", {
|
|
3020
|
+
const argv = await yargs(rest).usage("Usage: ay read/cat/tail/head <keyword> [options]\n\nPagination (static read; render the log once, window the rendered lines):\n --last N | --head N last / first N lines\n --range A:B lines A..B (1-indexed, inclusive)\n --before-line L [--limit N] the page of N lines ending just above line L\n\nWait for output (follow with a predicate; exit 0 matched / 1 exited /\n2 no match / 3 --fail-on hit):\n --until TEXT [--timeout 10m] return as soon as TEXT is printed\n --fail-on TEXT give up early (exit 3) if TEXT shows up first\n --count N require N hits of --until (default 1)\n --regex | -i | -q regex / case-insensitive / only print the match").option("follow", {
|
|
2783
3021
|
alias: "f",
|
|
2784
3022
|
type: "boolean",
|
|
2785
3023
|
default: false,
|
|
2786
3024
|
description: "Follow log output (Ctrl-C to stop)"
|
|
3025
|
+
}).option("until", {
|
|
3026
|
+
type: "string",
|
|
3027
|
+
description: "Follow until this text appears in the output, then exit. Implies -f. Exit 0 matched, 1 agent exited without it, 2 --timeout/interrupted, 3 --fail-on hit first. Literal substring by default; matches only output arriving from now on."
|
|
3028
|
+
}).option("fail-on", {
|
|
3029
|
+
type: "string",
|
|
3030
|
+
description: "Give up with exit 3 if this text appears before --until does (e.g. --until 'all tests passed' --fail-on 'FAILED')"
|
|
3031
|
+
}).option("count", {
|
|
3032
|
+
type: "number",
|
|
3033
|
+
description: "Require this many --until hits before returning (default 1)"
|
|
3034
|
+
}).option("regex", {
|
|
3035
|
+
type: "boolean",
|
|
3036
|
+
default: false,
|
|
3037
|
+
description: "Treat --until as a regular expression"
|
|
3038
|
+
}).option("ignore-case", {
|
|
3039
|
+
alias: "i",
|
|
3040
|
+
type: "boolean",
|
|
3041
|
+
default: false,
|
|
3042
|
+
description: "Case-insensitive --until match"
|
|
3043
|
+
}).option("timeout", {
|
|
3044
|
+
type: "string",
|
|
3045
|
+
description: "Give up waiting for --until after this long (e.g. 30s, 10m)"
|
|
3046
|
+
}).option("match-backlog", {
|
|
3047
|
+
type: "boolean",
|
|
3048
|
+
default: false,
|
|
3049
|
+
description: "Also test the context window printed before following. Off by default: a hit from a PREVIOUS run would return instantly and wrongly."
|
|
3050
|
+
}).option("quiet", {
|
|
3051
|
+
alias: "q",
|
|
3052
|
+
type: "boolean",
|
|
3053
|
+
default: false,
|
|
3054
|
+
description: "With --until, print only the matching line (no streamed output)"
|
|
2787
3055
|
}).option("n", {
|
|
2788
3056
|
type: "number",
|
|
2789
3057
|
description: "Number of lines (default: 96 for tail/head)"
|
|
@@ -2823,7 +3091,35 @@ async function cmdRead(rest, { mode }) {
|
|
|
2823
3091
|
description: "Seconds before giving up reconnecting remote SSE (default: 120)"
|
|
2824
3092
|
}).help(false).version(false).exitProcess(false).parseAsync();
|
|
2825
3093
|
ensureEpipeExit();
|
|
2826
|
-
const
|
|
3094
|
+
const until = typeof argv.until === "string" ? argv.until : null;
|
|
3095
|
+
const plain = Boolean(argv.plain) || until !== null || !process.stdout.isTTY;
|
|
3096
|
+
const failOn = typeof argv["fail-on"] === "string" ? argv["fail-on"] : null;
|
|
3097
|
+
const untilOnlyFlags = [
|
|
3098
|
+
["--regex", argv.regex],
|
|
3099
|
+
["--ignore-case", argv["ignore-case"]],
|
|
3100
|
+
["--match-backlog", argv["match-backlog"]],
|
|
3101
|
+
["--timeout", typeof argv.timeout === "string" && argv.timeout.length > 0],
|
|
3102
|
+
["--fail-on", failOn !== null],
|
|
3103
|
+
["--count", argv.count !== void 0]
|
|
3104
|
+
];
|
|
3105
|
+
if (until === null) {
|
|
3106
|
+
const stray = untilOnlyFlags.find(([, set]) => set);
|
|
3107
|
+
if (stray) throw new Error(`${stray[0]} requires --until <pattern>`);
|
|
3108
|
+
}
|
|
3109
|
+
const compileSpec = (pattern) => compileUntil({
|
|
3110
|
+
pattern,
|
|
3111
|
+
regex: argv.regex,
|
|
3112
|
+
ignoreCase: argv["ignore-case"]
|
|
3113
|
+
});
|
|
3114
|
+
const untilCount = argv.count === void 0 ? 1 : Number(argv.count);
|
|
3115
|
+
if (until !== null && (!Number.isInteger(untilCount) || untilCount < 1)) throw new Error(`--count must be a positive integer (got ${argv.count})`);
|
|
3116
|
+
const untilMatch = until !== null ? compileSpec(until) : null;
|
|
3117
|
+
const failMatch = failOn !== null ? compileSpec(failOn) : null;
|
|
3118
|
+
let untilTimeoutMs = null;
|
|
3119
|
+
if (typeof argv.timeout === "string" && argv.timeout.length > 0) {
|
|
3120
|
+
untilTimeoutMs = parseDurationMs(argv.timeout);
|
|
3121
|
+
if (untilTimeoutMs === null) throw new Error(`invalid --timeout value: ${argv.timeout}`);
|
|
3122
|
+
}
|
|
2827
3123
|
const opts = {
|
|
2828
3124
|
all: argv.all,
|
|
2829
3125
|
active: false,
|
|
@@ -2837,9 +3133,20 @@ async function cmdRead(rest, { mode }) {
|
|
|
2837
3133
|
const nFlag2 = argv.n;
|
|
2838
3134
|
const n2 = nFlag2 !== void 0 && Number.isFinite(nFlag2) && nFlag2 > 0 ? Math.floor(nFlag2) : mode === "cat" ? 0 : 96;
|
|
2839
3135
|
const reconnectTimeoutMs = (argv["reconnect-timeout"] ?? 120) * 1e3;
|
|
2840
|
-
if (remote)
|
|
3136
|
+
if (remote) {
|
|
3137
|
+
if (untilMatch === null) return runRemoteRead(remote, mode, argv.follow, n2, reconnectTimeoutMs, plain);
|
|
3138
|
+
return runRemoteRead(remote, mode, true, n2, reconnectTimeoutMs, plain, {
|
|
3139
|
+
tally: makeTally(untilMatch, untilCount),
|
|
3140
|
+
failTally: failMatch ? makeTally(failMatch, 1) : null,
|
|
3141
|
+
pattern: until,
|
|
3142
|
+
failPattern: failOn,
|
|
3143
|
+
quiet: Boolean(argv.quiet),
|
|
3144
|
+
timeoutMs: untilTimeoutMs,
|
|
3145
|
+
matchBacklog: Boolean(argv["match-backlog"])
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
2841
3148
|
}
|
|
2842
|
-
const follow = argv.follow;
|
|
3149
|
+
const follow = argv.follow || until !== null;
|
|
2843
3150
|
const nFlag = argv.n;
|
|
2844
3151
|
const n = nFlag !== void 0 && Number.isFinite(nFlag) && nFlag > 0 ? Math.floor(nFlag) : mode === "cat" ? 0 : 96;
|
|
2845
3152
|
const record = await resolveOne(keyword, opts);
|
|
@@ -2866,10 +3173,31 @@ async function cmdRead(rest, { mode }) {
|
|
|
2866
3173
|
rows: size?.rows
|
|
2867
3174
|
});
|
|
2868
3175
|
process.stderr.write(header + "\n");
|
|
2869
|
-
|
|
2870
|
-
|
|
3176
|
+
if (!(untilMatch && argv.quiet)) {
|
|
3177
|
+
process.stdout.write(rendered);
|
|
3178
|
+
if (!rendered.endsWith("\n")) process.stdout.write("\n");
|
|
3179
|
+
}
|
|
3180
|
+
const tally = untilMatch ? makeTally(untilMatch, untilCount) : null;
|
|
3181
|
+
const failTally = failMatch ? makeTally(failMatch, 1) : null;
|
|
3182
|
+
if (tally && argv["match-backlog"]) {
|
|
3183
|
+
const lines = rendered.split("\n");
|
|
3184
|
+
if (failTally?.feedAll(lines)) return reportUntil("failed", failTally.last, Boolean(argv.quiet), failOn);
|
|
3185
|
+
if (tally.feedAll(lines)) return reportUntil("match", tally.last, Boolean(argv.quiet));
|
|
3186
|
+
}
|
|
2871
3187
|
setInterval(() => void recordRead(reader.key, record.pid), 3e4).unref?.();
|
|
2872
|
-
return plain ? followPlainLocal(logPath, buf, stats.size
|
|
3188
|
+
return plain ? followPlainLocal(logPath, buf, stats.size, tally ? {
|
|
3189
|
+
tally,
|
|
3190
|
+
failTally,
|
|
3191
|
+
pattern: until,
|
|
3192
|
+
failPattern: failOn,
|
|
3193
|
+
quiet: Boolean(argv.quiet),
|
|
3194
|
+
timeoutMs: untilTimeoutMs,
|
|
3195
|
+
matchBacklog: Boolean(argv["match-backlog"]),
|
|
3196
|
+
pid: record.pid
|
|
3197
|
+
} : void 0, {
|
|
3198
|
+
cols: size?.cols,
|
|
3199
|
+
rows: size?.rows
|
|
3200
|
+
}) : followRawLocal(logPath, buf, stats.size);
|
|
2873
3201
|
}
|
|
2874
3202
|
const allLines = await renderRawLogLines(buf, {
|
|
2875
3203
|
cols: size?.cols,
|
|
@@ -2936,9 +3264,12 @@ function installStreamSignals(stop) {
|
|
|
2936
3264
|
* Coalescing file watcher: re-reads `logPath` on every change, hands each newly
|
|
2937
3265
|
* appended byte range to `onChunk`, and never overlaps reads (a change that
|
|
2938
3266
|
* arrives mid-read is serviced once the current read finishes). `startOffset`
|
|
2939
|
-
* is where the already-emitted prefix ends. Resolves when `stop` is signalled
|
|
3267
|
+
* is where the already-emitted prefix ends. Resolves when `stop` is signalled,
|
|
3268
|
+
* or when `signal` aborts (how `--until` stops itself on a match / timeout /
|
|
3269
|
+
* the agent's death), and returns the byte offset it stopped at — so a caller
|
|
3270
|
+
* can drain whatever landed after the last watch event.
|
|
2940
3271
|
*/
|
|
2941
|
-
async function watchAppend(logPath, startOffset, onChunk, onStop) {
|
|
3272
|
+
async function watchAppend(logPath, startOffset, onChunk, onStop, signal) {
|
|
2942
3273
|
const { watch } = await import("fs");
|
|
2943
3274
|
let offset = startOffset;
|
|
2944
3275
|
let reading = false;
|
|
@@ -2952,12 +3283,15 @@ async function watchAppend(logPath, startOffset, onChunk, onStop) {
|
|
|
2952
3283
|
watcher.close();
|
|
2953
3284
|
} catch {}
|
|
2954
3285
|
dispose();
|
|
3286
|
+
signal?.removeEventListener("abort", finish);
|
|
2955
3287
|
onStop();
|
|
2956
3288
|
resolve();
|
|
2957
3289
|
};
|
|
2958
3290
|
const dispose = installStreamSignals(finish);
|
|
3291
|
+
if (signal) if (signal.aborted) queueMicrotask(finish);
|
|
3292
|
+
else signal.addEventListener("abort", finish);
|
|
2959
3293
|
const pump = async () => {
|
|
2960
|
-
if (reading) {
|
|
3294
|
+
if (reading || done) {
|
|
2961
3295
|
pending = true;
|
|
2962
3296
|
return;
|
|
2963
3297
|
}
|
|
@@ -2970,6 +3304,7 @@ async function watchAppend(logPath, startOffset, onChunk, onStop) {
|
|
|
2970
3304
|
} catch {
|
|
2971
3305
|
break;
|
|
2972
3306
|
}
|
|
3307
|
+
if (done) break;
|
|
2973
3308
|
if (full.length > offset) {
|
|
2974
3309
|
const chunk = full.slice(offset);
|
|
2975
3310
|
offset = full.length;
|
|
@@ -2981,6 +3316,7 @@ async function watchAppend(logPath, startOffset, onChunk, onStop) {
|
|
|
2981
3316
|
const watcher = watch(logPath, () => void pump());
|
|
2982
3317
|
pump();
|
|
2983
3318
|
});
|
|
3319
|
+
return offset;
|
|
2984
3320
|
}
|
|
2985
3321
|
/**
|
|
2986
3322
|
* Default (interactive) follow: append each new byte range with ANSI/control
|
|
@@ -2997,6 +3333,22 @@ async function followRawLocal(logPath, buf, startOffset = buf.length) {
|
|
|
2997
3333
|
}, () => {});
|
|
2998
3334
|
return 0;
|
|
2999
3335
|
}
|
|
3336
|
+
/**
|
|
3337
|
+
* `@xterm/headless`'s Terminal constructor, across runtimes.
|
|
3338
|
+
*
|
|
3339
|
+
* The package is CJS, so `await import()` resolves to `{ default: { Terminal } }`
|
|
3340
|
+
* under node while bun hoists the named exports — a plain
|
|
3341
|
+
* `const { Terminal } = await import(…)` is therefore `undefined` on node and
|
|
3342
|
+
* every render throws "Terminal is not a constructor". That is the runtime npm
|
|
3343
|
+
* users get from `dist/`, where it silently degraded the static render to the
|
|
3344
|
+
* ANSI-strip fallback and broke `ay tail -f` into a pipe outright.
|
|
3345
|
+
*/
|
|
3346
|
+
async function loadXtermTerminal() {
|
|
3347
|
+
const mod = await import("@xterm/headless");
|
|
3348
|
+
const Terminal = mod.Terminal ?? mod.default?.Terminal;
|
|
3349
|
+
if (!Terminal) throw new Error("@xterm/headless: no Terminal export");
|
|
3350
|
+
return Terminal;
|
|
3351
|
+
}
|
|
3000
3352
|
/** Absolute index (scrollback + viewport row) of the row the cursor sits on. */
|
|
3001
3353
|
function cursorAbs(term) {
|
|
3002
3354
|
return term.buffer.active.baseY + term.buffer.active.cursorY;
|
|
@@ -3025,12 +3377,11 @@ function finalizedLines(term, fromAbs) {
|
|
|
3025
3377
|
* settled, so the output is clean, newline-terminated, line-buffered text a
|
|
3026
3378
|
* script can read. On stop, flush the line the cursor is still sitting on.
|
|
3027
3379
|
*/
|
|
3028
|
-
async function followPlainLocal(logPath, buf, startOffset = buf.length) {
|
|
3029
|
-
process.stderr.write(`following... (plain; Ctrl-C / SIGTERM to stop)\n`);
|
|
3030
|
-
const
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
rows: 50,
|
|
3380
|
+
async function followPlainLocal(logPath, buf, startOffset = buf.length, until, geom) {
|
|
3381
|
+
process.stderr.write(until ? untilBanner(until) : `following... (plain; Ctrl-C / SIGTERM to stop)\n`);
|
|
3382
|
+
const term = new (await (loadXtermTerminal()))({
|
|
3383
|
+
cols: geom?.cols ?? 200,
|
|
3384
|
+
rows: geom?.rows ?? 50,
|
|
3034
3385
|
scrollback: 5e4,
|
|
3035
3386
|
allowProposedApi: true
|
|
3036
3387
|
});
|
|
@@ -3041,19 +3392,197 @@ async function followPlainLocal(logPath, buf, startOffset = buf.length) {
|
|
|
3041
3392
|
};
|
|
3042
3393
|
await feed(buf);
|
|
3043
3394
|
let emitted = cursorAbs(term);
|
|
3395
|
+
let anchor = null;
|
|
3396
|
+
for (let i = emitted; i >= Math.max(0, emitted - ANCHOR_LOOKBACK_ROWS); i--) {
|
|
3397
|
+
const line = lineAt(i);
|
|
3398
|
+
if (line) {
|
|
3399
|
+
anchor = line;
|
|
3400
|
+
break;
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
const ac = new AbortController();
|
|
3404
|
+
let outcome = "stopped";
|
|
3405
|
+
const judge = until ? makeJudge(until.tally, until.failTally) : null;
|
|
3406
|
+
const settled = () => judge?.settled === true;
|
|
3407
|
+
const testLine = (line) => {
|
|
3408
|
+
if (judge && !judge.settled && judge.test(line)) {
|
|
3409
|
+
outcome = judge.outcome ?? "match";
|
|
3410
|
+
ac.abort();
|
|
3411
|
+
}
|
|
3412
|
+
};
|
|
3044
3413
|
const flushCommitted = () => {
|
|
3045
|
-
for (const line of finalizedLines(term, emitted))
|
|
3414
|
+
for (const line of finalizedLines(term, emitted)) {
|
|
3415
|
+
if (!until?.quiet) process.stdout.write(line + "\n");
|
|
3416
|
+
if (!until) continue;
|
|
3417
|
+
if (line) anchor = line;
|
|
3418
|
+
testLine(line);
|
|
3419
|
+
}
|
|
3046
3420
|
emitted = cursorAbs(term);
|
|
3047
3421
|
};
|
|
3048
|
-
|
|
3422
|
+
const timer = until && until.timeoutMs !== null ? setTimeout(() => {
|
|
3423
|
+
outcome = "timeout";
|
|
3424
|
+
ac.abort();
|
|
3425
|
+
}, until.timeoutMs) : null;
|
|
3426
|
+
let deadSince = null;
|
|
3427
|
+
const liveness = until ? setInterval(() => {
|
|
3428
|
+
if (isPidAlive(until.pid)) {
|
|
3429
|
+
deadSince = null;
|
|
3430
|
+
return;
|
|
3431
|
+
}
|
|
3432
|
+
if (deadSince === null) {
|
|
3433
|
+
deadSince = Date.now();
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3436
|
+
outcome = "exited";
|
|
3437
|
+
ac.abort();
|
|
3438
|
+
}, UNTIL_LIVENESS_POLL_MS) : null;
|
|
3439
|
+
liveness?.unref?.();
|
|
3440
|
+
/**
|
|
3441
|
+
* Emit the cursor's own row — the line still being written — so the last
|
|
3442
|
+
* partial line isn't lost when we stop mid-stream, and so a pattern printed
|
|
3443
|
+
* without a trailing newline still counts (it IS on screen). Self-guarded to
|
|
3444
|
+
* run at most once: a row printed here may finalize later, and emitting it
|
|
3445
|
+
* again would duplicate the agent's last line of output.
|
|
3446
|
+
*/
|
|
3447
|
+
let partialFlushed = false;
|
|
3448
|
+
const flushPartial = () => {
|
|
3449
|
+
if (partialFlushed) return;
|
|
3450
|
+
partialFlushed = true;
|
|
3451
|
+
const last = lineAt(cursorAbs(term));
|
|
3452
|
+
if (!last) return;
|
|
3453
|
+
if (!until?.quiet) process.stdout.write(last + "\n");
|
|
3454
|
+
if (!until) return;
|
|
3455
|
+
anchor = last;
|
|
3456
|
+
testLine(last);
|
|
3457
|
+
};
|
|
3458
|
+
const stoppedAt = await watchAppend(logPath, startOffset, async (chunk) => {
|
|
3049
3459
|
await feed(chunk);
|
|
3050
3460
|
flushCommitted();
|
|
3051
|
-
},
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3461
|
+
}, flushCommitted, until ? ac.signal : void 0);
|
|
3462
|
+
if (timer) clearTimeout(timer);
|
|
3463
|
+
if (liveness) clearInterval(liveness);
|
|
3464
|
+
if (!until) {
|
|
3465
|
+
flushPartial();
|
|
3466
|
+
return 0;
|
|
3467
|
+
}
|
|
3468
|
+
if (!settled()) {
|
|
3469
|
+
const tailBytes = await readFile(logPath).catch(() => null);
|
|
3470
|
+
if (tailBytes === null) {
|
|
3471
|
+
flushPartial();
|
|
3472
|
+
await drainFinalLog(until.pid, logPath, anchor, until, testLine);
|
|
3473
|
+
} else if (tailBytes.length > stoppedAt) {
|
|
3474
|
+
await feed(tailBytes.slice(stoppedAt));
|
|
3475
|
+
flushCommitted();
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
flushPartial();
|
|
3479
|
+
return reportUntil(outcome, judge?.matched ?? null, until.quiet, until.failPattern, {
|
|
3480
|
+
hits: until.tally.hits,
|
|
3481
|
+
needed: until.tally.needed
|
|
3055
3482
|
});
|
|
3056
|
-
|
|
3483
|
+
}
|
|
3484
|
+
/** Cadence of the `--until` liveness poll; two ticks are needed to call it dead. */
|
|
3485
|
+
const UNTIL_LIVENESS_POLL_MS = 500;
|
|
3486
|
+
/**
|
|
3487
|
+
* Cadence of the REMOTE liveness poll; two ticks are needed to call it dead.
|
|
3488
|
+
* Slower than the local one because each tick is an HTTP round trip, and the
|
|
3489
|
+
* stream itself — not this poll — is what a wait normally ends on.
|
|
3490
|
+
*/
|
|
3491
|
+
const UNTIL_REMOTE_STATUS_POLL_MS = 2e3;
|
|
3492
|
+
/**
|
|
3493
|
+
* Whether a remote agent is still running: "live", "gone" (the host answered and
|
|
3494
|
+
* the agent is exited or no longer listed), or null when the poll itself failed.
|
|
3495
|
+
*
|
|
3496
|
+
* Asks `/api/ls`, not `/api/status/<kw>`: only the TS server serves the latter,
|
|
3497
|
+
* while the Rust server — the default `ay serve` — has no such route and 404s
|
|
3498
|
+
* every poll, so a status-based probe silently never fires.
|
|
3499
|
+
*
|
|
3500
|
+
* Null is deliberately distinct from "gone": a dropped packet or a restarting
|
|
3501
|
+
* host would otherwise end a wait with "the agent exited", the one verdict a
|
|
3502
|
+
* caller acts on by giving up and reporting the work unfinished.
|
|
3503
|
+
*/
|
|
3504
|
+
async function remoteAgentLiveness(remote, keyword) {
|
|
3505
|
+
const params = new URLSearchParams({
|
|
3506
|
+
keyword,
|
|
3507
|
+
all: "1"
|
|
3508
|
+
});
|
|
3509
|
+
try {
|
|
3510
|
+
const res = await remoteGet(remote, `/api/ls?${params}`);
|
|
3511
|
+
if (!res.ok) return null;
|
|
3512
|
+
const records = await res.json();
|
|
3513
|
+
if (!Array.isArray(records)) return null;
|
|
3514
|
+
if (records.length === 0) return "gone";
|
|
3515
|
+
return records.every((r) => r.status === "exited") ? "gone" : "live";
|
|
3516
|
+
} catch {
|
|
3517
|
+
return null;
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
/** How far back from the live frontier to look for a non-empty anchor line. */
|
|
3521
|
+
const ANCHOR_LOOKBACK_ROWS = 200;
|
|
3522
|
+
/**
|
|
3523
|
+
* Last-chance `--until` match against the log an exited agent left behind.
|
|
3524
|
+
*
|
|
3525
|
+
* On a clean exit the raw byte log we were following is replaced by a rendered
|
|
3526
|
+
* scrollback dump at a NEW path, and the pid index is repointed at it. A pattern
|
|
3527
|
+
* printed in the agent's last breath can therefore be missing from everything we
|
|
3528
|
+
* saw yet present in that file — so re-resolve the path from the registry and
|
|
3529
|
+
* scan it.
|
|
3530
|
+
*
|
|
3531
|
+
* Only the lines AFTER `anchor` (the newest line we had already seen, matched
|
|
3532
|
+
* from the END of the file) are tested, so the no-backlog-matching promise
|
|
3533
|
+
* survives: a hit from before this command started cannot be reported. When the
|
|
3534
|
+
* anchor can't be located the drain is skipped rather than guessed at — a missed
|
|
3535
|
+
* match surfaces as a loud exit 1, a false one as a silent wrong answer.
|
|
3536
|
+
*
|
|
3537
|
+
* Feeds those lines to `testLine`, which owns the verdict — so a `--count` wait
|
|
3538
|
+
* finishes on hits split across the stream and this drain, and `--fail-on` still
|
|
3539
|
+
* wins over `--until` on the agent's last line.
|
|
3540
|
+
*/
|
|
3541
|
+
async function drainFinalLog(pid, followedPath, anchor, until, testLine) {
|
|
3542
|
+
if (anchor === null) return;
|
|
3543
|
+
const advertised = (await readGlobalPids()).find((r) => r.pid === pid)?.log_file;
|
|
3544
|
+
const derived = followedPath.endsWith(".raw.log") ? followedPath.slice(0, -8) + ".log" : null;
|
|
3545
|
+
const finalPath = advertised && advertised !== followedPath ? advertised : derived;
|
|
3546
|
+
if (!finalPath) return;
|
|
3547
|
+
const text = await readFile(finalPath, "utf8").catch(() => null);
|
|
3548
|
+
if (text === null) return;
|
|
3549
|
+
const fresh = linesAfterAnchor(text.split("\n").map((l) => l.trimEnd()), anchor);
|
|
3550
|
+
if (fresh === null) return;
|
|
3551
|
+
for (const line of fresh) {
|
|
3552
|
+
if (!until.quiet) process.stdout.write(line + "\n");
|
|
3553
|
+
testLine(line);
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
/** The "waiting for X | fail on Y | timeout Ns…" banner shared by both followers. */
|
|
3557
|
+
function untilBanner(wait) {
|
|
3558
|
+
const parts = [`waiting for ${JSON.stringify(wait.pattern)}`];
|
|
3559
|
+
if (wait.tally.needed > 1) parts.push(`x${wait.tally.needed}`);
|
|
3560
|
+
if (wait.tally.hits > 0) parts.push(`(${wait.tally.hits} already seen)`);
|
|
3561
|
+
if (wait.failPattern !== null) parts.push(`| fail on ${JSON.stringify(wait.failPattern)}`);
|
|
3562
|
+
if (wait.timeoutMs !== null) parts.push(`| timeout ${Math.round(wait.timeoutMs / 1e3)}s`);
|
|
3563
|
+
return `${parts.join(" ")}… (Ctrl-C / SIGTERM to stop)\n`;
|
|
3564
|
+
}
|
|
3565
|
+
/**
|
|
3566
|
+
* Report an `--until` result and return its exit code. In quiet mode the matching
|
|
3567
|
+
* line is the whole of stdout (a caller can capture it); the human-readable
|
|
3568
|
+
* verdict always goes to stderr so it never contaminates that.
|
|
3569
|
+
*
|
|
3570
|
+
* `progress` (hits/needed) is named on the failure paths only when `--count` asked
|
|
3571
|
+
* for more than one: "timed out with 2/3 matches" is the difference between a wait
|
|
3572
|
+
* that was close and one that never started.
|
|
3573
|
+
*/
|
|
3574
|
+
function reportUntil(outcome, matched, quiet, failPattern, progress) {
|
|
3575
|
+
const short = progress && progress.needed > 1 ? ` (${progress.hits}/${progress.needed} matches)` : "";
|
|
3576
|
+
if (outcome === "match" && matched !== null) {
|
|
3577
|
+
if (quiet) process.stdout.write(matched + "\n");
|
|
3578
|
+
process.stderr.write(`[until] matched: ${matched}\n`);
|
|
3579
|
+
} else if (outcome === "failed") {
|
|
3580
|
+
if (quiet && matched !== null) process.stdout.write(matched + "\n");
|
|
3581
|
+
process.stderr.write(`[until] --fail-on ${JSON.stringify(failPattern ?? "")} hit first: ${matched ?? ""}\n`);
|
|
3582
|
+
} else if (outcome === "exited") process.stderr.write(`[until] agent exited without printing it${short}\n`);
|
|
3583
|
+
else if (outcome === "timeout") process.stderr.write(`[until] timed out with no match${short}\n`);
|
|
3584
|
+
else process.stderr.write(`[until] stopped before a match${short}\n`);
|
|
3585
|
+
return untilExitCode(outcome);
|
|
3057
3586
|
}
|
|
3058
3587
|
/**
|
|
3059
3588
|
* Read an agent's last-known PTY geometry from `~/.agent-yes/ptysize/<pid>`
|
|
@@ -3152,8 +3681,7 @@ async function renderRawLogLines(buf, geom) {
|
|
|
3152
3681
|
const rows = geom?.rows && geom.rows > 0 ? geom.rows : 50;
|
|
3153
3682
|
const scrollback = 5e4;
|
|
3154
3683
|
try {
|
|
3155
|
-
const
|
|
3156
|
-
const term = new Terminal({
|
|
3684
|
+
const term = new (await (loadXtermTerminal()))({
|
|
3157
3685
|
cols,
|
|
3158
3686
|
rows,
|
|
3159
3687
|
scrollback,
|
|
@@ -4351,7 +4879,7 @@ async function cmdAttach(rest) {
|
|
|
4351
4879
|
const winsizeDir = path.join(ayHome, "winsize");
|
|
4352
4880
|
await mkdir(winsizeDir, { recursive: true });
|
|
4353
4881
|
const winsizePath = path.join(winsizeDir, String(record.pid));
|
|
4354
|
-
const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-
|
|
4882
|
+
const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-cvUh5Oab.js");
|
|
4355
4883
|
const daemonBase = await resolveDaemonHttpBase().catch(() => null);
|
|
4356
4884
|
const daemonToken = daemonBase ? await loadTokenReadOnly().catch(() => null) ?? "" : "";
|
|
4357
4885
|
const capViewer = `attach:${process.pid}`;
|
|
@@ -5078,7 +5606,7 @@ async function cmdNotify(rest) {
|
|
|
5078
5606
|
}
|
|
5079
5607
|
const ensure = async () => {
|
|
5080
5608
|
if (!argv["ensure-daemon"]) return;
|
|
5081
|
-
const { ensureDaemon } = await import("./notifyDaemon-
|
|
5609
|
+
const { ensureDaemon } = await import("./notifyDaemon-BPpVmmj0.js");
|
|
5082
5610
|
await ensureDaemon().catch(() => null);
|
|
5083
5611
|
};
|
|
5084
5612
|
await heartbeatWatcher(parent, selfStartedAt);
|
|
@@ -5152,7 +5680,7 @@ async function cmdNotifyCursor(args) {
|
|
|
5152
5680
|
}
|
|
5153
5681
|
async function cmdNotifyd(rest) {
|
|
5154
5682
|
const sub = rest[0] ?? "status";
|
|
5155
|
-
const daemon = await import("./notifyDaemon-
|
|
5683
|
+
const daemon = await import("./notifyDaemon-BPpVmmj0.js");
|
|
5156
5684
|
switch (sub) {
|
|
5157
5685
|
case "run": return daemon.runDaemon();
|
|
5158
5686
|
case "once": return daemon.runDaemon({ once: true });
|
|
@@ -5179,4 +5707,4 @@ async function cmdNotifyd(rest) {
|
|
|
5179
5707
|
|
|
5180
5708
|
//#endregion
|
|
5181
5709
|
export { sendPayloadCapError as $, isUnknownManagerToken as A, readLogForRender as B, finalizedLines as C, isPidAlive as D, isExitRequest as E, listRecords as F, renderLogTailLines as G, readPtysize as H, matchKeyword as I, resolveOne as J, renderRawLog as K, menuSelectKeys as L, isUserTyping as M, lastReadAt as N, isSlashCommand as O, lastStdinAt as P, runSubcommand as Q, probeStdinReachable as R, extractTaskCounts as S, isBareManagerInvocation as T, recentMessageEdges as U, readNotes as V, recentReadEdges as W, resolveResumeArgs as X, resolveReadWindow as Y, restartHintLines as Z, envelopeAttribution as _, SEND_BODY_MAX_CHARS as a, writeKeysPaced as at, extractMenu as b, backoffWhileTyping as c, framePaste as ct, cmdHelp as d, buildAgentForest as dt, snapshotStatus as et, confirmStdinUnreachable as f, flattenForest as ft, deriveLiveStatus as g, deriveLiveState as h, READ_WINDOW_MS as i, waitForLogQuiet as it, isUnreachableWriteErrno as j, isSubcommand as k, buildEnvelope as l, shouldFramePaste as lt, cursorAbs as m, MAX_RENDER_BYTES as n, stopTipForCli as nt, SEND_EXIT_UNREACHABLE as o, writeToIpc as ot, controlCodeFromName as p, renderRawLogLines as q, READ_PAGE_DEFAULT as r, submitAndConfirm as rt, TYPING_WINDOW_MS as s, withIpcLock as st, GRACEFUL_EXIT_COMMANDS as t, stdinActivityPath as tt, cliDefaults as u, TYPING_BADGE as ut, envelopeCostFor as v, isAgentStuck as w, extractNeedsInput as x, extractBadges as y, readAgentPtysize as z };
|
|
5182
|
-
//# sourceMappingURL=subcommands-
|
|
5710
|
+
//# sourceMappingURL=subcommands-DoKUSvaF.js.map
|