@phnx-labs/agents-cli 1.20.45 → 1.20.47

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 (35) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/commands/secrets-import.d.ts +18 -0
  3. package/dist/commands/secrets-import.js +74 -0
  4. package/dist/commands/secrets.js +2 -0
  5. package/dist/index.js +14 -119
  6. package/dist/lib/daemon.js +40 -6
  7. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  8. package/dist/lib/secrets/fallback.d.ts +48 -0
  9. package/dist/lib/secrets/fallback.js +48 -0
  10. package/dist/lib/secrets/index.d.ts +11 -0
  11. package/dist/lib/secrets/index.js +20 -2
  12. package/dist/lib/secrets/linux.d.ts +7 -0
  13. package/dist/lib/secrets/linux.js +113 -5
  14. package/dist/lib/secrets/windows.d.ts +7 -0
  15. package/dist/lib/secrets/windows.js +110 -5
  16. package/dist/lib/self-heal/checks/path.d.ts +2 -0
  17. package/dist/lib/self-heal/checks/path.js +30 -0
  18. package/dist/lib/self-heal/checks/resources.d.ts +2 -0
  19. package/dist/lib/self-heal/checks/resources.js +36 -0
  20. package/dist/lib/self-heal/checks/shadowing.d.ts +2 -0
  21. package/dist/lib/self-heal/checks/shadowing.js +48 -0
  22. package/dist/lib/self-heal/checks/shims.d.ts +2 -0
  23. package/dist/lib/self-heal/checks/shims.js +35 -0
  24. package/dist/lib/self-heal/registry.d.ts +22 -0
  25. package/dist/lib/self-heal/registry.js +66 -0
  26. package/dist/lib/self-heal/types.d.ts +41 -0
  27. package/dist/lib/self-heal/types.js +21 -0
  28. package/dist/lib/session/discover.js +41 -0
  29. package/dist/lib/shim-heal.d.ts +23 -0
  30. package/dist/lib/shim-heal.js +109 -0
  31. package/dist/lib/shims.d.ts +6 -0
  32. package/dist/lib/shims.js +1 -1
  33. package/dist/lib/versions.d.ts +16 -0
  34. package/dist/lib/versions.js +83 -12
  35. package/package.json +1 -1
@@ -0,0 +1,41 @@
1
+ export type HealCheckId = 'resources' | 'shims' | 'shadowing' | 'path';
2
+ /** When the daemon schedules a check. */
3
+ export type HealCadence = 'startup' | 'frequent' | 'periodic';
4
+ export interface HealCtx {
5
+ /** 'safe' = daemon (low-risk only); 'full' = doctor --fix (everything). */
6
+ mode: 'safe' | 'full';
7
+ /** Detect only — never write. Powers `agents doctor` (read-only) and previews. */
8
+ dryRun: boolean;
9
+ }
10
+ /** Outcome of one check. `ok` means nothing was wrong. */
11
+ export interface CheckResult {
12
+ /** Things repaired (or, under dryRun, that WOULD be repaired). Human-readable. */
13
+ fixed: string[];
14
+ /** Detected but not auto-fixed: unfixable, or risky-in-safe-mode. Human-readable. */
15
+ needsAttention: string[];
16
+ /** True iff detect found nothing wrong (fixed and needsAttention both empty). */
17
+ ok: boolean;
18
+ }
19
+ export interface HealCheck {
20
+ id: HealCheckId;
21
+ title: string;
22
+ /** Restrict to these platforms; omit to run on all. */
23
+ platforms?: NodeJS.Platform[];
24
+ cadence: HealCadence;
25
+ /** Detect + (repair unless dryRun). Must be headless (no TTY/prompt) and idempotent. */
26
+ run(ctx: HealCtx): Promise<CheckResult>;
27
+ }
28
+ export interface CheckReport {
29
+ id: HealCheckId;
30
+ title: string;
31
+ result: CheckResult | null;
32
+ /** Set when the check itself threw (isolated — one check failing never aborts the run). */
33
+ error?: string;
34
+ }
35
+ export interface SelfHealReport {
36
+ checks: CheckReport[];
37
+ }
38
+ /** Convenience: an all-clear result. */
39
+ export declare function okResult(): CheckResult;
40
+ /** Build a CheckResult from collected fixes/attention items (ok iff both empty). */
41
+ export declare function resultOf(fixed: string[], needsAttention: string[]): CheckResult;
@@ -0,0 +1,21 @@
1
+ // Unified self-heal subsystem — shared shapes.
2
+ //
3
+ // agents-cli had ~37 separate repair routines scattered across the daemon, every
4
+ // CLI startup, and a handful of commands, each hand-rolling detect+fix on its own
5
+ // trigger. This subsystem gives every repairable class of problem ONE shape — a
6
+ // HealCheck — driven by ONE runner, hosted behind TWO front doors (the daemon,
7
+ // on tiered schedules, and `agents doctor`, on demand).
8
+ //
9
+ // A check's `run()` both detects and repairs in a single pass (repair is skipped
10
+ // when `ctx.dryRun`), mirroring the existing resource heal (heal.ts) which computes
11
+ // and applies together. `mode` gates how aggressive a repair may be: 'safe' (the
12
+ // daemon default) fixes only low-risk drift and merely reports risky conditions;
13
+ // 'full' (`agents doctor --fix`) applies everything.
14
+ /** Convenience: an all-clear result. */
15
+ export function okResult() {
16
+ return { fixed: [], needsAttention: [], ok: true };
17
+ }
18
+ /** Build a CheckResult from collected fixes/attention items (ok iff both empty). */
19
+ export function resultOf(fixed, needsAttention) {
20
+ return { fixed, needsAttention, ok: fixed.length === 0 && needsAttention.length === 0 };
21
+ }
@@ -2456,6 +2456,8 @@ export function readKimiMeta(filePath) {
2456
2456
  project = parts.slice(0, -1).join('/');
2457
2457
  }
2458
2458
  }
2459
+ // Parse wire.jsonl to extract message count and token usage
2460
+ const { messageCount, tokenCount } = parseKimiWireMetrics(sessionDir);
2459
2461
  const meta = {
2460
2462
  id: sessionId,
2461
2463
  shortId,
@@ -2464,9 +2466,48 @@ export function readKimiMeta(filePath) {
2464
2466
  project,
2465
2467
  filePath,
2466
2468
  topic,
2469
+ messageCount,
2470
+ tokenCount: tokenCount > 0 ? tokenCount : undefined,
2467
2471
  };
2468
2472
  return { meta, content: lastPrompt || '' };
2469
2473
  }
2474
+ /** Parse Kimi's wire.jsonl to extract message count and token usage.
2475
+ * TODO: optimize to stream (like scanClaudeSession) to avoid loading large files into memory.
2476
+ * For now, synchronous readFileSync matches the pattern of reading state.json and is acceptable
2477
+ * since session dirs are usually fresh in FS cache during incremental scans. */
2478
+ function parseKimiWireMetrics(sessionDir) {
2479
+ const wirePath = path.join(sessionDir, 'agents', 'main', 'wire.jsonl');
2480
+ let messageCount = 0;
2481
+ let tokenCount = 0;
2482
+ if (!fs.existsSync(wirePath)) {
2483
+ return { messageCount: 0, tokenCount: 0 };
2484
+ }
2485
+ try {
2486
+ const lines = fs.readFileSync(wirePath, 'utf-8').split('\n');
2487
+ for (const line of lines) {
2488
+ if (!line.trim())
2489
+ continue;
2490
+ try {
2491
+ const event = JSON.parse(line);
2492
+ if (event.type === 'context.append_message') {
2493
+ messageCount++;
2494
+ }
2495
+ else if (event.type === 'usage.record' && event.usage) {
2496
+ // Kimi usage structure: inputOther + output + inputCacheRead + inputCacheCreation
2497
+ const u = event.usage;
2498
+ tokenCount += (u.inputOther || 0) + (u.output || 0) + (u.inputCacheRead || 0) + (u.inputCacheCreation || 0);
2499
+ }
2500
+ }
2501
+ catch {
2502
+ // Malformed line, skip
2503
+ }
2504
+ }
2505
+ }
2506
+ catch {
2507
+ // If wire.jsonl can't be read, return 0s (graceful degradation)
2508
+ }
2509
+ return { messageCount, tokenCount };
2510
+ }
2470
2511
  /** Parse a time filter string (relative like '7d' or ISO timestamp) into epoch milliseconds. */
2471
2512
  export function parseTimeFilter(input) {
2472
2513
  const relativeMatch = input.match(/^(\d+)([mhdw])$/i);
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Heal the shim/shadow/PATH conditions silently, then return the notice lines to
3
+ * print ONCE for whatever is left (a real-binary shadow we won't move; a PATH entry
4
+ * that was just added / needs a reload), or null when there's nothing new to say.
5
+ */
6
+ export declare function healShimsInteractive(): Promise<string[] | null>;
7
+ type PathNoticeState = 'ok' | 'added' | 'reload';
8
+ /**
9
+ * A stable signature of the conditions worth surfacing: the sorted set of
10
+ * real-binary shadows plus the PATH notice state. Empty string = nothing to say.
11
+ */
12
+ export declare function computeShimNoticeSignature(input: {
13
+ shadowNotes: string[];
14
+ pathState: PathNoticeState;
15
+ }): string;
16
+ /**
17
+ * Whether to surface the notice for the current condition. Returns false (stay
18
+ * quiet) when the exact same signature was already surfaced, or when there's
19
+ * nothing to say. On true it records the signature so the next shell with the same
20
+ * state is suppressed. An empty signature clears the marker.
21
+ */
22
+ export declare function shouldSurfaceShimNotice(signature: string): boolean;
23
+ export {};
@@ -0,0 +1,109 @@
1
+ // Interactive shim-heal for the CLI startup path + the persistent notice-state that
2
+ // replaces the old per-PPID sentinel.
3
+ //
4
+ // The actual repair (regenerating shims, adopting symlink launchers, adding the
5
+ // shims dir to PATH) lives in the unified self-heal registry — this module just
6
+ // drives the shim-relevant checks SILENTLY on a normal `agents` invocation and then
7
+ // decides whether to print a one-time notice about anything left. The old flow
8
+ // re-ran its whole detect-and-nag on every new terminal (its sentinel was keyed to
9
+ // process.ppid); the persistent signature here means an unresolved condition is
10
+ // surfaced once, not on every shell.
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { getRuntimeStateDir } from './state.js';
14
+ /**
15
+ * Heal the shim/shadow/PATH conditions silently, then return the notice lines to
16
+ * print ONCE for whatever is left (a real-binary shadow we won't move; a PATH entry
17
+ * that was just added / needs a reload), or null when there's nothing new to say.
18
+ */
19
+ export async function healShimsInteractive() {
20
+ const { runSelfHeal } = await import('./self-heal/registry.js');
21
+ const report = await runSelfHeal({ checks: ['shims', 'shadowing', 'path'], mode: 'safe' });
22
+ const shadowNotes = [];
23
+ let pathAdded = null;
24
+ let pathReload = null;
25
+ for (const c of report.checks) {
26
+ if (!c.result)
27
+ continue;
28
+ if (c.id === 'shadowing')
29
+ shadowNotes.push(...c.result.needsAttention);
30
+ if (c.id === 'path') {
31
+ for (const f of c.result.fixed)
32
+ pathAdded = f; // "added shims to PATH (~/.zshrc)"
33
+ for (const a of c.result.needsAttention)
34
+ pathReload = a; // "...not loaded — open a new terminal"
35
+ }
36
+ }
37
+ const pathState = pathAdded ? 'added' : pathReload ? 'reload' : 'ok';
38
+ const signature = computeShimNoticeSignature({ shadowNotes, pathState });
39
+ if (!shouldSurfaceShimNotice(signature))
40
+ return null;
41
+ const lines = [];
42
+ if (pathAdded) {
43
+ lines.push(pathAdded);
44
+ lines.push('Open a new terminal (or source your shell rc) to pick it up.');
45
+ }
46
+ else if (pathReload) {
47
+ lines.push(pathReload);
48
+ }
49
+ if (shadowNotes.length > 0) {
50
+ lines.push('These agent commands run a native binary instead of the version-managed shim:');
51
+ for (const note of shadowNotes)
52
+ lines.push(` ${note}`);
53
+ lines.push("It's a real binary (not a symlink), so agents-cli won't move it — reorder PATH or remove it to hand it over.");
54
+ }
55
+ return lines.length > 0 ? lines : null;
56
+ }
57
+ function noticeStatePath() {
58
+ return path.join(getRuntimeStateDir(), 'shim-notice.json');
59
+ }
60
+ /**
61
+ * A stable signature of the conditions worth surfacing: the sorted set of
62
+ * real-binary shadows plus the PATH notice state. Empty string = nothing to say.
63
+ */
64
+ export function computeShimNoticeSignature(input) {
65
+ const shadows = [...input.shadowNotes].sort().join(',');
66
+ const parts = [];
67
+ if (shadows)
68
+ parts.push(`shadow:${shadows}`);
69
+ if (input.pathState !== 'ok')
70
+ parts.push(`path:${input.pathState}`);
71
+ return parts.join('|');
72
+ }
73
+ function readLastNoticeSignature() {
74
+ try {
75
+ const parsed = JSON.parse(fs.readFileSync(noticeStatePath(), 'utf-8'));
76
+ return typeof parsed.signature === 'string' ? parsed.signature : null;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ function writeLastNoticeSignature(signature) {
83
+ try {
84
+ fs.mkdirSync(getRuntimeStateDir(), { recursive: true });
85
+ fs.writeFileSync(noticeStatePath(), JSON.stringify({ signature }));
86
+ }
87
+ catch {
88
+ /* best-effort: never block a command on the marker */
89
+ }
90
+ }
91
+ /**
92
+ * Whether to surface the notice for the current condition. Returns false (stay
93
+ * quiet) when the exact same signature was already surfaced, or when there's
94
+ * nothing to say. On true it records the signature so the next shell with the same
95
+ * state is suppressed. An empty signature clears the marker.
96
+ */
97
+ export function shouldSurfaceShimNotice(signature) {
98
+ if (!signature) {
99
+ try {
100
+ fs.rmSync(noticeStatePath(), { force: true });
101
+ }
102
+ catch { /* best-effort */ }
103
+ return false;
104
+ }
105
+ if (readLastNoticeSignature() === signature)
106
+ return false;
107
+ writeLastNoticeSignature(signature);
108
+ return true;
109
+ }
@@ -278,6 +278,12 @@ export declare function getConfigSymlinkVersion(agent: AgentId): string | null;
278
278
  */
279
279
  export declare function onDiskShimFile(cliCommand: string, platform: NodeJS.Platform): string;
280
280
  export declare function shimExists(agent: AgentId): boolean;
281
+ /**
282
+ * True if the on-disk shim's schema version matches `SHIM_SCHEMA_VERSION`.
283
+ * False means either the shim is missing, is pre-v2 (no marker), or is an
284
+ * older version that needs regeneration.
285
+ */
286
+ export declare function isShimCurrent(agent: AgentId): boolean;
281
287
  /**
282
288
  * Regenerate the shim if it's missing or outdated. Returns a status describing
283
289
  * what happened — callers can surface a one-line notice to the user ("Updated
package/dist/lib/shims.js CHANGED
@@ -1606,7 +1606,7 @@ function readShimSchemaVersion(agent) {
1606
1606
  * False means either the shim is missing, is pre-v2 (no marker), or is an
1607
1607
  * older version that needs regeneration.
1608
1608
  */
1609
- function isShimCurrent(agent) {
1609
+ export function isShimCurrent(agent) {
1610
1610
  const version = readShimSchemaVersion(agent);
1611
1611
  return version === SHIM_SCHEMA_VERSION;
1612
1612
  }
@@ -320,6 +320,21 @@ export declare function isMissingBinarySignature(output: string): boolean;
320
320
  * missing-binary signature (see isMissingBinarySignature) fails the check; a
321
321
  * plain nonzero exit or a timeout is treated as healthy so we never false-fail.
322
322
  */
323
+ /**
324
+ * Compose the spawn spec for a `<binary> --version` launch probe. On Windows the
325
+ * `.cmd` wrapper runs through cmd.exe, so the path is fully quoted into ONE
326
+ * command line and the args array is emptied (composeWin32CommandLine) — the
327
+ * DEP0190-safe pattern the real launch uses. Critically this keeps a spaced
328
+ * Windows profile path (`C:\Users\John Doe\…\claude.cmd`) intact; passing the raw
329
+ * path to a shell would split it at the space and false-fail a HEALTHY install.
330
+ * On POSIX no shell is involved and the binary is exec'd directly. Pure/exported
331
+ * so the quoting is unit-testable without spawning.
332
+ */
333
+ export declare function probeSpawnSpec(binary: string, isWin: boolean): {
334
+ command: string;
335
+ args: string[];
336
+ shell: boolean;
337
+ };
323
338
  export declare function verifyInstalledBinaryLaunches(agent: AgentId, version: string): Promise<{
324
339
  ok: boolean;
325
340
  detail?: string;
@@ -344,6 +359,7 @@ export declare function verifyInstalledBinaryLaunches(agent: AgentId, version: s
344
359
  * droid) have no such tarball and are returned unchanged.
345
360
  */
346
361
  export declare function ensureAgentRunnable(agent: AgentId, version: string, log?: (message: string) => void): Promise<string | null>;
362
+ export declare function healBrokenDefaultLaunches(log?: (m: string) => void): Promise<string[]>;
347
363
  /** Outcome of syncing resources to a version home, keyed by resource type. */
348
364
  export interface SyncResult {
349
365
  commands: boolean;
@@ -33,7 +33,7 @@ import { discoverPermissionGroups, getActivePermissionPresetName, readPermission
33
33
  import { parseMcpServerConfig } from './mcp.js';
34
34
  import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink } from './shims.js';
35
35
  import { importInstallScriptBinary } from './import.js';
36
- import { IS_WINDOWS } from './platform/index.js';
36
+ import { IS_WINDOWS, composeWin32CommandLine } from './platform/index.js';
37
37
  import { pruneVersionHomeHookEntriesFromSettings } from './hooks.js';
38
38
  import { supports, explainSkip } from './capabilities.js';
39
39
  import { discoverPlugins } from './plugins.js';
@@ -1621,22 +1621,48 @@ export function isMissingBinarySignature(output) {
1621
1621
  * missing-binary signature (see isMissingBinarySignature) fails the check; a
1622
1622
  * plain nonzero exit or a timeout is treated as healthy so we never false-fail.
1623
1623
  */
1624
+ /**
1625
+ * Compose the spawn spec for a `<binary> --version` launch probe. On Windows the
1626
+ * `.cmd` wrapper runs through cmd.exe, so the path is fully quoted into ONE
1627
+ * command line and the args array is emptied (composeWin32CommandLine) — the
1628
+ * DEP0190-safe pattern the real launch uses. Critically this keeps a spaced
1629
+ * Windows profile path (`C:\Users\John Doe\…\claude.cmd`) intact; passing the raw
1630
+ * path to a shell would split it at the space and false-fail a HEALTHY install.
1631
+ * On POSIX no shell is involved and the binary is exec'd directly. Pure/exported
1632
+ * so the quoting is unit-testable without spawning.
1633
+ */
1634
+ export function probeSpawnSpec(binary, isWin) {
1635
+ if (isWin)
1636
+ return { command: composeWin32CommandLine(binary, ['--version']), args: [], shell: true };
1637
+ return { command: binary, args: ['--version'], shell: false };
1638
+ }
1624
1639
  export async function verifyInstalledBinaryLaunches(agent, version) {
1625
- // Windows: `getBinaryPath` returns the extensionless `.bin/<cli>` (a shell
1626
- // wrapper), NOT the `.cmd`/`.exe` that actually launches there — `execFile`ing
1627
- // it would ENOENT on a perfectly healthy install, and the integrity gate would
1628
- // then WIPE it. The gutted-native-binary failure this guards against is a POSIX
1629
- // concern in practice; treat win32 as healthy rather than risk destroying a
1630
- // good install. (isVersionInstalled already validates presence on Windows.)
1631
- if (process.platform === 'win32')
1632
- return { ok: true };
1633
- const binary = getBinaryPath(agent, version);
1640
+ // The real launch target differs by platform, so probe whatever `agents run`
1641
+ // actually execs. On Windows that's the npm `.cmd` wrapper (exec.ts uses
1642
+ // `absPath + '.cmd'`), which chains to the native `.exe`; a gutted install
1643
+ // (renamed/missing `.exe`) makes that wrapper emit "is not recognized" the
1644
+ // exact win-mini failure a vendor auto-update leaves behind. Probing the
1645
+ // extensionless `.bin/<cli>` instead would ENOENT even on a HEALTHY Windows
1646
+ // install, so we DON'T. On POSIX the `.bin/<cli>` binary is the launch target
1647
+ // and is probed directly.
1648
+ const isWin = process.platform === 'win32';
1649
+ const binary = isWin ? getBinaryPath(agent, version) + '.cmd' : getBinaryPath(agent, version);
1634
1650
  if (!fs.existsSync(binary)) {
1635
- return { ok: false, detail: `binary not found at ${binary}` };
1651
+ // Windows: a missing `.cmd` means a non-npm/global agent (droid.exe) we can't
1652
+ // safely probe — treat as healthy (isVersionInstalled validates presence).
1653
+ // POSIX: a missing launch binary is a genuine gutted install.
1654
+ return isWin ? { ok: true } : { ok: false, detail: `binary not found at ${binary}` };
1636
1655
  }
1637
1656
  try {
1638
- await execFileAsync(binary, ['--version'], {
1657
+ // On Windows the `.cmd` runs via cmd.exe (shell). Pass a single FULLY-QUOTED
1658
+ // command line + EMPTY args (composeWin32CommandLine) — the same DEP0190-safe
1659
+ // pattern the real launch uses (exec.ts) — so a space in the Windows profile
1660
+ // path (`C:\Users\John Doe\…`) can't split the path and false-fail a healthy
1661
+ // install into a destructive reinstall.
1662
+ const spec = probeSpawnSpec(binary, isWin);
1663
+ await execFileAsync(spec.command, spec.args, {
1639
1664
  timeout: 15000,
1665
+ shell: spec.shell,
1640
1666
  env: { ...process.env, HOME: getVersionHomePath(agent, version) },
1641
1667
  });
1642
1668
  return { ok: true };
@@ -1703,6 +1729,51 @@ export async function ensureAgentRunnable(agent, version, log) {
1703
1729
  }
1704
1730
  return null;
1705
1731
  }
1732
+ /**
1733
+ * Proactive launch-health pass for the daemon. Probe the DEFAULT version of
1734
+ * every npm-package agent and repair any that won't launch (via
1735
+ * ensureAgentRunnable), so a gutted install is healed BEFORE the user's next
1736
+ * `agents run` hits a raw ENOENT — the run-time heal (ensureAgentRunnable) only
1737
+ * fires once a run is already starting; this catches it in the background.
1738
+ *
1739
+ * Returns a label (`agent@broken→healed`) for each version actually repaired, so
1740
+ * the daemon can log/notify. A version that already launches costs one cheap
1741
+ * `--version` probe and is left untouched.
1742
+ */
1743
+ const failedRepairAt = new Map();
1744
+ const REPAIR_COOLDOWN_MS = 24 * 60 * 60_000;
1745
+ export async function healBrokenDefaultLaunches(log) {
1746
+ const repaired = [];
1747
+ for (const agent of Object.keys(AGENTS)) {
1748
+ if (!AGENTS[agent].npmPackage)
1749
+ continue; // native/global agents have no gutted-tarball failure mode
1750
+ const version = getGlobalDefault(agent);
1751
+ if (!version)
1752
+ continue;
1753
+ if ((await verifyInstalledBinaryLaunches(agent, version)).ok)
1754
+ continue;
1755
+ // Backoff: a version whose repair just failed (offline, npm 404, an arch the
1756
+ // registry can't serve) must NOT re-trigger a full clean-reinstall +
1757
+ // install-latest on every 6h pass. Skip it for a day; a daemon restart clears
1758
+ // the memo, giving a fresh attempt.
1759
+ const key = `${agent}@${version}`;
1760
+ const last = failedRepairAt.get(key);
1761
+ if (last !== undefined && Date.now() - last < REPAIR_COOLDOWN_MS) {
1762
+ log?.(`${AGENTS[agent].name}@${version} still won't launch — repair attempted recently, skipping until cooldown elapses.`);
1763
+ continue;
1764
+ }
1765
+ log?.(`${AGENTS[agent].name}@${version} won't launch — repairing…`);
1766
+ const healed = await ensureAgentRunnable(agent, version, log);
1767
+ if (healed) {
1768
+ failedRepairAt.delete(key);
1769
+ repaired.push(`${agent}@${version}${healed === version ? '' : `→${healed}`}`);
1770
+ }
1771
+ else {
1772
+ failedRepairAt.set(key, Date.now());
1773
+ }
1774
+ }
1775
+ return repaired;
1776
+ }
1706
1777
  async function getCliVersionFromPath(agent) {
1707
1778
  const agentConfig = AGENTS[agent];
1708
1779
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.45",
3
+ "version": "1.20.47",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",