@phnx-labs/agents-cli 1.20.58 → 1.20.60

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 (49) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.md +15 -7
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/exec.js +39 -2
  5. package/dist/commands/output.d.ts +19 -0
  6. package/dist/commands/output.js +333 -0
  7. package/dist/commands/secrets.js +6 -6
  8. package/dist/index.js +2 -1
  9. package/dist/lib/agents.js +19 -13
  10. package/dist/lib/hosts/dispatch.d.ts +36 -0
  11. package/dist/lib/hosts/dispatch.js +40 -2
  12. package/dist/lib/hosts/passthrough.js +1 -0
  13. package/dist/lib/mcp.js +1 -1
  14. package/dist/lib/output/git-output.d.ts +74 -0
  15. package/dist/lib/output/git-output.js +213 -0
  16. package/dist/lib/permissions.d.ts +26 -5
  17. package/dist/lib/permissions.js +212 -37
  18. package/dist/lib/plugins.d.ts +8 -0
  19. package/dist/lib/plugins.js +108 -0
  20. package/dist/lib/project-root.js +2 -1
  21. package/dist/lib/resources/mcp.js +1 -1
  22. package/dist/lib/resources/permissions.d.ts +1 -1
  23. package/dist/lib/resources/permissions.js +8 -2
  24. package/dist/lib/resources/skills.js +6 -1
  25. package/dist/lib/resources/types.d.ts +1 -1
  26. package/dist/lib/routines.d.ts +16 -0
  27. package/dist/lib/routines.js +46 -1
  28. package/dist/lib/secrets/remote.d.ts +7 -2
  29. package/dist/lib/secrets/remote.js +11 -10
  30. package/dist/lib/session/db.d.ts +3 -0
  31. package/dist/lib/session/db.js +20 -4
  32. package/dist/lib/session/discover.d.ts +2 -0
  33. package/dist/lib/session/discover.js +40 -4
  34. package/dist/lib/session/types.d.ts +2 -0
  35. package/dist/lib/shims.js +13 -3
  36. package/dist/lib/skills.js +14 -1
  37. package/dist/lib/staleness/detectors/permissions.js +50 -3
  38. package/dist/lib/staleness/detectors/subagents.js +31 -12
  39. package/dist/lib/staleness/detectors/workflows.js +33 -0
  40. package/dist/lib/staleness/writers/commands.js +3 -3
  41. package/dist/lib/staleness/writers/subagents.js +13 -5
  42. package/dist/lib/startup/command-registry.d.ts +1 -0
  43. package/dist/lib/startup/command-registry.js +2 -0
  44. package/dist/lib/subagents.d.ts +11 -1
  45. package/dist/lib/subagents.js +117 -26
  46. package/dist/lib/versions.js +12 -2
  47. package/dist/lib/workflows.d.ts +5 -3
  48. package/dist/lib/workflows.js +246 -9
  49. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.20.60
6
+
7
+ - **Fix Goose skill sync status for its native central-storage path.** `agents skills list goose@<version>` now reports skills under `~/.agents/skills/` as installed instead of falsely requiring a per-version `.config/goose/skills/` copy that Goose never reads. Source: `apps/cli/src/lib/skills.ts`.
8
+ - **Correct the documented `auto` and ACP `skip` semantics.** The README and bundled `run` skill now distinguish Kimi's interactive `--auto` from its already-auto-approved headless `-p` path, document Droid's native `--auto high`, and explain that ACP `skip` prefers `allow_always` but falls back to the first permission option offered by the server. Documentation only; runtime behavior is unchanged. Source: `README.md`, `skills/run/SKILL.md`.
9
+ - **Wire Antigravity subagents and Kimi workflow sync (RUSH-1548, RUSH-1581).** Antigravity now receives subagents as custom-agent Markdown under `~/.gemini/config/agents/<name>/agent.md` with the `>= 1.0.16` version gate enforced during sync. Kimi receives workflows as managed `type: flow` skills under `.kimi-code/skills/<name>/SKILL.md`, using the canonical slug as the flow name and an `agents_workflow` marker so native user-owned flows are not overwritten or removed. Antigravity workflows remain disabled for version-home sync until a global filesystem target is documented. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/subagents.ts`, `apps/cli/src/lib/workflows.ts`.
10
+ - **Release retries rebuild the exact merged, CI-tested release tree even after `main` advances.** A registry/auth failure after the release PR merged previously stranded that version because the catch-up guard required the release merge to remain current `main`. The local release script now validates the original PR head and full green matrix, verifies the SHA-pinned keychain helper, rebuilds the unpinned menu-bar helper from historical source in a detached temporary worktree, rejects mismatched remote tags, and tags/publishes that exact merge without including later commits. Source: `apps/cli/scripts/release.sh`.
11
+ - **Wire Gemini plugins/subagents and Goose workflows/allowlists.** Gemini now syncs plugin bundles as Gemini extensions (`.gemini/extensions/<name>/gemini-extension.json`) from CLI 0.8.0+ and subagents as `.gemini/agents/*.md` from CLI 0.36.0+. Goose now syncs workflows as recipe/subrecipe YAML under `.config/goose/recipes/` and permission groups into `.config/goose/permission.yaml`. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/plugins.ts`, `apps/cli/src/lib/subagents.ts`, `apps/cli/src/lib/workflows.ts`, `apps/cli/src/lib/permissions.ts`. (RUSH-1568, RUSH-1569, RUSH-1574, RUSH-1582)
12
+ - **Wire Gemini permissions/allowlist support (RUSH-1567).** Gemini permission groups now sync Bash allow/deny rules into `.gemini/settings.json` as `tools.core` / `tools.exclude` entries with per-command `ShellTool(...)` patterns; non-Bash canonical permissions remain unsupported by Gemini's native tool grammar and are skipped. Flip `allowlist: true`, register the permission writer/detector through the capability table, and replace the dormant legacy `tools.allowed` serializer. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/permissions.ts`, `apps/cli/src/lib/resources/permissions.ts`, `apps/cli/src/lib/staleness/detectors/permissions.ts`.
13
+ - **Fix: `agents run <agent>@<version> --host <host>` now forwards the version pin and most run flags to the remote host.** Previously the `--host` branch stripped `@version` and ignored `--strategy`, `--effort`, `--add-dir`, `--json`, `--verbose`, `--timeout`, `--yes`, and `--acp`, so the remote host applied its own defaults. The local CLI now parses `agent@version` verbatim, normalizes `--strategy`/`--balanced`, makes `--add-dir` paths remote-portable, and forwards all of these flags to the remote `agents run` invocation. `--add-dir` portability uses the same `~`/`$HOME` re-rooting that `--cwd` already uses, so a Linux remote resolves home paths against its own `/home/<user>`. Source: `apps/cli/src/lib/hosts/dispatch.ts`, `apps/cli/src/commands/exec.ts`, `apps/cli/src/lib/hosts/dispatch.test.ts`.
14
+ - **Fix: routine edits no longer rewrite the whole YAML file, so `~/.agents` stays clean and `agents repo pull` can sync routines across the fleet.** `writeJob` previously re-emitted the entire document via `yaml.stringify` on every mutation (pause/resume, `routines devices --set`, add), restyling untouched scalars — unquoting `schedule`, re-wrapping the folded `prompt` block — which left the git-backed user repo perpetually dirty. That made cross-device `agents repo pull` refuse ("uncommitted changes"), so a `devices:` pin set on one machine never reached the others and `Devices: all` routines kept firing on every box. A new `serializeJob` edits only the changed keys via the YAML Document API, preserving byte-for-byte formatting of untouched nodes; new/unparseable/non-mapping files fall back to canonical stringify. Source: `apps/cli/src/lib/routines.ts`, `apps/cli/src/lib/__tests__/routines.serialize.test.ts`.
15
+
16
+ ## 1.20.59
17
+
18
+ - **Fix: remote secrets now choose the Windows PowerShell wrapper from the original `--host` name, not the resolved `user@ip` SSH target.** Inline enrolled Windows hosts resolve to address-based SSH targets, but the OS registry is keyed by the host name; `agents secrets view/list/exec --host <windows>`, `agents run --secrets bundle@<windows>`, and remote secrets unlock/export paths now pass that original name into command construction so Windows hosts no longer fall back to `bash -lc`. Source: `apps/cli/src/lib/secrets/remote.ts`, `apps/cli/src/commands/secrets.ts`, `apps/cli/src/commands/exec.ts`. (RUSH-1431)
19
+ - **Wire Droid skills support (RUSH-1397).** Droid loads skills from `.factory/skills/` (since 0.26.0). Flip `skills: { since: '0.26.0' }`, register generic skills writer/detector. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/__tests__/capabilities.test.ts`.
20
+ - **Wire skills support for Goose CLI (RUSH-1394).** Goose reads skills directly from `~/.agents/skills/` via the Summon extension (block-goose-cli >= 1.25.0). Flip `skills: { since: '1.25.0' }`, register generic skills writer/detector. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/staleness/writers/commands.ts`.
21
+ - **Wire Droid allowlist support (RUSH-1396).** Droid stores allow/deny in `.factory/settings.json` (`commandAllowlist`/`commandDenylist`). Flip `allowlist: true` since 0.57.5, add `convertToDroidFormat`, wire writer/detector. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/permissions.ts`.
22
+ - **Wire Codex permissions/allowlist support (RUSH-1566).** Codex stores allow/deny in `.codex/config.toml` (`approval_policy`, `sandbox_mode`). Flip `allowlist: true` since 0.128.0, add `convertToCodexFormat`, wire writer/detector. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/permissions.ts`.
23
+ - **OpenCode permissions write to the loaded config path (RUSH-1623).** Global config is `~/.config/opencode/opencode.jsonc` (not `~/.opencode/`); project config is `opencode.jsonc` at the project root. Source: `apps/cli/src/lib/permissions.ts`, `apps/cli/src/lib/agents.ts`.
24
+ - **Catch-up publishes now require the exact CI-tested release tree and its full green matrix.** A package version already present on `main` no longer counts as release validation by itself: `release.sh` resolves the merged `release/v<version>` PR, requires its merge commit to be current `main`, fetches the PR head that ran CI, requires that head tree to equal current `main`, and rechecks every expected CI context before publishing. This closes the path that let 1.20.58 reach npm before its tag-triggered Windows matrix exposed failures. Source: `apps/cli/scripts/release.sh`.
25
+ - **Windows release validation now matches portable path behavior.** Home-relative project paths normalize native separators to `/` before they are stored as `~/…`, and the systemd-manifest and remote-shell assertions now compare the escaped/quoted forms that the runtime deliberately emits. This restores the Windows Node 22/24 release matrix without weakening command escaping. Source: `apps/cli/src/lib/project-root.ts`, `apps/cli/src/lib/{project-root,daemon}.test.ts`, `apps/cli/src/lib/hosts/dispatch.test.ts`.
26
+ - **Wire subagents support for Kiro CLI.** Kiro custom agents are JSON files under `~/.kiro/agents/*.json` (introduced in kiro-cli v1.23.0). Flip Kiro's `subagents: { since: '1.23.0' }`, add `transformSubagentForKiro`, and wire the subagents writer, detector, install/remove, and orphan-detection paths. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/subagents.ts`, `apps/cli/src/lib/staleness/{writers,detectors}/subagents.ts`. (RUSH-1393)
27
+
5
28
  ## 1.20.58
6
29
 
7
30
  - **Self-updating agent CLIs are represented as one live installation.** `agents view` no longer invents version-home rows for single-binary installers such as Droid, Grok, Cursor, Kiro, Goose, and Hermes; it reports the version returned by the installed binary and folds away stale per-version directories. `agents add <agent>@<version>` now installs or keeps that agent's current release instead of rejecting an unsupported pinned install. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/versions.ts`, `apps/cli/src/commands/{versions,view}.ts`. (RUSH-1321)
@@ -9,7 +32,6 @@
9
32
  - **Wire allowlist support for Cursor CLI.** Cursor agent CLI stores allow/deny in `~/.cursor/cli-config.json` (`permissions.allow`/`deny` with Shell/Read/Write/WebFetch/Mcp). Flip `allowlist: true`, add `convertToCursorFormat` (Bash→Shell), and write via `applyPermissionsToVersion` + detector. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/permissions.ts`. (RUSH-1387)
10
33
  - **GitHub Copilot CLI subagents now sync (RUSH-1390).** Installed subagents flatten into GitHub Copilot custom-agent profiles at `~/.copilot/agents/<name>.agent.md` (the Droid custom-droid format), gated to Copilot CLI ≥ 0.0.353. `agents subagents list/view` now surfaces synced Copilot agents and `agents subagents remove` soft-deletes their `.agent.md` files to trash — both previously skipped `copilot` entirely. Source: `apps/cli/src/lib/subagents.ts` (`listSubagentsForAgent`, `removeSubagentFromVersion`, `transformSubagentForCopilot`), `apps/cli/src/lib/staleness/writers/subagents.ts`, `apps/cli/src/lib/staleness/detectors/subagents.ts`, `apps/cli/src/lib/agents.ts`.
11
34
  - **Menu-bar Quick Dispatch preserves typed drafts when focus is stolen (RUSH-1592).** If another app activates while the `Cmd-Shift-O` capture panel is open, the panel can hide without destroying the note; the next summon restores the draft text plus selected screenshots, action, and agents. Return submits and clears the draft; Escape clears without dispatching. Source: `apps/cli/menubar/Sources/MenubarHelper/PromptPanel.swift`, `apps/cli/docs/menubar.md`.
12
- - **Wire subagents support for Kiro CLI.** Kiro custom agents are JSON files under `~/.kiro/agents/*.json` (introduced in kiro-cli v1.23.0). Flip Kiro's `subagents: { since: '1.23.0' }`, add `transformSubagentForKiro`, and wire the subagents writer, detector, install/remove, and orphan-detection paths. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/subagents.ts`, `apps/cli/src/lib/staleness/{writers,detectors}/subagents.ts`. (RUSH-1393)
13
35
  - **Menu-bar ticket agents now carry every selected screenshot into the Linear issue (RUSH-1668).** `Cmd-Shift-O` already passed selected file paths to the ticket agent, but the prompt only asked it to inspect them, so agents could create text-only issues and stop. The brief now identifies every selected path as user-provided ticket material, requires each file to be uploaded, supplies the existing `linear update <id> --proof <path>` path as a reliable default, and leaves description/comment/other placement to the agent's judgment. Source: `apps/cli/menubar/Sources/MenubarHelper/{AgentsCLI,IssueSelfTest}.swift`, `apps/cli/docs/menubar.md`.
14
36
  - **`agents sessions --active --json` now carries session attachment metadata for Factory previews (RUSH-1524).** Claude and Droid prompt image/document blocks that reference local files are preserved as `{ path, name, mediaType, sizeBytes }`, and the active-session state dedupes them into `attachments` so consumers can render screenshot thumbnails and open the original files instead of only seeing an attachment count. Source: `apps/cli/src/lib/session/parse.ts`, `apps/cli/src/lib/session/state.ts`, `apps/cli/src/lib/session/active.ts`.
15
37
  - **Retired the standalone `com.phnx-labs.agents-secrets-agent` launchd service — the always-on daemon is now the sole broker host (#416, step 2).** `ensureAgentRunning()` no longer installs a separate launchd service: it retires any leftover plist via the new `retireLegacySecretsAgentService()` and relies on the daemon (Path 0), with a one-off detached broker as the only fallback. The upgrade migration (`scripts/postinstall.js` → `healLongRunningProcesses`) now `launchctl bootout`s the legacy service **first**, then (re)starts the daemon so it takes over the broker socket, instead of kickstarting the old service onto new code. `agents secrets start` is now a thin alias that brings the daemon up (and waits for the broker to answer); `agents secrets stop` locks all bundles and retires any leftover legacy service while leaving the always-on daemon running; `agents secrets status` reports broker reachability (daemon-hosted vs standalone) rather than "service installed". The stale broker teardown (version-skew self-heal) retires the legacy service instead of kickstarting it. Source: `apps/cli/src/lib/secrets/agent.ts` (`retireLegacySecretsAgentService`, `ensureAgentRunning`, `teardownStaleBroker`, `uninstallSecretsAgentService`; removed `installSecretsAgentService`/`kickstartSecretsAgentService`/`generateServicePlist`), `apps/cli/scripts/postinstall.js` (`healLongRunningProcesses`), `apps/cli/src/commands/secrets.ts` (`start`/`stop`/`status`).
package/README.md CHANGED
@@ -161,9 +161,11 @@ Supports plan (read-only), edit, auto, and skip modes, effort levels, JSON outpu
161
161
 
162
162
  Treat `skip` as a last-resort escape hatch. In direct-exec runs (without `--acp`),
163
163
  agents-cli forwards the harness's native no-prompt flag; it does not add another
164
- safety layer. Prefer `auto` where the harness has a smart classifier (Claude Code and
165
- GitHub Copilot), or `edit` everywhere else. Harnesses without a native bypass flag
166
- reject direct-exec `skip`.
164
+ safety layer. Prefer `auto` where it adds a safer automatic policy (smart classifier
165
+ on Claude/Copilot, native high-auto mode on Droid, or interactive Kimi), or `edit`
166
+ everywhere else. For headless Kimi, `edit`, `auto`, and `skip` all use the same
167
+ already-auto-approved `-p` behavior, so prefer `edit` rather than signaling a blanket
168
+ bypass. Harnesses without a native bypass flag reject direct-exec `skip`.
167
169
 
168
170
  | Harness | Direct-exec `--mode skip` becomes |
169
171
  |---|---|
@@ -179,8 +181,9 @@ reject direct-exec `skip`.
179
181
  | Droid | `--skip-permissions-unsafe` |
180
182
 
181
183
  With `--acp`, these native flags are not used. agents-cli instead grants `skip`
182
- permission requests at the ACP protocol layer with `allow_always`; the same
183
- last-resort warning applies.
184
+ permission requests at the ACP protocol layer: it selects `allow_always` when offered,
185
+ otherwise the first permission option offered by the server. The same last-resort
186
+ warning applies.
184
187
 
185
188
  Codex has no native smart-classifier mode, so `agents run codex --mode auto` resolves
186
189
  to sandboxed `edit` and can still prompt. `agents run codex --mode skip` is different:
@@ -855,9 +858,10 @@ Which DotAgents resources each agent CLI can load. Source of truth: [src/lib/age
855
858
  | OpenCode | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
856
859
  | Copilot | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
857
860
  | Amp | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
858
- | Kiro | yes | no | yes | >= 2.8.0 | yes | yes | no | no | `AGENTS.md` | no |
861
+ | Kiro | yes | no | yes | >= 2.8.0 | yes | yes | no | >= 1.23.0 | `AGENTS.md` | no |
859
862
  | Goose | yes | no | yes | no | no | no | no | no | `AGENTS.md` | no |
860
863
  | Roo Code | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
864
+ | Droid | yes | yes | yes | >= 0.57.5 | >= 0.26.0 | yes | yes | yes | `AGENTS.md` | no |
861
865
 
862
866
  **Legend:** `yes` / `no` = synced or skipped at install time. `skills ($name)` = no file-based slash-command dir; behavior ships as a generated skill invoked with `$command`. `gateway` = OpenClaw resolves slash commands at runtime, not from synced files. Version suffixes are enforced at sync time — out-of-range versions are skipped with a clear message.
863
867
 
@@ -875,13 +879,17 @@ Which DotAgents resources each agent CLI can load. Source of truth: [src/lib/age
875
879
  | Grok Build | -- | yes | yes |
876
880
  | Antigravity | -- | yes | -- |
877
881
  | Copilot | -- | -- | yes |
878
- | OpenClaw, Amp, Kiro, Goose, Roo | -- | -- | -- |
882
+ | OpenClaw, Amp, Goose, Roo | -- | -- | -- |
883
+ | Kiro | -- | -- | -- |
879
884
 
880
885
  ### Version-gated sync
881
886
 
882
887
  | Capability | Agent | Gate |
883
888
  |------------|-------|------|
884
889
  | Hooks | Codex | >= 0.116.0 |
890
+ | Subagents | Kiro | >= 1.23.0 |
891
+ | Skills | Droid | >= 0.26.0 |
892
+ | Permissions | Droid | >= 0.57.5 |
885
893
  | Hooks | Gemini | >= 0.26.0 |
886
894
  | Permissions | Kiro | >= 2.8.0 |
887
895
  | File-based commands | Codex | < 0.117.0 (0.117+ uses command-as-skill) |
package/dist/bin/agents CHANGED
Binary file
@@ -482,6 +482,7 @@ export function registerRunCommand(program) {
482
482
  const { resolveHost, resolveHostByCap } = await import('../lib/hosts/registry.js');
483
483
  const { dispatchToHost, runInteractiveOnHost } = await import('../lib/hosts/dispatch.js');
484
484
  const { registerHostSession, registerInteractiveHostSession } = await import('../lib/hosts/session-index.js');
485
+ const { normalizeRunStrategy, RUN_STRATEGIES } = await import('../lib/rotate.js');
485
486
  // A password-auth device throws DeviceOffloadUnsupportedError here; it's
486
487
  // printed cleanly by the top-level catch in index.ts (covers every
487
488
  // resolveHost caller), so it never falls through to capability routing.
@@ -506,12 +507,30 @@ export function registerRunCommand(program) {
506
507
  process.exit(1);
507
508
  }
508
509
  try {
509
- const runAgent = agentSpec.split('@')[0];
510
+ const [runAgent, rawRunVersion] = agentSpec.split('@');
511
+ // Forward the explicit @version pin verbatim. Resolving aliases like
512
+ // @latest locally would check local installs, but the remote host may
513
+ // have versions the laptop does not. The remote agents CLI resolves
514
+ // aliases against its own installed versions.
515
+ const runVersion = rawRunVersion || undefined;
516
+ // Normalize the effective strategy exactly like the local path so we
517
+ // fail fast on invalid input and can forward --balanced/--strategy.
518
+ const explicitStrategy = options.strategy ? normalizeRunStrategy(options.strategy) : null;
519
+ if (options.strategy && !explicitStrategy) {
520
+ console.error(chalk.red(`Invalid strategy: ${options.strategy}. Use ${RUN_STRATEGIES.join(', ')}.`));
521
+ process.exit(1);
522
+ }
523
+ if (options.balanced && explicitStrategy && explicitStrategy !== 'balanced') {
524
+ console.error(chalk.red('--balanced conflicts with --strategy. Use one strategy override.'));
525
+ process.exit(1);
526
+ }
527
+ const runStrategy = options.balanced ? 'balanced' : explicitStrategy ?? undefined;
510
528
  // Working directory on the host: an explicit --remote-cwd is used
511
529
  // verbatim; --cwd/--project are made portable (a local-home absolute
512
530
  // becomes `~/…` so the remote shell re-roots it at ITS home).
513
531
  const { toRemotePortable } = await import('../lib/project-root.js');
514
532
  const hostCwd = options.remoteCwd ?? (options.cwd ? toRemotePortable(options.cwd) : undefined);
533
+ const hostAddDirs = options.addDir.length > 0 ? options.addDir.map(toRemotePortable) : undefined;
515
534
  // `--resume [id]`: commander yields the string id, or `true` when the
516
535
  // flag is passed bare. A bare resume needs the interactive picker,
517
536
  // which can't run over a detached remote dispatch — only forward a
@@ -549,9 +568,18 @@ export function registerRunCommand(program) {
549
568
  }
550
569
  const exitCode = await runInteractiveOnHost(host, {
551
570
  agent: runAgent,
571
+ version: resumeId ? undefined : runVersion,
572
+ strategy: resumeId ? undefined : runStrategy,
552
573
  prompt,
553
574
  mode: options.mode,
554
575
  model: options.model,
576
+ effort: options.effort,
577
+ addDir: hostAddDirs,
578
+ json: options.json,
579
+ verbose: options.verbose,
580
+ timeout: options.timeout,
581
+ yes: options.yes,
582
+ acp: options.acp,
555
583
  remoteCwd: hostCwd,
556
584
  sessionId: hostSessionId,
557
585
  name: options.name,
@@ -571,9 +599,18 @@ export function registerRunCommand(program) {
571
599
  const hostSessionId = runAgent === 'claude' && !resumeId ? randomUUID() : undefined;
572
600
  const { task, exitCode } = await dispatchToHost(host, {
573
601
  agent: runAgent,
602
+ version: resumeId ? undefined : runVersion,
603
+ strategy: resumeId ? undefined : runStrategy,
574
604
  prompt,
575
605
  mode: options.mode,
576
606
  model: options.model,
607
+ effort: options.effort,
608
+ addDir: hostAddDirs,
609
+ json: options.json,
610
+ verbose: options.verbose,
611
+ timeout: options.timeout,
612
+ yes: options.yes,
613
+ acp: options.acp,
577
614
  remoteCwd: hostCwd,
578
615
  sessionId: hostSessionId,
579
616
  name: options.name,
@@ -1227,7 +1264,7 @@ export function registerRunCommand(program) {
1227
1264
  // Remote bundle (`bundle@host`): resolve over SSH and inject
1228
1265
  // ephemerally — values never touch this machine's keychain or disk.
1229
1266
  const target = await resolveSshTarget(host);
1230
- const bundleEnv = await remoteResolveEnv(target, bundleName);
1267
+ const bundleEnv = await remoteResolveEnv(target, bundleName, { osLookupName: host });
1231
1268
  console.log(chalk.gray(`[secrets] Resolved ${bundleName}@${host}: ${Object.keys(bundleEnv).length} keys (remote, ephemeral)`));
1232
1269
  secretsEnv = { ...secretsEnv, ...bundleEnv };
1233
1270
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Output command — productivity: token *burn* vs shipped *output*.
3
+ *
4
+ * `agents cost` answers "what did we burn?" (dollars + duration). This joins that
5
+ * burn to what actually shipped — real generated (output) tokens plus PRs and
6
+ * commits across every git identity — so you can see burn-vs-output and ratios
7
+ * like $/PR and output-tokens/$. Pure SQLite + local git/gh, no server, no
8
+ * telemetry — the same offline spirit as `cost`.
9
+ *
10
+ * Why not just show `token_count`? Because that number sums cache-read/-write
11
+ * context re-counted every turn and is dominated by cheap re-reads (often ~100x
12
+ * the real generation). `output_tokens` (scanned per-agent into the session DB)
13
+ * is the honest "work produced" signal, and it is what this command leads with.
14
+ *
15
+ * `--all-hosts` fans the same rollup across every online device (`ag devices`)
16
+ * over SSH and merges — one fleet-wide burn-vs-output view.
17
+ */
18
+ import type { Command } from 'commander';
19
+ export declare function registerOutputCommand(program: Command): void;
@@ -0,0 +1,333 @@
1
+ import chalk from 'chalk';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import { execFile } from 'child_process';
5
+ import { promisify } from 'util';
6
+ import { addHostOption } from '../lib/hosts/option.js';
7
+ import { discoverSessions, parseTimeFilter } from '../lib/session/discover.js';
8
+ import { queryUsageRollup } from '../lib/session/db.js';
9
+ import { formatUsd, PRICING_VERSION } from '../lib/pricing/index.js';
10
+ import { formatDuration } from '../lib/session/render.js';
11
+ import { terminalWidth, truncateToWidth, padToWidth } from '../lib/session/width.js';
12
+ import { collectGitOutput } from '../lib/output/git-output.js';
13
+ import { loadDevices } from '../lib/devices/registry.js';
14
+ import { machineId } from '../lib/session/sync/config.js';
15
+ const execFileAsync = promisify(execFile);
16
+ export function registerOutputCommand(program) {
17
+ addHostOption(program.command('output'))
18
+ .description('Productivity rollup — token burn vs shipped output (PRs, commits) across agents')
19
+ .option('--json', 'Output the rollup as JSON')
20
+ .option('--since <time>', 'Only sessions/commits newer than this: 1h, 24h, 7d, 4w, 1mo, 1y, or ISO date (default 7d)')
21
+ .option('--by <dimension>', 'Group the burn/output breakdown by: agent (default), project, or day')
22
+ .option('--repos-dir <dir>', 'Root scanned for git repos (default ~/src)')
23
+ .option('--author <email...>', 'Count commits by these author emails (default: your git identities)')
24
+ .option('--login <login...>', 'Count PRs for these GitHub logins (default: current gh user)')
25
+ .option('--no-prs', 'Skip the GitHub PR lookup (commits only)')
26
+ .option('--all-hosts', 'Aggregate across every online device (ag devices) over SSH')
27
+ .addHelpText('after', `
28
+ Examples:
29
+ agents output Last 7 days: burn, output tokens, PRs, commits, ratios
30
+ agents output --since 24h Last 24 hours
31
+ agents output --since 1mo Last month (units: 1h 24h 7d 4w 1mo 1y, or ISO date)
32
+ agents output --all-hosts Fleet-wide, folding in every online machine
33
+ agents output --by day --json Machine-readable daily burn/output rollup
34
+
35
+ Burn (cost) is computed offline from a versioned per-model price table (${PRICING_VERSION}).
36
+ Output tokens are the real generated tokens — NOT the cache-inflated total token count.
37
+ `)
38
+ .action(async (options) => {
39
+ await outputAction(options);
40
+ });
41
+ }
42
+ function resolveGroup(by) {
43
+ if (by === undefined)
44
+ return 'agent';
45
+ if (by === 'agent' || by === 'project' || by === 'day')
46
+ return by;
47
+ console.error(chalk.red('error: --by must be one of: agent, project, day'));
48
+ process.exit(1);
49
+ }
50
+ /** Compact token formatter: 38.6M, 4.1K, 10.6B. */
51
+ function formatCompact(n) {
52
+ const abs = Math.abs(n);
53
+ if (abs >= 1e9)
54
+ return `${(n / 1e9).toFixed(1)}B`;
55
+ if (abs >= 1e6)
56
+ return `${(n / 1e6).toFixed(1)}M`;
57
+ if (abs >= 1e3)
58
+ return `${(n / 1e3).toFixed(1)}K`;
59
+ return String(n);
60
+ }
61
+ function emptyBurn() {
62
+ return { costUsd: 0, outputTokens: 0, tokenCount: 0, sessionCount: 0, durationMs: 0 };
63
+ }
64
+ function sumBurn(rows) {
65
+ return rows.reduce((acc, r) => {
66
+ acc.costUsd += r.costUsd;
67
+ acc.outputTokens += r.outputTokens;
68
+ acc.tokenCount += r.tokenCount;
69
+ acc.sessionCount += r.sessionCount;
70
+ acc.durationMs += r.durationMs;
71
+ return acc;
72
+ }, emptyBurn());
73
+ }
74
+ /** Compute this machine's payload from the local session DB + git. */
75
+ async function computeLocalPayload(options, includePrs) {
76
+ const since = options.since ?? '7d';
77
+ const sinceMs = parseTimeFilter(since);
78
+ // Ensure the index is fresh (and migrated to v12 so output_tokens is populated).
79
+ await discoverSessions({ all: true, since, limit: 1 });
80
+ const filter = { sinceMs };
81
+ const groupBy = resolveGroup(options.by);
82
+ const breakdown = queryUsageRollup({ ...filter, groupBy });
83
+ const burn = sumBurn(breakdown);
84
+ const reposDir = options.reposDir ?? path.join(os.homedir(), 'src');
85
+ const git = await collectGitOutput({
86
+ reposDir,
87
+ sinceMs,
88
+ authors: options.author,
89
+ logins: options.login,
90
+ includePrs,
91
+ });
92
+ return {
93
+ machine: machineId(),
94
+ pricingVersion: PRICING_VERSION,
95
+ since,
96
+ burn,
97
+ output: {
98
+ commits: git.commits,
99
+ commitShas: git.commitShas,
100
+ prsOpened: git.prsOpened,
101
+ prsMerged: git.prsMerged,
102
+ reposScanned: git.reposScanned,
103
+ ghAvailable: git.ghAvailable,
104
+ authors: git.authors,
105
+ logins: git.logins,
106
+ },
107
+ breakdown: { by: groupBy, rows: breakdown },
108
+ uncostedAgents: groupBy === 'agent' ? breakdown.filter(r => r.costUsd === 0).map(r => r.key) : [],
109
+ };
110
+ }
111
+ /** Fetch one remote device's payload by re-invoking `agents output --json --host <name>`. */
112
+ async function fetchRemotePayload(device, options) {
113
+ const args = ['output', '--json', '--no-prs', '--host', device, '--since', options.since ?? '7d'];
114
+ if (options.by)
115
+ args.push('--by', options.by);
116
+ if (options.reposDir)
117
+ args.push('--repos-dir', options.reposDir);
118
+ for (const a of options.author ?? [])
119
+ args.push('--author', a);
120
+ try {
121
+ const { stdout } = await execFileAsync('agents', args, {
122
+ timeout: 120_000,
123
+ maxBuffer: 64 * 1024 * 1024,
124
+ });
125
+ const parsed = JSON.parse(stdout);
126
+ parsed.machine = parsed.machine || device;
127
+ return parsed;
128
+ }
129
+ catch (err) {
130
+ return {
131
+ machine: device,
132
+ pricingVersion: PRICING_VERSION,
133
+ since: options.since ?? '7d',
134
+ burn: emptyBurn(),
135
+ output: { commits: 0, commitShas: [], prsOpened: 0, prsMerged: 0, reposScanned: 0, ghAvailable: false, authors: [], logins: [] },
136
+ breakdown: { by: resolveGroup(options.by), rows: [] },
137
+ uncostedAgents: [],
138
+ error: (err?.stderr || err?.message || 'unreachable').toString().split('\n')[0].slice(0, 120),
139
+ };
140
+ }
141
+ }
142
+ async function outputAction(options) {
143
+ const includePrs = options.prs !== false;
144
+ if (!options.allHosts) {
145
+ const payload = await computeLocalPayload(options, includePrs);
146
+ if (options.json) {
147
+ process.stdout.write(JSON.stringify(withRatios(payload, [payload]), null, 2) + '\n');
148
+ return;
149
+ }
150
+ renderSingle(payload);
151
+ return;
152
+ }
153
+ // Fleet: local payload + every online device, folded in over SSH.
154
+ const self = machineId();
155
+ const registry = await loadDevices();
156
+ const remotes = Object.values(registry)
157
+ .filter(d => d.tailscale?.online && d.name !== self)
158
+ .map(d => d.name);
159
+ if (!options.json)
160
+ console.error(chalk.gray(`Folding in ${remotes.length} online device${remotes.length !== 1 ? 's' : ''}…`));
161
+ // PRs are global (gh search by author, not machine-bound) — compute once, locally.
162
+ const local = await computeLocalPayload(options, includePrs);
163
+ const remotePayloads = await Promise.all(remotes.map(d => fetchRemotePayload(d, options)));
164
+ const machines = [local, ...remotePayloads];
165
+ if (options.json) {
166
+ process.stdout.write(JSON.stringify(withRatios(mergeMachines(machines, options), machines), null, 2) + '\n');
167
+ return;
168
+ }
169
+ renderFleet(machines, options);
170
+ }
171
+ /** Merge per-machine payloads into a combined one (burn + commits summed; PRs local-only). */
172
+ function mergeMachines(machines, options) {
173
+ const burn = emptyBurn();
174
+ const byKey = new Map();
175
+ // Union commit SHAs across machines: shared repos (cloned on several boxes)
176
+ // expose the same commits to `git log` on each, so summing counts would
177
+ // multi-count. A commit's SHA is its global identity — union, don't add.
178
+ const allShas = new Set();
179
+ const uncosted = new Set();
180
+ for (const m of machines) {
181
+ burn.costUsd += m.burn.costUsd;
182
+ burn.outputTokens += m.burn.outputTokens;
183
+ burn.tokenCount += m.burn.tokenCount;
184
+ burn.sessionCount += m.burn.sessionCount;
185
+ burn.durationMs += m.burn.durationMs;
186
+ for (const s of m.output.commitShas)
187
+ allShas.add(s);
188
+ for (const a of m.uncostedAgents)
189
+ uncosted.add(a);
190
+ for (const r of m.breakdown.rows) {
191
+ const cur = byKey.get(r.key) ?? { key: r.key, costUsd: 0, durationMs: 0, sessionCount: 0, tokenCount: 0, outputTokens: 0 };
192
+ cur.costUsd += r.costUsd;
193
+ cur.durationMs += r.durationMs;
194
+ cur.sessionCount += r.sessionCount;
195
+ cur.tokenCount += r.tokenCount;
196
+ cur.outputTokens += r.outputTokens;
197
+ byKey.set(r.key, cur);
198
+ }
199
+ }
200
+ const rows = [...byKey.values()].sort((a, b) => b.costUsd - a.costUsd);
201
+ // PRs from the local machine only (machines[0]) — gh search is not machine-scoped.
202
+ const localGit = machines[0].output;
203
+ return {
204
+ machine: 'fleet',
205
+ pricingVersion: PRICING_VERSION,
206
+ since: options.since ?? '7d',
207
+ burn,
208
+ output: { ...localGit, commits: allShas.size, commitShas: [...allShas] },
209
+ breakdown: { by: resolveGroup(options.by), rows },
210
+ uncostedAgents: [...uncosted],
211
+ };
212
+ }
213
+ /** Attach burn-vs-output ratios to a payload for JSON output. */
214
+ function withRatios(payload, machines) {
215
+ const prsTotal = payload.output.prsOpened + payload.output.prsMerged;
216
+ return {
217
+ ...payload,
218
+ ratios: {
219
+ costPerPr: prsTotal > 0 ? payload.burn.costUsd / prsTotal : null,
220
+ costPerCommit: payload.output.commits > 0 ? payload.burn.costUsd / payload.output.commits : null,
221
+ outputTokensPerUsd: payload.burn.costUsd > 0 ? payload.burn.outputTokens / payload.burn.costUsd : null,
222
+ },
223
+ machines: machines.length > 1 ? machines.map(m => ({ machine: m.machine, burn: m.burn, commits: m.output.commits, error: m.error })) : undefined,
224
+ };
225
+ }
226
+ /** Shared header line: burned · output tokens · PRs · commits, plus ratios. */
227
+ function headerLines(payload) {
228
+ const prsTotal = payload.output.prsOpened + payload.output.prsMerged;
229
+ const out = [];
230
+ out.push(' ' +
231
+ `${chalk.green(formatUsd(payload.burn.costUsd))} burned` +
232
+ chalk.gray(' · ') +
233
+ `${chalk.cyan(formatCompact(payload.burn.outputTokens))} output tokens` +
234
+ chalk.gray(' · ') +
235
+ `${chalk.yellow(String(prsTotal))} PRs ${chalk.gray(`(${payload.output.prsMerged} merged)`)}` +
236
+ chalk.gray(' · ') +
237
+ `${chalk.yellow(String(payload.output.commits))} commits`);
238
+ const ratios = [];
239
+ if (prsTotal > 0)
240
+ ratios.push(`${formatUsd(payload.burn.costUsd / prsTotal)}/PR`);
241
+ if (payload.output.commits > 0)
242
+ ratios.push(`${formatUsd(payload.burn.costUsd / payload.output.commits)}/commit`);
243
+ if (payload.burn.costUsd > 0)
244
+ ratios.push(`${formatCompact(Math.round(payload.burn.outputTokens / payload.burn.costUsd))} out-tok/$`);
245
+ if (ratios.length > 0)
246
+ out.push(chalk.gray(' ' + ratios.join(' · ')));
247
+ return out;
248
+ }
249
+ /** Render the per-group burn/output table. */
250
+ function renderBreakdown(rows, groupBy) {
251
+ const out = [chalk.bold(`By ${groupBy}`)];
252
+ if (rows.length === 0)
253
+ return out;
254
+ const cols = terminalWidth();
255
+ const burnW = Math.max(...rows.map(r => formatUsd(r.costUsd).length), 4);
256
+ const outW = Math.max(...rows.map(r => formatCompact(r.outputTokens).length), 6);
257
+ const sessW = Math.max(...rows.map(r => String(r.sessionCount).length), 3);
258
+ const fixedW = 2 + 2 + burnW + 2 + outW + 2 + sessW + 8;
259
+ const keyW = Math.max(8, Math.min(Math.max(...rows.map(r => r.key.length), groupBy.length), cols - fixedW));
260
+ out.push(' ' + chalk.gray(padToWidth('', keyW)) + ' ' + chalk.gray(padToWidth('burn', burnW)) + ' ' + chalk.gray(padToWidth('output', outW)) + ' ' + chalk.gray('sessions'));
261
+ for (const r of rows) {
262
+ out.push(' ' +
263
+ padToWidth(truncateToWidth(r.key, keyW), keyW) +
264
+ ' ' +
265
+ chalk.green(padToWidth(formatUsd(r.costUsd), burnW)) +
266
+ ' ' +
267
+ chalk.cyan(padToWidth(formatCompact(r.outputTokens), outW)) +
268
+ ' ' +
269
+ chalk.gray(padToWidth(String(r.sessionCount), sessW)));
270
+ }
271
+ return out;
272
+ }
273
+ function renderSingle(payload) {
274
+ const out = [];
275
+ out.push(chalk.bold('Output') + chalk.gray(` · pricing ${payload.pricingVersion} · since ${payload.since}`));
276
+ out.push(...headerLines(payload));
277
+ out.push('');
278
+ if (payload.burn.sessionCount === 0) {
279
+ out.push(chalk.gray('No sessions with cost data found. Run `agents sessions --all` to index, then retry.'));
280
+ console.log(out.join('\n'));
281
+ return;
282
+ }
283
+ out.push(...renderBreakdown(payload.breakdown.rows, payload.breakdown.by));
284
+ out.push('');
285
+ out.push(chalk.bold('Shipped'));
286
+ out.push(` ${chalk.yellow(String(payload.output.commits))} commits across ${payload.output.reposScanned} repos` + chalk.gray(` (authors: ${payload.output.authors.length > 0 ? payload.output.authors.join(', ') : 'none detected'})`));
287
+ if (payload.output.ghAvailable) {
288
+ out.push(` ${chalk.yellow(String(payload.output.prsOpened))} PRs opened, ${chalk.yellow(String(payload.output.prsMerged))} merged` + chalk.gray(` (logins: ${payload.output.logins.join(', ')})`));
289
+ }
290
+ else {
291
+ out.push(chalk.gray(' PRs: gh unavailable or unauthed — not counted'));
292
+ }
293
+ out.push('');
294
+ out.push(chalk.gray(notCountedLine(payload.uncostedAgents)));
295
+ if (payload.burn.durationMs > 0)
296
+ out.push(chalk.gray(`agent wall-clock: ${formatDuration(payload.burn.durationMs)}`));
297
+ console.log(out.join('\n'));
298
+ }
299
+ function renderFleet(machines, options) {
300
+ const merged = mergeMachines(machines, options);
301
+ const out = [];
302
+ out.push(chalk.bold('Output') + chalk.gray(` · fleet (${machines.length} machines) · pricing ${merged.pricingVersion} · since ${merged.since}`));
303
+ out.push(...headerLines(merged));
304
+ out.push('');
305
+ // By machine.
306
+ out.push(chalk.bold('By machine'));
307
+ const nameW = Math.max(...machines.map(m => m.machine.length), 7);
308
+ const burnW = Math.max(...machines.map(m => formatUsd(m.burn.costUsd).length), 4);
309
+ for (const m of machines) {
310
+ const note = m.error ? chalk.red(` (${m.error})`) : '';
311
+ out.push(' ' +
312
+ padToWidth(m.machine, nameW) +
313
+ ' ' +
314
+ chalk.green(padToWidth(formatUsd(m.burn.costUsd), burnW)) +
315
+ ' ' +
316
+ chalk.cyan(padToWidth(formatCompact(m.burn.outputTokens), 7)) +
317
+ ' ' +
318
+ chalk.gray(`${m.burn.sessionCount} sessions, ${m.output.commits} commits`) +
319
+ note);
320
+ }
321
+ out.push('');
322
+ out.push(...renderBreakdown(merged.breakdown.rows, merged.breakdown.by));
323
+ out.push('');
324
+ out.push(chalk.gray(notCountedLine(merged.uncostedAgents)));
325
+ console.log(out.join('\n'));
326
+ }
327
+ function notCountedLine(uncosted) {
328
+ const notes = [];
329
+ if (uncosted.length > 0)
330
+ notes.push(`no price table for: ${[...new Set(uncosted)].join(', ')} (burn undercounts)`);
331
+ notes.push('cloud runs (Rush/Codex/Factory) not counted');
332
+ return 'not counted: ' + notes.join(' · ');
333
+ }
@@ -328,12 +328,12 @@ async function browseRemote(targets, args, tty) {
328
328
  if (tty) {
329
329
  for (const t of targets) {
330
330
  const target = await resolveSshTarget(t);
331
- render(t, remoteSecretsRaw(target, args, { tty: true }));
331
+ render(t, remoteSecretsRaw(target, args, { tty: true, osLookupName: t }));
332
332
  }
333
333
  }
334
334
  else {
335
- const resolved = await Promise.all(targets.map((t) => resolveSshTarget(t)));
336
- const results = resolved.map((target) => remoteSecretsRaw(target, args));
335
+ const resolved = await Promise.all(targets.map(async (t) => ({ name: t, target: await resolveSshTarget(t) })));
336
+ const results = resolved.map(({ name, target }) => remoteSecretsRaw(target, args, { osLookupName: name }));
337
337
  targets.forEach((t, i) => render(t, results[i]));
338
338
  }
339
339
  if (failures > 0)
@@ -1476,7 +1476,7 @@ Examples:
1476
1476
  // engine (BatchMode, ConnectTimeout, keepalive, control-socket
1477
1477
  // reuse) via the same path the READ inverse (`remoteResolveEnv`)
1478
1478
  // uses. `--from -` reads the .env off ssh stdin.
1479
- res = remoteSecretsRaw(host, ['import', resolvedBundleName, '--from', '-', ...(opts.force ? ['--force'] : [])], { input: dotenv });
1479
+ res = remoteSecretsRaw(host, ['import', resolvedBundleName, '--from', '-', ...(opts.force ? ['--force'] : [])], { input: dotenv, osLookupName: host });
1480
1480
  }
1481
1481
  if (res.code === null) {
1482
1482
  failures++;
@@ -1584,7 +1584,7 @@ Examples:
1584
1584
  // locally instead.
1585
1585
  const { assertRemoteBundleFlagsUnsupported } = await import('../lib/secrets/bundles.js');
1586
1586
  assertRemoteBundleFlagsUnsupported(bundleName, execOpts.host, { keys: keysSubset, allowExpired: execOpts.allowExpired }, { keysFlag: '--keys', allowExpiredFlag: '--allow-expired' });
1587
- secretEnv = await remoteResolveEnv(await resolveSshTarget(execOpts.host), bundleName);
1587
+ secretEnv = await remoteResolveEnv(await resolveSshTarget(execOpts.host), bundleName, { osLookupName: execOpts.host });
1588
1588
  }
1589
1589
  else {
1590
1590
  const { readAndResolveBundleEnv } = await import('../lib/secrets/bundles.js');
@@ -1738,7 +1738,7 @@ Examples:
1738
1738
  // sees a real TTY, which requires our local terminal to pass straight
1739
1739
  // through. The remote's prompt + output stream to this terminal; we get
1740
1740
  // back only the exit code.
1741
- const code = remoteSecretsStream(target, unlockArgs);
1741
+ const code = remoteSecretsStream(target, unlockArgs, { osLookupName: h });
1742
1742
  if (code === 0) {
1743
1743
  console.log(chalk.green(`${h}: unlocked`));
1744
1744
  }
package/dist/index.js CHANGED
@@ -50,7 +50,7 @@ if (IS_DEV_BUILD) {
50
50
  // module on each invocation (which loaded the whole ~50-module tree before the
51
51
  // first byte of output), the registry maps a command name to a thunk that
52
52
  // imports only what that command needs. See src/lib/startup/command-registry.ts.
53
- import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadFeed, } from './lib/startup/command-registry.js';
53
+ import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadFeed, } from './lib/startup/command-registry.js';
54
54
  import { applyGlobalHelpConventions } from './lib/help.js';
55
55
  import { renderWhatsNew } from './lib/whats-new.js';
56
56
  import { emit, redactArgs } from './lib/events.js';
@@ -690,6 +690,7 @@ async function registerAllEagerCommands() {
690
690
  await reg(loadFactory);
691
691
  await reg(loadUsage);
692
692
  await reg(loadCost);
693
+ await reg(loadOutput);
693
694
  await reg(loadBudget);
694
695
  await reg(loadAlias);
695
696
  await reg(loadPty);