@phnx-labs/agents-cli 1.20.26 → 1.20.28
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 +29 -0
- package/dist/commands/doctor.d.ts +5 -2
- package/dist/commands/doctor.js +182 -30
- package/dist/commands/exec.d.ts +1 -1
- package/dist/commands/exec.js +177 -6
- package/dist/commands/hosts.d.ts +11 -0
- package/dist/commands/hosts.js +229 -0
- package/dist/commands/repo.d.ts +29 -0
- package/dist/commands/repo.js +174 -38
- package/dist/commands/secrets.d.ts +2 -7
- package/dist/commands/secrets.js +15 -23
- package/dist/commands/sessions.d.ts +2 -0
- package/dist/commands/sessions.js +27 -25
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +22 -5
- package/dist/commands/view.js +27 -11
- package/dist/index.js +4 -13
- package/dist/lib/agent-spec.d.ts +36 -0
- package/dist/lib/agent-spec.js +157 -0
- package/dist/lib/agents.d.ts +1 -0
- package/dist/lib/agents.js +45 -4
- package/dist/lib/browser/drivers/ssh.d.ts +47 -2
- package/dist/lib/browser/drivers/ssh.js +113 -24
- package/dist/lib/browser/profiles.js +28 -1
- package/dist/lib/browser/runtime-state.js +28 -8
- package/dist/lib/browser/types.d.ts +10 -1
- package/dist/lib/cli-resources.js +10 -1
- package/dist/lib/daemon.js +32 -0
- package/dist/lib/doctor-diff.d.ts +19 -0
- package/dist/lib/doctor-diff.js +107 -15
- package/dist/lib/exec.d.ts +27 -0
- package/dist/lib/exec.js +62 -19
- package/dist/lib/heal.d.ts +107 -0
- package/dist/lib/heal.js +279 -0
- package/dist/lib/hooks.d.ts +17 -0
- package/dist/lib/hooks.js +127 -3
- package/dist/lib/hosts/dispatch.d.ts +26 -0
- package/dist/lib/hosts/dispatch.js +71 -0
- package/dist/lib/hosts/progress.d.ts +21 -0
- package/dist/lib/hosts/progress.js +49 -0
- package/dist/lib/hosts/providers/local.d.ts +17 -0
- package/dist/lib/hosts/providers/local.js +81 -0
- package/dist/lib/hosts/ready.d.ts +37 -0
- package/dist/lib/hosts/ready.js +88 -0
- package/dist/lib/hosts/registry.d.ts +22 -0
- package/dist/lib/hosts/registry.js +65 -0
- package/dist/lib/hosts/ssh-config.d.ts +37 -0
- package/dist/lib/hosts/ssh-config.js +157 -0
- package/dist/lib/hosts/tasks.d.ts +32 -0
- package/dist/lib/hosts/tasks.js +58 -0
- package/dist/lib/hosts/types.d.ts +51 -0
- package/dist/lib/hosts/types.js +21 -0
- package/dist/lib/loop.d.ts +9 -0
- package/dist/lib/loop.js +13 -1
- package/dist/lib/mcp.js +12 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.js +9 -5
- package/dist/lib/platform/exec.d.ts +10 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/platform/index.d.ts +1 -0
- package/dist/lib/platform/index.js +1 -0
- package/dist/lib/platform/links.d.ts +15 -0
- package/dist/lib/platform/links.js +42 -0
- package/dist/lib/platform/paths.d.ts +18 -0
- package/dist/lib/platform/paths.js +22 -0
- package/dist/lib/platform/posixpath.d.ts +28 -0
- package/dist/lib/platform/posixpath.js +153 -0
- package/dist/lib/plugin-marketplace.d.ts +18 -0
- package/dist/lib/plugin-marketplace.js +67 -1
- package/dist/lib/plugins.d.ts +33 -1
- package/dist/lib/plugins.js +56 -11
- package/dist/lib/project-launch.js +6 -3
- package/dist/lib/sandbox.js +5 -2
- package/dist/lib/self-update.js +7 -2
- package/dist/lib/session/db.d.ts +23 -0
- package/dist/lib/session/db.js +76 -1
- package/dist/lib/session/discover.d.ts +26 -0
- package/dist/lib/session/discover.js +75 -4
- package/dist/lib/session/relative-time.d.ts +7 -0
- package/dist/lib/session/relative-time.js +28 -0
- package/dist/lib/session/remote.d.ts +61 -0
- package/dist/lib/session/remote.js +221 -0
- package/dist/lib/ssh-exec.d.ts +45 -0
- package/dist/lib/ssh-exec.js +61 -0
- package/dist/lib/staleness/detectors/commands.js +7 -6
- package/dist/lib/staleness/writers/commands.js +7 -12
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/startup/dev-build.d.ts +22 -0
- package/dist/lib/startup/dev-build.js +41 -0
- package/dist/lib/types.d.ts +28 -0
- package/dist/lib/versions.d.ts +9 -3
- package/dist/lib/versions.js +43 -7
- package/package.json +3 -3
- package/scripts/postinstall.js +62 -0
package/dist/lib/doctor-diff.js
CHANGED
|
@@ -23,7 +23,9 @@ import * as fs from 'fs';
|
|
|
23
23
|
import * as path from 'path';
|
|
24
24
|
import { AGENTS, agentConfigDirName } from './agents.js';
|
|
25
25
|
import { getProjectAgentsDir, getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos, getResolvedRulesDir, getUserRulesDir, getEffectivePromptcutsPath, } from './state.js';
|
|
26
|
-
import { getAvailableResources, getActuallySyncedResources, getVersionHomePath, } from './versions.js';
|
|
26
|
+
import { getAvailableResources, getActuallySyncedResources, getVersionHomePath, compareVersions, } from './versions.js';
|
|
27
|
+
import { discoverPlugins, marketplaceSpecForName } from './plugins.js';
|
|
28
|
+
import { pluginInstallDir, repairableManifestFields } from './plugin-marketplace.js';
|
|
27
29
|
import { markdownToToml } from './convert.js';
|
|
28
30
|
import { resolveImports, supportsRulesImports } from './rules/compile.js';
|
|
29
31
|
import { listCommandsInVersionHome, getVersionCommandsDir } from './commands.js';
|
|
@@ -80,13 +82,13 @@ function buildLayerBases(cwd, kind, opts = {}) {
|
|
|
80
82
|
return out;
|
|
81
83
|
}
|
|
82
84
|
// ─── commands ─────────────────────────────────────────────────────────────────
|
|
83
|
-
function diffCommands(agent, version, cwd) {
|
|
85
|
+
function diffCommands(agent, version, cwd, excludeProject = false) {
|
|
84
86
|
const agentConfig = AGENTS[agent];
|
|
85
87
|
const isToml = agentConfig.format === 'toml';
|
|
86
88
|
const ext = isToml ? '.toml' : '.md';
|
|
87
89
|
const homeDir = getVersionCommandsDir(agent, version);
|
|
88
90
|
const installed = new Set(listCommandsInVersionHome(agent, version));
|
|
89
|
-
const layerBases = buildLayerBases(cwd, 'commands');
|
|
91
|
+
const layerBases = buildLayerBases(cwd, 'commands', { excludeProject });
|
|
90
92
|
const sourceByName = new Map();
|
|
91
93
|
for (const base of layerBases) {
|
|
92
94
|
if (!fs.existsSync(base.path))
|
|
@@ -190,10 +192,10 @@ function dirsContentMatch(src, dst) {
|
|
|
190
192
|
}
|
|
191
193
|
return true;
|
|
192
194
|
}
|
|
193
|
-
function diffSkills(agent, version, cwd) {
|
|
195
|
+
function diffSkills(agent, version, cwd, excludeProject = false) {
|
|
194
196
|
const homeDir = getVersionSkillsDir(agent, version);
|
|
195
197
|
const installed = new Set(listSkillsInVersionHome(agent, version));
|
|
196
|
-
const layerBases = buildLayerBases(cwd, 'skills');
|
|
198
|
+
const layerBases = buildLayerBases(cwd, 'skills', { excludeProject });
|
|
197
199
|
const sourceByName = new Map();
|
|
198
200
|
for (const base of layerBases) {
|
|
199
201
|
if (!fs.existsSync(base.path))
|
|
@@ -298,8 +300,8 @@ function diffHooks(agent, version, cwd) {
|
|
|
298
300
|
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
299
301
|
}
|
|
300
302
|
// ─── rules / memory ───────────────────────────────────────────────────────────
|
|
301
|
-
function listRulesNames(cwd) {
|
|
302
|
-
const projectDir = getProjectAgentsDir(cwd);
|
|
303
|
+
function listRulesNames(cwd, excludeProject = false) {
|
|
304
|
+
const projectDir = excludeProject ? null : getProjectAgentsDir(cwd);
|
|
303
305
|
const userRules = getUserRulesDir();
|
|
304
306
|
const systemRules = getResolvedRulesDir();
|
|
305
307
|
const extras = getEnabledExtraRepos();
|
|
@@ -349,11 +351,11 @@ function expectedRuleContent(agent, name, sourcePath) {
|
|
|
349
351
|
}
|
|
350
352
|
return readSafe(sourcePath);
|
|
351
353
|
}
|
|
352
|
-
function diffRules(agent, version, cwd) {
|
|
354
|
+
function diffRules(agent, version, cwd, excludeProject = false) {
|
|
353
355
|
const agentConfig = AGENTS[agent];
|
|
354
356
|
const versionHome = getVersionHomePath(agent, version);
|
|
355
357
|
const configDir = path.join(versionHome, agentConfigDirName(agent));
|
|
356
|
-
const sourcesByName = listRulesNames(cwd);
|
|
358
|
+
const sourcesByName = listRulesNames(cwd, excludeProject);
|
|
357
359
|
// Files actually present in the version home.
|
|
358
360
|
const homeFiles = new Set();
|
|
359
361
|
if (fs.existsSync(configDir)) {
|
|
@@ -425,6 +427,91 @@ function diffPresenceOnly(kind, available, synced) {
|
|
|
425
427
|
}
|
|
426
428
|
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
427
429
|
}
|
|
430
|
+
// ─── plugins (content-aware) ───────────────────────────────────────────────
|
|
431
|
+
function listPluginSkillDirs(pluginDir) {
|
|
432
|
+
const d = path.join(pluginDir, 'skills');
|
|
433
|
+
try {
|
|
434
|
+
return fs.readdirSync(d, { withFileTypes: true })
|
|
435
|
+
.filter((e) => (e.isDirectory() || e.isSymbolicLink()) && fs.existsSync(path.join(d, e.name, 'SKILL.md')))
|
|
436
|
+
.map((e) => e.name);
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
return [];
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function listPluginCommandFiles(pluginDir) {
|
|
443
|
+
const d = path.join(pluginDir, 'commands');
|
|
444
|
+
try {
|
|
445
|
+
return fs.readdirSync(d).filter((f) => f.endsWith('.md')).map((f) => f.replace(/\.md$/, ''));
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
return [];
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Describe how a version's marketplace MIRROR of a plugin diverges from its
|
|
453
|
+
* central source — the detail presence-only checks miss. Surfaces a stale mirror
|
|
454
|
+
* version, a Claude-invalid manifest, and (the part users care about) the
|
|
455
|
+
* plugin's own skills/commands that never made it into the mirror. Returns null
|
|
456
|
+
* when the mirror faithfully matches source.
|
|
457
|
+
*/
|
|
458
|
+
export function describePluginDrift(central, mirrorDir) {
|
|
459
|
+
if (!fs.existsSync(mirrorDir))
|
|
460
|
+
return 'mirror missing';
|
|
461
|
+
const parts = [];
|
|
462
|
+
let mManifest = null;
|
|
463
|
+
try {
|
|
464
|
+
mManifest = JSON.parse(fs.readFileSync(path.join(mirrorDir, '.claude-plugin', 'plugin.json'), 'utf-8'));
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
mManifest = null;
|
|
468
|
+
}
|
|
469
|
+
const mVer = mManifest && typeof mManifest.version === 'string' ? mManifest.version : undefined;
|
|
470
|
+
const cVer = central.manifest.version;
|
|
471
|
+
if (mVer && cVer && compareVersions(cVer, mVer) > 0)
|
|
472
|
+
parts.push(`${mVer}→${cVer}`);
|
|
473
|
+
if (mManifest && repairableManifestFields(mManifest).length > 0)
|
|
474
|
+
parts.push('invalid manifest');
|
|
475
|
+
const mirrorSkills = new Set(listPluginSkillDirs(mirrorDir));
|
|
476
|
+
const missSkills = listPluginSkillDirs(central.root).filter((s) => !mirrorSkills.has(s)).sort();
|
|
477
|
+
const mirrorCmds = new Set(listPluginCommandFiles(mirrorDir));
|
|
478
|
+
const missCmds = listPluginCommandFiles(central.root).filter((c) => !mirrorCmds.has(c)).sort();
|
|
479
|
+
if (missSkills.length)
|
|
480
|
+
parts.push(`missing skill${missSkills.length > 1 ? 's' : ''}: ${missSkills.join(', ')}`);
|
|
481
|
+
if (missCmds.length)
|
|
482
|
+
parts.push(`missing command${missCmds.length > 1 ? 's' : ''}: ${missCmds.join(', ')}`);
|
|
483
|
+
return parts.length ? parts.join(', ') : null;
|
|
484
|
+
}
|
|
485
|
+
function diffPlugins(agent, version, cwd) {
|
|
486
|
+
const versionHome = getVersionHomePath(agent, version);
|
|
487
|
+
const synced = new Set(getActuallySyncedResources(agent, version, { cwd }).plugins);
|
|
488
|
+
const rows = [];
|
|
489
|
+
const seen = new Set();
|
|
490
|
+
for (const p of discoverPlugins({ cwd })) {
|
|
491
|
+
if (seen.has(p.name))
|
|
492
|
+
continue; // dedupe across marketplaces for the readout
|
|
493
|
+
seen.add(p.name);
|
|
494
|
+
if (!synced.has(p.name)) {
|
|
495
|
+
rows.push({ kind: 'plugins', name: p.name, status: 'missing', sourcePath: p.root });
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
const mirror = pluginInstallDir(p, marketplaceSpecForName(p.marketplace), agent, versionHome);
|
|
499
|
+
const detail = describePluginDrift(p, mirror);
|
|
500
|
+
rows.push({
|
|
501
|
+
kind: 'plugins',
|
|
502
|
+
name: p.name,
|
|
503
|
+
status: detail ? 'diff' : 'ok',
|
|
504
|
+
sourcePath: p.root,
|
|
505
|
+
homePath: mirror,
|
|
506
|
+
...(detail ? { detail } : {}),
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
for (const name of synced) {
|
|
510
|
+
if (!seen.has(name))
|
|
511
|
+
rows.push({ kind: 'plugins', name, status: 'extra' });
|
|
512
|
+
}
|
|
513
|
+
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
514
|
+
}
|
|
428
515
|
function diffPromptcuts() {
|
|
429
516
|
const sourcePath = getEffectivePromptcutsPath();
|
|
430
517
|
if (!fs.existsSync(sourcePath))
|
|
@@ -432,9 +519,14 @@ function diffPromptcuts() {
|
|
|
432
519
|
return [{ kind: 'promptcuts', name: 'promptcuts.yaml', status: 'ok', sourcePath }];
|
|
433
520
|
}
|
|
434
521
|
export function diffVersionResources(agent, version, options = {}) {
|
|
435
|
-
const
|
|
522
|
+
const rawCwd = options.cwd ?? process.cwd();
|
|
523
|
+
const excludeProject = options.excludeProject ?? false;
|
|
436
524
|
const home = getVersionHomePath(agent, version);
|
|
437
525
|
const requested = new Set(options.kinds ?? ALL_KINDS);
|
|
526
|
+
// When excluding the project layer, resolve every per-cwd lookup against a
|
|
527
|
+
// neutral cwd so no `<cwd>/.agents/` is ever discovered.
|
|
528
|
+
const cwd = rawCwd;
|
|
529
|
+
const projectDir = excludeProject ? null : getProjectAgentsDir(cwd);
|
|
438
530
|
const available = getAvailableResources(cwd);
|
|
439
531
|
const synced = getActuallySyncedResources(agent, version, { cwd });
|
|
440
532
|
const empty = {
|
|
@@ -449,13 +541,13 @@ export function diffVersionResources(agent, version, options = {}) {
|
|
|
449
541
|
promptcuts: [],
|
|
450
542
|
};
|
|
451
543
|
if (requested.has('commands'))
|
|
452
|
-
empty.commands = diffCommands(agent, version, cwd);
|
|
544
|
+
empty.commands = diffCommands(agent, version, cwd, excludeProject);
|
|
453
545
|
if (requested.has('skills'))
|
|
454
|
-
empty.skills = diffSkills(agent, version, cwd);
|
|
546
|
+
empty.skills = diffSkills(agent, version, cwd, excludeProject);
|
|
455
547
|
if (requested.has('hooks'))
|
|
456
548
|
empty.hooks = diffHooks(agent, version, cwd);
|
|
457
549
|
if (requested.has('rules'))
|
|
458
|
-
empty.rules = diffRules(agent, version, cwd);
|
|
550
|
+
empty.rules = diffRules(agent, version, cwd, excludeProject);
|
|
459
551
|
if (requested.has('mcp'))
|
|
460
552
|
empty.mcp = diffPresenceOnly('mcp', available.mcp, synced.mcp);
|
|
461
553
|
if (requested.has('permissions'))
|
|
@@ -463,7 +555,7 @@ export function diffVersionResources(agent, version, options = {}) {
|
|
|
463
555
|
if (requested.has('subagents'))
|
|
464
556
|
empty.subagents = diffPresenceOnly('subagents', available.subagents, synced.subagents);
|
|
465
557
|
if (requested.has('plugins'))
|
|
466
|
-
empty.plugins =
|
|
558
|
+
empty.plugins = diffPlugins(agent, version, cwd);
|
|
467
559
|
if (requested.has('promptcuts'))
|
|
468
560
|
empty.promptcuts = diffPromptcuts();
|
|
469
561
|
let ok = 0, diff = 0, missing = 0, extra = 0;
|
|
@@ -485,7 +577,7 @@ export function diffVersionResources(agent, version, options = {}) {
|
|
|
485
577
|
home,
|
|
486
578
|
cwd,
|
|
487
579
|
layers: {
|
|
488
|
-
project:
|
|
580
|
+
project: projectDir,
|
|
489
581
|
user: getUserAgentsDir(),
|
|
490
582
|
system: getSystemAgentsDir(),
|
|
491
583
|
extras: getEnabledExtraRepos().map((e) => ({ alias: e.alias, dir: e.dir })),
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -80,6 +80,14 @@ export interface ExecOptions {
|
|
|
80
80
|
addDirs?: string[];
|
|
81
81
|
timeout?: string;
|
|
82
82
|
sessionId?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Resume the conversation named by `sessionId` using the agent's NATIVE resume
|
|
85
|
+
* form (claude `--resume`, codex `resume`) instead of the default `--session-id`
|
|
86
|
+
* create. Only set for agents where `nativeResume` returns true; other agents
|
|
87
|
+
* resume via a `/continue <id>` first message (Tier 2), which needs no flag and
|
|
88
|
+
* leaves this unset.
|
|
89
|
+
*/
|
|
90
|
+
resume?: boolean;
|
|
83
91
|
verbose?: boolean;
|
|
84
92
|
env?: Record<string, string>;
|
|
85
93
|
/**
|
|
@@ -150,6 +158,19 @@ export interface AgentCommandTemplate {
|
|
|
150
158
|
modelFlag?: string;
|
|
151
159
|
printFlags?: string[];
|
|
152
160
|
verboseFlag?: string;
|
|
161
|
+
/**
|
|
162
|
+
* How this agent natively resumes a prior conversation. Presence here is the
|
|
163
|
+
* single source of truth for `nativeResume(agent)` — agents without it fall
|
|
164
|
+
* back to the universal `/continue <id>` replay (Tier 2). Two shapes:
|
|
165
|
+
* { flag } — append `<flag> <id>` (e.g. claude `--resume <id>`)
|
|
166
|
+
* { subcommand } — replace the headless base subcommand with `<subcommand> <id>`
|
|
167
|
+
* (codex: `codex exec` -> `codex exec resume <id>`)
|
|
168
|
+
*/
|
|
169
|
+
resume?: {
|
|
170
|
+
flag: string;
|
|
171
|
+
} | {
|
|
172
|
+
subcommand: string;
|
|
173
|
+
};
|
|
153
174
|
}
|
|
154
175
|
/**
|
|
155
176
|
* CLI command templates for every supported agent.
|
|
@@ -158,6 +179,12 @@ export interface AgentCommandTemplate {
|
|
|
158
179
|
* AGENTS[agent].capabilities.modes. A test in exec.test.ts asserts this.
|
|
159
180
|
*/
|
|
160
181
|
export declare const AGENT_COMMANDS: Record<AgentId, AgentCommandTemplate>;
|
|
182
|
+
/**
|
|
183
|
+
* Whether `agent` has a native resume form (Tier 1). Derived solely from the
|
|
184
|
+
* command template's `resume` field — the single source of truth. Agents that
|
|
185
|
+
* return false resume via the universal Tier-2 `/continue` replay instead.
|
|
186
|
+
*/
|
|
187
|
+
export declare function nativeResume(agent: AgentId): boolean;
|
|
161
188
|
/** Assemble the full CLI argument array for an agent invocation. */
|
|
162
189
|
export declare function buildExecCommand(options: ExecOptions): string[];
|
|
163
190
|
/** Spawn an agent and return its exit code. Convenience wrapper over spawnAgent. */
|
package/dist/lib/exec.js
CHANGED
|
@@ -261,10 +261,12 @@ export const AGENT_COMMANDS = {
|
|
|
261
261
|
modelFlag: '--model',
|
|
262
262
|
printFlags: ['--print'],
|
|
263
263
|
verboseFlag: '--verbose',
|
|
264
|
+
resume: { flag: '--resume' },
|
|
264
265
|
},
|
|
265
266
|
codex: {
|
|
266
267
|
base: ['codex', 'exec'],
|
|
267
268
|
promptFlag: 'positional',
|
|
269
|
+
resume: { subcommand: 'resume' },
|
|
268
270
|
modeFlags: {
|
|
269
271
|
// NOTE: codex has no read-only mode in --sandbox; 'plan' here means
|
|
270
272
|
// "workspace-write but no auto-approval" — closer to plan-as-restraint.
|
|
@@ -302,6 +304,9 @@ export const AGENT_COMMANDS = {
|
|
|
302
304
|
opencode: {
|
|
303
305
|
base: ['opencode', 'run'],
|
|
304
306
|
promptFlag: 'positional',
|
|
307
|
+
// opencode's native resume is `opencode --session <id>` (NOT under `run`), so
|
|
308
|
+
// it does not compose with this headless `run` base. Until that's verified on
|
|
309
|
+
// a box with opencode installed, opencode resumes via Tier-2 `/continue`.
|
|
305
310
|
modeFlags: {
|
|
306
311
|
plan: ['--agent', 'plan'],
|
|
307
312
|
edit: ['--agent', 'build'],
|
|
@@ -436,6 +441,14 @@ export const AGENT_COMMANDS = {
|
|
|
436
441
|
modelFlag: '-m',
|
|
437
442
|
},
|
|
438
443
|
};
|
|
444
|
+
/**
|
|
445
|
+
* Whether `agent` has a native resume form (Tier 1). Derived solely from the
|
|
446
|
+
* command template's `resume` field — the single source of truth. Agents that
|
|
447
|
+
* return false resume via the universal Tier-2 `/continue` replay instead.
|
|
448
|
+
*/
|
|
449
|
+
export function nativeResume(agent) {
|
|
450
|
+
return AGENT_COMMANDS[agent]?.resume !== undefined;
|
|
451
|
+
}
|
|
439
452
|
/** Assemble the full CLI argument array for an agent invocation. */
|
|
440
453
|
export function buildExecCommand(options) {
|
|
441
454
|
const template = AGENT_COMMANDS[options.agent];
|
|
@@ -453,30 +466,36 @@ export function buildExecCommand(options) {
|
|
|
453
466
|
cmd.splice(1, 1);
|
|
454
467
|
}
|
|
455
468
|
}
|
|
469
|
+
// Native resume with a `{ subcommand }` shape (codex) appends the resume verb
|
|
470
|
+
// to the base: `codex exec` -> `codex exec resume` (headless) and, after the
|
|
471
|
+
// interactive drop above, `codex` -> `codex resume` (TUI). The session id is
|
|
472
|
+
// pushed later as the first positional (before any prompt). `{ flag }` agents
|
|
473
|
+
// (claude) need no base change — the flag is appended with the id below.
|
|
474
|
+
const resumeSpec = options.resume ? template.resume : undefined;
|
|
475
|
+
if (resumeSpec && 'subcommand' in resumeSpec) {
|
|
476
|
+
cmd.push(resumeSpec.subcommand);
|
|
477
|
+
}
|
|
456
478
|
// Use versioned alias if a specific version was requested (e.g., claude@2.1.98).
|
|
457
479
|
// Resolve to the absolute path of the shim so spawn doesn't depend on PATH —
|
|
458
480
|
// on Linux installs where the shims dir isn't on PATH, spawning the bare
|
|
459
481
|
// versioned name fails with ENOENT even though `agents view` shows the agent.
|
|
460
482
|
//
|
|
461
|
-
// On Windows
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
483
|
+
// On Windows the shims dir holds a `.cmd` companion next to the bash alias
|
|
484
|
+
// (see createVersionedAlias); prefer it so spawn() can launch it (the bash
|
|
485
|
+
// script is not directly executable by cmd.exe). When no shim exists on disk
|
|
486
|
+
// we fall back to the bare versioned name, which spawnAgent() resolves via
|
|
487
|
+
// PATH (+ PATHEXT/shell on Windows).
|
|
465
488
|
if (options.version && cmd.length > 0) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
cmd[0] = binaryPath;
|
|
474
|
-
}
|
|
489
|
+
const versionedName = `${cmd[0]}@${options.version}`;
|
|
490
|
+
const absPath = path.join(getShimsDir(), versionedName);
|
|
491
|
+
if (process.platform === 'win32' && fs.existsSync(absPath + '.cmd')) {
|
|
492
|
+
cmd[0] = absPath + '.cmd';
|
|
493
|
+
}
|
|
494
|
+
else if (fs.existsSync(absPath)) {
|
|
495
|
+
cmd[0] = absPath;
|
|
475
496
|
}
|
|
476
497
|
else {
|
|
477
|
-
|
|
478
|
-
const absPath = path.join(getShimsDir(), versionedName);
|
|
479
|
-
cmd[0] = fs.existsSync(absPath) ? absPath : versionedName;
|
|
498
|
+
cmd[0] = versionedName;
|
|
480
499
|
}
|
|
481
500
|
}
|
|
482
501
|
// Add reasoning effort flags (before mode flags for codex -c positioning)
|
|
@@ -505,15 +524,39 @@ export function buildExecCommand(options) {
|
|
|
505
524
|
// AGENT_COMMANDS.modeFlags drifted apart. Tests assert they agree.
|
|
506
525
|
throw new Error(`Internal error: ${options.agent} declares '${resolvedMode}' in capabilities.modes but has no entry in AGENT_COMMANDS.modeFlags.${resolvedMode}.`);
|
|
507
526
|
}
|
|
508
|
-
|
|
527
|
+
if (resumeSpec && 'subcommand' in resumeSpec) {
|
|
528
|
+
// codex `exec resume` / `resume` does NOT accept `--sandbox <mode>` (only the
|
|
529
|
+
// bypass flag, verified against `codex exec resume --help`). So the standard
|
|
530
|
+
// codex modeFlags can't be reused on resume. A non-plan HEADLESS resume needs
|
|
531
|
+
// the bypass or it stalls on approval prompts; plan and interactive resume
|
|
532
|
+
// inherit codex's default sandbox and pass no flag.
|
|
533
|
+
if (!interactive && resolvedMode !== 'plan') {
|
|
534
|
+
cmd.push('--dangerously-bypass-approvals-and-sandbox');
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
cmd.push(...modeFlags);
|
|
539
|
+
}
|
|
509
540
|
// Add print/headless flags only when a prompt is provided. Without a prompt
|
|
510
541
|
// the caller wants an interactive REPL -- passing --print would immediately
|
|
511
542
|
// wait on stdin and never render the TUI.
|
|
512
543
|
if (!interactive && options.headless && template.printFlags) {
|
|
513
544
|
cmd.push(...template.printFlags);
|
|
514
545
|
}
|
|
515
|
-
//
|
|
516
|
-
|
|
546
|
+
// Resume vs. create. With `resume`, emit the agent's NATIVE resume reference:
|
|
547
|
+
// `{ flag }` agents append `<flag> <id>` (claude `--resume <id>`); `{ subcommand }`
|
|
548
|
+
// agents (codex) already pushed the verb above, so the id is the first
|
|
549
|
+
// positional here — placed before the prompt positional appended later. Without
|
|
550
|
+
// `resume`, the legacy claude-only `--session-id` CREATES a session with that id.
|
|
551
|
+
if (options.resume && options.sessionId && resumeSpec) {
|
|
552
|
+
if ('flag' in resumeSpec) {
|
|
553
|
+
cmd.push(resumeSpec.flag, options.sessionId);
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
cmd.push(options.sessionId);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
else if (options.sessionId && options.agent === 'claude') {
|
|
517
560
|
cmd.push('--session-id', options.sessionId);
|
|
518
561
|
}
|
|
519
562
|
// Add model (only if explicitly provided by user)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource heal engine — close the gap between what DotAgents repos DEFINE and
|
|
3
|
+
* what is actually present/valid in each installed agent home.
|
|
4
|
+
*
|
|
5
|
+
* Powers two callers:
|
|
6
|
+
* - `agents doctor --fix` — explicit, operator-driven. Mode 'full': fills
|
|
7
|
+
* missing, overwrites drifted content, and refreshes stale plugins even when
|
|
8
|
+
* the baseline is unknown (the operator asked for it).
|
|
9
|
+
* - the routines daemon's periodic safety check — Mode 'safe': fixes only the
|
|
10
|
+
* unambiguous gaps (missing resources, Claude-invalid plugin manifests, and
|
|
11
|
+
* provably-unmodified stale plugins). Drift and risky refreshes are reported,
|
|
12
|
+
* never clobbered.
|
|
13
|
+
*
|
|
14
|
+
* Built on the LIVE-home diff (`diffVersionResources`) — NOT the staleness
|
|
15
|
+
* manifest. `isStale()` only compares the last-synced manifest against the
|
|
16
|
+
* sources, so home-side rot (a deleted, corrupted, or Claude-rejected file in a
|
|
17
|
+
* version home whose source never changed) is invisible to it and to the sync
|
|
18
|
+
* fast-guard. The diff reads the actual home, so heal catches exactly that class
|
|
19
|
+
* of drift — the kind that silently broke the `code` plugin on a non-default
|
|
20
|
+
* Claude version.
|
|
21
|
+
*
|
|
22
|
+
* Heal FILLS and FIXES; it never deletes. Orphan/extra removal stays the job of
|
|
23
|
+
* `agents prune cleanup`, so a heal pass can never lose work.
|
|
24
|
+
*/
|
|
25
|
+
import type { AgentId } from './types.js';
|
|
26
|
+
import { type DoctorKind, type DiffStatus } from './doctor-diff.js';
|
|
27
|
+
export interface HealedResource {
|
|
28
|
+
kind: DoctorKind;
|
|
29
|
+
name: string;
|
|
30
|
+
/** Why it was healed: 'missing' (filled) or 'diff' (overwritten / re-pushed). */
|
|
31
|
+
was: DiffStatus;
|
|
32
|
+
}
|
|
33
|
+
export interface SkippedResource {
|
|
34
|
+
kind: DoctorKind;
|
|
35
|
+
name: string;
|
|
36
|
+
/** 'drift': hand-edited content left untouched in 'safe' mode.
|
|
37
|
+
* 'unreconcilable': heal wrote it but the diff still flags it — a source/home
|
|
38
|
+
* asymmetry the writer can't satisfy (e.g. a hook sidecar the installer omits),
|
|
39
|
+
* surfaced honestly instead of "fixed" on every pass. */
|
|
40
|
+
reason: 'drift' | 'unreconcilable';
|
|
41
|
+
}
|
|
42
|
+
export interface VersionHealResult {
|
|
43
|
+
agent: AgentId;
|
|
44
|
+
version: string;
|
|
45
|
+
healed: HealedResource[];
|
|
46
|
+
skipped: SkippedResource[];
|
|
47
|
+
}
|
|
48
|
+
export interface ManifestRepairResult {
|
|
49
|
+
plugin: string;
|
|
50
|
+
/** Bare-name fields stripped from the source plugin.json (e.g. ["skills"]). */
|
|
51
|
+
droppedFields: string[];
|
|
52
|
+
}
|
|
53
|
+
export interface PluginRefreshResult {
|
|
54
|
+
plugin: string;
|
|
55
|
+
from: string;
|
|
56
|
+
to: string;
|
|
57
|
+
}
|
|
58
|
+
export interface PluginRefreshSkip {
|
|
59
|
+
plugin: string;
|
|
60
|
+
from: string;
|
|
61
|
+
upstream: string;
|
|
62
|
+
/** 'modified': central diverged from baseline. 'no-baseline': pre-tracking install. */
|
|
63
|
+
reason: 'modified' | 'no-baseline';
|
|
64
|
+
}
|
|
65
|
+
export interface HealResult {
|
|
66
|
+
versions: VersionHealResult[];
|
|
67
|
+
repairedManifests: ManifestRepairResult[];
|
|
68
|
+
refreshedPlugins: PluginRefreshResult[];
|
|
69
|
+
skippedPlugins: PluginRefreshSkip[];
|
|
70
|
+
}
|
|
71
|
+
export interface HealOptions {
|
|
72
|
+
/** 'full' (doctor --fix): fix drift + refresh unknown-baseline plugins.
|
|
73
|
+
* 'safe' (daemon): missing + invalid-manifest + unmodified refresh only. */
|
|
74
|
+
mode: 'full' | 'safe';
|
|
75
|
+
/** Resolution cwd. Defaults to the home dir so no project layer is ever
|
|
76
|
+
* resolved — heal targets the GLOBAL install, never a project. Tests override. */
|
|
77
|
+
cwd?: string;
|
|
78
|
+
/** Scope to one agent; omit to heal every installed agent. */
|
|
79
|
+
agent?: AgentId;
|
|
80
|
+
/** Scope to specific versions of `agent`; omit for all installed versions. */
|
|
81
|
+
versions?: string[];
|
|
82
|
+
/** Compute the plan without writing anything. */
|
|
83
|
+
dryRun?: boolean;
|
|
84
|
+
}
|
|
85
|
+
/** True when a heal pass made (or would make) any change at all. */
|
|
86
|
+
export declare function healChangedAnything(r: HealResult): boolean;
|
|
87
|
+
/** One-line summary of a heal pass for daemon logs. */
|
|
88
|
+
export declare function summarizeHeal(r: HealResult): string;
|
|
89
|
+
/**
|
|
90
|
+
* Fire a native desktop notification when a background heal did something
|
|
91
|
+
* noteworthy. Best-effort — missing `osascript`/`notify-send` or no display is
|
|
92
|
+
* swallowed. Silent when the pass auto-fixed everything and nothing needs the
|
|
93
|
+
* operator (no point pinging them for routine self-healing).
|
|
94
|
+
*/
|
|
95
|
+
export declare function notifyHeal(r: HealResult): void;
|
|
96
|
+
/**
|
|
97
|
+
* Strip Claude-invalid bare-name `skills`/`commands` fields from every central
|
|
98
|
+
* plugin's SOURCE plugin.json. Unambiguously safe (Claude auto-discovers both
|
|
99
|
+
* from their directories) and the precondition for those plugins loading at all.
|
|
100
|
+
*/
|
|
101
|
+
export declare function repairCentralPluginManifests(dryRun?: boolean): ManifestRepairResult[];
|
|
102
|
+
/**
|
|
103
|
+
* Run a heal pass. Repairs the central plugin layer once (manifest + stale
|
|
104
|
+
* refresh), then reconciles every targeted (agent, version) home against its
|
|
105
|
+
* live diff. Returns a full account of what changed (or would, under dryRun).
|
|
106
|
+
*/
|
|
107
|
+
export declare function heal(opts: HealOptions): Promise<HealResult>;
|