@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.
- package/CHANGELOG.md +23 -1
- package/README.md +15 -7
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +39 -2
- package/dist/commands/output.d.ts +19 -0
- package/dist/commands/output.js +333 -0
- package/dist/commands/secrets.js +6 -6
- package/dist/index.js +2 -1
- package/dist/lib/agents.js +19 -13
- package/dist/lib/hosts/dispatch.d.ts +36 -0
- package/dist/lib/hosts/dispatch.js +40 -2
- package/dist/lib/hosts/passthrough.js +1 -0
- package/dist/lib/mcp.js +1 -1
- package/dist/lib/output/git-output.d.ts +74 -0
- package/dist/lib/output/git-output.js +213 -0
- package/dist/lib/permissions.d.ts +26 -5
- package/dist/lib/permissions.js +212 -37
- package/dist/lib/plugins.d.ts +8 -0
- package/dist/lib/plugins.js +108 -0
- package/dist/lib/project-root.js +2 -1
- package/dist/lib/resources/mcp.js +1 -1
- package/dist/lib/resources/permissions.d.ts +1 -1
- package/dist/lib/resources/permissions.js +8 -2
- package/dist/lib/resources/skills.js +6 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/routines.d.ts +16 -0
- package/dist/lib/routines.js +46 -1
- package/dist/lib/secrets/remote.d.ts +7 -2
- package/dist/lib/secrets/remote.js +11 -10
- package/dist/lib/session/db.d.ts +3 -0
- package/dist/lib/session/db.js +20 -4
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +40 -4
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/shims.js +13 -3
- package/dist/lib/skills.js +14 -1
- package/dist/lib/staleness/detectors/permissions.js +50 -3
- package/dist/lib/staleness/detectors/subagents.js +31 -12
- package/dist/lib/staleness/detectors/workflows.js +33 -0
- package/dist/lib/staleness/writers/commands.js +3 -3
- package/dist/lib/staleness/writers/subagents.js +13 -5
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/subagents.d.ts +11 -1
- package/dist/lib/subagents.js +117 -26
- package/dist/lib/versions.js +12 -2
- package/dist/lib/workflows.d.ts +5 -3
- package/dist/lib/workflows.js +246 -9
- 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
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
183
|
-
last-resort
|
|
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 |
|
|
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,
|
|
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
|
package/dist/commands/exec.js
CHANGED
|
@@ -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('@')
|
|
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
|
+
}
|
package/dist/commands/secrets.js
CHANGED
|
@@ -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);
|