agent-yes 1.85.0 → 1.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
- import "./logger-B9h0djqx.js";
2
1
  import { r as readGlobalPids } from "./globalPidIndex-Cr-g75QF.js";
2
+ import { a as resolveRemoteSpec, i as readRemotes } from "./remotes-CFrho898.js";
3
+ import ms from "ms";
3
4
  import yargs from "yargs";
4
5
  import { appendFile, mkdir, open, readFile, stat, writeFile } from "fs/promises";
5
6
  import { homedir } from "os";
@@ -126,7 +127,9 @@ const SUBCOMMANDS = new Set([
126
127
  "head",
127
128
  "send",
128
129
  "restart",
129
- "note"
130
+ "note",
131
+ "serve",
132
+ "remote"
130
133
  ]);
131
134
  const IDLE_THRESHOLD_MS = 60 * 1e3;
132
135
  function isSubcommand(name) {
@@ -153,6 +156,14 @@ async function runSubcommand(argv) {
153
156
  case "send": return await cmdSend(rest);
154
157
  case "restart": return await cmdRestart(rest);
155
158
  case "note": return await cmdNote(rest);
159
+ case "serve": {
160
+ const { cmdServe } = await import("./serve-8dWQHSBu.js");
161
+ return cmdServe(rest);
162
+ }
163
+ case "remote": {
164
+ const { cmdRemote } = await import("./remotes-kfUzk-JT.js");
165
+ return cmdRemote(rest);
166
+ }
156
167
  default: return null;
157
168
  }
158
169
  } catch (err) {
@@ -199,6 +210,254 @@ async function resolveOne(keyword, opts) {
199
210
  const lines = matches.slice(0, 10).map((r) => ` ${r.pid} ${r.cli} ${r.cwd}`).join("\n");
200
211
  throw new Error(`keyword "${keyword}" matched ${matches.length} agents — disambiguate by pid or pass --latest:\n${lines}`);
201
212
  }
213
+ async function remoteGet(remote, pathname) {
214
+ return fetch(`${remote.url}${pathname}`, { headers: { Authorization: `Bearer ${remote.token}` } });
215
+ }
216
+ async function remotePost(remote, pathname, body) {
217
+ return fetch(`${remote.url}${pathname}`, {
218
+ method: "POST",
219
+ headers: {
220
+ Authorization: `Bearer ${remote.token}`,
221
+ "Content-Type": "application/json"
222
+ },
223
+ body: JSON.stringify(body)
224
+ });
225
+ }
226
+ async function runRemoteLs(remote, opts) {
227
+ const params = new URLSearchParams();
228
+ if (remote.keyword) params.set("keyword", remote.keyword);
229
+ if (opts.all) params.set("all", "1");
230
+ if (opts.active) params.set("active", "1");
231
+ const res = await remoteGet(remote, `/api/ls?${params}`);
232
+ if (!res.ok) {
233
+ process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
234
+ return 1;
235
+ }
236
+ const records = await res.json();
237
+ if (records.length === 0) {
238
+ process.stderr.write(remote.keyword ? `no agents matched "${remote.keyword}" on ${remote.url}\n` : `no running agents on ${remote.url}\n`);
239
+ return 0;
240
+ }
241
+ process.stderr.write(`[remote ${remote.url}]\n`);
242
+ const termWidth = process.stdout.columns ?? 120;
243
+ const widths = {
244
+ pid: Math.max(3, ...records.map((r) => String(r.pid).length)),
245
+ cli: Math.max(3, ...records.map((r) => String(r.cli).length)),
246
+ status: Math.max(6, ...records.map((r) => String(r.status).length)),
247
+ cwd: Math.max(3, ...records.map((r) => String(r.cwd).length))
248
+ };
249
+ const fixedWidth = widths.pid + widths.cli + widths.status + widths.cwd + 8;
250
+ const promptBudget = Math.max(20, termWidth - fixedWidth - 1);
251
+ const header = [
252
+ "PID".padEnd(widths.pid),
253
+ "CLI".padEnd(widths.cli),
254
+ "STATUS".padEnd(widths.status),
255
+ "CWD".padEnd(widths.cwd),
256
+ "PROMPT"
257
+ ].join(" ") + "\n";
258
+ process.stdout.write(header);
259
+ for (const r of records) {
260
+ const label = r.prompt ? truncate(`→ ${r.prompt}`, promptBudget) : "";
261
+ process.stdout.write([
262
+ String(r.pid).padEnd(widths.pid),
263
+ String(r.cli).padEnd(widths.cli),
264
+ String(r.status).padEnd(widths.status),
265
+ String(r.cwd).padEnd(widths.cwd),
266
+ label
267
+ ].join(" ") + "\n");
268
+ }
269
+ return 0;
270
+ }
271
+ async function runRemoteRead(remote, mode, follow, n, reconnectTimeoutMs = 12e4) {
272
+ const keyword = remote.keyword ?? "";
273
+ if (!keyword) {
274
+ process.stderr.write("remote tail/cat/head requires a keyword (e.g. token@host:port:keyword)\n");
275
+ return 1;
276
+ }
277
+ if (mode === "tail" && follow) {
278
+ const ac = new AbortController();
279
+ process.on("SIGINT", () => ac.abort());
280
+ const deadline = Date.now() + reconnectTimeoutMs;
281
+ let delay = 1e3;
282
+ let attempt = 0;
283
+ process.stderr.write(`[remote ${remote.url} ${keyword}]\nfollowing... (Ctrl-C to stop, timeout: ${Math.round(reconnectTimeoutMs / 1e3)}s)\n`);
284
+ while (!ac.signal.aborted) try {
285
+ const res = await fetch(`${remote.url}/api/tail/${encodeURIComponent(keyword)}`, {
286
+ headers: {
287
+ Authorization: `Bearer ${remote.token}`,
288
+ Accept: "text/event-stream"
289
+ },
290
+ signal: ac.signal
291
+ });
292
+ if (!res.ok) {
293
+ if (res.status === 401 || res.status === 404) {
294
+ process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
295
+ return 1;
296
+ }
297
+ throw new Error(`HTTP ${res.status}`);
298
+ }
299
+ if (attempt > 0) process.stderr.write("remote: reconnected\n");
300
+ delay = 1e3;
301
+ const reader = res.body.getReader();
302
+ const dec = new TextDecoder();
303
+ let buf = "";
304
+ while (true) {
305
+ const { done, value } = await reader.read();
306
+ if (done) break;
307
+ buf += dec.decode(value, { stream: true });
308
+ const lines = buf.split("\n");
309
+ buf = lines.pop() ?? "";
310
+ for (const line of lines) {
311
+ if (!line.startsWith("data: ")) continue;
312
+ try {
313
+ const text = JSON.parse(line.slice(6));
314
+ process.stdout.write(text);
315
+ if (!text.endsWith("\n")) process.stdout.write("\n");
316
+ } catch {}
317
+ }
318
+ }
319
+ break;
320
+ } catch (e) {
321
+ if (e.name === "AbortError" || ac.signal.aborted) return 0;
322
+ if (Date.now() >= deadline) {
323
+ process.stderr.write(`remote: timeout after ${Math.round(reconnectTimeoutMs / 1e3)}s, giving up\n`);
324
+ return 1;
325
+ }
326
+ process.stderr.write(`remote: disconnected (${e.message}), retrying in ${delay / 1e3}s…\n`);
327
+ await new Promise((resolve, reject) => {
328
+ const t = setTimeout(resolve, delay);
329
+ ac.signal.addEventListener("abort", () => {
330
+ clearTimeout(t);
331
+ reject(/* @__PURE__ */ new Error("abort"));
332
+ });
333
+ }).catch(() => {});
334
+ if (ac.signal.aborted) return 0;
335
+ delay = Math.min(delay * 2, 3e4);
336
+ attempt++;
337
+ }
338
+ return 0;
339
+ }
340
+ const params = new URLSearchParams({
341
+ mode,
342
+ n: String(n)
343
+ });
344
+ const res = await remoteGet(remote, `/api/read/${encodeURIComponent(keyword)}?${params}`);
345
+ if (!res.ok) {
346
+ process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
347
+ return 1;
348
+ }
349
+ const text = await res.text();
350
+ process.stderr.write(`[remote ${remote.url} ${keyword}]\n`);
351
+ process.stdout.write(text);
352
+ if (!text.endsWith("\n")) process.stdout.write("\n");
353
+ return 0;
354
+ }
355
+ async function runRemoteSend(remote, msg, code) {
356
+ const keyword = remote.keyword ?? "";
357
+ if (!keyword) {
358
+ process.stderr.write("remote send requires a keyword (e.g. token@host:port:keyword)\n");
359
+ return 1;
360
+ }
361
+ const res = await remotePost(remote, "/api/send", {
362
+ keyword,
363
+ msg,
364
+ code
365
+ });
366
+ if (!res.ok) {
367
+ process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
368
+ return 1;
369
+ }
370
+ const data = await res.json();
371
+ process.stdout.write(`sent to remote pid ${data.pid} (${remote.url} ${keyword})\n`);
372
+ return 0;
373
+ }
374
+ async function runRemoteStatus(remote) {
375
+ const keyword = remote.keyword ?? "";
376
+ if (!keyword) {
377
+ process.stderr.write("remote status requires a keyword (e.g. token@host:port:keyword)\n");
378
+ return 1;
379
+ }
380
+ const res = await remoteGet(remote, `/api/status/${encodeURIComponent(keyword)}`);
381
+ if (!res.ok) {
382
+ process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
383
+ return 1;
384
+ }
385
+ process.stdout.write(JSON.stringify(await res.json(), null, 2) + "\n");
386
+ return 0;
387
+ }
388
+ async function fetchRemoteRecordsRaw(url, token, opts) {
389
+ const params = new URLSearchParams();
390
+ if (opts.all) params.set("all", "1");
391
+ if (opts.active) params.set("active", "1");
392
+ if (opts.keyword) params.set("keyword", opts.keyword);
393
+ try {
394
+ const res = await fetch(`${url}/api/ls?${params}`, {
395
+ headers: { Authorization: `Bearer ${token}` },
396
+ signal: AbortSignal.timeout(5e3)
397
+ });
398
+ if (!res.ok) return [];
399
+ return await res.json();
400
+ } catch {
401
+ return [];
402
+ }
403
+ }
404
+ async function runAllRemotesLs(opts) {
405
+ const remotes = await readRemotes();
406
+ const localOpts = {
407
+ all: opts.all,
408
+ active: opts.active,
409
+ json: true,
410
+ latest: false,
411
+ cwdScope: null
412
+ };
413
+ const [localResult, ...remoteResults] = await Promise.allSettled([listRecords(opts.keyword, localOpts).then((recs) => ({
414
+ host: "local",
415
+ records: recs
416
+ })), ...Array.from(remotes.entries()).map(([alias, cfg]) => fetchRemoteRecordsRaw(cfg.url, cfg.token, opts).then((records) => ({
417
+ host: alias,
418
+ records
419
+ })))]);
420
+ const rows = [];
421
+ if (localResult.status === "fulfilled") for (const r of localResult.value.records) rows.push({
422
+ host: "local",
423
+ rec: r
424
+ });
425
+ for (const res of remoteResults) if (res.status === "fulfilled") for (const r of res.value.records) rows.push({
426
+ host: res.value.host,
427
+ rec: r
428
+ });
429
+ if (rows.length === 0) {
430
+ process.stderr.write("no running agents\n");
431
+ return 0;
432
+ }
433
+ const termWidth = process.stdout.columns ?? 120;
434
+ const hostW = Math.max(4, ...rows.map((r) => r.host.length));
435
+ const pidW = Math.max(3, ...rows.map((r) => String(r.rec.pid).length));
436
+ const cliW = Math.max(3, ...rows.map((r) => String(r.rec.cli).length));
437
+ const statusW = Math.max(6, ...rows.map((r) => String(r.rec.status).length));
438
+ const cwdW = Math.max(3, ...rows.map((r) => shortenPath(String(r.rec.cwd)).length));
439
+ const promptBudget = Math.max(20, termWidth - hostW - pidW - cliW - statusW - cwdW - 10 - 1);
440
+ process.stdout.write([
441
+ "HOST".padEnd(hostW),
442
+ "PID".padEnd(pidW),
443
+ "CLI".padEnd(cliW),
444
+ "STATUS".padEnd(statusW),
445
+ "CWD".padEnd(cwdW),
446
+ "PROMPT"
447
+ ].join(" ") + "\n");
448
+ for (const { host, rec } of rows) {
449
+ const label = rec.prompt ? truncate(`→ ${rec.prompt}`, promptBudget) : "";
450
+ process.stdout.write([
451
+ host.padEnd(hostW),
452
+ String(rec.pid).padEnd(pidW),
453
+ String(rec.cli).padEnd(cliW),
454
+ String(rec.status).padEnd(statusW),
455
+ shortenPath(String(rec.cwd)).padEnd(cwdW),
456
+ label
457
+ ].join(" ") + "\n");
458
+ }
459
+ return 0;
460
+ }
202
461
  async function cmdLs(rest) {
203
462
  const y = yargs(rest).usage("Usage: ay ls [keyword] [options]\n ay list [keyword] [options]\n ay ps [keyword] [options]\n\nList running agents. Optionally filter by keyword (pid, cwd substring, or prompt substring).").option("all", {
204
463
  type: "boolean",
@@ -219,18 +478,34 @@ async function cmdLs(rest) {
219
478
  }).option("cwd", {
220
479
  type: "string",
221
480
  description: "Restrict to agents whose cwd starts with dir"
481
+ }).option("all-remotes", {
482
+ type: "boolean",
483
+ default: false,
484
+ description: "Include agents from all configured remotes (remotes.yaml)"
222
485
  }).option("help", {
223
486
  alias: "h",
224
487
  type: "boolean",
225
488
  default: false,
226
489
  description: "Show this help"
227
- }).example("ay ls", "list running agents").example("ay ls --all", "include exited agents").example("ay ls --json", "machine-readable output").example("ay ls symval", "filter by cwd/prompt keyword").help(false).version(false).exitProcess(false);
490
+ }).example("ay ls", "list running agents").example("ay ls --all-remotes", "include all configured remote machines").example("ay ls --all", "include exited agents").example("ay ls --json", "machine-readable output").example("ay ls symval", "filter by cwd/prompt keyword").help(false).version(false).exitProcess(false);
228
491
  const argv = await y.parseAsync();
229
492
  if (argv.help || argv.h) {
230
493
  process.stdout.write(await y.getHelp() + "\n");
231
494
  return 0;
232
495
  }
496
+ if (argv["all-remotes"]) return runAllRemotesLs({
497
+ all: argv.all,
498
+ active: argv.active,
499
+ keyword: argv._[0] !== void 0 ? String(argv._[0]) : void 0
500
+ });
233
501
  const keyword = argv._[0] !== void 0 ? String(argv._[0]) : void 0;
502
+ if (keyword) {
503
+ const remote = await resolveRemoteSpec(keyword);
504
+ if (remote) return runRemoteLs(remote, {
505
+ all: argv.all,
506
+ active: argv.active
507
+ });
508
+ }
234
509
  const opts = {
235
510
  all: argv.all,
236
511
  active: argv.active,
@@ -309,6 +584,7 @@ async function cmdLs(rest) {
309
584
  if (alive) {
310
585
  hints.push(` ay status ${alive.pid} # JSON status snapshot\n`);
311
586
  hints.push(` ay status ${alive.pid} --watch # stream changes as JSON\n`);
587
+ hints.push(` ay status ${alive.pid} --wait-idle # block until state == idle\n`);
312
588
  hints.push(` ay tail ${alive.pid} # view latest output\n`);
313
589
  hints.push(` ay tail -f ${alive.pid} # follow live output\n`);
314
590
  hints.push(` ay send ${alive.pid} "next: ..." # send a prompt (keyword: pid, cwd, or prompt substring)\n`);
@@ -360,6 +636,10 @@ async function cmdRead(rest, { mode }) {
360
636
  }).option("cwd", {
361
637
  type: "string",
362
638
  description: "Restrict to agents under this dir"
639
+ }).option("reconnect-timeout", {
640
+ type: "number",
641
+ default: 120,
642
+ description: "Seconds before giving up reconnecting remote SSE (default: 120)"
363
643
  }).help(false).version(false).exitProcess(false).parseAsync();
364
644
  const opts = {
365
645
  all: argv.all,
@@ -369,6 +649,13 @@ async function cmdRead(rest, { mode }) {
369
649
  cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null
370
650
  };
371
651
  const keyword = argv._[0] !== void 0 ? String(argv._[0]) : void 0;
652
+ if (keyword) {
653
+ const remote = await resolveRemoteSpec(keyword);
654
+ const nFlag2 = argv.n;
655
+ const n2 = nFlag2 !== void 0 && Number.isFinite(nFlag2) && nFlag2 > 0 ? Math.floor(nFlag2) : mode === "cat" ? 0 : 96;
656
+ const reconnectTimeoutMs = (argv["reconnect-timeout"] ?? 120) * 1e3;
657
+ if (remote) return runRemoteRead(remote, mode, argv.follow, n2, reconnectTimeoutMs);
658
+ }
372
659
  const follow = argv.follow;
373
660
  const nFlag = argv.n;
374
661
  const n = nFlag !== void 0 && Number.isFinite(nFlag) && nFlag > 0 ? Math.floor(nFlag) : mode === "cat" ? 0 : 96;
@@ -555,7 +842,12 @@ async function cmdSend(rest) {
555
842
  const keyword = argv._[0] !== void 0 ? String(argv._[0]) : void 0;
556
843
  const rawMessage = argv._.slice(1).map(String).join(" ");
557
844
  if (!keyword) throw new Error("usage: ay send <keyword> <msg|-> [--code=enter|esc|ctrl-c|ctrl-y|tab|none]");
558
- const trailing = controlCodeFromName(argv.code.toLowerCase());
845
+ const codeName = argv.code.toLowerCase();
846
+ {
847
+ const remote = await resolveRemoteSpec(keyword);
848
+ if (remote) return runRemoteSend(remote, rawMessage, codeName);
849
+ }
850
+ const trailing = controlCodeFromName(codeName);
559
851
  const record = await resolveOne(keyword, opts);
560
852
  const fifoPath = record.fifo_file;
561
853
  if (!fifoPath) throw new Error(`pid ${record.pid}: no fifo_file recorded — agent was not started with --stdpush (or was spawned by Rust which doesn't yet support FIFO IPC; see ROADMAP item 10)`);
@@ -722,6 +1014,13 @@ async function cmdStatus(rest) {
722
1014
  type: "boolean",
723
1015
  default: false,
724
1016
  description: "Stream changes as JSON"
1017
+ }).option("wait-idle", {
1018
+ type: "boolean",
1019
+ default: false,
1020
+ description: "Block until state == idle. Exit 0 idle, 1 stopped, 2 timeout"
1021
+ }).option("timeout", {
1022
+ type: "string",
1023
+ description: "Timeout for --wait-idle (e.g. 30s, 5m). Default: no timeout"
725
1024
  }).option("interval", {
726
1025
  type: "number",
727
1026
  default: 2,
@@ -742,10 +1041,17 @@ async function cmdStatus(rest) {
742
1041
  cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null
743
1042
  };
744
1043
  const keyword = argv._[0] !== void 0 ? String(argv._[0]) : void 0;
745
- if (!keyword) throw new Error("usage: ay status <keyword> [--watch] [--interval=N]");
1044
+ if (!keyword) throw new Error("usage: ay status <keyword> [--watch | --wait-idle] [--timeout=Ns]");
1045
+ {
1046
+ const remote = await resolveRemoteSpec(keyword);
1047
+ if (remote) return runRemoteStatus(remote);
1048
+ }
746
1049
  const watch = argv.watch;
1050
+ const waitIdle = argv["wait-idle"];
747
1051
  const intervalFlag = argv.interval;
748
1052
  const intervalMs = Math.max(500, (Number.isFinite(intervalFlag) ? intervalFlag : 2) * 1e3);
1053
+ const timeoutMs = typeof argv.timeout === "string" && argv.timeout.length > 0 ? ms(argv.timeout) ?? NaN : null;
1054
+ if (timeoutMs !== null && !Number.isFinite(timeoutMs)) throw new Error(`invalid --timeout value: ${argv.timeout}`);
749
1055
  const record = await resolveOne(keyword, opts);
750
1056
  const emit = (snap, ts) => {
751
1057
  const out = ts !== void 0 ? {
@@ -754,6 +1060,25 @@ async function cmdStatus(rest) {
754
1060
  } : snap;
755
1061
  process.stdout.write(JSON.stringify(out) + "\n");
756
1062
  };
1063
+ if (waitIdle) {
1064
+ const startedAt = Date.now();
1065
+ for (;;) {
1066
+ const snap = await snapshotStatus(record);
1067
+ if (snap.state === "idle") {
1068
+ emit(snap);
1069
+ return 0;
1070
+ }
1071
+ if (snap.state === "stopped") {
1072
+ emit(snap);
1073
+ return 1;
1074
+ }
1075
+ if (timeoutMs !== null && Date.now() - startedAt >= timeoutMs) {
1076
+ emit(snap);
1077
+ return 2;
1078
+ }
1079
+ await new Promise((r) => setTimeout(r, intervalMs));
1080
+ }
1081
+ }
757
1082
  if (!watch) {
758
1083
  emit(await snapshotStatus(record));
759
1084
  return 0;
@@ -783,5 +1108,5 @@ async function cmdStatus(rest) {
783
1108
  }
784
1109
 
785
1110
  //#endregion
786
- export { isSubcommand, runSubcommand };
787
- //# sourceMappingURL=subcommands-BwWcA9uo.js.map
1111
+ export { matchKeyword as a, resolveOne as c, writeToIpc as d, listRecords as i, runSubcommand as l, isPidAlive as n, readNotes as o, isSubcommand as r, renderRawLog as s, controlCodeFromName as t, snapshotStatus as u };
1112
+ //# sourceMappingURL=subcommands-BltyYaEs.js.map
@@ -175,4 +175,4 @@ async function startTray() {
175
175
 
176
176
  //#endregion
177
177
  export { ensureTray, startTray };
178
- //# sourceMappingURL=tray-CH_G7aXM.js.map
178
+ //# sourceMappingURL=tray-DHuD0nEk.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-ftOiNICT.js";
2
+ import { r as getInstalledPackage } from "./versionChecker-gyE00XVe.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-C1JXxoPi.js";
5
5
  import { r as readGlobalPids } from "./globalPidIndex-Cr-g75QF.js";
@@ -1693,4 +1693,4 @@ function sleep(ms) {
1693
1693
 
1694
1694
  //#endregion
1695
1695
  export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
1696
- //# sourceMappingURL=ts-D0ddYVke.js.map
1696
+ //# sourceMappingURL=ts-CSMhtgoi.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.85.0";
10
+ var version = "1.87.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-ftOiNICT.js.map
224
+ //# sourceMappingURL=versionChecker-gyE00XVe.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.85.0",
3
+ "version": "1.87.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",
package/ts/remotes.ts ADDED
@@ -0,0 +1,161 @@
1
+ import { mkdir, readFile, writeFile } from "fs/promises";
2
+ import { homedir } from "os";
3
+ import path from "path";
4
+ import yaml from "yaml";
5
+
6
+ function remotesPath(): string {
7
+ const dir = process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
8
+ return path.join(dir, "remotes.yaml");
9
+ }
10
+
11
+ export interface RemoteConfig {
12
+ url: string; // e.g. "http://192.168.1.5:7432"
13
+ token: string;
14
+ }
15
+
16
+ export interface ResolvedRemote {
17
+ url: string;
18
+ token: string;
19
+ keyword?: string;
20
+ }
21
+
22
+ export async function readRemotes(): Promise<Map<string, RemoteConfig>> {
23
+ let raw: string;
24
+ try {
25
+ raw = await readFile(remotesPath(), "utf-8");
26
+ } catch {
27
+ return new Map();
28
+ }
29
+ const doc = yaml.parse(raw) ?? {};
30
+ const remotes = doc.remotes ?? {};
31
+ const map = new Map<string, RemoteConfig>();
32
+ for (const [alias, cfg] of Object.entries(remotes)) {
33
+ if (cfg && typeof (cfg as any).url === "string" && typeof (cfg as any).token === "string") {
34
+ map.set(alias, { url: (cfg as any).url, token: (cfg as any).token });
35
+ }
36
+ }
37
+ return map;
38
+ }
39
+
40
+ export async function writeRemoteAlias(alias: string, config: RemoteConfig): Promise<void> {
41
+ const remotes = await readRemotes();
42
+ remotes.set(alias, config);
43
+ const doc: Record<string, any> = {};
44
+ for (const [k, v] of remotes) doc[k] = v;
45
+ await mkdir(path.dirname(remotesPath()), { recursive: true });
46
+ await writeFile(remotesPath(), yaml.stringify({ remotes: doc }));
47
+ }
48
+
49
+ export async function deleteRemoteAlias(alias: string): Promise<void> {
50
+ const remotes = await readRemotes();
51
+ remotes.delete(alias);
52
+ const doc: Record<string, any> = {};
53
+ for (const [k, v] of remotes) doc[k] = v;
54
+ await writeFile(remotesPath(), yaml.stringify({ remotes: doc }));
55
+ }
56
+
57
+ /** Parse token@host:port[:keyword] — the `@` is a hard signal this is remote. */
58
+ export function parseDirectRemoteSpec(
59
+ spec: string,
60
+ ): { token: string; host: string; port: number; keyword?: string; baseUrl: string } | null {
61
+ const m = /^([^@]+)@([^:@]+):(\d+)(?::(.+))?$/.exec(spec);
62
+ if (!m) return null;
63
+ const host = m[2]!;
64
+ const port = parseInt(m[3]!, 10);
65
+ return {
66
+ token: m[1]!,
67
+ host,
68
+ port,
69
+ keyword: m[4] || undefined,
70
+ baseUrl: `http://${host}:${port}`,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Resolve a spec to connection details.
76
+ * Accepts:
77
+ * token@host:port[:keyword] — direct
78
+ * alias[:keyword] — looked up in ~/.agent-yes/remotes.yaml
79
+ * Returns null if the spec doesn't match any remote.
80
+ */
81
+ export async function resolveRemoteSpec(spec: string): Promise<ResolvedRemote | null> {
82
+ const direct = parseDirectRemoteSpec(spec);
83
+ if (direct) {
84
+ return { url: direct.baseUrl, token: direct.token, keyword: direct.keyword };
85
+ }
86
+
87
+ // alias[:keyword]
88
+ const colonIdx = spec.indexOf(":");
89
+ const alias = colonIdx >= 0 ? spec.slice(0, colonIdx) : spec;
90
+ const keyword = colonIdx >= 0 ? spec.slice(colonIdx + 1) || undefined : undefined;
91
+
92
+ const remotes = await readRemotes();
93
+ const cfg = remotes.get(alias);
94
+ if (!cfg) return null;
95
+ return { url: cfg.url, token: cfg.token, keyword };
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // ay remote subcommand
100
+ // ---------------------------------------------------------------------------
101
+
102
+ export async function cmdRemote(rest: string[]): Promise<number> {
103
+ const sub = rest[0];
104
+
105
+ if (!sub || sub === "ls" || sub === "list") {
106
+ const remotes = await readRemotes();
107
+ if (remotes.size === 0) {
108
+ process.stdout.write("no remotes configured\n");
109
+ process.stderr.write(
110
+ "\n" +
111
+ " ay remote add <alias> <url> <token> # add a remote\n" +
112
+ " ay serve # start server (prints token)\n",
113
+ );
114
+ return 0;
115
+ }
116
+ for (const [alias, cfg] of remotes) {
117
+ const preview = cfg.token.length > 8 ? cfg.token.slice(0, 8) + "..." : cfg.token;
118
+ process.stdout.write(`${alias}\t${cfg.url}\ttoken:${preview}\n`);
119
+ }
120
+ return 0;
121
+ }
122
+
123
+ if (sub === "add") {
124
+ const [, alias, url, token] = rest;
125
+ if (!alias || !url || !token) {
126
+ process.stderr.write("usage: ay remote add <alias> <url> <token>\n");
127
+ process.stderr.write(
128
+ " example: ay remote add work-mac http://192.168.1.5:7432 mytoken123\n",
129
+ );
130
+ return 1;
131
+ }
132
+ await writeRemoteAlias(alias, { url, token });
133
+ process.stdout.write(`remote '${alias}' added → ${url}\n`);
134
+ process.stderr.write(`\n ay ls ${alias} # list agents on ${alias}\n`);
135
+ return 0;
136
+ }
137
+
138
+ if (sub === "rm" || sub === "remove" || sub === "delete") {
139
+ const alias = rest[1];
140
+ if (!alias) {
141
+ process.stderr.write("usage: ay remote rm <alias>\n");
142
+ return 1;
143
+ }
144
+ const remotes = await readRemotes();
145
+ if (!remotes.has(alias)) {
146
+ process.stderr.write(`remote '${alias}' not found\n`);
147
+ return 1;
148
+ }
149
+ await deleteRemoteAlias(alias);
150
+ process.stdout.write(`remote '${alias}' removed\n`);
151
+ return 0;
152
+ }
153
+
154
+ process.stderr.write(`ay remote: unknown subcommand '${sub}'\n`);
155
+ process.stderr.write(
156
+ " ay remote ls # list configured remotes\n" +
157
+ " ay remote add <alias> <url> <token> # add a remote\n" +
158
+ " ay remote rm <alias> # remove a remote\n",
159
+ );
160
+ return 1;
161
+ }