claude-agent-ledger 0.7.0 → 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 +69 -97
- package/bin/agent-ledger.ts +55 -5
- package/package.json +14 -4
- package/src/explain.ts +4 -2
- package/src/pricing.ts +45 -20
- 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 -200
- package/CONTRIBUTING.md +0 -42
- package/bun.lock +0 -29
- 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/test/property.test.ts +0 -188
- package/tsconfig.json +0 -20
package/README.md
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/claude-agent-ledger)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
>
|
|
9
|
-
>
|
|
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.
|
|
10
11
|
|
|
11
12
|
---
|
|
12
13
|
|
|
@@ -26,32 +27,40 @@ If you run **seven** subagents daily (reliability, release, review, iOS factory,
|
|
|
26
27
|
|
|
27
28
|
## Install
|
|
28
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
|
+
|
|
29
33
|
```bash
|
|
30
|
-
|
|
31
|
-
# or:
|
|
34
|
+
bun install -g claude-agent-ledger
|
|
35
|
+
# or, with Bun already available: npm install -g claude-agent-ledger
|
|
32
36
|
|
|
33
37
|
agent-ledger week --summary # the dashboard (start here)
|
|
34
|
-
agent-ledger week --tree #
|
|
35
|
-
agent-ledger week --by tool # which tool ate the budget (Read/Bash/Grep/MCP)
|
|
38
|
+
agent-ledger week --tree # parent → child subagent attribution
|
|
39
|
+
agent-ledger week --by tool # which tool ate the budget (Read/Bash/Grep/MCP)
|
|
36
40
|
agent-ledger week --by model # opus vs sonnet vs haiku
|
|
37
41
|
agent-ledger week --by project # which project ate your tokens
|
|
38
42
|
agent-ledger week --by session # which single sessions went sideways
|
|
39
43
|
agent-ledger week --by day # daily bar chart with peak/avg/variance
|
|
40
|
-
agent-ledger today --budget 50 # CI gate: exit 2 if today > $50
|
|
41
|
-
agent-ledger week --json | jq # JSON output for
|
|
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
|
|
42
46
|
agent-ledger month --md > report.md
|
|
43
47
|
|
|
44
|
-
# Live tail — polling delta meter
|
|
48
|
+
# Live tail — polling delta meter
|
|
45
49
|
agent-ledger watch # refresh every 10s
|
|
46
50
|
agent-ledger watch 4 # refresh every 4s
|
|
47
51
|
|
|
48
|
-
# Root-cause analysis on a single expensive session
|
|
52
|
+
# Root-cause analysis on a single expensive session
|
|
49
53
|
agent-ledger explain <session-id> # heuristic: top-3 turns + tool/model breakdown
|
|
50
54
|
ANTHROPIC_API_KEY=sk-ant-... agent-ledger explain <session-id>
|
|
51
|
-
# LLM mode: Haiku 4.5 ≤4-bullet root cause (
|
|
55
|
+
# LLM mode: Haiku 4.5 ≤4-bullet root cause (typically <$0.005/run)
|
|
52
56
|
```
|
|
53
57
|
|
|
54
|
-
|
|
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.
|
|
55
64
|
|
|
56
65
|
## Why this and not `/cost`?
|
|
57
66
|
|
|
@@ -70,77 +79,23 @@ Claude Code's built-in `/cost` shows the **current session** cost. That's it.
|
|
|
70
79
|
|
|
71
80
|
Run `agent-ledger week --summary` once and decide.
|
|
72
81
|
|
|
73
|
-
## Example
|
|
74
|
-
|
|
75
|
-
### `--summary` (the dashboard)
|
|
76
|
-
|
|
77
|
-
```
|
|
78
|
-
agent-ledger week summary 2026-04-15 → 2026-04-22
|
|
79
|
-
|
|
80
|
-
Shadow cost $13,450.05
|
|
81
|
-
Sessions 219
|
|
82
|
-
Projects 9
|
|
83
|
-
Plan reference $200/mo (Max)
|
|
84
|
-
Multiplier 252× vs Max plan
|
|
85
|
-
|
|
86
|
-
Top subagent (main) $12,787.19 (95%)
|
|
87
|
-
Top model claude-opus-4-7 $7,278.87 (54%)
|
|
88
|
-
Top project ~/clawbot $6,657.93 (50%)
|
|
89
|
-
Peak day 2026-04-15 $3,679.16
|
|
90
|
-
|
|
91
|
-
Project mix
|
|
92
|
-
~/clawbot $6,657.93 (50%)
|
|
93
|
-
~/clawbot-work $4,433.32 (33%)
|
|
94
|
-
~/ZStack-zstack-workspace $1,890.02 (14%)
|
|
95
|
-
|
|
96
|
-
Cache 1h writes 119.65M tokens · $3589.46
|
|
97
|
-
Cache reads 5.59B tokens · $8236.15
|
|
98
|
-
Cache reuse 38× reads/writes
|
|
99
|
-
Server tools none
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Default — by subagent
|
|
103
|
-
|
|
104
|
-
```
|
|
105
|
-
agent-ledger week · 2026-04-14 → 2026-04-21
|
|
106
|
-
|
|
107
|
-
subagent sess tokens(in/out) cache tokens (1h/5m/read) cost
|
|
108
|
-
───────────────────────────────────────────────────────────────────────────────────────────
|
|
109
|
-
(main) 28 514K / 15.87M 1h:116.10M 5m: 0 r:5229.48M $12524.97
|
|
110
|
-
general-purpose 148 113K / 955K 1h: 0 5m: 20.67M r: 183.94M $599.97
|
|
111
|
-
Swift Developer 13 4K / 574K 1h: 0 5m: 1.61M r: 47.55M $28.93
|
|
112
|
-
Market Researcher 10 107K / 74K 1h: 0 5m: 863K r: 8.61M $7.26
|
|
113
|
-
Explore 13 176K / 77K 1h: 0 5m: 2.60M r: 22.38M $6.47
|
|
114
|
-
zstackws 3 232 / 20K 1h: 0 5m: 829K r: 3.52M $4.47
|
|
115
|
-
claude-code-guide 1 2K / 2K 1h: 0 5m: 56K r: 223K $0.08
|
|
116
|
-
───────────────────────────────────────────────────────────────────────────────────────────
|
|
117
|
-
total 216 916K / 17.57M 1h:116.10M 5m: 26.62M r:5495.71M $13172.14
|
|
118
|
-
```
|
|
82
|
+
## Example
|
|
119
83
|
|
|
120
|
-
|
|
84
|
+
This is deterministic output from the repository's synthetic fixture, using the
|
|
85
|
+
price snapshot documented below:
|
|
121
86
|
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
───────────────────────────────────────────────────────────────────────────────────────────
|
|
125
|
-
claude-opus-4-7 16 59K / 8.35M 1h: 61.15M 5m: 2.32M r:2974.59M $6967.29
|
|
126
|
-
claude-opus-4-6 127 544K / 8.25M 1h: 55.02M 5m: 13.30M r:2422.18M $6160.46
|
|
127
|
-
claude-sonnet-4-6 65 135K / 946K 1h: 0 5m: 8.81M r: 98.12M $77.07
|
|
128
|
-
claude-haiku-4-5 13 178K / 67K 1h: 0 5m: 2.19M r: 21.57M $4.33
|
|
129
|
-
```
|
|
87
|
+
```text
|
|
88
|
+
agent-ledger today summary
|
|
130
89
|
|
|
131
|
-
|
|
90
|
+
Shadow cost $0.08
|
|
91
|
+
Sessions 2
|
|
92
|
+
Projects 1
|
|
132
93
|
|
|
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
|
|
133
98
|
```
|
|
134
|
-
2026-04-15 $3749.84 ████████████████████████████████████ (18 sess)
|
|
135
|
-
2026-04-16 $2492.99 ████████████████████████ (175 sess)
|
|
136
|
-
2026-04-17 $2111.63 ████████████████████ (9 sess)
|
|
137
|
-
2026-04-18 $1840.02 ██████████████████ (18 sess)
|
|
138
|
-
2026-04-19 $644.97 ██████ (5 sess)
|
|
139
|
-
2026-04-20 $1095.58 ███████████ (6 sess)
|
|
140
|
-
2026-04-21 $1273.55 ████████████ (9 sess)
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
Yes those are real numbers. Yes it's a lot. See [Shadow cost vs actual bill](#shadow-cost-vs-actual-bill).
|
|
144
99
|
|
|
145
100
|
## Shadow cost vs actual bill
|
|
146
101
|
|
|
@@ -151,9 +106,13 @@ If you're on a fixed-price subscription (Claude Pro / Max / Claude Code plan), y
|
|
|
151
106
|
- Understanding where time-and-token budget goes, regardless of how you pay
|
|
152
107
|
- Spotting runaway agents (a 10× spike is still a 10× spike, capped or not)
|
|
153
108
|
- Deciding when to downshift to Haiku or up to Opus per task
|
|
154
|
-
-
|
|
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.
|
|
155
112
|
|
|
156
|
-
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.
|
|
157
116
|
|
|
158
117
|
## How it works
|
|
159
118
|
|
|
@@ -163,10 +122,10 @@ and any `server_tool_use` (web_search / web_fetch) requests. Subagent invocation
|
|
|
163
122
|
under `<session>/subagents/agent-*.jsonl`, with a sidecar `.meta.json` carrying the real
|
|
164
123
|
subagent name (e.g. `Reality Checker`, `ios-factory`).
|
|
165
124
|
|
|
166
|
-
`claude-agent-ledger` walks the tree, resolves subagent types from the sidecar
|
|
167
|
-
and aggregates by your chosen key
|
|
168
|
-
|
|
169
|
-
|
|
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).
|
|
170
129
|
|
|
171
130
|
### Cache accounting
|
|
172
131
|
|
|
@@ -182,24 +141,31 @@ Naive trackers lump these together as "input" and either overstate (by counting
|
|
|
182
141
|
|
|
183
142
|
### Server tool pricing
|
|
184
143
|
|
|
185
|
-
`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:
|
|
186
147
|
|
|
187
148
|
```
|
|
188
149
|
server tools: web_search ×7, web_fetch ×0 ($0.07)
|
|
189
150
|
```
|
|
190
151
|
|
|
191
|
-
|
|
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.
|
|
192
155
|
|
|
193
156
|
## Flags
|
|
194
157
|
|
|
195
158
|
| Flag | What it does |
|
|
196
159
|
|---|---|
|
|
197
160
|
| `--summary` | One-screen dashboard with all the headline numbers |
|
|
198
|
-
| `--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 |
|
|
199
164
|
| `--anonymize` | Replace project paths and session ids with `~/repo-A`, `sess-A`, ... so you can share screenshots without doxing your employer / private repos. |
|
|
200
165
|
| `--md` | Markdown table output (good for committing or sharing) |
|
|
201
166
|
| `--json` | Raw JSON output (pipe into jq) |
|
|
202
|
-
| `--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 |
|
|
203
169
|
|
|
204
170
|
| Env | What it does |
|
|
205
171
|
|---|---|
|
|
@@ -221,11 +187,16 @@ If Anthropic's actual rate differs (or web_fetch has its own price), open an iss
|
|
|
221
187
|
|
|
222
188
|
## Status
|
|
223
189
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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.
|
|
229
200
|
|
|
230
201
|
- [x] Log parser (JSONL streaming, recursive subagent discovery)
|
|
231
202
|
- [x] Per-subagent aggregation
|
|
@@ -235,7 +206,7 @@ templates.
|
|
|
235
206
|
- [x] ANSI color, TTY-aware
|
|
236
207
|
- [x] Cache 1h/5m TTL split pricing
|
|
237
208
|
- [x] `--plan pro|max` to mask shadow cost
|
|
238
|
-
- [x] `server_tool_use`
|
|
209
|
+
- [x] `server_tool_use` accounting (paid web search, no-fee web fetch)
|
|
239
210
|
- [ ] Cost forecasting (trailing 7-day burn rate)
|
|
240
211
|
- [ ] Calibration against real Anthropic invoices
|
|
241
212
|
- [ ] Homebrew tap
|
|
@@ -253,14 +224,15 @@ agent-ledger today
|
|
|
253
224
|
## Development
|
|
254
225
|
|
|
255
226
|
```bash
|
|
256
|
-
bun test #
|
|
227
|
+
bun test # 35 pass, 0 fail
|
|
257
228
|
bun run typecheck
|
|
229
|
+
bun run build
|
|
258
230
|
```
|
|
259
231
|
|
|
260
232
|
## Author
|
|
261
233
|
|
|
262
|
-
Built by [
|
|
263
|
-
|
|
234
|
+
Built by [Jie Xiang](https://jiexiang.dev)
|
|
235
|
+
([@XJM-free](https://github.com/XJM-free)).
|
|
264
236
|
|
|
265
237
|
## License
|
|
266
238
|
|
package/bin/agent-ledger.ts
CHANGED
|
@@ -3,8 +3,9 @@ import { Aggregator, type GroupKey, SubagentGraph } from '../src/aggregator.ts';
|
|
|
3
3
|
import { analyzeSession, formatHeuristic, llmExplain } from '../src/explain.ts';
|
|
4
4
|
import { formatMarkdown, formatSummary, formatTable, formatTrend } from '../src/format.ts';
|
|
5
5
|
import { parseAll } from '../src/parser.ts';
|
|
6
|
+
import { isKnownModel } from '../src/pricing.ts';
|
|
6
7
|
import { formatTree, formatTreeMarkdown } from '../src/tree.ts';
|
|
7
|
-
import type { LedgerReport } from '../src/types.ts';
|
|
8
|
+
import type { LedgerReport, SubagentTreeNode } from '../src/types.ts';
|
|
8
9
|
|
|
9
10
|
type Period = 'today' | 'week' | 'month';
|
|
10
11
|
type Plan = 'payg' | 'pro' | 'max';
|
|
@@ -57,7 +58,7 @@ Flags:
|
|
|
57
58
|
--budget <USD> Exit non-zero (code 2) if total cost > USD. Pipe-friendly.
|
|
58
59
|
--md Markdown table output (good for sharing / commits)
|
|
59
60
|
--json Raw JSON output (also works with --summary now)
|
|
60
|
-
--plan pro|max
|
|
61
|
+
--plan pro|max Mask estimated costs in grouped table views.
|
|
61
62
|
--anonymize Replace project paths and session ids with safe placeholders.
|
|
62
63
|
--verbose Print parse stats (files, turns, elapsed) to stderr.
|
|
63
64
|
|
|
@@ -117,6 +118,16 @@ function parseArgs(argv: string[]): Args {
|
|
|
117
118
|
}
|
|
118
119
|
budget = n;
|
|
119
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();
|
|
120
131
|
}
|
|
121
132
|
}
|
|
122
133
|
return {
|
|
@@ -144,6 +155,29 @@ function anonymizeReport(report: LedgerReport, group: GroupKey): LedgerReport {
|
|
|
144
155
|
return { ...report, rows: newRows };
|
|
145
156
|
}
|
|
146
157
|
|
|
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);
|
|
179
|
+
}
|
|
180
|
+
|
|
147
181
|
function planMask(report: LedgerReport, plan: Plan): LedgerReport {
|
|
148
182
|
if (plan === 'payg') return report;
|
|
149
183
|
const zero = () => ({
|
|
@@ -231,7 +265,9 @@ async function explainCmd(sessionPrefix: string | undefined): Promise<void> {
|
|
|
231
265
|
console.log(llm);
|
|
232
266
|
} else if (!process.env.ANTHROPIC_API_KEY) {
|
|
233
267
|
console.log('');
|
|
234
|
-
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
|
+
);
|
|
235
271
|
}
|
|
236
272
|
}
|
|
237
273
|
|
|
@@ -258,8 +294,12 @@ async function main(): Promise<void> {
|
|
|
258
294
|
const graph = args.tree ? new SubagentGraph() : undefined;
|
|
259
295
|
|
|
260
296
|
let turnCount = 0;
|
|
297
|
+
const unknownModels = new Set<string>();
|
|
261
298
|
for await (const turn of parseAll({ from, to })) {
|
|
262
299
|
turnCount++;
|
|
300
|
+
if (turn.model && !isKnownModel(turn.model)) {
|
|
301
|
+
unknownModels.add(turn.model);
|
|
302
|
+
}
|
|
263
303
|
aggBySub.add(turn);
|
|
264
304
|
aggByMod?.add(turn);
|
|
265
305
|
aggByDay?.add(turn);
|
|
@@ -268,6 +308,14 @@ async function main(): Promise<void> {
|
|
|
268
308
|
graph?.add(turn);
|
|
269
309
|
}
|
|
270
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
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
271
319
|
if (args.verbose) {
|
|
272
320
|
const elapsed = Date.now() - t0;
|
|
273
321
|
console.error(
|
|
@@ -278,7 +326,8 @@ async function main(): Promise<void> {
|
|
|
278
326
|
let totalCost = aggBySub.finalize().total.cost.totalCost;
|
|
279
327
|
|
|
280
328
|
if (args.tree) {
|
|
281
|
-
const
|
|
329
|
+
const rawRoots = graph!.finalize(20);
|
|
330
|
+
const roots = args.anonymize ? anonymizeTrees(rawRoots) : rawRoots;
|
|
282
331
|
totalCost = roots.reduce((s, r) => s + r.totalCost, 0);
|
|
283
332
|
const out =
|
|
284
333
|
args.json
|
|
@@ -308,8 +357,9 @@ async function main(): Promise<void> {
|
|
|
308
357
|
} else {
|
|
309
358
|
let report = aggUser!.finalize();
|
|
310
359
|
if (args.anonymize) report = anonymizeReport(report, args.by);
|
|
311
|
-
|
|
360
|
+
// Keep the unmasked estimate for --budget even when --plan hides it.
|
|
312
361
|
totalCost = report.total.cost.totalCost;
|
|
362
|
+
report = planMask(report, args.plan);
|
|
313
363
|
const output =
|
|
314
364
|
args.json
|
|
315
365
|
? JSON.stringify(report, null, 2)
|
package/package.json
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-agent-ledger",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "See exactly where your Claude Code budget goes — per subagent, per task.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"agent-ledger": "
|
|
7
|
+
"agent-ledger": "bin/agent-ledger.ts"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
9
15
|
"scripts": {
|
|
10
16
|
"dev": "bun run bin/agent-ledger.ts",
|
|
11
17
|
"test": "bun test",
|
|
12
|
-
"build": "bun build bin/agent-ledger.ts --target=
|
|
18
|
+
"build": "bun build bin/agent-ledger.ts --target=bun --outdir dist",
|
|
13
19
|
"typecheck": "tsc --noEmit"
|
|
14
20
|
},
|
|
15
21
|
"engines": {
|
|
@@ -27,7 +33,11 @@
|
|
|
27
33
|
"license": "MIT",
|
|
28
34
|
"repository": {
|
|
29
35
|
"type": "git",
|
|
30
|
-
"url": "https://github.com/XJM-free/claude-agent-ledger.git"
|
|
36
|
+
"url": "git+https://github.com/XJM-free/claude-agent-ledger.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/XJM-free/claude-agent-ledger#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/XJM-free/claude-agent-ledger/issues"
|
|
31
41
|
},
|
|
32
42
|
"devDependencies": {
|
|
33
43
|
"@types/bun": "latest",
|
package/src/explain.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// tool-call breakdown + cache-reuse stats.
|
|
6
6
|
// 2. LLM mode (set ANTHROPIC_API_KEY): pipes the same evidence into
|
|
7
7
|
// Haiku 4.5 for a 4-bullet verdict ("prompt bloat", "Opus mis-selection",
|
|
8
|
-
// "no cache reuse", "tool output overflow").
|
|
8
|
+
// "no cache reuse", "tool output overflow"). Typically costs <$0.005/run.
|
|
9
9
|
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { join } from 'node:path';
|
|
@@ -166,7 +166,9 @@ interface AnthropicMsgRes {
|
|
|
166
166
|
export async function llmExplain(s: SessionAnalysis): Promise<string | undefined> {
|
|
167
167
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
168
168
|
if (!apiKey) return undefined;
|
|
169
|
-
|
|
169
|
+
// The session identifier is useful locally but adds no analytical value to
|
|
170
|
+
// the API request, so keep it off the network.
|
|
171
|
+
const evidence = formatHeuristic({ ...s, sessionId: '[redacted]' });
|
|
170
172
|
const prompt = `You are a Claude Code cost-optimization expert. A user's session burned ${fmtUsd(s.totalCost)} across ${s.turnCount} turns. Below is the per-turn breakdown:
|
|
171
173
|
|
|
172
174
|
${evidence}
|
package/src/pricing.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CostBreakdown, ModelId, TokenUsage } from './types.ts';
|
|
2
2
|
|
|
3
3
|
// Prices are per 1M tokens, USD. Keep in sync with
|
|
4
|
-
// https://
|
|
4
|
+
// https://platform.claude.com/docs/en/about-claude/pricing as of 2026-07-23.
|
|
5
5
|
//
|
|
6
6
|
// Prompt caching (Anthropic):
|
|
7
7
|
// cache_creation 5-min TTL: 1.25× base input rate
|
|
@@ -10,15 +10,31 @@ import type { CostBreakdown, ModelId, TokenUsage } from './types.ts';
|
|
|
10
10
|
//
|
|
11
11
|
// We include both current and prior-generation model IDs because real logs span
|
|
12
12
|
// version bumps (Claude Code does not auto-rewrite historical logs when you upgrade).
|
|
13
|
+
const SONNET_5_STANDARD_PRICE_START = new Date('2026-09-01T00:00:00Z');
|
|
14
|
+
const SONNET_5_INTRO_RATE = { input: 2, output: 10 };
|
|
15
|
+
const SONNET_5_STANDARD_RATE = { input: 3, output: 15 };
|
|
16
|
+
|
|
13
17
|
const PRICING: Record<string, { input: number; output: number }> = {
|
|
14
|
-
// Current generation (2026-
|
|
15
|
-
'claude-
|
|
18
|
+
// Current generation (2026-07)
|
|
19
|
+
'claude-fable-5': { input: 10, output: 50 },
|
|
20
|
+
'claude-mythos-5': { input: 10, output: 50 },
|
|
21
|
+
'claude-opus-4-8': { input: 5, output: 25 },
|
|
22
|
+
'claude-opus-4-7': { input: 5, output: 25 },
|
|
23
|
+
'claude-opus-4-6': { input: 5, output: 25 },
|
|
24
|
+
'claude-opus-4-5': { input: 5, output: 25 },
|
|
25
|
+
// resolveModel applies the introductory rate through 2026-08-31.
|
|
26
|
+
'claude-sonnet-5': SONNET_5_STANDARD_RATE,
|
|
16
27
|
'claude-sonnet-4-6': { input: 3, output: 15 },
|
|
17
|
-
'claude-haiku-4-5-20251001': { input: 0.8, output: 4 },
|
|
18
|
-
// Prior generation (still appears in logs written before model upgrades)
|
|
19
|
-
'claude-opus-4-6': { input: 15, output: 75 },
|
|
20
|
-
'claude-opus-4-5': { input: 15, output: 75 },
|
|
21
28
|
'claude-sonnet-4-5': { input: 3, output: 15 },
|
|
29
|
+
'claude-haiku-4-5-20251001': { input: 1, output: 5 },
|
|
30
|
+
'claude-haiku-4-5': { input: 1, output: 5 },
|
|
31
|
+
// Prior generation (still appears in logs written before model upgrades)
|
|
32
|
+
'claude-opus-4-1-20250805': { input: 15, output: 75 },
|
|
33
|
+
'claude-opus-4-20250514': { input: 15, output: 75 },
|
|
34
|
+
'claude-sonnet-4-20250514': { input: 3, output: 15 },
|
|
35
|
+
'claude-haiku-3-5-20241022': { input: 0.8, output: 4 },
|
|
36
|
+
'claude-3-haiku-20240307': { input: 0.25, output: 1.25 },
|
|
37
|
+
// Kept for the aliases observed in older Claude Code logs.
|
|
22
38
|
'claude-haiku-4-0': { input: 0.8, output: 4 },
|
|
23
39
|
};
|
|
24
40
|
|
|
@@ -30,25 +46,30 @@ const CACHE_5M_MULTIPLIER = 1.25;
|
|
|
30
46
|
const CACHE_1H_MULTIPLIER = 2.0;
|
|
31
47
|
const CACHE_READ_MULTIPLIER = 0.1;
|
|
32
48
|
|
|
33
|
-
// Anthropic server-tool pricing (
|
|
49
|
+
// Anthropic server-tool pricing (official pricing docs as of 2026-07-23):
|
|
34
50
|
// Web search: $10 per 1,000 requests = $0.01 / request
|
|
35
|
-
// Web fetch:
|
|
51
|
+
// Web fetch: no per-request charge; fetched content still incurs token costs
|
|
36
52
|
const WEB_SEARCH_PRICE_PER_REQUEST = 0.01;
|
|
37
|
-
const WEB_FETCH_PRICE_PER_REQUEST = 0
|
|
53
|
+
const WEB_FETCH_PRICE_PER_REQUEST = 0;
|
|
38
54
|
|
|
39
|
-
function resolveModel(model: ModelId | undefined): { input: number; output: number } {
|
|
55
|
+
function resolveModel(model: ModelId | undefined, asOf: Date): { input: number; output: number } {
|
|
40
56
|
if (!model) return { input: 0, output: 0 };
|
|
41
57
|
if (FREE_MODELS.has(model)) return { input: 0, output: 0 };
|
|
58
|
+
if (model === 'claude-sonnet-5' || model.startsWith('claude-sonnet-5-')) {
|
|
59
|
+
return asOf < SONNET_5_STANDARD_PRICE_START
|
|
60
|
+
? SONNET_5_INTRO_RATE
|
|
61
|
+
: SONNET_5_STANDARD_RATE;
|
|
62
|
+
}
|
|
42
63
|
const exact = PRICING[model];
|
|
43
64
|
if (exact) return exact;
|
|
44
|
-
//
|
|
45
|
-
//
|
|
65
|
+
// A provider/date-suffixed ID can inherit a verified base model's rate.
|
|
66
|
+
// Unknown future model families deliberately return zero instead of silently
|
|
67
|
+
// inventing a price; the CLI warns when it sees one.
|
|
46
68
|
for (const key of Object.keys(PRICING)) {
|
|
47
|
-
if (model.startsWith(key))
|
|
69
|
+
if (model.startsWith(`${key}-`)) {
|
|
70
|
+
return PRICING[key] as { input: number; output: number };
|
|
71
|
+
}
|
|
48
72
|
}
|
|
49
|
-
if (model.startsWith('claude-opus-')) return { input: 15, output: 75 };
|
|
50
|
-
if (model.startsWith('claude-sonnet-')) return { input: 3, output: 15 };
|
|
51
|
-
if (model.startsWith('claude-haiku-')) return { input: 0.8, output: 4 };
|
|
52
73
|
return { input: 0, output: 0 };
|
|
53
74
|
}
|
|
54
75
|
|
|
@@ -70,8 +91,12 @@ function splitCacheCreation(usage: TokenUsage): CacheBreakdown {
|
|
|
70
91
|
};
|
|
71
92
|
}
|
|
72
93
|
|
|
73
|
-
export function costFor(
|
|
74
|
-
|
|
94
|
+
export function costFor(
|
|
95
|
+
usage: TokenUsage,
|
|
96
|
+
model: ModelId | undefined,
|
|
97
|
+
asOf: Date = new Date(),
|
|
98
|
+
): CostBreakdown {
|
|
99
|
+
const rate = resolveModel(model, asOf);
|
|
75
100
|
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
76
101
|
const { fiveMin, oneHour } = splitCacheCreation(usage);
|
|
77
102
|
|
|
@@ -110,5 +135,5 @@ export function isKnownModel(model: ModelId | undefined): boolean {
|
|
|
110
135
|
if (!model) return false;
|
|
111
136
|
if (FREE_MODELS.has(model)) return true;
|
|
112
137
|
if (PRICING[model]) return true;
|
|
113
|
-
return Object.keys(PRICING).some((key) => model.startsWith(key));
|
|
138
|
+
return Object.keys(PRICING).some((key) => model.startsWith(`${key}-`));
|
|
114
139
|
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: Bug report
|
|
3
|
-
about: Report incorrect output, crashes, or wrong attribution
|
|
4
|
-
title: ''
|
|
5
|
-
labels: bug
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## What happened
|
|
9
|
-
|
|
10
|
-
<!-- What did agent-ledger do? What did you expect it to do instead? -->
|
|
11
|
-
|
|
12
|
-
## How to reproduce
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
agent-ledger <your-exact-command>
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## Output (redact sensitive paths / subagent names)
|
|
19
|
-
|
|
20
|
-
```
|
|
21
|
-
<paste the output here>
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Your setup
|
|
25
|
-
|
|
26
|
-
- OS: <!-- macOS 15.3 / Ubuntu 22.04 / ... -->
|
|
27
|
-
- Bun version: <!-- `bun --version` -->
|
|
28
|
-
- agent-ledger version: <!-- `agent-ledger --version` (pre-alpha: use commit SHA) -->
|
|
29
|
-
- Claude Code version: <!-- `claude --version` -->
|
|
30
|
-
|
|
31
|
-
## If you're on pay-as-you-go, the calibration ask
|
|
32
|
-
|
|
33
|
-
If you compare `agent-ledger month` against a real Anthropic invoice and
|
|
34
|
-
they differ by > 10%, I want to hear about it. Even one line: "agent-ledger
|
|
35
|
-
says $X, invoice says $Y" is incredibly useful. Thanks.
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: Model pricing gap
|
|
3
|
-
about: A model ID shows $0 in your ledger — we're missing it from the price table
|
|
4
|
-
title: 'pricing: add <model-id>'
|
|
5
|
-
labels: pricing
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## Model ID
|
|
9
|
-
|
|
10
|
-
<!-- e.g. claude-opus-4-8-20260301 -->
|
|
11
|
-
|
|
12
|
-
## Anthropic's published rates
|
|
13
|
-
|
|
14
|
-
- Input: $<X> / 1M tokens
|
|
15
|
-
- Output: $<Y> / 1M tokens
|
|
16
|
-
- Link to Anthropic pricing page (with date):
|
|
17
|
-
|
|
18
|
-
## Where you saw it
|
|
19
|
-
|
|
20
|
-
Sample from your log (redact the content, keep the model field):
|
|
21
|
-
|
|
22
|
-
```json
|
|
23
|
-
{"message":{"model":"<model-id>", "usage":{"input_tokens":..., "output_tokens":...}}}
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Optional: PR
|
|
27
|
-
|
|
28
|
-
If you want to open the PR, the pricing table lives in `src/pricing.ts`. Add a
|
|
29
|
-
matching test in `test/pricing.test.ts`.
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
## What this PR does
|
|
2
|
-
|
|
3
|
-
<!-- One or two sentences. -->
|
|
4
|
-
|
|
5
|
-
## Checklist
|
|
6
|
-
|
|
7
|
-
- [ ] `bun run typecheck` passes
|
|
8
|
-
- [ ] `bun test` passes
|
|
9
|
-
- [ ] If you touched `src/pricing.ts`, there's a matching test in
|
|
10
|
-
`test/pricing.test.ts`
|
|
11
|
-
- [ ] If you touched `src/parser.ts`, the change doesn't break the nested
|
|
12
|
-
`subagents/` walk (see `test/parser.test.ts`)
|
|
13
|
-
- [ ] No runtime dependencies added (this tool intentionally has none)
|
|
14
|
-
|
|
15
|
-
## Log snippet (if relevant)
|
|
16
|
-
|
|
17
|
-
<!-- If the change is about handling some real log shape, include a redacted
|
|
18
|
-
example of what it looks like. Helps me reason about whether other tools
|
|
19
|
-
produce the same shape. -->
|