agent-yes 1.85.0 → 1.86.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,5 @@
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
3
  import yargs from "yargs";
4
4
  import { appendFile, mkdir, open, readFile, stat, writeFile } from "fs/promises";
5
5
  import { homedir } from "os";
@@ -126,7 +126,9 @@ const SUBCOMMANDS = new Set([
126
126
  "head",
127
127
  "send",
128
128
  "restart",
129
- "note"
129
+ "note",
130
+ "serve",
131
+ "remote"
130
132
  ]);
131
133
  const IDLE_THRESHOLD_MS = 60 * 1e3;
132
134
  function isSubcommand(name) {
@@ -153,6 +155,14 @@ async function runSubcommand(argv) {
153
155
  case "send": return await cmdSend(rest);
154
156
  case "restart": return await cmdRestart(rest);
155
157
  case "note": return await cmdNote(rest);
158
+ case "serve": {
159
+ const { cmdServe } = await import("./serve-D0NnTXRD.js");
160
+ return cmdServe(rest);
161
+ }
162
+ case "remote": {
163
+ const { cmdRemote } = await import("./remotes-kfUzk-JT.js");
164
+ return cmdRemote(rest);
165
+ }
156
166
  default: return null;
157
167
  }
158
168
  } catch (err) {
@@ -199,6 +209,254 @@ async function resolveOne(keyword, opts) {
199
209
  const lines = matches.slice(0, 10).map((r) => ` ${r.pid} ${r.cli} ${r.cwd}`).join("\n");
200
210
  throw new Error(`keyword "${keyword}" matched ${matches.length} agents — disambiguate by pid or pass --latest:\n${lines}`);
201
211
  }
212
+ async function remoteGet(remote, pathname) {
213
+ return fetch(`${remote.url}${pathname}`, { headers: { Authorization: `Bearer ${remote.token}` } });
214
+ }
215
+ async function remotePost(remote, pathname, body) {
216
+ return fetch(`${remote.url}${pathname}`, {
217
+ method: "POST",
218
+ headers: {
219
+ Authorization: `Bearer ${remote.token}`,
220
+ "Content-Type": "application/json"
221
+ },
222
+ body: JSON.stringify(body)
223
+ });
224
+ }
225
+ async function runRemoteLs(remote, opts) {
226
+ const params = new URLSearchParams();
227
+ if (remote.keyword) params.set("keyword", remote.keyword);
228
+ if (opts.all) params.set("all", "1");
229
+ if (opts.active) params.set("active", "1");
230
+ const res = await remoteGet(remote, `/api/ls?${params}`);
231
+ if (!res.ok) {
232
+ process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
233
+ return 1;
234
+ }
235
+ const records = await res.json();
236
+ if (records.length === 0) {
237
+ process.stderr.write(remote.keyword ? `no agents matched "${remote.keyword}" on ${remote.url}\n` : `no running agents on ${remote.url}\n`);
238
+ return 0;
239
+ }
240
+ process.stderr.write(`[remote ${remote.url}]\n`);
241
+ const termWidth = process.stdout.columns ?? 120;
242
+ const widths = {
243
+ pid: Math.max(3, ...records.map((r) => String(r.pid).length)),
244
+ cli: Math.max(3, ...records.map((r) => String(r.cli).length)),
245
+ status: Math.max(6, ...records.map((r) => String(r.status).length)),
246
+ cwd: Math.max(3, ...records.map((r) => String(r.cwd).length))
247
+ };
248
+ const fixedWidth = widths.pid + widths.cli + widths.status + widths.cwd + 8;
249
+ const promptBudget = Math.max(20, termWidth - fixedWidth - 1);
250
+ const header = [
251
+ "PID".padEnd(widths.pid),
252
+ "CLI".padEnd(widths.cli),
253
+ "STATUS".padEnd(widths.status),
254
+ "CWD".padEnd(widths.cwd),
255
+ "PROMPT"
256
+ ].join(" ") + "\n";
257
+ process.stdout.write(header);
258
+ for (const r of records) {
259
+ const label = r.prompt ? truncate(`→ ${r.prompt}`, promptBudget) : "";
260
+ process.stdout.write([
261
+ String(r.pid).padEnd(widths.pid),
262
+ String(r.cli).padEnd(widths.cli),
263
+ String(r.status).padEnd(widths.status),
264
+ String(r.cwd).padEnd(widths.cwd),
265
+ label
266
+ ].join(" ") + "\n");
267
+ }
268
+ return 0;
269
+ }
270
+ async function runRemoteRead(remote, mode, follow, n, reconnectTimeoutMs = 12e4) {
271
+ const keyword = remote.keyword ?? "";
272
+ if (!keyword) {
273
+ process.stderr.write("remote tail/cat/head requires a keyword (e.g. token@host:port:keyword)\n");
274
+ return 1;
275
+ }
276
+ if (mode === "tail" && follow) {
277
+ const ac = new AbortController();
278
+ process.on("SIGINT", () => ac.abort());
279
+ const deadline = Date.now() + reconnectTimeoutMs;
280
+ let delay = 1e3;
281
+ let attempt = 0;
282
+ process.stderr.write(`[remote ${remote.url} ${keyword}]\nfollowing... (Ctrl-C to stop, timeout: ${Math.round(reconnectTimeoutMs / 1e3)}s)\n`);
283
+ while (!ac.signal.aborted) try {
284
+ const res = await fetch(`${remote.url}/api/tail/${encodeURIComponent(keyword)}`, {
285
+ headers: {
286
+ Authorization: `Bearer ${remote.token}`,
287
+ Accept: "text/event-stream"
288
+ },
289
+ signal: ac.signal
290
+ });
291
+ if (!res.ok) {
292
+ if (res.status === 401 || res.status === 404) {
293
+ process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
294
+ return 1;
295
+ }
296
+ throw new Error(`HTTP ${res.status}`);
297
+ }
298
+ if (attempt > 0) process.stderr.write("remote: reconnected\n");
299
+ delay = 1e3;
300
+ const reader = res.body.getReader();
301
+ const dec = new TextDecoder();
302
+ let buf = "";
303
+ while (true) {
304
+ const { done, value } = await reader.read();
305
+ if (done) break;
306
+ buf += dec.decode(value, { stream: true });
307
+ const lines = buf.split("\n");
308
+ buf = lines.pop() ?? "";
309
+ for (const line of lines) {
310
+ if (!line.startsWith("data: ")) continue;
311
+ try {
312
+ const text = JSON.parse(line.slice(6));
313
+ process.stdout.write(text);
314
+ if (!text.endsWith("\n")) process.stdout.write("\n");
315
+ } catch {}
316
+ }
317
+ }
318
+ break;
319
+ } catch (e) {
320
+ if (e.name === "AbortError" || ac.signal.aborted) return 0;
321
+ if (Date.now() >= deadline) {
322
+ process.stderr.write(`remote: timeout after ${Math.round(reconnectTimeoutMs / 1e3)}s, giving up\n`);
323
+ return 1;
324
+ }
325
+ process.stderr.write(`remote: disconnected (${e.message}), retrying in ${delay / 1e3}s…\n`);
326
+ await new Promise((resolve, reject) => {
327
+ const t = setTimeout(resolve, delay);
328
+ ac.signal.addEventListener("abort", () => {
329
+ clearTimeout(t);
330
+ reject(/* @__PURE__ */ new Error("abort"));
331
+ });
332
+ }).catch(() => {});
333
+ if (ac.signal.aborted) return 0;
334
+ delay = Math.min(delay * 2, 3e4);
335
+ attempt++;
336
+ }
337
+ return 0;
338
+ }
339
+ const params = new URLSearchParams({
340
+ mode,
341
+ n: String(n)
342
+ });
343
+ const res = await remoteGet(remote, `/api/read/${encodeURIComponent(keyword)}?${params}`);
344
+ if (!res.ok) {
345
+ process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
346
+ return 1;
347
+ }
348
+ const text = await res.text();
349
+ process.stderr.write(`[remote ${remote.url} ${keyword}]\n`);
350
+ process.stdout.write(text);
351
+ if (!text.endsWith("\n")) process.stdout.write("\n");
352
+ return 0;
353
+ }
354
+ async function runRemoteSend(remote, msg, code) {
355
+ const keyword = remote.keyword ?? "";
356
+ if (!keyword) {
357
+ process.stderr.write("remote send requires a keyword (e.g. token@host:port:keyword)\n");
358
+ return 1;
359
+ }
360
+ const res = await remotePost(remote, "/api/send", {
361
+ keyword,
362
+ msg,
363
+ code
364
+ });
365
+ if (!res.ok) {
366
+ process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
367
+ return 1;
368
+ }
369
+ const data = await res.json();
370
+ process.stdout.write(`sent to remote pid ${data.pid} (${remote.url} ${keyword})\n`);
371
+ return 0;
372
+ }
373
+ async function runRemoteStatus(remote) {
374
+ const keyword = remote.keyword ?? "";
375
+ if (!keyword) {
376
+ process.stderr.write("remote status requires a keyword (e.g. token@host:port:keyword)\n");
377
+ return 1;
378
+ }
379
+ const res = await remoteGet(remote, `/api/status/${encodeURIComponent(keyword)}`);
380
+ if (!res.ok) {
381
+ process.stderr.write(`remote error ${res.status}: ${await res.text()}\n`);
382
+ return 1;
383
+ }
384
+ process.stdout.write(JSON.stringify(await res.json(), null, 2) + "\n");
385
+ return 0;
386
+ }
387
+ async function fetchRemoteRecordsRaw(url, token, opts) {
388
+ const params = new URLSearchParams();
389
+ if (opts.all) params.set("all", "1");
390
+ if (opts.active) params.set("active", "1");
391
+ if (opts.keyword) params.set("keyword", opts.keyword);
392
+ try {
393
+ const res = await fetch(`${url}/api/ls?${params}`, {
394
+ headers: { Authorization: `Bearer ${token}` },
395
+ signal: AbortSignal.timeout(5e3)
396
+ });
397
+ if (!res.ok) return [];
398
+ return await res.json();
399
+ } catch {
400
+ return [];
401
+ }
402
+ }
403
+ async function runAllRemotesLs(opts) {
404
+ const remotes = await readRemotes();
405
+ const localOpts = {
406
+ all: opts.all,
407
+ active: opts.active,
408
+ json: true,
409
+ latest: false,
410
+ cwdScope: null
411
+ };
412
+ const [localResult, ...remoteResults] = await Promise.allSettled([listRecords(opts.keyword, localOpts).then((recs) => ({
413
+ host: "local",
414
+ records: recs
415
+ })), ...Array.from(remotes.entries()).map(([alias, cfg]) => fetchRemoteRecordsRaw(cfg.url, cfg.token, opts).then((records) => ({
416
+ host: alias,
417
+ records
418
+ })))]);
419
+ const rows = [];
420
+ if (localResult.status === "fulfilled") for (const r of localResult.value.records) rows.push({
421
+ host: "local",
422
+ rec: r
423
+ });
424
+ for (const res of remoteResults) if (res.status === "fulfilled") for (const r of res.value.records) rows.push({
425
+ host: res.value.host,
426
+ rec: r
427
+ });
428
+ if (rows.length === 0) {
429
+ process.stderr.write("no running agents\n");
430
+ return 0;
431
+ }
432
+ const termWidth = process.stdout.columns ?? 120;
433
+ const hostW = Math.max(4, ...rows.map((r) => r.host.length));
434
+ const pidW = Math.max(3, ...rows.map((r) => String(r.rec.pid).length));
435
+ const cliW = Math.max(3, ...rows.map((r) => String(r.rec.cli).length));
436
+ const statusW = Math.max(6, ...rows.map((r) => String(r.rec.status).length));
437
+ const cwdW = Math.max(3, ...rows.map((r) => shortenPath(String(r.rec.cwd)).length));
438
+ const promptBudget = Math.max(20, termWidth - hostW - pidW - cliW - statusW - cwdW - 10 - 1);
439
+ process.stdout.write([
440
+ "HOST".padEnd(hostW),
441
+ "PID".padEnd(pidW),
442
+ "CLI".padEnd(cliW),
443
+ "STATUS".padEnd(statusW),
444
+ "CWD".padEnd(cwdW),
445
+ "PROMPT"
446
+ ].join(" ") + "\n");
447
+ for (const { host, rec } of rows) {
448
+ const label = rec.prompt ? truncate(`→ ${rec.prompt}`, promptBudget) : "";
449
+ process.stdout.write([
450
+ host.padEnd(hostW),
451
+ String(rec.pid).padEnd(pidW),
452
+ String(rec.cli).padEnd(cliW),
453
+ String(rec.status).padEnd(statusW),
454
+ shortenPath(String(rec.cwd)).padEnd(cwdW),
455
+ label
456
+ ].join(" ") + "\n");
457
+ }
458
+ return 0;
459
+ }
202
460
  async function cmdLs(rest) {
203
461
  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
462
  type: "boolean",
@@ -219,18 +477,34 @@ async function cmdLs(rest) {
219
477
  }).option("cwd", {
220
478
  type: "string",
221
479
  description: "Restrict to agents whose cwd starts with dir"
480
+ }).option("all-remotes", {
481
+ type: "boolean",
482
+ default: false,
483
+ description: "Include agents from all configured remotes (remotes.yaml)"
222
484
  }).option("help", {
223
485
  alias: "h",
224
486
  type: "boolean",
225
487
  default: false,
226
488
  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);
489
+ }).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
490
  const argv = await y.parseAsync();
229
491
  if (argv.help || argv.h) {
230
492
  process.stdout.write(await y.getHelp() + "\n");
231
493
  return 0;
232
494
  }
495
+ if (argv["all-remotes"]) return runAllRemotesLs({
496
+ all: argv.all,
497
+ active: argv.active,
498
+ keyword: argv._[0] !== void 0 ? String(argv._[0]) : void 0
499
+ });
233
500
  const keyword = argv._[0] !== void 0 ? String(argv._[0]) : void 0;
501
+ if (keyword) {
502
+ const remote = await resolveRemoteSpec(keyword);
503
+ if (remote) return runRemoteLs(remote, {
504
+ all: argv.all,
505
+ active: argv.active
506
+ });
507
+ }
234
508
  const opts = {
235
509
  all: argv.all,
236
510
  active: argv.active,
@@ -360,6 +634,10 @@ async function cmdRead(rest, { mode }) {
360
634
  }).option("cwd", {
361
635
  type: "string",
362
636
  description: "Restrict to agents under this dir"
637
+ }).option("reconnect-timeout", {
638
+ type: "number",
639
+ default: 120,
640
+ description: "Seconds before giving up reconnecting remote SSE (default: 120)"
363
641
  }).help(false).version(false).exitProcess(false).parseAsync();
364
642
  const opts = {
365
643
  all: argv.all,
@@ -369,6 +647,13 @@ async function cmdRead(rest, { mode }) {
369
647
  cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null
370
648
  };
371
649
  const keyword = argv._[0] !== void 0 ? String(argv._[0]) : void 0;
650
+ if (keyword) {
651
+ const remote = await resolveRemoteSpec(keyword);
652
+ const nFlag2 = argv.n;
653
+ const n2 = nFlag2 !== void 0 && Number.isFinite(nFlag2) && nFlag2 > 0 ? Math.floor(nFlag2) : mode === "cat" ? 0 : 96;
654
+ const reconnectTimeoutMs = (argv["reconnect-timeout"] ?? 120) * 1e3;
655
+ if (remote) return runRemoteRead(remote, mode, argv.follow, n2, reconnectTimeoutMs);
656
+ }
372
657
  const follow = argv.follow;
373
658
  const nFlag = argv.n;
374
659
  const n = nFlag !== void 0 && Number.isFinite(nFlag) && nFlag > 0 ? Math.floor(nFlag) : mode === "cat" ? 0 : 96;
@@ -555,7 +840,12 @@ async function cmdSend(rest) {
555
840
  const keyword = argv._[0] !== void 0 ? String(argv._[0]) : void 0;
556
841
  const rawMessage = argv._.slice(1).map(String).join(" ");
557
842
  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());
843
+ const codeName = argv.code.toLowerCase();
844
+ {
845
+ const remote = await resolveRemoteSpec(keyword);
846
+ if (remote) return runRemoteSend(remote, rawMessage, codeName);
847
+ }
848
+ const trailing = controlCodeFromName(codeName);
559
849
  const record = await resolveOne(keyword, opts);
560
850
  const fifoPath = record.fifo_file;
561
851
  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)`);
@@ -743,6 +1033,10 @@ async function cmdStatus(rest) {
743
1033
  };
744
1034
  const keyword = argv._[0] !== void 0 ? String(argv._[0]) : void 0;
745
1035
  if (!keyword) throw new Error("usage: ay status <keyword> [--watch] [--interval=N]");
1036
+ {
1037
+ const remote = await resolveRemoteSpec(keyword);
1038
+ if (remote) return runRemoteStatus(remote);
1039
+ }
746
1040
  const watch = argv.watch;
747
1041
  const intervalFlag = argv.interval;
748
1042
  const intervalMs = Math.max(500, (Number.isFinite(intervalFlag) ? intervalFlag : 2) * 1e3);
@@ -783,5 +1077,5 @@ async function cmdStatus(rest) {
783
1077
  }
784
1078
 
785
1079
  //#endregion
786
- export { isSubcommand, runSubcommand };
787
- //# sourceMappingURL=subcommands-BwWcA9uo.js.map
1080
+ 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 };
1081
+ //# sourceMappingURL=subcommands-BpGEGOQM.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-BCrJk4Zj.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-DWuvdSWr.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.86.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-BCrJk4Zj.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.86.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
+ }