agent-yes 1.75.1 → 1.75.3

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 CHANGED
@@ -130,6 +130,25 @@ claude-yes --exit-on-idle=60s "run all tests and commit current changes"
130
130
  claude-code-execute claude-yes "your task here"
131
131
  ```
132
132
 
133
+ ### Inspect and message running agents (`cy ls / read / send`)
134
+
135
+ From any terminal you can list and interact with agents that are already
136
+ running on the machine — both TS- and Rust-spawned ones:
137
+
138
+ ```bash
139
+ cy ls # list all running agents
140
+ cy ls codex # filter (matches pid, cwd, cli, or prompt)
141
+ cy tail <keyword> # render last 96 lines via @xterm/headless
142
+ cy read <keyword> # full rendered log
143
+ cy send <keyword> "next: run tests" # append a prompt to that agent's stdin
144
+ cy send <keyword> "" --code=ctrl-c # send a Ctrl+C
145
+ ```
146
+
147
+ `cy` (and `ay` / `agent-yes`) writes to a shared registry at
148
+ `~/.agent-yes/pids.jsonl` and a per-pid FIFO at `~/.agent-yes/fifo/<pid>.stdin`,
149
+ so subcommands work whether the target agent is the TS or Rust runtime.
150
+ Detailed reference (Japanese): [`docs/cy-subcommands.md`](./docs/cy-subcommands.md).
151
+
133
152
  ### Docker Usage
134
153
 
135
154
  You can run `agent-yes` in a Docker container with all AI CLI tools pre-installed.
@@ -0,0 +1,12 @@
1
+ import { t as CLIS_CONFIG } from "./ts-vc6cm9z6.js";
2
+ import "./logger-B9h0djqx.js";
3
+ import "./versionChecker-IVJmMHfo.js";
4
+ import "./pidStore-C1JXxoPi.js";
5
+ import "./globalPidIndex-Cr-g75QF.js";
6
+
7
+ //#region ts/SUPPORTED_CLIS.ts
8
+ const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
9
+
10
+ //#endregion
11
+ export { SUPPORTED_CLIS };
12
+ //# sourceMappingURL=SUPPORTED_CLIS-C9qkWtPg.js.map
package/dist/cli.js CHANGED
@@ -1,14 +1,13 @@
1
1
  #!/usr/bin/env bun
2
2
  import { n as logger } from "./logger-B9h0djqx.js";
3
- import { n as version, t as name } from "./package-DRIilF5m.js";
3
+ import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-IVJmMHfo.js";
4
4
  import { argv } from "process";
5
5
  import { execFileSync, spawn } from "child_process";
6
6
  import ms from "ms";
7
7
  import yargs from "yargs";
8
8
  import { hideBin } from "yargs/helpers";
9
- import { existsSync, lstatSync, mkdirSync, readlinkSync, unlinkSync } from "fs";
10
- import { chmod, copyFile, mkdir, readFile, writeFile } from "fs/promises";
11
- import { homedir } from "os";
9
+ import { existsSync, mkdirSync, unlinkSync } from "fs";
10
+ import { chmod, copyFile } from "fs/promises";
12
11
  import path from "path";
13
12
 
14
13
  //#region ts/parseCliArgs.ts
@@ -237,176 +236,6 @@ function parseCliArgs(argv, supportedClis) {
237
236
  };
238
237
  }
239
238
 
240
- //#endregion
241
- //#region ts/versionChecker.ts
242
- const CACHE_DIR = path.join(homedir(), ".cache", "agent-yes");
243
- const CACHE_FILE = path.join(CACHE_DIR, "update-check.json");
244
- const TTL_MS = 3600 * 1e3;
245
- async function readUpdateCache() {
246
- try {
247
- const raw = await readFile(CACHE_FILE, "utf8");
248
- return JSON.parse(raw);
249
- } catch {
250
- return null;
251
- }
252
- }
253
- async function writeUpdateCache(data) {
254
- await mkdir(CACHE_DIR, { recursive: true });
255
- await writeFile(CACHE_FILE, JSON.stringify(data));
256
- }
257
- function detectPackageManager() {
258
- if (process.env.BUN_INSTALL || process.execPath?.includes("bun") || process.env.npm_execpath?.includes("bun")) return "bun";
259
- return "npm";
260
- }
261
- /**
262
- * Check for updates, auto-install if newer version is available, and re-exec
263
- * so the current invocation always runs the latest code.
264
- *
265
- * Uses a 1-hour TTL cache to avoid hitting the registry on every run.
266
- * All errors are swallowed — network issues must never break the tool.
267
- * Set AGENT_YES_NO_UPDATE=1 to opt out.
268
- *
269
- * The AGENT_YES_UPDATED env var prevents infinite re-exec loops:
270
- * after updating we re-exec with AGENT_YES_UPDATED=<version> so the
271
- * new process skips the update check.
272
- */
273
- async function checkAndAutoUpdate() {
274
- if (process.env.AGENT_YES_NO_UPDATE) return;
275
- if (process.env.AGENT_YES_UPDATED) return;
276
- if (import.meta.url.startsWith("file://") && !import.meta.url.includes("node_modules")) {
277
- const scriptDir = path.dirname(new URL(import.meta.url).pathname);
278
- const repoRoot = path.resolve(scriptDir, "..");
279
- if (existsSync(path.join(repoRoot, ".git"))) return;
280
- }
281
- try {
282
- let latestVersion;
283
- const cache = await readUpdateCache();
284
- if (cache && Date.now() - cache.checkedAt < TTL_MS) latestVersion = cache.latestVersion;
285
- else {
286
- const fetched = await fetchLatestVersion();
287
- if (!fetched) return;
288
- latestVersion = fetched;
289
- await writeUpdateCache({
290
- checkedAt: Date.now(),
291
- latestVersion
292
- });
293
- }
294
- if (compareVersions(version, latestVersion) < 0) {
295
- if (await runInstall(latestVersion)) reExec(latestVersion);
296
- }
297
- } catch {}
298
- }
299
- async function runInstall(latestVersion) {
300
- const installCmd = detectPackageManager() === "bun" ? `bun add -g agent-yes@${latestVersion}` : `npm install -g agent-yes@${latestVersion}`;
301
- process.stderr.write(`\x1b[33m[agent-yes] Updating ${version} → ${latestVersion}…\x1b[0m\n`);
302
- try {
303
- const { execaCommand } = await import("execa");
304
- await execaCommand(installCmd, { stdio: "inherit" });
305
- process.stderr.write(`\x1b[32m[agent-yes] Updated to ${latestVersion}\x1b[0m\n`);
306
- return true;
307
- } catch {
308
- process.stderr.write(`\x1b[31m[agent-yes] Auto-update failed. Run: ${installCmd}\x1b[0m\n`);
309
- return false;
310
- }
311
- }
312
- /**
313
- * Re-exec the current process so the newly installed version runs.
314
- * Sets AGENT_YES_UPDATED=<version> to prevent an infinite loop.
315
- */
316
- function reExec(version) {
317
- const [bin, ...args] = process.argv;
318
- process.stderr.write(`\x1b[36m[agent-yes] Restarting with v${version}…\x1b[0m\n`);
319
- try {
320
- execFileSync(bin, args, {
321
- stdio: "inherit",
322
- env: {
323
- ...process.env,
324
- AGENT_YES_UPDATED: version
325
- }
326
- });
327
- process.exit(0);
328
- } catch (err) {
329
- process.exit(err.status ?? 1);
330
- }
331
- }
332
- /**
333
- * Fetch the latest version of the package from npm registry
334
- */
335
- async function fetchLatestVersion() {
336
- try {
337
- const response = await fetch(`https://registry.npmjs.org/${name}/latest`, { signal: AbortSignal.timeout(3e3) });
338
- if (!response.ok) return null;
339
- return (await response.json()).version;
340
- } catch {
341
- return null;
342
- }
343
- }
344
- /**
345
- * Compare two semantic versions
346
- * Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
347
- */
348
- function compareVersions(v1, v2) {
349
- const parts1 = v1.split(".").map(Number);
350
- const parts2 = v2.split(".").map(Number);
351
- for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
352
- const part1 = parts1[i] || 0;
353
- const part2 = parts2[i] || 0;
354
- if (part1 > part2) return 1;
355
- if (part1 < part2) return -1;
356
- }
357
- return 0;
358
- }
359
- /**
360
- * Detect how agent-yes was installed.
361
- * Returns a short label: "git", "bun link", "bun", "npm", "npx", or "unknown"
362
- */
363
- function detectInstallMethod() {
364
- try {
365
- const scriptDir = path.dirname(new URL(import.meta.url).pathname);
366
- if (!scriptDir.includes("node_modules")) {
367
- const repoRoot = path.resolve(scriptDir, "..");
368
- if (existsSync(path.join(repoRoot, ".git"))) return "git";
369
- return "source";
370
- }
371
- const nodeModulesEntry = scriptDir.replace(/\/dist$/, "");
372
- try {
373
- if (lstatSync(nodeModulesEntry).isSymbolicLink()) {
374
- const target = readlinkSync(nodeModulesEntry);
375
- const resolvedTarget = path.resolve(path.dirname(nodeModulesEntry), target);
376
- if (existsSync(path.join(resolvedTarget, ".git"))) return "bun link (git)";
377
- return "bun link";
378
- }
379
- } catch {}
380
- if (scriptDir.includes(".bun/")) return "bun";
381
- if (scriptDir.includes(".npm/")) return "npx";
382
- if (process.env.npm_execpath?.includes("bun")) return "bun";
383
- if (process.env.npm_config_user_agent?.startsWith("bun")) return "bun";
384
- if (process.env.npm_config_user_agent?.startsWith("npm")) return "npm";
385
- return "npm";
386
- } catch {
387
- return "unknown";
388
- }
389
- }
390
- /**
391
- * Format version string with install method
392
- */
393
- function versionString() {
394
- return `agent-yes v${version} (${detectInstallMethod()})`;
395
- }
396
- /**
397
- * Display version information with async latest version check
398
- */
399
- async function displayVersion() {
400
- console.log(versionString());
401
- const latestVersion = await fetchLatestVersion();
402
- if (latestVersion) {
403
- const comparison = compareVersions(version, latestVersion);
404
- if (comparison < 0) console.log(`\x1b[33m${latestVersion} (update available)\x1b[0m`);
405
- else if (comparison > 0) console.log(`${latestVersion} (latest published)`);
406
- else console.log(`${latestVersion} (latest)`);
407
- } else console.log("(unable to check for updates)");
408
- }
409
-
410
239
  //#endregion
411
240
  //#region ts/rustBinary.ts
412
241
  /**
@@ -564,14 +393,15 @@ function getRustBinaryVersion(binaryPath) {
564
393
  function autoRebuildIfOutdated(binaryPath, verbose) {
565
394
  if (!binaryPath.includes("/target/release") && !binaryPath.includes("/target/debug")) return true;
566
395
  const binaryVersion = getRustBinaryVersion(binaryPath);
567
- if (verbose) console.log(`[rust] Binary version: ${binaryVersion}, package version: ${version}`);
568
- if (binaryVersion === version) return true;
396
+ const pkgVersion = getInstalledPackage().version;
397
+ if (verbose) console.log(`[rust] Binary version: ${binaryVersion}, package version: ${pkgVersion}`);
398
+ if (binaryVersion === pkgVersion) return true;
569
399
  const rsDir = binaryPath.replace(/\/target\/(release|debug)\/agent-yes.*$/, "");
570
400
  if (!existsSync(path.join(rsDir, "Cargo.toml"))) {
571
401
  if (verbose) console.log(`[rust] Cannot find Cargo.toml at ${rsDir}, skipping rebuild`);
572
402
  return true;
573
403
  }
574
- process.stderr.write(`\x1b[33m[rust] Binary outdated (${binaryVersion ?? "unknown"} → ${version}), rebuilding…\x1b[0m\n`);
404
+ process.stderr.write(`\x1b[33m[rust] Binary outdated (${binaryVersion ?? "unknown"} → ${pkgVersion}), rebuilding…\x1b[0m\n`);
575
405
  try {
576
406
  execFileSync("cargo", ["build", ...binaryPath.includes("/target/release") ? ["--release"] : []], {
577
407
  cwd: rsDir,
@@ -635,7 +465,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
635
465
  }
636
466
  }
637
467
  {
638
- const { isSubcommand, runSubcommand } = await import("./subcommands-NWIZy8od.js");
468
+ const { isSubcommand, runSubcommand } = await import("./subcommands-DSm9WL-6.js");
639
469
  if (isSubcommand(process.argv[2])) {
640
470
  const code = await runSubcommand(process.argv);
641
471
  process.exit(code ?? 0);
@@ -645,12 +475,12 @@ await checkAndAutoUpdate();
645
475
  logger.info(versionString());
646
476
  const config = parseCliArgs(process.argv);
647
477
  if (config.tray) {
648
- const { startTray } = await import("./tray-D5deJPjk.js");
478
+ const { startTray } = await import("./tray-CH_G7aXM.js");
649
479
  await startTray();
650
480
  await new Promise(() => {});
651
481
  }
652
482
  {
653
- const { ensureTray } = await import("./tray-D5deJPjk.js");
483
+ const { ensureTray } = await import("./tray-CH_G7aXM.js");
654
484
  ensureTray();
655
485
  }
656
486
  if (config.useRust) {
@@ -664,7 +494,7 @@ if (config.useRust) {
664
494
  }
665
495
  }
666
496
  if (rustBinary) {
667
- const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-CqAT1Zud.js");
497
+ const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-C9qkWtPg.js");
668
498
  const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
669
499
  if (config.verbose) {
670
500
  console.log(`[rust] Using binary: ${rustBinary}`);
@@ -694,7 +524,7 @@ if (config.showVersion) {
694
524
  process.exit(0);
695
525
  }
696
526
  if (config.appendPrompt) {
697
- const { PidStore } = await import("./pidStore-CF54dFqr.js");
527
+ const { PidStore } = await import("./pidStore-BCsY5BW3.js");
698
528
  const ipcPath = await PidStore.findActiveFifo(process.cwd());
699
529
  if (!ipcPath) {
700
530
  console.error("No active agent with IPC found in current directory.");
@@ -1,5 +1,5 @@
1
1
  import { n as logger } from "./logger-B9h0djqx.js";
2
- import { appendFile, mkdir, readFile } from "fs/promises";
2
+ import { appendFile, mkdir, readFile, rename, writeFile } from "fs/promises";
3
3
  import { homedir } from "os";
4
4
  import path from "path";
5
5
  import { lock } from "proper-lockfile";
@@ -108,7 +108,37 @@ function isProcessAlive(pid) {
108
108
  return false;
109
109
  }
110
110
  }
111
+ const COMPACT_THRESHOLD_LINES = 500;
112
+ /**
113
+ * Best-effort compaction: rewrite the JSONL file with one line per known pid,
114
+ * dropping records whose pid is dead AND status is exited (those won't be
115
+ * referenced by `cy ls` anyway). Triggered opportunistically when the raw
116
+ * file grows past `COMPACT_THRESHOLD_LINES`. Safe to call unconditionally;
117
+ * it no-ops when the file is already small enough.
118
+ */
119
+ async function maybeCompactGlobalPids() {
120
+ let raw;
121
+ try {
122
+ raw = await readFile(resolveGlobalFile(), "utf-8");
123
+ } catch (err) {
124
+ if (err.code === "ENOENT") return;
125
+ return;
126
+ }
127
+ const lineCount = raw.split("\n").filter((l) => l.trim()).length;
128
+ if (lineCount < COMPACT_THRESHOLD_LINES) return;
129
+ try {
130
+ await withLock(async () => {
131
+ const keep = (await readGlobalPidsRaw()).filter((r) => r.status !== "exited" || isProcessAlive(r.pid));
132
+ const tmpFile = resolveGlobalFile() + ".compact";
133
+ await writeFile(tmpFile, keep.map((r) => JSON.stringify(r)).join("\n") + (keep.length ? "\n" : ""));
134
+ await rename(tmpFile, resolveGlobalFile());
135
+ logger.debug(`[globalPidIndex] compacted ${lineCount} → ${keep.length} lines`);
136
+ });
137
+ } catch (error) {
138
+ logger.debug("[globalPidIndex] compact failed:", error);
139
+ }
140
+ }
111
141
 
112
142
  //#endregion
113
- export { readGlobalPids as n, updateGlobalPidStatus as r, appendGlobalPid as t };
114
- //# sourceMappingURL=globalPidIndex-BHCkWll6.js.map
143
+ export { updateGlobalPidStatus as i, maybeCompactGlobalPids as n, readGlobalPids as r, appendGlobalPid as t };
144
+ //# sourceMappingURL=globalPidIndex-Cr-g75QF.js.map
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
- import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-DfT_yx7e.js";
1
+ import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-vc6cm9z6.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import "./pidStore-CYmzzaQQ.js";
4
- import "./globalPidIndex-BHCkWll6.js";
3
+ import "./versionChecker-IVJmMHfo.js";
4
+ import "./pidStore-C1JXxoPi.js";
5
+ import "./globalPidIndex-Cr-g75QF.js";
5
6
 
6
7
  export { AgentContext, CLIS_CONFIG, config, agentYes as default, removeControlCharacters };
@@ -0,0 +1,5 @@
1
+ import "./logger-B9h0djqx.js";
2
+ import { t as PidStore } from "./pidStore-C1JXxoPi.js";
3
+ import "./globalPidIndex-Cr-g75QF.js";
4
+
5
+ export { PidStore };
@@ -1,5 +1,5 @@
1
1
  import { n as logger } from "./logger-B9h0djqx.js";
2
- import { r as updateGlobalPidStatus, t as appendGlobalPid } from "./globalPidIndex-BHCkWll6.js";
2
+ import { i as updateGlobalPidStatus, n as maybeCompactGlobalPids, t as appendGlobalPid } from "./globalPidIndex-Cr-g75QF.js";
3
3
  import { closeSync, existsSync, fsyncSync, openSync } from "fs";
4
4
  import { appendFile, mkdir, readFile, rename, writeFile } from "fs/promises";
5
5
  import path from "path";
@@ -239,7 +239,7 @@ var PidStore = class PidStore {
239
239
  exit_code: null,
240
240
  exit_reason: null,
241
241
  started_at: now
242
- }).catch(() => null);
242
+ }).then(() => maybeCompactGlobalPids()).catch(() => null);
243
243
  return result;
244
244
  }
245
245
  async updateStatus(pid, status, extra) {
@@ -337,4 +337,4 @@ pid-db/
337
337
 
338
338
  //#endregion
339
339
  export { PidStore as t };
340
- //# sourceMappingURL=pidStore-CYmzzaQQ.js.map
340
+ //# sourceMappingURL=pidStore-C1JXxoPi.js.map
@@ -28,6 +28,21 @@ function isProcessRunning(pid) {
28
28
  }
29
29
  }
30
30
  /**
31
+ * Build an env that scrubs inherited GIT_* repo-locating vars so the spawned
32
+ * `git` resolves the repo from `cwd` only. Without this, running inside a git
33
+ * hook (where GIT_DIR / GIT_INDEX_FILE / GIT_WORK_TREE / GIT_COMMON_DIR are
34
+ * exported) makes `git rev-parse --show-toplevel` use the hook's repo context
35
+ * instead of the requested directory.
36
+ */
37
+ function gitCleanEnv() {
38
+ const env = { ...process.env };
39
+ delete env.GIT_DIR;
40
+ delete env.GIT_WORK_TREE;
41
+ delete env.GIT_INDEX_FILE;
42
+ delete env.GIT_COMMON_DIR;
43
+ return env;
44
+ }
45
+ /**
31
46
  * Get git repository root for a directory
32
47
  */
33
48
  function getGitRoot(cwd) {
@@ -39,7 +54,8 @@ function getGitRoot(cwd) {
39
54
  "pipe",
40
55
  "pipe",
41
56
  "ignore"
42
- ]
57
+ ],
58
+ env: gitCleanEnv()
43
59
  }).trim();
44
60
  } catch {
45
61
  return null;
@@ -260,4 +276,4 @@ function shouldUseLock(_cwd) {
260
276
 
261
277
  //#endregion
262
278
  export { shouldUseLock as i, getRunningAgentCount as n, releaseLock as r, acquireLock as t };
263
- //# sourceMappingURL=runningLock-DQWJSptq.js.map
279
+ //# sourceMappingURL=runningLock-C22d9SRJ.js.map
@@ -1,5 +1,5 @@
1
1
  import "./logger-B9h0djqx.js";
2
- import { n as readGlobalPids } from "./globalPidIndex-BHCkWll6.js";
2
+ import { r as readGlobalPids } from "./globalPidIndex-Cr-g75QF.js";
3
3
  import { readFile, stat } from "fs/promises";
4
4
  import { homedir } from "os";
5
5
  import path from "path";
@@ -388,4 +388,4 @@ async function writeToIpc(ipcPath, payload) {
388
388
 
389
389
  //#endregion
390
390
  export { isSubcommand, runSubcommand };
391
- //# sourceMappingURL=subcommands-NWIZy8od.js.map
391
+ //# sourceMappingURL=subcommands-DSm9WL-6.js.map
@@ -1,4 +1,4 @@
1
- import { n as getRunningAgentCount } from "./runningLock-DQWJSptq.js";
1
+ import { n as getRunningAgentCount } from "./runningLock-C22d9SRJ.js";
2
2
  import { existsSync } from "fs";
3
3
  import { mkdir, readFile, unlink, writeFile } from "fs/promises";
4
4
  import { homedir } from "os";
@@ -175,4 +175,4 @@ async function startTray() {
175
175
 
176
176
  //#endregion
177
177
  export { ensureTray, startTray };
178
- //# sourceMappingURL=tray-D5deJPjk.js.map
178
+ //# sourceMappingURL=tray-CH_G7aXM.js.map
@@ -1,13 +1,14 @@
1
1
  import { n as logger, t as addTransport } from "./logger-B9h0djqx.js";
2
- import { n as version } from "./package-DRIilF5m.js";
3
- import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-DQWJSptq.js";
4
- import { t as PidStore } from "./pidStore-CYmzzaQQ.js";
2
+ import { r as getInstalledPackage } from "./versionChecker-IVJmMHfo.js";
3
+ import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-C22d9SRJ.js";
4
+ import { t as PidStore } from "./pidStore-C1JXxoPi.js";
5
5
  import { arch, platform } from "process";
6
6
  import { execSync } from "child_process";
7
7
  import { closeSync, constants, createReadStream, existsSync, mkdirSync, openSync } from "fs";
8
8
  import { mkdir, readFile, readdir, unlink, writeFile } from "fs/promises";
9
9
  import { homedir } from "os";
10
10
  import path, { dirname, join } from "path";
11
+ import { fileURLToPath } from "url";
11
12
  import { execaCommandSync, parseCommandString } from "execa";
12
13
  import { fromReadable, fromWritable } from "from-node-stream";
13
14
  import DIE from "phpdie";
@@ -15,7 +16,6 @@ import sflow from "sflow";
15
16
  import xterm from "@xterm/headless";
16
17
  import { createServer } from "net";
17
18
  import { execSync as execSync$1 } from "node:child_process";
18
- import { fileURLToPath } from "url";
19
19
  import os from "node:os";
20
20
  import { readFile as readFile$1 } from "node:fs/promises";
21
21
  import path$1 from "node:path";
@@ -775,7 +775,7 @@ function spawnAgent(options) {
775
775
  let [bin, ...args] = [...parseCommandString(cliConf?.binary || cli), ...cliArgs];
776
776
  logger.debug(`Spawning ${bin} with args: ${JSON.stringify(args)}`);
777
777
  const spawned = pty.spawn(bin, args, ptyOptions);
778
- logger.info(`[${cli}-yes] Spawned ${bin} with PID ${spawned.pid} (agent-yes v${version})`);
778
+ logger.info(`[${cli}-yes] Spawned ${bin} with PID ${spawned.pid} (agent-yes v${getInstalledPackage().version})`);
779
779
  return spawned;
780
780
  };
781
781
  return tryCatch((error, attempts, spawn, ...args) => {
@@ -1679,4 +1679,4 @@ function sleep(ms) {
1679
1679
 
1680
1680
  //#endregion
1681
1681
  export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
1682
- //# sourceMappingURL=ts-DfT_yx7e.js.map
1682
+ //# sourceMappingURL=ts-vc6cm9z6.js.map
@@ -0,0 +1,224 @@
1
+ import { execFileSync } from "child_process";
2
+ import { existsSync, lstatSync, readFileSync, readlinkSync } from "fs";
3
+ import { mkdir, readFile, writeFile } from "fs/promises";
4
+ import { homedir } from "os";
5
+ import path from "path";
6
+ import { fileURLToPath } from "url";
7
+
8
+ //#region package.json
9
+ var name = "agent-yes";
10
+ var version = "1.75.3";
11
+
12
+ //#endregion
13
+ //#region ts/versionChecker.ts
14
+ const CACHE_DIR = path.join(homedir(), ".cache", "agent-yes");
15
+ const CACHE_FILE = path.join(CACHE_DIR, "update-check.json");
16
+ const TTL_MS = 3600 * 1e3;
17
+ const CANONICAL_PKG_NAME = "agent-yes";
18
+ let cachedInstalledPkg = null;
19
+ /**
20
+ * Read the live `package.json` from disk for the running module.
21
+ *
22
+ * The bundled `package.json` import is inlined at build time; if `dist/` is
23
+ * published without a fresh build (issue #39), the inlined `version` lies
24
+ * and the auto-update loop fires forever. Reading the on-disk manifest each
25
+ * run keeps the version honest even when the bundle is stale.
26
+ */
27
+ function getInstalledPackage() {
28
+ if (cachedInstalledPkg) return cachedInstalledPkg;
29
+ let dir = null;
30
+ try {
31
+ dir = path.dirname(fileURLToPath(import.meta.url));
32
+ } catch {}
33
+ if (dir) for (let i = 0; i < 6; i++) {
34
+ const candidate = path.join(dir, "package.json");
35
+ try {
36
+ if (existsSync(candidate)) {
37
+ const json = JSON.parse(readFileSync(candidate, "utf8"));
38
+ if (json.name === name && typeof json.version === "string") {
39
+ cachedInstalledPkg = {
40
+ name: json.name,
41
+ version: json.version
42
+ };
43
+ return cachedInstalledPkg;
44
+ }
45
+ }
46
+ } catch {}
47
+ const parent = path.dirname(dir);
48
+ if (parent === dir) break;
49
+ dir = parent;
50
+ }
51
+ cachedInstalledPkg = {
52
+ name,
53
+ version
54
+ };
55
+ return cachedInstalledPkg;
56
+ }
57
+ async function readUpdateCache() {
58
+ try {
59
+ const raw = await readFile(CACHE_FILE, "utf8");
60
+ return JSON.parse(raw);
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+ async function writeUpdateCache(data) {
66
+ await mkdir(CACHE_DIR, { recursive: true });
67
+ await writeFile(CACHE_FILE, JSON.stringify(data));
68
+ }
69
+ function detectPackageManager() {
70
+ if (process.env.BUN_INSTALL || process.execPath?.includes("bun") || process.env.npm_execpath?.includes("bun")) return "bun";
71
+ return "npm";
72
+ }
73
+ /**
74
+ * Check for updates, auto-install if newer version is available, and re-exec
75
+ * so the current invocation always runs the latest code.
76
+ *
77
+ * Uses a 1-hour TTL cache to avoid hitting the registry on every run.
78
+ * All errors are swallowed — network issues must never break the tool.
79
+ * Set AGENT_YES_NO_UPDATE=1 to opt out.
80
+ *
81
+ * The AGENT_YES_UPDATED env var prevents infinite re-exec loops:
82
+ * after updating we re-exec with AGENT_YES_UPDATED=<version> so the
83
+ * new process skips the update check.
84
+ */
85
+ async function checkAndAutoUpdate() {
86
+ if (process.env.AGENT_YES_NO_UPDATE) return;
87
+ if (process.env.AGENT_YES_UPDATED) return;
88
+ if (import.meta.url.startsWith("file://") && !import.meta.url.includes("node_modules")) {
89
+ const scriptDir = path.dirname(new URL(import.meta.url).pathname);
90
+ const repoRoot = path.resolve(scriptDir, "..");
91
+ if (existsSync(path.join(repoRoot, ".git"))) return;
92
+ }
93
+ try {
94
+ let latestVersion;
95
+ const cache = await readUpdateCache();
96
+ if (cache && Date.now() - cache.checkedAt < TTL_MS) latestVersion = cache.latestVersion;
97
+ else {
98
+ const fetched = await fetchLatestVersion();
99
+ if (!fetched) return;
100
+ latestVersion = fetched;
101
+ await writeUpdateCache({
102
+ checkedAt: Date.now(),
103
+ latestVersion
104
+ });
105
+ }
106
+ if (compareVersions(getInstalledPackage().version, latestVersion) < 0) {
107
+ if (await runInstall(latestVersion)) reExec(latestVersion);
108
+ }
109
+ } catch {}
110
+ }
111
+ async function runInstall(latestVersion) {
112
+ const installCmd = detectPackageManager() === "bun" ? `bun add -g ${CANONICAL_PKG_NAME}@${latestVersion}` : `npm install -g ${CANONICAL_PKG_NAME}@${latestVersion}`;
113
+ process.stderr.write(`\x1b[33m[agent-yes] Updating ${getInstalledPackage().version} → ${latestVersion}…\x1b[0m\n`);
114
+ try {
115
+ const { execaCommand } = await import("execa");
116
+ await execaCommand(installCmd, { stdio: "inherit" });
117
+ process.stderr.write(`\x1b[32m[agent-yes] Updated to ${latestVersion}\x1b[0m\n`);
118
+ return true;
119
+ } catch {
120
+ process.stderr.write(`\x1b[31m[agent-yes] Auto-update failed. Run: ${installCmd}\x1b[0m\n`);
121
+ return false;
122
+ }
123
+ }
124
+ /**
125
+ * Re-exec the current process so the newly installed version runs.
126
+ * Sets AGENT_YES_UPDATED=<version> to prevent an infinite loop.
127
+ */
128
+ function reExec(version) {
129
+ const [bin, ...args] = process.argv;
130
+ process.stderr.write(`\x1b[36m[agent-yes] Restarting with v${version}…\x1b[0m\n`);
131
+ try {
132
+ execFileSync(bin, args, {
133
+ stdio: "inherit",
134
+ env: {
135
+ ...process.env,
136
+ AGENT_YES_UPDATED: version
137
+ }
138
+ });
139
+ process.exit(0);
140
+ } catch (err) {
141
+ process.exit(err.status ?? 1);
142
+ }
143
+ }
144
+ /**
145
+ * Fetch the latest version of the package from npm registry
146
+ */
147
+ async function fetchLatestVersion() {
148
+ try {
149
+ const response = await fetch(`https://registry.npmjs.org/${CANONICAL_PKG_NAME}/latest`, { signal: AbortSignal.timeout(3e3) });
150
+ if (!response.ok) return null;
151
+ return (await response.json()).version;
152
+ } catch {
153
+ return null;
154
+ }
155
+ }
156
+ /**
157
+ * Compare two semantic versions
158
+ * Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
159
+ */
160
+ function compareVersions(v1, v2) {
161
+ const parts1 = v1.split(".").map(Number);
162
+ const parts2 = v2.split(".").map(Number);
163
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
164
+ const part1 = parts1[i] || 0;
165
+ const part2 = parts2[i] || 0;
166
+ if (part1 > part2) return 1;
167
+ if (part1 < part2) return -1;
168
+ }
169
+ return 0;
170
+ }
171
+ /**
172
+ * Detect how agent-yes was installed.
173
+ * Returns a short label: "git", "bun link", "bun", "npm", "npx", or "unknown"
174
+ */
175
+ function detectInstallMethod() {
176
+ try {
177
+ const scriptDir = path.dirname(new URL(import.meta.url).pathname);
178
+ if (!scriptDir.includes("node_modules")) {
179
+ const repoRoot = path.resolve(scriptDir, "..");
180
+ if (existsSync(path.join(repoRoot, ".git"))) return "git";
181
+ return "source";
182
+ }
183
+ const nodeModulesEntry = scriptDir.replace(/\/dist$/, "");
184
+ try {
185
+ if (lstatSync(nodeModulesEntry).isSymbolicLink()) {
186
+ const target = readlinkSync(nodeModulesEntry);
187
+ const resolvedTarget = path.resolve(path.dirname(nodeModulesEntry), target);
188
+ if (existsSync(path.join(resolvedTarget, ".git"))) return "bun link (git)";
189
+ return "bun link";
190
+ }
191
+ } catch {}
192
+ if (scriptDir.includes(".bun/")) return "bun";
193
+ if (scriptDir.includes(".npm/")) return "npx";
194
+ if (process.env.npm_execpath?.includes("bun")) return "bun";
195
+ if (process.env.npm_config_user_agent?.startsWith("bun")) return "bun";
196
+ if (process.env.npm_config_user_agent?.startsWith("npm")) return "npm";
197
+ return "npm";
198
+ } catch {
199
+ return "unknown";
200
+ }
201
+ }
202
+ /**
203
+ * Format version string with install method
204
+ */
205
+ function versionString() {
206
+ return `agent-yes v${getInstalledPackage().version} (${detectInstallMethod()})`;
207
+ }
208
+ /**
209
+ * Display version information with async latest version check
210
+ */
211
+ async function displayVersion() {
212
+ console.log(versionString());
213
+ const latestVersion = await fetchLatestVersion();
214
+ if (latestVersion) {
215
+ const comparison = compareVersions(getInstalledPackage().version, latestVersion);
216
+ if (comparison < 0) console.log(`\x1b[33m${latestVersion} (update available)\x1b[0m`);
217
+ else if (comparison > 0) console.log(`${latestVersion} (latest published)`);
218
+ else console.log(`${latestVersion} (latest)`);
219
+ } else console.log("(unable to check for updates)");
220
+ }
221
+
222
+ //#endregion
223
+ export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
224
+ //# sourceMappingURL=versionChecker-IVJmMHfo.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.75.1",
3
+ "version": "1.75.3",
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",
@@ -77,7 +77,7 @@
77
77
  "fmt": "oxlint --fix --fix-suggestions && oxfmt",
78
78
  "verify-deps": "node ./scripts/verify-deps.js",
79
79
  "_prepack": "bun run build",
80
- "prepublishOnly": "npm run verify-deps",
80
+ "prepublishOnly": "bun run build && npm run verify-deps",
81
81
  "prepare": "husky",
82
82
  "release": "standard-version && npm publish",
83
83
  "release:beta": "standard-version && npm publish --tag beta",
@@ -142,6 +142,69 @@ describe("globalPidIndex", () => {
142
142
  expect(records).toEqual([]);
143
143
  });
144
144
 
145
+ it("maybeCompactGlobalPids no-ops when below threshold", async () => {
146
+ const mod = await loadModule();
147
+ await mod.appendGlobalPid({
148
+ pid: 1234,
149
+ cli: "claude",
150
+ prompt: null,
151
+ cwd: "/a",
152
+ log_file: null,
153
+ status: "active",
154
+ exit_code: null,
155
+ exit_reason: null,
156
+ started_at: 1,
157
+ });
158
+ const before = (await import("fs/promises")).readFile;
159
+ const beforeContent = await before(mod.getGlobalPidIndexPath(), "utf-8");
160
+ await mod.maybeCompactGlobalPids();
161
+ const afterContent = await before(mod.getGlobalPidIndexPath(), "utf-8");
162
+ expect(afterContent).toBe(beforeContent);
163
+ });
164
+
165
+ it("maybeCompactGlobalPids collapses event spam to one line per pid", async () => {
166
+ const mod = await loadModule();
167
+ // Emit > 500 status events across two pids (one alive, one will be exited+dead)
168
+ for (let i = 0; i < 260; i++) {
169
+ await mod.appendGlobalPid({
170
+ pid: process.pid,
171
+ cli: "claude",
172
+ prompt: null,
173
+ cwd: "/a",
174
+ log_file: null,
175
+ status: "active",
176
+ exit_code: null,
177
+ exit_reason: null,
178
+ started_at: 1,
179
+ });
180
+ await mod.appendGlobalPid({
181
+ pid: 999999, // dead
182
+ cli: "codex",
183
+ prompt: null,
184
+ cwd: "/b",
185
+ log_file: null,
186
+ status: "exited",
187
+ exit_code: 0,
188
+ exit_reason: "done",
189
+ started_at: 1,
190
+ });
191
+ }
192
+ const fs = await import("fs/promises");
193
+ const before = (await fs.readFile(mod.getGlobalPidIndexPath(), "utf-8")).split("\n").length;
194
+ await mod.maybeCompactGlobalPids();
195
+ const after = (await fs.readFile(mod.getGlobalPidIndexPath(), "utf-8")).split("\n").length;
196
+ // Compaction must have shrunk the file dramatically and dropped the
197
+ // dead-and-exited pid 999999 entirely.
198
+ expect(after).toBeLessThan(before / 10);
199
+ const records = await mod.readGlobalPids();
200
+ expect(records.map((r) => r.pid)).toEqual([process.pid]);
201
+ });
202
+
203
+ it("maybeCompactGlobalPids on missing file is a noop", async () => {
204
+ const mod = await loadModule();
205
+ await mod.maybeCompactGlobalPids(); // no throw, no error
206
+ });
207
+
145
208
  it("skips corrupt lines without throwing", async () => {
146
209
  const mod = await loadModule();
147
210
  await mod.appendGlobalPid({
@@ -1,4 +1,4 @@
1
- import { appendFile, mkdir, readFile } from "fs/promises";
1
+ import { appendFile, mkdir, readFile, rename, writeFile } from "fs/promises";
2
2
  import { homedir } from "os";
3
3
  import path from "path";
4
4
  import { lock } from "proper-lockfile";
@@ -153,3 +153,40 @@ function isProcessAlive(pid: number): boolean {
153
153
  return false;
154
154
  }
155
155
  }
156
+
157
+ const COMPACT_THRESHOLD_LINES = 500; // raw events; one merged record per pid
158
+
159
+ /**
160
+ * Best-effort compaction: rewrite the JSONL file with one line per known pid,
161
+ * dropping records whose pid is dead AND status is exited (those won't be
162
+ * referenced by `cy ls` anyway). Triggered opportunistically when the raw
163
+ * file grows past `COMPACT_THRESHOLD_LINES`. Safe to call unconditionally;
164
+ * it no-ops when the file is already small enough.
165
+ */
166
+ export async function maybeCompactGlobalPids(): Promise<void> {
167
+ let raw: string;
168
+ try {
169
+ raw = await readFile(resolveGlobalFile(), "utf-8");
170
+ } catch (err: any) {
171
+ if (err.code === "ENOENT") return;
172
+ return;
173
+ }
174
+ const lineCount = raw.split("\n").filter((l) => l.trim()).length;
175
+ if (lineCount < COMPACT_THRESHOLD_LINES) return;
176
+
177
+ try {
178
+ await withLock(async () => {
179
+ const merged = await readGlobalPidsRaw();
180
+ // Drop dead-and-exited entries; keep dead-but-not-yet-exited so a later
181
+ // status-update from elsewhere can still be matched against them.
182
+ const keep = merged.filter((r) => r.status !== "exited" || isProcessAlive(r.pid));
183
+ const tmpFile = resolveGlobalFile() + ".compact";
184
+ const content = keep.map((r) => JSON.stringify(r)).join("\n") + (keep.length ? "\n" : "");
185
+ await writeFile(tmpFile, content);
186
+ await rename(tmpFile, resolveGlobalFile());
187
+ logger.debug(`[globalPidIndex] compacted ${lineCount} → ${keep.length} lines`);
188
+ });
189
+ } catch (error) {
190
+ logger.debug("[globalPidIndex] compact failed:", error);
191
+ }
192
+ }
package/ts/pidStore.ts CHANGED
@@ -2,7 +2,11 @@ import { mkdir, writeFile } from "fs/promises";
2
2
  import path from "path";
3
3
  import { logger } from "./logger.ts";
4
4
  import { JsonlStore } from "./JsonlStore.ts";
5
- import { appendGlobalPid, updateGlobalPidStatus } from "./globalPidIndex.ts";
5
+ import {
6
+ appendGlobalPid,
7
+ maybeCompactGlobalPids,
8
+ updateGlobalPidStatus,
9
+ } from "./globalPidIndex.ts";
6
10
 
7
11
  export interface PidRecord {
8
12
  _id?: string;
@@ -100,7 +104,9 @@ export class PidStore {
100
104
  exit_code: null,
101
105
  exit_reason: null,
102
106
  started_at: now,
103
- }).catch(() => null);
107
+ })
108
+ .then(() => maybeCompactGlobalPids())
109
+ .catch(() => null);
104
110
 
105
111
  return result;
106
112
  }
package/ts/runningLock.ts CHANGED
@@ -43,6 +43,22 @@ function isProcessRunning(pid: number): boolean {
43
43
  }
44
44
  }
45
45
 
46
+ /**
47
+ * Build an env that scrubs inherited GIT_* repo-locating vars so the spawned
48
+ * `git` resolves the repo from `cwd` only. Without this, running inside a git
49
+ * hook (where GIT_DIR / GIT_INDEX_FILE / GIT_WORK_TREE / GIT_COMMON_DIR are
50
+ * exported) makes `git rev-parse --show-toplevel` use the hook's repo context
51
+ * instead of the requested directory.
52
+ */
53
+ function gitCleanEnv(): NodeJS.ProcessEnv {
54
+ const env: NodeJS.ProcessEnv = { ...process.env };
55
+ delete env.GIT_DIR;
56
+ delete env.GIT_WORK_TREE;
57
+ delete env.GIT_INDEX_FILE;
58
+ delete env.GIT_COMMON_DIR;
59
+ return env;
60
+ }
61
+
46
62
  /**
47
63
  * Get git repository root for a directory
48
64
  */
@@ -52,6 +68,7 @@ function getGitRoot(cwd: string): string | null {
52
68
  cwd,
53
69
  encoding: "utf8",
54
70
  stdio: ["pipe", "pipe", "ignore"],
71
+ env: gitCleanEnv(),
55
72
  });
56
73
  return result.trim();
57
74
  } catch {
package/ts/rustBinary.ts CHANGED
@@ -6,7 +6,7 @@ import { execFileSync } from "child_process";
6
6
  import { existsSync, mkdirSync, unlinkSync } from "fs";
7
7
  import { chmod, copyFile } from "fs/promises";
8
8
  import path from "path";
9
- import pkg from "../package.json" with { type: "json" };
9
+ import { getInstalledPackage } from "./versionChecker.ts";
10
10
 
11
11
  // Platform/arch to binary name mapping
12
12
  const PLATFORM_MAP: Record<string, string> = {
@@ -223,11 +223,12 @@ function autoRebuildIfOutdated(binaryPath: string, verbose: boolean): boolean {
223
223
  }
224
224
 
225
225
  const binaryVersion = getRustBinaryVersion(binaryPath);
226
+ const pkgVersion = getInstalledPackage().version;
226
227
  if (verbose) {
227
- console.log(`[rust] Binary version: ${binaryVersion}, package version: ${pkg.version}`);
228
+ console.log(`[rust] Binary version: ${binaryVersion}, package version: ${pkgVersion}`);
228
229
  }
229
230
 
230
- if (binaryVersion === pkg.version) {
231
+ if (binaryVersion === pkgVersion) {
231
232
  return true; // up to date
232
233
  }
233
234
 
@@ -239,7 +240,7 @@ function autoRebuildIfOutdated(binaryPath: string, verbose: boolean): boolean {
239
240
  }
240
241
 
241
242
  process.stderr.write(
242
- `\x1b[33m[rust] Binary outdated (${binaryVersion ?? "unknown"} → ${pkg.version}), rebuilding…\x1b[0m\n`,
243
+ `\x1b[33m[rust] Binary outdated (${binaryVersion ?? "unknown"} → ${pkgVersion}), rebuilding…\x1b[0m\n`,
243
244
  );
244
245
 
245
246
  try {
@@ -1,12 +1,14 @@
1
1
  import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
2
2
  import {
3
+ _setInstalledPackageForTesting,
3
4
  checkAndAutoUpdate,
4
5
  compareVersions,
5
6
  fetchLatestVersion,
6
7
  displayVersion,
7
8
  detectInstallMethod,
9
+ getInstalledPackage,
8
10
  versionString,
9
- } from "./versionChecker";
11
+ } from "./versionChecker.ts";
10
12
 
11
13
  vi.mock("execa", () => ({ execaCommand: vi.fn().mockResolvedValue({}) }));
12
14
  vi.mock("fs/promises", () => ({
@@ -18,8 +20,12 @@ vi.mock("fs", async (importOriginal) => {
18
20
  const actual = await importOriginal<typeof import("fs")>();
19
21
  return {
20
22
  ...actual,
21
- // Return false for .git checks so the dev-checkout guard doesn't skip auto-update in tests
23
+ // Return false for .git / package.json lookups so neither the dev-checkout
24
+ // guard nor getInstalledPackage's disk read fires during the auto-update tests.
22
25
  existsSync: vi.fn(() => false),
26
+ readFileSync: vi.fn(() => {
27
+ throw new Error("readFileSync not stubbed");
28
+ }),
23
29
  lstatSync: actual.lstatSync,
24
30
  readlinkSync: actual.readlinkSync,
25
31
  };
@@ -99,6 +105,7 @@ describe("versionChecker", () => {
99
105
  describe("checkAndAutoUpdate", () => {
100
106
  beforeEach(() => {
101
107
  vi.clearAllMocks();
108
+ _setInstalledPackageForTesting(null);
102
109
  vi.stubGlobal("fetch", vi.fn());
103
110
  vi.spyOn(process.stderr, "write").mockImplementation(() => true);
104
111
  // Use a mock for process.exit to prevent actual exit in tests
@@ -300,4 +307,77 @@ describe("versionChecker", () => {
300
307
  expect(str).toMatch(/agent-yes v\d+\.\d+\.\d+ \(.+\)/);
301
308
  });
302
309
  });
310
+
311
+ // Regression test for https://github.com/snomiao/agent-yes/issues/39:
312
+ // a stale bundled version string must not pin the auto-update comparison
313
+ // when a fresh package.json is on disk next to the running module.
314
+ describe("getInstalledPackage (issue #39)", () => {
315
+ beforeEach(() => {
316
+ vi.clearAllMocks();
317
+ _setInstalledPackageForTesting(null);
318
+ });
319
+
320
+ afterEach(() => {
321
+ vi.restoreAllMocks();
322
+ _setInstalledPackageForTesting(null);
323
+ });
324
+
325
+ it("prefers the on-disk package.json over the bundled (potentially stale) import", async () => {
326
+ const fs = await import("fs");
327
+ vi.mocked(fs.existsSync).mockReturnValueOnce(true);
328
+ vi.mocked(fs.readFileSync).mockReturnValueOnce(
329
+ JSON.stringify({ name: "agent-yes", version: "999.0.0" }) as any,
330
+ );
331
+
332
+ const resolved = getInstalledPackage();
333
+ expect(resolved.version).toBe("999.0.0");
334
+ expect(resolved.name).toBe("agent-yes");
335
+ });
336
+
337
+ it("continues walking parents when a candidate package.json is unreadable", async () => {
338
+ // Per-candidate try/catch: an unreadable/unparsable manifest at one
339
+ // level must not abort the upward walk and silently fall back to the
340
+ // bundled (stale) manifest. The walk must keep going until it finds
341
+ // a matching package.json or exhausts parents.
342
+ const fs = await import("fs");
343
+ let call = 0;
344
+ vi.mocked(fs.existsSync).mockImplementation(() => true);
345
+ vi.mocked(fs.readFileSync).mockImplementation(() => {
346
+ call += 1;
347
+ if (call === 1) throw new Error("EACCES");
348
+ if (call === 2) return "{not json" as any;
349
+ return JSON.stringify({ name: "agent-yes", version: "999.0.0" }) as any;
350
+ });
351
+
352
+ const resolved = getInstalledPackage();
353
+ expect(resolved.version).toBe("999.0.0");
354
+ expect(call).toBeGreaterThanOrEqual(3);
355
+ });
356
+
357
+ it("does not trigger an auto-update when on-disk version already matches the registry", async () => {
358
+ // Simulate the post-fix scenario: the bundled `pkg.version` (frozen at
359
+ // build time) is older than the registry, but the runtime resolver
360
+ // surfaces the correct on-disk version and the comparison short-circuits.
361
+ // Pre-fix behavior was install + reExec on every invocation → infinite loop.
362
+ _setInstalledPackageForTesting({ name: "agent-yes", version: "999.0.0" });
363
+
364
+ const { readFile } = await import("fs/promises");
365
+ const { execaCommand } = await import("execa");
366
+ vi.mocked(readFile).mockRejectedValueOnce(new Error("no cache"));
367
+ vi.stubGlobal(
368
+ "fetch",
369
+ vi.fn().mockResolvedValue({
370
+ ok: true,
371
+ json: async () => ({ version: "999.0.0" }),
372
+ } as Response),
373
+ );
374
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
375
+ vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
376
+ delete process.env.AGENT_YES_NO_UPDATE;
377
+ delete process.env.AGENT_YES_UPDATED;
378
+
379
+ await checkAndAutoUpdate();
380
+ expect(execaCommand).not.toHaveBeenCalled();
381
+ });
382
+ });
303
383
  });
@@ -1,14 +1,80 @@
1
1
  import { execFileSync } from "child_process";
2
- import { existsSync, lstatSync, readlinkSync } from "fs";
2
+ import { existsSync, lstatSync, readFileSync, readlinkSync } from "fs";
3
3
  import { mkdir, readFile, writeFile } from "fs/promises";
4
4
  import { homedir } from "os";
5
5
  import path from "path";
6
- import pkg from "../package.json" with { type: "json" };
6
+ import { fileURLToPath } from "url";
7
+ import bundledPkg from "../package.json" with { type: "json" };
7
8
 
8
9
  const CACHE_DIR = path.join(homedir(), ".cache", "agent-yes");
9
10
  const CACHE_FILE = path.join(CACHE_DIR, "update-check.json");
10
11
  const TTL_MS = 60 * 60 * 1000; // 1 hour
11
12
 
13
+ // The release pipeline publishes both `agent-yes` and `claude-yes` from the
14
+ // same source by flipping `package.json#name` and re-running `npm publish`
15
+ // (which now triggers `bun run build`, rebuilding dist with whichever name is
16
+ // set). The auto-updater's registry lookup, install command, and shared
17
+ // cache file must all stay pinned to the canonical package — otherwise a
18
+ // `claude-yes` install would query `claude-yes/latest` while `runInstall`
19
+ // still hard-codes `agent-yes`.
20
+ const CANONICAL_PKG_NAME = "agent-yes";
21
+
22
+ let cachedInstalledPkg: { name: string; version: string } | null = null;
23
+
24
+ /**
25
+ * Read the live `package.json` from disk for the running module.
26
+ *
27
+ * The bundled `package.json` import is inlined at build time; if `dist/` is
28
+ * published without a fresh build (issue #39), the inlined `version` lies
29
+ * and the auto-update loop fires forever. Reading the on-disk manifest each
30
+ * run keeps the version honest even when the bundle is stale.
31
+ */
32
+ export function getInstalledPackage(): { name: string; version: string } {
33
+ if (cachedInstalledPkg) return cachedInstalledPkg;
34
+ let dir: string | null = null;
35
+ try {
36
+ dir = path.dirname(fileURLToPath(import.meta.url));
37
+ } catch {
38
+ // import.meta.url malformed; fall through to bundled
39
+ }
40
+ if (dir) {
41
+ for (let i = 0; i < 6; i++) {
42
+ const candidate = path.join(dir, "package.json");
43
+ // A per-candidate try/catch: a transient read error, partial write, or
44
+ // BOM on any single package.json must NOT abort the upward walk —
45
+ // otherwise we'd silently fall back to the stale bundled manifest that
46
+ // issue #39 was about. Keep walking until we either find a matching
47
+ // manifest or exhaust parents.
48
+ try {
49
+ if (existsSync(candidate)) {
50
+ const json = JSON.parse(readFileSync(candidate, "utf8")) as {
51
+ name?: string;
52
+ version?: string;
53
+ };
54
+ if (json.name === bundledPkg.name && typeof json.version === "string") {
55
+ cachedInstalledPkg = { name: json.name, version: json.version };
56
+ return cachedInstalledPkg;
57
+ }
58
+ }
59
+ } catch {
60
+ // unreadable / unparsable — continue walking
61
+ }
62
+ const parent = path.dirname(dir);
63
+ if (parent === dir) break;
64
+ dir = parent;
65
+ }
66
+ }
67
+ cachedInstalledPkg = { name: bundledPkg.name, version: bundledPkg.version };
68
+ return cachedInstalledPkg;
69
+ }
70
+
71
+ /** Test-only: clear or seed the memoized lookup. */
72
+ export function _setInstalledPackageForTesting(
73
+ value: { name: string; version: string } | null,
74
+ ): void {
75
+ cachedInstalledPkg = value;
76
+ }
77
+
12
78
  type UpdateCache = { checkedAt: number; latestVersion: string };
13
79
 
14
80
  async function readUpdateCache(): Promise<UpdateCache | null> {
@@ -75,7 +141,7 @@ export async function checkAndAutoUpdate(): Promise<void> {
75
141
  await writeUpdateCache({ checkedAt: Date.now(), latestVersion });
76
142
  }
77
143
 
78
- if (compareVersions(pkg.version, latestVersion) < 0) {
144
+ if (compareVersions(getInstalledPackage().version, latestVersion) < 0) {
79
145
  const installed = await runInstall(latestVersion);
80
146
  if (installed) {
81
147
  reExec(latestVersion);
@@ -90,10 +156,12 @@ async function runInstall(latestVersion: string): Promise<boolean> {
90
156
  const pm = detectPackageManager();
91
157
  const installCmd =
92
158
  pm === "bun"
93
- ? `bun add -g agent-yes@${latestVersion}`
94
- : `npm install -g agent-yes@${latestVersion}`;
159
+ ? `bun add -g ${CANONICAL_PKG_NAME}@${latestVersion}`
160
+ : `npm install -g ${CANONICAL_PKG_NAME}@${latestVersion}`;
95
161
 
96
- process.stderr.write(`\x1b[33m[agent-yes] Updating ${pkg.version} → ${latestVersion}…\x1b[0m\n`);
162
+ process.stderr.write(
163
+ `\x1b[33m[agent-yes] Updating ${getInstalledPackage().version} → ${latestVersion}…\x1b[0m\n`,
164
+ );
97
165
  try {
98
166
  const { execaCommand } = await import("execa");
99
167
  await execaCommand(installCmd, { stdio: "inherit" });
@@ -128,7 +196,7 @@ function reExec(version: string): never {
128
196
  */
129
197
  export async function fetchLatestVersion(): Promise<string | null> {
130
198
  try {
131
- const response = await fetch(`https://registry.npmjs.org/${pkg.name}/latest`, {
199
+ const response = await fetch(`https://registry.npmjs.org/${CANONICAL_PKG_NAME}/latest`, {
132
200
  signal: AbortSignal.timeout(3000), // 3 second timeout
133
201
  });
134
202
 
@@ -215,7 +283,7 @@ export function detectInstallMethod(): string {
215
283
  * Format version string with install method
216
284
  */
217
285
  export function versionString(): string {
218
- return `agent-yes v${pkg.version} (${detectInstallMethod()})`;
286
+ return `agent-yes v${getInstalledPackage().version} (${detectInstallMethod()})`;
219
287
  }
220
288
 
221
289
  /**
@@ -227,7 +295,7 @@ export async function displayVersion(): Promise<void> {
227
295
  const latestVersion = await fetchLatestVersion();
228
296
 
229
297
  if (latestVersion) {
230
- const comparison = compareVersions(pkg.version, latestVersion);
298
+ const comparison = compareVersions(getInstalledPackage().version, latestVersion);
231
299
 
232
300
  if (comparison < 0) {
233
301
  console.log(`\x1b[33m${latestVersion} (update available)\x1b[0m`);
@@ -1,11 +0,0 @@
1
- import { t as CLIS_CONFIG } from "./ts-DfT_yx7e.js";
2
- import "./logger-B9h0djqx.js";
3
- import "./pidStore-CYmzzaQQ.js";
4
- import "./globalPidIndex-BHCkWll6.js";
5
-
6
- //#region ts/SUPPORTED_CLIS.ts
7
- const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
8
-
9
- //#endregion
10
- export { SUPPORTED_CLIS };
11
- //# sourceMappingURL=SUPPORTED_CLIS-CqAT1Zud.js.map
@@ -1,7 +0,0 @@
1
- //#region package.json
2
- var name = "agent-yes";
3
- var version = "1.75.0";
4
-
5
- //#endregion
6
- export { version as n, name as t };
7
- //# sourceMappingURL=package-DRIilF5m.js.map
@@ -1,5 +0,0 @@
1
- import "./logger-B9h0djqx.js";
2
- import { t as PidStore } from "./pidStore-CYmzzaQQ.js";
3
- import "./globalPidIndex-BHCkWll6.js";
4
-
5
- export { PidStore };