@phnx-labs/agents-cli 1.14.7 → 1.16.0

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 (105) hide show
  1. package/CHANGELOG.md +78 -39
  2. package/README.md +74 -7
  3. package/dist/commands/alias.js +2 -2
  4. package/dist/commands/beta.js +6 -1
  5. package/dist/commands/browser-picker.d.ts +21 -0
  6. package/dist/commands/browser-picker.js +114 -0
  7. package/dist/commands/browser.js +546 -75
  8. package/dist/commands/commands.js +72 -22
  9. package/dist/commands/daemon.js +2 -2
  10. package/dist/commands/exec.js +9 -2
  11. package/dist/commands/fork.js +2 -2
  12. package/dist/commands/hooks.js +71 -26
  13. package/dist/commands/mcp.js +85 -43
  14. package/dist/commands/plugins.js +48 -15
  15. package/dist/commands/prune.d.ts +0 -20
  16. package/dist/commands/prune.js +291 -16
  17. package/dist/commands/pull.js +3 -3
  18. package/dist/commands/repo.js +1 -1
  19. package/dist/commands/routines.js +2 -2
  20. package/dist/commands/secrets.js +37 -1
  21. package/dist/commands/sessions.js +62 -19
  22. package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
  23. package/dist/commands/{init.js → setup.js} +32 -21
  24. package/dist/commands/skills.js +60 -19
  25. package/dist/commands/subagents.js +41 -13
  26. package/dist/commands/teams.js +2 -3
  27. package/dist/commands/usage.js +6 -0
  28. package/dist/commands/utils.d.ts +16 -0
  29. package/dist/commands/utils.js +32 -0
  30. package/dist/commands/versions.js +8 -6
  31. package/dist/commands/view.js +61 -16
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.js +17 -20
  34. package/dist/lib/agents.js +2 -2
  35. package/dist/lib/auto-pull-worker.js +2 -3
  36. package/dist/lib/auto-pull.js +2 -2
  37. package/dist/lib/browser/cdp.d.ts +7 -1
  38. package/dist/lib/browser/cdp.js +29 -1
  39. package/dist/lib/browser/chrome.js +6 -3
  40. package/dist/lib/browser/devices.d.ts +4 -0
  41. package/dist/lib/browser/devices.js +27 -0
  42. package/dist/lib/browser/drivers/local.js +9 -4
  43. package/dist/lib/browser/drivers/ssh.d.ts +1 -0
  44. package/dist/lib/browser/drivers/ssh.js +32 -4
  45. package/dist/lib/browser/ipc.js +145 -23
  46. package/dist/lib/browser/profiles.d.ts +5 -2
  47. package/dist/lib/browser/profiles.js +77 -37
  48. package/dist/lib/browser/service.d.ts +84 -13
  49. package/dist/lib/browser/service.js +806 -122
  50. package/dist/lib/browser/types.d.ts +81 -3
  51. package/dist/lib/browser/types.js +16 -0
  52. package/dist/lib/cloud/rush.js +2 -2
  53. package/dist/lib/cloud/store.js +2 -2
  54. package/dist/lib/commands.d.ts +1 -0
  55. package/dist/lib/commands.js +6 -2
  56. package/dist/lib/daemon.js +6 -7
  57. package/dist/lib/doctor-diff.js +4 -4
  58. package/dist/lib/events.d.ts +94 -1
  59. package/dist/lib/events.js +264 -6
  60. package/dist/lib/exec.js +16 -10
  61. package/dist/lib/hooks.d.ts +11 -7
  62. package/dist/lib/hooks.js +125 -49
  63. package/dist/lib/migrate.d.ts +1 -1
  64. package/dist/lib/migrate.js +1178 -21
  65. package/dist/lib/models.js +2 -2
  66. package/dist/lib/permissions.d.ts +14 -11
  67. package/dist/lib/permissions.js +46 -42
  68. package/dist/lib/plugins.d.ts +30 -1
  69. package/dist/lib/plugins.js +75 -3
  70. package/dist/lib/pty-server.js +9 -10
  71. package/dist/lib/resources/hooks.d.ts +5 -1
  72. package/dist/lib/resources/hooks.js +21 -4
  73. package/dist/lib/rotate.js +3 -4
  74. package/dist/lib/routines.d.ts +15 -0
  75. package/dist/lib/routines.js +68 -0
  76. package/dist/lib/runner.js +9 -5
  77. package/dist/lib/secrets/index.d.ts +14 -11
  78. package/dist/lib/secrets/index.js +49 -21
  79. package/dist/lib/secrets/linux.d.ts +27 -0
  80. package/dist/lib/secrets/linux.js +161 -0
  81. package/dist/lib/session/active.d.ts +3 -0
  82. package/dist/lib/session/active.js +92 -6
  83. package/dist/lib/session/cloud.js +2 -2
  84. package/dist/lib/session/db.d.ts +4 -0
  85. package/dist/lib/session/db.js +34 -3
  86. package/dist/lib/session/discover.js +30 -15
  87. package/dist/lib/session/team-filter.js +2 -2
  88. package/dist/lib/shims.d.ts +2 -2
  89. package/dist/lib/shims.js +6 -6
  90. package/dist/lib/skills.js +6 -2
  91. package/dist/lib/state.d.ts +86 -14
  92. package/dist/lib/state.js +150 -23
  93. package/dist/lib/subagents.d.ts +28 -0
  94. package/dist/lib/subagents.js +98 -1
  95. package/dist/lib/sync-manifest.d.ts +1 -1
  96. package/dist/lib/sync-manifest.js +3 -3
  97. package/dist/lib/teams/persistence.js +15 -5
  98. package/dist/lib/teams/registry.js +2 -2
  99. package/dist/lib/types.d.ts +32 -3
  100. package/dist/lib/types.js +3 -3
  101. package/dist/lib/usage.d.ts +1 -1
  102. package/dist/lib/usage.js +15 -48
  103. package/dist/lib/versions.js +31 -21
  104. package/package.json +1 -1
  105. package/scripts/postinstall.js +37 -9
@@ -4,14 +4,14 @@ import * as fs from 'fs';
4
4
  import * as os from 'os';
5
5
  import * as path from 'path';
6
6
  import { checkbox } from '@inquirer/prompts';
7
- import { ALL_AGENT_IDS, resolveAgentName, formatAgentError, agentLabel, } from '../lib/agents.js';
7
+ import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError, agentLabel, } from '../lib/agents.js';
8
8
  import { cloneRepo } from '../lib/git.js';
9
- import { discoverCommands, resolveCommandSource, installCommandCentrally, uninstallCommand, listCentralCommands, getCommandInfo, diffVersionCommands, iterCommandsCapableVersions, } from '../lib/commands.js';
9
+ import { discoverCommands, resolveCommandSource, installCommandCentrally, listCentralCommands, listInstalledCommandsWithScope, getCommandInfo, diffVersionCommands, iterCommandsCapableVersions, removeCommandFromVersion, } from '../lib/commands.js';
10
10
  import { getCommandsDir } from '../lib/state.js';
11
11
  import { showResourceList, buildTargetsSection, } from './resource-view.js';
12
- import { getGlobalDefault, resolveVersionAlias, syncResourcesToVersion, promptAgentVersionSelection, resolveAgentVersionTargets, } from '../lib/versions.js';
12
+ import { getGlobalDefault, resolveVersionAlias, syncResourcesToVersion, promptAgentVersionSelection, getVersionHomePath, resolveAgentVersionTargets, } from '../lib/versions.js';
13
13
  import { recordVersionResources } from '../lib/state.js';
14
- import { isPromptCancelled, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, } from './utils.js';
14
+ import { isPromptCancelled, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, promptRemovalTargets, } from './utils.js';
15
15
  /** Register the `agents commands` command tree (list, add, remove, sync, prune, view). */
16
16
  export function registerCommandsCommands(program) {
17
17
  const commandsCmd = program
@@ -271,15 +271,29 @@ Examples:
271
271
  agents commands remove
272
272
  `)
273
273
  .action(async (name, options) => {
274
+ const cmdTargetMap = new Map();
275
+ for (const { agent, version } of iterCommandsCapableVersions()) {
276
+ const home = getVersionHomePath(agent, version);
277
+ const commands = listInstalledCommandsWithScope(agent, process.cwd(), { home });
278
+ for (const cmd of commands) {
279
+ if (cmd.scope !== 'user')
280
+ continue;
281
+ const existing = cmdTargetMap.get(cmd.name);
282
+ if (existing) {
283
+ existing.targets.push({ agent, version });
284
+ }
285
+ else {
286
+ cmdTargetMap.set(cmd.name, { name: cmd.name, targets: [{ agent, version }] });
287
+ }
288
+ }
289
+ }
274
290
  let commandsToRemove;
275
291
  if (name) {
276
292
  commandsToRemove = [name];
277
293
  }
278
294
  else {
279
- // Interactive picker
280
- const centralCommands = listCentralCommands();
281
- if (centralCommands.length === 0) {
282
- console.log(chalk.yellow('No commands installed.'));
295
+ if (cmdTargetMap.size === 0) {
296
+ console.log(chalk.yellow('No commands installed in any version.'));
283
297
  return;
284
298
  }
285
299
  if (!isInteractiveTerminal()) {
@@ -288,12 +302,16 @@ Examples:
288
302
  ]);
289
303
  }
290
304
  try {
305
+ const choices = Array.from(cmdTargetMap.values()).map((cmd) => {
306
+ const agents = [...new Set(cmd.targets.map((t) => AGENTS[t.agent].name))];
307
+ return {
308
+ value: cmd.name,
309
+ name: `${cmd.name} (${agents.join(', ')})`,
310
+ };
311
+ });
291
312
  const selected = await checkbox({
292
313
  message: 'Select commands to remove',
293
- choices: centralCommands.map((cmd) => ({
294
- value: cmd,
295
- name: cmd,
296
- })),
314
+ choices,
297
315
  });
298
316
  if (selected.length === 0) {
299
317
  console.log(chalk.gray('No commands selected.'));
@@ -309,20 +327,52 @@ Examples:
309
327
  throw err;
310
328
  }
311
329
  }
312
- const agents = options?.agents
313
- ? options.agents.split(',')
314
- : ALL_AGENT_IDS;
330
+ let removed = 0;
315
331
  for (const cmdName of commandsToRemove) {
316
- let removed = 0;
317
- for (const agentId of agents) {
318
- if (uninstallCommand(agentId, cmdName)) {
319
- console.log(` ${chalk.red('-')} ${agentLabel(agentId)}: ${cmdName}`);
332
+ const cmdInfo = cmdTargetMap.get(cmdName);
333
+ if (!cmdInfo || cmdInfo.targets.length === 0) {
334
+ console.log(chalk.yellow(` Command '${cmdName}' not found in any version.`));
335
+ continue;
336
+ }
337
+ // Filter by --agents if specified
338
+ let availableTargets = cmdInfo.targets;
339
+ if (options?.agents) {
340
+ const requestedAgents = new Set(options.agents.split(','));
341
+ availableTargets = availableTargets.filter((t) => requestedAgents.has(t.agent));
342
+ }
343
+ if (availableTargets.length === 0) {
344
+ console.log(chalk.yellow(` Command '${cmdName}' not found in specified agents.`));
345
+ continue;
346
+ }
347
+ const removalTargets = availableTargets.map((t) => ({
348
+ agent: t.agent,
349
+ version: t.version,
350
+ label: `${agentLabel(t.agent)}@${t.version}`,
351
+ }));
352
+ const selectedTargets = await promptRemovalTargets(cmdName, removalTargets, {
353
+ skipPrompt: !!options?.agents,
354
+ });
355
+ if (selectedTargets.length === 0) {
356
+ console.log(chalk.gray(` Skipped '${cmdName}'.`));
357
+ continue;
358
+ }
359
+ for (const target of selectedTargets) {
360
+ const result = removeCommandFromVersion(target.agent, target.version, cmdName);
361
+ if (result.success) {
362
+ console.log(` ${chalk.red('-')} ${target.label}: ${cmdName}`);
320
363
  removed++;
321
364
  }
365
+ else if (result.error) {
366
+ console.log(` ${chalk.yellow('!')} ${target.label}: ${result.error}`);
367
+ }
322
368
  }
323
- if (removed === 0) {
324
- console.log(chalk.yellow(`Command '${cmdName}' not found for any agent`));
325
- }
369
+ }
370
+ if (removed === 0) {
371
+ console.log(chalk.yellow('No commands removed.'));
372
+ }
373
+ else {
374
+ console.log(chalk.green(`\nRemoved ${removed} command(s) from version homes.`));
375
+ console.log(chalk.gray('Central source unchanged. Commands will re-sync on next agent launch.'));
326
376
  }
327
377
  });
328
378
  // `commands sync` is gone — sync runs automatically when the agent launches.
@@ -91,8 +91,8 @@ you never need to start it manually.
91
91
  .action(async (options) => {
92
92
  warnDeprecated('logs', 'agents routines scheduler-logs');
93
93
  if (options.follow) {
94
- const { getAgentsDir } = await import('../lib/state.js');
95
- const logPath = path.join(getAgentsDir(), 'helpers/daemon/logs.jsonl');
94
+ const { getDaemonDir } = await import('../lib/state.js');
95
+ const logPath = path.join(getDaemonDir(), 'logs.jsonl');
96
96
  const child = spawn('tail', ['-f', logPath], { stdio: ['ignore', 'pipe', 'pipe'] });
97
97
  child.stdout.pipe(process.stdout);
98
98
  child.stderr.pipe(process.stderr);
@@ -8,7 +8,7 @@
8
8
  import chalk from 'chalk';
9
9
  import { buildExecCommand, parseExecEnv, execAgent, runWithFallback, AGENT_COMMANDS, } from '../lib/exec.js';
10
10
  import { profileExists, resolveProfileForRun } from '../lib/profiles.js';
11
- import { readBundle, resolveBundleEnv } from '../lib/secrets/bundles.js';
11
+ import { readBundle, resolveBundleEnv, describeBundle } from '../lib/secrets/bundles.js';
12
12
  import { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, RUN_STRATEGIES, } from '../lib/rotate.js';
13
13
  import { resolveVersionAlias } from '../lib/versions.js';
14
14
  const VALID_AGENTS = Object.keys(AGENT_COMMANDS);
@@ -36,7 +36,7 @@ export function registerRunCommand(program) {
36
36
  .option('--cwd <dir>', 'Working directory for the agent (defaults to current directory)')
37
37
  .option('--add-dir <dir>', 'Grant access to an additional directory outside the project (Claude only, repeatable)', (val, prev) => [...prev, val], [])
38
38
  .option('--json', 'Stream events as JSON lines (for parsing by other tools)')
39
- .option('--headless', 'Non-interactive mode (default for run)', true)
39
+ .option('--headless', 'Non-interactive mode (auto-enabled when prompt provided)', false)
40
40
  .option('-i, --interactive', 'Force interactive mode even when a prompt is provided')
41
41
  .option('--session-id <id>', 'Resume a previous conversation (Claude only)')
42
42
  .option('--verbose', 'Show detailed execution logs')
@@ -197,6 +197,13 @@ Examples:
197
197
  for (const bundleName of options.secrets) {
198
198
  try {
199
199
  const bundle = readBundle(bundleName);
200
+ const entries = describeBundle(bundle);
201
+ const counts = {};
202
+ for (const e of entries) {
203
+ counts[e.kind] = (counts[e.kind] || 0) + 1;
204
+ }
205
+ const breakdown = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(', ');
206
+ console.log(chalk.gray(`[secrets] Resolved ${bundleName}: ${entries.length} keys (${breakdown})`));
200
207
  secretsEnv = { ...secretsEnv, ...resolveBundleEnv(bundle) };
201
208
  }
202
209
  catch (err) {
@@ -15,14 +15,14 @@ import { isPromptCancelled } from './utils.js';
15
15
  export function registerForkCommand(program) {
16
16
  program
17
17
  .command('fork')
18
- .description('Copy the default config repo to your own GitHub so you can push changes. Runs once after init.')
18
+ .description('Copy the default config repo to your own GitHub so you can push changes. Runs once after setup.')
19
19
  .addHelpText('after', `
20
20
  Examples:
21
21
  # Fork the default repo to your GitHub account
22
22
  agents fork
23
23
 
24
24
  When to use:
25
- - You initialized with 'agents init' using the default config
25
+ - You set up with 'agents setup' using the default config
26
26
  - You've customized commands, skills, or settings
27
27
  - You want to save your changes to your own GitHub repo
28
28
 
@@ -7,10 +7,10 @@ import { checkbox } from '@inquirer/prompts';
7
7
  import { AGENTS, HOOKS_CAPABLE_AGENTS, resolveAgentName, formatAgentError, agentLabel, } from '../lib/agents.js';
8
8
  import { supports } from '../lib/capabilities.js';
9
9
  import { cloneRepo } from '../lib/git.js';
10
- import { discoverHooksFromRepo, installHooksCentrally, listCentralHooks, listInstalledHooksWithScope, removeHook, getHookInfo, parseHookManifest, } from '../lib/hooks.js';
10
+ import { discoverHooksFromRepo, installHooksCentrally, listCentralHooks, listInstalledHooksWithScope, getHookInfo, parseHookManifest, iterHooksCapableVersions, removeHookFromVersion, } from '../lib/hooks.js';
11
11
  import { listInstalledVersions, getGlobalDefault, resolveVersionAlias, syncResourcesToVersion, promptAgentVersionSelection, getVersionHomePath, resolveAgentVersionTargets, } from '../lib/versions.js';
12
12
  import { recordVersionResources } from '../lib/state.js';
13
- import { isPromptCancelled, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, } from './utils.js';
13
+ import { isPromptCancelled, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, promptRemovalTargets, } from './utils.js';
14
14
  /** Register the `agents hooks` command tree (list, add, remove, sync, prune, view). */
15
15
  export function registerHooksCommands(program) {
16
16
  const hooksCmd = program.command('hooks')
@@ -396,15 +396,29 @@ Examples:
396
396
  agents hooks remove
397
397
  `)
398
398
  .action(async (name, options) => {
399
+ const hookTargetMap = new Map();
400
+ for (const { agent, version } of iterHooksCapableVersions()) {
401
+ const home = getVersionHomePath(agent, version);
402
+ const hooks = listInstalledHooksWithScope(agent, process.cwd(), { home });
403
+ for (const hook of hooks) {
404
+ if (hook.scope !== 'user')
405
+ continue;
406
+ const existing = hookTargetMap.get(hook.name);
407
+ if (existing) {
408
+ existing.targets.push({ agent, version });
409
+ }
410
+ else {
411
+ hookTargetMap.set(hook.name, { name: hook.name, targets: [{ agent, version }] });
412
+ }
413
+ }
414
+ }
399
415
  let hooksToRemove;
400
416
  if (name) {
401
417
  hooksToRemove = [name];
402
418
  }
403
419
  else {
404
- // Interactive picker
405
- const centralHooks = listCentralHooks();
406
- if (centralHooks.length === 0) {
407
- console.log(chalk.yellow('No hooks installed.'));
420
+ if (hookTargetMap.size === 0) {
421
+ console.log(chalk.yellow('No hooks installed in any version.'));
408
422
  return;
409
423
  }
410
424
  if (!isInteractiveTerminal()) {
@@ -413,12 +427,16 @@ Examples:
413
427
  ]);
414
428
  }
415
429
  try {
430
+ const choices = Array.from(hookTargetMap.values()).map((hook) => {
431
+ const agents = [...new Set(hook.targets.map((t) => AGENTS[t.agent].name))];
432
+ return {
433
+ value: hook.name,
434
+ name: `${hook.name} (${agents.join(', ')})`,
435
+ };
436
+ });
416
437
  const selected = await checkbox({
417
438
  message: 'Select hooks to remove',
418
- choices: centralHooks.map((hook) => ({
419
- value: hook.name,
420
- name: hook.name,
421
- })),
439
+ choices,
422
440
  });
423
441
  if (selected.length === 0) {
424
442
  console.log(chalk.gray('No hooks selected.'));
@@ -434,26 +452,53 @@ Examples:
434
452
  throw err;
435
453
  }
436
454
  }
437
- const agents = options?.agents
438
- ? options.agents.split(',')
439
- : Array.from(HOOKS_CAPABLE_AGENTS);
455
+ let removed = 0;
440
456
  for (const hookName of hooksToRemove) {
441
- const result = await removeHook(hookName, agents);
442
- let removed = 0;
443
- for (const item of result.removed) {
444
- const [, agentId] = item.split(':');
445
- console.log(` ${chalk.red('-')} ${agentLabel(agentId)}: ${hookName}`);
446
- removed++;
447
- }
448
- if (result.errors.length > 0) {
449
- for (const error of result.errors) {
450
- console.log(chalk.red(` ${error}`));
451
- }
457
+ const hookInfo = hookTargetMap.get(hookName);
458
+ if (!hookInfo || hookInfo.targets.length === 0) {
459
+ console.log(chalk.yellow(` Hook '${hookName}' not found in any version.`));
460
+ continue;
461
+ }
462
+ // Filter by --agents if specified
463
+ let availableTargets = hookInfo.targets;
464
+ if (options?.agents) {
465
+ const requestedAgents = new Set(options.agents.split(','));
466
+ availableTargets = availableTargets.filter((t) => requestedAgents.has(t.agent));
467
+ }
468
+ if (availableTargets.length === 0) {
469
+ console.log(chalk.yellow(` Hook '${hookName}' not found in specified agents.`));
470
+ continue;
471
+ }
472
+ const removalTargets = availableTargets.map((t) => ({
473
+ agent: t.agent,
474
+ version: t.version,
475
+ label: `${agentLabel(t.agent)}@${t.version}`,
476
+ }));
477
+ const selectedTargets = await promptRemovalTargets(hookName, removalTargets, {
478
+ skipPrompt: !!options?.agents,
479
+ });
480
+ if (selectedTargets.length === 0) {
481
+ console.log(chalk.gray(` Skipped '${hookName}'.`));
482
+ continue;
452
483
  }
453
- if (removed === 0) {
454
- console.log(chalk.yellow(`Hook '${hookName}' not found for any agent`));
484
+ for (const target of selectedTargets) {
485
+ const result = removeHookFromVersion(target.agent, target.version, hookName);
486
+ if (result.success) {
487
+ console.log(` ${chalk.red('-')} ${target.label}: ${hookName}`);
488
+ removed++;
489
+ }
490
+ else if (result.error) {
491
+ console.log(` ${chalk.yellow('!')} ${target.label}: ${result.error}`);
492
+ }
455
493
  }
456
494
  }
495
+ if (removed === 0) {
496
+ console.log(chalk.yellow('No hooks removed.'));
497
+ }
498
+ else {
499
+ console.log(chalk.green(`\nRemoved ${removed} hook(s) from version homes.`));
500
+ console.log(chalk.gray('Central source unchanged. Hooks will re-sync on next agent launch.'));
501
+ }
457
502
  });
458
503
  // `hooks sync` is gone — sync runs automatically when the agent launches.
459
504
  hooksCmd
@@ -6,8 +6,8 @@ import { readManifest, writeManifest, createDefaultManifest } from '../lib/manif
6
6
  import { listMcpServerConfigs } from '../lib/mcp.js';
7
7
  import { getMcpDir } from '../lib/state.js';
8
8
  import { getEffectiveHome, getGlobalDefault, listInstalledVersions, getVersionHomePath, resolveInstalledAgentTargets, resolveConfiguredAgentTargets, resolveVersionAlias, } from '../lib/versions.js';
9
- import { getAgentsDir } from '../lib/state.js';
10
- import { isPromptCancelled, isInteractiveTerminal, requireInteractiveSelection } from './utils.js';
9
+ import { getUserAgentsDir } from '../lib/state.js';
10
+ import { isPromptCancelled, isInteractiveTerminal, requireInteractiveSelection, promptRemovalTargets } from './utils.js';
11
11
  import { showResourceList, buildTargetsSection, } from './resource-view.js';
12
12
  /** Parse a comma-separated --agents string into validated agent IDs and optional version targets. */
13
13
  function parseMcpAgentTargets(value) {
@@ -163,7 +163,7 @@ Examples:
163
163
  console.log(chalk.gray('HTTP: agents mcp add <name> <url> --transport http'));
164
164
  process.exit(1);
165
165
  }
166
- const localPath = getAgentsDir();
166
+ const localPath = getUserAgentsDir();
167
167
  const manifest = readManifest(localPath) || createDefaultManifest();
168
168
  manifest.mcp = manifest.mcp || {};
169
169
  const targetConfig = parseMcpAgentTargets(options.agents);
@@ -219,20 +219,33 @@ Examples:
219
219
  .action(async (name, options) => {
220
220
  const cwd = process.cwd();
221
221
  const cliStates = await getAllCliStates();
222
+ const mcpTargetMap = new Map();
223
+ for (const agentId of MCP_CAPABLE_AGENTS) {
224
+ if (!cliStates[agentId]?.installed && listInstalledVersions(agentId).length === 0)
225
+ continue;
226
+ for (const version of listInstalledVersions(agentId)) {
227
+ const home = getVersionHomePath(agentId, version);
228
+ const configPath = getMcpConfigPathForHome(agentId, home);
229
+ const mcps = parseMcpConfig(agentId, configPath);
230
+ for (const mcpName of Object.keys(mcps)) {
231
+ const existing = mcpTargetMap.get(mcpName);
232
+ if (existing) {
233
+ existing.targets.push({ agentId, version, home });
234
+ }
235
+ else {
236
+ mcpTargetMap.set(mcpName, { name: mcpName, targets: [{ agentId, version, home }] });
237
+ }
238
+ }
239
+ }
240
+ }
222
241
  let mcpsToRemove;
223
- let targets;
224
242
  if (name) {
225
243
  mcpsToRemove = [name];
226
- const installedAgents = MCP_CAPABLE_AGENTS.filter((agentId) => cliStates[agentId]?.installed || listInstalledVersions(agentId).length > 0);
227
- targets = options?.agents
228
- ? resolveInstalledAgentTargets(options.agents, MCP_CAPABLE_AGENTS)
229
- : resolveConfiguredAgentTargets(installedAgents, undefined, MCP_CAPABLE_AGENTS);
230
244
  }
231
245
  else {
232
- // Interactive picker: collect all MCPs across all installed agents
233
- const installedAgents = MCP_CAPABLE_AGENTS.filter((agentId) => cliStates[agentId]?.installed);
234
- if (installedAgents.length === 0) {
235
- console.log(chalk.yellow('No MCP-capable agents installed.'));
246
+ // Interactive picker for MCP selection
247
+ if (mcpTargetMap.size === 0) {
248
+ console.log(chalk.yellow('No MCP servers configured.'));
236
249
  return;
237
250
  }
238
251
  if (!isInteractiveTerminal()) {
@@ -241,42 +254,22 @@ Examples:
241
254
  'agents mcp remove my-server --agents codex,claude',
242
255
  ]);
243
256
  }
244
- // Gather all unique MCPs across agents (with agent info for display)
245
- const mcpMap = new Map();
246
- for (const agentId of installedAgents) {
247
- const mcps = listInstalledMcpsWithScope(agentId, cwd, { home: getEffectiveHome(agentId) });
248
- for (const mcp of mcps) {
249
- const existing = mcpMap.get(mcp.name);
250
- if (existing) {
251
- existing.agents.push(AGENTS[agentId].name);
252
- }
253
- else {
254
- mcpMap.set(mcp.name, {
255
- name: mcp.name,
256
- agents: [AGENTS[agentId].name],
257
- command: mcp.command,
258
- });
259
- }
260
- }
261
- }
262
- if (mcpMap.size === 0) {
263
- console.log(chalk.yellow('No MCP servers configured.'));
264
- return;
265
- }
266
257
  try {
267
258
  const selected = await checkbox({
268
259
  message: 'Select MCP servers to remove',
269
- choices: Array.from(mcpMap.values()).map((mcp) => ({
270
- value: mcp.name,
271
- name: `${mcp.name} (${mcp.agents.join(', ')})`,
272
- })),
260
+ choices: Array.from(mcpTargetMap.values()).map((mcp) => {
261
+ const agents = [...new Set(mcp.targets.map((t) => AGENTS[t.agentId].name))];
262
+ return {
263
+ value: mcp.name,
264
+ name: `${mcp.name} (${agents.join(', ')})`,
265
+ };
266
+ }),
273
267
  });
274
268
  if (selected.length === 0) {
275
269
  console.log(chalk.gray('No MCPs selected.'));
276
270
  return;
277
271
  }
278
272
  mcpsToRemove = selected;
279
- targets = resolveConfiguredAgentTargets(installedAgents, undefined, MCP_CAPABLE_AGENTS);
280
273
  }
281
274
  catch (err) {
282
275
  if (isPromptCancelled(err)) {
@@ -286,10 +279,59 @@ Examples:
286
279
  throw err;
287
280
  }
288
281
  }
289
- // Execute removals - try each MCP on each target agent
282
+ // Execute removals with target selection
290
283
  let removed = 0;
291
284
  for (const mcpName of mcpsToRemove) {
292
- const results = await unregisterMcpFromTargets(targets, mcpName);
285
+ const mcpInfo = mcpTargetMap.get(mcpName);
286
+ if (!mcpInfo || mcpInfo.targets.length === 0) {
287
+ console.log(chalk.yellow(` MCP '${mcpName}' not found in any agent.`));
288
+ continue;
289
+ }
290
+ // If --agents was specified, filter targets
291
+ let availableTargets = mcpInfo.targets;
292
+ if (options?.agents) {
293
+ const requestedTargets = resolveInstalledAgentTargets(options.agents, MCP_CAPABLE_AGENTS);
294
+ const requested = new Set();
295
+ for (const aid of requestedTargets.directAgents) {
296
+ for (const ver of listInstalledVersions(aid)) {
297
+ requested.add(`${aid}@${ver}`);
298
+ }
299
+ }
300
+ for (const [aid, versions] of requestedTargets.versionSelections) {
301
+ for (const ver of versions) {
302
+ requested.add(`${aid}@${ver}`);
303
+ }
304
+ }
305
+ availableTargets = availableTargets.filter((t) => requested.has(`${t.agentId}@${t.version}`));
306
+ }
307
+ if (availableTargets.length === 0) {
308
+ console.log(chalk.yellow(` MCP '${mcpName}' not found in specified agents.`));
309
+ continue;
310
+ }
311
+ // Show target picker if multiple targets and no --agents flag
312
+ const removalTargets = availableTargets.map((t) => ({
313
+ agent: t.agentId,
314
+ version: t.version,
315
+ label: formatTargetLabel(t.agentId, t.version),
316
+ }));
317
+ const selectedTargets = await promptRemovalTargets(mcpName, removalTargets, {
318
+ skipPrompt: !!options?.agents,
319
+ });
320
+ if (selectedTargets.length === 0) {
321
+ console.log(chalk.gray(` Skipped '${mcpName}'.`));
322
+ continue;
323
+ }
324
+ // Build targets structure for unregister
325
+ const versionSelections = new Map();
326
+ for (const t of selectedTargets) {
327
+ const versions = versionSelections.get(t.agent) || [];
328
+ if (!versions.includes(t.version)) {
329
+ versions.push(t.version);
330
+ versionSelections.set(t.agent, versions);
331
+ }
332
+ }
333
+ const targetsToRemove = { directAgents: [], versionSelections };
334
+ const results = await unregisterMcpFromTargets(targetsToRemove, mcpName);
293
335
  for (const result of results) {
294
336
  if (result.success) {
295
337
  console.log(` ${chalk.red('-')} ${formatTargetLabel(result.agentId, result.version)}: ${mcpName}`);
@@ -400,7 +442,7 @@ Examples:
400
442
  agents mcp register --agents codex@0.116.0
401
443
  `)
402
444
  .action(async (name, options) => {
403
- const localPath = getAgentsDir();
445
+ const localPath = getUserAgentsDir();
404
446
  const manifest = readManifest(localPath);
405
447
  if (!manifest?.mcp) {
406
448
  console.log(chalk.yellow('No MCP servers in manifest'));
@@ -467,7 +509,7 @@ function buildMcpRows(opts) {
467
509
  const centralServers = new Map();
468
510
  for (const s of listMcpServerConfigs())
469
511
  centralServers.set(s.name, s);
470
- const manifest = readManifest(getAgentsDir());
512
+ const manifest = readManifest(getUserAgentsDir());
471
513
  const manifestEntries = manifest?.mcp || {};
472
514
  const targetPairs = iterMcpCapableVersions({
473
515
  agent: opts.filterAgent,
@@ -12,7 +12,7 @@ import { PLUGINS_CAPABLE_AGENTS, agentLabel } from '../lib/agents.js';
12
12
  import { discoverPlugins, getPlugin, pluginSupportsAgent, removePluginFromVersion } from '../lib/plugins.js';
13
13
  import { listInstalledVersions, syncResourcesToVersion, getGlobalDefault, getVersionHomePath, } from '../lib/versions.js';
14
14
  import { isPluginSynced } from '../lib/plugins.js';
15
- import { isPromptCancelled, isInteractiveTerminal, requireDestructiveArg, requireInteractiveSelection, } from './utils.js';
15
+ import { isPromptCancelled, isInteractiveTerminal, requireDestructiveArg, requireInteractiveSelection, promptRemovalTargets, } from './utils.js';
16
16
  import { itemPicker } from '../lib/picker.js';
17
17
  import { showResourceList, buildTargetsSection, } from './resource-view.js';
18
18
  import { getPluginsDir } from '../lib/state.js';
@@ -255,7 +255,7 @@ Examples:
255
255
  # Unsync but keep source directory
256
256
  agents plugins remove rush-toolkit --keep-source
257
257
  `)
258
- .action((nameArg, options) => {
258
+ .action(async (nameArg, options) => {
259
259
  if (!nameArg) {
260
260
  requireDestructiveArg({
261
261
  argName: 'name',
@@ -275,31 +275,64 @@ Examples:
275
275
  console.log(chalk.red(`Plugin '${name}' not found`));
276
276
  process.exit(1);
277
277
  }
278
- let totalSkills = 0;
279
- let totalHooks = 0;
280
- let totalPerms = 0;
281
- let versionsTouched = 0;
278
+ // Build list of targets that have this plugin synced
279
+ const availableTargets = [];
282
280
  for (const agentId of PLUGINS_CAPABLE_AGENTS) {
281
+ if (plugin && !pluginSupportsAgent(plugin, agentId))
282
+ continue;
283
283
  const versions = listInstalledVersions(agentId);
284
284
  for (const version of versions) {
285
285
  const versionHome = getVersionHomePath(agentId, version);
286
- const r = removePluginFromVersion(name, resolvedRoot, agentId, versionHome);
287
- if (r.skills.length > 0 || r.hooks.length > 0 || r.permissions > 0) {
288
- versionsTouched += 1;
289
- totalSkills += r.skills.length;
290
- totalHooks += r.hooks.length;
291
- totalPerms += r.permissions;
292
- console.log(chalk.gray(` ${agentLabel(agentId)}@${version}: ${r.skills.length} skill(s), ${r.hooks.length} hook(s), ${r.permissions} perm(s)`));
286
+ if (plugin && isPluginSynced(plugin, agentId, versionHome)) {
287
+ availableTargets.push({ agent: agentId, version });
293
288
  }
294
289
  }
295
290
  }
296
- console.log(chalk.green(`Unsynced ${name} from ${versionsTouched} version(s) — ${totalSkills} skills, ${totalHooks} hooks, ${totalPerms} permissions`));
297
- if (!options.keepSource) {
291
+ if (availableTargets.length === 0) {
292
+ console.log(chalk.yellow(`Plugin '${name}' not synced to any version.`));
293
+ if (!options.keepSource && fs.existsSync(pluginRoot)) {
294
+ fs.rmSync(pluginRoot, { recursive: true, force: true });
295
+ console.log(chalk.green(`Deleted ${formatPath(pluginRoot)}`));
296
+ }
297
+ return;
298
+ }
299
+ // Show multi-select picker for targets
300
+ const removalTargets = availableTargets.map((t) => ({
301
+ agent: t.agent,
302
+ version: t.version,
303
+ label: `${agentLabel(t.agent)}@${t.version}`,
304
+ }));
305
+ const selectedTargets = await promptRemovalTargets(name, removalTargets);
306
+ if (selectedTargets.length === 0) {
307
+ console.log(chalk.gray('Cancelled.'));
308
+ return;
309
+ }
310
+ let totalSkills = 0;
311
+ let totalHooks = 0;
312
+ let totalPerms = 0;
313
+ let versionsTouched = 0;
314
+ for (const target of selectedTargets) {
315
+ const versionHome = getVersionHomePath(target.agent, target.version);
316
+ const r = removePluginFromVersion(name, resolvedRoot, target.agent, versionHome);
317
+ if (r.skills.length > 0 || r.hooks.length > 0 || r.permissions > 0) {
318
+ versionsTouched += 1;
319
+ totalSkills += r.skills.length;
320
+ totalHooks += r.hooks.length;
321
+ totalPerms += r.permissions;
322
+ console.log(` ${chalk.red('-')} ${target.label}: ${r.skills.length} skill(s), ${r.hooks.length} hook(s), ${r.permissions} perm(s)`);
323
+ }
324
+ }
325
+ console.log(chalk.green(`\nUnsynced ${name} from ${versionsTouched} version(s) — ${totalSkills} skills, ${totalHooks} hooks, ${totalPerms} permissions`));
326
+ // Only delete source if ALL targets were selected
327
+ if (!options.keepSource && selectedTargets.length === availableTargets.length) {
298
328
  if (fs.existsSync(pluginRoot)) {
299
329
  fs.rmSync(pluginRoot, { recursive: true, force: true });
300
330
  console.log(chalk.green(`Deleted ${formatPath(pluginRoot)}`));
301
331
  }
302
332
  }
333
+ else if (!options.keepSource && selectedTargets.length < availableTargets.length) {
334
+ console.log(chalk.gray(`Source kept — plugin still synced to other versions.`));
335
+ }
303
336
  else {
304
337
  console.log(chalk.gray(`Kept source at ${formatPath(pluginRoot)}`));
305
338
  }
@@ -1,22 +1,2 @@
1
- /**
2
- * Top-level `agents prune` — destructive cleanup across the install.
3
- *
4
- * Two kinds of cleanup, one verb:
5
- * - Resource orphans: command/skill/hook files inside a version home that no
6
- * longer come from any source (deleted from ~/.agents/ but never reconciled
7
- * into the version install).
8
- * - Version duplicates: older installed versions of an agent that share an
9
- * account with a newer installed version of the same agent (the older copy
10
- * is redundant; the newer one is what's signed in and active).
11
- *
12
- * Sync (additive: copy missing/changed files into version homes) is no longer
13
- * a user-facing verb — `syncResourcesToVersion` runs at agent launch and
14
- * applies adds/updates automatically. Pruning, however, is destructive, so it
15
- * stays explicit.
16
- *
17
- * Default scope: each agent's currently-pinned default version for orphan
18
- * cleanup, plus the standard cross-agent version-dedup pass. Pass `--all`
19
- * to widen orphan cleanup to every installed version.
20
- */
21
1
  import type { Command } from 'commander';
22
2
  export declare function registerPruneCommand(program: Command): void;