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.
Files changed (125) hide show
  1. package/README.md +9 -44
  2. package/agent-yes.config.schema.json +0 -1
  3. package/default.config.yaml +0 -138
  4. package/dist/{JsonlStore-CPGIR0Wy.js → JsonlStore-BqF7AQ6E.js} +1 -1
  5. package/dist/{SUPPORTED_CLIS-DFOStQkx.js → SUPPORTED_CLIS-CNrMN-89.js} +2 -2
  6. package/dist/SUPPORTED_CLIS-DibEQVjZ.js +12 -0
  7. package/dist/{agent-yes.config-2xKNJM5Y.js → agent-yes.config-C77SodMP.js} +2 -2
  8. package/dist/{agentShare-CLhjMsHZ.js → agentShare-YWrrxqN2.js} +10 -10
  9. package/dist/{agentYesHome-CtmsY6I-.js → agentYesHome-BNtn19J4.js} +1 -1
  10. package/dist/{askCli-Bbxt9Umz.js → askCli-DR9vhdcV.js} +6 -6
  11. package/dist/{browser-_2ndf80G.js → browser-XQnbPssj.js} +5 -5
  12. package/dist/{browser-CorYp8Yu.js → browser-oqC3Ln2a.js} +1 -1
  13. package/dist/callback-DK1hFHPw.js +13 -0
  14. package/dist/{callback-BVKHRbUE.js → callback-DwnU5Bnx.js} +6 -6
  15. package/dist/{callbackCore-Bbe7rpN2.js → callbackCore-Bl2gGcxg.js} +1 -1
  16. package/dist/{callbackCore-C0Cg9RbS.js → callbackCore-LDxJc8LY.js} +1 -1
  17. package/dist/{channels-CkaCyvEp.js → channels-C3w3eAES.js} +2 -2
  18. package/dist/{channels-3gz9gHP7.js → channels-hOG3-R_3.js} +6 -6
  19. package/dist/channels.js +5 -5
  20. package/dist/cli.js +14 -14
  21. package/dist/{cmdPs-Cu__YsO7.js → cmdPs-tpr2ARd3.js} +9 -9
  22. package/dist/{configShared-DYxlvrH0.js → configShared-DKTgiz7K.js} +1 -1
  23. package/dist/{cwdConflictWarn-Buy3r_qc.js → cwdConflictWarn-CqN5pJ-7.js} +2 -2
  24. package/dist/{cwdPassthroughHint-BlWO-h5w.js → cwdPassthroughHint-DxdqgwEN.js} +1 -1
  25. package/dist/{e2e-OLfcNxiv.js → e2e-z1xpwSAg.js} +1 -1
  26. package/dist/{expose-Bfo-dZdy.js → expose-DTxYWQeW.js} +1 -1
  27. package/dist/{forkNested-jc2ER9xn.js → forkNested-CZAvQj0y.js} +8 -5
  28. package/dist/{globalPidIndex-Dq4KY2IV.js → globalPidIndex-DoaKEUkO.js} +1 -1
  29. package/dist/{globalPidIndex-CQX_0JSB.js → globalPidIndex-yvWZXeWT.js} +1 -1
  30. package/dist/{hist-DiOYRXuD.js → hist-QkZZBp-y.js} +1 -1
  31. package/dist/{html2canvas-pro.esm-D19oTQeV.js → html2canvas-pro.esm-DbGFekkq.js} +1 -1
  32. package/dist/{identity-CLaJ9-uE.js → identity-CKRuYUFi.js} +1 -1
  33. package/dist/{identity-BmDYOuZE.js → identity-DFr3dfTO.js} +1 -1
  34. package/dist/index.js +8 -8
  35. package/dist/{initMsg-CTAIqBWF.js → initMsg-C8ISz_C7.js} +1 -1
  36. package/dist/{initMsg-Bat5PUDf.js → initMsg-DAwRS89d.js} +1 -1
  37. package/dist/{invokedCli-uqM2YYA7.js → invokedCli-zdFbz1ST.js} +2 -5
  38. package/dist/{messageLog-BYFwioFY.js → messageLog-DdjBNqgt.js} +1 -1
  39. package/dist/{needsInput-DhfIwxse.js → needsInput-BHopZGM8.js} +1 -1
  40. package/dist/{nodeRuntime-CKc1BN5J.js → nodeRuntime-C_LtZrpR.js} +2 -2
  41. package/dist/{notifyDaemon-BmtjtW5L.js → notifyDaemon-Dx5sjDIx.js} +9 -9
  42. package/dist/{notifyStore-Dus2D1XA.js → notifyStore-2SWKGwZI.js} +2 -2
  43. package/dist/{notifyStore-0hlFPa6z.js → notifyStore-CdsB3crt.js} +1 -1
  44. package/dist/{openBrowser-Dmg2Oi4q.js → openBrowser-B50Lhaob.js} +1 -1
  45. package/dist/{parentLink-DS-XflaJ.js → parentLink-i817PzXK.js} +2 -2
  46. package/dist/{parentLink-BwX4jaGD.js → parentLink-svMgYhEi.js} +2 -2
  47. package/dist/{parentPingLoop-BtggDU06.js → parentPingLoop-DV7fBiMN.js} +7 -7
  48. package/dist/{peer-Dr4zvtcd.js → peer-BZyUyCt3.js} +3 -3
  49. package/dist/peer-CGSJluSw.js +4 -0
  50. package/dist/{pidStore-DJqSZPJi.js → pidStore-B73xnAeT.js} +4 -4
  51. package/dist/pidStore-V51mbgZY.js +6 -0
  52. package/dist/{reaper-B231i49s.js → reaper-BDMiMfwK.js} +1 -1
  53. package/dist/{reaper-C4BvNylH.js → reaper-UimuuTfO.js} +2 -2
  54. package/dist/{remotes-IzQHupeo.js → remotes-Cdlaj7AP.js} +17 -9
  55. package/dist/{remotes-BiftUp09.js → remotes-gAI6-nv9.js} +3 -3
  56. package/dist/{runningLock-DBoONoM-.js → runningLock-BOIPD_Vs.js} +1 -1
  57. package/dist/rustBinary-B7cNTxWn.js +4 -0
  58. package/dist/{rustBinary-ISnNc7Uf.js → rustBinary-DDwz4LdD.js} +73 -6
  59. package/dist/{schedule-D37Q9LGO.js → schedule-Bc5tKPJi.js} +12 -12
  60. package/dist/{serve-Dw0x9Of9.js → serve-ClCAkuSj.js} +41 -41
  61. package/dist/{setup-BmBjOyza.js → setup-D6v3wsXq.js} +4 -4
  62. package/dist/{share-DTPZgbfH.js → share-BvU4I8BI.js} +2 -2
  63. package/dist/{share-RgJUhzqG.js → share-DTlaFn6s.js} +2 -2
  64. package/dist/spawnGate-CdewnL8m.js +5 -0
  65. package/dist/{spawnGate-CS3oFdJ9.js → spawnGate-DtC_ffme.js} +3 -3
  66. package/dist/{store-C7I2CAu9.js → store-CnxYGoia.js} +1 -1
  67. package/dist/{subcommands-DKqbwkvb.js → subcommands-D0TqxBxb.js} +67 -85
  68. package/dist/subcommands-DLqtyQdw.js +11 -0
  69. package/dist/{systemPathLink-DGVcx0fg.js → systemPathLink-Df_pstm2.js} +1 -1
  70. package/dist/{terminal-BQ79zaFa.js → terminal-Dzu-PTIU.js} +2 -2
  71. package/dist/terminal.js +1 -1
  72. package/dist/{todoAutomation-BETGOik9.js → todoAutomation-CbVVS4re.js} +2 -2
  73. package/dist/{todoCli-DUoxA1dh.js → todoCli-Cf0EHvfy.js} +6 -6
  74. package/dist/{todoIdentity-BffBxRHA.js → todoIdentity-NEceTmEe.js} +2 -2
  75. package/dist/{todoStore-Wk8U4zJ8.js → todoStore-DF6vxHgV.js} +2 -2
  76. package/dist/{tray-Bxq_sfvU.js → tray-BeIs4ffu.js} +2 -2
  77. package/dist/{trayApp-BsETHuMR.js → trayApp-DCG8CjCV.js} +3 -3
  78. package/dist/trayApp-kjebnTl9.js +5 -0
  79. package/dist/{ts-Bb-h7XQP.js → ts-BmGAFUb7.js} +15 -19
  80. package/dist/{versionChecker-D9LB-MW5.js → versionChecker-DzWZizq2.js} +3 -3
  81. package/dist/{webrtcLink-B72AQ2eb.js → webrtcLink-DLaJfTcB.js} +2 -2
  82. package/dist/{webrtcRemote-D68dkqFp.js → webrtcRemote-CIbq5Hdt.js} +3 -3
  83. package/dist/{widget-gRjJprB5.js → widget-hU12_Sog.js} +3 -3
  84. package/dist/widget.js +1 -1
  85. package/dist/widgets.js +5 -5
  86. package/dist/{workspaceConfig-EK7XNsgR.js → workspaceConfig-DctWuxUP.js} +2 -2
  87. package/dist/ws-jKReYJJw.js +12 -0
  88. package/dist/{ws-BFli3FKi.js → ws-pRbF59zo.js} +3 -3
  89. package/examples/.agent-yes.config.yaml +0 -9
  90. package/examples/spawn-notify.sh +1 -1
  91. package/lab/ui/index.html +123 -34
  92. package/lab/ui/landing.html +7 -8
  93. package/package.json +1 -6
  94. package/ts/buildRustArgs.spec.ts +8 -10
  95. package/ts/configLoader.spec.ts +2 -2
  96. package/ts/cwdConflictWarn.spec.ts +1 -1
  97. package/ts/cwdConflictWarn.ts +1 -1
  98. package/ts/forkNested.spec.ts +2 -0
  99. package/ts/forkNested.ts +5 -2
  100. package/ts/index.ts +2 -8
  101. package/ts/invokedCli.spec.ts +0 -1
  102. package/ts/invokedCli.ts +1 -2
  103. package/ts/needsInput.spec.ts +7 -3
  104. package/ts/needsInput.ts +1 -1
  105. package/ts/parseCliArgs.spec.ts +1 -1
  106. package/ts/parseCliArgs.ts +1 -1
  107. package/ts/postbuild.ts +1 -1
  108. package/ts/remotes.ts +24 -7
  109. package/ts/rustBinary.spec.ts +81 -0
  110. package/ts/rustBinary.ts +106 -9
  111. package/ts/subcommands.spec.ts +7 -9
  112. package/ts/subcommands.ts +90 -64
  113. package/ts/todoParse.ts +1 -1
  114. package/dist/SUPPORTED_CLIS-C1rgBiLd.js +0 -12
  115. package/dist/callback-C64-nP2U.js +0 -13
  116. package/dist/gemini-yes.js +0 -10
  117. package/dist/openrouter-yes.js +0 -10
  118. package/dist/orcy.js +0 -10
  119. package/dist/peer-u1AUAZeA.js +0 -4
  120. package/dist/pidStore-D-b8I6Qx.js +0 -6
  121. package/dist/spawnGate-C6YKlnmb.js +0 -5
  122. package/dist/subcommands-TAOmVCxo.js +0 -11
  123. package/dist/trayApp-BHx2FN2d.js +0 -5
  124. package/dist/ws-CklRMQwW.js +0 -12
  125. 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/gemini render their permission + AskUserQuestion menus inside a
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.
@@ -131,7 +131,7 @@ describe("CLI argument parsing", () => {
131
131
  "/usr/local/bin/claude-yes",
132
132
  "--prompt",
133
133
  "test",
134
- "gemini",
134
+ "codex",
135
135
  ]);
136
136
 
137
137
  // cliName (from script) takes precedence over positional arg
@@ -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, gemini",
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", orcy: "openrouter" };
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 { url: direct.baseUrl, token: direct.token, keyword: direct.keyword };
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(link: string, keyword?: string): Promise<ResolvedRemote> {
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
- const cacheDir =
59
- process.env.AGENT_YES_CACHE_DIR ||
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
- return path.join(cacheDir, "bin", getInstalledPackage().version);
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);
@@ -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`, `deepseek` and `ds` existed only on the TS side, and running
149
- // them on the Rust binary directly launched an agent with the subcommand as
150
- // its prompt. Parse both sources and require them to stay identical.
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", "gemini"];
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 three known CLIs to their /exit-style commands", async () => {
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