@phnx-labs/agents-cli 1.20.51 → 1.20.53

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 (142) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +12 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/browser.js +215 -7
  5. package/dist/commands/cloud.d.ts +3 -0
  6. package/dist/commands/cloud.js +8 -1
  7. package/dist/commands/events.d.ts +1 -1
  8. package/dist/commands/events.js +2 -3
  9. package/dist/commands/exec.js +82 -3
  10. package/dist/commands/factory.js +8 -0
  11. package/dist/commands/feed.d.ts +38 -0
  12. package/dist/commands/feed.js +274 -0
  13. package/dist/commands/logs.d.ts +5 -1
  14. package/dist/commands/logs.js +248 -3
  15. package/dist/commands/mcp.js +7 -0
  16. package/dist/commands/memory.d.ts +9 -0
  17. package/dist/commands/memory.js +164 -0
  18. package/dist/commands/message.d.ts +11 -6
  19. package/dist/commands/message.js +140 -5
  20. package/dist/commands/routines.js +12 -0
  21. package/dist/commands/secrets-migrate.d.ts +2 -1
  22. package/dist/commands/secrets-migrate.js +88 -13
  23. package/dist/commands/secrets.d.ts +22 -0
  24. package/dist/commands/secrets.js +176 -42
  25. package/dist/commands/sessions.js +10 -1
  26. package/dist/commands/teams.js +4 -0
  27. package/dist/commands/worktree.js +4 -2
  28. package/dist/index.js +21 -22
  29. package/dist/lib/agents.js +249 -17
  30. package/dist/lib/answer-router.d.ts +75 -0
  31. package/dist/lib/answer-router.js +149 -0
  32. package/dist/lib/ask-classifier.d.ts +71 -0
  33. package/dist/lib/ask-classifier.js +197 -0
  34. package/dist/lib/browser/login-detection.d.ts +94 -0
  35. package/dist/lib/browser/login-detection.js +274 -0
  36. package/dist/lib/browser/profiles.d.ts +17 -8
  37. package/dist/lib/browser/profiles.js +27 -8
  38. package/dist/lib/browser/secret-ref.d.ts +10 -0
  39. package/dist/lib/browser/secret-ref.js +14 -0
  40. package/dist/lib/browser/service.js +14 -12
  41. package/dist/lib/cloud/antigravity.d.ts +0 -2
  42. package/dist/lib/cloud/antigravity.js +2 -17
  43. package/dist/lib/cloud/codex.js +3 -18
  44. package/dist/lib/cloud/rush.d.ts +15 -0
  45. package/dist/lib/cloud/rush.js +10 -16
  46. package/dist/lib/cloud/stream.js +2 -0
  47. package/dist/lib/cloud/types.d.ts +21 -0
  48. package/dist/lib/cloud/types.js +81 -0
  49. package/dist/lib/crabbox/cli.d.ts +1 -1
  50. package/dist/lib/crabbox/cli.js +12 -2
  51. package/dist/lib/crabbox/lease.d.ts +19 -0
  52. package/dist/lib/crabbox/lease.js +22 -11
  53. package/dist/lib/crabbox/progress.d.ts +62 -0
  54. package/dist/lib/crabbox/progress.js +129 -0
  55. package/dist/lib/crabbox/runtimes.d.ts +38 -1
  56. package/dist/lib/crabbox/runtimes.js +98 -5
  57. package/dist/lib/daemon.d.ts +12 -9
  58. package/dist/lib/daemon.js +32 -17
  59. package/dist/lib/events.d.ts +31 -5
  60. package/dist/lib/events.js +291 -101
  61. package/dist/lib/exec.js +20 -1
  62. package/dist/lib/feed-outcome.d.ts +101 -0
  63. package/dist/lib/feed-outcome.js +244 -0
  64. package/dist/lib/feed-policy.d.ts +30 -0
  65. package/dist/lib/feed-policy.js +133 -0
  66. package/dist/lib/feed.d.ts +180 -0
  67. package/dist/lib/feed.js +627 -0
  68. package/dist/lib/git.d.ts +17 -1
  69. package/dist/lib/git.js +20 -1
  70. package/dist/lib/hooks.js +529 -14
  71. package/dist/lib/hosts/passthrough.d.ts +3 -3
  72. package/dist/lib/hosts/passthrough.js +3 -3
  73. package/dist/lib/mailbox-gc.d.ts +22 -0
  74. package/dist/lib/mailbox-gc.js +161 -0
  75. package/dist/lib/mailbox.d.ts +26 -2
  76. package/dist/lib/mailbox.js +80 -5
  77. package/dist/lib/mcp.js +82 -0
  78. package/dist/lib/memory.d.ts +55 -0
  79. package/dist/lib/memory.js +274 -0
  80. package/dist/lib/notify.d.ts +16 -0
  81. package/dist/lib/notify.js +61 -0
  82. package/dist/lib/operator.d.ts +26 -0
  83. package/dist/lib/operator.js +107 -0
  84. package/dist/lib/plugins.d.ts +35 -0
  85. package/dist/lib/plugins.js +217 -0
  86. package/dist/lib/remote-agents-json.d.ts +14 -0
  87. package/dist/lib/remote-agents-json.js +94 -0
  88. package/dist/lib/resources/mcp.js +44 -0
  89. package/dist/lib/resources/memory.d.ts +15 -0
  90. package/dist/lib/resources/memory.js +46 -0
  91. package/dist/lib/resources/types.d.ts +2 -2
  92. package/dist/lib/rotate.js +2 -0
  93. package/dist/lib/runner.d.ts +43 -0
  94. package/dist/lib/runner.js +323 -74
  95. package/dist/lib/sandbox.js +6 -0
  96. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  97. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  98. package/dist/lib/secrets/agent.d.ts +21 -0
  99. package/dist/lib/secrets/agent.js +63 -1
  100. package/dist/lib/secrets/bundles.d.ts +33 -1
  101. package/dist/lib/secrets/bundles.js +76 -21
  102. package/dist/lib/secrets/icloud-import.d.ts +79 -0
  103. package/dist/lib/secrets/icloud-import.js +203 -0
  104. package/dist/lib/secrets/index.d.ts +142 -2
  105. package/dist/lib/secrets/index.js +695 -30
  106. package/dist/lib/secrets/remote.js +1 -1
  107. package/dist/lib/secrets/sync.js +1 -1
  108. package/dist/lib/session/active.d.ts +18 -0
  109. package/dist/lib/session/active.js +47 -17
  110. package/dist/lib/session/db.d.ts +9 -1
  111. package/dist/lib/session/db.js +18 -3
  112. package/dist/lib/session/discover.d.ts +13 -0
  113. package/dist/lib/session/discover.js +32 -2
  114. package/dist/lib/session/parse.d.ts +8 -0
  115. package/dist/lib/session/parse.js +42 -21
  116. package/dist/lib/session/remote-active.js +8 -89
  117. package/dist/lib/session/state.d.ts +11 -0
  118. package/dist/lib/session/state.js +50 -1
  119. package/dist/lib/session/tail.d.ts +23 -4
  120. package/dist/lib/session/tail.js +34 -16
  121. package/dist/lib/session/throughput.d.ts +30 -0
  122. package/dist/lib/session/throughput.js +86 -0
  123. package/dist/lib/shim-heal.d.ts +12 -3
  124. package/dist/lib/shim-heal.js +12 -6
  125. package/dist/lib/staleness/detectors/subagents.js +57 -3
  126. package/dist/lib/staleness/writers/hooks.js +7 -3
  127. package/dist/lib/staleness/writers/subagents.js +37 -6
  128. package/dist/lib/startup/command-registry.d.ts +2 -0
  129. package/dist/lib/startup/command-registry.js +4 -0
  130. package/dist/lib/state.d.ts +2 -0
  131. package/dist/lib/state.js +25 -8
  132. package/dist/lib/subagents.d.ts +52 -0
  133. package/dist/lib/subagents.js +315 -12
  134. package/dist/lib/teams/agents.js +6 -3
  135. package/dist/lib/teams/worktree.d.ts +8 -0
  136. package/dist/lib/teams/worktree.js +8 -0
  137. package/dist/lib/types.d.ts +20 -2
  138. package/dist/lib/versions.js +38 -48
  139. package/dist/lib/whats-new.d.ts +5 -3
  140. package/dist/lib/whats-new.js +25 -5
  141. package/package.json +4 -3
  142. package/scripts/postinstall.js +61 -1
@@ -24,6 +24,7 @@ import { parseDuration } from '../lib/hooks/cache.js';
24
24
  import { emit } from '../lib/events.js';
25
25
  import { registerCommandGroups, setHelpSections } from '../lib/help.js';
26
26
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
27
+ import { discoverSyncedBundles, importSyncedBundle, } from '../lib/secrets/icloud-import.js';
27
28
  import { registerSecretsSyncCommands } from './secrets-sync.js';
28
29
  import { registerSecretsMigrateAclCommand } from './secrets-migrate.js';
29
30
  import { registerSecretsImportKeyringCommand } from './secrets-import.js';
@@ -135,6 +136,104 @@ async function resolveVault(vaultOpt) {
135
136
  export function readImportDotenv(from) {
136
137
  return from === '-' ? readStdinSync() : fs.readFileSync(from, 'utf-8');
137
138
  }
139
+ /**
140
+ * Parse the unified `--from <source>` value: a .env path (`-` reads stdin),
141
+ * `1password:<vault>` (bare `1password` prompts for the vault), or `icloud`
142
+ * (legacy iCloud Keychain bundles). The deprecated `--from-1password --vault`
143
+ * pair maps onto the 1password source. A file literally named `icloud` or
144
+ * `1password` can still be imported via an explicit path (`./icloud`).
145
+ */
146
+ export function parseImportSource(opts) {
147
+ if (opts.from && opts.from1password) {
148
+ throw new Error('--from and --from-1password are mutually exclusive.');
149
+ }
150
+ if (opts.from1password)
151
+ return { kind: '1password', vault: opts.vault };
152
+ if (!opts.from) {
153
+ throw new Error("Pass --from <source>: a .env path (- reads stdin), '1password:<vault>', or 'icloud'.");
154
+ }
155
+ if (opts.from === 'icloud')
156
+ return { kind: 'icloud' };
157
+ if (opts.from === '1password')
158
+ return { kind: '1password', vault: opts.vault };
159
+ if (opts.from.startsWith('1password:')) {
160
+ const vault = opts.from.slice('1password:'.length);
161
+ return { kind: '1password', vault: vault || opts.vault };
162
+ }
163
+ return { kind: 'dotenv', path: opts.from };
164
+ }
165
+ /**
166
+ * `secrets import --from icloud` — recover bundles stranded in the iCloud
167
+ * Keychain by the device-local cutover. With a bundle name, imports exactly
168
+ * that bundle; without one, interactively multi-selects from everything
169
+ * discovered (all pre-checked — the common case is "bring them all back").
170
+ */
171
+ async function importFromICloud(bundleName, opts) {
172
+ if (process.platform !== 'darwin') {
173
+ throw new Error('--from icloud reads the macOS iCloud Keychain and is only available on macOS.');
174
+ }
175
+ const candidates = discoverSyncedBundles();
176
+ if (candidates.length === 0) {
177
+ console.log('No legacy iCloud Keychain bundles found.');
178
+ return;
179
+ }
180
+ const describe = (c) => `${c.name} (${c.keys.length} key${c.keys.length === 1 ? '' : 's'}${c.hasMeta ? '' : ', no metadata'})`;
181
+ let chosen;
182
+ if (bundleName) {
183
+ const hit = candidates.find((c) => c.name === bundleName);
184
+ if (!hit) {
185
+ throw new Error(`No iCloud Keychain bundle named '${bundleName}'. Found: ${candidates.map((c) => c.name).join(', ')}`);
186
+ }
187
+ chosen = [hit];
188
+ }
189
+ else if (!isInteractiveTerminal()) {
190
+ throw new Error(`Found ${candidates.length} iCloud Keychain bundle(s): ${candidates.map((c) => c.name).join(', ')}. ` +
191
+ 'Pass a bundle name to import non-interactively.');
192
+ }
193
+ else {
194
+ const { checkbox } = await import('@inquirer/prompts');
195
+ chosen = await checkbox({
196
+ message: 'Which iCloud Keychain bundles to import?',
197
+ choices: candidates.map((c) => ({ name: describe(c), value: c, checked: true })),
198
+ });
199
+ if (chosen.length === 0) {
200
+ console.log('Nothing selected.');
201
+ return;
202
+ }
203
+ }
204
+ for (const candidate of chosen) {
205
+ const result = importSyncedBundle(candidate, opts);
206
+ const parts = [`imported ${result.added} key(s)`];
207
+ if (result.skipped)
208
+ parts.push(`skipped ${result.skipped} (already set, pass --force)`);
209
+ if (result.missing.length)
210
+ parts.push(`unreadable (left in iCloud): ${result.missing.join(', ')}`);
211
+ if (result.unimportable.length)
212
+ parts.push(`reserved, not importable (left in iCloud): ${result.unimportable.join(', ')}`);
213
+ if (opts.purge)
214
+ parts.push(`purged ${result.purged} iCloud item(s)`);
215
+ const line = `${candidate.name}: ${parts.join(', ')}`;
216
+ const warn = result.missing.length > 0 || result.unimportable.length > 0;
217
+ console.log(warn ? chalk.yellow(line) : chalk.green(line));
218
+ }
219
+ }
220
+ /**
221
+ * Printed under a "bundle not found" failure: if the name matches a bundle
222
+ * stranded in the iCloud Keychain (pre-device-local-cutover era), point at the
223
+ * recovery command instead of leaving a dead end.
224
+ */
225
+ function maybePrintSyncedHint(name) {
226
+ if (process.platform !== 'darwin')
227
+ return;
228
+ try {
229
+ if (discoverSyncedBundles().some((c) => c.name === name)) {
230
+ console.error(chalk.yellow(`A legacy iCloud Keychain copy of '${name}' exists. Recover it with: agents secrets import ${name} --from icloud`));
231
+ }
232
+ }
233
+ catch {
234
+ // Hint only — never mask the original error.
235
+ }
236
+ }
138
237
  /**
139
238
  * Build the remote `agents secrets unlock` argv for `unlock --host`. `--all`
140
239
  * forwards verbatim; otherwise the explicit bundle names. A `--ttl` is passed
@@ -525,7 +624,8 @@ export function registerSecretsCommands(program) {
525
624
  agents secrets status show held bundles + when they lock
526
625
  agents secrets rotate <bundle> <key> rotate value, preserve metadata
527
626
  agents secrets import <bundle> --from .env bulk import from .env
528
- agents secrets import <bundle> --from-1password --vault <name>
627
+ agents secrets import <bundle> --from 1password:<vault>
628
+ agents secrets import --from icloud recover legacy iCloud Keychain bundles
529
629
  agents secrets generate [length] generate a random password / PIN / hex
530
630
  agents secrets migrate-acl upgrade legacy items to the biometry ACL
531
631
  `,
@@ -610,7 +710,15 @@ export function registerSecretsCommands(program) {
610
710
  return;
611
711
  }
612
712
  const resolvedName = name ?? (await pickBundleName('view'));
613
- const bundle = readBundle(resolvedName);
713
+ let bundle;
714
+ try {
715
+ bundle = readBundle(resolvedName);
716
+ }
717
+ catch (err) {
718
+ console.error(chalk.red(err.message));
719
+ maybePrintSyncedHint(resolvedName);
720
+ process.exit(1);
721
+ }
614
722
  const entries = describeBundle(bundle);
615
723
  console.log(chalk.bold(bundle.name));
616
724
  if (bundle.description)
@@ -672,7 +780,7 @@ export function registerSecretsCommands(program) {
672
780
  emit('secrets.get', {
673
781
  module: 'secrets',
674
782
  bundle: bundle.name,
675
- caller: 'view --reveal',
783
+ operation: 'view --reveal',
676
784
  source: 'reveal',
677
785
  status: 'success',
678
786
  keyCount: exposedCount,
@@ -713,24 +821,48 @@ export function registerSecretsCommands(program) {
713
821
  }
714
822
  });
715
823
  cmd
716
- .command('get <item>')
717
- .description('Print a raw keychain item by name (for shell hooks/automation). Cross-platform; no bundle required.')
718
- .action((item) => {
824
+ .command('get <item> [key]')
825
+ .description('Print one secret value for shell hooks/automation. One arg = a raw keychain item by name; two args = one KEY out of a bundle (`get <bundle> <KEY>`). Cross-platform.')
826
+ .action((item, key) => {
827
+ if (key === undefined) {
828
+ // Raw keychain item path — unchanged.
829
+ try {
830
+ // Routes through the platform keychain layer: macOS reads bare items
831
+ // via /usr/bin/security (no Touch ID), Linux via secret-tool with the
832
+ // encrypted-file fallback. The value goes to stdout (newline-terminated
833
+ // so `$(agents secrets get NAME)` captures it cleanly); diagnostics go
834
+ // to stderr so they never pollute the captured value.
835
+ const value = getKeychainToken(item);
836
+ // Raw item reads bypass readAndResolveBundleEnv, so audit here too.
837
+ // `item` is the keychain service name, never the value.
838
+ emit('secrets.get', { module: 'secrets', item, source: 'raw-item', status: 'success' });
839
+ process.stdout.write(value.endsWith('\n') ? value : `${value}\n`);
840
+ }
841
+ catch {
842
+ // Missing item is a normal, quiet outcome for a hook probe: exit 1,
843
+ // print nothing to stdout. Callers test the exit code / empty capture.
844
+ process.exit(1);
845
+ }
846
+ return;
847
+ }
848
+ // Bundle-key path: `get <bundle> <KEY>` prints exactly one resolved value.
849
+ // Ungated like the raw path (it IS the automation primitive); the
850
+ // `secrets.get` audit event is emitted inside readAndResolveBundleEnv.
719
851
  try {
720
- // Routes through the platform keychain layer: macOS reads bare items
721
- // via /usr/bin/security (no Touch ID), Linux via secret-tool with the
722
- // encrypted-file fallback. The value goes to stdout (newline-terminated
723
- // so `$(agents secrets get NAME)` captures it cleanly); diagnostics go
724
- // to stderr so they never pollute the captured value.
725
- const value = getKeychainToken(item);
726
- // Raw item reads bypass readAndResolveBundleEnv, so audit here too.
727
- // `item` is the keychain service name, never the value.
728
- emit('secrets.get', { module: 'secrets', item, source: 'raw-item', status: 'success' });
852
+ if (!bundleExists(item)) {
853
+ console.error(chalk.red(`Secrets bundle '${item}' not found.`));
854
+ process.exit(1);
855
+ }
856
+ const { env } = readAndResolveBundleEnv(item, { caller: 'secrets get', keys: [key] });
857
+ if (!(key in env)) {
858
+ console.error(chalk.red(`Key '${key}' not in bundle '${item}'.`));
859
+ process.exit(1);
860
+ }
861
+ const value = env[key];
729
862
  process.stdout.write(value.endsWith('\n') ? value : `${value}\n`);
730
863
  }
731
- catch {
732
- // Missing item is a normal, quiet outcome for a hook probe: exit 1,
733
- // print nothing to stdout. Callers test the exit code / empty capture.
864
+ catch (err) {
865
+ console.error(chalk.red(err.message));
734
866
  process.exit(1);
735
867
  }
736
868
  });
@@ -1160,23 +1292,34 @@ Examples:
1160
1292
  });
1161
1293
  cmd
1162
1294
  .command('import [bundle]')
1163
- .description('Import keys from a .env file or a 1Password vault into a bundle. The bundle is created if it does not exist. Values are stored in the bundle\'s backend (keychain by default).')
1164
- .option('--from <path>', 'Path to a .env file (use - to read the .env from stdin)')
1165
- .option('--from-1password', 'Import secrets from a 1Password vault (requires the op CLI)')
1166
- .option('--vault <name>', '1Password vault name (used with --from-1password)')
1295
+ .description('Import keys into a bundle from a .env file, a 1Password vault, or legacy iCloud Keychain bundles. The bundle is created if it does not exist. Values are stored in the bundle\'s backend (keychain by default).')
1296
+ .option('--from <source>', "Source: a .env path (- reads stdin), '1password:<vault>', or 'icloud' (legacy iCloud Keychain bundles)")
1297
+ .addOption(new Option('--from-1password', 'deprecated alias for --from 1password:<vault>').hideHelp())
1298
+ .addOption(new Option('--vault <name>', 'deprecated: name the vault in --from 1password:<vault>').hideHelp())
1167
1299
  .option('--all-plaintext', 'Store every imported value as a literal in the bundle metadata (skip keychain item creation)')
1168
1300
  .option('--backend <backend>', 'When creating the bundle: keychain (default) or file (passphrase-encrypted, headless-readable)', 'keychain')
1169
1301
  .option('--force', 'Overwrite an existing key in the bundle')
1302
+ .option('--purge', 'With --from icloud: delete the iCloud copies after a successful import (iCloud propagates the deletion to your other devices)')
1170
1303
  .action(async (bundleName, opts) => {
1171
1304
  try {
1172
- if (!opts.from && !opts.from1password) {
1173
- throw new Error('Pass --from <path> to import a .env file, or --from-1password to import from a 1Password vault.');
1305
+ const source = parseImportSource(opts);
1306
+ if (opts.purge && source.kind !== 'icloud') {
1307
+ throw new Error('--purge only applies to --from icloud.');
1174
1308
  }
1175
- if (opts.from && opts.from1password) {
1176
- throw new Error('--from and --from-1password are mutually exclusive.');
1309
+ if (opts.from1password) {
1310
+ console.log(chalk.yellow('--from-1password is deprecated; use --from 1password:<vault>.'));
1177
1311
  }
1178
- const resolvedBundleName = bundleName ?? (await pickBundleName('import into'));
1179
1312
  const requestedBackend = parseBackendOpt(opts.backend);
1313
+ if (source.kind === 'icloud') {
1314
+ await importFromICloud(bundleName, {
1315
+ force: opts.force,
1316
+ allPlaintext: opts.allPlaintext,
1317
+ backend: requestedBackend === 'file' ? 'file' : undefined,
1318
+ purge: opts.purge,
1319
+ });
1320
+ return;
1321
+ }
1322
+ const resolvedBundleName = bundleName ?? (await pickBundleName('import into'));
1180
1323
  // Read the bundle if it exists (inheriting its backend); otherwise
1181
1324
  // create it with the requested backend so a single `import --backend
1182
1325
  // file` works (this is what `export --host ... --remote-backend file`
@@ -1199,9 +1342,9 @@ Examples:
1199
1342
  const store = bundleItemStore(bundle.backend, { noAcl: bundlePolicy(bundle) === 'never' });
1200
1343
  let added = 0;
1201
1344
  let skipped = 0;
1202
- if (opts.from1password) {
1345
+ if (source.kind === '1password') {
1203
1346
  assertOpAvailable();
1204
- const vault = await resolveVault(opts.vault);
1347
+ const vault = await resolveVault(source.vault);
1205
1348
  const items = listItems(vault);
1206
1349
  const { secrets, skipped: opSkipped } = extractSecrets(items, vault);
1207
1350
  for (const { envKey, value } of secrets) {
@@ -1226,7 +1369,7 @@ Examples:
1226
1369
  console.log(chalk.green(`Imported ${added} key(s) from 1Password vault '${vault}'${skipped ? `, skipped ${skipped} (already set, pass --force)` : ''}.`));
1227
1370
  }
1228
1371
  else {
1229
- const raw = readImportDotenv(opts.from);
1372
+ const raw = readImportDotenv(source.path);
1230
1373
  const pairs = parseDotenv(raw);
1231
1374
  for (const [key, value] of Object.entries(pairs)) {
1232
1375
  if (!opts.force && key in bundle.vars) {
@@ -1720,20 +1863,11 @@ Examples:
1720
1863
  console.error(chalk.yellow('Aborted.'));
1721
1864
  return;
1722
1865
  }
1723
- const wasDaily = bundlePolicy(bundle) === 'daily';
1724
1866
  bundle.policy = next;
1867
+ // writeBundle evicts any broker-held copy, so tightening daily ->
1868
+ // always/never takes effect NOW: the next read re-prompts (`always`)
1869
+ // or reads its no-ACL item directly (`never`).
1725
1870
  writeBundle(bundle);
1726
- // Tightening daily -> always/never must take effect NOW, not up to the
1727
- // ~7d hold later. If the broker is already serving this bundle silently
1728
- // (auto-cached under the old `daily` policy), evict it so the next read
1729
- // re-prompts (`always`) or reads its no-ACL item directly (`never`).
1730
- // macOS-only + best-effort; agentLock no-ops off darwin / with no broker.
1731
- if (wasDaily && next !== 'daily') {
1732
- try {
1733
- await agentLock(bundle.name);
1734
- }
1735
- catch { /* broker down — nothing held */ }
1736
- }
1737
1871
  console.log(chalk.green(`${bundle.name} policy set to ${next}.`));
1738
1872
  if (next === 'daily') {
1739
1873
  console.log(chalk.gray('Held by the secrets-agent for ~7 days after one unlock (auto-cache is on by default; disable with `secrets.agent.auto: false` in agents.yaml).'));
@@ -25,7 +25,7 @@ import { machineId, normalizeHost } from '../lib/session/sync/config.js';
25
25
  import { gatherRemoteActive, NO_FANOUT_ENV } from '../lib/session/remote-active.js';
26
26
  import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
27
27
  import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/session/width.js';
28
- import { discoverSessions, countSessionsInScope, resolveSessionById, searchContentIndex } from '../lib/session/discover.js';
28
+ import { discoverSessions, countSessionsInScope, resolveSessionById, searchContentIndex, getSessionRoots } from '../lib/session/discover.js';
29
29
  import { filterTeamSessions } from '../lib/session/team-filter.js';
30
30
  import { parseSession } from '../lib/session/parse.js';
31
31
  import { runRemoteSessions, buildForwardedArgs } from '../lib/session/remote.js';
@@ -796,6 +796,14 @@ async function sessionsAction(query, options) {
796
796
  if (options.device && options.device.length > 0) {
797
797
  options.host = [...(options.host ?? []), ...options.device];
798
798
  }
799
+ // --roots: emit the local session-scan directories, per agent, as JSON. A pure
800
+ // machine-readable query (no listing/render) — external watchers (the Factory
801
+ // extension's fs.watch) read it to track the same dirs the CLI scans, instead
802
+ // of hardcoding `~/.claude|.codex|.gemini`. Always local; ignores other flags.
803
+ if (options.roots) {
804
+ process.stdout.write(JSON.stringify(getSessionRoots(), null, 2) + '\n');
805
+ return;
806
+ }
799
807
  // --host WITHOUT --active. `--json` fans the recent listing out and emits ONE
800
808
  // clean merged SessionMeta[] array (same shape as the local --json path), for
801
809
  // scripts/extensions that JSON.parse a remote's history. Without --json it
@@ -2047,6 +2055,7 @@ export function registerSessionsCommands(program) {
2047
2055
  .option('--artifacts', 'List all files written or edited during a session')
2048
2056
  .option('--artifact <name>', 'Read a specific artifact by filename or path (outputs to stdout)')
2049
2057
  .option('--active', 'Show only sessions running right now across terminals, teams, cloud, and headless agents')
2058
+ .option('--roots', 'With --json: emit the on-disk directories scanned for session transcripts, per agent (for external watchers)')
2050
2059
  .option('--local', 'Only this machine — skip the cross-machine SSH fan-out (default listing and --active)')
2051
2060
  .option('--waiting', 'With --active: show only sessions waiting on your input (exits non-zero if any)')
2052
2061
  .option('--tree', 'Group the listing by directory; drops the id/version columns for readability')
@@ -5,6 +5,7 @@ import { addHostOption } from '../lib/hosts/option.js';
5
5
  import * as path from 'path';
6
6
  import { AgentManager, checkCliSignedIn, collectTeamsDoctorData, getAgentsDir, VALID_TASK_TYPES, } from '../lib/teams/agents.js';
7
7
  import { resolveProvider } from '../lib/cloud/registry.js';
8
+ import { emit } from '../lib/events.js';
8
9
  import { runSupervisor } from '../lib/teams/supervisor.js';
9
10
  import { debug } from '../lib/teams/debug.js';
10
11
  import { runPrWatch, DEFAULT_MAX_WAVES, } from '../lib/teams/pr-watch.js';
@@ -1317,6 +1318,7 @@ export function registerTeamsCommands(program) {
1317
1318
  }
1318
1319
  try {
1319
1320
  const result = await handleSpawn(mgr, team, agent, effectiveTask, cwd, opts.mode, opts.effort, null, cwd, version, opts.name ?? null, after, opts.model ?? null, envOverrides ?? null, taskType, cloudProviderId, cloudSessionId, opts.repo ?? null, opts.branch ?? null, worktreeName, worktreePath, profileName, hostName, hostTarget, hostRepoPath);
1321
+ emit('teams.add', { module: 'teams', team, agent, name: result.name, agent_id: result.agent_id, status: result.status });
1320
1322
  if (isJsonMode(opts)) {
1321
1323
  console.log(JSON.stringify(result, null, 2));
1322
1324
  return;
@@ -1487,6 +1489,7 @@ export function registerTeamsCommands(program) {
1487
1489
  await warnUnsignedTeammates(mgr, team);
1488
1490
  await warnThrottledTeammates(mgr, team);
1489
1491
  }
1492
+ emit('teams.start', { module: 'teams', team, watch: Boolean(opts.watch) });
1490
1493
  if (!opts.watch) {
1491
1494
  await runOneWave(mgr, team, Boolean(opts.json));
1492
1495
  return;
@@ -1526,6 +1529,7 @@ export function registerTeamsCommands(program) {
1526
1529
  },
1527
1530
  });
1528
1531
  const elapsed = Math.floor(result.elapsed_ms / 1000);
1532
+ emit('teams.complete', { module: 'teams', team, stoppedBy: result.stoppedBy, waves: result.waves, durationMs: result.elapsed_ms });
1529
1533
  if (result.stoppedBy === 'drained') {
1530
1534
  console.log(chalk.green(`Factory drained in ${elapsed}s (${result.waves} waves).`));
1531
1535
  }
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { die } from '../lib/format.js';
3
+ import { getGitRoot } from '../lib/git.js';
3
4
  import { execFile } from 'child_process';
4
5
  import { promisify } from 'util';
5
6
  import * as fs from 'fs/promises';
@@ -14,10 +15,11 @@ function isValidTerminalId(id) {
14
15
  // hostile or buggy caller can't inject path traversal or shell metachars.
15
16
  return /^[A-Za-z0-9._-]+$/.test(id) && id.length > 0 && id.length <= 128;
16
17
  }
18
+ // Thin command-layer wrapper over the shared lib/git.ts `getGitRoot`: turns its
19
+ // throw into a clean `die()` message. The git plumbing lives in one place now.
17
20
  async function gitRoot(cwd) {
18
21
  try {
19
- const { stdout } = await execFileAsync('git', ['rev-parse', '--show-toplevel'], { cwd });
20
- return stdout.trim();
22
+ return await getGitRoot(cwd);
21
23
  }
22
24
  catch {
23
25
  die(`Not inside a git repo: ${cwd}`);
package/dist/index.js CHANGED
@@ -50,7 +50,7 @@ if (IS_DEV_BUILD) {
50
50
  // module on each invocation (which loaded the whole ~50-module tree before the
51
51
  // first byte of output), the registry maps a command name to a thunk that
52
52
  // imports only what that command needs. See src/lib/startup/command-registry.ts.
53
- import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadSsh, loadPull, loadPush, loadRepo, loadSetup, } from './lib/startup/command-registry.js';
53
+ import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadFeed, } from './lib/startup/command-registry.js';
54
54
  import { applyGlobalHelpConventions } from './lib/help.js';
55
55
  import { renderWhatsNew } from './lib/whats-new.js';
56
56
  import { emit, redactArgs } from './lib/events.js';
@@ -73,6 +73,7 @@ program
73
73
  .name('agents')
74
74
  .description('Environment manager for AI agents')
75
75
  .version(VERSION)
76
+ .option('--verbose', 'Show startup self-heal details on stderr')
76
77
  .helpOption('-h, --help', 'Show help')
77
78
  .addHelpCommand(false);
78
79
  // ─── Audit backbone ────────────────────────────────────────────────────────────
@@ -101,7 +102,10 @@ program.hook('preAction', (_thisCommand, actionCommand) => {
101
102
  emit('command.start', {
102
103
  module: parts[0],
103
104
  command: parts.join(' '),
104
- args: redactArgs(actionCommand.args),
105
+ // Commander exposes positional operands in actionCommand.args but omits
106
+ // parsed option values. Audit the real argv so sensitive flags are seen
107
+ // and redacted instead of silently bypassing the policy.
108
+ args: redactArgs(process.argv.slice(2, 22)),
105
109
  cwd: process.cwd(),
106
110
  });
107
111
  }
@@ -204,6 +208,7 @@ Automation tips:
204
208
  Options:
205
209
  -V, --version Show version number
206
210
  -h, --help Show help
211
+ --verbose Show startup self-heal details on stderr
207
212
 
208
213
  System config lives in ~/.agents/.system/. Run 'agents <command> --help' for details.
209
214
  `;
@@ -461,8 +466,8 @@ async function checkForUpdates() {
461
466
  }
462
467
  }
463
468
  }
464
- async function maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequested) {
465
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
469
+ async function maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequested, verboseStartup) {
470
+ if (!verboseStartup && (!process.stdin.isTTY || !process.stdout.isTTY)) {
466
471
  return;
467
472
  }
468
473
  // Pure documentation paths must never trigger interactive repair — mirrors
@@ -484,8 +489,12 @@ async function maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequ
484
489
  // (a real native binary shadowing the shim) or is worth saying once (a PATH entry
485
490
  // just added). Suppression is persistent and keyed to the condition — a new
486
491
  // terminal no longer re-nags (the old per-PPID sentinel did, every shell).
487
- const { healShimsInteractive } = await import('./lib/shim-heal.js');
488
- const noticeLines = await healShimsInteractive();
492
+ const { runInteractiveShimHeal } = await import('./lib/shim-heal.js');
493
+ const { summarizeSelfHeal } = await import('./lib/self-heal/registry.js');
494
+ const { noticeLines, report } = await runInteractiveShimHeal();
495
+ if (verboseStartup) {
496
+ process.stderr.write(`[agents] startup self-heal: ${summarizeSelfHeal(report)}\n`);
497
+ }
489
498
  if (noticeLines) {
490
499
  for (const line of noticeLines)
491
500
  console.log(chalk.gray(line));
@@ -496,17 +505,8 @@ async function maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequ
496
505
  // close over entry-point-local state (program re-parsing, VERSION, the npm
497
506
  // upgrade helpers). The lazy registrar and the all-commands fallback below both
498
507
  // call them, so the behavior is identical to the old eager registration.
499
- /** Deprecated `memory` command hard error pointing users at `rules`. */
500
- function registerMemoryCommand(p) {
501
- p.command('memory', { hidden: true })
502
- .allowUnknownOption()
503
- .allowExcessArguments()
504
- .action(() => {
505
- console.error(chalk.red('"agents memory" has been renamed to "agents rules".'));
506
- console.error(chalk.gray('Run "agents rules --help" for usage.\n'));
507
- process.exit(1);
508
- });
509
- }
508
+ // memory is a first-class resource command (see commands/memory.ts via
509
+ // COMMAND_LOADERS). The old memory→rules tombstone was removed in RUSH-1330.
510
510
  /** Deprecated `perms` alias — re-parses as `permissions`. */
511
511
  function registerPermsAliasCommand(p) {
512
512
  p.command('perms', { hidden: true })
@@ -613,9 +613,6 @@ async function reg(loader) {
613
613
  */
614
614
  async function registerEagerForRequest(name) {
615
615
  switch (name) {
616
- case 'memory':
617
- registerMemoryCommand(program);
618
- return true;
619
616
  case 'perms':
620
617
  // The action re-parses as `permissions`, so that target must exist too.
621
618
  registerPermsAliasCommand(program);
@@ -656,7 +653,7 @@ async function registerAllEagerCommands() {
656
653
  await reg(loadHooks);
657
654
  await reg(loadSkills);
658
655
  await reg(loadRules);
659
- registerMemoryCommand(program);
656
+ await reg(loadMemory);
660
657
  await reg(loadPermissions);
661
658
  registerPermsAliasCommand(program);
662
659
  await reg(loadMcp);
@@ -704,6 +701,7 @@ async function registerAllEagerCommands() {
704
701
  await reg(loadLogs);
705
702
  await reg(loadEvents);
706
703
  await reg(loadAudit);
704
+ await reg(loadFeed);
707
705
  await reg(loadSsh);
708
706
  registerJobsCronAliasCommand(program, 'jobs');
709
707
  registerJobsCronAliasCommand(program, 'cron');
@@ -764,6 +762,7 @@ program.on('command:*', (operands) => {
764
762
  // and whether the update check + background sync run at all.
765
763
  const passedArgs = process.argv.slice(2);
766
764
  const requestedCommand = passedArgs.find((arg) => !arg.startsWith('-'));
765
+ const verboseStartup = passedArgs.includes('--verbose');
767
766
  // Help and version output are pure documentation — they must never gate on
768
767
  // setup, otherwise `agents <cmd> --help` becomes useless on a fresh box.
769
768
  const helpOrVersionRequested = passedArgs.some((arg) => arg === '--help' || arg === '-h' || arg === '--version' || arg === '-V');
@@ -918,7 +917,7 @@ if (passedArgs.length === 0) {
918
917
  process.exit(0);
919
918
  }
920
919
  try {
921
- await maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequested);
920
+ await maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequested, verboseStartup);
922
921
  await program.parseAsync();
923
922
  }
924
923
  catch (err) {