tokenleak 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/tokenleak.js +250 -361
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenleak",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Visualise your AI coding-assistant token usage across providers — heatmaps, dashboards, and shareable cards.",
5
5
  "type": "module",
6
6
  "bin": {
package/tokenleak.js CHANGED
@@ -422,18 +422,29 @@ var DEFAULT_DAYS = 90;
422
422
  var DEFAULT_CONCURRENCY = 3;
423
423
  var MAX_JSONL_RECORD_BYTES = 10 * 1024 * 1024;
424
424
  var SCHEMA_VERSION = 1;
425
+ // packages/core/dist/date-utils.js
426
+ var ONE_DAY_MS = 86400000;
427
+ function dateToUtcMs(dateString) {
428
+ return new Date(dateString + "T00:00:00Z").getTime();
429
+ }
430
+ function formatDateStringUtc(date) {
431
+ return date.toISOString().slice(0, 10);
432
+ }
433
+ function compareDateStrings(a, b) {
434
+ return dateToUtcMs(a) - dateToUtcMs(b);
435
+ }
436
+
425
437
  // packages/core/dist/aggregation/streaks.js
426
438
  function calculateStreaks(daily) {
427
439
  if (daily.length === 0) {
428
440
  return { current: 0, longest: 0 };
429
441
  }
430
- const sorted = [...daily].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
431
- const ONE_DAY_MS = 86400000;
442
+ const sorted = [...daily].sort((a, b) => dateToUtcMs(a.date) - dateToUtcMs(b.date));
432
443
  let longest = 1;
433
444
  let currentRun = 1;
434
445
  for (let i = 1;i < sorted.length; i++) {
435
- const prev = new Date(sorted[i - 1].date).getTime();
436
- const curr = new Date(sorted[i].date).getTime();
446
+ const prev = dateToUtcMs(sorted[i - 1].date);
447
+ const curr = dateToUtcMs(sorted[i].date);
437
448
  const diff = curr - prev;
438
449
  if (diff === ONE_DAY_MS) {
439
450
  currentRun++;
@@ -446,8 +457,8 @@ function calculateStreaks(daily) {
446
457
  }
447
458
  let current = 1;
448
459
  for (let i = sorted.length - 1;i > 0; i--) {
449
- const curr = new Date(sorted[i].date).getTime();
450
- const prev = new Date(sorted[i - 1].date).getTime();
460
+ const curr = dateToUtcMs(sorted[i].date);
461
+ const prev = dateToUtcMs(sorted[i - 1].date);
451
462
  if (curr - prev === ONE_DAY_MS) {
452
463
  current++;
453
464
  } else {
@@ -461,13 +472,12 @@ function rollingWindow(daily, days, referenceDate) {
461
472
  if (daily.length === 0 || days <= 0) {
462
473
  return { tokens: 0, cost: 0 };
463
474
  }
464
- const refTime = new Date(referenceDate).getTime();
465
- const ONE_DAY_MS = 86400000;
475
+ const refTime = dateToUtcMs(referenceDate);
466
476
  const windowStart = refTime - (days - 1) * ONE_DAY_MS;
467
477
  let tokens = 0;
468
478
  let cost = 0;
469
479
  for (const entry of daily) {
470
- const entryTime = new Date(entry.date).getTime();
480
+ const entryTime = dateToUtcMs(entry.date);
471
481
  if (entryTime >= windowStart && entryTime <= refTime) {
472
482
  tokens += entry.totalTokens;
473
483
  cost += entry.cost;
@@ -507,7 +517,7 @@ function dayOfWeekBreakdown(daily) {
507
517
  count: 0
508
518
  }));
509
519
  for (const entry of daily) {
510
- const dayIndex = new Date(entry.date + "T00:00:00").getUTCDay();
520
+ const dayIndex = new Date(dateToUtcMs(entry.date)).getUTCDay();
511
521
  const bucket = buckets[dayIndex];
512
522
  bucket.tokens += entry.totalTokens;
513
523
  bucket.cost += entry.cost;
@@ -577,6 +587,7 @@ function topModels(daily, limit = DEFAULT_LIMIT) {
577
587
  return entries.slice(0, limit);
578
588
  }
579
589
  // packages/core/dist/aggregation/aggregate.js
590
+ var ROLLING_WINDOW_DAYS = 30;
580
591
  function aggregate(daily, referenceDate) {
581
592
  const streaks = calculateStreaks(daily);
582
593
  const rolling30 = rollingWindow(daily, 30, referenceDate);
@@ -586,20 +597,18 @@ function aggregate(daily, referenceDate) {
586
597
  const cache = cacheHitRate(daily);
587
598
  const models = topModels(daily);
588
599
  let totalTokens = 0;
600
+ let totalInputTokens = 0;
601
+ let totalOutputTokens = 0;
589
602
  let totalCost = 0;
590
603
  for (const entry of daily) {
591
604
  totalTokens += entry.totalTokens;
605
+ totalInputTokens += entry.inputTokens;
606
+ totalOutputTokens += entry.outputTokens;
592
607
  totalCost += entry.cost;
593
608
  }
609
+ const rolling30dTopModel = computeRolling30dTopModel(daily, referenceDate);
594
610
  const activeDays = daily.length;
595
- let totalDays = 0;
596
- if (daily.length > 0) {
597
- const sorted = [...daily].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
598
- const first = new Date(sorted[0].date).getTime();
599
- const last = new Date(sorted[sorted.length - 1].date).getTime();
600
- const ONE_DAY_MS = 86400000;
601
- totalDays = Math.round((last - first) / ONE_DAY_MS) + 1;
602
- }
611
+ const totalDays = computeTotalDays(daily);
603
612
  const averages = calculateAverages(daily, totalDays);
604
613
  return {
605
614
  currentStreak: streaks.current,
@@ -613,13 +622,46 @@ function aggregate(daily, referenceDate) {
613
622
  averageDailyCost: averages.cost,
614
623
  cacheHitRate: cache,
615
624
  totalTokens,
625
+ totalInputTokens,
626
+ totalOutputTokens,
616
627
  totalCost,
617
628
  totalDays,
618
629
  activeDays,
619
630
  dayOfWeek: dow,
620
- topModels: models
631
+ topModels: models,
632
+ rolling30dTopModel
621
633
  };
622
634
  }
635
+ function computeRolling30dTopModel(daily, referenceDate) {
636
+ const refTime = dateToUtcMs(referenceDate);
637
+ const windowStart = refTime - (ROLLING_WINDOW_DAYS - 1) * ONE_DAY_MS;
638
+ const modelTokensMap = new Map;
639
+ for (const entry of daily) {
640
+ const entryTime = dateToUtcMs(entry.date);
641
+ if (entryTime >= windowStart && entryTime <= refTime) {
642
+ for (const m of entry.models) {
643
+ modelTokensMap.set(m.model, (modelTokensMap.get(m.model) ?? 0) + m.totalTokens);
644
+ }
645
+ }
646
+ }
647
+ let topModel = null;
648
+ let maxTokens = 0;
649
+ for (const [model, tokens] of modelTokensMap) {
650
+ if (tokens > maxTokens) {
651
+ maxTokens = tokens;
652
+ topModel = model;
653
+ }
654
+ }
655
+ return topModel;
656
+ }
657
+ function computeTotalDays(daily) {
658
+ if (daily.length === 0)
659
+ return 0;
660
+ const sorted = [...daily].sort((a, b) => dateToUtcMs(a.date) - dateToUtcMs(b.date));
661
+ const first = dateToUtcMs(sorted[0].date);
662
+ const last = dateToUtcMs(sorted[sorted.length - 1].date);
663
+ return Math.round((last - first) / ONE_DAY_MS) + 1;
664
+ }
623
665
  // packages/core/dist/aggregation/merge.js
624
666
  function mergeProviderData(providers) {
625
667
  const dateMap = new Map;
@@ -648,7 +690,7 @@ function mergeProviderData(providers) {
648
690
  }
649
691
  }
650
692
  }
651
- return [...dateMap.values()].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
693
+ return [...dateMap.values()].sort((a, b) => compareDateStrings(a.date, b.date));
652
694
  }
653
695
  // packages/core/dist/aggregation/compare.js
654
696
  function computeDeltas(statsA, statsB) {
@@ -683,19 +725,18 @@ function parseCompareRange(rangeStr) {
683
725
  return { since, until };
684
726
  }
685
727
  function computePreviousPeriod(current) {
686
- const ONE_DAY_MS = 86400000;
687
- const sinceMs = new Date(current.since).getTime();
688
- const untilMs = new Date(current.until).getTime();
728
+ const sinceMs = dateToUtcMs(current.since);
729
+ const untilMs = dateToUtcMs(current.until);
689
730
  const periodDays = Math.round((untilMs - sinceMs) / ONE_DAY_MS);
690
731
  const prevUntil = new Date(sinceMs - ONE_DAY_MS);
691
732
  const prevSince = new Date(prevUntil.getTime() - periodDays * ONE_DAY_MS);
692
733
  return {
693
- since: prevSince.toISOString().slice(0, 10),
694
- until: prevUntil.toISOString().slice(0, 10)
734
+ since: formatDateStringUtc(prevSince),
735
+ until: formatDateStringUtc(prevUntil)
695
736
  };
696
737
  }
697
738
  // packages/core/dist/index.js
698
- var VERSION = "0.2.0";
739
+ var VERSION = "0.4.0";
699
740
 
700
741
  // packages/registry/dist/models/normalizer.js
701
742
  var DATE_SUFFIX_PATTERN = /-\d{8}$/;
@@ -915,6 +956,13 @@ async function* splitJsonlRecords(filePath) {
915
956
  import { existsSync, readdirSync, statSync } from "fs";
916
957
  import { join } from "path";
917
958
  import { homedir } from "os";
959
+
960
+ // packages/registry/dist/utils.js
961
+ function isInRange(date, range) {
962
+ return date >= range.since && date <= range.until;
963
+ }
964
+
965
+ // packages/registry/dist/providers/claude-code.js
918
966
  var DEFAULT_BASE_DIR = join(homedir(), ".claude", "projects");
919
967
  var CLAUDE_CODE_COLORS = {
920
968
  primary: "#ff6b35",
@@ -981,9 +1029,6 @@ function extractUsage(record) {
981
1029
  cacheWriteTokens
982
1030
  };
983
1031
  }
984
- function isInRange(date, range) {
985
- return date >= range.since && date <= range.until;
986
- }
987
1032
  function buildDailyUsage(records) {
988
1033
  const byDate = new Map;
989
1034
  for (const rec of records) {
@@ -1011,7 +1056,7 @@ function buildDailyUsage(records) {
1011
1056
  mb.outputTokens += rec.outputTokens;
1012
1057
  mb.cacheReadTokens += rec.cacheReadTokens;
1013
1058
  mb.cacheWriteTokens += rec.cacheWriteTokens;
1014
- mb.totalTokens += rec.inputTokens + rec.outputTokens;
1059
+ mb.totalTokens += rec.inputTokens + rec.outputTokens + rec.cacheReadTokens + rec.cacheWriteTokens;
1015
1060
  mb.cost += cost;
1016
1061
  }
1017
1062
  const daily = [];
@@ -1125,9 +1170,6 @@ function extractDate(timestamp) {
1125
1170
  const match = /^(\d{4}-\d{2}-\d{2})/.exec(timestamp);
1126
1171
  return match ? match[1] : null;
1127
1172
  }
1128
- function isInRange2(date, range) {
1129
- return date >= range.since && date <= range.until;
1130
- }
1131
1173
 
1132
1174
  class CodexProvider {
1133
1175
  name = "codex";
@@ -1161,7 +1203,7 @@ class CodexProvider {
1161
1203
  continue;
1162
1204
  }
1163
1205
  const date = extractDate(event.timestamp);
1164
- if (!date || !isInRange2(date, range)) {
1206
+ if (!date || !isInRange(date, range)) {
1165
1207
  continue;
1166
1208
  }
1167
1209
  const normalizedModel = normalizeModelName(compactModelDateSuffix(event.model));
@@ -1188,7 +1230,9 @@ class CodexProvider {
1188
1230
  const breakdown = modelMap.get(normalizedModel);
1189
1231
  breakdown.inputTokens += inputTokens;
1190
1232
  breakdown.outputTokens += outputTokens;
1191
- breakdown.totalTokens += inputTokens + outputTokens;
1233
+ breakdown.cacheReadTokens += cacheReadTokens;
1234
+ breakdown.cacheWriteTokens += cacheWriteTokens;
1235
+ breakdown.totalTokens += inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
1192
1236
  breakdown.cost += cost;
1193
1237
  }
1194
1238
  } catch {
@@ -1248,9 +1292,6 @@ function extractDate2(createdAt) {
1248
1292
  }
1249
1293
  return new Date(createdAt).toISOString().slice(0, 10);
1250
1294
  }
1251
- function isWithinRange(date, range) {
1252
- return date >= range.since && date <= range.until;
1253
- }
1254
1295
  function buildProviderData(records) {
1255
1296
  const byDate = new Map;
1256
1297
  for (const record of records) {
@@ -1280,13 +1321,15 @@ function buildProviderData(records) {
1280
1321
  let dayCost = 0;
1281
1322
  for (const [model, usage] of modelMap) {
1282
1323
  const cost = estimateCost(model, usage.inputTokens, usage.outputTokens, 0, 0);
1283
- const modelTotal = usage.inputTokens + usage.outputTokens;
1324
+ const cacheReadTokens = 0;
1325
+ const cacheWriteTokens = 0;
1326
+ const modelTotal = usage.inputTokens + usage.outputTokens + cacheReadTokens + cacheWriteTokens;
1284
1327
  models.push({
1285
1328
  model,
1286
1329
  inputTokens: usage.inputTokens,
1287
1330
  outputTokens: usage.outputTokens,
1288
- cacheReadTokens: 0,
1289
- cacheWriteTokens: 0,
1331
+ cacheReadTokens,
1332
+ cacheWriteTokens,
1290
1333
  totalTokens: modelTotal,
1291
1334
  cost
1292
1335
  });
@@ -1324,7 +1367,7 @@ function loadFromSqlite(dbPath, range) {
1324
1367
  const records = [];
1325
1368
  for (const row of rows) {
1326
1369
  const date = extractDate2(row.created_at);
1327
- if (isWithinRange(date, range)) {
1370
+ if (isInRange(date, range)) {
1328
1371
  records.push({
1329
1372
  date,
1330
1373
  model: row.model,
@@ -1352,7 +1395,7 @@ function loadFromJson(sessionsDir, range) {
1352
1395
  continue;
1353
1396
  }
1354
1397
  const date = extractDate2(msg.created_at);
1355
- if (isWithinRange(date, range)) {
1398
+ if (isInRange(date, range)) {
1356
1399
  records.push({
1357
1400
  date,
1358
1401
  model: msg.model,
@@ -1413,46 +1456,42 @@ var DARK_THEME = {
1413
1456
  muted: "#7d8590",
1414
1457
  border: "#30363d",
1415
1458
  cardBackground: "#161b22",
1416
- heatmap: ["#161b22", "#0e4429", "#006d32", "#26a641", "#39d353"],
1459
+ heatmap: ["#161b22", "#1e3a5f", "#2563eb", "#3b82f6", "#1d4ed8"],
1417
1460
  accent: "#58a6ff",
1418
1461
  accentSecondary: "#bc8cff",
1419
- barFill: "#58a6ff",
1462
+ barFill: "#3b82f6",
1420
1463
  barBackground: "#21262d"
1421
1464
  };
1422
1465
  var LIGHT_THEME = {
1423
1466
  background: "#ffffff",
1424
- foreground: "#1f2328",
1425
- muted: "#656d76",
1426
- border: "#d0d7de",
1427
- cardBackground: "#f6f8fa",
1428
- heatmap: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
1429
- accent: "#0969da",
1430
- accentSecondary: "#8250df",
1431
- barFill: "#0969da",
1432
- barBackground: "#eaeef2"
1467
+ foreground: "#1a1a2e",
1468
+ muted: "#8b8fa3",
1469
+ border: "#e5e7eb",
1470
+ cardBackground: "#f8f9fc",
1471
+ heatmap: ["#ebedf0", "#c6d4f7", "#8da4ef", "#5b6abf", "#2f3778"],
1472
+ accent: "#3b5bdb",
1473
+ accentSecondary: "#7048e8",
1474
+ barFill: "#5b6abf",
1475
+ barBackground: "#ebedf0"
1433
1476
  };
1434
1477
  function getTheme(mode) {
1435
1478
  return mode === "dark" ? DARK_THEME : LIGHT_THEME;
1436
1479
  }
1437
1480
 
1438
1481
  // packages/renderers/dist/svg/layout.js
1439
- var PADDING = 24;
1440
- var CELL_SIZE = 12;
1441
- var CELL_GAP = 3;
1442
- var HEADER_HEIGHT = 60;
1443
- var MONTH_LABEL_HEIGHT = 20;
1444
- var DAY_LABEL_WIDTH = 32;
1482
+ var PADDING = 40;
1483
+ var CELL_SIZE = 16;
1484
+ var CELL_GAP = 4;
1485
+ var MONTH_LABEL_HEIGHT = 24;
1486
+ var DAY_LABEL_WIDTH = 44;
1445
1487
  var HEATMAP_ROWS = 7;
1446
- var SECTION_GAP = 28;
1447
- var STAT_ROW_HEIGHT = 28;
1448
- var BAR_HEIGHT = 20;
1449
- var BAR_GAP = 8;
1450
- var BAR_LABEL_WIDTH = 120;
1451
- var FONT_SIZE_TITLE = 20;
1488
+ var SECTION_GAP = 32;
1489
+ var FONT_SIZE_TITLE = 28;
1452
1490
  var FONT_SIZE_SUBTITLE = 14;
1453
- var FONT_SIZE_BODY = 12;
1454
- var FONT_SIZE_SMALL = 10;
1455
- var FONT_FAMILY = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif";
1491
+ var FONT_SIZE_SMALL = 11;
1492
+ var FONT_SIZE_STAT_VALUE = 32;
1493
+ var FONT_SIZE_STAT_LABEL = 11;
1494
+ var FONT_FAMILY = "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif";
1456
1495
 
1457
1496
  // packages/renderers/dist/svg/utils.js
1458
1497
  function escapeXml(str) {
@@ -1472,28 +1511,24 @@ function group(children, transform) {
1472
1511
  }
1473
1512
  function formatNumber(n) {
1474
1513
  if (n >= 1e6) {
1475
- return `${(n / 1e6).toFixed(1)}M`;
1514
+ const millions = Number((n / 1e6).toFixed(1));
1515
+ if (millions >= 1000) {
1516
+ return `${(n / 1e9).toFixed(1)}B`;
1517
+ }
1518
+ return `${millions.toFixed(1)}M`;
1476
1519
  }
1477
1520
  if (n >= 1000) {
1478
- return `${(n / 1000).toFixed(1)}K`;
1521
+ const thousands = Number((n / 1000).toFixed(1));
1522
+ if (thousands >= 1000) {
1523
+ return `${(n / 1e6).toFixed(1)}M`;
1524
+ }
1525
+ return `${thousands.toFixed(1)}K`;
1479
1526
  }
1480
1527
  return n.toFixed(0);
1481
1528
  }
1482
- function formatCost(cost) {
1483
- if (cost >= 100) {
1484
- return `$${cost.toFixed(0)}`;
1485
- }
1486
- if (cost >= 1) {
1487
- return `$${cost.toFixed(2)}`;
1488
- }
1489
- return `$${cost.toFixed(4)}`;
1490
- }
1491
- function formatPercent(rate) {
1492
- return `${(rate * 100).toFixed(1)}%`;
1493
- }
1494
1529
 
1495
1530
  // packages/renderers/dist/svg/heatmap.js
1496
- var DAY_LABELS2 = ["", "Mon", "", "Wed", "", "Fri", ""];
1531
+ var DAY_LABELS2 = ["Mon", "", "Wed", "", "Fri", "", "Sun"];
1497
1532
  var MONTH_NAMES = [
1498
1533
  "Jan",
1499
1534
  "Feb",
@@ -1538,10 +1573,10 @@ function renderHeatmap(daily, theme, options = {}) {
1538
1573
  const dates = daily.map((d) => d.date).sort();
1539
1574
  const endStr = options.endDate ?? dates[dates.length - 1] ?? new Date().toISOString().slice(0, 10);
1540
1575
  const startStr = options.startDate ?? dates[0] ?? endStr;
1541
- const end = new Date(endStr);
1542
- const start = new Date(startStr);
1543
- const startDay = start.getDay();
1544
- start.setDate(start.getDate() - startDay);
1576
+ const end = new Date(endStr + "T00:00:00Z");
1577
+ const start = new Date(startStr + "T00:00:00Z");
1578
+ const startDay = start.getUTCDay();
1579
+ start.setUTCDate(start.getUTCDate() - startDay);
1545
1580
  const cells = [];
1546
1581
  const allTokens = Array.from(tokenMap.values());
1547
1582
  const quantiles = computeQuantiles(allTokens);
@@ -1549,19 +1584,20 @@ function renderHeatmap(daily, theme, options = {}) {
1549
1584
  let col = 0;
1550
1585
  const monthLabels = [];
1551
1586
  let lastMonth = -1;
1587
+ const cellRadius = 3;
1552
1588
  while (current <= end) {
1553
- const row = current.getDay();
1589
+ const row = current.getUTCDay();
1554
1590
  const dateStr = current.toISOString().slice(0, 10);
1555
1591
  const tokens = tokenMap.get(dateStr) ?? 0;
1556
1592
  const level = getLevel(tokens, quantiles);
1557
1593
  const x = DAY_LABEL_WIDTH + col * (CELL_SIZE + CELL_GAP);
1558
1594
  const y = MONTH_LABEL_HEIGHT + row * (CELL_SIZE + CELL_GAP);
1559
1595
  const title = `${dateStr}: ${tokens.toLocaleString()} tokens`;
1560
- cells.push(`<rect x="${x}" y="${y}" width="${CELL_SIZE}" height="${CELL_SIZE}" fill="${escapeXml(theme.heatmap[level])}" rx="2"><title>${escapeXml(title)}</title></rect>`);
1561
- const month = current.getMonth();
1596
+ cells.push(`<rect x="${x}" y="${y}" width="${CELL_SIZE}" height="${CELL_SIZE}" fill="${escapeXml(theme.heatmap[level])}" rx="${cellRadius}"><title>${escapeXml(title)}</title></rect>`);
1597
+ const month = current.getUTCMonth();
1562
1598
  if (month !== lastMonth && row === 0) {
1563
1599
  lastMonth = month;
1564
- monthLabels.push(text(x, MONTH_LABEL_HEIGHT - 6, MONTH_NAMES[month] ?? "", {
1600
+ monthLabels.push(text(x, MONTH_LABEL_HEIGHT - 8, MONTH_NAMES[month] ?? "", {
1565
1601
  fill: theme.muted,
1566
1602
  "font-size": FONT_SIZE_SMALL,
1567
1603
  "font-family": FONT_FAMILY
@@ -1570,12 +1606,12 @@ function renderHeatmap(daily, theme, options = {}) {
1570
1606
  if (row === 6) {
1571
1607
  col++;
1572
1608
  }
1573
- current.setDate(current.getDate() + 1);
1609
+ current.setUTCDate(current.getUTCDate() + 1);
1574
1610
  }
1575
1611
  const dayLabels = DAY_LABELS2.map((label, i) => {
1576
1612
  if (!label)
1577
1613
  return "";
1578
- const y = MONTH_LABEL_HEIGHT + i * (CELL_SIZE + CELL_GAP) + CELL_SIZE - 1;
1614
+ const y = MONTH_LABEL_HEIGHT + i * (CELL_SIZE + CELL_GAP) + CELL_SIZE - 2;
1579
1615
  return text(0, y, label, {
1580
1616
  fill: theme.muted,
1581
1617
  "font-size": FONT_SIZE_SMALL,
@@ -1583,291 +1619,144 @@ function renderHeatmap(daily, theme, options = {}) {
1583
1619
  });
1584
1620
  });
1585
1621
  const totalCols = col + 1;
1586
- const width = DAY_LABEL_WIDTH + totalCols * (CELL_SIZE + CELL_GAP);
1587
- const height = MONTH_LABEL_HEIGHT + HEATMAP_ROWS * (CELL_SIZE + CELL_GAP);
1588
- const svg = group([...monthLabels, ...dayLabels, ...cells]);
1589
- return { svg, width, height };
1622
+ const gridWidth = DAY_LABEL_WIDTH + totalCols * CELL_SIZE + Math.max(0, totalCols - 1) * CELL_GAP;
1623
+ const height = MONTH_LABEL_HEIGHT + HEATMAP_ROWS * CELL_SIZE + (HEATMAP_ROWS - 1) * CELL_GAP;
1624
+ const legendY = height + 16;
1625
+ const legendItems = [];
1626
+ const legendStartX = 0;
1627
+ legendItems.push(text(legendStartX, legendY + CELL_SIZE - 2, "LESS", {
1628
+ fill: theme.muted,
1629
+ "font-size": 9,
1630
+ "font-family": FONT_FAMILY,
1631
+ "font-weight": "600",
1632
+ "letter-spacing": "0.5"
1633
+ }));
1634
+ const legendBoxStart = legendStartX + 40;
1635
+ for (let i = 0;i < 5; i++) {
1636
+ legendItems.push(`<rect x="${legendBoxStart + i * (CELL_SIZE + 3)}" y="${legendY}" width="${CELL_SIZE}" height="${CELL_SIZE}" fill="${escapeXml(theme.heatmap[i])}" rx="${cellRadius}"/>`);
1637
+ }
1638
+ legendItems.push(text(legendBoxStart + 5 * (CELL_SIZE + 3) + 4, legendY + CELL_SIZE - 2, "MORE", {
1639
+ fill: theme.muted,
1640
+ "font-size": 9,
1641
+ "font-family": FONT_FAMILY,
1642
+ "font-weight": "600",
1643
+ "letter-spacing": "0.5"
1644
+ }));
1645
+ const totalHeight = legendY + CELL_SIZE + 8;
1646
+ const legendRightX = legendBoxStart + 5 * (CELL_SIZE + 3) + 4 + 40;
1647
+ const width = Math.max(gridWidth, legendRightX);
1648
+ const svg = group([...monthLabels, ...dayLabels, ...cells, ...legendItems]);
1649
+ return { svg, width, height: totalHeight };
1590
1650
  }
1591
1651
 
1592
- // packages/renderers/dist/svg/stats-panel.js
1593
- function buildStatItems(stats) {
1594
- return [
1595
- { label: "Current Streak", value: `${stats.currentStreak} days` },
1596
- { label: "Longest Streak", value: `${stats.longestStreak} days` },
1597
- { label: "Total Tokens", value: formatNumber(stats.totalTokens) },
1598
- { label: "Total Cost", value: formatCost(stats.totalCost) },
1599
- { label: "30-Day Tokens", value: formatNumber(stats.rolling30dTokens) },
1600
- { label: "30-Day Cost", value: formatCost(stats.rolling30dCost) },
1601
- { label: "Avg Daily Tokens", value: formatNumber(stats.averageDailyTokens) },
1602
- { label: "Cache Hit Rate", value: formatPercent(stats.cacheHitRate) },
1603
- { label: "Active Days", value: `${stats.activeDays} / ${stats.totalDays}` }
1604
- ];
1605
- }
1606
- function renderStatsPanel(stats, theme) {
1607
- const items = buildStatItems(stats);
1608
- const width = 280;
1609
- const children = [];
1610
- for (let i = 0;i < items.length; i++) {
1611
- const item = items[i];
1612
- if (!item)
1613
- continue;
1614
- const y = i * STAT_ROW_HEIGHT + STAT_ROW_HEIGHT;
1615
- children.push(text(0, y, item.label, {
1652
+ // packages/renderers/dist/svg/svg-renderer.js
1653
+ var MIN_SVG_WIDTH = 800;
1654
+ function renderHeaderStat(x, y, label, value, theme, align = "end") {
1655
+ const anchor = align === "end" ? "end" : "start";
1656
+ return group([
1657
+ text(x, y, label, {
1616
1658
  fill: theme.muted,
1617
- "font-size": FONT_SIZE_SMALL,
1618
- "font-family": FONT_FAMILY
1619
- }));
1620
- children.push(text(width - 8, y, item.value, {
1621
- fill: theme.foreground,
1622
- "font-size": FONT_SIZE_BODY,
1659
+ "font-size": FONT_SIZE_STAT_LABEL,
1623
1660
  "font-family": FONT_FAMILY,
1624
- "text-anchor": "end"
1625
- }));
1626
- }
1627
- const height = items.length * STAT_ROW_HEIGHT + STAT_ROW_HEIGHT;
1628
- return { svg: group(children), width, height };
1629
- }
1630
-
1631
- // packages/renderers/dist/svg/insights-panel.js
1632
- var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
1633
- function buildInsights(stats, providers) {
1634
- const items = [];
1635
- if (stats.peakDay) {
1636
- items.push({
1637
- label: "Peak Day",
1638
- value: `${stats.peakDay.date} (${formatNumber(stats.peakDay.tokens)} tokens)`
1639
- });
1640
- }
1641
- if (stats.dayOfWeek.length > 0) {
1642
- const sorted = [...stats.dayOfWeek].sort((a, b) => b.tokens - a.tokens);
1643
- const top = sorted[0];
1644
- if (top) {
1645
- items.push({
1646
- label: "Most Active Day",
1647
- value: DAY_NAMES[top.day] ?? top.label
1648
- });
1649
- }
1650
- }
1651
- if (stats.topModels.length > 0) {
1652
- const top = stats.topModels[0];
1653
- if (top) {
1654
- const pct = top.percentage < 1 ? top.percentage * 100 : top.percentage;
1655
- items.push({
1656
- label: "Top Model",
1657
- value: `${top.model} (${pct.toFixed(1)}%)`
1658
- });
1659
- }
1660
- }
1661
- if (providers.length > 0) {
1662
- const sorted = [...providers].sort((a, b) => b.totalTokens - a.totalTokens);
1663
- const top = sorted[0];
1664
- if (top) {
1665
- items.push({
1666
- label: "Top Provider",
1667
- value: `${top.displayName} (${formatNumber(top.totalTokens)} tokens)`
1668
- });
1669
- }
1670
- }
1671
- return items;
1672
- }
1673
- function renderInsightsPanel(stats, providers, theme) {
1674
- const items = buildInsights(stats, providers);
1675
- const width = 360;
1676
- const children = [];
1677
- for (let i = 0;i < items.length; i++) {
1678
- const item = items[i];
1679
- if (!item)
1680
- continue;
1681
- const y = i * STAT_ROW_HEIGHT + STAT_ROW_HEIGHT;
1682
- children.push(text(0, y, item.label, {
1683
- fill: theme.muted,
1684
- "font-size": FONT_SIZE_SMALL,
1685
- "font-family": FONT_FAMILY
1686
- }));
1687
- children.push(text(width - 8, y, item.value, {
1661
+ "font-weight": "700",
1662
+ "text-anchor": anchor,
1663
+ "letter-spacing": "1"
1664
+ }),
1665
+ text(x, y + 34, value, {
1688
1666
  fill: theme.foreground,
1689
- "font-size": FONT_SIZE_BODY,
1667
+ "font-size": FONT_SIZE_STAT_VALUE,
1690
1668
  "font-family": FONT_FAMILY,
1691
- "text-anchor": "end"
1692
- }));
1693
- }
1694
- const height = Math.max(items.length * STAT_ROW_HEIGHT + STAT_ROW_HEIGHT, STAT_ROW_HEIGHT);
1695
- return { svg: group(children), width, height };
1696
- }
1697
-
1698
- // packages/renderers/dist/svg/day-of-week-chart.js
1699
- function renderDayOfWeekChart(dayOfWeek, theme) {
1700
- const chartWidth = 300;
1701
- const barAreaWidth = chartWidth - BAR_LABEL_WIDTH;
1702
- const maxTokens = Math.max(...dayOfWeek.map((d) => d.tokens), 1);
1703
- const children = [];
1704
- for (let i = 0;i < dayOfWeek.length; i++) {
1705
- const entry = dayOfWeek[i];
1706
- if (!entry)
1707
- continue;
1708
- const y = i * (BAR_HEIGHT + BAR_GAP);
1709
- const barWidth = Math.max(entry.tokens / maxTokens * barAreaWidth, 0);
1710
- children.push(text(0, y + BAR_HEIGHT - 4, entry.label, {
1711
- fill: theme.muted,
1712
- "font-size": FONT_SIZE_SMALL,
1713
- "font-family": FONT_FAMILY
1714
- }));
1715
- children.push(rect(BAR_LABEL_WIDTH, y, barAreaWidth, BAR_HEIGHT, theme.barBackground, 3));
1716
- if (barWidth > 0) {
1717
- children.push(rect(BAR_LABEL_WIDTH, y, barWidth, BAR_HEIGHT, theme.barFill, 3));
1718
- }
1719
- children.push(text(BAR_LABEL_WIDTH + barAreaWidth + 8, y + BAR_HEIGHT - 4, formatNumber(entry.tokens), {
1720
- fill: theme.foreground,
1721
- "font-size": FONT_SIZE_SMALL,
1722
- "font-family": FONT_FAMILY
1723
- }));
1724
- }
1725
- const height = dayOfWeek.length * (BAR_HEIGHT + BAR_GAP);
1726
- const width = chartWidth + 60;
1727
- return { svg: group(children), width, height };
1669
+ "font-weight": "700",
1670
+ "text-anchor": anchor
1671
+ })
1672
+ ]);
1728
1673
  }
1729
-
1730
- // packages/renderers/dist/svg/model-chart.js
1731
- function renderModelChart(topModels2, theme) {
1732
- const chartWidth = 360;
1733
- const barAreaWidth = chartWidth - BAR_LABEL_WIDTH;
1734
- const maxTokens = Math.max(...topModels2.map((m) => m.tokens), 1);
1735
- const children = [];
1736
- for (let i = 0;i < topModels2.length; i++) {
1737
- const entry = topModels2[i];
1738
- if (!entry)
1739
- continue;
1740
- const y = i * (BAR_HEIGHT + BAR_GAP);
1741
- const barWidth = Math.max(entry.tokens / maxTokens * barAreaWidth, 0);
1742
- const label = entry.model.length > 18 ? entry.model.slice(0, 17) + "\u2026" : entry.model;
1743
- children.push(text(0, y + BAR_HEIGHT - 4, label, {
1674
+ function renderBottomStat(x, y, label, value, theme) {
1675
+ return group([
1676
+ text(x, y, label, {
1744
1677
  fill: theme.muted,
1745
- "font-size": FONT_SIZE_SMALL,
1746
- "font-family": FONT_FAMILY
1747
- }));
1748
- children.push(rect(BAR_LABEL_WIDTH, y, barAreaWidth, BAR_HEIGHT, theme.barBackground, 3));
1749
- if (barWidth > 0) {
1750
- children.push(rect(BAR_LABEL_WIDTH, y, barWidth, BAR_HEIGHT, theme.accentSecondary, 3));
1751
- }
1752
- const pct = entry.percentage < 1 ? entry.percentage * 100 : entry.percentage;
1753
- const valueStr = `${formatNumber(entry.tokens)} (${pct.toFixed(1)}%)`;
1754
- children.push(text(BAR_LABEL_WIDTH + barAreaWidth + 8, y + BAR_HEIGHT - 4, valueStr, {
1678
+ "font-size": FONT_SIZE_STAT_LABEL,
1679
+ "font-family": FONT_FAMILY,
1680
+ "font-weight": "700",
1681
+ "letter-spacing": "0.8"
1682
+ }),
1683
+ text(x, y + 32, value, {
1755
1684
  fill: theme.foreground,
1756
- "font-size": FONT_SIZE_SMALL,
1757
- "font-family": FONT_FAMILY
1758
- }));
1759
- }
1760
- const height = Math.max(topModels2.length * (BAR_HEIGHT + BAR_GAP), BAR_HEIGHT);
1761
- const width = chartWidth + 100;
1762
- return { svg: group(children), width, height };
1685
+ "font-size": 22,
1686
+ "font-family": FONT_FAMILY,
1687
+ "font-weight": "700"
1688
+ })
1689
+ ]);
1763
1690
  }
1764
1691
 
1765
- // packages/renderers/dist/svg/svg-renderer.js
1766
- var MIN_SVG_WIDTH = 520;
1767
-
1768
1692
  class SvgRenderer {
1769
1693
  format = "svg";
1770
1694
  async render(output, options) {
1771
1695
  const theme = getTheme(options.theme);
1772
- let y = PADDING;
1773
1696
  const sections = [];
1774
1697
  const sectionWidths = [];
1775
- sections.push(group([
1776
- text(PADDING, y + FONT_SIZE_TITLE + 4, "Tokenleak", {
1777
- fill: theme.foreground,
1778
- "font-size": FONT_SIZE_TITLE,
1779
- "font-family": FONT_FAMILY,
1780
- "font-weight": "bold"
1781
- }),
1782
- text(PADDING, y + FONT_SIZE_TITLE + 4 + 20, `${output.dateRange.since} \u2014 ${output.dateRange.until}`, {
1783
- fill: theme.muted,
1784
- "font-size": FONT_SIZE_SUBTITLE,
1785
- "font-family": FONT_FAMILY
1786
- })
1787
- ]));
1788
- y += HEADER_HEIGHT + SECTION_GAP;
1789
- if (output.providers.length > 0) {
1790
- const providerNames = output.providers.map((p) => p.displayName).join(" \xB7 ");
1791
- sections.push(text(PADDING, y, providerNames, {
1792
- fill: theme.accent,
1793
- "font-size": FONT_SIZE_SUBTITLE,
1794
- "font-family": FONT_FAMILY
1795
- }));
1796
- y += SECTION_GAP;
1797
- }
1698
+ let y = PADDING;
1699
+ const providerName = output.providers.length > 0 ? output.providers.map((p) => p.displayName).join(" + ") : "Tokenleak";
1700
+ sections.push(text(PADDING, y + FONT_SIZE_TITLE, providerName, {
1701
+ fill: theme.foreground,
1702
+ "font-size": FONT_SIZE_TITLE,
1703
+ "font-family": FONT_FAMILY,
1704
+ "font-weight": "700"
1705
+ }));
1706
+ sections.push(text(PADDING, y + FONT_SIZE_TITLE + 22, `${output.dateRange.since} \u2014 ${output.dateRange.until}`, {
1707
+ fill: theme.muted,
1708
+ "font-size": FONT_SIZE_SUBTITLE,
1709
+ "font-family": FONT_FAMILY
1710
+ }));
1798
1711
  const allDaily = output.providers.flatMap((p) => p.daily);
1712
+ const heatmap = renderHeatmap(allDaily, theme, {
1713
+ startDate: output.dateRange.since,
1714
+ endDate: output.dateRange.until
1715
+ });
1716
+ sectionWidths.push(heatmap.width);
1717
+ const contentWidth = Math.max(heatmap.width, MIN_SVG_WIDTH - PADDING * 2);
1718
+ const stats = output.aggregated;
1719
+ const headerStatSpacing = 180;
1720
+ const headerStatsX = PADDING + contentWidth;
1721
+ sections.push(renderHeaderStat(headerStatsX, y, "INPUT TOKENS", formatNumber(stats.totalInputTokens), theme));
1722
+ sections.push(renderHeaderStat(headerStatsX - headerStatSpacing, y, "OUTPUT TOKENS", formatNumber(stats.totalOutputTokens), theme));
1723
+ sections.push(renderHeaderStat(headerStatsX - headerStatSpacing * 2, y, "TOTAL TOKENS", formatNumber(stats.totalTokens), theme));
1724
+ y += FONT_SIZE_TITLE + 22 + SECTION_GAP;
1799
1725
  if (allDaily.length > 0) {
1800
- sections.push(text(PADDING, y, "Activity", {
1801
- fill: theme.foreground,
1802
- "font-size": FONT_SIZE_SUBTITLE,
1803
- "font-family": FONT_FAMILY,
1804
- "font-weight": "bold"
1805
- }));
1806
- y += 16;
1807
- const heatmap = renderHeatmap(allDaily, theme, {
1808
- startDate: output.dateRange.since,
1809
- endDate: output.dateRange.until
1810
- });
1811
1726
  sections.push(group([heatmap.svg], `translate(${PADDING}, ${y})`));
1812
- sectionWidths.push(heatmap.width);
1813
- y += heatmap.height + SECTION_GAP;
1814
- }
1815
- sections.push(text(PADDING, y, "Statistics", {
1816
- fill: theme.foreground,
1817
- "font-size": FONT_SIZE_SUBTITLE,
1727
+ y += heatmap.height + SECTION_GAP + 8;
1728
+ }
1729
+ sections.push(`<line x1="${PADDING}" y1="${y}" x2="${PADDING + contentWidth}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
1730
+ y += SECTION_GAP;
1731
+ const numCards = 4;
1732
+ const cardWidth = contentWidth / numCards;
1733
+ const topModel = stats.topModels.length > 0 ? stats.topModels[0] : null;
1734
+ const topModelLabel = topModel ? `${topModel.model} (${formatNumber(topModel.tokens)})` : "N/A";
1735
+ sections.push(renderBottomStat(PADDING, y, "MOST USED MODEL", topModelLabel, theme));
1736
+ const recent30Label = stats.rolling30dTopModel ? `${stats.rolling30dTopModel} (${formatNumber(stats.rolling30dTokens)})` : formatNumber(stats.rolling30dTokens);
1737
+ sections.push(renderBottomStat(PADDING + cardWidth, y, "RECENT USE (LAST 30 DAYS)", recent30Label, theme));
1738
+ sections.push(renderBottomStat(PADDING + cardWidth * 2, y, "LONGEST STREAK", `${stats.longestStreak} days`, theme));
1739
+ sections.push(renderBottomStat(PADDING + cardWidth * 3, y, "CURRENT STREAK", `${stats.currentStreak} days`, theme));
1740
+ y += 56 + SECTION_GAP;
1741
+ sections.push(`<line x1="${PADDING}" y1="${y - SECTION_GAP / 2}" x2="${PADDING + contentWidth}" y2="${y - SECTION_GAP / 2}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
1742
+ sections.push(renderBottomStat(PADDING, y, "TOTAL COST", stats.totalCost >= 100 ? `$${stats.totalCost.toFixed(0)}` : `$${stats.totalCost.toFixed(2)}`, theme));
1743
+ sections.push(renderBottomStat(PADDING + cardWidth, y, "CACHE HIT RATE", `${(stats.cacheHitRate * 100).toFixed(1)}%`, theme));
1744
+ sections.push(renderBottomStat(PADDING + cardWidth * 2, y, "ACTIVE DAYS", `${stats.activeDays} / ${stats.totalDays}`, theme));
1745
+ sections.push(renderBottomStat(PADDING + cardWidth * 3, y, "AVG DAILY TOKENS", formatNumber(stats.averageDailyTokens), theme));
1746
+ y += 56 + PADDING;
1747
+ const totalHeight = y;
1748
+ const svgWidth = Math.max(contentWidth + PADDING * 2, MIN_SVG_WIDTH);
1749
+ sections.push(text(svgWidth - PADDING, totalHeight - 16, "tokenleak", {
1750
+ fill: theme.muted,
1751
+ "font-size": 10,
1818
1752
  "font-family": FONT_FAMILY,
1819
- "font-weight": "bold"
1753
+ "text-anchor": "end",
1754
+ opacity: "0.4"
1820
1755
  }));
1821
- y += 16;
1822
- const stats = renderStatsPanel(output.aggregated, theme);
1823
- sections.push(group([stats.svg], `translate(${PADDING}, ${y})`));
1824
- sectionWidths.push(stats.width);
1825
- y += stats.height + SECTION_GAP;
1826
- if (output.aggregated.dayOfWeek.length > 0) {
1827
- sections.push(text(PADDING, y, "Day of Week", {
1828
- fill: theme.foreground,
1829
- "font-size": FONT_SIZE_SUBTITLE,
1830
- "font-family": FONT_FAMILY,
1831
- "font-weight": "bold"
1832
- }));
1833
- y += 16;
1834
- const dowChart = renderDayOfWeekChart(output.aggregated.dayOfWeek, theme);
1835
- sections.push(group([dowChart.svg], `translate(${PADDING}, ${y})`));
1836
- sectionWidths.push(dowChart.width);
1837
- y += dowChart.height + SECTION_GAP;
1838
- }
1839
- if (output.aggregated.topModels.length > 0) {
1840
- sections.push(text(PADDING, y, "Top Models", {
1841
- fill: theme.foreground,
1842
- "font-size": FONT_SIZE_SUBTITLE,
1843
- "font-family": FONT_FAMILY,
1844
- "font-weight": "bold"
1845
- }));
1846
- y += 16;
1847
- const modelChart = renderModelChart(output.aggregated.topModels, theme);
1848
- sections.push(group([modelChart.svg], `translate(${PADDING}, ${y})`));
1849
- sectionWidths.push(modelChart.width);
1850
- y += modelChart.height + SECTION_GAP;
1851
- }
1852
- if (options.showInsights) {
1853
- sections.push(text(PADDING, y, "Insights", {
1854
- fill: theme.foreground,
1855
- "font-size": FONT_SIZE_SUBTITLE,
1856
- "font-family": FONT_FAMILY,
1857
- "font-weight": "bold"
1858
- }));
1859
- y += 16;
1860
- const insights = renderInsightsPanel(output.aggregated, output.providers, theme);
1861
- sections.push(group([insights.svg], `translate(${PADDING}, ${y})`));
1862
- sectionWidths.push(insights.width);
1863
- y += insights.height + SECTION_GAP;
1864
- }
1865
- const totalHeight = y + PADDING;
1866
- const maxContentWidth = sectionWidths.length > 0 ? Math.max(...sectionWidths) : MIN_SVG_WIDTH - PADDING * 2;
1867
- const svgWidth = Math.max(maxContentWidth + PADDING * 2, MIN_SVG_WIDTH);
1868
1756
  const svgContent = [
1869
1757
  `<svg xmlns="http://www.w3.org/2000/svg" width="${svgWidth}" height="${totalHeight}" viewBox="0 0 ${svgWidth} ${totalHeight}">`,
1870
- rect(0, 0, svgWidth, totalHeight, theme.background, 8),
1758
+ `<defs><style>@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&amp;display=swap');</style></defs>`,
1759
+ rect(0, 0, svgWidth, totalHeight, theme.background, 12),
1871
1760
  ...sections,
1872
1761
  "</svg>"
1873
1762
  ].join(`
@@ -2034,7 +1923,7 @@ var BOX_TL = "\u250C";
2034
1923
  var BOX_TR = "\u2510";
2035
1924
  var BOX_BL = "\u2514";
2036
1925
  var BOX_BR = "\u2518";
2037
- var DAY_NAMES2 = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1926
+ var DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
2038
1927
  var BAR_CHAR = "\u2588";
2039
1928
  var MAX_BAR_LENGTH = 20;
2040
1929
  function formatTokens(count) {
@@ -2049,7 +1938,7 @@ function formatTokens(count) {
2049
1938
  function formatCost2(cost) {
2050
1939
  return `$${cost.toFixed(2)}`;
2051
1940
  }
2052
- function formatPercent2(rate) {
1941
+ function formatPercent(rate) {
2053
1942
  return `${(rate * 100).toFixed(1)}%`;
2054
1943
  }
2055
1944
  function divider(width) {
@@ -2089,7 +1978,7 @@ function renderStats(stats, width, noColor2) {
2089
1978
  ["7d Cost", formatCost2(stats.rolling7dCost)],
2090
1979
  ["Avg Daily Tokens", formatTokens(stats.averageDailyTokens)],
2091
1980
  ["Avg Daily Cost", formatCost2(stats.averageDailyCost)],
2092
- ["Cache Hit Rate", formatPercent2(stats.cacheHitRate)],
1981
+ ["Cache Hit Rate", formatPercent(stats.cacheHitRate)],
2093
1982
  ["Active Days", `${stats.activeDays} / ${stats.totalDays}`]
2094
1983
  ];
2095
1984
  if (stats.peakDay) {
@@ -2106,7 +1995,7 @@ function renderDayOfWeek(stats, width, noColor2) {
2106
1995
  const lines = [];
2107
1996
  const maxTokens = Math.max(...stats.dayOfWeek.map((d) => d.tokens), 0);
2108
1997
  for (const entry of stats.dayOfWeek) {
2109
- const label = DAY_NAMES2[entry.day] ?? `Day${entry.day}`;
1998
+ const label = DAY_NAMES[entry.day] ?? `Day${entry.day}`;
2110
1999
  const bar = dayBar(entry.tokens, maxTokens, noColor2);
2111
2000
  const tokenStr = formatTokens(entry.tokens);
2112
2001
  const line = ` ${label} ${bar} ${tokenStr}`;
@@ -2118,7 +2007,7 @@ function renderDayOfWeek(stats, width, noColor2) {
2118
2007
  function renderTopModels(stats, width, noColor2) {
2119
2008
  const lines = [];
2120
2009
  for (const model of stats.topModels.slice(0, 5)) {
2121
- const pct = formatPercent2(model.percentage);
2010
+ const pct = formatPercent(model.percentage);
2122
2011
  const tokens = formatTokens(model.tokens);
2123
2012
  const line = ` ${colorize(model.model, "yellow", noColor2)} ${tokens} ${pct}`;
2124
2013
  lines.push(line.length > width ? line.slice(0, width) : line);
@@ -2132,7 +2021,7 @@ function renderInsights(stats, noColor2) {
2132
2021
  insights.push(`You have a ${stats.currentStreak}-day coding streak going!`);
2133
2022
  }
2134
2023
  if (stats.cacheHitRate > 0.5) {
2135
- insights.push(`Cache hit rate is ${formatPercent2(stats.cacheHitRate)} - good cache reuse.`);
2024
+ insights.push(`Cache hit rate is ${formatPercent(stats.cacheHitRate)} - good cache reuse.`);
2136
2025
  }
2137
2026
  if (stats.cacheHitRate < 0.1 && stats.totalTokens > 0) {
2138
2027
  insights.push("Cache hit rate is low - consider enabling prompt caching.");