@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/commands/cloud.js +142 -13
  3. package/dist/commands/exec.js +13 -1
  4. package/dist/commands/menubar.d.ts +10 -0
  5. package/dist/commands/menubar.js +83 -0
  6. package/dist/commands/routines.js +34 -1
  7. package/dist/commands/secrets.d.ts +1 -1
  8. package/dist/commands/secrets.js +95 -38
  9. package/dist/index.js +292 -225
  10. package/dist/lib/agents.js +8 -0
  11. package/dist/lib/cloud/antigravity.d.ts +70 -0
  12. package/dist/lib/cloud/antigravity.js +196 -0
  13. package/dist/lib/cloud/codex.d.ts +1 -0
  14. package/dist/lib/cloud/codex.js +8 -2
  15. package/dist/lib/cloud/factory.d.ts +79 -18
  16. package/dist/lib/cloud/factory.js +324 -26
  17. package/dist/lib/cloud/registry.d.ts +18 -2
  18. package/dist/lib/cloud/registry.js +28 -4
  19. package/dist/lib/cloud/types.d.ts +73 -2
  20. package/dist/lib/cloud/types.js +17 -0
  21. package/dist/lib/exec.d.ts +2 -0
  22. package/dist/lib/exec.js +5 -0
  23. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +20 -0
  24. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  25. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +115 -0
  26. package/dist/lib/menubar/install-menubar.d.ts +57 -0
  27. package/dist/lib/menubar/install-menubar.js +291 -0
  28. package/dist/lib/secrets/agent.d.ts +9 -1
  29. package/dist/lib/secrets/agent.js +91 -10
  30. package/dist/lib/secrets/bundles.d.ts +19 -12
  31. package/dist/lib/secrets/bundles.js +22 -14
  32. package/dist/lib/self-update.d.ts +34 -0
  33. package/dist/lib/self-update.js +63 -2
  34. package/dist/lib/startup/command-registry.d.ts +99 -0
  35. package/dist/lib/startup/command-registry.js +136 -0
  36. package/dist/lib/types.d.ts +8 -0
  37. package/dist/lib/version.d.ts +11 -0
  38. package/dist/lib/version.js +20 -0
  39. package/package.json +5 -3
  40. 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.
@@ -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 Cloud, Codex Cloud, Factory).')
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 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
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
- const provider = resolveProvider(options.provider);
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
- const spinner = ora({ text: `Dispatching to ${provider.name}...`, stream: process.stderr }).start();
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 provider.dispatch(dispatchOptions);
181
- spinner.succeed(`Task ${task.id} dispatched to ${provider.name}`);
243
+ task = await dispatchOnce();
182
244
  }
183
245
  catch (err) {
184
- spinner.fail('Dispatch failed');
185
- die(err.message);
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
  }
@@ -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
- .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 `-`