claude-agent-ledger 0.4.0 → 0.7.0
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 +104 -0
- package/README.md +18 -1
- package/bin/agent-ledger.ts +206 -41
- package/bun.lock +5 -0
- package/package.json +2 -1
- package/src/aggregator.ts +173 -71
- package/src/explain.ts +207 -0
- package/src/parser.ts +110 -32
- package/src/tree.ts +107 -0
- package/src/types.ts +26 -8
- package/test/property.test.ts +188 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,110 @@ All notable changes to `claude-agent-ledger` will be documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
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
|
+
|
|
8
112
|
## [0.4.0] — 2026-04-22
|
|
9
113
|
|
|
10
114
|
### Added
|
package/README.md
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/claude-agent-ledger)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
+
> *Even I needed `--anonymize` before posting my own data publicly.
|
|
9
|
+
> That's why the flag exists. Use it before you screenshot.*
|
|
10
|
+
|
|
8
11
|
---
|
|
9
12
|
|
|
10
13
|
## Why
|
|
@@ -28,11 +31,24 @@ npm install -g claude-agent-ledger
|
|
|
28
31
|
# or: bun install -g claude-agent-ledger
|
|
29
32
|
|
|
30
33
|
agent-ledger week --summary # the dashboard (start here)
|
|
31
|
-
agent-ledger week --
|
|
34
|
+
agent-ledger week --tree # subagent fan-out tree (the MOAT view) — new in 0.5
|
|
35
|
+
agent-ledger week --by tool # which tool ate the budget (Read/Bash/Grep/MCP) — new in 0.5
|
|
36
|
+
agent-ledger week --by model # opus vs sonnet vs haiku
|
|
32
37
|
agent-ledger week --by project # which project ate your tokens
|
|
33
38
|
agent-ledger week --by session # which single sessions went sideways
|
|
34
39
|
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 — new in 0.5
|
|
41
|
+
agent-ledger week --json | jq # JSON output for ANY view incl. summary — new in 0.5
|
|
35
42
|
agent-ledger month --md > report.md
|
|
43
|
+
|
|
44
|
+
# Live tail — polling delta meter — new in 0.7
|
|
45
|
+
agent-ledger watch # refresh every 10s
|
|
46
|
+
agent-ledger watch 4 # refresh every 4s
|
|
47
|
+
|
|
48
|
+
# Root-cause analysis on a single expensive session — new in 0.6
|
|
49
|
+
agent-ledger explain <session-id> # heuristic: top-3 turns + tool/model breakdown
|
|
50
|
+
ANTHROPIC_API_KEY=sk-ant-... agent-ledger explain <session-id>
|
|
51
|
+
# LLM mode: Haiku 4.5 ≤4-bullet root cause (~$0.001/run)
|
|
36
52
|
```
|
|
37
53
|
|
|
38
54
|
No server. No account. No data leaves your machine.
|
|
@@ -180,6 +196,7 @@ If Anthropic's actual rate differs (or web_fetch has its own price), open an iss
|
|
|
180
196
|
|---|---|
|
|
181
197
|
| `--summary` | One-screen dashboard with all the headline numbers |
|
|
182
198
|
| `--by <subagent\|model\|day\|project\|session>` | Group rows. Default: `subagent`. `day` renders as ASCII bar chart. |
|
|
199
|
+
| `--anonymize` | Replace project paths and session ids with `~/repo-A`, `sess-A`, ... so you can share screenshots without doxing your employer / private repos. |
|
|
183
200
|
| `--md` | Markdown table output (good for committing or sharing) |
|
|
184
201
|
| `--json` | Raw JSON output (pipe into jq) |
|
|
185
202
|
| `--plan pro\|max` | Hide dollar columns; show token utilization only |
|
package/bin/agent-ledger.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
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 { formatTree, formatTreeMarkdown } from '../src/tree.ts';
|
|
7
|
+
import type { LedgerReport } from '../src/types.ts';
|
|
6
8
|
|
|
7
9
|
type Period = 'today' | 'week' | 'month';
|
|
8
10
|
type Plan = 'payg' | 'pro' | 'max';
|
|
@@ -14,6 +16,10 @@ interface Args {
|
|
|
14
16
|
plan: Plan;
|
|
15
17
|
by: GroupKey;
|
|
16
18
|
summary: boolean;
|
|
19
|
+
tree: boolean;
|
|
20
|
+
anonymize: boolean;
|
|
21
|
+
budget?: number; // USD threshold; non-zero exit if exceeded
|
|
22
|
+
verbose: boolean;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
function periodRange(period: Period): { from: Date; to: Date } {
|
|
@@ -32,32 +38,39 @@ function periodRange(period: Period): { from: Date; to: Date } {
|
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
function usage(): never {
|
|
35
|
-
console.error(`Usage:
|
|
41
|
+
console.error(`Usage:
|
|
42
|
+
agent-ledger <today|week|month> [flags] — aggregate views
|
|
43
|
+
agent-ledger explain <sessionId> — root-cause why one session was expensive (new in 0.6)
|
|
36
44
|
|
|
37
45
|
Flags:
|
|
38
|
-
--summary One-screen dashboard
|
|
39
|
-
|
|
40
|
-
--by <subagent|model|day|project|session>
|
|
46
|
+
--summary One-screen dashboard (start here).
|
|
47
|
+
--by <subagent|model|day|project|session|tool>
|
|
41
48
|
Group rows by:
|
|
42
49
|
subagent (default) — agent attribution
|
|
43
50
|
model — opus vs sonnet vs haiku
|
|
44
51
|
day — daily ASCII bar chart
|
|
45
52
|
project — by Claude Code project (decoded path)
|
|
46
53
|
session — by session id (top spenders)
|
|
54
|
+
tool — by Read/Bash/Write/Grep — what your turns burn on
|
|
55
|
+
--tree Subagent graph: parent→child cost attribution.
|
|
56
|
+
The MOAT view: orchestrator → 3×researcher + 6×swift-dev = $312.
|
|
57
|
+
--budget <USD> Exit non-zero (code 2) if total cost > USD. Pipe-friendly.
|
|
47
58
|
--md Markdown table output (good for sharing / commits)
|
|
48
|
-
--json Raw JSON output (
|
|
59
|
+
--json Raw JSON output (also works with --summary now)
|
|
49
60
|
--plan pro|max Suppress dollar columns (token utilization only)
|
|
61
|
+
--anonymize Replace project paths and session ids with safe placeholders.
|
|
62
|
+
--verbose Print parse stats (files, turns, elapsed) to stderr.
|
|
50
63
|
|
|
51
64
|
Environment:
|
|
52
65
|
NO_COLOR=1 Disable ANSI colors
|
|
53
66
|
FORCE_COLOR=1 Force colors when piping
|
|
54
67
|
|
|
55
68
|
Examples:
|
|
56
|
-
agent-ledger week --summary
|
|
57
|
-
agent-ledger week --
|
|
58
|
-
agent-ledger week --by
|
|
59
|
-
agent-ledger month --md > month.md
|
|
69
|
+
agent-ledger week --summary # the dashboard
|
|
70
|
+
agent-ledger week --tree # subagent fan-out tree
|
|
71
|
+
agent-ledger week --by tool # which Claude tool ate the budget
|
|
60
72
|
agent-ledger week --json | jq '.total.cost.totalCost'
|
|
73
|
+
agent-ledger today --budget 50 # CI gate: fail if today > $50
|
|
61
74
|
`);
|
|
62
75
|
process.exit(1);
|
|
63
76
|
}
|
|
@@ -68,6 +81,7 @@ function parseArgs(argv: string[]): Args {
|
|
|
68
81
|
|
|
69
82
|
let plan: Plan = 'payg';
|
|
70
83
|
let by: GroupKey = 'subagent';
|
|
84
|
+
let budget: number | undefined;
|
|
71
85
|
for (let i = 0; i < rest.length; i++) {
|
|
72
86
|
const flag = rest[i];
|
|
73
87
|
if (flag === '--plan') {
|
|
@@ -85,13 +99,24 @@ function parseArgs(argv: string[]): Args {
|
|
|
85
99
|
next === 'model' ||
|
|
86
100
|
next === 'day' ||
|
|
87
101
|
next === 'project' ||
|
|
88
|
-
next === 'session'
|
|
102
|
+
next === 'session' ||
|
|
103
|
+
next === 'tool'
|
|
89
104
|
) {
|
|
90
105
|
by = next;
|
|
91
106
|
i++;
|
|
92
107
|
} else {
|
|
108
|
+
console.error(`agent-ledger: unknown --by value '${next}'`);
|
|
93
109
|
usage();
|
|
94
110
|
}
|
|
111
|
+
} else if (flag === '--budget') {
|
|
112
|
+
const next = rest[i + 1];
|
|
113
|
+
const n = Number(next);
|
|
114
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
115
|
+
console.error(`agent-ledger: --budget requires a positive number (got '${next}')`);
|
|
116
|
+
usage();
|
|
117
|
+
}
|
|
118
|
+
budget = n;
|
|
119
|
+
i++;
|
|
95
120
|
}
|
|
96
121
|
}
|
|
97
122
|
return {
|
|
@@ -101,7 +126,22 @@ function parseArgs(argv: string[]): Args {
|
|
|
101
126
|
plan,
|
|
102
127
|
by,
|
|
103
128
|
summary: rest.includes('--summary'),
|
|
129
|
+
tree: rest.includes('--tree'),
|
|
130
|
+
anonymize: rest.includes('--anonymize'),
|
|
131
|
+
budget,
|
|
132
|
+
verbose: rest.includes('--verbose'),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function anonymizeReport(report: LedgerReport, group: GroupKey): LedgerReport {
|
|
137
|
+
if (group !== 'project' && group !== 'session') return report;
|
|
138
|
+
const prefix = group === 'project' ? '~/repo-' : 'sess-';
|
|
139
|
+
const labelFor = (i: number): string => {
|
|
140
|
+
if (i < 26) return prefix + String.fromCharCode(65 + i);
|
|
141
|
+
return prefix + String.fromCharCode(65 + Math.floor(i / 26) - 1) + String.fromCharCode(65 + (i % 26));
|
|
104
142
|
};
|
|
143
|
+
const newRows = report.rows.map((r, i) => ({ ...r, subagent: labelFor(i) }));
|
|
144
|
+
return { ...report, rows: newRows };
|
|
105
145
|
}
|
|
106
146
|
|
|
107
147
|
function planMask(report: LedgerReport, plan: Plan): LedgerReport {
|
|
@@ -122,47 +162,172 @@ function planMask(report: LedgerReport, plan: Plan): LedgerReport {
|
|
|
122
162
|
};
|
|
123
163
|
}
|
|
124
164
|
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
165
|
+
// `--watch` live-tail mode. Re-aggregates "today" every N seconds and
|
|
166
|
+
// prints a clean delta meter. Polling-based (no inotify) so it works on
|
|
167
|
+
// any platform Bun runs on. Ctrl-C to exit.
|
|
168
|
+
async function watchCmd(intervalSec: number): Promise<void> {
|
|
169
|
+
const interval = Math.max(2, intervalSec);
|
|
170
|
+
let prevCost = 0;
|
|
171
|
+
let prevTurnCount = 0;
|
|
172
|
+
const t0 = Date.now();
|
|
173
|
+
console.log(`agent-ledger watch — refreshing every ${interval}s · Ctrl-C to exit`);
|
|
174
|
+
console.log('');
|
|
175
|
+
const tick = async () => {
|
|
176
|
+
const { from, to } = periodRange('today');
|
|
177
|
+
const agg = new Aggregator('subagent', from, to);
|
|
178
|
+
let turns = 0;
|
|
179
|
+
for await (const turn of parseAll({ from, to })) {
|
|
180
|
+
turns++;
|
|
181
|
+
agg.add(turn);
|
|
182
|
+
}
|
|
183
|
+
const report = agg.finalize();
|
|
184
|
+
const cost = report.total.cost.totalCost;
|
|
185
|
+
const dCost = cost - prevCost;
|
|
186
|
+
const dTurns = turns - prevTurnCount;
|
|
187
|
+
const elapsedMin = ((Date.now() - t0) / 60000).toFixed(1);
|
|
188
|
+
const arrow = dCost > 0 ? '↑' : dCost < 0 ? '↓' : '·';
|
|
189
|
+
const ts = new Date().toISOString().slice(11, 19);
|
|
190
|
+
const top = report.rows[0];
|
|
191
|
+
const topLabel = top ? `top: ${top.subagent} ${fmtUsdInline(top.cost.totalCost)}` : 'no data';
|
|
192
|
+
console.log(
|
|
193
|
+
`[${ts}] today $${cost.toFixed(2)} ${arrow} +$${dCost.toFixed(2)} · ${turns} turns (Δ${dTurns}) · ${topLabel} · live ${elapsedMin}m`,
|
|
194
|
+
);
|
|
195
|
+
prevCost = cost;
|
|
196
|
+
prevTurnCount = turns;
|
|
197
|
+
};
|
|
198
|
+
await tick();
|
|
199
|
+
const id = setInterval(() => {
|
|
200
|
+
tick().catch((e) => console.error('watch tick error:', e));
|
|
201
|
+
}, interval * 1000);
|
|
202
|
+
process.on('SIGINT', () => {
|
|
203
|
+
clearInterval(id);
|
|
204
|
+
console.log('\nagent-ledger watch — exiting');
|
|
205
|
+
process.exit(0);
|
|
206
|
+
});
|
|
207
|
+
// Keep process alive
|
|
208
|
+
await new Promise(() => {});
|
|
130
209
|
}
|
|
131
210
|
|
|
132
|
-
|
|
133
|
-
|
|
211
|
+
const fmtUsdInline = (n: number): string => '$' + n.toFixed(n < 0.01 ? 4 : 2);
|
|
212
|
+
|
|
213
|
+
async function explainCmd(sessionPrefix: string | undefined): Promise<void> {
|
|
214
|
+
if (!sessionPrefix) {
|
|
215
|
+
console.error('agent-ledger explain: missing sessionId argument');
|
|
216
|
+
console.error('Usage: agent-ledger explain <sessionId>');
|
|
217
|
+
console.error(' (sessionId can be a full uuid or 8-char prefix)');
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
const analysis = await analyzeSession(sessionPrefix);
|
|
221
|
+
if (!analysis) {
|
|
222
|
+
console.error(`agent-ledger explain: no session found matching '${sessionPrefix}'`);
|
|
223
|
+
console.error(' (try the 8-char prefix from `agent-ledger week --by session`)');
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
const heuristic = formatHeuristic(analysis);
|
|
227
|
+
console.log(heuristic);
|
|
228
|
+
const llm = await llmExplain(analysis);
|
|
229
|
+
if (llm) {
|
|
230
|
+
console.log('');
|
|
231
|
+
console.log(llm);
|
|
232
|
+
} else if (!process.env.ANTHROPIC_API_KEY) {
|
|
233
|
+
console.log('');
|
|
234
|
+
console.log('💡 Set ANTHROPIC_API_KEY to enable LLM root-cause analysis (~$0.001/run via Haiku 4.5)');
|
|
235
|
+
}
|
|
134
236
|
}
|
|
135
237
|
|
|
136
238
|
async function main(): Promise<void> {
|
|
239
|
+
// Subcommand dispatch.
|
|
240
|
+
if (process.argv[2] === 'explain') {
|
|
241
|
+
return explainCmd(process.argv[3]);
|
|
242
|
+
}
|
|
243
|
+
if (process.argv[2] === 'watch') {
|
|
244
|
+
const interval = Number(process.argv[3]) || 10;
|
|
245
|
+
return watchCmd(interval);
|
|
246
|
+
}
|
|
137
247
|
const args = parseArgs(process.argv.slice(2));
|
|
138
248
|
const { from, to } = periodRange(args.period);
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
249
|
+
const t0 = Date.now();
|
|
250
|
+
|
|
251
|
+
// Determine which aggregators we need based on flags.
|
|
252
|
+
// Single parser pass dispatches to all in parallel — no buffering.
|
|
253
|
+
const aggBySub = new Aggregator('subagent', from, to);
|
|
254
|
+
const aggByMod = args.summary ? new Aggregator('model', from, to) : undefined;
|
|
255
|
+
const aggByDay = args.summary ? new Aggregator('day', from, to) : undefined;
|
|
256
|
+
const aggByProj = args.summary ? new Aggregator('project', from, to) : undefined;
|
|
257
|
+
const aggUser = !args.summary && !args.tree ? new Aggregator(args.by, from, to) : undefined;
|
|
258
|
+
const graph = args.tree ? new SubagentGraph() : undefined;
|
|
259
|
+
|
|
260
|
+
let turnCount = 0;
|
|
261
|
+
for await (const turn of parseAll({ from, to })) {
|
|
262
|
+
turnCount++;
|
|
263
|
+
aggBySub.add(turn);
|
|
264
|
+
aggByMod?.add(turn);
|
|
265
|
+
aggByDay?.add(turn);
|
|
266
|
+
aggByProj?.add(turn);
|
|
267
|
+
aggUser?.add(turn);
|
|
268
|
+
graph?.add(turn);
|
|
150
269
|
}
|
|
151
270
|
|
|
152
|
-
|
|
153
|
-
|
|
271
|
+
if (args.verbose) {
|
|
272
|
+
const elapsed = Date.now() - t0;
|
|
273
|
+
console.error(
|
|
274
|
+
`agent-ledger: parsed ${turnCount} turns from ~/.claude/projects in ${elapsed}ms`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let totalCost = aggBySub.finalize().total.cost.totalCost;
|
|
154
279
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
280
|
+
if (args.tree) {
|
|
281
|
+
const roots = graph!.finalize(20);
|
|
282
|
+
totalCost = roots.reduce((s, r) => s + r.totalCost, 0);
|
|
283
|
+
const out =
|
|
284
|
+
args.json
|
|
285
|
+
? JSON.stringify({ roots, totalCost }, null, 2)
|
|
286
|
+
: args.markdown
|
|
287
|
+
? formatTreeMarkdown(roots, totalCost)
|
|
288
|
+
: formatTree(roots, totalCost);
|
|
289
|
+
console.log(out);
|
|
290
|
+
} else if (args.summary) {
|
|
291
|
+
const bySubR = aggBySub.finalize();
|
|
292
|
+
const byModR = aggByMod!.finalize();
|
|
293
|
+
const byDayR = aggByDay!.finalize();
|
|
294
|
+
let byProjR = aggByProj!.finalize();
|
|
295
|
+
if (args.anonymize) byProjR = anonymizeReport(byProjR, 'project');
|
|
296
|
+
totalCost = bySubR.total.cost.totalCost;
|
|
297
|
+
if (args.json) {
|
|
298
|
+
console.log(
|
|
299
|
+
JSON.stringify(
|
|
300
|
+
{ subagent: bySubR, model: byModR, day: byDayR, project: byProjR, totalCost },
|
|
301
|
+
null,
|
|
302
|
+
2,
|
|
303
|
+
),
|
|
304
|
+
);
|
|
305
|
+
} else {
|
|
306
|
+
console.log(formatSummary(bySubR, byModR, byDayR, byProjR, args.period));
|
|
307
|
+
}
|
|
162
308
|
} else {
|
|
163
|
-
|
|
309
|
+
let report = aggUser!.finalize();
|
|
310
|
+
if (args.anonymize) report = anonymizeReport(report, args.by);
|
|
311
|
+
report = planMask(report, args.plan);
|
|
312
|
+
totalCost = report.total.cost.totalCost;
|
|
313
|
+
const output =
|
|
314
|
+
args.json
|
|
315
|
+
? JSON.stringify(report, null, 2)
|
|
316
|
+
: args.markdown
|
|
317
|
+
? formatMarkdown(report, args.period)
|
|
318
|
+
: args.by === 'day'
|
|
319
|
+
? formatTrend(report, args.period)
|
|
320
|
+
: formatTable(report, args.period, args.by);
|
|
321
|
+
console.log(output);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Budget gate — non-zero exit code so this can be a CI/shell hook.
|
|
325
|
+
if (args.budget !== undefined && totalCost > args.budget) {
|
|
326
|
+
console.error(
|
|
327
|
+
`agent-ledger: BUDGET EXCEEDED — total $${totalCost.toFixed(2)} > budget $${args.budget.toFixed(2)}`,
|
|
328
|
+
);
|
|
329
|
+
process.exit(2);
|
|
164
330
|
}
|
|
165
|
-
console.log(output);
|
|
166
331
|
}
|
|
167
332
|
|
|
168
333
|
main().catch((err) => {
|
package/bun.lock
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"name": "agent-ledger",
|
|
7
7
|
"devDependencies": {
|
|
8
8
|
"@types/bun": "latest",
|
|
9
|
+
"fast-check": "^4.7.0",
|
|
9
10
|
"typescript": "^5.6.0",
|
|
10
11
|
},
|
|
11
12
|
},
|
|
@@ -17,6 +18,10 @@
|
|
|
17
18
|
|
|
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=="],
|
|
19
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
|
+
|
|
20
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=="],
|
|
21
26
|
|
|
22
27
|
"undici-types": ["undici-types@7.19.2", "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-agent-ledger",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "See exactly where your Claude Code budget goes — per subagent, per task.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/bun": "latest",
|
|
34
|
+
"fast-check": "^4.7.0",
|
|
34
35
|
"typescript": "^5.6.0"
|
|
35
36
|
}
|
|
36
37
|
}
|