agent-yes 1.94.2 → 1.96.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 { t as CLIS_CONFIG } from "./ts-DkjQJTcB.js";
2
+
3
+ //#region ts/SUPPORTED_CLIS.ts
4
+ const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
5
+
6
+ //#endregion
7
+ export { SUPPORTED_CLIS as t };
8
+ //# sourceMappingURL=SUPPORTED_CLIS-B2FAlgXF.js.map
@@ -0,0 +1,8 @@
1
+ import "./ts-DkjQJTcB.js";
2
+ import "./logger-B9h0djqx.js";
3
+ import "./versionChecker-xqnqyGKE.js";
4
+ import "./pidStore-DTzl6zeh.js";
5
+ import "./globalPidIndex-yVd3mbsV.js";
6
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-B2FAlgXF.js";
7
+
8
+ export { SUPPORTED_CLIS };
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-BY5g27iW.js";
3
+ import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-xqnqyGKE.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-DjrOWqD9.js");
485
+ const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-EieqoC9Z.js");
486
486
  if (isHelpFlag && process.argv.length === 3) {
487
487
  cmdHelp();
488
488
  process.exit(0);
@@ -496,12 +496,12 @@ await checkAndAutoUpdate();
496
496
  logger.info(versionString());
497
497
  const config = parseCliArgs(process.argv);
498
498
  if (config.tray) {
499
- const { startTray } = await import("./tray-DHuD0nEk.js");
499
+ const { startTray } = await import("./tray-D4cJA4UH.js");
500
500
  await startTray();
501
501
  await new Promise(() => {});
502
502
  }
503
503
  {
504
- const { ensureTray } = await import("./tray-DHuD0nEk.js");
504
+ const { ensureTray } = await import("./tray-D4cJA4UH.js");
505
505
  ensureTray();
506
506
  }
507
507
  if (config.useRust) {
@@ -515,7 +515,7 @@ if (config.useRust) {
515
515
  }
516
516
  }
517
517
  if (rustBinary) {
518
- const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-Bo4qbT_0.js");
518
+ const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-G8izHOJP.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-DtwVuD8n.js";
1
+ import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-DkjQJTcB.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import "./versionChecker-BY5g27iW.js";
3
+ import "./versionChecker-xqnqyGKE.js";
4
4
  import "./pidStore-DTzl6zeh.js";
5
5
  import "./globalPidIndex-yVd3mbsV.js";
6
6
 
@@ -147,4 +147,4 @@ async function cmdRemote(rest) {
147
147
 
148
148
  //#endregion
149
149
  export { resolveRemoteSpec as a, readRemotes as i, deleteRemoteAlias as n, writeRemoteAlias as o, parseDirectRemoteSpec as r, cmdRemote as t };
150
- //# sourceMappingURL=remotes-Bjp2GYPz.js.map
150
+ //# sourceMappingURL=remotes-C3xPRtfg.js.map
@@ -1,3 +1,3 @@
1
- import { a as resolveRemoteSpec, i as readRemotes, n as deleteRemoteAlias, o as writeRemoteAlias, r as parseDirectRemoteSpec, t as cmdRemote } from "./remotes-Bjp2GYPz.js";
1
+ import { a as resolveRemoteSpec, i as readRemotes, n as deleteRemoteAlias, o as writeRemoteAlias, r as parseDirectRemoteSpec, t as cmdRemote } from "./remotes-C3xPRtfg.js";
2
2
 
3
3
  export { cmdRemote };
@@ -1,7 +1,11 @@
1
+ import "./ts-DkjQJTcB.js";
1
2
  import "./logger-B9h0djqx.js";
3
+ import "./versionChecker-xqnqyGKE.js";
4
+ import "./pidStore-DTzl6zeh.js";
2
5
  import "./globalPidIndex-yVd3mbsV.js";
3
- import "./remotes-Bjp2GYPz.js";
4
- import { c as readNotes, f as snapshotStatus, l as renderRawLog, m as writeToIpc, o as listRecords, r as controlCodeFromName, u as resolveOne } from "./subcommands-D9wmaZ3U.js";
6
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-B2FAlgXF.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-CcOYsLYD.js";
5
9
  import yargs from "yargs";
6
10
  import { mkdir, readFile, writeFile } from "fs/promises";
7
11
  import { homedir } from "os";
@@ -107,7 +111,7 @@ async function cmdServe(rest) {
107
111
  Start an HTTP API server so remote machines can list/tail/send agents.
108
112
 
109
113
  Options:
110
- --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 --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`);
114
+ --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 --share [URL] Share over WebRTC to agent-yes.com (bare flag mints a room+link)\n --allow-spawn Let the shared console launch new agents (asks y/N per request)\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`);
111
115
  return 0;
112
116
  }
113
117
  const sub = rest[0];
@@ -129,6 +133,13 @@ Options:
129
133
  }).option("tls-key", {
130
134
  type: "string",
131
135
  description: "TLS private key file (PEM)"
136
+ }).option("share", {
137
+ type: "string",
138
+ description: "Share over WebRTC: bare flag mints a room+link, or pass webrtc://room:token@host"
139
+ }).option("allow-spawn", {
140
+ type: "boolean",
141
+ default: false,
142
+ description: "Allow the shared console to spawn new agents (asks y/N per request on a TTY)"
132
143
  }).help(false).version(false).exitProcess(false).parseAsync();
133
144
  const port = argv.port ?? DEFAULT_PORT;
134
145
  const host = argv.host ?? "127.0.0.1";
@@ -143,6 +154,20 @@ Options:
143
154
  const scheme = useHttps ? "https" : "http";
144
155
  if (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");
145
156
  const token = await loadOrCreateToken(tokenFlag);
157
+ const allowSpawn = argv["allow-spawn"] === true;
158
+ const spawnQueue = [];
159
+ let stdinWired = false;
160
+ const confirmSpawn = (cli, cwd, prompt) => {
161
+ if (!process.stdin.isTTY) return Promise.resolve(true);
162
+ if (!stdinWired) {
163
+ stdinWired = true;
164
+ process.stdin.setEncoding("utf8");
165
+ process.stdin.on("data", (d) => spawnQueue.shift()?.(/^y/i.test(d.trim())));
166
+ process.stdin.resume();
167
+ }
168
+ process.stdout.write(`\n⚠ console requests spawn: ay ${cli}${prompt ? ` -- "${prompt.slice(0, 60)}"` : ""}\n cwd: ${cwd}\n allow? [y/N] `);
169
+ return new Promise((res) => spawnQueue.push(res));
170
+ };
146
171
  const serverOpts = {
147
172
  hostname: host,
148
173
  port,
@@ -197,6 +222,7 @@ Options:
197
222
  const tailM = /^\/api\/tail\/(.+)$/.exec(p);
198
223
  if (req.method === "GET" && tailM) {
199
224
  const keyword = decodeURIComponent(tailM[1]);
225
+ const raw = url.searchParams.get("raw") === "1";
200
226
  try {
201
227
  const record = await resolveOne(keyword, defaultOpts());
202
228
  if (!record.log_file) return new Response(`pid ${record.pid}: no log_file`, { status: 404 });
@@ -206,7 +232,8 @@ Options:
206
232
  const send = (text) => ctrl.enqueue(enc.encode(`data: ${JSON.stringify(text)}\n\n`));
207
233
  const ping = () => ctrl.enqueue(enc.encode(": ping\n\n"));
208
234
  const initBuf = await readFile(logPath).catch(() => Buffer.alloc(0));
209
- send(await renderRawLog(initBuf, {
235
+ if (raw) send(new TextDecoder().decode(initBuf.slice(Math.max(0, initBuf.length - 65536))));
236
+ else send(await renderRawLog(initBuf, {
210
237
  mode: "tail",
211
238
  n: 96
212
239
  }));
@@ -231,8 +258,11 @@ Options:
231
258
  if (full.length <= offset) return;
232
259
  const chunk = full.slice(offset);
233
260
  offset = full.length;
234
- const text = new TextDecoder().decode(chunk).replace(ansiRe, "").replace(ctrlRe, "");
235
- if (text.trim()) send(text.trimStart());
261
+ if (raw) send(new TextDecoder().decode(chunk));
262
+ else {
263
+ const text = new TextDecoder().decode(chunk).replace(ansiRe, "").replace(ctrlRe, "");
264
+ if (text.trim()) send(text.trimStart());
265
+ }
236
266
  } catch {}
237
267
  }, 300);
238
268
  req.signal.addEventListener("abort", () => {
@@ -279,6 +309,72 @@ Options:
279
309
  return new Response(e.message, { status: 404 });
280
310
  }
281
311
  }
312
+ const resizeM = /^\/api\/resize\/(.+)$/.exec(p);
313
+ if (req.method === "POST" && resizeM) {
314
+ const keyword = decodeURIComponent(resizeM[1]);
315
+ let body;
316
+ try {
317
+ body = await req.json();
318
+ } catch {
319
+ return new Response("invalid JSON body", { status: 400 });
320
+ }
321
+ const cols = Math.max(1, Math.floor(Number(body.cols) || 0));
322
+ const rows = Math.max(1, Math.floor(Number(body.rows) || 0));
323
+ if (!cols || !rows) return new Response("missing cols/rows", { status: 400 });
324
+ try {
325
+ const record = await resolveOne(keyword, defaultOpts());
326
+ const ayHome = process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
327
+ const winsizeDir = path.join(ayHome, "winsize");
328
+ await mkdir(winsizeDir, { recursive: true });
329
+ await writeFile(path.join(winsizeDir, String(record.pid)), `${cols} ${rows} ${Date.now()}\n`);
330
+ try {
331
+ process.kill(record.pid, "SIGWINCH");
332
+ } catch {}
333
+ return Response.json({
334
+ ok: true,
335
+ pid: record.pid,
336
+ cols,
337
+ rows
338
+ });
339
+ } catch (e) {
340
+ return new Response(e.message, { status: 404 });
341
+ }
342
+ }
343
+ if (req.method === "POST" && p === "/api/spawn") {
344
+ if (!allowSpawn) return new Response("spawning disabled — start: ay serve --share --allow-spawn", { status: 403 });
345
+ let body;
346
+ try {
347
+ body = await req.json();
348
+ } catch {
349
+ return new Response("invalid JSON body", { status: 400 });
350
+ }
351
+ const cli = String(body.cli ?? "claude");
352
+ if (!SUPPORTED_CLIS.includes(cli)) return new Response(`unsupported cli: ${cli}`, { status: 400 });
353
+ const cwd = typeof body.cwd === "string" && body.cwd ? body.cwd : process.cwd();
354
+ const prompt = String(body.prompt ?? "");
355
+ if (!await confirmSpawn(cli, cwd, prompt)) return new Response("denied by host", { status: 403 });
356
+ try {
357
+ const child = Bun.spawn([
358
+ "ay",
359
+ cli,
360
+ ...prompt ? ["--", prompt] : []
361
+ ], {
362
+ cwd,
363
+ stdin: "ignore",
364
+ stdout: "ignore",
365
+ stderr: "ignore"
366
+ });
367
+ child.unref();
368
+ return Response.json({
369
+ ok: true,
370
+ pid: child.pid,
371
+ cli,
372
+ cwd
373
+ });
374
+ } catch (e) {
375
+ return new Response(e.message, { status: 500 });
376
+ }
377
+ }
282
378
  return new Response("Not Found", { status: 404 });
283
379
  }
284
380
  };
@@ -296,6 +392,20 @@ Options:
296
392
  process.stdout.write(`save as alias:\n`);
297
393
  process.stdout.write(` ay remote add <alias> ${scheme}://${token}@<host>:${port}\n\n`);
298
394
  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");
395
+ if (argv.share !== void 0) {
396
+ const shareUrl = typeof argv.share === "string" && argv.share.startsWith("webrtc://") ? argv.share : void 0;
397
+ try {
398
+ const { startShare } = await import("./share-DUhUA1Pi.js");
399
+ const { link } = await startShare({
400
+ url: shareUrl,
401
+ apiUrl: `http://127.0.0.1:${port}`,
402
+ apiToken: token
403
+ });
404
+ process.stdout.write(`\nshared over WebRTC — open this link (the token is eaten from the URL on open):\n ${link}\n\n`);
405
+ } catch (e) {
406
+ process.stderr.write(`ay serve --share failed: ${e.message}\n`);
407
+ }
408
+ }
299
409
  process.stdout.write(`(Ctrl-C to stop)\n`);
300
410
  await new Promise((resolve) => {
301
411
  process.on("SIGINT", () => {
@@ -312,4 +422,4 @@ Options:
312
422
 
313
423
  //#endregion
314
424
  export { cmdServe };
315
- //# sourceMappingURL=serve-CixOwENN.js.map
425
+ //# sourceMappingURL=serve-CuAPBK4y.js.map
@@ -0,0 +1,184 @@
1
+ import { randomBytes } from "crypto";
2
+
3
+ //#region ts/share.ts
4
+ const SUB = "ay-signal-1";
5
+ const ICE = [{ urls: "stun:stun.l.google.com:19302" }];
6
+ const MAX_CHUNK = 15e3;
7
+ const DEFAULT_SIGHOST = "s.agent-yes.com";
8
+ function parseShareUrl(s) {
9
+ const m = /^webrtc:\/\/([^:@/]+):([^@/]+)@(.+)$/.exec(s);
10
+ if (!m) throw new Error(`bad --share url: ${s} (want webrtc://room:token@host)`);
11
+ return {
12
+ room: m[1],
13
+ token: m[2],
14
+ host: m[3]
15
+ };
16
+ }
17
+ async function importRTC() {
18
+ try {
19
+ return (await import("node-datachannel/polyfill")).RTCPeerConnection;
20
+ } catch {
21
+ try {
22
+ const { existsSync, symlinkSync, mkdirSync, readdirSync } = await import("fs");
23
+ const path = (await import("path")).default;
24
+ const { createRequire } = await import("module");
25
+ const require = createRequire(import.meta.url);
26
+ const pkg = path.dirname(require.resolve("node-datachannel/package.json"));
27
+ const bin = path.join(pkg, "build", "Release", "node_datachannel.node");
28
+ const cacheRoot = path.join((await import("os")).homedir(), ".bun", "install", "cache");
29
+ if (existsSync(bin) && existsSync(cacheRoot)) for (const d of readdirSync(cacheRoot)) {
30
+ if (!d.startsWith("node-datachannel@")) continue;
31
+ const dst = path.join(cacheRoot, d, "build", "Release");
32
+ mkdirSync(dst, { recursive: true });
33
+ const link = path.join(dst, "node_datachannel.node");
34
+ if (!existsSync(link)) symlinkSync(bin, link);
35
+ }
36
+ } catch {}
37
+ return (await import("node-datachannel/polyfill")).RTCPeerConnection;
38
+ }
39
+ }
40
+ /** Start the share bridge. Resolves once signaling is connected; runs until the
41
+ * process exits, reconnecting signaling on drop. Returns the shareable link. */
42
+ async function startShare(opts) {
43
+ opts.url;
44
+ const sighost = opts.sighost ?? DEFAULT_SIGHOST;
45
+ const { room, token, host } = opts.url ? parseShareUrl(opts.url) : {
46
+ room: "r" + randomBytes(3).toString("hex"),
47
+ token: randomBytes(32).toString("hex"),
48
+ host: sighost
49
+ };
50
+ const RTCPeerConnection = await importRTC();
51
+ const wsScheme = host.startsWith("localhost") || host.startsWith("127.") ? "ws" : "wss";
52
+ const link = `${host === "s.agent-yes.com" ? "https://agent-yes.com" : "http://localhost:7778"}/#${room}:${token}${host === "s.agent-yes.com" ? "" : "@" + host}`;
53
+ const peers = /* @__PURE__ */ new Map();
54
+ const connectSignaling = (onReady) => {
55
+ const ws = new WebSocket(`${wsScheme}://${host}/${room}`, [SUB]);
56
+ let ready = false;
57
+ ws.onopen = () => {
58
+ ws.send(JSON.stringify({
59
+ type: "hello",
60
+ role: "host",
61
+ token
62
+ }));
63
+ ready = true;
64
+ onReady();
65
+ };
66
+ ws.onmessage = async (ev) => {
67
+ const m = JSON.parse(ev.data);
68
+ if (m.type === "peer-join") startPeer(ws, m.peer);
69
+ else if (m.type === "answer") await peers.get(m.from)?.pc.setRemoteDescription({
70
+ type: "answer",
71
+ sdp: m.sdp
72
+ });
73
+ else if (m.type === "candidate") await peers.get(m.from)?.pc.addIceCandidate(m.candidate).catch(() => {});
74
+ else if (m.type === "peer-leave") closePeer(m.peer);
75
+ };
76
+ ws.onclose = () => {
77
+ setTimeout(() => connectSignaling(() => {}), ready ? 1500 : 4e3);
78
+ };
79
+ ws.onerror = () => {};
80
+ return ws;
81
+ };
82
+ function startPeer(ws, peerId) {
83
+ const pc = new RTCPeerConnection({ iceServers: ICE });
84
+ const aborts = /* @__PURE__ */ new Map();
85
+ peers.set(peerId, {
86
+ pc,
87
+ aborts
88
+ });
89
+ pc.onicecandidate = (e) => {
90
+ if (e.candidate) ws.send(JSON.stringify({
91
+ type: "candidate",
92
+ to: peerId,
93
+ candidate: e.candidate
94
+ }));
95
+ };
96
+ pc.onconnectionstatechange = () => {
97
+ if ([
98
+ "failed",
99
+ "closed",
100
+ "disconnected"
101
+ ].includes(pc.connectionState)) closePeer(peerId);
102
+ };
103
+ const dc = pc.createDataChannel("api");
104
+ dc.onmessage = (e) => onReq(dc, aborts, JSON.parse(e.data));
105
+ pc.createOffer().then((o) => pc.setLocalDescription(o)).then(() => ws.send(JSON.stringify({
106
+ type: "offer",
107
+ to: peerId,
108
+ sdp: pc.localDescription.sdp
109
+ })));
110
+ }
111
+ function closePeer(peerId) {
112
+ const p = peers.get(peerId);
113
+ if (!p) return;
114
+ for (const a of p.aborts.values()) a.abort();
115
+ try {
116
+ p.pc.close();
117
+ } catch {}
118
+ peers.delete(peerId);
119
+ }
120
+ function send(dc, obj) {
121
+ if (dc.readyState === "open") dc.send(JSON.stringify(obj));
122
+ }
123
+ async function onReq(dc, aborts, req) {
124
+ if (req.t === "abort") {
125
+ aborts.get(req.id)?.abort();
126
+ aborts.delete(req.id);
127
+ return;
128
+ }
129
+ if (req.t !== "req") return;
130
+ const { id, method, path: p, body } = req;
131
+ const ac = new AbortController();
132
+ aborts.set(id, ac);
133
+ try {
134
+ const res = await fetch(opts.apiUrl + p, {
135
+ method,
136
+ headers: {
137
+ Authorization: `Bearer ${opts.apiToken}`,
138
+ ...body ? { "Content-Type": "application/json" } : {}
139
+ },
140
+ body: body ?? void 0,
141
+ signal: ac.signal
142
+ });
143
+ send(dc, {
144
+ t: "res",
145
+ id,
146
+ status: res.status,
147
+ ct: res.headers.get("content-type") ?? ""
148
+ });
149
+ const reader = res.body.getReader();
150
+ const dec = new TextDecoder();
151
+ for (;;) {
152
+ const { done, value } = await reader.read();
153
+ if (done) break;
154
+ const text = dec.decode(value, { stream: true });
155
+ for (let i = 0; i < text.length; i += MAX_CHUNK) send(dc, {
156
+ t: "data",
157
+ id,
158
+ chunk: text.slice(i, i + MAX_CHUNK)
159
+ });
160
+ }
161
+ send(dc, {
162
+ t: "end",
163
+ id
164
+ });
165
+ } catch (e) {
166
+ if (e.name !== "AbortError") send(dc, {
167
+ t: "end",
168
+ id,
169
+ error: String(e)
170
+ });
171
+ } finally {
172
+ aborts.delete(id);
173
+ }
174
+ }
175
+ await new Promise((resolve) => connectSignaling(resolve));
176
+ return {
177
+ room,
178
+ link
179
+ };
180
+ }
181
+
182
+ //#endregion
183
+ export { startShare };
184
+ //# sourceMappingURL=share-DUhUA1Pi.js.map
@@ -1,5 +1,5 @@
1
1
  import { i as readGlobalPids } from "./globalPidIndex-yVd3mbsV.js";
2
- import { a as resolveRemoteSpec, i as readRemotes } from "./remotes-Bjp2GYPz.js";
2
+ import { a as resolveRemoteSpec, i as readRemotes } from "./remotes-C3xPRtfg.js";
3
3
  import ms from "ms";
4
4
  import yargs from "yargs";
5
5
  import { appendFile, mkdir, open, readFile, stat, writeFile } from "fs/promises";
@@ -162,11 +162,11 @@ async function runSubcommand(argv) {
162
162
  case "restart": return await cmdRestart(rest);
163
163
  case "note": return await cmdNote(rest);
164
164
  case "serve": {
165
- const { cmdServe } = await import("./serve-CixOwENN.js");
165
+ const { cmdServe } = await import("./serve-CuAPBK4y.js");
166
166
  return cmdServe(rest);
167
167
  }
168
168
  case "remote": {
169
- const { cmdRemote } = await import("./remotes-oNI1fR7_.js");
169
+ const { cmdRemote } = await import("./remotes-C9WMt5PY.js");
170
170
  return cmdRemote(rest);
171
171
  }
172
172
  case "help": return cmdHelp();
@@ -1447,4 +1447,4 @@ async function cmdStatus(rest) {
1447
1447
 
1448
1448
  //#endregion
1449
1449
  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-D9wmaZ3U.js.map
1450
+ //# sourceMappingURL=subcommands-CcOYsLYD.js.map
@@ -1,6 +1,6 @@
1
1
  import "./logger-B9h0djqx.js";
2
2
  import "./globalPidIndex-yVd3mbsV.js";
3
- import "./remotes-Bjp2GYPz.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-D9wmaZ3U.js";
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";
5
5
 
6
6
  export { cmdHelp, isSubcommand, runSubcommand };
@@ -175,4 +175,4 @@ async function startTray() {
175
175
 
176
176
  //#endregion
177
177
  export { ensureTray, startTray };
178
- //# sourceMappingURL=tray-DHuD0nEk.js.map
178
+ //# sourceMappingURL=tray-D4cJA4UH.js.map
@@ -1,5 +1,5 @@
1
1
  import { n as logger, t as addTransport } from "./logger-B9h0djqx.js";
2
- import { r as getInstalledPackage } from "./versionChecker-BY5g27iW.js";
2
+ import { r as getInstalledPackage } from "./versionChecker-xqnqyGKE.js";
3
3
  import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-C22d9SRJ.js";
4
4
  import { t as PidStore } from "./pidStore-DTzl6zeh.js";
5
5
  import { i as readGlobalPids } from "./globalPidIndex-yVd3mbsV.js";
@@ -1705,4 +1705,4 @@ function sleep(ms) {
1705
1705
 
1706
1706
  //#endregion
1707
1707
  export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
1708
- //# sourceMappingURL=ts-DtwVuD8n.js.map
1708
+ //# sourceMappingURL=ts-DkjQJTcB.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.94.2";
10
+ var version = "1.96.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-BY5g27iW.js.map
224
+ //# sourceMappingURL=versionChecker-xqnqyGKE.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.94.2",
3
+ "version": "1.96.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",
@@ -69,6 +69,7 @@
69
69
  },
70
70
  "scripts": {
71
71
  "build": "tsdown",
72
+ "cf": "bun scripts/cf.ts",
72
73
  "build:rs": "cargo install --path rs --features swarm",
73
74
  "postbuild": "bun ./ts/postbuild.ts",
74
75
  "demo": "bun run build && bun link && claude-yes -- demo",
@@ -92,6 +93,7 @@
92
93
  "execa": "^9.6.1",
93
94
  "from-node-stream": "^0.2.0",
94
95
  "ms": "^2.1.3",
96
+ "node-datachannel": "^0.32.3",
95
97
  "phpdie": "^1.7.0",
96
98
  "proper-lockfile": "^4.1.2",
97
99
  "sflow": "^1.27.0",
@@ -142,5 +144,7 @@
142
144
  "engines": {
143
145
  "node": ">=22.0.0"
144
146
  },
145
- "trustedDependencies": []
147
+ "trustedDependencies": [
148
+ "node-datachannel"
149
+ ]
146
150
  }
package/scripts/cf.ts ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bun
2
+ // Thin wrapper that runs `wrangler` against the SNOLAB Cloudflare account using
3
+ // the API token saved in .env.local — so we never depend on `wrangler login`
4
+ // state (which points at a different account) and never pass the token on the
5
+ // CLI. Usage: bun scripts/cf.ts <wrangler args...>
6
+ // e.g. bun scripts/cf.ts whoami
7
+ // bun scripts/cf.ts pages deploy ./dist --project-name agent-yes
8
+ import { spawnSync } from "node:child_process";
9
+ import { existsSync, readFileSync, renameSync, rmSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import path from "node:path";
12
+
13
+ // SNOLAB account — agent-yes.com lives here. Account id is not a secret.
14
+ const SNOLAB_ACCOUNT_ID = "0beef4cd2d2da6befa47d8d149d6e157";
15
+
16
+ const root = path.join(import.meta.dir, "..");
17
+ const env: Record<string, string | undefined> = { ...process.env };
18
+
19
+ // Load .env.local (bun also auto-loads it, but be explicit so this works from
20
+ // any cwd and is obvious to a reader).
21
+ try {
22
+ for (const line of readFileSync(path.join(root, ".env.local"), "utf8").split("\n")) {
23
+ const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*?)\s*$/);
24
+ if (m && !(m[1] in env)) env[m[1]] = m[2];
25
+ }
26
+ } catch {
27
+ /* no .env.local — fall through to the check below */
28
+ }
29
+
30
+ if (!env.CLOUDFLARE_API_TOKEN) {
31
+ console.error("CLOUDFLARE_API_TOKEN is missing — add it to .env.local (see scripts/cf.ts).");
32
+ process.exit(1);
33
+ }
34
+ env.CLOUDFLARE_ACCOUNT_ID = SNOLAB_ACCOUNT_ID;
35
+
36
+ // wrangler otherwise prefers a stored OAuth login over CLOUDFLARE_API_TOKEN and
37
+ // pins the OAuth account (Axon), ignoring CLOUDFLARE_ACCOUNT_ID. Two sources to
38
+ // neutralise: the global OAuth config (~/.wrangler) and a project-level account
39
+ // cache (.wrangler/wrangler-account.json) that pins whatever account first
40
+ // deployed. Drop the cache, move the OAuth config aside for the run, restore it.
41
+ rmSync(path.join(root, ".wrangler/wrangler-account.json"), { force: true });
42
+ const oauthCfg = path.join(homedir(), ".wrangler/config/default.toml");
43
+ const oauthBak = oauthCfg + ".cf-bak";
44
+ const hadOauth = existsSync(oauthCfg);
45
+ if (hadOauth) renameSync(oauthCfg, oauthBak);
46
+ try {
47
+ const r = spawnSync("bunx", ["wrangler", ...process.argv.slice(2)], { stdio: "inherit", env });
48
+ process.exitCode = r.status ?? 1;
49
+ } finally {
50
+ if (hadOauth && existsSync(oauthBak)) renameSync(oauthBak, oauthCfg);
51
+ }
package/ts/rustBinary.ts CHANGED
@@ -41,10 +41,7 @@ export function getBinaryName(): string {
41
41
  */
42
42
  export function getBinDir(): string {
43
43
  // First check for binaries in the npm package
44
- const packageBinDir = path.resolve(
45
- import.meta.dirname ?? import.meta.dir,
46
- "../bin",
47
- );
44
+ const packageBinDir = path.resolve(import.meta.dirname ?? import.meta.dir, "../bin");
48
45
  if (existsSync(packageBinDir)) {
49
46
  return packageBinDir;
50
47
  }
@@ -61,8 +58,7 @@ export function getBinDir(): string {
61
58
  const cacheDir =
62
59
  process.env.AGENT_YES_CACHE_DIR ||
63
60
  path.join(
64
- process.env.XDG_CACHE_HOME ||
65
- path.join(process.env.HOME || "/tmp", ".cache"),
61
+ process.env.XDG_CACHE_HOME || path.join(process.env.HOME || "/tmp", ".cache"),
66
62
  "agent-yes",
67
63
  );
68
64
 
@@ -78,14 +74,8 @@ export function findRustBinary(verbose = false): string | undefined {
78
74
  const ext = process.platform === "win32" ? ".exe" : "";
79
75
  const searchPaths = [
80
76
  // 1. Check relative to this script (in the repo during development)
81
- path.resolve(
82
- import.meta.dirname ?? import.meta.dir,
83
- `../rs/target/release/agent-yes${ext}`,
84
- ),
85
- path.resolve(
86
- import.meta.dirname ?? import.meta.dir,
87
- `../rs/target/debug/agent-yes${ext}`,
88
- ),
77
+ path.resolve(import.meta.dirname ?? import.meta.dir, `../rs/target/release/agent-yes${ext}`),
78
+ path.resolve(import.meta.dirname ?? import.meta.dir, `../rs/target/debug/agent-yes${ext}`),
89
79
 
90
80
  // 2. Check in npm package bin directory
91
81
  path.join(getBinDir(), binaryName),
@@ -149,9 +139,7 @@ export async function downloadBinary(verbose = false): Promise<string> {
149
139
 
150
140
  const response = await fetch(url);
151
141
  if (!response.ok) {
152
- throw new Error(
153
- `Failed to download binary: ${response.status} ${response.statusText}`,
154
- );
142
+ throw new Error(`Failed to download binary: ${response.status} ${response.statusText}`);
155
143
  }
156
144
 
157
145
  const isWindows = process.platform === "win32";
@@ -243,19 +231,14 @@ function getRustBinaryVersion(binaryPath: string): string | null {
243
231
  */
244
232
  function autoRebuildIfOutdated(binaryPath: string, verbose: boolean): boolean {
245
233
  // Only auto-rebuild for local dev builds (target/release or target/debug)
246
- if (
247
- !binaryPath.includes("/target/release") &&
248
- !binaryPath.includes("/target/debug")
249
- ) {
234
+ if (!binaryPath.includes("/target/release") && !binaryPath.includes("/target/debug")) {
250
235
  return true; // not a dev build, skip
251
236
  }
252
237
 
253
238
  const binaryVersion = getRustBinaryVersion(binaryPath);
254
239
  const pkgVersion = getInstalledPackage().version;
255
240
  if (verbose) {
256
- console.log(
257
- `[rust] Binary version: ${binaryVersion}, package version: ${pkgVersion}`,
258
- );
241
+ console.log(`[rust] Binary version: ${binaryVersion}, package version: ${pkgVersion}`);
259
242
  }
260
243
 
261
244
  if (binaryVersion === pkgVersion) {
@@ -263,15 +246,9 @@ function autoRebuildIfOutdated(binaryPath: string, verbose: boolean): boolean {
263
246
  }
264
247
 
265
248
  // Find the rs/ directory relative to the binary (binary is at rs/target/release/agent-yes)
266
- const rsDir = binaryPath.replace(
267
- /\/target\/(release|debug)\/agent-yes.*$/,
268
- "",
269
- );
249
+ const rsDir = binaryPath.replace(/\/target\/(release|debug)\/agent-yes.*$/, "");
270
250
  if (!existsSync(path.join(rsDir, "Cargo.toml"))) {
271
- if (verbose)
272
- console.log(
273
- `[rust] Cannot find Cargo.toml at ${rsDir}, skipping rebuild`,
274
- );
251
+ if (verbose) console.log(`[rust] Cannot find Cargo.toml at ${rsDir}, skipping rebuild`);
275
252
  return true; // can't rebuild, use as-is
276
253
  }
277
254
 
@@ -299,9 +276,7 @@ function autoRebuildIfOutdated(binaryPath: string, verbose: boolean): boolean {
299
276
  process.stderr.write(`\x1b[32m[rust] Rebuild complete\x1b[0m\n`);
300
277
  return true;
301
278
  } catch {
302
- process.stderr.write(
303
- `\x1b[31m[rust] Auto-rebuild failed, using outdated binary\x1b[0m\n`,
304
- );
279
+ process.stderr.write(`\x1b[31m[rust] Auto-rebuild failed, using outdated binary\x1b[0m\n`);
305
280
  return true; // still usable, just old
306
281
  }
307
282
  }
package/ts/serve.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  writeToIpc,
14
14
  type CommonOpts,
15
15
  } from "./subcommands.ts";
16
+ import { SUPPORTED_CLIS } from "./SUPPORTED_CLIS.ts";
16
17
 
17
18
  const DEFAULT_PORT = 7432;
18
19
 
@@ -123,6 +124,8 @@ export async function cmdServe(rest: string[]): Promise<number> {
123
124
  ` --port N Port to listen on (default: ${DEFAULT_PORT})\n` +
124
125
  ` --host HOST Interface to bind (default: 127.0.0.1; use 0.0.0.0 to expose)\n` +
125
126
  ` --token TOKEN Auth token (auto-generated and saved if omitted)\n` +
127
+ ` --share [URL] Share over WebRTC to agent-yes.com (bare flag mints a room+link)\n` +
128
+ ` --allow-spawn Let the shared console launch new agents (asks y/N per request)\n` +
126
129
  ` --tls-cert FILE TLS certificate PEM\n` +
127
130
  ` --tls-key FILE TLS private key PEM\n\n` +
128
131
  `Subcommands:\n` +
@@ -153,6 +156,16 @@ export async function cmdServe(rest: string[]): Promise<number> {
153
156
  .option("token", { type: "string", description: "Auth token (auto-generated if omitted)" })
154
157
  .option("tls-cert", { type: "string", description: "TLS certificate file (PEM)" })
155
158
  .option("tls-key", { type: "string", description: "TLS private key file (PEM)" })
159
+ .option("share", {
160
+ type: "string",
161
+ description:
162
+ "Share over WebRTC: bare flag mints a room+link, or pass webrtc://room:token@host",
163
+ })
164
+ .option("allow-spawn", {
165
+ type: "boolean",
166
+ default: false,
167
+ description: "Allow the shared console to spawn new agents (asks y/N per request on a TTY)",
168
+ })
156
169
  .help(false)
157
170
  .version(false)
158
171
  .exitProcess(false);
@@ -178,6 +191,26 @@ export async function cmdServe(rest: string[]): Promise<number> {
178
191
  }
179
192
 
180
193
  const token = await loadOrCreateToken(tokenFlag);
194
+ const allowSpawn = argv["allow-spawn"] === true;
195
+
196
+ // Spawn confirmation: launch requests are gated by --allow-spawn AND, on a TTY,
197
+ // an interactive y/N per request (a leaked launch link can't silently spawn).
198
+ const spawnQueue: Array<(ok: boolean) => void> = [];
199
+ let stdinWired = false;
200
+ const confirmSpawn = (cli: string, cwd: string, prompt: string): Promise<boolean> => {
201
+ if (!process.stdin.isTTY) return Promise.resolve(true); // flag is the consent when headless
202
+ if (!stdinWired) {
203
+ stdinWired = true;
204
+ process.stdin.setEncoding("utf8");
205
+ process.stdin.on("data", (d: string) => spawnQueue.shift()?.(/^y/i.test(d.trim())));
206
+ process.stdin.resume();
207
+ }
208
+ process.stdout.write(
209
+ `\n⚠ console requests spawn: ay ${cli}${prompt ? ` -- "${prompt.slice(0, 60)}"` : ""}\n` +
210
+ ` cwd: ${cwd}\n allow? [y/N] `,
211
+ );
212
+ return new Promise((res) => spawnQueue.push(res));
213
+ };
181
214
 
182
215
  const serverOpts: any = {
183
216
  hostname: host,
@@ -246,6 +279,9 @@ export async function cmdServe(rest: string[]): Promise<number> {
246
279
  const tailM = /^\/api\/tail\/(.+)$/.exec(p);
247
280
  if (req.method === "GET" && tailM) {
248
281
  const keyword = decodeURIComponent(tailM[1]!);
282
+ // raw=1 streams the unmodified PTY bytes (ANSI/cursor control intact) so a
283
+ // browser xterm.js can render the real terminal; default stays ANSI-stripped.
284
+ const raw = url.searchParams.get("raw") === "1";
249
285
  try {
250
286
  const record = await resolveOne(keyword, defaultOpts());
251
287
  if (!record.log_file)
@@ -259,10 +295,12 @@ export async function cmdServe(rest: string[]): Promise<number> {
259
295
  ctrl.enqueue(enc.encode(`data: ${JSON.stringify(text)}\n\n`));
260
296
  const ping = () => ctrl.enqueue(enc.encode(": ping\n\n"));
261
297
 
262
- // Initial tail
298
+ // Initial tail. Raw: replay the last ~64 KB of PTY bytes (enough to
299
+ // contain a recent full-screen redraw so xterm converges fast).
263
300
  const initBuf = await readFile(logPath).catch(() => Buffer.alloc(0));
264
- const initText = await renderRawLog(initBuf, { mode: "tail", n: 96 });
265
- send(initText);
301
+ if (raw)
302
+ send(new TextDecoder().decode(initBuf.slice(Math.max(0, initBuf.length - 65536))));
303
+ else send(await renderRawLog(initBuf, { mode: "tail", n: 96 }));
266
304
 
267
305
  let offset = initBuf.length;
268
306
  let closed = false;
@@ -291,11 +329,15 @@ export async function cmdServe(rest: string[]): Promise<number> {
291
329
  if (full.length <= offset) return;
292
330
  const chunk = full.slice(offset);
293
331
  offset = full.length;
294
- const text = new TextDecoder()
295
- .decode(chunk)
296
- .replace(ansiRe, "")
297
- .replace(ctrlRe, "");
298
- if (text.trim()) send(text.trimStart());
332
+ if (raw) {
333
+ send(new TextDecoder().decode(chunk));
334
+ } else {
335
+ const text = new TextDecoder()
336
+ .decode(chunk)
337
+ .replace(ansiRe, "")
338
+ .replace(ctrlRe, "");
339
+ if (text.trim()) send(text.trimStart());
340
+ }
299
341
  } catch {
300
342
  /* log gone */
301
343
  }
@@ -356,6 +398,74 @@ export async function cmdServe(rest: string[]): Promise<number> {
356
398
  }
357
399
  }
358
400
 
401
+ // POST /api/resize/:keyword body {cols, rows} — drive the agent's PTY size.
402
+ // Mirrors `ay attach`: write ~/.agent-yes/winsize/<pid> then SIGWINCH; the
403
+ // agent's resize listener picks it up and reflows its TUI to that width.
404
+ const resizeM = /^\/api\/resize\/(.+)$/.exec(p);
405
+ if (req.method === "POST" && resizeM) {
406
+ const keyword = decodeURIComponent(resizeM[1]!);
407
+ let body: { cols?: number; rows?: number };
408
+ try {
409
+ body = await req.json();
410
+ } catch {
411
+ return new Response("invalid JSON body", { status: 400 });
412
+ }
413
+ const cols = Math.max(1, Math.floor(Number(body.cols) || 0));
414
+ const rows = Math.max(1, Math.floor(Number(body.rows) || 0));
415
+ if (!cols || !rows) return new Response("missing cols/rows", { status: 400 });
416
+ try {
417
+ const record = await resolveOne(keyword, defaultOpts());
418
+ const ayHome = process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
419
+ const winsizeDir = path.join(ayHome, "winsize");
420
+ await mkdir(winsizeDir, { recursive: true });
421
+ await writeFile(
422
+ path.join(winsizeDir, String(record.pid)),
423
+ `${cols} ${rows} ${Date.now()}\n`,
424
+ );
425
+ try {
426
+ process.kill(record.pid, "SIGWINCH");
427
+ } catch {
428
+ /* agent gone */
429
+ }
430
+ return Response.json({ ok: true, pid: record.pid, cols, rows });
431
+ } catch (e) {
432
+ return new Response((e as Error).message, { status: 404 });
433
+ }
434
+ }
435
+
436
+ // POST /api/spawn body {cli, cwd, prompt} — launch a new agent (gated)
437
+ if (req.method === "POST" && p === "/api/spawn") {
438
+ if (!allowSpawn)
439
+ return new Response("spawning disabled — start: ay serve --share --allow-spawn", {
440
+ status: 403,
441
+ });
442
+ let body: { cli?: string; cwd?: string; prompt?: string };
443
+ try {
444
+ body = await req.json();
445
+ } catch {
446
+ return new Response("invalid JSON body", { status: 400 });
447
+ }
448
+ const cli = String(body.cli ?? "claude");
449
+ if (!SUPPORTED_CLIS.includes(cli as never))
450
+ return new Response(`unsupported cli: ${cli}`, { status: 400 });
451
+ const cwd = typeof body.cwd === "string" && body.cwd ? body.cwd : process.cwd();
452
+ const prompt = String(body.prompt ?? "");
453
+ if (!(await confirmSpawn(cli, cwd, prompt)))
454
+ return new Response("denied by host", { status: 403 });
455
+ try {
456
+ const child = Bun.spawn(["ay", cli, ...(prompt ? ["--", prompt] : [])], {
457
+ cwd,
458
+ stdin: "ignore",
459
+ stdout: "ignore",
460
+ stderr: "ignore",
461
+ });
462
+ child.unref();
463
+ return Response.json({ ok: true, pid: child.pid, cli, cwd });
464
+ } catch (e) {
465
+ return new Response((e as Error).message, { status: 500 });
466
+ }
467
+ }
468
+
359
469
  return new Response("Not Found", { status: 404 });
360
470
  },
361
471
  };
@@ -380,6 +490,27 @@ export async function cmdServe(rest: string[]): Promise<number> {
380
490
  ` openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=localhost'\n\n`,
381
491
  );
382
492
  }
493
+ // --share: bridge this local server to a WebRTC room so the agent-yes.com
494
+ // console can reach it peer-to-peer. Bare flag mints a room; a webrtc:// value
495
+ // joins an explicit one.
496
+ if (argv.share !== undefined) {
497
+ const shareUrl =
498
+ typeof argv.share === "string" && argv.share.startsWith("webrtc://") ? argv.share : undefined;
499
+ try {
500
+ const { startShare } = await import("./share.ts");
501
+ const { link } = await startShare({
502
+ url: shareUrl,
503
+ apiUrl: `http://127.0.0.1:${port}`,
504
+ apiToken: token,
505
+ });
506
+ process.stdout.write(
507
+ `\nshared over WebRTC — open this link (the token is eaten from the URL on open):\n ${link}\n\n`,
508
+ );
509
+ } catch (e) {
510
+ process.stderr.write(`ay serve --share failed: ${(e as Error).message}\n`);
511
+ }
512
+ }
513
+
383
514
  process.stdout.write(`(Ctrl-C to stop)\n`);
384
515
 
385
516
  await new Promise<void>((resolve) => {
package/ts/share.ts ADDED
@@ -0,0 +1,190 @@
1
+ // `ay serve --share` host peer: connect to the signaling server as a room host
2
+ // and bridge each browser peer's WebRTC DataChannel to this machine's local
3
+ // `ay serve` HTTP API. The browser (agent-yes.com) thus reaches local agents
4
+ // peer-to-peer — no public port, no tunnel. See lab/ui/cf/worker.ts for the
5
+ // signaling protocol and lab/ui/index.html for the browser side.
6
+ import { randomBytes } from "crypto";
7
+
8
+ const SUB = "ay-signal-1";
9
+ const ICE = [{ urls: "stun:stun.l.google.com:19302" }];
10
+ const MAX_CHUNK = 15_000; // keep DataChannel messages under the SCTP limit
11
+ const DEFAULT_SIGHOST = "s.agent-yes.com";
12
+
13
+ export interface ShareOpts {
14
+ /** webrtc://room:token@host, or undefined to mint a fresh room+token */
15
+ url?: string;
16
+ /** signaling host when minting (default s.agent-yes.com) */
17
+ sighost?: string;
18
+ /** local ay-serve base URL the channel bridges to */
19
+ apiUrl: string;
20
+ /** bearer token for the local ay-serve API */
21
+ apiToken: string;
22
+ }
23
+
24
+ function parseShareUrl(s: string): { room: string; token: string; host: string } {
25
+ const m = /^webrtc:\/\/([^:@/]+):([^@/]+)@(.+)$/.exec(s);
26
+ if (!m) throw new Error(`bad --share url: ${s} (want webrtc://room:token@host)`);
27
+ return { room: m[1]!, token: m[2]!, host: m[3]! };
28
+ }
29
+
30
+ // node-datachannel ships a native addon. Under Bun the module sometimes resolves
31
+ // from the global cache where the prebuilt .node isn't linked; this best-effort
32
+ // shim symlinks it in before we import. In a normal npm/bunx install the binary
33
+ // resolves from node_modules and the first import just works.
34
+ async function importRTC(): Promise<any> {
35
+ try {
36
+ return (await import("node-datachannel/polyfill")).RTCPeerConnection;
37
+ } catch {
38
+ try {
39
+ const { existsSync, symlinkSync, mkdirSync, readdirSync } = await import("fs");
40
+ const path = (await import("path")).default;
41
+ const { createRequire } = await import("module");
42
+ const require = createRequire(import.meta.url);
43
+ const pkg = path.dirname(require.resolve("node-datachannel/package.json"));
44
+ const bin = path.join(pkg, "build", "Release", "node_datachannel.node");
45
+ const cacheRoot = path.join((await import("os")).homedir(), ".bun", "install", "cache");
46
+ if (existsSync(bin) && existsSync(cacheRoot)) {
47
+ for (const d of readdirSync(cacheRoot)) {
48
+ if (!d.startsWith("node-datachannel@")) continue;
49
+ const dst = path.join(cacheRoot, d, "build", "Release");
50
+ mkdirSync(dst, { recursive: true });
51
+ const link = path.join(dst, "node_datachannel.node");
52
+ if (!existsSync(link)) symlinkSync(bin, link);
53
+ }
54
+ }
55
+ } catch {
56
+ /* fall through — rethrow the original import error below */
57
+ }
58
+ return (await import("node-datachannel/polyfill")).RTCPeerConnection;
59
+ }
60
+ }
61
+
62
+ /** Start the share bridge. Resolves once signaling is connected; runs until the
63
+ * process exits, reconnecting signaling on drop. Returns the shareable link. */
64
+ export async function startShare(opts: ShareOpts): Promise<{ room: string; link: string }> {
65
+ const minted = !opts.url;
66
+ const sighost = opts.sighost ?? DEFAULT_SIGHOST;
67
+ const { room, token, host } = opts.url
68
+ ? parseShareUrl(opts.url)
69
+ : {
70
+ room: "r" + randomBytes(3).toString("hex"),
71
+ token: randomBytes(32).toString("hex"),
72
+ host: sighost,
73
+ };
74
+
75
+ const RTCPeerConnection = await importRTC();
76
+ const wsScheme = host.startsWith("localhost") || host.startsWith("127.") ? "ws" : "wss";
77
+ const ui = host === "s.agent-yes.com" ? "https://agent-yes.com" : "http://localhost:7778";
78
+ const suffix = host === "s.agent-yes.com" ? "" : "@" + host;
79
+ const link = `${ui}/#${room}:${token}${suffix}`;
80
+
81
+ type Peer = { pc: any; aborts: Map<number, AbortController> };
82
+ const peers = new Map<string, Peer>();
83
+
84
+ const connectSignaling = (onReady: () => void) => {
85
+ const ws = new WebSocket(`${wsScheme}://${host}/${room}`, [SUB]);
86
+ let ready = false;
87
+ ws.onopen = () => {
88
+ ws.send(JSON.stringify({ type: "hello", role: "host", token }));
89
+ ready = true;
90
+ onReady();
91
+ };
92
+ ws.onmessage = async (ev) => {
93
+ const m = JSON.parse(ev.data as string);
94
+ if (m.type === "peer-join") startPeer(ws, m.peer);
95
+ else if (m.type === "answer")
96
+ await peers.get(m.from)?.pc.setRemoteDescription({ type: "answer", sdp: m.sdp });
97
+ else if (m.type === "candidate")
98
+ await peers
99
+ .get(m.from)
100
+ ?.pc.addIceCandidate(m.candidate)
101
+ .catch(() => {});
102
+ else if (m.type === "peer-leave") closePeer(m.peer);
103
+ };
104
+ ws.onclose = () => {
105
+ // Keep established WebRTC peers; just re-establish the rendezvous so new
106
+ // browsers can still join. Backoff a little to avoid hot-looping.
107
+ setTimeout(() => connectSignaling(() => {}), ready ? 1500 : 4000);
108
+ };
109
+ ws.onerror = () => {};
110
+ return ws;
111
+ };
112
+
113
+ function startPeer(ws: WebSocket, peerId: string) {
114
+ const pc = new RTCPeerConnection({ iceServers: ICE });
115
+ const aborts = new Map<number, AbortController>();
116
+ peers.set(peerId, { pc, aborts });
117
+ pc.onicecandidate = (e: any) => {
118
+ if (e.candidate)
119
+ ws.send(JSON.stringify({ type: "candidate", to: peerId, candidate: e.candidate }));
120
+ };
121
+ pc.onconnectionstatechange = () => {
122
+ if (["failed", "closed", "disconnected"].includes(pc.connectionState)) closePeer(peerId);
123
+ };
124
+ const dc = pc.createDataChannel("api");
125
+ dc.onmessage = (e: any) => onReq(dc, aborts, JSON.parse(e.data));
126
+ pc.createOffer()
127
+ .then((o: any) => pc.setLocalDescription(o))
128
+ .then(() =>
129
+ ws.send(JSON.stringify({ type: "offer", to: peerId, sdp: pc.localDescription.sdp })),
130
+ );
131
+ }
132
+
133
+ function closePeer(peerId: string) {
134
+ const p = peers.get(peerId);
135
+ if (!p) return;
136
+ for (const a of p.aborts.values()) a.abort();
137
+ try {
138
+ p.pc.close();
139
+ } catch {
140
+ /* already closed */
141
+ }
142
+ peers.delete(peerId);
143
+ }
144
+
145
+ function send(dc: any, obj: object) {
146
+ if (dc.readyState === "open") dc.send(JSON.stringify(obj));
147
+ }
148
+
149
+ async function onReq(dc: any, aborts: Map<number, AbortController>, req: any) {
150
+ if (req.t === "abort") {
151
+ aborts.get(req.id)?.abort();
152
+ aborts.delete(req.id);
153
+ return;
154
+ }
155
+ if (req.t !== "req") return;
156
+ const { id, method, path: p, body } = req;
157
+ const ac = new AbortController();
158
+ aborts.set(id, ac);
159
+ try {
160
+ const res = await fetch(opts.apiUrl + p, {
161
+ method,
162
+ headers: {
163
+ Authorization: `Bearer ${opts.apiToken}`,
164
+ ...(body ? { "Content-Type": "application/json" } : {}),
165
+ },
166
+ body: body ?? undefined,
167
+ signal: ac.signal,
168
+ });
169
+ send(dc, { t: "res", id, status: res.status, ct: res.headers.get("content-type") ?? "" });
170
+ const reader = res.body!.getReader();
171
+ const dec = new TextDecoder();
172
+ for (;;) {
173
+ const { done, value } = await reader.read();
174
+ if (done) break;
175
+ const text = dec.decode(value, { stream: true });
176
+ for (let i = 0; i < text.length; i += MAX_CHUNK)
177
+ send(dc, { t: "data", id, chunk: text.slice(i, i + MAX_CHUNK) });
178
+ }
179
+ send(dc, { t: "end", id });
180
+ } catch (e) {
181
+ if ((e as Error).name !== "AbortError") send(dc, { t: "end", id, error: String(e) });
182
+ } finally {
183
+ aborts.delete(id);
184
+ }
185
+ }
186
+
187
+ await new Promise<void>((resolve) => connectSignaling(resolve));
188
+ void minted; // (informational) caller decides how to surface the link
189
+ return { room, link };
190
+ }
@@ -1,12 +0,0 @@
1
- import { t as CLIS_CONFIG } from "./ts-DtwVuD8n.js";
2
- import "./logger-B9h0djqx.js";
3
- import "./versionChecker-BY5g27iW.js";
4
- import "./pidStore-DTzl6zeh.js";
5
- import "./globalPidIndex-yVd3mbsV.js";
6
-
7
- //#region ts/SUPPORTED_CLIS.ts
8
- const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
9
-
10
- //#endregion
11
- export { SUPPORTED_CLIS };
12
- //# sourceMappingURL=SUPPORTED_CLIS-Bo4qbT_0.js.map