@phnx-labs/agents-cli 1.22.41 → 1.22.43

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 (253) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +10 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/bootstrap.js +25 -7
  5. package/dist/cli/command-registry.d.ts +2 -3
  6. package/dist/cli/command-registry.js +4 -8
  7. package/dist/commands/accounts.d.ts +29 -0
  8. package/dist/commands/accounts.js +220 -29
  9. package/dist/commands/alias.d.ts +7 -4
  10. package/dist/commands/alias.js +34 -9
  11. package/dist/commands/auth.d.ts +9 -0
  12. package/dist/commands/auth.js +108 -0
  13. package/dist/commands/browser-sessions-picker.js +42 -62
  14. package/dist/commands/browser.js +30 -38
  15. package/dist/commands/computer-actions.d.ts +1 -1
  16. package/dist/commands/computer-actions.js +1 -1
  17. package/dist/commands/computer-sessions-picker.js +32 -55
  18. package/dist/commands/computer.d.ts +1 -1
  19. package/dist/commands/computer.js +22 -8
  20. package/dist/commands/daemon.js +1 -1
  21. package/dist/commands/doctor.d.ts +5 -3
  22. package/dist/commands/doctor.js +23 -8
  23. package/dist/commands/exec.js +3 -2
  24. package/dist/commands/feed.js +2 -31
  25. package/dist/commands/harness.js +3 -3
  26. package/dist/commands/hooks.js +1 -1
  27. package/dist/commands/import.js +1 -1
  28. package/dist/commands/insights.js +163 -85
  29. package/dist/commands/inspect.js +2 -2
  30. package/dist/commands/mine.js +1 -1
  31. package/dist/commands/monitors.js +1 -1
  32. package/dist/commands/org.d.ts +6 -0
  33. package/dist/commands/org.js +175 -0
  34. package/dist/commands/packages.js +1 -1
  35. package/dist/commands/profiles.js +2 -2
  36. package/dist/commands/prune.js +1 -1
  37. package/dist/commands/routines.js +44 -26
  38. package/dist/commands/run-account-picker.d.ts +19 -0
  39. package/dist/commands/run-account-picker.js +81 -0
  40. package/dist/commands/sessions-picker-factory.d.ts +40 -0
  41. package/dist/commands/sessions-picker-factory.js +67 -0
  42. package/dist/commands/sessions-picker.js +124 -122
  43. package/dist/commands/setup-browser.js +4 -4
  44. package/dist/commands/setup-mine.js +1 -1
  45. package/dist/commands/setup-preferences.js +2 -2
  46. package/dist/commands/setup.js +10 -4
  47. package/dist/commands/snapshot.d.ts +1 -1
  48. package/dist/commands/snapshot.js +4 -4
  49. package/dist/commands/status.d.ts +4 -2
  50. package/dist/commands/status.js +10 -7
  51. package/dist/commands/sync.d.ts +1 -0
  52. package/dist/commands/sync.js +30 -3
  53. package/dist/commands/versions.js +1 -1
  54. package/dist/commands/view.js +7 -1
  55. package/dist/commands/webhook.js +1 -1
  56. package/dist/lib/account-capabilities.d.ts +10 -0
  57. package/dist/lib/account-capabilities.js +34 -0
  58. package/dist/lib/accounting/rotate.d.ts +5 -3
  59. package/dist/lib/accounting/rotate.js +36 -7
  60. package/dist/lib/accounting/usage.d.ts +18 -4
  61. package/dist/lib/accounting/usage.js +138 -27
  62. package/dist/lib/agent-spec/agents.d.ts +584 -0
  63. package/dist/lib/agent-spec/agents.js +3127 -0
  64. package/dist/lib/agent-spec/provider.js +1 -1
  65. package/dist/lib/agents.d.ts +2 -584
  66. package/dist/lib/agents.js +2 -3127
  67. package/dist/lib/browser/drivers/local.d.ts +8 -2
  68. package/dist/lib/browser/drivers/local.js +8 -2
  69. package/dist/lib/browser/drivers/ssh.d.ts +8 -2
  70. package/dist/lib/browser/drivers/ssh.js +10 -4
  71. package/dist/lib/browser/hygiene.d.ts +1 -1
  72. package/dist/lib/browser/hygiene.js +5 -1
  73. package/dist/lib/browser/profiles.d.ts +74 -1
  74. package/dist/lib/browser/profiles.js +114 -5
  75. package/dist/lib/browser/runtime-state.d.ts +11 -5
  76. package/dist/lib/browser/runtime-state.js +13 -9
  77. package/dist/lib/browser/service.d.ts +68 -30
  78. package/dist/lib/browser/service.js +305 -194
  79. package/dist/lib/browser/types.d.ts +95 -3
  80. package/dist/lib/browser/types.js +51 -0
  81. package/dist/lib/catchup.js +1 -1
  82. package/dist/lib/cloud/rush.d.ts +0 -15
  83. package/dist/lib/cloud/rush.js +0 -35
  84. package/dist/lib/computer/actions.d.ts +1 -1
  85. package/dist/lib/computer/actions.js +1 -1
  86. package/dist/lib/{computer-rpc.js → computer/computer-rpc.js} +5 -5
  87. package/dist/lib/computer/dispatch.d.ts +1 -1
  88. package/dist/lib/computer/download.d.ts +1 -1
  89. package/dist/lib/computer/download.js +3 -3
  90. package/dist/lib/{ssh-tunnel.d.ts → computer/ssh-tunnel.d.ts} +1 -1
  91. package/dist/lib/{ssh-tunnel.js → computer/ssh-tunnel.js} +12 -12
  92. package/dist/lib/crabbox/runtimes.d.ts +6 -2
  93. package/dist/lib/crabbox/runtimes.js +21 -5
  94. package/dist/lib/daemon/daemon.js +87 -59
  95. package/dist/lib/{runner.d.ts → daemon/runner.d.ts} +28 -11
  96. package/dist/lib/{runner.js → daemon/runner.js} +143 -123
  97. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  98. package/dist/lib/devices/doctor-findings.js +4 -4
  99. package/dist/lib/devices/fleet-inventory.js +1 -1
  100. package/dist/lib/devices/harness-inventory.js +1 -1
  101. package/dist/lib/devices/health-report.js +1 -1
  102. package/dist/lib/devices/registry.js +4 -22
  103. package/dist/lib/doctor-diff.d.ts +1 -1
  104. package/dist/lib/doctor-diff.js +1 -1
  105. package/dist/lib/drift-sync.d.ts +1 -1
  106. package/dist/lib/drift-sync.js +2 -2
  107. package/dist/lib/drift.d.ts +1 -1
  108. package/dist/lib/drift.js +1 -1
  109. package/dist/lib/entitlement.d.ts +31 -0
  110. package/dist/lib/entitlement.js +137 -0
  111. package/dist/lib/exec.js +52 -205
  112. package/dist/lib/feed/feed.js +2 -8
  113. package/dist/lib/format.d.ts +9 -0
  114. package/dist/lib/format.js +16 -0
  115. package/dist/lib/fs-atomic.d.ts +7 -0
  116. package/dist/lib/fs-atomic.js +9 -0
  117. package/dist/lib/github/pr-mergeable.d.ts +49 -0
  118. package/dist/lib/github/pr-mergeable.js +157 -0
  119. package/dist/lib/github/pr-verdict.d.ts +67 -0
  120. package/dist/lib/github/pr-verdict.js +72 -0
  121. package/dist/lib/harness/adapter.d.ts +161 -0
  122. package/dist/lib/harness/adapter.js +47 -0
  123. package/dist/lib/harness/adapters/claude.d.ts +2 -0
  124. package/dist/lib/harness/adapters/claude.js +110 -0
  125. package/dist/lib/harness/adapters/codex.d.ts +2 -0
  126. package/dist/lib/harness/adapters/codex.js +83 -0
  127. package/dist/lib/harness/adapters/copilot.d.ts +2 -0
  128. package/dist/lib/harness/adapters/copilot.js +23 -0
  129. package/dist/lib/harness/adapters/cursor.d.ts +2 -0
  130. package/dist/lib/harness/adapters/cursor.js +38 -0
  131. package/dist/lib/harness/adapters/droid.d.ts +2 -0
  132. package/dist/lib/harness/adapters/droid.js +17 -0
  133. package/dist/lib/harness/adapters/grok.d.ts +2 -0
  134. package/dist/lib/harness/adapters/grok.js +14 -0
  135. package/dist/lib/harness/adapters/kimi.d.ts +2 -0
  136. package/dist/lib/harness/adapters/kimi.js +49 -0
  137. package/dist/lib/harness/adapters/muse.d.ts +2 -0
  138. package/dist/lib/harness/adapters/muse.js +44 -0
  139. package/dist/lib/harness/adapters/opencode.d.ts +2 -0
  140. package/dist/lib/harness/adapters/opencode.js +14 -0
  141. package/dist/lib/harness/exec-config-version.d.ts +8 -0
  142. package/dist/lib/harness/exec-config-version.js +22 -0
  143. package/dist/lib/harness/index.d.ts +1 -0
  144. package/dist/lib/harness/index.js +34 -0
  145. package/dist/lib/{hooks.d.ts → hooks/install.d.ts} +1 -1
  146. package/dist/lib/{hooks.js → hooks/install.js} +11 -11
  147. package/dist/lib/hosts/passthrough.d.ts +10 -0
  148. package/dist/lib/hosts/passthrough.js +28 -6
  149. package/dist/lib/hosts/session-index.js +1 -1
  150. package/dist/lib/import.js +1 -1
  151. package/dist/lib/installations/index.d.ts +4 -4
  152. package/dist/lib/installations/index.js +3 -4
  153. package/dist/lib/installations/migrate.js +6 -6
  154. package/dist/lib/{shims.d.ts → installations/shims.d.ts} +2 -2
  155. package/dist/lib/{shims.js → installations/shims.js} +13 -115
  156. package/dist/lib/installations/store.d.ts +141 -3
  157. package/dist/lib/installations/store.js +475 -5
  158. package/dist/lib/installations/versions.d.ts +40 -125
  159. package/dist/lib/installations/versions.js +57 -462
  160. package/dist/lib/isolation-boundary-report.d.ts +1 -1
  161. package/dist/lib/isolation-boundary-report.js +1 -1
  162. package/dist/lib/mailbox-gc.js +2 -3
  163. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  164. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  165. package/dist/lib/menubar/install-menubar.js +19 -8
  166. package/dist/lib/monitors/config.d.ts +2 -2
  167. package/dist/lib/monitors/config.js +3 -2
  168. package/dist/lib/monitors/dispatch.d.ts +1 -1
  169. package/dist/lib/monitors/dispatch.js +2 -2
  170. package/dist/lib/monitors/state.d.ts +1 -1
  171. package/dist/lib/origin-machine.d.ts +18 -0
  172. package/dist/lib/origin-machine.js +34 -0
  173. package/dist/lib/perf/db.js +1 -1
  174. package/dist/lib/perf/spool.js +1 -1
  175. package/dist/lib/plugins/plugins.js +1 -1
  176. package/dist/lib/plugins/skills.js +1 -1
  177. package/dist/lib/prix-account.d.ts +158 -0
  178. package/dist/lib/prix-account.js +214 -0
  179. package/dist/lib/profiles.d.ts +7 -0
  180. package/dist/lib/profiles.js +26 -1
  181. package/dist/lib/project-key.js +2 -2
  182. package/dist/lib/project-launch.js +1 -1
  183. package/dist/lib/refresh.js +2 -2
  184. package/dist/lib/resource-inventory.d.ts +1 -1
  185. package/dist/lib/resource-inventory.js +1 -1
  186. package/dist/lib/rules/rules.js +1 -1
  187. package/dist/lib/sandbox.d.ts +24 -0
  188. package/dist/lib/sandbox.js +101 -0
  189. package/dist/lib/scheduling/routines.d.ts +18 -9
  190. package/dist/lib/scheduling/routines.js +17 -9
  191. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  192. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  193. package/dist/lib/secrets/agent.js +9 -1
  194. package/dist/lib/secrets/index.js +1 -1
  195. package/dist/lib/secrets/list-filter.js +1 -1
  196. package/dist/lib/self-heal/checks/hook-runtime.js +1 -1
  197. package/dist/lib/self-heal/checks/path.js +1 -1
  198. package/dist/lib/self-heal/checks/shadowing.js +1 -1
  199. package/dist/lib/self-heal/checks/shims.js +1 -1
  200. package/dist/lib/service-manifest.d.ts +20 -0
  201. package/dist/lib/service-manifest.js +34 -0
  202. package/dist/lib/session/cloud.js +1 -1
  203. package/dist/lib/session/db.js +1 -1
  204. package/dist/lib/session/discover.d.ts +39 -0
  205. package/dist/lib/session/discover.js +3 -3
  206. package/dist/lib/session/fork.js +1 -1
  207. package/dist/lib/session/live-metadata.js +1 -1
  208. package/dist/lib/session/origin-machine.d.ts +1 -18
  209. package/dist/lib/session/origin-machine.js +4 -34
  210. package/dist/lib/session/parse.js +24 -42
  211. package/dist/lib/session/recovery.js +1 -1
  212. package/dist/lib/session/relative-time.d.ts +1 -44
  213. package/dist/lib/session/relative-time.js +4 -120
  214. package/dist/lib/session/remote-list.js +1 -1
  215. package/dist/lib/session/remote.js +2 -2
  216. package/dist/lib/session/short-id.d.ts +1 -17
  217. package/dist/lib/session/short-id.js +4 -20
  218. package/dist/lib/session/width.d.ts +1 -29
  219. package/dist/lib/session/width.js +4 -101
  220. package/dist/lib/snapshot.d.ts +2 -2
  221. package/dist/lib/snapshot.js +1 -1
  222. package/dist/lib/staleness/prune.d.ts +1 -1
  223. package/dist/lib/staleness/prune.js +1 -1
  224. package/dist/lib/staleness/writers/hooks.js +1 -1
  225. package/dist/lib/startup/command-registry.d.ts +4 -1
  226. package/dist/lib/startup/command-registry.js +12 -5
  227. package/dist/lib/teams/agents.js +2 -19
  228. package/dist/lib/teams/parsers.js +20 -26
  229. package/dist/lib/teams/placement-probe.js +1 -1
  230. package/dist/lib/teams/registry.js +2 -21
  231. package/dist/lib/teams/scheduler.js +1 -1
  232. package/dist/lib/text/relative-time.d.ts +44 -0
  233. package/dist/lib/text/relative-time.js +120 -0
  234. package/dist/lib/text/short-id.d.ts +17 -0
  235. package/dist/lib/text/short-id.js +20 -0
  236. package/dist/lib/text/width.d.ts +29 -0
  237. package/dist/lib/text/width.js +101 -0
  238. package/dist/lib/triggers/handlers.d.ts +7 -0
  239. package/dist/lib/triggers/handlers.js +9 -1
  240. package/dist/lib/triggers/webhook.d.ts +1 -0
  241. package/dist/lib/triggers/webhook.js +3 -2
  242. package/dist/lib/uninstall.js +1 -1
  243. package/dist/lib/version.d.ts +9 -2
  244. package/dist/lib/version.js +11 -9
  245. package/dist/lib/view-types.d.ts +4 -0
  246. package/package.json +1 -1
  247. package/dist/commands/tickets.d.ts +0 -2
  248. package/dist/commands/tickets.js +0 -43
  249. package/dist/lib/observe-aliases.d.ts +0 -30
  250. package/dist/lib/observe-aliases.js +0 -38
  251. package/dist/lib/tickets/list.d.ts +0 -53
  252. package/dist/lib/tickets/list.js +0 -153
  253. /package/dist/lib/{computer-rpc.d.ts → computer/computer-rpc.d.ts} +0 -0
@@ -1,10 +1,17 @@
1
1
  import * as crypto from 'crypto';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
+ import { execFile } from 'child_process';
5
+ import { promisify } from 'util';
6
+ import * as yaml from 'yaml';
4
7
  import { atomicWriteFileSync } from '../fs-atomic.js';
5
- import { getVersionsDir } from '../state.js';
6
- import { VERSION_RE } from '../agent-spec/primitives.js';
8
+ import { getHomeDir, getUserAgentsDir, getVersionsDir, readMeta } from '../state.js';
9
+ import { VERSION_RE, compareVersions } from '../agent-spec/primitives.js';
10
+ import { AGENTS, findInPath } from '../agents.js';
11
+ import { IS_WINDOWS } from '../platform/index.js';
12
+ import { getConfigSymlinkVersion } from './shims.js';
7
13
  import { INSTALLATION_RECORD_FILE, INSTALLATION_SCHEMA } from './types.js';
14
+ const execFileAsync = promisify(execFile);
8
15
  /**
9
16
  * Persistence for {@link Installation} records.
10
17
  *
@@ -15,10 +22,12 @@ import { INSTALLATION_RECORD_FILE, INSTALLATION_SCHEMA } from './types.js';
15
22
  * follow. It is also why the file name is registered in versions.ts's
16
23
  * `PRESERVED_ON_CLEAN_REINSTALL` — a repair reinstall must not mint a new id.
17
24
  *
18
- * Deliberately depends on nothing but `state`/`fs-atomic`/`primitives` so
19
- * versions.ts can import it without an import cycle.
25
+ * Path/enumeration plus frozen installation records. Depends on Node builtins,
26
+ * installations-local modules, and base lib (`state`/`fs-atomic`/`primitives`/
27
+ * `agents`/`shims`/`platform`) — never `plugins/`, `rules/`, `session/`, or
28
+ * `devices/`, so those modules can import this file without an import cycle.
20
29
  */
21
- /** Directory holding one installation. Mirrors versions.ts `getVersionDir`. */
30
+ /** Directory holding one installation. Same path as {@link getVersionDir}. */
22
31
  export function installationDir(agent, label) {
23
32
  return path.join(getVersionsDir(), agent, label);
24
33
  }
@@ -194,3 +203,464 @@ export function listInstallations(agent) {
194
203
  }
195
204
  return out;
196
205
  }
206
+ /**
207
+ * Get the directory where a specific version is installed.
208
+ */
209
+ export function getVersionDir(agent, version) {
210
+ return path.join(getVersionsDir(), agent, version);
211
+ }
212
+ /**
213
+ * Grok binaries are never trusted below this size when picking among several
214
+ * `grok-*` candidates with no exact version match (see
215
+ * {@link resolveGrokFallbackBinary}). Grok's real native binary is ~100MB+; a
216
+ * stray non-binary artifact sharing the `grok-*` naming pattern (a wrapper or
217
+ * alias script) is at most a few hundred bytes, so 1MB comfortably separates
218
+ * the two without depending on file content.
219
+ */
220
+ const MIN_GROK_BINARY_BYTES = 1_000_000;
221
+ /**
222
+ * Pick the real grok binary among `grok-*` entries in `downloadsDir` when NO
223
+ * filename carries the pinned version string. Grok self-updates its binary in
224
+ * place while running under the shim, so a version-home's downloads dir can
225
+ * accumulate several `grok-*` files whose names have drifted away from that
226
+ * version-home's pinned version (RUSH-2459: on yosemite-s0, version-home
227
+ * `0.2.82` held `grok-1.0.0-linux-aarch64` after grok self-updated, plus a
228
+ * stale, unrelated 99-byte `grok-0.2.118-linux-aarch64` wrapper script that
229
+ * happened to sort alphabetically first).
230
+ *
231
+ * Never blindly take whatever `fs.readdirSync` returns first — that is exactly
232
+ * the bug this replaces. Instead: exclude anything under
233
+ * {@link MIN_GROK_BINARY_BYTES} (rejects wrapper/alias artifacts on size
234
+ * alone, no content sniffing needed) and, among the survivors, prefer the most
235
+ * recently modified — the file grok's self-updater actually wrote last.
236
+ * Returns null (fail loud, never guess) when nothing survives the size filter.
237
+ */
238
+ export function resolveGrokFallbackBinary(downloadsDir) {
239
+ let entries;
240
+ try {
241
+ entries = fs.readdirSync(downloadsDir);
242
+ }
243
+ catch {
244
+ return null;
245
+ }
246
+ let best = null;
247
+ for (const entry of entries) {
248
+ if (!entry.startsWith('grok-'))
249
+ continue;
250
+ const full = path.join(downloadsDir, entry);
251
+ let stat;
252
+ try {
253
+ stat = fs.statSync(full);
254
+ }
255
+ catch {
256
+ continue;
257
+ }
258
+ if (!stat.isFile() || stat.size < MIN_GROK_BINARY_BYTES)
259
+ continue;
260
+ if (!best || stat.mtimeMs > best.mtimeMs)
261
+ best = { name: entry, mtimeMs: stat.mtimeMs };
262
+ }
263
+ return best ? path.join(downloadsDir, best.name) : null;
264
+ }
265
+ /**
266
+ * Get the binary path for a specific agent version.
267
+ */
268
+ export function getBinaryPath(agent, version) {
269
+ const agentConfig = AGENTS[agent];
270
+ if (agent === 'grok') {
271
+ const grokDownloads = path.join(getVersionHomePath(agent, version), '.grok', 'downloads');
272
+ // Best effort: first matching file for this version
273
+ try {
274
+ const entries = fs.readdirSync(grokDownloads);
275
+ const match = entries.find((e) => e.includes(version) && e.startsWith('grok-'));
276
+ if (match)
277
+ return path.join(grokDownloads, match);
278
+ }
279
+ catch { }
280
+ const fallback = resolveGrokFallbackBinary(grokDownloads);
281
+ if (fallback)
282
+ return fallback;
283
+ return path.join(grokDownloads, `grok-${version}`);
284
+ }
285
+ if (agent === 'droid') {
286
+ // Factory.ai's installer drops a standalone native binary (no npm package,
287
+ // nothing in node_modules/.bin). The binary is global, not per-version —
288
+ // config isolation rides the ~/.factory symlink switch, not a separate
289
+ // binary per version. Install location is platform-specific:
290
+ // macOS/Linux: ~/.local/bin/droid (curl app.factory.ai/cli | sh)
291
+ // Windows: %USERPROFILE%\bin\droid.exe (irm app.factory.ai/cli/windows | iex)
292
+ // Mirror the shim's `droid` branch so isVersionInstalled/`agents view`
293
+ // agree with what actually executes.
294
+ return IS_WINDOWS
295
+ ? path.join(getHomeDir(), 'bin', 'droid.exe')
296
+ : path.join(getHomeDir(), '.local', 'bin', 'droid');
297
+ }
298
+ if (agent === 'muse') {
299
+ // Muse Code install script drops a self-updating launcher at
300
+ // ~/.local/bin/muse (curl -fsSL https://dev.meta.ai/install.sh | sh).
301
+ // Same global-binary shape as droid: one binary for every version dir,
302
+ // config isolation via version-home HOME rewrite. Mirror the shim's
303
+ // `muse` branch so isVersionInstalled / agents view / agents import
304
+ // agree with what executes.
305
+ return IS_WINDOWS
306
+ ? path.join(getHomeDir(), 'bin', 'muse.exe')
307
+ : path.join(getHomeDir(), '.local', 'bin', 'muse');
308
+ }
309
+ if (agent === 'warp') {
310
+ // Warp Agent CLI installs a single global, self-updating `warp` binary at
311
+ // ~/.local/bin/warp via the curl installer (Windows: the agent-cli.ps1) —
312
+ // like droid/muse. Resolve the real binary on PATH (findInPath skips our own
313
+ // shims dir) so isVersionInstalled / agents view agree with what executes.
314
+ // When warp is not installed, fall back to its default install path so
315
+ // isVersionInstalled reports uninstalled honestly.
316
+ const onPath = findInPath('warp');
317
+ if (onPath)
318
+ return onPath;
319
+ return IS_WINDOWS
320
+ ? path.join(getHomeDir(), 'bin', 'warp.exe')
321
+ : path.join(getHomeDir(), '.local', 'bin', 'warp');
322
+ }
323
+ const versionDir = getVersionDir(agent, version);
324
+ return path.join(versionDir, 'node_modules', '.bin', agentConfig.cliCommand);
325
+ }
326
+ /**
327
+ * Does this agent resolve to ONE global binary that is the same file regardless
328
+ * of the `version` argument? (droid → always `~/.local/bin/droid`.) Computed
329
+ * generically by probing `getBinaryPath` with two distinct versions rather than
330
+ * hardcoding an agent id, so it stays correct if another global-binary agent is
331
+ * added.
332
+ *
333
+ * This is the narrower cousin of `isSelfUpdatingAgent`: every global-binary
334
+ * agent is self-updating, but grok is self-updating WITHOUT a global binary — it
335
+ * stores a real per-version binary copy under each version-home
336
+ * (`versions/grok/<v>/home/.grok/downloads/grok-<v>`), so its version-homes are
337
+ * genuinely distinct and must NOT be collapsed. Gate the single-binary
338
+ * collapse/live-version logic on THIS predicate; gate pin-refusal / "switch
339
+ * profile" copy on `isSelfUpdatingAgent`.
340
+ */
341
+ export function isGlobalBinaryAgent(agent) {
342
+ return getBinaryPath(agent, '0.0.0-probe-a') === getBinaryPath(agent, '0.0.0-probe-b');
343
+ }
344
+ // Live-version cache for self-updating global binaries. `<cli> --version` is a
345
+ // ~real shell-out, so hold the result briefly: the same `agents view` render
346
+ // asks for it from both listInstalledVersions (sync) and the label path (async).
347
+ const LIVE_VERSION_TTL_MS = 5000;
348
+ const liveVersionCache = new Map();
349
+ /** Drop the live-version cache (call after an install/remove that changes the
350
+ * running binary, e.g. `agents add droid@latest`). */
351
+ export function invalidateLiveVersionCache(agent) {
352
+ if (agent)
353
+ liveVersionCache.delete(agent);
354
+ else
355
+ liveVersionCache.clear();
356
+ }
357
+ /**
358
+ * Resolve the version the ONE globally-installed binary actually reports via
359
+ * `<cli> --version`, cached for {@link LIVE_VERSION_TTL_MS}. For a self-updating
360
+ * global-binary agent (droid) this is the single source of truth for "which
361
+ * version is installed" — the on-disk version-dir NAMES are just stale labels
362
+ * left behind by successive `agents add`/self-update cycles. Returns null when
363
+ * the binary isn't on PATH or the probe fails.
364
+ */
365
+ export async function getCliVersionFromPath(agent) {
366
+ const agentConfig = AGENTS[agent];
367
+ try {
368
+ const { stdout } = await execFileAsync(agentConfig.cliCommand, ['--version'], { timeout: 3000, shell: process.platform === 'win32' });
369
+ const match = stdout.match(/(\d+\.\d+\.\d+)/);
370
+ return match ? match[1] : null;
371
+ }
372
+ catch {
373
+ return null;
374
+ }
375
+ }
376
+ export async function getLiveVersion(agent) {
377
+ const cached = liveVersionCache.get(agent);
378
+ if (cached && Date.now() - cached.at < LIVE_VERSION_TTL_MS)
379
+ return cached.version;
380
+ const version = await getCliVersionFromPath(agent);
381
+ liveVersionCache.set(agent, { at: Date.now(), version });
382
+ return version;
383
+ }
384
+ /**
385
+ * Synchronous, non-blocking read of the live-version cache — returns the value
386
+ * only if a recent {@link getLiveVersion} call already warmed it, else null.
387
+ * `listInstalledVersions` is sync and must not shell out, so it prefers this
388
+ * warm value (accurate) and otherwise falls back to the newest on-disk dir.
389
+ */
390
+ export function getCachedLiveVersion(agent) {
391
+ const cached = liveVersionCache.get(agent);
392
+ if (cached && Date.now() - cached.at < LIVE_VERSION_TTL_MS)
393
+ return cached.version;
394
+ return null;
395
+ }
396
+ /**
397
+ * Get the isolated HOME directory for a specific agent version.
398
+ * Each version has its own config isolation (like jobs sandbox).
399
+ */
400
+ export function getVersionHomePath(agent, version) {
401
+ return path.join(getVersionDir(agent, version), 'home');
402
+ }
403
+ /**
404
+ * Resolve the REAL launch binary for an npm-package agent version: the file the
405
+ * installed package's `bin` entry points to — e.g.
406
+ * node_modules/@anthropic-ai/claude-code/bin/claude.exe on Windows. This is the
407
+ * executable that the node_modules/.bin/<cli>.cmd wrapper ultimately execs (and
408
+ * what `agents run` spawns), NOT the wrapper itself.
409
+ *
410
+ * The distinction is load-bearing: npm leaves the tiny node_modules/.bin/<cli>
411
+ * and <cli>.cmd wrappers in place even after a vendor auto-updater destroys the
412
+ * multi-hundred-MB real binary the wrapper points at. Keying "installed" on the
413
+ * wrapper (getBinaryPath) — or on the version dir — therefore reports a gutted
414
+ * install as healthy, and `agents run` then dies at spawn with
415
+ * "'...claude.exe' is not recognized".
416
+ *
417
+ * Returns null for agents without an npm package (grok/droid/installScript,
418
+ * whose getBinaryPath already resolves the real per-host binary) or when the
419
+ * installed package.json can't be read/parsed — callers fall back to the
420
+ * generic getBinaryPath check in those cases.
421
+ */
422
+ function getPackageBinaryPath(agent, version) {
423
+ const agentConfig = AGENTS[agent];
424
+ if (!agentConfig.npmPackage)
425
+ return null;
426
+ const pkgRoot = path.join(getVersionDir(agent, version), 'node_modules', agentConfig.npmPackage);
427
+ let bin;
428
+ try {
429
+ const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf-8'));
430
+ bin = pkg.bin;
431
+ }
432
+ catch {
433
+ return null;
434
+ }
435
+ let rel;
436
+ if (typeof bin === 'string') {
437
+ rel = bin;
438
+ }
439
+ else if (bin && typeof bin === 'object') {
440
+ const map = bin;
441
+ // Prefer the entry named after our launch command; else the first bin.
442
+ rel = map[agentConfig.cliCommand] ?? Object.values(map)[0];
443
+ }
444
+ if (!rel || typeof rel !== 'string')
445
+ return null;
446
+ return path.join(pkgRoot, rel);
447
+ }
448
+ /**
449
+ * Check if a specific version is installed.
450
+ *
451
+ * Probes the actual launch binary (the same executable the shims run), not just
452
+ * the version dir or the node_modules/.bin/<cli> wrapper. For npm agents that
453
+ * means statting the package's real `bin` target (getPackageBinaryPath), so a
454
+ * present-but-gutted install (vendor auto-updater destroyed the binary) reports
455
+ * as NOT installed and `agents add` re-runs its install step to repair it.
456
+ */
457
+ export function isVersionInstalled(agent, version) {
458
+ const packageBinary = getPackageBinaryPath(agent, version);
459
+ if (packageBinary !== null)
460
+ return fs.existsSync(packageBinary);
461
+ return fs.existsSync(getBinaryPath(agent, version));
462
+ }
463
+ // Per-process cache for listInstalledVersions. The agent's versions dir mtime
464
+ // changes whenever a version dir is added or removed (install/remove), so a
465
+ // stamp match means the installed set is unchanged and we skip the readdir +
466
+ // N binary stats. Mirrors the readMeta() cache in state.ts. Hot path:
467
+ // resolveAgentTargets and every enumerate-style consumer hit this.
468
+ const installedVersionsCache = new Map();
469
+ /** Drop the installed-versions cache (call after install/remove mutations). */
470
+ export function invalidateInstalledVersionsCache(agent) {
471
+ if (agent)
472
+ installedVersionsCache.delete(agent);
473
+ else
474
+ installedVersionsCache.clear();
475
+ }
476
+ /**
477
+ * Choose the single canonical version-dir to represent a self-updating
478
+ * global-binary agent (droid). All its version dirs map to ONE binary, so
479
+ * exactly one is real; the rest are stale labels. Prefer, in order: the dir the
480
+ * live config symlink points at (what actually runs), the recorded global
481
+ * default, the live `--version` (when the cache is warm), else the newest dir.
482
+ * `versions` MUST be sorted ascending. Callers guarantee it is non-empty.
483
+ */
484
+ export function pickCanonicalGlobalBinaryVersion(agent, versions) {
485
+ const symlinkVersion = getConfigSymlinkVersion(agent);
486
+ if (symlinkVersion && versions.includes(symlinkVersion))
487
+ return symlinkVersion;
488
+ const globalDefault = getGlobalDefault(agent);
489
+ if (globalDefault && versions.includes(globalDefault))
490
+ return globalDefault;
491
+ const live = getCachedLiveVersion(agent);
492
+ if (live && versions.includes(live))
493
+ return live;
494
+ return versions[versions.length - 1];
495
+ }
496
+ /**
497
+ * Collapse a global-binary agent's phantom version dirs to the single canonical
498
+ * one (see {@link pickCanonicalGlobalBinaryVersion}). No-op for npm-packaged and
499
+ * per-version agents (claude/codex/grok/…), whose version dirs are genuinely
500
+ * distinct installs.
501
+ */
502
+ function collapseGlobalBinaryVersions(agent, versions) {
503
+ if (!isGlobalBinaryAgent(agent) || versions.length === 0)
504
+ return versions;
505
+ return [pickCanonicalGlobalBinaryVersion(agent, versions)];
506
+ }
507
+ /**
508
+ * List all installed versions for an agent (cached by versions-dir mtime).
509
+ *
510
+ * For a self-updating global-binary agent (droid) every version dir resolves to
511
+ * the SAME binary, so this collapses them to a single canonical entry — one
512
+ * install, one row in `agents view`, never the phantom set of semver dir names.
513
+ */
514
+ export function listInstalledVersions(agent) {
515
+ const agentVersionsDir = path.join(getVersionsDir(), agent);
516
+ let stamp;
517
+ try {
518
+ stamp = fs.statSync(agentVersionsDir).mtimeMs;
519
+ }
520
+ catch {
521
+ installedVersionsCache.set(agent, { stamp: 0, versions: [] });
522
+ return [];
523
+ }
524
+ const cached = installedVersionsCache.get(agent);
525
+ if (cached && cached.stamp === stamp) {
526
+ // Collapse is applied per-call (not cached): it depends on the live-version
527
+ // cache + config symlink, which can change without the versions-dir mtime.
528
+ return collapseGlobalBinaryVersions(agent, cached.versions);
529
+ }
530
+ const entries = fs.readdirSync(agentVersionsDir, { withFileTypes: true });
531
+ const versions = [];
532
+ for (const entry of entries) {
533
+ if (entry.isDirectory()) {
534
+ // Probe the real launch binary (isVersionInstalled), not just the
535
+ // node_modules/.bin wrapper — a gutted install must not count as healthy
536
+ // in the balanced account/version picker.
537
+ if (isVersionInstalled(agent, entry.name)) {
538
+ versions.push(entry.name);
539
+ }
540
+ }
541
+ }
542
+ versions.sort(compareVersions);
543
+ installedVersionsCache.set(agent, { stamp, versions });
544
+ return collapseGlobalBinaryVersions(agent, versions);
545
+ }
546
+ /**
547
+ * Get the global default version for an agent.
548
+ */
549
+ export function getGlobalDefault(agent) {
550
+ const meta = readMeta();
551
+ return meta.agents?.[agent] || null;
552
+ }
553
+ /**
554
+ * Get the preferred ISOLATED version for an agent — the copy a bare
555
+ * `agents run <agent>` falls back to when there is no global default.
556
+ */
557
+ export function getIsolatedDefault(agent) {
558
+ const meta = readMeta();
559
+ return meta.isolatedAgents?.[agent] || null;
560
+ }
561
+ /**
562
+ * Path to the sentinel file that marks a version as an isolated install.
563
+ *
564
+ * It lives at the version-dir root (a sibling of `home/`), so it is carried
565
+ * along when `softDeleteVersionDir` moves the whole directory to trash and is
566
+ * restored intact by `agents trash restore`. Its mere presence is the marker;
567
+ * the contents are an informational timestamp only.
568
+ */
569
+ function getIsolatedMarkerPath(agent, version) {
570
+ return path.join(getVersionDir(agent, version), '.isolated');
571
+ }
572
+ /**
573
+ * Mark an installed version as an isolated install (`agents add --isolated`).
574
+ *
575
+ * Isolated versions are fully self-contained: they never become the global
576
+ * default and never own the user's real `~/.<agent>` config directory. This
577
+ * flag is what keeps every "adopting" code path away from them.
578
+ */
579
+ export function markVersionIsolated(agent, version) {
580
+ fs.writeFileSync(getIsolatedMarkerPath(agent, version), `${new Date().toISOString()}\n`, { mode: 0o600 });
581
+ }
582
+ /**
583
+ * Whether a version was installed as an isolated install (`agents add --isolated`).
584
+ *
585
+ * Used to exclude such versions from global-default promotion and from any
586
+ * flow that would touch the user's real `~/.<agent>` directory, and to gate the
587
+ * `--isolated` safety check on `agents remove`.
588
+ */
589
+ export function isVersionIsolated(agent, version) {
590
+ return fs.existsSync(getIsolatedMarkerPath(agent, version));
591
+ }
592
+ /**
593
+ * Get version specified in a project-root agents.yaml (not the user ~/.agents/.system/agents.yaml).
594
+ */
595
+ export function getProjectVersion(agent, startPath) {
596
+ const userAgentsYaml = path.join(getUserAgentsDir(), 'agents.yaml');
597
+ let dir = path.resolve(startPath);
598
+ while (dir !== path.dirname(dir)) {
599
+ const manifestPath = path.join(dir, 'agents.yaml');
600
+ if (manifestPath !== userAgentsYaml && fs.existsSync(manifestPath)) {
601
+ try {
602
+ const content = fs.readFileSync(manifestPath, 'utf-8');
603
+ const parsed = yaml.parse(content);
604
+ const version = parsed?.agents?.[agent];
605
+ if (typeof version === 'string' && version.trim()) {
606
+ const normalized = version.trim();
607
+ if (!VERSION_RE.test(normalized)) {
608
+ throw new Error(`Invalid version in agents.yaml for ${agent}: ${normalized}. Allowed: latest or [A-Za-z0-9._+-]{1,64}`);
609
+ }
610
+ return normalized;
611
+ }
612
+ }
613
+ catch (err) {
614
+ if (err instanceof Error && err.message.startsWith('Invalid version in agents.yaml')) {
615
+ throw err;
616
+ }
617
+ // Ignore parsing errors
618
+ }
619
+ }
620
+ dir = path.dirname(dir);
621
+ }
622
+ return null;
623
+ }
624
+ /**
625
+ * Get the resolved version for an agent in the current context.
626
+ * Checks project manifest first, then global default.
627
+ */
628
+ export function resolveVersion(agent, projectPath) {
629
+ // Check project manifest
630
+ if (projectPath) {
631
+ const version = getProjectVersion(agent, projectPath);
632
+ if (version) {
633
+ return version;
634
+ }
635
+ }
636
+ // Fall back to global default
637
+ const globalDefault = getGlobalDefault(agent);
638
+ if (globalDefault)
639
+ return globalDefault;
640
+ // Last resort: the preferred isolated copy. Strictly a fallback — a global
641
+ // default always wins, so nothing changes for anyone who has one. Without this,
642
+ // an isolated-only user cannot reach their installs by bare name at all: the
643
+ // resolution chain ended here, so `agents run codex` fell through to whatever
644
+ // `codex` meant on PATH, and only `agents run codex@<version>` worked.
645
+ //
646
+ // The pointer is verified on read. It survives in agents.yaml across a trash +
647
+ // restore cycle, but a version removed for good would otherwise leave a dangling
648
+ // pin that resolves to a directory that is not there.
649
+ const isolated = getIsolatedDefault(agent);
650
+ if (isolated && isVersionInstalled(agent, isolated) && isVersionIsolated(agent, isolated)) {
651
+ return isolated;
652
+ }
653
+ return null;
654
+ }
655
+ /**
656
+ * Get the effective HOME directory for an agent.
657
+ * If version-managed with a resolved version, returns the version's home directory.
658
+ * Otherwise returns the real HOME.
659
+ */
660
+ export function getEffectiveHome(agentId) {
661
+ const resolved = resolveVersion(agentId, process.cwd());
662
+ if (resolved && isVersionInstalled(agentId, resolved)) {
663
+ return getVersionHomePath(agentId, resolved);
664
+ }
665
+ return getHomeDir();
666
+ }