agent-yes 1.113.0 → 1.115.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/README.md +1 -1
- package/dist/SUPPORTED_CLIS-DUVB1HyL.js +8 -0
- package/dist/{SUPPORTED_CLIS-BpO7ZYx_.js → SUPPORTED_CLIS-VayLM5qX.js} +2 -2
- package/dist/cli.js +3 -3
- package/dist/index.js +2 -2
- package/dist/{serve-YPd9gtRO.js → serve-CP61tKuJ.js} +70 -16
- package/dist/{share-BsCeIfQM.js → share-DwzKXEsJ.js} +20 -3
- package/dist/{subcommands-DU5RXU0g.js → subcommands-SOHKtDbk.js} +1 -1
- package/dist/{subcommands-Za5uwCq6.js → subcommands-t1uOb17r.js} +2 -2
- package/dist/{ts-ChVJOKNr.js → ts-B62nAAfY.js} +2 -2
- package/dist/{versionChecker-wxlWV_VT.js → versionChecker-BdkE7S2A.js} +2 -2
- package/lab/ui/console-logic.js +24 -1
- package/lab/ui/index.html +177 -22
- package/package.json +1 -1
- package/ts/serve.ts +75 -8
- package/ts/share.ts +34 -3
- package/dist/SUPPORTED_CLIS-DtJakX4G.js +0 -8
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Or with a package manager you already have:
|
|
|
24
24
|
bun add -g agent-yes # or: npm install -g agent-yes
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Then: `ay claude` (run an agent with auto-yes) · `ay serve share` (web console + shareable link) · live console at https://agent-yes.com
|
|
27
|
+
Then: `ay claude` (run an agent with auto-yes) · `ay serve --share` (web console + shareable link) · live console at https://agent-yes.com
|
|
28
28
|
|
|
29
29
|
## Features
|
|
30
30
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "./ts-B62nAAfY.js";
|
|
2
|
+
import "./logger-B9h0djqx.js";
|
|
3
|
+
import "./versionChecker-BdkE7S2A.js";
|
|
4
|
+
import "./pidStore-DBjlqzo8.js";
|
|
5
|
+
import "./globalPidIndex-yVd3mbsV.js";
|
|
6
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-VayLM5qX.js";
|
|
7
|
+
|
|
8
|
+
export { SUPPORTED_CLIS };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { t as CLIS_CONFIG } from "./ts-
|
|
1
|
+
import { t as CLIS_CONFIG } from "./ts-B62nAAfY.js";
|
|
2
2
|
|
|
3
3
|
//#region ts/SUPPORTED_CLIS.ts
|
|
4
4
|
const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
|
|
5
5
|
|
|
6
6
|
//#endregion
|
|
7
7
|
export { SUPPORTED_CLIS as t };
|
|
8
|
-
//# sourceMappingURL=SUPPORTED_CLIS-
|
|
8
|
+
//# sourceMappingURL=SUPPORTED_CLIS-VayLM5qX.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { n as logger } from "./logger-B9h0djqx.js";
|
|
3
|
-
import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-
|
|
3
|
+
import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-BdkE7S2A.js";
|
|
4
4
|
import { argv } from "process";
|
|
5
5
|
import { execFileSync, spawn } from "child_process";
|
|
6
6
|
import ms from "ms";
|
|
@@ -482,7 +482,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
|
|
|
482
482
|
{
|
|
483
483
|
const rawArg = process.argv[2];
|
|
484
484
|
const isHelpFlag = rawArg === "-h" || rawArg === "--help";
|
|
485
|
-
const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-
|
|
485
|
+
const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-SOHKtDbk.js");
|
|
486
486
|
if (isHelpFlag && process.argv.length === 3) {
|
|
487
487
|
cmdHelp();
|
|
488
488
|
process.exit(0);
|
|
@@ -515,7 +515,7 @@ if (config.useRust) {
|
|
|
515
515
|
}
|
|
516
516
|
}
|
|
517
517
|
if (rustBinary) {
|
|
518
|
-
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-
|
|
518
|
+
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-DUVB1HyL.js");
|
|
519
519
|
const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
|
|
520
520
|
if (config.verbose) {
|
|
521
521
|
console.log(`[rust] Using binary: ${rustBinary}`);
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-
|
|
1
|
+
import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-B62nAAfY.js";
|
|
2
2
|
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-BdkE7S2A.js";
|
|
4
4
|
import "./pidStore-DBjlqzo8.js";
|
|
5
5
|
import "./globalPidIndex-yVd3mbsV.js";
|
|
6
6
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import "./ts-
|
|
1
|
+
import "./ts-B62nAAfY.js";
|
|
2
2
|
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-BdkE7S2A.js";
|
|
4
4
|
import "./pidStore-DBjlqzo8.js";
|
|
5
5
|
import "./globalPidIndex-yVd3mbsV.js";
|
|
6
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-
|
|
6
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-VayLM5qX.js";
|
|
7
7
|
import "./remotes-C3xPRtfg.js";
|
|
8
|
-
import { c as listRecords, d as renderRawLog, f as resolveOne, g as writeToIpc, m as snapshotStatus, r as controlCodeFromName, u as readNotes } from "./subcommands-
|
|
8
|
+
import { c as listRecords, d as renderRawLog, f as resolveOne, g as writeToIpc, m as snapshotStatus, r as controlCodeFromName, u as readNotes } from "./subcommands-t1uOb17r.js";
|
|
9
9
|
import yargs from "yargs";
|
|
10
10
|
import { mkdir, open, readFile, writeFile } from "fs/promises";
|
|
11
11
|
import { homedir, hostname, userInfo } from "os";
|
|
@@ -176,6 +176,11 @@ Options:
|
|
|
176
176
|
description: "Deprecated no-op — the console can always spawn agents"
|
|
177
177
|
}).help(false).version(false).exitProcess(false).parseAsync();
|
|
178
178
|
if (argv.daemon) return cmdServeDaemon("install", rest.filter((a) => a !== "--daemon" && a !== "-d"));
|
|
179
|
+
const stray = argv._.map(String);
|
|
180
|
+
if (stray.length) {
|
|
181
|
+
const hint = stray.includes("share") ? " (did you mean --share?)" : "";
|
|
182
|
+
process.stderr.write(`ay serve: ignoring unknown argument${stray.length > 1 ? "s" : ""}: ${stray.join(" ")}${hint}\n`);
|
|
183
|
+
}
|
|
179
184
|
const port = argv.port ?? DEFAULT_PORT;
|
|
180
185
|
const host = argv.host ?? "127.0.0.1";
|
|
181
186
|
const tokenFlag = typeof argv.token === "string" ? argv.token : void 0;
|
|
@@ -220,6 +225,58 @@ Options:
|
|
|
220
225
|
return null;
|
|
221
226
|
}
|
|
222
227
|
};
|
|
228
|
+
const GIT_TTL_MS = 5e3;
|
|
229
|
+
const gitCache = /* @__PURE__ */ new Map();
|
|
230
|
+
const gitStatus = async (cwd) => {
|
|
231
|
+
if (!cwd) return null;
|
|
232
|
+
const now = Date.now();
|
|
233
|
+
const hit = gitCache.get(cwd);
|
|
234
|
+
if (hit && now - hit.at < GIT_TTL_MS) return hit.val;
|
|
235
|
+
let val = null;
|
|
236
|
+
try {
|
|
237
|
+
const proc = Bun.spawn([
|
|
238
|
+
"git",
|
|
239
|
+
"status",
|
|
240
|
+
"--porcelain",
|
|
241
|
+
"--branch"
|
|
242
|
+
], {
|
|
243
|
+
cwd,
|
|
244
|
+
stdout: "pipe",
|
|
245
|
+
stderr: "ignore",
|
|
246
|
+
signal: AbortSignal.timeout(2e3)
|
|
247
|
+
});
|
|
248
|
+
const out = await new Response(proc.stdout).text();
|
|
249
|
+
await proc.exited;
|
|
250
|
+
if (proc.exitCode === 0) {
|
|
251
|
+
const lines = out.split("\n");
|
|
252
|
+
const h = /^## (.+)$/.exec(lines[0] ?? "")?.[1] ?? "";
|
|
253
|
+
const unborn = /^No commits yet on (.+)$/.exec(h);
|
|
254
|
+
const branch = unborn ? unborn[1] : /^(.+?)(?:\.\.\.|\s|$)/.exec(h)?.[1] || null;
|
|
255
|
+
const ahead = Number(/\bahead (\d+)/.exec(h)?.[1] ?? 0);
|
|
256
|
+
const behind = Number(/\bbehind (\d+)/.exec(h)?.[1] ?? 0);
|
|
257
|
+
const changed = lines.slice(1).filter((l) => l.trim().length > 0).length;
|
|
258
|
+
val = {
|
|
259
|
+
branch,
|
|
260
|
+
dirty: changed > 0,
|
|
261
|
+
changed,
|
|
262
|
+
ahead,
|
|
263
|
+
behind
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
val = null;
|
|
268
|
+
}
|
|
269
|
+
gitCache.set(cwd, {
|
|
270
|
+
at: now,
|
|
271
|
+
val
|
|
272
|
+
});
|
|
273
|
+
return val;
|
|
274
|
+
};
|
|
275
|
+
const withMeta = async (r) => ({
|
|
276
|
+
...r,
|
|
277
|
+
title: await logTitle(r.log_file),
|
|
278
|
+
git: r.status === "exited" ? null : await gitStatus(r.cwd)
|
|
279
|
+
});
|
|
223
280
|
const apiFetch = async (req) => {
|
|
224
281
|
if (!checkAuth(req, token)) return new Response("Unauthorized", { status: 401 });
|
|
225
282
|
const url = new URL(req.url);
|
|
@@ -232,11 +289,7 @@ Options:
|
|
|
232
289
|
});
|
|
233
290
|
try {
|
|
234
291
|
const records = await listRecords(keyword, opts);
|
|
235
|
-
|
|
236
|
-
...r,
|
|
237
|
-
title: await logTitle(r.log_file)
|
|
238
|
-
})));
|
|
239
|
-
return Response.json(withTitles);
|
|
292
|
+
return Response.json(await Promise.all(records.map(withMeta)));
|
|
240
293
|
} catch (e) {
|
|
241
294
|
return new Response(e.message, { status: 500 });
|
|
242
295
|
}
|
|
@@ -258,10 +311,7 @@ Options:
|
|
|
258
311
|
const sent = /* @__PURE__ */ new Map();
|
|
259
312
|
const compute = async () => {
|
|
260
313
|
const records = await listRecords(keyword, opts);
|
|
261
|
-
return Promise.all(records.map(
|
|
262
|
-
...r,
|
|
263
|
-
title: await logTitle(r.log_file)
|
|
264
|
-
})));
|
|
314
|
+
return Promise.all(records.map(withMeta));
|
|
265
315
|
};
|
|
266
316
|
const tick = async (first) => {
|
|
267
317
|
if (closed) return;
|
|
@@ -601,16 +651,18 @@ Options:
|
|
|
601
651
|
process.stdout.write(` ay remote add <alias> ${scheme}://${token}@<host>:${port}\n\n`);
|
|
602
652
|
if (!useHttps) process.stdout.write("for HTTPS: ay serve --tls-cert cert.pem --tls-key key.pem\n openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=localhost'\n\n");
|
|
603
653
|
}
|
|
654
|
+
let closeShare;
|
|
604
655
|
if (wantWebrtc) {
|
|
605
656
|
const webrtcVal = argv.webrtc ?? argv.share;
|
|
606
657
|
const explicitUrl = typeof webrtcVal === "string" && webrtcVal.startsWith("webrtc://") ? webrtcVal : void 0;
|
|
607
658
|
try {
|
|
608
|
-
const { startShare, loadOrCreateShareRoom } = await import("./share-
|
|
609
|
-
const { link } = await startShare({
|
|
659
|
+
const { startShare, loadOrCreateShareRoom } = await import("./share-DwzKXEsJ.js");
|
|
660
|
+
const { link, close } = await startShare({
|
|
610
661
|
url: explicitUrl ?? await loadOrCreateShareRoom(),
|
|
611
662
|
localFetch: apiFetch,
|
|
612
663
|
apiToken: token
|
|
613
664
|
});
|
|
665
|
+
closeShare = close;
|
|
614
666
|
process.stdout.write(`${wantHttp ? "\n" : ""}shared over WebRTC — open this link (the token is eaten from the URL on open):\n ${link}\n` + (explicitUrl ? "\n" : ` (persistent room — same link across restarts; delete ~/.agent-yes/.share-room to rotate)\n\n`));
|
|
615
667
|
} catch (e) {
|
|
616
668
|
process.stderr.write(`ay serve --webrtc failed: ${e.message}\n`);
|
|
@@ -620,10 +672,12 @@ Options:
|
|
|
620
672
|
process.stdout.write(`(Ctrl-C to stop)\n`);
|
|
621
673
|
await new Promise((resolve) => {
|
|
622
674
|
process.on("SIGINT", () => {
|
|
675
|
+
closeShare?.();
|
|
623
676
|
server?.stop();
|
|
624
677
|
resolve();
|
|
625
678
|
});
|
|
626
679
|
process.on("SIGTERM", () => {
|
|
680
|
+
closeShare?.();
|
|
627
681
|
server?.stop();
|
|
628
682
|
resolve();
|
|
629
683
|
});
|
|
@@ -633,4 +687,4 @@ Options:
|
|
|
633
687
|
|
|
634
688
|
//#endregion
|
|
635
689
|
export { cmdServe };
|
|
636
|
-
//# sourceMappingURL=serve-
|
|
690
|
+
//# sourceMappingURL=serve-CP61tKuJ.js.map
|
|
@@ -68,8 +68,12 @@ async function startShare(opts) {
|
|
|
68
68
|
const wsScheme = host.startsWith("localhost") || host.startsWith("127.") ? "ws" : "wss";
|
|
69
69
|
const link = `${host === "s.agent-yes.com" ? "https://agent-yes.com" : "http://localhost:7778"}/#${room}:${token}${host === "s.agent-yes.com" ? "" : "@" + host}`;
|
|
70
70
|
const peers = /* @__PURE__ */ new Map();
|
|
71
|
+
let closed = false;
|
|
72
|
+
let currentWs;
|
|
71
73
|
const connectSignaling = (onReady) => {
|
|
74
|
+
if (closed) return;
|
|
72
75
|
const ws = new WebSocket(`${wsScheme}://${host}/${room}`, [SUB]);
|
|
76
|
+
currentWs = ws;
|
|
73
77
|
let ready = false;
|
|
74
78
|
ws.onopen = () => {
|
|
75
79
|
ws.send(JSON.stringify({
|
|
@@ -81,6 +85,7 @@ async function startShare(opts) {
|
|
|
81
85
|
onReady();
|
|
82
86
|
};
|
|
83
87
|
ws.onmessage = async (ev) => {
|
|
88
|
+
if (closed) return;
|
|
84
89
|
const m = JSON.parse(ev.data);
|
|
85
90
|
if (m.type === "peer-join") startPeer(ws, m.peer);
|
|
86
91
|
else if (m.type === "answer") await peers.get(m.from)?.pc.setRemoteDescription({
|
|
@@ -91,6 +96,7 @@ async function startShare(opts) {
|
|
|
91
96
|
else if (m.type === "peer-leave") closePeer(m.peer);
|
|
92
97
|
};
|
|
93
98
|
ws.onclose = () => {
|
|
99
|
+
if (closed) return;
|
|
94
100
|
setTimeout(() => connectSignaling(() => {}), ready ? 1500 : 4e3);
|
|
95
101
|
};
|
|
96
102
|
ws.onerror = () => {};
|
|
@@ -135,7 +141,10 @@ async function startShare(opts) {
|
|
|
135
141
|
peers.delete(peerId);
|
|
136
142
|
}
|
|
137
143
|
function send(dc, obj) {
|
|
138
|
-
if (dc.readyState
|
|
144
|
+
if (dc.readyState !== "open") return;
|
|
145
|
+
try {
|
|
146
|
+
dc.send(JSON.stringify(obj));
|
|
147
|
+
} catch {}
|
|
139
148
|
}
|
|
140
149
|
async function onReq(dc, aborts, req) {
|
|
141
150
|
if (req.t === "abort") {
|
|
@@ -190,12 +199,20 @@ async function startShare(opts) {
|
|
|
190
199
|
}
|
|
191
200
|
}
|
|
192
201
|
await new Promise((resolve) => connectSignaling(resolve));
|
|
202
|
+
const close = () => {
|
|
203
|
+
closed = true;
|
|
204
|
+
try {
|
|
205
|
+
currentWs?.close();
|
|
206
|
+
} catch {}
|
|
207
|
+
for (const peerId of [...peers.keys()]) closePeer(peerId);
|
|
208
|
+
};
|
|
193
209
|
return {
|
|
194
210
|
room,
|
|
195
|
-
link
|
|
211
|
+
link,
|
|
212
|
+
close
|
|
196
213
|
};
|
|
197
214
|
}
|
|
198
215
|
|
|
199
216
|
//#endregion
|
|
200
217
|
export { loadOrCreateShareRoom, startShare };
|
|
201
|
-
//# sourceMappingURL=share-
|
|
218
|
+
//# sourceMappingURL=share-DwzKXEsJ.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "./logger-B9h0djqx.js";
|
|
2
2
|
import "./globalPidIndex-yVd3mbsV.js";
|
|
3
3
|
import "./remotes-C3xPRtfg.js";
|
|
4
|
-
import { a as finalizedLines, c as listRecords, d as renderRawLog, f as resolveOne, g as writeToIpc, h as stopTipForCli, i as cursorAbs, l as matchKeyword, m as snapshotStatus, n as cmdHelp, o as isPidAlive, p as runSubcommand, r as controlCodeFromName, s as isSubcommand, t as GRACEFUL_EXIT_COMMANDS, u as readNotes } from "./subcommands-
|
|
4
|
+
import { a as finalizedLines, c as listRecords, d as renderRawLog, f as resolveOne, g as writeToIpc, h as stopTipForCli, i as cursorAbs, l as matchKeyword, m as snapshotStatus, n as cmdHelp, o as isPidAlive, p as runSubcommand, r as controlCodeFromName, s as isSubcommand, t as GRACEFUL_EXIT_COMMANDS, u as readNotes } from "./subcommands-t1uOb17r.js";
|
|
5
5
|
|
|
6
6
|
export { cmdHelp, isSubcommand, runSubcommand };
|
|
@@ -163,7 +163,7 @@ async function runSubcommand(argv) {
|
|
|
163
163
|
case "restart": return await cmdRestart(rest);
|
|
164
164
|
case "note": return await cmdNote(rest);
|
|
165
165
|
case "serve": {
|
|
166
|
-
const { cmdServe } = await import("./serve-
|
|
166
|
+
const { cmdServe } = await import("./serve-CP61tKuJ.js");
|
|
167
167
|
return cmdServe(rest);
|
|
168
168
|
}
|
|
169
169
|
case "setup": {
|
|
@@ -1595,4 +1595,4 @@ async function cmdStatus(rest) {
|
|
|
1595
1595
|
|
|
1596
1596
|
//#endregion
|
|
1597
1597
|
export { finalizedLines as a, listRecords as c, renderRawLog as d, resolveOne as f, writeToIpc as g, stopTipForCli as h, cursorAbs as i, matchKeyword as l, snapshotStatus as m, cmdHelp as n, isPidAlive as o, runSubcommand as p, controlCodeFromName as r, isSubcommand as s, GRACEFUL_EXIT_COMMANDS as t, readNotes as u };
|
|
1598
|
-
//# sourceMappingURL=subcommands-
|
|
1598
|
+
//# sourceMappingURL=subcommands-t1uOb17r.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as logger, t as addTransport } from "./logger-B9h0djqx.js";
|
|
2
|
-
import { r as getInstalledPackage } from "./versionChecker-
|
|
2
|
+
import { r as getInstalledPackage } from "./versionChecker-BdkE7S2A.js";
|
|
3
3
|
import { n as agentYesHome, t as PidStore } from "./pidStore-DBjlqzo8.js";
|
|
4
4
|
import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-CJxsoGdb.js";
|
|
5
5
|
import { i as readGlobalPids } from "./globalPidIndex-yVd3mbsV.js";
|
|
@@ -1714,4 +1714,4 @@ function sleep(ms) {
|
|
|
1714
1714
|
|
|
1715
1715
|
//#endregion
|
|
1716
1716
|
export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
|
|
1717
|
-
//# sourceMappingURL=ts-
|
|
1717
|
+
//# sourceMappingURL=ts-B62nAAfY.js.map
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
|
|
|
7
7
|
|
|
8
8
|
//#region package.json
|
|
9
9
|
var name = "agent-yes";
|
|
10
|
-
var version = "1.
|
|
10
|
+
var version = "1.115.0";
|
|
11
11
|
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region ts/versionChecker.ts
|
|
@@ -221,4 +221,4 @@ async function displayVersion() {
|
|
|
221
221
|
|
|
222
222
|
//#endregion
|
|
223
223
|
export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
|
|
224
|
-
//# sourceMappingURL=versionChecker-
|
|
224
|
+
//# sourceMappingURL=versionChecker-BdkE7S2A.js.map
|
package/lab/ui/console-logic.js
CHANGED
|
@@ -115,6 +115,20 @@ export function tagsFor(e) {
|
|
|
115
115
|
return t;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
// Compact git indicator from the record's `git` snapshot (server-side
|
|
119
|
+
// `git status --porcelain --branch`): "±3" changed files, "↑1" ahead, "↓2"
|
|
120
|
+
// behind. Returns "" when there's no git info or the tree is clean and in sync,
|
|
121
|
+
// so a tidy repo adds no noise. Branch itself is shown via the path identity.
|
|
122
|
+
export function gitLabel(e) {
|
|
123
|
+
const g = e.git;
|
|
124
|
+
if (!g) return "";
|
|
125
|
+
const parts = [];
|
|
126
|
+
if (g.changed > 0) parts.push("±" + g.changed);
|
|
127
|
+
if (g.ahead > 0) parts.push("↑" + g.ahead);
|
|
128
|
+
if (g.behind > 0) parts.push("↓" + g.behind);
|
|
129
|
+
return parts.join(" ");
|
|
130
|
+
}
|
|
131
|
+
|
|
118
132
|
// Human age of an agent ("12s" / "5m" / "3h"). `now` is injectable so tests
|
|
119
133
|
// don't depend on the wall clock; the browser calls age(e) and gets Date.now().
|
|
120
134
|
export function age(e, now = Date.now()) {
|
|
@@ -130,7 +144,16 @@ export function age(e, now = Date.now()) {
|
|
|
130
144
|
// case-insensitive substring search over title/prompt/cli/cwd/status.
|
|
131
145
|
export function matches(e, toks) {
|
|
132
146
|
const hay =
|
|
133
|
-
(e.title || "") +
|
|
147
|
+
(e.title || "") +
|
|
148
|
+
" " +
|
|
149
|
+
(e.prompt || "") +
|
|
150
|
+
" " +
|
|
151
|
+
e.cli +
|
|
152
|
+
" " +
|
|
153
|
+
(e.cwd || "") +
|
|
154
|
+
" " +
|
|
155
|
+
e.status +
|
|
156
|
+
(e.git?.dirty ? " dirty" : "");
|
|
134
157
|
return toks.every((tok) => {
|
|
135
158
|
tok = tok.toLowerCase();
|
|
136
159
|
const ci = tok.indexOf(":");
|
package/lab/ui/index.html
CHANGED
|
@@ -505,6 +505,17 @@
|
|
|
505
505
|
color: var(--muted);
|
|
506
506
|
font-size: 11.5px;
|
|
507
507
|
}
|
|
508
|
+
/* git chip: ±changed ↑ahead ↓behind, amber when the worktree is dirty */
|
|
509
|
+
.git {
|
|
510
|
+
font-family: var(--mono);
|
|
511
|
+
font-size: 10.5px;
|
|
512
|
+
color: var(--muted);
|
|
513
|
+
white-space: nowrap;
|
|
514
|
+
flex: none;
|
|
515
|
+
}
|
|
516
|
+
.git.dirty {
|
|
517
|
+
color: var(--amber);
|
|
518
|
+
}
|
|
508
519
|
.detail {
|
|
509
520
|
color: var(--muted);
|
|
510
521
|
font-size: 12.5px;
|
|
@@ -737,6 +748,7 @@
|
|
|
737
748
|
/>
|
|
738
749
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
739
750
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
751
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
|
|
740
752
|
<script type="module">
|
|
741
753
|
// codehost room transport — vendored from codehost's `bun run build:lib`.
|
|
742
754
|
// Loaded as a module (deferred), so the classic script below awaits the
|
|
@@ -821,6 +833,7 @@
|
|
|
821
833
|
repoBranch,
|
|
822
834
|
ident,
|
|
823
835
|
tagsFor,
|
|
836
|
+
gitLabel,
|
|
824
837
|
age,
|
|
825
838
|
matches,
|
|
826
839
|
nextIndex,
|
|
@@ -909,6 +922,7 @@
|
|
|
909
922
|
connect() {
|
|
910
923
|
return new Promise((resolve, reject) => {
|
|
911
924
|
const ws = new WebSocket(`wss://${this.host}/${this.room}`, [SUB]);
|
|
925
|
+
this.ws = ws; // kept so close() can drop the signaling registration too
|
|
912
926
|
let pc,
|
|
913
927
|
settled = false;
|
|
914
928
|
const fail = (e) => {
|
|
@@ -966,6 +980,7 @@
|
|
|
966
980
|
if (stream) stream(r.chunk);
|
|
967
981
|
} else if (r.t === "end") {
|
|
968
982
|
if (call) {
|
|
983
|
+
clearTimeout(call.timer);
|
|
969
984
|
this.calls.delete(r.id);
|
|
970
985
|
r.error
|
|
971
986
|
? call.reject(new Error(r.error))
|
|
@@ -976,8 +991,21 @@
|
|
|
976
991
|
req(method, path, body) {
|
|
977
992
|
const id = this.nextId++;
|
|
978
993
|
return new Promise((resolve, reject) => {
|
|
979
|
-
|
|
980
|
-
|
|
994
|
+
// Without a deadline a request over a silently-dead DataChannel (host
|
|
995
|
+
// gone, ICE not yet timed out) never settles, so the caller — and the
|
|
996
|
+
// poll loop — hangs forever and the room never reconnects. Reject on a
|
|
997
|
+
// timeout so listSource sees the failure and triggers backoff.
|
|
998
|
+
const timer = setTimeout(() => {
|
|
999
|
+
if (this.calls.delete(id)) reject(new Error("request timed out"));
|
|
1000
|
+
}, 12000);
|
|
1001
|
+
this.calls.set(id, { status: 0, body: "", resolve, reject, timer });
|
|
1002
|
+
try {
|
|
1003
|
+
this.dc.send(JSON.stringify({ t: "req", id, method, path, body }));
|
|
1004
|
+
} catch (e) {
|
|
1005
|
+
clearTimeout(timer);
|
|
1006
|
+
this.calls.delete(id);
|
|
1007
|
+
reject(e); // channel already torn down
|
|
1008
|
+
}
|
|
981
1009
|
});
|
|
982
1010
|
}
|
|
983
1011
|
subscribe(path, onRaw) {
|
|
@@ -991,6 +1019,26 @@
|
|
|
991
1019
|
} catch {}
|
|
992
1020
|
};
|
|
993
1021
|
}
|
|
1022
|
+
// Tear down BOTH wires. Closing only the pc leaves the signaling socket
|
|
1023
|
+
// open and this client registered in the room, so each reconnect would
|
|
1024
|
+
// leak another peer on the host. onstate is detached by the caller first.
|
|
1025
|
+
close() {
|
|
1026
|
+
try {
|
|
1027
|
+
this.ws?.close();
|
|
1028
|
+
} catch {}
|
|
1029
|
+
try {
|
|
1030
|
+
this.pc?.close();
|
|
1031
|
+
} catch {}
|
|
1032
|
+
this.dc = null;
|
|
1033
|
+
// Settle anything still in flight now, rather than letting each req's
|
|
1034
|
+
// 12s timeout fire long after the client is gone.
|
|
1035
|
+
for (const c of this.calls.values()) {
|
|
1036
|
+
clearTimeout(c.timer);
|
|
1037
|
+
c.reject(new Error("connection closed"));
|
|
1038
|
+
}
|
|
1039
|
+
this.calls.clear();
|
|
1040
|
+
this.streams.clear();
|
|
1041
|
+
}
|
|
994
1042
|
}
|
|
995
1043
|
|
|
996
1044
|
// ---- codehost rooms: a THIRD wire -----------------------------------
|
|
@@ -1298,6 +1346,11 @@
|
|
|
1298
1346
|
s.serverCount = 0;
|
|
1299
1347
|
s.byPid = new Map();
|
|
1300
1348
|
s.agents = [];
|
|
1349
|
+
// A failed poll on an agent-yes room means the tunnel is unresponsive
|
|
1350
|
+
// (host gone/restarted) — the fastest, most reliable drop signal we get,
|
|
1351
|
+
// since WebRTC's own failed/closed state can lag 15-30s. Kick off the
|
|
1352
|
+
// backoff reconnect (no-op if one's already queued or the room's gone).
|
|
1353
|
+
if (s.kind === "rtc") scheduleRtcReconnect(s);
|
|
1301
1354
|
return [];
|
|
1302
1355
|
}
|
|
1303
1356
|
}
|
|
@@ -1333,6 +1386,16 @@
|
|
|
1333
1386
|
// Compact list: one line per agent (dot + cli + title), persisted per device.
|
|
1334
1387
|
let compactList = localStorage.getItem("ay.compactList") === "1";
|
|
1335
1388
|
|
|
1389
|
+
// A small git chip (±changed ↑ahead ↓behind) for a row, amber when the tree
|
|
1390
|
+
// is dirty. Empty for clean/in-sync repos and non-git cwds — no noise.
|
|
1391
|
+
function gitChipHtml(e) {
|
|
1392
|
+
const label = gitLabel(e);
|
|
1393
|
+
if (!label) return "";
|
|
1394
|
+
const g = e.git || {};
|
|
1395
|
+
const tip = `git: ${g.changed || 0} changed · ${g.ahead || 0} ahead · ${g.behind || 0} behind`;
|
|
1396
|
+
return `<span class="git${g.dirty ? " dirty" : ""}" title="${esc(tip)}">${esc(label)}</span>`;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1336
1399
|
function renderList() {
|
|
1337
1400
|
const toks = $("q").value.trim().split(/\s+/).filter(Boolean);
|
|
1338
1401
|
const shown = entries.filter((e) => matches(e, toks));
|
|
@@ -1355,6 +1418,7 @@
|
|
|
1355
1418
|
${hasIdent(id) ? `<span class="cident" title="${esc(fullIdent(e))}">${esc(id)}</span>` : ""}
|
|
1356
1419
|
${cli ? `<span class="cname">${esc(cli)}</span>` : ""}
|
|
1357
1420
|
<span class="ctitle ${e.title ? "" : "dim"}" title="${esc(t)}">${esc(t)}</span>
|
|
1421
|
+
${gitChipHtml(e)}
|
|
1358
1422
|
<span class="age">${age(e)}</span></div>`;
|
|
1359
1423
|
})
|
|
1360
1424
|
.join("") || `<div class="empty">no match</div>`;
|
|
@@ -1376,6 +1440,7 @@
|
|
|
1376
1440
|
<div class="r1"><span class="dot ${esc(e.status)}"></span>
|
|
1377
1441
|
<span class="name">${esc(cliLabel(e) || ident(e) || "agent")}</span>
|
|
1378
1442
|
<span class="badge">pid ${e.pid}</span>
|
|
1443
|
+
${gitChipHtml(e)}
|
|
1379
1444
|
<span class="age">${age(e)}</span></div>
|
|
1380
1445
|
${e.title ? `<div class="rowtitle" title="${esc(e.title)}">${esc(e.title)}</div>` : ""}
|
|
1381
1446
|
${e.prompt ? `<div class="detail" title="${esc(e.prompt)}">${esc(e.prompt)}</div>` : ""}
|
|
@@ -1513,6 +1578,19 @@
|
|
|
1513
1578
|
});
|
|
1514
1579
|
fit = new FitAddon.FitAddon();
|
|
1515
1580
|
term.loadAddon(fit);
|
|
1581
|
+
// Make plain URLs in the output clickable. Agents that emit OSC 8
|
|
1582
|
+
// hyperlinks already get clickable text, but most output is bare URLs —
|
|
1583
|
+
// this addon scans the rendered buffer and links them too. Click opens a
|
|
1584
|
+
// new tab (noopener so the page can't be tampered with via window.opener).
|
|
1585
|
+
try {
|
|
1586
|
+
term.loadAddon(
|
|
1587
|
+
new WebLinksAddon.WebLinksAddon((e, uri) =>
|
|
1588
|
+
window.open(uri, "_blank", "noopener,noreferrer"),
|
|
1589
|
+
),
|
|
1590
|
+
);
|
|
1591
|
+
} catch {
|
|
1592
|
+
/* addon CDN blocked — terminal still works, just without auto-links */
|
|
1593
|
+
}
|
|
1516
1594
|
term.open(logEl);
|
|
1517
1595
|
// On a phone, auto-focusing yanks up the soft keyboard the moment you open
|
|
1518
1596
|
// an agent — even when you only meant to read the tail. Skip it there; a tap
|
|
@@ -1709,6 +1787,8 @@
|
|
|
1709
1787
|
function removeSource(room) {
|
|
1710
1788
|
const s = sources.get(room);
|
|
1711
1789
|
if (!s) return;
|
|
1790
|
+
s.removed = true; // stop any pending exp-backoff reconnect (see below)
|
|
1791
|
+
clearTimeout(s.reconnectTimer);
|
|
1712
1792
|
unsubscribeSource(s);
|
|
1713
1793
|
try {
|
|
1714
1794
|
s.client?.close?.();
|
|
@@ -1717,6 +1797,97 @@
|
|
|
1717
1797
|
sources.delete(room);
|
|
1718
1798
|
}
|
|
1719
1799
|
|
|
1800
|
+
// Exponential-backoff reconnect for an agent-yes share room (the RTCClient
|
|
1801
|
+
// wire). codehost rooms already self-heal via room-client.js; the agent-yes
|
|
1802
|
+
// viewer was one-shot, so a dropped DataChannel (e.g. the host restarting)
|
|
1803
|
+
// left the room dead until a manual reload. Retry with jittered backoff
|
|
1804
|
+
// until the room is forgotten.
|
|
1805
|
+
const RTC_BACKOFF_MIN = 1000,
|
|
1806
|
+
RTC_BACKOFF_MAX = 30000;
|
|
1807
|
+
async function connectRtcSource(s) {
|
|
1808
|
+
s.reconnecting = true; // block overlapping attempts while this one is in flight
|
|
1809
|
+
try {
|
|
1810
|
+
// Detach the stale peer's onstate BEFORE closing it — otherwise its own
|
|
1811
|
+
// "closed" event schedules another reconnect mid-replace, and that timer
|
|
1812
|
+
// fires after we've already recovered, churning a healthy channel forever.
|
|
1813
|
+
if (s.client) s.client.onstate = () => {};
|
|
1814
|
+
// The delta stream died silently with the old client (RTCClient never
|
|
1815
|
+
// fires onError), leaving s.streaming=true and s.unsub set — which would
|
|
1816
|
+
// make the reconcile poll skip this source AND block re-subscription.
|
|
1817
|
+
// Clear it so the fresh transport below re-establishes the stream.
|
|
1818
|
+
unsubscribeSource(s);
|
|
1819
|
+
try {
|
|
1820
|
+
s.client?.close?.(); // drop the stale peer AND its signaling socket
|
|
1821
|
+
} catch {}
|
|
1822
|
+
const c = new RTCClient(s.host, s.id, s.token);
|
|
1823
|
+
c.onstate = (st) => {
|
|
1824
|
+
if (st === "failed" || st === "closed") {
|
|
1825
|
+
s.live = false;
|
|
1826
|
+
renderRoomsIfOpen();
|
|
1827
|
+
scheduleRtcReconnect(s); // "disconnected" is transient; ignore it
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
try {
|
|
1831
|
+
await c.connect();
|
|
1832
|
+
} catch (e) {
|
|
1833
|
+
c.close(); // connect() already opened the signaling socket — don't
|
|
1834
|
+
throw e; // leak it on every failed retry of an offline room
|
|
1835
|
+
}
|
|
1836
|
+
if (s.removed) {
|
|
1837
|
+
c.close(); // room was forgotten mid-connect — don't install/leak it
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
s.client = c;
|
|
1841
|
+
s.tx = rtcTx(c);
|
|
1842
|
+
s.reconnectDelay = RTC_BACKOFF_MIN; // a healthy connect resets the backoff
|
|
1843
|
+
clearTimeout(s.reconnectTimer); // cancel any reconnect queued during replace
|
|
1844
|
+
s.reconnectTimer = null;
|
|
1845
|
+
subscribeSource(s); // re-establish the live delta stream over the new wire
|
|
1846
|
+
// agent-yes share (unlike codehost) sends no per-agent device id, so
|
|
1847
|
+
// every machine's rows would be unlabelled. Ask the host who it is once
|
|
1848
|
+
// and tag this room's agents with it in listSource. Best-effort: an older
|
|
1849
|
+
// host with no /api/whoami just leaves rows device-less.
|
|
1850
|
+
try {
|
|
1851
|
+
s.deviceLabel = (await s.tx.fetchJSON("/api/whoami"))?.host || "";
|
|
1852
|
+
// whoami resolves after the first listSource may have already rendered
|
|
1853
|
+
// this room's rows device-less (and activity-gated polling might not
|
|
1854
|
+
// re-fire soon on a backgrounded tab). Re-render now so the host label
|
|
1855
|
+
// lands immediately instead of lagging a poll.
|
|
1856
|
+
if (s.deviceLabel) loadList();
|
|
1857
|
+
} catch {
|
|
1858
|
+
s.deviceLabel = "";
|
|
1859
|
+
}
|
|
1860
|
+
} finally {
|
|
1861
|
+
s.reconnecting = false;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
function scheduleRtcReconnect(s) {
|
|
1865
|
+
// forgotten, already queued, or a connect is already in flight
|
|
1866
|
+
if (s.removed || s.reconnectTimer || s.reconnecting) return;
|
|
1867
|
+
const base = s.reconnectDelay || RTC_BACKOFF_MIN;
|
|
1868
|
+
const delay = Math.round(base * (0.75 + Math.random() * 0.5)); // jitter
|
|
1869
|
+
s.reconnectDelay = Math.min(base * 2, RTC_BACKOFF_MAX);
|
|
1870
|
+
s.reconnectTimer = setTimeout(async () => {
|
|
1871
|
+
s.reconnectTimer = null;
|
|
1872
|
+
if (s.removed || s.reconnecting) return;
|
|
1873
|
+
try {
|
|
1874
|
+
await connectRtcSource(s);
|
|
1875
|
+
s.live = true;
|
|
1876
|
+
renderRoomsIfOpen();
|
|
1877
|
+
// Await the refresh: a prior failed poll cleared `entries`, so select()
|
|
1878
|
+
// below would find nothing and skip the rebind until the list is back.
|
|
1879
|
+
await loadList();
|
|
1880
|
+
// A terminal opened before the drop still holds the dead tx (its tail
|
|
1881
|
+
// subscription + send closures). Re-select to rebind it to the new
|
|
1882
|
+
// transport, else the room looks live but the terminal stays frozen.
|
|
1883
|
+
if (sel && sel.startsWith(s.id + "#")) select(sel);
|
|
1884
|
+
} catch {
|
|
1885
|
+
s.live = false;
|
|
1886
|
+
scheduleRtcReconnect(s); // keep trying
|
|
1887
|
+
}
|
|
1888
|
+
}, delay);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1720
1891
|
// Add a room to the fleet and connect it — WITHOUT dropping the others, so
|
|
1721
1892
|
// every saved room streams its agents at once. Idempotent: a room already
|
|
1722
1893
|
// in the fleet just refreshes. The connection runs in the background; the
|
|
@@ -1751,29 +1922,13 @@
|
|
|
1751
1922
|
s.client = c;
|
|
1752
1923
|
s.tx = c;
|
|
1753
1924
|
} else {
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
if (st === "failed" || st === "closed") {
|
|
1757
|
-
s.live = false;
|
|
1758
|
-
renderRoomsIfOpen();
|
|
1759
|
-
}
|
|
1760
|
-
};
|
|
1761
|
-
await c.connect();
|
|
1762
|
-
s.client = c;
|
|
1763
|
-
s.tx = rtcTx(c);
|
|
1764
|
-
// agent-yes share (unlike codehost) sends no per-agent device id, so
|
|
1765
|
-
// every machine's rows would be unlabelled. Ask the host who it is
|
|
1766
|
-
// once and tag this room's agents with it in listSource. Best-effort:
|
|
1767
|
-
// an older host with no /api/whoami just leaves rows device-less.
|
|
1768
|
-
try {
|
|
1769
|
-
s.deviceLabel = (await s.tx.fetchJSON("/api/whoami"))?.host || "";
|
|
1770
|
-
} catch {
|
|
1771
|
-
s.deviceLabel = "";
|
|
1772
|
-
}
|
|
1925
|
+
s.token = token;
|
|
1926
|
+
await connectRtcSource(s);
|
|
1773
1927
|
}
|
|
1774
1928
|
s.live = true;
|
|
1775
1929
|
} catch (e) {
|
|
1776
1930
|
s.live = false;
|
|
1931
|
+
if (s.kind === "rtc") scheduleRtcReconnect(s);
|
|
1777
1932
|
}
|
|
1778
1933
|
s.tried = true;
|
|
1779
1934
|
renderRoomsIfOpen();
|
|
@@ -1843,7 +1998,7 @@
|
|
|
1843
1998
|
<span style="opacity:.6">Windows:</span>
|
|
1844
1999
|
<code id="cmdinstallwin" class="copy" title="click to copy">powershell -c "irm https://agent-yes.com/setup.ps1 | iex"</code></div>
|
|
1845
2000
|
<div class="rconnect">share your own fleet — run this, then open the printed link:
|
|
1846
|
-
<code id="cmd" class="copy" title="click to copy">ay serve share</code></div>
|
|
2001
|
+
<code id="cmd" class="copy" title="click to copy">ay serve --share</code></div>
|
|
1847
2002
|
<div class="rconnect">or view a <b>codehost</b> room — paste <code>ch:<room-token></code> (or a
|
|
1848
2003
|
codehost.dev share link) above; every machine in the room shows its agents here.</div>`;
|
|
1849
2004
|
}
|
package/package.json
CHANGED
package/ts/serve.ts
CHANGED
|
@@ -228,6 +228,18 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
228
228
|
return cmdServeDaemon("install", fwd);
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
// `ay serve` takes only flags (plus the install/uninstall/logs subcommands
|
|
232
|
+
// handled above). A bare word like `ay serve share` is silently dropped into
|
|
233
|
+
// argv._ by yargs and would otherwise start in the wrong mode — most often
|
|
234
|
+
// it's a typo for the `--share` flag — so warn instead of quietly ignoring it.
|
|
235
|
+
const stray = (argv._ as Array<string | number>).map(String);
|
|
236
|
+
if (stray.length) {
|
|
237
|
+
const hint = stray.includes("share") ? " (did you mean --share?)" : "";
|
|
238
|
+
process.stderr.write(
|
|
239
|
+
`ay serve: ignoring unknown argument${stray.length > 1 ? "s" : ""}: ${stray.join(" ")}${hint}\n`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
231
243
|
const port = (argv.port as number) ?? DEFAULT_PORT;
|
|
232
244
|
const host = (argv.host as string) ?? "127.0.0.1";
|
|
233
245
|
const tokenFlag = typeof argv.token === "string" ? argv.token : undefined;
|
|
@@ -291,6 +303,62 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
291
303
|
}
|
|
292
304
|
};
|
|
293
305
|
|
|
306
|
+
// Per-cwd git snapshot for the list: branch + dirty/changed count + ahead/behind
|
|
307
|
+
// vs upstream, all from a single `git status --porcelain --branch`. Cached per
|
|
308
|
+
// cwd with a short TTL so the 1s subscribe tick (and /api/ls polls) spawn at most
|
|
309
|
+
// one git per repo every few seconds — agents sharing a cwd share the result.
|
|
310
|
+
// Non-git dirs, errors, and timeouts cache as null.
|
|
311
|
+
interface GitInfo {
|
|
312
|
+
branch: string | null;
|
|
313
|
+
dirty: boolean;
|
|
314
|
+
changed: number;
|
|
315
|
+
ahead: number;
|
|
316
|
+
behind: number;
|
|
317
|
+
}
|
|
318
|
+
const GIT_TTL_MS = 5000;
|
|
319
|
+
const gitCache = new Map<string, { at: number; val: GitInfo | null }>();
|
|
320
|
+
const gitStatus = async (cwd: string | null | undefined): Promise<GitInfo | null> => {
|
|
321
|
+
if (!cwd) return null;
|
|
322
|
+
const now = Date.now();
|
|
323
|
+
const hit = gitCache.get(cwd);
|
|
324
|
+
if (hit && now - hit.at < GIT_TTL_MS) return hit.val;
|
|
325
|
+
let val: GitInfo | null = null;
|
|
326
|
+
try {
|
|
327
|
+
const proc = Bun.spawn(["git", "status", "--porcelain", "--branch"], {
|
|
328
|
+
cwd,
|
|
329
|
+
stdout: "pipe",
|
|
330
|
+
stderr: "ignore",
|
|
331
|
+
signal: AbortSignal.timeout(2000),
|
|
332
|
+
});
|
|
333
|
+
const out = await new Response(proc.stdout).text();
|
|
334
|
+
await proc.exited;
|
|
335
|
+
if (proc.exitCode === 0) {
|
|
336
|
+
const lines = out.split("\n");
|
|
337
|
+
// Branch header, e.g. "## main...origin/main [ahead 1, behind 2]",
|
|
338
|
+
// "## main" (no upstream), "## HEAD (no branch)", or "## No commits yet on x".
|
|
339
|
+
const h = /^## (.+)$/.exec(lines[0] ?? "")?.[1] ?? "";
|
|
340
|
+
const unborn = /^No commits yet on (.+)$/.exec(h);
|
|
341
|
+
const branch = unborn ? unborn[1]! : /^(.+?)(?:\.\.\.|\s|$)/.exec(h)?.[1] || null;
|
|
342
|
+
const ahead = Number(/\bahead (\d+)/.exec(h)?.[1] ?? 0);
|
|
343
|
+
const behind = Number(/\bbehind (\d+)/.exec(h)?.[1] ?? 0);
|
|
344
|
+
const changed = lines.slice(1).filter((l) => l.trim().length > 0).length;
|
|
345
|
+
val = { branch, dirty: changed > 0, changed, ahead, behind };
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
val = null; // git missing, not a repo, or timed out
|
|
349
|
+
}
|
|
350
|
+
gitCache.set(cwd, { at: now, val });
|
|
351
|
+
return val;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// One agent record decorated for the console: the latest OSC title + a git
|
|
355
|
+
// snapshot (skipped for exited agents — their repo state is no longer live).
|
|
356
|
+
const withMeta = async (r: Awaited<ReturnType<typeof listRecords>>[number]) => ({
|
|
357
|
+
...r,
|
|
358
|
+
title: await logTitle(r.log_file),
|
|
359
|
+
git: r.status === "exited" ? null : await gitStatus(r.cwd),
|
|
360
|
+
});
|
|
361
|
+
|
|
294
362
|
// The whole API as a plain handler: served over HTTP by Bun.serve (--http)
|
|
295
363
|
// and called in-process by the WebRTC bridge (--webrtc) — the latter needs
|
|
296
364
|
// no TCP port at all.
|
|
@@ -311,10 +379,7 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
311
379
|
});
|
|
312
380
|
try {
|
|
313
381
|
const records = await listRecords(keyword, opts);
|
|
314
|
-
|
|
315
|
-
records.map(async (r) => ({ ...r, title: await logTitle(r.log_file) })),
|
|
316
|
-
);
|
|
317
|
-
return Response.json(withTitles);
|
|
382
|
+
return Response.json(await Promise.all(records.map(withMeta)));
|
|
318
383
|
} catch (e) {
|
|
319
384
|
return new Response((e as Error).message, { status: 500 });
|
|
320
385
|
}
|
|
@@ -348,9 +413,7 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
348
413
|
const sent = new Map<number, string>();
|
|
349
414
|
const compute = async () => {
|
|
350
415
|
const records = await listRecords(keyword, opts);
|
|
351
|
-
return Promise.all(
|
|
352
|
-
records.map(async (r) => ({ ...r, title: await logTitle(r.log_file) })),
|
|
353
|
-
);
|
|
416
|
+
return Promise.all(records.map(withMeta));
|
|
354
417
|
};
|
|
355
418
|
const tick = async (first: boolean) => {
|
|
356
419
|
if (closed) return;
|
|
@@ -777,6 +840,7 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
777
840
|
// can reach this machine peer-to-peer. The bridge calls apiFetch in-process,
|
|
778
841
|
// so without --http no port is opened at all. Bare flag mints a room; a
|
|
779
842
|
// webrtc:// value joins an explicit one.
|
|
843
|
+
let closeShare: (() => void) | undefined; // closes WebRTC peers on shutdown
|
|
780
844
|
if (wantWebrtc) {
|
|
781
845
|
const webrtcVal = (argv.webrtc ?? argv.share) as string | undefined;
|
|
782
846
|
const explicitUrl =
|
|
@@ -785,11 +849,12 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
785
849
|
const { startShare, loadOrCreateShareRoom } = await import("./share.ts");
|
|
786
850
|
// No explicit webrtc:// URL → reuse the persisted room (minted once and
|
|
787
851
|
// saved like the serve token), so the link is stable across restarts.
|
|
788
|
-
const { link } = await startShare({
|
|
852
|
+
const { link, close } = await startShare({
|
|
789
853
|
url: explicitUrl ?? (await loadOrCreateShareRoom()),
|
|
790
854
|
localFetch: apiFetch,
|
|
791
855
|
apiToken: token,
|
|
792
856
|
});
|
|
857
|
+
closeShare = close;
|
|
793
858
|
process.stdout.write(
|
|
794
859
|
`${wantHttp ? "\n" : ""}shared over WebRTC — open this link (the token is eaten from the URL on open):\n ${link}\n` +
|
|
795
860
|
(explicitUrl
|
|
@@ -806,10 +871,12 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
806
871
|
|
|
807
872
|
await new Promise<void>((resolve) => {
|
|
808
873
|
process.on("SIGINT", () => {
|
|
874
|
+
closeShare?.();
|
|
809
875
|
server?.stop();
|
|
810
876
|
resolve();
|
|
811
877
|
});
|
|
812
878
|
process.on("SIGTERM", () => {
|
|
879
|
+
closeShare?.();
|
|
813
880
|
server?.stop();
|
|
814
881
|
resolve();
|
|
815
882
|
});
|
package/ts/share.ts
CHANGED
|
@@ -90,7 +90,9 @@ async function importRTC(): Promise<any> {
|
|
|
90
90
|
|
|
91
91
|
/** Start the share bridge. Resolves once signaling is connected; runs until the
|
|
92
92
|
* process exits, reconnecting signaling on drop. Returns the shareable link. */
|
|
93
|
-
export async function startShare(
|
|
93
|
+
export async function startShare(
|
|
94
|
+
opts: ShareOpts,
|
|
95
|
+
): Promise<{ room: string; link: string; close: () => void }> {
|
|
94
96
|
const minted = !opts.url;
|
|
95
97
|
const sighost = opts.sighost ?? DEFAULT_SIGHOST;
|
|
96
98
|
const { room, token, host } = opts.url
|
|
@@ -109,9 +111,13 @@ export async function startShare(opts: ShareOpts): Promise<{ room: string; link:
|
|
|
109
111
|
|
|
110
112
|
type Peer = { pc: any; aborts: Map<number, AbortController> };
|
|
111
113
|
const peers = new Map<string, Peer>();
|
|
114
|
+
let closed = false; // set by close(); stops signaling reconnect + new peers
|
|
115
|
+
let currentWs: WebSocket | undefined; // the live rendezvous socket, for close()
|
|
112
116
|
|
|
113
117
|
const connectSignaling = (onReady: () => void) => {
|
|
118
|
+
if (closed) return; // a reconnect timer queued before close() must not revive it
|
|
114
119
|
const ws = new WebSocket(`${wsScheme}://${host}/${room}`, [SUB]);
|
|
120
|
+
currentWs = ws;
|
|
115
121
|
let ready = false;
|
|
116
122
|
ws.onopen = () => {
|
|
117
123
|
ws.send(JSON.stringify({ type: "hello", role: "host", token }));
|
|
@@ -119,6 +125,7 @@ export async function startShare(opts: ShareOpts): Promise<{ room: string; link:
|
|
|
119
125
|
onReady();
|
|
120
126
|
};
|
|
121
127
|
ws.onmessage = async (ev) => {
|
|
128
|
+
if (closed) return;
|
|
122
129
|
const m = JSON.parse(ev.data as string);
|
|
123
130
|
if (m.type === "peer-join") startPeer(ws, m.peer);
|
|
124
131
|
else if (m.type === "answer")
|
|
@@ -131,6 +138,7 @@ export async function startShare(opts: ShareOpts): Promise<{ room: string; link:
|
|
|
131
138
|
else if (m.type === "peer-leave") closePeer(m.peer);
|
|
132
139
|
};
|
|
133
140
|
ws.onclose = () => {
|
|
141
|
+
if (closed) return; // shutting down — don't resurrect the rendezvous
|
|
134
142
|
// Keep established WebRTC peers; just re-establish the rendezvous so new
|
|
135
143
|
// browsers can still join. Backoff a little to avoid hot-looping.
|
|
136
144
|
setTimeout(() => connectSignaling(() => {}), ready ? 1500 : 4000);
|
|
@@ -172,7 +180,16 @@ export async function startShare(opts: ShareOpts): Promise<{ room: string; link:
|
|
|
172
180
|
}
|
|
173
181
|
|
|
174
182
|
function send(dc: any, obj: object) {
|
|
175
|
-
|
|
183
|
+
// readyState alone is racy: node-datachannel can still report "open" for a
|
|
184
|
+
// tick after a dropped peer's channel is torn down underneath, so dc.send()
|
|
185
|
+
// throws "DataChannel is closed". Swallow it — the frame is for a peer that's
|
|
186
|
+
// already gone (closePeer aborts its in-flight requests right behind this).
|
|
187
|
+
if (dc.readyState !== "open") return;
|
|
188
|
+
try {
|
|
189
|
+
dc.send(JSON.stringify(obj));
|
|
190
|
+
} catch {
|
|
191
|
+
/* peer vanished mid-send; dropping the frame is correct */
|
|
192
|
+
}
|
|
176
193
|
}
|
|
177
194
|
|
|
178
195
|
async function onReq(dc: any, aborts: Map<number, AbortController>, req: any) {
|
|
@@ -218,5 +235,19 @@ export async function startShare(opts: ShareOpts): Promise<{ room: string; link:
|
|
|
218
235
|
|
|
219
236
|
await new Promise<void>((resolve) => connectSignaling(resolve));
|
|
220
237
|
void minted; // (informational) caller decides how to surface the link
|
|
221
|
-
|
|
238
|
+
|
|
239
|
+
// Clean shutdown: stop the rendezvous (so it can't reconnect or accept new
|
|
240
|
+
// peers) and close every peer connection so browsers get an immediate
|
|
241
|
+
// DataChannel close and reconnect right away, instead of waiting out the
|
|
242
|
+
// ~15-30s ICE timeout that an abrupt process exit would otherwise force.
|
|
243
|
+
const close = () => {
|
|
244
|
+
closed = true;
|
|
245
|
+
try {
|
|
246
|
+
currentWs?.close();
|
|
247
|
+
} catch {
|
|
248
|
+
/* already closing */
|
|
249
|
+
}
|
|
250
|
+
for (const peerId of [...peers.keys()]) closePeer(peerId);
|
|
251
|
+
};
|
|
252
|
+
return { room, link, close };
|
|
222
253
|
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import "./ts-ChVJOKNr.js";
|
|
2
|
-
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./versionChecker-wxlWV_VT.js";
|
|
4
|
-
import "./pidStore-DBjlqzo8.js";
|
|
5
|
-
import "./globalPidIndex-yVd3mbsV.js";
|
|
6
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-BpO7ZYx_.js";
|
|
7
|
-
|
|
8
|
-
export { SUPPORTED_CLIS };
|