@phnx-labs/agents-cli 1.20.0 → 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 (105) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +4 -4
  3. package/dist/commands/cli.js +3 -3
  4. package/dist/commands/cloud.js +1 -1
  5. package/dist/commands/commands.js +24 -7
  6. package/dist/commands/exec.js +36 -16
  7. package/dist/commands/feedback.d.ts +7 -0
  8. package/dist/commands/feedback.js +89 -0
  9. package/dist/commands/helper.d.ts +12 -0
  10. package/dist/commands/helper.js +87 -0
  11. package/dist/commands/hooks.js +86 -7
  12. package/dist/commands/mcp.js +166 -10
  13. package/dist/commands/packages.js +196 -27
  14. package/dist/commands/permissions.js +21 -6
  15. package/dist/commands/profiles.d.ts +8 -0
  16. package/dist/commands/profiles.js +117 -4
  17. package/dist/commands/pull.js +4 -4
  18. package/dist/commands/routines.js +6 -6
  19. package/dist/commands/rules.js +8 -4
  20. package/dist/commands/secrets-migrate.d.ts +24 -0
  21. package/dist/commands/secrets-migrate.js +198 -0
  22. package/dist/commands/secrets-sync.d.ts +11 -0
  23. package/dist/commands/secrets-sync.js +155 -0
  24. package/dist/commands/secrets.js +74 -39
  25. package/dist/commands/skills.js +22 -5
  26. package/dist/commands/subagents.js +69 -49
  27. package/dist/commands/teams.js +48 -10
  28. package/dist/commands/utils.d.ts +33 -0
  29. package/dist/commands/utils.js +139 -0
  30. package/dist/commands/versions.js +4 -4
  31. package/dist/commands/view.d.ts +6 -0
  32. package/dist/commands/view.js +164 -8
  33. package/dist/commands/workflows.js +29 -6
  34. package/dist/index.js +4 -0
  35. package/dist/lib/acp/client.js +6 -1
  36. package/dist/lib/agents.d.ts +4 -0
  37. package/dist/lib/agents.js +18 -14
  38. package/dist/lib/auto-pull-worker.js +18 -1
  39. package/dist/lib/browser/chrome.js +4 -0
  40. package/dist/lib/browser/drivers/ssh.js +1 -1
  41. package/dist/lib/browser/profiles.d.ts +3 -3
  42. package/dist/lib/browser/profiles.js +3 -3
  43. package/dist/lib/browser/service.js +19 -0
  44. package/dist/lib/browser/types.d.ts +4 -4
  45. package/dist/lib/cli-resources.d.ts +36 -8
  46. package/dist/lib/cli-resources.js +268 -46
  47. package/dist/lib/cloud/factory.d.ts +1 -1
  48. package/dist/lib/cloud/factory.js +1 -1
  49. package/dist/lib/events.d.ts +16 -2
  50. package/dist/lib/events.js +33 -2
  51. package/dist/lib/exec.d.ts +39 -11
  52. package/dist/lib/exec.js +90 -31
  53. package/dist/lib/help.js +11 -5
  54. package/dist/lib/hooks/cache.d.ts +38 -0
  55. package/dist/lib/hooks/cache.js +242 -0
  56. package/dist/lib/hooks/profile.d.ts +33 -0
  57. package/dist/lib/hooks/profile.js +129 -0
  58. package/dist/lib/hooks.d.ts +0 -10
  59. package/dist/lib/hooks.js +68 -15
  60. package/dist/lib/mcp.d.ts +15 -0
  61. package/dist/lib/mcp.js +40 -0
  62. package/dist/lib/permissions.d.ts +13 -0
  63. package/dist/lib/permissions.js +51 -1
  64. package/dist/lib/plugins.js +15 -1
  65. package/dist/lib/profiles-presets.d.ts +26 -0
  66. package/dist/lib/profiles-presets.js +187 -8
  67. package/dist/lib/profiles.d.ts +34 -0
  68. package/dist/lib/profiles.js +112 -1
  69. package/dist/lib/routines-format.d.ts +17 -5
  70. package/dist/lib/routines-format.js +37 -16
  71. package/dist/lib/routines.d.ts +1 -1
  72. package/dist/lib/routines.js +2 -2
  73. package/dist/lib/runner.js +64 -10
  74. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  75. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  76. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
  77. package/dist/lib/secrets/bundles.d.ts +18 -22
  78. package/dist/lib/secrets/bundles.js +75 -99
  79. package/dist/lib/secrets/index.d.ts +51 -27
  80. package/dist/lib/secrets/index.js +147 -156
  81. package/dist/lib/secrets/install-helper.d.ts +45 -0
  82. package/dist/lib/secrets/install-helper.js +165 -0
  83. package/dist/lib/secrets/linux.js +4 -4
  84. package/dist/lib/secrets/sync.d.ts +56 -0
  85. package/dist/lib/secrets/sync.js +180 -0
  86. package/dist/lib/session/render.js +4 -4
  87. package/dist/lib/session/types.d.ts +1 -1
  88. package/dist/lib/shims.d.ts +4 -1
  89. package/dist/lib/shims.js +5 -35
  90. package/dist/lib/state.d.ts +14 -1
  91. package/dist/lib/state.js +49 -5
  92. package/dist/lib/teams/agents.d.ts +5 -4
  93. package/dist/lib/teams/agents.js +47 -21
  94. package/dist/lib/teams/api.d.ts +2 -1
  95. package/dist/lib/teams/api.js +4 -3
  96. package/dist/lib/types.d.ts +57 -1
  97. package/dist/lib/types.js +2 -0
  98. package/dist/lib/usage.d.ts +27 -2
  99. package/dist/lib/usage.js +100 -17
  100. package/dist/lib/versions.d.ts +35 -1
  101. package/dist/lib/versions.js +267 -64
  102. package/package.json +9 -8
  103. package/scripts/install-helper.js +97 -0
  104. package/scripts/postinstall.js +16 -0
  105. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
@@ -511,16 +511,130 @@ export function getActuallySyncedResources(agent, version, options = {}) {
511
511
  }
512
512
  return result;
513
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
+ }
514
619
  /**
515
620
  * Compare available resources with what's ACTUALLY synced to version home.
516
621
  * Returns only NEW resources that haven't been synced yet.
517
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.
518
628
  */
519
- 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
+ };
520
634
  return {
521
- commands: available.commands.filter(c => !actuallySynced.commands.includes(c)),
522
- skills: available.skills.filter(s => !actuallySynced.skills.includes(s)),
523
- 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)),
524
638
  // Memory/rules presets are mutually exclusive — only one can be active.
525
639
  // If any preset is synced, don't report others as "new".
526
640
  memory: actuallySynced.memory.length > 0
@@ -528,9 +642,9 @@ export function getNewResources(available, actuallySynced) {
528
642
  : available.memory.filter(m => !actuallySynced.memory.includes(m)),
529
643
  mcp: available.mcp.filter(m => !actuallySynced.mcp.includes(m)),
530
644
  permissions: available.permissions.filter(p => !actuallySynced.permissions.includes(p)),
531
- subagents: available.subagents.filter(s => !actuallySynced.subagents.includes(s)),
532
- plugins: available.plugins.filter(p => !actuallySynced.plugins.includes(p)),
533
- 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)),
534
648
  // Promptcuts aren't version-scoped — the hook reads ~/.agents/promptcuts.yaml
535
649
  // directly, so there is never a "new" per-version state to reconcile.
536
650
  promptcuts: false,
@@ -1027,56 +1141,50 @@ export function setGlobalDefault(agent, version) {
1027
1141
  */
1028
1142
  export async function installVersion(agent, version, onProgress) {
1029
1143
  const agentConfig = AGENTS[agent];
1030
- if (!agentConfig.npmPackage) {
1031
- // Support agents that provide an installScript (cursor, roo, goose, kiro, grok, etc.)
1032
- if (agentConfig.installScript) {
1033
- // For grok we give special love because the user asked for first-class support
1034
- if (agent === 'grok') {
1035
- onProgress?.(`Installing Grok ${version} via official installer...`);
1036
- try {
1037
- const script = agentConfig.installScript.replace('VERSION', version);
1038
- // The official installer supports -s <version>
1039
- const { exec } = await import('child_process');
1040
- const { promisify } = await import('util');
1041
- const execAsync = promisify(exec);
1042
- await execAsync(script, { timeout: 120000 });
1043
- onProgress?.('Grok binary installed. Setting up agents-cli version home for isolation...');
1044
- }
1045
- catch (err) {
1046
- return { success: false, installedVersion: version, error: `Grok installer failed: ${err.message}` };
1047
- }
1048
- }
1049
- else {
1050
- return {
1051
- success: false,
1052
- installedVersion: version,
1053
- error: `${agent} uses an external installer. Run: ${agentConfig.installScript}`,
1054
- };
1055
- }
1056
- }
1057
- else {
1058
- return { success: false, installedVersion: version, error: 'Agent has no npm package' };
1059
- }
1060
- }
1061
1144
  // Validate before deriving filesystem paths or npm package specs. The CLI
1062
1145
  // parser already enforces this for user input; this guard protects direct
1063
1146
  // callers and tests the critical install path at the source.
1064
1147
  if (!VERSION_RE.test(version)) {
1065
1148
  throw new Error(`Invalid version: ${JSON.stringify(version)}`);
1066
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
+ }
1067
1183
  ensureAgentsDir();
1068
1184
  const versionDir = getVersionDir(agent, version);
1069
1185
  // Create version directory and isolated home
1070
1186
  fs.mkdirSync(versionDir, { recursive: true });
1071
1187
  fs.mkdirSync(path.join(versionDir, 'home'), { recursive: true });
1072
- // For agents using external installers (especially grok), we don't do npm install.
1073
- // The binary is managed by the agent's own installer; we only manage the isolated home + resources.
1074
- if (agent === 'grok' || !agentConfig.npmPackage) {
1075
- // Grok (and similar) — binary already installed by the step above (or user ran external installer).
1076
- // We still want to record the version for config isolation.
1077
- createVersionedAlias(agent, version);
1078
- return { success: true, installedVersion: version };
1079
- }
1080
1188
  // Initialize package.json (only for real npm agents)
1081
1189
  const packageJson = {
1082
1190
  name: `agents-${agent}-${version}`,
@@ -1408,6 +1516,18 @@ export async function getInstalledVersion(agent, version) {
1408
1516
  return version;
1409
1517
  }
1410
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
+ }
1411
1531
  /**
1412
1532
  * Get the diff between central resources (~/.agents/) and what's synced to a version.
1413
1533
  * Uses filesystem state - no tracking needed.
@@ -1646,12 +1766,11 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1646
1766
  }
1647
1767
  }
1648
1768
  }
1649
- // Fast guard: skip the entire sync when no selection is active and nothing
1650
- // has changed since the last full sync. Drops steady-state cost from ~16s
1651
- // (unconditional file copies) to ~8-15ms (`isStale` walks the manifest's
1652
- // fingerprints, short-circuiting at the first mismatch). Numbers from
1653
- // scripts/bench-staleness.ts against a real ~50-resource project.
1654
- 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) {
1655
1774
  const manifest = loadManifest(agent, version);
1656
1775
  if (manifest && !isStale(manifest, agent, version, cwd)) {
1657
1776
  return { commands: false, skills: false, hooks: false, memory: [], permissions: false, mcp: [], subagents: [], plugins: [], workflows: [] };
@@ -1758,8 +1877,8 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1758
1877
  // not pass an explicit `selection`. Callers that pass explicit selections
1759
1878
  // are using the incremental/additive API (sync exactly these; leave others
1760
1879
  // alone), so the sweep would be a contract violation there. The
1761
- // cross-project leak fixed by RUSH-670 always comes from the no-selection
1762
- // shim auto-sync at launch.
1880
+ // cross-project leak always comes from the no-selection shim auto-sync at
1881
+ // launch.
1763
1882
  if (!userPassedSelection && COMMANDS_CAPABLE_AGENTS.includes(agent) && !shouldInstallCommandAsSkill(agent, version)) {
1764
1883
  const commandsTargetSweep = path.join(agentDir, agentConfig.commandsSubdir);
1765
1884
  if (fs.existsSync(commandsTargetSweep)) {
@@ -2121,6 +2240,24 @@ export function getEffectiveHome(agentId) {
2121
2240
  }
2122
2241
  return os.homedir();
2123
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
+ }
2124
2261
  /**
2125
2262
  * Resolve a comma-separated --agents list into concrete version selections.
2126
2263
  * Bare agents target the default version, or the newest installed version when no default exists.
@@ -2130,10 +2267,26 @@ export function resolveAgentVersionTargets(value, availableAgents, options = {})
2130
2267
  const selectedAgents = [];
2131
2268
  const versionSelections = new Map();
2132
2269
  const explicitSelections = new Set();
2133
- const targets = value
2270
+ const rawTargets = value
2134
2271
  .split(',')
2135
2272
  .map((item) => item.trim())
2136
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
+ }
2137
2290
  for (const target of targets) {
2138
2291
  const atIndex = target.indexOf('@');
2139
2292
  const agentToken = (atIndex === -1 ? target : target.slice(0, atIndex)).trim();
@@ -2142,7 +2295,7 @@ export function resolveAgentVersionTargets(value, availableAgents, options = {})
2142
2295
  continue;
2143
2296
  }
2144
2297
  if (atIndex !== -1 && !versionToken) {
2145
- 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.`);
2146
2299
  }
2147
2300
  const agentId = resolveAgentName(agentToken);
2148
2301
  if (!agentId || !availableAgents.includes(agentId)) {
@@ -2182,8 +2335,13 @@ export function resolveAgentVersionTargets(value, availableAgents, options = {})
2182
2335
  explicitSelections.add(agentId);
2183
2336
  continue;
2184
2337
  }
2338
+ if (versionToken === 'all') {
2339
+ versionSelections.set(agentId, [...installedVersions]);
2340
+ explicitSelections.add(agentId);
2341
+ continue;
2342
+ }
2185
2343
  if (!installedVersions.includes(versionToken)) {
2186
- throw new Error(`Version ${versionToken} is not installed for ${AGENTS[agentId].name}. Installed versions: ${installedVersions.join(', ')}`);
2344
+ throw new VersionNotInstalledError(agentId, versionToken, installedVersions);
2187
2345
  }
2188
2346
  const explicitVersions = explicitSelections.has(agentId)
2189
2347
  ? (versionSelections.get(agentId) || [])
@@ -2206,10 +2364,28 @@ export function resolveInstalledAgentTargets(value, availableAgents, options = {
2206
2364
  const selectedAgents = [];
2207
2365
  const directAgents = [];
2208
2366
  const versionSelections = new Map();
2209
- const targets = value
2367
+ const rawTargets = value
2210
2368
  .split(',')
2211
2369
  .map((item) => item.trim())
2212
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
+ }
2213
2389
  const addVersionTarget = (agentId, version) => {
2214
2390
  const versions = versionSelections.get(agentId) || [];
2215
2391
  if (!versions.includes(version)) {
@@ -2229,7 +2405,7 @@ export function resolveInstalledAgentTargets(value, availableAgents, options = {
2229
2405
  continue;
2230
2406
  }
2231
2407
  if (atIndex !== -1 && !versionToken) {
2232
- 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.`);
2233
2409
  }
2234
2410
  const agentId = resolveAgentName(agentToken);
2235
2411
  if (!agentId || !availableAgents.includes(agentId)) {
@@ -2262,11 +2438,20 @@ export function resolveInstalledAgentTargets(value, availableAgents, options = {
2262
2438
  addVersionTarget(agentId, defaultVersion);
2263
2439
  continue;
2264
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
+ }
2265
2450
  if (installedVersions.length === 0) {
2266
2451
  throw new Error(`No managed versions are installed for ${AGENTS[agentId].name}. Run: agents add ${agentId}@latest`);
2267
2452
  }
2268
2453
  if (!installedVersions.includes(versionToken)) {
2269
- throw new Error(`Version ${versionToken} is not installed for ${AGENTS[agentId].name}. Installed versions: ${installedVersions.join(', ')}`);
2454
+ throw new VersionNotInstalledError(agentId, versionToken, installedVersions);
2270
2455
  }
2271
2456
  addVersionTarget(agentId, versionToken);
2272
2457
  }
@@ -2320,9 +2505,15 @@ export async function promptAgentVersionSelection(availableAgents, options = {})
2320
2505
  const defaultVer = getGlobalDefault(agentId);
2321
2506
  if (versions.length === 0)
2322
2507
  return `${AGENTS[agentId].name} ${chalk.gray('(not installed)')}`;
2323
- if (defaultVer)
2324
- return `${AGENTS[agentId].name} ${chalk.gray(`(active: ${defaultVer})`)}`;
2325
- 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})`)}`;
2326
2517
  };
2327
2518
  let selectedAgents;
2328
2519
  if (options.skipPrompts) {
@@ -2337,6 +2528,18 @@ export async function promptAgentVersionSelection(availableAgents, options = {})
2337
2528
  }
2338
2529
  }
2339
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
+ }
2340
2543
  // Prompt for agent selection
2341
2544
  const checkboxResult = await checkbox({
2342
2545
  message: 'Which agents should receive these resources?',
@@ -2371,7 +2574,7 @@ export async function promptAgentVersionSelection(availableAgents, options = {})
2371
2574
  const versionResult = await checkbox({
2372
2575
  message: `Which versions of ${AGENTS[agentId].name} should receive these resources?`,
2373
2576
  choices: [
2374
- { name: chalk.bold('All versions'), value: 'all', checked: false },
2577
+ { name: chalk.bold(`All versions (${versions.length})`), value: 'all', checked: false },
2375
2578
  ...versions.map((v) => {
2376
2579
  const base = v === defaultVer ? `${v} (default)` : v;
2377
2580
  let label = base.padEnd(maxLabelLen);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.0",
3
+ "version": "1.20.3",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -25,12 +25,13 @@
25
25
  "dist/**/*.d.ts",
26
26
  "dist/lib/secrets/Agents CLI.app/**",
27
27
  "scripts/postinstall.js",
28
+ "scripts/install-helper.js",
28
29
  "CHANGELOG.md",
29
30
  "README.md",
30
31
  "LICENSE"
31
32
  ],
32
33
  "publishConfig": {
33
- "provenance": true
34
+ "provenance": false
34
35
  },
35
36
  "repository": {
36
37
  "type": "git",
@@ -78,8 +79,8 @@
78
79
  "@xterm/headless": "6.0.0",
79
80
  "@zed-industries/agent-client-protocol": "0.4.5",
80
81
  "chalk": "5.6.2",
81
- "commander": "12.1.0",
82
- "croner": "9.1.0",
82
+ "commander": "15.0.0",
83
+ "croner": "10.0.1",
83
84
  "diff": "9.0.0",
84
85
  "marked": "15.0.12",
85
86
  "marked-terminal": "7.3.0",
@@ -88,14 +89,14 @@
88
89
  "proper-lockfile": "4.1.2",
89
90
  "simple-git": "3.36.0",
90
91
  "smol-toml": "1.6.1",
91
- "yaml": "2.8.3"
92
+ "yaml": "2.9.0"
92
93
  },
93
94
  "devDependencies": {
94
- "@types/diff": "6.0.0",
95
+ "@types/diff": "8.0.0",
95
96
  "@types/marked-terminal": "6.1.1",
96
- "@types/node": "22.19.10",
97
+ "@types/node": "25.9.1",
97
98
  "tsx": "4.22.3",
98
- "typescript": "5.9.3",
99
+ "typescript": "6.0.3",
99
100
  "vitest": "4.1.6"
100
101
  }
101
102
  }
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copy the bundled `Agents CLI.app` to a stable user-scoped path so its
4
+ * keychain ACLs survive future npm installs/updates.
5
+ *
6
+ * Source: dist/lib/secrets/Agents CLI.app (in the installed npm package)
7
+ * bin/Agents CLI.app (in a raw working tree)
8
+ * Destination: ~/Library/Application Support/agents-cli/Agents CLI.app
9
+ *
10
+ * Invoked by scripts/postinstall.js on global install, and by
11
+ * scripts/install.sh for dev installs. Pure Node, no agents-cli imports,
12
+ * so it runs before the package's TS is wired up.
13
+ *
14
+ * macOS only — silently no-ops on other platforms so postinstall stays clean.
15
+ */
16
+
17
+ import { spawnSync } from 'child_process';
18
+ import * as fs from 'fs';
19
+ import * as os from 'os';
20
+ import * as path from 'path';
21
+ import { fileURLToPath } from 'url';
22
+
23
+ const APP_BUNDLE_NAME = 'Agents CLI.app';
24
+ const INSTALL_DIR_NAME = 'agents-cli';
25
+
26
+ function destAppPath() {
27
+ return path.join(os.homedir(), 'Library', 'Application Support', INSTALL_DIR_NAME, APP_BUNDLE_NAME);
28
+ }
29
+
30
+ function findSourceApp() {
31
+ const here = path.dirname(fileURLToPath(import.meta.url));
32
+ // From scripts/install-helper.js, look in the installed npm layout first
33
+ // (../dist/lib/secrets/...) then a raw repo layout (../bin/...).
34
+ const candidates = [
35
+ path.resolve(here, '..', 'dist', 'lib', 'secrets', APP_BUNDLE_NAME),
36
+ path.resolve(here, '..', 'bin', APP_BUNDLE_NAME),
37
+ ];
38
+ for (const c of candidates) {
39
+ if (fs.existsSync(c)) return c;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ function codesignVerify(appPath) {
45
+ const r = spawnSync('codesign', ['--verify', '--deep', '--strict', appPath], {
46
+ stdio: ['ignore', 'pipe', 'pipe'],
47
+ encoding: 'utf-8',
48
+ });
49
+ return { ok: r.status === 0, output: (r.stderr || r.stdout || '').toString().trim() };
50
+ }
51
+
52
+ function copyAppBundle(src, dest) {
53
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
54
+ if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
55
+ const r = spawnSync('cp', ['-R', src, dest], { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8' });
56
+ if (r.status !== 0) {
57
+ const msg = (r.stderr || r.stdout || '').toString().trim();
58
+ throw new Error(`Failed to copy ${src} -> ${dest}: ${msg || 'unknown error'}`);
59
+ }
60
+ }
61
+
62
+ function main() {
63
+ if (process.platform !== 'darwin') return;
64
+ const force = process.argv.includes('--force');
65
+ const dest = destAppPath();
66
+
67
+ if (!force && fs.existsSync(dest) && codesignVerify(dest).ok) {
68
+ return;
69
+ }
70
+
71
+ const src = findSourceApp();
72
+ if (!src) {
73
+ // No source bundle to install. Stay silent during postinstall so we don't
74
+ // create noise on Linux/Windows or on builds that intentionally omit the
75
+ // helper. `agents helper install` surfaces a clearer error if a user
76
+ // actually needs the helper.
77
+ return;
78
+ }
79
+
80
+ try {
81
+ copyAppBundle(src, dest);
82
+ } catch (err) {
83
+ process.stderr.write(`agents-cli: failed to install Keychain helper: ${err.message}\n`);
84
+ return;
85
+ }
86
+
87
+ const verify = codesignVerify(dest);
88
+ if (!verify.ok) {
89
+ process.stderr.write(
90
+ `agents-cli: installed helper failed codesign verification at ${dest}\n${verify.output}\n`
91
+ );
92
+ return;
93
+ }
94
+ process.stdout.write(` Installed Keychain helper: ${dest}\n`);
95
+ }
96
+
97
+ main();
@@ -7,6 +7,7 @@ import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
  import * as os from 'os';
9
9
  import * as readline from 'readline';
10
+ import { spawnSync } from 'child_process';
10
11
  import { fileURLToPath } from 'url';
11
12
 
12
13
  const HOME = os.homedir();
@@ -14,6 +15,16 @@ const SHIMS_DIR = path.join(HOME, '.agents', '.cache', 'shims');
14
15
  const SYSTEM_DIR = path.join(HOME, '.agents-system');
15
16
  const USER_DIR = path.join(HOME, '.agents');
16
17
  const AGENTS_BIN = fileURLToPath(new URL('../dist/index.js', import.meta.url));
18
+ const INSTALL_HELPER_SCRIPT = fileURLToPath(new URL('./install-helper.js', import.meta.url));
19
+
20
+ function installKeychainHelper() {
21
+ if (process.platform !== 'darwin') return;
22
+ if (!fs.existsSync(INSTALL_HELPER_SCRIPT)) return;
23
+ // Sub-process so a hard failure (codesign / spctl missing on a weird host)
24
+ // can't take down the rest of postinstall. install-helper.js stays silent
25
+ // on no-op and emits one stdout line on success.
26
+ spawnSync(process.execPath, [INSTALL_HELPER_SCRIPT], { stdio: 'inherit' });
27
+ }
17
28
 
18
29
  function shellQuote(value) {
19
30
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -24,6 +35,7 @@ const isGlobalInstall = process.env.npm_config_global || process.argv.includes('
24
35
  if (!isGlobalInstall) {
25
36
  // Still create user directories for local installs
26
37
  fs.mkdirSync(USER_DIR, { recursive: true, mode: 0o700 });
38
+ installKeychainHelper();
27
39
  console.log(`
28
40
  agents-cli installed locally.
29
41
  To complete setup, run: npx agents setup
@@ -36,6 +48,10 @@ fs.mkdirSync(SHIMS_DIR, { recursive: true });
36
48
  fs.mkdirSync(SYSTEM_DIR, { recursive: true });
37
49
  fs.mkdirSync(USER_DIR, { recursive: true, mode: 0o700 });
38
50
 
51
+ // Copy the signed macOS Keychain helper to a stable user path so its trusted-app
52
+ // ACLs survive future npm publishes (which re-sign the bundle).
53
+ installKeychainHelper();
54
+
39
55
  // One-shot idempotent migrations
40
56
  function runMigrations() {
41
57
  // 1. Move agents.yaml from system to user repo