agent-yes 1.96.0 → 1.98.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,546 @@
1
+ import "./ts-BuFWTNL9.js";
2
+ import "./logger-B9h0djqx.js";
3
+ import "./versionChecker-CpNUvHBx.js";
4
+ import "./pidStore-DBjlqzo8.js";
5
+ import "./globalPidIndex-yVd3mbsV.js";
6
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-C0a9K6I5.js";
7
+ import "./remotes-C3xPRtfg.js";
8
+ import { c as readNotes, f as snapshotStatus, l as renderRawLog, m as writeToIpc, o as listRecords, r as controlCodeFromName, u as resolveOne } from "./subcommands-D4Muugfr.js";
9
+ import yargs from "yargs";
10
+ import { mkdir, open, readFile, writeFile } from "fs/promises";
11
+ import { homedir } from "os";
12
+ import path from "path";
13
+ import { fileURLToPath } from "node:url";
14
+ import { watch } from "node:fs";
15
+ import { randomBytes, timingSafeEqual } from "crypto";
16
+
17
+ //#region ts/serve.ts
18
+ const DEFAULT_PORT = 7432;
19
+ function agentYesHome() {
20
+ return process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
21
+ }
22
+ function tokenPath() {
23
+ return path.join(agentYesHome(), ".serve-token");
24
+ }
25
+ async function loadOrCreateToken(tokenFlag) {
26
+ if (tokenFlag) return tokenFlag;
27
+ try {
28
+ return (await readFile(tokenPath(), "utf-8")).trim();
29
+ } catch {
30
+ const token = randomBytes(20).toString("hex");
31
+ await mkdir(agentYesHome(), { recursive: true });
32
+ await writeFile(tokenPath(), token, { mode: 384 });
33
+ return token;
34
+ }
35
+ }
36
+ function tokenEqual(provided, expectedToken) {
37
+ const maxLen = Math.max(provided.length, expectedToken.length);
38
+ return timingSafeEqual(Buffer.from(provided.padEnd(maxLen, "\0")), Buffer.from(expectedToken.padEnd(maxLen, "\0"))) && provided.length === expectedToken.length;
39
+ }
40
+ function checkAuth(req, expectedToken) {
41
+ const auth = req.headers.get("authorization") ?? "";
42
+ if (auth.startsWith("Bearer ")) return tokenEqual(auth.slice(7), expectedToken);
43
+ const q = new URL(req.url).searchParams.get("token");
44
+ return q ? tokenEqual(q, expectedToken) : false;
45
+ }
46
+ const defaultOpts = (overrides = {}) => ({
47
+ all: false,
48
+ active: false,
49
+ json: true,
50
+ latest: true,
51
+ cwdScope: null,
52
+ ...overrides
53
+ });
54
+ const DAEMON_NAME = "agent-yes";
55
+ async function cmdServeDaemon(sub, args) {
56
+ const oxmgrBin = Bun.which("oxmgr");
57
+ if (!oxmgrBin) {
58
+ process.stderr.write("ay serve install: oxmgr not found\n install with: cargo install oxmgr\n or: bun add -g oxmgr\n");
59
+ return 1;
60
+ }
61
+ if (sub === "install") {
62
+ const token = await loadOrCreateToken(void 0);
63
+ const serveCmd = [
64
+ "ay",
65
+ "serve",
66
+ ...args
67
+ ].join(" ");
68
+ const code = await Bun.spawn([
69
+ oxmgrBin,
70
+ "start",
71
+ serveCmd,
72
+ "--name",
73
+ DAEMON_NAME,
74
+ "--restart",
75
+ "always"
76
+ ], { stdio: [
77
+ "ignore",
78
+ "inherit",
79
+ "inherit"
80
+ ] }).exited;
81
+ if (code === 0) {
82
+ const portM = /--port[=\s](\d+)/.exec(args.join(" "));
83
+ const port = portM ? Number(portM[1]) : DEFAULT_PORT;
84
+ process.stdout.write(`\ninstalled '${DAEMON_NAME}' as a daemon via oxmgr\n`);
85
+ process.stdout.write(`token: ${token}\n\n`);
86
+ process.stdout.write(` ay ls ${token}@<host>:${port}\n`);
87
+ process.stdout.write(` ay remote add <alias> http://${token}@<host>:${port}\n`);
88
+ process.stdout.write(` ay serve logs # view server logs\n`);
89
+ process.stdout.write(` ay serve uninstall # remove daemon\n`);
90
+ if (args.some((a) => a.startsWith("--webrtc") || a.startsWith("--share"))) process.stdout.write(`\nthe WebRTC share link is printed by the daemon — see: ay serve logs\n`);
91
+ }
92
+ return code ?? 1;
93
+ }
94
+ if (sub === "uninstall") return await Bun.spawn([
95
+ oxmgrBin,
96
+ "delete",
97
+ DAEMON_NAME
98
+ ], { stdio: [
99
+ "ignore",
100
+ "inherit",
101
+ "inherit"
102
+ ] }).exited ?? 1;
103
+ if (sub === "logs") return await Bun.spawn([
104
+ oxmgrBin,
105
+ "logs",
106
+ DAEMON_NAME,
107
+ ...args
108
+ ], { stdio: [
109
+ "ignore",
110
+ "inherit",
111
+ "inherit"
112
+ ] }).exited ?? 1;
113
+ return 1;
114
+ }
115
+ async function cmdServe(rest) {
116
+ if (rest.includes("-h") || rest.includes("--help")) {
117
+ process.stdout.write(`Usage: ay serve [options]
118
+
119
+ Start an API server (HTTP and/or WebRTC) so browsers and remote machines
120
+ can list/tail/send agents.
121
+
122
+ Modes (default: --http):
123
+ --http HTTP API + web console on --port; no WebRTC
124
+ --webrtc [URL] Share over WebRTC (bare flag mints a room+link on
125
+ agent-yes.com, or pass webrtc://room:token@host).
126
+ Alone it needs NO port — combine with --http for both.
127
+ --share [URL] Legacy alias for --http --webrtc
128
+
129
+ Options:
130
+ --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 --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 via oxmgr\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`);
131
+ return 0;
132
+ }
133
+ const sub = rest[0];
134
+ if (sub === "install" || sub === "uninstall" || sub === "logs") return cmdServeDaemon(sub, rest.slice(1));
135
+ const argv = await yargs(rest).usage("Usage: ay serve [options]").option("port", {
136
+ type: "number",
137
+ default: DEFAULT_PORT,
138
+ description: "Port to listen on"
139
+ }).option("host", {
140
+ type: "string",
141
+ default: "127.0.0.1",
142
+ description: "Interface to bind (use 0.0.0.0 to expose)"
143
+ }).option("token", {
144
+ type: "string",
145
+ description: "Auth token (auto-generated if omitted)"
146
+ }).option("tls-cert", {
147
+ type: "string",
148
+ description: "TLS certificate file (PEM)"
149
+ }).option("tls-key", {
150
+ type: "string",
151
+ description: "TLS private key file (PEM)"
152
+ }).option("http", {
153
+ type: "boolean",
154
+ description: "Serve the HTTP API + web console on --port (default mode)"
155
+ }).option("webrtc", {
156
+ type: "string",
157
+ description: "Share over WebRTC: bare flag mints a room+link, or pass webrtc://room:token@host. Needs no port unless combined with --http"
158
+ }).option("share", {
159
+ type: "string",
160
+ description: "Legacy alias for --http --webrtc"
161
+ }).option("daemon", {
162
+ alias: "d",
163
+ type: "boolean",
164
+ default: false,
165
+ description: "Install as a background daemon via oxmgr (same as: ay serve install <flags>)"
166
+ }).option("allow-spawn", {
167
+ type: "boolean",
168
+ default: false,
169
+ description: "Deprecated no-op — the console can always spawn agents"
170
+ }).help(false).version(false).exitProcess(false).parseAsync();
171
+ const port = argv.port ?? DEFAULT_PORT;
172
+ const host = argv.host ?? "127.0.0.1";
173
+ const tokenFlag = typeof argv.token === "string" ? argv.token : void 0;
174
+ const certPath = typeof argv["tls-cert"] === "string" ? argv["tls-cert"] : void 0;
175
+ const keyPath = typeof argv["tls-key"] === "string" ? argv["tls-key"] : void 0;
176
+ if (certPath && !keyPath || !certPath && keyPath) {
177
+ process.stderr.write("ay serve: --tls-cert and --tls-key must both be provided\n");
178
+ return 1;
179
+ }
180
+ const useHttps = !!(certPath && keyPath);
181
+ const scheme = useHttps ? "https" : "http";
182
+ const wantWebrtc = argv.webrtc !== void 0 || argv.share !== void 0;
183
+ const wantHttp = argv.http === true || argv.share !== void 0 || argv.webrtc === void 0;
184
+ 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");
185
+ const token = await loadOrCreateToken(tokenFlag);
186
+ const titleCache = /* @__PURE__ */ new Map();
187
+ const logTitle = async (logFile) => {
188
+ if (!logFile) return null;
189
+ try {
190
+ const fh = await open(logFile, "r");
191
+ try {
192
+ const { size, mtimeMs } = await fh.stat();
193
+ const hit = titleCache.get(logFile);
194
+ if (hit && hit.size === size && hit.mtimeMs === mtimeMs) return hit.title;
195
+ const len = Math.min(size, 65536);
196
+ const buf = Buffer.allocUnsafe(len);
197
+ const { bytesRead } = await fh.read(buf, 0, len, size - len);
198
+ const text = buf.toString("utf-8", 0, bytesRead);
199
+ const oscTitleRe = /\x1b\][02];([^\x07\x1b]*)(?:\x07|\x1b\\)/g;
200
+ let title = null;
201
+ for (let m; m = oscTitleRe.exec(text);) if (m[1].trim()) title = m[1].trim();
202
+ titleCache.set(logFile, {
203
+ size,
204
+ mtimeMs,
205
+ title
206
+ });
207
+ return title;
208
+ } finally {
209
+ await fh.close();
210
+ }
211
+ } catch {
212
+ return null;
213
+ }
214
+ };
215
+ const apiFetch = async (req) => {
216
+ if (!checkAuth(req, token)) return new Response("Unauthorized", { status: 401 });
217
+ const url = new URL(req.url);
218
+ const p = url.pathname;
219
+ if (req.method === "GET" && p === "/api/ls") {
220
+ const keyword = url.searchParams.get("keyword") ?? void 0;
221
+ const opts = defaultOpts({
222
+ all: url.searchParams.get("all") === "1",
223
+ active: url.searchParams.get("active") === "1"
224
+ });
225
+ try {
226
+ const records = await listRecords(keyword, opts);
227
+ const withTitles = await Promise.all(records.map(async (r) => ({
228
+ ...r,
229
+ title: await logTitle(r.log_file)
230
+ })));
231
+ return Response.json(withTitles);
232
+ } catch (e) {
233
+ return new Response(e.message, { status: 500 });
234
+ }
235
+ }
236
+ if (req.method === "GET" && p === "/api/notes") {
237
+ const notes = await readNotes();
238
+ return Response.json(Object.fromEntries(notes));
239
+ }
240
+ const statusM = /^\/api\/status\/(.+)$/.exec(p);
241
+ if (req.method === "GET" && statusM) {
242
+ const keyword = decodeURIComponent(statusM[1]);
243
+ try {
244
+ const snap = await snapshotStatus(await resolveOne(keyword, defaultOpts({ all: true })));
245
+ return Response.json(snap);
246
+ } catch (e) {
247
+ return new Response(e.message, { status: 404 });
248
+ }
249
+ }
250
+ const readM = /^\/api\/read\/(.+)$/.exec(p);
251
+ if (req.method === "GET" && readM) {
252
+ const keyword = decodeURIComponent(readM[1]);
253
+ const mode = url.searchParams.get("mode") ?? "tail";
254
+ const n = parseInt(url.searchParams.get("n") ?? "96", 10) || 96;
255
+ try {
256
+ const record = await resolveOne(keyword, defaultOpts());
257
+ if (!record.log_file) return new Response(`pid ${record.pid}: no log_file`, { status: 404 });
258
+ const text = await renderRawLog(await readFile(record.log_file), {
259
+ mode,
260
+ n
261
+ });
262
+ return new Response(text, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
263
+ } catch (e) {
264
+ return new Response(e.message, { status: 404 });
265
+ }
266
+ }
267
+ const sizeM = /^\/api\/size\/(.+)$/.exec(p);
268
+ if (req.method === "GET" && sizeM) {
269
+ const keyword = decodeURIComponent(sizeM[1]);
270
+ try {
271
+ const record = await resolveOne(keyword, defaultOpts());
272
+ const ayHome = process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
273
+ let cols = null;
274
+ let rows = null;
275
+ try {
276
+ const [c = 0, r = 0] = (await readFile(path.join(ayHome, "ptysize", String(record.pid)), "utf-8")).trim().split(/\s+/).map(Number);
277
+ if (c > 0 && r > 0) {
278
+ cols = c;
279
+ rows = r;
280
+ }
281
+ } catch {}
282
+ return Response.json({
283
+ pid: record.pid,
284
+ cols,
285
+ rows
286
+ });
287
+ } catch (e) {
288
+ return new Response(e.message, { status: 404 });
289
+ }
290
+ }
291
+ const tailM = /^\/api\/tail\/(.+)$/.exec(p);
292
+ if (req.method === "GET" && tailM) {
293
+ const keyword = decodeURIComponent(tailM[1]);
294
+ const raw = url.searchParams.get("raw") === "1";
295
+ try {
296
+ const record = await resolveOne(keyword, defaultOpts());
297
+ if (!record.log_file) return new Response(`pid ${record.pid}: no log_file`, { status: 404 });
298
+ const logPath = record.log_file;
299
+ const stream = new ReadableStream({ async start(ctrl) {
300
+ const enc = new TextEncoder();
301
+ const send = (text) => ctrl.enqueue(enc.encode(`data: ${JSON.stringify(text)}\n\n`));
302
+ const ping = () => ctrl.enqueue(enc.encode(": ping\n\n"));
303
+ const initBuf = await readFile(logPath).catch(() => Buffer.alloc(0));
304
+ if (raw) send(new TextDecoder().decode(initBuf.slice(Math.max(0, initBuf.length - 65536))));
305
+ else send(await renderRawLog(initBuf, {
306
+ mode: "tail",
307
+ n: 96
308
+ }));
309
+ let offset = initBuf.length;
310
+ let closed = false;
311
+ const heartbeat = setInterval(() => {
312
+ if (closed) {
313
+ clearInterval(heartbeat);
314
+ return;
315
+ }
316
+ ping();
317
+ }, 15e3);
318
+ const ansiRe = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-Z\\-_]/g;
319
+ const ctrlRe = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
320
+ const fh = await open(logPath, "r").catch(() => null);
321
+ let reading = false;
322
+ const flush = async () => {
323
+ if (closed || reading || !fh) return;
324
+ reading = true;
325
+ try {
326
+ const { size } = await fh.stat();
327
+ if (size < offset) offset = size;
328
+ if (size > offset) {
329
+ const len = size - offset;
330
+ const buf = Buffer.allocUnsafe(len);
331
+ const { bytesRead } = await fh.read(buf, 0, len, offset);
332
+ offset += bytesRead;
333
+ const chunk = buf.subarray(0, bytesRead);
334
+ if (raw) send(new TextDecoder().decode(chunk));
335
+ else {
336
+ const text = new TextDecoder().decode(chunk).replace(ansiRe, "").replace(ctrlRe, "");
337
+ if (text.trim()) send(text.trimStart());
338
+ }
339
+ }
340
+ } catch {} finally {
341
+ reading = false;
342
+ }
343
+ };
344
+ let watcher = null;
345
+ try {
346
+ watcher = watch(logPath, () => void flush());
347
+ } catch {}
348
+ const poller = setInterval(() => void flush(), 60);
349
+ req.signal.addEventListener("abort", () => {
350
+ closed = true;
351
+ clearInterval(heartbeat);
352
+ clearInterval(poller);
353
+ try {
354
+ watcher?.close();
355
+ } catch {}
356
+ fh?.close().catch(() => {});
357
+ try {
358
+ ctrl.close();
359
+ } catch {}
360
+ });
361
+ } });
362
+ return new Response(stream, { headers: {
363
+ "Content-Type": "text/event-stream",
364
+ "Cache-Control": "no-cache",
365
+ Connection: "keep-alive"
366
+ } });
367
+ } catch (e) {
368
+ return new Response(e.message, { status: 404 });
369
+ }
370
+ }
371
+ if (req.method === "POST" && p === "/api/send") {
372
+ let body;
373
+ try {
374
+ body = await req.json();
375
+ } catch {
376
+ return new Response("invalid JSON body", { status: 400 });
377
+ }
378
+ const { keyword, msg = "", code = "enter" } = body;
379
+ if (!keyword || typeof keyword !== "string") return new Response("missing keyword", { status: 400 });
380
+ try {
381
+ const record = await resolveOne(keyword, defaultOpts());
382
+ if (!record.fifo_file) return new Response(`pid ${record.pid}: no fifo_file`, { status: 409 });
383
+ const trailing = controlCodeFromName(code.toLowerCase());
384
+ if (msg && trailing) {
385
+ await writeToIpc(record.fifo_file, msg);
386
+ await new Promise((r) => setTimeout(r, 200));
387
+ await writeToIpc(record.fifo_file, trailing);
388
+ } else await writeToIpc(record.fifo_file, msg + trailing);
389
+ return Response.json({
390
+ ok: true,
391
+ pid: record.pid
392
+ });
393
+ } catch (e) {
394
+ return new Response(e.message, { status: 404 });
395
+ }
396
+ }
397
+ const resizeM = /^\/api\/resize\/(.+)$/.exec(p);
398
+ if (req.method === "POST" && resizeM) {
399
+ const keyword = decodeURIComponent(resizeM[1]);
400
+ let body;
401
+ try {
402
+ body = await req.json();
403
+ } catch {
404
+ return new Response("invalid JSON body", { status: 400 });
405
+ }
406
+ const cols = Math.max(1, Math.floor(Number(body.cols) || 0));
407
+ const rows = Math.max(1, Math.floor(Number(body.rows) || 0));
408
+ if (!cols || !rows) return new Response("missing cols/rows", { status: 400 });
409
+ try {
410
+ const record = await resolveOne(keyword, defaultOpts());
411
+ const ayHome = process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
412
+ const winsizeDir = path.join(ayHome, "winsize");
413
+ await mkdir(winsizeDir, { recursive: true });
414
+ await writeFile(path.join(winsizeDir, String(record.pid)), `${cols} ${rows} ${Date.now()}\n`);
415
+ try {
416
+ process.kill(record.pid, "SIGWINCH");
417
+ } catch {}
418
+ return Response.json({
419
+ ok: true,
420
+ pid: record.pid,
421
+ cols,
422
+ rows
423
+ });
424
+ } catch (e) {
425
+ return new Response(e.message, { status: 404 });
426
+ }
427
+ }
428
+ if (req.method === "POST" && p === "/api/spawn") {
429
+ let body;
430
+ try {
431
+ body = await req.json();
432
+ } catch {
433
+ return new Response("invalid JSON body", { status: 400 });
434
+ }
435
+ const cli = String(body.cli ?? "claude");
436
+ if (!SUPPORTED_CLIS.includes(cli)) return new Response(`unsupported cli: ${cli}`, { status: 400 });
437
+ const cwd = typeof body.cwd === "string" && body.cwd ? body.cwd : process.cwd();
438
+ const prompt = String(body.prompt ?? "");
439
+ process.stderr.write(`→ console spawned: ay ${cli}${prompt ? ` -- "${prompt.slice(0, 60)}"` : ""} (cwd: ${cwd})\n`);
440
+ try {
441
+ const child = Bun.spawn([
442
+ "ay",
443
+ cli,
444
+ ...prompt ? ["--", prompt] : []
445
+ ], {
446
+ cwd,
447
+ stdin: "ignore",
448
+ stdout: "ignore",
449
+ stderr: "ignore"
450
+ });
451
+ child.unref();
452
+ return Response.json({
453
+ ok: true,
454
+ pid: child.pid,
455
+ cli,
456
+ cwd
457
+ });
458
+ } catch (e) {
459
+ return new Response(e.message, { status: 500 });
460
+ }
461
+ }
462
+ return new Response("Not Found", { status: 404 });
463
+ };
464
+ const uiDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "lab", "ui");
465
+ const serveUiFile = async (name, type) => {
466
+ try {
467
+ const buf = await readFile(path.join(uiDir, name));
468
+ return new Response(buf, { headers: { "Content-Type": type } });
469
+ } catch {
470
+ return new Response("UI assets not found in this install — use the /api endpoints", { status: 404 });
471
+ }
472
+ };
473
+ const httpFetch = async (req) => {
474
+ const p = new URL(req.url).pathname;
475
+ if (req.method === "GET" && (p === "/" || p === "/index.html")) return serveUiFile("index.html", "text/html; charset=utf-8");
476
+ if (req.method === "GET" && p === "/room-client.js") return serveUiFile("room-client.js", "text/javascript; charset=utf-8");
477
+ if (req.method === "GET" && p === "/favicon.ico") return new Response(null, { status: 204 });
478
+ return apiFetch(req);
479
+ };
480
+ const serverOpts = {
481
+ hostname: host,
482
+ port,
483
+ idleTimeout: 0,
484
+ fetch: httpFetch
485
+ };
486
+ if (useHttps) serverOpts.tls = {
487
+ cert: Bun.file(certPath),
488
+ key: Bun.file(keyPath)
489
+ };
490
+ let server = null;
491
+ if (wantHttp) {
492
+ try {
493
+ server = Bun.serve(serverOpts);
494
+ } catch (e) {
495
+ if (e.code === "EADDRINUSE") {
496
+ process.stderr.write(`ay serve: port ${port} is already in use — pick another with --port N,\nor run a port-free WebRTC-only share with: ay serve --webrtc\n`);
497
+ return 1;
498
+ }
499
+ throw e;
500
+ }
501
+ const uiHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
502
+ process.stdout.write(`ay serve ${scheme}://${host}:${port}\n`);
503
+ process.stdout.write(`token: ${token}\n\n`);
504
+ process.stdout.write(`web console (token in the # is eaten on open):\n`);
505
+ process.stdout.write(` ${scheme}://${uiHost}:${port}/#k=${token}\n\n`);
506
+ process.stdout.write(`connect from another machine:\n`);
507
+ process.stdout.write(` ay ls ${token}@<host>:${port}\n`);
508
+ process.stdout.write(` ay tail ${token}@<host>:${port}:<keyword>\n`);
509
+ process.stdout.write(` ay send ${token}@<host>:${port}:<keyword> "message"\n\n`);
510
+ process.stdout.write(`save as alias:\n`);
511
+ process.stdout.write(` ay remote add <alias> ${scheme}://${token}@<host>:${port}\n\n`);
512
+ 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");
513
+ }
514
+ if (wantWebrtc) {
515
+ const webrtcVal = argv.webrtc ?? argv.share;
516
+ const shareUrl = typeof webrtcVal === "string" && webrtcVal.startsWith("webrtc://") ? webrtcVal : void 0;
517
+ try {
518
+ const { startShare } = await import("./share-D-r6y3xD.js");
519
+ const { link } = await startShare({
520
+ url: shareUrl,
521
+ localFetch: apiFetch,
522
+ apiToken: token
523
+ });
524
+ process.stdout.write(`${wantHttp ? "\n" : ""}shared over WebRTC — open this link (the token is eaten from the URL on open):\n ${link}\n\n`);
525
+ } catch (e) {
526
+ process.stderr.write(`ay serve --webrtc failed: ${e.message}\n`);
527
+ if (!wantHttp) return 1;
528
+ }
529
+ }
530
+ process.stdout.write(`(Ctrl-C to stop)\n`);
531
+ await new Promise((resolve) => {
532
+ process.on("SIGINT", () => {
533
+ server?.stop();
534
+ resolve();
535
+ });
536
+ process.on("SIGTERM", () => {
537
+ server?.stop();
538
+ resolve();
539
+ });
540
+ });
541
+ return 0;
542
+ }
543
+
544
+ //#endregion
545
+ export { cmdServe };
546
+ //# sourceMappingURL=serve-DPY37v0u.js.map
@@ -131,7 +131,7 @@ async function startShare(opts) {
131
131
  const ac = new AbortController();
132
132
  aborts.set(id, ac);
133
133
  try {
134
- const res = await fetch(opts.apiUrl + p, {
134
+ const res = await opts.localFetch(new Request(`http://ay.local${p}`, {
135
135
  method,
136
136
  headers: {
137
137
  Authorization: `Bearer ${opts.apiToken}`,
@@ -139,7 +139,7 @@ async function startShare(opts) {
139
139
  },
140
140
  body: body ?? void 0,
141
141
  signal: ac.signal
142
- });
142
+ }));
143
143
  send(dc, {
144
144
  t: "res",
145
145
  id,
@@ -181,4 +181,4 @@ async function startShare(opts) {
181
181
 
182
182
  //#endregion
183
183
  export { startShare };
184
- //# sourceMappingURL=share-DUhUA1Pi.js.map
184
+ //# sourceMappingURL=share-D-r6y3xD.js.map
@@ -131,6 +131,7 @@ const SUBCOMMANDS = new Set([
131
131
  "restart",
132
132
  "note",
133
133
  "serve",
134
+ "setup",
134
135
  "remote",
135
136
  "help"
136
137
  ]);
@@ -162,9 +163,13 @@ async function runSubcommand(argv) {
162
163
  case "restart": return await cmdRestart(rest);
163
164
  case "note": return await cmdNote(rest);
164
165
  case "serve": {
165
- const { cmdServe } = await import("./serve-CuAPBK4y.js");
166
+ const { cmdServe } = await import("./serve-DPY37v0u.js");
166
167
  return cmdServe(rest);
167
168
  }
169
+ case "setup": {
170
+ const { cmdSetup } = await import("./setup.ts");
171
+ return cmdSetup(rest);
172
+ }
168
173
  case "remote": {
169
174
  const { cmdRemote } = await import("./remotes-C9WMt5PY.js");
170
175
  return cmdRemote(rest);
@@ -179,7 +184,7 @@ async function runSubcommand(argv) {
179
184
  }
180
185
  }
181
186
  function cmdHelp() {
182
- process.stdout.write("ay - agent-yes CLI\n\nManagement:\n ay ls [keyword] list running agents\n ay tail [-f] <keyword> stream output (Ctrl-C to stop)\n ay cat <keyword> full log\n ay head <keyword> first N lines\n ay send <keyword> <msg> send a message\n ay attach <keyword> interactive attach (detach: Ctrl-\\)\n ay stop <keyword> graceful shutdown (/exit for claude/codex)\n ay status <keyword> agent status snapshot\n\nRemote:\n ay serve [--port N] start HTTP API server (prints token)\n ay remote add <alias> http://<token>@<host>:<port>\n ay remote ls / rm <alias> manage saved remotes\n ay ls <token>@<host>:<port> connect inline (no alias needed)\n ay send <token>@<host>:<port>:<kw> <msg>\n\nRun an agent:\n ay [claude|codex|gemini|...] [options] -- [prompt]\n ay claude -- \"fix the bug in auth.ts\"\n ay claude --help full agent-runner options\n\nLabs (examples at https://github.com/snomiao/agent-yes/tree/main/lab):\n local-role-play/ designer + builder on one machine\n http-remote/ ay serve remote access demo\n p2p-pairing/ libp2p P2P (needs: cargo build --features swarm)\n");
187
+ process.stdout.write("ay - agent-yes CLI\n\nManagement:\n ay ls [keyword] list running agents\n ay tail [-f] <keyword> stream output (Ctrl-C to stop)\n ay cat <keyword> full log\n ay head <keyword> first N lines\n ay send <keyword> <msg> send a message\n ay attach <keyword> interactive attach (detach: Ctrl-\\)\n ay stop <keyword> graceful shutdown (/exit for claude/codex)\n ay status <keyword> agent status snapshot\n\nRemote:\n ay setup guided setup: pick a workspace, share to agent-yes.com\n ay serve [--port N] start HTTP API server (prints token)\n ay remote add <alias> http://<token>@<host>:<port>\n ay remote ls / rm <alias> manage saved remotes\n ay ls <token>@<host>:<port> connect inline (no alias needed)\n ay send <token>@<host>:<port>:<kw> <msg>\n\nRun an agent:\n ay [claude|codex|gemini|...] [options] -- [prompt]\n ay claude -- \"fix the bug in auth.ts\"\n ay claude --help full agent-runner options\n\nLabs (examples at https://github.com/snomiao/agent-yes/tree/main/lab):\n local-role-play/ designer + builder on one machine\n http-remote/ ay serve remote access demo\n p2p-pairing/ libp2p P2P (needs: cargo build --features swarm)\n");
183
188
  return 0;
184
189
  }
185
190
  function matchKeyword(record, keyword) {
@@ -1447,4 +1452,4 @@ async function cmdStatus(rest) {
1447
1452
 
1448
1453
  //#endregion
1449
1454
  export { isSubcommand as a, readNotes as c, runSubcommand as d, snapshotStatus as f, isPidAlive as i, renderRawLog as l, writeToIpc as m, cmdHelp as n, listRecords as o, stopTipForCli as p, controlCodeFromName as r, matchKeyword as s, GRACEFUL_EXIT_COMMANDS as t, resolveOne as u };
1450
- //# sourceMappingURL=subcommands-CcOYsLYD.js.map
1455
+ //# sourceMappingURL=subcommands-D4Muugfr.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 isSubcommand, c as readNotes, d as runSubcommand, f as snapshotStatus, i as isPidAlive, l as renderRawLog, m as writeToIpc, n as cmdHelp, o as listRecords, p as stopTipForCli, r as controlCodeFromName, s as matchKeyword, t as GRACEFUL_EXIT_COMMANDS, u as resolveOne } from "./subcommands-CcOYsLYD.js";
4
+ import { a as isSubcommand, c as readNotes, d as runSubcommand, f as snapshotStatus, i as isPidAlive, l as renderRawLog, m as writeToIpc, n as cmdHelp, o as listRecords, p as stopTipForCli, r as controlCodeFromName, s as matchKeyword, t as GRACEFUL_EXIT_COMMANDS, u as resolveOne } from "./subcommands-D4Muugfr.js";
5
5
 
6
6
  export { cmdHelp, isSubcommand, runSubcommand };
@@ -1,4 +1,4 @@
1
- import { n as getRunningAgentCount } from "./runningLock-C22d9SRJ.js";
1
+ import { n as getRunningAgentCount } from "./runningLock-CJxsoGdb.js";
2
2
  import { existsSync } from "fs";
3
3
  import { mkdir, readFile, unlink, writeFile } from "fs/promises";
4
4
  import { homedir } from "os";
@@ -175,4 +175,4 @@ async function startTray() {
175
175
 
176
176
  //#endregion
177
177
  export { ensureTray, startTray };
178
- //# sourceMappingURL=tray-D4cJA4UH.js.map
178
+ //# sourceMappingURL=tray-CWQe9DMY.js.map
@@ -1,7 +1,7 @@
1
1
  import { n as logger, t as addTransport } from "./logger-B9h0djqx.js";
2
- import { r as getInstalledPackage } from "./versionChecker-xqnqyGKE.js";
3
- import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-C22d9SRJ.js";
4
- import { t as PidStore } from "./pidStore-DTzl6zeh.js";
2
+ import { r as getInstalledPackage } from "./versionChecker-CpNUvHBx.js";
3
+ import { n as agentYesHome, t as PidStore } from "./pidStore-DBjlqzo8.js";
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";
6
6
  import { arch, platform } from "process";
7
7
  import { execSync } from "child_process";
@@ -1423,10 +1423,19 @@ async function agentYes({ cli, cliArgs = [], prompt, robust = true, cwd, env, ex
1423
1423
  notifyWebhook("EXIT", `${exitReason} exitCode=${exitCode ?? "?"}`, workingDir).catch(() => null);
1424
1424
  return pendingExitCode.resolve(exitCode);
1425
1425
  });
1426
+ const writeCurrentPtysize = (cols, rows) => {
1427
+ const dir = path.join(agentYesHome(), "ptysize");
1428
+ mkdir(dir, { recursive: true }).then(() => writeFile(path.join(dir, String(process.pid)), `${cols} ${rows}\n`)).catch(() => null);
1429
+ };
1430
+ {
1431
+ const { cols, rows } = getTerminalDimensions();
1432
+ writeCurrentPtysize(cols, rows);
1433
+ }
1426
1434
  process.stdout.on("resize", () => {
1427
1435
  const { cols, rows } = getTerminalDimensions();
1428
1436
  shell.resize(cols, rows);
1429
1437
  xtermProxy.resize(cols, rows);
1438
+ writeCurrentPtysize(cols, rows);
1430
1439
  });
1431
1440
  const isStillWorkingQ = () => {
1432
1441
  const rendered = xtermProxy.tail(24).replace(/\s+/g, " ");
@@ -1705,4 +1714,4 @@ function sleep(ms) {
1705
1714
 
1706
1715
  //#endregion
1707
1716
  export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
1708
- //# sourceMappingURL=ts-DkjQJTcB.js.map
1717
+ //# sourceMappingURL=ts-BuFWTNL9.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.96.0";
10
+ var version = "1.98.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-xqnqyGKE.js.map
224
+ //# sourceMappingURL=versionChecker-CpNUvHBx.js.map