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.
- package/default.config.yaml +19 -0
- package/dist/SUPPORTED_CLIS-Cvm7yo5d.js +8 -0
- package/dist/{SUPPORTED_CLIS-BleNYXA2.js → SUPPORTED_CLIS-D_-bIOlW.js} +2 -2
- package/dist/{agent-yes.config-z-IPzH5U.js → agent-yes.config-D6ycMApr.js} +2 -65
- package/dist/cli.js +6 -6
- package/dist/configShared-C5QaNPnz.js +71 -0
- package/dist/{globalPidIndex-gZuTvTBs.js → globalPidIndex-C7r2m6s7.js} +19 -20
- package/dist/index.js +4 -4
- package/dist/pidStore-C4c2O15q.js +5 -0
- package/dist/{pidStore-B5vBu8Px.js → pidStore-CGKIhaJO.js} +5 -4
- package/dist/reaper-BLVA780B.js +3 -0
- package/dist/{reaper-Dj8R7ltI.js → reaper-BkjPN7mw.js} +24 -2
- package/dist/{remotes-CpGcTr7A.js → remotes-BRCDVnR7.js} +1 -1
- package/dist/{remotes-D2fqaRU8.js → remotes-D8GvSbhf.js} +1 -1
- package/dist/{schedule-e4f7NlA2.js → schedule-D2cn8N7o.js} +7 -7
- package/dist/{serve-CzztmZ_N.js → serve-Bo3bDXQG.js} +202 -58
- package/dist/{setup-CPyRNiIA.js → setup-CvOr258q.js} +3 -3
- package/dist/{share-CS9XVrLF.js → share-YuM6-Q6A.js} +71 -13
- package/dist/{subcommands-CQowpr1t.js → subcommands-ClVHy-xI.js} +647 -32
- package/dist/subcommands-Llf9o8nh.js +7 -0
- package/dist/{tray-DjCIyakK.js → tray-BVnJLThD.js} +1 -1
- package/dist/{ts-9GThuc3w.js → ts-DGIglR4L.js} +10 -7
- package/dist/{versionChecker-Bv9XKddN.js → versionChecker-gaQkM2Hy.js} +2 -2
- package/dist/{workspaceConfig-XP2NEWmV.js → workspaceConfig-BJO4fzEn.js} +1 -1
- package/lab/ui/console-logic.js +222 -10
- package/lab/ui/icon.svg +5 -0
- package/lab/ui/index.html +1152 -28
- package/lab/ui/landing.html +276 -0
- package/lab/ui/manifest.webmanifest +14 -0
- package/lab/ui/sw.js +56 -0
- package/package.json +5 -1
- package/ts/agentTree.spec.ts +92 -0
- package/ts/agentTree.ts +149 -0
- package/ts/configShared.ts +4 -0
- package/ts/globalPidIndex.ts +28 -20
- package/ts/idleWaiter.spec.ts +7 -1
- package/ts/index.ts +9 -0
- package/ts/lsWatch.spec.ts +61 -0
- package/ts/lsWatch.ts +94 -0
- package/ts/needsInput.spec.ts +55 -0
- package/ts/needsInput.ts +68 -0
- package/ts/pidStore.ts +3 -0
- package/ts/reaper.spec.ts +26 -2
- package/ts/reaper.ts +25 -0
- package/ts/resultEnvelope.spec.ts +43 -0
- package/ts/resultEnvelope.ts +88 -0
- package/ts/serve.ts +276 -41
- package/ts/share.ts +144 -27
- package/ts/subcommands.ts +0 -0
- package/ts/todoParse.spec.ts +68 -0
- package/ts/todoParse.ts +88 -0
- package/ts/utils.spec.ts +4 -1
- package/dist/SUPPORTED_CLIS-ClaOErso.js +0 -8
- package/dist/pidStore-7y1cTcAE.js +0 -5
- package/dist/reaper-HqcUms2d.js +0 -3
- 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.
|
|
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
|
+
});
|
package/ts/agentTree.ts
ADDED
|
@@ -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
|
+
}
|
package/ts/configShared.ts
CHANGED
|
@@ -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
|
}
|