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/CHANGELOG.md DELETED
@@ -1,105 +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.4.1] — 2026-04-22
9
-
10
- ### Added
11
- - `--anonymize` flag — replaces project paths with `~/repo-A`, `~/repo-B`,
12
- ... and session ids with `sess-A`, `sess-B`, ... ordered by spend descending.
13
- Use this when you want to share your output publicly (HN / Bluesky /
14
- presentation) without leaking employer names or internal repo names.
15
- Subagent / model / day labels stay untouched (those are public info).
16
-
17
- ## [0.4.0] — 2026-04-22
18
-
19
- ### Added
20
- - `--by project` — group spend by Claude Code project directory.
21
- Decodes `~/.claude/projects/-Users-xiangjie-clawbot` → `~/clawbot` so you can
22
- see which project is burning budget.
23
- - `--by session` — group by session ID. Surfaces the single most expensive
24
- conversations (useful for hunting the one that went sideways).
25
- - Summary dashboard now shows:
26
- - `Projects` count
27
- - `Top project` line
28
- - `Project mix` block — top 3 projects with $ and %
29
- - `decodeProjectId()` exported for reuse. Special-cases the `~/` home prefix,
30
- falls back to slash-joined path for other roots.
31
- - Row label column now auto-widens up to 40 chars for project paths.
32
-
33
- ### Changed
34
- - `formatTable` takes an optional `groupName` arg so the header column matches
35
- the group (e.g. "project", "session").
36
-
37
- ## [0.3.0] — 2026-04-22
38
-
39
- ### Added
40
- - `--summary` flag — single-screen dashboard with shadow cost, top subagent /
41
- model / day, cache reuse ratio, and **leverage vs $200/mo Max plan**.
42
- This is the killer view for "why install over /cost?"
43
- - Auto-derived insights footer on the default table:
44
- - Top contributor with %
45
- - 1h cache spend call-out (when ≥5% of total)
46
- - Cache reuse ratio (read/write) with verdict
47
- - Monthly $ equivalent + leverage vs Max plan
48
- - Daily-mode insights: peak day, avg/day, peak/trough variance
49
-
50
- ### Changed
51
- - `--by day` polish:
52
- - Bar fixed to 40-char width so session counts align in their own column
53
- - Brighter cyan (`\x1b[96m`) bar character
54
- - Total row separated by horizontal rule, properly aligned
55
- - Header line now shows period + total + sessions + group count at a glance:
56
- `agent-ledger week 2026-04-15 → 2026-04-22 · $13,356.27 shadow · 217 sessions · 7 agents`
57
-
58
- ## [0.2.0] — 2026-04-21
59
-
60
- ### Added
61
- - `--by <subagent|model|day>` flag — group rows by subagent (default), by
62
- Claude model, or by date. The `day` mode renders an ASCII bar chart of
63
- daily spend, useful for spotting "expensive Tuesdays."
64
- - Server-tool pricing: `web_search_requests` and `web_fetch_requests` from
65
- the `server_tool_use` block are now counted at $0.01/request and surface
66
- in a footer row when present.
67
- - ANSI color output, TTY-aware (NO_COLOR / FORCE_COLOR respected).
68
- - Subagent / model / day labels in cyan
69
- - Cost amounts color-coded by magnitude (red ≥$1000, yellow ≥$100,
70
- green ≥$1, dim <$1)
71
- - Total row bolded
72
- - New tests for server-tool pricing, `--by model`, and `--by day`
73
- (22 tests, up from 17).
74
-
75
- ### Changed
76
- - `aggregate(turns, from, to)` accepts an optional 4th arg `group: GroupKey`
77
- (`'subagent' | 'model' | 'day'`). Default behavior unchanged.
78
- - `AggregatedRow` now carries `webSearchRequests` and `webFetchRequests`
79
- counters; `CostBreakdown` adds `serverToolUseCost`.
80
-
81
- ### Not yet
82
- - Calibration against real Anthropic invoices (need volunteers — see
83
- `.github/ISSUE_TEMPLATE/`)
84
- - Cost forecasting (trailing 7-day burn rate)
85
- - Homebrew tap
86
-
87
- ## [0.1.0] — 2026-04-21
88
-
89
- Initial public release.
90
-
91
- ### Added
92
- - JSONL parser with recursive walk of `~/.claude/projects/**`, including nested
93
- `subagents/` directories
94
- - Per-subagent attribution via sidecar `agent-<hash>.meta.json` files
95
- - Pricing with cache TTL split: `ephemeral_5m_input_tokens` (1.25×) vs
96
- `ephemeral_1h_input_tokens` (2×)
97
- - Terminal table output (default) and Markdown export (`--md`)
98
- - JSON output (`--json`) for piping into other tools
99
- - `--plan pro|max` to suppress dollar columns on fixed-price subscriptions
100
- - Support for current and prior-generation model IDs (Opus 4.6/4.7,
101
- Sonnet 4.5/4.6, Haiku 4.0/4.5)
102
- - Family-prefix fallback for unknown future models (e.g. `claude-opus-5-*`
103
- resolves to Opus pricing)
104
- - `<synthetic>` turns (Claude Code context compaction) correctly priced at $0
105
- - 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,24 +0,0 @@
1
- {
2
- "lockfileVersion": 1,
3
- "configVersion": 1,
4
- "workspaces": {
5
- "": {
6
- "name": "agent-ledger",
7
- "devDependencies": {
8
- "@types/bun": "latest",
9
- "typescript": "^5.6.0",
10
- },
11
- },
12
- },
13
- "packages": {
14
- "@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=="],
15
-
16
- "@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=="],
17
-
18
- "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=="],
19
-
20
- "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=="],
21
-
22
- "undici-types": ["undici-types@7.19.2", "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
23
- }
24
- }
@@ -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}}}
@@ -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
- });
@@ -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
- });
package/tsconfig.json DELETED
@@ -1,20 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "lib": ["ES2022"],
7
- "types": ["bun-types"],
8
- "strict": true,
9
- "noUncheckedIndexedAccess": true,
10
- "noImplicitOverride": true,
11
- "skipLibCheck": true,
12
- "allowImportingTsExtensions": true,
13
- "resolveJsonModule": true,
14
- "isolatedModules": true,
15
- "noEmit": true,
16
- "forceConsistentCasingInFileNames": true,
17
- "verbatimModuleSyntax": true
18
- },
19
- "include": ["src/**/*", "bin/**/*", "test/**/*"]
20
- }