@phnx-labs/agents-cli 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/commands/browser.js +248 -9
  3. package/dist/commands/cloud.js +8 -0
  4. package/dist/commands/exec.js +70 -1
  5. package/dist/commands/plugins.js +179 -5
  6. package/dist/commands/prune.js +6 -0
  7. package/dist/commands/secrets.js +117 -19
  8. package/dist/commands/view.js +21 -8
  9. package/dist/commands/workflows.d.ts +10 -0
  10. package/dist/commands/workflows.js +457 -0
  11. package/dist/index.js +31 -16
  12. package/dist/lib/browser/cdp.js +7 -4
  13. package/dist/lib/browser/chrome.d.ts +10 -0
  14. package/dist/lib/browser/chrome.js +37 -2
  15. package/dist/lib/browser/drivers/local.js +13 -2
  16. package/dist/lib/browser/input.d.ts +1 -0
  17. package/dist/lib/browser/input.js +3 -0
  18. package/dist/lib/browser/ipc.js +14 -0
  19. package/dist/lib/browser/profiles.d.ts +5 -0
  20. package/dist/lib/browser/profiles.js +45 -0
  21. package/dist/lib/browser/service.d.ts +10 -0
  22. package/dist/lib/browser/service.js +29 -1
  23. package/dist/lib/browser/types.d.ts +11 -1
  24. package/dist/lib/cloud/rush.d.ts +28 -1
  25. package/dist/lib/cloud/rush.js +68 -13
  26. package/dist/lib/commands.d.ts +0 -15
  27. package/dist/lib/commands.js +5 -5
  28. package/dist/lib/hooks.js +24 -11
  29. package/dist/lib/migrate.js +59 -1
  30. package/dist/lib/permissions.d.ts +0 -58
  31. package/dist/lib/permissions.js +10 -10
  32. package/dist/lib/plugins.d.ts +75 -34
  33. package/dist/lib/plugins.js +640 -133
  34. package/dist/lib/resource-patterns.d.ts +41 -0
  35. package/dist/lib/resource-patterns.js +82 -0
  36. package/dist/lib/resources/index.d.ts +17 -0
  37. package/dist/lib/resources/index.js +7 -0
  38. package/dist/lib/resources/types.d.ts +1 -1
  39. package/dist/lib/resources/workflows.d.ts +24 -0
  40. package/dist/lib/resources/workflows.js +110 -0
  41. package/dist/lib/resources.d.ts +6 -1
  42. package/dist/lib/resources.js +12 -2
  43. package/dist/lib/session/db.d.ts +18 -0
  44. package/dist/lib/session/db.js +106 -7
  45. package/dist/lib/session/discover.d.ts +6 -0
  46. package/dist/lib/session/discover.js +28 -17
  47. package/dist/lib/shims.d.ts +3 -51
  48. package/dist/lib/shims.js +18 -10
  49. package/dist/lib/sqlite.js +10 -4
  50. package/dist/lib/state.d.ts +15 -2
  51. package/dist/lib/state.js +29 -8
  52. package/dist/lib/types.d.ts +43 -14
  53. package/dist/lib/versions.d.ts +3 -0
  54. package/dist/lib/versions.js +139 -27
  55. package/dist/lib/workflows.d.ts +79 -0
  56. package/dist/lib/workflows.js +233 -0
  57. package/package.json +1 -5
  58. package/scripts/postinstall.js +59 -58
  59. package/dist/commands/fork.d.ts +0 -10
  60. package/dist/commands/fork.js +0 -146
@@ -8,10 +8,10 @@
8
8
  import * as fs from 'fs';
9
9
  import * as path from 'path';
10
10
  import chalk from 'chalk';
11
+ import { input } from '@inquirer/prompts';
11
12
  import { PLUGINS_CAPABLE_AGENTS, agentLabel } from '../lib/agents.js';
12
- import { discoverPlugins, getPlugin, pluginSupportsAgent, removePluginFromVersion } from '../lib/plugins.js';
13
+ import { discoverPlugins, getPlugin, pluginSupportsAgent, removePluginFromVersion, isPluginSynced, installPlugin, updatePlugin, loadUserConfig, saveUserConfig, checkPluginDependencies, } from '../lib/plugins.js';
13
14
  import { listInstalledVersions, syncResourcesToVersion, getGlobalDefault, getVersionHomePath, } from '../lib/versions.js';
14
- import { isPluginSynced } from '../lib/plugins.js';
15
15
  import { isPromptCancelled, isInteractiveTerminal, requireDestructiveArg, requireInteractiveSelection, promptRemovalTargets, } from './utils.js';
16
16
  import { itemPicker } from '../lib/picker.js';
17
17
  import { showResourceList, buildTargetsSection, } from './resource-view.js';
@@ -308,21 +308,45 @@ Examples:
308
308
  return;
309
309
  }
310
310
  let totalSkills = 0;
311
+ let totalCommands = 0;
312
+ let totalAgentDefs = 0;
311
313
  let totalHooks = 0;
312
314
  let totalPerms = 0;
315
+ let totalMcp = 0;
313
316
  let versionsTouched = 0;
314
317
  for (const target of selectedTargets) {
315
318
  const versionHome = getVersionHomePath(target.agent, target.version);
316
319
  const r = removePluginFromVersion(name, resolvedRoot, target.agent, versionHome);
317
- if (r.skills.length > 0 || r.hooks.length > 0 || r.permissions > 0) {
320
+ const anyRemoved = r.skills.length > 0 || r.commands.length > 0 || r.agentDefs.length > 0 ||
321
+ r.bin.length > 0 || r.hooks.length > 0 || r.permissions > 0 || r.mcp > 0;
322
+ if (anyRemoved) {
318
323
  versionsTouched += 1;
319
324
  totalSkills += r.skills.length;
325
+ totalCommands += r.commands.length;
326
+ totalAgentDefs += r.agentDefs.length;
320
327
  totalHooks += r.hooks.length;
321
328
  totalPerms += r.permissions;
322
- console.log(` ${chalk.red('-')} ${target.label}: ${r.skills.length} skill(s), ${r.hooks.length} hook(s), ${r.permissions} perm(s)`);
329
+ totalMcp += r.mcp;
330
+ const parts = [
331
+ r.skills.length > 0 ? `${r.skills.length} skill(s)` : null,
332
+ r.commands.length > 0 ? `${r.commands.length} command(s)` : null,
333
+ r.agentDefs.length > 0 ? `${r.agentDefs.length} agent def(s)` : null,
334
+ r.hooks.length > 0 ? `${r.hooks.length} hook(s)` : null,
335
+ r.permissions > 0 ? `${r.permissions} perm(s)` : null,
336
+ r.mcp > 0 ? `${r.mcp} MCP server(s)` : null,
337
+ ].filter(Boolean);
338
+ console.log(` ${chalk.red('-')} ${target.label}: ${parts.join(', ')}`);
323
339
  }
324
340
  }
325
- console.log(chalk.green(`\nUnsynced ${name} from ${versionsTouched} version(s) — ${totalSkills} skills, ${totalHooks} hooks, ${totalPerms} permissions`));
341
+ const summary = [
342
+ totalSkills > 0 ? `${totalSkills} skills` : null,
343
+ totalCommands > 0 ? `${totalCommands} commands` : null,
344
+ totalAgentDefs > 0 ? `${totalAgentDefs} agent defs` : null,
345
+ totalHooks > 0 ? `${totalHooks} hooks` : null,
346
+ totalPerms > 0 ? `${totalPerms} permissions` : null,
347
+ totalMcp > 0 ? `${totalMcp} MCP servers` : null,
348
+ ].filter(Boolean).join(', ') || 'nothing';
349
+ console.log(chalk.green(`\nUnsynced ${name} from ${versionsTouched} version(s) — ${summary}`));
326
350
  // Only delete source if ALL targets were selected
327
351
  if (!options.keepSource && selectedTargets.length === availableTargets.length) {
328
352
  if (fs.existsSync(pluginRoot)) {
@@ -337,6 +361,156 @@ Examples:
337
361
  console.log(chalk.gray(`Kept source at ${formatPath(pluginRoot)}`));
338
362
  }
339
363
  });
364
+ // agents plugins install <spec>
365
+ pluginsCmd
366
+ .command('install <spec>')
367
+ .description('Install a plugin from a git URL or local path (format: name@source or source)')
368
+ .addHelpText('after', `
369
+ Examples:
370
+ # Install from a git URL
371
+ agents plugins install my-plugin@https://github.com/user/my-plugin.git
372
+
373
+ # Install from a local path
374
+ agents plugins install /path/to/plugin
375
+
376
+ # Named install from a local path
377
+ agents plugins install rush-toolkit@~/Projects/rush-toolkit
378
+ `)
379
+ .action(async (spec) => {
380
+ console.log(chalk.gray(`Installing plugin from: ${spec}`));
381
+ let name;
382
+ let root;
383
+ try {
384
+ const result = await installPlugin(spec);
385
+ name = result.name;
386
+ root = result.root;
387
+ }
388
+ catch (err) {
389
+ console.log(chalk.red(`Install failed: ${err.message}`));
390
+ process.exit(1);
391
+ }
392
+ const plugin = getPlugin(name);
393
+ if (!plugin) {
394
+ console.log(chalk.red(`Installed but could not load plugin '${name}'`));
395
+ process.exit(1);
396
+ }
397
+ // Check dependencies
398
+ const missingDeps = checkPluginDependencies(plugin.manifest);
399
+ if (missingDeps.length > 0) {
400
+ console.log(chalk.yellow(`Warning: missing dependencies: ${missingDeps.join(', ')}`));
401
+ console.log(chalk.gray('Install them with: agents plugins install <name>@<source>'));
402
+ }
403
+ // Prompt for userConfig fields
404
+ if (plugin.manifest.userConfig && plugin.manifest.userConfig.length > 0 && isInteractiveTerminal()) {
405
+ const existingConfig = loadUserConfig(name);
406
+ const newConfig = await promptUserConfig(plugin.manifest, existingConfig);
407
+ if (Object.keys(newConfig).length > 0) {
408
+ saveUserConfig(name, { ...existingConfig, ...newConfig });
409
+ console.log(chalk.gray('User config saved.'));
410
+ }
411
+ }
412
+ // Sync to all supported installed versions
413
+ console.log();
414
+ let synced = 0;
415
+ for (const agentId of PLUGINS_CAPABLE_AGENTS) {
416
+ if (!pluginSupportsAgent(plugin, agentId))
417
+ continue;
418
+ const versions = listInstalledVersions(agentId);
419
+ if (versions.length === 0)
420
+ continue;
421
+ const defaultVer = getGlobalDefault(agentId);
422
+ const targetVersions = defaultVer ? [defaultVer] : [versions[versions.length - 1]];
423
+ for (const version of targetVersions) {
424
+ const syncResult = syncResourcesToVersion(agentId, version, { plugins: [name] });
425
+ if (syncResult.plugins.length > 0) {
426
+ console.log(chalk.green(` Synced to ${agentLabel(agentId)}@${version}`));
427
+ synced++;
428
+ }
429
+ }
430
+ }
431
+ if (synced === 0) {
432
+ console.log(chalk.gray(' No supported agent versions installed — run "agents use <agent>@<version>" to sync.'));
433
+ }
434
+ console.log(chalk.bold(`\nInstalled ${plugin.name} v${plugin.manifest.version} to ${formatPath(root)}`));
435
+ });
436
+ // agents plugins update [name]
437
+ pluginsCmd
438
+ .command('update [name]')
439
+ .description('Re-pull a plugin from its original source and re-sync to all versions')
440
+ .addHelpText('after', `
441
+ Examples:
442
+ # Update a specific plugin
443
+ agents plugins update rush-toolkit
444
+
445
+ # Update all plugins
446
+ agents plugins update
447
+ `)
448
+ .action(async (nameArg) => {
449
+ const plugins = nameArg ? [getPlugin(nameArg)].filter(Boolean) : discoverPlugins();
450
+ if (nameArg && plugins.length === 0) {
451
+ console.log(chalk.red(`Plugin '${nameArg}' not found`));
452
+ process.exit(1);
453
+ }
454
+ if (plugins.length === 0) {
455
+ console.log(chalk.gray('No plugins installed.'));
456
+ return;
457
+ }
458
+ for (const plugin of plugins) {
459
+ process.stdout.write(`Updating ${plugin.name}... `);
460
+ const result = await updatePlugin(plugin.name);
461
+ if (!result.success) {
462
+ console.log(chalk.red(`failed — ${result.error || 'unknown error'}`));
463
+ continue;
464
+ }
465
+ console.log(chalk.green('done'));
466
+ // Re-sync to all supported installed versions
467
+ for (const agentId of PLUGINS_CAPABLE_AGENTS) {
468
+ if (!pluginSupportsAgent(plugin, agentId))
469
+ continue;
470
+ const versions = listInstalledVersions(agentId);
471
+ const defaultVer = getGlobalDefault(agentId);
472
+ const targetVersions = defaultVer ? [defaultVer] : versions.slice(-1);
473
+ for (const version of targetVersions) {
474
+ const syncResult = syncResourcesToVersion(agentId, version, { plugins: [plugin.name] });
475
+ if (syncResult.plugins.length > 0) {
476
+ console.log(chalk.gray(` Re-synced to ${agentLabel(agentId)}@${version}`));
477
+ }
478
+ }
479
+ }
480
+ }
481
+ });
482
+ }
483
+ /**
484
+ * Prompt for missing or empty userConfig fields interactively.
485
+ * Only prompts for fields not already present in existingConfig.
486
+ */
487
+ async function promptUserConfig(manifest, existingConfig = {}) {
488
+ const result = {};
489
+ const fields = manifest.userConfig || [];
490
+ for (const field of fields) {
491
+ if (existingConfig[field.key] !== undefined)
492
+ continue;
493
+ const defaultValue = field.default ?? '';
494
+ try {
495
+ const value = await input({
496
+ message: field.description + (field.required ? ' (required)' : ' (optional)'),
497
+ default: defaultValue || undefined,
498
+ required: field.required ?? false,
499
+ });
500
+ if (value) {
501
+ result[field.key] = value;
502
+ }
503
+ else if (defaultValue) {
504
+ result[field.key] = defaultValue;
505
+ }
506
+ }
507
+ catch (err) {
508
+ if (isPromptCancelled(err))
509
+ break;
510
+ throw err;
511
+ }
512
+ }
513
+ return result;
340
514
  }
341
515
  /** Convert discovered plugins into rows suitable for the resource list view. */
342
516
  function buildPluginRows(plugins) {
@@ -404,12 +404,18 @@ Examples:
404
404
  # Full sweep: orphan resources + duplicate versions for current defaults
405
405
  agents prune
406
406
 
407
+ # Preview what a full sweep would remove
408
+ agents prune --dry-run
409
+
407
410
  # Just orphan skills
408
411
  agents prune skills
409
412
 
410
413
  # Just version dedup
411
414
  agents prune versions
412
415
 
416
+ # Deduplicate versions for one agent only
417
+ agents prune claude
418
+
413
419
  # Sweep every installed version's orphans, not only the defaults
414
420
  agents prune --all
415
421
 
@@ -9,6 +9,7 @@ import chalk from 'chalk';
9
9
  import * as fs from 'fs';
10
10
  import { bundleExists, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readBundle, rotateBundleSecret, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
11
11
  import { deleteKeychainToken, getKeychainToken, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
12
+ import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
12
13
  import { registerCommandGroups } from '../lib/help.js';
13
14
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
14
15
  /** Prompt the user for a secret value with masked input. Requires an interactive TTY. */
@@ -91,6 +92,24 @@ async function promptKeyName(bundleName) {
91
92
  },
92
93
  });
93
94
  }
95
+ /** Resolve a 1Password vault name — use the provided value, or prompt interactively. */
96
+ async function resolveVault(vaultOpt) {
97
+ if (vaultOpt)
98
+ return vaultOpt;
99
+ const vaults = listVaults();
100
+ if (vaults.length === 0)
101
+ throw new Error('No 1Password vaults found. Make sure you are signed in: op signin');
102
+ if (vaults.length === 1)
103
+ return vaults[0].name;
104
+ if (!isInteractiveTerminal()) {
105
+ throw new Error(`Multiple vaults found. Pass --vault <name> (available: ${vaults.map((v) => v.name).join(', ')})`);
106
+ }
107
+ const { select } = await import('@inquirer/prompts');
108
+ return await select({
109
+ message: 'Which 1Password vault?',
110
+ choices: vaults.map((v) => ({ name: v.name, value: v.name })),
111
+ });
112
+ }
94
113
  /** Read all available data from stdin synchronously, trimmed. */
95
114
  function readStdinSync() {
96
115
  const chunks = [];
@@ -268,6 +287,13 @@ Examples:
268
287
  # Import an entire .env file straight into keychain
269
288
  agents secrets import prod --from .env.prod
270
289
 
290
+ # Import secrets from a 1Password vault
291
+ agents secrets import prod --from-1password --vault "Rush Prod"
292
+
293
+ # Push a bundle back to 1Password (vault migration, backup)
294
+ agents secrets export prod --to-1password --vault "Rush Prod"
295
+ agents secrets export prod --to-1password --vault "Rush Prod" --force
296
+
271
297
  # See what's in a bundle (values masked)
272
298
  agents secrets view prod
273
299
 
@@ -649,35 +675,71 @@ Examples:
649
675
  });
650
676
  cmd
651
677
  .command('import [bundle]')
652
- .description('Import keys from a .env file into a bundle. By default every key is stored in keychain.')
653
- .requiredOption('--from <path>', 'Path to a .env file')
678
+ .description('Import keys from a .env file or a 1Password vault into a bundle. By default every key is stored in keychain.')
679
+ .option('--from <path>', 'Path to a .env file')
680
+ .option('--from-1password', 'Import secrets from a 1Password vault (requires the op CLI)')
681
+ .option('--vault <name>', '1Password vault name (used with --from-1password)')
654
682
  .option('--all-plaintext', 'Store every imported value as a literal in the bundle metadata (skip keychain item creation)')
655
683
  .option('--force', 'Overwrite an existing key in the bundle')
656
684
  .action(async (bundleName, opts) => {
657
685
  try {
686
+ if (!opts.from && !opts.from1password) {
687
+ throw new Error('Pass --from <path> to import a .env file, or --from-1password to import from a 1Password vault.');
688
+ }
689
+ if (opts.from && opts.from1password) {
690
+ throw new Error('--from and --from-1password are mutually exclusive.');
691
+ }
658
692
  const resolvedBundleName = bundleName ?? (await pickBundleName('import into'));
659
693
  const bundle = readBundle(resolvedBundleName);
660
- const raw = fs.readFileSync(opts.from, 'utf-8');
661
- const pairs = parseDotenv(raw);
662
694
  let added = 0;
663
695
  let skipped = 0;
664
- for (const [key, value] of Object.entries(pairs)) {
665
- if (!opts.force && key in bundle.vars) {
666
- skipped++;
667
- continue;
696
+ if (opts.from1password) {
697
+ assertOpAvailable();
698
+ const vault = await resolveVault(opts.vault);
699
+ const items = listItems(vault);
700
+ const { secrets, skipped: opSkipped } = extractSecrets(items, vault);
701
+ for (const { envKey, value } of secrets) {
702
+ if (!opts.force && envKey in bundle.vars) {
703
+ skipped++;
704
+ continue;
705
+ }
706
+ if (opts.allPlaintext) {
707
+ bundle.vars[envKey] = { value };
708
+ }
709
+ else {
710
+ const item = secretsKeychainItem(resolvedBundleName, envKey);
711
+ setKeychainToken(item, value, bundle.icloud_sync);
712
+ bundle.vars[envKey] = keychainRef(envKey);
713
+ }
714
+ added++;
668
715
  }
669
- if (opts.allPlaintext) {
670
- bundle.vars[key] = { value };
716
+ writeBundle(bundle);
717
+ if (opSkipped.length) {
718
+ console.log(chalk.yellow(`Skipped ${opSkipped.length} item(s) with no importable fields.`));
671
719
  }
672
- else {
673
- const item = secretsKeychainItem(resolvedBundleName, key);
674
- setKeychainToken(item, value, bundle.icloud_sync);
675
- bundle.vars[key] = keychainRef(key);
720
+ console.log(chalk.green(`Imported ${added} key(s) from 1Password vault '${vault}'${skipped ? `, skipped ${skipped} (already set, pass --force)` : ''}.`));
721
+ }
722
+ else {
723
+ const raw = fs.readFileSync(opts.from, 'utf-8');
724
+ const pairs = parseDotenv(raw);
725
+ for (const [key, value] of Object.entries(pairs)) {
726
+ if (!opts.force && key in bundle.vars) {
727
+ skipped++;
728
+ continue;
729
+ }
730
+ if (opts.allPlaintext) {
731
+ bundle.vars[key] = { value };
732
+ }
733
+ else {
734
+ const item = secretsKeychainItem(resolvedBundleName, key);
735
+ setKeychainToken(item, value, bundle.icloud_sync);
736
+ bundle.vars[key] = keychainRef(key);
737
+ }
738
+ added++;
676
739
  }
677
- added++;
740
+ writeBundle(bundle);
741
+ console.log(chalk.green(`Imported ${added} key(s)${skipped ? `, skipped ${skipped} (already set, pass --force)` : ''}.`));
678
742
  }
679
- writeBundle(bundle);
680
- console.log(chalk.green(`Imported ${added} key(s)${skipped ? `, skipped ${skipped} (already set, pass --force)` : ''}.`));
681
743
  }
682
744
  catch (err) {
683
745
  if (isPromptCancelled(err))
@@ -688,13 +750,49 @@ Examples:
688
750
  });
689
751
  cmd
690
752
  .command('export [bundle]')
691
- .description('Resolve a bundle and print KEY=VALUE lines (for `eval "$(agents secrets export prod)"`). Refuses on a TTY unless --plaintext.')
692
- .option('--plaintext', 'Acknowledge that the resolved values will be printed in the clear')
753
+ .description('Resolve a bundle and print KEY=VALUE lines, or push it to a 1Password vault with --to-1password.')
754
+ .option('--plaintext', 'Acknowledge that the resolved values will be printed in the clear (shell export mode)')
755
+ .option('--to-1password', 'Push every key in the bundle as a PASSWORD item in a 1Password vault')
756
+ .option('--vault <name>', '1Password vault name (used with --to-1password)')
757
+ .option('--force', 'Overwrite existing 1Password items (used with --to-1password)')
693
758
  .action(async (bundleName, opts) => {
694
759
  try {
695
760
  const { resolveBundleEnv, bundleToEnvPrefix, isReservedEnvName } = await import('../lib/secrets/bundles.js');
696
761
  const resolvedBundleName = bundleName ?? (await pickBundleName('export'));
697
762
  const bundle = readBundle(resolvedBundleName);
763
+ if (opts.to1password) {
764
+ assertOpAvailable();
765
+ const vault = await resolveVault(opts.vault);
766
+ const env = resolveBundleEnv(bundle);
767
+ let created = 0;
768
+ let overwritten = 0;
769
+ let skipped = 0;
770
+ for (const [key, value] of Object.entries(env)) {
771
+ const exists = itemExistsByTitle(key, vault);
772
+ if (exists) {
773
+ if (!opts.force) {
774
+ skipped++;
775
+ continue;
776
+ }
777
+ deleteItemByTitle(key, vault);
778
+ createPasswordItem(key, value, vault);
779
+ overwritten++;
780
+ }
781
+ else {
782
+ createPasswordItem(key, value, vault);
783
+ created++;
784
+ }
785
+ }
786
+ const parts = [];
787
+ if (created)
788
+ parts.push(`${created} created`);
789
+ if (overwritten)
790
+ parts.push(`${overwritten} overwritten`);
791
+ if (skipped)
792
+ parts.push(`${skipped} skipped (already exist, pass --force)`);
793
+ console.log(chalk.green(`Exported to 1Password vault '${vault}': ${parts.join(', ')}.`));
794
+ return;
795
+ }
698
796
  if (isInteractiveTerminal() && !opts.plaintext) {
699
797
  console.error(chalk.red('export to a TTY requires --plaintext (prevents shoulder-surfing).'));
700
798
  process.exit(1);
@@ -8,6 +8,7 @@ import { readManifest } from '../lib/manifest.js';
8
8
  import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, resolveVersionAlias, getAvailableResources, getActuallySyncedResources, getNewResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, } from '../lib/versions.js';
9
9
  import { getShimsDir, isShimsInPath, ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
10
10
  import { getAgentResources } from '../lib/resources.js';
11
+ import { WORKFLOW_CAPABLE_AGENTS } from '../lib/workflows.js';
11
12
  import { getAgentsDir, getUserAgentsDir, getEffectivePromptcutsPath, readMergedPromptcuts } from '../lib/state.js';
12
13
  import { isGitRepo, getGitSyncStatus } from '../lib/git.js';
13
14
  import { getCentralRulesFileName } from '../lib/rules/rules.js';
@@ -15,6 +16,10 @@ import { composeRulesFromState } from '../lib/rules/compose.js';
15
16
  import { getConfiguredRunStrategy } from '../lib/rotate.js';
16
17
  import { confirm } from '@inquirer/prompts';
17
18
  import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
19
+ function termLink(text, filePath) {
20
+ const url = `file://${filePath}`;
21
+ return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
22
+ }
18
23
  function formatLastActive(date) {
19
24
  if (!date)
20
25
  return '';
@@ -356,6 +361,8 @@ async function showInstalledVersions(filterAgentId) {
356
361
  synced.push('mcp');
357
362
  if (result.plugins.length > 0)
358
363
  synced.push('plugins');
364
+ if (result.workflows.length > 0)
365
+ synced.push('workflows');
359
366
  if (synced.length > 0) {
360
367
  console.log(chalk.green(`\nSynced to ${agentLabel(filterAgentId)}@${defaultVersion}: ${synced.join(', ')}`));
361
368
  }
@@ -478,6 +485,7 @@ async function showAgentResources(agentId, requestedVersion) {
478
485
  ...r,
479
486
  syncState: r.scope === 'project' ? undefined : getSyncState(r.name, 'hooks', hooksSync),
480
487
  })),
488
+ workflows: resources.workflows.map(r => ({ name: r.name, path: r.path, scope: r.scope })),
481
489
  };
482
490
  spinner.stop();
483
491
  // Render helper for resources
@@ -488,7 +496,8 @@ async function showAgentResources(agentId, requestedVersion) {
488
496
  return;
489
497
  }
490
498
  const versionStr = agentData.version ? ` (${agentData.version})` : '';
491
- console.log(` ${chalk.bold(agentData.agentName)}${chalk.gray(versionStr)}:`);
499
+ const agentHeader = home ? termLink(agentData.agentName, home) : agentData.agentName;
500
+ console.log(` ${chalk.bold(agentHeader)}${chalk.gray(versionStr)}:`);
492
501
  for (const r of items) {
493
502
  let nameColor = chalk.cyan;
494
503
  if (r.syncState === 'synced')
@@ -499,7 +508,8 @@ async function showAgentResources(agentId, requestedVersion) {
499
508
  nameColor = chalk.yellow;
500
509
  else if (r.syncState === 'deleted')
501
510
  nameColor = chalk.red;
502
- let display = nameColor(r.name);
511
+ const linkedName = r.path ? termLink(r.name, r.path) : r.name;
512
+ let display = nameColor(linkedName);
503
513
  if (r.ruleCount !== undefined)
504
514
  display += chalk.gray(` (${r.ruleCount} rules)`);
505
515
  // Source annotation: project overrides user, user overrides system
@@ -507,9 +517,8 @@ async function showAgentResources(agentId, requestedVersion) {
507
517
  : r.scope === 'user' ? chalk.cyan('[user]')
508
518
  : chalk.gray('[system]');
509
519
  display += ` ${sourceTag}`;
510
- const pathStr = r.path ? chalk.gray(formatPath(r.path, cwd)) : '';
511
520
  const syncStr = r.syncState ? chalk.gray(` [${r.syncState}]`) : '';
512
- console.log(` ${display.padEnd(38)} ${pathStr}${syncStr}`);
521
+ console.log(` ${display}${syncStr}`);
513
522
  }
514
523
  }
515
524
  // Render promptcuts (cross-agent, not per-version). Shortcuts are layered
@@ -559,6 +568,9 @@ async function showAgentResources(agentId, requestedVersion) {
559
568
  }
560
569
  }
561
570
  renderSection('MCP Servers', agentData.mcp);
571
+ if (WORKFLOW_CAPABLE_AGENTS.includes(agentId)) {
572
+ renderSection('Workflows', agentData.workflows);
573
+ }
562
574
  // Rules section with subrules breakdown
563
575
  function renderRulesSection() {
564
576
  console.log(chalk.bold('\nRules\n'));
@@ -588,16 +600,16 @@ async function showAgentResources(agentId, requestedVersion) {
588
600
  nameColor = chalk.yellow;
589
601
  else if (r.syncState === 'deleted')
590
602
  nameColor = chalk.red;
591
- let display = nameColor(r.name);
603
+ const linkedName = r.path ? termLink(r.name, r.path) : r.name;
604
+ let display = nameColor(linkedName);
592
605
  if (r.ruleCount !== undefined)
593
606
  display += chalk.gray(` (${r.ruleCount} rules)`);
594
607
  const sourceTag = r.scope === 'project' ? chalk.blue('[project]')
595
608
  : r.scope === 'user' ? chalk.cyan('[user]')
596
609
  : chalk.gray('[system]');
597
610
  display += ` ${sourceTag}`;
598
- const pathStr = r.path ? chalk.gray(formatPath(r.path, cwd)) : '';
599
611
  const syncStr = r.syncState ? chalk.gray(` [${r.syncState}]`) : '';
600
- console.log(` ${display.padEnd(38)} ${pathStr}${syncStr}`);
612
+ console.log(` ${display}${syncStr}`);
601
613
  // Show subrules for user-scope rules (the compiled CLAUDE.md)
602
614
  if (r.scope === 'user' && composedSubrules.length > 0) {
603
615
  for (const sub of composedSubrules) {
@@ -605,7 +617,8 @@ async function showAgentResources(agentId, requestedVersion) {
605
617
  : sub.layerScope === 'user' ? chalk.cyan('[user]')
606
618
  : sub.layerScope === 'extra' ? chalk.magenta(`[${sub.layerAlias || 'extra'}]`)
607
619
  : chalk.gray('[system]');
608
- console.log(` ${chalk.gray('-')} ${sub.name} ${scopeLabel}`);
620
+ const linkedSubName = termLink(sub.name, sub.sourcePath);
621
+ console.log(` ${chalk.gray('-')} ${linkedSubName} ${scopeLabel}`);
609
622
  }
610
623
  }
611
624
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Workflow management commands.
3
+ *
4
+ * Implements `agents workflows` — list, view, add, remove pipeline workflows
5
+ * (WORKFLOW.md bundles with optional subagents/, skills/, plugins/ subdirs).
6
+ * Run a workflow with: agents run <workflow-name>
7
+ */
8
+ import type { Command } from 'commander';
9
+ /** Register the `agents workflows` command tree (list, view, add, remove). */
10
+ export declare function registerWorkflowsCommands(program: Command): void;