@phnx-labs/agents-cli 1.20.0 → 1.20.4

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 (111) hide show
  1. package/CHANGELOG.md +81 -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/import.js +90 -37
  13. package/dist/commands/mcp.js +166 -10
  14. package/dist/commands/packages.js +196 -27
  15. package/dist/commands/permissions.js +21 -6
  16. package/dist/commands/profiles.d.ts +8 -0
  17. package/dist/commands/profiles.js +117 -4
  18. package/dist/commands/pull.js +4 -4
  19. package/dist/commands/routines.js +6 -6
  20. package/dist/commands/rules.js +8 -4
  21. package/dist/commands/secrets-migrate.d.ts +24 -0
  22. package/dist/commands/secrets-migrate.js +198 -0
  23. package/dist/commands/secrets-sync.d.ts +11 -0
  24. package/dist/commands/secrets-sync.js +155 -0
  25. package/dist/commands/secrets.js +74 -39
  26. package/dist/commands/skills.js +22 -5
  27. package/dist/commands/subagents.js +69 -49
  28. package/dist/commands/teams.js +48 -10
  29. package/dist/commands/utils.d.ts +33 -0
  30. package/dist/commands/utils.js +139 -0
  31. package/dist/commands/versions.js +4 -4
  32. package/dist/commands/view.d.ts +6 -0
  33. package/dist/commands/view.js +169 -8
  34. package/dist/commands/workflows.js +29 -6
  35. package/dist/index.js +4 -0
  36. package/dist/lib/acp/client.js +6 -1
  37. package/dist/lib/agents.d.ts +4 -0
  38. package/dist/lib/agents.js +41 -17
  39. package/dist/lib/auto-pull-worker.js +18 -1
  40. package/dist/lib/browser/chrome.js +4 -0
  41. package/dist/lib/browser/drivers/ssh.js +1 -1
  42. package/dist/lib/browser/profiles.d.ts +3 -3
  43. package/dist/lib/browser/profiles.js +3 -3
  44. package/dist/lib/browser/service.js +19 -0
  45. package/dist/lib/browser/types.d.ts +4 -4
  46. package/dist/lib/cli-resources.d.ts +36 -8
  47. package/dist/lib/cli-resources.js +268 -46
  48. package/dist/lib/cloud/factory.d.ts +1 -1
  49. package/dist/lib/cloud/factory.js +1 -1
  50. package/dist/lib/events.d.ts +16 -2
  51. package/dist/lib/events.js +33 -2
  52. package/dist/lib/exec.d.ts +39 -11
  53. package/dist/lib/exec.js +90 -31
  54. package/dist/lib/help.js +11 -5
  55. package/dist/lib/hooks/cache.d.ts +38 -0
  56. package/dist/lib/hooks/cache.js +242 -0
  57. package/dist/lib/hooks/profile.d.ts +33 -0
  58. package/dist/lib/hooks/profile.js +129 -0
  59. package/dist/lib/hooks.d.ts +0 -10
  60. package/dist/lib/hooks.js +68 -15
  61. package/dist/lib/import.d.ts +21 -0
  62. package/dist/lib/import.js +55 -2
  63. package/dist/lib/mcp.d.ts +15 -0
  64. package/dist/lib/mcp.js +40 -0
  65. package/dist/lib/permissions.d.ts +13 -0
  66. package/dist/lib/permissions.js +51 -1
  67. package/dist/lib/plugin-marketplace.d.ts +10 -0
  68. package/dist/lib/plugin-marketplace.js +47 -1
  69. package/dist/lib/plugins.js +15 -1
  70. package/dist/lib/profiles-presets.d.ts +26 -0
  71. package/dist/lib/profiles-presets.js +187 -8
  72. package/dist/lib/profiles.d.ts +34 -0
  73. package/dist/lib/profiles.js +112 -1
  74. package/dist/lib/pty-server.js +27 -3
  75. package/dist/lib/routines-format.d.ts +17 -5
  76. package/dist/lib/routines-format.js +37 -16
  77. package/dist/lib/routines.d.ts +1 -1
  78. package/dist/lib/routines.js +2 -2
  79. package/dist/lib/runner.js +64 -10
  80. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  81. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  82. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
  83. package/dist/lib/secrets/bundles.d.ts +18 -22
  84. package/dist/lib/secrets/bundles.js +75 -99
  85. package/dist/lib/secrets/index.d.ts +51 -27
  86. package/dist/lib/secrets/index.js +147 -156
  87. package/dist/lib/secrets/install-helper.d.ts +45 -0
  88. package/dist/lib/secrets/install-helper.js +165 -0
  89. package/dist/lib/secrets/linux.js +4 -4
  90. package/dist/lib/secrets/sync.d.ts +56 -0
  91. package/dist/lib/secrets/sync.js +180 -0
  92. package/dist/lib/session/render.js +4 -4
  93. package/dist/lib/session/types.d.ts +1 -1
  94. package/dist/lib/shims.d.ts +4 -1
  95. package/dist/lib/shims.js +5 -35
  96. package/dist/lib/state.d.ts +14 -1
  97. package/dist/lib/state.js +49 -5
  98. package/dist/lib/teams/agents.d.ts +5 -4
  99. package/dist/lib/teams/agents.js +47 -21
  100. package/dist/lib/teams/api.d.ts +2 -1
  101. package/dist/lib/teams/api.js +4 -3
  102. package/dist/lib/types.d.ts +57 -1
  103. package/dist/lib/types.js +2 -0
  104. package/dist/lib/usage.d.ts +27 -2
  105. package/dist/lib/usage.js +100 -17
  106. package/dist/lib/versions.d.ts +35 -1
  107. package/dist/lib/versions.js +288 -64
  108. package/package.json +13 -12
  109. package/scripts/install-helper.js +97 -0
  110. package/scripts/postinstall.js +16 -0
  111. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
@@ -31,6 +31,7 @@ import { applyPermissionsToVersion as applyPermsToVersion, PERMISSIONS_CAPABLE_A
31
31
  import { installMcpServers, parseMcpServerConfig } from './mcp.js';
32
32
  import { markdownToToml } from './convert.js';
33
33
  import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink } from './shims.js';
34
+ import { importInstallScriptBinary } from './import.js';
34
35
  import { listInstalledSubagents, transformSubagentForClaude, syncSubagentToOpenclaw, SUBAGENT_CAPABLE_AGENTS } from './subagents.js';
35
36
  import { WORKFLOW_CAPABLE_AGENTS, listInstalledWorkflows, syncWorkflowToVersion } from './workflows.js';
36
37
  import { registerHooksToSettings } from './hooks.js';
@@ -511,16 +512,130 @@ export function getActuallySyncedResources(agent, version, options = {}) {
511
512
  }
512
513
  return result;
513
514
  }
515
+ /**
516
+ * Names that exist ONLY in the project's `.agents/` layer (no matching entry in
517
+ * user/system/extra layers). Sync intentionally skips project-layer commands,
518
+ * skills, hooks, subagents, plugins, and workflows for security — see the
519
+ * defense comments above each sync branch in syncResourcesToVersion. Without
520
+ * this filter, those names would forever appear in the "New resources" diff
521
+ * because they live in `available` but never reach `actuallySynced`.
522
+ */
523
+ export function getProjectOnlyResources(cwd = process.cwd()) {
524
+ const empty = {
525
+ commands: new Set(), skills: new Set(), hooks: new Set(),
526
+ subagents: new Set(), plugins: new Set(), workflows: new Set(),
527
+ };
528
+ const projectAgentsDir = getProjectAgentsDir(cwd);
529
+ if (!projectAgentsDir)
530
+ return empty;
531
+ const trustedBases = [getUserAgentsDir(), getAgentsDir(), ...getEnabledExtraRepos().map(e => e.dir)];
532
+ const trustedNames = (relSubdir, predicate) => {
533
+ const acc = new Set();
534
+ for (const base of trustedBases) {
535
+ const dir = path.join(base, relSubdir);
536
+ if (!fs.existsSync(dir))
537
+ continue;
538
+ try {
539
+ for (const entry of fs.readdirSync(dir)) {
540
+ if (entry.startsWith('.'))
541
+ continue;
542
+ if (predicate(path.join(dir, entry), entry))
543
+ acc.add(entry);
544
+ }
545
+ }
546
+ catch { /* ignore unreadable */ }
547
+ }
548
+ return acc;
549
+ };
550
+ const readProjectNames = (relSubdir, predicate) => {
551
+ const dir = path.join(projectAgentsDir, relSubdir);
552
+ if (!fs.existsSync(dir))
553
+ return [];
554
+ try {
555
+ return fs.readdirSync(dir)
556
+ .filter(e => !e.startsWith('.'))
557
+ .filter(e => predicate(path.join(dir, e), e));
558
+ }
559
+ catch {
560
+ return [];
561
+ }
562
+ };
563
+ const isMdFile = (full, name) => name.endsWith('.md') && (() => { try {
564
+ return fs.statSync(full).isFile();
565
+ }
566
+ catch {
567
+ return false;
568
+ } })();
569
+ const isDir = (full) => { try {
570
+ return fs.statSync(full).isDirectory();
571
+ }
572
+ catch {
573
+ return false;
574
+ } };
575
+ const hasFile = (sub) => (full) => isDir(full) && fs.existsSync(path.join(full, sub));
576
+ const stripMd = (n) => n.replace(/\.md$/, '');
577
+ const trustedCommands = new Set([...trustedNames('commands', isMdFile)].map(stripMd));
578
+ const projectCommands = readProjectNames('commands', isMdFile).map(stripMd);
579
+ for (const n of projectCommands)
580
+ if (!trustedCommands.has(n))
581
+ empty.commands.add(n);
582
+ const trustedSkills = trustedNames('skills', (full) => isDir(full));
583
+ for (const n of readProjectNames('skills', (full) => isDir(full)))
584
+ if (!trustedSkills.has(n))
585
+ empty.skills.add(n);
586
+ // Hooks: project entries are files; trusted entries are also files. Name match
587
+ // is filename-with-extension (sync compares by full filename, line 2031).
588
+ const trustedHooks = trustedNames('hooks', (full) => { try {
589
+ return fs.statSync(full).isFile();
590
+ }
591
+ catch {
592
+ return false;
593
+ } });
594
+ for (const n of readProjectNames('hooks', (full) => { try {
595
+ return fs.statSync(full).isFile();
596
+ }
597
+ catch {
598
+ return false;
599
+ } })) {
600
+ if (!trustedHooks.has(n))
601
+ empty.hooks.add(n);
602
+ }
603
+ const trustedSubagents = trustedNames('subagents', hasFile('AGENT.md'));
604
+ for (const n of readProjectNames('subagents', hasFile('AGENT.md'))) {
605
+ if (!trustedSubagents.has(n))
606
+ empty.subagents.add(n);
607
+ }
608
+ const trustedWorkflows = trustedNames('workflows', hasFile('WORKFLOW.md'));
609
+ for (const n of readProjectNames('workflows', hasFile('WORKFLOW.md'))) {
610
+ if (!trustedWorkflows.has(n))
611
+ empty.workflows.add(n);
612
+ }
613
+ const trustedPlugins = trustedNames('plugins', hasFile('.claude-plugin/plugin.json'));
614
+ for (const n of readProjectNames('plugins', hasFile('.claude-plugin/plugin.json'))) {
615
+ if (!trustedPlugins.has(n))
616
+ empty.plugins.add(n);
617
+ }
618
+ return empty;
619
+ }
514
620
  /**
515
621
  * Compare available resources with what's ACTUALLY synced to version home.
516
622
  * Returns only NEW resources that haven't been synced yet.
517
623
  * Source of truth: the actual files/config, NOT agents.yaml tracking.
624
+ *
625
+ * `projectOnly` (recommended): the result of `getProjectOnlyResources(cwd)`.
626
+ * Names listed there are filtered out for kinds that sync intentionally
627
+ * excludes the project layer — otherwise they would re-appear as "new"
628
+ * on every run and "Yes, sync all new" would silently do nothing for them.
518
629
  */
519
- export function getNewResources(available, actuallySynced) {
630
+ export function getNewResources(available, actuallySynced, projectOnly) {
631
+ const exclude = projectOnly || {
632
+ commands: new Set(), skills: new Set(), hooks: new Set(),
633
+ subagents: new Set(), plugins: new Set(), workflows: new Set(),
634
+ };
520
635
  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)),
636
+ commands: available.commands.filter(c => !actuallySynced.commands.includes(c) && !exclude.commands.has(c)),
637
+ skills: available.skills.filter(s => !actuallySynced.skills.includes(s) && !exclude.skills.has(s)),
638
+ hooks: available.hooks.filter(h => !actuallySynced.hooks.includes(h) && !exclude.hooks.has(h)),
524
639
  // Memory/rules presets are mutually exclusive — only one can be active.
525
640
  // If any preset is synced, don't report others as "new".
526
641
  memory: actuallySynced.memory.length > 0
@@ -528,9 +643,9 @@ export function getNewResources(available, actuallySynced) {
528
643
  : available.memory.filter(m => !actuallySynced.memory.includes(m)),
529
644
  mcp: available.mcp.filter(m => !actuallySynced.mcp.includes(m)),
530
645
  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)),
646
+ subagents: available.subagents.filter(s => !actuallySynced.subagents.includes(s) && !exclude.subagents.has(s)),
647
+ plugins: available.plugins.filter(p => !actuallySynced.plugins.includes(p) && !exclude.plugins.has(p)),
648
+ workflows: available.workflows.filter(w => !actuallySynced.workflows.includes(w) && !exclude.workflows.has(w)),
534
649
  // Promptcuts aren't version-scoped — the hook reads ~/.agents/promptcuts.yaml
535
650
  // directly, so there is never a "new" per-version state to reconcile.
536
651
  promptcuts: false,
@@ -1027,56 +1142,70 @@ export function setGlobalDefault(agent, version) {
1027
1142
  */
1028
1143
  export async function installVersion(agent, version, onProgress) {
1029
1144
  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
1145
  // Validate before deriving filesystem paths or npm package specs. The CLI
1062
1146
  // parser already enforces this for user input; this guard protects direct
1063
1147
  // callers and tests the critical install path at the source.
1064
1148
  if (!VERSION_RE.test(version)) {
1065
1149
  throw new Error(`Invalid version: ${JSON.stringify(version)}`);
1066
1150
  }
1151
+ if (!agentConfig.npmPackage) {
1152
+ if (!agentConfig.installScript) {
1153
+ return { success: false, installedVersion: version, error: 'Agent has no npm package' };
1154
+ }
1155
+ if (version !== 'latest' && !agentConfig.installScript.includes('VERSION')) {
1156
+ return {
1157
+ success: false,
1158
+ installedVersion: version,
1159
+ error: `${agentConfig.name} installer does not support version-pinned installs. Use ${agent}@latest.`,
1160
+ };
1161
+ }
1162
+ let installedVersion = version;
1163
+ try {
1164
+ const script = agentConfig.installScript.replaceAll('VERSION', version);
1165
+ onProgress?.(`Installing ${agentConfig.name}@${version} via official installer...`);
1166
+ await execAsync(script, { timeout: 120000 });
1167
+ if (version === 'latest') {
1168
+ installedVersion = await getCliVersionFromPath(agent) || version;
1169
+ }
1170
+ onProgress?.(`${agentConfig.name} installed. Setting up agents-cli version home for isolation...`);
1171
+ }
1172
+ catch (err) {
1173
+ emit('version.install', { agent, version, error: err.message });
1174
+ return { success: false, installedVersion: version, error: `${agentConfig.name} installer failed: ${err.message}` };
1175
+ }
1176
+ ensureAgentsDir();
1177
+ const versionDir = getVersionDir(agent, installedVersion);
1178
+ fs.mkdirSync(versionDir, { recursive: true });
1179
+ fs.mkdirSync(path.join(versionDir, 'home'), { recursive: true });
1180
+ // Symlink the installed binary into the version's node_modules/.bin so
1181
+ // listInstalledVersions (which checks getBinaryPath) sees this version as
1182
+ // installed. Without this, `agents add antigravity@latest` succeeds
1183
+ // but `agents view` shows the agent under "Not Managed" because
1184
+ // listInstalledVersions returns [] — the installer drops the binary in
1185
+ // ~/.local/bin (or similar) rather than the version's node_modules/.bin.
1186
+ // Grok is special-cased in getBinaryPath itself (binary lives in
1187
+ // ~/.grok/downloads), so we skip the symlink there.
1188
+ if (agent !== 'grok') {
1189
+ try {
1190
+ const { stdout: whichOut } = await execFileAsync('which', [agentConfig.cliCommand]);
1191
+ const installedBinary = whichOut.trim();
1192
+ if (installedBinary && fs.existsSync(installedBinary)) {
1193
+ importInstallScriptBinary({ agentId: agent, npmPackage: agentConfig.npmPackage, cliCommand: agentConfig.cliCommand }, installedVersion, installedBinary, versionDir);
1194
+ }
1195
+ }
1196
+ catch {
1197
+ /* binary missing from PATH — install script failed silently; surface via the existing version.install error path below isn't possible here since the script returned 0. Leave the version dir empty so getBinaryPath check correctly reports it uninstalled. */
1198
+ }
1199
+ }
1200
+ createVersionedAlias(agent, installedVersion);
1201
+ emit('version.install', { agent, version: installedVersion });
1202
+ return { success: true, installedVersion };
1203
+ }
1067
1204
  ensureAgentsDir();
1068
1205
  const versionDir = getVersionDir(agent, version);
1069
1206
  // Create version directory and isolated home
1070
1207
  fs.mkdirSync(versionDir, { recursive: true });
1071
1208
  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
1209
  // Initialize package.json (only for real npm agents)
1081
1210
  const packageJson = {
1082
1211
  name: `agents-${agent}-${version}`,
@@ -1408,6 +1537,18 @@ export async function getInstalledVersion(agent, version) {
1408
1537
  return version;
1409
1538
  }
1410
1539
  }
1540
+ async function getCliVersionFromPath(agent) {
1541
+ const agentConfig = AGENTS[agent];
1542
+ try {
1543
+ await execFileAsync('which', [agentConfig.cliCommand]);
1544
+ const { stdout } = await execFileAsync(agentConfig.cliCommand, ['--version'], { timeout: 3000 });
1545
+ const match = stdout.match(/(\d+\.\d+\.\d+)/);
1546
+ return match ? match[1] : null;
1547
+ }
1548
+ catch {
1549
+ return null;
1550
+ }
1551
+ }
1411
1552
  /**
1412
1553
  * Get the diff between central resources (~/.agents/) and what's synced to a version.
1413
1554
  * Uses filesystem state - no tracking needed.
@@ -1646,12 +1787,11 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1646
1787
  }
1647
1788
  }
1648
1789
  }
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) {
1790
+ // Fast guard: skip the entire sync when the caller requested a full sync and
1791
+ // nothing has changed since the last full sync. Pattern-derived selections
1792
+ // still count as full syncs because they are the persisted intended scope,
1793
+ // not a one-off caller override.
1794
+ if (!userPassedSelection && !options.force) {
1655
1795
  const manifest = loadManifest(agent, version);
1656
1796
  if (manifest && !isStale(manifest, agent, version, cwd)) {
1657
1797
  return { commands: false, skills: false, hooks: false, memory: [], permissions: false, mcp: [], subagents: [], plugins: [], workflows: [] };
@@ -1758,8 +1898,8 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1758
1898
  // not pass an explicit `selection`. Callers that pass explicit selections
1759
1899
  // are using the incremental/additive API (sync exactly these; leave others
1760
1900
  // 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.
1901
+ // cross-project leak always comes from the no-selection shim auto-sync at
1902
+ // launch.
1763
1903
  if (!userPassedSelection && COMMANDS_CAPABLE_AGENTS.includes(agent) && !shouldInstallCommandAsSkill(agent, version)) {
1764
1904
  const commandsTargetSweep = path.join(agentDir, agentConfig.commandsSubdir);
1765
1905
  if (fs.existsSync(commandsTargetSweep)) {
@@ -2121,6 +2261,24 @@ export function getEffectiveHome(agentId) {
2121
2261
  }
2122
2262
  return os.homedir();
2123
2263
  }
2264
+ /**
2265
+ * Thrown when the user references an agent@version that is not installed.
2266
+ * Carries the parsed (agentId, version) so callers can react — e.g. prompt
2267
+ * to install it on demand — without having to parse the error message.
2268
+ */
2269
+ export class VersionNotInstalledError extends Error {
2270
+ agentId;
2271
+ version;
2272
+ installedVersions;
2273
+ constructor(agentId, version, installedVersions) {
2274
+ const installed = installedVersions.length > 0 ? installedVersions.join(', ') : '(none)';
2275
+ super(`Version ${version} is not installed for ${AGENTS[agentId].name}. Installed versions: ${installed}`);
2276
+ this.agentId = agentId;
2277
+ this.version = version;
2278
+ this.installedVersions = installedVersions;
2279
+ this.name = 'VersionNotInstalledError';
2280
+ }
2281
+ }
2124
2282
  /**
2125
2283
  * Resolve a comma-separated --agents list into concrete version selections.
2126
2284
  * Bare agents target the default version, or the newest installed version when no default exists.
@@ -2130,10 +2288,26 @@ export function resolveAgentVersionTargets(value, availableAgents, options = {})
2130
2288
  const selectedAgents = [];
2131
2289
  const versionSelections = new Map();
2132
2290
  const explicitSelections = new Set();
2133
- const targets = value
2291
+ const rawTargets = value
2134
2292
  .split(',')
2135
2293
  .map((item) => item.trim())
2136
2294
  .filter(Boolean);
2295
+ // Expand literal `all` (with optional @all) into every available agent's all
2296
+ // installed versions. Skip agents with no installed versions so `all` is
2297
+ // lenient — only explicit `claude@all` errors when claude isn't installed.
2298
+ const targets = [];
2299
+ for (const t of rawTargets) {
2300
+ if (t === 'all' || t === 'all@all') {
2301
+ for (const a of availableAgents) {
2302
+ if (listInstalledVersions(a).length > 0) {
2303
+ targets.push(`${a}@all`);
2304
+ }
2305
+ }
2306
+ }
2307
+ else {
2308
+ targets.push(t);
2309
+ }
2310
+ }
2137
2311
  for (const target of targets) {
2138
2312
  const atIndex = target.indexOf('@');
2139
2313
  const agentToken = (atIndex === -1 ? target : target.slice(0, atIndex)).trim();
@@ -2142,7 +2316,7 @@ export function resolveAgentVersionTargets(value, availableAgents, options = {})
2142
2316
  continue;
2143
2317
  }
2144
2318
  if (atIndex !== -1 && !versionToken) {
2145
- throw new Error(`Missing version in --agents entry '${target}'. Use agent@x.y.z or agent@default.`);
2319
+ throw new Error(`Missing version in --agents entry '${target}'. Use agent@x.y.z, agent@default, or agent@all.`);
2146
2320
  }
2147
2321
  const agentId = resolveAgentName(agentToken);
2148
2322
  if (!agentId || !availableAgents.includes(agentId)) {
@@ -2182,8 +2356,13 @@ export function resolveAgentVersionTargets(value, availableAgents, options = {})
2182
2356
  explicitSelections.add(agentId);
2183
2357
  continue;
2184
2358
  }
2359
+ if (versionToken === 'all') {
2360
+ versionSelections.set(agentId, [...installedVersions]);
2361
+ explicitSelections.add(agentId);
2362
+ continue;
2363
+ }
2185
2364
  if (!installedVersions.includes(versionToken)) {
2186
- throw new Error(`Version ${versionToken} is not installed for ${AGENTS[agentId].name}. Installed versions: ${installedVersions.join(', ')}`);
2365
+ throw new VersionNotInstalledError(agentId, versionToken, installedVersions);
2187
2366
  }
2188
2367
  const explicitVersions = explicitSelections.has(agentId)
2189
2368
  ? (versionSelections.get(agentId) || [])
@@ -2206,10 +2385,28 @@ export function resolveInstalledAgentTargets(value, availableAgents, options = {
2206
2385
  const selectedAgents = [];
2207
2386
  const directAgents = [];
2208
2387
  const versionSelections = new Map();
2209
- const targets = value
2388
+ const rawTargets = value
2210
2389
  .split(',')
2211
2390
  .map((item) => item.trim())
2212
2391
  .filter(Boolean);
2392
+ // Expand literal `all` (with optional @all) into every available agent's all
2393
+ // installed versions. Skip agents with no installed versions so `all` is
2394
+ // lenient — only explicit `claude@all` errors when claude isn't installed.
2395
+ // Mirrors resolveAgentVersionTargets so every --agents flag site supports
2396
+ // the same selector syntax.
2397
+ const targets = [];
2398
+ for (const t of rawTargets) {
2399
+ if (t === 'all' || t === 'all@all') {
2400
+ for (const a of availableAgents) {
2401
+ if (listInstalledVersions(a).length > 0) {
2402
+ targets.push(`${a}@all`);
2403
+ }
2404
+ }
2405
+ }
2406
+ else {
2407
+ targets.push(t);
2408
+ }
2409
+ }
2213
2410
  const addVersionTarget = (agentId, version) => {
2214
2411
  const versions = versionSelections.get(agentId) || [];
2215
2412
  if (!versions.includes(version)) {
@@ -2229,7 +2426,7 @@ export function resolveInstalledAgentTargets(value, availableAgents, options = {
2229
2426
  continue;
2230
2427
  }
2231
2428
  if (atIndex !== -1 && !versionToken) {
2232
- throw new Error(`Missing version in --agents entry '${target}'. Use agent@x.y.z or agent@default.`);
2429
+ throw new Error(`Missing version in --agents entry '${target}'. Use agent@x.y.z, agent@default, or agent@all.`);
2233
2430
  }
2234
2431
  const agentId = resolveAgentName(agentToken);
2235
2432
  if (!agentId || !availableAgents.includes(agentId)) {
@@ -2262,11 +2459,20 @@ export function resolveInstalledAgentTargets(value, availableAgents, options = {
2262
2459
  addVersionTarget(agentId, defaultVersion);
2263
2460
  continue;
2264
2461
  }
2462
+ if (versionToken === 'all') {
2463
+ if (installedVersions.length === 0) {
2464
+ throw new Error(`No managed versions are installed for ${AGENTS[agentId].name}. Run: agents add ${agentId}@latest`);
2465
+ }
2466
+ for (const version of installedVersions) {
2467
+ addVersionTarget(agentId, version);
2468
+ }
2469
+ continue;
2470
+ }
2265
2471
  if (installedVersions.length === 0) {
2266
2472
  throw new Error(`No managed versions are installed for ${AGENTS[agentId].name}. Run: agents add ${agentId}@latest`);
2267
2473
  }
2268
2474
  if (!installedVersions.includes(versionToken)) {
2269
- throw new Error(`Version ${versionToken} is not installed for ${AGENTS[agentId].name}. Installed versions: ${installedVersions.join(', ')}`);
2475
+ throw new VersionNotInstalledError(agentId, versionToken, installedVersions);
2270
2476
  }
2271
2477
  addVersionTarget(agentId, versionToken);
2272
2478
  }
@@ -2320,9 +2526,15 @@ export async function promptAgentVersionSelection(availableAgents, options = {})
2320
2526
  const defaultVer = getGlobalDefault(agentId);
2321
2527
  if (versions.length === 0)
2322
2528
  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]})`)}`;
2529
+ // Surface the version count when there's more than one — mirrors the new
2530
+ // `--agents <agent>@all` syntax so users can see at a glance how many
2531
+ // versions `@all` would target before the per-version prompt fires.
2532
+ const detail = versions.length > 1
2533
+ ? (defaultVer
2534
+ ? `active: ${defaultVer}, ${versions.length} versions installed`
2535
+ : `${versions.length} versions installed`)
2536
+ : (defaultVer ?? versions[0]);
2537
+ return `${AGENTS[agentId].name} ${chalk.gray(`(${detail})`)}`;
2326
2538
  };
2327
2539
  let selectedAgents;
2328
2540
  if (options.skipPrompts) {
@@ -2337,6 +2549,18 @@ export async function promptAgentVersionSelection(availableAgents, options = {})
2337
2549
  }
2338
2550
  }
2339
2551
  else {
2552
+ // Non-TTY without an explicit --agents value used to silently fall through
2553
+ // to default-picking inside the caller. That's surprising in scripts — fail
2554
+ // loud and point at the new `--agents` syntax instead.
2555
+ if (!(process.stdin.isTTY && process.stdout.isTTY)) {
2556
+ throw new Error('Non-interactive shell: cannot prompt for agent/version selection.\n' +
2557
+ 'Pass --agents explicitly. Examples:\n' +
2558
+ ' --agents claude (default version)\n' +
2559
+ ' --agents claude@all (every installed Claude version)\n' +
2560
+ ' --agents claude@2.1.141 (a specific version)\n' +
2561
+ ' --agents all (every installed version of every capable agent)\n' +
2562
+ 'Or pass --yes to auto-pick defaults.');
2563
+ }
2340
2564
  // Prompt for agent selection
2341
2565
  const checkboxResult = await checkbox({
2342
2566
  message: 'Which agents should receive these resources?',
@@ -2371,7 +2595,7 @@ export async function promptAgentVersionSelection(availableAgents, options = {})
2371
2595
  const versionResult = await checkbox({
2372
2596
  message: `Which versions of ${AGENTS[agentId].name} should receive these resources?`,
2373
2597
  choices: [
2374
- { name: chalk.bold('All versions'), value: 'all', checked: false },
2598
+ { name: chalk.bold(`All versions (${versions.length})`), value: 'all', checked: false },
2375
2599
  ...versions.map((v) => {
2376
2600
  const base = v === defaultVer ? `${v} (default)` : v;
2377
2601
  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.4",
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",
@@ -73,29 +74,29 @@
73
74
  "npm": ">=9"
74
75
  },
75
76
  "dependencies": {
76
- "@inquirer/prompts": "8.5.1",
77
+ "@inquirer/prompts": "8.5.2",
77
78
  "@types/proper-lockfile": "4.1.4",
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",
86
87
  "node-pty": "1.1.0",
87
- "ora": "8.2.0",
88
+ "ora": "9.4.0",
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
- "tsx": "4.22.3",
98
- "typescript": "5.9.3",
99
- "vitest": "4.1.6"
97
+ "@types/node": "25.9.2",
98
+ "tsx": "4.22.4",
99
+ "typescript": "6.0.3",
100
+ "vitest": "4.1.8"
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();