agent-yes 1.114.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--15XPtGd.js → SUPPORTED_CLIS-VayLM5qX.js} +2 -2
- package/dist/cli.js +3 -3
- package/dist/index.js +2 -2
- package/dist/{serve-DEDkRdog.js → serve-CP61tKuJ.js} +16 -7
- package/dist/{share-BsCeIfQM.js → share-DwzKXEsJ.js} +20 -3
- package/dist/{subcommands-8hMGFqkQ.js → subcommands-SOHKtDbk.js} +1 -1
- package/dist/{subcommands-Di_pcW0_.js → subcommands-t1uOb17r.js} +2 -2
- package/dist/{ts-CZQs_t8w.js → ts-B62nAAfY.js} +2 -2
- package/dist/{versionChecker-C1W2s4ZP.js → versionChecker-BdkE7S2A.js} +2 -2
- package/lab/ui/index.html +153 -22
- package/package.json +1 -1
- package/ts/serve.ts +17 -1
- package/ts/share.ts +34 -3
- package/dist/SUPPORTED_CLIS-DE31jVYi.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;
|
|
@@ -646,16 +651,18 @@ Options:
|
|
|
646
651
|
process.stdout.write(` ay remote add <alias> ${scheme}://${token}@<host>:${port}\n\n`);
|
|
647
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");
|
|
648
653
|
}
|
|
654
|
+
let closeShare;
|
|
649
655
|
if (wantWebrtc) {
|
|
650
656
|
const webrtcVal = argv.webrtc ?? argv.share;
|
|
651
657
|
const explicitUrl = typeof webrtcVal === "string" && webrtcVal.startsWith("webrtc://") ? webrtcVal : void 0;
|
|
652
658
|
try {
|
|
653
|
-
const { startShare, loadOrCreateShareRoom } = await import("./share-
|
|
654
|
-
const { link } = await startShare({
|
|
659
|
+
const { startShare, loadOrCreateShareRoom } = await import("./share-DwzKXEsJ.js");
|
|
660
|
+
const { link, close } = await startShare({
|
|
655
661
|
url: explicitUrl ?? await loadOrCreateShareRoom(),
|
|
656
662
|
localFetch: apiFetch,
|
|
657
663
|
apiToken: token
|
|
658
664
|
});
|
|
665
|
+
closeShare = close;
|
|
659
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`));
|
|
660
667
|
} catch (e) {
|
|
661
668
|
process.stderr.write(`ay serve --webrtc failed: ${e.message}\n`);
|
|
@@ -665,10 +672,12 @@ Options:
|
|
|
665
672
|
process.stdout.write(`(Ctrl-C to stop)\n`);
|
|
666
673
|
await new Promise((resolve) => {
|
|
667
674
|
process.on("SIGINT", () => {
|
|
675
|
+
closeShare?.();
|
|
668
676
|
server?.stop();
|
|
669
677
|
resolve();
|
|
670
678
|
});
|
|
671
679
|
process.on("SIGTERM", () => {
|
|
680
|
+
closeShare?.();
|
|
672
681
|
server?.stop();
|
|
673
682
|
resolve();
|
|
674
683
|
});
|
|
@@ -678,4 +687,4 @@ Options:
|
|
|
678
687
|
|
|
679
688
|
//#endregion
|
|
680
689
|
export { cmdServe };
|
|
681
|
-
//# 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/index.html
CHANGED
|
@@ -748,6 +748,7 @@
|
|
|
748
748
|
/>
|
|
749
749
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
750
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>
|
|
751
752
|
<script type="module">
|
|
752
753
|
// codehost room transport — vendored from codehost's `bun run build:lib`.
|
|
753
754
|
// Loaded as a module (deferred), so the classic script below awaits the
|
|
@@ -921,6 +922,7 @@
|
|
|
921
922
|
connect() {
|
|
922
923
|
return new Promise((resolve, reject) => {
|
|
923
924
|
const ws = new WebSocket(`wss://${this.host}/${this.room}`, [SUB]);
|
|
925
|
+
this.ws = ws; // kept so close() can drop the signaling registration too
|
|
924
926
|
let pc,
|
|
925
927
|
settled = false;
|
|
926
928
|
const fail = (e) => {
|
|
@@ -978,6 +980,7 @@
|
|
|
978
980
|
if (stream) stream(r.chunk);
|
|
979
981
|
} else if (r.t === "end") {
|
|
980
982
|
if (call) {
|
|
983
|
+
clearTimeout(call.timer);
|
|
981
984
|
this.calls.delete(r.id);
|
|
982
985
|
r.error
|
|
983
986
|
? call.reject(new Error(r.error))
|
|
@@ -988,8 +991,21 @@
|
|
|
988
991
|
req(method, path, body) {
|
|
989
992
|
const id = this.nextId++;
|
|
990
993
|
return new Promise((resolve, reject) => {
|
|
991
|
-
|
|
992
|
-
|
|
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
|
+
}
|
|
993
1009
|
});
|
|
994
1010
|
}
|
|
995
1011
|
subscribe(path, onRaw) {
|
|
@@ -1003,6 +1019,26 @@
|
|
|
1003
1019
|
} catch {}
|
|
1004
1020
|
};
|
|
1005
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
|
+
}
|
|
1006
1042
|
}
|
|
1007
1043
|
|
|
1008
1044
|
// ---- codehost rooms: a THIRD wire -----------------------------------
|
|
@@ -1310,6 +1346,11 @@
|
|
|
1310
1346
|
s.serverCount = 0;
|
|
1311
1347
|
s.byPid = new Map();
|
|
1312
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);
|
|
1313
1354
|
return [];
|
|
1314
1355
|
}
|
|
1315
1356
|
}
|
|
@@ -1537,6 +1578,19 @@
|
|
|
1537
1578
|
});
|
|
1538
1579
|
fit = new FitAddon.FitAddon();
|
|
1539
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
|
+
}
|
|
1540
1594
|
term.open(logEl);
|
|
1541
1595
|
// On a phone, auto-focusing yanks up the soft keyboard the moment you open
|
|
1542
1596
|
// an agent — even when you only meant to read the tail. Skip it there; a tap
|
|
@@ -1733,6 +1787,8 @@
|
|
|
1733
1787
|
function removeSource(room) {
|
|
1734
1788
|
const s = sources.get(room);
|
|
1735
1789
|
if (!s) return;
|
|
1790
|
+
s.removed = true; // stop any pending exp-backoff reconnect (see below)
|
|
1791
|
+
clearTimeout(s.reconnectTimer);
|
|
1736
1792
|
unsubscribeSource(s);
|
|
1737
1793
|
try {
|
|
1738
1794
|
s.client?.close?.();
|
|
@@ -1741,6 +1797,97 @@
|
|
|
1741
1797
|
sources.delete(room);
|
|
1742
1798
|
}
|
|
1743
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
|
+
|
|
1744
1891
|
// Add a room to the fleet and connect it — WITHOUT dropping the others, so
|
|
1745
1892
|
// every saved room streams its agents at once. Idempotent: a room already
|
|
1746
1893
|
// in the fleet just refreshes. The connection runs in the background; the
|
|
@@ -1775,29 +1922,13 @@
|
|
|
1775
1922
|
s.client = c;
|
|
1776
1923
|
s.tx = c;
|
|
1777
1924
|
} else {
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
if (st === "failed" || st === "closed") {
|
|
1781
|
-
s.live = false;
|
|
1782
|
-
renderRoomsIfOpen();
|
|
1783
|
-
}
|
|
1784
|
-
};
|
|
1785
|
-
await c.connect();
|
|
1786
|
-
s.client = c;
|
|
1787
|
-
s.tx = rtcTx(c);
|
|
1788
|
-
// agent-yes share (unlike codehost) sends no per-agent device id, so
|
|
1789
|
-
// every machine's rows would be unlabelled. Ask the host who it is
|
|
1790
|
-
// once and tag this room's agents with it in listSource. Best-effort:
|
|
1791
|
-
// an older host with no /api/whoami just leaves rows device-less.
|
|
1792
|
-
try {
|
|
1793
|
-
s.deviceLabel = (await s.tx.fetchJSON("/api/whoami"))?.host || "";
|
|
1794
|
-
} catch {
|
|
1795
|
-
s.deviceLabel = "";
|
|
1796
|
-
}
|
|
1925
|
+
s.token = token;
|
|
1926
|
+
await connectRtcSource(s);
|
|
1797
1927
|
}
|
|
1798
1928
|
s.live = true;
|
|
1799
1929
|
} catch (e) {
|
|
1800
1930
|
s.live = false;
|
|
1931
|
+
if (s.kind === "rtc") scheduleRtcReconnect(s);
|
|
1801
1932
|
}
|
|
1802
1933
|
s.tried = true;
|
|
1803
1934
|
renderRoomsIfOpen();
|
|
@@ -1867,7 +1998,7 @@
|
|
|
1867
1998
|
<span style="opacity:.6">Windows:</span>
|
|
1868
1999
|
<code id="cmdinstallwin" class="copy" title="click to copy">powershell -c "irm https://agent-yes.com/setup.ps1 | iex"</code></div>
|
|
1869
2000
|
<div class="rconnect">share your own fleet — run this, then open the printed link:
|
|
1870
|
-
<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>
|
|
1871
2002
|
<div class="rconnect">or view a <b>codehost</b> room — paste <code>ch:<room-token></code> (or a
|
|
1872
2003
|
codehost.dev share link) above; every machine in the room shows its agents here.</div>`;
|
|
1873
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;
|
|
@@ -828,6 +840,7 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
828
840
|
// can reach this machine peer-to-peer. The bridge calls apiFetch in-process,
|
|
829
841
|
// so without --http no port is opened at all. Bare flag mints a room; a
|
|
830
842
|
// webrtc:// value joins an explicit one.
|
|
843
|
+
let closeShare: (() => void) | undefined; // closes WebRTC peers on shutdown
|
|
831
844
|
if (wantWebrtc) {
|
|
832
845
|
const webrtcVal = (argv.webrtc ?? argv.share) as string | undefined;
|
|
833
846
|
const explicitUrl =
|
|
@@ -836,11 +849,12 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
836
849
|
const { startShare, loadOrCreateShareRoom } = await import("./share.ts");
|
|
837
850
|
// No explicit webrtc:// URL → reuse the persisted room (minted once and
|
|
838
851
|
// saved like the serve token), so the link is stable across restarts.
|
|
839
|
-
const { link } = await startShare({
|
|
852
|
+
const { link, close } = await startShare({
|
|
840
853
|
url: explicitUrl ?? (await loadOrCreateShareRoom()),
|
|
841
854
|
localFetch: apiFetch,
|
|
842
855
|
apiToken: token,
|
|
843
856
|
});
|
|
857
|
+
closeShare = close;
|
|
844
858
|
process.stdout.write(
|
|
845
859
|
`${wantHttp ? "\n" : ""}shared over WebRTC — open this link (the token is eaten from the URL on open):\n ${link}\n` +
|
|
846
860
|
(explicitUrl
|
|
@@ -857,10 +871,12 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
857
871
|
|
|
858
872
|
await new Promise<void>((resolve) => {
|
|
859
873
|
process.on("SIGINT", () => {
|
|
874
|
+
closeShare?.();
|
|
860
875
|
server?.stop();
|
|
861
876
|
resolve();
|
|
862
877
|
});
|
|
863
878
|
process.on("SIGTERM", () => {
|
|
879
|
+
closeShare?.();
|
|
864
880
|
server?.stop();
|
|
865
881
|
resolve();
|
|
866
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-CZQs_t8w.js";
|
|
2
|
-
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./versionChecker-C1W2s4ZP.js";
|
|
4
|
-
import "./pidStore-DBjlqzo8.js";
|
|
5
|
-
import "./globalPidIndex-yVd3mbsV.js";
|
|
6
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS--15XPtGd.js";
|
|
7
|
-
|
|
8
|
-
export { SUPPORTED_CLIS };
|