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 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-CZQs_t8w.js";
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--15XPtGd.js.map
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-C1W2s4ZP.js";
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-8hMGFqkQ.js");
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-DE31jVYi.js");
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-CZQs_t8w.js";
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-C1W2s4ZP.js";
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-CZQs_t8w.js";
1
+ import "./ts-B62nAAfY.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import "./versionChecker-C1W2s4ZP.js";
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--15XPtGd.js";
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-Di_pcW0_.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-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-BsCeIfQM.js");
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-DEDkRdog.js.map
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 === "open") dc.send(JSON.stringify(obj));
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-BsCeIfQM.js.map
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-Di_pcW0_.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-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-DEDkRdog.js");
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-Di_pcW0_.js.map
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-C1W2s4ZP.js";
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-CZQs_t8w.js.map
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.114.0";
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-C1W2s4ZP.js.map
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
- this.calls.set(id, { status: 0, body: "", resolve, reject });
992
- this.dc.send(JSON.stringify({ t: "req", id, method, path, body }));
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
- const c = new RTCClient(host, room, token);
1779
- c.onstate = (st) => {
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:&lt;room-token&gt;</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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.114.0",
3
+ "version": "1.115.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",
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(opts: ShareOpts): Promise<{ room: string; link: string }> {
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
- if (dc.readyState === "open") dc.send(JSON.stringify(obj));
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
- return { room, link };
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 };