@phnx-labs/agents-cli 1.19.2 → 1.20.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/README.md +72 -12
  3. package/dist/browser.js +0 -0
  4. package/dist/commands/browser.js +88 -16
  5. package/dist/commands/cli.d.ts +14 -0
  6. package/dist/commands/cli.js +244 -0
  7. package/dist/commands/cloud.js +1 -1
  8. package/dist/commands/commands.js +27 -10
  9. package/dist/commands/computer.js +18 -1
  10. package/dist/commands/doctor.d.ts +1 -1
  11. package/dist/commands/doctor.js +2 -2
  12. package/dist/commands/exec.js +38 -18
  13. package/dist/commands/factory.d.ts +3 -14
  14. package/dist/commands/factory.js +3 -3
  15. package/dist/commands/feedback.d.ts +7 -0
  16. package/dist/commands/feedback.js +89 -0
  17. package/dist/commands/helper.d.ts +12 -0
  18. package/dist/commands/helper.js +87 -0
  19. package/dist/commands/hooks.js +89 -10
  20. package/dist/commands/mcp.js +166 -10
  21. package/dist/commands/packages.js +196 -27
  22. package/dist/commands/permissions.js +21 -6
  23. package/dist/commands/plugins.js +11 -4
  24. package/dist/commands/profiles.d.ts +8 -0
  25. package/dist/commands/profiles.js +118 -5
  26. package/dist/commands/prune.js +39 -160
  27. package/dist/commands/pull.js +58 -5
  28. package/dist/commands/routines.js +107 -14
  29. package/dist/commands/rules.js +8 -4
  30. package/dist/commands/secrets-migrate.d.ts +24 -0
  31. package/dist/commands/secrets-migrate.js +198 -0
  32. package/dist/commands/secrets-sync.d.ts +11 -0
  33. package/dist/commands/secrets-sync.js +155 -0
  34. package/dist/commands/secrets.js +79 -46
  35. package/dist/commands/sessions.d.ts +28 -0
  36. package/dist/commands/sessions.js +98 -33
  37. package/dist/commands/setup.d.ts +1 -0
  38. package/dist/commands/setup.js +37 -28
  39. package/dist/commands/skills.js +25 -8
  40. package/dist/commands/subagents.js +69 -49
  41. package/dist/commands/teams.js +61 -10
  42. package/dist/commands/utils.d.ts +33 -0
  43. package/dist/commands/utils.js +139 -0
  44. package/dist/commands/versions.d.ts +4 -3
  45. package/dist/commands/versions.js +134 -130
  46. package/dist/commands/view.d.ts +6 -0
  47. package/dist/commands/view.js +175 -19
  48. package/dist/commands/workflows.js +29 -6
  49. package/dist/computer.js +0 -0
  50. package/dist/index.js +38 -6
  51. package/dist/lib/acp/client.js +6 -1
  52. package/dist/lib/acp/harnesses.js +8 -0
  53. package/dist/lib/agents.d.ts +4 -0
  54. package/dist/lib/agents.js +125 -34
  55. package/dist/lib/auto-pull-worker.js +18 -1
  56. package/dist/lib/browser/cdp.d.ts +8 -1
  57. package/dist/lib/browser/cdp.js +40 -3
  58. package/dist/lib/browser/chrome.d.ts +13 -0
  59. package/dist/lib/browser/chrome.js +46 -3
  60. package/dist/lib/browser/domain-skills.d.ts +51 -0
  61. package/dist/lib/browser/domain-skills.js +157 -0
  62. package/dist/lib/browser/drivers/local.js +45 -4
  63. package/dist/lib/browser/drivers/ssh.js +2 -2
  64. package/dist/lib/browser/ipc.d.ts +8 -1
  65. package/dist/lib/browser/ipc.js +37 -28
  66. package/dist/lib/browser/profiles.d.ts +16 -3
  67. package/dist/lib/browser/profiles.js +44 -4
  68. package/dist/lib/browser/service.d.ts +3 -0
  69. package/dist/lib/browser/service.js +40 -5
  70. package/dist/lib/browser/types.d.ts +11 -4
  71. package/dist/lib/cli-resources.d.ts +137 -0
  72. package/dist/lib/cli-resources.js +477 -0
  73. package/dist/lib/cloud/factory.d.ts +1 -1
  74. package/dist/lib/cloud/factory.js +1 -1
  75. package/dist/lib/cloud/rush.js +5 -5
  76. package/dist/lib/command-skills.js +0 -2
  77. package/dist/lib/computer-rpc.d.ts +3 -0
  78. package/dist/lib/computer-rpc.js +53 -0
  79. package/dist/lib/daemon.js +20 -0
  80. package/dist/lib/events.d.ts +16 -2
  81. package/dist/lib/events.js +33 -2
  82. package/dist/lib/exec.d.ts +42 -13
  83. package/dist/lib/exec.js +127 -33
  84. package/dist/lib/help.js +11 -5
  85. package/dist/lib/hooks/cache.d.ts +38 -0
  86. package/dist/lib/hooks/cache.js +242 -0
  87. package/dist/lib/hooks/profile.d.ts +33 -0
  88. package/dist/lib/hooks/profile.js +129 -0
  89. package/dist/lib/hooks.d.ts +0 -10
  90. package/dist/lib/hooks.js +246 -11
  91. package/dist/lib/mcp.d.ts +15 -0
  92. package/dist/lib/mcp.js +46 -0
  93. package/dist/lib/migrate.js +1 -1
  94. package/dist/lib/overdue.d.ts +26 -0
  95. package/dist/lib/overdue.js +101 -0
  96. package/dist/lib/permissions.d.ts +13 -0
  97. package/dist/lib/permissions.js +55 -1
  98. package/dist/lib/plugin-marketplace.js +1 -1
  99. package/dist/lib/plugins.js +15 -1
  100. package/dist/lib/profiles-presets.d.ts +26 -0
  101. package/dist/lib/profiles-presets.js +216 -0
  102. package/dist/lib/profiles.d.ts +34 -0
  103. package/dist/lib/profiles.js +112 -1
  104. package/dist/lib/resources/mcp.js +37 -0
  105. package/dist/lib/resources.d.ts +1 -1
  106. package/dist/lib/rotate.js +10 -4
  107. package/dist/lib/routines-format.d.ts +47 -0
  108. package/dist/lib/routines-format.js +194 -0
  109. package/dist/lib/routines.d.ts +8 -2
  110. package/dist/lib/routines.js +34 -14
  111. package/dist/lib/runner.js +83 -15
  112. package/dist/lib/scheduler.js +8 -1
  113. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  114. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  115. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
  116. package/dist/lib/secrets/bundles.d.ts +34 -17
  117. package/dist/lib/secrets/bundles.js +210 -36
  118. package/dist/lib/secrets/index.d.ts +49 -30
  119. package/dist/lib/secrets/index.js +126 -115
  120. package/dist/lib/secrets/install-helper.d.ts +45 -0
  121. package/dist/lib/secrets/install-helper.js +165 -0
  122. package/dist/lib/secrets/linux.js +4 -4
  123. package/dist/lib/secrets/sync.d.ts +56 -0
  124. package/dist/lib/secrets/sync.js +180 -0
  125. package/dist/lib/session/active.d.ts +8 -0
  126. package/dist/lib/session/active.js +3 -2
  127. package/dist/lib/session/db.d.ts +0 -4
  128. package/dist/lib/session/db.js +0 -26
  129. package/dist/lib/session/parse.d.ts +1 -0
  130. package/dist/lib/session/parse.js +44 -0
  131. package/dist/lib/session/render.js +4 -4
  132. package/dist/lib/session/types.d.ts +2 -2
  133. package/dist/lib/session/types.js +1 -1
  134. package/dist/lib/shims.d.ts +5 -2
  135. package/dist/lib/shims.js +70 -38
  136. package/dist/lib/state.d.ts +14 -2
  137. package/dist/lib/state.js +51 -20
  138. package/dist/lib/teams/agents.d.ts +5 -4
  139. package/dist/lib/teams/agents.js +48 -22
  140. package/dist/lib/teams/api.d.ts +2 -1
  141. package/dist/lib/teams/api.js +4 -3
  142. package/dist/lib/teams/parsers.d.ts +1 -1
  143. package/dist/lib/teams/parsers.js +153 -3
  144. package/dist/lib/teams/summarizer.js +18 -2
  145. package/dist/lib/teams/worktree.js +14 -3
  146. package/dist/lib/types.d.ts +63 -4
  147. package/dist/lib/types.js +8 -3
  148. package/dist/lib/usage.d.ts +27 -2
  149. package/dist/lib/usage.js +100 -17
  150. package/dist/lib/versions.d.ts +45 -3
  151. package/dist/lib/versions.js +455 -60
  152. package/package.json +15 -14
  153. package/scripts/install-helper.js +97 -0
  154. package/scripts/postinstall.js +16 -0
  155. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  156. package/npm-shrinkwrap.json +0 -3162
@@ -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++;
@@ -873,13 +906,12 @@ Examples:
873
906
  .option('--force', 'Overwrite existing 1Password items (used with --to-1password)')
874
907
  .action(async (bundleName, opts) => {
875
908
  try {
876
- const { resolveBundleEnv, bundleToEnvPrefix, isReservedEnvName } = await import('../lib/secrets/bundles.js');
909
+ const { readAndResolveBundleEnv, bundleToEnvPrefix, isReservedEnvName } = await import('../lib/secrets/bundles.js');
877
910
  const resolvedBundleName = bundleName ?? (await pickBundleName('export'));
878
- const bundle = readBundle(resolvedBundleName);
879
911
  if (opts.to1password) {
880
912
  assertOpAvailable();
881
913
  const vault = await resolveVault(opts.vault);
882
- const env = resolveBundleEnv(bundle, { caller: `1Password vault ${vault}` });
914
+ const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `1Password vault ${vault}` });
883
915
  let created = 0;
884
916
  let overwritten = 0;
885
917
  let skipped = 0;
@@ -909,11 +941,11 @@ Examples:
909
941
  console.log(chalk.green(`Exported to 1Password vault '${vault}': ${parts.join(', ')}.`));
910
942
  return;
911
943
  }
912
- if (isInteractiveTerminal() && !opts.plaintext) {
913
- 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).'));
914
946
  process.exit(1);
915
947
  }
916
- const env = resolveBundleEnv(bundle, { caller: `export to shell` });
948
+ const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `export to shell` });
917
949
  const prefix = bundleToEnvPrefix(resolvedBundleName);
918
950
  for (const [k, v] of Object.entries(env)) {
919
951
  const exportKey = isReservedEnvName(k) ? `${prefix}_${k}` : k;
@@ -939,10 +971,9 @@ Examples:
939
971
  console.error(chalk.red('Usage: agents secrets exec <bundle> -- <command...>'));
940
972
  process.exit(1);
941
973
  }
942
- const { resolveBundleEnv } = await import('../lib/secrets/bundles.js');
943
- const bundle = readBundle(bundleName);
974
+ const { readAndResolveBundleEnv } = await import('../lib/secrets/bundles.js');
944
975
  const [cmd, ...args] = commandParts;
945
- const secretEnv = resolveBundleEnv(bundle, { caller: `command ${cmd}` });
976
+ const { env: secretEnv } = readAndResolveBundleEnv(bundleName, { caller: `command ${cmd}` });
946
977
  const { spawn } = await import('child_process');
947
978
  const proc = spawn(cmd, args, {
948
979
  stdio: 'inherit',
@@ -1035,4 +1066,6 @@ Examples:
1035
1066
  console.log(password);
1036
1067
  }
1037
1068
  });
1069
+ registerSecretsSyncCommands(cmd);
1070
+ registerSecretsMigrateAclCommand(cmd);
1038
1071
  }
@@ -1,5 +1,33 @@
1
1
  import type { Command } from 'commander';
2
2
  import type { SessionMeta } from '../lib/session/types.js';
3
+ import { type ActiveSession } from '../lib/session/active.js';
4
+ /** Grouped + sorted view of active sessions for the --active renderer. */
5
+ export interface ActiveSessionsLayout {
6
+ workspaces: Array<{
7
+ /** Internal grouping key — `__cloud__`, `__unknown__`, or the cwd. */
8
+ key: string;
9
+ /** Sessions in this workspace, both windowed and flat (preserves total count). */
10
+ total: number;
11
+ /** Terminals grouped by IDE window (sorted by oldest startedAtMs). */
12
+ windows: Array<{
13
+ windowId: string;
14
+ sessions: ActiveSession[];
15
+ }>;
16
+ /** Everything else in this workspace: cloud, teams, headless, terminals without a windowId. */
17
+ flat: ActiveSession[];
18
+ }>;
19
+ }
20
+ /**
21
+ * Group sessions by workspace, then split each workspace into IDE-window
22
+ * sub-groups + a flat bucket. Pure function — no I/O — so the renderer's
23
+ * grouping rules can be tested without mocking the session scanner.
24
+ *
25
+ * Sort order:
26
+ * - workspaces: by session count descending, then key ascending
27
+ * - windows within a workspace: by oldest startedAtMs ascending
28
+ * - sessions within a window/flat bucket: input order preserved
29
+ */
30
+ export declare function groupActiveSessions(sessions: ActiveSession[]): ActiveSessionsLayout;
3
31
  /**
4
32
  * Build the shell command that resumes a picked session.
5
33
  *
@@ -181,18 +181,45 @@ function buildSessionDescription(s) {
181
181
  return s.topic;
182
182
  return '';
183
183
  }
184
- /** Render the unified active-session view. */
185
- async function renderActiveSessions(asJson) {
186
- const sessions = await getActiveSessions();
187
- if (asJson) {
188
- process.stdout.write(JSON.stringify(sessions, null, 2) + '\n');
189
- return;
190
- }
191
- if (sessions.length === 0) {
192
- console.log(chalk.gray('No active agent sessions.'));
193
- return;
194
- }
195
- // Group sessions by workspace (cwd), with cloud/undefined grouped separately
184
+ /**
185
+ * Render a single agent-session row inside an already-printed group header.
186
+ * Indent is the leading whitespace (2 spaces for flat groups, 4 inside a
187
+ * window sub-group).
188
+ */
189
+ function printActiveRow(s, indent) {
190
+ const kindCol = colorAgent(s.kind)(padRight(truncate(s.kind, 8), 9));
191
+ const hostCol = chalk.gray(padRight(truncate(s.host ?? '-', 8), 9));
192
+ const statusCol = statusColor(s.status)(padRight(truncate(s.status, 7), 8));
193
+ const pidCol = chalk.yellow(padRight(s.pid ? String(s.pid) : '-', 7));
194
+ const desc = buildSessionDescription(s);
195
+ console.log(indent +
196
+ pidCol +
197
+ kindCol +
198
+ hostCol +
199
+ statusCol +
200
+ chalk.white(truncate(desc || '-', 50)));
201
+ }
202
+ /**
203
+ * Short label for an IDE window. The slice key in live-terminals.json is
204
+ * `${vscode.env.sessionId}-${ext-host pid}`; the trailing pid is the cheap
205
+ * stable disambiguator. We surface it as `ext-pid` so two windows on the
206
+ * same repo are visibly different.
207
+ */
208
+ function shortWindowLabel(windowId) {
209
+ const m = windowId.match(/-(\d+)$/);
210
+ return m ? `ext-pid ${m[1]}` : `win ${windowId.slice(0, 8)}`;
211
+ }
212
+ /**
213
+ * Group sessions by workspace, then split each workspace into IDE-window
214
+ * sub-groups + a flat bucket. Pure function — no I/O — so the renderer's
215
+ * grouping rules can be tested without mocking the session scanner.
216
+ *
217
+ * Sort order:
218
+ * - workspaces: by session count descending, then key ascending
219
+ * - windows within a workspace: by oldest startedAtMs ascending
220
+ * - sessions within a window/flat bucket: input order preserved
221
+ */
222
+ export function groupActiveSessions(sessions) {
196
223
  const byWorkspace = new Map();
197
224
  for (const s of sessions) {
198
225
  const key = s.cwd ?? (s.context === 'cloud' ? '__cloud__' : '__unknown__');
@@ -200,7 +227,6 @@ async function renderActiveSessions(asJson) {
200
227
  list.push(s);
201
228
  byWorkspace.set(key, list);
202
229
  }
203
- // Sort workspaces: most sessions first, then alphabetically
204
230
  const sortedKeys = Array.from(byWorkspace.keys()).sort((a, b) => {
205
231
  const aCount = byWorkspace.get(a).length;
206
232
  const bCount = byWorkspace.get(b).length;
@@ -208,33 +234,71 @@ async function renderActiveSessions(asJson) {
208
234
  return bCount - aCount;
209
235
  return a.localeCompare(b);
210
236
  });
211
- let first = true;
212
- for (const key of sortedKeys) {
237
+ const workspaces = sortedKeys.map((key) => {
213
238
  const group = byWorkspace.get(key);
239
+ const windowedSessions = [];
240
+ const flat = [];
241
+ for (const s of group) {
242
+ if (s.context === 'terminal' && s.windowId)
243
+ windowedSessions.push(s);
244
+ else
245
+ flat.push(s);
246
+ }
247
+ const byWindow = new Map();
248
+ for (const s of windowedSessions) {
249
+ const list = byWindow.get(s.windowId) || [];
250
+ list.push(s);
251
+ byWindow.set(s.windowId, list);
252
+ }
253
+ const windowKeys = Array.from(byWindow.keys()).sort((a, b) => {
254
+ const aStart = Math.min(...byWindow.get(a).map(s => s.startedAtMs ?? Infinity));
255
+ const bStart = Math.min(...byWindow.get(b).map(s => s.startedAtMs ?? Infinity));
256
+ return aStart - bStart;
257
+ });
258
+ return {
259
+ key,
260
+ total: group.length,
261
+ windows: windowKeys.map((wid) => ({ windowId: wid, sessions: byWindow.get(wid) })),
262
+ flat,
263
+ };
264
+ });
265
+ return { workspaces };
266
+ }
267
+ /** Render the unified active-session view. */
268
+ async function renderActiveSessions(asJson) {
269
+ const sessions = await getActiveSessions();
270
+ if (asJson) {
271
+ process.stdout.write(JSON.stringify(sessions, null, 2) + '\n');
272
+ return;
273
+ }
274
+ if (sessions.length === 0) {
275
+ console.log(chalk.gray('No active agent sessions.'));
276
+ return;
277
+ }
278
+ const layout = groupActiveSessions(sessions);
279
+ let first = true;
280
+ for (const ws of layout.workspaces) {
214
281
  if (!first)
215
282
  console.log();
216
283
  first = false;
217
284
  // Print workspace header
218
- const header = key === '__cloud__'
285
+ const header = ws.key === '__cloud__'
219
286
  ? chalk.magenta.bold('cloud')
220
- : key === '__unknown__'
287
+ : ws.key === '__unknown__'
221
288
  ? chalk.gray.bold('unknown')
222
- : chalk.cyan.bold(shortCwd(key));
223
- console.log(`${header} ${chalk.gray(`(${group.length})`)}`);
224
- // Print each session in this workspace
225
- for (const s of group) {
226
- const kindCol = colorAgent(s.kind)(padRight(truncate(s.kind, 8), 9));
227
- const hostCol = chalk.gray(padRight(truncate(s.host ?? '-', 8), 9));
228
- const statusCol = statusColor(s.status)(padRight(truncate(s.status, 7), 8));
229
- const pidCol = chalk.yellow(padRight(s.pid ? String(s.pid) : '-', 7));
230
- const desc = buildSessionDescription(s);
231
- console.log(' ' +
232
- pidCol +
233
- kindCol +
234
- hostCol +
235
- statusCol +
236
- chalk.white(truncate(desc || '-', 50)));
289
+ : chalk.cyan.bold(shortCwd(ws.key));
290
+ console.log(`${header} ${chalk.gray(`(${ws.total})`)}`);
291
+ for (const win of ws.windows) {
292
+ // Host is per-process, but every terminal in the same IDE window shares
293
+ // an ancestor — take the first non-empty host as the window's label.
294
+ const host = win.sessions.find((s) => s.host)?.host ?? 'terminal';
295
+ const winHeader = `${chalk.gray(host)} ${chalk.gray('·')} ${chalk.gray(shortWindowLabel(win.windowId))} ${chalk.gray(`(${win.sessions.length})`)}`;
296
+ console.log(' ' + winHeader);
297
+ for (const s of win.sessions)
298
+ printActiveRow(s, ' ');
237
299
  }
300
+ for (const s of ws.flat)
301
+ printActiveRow(s, ' ');
238
302
  }
239
303
  const runningCount = sessions.filter(s => s.status === 'running').length;
240
304
  const idleCount = sessions.filter(s => s.status === 'idle').length;
@@ -673,7 +737,8 @@ export function buildResumeCommand(session) {
673
737
  case 'openclaw':
674
738
  case 'rush':
675
739
  case 'hermes':
676
- // Rush and Hermes sessions are captured artifacts, not resumable locally.
740
+ case 'grok':
741
+ // Grok (and some others) sessions are captured artifacts, not resumable the same way.
677
742
  return null;
678
743
  }
679
744
  }
@@ -9,6 +9,7 @@ import type { Command } from 'commander';
9
9
  export declare function runSetup(program: Command, options?: {
10
10
  force?: boolean;
11
11
  suppressFooter?: boolean;
12
+ systemRepo?: boolean;
12
13
  }): Promise<void>;
13
14
  /**
14
15
  * Ensure the system repo exists before running a command that needs it.
@@ -68,38 +68,46 @@ export async function runSetup(program, options = {}) {
68
68
  for (const install of unmanaged) {
69
69
  sessionCounts[install.agentId] = countSessionFiles(install.agentId);
70
70
  }
71
+ const systemRepo = process.env.AGENTS_SYSTEM_REPO || DEFAULT_SYSTEM_REPO;
71
72
  console.log(chalk.bold('\nWelcome to agents-cli.'));
72
- console.log(chalk.gray(`Cloning the system repo from ${systemRepoSlug(DEFAULT_SYSTEM_REPO)} into ~/.agents-system/.\n`));
73
- ensureAgentsDir();
74
- const spinner = ora('Cloning system repo...').start();
75
- if (isGitRepo(agentsDir)) {
76
- // --force on an existing repo: pull instead of re-clone
77
- const result = await pullRepo(agentsDir);
78
- if (!result.success) {
79
- spinner.fail(`Pull failed: ${result.error}`);
80
- console.log(chalk.gray('Fix the issue and re-run: agents setup --force'));
81
- process.exit(1);
82
- }
83
- spinner.succeed(`Updated to ${result.commit}`);
73
+ if (options.systemRepo === false) {
74
+ ensureAgentsDir();
75
+ console.log(chalk.gray('Skipping system repo clone (--no-system-repo).'));
76
+ console.log(chalk.gray(`Populate ~/.agents-system/ yourself before running other commands that depend on it.\n`));
84
77
  }
85
78
  else {
86
- // Check git is available
87
- try {
88
- const { execSync } = await import('child_process');
89
- execSync('which git', { stdio: 'ignore' });
90
- }
91
- catch {
92
- spinner.fail('git is not installed');
93
- console.log(chalk.gray('Install git first: https://git-scm.com/downloads'));
94
- process.exit(1);
79
+ console.log(chalk.gray(`Cloning the system repo from ${systemRepoSlug(systemRepo)} into ~/.agents-system/.\n`));
80
+ ensureAgentsDir();
81
+ const spinner = ora('Cloning system repo...').start();
82
+ if (isGitRepo(agentsDir)) {
83
+ // --force on an existing repo: pull instead of re-clone
84
+ const result = await pullRepo(agentsDir);
85
+ if (!result.success) {
86
+ spinner.fail(`Pull failed: ${result.error}`);
87
+ console.log(chalk.gray('Fix the issue and re-run: agents setup --force'));
88
+ process.exit(1);
89
+ }
90
+ spinner.succeed(`Updated to ${result.commit}`);
95
91
  }
96
- const result = await cloneIntoExisting(DEFAULT_SYSTEM_REPO, agentsDir);
97
- if (!result.success) {
98
- spinner.fail(`Clone failed: ${result.error}`);
99
- console.log(chalk.gray('Fix the issue and re-run: agents setup --force'));
100
- process.exit(1);
92
+ else {
93
+ // Check git is available
94
+ try {
95
+ const { execSync } = await import('child_process');
96
+ execSync('which git', { stdio: 'ignore' });
97
+ }
98
+ catch {
99
+ spinner.fail('git is not installed');
100
+ console.log(chalk.gray('Install git first: https://git-scm.com/downloads'));
101
+ process.exit(1);
102
+ }
103
+ const result = await cloneIntoExisting(systemRepo, agentsDir);
104
+ if (!result.success) {
105
+ spinner.fail(`Clone failed: ${result.error}`);
106
+ console.log(chalk.gray('Fix the issue and re-run: agents setup --force'));
107
+ process.exit(1);
108
+ }
109
+ spinner.succeed(`Cloned ${systemRepoSlug(systemRepo)} (${result.commit})`);
101
110
  }
102
- spinner.succeed(`Cloned ${systemRepoSlug(DEFAULT_SYSTEM_REPO)} (${result.commit})`);
103
111
  }
104
112
  // Offer to import existing unmanaged installations
105
113
  if (unmanaged.length > 0 && isInteractiveTerminal()) {
@@ -195,7 +203,8 @@ export function registerSetupCommand(program) {
195
203
  const setupCmd = program
196
204
  .command('setup')
197
205
  .description('First-time setup. Clones a config repo and installs agent CLIs.')
198
- .option('-f, --force', 'Re-run setup even if ~/.agents-system/ already exists (use with caution)');
206
+ .option('-f, --force', 'Re-run setup even if ~/.agents-system/ already exists (use with caution)')
207
+ .option('--no-system-repo', 'Skip cloning the system repo (you must populate ~/.agents-system/ yourself)');
199
208
  setHelpSections(setupCmd, {
200
209
  examples: `
201
210
  # First-time setup (clones the system repo into ~/.agents-system/)