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.
- package/dist/{SUPPORTED_CLIS-AzChegaJ.js → SUPPORTED_CLIS-BIlQQJa6.js} +3 -3
- package/dist/{SUPPORTED_CLIS-sxjBFTrE.js → SUPPORTED_CLIS-Ct9reIJH.js} +2 -2
- package/dist/{agentShare-CDOvU64K.js → agentShare-uzFuaLUH.js} +4 -4
- package/dist/{callback-DBOgmPV6.js → callback-BxTZx3xJ.js} +3 -3
- package/dist/{callback-Dr5rqkUu.js → callback-DRrURxmb.js} +4 -4
- package/dist/cli.js +7 -7
- package/dist/{cmdPs-DKuMlynW.js → cmdPs-BGIWbeBM.js} +4 -4
- package/dist/index.js +2 -2
- package/dist/{notifyDaemon-Ceyhz4el.js → notifyDaemon-EEbnpoYp.js} +4 -4
- package/dist/{parentPingLoop-C0hekeg6.js → parentPingLoop-C1XXpKK7.js} +2 -2
- package/dist/remotes-BDMnvGI6.js +5 -0
- package/dist/{remotes-5P__pAXA.js → remotes-DrYe_nfw.js} +101 -18
- package/dist/{rustBinary-BW3BGZ4y.js → rustBinary--pof-MTj.js} +2 -2
- package/dist/{rustBinary-0cD4pQIE.js → rustBinary-B7rq3nuz.js} +2 -2
- package/dist/{schedule-BRC5cDkf.js → schedule-BFUe9-Mf.js} +4 -4
- package/dist/{serve-BD-7GaKa.js → serve-Q4dnZc5l.js} +40 -21
- package/dist/{setup-Dad2A_-o.js → setup-aEuGSFjB.js} +3 -3
- package/dist/shareCmd-DkITELaM.js +415 -0
- package/dist/{subcommands--9gcc0ew.js → subcommands-CUWPMI_r.js} +36 -22
- package/dist/{subcommands-ClQCwxvk.js → subcommands-CuRZt3X6.js} +3 -3
- package/dist/{systemPathLink-VV-Vm6y2.js → systemPathLink-DoyBuRQq.js} +1 -1
- package/dist/{terminal-Bam4XVWi.js → terminal-CUn1_oPd.js} +2 -2
- package/dist/{todoCli-BDttv8mk.js → todoCli-DwYlxIAY.js} +1 -1
- package/dist/{tray-DWWsVS7Y.js → tray-BcpHVeMk.js} +1 -1
- package/dist/{trayApp-DOgsH0en.js → trayApp-BHwAJFzl.js} +2 -2
- package/dist/trayApp-ucuB_j5l.js +5 -0
- package/dist/{ts-BePSXUQl.js → ts-oIxnyQbE.js} +3 -3
- package/dist/{versionChecker-D6zM6Pdv.js → versionChecker-DOgDaagD.js} +2 -2
- package/dist/{webrtcLink-CnQf9pmu.js → webrtcLink-DFRyWNMs.js} +2 -2
- package/dist/{webrtcRemote-0RGrSwqd.js → webrtcRemote-Cd4vTrgv.js} +2 -2
- package/dist/{widget-DJgiOfgE.js → widget-CuagzQwz.js} +3 -3
- package/dist/{ws-Covfyaxn.js → ws-DcJBGzQP.js} +2 -2
- package/dist/{ws-DjGQGPCq.js → ws-ID7-zDiJ.js} +4 -4
- package/lab/ui/index.html +9 -3
- package/package.json +1 -1
- package/ts/remotes.ts +128 -13
- package/ts/serve.ts +22 -0
- package/ts/shareCmd.ts +369 -0
- package/ts/shareCore.spec.ts +236 -0
- package/ts/shareCore.ts +148 -0
- package/ts/subcommands.spec.ts +19 -0
- package/ts/subcommands.ts +33 -5
- package/ts/webrtcLink.ts +3 -1
- package/dist/remotes-CY69MeQB.js +0 -5
- package/dist/trayApp-s2VuJUP4.js +0 -5
package/ts/shareCore.ts
ADDED
|
@@ -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
|
+
}
|
package/ts/subcommands.spec.ts
CHANGED
|
@@ -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
|
|
667
|
-
// alias like `cy` (= claude-yes = "agent-yes claude") must NOT treat
|
|
668
|
-
// subcommands —
|
|
669
|
-
//
|
|
670
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/remotes-CY69MeQB.js
DELETED
package/dist/trayApp-s2VuJUP4.js
DELETED