@phnx-labs/agents-cli 1.20.21 → 1.20.22

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ **Secrets prompt policy: human-readable `always` / `daily`, and `secrets list` now shows it**
6
+
7
+ - Renamed the secrets-agent `tier` to a **prompt policy** with plain-language names: `biometry` → **`always`** (ask every time), `session` → **`daily`** (ask once, then held ~24h until screen-lock / sleep / logout). The old name `session` was misleading — it never meant "once per login session" — and collided with the half-dozen other "session" concepts in the CLI (`agents sessions`, sessions-sync, pty/browser sessions). Set it with `agents secrets policy <bundle> [always|daily]`.
8
+ - **Disclosure fixed.** `agents secrets list` now has a `POLICY` column — previously there was no way to tell which bundles would Touch-ID-prompt you. `daily` bundles currently held by the agent show `daily · Nh left`. `agents secrets view` and `create` now always state the policy (before, only the quiet tier was shown; the noisy default printed nothing).
9
+ - **Back-compat:** the policy still persists under the legacy `tier`/`session` token, so bundles stay readable across mixed CLI versions on synced machines. `agents secrets tier`, `--tier`, and the `biometry`/`session` values keep working as aliases.
10
+ - A third **`never`** policy (silent, no biometry ACL) is tracked for later in #421.
11
+
12
+ **Self-healing: long-running processes reload onto new code after an upgrade**
13
+
14
+ - Root cause behind a class of "stale behavior" bugs: a routines daemon or secrets-agent broker keeps running **pre-upgrade code** for days. An in-place `npm i -g` swaps the files but not the running processes, so fixes (keychain read-memoization, the broker fast-path, etc.) silently never take effect — the daemon kept popping Touch ID from the keychain because it predated the fix.
15
+ - **Heal-on-upgrade:** `postinstall` now bounces the routines daemon and kickstarts the persistent secrets-agent broker onto the just-installed code — the one moment we know the code changed. Best-effort, non-fatal, skipped in CI / with `AGENTS_NO_HEAL=1`.
16
+ - **Broker version-skew self-heal:** the broker's `ping` reports the version of the code it's running; `ensureAgentRunning` (the unlock / auto-cache path, never per-read) restarts a broker found running stale code, and a persistent broker self-exits on detecting an in-place upgrade so launchd relaunches it fresh. New `getCliVersionFresh()` re-reads `package.json` to detect the swap.
17
+ - No hot-path cost: all checks live on existing control-plane paths (postinstall, the broker sweep, `ensureAgentRunning`), never on a per-secret-read. macOS only. Complements #412 (daemon session-sync memoization) by ensuring the daemon actually *runs* that code.
18
+
5
19
  **`agents secrets start`: persistent secrets-agent service (fixes the broker under heavy load)**
6
20
 
7
21
  - On a heavily-loaded machine (many concurrent agents, high load average) the on-demand broker — a full CLI cold-start — couldn't get scheduled enough CPU to finish booting and bind its socket, so `unlock`/auto-cache silently failed and reads kept prompting. New `agents secrets start` installs the broker as a **launchd user service** (`RunAtLoad` + `KeepAlive`, `ProcessType: Interactive` for foreground scheduling priority): it starts once and stays up for the whole login session, so every read just connects — the cold start happens once (and launchd retries until it wins), never per read. `agents secrets stop` removes it; `agents secrets status` shows whether it's installed.
@@ -47,12 +47,16 @@ function isJsonMode(opts) {
47
47
  export function registerCloudCommands(program) {
48
48
  const cloud = program
49
49
  .command('cloud', { hidden: true })
50
- .description('Dispatch and manage cloud agent tasks across providers (Rush Cloud, Codex Cloud, Factory).')
50
+ .description('Dispatch and manage cloud agent tasks across providers (Rush, Codex, Factory, Antigravity).')
51
51
  .addHelpText('after', `
52
+ Each agent runs in its own cloud. Pass --agent and the provider is auto-selected
53
+ (claude→rush, codex→codex, droid→factory, antigravity→antigravity); --provider overrides.
54
+
52
55
  Providers:
53
- rush Rush Cloud — dispatches against a GitHub repo + branch
54
- codex Codex Cloud — runs in a pre-built Codex environment
55
- factory Factory podsDroid + computer-use targets
56
+ rush Rush Cloud — Claude against a GitHub repo + branch → PR
57
+ codex Codex Cloud — runs in a pre-built Codex environment (--env)
58
+ factory Factory Droid Computer droid exec on a cloud VM (--computer)
59
+ antigravity Gemini Managed Agents — Antigravity harness in a remote sandbox
56
60
 
57
61
  Examples:
58
62
  # Dispatch a quick fix to Rush Cloud and stream the output
@@ -92,8 +96,8 @@ Examples:
92
96
  cloud
93
97
  .command('run [prompt]')
94
98
  .description('Dispatch a task to a cloud agent.')
95
- .option('--provider <id>', 'Cloud backend: rush, codex, factory')
96
- .option('--agent <name>', 'Agent to run: claude, codex, droid')
99
+ .option('--provider <id>', 'Cloud backend: rush, codex, factory, antigravity (overrides agent auto-routing)')
100
+ .option('--agent <name>', 'Agent to run: claude, codex, droid, antigravity (auto-routes to its native cloud)')
97
101
  .option('--repo <owner/repo>', 'GitHub repository. Repeatable for multi-repo dispatch (Rush Cloud only).', (value, previous) => {
98
102
  const acc = Array.isArray(previous) ? previous : [];
99
103
  acc.push(value);
@@ -105,6 +109,7 @@ Examples:
105
109
  .option('--model <model>', 'Model override')
106
110
  .option('--env <id>', 'Codex Cloud environment ID')
107
111
  .option('--computer <name>', 'Factory/Droid computer target')
112
+ .option('--autonomy <level>', 'Factory/Droid autonomy: low, medium, high (default high)')
108
113
  .option('--mode <mode>', 'Execution mode (e.g., plan, edit, full)')
109
114
  .option('-b, --balanced', 'Shortcut for --strategy balanced. Route the factory run across all healthy accounts.')
110
115
  .option('--strategy <strategy>', 'Account selection strategy for the factory: balanced. Sends all healthy accounts so the factory pod rotates between them on rate-limit.')
@@ -142,7 +147,9 @@ Examples:
142
147
  process.stderr.write(chalk.dim(`Reading prompt from ${filePath} (${sizeKB} KB)\n`));
143
148
  }
144
149
  }
145
- const provider = resolveProvider(options.provider);
150
+ // Agent-aware: with no --provider, the agent routes to its native cloud
151
+ // (claude→rush, codex→codex, droid→factory, antigravity→antigravity).
152
+ const provider = resolveProvider(options.provider, options.agent);
146
153
  // --repo is repeatable: commander gives us an array via our collector.
147
154
  // A single --repo value arrives as a one-element array; keep the legacy
148
155
  // singular `repo` field in sync so providers that only know that field
@@ -166,6 +173,8 @@ Examples:
166
173
  dispatchOptions.providerOptions.env = options.env;
167
174
  if (options.computer)
168
175
  dispatchOptions.providerOptions.computer = options.computer;
176
+ if (options.autonomy)
177
+ dispatchOptions.providerOptions.autonomy = options.autonomy;
169
178
  if (options.mode)
170
179
  dispatchOptions.providerOptions.mode = options.mode;
171
180
  if (options.balanced || options.strategy === 'balanced') {
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `agents menubar` — manage the macOS menu-bar helper.
3
+ *
4
+ * The helper is a no-Dock status-bar app that surfaces running sessions, agents
5
+ * needing input, and routines, and launches new sessions. It auto-installs on
6
+ * upgrade (runMigration -> installMenubarLaunchAgentOnUpgrade) for every macOS
7
+ * user; these commands are the manual override.
8
+ */
9
+ import type { Command } from 'commander';
10
+ export declare function registerMenubarCommands(program: Command): void;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * `agents menubar` — manage the macOS menu-bar helper.
3
+ *
4
+ * The helper is a no-Dock status-bar app that surfaces running sessions, agents
5
+ * needing input, and routines, and launches new sessions. It auto-installs on
6
+ * upgrade (runMigration -> installMenubarLaunchAgentOnUpgrade) for every macOS
7
+ * user; these commands are the manual override.
8
+ */
9
+ import chalk from 'chalk';
10
+ import { enableMenubarService, disableMenubarService, getMenubarStatus, } from '../lib/menubar/install-menubar.js';
11
+ function notMac() {
12
+ if (process.platform !== 'darwin') {
13
+ console.log(chalk.yellow('The menu bar helper is macOS only.'));
14
+ return true;
15
+ }
16
+ return false;
17
+ }
18
+ export function registerMenubarCommands(program) {
19
+ const menubar = program
20
+ .command('menubar')
21
+ .description('Manage the macOS menu-bar helper (running sessions, agents awaiting input, routines)');
22
+ menubar
23
+ .command('enable')
24
+ .description('Install and start the menu-bar helper (launches at login)')
25
+ .action(() => {
26
+ if (notMac())
27
+ return;
28
+ const ok = enableMenubarService({ clearOptOut: true });
29
+ if (!ok) {
30
+ console.log(chalk.red('Could not enable: no menu-bar helper bundle ships with this install.'));
31
+ console.log(chalk.gray(' This build may predate the helper, or be a non-macOS package.'));
32
+ return;
33
+ }
34
+ console.log(chalk.green('Menu bar helper enabled.') + chalk.gray(' Look for the agents mark in your menu bar.'));
35
+ });
36
+ menubar
37
+ .command('disable')
38
+ .description('Stop and remove the menu-bar helper (stays off across upgrades)')
39
+ .action(() => {
40
+ if (notMac())
41
+ return;
42
+ disableMenubarService();
43
+ console.log(chalk.green('Menu bar helper disabled.') + chalk.gray(' Re-enable any time with `agents menubar enable`.'));
44
+ });
45
+ menubar
46
+ .command('status')
47
+ .description('Show whether the menu-bar helper is installed and running')
48
+ .option('--json', 'Emit machine-readable JSON')
49
+ .action((options) => {
50
+ const s = getMenubarStatus();
51
+ if (options.json) {
52
+ process.stdout.write(JSON.stringify(s) + '\n');
53
+ return;
54
+ }
55
+ if (s.platform !== 'darwin') {
56
+ console.log(chalk.yellow('The menu bar helper is macOS only.'));
57
+ return;
58
+ }
59
+ const yn = (b) => (b ? chalk.green('yes') : chalk.gray('no'));
60
+ console.log(chalk.bold('Menu bar helper\n'));
61
+ console.log(` running ${yn(s.running)}`);
62
+ console.log(` service installed ${yn(s.serviceInstalled)}`);
63
+ console.log(` app installed ${s.installedApp ? chalk.gray(s.installedApp) : chalk.gray('no')}`);
64
+ console.log(` bundle source ${s.source ? chalk.gray(s.source) : chalk.red('missing (cannot enable)')}`);
65
+ console.log(` disabled by user ${yn(s.disabledByUser)}`);
66
+ if (!s.serviceInstalled && !s.disabledByUser) {
67
+ console.log(chalk.gray('\n Enable it with `agents menubar enable`.'));
68
+ }
69
+ });
70
+ // Bare `agents menubar` -> status.
71
+ menubar.action(() => {
72
+ const s = getMenubarStatus();
73
+ if (s.platform !== 'darwin') {
74
+ console.log(chalk.yellow('The menu bar helper is macOS only.'));
75
+ return;
76
+ }
77
+ const yn = (b) => (b ? chalk.green('yes') : chalk.gray('no'));
78
+ console.log(chalk.bold('Menu bar helper\n'));
79
+ console.log(` running ${yn(s.running)}`);
80
+ console.log(` service installed ${yn(s.serviceInstalled)}`);
81
+ console.log(chalk.gray('\n enable | disable | status'));
82
+ });
83
+ }
@@ -124,9 +124,14 @@ export function registerRoutinesCommands(program) {
124
124
  routinesCmd
125
125
  .command('list')
126
126
  .description('See all scheduled jobs, when they run next, and their last execution status')
127
- .action(() => {
127
+ .option('--json', 'Emit machine-readable JSON instead of the table (used by the menu bar helper)')
128
+ .action((options) => {
128
129
  const jobs = listAllJobs(process.cwd());
129
130
  if (jobs.length === 0) {
131
+ if (options.json) {
132
+ process.stdout.write('[]\n');
133
+ return;
134
+ }
130
135
  console.log(chalk.gray('No jobs configured'));
131
136
  console.log(chalk.gray(' Add a job: agents routines add <path-to-job.yml>'));
132
137
  return;
@@ -142,6 +147,34 @@ export function registerRoutinesCommands(program) {
142
147
  catch {
143
148
  // Best-effort indicator; never block the list on detection errors.
144
149
  }
150
+ // Machine-readable path: same data the table renders, but structured.
151
+ // The menu bar helper relies on this so it never reimplements cron math.
152
+ if (options.json) {
153
+ const nowJson = new Date();
154
+ const payload = jobs.map((job) => {
155
+ const nextRun = scheduler.getNextRun(job.name);
156
+ const latestRun = getLatestRun(job.name);
157
+ return {
158
+ name: job.name,
159
+ agent: job.agent ?? null,
160
+ workflow: job.workflow ?? null,
161
+ repo: job.repo ?? null,
162
+ schedule: job.schedule,
163
+ scheduleHuman: humanizeCron(job.schedule, job.timezone),
164
+ timezone: job.timezone ?? null,
165
+ enabled: job.enabled,
166
+ overdue: overdueSet.has(job.name),
167
+ nextRun: nextRun ? nextRun.toISOString() : null,
168
+ nextRunHuman: humanizeNextRun(nextRun ?? null, nowJson, job.timezone),
169
+ lastStatus: latestRun?.status ?? null,
170
+ lastRunStartedAt: latestRun?.startedAt ?? null,
171
+ lastRunCompletedAt: latestRun?.completedAt ?? null,
172
+ };
173
+ });
174
+ scheduler.stopAll();
175
+ process.stdout.write(JSON.stringify(payload) + '\n');
176
+ return;
177
+ }
145
178
  console.log(chalk.bold('Scheduled Jobs\n'));
146
179
  // OSC 8 hyperlink helper — renders as a clickable link in supporting terminals.
147
180
  // Guarded on process.stdout.isTTY so that piped/redirected output never
@@ -5,7 +5,7 @@
5
5
  * and managing named bundles of environment variables backed by macOS
6
6
  * Keychain. Bundles are injected at run time via `agents run --secrets`.
7
7
  */
8
- import type { Command } from 'commander';
8
+ import { type Command } from 'commander';
9
9
  /**
10
10
  * SSH target for `export --to-ssh`: a bare ssh-config host alias (e.g. `yosemite-s0`)
11
11
  * or `user@host`. The strict allowlist blocks shell metacharacters and a leading `-`
@@ -5,10 +5,11 @@
5
5
  * and managing named bundles of environment variables backed by macOS
6
6
  * Keychain. Bundles are injected at run time via `agents run --secrets`.
7
7
  */
8
+ import { Option } from 'commander';
8
9
  import chalk from 'chalk';
9
10
  import * as fs from 'fs';
10
11
  import { spawnSync } from 'child_process';
11
- import { bundleExists, bundleItemStore, bundleTier, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, renameBundle, rotateBundleSecret, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
12
+ import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, renameBundle, rotateBundleSecret, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
12
13
  import { getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
13
14
  import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
14
15
  import { DEFAULT_TTL_MS, agentLoad, agentLock, agentStatus, ensureAgentRunning, installSecretsAgentService, runAgentLoadFromStdin, runSecretsAgent, secretsAgentServiceInstalled, uninstallSecretsAgentService, } from '../lib/secrets/agent.js';
@@ -212,8 +213,30 @@ function humanAge(iso) {
212
213
  return age;
213
214
  return `${age} ago`;
214
215
  }
216
+ /** Compact remaining-time for the list POLICY column: "19h" / "45m" / "2d". */
217
+ function compactRemaining(expiresAt) {
218
+ const ms = expiresAt - Date.now();
219
+ if (ms <= 0)
220
+ return 'expired';
221
+ const mins = Math.round(ms / 60000);
222
+ if (mins < 60)
223
+ return `${mins}m`;
224
+ const hours = Math.round(mins / 60);
225
+ if (hours < 24)
226
+ return `${hours}h`;
227
+ return `${Math.round(hours / 24)}d`;
228
+ }
229
+ /** The POLICY column for `secrets list`: the prompt policy, plus a "Nh left"
230
+ * hint when a `daily` bundle is currently held by the secrets-agent. `held`
231
+ * maps bundle name → expiry epoch-ms (from agentStatus()). */
232
+ function renderPolicyCol(b, held) {
233
+ if (bundlePolicy(b) === 'always')
234
+ return chalk.yellow('always ask');
235
+ const exp = held?.get(b.name);
236
+ return exp ? chalk.green(`daily · ${compactRemaining(exp)} left`) : chalk.gray('daily');
237
+ }
215
238
  /** Format a single bundle as a table row for the `secrets list` output. */
216
- function renderBundleRow(b) {
239
+ function renderBundleRow(b, held) {
217
240
  const entries = describeBundle(b);
218
241
  const keys = entries.length;
219
242
  const expiringCount = countExpiringSoon(b.meta);
@@ -231,6 +254,7 @@ function renderBundleRow(b) {
231
254
  : (b.created_at ? chalk.gray('never') : chalk.gray('?'));
232
255
  const head = `${chalk.cyan(b.name.padEnd(20))} ` +
233
256
  `${String(keys).padEnd(5)} ` +
257
+ `${padVisible(renderPolicyCol(b, held), 18)} ` +
234
258
  `${padVisible(expiring, 9)} ` +
235
259
  `${padVisible(created, 9)} ` +
236
260
  `${padVisible(updated, 9)} ` +
@@ -380,9 +404,12 @@ export function registerSecretsCommands(program) {
380
404
  # Inject the bundle into an agent run
381
405
  agents run claude "deploy the worker" --secrets prod
382
406
 
383
- # See what's in the bundle (values masked)
407
+ # See what's in the bundle (values masked); shows its prompt policy
384
408
  agents secrets view prod
385
409
 
410
+ # Stop a noisy automation bundle from prompting every run: ask once a day
411
+ agents secrets policy prod daily
412
+
386
413
  # Eval the bundle into your current shell
387
414
  eval "$(agents secrets export prod --plaintext)"
388
415
 
@@ -398,11 +425,16 @@ export function registerSecretsCommands(program) {
398
425
  or device passcode; cross-machine sync is handled by 'agents secrets push/pull'.
399
426
 
400
427
  Touch ID noise: macOS pops a prompt per bundle per process, so concurrent
401
- agents each re-prompt. 'agents secrets unlock <bundle>' holds the resolved
402
- bundle in a local agent after one prompt; later runs read it silently until
403
- it expires (default 24h), you 'lock' it, or the screen locks. Nothing on disk.
428
+ agents each re-prompt. Each bundle has a prompt policy, shown in the POLICY
429
+ column of 'agents secrets list':
430
+ always (default) ask for Touch ID every time never auto-held.
431
+ daily ask once, then hold it silently in the local agent up
432
+ to ~24h, until screen-lock / sleep / logout or 'lock'.
433
+ Set it with 'agents secrets policy <bundle> daily'. 'agents secrets unlock
434
+ <bundle>' holds any bundle after one prompt regardless of policy. Nothing on disk.
404
435
 
405
436
  See also:
437
+ agents secrets policy <bundle> daily ask once a day, not every run
406
438
  agents secrets unlock <bundle> hold a bundle after one Touch ID
407
439
  agents secrets lock wipe held bundles (re-prompt next read)
408
440
  agents secrets status show held bundles + when they lock
@@ -416,7 +448,7 @@ export function registerSecretsCommands(program) {
416
448
  registerCommandGroups(cmd, [
417
449
  { title: 'Bundle commands', names: ['list', 'view', 'create', 'rename', 'describe', 'delete'] },
418
450
  { title: 'Secret commands', names: ['add', 'rotate', 'remove', 'import', 'export'] },
419
- { title: 'Agent commands', names: ['start', 'stop', 'unlock', 'lock', 'status', 'tier'] },
451
+ { title: 'Agent commands', names: ['start', 'stop', 'unlock', 'lock', 'status', 'policy'] },
420
452
  { title: 'Raw item commands', names: ['get', 'set'] },
421
453
  { title: 'Sync commands', names: ['push', 'pull', 'remote-list'] },
422
454
  { title: 'Utilities', names: ['exec', 'generate', 'migrate-acl'] },
@@ -425,16 +457,28 @@ export function registerSecretsCommands(program) {
425
457
  .command('list')
426
458
  .alias('ls')
427
459
  .description('List configured secrets bundles')
428
- .action(() => {
460
+ .action(async () => {
429
461
  const bundles = listBundles();
430
462
  if (bundles.length === 0) {
431
463
  console.log(chalk.gray('No secrets bundles configured.'));
432
464
  console.log(chalk.gray('Try: agents secrets create <name>'));
433
465
  return;
434
466
  }
435
- console.log(chalk.bold(`${'NAME'.padEnd(20)} ${'KEYS'.padEnd(5)} ${'EXPIRING'.padEnd(9)} ${'CREATED'.padEnd(9)} ${'UPDATED'.padEnd(9)} ${'USED'.padEnd(7)} DESCRIPTION`));
467
+ // Cross-reference the secrets-agent so `daily` bundles that are currently
468
+ // held can show "· Nh left". Soft-fails to no hint if the broker is down.
469
+ const held = new Map();
470
+ if (process.platform === 'darwin') {
471
+ try {
472
+ for (const e of await agentStatus())
473
+ held.set(e.name, e.expiresAt);
474
+ }
475
+ catch {
476
+ /* broker not running — render policy without the countdown */
477
+ }
478
+ }
479
+ console.log(chalk.bold(`${'NAME'.padEnd(20)} ${'KEYS'.padEnd(5)} ${'POLICY'.padEnd(18)} ${'EXPIRING'.padEnd(9)} ${'CREATED'.padEnd(9)} ${'UPDATED'.padEnd(9)} ${'USED'.padEnd(7)} DESCRIPTION`));
436
480
  for (const b of bundles) {
437
- console.log(renderBundleRow(b));
481
+ console.log(renderBundleRow(b, held));
438
482
  }
439
483
  });
440
484
  cmd
@@ -455,8 +499,9 @@ export function registerSecretsCommands(program) {
455
499
  console.log(chalk.yellow('allow_exec: true'));
456
500
  if (bundle.backend === 'file')
457
501
  console.log(chalk.gray('backend: file (passphrase-encrypted; reads need AGENTS_SECRETS_PASSPHRASE, no Touch ID)'));
458
- if (bundleTier(bundle) === 'session')
459
- console.log(chalk.gray('tier: session (secrets-agent eligible)'));
502
+ console.log(bundlePolicy(bundle) === 'daily'
503
+ ? chalk.gray('policy: daily (ask once, then held ~24h until screen-lock / sleep / logout)')
504
+ : chalk.gray('policy: always (asks for Touch ID every time — never auto-held)'));
460
505
  if (bundle.created_at)
461
506
  console.log(chalk.gray(`created_at: ${bundle.created_at} (${humanAge(bundle.created_at)})`));
462
507
  if (bundle.updated_at)
@@ -578,14 +623,15 @@ export function registerSecretsCommands(program) {
578
623
  .description('Create an empty bundle')
579
624
  .option('--description <text>', 'Free-form description')
580
625
  .option('--allow-exec', 'Allow exec: refs in this bundle (off by default)')
581
- .option('--tier <tier>', 'secrets-agent tier: biometry (default) or session', 'biometry')
626
+ .option('--policy <policy>', 'prompt policy: always (default, ask every time) or daily (ask once a day)')
627
+ .addOption(new Option('--tier <policy>', 'deprecated alias for --policy').hideHelp())
582
628
  .option('--backend <backend>', 'storage backend: keychain (default) or file (passphrase-encrypted, headless-readable)', 'keychain')
583
629
  .option('--force', 'Overwrite an existing bundle')
584
630
  .action(async (name, opts) => {
585
631
  try {
586
632
  const resolvedName = name ?? (await promptBundleName());
587
633
  validateBundleName(resolvedName);
588
- const tier = parseTierOpt(opts.tier);
634
+ const policy = parsePolicyOpt(opts.policy ?? opts.tier);
589
635
  const backend = parseBackendOpt(opts.backend);
590
636
  if (bundleExists(resolvedName) && !opts.force) {
591
637
  console.error(chalk.red(`Bundle '${resolvedName}' already exists. Use --force to overwrite.`));
@@ -596,12 +642,15 @@ export function registerSecretsCommands(program) {
596
642
  description: opts.description,
597
643
  allow_exec: opts.allowExec,
598
644
  backend: backend === 'file' ? 'file' : undefined,
599
- tier,
645
+ policy,
600
646
  vars: {},
601
647
  };
602
648
  writeBundle(bundle);
603
- const tags = [tier === 'session' ? 'tier: session' : null, backend === 'file' ? 'backend: file' : null].filter(Boolean);
604
- console.log(chalk.green(`Bundle '${resolvedName}' created${tags.length ? ` (${tags.join(', ')})` : ''}.`));
649
+ const tags = [
650
+ policy === 'daily' ? 'policy: daily' : 'policy: always ask',
651
+ backend === 'file' ? 'backend: file' : null,
652
+ ].filter(Boolean);
653
+ console.log(chalk.green(`Bundle '${resolvedName}' created (${tags.join(', ')}).`));
605
654
  if (backend === 'file') {
606
655
  console.log(chalk.gray('File-backed: items are AES-256-GCM encrypted under AGENTS_SECRETS_PASSPHRASE (no Touch ID).'));
607
656
  }
@@ -1379,21 +1428,25 @@ Examples:
1379
1428
  }
1380
1429
  });
1381
1430
  cmd
1382
- .command('tier <bundle> [tier]')
1383
- .description("Show or set a bundle's secrets-agent tier: biometry (default) or session.")
1384
- .action((bundleName, tier) => {
1431
+ .command('policy <bundle> [policy]')
1432
+ .alias('tier')
1433
+ .description("Show or set a bundle's prompt policy: always (default, ask every time) or daily (ask once a day).")
1434
+ .action((bundleName, policyArg) => {
1385
1435
  try {
1386
1436
  const bundle = readBundle(bundleName);
1387
- if (tier === undefined) {
1388
- console.log(`${chalk.cyan(bundle.name)} tier: ${chalk.bold(bundleTier(bundle))}`);
1437
+ if (policyArg === undefined) {
1438
+ console.log(`${chalk.cyan(bundle.name)} policy: ${chalk.bold(bundlePolicy(bundle))}`);
1389
1439
  return;
1390
1440
  }
1391
- const next = parseTierOpt(tier);
1392
- bundle.tier = next;
1441
+ const next = parsePolicyOpt(policyArg);
1442
+ bundle.policy = next;
1393
1443
  writeBundle(bundle);
1394
- console.log(chalk.green(`${bundle.name} tier set to ${next}.`));
1395
- if (next === 'session') {
1396
- console.log(chalk.gray('Eligible for the secrets-agent: unlock it, or enable auto-cache with `secrets.agent.auto: true` in agents.yaml.'));
1444
+ console.log(chalk.green(`${bundle.name} policy set to ${next}.`));
1445
+ if (next === 'daily') {
1446
+ console.log(chalk.gray('Held by the secrets-agent after one unlock: run `agents secrets unlock`, or enable auto-cache with `secrets.agent.auto: true` in agents.yaml.'));
1447
+ }
1448
+ else {
1449
+ console.log(chalk.gray('Asks for Touch ID every time — never auto-held.'));
1397
1450
  }
1398
1451
  }
1399
1452
  catch (err) {
@@ -1411,7 +1464,7 @@ Examples:
1411
1464
  }
1412
1465
  process.stdout.write(chalk.gray('Installing launchd service…\n'));
1413
1466
  if (await installSecretsAgentService()) {
1414
- console.log(chalk.green('secrets-agent service running.') + chalk.gray(' It stays up across the session; unlock/auto-cache now connect instantly.'));
1467
+ console.log(chalk.green('secrets-agent service running.') + chalk.gray(' It stays up across your macOS login session; unlock/auto-cache now connect instantly.'));
1415
1468
  }
1416
1469
  else {
1417
1470
  console.error(chalk.red('Service installed but did not become reachable in time (machine may be heavily loaded — launchd will keep retrying).'));
@@ -1443,18 +1496,22 @@ Examples:
1443
1496
  registerSecretsSyncCommands(cmd);
1444
1497
  registerSecretsMigrateAclCommand(cmd);
1445
1498
  }
1446
- /** Validate a --tier value, exiting with a clear message on a bad one. `none`
1447
- * is rejected explicitly: it would require storing items without the biometry
1448
- * ACL (a separate signed-helper change), so it isn't offered yet. */
1449
- function parseTierOpt(raw) {
1450
- const v = (raw ?? 'biometry').toLowerCase();
1451
- if (v === 'biometry' || v === 'session')
1452
- return v;
1453
- if (v === 'none') {
1454
- console.error(chalk.red("tier 'none' (no biometry ACL) is not available yet — use 'biometry' or 'session'."));
1499
+ /** Validate a prompt-policy value, exiting with a clear message on a bad one.
1500
+ * Accepts the legacy `biometry`/`session` tokens as aliases for `always`/`daily`
1501
+ * so older flags and scripts keep working. `never`/`none` (no biometry ACL) is
1502
+ * rejected explicitly — it needs a separate signed-helper change (see
1503
+ * https://github.com/phnx-labs/agents-cli/issues/421). */
1504
+ function parsePolicyOpt(raw) {
1505
+ const v = (raw ?? 'always').toLowerCase();
1506
+ if (v === 'always' || v === 'biometry')
1507
+ return 'always';
1508
+ if (v === 'daily' || v === 'session')
1509
+ return 'daily';
1510
+ if (v === 'never' || v === 'none') {
1511
+ console.error(chalk.red("policy 'never' (no biometry ACL) is not available yet — see https://github.com/phnx-labs/agents-cli/issues/421. Use 'always' or 'daily'."));
1455
1512
  process.exit(1);
1456
1513
  }
1457
- console.error(chalk.red(`Invalid --tier '${raw}'. Use 'biometry' or 'session'.`));
1514
+ console.error(chalk.red(`Invalid policy '${raw}'. Use 'always' or 'daily'.`));
1458
1515
  process.exit(1);
1459
1516
  }
1460
1517
  /** Validate a --backend value, exiting with a clear message on a bad one. */
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
23
  const packageJsonPath = path.join(__dirname, '..', 'package.json');
24
24
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
25
25
  const VERSION = packageJson.version;
26
- import { NPM_PACKAGE_NAME, deriveGlobalPrefix, installPackageIntoPrefix, verifyInstalledVersion, refreshAliasShims, } from './lib/self-update.js';
26
+ import { NPM_PACKAGE_NAME, deriveGlobalPrefix, detectPackageManager, installPackageIntoPrefix, installPackageWithBun, verifyInstalledVersion, refreshAliasShims, } from './lib/self-update.js';
27
27
  // Detect dev/working-tree builds and default the noisy startup steps off.
28
28
  // Three cases trip this:
29
29
  // 1. Dev install (scripts/install.sh) — package.json version stamped 0.0.0-dev.<sha>
@@ -95,6 +95,7 @@ import { registerProfilesCommands } from './commands/profiles.js';
95
95
  import { registerSecretsCommands } from './commands/secrets.js';
96
96
  import { registerWalletCommands } from './commands/wallet.js';
97
97
  import { registerHelperCommand } from './commands/helper.js';
98
+ import { registerMenubarCommands } from './commands/menubar.js';
98
99
  import { registerFactoryCommands } from './commands/factory.js';
99
100
  import { registerUsageCommand } from './commands/usage.js';
100
101
  import { registerCostCommand } from './commands/cost.js';
@@ -347,8 +348,17 @@ function printResolvedPackage(metadata) {
347
348
  }
348
349
  async function installResolvedPackage(metadata) {
349
350
  const packageRoot = path.resolve(__dirname, '..');
350
- const prefix = deriveGlobalPrefix(packageRoot);
351
- await installPackageIntoPrefix(`${NPM_PACKAGE_NAME}@${metadata.version}`, prefix);
351
+ const spec = `${NPM_PACKAGE_NAME}@${metadata.version}`;
352
+ // Upgrade with the package manager that owns this install. A bun global
353
+ // install lives at <bunGlobalDir>/node_modules/... (no `lib` segment), so an
354
+ // `npm install --prefix` would write to <bunGlobalDir>/lib/node_modules and
355
+ // never touch the running copy — npm exits 0, the verify below fails.
356
+ if (detectPackageManager(packageRoot) === 'bun') {
357
+ await installPackageWithBun(spec);
358
+ }
359
+ else {
360
+ await installPackageIntoPrefix(spec, deriveGlobalPrefix(packageRoot));
361
+ }
352
362
  verifyInstalledVersion(packageRoot, metadata.version);
353
363
  refreshAliasShims(packageRoot);
354
364
  // The npm install above runs with --ignore-scripts, so the postinstall that
@@ -675,6 +685,7 @@ registerProfilesCommands(program);
675
685
  registerSecretsCommands(program);
676
686
  registerWalletCommands(program);
677
687
  registerHelperCommand(program);
688
+ registerMenubarCommands(program);
678
689
  registerBetaCommands(program);
679
690
  registerSyncCommand(program);
680
691
  registerRefreshRulesCommand(program);
@@ -916,6 +927,20 @@ if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
916
927
  }
917
928
  catch { /* migration must never block CLI startup */ }
918
929
  }
930
+ // Auto-enable the macOS menu-bar helper once, for every user. Best-effort and
931
+ // idempotent: installMenubarLaunchAgentOnUpgrade() no-ops when not on darwin,
932
+ // when the user ran `agents menubar disable` (sticky opt-out), when the service
933
+ // is already installed, or when no helper bundle ships with this build. This is
934
+ // a lightweight startup self-heal (two existsSync checks then return) rather
935
+ // than a migration-sentinel bump, so it covers fresh installs AND upgrades
936
+ // without re-running the full migration for the whole user base (issue #20).
937
+ if (process.platform === 'darwin' && process.env.AGENTS_SKIP_MIGRATION !== '1') {
938
+ try {
939
+ const { installMenubarLaunchAgentOnUpgrade } = await import('./lib/menubar/install-menubar.js');
940
+ installMenubarLaunchAgentOnUpgrade();
941
+ }
942
+ catch { /* never block CLI startup on the menu bar */ }
943
+ }
919
944
  try {
920
945
  await maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequested);
921
946
  await program.parseAsync();
@@ -207,6 +207,9 @@ export const AGENTS = {
207
207
  format: 'markdown',
208
208
  variableSyntax: '$ARGUMENTS',
209
209
  supportsHooks: true,
210
+ // Claude Code has no headless Anthropic-hosted dispatch CLI (only
211
+ // --remote-control, which bridges a *local* session). Its cloud is Rush.
212
+ cloudProvider: 'rush',
210
213
  capabilities: { hooks: true, mcp: true, allowlist: true, skills: true, commands: true, plugins: true, subagents: true, rules: { file: 'CLAUDE.md' }, workflows: true, modes: ['plan', 'edit', 'auto', 'skip'], rulesImports: true },
211
214
  },
212
215
  // codex hooks: gated to >= 0.116.0 (introduced [features] codex_hooks flag).
@@ -226,6 +229,7 @@ export const AGENTS = {
226
229
  format: 'markdown',
227
230
  variableSyntax: '$ARGUMENTS',
228
231
  supportsHooks: true,
232
+ cloudProvider: 'codex',
229
233
  capabilities: { hooks: { since: '0.116.0' }, mcp: true, allowlist: false, skills: true, commands: { until: '0.117.0' }, plugins: { since: '0.128.0' }, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit', 'skip'] },
230
234
  },
231
235
  gemini: {
@@ -412,6 +416,7 @@ export const AGENTS = {
412
416
  format: 'markdown',
413
417
  variableSyntax: '{{args}}',
414
418
  supportsHooks: true,
419
+ cloudProvider: 'antigravity',
415
420
  capabilities: { hooks: true, mcp: true, allowlist: true, skills: true, commands: true, plugins: true, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['edit', 'skip'], rulesImports: false },
416
421
  },
417
422
  // xAI Grok Build CLI (`grok`) — early beta, SuperGrok Heavy. Auth via OAuth on
@@ -509,6 +514,9 @@ export const AGENTS = {
509
514
  format: 'markdown',
510
515
  variableSyntax: '$ARGUMENTS',
511
516
  supportsHooks: false,
517
+ // Factory Droid Computers (cloud VMs) reached via `droid computer ssh` +
518
+ // remote headless `droid exec`.
519
+ cloudProvider: 'factory',
512
520
  capabilities: {
513
521
  hooks: false,
514
522
  mcp: true,