@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
@@ -8,10 +8,12 @@
8
8
  import chalk from 'chalk';
9
9
  import * as fs from 'fs';
10
10
  import { bundleExists, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readBundle, renameBundle, rotateBundleSecret, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
11
- import { deleteKeychainToken, getKeychainToken, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
11
+ import { deleteKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
12
12
  import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
13
13
  import { registerCommandGroups, setHelpSections } from '../lib/help.js';
14
14
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
15
+ import { registerSecretsSyncCommands } from './secrets-sync.js';
16
+ import { registerSecretsMigrateAclCommand } from './secrets-migrate.js';
15
17
  /** Prompt the user for a secret value with masked input. Requires an interactive TTY. */
16
18
  async function promptForSecret(message) {
17
19
  if (!isInteractiveTerminal()) {
@@ -178,7 +180,6 @@ function humanAge(iso) {
178
180
  function renderBundleRow(b) {
179
181
  const entries = describeBundle(b);
180
182
  const keys = entries.length;
181
- const sync = b.icloud_sync ? chalk.cyan('icloud') : '';
182
183
  const expiringCount = countExpiringSoon(b.meta);
183
184
  const expiring = expiringCount > 0 ? chalk.yellow(String(expiringCount)) : chalk.gray('-');
184
185
  // Timestamp distinction:
@@ -194,12 +195,11 @@ function renderBundleRow(b) {
194
195
  : (b.created_at ? chalk.gray('never') : chalk.gray('?'));
195
196
  const head = `${chalk.cyan(b.name.padEnd(20))} ` +
196
197
  `${String(keys).padEnd(5)} ` +
197
- `${padVisible(sync, 6)} ` +
198
198
  `${padVisible(expiring, 9)} ` +
199
199
  `${padVisible(created, 9)} ` +
200
200
  `${padVisible(updated, 9)} ` +
201
201
  `${padVisible(used, 7)}`;
202
- return b.description ? `${head} ${chalk.gray(b.description)}` : head.trimEnd();
202
+ return b.description ? `${head} ${chalk.gray(safePrint(b.description))}` : head.trimEnd();
203
203
  }
204
204
  /** Colorize a variable source kind (literal, keychain, env, file, exec). */
205
205
  function kindLabel(kind) {
@@ -220,6 +220,17 @@ function redact(value, reveal) {
220
220
  return '';
221
221
  return '*'.repeat(Math.min(value.length, 8));
222
222
  }
223
+ /**
224
+ * Strip ASCII / C1 control bytes from a string before printing it to the
225
+ * terminal. Bundle descriptions, notes, and remote-supplied names can carry
226
+ * arbitrary text and a malicious value containing ANSI escape sequences (e.g.
227
+ * OSC 52 clipboard set, screen-clear, cursor moves) would otherwise be
228
+ * interpreted by the user's terminal. Allow tab and newline so multi-line
229
+ * notes still render; strip everything else in the C0/C1 ranges plus DEL.
230
+ */
231
+ function safePrint(s) {
232
+ return s.replace(/[\x00-\x08\x0B-\x1F\x7F-\x9F]/g, '');
233
+ }
223
234
  /**
224
235
  * Build a VarMeta patch from CLI flags. Validates each provided field. Returns
225
236
  * undefined if no meta flag was passed (so callers know to skip meta updates).
@@ -286,7 +297,7 @@ function renderMetaLine(meta, reveal) {
286
297
  parts.push(colored);
287
298
  }
288
299
  if (meta.note) {
289
- let note = meta.note;
300
+ let note = safePrint(meta.note);
290
301
  if (!reveal && note.length > 80) {
291
302
  note = note.slice(0, 79) + '\u2026';
292
303
  }
@@ -314,7 +325,7 @@ function countExpiringSoon(meta) {
314
325
  export function registerSecretsCommands(program) {
315
326
  const cmd = program
316
327
  .command('secrets')
317
- .description('Named bundles of env variables backed by macOS Keychain (iCloud-synced by default). Inject into agents via `agents run --secrets <name>`.');
328
+ .description('Named bundles of env variables backed by macOS Keychain (device-local, biometry-gated). Inject into agents via `agents run --secrets <name>`.');
318
329
  setHelpSections(cmd, {
319
330
  examples: `
320
331
  # Create a bundle
@@ -340,23 +351,22 @@ export function registerSecretsCommands(program) {
340
351
  `,
341
352
  notes: `
342
353
  Bundles are containers; secrets are the variables inside them. Keychain values
343
- never touch disk in plaintext.
344
-
345
- iCloud sync: new bundles use the iCloud-synced keychain by default so they
346
- appear on other Macs (same iCloud account, iCloud Keychain enabled). Pass
347
- --no-icloud-sync at create time to keep values device-local instead.
354
+ never touch disk in plaintext. Every item is device-local and gated by Touch ID
355
+ or device passcode; cross-machine sync is handled by 'agents secrets push/pull'.
348
356
 
349
357
  See also:
350
358
  agents secrets rotate <bundle> <key> rotate value, preserve metadata
351
359
  agents secrets import <bundle> --from .env bulk import from .env
352
360
  agents secrets import <bundle> --from-1password --vault <name>
353
361
  agents secrets generate [length] generate a random password / PIN / hex
362
+ agents secrets migrate-acl upgrade legacy items to the biometry ACL
354
363
  `,
355
364
  });
356
365
  registerCommandGroups(cmd, [
357
366
  { title: 'Bundle commands', names: ['list', 'view', 'create', 'rename', 'describe', 'delete'] },
358
367
  { title: 'Secret commands', names: ['add', 'rotate', 'remove', 'import', 'export'] },
359
- { title: 'Utilities', names: ['exec', 'generate'] },
368
+ { title: 'Sync commands', names: ['push', 'pull', 'remote-list'] },
369
+ { title: 'Utilities', names: ['exec', 'generate', 'migrate-acl'] },
360
370
  ]);
361
371
  cmd
362
372
  .command('list')
@@ -369,7 +379,7 @@ export function registerSecretsCommands(program) {
369
379
  console.log(chalk.gray('Try: agents secrets create <name>'));
370
380
  return;
371
381
  }
372
- console.log(chalk.bold(`${'NAME'.padEnd(20)} ${'KEYS'.padEnd(5)} ${'SYNC'.padEnd(6)} ${'EXPIRING'.padEnd(9)} ${'CREATED'.padEnd(9)} ${'UPDATED'.padEnd(9)} ${'USED'.padEnd(7)} DESCRIPTION`));
382
+ console.log(chalk.bold(`${'NAME'.padEnd(20)} ${'KEYS'.padEnd(5)} ${'EXPIRING'.padEnd(9)} ${'CREATED'.padEnd(9)} ${'UPDATED'.padEnd(9)} ${'USED'.padEnd(7)} DESCRIPTION`));
373
383
  for (const b of bundles) {
374
384
  console.log(renderBundleRow(b));
375
385
  }
@@ -387,11 +397,9 @@ export function registerSecretsCommands(program) {
387
397
  const entries = describeBundle(bundle);
388
398
  console.log(chalk.bold(bundle.name));
389
399
  if (bundle.description)
390
- console.log(chalk.gray(bundle.description));
400
+ console.log(chalk.gray(safePrint(bundle.description)));
391
401
  if (bundle.allow_exec)
392
402
  console.log(chalk.yellow('allow_exec: true'));
393
- if (bundle.icloud_sync)
394
- console.log(chalk.cyan('icloud_sync: true'));
395
403
  if (bundle.created_at)
396
404
  console.log(chalk.gray(`created_at: ${bundle.created_at} (${humanAge(bundle.created_at)})`));
397
405
  if (bundle.updated_at)
@@ -408,19 +416,30 @@ export function registerSecretsCommands(program) {
408
416
  console.error(chalk.red('--reveal in a non-TTY requires --plaintext.'));
409
417
  process.exit(1);
410
418
  }
419
+ // Batch every keychain read into one helper call so --reveal pops
420
+ // Touch ID once for the whole bundle instead of once per key.
421
+ const revealedValues = new Map();
422
+ if (reveal) {
423
+ const items = entries
424
+ .filter((e) => e.kind === 'keychain')
425
+ .map((e) => secretsKeychainItem(bundle.name, e.detail));
426
+ try {
427
+ const fetched = getKeychainTokens(items);
428
+ for (const [item, value] of fetched)
429
+ revealedValues.set(item, value);
430
+ }
431
+ catch {
432
+ // Fall through to masked output on cancellation / batch failure.
433
+ }
434
+ }
411
435
  for (const e of entries) {
412
436
  if (e.kind === 'keychain') {
413
437
  const item = secretsKeychainItem(bundle.name, e.detail);
414
- const stored = hasKeychainToken(item, bundle.icloud_sync);
438
+ const stored = hasKeychainToken(item);
415
439
  const marker = stored ? chalk.green('stored') : chalk.red('missing');
416
440
  let valueCol = `[keychain:${e.detail}] ${marker}`;
417
- if (reveal && stored) {
418
- try {
419
- valueCol = redact(getKeychainToken(item, bundle.icloud_sync), true);
420
- }
421
- catch {
422
- // fall through to masked
423
- }
441
+ if (reveal && revealedValues.has(item)) {
442
+ valueCol = redact(revealedValues.get(item), true);
424
443
  }
425
444
  console.log(` ${chalk.cyan(e.key.padEnd(28))} ${kindLabel(e.kind).padEnd(18)} ${valueCol}`);
426
445
  }
@@ -429,7 +448,7 @@ export function registerSecretsCommands(program) {
429
448
  const literalValue = typeof raw === 'string'
430
449
  ? raw
431
450
  : (raw && typeof raw === 'object' && 'value' in raw ? raw.value : '');
432
- console.log(` ${chalk.cyan(e.key.padEnd(28))} ${kindLabel(e.kind).padEnd(18)} ${literalValue}`);
451
+ console.log(` ${chalk.cyan(e.key.padEnd(28))} ${kindLabel(e.kind).padEnd(18)} ${redact(literalValue, reveal)}`);
433
452
  }
434
453
  else {
435
454
  console.log(` ${chalk.cyan(e.key.padEnd(28))} ${kindLabel(e.kind).padEnd(18)} ${e.detail}`);
@@ -451,7 +470,6 @@ export function registerSecretsCommands(program) {
451
470
  .description('Create an empty bundle')
452
471
  .option('--description <text>', 'Free-form description')
453
472
  .option('--allow-exec', 'Allow exec: refs in this bundle (off by default)')
454
- .option('--no-icloud-sync', 'Store keychain values device-local instead of syncing them via iCloud Keychain')
455
473
  .option('--force', 'Overwrite an existing bundle')
456
474
  .action(async (name, opts) => {
457
475
  try {
@@ -465,7 +483,6 @@ export function registerSecretsCommands(program) {
465
483
  name: resolvedName,
466
484
  description: opts.description,
467
485
  allow_exec: opts.allowExec,
468
- icloud_sync: opts.icloudSync !== false,
469
486
  vars: {},
470
487
  };
471
488
  writeBundle(bundle);
@@ -600,12 +617,11 @@ export function registerSecretsCommands(program) {
600
617
  secretValue = await promptForSecret(`Enter value for ${resolvedBundleName}.${resolvedKey}`);
601
618
  }
602
619
  const item = secretsKeychainItem(resolvedBundleName, resolvedKey);
603
- setKeychainToken(item, secretValue, bundle.icloud_sync);
620
+ setKeychainToken(item, secretValue);
604
621
  bundle.vars[resolvedKey] = keychainRef(resolvedKey);
605
622
  applyMeta();
606
623
  writeBundle(bundle);
607
- const where = bundle.icloud_sync ? 'iCloud Keychain' : 'keychain';
608
- console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} stored in ${where} (${item}).`));
624
+ console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} stored in keychain (${item}).`));
609
625
  }
610
626
  catch (err) {
611
627
  if (isPromptCancelled(err))
@@ -665,8 +681,7 @@ Examples:
665
681
  clearMeta: opts.clearMeta,
666
682
  meta: metaPatch,
667
683
  });
668
- const where = bundle.icloud_sync ? 'iCloud Keychain' : 'keychain';
669
- console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} rotated in ${where}.`));
684
+ console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} rotated in keychain.`));
670
685
  }
671
686
  catch (err) {
672
687
  if (isPromptCancelled(err))
@@ -679,6 +694,7 @@ Examples:
679
694
  .command('remove [bundle] [key]')
680
695
  .description('Remove a key from the bundle. Purges the keychain item if the ref was keychain:. Use --keep-secret to retain it.')
681
696
  .option('--keep-secret', 'Leave the keychain item in place after removing the ref from the bundle')
697
+ .option('-y, --yes', 'Skip the confirmation prompt when purging a keychain item')
682
698
  .action(async (bundleName, key, opts) => {
683
699
  try {
684
700
  const resolvedBundleName = bundleName ?? (await pickBundleName('remove from'));
@@ -689,11 +705,28 @@ Examples:
689
705
  process.exit(1);
690
706
  }
691
707
  const raw = bundle.vars[resolvedKey];
708
+ const willPurge = !opts.keepSecret && typeof raw === 'string' && raw.startsWith('keychain:');
709
+ if (willPurge && !opts.yes) {
710
+ if (!isInteractiveTerminal()) {
711
+ console.error(chalk.red(`Refusing to purge keychain item for ${resolvedBundleName}.${resolvedKey} non-interactively. ` +
712
+ `Pass --yes to confirm or --keep-secret to retain the keychain entry.`));
713
+ process.exit(1);
714
+ }
715
+ const { confirm } = await import('@inquirer/prompts');
716
+ const ok = await confirm({
717
+ message: `Purge keychain item for ${resolvedBundleName}.${resolvedKey}? (use --keep-secret to retain)`,
718
+ default: false,
719
+ });
720
+ if (!ok) {
721
+ console.log(chalk.gray('Aborted. Bundle metadata unchanged.'));
722
+ return;
723
+ }
724
+ }
692
725
  delete bundle.vars[resolvedKey];
693
726
  writeBundle(bundle);
694
- if (!opts.keepSecret && typeof raw === 'string' && raw.startsWith('keychain:')) {
727
+ if (willPurge) {
695
728
  const item = secretsKeychainItem(resolvedBundleName, raw.slice('keychain:'.length));
696
- const removed = deleteKeychainToken(item, bundle.icloud_sync);
729
+ const removed = deleteKeychainToken(item);
697
730
  if (removed) {
698
731
  console.log(chalk.green(`Removed ${resolvedBundleName}.${resolvedKey} and purged keychain item.`));
699
732
  return;
@@ -738,7 +771,7 @@ Examples:
738
771
  }
739
772
  if (!opts.keepSecrets) {
740
773
  for (const { item } of keychainItemsForBundle(bundle)) {
741
- deleteKeychainToken(item, bundle.icloud_sync);
774
+ deleteKeychainToken(item);
742
775
  }
743
776
  }
744
777
  const existed = deleteBundle(resolvedName);
@@ -824,7 +857,7 @@ Examples:
824
857
  }
825
858
  else {
826
859
  const item = secretsKeychainItem(resolvedBundleName, envKey);
827
- setKeychainToken(item, value, bundle.icloud_sync);
860
+ setKeychainToken(item, value);
828
861
  bundle.vars[envKey] = keychainRef(envKey);
829
862
  }
830
863
  added++;
@@ -848,7 +881,7 @@ Examples:
848
881
  }
849
882
  else {
850
883
  const item = secretsKeychainItem(resolvedBundleName, key);
851
- setKeychainToken(item, value, bundle.icloud_sync);
884
+ setKeychainToken(item, value);
852
885
  bundle.vars[key] = keychainRef(key);
853
886
  }
854
887
  added++;
@@ -908,8 +941,8 @@ Examples:
908
941
  console.log(chalk.green(`Exported to 1Password vault '${vault}': ${parts.join(', ')}.`));
909
942
  return;
910
943
  }
911
- if (isInteractiveTerminal() && !opts.plaintext) {
912
- console.error(chalk.red('export to a TTY requires --plaintext (prevents shoulder-surfing).'));
944
+ if (!opts.plaintext) {
945
+ console.error(chalk.red('export prints secrets in the clear and requires --plaintext (works for TTY and pipes alike).'));
913
946
  process.exit(1);
914
947
  }
915
948
  const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `export to shell` });
@@ -1033,4 +1066,6 @@ Examples:
1033
1066
  console.log(password);
1034
1067
  }
1035
1068
  });
1069
+ registerSecretsSyncCommands(cmd);
1070
+ registerSecretsMigrateAclCommand(cmd);
1036
1071
  }
@@ -7,9 +7,9 @@ import { select, checkbox } from '@inquirer/prompts';
7
7
  import { AGENTS, SKILLS_CAPABLE_AGENTS, resolveAgentName, formatAgentError, agentLabel, } from '../lib/agents.js';
8
8
  import { cloneRepo } from '../lib/git.js';
9
9
  import { discoverSkillsFromRepo, installSkillCentrally, listInstalledSkills, listInstalledSkillsWithScope, getSkillInfo, getSkillRules, getSkillsDir, countSkillFiles, tryParseSkillMetadata, diffVersionSkills, iterSkillsCapableVersions, removeSkillFromVersion, } from '../lib/skills.js';
10
- import { getGlobalDefault, resolveVersionAlias, syncResourcesToVersion, promptAgentVersionSelection, getVersionHomePath, resolveAgentVersionTargets, } from '../lib/versions.js';
10
+ import { getGlobalDefault, resolveVersionAlias, syncResourcesToVersion, promptAgentVersionSelection, getVersionHomePath, } from '../lib/versions.js';
11
11
  import { recordVersionResources } from '../lib/state.js';
12
- import { isPromptCancelled, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, promptRemovalTargets, } from './utils.js';
12
+ import { isPromptCancelled, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, resolveAgentTargetsAutoInstalling, promptRemovalTargets, } from './utils.js';
13
13
  import { showResourceList, buildTargetsSection, } from './resource-view.js';
14
14
  /** Register the `agents skills` command tree (list, add, remove, sync, prune, view). */
15
15
  export function registerSkillsCommands(program) {
@@ -202,11 +202,24 @@ Examples:
202
202
  spinner.succeed('Using local path');
203
203
  }
204
204
  }
205
- console.log(chalk.bold(`\nFound ${discoveredSkills.length} skill(s):`));
206
205
  if (discoveredSkills.length === 0) {
207
206
  console.log(chalk.yellow('No skills found (looking for SKILL.md files)'));
208
207
  return;
209
208
  }
209
+ // Filter by --names if provided. Mirrors the no-source path's behavior
210
+ // so users can pluck specific skills from a multi-skill repo.
211
+ const requestedNames = parseCommaSeparatedList(options.names);
212
+ if (requestedNames.length > 0) {
213
+ const discoveredNames = new Set(discoveredSkills.map((s) => s.name));
214
+ const missing = requestedNames.filter((n) => !discoveredNames.has(n));
215
+ if (missing.length > 0) {
216
+ console.log(chalk.red(`\nSkill(s) not found in source: ${missing.join(', ')}`));
217
+ console.log(chalk.gray(`Available: ${[...discoveredNames].join(', ')}`));
218
+ process.exit(1);
219
+ }
220
+ discoveredSkills = discoveredSkills.filter((s) => requestedNames.includes(s.name));
221
+ }
222
+ console.log(chalk.bold(`\nFound ${discoveredSkills.length} skill(s):`));
210
223
  for (const skill of discoveredSkills) {
211
224
  const nameColor = skill.parseError ? chalk.yellow : chalk.cyan;
212
225
  console.log(`\n ${nameColor(skill.name)}: ${skill.metadata.description || 'no description'}`);
@@ -238,13 +251,17 @@ Examples:
238
251
  let selectedAgents;
239
252
  let versionSelections;
240
253
  if (options.agents) {
241
- const result = resolveAgentVersionTargets(options.agents, SKILLS_CAPABLE_AGENTS);
254
+ const result = await resolveAgentTargetsAutoInstalling(options.agents, SKILLS_CAPABLE_AGENTS, { yes: options.yes });
255
+ if (!result) {
256
+ console.log(chalk.gray('Cancelled.'));
257
+ return;
258
+ }
242
259
  selectedAgents = result.selectedAgents;
243
260
  versionSelections = result.versionSelections;
244
261
  }
245
262
  else {
246
263
  const result = await promptAgentVersionSelection(SKILLS_CAPABLE_AGENTS, {
247
- skipPrompts: options.yes || !isInteractiveTerminal(),
264
+ skipPrompts: options.yes,
248
265
  });
249
266
  selectedAgents = result.selectedAgents;
250
267
  versionSelections = result.versionSelections;
@@ -9,13 +9,12 @@ import chalk from 'chalk';
9
9
  import ora from 'ora';
10
10
  import * as fs from 'fs';
11
11
  import * as path from 'path';
12
- import { checkbox } from '@inquirer/prompts';
13
- import { AGENTS, agentLabel } from '../lib/agents.js';
12
+ import { agentLabel } from '../lib/agents.js';
14
13
  import { cloneRepo } from '../lib/git.js';
15
14
  import { discoverSubagentsFromRepo, installSubagentCentrally, listInstalledSubagents, getInstalledSubagent, listSubagentsForAgent, SUBAGENT_CAPABLE_AGENTS, iterSubagentsCapableVersions, removeSubagentFromVersion, } from '../lib/subagents.js';
16
- import { listInstalledVersions, syncResourcesToVersion, getGlobalDefault, getVersionHomePath, } from '../lib/versions.js';
17
- import { getSubagentsDir } from '../lib/state.js';
18
- import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection, requireDestructiveArg, promptRemovalTargets, } from './utils.js';
15
+ import { listInstalledVersions, syncResourcesToVersion, getGlobalDefault, getVersionHomePath, promptAgentVersionSelection, } from '../lib/versions.js';
16
+ import { getSubagentsDir, recordVersionResources } from '../lib/state.js';
17
+ import { requireDestructiveArg, promptRemovalTargets, parseCommaSeparatedList, resolveAgentTargetsAutoInstalling, } from './utils.js';
19
18
  import { showResourceList, buildTargetsSection, } from './resource-view.js';
20
19
  /** Replace the home directory prefix with ~ for display. */
21
20
  function formatPath(p) {
@@ -102,13 +101,20 @@ Examples:
102
101
  subagentsCmd
103
102
  .command('add <source>')
104
103
  .description('Install subagents from a source (GitHub, local path) and sync to agent versions')
105
- .option('-a, --agents <agents...>', 'Targets: claude, openclaw (defaults to all subagent-capable agents)')
104
+ .option('-a, --agents <list>', 'Targets: claude, openclaw, claude@2.1.141, claude@all, all')
105
+ .option('--names <list>', 'Subagent names from the source (comma-separated)')
106
106
  .option('-y, --yes', 'Skip all prompts and confirmations')
107
107
  .addHelpText('after', `
108
108
  Examples:
109
109
  # Install from GitHub
110
110
  agents subagents add gh:team/subagents --agents claude,openclaw
111
111
 
112
+ # Pluck specific subagents from a multi-subagent repo
113
+ agents subagents add gh:team/subagents --names code-reviewer,planner
114
+
115
+ # Install across every installed Claude version
116
+ agents subagents add gh:team/subagents --agents claude@all
117
+
112
118
  # Install from local directory (must contain subagents/*/AGENT.md)
113
119
  agents subagents add ~/my-subagent --agents claude
114
120
 
@@ -117,9 +123,13 @@ Examples:
117
123
  `)
118
124
  .action(async (source, options) => {
119
125
  const spinner = ora({ text: 'Fetching source...', isSilent: !process.stdout.isTTY }).start();
120
- // Clone or use local source
126
+ // Clone or use local source. Accept any git-like scheme to match the
127
+ // other <resource> add commands (skills, workflows, commands, hooks).
121
128
  let sourcePath;
122
- if (source.startsWith('gh:') || source.startsWith('http')) {
129
+ const isGitRepo = source.startsWith('gh:') || source.startsWith('git:') ||
130
+ source.startsWith('ssh:') || source.startsWith('https://') ||
131
+ source.startsWith('http://');
132
+ if (isGitRepo) {
123
133
  try {
124
134
  const cloneResult = await cloneRepo(source);
125
135
  sourcePath = cloneResult.localPath;
@@ -138,12 +148,24 @@ Examples:
138
148
  }
139
149
  // Discover subagents
140
150
  spinner.text = 'Discovering subagents...';
141
- const discovered = discoverSubagentsFromRepo(sourcePath);
151
+ let discovered = discoverSubagentsFromRepo(sourcePath);
142
152
  if (discovered.length === 0) {
143
153
  spinner.fail('No subagents found in source');
144
154
  console.log(chalk.gray(`Expected: subagents/*/AGENT.md`));
145
155
  process.exit(1);
146
156
  }
157
+ // --names filter: pluck specific subagents from a multi-subagent source.
158
+ const requestedNames = parseCommaSeparatedList(options.names);
159
+ if (requestedNames.length > 0) {
160
+ const discoveredNames = new Set(discovered.map((s) => s.name));
161
+ const missing = requestedNames.filter((n) => !discoveredNames.has(n));
162
+ if (missing.length > 0) {
163
+ spinner.fail(`Subagent(s) not found in source: ${missing.join(', ')}`);
164
+ console.log(chalk.gray(`Available: ${[...discoveredNames].join(', ')}`));
165
+ process.exit(1);
166
+ }
167
+ discovered = discovered.filter((s) => requestedNames.includes(s.name));
168
+ }
147
169
  spinner.succeed(`Found ${discovered.length} subagent(s)`);
148
170
  // Show what we found
149
171
  console.log();
@@ -151,42 +173,29 @@ Examples:
151
173
  console.log(` ${chalk.cyan(sub.name)}: ${chalk.gray(sub.frontmatter.description)}`);
152
174
  }
153
175
  console.log();
154
- // Determine target agents
155
- let targetAgents = options.agents || [];
156
- if (targetAgents.length === 0 && !options.yes) {
157
- // Prompt for target agents
158
- const installedAgents = SUBAGENT_CAPABLE_AGENTS.filter(id => {
159
- const versions = listInstalledVersions(id);
160
- return versions.length > 0;
161
- });
162
- if (installedAgents.length === 0) {
163
- console.log(chalk.yellow('No subagent-capable agents installed'));
164
- console.log(chalk.gray('Subagents will be stored centrally and synced when you install claude or openclaw'));
165
- targetAgents = [];
166
- }
167
- else {
168
- if (!isInteractiveTerminal()) {
169
- requireInteractiveSelection('Selecting target agents for subagents', [
170
- 'agents subagents add <source> --agents claude openclaw',
171
- 'agents subagents add <source> --yes',
172
- ]);
173
- }
174
- try {
175
- targetAgents = await checkbox({
176
- message: 'Install to which agents?',
177
- choices: installedAgents.map(id => ({
178
- name: AGENTS[id].name,
179
- value: id,
180
- checked: true,
181
- })),
182
- });
183
- }
184
- catch (err) {
185
- if (isPromptCancelled(err))
186
- return;
187
- throw err;
188
- }
176
+ // Determine target agent versions, using the same path skills/workflows use.
177
+ // Back-compat: commander's old `--agents <agents...>` shape arrives as an array;
178
+ // join it with commas so resolveAgentVersionTargets can parse it.
179
+ const agentsArg = Array.isArray(options.agents)
180
+ ? options.agents.join(',')
181
+ : options.agents;
182
+ let selectedAgents;
183
+ let versionSelections;
184
+ if (agentsArg) {
185
+ const result = await resolveAgentTargetsAutoInstalling(agentsArg, SUBAGENT_CAPABLE_AGENTS, { yes: options.yes });
186
+ if (!result) {
187
+ console.log(chalk.gray('Cancelled.'));
188
+ return;
189
189
  }
190
+ selectedAgents = result.selectedAgents;
191
+ versionSelections = result.versionSelections;
192
+ }
193
+ else {
194
+ const result = await promptAgentVersionSelection(SUBAGENT_CAPABLE_AGENTS, {
195
+ skipPrompts: options.yes,
196
+ });
197
+ selectedAgents = result.selectedAgents;
198
+ versionSelections = result.versionSelections;
190
199
  }
191
200
  // Install centrally
192
201
  const installSpinner = ora({ text: 'Installing subagents...', isSilent: !process.stdout.isTTY }).start();
@@ -198,16 +207,27 @@ Examples:
198
207
  }
199
208
  }
200
209
  installSpinner.succeed(`Installed ${discovered.length} subagent(s) to ${formatPath(getSubagentsDir())}`);
201
- // Sync to target agents
202
- if (targetAgents.length > 0) {
210
+ // Sync to selected versions
211
+ if (versionSelections.size > 0) {
203
212
  const syncSpinner = ora({ text: 'Syncing to agents...', isSilent: !process.stdout.isTTY }).start();
204
- for (const agentId of targetAgents) {
205
- const versions = listInstalledVersions(agentId);
213
+ const subagentNames = discovered.map((s) => s.name);
214
+ let synced = 0;
215
+ for (const [agentId, versions] of versionSelections) {
206
216
  for (const version of versions) {
207
217
  syncResourcesToVersion(agentId, version);
218
+ recordVersionResources(agentId, version, 'subagents', subagentNames);
219
+ synced++;
208
220
  }
209
221
  }
210
- syncSpinner.succeed(`Synced to ${targetAgents.map(id => agentLabel(id)).join(', ')}`);
222
+ if (synced > 0) {
223
+ syncSpinner.succeed(`Synced to ${synced} agent version(s) across ${selectedAgents.map((id) => agentLabel(id)).join(', ')}`);
224
+ }
225
+ else {
226
+ syncSpinner.info('No version-managed agents to sync');
227
+ }
228
+ }
229
+ else {
230
+ console.log(chalk.gray('Stored centrally; no agent versions selected for sync.'));
211
231
  }
212
232
  console.log();
213
233
  });
@@ -14,6 +14,7 @@ import { buildPreview as buildSessionPreview } from './sessions-picker.js';
14
14
  import { parseExecEnv } from '../lib/exec.js';
15
15
  import { teamPicker, printTeamTable } from './teams-picker.js';
16
16
  import { itemPicker } from '../lib/picker.js';
17
+ import { profileExists, readProfile } from '../lib/profiles.js';
17
18
  import { isPromptCancelled, isInteractiveTerminal, requireDestructiveArg, requireInteractiveSelection, } from './utils.js';
18
19
  const AGENT_NAMES = {
19
20
  claude: 'Claude',
@@ -25,7 +26,8 @@ const AGENT_NAMES = {
25
26
  antigravity: 'Antigravity',
26
27
  };
27
28
  const VALID_AGENTS = Object.keys(AGENT_NAMES);
28
- const VALID_MODES = ['plan', 'edit', 'full'];
29
+ // 'full' kept as historical alias for 'skip'; normalized to 'skip' downstream.
30
+ const VALID_MODES = ['plan', 'edit', 'auto', 'skip', 'full'];
29
31
  const VALID_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'auto'];
30
32
  const VALID_CLOUD_PROVIDERS = ['rush', 'codex', 'factory'];
31
33
  // Auto-enable JSON mode when piped / not a TTY so AI agent consumers get
@@ -87,14 +89,48 @@ function fullName(type, version) {
87
89
  const name = AGENT_NAMES[type];
88
90
  return version ? `${name} ${version}` : name;
89
91
  }
92
+ /**
93
+ * Resolve a teammate spec to its execution target.
94
+ *
95
+ * Accepts:
96
+ * - `claude` — default version of an installed agent
97
+ * - `claude@2.1.112` — pinned version of an installed agent
98
+ * - `<profile-name>` — runs through `agents run <profile>`, with the
99
+ * profile's host agent used as the underlying
100
+ * AgentType for event parsing and CLI checks.
101
+ *
102
+ * `agent` is always the underlying harness so event parsers, CLI-availability
103
+ * checks, and version pins keep working. `profileName` is set only when the
104
+ * spec resolved through a profile.
105
+ */
90
106
  function parseTeammate(spec) {
91
107
  const [name, version] = spec.split('@');
92
- if (!VALID_AGENTS.includes(name)) {
93
- die(`Unknown teammate '${name}'. Available: ${VALID_AGENTS.join(', ')}.\n` +
94
- ` Use the form 'claude' or 'claude@2.1.112' (see 'agents view' for installed versions).`);
108
+ if (VALID_AGENTS.includes(name)) {
109
+ const agent = name;
110
+ return {
111
+ agent,
112
+ version: resolveVersionAlias(agent, version) ?? null,
113
+ profileName: null,
114
+ };
115
+ }
116
+ // Not a built-in agent id — try resolving as a profile name. A profile
117
+ // pinning a version is allowed; `profile@<override>` is not (would conflict
118
+ // with the profile's own host.version).
119
+ if (!version && profileExists(name)) {
120
+ try {
121
+ const profile = readProfile(name);
122
+ return {
123
+ agent: profile.host.agent,
124
+ version: profile.host.version ?? null,
125
+ profileName: profile.name,
126
+ };
127
+ }
128
+ catch (err) {
129
+ die(`Profile '${name}' is malformed: ${err.message}`);
130
+ }
95
131
  }
96
- const agent = name;
97
- return { agent, version: resolveVersionAlias(agent, version) ?? null };
132
+ die(`Unknown teammate '${spec}'. Available agents: ${VALID_AGENTS.join(', ')}.\n` +
133
+ ` Use 'claude', 'claude@2.1.112', or the name of a profile from 'agents view'.`);
98
134
  }
99
135
  function shortId(id) {
100
136
  return id.slice(0, 8);
@@ -547,6 +583,8 @@ export function registerTeamsCommands(program) {
547
583
  Teammate syntax:
548
584
  'claude' the default Claude version on this machine
549
585
  'claude@2.1.112' a specific installed version (see 'agents view')
586
+ '<profile>' a profile from 'agents view' — runs through 'agents
587
+ run <profile>' with the profile's host harness
550
588
 
551
589
  Short aliases:
552
590
  teams c = create teams a = add teams s = status
@@ -711,7 +749,7 @@ export function registerTeamsCommands(program) {
711
749
  .alias('a')
712
750
  .description("Add a teammate to work on a task. Runs in background; returns immediately. Use 'status' to check in.")
713
751
  .option('-n, --name <name>', 'Friendly name for this teammate (e.g. alice). Required if using --after. Unique within team.')
714
- .option('-m, --mode <mode>', `Permissions: plan (read-only) | edit (can write files) | full (write + skip permission prompts)`, 'edit')
752
+ .option('-m, --mode <mode>', `Permissions: plan (read-only) | edit (can write files) | auto (smart classifier auto-approves safe ops) | skip (bypass all permission prompts). 'full' accepted as alias for skip.`, 'edit')
715
753
  .option('-e, --effort <effort>', `Reasoning intensity: ${VALID_EFFORTS.join('|')}`, 'medium')
716
754
  .option('--model <model>', 'Override the effort tier and use this specific model (e.g. claude-opus-4-6)')
717
755
  .option('--env <key=value>', 'Set an environment variable for this teammate (repeatable for multiple vars)', (val, prev) => [...prev, val], [])
@@ -747,7 +785,7 @@ export function registerTeamsCommands(program) {
747
785
  die(`--cloud rush requires --repo <owner/repo>`);
748
786
  }
749
787
  }
750
- const { agent, version } = parseTeammate(teammate);
788
+ const { agent, version, profileName } = parseTeammate(teammate);
751
789
  if (version && !isVersionInstalled(agent, version)) {
752
790
  die(`${AGENT_NAMES[agent]} ${version} isn't installed.\n` +
753
791
  ` Install it: agents add ${agent}@${version}\n` +
@@ -872,12 +910,12 @@ export function registerTeamsCommands(program) {
872
910
  }
873
911
  }
874
912
  try {
875
- const result = await handleSpawn(mgr, team, agent, effectiveTask, cwd, opts.mode, opts.effort, null, cwd, version, opts.name ?? null, after, opts.model ?? null, envOverrides ?? null, taskType, cloudProviderId, cloudSessionId, opts.repo ?? null, opts.branch ?? null, worktreeName, worktreePath);
913
+ const result = await handleSpawn(mgr, team, agent, effectiveTask, cwd, opts.mode, opts.effort, null, cwd, version, opts.name ?? null, after, opts.model ?? null, envOverrides ?? null, taskType, cloudProviderId, cloudSessionId, opts.repo ?? null, opts.branch ?? null, worktreeName, worktreePath, profileName);
876
914
  if (isJsonMode(opts)) {
877
915
  console.log(JSON.stringify(result, null, 2));
878
916
  return;
879
917
  }
880
- const who = fullName(agent, version);
918
+ const who = profileName ? `${profileName} (via ${fullName(agent, version)})` : fullName(agent, version);
881
919
  const staged = result.status === 'pending';
882
920
  const verb = staged ? 'Staged' : 'Welcomed';
883
921
  const greeting = result.name