agent-yes 1.298.1 → 1.299.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.
Files changed (45) hide show
  1. package/dist/{SUPPORTED_CLIS-AzChegaJ.js → SUPPORTED_CLIS-BIlQQJa6.js} +3 -3
  2. package/dist/{SUPPORTED_CLIS-sxjBFTrE.js → SUPPORTED_CLIS-Ct9reIJH.js} +2 -2
  3. package/dist/{agentShare-CDOvU64K.js → agentShare-uzFuaLUH.js} +4 -4
  4. package/dist/{callback-DBOgmPV6.js → callback-BxTZx3xJ.js} +3 -3
  5. package/dist/{callback-Dr5rqkUu.js → callback-DRrURxmb.js} +4 -4
  6. package/dist/cli.js +7 -7
  7. package/dist/{cmdPs-DKuMlynW.js → cmdPs-BGIWbeBM.js} +4 -4
  8. package/dist/index.js +2 -2
  9. package/dist/{notifyDaemon-Ceyhz4el.js → notifyDaemon-EEbnpoYp.js} +4 -4
  10. package/dist/{parentPingLoop-C0hekeg6.js → parentPingLoop-C1XXpKK7.js} +2 -2
  11. package/dist/remotes-BDMnvGI6.js +5 -0
  12. package/dist/{remotes-5P__pAXA.js → remotes-DrYe_nfw.js} +101 -18
  13. package/dist/{rustBinary-BW3BGZ4y.js → rustBinary--pof-MTj.js} +2 -2
  14. package/dist/{rustBinary-0cD4pQIE.js → rustBinary-B7rq3nuz.js} +2 -2
  15. package/dist/{schedule-BRC5cDkf.js → schedule-BFUe9-Mf.js} +4 -4
  16. package/dist/{serve-BD-7GaKa.js → serve-Q4dnZc5l.js} +40 -21
  17. package/dist/{setup-Dad2A_-o.js → setup-aEuGSFjB.js} +3 -3
  18. package/dist/shareCmd-DkITELaM.js +415 -0
  19. package/dist/{subcommands--9gcc0ew.js → subcommands-CUWPMI_r.js} +36 -22
  20. package/dist/{subcommands-ClQCwxvk.js → subcommands-CuRZt3X6.js} +3 -3
  21. package/dist/{systemPathLink-VV-Vm6y2.js → systemPathLink-DoyBuRQq.js} +1 -1
  22. package/dist/{terminal-Bam4XVWi.js → terminal-CUn1_oPd.js} +2 -2
  23. package/dist/{todoCli-BDttv8mk.js → todoCli-DwYlxIAY.js} +1 -1
  24. package/dist/{tray-DWWsVS7Y.js → tray-BcpHVeMk.js} +1 -1
  25. package/dist/{trayApp-DOgsH0en.js → trayApp-BHwAJFzl.js} +2 -2
  26. package/dist/trayApp-ucuB_j5l.js +5 -0
  27. package/dist/{ts-BePSXUQl.js → ts-oIxnyQbE.js} +3 -3
  28. package/dist/{versionChecker-D6zM6Pdv.js → versionChecker-DOgDaagD.js} +2 -2
  29. package/dist/{webrtcLink-CnQf9pmu.js → webrtcLink-DFRyWNMs.js} +2 -2
  30. package/dist/{webrtcRemote-0RGrSwqd.js → webrtcRemote-Cd4vTrgv.js} +2 -2
  31. package/dist/{widget-DJgiOfgE.js → widget-CuagzQwz.js} +3 -3
  32. package/dist/{ws-Covfyaxn.js → ws-DcJBGzQP.js} +2 -2
  33. package/dist/{ws-DjGQGPCq.js → ws-ID7-zDiJ.js} +4 -4
  34. package/lab/ui/index.html +9 -3
  35. package/package.json +1 -1
  36. package/ts/remotes.ts +128 -13
  37. package/ts/serve.ts +22 -0
  38. package/ts/shareCmd.ts +369 -0
  39. package/ts/shareCore.spec.ts +236 -0
  40. package/ts/shareCore.ts +148 -0
  41. package/ts/subcommands.spec.ts +19 -0
  42. package/ts/subcommands.ts +33 -5
  43. package/ts/webrtcLink.ts +3 -1
  44. package/dist/remotes-CY69MeQB.js +0 -5
  45. package/dist/trayApp-s2VuJUP4.js +0 -5
@@ -0,0 +1,148 @@
1
+ import type { networkInterfaces } from "node:os";
2
+
3
+ // Pure helpers behind `ay share` (ts/shareCmd.ts, the interactive shell): which
4
+ // daemon args a mode needs, LAN/tailscale detection parsing, the exact
5
+ // third-party commands the [y/N] prompts show, and the share URL format.
6
+
7
+ export type ShareMode = "local" | "portless" | "lan" | "tailscale" | "webrtc";
8
+ export const SHARE_MODES: ShareMode[] = ["local", "portless", "lan", "tailscale", "webrtc"];
9
+ export const DEFAULT_SHARE_PORT = 7432;
10
+ export const DEFAULT_TS_PATH = "/ay";
11
+
12
+ export function flagValue(args: string[], flag: string): string | undefined {
13
+ for (let i = 0; i < args.length; i++) {
14
+ const a = args[i]!;
15
+ if (a === flag) return args[i + 1];
16
+ if (a.startsWith(`${flag}=`)) return a.slice(flag.length + 1);
17
+ }
18
+ return undefined;
19
+ }
20
+
21
+ export function hasFlag(args: string[], flag: string): boolean {
22
+ return args.some((a) => a === flag || a.startsWith(`${flag}=`));
23
+ }
24
+
25
+ export function dropFlagWithValue(args: string[], flag: string): string[] {
26
+ const out: string[] = [];
27
+ for (let i = 0; i < args.length; i++) {
28
+ const a = args[i]!;
29
+ if (a === flag) {
30
+ i++;
31
+ continue;
32
+ }
33
+ if (a.startsWith(`${flag}=`)) continue;
34
+ out.push(a);
35
+ }
36
+ return out;
37
+ }
38
+
39
+ /**
40
+ * The daemon args a mode needs, built on top of whatever the daemon already runs
41
+ * with (so sharing over one more way never drops an existing one). `prior` null =
42
+ * no daemon installed. Returns `prior` unchanged when it already fits.
43
+ */
44
+ export function planDaemonArgs(
45
+ prior: string[] | null,
46
+ mode: ShareMode,
47
+ defaultPort = DEFAULT_SHARE_PORT,
48
+ ): string[] {
49
+ let args = [...(prior ?? [])];
50
+ const hasWebrtc = hasFlag(args, "--webrtc") || hasFlag(args, "--share");
51
+ // HTTP is the default mode — on unless the daemon is webrtc-only.
52
+ const hasHttp = hasFlag(args, "--http") || hasFlag(args, "--share") || !hasWebrtc;
53
+
54
+ if (mode === "webrtc") {
55
+ if (!hasWebrtc) {
56
+ if (prior !== null && hasHttp && !hasFlag(args, "--http")) args.push("--http");
57
+ args.push("--webrtc");
58
+ }
59
+ return args;
60
+ }
61
+ if (!hasHttp) args.push("--http");
62
+ // Portless owns the port (it assigns the backend and fronts it at
63
+ // https://agent-yes.localhost), so it's exactly the mode WITHOUT --port; every
64
+ // other HTTP mode needs a stable port to point a browser / LAN / tailscale at.
65
+ if (mode === "portless") return dropFlagWithValue(args, "--port");
66
+ if (flagValue(args, "--port") === undefined) args.push("--port", String(defaultPort));
67
+ if (mode === "lan" && flagValue(args, "--host") !== "0.0.0.0") {
68
+ args = dropFlagWithValue(args, "--host");
69
+ args.push("--host", "0.0.0.0");
70
+ }
71
+ return args;
72
+ }
73
+
74
+ type Ifaces = ReturnType<typeof networkInterfaces>;
75
+
76
+ /** First private-range IPv4 (not loopback, not Tailscale's 100.64/10 CGNAT). */
77
+ export function pickLanIp(ifaces: Ifaces): string | null {
78
+ for (const list of Object.values(ifaces)) {
79
+ for (const a of list ?? []) {
80
+ if (a.family !== "IPv4" || a.internal) continue;
81
+ const [x, y] = a.address.split(".").map(Number) as [number, number];
82
+ const priv = x === 10 || (x === 192 && y === 168) || (x === 172 && y >= 16 && y <= 31);
83
+ if (priv) return a.address;
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+
89
+ /** MagicDNS name of this node from `tailscale status --json`, or null when not up. */
90
+ export function tailscaleDnsName(status: any): string | null {
91
+ if (!status || status.BackendState !== "Running") return null;
92
+ const dns = String(status.Self?.DNSName ?? "").replace(/\.$/, "");
93
+ return dns || null;
94
+ }
95
+
96
+ export type TsRoute =
97
+ | { status: "ok" }
98
+ | { status: "missing" }
99
+ | { status: "conflict"; proxy: string };
100
+
101
+ /**
102
+ * Is `https://<dns>/<mount>` already proxied to our loopback port? Tailscale
103
+ * strips the mount before forwarding, so the target must be the port's root.
104
+ */
105
+ export function findTailscaleRoute(
106
+ serveStatus: any,
107
+ dns: string,
108
+ mount: string,
109
+ port: number,
110
+ ): TsRoute {
111
+ const handlers = serveStatus?.Web?.[`${dns}:443`]?.Handlers ?? {};
112
+ const want = mount.replace(/\/+$/, "");
113
+ for (const [p, h] of Object.entries<any>(handlers)) {
114
+ if (p.replace(/\/+$/, "") !== want) continue;
115
+ const proxy = String(h?.Proxy ?? "");
116
+ const m = /^(?:https?:\/\/)?(?:127\.0\.0\.1|localhost|\[::1\]):(\d+)(\/.*)?$/.exec(proxy);
117
+ if (m && Number(m[1]) === port && (!m[2] || m[2] === "/")) return { status: "ok" };
118
+ return { status: "conflict", proxy };
119
+ }
120
+ return { status: "missing" };
121
+ }
122
+
123
+ export function tailscaleServeArgv(mount: string, port: number): string[] {
124
+ return [
125
+ "tailscale",
126
+ "serve",
127
+ "--bg",
128
+ "--https=443",
129
+ `--set-path=${mount}`,
130
+ `http://127.0.0.1:${port}`,
131
+ ];
132
+ }
133
+
134
+ export function tailscaleServeOffArgv(mount: string): string[] {
135
+ return ["tailscale", "serve", "--https=443", `--set-path=${mount}`, "off"];
136
+ }
137
+
138
+ /** How to install portless with what's on PATH (npm first, as its docs say). */
139
+ export function portlessInstallArgv(has: (bin: string) => boolean): string[] | null {
140
+ if (has("npm")) return ["npm", "install", "-g", "portless"];
141
+ if (has("bun")) return ["bun", "add", "-g", "portless"];
142
+ return null;
143
+ }
144
+
145
+ /** The one URL for the web console AND `ay connect` (HTTP modes). */
146
+ export function httpShareUrl(base: string, token: string): string {
147
+ return `${base.replace(/\/+$/, "")}/#k=${encodeURIComponent(token)}`;
148
+ }
@@ -199,6 +199,16 @@ describe("subcommands.isSubcommand", () => {
199
199
  expect(isSubcommand("ls", false)).toBe(true);
200
200
  expect(isSubcommand("send", false)).toBe(true);
201
201
  });
202
+
203
+ it("gates `share` / `connect` on the manager entry — they are prompt words", async () => {
204
+ const { isSubcommand } = await loadModule();
205
+ // `cy connect the frontend to the API` / `cy share this design` must reach
206
+ // the agent; the commands live on `ay share` / `ay connect`.
207
+ for (const sub of ["share", "connect"]) {
208
+ expect(isSubcommand(sub, true)).toBe(true);
209
+ expect(isSubcommand(sub, false)).toBe(false);
210
+ }
211
+ });
202
212
  });
203
213
 
204
214
  describe("subcommands ↔ rs/src/cli.rs subcommand mirror", () => {
@@ -295,6 +305,15 @@ describe("subcommands.cmdHelp", () => {
295
305
  expect(await capture()).toContain("ay notify watch --unread"); // Management entry
296
306
  });
297
307
 
308
+ it("labels `ay share` / `ay connect` as ay-only for cli-bound aliases", async () => {
309
+ // Both lines stay listed (sharing is how the console is reached at all) but
310
+ // must not read as invokable through `cy`, where those words are prompt text.
311
+ const alias = await capture(false);
312
+ expect(alias).toContain("ay share");
313
+ expect(alias).toContain("`cy share`/`cy connect` are prompts");
314
+ expect(await capture(true)).not.toContain("are prompts");
315
+ });
316
+
298
317
  it("stays plain for a human shell (no AGENT_YES_PID)", async () => {
299
318
  const out = await capture();
300
319
  expect(out).not.toContain("You are running inside an agent");
package/ts/subcommands.ts CHANGED
@@ -663,11 +663,21 @@ const SUBCOMMANDS = new Set([
663
663
  "help",
664
664
  ]);
665
665
 
666
- // Subcommands reserved for the GENERIC manager (`ay` / `agent-yes`). A cli-bound
667
- // alias like `cy` (= claude-yes = "agent-yes claude") must NOT treat these as
668
- // subcommands — `cy setup …` should run claude with that text, not manage the
669
- // host. Kept separate from SUBCOMMANDS so a runner alias falls straight through.
670
- const MANAGER_SUBCOMMANDS = new Set(["setup", "ws"]);
666
+ // Subcommands recognised ONLY on the GENERIC manager entry (`ay` / `agent-yes`).
667
+ // A cli-bound alias like `cy` (= claude-yes = "agent-yes claude") must NOT treat
668
+ // these as subcommands — it falls straight through to running the agent with that
669
+ // text. Two reasons a name lands here: it manages the host, or it is an ordinary
670
+ // English verb people open prompts with (`cy connect the frontend to the API`
671
+ // must reach claude). Kept one per line so the two runtimes' copies of this list
672
+ // stay easy to keep in sync.
673
+ const MANAGER_SUBCOMMANDS = new Set([
674
+ // manage this host
675
+ "setup",
676
+ "ws",
677
+ // prompt-word verbs: reachable as `ay share` / `ay connect`
678
+ "share",
679
+ "connect",
680
+ ]);
671
681
 
672
682
  const IDLE_THRESHOLD_MS = 60 * 1000;
673
683
 
@@ -1108,6 +1118,14 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
1108
1118
  const { cmdRemote } = await import("./remotes.ts");
1109
1119
  return cmdRemote(rest);
1110
1120
  }
1121
+ case "share": {
1122
+ const { cmdShare } = await import("./shareCmd.ts");
1123
+ return cmdShare(rest);
1124
+ }
1125
+ case "connect": {
1126
+ const { cmdConnect } = await import("./remotes.ts");
1127
+ return cmdConnect(rest);
1128
+ }
1111
1129
  case "expose": {
1112
1130
  const { cmdExpose } = await import("./expose.ts");
1113
1131
  return cmdExpose(rest);
@@ -1252,6 +1270,13 @@ export async function cmdHelp(managerCommands = true): Promise<number> {
1252
1270
  ? ` ay ws ls [--status] list <owner>/<repo>/tree/<branch> workspaces\n` +
1253
1271
  ` ay ws new <owner>/<repo>[@branch] clone/refresh a workspace (ay ws help for more)\n`
1254
1272
  : ``;
1273
+ // `share` / `connect` are manager-entry-only for the other reason in
1274
+ // MANAGER_SUBCOMMANDS: they are prompt words, so `cy share …` runs the agent.
1275
+ // Both lines stay visible (`ay share` is how you reach the console at all) —
1276
+ // just labelled, so nobody types `cy connect <url>` and starts a session.
1277
+ const shareAliasNote = managerCommands
1278
+ ? ``
1279
+ : ` (\`ay\` only — \`cy share\`/\`cy connect\` are prompts)\n`;
1255
1280
  // Only agents carry AGENT_YES_PID — a human shell never sets it — so this
1256
1281
  // section is skipped entirely (no async work at all) for interactive use.
1257
1282
  const self = process.env.AGENT_YES_PID ? await resolveSender() : null;
@@ -1299,6 +1324,9 @@ export async function cmdHelp(managerCommands = true): Promise<number> {
1299
1324
  `\n` +
1300
1325
  `Remote:\n` +
1301
1326
  setupLine +
1327
+ ` ay share [local|lan|tailscale|webrtc] share this machine: one URL for web console + CLI\n` +
1328
+ ` ay connect <share-url> [alias] save another machine's share URL as a remote\n` +
1329
+ shareAliasNote +
1302
1330
  ` ay schedule <when> <cli> -- <msg> run an agent on a schedule (HH:MM or cron)\n` +
1303
1331
  ` ay serve [--port N] start HTTP API server (prints token)\n` +
1304
1332
  ` ay serve status show serve daemon/server status\n` +
package/ts/webrtcLink.ts CHANGED
@@ -85,5 +85,7 @@ export function toWebrtcUrl(link: string): string | null {
85
85
 
86
86
  /** True if `spec` looks like a WebRTC share link (vs. an http remote or alias). */
87
87
  export function isWebrtcSpec(spec: string): boolean {
88
- return spec.startsWith("webrtc://") || (/^https?:\/\//.test(spec) && spec.includes("#"));
88
+ // An http(s) URL counts only when its fragment parses as a room — an
89
+ // `ay share` console link (…/#k=<token>) is an http remote, not a room.
90
+ return spec.startsWith("webrtc://") || parseRaw(spec) !== null;
89
91
  }
@@ -1,5 +0,0 @@
1
- import "./e2e-z1xpwSAg.js";
2
- import "./webrtcLink-CnQf9pmu.js";
3
- import { a as resolveRemoteSpec, i as readRemotes, n as deleteRemoteAlias, o as writeRemoteAlias, r as parseDirectRemoteSpec, t as cmdRemote } from "./remotes-5P__pAXA.js";
4
-
5
- export { cmdRemote };
@@ -1,5 +0,0 @@
1
- import "./versionChecker-D6zM6Pdv.js";
2
- import "./rustBinary-BW3BGZ4y.js";
3
- import { a as trayHiddenMarker, i as launchTray, n as hasDesktop, r as isTrayHidden, t as cmdTray } from "./trayApp-DOgsH0en.js";
4
-
5
- export { cmdTray };