@phnx-labs/agents-cli 1.20.21 → 1.20.23
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 +142 -13
- package/dist/commands/exec.js +13 -1
- 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 +292 -225
- 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/codex.d.ts +1 -0
- package/dist/lib/cloud/codex.js +8 -2
- package/dist/lib/cloud/factory.d.ts +79 -18
- package/dist/lib/cloud/factory.js +324 -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 +73 -2
- package/dist/lib/cloud/types.js +17 -0
- package/dist/lib/exec.d.ts +2 -0
- package/dist/lib/exec.js +5 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +20 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +115 -0
- 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/startup/command-registry.d.ts +99 -0
- package/dist/lib/startup/command-registry.js +136 -0
- 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 +5 -3
- 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
|
@@ -4,6 +4,7 @@ import ora from 'ora';
|
|
|
4
4
|
import { resolveProvider, getAllProviders, getDefaultProviderId } from '../lib/cloud/registry.js';
|
|
5
5
|
import { insertTask, updateTaskStatus, getTaskById, listTasks as listStoredTasks, listActiveTasks } from '../lib/cloud/store.js';
|
|
6
6
|
import { renderStream } from '../lib/cloud/stream.js';
|
|
7
|
+
import { MissingTargetError } from '../lib/cloud/types.js';
|
|
7
8
|
/** Print an error message to stderr and exit. */
|
|
8
9
|
function die(msg, code = 1) {
|
|
9
10
|
console.error(chalk.red(msg));
|
|
@@ -43,16 +44,61 @@ function statusColor(status) {
|
|
|
43
44
|
function isJsonMode(opts) {
|
|
44
45
|
return Boolean(opts.json) || !process.stdout.isTTY;
|
|
45
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* After a `MissingTargetError`, try to resolve the target interactively.
|
|
49
|
+
* Returns the chosen id, or undefined when no interactive resolution is
|
|
50
|
+
* possible (non-TTY/JSON, provider can't enumerate, or user cancels) — the
|
|
51
|
+
* caller then prints the error's guidance.
|
|
52
|
+
*
|
|
53
|
+
* Codex has no `listTargets` (no list-environments CLI), so it always returns
|
|
54
|
+
* undefined here and the user sees the `codex cloud` guidance. Factory lists
|
|
55
|
+
* Droid Computers; if listing fails (not signed in) or parses to nothing, we
|
|
56
|
+
* fall back to a free-text prompt so a dispatch is never hard-blocked.
|
|
57
|
+
*/
|
|
58
|
+
async function pickMissingTarget(provider, err, json) {
|
|
59
|
+
if (json || !process.stdout.isTTY)
|
|
60
|
+
return undefined;
|
|
61
|
+
if (!provider.listTargets)
|
|
62
|
+
return undefined;
|
|
63
|
+
const { select, input } = await import('@inquirer/prompts');
|
|
64
|
+
const promptName = err.kind === 'env' ? 'environment' : 'computer';
|
|
65
|
+
let targets;
|
|
66
|
+
try {
|
|
67
|
+
targets = await provider.listTargets();
|
|
68
|
+
}
|
|
69
|
+
catch (listErr) {
|
|
70
|
+
process.stderr.write(chalk.dim(`Could not list ${promptName}s: ${listErr.message}\n`));
|
|
71
|
+
targets = [];
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
if (targets.length > 0) {
|
|
75
|
+
return await select({
|
|
76
|
+
message: `Select a ${promptName}`,
|
|
77
|
+
choices: targets.map((t) => ({ value: t.id, name: t.label ? `${t.id} ${chalk.dim(t.label)}` : t.id })),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const typed = (await input({ message: `No ${promptName}s found. Enter a ${promptName} name (blank to cancel):` })).trim();
|
|
81
|
+
return typed || undefined;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// User hit Ctrl-C / Esc on the prompt.
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
46
88
|
/** Register the `agents cloud` command tree (run, list, status, logs, cancel, message, providers). */
|
|
47
89
|
export function registerCloudCommands(program) {
|
|
48
90
|
const cloud = program
|
|
49
91
|
.command('cloud', { hidden: true })
|
|
50
|
-
.description('Dispatch and manage cloud agent tasks across providers (Rush
|
|
92
|
+
.description('Dispatch and manage cloud agent tasks across providers (Rush, Codex, Factory, Antigravity).')
|
|
51
93
|
.addHelpText('after', `
|
|
94
|
+
Each agent runs in its own cloud. Pass --agent and the provider is auto-selected
|
|
95
|
+
(claude→rush, codex→codex, droid→factory, antigravity→antigravity); --provider overrides.
|
|
96
|
+
|
|
52
97
|
Providers:
|
|
53
|
-
rush
|
|
54
|
-
codex
|
|
55
|
-
factory
|
|
98
|
+
rush Rush Cloud — Claude against a GitHub repo + branch → PR
|
|
99
|
+
codex Codex Cloud — runs in a pre-built Codex environment (--env)
|
|
100
|
+
factory Factory Droid Computer — droid exec on a cloud VM (--computer)
|
|
101
|
+
antigravity Gemini Managed Agents — Antigravity harness in a remote sandbox
|
|
56
102
|
|
|
57
103
|
Examples:
|
|
58
104
|
# Dispatch a quick fix to Rush Cloud and stream the output
|
|
@@ -92,8 +138,8 @@ Examples:
|
|
|
92
138
|
cloud
|
|
93
139
|
.command('run [prompt]')
|
|
94
140
|
.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')
|
|
141
|
+
.option('--provider <id>', 'Cloud backend: rush, codex, factory, antigravity (overrides agent auto-routing)')
|
|
142
|
+
.option('--agent <name>', 'Agent to run: claude, codex, droid, antigravity (auto-routes to its native cloud)')
|
|
97
143
|
.option('--repo <owner/repo>', 'GitHub repository. Repeatable for multi-repo dispatch (Rush Cloud only).', (value, previous) => {
|
|
98
144
|
const acc = Array.isArray(previous) ? previous : [];
|
|
99
145
|
acc.push(value);
|
|
@@ -105,6 +151,7 @@ Examples:
|
|
|
105
151
|
.option('--model <model>', 'Model override')
|
|
106
152
|
.option('--env <id>', 'Codex Cloud environment ID')
|
|
107
153
|
.option('--computer <name>', 'Factory/Droid computer target')
|
|
154
|
+
.option('--autonomy <level>', 'Factory/Droid autonomy: low, medium, high (default high)')
|
|
108
155
|
.option('--mode <mode>', 'Execution mode (e.g., plan, edit, full)')
|
|
109
156
|
.option('-b, --balanced', 'Shortcut for --strategy balanced. Route the factory run across all healthy accounts.')
|
|
110
157
|
.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 +189,9 @@ Examples:
|
|
|
142
189
|
process.stderr.write(chalk.dim(`Reading prompt from ${filePath} (${sizeKB} KB)\n`));
|
|
143
190
|
}
|
|
144
191
|
}
|
|
145
|
-
|
|
192
|
+
// Agent-aware: with no --provider, the agent routes to its native cloud
|
|
193
|
+
// (claude→rush, codex→codex, droid→factory, antigravity→antigravity).
|
|
194
|
+
const provider = resolveProvider(options.provider, options.agent);
|
|
146
195
|
// --repo is repeatable: commander gives us an array via our collector.
|
|
147
196
|
// A single --repo value arrives as a one-element array; keep the legacy
|
|
148
197
|
// singular `repo` field in sync so providers that only know that field
|
|
@@ -166,6 +215,8 @@ Examples:
|
|
|
166
215
|
dispatchOptions.providerOptions.env = options.env;
|
|
167
216
|
if (options.computer)
|
|
168
217
|
dispatchOptions.providerOptions.computer = options.computer;
|
|
218
|
+
if (options.autonomy)
|
|
219
|
+
dispatchOptions.providerOptions.autonomy = options.autonomy;
|
|
169
220
|
if (options.mode)
|
|
170
221
|
dispatchOptions.providerOptions.mode = options.mode;
|
|
171
222
|
if (options.balanced || options.strategy === 'balanced') {
|
|
@@ -173,16 +224,41 @@ Examples:
|
|
|
173
224
|
}
|
|
174
225
|
if (options.uploadAccountTokens)
|
|
175
226
|
dispatchOptions.providerOptions.uploadAccountTokens = true;
|
|
176
|
-
// Dispatch
|
|
177
|
-
|
|
227
|
+
// Dispatch. On a missing pre-provisioned target (Codex env / Factory
|
|
228
|
+
// computer), offer an interactive picker instead of a raw error.
|
|
229
|
+
const dispatchOnce = async () => {
|
|
230
|
+
const spinner = ora({ text: `Dispatching to ${provider.name}...`, stream: process.stderr }).start();
|
|
231
|
+
try {
|
|
232
|
+
const t = await provider.dispatch(dispatchOptions);
|
|
233
|
+
spinner.succeed(`Task ${t.id} dispatched to ${provider.name}`);
|
|
234
|
+
return t;
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
spinner.fail('Dispatch failed');
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
178
241
|
let task;
|
|
179
242
|
try {
|
|
180
|
-
task = await
|
|
181
|
-
spinner.succeed(`Task ${task.id} dispatched to ${provider.name}`);
|
|
243
|
+
task = await dispatchOnce();
|
|
182
244
|
}
|
|
183
245
|
catch (err) {
|
|
184
|
-
|
|
185
|
-
|
|
246
|
+
if (err instanceof MissingTargetError) {
|
|
247
|
+
const picked = await pickMissingTarget(provider, err, json);
|
|
248
|
+
if (!picked) {
|
|
249
|
+
die(err.guidance ? `${err.message}\n\n${err.guidance}` : err.message);
|
|
250
|
+
}
|
|
251
|
+
dispatchOptions.providerOptions[err.kind] = picked;
|
|
252
|
+
try {
|
|
253
|
+
task = await dispatchOnce();
|
|
254
|
+
}
|
|
255
|
+
catch (err2) {
|
|
256
|
+
die(err2.message);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
die(err.message);
|
|
261
|
+
}
|
|
186
262
|
}
|
|
187
263
|
// Persist locally
|
|
188
264
|
insertTask(task);
|
|
@@ -413,4 +489,57 @@ Examples:
|
|
|
413
489
|
console.log(` ${p.id.padEnd(12)} ${p.name.padEnd(20)} ${status}${defaultTag}`);
|
|
414
490
|
}
|
|
415
491
|
});
|
|
492
|
+
// ── agents cloud envs ─────────────────────────────────────────────────
|
|
493
|
+
// Discover the pre-provisioned targets a provider runs inside — Codex
|
|
494
|
+
// environments, Factory Droid Computers — so users don't copy opaque IDs
|
|
495
|
+
// out of a web UI.
|
|
496
|
+
cloud
|
|
497
|
+
.command('envs')
|
|
498
|
+
.alias('targets')
|
|
499
|
+
.description('List the pre-provisioned targets (Codex environments / Droid Computers) you can dispatch into.')
|
|
500
|
+
.option('--provider <id>', 'Only this provider (codex, factory, ...)')
|
|
501
|
+
.option('--json', 'JSON output')
|
|
502
|
+
.action(async (options) => {
|
|
503
|
+
const json = isJsonMode(options);
|
|
504
|
+
const only = options.provider;
|
|
505
|
+
// Providers that run inside a pre-provisioned target declare targetKind.
|
|
506
|
+
const providers = getAllProviders().filter((p) => p.targetKind && (!only || p.id === only));
|
|
507
|
+
if (only && providers.length === 0) {
|
|
508
|
+
die(`Provider '${only}' has no pre-provisioned targets (or is unknown). Targets apply to: codex, factory.`);
|
|
509
|
+
}
|
|
510
|
+
const results = [];
|
|
511
|
+
for (const p of providers) {
|
|
512
|
+
const kind = p.targetKind;
|
|
513
|
+
if (!p.listTargets) {
|
|
514
|
+
// Not enumerable (Codex). Surface guidance instead of a list.
|
|
515
|
+
const guidance = kind === 'env'
|
|
516
|
+
? 'Codex environments are not listable from the CLI. Browse/create them with `codex cloud` (interactive), then use --env <id>.'
|
|
517
|
+
: 'Not enumerable from the CLI.';
|
|
518
|
+
results.push({ provider: p.id, kind, targets: [], note: guidance });
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
const targets = await p.listTargets();
|
|
523
|
+
results.push({ provider: p.id, kind, targets: targets.map((t) => ({ id: t.id, label: t.label })) });
|
|
524
|
+
}
|
|
525
|
+
catch (err) {
|
|
526
|
+
results.push({ provider: p.id, kind, targets: [], note: err.message });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (json) {
|
|
530
|
+
process.stdout.write(JSON.stringify(results, null, 2) + '\n');
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
for (const r of results) {
|
|
534
|
+
console.log(chalk.bold(`\n${r.provider}`) + chalk.dim(` (${r.kind})`));
|
|
535
|
+
if (r.targets.length > 0) {
|
|
536
|
+
for (const t of r.targets) {
|
|
537
|
+
console.log(` ${t.id}${t.label ? ' ' + chalk.dim(t.label) : ''}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
console.log(chalk.dim(` ${r.note ?? 'none'}`));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
});
|
|
416
545
|
}
|
package/dist/commands/exec.js
CHANGED
|
@@ -150,6 +150,10 @@ export function registerRunCommand(program) {
|
|
|
150
150
|
|
|
151
151
|
# Inject a keychain-backed secrets bundle
|
|
152
152
|
agents run claude "deploy the worker" --secrets prod --mode edit
|
|
153
|
+
|
|
154
|
+
# Pass arbitrary native flags to the underlying CLI via -- separator
|
|
155
|
+
agents run kimi -- --plan --some-kimi-option value
|
|
156
|
+
agents run claude "fix the bug" -- --custom-flag
|
|
153
157
|
`,
|
|
154
158
|
notes: `
|
|
155
159
|
Modes (not every agent supports every mode — check agents.yaml capabilities):
|
|
@@ -168,9 +172,16 @@ export function registerRunCommand(program) {
|
|
|
168
172
|
Fallback: --fallback codex,gemini retries on rate-limit failure via /continue handoff. Each entry accepts @version.
|
|
169
173
|
|
|
170
174
|
Resume: --session-id <id> continues a prior Claude conversation.
|
|
175
|
+
|
|
176
|
+
Passthrough: everything after -- is forwarded verbatim to the underlying agent CLI.
|
|
177
|
+
agents run kimi -- --plan --some-native-flag value
|
|
171
178
|
`,
|
|
172
179
|
});
|
|
173
|
-
runCmd.action(async (agentSpec, prompt, options) => {
|
|
180
|
+
runCmd.action(async (agentSpec, prompt, options, command) => {
|
|
181
|
+
// Capture everything after -- as passthrough args forwarded verbatim to the underlying CLI.
|
|
182
|
+
// Use command.args (all positional strings) and strip the declared positional args from the front.
|
|
183
|
+
const declaredArgCount = prompt !== undefined ? 2 : 1;
|
|
184
|
+
const passthroughArgs = command.args.slice(declaredArgCount);
|
|
174
185
|
// --resume-checkpoint short-circuits normal dispatch entirely: the
|
|
175
186
|
// checkpoint already carries the agent, version, prompt, session id,
|
|
176
187
|
// iteration, and loop config of the killed run. Reconstruct ExecOptions
|
|
@@ -642,6 +653,7 @@ export function registerRunCommand(program) {
|
|
|
642
653
|
env,
|
|
643
654
|
toolsRestrict: workflowToolsRestrict,
|
|
644
655
|
mcpConfigPath: workflowMcpConfigPath,
|
|
656
|
+
passthroughArgs,
|
|
645
657
|
};
|
|
646
658
|
if (options.interactive && options.headless) {
|
|
647
659
|
console.error(chalk.red('--interactive and --headless are mutually exclusive. Pass one, or neither (mode is inferred from prompt presence).'));
|
|
@@ -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 `-`
|