agent-yes 1.204.0 → 1.206.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.
@@ -1,14 +1,15 @@
1
- import "./ts-D9LzuGpU.js";
1
+ import "./ts-DnqRreF4.js";
2
2
  import "./logger-CDIsZ-Pp.js";
3
- import { r as getInstalledPackage } from "./versionChecker-C8ScoMqV.js";
4
- import { t as findSpawnHiddenLauncher } from "./rustBinary-DMx8ljD0.js";
3
+ import { r as getInstalledPackage } from "./versionChecker-d5c6_vgS.js";
4
+ import { t as findSpawnHiddenLauncher } from "./rustBinary-BjRvvEs8.js";
5
+ import { t as agentYesHome$1 } from "./agentYesHome-CtHb5b71.js";
5
6
  import "./pidStore-BIvsBQ8X.js";
6
7
  import { a as updateGlobalPidStatus } from "./globalPidIndex-CoNr7tS8.js";
7
8
  import { r as recordInbox } from "./messageLog-CxrKJj77.js";
8
9
  import { t as pgidForWrapper } from "./reaper-BUHCyxdF.js";
9
10
  import "./configShared-aKTg-sa5.js";
10
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DqsPHFU9.js";
11
- import { A as renderRawLog, D as recentMessageEdges, E as readPtysize, L as snapshotStatus, M as resolveOne, O as recentReadEdges, S as listRecords, T as readNotes, U as writeToIpc, b as isUserTyping, et as TYPING_BADGE, f as extractNeedsInput, k as renderLogTailLines, l as deriveLiveStatus, o as controlCodeFromName, p as extractTaskCounts, u as extractBadges } from "./subcommands-B7NvTmdk.js";
11
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DQSgURGW.js";
12
+ import { A as renderRawLog, D as recentMessageEdges, E as readPtysize, L as snapshotStatus, M as resolveOne, O as recentReadEdges, S as listRecords, T as readNotes, U as writeToIpc, b as isUserTyping, f as extractNeedsInput, k as renderLogTailLines, l as deriveLiveStatus, o as controlCodeFromName, p as extractTaskCounts, q as isTransientLockMkdirError, tt as TYPING_BADGE, u as extractBadges } from "./subcommands-DkrMRVNh.js";
12
13
  import "./e2e-jb0Hp43q.js";
13
14
  import "./webrtcLink-B7REGtK2.js";
14
15
  import "./remotes-Cim0dBU7.js";
@@ -16,7 +17,7 @@ import { a as getSpawnHook, c as hasProvisionHook, d as resolveSpawnCwd, i as ge
16
17
  import { n as liveEnv, t as ensureNodeRuntime } from "./nodeRuntime-CwNJuwH5.js";
17
18
  import { r as spawnRejectionReason } from "./spawnGate-CXjEz6Pf.js";
18
19
  import yargs from "yargs";
19
- import { mkdir, open, readFile, stat, unlink, writeFile } from "fs/promises";
20
+ import { mkdir, open, readFile, rename, rm, stat, unlink, writeFile } from "fs/promises";
20
21
  import { arch, cpus, freemem, homedir, hostname, loadavg, platform, totalmem, uptime, userInfo } from "os";
21
22
  import path from "path";
22
23
  import { execFileSync } from "node:child_process";
@@ -42,6 +43,170 @@ import { existsSync, renameSync, watch, writeFileSync } from "node:fs";
42
43
  */
43
44
  const isTerminalReply = (s) => /^(?:\x1b\[(?:\??\d+(?:;\d+)*R|\?[\d;]*c|>[\d;]*c|\d*n))+$/.test(s);
44
45
 
46
+ //#endregion
47
+ //#region ts/statusText.ts
48
+ /**
49
+ * Extract a short "what is the agent doing right now?" line from the rendered
50
+ * terminal screen. Claude Code paints this as a spinner/status line, e.g.
51
+ * "✶ Verifying calendar meetings with real data… (6m 30s · ↓ 19.5k tokens)".
52
+ */
53
+ const SPINNER_PREFIX = /^[\u2800-\u28ff✶✻✢✳✽✦✧✩✷✸✹✺✼·•●◐◓◒◑]\s+/u;
54
+ const CONTROL = /[\x00-\x1f\x7f-\x9f]/g;
55
+ function parseStatusText(lines) {
56
+ for (let i = lines.length - 1; i >= 0; i--) {
57
+ const line = lines[i]?.replace(CONTROL, "").trim();
58
+ if (!line || line.length < 3) continue;
59
+ if (!SPINNER_PREFIX.test(line)) continue;
60
+ if (/^(?:[•·]\s*)?(?:esc|ctrl|enter|return|shift|tab)\b/i.test(line)) continue;
61
+ return line.slice(0, 220).trim();
62
+ }
63
+ return null;
64
+ }
65
+
66
+ //#endregion
67
+ //#region ts/serveLock.ts
68
+ /**
69
+ * Singleton lock for the WebRTC host role: one `ay serve` host per
70
+ * ~/.agent-yes. Two hosts sharing the persisted room (~/.agent-yes/.share-room)
71
+ * fight over every viewer connection — the exact outage seen live: an orphaned
72
+ * `ay serve --webrtc` from a previous manager era plus a manual run left the
73
+ * managed daemon crash-looping (12 watchdog restarts) and the share link
74
+ * unloadable. The lock makes the loser fail FAST with a pointer to the owner
75
+ * instead of silently contending.
76
+ *
77
+ * mkdir-based (atomic on every platform), owner JSON alongside, heartbeat
78
+ * refresh while held. A stale owner — dead pid, or a heartbeat older than
79
+ * SERVE_LOCK_STALE_MS (a SIGKILLed host can't clean up) — is stolen, so a
80
+ * crashed host never wedges the next start.
81
+ */
82
+ const SERVE_LOCK_STALE_MS = 2e4;
83
+ const SERVE_LOCK_BEAT_MS = 5e3;
84
+ const SERVE_LOCK_GRACE_MS = 12e3;
85
+ function lockDir() {
86
+ return path.join(agentYesHome$1(), "webrtc-host.lock");
87
+ }
88
+ function ownerPath() {
89
+ return path.join(lockDir(), "owner.json");
90
+ }
91
+ function pidAlive(pid) {
92
+ try {
93
+ process.kill(pid, 0);
94
+ return true;
95
+ } catch (e) {
96
+ return e.code === "EPERM";
97
+ }
98
+ }
99
+ async function readOwner() {
100
+ try {
101
+ const o = JSON.parse(await readFile(ownerPath(), "utf-8"));
102
+ return typeof o?.pid === "number" ? o : null;
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+ /** Pure staleness decision, exported for tests. */
108
+ function isOwnerStale(owner, now, alive, staleMs = SERVE_LOCK_STALE_MS) {
109
+ if (!owner) return true;
110
+ if (!alive(owner.pid)) return true;
111
+ return now - owner.beat_at > staleMs;
112
+ }
113
+ async function stampOwner(startedAt) {
114
+ const tmp = `${ownerPath()}.${process.pid}.tmp`;
115
+ try {
116
+ await writeFile(tmp, JSON.stringify({
117
+ pid: process.pid,
118
+ started_at: startedAt,
119
+ beat_at: Date.now()
120
+ }));
121
+ await rename(tmp, ownerPath());
122
+ return true;
123
+ } catch {
124
+ await rm(tmp, { force: true }).catch(() => {});
125
+ return false;
126
+ }
127
+ }
128
+ /**
129
+ * Acquire the host lock, retrying for up to `graceMs`. Returns fast on a live
130
+ * owner once the grace expires (never blocks a daemon boot forever). With
131
+ * `takeover`, a live owner is SIGTERM'd (then SIGKILL'd) and the lock stolen.
132
+ */
133
+ async function acquireWebrtcHostLock(opts) {
134
+ const graceMs = opts?.graceMs ?? SERVE_LOCK_GRACE_MS;
135
+ const staleMs = opts?.staleMs ?? SERVE_LOCK_STALE_MS;
136
+ const beatMs = opts?.beatMs ?? SERVE_LOCK_BEAT_MS;
137
+ const startedAt = Date.now();
138
+ const deadline = startedAt + graceMs;
139
+ let tookOver = false;
140
+ for (;;) try {
141
+ await mkdir(lockDir(), { recursive: false });
142
+ if (!await stampOwner(startedAt)) {
143
+ await rm(lockDir(), {
144
+ recursive: true,
145
+ force: true
146
+ }).catch(() => {});
147
+ return {
148
+ ok: false,
149
+ owner: null
150
+ };
151
+ }
152
+ const beat = setInterval(() => {
153
+ (async () => {
154
+ if ((await readOwner())?.pid !== process.pid) {
155
+ clearInterval(beat);
156
+ return;
157
+ }
158
+ await stampOwner(startedAt);
159
+ })();
160
+ }, beatMs);
161
+ if (typeof beat.unref === "function") beat.unref();
162
+ return {
163
+ ok: true,
164
+ release: async () => {
165
+ clearInterval(beat);
166
+ if ((await readOwner())?.pid === process.pid) await rm(lockDir(), {
167
+ recursive: true,
168
+ force: true
169
+ }).catch(() => {});
170
+ }
171
+ };
172
+ } catch (e) {
173
+ const code = e.code;
174
+ if (!isTransientLockMkdirError(code)) throw e;
175
+ if (code !== "EEXIST") {
176
+ await new Promise((r) => setTimeout(r, 25));
177
+ continue;
178
+ }
179
+ const owner = await readOwner();
180
+ if (isOwnerStale(owner, Date.now(), pidAlive, staleMs)) {
181
+ await rm(lockDir(), {
182
+ recursive: true,
183
+ force: true
184
+ }).catch(() => {});
185
+ continue;
186
+ }
187
+ if (opts?.takeover && owner && !tookOver) {
188
+ tookOver = true;
189
+ try {
190
+ process.kill(owner.pid, "SIGTERM");
191
+ } catch {}
192
+ await new Promise((r) => setTimeout(r, opts?.takeoverWaitMs ?? 2e3));
193
+ if (pidAlive(owner.pid)) try {
194
+ process.kill(owner.pid, "SIGKILL");
195
+ } catch {}
196
+ await rm(lockDir(), {
197
+ recursive: true,
198
+ force: true
199
+ }).catch(() => {});
200
+ continue;
201
+ }
202
+ if (Date.now() >= deadline) return {
203
+ ok: false,
204
+ owner
205
+ };
206
+ await new Promise((r) => setTimeout(r, 250));
207
+ }
208
+ }
209
+
45
210
  //#endregion
46
211
  //#region ts/strayServe.ts
47
212
  const MANAGEMENT_COMMAND_RE = /\b(?:install|uninstall|status|logs|restart)\b/;
@@ -91,7 +256,49 @@ async function listStrayServeProcesses() {
91
256
 
92
257
  //#endregion
93
258
  //#region ts/serve.ts
94
- const DEFAULT_PORT = 7432;
259
+ const DEFAULT_PORT = 0;
260
+ const PORTLESS_APP_NAME = "agent-yes";
261
+ const PORTLESS_CHILD_ENV = "AGENT_YES_PORTLESS_CHILD";
262
+ function portlessConsoleUrl(token) {
263
+ return portlessConsoleUrlForOrigin(process.env.PORTLESS_URL?.replace(/\/$/, "") || `https://${PORTLESS_APP_NAME}.localhost`, token);
264
+ }
265
+ function portlessConsoleUrlForOrigin(origin, token) {
266
+ return `${origin}${token ? `/#k=${encodeURIComponent(token)}` : "/"}`;
267
+ }
268
+ async function resolvedPortlessConsoleUrl(token) {
269
+ if (process.env.PORTLESS_URL) return portlessConsoleUrl(token);
270
+ try {
271
+ const port = Number((await readFile(path.join(homedir(), ".portless", "proxy.port"), "utf-8")).trim());
272
+ if (Number.isInteger(port) && port > 0) return portlessConsoleUrlForOrigin(`https://${PORTLESS_APP_NAME}.localhost${port === 443 ? "" : `:${port}`}`, token);
273
+ } catch {}
274
+ return portlessConsoleUrl(token);
275
+ }
276
+ async function runWithPortless(rest) {
277
+ const portless = Bun.which("portless");
278
+ if (!portless) {
279
+ process.stderr.write("ay serve: Portless is required for local HTTPS. Install it with: npm install -g portless\n");
280
+ return 1;
281
+ }
282
+ const script = process.argv[1];
283
+ const self = script && /\.[cm]?[jt]s$/.test(script) ? [process.execPath, script] : [process.execPath];
284
+ return await Bun.spawn([
285
+ portless,
286
+ PORTLESS_APP_NAME,
287
+ ...self,
288
+ "serve",
289
+ ...rest
290
+ ], {
291
+ stdio: [
292
+ "inherit",
293
+ "inherit",
294
+ "inherit"
295
+ ],
296
+ env: {
297
+ ...liveEnv(),
298
+ [PORTLESS_CHILD_ENV]: "1"
299
+ }
300
+ }).exited ?? 1;
301
+ }
95
302
  /**
96
303
  * Normalize a user-supplied GitHub-ish source into the standard
97
304
  * `<owner>/<repo>/tree/<branch>` path that codehost/provision's `parseSpec`
@@ -565,7 +772,18 @@ async function readDaemonServeArgs(mgr) {
565
772
  }
566
773
  function portFromArgs(args) {
567
774
  const m = /--port[=\s](\d+)/.exec(args.join(" "));
568
- return m ? Number(m[1]) : DEFAULT_PORT;
775
+ return m ? Number(m[1]) : null;
776
+ }
777
+ function argsUsePortless(args) {
778
+ return (args.some((a) => a.startsWith("--http") || a.startsWith("--share")) || !args.some((a) => a.startsWith("--webrtc"))) && portFromArgs(args) === null;
779
+ }
780
+ async function portlessAppPort() {
781
+ try {
782
+ const route = JSON.parse(await readFile(path.join(homedir(), ".portless", "routes.json"), "utf-8")).find((r) => r.hostname === `${PORTLESS_APP_NAME}.localhost`);
783
+ return typeof route?.port === "number" ? route.port : null;
784
+ } catch {
785
+ return null;
786
+ }
569
787
  }
570
788
  async function warnStrayServeProcesses() {
571
789
  const stray = await listStrayServeProcesses();
@@ -586,6 +804,7 @@ function explicitWebrtcUrl(args) {
586
804
  }
587
805
  }
588
806
  async function fetchDaemonVersion(port, token) {
807
+ if (port === null) return null;
589
808
  try {
590
809
  const r = await fetch(`http://127.0.0.1:${port}/api/version`, {
591
810
  headers: { Authorization: `Bearer ${token}` },
@@ -673,7 +892,7 @@ async function cmdServeDaemon(sub, args) {
673
892
  let shareLink = null;
674
893
  let shareLinkMinted = false;
675
894
  if (webrtcDaemon) try {
676
- const { loadOrCreateShareRoom, shareLinkFromRoomUrl } = await import("./share-Ciw1mWVN.js");
895
+ const { loadOrCreateShareRoom, shareLinkFromRoomUrl } = await import("./share-_QYjPN7q.js");
677
896
  const explicit = explicitWebrtcUrl(effArgs);
678
897
  shareLink = shareLinkFromRoomUrl(explicit ?? await loadOrCreateShareRoom());
679
898
  shareLinkMinted = !explicit;
@@ -688,7 +907,7 @@ async function cmdServeDaemon(sub, args) {
688
907
  };
689
908
  if (priorArgs !== null) {
690
909
  const sameConfig = JSON.stringify(effArgs) === JSON.stringify(priorArgs);
691
- const runningVer = await fetchDaemonVersion(portFromArgs(effArgs), token);
910
+ const runningVer = await fetchDaemonVersion(argsUsePortless(effArgs) ? await portlessAppPort() : portFromArgs(effArgs), token);
692
911
  if (runningVer === current && sameConfig) {
693
912
  await ensureBootAutostart(mgr);
694
913
  process.stdout.write(`'${DAEMON_NAME}' already running v${current} (up to date)\n`);
@@ -755,7 +974,8 @@ async function cmdServeDaemon(sub, args) {
755
974
  }).exited;
756
975
  if (code === 0) {
757
976
  const onBoot = await ensureBootAutostart(mgr);
758
- const port = portFromArgs(effArgs);
977
+ const port = argsUsePortless(effArgs) ? await portlessAppPort() : portFromArgs(effArgs);
978
+ const localUrl = argsUsePortless(effArgs) ? await resolvedPortlessConsoleUrl(token) : null;
759
979
  const httpish = effArgs.some((a) => a.startsWith("--http") || a.startsWith("--share")) || !effArgs.some((a) => a.startsWith("--webrtc"));
760
980
  process.stdout.write(`\n${priorArgs !== null ? `rolled '${DAEMON_NAME}' forward to` : `installed '${DAEMON_NAME}' as a daemon via ${mgr.id} —`} v${current}\n`);
761
981
  if (mgr.id === "oxmgr" && process.platform === "win32") process.stdout.write(onBoot ? `start-on-login: enabled (Task Scheduler ONLOGON runs \`oxmgr daemon run\`)\n` : `start-on-login: not registered — run \`oxmgr service install\` to enable\n`);
@@ -764,8 +984,11 @@ async function cmdServeDaemon(sub, args) {
764
984
  else process.stdout.write(onBoot ? `start-on-boot: enabled (pm2 startup registered with the system init)\n` : `start-on-boot: not registered — run \`pm2 startup\` (may need sudo) to enable\n`);
765
985
  process.stdout.write(`token: ${token}\n\n`);
766
986
  if (httpish) {
767
- process.stdout.write(` ay ls ${token}@<host>:${port}\n`);
768
- process.stdout.write(` ay remote add <alias> http://${token}@<host>:${port}\n`);
987
+ if (argsUsePortless(effArgs)) process.stdout.write(` web console: ${localUrl}\n`);
988
+ else if (port !== null) {
989
+ process.stdout.write(` ay ls ${token}@<host>:${port}\n`);
990
+ process.stdout.write(` ay remote add <alias> http://${token}@<host>:${port}\n`);
991
+ }
769
992
  }
770
993
  process.stdout.write(` ay serve logs # view server logs\n`);
771
994
  process.stdout.write(` ay serve uninstall # remove daemon\n`);
@@ -796,7 +1019,9 @@ async function cmdServeStatus(args) {
796
1019
  const daemonArgs = mgr ? await readDaemonServeArgs(mgr) : null;
797
1020
  const installed = daemonArgs !== null;
798
1021
  const a = daemonArgs ?? [];
799
- const port = portFromArgs(a);
1022
+ const local = argsUsePortless(a);
1023
+ const port = local ? await portlessAppPort() : portFromArgs(a);
1024
+ const localUrl = local ? await resolvedPortlessConsoleUrl(token ?? void 0) : null;
800
1025
  const webrtcish = a.some((x) => x.startsWith("--webrtc") || x.startsWith("--share"));
801
1026
  const httpish = a.some((x) => x.startsWith("--http") || x.startsWith("--share")) || !a.some((x) => x.startsWith("--webrtc"));
802
1027
  const mode = httpish && webrtcish ? "http+webrtc" : webrtcish ? "webrtc" : "http";
@@ -808,6 +1033,7 @@ async function cmdServeStatus(args) {
808
1033
  installed,
809
1034
  mode,
810
1035
  port: httpish ? port : null,
1036
+ localUrl: httpish ? localUrl : null,
811
1037
  reachable: runningVersion !== null,
812
1038
  runningVersion,
813
1039
  currentVersion: current,
@@ -823,18 +1049,23 @@ async function cmdServeStatus(args) {
823
1049
  w(`manager: ${mgr ? mgr.id : "none — install pm2 or oxmgr to daemonize"}`);
824
1050
  if (installed) {
825
1051
  w(`installed: yes (via ${mgr.id})`);
826
- w(`mode: ${mode}${httpish ? ` (port ${port})` : ""}`);
1052
+ w(`mode: ${mode}${httpish ? local ? ` (${localUrl})` : ` (port ${port})` : ""}`);
827
1053
  if (a.length) w(`args: ${a.join(" ")}`);
828
1054
  } else w(`installed: no — start a daemon with: ay serve install [--share]`);
829
- if (runningVersion !== null) w(`http api: reachable on 127.0.0.1:${port} — v${runningVersion} (${runningVersion === current ? "up to date" : `outdated (current v${current})`})`);
830
- else if (mode === "webrtc") w(`http api: none (webrtc-only)`);
831
- else w(`http api: not reachable on 127.0.0.1:${port} (not running)`);
1055
+ if (runningVersion !== null) {
1056
+ const tag = runningVersion === current ? "up to date" : `outdated (current v${current})`;
1057
+ w(`http api: reachable ${local ? `via ${localUrl}` : `on 127.0.0.1:${port}`} — v${runningVersion} (${tag})`);
1058
+ } else if (mode === "webrtc") w(`http api: none (webrtc-only)`);
1059
+ else w(`http api: not reachable ${local ? `via ${localUrl}` : `on 127.0.0.1:${port}`} (not running)`);
832
1060
  w(`token: ${token ?? "(none yet — created on first serve)"}`);
833
1061
  if (shareLink) w(`share link: ${shareLink}`);
834
1062
  if (token && httpish) {
835
1063
  w();
836
- w(`connect: ay ls ${token}@<host>:${port}`);
837
- w(` ay remote add <alias> http://${token}@<host>:${port}`);
1064
+ if (local) w(`console: ${localUrl}`);
1065
+ else if (port !== null) {
1066
+ w(`connect: ay ls ${token}@<host>:${port}`);
1067
+ w(` ay remote add <alias> http://${token}@<host>:${port}`);
1068
+ }
838
1069
  }
839
1070
  return 0;
840
1071
  }
@@ -855,7 +1086,13 @@ Modes (default: --http):
855
1086
  --share [URL] Legacy alias for --http --webrtc
856
1087
 
857
1088
  Options:
858
- --port N Port to listen on (default: ${DEFAULT_PORT})\n --host HOST Interface to bind (default: 127.0.0.1; use 0.0.0.0 to expose)\n --token TOKEN Auth token (auto-generated and saved if omitted)\n -d, --daemon Install these flags as a background daemon (pm2/oxmgr)\n (same as: ay serve install <flags>)\n --allow-spawn Deprecated no-op — the console can always spawn agents\n --tls-cert FILE TLS certificate PEM\n --tls-key FILE TLS private key PEM\n\nSubcommands:\n ay serve install install as background daemon (oxmgr; pm2 fallback on Windows without the winfix build)\n ay serve status show daemon/server status (add --json for scripts)\n ay serve uninstall remove daemon\n ay serve logs view daemon logs\n\nOnce running, connect from another machine:\n ay ls <token>@<host>:${DEFAULT_PORT}\n ay remote add <alias> http://<token>@<host>:${DEFAULT_PORT}\n`);
1089
+ --port N Bypass Portless and listen on a fixed HTTP port
1090
+ --host HOST Interface to bind (default: 127.0.0.1; use 0.0.0.0 to expose)
1091
+ --token TOKEN Auth token (auto-generated and saved if omitted)
1092
+ -d, --daemon Install these flags as a background daemon (pm2/oxmgr)
1093
+ (same as: ay serve install <flags>)
1094
+ --local Explicitly use Portless (the default for HTTP mode)
1095
+ ${portlessConsoleUrl()}\n --allow-spawn Deprecated no-op — the console can always spawn agents\n --tls-cert FILE TLS certificate PEM\n --tls-key FILE TLS private key PEM\n\nSubcommands:\n ay serve install install as background daemon (oxmgr; pm2 fallback on Windows without the winfix build)\n ay serve status show daemon/server status (add --json for scripts)\n ay serve uninstall remove daemon\n ay serve logs view daemon logs\n\nOnce running, connect from another machine:\n ay ls <token>@<host>:<port>\n ay remote add <alias> http://<token>@<host>:<port>\n`);
859
1096
  return 0;
860
1097
  }
861
1098
  const sub = rest[0];
@@ -875,8 +1112,7 @@ Options:
875
1112
  if (sub === "install" || sub === "uninstall" || sub === "logs") return cmdServeDaemon(sub, rest.slice(1));
876
1113
  const argv = await yargs(rest).usage("Usage: ay serve [options]").option("port", {
877
1114
  type: "number",
878
- default: DEFAULT_PORT,
879
- description: "Port to listen on"
1115
+ description: "Bypass Portless and listen on a fixed port"
880
1116
  }).option("host", {
881
1117
  type: "string",
882
1118
  default: "127.0.0.1",
@@ -904,19 +1140,36 @@ Options:
904
1140
  type: "boolean",
905
1141
  default: false,
906
1142
  description: "Install as a background daemon (same as: ay serve install <flags>)"
1143
+ }).option("local", {
1144
+ type: "boolean",
1145
+ default: false,
1146
+ description: `Use Portless at ${portlessConsoleUrl()} (default for HTTP mode)`
907
1147
  }).option("allow-spawn", {
908
1148
  type: "boolean",
909
1149
  default: false,
910
1150
  description: "Deprecated no-op — the console can always spawn agents"
1151
+ }).option("takeover", {
1152
+ type: "boolean",
1153
+ default: false,
1154
+ description: "If another ay serve already hosts this home's WebRTC room, stop it and take the room"
911
1155
  }).help(false).version(false).exitProcess(false).parseAsync();
912
1156
  if (argv.daemon) return cmdServeDaemon("install", rest.filter((a) => a !== "--daemon" && a !== "-d"));
1157
+ const wantWebrtc = argv.webrtc !== void 0 || argv.share !== void 0;
1158
+ const wantHttp = argv.http === true || argv.share !== void 0 || argv.webrtc === void 0;
1159
+ const fixedPort = typeof argv.port === "number";
1160
+ const usePortless = wantHttp && !fixedPort;
1161
+ if (argv.local === true && fixedPort) {
1162
+ process.stderr.write("ay serve: --local and --port cannot be used together\n");
1163
+ return 1;
1164
+ }
1165
+ if (usePortless && process.env[PORTLESS_CHILD_ENV] !== "1" && process.env.PORTLESS !== "0") return runWithPortless(rest);
913
1166
  const stray = argv._.map(String);
914
1167
  if (stray.length) {
915
1168
  const hint = stray.includes("share") ? " (did you mean --share?)" : "";
916
1169
  process.stderr.write(`ay serve: ignoring unknown argument${stray.length > 1 ? "s" : ""}: ${stray.join(" ")}${hint}\n`);
917
1170
  }
918
1171
  delete process.env.AGENT_YES_PID;
919
- const port = argv.port ?? DEFAULT_PORT;
1172
+ const port = argv.port ?? (Number(process.env.PORT) || DEFAULT_PORT);
920
1173
  const host = argv.host ?? "127.0.0.1";
921
1174
  const tokenFlag = typeof argv.token === "string" ? argv.token : void 0;
922
1175
  const certPath = typeof argv["tls-cert"] === "string" ? argv["tls-cert"] : void 0;
@@ -927,8 +1180,17 @@ Options:
927
1180
  }
928
1181
  const useHttps = !!(certPath && keyPath);
929
1182
  const scheme = useHttps ? "https" : "http";
930
- const wantWebrtc = argv.webrtc !== void 0 || argv.share !== void 0;
931
- const wantHttp = argv.http === true || argv.share !== void 0 || argv.webrtc === void 0;
1183
+ let releaseHostLock = null;
1184
+ if (wantWebrtc && process.env.AGENT_YES_NO_SERVE_LOCK !== "1") {
1185
+ const lock = await acquireWebrtcHostLock({ takeover: argv.takeover === true });
1186
+ if (!lock.ok) {
1187
+ const o = lock.owner;
1188
+ const since = o ? new Date(o.started_at).toLocaleString() : "unknown";
1189
+ process.stderr.write(`ay serve: another instance${o ? ` (pid ${o.pid}, started ${since})` : ""} already hosts this home's WebRTC room\n ay serve status # inspect it\n ay serve --webrtc --takeover # stop it and take the room\n AGENT_YES_NO_SERVE_LOCK=1 # opt out (multi-room setups only)\n`);
1190
+ return 1;
1191
+ }
1192
+ releaseHostLock = lock.release;
1193
+ }
932
1194
  if (wantHttp && host !== "127.0.0.1" && host !== "localhost") process.stderr.write("ay serve: warning: binding to non-loopback — ensure your network is trusted or use Tailscale/VPN\n");
933
1195
  const token = await loadOrCreateToken(tokenFlag);
934
1196
  const titleCache = /* @__PURE__ */ new Map();
@@ -1017,6 +1279,25 @@ Options:
1017
1279
  return [];
1018
1280
  }
1019
1281
  };
1282
+ const statusTextCache = /* @__PURE__ */ new Map();
1283
+ const logStatusText = async (logFile) => {
1284
+ if (!logFile) return null;
1285
+ try {
1286
+ const { size, mtimeMs } = await stat(logFile);
1287
+ const hit = statusTextCache.get(logFile);
1288
+ if (hit && hit.size === size && hit.mtimeMs === mtimeMs) return hit.statusText;
1289
+ const lines = await renderLogTailLines(logFile, 40);
1290
+ const statusText = lines ? parseStatusText(lines) : null;
1291
+ statusTextCache.set(logFile, {
1292
+ size,
1293
+ mtimeMs,
1294
+ statusText
1295
+ });
1296
+ return statusText;
1297
+ } catch {
1298
+ return null;
1299
+ }
1300
+ };
1020
1301
  const niCache = /* @__PURE__ */ new Map();
1021
1302
  const logNeedsInput = async (logFile, cli) => {
1022
1303
  if (!logFile) return null;
@@ -1178,6 +1459,7 @@ Options:
1178
1459
  status: status === "exited" ? status : r.unresponsive ? "stuck" : question ? "needs_input" : status,
1179
1460
  question,
1180
1461
  title: await logTitle(r.log_file),
1462
+ status_text: status === "exited" ? null : await logStatusText(r.log_file),
1181
1463
  git: status === "exited" ? null : await gitStatus(r.cwd),
1182
1464
  tasks: status === "exited" ? null : await logTasks(r.log_file),
1183
1465
  badges: status === "exited" ? [] : await logBadges(r.log_file).then(async (b) => await isUserTyping(r.pid) ? [...b, TYPING_BADGE.id] : b)
@@ -1497,7 +1779,7 @@ Options:
1497
1779
  }
1498
1780
  });
1499
1781
  if (req.method === "GET" && p === "/api/ws") {
1500
- const ws = await import("./ws-BfqEcKyO.js");
1782
+ const ws = await import("./ws-BmHpWaG_.js");
1501
1783
  try {
1502
1784
  await ws.loadProvision();
1503
1785
  } catch (e) {
@@ -1517,7 +1799,7 @@ Options:
1517
1799
  if (req.method === "GET" && p === "/api/ws/status") {
1518
1800
  const dirRaw = url.searchParams.get("path");
1519
1801
  if (!dirRaw) return new Response("missing ?path=<workspace dir>", { status: 400 });
1520
- const ws = await import("./ws-BfqEcKyO.js");
1802
+ const ws = await import("./ws-BmHpWaG_.js");
1521
1803
  let prov;
1522
1804
  try {
1523
1805
  prov = await ws.loadProvision();
@@ -2073,7 +2355,7 @@ Options:
2073
2355
  const perm = body.perm ?? "r";
2074
2356
  if (perm !== "r" && perm !== "rw") return new Response(`invalid perm ${perm} (want r or rw)`, { status: 400 });
2075
2357
  try {
2076
- const { createScopedShare } = await import("./agentShare-TJXTia4N.js");
2358
+ const { createScopedShare } = await import("./agentShare-k8224qiZ.js");
2077
2359
  const share = await createScopedShare({
2078
2360
  agent: body.agent,
2079
2361
  perm,
@@ -2088,12 +2370,12 @@ Options:
2088
2370
  }
2089
2371
  }
2090
2372
  if (req.method === "GET" && p === "/api/shares") {
2091
- const { listShares } = await import("./agentShare-TJXTia4N.js");
2373
+ const { listShares } = await import("./agentShare-k8224qiZ.js");
2092
2374
  return Response.json(listShares());
2093
2375
  }
2094
2376
  const revokeM = /^\/api\/share\/([^/]+)$/.exec(p);
2095
2377
  if (req.method === "DELETE" && revokeM) {
2096
- const { revokeShare } = await import("./agentShare-TJXTia4N.js");
2378
+ const { revokeShare } = await import("./agentShare-k8224qiZ.js");
2097
2379
  const ok = revokeShare(decodeURIComponent(revokeM[1]));
2098
2380
  return new Response(ok ? "revoked" : "no such share", { status: ok ? 200 : 404 });
2099
2381
  }
@@ -2209,17 +2491,18 @@ Options:
2209
2491
  throw e;
2210
2492
  }
2211
2493
  if (server) {
2494
+ const listenPort = server.port;
2212
2495
  const uiHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
2213
- process.stdout.write(`ay serve ${scheme}://${host}:${port}\n`);
2496
+ process.stdout.write(`ay serve ${scheme}://${host}:${listenPort}\n`);
2214
2497
  process.stdout.write(`token: ${token}\n\n`);
2215
2498
  process.stdout.write(`web console (token in the # is eaten on open):\n`);
2216
- process.stdout.write(` ${scheme}://${uiHost}:${port}/#k=${token}\n\n`);
2499
+ process.stdout.write(` ${usePortless ? portlessConsoleUrl(token) : `${scheme}://${uiHost}:${listenPort}/#k=${token}`}\n\n`);
2217
2500
  process.stdout.write(`connect from another machine:\n`);
2218
- process.stdout.write(` ay ls ${token}@<host>:${port}\n`);
2219
- process.stdout.write(` ay tail ${token}@<host>:${port}:<keyword>\n`);
2220
- process.stdout.write(` ay send ${token}@<host>:${port}:<keyword> "message"\n\n`);
2501
+ process.stdout.write(` ay ls ${token}@<host>:${listenPort}\n`);
2502
+ process.stdout.write(` ay tail ${token}@<host>:${listenPort}:<keyword>\n`);
2503
+ process.stdout.write(` ay send ${token}@<host>:${listenPort}:<keyword> "message"\n\n`);
2221
2504
  process.stdout.write(`save as alias:\n`);
2222
- process.stdout.write(` ay remote add <alias> ${scheme}://${token}@<host>:${port}\n\n`);
2505
+ process.stdout.write(` ay remote add <alias> ${scheme}://${token}@<host>:${listenPort}\n\n`);
2223
2506
  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");
2224
2507
  }
2225
2508
  }
@@ -2228,7 +2511,7 @@ Options:
2228
2511
  const webrtcVal = argv.webrtc ?? argv.share;
2229
2512
  const explicitUrl = typeof webrtcVal === "string" && webrtcVal.startsWith("webrtc://") ? webrtcVal : void 0;
2230
2513
  try {
2231
- const { startShare, loadOrCreateShareRoom } = await import("./share-Ciw1mWVN.js");
2514
+ const { startShare, loadOrCreateShareRoom } = await import("./share-_QYjPN7q.js");
2232
2515
  const linkFile = path.join(process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes"), ".share-link");
2233
2516
  const announce = async (room, link, rotated) => {
2234
2517
  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)";
@@ -2256,7 +2539,10 @@ Options:
2256
2539
  await announce(room, link, false);
2257
2540
  } catch (e) {
2258
2541
  process.stderr.write(`ay serve --webrtc failed: ${e.message}\n`);
2259
- if (!wantHttp) return 1;
2542
+ if (!wantHttp) {
2543
+ await releaseHostLock?.().catch(() => {});
2544
+ return 1;
2545
+ }
2260
2546
  }
2261
2547
  }
2262
2548
  let heartbeat;
@@ -2275,9 +2561,9 @@ Options:
2275
2561
  const shutdown = (resolve) => {
2276
2562
  if (heartbeat) clearInterval(heartbeat);
2277
2563
  closeShare?.();
2278
- import("./agentShare-TJXTia4N.js").then((m) => m.revokeAllShares()).catch(() => {});
2564
+ import("./agentShare-k8224qiZ.js").then((m) => m.revokeAllShares()).catch(() => {});
2279
2565
  server?.stop();
2280
- resolve();
2566
+ Promise.resolve(releaseHostLock?.()).catch(() => {}).then(resolve);
2281
2567
  };
2282
2568
  await new Promise((resolve) => {
2283
2569
  process.on("SIGINT", () => shutdown(resolve));
@@ -2288,4 +2574,4 @@ Options:
2288
2574
 
2289
2575
  //#endregion
2290
2576
  export { cmdServe };
2291
- //# sourceMappingURL=serve-eqRnhz8Z.js.map
2577
+ //# sourceMappingURL=serve-crLypY_c.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-eqRnhz8Z.js");
35
+ const { cmdServe } = await import("./serve-crLypY_c.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-BgD5RFXj.js.map
45
+ //# sourceMappingURL=setup-BPNkeSmY.js.map
@@ -1,4 +1,4 @@
1
1
  import "./e2e-jb0Hp43q.js";
2
- import { n as shareLinkFromRoomUrl, r as startShare, t as loadOrCreateShareRoom } from "./share-BfIU8t_h.js";
2
+ import { n as shareLinkFromRoomUrl, r as startShare, t as loadOrCreateShareRoom } from "./share-vchIyVd8.js";
3
3
 
4
4
  export { loadOrCreateShareRoom, shareLinkFromRoomUrl, startShare };