tokenmaxing 0.1.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/src/report.js ADDED
@@ -0,0 +1,974 @@
1
+ import {
2
+ compactNumber,
3
+ dollars,
4
+ formatInteger,
5
+ formatPercent,
6
+ sortedEntries,
7
+ tokenTotal,
8
+ } from "./utils.js";
9
+ import { estimateModelCosts } from "./pricing.js";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // palette — ember on black, edex-terminal style
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const RESET = "\x1b[0m";
16
+ const BOLD = "\x1b[1m";
17
+
18
+ const RED = [255, 59, 48];
19
+ const EMBER = [255, 94, 0];
20
+ const ORANGE = [255, 138, 0];
21
+ const AMBER = [255, 184, 48];
22
+ const FG = [235, 226, 210];
23
+ const GRAY = [128, 119, 104];
24
+ const SHADOW = [82, 74, 64];
25
+ const COAL = [54, 49, 43];
26
+ const HEAT_STOPS = [
27
+ [255, 59, 48],
28
+ [255, 94, 0],
29
+ [255, 184, 48],
30
+ ];
31
+ const GRID_LEVELS = [
32
+ [92, 36, 26],
33
+ [178, 60, 30],
34
+ [255, 94, 0],
35
+ [255, 184, 48],
36
+ ];
37
+
38
+ // categorical provider hues — claude warm orange, codex cool cyan, combined amber
39
+ const CYAN = [0, 209, 255];
40
+ const PROVIDER_COLORS = {
41
+ claude: ORANGE,
42
+ codex: CYAN,
43
+ all: AMBER,
44
+ };
45
+
46
+ function providerKey(name) {
47
+ const value = String(name || "").toLowerCase();
48
+ if (value.includes("codex") || /^(gpt|o3|o4|codex)/.test(value)) return "codex";
49
+ if (value.includes("claude")) return "claude";
50
+ return "all";
51
+ }
52
+
53
+ function providerColor(name) {
54
+ return PROVIDER_COLORS[providerKey(name)] || ORANGE;
55
+ }
56
+
57
+ const colorEnabled =
58
+ process.env.NO_COLOR === undefined &&
59
+ (process.env.FORCE_COLOR !== undefined || process.stdout.isTTY === true);
60
+
61
+ export function isColorEnabled() {
62
+ return colorEnabled;
63
+ }
64
+
65
+ function fg([r, g, b]) {
66
+ return colorEnabled ? `\x1b[38;2;${r};${g};${b}m` : "";
67
+ }
68
+
69
+ function bold(text) {
70
+ return colorEnabled ? `${BOLD}${text}${RESET}` : text;
71
+ }
72
+
73
+ function reset() {
74
+ return colorEnabled ? RESET : "";
75
+ }
76
+
77
+ function paint(rgb, text) {
78
+ return `${fg(rgb)}${text}${reset()}`;
79
+ }
80
+
81
+ function lerp(a, b, t) {
82
+ return [
83
+ Math.round(a[0] + (b[0] - a[0]) * t),
84
+ Math.round(a[1] + (b[1] - a[1]) * t),
85
+ Math.round(a[2] + (b[2] - a[2]) * t),
86
+ ];
87
+ }
88
+
89
+ function heat(t) {
90
+ const clamped = Math.min(1, Math.max(0, t));
91
+ const [low, mid, high] = HEAT_STOPS;
92
+ return clamped < 0.5 ? lerp(low, mid, clamped * 2) : lerp(mid, high, (clamped - 0.5) * 2);
93
+ }
94
+
95
+ function scale(rgb, k) {
96
+ return [Math.round(rgb[0] * k), Math.round(rgb[1] * k), Math.round(rgb[2] * k)];
97
+ }
98
+
99
+ // single-hue ramp: dim at the base, full saturation toward the bar tip
100
+ function shadeRamp(color) {
101
+ const base = scale(color, 0.42);
102
+ const tip = lerp(color, [255, 255, 255], 0.12);
103
+ return (t) => (t < 0.5 ? lerp(base, color, t * 2) : lerp(color, tip, (t - 0.5) * 2));
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // ansi-aware layout helpers
108
+ // ---------------------------------------------------------------------------
109
+
110
+ const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
111
+
112
+ function visibleLength(text) {
113
+ return text.replace(ANSI_PATTERN, "").length;
114
+ }
115
+
116
+ function padEndVisible(text, width) {
117
+ return text + " ".repeat(Math.max(0, width - visibleLength(text)));
118
+ }
119
+
120
+ function padStartVisible(text, width) {
121
+ return " ".repeat(Math.max(0, width - visibleLength(text))) + text;
122
+ }
123
+
124
+ function truncate(text, width) {
125
+ return text.length > width ? `${text.slice(0, width - 1)}…` : text;
126
+ }
127
+
128
+ function wrapText(text, width) {
129
+ const words = String(text).split(/\s+/);
130
+ const wrapped = [];
131
+ let current = "";
132
+ for (const word of words) {
133
+ if (current && current.length + word.length + 1 > width) {
134
+ wrapped.push(current);
135
+ current = word;
136
+ } else {
137
+ current = current ? `${current} ${word}` : word;
138
+ }
139
+ }
140
+ if (current) wrapped.push(current);
141
+ return wrapped;
142
+ }
143
+
144
+ function reportWidth() {
145
+ const columns = process.stdout.columns || Number(process.env.COLUMNS) || 96;
146
+ return Math.min(Math.max(columns - 2, 80), 104);
147
+ }
148
+
149
+ function fmtUSD(value) {
150
+ return `$${(Number(value) || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // banner — 5x5 pixel font folded into half-blocks
155
+ // ---------------------------------------------------------------------------
156
+
157
+ const GLYPHS = {
158
+ A: [".###.", "#...#", "#####", "#...#", "#...#"],
159
+ I: ["#####", "..#..", "..#..", "..#..", "#####"],
160
+ W: ["#...#", "#...#", "#.#.#", "##.##", "#...#"],
161
+ R: ["####.", "#...#", "####.", "#..#.", "#...#"],
162
+ P: ["####.", "#...#", "####.", "#....", "#...."],
163
+ E: ["#####", "#....", "####.", "#....", "#####"],
164
+ D: ["####.", "#...#", "#...#", "#...#", "####."],
165
+ " ": ["..", "..", "..", "..", ".."],
166
+ };
167
+
168
+ function banner(text, indent, colorFn = heat) {
169
+ const grid = ["", "", "", "", ""];
170
+ for (const letter of text) {
171
+ const glyph = GLYPHS[letter] || GLYPHS[" "];
172
+ for (let row = 0; row < 5; row += 1) grid[row] += `${glyph[row]}.`;
173
+ }
174
+
175
+ const totalCols = grid[0].length;
176
+ const lines = [];
177
+ for (let pair = 0; pair < 3; pair += 1) {
178
+ const top = grid[pair * 2];
179
+ const bottom = grid[pair * 2 + 1] || ".".repeat(totalCols);
180
+ let line = " ".repeat(indent);
181
+ for (let col = 0; col < totalCols; col += 1) {
182
+ const upper = top[col] === "#";
183
+ const lower = bottom[col] === "#";
184
+ const glyphChar = upper && lower ? "█" : upper ? "▀" : lower ? "▄" : " ";
185
+ if (glyphChar === " ") {
186
+ line += " ";
187
+ } else {
188
+ line += fg(colorFn(totalCols <= 1 ? 1 : col / (totalCols - 1))) + glyphChar;
189
+ }
190
+ }
191
+ lines.push(line + reset());
192
+ }
193
+ return lines;
194
+ }
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // panel frame — edex-style boxed sections
198
+ // ---------------------------------------------------------------------------
199
+
200
+ function panel(title, contentLines, width) {
201
+ const interior = width - 4;
202
+ const lines = [];
203
+
204
+ const head =
205
+ paint(COAL, "┌── ") +
206
+ bold(paint(ORANGE, title.toUpperCase())) +
207
+ paint(SHADOW, " ");
208
+ lines.push(head + paint(COAL, `${"─".repeat(Math.max(0, width - visibleLength(head) - 1))}┐`));
209
+ lines.push(paint(COAL, "│") + " ".repeat(width - 2) + paint(COAL, "│"));
210
+
211
+ for (const content of contentLines) {
212
+ lines.push(paint(COAL, "│ ") + padEndVisible(content, interior) + paint(COAL, " │"));
213
+ }
214
+
215
+ lines.push(paint(COAL, "│") + " ".repeat(width - 2) + paint(COAL, "│"));
216
+ lines.push(paint(COAL, `└${"─".repeat(width - 2)}┘`));
217
+ return lines;
218
+ }
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // widgets — every widget takes the panel interior width
222
+ // ---------------------------------------------------------------------------
223
+
224
+ const PARTIALS = "▏▎▍▌▋▊▉█";
225
+
226
+ function gradientBar(ratio, width, colorFn = heat) {
227
+ const cells = Math.min(1, Math.max(0, ratio)) * width;
228
+ const full = Math.floor(cells);
229
+ const fracIndex = Math.round((cells - full) * 8);
230
+
231
+ let out = "";
232
+ for (let cell = 0; cell < full; cell += 1) {
233
+ out += fg(colorFn(width <= 1 ? 1 : cell / (width - 1))) + "█";
234
+ }
235
+ let used = full;
236
+ if (fracIndex > 0 && full < width) {
237
+ out += fg(colorFn(width <= 1 ? 1 : full / (width - 1))) + PARTIALS[fracIndex - 1];
238
+ used += 1;
239
+ }
240
+ out += fg(COAL) + "·".repeat(Math.max(0, width - used)) + reset();
241
+ return out;
242
+ }
243
+
244
+ function barChart(items, width, options = {}) {
245
+ const clean = items.filter((item) => Number.isFinite(item.value) && item.value >= 0);
246
+ if (!clean.length) return [paint(GRAY, "no data")];
247
+
248
+ const labelWidth = options.labelWidth || 18;
249
+ const valueWidth = options.valueWidth || 9;
250
+ const prefixWidth = options.ranked ? 3 : 0;
251
+ const barWidth = Math.max(14, width - labelWidth - valueWidth - prefixWidth - 3);
252
+ const max = Math.max(...clean.map((item) => item.value), Number.EPSILON);
253
+ const valueFormatter = options.valueFormatter || compactNumber;
254
+
255
+ return clean.map((item, index) => {
256
+ const hue = item.color || null;
257
+ const colorFn = hue ? shadeRamp(hue) : heat;
258
+ const isTop = options.rankTop && item.value === max;
259
+ const labelColor = hue || (isTop ? AMBER : GRAY);
260
+ const label = padEndVisible(paint(labelColor, truncate(item.label, labelWidth)), labelWidth);
261
+ const value = padStartVisible(
262
+ isTop ? bold(paint(AMBER, valueFormatter(item.value))) : paint(hue || FG, valueFormatter(item.value)),
263
+ valueWidth,
264
+ );
265
+ const prefix = options.ranked
266
+ ? paint(index === 0 ? RED : SHADOW, `${String(index + 1).padStart(2, "0")} `)
267
+ : "";
268
+ return `${prefix}${label} ${paint(SHADOW, "▕")}${gradientBar(item.value / max, barWidth, colorFn)} ${value}`;
269
+ });
270
+ }
271
+
272
+ function statTiles(stats, width) {
273
+ const columns = 3;
274
+ const gap = 2;
275
+ const tileWidth = Math.floor((width - gap * (columns - 1)) / columns);
276
+ const inner = tileWidth - 4;
277
+ const lines = [];
278
+
279
+ for (let start = 0; start < stats.length; start += columns) {
280
+ const row = stats.slice(start, start + columns);
281
+ const top = [];
282
+ const labelLine = [];
283
+ const valueLine = [];
284
+ const bottom = [];
285
+
286
+ for (const stat of row) {
287
+ top.push(paint(COAL, `┌${"─".repeat(tileWidth - 2)}┐`));
288
+ labelLine.push(
289
+ paint(COAL, "│ ") +
290
+ padEndVisible(paint(GRAY, truncate(stat.label.toUpperCase(), inner)), inner) +
291
+ paint(COAL, " │"),
292
+ );
293
+ const accent = stat.accent || ORANGE;
294
+ valueLine.push(
295
+ paint(COAL, "│ ") +
296
+ padEndVisible(bold(paint(accent, truncate(String(stat.value), inner))), inner) +
297
+ paint(COAL, " │"),
298
+ );
299
+ bottom.push(paint(COAL, `└${"─".repeat(tileWidth - 2)}┘`));
300
+ }
301
+
302
+ const joiner = " ".repeat(gap);
303
+ lines.push(top.join(joiner), labelLine.join(joiner), valueLine.join(joiner), bottom.join(joiner));
304
+ }
305
+ return lines;
306
+ }
307
+
308
+ const BLOCKS_VERTICAL = "▁▂▃▄▅▆▇█";
309
+
310
+ function hourHistogram(hours) {
311
+ const total = hours.reduce((sum, value) => sum + (Number(value) || 0), 0);
312
+ if (!total) return [paint(GRAY, "no data")];
313
+
314
+ const max = Math.max(...hours, Number.EPSILON);
315
+ let peakHour = 0;
316
+ for (let hour = 0; hour < 24; hour += 1) {
317
+ if ((hours[hour] || 0) > (hours[peakHour] || 0)) peakHour = hour;
318
+ }
319
+
320
+ const ROWS = 6;
321
+ const lines = [];
322
+ for (let row = ROWS; row >= 1; row -= 1) {
323
+ let line = "";
324
+ for (let hour = 0; hour < 24; hour += 1) {
325
+ const level = ((hours[hour] || 0) / max) * ROWS;
326
+ const fill = Math.min(1, Math.max(0, level - (row - 1)));
327
+ const index = Math.round(fill * 8);
328
+ if (index === 0) {
329
+ line += " ";
330
+ } else {
331
+ const color = hour === peakHour ? RED : heat((hours[hour] || 0) / max);
332
+ line += fg(color) + BLOCKS_VERTICAL[index - 1].repeat(2) + reset() + " ";
333
+ }
334
+ }
335
+ lines.push(line);
336
+ }
337
+
338
+ lines.push(paint(COAL, "▔".repeat(24 * 3 - 1)));
339
+
340
+ let axis = "";
341
+ for (let hour = 0; hour < 24; hour += 1) {
342
+ if (hour % 3 === 0 || hour === peakHour) {
343
+ const color = hour === peakHour ? RED : SHADOW;
344
+ axis += paint(color, String(hour).padStart(2, "0")) + " ";
345
+ } else {
346
+ axis += " ";
347
+ }
348
+ }
349
+ lines.push(axis);
350
+ lines.push("");
351
+ lines.push(
352
+ paint(GRAY, "peak ") +
353
+ bold(paint(RED, `${String(peakHour).padStart(2, "0")}:00`)) +
354
+ paint(GRAY, ` — ${formatInteger(hours[peakHour])} messages (${formatPercent(hours[peakHour] / total)} of all activity)`),
355
+ );
356
+ return lines;
357
+ }
358
+
359
+ // github-contribution-style daily activity matrix, edex memory-grid flavored
360
+ function activityMatrix(dailyActivity, width) {
361
+ const days = Object.keys(dailyActivity || {}).sort();
362
+ if (!days.length) return [paint(GRAY, "no data")];
363
+
364
+ const LABEL_WIDTH = 4;
365
+ const first = new Date(`${days[0]}T00:00:00`);
366
+ const last = new Date(`${days[days.length - 1]}T00:00:00`);
367
+
368
+ // grid columns are weeks starting monday; drop to single-char cells when a
369
+ // full history (e.g. a whole year) would not fit, trim oldest weeks last
370
+ const gridStart = new Date(first);
371
+ gridStart.setDate(gridStart.getDate() - ((gridStart.getDay() + 6) % 7));
372
+ const totalWeeks = Math.floor((last - gridStart) / (7 * 86400000)) + 1;
373
+ const cellWidth = LABEL_WIDTH + totalWeeks * 2 <= width ? 2 : 1;
374
+ const maxWeeks = Math.floor((width - LABEL_WIDTH) / cellWidth);
375
+ const weeks = Math.min(totalWeeks, maxWeeks);
376
+ const start = new Date(gridStart);
377
+ start.setDate(start.getDate() + (totalWeeks - weeks) * 7);
378
+
379
+ const max = Math.max(...Object.values(dailyActivity), 1);
380
+ const dayKey = (date) =>
381
+ `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
382
+
383
+ // month labels across the top
384
+ const monthCells = Array.from({ length: LABEL_WIDTH + weeks * cellWidth }, () => " ");
385
+ let lastMonth = -1;
386
+ let lastLabelEnd = -1;
387
+ for (let week = 0; week < weeks; week += 1) {
388
+ const weekDate = new Date(start);
389
+ weekDate.setDate(weekDate.getDate() + week * 7);
390
+ const month = weekDate.getMonth();
391
+ const at = LABEL_WIDTH + week * cellWidth;
392
+ if (month !== lastMonth && at > lastLabelEnd && at + 3 <= monthCells.length) {
393
+ for (let offset = 0; offset < 3; offset += 1) monthCells[at + offset] = MONTH_NAMES[month][offset];
394
+ lastLabelEnd = at + 3;
395
+ lastMonth = month;
396
+ }
397
+ }
398
+ const monthRow = paint(GRAY, monthCells.join("").trimEnd());
399
+
400
+ const DAY_LABELS = ["MON", "", "WED", "", "FRI", "", "SUN"];
401
+ const gap = cellWidth === 2 ? " " : "";
402
+ const rows = [monthRow];
403
+ for (let weekday = 0; weekday < 7; weekday += 1) {
404
+ let row = padEndVisible(paint(GRAY, DAY_LABELS[weekday]), LABEL_WIDTH);
405
+ for (let week = 0; week < weeks; week += 1) {
406
+ const date = new Date(start);
407
+ date.setDate(date.getDate() + week * 7 + weekday);
408
+ if (date < first || date > last) {
409
+ row += " " + gap;
410
+ continue;
411
+ }
412
+ const value = dailyActivity[dayKey(date)] || 0;
413
+ if (value <= 0) {
414
+ row += paint(GRAY, "·") + gap;
415
+ } else {
416
+ const level = Math.min(GRID_LEVELS.length - 1, Math.floor((value / max) * GRID_LEVELS.length));
417
+ row += paint(GRID_LEVELS[level], "■") + gap;
418
+ }
419
+ }
420
+ rows.push(row);
421
+ }
422
+
423
+ // streaks + busiest day, edex "using x of y" style header
424
+ const activeDays = days.filter((day) => dailyActivity[day] > 0);
425
+ const spanDays = Math.round((last - first) / 86400000) + 1;
426
+ let longestStreak = 0;
427
+ let streak = 0;
428
+ let previousTime = null;
429
+ for (const day of activeDays) {
430
+ const time = new Date(`${day}T00:00:00`).getTime();
431
+ streak = previousTime !== null && time - previousTime === 86400000 ? streak + 1 : 1;
432
+ longestStreak = Math.max(longestStreak, streak);
433
+ previousTime = time;
434
+ }
435
+ const busiest = activeDays.reduce((best, day) => (dailyActivity[day] > dailyActivity[best] ? day : best), activeDays[0]);
436
+
437
+ const header =
438
+ paint(GRAY, "ACTIVE ") +
439
+ bold(paint(AMBER, `${activeDays.length}`)) +
440
+ paint(GRAY, ` OUT OF ${spanDays} DAYS`) +
441
+ paint(SHADOW, totalWeeks > weeks ? ` · showing last ${weeks} weeks` : "");
442
+
443
+ let legend = paint(SHADOW, "LESS ") + paint(GRAY, "· ");
444
+ for (const level of GRID_LEVELS) legend += paint(level, "■ ");
445
+ legend += paint(SHADOW, "MORE");
446
+ const facts =
447
+ paint(GRAY, "longest streak ") +
448
+ bold(paint(RED, `${longestStreak}d`)) +
449
+ paint(GRAY, " · busiest ") +
450
+ paint(AMBER, `${monthLabel(busiest.slice(0, 7)).split(" ")[0]} ${Number(busiest.slice(8))}`) +
451
+ paint(GRAY, ` — ${formatInteger(dailyActivity[busiest])} messages`);
452
+
453
+ return [header, "", ...rows, "", legend, facts];
454
+ }
455
+
456
+ function stackedShareBar(segments, width) {
457
+ const barWidth = width - 2;
458
+ const total = segments.reduce((sum, segment) => sum + segment.value, 0);
459
+ if (!total) return [paint(GRAY, "no data")];
460
+
461
+ let bar = "";
462
+ let used = 0;
463
+ const visible = segments.filter((segment) => segment.value > 0);
464
+ for (let index = 0; index < visible.length; index += 1) {
465
+ const segment = visible[index];
466
+ const isLast = index === visible.length - 1;
467
+ const cells = isLast
468
+ ? Math.max(0, barWidth - used)
469
+ : Math.max(1, Math.round((segment.value / total) * barWidth));
470
+ bar += fg(segment.color) + "█".repeat(Math.max(0, cells));
471
+ used += cells;
472
+ }
473
+ bar += reset();
474
+
475
+ const lines = [bar, ""];
476
+ const labelWidth = Math.max(...segments.map((segment) => segment.label.length)) + 2;
477
+ for (const segment of segments) {
478
+ const share = segment.value / total;
479
+ lines.push(
480
+ paint(segment.color, "■ ") +
481
+ padEndVisible(paint(GRAY, segment.label.toUpperCase()), labelWidth) +
482
+ padStartVisible(paint(FG, compactNumber(segment.value)), 9) +
483
+ padStartVisible(paint(segment.value > 0 ? AMBER : SHADOW, formatPercent(share)), 8),
484
+ );
485
+ }
486
+ return lines;
487
+ }
488
+
489
+ function tokenSummaryTable(reports) {
490
+ const columns = [
491
+ { key: "name", label: "SOURCE", width: 12, left: true },
492
+ { key: "input", label: "INPUT", width: 8 },
493
+ { key: "output", label: "OUTPUT", width: 8 },
494
+ { key: "cacheRead", label: "CACHE R", width: 8 },
495
+ { key: "cacheCreation", label: "CACHE W", width: 8 },
496
+ { key: "reasoning", label: "REASON", width: 8 },
497
+ { key: "total", label: "TOTAL", width: 9 },
498
+ ];
499
+
500
+ const rows = reports.map((report) => ({
501
+ name: report.provider,
502
+ hue: providerColor(report.key),
503
+ input: report.tokens.input || 0,
504
+ output: report.tokens.output || 0,
505
+ cacheRead: report.tokens.cacheRead || 0,
506
+ cacheCreation: report.tokens.cacheCreation || 0,
507
+ reasoning: report.tokens.reasoning || 0,
508
+ total: tokenTotal(report.tokens),
509
+ combined: report.key === "all",
510
+ }));
511
+
512
+ const header = columns
513
+ .map((column) =>
514
+ column.left
515
+ ? padEndVisible(paint(SHADOW, column.label), column.width)
516
+ : padStartVisible(paint(SHADOW, column.label), column.width),
517
+ )
518
+ .join(" ");
519
+
520
+ const tableWidth = columns.reduce((sum, column) => sum + column.width + 1, -1);
521
+ const lines = [header, paint(COAL, "─".repeat(tableWidth))];
522
+
523
+ for (const row of rows) {
524
+ const cells = columns.map((column) => {
525
+ if (column.left) {
526
+ const name = truncate(row.name, column.width);
527
+ return padEndVisible(row.combined ? bold(paint(row.hue, name)) : paint(row.hue, name), column.width);
528
+ }
529
+ const value = compactNumber(row[column.key]);
530
+ const color = column.key === "total" ? AMBER : row[column.key] > 0 ? FG : SHADOW;
531
+ return padStartVisible(row.combined ? bold(paint(color, value)) : paint(color, value), column.width);
532
+ });
533
+ lines.push(cells.join(" "));
534
+ }
535
+ return lines;
536
+ }
537
+
538
+ // ---------------------------------------------------------------------------
539
+ // month / model labels
540
+ // ---------------------------------------------------------------------------
541
+
542
+ const MONTH_NAMES = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
543
+
544
+ function monthLabel(month) {
545
+ const [year, monthNumber] = String(month).split("-");
546
+ const name = MONTH_NAMES[Number(monthNumber) - 1];
547
+ return name ? `${name} ${year}` : month;
548
+ }
549
+
550
+ function shortDate(iso) {
551
+ const date = new Date(iso);
552
+ if (!Number.isFinite(date.getTime())) return iso;
553
+ return `${MONTH_NAMES[date.getMonth()]} ${String(date.getDate()).padStart(2, "0")}`;
554
+ }
555
+
556
+ function modelLabel(usage) {
557
+ return `${usage.provider || ""} ${usage.model || "unknown"}`.trim();
558
+ }
559
+
560
+ // ---------------------------------------------------------------------------
561
+ // burn estimation — list prices applied to per-model token splits
562
+ // ---------------------------------------------------------------------------
563
+
564
+ function buildBurnLines(combined, width) {
565
+ const lines = [];
566
+ const hasLocalSpend = Object.keys(combined.monthlySpendUSD).length > 0;
567
+
568
+ if (hasLocalSpend) {
569
+ lines.push(
570
+ ...barChart(
571
+ sortedEntries(combined.monthlySpendUSD).map(([month, spendUSD]) => ({
572
+ label: monthLabel(month),
573
+ value: spendUSD,
574
+ })),
575
+ width,
576
+ { valueFormatter: fmtUSD, rankTop: true, valueWidth: 11 },
577
+ ),
578
+ );
579
+ return lines;
580
+ }
581
+
582
+ const { priced, unpriced, totalUSD } = estimateModelCosts(combined.modelUsage);
583
+ if (!priced.length) {
584
+ lines.push(paint(GRAY, "no local spend cache and no models matched the pricing table"));
585
+ return lines;
586
+ }
587
+
588
+ lines.push(paint(GRAY, "EST. ALL-TIME BURN ") + bold(paint(RED, `≈ ${fmtUSD(totalUSD)}`)));
589
+ lines.push("");
590
+
591
+ // allocate the estimated total across months in proportion to token volume
592
+ const monthTotals = Object.entries(combined.monthlyTokens || {}).map(([month, tokens]) => ({
593
+ month,
594
+ tokens: tokenTotal(tokens),
595
+ }));
596
+ const trackedTokens = monthTotals.reduce((sum, entry) => sum + entry.tokens, 0);
597
+ if (trackedTokens > 0) {
598
+ lines.push(
599
+ ...barChart(
600
+ monthTotals
601
+ .sort((a, b) => a.month.localeCompare(b.month))
602
+ .map((entry) => ({
603
+ label: monthLabel(entry.month),
604
+ value: (totalUSD * entry.tokens) / trackedTokens,
605
+ })),
606
+ width,
607
+ { valueFormatter: (value) => `≈ ${fmtUSD(value)}`, rankTop: true, valueWidth: 12 },
608
+ ),
609
+ );
610
+ lines.push("");
611
+ }
612
+
613
+ lines.push(paint(SHADOW, "BY MODEL"));
614
+ lines.push(
615
+ ...barChart(
616
+ priced.slice(0, 8).map((entry) => ({
617
+ label: modelLabel(entry.usage),
618
+ value: entry.costUSD,
619
+ })),
620
+ width,
621
+ { valueFormatter: (value) => `≈ ${fmtUSD(value)}`, labelWidth: 32, valueWidth: 12, ranked: true },
622
+ ),
623
+ );
624
+ lines.push("");
625
+ const footnotes = [
626
+ "estimated from public API list prices per 1M tokens — subscription plans not reflected",
627
+ "reasoning tokens billed within output",
628
+ ];
629
+ if (unpriced.length) footnotes.push(`unpriced models excluded: ${unpriced.join(", ")}`);
630
+ for (const footnote of footnotes) {
631
+ for (const wrapped of wrapText(footnote, width)) lines.push(paint(SHADOW, wrapped));
632
+ }
633
+ return lines;
634
+ }
635
+
636
+ // ---------------------------------------------------------------------------
637
+ // hero — the one big screenshot stat
638
+ // ---------------------------------------------------------------------------
639
+
640
+ const WAR_AND_PEACE_WORDS = 587287;
641
+
642
+ // full-height 5x5 digit font — rendered upright (no half-block folding) so the
643
+ // numbers stay unambiguous; magnitude (BILLION/MILLION/…) goes in the label
644
+ const HERO_DIGITS = {
645
+ "0": ["█████", "█ █", "█ █", "█ █", "█████"],
646
+ "1": [" █ ", " ██ ", " █ ", " █ ", "█████"],
647
+ "2": ["█████", " █", "█████", "█ ", "█████"],
648
+ "3": ["█████", " █", " ████", " █", "█████"],
649
+ "4": ["█ █", "█ █", "█████", " █", " █"],
650
+ "5": ["█████", "█ ", "█████", " █", "█████"],
651
+ "6": ["█████", "█ ", "█████", "█ █", "█████"],
652
+ "7": ["█████", " █", " █ ", " █ ", " █ "],
653
+ "8": ["█████", "█ █", "█████", "█ █", "█████"],
654
+ "9": ["█████", "█ █", "█████", " █", "█████"],
655
+ ".": [" ", " ", " ", " ", " ██ "],
656
+ };
657
+
658
+ const MAGNITUDE_WORDS = { K: "THOUSAND", M: "MILLION", B: "BILLION", T: "TRILLION" };
659
+
660
+ function centerVisible(line, width) {
661
+ const pad = Math.max(0, Math.floor((width - visibleLength(line)) / 2));
662
+ return " ".repeat(pad) + line;
663
+ }
664
+
665
+ function heroColor(t) {
666
+ return t < 0.5 ? lerp(EMBER, AMBER, t * 2) : lerp(AMBER, [255, 240, 205], (t - 0.5) * 2);
667
+ }
668
+
669
+ // big upright number, each pixel doubled horizontally for weight
670
+ function heroNumber(text) {
671
+ const rows = ["", "", "", "", ""];
672
+ for (const char of text) {
673
+ const glyph = HERO_DIGITS[char] || HERO_DIGITS["."];
674
+ for (let row = 0; row < 5; row += 1) {
675
+ rows[row] += glyph[row].replace(/./g, (cell) => (cell === "█" ? "██" : " ")) + " ";
676
+ }
677
+ }
678
+
679
+ const totalCols = Math.max(...rows.map((row) => row.length));
680
+ return rows.map((row) => {
681
+ let out = "";
682
+ for (let col = 0; col < row.length; col += 1) {
683
+ out += row[col] === " " ? " " : fg(heroColor(totalCols <= 1 ? 1 : col / (totalCols - 1))) + row[col];
684
+ }
685
+ return out + reset();
686
+ });
687
+ }
688
+
689
+ function heroBlock(combined, width) {
690
+ const tokens = tokenTotal(combined.tokens);
691
+ const useTokens = tokens > 0;
692
+ const compact = compactNumber(useTokens ? tokens : combined.totalMessages);
693
+ const match = compact.match(/^([\d.,]+)([A-Z]?)$/) || [compact, compact, ""];
694
+ const digits = match[1].replace(/,/g, "");
695
+ const magnitude = MAGNITUDE_WORDS[match[2]] || "";
696
+ const noun = useTokens ? "TOKENS PROCESSED" : "MESSAGES SENT";
697
+ const label = `${magnitude ? `${magnitude} ` : ""}${noun}`;
698
+
699
+ const big = heroNumber(digits);
700
+ const lines = ["", ...big.map((line) => centerVisible(line, width)), ""];
701
+ lines.push(centerVisible(bold(paint(AMBER, label.split("").join(" "))), width));
702
+
703
+ if (useTokens) {
704
+ const words = Math.round(tokens * 0.75);
705
+ const novels = words / WAR_AND_PEACE_WORDS;
706
+ const burn = estimateModelCosts(combined.modelUsage).totalUSD;
707
+ const novelText =
708
+ novels >= 1 ? `${novels < 10 ? novels.toFixed(1) : compactNumber(Math.round(novels))}× War & Peace` : null;
709
+ const parts = [`≈ ${compactNumber(words)} words`];
710
+ if (novelText) parts.push(novelText);
711
+ if (burn > 0) parts.push(`≈ ${fmtUSD(burn)} at list price`);
712
+ lines.push("");
713
+ lines.push(centerVisible(paint(SHADOW, parts.join(" · ")), width));
714
+ }
715
+
716
+ return lines;
717
+ }
718
+
719
+ // ---------------------------------------------------------------------------
720
+ // report
721
+ // ---------------------------------------------------------------------------
722
+
723
+ export function renderReportBlocks(payload) {
724
+ const { reports, combined, generatedAt } = payload;
725
+ const width = reportWidth();
726
+ const interior = width - 4;
727
+ const blocks = [];
728
+ let lines = [];
729
+ const endBlock = () => {
730
+ blocks.push(lines.join("\n"));
731
+ lines = [];
732
+ };
733
+
734
+ // banner + status header
735
+ lines.push("");
736
+ lines.push(...banner("AI WRAPPED", 2));
737
+ lines.push("");
738
+ const range =
739
+ combined.firstSeen && combined.lastSeen
740
+ ? `${shortDate(combined.firstSeen)} → ${shortDate(combined.lastSeen)}`
741
+ : "no history";
742
+ const subtitle =
743
+ " " +
744
+ paint(GRAY, "TERMINAL USAGE REPORT ") +
745
+ paint(SHADOW, "· ") +
746
+ paint(AMBER, range) +
747
+ paint(SHADOW, " · ") +
748
+ paint(GRAY, "LOCAL ONLY");
749
+ const generated = paint(SHADOW, `generated ${new Date(generatedAt).toLocaleString()}`);
750
+ if (visibleLength(subtitle) + visibleLength(generated) + 3 <= width) {
751
+ lines.push(`${subtitle}${paint(SHADOW, " · ")}${generated}`);
752
+ } else {
753
+ lines.push(subtitle);
754
+ lines.push(` ${generated}`);
755
+ }
756
+ for (const report of reports) {
757
+ const hue = providerColor(report.key);
758
+ const dot = report.available ? paint(hue, "●") : paint(RED, "○");
759
+ const status = report.available ? paint(GRAY, report.root || "n/a") : paint(RED, "missing");
760
+ const extra =
761
+ report.key === "claude" && report.available && report.projectConfigDirs
762
+ ? paint(SHADOW, ` +${report.projectConfigDirs} project .claude dirs`)
763
+ : "";
764
+ lines.push(` ${dot} ${padEndVisible(paint(hue, report.provider), 13)} ${status}${extra}`);
765
+ }
766
+ lines.push("");
767
+ endBlock();
768
+
769
+ // hero — the screenshot stat
770
+ lines.push(...heroBlock(combined, width));
771
+ lines.push("");
772
+ endBlock();
773
+
774
+ // overview tiles
775
+ const burn = estimateModelCosts(combined.modelUsage);
776
+ const spendTile = combined.knownSpendUSD
777
+ ? { label: "known spend", value: fmtUSD(combined.knownSpendUSD), accent: AMBER }
778
+ : burn.totalUSD > 0
779
+ ? { label: "est. burn (list price)", value: `≈ ${fmtUSD(burn.totalUSD)}`, accent: AMBER }
780
+ : { label: "known spend", value: dollars(0), accent: AMBER };
781
+ lines.push(
782
+ ...statTiles(
783
+ [
784
+ { label: "messages", value: formatInteger(combined.totalMessages), accent: RED },
785
+ { label: "all-time tokens", value: compactNumber(tokenTotal(combined.tokens)) },
786
+ { label: "sessions", value: `${formatInteger(combined.totalSessions)} · ${formatInteger(combined.sessionFiles)} files` },
787
+ spendTile,
788
+ {
789
+ label: "favorite model",
790
+ value: combined.favoriteModel ? combined.favoriteModel.model : "unknown",
791
+ accent: combined.favoriteModel ? providerColor(combined.favoriteModel.model) : RED,
792
+ },
793
+ { label: "history range", value: range, accent: AMBER },
794
+ ],
795
+ width,
796
+ ),
797
+ );
798
+ lines.push("");
799
+ endBlock();
800
+
801
+ // 01 messages by source
802
+ lines.push(
803
+ ...panel(
804
+ "01 · messages",
805
+ barChart(
806
+ [combined, ...reports].map((report) => ({
807
+ label: report.provider,
808
+ value: report.totalMessages,
809
+ color: providerColor(report.key),
810
+ })),
811
+ interior,
812
+ { valueFormatter: formatInteger, valueWidth: 11 },
813
+ ),
814
+ width,
815
+ ),
816
+ );
817
+ endBlock();
818
+
819
+ // 02 monthly tokens
820
+ lines.push(
821
+ ...panel(
822
+ "02 · monthly tokens",
823
+ barChart(
824
+ sortedEntries(combined.monthlyTokens).map(([month, tokens]) => ({
825
+ label: monthLabel(month),
826
+ value: tokenTotal(tokens),
827
+ })),
828
+ interior,
829
+ { valueFormatter: compactNumber, rankTop: true },
830
+ ),
831
+ width,
832
+ ),
833
+ );
834
+ endBlock();
835
+
836
+ // 03 burn — local spend cache when present, list-price estimate otherwise
837
+ lines.push(...panel("03 · burn — list-price estimate", buildBurnLines(combined, interior), width));
838
+ endBlock();
839
+
840
+ // 04 all-time tokens by source
841
+ lines.push(
842
+ ...panel(
843
+ "04 · all-time tokens",
844
+ barChart(
845
+ [combined, ...reports].map((report) => ({
846
+ label: report.provider,
847
+ value: tokenTotal(report.tokens),
848
+ color: providerColor(report.key),
849
+ })),
850
+ interior,
851
+ { valueFormatter: compactNumber },
852
+ ),
853
+ width,
854
+ ),
855
+ );
856
+ endBlock();
857
+
858
+ // 05 token ledger
859
+ lines.push(...panel("05 · token ledger", tokenSummaryTable([combined, ...reports]), width));
860
+ endBlock();
861
+
862
+ // 06 token mix
863
+ lines.push(
864
+ ...panel(
865
+ "06 · token mix",
866
+ stackedShareBar(
867
+ [
868
+ { label: "input", value: combined.tokens.input || 0, color: AMBER },
869
+ { label: "output", value: combined.tokens.output || 0, color: RED },
870
+ { label: "cache read", value: combined.tokens.cacheRead || 0, color: EMBER },
871
+ { label: "cache write", value: combined.tokens.cacheCreation || 0, color: ORANGE },
872
+ { label: "reasoning", value: combined.tokens.reasoning || 0, color: GRAY },
873
+ ],
874
+ interior,
875
+ ),
876
+ width,
877
+ ),
878
+ );
879
+ endBlock();
880
+
881
+ // 07 model leaderboard (total tokens)
882
+ lines.push(
883
+ ...panel(
884
+ "07 · model leaderboard — total tokens",
885
+ barChart(
886
+ Object.values(combined.modelUsage)
887
+ .map((usage) => ({ label: modelLabel(usage), value: tokenTotal(usage), color: providerColor(usage.provider) }))
888
+ .sort((a, b) => b.value - a.value)
889
+ .slice(0, 16),
890
+ interior,
891
+ { valueFormatter: compactNumber, labelWidth: 32, ranked: true },
892
+ ),
893
+ width,
894
+ ),
895
+ );
896
+ endBlock();
897
+
898
+ // 08 model standing (input + output)
899
+ lines.push(
900
+ ...panel(
901
+ "08 · model standing — input + output",
902
+ [
903
+ ...barChart(
904
+ Object.values(combined.modelUsage)
905
+ .map((usage) => ({
906
+ label: modelLabel(usage),
907
+ value: (usage.input || 0) + (usage.output || 0),
908
+ color: providerColor(usage.provider),
909
+ }))
910
+ .sort((a, b) => b.value - a.value)
911
+ .slice(0, 12),
912
+ interior,
913
+ { valueFormatter: compactNumber, labelWidth: 32, ranked: true },
914
+ ),
915
+ "",
916
+ paint(SHADOW, "favorite = highest input + output token count"),
917
+ ],
918
+ width,
919
+ ),
920
+ );
921
+ endBlock();
922
+
923
+ // 09 time of day
924
+ lines.push(...panel("09 · active hours", hourHistogram(combined.hourlyActivity), width));
925
+ endBlock();
926
+
927
+ // 10 daily activity matrix
928
+ lines.push(...panel("10 · activity matrix", activityMatrix(combined.dailyActivity, interior), width));
929
+ endBlock();
930
+
931
+ return blocks;
932
+ }
933
+
934
+ // ---------------------------------------------------------------------------
935
+ // decode reveal — scramble cipher glyphs, then resolve left-to-right
936
+ // ---------------------------------------------------------------------------
937
+
938
+ const SCRAMBLE_CHARS = "01<>/\\|+=*#%&@?!;:^~";
939
+ const SCRAMBLE_TARGET = /[A-Za-z0-9]/;
940
+
941
+ function sleep(ms) {
942
+ return new Promise((resolve) => setTimeout(resolve, ms));
943
+ }
944
+
945
+ export async function animateReveal(text) {
946
+ const lines = text.split("\n");
947
+ process.stdout.write("\x1b[?25l");
948
+ try {
949
+ for (const line of lines) {
950
+ const plain = line.replace(ANSI_PATTERN, "");
951
+ if (!plain.trim()) {
952
+ process.stdout.write(`${line}\n`);
953
+ continue;
954
+ }
955
+ const FRAMES = 2;
956
+ for (let frame = 0; frame < FRAMES; frame += 1) {
957
+ const resolved = Math.floor(plain.length * (frame / FRAMES));
958
+ let scrambled = "";
959
+ for (let index = 0; index < plain.length; index += 1) {
960
+ const char = plain[index];
961
+ scrambled +=
962
+ index < resolved || !SCRAMBLE_TARGET.test(char)
963
+ ? char
964
+ : SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)];
965
+ }
966
+ process.stdout.write(`\r\x1b[2K${fg(SHADOW)}${scrambled}${reset()}`);
967
+ await sleep(5);
968
+ }
969
+ process.stdout.write(`\r\x1b[2K${line}\n`);
970
+ }
971
+ } finally {
972
+ process.stdout.write("\x1b[?25h");
973
+ }
974
+ }