@phnx-labs/agents-cli 1.19.2 → 1.20.3

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 (156) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/README.md +72 -12
  3. package/dist/browser.js +0 -0
  4. package/dist/commands/browser.js +88 -16
  5. package/dist/commands/cli.d.ts +14 -0
  6. package/dist/commands/cli.js +244 -0
  7. package/dist/commands/cloud.js +1 -1
  8. package/dist/commands/commands.js +27 -10
  9. package/dist/commands/computer.js +18 -1
  10. package/dist/commands/doctor.d.ts +1 -1
  11. package/dist/commands/doctor.js +2 -2
  12. package/dist/commands/exec.js +38 -18
  13. package/dist/commands/factory.d.ts +3 -14
  14. package/dist/commands/factory.js +3 -3
  15. package/dist/commands/feedback.d.ts +7 -0
  16. package/dist/commands/feedback.js +89 -0
  17. package/dist/commands/helper.d.ts +12 -0
  18. package/dist/commands/helper.js +87 -0
  19. package/dist/commands/hooks.js +89 -10
  20. package/dist/commands/mcp.js +166 -10
  21. package/dist/commands/packages.js +196 -27
  22. package/dist/commands/permissions.js +21 -6
  23. package/dist/commands/plugins.js +11 -4
  24. package/dist/commands/profiles.d.ts +8 -0
  25. package/dist/commands/profiles.js +118 -5
  26. package/dist/commands/prune.js +39 -160
  27. package/dist/commands/pull.js +58 -5
  28. package/dist/commands/routines.js +107 -14
  29. package/dist/commands/rules.js +8 -4
  30. package/dist/commands/secrets-migrate.d.ts +24 -0
  31. package/dist/commands/secrets-migrate.js +198 -0
  32. package/dist/commands/secrets-sync.d.ts +11 -0
  33. package/dist/commands/secrets-sync.js +155 -0
  34. package/dist/commands/secrets.js +79 -46
  35. package/dist/commands/sessions.d.ts +28 -0
  36. package/dist/commands/sessions.js +98 -33
  37. package/dist/commands/setup.d.ts +1 -0
  38. package/dist/commands/setup.js +37 -28
  39. package/dist/commands/skills.js +25 -8
  40. package/dist/commands/subagents.js +69 -49
  41. package/dist/commands/teams.js +61 -10
  42. package/dist/commands/utils.d.ts +33 -0
  43. package/dist/commands/utils.js +139 -0
  44. package/dist/commands/versions.d.ts +4 -3
  45. package/dist/commands/versions.js +134 -130
  46. package/dist/commands/view.d.ts +6 -0
  47. package/dist/commands/view.js +175 -19
  48. package/dist/commands/workflows.js +29 -6
  49. package/dist/computer.js +0 -0
  50. package/dist/index.js +38 -6
  51. package/dist/lib/acp/client.js +6 -1
  52. package/dist/lib/acp/harnesses.js +8 -0
  53. package/dist/lib/agents.d.ts +4 -0
  54. package/dist/lib/agents.js +125 -34
  55. package/dist/lib/auto-pull-worker.js +18 -1
  56. package/dist/lib/browser/cdp.d.ts +8 -1
  57. package/dist/lib/browser/cdp.js +40 -3
  58. package/dist/lib/browser/chrome.d.ts +13 -0
  59. package/dist/lib/browser/chrome.js +46 -3
  60. package/dist/lib/browser/domain-skills.d.ts +51 -0
  61. package/dist/lib/browser/domain-skills.js +157 -0
  62. package/dist/lib/browser/drivers/local.js +45 -4
  63. package/dist/lib/browser/drivers/ssh.js +2 -2
  64. package/dist/lib/browser/ipc.d.ts +8 -1
  65. package/dist/lib/browser/ipc.js +37 -28
  66. package/dist/lib/browser/profiles.d.ts +16 -3
  67. package/dist/lib/browser/profiles.js +44 -4
  68. package/dist/lib/browser/service.d.ts +3 -0
  69. package/dist/lib/browser/service.js +40 -5
  70. package/dist/lib/browser/types.d.ts +11 -4
  71. package/dist/lib/cli-resources.d.ts +137 -0
  72. package/dist/lib/cli-resources.js +477 -0
  73. package/dist/lib/cloud/factory.d.ts +1 -1
  74. package/dist/lib/cloud/factory.js +1 -1
  75. package/dist/lib/cloud/rush.js +5 -5
  76. package/dist/lib/command-skills.js +0 -2
  77. package/dist/lib/computer-rpc.d.ts +3 -0
  78. package/dist/lib/computer-rpc.js +53 -0
  79. package/dist/lib/daemon.js +20 -0
  80. package/dist/lib/events.d.ts +16 -2
  81. package/dist/lib/events.js +33 -2
  82. package/dist/lib/exec.d.ts +42 -13
  83. package/dist/lib/exec.js +127 -33
  84. package/dist/lib/help.js +11 -5
  85. package/dist/lib/hooks/cache.d.ts +38 -0
  86. package/dist/lib/hooks/cache.js +242 -0
  87. package/dist/lib/hooks/profile.d.ts +33 -0
  88. package/dist/lib/hooks/profile.js +129 -0
  89. package/dist/lib/hooks.d.ts +0 -10
  90. package/dist/lib/hooks.js +246 -11
  91. package/dist/lib/mcp.d.ts +15 -0
  92. package/dist/lib/mcp.js +46 -0
  93. package/dist/lib/migrate.js +1 -1
  94. package/dist/lib/overdue.d.ts +26 -0
  95. package/dist/lib/overdue.js +101 -0
  96. package/dist/lib/permissions.d.ts +13 -0
  97. package/dist/lib/permissions.js +55 -1
  98. package/dist/lib/plugin-marketplace.js +1 -1
  99. package/dist/lib/plugins.js +15 -1
  100. package/dist/lib/profiles-presets.d.ts +26 -0
  101. package/dist/lib/profiles-presets.js +216 -0
  102. package/dist/lib/profiles.d.ts +34 -0
  103. package/dist/lib/profiles.js +112 -1
  104. package/dist/lib/resources/mcp.js +37 -0
  105. package/dist/lib/resources.d.ts +1 -1
  106. package/dist/lib/rotate.js +10 -4
  107. package/dist/lib/routines-format.d.ts +47 -0
  108. package/dist/lib/routines-format.js +194 -0
  109. package/dist/lib/routines.d.ts +8 -2
  110. package/dist/lib/routines.js +34 -14
  111. package/dist/lib/runner.js +83 -15
  112. package/dist/lib/scheduler.js +8 -1
  113. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  114. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  115. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
  116. package/dist/lib/secrets/bundles.d.ts +34 -17
  117. package/dist/lib/secrets/bundles.js +210 -36
  118. package/dist/lib/secrets/index.d.ts +49 -30
  119. package/dist/lib/secrets/index.js +126 -115
  120. package/dist/lib/secrets/install-helper.d.ts +45 -0
  121. package/dist/lib/secrets/install-helper.js +165 -0
  122. package/dist/lib/secrets/linux.js +4 -4
  123. package/dist/lib/secrets/sync.d.ts +56 -0
  124. package/dist/lib/secrets/sync.js +180 -0
  125. package/dist/lib/session/active.d.ts +8 -0
  126. package/dist/lib/session/active.js +3 -2
  127. package/dist/lib/session/db.d.ts +0 -4
  128. package/dist/lib/session/db.js +0 -26
  129. package/dist/lib/session/parse.d.ts +1 -0
  130. package/dist/lib/session/parse.js +44 -0
  131. package/dist/lib/session/render.js +4 -4
  132. package/dist/lib/session/types.d.ts +2 -2
  133. package/dist/lib/session/types.js +1 -1
  134. package/dist/lib/shims.d.ts +5 -2
  135. package/dist/lib/shims.js +70 -38
  136. package/dist/lib/state.d.ts +14 -2
  137. package/dist/lib/state.js +51 -20
  138. package/dist/lib/teams/agents.d.ts +5 -4
  139. package/dist/lib/teams/agents.js +48 -22
  140. package/dist/lib/teams/api.d.ts +2 -1
  141. package/dist/lib/teams/api.js +4 -3
  142. package/dist/lib/teams/parsers.d.ts +1 -1
  143. package/dist/lib/teams/parsers.js +153 -3
  144. package/dist/lib/teams/summarizer.js +18 -2
  145. package/dist/lib/teams/worktree.js +14 -3
  146. package/dist/lib/types.d.ts +63 -4
  147. package/dist/lib/types.js +8 -3
  148. package/dist/lib/usage.d.ts +27 -2
  149. package/dist/lib/usage.js +100 -17
  150. package/dist/lib/versions.d.ts +45 -3
  151. package/dist/lib/versions.js +455 -60
  152. package/package.json +15 -14
  153. package/scripts/install-helper.js +97 -0
  154. package/scripts/postinstall.js +16 -0
  155. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  156. package/npm-shrinkwrap.json +0 -3162
@@ -23,9 +23,9 @@ import { promisify } from 'util';
23
23
  import chalk from 'chalk';
24
24
  import * as TOML from 'smol-toml';
25
25
  import { checkbox, select } from '@inquirer/prompts';
26
- import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getCommandsDir, getSkillsDir, getHooksDir, getResolvedRulesDir, getUserRulesDir, clearVersionResources, getVersionResources, ensureVersionResourcePatterns, getProjectAgentsDir, getPromptcutsPath, getUserPromptcutsPath, getEnabledExtraRepos, getAgentsDir, getUserAgentsDir, getTrashVersionsDir, getActiveRulesPreset } from './state.js';
26
+ import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getCommandsDir, getSkillsDir, getHooksDir, getResolvedRulesDir, getUserRulesDir, getVersionResources, ensureVersionResourcePatterns, getProjectAgentsDir, getPromptcutsPath, getUserPromptcutsPath, getEnabledExtraRepos, getAgentsDir, getUserAgentsDir, getTrashVersionsDir, getActiveRulesPreset } from './state.js';
27
27
  import { defaultPatterns, expandPatterns } from './resource-patterns.js';
28
- import { resolveResource, listResources } from './resources.js';
28
+ import { listResources } from './resources.js';
29
29
  import { AGENTS, getAccountEmail, MCP_CAPABLE_AGENTS, COMMANDS_CAPABLE_AGENTS, getMcpConfigPathForHome, parseMcpConfig, resolveAgentName, formatAgentError } from './agents.js';
30
30
  import { applyPermissionsToVersion as applyPermsToVersion, PERMISSIONS_CAPABLE_AGENTS, discoverPermissionGroups, buildPermissionsFromGroups, CODEX_RULES_FILENAME, getActivePermissionPresetName, readPermissionPresetRecipe, PERMISSION_PRESET_ENV_VAR } from './permissions.js';
31
31
  import { installMcpServers, parseMcpServerConfig } from './mcp.js';
@@ -296,13 +296,21 @@ export function getActuallySyncedResources(agent, version, options = {}) {
296
296
  workflows: [],
297
297
  promptcuts: false,
298
298
  };
299
- // Commands - check what files exist in version home
300
- const commandsDir = path.join(configDir, agentConfig.commandsSubdir);
301
- if (fs.existsSync(commandsDir)) {
302
- const ext = agentConfig.format === 'toml' ? '.toml' : '.md';
303
- result.commands = fs.readdirSync(commandsDir)
304
- .filter(f => f.endsWith(ext))
305
- .map(f => f.replace(new RegExp(`\\${ext}$`), ''));
299
+ // Commands - check what files exist in version home.
300
+ // For agent/version pairs that store commands as converted skills (e.g. Codex >= 0.117.0),
301
+ // detect them via the agents_command marker in skills/<name>/SKILL.md — otherwise the
302
+ // diff falsely reports every command as "new" every run and re-prompts on `agents view`.
303
+ if (shouldInstallCommandAsSkill(agent, version)) {
304
+ result.commands = listCommandSkillsInVersion(path.join(configDir));
305
+ }
306
+ else {
307
+ const commandsDir = path.join(configDir, agentConfig.commandsSubdir);
308
+ if (fs.existsSync(commandsDir)) {
309
+ const ext = agentConfig.format === 'toml' ? '.toml' : '.md';
310
+ result.commands = fs.readdirSync(commandsDir)
311
+ .filter(f => f.endsWith(ext))
312
+ .map(f => f.replace(new RegExp(`\\${ext}$`), ''));
313
+ }
306
314
  }
307
315
  // Skills - check what directories exist AND content matches central source
308
316
  const skillsDir = path.join(configDir, 'skills');
@@ -503,16 +511,130 @@ export function getActuallySyncedResources(agent, version, options = {}) {
503
511
  }
504
512
  return result;
505
513
  }
514
+ /**
515
+ * Names that exist ONLY in the project's `.agents/` layer (no matching entry in
516
+ * user/system/extra layers). Sync intentionally skips project-layer commands,
517
+ * skills, hooks, subagents, plugins, and workflows for security — see the
518
+ * defense comments above each sync branch in syncResourcesToVersion. Without
519
+ * this filter, those names would forever appear in the "New resources" diff
520
+ * because they live in `available` but never reach `actuallySynced`.
521
+ */
522
+ export function getProjectOnlyResources(cwd = process.cwd()) {
523
+ const empty = {
524
+ commands: new Set(), skills: new Set(), hooks: new Set(),
525
+ subagents: new Set(), plugins: new Set(), workflows: new Set(),
526
+ };
527
+ const projectAgentsDir = getProjectAgentsDir(cwd);
528
+ if (!projectAgentsDir)
529
+ return empty;
530
+ const trustedBases = [getUserAgentsDir(), getAgentsDir(), ...getEnabledExtraRepos().map(e => e.dir)];
531
+ const trustedNames = (relSubdir, predicate) => {
532
+ const acc = new Set();
533
+ for (const base of trustedBases) {
534
+ const dir = path.join(base, relSubdir);
535
+ if (!fs.existsSync(dir))
536
+ continue;
537
+ try {
538
+ for (const entry of fs.readdirSync(dir)) {
539
+ if (entry.startsWith('.'))
540
+ continue;
541
+ if (predicate(path.join(dir, entry), entry))
542
+ acc.add(entry);
543
+ }
544
+ }
545
+ catch { /* ignore unreadable */ }
546
+ }
547
+ return acc;
548
+ };
549
+ const readProjectNames = (relSubdir, predicate) => {
550
+ const dir = path.join(projectAgentsDir, relSubdir);
551
+ if (!fs.existsSync(dir))
552
+ return [];
553
+ try {
554
+ return fs.readdirSync(dir)
555
+ .filter(e => !e.startsWith('.'))
556
+ .filter(e => predicate(path.join(dir, e), e));
557
+ }
558
+ catch {
559
+ return [];
560
+ }
561
+ };
562
+ const isMdFile = (full, name) => name.endsWith('.md') && (() => { try {
563
+ return fs.statSync(full).isFile();
564
+ }
565
+ catch {
566
+ return false;
567
+ } })();
568
+ const isDir = (full) => { try {
569
+ return fs.statSync(full).isDirectory();
570
+ }
571
+ catch {
572
+ return false;
573
+ } };
574
+ const hasFile = (sub) => (full) => isDir(full) && fs.existsSync(path.join(full, sub));
575
+ const stripMd = (n) => n.replace(/\.md$/, '');
576
+ const trustedCommands = new Set([...trustedNames('commands', isMdFile)].map(stripMd));
577
+ const projectCommands = readProjectNames('commands', isMdFile).map(stripMd);
578
+ for (const n of projectCommands)
579
+ if (!trustedCommands.has(n))
580
+ empty.commands.add(n);
581
+ const trustedSkills = trustedNames('skills', (full) => isDir(full));
582
+ for (const n of readProjectNames('skills', (full) => isDir(full)))
583
+ if (!trustedSkills.has(n))
584
+ empty.skills.add(n);
585
+ // Hooks: project entries are files; trusted entries are also files. Name match
586
+ // is filename-with-extension (sync compares by full filename, line 2031).
587
+ const trustedHooks = trustedNames('hooks', (full) => { try {
588
+ return fs.statSync(full).isFile();
589
+ }
590
+ catch {
591
+ return false;
592
+ } });
593
+ for (const n of readProjectNames('hooks', (full) => { try {
594
+ return fs.statSync(full).isFile();
595
+ }
596
+ catch {
597
+ return false;
598
+ } })) {
599
+ if (!trustedHooks.has(n))
600
+ empty.hooks.add(n);
601
+ }
602
+ const trustedSubagents = trustedNames('subagents', hasFile('AGENT.md'));
603
+ for (const n of readProjectNames('subagents', hasFile('AGENT.md'))) {
604
+ if (!trustedSubagents.has(n))
605
+ empty.subagents.add(n);
606
+ }
607
+ const trustedWorkflows = trustedNames('workflows', hasFile('WORKFLOW.md'));
608
+ for (const n of readProjectNames('workflows', hasFile('WORKFLOW.md'))) {
609
+ if (!trustedWorkflows.has(n))
610
+ empty.workflows.add(n);
611
+ }
612
+ const trustedPlugins = trustedNames('plugins', hasFile('.claude-plugin/plugin.json'));
613
+ for (const n of readProjectNames('plugins', hasFile('.claude-plugin/plugin.json'))) {
614
+ if (!trustedPlugins.has(n))
615
+ empty.plugins.add(n);
616
+ }
617
+ return empty;
618
+ }
506
619
  /**
507
620
  * Compare available resources with what's ACTUALLY synced to version home.
508
621
  * Returns only NEW resources that haven't been synced yet.
509
622
  * Source of truth: the actual files/config, NOT agents.yaml tracking.
623
+ *
624
+ * `projectOnly` (recommended): the result of `getProjectOnlyResources(cwd)`.
625
+ * Names listed there are filtered out for kinds that sync intentionally
626
+ * excludes the project layer — otherwise they would re-appear as "new"
627
+ * on every run and "Yes, sync all new" would silently do nothing for them.
510
628
  */
511
- export function getNewResources(available, actuallySynced) {
629
+ export function getNewResources(available, actuallySynced, projectOnly) {
630
+ const exclude = projectOnly || {
631
+ commands: new Set(), skills: new Set(), hooks: new Set(),
632
+ subagents: new Set(), plugins: new Set(), workflows: new Set(),
633
+ };
512
634
  return {
513
- commands: available.commands.filter(c => !actuallySynced.commands.includes(c)),
514
- skills: available.skills.filter(s => !actuallySynced.skills.includes(s)),
515
- hooks: available.hooks.filter(h => !actuallySynced.hooks.includes(h)),
635
+ commands: available.commands.filter(c => !actuallySynced.commands.includes(c) && !exclude.commands.has(c)),
636
+ skills: available.skills.filter(s => !actuallySynced.skills.includes(s) && !exclude.skills.has(s)),
637
+ hooks: available.hooks.filter(h => !actuallySynced.hooks.includes(h) && !exclude.hooks.has(h)),
516
638
  // Memory/rules presets are mutually exclusive — only one can be active.
517
639
  // If any preset is synced, don't report others as "new".
518
640
  memory: actuallySynced.memory.length > 0
@@ -520,9 +642,9 @@ export function getNewResources(available, actuallySynced) {
520
642
  : available.memory.filter(m => !actuallySynced.memory.includes(m)),
521
643
  mcp: available.mcp.filter(m => !actuallySynced.mcp.includes(m)),
522
644
  permissions: available.permissions.filter(p => !actuallySynced.permissions.includes(p)),
523
- subagents: available.subagents.filter(s => !actuallySynced.subagents.includes(s)),
524
- plugins: available.plugins.filter(p => !actuallySynced.plugins.includes(p)),
525
- workflows: available.workflows.filter(w => !actuallySynced.workflows.includes(w)),
645
+ subagents: available.subagents.filter(s => !actuallySynced.subagents.includes(s) && !exclude.subagents.has(s)),
646
+ plugins: available.plugins.filter(p => !actuallySynced.plugins.includes(p) && !exclude.plugins.has(p)),
647
+ workflows: available.workflows.filter(w => !actuallySynced.workflows.includes(w) && !exclude.workflows.has(w)),
526
648
  // Promptcuts aren't version-scoped — the hook reads ~/.agents/promptcuts.yaml
527
649
  // directly, so there is never a "new" per-version state to reconcile.
528
650
  promptcuts: false,
@@ -554,10 +676,15 @@ export function hasNewResources(diff, agent, version) {
554
676
  * Build a summary string of new resources.
555
677
  * E.g., "2 commands, 5 permission groups"
556
678
  */
557
- function buildNewResourcesSummary(newResources, agent) {
679
+ function buildNewResourcesSummary(newResources, agent, version) {
558
680
  const agentConfig = AGENTS[agent];
559
681
  const parts = [];
560
- if (newResources.commands.length > 0 && COMMANDS_CAPABLE_AGENTS.includes(agent)) {
682
+ // Use version-aware gates so Codex >= 0.117.0 (which converts commands to skills) doesn't
683
+ // double-count and so "16 commands" never appears in the summary when commands have
684
+ // already been emitted as skills in the version home.
685
+ const commandsApply = version ? supports(agent, 'commands', version).ok : COMMANDS_CAPABLE_AGENTS.includes(agent);
686
+ const commandsAsSkills = version ? shouldInstallCommandAsSkill(agent, version) : false;
687
+ if (newResources.commands.length > 0 && (commandsApply || commandsAsSkills)) {
561
688
  parts.push(`${newResources.commands.length} command${newResources.commands.length === 1 ? '' : 's'}`);
562
689
  }
563
690
  if (newResources.skills.length > 0) {
@@ -566,7 +693,7 @@ function buildNewResourcesSummary(newResources, agent) {
566
693
  if (newResources.hooks.length > 0 && agentConfig.supportsHooks) {
567
694
  parts.push(`${newResources.hooks.length} hook${newResources.hooks.length === 1 ? '' : 's'}`);
568
695
  }
569
- if (newResources.memory.length > 0 && COMMANDS_CAPABLE_AGENTS.includes(agent)) {
696
+ if (newResources.memory.length > 0 && (commandsApply || commandsAsSkills)) {
570
697
  parts.push(`${newResources.memory.length} rule file${newResources.memory.length === 1 ? '' : 's'}`);
571
698
  }
572
699
  if (newResources.mcp.length > 0 && MCP_CAPABLE_AGENTS.includes(agent)) {
@@ -590,15 +717,21 @@ function buildNewResourcesSummary(newResources, agent) {
590
717
  * Prompt user to select which NEW resources to sync.
591
718
  * Only shows resources that haven't been synced yet.
592
719
  */
593
- export async function promptNewResourceSelection(agent, newResources) {
720
+ export async function promptNewResourceSelection(agent, newResources, version) {
594
721
  const agentConfig = AGENTS[agent];
595
722
  const selection = {};
723
+ // Version-aware gates. When version is known, prefer per-version capability checks; the
724
+ // commands branch is allowed when either native commands are supported OR when the
725
+ // version emits commands as converted skills (Codex >= 0.117.0).
726
+ const commandsApply = version ? supports(agent, 'commands', version).ok : COMMANDS_CAPABLE_AGENTS.includes(agent);
727
+ const commandsAsSkills = version ? shouldInstallCommandAsSkill(agent, version) : false;
728
+ const commandsBranch = commandsApply || commandsAsSkills;
596
729
  // Get permission group info for display
597
730
  const permissionGroups = discoverPermissionGroups();
598
731
  const newPermissionGroups = permissionGroups.filter(g => newResources.permissions.includes(g.name));
599
732
  const totalNewPermissionRules = newPermissionGroups.reduce((sum, g) => sum + g.ruleCount, 0);
600
733
  // Build the summary
601
- const summary = buildNewResourcesSummary(newResources, agent);
734
+ const summary = buildNewResourcesSummary(newResources, agent, version);
602
735
  console.log(chalk.cyan(`\nNew resources available:`));
603
736
  console.log(chalk.gray(` ${summary}`));
604
737
  // Ask how to handle new resources
@@ -616,13 +749,13 @@ export async function promptNewResourceSelection(agent, newResources) {
616
749
  }
617
750
  if (action === 'all') {
618
751
  // Sync all new resources
619
- if (newResources.commands.length > 0 && COMMANDS_CAPABLE_AGENTS.includes(agent))
752
+ if (newResources.commands.length > 0 && commandsBranch)
620
753
  selection.commands = newResources.commands;
621
754
  if (newResources.skills.length > 0)
622
755
  selection.skills = newResources.skills;
623
756
  if (newResources.hooks.length > 0 && agentConfig.supportsHooks)
624
757
  selection.hooks = newResources.hooks;
625
- if (newResources.memory.length > 0 && COMMANDS_CAPABLE_AGENTS.includes(agent))
758
+ if (newResources.memory.length > 0 && commandsBranch)
626
759
  selection.memory = newResources.memory;
627
760
  if (newResources.mcp.length > 0 && MCP_CAPABLE_AGENTS.includes(agent))
628
761
  selection.mcp = newResources.mcp;
@@ -637,7 +770,7 @@ export async function promptNewResourceSelection(agent, newResources) {
637
770
  return selection;
638
771
  }
639
772
  // Select specific items for each category
640
- if (newResources.commands.length > 0 && COMMANDS_CAPABLE_AGENTS.includes(agent)) {
773
+ if (newResources.commands.length > 0 && commandsBranch) {
641
774
  const selected = await checkbox({
642
775
  message: 'Select new commands to sync:',
643
776
  choices: newResources.commands.map(c => ({ name: c, value: c, checked: true })),
@@ -661,7 +794,7 @@ export async function promptNewResourceSelection(agent, newResources) {
661
794
  if (selected.length > 0)
662
795
  selection.hooks = selected;
663
796
  }
664
- if (newResources.memory.length > 0 && COMMANDS_CAPABLE_AGENTS.includes(agent)) {
797
+ if (newResources.memory.length > 0 && commandsBranch) {
665
798
  const selected = await checkbox({
666
799
  message: 'Select new rule files to sync:',
667
800
  choices: newResources.memory.map(m => ({ name: m, value: m, checked: true })),
@@ -873,8 +1006,26 @@ export function getVersionDir(agent, version) {
873
1006
  * Get the binary path for a specific agent version.
874
1007
  */
875
1008
  export function getBinaryPath(agent, version) {
876
- const versionDir = getVersionDir(agent, version);
877
1009
  const agentConfig = AGENTS[agent];
1010
+ if (agent === 'grok') {
1011
+ // Grok binaries live in the global ~/.grok/downloads, not per-version node_modules.
1012
+ // We return a best-effort path (used for display / checks). Real resolution
1013
+ // happens in agents.ts resolveGrokBinary + the generated shims.
1014
+ const grokDownloads = path.join(os.homedir(), '.grok', 'downloads');
1015
+ // Best effort: first matching file for this version
1016
+ try {
1017
+ const entries = fs.readdirSync(grokDownloads);
1018
+ const match = entries.find((e) => e.includes(version) && e.startsWith('grok-'));
1019
+ if (match)
1020
+ return path.join(grokDownloads, match);
1021
+ const first = entries.find((e) => e.startsWith('grok-'));
1022
+ if (first)
1023
+ return path.join(grokDownloads, first);
1024
+ }
1025
+ catch { }
1026
+ return path.join(grokDownloads, `grok-${version}`);
1027
+ }
1028
+ const versionDir = getVersionDir(agent, version);
878
1029
  return path.join(versionDir, 'node_modules', '.bin', agentConfig.cliCommand);
879
1030
  }
880
1031
  /**
@@ -940,7 +1091,7 @@ export function listInstalledVersions(agent) {
940
1091
  * List every version directory for an agent, including ones missing the
941
1092
  * binary (typically home-only leftovers from a prior `removeVersion`).
942
1093
  *
943
- * Used by `agents prune` to surface stale installs that the regular
1094
+ * Used by `agents prune cleanup` to surface stale installs that the regular
944
1095
  * `listInstalledVersions` filters out. Do NOT use elsewhere — every other
945
1096
  * call site assumes a working binary.
946
1097
  */
@@ -990,21 +1141,51 @@ export function setGlobalDefault(agent, version) {
990
1141
  */
991
1142
  export async function installVersion(agent, version, onProgress) {
992
1143
  const agentConfig = AGENTS[agent];
993
- if (!agentConfig.npmPackage) {
994
- return { success: false, installedVersion: version, error: 'Agent has no npm package' };
995
- }
996
1144
  // Validate before deriving filesystem paths or npm package specs. The CLI
997
1145
  // parser already enforces this for user input; this guard protects direct
998
1146
  // callers and tests the critical install path at the source.
999
1147
  if (!VERSION_RE.test(version)) {
1000
1148
  throw new Error(`Invalid version: ${JSON.stringify(version)}`);
1001
1149
  }
1150
+ if (!agentConfig.npmPackage) {
1151
+ if (!agentConfig.installScript) {
1152
+ return { success: false, installedVersion: version, error: 'Agent has no npm package' };
1153
+ }
1154
+ if (version !== 'latest' && !agentConfig.installScript.includes('VERSION')) {
1155
+ return {
1156
+ success: false,
1157
+ installedVersion: version,
1158
+ error: `${agentConfig.name} installer does not support version-pinned installs. Use ${agent}@latest.`,
1159
+ };
1160
+ }
1161
+ let installedVersion = version;
1162
+ try {
1163
+ const script = agentConfig.installScript.replaceAll('VERSION', version);
1164
+ onProgress?.(`Installing ${agentConfig.name}@${version} via official installer...`);
1165
+ await execAsync(script, { timeout: 120000 });
1166
+ if (version === 'latest') {
1167
+ installedVersion = await getCliVersionFromPath(agent) || version;
1168
+ }
1169
+ onProgress?.(`${agentConfig.name} installed. Setting up agents-cli version home for isolation...`);
1170
+ }
1171
+ catch (err) {
1172
+ emit('version.install', { agent, version, error: err.message });
1173
+ return { success: false, installedVersion: version, error: `${agentConfig.name} installer failed: ${err.message}` };
1174
+ }
1175
+ ensureAgentsDir();
1176
+ const versionDir = getVersionDir(agent, installedVersion);
1177
+ fs.mkdirSync(versionDir, { recursive: true });
1178
+ fs.mkdirSync(path.join(versionDir, 'home'), { recursive: true });
1179
+ createVersionedAlias(agent, installedVersion);
1180
+ emit('version.install', { agent, version: installedVersion });
1181
+ return { success: true, installedVersion };
1182
+ }
1002
1183
  ensureAgentsDir();
1003
1184
  const versionDir = getVersionDir(agent, version);
1004
1185
  // Create version directory and isolated home
1005
1186
  fs.mkdirSync(versionDir, { recursive: true });
1006
1187
  fs.mkdirSync(path.join(versionDir, 'home'), { recursive: true });
1007
- // Initialize package.json
1188
+ // Initialize package.json (only for real npm agents)
1008
1189
  const packageJson = {
1009
1190
  name: `agents-${agent}-${version}`,
1010
1191
  version: '1.0.0',
@@ -1147,8 +1328,6 @@ export function removeVersion(agent, version) {
1147
1328
  }
1148
1329
  // Remove versioned alias (e.g., claude@2.0.65)
1149
1330
  removeVersionedAlias(agent, version);
1150
- // Clear resource tracking for this version
1151
- clearVersionResources(agent, version);
1152
1331
  // Clear default if it was the removed version - user must explicitly pick a new one
1153
1332
  if (getGlobalDefault(agent) === version) {
1154
1333
  const meta = readMeta();
@@ -1175,6 +1354,23 @@ export function removeVersion(agent, version) {
1175
1354
  emit('version.remove', { agent, version });
1176
1355
  return true;
1177
1356
  }
1357
+ /**
1358
+ * Print the standard footer after one or more versions were soft-deleted to
1359
+ * trash. Reminds the user that sessions stay readable and how to restore.
1360
+ */
1361
+ export function printTrashFooter(moved) {
1362
+ if (moved.length === 0)
1363
+ return;
1364
+ console.log();
1365
+ console.log(chalk.gray('Sessions remain accessible via `agents sessions`.'));
1366
+ if (moved.length === 1) {
1367
+ const { agent, version } = moved[0];
1368
+ console.log(chalk.gray(`Restore with: agents trash restore ${agent}@${version}`));
1369
+ }
1370
+ else {
1371
+ console.log(chalk.gray('Restore with: agents trash restore <agent>@<version> (run `agents trash list` to see)'));
1372
+ }
1373
+ }
1178
1374
  /**
1179
1375
  * Remove all versions of an agent. Preserves each version's `home/` directory
1180
1376
  * so conversation history is never deleted; the per-version folders (now
@@ -1320,6 +1516,18 @@ export async function getInstalledVersion(agent, version) {
1320
1516
  return version;
1321
1517
  }
1322
1518
  }
1519
+ async function getCliVersionFromPath(agent) {
1520
+ const agentConfig = AGENTS[agent];
1521
+ try {
1522
+ await execFileAsync('which', [agentConfig.cliCommand]);
1523
+ const { stdout } = await execFileAsync(agentConfig.cliCommand, ['--version'], { timeout: 3000 });
1524
+ const match = stdout.match(/(\d+\.\d+\.\d+)/);
1525
+ return match ? match[1] : null;
1526
+ }
1527
+ catch {
1528
+ return null;
1529
+ }
1530
+ }
1323
1531
  /**
1324
1532
  * Get the diff between central resources (~/.agents/) and what's synced to a version.
1325
1533
  * Uses filesystem state - no tracking needed.
@@ -1558,12 +1766,11 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1558
1766
  }
1559
1767
  }
1560
1768
  }
1561
- // Fast guard: skip the entire sync when no selection is active and nothing
1562
- // has changed since the last full sync. Drops steady-state cost from ~16s
1563
- // (unconditional file copies) to ~8-15ms (`isStale` walks the manifest's
1564
- // fingerprints, short-circuiting at the first mismatch). Numbers from
1565
- // scripts/bench-staleness.ts against a real ~50-resource project.
1566
- if (!selection && !options.force) {
1769
+ // Fast guard: skip the entire sync when the caller requested a full sync and
1770
+ // nothing has changed since the last full sync. Pattern-derived selections
1771
+ // still count as full syncs because they are the persisted intended scope,
1772
+ // not a one-off caller override.
1773
+ if (!userPassedSelection && !options.force) {
1567
1774
  const manifest = loadManifest(agent, version);
1568
1775
  if (manifest && !isStale(manifest, agent, version, cwd)) {
1569
1776
  return { commands: false, skills: false, hooks: false, memory: [], permissions: false, mcp: [], subagents: [], plugins: [], workflows: [] };
@@ -1626,13 +1833,25 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1626
1833
  }
1627
1834
  const syncedCommands = [];
1628
1835
  for (const cmd of commandsToSync) {
1629
- const resolved = resolveResource('commands', `${cmd}.md`, cwd);
1630
- if (!resolved || fs.lstatSync(resolved.path).isSymbolicLink())
1836
+ // Commands are content that gets injected into the agent's prompt
1837
+ // surface (slash commands, skill bodies). We intentionally do NOT pull
1838
+ // from the project's own .agents/commands/ directory: a cloned public
1839
+ // repo could ship a command whose body instructs the agent to do
1840
+ // something harmful the next time the user invokes it. Commands must
1841
+ // come from the user's central ~/.agents/commands/, the system layer,
1842
+ // or an explicitly enabled extra repo. Same defense as hooks below.
1843
+ const candidates = [
1844
+ safeJoin(path.join(userAgentsDir, 'commands'), `${cmd}.md`),
1845
+ safeJoin(getCommandsDir(), `${cmd}.md`),
1846
+ ...extraRepos.map((e) => safeJoin(path.join(e.dir, 'commands'), `${cmd}.md`)),
1847
+ ];
1848
+ const srcFile = candidates.find((p) => p && fs.existsSync(p) && !fs.lstatSync(p).isSymbolicLink()) || null;
1849
+ if (!srcFile)
1631
1850
  continue;
1632
- const srcFile = resolved.path;
1633
1851
  if (commandsAsSkills) {
1852
+ // Project skills dir is intentionally excluded for the same reason
1853
+ // commands are: the body of a project skill becomes agent context.
1634
1854
  const skillSourceDirs = [
1635
- projectAgentsDir ? path.join(projectAgentsDir, 'skills') : null,
1636
1855
  path.join(userAgentsDir, 'skills'),
1637
1856
  getSkillsDir(),
1638
1857
  ...extraRepos.map((e) => path.join(e.dir, 'skills')),
@@ -1653,6 +1872,30 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1653
1872
  }
1654
1873
  result.commands = syncedCommands.length > 0;
1655
1874
  }
1875
+ // Orphan-sweep stale top-level command files from previous syncs under a
1876
+ // different cwd. Only runs in "full sync" mode — i.e. when the caller did
1877
+ // not pass an explicit `selection`. Callers that pass explicit selections
1878
+ // are using the incremental/additive API (sync exactly these; leave others
1879
+ // alone), so the sweep would be a contract violation there. The
1880
+ // cross-project leak always comes from the no-selection shim auto-sync at
1881
+ // launch.
1882
+ if (!userPassedSelection && COMMANDS_CAPABLE_AGENTS.includes(agent) && !shouldInstallCommandAsSkill(agent, version)) {
1883
+ const commandsTargetSweep = path.join(agentDir, agentConfig.commandsSubdir);
1884
+ if (fs.existsSync(commandsTargetSweep)) {
1885
+ const ext = agentConfig.format === 'toml' ? '.toml' : '.md';
1886
+ const trustedCommands = new Set(commandsToSync);
1887
+ for (const entry of fs.readdirSync(commandsTargetSweep, { withFileTypes: true })) {
1888
+ if (!entry.isFile() || entry.name.startsWith('.'))
1889
+ continue;
1890
+ if (!entry.name.endsWith(ext))
1891
+ continue;
1892
+ const name = entry.name.slice(0, -ext.length);
1893
+ if (!trustedCommands.has(name)) {
1894
+ removePath(safeJoin(commandsTargetSweep, entry.name));
1895
+ }
1896
+ }
1897
+ }
1898
+ }
1656
1899
  // Sync skills (skip if agent natively reads ~/.agents/skills/)
1657
1900
  if (agentConfig.nativeAgentsSkillsDir) {
1658
1901
  // Clean up stale skills symlink/dir — agent reads from ~/.agents/skills/ directly
@@ -1677,10 +1920,19 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1677
1920
  fs.mkdirSync(skillsTarget, { recursive: true });
1678
1921
  const syncedSkills = [];
1679
1922
  for (const skill of skillsToSync) {
1680
- const resolved = resolveResource('skills', skill, cwd);
1681
- const srcDir = resolved && fs.existsSync(resolved.path) && fs.lstatSync(resolved.path).isDirectory()
1682
- ? resolved.path
1683
- : null;
1923
+ // Same defense as commands and hooks: don't pull skills from the
1924
+ // project's .agents/skills/ directory. A skill's contents (SKILL.md
1925
+ // and any auxiliary scripts) get loaded into the agent's tool/context
1926
+ // surface, and a malicious public repo could ship a SKILL.md whose
1927
+ // body coerces the agent. Trusted layers only.
1928
+ const skillCandidates = [
1929
+ safeJoin(path.join(userAgentsDir, 'skills'), skill),
1930
+ safeJoin(getSkillsDir(), skill),
1931
+ ...extraRepos.map((e) => safeJoin(path.join(e.dir, 'skills'), skill)),
1932
+ ];
1933
+ const srcDir = skillCandidates.find((p) => fs.existsSync(p) &&
1934
+ !fs.lstatSync(p).isSymbolicLink() &&
1935
+ fs.lstatSync(p).isDirectory()) || null;
1684
1936
  if (!srcDir)
1685
1937
  continue;
1686
1938
  const destDir = safeJoin(skillsTarget, skill);
@@ -1690,6 +1942,21 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1690
1942
  }
1691
1943
  result.skills = syncedSkills.length > 0;
1692
1944
  }
1945
+ // Orphan-sweep stale skill directories from previous syncs under a
1946
+ // different cwd. Only runs in "full sync" mode (no explicit selection) —
1947
+ // see the matching guard on the commands sweep above for why. Skip
1948
+ // dot-dirs to keep plugin-managed subtrees (.plugins/, .promptcuts) intact.
1949
+ const skillsTargetSweep = path.join(agentDir, 'skills');
1950
+ if (!userPassedSelection && fs.existsSync(skillsTargetSweep) && !fs.lstatSync(skillsTargetSweep).isSymbolicLink()) {
1951
+ const trustedSkills = new Set(skillsToSync);
1952
+ for (const entry of fs.readdirSync(skillsTargetSweep, { withFileTypes: true })) {
1953
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
1954
+ continue;
1955
+ if (!trustedSkills.has(entry.name)) {
1956
+ removePath(safeJoin(skillsTargetSweep, entry.name));
1957
+ }
1958
+ }
1959
+ }
1693
1960
  }
1694
1961
  // Sync hooks (if agent supports them at this version)
1695
1962
  const hooksGate = supports(agent, 'hooks', version);
@@ -1742,7 +2009,9 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1742
2009
  result.hooks = syncedHooks.length > 0;
1743
2010
  // Register hooks into agent-native settings.json/hooks.json. Gemini
1744
2011
  // shipped hooks in 0.26.0; gate already passed above so this is safe.
1745
- if (agent === 'claude' || agent === 'codex' || agent === 'gemini') {
2012
+ // Grok auto-discovers from ~/.grok/hooks/ so the script copy above
2013
+ // is sufficient — no settings.json registration needed.
2014
+ if (agent === 'claude' || agent === 'codex' || agent === 'gemini' || agent === 'antigravity') {
1746
2015
  registerHooksToSettings(agent, versionHome);
1747
2016
  }
1748
2017
  }
@@ -1781,6 +2050,10 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1781
2050
  // ~/.agents/permissions/presets/<name>.yaml pick a subset via `includes:`.
1782
2051
  // If AGENTS_PERMISSION_PRESET is set, we resolve that recipe and use its
1783
2052
  // includes list as the group filter (intersected with groups on disk).
2053
+ // Note: discoverPermissionGroups intentionally reads from user + system
2054
+ // only — never from a project's .agents/permissions/. Permissions gate
2055
+ // every other action, so a cloned public repo must not be able to widen
2056
+ // its own sandbox by shipping a permissions group. Same defense as hooks.
1784
2057
  const permissionGroups = discoverPermissionGroups();
1785
2058
  const allGroupNames = permissionGroups.map(g => g.name);
1786
2059
  const activePresetName = getActivePermissionPresetName();
@@ -1825,15 +2098,30 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1825
2098
  // Install MCP servers (if agent supports them)
1826
2099
  // For Claude/Codex: uses CLI commands (claude mcp add, codex mcp add)
1827
2100
  // For others: edits config files directly
1828
- const mcpToSync = selection
2101
+ //
2102
+ // Mirror the hooks defense: exclude project-scoped MCPs from the sync. An
2103
+ // MCP server is an executable invoked under the agent's authority, so a
2104
+ // cloned public repo's .agents/mcp/foo.yaml could install an arbitrary
2105
+ // command. We pre-compute the set of project-scoped names and drop them
2106
+ // before handing the list to installMcpServers. (The deeper helper-side
2107
+ // dedup in lib/mcp.ts still lets a project entry shadow a same-named
2108
+ // user entry, so name-collision shadowing is not fully closed here —
2109
+ // tracked separately for a follow-up in lib/mcp.ts.)
2110
+ const projectScopedMcpNames = new Set(getScopedMcpResources(cwd).filter(r => r.scope === 'project').map(r => r.name));
2111
+ const mcpToSyncAll = selection
1829
2112
  ? resolveSelection(selection.mcp, available.mcp)
1830
2113
  : (MCP_CAPABLE_AGENTS.includes(agent) ? available.mcp : []);
2114
+ const mcpToSync = mcpToSyncAll.filter(n => !projectScopedMcpNames.has(n));
1831
2115
  if (mcpToSync.length > 0 && MCP_CAPABLE_AGENTS.includes(agent)) {
1832
2116
  const mcpResult = installMcpServers(agent, version, versionHome, mcpToSync, { cwd });
1833
2117
  result.mcp = mcpResult.applied;
1834
2118
  // mcp patterns already written via ensureVersionResourcePatterns above.
1835
2119
  }
1836
- // Sync subagents (claude and openclaw only)
2120
+ // Sync subagents (claude and openclaw only).
2121
+ // Note: listInstalledSubagents (used to populate the map below) reads only
2122
+ // user + system layers — never project. Subagents bundle prompts that fire
2123
+ // when the agent delegates work, so a cloned public repo must not be able
2124
+ // to plant a subagent the user later invokes. Same defense as hooks.
1837
2125
  const subagentsToSync = selection
1838
2126
  ? resolveSelection(selection.subagents, available.subagents)
1839
2127
  : (SUBAGENT_CAPABLE_AGENTS.includes(agent) ? available.subagents : []);
@@ -1864,6 +2152,29 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1864
2152
  }
1865
2153
  catch { /* resource sync failed for this item */ }
1866
2154
  }
2155
+ // Orphan-sweep stale subagents. Same selection-mode guard as the
2156
+ // commands/skills sweeps above. Claude stores them as flat .md files
2157
+ // under `<agentDir>/agents/`; OpenClaw stores them as subdirs at the
2158
+ // same level as commands/skills/hooks/plugins (no isolated parent dir),
2159
+ // which means a directory-readdir sweep would unsafely hit unrelated
2160
+ // resources. For OpenClaw we lean on the existing per-name copy path —
2161
+ // if the user wants strict isolation on OpenClaw, track via manifest.
2162
+ if (!userPassedSelection && agent === 'claude') {
2163
+ const claudeAgentsDir = path.join(agentDir, 'agents');
2164
+ if (fs.existsSync(claudeAgentsDir)) {
2165
+ const trustedSubagents = new Set(subagentsToSync);
2166
+ for (const entry of fs.readdirSync(claudeAgentsDir, { withFileTypes: true })) {
2167
+ if (!entry.isFile() || entry.name.startsWith('.'))
2168
+ continue;
2169
+ if (!entry.name.endsWith('.md'))
2170
+ continue;
2171
+ const name = entry.name.slice(0, -'.md'.length);
2172
+ if (!trustedSubagents.has(name)) {
2173
+ removePath(safeJoin(claudeAgentsDir, entry.name));
2174
+ }
2175
+ }
2176
+ }
2177
+ }
1867
2178
  // subagent patterns already written via ensureVersionResourcePatterns above.
1868
2179
  }
1869
2180
  // Sync plugins (claude and openclaw)
@@ -1929,6 +2240,24 @@ export function getEffectiveHome(agentId) {
1929
2240
  }
1930
2241
  return os.homedir();
1931
2242
  }
2243
+ /**
2244
+ * Thrown when the user references an agent@version that is not installed.
2245
+ * Carries the parsed (agentId, version) so callers can react — e.g. prompt
2246
+ * to install it on demand — without having to parse the error message.
2247
+ */
2248
+ export class VersionNotInstalledError extends Error {
2249
+ agentId;
2250
+ version;
2251
+ installedVersions;
2252
+ constructor(agentId, version, installedVersions) {
2253
+ const installed = installedVersions.length > 0 ? installedVersions.join(', ') : '(none)';
2254
+ super(`Version ${version} is not installed for ${AGENTS[agentId].name}. Installed versions: ${installed}`);
2255
+ this.agentId = agentId;
2256
+ this.version = version;
2257
+ this.installedVersions = installedVersions;
2258
+ this.name = 'VersionNotInstalledError';
2259
+ }
2260
+ }
1932
2261
  /**
1933
2262
  * Resolve a comma-separated --agents list into concrete version selections.
1934
2263
  * Bare agents target the default version, or the newest installed version when no default exists.
@@ -1938,10 +2267,26 @@ export function resolveAgentVersionTargets(value, availableAgents, options = {})
1938
2267
  const selectedAgents = [];
1939
2268
  const versionSelections = new Map();
1940
2269
  const explicitSelections = new Set();
1941
- const targets = value
2270
+ const rawTargets = value
1942
2271
  .split(',')
1943
2272
  .map((item) => item.trim())
1944
2273
  .filter(Boolean);
2274
+ // Expand literal `all` (with optional @all) into every available agent's all
2275
+ // installed versions. Skip agents with no installed versions so `all` is
2276
+ // lenient — only explicit `claude@all` errors when claude isn't installed.
2277
+ const targets = [];
2278
+ for (const t of rawTargets) {
2279
+ if (t === 'all' || t === 'all@all') {
2280
+ for (const a of availableAgents) {
2281
+ if (listInstalledVersions(a).length > 0) {
2282
+ targets.push(`${a}@all`);
2283
+ }
2284
+ }
2285
+ }
2286
+ else {
2287
+ targets.push(t);
2288
+ }
2289
+ }
1945
2290
  for (const target of targets) {
1946
2291
  const atIndex = target.indexOf('@');
1947
2292
  const agentToken = (atIndex === -1 ? target : target.slice(0, atIndex)).trim();
@@ -1950,7 +2295,7 @@ export function resolveAgentVersionTargets(value, availableAgents, options = {})
1950
2295
  continue;
1951
2296
  }
1952
2297
  if (atIndex !== -1 && !versionToken) {
1953
- throw new Error(`Missing version in --agents entry '${target}'. Use agent@x.y.z or agent@default.`);
2298
+ throw new Error(`Missing version in --agents entry '${target}'. Use agent@x.y.z, agent@default, or agent@all.`);
1954
2299
  }
1955
2300
  const agentId = resolveAgentName(agentToken);
1956
2301
  if (!agentId || !availableAgents.includes(agentId)) {
@@ -1990,8 +2335,13 @@ export function resolveAgentVersionTargets(value, availableAgents, options = {})
1990
2335
  explicitSelections.add(agentId);
1991
2336
  continue;
1992
2337
  }
2338
+ if (versionToken === 'all') {
2339
+ versionSelections.set(agentId, [...installedVersions]);
2340
+ explicitSelections.add(agentId);
2341
+ continue;
2342
+ }
1993
2343
  if (!installedVersions.includes(versionToken)) {
1994
- throw new Error(`Version ${versionToken} is not installed for ${AGENTS[agentId].name}. Installed versions: ${installedVersions.join(', ')}`);
2344
+ throw new VersionNotInstalledError(agentId, versionToken, installedVersions);
1995
2345
  }
1996
2346
  const explicitVersions = explicitSelections.has(agentId)
1997
2347
  ? (versionSelections.get(agentId) || [])
@@ -2014,10 +2364,28 @@ export function resolveInstalledAgentTargets(value, availableAgents, options = {
2014
2364
  const selectedAgents = [];
2015
2365
  const directAgents = [];
2016
2366
  const versionSelections = new Map();
2017
- const targets = value
2367
+ const rawTargets = value
2018
2368
  .split(',')
2019
2369
  .map((item) => item.trim())
2020
2370
  .filter(Boolean);
2371
+ // Expand literal `all` (with optional @all) into every available agent's all
2372
+ // installed versions. Skip agents with no installed versions so `all` is
2373
+ // lenient — only explicit `claude@all` errors when claude isn't installed.
2374
+ // Mirrors resolveAgentVersionTargets so every --agents flag site supports
2375
+ // the same selector syntax.
2376
+ const targets = [];
2377
+ for (const t of rawTargets) {
2378
+ if (t === 'all' || t === 'all@all') {
2379
+ for (const a of availableAgents) {
2380
+ if (listInstalledVersions(a).length > 0) {
2381
+ targets.push(`${a}@all`);
2382
+ }
2383
+ }
2384
+ }
2385
+ else {
2386
+ targets.push(t);
2387
+ }
2388
+ }
2021
2389
  const addVersionTarget = (agentId, version) => {
2022
2390
  const versions = versionSelections.get(agentId) || [];
2023
2391
  if (!versions.includes(version)) {
@@ -2037,7 +2405,7 @@ export function resolveInstalledAgentTargets(value, availableAgents, options = {
2037
2405
  continue;
2038
2406
  }
2039
2407
  if (atIndex !== -1 && !versionToken) {
2040
- throw new Error(`Missing version in --agents entry '${target}'. Use agent@x.y.z or agent@default.`);
2408
+ throw new Error(`Missing version in --agents entry '${target}'. Use agent@x.y.z, agent@default, or agent@all.`);
2041
2409
  }
2042
2410
  const agentId = resolveAgentName(agentToken);
2043
2411
  if (!agentId || !availableAgents.includes(agentId)) {
@@ -2070,11 +2438,20 @@ export function resolveInstalledAgentTargets(value, availableAgents, options = {
2070
2438
  addVersionTarget(agentId, defaultVersion);
2071
2439
  continue;
2072
2440
  }
2441
+ if (versionToken === 'all') {
2442
+ if (installedVersions.length === 0) {
2443
+ throw new Error(`No managed versions are installed for ${AGENTS[agentId].name}. Run: agents add ${agentId}@latest`);
2444
+ }
2445
+ for (const version of installedVersions) {
2446
+ addVersionTarget(agentId, version);
2447
+ }
2448
+ continue;
2449
+ }
2073
2450
  if (installedVersions.length === 0) {
2074
2451
  throw new Error(`No managed versions are installed for ${AGENTS[agentId].name}. Run: agents add ${agentId}@latest`);
2075
2452
  }
2076
2453
  if (!installedVersions.includes(versionToken)) {
2077
- throw new Error(`Version ${versionToken} is not installed for ${AGENTS[agentId].name}. Installed versions: ${installedVersions.join(', ')}`);
2454
+ throw new VersionNotInstalledError(agentId, versionToken, installedVersions);
2078
2455
  }
2079
2456
  addVersionTarget(agentId, versionToken);
2080
2457
  }
@@ -2128,9 +2505,15 @@ export async function promptAgentVersionSelection(availableAgents, options = {})
2128
2505
  const defaultVer = getGlobalDefault(agentId);
2129
2506
  if (versions.length === 0)
2130
2507
  return `${AGENTS[agentId].name} ${chalk.gray('(not installed)')}`;
2131
- if (defaultVer)
2132
- return `${AGENTS[agentId].name} ${chalk.gray(`(active: ${defaultVer})`)}`;
2133
- return `${AGENTS[agentId].name} ${chalk.gray(`(${versions[0]})`)}`;
2508
+ // Surface the version count when there's more than one — mirrors the new
2509
+ // `--agents <agent>@all` syntax so users can see at a glance how many
2510
+ // versions `@all` would target before the per-version prompt fires.
2511
+ const detail = versions.length > 1
2512
+ ? (defaultVer
2513
+ ? `active: ${defaultVer}, ${versions.length} versions installed`
2514
+ : `${versions.length} versions installed`)
2515
+ : (defaultVer ?? versions[0]);
2516
+ return `${AGENTS[agentId].name} ${chalk.gray(`(${detail})`)}`;
2134
2517
  };
2135
2518
  let selectedAgents;
2136
2519
  if (options.skipPrompts) {
@@ -2145,6 +2528,18 @@ export async function promptAgentVersionSelection(availableAgents, options = {})
2145
2528
  }
2146
2529
  }
2147
2530
  else {
2531
+ // Non-TTY without an explicit --agents value used to silently fall through
2532
+ // to default-picking inside the caller. That's surprising in scripts — fail
2533
+ // loud and point at the new `--agents` syntax instead.
2534
+ if (!(process.stdin.isTTY && process.stdout.isTTY)) {
2535
+ throw new Error('Non-interactive shell: cannot prompt for agent/version selection.\n' +
2536
+ 'Pass --agents explicitly. Examples:\n' +
2537
+ ' --agents claude (default version)\n' +
2538
+ ' --agents claude@all (every installed Claude version)\n' +
2539
+ ' --agents claude@2.1.141 (a specific version)\n' +
2540
+ ' --agents all (every installed version of every capable agent)\n' +
2541
+ 'Or pass --yes to auto-pick defaults.');
2542
+ }
2148
2543
  // Prompt for agent selection
2149
2544
  const checkboxResult = await checkbox({
2150
2545
  message: 'Which agents should receive these resources?',
@@ -2179,7 +2574,7 @@ export async function promptAgentVersionSelection(availableAgents, options = {})
2179
2574
  const versionResult = await checkbox({
2180
2575
  message: `Which versions of ${AGENTS[agentId].name} should receive these resources?`,
2181
2576
  choices: [
2182
- { name: chalk.bold('All versions'), value: 'all', checked: false },
2577
+ { name: chalk.bold(`All versions (${versions.length})`), value: 'all', checked: false },
2183
2578
  ...versions.map((v) => {
2184
2579
  const base = v === defaultVer ? `${v} (default)` : v;
2185
2580
  let label = base.padEnd(maxLabelLen);