claude-agent-ledger 0.7.0 → 0.7.2
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 +73 -11
- 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
|
@@ -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. -->
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
test:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
steps:
|
|
13
|
-
- uses: actions/checkout@v4
|
|
14
|
-
- uses: oven-sh/setup-bun@v2
|
|
15
|
-
with:
|
|
16
|
-
bun-version: latest
|
|
17
|
-
- run: bun install --frozen-lockfile
|
|
18
|
-
- run: bun run typecheck
|
|
19
|
-
- run: bun test
|
package/CHANGELOG.md
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to `claude-agent-ledger` will be documented here.
|
|
4
|
-
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
-
|
|
8
|
-
## [0.7.0] — 2026-04-23
|
|
9
|
-
|
|
10
|
-
### Added — live tail + property tests
|
|
11
|
-
|
|
12
|
-
- **`agent-ledger watch [intervalSec]`** — live polling mode. Re-aggregates
|
|
13
|
-
today's cost every N seconds (default 10), prints a delta meter:
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
[14:46:34] today $3310.71 ↑ +$3310.71 · 9560 turns (Δ9560) · top: (main) $3167.95
|
|
17
|
-
[14:46:40] today $3310.71 · +$0.00 · 9560 turns (Δ0) · top: (main) $3167.95
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Answers "what's happening right now" without needing to re-run queries.
|
|
21
|
-
Ctrl-C to exit.
|
|
22
|
-
|
|
23
|
-
- **Property tests via `fast-check`** — 7 new tests, 1400+ generated cases
|
|
24
|
-
covering:
|
|
25
|
-
- `costFor()` returns non-negative for any valid input
|
|
26
|
-
- `totalCost === sum(components)` exactly (ε < 1e-9)
|
|
27
|
-
- `cache_read ≤ fresh input` cost (90% discount holds)
|
|
28
|
-
- `cache_1h ≥ cache_5m` cost (1h tier always more expensive)
|
|
29
|
-
- `Aggregator.total === sum(rows)` for any random turn stream
|
|
30
|
-
- Non-assistant turns contribute 0 cost
|
|
31
|
-
- `SubagentGraph.totalCost ≥ selfCost` (children only add)
|
|
32
|
-
|
|
33
|
-
Total test count: 22 → 29 (kept all prior tests passing).
|
|
34
|
-
|
|
35
|
-
## [0.6.0] — 2026-04-23
|
|
36
|
-
|
|
37
|
-
### Added — `agent-ledger explain <sessionId>` (the LLM-native move)
|
|
38
|
-
|
|
39
|
-
A new subcommand that answers **"why was this single session expensive?"**
|
|
40
|
-
This is the question Helicone / Langfuse / OpenLLMetry physically cannot
|
|
41
|
-
answer — they observe API calls, not Claude Code's prompts and tool calls.
|
|
42
|
-
|
|
43
|
-
Two modes:
|
|
44
|
-
|
|
45
|
-
- **Heuristic mode** (default, no API key needed) — analyzes the session
|
|
46
|
-
JSONL and prints:
|
|
47
|
-
- Subagent + total cost + turn count
|
|
48
|
-
- Model breakdown (which model dominated)
|
|
49
|
-
- Tool invocation count (Read/Bash/Grep/Edit/MCP — top 8)
|
|
50
|
-
- Top 3 most expensive turns with full token + tool breakdown
|
|
51
|
-
- Cache reuse ratio + verdict (`<5×` warning, `>20×` good)
|
|
52
|
-
|
|
53
|
-
- **LLM mode** (set `ANTHROPIC_API_KEY`) — pipes the heuristic evidence into
|
|
54
|
-
Haiku 4.5 with a tightly-scoped prompt: "identify the SPECIFIC root cause
|
|
55
|
-
+ ONE concrete action to cut this session's cost by 50%+." Costs about
|
|
56
|
-
$0.001/run. Cite turn numbers from the breakdown.
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
agent-ledger explain 63063a38
|
|
60
|
-
agent-ledger explain b988bd89-803a-4c78 # full uuid or 8-char prefix both work
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
## [0.5.0] — 2026-04-23
|
|
64
|
-
|
|
65
|
-
### Added — performance + production-readiness foundation
|
|
66
|
-
|
|
67
|
-
- **Streaming JSONL parser** — replaces `await file.text()` (which OOM'd on
|
|
68
|
-
10GB+ logs). Now reads via `Bun.file().stream()` + `TextDecoder` line-by-line.
|
|
69
|
-
Memory profile is constant regardless of log size.
|
|
70
|
-
- **Concurrent file walk** — `findSessionLogs` parallelizes per-project
|
|
71
|
-
`readdir` (`Promise.all`), and `parseAll` reads up to 8 JSONL files
|
|
72
|
-
concurrently. ~5–10× cold-start speedup on multi-project setups.
|
|
73
|
-
- **Zero materialization** — bin no longer collects every turn into an array
|
|
74
|
-
before aggregating. Instead one parser stream fans out to N `Aggregator`
|
|
75
|
-
instances in parallel. Memory is now O(unique-keys) not O(turns).
|
|
76
|
-
- **`--by tool`** — new GroupKey. Attribute spend per tool call (`Read`,
|
|
77
|
-
`Bash`, `Grep`, `Write`, MCP tool names). Answers "which tool is eating my
|
|
78
|
-
budget" — the most actionable lens for a Claude Code user.
|
|
79
|
-
- **`--tree` (the MOAT)** — subagent-graph cost attribution. Walks the
|
|
80
|
-
parent→child orchestration tree and shows
|
|
81
|
-
`orchestrator → 3×researcher + 6×swift-dev = $312`. Helicone / Langfuse /
|
|
82
|
-
OpenLLMetry can't do this — they observe API calls, not Claude Code's
|
|
83
|
-
multi-agent runtime.
|
|
84
|
-
- **`--budget <USD>`** — exit code `2` if total cost exceeds budget. Wire it
|
|
85
|
-
into your CI / shell startup / `pre-commit` to gate spend.
|
|
86
|
-
- **`--json` works for `--summary`** — previously summary was text-only.
|
|
87
|
-
Now pipe-friendly: `agent-ledger week --summary --json | jq`.
|
|
88
|
-
- **`--verbose`** — print parse stats (turns / elapsed) to stderr.
|
|
89
|
-
|
|
90
|
-
### Changed
|
|
91
|
-
|
|
92
|
-
- `Aggregator` is now a class (`add()` / `finalize()`) instead of an async
|
|
93
|
-
function. Enables single-pass fan-out and live-tail in v0.6.
|
|
94
|
-
- `parseAll` and `parseFile` no longer block on full file reads.
|
|
95
|
-
- `SessionTurn` adds `parentSessionId` and `toolUses[]` fields.
|
|
96
|
-
|
|
97
|
-
### Internal
|
|
98
|
-
|
|
99
|
-
- Backwards-compatible: the old `aggregate(turns, from, to, group)` async
|
|
100
|
-
function still exists and wraps the new `Aggregator` class. All v0.4.x
|
|
101
|
-
tests pass without modification (22/22).
|
|
102
|
-
|
|
103
|
-
## [0.4.1] — 2026-04-22
|
|
104
|
-
|
|
105
|
-
### Added
|
|
106
|
-
- `--anonymize` flag — replaces project paths with `~/repo-A`, `~/repo-B`,
|
|
107
|
-
... and session ids with `sess-A`, `sess-B`, ... ordered by spend descending.
|
|
108
|
-
Use this when you want to share your output publicly (HN / Bluesky /
|
|
109
|
-
presentation) without leaking employer names or internal repo names.
|
|
110
|
-
Subagent / model / day labels stay untouched (those are public info).
|
|
111
|
-
|
|
112
|
-
## [0.4.0] — 2026-04-22
|
|
113
|
-
|
|
114
|
-
### Added
|
|
115
|
-
- `--by project` — group spend by Claude Code project directory.
|
|
116
|
-
Decodes `~/.claude/projects/-Users-xiangjie-clawbot` → `~/clawbot` so you can
|
|
117
|
-
see which project is burning budget.
|
|
118
|
-
- `--by session` — group by session ID. Surfaces the single most expensive
|
|
119
|
-
conversations (useful for hunting the one that went sideways).
|
|
120
|
-
- Summary dashboard now shows:
|
|
121
|
-
- `Projects` count
|
|
122
|
-
- `Top project` line
|
|
123
|
-
- `Project mix` block — top 3 projects with $ and %
|
|
124
|
-
- `decodeProjectId()` exported for reuse. Special-cases the `~/` home prefix,
|
|
125
|
-
falls back to slash-joined path for other roots.
|
|
126
|
-
- Row label column now auto-widens up to 40 chars for project paths.
|
|
127
|
-
|
|
128
|
-
### Changed
|
|
129
|
-
- `formatTable` takes an optional `groupName` arg so the header column matches
|
|
130
|
-
the group (e.g. "project", "session").
|
|
131
|
-
|
|
132
|
-
## [0.3.0] — 2026-04-22
|
|
133
|
-
|
|
134
|
-
### Added
|
|
135
|
-
- `--summary` flag — single-screen dashboard with shadow cost, top subagent /
|
|
136
|
-
model / day, cache reuse ratio, and **leverage vs $200/mo Max plan**.
|
|
137
|
-
This is the killer view for "why install over /cost?"
|
|
138
|
-
- Auto-derived insights footer on the default table:
|
|
139
|
-
- Top contributor with %
|
|
140
|
-
- 1h cache spend call-out (when ≥5% of total)
|
|
141
|
-
- Cache reuse ratio (read/write) with verdict
|
|
142
|
-
- Monthly $ equivalent + leverage vs Max plan
|
|
143
|
-
- Daily-mode insights: peak day, avg/day, peak/trough variance
|
|
144
|
-
|
|
145
|
-
### Changed
|
|
146
|
-
- `--by day` polish:
|
|
147
|
-
- Bar fixed to 40-char width so session counts align in their own column
|
|
148
|
-
- Brighter cyan (`\x1b[96m`) bar character
|
|
149
|
-
- Total row separated by horizontal rule, properly aligned
|
|
150
|
-
- Header line now shows period + total + sessions + group count at a glance:
|
|
151
|
-
`agent-ledger week 2026-04-15 → 2026-04-22 · $13,356.27 shadow · 217 sessions · 7 agents`
|
|
152
|
-
|
|
153
|
-
## [0.2.0] — 2026-04-21
|
|
154
|
-
|
|
155
|
-
### Added
|
|
156
|
-
- `--by <subagent|model|day>` flag — group rows by subagent (default), by
|
|
157
|
-
Claude model, or by date. The `day` mode renders an ASCII bar chart of
|
|
158
|
-
daily spend, useful for spotting "expensive Tuesdays."
|
|
159
|
-
- Server-tool pricing: `web_search_requests` and `web_fetch_requests` from
|
|
160
|
-
the `server_tool_use` block are now counted at $0.01/request and surface
|
|
161
|
-
in a footer row when present.
|
|
162
|
-
- ANSI color output, TTY-aware (NO_COLOR / FORCE_COLOR respected).
|
|
163
|
-
- Subagent / model / day labels in cyan
|
|
164
|
-
- Cost amounts color-coded by magnitude (red ≥$1000, yellow ≥$100,
|
|
165
|
-
green ≥$1, dim <$1)
|
|
166
|
-
- Total row bolded
|
|
167
|
-
- New tests for server-tool pricing, `--by model`, and `--by day`
|
|
168
|
-
(22 tests, up from 17).
|
|
169
|
-
|
|
170
|
-
### Changed
|
|
171
|
-
- `aggregate(turns, from, to)` accepts an optional 4th arg `group: GroupKey`
|
|
172
|
-
(`'subagent' | 'model' | 'day'`). Default behavior unchanged.
|
|
173
|
-
- `AggregatedRow` now carries `webSearchRequests` and `webFetchRequests`
|
|
174
|
-
counters; `CostBreakdown` adds `serverToolUseCost`.
|
|
175
|
-
|
|
176
|
-
### Not yet
|
|
177
|
-
- Calibration against real Anthropic invoices (need volunteers — see
|
|
178
|
-
`.github/ISSUE_TEMPLATE/`)
|
|
179
|
-
- Cost forecasting (trailing 7-day burn rate)
|
|
180
|
-
- Homebrew tap
|
|
181
|
-
|
|
182
|
-
## [0.1.0] — 2026-04-21
|
|
183
|
-
|
|
184
|
-
Initial public release.
|
|
185
|
-
|
|
186
|
-
### Added
|
|
187
|
-
- JSONL parser with recursive walk of `~/.claude/projects/**`, including nested
|
|
188
|
-
`subagents/` directories
|
|
189
|
-
- Per-subagent attribution via sidecar `agent-<hash>.meta.json` files
|
|
190
|
-
- Pricing with cache TTL split: `ephemeral_5m_input_tokens` (1.25×) vs
|
|
191
|
-
`ephemeral_1h_input_tokens` (2×)
|
|
192
|
-
- Terminal table output (default) and Markdown export (`--md`)
|
|
193
|
-
- JSON output (`--json`) for piping into other tools
|
|
194
|
-
- `--plan pro|max` to suppress dollar columns on fixed-price subscriptions
|
|
195
|
-
- Support for current and prior-generation model IDs (Opus 4.6/4.7,
|
|
196
|
-
Sonnet 4.5/4.6, Haiku 4.0/4.5)
|
|
197
|
-
- Family-prefix fallback for unknown future models (e.g. `claude-opus-5-*`
|
|
198
|
-
resolves to Opus pricing)
|
|
199
|
-
- `<synthetic>` turns (Claude Code context compaction) correctly priced at $0
|
|
200
|
-
- Published to npm as `claude-agent-ledger`
|
package/CONTRIBUTING.md
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# Contributing
|
|
2
|
-
|
|
3
|
-
Thanks for looking. This project is pre-alpha and in active shape — I'm still learning
|
|
4
|
-
what people actually need from a Claude Code ledger.
|
|
5
|
-
|
|
6
|
-
## Quick help
|
|
7
|
-
|
|
8
|
-
The most useful contributions right now:
|
|
9
|
-
|
|
10
|
-
1. **Run it on your own logs and tell me what it got wrong.** Open an issue with
|
|
11
|
-
the shadow cost number `agent-ledger month` reports, and the actual Anthropic
|
|
12
|
-
invoice if you're on pay-as-you-go. I want to calibrate.
|
|
13
|
-
2. **Add a model I missed.** `src/pricing.ts` has the table. Open a PR with the
|
|
14
|
-
model ID and the published rates, plus a test in `test/pricing.test.ts`.
|
|
15
|
-
3. **Report a log format surprise.** If `agent-ledger` skips usage records or
|
|
16
|
-
attributes them to `(main)` when they should be a subagent, paste a redacted
|
|
17
|
-
log snippet into an issue.
|
|
18
|
-
|
|
19
|
-
## Running locally
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
bun install
|
|
23
|
-
bun test
|
|
24
|
-
bun run typecheck
|
|
25
|
-
bun run bin/agent-ledger.ts today
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Style
|
|
29
|
-
|
|
30
|
-
- TypeScript strict. `noUncheckedIndexedAccess` is on.
|
|
31
|
-
- Short functions. If a function needs a comment to explain what it does, the
|
|
32
|
-
function is probably doing too much.
|
|
33
|
-
- No runtime dependencies except Bun built-ins (we want a small install surface).
|
|
34
|
-
- Tests use `bun:test`. Fixtures live in `test/fixtures/`.
|
|
35
|
-
|
|
36
|
-
## Out of scope (for now)
|
|
37
|
-
|
|
38
|
-
- Real-time dashboards / web UIs
|
|
39
|
-
- Sending data anywhere (the tool is read-only by design)
|
|
40
|
-
- Tracking API keys or authenticating to Anthropic
|
|
41
|
-
|
|
42
|
-
If you want those, that's a different tool — and that's fine, just not this one.
|
package/bun.lock
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"lockfileVersion": 1,
|
|
3
|
-
"configVersion": 1,
|
|
4
|
-
"workspaces": {
|
|
5
|
-
"": {
|
|
6
|
-
"name": "agent-ledger",
|
|
7
|
-
"devDependencies": {
|
|
8
|
-
"@types/bun": "latest",
|
|
9
|
-
"fast-check": "^4.7.0",
|
|
10
|
-
"typescript": "^5.6.0",
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
},
|
|
14
|
-
"packages": {
|
|
15
|
-
"@types/bun": ["@types/bun@1.3.12", "https://registry.npmmirror.com/@types/bun/-/bun-1.3.12.tgz", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="],
|
|
16
|
-
|
|
17
|
-
"@types/node": ["@types/node@25.6.0", "https://registry.npmmirror.com/@types/node/-/node-25.6.0.tgz", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
|
|
18
|
-
|
|
19
|
-
"bun-types": ["bun-types@1.3.12", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.12.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
|
20
|
-
|
|
21
|
-
"fast-check": ["fast-check@4.7.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ=="],
|
|
22
|
-
|
|
23
|
-
"pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="],
|
|
24
|
-
|
|
25
|
-
"typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
26
|
-
|
|
27
|
-
"undici-types": ["undici-types@7.19.2", "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
|
|
28
|
-
}
|
|
29
|
-
}
|
package/test/aggregator.test.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { aggregate } from '../src/aggregator.ts';
|
|
4
|
-
import { parseFile } from '../src/parser.ts';
|
|
5
|
-
|
|
6
|
-
const FIXTURE = join(import.meta.dir, 'fixtures', 'sample.jsonl');
|
|
7
|
-
|
|
8
|
-
describe('aggregate()', () => {
|
|
9
|
-
test('groups assistant turns by subagent', async () => {
|
|
10
|
-
const from = new Date('2026-04-21T00:00:00Z');
|
|
11
|
-
const to = new Date('2026-04-21T23:59:59Z');
|
|
12
|
-
const report = await aggregate(parseFile(FIXTURE), from, to);
|
|
13
|
-
|
|
14
|
-
const agents = report.rows.map((r) => r.subagent);
|
|
15
|
-
expect(agents).toContain('ios-factory');
|
|
16
|
-
expect(agents).toContain('Reality Checker');
|
|
17
|
-
expect(agents).toContain('(main)');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test('skips user turns (no usage data)', async () => {
|
|
21
|
-
const from = new Date('2026-04-21T00:00:00Z');
|
|
22
|
-
const to = new Date('2026-04-21T23:59:59Z');
|
|
23
|
-
const report = await aggregate(parseFile(FIXTURE), from, to);
|
|
24
|
-
|
|
25
|
-
// 4 assistant turns in fixture (3 subagents, some with >1 turn each).
|
|
26
|
-
const totalSessions = report.total.sessionCount;
|
|
27
|
-
expect(totalSessions).toBeGreaterThan(0);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('rows are sorted by total cost descending', async () => {
|
|
31
|
-
const from = new Date('2026-04-21T00:00:00Z');
|
|
32
|
-
const to = new Date('2026-04-21T23:59:59Z');
|
|
33
|
-
const report = await aggregate(parseFile(FIXTURE), from, to);
|
|
34
|
-
|
|
35
|
-
for (let i = 1; i < report.rows.length; i++) {
|
|
36
|
-
const prev = report.rows[i - 1]!.cost.totalCost;
|
|
37
|
-
const curr = report.rows[i]!.cost.totalCost;
|
|
38
|
-
expect(prev).toBeGreaterThanOrEqual(curr);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('total row sums all agent rows', async () => {
|
|
43
|
-
const from = new Date('2026-04-21T00:00:00Z');
|
|
44
|
-
const to = new Date('2026-04-21T23:59:59Z');
|
|
45
|
-
const report = await aggregate(parseFile(FIXTURE), from, to);
|
|
46
|
-
|
|
47
|
-
const sumCost = report.rows.reduce((s, r) => s + r.cost.totalCost, 0);
|
|
48
|
-
expect(report.total.cost.totalCost).toBeCloseTo(sumCost, 6);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('--by model groups by model id', async () => {
|
|
52
|
-
const from = new Date('2026-04-21T00:00:00Z');
|
|
53
|
-
const to = new Date('2026-04-21T23:59:59Z');
|
|
54
|
-
const report = await aggregate(parseFile(FIXTURE), from, to, 'model');
|
|
55
|
-
|
|
56
|
-
// Fixture has assistant turns with claude-opus-4-7 model
|
|
57
|
-
const labels = report.rows.map((r) => r.subagent);
|
|
58
|
-
expect(labels.some((l) => l.includes('claude-') || l === '(no-model)')).toBe(true);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test('--by day groups by ISO date and sorts chronologically', async () => {
|
|
62
|
-
const from = new Date('2026-04-21T00:00:00Z');
|
|
63
|
-
const to = new Date('2026-04-21T23:59:59Z');
|
|
64
|
-
const report = await aggregate(parseFile(FIXTURE), from, to, 'day');
|
|
65
|
-
|
|
66
|
-
// Each row label should look like a date YYYY-MM-DD
|
|
67
|
-
for (const row of report.rows) {
|
|
68
|
-
expect(row.subagent).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
69
|
-
}
|
|
70
|
-
// Ascending date order
|
|
71
|
-
for (let i = 1; i < report.rows.length; i++) {
|
|
72
|
-
expect(report.rows[i - 1]!.subagent <= report.rows[i]!.subagent).toBe(true);
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
});
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
{"type":"assistant","timestamp":"2026-04-21T10:05:00Z","message":{"role":"assistant","model":"claude-sonnet-4-6","usage":{"input_tokens":4200,"output_tokens":1300,"cache_creation_input_tokens":2000,"cache_read_input_tokens":22000}}}
|
|
2
|
-
{"type":"assistant","timestamp":"2026-04-21T10:07:00Z","message":{"role":"assistant","model":"claude-sonnet-4-6","usage":{"input_tokens":800,"output_tokens":220,"cache_read_input_tokens":26000}}}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"agentType":"Reality Checker","description":"Review migration safety"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
{"type":"user","timestamp":"2026-04-21T10:00:00Z","message":{"role":"user","content":"hello"}}
|
|
2
|
-
{"type":"assistant","timestamp":"2026-04-21T10:00:12Z","message":{"role":"assistant","model":"claude-opus-4-7","usage":{"input_tokens":1200,"output_tokens":420,"cache_read_input_tokens":8000}}}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
{"type":"user","timestamp":"2026-04-21T10:00:00Z","message":{"role":"user","content":"Fix the release script"}}
|
|
2
|
-
{"type":"assistant","timestamp":"2026-04-21T10:00:12Z","subagentType":"ios-factory","message":{"role":"assistant","model":"claude-opus-4-7","usage":{"input_tokens":4200,"output_tokens":1180,"cache_creation_input_tokens":2400,"cache_read_input_tokens":18000}}}
|
|
3
|
-
{"type":"assistant","timestamp":"2026-04-21T10:01:02Z","subagentType":"ios-factory","message":{"role":"assistant","model":"claude-opus-4-7","usage":{"input_tokens":2100,"output_tokens":640,"cache_read_input_tokens":22000}}}
|
|
4
|
-
{"type":"user","timestamp":"2026-04-21T11:30:00Z","message":{"role":"user","content":"Review this PR"}}
|
|
5
|
-
{"type":"assistant","timestamp":"2026-04-21T11:30:08Z","subagentType":"Reality Checker","message":{"role":"assistant","model":"claude-sonnet-4-6","usage":{"input_tokens":8800,"output_tokens":1520,"cache_creation_input_tokens":1200,"cache_read_input_tokens":44000}}}
|
|
6
|
-
{"type":"assistant","timestamp":"2026-04-21T15:10:00Z","message":{"role":"assistant","model":"claude-haiku-4-5-20251001","usage":{"input_tokens":1200,"output_tokens":320}}}
|
package/test/parser.test.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { aggregate } from '../src/aggregator.ts';
|
|
4
|
-
import { parseAll } from '../src/parser.ts';
|
|
5
|
-
|
|
6
|
-
const PROJECTS_DIR = join(import.meta.dir, 'fixtures', 'projects');
|
|
7
|
-
|
|
8
|
-
describe('parseAll()', () => {
|
|
9
|
-
test('walks nested subagents/ directories and attributes subagent type', async () => {
|
|
10
|
-
const from = new Date('2026-04-21T00:00:00Z');
|
|
11
|
-
const to = new Date('2026-04-21T23:59:59Z');
|
|
12
|
-
|
|
13
|
-
const report = await aggregate(
|
|
14
|
-
parseAll({ from, to, projectsDir: PROJECTS_DIR }),
|
|
15
|
-
from,
|
|
16
|
-
to,
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
const agents = report.rows.map((r) => r.subagent).sort();
|
|
20
|
-
expect(agents).toContain('(main)');
|
|
21
|
-
expect(agents).toContain('Reality Checker');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test('main-session turns are attributed to (main), not to any subagent', async () => {
|
|
25
|
-
const from = new Date('2026-04-21T00:00:00Z');
|
|
26
|
-
const to = new Date('2026-04-21T23:59:59Z');
|
|
27
|
-
|
|
28
|
-
const report = await aggregate(
|
|
29
|
-
parseAll({ from, to, projectsDir: PROJECTS_DIR }),
|
|
30
|
-
from,
|
|
31
|
-
to,
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
const main = report.rows.find((r) => r.subagent === '(main)');
|
|
35
|
-
const realityChecker = report.rows.find((r) => r.subagent === 'Reality Checker');
|
|
36
|
-
|
|
37
|
-
expect(main).toBeDefined();
|
|
38
|
-
expect(realityChecker).toBeDefined();
|
|
39
|
-
expect(main!.sessionCount).toBe(1);
|
|
40
|
-
expect(realityChecker!.sessionCount).toBe(1);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('respects date range filter', async () => {
|
|
44
|
-
const from = new Date('2030-01-01T00:00:00Z');
|
|
45
|
-
const to = new Date('2030-01-02T00:00:00Z');
|
|
46
|
-
|
|
47
|
-
const report = await aggregate(
|
|
48
|
-
parseAll({ from, to, projectsDir: PROJECTS_DIR }),
|
|
49
|
-
from,
|
|
50
|
-
to,
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
expect(report.rows.length).toBe(0);
|
|
54
|
-
});
|
|
55
|
-
});
|
package/test/pricing.test.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { costFor, isKnownModel } from '../src/pricing.ts';
|
|
3
|
-
|
|
4
|
-
describe('costFor()', () => {
|
|
5
|
-
test('Opus output is $75/M', () => {
|
|
6
|
-
const cost = costFor(
|
|
7
|
-
{ input_tokens: 0, output_tokens: 1_000_000 },
|
|
8
|
-
'claude-opus-4-7',
|
|
9
|
-
);
|
|
10
|
-
expect(cost.outputCost).toBeCloseTo(75, 6);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
test('Opus input is $15/M', () => {
|
|
14
|
-
const cost = costFor(
|
|
15
|
-
{ input_tokens: 1_000_000, output_tokens: 0 },
|
|
16
|
-
'claude-opus-4-7',
|
|
17
|
-
);
|
|
18
|
-
expect(cost.inputCost).toBeCloseTo(15, 6);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test('5-minute cache creation is 1.25× base input', () => {
|
|
22
|
-
const cost = costFor(
|
|
23
|
-
{
|
|
24
|
-
input_tokens: 0,
|
|
25
|
-
output_tokens: 0,
|
|
26
|
-
cache_creation: { ephemeral_5m_input_tokens: 1_000_000 },
|
|
27
|
-
},
|
|
28
|
-
'claude-opus-4-7',
|
|
29
|
-
);
|
|
30
|
-
// Opus input $15 × 1.25 = $18.75 per 1M
|
|
31
|
-
expect(cost.cacheCreation5mCost).toBeCloseTo(18.75, 4);
|
|
32
|
-
expect(cost.cacheCreation1hCost).toBeCloseTo(0, 6);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('1-hour cache creation is 2× base input (the TTL trap)', () => {
|
|
36
|
-
const cost = costFor(
|
|
37
|
-
{
|
|
38
|
-
input_tokens: 0,
|
|
39
|
-
output_tokens: 0,
|
|
40
|
-
cache_creation: { ephemeral_1h_input_tokens: 1_000_000 },
|
|
41
|
-
},
|
|
42
|
-
'claude-opus-4-7',
|
|
43
|
-
);
|
|
44
|
-
// Opus input $15 × 2 = $30 per 1M
|
|
45
|
-
expect(cost.cacheCreation1hCost).toBeCloseTo(30, 4);
|
|
46
|
-
expect(cost.cacheCreation5mCost).toBeCloseTo(0, 6);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('cache read is 10% of base input', () => {
|
|
50
|
-
const cost = costFor(
|
|
51
|
-
{
|
|
52
|
-
input_tokens: 0,
|
|
53
|
-
output_tokens: 0,
|
|
54
|
-
cache_read_input_tokens: 1_000_000,
|
|
55
|
-
},
|
|
56
|
-
'claude-opus-4-7',
|
|
57
|
-
);
|
|
58
|
-
// Opus input $15 × 0.1 = $1.50 per 1M
|
|
59
|
-
expect(cost.cacheReadCost).toBeCloseTo(1.5, 4);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('legacy flat cache_creation_input_tokens falls back to 5m TTL pricing', () => {
|
|
63
|
-
const cost = costFor(
|
|
64
|
-
{
|
|
65
|
-
input_tokens: 0,
|
|
66
|
-
output_tokens: 0,
|
|
67
|
-
cache_creation_input_tokens: 1_000_000,
|
|
68
|
-
},
|
|
69
|
-
'claude-opus-4-7',
|
|
70
|
-
);
|
|
71
|
-
expect(cost.cacheCreation5mCost).toBeCloseTo(18.75, 4);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test('prior-generation model IDs (opus-4-6) resolve to current pricing', () => {
|
|
75
|
-
const cost = costFor(
|
|
76
|
-
{ input_tokens: 1_000_000, output_tokens: 0 },
|
|
77
|
-
'claude-opus-4-6',
|
|
78
|
-
);
|
|
79
|
-
expect(cost.inputCost).toBeCloseTo(15, 6);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('unknown future opus variant falls back to opus family pricing', () => {
|
|
83
|
-
const cost = costFor(
|
|
84
|
-
{ input_tokens: 1_000_000, output_tokens: 0 },
|
|
85
|
-
'claude-opus-9-9',
|
|
86
|
-
);
|
|
87
|
-
expect(cost.inputCost).toBeCloseTo(15, 6);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test('<synthetic> model is free', () => {
|
|
91
|
-
const cost = costFor(
|
|
92
|
-
{
|
|
93
|
-
input_tokens: 1_000_000,
|
|
94
|
-
output_tokens: 1_000_000,
|
|
95
|
-
cache_read_input_tokens: 1_000_000,
|
|
96
|
-
},
|
|
97
|
-
'<synthetic>',
|
|
98
|
-
);
|
|
99
|
-
expect(cost.totalCost).toBe(0);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test('isKnownModel recognizes current, prior, and family matches', () => {
|
|
103
|
-
expect(isKnownModel('claude-opus-4-7')).toBe(true);
|
|
104
|
-
expect(isKnownModel('claude-opus-4-6')).toBe(true);
|
|
105
|
-
expect(isKnownModel('claude-sonnet-4-6')).toBe(true);
|
|
106
|
-
expect(isKnownModel('<synthetic>')).toBe(true);
|
|
107
|
-
expect(isKnownModel('something-entirely-unknown')).toBe(false);
|
|
108
|
-
expect(isKnownModel(undefined)).toBe(false);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test('server_tool_use: web_search at $0.01/req', () => {
|
|
112
|
-
const cost = costFor(
|
|
113
|
-
{
|
|
114
|
-
input_tokens: 0,
|
|
115
|
-
output_tokens: 0,
|
|
116
|
-
server_tool_use: { web_search_requests: 100 },
|
|
117
|
-
},
|
|
118
|
-
'claude-opus-4-7',
|
|
119
|
-
);
|
|
120
|
-
// 100 × $0.01 = $1.00
|
|
121
|
-
expect(cost.serverToolUseCost).toBeCloseTo(1.0, 6);
|
|
122
|
-
expect(cost.totalCost).toBeCloseTo(1.0, 6);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test('server_tool_use: web_fetch counted, summed with web_search', () => {
|
|
126
|
-
const cost = costFor(
|
|
127
|
-
{
|
|
128
|
-
input_tokens: 0,
|
|
129
|
-
output_tokens: 0,
|
|
130
|
-
server_tool_use: { web_search_requests: 50, web_fetch_requests: 50 },
|
|
131
|
-
},
|
|
132
|
-
'claude-opus-4-7',
|
|
133
|
-
);
|
|
134
|
-
expect(cost.serverToolUseCost).toBeCloseTo(1.0, 6);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test('server_tool_use absent → no cost', () => {
|
|
138
|
-
const cost = costFor(
|
|
139
|
-
{ input_tokens: 1000, output_tokens: 1000 },
|
|
140
|
-
'claude-opus-4-7',
|
|
141
|
-
);
|
|
142
|
-
expect(cost.serverToolUseCost).toBe(0);
|
|
143
|
-
});
|
|
144
|
-
});
|