agent-yes 1.272.0 → 1.274.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/README.md +9 -44
- package/agent-yes.config.schema.json +0 -1
- package/default.config.yaml +0 -138
- package/dist/{JsonlStore-CPGIR0Wy.js → JsonlStore-BqF7AQ6E.js} +1 -1
- package/dist/{SUPPORTED_CLIS-DFOStQkx.js → SUPPORTED_CLIS-CNrMN-89.js} +2 -2
- package/dist/SUPPORTED_CLIS-DibEQVjZ.js +12 -0
- package/dist/{agent-yes.config-2xKNJM5Y.js → agent-yes.config-C77SodMP.js} +2 -2
- package/dist/{agentShare-CLhjMsHZ.js → agentShare-YWrrxqN2.js} +10 -10
- package/dist/{agentYesHome-CtmsY6I-.js → agentYesHome-BNtn19J4.js} +1 -1
- package/dist/{askCli-Bbxt9Umz.js → askCli-DR9vhdcV.js} +6 -6
- package/dist/{browser-_2ndf80G.js → browser-XQnbPssj.js} +5 -5
- package/dist/{browser-CorYp8Yu.js → browser-oqC3Ln2a.js} +1 -1
- package/dist/callback-DK1hFHPw.js +13 -0
- package/dist/{callback-BVKHRbUE.js → callback-DwnU5Bnx.js} +6 -6
- package/dist/{callbackCore-Bbe7rpN2.js → callbackCore-Bl2gGcxg.js} +1 -1
- package/dist/{callbackCore-C0Cg9RbS.js → callbackCore-LDxJc8LY.js} +1 -1
- package/dist/{channels-CkaCyvEp.js → channels-C3w3eAES.js} +2 -2
- package/dist/{channels-3gz9gHP7.js → channels-hOG3-R_3.js} +6 -6
- package/dist/channels.js +5 -5
- package/dist/cli.js +14 -14
- package/dist/{cmdPs-Cu__YsO7.js → cmdPs-tpr2ARd3.js} +9 -9
- package/dist/{configShared-DYxlvrH0.js → configShared-DKTgiz7K.js} +1 -1
- package/dist/{cwdConflictWarn-Buy3r_qc.js → cwdConflictWarn-CqN5pJ-7.js} +2 -2
- package/dist/{cwdPassthroughHint-BlWO-h5w.js → cwdPassthroughHint-DxdqgwEN.js} +1 -1
- package/dist/{e2e-OLfcNxiv.js → e2e-z1xpwSAg.js} +1 -1
- package/dist/{expose-Bfo-dZdy.js → expose-DTxYWQeW.js} +1 -1
- package/dist/{forkNested-jc2ER9xn.js → forkNested-CZAvQj0y.js} +8 -5
- package/dist/{globalPidIndex-Dq4KY2IV.js → globalPidIndex-DoaKEUkO.js} +1 -1
- package/dist/{globalPidIndex-CQX_0JSB.js → globalPidIndex-yvWZXeWT.js} +1 -1
- package/dist/{hist-DiOYRXuD.js → hist-QkZZBp-y.js} +1 -1
- package/dist/{html2canvas-pro.esm-D19oTQeV.js → html2canvas-pro.esm-DbGFekkq.js} +1 -1
- package/dist/{identity-CLaJ9-uE.js → identity-CKRuYUFi.js} +1 -1
- package/dist/{identity-BmDYOuZE.js → identity-DFr3dfTO.js} +1 -1
- package/dist/index.js +8 -8
- package/dist/{initMsg-CTAIqBWF.js → initMsg-C8ISz_C7.js} +1 -1
- package/dist/{initMsg-Bat5PUDf.js → initMsg-DAwRS89d.js} +1 -1
- package/dist/{invokedCli-uqM2YYA7.js → invokedCli-zdFbz1ST.js} +2 -5
- package/dist/{messageLog-BYFwioFY.js → messageLog-DdjBNqgt.js} +1 -1
- package/dist/{needsInput-DhfIwxse.js → needsInput-BHopZGM8.js} +1 -1
- package/dist/{nodeRuntime-CKc1BN5J.js → nodeRuntime-C_LtZrpR.js} +2 -2
- package/dist/{notifyDaemon-BmtjtW5L.js → notifyDaemon-Dx5sjDIx.js} +9 -9
- package/dist/{notifyStore-Dus2D1XA.js → notifyStore-2SWKGwZI.js} +2 -2
- package/dist/{notifyStore-0hlFPa6z.js → notifyStore-CdsB3crt.js} +1 -1
- package/dist/{openBrowser-Dmg2Oi4q.js → openBrowser-B50Lhaob.js} +1 -1
- package/dist/{parentLink-DS-XflaJ.js → parentLink-i817PzXK.js} +2 -2
- package/dist/{parentLink-BwX4jaGD.js → parentLink-svMgYhEi.js} +2 -2
- package/dist/{parentPingLoop-BtggDU06.js → parentPingLoop-DV7fBiMN.js} +7 -7
- package/dist/{peer-Dr4zvtcd.js → peer-BZyUyCt3.js} +3 -3
- package/dist/peer-CGSJluSw.js +4 -0
- package/dist/{pidStore-DJqSZPJi.js → pidStore-B73xnAeT.js} +4 -4
- package/dist/pidStore-V51mbgZY.js +6 -0
- package/dist/{reaper-B231i49s.js → reaper-BDMiMfwK.js} +1 -1
- package/dist/{reaper-C4BvNylH.js → reaper-UimuuTfO.js} +2 -2
- package/dist/{remotes-IzQHupeo.js → remotes-Cdlaj7AP.js} +17 -9
- package/dist/{remotes-BiftUp09.js → remotes-gAI6-nv9.js} +3 -3
- package/dist/{runningLock-DBoONoM-.js → runningLock-BOIPD_Vs.js} +1 -1
- package/dist/rustBinary-B7cNTxWn.js +4 -0
- package/dist/{rustBinary-ISnNc7Uf.js → rustBinary-DDwz4LdD.js} +73 -6
- package/dist/{schedule-D37Q9LGO.js → schedule-Bc5tKPJi.js} +12 -12
- package/dist/{serve-Dw0x9Of9.js → serve-ClCAkuSj.js} +41 -41
- package/dist/{setup-BmBjOyza.js → setup-D6v3wsXq.js} +4 -4
- package/dist/{share-DTPZgbfH.js → share-BvU4I8BI.js} +2 -2
- package/dist/{share-RgJUhzqG.js → share-DTlaFn6s.js} +2 -2
- package/dist/spawnGate-CdewnL8m.js +5 -0
- package/dist/{spawnGate-CS3oFdJ9.js → spawnGate-DtC_ffme.js} +3 -3
- package/dist/{store-C7I2CAu9.js → store-CnxYGoia.js} +1 -1
- package/dist/{subcommands-DKqbwkvb.js → subcommands-D0TqxBxb.js} +67 -85
- package/dist/subcommands-DLqtyQdw.js +11 -0
- package/dist/{systemPathLink-DGVcx0fg.js → systemPathLink-Df_pstm2.js} +1 -1
- package/dist/{terminal-BQ79zaFa.js → terminal-Dzu-PTIU.js} +2 -2
- package/dist/terminal.js +1 -1
- package/dist/{todoAutomation-BETGOik9.js → todoAutomation-CbVVS4re.js} +2 -2
- package/dist/{todoCli-DUoxA1dh.js → todoCli-Cf0EHvfy.js} +6 -6
- package/dist/{todoIdentity-BffBxRHA.js → todoIdentity-NEceTmEe.js} +2 -2
- package/dist/{todoStore-Wk8U4zJ8.js → todoStore-DF6vxHgV.js} +2 -2
- package/dist/{tray-Bxq_sfvU.js → tray-BeIs4ffu.js} +2 -2
- package/dist/{trayApp-BsETHuMR.js → trayApp-DCG8CjCV.js} +3 -3
- package/dist/trayApp-kjebnTl9.js +5 -0
- package/dist/{ts-Bb-h7XQP.js → ts-BmGAFUb7.js} +15 -19
- package/dist/{versionChecker-D9LB-MW5.js → versionChecker-DzWZizq2.js} +3 -3
- package/dist/{webrtcLink-B72AQ2eb.js → webrtcLink-DLaJfTcB.js} +2 -2
- package/dist/{webrtcRemote-D68dkqFp.js → webrtcRemote-CIbq5Hdt.js} +3 -3
- package/dist/{widget-gRjJprB5.js → widget-hU12_Sog.js} +3 -3
- package/dist/widget.js +1 -1
- package/dist/widgets.js +5 -5
- package/dist/{workspaceConfig-EK7XNsgR.js → workspaceConfig-DctWuxUP.js} +2 -2
- package/dist/ws-jKReYJJw.js +12 -0
- package/dist/{ws-BFli3FKi.js → ws-pRbF59zo.js} +3 -3
- package/examples/.agent-yes.config.yaml +0 -9
- package/examples/spawn-notify.sh +1 -1
- package/lab/ui/index.html +123 -34
- package/lab/ui/landing.html +7 -8
- package/package.json +1 -6
- package/ts/buildRustArgs.spec.ts +8 -10
- package/ts/configLoader.spec.ts +2 -2
- package/ts/cwdConflictWarn.spec.ts +1 -1
- package/ts/cwdConflictWarn.ts +1 -1
- package/ts/forkNested.spec.ts +2 -0
- package/ts/forkNested.ts +5 -2
- package/ts/index.ts +2 -8
- package/ts/invokedCli.spec.ts +0 -1
- package/ts/invokedCli.ts +1 -2
- package/ts/needsInput.spec.ts +7 -3
- package/ts/needsInput.ts +1 -1
- package/ts/parseCliArgs.spec.ts +1 -1
- package/ts/parseCliArgs.ts +1 -1
- package/ts/postbuild.ts +1 -1
- package/ts/remotes.ts +24 -7
- package/ts/rustBinary.spec.ts +81 -0
- package/ts/rustBinary.ts +106 -9
- package/ts/subcommands.spec.ts +7 -9
- package/ts/subcommands.ts +90 -64
- package/ts/todoParse.ts +1 -1
- package/dist/SUPPORTED_CLIS-C1rgBiLd.js +0 -12
- package/dist/callback-C64-nP2U.js +0 -13
- package/dist/gemini-yes.js +0 -10
- package/dist/openrouter-yes.js +0 -10
- package/dist/orcy.js +0 -10
- package/dist/peer-u1AUAZeA.js +0 -4
- package/dist/pidStore-D-b8I6Qx.js +0 -6
- package/dist/spawnGate-C6YKlnmb.js +0 -5
- package/dist/subcommands-TAOmVCxo.js +0 -11
- package/dist/trayApp-BHx2FN2d.js +0 -5
- package/dist/ws-CklRMQwW.js +0 -12
- package/scripts/deepseek-codex.ts +0 -580
package/ts/needsInput.ts
CHANGED
|
@@ -78,7 +78,7 @@ export interface MenuState {
|
|
|
78
78
|
|
|
79
79
|
// An option row: an optional box border / cursor glyph / bullet, then "N. " (the
|
|
80
80
|
// trailing space rejects version-like "3.5GB" that isn't a menu option). The `│`
|
|
81
|
-
// matters: claude
|
|
81
|
+
// matters: claude renders its permission + AskUserQuestion menus inside a
|
|
82
82
|
// rounded box, so real option rows arrive as "│ ❯ 1. Yes" — without it only the
|
|
83
83
|
// cursor row is ever collected and `ay select N` can't reach any other option.
|
|
84
84
|
// Keep `-` last in the class so it stays a literal rather than a range.
|
package/ts/parseCliArgs.spec.ts
CHANGED
package/ts/parseCliArgs.ts
CHANGED
|
@@ -173,7 +173,7 @@ export function parseCliArgs(argv: string[], supportedClis?: readonly string[])
|
|
|
173
173
|
hidden: true,
|
|
174
174
|
})
|
|
175
175
|
.positional("cli", {
|
|
176
|
-
describe: "The AI CLI to run, e.g., claude, codex, copilot, cursor
|
|
176
|
+
describe: "The AI CLI to run, e.g., claude, codex, copilot, cursor",
|
|
177
177
|
type: "string",
|
|
178
178
|
choices: supportedClis as string[] | undefined,
|
|
179
179
|
demandOption: false,
|
package/ts/postbuild.ts
CHANGED
|
@@ -13,7 +13,7 @@ const cliNames = [...Object.keys(CLIS_CONFIG), "agent"];
|
|
|
13
13
|
const suffixes = ["-yes"];
|
|
14
14
|
|
|
15
15
|
// Short aliases: maps alias name → target CLI name (alias resolves in parseCliArgs.ts)
|
|
16
|
-
const shortAliases: Record<string, string> = { cy: "claude"
|
|
16
|
+
const shortAliases: Record<string, string> = { cy: "claude" };
|
|
17
17
|
|
|
18
18
|
// Under Bun (dev via `bun link`), run TypeScript source directly — no build needed.
|
|
19
19
|
// Under Node (published install or CI), use the compiled dist/cli.js.
|
package/ts/remotes.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdir, readFile, writeFile } from "fs/promises";
|
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import yaml from "yaml";
|
|
5
|
-
import { isWebrtcSpec } from "./webrtcLink.ts";
|
|
5
|
+
import { isWebrtcSpec, parseWebrtcLink } from "./webrtcLink.ts";
|
|
6
6
|
|
|
7
7
|
function remotesPath(): string {
|
|
8
8
|
const dir = process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
|
|
@@ -18,6 +18,14 @@ export interface ResolvedRemote {
|
|
|
18
18
|
url: string;
|
|
19
19
|
token: string;
|
|
20
20
|
keyword?: string;
|
|
21
|
+
/** Stable, human-readable name for display — never the loopback bridge URL. */
|
|
22
|
+
label: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A display label for a WebRTC share link: the room id, never the local bridge. */
|
|
26
|
+
function webrtcLabel(link: string): string {
|
|
27
|
+
const parsed = parseWebrtcLink(link);
|
|
28
|
+
return parsed ? `webrtc:${parsed.room}` : "webrtc";
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
export async function readRemotes(): Promise<Map<string, RemoteConfig>> {
|
|
@@ -82,11 +90,16 @@ export function parseDirectRemoteSpec(
|
|
|
82
90
|
export async function resolveRemoteSpec(spec: string): Promise<ResolvedRemote | null> {
|
|
83
91
|
// Inline WebRTC share link: `ay ls webrtc://…` or `ay ls https://…/w/#room:token`.
|
|
84
92
|
// These carry their own secret and have no keyword (use an alias to add one).
|
|
85
|
-
if (isWebrtcSpec(spec)) return resolveWebrtc(spec, undefined);
|
|
93
|
+
if (isWebrtcSpec(spec)) return resolveWebrtc(spec, undefined, webrtcLabel(spec));
|
|
86
94
|
|
|
87
95
|
const direct = parseDirectRemoteSpec(spec);
|
|
88
96
|
if (direct) {
|
|
89
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
url: direct.baseUrl,
|
|
99
|
+
token: direct.token,
|
|
100
|
+
keyword: direct.keyword,
|
|
101
|
+
label: `${direct.host}:${direct.port}`,
|
|
102
|
+
};
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
// alias[:keyword]
|
|
@@ -98,8 +111,8 @@ export async function resolveRemoteSpec(spec: string): Promise<ResolvedRemote |
|
|
|
98
111
|
const cfg = remotes.get(alias);
|
|
99
112
|
if (!cfg) return null;
|
|
100
113
|
// A saved alias may point at a WebRTC link; bridge it just like an inline one.
|
|
101
|
-
if (isWebrtcSpec(cfg.url)) return resolveWebrtc(cfg.url, keyword);
|
|
102
|
-
return { url: cfg.url, token: cfg.token, keyword };
|
|
114
|
+
if (isWebrtcSpec(cfg.url)) return resolveWebrtc(cfg.url, keyword, alias);
|
|
115
|
+
return { url: cfg.url, token: cfg.token, keyword, label: alias };
|
|
103
116
|
}
|
|
104
117
|
|
|
105
118
|
/**
|
|
@@ -107,10 +120,14 @@ export async function resolveRemoteSpec(spec: string): Promise<ResolvedRemote |
|
|
|
107
120
|
* ordinary http remote, so every fetch-based remote command works unchanged.
|
|
108
121
|
* The bridge lives for the rest of the process (torn down on `process.exit`).
|
|
109
122
|
*/
|
|
110
|
-
async function resolveWebrtc(
|
|
123
|
+
async function resolveWebrtc(
|
|
124
|
+
link: string,
|
|
125
|
+
keyword: string | undefined,
|
|
126
|
+
label: string,
|
|
127
|
+
): Promise<ResolvedRemote> {
|
|
111
128
|
const { startWebrtcBridge } = await import("./webrtcRemote.ts");
|
|
112
129
|
const bridge = await startWebrtcBridge(link);
|
|
113
|
-
return { url: bridge.baseUrl, token: bridge.token, keyword };
|
|
130
|
+
return { url: bridge.baseUrl, token: bridge.token, keyword, label };
|
|
114
131
|
}
|
|
115
132
|
|
|
116
133
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { gcOldBinaryDirs } from "./rustBinary.ts";
|
|
6
|
+
import { _setInstalledPackageForTesting } from "./versionChecker.ts";
|
|
7
|
+
|
|
8
|
+
// Startup GC for the versioned binary download cache (PERFORMANCE-EVENT
|
|
9
|
+
// 2026-08-13): each release pins ~/.cache/agent-yes/bin/<version>/, and
|
|
10
|
+
// nothing used to delete the old ones. GC must remove ONLY strict x.y.z dirs
|
|
11
|
+
// strictly below the current version, and report the freed byte count.
|
|
12
|
+
|
|
13
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "ay-gc-"));
|
|
14
|
+
const prevCacheDir = process.env.AGENT_YES_CACHE_DIR;
|
|
15
|
+
|
|
16
|
+
function seed(name: string, bytes: number): string {
|
|
17
|
+
const dir = path.join(root, "agent-yes", "bin", name);
|
|
18
|
+
mkdirSync(dir, { recursive: true });
|
|
19
|
+
writeFileSync(path.join(dir, "agent-yes-darwin-arm64"), "x".repeat(bytes));
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
beforeAll(() => {
|
|
24
|
+
process.env.AGENT_YES_CACHE_DIR = path.join(root, "agent-yes");
|
|
25
|
+
_setInstalledPackageForTesting({ name: "agent-yes", version: "1.272.0" });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterAll(() => {
|
|
29
|
+
if (prevCacheDir === undefined) delete process.env.AGENT_YES_CACHE_DIR;
|
|
30
|
+
else process.env.AGENT_YES_CACHE_DIR = prevCacheDir;
|
|
31
|
+
_setInstalledPackageForTesting(null);
|
|
32
|
+
rmSync(root, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("gcOldBinaryDirs", () => {
|
|
36
|
+
it("removes only strict x.y.z dirs strictly below the current version", () => {
|
|
37
|
+
const oldA = seed("1.271.0", 10);
|
|
38
|
+
const oldB = seed("1.256.1", 20);
|
|
39
|
+
const current = seed("1.272.0", 5);
|
|
40
|
+
const newer = seed("1.273.0", 7);
|
|
41
|
+
const prerelease = seed("1.270.0-beta.0", 9);
|
|
42
|
+
const junk = seed("temp", 3);
|
|
43
|
+
|
|
44
|
+
const res = gcOldBinaryDirs();
|
|
45
|
+
|
|
46
|
+
expect(res.removed.sort()).toEqual(["1.256.1", "1.271.0"]);
|
|
47
|
+
expect(res.freedBytes).toBe(30);
|
|
48
|
+
expect(existsSync(oldA)).toBe(false);
|
|
49
|
+
expect(existsSync(oldB)).toBe(false);
|
|
50
|
+
for (const keep of [current, newer, prerelease, junk]) {
|
|
51
|
+
expect(existsSync(keep), `should keep ${keep}`).toBe(true);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("is idempotent — a second run removes nothing", () => {
|
|
56
|
+
const res = gcOldBinaryDirs();
|
|
57
|
+
expect(res.removed).toEqual([]);
|
|
58
|
+
expect(res.freedBytes).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("does nothing when the cache dir does not exist", () => {
|
|
62
|
+
const prev = process.env.AGENT_YES_CACHE_DIR;
|
|
63
|
+
process.env.AGENT_YES_CACHE_DIR = path.join(root, "does-not-exist");
|
|
64
|
+
try {
|
|
65
|
+
expect(gcOldBinaryDirs()).toEqual({ removed: [], freedBytes: 0 });
|
|
66
|
+
} finally {
|
|
67
|
+
process.env.AGENT_YES_CACHE_DIR = prev;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("does nothing when the current version is a pre-release", () => {
|
|
72
|
+
_setInstalledPackageForTesting({ name: "agent-yes", version: "1.272.0-beta.0" });
|
|
73
|
+
try {
|
|
74
|
+
const res = gcOldBinaryDirs();
|
|
75
|
+
expect(res.removed).toEqual([]);
|
|
76
|
+
expect(res.freedBytes).toBe(0);
|
|
77
|
+
} finally {
|
|
78
|
+
_setInstalledPackageForTesting({ name: "agent-yes", version: "1.272.0" });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
package/ts/rustBinary.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { execFileSync } from "child_process";
|
|
6
|
-
import { existsSync, mkdirSync, unlinkSync } from "fs";
|
|
6
|
+
import { existsSync, mkdirSync, readdirSync, rmSync, statSync, unlinkSync } from "fs";
|
|
7
7
|
import { chmod, copyFile } from "fs/promises";
|
|
8
8
|
import path from "path";
|
|
9
|
-
import { getInstalledPackage } from "./versionChecker.ts";
|
|
9
|
+
import { compareVersions, getInstalledPackage } from "./versionChecker.ts";
|
|
10
10
|
|
|
11
11
|
// Platform/arch to binary name mapping
|
|
12
12
|
const PLATFORM_MAP: Record<string, string> = {
|
|
@@ -36,6 +36,22 @@ export function getBinaryName(): string {
|
|
|
36
36
|
return binaryName + (platform === "win32" ? ".exe" : "");
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The version-scoped download cache root (`~/.cache/agent-yes/bin/`) WITHOUT
|
|
41
|
+
* the version segment. Independent of `getBinDir()`'s npm-package shortcut:
|
|
42
|
+
* GC only ever touches the user cache, never a packaged bin dir.
|
|
43
|
+
*/
|
|
44
|
+
function getCacheBinRoot(): string {
|
|
45
|
+
const cacheDir =
|
|
46
|
+
process.env.AGENT_YES_CACHE_DIR ||
|
|
47
|
+
path.join(
|
|
48
|
+
process.env.XDG_CACHE_HOME || path.join(process.env.HOME || "/tmp", ".cache"),
|
|
49
|
+
"agent-yes",
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return path.join(cacheDir, "bin");
|
|
53
|
+
}
|
|
54
|
+
|
|
39
55
|
/**
|
|
40
56
|
* Get the directory where binaries are stored
|
|
41
57
|
*/
|
|
@@ -55,14 +71,79 @@ export function getBinDir(): string {
|
|
|
55
71
|
// previous (older) release downloaded, and a published binary fix would never
|
|
56
72
|
// reach a machine that already has one cached. A version segment makes a new
|
|
57
73
|
// release miss the path and re-download; old binaries sit harmlessly beside it.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
path.join(
|
|
61
|
-
process.env.XDG_CACHE_HOME || path.join(process.env.HOME || "/tmp", ".cache"),
|
|
62
|
-
"agent-yes",
|
|
63
|
-
);
|
|
74
|
+
return path.join(getCacheBinRoot(), getInstalledPackage().version);
|
|
75
|
+
}
|
|
64
76
|
|
|
65
|
-
|
|
77
|
+
// Only a strict `x.y.z` release version is ever eligible for GC. A pre-release
|
|
78
|
+
// or dev suffix (e.g. `1.270.0-beta.0`) is never deleted, even if semver would
|
|
79
|
+
// order it below the current version — a user may still be running it.
|
|
80
|
+
const STRICT_SEMVER = /^\d+\.\d+\.\d+$/;
|
|
81
|
+
|
|
82
|
+
export interface BinaryGcResult {
|
|
83
|
+
/** Version dir names that were removed. */
|
|
84
|
+
removed: string[];
|
|
85
|
+
/** Total bytes of removed files. */
|
|
86
|
+
freedBytes: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function dirSizeBytes(dir: string): number {
|
|
90
|
+
let total = 0;
|
|
91
|
+
try {
|
|
92
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
93
|
+
const p = path.join(dir, entry.name);
|
|
94
|
+
if (entry.isDirectory()) total += dirSizeBytes(p);
|
|
95
|
+
else if (entry.isFile()) {
|
|
96
|
+
try {
|
|
97
|
+
total += statSync(p).size;
|
|
98
|
+
} catch {
|
|
99
|
+
/* raced with deletion — count what we can */
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
/* unreadable dir — best effort */
|
|
105
|
+
}
|
|
106
|
+
return total;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Delete versioned binary cache dirs strictly older than the CURRENT package
|
|
111
|
+
* version.
|
|
112
|
+
*
|
|
113
|
+
* `~/.cache/agent-yes/bin/<version>/` accumulates one dir per release a user
|
|
114
|
+
* has ever run (PERFORMANCE-EVENT 2026-08-13), each holding a ~20-30 MiB
|
|
115
|
+
* binary that nothing ever cleaned up. Keep the current version; delete only
|
|
116
|
+
* dirs named `x.y.z` that compare strictly less than it (never pre-release /
|
|
117
|
+
* dev, never anything newer or malformed). Idempotent and best-effort: a dir
|
|
118
|
+
* that cannot be removed simply survives until the next run.
|
|
119
|
+
*/
|
|
120
|
+
export function gcOldBinaryDirs(): BinaryGcResult {
|
|
121
|
+
const result: BinaryGcResult = { removed: [], freedBytes: 0 };
|
|
122
|
+
const current = getInstalledPackage().version;
|
|
123
|
+
if (!STRICT_SEMVER.test(current)) return result; // dev/pre-release current — do nothing
|
|
124
|
+
|
|
125
|
+
const root = getCacheBinRoot();
|
|
126
|
+
let entries: string[];
|
|
127
|
+
try {
|
|
128
|
+
entries = readdirSync(root);
|
|
129
|
+
} catch {
|
|
130
|
+
return result; // no cache dir at all — nothing to collect
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const name of entries) {
|
|
134
|
+
if (!STRICT_SEMVER.test(name)) continue;
|
|
135
|
+
if (compareVersions(name, current) >= 0) continue;
|
|
136
|
+
const dir = path.join(root, name);
|
|
137
|
+
const size = dirSizeBytes(dir);
|
|
138
|
+
try {
|
|
139
|
+
rmSync(dir, { recursive: true, force: true });
|
|
140
|
+
result.removed.push(name);
|
|
141
|
+
result.freedBytes += size;
|
|
142
|
+
} catch {
|
|
143
|
+
// locked/in-use dir — leave it for the next run
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
66
147
|
}
|
|
67
148
|
|
|
68
149
|
/**
|
|
@@ -339,6 +420,22 @@ export async function getRustBinary(
|
|
|
339
420
|
): Promise<string> {
|
|
340
421
|
const { verbose = false, forceDownload = false } = options;
|
|
341
422
|
|
|
423
|
+
// Startup GC: collect cache dirs left behind by OLDER package versions (each
|
|
424
|
+
// release pins its own <version>/ subdir; nothing ever deleted them — see
|
|
425
|
+
// PERFORMANCE-EVENT 2026-08-13). Safe and idempotent: only strict x.y.z dirs
|
|
426
|
+
// that are strictly below the current version are removed, never this
|
|
427
|
+
// version's dir, and all failures are swallowed.
|
|
428
|
+
try {
|
|
429
|
+
const freed = gcOldBinaryDirs();
|
|
430
|
+
if (verbose && freed.removed.length > 0) {
|
|
431
|
+
console.log(
|
|
432
|
+
`[rust] GC removed ${freed.removed.length} old binary cache dir(s): ${freed.removed.join(", ")}`,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
/* never block the agent run on cache GC */
|
|
437
|
+
}
|
|
438
|
+
|
|
342
439
|
// First try to find existing binary
|
|
343
440
|
if (!forceDownload) {
|
|
344
441
|
const existing = findRustBinary(verbose);
|
package/ts/subcommands.spec.ts
CHANGED
|
@@ -121,8 +121,6 @@ describe("subcommands.isSubcommand", () => {
|
|
|
121
121
|
expect(isSubcommand("stop")).toBe(true);
|
|
122
122
|
expect(isSubcommand("tail")).toBe(true);
|
|
123
123
|
expect(isSubcommand("send")).toBe(true);
|
|
124
|
-
expect(isSubcommand("deepseek")).toBe(true);
|
|
125
|
-
expect(isSubcommand("ds")).toBe(true);
|
|
126
124
|
expect(isSubcommand("not-a-command")).toBe(false);
|
|
127
125
|
expect(isSubcommand(undefined)).toBe(false);
|
|
128
126
|
});
|
|
@@ -145,9 +143,9 @@ describe("subcommands ↔ rs/src/cli.rs subcommand mirror", () => {
|
|
|
145
143
|
// The Rust runner keeps its OWN hardcoded copy of both lists (it must decide
|
|
146
144
|
// whether to re-exec the JS launcher before clap swallows the word as prompt
|
|
147
145
|
// text). Nothing enforced that copy, so it silently drifted: `key`, `select`,
|
|
148
|
-
// `todo`, `tray
|
|
149
|
-
//
|
|
150
|
-
//
|
|
146
|
+
// `todo`, `tray` existed only on the TS side, and running them on the Rust
|
|
147
|
+
// binary directly launched an agent with the subcommand as its prompt. Parse
|
|
148
|
+
// both sources and require them to stay identical.
|
|
151
149
|
const names = (src: string, re: RegExp): string[] =>
|
|
152
150
|
[...(src.match(re)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((m) => m[1]).sort();
|
|
153
151
|
|
|
@@ -171,7 +169,7 @@ describe("subcommands ↔ rs/src/cli.rs subcommand mirror", () => {
|
|
|
171
169
|
});
|
|
172
170
|
|
|
173
171
|
describe("subcommands.isUnknownManagerToken (footgun guard)", () => {
|
|
174
|
-
const CLIS = ["claude", "codex"
|
|
172
|
+
const CLIS = ["claude", "codex"];
|
|
175
173
|
it("flags a bare non-cli non-subcommand word on the manager entry", async () => {
|
|
176
174
|
const { isUnknownManagerToken } = await loadModule();
|
|
177
175
|
// `ay frobnicate` / a newer subcommand on an older build — neither a
|
|
@@ -232,6 +230,7 @@ describe("subcommands.cmdHelp", () => {
|
|
|
232
230
|
expect(await capture()).toContain("ay setup"); // default = manager
|
|
233
231
|
expect(await capture(false)).not.toContain("ay setup"); // cli-bound alias (cy)
|
|
234
232
|
expect(await capture(false)).toContain("ay ls"); // universal commands still shown
|
|
233
|
+
expect(await capture()).toContain("ay notify watch --unread"); // Management entry
|
|
235
234
|
});
|
|
236
235
|
|
|
237
236
|
it("stays plain for a human shell (no AGENT_YES_PID)", async () => {
|
|
@@ -280,6 +279,7 @@ describe("subcommands.cmdHelp", () => {
|
|
|
280
279
|
expect(out).toContain("Spawn a sub-agent");
|
|
281
280
|
expect(out).toContain(`ay ls --cwd /work/parent/child`);
|
|
282
281
|
expect(out).toContain(`ay ls --watch --cwd /work/parent/child`);
|
|
282
|
+
expect(out).toContain("ay notify watch --unread");
|
|
283
283
|
} finally {
|
|
284
284
|
if (saved === undefined) delete process.env.AGENT_YES_PID;
|
|
285
285
|
else process.env.AGENT_YES_PID = saved;
|
|
@@ -349,7 +349,6 @@ describe("subcommands.stopTipForCli", () => {
|
|
|
349
349
|
expect(stopTipForCli("claude", 1234)).toMatch(/ay stop 1234/);
|
|
350
350
|
expect(stopTipForCli("claude", 1234)).toMatch(/\/exit/);
|
|
351
351
|
expect(stopTipForCli("codex", 99)).toMatch(/ay stop 99/);
|
|
352
|
-
expect(stopTipForCli("gemini", 7)).toMatch(/\/quit/);
|
|
353
352
|
});
|
|
354
353
|
|
|
355
354
|
it("returns null for CLIs without a known graceful command", async () => {
|
|
@@ -360,11 +359,10 @@ describe("subcommands.stopTipForCli", () => {
|
|
|
360
359
|
});
|
|
361
360
|
|
|
362
361
|
describe("subcommands.GRACEFUL_EXIT_COMMANDS", () => {
|
|
363
|
-
it("maps the
|
|
362
|
+
it("maps the known CLIs to their /exit-style commands", async () => {
|
|
364
363
|
const { GRACEFUL_EXIT_COMMANDS } = await loadModule();
|
|
365
364
|
expect(GRACEFUL_EXIT_COMMANDS["claude"]).toBe("/exit");
|
|
366
365
|
expect(GRACEFUL_EXIT_COMMANDS["codex"]).toBe("/exit");
|
|
367
|
-
expect(GRACEFUL_EXIT_COMMANDS["gemini"]).toBe("/quit");
|
|
368
366
|
});
|
|
369
367
|
});
|
|
370
368
|
|