@phnx-labs/agents-cli 1.20.72 → 1.20.73

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 (219) hide show
  1. package/CHANGELOG.md +299 -0
  2. package/README.md +40 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +12 -0
  5. package/dist/commands/activity.js +68 -0
  6. package/dist/commands/apply.d.ts +13 -0
  7. package/dist/commands/apply.js +28 -3
  8. package/dist/commands/browser.js +1 -1
  9. package/dist/commands/cloud.js +21 -11
  10. package/dist/commands/doctor.js +8 -4
  11. package/dist/commands/events.d.ts +11 -10
  12. package/dist/commands/events.js +30 -21
  13. package/dist/commands/exec.d.ts +34 -0
  14. package/dist/commands/exec.js +337 -39
  15. package/dist/commands/feed.d.ts +13 -0
  16. package/dist/commands/feed.js +130 -29
  17. package/dist/commands/hq.d.ts +2 -0
  18. package/dist/commands/hq.js +58 -0
  19. package/dist/commands/lease.d.ts +19 -0
  20. package/dist/commands/lease.js +138 -11
  21. package/dist/commands/login.d.ts +2 -0
  22. package/dist/commands/login.js +123 -0
  23. package/dist/commands/logs.js +1 -1
  24. package/dist/commands/mine.d.ts +27 -0
  25. package/dist/commands/mine.js +207 -0
  26. package/dist/commands/monitors.js +79 -49
  27. package/dist/commands/plugins.js +1 -0
  28. package/dist/commands/profiles.js +158 -0
  29. package/dist/commands/repo.js +2 -60
  30. package/dist/commands/resources.d.ts +5 -0
  31. package/dist/commands/resources.js +95 -0
  32. package/dist/commands/routines.d.ts +2 -0
  33. package/dist/commands/routines.js +139 -37
  34. package/dist/commands/secrets.js +234 -47
  35. package/dist/commands/send.d.ts +15 -0
  36. package/dist/commands/send.js +62 -0
  37. package/dist/commands/sessions-tail.d.ts +1 -0
  38. package/dist/commands/sessions-tail.js +20 -9
  39. package/dist/commands/sessions.js +18 -3
  40. package/dist/commands/setup-mine.d.ts +18 -0
  41. package/dist/commands/setup-mine.js +106 -0
  42. package/dist/commands/setup-share.js +2 -2
  43. package/dist/commands/setup.js +3 -1
  44. package/dist/commands/share.d.ts +8 -1
  45. package/dist/commands/share.js +122 -37
  46. package/dist/commands/ssh.d.ts +9 -0
  47. package/dist/commands/ssh.js +196 -26
  48. package/dist/commands/sync.js +1 -1
  49. package/dist/commands/teams.js +11 -2
  50. package/dist/index.js +79 -10
  51. package/dist/lib/activity.d.ts +124 -0
  52. package/dist/lib/activity.js +542 -0
  53. package/dist/lib/agents.js +16 -5
  54. package/dist/lib/artifact-actions.d.ts +1 -1
  55. package/dist/lib/artifact-actions.js +1 -1
  56. package/dist/lib/ask-classifier.d.ts +2 -1
  57. package/dist/lib/ask-classifier.js +3 -3
  58. package/dist/lib/auth-health.d.ts +32 -4
  59. package/dist/lib/auth-health.js +40 -1
  60. package/dist/lib/auto-pull.js +8 -1
  61. package/dist/lib/brand.d.ts +40 -0
  62. package/dist/lib/brand.js +122 -0
  63. package/dist/lib/browser/drivers/ssh.js +4 -7
  64. package/dist/lib/browser/service.js +6 -8
  65. package/dist/lib/channels/providers/index.d.ts +2 -0
  66. package/dist/lib/channels/providers/index.js +20 -0
  67. package/dist/lib/channels/providers/mailbox.d.ts +2 -0
  68. package/dist/lib/channels/providers/mailbox.js +26 -0
  69. package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
  70. package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
  71. package/dist/lib/channels/providers/rush.d.ts +6 -0
  72. package/dist/lib/channels/providers/rush.js +55 -0
  73. package/dist/lib/channels/registry.d.ts +42 -0
  74. package/dist/lib/channels/registry.js +20 -0
  75. package/dist/lib/channels/resolve.d.ts +11 -0
  76. package/dist/lib/channels/resolve.js +12 -0
  77. package/dist/lib/cli-resources.d.ts +18 -0
  78. package/dist/lib/cli-resources.js +53 -4
  79. package/dist/lib/cloud/codex.js +6 -3
  80. package/dist/lib/cloud/factory.d.ts +1 -0
  81. package/dist/lib/cloud/factory.js +10 -4
  82. package/dist/lib/cloud/rush.d.ts +3 -1
  83. package/dist/lib/cloud/rush.js +6 -1
  84. package/dist/lib/cloud/types.d.ts +2 -0
  85. package/dist/lib/codex-home.d.ts +35 -0
  86. package/dist/lib/codex-home.js +136 -0
  87. package/dist/lib/crabbox/cli.d.ts +44 -0
  88. package/dist/lib/crabbox/cli.js +120 -20
  89. package/dist/lib/crabbox/lease.d.ts +52 -1
  90. package/dist/lib/crabbox/lease.js +117 -16
  91. package/dist/lib/crabbox/progress.d.ts +31 -0
  92. package/dist/lib/crabbox/progress.js +76 -0
  93. package/dist/lib/crabbox/runtimes.d.ts +4 -0
  94. package/dist/lib/crabbox/runtimes.js +33 -8
  95. package/dist/lib/crabbox/setup-copy.d.ts +87 -0
  96. package/dist/lib/crabbox/setup-copy.js +127 -0
  97. package/dist/lib/daemon.d.ts +5 -5
  98. package/dist/lib/daemon.js +9 -9
  99. package/dist/lib/drive-sync.js +6 -3
  100. package/dist/lib/event-stream.d.ts +36 -0
  101. package/dist/lib/event-stream.js +68 -0
  102. package/dist/lib/events.d.ts +1 -1
  103. package/dist/lib/exec.d.ts +11 -0
  104. package/dist/lib/exec.js +23 -2
  105. package/dist/lib/feed-policy.d.ts +1 -1
  106. package/dist/lib/feed-policy.js +14 -9
  107. package/dist/lib/feed-ranking.d.ts +32 -0
  108. package/dist/lib/feed-ranking.js +224 -0
  109. package/dist/lib/feed.d.ts +35 -2
  110. package/dist/lib/feed.js +66 -3
  111. package/dist/lib/fleet/auth-sync.d.ts +53 -0
  112. package/dist/lib/fleet/auth-sync.js +92 -0
  113. package/dist/lib/fleet/remote-login.d.ts +187 -0
  114. package/dist/lib/fleet/remote-login.js +556 -0
  115. package/dist/lib/format.d.ts +30 -3
  116. package/dist/lib/format.js +34 -5
  117. package/dist/lib/fs-atomic.d.ts +7 -2
  118. package/dist/lib/fs-atomic.js +8 -12
  119. package/dist/lib/git.d.ts +16 -0
  120. package/dist/lib/git.js +79 -2
  121. package/dist/lib/heal.js +11 -3
  122. package/dist/lib/hosts/progress.d.ts +28 -10
  123. package/dist/lib/hosts/progress.js +79 -22
  124. package/dist/lib/hosts/remote-cmd.js +4 -0
  125. package/dist/lib/hq/floor.d.ts +87 -0
  126. package/dist/lib/hq/floor.js +226 -0
  127. package/dist/lib/menubar/install-menubar.js +14 -20
  128. package/dist/lib/notify.d.ts +1 -0
  129. package/dist/lib/notify.js +3 -3
  130. package/dist/lib/open-url.d.ts +2 -0
  131. package/dist/lib/open-url.js +19 -0
  132. package/dist/lib/openclaw-keychain.d.ts +56 -0
  133. package/dist/lib/openclaw-keychain.js +236 -0
  134. package/dist/lib/overdue.js +10 -0
  135. package/dist/lib/permissions.d.ts +44 -1
  136. package/dist/lib/permissions.js +284 -7
  137. package/dist/lib/project-launch.d.ts +6 -12
  138. package/dist/lib/project-launch.js +13 -228
  139. package/dist/lib/project-resources.d.ts +7 -0
  140. package/dist/lib/project-resources.js +291 -0
  141. package/dist/lib/pty-client.d.ts +27 -0
  142. package/dist/lib/pty-client.js +136 -10
  143. package/dist/lib/refresh.js +14 -10
  144. package/dist/lib/resource-profiles.d.ts +26 -0
  145. package/dist/lib/resource-profiles.js +157 -0
  146. package/dist/lib/resources/permissions.js +7 -1
  147. package/dist/lib/resources/types.d.ts +1 -1
  148. package/dist/lib/resources.d.ts +1 -1
  149. package/dist/lib/resources.js +32 -3
  150. package/dist/lib/routines.d.ts +11 -0
  151. package/dist/lib/routines.js +56 -15
  152. package/dist/lib/runner.d.ts +1 -0
  153. package/dist/lib/runner.js +66 -5
  154. package/dist/lib/secrets/bundles.d.ts +16 -3
  155. package/dist/lib/secrets/bundles.js +206 -37
  156. package/dist/lib/secrets/icloud-import.d.ts +2 -2
  157. package/dist/lib/secrets/icloud-import.js +9 -6
  158. package/dist/lib/secrets/mcp.js +1 -1
  159. package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
  160. package/dist/lib/secrets/vault-age-helper.js +34 -0
  161. package/dist/lib/secrets/vault.d.ts +49 -0
  162. package/dist/lib/secrets/vault.js +397 -0
  163. package/dist/lib/self-heal/checks/path.js +3 -1
  164. package/dist/lib/self-heal/checks/shadowing.js +10 -2
  165. package/dist/lib/self-heal/checks/shims.js +19 -11
  166. package/dist/lib/session/cloud.d.ts +2 -2
  167. package/dist/lib/session/cloud.js +2 -2
  168. package/dist/lib/session/db.d.ts +4 -0
  169. package/dist/lib/session/db.js +44 -7
  170. package/dist/lib/session/discover.d.ts +2 -0
  171. package/dist/lib/session/discover.js +114 -3
  172. package/dist/lib/session/stream-render.d.ts +3 -0
  173. package/dist/lib/session/stream-render.js +130 -0
  174. package/dist/lib/session/types.d.ts +6 -0
  175. package/dist/lib/share/analytics.d.ts +13 -0
  176. package/dist/lib/share/analytics.js +45 -0
  177. package/dist/lib/share/config.d.ts +19 -5
  178. package/dist/lib/share/config.js +44 -8
  179. package/dist/lib/share/provision.d.ts +58 -6
  180. package/dist/lib/share/provision.js +97 -22
  181. package/dist/lib/share/publish.d.ts +36 -13
  182. package/dist/lib/share/publish.js +55 -8
  183. package/dist/lib/share/worker-template.js +83 -17
  184. package/dist/lib/shims.d.ts +9 -0
  185. package/dist/lib/shims.js +91 -13
  186. package/dist/lib/ssh-exec.d.ts +14 -0
  187. package/dist/lib/ssh-exec.js +57 -0
  188. package/dist/lib/staleness/detectors/hooks.js +25 -1
  189. package/dist/lib/staleness/detectors/permissions.js +66 -0
  190. package/dist/lib/staleness/detectors/workflows.js +20 -0
  191. package/dist/lib/staleness/writers/hooks.js +58 -4
  192. package/dist/lib/staleness/writers/permissions.js +2 -2
  193. package/dist/lib/staleness/writers/skills.js +1 -1
  194. package/dist/lib/staleness/writers/sources.d.ts +10 -1
  195. package/dist/lib/staleness/writers/sources.js +68 -1
  196. package/dist/lib/star-nudge.d.ts +45 -0
  197. package/dist/lib/star-nudge.js +91 -0
  198. package/dist/lib/startup/command-registry.d.ts +6 -1
  199. package/dist/lib/startup/command-registry.js +17 -3
  200. package/dist/lib/state.d.ts +2 -0
  201. package/dist/lib/state.js +3 -0
  202. package/dist/lib/subagents-registry.d.ts +2 -0
  203. package/dist/lib/subagents-registry.js +3 -0
  204. package/dist/lib/teams/parsers.js +214 -6
  205. package/dist/lib/teams/supervisor.d.ts +7 -0
  206. package/dist/lib/teams/supervisor.js +2 -1
  207. package/dist/lib/template.d.ts +1 -1
  208. package/dist/lib/template.js +1 -1
  209. package/dist/lib/triggers/webhook.js +36 -3
  210. package/dist/lib/types.d.ts +73 -0
  211. package/dist/lib/version.d.ts +40 -0
  212. package/dist/lib/version.js +94 -16
  213. package/dist/lib/versions.d.ts +11 -0
  214. package/dist/lib/versions.js +208 -76
  215. package/dist/lib/workflows.d.ts +2 -0
  216. package/dist/lib/workflows.js +88 -0
  217. package/package.json +2 -1
  218. package/dist/commands/daemon.d.ts +0 -10
  219. package/dist/commands/daemon.js +0 -121
@@ -10,13 +10,15 @@ import chalk from 'chalk';
10
10
  import { padVisible, readStdinSync } from '../lib/format.js';
11
11
  import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
12
12
  import * as fs from 'fs';
13
+ import * as os from 'os';
14
+ import * as path from 'path';
13
15
  import { SSH_TARGET_RE, assertValidSshTarget, sshExec } from '../lib/ssh-exec.js';
14
16
  import { quoteWin32ExecArg, composeWin32CommandLine } from '../lib/platform/index.js';
15
17
  import { ensureDaemonStarted, isDaemonRunning } from '../lib/daemon.js';
16
18
  import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveSshTarget, } from '../lib/secrets/remote.js';
17
19
  import { remoteShellFor, buildWindowsStdinImportCommand } from '../lib/hosts/remote-cmd.js';
18
20
  import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
19
- import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, isHeadlessSecretsContext, readBundle, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
21
+ import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, isHeadlessSecretsContext, readBundle, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, } from '../lib/secrets/bundles.js';
20
22
  import { encryptForFallback, decryptForFallback } from '../lib/secrets/filestore.js';
21
23
  import { getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
22
24
  import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
@@ -29,6 +31,7 @@ import { emit } from '../lib/events.js';
29
31
  import { registerCommandGroups, setHelpSections } from '../lib/help.js';
30
32
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
31
33
  import { discoverSyncedBundles, importSyncedBundle, } from '../lib/secrets/icloud-import.js';
34
+ import { getVaultSession, vaultExists } from '../lib/secrets/vault.js';
32
35
  import { registerSecretsSyncCommands } from './secrets-sync.js';
33
36
  import { registerSecretsMigrateAclCommand } from './secrets-migrate.js';
34
37
  import { registerSecretsImportKeyringCommand } from './secrets-import.js';
@@ -495,8 +498,12 @@ function renderBundleRow(b, held, cols = terminalWidth()) {
495
498
  `${padVisible(created, 9)} ` +
496
499
  `${padVisible(updated, 9)} ` +
497
500
  `${padVisible(used, 7)}`;
498
- // Mark file-backed bundles so `list` distinguishes them from keychain ones.
499
- const tag = b.backend === 'file' ? chalk.magenta('[file] ') : '';
501
+ // Mark non-keychain bundles so `list` distinguishes storage at a glance.
502
+ const tag = b.backend === 'file'
503
+ ? chalk.magenta('[file] ')
504
+ : b.backend === 'vault'
505
+ ? chalk.blue('[synced] ')
506
+ : '';
500
507
  // Cap the free-form description to whatever space is left on the line so a long
501
508
  // description can't push the row to 200+ chars and wrap into a smear.
502
509
  const budget = cols - stringWidth(head) - 1 - stringWidth(tag);
@@ -510,7 +517,11 @@ function renderBundleRow(b, held, cols = terminalWidth()) {
510
517
  function renderBundleCard(b, held, cols) {
511
518
  const keys = describeBundle(b).length;
512
519
  const used = b.last_used ? relativeAge(b.last_used) : (b.created_at ? 'never' : '?');
513
- const tag = b.backend === 'file' ? chalk.magenta(' [file]') : '';
520
+ const tag = b.backend === 'file'
521
+ ? chalk.magenta(' [file]')
522
+ : b.backend === 'vault'
523
+ ? chalk.blue(' [synced]')
524
+ : '';
514
525
  const meta = chalk.gray(`${keys} key${keys === 1 ? '' : 's'} · `) + renderPolicyCol(b, held) + chalk.gray(` · used ${used}`);
515
526
  const line1 = `${chalk.cyan(b.name)} ${meta}${tag}`;
516
527
  if (!b.description)
@@ -643,17 +654,17 @@ function countExpiringSoon(meta) {
643
654
  * keychain-backed bundle to `file` — shared by every `import` source so the
644
655
  * guard can't drift between them.
645
656
  */
646
- function resolveImportBundle(name, backendOpt) {
647
- const requestedBackend = parseBackendOpt(backendOpt);
657
+ function resolveImportBundle(name, backendOpt, synced = false) {
658
+ const requestedBackend = synced ? 'vault' : parseBackendOpt(backendOpt);
648
659
  if (bundleExists(name)) {
649
660
  const bundle = readBundle(name);
650
- if (requestedBackend === 'file' && bundle.backend !== 'file') {
651
- throw new Error(`Bundle '${name}' already exists with a keychain backend; ` +
652
- `--backend file cannot change it. Delete it first to recreate as file-backed.`);
661
+ if (requestedBackend !== 'keychain' && bundle.backend !== requestedBackend) {
662
+ throw new Error(`Bundle '${name}' already exists with a different backend; ` +
663
+ `delete it first to recreate it as ${requestedBackend === 'vault' ? 'synced' : `${requestedBackend}-backed`}.`);
653
664
  }
654
665
  return bundle;
655
666
  }
656
- return { name, backend: requestedBackend === 'file' ? 'file' : undefined, vars: {} };
667
+ return { name, backend: requestedBackend === 'keychain' ? undefined : requestedBackend, vars: {} };
657
668
  }
658
669
  /**
659
670
  * Apply KEY=VALUE entries into a bundle (keychain item or plaintext literal),
@@ -661,7 +672,7 @@ function resolveImportBundle(name, backendOpt) {
661
672
  * the .env, --from-file, and --from-ssh import paths.
662
673
  */
663
674
  function applyEnvToBundle(bundle, env, opts) {
664
- const store = bundleItemStore(bundle.backend, { noAcl: bundlePolicy(bundle) === 'never' });
675
+ const storedItems = new Map();
665
676
  let added = 0;
666
677
  let skipped = 0;
667
678
  for (const [key, value] of Object.entries(env)) {
@@ -674,12 +685,17 @@ function applyEnvToBundle(bundle, env, opts) {
674
685
  }
675
686
  else {
676
687
  const item = secretsKeychainItem(bundle.name, key);
677
- store.set(item, value);
688
+ storedItems.set(item, value);
678
689
  bundle.vars[key] = keychainRef(key);
679
690
  }
680
691
  added++;
681
692
  }
682
- writeBundle(bundle);
693
+ if (storedItems.size > 0) {
694
+ writeBundleWithItems(bundle, storedItems);
695
+ }
696
+ else {
697
+ writeBundle(bundle);
698
+ }
683
699
  return { added, skipped };
684
700
  }
685
701
  /** Register the `agents secrets` command tree. */
@@ -759,18 +775,14 @@ export function registerSecretsCommands(program) {
759
775
  .description('List configured secrets bundles (use --host/--hosts to list bundles on other machines over SSH)')
760
776
  .option('--host <target>', 'List bundles on a remote host over SSH (enrolled `agents hosts` name, ssh-config alias, or user@host)')
761
777
  .option('--hosts <list>', 'Comma-separated hosts to list in one shot, e.g. yosemite-s0,yosemite-s1')
778
+ .option('--json', 'Emit machine-readable JSON (bundle metadata only — never secret values) instead of the table')
762
779
  .action(async (opts) => {
763
780
  const targets = parseHostsOption(opts);
764
781
  if (targets.length > 0) {
765
- await browseRemote(targets, ['list'], false);
782
+ await browseRemote(targets, opts.json ? ['list', '--json'] : ['list'], false);
766
783
  return;
767
784
  }
768
785
  const bundles = listBundles();
769
- if (bundles.length === 0) {
770
- console.log(chalk.gray('No secrets bundles configured.'));
771
- console.log(chalk.gray('Try: agents secrets create <name>'));
772
- return;
773
- }
774
786
  // Cross-reference the secrets-agent so `daily` bundles that are currently
775
787
  // held can show "· held Nh". Soft-fails to no hint if the broker is down.
776
788
  const held = new Map();
@@ -783,6 +795,31 @@ export function registerSecretsCommands(program) {
783
795
  /* broker not running — render policy without the countdown */
784
796
  }
785
797
  }
798
+ if (opts.json) {
799
+ // Discovery payload for agents: metadata only, no secret values. Gated on
800
+ // the explicit --json flag (not stdout.isTTY) so piping the human table to
801
+ // a pager never silently swaps formats.
802
+ const payload = bundles.map((b) => ({
803
+ name: b.name,
804
+ keys: describeBundle(b).length,
805
+ policy: bundlePolicy(b),
806
+ backend: b.backend === 'file' ? 'file' : 'keychain',
807
+ allowExec: Boolean(b.allow_exec),
808
+ expiringSoon: countExpiringSoon(b.meta),
809
+ description: b.description ?? null,
810
+ createdAt: b.created_at ?? null,
811
+ updatedAt: b.updated_at ?? null,
812
+ lastUsed: b.last_used ?? null,
813
+ heldExpiresAt: held.get(b.name) ?? null,
814
+ }));
815
+ process.stdout.write(JSON.stringify(payload) + '\n');
816
+ return;
817
+ }
818
+ if (bundles.length === 0) {
819
+ console.log(chalk.gray('No secrets bundles configured.'));
820
+ console.log(chalk.gray('Try: agents secrets create <name>'));
821
+ return;
822
+ }
786
823
  const cols = terminalWidth();
787
824
  if (cols >= SECRETS_WIDE) {
788
825
  console.log(chalk.bold(`${'NAME'.padEnd(20)} ${'KEYS'.padEnd(5)} ${'POLICY'.padEnd(18)} ${'EXPIRING'.padEnd(9)} ${'CREATED'.padEnd(9)} ${'UPDATED'.padEnd(9)} ${'USED'.padEnd(7)} DESCRIPTION`));
@@ -802,6 +839,7 @@ export function registerSecretsCommands(program) {
802
839
  .description('Show a bundle. Keychain values are masked by default — pass --reveal to see them.')
803
840
  .option('--reveal', 'Print keychain-backed values in the clear (TTY only unless --plaintext)')
804
841
  .option('--plaintext', 'Allow --reveal in non-interactive shells (use with care)')
842
+ .option('--json', 'Emit machine-readable JSON (values masked unless --reveal) instead of the human view')
805
843
  .option('--host <target>', 'Show a bundle on a remote host over SSH (enrolled `agents hosts` name, ssh-config alias, or user@host)')
806
844
  .option('--hosts <list>', 'Comma-separated hosts to show in one shot, e.g. yosemite-s0,yosemite-s1')
807
845
  .action(async (name, opts) => {
@@ -817,6 +855,8 @@ export function registerSecretsCommands(program) {
817
855
  args.push('--reveal');
818
856
  if (opts.plaintext)
819
857
  args.push('--plaintext');
858
+ if (opts.json)
859
+ args.push('--json');
820
860
  // With --reveal, force a TTY so the remote keychain prompt can surface
821
861
  // (and the remote's "--reveal in a non-TTY needs --plaintext" gate is
822
862
  // satisfied) — only when this side is itself interactive.
@@ -835,6 +875,76 @@ export function registerSecretsCommands(program) {
835
875
  process.exit(1);
836
876
  }
837
877
  const entries = describeBundle(bundle);
878
+ if (opts.json) {
879
+ // Machine-readable discovery for agents. Values are null unless --reveal
880
+ // (which still routes through the same non-TTY/plaintext gate + audit
881
+ // event as the human path). Gated on the explicit flag, not stdout.isTTY,
882
+ // so a piped human view never silently swaps formats.
883
+ const reveal = Boolean(opts.reveal);
884
+ if (reveal && !isInteractiveTerminal() && !opts.plaintext) {
885
+ console.error(chalk.red('--reveal in a non-TTY requires --plaintext.'));
886
+ process.exit(1);
887
+ }
888
+ const revealed = new Map();
889
+ if (reveal) {
890
+ const items = entries
891
+ .filter((e) => e.kind === 'keychain')
892
+ .map((e) => secretsKeychainItem(bundle.name, e.detail));
893
+ try {
894
+ for (const [item, value] of getKeychainTokens(items))
895
+ revealed.set(item, value);
896
+ }
897
+ catch {
898
+ /* cancelled / batch failure — fall through to masked (null) values */
899
+ }
900
+ const exposed = revealed.size + entries.filter((e) => e.kind === 'literal').length;
901
+ if (exposed > 0) {
902
+ emit('secrets.get', {
903
+ module: 'secrets',
904
+ bundle: bundle.name,
905
+ operation: 'view --reveal --json',
906
+ source: 'reveal',
907
+ status: 'success',
908
+ keyCount: exposed,
909
+ });
910
+ }
911
+ }
912
+ const keys = entries.map((e) => {
913
+ const row = {
914
+ key: e.key,
915
+ kind: e.kind,
916
+ detail: e.detail || null,
917
+ value: null,
918
+ };
919
+ if (e.kind === 'keychain') {
920
+ const item = secretsKeychainItem(bundle.name, e.detail);
921
+ row.stored = hasKeychainToken(item);
922
+ if (reveal && revealed.has(item))
923
+ row.value = revealed.get(item);
924
+ }
925
+ else if (e.kind === 'literal' && reveal) {
926
+ const raw = bundle.vars[e.key];
927
+ row.value =
928
+ typeof raw === 'string'
929
+ ? raw
930
+ : (raw && typeof raw === 'object' && 'value' in raw ? raw.value : '');
931
+ }
932
+ return row;
933
+ });
934
+ process.stdout.write(JSON.stringify({
935
+ name: bundle.name,
936
+ description: bundle.description ?? null,
937
+ policy: bundlePolicy(bundle),
938
+ backend: bundle.backend === 'file' ? 'file' : 'keychain',
939
+ allowExec: Boolean(bundle.allow_exec),
940
+ createdAt: bundle.created_at ?? null,
941
+ updatedAt: bundle.updated_at ?? null,
942
+ lastUsed: bundle.last_used ?? null,
943
+ revealed: reveal,
944
+ keys,
945
+ }) + '\n');
946
+ return;
947
+ }
838
948
  console.log(chalk.bold(bundle.name));
839
949
  if (bundle.description)
840
950
  console.log(chalk.gray(safePrint(bundle.description)));
@@ -842,6 +952,8 @@ export function registerSecretsCommands(program) {
842
952
  console.log(chalk.yellow('allow_exec: true'));
843
953
  if (bundle.backend === 'file')
844
954
  console.log(chalk.gray('backend: file (passphrase-encrypted; reads need AGENTS_SECRETS_PASSPHRASE, no Touch ID)'));
955
+ if (bundle.backend === 'vault')
956
+ console.log(chalk.gray('storage: synced (age-encrypted ~/.agents/vault.age; needs agents login)'));
845
957
  if (bundlePolicy(bundle) === 'never') {
846
958
  console.log(chalk.red.bold('policy: never — NO biometry ACL; reads are silent (no Touch ID, no user-presence check). Automation-only.'));
847
959
  }
@@ -866,15 +978,18 @@ export function registerSecretsCommands(program) {
866
978
  console.error(chalk.red('--reveal in a non-TTY requires --plaintext.'));
867
979
  process.exit(1);
868
980
  }
869
- // Batch every keychain read into one helper call so --reveal pops
870
- // Touch ID once for the whole bundle instead of once per key.
981
+ // Batch every backend read into one helper call where supported, so
982
+ // keychain --reveal pops Touch ID once and synced/file bundles use
983
+ // their declared storage.
871
984
  const revealedValues = new Map();
872
985
  if (reveal) {
873
986
  const items = entries
874
987
  .filter((e) => e.kind === 'keychain')
875
988
  .map((e) => secretsKeychainItem(bundle.name, e.detail));
876
989
  try {
877
- const fetched = getKeychainTokens(items);
990
+ const fetched = bundle.backend
991
+ ? new Map(items.map((item) => [item, bundleItemStore(bundle.backend).get(item)]))
992
+ : getKeychainTokens(items);
878
993
  for (const [item, value] of fetched)
879
994
  revealedValues.set(item, value);
880
995
  }
@@ -905,7 +1020,7 @@ export function registerSecretsCommands(program) {
905
1020
  for (const e of entries) {
906
1021
  if (e.kind === 'keychain') {
907
1022
  const item = secretsKeychainItem(bundle.name, e.detail);
908
- const stored = hasKeychainToken(item);
1023
+ const stored = bundleItemStore(bundle.backend).has(item);
909
1024
  const marker = stored ? chalk.green('stored') : chalk.red('missing');
910
1025
  let valueCol = `[keychain:${e.detail}] ${marker}`;
911
1026
  if (reveal && revealedValues.has(item)) {
@@ -964,14 +1079,14 @@ export function registerSecretsCommands(program) {
964
1079
  // Ungated like the raw path (it IS the automation primitive); the
965
1080
  // `secrets.get` audit event is emitted inside readAndResolveBundleEnv.
966
1081
  try {
967
- if (!bundleExists(item)) {
1082
+ if (!bundleExists(item) && !(vaultExists() && !getVaultSession().loggedIn)) {
968
1083
  console.error(chalk.red(`Secrets bundle '${item}' not found.`));
969
1084
  process.exit(1);
970
1085
  }
971
1086
  // `secrets get` is the scriptable automation primitive ($(agents secrets
972
1087
  // get bundle KEY)); when embedded in a headless routine/CI script it must
973
1088
  // not pop an unwatched Touch ID prompt. Interactive use still prompts.
974
- const { env } = readAndResolveBundleEnv(item, { caller: 'secrets get', keys: [key], agentOnly: isHeadlessSecretsContext() });
1089
+ const { env } = readAndResolveBundleEnv(item, { caller: 'secrets get', keys: [key], keyMode: 'storage', agentOnly: isHeadlessSecretsContext() });
975
1090
  if (!(key in env)) {
976
1091
  console.error(chalk.red(`Key '${key}' not in bundle '${item}'.`));
977
1092
  process.exit(1);
@@ -1026,7 +1141,8 @@ export function registerSecretsCommands(program) {
1026
1141
  .option('--policy <policy>', 'prompt policy: daily (default, ask once a week), always (ask every time), or never (silent, NO biometry ACL — needs --i-understand)')
1027
1142
  .addOption(new Option('--tier <policy>', 'deprecated alias for --policy').hideHelp())
1028
1143
  .option('--i-understand', 'Confirm creating a "never"-policy bundle (no biometry ACL) without an interactive prompt')
1029
- .option('--backend <backend>', 'storage backend: keychain (default) or file (passphrase-encrypted, headless-readable)', 'keychain')
1144
+ .option('--backend <backend>', 'storage backend: keychain (default) or file (passphrase-encrypted)', 'keychain')
1145
+ .option('--synced', 'Store this bundle in the age-encrypted synced secrets file for user-managed cross-machine file sync')
1030
1146
  .option('--force', 'Overwrite an existing bundle')
1031
1147
  .action(async (name, opts) => {
1032
1148
  try {
@@ -1036,7 +1152,7 @@ export function registerSecretsCommands(program) {
1036
1152
  // inherits the configured default (`daily`) instead of being pinned.
1037
1153
  const policyOpt = opts.policy ?? opts.tier;
1038
1154
  const policy = policyOpt ? parsePolicyOpt(policyOpt) : undefined;
1039
- const backend = parseBackendOpt(opts.backend);
1155
+ const backend = opts.synced ? 'vault' : parseBackendOpt(opts.backend);
1040
1156
  if (bundleExists(resolvedName) && !opts.force) {
1041
1157
  console.error(chalk.red(`Bundle '${resolvedName}' already exists. Use --force to overwrite.`));
1042
1158
  process.exit(1);
@@ -1052,7 +1168,7 @@ export function registerSecretsCommands(program) {
1052
1168
  name: resolvedName,
1053
1169
  description: opts.description,
1054
1170
  allow_exec: opts.allowExec,
1055
- backend: backend === 'file' ? 'file' : undefined,
1171
+ backend: backend === 'keychain' ? undefined : backend,
1056
1172
  policy,
1057
1173
  vars: {},
1058
1174
  };
@@ -1062,7 +1178,11 @@ export function registerSecretsCommands(program) {
1062
1178
  : bundlePolicy(bundle) === 'always'
1063
1179
  ? 'policy: always ask'
1064
1180
  : 'policy: never (NO biometry ACL)';
1065
- const tags = [policyTag, backend === 'file' ? 'backend: file' : null].filter(Boolean);
1181
+ const tags = [
1182
+ policyTag,
1183
+ backend === 'file' ? 'backend: file' : null,
1184
+ backend === 'vault' ? 'synced' : null,
1185
+ ].filter(Boolean);
1066
1186
  console.log(chalk.green(`Bundle '${resolvedName}' created (${tags.join(', ')}).`));
1067
1187
  if (bundlePolicy(bundle) === 'never') {
1068
1188
  console.log(chalk.red('Stored without biometry protection — reads are silent. Automation-only; rotate anything sensitive out of it.'));
@@ -1070,6 +1190,9 @@ export function registerSecretsCommands(program) {
1070
1190
  if (backend === 'file') {
1071
1191
  console.log(chalk.gray('File-backed: items are AES-256-GCM encrypted under AGENTS_SECRETS_PASSPHRASE (no Touch ID).'));
1072
1192
  }
1193
+ if (backend === 'vault') {
1194
+ console.log(chalk.gray('Synced: items are encrypted in ~/.agents/vault.age. Copy that file with your sync tool of choice.'));
1195
+ }
1073
1196
  console.log(chalk.gray(`Try: agents secrets add ${resolvedName} MY_KEY`));
1074
1197
  }
1075
1198
  catch (err) {
@@ -1200,11 +1323,14 @@ export function registerSecretsCommands(program) {
1200
1323
  secretValue = await promptForSecret(`Enter value for ${resolvedBundleName}.${resolvedKey}`);
1201
1324
  }
1202
1325
  const item = secretsKeychainItem(resolvedBundleName, resolvedKey);
1203
- bundleItemStore(bundle.backend, { noAcl: bundlePolicy(bundle) === 'never' }).set(item, secretValue);
1204
1326
  bundle.vars[resolvedKey] = keychainRef(resolvedKey);
1205
1327
  applyMeta();
1206
- writeBundle(bundle);
1207
- const where = bundle.backend === 'file' ? 'encrypted file store' : 'keychain';
1328
+ writeBundleWithItems(bundle, new Map([[item, secretValue]]));
1329
+ const where = bundle.backend === 'file'
1330
+ ? 'encrypted file store'
1331
+ : bundle.backend === 'vault'
1332
+ ? 'synced secrets file'
1333
+ : 'keychain';
1208
1334
  console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} stored in ${where} (${item}).`));
1209
1335
  }
1210
1336
  catch (err) {
@@ -1265,7 +1391,8 @@ Examples:
1265
1391
  clearMeta: opts.clearMeta,
1266
1392
  meta: metaPatch,
1267
1393
  });
1268
- console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} rotated in keychain.`));
1394
+ const where = bundle.backend === 'vault' ? 'synced secrets file' : bundle.backend === 'file' ? 'encrypted file store' : 'keychain';
1395
+ console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} rotated in ${where}.`));
1269
1396
  }
1270
1397
  catch (err) {
1271
1398
  if (isPromptCancelled(err))
@@ -1312,7 +1439,11 @@ Examples:
1312
1439
  const item = secretsKeychainItem(resolvedBundleName, raw.slice('keychain:'.length));
1313
1440
  const removed = bundleItemStore(bundle.backend).delete(item);
1314
1441
  if (removed) {
1315
- const where = bundle.backend === 'file' ? 'encrypted file item' : 'keychain item';
1442
+ const where = bundle.backend === 'file'
1443
+ ? 'encrypted file item'
1444
+ : bundle.backend === 'vault'
1445
+ ? 'synced secrets item'
1446
+ : 'keychain item';
1316
1447
  console.log(chalk.green(`Removed ${resolvedBundleName}.${resolvedKey} and purged ${where}.`));
1317
1448
  return;
1318
1449
  }
@@ -1415,7 +1546,8 @@ Examples:
1415
1546
  .addOption(new Option('--from-1password', 'deprecated alias for --from 1password:<vault>').hideHelp())
1416
1547
  .addOption(new Option('--vault <name>', 'deprecated: name the vault in --from 1password:<vault>').hideHelp())
1417
1548
  .option('--all-plaintext', 'Store every imported value as a literal in the bundle metadata (skip keychain item creation)')
1418
- .option('--backend <backend>', 'When creating the bundle: keychain (default) or file (passphrase-encrypted, headless-readable)', 'keychain')
1549
+ .option('--backend <backend>', 'When creating the bundle: keychain (default) or file (passphrase-encrypted)', 'keychain')
1550
+ .option('--synced', 'When creating the bundle, store it in the age-encrypted synced secrets file')
1419
1551
  .option('--force', 'Overwrite an existing key in the bundle')
1420
1552
  .option('--purge', 'With --from icloud: delete the iCloud copies after a successful import (iCloud propagates the deletion to your other devices)')
1421
1553
  .option('--from-file <path>', 'Import from an AES-256-GCM encrypted offline bundle file (needs AGENTS_SECRETS_PASSPHRASE; symmetric counterpart of export --to-file)')
@@ -1440,7 +1572,7 @@ Examples:
1440
1572
  }
1441
1573
  const env = importBundleFromFile(opts.fromFile, passphrase);
1442
1574
  const resolvedBundleName = bundleName ?? (await pickBundleName('import into'));
1443
- const bundle = resolveImportBundle(resolvedBundleName, opts.backend);
1575
+ const bundle = resolveImportBundle(resolvedBundleName, opts.backend, opts.synced);
1444
1576
  const { added, skipped } = applyEnvToBundle(bundle, env, opts);
1445
1577
  console.log(chalk.green(`Imported ${added} key(s) from file${skipped ? `, skipped ${skipped} (already set, pass --force)` : ''}.`));
1446
1578
  return;
@@ -1453,7 +1585,7 @@ Examples:
1453
1585
  const resolvedBundleName = bundleName ?? (await pickBundleName('import into'));
1454
1586
  const target = await resolveSshTarget(opts.host);
1455
1587
  const env = await remoteResolveEnv(target, resolvedBundleName, { osLookupName: opts.host });
1456
- const bundle = resolveImportBundle(resolvedBundleName, opts.backend);
1588
+ const bundle = resolveImportBundle(resolvedBundleName, opts.backend, opts.synced);
1457
1589
  const { added, skipped } = applyEnvToBundle(bundle, env, opts);
1458
1590
  console.log(chalk.green(`Imported ${added} key(s) from ${opts.host}${skipped ? `, skipped ${skipped} (already set, pass --force)` : ''}.`));
1459
1591
  return;
@@ -1465,12 +1597,12 @@ Examples:
1465
1597
  if (opts.from1password) {
1466
1598
  console.log(chalk.yellow('--from-1password is deprecated; use --from 1password:<vault>.'));
1467
1599
  }
1468
- const requestedBackend = parseBackendOpt(opts.backend);
1600
+ const requestedBackend = opts.synced ? 'vault' : parseBackendOpt(opts.backend);
1469
1601
  if (source.kind === 'icloud') {
1470
1602
  await importFromICloud(bundleName, {
1471
1603
  force: opts.force,
1472
1604
  allPlaintext: opts.allPlaintext,
1473
- backend: requestedBackend === 'file' ? 'file' : undefined,
1605
+ backend: requestedBackend === 'keychain' ? undefined : requestedBackend,
1474
1606
  purge: opts.purge,
1475
1607
  });
1476
1608
  return;
@@ -1480,7 +1612,7 @@ Examples:
1480
1612
  // to downgrade keychain -> file) or creates it with the requested backend
1481
1613
  // so a single `import --backend file` works (what `export --host ...
1482
1614
  // --remote-backend file` drives on the remote).
1483
- const bundle = resolveImportBundle(resolvedBundleName, opts.backend);
1615
+ const bundle = resolveImportBundle(resolvedBundleName, opts.backend, opts.synced);
1484
1616
  if (source.kind === '1password') {
1485
1617
  assertOpAvailable();
1486
1618
  const vault = await resolveVault(source.vault);
@@ -1530,7 +1662,7 @@ Examples:
1530
1662
  throw new Error('--to-file needs AGENTS_SECRETS_PASSPHRASE set to encrypt the bundle. ' +
1531
1663
  'Set it for this command, then supply the same value when importing.');
1532
1664
  }
1533
- const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: 'export --to-file', agentOnly: isHeadlessSecretsContext() });
1665
+ const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: 'export --to-file', keyMode: 'storage', agentOnly: isHeadlessSecretsContext() });
1534
1666
  exportBundleToFile(env, opts.toFile, passphrase);
1535
1667
  console.log(chalk.green(`Exported ${Object.keys(env).length} key(s) to ${opts.toFile}`));
1536
1668
  return;
@@ -1557,7 +1689,7 @@ Examples:
1557
1689
  'bundle at rest on the remote. Set it for this command, then unlock it the same way per run.');
1558
1690
  }
1559
1691
  }
1560
- const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `ssh export`, agentOnly: isHeadlessSecretsContext() });
1692
+ const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `ssh export`, keyMode: 'storage', agentOnly: isHeadlessSecretsContext() });
1561
1693
  const dotenv = bundleEnvToDotenv(env);
1562
1694
  const keyCount = Object.keys(env).length;
1563
1695
  // Drive the remote's own `agents secrets import --from -` so the values
@@ -1622,7 +1754,7 @@ Examples:
1622
1754
  if (opts.to1password) {
1623
1755
  assertOpAvailable();
1624
1756
  const vault = await resolveVault(opts.vault);
1625
- const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `1Password vault ${vault}`, agentOnly: isHeadlessSecretsContext() });
1757
+ const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `1Password vault ${vault}`, keyMode: 'storage', agentOnly: isHeadlessSecretsContext() });
1626
1758
  let created = 0;
1627
1759
  let overwritten = 0;
1628
1760
  let skipped = 0;
@@ -1667,11 +1799,12 @@ Examples:
1667
1799
  // terminal stdin, so it is not headless and still prompts.
1668
1800
  const { env } = readAndResolveBundleEnv(resolvedBundleName, {
1669
1801
  caller: `export to shell`,
1802
+ keyMode: 'process',
1670
1803
  agentOnly: isHeadlessSecretsContext(),
1671
1804
  });
1672
1805
  if (opts.format === 'json') {
1673
- // Lossless, machine-readable form consumed by `remoteResolveEnv` over
1674
- // SSH. Single object of KEY -> value; values verbatim (newlines, quotes).
1806
+ // Machine-readable form consumed by `remoteResolveEnv` over SSH.
1807
+ // Single object of injected env KEY -> value; values verbatim.
1675
1808
  process.stdout.write(JSON.stringify(env));
1676
1809
  return;
1677
1810
  }
@@ -1766,6 +1899,60 @@ Examples:
1766
1899
  const { runSecretsMcpServer } = await import('../lib/secrets/mcp.js');
1767
1900
  await runSecretsMcpServer({ version: getCliVersion() });
1768
1901
  });
1902
+ const openclawKeychain = cmd
1903
+ .command('openclaw-keychain')
1904
+ .description('Migrate supported OpenClaw credentials from plaintext config to macOS Keychain-backed SecretRefs');
1905
+ openclawKeychain
1906
+ .command('migrate [config]')
1907
+ .description('Rewrite supported OpenClaw plaintext secrets to exec SecretRefs backed by macOS Keychain')
1908
+ .option('--account <name>', 'Keychain account name', 'openclaw')
1909
+ .option('--dry-run', 'Inspect the migration without writing Keychain or config changes')
1910
+ .action(async (configArg, opts) => {
1911
+ try {
1912
+ const configPath = configArg || path.join(os.homedir(), '.openclaw', 'openclaw.json');
1913
+ const { migrateOpenClawConfigToKeychainRefs, storeOpenClawKeychainServices } = await import('../lib/openclaw-keychain.js');
1914
+ const before = fs.readFileSync(configPath, 'utf-8');
1915
+ const config = JSON.parse(before);
1916
+ const result = migrateOpenClawConfigToKeychainRefs(config, { account: opts.account });
1917
+ if (result.unsupportedEnvKeys.length > 0) {
1918
+ throw new Error(`OpenClaw top-level env keys have no supported SecretRef target: ${result.unsupportedEnvKeys.join(', ')}. ` +
1919
+ `Move them to a supported credential field before removing plaintext.`);
1920
+ }
1921
+ if (!opts.dryRun) {
1922
+ storeOpenClawKeychainServices(result.services, opts.account);
1923
+ if (result.changed)
1924
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
1925
+ }
1926
+ console.log(`OpenClaw Keychain migration ${opts.dryRun ? 'plan' : 'complete'}: ${result.services.length} secret${result.services.length === 1 ? '' : 's'}, ${result.replacedPaths.length} ref${result.replacedPaths.length === 1 ? '' : 's'}, ${result.removedEnvPaths.length} env entr${result.removedEnvPaths.length === 1 ? 'y' : 'ies'} removed.`);
1927
+ for (const p of result.replacedPaths)
1928
+ console.log(`ref ${p}`);
1929
+ for (const p of result.removedEnvPaths)
1930
+ console.log(`removed ${p}`);
1931
+ process.exit(0);
1932
+ }
1933
+ catch (err) {
1934
+ console.error(chalk.red(err.message));
1935
+ process.exit(1);
1936
+ }
1937
+ });
1938
+ openclawKeychain
1939
+ .command('resolve')
1940
+ .description('OpenClaw exec SecretRef resolver for Keychain service ids')
1941
+ .option('--account <name>', 'Keychain account name', 'openclaw')
1942
+ .action(async (opts) => {
1943
+ try {
1944
+ const { readOpenClawKeychainService, resolveOpenClawKeychainRequest } = await import('../lib/openclaw-keychain.js');
1945
+ const raw = readStdinSync();
1946
+ const request = raw.trim() ? JSON.parse(raw) : {};
1947
+ const response = resolveOpenClawKeychainRequest(request, (service) => readOpenClawKeychainService(service, opts.account));
1948
+ process.stdout.write(JSON.stringify(response));
1949
+ process.exit(0);
1950
+ }
1951
+ catch {
1952
+ process.stdout.write(JSON.stringify({ protocolVersion: 1, values: {}, errors: { value: { code: 'NOT_FOUND' } } }));
1953
+ process.exit(1);
1954
+ }
1955
+ });
1769
1956
  cmd
1770
1957
  .command('generate [length]')
1771
1958
  .description('Generate a random password')
@@ -1921,7 +2108,7 @@ Examples:
1921
2108
  try {
1922
2109
  // noAgent: read the real keychain (one Touch ID) rather than the
1923
2110
  // agent we're about to populate.
1924
- const { bundle, env } = readAndResolveBundleEnv(name, { noAgent: true, caller: 'unlock' });
2111
+ const { bundle, env } = readAndResolveBundleEnv(name, { noAgent: true, caller: 'unlock', keyMode: 'storage' });
1925
2112
  if (await agentLoad(name, bundle, env, ttlMs)) {
1926
2113
  loaded++;
1927
2114
  // Persist a durable session snapshot so the unlock survives a daemon
@@ -2160,7 +2347,7 @@ function parseBackendOpt(raw) {
2160
2347
  const v = (raw ?? 'keychain').toLowerCase();
2161
2348
  if (v === 'keychain' || v === 'file')
2162
2349
  return v;
2163
- console.error(chalk.red(`Invalid --backend '${raw}'. Use 'keychain' or 'file'.`));
2350
+ console.error(chalk.red(`Invalid --backend '${raw}'. Use 'keychain' or 'file'. For cross-machine file sync, pass --synced.`));
2164
2351
  process.exit(1);
2165
2352
  }
2166
2353
  /** Human-readable "locks in 3 hours" / "locks in 5 minutes" from an epoch-ms expiry. */
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `agents send <text> --channel <name> --to <target>` — deliver a message over
3
+ * any registered channel (mailbox, telegram, imessage, slack, discord, apps).
4
+ *
5
+ * `agents notify <text>` — owner-facing alias. Fills channel + target from the
6
+ * `notify.owner` block in agents.yaml when not passed, so routines/agents ping
7
+ * the owner without hardcoding a chat id / host / account.
8
+ *
9
+ * Both go through one `doSend()` seam: resolve the provider for the channel
10
+ * (notify.transports picks the transport per host) and call provider.send().
11
+ * The agent mailbox is just one channel; `agents message` stays the richer
12
+ * agent-control command (feed-claim / identity / PTY / resume) on the same spool.
13
+ */
14
+ import type { Command } from 'commander';
15
+ export declare function registerSendCommand(program: Command): void;
@@ -0,0 +1,62 @@
1
+ import chalk from 'chalk';
2
+ import { die } from '../lib/format.js';
3
+ import { readMeta } from '../lib/state.js';
4
+ import { registerBuiltinProviders } from '../lib/channels/providers/index.js';
5
+ import { resolveTransport } from '../lib/channels/resolve.js';
6
+ async function doSend(text, channel, to, opts) {
7
+ registerBuiltinProviders();
8
+ const provider = resolveTransport(channel, readMeta());
9
+ const result = await provider.send(text, {
10
+ target: to,
11
+ thread: opts.thread,
12
+ attachments: opts.attachment,
13
+ from: opts.from,
14
+ dryRun: opts.dryRun,
15
+ });
16
+ if (opts.json) {
17
+ console.log(JSON.stringify(result));
18
+ if (!result.ok)
19
+ process.exit(1);
20
+ return;
21
+ }
22
+ if (!result.ok) {
23
+ die(`send failed [${result.channel} → ${result.id}]: ${result.error ?? 'unknown error'}`);
24
+ }
25
+ const suffix = result.msgId ? chalk.dim(` (${result.msgId})`) : '';
26
+ const dry = opts.dryRun ? chalk.dim(' [dry-run]') : '';
27
+ console.log(chalk.green(`Sent via ${result.channel} → ${result.id}`) + suffix + dry);
28
+ }
29
+ export function registerSendCommand(program) {
30
+ program
31
+ .command('send <text>')
32
+ .description('Send a message through a channel provider (mailbox, telegram, imessage, slack, discord).')
33
+ .requiredOption('--channel <name>', 'channel / provider name')
34
+ .requiredOption('--to <target>', 'channel-specific recipient id')
35
+ .option('--thread <id>', 'channel thread id / timestamp')
36
+ .option('--attachment <path...>', 'file attachment path (repeatable)')
37
+ .option('--from <who>', 'sender label (mailbox)')
38
+ .option('--json', 'output JSON')
39
+ .option('--dry-run', 'resolve + build but do not send')
40
+ .action(async (text, opts) => {
41
+ await doSend(text, opts.channel, opts.to, opts);
42
+ });
43
+ program
44
+ .command('notify <text>')
45
+ .description('Notify the owner (channel + target default to notify.owner in agents.yaml).')
46
+ .option('--channel <name>', 'override owner channel')
47
+ .option('--to <target>', 'override owner target')
48
+ .option('--thread <id>', 'channel thread id / timestamp')
49
+ .option('--attachment <path...>', 'file attachment path (repeatable)')
50
+ .option('--json', 'output JSON')
51
+ .option('--dry-run', 'resolve + build but do not send')
52
+ .action(async (text, opts) => {
53
+ const owner = readMeta().notify?.owner;
54
+ const channel = opts.channel ?? owner?.channel;
55
+ const to = opts.to ?? owner?.to;
56
+ if (!channel || !to) {
57
+ die('notify: no --channel/--to given and no notify.owner.{channel,to} in agents.yaml. ' +
58
+ 'Set notify.owner or pass --channel/--to.');
59
+ }
60
+ await doSend(text, channel, to, opts);
61
+ });
62
+ }