@phnx-labs/agents-cli 1.15.0 → 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 (87) hide show
  1. package/CHANGELOG.md +78 -39
  2. package/README.md +6 -6
  3. package/dist/commands/alias.js +2 -2
  4. package/dist/commands/browser-picker.d.ts +21 -0
  5. package/dist/commands/browser-picker.js +114 -0
  6. package/dist/commands/browser.js +546 -75
  7. package/dist/commands/commands.js +72 -22
  8. package/dist/commands/daemon.js +2 -2
  9. package/dist/commands/fork.js +2 -2
  10. package/dist/commands/hooks.js +71 -26
  11. package/dist/commands/mcp.js +81 -39
  12. package/dist/commands/plugins.js +48 -15
  13. package/dist/commands/prune.js +23 -1
  14. package/dist/commands/pull.js +3 -3
  15. package/dist/commands/repo.js +1 -1
  16. package/dist/commands/routines.js +2 -2
  17. package/dist/commands/secrets.js +37 -1
  18. package/dist/commands/sessions.js +62 -19
  19. package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
  20. package/dist/commands/{init.js → setup.js} +22 -21
  21. package/dist/commands/skills.js +60 -19
  22. package/dist/commands/subagents.js +41 -13
  23. package/dist/commands/utils.d.ts +16 -0
  24. package/dist/commands/utils.js +32 -0
  25. package/dist/commands/view.js +61 -16
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.js +17 -20
  28. package/dist/lib/agents.js +2 -2
  29. package/dist/lib/auto-pull-worker.js +2 -3
  30. package/dist/lib/auto-pull.js +2 -2
  31. package/dist/lib/browser/cdp.d.ts +7 -1
  32. package/dist/lib/browser/cdp.js +29 -1
  33. package/dist/lib/browser/chrome.js +5 -2
  34. package/dist/lib/browser/devices.d.ts +4 -0
  35. package/dist/lib/browser/devices.js +27 -0
  36. package/dist/lib/browser/drivers/local.js +9 -4
  37. package/dist/lib/browser/drivers/ssh.js +9 -2
  38. package/dist/lib/browser/ipc.js +144 -23
  39. package/dist/lib/browser/profiles.d.ts +5 -2
  40. package/dist/lib/browser/profiles.js +77 -37
  41. package/dist/lib/browser/service.d.ts +81 -13
  42. package/dist/lib/browser/service.js +738 -131
  43. package/dist/lib/browser/types.d.ts +81 -3
  44. package/dist/lib/browser/types.js +16 -0
  45. package/dist/lib/cloud/rush.js +2 -2
  46. package/dist/lib/cloud/store.js +2 -2
  47. package/dist/lib/commands.d.ts +1 -0
  48. package/dist/lib/commands.js +6 -2
  49. package/dist/lib/daemon.js +2 -3
  50. package/dist/lib/doctor-diff.js +4 -4
  51. package/dist/lib/events.js +2 -2
  52. package/dist/lib/hooks.d.ts +11 -7
  53. package/dist/lib/hooks.js +125 -49
  54. package/dist/lib/migrate.d.ts +1 -1
  55. package/dist/lib/migrate.js +1178 -21
  56. package/dist/lib/models.js +2 -2
  57. package/dist/lib/permissions.d.ts +8 -8
  58. package/dist/lib/permissions.js +8 -8
  59. package/dist/lib/plugins.d.ts +30 -1
  60. package/dist/lib/plugins.js +75 -3
  61. package/dist/lib/pty-server.js +9 -10
  62. package/dist/lib/resources/hooks.d.ts +5 -1
  63. package/dist/lib/resources/hooks.js +21 -4
  64. package/dist/lib/rotate.js +3 -4
  65. package/dist/lib/session/active.d.ts +3 -0
  66. package/dist/lib/session/active.js +92 -6
  67. package/dist/lib/session/cloud.js +2 -2
  68. package/dist/lib/session/db.js +8 -3
  69. package/dist/lib/session/discover.js +30 -15
  70. package/dist/lib/session/team-filter.js +2 -2
  71. package/dist/lib/shims.d.ts +2 -2
  72. package/dist/lib/shims.js +6 -6
  73. package/dist/lib/skills.js +6 -2
  74. package/dist/lib/state.d.ts +86 -14
  75. package/dist/lib/state.js +150 -23
  76. package/dist/lib/subagents.d.ts +28 -0
  77. package/dist/lib/subagents.js +98 -1
  78. package/dist/lib/sync-manifest.d.ts +1 -1
  79. package/dist/lib/sync-manifest.js +3 -3
  80. package/dist/lib/teams/persistence.js +15 -5
  81. package/dist/lib/teams/registry.js +2 -2
  82. package/dist/lib/types.d.ts +32 -3
  83. package/dist/lib/types.js +3 -3
  84. package/dist/lib/usage.js +2 -2
  85. package/dist/lib/versions.js +20 -21
  86. package/package.json +1 -1
  87. package/scripts/postinstall.js +1 -1
@@ -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);
@@ -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
@@ -7,7 +7,7 @@ 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
9
  import { getUserAgentsDir } from '../lib/state.js';
10
- import { isPromptCancelled, isInteractiveTerminal, requireInteractiveSelection } from './utils.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) {
@@ -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}`);
@@ -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
  }
@@ -27,6 +27,8 @@ 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
29
  import { diffVersionHooks, iterHooksCapableVersions, removeHookFromVersion, } from '../lib/hooks.js';
30
+ import { diffVersionPlugins, iterPluginsCapableVersions, removePluginSkillFromVersion, } from '../lib/plugins.js';
31
+ import { diffVersionSubagents, iterSubagentsCapableVersions, removeSubagentFromVersion, } from '../lib/subagents.js';
30
32
  import { getGlobalDefault } from '../lib/versions.js';
31
33
  import { resolveAgentName, formatAgentError } from '../lib/agents.js';
32
34
  import { pruneDuplicates } from './view.js';
@@ -34,7 +36,7 @@ import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
34
36
  import { getTrashDir } from '../lib/state.js';
35
37
  import { countSessionsOlderThan, deleteSessionsOlderThan } from '../lib/session/db.js';
36
38
  import { previewRunsPrune, pruneRuns, countAllRuns } from '../lib/routines.js';
37
- const RESOURCE_TYPES = ['commands', 'skills', 'hooks'];
39
+ const RESOURCE_TYPES = ['commands', 'skills', 'hooks', 'plugins', 'subagents'];
38
40
  const STATE_TYPES = ['trash', 'sessions', 'runs'];
39
41
  const ALL_TYPES = [...RESOURCE_TYPES, 'versions', ...STATE_TYPES];
40
42
  function scopePairs(pairs, all) {
@@ -68,6 +70,22 @@ function collectOrphans(types, all) {
68
70
  }
69
71
  }
70
72
  }
73
+ if (types.includes('plugins')) {
74
+ for (const { agent, version } of scopePairs(iterPluginsCapableVersions(), all)) {
75
+ const diff = diffVersionPlugins(agent, version);
76
+ if (diff.orphans.length > 0) {
77
+ groups.push({ type: 'plugins', agent, version, orphans: diff.orphans });
78
+ }
79
+ }
80
+ }
81
+ if (types.includes('subagents')) {
82
+ for (const { agent, version } of scopePairs(iterSubagentsCapableVersions(), all)) {
83
+ const diff = diffVersionSubagents(agent, version);
84
+ if (diff.orphans.length > 0) {
85
+ groups.push({ type: 'subagents', agent, version, orphans: diff.orphans });
86
+ }
87
+ }
88
+ }
71
89
  return groups;
72
90
  }
73
91
  function removeOne(group, name) {
@@ -78,6 +96,10 @@ function removeOne(group, name) {
78
96
  return removeSkillFromVersion(group.agent, group.version, name);
79
97
  case 'hooks':
80
98
  return removeHookFromVersion(group.agent, group.version, name);
99
+ case 'plugins':
100
+ return removePluginSkillFromVersion(group.agent, group.version, name);
101
+ case 'subagents':
102
+ return removeSubagentFromVersion(group.agent, group.version, name);
81
103
  }
82
104
  }
83
105
  function parseTarget(arg) {
@@ -80,12 +80,12 @@ Skip CLI installs with --skip-clis when you only want config updates, not versio
80
80
  // auto-syncs the system repo in the background and surfaces upstream
81
81
  // changes for user/extra repos as one-line notices. Repo lifecycle is
82
82
  // managed under `agents repo`. We keep this command functional today
83
- // because `agents init` still invokes it for first-time setup; once
84
- // init is refactored to call the bootstrap helpers directly, this
83
+ // because `agents setup` still invokes it for first-time setup; once
84
+ // setup is refactored to call the bootstrap helpers directly, this
85
85
  // command will hard-error like `agents memory` does.
86
86
  if (!options.yes && process.argv[2] === 'pull') {
87
87
  process.stderr.write('agents-cli: "agents pull" is deprecated.\n' +
88
- ' First-time setup: agents init\n' +
88
+ ' First-time setup: agents setup\n' +
89
89
  ' Force a sync now: agents repo pull\n' +
90
90
  ' Push your repo: agents repo push\n\n');
91
91
  }
@@ -257,7 +257,7 @@ Examples:
257
257
  const systemStatus = !systemOnDisk
258
258
  ? chalk.red('missing')
259
259
  : !systemIsGit
260
- ? chalk.yellow('not a git repo — run: agents init')
260
+ ? chalk.yellow('not a git repo — run: agents setup')
261
261
  : chalk.green('cloned');
262
262
  const systemCommitLabel = systemCommit ? chalk.gray(`(${systemCommit})`) : '';
263
263
  console.log(chalk.bold('System (~/.agents-system/)'));