@phnx-labs/agents-cli 1.20.50 → 1.20.52

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 (124) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/commands/browser-picker.js +1 -18
  3. package/dist/commands/browser.js +215 -7
  4. package/dist/commands/cloud.js +7 -25
  5. package/dist/commands/computer.d.ts +1 -0
  6. package/dist/commands/computer.js +129 -8
  7. package/dist/commands/events.d.ts +1 -1
  8. package/dist/commands/events.js +2 -3
  9. package/dist/commands/exec.js +66 -8
  10. package/dist/commands/factory.js +9 -4
  11. package/dist/commands/feed.d.ts +9 -0
  12. package/dist/commands/feed.js +69 -0
  13. package/dist/commands/inspect.js +1 -11
  14. package/dist/commands/logs.d.ts +5 -1
  15. package/dist/commands/logs.js +248 -3
  16. package/dist/commands/mcp.js +9 -6
  17. package/dist/commands/message.js +1 -4
  18. package/dist/commands/profiles.js +1 -18
  19. package/dist/commands/repo.js +33 -14
  20. package/dist/commands/resource-view.d.ts +1 -0
  21. package/dist/commands/resource-view.js +5 -17
  22. package/dist/commands/secrets.d.ts +23 -0
  23. package/dist/commands/secrets.js +174 -70
  24. package/dist/commands/sessions-picker.js +1 -18
  25. package/dist/commands/sessions.js +6 -8
  26. package/dist/commands/teams-picker.js +1 -32
  27. package/dist/commands/teams.js +5 -27
  28. package/dist/commands/tmux.js +1 -3
  29. package/dist/commands/view.js +1 -9
  30. package/dist/commands/worktree.js +1 -4
  31. package/dist/index.js +6 -2
  32. package/dist/lib/agents.d.ts +0 -4
  33. package/dist/lib/agents.js +20 -33
  34. package/dist/lib/auto-dispatch-linear.d.ts +18 -0
  35. package/dist/lib/auto-dispatch-linear.js +107 -0
  36. package/dist/lib/auto-dispatch-provider.d.ts +10 -0
  37. package/dist/lib/auto-dispatch-provider.js +25 -0
  38. package/dist/lib/auto-dispatch.d.ts +87 -0
  39. package/dist/lib/auto-dispatch.js +142 -0
  40. package/dist/lib/browser/cdp.js +11 -2
  41. package/dist/lib/browser/drivers/ssh.d.ts +28 -10
  42. package/dist/lib/browser/drivers/ssh.js +57 -18
  43. package/dist/lib/browser/login-detection.d.ts +94 -0
  44. package/dist/lib/browser/login-detection.js +274 -0
  45. package/dist/lib/browser/profiles.d.ts +17 -8
  46. package/dist/lib/browser/profiles.js +27 -8
  47. package/dist/lib/browser/refs.js +1 -5
  48. package/dist/lib/browser/secret-ref.d.ts +10 -0
  49. package/dist/lib/browser/secret-ref.js +14 -0
  50. package/dist/lib/browser/service.js +14 -12
  51. package/dist/lib/cli-resources.d.ts +0 -2
  52. package/dist/lib/cli-resources.js +30 -13
  53. package/dist/lib/cloud/rush.d.ts +15 -24
  54. package/dist/lib/cloud/rush.js +7 -32
  55. package/dist/lib/crabbox/cli.js +4 -1
  56. package/dist/lib/crabbox/lease.d.ts +6 -0
  57. package/dist/lib/crabbox/lease.js +40 -10
  58. package/dist/lib/crabbox/runtimes.d.ts +38 -1
  59. package/dist/lib/crabbox/runtimes.js +98 -5
  60. package/dist/lib/daemon.d.ts +12 -9
  61. package/dist/lib/daemon.js +73 -17
  62. package/dist/lib/events.d.ts +31 -5
  63. package/dist/lib/events.js +288 -101
  64. package/dist/lib/exec.js +32 -14
  65. package/dist/lib/feed.d.ts +56 -0
  66. package/dist/lib/feed.js +251 -0
  67. package/dist/lib/format.d.ts +38 -0
  68. package/dist/lib/format.js +108 -0
  69. package/dist/lib/git.d.ts +21 -0
  70. package/dist/lib/git.js +92 -0
  71. package/dist/lib/hooks/cache.d.ts +9 -2
  72. package/dist/lib/hooks/cache.js +220 -8
  73. package/dist/lib/hooks.js +24 -10
  74. package/dist/lib/hosts/passthrough.js +1 -0
  75. package/dist/lib/platform/exec.d.ts +4 -1
  76. package/dist/lib/platform/exec.js +8 -2
  77. package/dist/lib/resources.d.ts +0 -8
  78. package/dist/lib/resources.js +0 -10
  79. package/dist/lib/rotate.js +2 -0
  80. package/dist/lib/runner.js +10 -2
  81. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  82. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  83. package/dist/lib/secrets/agent.d.ts +21 -0
  84. package/dist/lib/secrets/agent.js +63 -1
  85. package/dist/lib/secrets/bundles.d.ts +33 -1
  86. package/dist/lib/secrets/bundles.js +38 -8
  87. package/dist/lib/secrets/icloud-import.d.ts +70 -0
  88. package/dist/lib/secrets/icloud-import.js +173 -0
  89. package/dist/lib/secrets/index.d.ts +36 -0
  90. package/dist/lib/secrets/index.js +99 -9
  91. package/dist/lib/secrets/remote.js +1 -1
  92. package/dist/lib/secrets/sync.js +1 -1
  93. package/dist/lib/session/active.d.ts +11 -1
  94. package/dist/lib/session/active.js +3 -0
  95. package/dist/lib/session/db.d.ts +1 -4
  96. package/dist/lib/session/db.js +20 -25
  97. package/dist/lib/session/discover.d.ts +2 -2
  98. package/dist/lib/session/discover.js +60 -48
  99. package/dist/lib/session/parse.js +35 -34
  100. package/dist/lib/session/render.d.ts +7 -3
  101. package/dist/lib/session/render.js +15 -9
  102. package/dist/lib/session/state.d.ts +55 -0
  103. package/dist/lib/session/state.js +100 -11
  104. package/dist/lib/session/types.d.ts +9 -0
  105. package/dist/lib/shims.d.ts +9 -3
  106. package/dist/lib/shims.js +44 -8
  107. package/dist/lib/ssh-tunnel.d.ts +33 -2
  108. package/dist/lib/ssh-tunnel.js +94 -7
  109. package/dist/lib/staleness/types.d.ts +0 -1
  110. package/dist/lib/startup/command-registry.d.ts +1 -0
  111. package/dist/lib/startup/command-registry.js +2 -0
  112. package/dist/lib/state.d.ts +2 -0
  113. package/dist/lib/state.js +25 -8
  114. package/dist/lib/teams/agents.js +6 -3
  115. package/dist/lib/types.d.ts +24 -1
  116. package/dist/lib/versions.d.ts +0 -26
  117. package/dist/lib/versions.js +2 -145
  118. package/dist/lib/warn-unpushed.d.ts +40 -0
  119. package/dist/lib/warn-unpushed.js +128 -0
  120. package/dist/lib/whats-new.d.ts +5 -3
  121. package/dist/lib/whats-new.js +25 -5
  122. package/package.json +3 -1
  123. package/dist/lib/resources/index.d.ts +0 -53
  124. package/dist/lib/resources/index.js +0 -76
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { Option } from 'commander';
9
9
  import chalk from 'chalk';
10
+ import { padVisible, readStdinSync } from '../lib/format.js';
10
11
  import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
11
12
  import * as fs from 'fs';
12
13
  import { SSH_TARGET_RE, assertValidSshTarget, sshExec } from '../lib/ssh-exec.js';
@@ -23,6 +24,7 @@ import { parseDuration } from '../lib/hooks/cache.js';
23
24
  import { emit } from '../lib/events.js';
24
25
  import { registerCommandGroups, setHelpSections } from '../lib/help.js';
25
26
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
27
+ import { discoverSyncedBundles, importSyncedBundle, } from '../lib/secrets/icloud-import.js';
26
28
  import { registerSecretsSyncCommands } from './secrets-sync.js';
27
29
  import { registerSecretsMigrateAclCommand } from './secrets-migrate.js';
28
30
  import { registerSecretsImportKeyringCommand } from './secrets-import.js';
@@ -125,23 +127,6 @@ async function resolveVault(vaultOpt) {
125
127
  });
126
128
  }
127
129
  /** Read all available data from stdin synchronously, trimmed. */
128
- function readStdinSync() {
129
- const chunks = [];
130
- const buf = Buffer.alloc(65536);
131
- while (true) {
132
- let bytesRead;
133
- try {
134
- bytesRead = fs.readSync(0, buf, 0, buf.length, null);
135
- }
136
- catch {
137
- break;
138
- }
139
- if (bytesRead === 0)
140
- break;
141
- chunks.push(Buffer.from(buf.subarray(0, bytesRead)));
142
- }
143
- return Buffer.concat(chunks).toString('utf-8').trim();
144
- }
145
130
  /**
146
131
  * Read the raw `.env` text for `import --from <path|->`. A `-` reads the .env
147
132
  * from stdin (the SSH push path: `export --host` pipes the resolved dotenv over
@@ -151,6 +136,101 @@ function readStdinSync() {
151
136
  export function readImportDotenv(from) {
152
137
  return from === '-' ? readStdinSync() : fs.readFileSync(from, 'utf-8');
153
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 (opts.purge)
212
+ parts.push(`purged ${result.purged} iCloud item(s)`);
213
+ const line = `${candidate.name}: ${parts.join(', ')}`;
214
+ console.log(result.missing.length ? chalk.yellow(line) : chalk.green(line));
215
+ }
216
+ }
217
+ /**
218
+ * Printed under a "bundle not found" failure: if the name matches a bundle
219
+ * stranded in the iCloud Keychain (pre-device-local-cutover era), point at the
220
+ * recovery command instead of leaving a dead end.
221
+ */
222
+ function maybePrintSyncedHint(name) {
223
+ if (process.platform !== 'darwin')
224
+ return;
225
+ try {
226
+ if (discoverSyncedBundles().some((c) => c.name === name)) {
227
+ console.error(chalk.yellow(`A legacy iCloud Keychain copy of '${name}' exists. Recover it with: agents secrets import ${name} --from icloud`));
228
+ }
229
+ }
230
+ catch {
231
+ // Hint only — never mask the original error.
232
+ }
233
+ }
154
234
  /**
155
235
  * Build the remote `agents secrets unlock` argv for `unlock --host`. `--all`
156
236
  * forwards verbatim; otherwise the explicit bundle names. A `--ttl` is passed
@@ -257,17 +337,6 @@ async function browseRemote(targets, args, tty) {
257
337
  process.exit(1);
258
338
  }
259
339
  /** Strip ANSI escape sequences so padding can be computed on visible width. */
260
- function visibleWidth(s) {
261
- // eslint-disable-next-line no-control-regex
262
- return s.replace(/\x1b\[[0-9;]*m/g, '').length;
263
- }
264
- /** padEnd that respects ANSI color codes (chalk-wrapped strings have invisible bytes). */
265
- function padVisible(s, n) {
266
- const w = visibleWidth(s);
267
- if (w >= n)
268
- return s;
269
- return s + ' '.repeat(n - w);
270
- }
271
340
  /** Render an ISO-8601 timestamp as a compact relative age: "now", "5m", "1h", "3d", "2w", "4mo", "1y". */
272
341
  function relativeAge(iso) {
273
342
  const t = Date.parse(iso);
@@ -552,7 +621,8 @@ export function registerSecretsCommands(program) {
552
621
  agents secrets status show held bundles + when they lock
553
622
  agents secrets rotate <bundle> <key> rotate value, preserve metadata
554
623
  agents secrets import <bundle> --from .env bulk import from .env
555
- agents secrets import <bundle> --from-1password --vault <name>
624
+ agents secrets import <bundle> --from 1password:<vault>
625
+ agents secrets import --from icloud recover legacy iCloud Keychain bundles
556
626
  agents secrets generate [length] generate a random password / PIN / hex
557
627
  agents secrets migrate-acl upgrade legacy items to the biometry ACL
558
628
  `,
@@ -637,7 +707,15 @@ export function registerSecretsCommands(program) {
637
707
  return;
638
708
  }
639
709
  const resolvedName = name ?? (await pickBundleName('view'));
640
- const bundle = readBundle(resolvedName);
710
+ let bundle;
711
+ try {
712
+ bundle = readBundle(resolvedName);
713
+ }
714
+ catch (err) {
715
+ console.error(chalk.red(err.message));
716
+ maybePrintSyncedHint(resolvedName);
717
+ process.exit(1);
718
+ }
641
719
  const entries = describeBundle(bundle);
642
720
  console.log(chalk.bold(bundle.name));
643
721
  if (bundle.description)
@@ -699,7 +777,7 @@ export function registerSecretsCommands(program) {
699
777
  emit('secrets.get', {
700
778
  module: 'secrets',
701
779
  bundle: bundle.name,
702
- caller: 'view --reveal',
780
+ operation: 'view --reveal',
703
781
  source: 'reveal',
704
782
  status: 'success',
705
783
  keyCount: exposedCount,
@@ -740,24 +818,48 @@ export function registerSecretsCommands(program) {
740
818
  }
741
819
  });
742
820
  cmd
743
- .command('get <item>')
744
- .description('Print a raw keychain item by name (for shell hooks/automation). Cross-platform; no bundle required.')
745
- .action((item) => {
821
+ .command('get <item> [key]')
822
+ .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.')
823
+ .action((item, key) => {
824
+ if (key === undefined) {
825
+ // Raw keychain item path — unchanged.
826
+ try {
827
+ // Routes through the platform keychain layer: macOS reads bare items
828
+ // via /usr/bin/security (no Touch ID), Linux via secret-tool with the
829
+ // encrypted-file fallback. The value goes to stdout (newline-terminated
830
+ // so `$(agents secrets get NAME)` captures it cleanly); diagnostics go
831
+ // to stderr so they never pollute the captured value.
832
+ const value = getKeychainToken(item);
833
+ // Raw item reads bypass readAndResolveBundleEnv, so audit here too.
834
+ // `item` is the keychain service name, never the value.
835
+ emit('secrets.get', { module: 'secrets', item, source: 'raw-item', status: 'success' });
836
+ process.stdout.write(value.endsWith('\n') ? value : `${value}\n`);
837
+ }
838
+ catch {
839
+ // Missing item is a normal, quiet outcome for a hook probe: exit 1,
840
+ // print nothing to stdout. Callers test the exit code / empty capture.
841
+ process.exit(1);
842
+ }
843
+ return;
844
+ }
845
+ // Bundle-key path: `get <bundle> <KEY>` prints exactly one resolved value.
846
+ // Ungated like the raw path (it IS the automation primitive); the
847
+ // `secrets.get` audit event is emitted inside readAndResolveBundleEnv.
746
848
  try {
747
- // Routes through the platform keychain layer: macOS reads bare items
748
- // via /usr/bin/security (no Touch ID), Linux via secret-tool with the
749
- // encrypted-file fallback. The value goes to stdout (newline-terminated
750
- // so `$(agents secrets get NAME)` captures it cleanly); diagnostics go
751
- // to stderr so they never pollute the captured value.
752
- const value = getKeychainToken(item);
753
- // Raw item reads bypass readAndResolveBundleEnv, so audit here too.
754
- // `item` is the keychain service name, never the value.
755
- emit('secrets.get', { module: 'secrets', item, source: 'raw-item', status: 'success' });
849
+ if (!bundleExists(item)) {
850
+ console.error(chalk.red(`Secrets bundle '${item}' not found.`));
851
+ process.exit(1);
852
+ }
853
+ const { env } = readAndResolveBundleEnv(item, { caller: 'secrets get', keys: [key] });
854
+ if (!(key in env)) {
855
+ console.error(chalk.red(`Key '${key}' not in bundle '${item}'.`));
856
+ process.exit(1);
857
+ }
858
+ const value = env[key];
756
859
  process.stdout.write(value.endsWith('\n') ? value : `${value}\n`);
757
860
  }
758
- catch {
759
- // Missing item is a normal, quiet outcome for a hook probe: exit 1,
760
- // print nothing to stdout. Callers test the exit code / empty capture.
861
+ catch (err) {
862
+ console.error(chalk.red(err.message));
761
863
  process.exit(1);
762
864
  }
763
865
  });
@@ -1187,23 +1289,34 @@ Examples:
1187
1289
  });
1188
1290
  cmd
1189
1291
  .command('import [bundle]')
1190
- .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).')
1191
- .option('--from <path>', 'Path to a .env file (use - to read the .env from stdin)')
1192
- .option('--from-1password', 'Import secrets from a 1Password vault (requires the op CLI)')
1193
- .option('--vault <name>', '1Password vault name (used with --from-1password)')
1292
+ .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).')
1293
+ .option('--from <source>', "Source: a .env path (- reads stdin), '1password:<vault>', or 'icloud' (legacy iCloud Keychain bundles)")
1294
+ .addOption(new Option('--from-1password', 'deprecated alias for --from 1password:<vault>').hideHelp())
1295
+ .addOption(new Option('--vault <name>', 'deprecated: name the vault in --from 1password:<vault>').hideHelp())
1194
1296
  .option('--all-plaintext', 'Store every imported value as a literal in the bundle metadata (skip keychain item creation)')
1195
1297
  .option('--backend <backend>', 'When creating the bundle: keychain (default) or file (passphrase-encrypted, headless-readable)', 'keychain')
1196
1298
  .option('--force', 'Overwrite an existing key in the bundle')
1299
+ .option('--purge', 'With --from icloud: delete the iCloud copies after a successful import (iCloud propagates the deletion to your other devices)')
1197
1300
  .action(async (bundleName, opts) => {
1198
1301
  try {
1199
- if (!opts.from && !opts.from1password) {
1200
- throw new Error('Pass --from <path> to import a .env file, or --from-1password to import from a 1Password vault.');
1302
+ const source = parseImportSource(opts);
1303
+ if (opts.purge && source.kind !== 'icloud') {
1304
+ throw new Error('--purge only applies to --from icloud.');
1201
1305
  }
1202
- if (opts.from && opts.from1password) {
1203
- throw new Error('--from and --from-1password are mutually exclusive.');
1306
+ if (opts.from1password) {
1307
+ console.log(chalk.yellow('--from-1password is deprecated; use --from 1password:<vault>.'));
1204
1308
  }
1205
- const resolvedBundleName = bundleName ?? (await pickBundleName('import into'));
1206
1309
  const requestedBackend = parseBackendOpt(opts.backend);
1310
+ if (source.kind === 'icloud') {
1311
+ await importFromICloud(bundleName, {
1312
+ force: opts.force,
1313
+ allPlaintext: opts.allPlaintext,
1314
+ backend: requestedBackend === 'file' ? 'file' : undefined,
1315
+ purge: opts.purge,
1316
+ });
1317
+ return;
1318
+ }
1319
+ const resolvedBundleName = bundleName ?? (await pickBundleName('import into'));
1207
1320
  // Read the bundle if it exists (inheriting its backend); otherwise
1208
1321
  // create it with the requested backend so a single `import --backend
1209
1322
  // file` works (this is what `export --host ... --remote-backend file`
@@ -1226,9 +1339,9 @@ Examples:
1226
1339
  const store = bundleItemStore(bundle.backend, { noAcl: bundlePolicy(bundle) === 'never' });
1227
1340
  let added = 0;
1228
1341
  let skipped = 0;
1229
- if (opts.from1password) {
1342
+ if (source.kind === '1password') {
1230
1343
  assertOpAvailable();
1231
- const vault = await resolveVault(opts.vault);
1344
+ const vault = await resolveVault(source.vault);
1232
1345
  const items = listItems(vault);
1233
1346
  const { secrets, skipped: opSkipped } = extractSecrets(items, vault);
1234
1347
  for (const { envKey, value } of secrets) {
@@ -1253,7 +1366,7 @@ Examples:
1253
1366
  console.log(chalk.green(`Imported ${added} key(s) from 1Password vault '${vault}'${skipped ? `, skipped ${skipped} (already set, pass --force)` : ''}.`));
1254
1367
  }
1255
1368
  else {
1256
- const raw = readImportDotenv(opts.from);
1369
+ const raw = readImportDotenv(source.path);
1257
1370
  const pairs = parseDotenv(raw);
1258
1371
  for (const [key, value] of Object.entries(pairs)) {
1259
1372
  if (!opts.force && key in bundle.vars) {
@@ -1747,20 +1860,11 @@ Examples:
1747
1860
  console.error(chalk.yellow('Aborted.'));
1748
1861
  return;
1749
1862
  }
1750
- const wasDaily = bundlePolicy(bundle) === 'daily';
1751
1863
  bundle.policy = next;
1864
+ // writeBundle evicts any broker-held copy, so tightening daily ->
1865
+ // always/never takes effect NOW: the next read re-prompts (`always`)
1866
+ // or reads its no-ACL item directly (`never`).
1752
1867
  writeBundle(bundle);
1753
- // Tightening daily -> always/never must take effect NOW, not up to the
1754
- // ~7d hold later. If the broker is already serving this bundle silently
1755
- // (auto-cached under the old `daily` policy), evict it so the next read
1756
- // re-prompts (`always`) or reads its no-ACL item directly (`never`).
1757
- // macOS-only + best-effort; agentLock no-ops off darwin / with no broker.
1758
- if (wasDaily && next !== 'daily') {
1759
- try {
1760
- await agentLock(bundle.name);
1761
- }
1762
- catch { /* broker down — nothing held */ }
1763
- }
1764
1868
  console.log(chalk.green(`${bundle.name} policy set to ${next}.`));
1765
1869
  if (next === 'daily') {
1766
1870
  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).'));
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import fs from 'node:fs';
9
9
  import chalk from 'chalk';
10
+ import { truncate, humanDuration } from '../lib/format.js';
10
11
  import { parseSession, sanitizeForTerminal } from '../lib/session/parse.js';
11
12
  import { cleanSessionPrompt, extractSessionTopic } from '../lib/session/prompt.js';
12
13
  import { linkPath, relativeToCwd } from '../lib/session/render.js';
@@ -154,21 +155,6 @@ function extractTiming(events) {
154
155
  const dur = Number.isNaN(lastMs) ? undefined : humanDuration(Math.max(0, lastMs - firstMs));
155
156
  return { startedAgo: ago, duration: dur };
156
157
  }
157
- function humanDuration(ms) {
158
- const s = Math.floor(ms / 1000);
159
- if (s < 60)
160
- return `${s}s`;
161
- const m = Math.floor(s / 60);
162
- if (m < 60)
163
- return `${m}m`;
164
- const h = Math.floor(m / 60);
165
- const mm = m % 60;
166
- if (h < 24)
167
- return mm ? `${h}h ${mm}m` : `${h}h`;
168
- const d = Math.floor(h / 24);
169
- const hh = h % 24;
170
- return hh ? `${d}d ${hh}h` : `${d}d`;
171
- }
172
158
  function countMessages(events) {
173
159
  return events.filter(e => e.type === 'message').length;
174
160
  }
@@ -364,9 +350,6 @@ function renderTodos(todos, termWidth) {
364
350
  }
365
351
  return out;
366
352
  }
367
- function truncate(s, max) {
368
- return s.length > max ? s.slice(0, max - 1) + '…' : s;
369
- }
370
353
  /** Show an interactive session picker and return the selected session with its action (resume or view). */
371
354
  export async function sessionPicker(config) {
372
355
  const picked = await itemPicker({
@@ -12,6 +12,7 @@ import * as os from 'os';
12
12
  import * as path from 'path';
13
13
  import { spawn } from 'child_process';
14
14
  import chalk from 'chalk';
15
+ import { truncate, padRight } from '../lib/format.js';
15
16
  import ora from 'ora';
16
17
  import { SESSION_AGENTS } from '../lib/session/types.js';
17
18
  import { discoverArtifacts, readArtifact, resolveArtifact } from '../lib/session/artifacts.js';
@@ -1349,8 +1350,11 @@ async function renderSession(session, mode, filters, options = {}) {
1349
1350
  process.stdout.write(renderMarkdown(renderConversationMarkdown(events, { redact: options.noRedact !== true })));
1350
1351
  return;
1351
1352
  }
1352
- // json — no header, raw events only (pipeable)
1353
- process.stdout.write(renderJson(events));
1353
+ // json — normalized events plus the durable session signals from the state
1354
+ // engine (plan text, PR, worktree, ticket). Pre-1.20.51 emitted a bare event
1355
+ // array; consumers that JSON.parse this now read `output.events` for the
1356
+ // array. See issue #743 (plan surfaced) and CHANGELOG for the shape change.
1357
+ process.stdout.write(renderJson(events, session));
1354
1358
  }
1355
1359
  function renderTopicCell(label, topic, query, visibleWidth, paddedWidth) {
1356
1360
  const lbl = (label ?? '').trim();
@@ -2289,9 +2293,3 @@ function formatAbsoluteTime(isoTimestamp) {
2289
2293
  const mm = String(d.getMinutes()).padStart(2, '0');
2290
2294
  return `${months[d.getMonth()]} ${d.getDate()} ${hh}:${mm}`;
2291
2295
  }
2292
- function padRight(s, width) {
2293
- return s.length >= width ? s : s + ' '.repeat(width - s.length);
2294
- }
2295
- function truncate(s, max) {
2296
- return s.length > max ? s.slice(0, max - 1) + '.' : s;
2297
- }
@@ -7,6 +7,7 @@
7
7
  * activity for each team.
8
8
  */
9
9
  import chalk from 'chalk';
10
+ import { relTime, truncate, humanDuration } from '../lib/format.js';
10
11
  import { itemPicker } from '../lib/picker.js';
11
12
  const AGENT_LABEL = {
12
13
  claude: 'Claude',
@@ -26,23 +27,6 @@ function statusColor(status) {
26
27
  default: return chalk.white;
27
28
  }
28
29
  }
29
- function relTime(iso) {
30
- const secs = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
31
- if (secs < 10)
32
- return 'just now';
33
- if (secs < 60)
34
- return `${secs}s ago`;
35
- if (secs < 3600)
36
- return `${Math.floor(secs / 60)}m ago`;
37
- if (secs < 86400)
38
- return `${Math.floor(secs / 3600)}h ago`;
39
- return `${Math.floor(secs / 86400)}d ago`;
40
- }
41
- function truncate(s, n) {
42
- if (s.length <= n)
43
- return s;
44
- return s.slice(0, n - 1) + '…';
45
- }
46
30
  function firstLine(s) {
47
31
  // Collapse to the first non-empty line so multi-line messages (markdown,
48
32
  // code blocks) render cleanly inside a one-line preview slot.
@@ -120,21 +104,6 @@ function formatComposition(agents) {
120
104
  .map(([type, n]) => (n === 1 ? type : `${type}×${n}`));
121
105
  return parts.join(' ');
122
106
  }
123
- function humanDuration(ms) {
124
- const s = Math.floor(ms / 1000);
125
- if (s < 60)
126
- return `${s}s`;
127
- const m = Math.floor(s / 60);
128
- if (m < 60)
129
- return `${m}m`;
130
- const h = Math.floor(m / 60);
131
- const mm = m % 60;
132
- if (h < 24)
133
- return mm ? `${h}h${mm}m` : `${h}h`;
134
- const d = Math.floor(h / 24);
135
- const hh = h % 24;
136
- return hh ? `${d}d${hh}h` : `${d}d`;
137
- }
138
107
  function runtimeSpan(t) {
139
108
  const startMs = new Date(t.created_at).getTime();
140
109
  const endMs = new Date(t.modified_at).getTime();
@@ -1,9 +1,11 @@
1
1
  import chalk from 'chalk';
2
+ import { die, relTime, truncate, isJsonMode, padRight } from '../lib/format.js';
2
3
  import * as fs from 'fs/promises';
3
4
  import { addHostOption } from '../lib/hosts/option.js';
4
5
  import * as path from 'path';
5
6
  import { AgentManager, checkCliSignedIn, collectTeamsDoctorData, getAgentsDir, VALID_TASK_TYPES, } from '../lib/teams/agents.js';
6
7
  import { resolveProvider } from '../lib/cloud/registry.js';
8
+ import { emit } from '../lib/events.js';
7
9
  import { runSupervisor } from '../lib/teams/supervisor.js';
8
10
  import { debug } from '../lib/teams/debug.js';
9
11
  import { runPrWatch, DEFAULT_MAX_WAVES, } from '../lib/teams/pr-watch.js';
@@ -48,13 +50,6 @@ const VALID_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'auto'];
48
50
  const VALID_CLOUD_PROVIDERS = ['rush', 'codex', 'factory'];
49
51
  // Auto-enable JSON mode when piped / not a TTY so AI agent consumers get
50
52
  // parseable output by default.
51
- function isJsonMode(opts) {
52
- return Boolean(opts.json) || !process.stdout.isTTY;
53
- }
54
- function die(msg, code = 1) {
55
- console.error(chalk.red(msg));
56
- process.exit(code);
57
- }
58
53
  function statusColor(status) {
59
54
  switch (status) {
60
55
  case 'pending': return chalk.blue;
@@ -65,23 +60,6 @@ function statusColor(status) {
65
60
  default: return chalk.white;
66
61
  }
67
62
  }
68
- function relTime(iso) {
69
- const secs = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
70
- if (secs < 10)
71
- return 'just now';
72
- if (secs < 60)
73
- return `${secs}s ago`;
74
- if (secs < 3600)
75
- return `${Math.floor(secs / 60)}m ago`;
76
- if (secs < 86400)
77
- return `${Math.floor(secs / 3600)}h ago`;
78
- return `${Math.floor(secs / 86400)}d ago`;
79
- }
80
- function truncate(s, n) {
81
- if (s.length <= n)
82
- return s;
83
- return s.slice(0, n - 1) + '…';
84
- }
85
63
  function compactPrompt(s, n = 160) {
86
64
  return truncate(s.replace(/\s+/g, ' ').trim(), n);
87
65
  }
@@ -98,9 +76,6 @@ function formatTimestamp(iso) {
98
76
  minute: '2-digit',
99
77
  });
100
78
  }
101
- function padRight(s, width) {
102
- return s.length >= width ? s : s + ' '.repeat(width - s.length);
103
- }
104
79
  function fullName(type, version) {
105
80
  const name = AGENT_NAMES[type];
106
81
  return version ? `${name} ${version}` : name;
@@ -1343,6 +1318,7 @@ export function registerTeamsCommands(program) {
1343
1318
  }
1344
1319
  try {
1345
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 });
1346
1322
  if (isJsonMode(opts)) {
1347
1323
  console.log(JSON.stringify(result, null, 2));
1348
1324
  return;
@@ -1513,6 +1489,7 @@ export function registerTeamsCommands(program) {
1513
1489
  await warnUnsignedTeammates(mgr, team);
1514
1490
  await warnThrottledTeammates(mgr, team);
1515
1491
  }
1492
+ emit('teams.start', { module: 'teams', team, watch: Boolean(opts.watch) });
1516
1493
  if (!opts.watch) {
1517
1494
  await runOneWave(mgr, team, Boolean(opts.json));
1518
1495
  return;
@@ -1552,6 +1529,7 @@ export function registerTeamsCommands(program) {
1552
1529
  },
1553
1530
  });
1554
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 });
1555
1533
  if (result.stoppedBy === 'drained') {
1556
1534
  console.log(chalk.green(`Factory drained in ${elapsed}s (${result.waves} waves).`));
1557
1535
  }
@@ -21,6 +21,7 @@
21
21
  * agents tmux kill-all [--yes]
22
22
  */
23
23
  import chalk from 'chalk';
24
+ import { truncate } from '../lib/format.js';
24
25
  import * as path from 'path';
25
26
  import { setHelpSections } from '../lib/help.js';
26
27
  import { assertTmuxAvailable, attachTmux, capturePane, createSession, getDefaultSocketPath, getTmuxVersion, hasSession, isTmuxInstalled, killAll, killSession, listSessions, readSessionMeta, sendKeys, splitPane, TmuxCommandError, TmuxSessionError, TmuxUnavailableError, } from '../lib/tmux/index.js';
@@ -395,9 +396,6 @@ function formatAge(secs) {
395
396
  const h = Math.floor(m / 60);
396
397
  return `${h}h ${m % 60}m`;
397
398
  }
398
- function truncate(s, n) {
399
- return s.length > n ? `${s.slice(0, n - 1)}…` : s;
400
- }
401
399
  function printMeta(m) {
402
400
  console.log(` ${chalk.bold('name')} ${m.name}`);
403
401
  console.log(` ${chalk.bold('socket')} ${m.socket}`);
@@ -1,5 +1,6 @@
1
1
  import { addHostOption } from '../lib/hosts/option.js';
2
2
  import chalk from 'chalk';
3
+ import { visibleWidth, termLink } from '../lib/format.js';
3
4
  import ora from 'ora';
4
5
  import * as fs from 'fs';
5
6
  import * as path from 'path';
@@ -67,12 +68,6 @@ function profileKindAndModel(model, planWidth) {
67
68
  const kind = 'profile'.padEnd(Math.max(planWidth, 'profile'.length));
68
69
  return `${kind} ${model}`;
69
70
  }
70
- function termLink(text, filePath) {
71
- if (!process.stdout.isTTY)
72
- return text;
73
- const url = `file://${filePath}`;
74
- return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
75
- }
76
71
  /**
77
72
  * Resolve a resource path to something the IDE can open inline. When `p` is a
78
73
  * directory, OSC 8 file:// links cause IDEs (Cursor/VS Code) to open it as a
@@ -112,9 +107,6 @@ function formatLastActive(date) {
112
107
  return chalk.gray(`${days}d ago`);
113
108
  return chalk.gray(`${days}d ago`);
114
109
  }
115
- function visibleWidth(s) {
116
- return s.replace(/\u001b\[[0-9;]*m/g, '').length;
117
- }
118
110
  function compareVersions(a, b) {
119
111
  const partsA = a.split('.').map(Number);
120
112
  const partsB = b.split('.').map(Number);
@@ -1,4 +1,5 @@
1
1
  import chalk from 'chalk';
2
+ import { die } from '../lib/format.js';
2
3
  import { execFile } from 'child_process';
3
4
  import { promisify } from 'util';
4
5
  import * as fs from 'fs/promises';
@@ -8,10 +9,6 @@ import { setHelpSections } from '../lib/help.js';
8
9
  const execFileAsync = promisify(execFile);
9
10
  const WORKTREE_SUBDIR = path.join('.agents', 'worktrees');
10
11
  const BRANCH_PREFIX = 'agents/';
11
- function die(msg, code = 1) {
12
- console.error(chalk.red(msg));
13
- process.exit(code);
14
- }
15
12
  function isValidTerminalId(id) {
16
13
  // Allow letters, digits, dot, dash, underscore. Reject anything else so a
17
14
  // hostile or buggy caller can't inject path traversal or shell metachars.
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, 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';
@@ -101,7 +101,10 @@ program.hook('preAction', (_thisCommand, actionCommand) => {
101
101
  emit('command.start', {
102
102
  module: parts[0],
103
103
  command: parts.join(' '),
104
- args: redactArgs(actionCommand.args),
104
+ // Commander exposes positional operands in actionCommand.args but omits
105
+ // parsed option values. Audit the real argv so sensitive flags are seen
106
+ // and redacted instead of silently bypassing the policy.
107
+ args: redactArgs(process.argv.slice(2, 22)),
105
108
  cwd: process.cwd(),
106
109
  });
107
110
  }
@@ -704,6 +707,7 @@ async function registerAllEagerCommands() {
704
707
  await reg(loadLogs);
705
708
  await reg(loadEvents);
706
709
  await reg(loadAudit);
710
+ await reg(loadFeed);
707
711
  await reg(loadSsh);
708
712
  registerJobsCronAliasCommand(program, 'jobs');
709
713
  registerJobsCronAliasCommand(program, 'cron');