@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.
- package/CHANGELOG.md +86 -0
- package/README.md +10 -4
- package/dist/bin/agents +0 -0
- package/dist/bootstrap.js +25 -7
- package/dist/cli/command-registry.d.ts +2 -3
- package/dist/cli/command-registry.js +4 -8
- package/dist/commands/accounts.d.ts +29 -0
- package/dist/commands/accounts.js +220 -29
- package/dist/commands/alias.d.ts +7 -4
- package/dist/commands/alias.js +34 -9
- package/dist/commands/auth.d.ts +9 -0
- package/dist/commands/auth.js +108 -0
- package/dist/commands/browser-sessions-picker.js +42 -62
- package/dist/commands/browser.js +30 -38
- package/dist/commands/computer-actions.d.ts +1 -1
- package/dist/commands/computer-actions.js +1 -1
- package/dist/commands/computer-sessions-picker.js +32 -55
- package/dist/commands/computer.d.ts +1 -1
- package/dist/commands/computer.js +22 -8
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/doctor.d.ts +5 -3
- package/dist/commands/doctor.js +23 -8
- package/dist/commands/exec.js +3 -2
- package/dist/commands/feed.js +2 -31
- package/dist/commands/harness.js +3 -3
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/import.js +1 -1
- package/dist/commands/insights.js +163 -85
- package/dist/commands/inspect.js +2 -2
- package/dist/commands/mine.js +1 -1
- package/dist/commands/monitors.js +1 -1
- package/dist/commands/org.d.ts +6 -0
- package/dist/commands/org.js +175 -0
- package/dist/commands/packages.js +1 -1
- package/dist/commands/profiles.js +2 -2
- package/dist/commands/prune.js +1 -1
- package/dist/commands/routines.js +44 -26
- package/dist/commands/run-account-picker.d.ts +19 -0
- package/dist/commands/run-account-picker.js +81 -0
- package/dist/commands/sessions-picker-factory.d.ts +40 -0
- package/dist/commands/sessions-picker-factory.js +67 -0
- package/dist/commands/sessions-picker.js +124 -122
- package/dist/commands/setup-browser.js +4 -4
- package/dist/commands/setup-mine.js +1 -1
- package/dist/commands/setup-preferences.js +2 -2
- package/dist/commands/setup.js +10 -4
- package/dist/commands/snapshot.d.ts +1 -1
- package/dist/commands/snapshot.js +4 -4
- package/dist/commands/status.d.ts +4 -2
- package/dist/commands/status.js +10 -7
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.js +30 -3
- package/dist/commands/versions.js +1 -1
- package/dist/commands/view.js +7 -1
- package/dist/commands/webhook.js +1 -1
- package/dist/lib/account-capabilities.d.ts +10 -0
- package/dist/lib/account-capabilities.js +34 -0
- package/dist/lib/accounting/rotate.d.ts +5 -3
- package/dist/lib/accounting/rotate.js +36 -7
- package/dist/lib/accounting/usage.d.ts +18 -4
- package/dist/lib/accounting/usage.js +138 -27
- package/dist/lib/agent-spec/agents.d.ts +584 -0
- package/dist/lib/agent-spec/agents.js +3127 -0
- package/dist/lib/agent-spec/provider.js +1 -1
- package/dist/lib/agents.d.ts +2 -584
- package/dist/lib/agents.js +2 -3127
- package/dist/lib/browser/drivers/local.d.ts +8 -2
- package/dist/lib/browser/drivers/local.js +8 -2
- package/dist/lib/browser/drivers/ssh.d.ts +8 -2
- package/dist/lib/browser/drivers/ssh.js +10 -4
- package/dist/lib/browser/hygiene.d.ts +1 -1
- package/dist/lib/browser/hygiene.js +5 -1
- package/dist/lib/browser/profiles.d.ts +74 -1
- package/dist/lib/browser/profiles.js +114 -5
- package/dist/lib/browser/runtime-state.d.ts +11 -5
- package/dist/lib/browser/runtime-state.js +13 -9
- package/dist/lib/browser/service.d.ts +68 -30
- package/dist/lib/browser/service.js +305 -194
- package/dist/lib/browser/types.d.ts +95 -3
- package/dist/lib/browser/types.js +51 -0
- package/dist/lib/catchup.js +1 -1
- package/dist/lib/cloud/rush.d.ts +0 -15
- package/dist/lib/cloud/rush.js +0 -35
- package/dist/lib/computer/actions.d.ts +1 -1
- package/dist/lib/computer/actions.js +1 -1
- package/dist/lib/{computer-rpc.js → computer/computer-rpc.js} +5 -5
- package/dist/lib/computer/dispatch.d.ts +1 -1
- package/dist/lib/computer/download.d.ts +1 -1
- package/dist/lib/computer/download.js +3 -3
- package/dist/lib/{ssh-tunnel.d.ts → computer/ssh-tunnel.d.ts} +1 -1
- package/dist/lib/{ssh-tunnel.js → computer/ssh-tunnel.js} +12 -12
- package/dist/lib/crabbox/runtimes.d.ts +6 -2
- package/dist/lib/crabbox/runtimes.js +21 -5
- package/dist/lib/daemon/daemon.js +87 -59
- package/dist/lib/{runner.d.ts → daemon/runner.d.ts} +28 -11
- package/dist/lib/{runner.js → daemon/runner.js} +143 -123
- package/dist/lib/devices/doctor-findings.d.ts +1 -1
- package/dist/lib/devices/doctor-findings.js +4 -4
- package/dist/lib/devices/fleet-inventory.js +1 -1
- package/dist/lib/devices/harness-inventory.js +1 -1
- package/dist/lib/devices/health-report.js +1 -1
- package/dist/lib/devices/registry.js +4 -22
- package/dist/lib/doctor-diff.d.ts +1 -1
- package/dist/lib/doctor-diff.js +1 -1
- package/dist/lib/drift-sync.d.ts +1 -1
- package/dist/lib/drift-sync.js +2 -2
- package/dist/lib/drift.d.ts +1 -1
- package/dist/lib/drift.js +1 -1
- package/dist/lib/entitlement.d.ts +31 -0
- package/dist/lib/entitlement.js +137 -0
- package/dist/lib/exec.js +52 -205
- package/dist/lib/feed/feed.js +2 -8
- package/dist/lib/format.d.ts +9 -0
- package/dist/lib/format.js +16 -0
- package/dist/lib/fs-atomic.d.ts +7 -0
- package/dist/lib/fs-atomic.js +9 -0
- package/dist/lib/github/pr-mergeable.d.ts +49 -0
- package/dist/lib/github/pr-mergeable.js +157 -0
- package/dist/lib/github/pr-verdict.d.ts +67 -0
- package/dist/lib/github/pr-verdict.js +72 -0
- package/dist/lib/harness/adapter.d.ts +161 -0
- package/dist/lib/harness/adapter.js +47 -0
- package/dist/lib/harness/adapters/claude.d.ts +2 -0
- package/dist/lib/harness/adapters/claude.js +110 -0
- package/dist/lib/harness/adapters/codex.d.ts +2 -0
- package/dist/lib/harness/adapters/codex.js +83 -0
- package/dist/lib/harness/adapters/copilot.d.ts +2 -0
- package/dist/lib/harness/adapters/copilot.js +23 -0
- package/dist/lib/harness/adapters/cursor.d.ts +2 -0
- package/dist/lib/harness/adapters/cursor.js +38 -0
- package/dist/lib/harness/adapters/droid.d.ts +2 -0
- package/dist/lib/harness/adapters/droid.js +17 -0
- package/dist/lib/harness/adapters/grok.d.ts +2 -0
- package/dist/lib/harness/adapters/grok.js +14 -0
- package/dist/lib/harness/adapters/kimi.d.ts +2 -0
- package/dist/lib/harness/adapters/kimi.js +49 -0
- package/dist/lib/harness/adapters/muse.d.ts +2 -0
- package/dist/lib/harness/adapters/muse.js +44 -0
- package/dist/lib/harness/adapters/opencode.d.ts +2 -0
- package/dist/lib/harness/adapters/opencode.js +14 -0
- package/dist/lib/harness/exec-config-version.d.ts +8 -0
- package/dist/lib/harness/exec-config-version.js +22 -0
- package/dist/lib/harness/index.d.ts +1 -0
- package/dist/lib/harness/index.js +34 -0
- package/dist/lib/{hooks.d.ts → hooks/install.d.ts} +1 -1
- package/dist/lib/{hooks.js → hooks/install.js} +11 -11
- package/dist/lib/hosts/passthrough.d.ts +10 -0
- package/dist/lib/hosts/passthrough.js +28 -6
- package/dist/lib/hosts/session-index.js +1 -1
- package/dist/lib/import.js +1 -1
- package/dist/lib/installations/index.d.ts +4 -4
- package/dist/lib/installations/index.js +3 -4
- package/dist/lib/installations/migrate.js +6 -6
- package/dist/lib/{shims.d.ts → installations/shims.d.ts} +2 -2
- package/dist/lib/{shims.js → installations/shims.js} +13 -115
- package/dist/lib/installations/store.d.ts +141 -3
- package/dist/lib/installations/store.js +475 -5
- package/dist/lib/installations/versions.d.ts +40 -125
- package/dist/lib/installations/versions.js +57 -462
- package/dist/lib/isolation-boundary-report.d.ts +1 -1
- package/dist/lib/isolation-boundary-report.js +1 -1
- package/dist/lib/mailbox-gc.js +2 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.js +19 -8
- package/dist/lib/monitors/config.d.ts +2 -2
- package/dist/lib/monitors/config.js +3 -2
- package/dist/lib/monitors/dispatch.d.ts +1 -1
- package/dist/lib/monitors/dispatch.js +2 -2
- package/dist/lib/monitors/state.d.ts +1 -1
- package/dist/lib/origin-machine.d.ts +18 -0
- package/dist/lib/origin-machine.js +34 -0
- package/dist/lib/perf/db.js +1 -1
- package/dist/lib/perf/spool.js +1 -1
- package/dist/lib/plugins/plugins.js +1 -1
- package/dist/lib/plugins/skills.js +1 -1
- package/dist/lib/prix-account.d.ts +158 -0
- package/dist/lib/prix-account.js +214 -0
- package/dist/lib/profiles.d.ts +7 -0
- package/dist/lib/profiles.js +26 -1
- package/dist/lib/project-key.js +2 -2
- package/dist/lib/project-launch.js +1 -1
- package/dist/lib/refresh.js +2 -2
- package/dist/lib/resource-inventory.d.ts +1 -1
- package/dist/lib/resource-inventory.js +1 -1
- package/dist/lib/rules/rules.js +1 -1
- package/dist/lib/sandbox.d.ts +24 -0
- package/dist/lib/sandbox.js +101 -0
- package/dist/lib/scheduling/routines.d.ts +18 -9
- package/dist/lib/scheduling/routines.js +17 -9
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.js +9 -1
- package/dist/lib/secrets/index.js +1 -1
- package/dist/lib/secrets/list-filter.js +1 -1
- package/dist/lib/self-heal/checks/hook-runtime.js +1 -1
- package/dist/lib/self-heal/checks/path.js +1 -1
- package/dist/lib/self-heal/checks/shadowing.js +1 -1
- package/dist/lib/self-heal/checks/shims.js +1 -1
- package/dist/lib/service-manifest.d.ts +20 -0
- package/dist/lib/service-manifest.js +34 -0
- package/dist/lib/session/cloud.js +1 -1
- package/dist/lib/session/db.js +1 -1
- package/dist/lib/session/discover.d.ts +39 -0
- package/dist/lib/session/discover.js +3 -3
- package/dist/lib/session/fork.js +1 -1
- package/dist/lib/session/live-metadata.js +1 -1
- package/dist/lib/session/origin-machine.d.ts +1 -18
- package/dist/lib/session/origin-machine.js +4 -34
- package/dist/lib/session/parse.js +24 -42
- package/dist/lib/session/recovery.js +1 -1
- package/dist/lib/session/relative-time.d.ts +1 -44
- package/dist/lib/session/relative-time.js +4 -120
- package/dist/lib/session/remote-list.js +1 -1
- package/dist/lib/session/remote.js +2 -2
- package/dist/lib/session/short-id.d.ts +1 -17
- package/dist/lib/session/short-id.js +4 -20
- package/dist/lib/session/width.d.ts +1 -29
- package/dist/lib/session/width.js +4 -101
- package/dist/lib/snapshot.d.ts +2 -2
- package/dist/lib/snapshot.js +1 -1
- package/dist/lib/staleness/prune.d.ts +1 -1
- package/dist/lib/staleness/prune.js +1 -1
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +4 -1
- package/dist/lib/startup/command-registry.js +12 -5
- package/dist/lib/teams/agents.js +2 -19
- package/dist/lib/teams/parsers.js +20 -26
- package/dist/lib/teams/placement-probe.js +1 -1
- package/dist/lib/teams/registry.js +2 -21
- package/dist/lib/teams/scheduler.js +1 -1
- package/dist/lib/text/relative-time.d.ts +44 -0
- package/dist/lib/text/relative-time.js +120 -0
- package/dist/lib/text/short-id.d.ts +17 -0
- package/dist/lib/text/short-id.js +20 -0
- package/dist/lib/text/width.d.ts +29 -0
- package/dist/lib/text/width.js +101 -0
- package/dist/lib/triggers/handlers.d.ts +7 -0
- package/dist/lib/triggers/handlers.js +9 -1
- package/dist/lib/triggers/webhook.d.ts +1 -0
- package/dist/lib/triggers/webhook.js +3 -2
- package/dist/lib/uninstall.js +1 -1
- package/dist/lib/version.d.ts +9 -2
- package/dist/lib/version.js +11 -9
- package/dist/lib/view-types.d.ts +4 -0
- package/package.json +1 -1
- package/dist/commands/tickets.d.ts +0 -2
- package/dist/commands/tickets.js +0 -43
- package/dist/lib/observe-aliases.d.ts +0 -30
- package/dist/lib/observe-aliases.js +0 -38
- package/dist/lib/tickets/list.d.ts +0 -53
- package/dist/lib/tickets/list.js +0 -153
- /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
|
-
*
|
|
19
|
-
*
|
|
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.
|
|
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
|
+
}
|