claude-agent-ledger 0.4.1 → 0.7.1
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/README.md +77 -89
- package/bin/agent-ledger.ts +243 -56
- package/package.json +15 -4
- package/src/aggregator.ts +173 -71
- package/src/explain.ts +209 -0
- package/src/parser.ts +110 -32
- package/src/pricing.ts +45 -20
- package/src/tree.ts +107 -0
- package/src/types.ts +26 -8
- package/.github/ISSUE_TEMPLATE/bug.md +0 -35
- package/.github/ISSUE_TEMPLATE/model-pricing.md +0 -29
- package/.github/pull_request_template.md +0 -19
- package/.github/workflows/ci.yml +0 -19
- package/CHANGELOG.md +0 -105
- package/CONTRIBUTING.md +0 -42
- package/bun.lock +0 -24
- package/test/aggregator.test.ts +0 -75
- package/test/fixtures/projects/proj-a/session-abc/subagents/agent-deadbeef.jsonl +0 -2
- package/test/fixtures/projects/proj-a/session-abc/subagents/agent-deadbeef.meta.json +0 -1
- package/test/fixtures/projects/proj-a/session-abc.jsonl +0 -2
- package/test/fixtures/sample.jsonl +0 -6
- package/test/parser.test.ts +0 -55
- package/test/pricing.test.ts +0 -144
- package/tsconfig.json +0 -20
package/README.md
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/claude-agent-ledger)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
+
> Use `--anonymize` before sharing output. It replaces project paths and
|
|
9
|
+
> session IDs, but it does not rename subagent or tool names — review those
|
|
10
|
+
> before you post a screenshot.
|
|
11
|
+
|
|
8
12
|
---
|
|
9
13
|
|
|
10
14
|
## Why
|
|
@@ -23,19 +27,40 @@ If you run **seven** subagents daily (reliability, release, review, iOS factory,
|
|
|
23
27
|
|
|
24
28
|
## Install
|
|
25
29
|
|
|
30
|
+
Requires [Bun](https://bun.sh/) 1.1 or newer. The CLI is Bun-native even when
|
|
31
|
+
you install it from npm.
|
|
32
|
+
|
|
26
33
|
```bash
|
|
27
|
-
|
|
28
|
-
# or:
|
|
34
|
+
bun install -g claude-agent-ledger
|
|
35
|
+
# or, with Bun already available: npm install -g claude-agent-ledger
|
|
29
36
|
|
|
30
37
|
agent-ledger week --summary # the dashboard (start here)
|
|
31
|
-
agent-ledger week --
|
|
38
|
+
agent-ledger week --tree # parent → child subagent attribution
|
|
39
|
+
agent-ledger week --by tool # which tool ate the budget (Read/Bash/Grep/MCP)
|
|
40
|
+
agent-ledger week --by model # opus vs sonnet vs haiku
|
|
32
41
|
agent-ledger week --by project # which project ate your tokens
|
|
33
42
|
agent-ledger week --by session # which single sessions went sideways
|
|
34
43
|
agent-ledger week --by day # daily bar chart with peak/avg/variance
|
|
44
|
+
agent-ledger today --budget 50 # CI gate: exit 2 if today > $50
|
|
45
|
+
agent-ledger week --json | jq # JSON output for any view, including summary
|
|
35
46
|
agent-ledger month --md > report.md
|
|
47
|
+
|
|
48
|
+
# Live tail — polling delta meter
|
|
49
|
+
agent-ledger watch # refresh every 10s
|
|
50
|
+
agent-ledger watch 4 # refresh every 4s
|
|
51
|
+
|
|
52
|
+
# Root-cause analysis on a single expensive session
|
|
53
|
+
agent-ledger explain <session-id> # heuristic: top-3 turns + tool/model breakdown
|
|
54
|
+
ANTHROPIC_API_KEY=sk-ant-... agent-ledger explain <session-id>
|
|
55
|
+
# LLM mode: Haiku 4.5 ≤4-bullet root cause (typically <$0.005/run)
|
|
36
56
|
```
|
|
37
57
|
|
|
38
|
-
|
|
58
|
+
All aggregate commands run locally, require no account, and make no network
|
|
59
|
+
requests. `explain` is also local by default. If you explicitly set
|
|
60
|
+
`ANTHROPIC_API_KEY`, it sends derived session metrics — subagent/model/tool
|
|
61
|
+
names, timestamps, token counts, and cost totals — to the
|
|
62
|
+
Anthropic API. The session identifier is redacted. It does **not** send prompts,
|
|
63
|
+
responses, tool content, or project paths.
|
|
39
64
|
|
|
40
65
|
## Why this and not `/cost`?
|
|
41
66
|
|
|
@@ -54,78 +79,24 @@ Claude Code's built-in `/cost` shows the **current session** cost. That's it.
|
|
|
54
79
|
|
|
55
80
|
Run `agent-ledger week --summary` once and decide.
|
|
56
81
|
|
|
57
|
-
## Example
|
|
82
|
+
## Example
|
|
58
83
|
|
|
59
|
-
|
|
84
|
+
This is deterministic output from the repository's synthetic fixture, using the
|
|
85
|
+
price snapshot documented below:
|
|
60
86
|
|
|
61
|
-
```
|
|
62
|
-
agent-ledger
|
|
63
|
-
|
|
64
|
-
Shadow cost $13,450.05
|
|
65
|
-
Sessions 219
|
|
66
|
-
Projects 9
|
|
67
|
-
Plan reference $200/mo (Max)
|
|
68
|
-
Multiplier 252× vs Max plan
|
|
69
|
-
|
|
70
|
-
Top subagent (main) $12,787.19 (95%)
|
|
71
|
-
Top model claude-opus-4-7 $7,278.87 (54%)
|
|
72
|
-
Top project ~/clawbot $6,657.93 (50%)
|
|
73
|
-
Peak day 2026-04-15 $3,679.16
|
|
74
|
-
|
|
75
|
-
Project mix
|
|
76
|
-
~/clawbot $6,657.93 (50%)
|
|
77
|
-
~/clawbot-work $4,433.32 (33%)
|
|
78
|
-
~/ZStack-zstack-workspace $1,890.02 (14%)
|
|
79
|
-
|
|
80
|
-
Cache 1h writes 119.65M tokens · $3589.46
|
|
81
|
-
Cache reads 5.59B tokens · $8236.15
|
|
82
|
-
Cache reuse 38× reads/writes
|
|
83
|
-
Server tools none
|
|
84
|
-
```
|
|
87
|
+
```text
|
|
88
|
+
agent-ledger today summary
|
|
85
89
|
|
|
86
|
-
|
|
90
|
+
Shadow cost $0.08
|
|
91
|
+
Sessions 2
|
|
92
|
+
Projects 1
|
|
87
93
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
───────────────────────────────────────────────────────────────────────────────────────────
|
|
93
|
-
(main) 28 514K / 15.87M 1h:116.10M 5m: 0 r:5229.48M $12524.97
|
|
94
|
-
general-purpose 148 113K / 955K 1h: 0 5m: 20.67M r: 183.94M $599.97
|
|
95
|
-
Swift Developer 13 4K / 574K 1h: 0 5m: 1.61M r: 47.55M $28.93
|
|
96
|
-
Market Researcher 10 107K / 74K 1h: 0 5m: 863K r: 8.61M $7.26
|
|
97
|
-
Explore 13 176K / 77K 1h: 0 5m: 2.60M r: 22.38M $6.47
|
|
98
|
-
zstackws 3 232 / 20K 1h: 0 5m: 829K r: 3.52M $4.47
|
|
99
|
-
claude-code-guide 1 2K / 2K 1h: 0 5m: 56K r: 223K $0.08
|
|
100
|
-
───────────────────────────────────────────────────────────────────────────────────────────
|
|
101
|
-
total 216 916K / 17.57M 1h:116.10M 5m: 26.62M r:5495.71M $13172.14
|
|
94
|
+
Top subagent Reality Checker $0.06 (74%)
|
|
95
|
+
Top model claude-sonnet-4-6 $0.06 (74%)
|
|
96
|
+
Cache reads 56K tokens · $0.02
|
|
97
|
+
Cache reuse 28× reads/writes
|
|
102
98
|
```
|
|
103
99
|
|
|
104
|
-
### `--by model`
|
|
105
|
-
|
|
106
|
-
```
|
|
107
|
-
subagent sess tokens(in/out) cache tokens (1h/5m/read) cost
|
|
108
|
-
───────────────────────────────────────────────────────────────────────────────────────────
|
|
109
|
-
claude-opus-4-7 16 59K / 8.35M 1h: 61.15M 5m: 2.32M r:2974.59M $6967.29
|
|
110
|
-
claude-opus-4-6 127 544K / 8.25M 1h: 55.02M 5m: 13.30M r:2422.18M $6160.46
|
|
111
|
-
claude-sonnet-4-6 65 135K / 946K 1h: 0 5m: 8.81M r: 98.12M $77.07
|
|
112
|
-
claude-haiku-4-5 13 178K / 67K 1h: 0 5m: 2.19M r: 21.57M $4.33
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### `--by day` (ASCII bar chart)
|
|
116
|
-
|
|
117
|
-
```
|
|
118
|
-
2026-04-15 $3749.84 ████████████████████████████████████ (18 sess)
|
|
119
|
-
2026-04-16 $2492.99 ████████████████████████ (175 sess)
|
|
120
|
-
2026-04-17 $2111.63 ████████████████████ (9 sess)
|
|
121
|
-
2026-04-18 $1840.02 ██████████████████ (18 sess)
|
|
122
|
-
2026-04-19 $644.97 ██████ (5 sess)
|
|
123
|
-
2026-04-20 $1095.58 ███████████ (6 sess)
|
|
124
|
-
2026-04-21 $1273.55 ████████████ (9 sess)
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Yes those are real numbers. Yes it's a lot. See [Shadow cost vs actual bill](#shadow-cost-vs-actual-bill).
|
|
128
|
-
|
|
129
100
|
## Shadow cost vs actual bill
|
|
130
101
|
|
|
131
102
|
The `cost` column shows what you **would** pay at Anthropic's pay-as-you-go API rates.
|
|
@@ -135,9 +106,13 @@ If you're on a fixed-price subscription (Claude Pro / Max / Claude Code plan), y
|
|
|
135
106
|
- Understanding where time-and-token budget goes, regardless of how you pay
|
|
136
107
|
- Spotting runaway agents (a 10× spike is still a 10× spike, capped or not)
|
|
137
108
|
- Deciding when to downshift to Haiku or up to Opus per task
|
|
138
|
-
-
|
|
109
|
+
- Comparing usage concentration with a fixed-price plan. Shadow cost is an
|
|
110
|
+
estimate, not a claim that subscription usage is interchangeable with raw API
|
|
111
|
+
capacity.
|
|
139
112
|
|
|
140
|
-
If you're on pay-as-you-go, the `cost` column
|
|
113
|
+
If you're on pay-as-you-go, treat the `cost` column as a directional estimate.
|
|
114
|
+
Batch discounts, long-context premiums, provider-specific rates, retries, and
|
|
115
|
+
future price changes can make an invoice differ.
|
|
141
116
|
|
|
142
117
|
## How it works
|
|
143
118
|
|
|
@@ -147,10 +122,10 @@ and any `server_tool_use` (web_search / web_fetch) requests. Subagent invocation
|
|
|
147
122
|
under `<session>/subagents/agent-*.jsonl`, with a sidecar `.meta.json` carrying the real
|
|
148
123
|
subagent name (e.g. `Reality Checker`, `ios-factory`).
|
|
149
124
|
|
|
150
|
-
`claude-agent-ledger` walks the tree, resolves subagent types from the sidecar
|
|
151
|
-
and aggregates by your chosen key
|
|
152
|
-
|
|
153
|
-
|
|
125
|
+
`claude-agent-ledger` walks the tree, resolves subagent types from the sidecar
|
|
126
|
+
metadata, and aggregates by your chosen key. Pricing is applied locally from
|
|
127
|
+
`src/pricing.ts`. Aggregate commands never touch the network after install; the
|
|
128
|
+
optional API-backed `explain` mode is disclosed in [Install](#install).
|
|
154
129
|
|
|
155
130
|
### Cache accounting
|
|
156
131
|
|
|
@@ -166,24 +141,31 @@ Naive trackers lump these together as "input" and either overstate (by counting
|
|
|
166
141
|
|
|
167
142
|
### Server tool pricing
|
|
168
143
|
|
|
169
|
-
`web_search_requests`
|
|
144
|
+
`web_search_requests` cost $0.01/request. `web_fetch_requests` are counted for
|
|
145
|
+
observability but have no per-request charge; fetched content still contributes
|
|
146
|
+
normal token costs. Both appear in the footer:
|
|
170
147
|
|
|
171
148
|
```
|
|
172
149
|
server tools: web_search ×7, web_fetch ×0 ($0.07)
|
|
173
150
|
```
|
|
174
151
|
|
|
175
|
-
|
|
152
|
+
Rates were checked against Anthropic's official pricing page on 2026-07-23.
|
|
153
|
+
If a log contains a model ID without a verified rate, the CLI warns on stderr
|
|
154
|
+
and leaves that model's token cost at `$0.00` instead of guessing.
|
|
176
155
|
|
|
177
156
|
## Flags
|
|
178
157
|
|
|
179
158
|
| Flag | What it does |
|
|
180
159
|
|---|---|
|
|
181
160
|
| `--summary` | One-screen dashboard with all the headline numbers |
|
|
182
|
-
| `--by <subagent\|model\|day\|project\|session>` | Group rows. Default: `subagent`. `day` renders as ASCII bar chart. |
|
|
161
|
+
| `--by <subagent\|model\|day\|project\|session\|tool>` | Group rows. Default: `subagent`. `day` renders as ASCII bar chart. |
|
|
162
|
+
| `--tree` | Render parent → child subagent cost attribution |
|
|
163
|
+
| `--budget <USD>` | Exit with code 2 when estimated cost exceeds the threshold |
|
|
183
164
|
| `--anonymize` | Replace project paths and session ids with `~/repo-A`, `sess-A`, ... so you can share screenshots without doxing your employer / private repos. |
|
|
184
165
|
| `--md` | Markdown table output (good for committing or sharing) |
|
|
185
166
|
| `--json` | Raw JSON output (pipe into jq) |
|
|
186
|
-
| `--plan pro\|max` |
|
|
167
|
+
| `--plan pro\|max` | Mask estimated costs in grouped table views; `--summary` and `--tree` keep their cost-oriented output |
|
|
168
|
+
| `--verbose` | Print parse counts and elapsed time to stderr |
|
|
187
169
|
|
|
188
170
|
| Env | What it does |
|
|
189
171
|
|---|---|
|
|
@@ -205,11 +187,16 @@ If Anthropic's actual rate differs (or web_fetch has its own price), open an iss
|
|
|
205
187
|
|
|
206
188
|
## Status
|
|
207
189
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
190
|
+
The npm badge above reports the current published version. See
|
|
191
|
+
[`CHANGELOG.md`](CHANGELOG.md) for release history.
|
|
192
|
+
|
|
193
|
+
The source price table was checked against Anthropic's published rates on
|
|
194
|
+
2026-07-23. Sonnet 5 automatically switches from its introductory rate to the
|
|
195
|
+
published standard rate on 2026-09-01. Pricing can change, so shadow costs are
|
|
196
|
+
estimates rather than invoice replicas; check
|
|
197
|
+
[`src/pricing.ts`](src/pricing.ts) and the
|
|
198
|
+
[official pricing page](https://platform.claude.com/docs/en/about-claude/pricing)
|
|
199
|
+
before making billing decisions.
|
|
213
200
|
|
|
214
201
|
- [x] Log parser (JSONL streaming, recursive subagent discovery)
|
|
215
202
|
- [x] Per-subagent aggregation
|
|
@@ -219,7 +206,7 @@ templates.
|
|
|
219
206
|
- [x] ANSI color, TTY-aware
|
|
220
207
|
- [x] Cache 1h/5m TTL split pricing
|
|
221
208
|
- [x] `--plan pro|max` to mask shadow cost
|
|
222
|
-
- [x] `server_tool_use`
|
|
209
|
+
- [x] `server_tool_use` accounting (paid web search, no-fee web fetch)
|
|
223
210
|
- [ ] Cost forecasting (trailing 7-day burn rate)
|
|
224
211
|
- [ ] Calibration against real Anthropic invoices
|
|
225
212
|
- [ ] Homebrew tap
|
|
@@ -237,14 +224,15 @@ agent-ledger today
|
|
|
237
224
|
## Development
|
|
238
225
|
|
|
239
226
|
```bash
|
|
240
|
-
bun test #
|
|
227
|
+
bun test # 35 pass, 0 fail
|
|
241
228
|
bun run typecheck
|
|
229
|
+
bun run build
|
|
242
230
|
```
|
|
243
231
|
|
|
244
232
|
## Author
|
|
245
233
|
|
|
246
|
-
Built by [
|
|
247
|
-
|
|
234
|
+
Built by [Jie Xiang](https://jiexiang.dev)
|
|
235
|
+
([@XJM-free](https://github.com/XJM-free)).
|
|
248
236
|
|
|
249
237
|
## License
|
|
250
238
|
|
package/bin/agent-ledger.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
2
|
+
import { Aggregator, type GroupKey, SubagentGraph } from '../src/aggregator.ts';
|
|
3
|
+
import { analyzeSession, formatHeuristic, llmExplain } from '../src/explain.ts';
|
|
3
4
|
import { formatMarkdown, formatSummary, formatTable, formatTrend } from '../src/format.ts';
|
|
4
5
|
import { parseAll } from '../src/parser.ts';
|
|
5
|
-
import
|
|
6
|
+
import { isKnownModel } from '../src/pricing.ts';
|
|
7
|
+
import { formatTree, formatTreeMarkdown } from '../src/tree.ts';
|
|
8
|
+
import type { LedgerReport, SubagentTreeNode } from '../src/types.ts';
|
|
6
9
|
|
|
7
10
|
type Period = 'today' | 'week' | 'month';
|
|
8
11
|
type Plan = 'payg' | 'pro' | 'max';
|
|
@@ -14,7 +17,10 @@ interface Args {
|
|
|
14
17
|
plan: Plan;
|
|
15
18
|
by: GroupKey;
|
|
16
19
|
summary: boolean;
|
|
20
|
+
tree: boolean;
|
|
17
21
|
anonymize: boolean;
|
|
22
|
+
budget?: number; // USD threshold; non-zero exit if exceeded
|
|
23
|
+
verbose: boolean;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
function periodRange(period: Period): { from: Date; to: Date } {
|
|
@@ -33,35 +39,39 @@ function periodRange(period: Period): { from: Date; to: Date } {
|
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
function usage(): never {
|
|
36
|
-
console.error(`Usage:
|
|
42
|
+
console.error(`Usage:
|
|
43
|
+
agent-ledger <today|week|month> [flags] — aggregate views
|
|
44
|
+
agent-ledger explain <sessionId> — root-cause why one session was expensive (new in 0.6)
|
|
37
45
|
|
|
38
46
|
Flags:
|
|
39
|
-
--summary One-screen dashboard
|
|
40
|
-
|
|
41
|
-
--by <subagent|model|day|project|session>
|
|
47
|
+
--summary One-screen dashboard (start here).
|
|
48
|
+
--by <subagent|model|day|project|session|tool>
|
|
42
49
|
Group rows by:
|
|
43
50
|
subagent (default) — agent attribution
|
|
44
51
|
model — opus vs sonnet vs haiku
|
|
45
52
|
day — daily ASCII bar chart
|
|
46
53
|
project — by Claude Code project (decoded path)
|
|
47
54
|
session — by session id (top spenders)
|
|
55
|
+
tool — by Read/Bash/Write/Grep — what your turns burn on
|
|
56
|
+
--tree Subagent graph: parent→child cost attribution.
|
|
57
|
+
The MOAT view: orchestrator → 3×researcher + 6×swift-dev = $312.
|
|
58
|
+
--budget <USD> Exit non-zero (code 2) if total cost > USD. Pipe-friendly.
|
|
48
59
|
--md Markdown table output (good for sharing / commits)
|
|
49
|
-
--json Raw JSON output (
|
|
50
|
-
--plan pro|max
|
|
51
|
-
--anonymize Replace project paths and session ids with safe
|
|
52
|
-
|
|
53
|
-
shareable without doxing yourself.
|
|
60
|
+
--json Raw JSON output (also works with --summary now)
|
|
61
|
+
--plan pro|max Mask estimated costs in grouped table views.
|
|
62
|
+
--anonymize Replace project paths and session ids with safe placeholders.
|
|
63
|
+
--verbose Print parse stats (files, turns, elapsed) to stderr.
|
|
54
64
|
|
|
55
65
|
Environment:
|
|
56
66
|
NO_COLOR=1 Disable ANSI colors
|
|
57
67
|
FORCE_COLOR=1 Force colors when piping
|
|
58
68
|
|
|
59
69
|
Examples:
|
|
60
|
-
agent-ledger week --summary
|
|
61
|
-
agent-ledger week --
|
|
62
|
-
agent-ledger week --by
|
|
63
|
-
agent-ledger month --md > month.md
|
|
70
|
+
agent-ledger week --summary # the dashboard
|
|
71
|
+
agent-ledger week --tree # subagent fan-out tree
|
|
72
|
+
agent-ledger week --by tool # which Claude tool ate the budget
|
|
64
73
|
agent-ledger week --json | jq '.total.cost.totalCost'
|
|
74
|
+
agent-ledger today --budget 50 # CI gate: fail if today > $50
|
|
65
75
|
`);
|
|
66
76
|
process.exit(1);
|
|
67
77
|
}
|
|
@@ -72,6 +82,7 @@ function parseArgs(argv: string[]): Args {
|
|
|
72
82
|
|
|
73
83
|
let plan: Plan = 'payg';
|
|
74
84
|
let by: GroupKey = 'subagent';
|
|
85
|
+
let budget: number | undefined;
|
|
75
86
|
for (let i = 0; i < rest.length; i++) {
|
|
76
87
|
const flag = rest[i];
|
|
77
88
|
if (flag === '--plan') {
|
|
@@ -89,13 +100,34 @@ function parseArgs(argv: string[]): Args {
|
|
|
89
100
|
next === 'model' ||
|
|
90
101
|
next === 'day' ||
|
|
91
102
|
next === 'project' ||
|
|
92
|
-
next === 'session'
|
|
103
|
+
next === 'session' ||
|
|
104
|
+
next === 'tool'
|
|
93
105
|
) {
|
|
94
106
|
by = next;
|
|
95
107
|
i++;
|
|
96
108
|
} else {
|
|
109
|
+
console.error(`agent-ledger: unknown --by value '${next}'`);
|
|
97
110
|
usage();
|
|
98
111
|
}
|
|
112
|
+
} else if (flag === '--budget') {
|
|
113
|
+
const next = rest[i + 1];
|
|
114
|
+
const n = Number(next);
|
|
115
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
116
|
+
console.error(`agent-ledger: --budget requires a positive number (got '${next}')`);
|
|
117
|
+
usage();
|
|
118
|
+
}
|
|
119
|
+
budget = n;
|
|
120
|
+
i++;
|
|
121
|
+
} else if (
|
|
122
|
+
flag !== '--md' &&
|
|
123
|
+
flag !== '--json' &&
|
|
124
|
+
flag !== '--summary' &&
|
|
125
|
+
flag !== '--tree' &&
|
|
126
|
+
flag !== '--anonymize' &&
|
|
127
|
+
flag !== '--verbose'
|
|
128
|
+
) {
|
|
129
|
+
console.error(`agent-ledger: unknown flag '${flag}'`);
|
|
130
|
+
usage();
|
|
99
131
|
}
|
|
100
132
|
}
|
|
101
133
|
return {
|
|
@@ -105,19 +137,17 @@ function parseArgs(argv: string[]): Args {
|
|
|
105
137
|
plan,
|
|
106
138
|
by,
|
|
107
139
|
summary: rest.includes('--summary'),
|
|
140
|
+
tree: rest.includes('--tree'),
|
|
108
141
|
anonymize: rest.includes('--anonymize'),
|
|
142
|
+
budget,
|
|
143
|
+
verbose: rest.includes('--verbose'),
|
|
109
144
|
};
|
|
110
145
|
}
|
|
111
146
|
|
|
112
|
-
// Replace identifying labels with safe placeholders. Run after aggregation,
|
|
113
|
-
// before formatting. We anonymize project paths (which leak employer + repo
|
|
114
|
-
// names) and session ids; leave subagent / model / day untouched (those are
|
|
115
|
-
// either public Anthropic info or generic agent type names).
|
|
116
147
|
function anonymizeReport(report: LedgerReport, group: GroupKey): LedgerReport {
|
|
117
148
|
if (group !== 'project' && group !== 'session') return report;
|
|
118
149
|
const prefix = group === 'project' ? '~/repo-' : 'sess-';
|
|
119
150
|
const labelFor = (i: number): string => {
|
|
120
|
-
// Use A..Z then AA..ZZ for many entries
|
|
121
151
|
if (i < 26) return prefix + String.fromCharCode(65 + i);
|
|
122
152
|
return prefix + String.fromCharCode(65 + Math.floor(i / 26) - 1) + String.fromCharCode(65 + (i % 26));
|
|
123
153
|
};
|
|
@@ -125,9 +155,27 @@ function anonymizeReport(report: LedgerReport, group: GroupKey): LedgerReport {
|
|
|
125
155
|
return { ...report, rows: newRows };
|
|
126
156
|
}
|
|
127
157
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
158
|
+
function anonymizeTrees(roots: SubagentTreeNode[]): SubagentTreeNode[] {
|
|
159
|
+
const labels = new Map<string, string>();
|
|
160
|
+
const labelFor = (sessionId: string): string => {
|
|
161
|
+
const existing = labels.get(sessionId);
|
|
162
|
+
if (existing) return existing;
|
|
163
|
+
const i = labels.size;
|
|
164
|
+
const suffix =
|
|
165
|
+
i < 26
|
|
166
|
+
? String.fromCharCode(65 + i)
|
|
167
|
+
: String.fromCharCode(65 + Math.floor(i / 26) - 1) +
|
|
168
|
+
String.fromCharCode(65 + (i % 26));
|
|
169
|
+
const label = `sess-${suffix}`;
|
|
170
|
+
labels.set(sessionId, label);
|
|
171
|
+
return label;
|
|
172
|
+
};
|
|
173
|
+
const visit = (node: SubagentTreeNode): SubagentTreeNode => ({
|
|
174
|
+
...node,
|
|
175
|
+
sessionId: labelFor(node.sessionId),
|
|
176
|
+
children: node.children.map(visit),
|
|
177
|
+
});
|
|
178
|
+
return roots.map(visit);
|
|
131
179
|
}
|
|
132
180
|
|
|
133
181
|
function planMask(report: LedgerReport, plan: Plan): LedgerReport {
|
|
@@ -148,49 +196,188 @@ function planMask(report: LedgerReport, plan: Plan): LedgerReport {
|
|
|
148
196
|
};
|
|
149
197
|
}
|
|
150
198
|
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
199
|
+
// `--watch` live-tail mode. Re-aggregates "today" every N seconds and
|
|
200
|
+
// prints a clean delta meter. Polling-based (no inotify) so it works on
|
|
201
|
+
// any platform Bun runs on. Ctrl-C to exit.
|
|
202
|
+
async function watchCmd(intervalSec: number): Promise<void> {
|
|
203
|
+
const interval = Math.max(2, intervalSec);
|
|
204
|
+
let prevCost = 0;
|
|
205
|
+
let prevTurnCount = 0;
|
|
206
|
+
const t0 = Date.now();
|
|
207
|
+
console.log(`agent-ledger watch — refreshing every ${interval}s · Ctrl-C to exit`);
|
|
208
|
+
console.log('');
|
|
209
|
+
const tick = async () => {
|
|
210
|
+
const { from, to } = periodRange('today');
|
|
211
|
+
const agg = new Aggregator('subagent', from, to);
|
|
212
|
+
let turns = 0;
|
|
213
|
+
for await (const turn of parseAll({ from, to })) {
|
|
214
|
+
turns++;
|
|
215
|
+
agg.add(turn);
|
|
216
|
+
}
|
|
217
|
+
const report = agg.finalize();
|
|
218
|
+
const cost = report.total.cost.totalCost;
|
|
219
|
+
const dCost = cost - prevCost;
|
|
220
|
+
const dTurns = turns - prevTurnCount;
|
|
221
|
+
const elapsedMin = ((Date.now() - t0) / 60000).toFixed(1);
|
|
222
|
+
const arrow = dCost > 0 ? '↑' : dCost < 0 ? '↓' : '·';
|
|
223
|
+
const ts = new Date().toISOString().slice(11, 19);
|
|
224
|
+
const top = report.rows[0];
|
|
225
|
+
const topLabel = top ? `top: ${top.subagent} ${fmtUsdInline(top.cost.totalCost)}` : 'no data';
|
|
226
|
+
console.log(
|
|
227
|
+
`[${ts}] today $${cost.toFixed(2)} ${arrow} +$${dCost.toFixed(2)} · ${turns} turns (Δ${dTurns}) · ${topLabel} · live ${elapsedMin}m`,
|
|
228
|
+
);
|
|
229
|
+
prevCost = cost;
|
|
230
|
+
prevTurnCount = turns;
|
|
231
|
+
};
|
|
232
|
+
await tick();
|
|
233
|
+
const id = setInterval(() => {
|
|
234
|
+
tick().catch((e) => console.error('watch tick error:', e));
|
|
235
|
+
}, interval * 1000);
|
|
236
|
+
process.on('SIGINT', () => {
|
|
237
|
+
clearInterval(id);
|
|
238
|
+
console.log('\nagent-ledger watch — exiting');
|
|
239
|
+
process.exit(0);
|
|
240
|
+
});
|
|
241
|
+
// Keep process alive
|
|
242
|
+
await new Promise(() => {});
|
|
156
243
|
}
|
|
157
244
|
|
|
158
|
-
|
|
159
|
-
|
|
245
|
+
const fmtUsdInline = (n: number): string => '$' + n.toFixed(n < 0.01 ? 4 : 2);
|
|
246
|
+
|
|
247
|
+
async function explainCmd(sessionPrefix: string | undefined): Promise<void> {
|
|
248
|
+
if (!sessionPrefix) {
|
|
249
|
+
console.error('agent-ledger explain: missing sessionId argument');
|
|
250
|
+
console.error('Usage: agent-ledger explain <sessionId>');
|
|
251
|
+
console.error(' (sessionId can be a full uuid or 8-char prefix)');
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
const analysis = await analyzeSession(sessionPrefix);
|
|
255
|
+
if (!analysis) {
|
|
256
|
+
console.error(`agent-ledger explain: no session found matching '${sessionPrefix}'`);
|
|
257
|
+
console.error(' (try the 8-char prefix from `agent-ledger week --by session`)');
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
const heuristic = formatHeuristic(analysis);
|
|
261
|
+
console.log(heuristic);
|
|
262
|
+
const llm = await llmExplain(analysis);
|
|
263
|
+
if (llm) {
|
|
264
|
+
console.log('');
|
|
265
|
+
console.log(llm);
|
|
266
|
+
} else if (!process.env.ANTHROPIC_API_KEY) {
|
|
267
|
+
console.log('');
|
|
268
|
+
console.log(
|
|
269
|
+
'💡 Set ANTHROPIC_API_KEY to enable LLM root-cause analysis (typically <$0.005/run via Haiku 4.5)',
|
|
270
|
+
);
|
|
271
|
+
}
|
|
160
272
|
}
|
|
161
273
|
|
|
162
274
|
async function main(): Promise<void> {
|
|
275
|
+
// Subcommand dispatch.
|
|
276
|
+
if (process.argv[2] === 'explain') {
|
|
277
|
+
return explainCmd(process.argv[3]);
|
|
278
|
+
}
|
|
279
|
+
if (process.argv[2] === 'watch') {
|
|
280
|
+
const interval = Number(process.argv[3]) || 10;
|
|
281
|
+
return watchCmd(interval);
|
|
282
|
+
}
|
|
163
283
|
const args = parseArgs(process.argv.slice(2));
|
|
164
284
|
const { from, to } = periodRange(args.period);
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
285
|
+
const t0 = Date.now();
|
|
286
|
+
|
|
287
|
+
// Determine which aggregators we need based on flags.
|
|
288
|
+
// Single parser pass dispatches to all in parallel — no buffering.
|
|
289
|
+
const aggBySub = new Aggregator('subagent', from, to);
|
|
290
|
+
const aggByMod = args.summary ? new Aggregator('model', from, to) : undefined;
|
|
291
|
+
const aggByDay = args.summary ? new Aggregator('day', from, to) : undefined;
|
|
292
|
+
const aggByProj = args.summary ? new Aggregator('project', from, to) : undefined;
|
|
293
|
+
const aggUser = !args.summary && !args.tree ? new Aggregator(args.by, from, to) : undefined;
|
|
294
|
+
const graph = args.tree ? new SubagentGraph() : undefined;
|
|
295
|
+
|
|
296
|
+
let turnCount = 0;
|
|
297
|
+
const unknownModels = new Set<string>();
|
|
298
|
+
for await (const turn of parseAll({ from, to })) {
|
|
299
|
+
turnCount++;
|
|
300
|
+
if (turn.model && !isKnownModel(turn.model)) {
|
|
301
|
+
unknownModels.add(turn.model);
|
|
302
|
+
}
|
|
303
|
+
aggBySub.add(turn);
|
|
304
|
+
aggByMod?.add(turn);
|
|
305
|
+
aggByDay?.add(turn);
|
|
306
|
+
aggByProj?.add(turn);
|
|
307
|
+
aggUser?.add(turn);
|
|
308
|
+
graph?.add(turn);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (unknownModels.size > 0) {
|
|
312
|
+
console.error(
|
|
313
|
+
'agent-ledger: no verified price for model(s): ' +
|
|
314
|
+
[...unknownModels].sort().join(', ') +
|
|
315
|
+
'; their token cost is shown as $0.00',
|
|
316
|
+
);
|
|
177
317
|
}
|
|
178
318
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
319
|
+
if (args.verbose) {
|
|
320
|
+
const elapsed = Date.now() - t0;
|
|
321
|
+
console.error(
|
|
322
|
+
`agent-ledger: parsed ${turnCount} turns from ~/.claude/projects in ${elapsed}ms`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let totalCost = aggBySub.finalize().total.cost.totalCost;
|
|
327
|
+
|
|
328
|
+
if (args.tree) {
|
|
329
|
+
const rawRoots = graph!.finalize(20);
|
|
330
|
+
const roots = args.anonymize ? anonymizeTrees(rawRoots) : rawRoots;
|
|
331
|
+
totalCost = roots.reduce((s, r) => s + r.totalCost, 0);
|
|
332
|
+
const out =
|
|
333
|
+
args.json
|
|
334
|
+
? JSON.stringify({ roots, totalCost }, null, 2)
|
|
335
|
+
: args.markdown
|
|
336
|
+
? formatTreeMarkdown(roots, totalCost)
|
|
337
|
+
: formatTree(roots, totalCost);
|
|
338
|
+
console.log(out);
|
|
339
|
+
} else if (args.summary) {
|
|
340
|
+
const bySubR = aggBySub.finalize();
|
|
341
|
+
const byModR = aggByMod!.finalize();
|
|
342
|
+
const byDayR = aggByDay!.finalize();
|
|
343
|
+
let byProjR = aggByProj!.finalize();
|
|
344
|
+
if (args.anonymize) byProjR = anonymizeReport(byProjR, 'project');
|
|
345
|
+
totalCost = bySubR.total.cost.totalCost;
|
|
346
|
+
if (args.json) {
|
|
347
|
+
console.log(
|
|
348
|
+
JSON.stringify(
|
|
349
|
+
{ subagent: bySubR, model: byModR, day: byDayR, project: byProjR, totalCost },
|
|
350
|
+
null,
|
|
351
|
+
2,
|
|
352
|
+
),
|
|
353
|
+
);
|
|
354
|
+
} else {
|
|
355
|
+
console.log(formatSummary(bySubR, byModR, byDayR, byProjR, args.period));
|
|
356
|
+
}
|
|
190
357
|
} else {
|
|
191
|
-
|
|
358
|
+
let report = aggUser!.finalize();
|
|
359
|
+
if (args.anonymize) report = anonymizeReport(report, args.by);
|
|
360
|
+
// Keep the unmasked estimate for --budget even when --plan hides it.
|
|
361
|
+
totalCost = report.total.cost.totalCost;
|
|
362
|
+
report = planMask(report, args.plan);
|
|
363
|
+
const output =
|
|
364
|
+
args.json
|
|
365
|
+
? JSON.stringify(report, null, 2)
|
|
366
|
+
: args.markdown
|
|
367
|
+
? formatMarkdown(report, args.period)
|
|
368
|
+
: args.by === 'day'
|
|
369
|
+
? formatTrend(report, args.period)
|
|
370
|
+
: formatTable(report, args.period, args.by);
|
|
371
|
+
console.log(output);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Budget gate — non-zero exit code so this can be a CI/shell hook.
|
|
375
|
+
if (args.budget !== undefined && totalCost > args.budget) {
|
|
376
|
+
console.error(
|
|
377
|
+
`agent-ledger: BUDGET EXCEEDED — total $${totalCost.toFixed(2)} > budget $${args.budget.toFixed(2)}`,
|
|
378
|
+
);
|
|
379
|
+
process.exit(2);
|
|
192
380
|
}
|
|
193
|
-
console.log(output);
|
|
194
381
|
}
|
|
195
382
|
|
|
196
383
|
main().catch((err) => {
|