claude-agent-ledger 0.2.0 → 0.3.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 +21 -0
- package/README.md +38 -4
- package/bin/agent-ledger.ts +39 -14
- package/package.json +1 -1
- package/src/format.ts +198 -19
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,27 @@ 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.3.0] — 2026-04-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `--summary` flag — single-screen dashboard with shadow cost, top subagent /
|
|
12
|
+
model / day, cache reuse ratio, and **leverage vs $200/mo Max plan**.
|
|
13
|
+
This is the killer view for "why install over /cost?"
|
|
14
|
+
- Auto-derived insights footer on the default table:
|
|
15
|
+
- Top contributor with %
|
|
16
|
+
- 1h cache spend call-out (when ≥5% of total)
|
|
17
|
+
- Cache reuse ratio (read/write) with verdict
|
|
18
|
+
- Monthly $ equivalent + leverage vs Max plan
|
|
19
|
+
- Daily-mode insights: peak day, avg/day, peak/trough variance
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- `--by day` polish:
|
|
23
|
+
- Bar fixed to 40-char width so session counts align in their own column
|
|
24
|
+
- Brighter cyan (`\x1b[96m`) bar character
|
|
25
|
+
- Total row separated by horizontal rule, properly aligned
|
|
26
|
+
- Header line now shows period + total + sessions + group count at a glance:
|
|
27
|
+
`agent-ledger week 2026-04-15 → 2026-04-22 · $13,356.27 shadow · 217 sessions · 7 agents`
|
|
28
|
+
|
|
8
29
|
## [0.2.0] — 2026-04-21
|
|
9
30
|
|
|
10
31
|
### Added
|
package/README.md
CHANGED
|
@@ -27,15 +27,49 @@ If you run **seven** subagents daily (reliability, release, review, iOS factory,
|
|
|
27
27
|
npm install -g claude-agent-ledger
|
|
28
28
|
# or: bun install -g claude-agent-ledger
|
|
29
29
|
|
|
30
|
-
agent-ledger
|
|
31
|
-
agent-ledger week --by model #
|
|
32
|
-
agent-ledger week --by day # daily bar chart
|
|
30
|
+
agent-ledger week --summary # the dashboard (start here)
|
|
31
|
+
agent-ledger week --by model # which Claude model burned the budget
|
|
32
|
+
agent-ledger week --by day # daily bar chart with peak/avg/variance
|
|
33
33
|
agent-ledger month --md > report.md
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
No server. No account. No data leaves your machine.
|
|
37
37
|
|
|
38
|
-
##
|
|
38
|
+
## Why this and not `/cost`?
|
|
39
|
+
|
|
40
|
+
Claude Code's built-in `/cost` shows the **current session** cost. That's it.
|
|
41
|
+
|
|
42
|
+
`claude-agent-ledger` answers questions `/cost` can't:
|
|
43
|
+
|
|
44
|
+
- Which of my **10+ subagents** burned the budget? (`--by model` / default)
|
|
45
|
+
- Which **day** of last week was the expensive one? (`--by day`)
|
|
46
|
+
- What would my Claude Max usage have cost on **pay-as-you-go**? (the "shadow cost" framing)
|
|
47
|
+
- Is my **cache reuse ratio** healthy? (auto-derived insight)
|
|
48
|
+
- What's the **leverage** I'm getting from my $200/mo subscription? (e.g. 250×)
|
|
49
|
+
|
|
50
|
+
Run `agent-ledger week --summary` once and decide.
|
|
51
|
+
|
|
52
|
+
## Example output (real, from the author's machine)
|
|
53
|
+
|
|
54
|
+
### `--summary` (the dashboard)
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
agent-ledger week summary 2026-04-15 → 2026-04-22
|
|
58
|
+
|
|
59
|
+
Shadow cost $13,356.27
|
|
60
|
+
Sessions 217
|
|
61
|
+
Plan reference $200/mo (Max)
|
|
62
|
+
Multiplier 250× vs Max plan
|
|
63
|
+
|
|
64
|
+
Top subagent (main) $12,701.19 (95%)
|
|
65
|
+
Top model claude-opus-4-7 $7,184.45 (54%)
|
|
66
|
+
Peak day 2026-04-15 $3,679.80
|
|
67
|
+
|
|
68
|
+
Cache 1h writes 119.45M tokens · $3583.42
|
|
69
|
+
Cache reads 5.54B tokens · $8162.93
|
|
70
|
+
Cache reuse 38× reads/writes
|
|
71
|
+
Server tools none
|
|
72
|
+
```
|
|
39
73
|
|
|
40
74
|
### Default — by subagent
|
|
41
75
|
|
package/bin/agent-ledger.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { aggregate, type GroupKey } from '../src/aggregator.ts';
|
|
3
|
-
import { formatMarkdown, formatTable, formatTrend } from '../src/format.ts';
|
|
3
|
+
import { formatMarkdown, formatSummary, formatTable, formatTrend } from '../src/format.ts';
|
|
4
4
|
import { parseAll } from '../src/parser.ts';
|
|
5
|
-
import type { LedgerReport } from '../src/types.ts';
|
|
5
|
+
import type { LedgerReport, SessionTurn } from '../src/types.ts';
|
|
6
6
|
|
|
7
7
|
type Period = 'today' | 'week' | 'month';
|
|
8
8
|
type Plan = 'payg' | 'pro' | 'max';
|
|
@@ -13,6 +13,7 @@ interface Args {
|
|
|
13
13
|
json: boolean;
|
|
14
14
|
plan: Plan;
|
|
15
15
|
by: GroupKey;
|
|
16
|
+
summary: boolean;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
function periodRange(period: Period): { from: Date; to: Date } {
|
|
@@ -34,24 +35,24 @@ function usage(): never {
|
|
|
34
35
|
console.error(`Usage: agent-ledger <today|week|month> [flags]
|
|
35
36
|
|
|
36
37
|
Flags:
|
|
37
|
-
--
|
|
38
|
-
|
|
39
|
-
--
|
|
40
|
-
|
|
38
|
+
--summary One-screen dashboard: total + top subagent + top
|
|
39
|
+
model + peak day + cache reuse + leverage vs $200/mo plan
|
|
40
|
+
--by <subagent|model|day> Group rows by subagent (default), model, or day
|
|
41
|
+
"day" renders as a daily ASCII bar chart
|
|
42
|
+
--md Markdown table output (good for sharing / commits)
|
|
43
|
+
--json Raw JSON output (for piping into jq)
|
|
41
44
|
--plan pro|max Suppress dollar columns (token utilization only)
|
|
42
|
-
Default is pay-as-you-go: shows shadow cost in $.
|
|
43
45
|
|
|
44
46
|
Environment:
|
|
45
47
|
NO_COLOR=1 Disable ANSI colors
|
|
46
|
-
FORCE_COLOR=1 Force colors
|
|
48
|
+
FORCE_COLOR=1 Force colors when piping
|
|
47
49
|
|
|
48
50
|
Examples:
|
|
49
|
-
agent-ledger
|
|
50
|
-
agent-ledger week --by model
|
|
51
|
+
agent-ledger week --summary # the dashboard
|
|
51
52
|
agent-ledger week --by day # daily bar chart
|
|
52
|
-
agent-ledger week --
|
|
53
|
-
agent-ledger month --
|
|
54
|
-
agent-ledger week --
|
|
53
|
+
agent-ledger week --by model # which Claude model burned the budget
|
|
54
|
+
agent-ledger month --md > month.md
|
|
55
|
+
agent-ledger week --json | jq '.total.cost.totalCost'
|
|
55
56
|
`);
|
|
56
57
|
process.exit(1);
|
|
57
58
|
}
|
|
@@ -88,6 +89,7 @@ function parseArgs(argv: string[]): Args {
|
|
|
88
89
|
json: rest.includes('--json'),
|
|
89
90
|
plan,
|
|
90
91
|
by,
|
|
92
|
+
summary: rest.includes('--summary'),
|
|
91
93
|
};
|
|
92
94
|
}
|
|
93
95
|
|
|
@@ -109,10 +111,33 @@ function planMask(report: LedgerReport, plan: Plan): LedgerReport {
|
|
|
109
111
|
};
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
// Materialize once so --summary can re-aggregate by 3 dimensions cheaply.
|
|
115
|
+
async function collectTurns(opts: { from: Date; to: Date }): Promise<SessionTurn[]> {
|
|
116
|
+
const out: SessionTurn[] = [];
|
|
117
|
+
for await (const t of parseAll(opts)) out.push(t);
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function* iterTurns(turns: SessionTurn[]): AsyncGenerator<SessionTurn> {
|
|
122
|
+
for (const t of turns) yield t;
|
|
123
|
+
}
|
|
124
|
+
|
|
112
125
|
async function main(): Promise<void> {
|
|
113
126
|
const args = parseArgs(process.argv.slice(2));
|
|
114
127
|
const { from, to } = periodRange(args.period);
|
|
115
|
-
|
|
128
|
+
const turns = await collectTurns({ from, to });
|
|
129
|
+
|
|
130
|
+
if (args.summary) {
|
|
131
|
+
const [bySub, byMod, byDay] = await Promise.all([
|
|
132
|
+
aggregate(iterTurns(turns), from, to, 'subagent'),
|
|
133
|
+
aggregate(iterTurns(turns), from, to, 'model'),
|
|
134
|
+
aggregate(iterTurns(turns), from, to, 'day'),
|
|
135
|
+
]);
|
|
136
|
+
console.log(formatSummary(bySub, byMod, byDay, args.period));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let report = await aggregate(iterTurns(turns), from, to, args.by);
|
|
116
141
|
report = planMask(report, args.plan);
|
|
117
142
|
|
|
118
143
|
let output: string;
|
package/package.json
CHANGED
package/src/format.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { AggregatedRow, LedgerReport } from './types.ts';
|
|
2
2
|
|
|
3
3
|
const PAD_LABEL = 22;
|
|
4
|
+
const BAR_WIDTH = 40;
|
|
5
|
+
const PLAN_MAX_PRICE = 200; // monthly USD reference for shadow-cost leverage
|
|
4
6
|
|
|
5
7
|
// Color helpers — TTY-aware, NO_COLOR-respecting (https://no-color.org/).
|
|
6
8
|
const USE_COLOR = (() => {
|
|
@@ -14,6 +16,7 @@ const c = {
|
|
|
14
16
|
bold: USE_COLOR ? '\x1b[1m' : '',
|
|
15
17
|
green: USE_COLOR ? '\x1b[32m' : '',
|
|
16
18
|
cyan: USE_COLOR ? '\x1b[36m' : '',
|
|
19
|
+
cyanBright: USE_COLOR ? '\x1b[96m' : '',
|
|
17
20
|
yellow: USE_COLOR ? '\x1b[33m' : '',
|
|
18
21
|
red: USE_COLOR ? '\x1b[31m' : '',
|
|
19
22
|
magenta: USE_COLOR ? '\x1b[35m' : '',
|
|
@@ -22,6 +25,7 @@ const c = {
|
|
|
22
25
|
};
|
|
23
26
|
|
|
24
27
|
function fmtTokens(n: number): string {
|
|
28
|
+
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(2)}B`;
|
|
25
29
|
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
|
|
26
30
|
if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`;
|
|
27
31
|
return String(n);
|
|
@@ -31,6 +35,10 @@ function fmtUsd(n: number): string {
|
|
|
31
35
|
return `$${n.toFixed(2)}`;
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
function fmtUsdComma(n: number): string {
|
|
39
|
+
return `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
function colorCost(n: number, padded: string): string {
|
|
35
43
|
if (!USE_COLOR) return padded;
|
|
36
44
|
if (n >= 1000) return `${c.bold}${c.red}${padded}${c.reset}`;
|
|
@@ -39,6 +47,11 @@ function colorCost(n: number, padded: string): string {
|
|
|
39
47
|
return `${c.dim}${padded}${c.reset}`;
|
|
40
48
|
}
|
|
41
49
|
|
|
50
|
+
function pct(part: number, whole: number): string {
|
|
51
|
+
if (whole === 0) return '0%';
|
|
52
|
+
return `${((part / whole) * 100).toFixed(0)}%`;
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
function formatRow(row: AggregatedRow, isTotal = false): string {
|
|
43
56
|
const labelText = row.subagent.padEnd(PAD_LABEL);
|
|
44
57
|
const label = isTotal
|
|
@@ -54,10 +67,74 @@ function formatRow(row: AggregatedRow, isTotal = false): string {
|
|
|
54
67
|
return ` ${label}${sessions} ${io} ${c.dim}1h:${c.reset}${cached1h} ${c.dim}5m:${c.reset}${cached5m} ${c.dim}r:${c.reset}${cacheRead} ${cost}`;
|
|
55
68
|
}
|
|
56
69
|
|
|
70
|
+
// Auto-derived insights. The whole point of installing the tool over /cost.
|
|
71
|
+
function deriveInsights(report: LedgerReport): string[] {
|
|
72
|
+
const lines: string[] = [];
|
|
73
|
+
const total = report.total.cost.totalCost;
|
|
74
|
+
if (total === 0) return lines;
|
|
75
|
+
|
|
76
|
+
// Top contributor with %
|
|
77
|
+
const top = report.rows[0];
|
|
78
|
+
if (top && top.cost.totalCost > 0) {
|
|
79
|
+
lines.push(
|
|
80
|
+
` ${c.cyanBright}⚡${c.reset} top: ${c.cyan}${top.subagent}${c.reset} drives ${c.bold}${pct(top.cost.totalCost, total)}${c.reset} of spend ${c.dim}(${fmtUsd(top.cost.totalCost)} of ${fmtUsd(total)})${c.reset}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 1h cache call-out (if non-trivial)
|
|
85
|
+
const oneHourCost =
|
|
86
|
+
report.total.cost.cacheCreation1hCost ?? 0;
|
|
87
|
+
if (oneHourCost >= total * 0.05) {
|
|
88
|
+
lines.push(
|
|
89
|
+
` ${c.yellow}💾${c.reset} 1h cache writes: ${fmtTokens(report.total.cacheCreation1hTokens)} tokens · ${fmtUsd(oneHourCost)} ${c.dim}(${pct(oneHourCost, total)} of total)${c.reset}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Cache reuse ratio — read vs write
|
|
94
|
+
const writeTokens =
|
|
95
|
+
report.total.cacheCreation1hTokens + report.total.cacheCreation5mTokens;
|
|
96
|
+
if (writeTokens > 0) {
|
|
97
|
+
const ratio = report.total.cacheReadTokens / writeTokens;
|
|
98
|
+
const verdict =
|
|
99
|
+
ratio >= 20
|
|
100
|
+
? `${c.green}excellent reuse${c.reset}`
|
|
101
|
+
: ratio >= 5
|
|
102
|
+
? `${c.yellow}OK reuse${c.reset}`
|
|
103
|
+
: `${c.red}low reuse${c.reset}`;
|
|
104
|
+
lines.push(
|
|
105
|
+
` ${c.cyanBright}📥${c.reset} cache reuse: ${fmtTokens(report.total.cacheReadTokens)} reads / ${fmtTokens(writeTokens)} writes = ${c.bold}${ratio.toFixed(0)}×${c.reset} ${c.dim}(${verdict}${c.dim})${c.reset}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Plan leverage
|
|
110
|
+
const dayCount =
|
|
111
|
+
Math.max(
|
|
112
|
+
1,
|
|
113
|
+
Math.round(
|
|
114
|
+
(report.period.to.getTime() - report.period.from.getTime()) /
|
|
115
|
+
(1000 * 60 * 60 * 24),
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
const monthlyEquivalent = (total / dayCount) * 30;
|
|
119
|
+
const leverage = monthlyEquivalent / PLAN_MAX_PRICE;
|
|
120
|
+
if (leverage >= 1) {
|
|
121
|
+
lines.push(
|
|
122
|
+
` ${c.magenta}💸${c.reset} ${fmtUsdComma(monthlyEquivalent)}/mo equivalent ${c.dim}vs $${PLAN_MAX_PRICE}/mo plan = ${c.reset}${c.bold}${leverage.toFixed(0)}×${c.reset} ${c.dim}leverage${c.reset}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return lines;
|
|
127
|
+
}
|
|
128
|
+
|
|
57
129
|
export function formatTable(report: LedgerReport, label: string): string {
|
|
58
|
-
const
|
|
59
|
-
`${c.
|
|
60
|
-
|
|
130
|
+
const summaryLine =
|
|
131
|
+
`${c.gray}${report.period.from.toISOString().slice(0, 10)} → ${report.period.to.toISOString().slice(0, 10)}${c.reset}` +
|
|
132
|
+
` ${c.dim}·${c.reset} ` +
|
|
133
|
+
`${c.bold}${fmtUsdComma(report.total.cost.totalCost)}${c.reset} ${c.dim}shadow${c.reset}` +
|
|
134
|
+
` ${c.dim}·${c.reset} ${report.total.sessionCount} sessions` +
|
|
135
|
+
` ${c.dim}·${c.reset} ${report.rows.length} ${label.includes('day') ? 'days' : label.includes('model') ? 'models' : 'agents'}`;
|
|
136
|
+
|
|
137
|
+
const header = `${c.bold}agent-ledger ${label}${c.reset} ${summaryLine}`;
|
|
61
138
|
|
|
62
139
|
const colsRaw =
|
|
63
140
|
' ' +
|
|
@@ -95,35 +172,137 @@ export function formatTable(report: LedgerReport, label: string): string {
|
|
|
95
172
|
);
|
|
96
173
|
}
|
|
97
174
|
|
|
175
|
+
// Insights
|
|
176
|
+
const insights = deriveInsights(report);
|
|
177
|
+
if (insights.length) {
|
|
178
|
+
lines.push('');
|
|
179
|
+
lines.push(...insights);
|
|
180
|
+
}
|
|
181
|
+
|
|
98
182
|
return lines.join('\n');
|
|
99
183
|
}
|
|
100
184
|
|
|
101
|
-
// Daily trend chart
|
|
185
|
+
// Daily trend chart with fixed-width bars.
|
|
102
186
|
export function formatTrend(report: LedgerReport, label: string): string {
|
|
103
187
|
const max = report.rows.reduce((m, r) => Math.max(m, r.cost.totalCost), 0);
|
|
104
|
-
const barWidth = 36;
|
|
105
188
|
|
|
106
|
-
const
|
|
107
|
-
`${c.
|
|
108
|
-
|
|
189
|
+
const summaryLine =
|
|
190
|
+
`${c.gray}${report.period.from.toISOString().slice(0, 10)} → ${report.period.to.toISOString().slice(0, 10)}${c.reset}` +
|
|
191
|
+
` ${c.dim}·${c.reset} ` +
|
|
192
|
+
`${c.bold}${fmtUsdComma(report.total.cost.totalCost)}${c.reset} ${c.dim}shadow${c.reset}` +
|
|
193
|
+
` ${c.dim}·${c.reset} ${report.total.sessionCount} sessions across ${report.rows.length} days`;
|
|
194
|
+
|
|
195
|
+
const header = `${c.bold}agent-ledger ${label} (daily)${c.reset} ${summaryLine}`;
|
|
109
196
|
|
|
110
197
|
const lines = [header, ''];
|
|
111
198
|
for (const row of report.rows) {
|
|
112
|
-
const bars = Math.round((row.cost.totalCost / Math.max(max, 0.01)) *
|
|
113
|
-
const
|
|
114
|
-
|
|
199
|
+
const bars = Math.round((row.cost.totalCost / Math.max(max, 0.01)) * BAR_WIDTH);
|
|
200
|
+
const barFilled = '█'.repeat(bars);
|
|
201
|
+
const barPadding = ' '.repeat(BAR_WIDTH - bars);
|
|
202
|
+
const bar = `${USE_COLOR ? c.cyanBright : ''}${barFilled}${USE_COLOR ? c.reset : ''}${barPadding}`;
|
|
115
203
|
const dateLabel = `${c.gray}${row.subagent}${c.reset}`;
|
|
116
|
-
const cost = colorCost(row.cost.totalCost, fmtUsd(row.cost.totalCost).padStart(
|
|
117
|
-
const
|
|
118
|
-
|
|
204
|
+
const cost = colorCost(row.cost.totalCost, fmtUsd(row.cost.totalCost).padStart(10));
|
|
205
|
+
const sessionsRaw = `${row.sessionCount} sess`;
|
|
206
|
+
const sessions = `${c.dim}· ${sessionsRaw}${c.reset}`;
|
|
207
|
+
lines.push(` ${dateLabel} ${cost} ${bar} ${sessions}`);
|
|
208
|
+
}
|
|
209
|
+
const sepWidth = 12 + 2 + 10 + 2 + BAR_WIDTH + 2 + 10;
|
|
210
|
+
lines.push(` ${c.gray}${'─'.repeat(sepWidth)}${c.reset}`);
|
|
211
|
+
const totalLabel = `${c.bold}total${c.reset}`.padEnd(12 + (USE_COLOR ? c.bold.length + c.reset.length : 0));
|
|
212
|
+
const totalCost = colorCost(
|
|
213
|
+
report.total.cost.totalCost,
|
|
214
|
+
fmtUsd(report.total.cost.totalCost).padStart(10),
|
|
215
|
+
);
|
|
216
|
+
lines.push(` ${totalLabel} ${totalCost}`);
|
|
217
|
+
|
|
218
|
+
// Daily-mode insights
|
|
219
|
+
const insights = deriveDailyInsights(report);
|
|
220
|
+
if (insights.length) {
|
|
221
|
+
lines.push('');
|
|
222
|
+
lines.push(...insights);
|
|
119
223
|
}
|
|
120
|
-
|
|
224
|
+
|
|
225
|
+
return lines.join('\n');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function deriveDailyInsights(report: LedgerReport): string[] {
|
|
229
|
+
if (report.rows.length === 0) return [];
|
|
230
|
+
const lines: string[] = [];
|
|
231
|
+
const peak = [...report.rows].sort((a, b) => b.cost.totalCost - a.cost.totalCost)[0]!;
|
|
232
|
+
const trough = [...report.rows]
|
|
233
|
+
.filter((r) => r.cost.totalCost > 0)
|
|
234
|
+
.sort((a, b) => a.cost.totalCost - b.cost.totalCost)[0];
|
|
235
|
+
const avg = report.total.cost.totalCost / report.rows.length;
|
|
236
|
+
|
|
237
|
+
lines.push(
|
|
238
|
+
` ${c.cyanBright}⚡${c.reset} peak: ${c.cyan}${peak.subagent}${c.reset} ${c.bold}${fmtUsd(peak.cost.totalCost)}${c.reset} ${c.dim}(${peak.sessionCount} sess)${c.reset}`,
|
|
239
|
+
);
|
|
121
240
|
lines.push(
|
|
122
|
-
` ${c.
|
|
123
|
-
report.total.cost.totalCost,
|
|
124
|
-
fmtUsd(report.total.cost.totalCost).padStart(9),
|
|
125
|
-
)}`,
|
|
241
|
+
` ${c.gray}📊${c.reset} avg/day: ${fmtUsd(avg)}`,
|
|
126
242
|
);
|
|
243
|
+
if (trough && trough.cost.totalCost > 0) {
|
|
244
|
+
const variance = peak.cost.totalCost / trough.cost.totalCost;
|
|
245
|
+
if (variance >= 3) {
|
|
246
|
+
lines.push(
|
|
247
|
+
` ${c.yellow}🌪${c.reset} variance: ${c.bold}${variance.toFixed(0)}×${c.reset} ${c.dim}between peak (${peak.subagent}) and trough (${trough.subagent})${c.reset}`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return lines;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Single-screen dashboard. Aggregates the same period across 3 dimensions.
|
|
255
|
+
export function formatSummary(
|
|
256
|
+
bySubagent: LedgerReport,
|
|
257
|
+
byModel: LedgerReport,
|
|
258
|
+
byDay: LedgerReport,
|
|
259
|
+
label: string,
|
|
260
|
+
): string {
|
|
261
|
+
const total = bySubagent.total.cost.totalCost;
|
|
262
|
+
const dayCount = Math.max(1, byDay.rows.length);
|
|
263
|
+
const monthlyEquivalent = (total / dayCount) * 30;
|
|
264
|
+
const leverage = monthlyEquivalent / PLAN_MAX_PRICE;
|
|
265
|
+
|
|
266
|
+
const top = (rows: AggregatedRow[]) => rows[0];
|
|
267
|
+
const tSub = top(bySubagent.rows);
|
|
268
|
+
const tMod = top(byModel.rows);
|
|
269
|
+
const tDay = [...byDay.rows].sort((a, b) => b.cost.totalCost - a.cost.totalCost)[0];
|
|
270
|
+
|
|
271
|
+
const k = (label: string, value: string) =>
|
|
272
|
+
` ${c.gray}${label.padEnd(20)}${c.reset} ${value}`;
|
|
273
|
+
const dollar = (n: number) => `${c.bold}${fmtUsdComma(n)}${c.reset}`;
|
|
274
|
+
|
|
275
|
+
const writeTokens =
|
|
276
|
+
bySubagent.total.cacheCreation1hTokens + bySubagent.total.cacheCreation5mTokens;
|
|
277
|
+
const reuseRatio = writeTokens > 0 ? bySubagent.total.cacheReadTokens / writeTokens : 0;
|
|
278
|
+
|
|
279
|
+
const lines = [
|
|
280
|
+
`${c.bold}agent-ledger ${label} summary${c.reset} ${c.gray}${bySubagent.period.from.toISOString().slice(0, 10)} → ${bySubagent.period.to.toISOString().slice(0, 10)}${c.reset}`,
|
|
281
|
+
'',
|
|
282
|
+
k('Shadow cost', dollar(total)),
|
|
283
|
+
k('Sessions', `${bySubagent.total.sessionCount}`),
|
|
284
|
+
k('Plan reference', `$${PLAN_MAX_PRICE}/mo (Max)`),
|
|
285
|
+
k('Multiplier', `${c.bold}${leverage.toFixed(0)}×${c.reset} ${c.dim}vs Max plan${c.reset}`),
|
|
286
|
+
'',
|
|
287
|
+
k('Top subagent', tSub ? `${c.cyan}${tSub.subagent}${c.reset} ${dollar(tSub.cost.totalCost)} ${c.dim}(${pct(tSub.cost.totalCost, total)})${c.reset}` : '—'),
|
|
288
|
+
k('Top model', tMod ? `${c.cyan}${tMod.subagent}${c.reset} ${dollar(tMod.cost.totalCost)} ${c.dim}(${pct(tMod.cost.totalCost, total)})${c.reset}` : '—'),
|
|
289
|
+
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
|
+
];
|
|
127
306
|
return lines.join('\n');
|
|
128
307
|
}
|
|
129
308
|
|