agent-yes 1.127.1 → 1.128.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.
@@ -0,0 +1,8 @@
1
+ import "./ts-blmSvxN5.js";
2
+ import "./logger-B9h0djqx.js";
3
+ import "./versionChecker-BmttWH-a.js";
4
+ import "./pidStore-CGKIhaJO.js";
5
+ import "./globalPidIndex-C7r2m6s7.js";
6
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-C3hbWNlY.js";
7
+
8
+ export { SUPPORTED_CLIS };
@@ -1,8 +1,8 @@
1
- import { t as CLIS_CONFIG } from "./ts-ShVdzMAQ.js";
1
+ import { t as CLIS_CONFIG } from "./ts-blmSvxN5.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-BZWx2SIK.js.map
8
+ //# sourceMappingURL=SUPPORTED_CLIS-C3hbWNlY.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-DzMmzvnT.js";
3
+ import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-BmttWH-a.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-DVAUjLPL.js");
485
+ const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-DA0MhdpJ.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-Bul6osAx.js");
518
+ const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-9oihdzOp.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-ShVdzMAQ.js";
1
+ import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-blmSvxN5.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import "./versionChecker-DzMmzvnT.js";
3
+ import "./versionChecker-BmttWH-a.js";
4
4
  import "./pidStore-CGKIhaJO.js";
5
5
  import "./globalPidIndex-C7r2m6s7.js";
6
6
 
@@ -1,9 +1,9 @@
1
- import "./ts-ShVdzMAQ.js";
1
+ import "./ts-blmSvxN5.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import "./versionChecker-DzMmzvnT.js";
3
+ import "./versionChecker-BmttWH-a.js";
4
4
  import "./pidStore-CGKIhaJO.js";
5
5
  import "./globalPidIndex-C7r2m6s7.js";
6
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-BZWx2SIK.js";
6
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-C3hbWNlY.js";
7
7
  import { n as resolveSpawnCwd } from "./workspaceConfig-BJO4fzEn.js";
8
8
  import { createHash } from "node:crypto";
9
9
 
@@ -141,4 +141,4 @@ async function cmdSchedule(rest) {
141
141
 
142
142
  //#endregion
143
143
  export { cmdSchedule };
144
- //# sourceMappingURL=schedule-BLbUSihu.js.map
144
+ //# sourceMappingURL=schedule-CNhFqdnP.js.map
@@ -1,13 +1,13 @@
1
- import "./ts-ShVdzMAQ.js";
1
+ import "./ts-blmSvxN5.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import { r as getInstalledPackage } from "./versionChecker-DzMmzvnT.js";
3
+ import { r as getInstalledPackage } from "./versionChecker-BmttWH-a.js";
4
4
  import "./pidStore-CGKIhaJO.js";
5
5
  import { a as updateGlobalPidStatus } from "./globalPidIndex-C7r2m6s7.js";
6
6
  import { t as pgidForWrapper } from "./reaper-BkjPN7mw.js";
7
7
  import "./configShared-C5QaNPnz.js";
8
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-BZWx2SIK.js";
8
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-C3hbWNlY.js";
9
9
  import "./remotes-D8GvSbhf.js";
10
- import { f as readNotes, g as snapshotStatus, m as resolveOne, o as extractTaskCounts, p as renderRawLog, r as controlCodeFromName, u as listRecords, v as writeToIpc } from "./subcommands-BBsfLrTZ.js";
10
+ import { f as readNotes, g as snapshotStatus, m as resolveOne, o as extractTaskCounts, p as renderRawLog, r as controlCodeFromName, u as listRecords, v as writeToIpc } from "./subcommands-7ZNa2rCG.js";
11
11
  import yargs from "yargs";
12
12
  import { mkdir, open, readFile, stat, writeFile } from "fs/promises";
13
13
  import { homedir, hostname, userInfo } from "os";
@@ -208,6 +208,18 @@ function portFromArgs(args) {
208
208
  const m = /--port[=\s](\d+)/.exec(args.join(" "));
209
209
  return m ? Number(m[1]) : DEFAULT_PORT;
210
210
  }
211
+ function explicitWebrtcUrl(args) {
212
+ for (let i = 0; i < args.length; i++) {
213
+ const a = args[i];
214
+ for (const flag of ["--webrtc", "--share"]) {
215
+ if (a === flag && args[i + 1]?.startsWith("webrtc://")) return args[i + 1];
216
+ if (a.startsWith(`${flag}=`)) {
217
+ const v = a.slice(flag.length + 1);
218
+ if (v.startsWith("webrtc://")) return v;
219
+ }
220
+ }
221
+ }
222
+ }
211
223
  async function fetchDaemonVersion(port, token) {
212
224
  try {
213
225
  const r = await fetch(`http://127.0.0.1:${port}/api/version`, {
@@ -231,14 +243,30 @@ async function cmdServeDaemon(sub, args) {
231
243
  const priorArgs = await readDaemonServeArgs(mgr);
232
244
  const effArgs = args.length ? args : priorArgs ?? [];
233
245
  const current = getInstalledPackage().version;
246
+ const webrtcDaemon = effArgs.some((a) => a.startsWith("--webrtc") || a.startsWith("--share"));
247
+ let shareLink = null;
248
+ let shareLinkMinted = false;
249
+ if (webrtcDaemon) try {
250
+ const { loadOrCreateShareRoom, shareLinkFromRoomUrl } = await import("./share-Cvb0PBKg.js");
251
+ const explicit = explicitWebrtcUrl(effArgs);
252
+ shareLink = shareLinkFromRoomUrl(explicit ?? await loadOrCreateShareRoom());
253
+ shareLinkMinted = !explicit;
254
+ } catch {}
255
+ const emitShareLink = () => {
256
+ if (!webrtcDaemon) return;
257
+ if (shareLink) process.stdout.write(`\nshared over WebRTC — open this link (the token is eaten from the URL on open):\n ${shareLink}\n` + (shareLinkMinted ? ` (persistent room — same link across restarts; delete ~/.agent-yes/.share-room to rotate)\n` : ``));
258
+ else process.stdout.write("\nthe WebRTC share link carries a secret, so the daemon does NOT log it —\nread it from ~/.agent-yes/.share-link (mode 0600). The room persists in\n~/.agent-yes/.share-room, so the link survives restarts.\n");
259
+ };
234
260
  if (priorArgs !== null) {
261
+ const sameConfig = JSON.stringify(effArgs) === JSON.stringify(priorArgs);
235
262
  const runningVer = await fetchDaemonVersion(portFromArgs(effArgs), token);
236
- if (runningVer === current) {
263
+ if (runningVer === current && sameConfig) {
237
264
  await ensureBootAutostart(mgr);
238
265
  process.stdout.write(`'${DAEMON_NAME}' already running v${current} (up to date)\n`);
266
+ emitShareLink();
239
267
  return 0;
240
268
  }
241
- process.stdout.write(`rolling '${DAEMON_NAME}' ${runningVer ? `v${runningVer}` : "(unknown)"} → v${current}…\n`);
269
+ process.stdout.write(runningVer === current ? `reconfiguring '${DAEMON_NAME}' (serve args changed)…\n` : `rolling '${DAEMON_NAME}' ${runningVer ? `v${runningVer}` : "(unknown)"} → v${current}…\n`);
242
270
  await spawnExit([
243
271
  mgr.bin,
244
272
  "stop",
@@ -251,7 +279,7 @@ async function cmdServeDaemon(sub, args) {
251
279
  ]);
252
280
  }
253
281
  const serveArgv = ayServeArgv(effArgs);
254
- const oxmgrHealth = effArgs.some((a) => a.startsWith("--webrtc") || a.startsWith("--share")) && mgr.id === "oxmgr" ? [
282
+ const oxmgrHealth = webrtcDaemon && mgr.id === "oxmgr" ? [
255
283
  "--health-cmd",
256
284
  ayServeArgv(["healthcheck"]).join(" "),
257
285
  "--health-interval",
@@ -291,7 +319,6 @@ async function cmdServeDaemon(sub, args) {
291
319
  if (code === 0) {
292
320
  const onBoot = await ensureBootAutostart(mgr);
293
321
  const port = portFromArgs(effArgs);
294
- const webrtcish = effArgs.some((a) => a.startsWith("--webrtc") || a.startsWith("--share"));
295
322
  const httpish = effArgs.some((a) => a.startsWith("--http") || a.startsWith("--share")) || !effArgs.some((a) => a.startsWith("--webrtc"));
296
323
  process.stdout.write(`\n${priorArgs !== null ? `rolled '${DAEMON_NAME}' forward to` : `installed '${DAEMON_NAME}' as a daemon via ${mgr.id} —`} v${current}\n`);
297
324
  if (mgr.id === "oxmgr") process.stdout.write(onBoot ? `start-on-boot: enabled (systemd --user + linger, starts at boot)\n` : `start-on-boot: not registered — needs a user systemd session; run \`oxmgr service install\` to enable\n`);
@@ -304,7 +331,7 @@ async function cmdServeDaemon(sub, args) {
304
331
  }
305
332
  process.stdout.write(` ay serve logs # view server logs\n`);
306
333
  process.stdout.write(` ay serve uninstall # remove daemon\n`);
307
- if (webrtcish) process.stdout.write("\nthe WebRTC share link carries a secret, so the daemon does NOT log it —\nread it from ~/.agent-yes/.share-link (mode 0600). The room persists in\n~/.agent-yes/.share-room, so the link survives restarts.\n");
334
+ emitShareLink();
308
335
  }
309
336
  return code ?? 1;
310
337
  }
@@ -1052,7 +1079,7 @@ Options:
1052
1079
  const webrtcVal = argv.webrtc ?? argv.share;
1053
1080
  const explicitUrl = typeof webrtcVal === "string" && webrtcVal.startsWith("webrtc://") ? webrtcVal : void 0;
1054
1081
  try {
1055
- const { startShare, loadOrCreateShareRoom } = await import("./share-YuM6-Q6A.js");
1082
+ const { startShare, loadOrCreateShareRoom } = await import("./share-Cvb0PBKg.js");
1056
1083
  const linkFile = path.join(process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes"), ".share-link");
1057
1084
  const announce = async (room, link, rotated) => {
1058
1085
  const lead = rotated ? "the room was rejected by signaling (stale generation) — rotated to a fresh link" : "shared over WebRTC — open this link (the token is eaten from the URL on open)";
@@ -1107,4 +1134,4 @@ Options:
1107
1134
 
1108
1135
  //#endregion
1109
1136
  export { cmdServe };
1110
- //# sourceMappingURL=serve-vOIzBN5C.js.map
1137
+ //# sourceMappingURL=serve-CgHUyl6K.js.map
@@ -32,7 +32,7 @@ async function cmdSetup(rest) {
32
32
  if (!existsSync(abs)) process.stderr.write(` note: that directory doesn't exist yet — create it, or agents spawned there will fail\n`);
33
33
  if (noShare) return 0;
34
34
  process.stdout.write(`\nsharing this machine to agent-yes.com…\n`);
35
- const { cmdServe } = await import("./serve-vOIzBN5C.js");
35
+ const { cmdServe } = await import("./serve-CgHUyl6K.js");
36
36
  return cmdServe([
37
37
  "install",
38
38
  "--share",
@@ -42,4 +42,4 @@ async function cmdSetup(rest) {
42
42
 
43
43
  //#endregion
44
44
  export { cmdSetup };
45
- //# sourceMappingURL=setup-BQUtWMgU.js.map
45
+ //# sourceMappingURL=setup-Bituq_if.js.map
@@ -244,6 +244,15 @@ function parseShareUrl(s) {
244
244
  host: m[3]
245
245
  };
246
246
  }
247
+ function formatShareLink(room, S, host) {
248
+ return `${host === "s.agent-yes.com" ? "https://agent-yes.com/w" : "http://localhost:7778/w"}/#${room}:${MARKER}${S}${host === "s.agent-yes.com" ? "" : "@" + host}`;
249
+ }
250
+ function shareLinkFromRoomUrl(url) {
251
+ const { room, token, host } = parseShareUrl(url);
252
+ const { s, v2 } = parseSecret(token);
253
+ if (!v2) throw new Error("refusing to derive a link for an unencrypted room — delete ~/.agent-yes/.share-room to rotate to an encrypted link");
254
+ return formatShareLink(room, s, host);
255
+ }
247
256
  async function linkFromBunCache() {
248
257
  const { existsSync, symlinkSync, mkdirSync, readdirSync } = await import("fs");
249
258
  const path = (await import("path")).default;
@@ -320,9 +329,7 @@ async function startShare(opts) {
320
329
  if (!v2) throw new Error("refusing to host an unencrypted room — delete ~/.agent-yes/.share-room to rotate to an encrypted link");
321
330
  let S = firstS;
322
331
  const wsScheme = host.startsWith("localhost") || host.startsWith("127.") ? "ws" : "wss";
323
- const ui = host === "s.agent-yes.com" ? "https://agent-yes.com/w" : "http://localhost:7778/w";
324
- const suffix = host === "s.agent-yes.com" ? "" : "@" + host;
325
- const mkLink = () => `${ui}/#${room}:${MARKER}${S}${suffix}`;
332
+ const mkLink = () => formatShareLink(room, S, host);
326
333
  let authToken = await deriveAuthToken(S, room, host);
327
334
  let link = mkLink();
328
335
  const RTCPeerConnection = await importRTC();
@@ -698,5 +705,5 @@ async function startShare(opts) {
698
705
  }
699
706
 
700
707
  //#endregion
701
- export { loadOrCreateShareRoom, startShare };
702
- //# sourceMappingURL=share-YuM6-Q6A.js.map
708
+ export { loadOrCreateShareRoom, shareLinkFromRoomUrl, startShare };
709
+ //# sourceMappingURL=share-Cvb0PBKg.js.map
@@ -524,15 +524,15 @@ async function runSubcommand(argv) {
524
524
  case "restart": return await cmdRestart(rest);
525
525
  case "note": return await cmdNote(rest);
526
526
  case "serve": {
527
- const { cmdServe } = await import("./serve-vOIzBN5C.js");
527
+ const { cmdServe } = await import("./serve-CgHUyl6K.js");
528
528
  return cmdServe(rest);
529
529
  }
530
530
  case "setup": {
531
- const { cmdSetup } = await import("./setup-BQUtWMgU.js");
531
+ const { cmdSetup } = await import("./setup-Bituq_if.js");
532
532
  return cmdSetup(rest);
533
533
  }
534
534
  case "schedule": {
535
- const { cmdSchedule } = await import("./schedule-BLbUSihu.js");
535
+ const { cmdSchedule } = await import("./schedule-CNhFqdnP.js");
536
536
  return cmdSchedule(rest);
537
537
  }
538
538
  case "remote": {
@@ -2304,4 +2304,4 @@ async function cmdResultSet(rest) {
2304
2304
 
2305
2305
  //#endregion
2306
2306
  export { stopTipForCli as _, extractNeedsInput as a, isPidAlive as c, matchKeyword as d, readNotes as f, snapshotStatus as g, runSubcommand as h, cursorAbs as i, isSubcommand as l, resolveOne as m, cmdHelp as n, extractTaskCounts as o, renderRawLog as p, controlCodeFromName as r, finalizedLines as s, GRACEFUL_EXIT_COMMANDS as t, listRecords as u, writeToIpc as v };
2307
- //# sourceMappingURL=subcommands-BBsfLrTZ.js.map
2307
+ //# sourceMappingURL=subcommands-7ZNa2rCG.js.map
@@ -2,6 +2,6 @@ import "./logger-B9h0djqx.js";
2
2
  import "./globalPidIndex-C7r2m6s7.js";
3
3
  import "./configShared-C5QaNPnz.js";
4
4
  import "./remotes-D8GvSbhf.js";
5
- import { _ as stopTipForCli, a as extractNeedsInput, c as isPidAlive, d as matchKeyword, f as readNotes, g as snapshotStatus, h as runSubcommand, i as cursorAbs, l as isSubcommand, m as resolveOne, n as cmdHelp, o as extractTaskCounts, p as renderRawLog, r as controlCodeFromName, s as finalizedLines, t as GRACEFUL_EXIT_COMMANDS, u as listRecords, v as writeToIpc } from "./subcommands-BBsfLrTZ.js";
5
+ import { _ as stopTipForCli, a as extractNeedsInput, c as isPidAlive, d as matchKeyword, f as readNotes, g as snapshotStatus, h as runSubcommand, i as cursorAbs, l as isSubcommand, m as resolveOne, n as cmdHelp, o as extractTaskCounts, p as renderRawLog, r as controlCodeFromName, s as finalizedLines, t as GRACEFUL_EXIT_COMMANDS, u as listRecords, v as writeToIpc } from "./subcommands-7ZNa2rCG.js";
6
6
 
7
7
  export { cmdHelp, isSubcommand, runSubcommand };
@@ -1,5 +1,5 @@
1
1
  import { n as logger, t as addTransport } from "./logger-B9h0djqx.js";
2
- import { r as getInstalledPackage } from "./versionChecker-DzMmzvnT.js";
2
+ import { r as getInstalledPackage } from "./versionChecker-BmttWH-a.js";
3
3
  import { t as agentYesHome } from "./agentYesHome-BvaUOzCV.js";
4
4
  import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-CJxsoGdb.js";
5
5
  import { t as PidStore } from "./pidStore-CGKIhaJO.js";
@@ -1787,4 +1787,4 @@ function sleep(ms) {
1787
1787
 
1788
1788
  //#endregion
1789
1789
  export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
1790
- //# sourceMappingURL=ts-ShVdzMAQ.js.map
1790
+ //# sourceMappingURL=ts-blmSvxN5.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.127.1";
10
+ var version = "1.128.0";
11
11
 
12
12
  //#endregion
13
13
  //#region ts/versionChecker.ts
@@ -215,4 +215,4 @@ async function displayVersion() {
215
215
 
216
216
  //#endregion
217
217
  export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
218
- //# sourceMappingURL=versionChecker-DzMmzvnT.js.map
218
+ //# sourceMappingURL=versionChecker-BmttWH-a.js.map
package/lab/ui/index.html CHANGED
@@ -1368,6 +1368,13 @@
1368
1368
  let es = null; // live-tail subscription closer
1369
1369
  let term = null; // xterm.js Terminal rendering the raw PTY stream
1370
1370
  let fit = null;
1371
+ // Re-fit the open terminal and re-assert its size to the agent's PTY. Set per
1372
+ // selection in select(); invoked on open AND whenever the tab is re-activated.
1373
+ // A backgrounded tab can miss window resizes, and another viewer may have
1374
+ // resized the shared PTY while we were away — either leaves our stream
1375
+ // rendering at the wrong width/height until something nudges it. null when no
1376
+ // agent is open.
1377
+ let resyncTerm = null;
1371
1378
  // Terminal font size (px) — adjustable from the ⋯ menu, persisted across
1372
1379
  // reloads, applied live to the open terminal and used by every new one.
1373
1380
  let termFontSize = 12;
@@ -2947,9 +2954,17 @@
2947
2954
  }
2948
2955
  suppressPush = false;
2949
2956
  };
2950
- tx.fetchJSON("/api/size/" + encodeURIComponent(pid))
2951
- .then(fitAndSync)
2952
- .catch(() => fitAndSync(null));
2957
+ // Fit + sync now, and on every later tab re-activation: re-measure our pane
2958
+ // and re-assert our size to the agent so the stream always reflows to OUR
2959
+ // viewport (see the visibilitychange hook in startPolling). Push-if-different
2960
+ // inside fitAndSync keeps an already-matching agent from a redundant SIGWINCH.
2961
+ resyncTerm = () => {
2962
+ if (sel !== selKey || !term) return;
2963
+ tx.fetchJSON("/api/size/" + encodeURIComponent(pid))
2964
+ .then(fitAndSync)
2965
+ .catch(() => fitAndSync(null));
2966
+ };
2967
+ resyncTerm();
2953
2968
 
2954
2969
  // True live tail via ay serve's SSE stream. First event is an xterm-rendered
2955
2970
  // tail snapshot; later events are incremental deltas. We normalise terminal
@@ -3719,6 +3734,11 @@
3719
3734
  if (document.visibilityState === "visible") {
3720
3735
  lastActivity = Date.now();
3721
3736
  loadList();
3737
+ // Re-assert our viewport size to the open agent: while we were away the
3738
+ // window may have resized (a hidden tab can miss it) or another viewer
3739
+ // may have resized the shared PTY, so the stream could be rendering at
3740
+ // the wrong width/height. fit + push-if-different brings it back.
3741
+ resyncTerm?.();
3722
3742
  }
3723
3743
  });
3724
3744
  watchVersion();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.127.1",
3
+ "version": "1.128.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",
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  import { describe, expect, it } from "vitest";
2
3
  import { buildStoredResult, normalizeEnvelope, resultPath, resultsDir } from "./resultEnvelope.ts";
3
4
 
@@ -38,6 +39,8 @@ describe("buildStoredResult", () => {
38
39
 
39
40
  describe("resultPath", () => {
40
41
  it("derives a per-pid path under the results dir", () => {
41
- expect(resultPath(777)).toBe(`${resultsDir()}/777.json`);
42
+ // Use path.join (not a hardcoded "/") so the expectation matches the
43
+ // platform separator — resultPath itself joins, so on Windows it's "\".
44
+ expect(resultPath(777)).toBe(path.join(resultsDir(), "777.json"));
42
45
  });
43
46
  });
package/ts/serve.ts CHANGED
@@ -276,6 +276,24 @@ function portFromArgs(args: string[]): number {
276
276
  return m ? Number(m[1]) : DEFAULT_PORT;
277
277
  }
278
278
 
279
+ // An explicit webrtc:// URL passed to --webrtc/--share in the daemon's serve args,
280
+ // or undefined for a bare flag (which mints a persisted room instead). Mirrors how
281
+ // cmdServe resolves argv.webrtc/argv.share, but over the raw arg list install holds
282
+ // (oxmgr splits the command on whitespace → `--webrtc url`; pm2/`=` → `--webrtc=url`).
283
+ function explicitWebrtcUrl(args: string[]): string | undefined {
284
+ for (let i = 0; i < args.length; i++) {
285
+ const a = args[i]!;
286
+ for (const flag of ["--webrtc", "--share"]) {
287
+ if (a === flag && args[i + 1]?.startsWith("webrtc://")) return args[i + 1];
288
+ if (a.startsWith(`${flag}=`)) {
289
+ const v = a.slice(flag.length + 1);
290
+ if (v.startsWith("webrtc://")) return v;
291
+ }
292
+ }
293
+ }
294
+ return undefined;
295
+ }
296
+
279
297
  // Ask the live daemon its version over the local HTTP API. null if it's not
280
298
  // listening (webrtc-only) or too old to expose /api/version — both of which we
281
299
  // treat as "outdated" so a re-install rolls it forward.
@@ -314,19 +332,68 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
314
332
  const effArgs = args.length ? args : (priorArgs ?? []);
315
333
  const current = getInstalledPackage().version;
316
334
 
335
+ // WebRTC daemon: resolve the share link up front so we can print it on every
336
+ // install path (fresh install, roll-forward, and the already-up-to-date no-op).
337
+ // The link is a pure transform of the room URL, so the foreground install
338
+ // command can show it even though the background daemon is what runs the bridge.
339
+ // Resolve (and persist, when auto-minting) the room BEFORE spawning, so the
340
+ // daemon reads the SAME ~/.agent-yes/.share-room and can't race us into minting
341
+ // a divergent one. We print the link directly — the install receipt already
342
+ // prints the bearer token, so the operator's terminal is the right trust scope
343
+ // for a secret-bearing link (unlike the daemon's persisted logs, which omit it).
344
+ const webrtcDaemon = effArgs.some((a) => a.startsWith("--webrtc") || a.startsWith("--share"));
345
+ let shareLink: string | null = null;
346
+ let shareLinkMinted = false; // auto-minted (persisted/rotatable) vs explicit URL
347
+ if (webrtcDaemon) {
348
+ try {
349
+ const { loadOrCreateShareRoom, shareLinkFromRoomUrl } = await import("./share.ts");
350
+ const explicit = explicitWebrtcUrl(effArgs);
351
+ shareLink = shareLinkFromRoomUrl(explicit ?? (await loadOrCreateShareRoom()));
352
+ shareLinkMinted = !explicit;
353
+ } catch {
354
+ /* best effort — fall back to the .share-link file hint in emitShareLink */
355
+ }
356
+ }
357
+ const emitShareLink = () => {
358
+ if (!webrtcDaemon) return;
359
+ if (shareLink)
360
+ process.stdout.write(
361
+ `\nshared over WebRTC — open this link (the token is eaten from the URL on open):\n` +
362
+ ` ${shareLink}\n` +
363
+ (shareLinkMinted
364
+ ? ` (persistent room — same link across restarts; delete ~/.agent-yes/.share-room to rotate)\n`
365
+ : ``),
366
+ );
367
+ else
368
+ process.stdout.write(
369
+ `\nthe WebRTC share link carries a secret, so the daemon does NOT log it —\n` +
370
+ `read it from ~/.agent-yes/.share-link (mode 0600). The room persists in\n` +
371
+ `~/.agent-yes/.share-room, so the link survives restarts.\n`,
372
+ );
373
+ };
374
+
317
375
  if (priorArgs !== null) {
318
- // A daemon already exists only disturb it if it's actually outdated.
376
+ // A daemon already exists. Treat this as a no-op only when it's BOTH current
377
+ // AND already running the requested config — otherwise a config change (e.g.
378
+ // `install --webrtc` over an --http daemon, which the version probe still
379
+ // reaches on the default port) would be silently ignored, and we'd print a
380
+ // share link for a WebRTC bridge that isn't actually running. A bare re-run
381
+ // passes no args, so effArgs === priorArgs and this stays a no-op as before.
382
+ const sameConfig = JSON.stringify(effArgs) === JSON.stringify(priorArgs);
319
383
  const runningVer = await fetchDaemonVersion(portFromArgs(effArgs), token);
320
- if (runningVer === current) {
384
+ if (runningVer === current && sameConfig) {
321
385
  await ensureBootAutostart(mgr);
322
386
  process.stdout.write(`'${DAEMON_NAME}' already running v${current} (up to date)\n`);
387
+ emitShareLink();
323
388
  return 0;
324
389
  }
325
- // Outdated (or unreachable/too-old to report) → graceful roll-forward.
326
- // `stop` sends SIGTERM, which cmdServe handles cleanly (closing share
327
- // peers so browsers reconnect fast), then we re-create with the new binary.
390
+ // Outdated, unreachable, or reconfigured → graceful roll-forward. `stop` sends
391
+ // SIGTERM, which cmdServe handles cleanly (closing share peers so browsers
392
+ // reconnect fast), then we re-create with the new binary/args.
328
393
  process.stdout.write(
329
- `rolling '${DAEMON_NAME}' ${runningVer ? `v${runningVer}` : "(unknown)"} → v${current}…\n`,
394
+ runningVer === current
395
+ ? `reconfiguring '${DAEMON_NAME}' (serve args changed)…\n`
396
+ : `rolling '${DAEMON_NAME}' ${runningVer ? `v${runningVer}` : "(unknown)"} → v${current}…\n`,
330
397
  );
331
398
  await spawnExit([mgr.bin, "stop", DAEMON_NAME]);
332
399
  await spawnExit([mgr.bin, "delete", DAEMON_NAME]);
@@ -340,7 +407,7 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
340
407
  // freeze the JS event loop (host answers nobody, no in-process timer can
341
408
  // recover it), so an EXTERNAL probe of the serve heartbeat is the only thing
342
409
  // that can detect+restart it. 15s stale + 3 misses at 10s ≈ 45s to auto-recover.
343
- const webrtcDaemon = effArgs.some((a) => a.startsWith("--webrtc") || a.startsWith("--share"));
410
+ // (webrtcDaemon resolved above, where we also derive the share link.)
344
411
  const oxmgrHealth =
345
412
  webrtcDaemon && mgr.id === "oxmgr"
346
413
  ? [
@@ -389,7 +456,6 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
389
456
  const onBoot = await ensureBootAutostart(mgr);
390
457
  const port = portFromArgs(effArgs);
391
458
  // Mirror cmdServe's mode resolution: webrtc-only daemons open no HTTP port.
392
- const webrtcish = effArgs.some((a) => a.startsWith("--webrtc") || a.startsWith("--share"));
393
459
  const httpish =
394
460
  effArgs.some((a) => a.startsWith("--http") || a.startsWith("--share")) ||
395
461
  !effArgs.some((a) => a.startsWith("--webrtc"));
@@ -421,13 +487,7 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
421
487
  }
422
488
  process.stdout.write(` ay serve logs # view server logs\n`);
423
489
  process.stdout.write(` ay serve uninstall # remove daemon\n`);
424
- if (webrtcish) {
425
- process.stdout.write(
426
- `\nthe WebRTC share link carries a secret, so the daemon does NOT log it —\n` +
427
- `read it from ~/.agent-yes/.share-link (mode 0600). The room persists in\n` +
428
- `~/.agent-yes/.share-room, so the link survives restarts.\n`,
429
- );
430
- }
490
+ emitShareLink();
431
491
  }
432
492
  return code ?? 1;
433
493
  }
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { shareLinkFromRoomUrl } from "./share.ts";
3
+ import { MARKER } from "../lab/ui/e2e.js";
4
+
5
+ const S = "a".repeat(64); // a valid 64-hex room secret
6
+ const TOK = `${MARKER}${S}`; // encrypted-room token (v2)
7
+
8
+ // shareLinkFromRoomUrl turns a persisted/explicit webrtc://room:token@host room
9
+ // into the browser console link `ay serve install` prints — it MUST match the
10
+ // link startShare announces from the same room (both go through formatShareLink).
11
+ describe("shareLinkFromRoomUrl", () => {
12
+ it("derives the prod console link (no host suffix; secret rides in the fragment)", () => {
13
+ const link = shareLinkFromRoomUrl(`webrtc://r1a2b3c:${TOK}@s.agent-yes.com`);
14
+ expect(link).toBe(`https://agent-yes.com/w/#r1a2b3c:${MARKER}${S}`);
15
+ });
16
+
17
+ it("derives a dev/self-hosted link carrying the signaling host in the fragment", () => {
18
+ const link = shareLinkFromRoomUrl(`webrtc://r1a2b3c:${TOK}@localhost:7778`);
19
+ expect(link).toBe(`http://localhost:7778/w/#r1a2b3c:${MARKER}${S}@localhost:7778`);
20
+ });
21
+
22
+ it("round-trips room + token in the fragment the browser splits back out", () => {
23
+ const link = shareLinkFromRoomUrl(`webrtc://room0:${TOK}@s.agent-yes.com`);
24
+ expect(link.split("#")[1]).toBe(`room0:${TOK}`);
25
+ });
26
+
27
+ it("refuses a legacy (unencrypted) room — operator must rotate to an encrypted link", () => {
28
+ expect(() => shareLinkFromRoomUrl(`webrtc://room0:${S}@s.agent-yes.com`)).toThrow(
29
+ /unencrypted/,
30
+ );
31
+ });
32
+
33
+ it("rejects a malformed room url", () => {
34
+ expect(() => shareLinkFromRoomUrl("not-a-webrtc-url")).toThrow(/webrtc:\/\//);
35
+ });
36
+ });
package/ts/share.ts CHANGED
@@ -160,6 +160,33 @@ function parseShareUrl(s: string): { room: string; token: string; host: string }
160
160
  return { room: m[1]!, token: m[2]!, host: m[3]! };
161
161
  }
162
162
 
163
+ // The browser console URL for a room — what the operator opens to reach the host.
164
+ // Pure function of (room, secret S, signaling host): S rides in the URL fragment
165
+ // (never sent to any server) and is eaten by the page on open. Single source of
166
+ // truth for the link format, shared by startShare's live announce and by
167
+ // shareLinkFromRoomUrl (so `ay serve install` prints the exact link the daemon serves).
168
+ function formatShareLink(room: string, S: string, host: string): string {
169
+ // The console web-app is served under /w/ (landing page lives at /). A non-prod
170
+ // signaling host targets the local dev UI and carries the host in the fragment.
171
+ const ui = host === "s.agent-yes.com" ? "https://agent-yes.com/w" : "http://localhost:7778/w";
172
+ const suffix = host === "s.agent-yes.com" ? "" : "@" + host;
173
+ return `${ui}/#${room}:${MARKER}${S}${suffix}`;
174
+ }
175
+
176
+ // Derive the shareable console link from a persisted/explicit webrtc://room:token@host
177
+ // URL WITHOUT starting a bridge, so `ay serve install` can print the same link the
178
+ // background daemon will serve. Throws on a legacy (unencrypted) room, mirroring
179
+ // startShare's refusal to host one.
180
+ export function shareLinkFromRoomUrl(url: string): string {
181
+ const { room, token, host } = parseShareUrl(url);
182
+ const { s, v2 } = parseSecret(token);
183
+ if (!v2)
184
+ throw new Error(
185
+ "refusing to derive a link for an unencrypted room — delete ~/.agent-yes/.share-room to rotate to an encrypted link",
186
+ );
187
+ return formatShareLink(room, s, host);
188
+ }
189
+
163
190
  // node-datachannel ships a native addon. Under Bun the module sometimes resolves
164
191
  // from the global cache where the prebuilt .node isn't linked; this best-effort
165
192
  // shim symlinks it in before we import. In a normal npm/bunx install the binary
@@ -280,10 +307,7 @@ export async function startShare(
280
307
  let S = firstS;
281
308
 
282
309
  const wsScheme = host.startsWith("localhost") || host.startsWith("127.") ? "ws" : "wss";
283
- // The console web-app is served under /w/ (landing page lives at /).
284
- const ui = host === "s.agent-yes.com" ? "https://agent-yes.com/w" : "http://localhost:7778/w";
285
- const suffix = host === "s.agent-yes.com" ? "" : "@" + host;
286
- const mkLink = () => `${ui}/#${room}:${MARKER}${S}${suffix}`;
310
+ const mkLink = () => formatShareLink(room, S, host);
287
311
  let authToken = await deriveAuthToken(S, room, host);
288
312
  let link = mkLink();
289
313
 
@@ -1,8 +0,0 @@
1
- import "./ts-ShVdzMAQ.js";
2
- import "./logger-B9h0djqx.js";
3
- import "./versionChecker-DzMmzvnT.js";
4
- import "./pidStore-CGKIhaJO.js";
5
- import "./globalPidIndex-C7r2m6s7.js";
6
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-BZWx2SIK.js";
7
-
8
- export { SUPPORTED_CLIS };