@phnx-labs/agents-cli 1.14.2 → 1.14.4

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 (121) hide show
  1. package/README.md +17 -7
  2. package/dist/browser.d.ts +2 -0
  3. package/dist/browser.js +7 -0
  4. package/dist/commands/browser.d.ts +3 -0
  5. package/dist/commands/browser.js +392 -0
  6. package/dist/commands/daemon.js +1 -1
  7. package/dist/commands/doctor.d.ts +16 -9
  8. package/dist/commands/doctor.js +248 -12
  9. package/dist/commands/prune.js +9 -3
  10. package/dist/commands/refresh-rules.d.ts +15 -0
  11. package/dist/commands/{refresh-memory.js → refresh-rules.js} +14 -14
  12. package/dist/commands/routines.js +1 -1
  13. package/dist/commands/rules.js +100 -4
  14. package/dist/commands/secrets.js +198 -11
  15. package/dist/commands/sync.js +19 -0
  16. package/dist/commands/teams.js +184 -22
  17. package/dist/commands/trash.d.ts +10 -0
  18. package/dist/commands/trash.js +187 -0
  19. package/dist/commands/view.js +47 -14
  20. package/dist/index.js +62 -4
  21. package/dist/lib/agents.js +2 -2
  22. package/dist/lib/browser/cdp.d.ts +24 -0
  23. package/dist/lib/browser/cdp.js +94 -0
  24. package/dist/lib/browser/chrome.d.ts +16 -0
  25. package/dist/lib/browser/chrome.js +157 -0
  26. package/dist/lib/browser/drivers/local.d.ts +8 -0
  27. package/dist/lib/browser/drivers/local.js +22 -0
  28. package/dist/lib/browser/drivers/ssh.d.ts +9 -0
  29. package/dist/lib/browser/drivers/ssh.js +129 -0
  30. package/dist/lib/browser/index.d.ts +5 -0
  31. package/dist/lib/browser/index.js +5 -0
  32. package/dist/lib/browser/input.d.ts +6 -0
  33. package/dist/lib/browser/input.js +52 -0
  34. package/dist/lib/browser/ipc.d.ts +12 -0
  35. package/dist/lib/browser/ipc.js +223 -0
  36. package/dist/lib/browser/profiles.d.ts +11 -0
  37. package/dist/lib/browser/profiles.js +61 -0
  38. package/dist/lib/browser/refs.d.ts +21 -0
  39. package/dist/lib/browser/refs.js +88 -0
  40. package/dist/lib/browser/service.d.ts +45 -0
  41. package/dist/lib/browser/service.js +404 -0
  42. package/dist/lib/browser/types.d.ts +73 -0
  43. package/dist/lib/browser/types.js +7 -0
  44. package/dist/lib/cloud/codex.js +1 -1
  45. package/dist/lib/cloud/registry.js +2 -2
  46. package/dist/lib/cloud/rush.js +2 -2
  47. package/dist/lib/cloud/store.js +2 -2
  48. package/dist/lib/daemon.d.ts +1 -1
  49. package/dist/lib/daemon.js +47 -11
  50. package/dist/lib/diff-text.d.ts +25 -0
  51. package/dist/lib/diff-text.js +47 -0
  52. package/dist/lib/doctor-diff.d.ts +64 -0
  53. package/dist/lib/doctor-diff.js +497 -0
  54. package/dist/lib/git.js +3 -3
  55. package/dist/lib/hooks.d.ts +6 -0
  56. package/dist/lib/hooks.js +6 -1
  57. package/dist/lib/migrate.js +123 -0
  58. package/dist/lib/pty-client.js +3 -3
  59. package/dist/lib/pty-server.js +36 -7
  60. package/dist/lib/resources/commands.d.ts +46 -0
  61. package/dist/lib/resources/commands.js +208 -0
  62. package/dist/lib/resources/hooks.d.ts +12 -0
  63. package/dist/lib/resources/hooks.js +136 -0
  64. package/dist/lib/resources/index.d.ts +36 -0
  65. package/dist/lib/resources/index.js +69 -0
  66. package/dist/lib/resources/mcp.d.ts +34 -0
  67. package/dist/lib/resources/mcp.js +483 -0
  68. package/dist/lib/resources/permissions.d.ts +13 -0
  69. package/dist/lib/resources/permissions.js +184 -0
  70. package/dist/lib/resources/rules.d.ts +43 -0
  71. package/dist/lib/resources/rules.js +146 -0
  72. package/dist/lib/resources/skills.d.ts +37 -0
  73. package/dist/lib/resources/skills.js +238 -0
  74. package/dist/lib/resources/subagents.d.ts +46 -0
  75. package/dist/lib/resources/subagents.js +198 -0
  76. package/dist/lib/resources/types.d.ts +82 -0
  77. package/dist/lib/resources/types.js +8 -0
  78. package/dist/lib/resources.js +1 -1
  79. package/dist/lib/rotate.d.ts +8 -1
  80. package/dist/lib/rotate.js +17 -4
  81. package/dist/lib/rules/compile.d.ts +104 -0
  82. package/dist/lib/{memory-compile.js → rules/compile.js} +160 -21
  83. package/dist/lib/rules/compose.d.ts +78 -0
  84. package/dist/lib/rules/compose.js +170 -0
  85. package/dist/lib/{memory.d.ts → rules/rules.d.ts} +5 -5
  86. package/dist/lib/{memory.js → rules/rules.js} +10 -10
  87. package/dist/lib/secrets/AgentsKeychain.app/Contents/CodeResources +0 -0
  88. package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
  89. package/dist/lib/secrets/bundles.d.ts +61 -4
  90. package/dist/lib/secrets/bundles.js +222 -54
  91. package/dist/lib/secrets/index.d.ts +24 -5
  92. package/dist/lib/secrets/index.js +70 -41
  93. package/dist/lib/session/active.js +5 -5
  94. package/dist/lib/session/db.js +4 -4
  95. package/dist/lib/session/discover.js +2 -2
  96. package/dist/lib/session/render.js +21 -7
  97. package/dist/lib/shims.d.ts +28 -4
  98. package/dist/lib/shims.js +72 -14
  99. package/dist/lib/state.d.ts +22 -28
  100. package/dist/lib/state.js +83 -78
  101. package/dist/lib/sync-manifest.d.ts +2 -2
  102. package/dist/lib/sync-manifest.js +5 -5
  103. package/dist/lib/teams/agents.d.ts +4 -2
  104. package/dist/lib/teams/agents.js +11 -4
  105. package/dist/lib/teams/api.d.ts +1 -1
  106. package/dist/lib/teams/api.js +2 -2
  107. package/dist/lib/teams/index.d.ts +1 -0
  108. package/dist/lib/teams/index.js +1 -0
  109. package/dist/lib/teams/persistence.js +3 -3
  110. package/dist/lib/teams/registry.d.ts +12 -1
  111. package/dist/lib/teams/registry.js +12 -2
  112. package/dist/lib/teams/worktree.d.ts +30 -0
  113. package/dist/lib/teams/worktree.js +96 -0
  114. package/dist/lib/types.d.ts +12 -6
  115. package/dist/lib/types.js +3 -3
  116. package/dist/lib/versions.d.ts +32 -3
  117. package/dist/lib/versions.js +147 -119
  118. package/package.json +3 -2
  119. package/scripts/postinstall.js +29 -0
  120. package/dist/commands/refresh-memory.d.ts +0 -15
  121. 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
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
@@ -164,6 +259,12 @@ Examples:
164
259
  # Add a literal (non-sensitive) value
165
260
  agents secrets add prod LOG_LEVEL --value info
166
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
+
167
268
  # Import an entire .env file straight into keychain
168
269
  agents secrets import prod --from .env.prod
169
270
 
@@ -187,7 +288,7 @@ Examples:
187
288
  `);
188
289
  registerCommandGroups(cmd, [
189
290
  { title: 'Bundle commands', names: ['list', 'view', 'create', 'delete'] },
190
- { title: 'Secret commands', names: ['add', 'remove', 'import', 'export'] },
291
+ { title: 'Secret commands', names: ['add', 'rotate', 'remove', 'import', 'export'] },
191
292
  ]);
192
293
  cmd
193
294
  .command('list')
@@ -200,7 +301,7 @@ Examples:
200
301
  console.log(chalk.gray('Try: agents secrets create <name>'));
201
302
  return;
202
303
  }
203
- 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`));
204
305
  for (const b of bundles) {
205
306
  console.log(renderBundleRow(b));
206
307
  }
@@ -259,6 +360,9 @@ Examples:
259
360
  else {
260
361
  console.log(` ${chalk.cyan(e.key.padEnd(28))} ${kindLabel(e.kind).padEnd(18)} ${e.detail}`);
261
362
  }
363
+ const metaLine = renderMetaLine(bundle.meta?.[e.key], reveal);
364
+ if (metaLine)
365
+ console.log(metaLine);
262
366
  }
263
367
  }
264
368
  catch (err) {
@@ -304,29 +408,45 @@ Examples:
304
408
  cmd
305
409
  .command('add [bundle] [key]')
306
410
  .description('Add a variable to a bundle. Defaults to keychain-backed; pass --value for literal, --env/--file/--exec for refs.')
307
- .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)')
308
412
  .option('--value-stdin', 'Read the value from stdin (stored in keychain unless combined with --value)')
309
413
  .option('--env <VAR>', 'Store as an env: ref that reads from the parent process.env at run time')
310
414
  .option('--file <path>', 'Store as a file: ref that reads from a file at run time')
311
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).')
312
419
  .action(async (bundleName, key, opts) => {
313
420
  try {
314
421
  const resolvedBundleName = bundleName ?? (await pickBundleName('add to'));
315
422
  const bundle = readBundle(resolvedBundleName);
316
423
  const resolvedKey = key ?? (await promptKeyName(resolvedBundleName));
317
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
+ }
318
428
  const sources = [opts.value !== undefined, Boolean(opts.env), Boolean(opts.file), Boolean(opts.exec)].filter(Boolean).length;
319
429
  if (sources > 1) {
320
430
  throw new Error('Pick one of: --value, --env, --file, --exec.');
321
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
+ };
322
440
  if (opts.env) {
323
441
  bundle.vars[resolvedKey] = `env:${opts.env}`;
442
+ applyMeta();
324
443
  writeBundle(bundle);
325
444
  console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} -> env:${opts.env}`));
326
445
  return;
327
446
  }
328
447
  if (opts.file) {
329
448
  bundle.vars[resolvedKey] = `file:${opts.file}`;
449
+ applyMeta();
330
450
  writeBundle(bundle);
331
451
  console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} -> file:${opts.file}`));
332
452
  return;
@@ -336,12 +456,14 @@ Examples:
336
456
  throw new Error(`Bundle '${resolvedBundleName}' does not allow exec refs. Re-create with --allow-exec.`);
337
457
  }
338
458
  bundle.vars[resolvedKey] = `exec:${opts.exec}`;
459
+ applyMeta();
339
460
  writeBundle(bundle);
340
461
  console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} -> exec:${opts.exec}`));
341
462
  return;
342
463
  }
343
464
  if (opts.value !== undefined) {
344
465
  bundle.vars[resolvedKey] = { value: opts.value };
466
+ applyMeta();
345
467
  writeBundle(bundle);
346
468
  console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} = <literal>`));
347
469
  return;
@@ -359,6 +481,7 @@ Examples:
359
481
  const item = secretsKeychainItem(resolvedBundleName, resolvedKey);
360
482
  setKeychainToken(item, secretValue, bundle.icloud_sync);
361
483
  bundle.vars[resolvedKey] = keychainRef(resolvedKey);
484
+ applyMeta();
362
485
  writeBundle(bundle);
363
486
  const where = bundle.icloud_sync ? 'iCloud Keychain' : 'keychain';
364
487
  console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} stored in ${where} (${item}).`));
@@ -370,10 +493,71 @@ Examples:
370
493
  process.exit(1);
371
494
  }
372
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
+ });
373
557
  cmd
374
558
  .command('remove [bundle] [key]')
375
559
  .description('Remove a key from the bundle. Purges the keychain item if the ref was keychain:. Use --keep-secret to retain it.')
376
- .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')
377
561
  .action(async (bundleName, key, opts) => {
378
562
  try {
379
563
  const resolvedBundleName = bundleName ?? (await pickBundleName('remove from'));
@@ -406,7 +590,7 @@ Examples:
406
590
  cmd
407
591
  .command('delete [name]')
408
592
  .description('Delete a bundle and purge all its keychain items (use --keep-secrets to retain them).')
409
- .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')
410
594
  .option('-y, --yes', 'Skip the confirmation prompt')
411
595
  .action(async (name, opts) => {
412
596
  try {
@@ -454,7 +638,7 @@ Examples:
454
638
  .command('import [bundle]')
455
639
  .description('Import keys from a .env file into a bundle. By default every key is stored in keychain.')
456
640
  .requiredOption('--from <path>', 'Path to a .env file')
457
- .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)')
458
642
  .option('--force', 'Overwrite an existing key in the bundle')
459
643
  .action(async (bundleName, opts) => {
460
644
  try {
@@ -495,7 +679,7 @@ Examples:
495
679
  .option('--plaintext', 'Acknowledge that the resolved values will be printed in the clear')
496
680
  .action(async (bundleName, opts) => {
497
681
  try {
498
- const { resolveBundleEnv } = await import('../lib/secrets/bundles.js');
682
+ const { resolveBundleEnv, bundleToEnvPrefix, isReservedEnvName } = await import('../lib/secrets/bundles.js');
499
683
  const resolvedBundleName = bundleName ?? (await pickBundleName('export'));
500
684
  const bundle = readBundle(resolvedBundleName);
501
685
  if (isInteractiveTerminal() && !opts.plaintext) {
@@ -503,9 +687,12 @@ Examples:
503
687
  process.exit(1);
504
688
  }
505
689
  const env = resolveBundleEnv(bundle);
690
+ const prefix = bundleToEnvPrefix(resolvedBundleName);
506
691
  for (const [k, v] of Object.entries(env)) {
507
- const escaped = v.replace(/'/g, `'\\''`);
508
- 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`);
509
696
  }
510
697
  }
511
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
  }