@phnx-labs/agents-cli 1.20.12 → 1.20.14

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 (67) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +3 -0
  3. package/dist/commands/computer-actions.d.ts +3 -0
  4. package/dist/commands/computer-actions.js +16 -0
  5. package/dist/commands/doctor.js +51 -7
  6. package/dist/commands/exec.js +25 -4
  7. package/dist/commands/import.js +17 -6
  8. package/dist/commands/inspect.d.ts +28 -1
  9. package/dist/commands/inspect.js +330 -47
  10. package/dist/commands/mcp.js +3 -3
  11. package/dist/commands/plugins.d.ts +2 -0
  12. package/dist/commands/plugins.js +69 -26
  13. package/dist/commands/prune.js +8 -5
  14. package/dist/commands/sync.js +1 -1
  15. package/dist/commands/teams.js +1 -0
  16. package/dist/commands/trash.d.ts +11 -0
  17. package/dist/commands/trash.js +57 -41
  18. package/dist/commands/versions.js +68 -20
  19. package/dist/commands/view.d.ts +1 -0
  20. package/dist/commands/view.js +56 -12
  21. package/dist/commands/wallet.d.ts +14 -0
  22. package/dist/commands/wallet.js +199 -0
  23. package/dist/index.js +4 -1
  24. package/dist/lib/agents.js +70 -22
  25. package/dist/lib/browser/ipc.d.ts +7 -0
  26. package/dist/lib/browser/ipc.js +43 -27
  27. package/dist/lib/capabilities.js +7 -1
  28. package/dist/lib/command-skills.d.ts +1 -0
  29. package/dist/lib/command-skills.js +23 -7
  30. package/dist/lib/exec.d.ts +32 -1
  31. package/dist/lib/exec.js +79 -7
  32. package/dist/lib/hooks.d.ts +21 -1
  33. package/dist/lib/hooks.js +69 -7
  34. package/dist/lib/mcp.js +33 -0
  35. package/dist/lib/models.js +5 -0
  36. package/dist/lib/picker.d.ts +2 -0
  37. package/dist/lib/picker.js +96 -6
  38. package/dist/lib/platform/index.d.ts +1 -0
  39. package/dist/lib/platform/index.js +1 -0
  40. package/dist/lib/platform/winpath.d.ts +35 -0
  41. package/dist/lib/platform/winpath.js +86 -0
  42. package/dist/lib/plugins.d.ts +24 -0
  43. package/dist/lib/plugins.js +37 -2
  44. package/dist/lib/project-launch.js +110 -5
  45. package/dist/lib/registry.js +15 -2
  46. package/dist/lib/rotate.d.ts +7 -0
  47. package/dist/lib/rotate.js +17 -7
  48. package/dist/lib/runner.js +14 -0
  49. package/dist/lib/sandbox.js +5 -2
  50. package/dist/lib/settings-manifest.d.ts +39 -0
  51. package/dist/lib/settings-manifest.js +163 -0
  52. package/dist/lib/shims.d.ts +1 -1
  53. package/dist/lib/shims.js +16 -31
  54. package/dist/lib/staleness/detectors/subagents.js +16 -0
  55. package/dist/lib/staleness/writers/subagents.js +11 -3
  56. package/dist/lib/subagents.d.ts +9 -0
  57. package/dist/lib/subagents.js +33 -0
  58. package/dist/lib/teams/agents.js +1 -1
  59. package/dist/lib/teams/parsers.d.ts +1 -1
  60. package/dist/lib/teams/parsers.js +6 -0
  61. package/dist/lib/types.d.ts +1 -1
  62. package/dist/lib/versions.d.ts +15 -3
  63. package/dist/lib/versions.js +88 -19
  64. package/dist/lib/wallet/index.d.ts +78 -0
  65. package/dist/lib/wallet/index.js +253 -0
  66. package/package.json +3 -3
  67. package/scripts/postinstall.js +35 -7
@@ -12,7 +12,7 @@ import { homeDir } from '../lib/platform/index.js';
12
12
  import { input } from '@inquirer/prompts';
13
13
  import { agentLabel } from '../lib/agents.js';
14
14
  import { capableAgents, isCapable } from '../lib/capabilities.js';
15
- import { discoverPlugins, getPlugin, pluginSupportsAgent, removePluginFromVersion, isPluginSynced, installPlugin, updatePlugin, loadUserConfig, saveUserConfig, checkPluginDependencies, hasPluginExecSurfaces, inspectPluginCapabilities, pluginCapabilityLabels, parseInstallSpec, syncPluginToVersion, } from '../lib/plugins.js';
15
+ import { discoverPlugins, getPlugin, pluginSupportsAgent, removePluginFromVersion, isPluginSynced, installPlugin, updatePlugin, loadUserConfig, saveUserConfig, checkPluginDependencies, hasPluginExecSurfaces, inspectPluginCapabilities, pluginCapabilityLabels, parseInstallSpec, syncPluginToVersion, pluginResourceGroups, } from '../lib/plugins.js';
16
16
  import { listInstalledVersions, syncResourcesToVersion, getGlobalDefault, getVersionHomePath, } from '../lib/versions.js';
17
17
  import { isPromptCancelled, isInteractiveTerminal, requireDestructiveArg, requireInteractiveSelection, promptRemovalTargets, } from './utils.js';
18
18
  import { itemPicker } from '../lib/picker.js';
@@ -276,14 +276,17 @@ Examples:
276
276
  // agents plugins sync <name> [agent]
277
277
  pluginsCmd
278
278
  .command('sync <name> [agent]')
279
- .description('Apply a plugin to the default version of an agent (or all supported agents if none specified)')
279
+ .description('Apply a plugin to an agent. Syncs every installed version (pass agent@version to target one).')
280
280
  .option('--allow-exec-surfaces', 'Enable the plugin even when it ships hooks/, .mcp.json, bin/, scripts/, settings.json, or permissions/')
281
281
  .addHelpText('after', `
282
282
  Examples:
283
- # Sync a plugin to a specific agent (default version)
283
+ # Sync a plugin to every installed version of an agent
284
284
  agents plugins sync rush-toolkit claude
285
285
 
286
- # Sync to all supported agents
286
+ # Sync to one specific version (parity with 'agents sync')
287
+ agents plugins sync rush-toolkit claude@2.1.142
288
+
289
+ # Sync to all supported agents (every installed version of each)
287
290
  agents plugins sync rush-toolkit
288
291
 
289
292
  # Re-affirm consent for a hooks-bearing plugin
@@ -295,12 +298,22 @@ Examples:
295
298
  console.log(chalk.red(`Plugin '${name}' not found`));
296
299
  process.exit(1);
297
300
  }
301
+ // Accept the same "agent@version" form as `agents sync`. Splitting here
302
+ // also means an unknown spec is reported cleanly rather than crashing
303
+ // isCapable() with a bare "claude@2.1.168".
304
+ let versionArg;
305
+ let agentName = agentArg;
306
+ if (agentArg && agentArg.includes('@')) {
307
+ const at = agentArg.lastIndexOf('@');
308
+ agentName = agentArg.slice(0, at);
309
+ versionArg = agentArg.slice(at + 1);
310
+ }
298
311
  // Determine target agents
299
312
  let targetAgents;
300
- if (agentArg) {
301
- const agentId = agentArg;
313
+ if (agentName) {
314
+ const agentId = agentName;
302
315
  if (!isCapable(agentId, 'plugins')) {
303
- console.log(chalk.red(`Agent '${agentArg}' does not support plugins`));
316
+ console.log(chalk.red(`Agent '${agentName}' does not support plugins`));
304
317
  process.exit(1);
305
318
  }
306
319
  if (!pluginSupportsAgent(plugin, agentId)) {
@@ -310,6 +323,10 @@ Examples:
310
323
  targetAgents = [agentId];
311
324
  }
312
325
  else {
326
+ if (versionArg) {
327
+ console.log(chalk.red(`A version (@${versionArg}) requires naming the agent, e.g. claude@${versionArg}`));
328
+ process.exit(1);
329
+ }
313
330
  targetAgents = capableAgents('plugins').filter(a => pluginSupportsAgent(plugin, a));
314
331
  }
315
332
  const allowExec = options.allowExecSurfaces === true;
@@ -317,8 +334,21 @@ Examples:
317
334
  const versions = listInstalledVersions(agentId);
318
335
  if (versions.length === 0)
319
336
  continue;
320
- const defaultVer = getGlobalDefault(agentId);
321
- const targetVersions = defaultVer ? [defaultVer] : [versions[versions.length - 1]];
337
+ // Default to EVERY installed version. The previous behaviour synced only
338
+ // the global default, which silently skipped non-default versions used
339
+ // by balanced rotation -- so a rotated version would lack the plugin's
340
+ // slash commands. An explicit agent@version narrows back to one.
341
+ let targetVersions;
342
+ if (versionArg) {
343
+ if (!versions.includes(versionArg)) {
344
+ console.log(chalk.red(`${agentLabel(agentId)} has no installed version ${versionArg} (installed: ${versions.join(', ')})`));
345
+ process.exit(1);
346
+ }
347
+ targetVersions = [versionArg];
348
+ }
349
+ else {
350
+ targetVersions = versions;
351
+ }
322
352
  for (const version of targetVersions) {
323
353
  const didSync = allowExec
324
354
  ? syncPluginToVersion(plugin, agentId, getVersionHomePath(agentId, version), { allowExecSurfaces: true, version }).success
@@ -711,6 +741,32 @@ function buildPluginRows(plugins) {
711
741
  });
712
742
  return rows;
713
743
  }
744
+ /** Per-category color for a plugin resource breakdown (shared with `agents inspect`). */
745
+ export const PLUGIN_GROUP_COLORS = {
746
+ skills: chalk.cyan,
747
+ commands: chalk.cyan,
748
+ subagents: chalk.magenta,
749
+ hooks: chalk.yellow,
750
+ mcp: chalk.green,
751
+ lsp: chalk.green,
752
+ monitors: chalk.blue,
753
+ bin: chalk.white,
754
+ scripts: chalk.white,
755
+ settings: chalk.gray,
756
+ };
757
+ /** Human-readable section header per category, used by the picker detail pane. */
758
+ const PLUGIN_GROUP_TITLES = {
759
+ skills: 'Skills',
760
+ commands: 'Commands',
761
+ subagents: 'Subagents',
762
+ hooks: 'Hooks',
763
+ mcp: 'MCP Servers',
764
+ lsp: 'LSP Servers',
765
+ monitors: 'Monitors',
766
+ bin: 'Bin',
767
+ scripts: 'Scripts',
768
+ settings: 'Settings',
769
+ };
714
770
  /** Build the multi-line detail pane shown when a plugin is selected in the picker. */
715
771
  function formatPluginDetail(plugin, targets) {
716
772
  const lines = [];
@@ -728,24 +784,11 @@ function formatPluginDetail(plugin, targets) {
728
784
  lines.push(' ' + chalk.gray('Supports: ') + supported.join(chalk.gray(' · ')));
729
785
  }
730
786
  lines.push(' ' + chalk.gray(formatPath(plugin.root)));
731
- const section = (label, items, colorFn) => {
732
- if (items.length === 0)
733
- return;
787
+ for (const group of pluginResourceGroups(plugin)) {
788
+ const colorFn = PLUGIN_GROUP_COLORS[group.label] ?? chalk.white;
734
789
  lines.push('');
735
- lines.push(chalk.bold(` ${label}`));
736
- lines.push(' ' + items.map(colorFn).join(chalk.gray(', ')));
737
- };
738
- section('Skills', plugin.skills.map((s) => `/${plugin.name}:${s}`), chalk.cyan);
739
- section('Commands', plugin.commands.map((c) => `/${plugin.name}:${c}`), chalk.cyan);
740
- section('Subagents', plugin.agentDefs, chalk.magenta);
741
- section('Hooks', plugin.hooks, chalk.yellow);
742
- section('MCP Servers', plugin.mcpServers, chalk.green);
743
- section('LSP Servers', plugin.lspServers, chalk.green);
744
- section('Monitors', plugin.monitors, chalk.blue);
745
- section('Bin', plugin.bin, chalk.white);
746
- section('Scripts', plugin.scripts, chalk.white);
747
- if (plugin.hasSettings) {
748
- section('Settings', ['settings.json'], chalk.gray);
790
+ lines.push(chalk.bold(` ${PLUGIN_GROUP_TITLES[group.label] ?? group.label}`));
791
+ lines.push(' ' + group.items.map((s) => colorFn(s)).join(chalk.gray(', ')));
749
792
  }
750
793
  if (targets.length > 0) {
751
794
  lines.push('');
@@ -26,7 +26,7 @@ import chalk from 'chalk';
26
26
  import { confirm } from '@inquirer/prompts';
27
27
  import { diffVersionCommands, iterCommandsCapableVersions, removeCommandFromVersion, } from '../lib/commands.js';
28
28
  import { diffVersionSkills, iterSkillsCapableVersions, removeSkillFromVersion, } from '../lib/skills.js';
29
- import { diffVersionHooks, iterHooksCapableVersions, removeHookFromVersion, } from '../lib/hooks.js';
29
+ import { listUnmanagedHooksInVersionHome, iterHooksCapableVersions, removeHookFromVersion, } from '../lib/hooks.js';
30
30
  import { diffVersionPlugins, iterPluginsCapableVersions, removePluginSkillFromVersion, } from '../lib/plugins.js';
31
31
  import { diffVersionSubagents, iterSubagentsCapableVersions, removeSubagentFromVersion, } from '../lib/subagents.js';
32
32
  import { getGlobalDefault } from '../lib/versions.js';
@@ -64,9 +64,12 @@ function collectOrphans(types, all) {
64
64
  }
65
65
  if (types.includes('hooks')) {
66
66
  for (const { agent, version } of scopePairs(iterHooksCapableVersions(), all)) {
67
- const diff = diffVersionHooks(agent, version);
68
- if (diff.orphans.length > 0) {
69
- groups.push({ type: 'hooks', agent, version, orphans: diff.orphans });
67
+ // Orphan hooks = scripts present in the version home that no
68
+ // agents.yaml/hooks.yaml entry registers, so they never fire. Same
69
+ // definition the doctor overview reports.
70
+ const orphans = listUnmanagedHooksInVersionHome(agent, version);
71
+ if (orphans.length > 0) {
72
+ groups.push({ type: 'hooks', agent, version, orphans });
70
73
  }
71
74
  }
72
75
  }
@@ -210,7 +213,7 @@ async function runOrphanPrune(resourceTypes, options) {
210
213
  return;
211
214
  }
212
215
  const total = groups.reduce((n, g) => n + g.orphans.length, 0);
213
- console.log(chalk.bold('Orphans (in version home, not in any source)\n'));
216
+ console.log(chalk.bold('Orphans (in version home, unmanaged by any source)\n'));
214
217
  for (const g of groups) {
215
218
  const label = `${g.type} · ${g.agent}@${g.version}`;
216
219
  console.log(` ${chalk.cyan(label)} ${g.orphans.join(', ')}`);
@@ -61,7 +61,7 @@ async function runSync(agentSpec, opts) {
61
61
  return;
62
62
  }
63
63
  agentId = parsed.agent;
64
- if (parsed.version !== 'latest')
64
+ if (parsed.version !== 'latest' && parsed.version !== 'oldest')
65
65
  version = parsed.version;
66
66
  }
67
67
  if (opts.agent) {
@@ -25,6 +25,7 @@ const AGENT_NAMES = {
25
25
  grok: 'Grok',
26
26
  antigravity: 'Antigravity',
27
27
  kimi: 'Kimi',
28
+ droid: 'Droid',
28
29
  };
29
30
  const VALID_AGENTS = Object.keys(AGENT_NAMES);
30
31
  // 'full' kept as historical alias for 'skip'; normalized to 'skip' downstream.
@@ -7,4 +7,15 @@
7
7
  * `rm -rf ~/.agents/.history/trash/` removes bytes from disk.
8
8
  */
9
9
  import type { Command } from 'commander';
10
+ /**
11
+ * Restore a soft-deleted version back into ~/.agents/.history/versions/.
12
+ * Shared by `agents trash restore` and the top-level `agents restore` alias.
13
+ * Exits the process with a non-zero code on any failure.
14
+ */
15
+ export declare function restoreVersion(target: string): void;
16
+ /**
17
+ * Register the top-level `agents restore` command — a shorthand for
18
+ * `agents trash restore` so users can undo a `remove`/`prune` directly.
19
+ */
20
+ export declare function registerRestoreCommand(program: Command): void;
10
21
  export declare function registerTrashCommands(program: Command): void;
@@ -110,6 +110,61 @@ function humanSize(bytes) {
110
110
  return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
111
111
  return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
112
112
  }
113
+ /**
114
+ * Restore a soft-deleted version back into ~/.agents/.history/versions/.
115
+ * Shared by `agents trash restore` and the top-level `agents restore` alias.
116
+ * Exits the process with a non-zero code on any failure.
117
+ */
118
+ export function restoreVersion(target) {
119
+ const parsed = parseAgentVersion(target);
120
+ if (!parsed) {
121
+ console.error(chalk.red(`Expected <agent>@<version>, got: ${target}`));
122
+ process.exit(1);
123
+ }
124
+ const { agent, version } = parsed;
125
+ const entries = listTrashEntries(agent);
126
+ const entry = pickLatest(entries, agent, version);
127
+ if (!entry) {
128
+ console.error(chalk.red(`No trashed copy found for ${agent}@${version}`));
129
+ console.error(chalk.gray('Run `agents trash list` to see what exists.'));
130
+ process.exit(1);
131
+ }
132
+ const dest = getVersionDir(agent, version);
133
+ if (fs.existsSync(dest)) {
134
+ console.error(chalk.red(`Cannot restore: ${dest} already exists.`));
135
+ console.error(chalk.gray('Move or remove the existing dir first, then re-run restore.'));
136
+ process.exit(1);
137
+ }
138
+ try {
139
+ fs.mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
140
+ fs.renameSync(entry.trashPath, dest);
141
+ }
142
+ catch (err) {
143
+ console.error(chalk.red(`Restore failed: ${err.message}`));
144
+ process.exit(1);
145
+ }
146
+ // Best-effort cleanup of empty stamp/version parents in trash.
147
+ try {
148
+ const verDir = path.dirname(entry.trashPath);
149
+ if (fs.readdirSync(verDir).length === 0)
150
+ fs.rmdirSync(verDir);
151
+ const agentDir = path.dirname(verDir);
152
+ if (fs.readdirSync(agentDir).length === 0)
153
+ fs.rmdirSync(agentDir);
154
+ }
155
+ catch { /* best-effort */ }
156
+ console.log(chalk.green(`Restored ${agentLabel(agent)}@${version} to ${dest}`));
157
+ }
158
+ /**
159
+ * Register the top-level `agents restore` command — a shorthand for
160
+ * `agents trash restore` so users can undo a `remove`/`prune` directly.
161
+ */
162
+ export function registerRestoreCommand(program) {
163
+ program
164
+ .command('restore <target>')
165
+ .description('Restore a soft-deleted agent version (e.g. "codex@0.141.0") removed via prune/remove')
166
+ .action((target) => restoreVersion(target));
167
+ }
113
168
  export function registerTrashCommands(program) {
114
169
  const trash = program
115
170
  .command('trash')
@@ -139,49 +194,10 @@ export function registerTrashCommands(program) {
139
194
  chalk.gray(`${e.stamp} ${size} ${e.trashPath}`));
140
195
  }
141
196
  console.log();
142
- console.log(chalk.gray('Restore with: agents trash restore <agent>@<version>'));
197
+ console.log(chalk.gray('Restore with: agents restore <agent>@<version>'));
143
198
  });
144
199
  trash
145
200
  .command('restore <target>')
146
201
  .description('Restore a soft-deleted version (e.g. "claude@2.1.110") back to ~/.agents/.history/versions/')
147
- .action((target) => {
148
- const parsed = parseAgentVersion(target);
149
- if (!parsed) {
150
- console.error(chalk.red(`Expected <agent>@<version>, got: ${target}`));
151
- process.exit(1);
152
- }
153
- const { agent, version } = parsed;
154
- const entries = listTrashEntries(agent);
155
- const entry = pickLatest(entries, agent, version);
156
- if (!entry) {
157
- console.error(chalk.red(`No trashed copy found for ${agent}@${version}`));
158
- console.error(chalk.gray('Run `agents trash list` to see what exists.'));
159
- process.exit(1);
160
- }
161
- const dest = getVersionDir(agent, version);
162
- if (fs.existsSync(dest)) {
163
- console.error(chalk.red(`Cannot restore: ${dest} already exists.`));
164
- console.error(chalk.gray('Move or remove the existing dir first, then re-run restore.'));
165
- process.exit(1);
166
- }
167
- try {
168
- fs.mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
169
- fs.renameSync(entry.trashPath, dest);
170
- }
171
- catch (err) {
172
- console.error(chalk.red(`Restore failed: ${err.message}`));
173
- process.exit(1);
174
- }
175
- // Best-effort cleanup of empty stamp/version parents in trash.
176
- try {
177
- const verDir = path.dirname(entry.trashPath);
178
- if (fs.readdirSync(verDir).length === 0)
179
- fs.rmdirSync(verDir);
180
- const agentDir = path.dirname(verDir);
181
- if (fs.readdirSync(agentDir).length === 0)
182
- fs.rmdirSync(agentDir);
183
- }
184
- catch { /* best-effort */ }
185
- console.log(chalk.green(`Restored ${agentLabel(agent)}@${version} to ${dest}`));
186
- });
202
+ .action((target) => restoreVersion(target));
187
203
  }
@@ -7,7 +7,8 @@ import { AGENTS, ALL_AGENT_IDS, getAccountEmail, getAccountInfo, agentLabel, } f
7
7
  import { formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
8
8
  import { viewAction } from './view.js';
9
9
  import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
10
- import { installVersion, removeVersion, listInstalledVersions, isVersionInstalled, isLatestInstalled, getGlobalDefault, setGlobalDefault, getVersionHomePath, getVersionDir, syncResourcesToVersion, parseAgentSpec, promptResourceSelection, promptNewResourceSelection, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, printTrashFooter, } from '../lib/versions.js';
10
+ import { installVersion, removeVersion, listInstalledVersions, isVersionInstalled, isLatestInstalled, isOldestInstalled, getGlobalDefault, setGlobalDefault, getVersionHomePath, getVersionDir, syncResourcesToVersion, parseAgentSpec, promptResourceSelection, promptNewResourceSelection, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, printTrashFooter, } from '../lib/versions.js';
11
+ import { carryForwardSettings } from '../lib/settings-manifest.js';
11
12
  import { createShim, createVersionedAlias, removeShim, shimExists, getShimsDir, getShimPath, getPathShadowingExecutable, isShimsInPath, getPathSetupInstructions, addShimsToPath, switchConfigSymlink, switchHomeFileSymlinks, } from '../lib/shims.js';
12
13
  import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
13
14
  import { tryAutoPull } from '../lib/git.js';
@@ -28,17 +29,6 @@ function fixSessionFilePaths(agent, version, oldVersionDir) {
28
29
  const trashPath = path.join(trashAgentDir, stamps[0]);
29
30
  updateSessionFilePaths(oldVersionDir, trashPath);
30
31
  }
31
- /**
32
- * Helper to get actual installed version for an agent.
33
- * Returns the latest installed version, or throws if none installed.
34
- */
35
- async function getInstalledVersionForAgent(agent) {
36
- const versions = listInstalledVersions(agent);
37
- if (versions.length > 0) {
38
- return versions[versions.length - 1];
39
- }
40
- throw new Error(`No versions of ${agent} installed`);
41
- }
42
32
  function formatAccountHint(info, usage) {
43
33
  const parts = [];
44
34
  if (info.email)
@@ -91,7 +81,17 @@ function warnIfShimShadowed(agent) {
91
81
  }
92
82
  console.log(chalk.yellow(` Warning: ${AGENTS[agent].cliCommand} currently resolves to ${shadowedBy}`));
93
83
  console.log(chalk.gray(` Managed shim: ${getShimPath(agent)}`));
94
- console.log(chalk.gray(` ${getPathSetupInstructions().split('\n').join('\n ')}`));
84
+ const result = addShimsToPath();
85
+ if (!result.success) {
86
+ console.log(chalk.gray(` ${getPathSetupInstructions().split('\n').join('\n ')}`));
87
+ return;
88
+ }
89
+ if (result.alreadyPresent) {
90
+ console.log(chalk.gray(` Shim PATH entry already set — ${AGENTS[agent].cliCommand} is shadowed by another binary. Remove or reorder it so ${getShimPath(agent)} takes priority.`));
91
+ return;
92
+ }
93
+ console.log(chalk.green(` Added shim directory to ${result.location}.`));
94
+ console.log(chalk.gray(` ${result.reloadHint}`));
95
95
  }
96
96
  async function versionPruneAction(specs, options, commandName) {
97
97
  const isProject = options.project;
@@ -105,7 +105,7 @@ async function versionPruneAction(specs, options, commandName) {
105
105
  }
106
106
  const { agent, version } = parsed;
107
107
  const agentConfig = AGENTS[agent];
108
- if (version === 'latest' || !spec.includes('@')) {
108
+ if (version === 'latest' || version === 'oldest' || !spec.includes('@')) {
109
109
  const versions = listInstalledVersions(agent);
110
110
  if (versions.length === 0) {
111
111
  console.log(chalk.gray(`No versions of ${agentLabel(agentConfig.id)} installed`));
@@ -139,7 +139,11 @@ async function versionPruneAction(specs, options, commandName) {
139
139
  }
140
140
  for (const v of toRemove) {
141
141
  const versionDir = getVersionDir(agent, v);
142
- removeVersion(agent, v);
142
+ const removed = removeVersion(agent, v);
143
+ if (!removed) {
144
+ console.log(chalk.red(`Failed to move ${agentLabel(agentConfig.id)}@${v} to trash — a file may be locked by a running process. Close any active sessions and try again.`));
145
+ continue;
146
+ }
143
147
  fixSessionFilePaths(agent, v, versionDir);
144
148
  console.log(chalk.green(`Moved ${agentLabel(agentConfig.id)}@${v} to trash`));
145
149
  moved.push({ agent, version: v });
@@ -166,7 +170,11 @@ async function versionPruneAction(specs, options, commandName) {
166
170
  }
167
171
  else {
168
172
  const versionDir = getVersionDir(agent, version);
169
- removeVersion(agent, version);
173
+ const removed = removeVersion(agent, version);
174
+ if (!removed) {
175
+ console.log(chalk.red(`Failed to move ${agentLabel(agentConfig.id)}@${version} to trash — a file may be locked by a running process. Close any active sessions and try again.`));
176
+ continue;
177
+ }
170
178
  fixSessionFilePaths(agent, version, versionDir);
171
179
  console.log(chalk.green(`Moved ${agentLabel(agentConfig.id)}@${version} to trash`));
172
180
  moved.push({ agent, version });
@@ -228,6 +236,9 @@ export function registerVersionsCommands(program) {
228
236
  # Install the latest version of an agent
229
237
  agents add claude@latest
230
238
 
239
+ # Install the oldest published version of an agent
240
+ agents add claude@oldest
241
+
231
242
  # Install a specific version (reproducibility)
232
243
  agents add claude@2.1.112
233
244
 
@@ -259,7 +270,7 @@ export function registerVersionsCommands(program) {
259
270
  console.log(chalk.yellow(`${agentLabel(agentConfig.id)} has no npm package. Install manually.`));
260
271
  continue;
261
272
  }
262
- // Check if already installed (handle 'latest' specially)
273
+ // Check if already installed (resolve 'latest'/'oldest' against npm first)
263
274
  let alreadyInstalled = false;
264
275
  let installedAsVersion = version;
265
276
  if (version === 'latest') {
@@ -269,6 +280,13 @@ export function registerVersionsCommands(program) {
269
280
  installedAsVersion = latestCheck.version;
270
281
  }
271
282
  }
283
+ else if (version === 'oldest') {
284
+ const oldestCheck = await isOldestInstalled(agent);
285
+ if (oldestCheck.installed && oldestCheck.version) {
286
+ alreadyInstalled = true;
287
+ installedAsVersion = oldestCheck.version;
288
+ }
289
+ }
272
290
  else {
273
291
  alreadyInstalled = isVersionInstalled(agent, version);
274
292
  }
@@ -290,6 +308,19 @@ export function registerVersionsCommands(program) {
290
308
  console.log(chalk.gray(` Created shim: ${getShimsDir()}/${agentConfig.cliCommand}`));
291
309
  }
292
310
  const installedVersion = result.installedVersion || version;
311
+ // Track the concrete version so a `--project` pin records it instead
312
+ // of the `latest`/`oldest` alias.
313
+ installedAsVersion = installedVersion;
314
+ // Seed the fresh version home with user settings from the current
315
+ // default version (settings.json, keybindings, codex config/auth).
316
+ // Gap-filling only — never overwrites what the new home has.
317
+ const carrySource = getGlobalDefault(agent);
318
+ if (carrySource && carrySource !== installedVersion) {
319
+ const carried = carryForwardSettings(agent, getVersionHomePath(agent, carrySource), getVersionHomePath(agent, installedVersion));
320
+ if (carried.applied.length > 0) {
321
+ console.log(chalk.gray(` Carried settings from ${agent}@${carrySource}: ${carried.applied.map(r => path.basename(r)).join(', ')}`));
322
+ }
323
+ }
293
324
  // Smart resource detection: compare available vs ACTUALLY synced (source of truth: files)
294
325
  const available = getAvailableResources();
295
326
  const actuallySynced = getActuallySyncedResources(agent, installedVersion);
@@ -428,14 +459,19 @@ export function registerVersionsCommands(program) {
428
459
  ? readManifest(process.cwd()) || createDefaultManifest()
429
460
  : createDefaultManifest();
430
461
  manifest.agents = manifest.agents || {};
431
- manifest.agents[agent] = version === 'latest' ? (await getInstalledVersionForAgent(agent)) : version;
462
+ manifest.agents[agent] = (version === 'latest' || version === 'oldest')
463
+ ? installedAsVersion
464
+ : version;
432
465
  writeManifest(process.cwd(), manifest);
433
466
  console.log(chalk.green(` Pinned ${agentLabel(agentConfig.id)}@${version} in .agents/agents.yaml`));
434
467
  }
435
468
  }
436
469
  });
437
470
  configureVersionPruneCommand(program.command('prune <specs...>'), 'prune');
438
- configureVersionPruneCommand(program.command('remove <specs...>', { hidden: true }), 'remove');
471
+ // `rm` and `purge` are commander aliases for `remove` (which is itself an
472
+ // alias for `prune`). Native `.aliases()` keeps them in lockstep — same
473
+ // action, same options, no duplicate registration.
474
+ configureVersionPruneCommand(program.command('remove <specs...>', { hidden: true }).aliases(['rm', 'purge']), 'remove');
439
475
  const useCmd = program
440
476
  .command('use <agent> [version]')
441
477
  .description('Switch the active version for an agent. This is the only command that sets the default.')
@@ -478,7 +514,7 @@ export function registerVersionsCommands(program) {
478
514
  return;
479
515
  }
480
516
  agent = parsed.agent;
481
- version = parsed.version === 'latest' ? undefined : parsed.version;
517
+ version = (parsed.version === 'latest' || parsed.version === 'oldest') ? undefined : parsed.version;
482
518
  }
483
519
  else {
484
520
  const agentLower = agentArg.toLowerCase();
@@ -679,6 +715,18 @@ export function registerVersionsCommands(program) {
679
715
  }
680
716
  }
681
717
  const previousDefault = getGlobalDefault(agentId);
718
+ // Carry user settings from the outgoing default into the target
719
+ // version home before switching. Gap-filling only, so versions that
720
+ // already have their own settings are left untouched.
721
+ if (previousDefault && previousDefault !== finalVersion) {
722
+ const carried = carryForwardSettings(agentId, getVersionHomePath(agentId, previousDefault), getVersionHomePath(agentId, finalVersion));
723
+ if (carried.applied.length > 0) {
724
+ console.log(chalk.gray(`Carried settings from ${agentId}@${previousDefault}: ${carried.applied.map(r => path.basename(r)).join(', ')}`));
725
+ if (carried.backupDir) {
726
+ console.log(chalk.gray(` Pre-merge backup: ${carried.backupDir}`));
727
+ }
728
+ }
729
+ }
682
730
  // Set global default
683
731
  setGlobalDefault(agentId, finalVersion);
684
732
  // Regenerate shim so it uses the latest script format
@@ -19,6 +19,7 @@ export interface ViewSectionFilter {
19
19
  rules?: boolean;
20
20
  hooks?: boolean;
21
21
  promptcuts?: boolean;
22
+ cli?: boolean;
22
23
  }
23
24
  /** Machine-readable entry for a single installed version. */
24
25
  export interface ViewJsonVersion {
@@ -6,8 +6,9 @@ import { AGENTS, ALL_AGENT_IDS, getAllCliStates, getAccountInfo, resolveAgentNam
6
6
  import { formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
7
7
  import { readManifest } from '../lib/manifest.js';
8
8
  import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, resolveVersionAlias, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, } from '../lib/versions.js';
9
- import { getShimsDir, isShimsInPath, ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
9
+ import { ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
10
10
  import { getAgentResources } from '../lib/resources.js';
11
+ import { listCliStatus } from '../lib/cli-resources.js';
11
12
  import { isCapable } from '../lib/capabilities.js';
12
13
  import { discoverPlugins, pluginSupportsAgent } from '../lib/plugins.js';
13
14
  import { getAgentsDir, getUserAgentsDir, getEffectivePromptcutsPath, readMergedPromptcuts } from '../lib/state.js';
@@ -114,7 +115,7 @@ function getProjectVersionFromCwd(agent) {
114
115
  return null;
115
116
  }
116
117
  }
117
- const SECTION_KEYS = ['commands', 'skills', 'mcp', 'workflows', 'plugins', 'rules', 'hooks', 'promptcuts'];
118
+ const SECTION_KEYS = ['commands', 'skills', 'mcp', 'workflows', 'plugins', 'rules', 'hooks', 'promptcuts', 'cli'];
118
119
  /**
119
120
  * Decide whether a section should render given the filter. If no flags are set,
120
121
  * everything renders (current behavior). If any flag is set, only those sections
@@ -167,6 +168,51 @@ function renderProfilesSection(profiles) {
167
168
  * Show installed versions for one or all agents.
168
169
  * Called when: `agents view` or `agents view claude`
169
170
  */
171
+ /** Color the source-layer tag for a host CLI, matching the rules-section convention. */
172
+ function hostCliSourceTag(source) {
173
+ if (source === 'project')
174
+ return chalk.blue('[project]');
175
+ if (source === 'user')
176
+ return chalk.cyan('[user]');
177
+ if (source === 'system')
178
+ return chalk.gray('[system]');
179
+ // Anything else is an extra repo, tagged by its alias.
180
+ return chalk.magenta(`[${source}]`);
181
+ }
182
+ /**
183
+ * Render the host-CLI section. Host CLIs are host-global: declared in any
184
+ * DotAgents repo's `cli/` (project > user > system > extras), installed to PATH
185
+ * rather than copied into a version home. They render identically in the overview
186
+ * and in a per-agent detail view because every agent on the host shares them.
187
+ * The source tag shows which repo layer declared each — so user-level and
188
+ * extra-repo manifests are visibly supported.
189
+ */
190
+ function renderHostClisSection(cwd) {
191
+ const { statuses, errors } = listCliStatus(cwd);
192
+ console.log(chalk.bold('\nHost CLIs\n'));
193
+ if (statuses.length === 0) {
194
+ console.log(` ${chalk.gray('none declared')} ${chalk.gray('— add one with `agents cli add <name>`')}`);
195
+ }
196
+ else {
197
+ const nameWidth = Math.max(...statuses.map((s) => s.manifest.name.length));
198
+ let anyMissing = false;
199
+ for (const { manifest, installed } of statuses) {
200
+ if (!installed)
201
+ anyMissing = true;
202
+ const status = installed ? chalk.green('installed') : chalk.red('missing ');
203
+ const linkedName = termLink(manifest.name.padEnd(nameWidth), linkTarget(manifest.path));
204
+ const tag = hostCliSourceTag(manifest.source);
205
+ const desc = manifest.description ? chalk.gray(` ${summarizeDescription(manifest.description, 60)}`) : '';
206
+ console.log(` ${status} ${chalk.cyan(linkedName)} ${tag}${desc}`);
207
+ }
208
+ if (anyMissing) {
209
+ console.log(chalk.gray(' Install missing with `agents cli install`'));
210
+ }
211
+ }
212
+ for (const err of errors) {
213
+ console.log(` ${chalk.red('error')} ${chalk.gray(err.file)}: ${chalk.gray(err.reason)}`);
214
+ }
215
+ }
170
216
  async function showInstalledVersions(filterAgentId) {
171
217
  const spinnerText = filterAgentId
172
218
  ? `Checking ${agentLabel(filterAgentId)} agents...`
@@ -537,16 +583,9 @@ async function showInstalledVersions(filterAgentId) {
537
583
  console.log(chalk.gray(' Run: agents add claude@latest'));
538
584
  console.log();
539
585
  }
540
- // Show shims path status at the end (only for full list with managed versions)
541
- if (versionManaged.length > 0 && !filterAgentId) {
542
- const shimsDir = getShimsDir();
543
- if (isShimsInPath()) {
544
- console.log(chalk.gray(`Shims: ${shimsDir} (in PATH)`));
545
- }
546
- else {
547
- console.log(chalk.yellow(`Shims: ${shimsDir} (not in PATH)`));
548
- console.log(chalk.gray('Add to PATH for automatic version switching'));
549
- }
586
+ // Host CLIs are host-global, not per-agent show them once in the overview.
587
+ if (!filterAgentId) {
588
+ renderHostClisSection(process.cwd());
550
589
  }
551
590
  // Check for new resources when viewing a specific agent
552
591
  if (filterAgentId && versionManaged.length > 0) {
@@ -903,6 +942,9 @@ async function showAgentResources(agentId, requestedVersion, filter) {
903
942
  if (shouldRenderSection('promptcuts', filter)) {
904
943
  renderPromptcuts();
905
944
  }
945
+ if (shouldRenderSection('cli', filter)) {
946
+ renderHostClisSection(cwd);
947
+ }
906
948
  // Show legend at the end if git repo exists and we showed all sections.
907
949
  // Filtered single-section views skip it — noise for promptcuts or plugins.
908
950
  if (hasGitRepo && !anyFilterSet) {
@@ -1194,6 +1236,7 @@ export async function viewAction(agentArg, options) {
1194
1236
  rules: options?.rules,
1195
1237
  hooks: options?.hooks,
1196
1238
  promptcuts: options?.promptcuts,
1239
+ cli: options?.cli,
1197
1240
  };
1198
1241
  const filterIsSet = SECTION_KEYS.some((k) => filter[k]);
1199
1242
  if (!agentArg) {
@@ -1274,6 +1317,7 @@ export function registerViewCommand(program) {
1274
1317
  .option('--rules', 'Show only rules in the detail view.')
1275
1318
  .option('--hooks', 'Show only hooks in the detail view.')
1276
1319
  .option('--promptcuts', 'Show only promptcuts in the detail view.')
1320
+ .option('--cli', 'Show only host CLIs (declared in cli/, installed to PATH).')
1277
1321
  .addHelpText('after', `
1278
1322
  Examples:
1279
1323
  # Show all installed agents with versions, accounts, and usage