@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.
- package/CHANGELOG.md +12 -0
- package/dist/commands/secrets-import.d.ts +18 -0
- package/dist/commands/secrets-import.js +74 -0
- package/dist/commands/secrets.js +2 -0
- package/dist/index.js +14 -119
- package/dist/lib/daemon.js +40 -6
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/secrets/fallback.d.ts +48 -0
- package/dist/lib/secrets/fallback.js +48 -0
- package/dist/lib/secrets/index.d.ts +11 -0
- package/dist/lib/secrets/index.js +20 -2
- package/dist/lib/secrets/linux.d.ts +7 -0
- package/dist/lib/secrets/linux.js +113 -5
- package/dist/lib/secrets/windows.d.ts +7 -0
- package/dist/lib/secrets/windows.js +110 -5
- package/dist/lib/self-heal/checks/path.d.ts +2 -0
- package/dist/lib/self-heal/checks/path.js +30 -0
- package/dist/lib/self-heal/checks/resources.d.ts +2 -0
- package/dist/lib/self-heal/checks/resources.js +36 -0
- package/dist/lib/self-heal/checks/shadowing.d.ts +2 -0
- package/dist/lib/self-heal/checks/shadowing.js +48 -0
- package/dist/lib/self-heal/checks/shims.d.ts +2 -0
- package/dist/lib/self-heal/checks/shims.js +35 -0
- package/dist/lib/self-heal/registry.d.ts +22 -0
- package/dist/lib/self-heal/registry.js +66 -0
- package/dist/lib/self-heal/types.d.ts +41 -0
- package/dist/lib/self-heal/types.js +21 -0
- package/dist/lib/session/discover.js +41 -0
- package/dist/lib/shim-heal.d.ts +23 -0
- package/dist/lib/shim-heal.js +109 -0
- package/dist/lib/shims.d.ts +6 -0
- package/dist/lib/shims.js +1 -1
- package/dist/lib/versions.d.ts +16 -0
- package/dist/lib/versions.js +83 -12
- 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
|
+
}
|
package/dist/lib/shims.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/lib/versions.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/versions.js
CHANGED
|
@@ -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
|
-
//
|
|
1626
|
-
//
|
|
1627
|
-
//
|
|
1628
|
-
//
|
|
1629
|
-
//
|
|
1630
|
-
//
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|