@phnx-labs/agents-cli 1.20.57 → 1.20.59
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 +30 -0
- package/README.md +40 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/defaults.js +24 -0
- package/dist/commands/exec.js +29 -5
- package/dist/commands/output.d.ts +19 -0
- package/dist/commands/output.js +333 -0
- package/dist/commands/secrets.js +34 -25
- package/dist/commands/versions.js +11 -3
- package/dist/commands/view.js +19 -4
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +42 -14
- package/dist/lib/daemon.d.ts +5 -5
- package/dist/lib/daemon.js +65 -17
- package/dist/lib/git.d.ts +9 -0
- package/dist/lib/git.js +12 -0
- package/dist/lib/hosts/dispatch.d.ts +21 -0
- package/dist/lib/hosts/dispatch.js +88 -5
- 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 +31 -1
- package/dist/lib/permissions.js +210 -9
- package/dist/lib/project-root.d.ts +65 -0
- package/dist/lib/project-root.js +134 -0
- package/dist/lib/resources/mcp.js +1 -1
- package/dist/lib/resources/permissions.js +5 -1
- package/dist/lib/resources/skills.js +6 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/secrets/agent.d.ts +26 -23
- package/dist/lib/secrets/agent.js +196 -216
- package/dist/lib/secrets/remote.d.ts +7 -2
- package/dist/lib/secrets/remote.js +12 -10
- package/dist/lib/session/active.d.ts +3 -0
- package/dist/lib/session/active.js +1 -0
- 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/parse.js +38 -15
- package/dist/lib/session/state.d.ts +4 -1
- package/dist/lib/session/state.js +18 -1
- package/dist/lib/session/types.d.ts +10 -0
- package/dist/lib/staleness/detectors/permissions.js +64 -1
- package/dist/lib/staleness/detectors/subagents.js +30 -0
- package/dist/lib/staleness/writers/commands.js +3 -3
- package/dist/lib/staleness/writers/subagents.js +13 -1
- 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 +23 -0
- package/dist/lib/subagents.js +161 -12
- package/dist/lib/teams/agents.d.ts +14 -0
- package/dist/lib/teams/agents.js +158 -22
- package/dist/lib/types.d.ts +13 -0
- package/dist/lib/versions.d.ts +39 -0
- package/dist/lib/versions.js +199 -12
- package/package.json +1 -1
- package/scripts/postinstall.js +26 -11
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.20.59
|
|
6
|
+
|
|
7
|
+
- **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)
|
|
8
|
+
- **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`.
|
|
9
|
+
- **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`.
|
|
10
|
+
- **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`.
|
|
11
|
+
- **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`.
|
|
12
|
+
- **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`.
|
|
13
|
+
- **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`.
|
|
14
|
+
- **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`.
|
|
15
|
+
## 1.20.58
|
|
16
|
+
|
|
17
|
+
- **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)
|
|
18
|
+
- **Stopped teammate resumes are transactional from launch through persistence.** If a local or remote resume fails, the existing teammate record, directory, runtime metadata, stdout mirror, and log cursor are restored; any replacement wrapper and its descendants are terminated as one process group. A successful resume whose log was truncated restarts parsing at byte zero, and a secondary restore-write failure retains the original launch error as its cause. Source: `apps/cli/src/lib/teams/agents.ts`, `apps/cli/src/lib/hosts/dispatch.ts`. (#1104, #1108)
|
|
19
|
+
- **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)
|
|
20
|
+
- **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`.
|
|
21
|
+
- **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`.
|
|
22
|
+
- **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)
|
|
23
|
+
- **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`.
|
|
24
|
+
- **`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`.
|
|
25
|
+
- **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`).
|
|
26
|
+
|
|
27
|
+
- **Clarify the native escape hatch behind `--mode skip`.** The README and bundled `run` skill now discourage `skip`, list its exact direct-exec per-harness flag mappings and ACP `allow_always` behavior, replace an older recommendation of unsafe `full` for ordinary writes, and distinguish Codex `auto` (sandboxed `edit`, which can still prompt) from Codex `skip` (`--dangerously-bypass-approvals-and-sandbox`, equivalent to unsandboxed `--yolo`). Documentation only; runtime behavior is unchanged. Source: `README.md`, `skills/run/SKILL.md`.
|
|
28
|
+
- **Wire allowlist support for Kiro CLI.** Kiro 2.8.0+ permission groups now sync into `~/.kiro/settings/permissions.yaml` as v3 capability rules for shell, filesystem, and web access; existing user-authored rules are preserved and duplicate generated rules are removed. Source: `apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/permissions.ts`, `apps/cli/src/lib/staleness/detectors/permissions.ts`. (RUSH-1392)
|
|
29
|
+
- **Fix: remote `agents secrets view <bundle>@host --reveal` no longer leaves a 60-second SSH control master behind.** The interactive `-tt` reveal path now opts out of default SSH multiplexing (`multiplex: false`), matching the transport guidance for one-shot commands that must not keep a `ControlPersist` socket open after a Touch ID/passphrase reveal. Source: `apps/cli/src/lib/secrets/remote.ts` (`remoteSecretsRaw`).
|
|
30
|
+
- **`agents run --host <host> --cwd <dir>` now sets the working directory ON the host (and a new `--project` shorthand jumps to a project by name).** Previously `--cwd` was silently dropped for `--host` runs — only the separate `--remote-cwd` flag worked — so `agents run claude --host s1 --cwd ~/src/foo` landed in the remote login-shell's default directory with no warning. `--cwd` is now forwarded as the host working directory, and a home-anchored path (`~/…`, `$HOME/…`, or a local-home absolute the shell already expanded like `/Users/me/…`) is re-rooted at the *remote* `$HOME` so it resolves correctly across machines with different home paths (`/Users/me` → `/home/me`). `--remote-cwd` remains as the explicit override. New `-P, --project <slug>[@worktree]` resolves a bare project name against your projects root (e.g. `~/src/github.com/<user>`) — auto-inferred from the repo you launch inside and cached in `agents.yaml`, or set/shown with `agents defaults project-root [path]`; `--project foo@fix` targets the `fix` git worktree. Works for local and `--host` runs. Verified end-to-end: `agents run claude --host yosemite-s1 --project agents-cli` runs the remote agent with `pwd` = `/home/muqsit/src/github.com/muqsitnawaz/agents-cli`. Source: `apps/cli/src/lib/project-root.ts` (new), `apps/cli/src/lib/hosts/dispatch.ts` (`remoteCdPrefix`), `apps/cli/src/commands/exec.ts` (`--project`/`--cwd` host wiring), `apps/cli/src/commands/defaults.ts` (`project-root`).
|
|
31
|
+
- **Fix daemon crash-looping when its pinned Node version is pruned (fleet-wide).** The routine daemon's launchd/systemd manifest hardcoded `~/.nvm/versions/node/v24.0.0/bin` on PATH, and launched the CLI entry bare when it was an extension-less shim or a `bin/agents → dist/index.js` symlink (an extension check on the link name missed it). The moment that exact nvm patch was upgraded away, the shim's `#!/usr/bin/env node` shebang fell through to an ancient system node (Node 18 → `SyntaxError: node:util has no export 'styleText'` from `@inquirer/core`), and the service crash-looped at import — observed at 100k+ restarts on Linux workers, silently killing all scheduled routines. `getDaemonLaunch` now detects Node-script entries by resolving symlinks and sniffing the shebang (not just the `.js`/`.cjs`/`.mjs` extension), so it pins them to `process.execPath`; and the generated PATH now leads with `path.dirname(process.execPath)` — the Node that installed the service — instead of a hardcoded nvm version, so both the shim and child routine processes always resolve a working runtime. Source: `apps/cli/src/lib/daemon.ts` (`getDaemonLaunch`, `isNodeScriptEntry`, `daemonNodeBinDir`, `generateSystemdUnit`, `generateLaunchdPlist`).
|
|
32
|
+
- **Fix global npm upgrades restarting the routines daemon through `scripts/postinstall.js`.** The postinstall process is itself `process.argv[1]`, so its daemon self-heal could stamp `node scripts/postinstall.js daemon _run` into launchd. Daemon startup now accepts an explicit CLI entry and postinstall passes the resolved signed native binary (or JavaScript entrypoint), with the same value threaded through launchd, systemd, and detached startup. Source: `apps/cli/scripts/postinstall.js`, `apps/cli/src/lib/daemon.ts`.
|
|
33
|
+
- **Fix a standalone secrets service stealing the daemon-hosted broker socket during postinstall.** The standalone and hosted brokers now bind through one race-safe owner arbitration path: an existing reachable broker wins without its socket being unlinked, a persistent losing service stays quiescent instead of triggering launchd restart churn, takes over if the owner stops, and releases its standby PID on service shutdown; only an unreachable stale socket is reclaimed. This covers the release ordering where postinstall restarts the daemon first and then kickstarts an installed standalone service. Source: `apps/cli/src/lib/secrets/agent.ts` (`bindBrokerSocket`, `runSecretsAgent`, `startHostedBroker`).
|
|
34
|
+
|
|
5
35
|
## 1.20.57
|
|
6
36
|
|
|
7
37
|
- **`agents teams resume` / `agents teams message` — resume a stopped teammate with a follow-up message.** A teammate that ended its turn with more to do (PR open awaiting review, headless turn cap, a redirect after the fact) could not be reached: `agents message` resolves only *live* sessions, so a completed/stopped/failed teammate had no path back short of finishing the work by hand or spawning a fresh, context-less teammate. `teams resume <team> <teammate> <message>` re-enters the teammate's **own** session with the message as the next user turn, re-launching through the same backend (local process or remote host) in its original worktree and flipping it back to `running` so `teams status` tracks it live. `teams message` is the same command with automatic routing by reconciled status: a **running** teammate is steered via its mailbox (delivered at its next tool call, no re-launch); a **stopped** one is resumed; a **pending** one is refused with a pointer to `teams start`. Works for every harness — the resume delegates to `agents run --resume`, inheriting native resume for Claude/Codex and the universal `/continue` replay for the rest (OpenCode, Grok, Kimi, …); the resume target is the teammate's captured underlying session id (`remoteSessionId ?? agentId`), and a non-Claude teammate that died before emitting a session id is refused with a clear error rather than resumed into a fresh run. This also makes good on `teams stop`'s long-standing "can be restarted later" promise, which no code implemented. Source: `apps/cli/src/commands/teams.ts` (`message`/`resume` subcommands, `decideTeamMessageRoute`), `apps/cli/src/lib/teams/agents.ts` (`AgentManager.resumeTeammate`, resume-aware `buildRunArgv`/`buildCommand`/`launchProcess`/`launchRemoteProcess`).
|
package/README.md
CHANGED
|
@@ -155,7 +155,37 @@ agents run claude "Review PRs merged this week, summarize risks" \
|
|
|
155
155
|
| agents run codex "Write regression tests for the top 3 risks"
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
-
Supports plan (read-only) and
|
|
158
|
+
Supports plan (read-only), edit, auto, and skip modes, effort levels, JSON output for scripting, and timeout limits.
|
|
159
|
+
|
|
160
|
+
### What does `--mode skip` actually do?
|
|
161
|
+
|
|
162
|
+
Treat `skip` as a last-resort escape hatch. In direct-exec runs (without `--acp`),
|
|
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`.
|
|
167
|
+
|
|
168
|
+
| Harness | Direct-exec `--mode skip` becomes |
|
|
169
|
+
|---|---|
|
|
170
|
+
| Claude Code | `--dangerously-skip-permissions` |
|
|
171
|
+
| Codex | `--dangerously-bypass-approvals-and-sandbox` (equivalent to `--yolo`) |
|
|
172
|
+
| Gemini | `--yolo` |
|
|
173
|
+
| Cursor | `-f` |
|
|
174
|
+
| OpenClaw | `--mode full` |
|
|
175
|
+
| GitHub Copilot | `--allow-all` (alias: `--yolo`) |
|
|
176
|
+
| Antigravity | `--dangerously-skip-permissions` |
|
|
177
|
+
| Grok | `--always-approve` |
|
|
178
|
+
| Kimi | `--yolo` interactively; no extra flag in headless `-p` runs, which already auto-approve |
|
|
179
|
+
| Droid | `--skip-permissions-unsafe` |
|
|
180
|
+
|
|
181
|
+
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
|
+
|
|
185
|
+
Codex has no native smart-classifier mode, so `agents run codex --mode auto` resolves
|
|
186
|
+
to sandboxed `edit` and can still prompt. `agents run codex --mode skip` is different:
|
|
187
|
+
it bypasses approvals **and** removes the sandbox. `full` remains an alias for `skip`,
|
|
188
|
+
but new scripts should use the explicit `skip` name.
|
|
159
189
|
|
|
160
190
|
### One protocol, every harness
|
|
161
191
|
|
|
@@ -432,7 +462,7 @@ tools:
|
|
|
432
462
|
---
|
|
433
463
|
```
|
|
434
464
|
|
|
435
|
-
Workflows that need to write — post PR comments, edit files, send Slack — should run with `--mode edit
|
|
465
|
+
Workflows that need to write — post PR comments, edit files, send Slack — should run with `--mode edit`, or `--mode auto` on Claude Code and GitHub Copilot. Reserve `--mode skip` (legacy alias: `full`) for last-resort bypasses. `agents run` defaults to `--mode plan` (read-only), which deadlocks at `ExitPlanMode` in headless runs.
|
|
436
466
|
|
|
437
467
|
Resolution is project > user > system: a `<repo>/.agents/workflows/<name>/` overrides a same-named workflow in `~/.agents/workflows/`. Commit project workflows with your repo so teammates get the same pipeline.
|
|
438
468
|
|
|
@@ -825,9 +855,10 @@ Which DotAgents resources each agent CLI can load. Source of truth: [src/lib/age
|
|
|
825
855
|
| OpenCode | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
|
|
826
856
|
| Copilot | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
|
|
827
857
|
| Amp | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
|
|
828
|
-
| Kiro | yes | no | yes |
|
|
858
|
+
| Kiro | yes | no | yes | >= 2.8.0 | yes | yes | no | >= 1.23.0 | `AGENTS.md` | no |
|
|
829
859
|
| Goose | yes | no | yes | no | no | no | no | no | `AGENTS.md` | no |
|
|
830
860
|
| Roo Code | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
|
|
861
|
+
| Droid | yes | yes | yes | >= 0.57.5 | >= 0.26.0 | yes | yes | yes | `AGENTS.md` | no |
|
|
831
862
|
|
|
832
863
|
**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.
|
|
833
864
|
|
|
@@ -845,14 +876,19 @@ Which DotAgents resources each agent CLI can load. Source of truth: [src/lib/age
|
|
|
845
876
|
| Grok Build | -- | yes | yes |
|
|
846
877
|
| Antigravity | -- | yes | -- |
|
|
847
878
|
| Copilot | -- | -- | yes |
|
|
848
|
-
| OpenClaw, Amp,
|
|
879
|
+
| OpenClaw, Amp, Goose, Roo | -- | -- | -- |
|
|
880
|
+
| Kiro | -- | -- | -- |
|
|
849
881
|
|
|
850
882
|
### Version-gated sync
|
|
851
883
|
|
|
852
884
|
| Capability | Agent | Gate |
|
|
853
885
|
|------------|-------|------|
|
|
854
886
|
| Hooks | Codex | >= 0.116.0 |
|
|
887
|
+
| Subagents | Kiro | >= 1.23.0 |
|
|
888
|
+
| Skills | Droid | >= 0.26.0 |
|
|
889
|
+
| Permissions | Droid | >= 0.57.5 |
|
|
855
890
|
| Hooks | Gemini | >= 0.26.0 |
|
|
891
|
+
| Permissions | Kiro | >= 2.8.0 |
|
|
856
892
|
| File-based commands | Codex | < 0.117.0 (0.117+ uses command-as-skill) |
|
|
857
893
|
| Plugins | Codex | >= 0.128.0 |
|
|
858
894
|
|
package/dist/bin/agents
CHANGED
|
Binary file
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { setHelpSections } from '../lib/help.js';
|
|
8
8
|
import { listRunDefaults, setRunDefault, unsetRunDefault, } from '../lib/run-defaults.js';
|
|
9
|
+
import { getProjectRoot, setProjectRoot } from '../lib/project-root.js';
|
|
9
10
|
function formatRunDefault(entry) {
|
|
10
11
|
const parts = [];
|
|
11
12
|
if (entry.defaults.mode)
|
|
@@ -86,4 +87,27 @@ export function registerDefaultsCommands(program) {
|
|
|
86
87
|
process.exit(1);
|
|
87
88
|
}
|
|
88
89
|
});
|
|
90
|
+
defaults
|
|
91
|
+
.command('project-root [path]')
|
|
92
|
+
.description('Show or set the projects root for `agents run --project` (auto-inferred when unset)')
|
|
93
|
+
.action((rootPath) => {
|
|
94
|
+
try {
|
|
95
|
+
if (!rootPath) {
|
|
96
|
+
const current = getProjectRoot();
|
|
97
|
+
if (current) {
|
|
98
|
+
console.log(`Projects root: ${chalk.white(current)}`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.log(chalk.gray('Projects root not set — auto-inferred and cached on first `--project` use.'));
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const stored = setProjectRoot(rootPath);
|
|
106
|
+
console.log(chalk.green(`Set projects root: ${stored}`));
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
console.error(chalk.red(err.message));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
89
113
|
}
|
package/dist/commands/exec.js
CHANGED
|
@@ -211,7 +211,8 @@ export function registerRunCommand(program) {
|
|
|
211
211
|
.option('--no-auto-secrets', 'Skip auto-injection of secrets declared by a workflow\'s frontmatter `secrets:` field. Has no effect on bare-agent runs.')
|
|
212
212
|
.option('--secrets-keys <keys>', 'Inject only this comma-separated subset of keys from --secrets bundles (e.g. KEY1,KEY2). Missing keys are an error. Applies to all --secrets bundles on this run.')
|
|
213
213
|
.option('--allow-expired', 'Inject secrets even if their expiry date has passed (overrides the pre-run expiry abort).')
|
|
214
|
-
.option('--cwd <dir>', 'Working directory for the agent (defaults to current directory)')
|
|
214
|
+
.option('--cwd <dir>', 'Working directory for the agent (defaults to current directory). With --host, the directory ON the host.')
|
|
215
|
+
.option('-P, --project <ref>', 'Project shorthand <slug>[@worktree], resolved against your projects root (auto-inferred, cached). Sets the cwd locally or on --host.')
|
|
215
216
|
.option('--add-dir <dir>', 'Grant access to an additional directory outside the project (Claude and Codex, repeatable)', (val, prev) => [...prev, val], [])
|
|
216
217
|
.option('--json', 'Stream events as JSON lines (for parsing by other tools)')
|
|
217
218
|
.option('--quiet', 'Suppress preamble (rotation banner, "Running:" line). Useful when piping JSON events to a parser.', false)
|
|
@@ -238,7 +239,7 @@ export function registerRunCommand(program) {
|
|
|
238
239
|
.option('--interval <dur>', 'Loop delay between iterations ("0" back-to-back, "30m" paces). Loop only.')
|
|
239
240
|
.option('--host <name>', 'Offload this run onto another machine over SSH instead of running locally — a device, a registered agent host, or user@host. See `agents devices` / `agents hosts`.')
|
|
240
241
|
.option('--device <name>', 'Alias of --host: offload this run onto a registered device (from `agents devices`).')
|
|
241
|
-
.option('--remote-cwd <dir>', '
|
|
242
|
+
.option('--remote-cwd <dir>', 'Explicit host working directory for --host runs (overrides --cwd; usually --cwd suffices).')
|
|
242
243
|
.option('--no-follow', 'With --host, dispatch detached and return immediately (track via `agents hosts ps/logs`).')
|
|
243
244
|
.option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match.')
|
|
244
245
|
.option('--lease [backend]', 'Invent a disposable cloud box for this run and tear it down after (via crabbox). Optional backend selects the cloud (hetzner/aws/do). Unlike --host, no machine is registered.')
|
|
@@ -454,6 +455,24 @@ export function registerRunCommand(program) {
|
|
|
454
455
|
// --host/--on/--computer: offload this run onto a registered agent host
|
|
455
456
|
// over SSH instead of running locally. The three flags are aliases.
|
|
456
457
|
const hostGiven = [options.host, options.device, options.on, options.computer].filter((v) => !!v);
|
|
458
|
+
// --project <slug>[@worktree]: resolve the projects-root shorthand into a
|
|
459
|
+
// cwd. On a host run it resolves home-relative (`~/…`, so the host expands
|
|
460
|
+
// it); locally it becomes an absolute path. It owns the working directory,
|
|
461
|
+
// so it is mutually exclusive with both --cwd and --remote-cwd.
|
|
462
|
+
if (options.project) {
|
|
463
|
+
if (options.cwd || options.remoteCwd) {
|
|
464
|
+
console.error(chalk.red('Pass --project alone — not with --cwd or --remote-cwd.'));
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
const { resolveProjectRef } = await import('../lib/project-root.js');
|
|
468
|
+
try {
|
|
469
|
+
options.cwd = await resolveProjectRef(options.project, { forRemote: hostGiven.length > 0 });
|
|
470
|
+
}
|
|
471
|
+
catch (err) {
|
|
472
|
+
console.error(chalk.red(err.message));
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
457
476
|
if (hostGiven.length > 0) {
|
|
458
477
|
if (new Set(hostGiven).size > 1) {
|
|
459
478
|
console.error(chalk.red('Conflicting --host/--device values — pass just one.'));
|
|
@@ -488,6 +507,11 @@ export function registerRunCommand(program) {
|
|
|
488
507
|
}
|
|
489
508
|
try {
|
|
490
509
|
const runAgent = agentSpec.split('@')[0];
|
|
510
|
+
// Working directory on the host: an explicit --remote-cwd is used
|
|
511
|
+
// verbatim; --cwd/--project are made portable (a local-home absolute
|
|
512
|
+
// becomes `~/…` so the remote shell re-roots it at ITS home).
|
|
513
|
+
const { toRemotePortable } = await import('../lib/project-root.js');
|
|
514
|
+
const hostCwd = options.remoteCwd ?? (options.cwd ? toRemotePortable(options.cwd) : undefined);
|
|
491
515
|
// `--resume [id]`: commander yields the string id, or `true` when the
|
|
492
516
|
// flag is passed bare. A bare resume needs the interactive picker,
|
|
493
517
|
// which can't run over a detached remote dispatch — only forward a
|
|
@@ -528,7 +552,7 @@ export function registerRunCommand(program) {
|
|
|
528
552
|
prompt,
|
|
529
553
|
mode: options.mode,
|
|
530
554
|
model: options.model,
|
|
531
|
-
remoteCwd:
|
|
555
|
+
remoteCwd: hostCwd,
|
|
532
556
|
sessionId: hostSessionId,
|
|
533
557
|
name: options.name,
|
|
534
558
|
resume: resumeId,
|
|
@@ -550,7 +574,7 @@ export function registerRunCommand(program) {
|
|
|
550
574
|
prompt,
|
|
551
575
|
mode: options.mode,
|
|
552
576
|
model: options.model,
|
|
553
|
-
remoteCwd:
|
|
577
|
+
remoteCwd: hostCwd,
|
|
554
578
|
sessionId: hostSessionId,
|
|
555
579
|
name: options.name,
|
|
556
580
|
resume: resumeId,
|
|
@@ -1203,7 +1227,7 @@ export function registerRunCommand(program) {
|
|
|
1203
1227
|
// Remote bundle (`bundle@host`): resolve over SSH and inject
|
|
1204
1228
|
// ephemerally — values never touch this machine's keychain or disk.
|
|
1205
1229
|
const target = await resolveSshTarget(host);
|
|
1206
|
-
const bundleEnv = await remoteResolveEnv(target, bundleName);
|
|
1230
|
+
const bundleEnv = await remoteResolveEnv(target, bundleName, { osLookupName: host });
|
|
1207
1231
|
console.log(chalk.gray(`[secrets] Resolved ${bundleName}@${host}: ${Object.keys(bundleEnv).length} keys (remote, ephemeral)`));
|
|
1208
1232
|
secretsEnv = { ...secretsEnv, ...bundleEnv };
|
|
1209
1233
|
}
|
|
@@ -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
|
+
}
|