@phnx-labs/agents-cli 1.15.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 (111) hide show
  1. package/CHANGELOG.md +143 -39
  2. package/README.md +6 -6
  3. package/dist/commands/alias.js +2 -2
  4. package/dist/commands/browser-picker.d.ts +21 -0
  5. package/dist/commands/browser-picker.js +114 -0
  6. package/dist/commands/browser.js +793 -83
  7. package/dist/commands/cloud.js +8 -0
  8. package/dist/commands/commands.js +72 -22
  9. package/dist/commands/daemon.js +2 -2
  10. package/dist/commands/exec.js +70 -1
  11. package/dist/commands/hooks.js +71 -26
  12. package/dist/commands/mcp.js +81 -39
  13. package/dist/commands/plugins.js +224 -17
  14. package/dist/commands/prune.js +29 -1
  15. package/dist/commands/pull.js +3 -3
  16. package/dist/commands/repo.js +1 -1
  17. package/dist/commands/routines.js +2 -2
  18. package/dist/commands/secrets.js +154 -20
  19. package/dist/commands/sessions.js +62 -19
  20. package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
  21. package/dist/commands/{init.js → setup.js} +22 -21
  22. package/dist/commands/skills.js +60 -19
  23. package/dist/commands/subagents.js +41 -13
  24. package/dist/commands/utils.d.ts +16 -0
  25. package/dist/commands/utils.js +32 -0
  26. package/dist/commands/view.js +78 -20
  27. package/dist/commands/workflows.d.ts +10 -0
  28. package/dist/commands/workflows.js +457 -0
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.js +48 -36
  31. package/dist/lib/agents.js +2 -2
  32. package/dist/lib/auto-pull-worker.js +2 -3
  33. package/dist/lib/auto-pull.js +2 -2
  34. package/dist/lib/browser/cdp.d.ts +7 -1
  35. package/dist/lib/browser/cdp.js +32 -1
  36. package/dist/lib/browser/chrome.d.ts +10 -0
  37. package/dist/lib/browser/chrome.js +41 -3
  38. package/dist/lib/browser/devices.d.ts +4 -0
  39. package/dist/lib/browser/devices.js +27 -0
  40. package/dist/lib/browser/drivers/local.js +22 -6
  41. package/dist/lib/browser/drivers/ssh.js +9 -2
  42. package/dist/lib/browser/input.d.ts +1 -0
  43. package/dist/lib/browser/input.js +3 -0
  44. package/dist/lib/browser/ipc.js +158 -23
  45. package/dist/lib/browser/profiles.d.ts +10 -2
  46. package/dist/lib/browser/profiles.js +122 -37
  47. package/dist/lib/browser/service.d.ts +91 -13
  48. package/dist/lib/browser/service.js +767 -132
  49. package/dist/lib/browser/types.d.ts +91 -3
  50. package/dist/lib/browser/types.js +16 -0
  51. package/dist/lib/cloud/rush.d.ts +28 -1
  52. package/dist/lib/cloud/rush.js +69 -14
  53. package/dist/lib/cloud/store.js +2 -2
  54. package/dist/lib/commands.d.ts +1 -15
  55. package/dist/lib/commands.js +11 -7
  56. package/dist/lib/daemon.js +2 -3
  57. package/dist/lib/doctor-diff.js +4 -4
  58. package/dist/lib/events.js +2 -2
  59. package/dist/lib/hooks.d.ts +11 -7
  60. package/dist/lib/hooks.js +138 -49
  61. package/dist/lib/migrate.d.ts +1 -1
  62. package/dist/lib/migrate.js +1237 -22
  63. package/dist/lib/models.js +2 -2
  64. package/dist/lib/permissions.d.ts +8 -66
  65. package/dist/lib/permissions.js +18 -18
  66. package/dist/lib/plugins.d.ts +94 -24
  67. package/dist/lib/plugins.js +702 -123
  68. package/dist/lib/pty-server.js +9 -10
  69. package/dist/lib/resource-patterns.d.ts +41 -0
  70. package/dist/lib/resource-patterns.js +82 -0
  71. package/dist/lib/resources/hooks.d.ts +5 -1
  72. package/dist/lib/resources/hooks.js +21 -4
  73. package/dist/lib/resources/index.d.ts +17 -0
  74. package/dist/lib/resources/index.js +7 -0
  75. package/dist/lib/resources/types.d.ts +1 -1
  76. package/dist/lib/resources/workflows.d.ts +24 -0
  77. package/dist/lib/resources/workflows.js +110 -0
  78. package/dist/lib/resources.d.ts +6 -1
  79. package/dist/lib/resources.js +12 -2
  80. package/dist/lib/rotate.js +3 -4
  81. package/dist/lib/session/active.d.ts +3 -0
  82. package/dist/lib/session/active.js +92 -6
  83. package/dist/lib/session/cloud.js +2 -2
  84. package/dist/lib/session/db.d.ts +18 -0
  85. package/dist/lib/session/db.js +109 -5
  86. package/dist/lib/session/discover.d.ts +6 -0
  87. package/dist/lib/session/discover.js +55 -29
  88. package/dist/lib/session/team-filter.js +2 -2
  89. package/dist/lib/shims.d.ts +4 -52
  90. package/dist/lib/shims.js +23 -15
  91. package/dist/lib/skills.js +6 -2
  92. package/dist/lib/sqlite.js +10 -4
  93. package/dist/lib/state.d.ts +101 -16
  94. package/dist/lib/state.js +179 -31
  95. package/dist/lib/subagents.d.ts +28 -0
  96. package/dist/lib/subagents.js +98 -1
  97. package/dist/lib/sync-manifest.d.ts +1 -1
  98. package/dist/lib/sync-manifest.js +3 -3
  99. package/dist/lib/teams/persistence.js +15 -5
  100. package/dist/lib/teams/registry.js +2 -2
  101. package/dist/lib/types.d.ts +75 -17
  102. package/dist/lib/types.js +3 -3
  103. package/dist/lib/usage.js +2 -2
  104. package/dist/lib/versions.d.ts +3 -0
  105. package/dist/lib/versions.js +158 -47
  106. package/dist/lib/workflows.d.ts +79 -0
  107. package/dist/lib/workflows.js +233 -0
  108. package/package.json +1 -5
  109. package/scripts/postinstall.js +60 -59
  110. package/dist/commands/fork.d.ts +0 -10
  111. package/dist/commands/fork.js +0 -146
@@ -8,11 +8,11 @@
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
- import { isPromptCancelled, isInteractiveTerminal, requireDestructiveArg, requireInteractiveSelection, } from './utils.js';
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';
18
18
  import { getPluginsDir } from '../lib/state.js';
@@ -255,7 +255,7 @@ Examples:
255
255
  # Unsync but keep source directory
256
256
  agents plugins remove rush-toolkit --keep-source
257
257
  `)
258
- .action((nameArg, options) => {
258
+ .action(async (nameArg, options) => {
259
259
  if (!nameArg) {
260
260
  requireDestructiveArg({
261
261
  argName: 'name',
@@ -275,35 +275,242 @@ Examples:
275
275
  console.log(chalk.red(`Plugin '${name}' not found`));
276
276
  process.exit(1);
277
277
  }
278
- let totalSkills = 0;
279
- let totalHooks = 0;
280
- let totalPerms = 0;
281
- let versionsTouched = 0;
278
+ // Build list of targets that have this plugin synced
279
+ const availableTargets = [];
282
280
  for (const agentId of PLUGINS_CAPABLE_AGENTS) {
281
+ if (plugin && !pluginSupportsAgent(plugin, agentId))
282
+ continue;
283
283
  const versions = listInstalledVersions(agentId);
284
284
  for (const version of versions) {
285
285
  const versionHome = getVersionHomePath(agentId, version);
286
- const r = removePluginFromVersion(name, resolvedRoot, agentId, versionHome);
287
- if (r.skills.length > 0 || r.hooks.length > 0 || r.permissions > 0) {
288
- versionsTouched += 1;
289
- totalSkills += r.skills.length;
290
- totalHooks += r.hooks.length;
291
- totalPerms += r.permissions;
292
- console.log(chalk.gray(` ${agentLabel(agentId)}@${version}: ${r.skills.length} skill(s), ${r.hooks.length} hook(s), ${r.permissions} perm(s)`));
286
+ if (plugin && isPluginSynced(plugin, agentId, versionHome)) {
287
+ availableTargets.push({ agent: agentId, version });
293
288
  }
294
289
  }
295
290
  }
296
- console.log(chalk.green(`Unsynced ${name} from ${versionsTouched} version(s) — ${totalSkills} skills, ${totalHooks} hooks, ${totalPerms} permissions`));
297
- if (!options.keepSource) {
291
+ if (availableTargets.length === 0) {
292
+ console.log(chalk.yellow(`Plugin '${name}' not synced to any version.`));
293
+ if (!options.keepSource && fs.existsSync(pluginRoot)) {
294
+ fs.rmSync(pluginRoot, { recursive: true, force: true });
295
+ console.log(chalk.green(`Deleted ${formatPath(pluginRoot)}`));
296
+ }
297
+ return;
298
+ }
299
+ // Show multi-select picker for targets
300
+ const removalTargets = availableTargets.map((t) => ({
301
+ agent: t.agent,
302
+ version: t.version,
303
+ label: `${agentLabel(t.agent)}@${t.version}`,
304
+ }));
305
+ const selectedTargets = await promptRemovalTargets(name, removalTargets);
306
+ if (selectedTargets.length === 0) {
307
+ console.log(chalk.gray('Cancelled.'));
308
+ return;
309
+ }
310
+ let totalSkills = 0;
311
+ let totalCommands = 0;
312
+ let totalAgentDefs = 0;
313
+ let totalHooks = 0;
314
+ let totalPerms = 0;
315
+ let totalMcp = 0;
316
+ let versionsTouched = 0;
317
+ for (const target of selectedTargets) {
318
+ const versionHome = getVersionHomePath(target.agent, target.version);
319
+ const r = removePluginFromVersion(name, resolvedRoot, target.agent, versionHome);
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) {
323
+ versionsTouched += 1;
324
+ totalSkills += r.skills.length;
325
+ totalCommands += r.commands.length;
326
+ totalAgentDefs += r.agentDefs.length;
327
+ totalHooks += r.hooks.length;
328
+ totalPerms += r.permissions;
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(', ')}`);
339
+ }
340
+ }
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}`));
350
+ // Only delete source if ALL targets were selected
351
+ if (!options.keepSource && selectedTargets.length === availableTargets.length) {
298
352
  if (fs.existsSync(pluginRoot)) {
299
353
  fs.rmSync(pluginRoot, { recursive: true, force: true });
300
354
  console.log(chalk.green(`Deleted ${formatPath(pluginRoot)}`));
301
355
  }
302
356
  }
357
+ else if (!options.keepSource && selectedTargets.length < availableTargets.length) {
358
+ console.log(chalk.gray(`Source kept — plugin still synced to other versions.`));
359
+ }
303
360
  else {
304
361
  console.log(chalk.gray(`Kept source at ${formatPath(pluginRoot)}`));
305
362
  }
306
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;
307
514
  }
308
515
  /** Convert discovered plugins into rows suitable for the resource list view. */
309
516
  function buildPluginRows(plugins) {
@@ -27,6 +27,8 @@ import { confirm } from '@inquirer/prompts';
27
27
  import { diffVersionCommands, iterCommandsCapableVersions, removeCommandFromVersion, } from '../lib/commands.js';
28
28
  import { diffVersionSkills, iterSkillsCapableVersions, removeSkillFromVersion, } from '../lib/skills.js';
29
29
  import { diffVersionHooks, iterHooksCapableVersions, removeHookFromVersion, } from '../lib/hooks.js';
30
+ import { diffVersionPlugins, iterPluginsCapableVersions, removePluginSkillFromVersion, } from '../lib/plugins.js';
31
+ import { diffVersionSubagents, iterSubagentsCapableVersions, removeSubagentFromVersion, } from '../lib/subagents.js';
30
32
  import { getGlobalDefault } from '../lib/versions.js';
31
33
  import { resolveAgentName, formatAgentError } from '../lib/agents.js';
32
34
  import { pruneDuplicates } from './view.js';
@@ -34,7 +36,7 @@ import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
34
36
  import { getTrashDir } from '../lib/state.js';
35
37
  import { countSessionsOlderThan, deleteSessionsOlderThan } from '../lib/session/db.js';
36
38
  import { previewRunsPrune, pruneRuns, countAllRuns } from '../lib/routines.js';
37
- const RESOURCE_TYPES = ['commands', 'skills', 'hooks'];
39
+ const RESOURCE_TYPES = ['commands', 'skills', 'hooks', 'plugins', 'subagents'];
38
40
  const STATE_TYPES = ['trash', 'sessions', 'runs'];
39
41
  const ALL_TYPES = [...RESOURCE_TYPES, 'versions', ...STATE_TYPES];
40
42
  function scopePairs(pairs, all) {
@@ -68,6 +70,22 @@ function collectOrphans(types, all) {
68
70
  }
69
71
  }
70
72
  }
73
+ if (types.includes('plugins')) {
74
+ for (const { agent, version } of scopePairs(iterPluginsCapableVersions(), all)) {
75
+ const diff = diffVersionPlugins(agent, version);
76
+ if (diff.orphans.length > 0) {
77
+ groups.push({ type: 'plugins', agent, version, orphans: diff.orphans });
78
+ }
79
+ }
80
+ }
81
+ if (types.includes('subagents')) {
82
+ for (const { agent, version } of scopePairs(iterSubagentsCapableVersions(), all)) {
83
+ const diff = diffVersionSubagents(agent, version);
84
+ if (diff.orphans.length > 0) {
85
+ groups.push({ type: 'subagents', agent, version, orphans: diff.orphans });
86
+ }
87
+ }
88
+ }
71
89
  return groups;
72
90
  }
73
91
  function removeOne(group, name) {
@@ -78,6 +96,10 @@ function removeOne(group, name) {
78
96
  return removeSkillFromVersion(group.agent, group.version, name);
79
97
  case 'hooks':
80
98
  return removeHookFromVersion(group.agent, group.version, name);
99
+ case 'plugins':
100
+ return removePluginSkillFromVersion(group.agent, group.version, name);
101
+ case 'subagents':
102
+ return removeSubagentFromVersion(group.agent, group.version, name);
81
103
  }
82
104
  }
83
105
  function parseTarget(arg) {
@@ -382,12 +404,18 @@ Examples:
382
404
  # Full sweep: orphan resources + duplicate versions for current defaults
383
405
  agents prune
384
406
 
407
+ # Preview what a full sweep would remove
408
+ agents prune --dry-run
409
+
385
410
  # Just orphan skills
386
411
  agents prune skills
387
412
 
388
413
  # Just version dedup
389
414
  agents prune versions
390
415
 
416
+ # Deduplicate versions for one agent only
417
+ agents prune claude
418
+
391
419
  # Sweep every installed version's orphans, not only the defaults
392
420
  agents prune --all
393
421
 
@@ -80,12 +80,12 @@ Skip CLI installs with --skip-clis when you only want config updates, not versio
80
80
  // auto-syncs the system repo in the background and surfaces upstream
81
81
  // changes for user/extra repos as one-line notices. Repo lifecycle is
82
82
  // managed under `agents repo`. We keep this command functional today
83
- // because `agents init` still invokes it for first-time setup; once
84
- // init is refactored to call the bootstrap helpers directly, this
83
+ // because `agents setup` still invokes it for first-time setup; once
84
+ // setup is refactored to call the bootstrap helpers directly, this
85
85
  // command will hard-error like `agents memory` does.
86
86
  if (!options.yes && process.argv[2] === 'pull') {
87
87
  process.stderr.write('agents-cli: "agents pull" is deprecated.\n' +
88
- ' First-time setup: agents init\n' +
88
+ ' First-time setup: agents setup\n' +
89
89
  ' Force a sync now: agents repo pull\n' +
90
90
  ' Push your repo: agents repo push\n\n');
91
91
  }
@@ -257,7 +257,7 @@ Examples:
257
257
  const systemStatus = !systemOnDisk
258
258
  ? chalk.red('missing')
259
259
  : !systemIsGit
260
- ? chalk.yellow('not a git repo — run: agents init')
260
+ ? chalk.yellow('not a git repo — run: agents setup')
261
261
  : chalk.green('cloned');
262
262
  const systemCommitLabel = systemCommit ? chalk.gray(`(${systemCommit})`) : '';
263
263
  console.log(chalk.bold('System (~/.agents-system/)'));
@@ -571,8 +571,8 @@ Examples:
571
571
  .action(async (options) => {
572
572
  if (options.follow) {
573
573
  const { exec: execCb } = await import('child_process');
574
- const { getAgentsDir } = await import('../lib/state.js');
575
- const logPath = path.join(getAgentsDir(), 'helpers/daemon/logs.jsonl');
574
+ const { getDaemonDir } = await import('../lib/state.js');
575
+ const logPath = path.join(getDaemonDir(), 'logs.jsonl');
576
576
  const child = execCb(`tail -f "${logPath}"`);
577
577
  child.stdout?.pipe(process.stdout);
578
578
  child.stderr?.pipe(process.stderr);
@@ -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
 
@@ -280,6 +306,10 @@ Examples:
280
306
  # Eval the bundle into your current shell
281
307
  eval "$(agents secrets export prod --plaintext)"
282
308
 
309
+ # Run a command with secrets injected
310
+ agents secrets exec prod -- ./deploy.sh
311
+ agents secrets exec hetzner.com -- crabbox list
312
+
283
313
  # Remove one key (purges the keychain item by default)
284
314
  agents secrets remove prod STRIPE_API_KEY
285
315
 
@@ -297,7 +327,7 @@ Examples:
297
327
  registerCommandGroups(cmd, [
298
328
  { title: 'Bundle commands', names: ['list', 'view', 'create', 'delete'] },
299
329
  { title: 'Secret commands', names: ['add', 'rotate', 'remove', 'import', 'export'] },
300
- { title: 'Utilities', names: ['generate'] },
330
+ { title: 'Utilities', names: ['exec', 'generate'] },
301
331
  ]);
302
332
  cmd
303
333
  .command('list')
@@ -645,35 +675,71 @@ Examples:
645
675
  });
646
676
  cmd
647
677
  .command('import [bundle]')
648
- .description('Import keys from a .env file into a bundle. By default every key is stored in keychain.')
649
- .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)')
650
682
  .option('--all-plaintext', 'Store every imported value as a literal in the bundle metadata (skip keychain item creation)')
651
683
  .option('--force', 'Overwrite an existing key in the bundle')
652
684
  .action(async (bundleName, opts) => {
653
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
+ }
654
692
  const resolvedBundleName = bundleName ?? (await pickBundleName('import into'));
655
693
  const bundle = readBundle(resolvedBundleName);
656
- const raw = fs.readFileSync(opts.from, 'utf-8');
657
- const pairs = parseDotenv(raw);
658
694
  let added = 0;
659
695
  let skipped = 0;
660
- for (const [key, value] of Object.entries(pairs)) {
661
- if (!opts.force && key in bundle.vars) {
662
- skipped++;
663
- 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++;
664
715
  }
665
- if (opts.allPlaintext) {
666
- 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.`));
667
719
  }
668
- else {
669
- const item = secretsKeychainItem(resolvedBundleName, key);
670
- setKeychainToken(item, value, bundle.icloud_sync);
671
- 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++;
672
739
  }
673
- added++;
740
+ writeBundle(bundle);
741
+ console.log(chalk.green(`Imported ${added} key(s)${skipped ? `, skipped ${skipped} (already set, pass --force)` : ''}.`));
674
742
  }
675
- writeBundle(bundle);
676
- console.log(chalk.green(`Imported ${added} key(s)${skipped ? `, skipped ${skipped} (already set, pass --force)` : ''}.`));
677
743
  }
678
744
  catch (err) {
679
745
  if (isPromptCancelled(err))
@@ -684,13 +750,49 @@ Examples:
684
750
  });
685
751
  cmd
686
752
  .command('export [bundle]')
687
- .description('Resolve a bundle and print KEY=VALUE lines (for `eval "$(agents secrets export prod)"`). Refuses on a TTY unless --plaintext.')
688
- .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)')
689
758
  .action(async (bundleName, opts) => {
690
759
  try {
691
760
  const { resolveBundleEnv, bundleToEnvPrefix, isReservedEnvName } = await import('../lib/secrets/bundles.js');
692
761
  const resolvedBundleName = bundleName ?? (await pickBundleName('export'));
693
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
+ }
694
796
  if (isInteractiveTerminal() && !opts.plaintext) {
695
797
  console.error(chalk.red('export to a TTY requires --plaintext (prevents shoulder-surfing).'));
696
798
  process.exit(1);
@@ -711,6 +813,38 @@ Examples:
711
813
  process.exit(1);
712
814
  }
713
815
  });
816
+ cmd
817
+ .command('exec <bundle> [command...]')
818
+ .description('Run a command with the bundle\'s secrets injected into the environment')
819
+ .allowUnknownOption()
820
+ .action(async (bundleName, commandParts) => {
821
+ try {
822
+ if (commandParts.length === 0) {
823
+ console.error(chalk.red('Usage: agents secrets exec <bundle> -- <command...>'));
824
+ process.exit(1);
825
+ }
826
+ const { resolveBundleEnv } = await import('../lib/secrets/bundles.js');
827
+ const bundle = readBundle(bundleName);
828
+ const secretEnv = resolveBundleEnv(bundle);
829
+ const { spawn } = await import('child_process');
830
+ const [cmd, ...args] = commandParts;
831
+ const proc = spawn(cmd, args, {
832
+ stdio: 'inherit',
833
+ env: { ...process.env, ...secretEnv },
834
+ });
835
+ proc.on('close', (code) => process.exit(code ?? 0));
836
+ proc.on('error', (err) => {
837
+ console.error(chalk.red(`Failed to run '${cmd}': ${err.message}`));
838
+ process.exit(1);
839
+ });
840
+ }
841
+ catch (err) {
842
+ if (isPromptCancelled(err))
843
+ return;
844
+ console.error(chalk.red(err.message));
845
+ process.exit(1);
846
+ }
847
+ });
714
848
  cmd
715
849
  .command('generate [length]')
716
850
  .description('Generate a random password')