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.
package/ts/share.ts CHANGED
@@ -1,8 +1,9 @@
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.
1
+ // `ay serve --webrtc` host peer: connect to the signaling server as a room host
2
+ // and bridge each browser peer's WebRTC DataChannel to this machine's `ay serve`
3
+ // API handler, called in-process no HTTP listener, no port, no tunnel. The
4
+ // browser (agent-yes.com) thus reaches local agents peer-to-peer. See
5
+ // lab/ui/cf/worker.ts for the signaling protocol and lab/ui/index.html for the
6
+ // browser side.
6
7
  import { randomBytes } from "crypto";
7
8
 
8
9
  const SUB = "ay-signal-1";
@@ -15,8 +16,8 @@ export interface ShareOpts {
15
16
  url?: string;
16
17
  /** signaling host when minting (default s.agent-yes.com) */
17
18
  sighost?: string;
18
- /** local ay-serve base URL the channel bridges to */
19
- apiUrl: string;
19
+ /** the local ay-serve API handler the channel bridges to (called in-process) */
20
+ localFetch: (req: Request) => Promise<Response>;
20
21
  /** bearer token for the local ay-serve API */
21
22
  apiToken: string;
22
23
  }
@@ -157,15 +158,18 @@ export async function startShare(opts: ShareOpts): Promise<{ room: string; link:
157
158
  const ac = new AbortController();
158
159
  aborts.set(id, ac);
159
160
  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
- });
161
+ // The host part is a placeholder — the handler only routes on the path.
162
+ const res = await opts.localFetch(
163
+ new Request(`http://ay.local${p}`, {
164
+ method,
165
+ headers: {
166
+ Authorization: `Bearer ${opts.apiToken}`,
167
+ ...(body ? { "Content-Type": "application/json" } : {}),
168
+ },
169
+ body: body ?? undefined,
170
+ signal: ac.signal,
171
+ }),
172
+ );
169
173
  send(dc, { t: "res", id, status: res.status, ct: res.headers.get("content-type") ?? "" });
170
174
  const reader = res.body!.getReader();
171
175
  const dec = new TextDecoder();
package/ts/subcommands.ts CHANGED
@@ -141,6 +141,7 @@ const SUBCOMMANDS = new Set([
141
141
  "restart",
142
142
  "note",
143
143
  "serve",
144
+ "setup",
144
145
  "remote",
145
146
  "help",
146
147
  ]);
@@ -190,6 +191,10 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
190
191
  const { cmdServe } = await import("./serve.ts");
191
192
  return cmdServe(rest);
192
193
  }
194
+ case "setup": {
195
+ const { cmdSetup } = await import("./setup.ts");
196
+ return cmdSetup(rest);
197
+ }
193
198
  case "remote": {
194
199
  const { cmdRemote } = await import("./remotes.ts");
195
200
  return cmdRemote(rest);
@@ -225,6 +230,7 @@ export function cmdHelp(): number {
225
230
  ` ay status <keyword> agent status snapshot\n` +
226
231
  `\n` +
227
232
  `Remote:\n` +
233
+ ` ay setup guided setup: pick a workspace, share to agent-yes.com\n` +
228
234
  ` ay serve [--port N] start HTTP API server (prints token)\n` +
229
235
  ` ay remote add <alias> http://<token>@<host>:<port>\n` +
230
236
  ` ay remote ls / rm <alias> manage saved remotes\n` +
@@ -1,8 +0,0 @@
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 };
@@ -1,5 +0,0 @@
1
- import "./logger-B9h0djqx.js";
2
- import { t as PidStore } from "./pidStore-DTzl6zeh.js";
3
- import "./globalPidIndex-yVd3mbsV.js";
4
-
5
- export { PidStore };
@@ -1,425 +0,0 @@
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
- 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";
9
- import yargs from "yargs";
10
- import { mkdir, readFile, writeFile } from "fs/promises";
11
- import { homedir } from "os";
12
- import path from "path";
13
- import { randomBytes, timingSafeEqual } from "crypto";
14
-
15
- //#region ts/serve.ts
16
- const DEFAULT_PORT = 7432;
17
- function agentYesHome() {
18
- return process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
19
- }
20
- function tokenPath() {
21
- return path.join(agentYesHome(), ".serve-token");
22
- }
23
- async function loadOrCreateToken(tokenFlag) {
24
- if (tokenFlag) return tokenFlag;
25
- try {
26
- return (await readFile(tokenPath(), "utf-8")).trim();
27
- } catch {
28
- const token = randomBytes(20).toString("hex");
29
- await mkdir(agentYesHome(), { recursive: true });
30
- await writeFile(tokenPath(), token, { mode: 384 });
31
- return token;
32
- }
33
- }
34
- function checkAuth(req, expectedToken) {
35
- const auth = req.headers.get("authorization") ?? "";
36
- if (!auth.startsWith("Bearer ")) return false;
37
- const provided = auth.slice(7);
38
- const maxLen = Math.max(provided.length, expectedToken.length);
39
- return timingSafeEqual(Buffer.from(provided.padEnd(maxLen, "\0")), Buffer.from(expectedToken.padEnd(maxLen, "\0"))) && provided.length === expectedToken.length;
40
- }
41
- const defaultOpts = (overrides = {}) => ({
42
- all: false,
43
- active: false,
44
- json: true,
45
- latest: true,
46
- cwdScope: null,
47
- ...overrides
48
- });
49
- const DAEMON_NAME = "agent-yes";
50
- async function cmdServeDaemon(sub, args) {
51
- const oxmgrBin = Bun.which("oxmgr");
52
- if (!oxmgrBin) {
53
- process.stderr.write("ay serve install: oxmgr not found\n install with: cargo install oxmgr\n or: bun add -g oxmgr\n");
54
- return 1;
55
- }
56
- if (sub === "install") {
57
- const token = await loadOrCreateToken(void 0);
58
- const serveCmd = [
59
- "ay",
60
- "serve",
61
- ...args
62
- ].join(" ");
63
- const code = await Bun.spawn([
64
- oxmgrBin,
65
- "start",
66
- serveCmd,
67
- "--name",
68
- DAEMON_NAME,
69
- "--restart",
70
- "always"
71
- ], { stdio: [
72
- "ignore",
73
- "inherit",
74
- "inherit"
75
- ] }).exited;
76
- if (code === 0) {
77
- process.stdout.write(`\ninstalled '${DAEMON_NAME}' as a daemon via oxmgr\n`);
78
- process.stdout.write(`token: ${token}\n\n`);
79
- process.stdout.write(` ay ls ${token}@<host>:${DEFAULT_PORT}\n`);
80
- process.stdout.write(` ay remote add <alias> http://${token}@<host>:${DEFAULT_PORT}\n`);
81
- process.stdout.write(` ay serve logs # view server logs\n`);
82
- process.stdout.write(` ay serve uninstall # remove daemon\n`);
83
- }
84
- return code ?? 1;
85
- }
86
- if (sub === "uninstall") return await Bun.spawn([
87
- oxmgrBin,
88
- "delete",
89
- DAEMON_NAME
90
- ], { stdio: [
91
- "ignore",
92
- "inherit",
93
- "inherit"
94
- ] }).exited ?? 1;
95
- if (sub === "logs") return await Bun.spawn([
96
- oxmgrBin,
97
- "logs",
98
- DAEMON_NAME,
99
- ...args
100
- ], { stdio: [
101
- "ignore",
102
- "inherit",
103
- "inherit"
104
- ] }).exited ?? 1;
105
- return 1;
106
- }
107
- async function cmdServe(rest) {
108
- if (rest.includes("-h") || rest.includes("--help")) {
109
- process.stdout.write(`Usage: ay serve [options]
110
-
111
- Start an HTTP API server so remote machines can list/tail/send agents.
112
-
113
- Options:
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`);
115
- return 0;
116
- }
117
- const sub = rest[0];
118
- if (sub === "install" || sub === "uninstall" || sub === "logs") return cmdServeDaemon(sub, rest.slice(1));
119
- const argv = await yargs(rest).usage("Usage: ay serve [options]").option("port", {
120
- type: "number",
121
- default: DEFAULT_PORT,
122
- description: "Port to listen on"
123
- }).option("host", {
124
- type: "string",
125
- default: "127.0.0.1",
126
- description: "Interface to bind (use 0.0.0.0 to expose)"
127
- }).option("token", {
128
- type: "string",
129
- description: "Auth token (auto-generated if omitted)"
130
- }).option("tls-cert", {
131
- type: "string",
132
- description: "TLS certificate file (PEM)"
133
- }).option("tls-key", {
134
- type: "string",
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)"
143
- }).help(false).version(false).exitProcess(false).parseAsync();
144
- const port = argv.port ?? DEFAULT_PORT;
145
- const host = argv.host ?? "127.0.0.1";
146
- const tokenFlag = typeof argv.token === "string" ? argv.token : void 0;
147
- const certPath = typeof argv["tls-cert"] === "string" ? argv["tls-cert"] : void 0;
148
- const keyPath = typeof argv["tls-key"] === "string" ? argv["tls-key"] : void 0;
149
- if (certPath && !keyPath || !certPath && keyPath) {
150
- process.stderr.write("ay serve: --tls-cert and --tls-key must both be provided\n");
151
- return 1;
152
- }
153
- const useHttps = !!(certPath && keyPath);
154
- const scheme = useHttps ? "https" : "http";
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");
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
- };
171
- const serverOpts = {
172
- hostname: host,
173
- port,
174
- async fetch(req) {
175
- if (!checkAuth(req, token)) return new Response("Unauthorized", { status: 401 });
176
- const url = new URL(req.url);
177
- const p = url.pathname;
178
- if (req.method === "GET" && p === "/api/ls") {
179
- const keyword = url.searchParams.get("keyword") ?? void 0;
180
- const opts = defaultOpts({
181
- all: url.searchParams.get("all") === "1",
182
- active: url.searchParams.get("active") === "1"
183
- });
184
- try {
185
- const records = await listRecords(keyword, opts);
186
- return Response.json(records);
187
- } catch (e) {
188
- return new Response(e.message, { status: 500 });
189
- }
190
- }
191
- if (req.method === "GET" && p === "/api/notes") {
192
- const notes = await readNotes();
193
- return Response.json(Object.fromEntries(notes));
194
- }
195
- const statusM = /^\/api\/status\/(.+)$/.exec(p);
196
- if (req.method === "GET" && statusM) {
197
- const keyword = decodeURIComponent(statusM[1]);
198
- try {
199
- const snap = await snapshotStatus(await resolveOne(keyword, defaultOpts({ all: true })));
200
- return Response.json(snap);
201
- } catch (e) {
202
- return new Response(e.message, { status: 404 });
203
- }
204
- }
205
- const readM = /^\/api\/read\/(.+)$/.exec(p);
206
- if (req.method === "GET" && readM) {
207
- const keyword = decodeURIComponent(readM[1]);
208
- const mode = url.searchParams.get("mode") ?? "tail";
209
- const n = parseInt(url.searchParams.get("n") ?? "96", 10) || 96;
210
- try {
211
- const record = await resolveOne(keyword, defaultOpts());
212
- if (!record.log_file) return new Response(`pid ${record.pid}: no log_file`, { status: 404 });
213
- const text = await renderRawLog(await readFile(record.log_file), {
214
- mode,
215
- n
216
- });
217
- return new Response(text, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
218
- } catch (e) {
219
- return new Response(e.message, { status: 404 });
220
- }
221
- }
222
- const tailM = /^\/api\/tail\/(.+)$/.exec(p);
223
- if (req.method === "GET" && tailM) {
224
- const keyword = decodeURIComponent(tailM[1]);
225
- const raw = url.searchParams.get("raw") === "1";
226
- try {
227
- const record = await resolveOne(keyword, defaultOpts());
228
- if (!record.log_file) return new Response(`pid ${record.pid}: no log_file`, { status: 404 });
229
- const logPath = record.log_file;
230
- const stream = new ReadableStream({ async start(ctrl) {
231
- const enc = new TextEncoder();
232
- const send = (text) => ctrl.enqueue(enc.encode(`data: ${JSON.stringify(text)}\n\n`));
233
- const ping = () => ctrl.enqueue(enc.encode(": ping\n\n"));
234
- const initBuf = await readFile(logPath).catch(() => Buffer.alloc(0));
235
- if (raw) send(new TextDecoder().decode(initBuf.slice(Math.max(0, initBuf.length - 65536))));
236
- else send(await renderRawLog(initBuf, {
237
- mode: "tail",
238
- n: 96
239
- }));
240
- let offset = initBuf.length;
241
- let closed = false;
242
- const heartbeat = setInterval(() => {
243
- if (closed) {
244
- clearInterval(heartbeat);
245
- return;
246
- }
247
- ping();
248
- }, 15e3);
249
- const ansiRe = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-Z\\-_]/g;
250
- const ctrlRe = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
251
- const poller = setInterval(async () => {
252
- if (closed) {
253
- clearInterval(poller);
254
- return;
255
- }
256
- try {
257
- const full = await readFile(logPath);
258
- if (full.length <= offset) return;
259
- const chunk = full.slice(offset);
260
- offset = full.length;
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
- }
266
- } catch {}
267
- }, 300);
268
- req.signal.addEventListener("abort", () => {
269
- closed = true;
270
- clearInterval(heartbeat);
271
- clearInterval(poller);
272
- try {
273
- ctrl.close();
274
- } catch {}
275
- });
276
- } });
277
- return new Response(stream, { headers: {
278
- "Content-Type": "text/event-stream",
279
- "Cache-Control": "no-cache",
280
- Connection: "keep-alive"
281
- } });
282
- } catch (e) {
283
- return new Response(e.message, { status: 404 });
284
- }
285
- }
286
- if (req.method === "POST" && p === "/api/send") {
287
- let body;
288
- try {
289
- body = await req.json();
290
- } catch {
291
- return new Response("invalid JSON body", { status: 400 });
292
- }
293
- const { keyword, msg = "", code = "enter" } = body;
294
- if (!keyword || typeof keyword !== "string") return new Response("missing keyword", { status: 400 });
295
- try {
296
- const record = await resolveOne(keyword, defaultOpts());
297
- if (!record.fifo_file) return new Response(`pid ${record.pid}: no fifo_file`, { status: 409 });
298
- const trailing = controlCodeFromName(code.toLowerCase());
299
- if (msg && trailing) {
300
- await writeToIpc(record.fifo_file, msg);
301
- await new Promise((r) => setTimeout(r, 200));
302
- await writeToIpc(record.fifo_file, trailing);
303
- } else await writeToIpc(record.fifo_file, msg + trailing);
304
- return Response.json({
305
- ok: true,
306
- pid: record.pid
307
- });
308
- } catch (e) {
309
- return new Response(e.message, { status: 404 });
310
- }
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
- }
378
- return new Response("Not Found", { status: 404 });
379
- }
380
- };
381
- if (useHttps) serverOpts.tls = {
382
- cert: Bun.file(certPath),
383
- key: Bun.file(keyPath)
384
- };
385
- const server = Bun.serve(serverOpts);
386
- process.stdout.write(`ay serve ${scheme}://${host}:${port}\n`);
387
- process.stdout.write(`token: ${token}\n\n`);
388
- process.stdout.write(`connect from another machine:\n`);
389
- process.stdout.write(` ay ls ${token}@<host>:${port}\n`);
390
- process.stdout.write(` ay tail ${token}@<host>:${port}:<keyword>\n`);
391
- process.stdout.write(` ay send ${token}@<host>:${port}:<keyword> "message"\n\n`);
392
- process.stdout.write(`save as alias:\n`);
393
- process.stdout.write(` ay remote add <alias> ${scheme}://${token}@<host>:${port}\n\n`);
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
- }
409
- process.stdout.write(`(Ctrl-C to stop)\n`);
410
- await new Promise((resolve) => {
411
- process.on("SIGINT", () => {
412
- server.stop();
413
- resolve();
414
- });
415
- process.on("SIGTERM", () => {
416
- server.stop();
417
- resolve();
418
- });
419
- });
420
- return 0;
421
- }
422
-
423
- //#endregion
424
- export { cmdServe };
425
- //# sourceMappingURL=serve-CuAPBK4y.js.map