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/src/parser.ts CHANGED
@@ -5,6 +5,9 @@ import type { SessionTurn, TokenUsage } from './types.ts';
5
5
 
6
6
  const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');
7
7
 
8
+ // Limit concurrent file reads so we don't exhaust fd on huge setups.
9
+ const CONCURRENCY = 8;
10
+
8
11
  export interface ParseOptions {
9
12
  from?: Date;
10
13
  to?: Date;
@@ -15,16 +18,9 @@ interface LogFile {
15
18
  path: string;
16
19
  projectId: string;
17
20
  subagentType: string | undefined;
21
+ parentSessionId?: string | undefined; // for subagent logs, the parent session
18
22
  }
19
23
 
20
- // Walk the projects directory and yield every JSONL log.
21
- // Real Claude Code layout (as of 2026-04):
22
- // ~/.claude/projects/<encoded-project>/<session-uuid>.jsonl (main session)
23
- // ~/.claude/projects/<encoded-project>/<session-uuid>/subagents/agent-<hash>.jsonl (subagent)
24
- // ~/.claude/projects/<encoded-project>/<session-uuid>/subagents/agent-<hash>.meta.json
25
- //
26
- // The .meta.json next to each subagent .jsonl carries { "agentType": "...", "description": "..." }.
27
- // We read it to attribute the subagent by its real name (e.g. "Reality Checker") rather than an opaque hash.
28
24
  export async function* findSessionLogs(
29
25
  projectsDir: string = CLAUDE_PROJECTS_DIR,
30
26
  ): AsyncGenerator<LogFile> {
@@ -34,11 +30,22 @@ export async function* findSessionLogs(
34
30
  } catch {
35
31
  return;
36
32
  }
37
- for (const project of projects) {
38
- yield* walkForJsonl(join(projectsDir, project), project);
33
+ // Parallelize the project-level walks; collect LogFiles via streaming queue.
34
+ const walkers = projects.map((project) =>
35
+ collectWalk(join(projectsDir, project), project),
36
+ );
37
+ const settled = await Promise.all(walkers);
38
+ for (const list of settled) {
39
+ for (const lf of list) yield lf;
39
40
  }
40
41
  }
41
42
 
43
+ async function collectWalk(dir: string, projectId: string): Promise<LogFile[]> {
44
+ const out: LogFile[] = [];
45
+ for await (const lf of walkForJsonl(dir, projectId)) out.push(lf);
46
+ return out;
47
+ }
48
+
42
49
  async function* walkForJsonl(dir: string, projectId: string): AsyncGenerator<LogFile> {
43
50
  let entries: string[] = [];
44
51
  try {
@@ -46,6 +53,9 @@ async function* walkForJsonl(dir: string, projectId: string): AsyncGenerator<Log
46
53
  } catch {
47
54
  return;
48
55
  }
56
+ // If this directory IS named 'subagents', its parent session is one level up.
57
+ // e.g. .../<session-uuid>/subagents/agent-XXX.jsonl → parent = <session-uuid>
58
+ const parentSession = basename(dir) === 'subagents' ? basename(dirname(dir)) : undefined;
49
59
  for (const name of entries) {
50
60
  if (name.startsWith('.') || name === 'memory') continue;
51
61
  const full = join(dir, name);
@@ -60,13 +70,18 @@ async function* walkForJsonl(dir: string, projectId: string): AsyncGenerator<Log
60
70
  continue;
61
71
  }
62
72
  if (name.endsWith('.jsonl')) {
63
- yield { path: full, projectId, subagentType: await resolveSubagentType(full) };
73
+ const subagentType = await resolveSubagentType(full);
74
+ yield {
75
+ path: full,
76
+ projectId,
77
+ subagentType,
78
+ parentSessionId: parentSession,
79
+ };
64
80
  }
65
81
  }
66
82
  }
67
83
 
68
84
  async function resolveSubagentType(jsonlPath: string): Promise<string | undefined> {
69
- // Subagent logs live under `.../subagents/agent-<hash>.jsonl` with a sibling `.meta.json`.
70
85
  if (basename(dirname(jsonlPath)) !== 'subagents') return undefined;
71
86
  const metaPath = jsonlPath.replace(/\.jsonl$/, '.meta.json');
72
87
  try {
@@ -78,26 +93,51 @@ async function resolveSubagentType(jsonlPath: string): Promise<string | undefine
78
93
  }
79
94
  }
80
95
 
81
- // Stream-parse one JSONL file. Skips malformed lines rather than aborting.
96
+ // Streaming line reader — does NOT buffer the whole file.
97
+ // Handles partial lines across chunk boundaries. Skips malformed JSON.
82
98
  export async function* parseFile(
83
99
  path: string,
84
100
  subagentType?: string,
85
101
  projectId?: string,
102
+ parentSessionId?: string,
86
103
  ): AsyncGenerator<SessionTurn> {
87
- const file = Bun.file(path);
88
- const text = await file.text();
89
104
  const sessionId = basename(path).replace('.jsonl', '');
90
-
91
- for (const line of text.split('\n')) {
92
- if (!line.trim()) continue;
93
- let obj: Record<string, unknown>;
94
- try {
95
- obj = JSON.parse(line);
96
- } catch {
97
- continue;
105
+ const stream = Bun.file(path).stream();
106
+ const decoder = new TextDecoder('utf-8', { fatal: false });
107
+ let buffer = '';
108
+ for await (const chunk of stream) {
109
+ buffer += decoder.decode(chunk, { stream: true });
110
+ let nl: number;
111
+ while ((nl = buffer.indexOf('\n')) !== -1) {
112
+ const line = buffer.slice(0, nl);
113
+ buffer = buffer.slice(nl + 1);
114
+ if (!line.trim()) continue;
115
+ const turn = tryParseLine(line, sessionId, subagentType, projectId, parentSessionId);
116
+ if (turn) yield turn;
98
117
  }
99
- yield extractTurn(obj, sessionId, subagentType, projectId);
100
118
  }
119
+ // Flush any trailing partial line.
120
+ buffer += decoder.decode();
121
+ if (buffer.trim()) {
122
+ const turn = tryParseLine(buffer, sessionId, subagentType, projectId, parentSessionId);
123
+ if (turn) yield turn;
124
+ }
125
+ }
126
+
127
+ function tryParseLine(
128
+ line: string,
129
+ sessionId: string,
130
+ subagentType: string | undefined,
131
+ projectId: string | undefined,
132
+ parentSessionId: string | undefined,
133
+ ): SessionTurn | undefined {
134
+ let obj: Record<string, unknown>;
135
+ try {
136
+ obj = JSON.parse(line);
137
+ } catch {
138
+ return undefined;
139
+ }
140
+ return extractTurn(obj, sessionId, subagentType, projectId, parentSessionId);
101
141
  }
102
142
 
103
143
  function extractTurn(
@@ -105,14 +145,29 @@ function extractTurn(
105
145
  sessionId: string,
106
146
  subagentType: string | undefined,
107
147
  projectId: string | undefined,
148
+ parentSessionId: string | undefined,
108
149
  ): SessionTurn {
109
150
  const msg = (obj.message ?? {}) as Record<string, unknown>;
110
151
  const usage = (msg.usage ?? undefined) as TokenUsage | undefined;
111
152
  const model = (msg.model ?? undefined) as string | undefined;
112
- // If the raw turn carries its own subagentType (our fixture format), prefer it;
113
- // otherwise fall back to the directory-derived one.
114
153
  const perTurnSubagent = ((obj.subagentType as string) ?? undefined) || undefined;
115
154
  const perTurnProject = ((obj.projectId as string) ?? undefined) || undefined;
155
+ const perTurnParent = ((obj.parentSessionId as string) ?? undefined) || undefined;
156
+
157
+ // Extract tool_use blocks from the assistant message content (if any).
158
+ // Used by the --by tool aggregator and subagent-graph attribution.
159
+ const toolUses: string[] = [];
160
+ const content = msg.content;
161
+ if (Array.isArray(content)) {
162
+ for (const block of content) {
163
+ if (block && typeof block === 'object') {
164
+ const b = block as Record<string, unknown>;
165
+ if (b.type === 'tool_use' && typeof b.name === 'string') {
166
+ toolUses.push(b.name);
167
+ }
168
+ }
169
+ }
170
+ }
116
171
 
117
172
  return {
118
173
  type: ((obj.type as string) ?? 'assistant') as SessionTurn['type'],
@@ -120,20 +175,43 @@ function extractTurn(
120
175
  sessionId,
121
176
  projectId: perTurnProject ?? projectId,
122
177
  subagentType: perTurnSubagent ?? subagentType,
178
+ parentSessionId: perTurnParent ?? parentSessionId,
123
179
  model,
124
180
  usage,
181
+ toolUses: toolUses.length > 0 ? toolUses : undefined,
125
182
  raw: obj,
126
183
  };
127
184
  }
128
185
 
129
- // Convenience: stream every turn across every log under ~/.claude/projects.
186
+ // Parallelized, streaming. Does not materialize all turns — caller iterates
187
+ // once and dispatches to aggregators. Reads up to CONCURRENCY files at a time.
130
188
  export async function* parseAll(opts: ParseOptions = {}): AsyncGenerator<SessionTurn> {
131
189
  const dir = opts.projectsDir ?? CLAUDE_PROJECTS_DIR;
132
- for await (const file of findSessionLogs(dir)) {
133
- for await (const turn of parseFile(file.path, file.subagentType, file.projectId)) {
134
- if (opts.from && new Date(turn.timestamp) < opts.from) continue;
135
- if (opts.to && new Date(turn.timestamp) > opts.to) continue;
136
- yield turn;
190
+ const files: LogFile[] = [];
191
+ for await (const f of findSessionLogs(dir)) files.push(f);
192
+
193
+ // Fan out file reads in batches of CONCURRENCY, yield turns in arrival order.
194
+ for (let i = 0; i < files.length; i += CONCURRENCY) {
195
+ const batch = files.slice(i, i + CONCURRENCY);
196
+ const streams = batch.map((f) =>
197
+ collectBatch(parseFile(f.path, f.subagentType, f.projectId, f.parentSessionId), opts),
198
+ );
199
+ const results = await Promise.all(streams);
200
+ for (const turns of results) {
201
+ for (const t of turns) yield t;
137
202
  }
138
203
  }
139
204
  }
205
+
206
+ async function collectBatch(
207
+ gen: AsyncGenerator<SessionTurn>,
208
+ opts: ParseOptions,
209
+ ): Promise<SessionTurn[]> {
210
+ const out: SessionTurn[] = [];
211
+ for await (const t of gen) {
212
+ if (opts.from && new Date(t.timestamp) < opts.from) continue;
213
+ if (opts.to && new Date(t.timestamp) > opts.to) continue;
214
+ out.push(t);
215
+ }
216
+ return out;
217
+ }
package/src/pricing.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { CostBreakdown, ModelId, TokenUsage } from './types.ts';
2
2
 
3
3
  // Prices are per 1M tokens, USD. Keep in sync with
4
- // https://www.anthropic.com/pricing as of 2026-04-21.
4
+ // https://platform.claude.com/docs/en/about-claude/pricing as of 2026-07-23.
5
5
  //
6
6
  // Prompt caching (Anthropic):
7
7
  // cache_creation 5-min TTL: 1.25× base input rate
@@ -10,15 +10,31 @@ import type { CostBreakdown, ModelId, TokenUsage } from './types.ts';
10
10
  //
11
11
  // We include both current and prior-generation model IDs because real logs span
12
12
  // version bumps (Claude Code does not auto-rewrite historical logs when you upgrade).
13
+ const SONNET_5_STANDARD_PRICE_START = new Date('2026-09-01T00:00:00Z');
14
+ const SONNET_5_INTRO_RATE = { input: 2, output: 10 };
15
+ const SONNET_5_STANDARD_RATE = { input: 3, output: 15 };
16
+
13
17
  const PRICING: Record<string, { input: number; output: number }> = {
14
- // Current generation (2026-04)
15
- 'claude-opus-4-7': { input: 15, output: 75 },
18
+ // Current generation (2026-07)
19
+ 'claude-fable-5': { input: 10, output: 50 },
20
+ 'claude-mythos-5': { input: 10, output: 50 },
21
+ 'claude-opus-4-8': { input: 5, output: 25 },
22
+ 'claude-opus-4-7': { input: 5, output: 25 },
23
+ 'claude-opus-4-6': { input: 5, output: 25 },
24
+ 'claude-opus-4-5': { input: 5, output: 25 },
25
+ // resolveModel applies the introductory rate through 2026-08-31.
26
+ 'claude-sonnet-5': SONNET_5_STANDARD_RATE,
16
27
  'claude-sonnet-4-6': { input: 3, output: 15 },
17
- 'claude-haiku-4-5-20251001': { input: 0.8, output: 4 },
18
- // Prior generation (still appears in logs written before model upgrades)
19
- 'claude-opus-4-6': { input: 15, output: 75 },
20
- 'claude-opus-4-5': { input: 15, output: 75 },
21
28
  'claude-sonnet-4-5': { input: 3, output: 15 },
29
+ 'claude-haiku-4-5-20251001': { input: 1, output: 5 },
30
+ 'claude-haiku-4-5': { input: 1, output: 5 },
31
+ // Prior generation (still appears in logs written before model upgrades)
32
+ 'claude-opus-4-1-20250805': { input: 15, output: 75 },
33
+ 'claude-opus-4-20250514': { input: 15, output: 75 },
34
+ 'claude-sonnet-4-20250514': { input: 3, output: 15 },
35
+ 'claude-haiku-3-5-20241022': { input: 0.8, output: 4 },
36
+ 'claude-3-haiku-20240307': { input: 0.25, output: 1.25 },
37
+ // Kept for the aliases observed in older Claude Code logs.
22
38
  'claude-haiku-4-0': { input: 0.8, output: 4 },
23
39
  };
24
40
 
@@ -30,25 +46,30 @@ const CACHE_5M_MULTIPLIER = 1.25;
30
46
  const CACHE_1H_MULTIPLIER = 2.0;
31
47
  const CACHE_READ_MULTIPLIER = 0.1;
32
48
 
33
- // Anthropic server-tool pricing (https://www.anthropic.com/pricing as of 2026-04):
49
+ // Anthropic server-tool pricing (official pricing docs as of 2026-07-23):
34
50
  // Web search: $10 per 1,000 requests = $0.01 / request
35
- // Web fetch: treated identically here pending Anthropic split (file an issue if wrong)
51
+ // Web fetch: no per-request charge; fetched content still incurs token costs
36
52
  const WEB_SEARCH_PRICE_PER_REQUEST = 0.01;
37
- const WEB_FETCH_PRICE_PER_REQUEST = 0.01;
53
+ const WEB_FETCH_PRICE_PER_REQUEST = 0;
38
54
 
39
- function resolveModel(model: ModelId | undefined): { input: number; output: number } {
55
+ function resolveModel(model: ModelId | undefined, asOf: Date): { input: number; output: number } {
40
56
  if (!model) return { input: 0, output: 0 };
41
57
  if (FREE_MODELS.has(model)) return { input: 0, output: 0 };
58
+ if (model === 'claude-sonnet-5' || model.startsWith('claude-sonnet-5-')) {
59
+ return asOf < SONNET_5_STANDARD_PRICE_START
60
+ ? SONNET_5_INTRO_RATE
61
+ : SONNET_5_STANDARD_RATE;
62
+ }
42
63
  const exact = PRICING[model];
43
64
  if (exact) return exact;
44
- // Fuzzy family prefix match as a safety net for unknown subversions.
45
- // e.g. "claude-opus-4-8-20260301" falls back to Opus pricing until we add it.
65
+ // A provider/date-suffixed ID can inherit a verified base model's rate.
66
+ // Unknown future model families deliberately return zero instead of silently
67
+ // inventing a price; the CLI warns when it sees one.
46
68
  for (const key of Object.keys(PRICING)) {
47
- if (model.startsWith(key)) return PRICING[key] as { input: number; output: number };
69
+ if (model.startsWith(`${key}-`)) {
70
+ return PRICING[key] as { input: number; output: number };
71
+ }
48
72
  }
49
- if (model.startsWith('claude-opus-')) return { input: 15, output: 75 };
50
- if (model.startsWith('claude-sonnet-')) return { input: 3, output: 15 };
51
- if (model.startsWith('claude-haiku-')) return { input: 0.8, output: 4 };
52
73
  return { input: 0, output: 0 };
53
74
  }
54
75
 
@@ -70,8 +91,12 @@ function splitCacheCreation(usage: TokenUsage): CacheBreakdown {
70
91
  };
71
92
  }
72
93
 
73
- export function costFor(usage: TokenUsage, model: ModelId | undefined): CostBreakdown {
74
- const rate = resolveModel(model);
94
+ export function costFor(
95
+ usage: TokenUsage,
96
+ model: ModelId | undefined,
97
+ asOf: Date = new Date(),
98
+ ): CostBreakdown {
99
+ const rate = resolveModel(model, asOf);
75
100
  const cacheRead = usage.cache_read_input_tokens ?? 0;
76
101
  const { fiveMin, oneHour } = splitCacheCreation(usage);
77
102
 
@@ -110,5 +135,5 @@ export function isKnownModel(model: ModelId | undefined): boolean {
110
135
  if (!model) return false;
111
136
  if (FREE_MODELS.has(model)) return true;
112
137
  if (PRICING[model]) return true;
113
- return Object.keys(PRICING).some((key) => model.startsWith(key));
138
+ return Object.keys(PRICING).some((key) => model.startsWith(`${key}-`));
114
139
  }
package/src/tree.ts ADDED
@@ -0,0 +1,107 @@
1
+ // ASCII tree renderer for subagent graph (the MOAT feature).
2
+ // Helicone/Langfuse observe API calls; only Claude Code knows that
3
+ // orchestrator → 3×researcher + 6×swift-dev. We render that tree.
4
+
5
+ import type { SubagentTreeNode } from './types.ts';
6
+
7
+ const fmtUsd = (n: number): string => {
8
+ if (n < 0.01) return '$' + n.toFixed(4);
9
+ return '$' + n.toFixed(2);
10
+ };
11
+
12
+ const dim = (s: string): string =>
13
+ process.env.NO_COLOR || !process.stdout.isTTY ? s : `\x1b[2m${s}\x1b[0m`;
14
+
15
+ const bold = (s: string): string =>
16
+ process.env.NO_COLOR || !process.stdout.isTTY ? s : `\x1b[1m${s}\x1b[0m`;
17
+
18
+ const amber = (s: string): string =>
19
+ process.env.NO_COLOR || !process.stdout.isTTY ? s : `\x1b[38;5;215m${s}\x1b[0m`;
20
+
21
+ export function formatTree(roots: SubagentTreeNode[], totalCost: number): string {
22
+ if (roots.length === 0) return dim('No subagent graphs found in this period.');
23
+
24
+ const lines: string[] = [];
25
+ lines.push(
26
+ bold('Subagent graphs') +
27
+ dim(' · top ') +
28
+ roots.length +
29
+ dim(' orchestrators by total cost · grand-total ') +
30
+ amber(fmtUsd(totalCost)),
31
+ );
32
+ lines.push('');
33
+ for (const root of roots) {
34
+ renderNode(root, '', true, lines, totalCost, true);
35
+ lines.push('');
36
+ }
37
+ return lines.join('\n');
38
+ }
39
+
40
+ function renderNode(
41
+ node: SubagentTreeNode,
42
+ prefix: string,
43
+ isLast: boolean,
44
+ lines: string[],
45
+ grandTotal: number,
46
+ isRoot: boolean,
47
+ ): void {
48
+ const connector = isRoot ? '' : isLast ? '└─ ' : '├─ ';
49
+ const sessionShort = dim(node.sessionId.slice(0, 8));
50
+ const pct = grandTotal > 0 ? ((node.totalCost / grandTotal) * 100).toFixed(1) + '%' : '—';
51
+ const turns = dim(`${node.turnCount} turns`);
52
+
53
+ if (isRoot) {
54
+ const childCount = countDescendants(node);
55
+ const childInfo = childCount > 0 ? dim(` (${childCount} children)`) : '';
56
+ lines.push(
57
+ `${bold(node.label)} ${sessionShort}${childInfo} ${amber(fmtUsd(node.totalCost))} ${dim(`(${pct})`)}`,
58
+ );
59
+ } else {
60
+ const selfNote =
61
+ node.children.length > 0
62
+ ? dim(` self ${fmtUsd(node.selfCost)}`)
63
+ : '';
64
+ lines.push(
65
+ `${dim(prefix)}${connector}${node.label} ${sessionShort} ${amber(fmtUsd(node.totalCost))}${selfNote} ${turns}`,
66
+ );
67
+ }
68
+
69
+ const childPrefix = prefix + (isRoot ? '' : isLast ? ' ' : '│ ');
70
+ node.children.forEach((child, idx) => {
71
+ const last = idx === node.children.length - 1;
72
+ renderNode(child, childPrefix, last, lines, grandTotal, false);
73
+ });
74
+ }
75
+
76
+ function countDescendants(node: SubagentTreeNode): number {
77
+ let n = node.children.length;
78
+ for (const c of node.children) n += countDescendants(c);
79
+ return n;
80
+ }
81
+
82
+ // Markdown variant — same data, GH-friendly nested list.
83
+ export function formatTreeMarkdown(roots: SubagentTreeNode[], totalCost: number): string {
84
+ const lines: string[] = ['# Subagent graphs', ''];
85
+ lines.push(`Top ${roots.length} orchestrators by total cost. Grand total: **${fmtUsd(totalCost)}**.`);
86
+ lines.push('');
87
+ for (const root of roots) {
88
+ const childCount = countDescendants(root);
89
+ lines.push(
90
+ `## ${root.label} \`${root.sessionId.slice(0, 8)}\` — ${fmtUsd(root.totalCost)} (${childCount} children, ${root.turnCount} turns)`,
91
+ );
92
+ mdNode(root, lines, 0);
93
+ lines.push('');
94
+ }
95
+ return lines.join('\n');
96
+ }
97
+
98
+ function mdNode(node: SubagentTreeNode, lines: string[], depth: number): void {
99
+ for (const child of node.children) {
100
+ const indent = ' '.repeat(depth);
101
+ const self = child.children.length > 0 ? ` _self ${fmtUsd(child.selfCost)}_` : '';
102
+ lines.push(
103
+ `${indent}- **${child.label}** \`${child.sessionId.slice(0, 8)}\` — ${fmtUsd(child.totalCost)} (${child.turnCount} turns)${self}`,
104
+ );
105
+ mdNode(child, lines, depth + 1);
106
+ }
107
+ }
package/src/types.ts CHANGED
@@ -1,8 +1,15 @@
1
- export type ModelId =
2
- | 'claude-opus-4-7'
3
- | 'claude-sonnet-4-6'
4
- | 'claude-haiku-4-5-20251001'
5
- | string;
1
+ // Known model ids — kept narrow for documentation. Use `string` at the boundary.
2
+ export const KNOWN_MODELS = [
3
+ 'claude-opus-4-7',
4
+ 'claude-sonnet-4-6',
5
+ 'claude-haiku-4-5-20251001',
6
+ 'claude-opus-4-6',
7
+ 'claude-opus-4-5',
8
+ 'claude-sonnet-4-5',
9
+ 'claude-haiku-4-0',
10
+ ] as const;
11
+
12
+ export type ModelId = string;
6
13
 
7
14
  export type TurnType = 'user' | 'assistant' | 'summary' | 'system';
8
15
 
@@ -19,12 +26,9 @@ export interface ServerToolUse {
19
26
  export interface TokenUsage {
20
27
  input_tokens: number;
21
28
  output_tokens: number;
22
- // Legacy flat counter. Real logs also carry `cache_creation` with TTL split; if present,
23
- // prefer that for accurate pricing.
24
29
  cache_creation_input_tokens?: number;
25
30
  cache_read_input_tokens?: number;
26
31
  cache_creation?: CacheCreationDetail;
27
- // Anthropic server-side tool use (web_search, web_fetch). Priced per request, not per token.
28
32
  server_tool_use?: ServerToolUse;
29
33
  }
30
34
 
@@ -34,8 +38,10 @@ export interface SessionTurn {
34
38
  sessionId: string;
35
39
  projectId?: string | undefined;
36
40
  subagentType?: string | undefined;
41
+ parentSessionId?: string | undefined;
37
42
  model?: ModelId | undefined;
38
43
  usage?: TokenUsage | undefined;
44
+ toolUses?: string[] | undefined; // tool names invoked in this assistant turn
39
45
  raw: unknown;
40
46
  }
41
47
 
@@ -67,3 +73,15 @@ export interface LedgerReport {
67
73
  rows: AggregatedRow[];
68
74
  total: AggregatedRow;
69
75
  }
76
+
77
+ // Subagent-graph node — the MOAT feature.
78
+ // Represents one parent session's tree-rooted cost attribution:
79
+ // the parent's own cost + all child subagent costs, recursively.
80
+ export interface SubagentTreeNode {
81
+ sessionId: string;
82
+ label: string; // subagentType or '(orchestrator)'
83
+ selfCost: number;
84
+ totalCost: number; // self + children
85
+ children: SubagentTreeNode[];
86
+ turnCount: number;
87
+ }
@@ -1,35 +0,0 @@
1
- ---
2
- name: Bug report
3
- about: Report incorrect output, crashes, or wrong attribution
4
- title: ''
5
- labels: bug
6
- ---
7
-
8
- ## What happened
9
-
10
- <!-- What did agent-ledger do? What did you expect it to do instead? -->
11
-
12
- ## How to reproduce
13
-
14
- ```bash
15
- agent-ledger <your-exact-command>
16
- ```
17
-
18
- ## Output (redact sensitive paths / subagent names)
19
-
20
- ```
21
- <paste the output here>
22
- ```
23
-
24
- ## Your setup
25
-
26
- - OS: <!-- macOS 15.3 / Ubuntu 22.04 / ... -->
27
- - Bun version: <!-- `bun --version` -->
28
- - agent-ledger version: <!-- `agent-ledger --version` (pre-alpha: use commit SHA) -->
29
- - Claude Code version: <!-- `claude --version` -->
30
-
31
- ## If you're on pay-as-you-go, the calibration ask
32
-
33
- If you compare `agent-ledger month` against a real Anthropic invoice and
34
- they differ by > 10%, I want to hear about it. Even one line: "agent-ledger
35
- says $X, invoice says $Y" is incredibly useful. Thanks.
@@ -1,29 +0,0 @@
1
- ---
2
- name: Model pricing gap
3
- about: A model ID shows $0 in your ledger — we're missing it from the price table
4
- title: 'pricing: add <model-id>'
5
- labels: pricing
6
- ---
7
-
8
- ## Model ID
9
-
10
- <!-- e.g. claude-opus-4-8-20260301 -->
11
-
12
- ## Anthropic's published rates
13
-
14
- - Input: $<X> / 1M tokens
15
- - Output: $<Y> / 1M tokens
16
- - Link to Anthropic pricing page (with date):
17
-
18
- ## Where you saw it
19
-
20
- Sample from your log (redact the content, keep the model field):
21
-
22
- ```json
23
- {"message":{"model":"<model-id>", "usage":{"input_tokens":..., "output_tokens":...}}}
24
- ```
25
-
26
- ## Optional: PR
27
-
28
- If you want to open the PR, the pricing table lives in `src/pricing.ts`. Add a
29
- matching test in `test/pricing.test.ts`.
@@ -1,19 +0,0 @@
1
- ## What this PR does
2
-
3
- <!-- One or two sentences. -->
4
-
5
- ## Checklist
6
-
7
- - [ ] `bun run typecheck` passes
8
- - [ ] `bun test` passes
9
- - [ ] If you touched `src/pricing.ts`, there's a matching test in
10
- `test/pricing.test.ts`
11
- - [ ] If you touched `src/parser.ts`, the change doesn't break the nested
12
- `subagents/` walk (see `test/parser.test.ts`)
13
- - [ ] No runtime dependencies added (this tool intentionally has none)
14
-
15
- ## Log snippet (if relevant)
16
-
17
- <!-- If the change is about handling some real log shape, include a redacted
18
- example of what it looks like. Helps me reason about whether other tools
19
- produce the same shape. -->
@@ -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