claude-agent-ledger 0.3.0 → 0.4.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 +20 -0
- package/README.md +23 -10
- package/bin/agent-ledger.ts +18 -6
- package/package.json +1 -1
- package/src/aggregator.ts +22 -2
- package/src/format.ts +52 -24
- package/src/parser.ts +11 -6
- package/src/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ 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.4.0] — 2026-04-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `--by project` — group spend by Claude Code project directory.
|
|
12
|
+
Decodes `~/.claude/projects/-Users-xiangjie-clawbot` → `~/clawbot` so you can
|
|
13
|
+
see which project is burning budget.
|
|
14
|
+
- `--by session` — group by session ID. Surfaces the single most expensive
|
|
15
|
+
conversations (useful for hunting the one that went sideways).
|
|
16
|
+
- Summary dashboard now shows:
|
|
17
|
+
- `Projects` count
|
|
18
|
+
- `Top project` line
|
|
19
|
+
- `Project mix` block — top 3 projects with $ and %
|
|
20
|
+
- `decodeProjectId()` exported for reuse. Special-cases the `~/` home prefix,
|
|
21
|
+
falls back to slash-joined path for other roots.
|
|
22
|
+
- Row label column now auto-widens up to 40 chars for project paths.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- `formatTable` takes an optional `groupName` arg so the header column matches
|
|
26
|
+
the group (e.g. "project", "session").
|
|
27
|
+
|
|
8
28
|
## [0.3.0] — 2026-04-22
|
|
9
29
|
|
|
10
30
|
### Added
|
package/README.md
CHANGED
|
@@ -29,6 +29,8 @@ npm install -g claude-agent-ledger
|
|
|
29
29
|
|
|
30
30
|
agent-ledger week --summary # the dashboard (start here)
|
|
31
31
|
agent-ledger week --by model # which Claude model burned the budget
|
|
32
|
+
agent-ledger week --by project # which project ate your tokens
|
|
33
|
+
agent-ledger week --by session # which single sessions went sideways
|
|
32
34
|
agent-ledger week --by day # daily bar chart with peak/avg/variance
|
|
33
35
|
agent-ledger month --md > report.md
|
|
34
36
|
```
|
|
@@ -41,7 +43,10 @@ Claude Code's built-in `/cost` shows the **current session** cost. That's it.
|
|
|
41
43
|
|
|
42
44
|
`claude-agent-ledger` answers questions `/cost` can't:
|
|
43
45
|
|
|
44
|
-
- Which of my **10+ subagents** burned the budget? (
|
|
46
|
+
- Which of my **10+ subagents** burned the budget? (default)
|
|
47
|
+
- Which **Claude model** burned the budget — Opus vs Sonnet vs Haiku? (`--by model`)
|
|
48
|
+
- Which **project** in `~/.claude/projects/` ate the tokens? (`--by project`)
|
|
49
|
+
- Which **single sessions** went sideways and burned $X? (`--by session`)
|
|
45
50
|
- Which **day** of last week was the expensive one? (`--by day`)
|
|
46
51
|
- What would my Claude Max usage have cost on **pay-as-you-go**? (the "shadow cost" framing)
|
|
47
52
|
- Is my **cache reuse ratio** healthy? (auto-derived insight)
|
|
@@ -56,17 +61,24 @@ Run `agent-ledger week --summary` once and decide.
|
|
|
56
61
|
```
|
|
57
62
|
agent-ledger week summary 2026-04-15 → 2026-04-22
|
|
58
63
|
|
|
59
|
-
Shadow cost $13,
|
|
60
|
-
Sessions
|
|
64
|
+
Shadow cost $13,450.05
|
|
65
|
+
Sessions 219
|
|
66
|
+
Projects 9
|
|
61
67
|
Plan reference $200/mo (Max)
|
|
62
|
-
Multiplier
|
|
68
|
+
Multiplier 252× vs Max plan
|
|
63
69
|
|
|
64
|
-
Top subagent (main) $12,
|
|
65
|
-
Top model claude-opus-4-7 $7,
|
|
66
|
-
|
|
70
|
+
Top subagent (main) $12,787.19 (95%)
|
|
71
|
+
Top model claude-opus-4-7 $7,278.87 (54%)
|
|
72
|
+
Top project ~/clawbot $6,657.93 (50%)
|
|
73
|
+
Peak day 2026-04-15 $3,679.16
|
|
67
74
|
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
Project mix
|
|
76
|
+
~/clawbot $6,657.93 (50%)
|
|
77
|
+
~/clawbot-work $4,433.32 (33%)
|
|
78
|
+
~/ZStack-zstack-workspace $1,890.02 (14%)
|
|
79
|
+
|
|
80
|
+
Cache 1h writes 119.65M tokens · $3589.46
|
|
81
|
+
Cache reads 5.59B tokens · $8236.15
|
|
70
82
|
Cache reuse 38× reads/writes
|
|
71
83
|
Server tools none
|
|
72
84
|
```
|
|
@@ -166,7 +178,8 @@ If Anthropic's actual rate differs (or web_fetch has its own price), open an iss
|
|
|
166
178
|
|
|
167
179
|
| Flag | What it does |
|
|
168
180
|
|---|---|
|
|
169
|
-
| `--
|
|
181
|
+
| `--summary` | One-screen dashboard with all the headline numbers |
|
|
182
|
+
| `--by <subagent\|model\|day\|project\|session>` | Group rows. Default: `subagent`. `day` renders as ASCII bar chart. |
|
|
170
183
|
| `--md` | Markdown table output (good for committing or sharing) |
|
|
171
184
|
| `--json` | Raw JSON output (pipe into jq) |
|
|
172
185
|
| `--plan pro\|max` | Hide dollar columns; show token utilization only |
|
package/bin/agent-ledger.ts
CHANGED
|
@@ -37,8 +37,13 @@ function usage(): never {
|
|
|
37
37
|
Flags:
|
|
38
38
|
--summary One-screen dashboard: total + top subagent + top
|
|
39
39
|
model + peak day + cache reuse + leverage vs $200/mo plan
|
|
40
|
-
--by <subagent|model|day>
|
|
41
|
-
|
|
40
|
+
--by <subagent|model|day|project|session>
|
|
41
|
+
Group rows by:
|
|
42
|
+
subagent (default) — agent attribution
|
|
43
|
+
model — opus vs sonnet vs haiku
|
|
44
|
+
day — daily ASCII bar chart
|
|
45
|
+
project — by Claude Code project (decoded path)
|
|
46
|
+
session — by session id (top spenders)
|
|
42
47
|
--md Markdown table output (good for sharing / commits)
|
|
43
48
|
--json Raw JSON output (for piping into jq)
|
|
44
49
|
--plan pro|max Suppress dollar columns (token utilization only)
|
|
@@ -75,7 +80,13 @@ function parseArgs(argv: string[]): Args {
|
|
|
75
80
|
}
|
|
76
81
|
} else if (flag === '--by') {
|
|
77
82
|
const next = rest[i + 1];
|
|
78
|
-
if (
|
|
83
|
+
if (
|
|
84
|
+
next === 'subagent' ||
|
|
85
|
+
next === 'model' ||
|
|
86
|
+
next === 'day' ||
|
|
87
|
+
next === 'project' ||
|
|
88
|
+
next === 'session'
|
|
89
|
+
) {
|
|
79
90
|
by = next;
|
|
80
91
|
i++;
|
|
81
92
|
} else {
|
|
@@ -128,12 +139,13 @@ async function main(): Promise<void> {
|
|
|
128
139
|
const turns = await collectTurns({ from, to });
|
|
129
140
|
|
|
130
141
|
if (args.summary) {
|
|
131
|
-
const [bySub, byMod, byDay] = await Promise.all([
|
|
142
|
+
const [bySub, byMod, byDay, byProj] = await Promise.all([
|
|
132
143
|
aggregate(iterTurns(turns), from, to, 'subagent'),
|
|
133
144
|
aggregate(iterTurns(turns), from, to, 'model'),
|
|
134
145
|
aggregate(iterTurns(turns), from, to, 'day'),
|
|
146
|
+
aggregate(iterTurns(turns), from, to, 'project'),
|
|
135
147
|
]);
|
|
136
|
-
console.log(formatSummary(bySub, byMod, byDay, args.period));
|
|
148
|
+
console.log(formatSummary(bySub, byMod, byDay, byProj, args.period));
|
|
137
149
|
return;
|
|
138
150
|
}
|
|
139
151
|
|
|
@@ -148,7 +160,7 @@ async function main(): Promise<void> {
|
|
|
148
160
|
} else if (args.by === 'day') {
|
|
149
161
|
output = formatTrend(report, args.period);
|
|
150
162
|
} else {
|
|
151
|
-
output = formatTable(report, args.period);
|
|
163
|
+
output = formatTable(report, args.period, args.by);
|
|
152
164
|
}
|
|
153
165
|
console.log(output);
|
|
154
166
|
}
|
package/package.json
CHANGED
package/src/aggregator.ts
CHANGED
|
@@ -3,8 +3,26 @@ import type { AggregatedRow, LedgerReport, SessionTurn } from './types.ts';
|
|
|
3
3
|
|
|
4
4
|
const MAIN_AGENT_LABEL = '(main)';
|
|
5
5
|
const UNKNOWN_MODEL_LABEL = '(no-model)';
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const UNKNOWN_PROJECT_LABEL = '(no-project)';
|
|
7
|
+
|
|
8
|
+
export type GroupKey = 'subagent' | 'model' | 'day' | 'project' | 'session';
|
|
9
|
+
|
|
10
|
+
// Decode Claude Code's project directory name to something readable.
|
|
11
|
+
// Encoding rule: absolute path with `/` replaced by `-` (leading `-` for root).
|
|
12
|
+
// "-Users-xiangjie-clawbot" → "~/clawbot"
|
|
13
|
+
// "-Users-xiangjie-newlive-agent-ledger" → "~/newlive/agent-ledger" (best-effort; ambiguous on dirs containing '-')
|
|
14
|
+
// We special-case the "$HOME" prefix (`-Users-<user>-`) to render `~` and skip the rest of the
|
|
15
|
+
// "-" → "/" conversion, since that's the only place we can be sure of segment boundaries.
|
|
16
|
+
export function decodeProjectId(encoded: string): string {
|
|
17
|
+
if (!encoded.startsWith('-')) return encoded;
|
|
18
|
+
const parts = encoded.slice(1).split('-'); // drop leading '-'
|
|
19
|
+
// Match common $HOME pattern: ['Users', '<user>', ...rest]
|
|
20
|
+
if (parts.length >= 3 && parts[0] === 'Users') {
|
|
21
|
+
return '~/' + parts.slice(2).join('-');
|
|
22
|
+
}
|
|
23
|
+
// Fallback: just convert all - to /
|
|
24
|
+
return '/' + parts.join('/');
|
|
25
|
+
}
|
|
8
26
|
|
|
9
27
|
function emptyRow(label: string): AggregatedRow {
|
|
10
28
|
return {
|
|
@@ -44,6 +62,8 @@ function splitCache(turn: SessionTurn): { fiveMin: number; oneHour: number } {
|
|
|
44
62
|
function keyOf(turn: SessionTurn, group: GroupKey): string {
|
|
45
63
|
if (group === 'model') return turn.model ?? UNKNOWN_MODEL_LABEL;
|
|
46
64
|
if (group === 'day') return turn.timestamp.slice(0, 10);
|
|
65
|
+
if (group === 'project') return decodeProjectId(turn.projectId ?? UNKNOWN_PROJECT_LABEL);
|
|
66
|
+
if (group === 'session') return turn.sessionId.slice(0, 8);
|
|
47
67
|
return turn.subagentType ?? MAIN_AGENT_LABEL;
|
|
48
68
|
}
|
|
49
69
|
|
package/src/format.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import type { AggregatedRow, LedgerReport } from './types.ts';
|
|
2
2
|
|
|
3
3
|
const PAD_LABEL = 22;
|
|
4
|
+
const PAD_LABEL_WIDE = 40; // for project paths
|
|
4
5
|
const BAR_WIDTH = 40;
|
|
5
6
|
const PLAN_MAX_PRICE = 200; // monthly USD reference for shadow-cost leverage
|
|
6
7
|
|
|
8
|
+
// Detect when we should give labels more room (project paths can be long).
|
|
9
|
+
function widestLabel(rows: { subagent: string }[]): number {
|
|
10
|
+
let w = 0;
|
|
11
|
+
for (const r of rows) if (r.subagent.length > w) w = r.subagent.length;
|
|
12
|
+
return w;
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
// Color helpers — TTY-aware, NO_COLOR-respecting (https://no-color.org/).
|
|
8
16
|
const USE_COLOR = (() => {
|
|
9
17
|
if (process.env.NO_COLOR) return false;
|
|
@@ -52,8 +60,8 @@ function pct(part: number, whole: number): string {
|
|
|
52
60
|
return `${((part / whole) * 100).toFixed(0)}%`;
|
|
53
61
|
}
|
|
54
62
|
|
|
55
|
-
function formatRow(row: AggregatedRow, isTotal = false): string {
|
|
56
|
-
const labelText = row.subagent.padEnd(
|
|
63
|
+
function formatRow(row: AggregatedRow, padWidth: number, isTotal = false): string {
|
|
64
|
+
const labelText = row.subagent.padEnd(padWidth);
|
|
57
65
|
const label = isTotal
|
|
58
66
|
? `${c.bold}${labelText}${c.reset}`
|
|
59
67
|
: `${c.cyan}${labelText}${c.reset}`;
|
|
@@ -126,19 +134,20 @@ function deriveInsights(report: LedgerReport): string[] {
|
|
|
126
134
|
return lines;
|
|
127
135
|
}
|
|
128
136
|
|
|
129
|
-
export function formatTable(report: LedgerReport, label: string): string {
|
|
137
|
+
export function formatTable(report: LedgerReport, label: string, groupName: string = 'subagent'): string {
|
|
138
|
+
const padWidth = Math.max(PAD_LABEL, Math.min(PAD_LABEL_WIDE, widestLabel(report.rows) + 2));
|
|
130
139
|
const summaryLine =
|
|
131
140
|
`${c.gray}${report.period.from.toISOString().slice(0, 10)} → ${report.period.to.toISOString().slice(0, 10)}${c.reset}` +
|
|
132
141
|
` ${c.dim}·${c.reset} ` +
|
|
133
142
|
`${c.bold}${fmtUsdComma(report.total.cost.totalCost)}${c.reset} ${c.dim}shadow${c.reset}` +
|
|
134
143
|
` ${c.dim}·${c.reset} ${report.total.sessionCount} sessions` +
|
|
135
|
-
` ${c.dim}·${c.reset} ${report.rows.length} ${
|
|
144
|
+
` ${c.dim}·${c.reset} ${report.rows.length} ${groupName}s`;
|
|
136
145
|
|
|
137
146
|
const header = `${c.bold}agent-ledger ${label}${c.reset} ${summaryLine}`;
|
|
138
147
|
|
|
139
148
|
const colsRaw =
|
|
140
149
|
' ' +
|
|
141
|
-
|
|
150
|
+
groupName.padEnd(padWidth) +
|
|
142
151
|
'sess'.padStart(4) +
|
|
143
152
|
' ' +
|
|
144
153
|
'tokens(in/out)'.padStart(18) +
|
|
@@ -148,7 +157,7 @@ export function formatTable(report: LedgerReport, label: string): string {
|
|
|
148
157
|
'cost'.padStart(9);
|
|
149
158
|
const cols = `${c.gray}${colsRaw}${c.reset}`;
|
|
150
159
|
|
|
151
|
-
const sepRaw = ' ' + '─'.repeat(
|
|
160
|
+
const sepRaw = ' ' + '─'.repeat(padWidth + 4 + 2 + 18 + 2 + 32 + 2 + 9);
|
|
152
161
|
const sep = `${c.gray}${sepRaw}${c.reset}`;
|
|
153
162
|
|
|
154
163
|
const lines = [
|
|
@@ -156,9 +165,9 @@ export function formatTable(report: LedgerReport, label: string): string {
|
|
|
156
165
|
'',
|
|
157
166
|
cols,
|
|
158
167
|
sep,
|
|
159
|
-
...report.rows.map((r) => formatRow(r)),
|
|
168
|
+
...report.rows.map((r) => formatRow(r, padWidth)),
|
|
160
169
|
sep,
|
|
161
|
-
formatRow({ ...report.total, subagent: 'total' }, true),
|
|
170
|
+
formatRow({ ...report.total, subagent: 'total' }, padWidth, true),
|
|
162
171
|
];
|
|
163
172
|
|
|
164
173
|
// Server tool footer (only when present, since most rows have 0)
|
|
@@ -251,11 +260,12 @@ function deriveDailyInsights(report: LedgerReport): string[] {
|
|
|
251
260
|
return lines;
|
|
252
261
|
}
|
|
253
262
|
|
|
254
|
-
// Single-screen dashboard. Aggregates the same period across
|
|
263
|
+
// Single-screen dashboard. Aggregates the same period across 4 dimensions.
|
|
255
264
|
export function formatSummary(
|
|
256
265
|
bySubagent: LedgerReport,
|
|
257
266
|
byModel: LedgerReport,
|
|
258
267
|
byDay: LedgerReport,
|
|
268
|
+
byProject: LedgerReport,
|
|
259
269
|
label: string,
|
|
260
270
|
): string {
|
|
261
271
|
const total = bySubagent.total.cost.totalCost;
|
|
@@ -267,6 +277,9 @@ export function formatSummary(
|
|
|
267
277
|
const tSub = top(bySubagent.rows);
|
|
268
278
|
const tMod = top(byModel.rows);
|
|
269
279
|
const tDay = [...byDay.rows].sort((a, b) => b.cost.totalCost - a.cost.totalCost)[0];
|
|
280
|
+
const tProj = top(byProject.rows);
|
|
281
|
+
// Top 3 projects for quick distribution view
|
|
282
|
+
const topProjects = byProject.rows.slice(0, 3);
|
|
270
283
|
|
|
271
284
|
const k = (label: string, value: string) =>
|
|
272
285
|
` ${c.gray}${label.padEnd(20)}${c.reset} ${value}`;
|
|
@@ -281,28 +294,43 @@ export function formatSummary(
|
|
|
281
294
|
'',
|
|
282
295
|
k('Shadow cost', dollar(total)),
|
|
283
296
|
k('Sessions', `${bySubagent.total.sessionCount}`),
|
|
297
|
+
k('Projects', `${byProject.rows.length}`),
|
|
284
298
|
k('Plan reference', `$${PLAN_MAX_PRICE}/mo (Max)`),
|
|
285
299
|
k('Multiplier', `${c.bold}${leverage.toFixed(0)}×${c.reset} ${c.dim}vs Max plan${c.reset}`),
|
|
286
300
|
'',
|
|
287
301
|
k('Top subagent', tSub ? `${c.cyan}${tSub.subagent}${c.reset} ${dollar(tSub.cost.totalCost)} ${c.dim}(${pct(tSub.cost.totalCost, total)})${c.reset}` : '—'),
|
|
288
302
|
k('Top model', tMod ? `${c.cyan}${tMod.subagent}${c.reset} ${dollar(tMod.cost.totalCost)} ${c.dim}(${pct(tMod.cost.totalCost, total)})${c.reset}` : '—'),
|
|
303
|
+
k('Top project', tProj ? `${c.cyan}${tProj.subagent}${c.reset} ${dollar(tProj.cost.totalCost)} ${c.dim}(${pct(tProj.cost.totalCost, total)})${c.reset}` : '—'),
|
|
289
304
|
k('Peak day', tDay ? `${c.cyan}${tDay.subagent}${c.reset} ${dollar(tDay.cost.totalCost)}` : '—'),
|
|
290
|
-
'',
|
|
291
|
-
k('Cache 1h writes', `${fmtTokens(bySubagent.total.cacheCreation1hTokens)} tokens · ${fmtUsd(bySubagent.total.cost.cacheCreation1hCost)}`),
|
|
292
|
-
k('Cache reads', `${fmtTokens(bySubagent.total.cacheReadTokens)} tokens · ${fmtUsd(bySubagent.total.cost.cacheReadCost)}`),
|
|
293
|
-
k(
|
|
294
|
-
'Cache reuse',
|
|
295
|
-
writeTokens > 0
|
|
296
|
-
? `${c.bold}${reuseRatio.toFixed(0)}×${c.reset} ${c.dim}reads/writes${c.reset}`
|
|
297
|
-
: 'n/a',
|
|
298
|
-
),
|
|
299
|
-
k(
|
|
300
|
-
'Server tools',
|
|
301
|
-
bySubagent.total.webSearchRequests + bySubagent.total.webFetchRequests > 0
|
|
302
|
-
? `web_search ×${bySubagent.total.webSearchRequests}, web_fetch ×${bySubagent.total.webFetchRequests} (${fmtUsd(bySubagent.total.cost.serverToolUseCost)})`
|
|
303
|
-
: `${c.dim}none${c.reset}`,
|
|
304
|
-
),
|
|
305
305
|
];
|
|
306
|
+
|
|
307
|
+
// If multiple projects, list top 3 as a small breakdown
|
|
308
|
+
if (topProjects.length > 1) {
|
|
309
|
+
lines.push('');
|
|
310
|
+
lines.push(` ${c.gray}Project mix${c.reset}`);
|
|
311
|
+
for (const p of topProjects) {
|
|
312
|
+
lines.push(
|
|
313
|
+
` ${c.cyan}${p.subagent.padEnd(36)}${c.reset} ${dollar(p.cost.totalCost)} ${c.dim}(${pct(p.cost.totalCost, total)})${c.reset}`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
lines.push('');
|
|
319
|
+
lines.push(k('Cache 1h writes', `${fmtTokens(bySubagent.total.cacheCreation1hTokens)} tokens · ${fmtUsd(bySubagent.total.cost.cacheCreation1hCost)}`));
|
|
320
|
+
lines.push(k('Cache reads', `${fmtTokens(bySubagent.total.cacheReadTokens)} tokens · ${fmtUsd(bySubagent.total.cost.cacheReadCost)}`));
|
|
321
|
+
lines.push(k(
|
|
322
|
+
'Cache reuse',
|
|
323
|
+
writeTokens > 0
|
|
324
|
+
? `${c.bold}${reuseRatio.toFixed(0)}×${c.reset} ${c.dim}reads/writes${c.reset}`
|
|
325
|
+
: 'n/a',
|
|
326
|
+
));
|
|
327
|
+
lines.push(k(
|
|
328
|
+
'Server tools',
|
|
329
|
+
bySubagent.total.webSearchRequests + bySubagent.total.webFetchRequests > 0
|
|
330
|
+
? `web_search ×${bySubagent.total.webSearchRequests}, web_fetch ×${bySubagent.total.webFetchRequests} (${fmtUsd(bySubagent.total.cost.serverToolUseCost)})`
|
|
331
|
+
: `${c.dim}none${c.reset}`,
|
|
332
|
+
));
|
|
333
|
+
|
|
306
334
|
return lines.join('\n');
|
|
307
335
|
}
|
|
308
336
|
|
package/src/parser.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface ParseOptions {
|
|
|
13
13
|
|
|
14
14
|
interface LogFile {
|
|
15
15
|
path: string;
|
|
16
|
+
projectId: string;
|
|
16
17
|
subagentType: string | undefined;
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -34,11 +35,11 @@ export async function* findSessionLogs(
|
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
36
37
|
for (const project of projects) {
|
|
37
|
-
yield* walkForJsonl(join(projectsDir, project));
|
|
38
|
+
yield* walkForJsonl(join(projectsDir, project), project);
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
async function* walkForJsonl(dir: string): AsyncGenerator<LogFile> {
|
|
42
|
+
async function* walkForJsonl(dir: string, projectId: string): AsyncGenerator<LogFile> {
|
|
42
43
|
let entries: string[] = [];
|
|
43
44
|
try {
|
|
44
45
|
entries = await readdir(dir);
|
|
@@ -55,11 +56,11 @@ async function* walkForJsonl(dir: string): AsyncGenerator<LogFile> {
|
|
|
55
56
|
continue;
|
|
56
57
|
}
|
|
57
58
|
if (isDir) {
|
|
58
|
-
yield* walkForJsonl(full);
|
|
59
|
+
yield* walkForJsonl(full, projectId);
|
|
59
60
|
continue;
|
|
60
61
|
}
|
|
61
62
|
if (name.endsWith('.jsonl')) {
|
|
62
|
-
yield { path: full, subagentType: await resolveSubagentType(full) };
|
|
63
|
+
yield { path: full, projectId, subagentType: await resolveSubagentType(full) };
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
}
|
|
@@ -81,6 +82,7 @@ async function resolveSubagentType(jsonlPath: string): Promise<string | undefine
|
|
|
81
82
|
export async function* parseFile(
|
|
82
83
|
path: string,
|
|
83
84
|
subagentType?: string,
|
|
85
|
+
projectId?: string,
|
|
84
86
|
): AsyncGenerator<SessionTurn> {
|
|
85
87
|
const file = Bun.file(path);
|
|
86
88
|
const text = await file.text();
|
|
@@ -94,7 +96,7 @@ export async function* parseFile(
|
|
|
94
96
|
} catch {
|
|
95
97
|
continue;
|
|
96
98
|
}
|
|
97
|
-
yield extractTurn(obj, sessionId, subagentType);
|
|
99
|
+
yield extractTurn(obj, sessionId, subagentType, projectId);
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
102
|
|
|
@@ -102,6 +104,7 @@ function extractTurn(
|
|
|
102
104
|
obj: Record<string, unknown>,
|
|
103
105
|
sessionId: string,
|
|
104
106
|
subagentType: string | undefined,
|
|
107
|
+
projectId: string | undefined,
|
|
105
108
|
): SessionTurn {
|
|
106
109
|
const msg = (obj.message ?? {}) as Record<string, unknown>;
|
|
107
110
|
const usage = (msg.usage ?? undefined) as TokenUsage | undefined;
|
|
@@ -109,11 +112,13 @@ function extractTurn(
|
|
|
109
112
|
// If the raw turn carries its own subagentType (our fixture format), prefer it;
|
|
110
113
|
// otherwise fall back to the directory-derived one.
|
|
111
114
|
const perTurnSubagent = ((obj.subagentType as string) ?? undefined) || undefined;
|
|
115
|
+
const perTurnProject = ((obj.projectId as string) ?? undefined) || undefined;
|
|
112
116
|
|
|
113
117
|
return {
|
|
114
118
|
type: ((obj.type as string) ?? 'assistant') as SessionTurn['type'],
|
|
115
119
|
timestamp: (obj.timestamp as string) ?? new Date().toISOString(),
|
|
116
120
|
sessionId,
|
|
121
|
+
projectId: perTurnProject ?? projectId,
|
|
117
122
|
subagentType: perTurnSubagent ?? subagentType,
|
|
118
123
|
model,
|
|
119
124
|
usage,
|
|
@@ -125,7 +130,7 @@ function extractTurn(
|
|
|
125
130
|
export async function* parseAll(opts: ParseOptions = {}): AsyncGenerator<SessionTurn> {
|
|
126
131
|
const dir = opts.projectsDir ?? CLAUDE_PROJECTS_DIR;
|
|
127
132
|
for await (const file of findSessionLogs(dir)) {
|
|
128
|
-
for await (const turn of parseFile(file.path, file.subagentType)) {
|
|
133
|
+
for await (const turn of parseFile(file.path, file.subagentType, file.projectId)) {
|
|
129
134
|
if (opts.from && new Date(turn.timestamp) < opts.from) continue;
|
|
130
135
|
if (opts.to && new Date(turn.timestamp) > opts.to) continue;
|
|
131
136
|
yield turn;
|
package/src/types.ts
CHANGED