agent-yes 1.122.3 → 1.124.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.
Files changed (56) hide show
  1. package/default.config.yaml +19 -0
  2. package/dist/SUPPORTED_CLIS-Cvm7yo5d.js +8 -0
  3. package/dist/{SUPPORTED_CLIS-BleNYXA2.js → SUPPORTED_CLIS-D_-bIOlW.js} +2 -2
  4. package/dist/{agent-yes.config-z-IPzH5U.js → agent-yes.config-D6ycMApr.js} +2 -65
  5. package/dist/cli.js +6 -6
  6. package/dist/configShared-C5QaNPnz.js +71 -0
  7. package/dist/{globalPidIndex-gZuTvTBs.js → globalPidIndex-C7r2m6s7.js} +19 -20
  8. package/dist/index.js +4 -4
  9. package/dist/pidStore-C4c2O15q.js +5 -0
  10. package/dist/{pidStore-B5vBu8Px.js → pidStore-CGKIhaJO.js} +5 -4
  11. package/dist/reaper-BLVA780B.js +3 -0
  12. package/dist/{reaper-Dj8R7ltI.js → reaper-BkjPN7mw.js} +24 -2
  13. package/dist/{remotes-CpGcTr7A.js → remotes-BRCDVnR7.js} +1 -1
  14. package/dist/{remotes-D2fqaRU8.js → remotes-D8GvSbhf.js} +1 -1
  15. package/dist/{schedule-e4f7NlA2.js → schedule-D2cn8N7o.js} +7 -7
  16. package/dist/{serve-CzztmZ_N.js → serve-Bo3bDXQG.js} +202 -58
  17. package/dist/{setup-CPyRNiIA.js → setup-CvOr258q.js} +3 -3
  18. package/dist/{share-CS9XVrLF.js → share-YuM6-Q6A.js} +71 -13
  19. package/dist/{subcommands-CQowpr1t.js → subcommands-ClVHy-xI.js} +647 -32
  20. package/dist/subcommands-Llf9o8nh.js +7 -0
  21. package/dist/{tray-DjCIyakK.js → tray-BVnJLThD.js} +1 -1
  22. package/dist/{ts-9GThuc3w.js → ts-DGIglR4L.js} +10 -7
  23. package/dist/{versionChecker-Bv9XKddN.js → versionChecker-gaQkM2Hy.js} +2 -2
  24. package/dist/{workspaceConfig-XP2NEWmV.js → workspaceConfig-BJO4fzEn.js} +1 -1
  25. package/lab/ui/console-logic.js +222 -10
  26. package/lab/ui/icon.svg +5 -0
  27. package/lab/ui/index.html +1152 -28
  28. package/lab/ui/landing.html +276 -0
  29. package/lab/ui/manifest.webmanifest +14 -0
  30. package/lab/ui/sw.js +56 -0
  31. package/package.json +5 -1
  32. package/ts/agentTree.spec.ts +92 -0
  33. package/ts/agentTree.ts +149 -0
  34. package/ts/configShared.ts +4 -0
  35. package/ts/globalPidIndex.ts +28 -20
  36. package/ts/idleWaiter.spec.ts +7 -1
  37. package/ts/index.ts +9 -0
  38. package/ts/lsWatch.spec.ts +61 -0
  39. package/ts/lsWatch.ts +94 -0
  40. package/ts/needsInput.spec.ts +55 -0
  41. package/ts/needsInput.ts +68 -0
  42. package/ts/pidStore.ts +3 -0
  43. package/ts/reaper.spec.ts +26 -2
  44. package/ts/reaper.ts +25 -0
  45. package/ts/resultEnvelope.spec.ts +43 -0
  46. package/ts/resultEnvelope.ts +88 -0
  47. package/ts/serve.ts +276 -41
  48. package/ts/share.ts +144 -27
  49. package/ts/subcommands.ts +0 -0
  50. package/ts/todoParse.spec.ts +68 -0
  51. package/ts/todoParse.ts +88 -0
  52. package/ts/utils.spec.ts +4 -1
  53. package/dist/SUPPORTED_CLIS-ClaOErso.js +0 -8
  54. package/dist/pidStore-7y1cTcAE.js +0 -5
  55. package/dist/reaper-HqcUms2d.js +0 -3
  56. package/dist/subcommands-KAbIcd8_.js +0 -6
@@ -0,0 +1,276 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <script>
7
+ // Backward-compat: share links used to be agent-yes.com/#room:token (the
8
+ // console lived at /). It now lives at /w/. The # fragment never reaches the
9
+ // server, so forward old links here, client-side, keeping the exact hash.
10
+ // No room hash → fall through and show this landing page normally.
11
+ (function () {
12
+ var h = location.hash;
13
+ if (h && /^#[A-Za-z0-9_-]+:.+/.test(h)) location.replace("/w/" + h);
14
+ })();
15
+ </script>
16
+ <title>agent-yes — drive any AI coding CLI on autopilot</title>
17
+ <meta
18
+ name="description"
19
+ content="agent-yes wraps Claude, Codex, Gemini and more, auto-answering their prompts so they run unattended — and ay serve streams a live, end-to-end-encrypted console to your browser."
20
+ />
21
+ <link
22
+ rel="icon"
23
+ href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ctext y='14' font-size='14'%3E%E2%9C%93%3C/text%3E%3C/svg%3E"
24
+ />
25
+ <style>
26
+ :root {
27
+ --bg: #0d1117;
28
+ --panel: #161b22;
29
+ --border: #30363d;
30
+ --fg: #e6edf3;
31
+ --muted: #9198a1;
32
+ --accent: #2f81f7;
33
+ --green: #3fb950;
34
+ --code: #1f2530;
35
+ }
36
+ @media (prefers-color-scheme: light) {
37
+ :root {
38
+ --bg: #ffffff;
39
+ --panel: #f6f8fa;
40
+ --border: #d0d7de;
41
+ --fg: #1f2328;
42
+ --muted: #59636e;
43
+ --accent: #0969da;
44
+ --green: #1a7f37;
45
+ --code: #eff2f5;
46
+ }
47
+ }
48
+ * {
49
+ box-sizing: border-box;
50
+ }
51
+ html,
52
+ body {
53
+ margin: 0;
54
+ }
55
+ body {
56
+ background: var(--bg);
57
+ color: var(--fg);
58
+ font:
59
+ 15px/1.6 -apple-system,
60
+ BlinkMacSystemFont,
61
+ "Segoe UI",
62
+ Helvetica,
63
+ Arial,
64
+ sans-serif;
65
+ -webkit-font-smoothing: antialiased;
66
+ }
67
+ a {
68
+ color: var(--accent);
69
+ text-decoration: none;
70
+ }
71
+ a:hover {
72
+ text-decoration: underline;
73
+ }
74
+ .wrap {
75
+ max-width: 760px;
76
+ margin: 0 auto;
77
+ padding: 0 20px;
78
+ }
79
+ header {
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: space-between;
83
+ padding: 22px 0;
84
+ }
85
+ .brand {
86
+ font-weight: 700;
87
+ font-size: 18px;
88
+ letter-spacing: -0.01em;
89
+ }
90
+ .brand .tick {
91
+ color: var(--green);
92
+ }
93
+ nav a {
94
+ color: var(--muted);
95
+ margin-left: 18px;
96
+ font-size: 14px;
97
+ }
98
+ .hero {
99
+ padding: 56px 0 40px;
100
+ }
101
+ h1 {
102
+ font-size: clamp(30px, 6vw, 46px);
103
+ line-height: 1.1;
104
+ letter-spacing: -0.02em;
105
+ margin: 0 0 16px;
106
+ }
107
+ .sub {
108
+ font-size: clamp(16px, 2.4vw, 19px);
109
+ color: var(--muted);
110
+ max-width: 60ch;
111
+ margin: 0 0 28px;
112
+ }
113
+ .cta {
114
+ display: flex;
115
+ gap: 12px;
116
+ flex-wrap: wrap;
117
+ align-items: center;
118
+ }
119
+ .btn {
120
+ display: inline-block;
121
+ padding: 11px 20px;
122
+ border-radius: 8px;
123
+ font-weight: 600;
124
+ font-size: 15px;
125
+ background: var(--accent);
126
+ color: #fff;
127
+ border: 1px solid transparent;
128
+ }
129
+ .btn:hover {
130
+ text-decoration: none;
131
+ filter: brightness(1.08);
132
+ }
133
+ .btn.ghost {
134
+ background: transparent;
135
+ color: var(--fg);
136
+ border-color: var(--border);
137
+ }
138
+ .install {
139
+ margin: 14px 0 0;
140
+ }
141
+ .install code,
142
+ pre code {
143
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
144
+ }
145
+ pre {
146
+ background: var(--code);
147
+ border: 1px solid var(--border);
148
+ border-radius: 8px;
149
+ padding: 14px 16px;
150
+ overflow-x: auto;
151
+ font-size: 13.5px;
152
+ margin: 8px 0;
153
+ }
154
+ .muted {
155
+ color: var(--muted);
156
+ }
157
+ .grid {
158
+ display: grid;
159
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
160
+ gap: 16px;
161
+ margin: 36px 0;
162
+ }
163
+ .card {
164
+ background: var(--panel);
165
+ border: 1px solid var(--border);
166
+ border-radius: 10px;
167
+ padding: 16px 18px;
168
+ }
169
+ .card h3 {
170
+ margin: 0 0 6px;
171
+ font-size: 15px;
172
+ }
173
+ .card p {
174
+ margin: 0;
175
+ color: var(--muted);
176
+ font-size: 14px;
177
+ }
178
+ footer {
179
+ border-top: 1px solid var(--border);
180
+ margin-top: 48px;
181
+ padding: 24px 0 40px;
182
+ color: var(--muted);
183
+ font-size: 13px;
184
+ }
185
+ .clis {
186
+ color: var(--muted);
187
+ font-size: 13px;
188
+ margin-top: 10px;
189
+ }
190
+ .clis b {
191
+ color: var(--fg);
192
+ font-weight: 600;
193
+ }
194
+ </style>
195
+ </head>
196
+ <body>
197
+ <div class="wrap">
198
+ <header>
199
+ <div class="brand"><span class="tick">✓</span> agent-yes</div>
200
+ <nav>
201
+ <a href="/w/">Console</a>
202
+ <a href="/blog/">Blog</a>
203
+ <a href="https://github.com/snomiao/agent-yes">GitHub</a>
204
+ </nav>
205
+ </header>
206
+
207
+ <section class="hero">
208
+ <h1>Drive any AI coding CLI on autopilot.</h1>
209
+ <p class="sub">
210
+ agent-yes wraps Claude, Codex, Gemini, Copilot and more — auto-answering their prompts so
211
+ they keep working unattended. Then <code>ay serve</code>
212
+ streams a live, end-to-end-encrypted console to your browser so you can watch, steer, and
213
+ spawn agents from anywhere.
214
+ </p>
215
+ <div class="cta">
216
+ <a class="btn" href="/w/">Open the console →</a>
217
+ <a class="btn ghost" href="https://github.com/snomiao/agent-yes">Star on GitHub</a>
218
+ </div>
219
+ <div class="install">
220
+ <pre><code># macOS / Linux
221
+ curl -fsSL https://agent-yes.com/setup.sh | sh
222
+
223
+ # Windows (PowerShell)
224
+ powershell -c "irm https://agent-yes.com/setup.ps1 | iex"</code></pre>
225
+ <div class="clis">
226
+ Wrappers: <b>claude-yes</b> · <b>codex-yes</b> · <b>gemini-yes</b> ·
227
+ <b>copilot-yes</b> · <b>cursor-yes</b> · <b>qwen-yes</b> · <b>grok-yes</b> — or just
228
+ <b>ay</b>.
229
+ </div>
230
+ </div>
231
+ </section>
232
+
233
+ <section class="grid">
234
+ <div class="card">
235
+ <h3>Auto-yes the prompts</h3>
236
+ <p>
237
+ Trust dialogs, theme pickers, "continue?", overload retries — handled, so the agent
238
+ never stalls waiting on you.
239
+ </p>
240
+ </div>
241
+ <div class="card">
242
+ <h3>Live console, anywhere</h3>
243
+ <p>
244
+ <code>ay serve</code> shares a WebRTC console at <a href="/w/">/w/</a> — list, tail, and
245
+ steer every agent from your phone or laptop.
246
+ </p>
247
+ </div>
248
+ <div class="card">
249
+ <h3>End-to-end encrypted</h3>
250
+ <p>
251
+ The share link's secret never reaches the signaling server; traffic is AES-GCM between
252
+ your browser and your machine.
253
+ </p>
254
+ </div>
255
+ <div class="card">
256
+ <h3>Many CLIs, one wrapper</h3>
257
+ <p>
258
+ One tool across Claude, Codex, Gemini, Copilot, Cursor, Qwen, Grok — same flags, same
259
+ live console.
260
+ </p>
261
+ </div>
262
+ </section>
263
+
264
+ <footer class="wrap" style="padding-left: 0; padding-right: 0">
265
+ <div>
266
+ <a href="/w/">Console</a> · <a href="/blog/">Blog</a> ·
267
+ <a href="https://github.com/snomiao/agent-yes">GitHub</a> ·
268
+ <a href="https://www.npmjs.com/package/agent-yes">npm</a>
269
+ </div>
270
+ <div style="margin-top: 8px">
271
+ MIT · made by <a href="https://github.com/snomiao">snomiao</a>
272
+ </div>
273
+ </footer>
274
+ </div>
275
+ </body>
276
+ </html>
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "agent-yes console",
3
+ "short_name": "agent-yes",
4
+ "description": "Live, end-to-end-encrypted console for your agent-yes agents.",
5
+ "start_url": "/w/",
6
+ "scope": "/w/",
7
+ "display": "standalone",
8
+ "background_color": "#0d1117",
9
+ "theme_color": "#0d1117",
10
+ "icons": [
11
+ { "src": "./icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" },
12
+ { "src": "./icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "maskable" }
13
+ ]
14
+ }
package/lab/ui/sw.js ADDED
@@ -0,0 +1,56 @@
1
+ // Service worker for the agent-yes console PWA (scope: /w/).
2
+ //
3
+ // Strategy: NETWORK-FIRST for the same-origin /w/ shell. The console speaks a
4
+ // versioned wire protocol to the signaling server, so it must never run stale —
5
+ // online we always fetch fresh (and refresh the cache); the cache is only a
6
+ // fallback when offline, which is what makes the installed app launchable with no
7
+ // network. WebSocket signaling and cross-origin CDN scripts are not GET fetches we
8
+ // own, so they pass straight through.
9
+ const CACHE = "agent-yes-w-v1";
10
+ const SHELL = [
11
+ "./",
12
+ "./index.html",
13
+ "./room-client.js",
14
+ "./console-logic.js",
15
+ "./e2e.js",
16
+ "./manifest.webmanifest",
17
+ "./icon.svg",
18
+ ];
19
+
20
+ self.addEventListener("install", (e) => {
21
+ self.skipWaiting();
22
+ e.waitUntil(caches.open(CACHE).then((c) => c.addAll(SHELL).catch(() => {})));
23
+ });
24
+
25
+ self.addEventListener("activate", (e) => {
26
+ e.waitUntil(
27
+ (async () => {
28
+ const keys = await caches.keys();
29
+ await Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)));
30
+ await self.clients.claim();
31
+ })(),
32
+ );
33
+ });
34
+
35
+ self.addEventListener("fetch", (e) => {
36
+ const req = e.request;
37
+ if (req.method !== "GET") return;
38
+ const url = new URL(req.url);
39
+ // Only the same-origin /w/ shell; let everything else (CDN, signaling) be.
40
+ if (url.origin !== self.location.origin || !url.pathname.startsWith("/w/")) return;
41
+ e.respondWith(
42
+ (async () => {
43
+ try {
44
+ const res = await fetch(req);
45
+ if (res && res.ok) {
46
+ const c = await caches.open(CACHE);
47
+ c.put(req, res.clone());
48
+ }
49
+ return res;
50
+ } catch {
51
+ const cached = await caches.match(req);
52
+ return cached || (await caches.match("./index.html")) || Response.error();
53
+ }
54
+ })(),
55
+ );
56
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.122.3",
3
+ "version": "1.124.0",
4
4
  "description": "A wrapper tool that automates interactions with various AI CLI tools by automatically handling common prompts and responses.",
5
5
  "keywords": [
6
6
  "ai",
@@ -58,8 +58,12 @@
58
58
  "lab/ui/console-logic.js",
59
59
  "lab/ui/e2e.d.ts",
60
60
  "lab/ui/e2e.js",
61
+ "lab/ui/icon.svg",
61
62
  "lab/ui/index.html",
63
+ "lab/ui/landing.html",
64
+ "lab/ui/manifest.webmanifest",
62
65
  "lab/ui/room-client.js",
66
+ "lab/ui/sw.js",
63
67
  "lab/ui/blog/**"
64
68
  ],
65
69
  "type": "module",
@@ -0,0 +1,92 @@
1
+ import { expect, test } from "vitest";
2
+ import { buildAgentForest, flattenForest, foldLayers } from "./agentTree.ts";
3
+ import type { LayerNode } from "./agentTree.ts";
4
+ import type { GlobalPidRecord } from "./globalPidIndex.ts";
5
+
6
+ const mk = (pid: number, wrapper_pid: number, parent_pid?: number): GlobalPidRecord =>
7
+ ({ pid, wrapper_pid, parent_pid }) as GlobalPidRecord;
8
+
9
+ const pidsOf = (recs: GlobalPidRecord[]) =>
10
+ flattenForest(buildAgentForest(recs))
11
+ .map((r) => r.record.pid)
12
+ .sort((a, b) => a - b);
13
+
14
+ test("links child under parent via parent_pid === wrapper_pid", () => {
15
+ const rows = flattenForest(buildAgentForest([mk(1, 10), mk(2, 20, 10)]));
16
+ expect(rows.map((r) => r.record.pid)).toEqual([1, 2]);
17
+ expect(rows[1]!.depth).toBe(1); // child indented under root
18
+ });
19
+
20
+ test("orphan (parent not present) renders at top level, never vanishes", () => {
21
+ const rows = flattenForest(buildAgentForest([mk(1, 10), mk(2, 20, 10), mk(3, 30, 999)]));
22
+ expect(pidsOf([mk(1, 10), mk(2, 20, 10), mk(3, 30, 999)])).toEqual([1, 2, 3]);
23
+ expect(rows.find((r) => r.record.pid === 3)!.depth).toBe(0); // orphan is a root
24
+ });
25
+
26
+ // Regression: a parent_pid cycle (only via pid reuse across a reboot) used to
27
+ // drop every node in the cycle, since none became a root — they'd disappear from
28
+ // `ay ls` entirely. Each node must still render exactly once.
29
+ test("2-node parent_pid cycle still renders both nodes", () => {
30
+ expect(pidsOf([mk(1, 10, 20), mk(2, 20, 10)])).toEqual([1, 2]);
31
+ });
32
+
33
+ test("3-node parent_pid cycle still renders all nodes", () => {
34
+ expect(pidsOf([mk(1, 10, 30), mk(2, 20, 10), mk(3, 30, 20)])).toEqual([1, 2, 3]);
35
+ });
36
+
37
+ test("self-parent is treated as a root, not dropped or self-nested", () => {
38
+ expect(pidsOf([mk(1, 10, 10)])).toEqual([1]);
39
+ });
40
+
41
+ // foldLayers: the VSCode-explorer-style collapse used by the console to nest
42
+ // rooms > peers > agents and fold single-child chains onto one row.
43
+ const ln = (label: string, children: LayerNode[] = []): LayerNode => ({
44
+ label,
45
+ kind: "node",
46
+ children,
47
+ });
48
+
49
+ test("foldLayers collapses a single-child chain onto one row", () => {
50
+ const rows = foldLayers([ln("a", [ln("b", [ln("c")])])]);
51
+ expect(rows).toHaveLength(1);
52
+ expect(rows[0]!.segments.map((s) => s.label)).toEqual(["a", "b", "c"]);
53
+ expect(rows[0]!.depth).toBe(0);
54
+ expect(rows[0]!.prefix).toBe("");
55
+ });
56
+
57
+ test("foldLayers branches a multi-child node into indented ├─/└─ rows", () => {
58
+ const rows = foldLayers([ln("root", [ln("x"), ln("y")])]);
59
+ expect(rows.map((r) => r.segments[0]!.label)).toEqual(["root", "x", "y"]);
60
+ expect(rows[0]!.depth).toBe(0);
61
+ expect(rows[1]!).toMatchObject({ depth: 1, prefix: "├─ " });
62
+ expect(rows[2]!).toMatchObject({ depth: 1, prefix: "└─ " }); // last child
63
+ });
64
+
65
+ test("foldLayers builds nested prefixes with both ancestor connectors", () => {
66
+ // root → {a (not last), b (last)}, each with two children, so depth-2 rows
67
+ // exercise both ancestor connectors: "│ " under a, " " under the last child b.
68
+ const rows = foldLayers([
69
+ ln("root", [ln("a", [ln("a1"), ln("a2")]), ln("b", [ln("b1"), ln("b2")])]),
70
+ ]);
71
+ const at = (label: string) => rows.find((r) => r.segments[0]!.label === label)!;
72
+ expect(at("a1")).toMatchObject({ depth: 2, prefix: "│ ├─ " });
73
+ expect(at("a2").prefix).toBe("│ └─ ");
74
+ expect(at("b1").prefix).toBe(" ├─ ");
75
+ expect(at("b2").prefix).toBe(" └─ ");
76
+ });
77
+
78
+ test("foldLayers renders a node shared by two parents only once (visited guard)", () => {
79
+ // Diamond: root → {a, b}, both pointing at the same leaf c. The visited guard
80
+ // folds c into whichever branch reaches it first (a), so it isn't duplicated.
81
+ const c = ln("c");
82
+ const rows = foldLayers([ln("root", [ln("a", [c]), ln("b", [c])])]);
83
+ const labelsOf = (i: number) => rows[i]!.segments.map((s) => s.label);
84
+ expect(labelsOf(0)).toEqual(["root"]);
85
+ expect(labelsOf(1)).toEqual(["a", "c"]); // c folded into a's chain
86
+ expect(rows[1]!.prefix).toBe("├─ ");
87
+ expect(labelsOf(2)).toEqual(["b"]); // b's duplicate c is skipped, not re-rendered
88
+ expect(rows[2]!.prefix).toBe("└─ ");
89
+ expect(rows.flatMap((r) => r.segments.map((s) => s.label)).filter((l) => l === "c")).toHaveLength(
90
+ 1,
91
+ );
92
+ });
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Agent hierarchy: link the flat pid registry into a parent→child forest and
3
+ * render it as a tree.
4
+ *
5
+ * A nested `ay` launched from inside another agent inherits its parent's
6
+ * wrapper pid via the AGENT_YES_PID env var (injected by both runtimes — see
7
+ * ts/index.ts and rs/src/pty_spawner.rs). We record that as `parent_pid`, so a
8
+ * child links to its parent with: child.parent_pid === parent.wrapper_pid.
9
+ *
10
+ * Used by `ay ls` (CLI table) and the console (the deepest "agents > agents
11
+ * subtree" layer of signalling-server > rooms > peers > agents > subtree).
12
+ */
13
+
14
+ import type { GlobalPidRecord } from "./globalPidIndex.ts";
15
+
16
+ export interface ForestNode {
17
+ record: GlobalPidRecord;
18
+ children: ForestNode[];
19
+ }
20
+
21
+ /**
22
+ * Link records into a forest via parent_pid === wrapper_pid. Records whose
23
+ * parent isn't present in the set (top-level agents, or links into agents
24
+ * filtered out by a keyword/scope) become roots. Root and sibling order follows
25
+ * the input order, so a caller that pre-sorts (e.g. newest-first) is preserved.
26
+ */
27
+ export function buildAgentForest(records: GlobalPidRecord[]): ForestNode[] {
28
+ const nodes: ForestNode[] = records.map((record) => ({ record, children: [] }));
29
+ const byWrapper = new Map<number, ForestNode>();
30
+ for (const n of nodes) {
31
+ const w = n.record.wrapper_pid;
32
+ // If two live agents ever share a wrapper pid (pid reuse across a reboot),
33
+ // last one wins — harmless for display.
34
+ if (typeof w === "number" && w > 0) byWrapper.set(w, n);
35
+ }
36
+ const roots: ForestNode[] = [];
37
+ for (const n of nodes) {
38
+ const p = n.record.parent_pid;
39
+ const parent = typeof p === "number" && p > 0 ? byWrapper.get(p) : undefined;
40
+ if (parent && parent !== n) parent.children.push(n);
41
+ else roots.push(n);
42
+ }
43
+ // Cycle safety: a 2+ node parent_pid cycle (possible only via pid reuse across a
44
+ // reboot) links every member as someone's child, so none become roots and they'd
45
+ // vanish from the output entirely. Mark everything reachable from the current
46
+ // roots, then append any unreached node as its own root; flattenForest's visited
47
+ // guard then renders each exactly once. Mirrors the console JS recovery pass
48
+ // (lab/ui/console-logic.js agentForestNodes).
49
+ const seen = new Set<ForestNode>();
50
+ const mark = (n: ForestNode) => {
51
+ if (seen.has(n)) return;
52
+ seen.add(n);
53
+ n.children.forEach(mark);
54
+ };
55
+ roots.forEach(mark);
56
+ for (const n of nodes) if (!seen.has(n)) roots.push(n);
57
+ return roots;
58
+ }
59
+
60
+ export interface FlatRow {
61
+ record: GlobalPidRecord;
62
+ /** Box-drawing branch prefix, e.g. "", "├─ ", "│ └─ ". Empty for roots. */
63
+ prefix: string;
64
+ depth: number;
65
+ }
66
+
67
+ /**
68
+ * Depth-first flatten a forest into rows carrying a box-drawing branch prefix.
69
+ * A `visited` guard makes a pathological parent_pid cycle terminate instead of
70
+ * recursing forever.
71
+ */
72
+ export function flattenForest(roots: ForestNode[]): FlatRow[] {
73
+ const rows: FlatRow[] = [];
74
+ const visited = new Set<ForestNode>();
75
+ const walk = (node: ForestNode, ancestorsLast: boolean[]) => {
76
+ if (visited.has(node)) return;
77
+ visited.add(node);
78
+ const depth = ancestorsLast.length;
79
+ let prefix = "";
80
+ for (let i = 0; i < depth - 1; i++) prefix += ancestorsLast[i] ? " " : "│ ";
81
+ if (depth > 0) prefix += ancestorsLast[depth - 1] ? "└─ " : "├─ ";
82
+ rows.push({ record: node.record, prefix, depth });
83
+ node.children.forEach((c, i) => walk(c, [...ancestorsLast, i === node.children.length - 1]));
84
+ };
85
+ for (const r of roots) walk(r, []);
86
+ return rows;
87
+ }
88
+
89
+ /**
90
+ * Generic VSCode-explorer-style layered tree node, used by the console to nest
91
+ * rooms > peers > agents and fold away any layer that has a single child.
92
+ */
93
+ export interface LayerNode {
94
+ /** Short label for this layer node, e.g. a room name, host, or agent title. */
95
+ label: string;
96
+ /** Layer kind, for styling/icons in the UI (e.g. "room", "peer", "agent"). */
97
+ kind: string;
98
+ children: LayerNode[];
99
+ /** Arbitrary payload (e.g. the agent record) for leaves. */
100
+ data?: unknown;
101
+ }
102
+
103
+ export interface FoldedRow {
104
+ /** Labels folded onto this one line (a single-child chain), parent→child. */
105
+ segments: { label: string; kind: string }[];
106
+ depth: number;
107
+ prefix: string;
108
+ node: LayerNode;
109
+ }
110
+
111
+ /**
112
+ * Fold + flatten a layer forest the way VSCode's explorer collapses a chain of
113
+ * single-child folders (`com/example/app`) onto one row, and only indents into a
114
+ * tree where a node actually has multiple children.
115
+ *
116
+ * - A node with exactly one child is merged with that child: their labels join
117
+ * on this row and we descend without adding depth.
118
+ * - A node with 0 or ≥2 children ends the current row; each child (when ≥2)
119
+ * starts a new indented row with ├─ / └─ branches.
120
+ */
121
+ export function foldLayers(roots: LayerNode[]): FoldedRow[] {
122
+ const rows: FoldedRow[] = [];
123
+ const visited = new Set<LayerNode>();
124
+ const walk = (start: LayerNode, ancestorsLast: boolean[]) => {
125
+ // Collapse the single-child chain starting at `start`.
126
+ const segments: { label: string; kind: string }[] = [];
127
+ let node = start;
128
+ while (true) {
129
+ if (visited.has(node)) break;
130
+ visited.add(node);
131
+ segments.push({ label: node.label, kind: node.kind });
132
+ if (node.children.length === 1) {
133
+ node = node.children[0]!;
134
+ continue;
135
+ }
136
+ break;
137
+ }
138
+ const depth = ancestorsLast.length;
139
+ let prefix = "";
140
+ for (let i = 0; i < depth - 1; i++) prefix += ancestorsLast[i] ? " " : "│ ";
141
+ if (depth > 0) prefix += ancestorsLast[depth - 1] ? "└─ " : "├─ ";
142
+ rows.push({ segments, depth, prefix, node });
143
+ // Branch into the children of the chain's tail (only reached when ≥2).
144
+ node.children.forEach((c, i) => walk(c, [...ancestorsLast, i === node.children.length - 1]));
145
+ };
146
+ // A single root collapses away too: only branch the roots when there are ≥2.
147
+ for (const r of roots) walk(r, []);
148
+ return rows;
149
+ }
@@ -18,6 +18,7 @@ type RawCliConfig = Omit<
18
18
  | "updateAvailable"
19
19
  | "exitCommands"
20
20
  | "autoRetry"
21
+ | "needsInput"
21
22
  > & {
22
23
  ready?: RegexSource[];
23
24
  fatal?: RegexSource[];
@@ -28,6 +29,7 @@ type RawCliConfig = Omit<
28
29
  restartWithoutContinueArg?: RegexSource[];
29
30
  updateAvailable?: RegexSource[];
30
31
  autoRetry?: RegexSource[];
32
+ needsInput?: RegexSource[];
31
33
  exitCommands?: string[];
32
34
  exitCommand?: string[];
33
35
  };
@@ -81,6 +83,7 @@ export function normalizeCliConfig(raw: RawCliConfig): AgentCliConfig {
81
83
  restartWithoutContinueArg,
82
84
  updateAvailable,
83
85
  autoRetry,
86
+ needsInput,
84
87
  exitCommands,
85
88
  exitCommand,
86
89
  ...rest
@@ -97,6 +100,7 @@ export function normalizeCliConfig(raw: RawCliConfig): AgentCliConfig {
97
100
  restartWithoutContinueArg: compileRegexList(restartWithoutContinueArg),
98
101
  updateAvailable: compileRegexList(updateAvailable),
99
102
  autoRetry: compileRegexList(autoRetry),
103
+ needsInput: compileRegexList(needsInput),
100
104
  exitCommands: exitCommands ?? exitCommand,
101
105
  };
102
106
  }