@phnx-labs/agents-cli 1.14.1 → 1.14.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +31 -3
  2. package/dist/commands/browser.d.ts +2 -0
  3. package/dist/commands/browser.js +388 -0
  4. package/dist/commands/daemon.js +1 -1
  5. package/dist/commands/doctor.d.ts +16 -9
  6. package/dist/commands/doctor.js +248 -12
  7. package/dist/commands/exec.js +17 -17
  8. package/dist/commands/prune.js +9 -3
  9. package/dist/commands/refresh-rules.d.ts +15 -0
  10. package/dist/commands/{refresh-memory.js → refresh-rules.js} +14 -14
  11. package/dist/commands/routines.js +1 -1
  12. package/dist/commands/rules.js +100 -4
  13. package/dist/commands/secrets.js +206 -12
  14. package/dist/commands/sync.js +19 -0
  15. package/dist/commands/teams.js +162 -22
  16. package/dist/commands/trash.d.ts +10 -0
  17. package/dist/commands/trash.js +187 -0
  18. package/dist/commands/view.js +46 -13
  19. package/dist/index.js +62 -4
  20. package/dist/lib/agents.js +2 -2
  21. package/dist/lib/browser/cdp.d.ts +24 -0
  22. package/dist/lib/browser/cdp.js +94 -0
  23. package/dist/lib/browser/chrome.d.ts +16 -0
  24. package/dist/lib/browser/chrome.js +157 -0
  25. package/dist/lib/browser/drivers/local.d.ts +8 -0
  26. package/dist/lib/browser/drivers/local.js +22 -0
  27. package/dist/lib/browser/drivers/ssh.d.ts +9 -0
  28. package/dist/lib/browser/drivers/ssh.js +129 -0
  29. package/dist/lib/browser/index.d.ts +5 -0
  30. package/dist/lib/browser/index.js +5 -0
  31. package/dist/lib/browser/input.d.ts +6 -0
  32. package/dist/lib/browser/input.js +52 -0
  33. package/dist/lib/browser/ipc.d.ts +12 -0
  34. package/dist/lib/browser/ipc.js +223 -0
  35. package/dist/lib/browser/profiles.d.ts +11 -0
  36. package/dist/lib/browser/profiles.js +61 -0
  37. package/dist/lib/browser/refs.d.ts +21 -0
  38. package/dist/lib/browser/refs.js +88 -0
  39. package/dist/lib/browser/service.d.ts +45 -0
  40. package/dist/lib/browser/service.js +404 -0
  41. package/dist/lib/browser/types.d.ts +73 -0
  42. package/dist/lib/browser/types.js +7 -0
  43. package/dist/lib/cloud/codex.js +1 -1
  44. package/dist/lib/cloud/registry.js +2 -2
  45. package/dist/lib/cloud/rush.js +2 -2
  46. package/dist/lib/cloud/store.js +2 -2
  47. package/dist/lib/daemon.d.ts +1 -1
  48. package/dist/lib/daemon.js +47 -11
  49. package/dist/lib/diff-text.d.ts +25 -0
  50. package/dist/lib/diff-text.js +47 -0
  51. package/dist/lib/doctor-diff.d.ts +64 -0
  52. package/dist/lib/doctor-diff.js +497 -0
  53. package/dist/lib/git.js +3 -3
  54. package/dist/lib/hooks.d.ts +6 -0
  55. package/dist/lib/hooks.js +6 -1
  56. package/dist/lib/migrate.js +77 -0
  57. package/dist/lib/pty-client.js +3 -3
  58. package/dist/lib/pty-server.js +36 -7
  59. package/dist/lib/resources.js +1 -1
  60. package/dist/lib/rotate.d.ts +43 -26
  61. package/dist/lib/rotate.js +99 -44
  62. package/dist/lib/rules/compile.d.ts +104 -0
  63. package/dist/lib/{memory-compile.js → rules/compile.js} +160 -21
  64. package/dist/lib/rules/compose.d.ts +78 -0
  65. package/dist/lib/rules/compose.js +170 -0
  66. package/dist/lib/{memory.d.ts → rules/rules.d.ts} +5 -5
  67. package/dist/lib/{memory.js → rules/rules.js} +10 -10
  68. package/dist/lib/secrets/AgentsKeychain.app/Contents/CodeResources +0 -0
  69. package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
  70. package/dist/lib/secrets/bundles.d.ts +61 -4
  71. package/dist/lib/secrets/bundles.js +222 -54
  72. package/dist/lib/secrets/index.d.ts +24 -5
  73. package/dist/lib/secrets/index.js +70 -41
  74. package/dist/lib/session/active.js +5 -5
  75. package/dist/lib/session/db.js +4 -4
  76. package/dist/lib/session/discover.js +2 -2
  77. package/dist/lib/session/render.js +21 -7
  78. package/dist/lib/shims.d.ts +28 -4
  79. package/dist/lib/shims.js +72 -14
  80. package/dist/lib/state.d.ts +22 -28
  81. package/dist/lib/state.js +83 -76
  82. package/dist/lib/sync-manifest.d.ts +2 -2
  83. package/dist/lib/sync-manifest.js +5 -5
  84. package/dist/lib/teams/agents.d.ts +4 -2
  85. package/dist/lib/teams/agents.js +11 -4
  86. package/dist/lib/teams/api.d.ts +1 -1
  87. package/dist/lib/teams/api.js +2 -2
  88. package/dist/lib/teams/index.d.ts +1 -0
  89. package/dist/lib/teams/index.js +1 -0
  90. package/dist/lib/teams/persistence.js +3 -3
  91. package/dist/lib/teams/registry.d.ts +8 -1
  92. package/dist/lib/teams/registry.js +8 -2
  93. package/dist/lib/teams/worktree.d.ts +30 -0
  94. package/dist/lib/teams/worktree.js +96 -0
  95. package/dist/lib/types.d.ts +13 -7
  96. package/dist/lib/types.js +3 -3
  97. package/dist/lib/versions.d.ts +30 -2
  98. package/dist/lib/versions.js +127 -105
  99. package/package.json +1 -1
  100. package/scripts/postinstall.js +29 -0
  101. package/dist/commands/refresh-memory.d.ts +0 -15
  102. package/dist/lib/memory-compile.d.ts +0 -66
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import chalk from 'chalk';
9
9
  import * as fs from 'fs';
10
- import { bundleExists, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, parseDotenv, readBundle, validateBundleName, validateEnvKey, writeBundle, } from '../lib/secrets/bundles.js';
10
+ import { bundleExists, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readBundle, rotateBundleSecret, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
11
11
  import { deleteKeychainToken, getKeychainToken, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
12
12
  import { registerCommandGroups } from '../lib/help.js';
13
13
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
@@ -114,7 +114,11 @@ function renderBundleRow(b) {
114
114
  const entries = describeBundle(b);
115
115
  const keys = entries.length;
116
116
  const sensitive = entries.filter((e) => e.kind === 'keychain').length;
117
- return `${chalk.cyan(b.name.padEnd(20))} ${String(keys).padEnd(6)} ${chalk.yellow(String(sensitive).padEnd(10))} ${chalk.gray(b.description || '')}`;
117
+ const expiring = countExpiringSoon(b.meta);
118
+ const expiringCol = expiring > 0
119
+ ? chalk.yellow(String(expiring).padEnd(10))
120
+ : ''.padEnd(10);
121
+ return `${chalk.cyan(b.name.padEnd(20))} ${String(keys).padEnd(6)} ${chalk.yellow(String(sensitive).padEnd(10))} ${expiringCol} ${chalk.gray(b.description || '')}`;
118
122
  }
119
123
  /** Colorize a variable source kind (literal, keychain, env, file, exec). */
120
124
  function kindLabel(kind) {
@@ -135,11 +139,102 @@ function redact(value, reveal) {
135
139
  return '';
136
140
  return '*'.repeat(Math.min(value.length, 8));
137
141
  }
142
+ /**
143
+ * Build a VarMeta patch from CLI flags. Validates each provided field. Returns
144
+ * undefined if no meta flag was passed (so callers know to skip meta updates).
145
+ *
146
+ * `--note -` reads the note from stdin so users can pass long/multi-line notes
147
+ * without shell-escaping. It's mutually exclusive with `--value-stdin`; both
148
+ * trying to consume stdin would race and silently corrupt one or the other.
149
+ */
150
+ function buildMetaPatch(raw) {
151
+ if (raw.type === undefined && raw.expires === undefined && raw.note === undefined) {
152
+ return undefined;
153
+ }
154
+ const patch = {};
155
+ if (raw.type !== undefined) {
156
+ validateSecretType(raw.type);
157
+ patch.type = raw.type;
158
+ }
159
+ if (raw.expires !== undefined) {
160
+ validateExpiresFutureDated(raw.expires);
161
+ patch.expires = raw.expires;
162
+ }
163
+ if (raw.note !== undefined) {
164
+ if (raw.note === '-') {
165
+ if (raw.valueStdin) {
166
+ throw new Error('--note - and --value-stdin both want stdin; only one can read it.');
167
+ }
168
+ const fromStdin = readStdinSync();
169
+ if (!fromStdin)
170
+ throw new Error('No note received on stdin.');
171
+ patch.note = fromStdin;
172
+ }
173
+ else {
174
+ patch.note = raw.note;
175
+ }
176
+ }
177
+ return patch;
178
+ }
179
+ /** Whole days from now until midnight-UTC of the given ISO date. Negative if past. */
180
+ function daysUntil(iso) {
181
+ const target = new Date(iso + 'T23:59:59Z').getTime();
182
+ const now = Date.now();
183
+ return Math.floor((target - now) / (24 * 60 * 60 * 1000));
184
+ }
185
+ /** Render the meta line under a var, indented. Returns empty string if nothing to show. */
186
+ function renderMetaLine(meta, reveal) {
187
+ if (!meta)
188
+ return '';
189
+ const parts = [];
190
+ if (meta.type)
191
+ parts.push(`type: ${meta.type}`);
192
+ if (meta.expires) {
193
+ const days = daysUntil(meta.expires);
194
+ const tail = `(in ${days} days)`;
195
+ let colored;
196
+ if (days < 0) {
197
+ colored = chalk.red(`expires: ${meta.expires} ${tail}`);
198
+ }
199
+ else if (days < 30) {
200
+ colored = chalk.yellow(`expires: ${meta.expires} ${tail}`);
201
+ }
202
+ else {
203
+ colored = chalk.gray(`expires: ${meta.expires} ${tail}`);
204
+ }
205
+ parts.push(colored);
206
+ }
207
+ if (meta.note) {
208
+ let note = meta.note;
209
+ if (!reveal && note.length > 80) {
210
+ note = note.slice(0, 79) + '\u2026';
211
+ }
212
+ parts.push(`note: ${note}`);
213
+ }
214
+ if (parts.length === 0)
215
+ return '';
216
+ return ` ${parts.join(' ')}`;
217
+ }
218
+ /** Count entries in `meta` whose `expires` falls in the next 30 days. */
219
+ function countExpiringSoon(meta) {
220
+ if (!meta)
221
+ return 0;
222
+ let n = 0;
223
+ for (const m of Object.values(meta)) {
224
+ if (!m.expires)
225
+ continue;
226
+ const d = daysUntil(m.expires);
227
+ if (d >= 0 && d < 30)
228
+ n++;
229
+ }
230
+ return n;
231
+ }
138
232
  /** Register the `agents secrets` command tree. */
139
233
  export function registerSecretsCommands(program) {
140
234
  const cmd = program
141
235
  .command('secrets')
142
- .description('Named bundles of env variables backed by macOS Keychain. Inject into agents via `agents run --secrets <name>`.')
236
+ .description('Named bundles of env variables backed by macOS Keychain (with optional iCloud sync). Inject into agents via `agents run --secrets <name>`.')
237
+ .hook('preAction', () => { migrateLegacyBundles(); })
143
238
  .addHelpText('after', `
144
239
  Workflow:
145
240
  Bundles are containers; secrets are the variables inside them. Create a
@@ -147,16 +242,29 @@ Workflow:
147
242
  run with --secrets <name>. Keychain-backed values never touch disk in
148
243
  plaintext.
149
244
 
245
+ Pass --icloud-sync at create time to store values in the iCloud-synced
246
+ keychain so they appear automatically on your other Macs (same iCloud
247
+ account, iCloud Keychain enabled). Without the flag, values are device-local.
248
+
150
249
  Examples:
151
250
  # Create a bundle for production credentials
152
251
  agents secrets create prod --description "Production keys for the api stack"
153
252
 
253
+ # Create a bundle that syncs to your other Macs via iCloud Keychain
254
+ agents secrets create npm-tokens --icloud-sync
255
+
154
256
  # Add a keychain-backed secret (prompts for the value)
155
257
  agents secrets add prod STRIPE_API_KEY
156
258
 
157
259
  # Add a literal (non-sensitive) value
158
260
  agents secrets add prod LOG_LEVEL --value info
159
261
 
262
+ # Add with metadata
263
+ agents secrets add prod STRIPE_API_KEY --type api-key --expires 2027-01-15 --note "Live key, owner: payments-team"
264
+
265
+ # Rotate a key (replaces the value, preserves metadata unless overridden)
266
+ agents secrets rotate prod STRIPE_API_KEY
267
+
160
268
  # Import an entire .env file straight into keychain
161
269
  agents secrets import prod --from .env.prod
162
270
 
@@ -180,7 +288,7 @@ Examples:
180
288
  `);
181
289
  registerCommandGroups(cmd, [
182
290
  { title: 'Bundle commands', names: ['list', 'view', 'create', 'delete'] },
183
- { title: 'Secret commands', names: ['add', 'remove', 'import', 'export'] },
291
+ { title: 'Secret commands', names: ['add', 'rotate', 'remove', 'import', 'export'] },
184
292
  ]);
185
293
  cmd
186
294
  .command('list')
@@ -193,7 +301,7 @@ Examples:
193
301
  console.log(chalk.gray('Try: agents secrets create <name>'));
194
302
  return;
195
303
  }
196
- console.log(chalk.bold(`${'NAME'.padEnd(20)} ${'KEYS'.padEnd(6)} ${'SENSITIVE'.padEnd(10)} DESCRIPTION`));
304
+ console.log(chalk.bold(`${'NAME'.padEnd(20)} ${'KEYS'.padEnd(6)} ${'SENSITIVE'.padEnd(10)} ${'EXPIRING'.padEnd(10)} DESCRIPTION`));
197
305
  for (const b of bundles) {
198
306
  console.log(renderBundleRow(b));
199
307
  }
@@ -252,6 +360,9 @@ Examples:
252
360
  else {
253
361
  console.log(` ${chalk.cyan(e.key.padEnd(28))} ${kindLabel(e.kind).padEnd(18)} ${e.detail}`);
254
362
  }
363
+ const metaLine = renderMetaLine(bundle.meta?.[e.key], reveal);
364
+ if (metaLine)
365
+ console.log(metaLine);
255
366
  }
256
367
  }
257
368
  catch (err) {
@@ -297,29 +408,45 @@ Examples:
297
408
  cmd
298
409
  .command('add [bundle] [key]')
299
410
  .description('Add a variable to a bundle. Defaults to keychain-backed; pass --value for literal, --env/--file/--exec for refs.')
300
- .option('--value <v>', 'Store as a plaintext literal in the YAML (non-sensitive values only)')
411
+ .option('--value <v>', 'Store as a plaintext literal in the bundle (non-sensitive values only)')
301
412
  .option('--value-stdin', 'Read the value from stdin (stored in keychain unless combined with --value)')
302
413
  .option('--env <VAR>', 'Store as an env: ref that reads from the parent process.env at run time')
303
414
  .option('--file <path>', 'Store as a file: ref that reads from a file at run time')
304
415
  .option('--exec <cmd>', 'Store as an exec: ref that runs a command at run time (requires allow_exec)')
416
+ .option('--type <kind>', 'Tag this secret with a type (api-key, token, password, url, database-url, ssh-key, certificate, webhook, note)')
417
+ .option('--expires <YYYY-MM-DD>', 'Mark when this secret expires (must be future-dated)')
418
+ .option('--note <text>', 'Attach a freeform note. Pass `-` to read from stdin (mutually exclusive with --value-stdin).')
305
419
  .action(async (bundleName, key, opts) => {
306
420
  try {
307
421
  const resolvedBundleName = bundleName ?? (await pickBundleName('add to'));
308
422
  const bundle = readBundle(resolvedBundleName);
309
423
  const resolvedKey = key ?? (await promptKeyName(resolvedBundleName));
310
424
  validateEnvKey(resolvedKey);
425
+ if (resolvedKey in bundle.vars) {
426
+ throw new Error(`Key '${resolvedKey}' already exists in bundle '${resolvedBundleName}'. Use 'agents secrets rotate' to refresh it.`);
427
+ }
311
428
  const sources = [opts.value !== undefined, Boolean(opts.env), Boolean(opts.file), Boolean(opts.exec)].filter(Boolean).length;
312
429
  if (sources > 1) {
313
430
  throw new Error('Pick one of: --value, --env, --file, --exec.');
314
431
  }
432
+ const metaPatch = buildMetaPatch(opts);
433
+ const applyMeta = () => {
434
+ if (!metaPatch)
435
+ return;
436
+ if (!bundle.meta)
437
+ bundle.meta = {};
438
+ bundle.meta[resolvedKey] = { ...(bundle.meta[resolvedKey] ?? {}), ...metaPatch };
439
+ };
315
440
  if (opts.env) {
316
441
  bundle.vars[resolvedKey] = `env:${opts.env}`;
442
+ applyMeta();
317
443
  writeBundle(bundle);
318
444
  console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} -> env:${opts.env}`));
319
445
  return;
320
446
  }
321
447
  if (opts.file) {
322
448
  bundle.vars[resolvedKey] = `file:${opts.file}`;
449
+ applyMeta();
323
450
  writeBundle(bundle);
324
451
  console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} -> file:${opts.file}`));
325
452
  return;
@@ -329,12 +456,14 @@ Examples:
329
456
  throw new Error(`Bundle '${resolvedBundleName}' does not allow exec refs. Re-create with --allow-exec.`);
330
457
  }
331
458
  bundle.vars[resolvedKey] = `exec:${opts.exec}`;
459
+ applyMeta();
332
460
  writeBundle(bundle);
333
461
  console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} -> exec:${opts.exec}`));
334
462
  return;
335
463
  }
336
464
  if (opts.value !== undefined) {
337
465
  bundle.vars[resolvedKey] = { value: opts.value };
466
+ applyMeta();
338
467
  writeBundle(bundle);
339
468
  console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} = <literal>`));
340
469
  return;
@@ -352,6 +481,7 @@ Examples:
352
481
  const item = secretsKeychainItem(resolvedBundleName, resolvedKey);
353
482
  setKeychainToken(item, secretValue, bundle.icloud_sync);
354
483
  bundle.vars[resolvedKey] = keychainRef(resolvedKey);
484
+ applyMeta();
355
485
  writeBundle(bundle);
356
486
  const where = bundle.icloud_sync ? 'iCloud Keychain' : 'keychain';
357
487
  console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} stored in ${where} (${item}).`));
@@ -363,10 +493,71 @@ Examples:
363
493
  process.exit(1);
364
494
  }
365
495
  });
496
+ cmd
497
+ .command('rotate [bundle] [key]')
498
+ .description('Rotate an existing keychain-backed secret (replaces the value, preserves metadata unless overridden).')
499
+ .option('--value <v>', 'New value (non-secret cases). Prompts interactively if omitted.')
500
+ .option('--value-stdin', 'Read the new value from stdin (stored in keychain unless combined with --value)')
501
+ .option('--type <kind>', 'Update the type metadata (api-key, token, password, url, database-url, ssh-key, certificate, webhook, note)')
502
+ .option('--expires <YYYY-MM-DD>', 'Update the expiration date (must be future-dated)')
503
+ .option('--note <text>', 'Update the note. Pass `-` to read from stdin (mutually exclusive with --value-stdin).')
504
+ .option('--clear-meta', 'Wipe all metadata for this key while rotating')
505
+ .addHelpText('after', `
506
+ Examples:
507
+ # Rotate the value, preserve all metadata
508
+ agents secrets rotate prod STRIPE_API_KEY
509
+
510
+ # Rotate with a metadata refresh
511
+ agents secrets rotate prod STRIPE_API_KEY --type api-key --expires 2027-01-15 --note "rotated after employee offboarding"
512
+ `)
513
+ .action(async (bundleName, key, opts) => {
514
+ try {
515
+ const resolvedBundleName = bundleName ?? (await pickBundleName('rotate in'));
516
+ const bundle = readBundle(resolvedBundleName);
517
+ const resolvedKey = key ?? (await pickKey(bundle, 'rotate'));
518
+ if (!(resolvedKey in bundle.vars)) {
519
+ throw new Error(`Key '${resolvedKey}' not in bundle '${resolvedBundleName}'. Use 'agents secrets add' to add a new key.`);
520
+ }
521
+ const raw = bundle.vars[resolvedKey];
522
+ if (typeof raw !== 'string' || !raw.startsWith('keychain:')) {
523
+ throw new Error(`Key '${resolvedKey}' in bundle '${resolvedBundleName}' is not keychain-backed; cannot rotate.`);
524
+ }
525
+ const metaPatch = buildMetaPatch(opts);
526
+ if (opts.clearMeta && metaPatch) {
527
+ throw new Error('--clear-meta and --type/--expires/--note are mutually exclusive.');
528
+ }
529
+ // Resolve the new value: --value > --value-stdin > prompt.
530
+ let newValue;
531
+ if (opts.value !== undefined) {
532
+ newValue = opts.value;
533
+ }
534
+ else if (opts.valueStdin) {
535
+ newValue = readStdinSync();
536
+ if (!newValue)
537
+ throw new Error('No value received on stdin.');
538
+ }
539
+ else {
540
+ newValue = await promptForSecret(`Enter new value for ${resolvedBundleName}.${resolvedKey}`);
541
+ }
542
+ rotateBundleSecret(bundle, resolvedKey, {
543
+ newValue,
544
+ clearMeta: opts.clearMeta,
545
+ meta: metaPatch,
546
+ });
547
+ const where = bundle.icloud_sync ? 'iCloud Keychain' : 'keychain';
548
+ console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} rotated in ${where}.`));
549
+ }
550
+ catch (err) {
551
+ if (isPromptCancelled(err))
552
+ return;
553
+ console.error(chalk.red(err.message));
554
+ process.exit(1);
555
+ }
556
+ });
366
557
  cmd
367
558
  .command('remove [bundle] [key]')
368
559
  .description('Remove a key from the bundle. Purges the keychain item if the ref was keychain:. Use --keep-secret to retain it.')
369
- .option('--keep-secret', 'Leave the keychain item in place after removing the YAML ref')
560
+ .option('--keep-secret', 'Leave the keychain item in place after removing the ref from the bundle')
370
561
  .action(async (bundleName, key, opts) => {
371
562
  try {
372
563
  const resolvedBundleName = bundleName ?? (await pickBundleName('remove from'));
@@ -399,7 +590,7 @@ Examples:
399
590
  cmd
400
591
  .command('delete [name]')
401
592
  .description('Delete a bundle and purge all its keychain items (use --keep-secrets to retain them).')
402
- .option('--keep-secrets', 'Leave keychain items in place after deleting the bundle file')
593
+ .option('--keep-secrets', 'Leave keychain items in place after deleting the bundle')
403
594
  .option('-y, --yes', 'Skip the confirmation prompt')
404
595
  .action(async (name, opts) => {
405
596
  try {
@@ -447,7 +638,7 @@ Examples:
447
638
  .command('import [bundle]')
448
639
  .description('Import keys from a .env file into a bundle. By default every key is stored in keychain.')
449
640
  .requiredOption('--from <path>', 'Path to a .env file')
450
- .option('--all-plaintext', 'Store every imported value as a YAML literal (skip keychain prompts)')
641
+ .option('--all-plaintext', 'Store every imported value as a literal in the bundle metadata (skip keychain item creation)')
451
642
  .option('--force', 'Overwrite an existing key in the bundle')
452
643
  .action(async (bundleName, opts) => {
453
644
  try {
@@ -488,7 +679,7 @@ Examples:
488
679
  .option('--plaintext', 'Acknowledge that the resolved values will be printed in the clear')
489
680
  .action(async (bundleName, opts) => {
490
681
  try {
491
- const { resolveBundleEnv } = await import('../lib/secrets/bundles.js');
682
+ const { resolveBundleEnv, bundleToEnvPrefix, isReservedEnvName } = await import('../lib/secrets/bundles.js');
492
683
  const resolvedBundleName = bundleName ?? (await pickBundleName('export'));
493
684
  const bundle = readBundle(resolvedBundleName);
494
685
  if (isInteractiveTerminal() && !opts.plaintext) {
@@ -496,9 +687,12 @@ Examples:
496
687
  process.exit(1);
497
688
  }
498
689
  const env = resolveBundleEnv(bundle);
690
+ const prefix = bundleToEnvPrefix(resolvedBundleName);
499
691
  for (const [k, v] of Object.entries(env)) {
500
- const escaped = v.replace(/'/g, `'\\''`);
501
- process.stdout.write(`export ${k}='${escaped}'\n`);
692
+ const exportKey = isReservedEnvName(k) ? `${prefix}_${k}` : k;
693
+ const needsQuotes = /[\s$`"'\\|&;<>(){}[\]!#~]/.test(v);
694
+ const output = needsQuotes ? `'${v.replace(/'/g, `'\\''`)}'` : v;
695
+ process.stdout.write(`export ${exportKey}=${output}\n`);
502
696
  }
503
697
  }
504
698
  catch (err) {
@@ -5,9 +5,11 @@
5
5
  * synchronize resources (commands, skills, hooks, memory, MCP, etc.)
6
6
  * into a specific agent version home before launch.
7
7
  */
8
+ import * as path from 'path';
8
9
  import chalk from 'chalk';
9
10
  import { AGENTS } from '../lib/agents.js';
10
11
  import { isVersionInstalled, syncResourcesToVersion } from '../lib/versions.js';
12
+ import { compileRulesForProject } from '../lib/rules/compile.js';
11
13
  /** Register the hidden `agents sync` command. */
12
14
  export function registerSyncCommand(program) {
13
15
  program
@@ -39,6 +41,14 @@ export function registerSyncCommand(program) {
39
41
  return;
40
42
  }
41
43
  const result = syncResourcesToVersion(agentId, version, undefined, { projectDir, cwd });
44
+ // Compile project-scope rules into the workspace itself so each agent's
45
+ // native loader picks up cwd/<INSTRUCTIONS_FILE>. projectDir is the
46
+ // .agents/ directory; the workspace root is its parent.
47
+ let projectCompile = null;
48
+ if (projectDir) {
49
+ const projectRoot = path.dirname(projectDir);
50
+ projectCompile = compileRulesForProject(projectRoot);
51
+ }
42
52
  if (quiet) {
43
53
  return;
44
54
  }
@@ -65,5 +75,14 @@ export function registerSyncCommand(program) {
65
75
  else {
66
76
  console.log(chalk.gray('No resources to sync'));
67
77
  }
78
+ if (projectCompile?.compiled) {
79
+ const linkInfo = projectCompile.symlinks.length > 0
80
+ ? ` (+ ${projectCompile.symlinks.join(', ')})`
81
+ : '';
82
+ console.log(chalk.gray(`Compiled project rules → ${projectCompile.agentsPath}${linkInfo}`));
83
+ }
84
+ if (projectCompile && projectCompile.skippedClobber.length > 0) {
85
+ console.log(chalk.yellow(`Skipped (user-authored, not overwritten): ${projectCompile.skippedClobber.join(', ')}`));
86
+ }
68
87
  });
69
88
  }