@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 +14 -0
- package/dist/commands/cloud.js +16 -7
- package/dist/commands/menubar.d.ts +10 -0
- package/dist/commands/menubar.js +83 -0
- package/dist/commands/routines.js +34 -1
- package/dist/commands/secrets.d.ts +1 -1
- package/dist/commands/secrets.js +95 -38
- package/dist/index.js +28 -3
- package/dist/lib/agents.js +8 -0
- package/dist/lib/cloud/antigravity.d.ts +70 -0
- package/dist/lib/cloud/antigravity.js +196 -0
- package/dist/lib/cloud/factory.d.ts +68 -18
- package/dist/lib/cloud/factory.js +269 -26
- package/dist/lib/cloud/registry.d.ts +18 -2
- package/dist/lib/cloud/registry.js +28 -4
- package/dist/lib/cloud/types.d.ts +32 -2
- package/dist/lib/menubar/install-menubar.d.ts +57 -0
- package/dist/lib/menubar/install-menubar.js +291 -0
- package/dist/lib/secrets/agent.d.ts +9 -1
- package/dist/lib/secrets/agent.js +91 -10
- package/dist/lib/secrets/bundles.d.ts +19 -12
- package/dist/lib/secrets/bundles.js +22 -14
- package/dist/lib/self-update.d.ts +34 -0
- package/dist/lib/self-update.js +63 -2
- package/dist/lib/types.d.ts +8 -0
- package/dist/lib/version.d.ts +11 -0
- package/dist/lib/version.js +20 -0
- package/package.json +3 -2
- package/scripts/postinstall.js +35 -0
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.
|
package/dist/commands/cloud.js
CHANGED
|
@@ -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
|
|
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
|
|
54
|
-
codex
|
|
55
|
-
factory
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
|
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 `-`
|
package/dist/commands/secrets.js
CHANGED
|
@@ -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,
|
|
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.
|
|
402
|
-
|
|
403
|
-
|
|
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', '
|
|
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
|
-
|
|
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
|
-
|
|
459
|
-
|
|
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('--
|
|
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
|
|
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
|
-
|
|
645
|
+
policy,
|
|
600
646
|
vars: {},
|
|
601
647
|
};
|
|
602
648
|
writeBundle(bundle);
|
|
603
|
-
const tags = [
|
|
604
|
-
|
|
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('
|
|
1383
|
-
.
|
|
1384
|
-
.
|
|
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 (
|
|
1388
|
-
console.log(`${chalk.cyan(bundle.name)}
|
|
1437
|
+
if (policyArg === undefined) {
|
|
1438
|
+
console.log(`${chalk.cyan(bundle.name)} policy: ${chalk.bold(bundlePolicy(bundle))}`);
|
|
1389
1439
|
return;
|
|
1390
1440
|
}
|
|
1391
|
-
const next =
|
|
1392
|
-
bundle.
|
|
1441
|
+
const next = parsePolicyOpt(policyArg);
|
|
1442
|
+
bundle.policy = next;
|
|
1393
1443
|
writeBundle(bundle);
|
|
1394
|
-
console.log(chalk.green(`${bundle.name}
|
|
1395
|
-
if (next === '
|
|
1396
|
-
console.log(chalk.gray('
|
|
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
|
|
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
|
|
1447
|
-
*
|
|
1448
|
-
*
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
if (v === '
|
|
1454
|
-
|
|
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
|
|
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
|
|
351
|
-
|
|
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();
|
package/dist/lib/agents.js
CHANGED
|
@@ -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,
|