tokenleak 1.0.1 → 1.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.
Files changed (3) hide show
  1. package/README.md +9 -14
  2. package/package.json +1 -1
  3. package/tokenleak +2321 -487
package/tokenleak CHANGED
@@ -755,8 +755,236 @@ function computePreviousPeriod(current) {
755
755
  until: formatDateStringUtc(prevUntil)
756
756
  };
757
757
  }
758
+ // packages/core/dist/aggregation/more.js
759
+ function daysInMonth(dateString) {
760
+ const [year, month] = dateString.split("-").map(Number);
761
+ if (!year || !month) {
762
+ return 30;
763
+ }
764
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
765
+ }
766
+ function buildInputOutput(providers) {
767
+ let inputTokens = 0;
768
+ let outputTokens = 0;
769
+ for (const provider of providers) {
770
+ for (const day of provider.daily) {
771
+ inputTokens += day.inputTokens;
772
+ outputTokens += day.outputTokens;
773
+ }
774
+ }
775
+ const nonCacheTokens = inputTokens + outputTokens;
776
+ return {
777
+ inputPerOutput: outputTokens > 0 ? inputTokens / outputTokens : null,
778
+ outputPerInput: inputTokens > 0 ? outputTokens / inputTokens : null,
779
+ outputShare: nonCacheTokens > 0 ? outputTokens / nonCacheTokens : 0
780
+ };
781
+ }
782
+ function buildMonthlyBurn(providers, range) {
783
+ const monthPrefix = range.until.slice(0, 7);
784
+ const monthStart = `${monthPrefix}-01`;
785
+ const observedSince = range.since > monthStart ? range.since : monthStart;
786
+ const observedDays = Math.max(1, Math.round((Date.parse(`${range.until}T00:00:00Z`) - Date.parse(`${observedSince}T00:00:00Z`)) / 86400000) + 1);
787
+ let observedTokens = 0;
788
+ let observedCost = 0;
789
+ for (const provider of providers) {
790
+ for (const day of provider.daily) {
791
+ if (day.date >= observedSince && day.date <= range.until) {
792
+ observedTokens += day.totalTokens;
793
+ observedCost += day.cost;
794
+ }
795
+ }
796
+ }
797
+ const calendarDays = daysInMonth(range.until);
798
+ const tokensPerDay = observedTokens / observedDays;
799
+ const costPerDay = observedCost / observedDays;
800
+ return {
801
+ projectedTokens: tokensPerDay * calendarDays,
802
+ projectedCost: costPerDay * calendarDays,
803
+ observedDays,
804
+ calendarDays
805
+ };
806
+ }
807
+ function buildCacheEconomics(providers) {
808
+ let readTokens = 0;
809
+ let writeTokens = 0;
810
+ let inputTokens = 0;
811
+ for (const provider of providers) {
812
+ for (const day of provider.daily) {
813
+ readTokens += day.cacheReadTokens;
814
+ writeTokens += day.cacheWriteTokens;
815
+ inputTokens += day.inputTokens;
816
+ }
817
+ }
818
+ const readCoverage = readTokens + inputTokens > 0 ? readTokens / (readTokens + inputTokens) : 0;
819
+ return {
820
+ readTokens,
821
+ writeTokens,
822
+ readCoverage,
823
+ reuseRatio: writeTokens > 0 ? readTokens / writeTokens : null
824
+ };
825
+ }
826
+ function collectEvents(providers) {
827
+ return providers.flatMap((provider) => provider.events ?? []);
828
+ }
829
+ function buildHourOfDay(events) {
830
+ const buckets = Array.from({ length: 24 }, (_, hour) => ({
831
+ hour,
832
+ tokens: 0,
833
+ cost: 0,
834
+ count: 0
835
+ }));
836
+ for (const event of events) {
837
+ const date = new Date(event.timestamp);
838
+ if (Number.isNaN(date.getTime())) {
839
+ continue;
840
+ }
841
+ const bucket = buckets[date.getUTCHours()];
842
+ if (!bucket) {
843
+ continue;
844
+ }
845
+ bucket.tokens += event.totalTokens;
846
+ bucket.cost += event.cost;
847
+ bucket.count += 1;
848
+ }
849
+ return buckets;
850
+ }
851
+ function buildSessionMetrics(events) {
852
+ const sessions = new Map;
853
+ const projects = new Map;
854
+ for (const event of events) {
855
+ const key = event.sessionId?.trim() || `${event.provider}:${event.timestamp}`;
856
+ const timestamp = Date.parse(event.timestamp);
857
+ const safeTime = Number.isFinite(timestamp) ? timestamp : 0;
858
+ let session = sessions.get(key);
859
+ if (!session) {
860
+ session = {
861
+ label: event.projectId?.trim() || event.sessionId?.trim() || key,
862
+ tokens: 0,
863
+ cost: 0,
864
+ count: 0,
865
+ projectId: event.projectId?.trim() || undefined,
866
+ firstTimestamp: safeTime,
867
+ lastTimestamp: safeTime,
868
+ explicitDurationMs: 0,
869
+ hasExplicitDuration: false
870
+ };
871
+ sessions.set(key, session);
872
+ }
873
+ session.tokens += event.totalTokens;
874
+ session.cost += event.cost;
875
+ session.count += 1;
876
+ session.firstTimestamp = Math.min(session.firstTimestamp, safeTime);
877
+ session.lastTimestamp = Math.max(session.lastTimestamp, safeTime);
878
+ if (typeof event.durationMs === "number" && Number.isFinite(event.durationMs)) {
879
+ session.explicitDurationMs += Math.max(0, event.durationMs);
880
+ session.hasExplicitDuration = true;
881
+ }
882
+ if (event.projectId?.trim()) {
883
+ projects.set(event.projectId, (projects.get(event.projectId) ?? 0) + event.totalTokens);
884
+ }
885
+ }
886
+ const sessionEntries = [...sessions.values()];
887
+ const totalSessions = sessionEntries.length;
888
+ let totalTokens = 0;
889
+ let totalCost = 0;
890
+ let totalMessages = 0;
891
+ let durationTotal = 0;
892
+ let durationCount = 0;
893
+ let longestSession = null;
894
+ let longestSessionDuration = -1;
895
+ for (const session of sessionEntries) {
896
+ totalTokens += session.tokens;
897
+ totalCost += session.cost;
898
+ totalMessages += session.count;
899
+ const derivedDurationMs = session.hasExplicitDuration ? session.explicitDurationMs : session.lastTimestamp > session.firstTimestamp ? session.lastTimestamp - session.firstTimestamp : 0;
900
+ if (derivedDurationMs > 0) {
901
+ durationTotal += derivedDurationMs;
902
+ durationCount += 1;
903
+ }
904
+ if (derivedDurationMs > longestSessionDuration || derivedDurationMs === longestSessionDuration && (!longestSession || session.tokens > longestSession.tokens)) {
905
+ longestSessionDuration = derivedDurationMs;
906
+ longestSession = {
907
+ label: session.label,
908
+ tokens: session.tokens,
909
+ cost: session.cost,
910
+ count: session.count,
911
+ durationMs: derivedDurationMs > 0 ? derivedDurationMs : null
912
+ };
913
+ }
914
+ }
915
+ let topProject = null;
916
+ for (const [name, tokens] of projects) {
917
+ if (!topProject || tokens > topProject.tokens) {
918
+ topProject = { name, tokens };
919
+ }
920
+ }
921
+ return {
922
+ totalSessions,
923
+ averageTokens: totalSessions > 0 ? totalTokens / totalSessions : 0,
924
+ averageCost: totalSessions > 0 ? totalCost / totalSessions : 0,
925
+ averageMessages: totalSessions > 0 ? totalMessages / totalSessions : 0,
926
+ averageDurationMs: durationCount > 0 ? durationTotal / durationCount : null,
927
+ longestSession,
928
+ projectCount: projects.size,
929
+ topProject
930
+ };
931
+ }
932
+ function computeModelMixShift(currentProviders, previousProviders, limit = 5) {
933
+ const currentModelTokens = new Map;
934
+ const previousModelTokens = new Map;
935
+ let currentTotal = 0;
936
+ let previousTotal = 0;
937
+ for (const provider of currentProviders) {
938
+ for (const day of provider.daily) {
939
+ for (const model of day.models) {
940
+ currentModelTokens.set(model.model, (currentModelTokens.get(model.model) ?? 0) + model.totalTokens);
941
+ currentTotal += model.totalTokens;
942
+ }
943
+ }
944
+ }
945
+ for (const provider of previousProviders) {
946
+ for (const day of provider.daily) {
947
+ for (const model of day.models) {
948
+ previousModelTokens.set(model.model, (previousModelTokens.get(model.model) ?? 0) + model.totalTokens);
949
+ previousTotal += model.totalTokens;
950
+ }
951
+ }
952
+ }
953
+ const models = new Set([
954
+ ...currentModelTokens.keys(),
955
+ ...previousModelTokens.keys()
956
+ ]);
957
+ return [...models].map((model) => {
958
+ const currentTokens = currentModelTokens.get(model) ?? 0;
959
+ const previousTokens = previousModelTokens.get(model) ?? 0;
960
+ const currentShare = currentTotal > 0 ? currentTokens / currentTotal : 0;
961
+ const previousShare = previousTotal > 0 ? previousTokens / previousTotal : 0;
962
+ return {
963
+ model,
964
+ currentShare,
965
+ previousShare,
966
+ deltaShare: currentShare - previousShare,
967
+ currentTokens,
968
+ previousTokens
969
+ };
970
+ }).sort((a, b) => Math.abs(b.deltaShare) - Math.abs(a.deltaShare)).slice(0, limit);
971
+ }
972
+ function buildMoreStats(providers, range, compare = null) {
973
+ const events = collectEvents(providers);
974
+ return {
975
+ inputOutput: buildInputOutput(providers),
976
+ monthlyBurn: buildMonthlyBurn(providers, range),
977
+ cacheEconomics: buildCacheEconomics(providers),
978
+ hourOfDay: buildHourOfDay(events),
979
+ sessionMetrics: buildSessionMetrics(events),
980
+ compare: compare ? {
981
+ previousRange: compare.previousRange,
982
+ modelMixShift: computeModelMixShift(providers, compare.previousProviders)
983
+ } : null
984
+ };
985
+ }
758
986
  // packages/core/dist/index.js
759
- var VERSION = "1.0.1";
987
+ var VERSION = "1.0.2";
760
988
 
761
989
  // packages/registry/dist/models/normalizer.js
762
990
  var DATE_SUFFIX_PATTERN = /-\d{8}$/;
@@ -1022,7 +1250,7 @@ async function* splitJsonlRecords(filePath) {
1022
1250
  }
1023
1251
  // packages/registry/dist/providers/claude-code.js
1024
1252
  import { existsSync, readdirSync, statSync } from "fs";
1025
- import { join } from "path";
1253
+ import { dirname, join, relative, sep } from "path";
1026
1254
  import { homedir } from "os";
1027
1255
 
1028
1256
  // packages/registry/dist/utils.js
@@ -1101,6 +1329,7 @@ function extractUsage(record) {
1101
1329
  }
1102
1330
  return {
1103
1331
  date,
1332
+ timestamp,
1104
1333
  model,
1105
1334
  inputTokens,
1106
1335
  outputTokens,
@@ -1181,13 +1410,18 @@ class ClaudeCodeProvider {
1181
1410
  async load(range) {
1182
1411
  const files = collectJsonlFiles(this.baseDir);
1183
1412
  const allRecords = [];
1413
+ const allEvents = [];
1184
1414
  for (const file of files) {
1185
1415
  const latestRecordsByMessageId = new Map;
1186
1416
  const anonymousRecords = [];
1417
+ const relativeFile = relative(this.baseDir, file).split(sep).join("/");
1418
+ const projectId = relative(this.baseDir, dirname(file)).split(sep).join("/");
1187
1419
  try {
1188
1420
  for await (const record of splitJsonlRecords(file)) {
1189
1421
  const usage = extractUsage(record);
1190
1422
  if (usage !== null && isInRange(usage.date, range)) {
1423
+ usage.sessionId = relativeFile;
1424
+ usage.projectId = projectId;
1191
1425
  if (usage.messageId) {
1192
1426
  latestRecordsByMessageId.set(usage.messageId, usage);
1193
1427
  } else {
@@ -1201,6 +1435,24 @@ class ClaudeCodeProvider {
1201
1435
  allRecords.push(...latestRecordsByMessageId.values(), ...anonymousRecords);
1202
1436
  }
1203
1437
  const daily = buildDailyUsage(allRecords);
1438
+ for (const record of allRecords) {
1439
+ const normalizedModel = normalizeModelName(record.model);
1440
+ const cost = estimateCost(record.model, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens);
1441
+ allEvents.push({
1442
+ provider: this.name,
1443
+ timestamp: record.timestamp,
1444
+ date: record.date,
1445
+ model: normalizedModel,
1446
+ inputTokens: record.inputTokens,
1447
+ outputTokens: record.outputTokens,
1448
+ cacheReadTokens: record.cacheReadTokens,
1449
+ cacheWriteTokens: record.cacheWriteTokens,
1450
+ totalTokens: record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens,
1451
+ cost,
1452
+ sessionId: record.sessionId,
1453
+ projectId: record.projectId
1454
+ });
1455
+ }
1204
1456
  const totalTokens = daily.reduce((sum, d) => sum + d.totalTokens, 0);
1205
1457
  const totalCost = daily.reduce((sum, d) => sum + d.cost, 0);
1206
1458
  return {
@@ -1209,13 +1461,14 @@ class ClaudeCodeProvider {
1209
1461
  daily,
1210
1462
  totalTokens,
1211
1463
  totalCost,
1212
- colors: this.colors
1464
+ colors: this.colors,
1465
+ events: allEvents
1213
1466
  };
1214
1467
  }
1215
1468
  }
1216
1469
  // packages/registry/dist/providers/codex.js
1217
1470
  import { existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1218
- import { join as join2 } from "path";
1471
+ import { dirname as dirname2, join as join2, relative as relative2, sep as sep2 } from "path";
1219
1472
  import { homedir as homedir2 } from "os";
1220
1473
  var CODEX_COLORS = {
1221
1474
  primary: "#10a37f",
@@ -1373,6 +1626,7 @@ function parseTokenCountUsage(record, context) {
1373
1626
  const inputTokens = Math.max(0, usage.inputTokens - cacheReadTokens);
1374
1627
  return {
1375
1628
  date,
1629
+ timestamp,
1376
1630
  model: context.model,
1377
1631
  inputTokens,
1378
1632
  outputTokens: usage.outputTokens,
@@ -1403,6 +1657,7 @@ function parseUsageRecord(record, context) {
1403
1657
  }
1404
1658
  return {
1405
1659
  date,
1660
+ timestamp: legacyEvent.timestamp,
1406
1661
  model: compactModelDateSuffix(legacyEvent.model),
1407
1662
  inputTokens: legacyEvent.usage.input_tokens,
1408
1663
  outputTokens: legacyEvent.usage.output_tokens,
@@ -1429,11 +1684,14 @@ class CodexProvider {
1429
1684
  async load(range) {
1430
1685
  const dailyMap = new Map;
1431
1686
  const files = collectJsonlFiles2(this.sessionsDir);
1687
+ const events = [];
1432
1688
  for (const file of files) {
1433
1689
  const context = {
1434
1690
  model: "gpt-5",
1435
1691
  previousTotals: null
1436
1692
  };
1693
+ const relativeFile = relative2(this.sessionsDir, file).split(sep2).join("/");
1694
+ const projectDir = relative2(this.sessionsDir, dirname2(file)).split(sep2).join("/");
1437
1695
  try {
1438
1696
  for await (const record of splitJsonlRecords(file)) {
1439
1697
  const usage = parseUsageRecord(record, context);
@@ -1443,12 +1701,28 @@ class CodexProvider {
1443
1701
  if (!isInRange(usage.date, range)) {
1444
1702
  continue;
1445
1703
  }
1704
+ usage.sessionId = relativeFile;
1705
+ usage.projectId = projectDir === "." ? undefined : projectDir;
1446
1706
  const normalizedModel = normalizeModelName(compactModelDateSuffix(usage.model));
1447
1707
  const inputTokens = usage.inputTokens;
1448
1708
  const outputTokens = usage.outputTokens;
1449
1709
  const cacheReadTokens = usage.cacheReadTokens;
1450
1710
  const cacheWriteTokens = usage.cacheWriteTokens;
1451
1711
  const cost = estimateCost(normalizedModel, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
1712
+ events.push({
1713
+ provider: this.name,
1714
+ timestamp: usage.timestamp,
1715
+ date: usage.date,
1716
+ model: normalizedModel,
1717
+ inputTokens,
1718
+ outputTokens,
1719
+ cacheReadTokens,
1720
+ cacheWriteTokens,
1721
+ totalTokens: inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens,
1722
+ cost,
1723
+ sessionId: usage.sessionId,
1724
+ projectId: usage.projectId
1725
+ });
1452
1726
  if (!dailyMap.has(usage.date)) {
1453
1727
  dailyMap.set(usage.date, new Map);
1454
1728
  }
@@ -1503,7 +1777,8 @@ class CodexProvider {
1503
1777
  daily,
1504
1778
  totalTokens,
1505
1779
  totalCost,
1506
- colors: this.colors
1780
+ colors: this.colors,
1781
+ events
1507
1782
  };
1508
1783
  }
1509
1784
  }
@@ -1513,7 +1788,7 @@ import { join as join3 } from "path";
1513
1788
  import { homedir as homedir3 } from "os";
1514
1789
  import { Database } from "bun:sqlite";
1515
1790
  var PROVIDER_NAME = "open-code";
1516
- var DISPLAY_NAME = "Open Code";
1791
+ var DISPLAY_NAME = "OpenCode";
1517
1792
  var COLORS = {
1518
1793
  primary: "#6366f1",
1519
1794
  secondary: "#a78bfa",
@@ -1549,12 +1824,46 @@ function extractDate2(createdAt) {
1549
1824
  }
1550
1825
  return date.toISOString().slice(0, 10);
1551
1826
  }
1827
+ function toTimestampMillis(createdAt) {
1828
+ const timestamp = typeof createdAt === "number" ? createdAt : Number.isNaN(Number(createdAt)) ? Date.parse(createdAt) : Number(createdAt);
1829
+ if (!Number.isFinite(timestamp)) {
1830
+ return null;
1831
+ }
1832
+ const millis = Math.abs(timestamp) >= 1000000000000 ? timestamp : timestamp * 1000;
1833
+ return Number.isFinite(millis) ? millis : null;
1834
+ }
1835
+ function toIsoTimestamp(createdAt) {
1836
+ const millis = toTimestampMillis(createdAt);
1837
+ if (millis === null) {
1838
+ return null;
1839
+ }
1840
+ const date = new Date(millis);
1841
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
1842
+ }
1552
1843
  function getRecordCost(record) {
1553
1844
  if (typeof record.explicitCost === "number" && Number.isFinite(record.explicitCost)) {
1554
1845
  return record.explicitCost;
1555
1846
  }
1556
1847
  return estimateCost(record.model, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens);
1557
1848
  }
1849
+ function toUsageEvent(record) {
1850
+ const totalTokens = record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens;
1851
+ return {
1852
+ provider: PROVIDER_NAME,
1853
+ timestamp: record.timestamp,
1854
+ date: record.date,
1855
+ model: normalizeModelName(record.model),
1856
+ inputTokens: record.inputTokens,
1857
+ outputTokens: record.outputTokens,
1858
+ cacheReadTokens: record.cacheReadTokens,
1859
+ cacheWriteTokens: record.cacheWriteTokens,
1860
+ totalTokens,
1861
+ cost: getRecordCost(record),
1862
+ sessionId: record.sessionId,
1863
+ projectId: record.projectId,
1864
+ durationMs: record.durationMs
1865
+ };
1866
+ }
1558
1867
  function buildProviderData(records) {
1559
1868
  const byDate = new Map;
1560
1869
  for (const record of records) {
@@ -1614,7 +1923,8 @@ function buildProviderData(records) {
1614
1923
  daily,
1615
1924
  totalTokens,
1616
1925
  totalCost,
1617
- colors: COLORS
1926
+ colors: COLORS,
1927
+ events: records.map(toUsageEvent)
1618
1928
  };
1619
1929
  }
1620
1930
  function loadFromSqlite(dbPath, range) {
@@ -1629,18 +1939,21 @@ function loadFromSqlite(dbPath, range) {
1629
1939
  if (tables.length === 0) {
1630
1940
  return [];
1631
1941
  }
1632
- const rows = db.query("SELECT model, input_tokens, output_tokens, created_at FROM messages WHERE role = 'assistant'").all();
1942
+ const rows = db.query("SELECT model, session_id, input_tokens, output_tokens, created_at FROM messages WHERE role = 'assistant'").all();
1633
1943
  const records = [];
1634
1944
  for (const row of rows) {
1635
1945
  const date = extractDate2(row.created_at);
1636
- if (date && isInRange(date, range)) {
1946
+ const timestamp = toIsoTimestamp(row.created_at);
1947
+ if (date && timestamp && isInRange(date, range)) {
1637
1948
  records.push({
1638
1949
  date,
1950
+ timestamp,
1639
1951
  model: row.model,
1640
1952
  inputTokens: row.input_tokens,
1641
1953
  outputTokens: row.output_tokens,
1642
1954
  cacheReadTokens: 0,
1643
- cacheWriteTokens: 0
1955
+ cacheWriteTokens: 0,
1956
+ sessionId: row.session_id
1644
1957
  });
1645
1958
  }
1646
1959
  }
@@ -1666,14 +1979,17 @@ function loadFromLegacyJson(sessionsDir, range) {
1666
1979
  continue;
1667
1980
  }
1668
1981
  const date = extractDate2(msg.created_at);
1669
- if (date && isInRange(date, range)) {
1982
+ const timestamp = toIsoTimestamp(msg.created_at);
1983
+ if (date && timestamp && isInRange(date, range)) {
1670
1984
  records.push({
1671
1985
  date,
1986
+ timestamp,
1672
1987
  model: msg.model,
1673
1988
  inputTokens: msg.usage.input_tokens,
1674
1989
  outputTokens: msg.usage.output_tokens,
1675
1990
  cacheReadTokens: 0,
1676
- cacheWriteTokens: 0
1991
+ cacheWriteTokens: 0,
1992
+ sessionId: file
1677
1993
  });
1678
1994
  }
1679
1995
  }
@@ -1711,7 +2027,8 @@ function loadFromCurrentStorage(baseDir, range) {
1711
2027
  continue;
1712
2028
  }
1713
2029
  const date = extractDate2(createdAt);
1714
- if (!date || !isInRange(date, range)) {
2030
+ const timestamp = toIsoTimestamp(createdAt);
2031
+ if (!date || !timestamp || !isInRange(date, range)) {
1715
2032
  continue;
1716
2033
  }
1717
2034
  const inputTokens = typeof message.tokens?.input === "number" ? message.tokens.input : 0;
@@ -1720,13 +2037,21 @@ function loadFromCurrentStorage(baseDir, range) {
1720
2037
  const cacheWriteTokens = typeof message.tokens?.cache?.write === "number" ? message.tokens.cache.write : 0;
1721
2038
  const record = {
1722
2039
  date,
2040
+ timestamp,
1723
2041
  model,
1724
2042
  inputTokens,
1725
2043
  outputTokens,
1726
2044
  cacheReadTokens,
1727
2045
  cacheWriteTokens,
1728
- explicitCost: typeof message.cost === "number" ? message.cost : undefined
2046
+ explicitCost: typeof message.cost === "number" ? message.cost : undefined,
2047
+ sessionId: typeof message.sessionID === "string" && message.sessionID || sessionDir
1729
2048
  };
2049
+ const completedAt = message.time?.completed;
2050
+ const completedMs = typeof completedAt === "string" || typeof completedAt === "number" ? toTimestampMillis(completedAt) : null;
2051
+ const createdMs = Date.parse(record.timestamp);
2052
+ if (completedMs !== null && Number.isFinite(createdMs) && completedMs > createdMs) {
2053
+ record.durationMs = completedMs - createdMs;
2054
+ }
1730
2055
  const totalTokens = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
1731
2056
  if (totalTokens === 0 && !(typeof record.explicitCost === "number" && record.explicitCost > 0)) {
1732
2057
  continue;
@@ -1792,66 +2117,10 @@ class JsonRenderer {
1792
2117
  return JSON.stringify(output, null, 2);
1793
2118
  }
1794
2119
  }
1795
- // packages/renderers/dist/svg/theme.js
1796
- var DARK_THEME = {
1797
- background: "#0d1117",
1798
- foreground: "#e6edf3",
1799
- muted: "#7d8590",
1800
- border: "#30363d",
1801
- cardBackground: "#161b22",
1802
- heatmap: ["#161b22", "#1e3a5f", "#2563eb", "#3b82f6", "#1d4ed8"],
1803
- accent: "#58a6ff",
1804
- accentSecondary: "#bc8cff",
1805
- barFill: "#3b82f6",
1806
- barBackground: "#21262d"
1807
- };
1808
- var LIGHT_THEME = {
1809
- background: "#ffffff",
1810
- foreground: "#1a1a2e",
1811
- muted: "#8b8fa3",
1812
- border: "#e5e7eb",
1813
- cardBackground: "#f8f9fc",
1814
- heatmap: ["#ebedf0", "#c6d4f7", "#8da4ef", "#5b6abf", "#2f3778"],
1815
- accent: "#3b5bdb",
1816
- accentSecondary: "#7048e8",
1817
- barFill: "#5b6abf",
1818
- barBackground: "#ebedf0"
1819
- };
1820
- function getTheme(mode) {
1821
- return mode === "dark" ? DARK_THEME : LIGHT_THEME;
1822
- }
1823
-
1824
- // packages/renderers/dist/svg/layout.js
1825
- var PADDING = 40;
1826
- var CELL_SIZE = 16;
1827
- var CELL_GAP = 4;
1828
- var MONTH_LABEL_HEIGHT = 24;
1829
- var DAY_LABEL_WIDTH = 44;
1830
- var HEATMAP_ROWS = 7;
1831
- var SECTION_GAP = 32;
1832
- var FONT_SIZE_TITLE = 28;
1833
- var FONT_SIZE_SUBTITLE = 14;
1834
- var FONT_SIZE_SMALL = 11;
1835
- var FONT_SIZE_STAT_VALUE = 32;
1836
- var FONT_SIZE_STAT_LABEL = 11;
1837
- var FONT_FAMILY = "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif";
1838
-
1839
2120
  // packages/renderers/dist/svg/utils.js
1840
2121
  function escapeXml(str) {
1841
2122
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1842
2123
  }
1843
- function rect(x, y, w, h, fill, rx) {
1844
- const rxAttr = rx !== undefined ? ` rx="${rx}"` : "";
1845
- return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${escapeXml(fill)}"${rxAttr}/>`;
1846
- }
1847
- function text(x, y, content, attrs) {
1848
- const attrStr = attrs ? Object.entries(attrs).map(([k, v]) => ` ${k}="${typeof v === "string" ? escapeXml(v) : v}"`).join("") : "";
1849
- return `<text x="${x}" y="${y}"${attrStr}>${escapeXml(content)}</text>`;
1850
- }
1851
- function group(children, transform) {
1852
- const transformAttr = transform ? ` transform="${escapeXml(transform)}"` : "";
1853
- return `<g${transformAttr}>${children.join("")}</g>`;
1854
- }
1855
2124
  function formatNumber(n) {
1856
2125
  if (n >= 1e6) {
1857
2126
  const millions = Number((n / 1e6).toFixed(1));
@@ -1879,282 +2148,45 @@ function formatCost(cost) {
1879
2148
  return `$${cost.toFixed(4)}`;
1880
2149
  }
1881
2150
 
1882
- // packages/renderers/dist/svg/heatmap.js
1883
- var DAY_LABELS2 = ["Mon", "", "Wed", "", "Fri", "", "Sun"];
1884
- var MONTH_NAMES = [
1885
- "Jan",
1886
- "Feb",
1887
- "Mar",
1888
- "Apr",
1889
- "May",
1890
- "Jun",
1891
- "Jul",
1892
- "Aug",
1893
- "Sep",
1894
- "Oct",
1895
- "Nov",
1896
- "Dec"
1897
- ];
1898
- function getLevel(tokens, quantiles) {
1899
- if (tokens <= 0)
1900
- return 0;
1901
- if (tokens <= quantiles[0])
1902
- return 1;
1903
- if (tokens <= quantiles[1])
1904
- return 2;
1905
- if (tokens <= quantiles[2])
1906
- return 3;
1907
- return 4;
1908
- }
1909
- function computeQuantiles(values) {
1910
- const nonZero = values.filter((v) => v > 0).sort((a, b) => a - b);
1911
- if (nonZero.length === 0)
1912
- return [0, 0, 0];
1913
- const q = (p) => {
1914
- const idx = Math.floor(p * (nonZero.length - 1));
1915
- return nonZero[idx] ?? 0;
1916
- };
1917
- return [q(0.25), q(0.5), q(0.75)];
1918
- }
1919
- function renderHeatmap(daily, theme, options = {}) {
1920
- const tokenMap = new Map;
1921
- for (const d of daily) {
1922
- const existing = tokenMap.get(d.date) ?? 0;
1923
- tokenMap.set(d.date, existing + d.totalTokens);
1924
- }
1925
- const dates = daily.map((d) => d.date).sort();
1926
- const endStr = options.endDate ?? dates[dates.length - 1] ?? new Date().toISOString().slice(0, 10);
1927
- const startStr = options.startDate ?? dates[0] ?? endStr;
1928
- const end = new Date(endStr + "T00:00:00Z");
1929
- const start = new Date(startStr + "T00:00:00Z");
1930
- const startDay = start.getUTCDay();
1931
- start.setUTCDate(start.getUTCDate() - startDay);
1932
- const cells = [];
1933
- const allTokens = Array.from(tokenMap.values());
1934
- const quantiles = computeQuantiles(allTokens);
1935
- const current = new Date(start);
1936
- let col = 0;
1937
- const monthLabels = [];
1938
- let lastMonth = -1;
1939
- const cellRadius = 3;
1940
- while (current <= end) {
1941
- const row = current.getUTCDay();
1942
- const dateStr = current.toISOString().slice(0, 10);
1943
- const tokens = tokenMap.get(dateStr) ?? 0;
1944
- const level = getLevel(tokens, quantiles);
1945
- const x = DAY_LABEL_WIDTH + col * (CELL_SIZE + CELL_GAP);
1946
- const y = MONTH_LABEL_HEIGHT + row * (CELL_SIZE + CELL_GAP);
1947
- const title = `${dateStr}: ${tokens.toLocaleString()} tokens`;
1948
- 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>`);
1949
- const month = current.getUTCMonth();
1950
- if (month !== lastMonth && row === 0) {
1951
- lastMonth = month;
1952
- monthLabels.push(text(x, MONTH_LABEL_HEIGHT - 8, MONTH_NAMES[month] ?? "", {
1953
- fill: theme.muted,
1954
- "font-size": FONT_SIZE_SMALL,
1955
- "font-family": FONT_FAMILY
1956
- }));
1957
- }
1958
- if (row === 6) {
1959
- col++;
1960
- }
1961
- current.setUTCDate(current.getUTCDate() + 1);
1962
- }
1963
- const dayLabels = DAY_LABELS2.map((label, i) => {
1964
- if (!label)
1965
- return "";
1966
- const y = MONTH_LABEL_HEIGHT + i * (CELL_SIZE + CELL_GAP) + CELL_SIZE - 2;
1967
- return text(0, y, label, {
1968
- fill: theme.muted,
1969
- "font-size": FONT_SIZE_SMALL,
1970
- "font-family": FONT_FAMILY
1971
- });
1972
- });
1973
- const totalCols = col + 1;
1974
- const gridWidth = DAY_LABEL_WIDTH + totalCols * CELL_SIZE + Math.max(0, totalCols - 1) * CELL_GAP;
1975
- const height = MONTH_LABEL_HEIGHT + HEATMAP_ROWS * CELL_SIZE + (HEATMAP_ROWS - 1) * CELL_GAP;
1976
- const legendY = height + 16;
1977
- const legendItems = [];
1978
- const legendStartX = 0;
1979
- legendItems.push(text(legendStartX, legendY + CELL_SIZE - 2, "LESS", {
1980
- fill: theme.muted,
1981
- "font-size": 9,
1982
- "font-family": FONT_FAMILY,
1983
- "font-weight": "600",
1984
- "letter-spacing": "0.5"
1985
- }));
1986
- const legendBoxStart = legendStartX + 40;
1987
- for (let i = 0;i < 5; i++) {
1988
- legendItems.push(`<rect x="${legendBoxStart + i * (CELL_SIZE + 3)}" y="${legendY}" width="${CELL_SIZE}" height="${CELL_SIZE}" fill="${escapeXml(theme.heatmap[i])}" rx="${cellRadius}"/>`);
1989
- }
1990
- legendItems.push(text(legendBoxStart + 5 * (CELL_SIZE + 3) + 4, legendY + CELL_SIZE - 2, "MORE", {
1991
- fill: theme.muted,
1992
- "font-size": 9,
1993
- "font-family": FONT_FAMILY,
1994
- "font-weight": "600",
1995
- "letter-spacing": "0.5"
1996
- }));
1997
- const totalHeight = legendY + CELL_SIZE + 8;
1998
- const legendRightX = legendBoxStart + 5 * (CELL_SIZE + 3) + 4 + 40;
1999
- const width = Math.max(gridWidth, legendRightX);
2000
- const svg = group([...monthLabels, ...dayLabels, ...cells, ...legendItems]);
2001
- return { svg, width, height: totalHeight };
2002
- }
2003
-
2004
- // packages/renderers/dist/svg/svg-renderer.js
2005
- var MIN_SVG_WIDTH = 1000;
2006
- var MAX_STAT_VALUE_CHARS = 28;
2007
- function truncateText(value, maxChars) {
2008
- if (value.length <= maxChars)
2009
- return value;
2010
- return value.slice(0, maxChars - 1) + "\u2026";
2011
- }
2012
- function renderHeaderStat(x, y, label, value, theme, align = "end") {
2013
- const anchor = align === "end" ? "end" : "start";
2014
- return group([
2015
- text(x, y, label, {
2016
- fill: theme.muted,
2017
- "font-size": FONT_SIZE_STAT_LABEL,
2018
- "font-family": FONT_FAMILY,
2019
- "font-weight": "700",
2020
- "text-anchor": anchor,
2021
- "letter-spacing": "1"
2022
- }),
2023
- text(x, y + 34, value, {
2024
- fill: theme.foreground,
2025
- "font-size": FONT_SIZE_STAT_VALUE,
2026
- "font-family": FONT_FAMILY,
2027
- "font-weight": "700",
2028
- "text-anchor": anchor
2029
- })
2030
- ]);
2031
- }
2032
- function renderBottomStat(x, y, label, value, theme) {
2033
- return group([
2034
- text(x, y, label, {
2035
- fill: theme.muted,
2036
- "font-size": FONT_SIZE_STAT_LABEL,
2037
- "font-family": FONT_FAMILY,
2038
- "font-weight": "700",
2039
- "letter-spacing": "0.8"
2040
- }),
2041
- text(x, y + 32, value, {
2042
- fill: theme.foreground,
2043
- "font-size": 18,
2044
- "font-family": FONT_FAMILY,
2045
- "font-weight": "700"
2046
- })
2047
- ]);
2048
- }
2049
-
2050
- class SvgRenderer {
2051
- format = "svg";
2052
- async render(output, options) {
2053
- const theme = getTheme(options.theme);
2054
- const sections = [];
2055
- const sectionWidths = [];
2056
- let y = PADDING;
2057
- const providerName = output.providers.length > 0 ? output.providers.map((p) => p.displayName).join(" + ") : "Tokenleak";
2058
- sections.push(text(PADDING, y + FONT_SIZE_TITLE, providerName, {
2059
- fill: theme.foreground,
2060
- "font-size": FONT_SIZE_TITLE,
2061
- "font-family": FONT_FAMILY,
2062
- "font-weight": "700"
2063
- }));
2064
- sections.push(text(PADDING, y + FONT_SIZE_TITLE + 22, `${output.dateRange.since} \u2014 ${output.dateRange.until}`, {
2065
- fill: theme.muted,
2066
- "font-size": FONT_SIZE_SUBTITLE,
2067
- "font-family": FONT_FAMILY
2068
- }));
2069
- const allDaily = output.providers.flatMap((p) => p.daily);
2070
- const heatmap = renderHeatmap(allDaily, theme, {
2071
- startDate: output.dateRange.since,
2072
- endDate: output.dateRange.until
2073
- });
2074
- sectionWidths.push(heatmap.width);
2075
- const contentWidth = Math.max(heatmap.width, MIN_SVG_WIDTH - PADDING * 2);
2076
- const stats = output.aggregated;
2077
- const headerStatSpacing = 180;
2078
- const headerStatsX = PADDING + contentWidth;
2079
- sections.push(renderHeaderStat(headerStatsX, y, "INPUT TOKENS", formatNumber(stats.totalInputTokens), theme));
2080
- sections.push(renderHeaderStat(headerStatsX - headerStatSpacing, y, "OUTPUT TOKENS", formatNumber(stats.totalOutputTokens), theme));
2081
- sections.push(renderHeaderStat(headerStatsX - headerStatSpacing * 2, y, "TOTAL TOKENS", formatNumber(stats.totalTokens), theme));
2082
- y += FONT_SIZE_TITLE + 22 + SECTION_GAP;
2083
- if (allDaily.length > 0) {
2084
- sections.push(group([heatmap.svg], `translate(${PADDING}, ${y})`));
2085
- y += heatmap.height + SECTION_GAP + 8;
2086
- }
2087
- sections.push(`<line x1="${PADDING}" y1="${y}" x2="${PADDING + contentWidth}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
2088
- y += SECTION_GAP;
2089
- const col1Width = contentWidth * 0.35;
2090
- const col2Width = contentWidth * 0.35;
2091
- const col3Width = contentWidth * 0.15;
2092
- const col4Width = contentWidth * 0.15;
2093
- const topModel = stats.topModels.length > 0 ? stats.topModels[0] : null;
2094
- const topModelLabel = topModel ? truncateText(`${topModel.model} (${formatNumber(topModel.tokens)})`, MAX_STAT_VALUE_CHARS) : "N/A";
2095
- sections.push(renderBottomStat(PADDING, y, "MOST USED MODEL", topModelLabel, theme));
2096
- const recent30Label = stats.rolling30dTopModel ? truncateText(`${stats.rolling30dTopModel} (${formatNumber(stats.rolling30dTokens)})`, MAX_STAT_VALUE_CHARS) : formatNumber(stats.rolling30dTokens);
2097
- sections.push(renderBottomStat(PADDING + col1Width, y, "RECENT USE (LAST 30 DAYS)", recent30Label, theme));
2098
- sections.push(renderBottomStat(PADDING + col1Width + col2Width, y, "LONGEST STREAK", `${stats.longestStreak} days`, theme));
2099
- sections.push(renderBottomStat(PADDING + col1Width + col2Width + col3Width, y, "CURRENT STREAK", `${stats.currentStreak} days`, theme));
2100
- y += 56 + SECTION_GAP;
2101
- const evenCardWidth = contentWidth / 4;
2102
- 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"/>`);
2103
- sections.push(renderBottomStat(PADDING, y, "TOTAL COST", stats.totalCost >= 100 ? `$${stats.totalCost.toFixed(0)}` : `$${stats.totalCost.toFixed(2)}`, theme));
2104
- sections.push(renderBottomStat(PADDING + evenCardWidth, y, "CACHE HIT RATE", `${(stats.cacheHitRate * 100).toFixed(1)}%`, theme));
2105
- sections.push(renderBottomStat(PADDING + evenCardWidth * 2, y, "ACTIVE DAYS", `${stats.activeDays} / ${stats.totalDays}`, theme));
2106
- sections.push(renderBottomStat(PADDING + evenCardWidth * 3, y, "AVG DAILY TOKENS", formatNumber(stats.averageDailyTokens), theme));
2107
- y += 56 + PADDING;
2108
- const totalHeight = y;
2109
- const svgWidth = Math.max(contentWidth + PADDING * 2, MIN_SVG_WIDTH);
2110
- sections.push(text(svgWidth - PADDING, totalHeight - 16, "tokenleak", {
2111
- fill: theme.muted,
2112
- "font-size": 10,
2113
- "font-family": FONT_FAMILY,
2114
- "text-anchor": "end",
2115
- opacity: "0.4"
2116
- }));
2117
- const svgContent = [
2118
- `<svg xmlns="http://www.w3.org/2000/svg" width="${svgWidth}" height="${totalHeight}" viewBox="0 0 ${svgWidth} ${totalHeight}">`,
2119
- `<defs><style>@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&amp;display=swap');</style></defs>`,
2120
- rect(0, 0, svgWidth, totalHeight, theme.background, 12),
2121
- ...sections,
2122
- "</svg>"
2123
- ].join(`
2124
- `);
2125
- return svgContent;
2126
- }
2127
- }
2128
- // packages/renderers/dist/png/terminal-card.js
2151
+ // packages/renderers/dist/card/layout.js
2129
2152
  var CARD_PADDING = 48;
2130
2153
  var TITLEBAR_HEIGHT = 48;
2131
2154
  var DOT_RADIUS = 6;
2132
2155
  var DOT_GAP = 8;
2133
- var CELL_SIZE2 = 16;
2134
- var CELL_GAP2 = 4;
2156
+ var CELL_SIZE = 16;
2157
+ var CELL_GAP = 4;
2135
2158
  var STAT_GRID_COLS = 3;
2136
- var MODEL_BAR_HEIGHT = 8;
2137
- var DAY_LABEL_WIDTH2 = 44;
2138
- var MONTH_LABEL_HEIGHT2 = 24;
2159
+ var MODEL_BAR_HEIGHT = 11;
2160
+ var DAY_LABEL_WIDTH = 44;
2161
+ var MONTH_LABEL_HEIGHT = 24;
2139
2162
  var PROVIDER_SECTION_GAP = 36;
2140
- var FONT_FAMILY2 = "'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', monospace";
2163
+ var MIN_CONTENT_WIDTH = 700;
2164
+ var MODEL_NAME_WIDTH = 220;
2165
+ var MODEL_BAR_GAP = 36;
2166
+ var MODEL_PERCENT_WIDTH = 40;
2167
+
2168
+ // packages/renderers/dist/png/terminal-card.js
2169
+ var FONT_FAMILY = "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif";
2170
+ var MONO_FONT_FAMILY = "'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', monospace";
2141
2171
  function getCardTheme(mode) {
2142
2172
  if (mode === "dark") {
2143
2173
  return {
2144
- bg: "#0c0c0c",
2145
- fg: "#ffffff",
2146
- muted: "#52525b",
2147
- border: "rgba(255,255,255,0.06)",
2148
- accent: "#10b981",
2149
- heatmapEmpty: "#1a1a1a",
2150
- barTrack: "#1c1c1c",
2151
- titlebarBorder: "rgba(255,255,255,0.06)"
2174
+ bg: "#09090b",
2175
+ fg: "#f0f0f0",
2176
+ muted: "#6b7280",
2177
+ labelFg: "#b0b8c4",
2178
+ border: "rgba(255,255,255,0.08)",
2179
+ accent: "#34d399",
2180
+ heatmapEmpty: "#1a1a22",
2181
+ barTrack: "#151520",
2182
+ titlebarBorder: "rgba(255,255,255,0.08)"
2152
2183
  };
2153
2184
  }
2154
2185
  return {
2155
2186
  bg: "#fafafa",
2156
2187
  fg: "#18181b",
2157
2188
  muted: "#a1a1aa",
2189
+ labelFg: "#71717a",
2158
2190
  border: "rgba(0,0,0,0.08)",
2159
2191
  accent: "#059669",
2160
2192
  heatmapEmpty: "#e4e4e7",
@@ -2166,7 +2198,7 @@ function buildHeatmapScale(colors, isDark) {
2166
2198
  const [startHex, endHex] = colors.gradient;
2167
2199
  const s = hexToRgb(startHex);
2168
2200
  const e = hexToRgb(endHex);
2169
- const opacities = isDark ? [0.15, 0.35, 0.6, 1] : [0.2, 0.4, 0.65, 1];
2201
+ const opacities = isDark ? [0.25, 0.5, 0.75, 1] : [0.2, 0.4, 0.65, 1];
2170
2202
  return [
2171
2203
  "transparent",
2172
2204
  ...opacities.map((t) => {
@@ -2189,7 +2221,7 @@ function rgbToHex(r, g, b) {
2189
2221
  const toHex = (n) => n.toString(16).padStart(2, "0");
2190
2222
  return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
2191
2223
  }
2192
- function computeQuantiles2(values) {
2224
+ function computeQuantiles(values) {
2193
2225
  const nonZero = values.filter((v) => v > 0).sort((a, b) => a - b);
2194
2226
  if (nonZero.length === 0)
2195
2227
  return [0, 0, 0];
@@ -2199,7 +2231,7 @@ function computeQuantiles2(values) {
2199
2231
  };
2200
2232
  return [q(0.25), q(0.5), q(0.75)];
2201
2233
  }
2202
- function getLevel2(tokens, quantiles) {
2234
+ function getLevel(tokens, quantiles) {
2203
2235
  if (tokens <= 0)
2204
2236
  return 0;
2205
2237
  if (tokens <= quantiles[0])
@@ -2210,7 +2242,7 @@ function getLevel2(tokens, quantiles) {
2210
2242
  return 3;
2211
2243
  return 4;
2212
2244
  }
2213
- var MONTH_NAMES2 = [
2245
+ var MONTH_NAMES = [
2214
2246
  "Jan",
2215
2247
  "Feb",
2216
2248
  "Mar",
@@ -2253,6 +2285,41 @@ function formatPercentage(rate) {
2253
2285
  function formatStreak(n) {
2254
2286
  return `${n} day${n !== 1 ? "s" : ""}`;
2255
2287
  }
2288
+ function formatRatio(value, suffix = "x") {
2289
+ if (value === null || !Number.isFinite(value)) {
2290
+ return "n/a";
2291
+ }
2292
+ return `${value.toFixed(value >= 10 ? 1 : 2)}${suffix}`;
2293
+ }
2294
+ function formatPercentPoints(value) {
2295
+ const prefix = value >= 0 ? "+" : "";
2296
+ return `${prefix}${(value * 100).toFixed(1)}pp`;
2297
+ }
2298
+ function formatHour(hour) {
2299
+ return `${hour.toString().padStart(2, "0")}:00`;
2300
+ }
2301
+ function formatDuration(durationMs) {
2302
+ if (durationMs === null || durationMs === undefined || durationMs <= 0) {
2303
+ return "n/a";
2304
+ }
2305
+ const totalMinutes = Math.round(durationMs / 60000);
2306
+ if (totalMinutes < 60) {
2307
+ return `${totalMinutes}m`;
2308
+ }
2309
+ const hours = Math.floor(totalMinutes / 60);
2310
+ const minutes = totalMinutes % 60;
2311
+ return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
2312
+ }
2313
+ function truncateText(value, maxLength) {
2314
+ return value.length > maxLength ? `${value.slice(0, maxLength - 1)}\u2026` : value;
2315
+ }
2316
+ function formatSessionSummary(summary) {
2317
+ const duration = formatDuration(summary.durationMs);
2318
+ if (duration === "n/a") {
2319
+ return truncateText(summary.label, 20);
2320
+ }
2321
+ return truncateText(`${summary.label} \xB7 ${duration}`, 24);
2322
+ }
2256
2323
  function renderProviderHeatmap(daily, since, until, heatmapColors, emptyColor) {
2257
2324
  const tokenMap = new Map;
2258
2325
  for (const d of daily) {
@@ -2265,7 +2332,7 @@ function renderProviderHeatmap(daily, since, until, heatmapColors, emptyColor) {
2265
2332
  const start = new Date(startStr + "T00:00:00Z");
2266
2333
  start.setUTCDate(start.getUTCDate() - start.getUTCDay());
2267
2334
  const allTokens = Array.from(tokenMap.values());
2268
- const quantiles = computeQuantiles2(allTokens);
2335
+ const quantiles = computeQuantiles(allTokens);
2269
2336
  const cells = [];
2270
2337
  const monthLabels = [];
2271
2338
  let lastMonth = -1;
@@ -2275,37 +2342,183 @@ function renderProviderHeatmap(daily, since, until, heatmapColors, emptyColor) {
2275
2342
  const row = current.getUTCDay();
2276
2343
  const dateStr = current.toISOString().slice(0, 10);
2277
2344
  const tokens = tokenMap.get(dateStr) ?? 0;
2278
- const level = getLevel2(tokens, quantiles);
2279
- const x = DAY_LABEL_WIDTH2 + col * (CELL_SIZE2 + CELL_GAP2);
2280
- const y = MONTH_LABEL_HEIGHT2 + row * (CELL_SIZE2 + CELL_GAP2);
2345
+ const level = getLevel(tokens, quantiles);
2346
+ const x = DAY_LABEL_WIDTH + col * (CELL_SIZE + CELL_GAP);
2347
+ const y = MONTH_LABEL_HEIGHT + row * (CELL_SIZE + CELL_GAP);
2281
2348
  const fill = level === 0 ? emptyColor : heatmapColors[level];
2282
2349
  const title = `${dateStr}: ${tokens.toLocaleString()} tokens`;
2283
- cells.push(`<rect x="${x}" y="${y}" width="${CELL_SIZE2}" height="${CELL_SIZE2}" fill="${escapeXml(fill)}" rx="3"><title>${escapeXml(title)}</title></rect>`);
2350
+ cells.push(`<rect x="${x}" y="${y}" width="${CELL_SIZE}" height="${CELL_SIZE}" fill="${escapeXml(fill)}" rx="3"><title>${escapeXml(title)}</title></rect>`);
2284
2351
  const month = current.getUTCMonth();
2285
2352
  if (month !== lastMonth && row === 0) {
2286
2353
  lastMonth = month;
2287
- monthLabels.push(`<text x="${x}" y="${MONTH_LABEL_HEIGHT2 - 8}" fill="__MUTED__" font-size="11" font-family="${escapeXml(FONT_FAMILY2)}">${escapeXml(MONTH_NAMES2[month] ?? "")}</text>`);
2354
+ monthLabels.push(`<text x="${x}" y="${MONTH_LABEL_HEIGHT - 8}" fill="__MUTED__" font-size="11" font-family="${escapeXml(FONT_FAMILY)}">${escapeXml(MONTH_NAMES[month] ?? "")}</text>`);
2288
2355
  }
2289
2356
  if (row === 6)
2290
2357
  col++;
2291
2358
  current.setUTCDate(current.getUTCDate() + 1);
2292
2359
  }
2293
2360
  const dayLabels = [
2361
+ { label: "Sun", row: 0 },
2294
2362
  { label: "Mon", row: 1 },
2363
+ { label: "Tue", row: 2 },
2295
2364
  { label: "Wed", row: 3 },
2365
+ { label: "Thu", row: 4 },
2296
2366
  { label: "Fri", row: 5 },
2297
- { label: "Sun", row: 0 }
2367
+ { label: "Sat", row: 6 }
2298
2368
  ].map((d) => {
2299
- const y = MONTH_LABEL_HEIGHT2 + d.row * (CELL_SIZE2 + CELL_GAP2) + CELL_SIZE2 - 2;
2300
- return `<text x="0" y="${y}" fill="__MUTED__" font-size="11" font-family="${escapeXml(FONT_FAMILY2)}">${escapeXml(d.label)}</text>`;
2369
+ const y = MONTH_LABEL_HEIGHT + d.row * (CELL_SIZE + CELL_GAP) + CELL_SIZE - 2;
2370
+ return `<text x="0" y="${y}" fill="__MUTED__" font-size="11" font-family="${escapeXml(FONT_FAMILY)}">${escapeXml(d.label)}</text>`;
2301
2371
  }).join("");
2302
2372
  const totalCols = col + 1;
2303
- const gridWidth = DAY_LABEL_WIDTH2 + totalCols * (CELL_SIZE2 + CELL_GAP2);
2304
- const height = MONTH_LABEL_HEIGHT2 + 7 * (CELL_SIZE2 + CELL_GAP2);
2373
+ const gridWidth = DAY_LABEL_WIDTH + totalCols * (CELL_SIZE + CELL_GAP);
2374
+ const height = MONTH_LABEL_HEIGHT + 7 * (CELL_SIZE + CELL_GAP);
2305
2375
  const svg = [dayLabels, ...monthLabels, ...cells].join(`
2306
2376
  `);
2307
2377
  return { svg, gridWidth, height };
2308
2378
  }
2379
+ function renderSectionHeader(x, y, title, theme, cardAccent) {
2380
+ const parts = [];
2381
+ parts.push(`<rect x="${x}" y="${y - 8}" width="3" height="10" rx="1.5" fill="${escapeXml(cardAccent)}" opacity="0.6"/>`);
2382
+ parts.push(`<text x="${x + 12}" y="${y}" fill="${escapeXml(theme.labelFg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" letter-spacing="1.8">${escapeXml(title)}</text>`);
2383
+ return parts.join(`
2384
+ `);
2385
+ }
2386
+ function renderMetricCard(x, y, width, title, lines, theme, cardAccent) {
2387
+ const parts = [];
2388
+ const height = 38 + lines.length * 22;
2389
+ parts.push(`<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="10" fill="${escapeXml(theme.barTrack)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
2390
+ parts.push(`<text x="${x + 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="10" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" letter-spacing="1.6">${escapeXml(title)}</text>`);
2391
+ lines.forEach((line, index) => {
2392
+ const lineY = y + 48 + index * 22;
2393
+ parts.push(`<text x="${x + 18}" y="${lineY}" fill="${escapeXml(theme.fg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600">${escapeXml(line.label)}</text>`);
2394
+ parts.push(`<text x="${x + width - 18}" y="${lineY}" fill="${escapeXml(line.accent ? cardAccent : theme.fg)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" text-anchor="end">${escapeXml(line.value)}</text>`);
2395
+ });
2396
+ return parts.join(`
2397
+ `);
2398
+ }
2399
+ function buildProviderHourBuckets(providers) {
2400
+ return providers.map((p) => {
2401
+ const hours = new Array(24).fill(0);
2402
+ for (const event of p.events ?? []) {
2403
+ const date = new Date(event.timestamp);
2404
+ if (!Number.isNaN(date.getTime())) {
2405
+ const h = date.getUTCHours();
2406
+ hours[h] += event.totalTokens;
2407
+ }
2408
+ }
2409
+ return { provider: p.provider, color: p.colors.primary, hours };
2410
+ });
2411
+ }
2412
+ function renderHourOfDayChart(x, y, width, hourOfDay, theme, cardAccent, providers, isDark) {
2413
+ const chartHeight = 140;
2414
+ const innerHeight = 72;
2415
+ const baselineY = y + 92;
2416
+ const barAreaX = x + 18;
2417
+ const barAreaWidth = width - 36;
2418
+ const barGap = 4;
2419
+ const barWidth = (barAreaWidth - barGap * 23) / 24;
2420
+ const maxTokens = Math.max(...hourOfDay.map((entry) => entry.tokens), 0);
2421
+ const busiest = hourOfDay.reduce((best, entry) => best === null || entry.tokens > best.tokens ? entry : best, null);
2422
+ const isMulti = providers.length > 1;
2423
+ const providerBuckets = isMulti ? buildProviderHourBuckets(providers) : [];
2424
+ let legendSvg = "";
2425
+ if (isMulti && providerBuckets.length > 0) {
2426
+ const titleWidth = 105;
2427
+ let legendX = x + 18 + titleWidth + 12;
2428
+ const legendY = y + 22;
2429
+ for (const bucket of providerBuckets) {
2430
+ const displayName = providers.find((p) => p.provider === bucket.provider)?.displayName ?? bucket.provider;
2431
+ legendSvg += `<rect x="${legendX}" y="${legendY - 8}" width="8" height="8" rx="2" fill="${escapeXml(bucket.color)}" opacity="0.85"/>` + `<text x="${legendX + 12}" y="${legendY - 1}" fill="${escapeXml(theme.fg)}" font-size="9" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600">${escapeXml(displayName)}</text>`;
2432
+ legendX += 12 + displayName.length * 5.5 + 16;
2433
+ }
2434
+ }
2435
+ const bars = [
2436
+ `<rect x="${x}" y="${y}" width="${width}" height="${chartHeight}" rx="10" fill="${escapeXml(theme.barTrack)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`,
2437
+ `<text x="${x + 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="10" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" letter-spacing="1.6">HOUR OF DAY</text>`,
2438
+ legendSvg,
2439
+ `<text x="${x + width - 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="end">${escapeXml(busiest ? `${formatHour(busiest.hour)} peak` : "No session events")}</text>`,
2440
+ `<line x1="${barAreaX}" y1="${baselineY}" x2="${barAreaX + barAreaWidth}" y2="${baselineY}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`
2441
+ ];
2442
+ bars.push(`<defs><filter id="peakGlow" x="-50%" y="-50%" width="200%" height="200%">` + `<feGaussianBlur stdDeviation="4" result="blur"/>` + `<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>` + `</filter></defs>`);
2443
+ if (isMulti) {
2444
+ const provBaseOpacity = isDark ? "0.45" : "0.3";
2445
+ const provMidOpacity = isDark ? "0.85" : "0.75";
2446
+ for (let bi = 0;bi < providerBuckets.length; bi++) {
2447
+ const gradId = `hod-prov-${bi}`;
2448
+ bars.push(`<defs><linearGradient id="${escapeXml(gradId)}" x1="0%" y1="100%" x2="0%" y2="0%">` + `<stop offset="0%" stop-color="${escapeXml(providerBuckets[bi].color)}" stop-opacity="${provBaseOpacity}"/>` + `<stop offset="60%" stop-color="${escapeXml(providerBuckets[bi].color)}" stop-opacity="${provMidOpacity}"/>` + `<stop offset="100%" stop-color="${escapeXml(providerBuckets[bi].color)}" stop-opacity="1"/>` + `</linearGradient></defs>`);
2449
+ }
2450
+ hourOfDay.forEach((entry, index) => {
2451
+ if (entry.tokens <= 0)
2452
+ return;
2453
+ const totalRatio = entry.tokens / maxTokens;
2454
+ const totalBarHeight = Math.max(4, totalRatio * innerHeight);
2455
+ const colX = barAreaX + index * (barWidth + barGap);
2456
+ const isPeak = busiest !== null && entry.hour === busiest.hour;
2457
+ if (isPeak) {
2458
+ const topY2 = baselineY - totalBarHeight;
2459
+ bars.push(`<rect x="${colX - 2}" y="${topY2 - 2}" width="${barWidth + 4}" height="${totalBarHeight + 4}" rx="5" fill="${escapeXml(providerBuckets[0]?.color ?? cardAccent)}" opacity="0.12" filter="url(#peakGlow)"/>`);
2460
+ }
2461
+ const clipId = `hod-clip-${index}`;
2462
+ const topY = baselineY - totalBarHeight;
2463
+ bars.push(`<defs><clipPath id="${escapeXml(clipId)}">` + `<rect x="${colX}" y="${topY}" width="${barWidth}" height="${totalBarHeight}" rx="3"/>` + `</clipPath></defs>`);
2464
+ let offsetY = 0;
2465
+ for (let bi = 0;bi < providerBuckets.length; bi++) {
2466
+ const tokens = providerBuckets[bi].hours[index] ?? 0;
2467
+ if (tokens <= 0)
2468
+ continue;
2469
+ const segHeight = tokens / entry.tokens * totalBarHeight;
2470
+ const segY = baselineY - offsetY - segHeight;
2471
+ bars.push(`<rect x="${colX}" y="${segY}" width="${barWidth}" height="${segHeight}" fill="url(#hod-prov-${bi})" clip-path="url(#${escapeXml(clipId)})"/>`);
2472
+ offsetY += segHeight;
2473
+ }
2474
+ });
2475
+ } else {
2476
+ const hodGradId = "hod-bar-grad";
2477
+ const hodBaseOpacity = isDark ? "0.25" : "0.1";
2478
+ const hodMidOpacity = isDark ? "0.75" : "0.6";
2479
+ bars.push(`<defs><linearGradient id="${escapeXml(hodGradId)}" x1="0%" y1="100%" x2="0%" y2="0%">` + `<stop offset="0%" stop-color="${escapeXml(cardAccent)}" stop-opacity="${hodBaseOpacity}"/>` + `<stop offset="40%" stop-color="${escapeXml(cardAccent)}" stop-opacity="${hodMidOpacity}"/>` + `<stop offset="100%" stop-color="${escapeXml(cardAccent)}" stop-opacity="1"/>` + `</linearGradient></defs>`);
2480
+ hourOfDay.forEach((entry, index) => {
2481
+ const ratio = maxTokens > 0 ? entry.tokens / maxTokens : 0;
2482
+ const barHeight = maxTokens > 0 ? Math.max(4, ratio * innerHeight) : 4;
2483
+ const colX = barAreaX + index * (barWidth + barGap);
2484
+ const colY = baselineY - barHeight;
2485
+ const isPeak = busiest !== null && entry.hour === busiest.hour && entry.tokens > 0;
2486
+ if (isPeak) {
2487
+ bars.push(`<rect x="${colX - 2}" y="${colY - 2}" width="${barWidth + 4}" height="${barHeight + 4}" rx="5" fill="${escapeXml(cardAccent)}" opacity="0.15" filter="url(#peakGlow)"/>`);
2488
+ }
2489
+ bars.push(`<rect x="${colX}" y="${colY}" width="${barWidth}" height="${barHeight}" rx="3" fill="url(#${escapeXml(hodGradId)})" opacity="${0.35 + ratio * 0.65}"/>`);
2490
+ });
2491
+ }
2492
+ [0, 3, 6, 9, 12, 15, 18, 21].forEach((hour) => {
2493
+ const labelX = barAreaX + hour * (barWidth + barGap) + barWidth / 2;
2494
+ bars.push(`<text x="${labelX}" y="${y + 116}" fill="${escapeXml(theme.labelFg)}" font-size="9" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="middle">${escapeXml(hour.toString().padStart(2, "0"))}</text>`);
2495
+ });
2496
+ return {
2497
+ svg: bars.join(`
2498
+ `),
2499
+ height: chartHeight
2500
+ };
2501
+ }
2502
+ function renderModelMixShift(x, y, width, more, theme, cardAccent) {
2503
+ if (!more.compare || more.compare.modelMixShift.length === 0) {
2504
+ return { svg: "", height: 0 };
2505
+ }
2506
+ const rows = more.compare.modelMixShift.slice(0, 4);
2507
+ const height = 38 + rows.length * 24;
2508
+ const parts = [
2509
+ `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="10" fill="${escapeXml(theme.barTrack)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`,
2510
+ `<text x="${x + 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="10" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" letter-spacing="1.6">MODEL MIX SHIFT</text>`,
2511
+ `<text x="${x + width - 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="end">${escapeXml(`${more.compare.previousRange.since} \u2192 ${more.compare.previousRange.until}`)}</text>`
2512
+ ];
2513
+ rows.forEach((row, index) => {
2514
+ const lineY = y + 48 + index * 24;
2515
+ parts.push(`<text x="${x + 18}" y="${lineY}" fill="${escapeXml(theme.fg)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600">${escapeXml(truncateText(row.model, 28))}</text>`);
2516
+ parts.push(`<text x="${x + width - 18}" y="${lineY}" fill="${escapeXml(row.deltaShare >= 0 ? cardAccent : "#f97316")}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" text-anchor="end">${escapeXml(formatPercentPoints(row.deltaShare))}</text>`);
2517
+ parts.push(`<text x="${x + width - 110}" y="${lineY}" fill="${escapeXml(theme.labelFg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="end">${escapeXml(`${(row.previousShare * 100).toFixed(1)}% \u2192 ${(row.currentShare * 100).toFixed(1)}%`)}</text>`);
2518
+ });
2519
+ return { svg: parts.join(`
2520
+ `), height };
2521
+ }
2309
2522
  function renderTerminalCardSvg(output, options) {
2310
2523
  const theme = getCardTheme(options.theme);
2311
2524
  const isDark = options.theme === "dark";
@@ -2313,6 +2526,8 @@ function renderTerminalCardSvg(output, options) {
2313
2526
  const stats = output.aggregated;
2314
2527
  const { since, until } = output.dateRange;
2315
2528
  const providers = output.providers;
2529
+ const cardAccent = providers.length === 1 ? providers[0]?.colors.primary ?? theme.accent : theme.accent;
2530
+ const barAccent = providers.length > 1 ? isDark ? "#c4d0e0" : "#000000" : cardAccent;
2316
2531
  const providerHeatmaps = providers.map((p) => {
2317
2532
  const heatmapColors = buildHeatmapScale(p.colors, isDark);
2318
2533
  return {
@@ -2322,7 +2537,7 @@ function renderTerminalCardSvg(output, options) {
2322
2537
  };
2323
2538
  });
2324
2539
  const maxHeatmapWidth = providerHeatmaps.reduce((max, ph) => Math.max(max, ph.heatmap.gridWidth), 0);
2325
- const minContentWidth = Math.max(maxHeatmapWidth, 700);
2540
+ const minContentWidth = Math.max(maxHeatmapWidth, MIN_CONTENT_WIDTH);
2326
2541
  const cardWidth = minContentWidth + pad * 2;
2327
2542
  const contentWidth = cardWidth - pad * 2;
2328
2543
  let y = 0;
@@ -2340,24 +2555,22 @@ function renderTerminalCardSvg(output, options) {
2340
2555
  for (const dot of dots) {
2341
2556
  sections.push(`<circle cx="${dot.cx}" cy="${dotY}" r="${DOT_RADIUS}" fill="${escapeXml(dot.color)}"/>`);
2342
2557
  }
2343
- const titleX = dots[2].cx + DOT_RADIUS + 20;
2344
- sections.push(`<text x="${titleX}" y="${dotY + 5}" fill="${escapeXml(theme.muted)}" font-size="13" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="500">${escapeXml("tokenleak")}</text>`);
2345
2558
  sections.push(`<line x1="0" y1="${TITLEBAR_HEIGHT}" x2="${cardWidth}" y2="${TITLEBAR_HEIGHT}" stroke="${escapeXml(theme.titlebarBorder)}" stroke-width="1"/>`);
2346
2559
  y = TITLEBAR_HEIGHT + pad * 0.6;
2347
- sections.push(`<text x="${pad}" y="${y + 16}" font-size="15" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="500">` + `<tspan fill="${escapeXml(theme.accent)}">$</tspan>` + `<tspan fill="${escapeXml(theme.fg)}"> tokenleak</tspan>` + `<tspan fill="${escapeXml(theme.accent)}">_</tspan>` + `</text>`);
2560
+ sections.push(`<text x="${pad}" y="${y + 16}" font-size="15" font-family="${escapeXml(MONO_FONT_FAMILY)}" font-weight="500">` + `<tspan fill="${escapeXml(cardAccent)}">$</tspan>` + `<tspan fill="${escapeXml(theme.fg)}"> tokenleak</tspan>` + `<tspan fill="${escapeXml(cardAccent)}">_</tspan>` + `</text>`);
2348
2561
  y += 40;
2349
2562
  const dateRangeText = formatDateRange(since, until);
2350
- sections.push(`<text x="${pad}" y="${y + 14}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="600" letter-spacing="2">${escapeXml(dateRangeText)}</text>`);
2563
+ sections.push(`<text x="${pad}" y="${y + 14}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600" letter-spacing="2">${escapeXml(dateRangeText)}</text>`);
2351
2564
  y += 40;
2352
2565
  for (let pi = 0;pi < providerHeatmaps.length; pi++) {
2353
2566
  const { provider, heatmap, heatmapColors } = providerHeatmaps[pi];
2354
- const provDotRadius = 5;
2567
+ const provDotRadius = 7;
2355
2568
  const provColor = provider.colors.primary;
2356
- sections.push(`<circle cx="${pad + provDotRadius}" cy="${y + 8}" r="${provDotRadius}" fill="${escapeXml(provColor)}"/>`);
2357
- sections.push(`<text x="${pad + provDotRadius * 2 + 10}" y="${y + 13}" fill="${escapeXml(theme.fg)}" font-size="14" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="600">${escapeXml(provider.displayName)}</text>`);
2569
+ sections.push(`<circle cx="${pad + provDotRadius}" cy="${y + 10}" r="${provDotRadius}" fill="${escapeXml(provColor)}"/>`);
2570
+ sections.push(`<text x="${pad + provDotRadius * 2 + 12}" y="${y + 15}" fill="${escapeXml(theme.fg)}" font-size="17" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700">${escapeXml(provider.displayName)}</text>`);
2358
2571
  const summaryText = `${formatNumber(provider.totalTokens)} tokens \xB7 ${formatCost(provider.totalCost)}`;
2359
- sections.push(`<text x="${cardWidth - pad}" y="${y + 13}" fill="${escapeXml(theme.muted)}" font-size="11" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="500" text-anchor="end">${escapeXml(summaryText)}</text>`);
2360
- y += 28;
2572
+ sections.push(`<text x="${cardWidth - pad}" y="${y + 15}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="end">${escapeXml(summaryText)}</text>`);
2573
+ y += 32;
2361
2574
  const heatmapSvg = heatmap.svg.replace(/__MUTED__/g, escapeXml(theme.muted));
2362
2575
  sections.push(`<g transform="translate(${pad}, ${y})">`);
2363
2576
  sections.push(heatmapSvg);
@@ -2372,13 +2585,13 @@ function renderTerminalCardSvg(output, options) {
2372
2585
  }
2373
2586
  }
2374
2587
  if (providers.length === 0) {
2375
- sections.push(`<text x="${pad}" y="${y + 14}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="500">${escapeXml("No provider data")}</text>`);
2588
+ sections.push(`<text x="${pad}" y="${y + 14}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500">${escapeXml("No provider data")}</text>`);
2376
2589
  y += 32;
2377
2590
  }
2378
2591
  sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
2379
2592
  y += 28;
2380
2593
  if (providers.length > 1) {
2381
- sections.push(`<text x="${pad}" y="${y}" fill="${escapeXml(theme.muted)}" font-size="10" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="600" letter-spacing="2">${escapeXml("OVERALL")}</text>`);
2594
+ sections.push(`<text x="${pad}" y="${y}" fill="${escapeXml(theme.muted)}" font-size="10" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600" letter-spacing="2">${escapeXml("OVERALL")}</text>`);
2382
2595
  y += 24;
2383
2596
  }
2384
2597
  const statColWidth = contentWidth / STAT_GRID_COLS;
@@ -2396,9 +2609,9 @@ function renderTerminalCardSvg(output, options) {
2396
2609
  for (let i = 0;i < row.length; i++) {
2397
2610
  const stat = row[i];
2398
2611
  const x = pad + i * statColWidth;
2399
- sections.push(`<text x="${x}" y="${startY}" fill="${escapeXml(theme.muted)}" font-size="10" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="600" letter-spacing="1.5">${escapeXml(stat.label)}</text>`);
2400
- const valueColor = stat.accent ? theme.accent : theme.fg;
2401
- sections.push(`<text x="${x}" y="${startY + 28}" fill="${escapeXml(valueColor)}" font-size="22" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="700">${escapeXml(stat.value)}</text>`);
2612
+ sections.push(`<text x="${x}" y="${startY}" fill="${escapeXml(theme.muted)}" font-size="10" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600" letter-spacing="1.5">${escapeXml(stat.label)}</text>`);
2613
+ const valueColor = stat.accent && providers.length === 1 ? cardAccent : theme.fg;
2614
+ sections.push(`<text x="${x}" y="${startY + 28}" fill="${escapeXml(valueColor)}" font-size="22" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700">${escapeXml(stat.value)}</text>`);
2402
2615
  }
2403
2616
  }
2404
2617
  renderStatRow(statsRow1, y);
@@ -2408,38 +2621,152 @@ function renderTerminalCardSvg(output, options) {
2408
2621
  y += 8;
2409
2622
  sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
2410
2623
  y += 28;
2411
- sections.push(`<text x="${pad}" y="${y}" fill="${escapeXml(theme.muted)}" font-size="10" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="600" letter-spacing="2">${escapeXml("TOP MODELS")}</text>`);
2624
+ sections.push(renderSectionHeader(pad, y, "TOP MODELS", theme, cardAccent));
2412
2625
  y += 24;
2413
2626
  const topModels2 = stats.topModels.slice(0, 3);
2414
- const modelNameWidth = 200;
2415
- const percentWidth = 60;
2416
- const barMaxWidth = contentWidth - modelNameWidth - percentWidth - 20;
2417
- for (const model of topModels2) {
2627
+ const rankWidth = 28;
2628
+ const modelNameWidth = MODEL_NAME_WIDTH;
2629
+ const barGap = MODEL_BAR_GAP;
2630
+ const percentX = cardWidth - pad;
2631
+ const barX = pad + rankWidth + modelNameWidth;
2632
+ const barMaxWidth = Math.max(48, percentX - barX - barGap);
2633
+ for (const [index, model] of topModels2.entries()) {
2418
2634
  const barWidth = Math.max(4, model.percentage / 100 * barMaxWidth);
2419
- sections.push(`<text x="${pad}" y="${y + MODEL_BAR_HEIGHT + 4}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="400">${escapeXml(model.model)}</text>`);
2420
- const barX = pad + modelNameWidth;
2421
- sections.push(`<rect x="${barX}" y="${y}" width="${barMaxWidth}" height="${MODEL_BAR_HEIGHT}" rx="4" fill="${escapeXml(theme.barTrack)}"/>`);
2422
- const gradId = `grad-${model.model.replace(/[^a-zA-Z0-9]/g, "")}`;
2423
- sections.push(`<defs><linearGradient id="${escapeXml(gradId)}" x1="0%" y1="0%" x2="100%" y2="0%">` + `<stop offset="0%" stop-color="${escapeXml(theme.accent)}44"/>` + `<stop offset="100%" stop-color="${escapeXml(theme.accent)}"/>` + `</linearGradient></defs>`);
2424
- sections.push(`<rect x="${barX}" y="${y}" width="${barWidth}" height="${MODEL_BAR_HEIGHT}" rx="4" fill="url(#${escapeXml(gradId)})"/>`);
2425
- sections.push(`<text x="${barX + barMaxWidth + 12}" y="${y + MODEL_BAR_HEIGHT + 4}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="500" text-anchor="end">${escapeXml(`${model.percentage.toFixed(0)}%`)}</text>`);
2635
+ sections.push(`<text x="${pad}" y="${y + MODEL_BAR_HEIGHT - 1}" fill="${escapeXml(cardAccent)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" opacity="0.7">${escapeXml(String(index + 1))}</text>`);
2636
+ sections.push(`<text x="${pad + rankWidth}" y="${y + MODEL_BAR_HEIGHT - 1}" fill="${escapeXml(theme.fg)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500">${escapeXml(model.model)}</text>`);
2637
+ const trackColor = isDark ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.06)";
2638
+ sections.push(`<rect x="${barX}" y="${y}" width="${barMaxWidth}" height="${MODEL_BAR_HEIGHT}" rx="6" fill="${escapeXml(trackColor)}"/>`);
2639
+ const gradId = `grad-${index}-${model.model.replace(/[^a-zA-Z0-9]/g, "")}`;
2640
+ sections.push(`<defs><linearGradient id="${escapeXml(gradId)}" x1="0%" y1="0%" x2="100%" y2="0%">` + `<stop offset="0%" stop-color="${escapeXml(barAccent)}" stop-opacity="${isDark ? "0.45" : "0.27"}"/>` + `<stop offset="100%" stop-color="${escapeXml(barAccent)}" stop-opacity="1"/>` + `</linearGradient></defs>`);
2641
+ sections.push(`<rect x="${barX}" y="${y}" width="${barWidth}" height="${MODEL_BAR_HEIGHT}" rx="6" fill="url(#${escapeXml(gradId)})"/>`);
2642
+ sections.push(`<text x="${percentX}" y="${y + MODEL_BAR_HEIGHT - 1}" fill="${escapeXml(theme.fg)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600" text-anchor="end">${escapeXml(`${model.percentage.toFixed(0)}%`)}</text>`);
2426
2643
  y += 32;
2427
2644
  }
2428
- y += pad * 0.5;
2429
- const cardHeight = y;
2430
- const svg = sections.join(`
2645
+ if (options.more && output.more) {
2646
+ const more = output.more;
2647
+ const cardGap = 16;
2648
+ const detailCardWidth = (contentWidth - cardGap) / 2;
2649
+ y += 8;
2650
+ sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
2651
+ y += 28;
2652
+ sections.push(renderSectionHeader(pad, y, "MORE", theme, cardAccent));
2653
+ y += 24;
2654
+ const efficiencyLines = [
2655
+ {
2656
+ label: "Input / Output",
2657
+ value: more.inputOutput.inputPerOutput === null ? "n/a" : `${more.inputOutput.inputPerOutput.toFixed(2)} : 1`,
2658
+ accent: true
2659
+ },
2660
+ {
2661
+ label: "Output / Input",
2662
+ value: formatRatio(more.inputOutput.outputPerInput)
2663
+ },
2664
+ {
2665
+ label: "Output Share",
2666
+ value: formatPercentage(more.inputOutput.outputShare)
2667
+ }
2668
+ ];
2669
+ sections.push(renderMetricCard(pad, y, detailCardWidth, "INPUT / OUTPUT", efficiencyLines, theme, cardAccent));
2670
+ const burnLines = [
2671
+ {
2672
+ label: "Projected Cost",
2673
+ value: formatCost(more.monthlyBurn.projectedCost),
2674
+ accent: true
2675
+ },
2676
+ {
2677
+ label: "Projected Tokens",
2678
+ value: formatNumber(more.monthlyBurn.projectedTokens)
2679
+ },
2680
+ {
2681
+ label: "Based On",
2682
+ value: `${more.monthlyBurn.observedDays} / ${more.monthlyBurn.calendarDays} days`
2683
+ }
2684
+ ];
2685
+ sections.push(renderMetricCard(pad + detailCardWidth + cardGap, y, detailCardWidth, "PROJECTED MONTHLY BURN", burnLines, theme, cardAccent));
2686
+ y += 38 + Math.max(efficiencyLines.length, burnLines.length) * 22 + 16;
2687
+ const cacheLines = [
2688
+ {
2689
+ label: "Cache Reads",
2690
+ value: formatNumber(more.cacheEconomics.readTokens),
2691
+ accent: true
2692
+ },
2693
+ {
2694
+ label: "Cache Writes",
2695
+ value: formatNumber(more.cacheEconomics.writeTokens)
2696
+ },
2697
+ {
2698
+ label: "Read Coverage",
2699
+ value: formatPercentage(more.cacheEconomics.readCoverage)
2700
+ },
2701
+ {
2702
+ label: "Reuse Ratio",
2703
+ value: formatRatio(more.cacheEconomics.reuseRatio)
2704
+ }
2705
+ ];
2706
+ sections.push(renderMetricCard(pad, y, detailCardWidth, "CACHE ECONOMICS", cacheLines, theme, cardAccent));
2707
+ const sessionLines = [
2708
+ {
2709
+ label: "Sessions",
2710
+ value: String(more.sessionMetrics.totalSessions),
2711
+ accent: true
2712
+ },
2713
+ {
2714
+ label: "Avg Tokens",
2715
+ value: formatNumber(more.sessionMetrics.averageTokens)
2716
+ },
2717
+ {
2718
+ label: "Avg Messages",
2719
+ value: more.sessionMetrics.averageMessages.toFixed(1)
2720
+ },
2721
+ {
2722
+ label: "Avg Duration",
2723
+ value: formatDuration(more.sessionMetrics.averageDurationMs)
2724
+ },
2725
+ {
2726
+ label: "Longest Session",
2727
+ value: more.sessionMetrics.longestSession ? formatSessionSummary(more.sessionMetrics.longestSession) : "n/a"
2728
+ },
2729
+ {
2730
+ label: "Top Project",
2731
+ value: more.sessionMetrics.topProject ? truncateText(more.sessionMetrics.topProject.name, 20) : "n/a"
2732
+ }
2733
+ ];
2734
+ sections.push(renderMetricCard(pad + detailCardWidth + cardGap, y, detailCardWidth, "SESSION STATS", sessionLines, theme, cardAccent));
2735
+ y += 38 + Math.max(cacheLines.length, sessionLines.length) * 22 + 16;
2736
+ const hourChart = renderHourOfDayChart(pad, y, contentWidth, more.hourOfDay, theme, cardAccent, providers, isDark);
2737
+ sections.push(hourChart.svg);
2738
+ y += hourChart.height + 16;
2739
+ const mixShift = renderModelMixShift(pad, y, contentWidth, more, theme, cardAccent);
2740
+ if (mixShift.height > 0) {
2741
+ sections.push(mixShift.svg);
2742
+ y += mixShift.height + 12;
2743
+ }
2744
+ }
2745
+ y += pad * 0.5;
2746
+ const cardHeight = y;
2747
+ const svg = sections.join(`
2431
2748
  `).replace("__CARD_HEIGHT__", String(cardHeight));
2432
2749
  return [
2433
- `<svg xmlns="http://www.w3.org/2000/svg" width="${cardWidth}" height="${cardHeight}" viewBox="0 0 ${cardWidth} ${cardHeight}" shape-rendering="geometricPrecision" text-rendering="geometricPrecision">`,
2750
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${cardWidth}" height="${cardHeight}" viewBox="0 0 ${cardWidth} ${cardHeight}" shape-rendering="geometricPrecision" text-rendering="optimizeLegibility" color-rendering="optimizeQuality">`,
2434
2751
  svg,
2435
2752
  "</svg>"
2436
2753
  ].join(`
2437
2754
  `);
2438
2755
  }
2439
2756
 
2757
+ // packages/renderers/dist/svg/svg-renderer.js
2758
+ class SvgRenderer {
2759
+ format = "svg";
2760
+ async render(output, options) {
2761
+ return renderTerminalCardSvg(output, {
2762
+ ...options,
2763
+ format: "svg"
2764
+ });
2765
+ }
2766
+ }
2440
2767
  // packages/renderers/dist/png/png-renderer.js
2441
2768
  import sharp from "sharp";
2442
- var PNG_DENSITY = 288;
2769
+ var PNG_DENSITY = 432;
2443
2770
 
2444
2771
  class PngRenderer {
2445
2772
  format = "png";
@@ -2448,7 +2775,9 @@ class PngRenderer {
2448
2775
  const pngBuffer = await sharp(Buffer.from(svgString), {
2449
2776
  density: PNG_DENSITY
2450
2777
  }).png({
2451
- compressionLevel: 9
2778
+ adaptiveFiltering: true,
2779
+ compressionLevel: 9,
2780
+ force: true
2452
2781
  }).toBuffer();
2453
2782
  return pngBuffer;
2454
2783
  }
@@ -2475,20 +2804,8 @@ var HEATMAP_BLOCKS = {
2475
2804
  DARK: "\u2593",
2476
2805
  MEDIUM: "\u2592",
2477
2806
  LIGHT: "\u2591",
2478
- EMPTY: " "
2807
+ EMPTY: "\xB7"
2479
2808
  };
2480
- function intensityBlock(value, max) {
2481
- if (max <= 0 || value <= 0)
2482
- return HEATMAP_BLOCKS.EMPTY;
2483
- const ratio = value / max;
2484
- if (ratio >= 0.75)
2485
- return HEATMAP_BLOCKS.FULL;
2486
- if (ratio >= 0.5)
2487
- return HEATMAP_BLOCKS.DARK;
2488
- if (ratio >= 0.25)
2489
- return HEATMAP_BLOCKS.MEDIUM;
2490
- return HEATMAP_BLOCKS.LIGHT;
2491
- }
2492
2809
  function intensityColor(value, max) {
2493
2810
  if (max <= 0 || value <= 0)
2494
2811
  return "dim";
@@ -2503,9 +2820,10 @@ function intensityColor(value, max) {
2503
2820
  }
2504
2821
 
2505
2822
  // packages/renderers/dist/terminal/heatmap.js
2506
- var DAY_LABELS3 = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
2823
+ var DAY_LABELS2 = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
2507
2824
  var MONTH_LABELS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
2508
- var DAY_LABEL_WIDTH3 = 4;
2825
+ var DAY_LABEL_WIDTH2 = 4;
2826
+ var WEEK_COLUMN_WIDTH = 2;
2509
2827
  var LEGEND_TEXT = "Less";
2510
2828
  var LEGEND_TEXT_MORE = "More";
2511
2829
  function buildUsageMap(daily) {
@@ -2515,83 +2833,117 @@ function buildUsageMap(daily) {
2515
2833
  }
2516
2834
  return map;
2517
2835
  }
2518
- function renderTerminalHeatmap(daily, options) {
2519
- if (daily.length === 0) {
2520
- return " No usage data available.";
2836
+ function computeQuantiles2(values) {
2837
+ const nonZero = values.filter((value) => value > 0).sort((a, b) => a - b);
2838
+ if (nonZero.length === 0)
2839
+ return [0, 0, 0];
2840
+ const quantile = (ratio) => {
2841
+ const index = Math.floor(ratio * (nonZero.length - 1));
2842
+ return nonZero[index] ?? 0;
2843
+ };
2844
+ return [quantile(0.25), quantile(0.5), quantile(0.75)];
2845
+ }
2846
+ function getHeatmapBlock(tokens, quantiles) {
2847
+ if (tokens <= 0)
2848
+ return HEATMAP_BLOCKS.EMPTY;
2849
+ if (tokens <= quantiles[0])
2850
+ return HEATMAP_BLOCKS.LIGHT;
2851
+ if (tokens <= quantiles[1])
2852
+ return HEATMAP_BLOCKS.MEDIUM;
2853
+ if (tokens <= quantiles[2])
2854
+ return HEATMAP_BLOCKS.DARK;
2855
+ return HEATMAP_BLOCKS.FULL;
2856
+ }
2857
+ function formatDate(date) {
2858
+ const year = date.getUTCFullYear();
2859
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
2860
+ const day = String(date.getUTCDate()).padStart(2, "0");
2861
+ return `${year}-${month}-${day}`;
2862
+ }
2863
+ function buildMonthHeader(weeks) {
2864
+ const header = Array.from({ length: weeks.length * WEEK_COLUMN_WIDTH }, () => " ");
2865
+ let lastMonth = -1;
2866
+ let nextFreeIndex = 0;
2867
+ for (let weekIndex = 0;weekIndex < weeks.length; weekIndex++) {
2868
+ const firstDay = weeks[weekIndex]?.[0];
2869
+ if (!firstDay)
2870
+ continue;
2871
+ const month = firstDay.getUTCMonth();
2872
+ if (month === lastMonth)
2873
+ continue;
2874
+ lastMonth = month;
2875
+ const desiredStart = weekIndex * WEEK_COLUMN_WIDTH;
2876
+ const startIndex = Math.max(desiredStart, nextFreeIndex);
2877
+ const remaining = header.length - startIndex;
2878
+ if (remaining <= 0)
2879
+ continue;
2880
+ const fullLabel = MONTH_LABELS[month] ?? "";
2881
+ const label = remaining >= fullLabel.length ? fullLabel : remaining >= 2 ? fullLabel.slice(0, 2) : fullLabel.slice(0, 1);
2882
+ for (let offset = 0;offset < label.length; offset++) {
2883
+ header[startIndex + offset] = label[offset] ?? " ";
2884
+ }
2885
+ nextFreeIndex = startIndex + label.length + 1;
2521
2886
  }
2522
- const usageMap = buildUsageMap(daily);
2523
- const maxTokens = Math.max(...usageMap.values(), 0);
2524
- const dates = daily.map((d) => d.date).sort();
2525
- const startDate = new Date(dates[0]);
2526
- const endDate = new Date(dates[dates.length - 1]);
2887
+ return `${" ".repeat(DAY_LABEL_WIDTH2)}${header.join("")}`;
2888
+ }
2889
+ function buildWeeks(startDate, endDate) {
2527
2890
  const alignedStart = new Date(startDate);
2528
- alignedStart.setDate(alignedStart.getDate() - alignedStart.getDay());
2891
+ alignedStart.setUTCDate(alignedStart.getUTCDate() - alignedStart.getUTCDay());
2529
2892
  const weeks = [];
2530
- const current = new Date(alignedStart);
2531
- while (current <= endDate) {
2893
+ const cursor = new Date(alignedStart);
2894
+ while (cursor <= endDate) {
2532
2895
  const week = [];
2533
- for (let d = 0;d < 7; d++) {
2534
- week.push(new Date(current));
2535
- current.setDate(current.getDate() + 1);
2896
+ for (let day = 0;day < 7; day++) {
2897
+ week.push(new Date(cursor));
2898
+ cursor.setUTCDate(cursor.getUTCDate() + 1);
2536
2899
  }
2537
2900
  weeks.push(week);
2538
2901
  }
2539
- const availableWidth = options.width - DAY_LABEL_WIDTH3;
2540
- const maxWeeks = Math.min(weeks.length, availableWidth);
2541
- const displayWeeks = weeks.slice(Math.max(0, weeks.length - maxWeeks));
2542
- const lines = [];
2543
- let monthHeader = " ".repeat(DAY_LABEL_WIDTH3);
2544
- let lastMonth = -1;
2545
- for (const week of displayWeeks) {
2546
- const month = week[0].getMonth();
2547
- if (month !== lastMonth) {
2548
- monthHeader += MONTH_LABELS[month];
2549
- lastMonth = month;
2550
- const labelLen = MONTH_LABELS[month].length;
2551
- } else {
2552
- monthHeader += " ";
2553
- }
2554
- }
2555
- if (monthHeader.length > options.width) {
2556
- monthHeader = monthHeader.slice(0, options.width);
2902
+ return weeks;
2903
+ }
2904
+ function renderTerminalHeatmap(daily, options) {
2905
+ if (daily.length === 0) {
2906
+ return " No usage data available.";
2557
2907
  }
2558
- lines.push(monthHeader);
2559
- for (let dayIdx = 0;dayIdx < 7; dayIdx++) {
2560
- const label = dayIdx % 2 === 1 ? DAY_LABELS3[dayIdx] : " ";
2561
- let line = label + " ";
2562
- line = line.slice(0, DAY_LABEL_WIDTH3);
2908
+ const usageMap = buildUsageMap(daily);
2909
+ const quantiles = computeQuantiles2(Array.from(usageMap.values()));
2910
+ const maxTokens = Math.max(...usageMap.values(), 0);
2911
+ const dates = daily.map((entry) => entry.date).sort();
2912
+ const startDate = new Date(`${dates[0]}T00:00:00Z`);
2913
+ const endDate = new Date(`${dates[dates.length - 1]}T00:00:00Z`);
2914
+ const weeks = buildWeeks(startDate, endDate);
2915
+ const availableColumns = Math.max(WEEK_COLUMN_WIDTH, options.width - DAY_LABEL_WIDTH2);
2916
+ const maxWeeks = Math.max(1, Math.floor(availableColumns / WEEK_COLUMN_WIDTH));
2917
+ const displayWeeks = weeks.slice(Math.max(0, weeks.length - maxWeeks));
2918
+ const lines = [buildMonthHeader(displayWeeks)];
2919
+ for (let dayIndex = 0;dayIndex < 7; dayIndex++) {
2920
+ const label = DAY_LABELS2[dayIndex] ?? " ";
2921
+ let line = `${label} `.slice(0, DAY_LABEL_WIDTH2);
2563
2922
  for (const week of displayWeeks) {
2564
- const date = week[dayIdx];
2565
- if (!date || date > endDate || date < startDate) {
2566
- line += " ";
2923
+ const date = week[dayIndex];
2924
+ if (!date || date < startDate || date > endDate) {
2925
+ line += `${HEATMAP_BLOCKS.EMPTY} `;
2567
2926
  continue;
2568
2927
  }
2569
- const dateStr = formatDate(date);
2570
- const tokens = usageMap.get(dateStr) ?? 0;
2571
- const block = intensityBlock(tokens, maxTokens);
2928
+ const dateString = formatDate(date);
2929
+ const tokens = usageMap.get(dateString) ?? 0;
2930
+ const block = getHeatmapBlock(tokens, quantiles);
2572
2931
  const color = intensityColor(tokens, maxTokens);
2573
- line += colorize(block, color, options.noColor);
2932
+ line += `${colorize(block, color, options.noColor)} `;
2574
2933
  }
2575
- lines.push(line);
2934
+ lines.push(line.trimEnd());
2576
2935
  }
2577
- const legendBlocks = [
2936
+ const legend = `${" ".repeat(DAY_LABEL_WIDTH2)}${LEGEND_TEXT} ${[
2578
2937
  HEATMAP_BLOCKS.EMPTY,
2579
2938
  HEATMAP_BLOCKS.LIGHT,
2580
2939
  HEATMAP_BLOCKS.MEDIUM,
2581
2940
  HEATMAP_BLOCKS.DARK,
2582
2941
  HEATMAP_BLOCKS.FULL
2583
- ];
2584
- const legend = `${" ".repeat(DAY_LABEL_WIDTH3)}${LEGEND_TEXT} ${legendBlocks.join("")} ${LEGEND_TEXT_MORE}`;
2942
+ ].join("")} ${LEGEND_TEXT_MORE}`;
2585
2943
  lines.push(legend);
2586
2944
  return lines.join(`
2587
2945
  `);
2588
2946
  }
2589
- function formatDate(date) {
2590
- const y = date.getFullYear();
2591
- const m = String(date.getMonth() + 1).padStart(2, "0");
2592
- const d = String(date.getDate()).padStart(2, "0");
2593
- return `${y}-${m}-${d}`;
2594
- }
2595
2947
 
2596
2948
  // packages/renderers/dist/terminal/dashboard.js
2597
2949
  var BOX_H = "\u2500";
@@ -2686,11 +3038,18 @@ function renderDayOfWeek(stats, width, noColor2) {
2686
3038
  }
2687
3039
  function renderTopModels(stats, width, noColor2) {
2688
3040
  const lines = [];
3041
+ const nameWidth = Math.min(28, Math.max(16, Math.floor(width * 0.35)));
3042
+ const pctWidth = 4;
3043
+ const barGap = 2;
3044
+ const barWidth = Math.max(8, width - nameWidth - pctWidth - 6);
2689
3045
  for (const model of stats.topModels.slice(0, 5)) {
2690
3046
  const pct = formatSharePercent(model.percentage);
2691
- const tokens = formatTokens(model.tokens);
2692
- const line = ` ${colorize(model.model, "yellow", noColor2)} ${tokens} ${pct}`;
2693
- lines.push(line.length > width ? line.slice(0, width) : line);
3047
+ const normalizedName = model.model.length > nameWidth ? `${model.model.slice(0, nameWidth - 1)}\u2026` : model.model;
3048
+ const fillLength = Math.max(1, Math.round(model.percentage / 100 * barWidth));
3049
+ const fill = colorize(BAR_CHAR.repeat(fillLength), "green", noColor2);
3050
+ const track = "\u2591".repeat(Math.max(0, barWidth - fillLength));
3051
+ const line = ` ${colorize(normalizedName.padEnd(nameWidth), "yellow", noColor2)}${" ".repeat(barGap)}${fill}${track}${" ".repeat(barGap)}${pct.padStart(pctWidth)}`;
3052
+ lines.push(line);
2694
3053
  }
2695
3054
  return lines.join(`
2696
3055
  `);
@@ -2757,7 +3116,8 @@ function renderDashboard(output, options) {
2757
3116
  }
2758
3117
  for (let i = 0;i < output.providers.length; i++) {
2759
3118
  const provider = output.providers[i];
2760
- sections.push(renderProviderSection(provider, output.aggregated, width, noColor2, options.showInsights));
3119
+ const providerStats = aggregate(provider.daily, output.dateRange.until);
3120
+ sections.push(renderProviderSection(provider, providerStats, width, noColor2, options.showInsights));
2761
3121
  if (i < output.providers.length - 1) {
2762
3122
  sections.push("");
2763
3123
  sections.push(divider(width));
@@ -2802,7 +3162,7 @@ class TerminalRenderer {
2802
3162
  }
2803
3163
  }
2804
3164
  // packages/renderers/dist/live/template.js
2805
- var MONTH_NAMES3 = [
3165
+ var MONTH_NAMES2 = [
2806
3166
  "Jan",
2807
3167
  "Feb",
2808
3168
  "Mar",
@@ -2858,7 +3218,7 @@ function computeQuantiles3(values) {
2858
3218
  };
2859
3219
  return [q(0.25), q(0.5), q(0.75)];
2860
3220
  }
2861
- function getLevel3(tokens, quantiles) {
3221
+ function getLevel2(tokens, quantiles) {
2862
3222
  if (tokens <= 0)
2863
3223
  return 0;
2864
3224
  if (tokens <= quantiles[0])
@@ -2918,12 +3278,12 @@ function buildHeatmapCells(daily, since, until) {
2918
3278
  const row = current.getUTCDay();
2919
3279
  const dateStr = current.toISOString().slice(0, 10);
2920
3280
  const tokens = tokenMap.get(dateStr) ?? 0;
2921
- const level = getLevel3(tokens, quantiles);
3281
+ const level = getLevel2(tokens, quantiles);
2922
3282
  cells.push({ date: dateStr, tokens, level, row, col });
2923
3283
  const month = current.getUTCMonth();
2924
3284
  if (month !== lastMonth && row === 0) {
2925
3285
  lastMonth = month;
2926
- months.push({ label: MONTH_NAMES3[month] ?? "", col });
3286
+ months.push({ label: MONTH_NAMES2[month] ?? "", col });
2927
3287
  }
2928
3288
  if (row === 6)
2929
3289
  col++;
@@ -2934,21 +3294,17 @@ function buildHeatmapCells(daily, since, until) {
2934
3294
  function renderProviderHeatmapHtml(provider, since, until, isDark, emptyCell) {
2935
3295
  const heatmapColors = buildHeatmapScale2(provider.colors, isDark);
2936
3296
  const { cells, months, totalCols } = buildHeatmapCells(provider.daily, since, until);
2937
- const cellSize = 16;
2938
- const cellGap = 4;
2939
- const dayLabelWidth = 44;
2940
- const monthLabelHeight = 24;
2941
- const heatmapWidth = dayLabelWidth + totalCols * (cellSize + cellGap);
2942
- const heatmapHeight = monthLabelHeight + 7 * (cellSize + cellGap);
3297
+ const heatmapWidth = DAY_LABEL_WIDTH + totalCols * (CELL_SIZE + CELL_GAP);
3298
+ const heatmapHeight = MONTH_LABEL_HEIGHT + 7 * (CELL_SIZE + CELL_GAP);
2943
3299
  const cellsHtml = cells.map((c) => {
2944
- const x = dayLabelWidth + c.col * (cellSize + cellGap);
2945
- const y = monthLabelHeight + c.row * (cellSize + cellGap);
3300
+ const x = DAY_LABEL_WIDTH + c.col * (CELL_SIZE + CELL_GAP);
3301
+ const y = MONTH_LABEL_HEIGHT + c.row * (CELL_SIZE + CELL_GAP);
2946
3302
  const fill = c.level === 0 ? emptyCell : heatmapColors[c.level];
2947
3303
  return `<div class="heatmap-cell" style="left:${x}px;top:${y}px;background:${fill}" data-date="${esc(c.date)}" data-tokens="${c.tokens}"></div>`;
2948
3304
  }).join(`
2949
3305
  `);
2950
3306
  const monthLabelsHtml = months.map((m) => {
2951
- const x = dayLabelWidth + m.col * (cellSize + cellGap);
3307
+ const x = DAY_LABEL_WIDTH + m.col * (CELL_SIZE + CELL_GAP);
2952
3308
  return `<span class="month-label" style="left:${x}px">${esc(m.label)}</span>`;
2953
3309
  }).join(`
2954
3310
  `);
@@ -2958,7 +3314,7 @@ function renderProviderHeatmapHtml(provider, since, until, isDark, emptyCell) {
2958
3314
  { label: "Fri", row: 5 },
2959
3315
  { label: "Sun", row: 0 }
2960
3316
  ].map((d) => {
2961
- const y = monthLabelHeight + d.row * (cellSize + cellGap) + cellSize - 2;
3317
+ const y = MONTH_LABEL_HEIGHT + d.row * (CELL_SIZE + CELL_GAP) + CELL_SIZE - 2;
2962
3318
  return `<span class="day-label" style="top:${y - 10}px">${d.label}</span>`;
2963
3319
  }).join(`
2964
3320
  `);
@@ -3080,8 +3436,8 @@ function generateHtml(output, options) {
3080
3436
  .heatmap-container { position: relative; margin-bottom: 8px; }
3081
3437
  .heatmap-cell {
3082
3438
  position: absolute;
3083
- width: 16px;
3084
- height: 16px;
3439
+ width: ${CELL_SIZE}px;
3440
+ height: ${CELL_SIZE}px;
3085
3441
  border-radius: 3px;
3086
3442
  cursor: pointer;
3087
3443
  }
@@ -3111,11 +3467,17 @@ function generateHtml(output, options) {
3111
3467
  .stat-value.accent { color: ${accent}; }
3112
3468
  .models-section { margin-top: 8px; }
3113
3469
  .models-label { color: ${muted}; font-size: 10px; font-weight: 600; letter-spacing: 2px; margin-bottom: 16px; }
3114
- .model-row { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
3115
- .model-name { color: ${muted}; font-size: 12px; width: 200px; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3116
- .model-bar-track { flex: 1; height: 8px; background: ${barTrack}; border-radius: 4px; overflow: hidden; }
3470
+ .model-row {
3471
+ display: grid;
3472
+ grid-template-columns: ${MODEL_NAME_WIDTH}px minmax(0, 1fr) ${MODEL_PERCENT_WIDTH}px;
3473
+ align-items: center;
3474
+ column-gap: ${MODEL_BAR_GAP}px;
3475
+ margin-bottom: 16px;
3476
+ }
3477
+ .model-name { color: ${muted}; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3478
+ .model-bar-track { width: 100%; height: 8px; background: ${barTrack}; border-radius: 4px; overflow: hidden; }
3117
3479
  .model-bar-fill { height: 100%; border-radius: 4px; background: linear-gradient(90deg, ${accent}44, ${accent}); }
3118
- .model-pct { color: ${muted}; font-size: 12px; width: 40px; text-align: right; flex-shrink: 0; }
3480
+ .model-pct { color: ${muted}; font-size: 12px; text-align: right; }
3119
3481
  .refresh-btn {
3120
3482
  display: inline-flex;
3121
3483
  align-items: center;
@@ -3299,6 +3661,1097 @@ function handleError(error) {
3299
3661
  process.exit(1);
3300
3662
  }
3301
3663
 
3664
+ // packages/cli/src/flags.ts
3665
+ var CLI_FLAG_ORDER = [
3666
+ "format",
3667
+ "theme",
3668
+ "since",
3669
+ "until",
3670
+ "days",
3671
+ "output",
3672
+ "width",
3673
+ "provider",
3674
+ "compare",
3675
+ "upload",
3676
+ "claude",
3677
+ "codex",
3678
+ "openCode",
3679
+ "allProviders",
3680
+ "listProviders",
3681
+ "more",
3682
+ "clipboard",
3683
+ "open",
3684
+ "liveServer",
3685
+ "noColor",
3686
+ "noInsights"
3687
+ ];
3688
+ var CLI_FLAG_NAMES = {
3689
+ format: "--format",
3690
+ theme: "--theme",
3691
+ since: "--since",
3692
+ until: "--until",
3693
+ days: "--days",
3694
+ output: "--output",
3695
+ width: "--width",
3696
+ provider: "--provider",
3697
+ compare: "--compare",
3698
+ upload: "--upload",
3699
+ claude: "--claude",
3700
+ codex: "--codex",
3701
+ openCode: "--open-code",
3702
+ allProviders: "--all-providers",
3703
+ listProviders: "--list-providers",
3704
+ more: "--more",
3705
+ clipboard: "--clipboard",
3706
+ open: "--open",
3707
+ liveServer: "--live-server",
3708
+ noColor: "--no-color",
3709
+ noInsights: "--no-insights"
3710
+ };
3711
+ function buildCliArgTokens(cliArgs) {
3712
+ const tokens = [];
3713
+ for (const key of CLI_FLAG_ORDER) {
3714
+ const value = cliArgs[key];
3715
+ if (value === undefined || value === false || value === null) {
3716
+ continue;
3717
+ }
3718
+ const flag = CLI_FLAG_NAMES[key];
3719
+ if (!flag)
3720
+ continue;
3721
+ tokens.push(flag);
3722
+ if (value !== true) {
3723
+ tokens.push(String(value));
3724
+ }
3725
+ }
3726
+ return tokens;
3727
+ }
3728
+ function buildCliPreview(cliArgs) {
3729
+ const tokens = buildCliArgTokens(cliArgs);
3730
+ return tokens.length === 0 ? "tokenleak" : `tokenleak ${tokens.join(" ")}`;
3731
+ }
3732
+
3733
+ // packages/cli/src/interactive.ts
3734
+ import { emitKeypressEvents } from "readline";
3735
+ import { createInterface } from "readline/promises";
3736
+ var INTERACTIVE_FLAG_LINES = [
3737
+ "-f, --format <format> terminal | png | svg | json",
3738
+ "-t, --theme <theme> dark | light",
3739
+ "-s, --since <date> YYYY-MM-DD start date",
3740
+ "-u, --until <date> YYYY-MM-DD end date",
3741
+ "-d, --days <number> trailing days window",
3742
+ "-o, --output <path> write output to a file",
3743
+ "-w, --width <number> terminal render width",
3744
+ "-p, --provider <list> comma-separated providers",
3745
+ " --claude shortcut for Claude Code",
3746
+ " --codex shortcut for Codex",
3747
+ " --open-code shortcut for Open Code",
3748
+ " --all-providers ignore provider filters",
3749
+ " --list-providers show provider registry",
3750
+ " --compare <range> auto or YYYY-MM-DD..YYYY-MM-DD",
3751
+ " --more richer PNG/SVG stats",
3752
+ " --clipboard copy rendered output",
3753
+ " --open open generated file",
3754
+ " --upload <target> gist",
3755
+ "-L, --live-server local interactive dashboard",
3756
+ " --no-color disable ANSI colors",
3757
+ " --no-insights hide terminal insights",
3758
+ " --help print help",
3759
+ " --version print version"
3760
+ ];
3761
+ var ESC2 = "\x1B[";
3762
+ var RESET = `${ESC2}0m`;
3763
+ var BOLD = `${ESC2}1m`;
3764
+ var DIM = `${ESC2}2m`;
3765
+ var CYAN = `${ESC2}36m`;
3766
+ var GREEN = `${ESC2}32m`;
3767
+ var YELLOW = `${ESC2}33m`;
3768
+ var RED = `${ESC2}31m`;
3769
+ var WHITE = `${ESC2}97m`;
3770
+ var HOME_CLEAR = "\x1B[H\x1B[J";
3771
+ var HIDE_CURSOR = "\x1B[?25l";
3772
+ var SHOW_CURSOR = "\x1B[?25h";
3773
+ var ALT_SCREEN_ON = "\x1B[?1049h";
3774
+ var ALT_SCREEN_OFF = "\x1B[?1049l";
3775
+ var LOADING_TICK_MS = 120;
3776
+ function color(text2, code) {
3777
+ return `${code}${text2}${RESET}`;
3778
+ }
3779
+ function stripAnsi(text2) {
3780
+ return text2.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
3781
+ }
3782
+ function visibleLength(text2) {
3783
+ return stripAnsi(text2).length;
3784
+ }
3785
+ function padVisible(text2, width) {
3786
+ const padding = Math.max(0, width - visibleLength(text2));
3787
+ return text2 + " ".repeat(padding);
3788
+ }
3789
+ function truncateVisible(text2, width) {
3790
+ if (width <= 0)
3791
+ return "";
3792
+ const plain = stripAnsi(text2);
3793
+ if (plain.length <= width)
3794
+ return text2;
3795
+ const limit = width <= 3 ? width : width - 3;
3796
+ let visibleCount = 0;
3797
+ let index = 0;
3798
+ let result = "";
3799
+ let sawAnsi = false;
3800
+ while (index < text2.length && visibleCount < limit) {
3801
+ if (text2[index] === "\x1B") {
3802
+ const match = text2.slice(index).match(/^\x1b\[[0-9;?]*[A-Za-z]/);
3803
+ if (match) {
3804
+ result += match[0];
3805
+ index += match[0].length;
3806
+ sawAnsi = true;
3807
+ continue;
3808
+ }
3809
+ }
3810
+ result += text2[index];
3811
+ index += 1;
3812
+ visibleCount += 1;
3813
+ }
3814
+ if (width <= 3) {
3815
+ return sawAnsi ? `${result}${RESET}` : result;
3816
+ }
3817
+ return sawAnsi ? `${result}...${RESET}` : `${result}...`;
3818
+ }
3819
+ function joinColumns(left, right, totalWidth) {
3820
+ const gutter = 3;
3821
+ const leftWidth = Math.max(42, Math.min(58, Math.floor(totalWidth * 0.44)));
3822
+ const rightWidth = Math.max(36, totalWidth - leftWidth - gutter);
3823
+ const rows = Math.max(left.length, right.length);
3824
+ const lines = [];
3825
+ for (let index = 0;index < rows; index++) {
3826
+ const leftLine = truncateVisible(left[index] ?? "", leftWidth);
3827
+ const rightLine = truncateVisible(right[index] ?? "", rightWidth);
3828
+ lines.push(`${padVisible(leftLine, leftWidth)}${" ".repeat(gutter)}${rightLine}`);
3829
+ }
3830
+ return lines;
3831
+ }
3832
+ function renderRule(width) {
3833
+ return color("-".repeat(width), DIM);
3834
+ }
3835
+ function describeRequest(args) {
3836
+ const output = typeof args["output"] === "string" ? args["output"] : null;
3837
+ if (args["liveServer"]) {
3838
+ return {
3839
+ title: "Live Dashboard",
3840
+ loadingTitle: "Starting live dashboard",
3841
+ loadingDetail: "Launching the local server. Press Ctrl-C in the live view to stop it, then you will return here.",
3842
+ executionMode: "inherit"
3843
+ };
3844
+ }
3845
+ if (args["listProviders"]) {
3846
+ return {
3847
+ title: "Provider Registry",
3848
+ loadingTitle: "Loading provider registry",
3849
+ loadingDetail: "Checking registered providers and current availability.",
3850
+ executionMode: "capture"
3851
+ };
3852
+ }
3853
+ if (args["compare"]) {
3854
+ return {
3855
+ title: "Compare Report",
3856
+ loadingTitle: "Building compare report",
3857
+ loadingDetail: output ? `Computing period deltas and writing the report to ${output}.` : "Computing period deltas for the current and previous windows.",
3858
+ executionMode: "capture"
3859
+ };
3860
+ }
3861
+ switch (args["format"]) {
3862
+ case "json":
3863
+ return {
3864
+ title: "JSON Export",
3865
+ loadingTitle: "Generating JSON report",
3866
+ loadingDetail: output ? `Collecting token usage and writing JSON to ${output}.` : "Collecting token usage and building structured JSON output.",
3867
+ executionMode: "capture"
3868
+ };
3869
+ case "svg":
3870
+ return {
3871
+ title: "SVG Export",
3872
+ loadingTitle: "Rendering SVG",
3873
+ loadingDetail: output ? `Rendering a vector card and writing it to ${output}.` : "Rendering a vector card from your usage data.",
3874
+ executionMode: "capture"
3875
+ };
3876
+ case "png":
3877
+ return {
3878
+ title: "PNG Export",
3879
+ loadingTitle: "Rendering PNG",
3880
+ loadingDetail: output ? `Rendering the PNG card and writing it to ${output}. This can take a few seconds.` : "Rendering the PNG card. This can take a few seconds.",
3881
+ executionMode: "capture"
3882
+ };
3883
+ default:
3884
+ return {
3885
+ title: "Terminal Dashboard",
3886
+ loadingTitle: "Generating terminal dashboard",
3887
+ loadingDetail: "Reading provider logs and aggregating token usage.",
3888
+ executionMode: "capture"
3889
+ };
3890
+ }
3891
+ }
3892
+ function finalizeCliArgs(args) {
3893
+ const finalized = { ...args };
3894
+ const format = finalized["format"];
3895
+ if (finalized["compare"] && (format === "png" || format === "svg")) {
3896
+ finalized["more"] = true;
3897
+ }
3898
+ if (finalized["open"] && finalized["output"] === undefined && typeof format === "string") {
3899
+ if (format === "png" || format === "svg" || format === "json") {
3900
+ finalized["output"] = `tokenleak.${format}`;
3901
+ } else {
3902
+ delete finalized["open"];
3903
+ }
3904
+ }
3905
+ if (format === "png") {
3906
+ delete finalized["clipboard"];
3907
+ delete finalized["upload"];
3908
+ }
3909
+ return finalized;
3910
+ }
3911
+ function createRunCommand(args) {
3912
+ const finalizedArgs = finalizeCliArgs(args);
3913
+ return {
3914
+ type: "run",
3915
+ request: {
3916
+ args: finalizedArgs,
3917
+ preview: buildCliPreview(finalizedArgs),
3918
+ ...describeRequest(finalizedArgs)
3919
+ }
3920
+ };
3921
+ }
3922
+ function renderMenu(options, selectedIndex) {
3923
+ return options.map((option, index) => {
3924
+ const isSelected = index === selectedIndex;
3925
+ const prefix = isSelected ? color(">", GREEN) : " ";
3926
+ const number = isSelected ? color(option.digit, WHITE + BOLD) : color(option.digit, YELLOW);
3927
+ const title = isSelected ? color(option.title, WHITE + BOLD) : color(option.title, WHITE);
3928
+ const description = isSelected ? color(option.description, CYAN) : color(option.description, DIM);
3929
+ return `${prefix} [${number}] ${title} ${description}`;
3930
+ });
3931
+ }
3932
+ function renderFlagPanel() {
3933
+ return [
3934
+ color("All Flags", WHITE + BOLD),
3935
+ color("Every flag remains available while using the launcher.", DIM),
3936
+ "",
3937
+ ...INTERACTIVE_FLAG_LINES.map((line) => color(line, CYAN))
3938
+ ];
3939
+ }
3940
+ function renderMenuPanel(context, options, selectedIndex) {
3941
+ const selected = options[selectedIndex];
3942
+ return [
3943
+ color("Tokenleak Interactive Launcher", WHITE + BOLD),
3944
+ `${color(`v${context.version}`, YELLOW)} ${color("interactive command center", CYAN)}`,
3945
+ "",
3946
+ color("Arrow keys move. Number keys jump directly. Enter runs the selected action.", DIM),
3947
+ color("Commands run inside this session, so you can keep selecting without leaving tokenleak.", DIM),
3948
+ "",
3949
+ ...renderMenu(options, selectedIndex),
3950
+ "",
3951
+ color("Preview", WHITE + BOLD),
3952
+ color(selected.preview, GREEN),
3953
+ "",
3954
+ color("Keys", WHITE + BOLD),
3955
+ `${color("Up/Down", YELLOW)} move ${color("Enter", YELLOW)} run ${color("H", YELLOW)} help ${color("Q", YELLOW)} quit`,
3956
+ "",
3957
+ renderRule(44)
3958
+ ];
3959
+ }
3960
+ function renderHelpOverlay(helpText, width) {
3961
+ const lines = helpText.trimEnd().split(`
3962
+ `);
3963
+ const header = [
3964
+ color("Tokenleak Help", WHITE + BOLD),
3965
+ color("Press Enter, Escape, H, or Q to return to the launcher.", DIM),
3966
+ ""
3967
+ ];
3968
+ return `${HOME_CLEAR}${HIDE_CURSOR}${[...header, ...lines.map((line) => truncateVisible(line, width))].join(`
3969
+ `)}`;
3970
+ }
3971
+ function renderLauncher(context, options, selectedIndex) {
3972
+ const width = process.stdout.columns ?? 120;
3973
+ const menuPanel = renderMenuPanel(context, options, selectedIndex);
3974
+ const flagPanel = renderFlagPanel();
3975
+ const body = width >= 118 ? joinColumns(menuPanel, flagPanel, width) : [...menuPanel, "", ...flagPanel];
3976
+ return `${HOME_CLEAR}${HIDE_CURSOR}${body.join(`
3977
+ `)}`;
3978
+ }
3979
+ function renderProgressBar(frame, width) {
3980
+ const innerWidth = Math.max(12, width - 2);
3981
+ const headSize = Math.max(4, Math.floor(innerWidth / 5));
3982
+ const travel = Math.max(1, innerWidth - headSize);
3983
+ const cycle = travel * 2;
3984
+ const offset = frame % cycle;
3985
+ const start = offset <= travel ? offset : cycle - offset;
3986
+ const cells = Array.from({ length: innerWidth }, (_, index) => {
3987
+ if (index >= start && index < start + headSize) {
3988
+ return "=";
3989
+ }
3990
+ return "-";
3991
+ }).join("");
3992
+ return color(`[${cells}]`, CYAN);
3993
+ }
3994
+ function renderLoading(request, frame = 0, startedAt = Date.now()) {
3995
+ const elapsedSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1000));
3996
+ const progressBar = renderProgressBar(frame, 34);
3997
+ const lines = [
3998
+ color(request.loadingTitle, WHITE + BOLD),
3999
+ color(request.preview, CYAN),
4000
+ "",
4001
+ color(request.loadingDetail, DIM),
4002
+ "",
4003
+ color("Status", WHITE + BOLD),
4004
+ color("Working... stay in tokenleak while this finishes.", YELLOW),
4005
+ "",
4006
+ color("Progress", WHITE + BOLD),
4007
+ progressBar,
4008
+ color(`Elapsed ${elapsedSeconds}s`, DIM),
4009
+ "",
4010
+ renderRule(44)
4011
+ ];
4012
+ return `${HOME_CLEAR}${HIDE_CURSOR}${lines.join(`
4013
+ `)}`;
4014
+ }
4015
+ function clipOutputLines(lines, limit) {
4016
+ if (limit <= 0)
4017
+ return [];
4018
+ if (lines.length <= limit)
4019
+ return lines;
4020
+ const visible = lines.slice(0, Math.max(0, limit - 1));
4021
+ visible.push(color(`... ${lines.length - visible.length} more lines hidden`, DIM));
4022
+ return visible;
4023
+ }
4024
+ function renderOutputSection(title, content, width, maxLines) {
4025
+ const normalized = content.trimEnd();
4026
+ if (!normalized)
4027
+ return [];
4028
+ const lines = normalized.split(`
4029
+ `).map((line) => truncateVisible(line, width));
4030
+ return [
4031
+ color(title, WHITE + BOLD),
4032
+ ...clipOutputLines(lines, maxLines),
4033
+ ""
4034
+ ];
4035
+ }
4036
+ function renderResult(request, result) {
4037
+ const width = Math.max(60, (process.stdout.columns ?? 120) - 1);
4038
+ const rows = process.stdout.rows ?? 40;
4039
+ const statusColor = result.ok ? GREEN : RED;
4040
+ const statusLabel = result.ok ? "Completed" : "Failed";
4041
+ const fixedLines = 10;
4042
+ const outputBudget = Math.max(8, rows - fixedLines);
4043
+ const firstSectionLines = result.stdout.trim() && result.stderr.trim() ? Math.max(4, Math.floor(outputBudget * 0.6)) : outputBudget;
4044
+ const secondSectionLines = Math.max(4, outputBudget - firstSectionLines);
4045
+ const body = [
4046
+ color(request.title, WHITE + BOLD),
4047
+ color(request.preview, CYAN),
4048
+ "",
4049
+ `${color("Status", WHITE + BOLD)} ${color(statusLabel, statusColor)}`,
4050
+ color(result.summary, DIM),
4051
+ "",
4052
+ ...renderOutputSection("Output", result.stdout, width, firstSectionLines),
4053
+ ...renderOutputSection("Messages", result.stderr, width, secondSectionLines),
4054
+ renderRule(44),
4055
+ `${color("Enter", YELLOW)} launcher ${color("Q", YELLOW)} quit`
4056
+ ];
4057
+ return `${HOME_CLEAR}${HIDE_CURSOR}${body.join(`
4058
+ `)}`;
4059
+ }
4060
+ function enterAltScreen() {
4061
+ process.stdout.write(`${ALT_SCREEN_ON}${HOME_CLEAR}${HIDE_CURSOR}`);
4062
+ }
4063
+ function leaveAltScreen() {
4064
+ process.stdout.write(`${SHOW_CURSOR}${ALT_SCREEN_OFF}`);
4065
+ }
4066
+ function paint(content) {
4067
+ process.stdout.write(content);
4068
+ }
4069
+ function suspendRawMode() {
4070
+ if (process.stdin.isTTY) {
4071
+ process.stdin.setRawMode(false);
4072
+ }
4073
+ process.stdin.pause();
4074
+ process.stdout.write(SHOW_CURSOR);
4075
+ }
4076
+ function resumeRawMode() {
4077
+ if (process.stdin.isTTY) {
4078
+ process.stdin.setRawMode(true);
4079
+ }
4080
+ process.stdin.resume();
4081
+ process.stdout.write(HIDE_CURSOR);
4082
+ }
4083
+
4084
+ class InteractiveExitError extends Error {
4085
+ constructor() {
4086
+ super("Interactive session cancelled");
4087
+ this.name = "InteractiveExitError";
4088
+ }
4089
+ }
4090
+ var PROVIDER_CHOICES = [
4091
+ { value: "claude-code", label: "Claude Code", description: "Anthropic project logs" },
4092
+ { value: "codex", label: "Codex", description: "OpenAI session logs" },
4093
+ { value: "open-code", label: "Open Code", description: "Open Code storage and database" }
4094
+ ];
4095
+ function isInteractiveExitError(error) {
4096
+ return error instanceof InteractiveExitError;
4097
+ }
4098
+ function parsePositiveInteger(value) {
4099
+ const trimmed = value.trim();
4100
+ if (trimmed === "")
4101
+ return null;
4102
+ const parsed = Number(trimmed);
4103
+ if (!Number.isInteger(parsed) || parsed <= 0) {
4104
+ throw new Error(`Expected a positive whole number, received "${value}".`);
4105
+ }
4106
+ return parsed;
4107
+ }
4108
+ function renderChoiceScreen(title, description, options, selectedIndex, selectedValues, footer = `${color("Up/Down", YELLOW)} move ${color("Space", YELLOW)} toggle ${color("Enter", YELLOW)} confirm ${color("Ctrl-C", YELLOW)} exit`) {
4109
+ const optionLines = options.map((option, index) => {
4110
+ const isSelected = index === selectedIndex;
4111
+ const isChecked = selectedValues ? selectedValues.has(option.value) : isSelected;
4112
+ const pointer = isSelected ? color(">", GREEN) : " ";
4113
+ const checkbox = selectedValues ? isChecked ? color("[x]", GREEN) : color("[ ]", DIM) : isSelected ? color("[\u2022]", GREEN) : color("[ ]", DIM);
4114
+ const titleColor = isSelected ? WHITE + BOLD : WHITE;
4115
+ const descriptionColor = isSelected ? CYAN : DIM;
4116
+ return `${pointer} ${checkbox} ${color(option.label, titleColor)} ${color(option.description, descriptionColor)}`;
4117
+ });
4118
+ const lines = [
4119
+ color(title, WHITE + BOLD),
4120
+ color(description, DIM),
4121
+ "",
4122
+ ...optionLines,
4123
+ "",
4124
+ renderRule(44),
4125
+ footer
4126
+ ];
4127
+ return `${HOME_CLEAR}${HIDE_CURSOR}${lines.join(`
4128
+ `)}`;
4129
+ }
4130
+ async function ask(prompt, initialValue = "") {
4131
+ const readline = createInterface({
4132
+ input: process.stdin,
4133
+ output: process.stdout
4134
+ });
4135
+ let settled = false;
4136
+ return new Promise((resolve, reject) => {
4137
+ const finish = (fn) => {
4138
+ if (settled)
4139
+ return;
4140
+ settled = true;
4141
+ readline.off("SIGINT", onSigint);
4142
+ readline.close();
4143
+ fn();
4144
+ };
4145
+ const onSigint = () => {
4146
+ finish(() => reject(new InteractiveExitError));
4147
+ };
4148
+ readline.on("SIGINT", onSigint);
4149
+ const suffix = initialValue ? ` (${initialValue})` : "";
4150
+ readline.question(`${prompt}${suffix}: `).then((value) => {
4151
+ finish(() => resolve(value.trim() || initialValue));
4152
+ }).catch((error) => {
4153
+ finish(() => reject(error));
4154
+ });
4155
+ });
4156
+ }
4157
+ async function askYesNo(prompt, defaultValue = false) {
4158
+ const hint = defaultValue ? "Y/n" : "y/N";
4159
+ const value = (await ask(`${prompt} [${hint}]`)).toLowerCase();
4160
+ if (value === "")
4161
+ return defaultValue;
4162
+ return value === "y" || value === "yes";
4163
+ }
4164
+ async function promptSingleChoice(title, description, options, initialIndex = 0) {
4165
+ return new Promise((resolve, reject) => {
4166
+ let selectedIndex = Math.max(0, Math.min(initialIndex, options.length - 1));
4167
+ const onKeypress = (_input, key) => {
4168
+ if (key.ctrl && key.name === "c") {
4169
+ cleanup();
4170
+ reject(new InteractiveExitError);
4171
+ return;
4172
+ }
4173
+ if (key.name === "up") {
4174
+ selectedIndex = (selectedIndex - 1 + options.length) % options.length;
4175
+ render();
4176
+ return;
4177
+ }
4178
+ if (key.name === "down") {
4179
+ selectedIndex = (selectedIndex + 1) % options.length;
4180
+ render();
4181
+ return;
4182
+ }
4183
+ const digit = key.sequence?.match(/^[1-9]$/)?.[0];
4184
+ if (digit) {
4185
+ const index = Number(digit) - 1;
4186
+ if (index < options.length) {
4187
+ selectedIndex = index;
4188
+ cleanup();
4189
+ resolve(options[selectedIndex].value);
4190
+ }
4191
+ return;
4192
+ }
4193
+ if (key.name === "return" || key.name === "enter") {
4194
+ cleanup();
4195
+ resolve(options[selectedIndex].value);
4196
+ }
4197
+ };
4198
+ function render() {
4199
+ paint(renderChoiceScreen(title, description, options, selectedIndex, undefined, `${color("Up/Down", YELLOW)} move ${color("1-9", YELLOW)} pick ${color("Enter", YELLOW)} confirm ${color("Ctrl-C", YELLOW)} exit`));
4200
+ }
4201
+ function cleanup() {
4202
+ process.stdin.off("keypress", onKeypress);
4203
+ suspendRawMode();
4204
+ }
4205
+ render();
4206
+ resumeRawMode();
4207
+ process.stdin.on("keypress", onKeypress);
4208
+ });
4209
+ }
4210
+ async function promptMultiChoice(title, description, options, initialValues = []) {
4211
+ return new Promise((resolve, reject) => {
4212
+ let selectedIndex = 0;
4213
+ const selectedValues = new Set(initialValues);
4214
+ const onKeypress = (_input, key) => {
4215
+ if (key.ctrl && key.name === "c") {
4216
+ cleanup();
4217
+ reject(new InteractiveExitError);
4218
+ return;
4219
+ }
4220
+ if (key.name === "up") {
4221
+ selectedIndex = (selectedIndex - 1 + options.length) % options.length;
4222
+ render();
4223
+ return;
4224
+ }
4225
+ if (key.name === "down") {
4226
+ selectedIndex = (selectedIndex + 1) % options.length;
4227
+ render();
4228
+ return;
4229
+ }
4230
+ if (key.name === "space") {
4231
+ toggleSelected(selectedIndex);
4232
+ render();
4233
+ return;
4234
+ }
4235
+ const digit = key.sequence?.match(/^[1-9]$/)?.[0];
4236
+ if (digit) {
4237
+ const index = Number(digit) - 1;
4238
+ if (index < options.length) {
4239
+ selectedIndex = index;
4240
+ toggleSelected(selectedIndex);
4241
+ render();
4242
+ }
4243
+ return;
4244
+ }
4245
+ if (key.name === "return" || key.name === "enter") {
4246
+ cleanup();
4247
+ resolve(Array.from(selectedValues));
4248
+ }
4249
+ };
4250
+ function toggleSelected(index) {
4251
+ const value = options[index].value;
4252
+ if (selectedValues.has(value)) {
4253
+ selectedValues.delete(value);
4254
+ } else {
4255
+ selectedValues.add(value);
4256
+ }
4257
+ }
4258
+ function render() {
4259
+ paint(renderChoiceScreen(title, description, options, selectedIndex, selectedValues));
4260
+ }
4261
+ function cleanup() {
4262
+ process.stdin.off("keypress", onKeypress);
4263
+ suspendRawMode();
4264
+ }
4265
+ render();
4266
+ resumeRawMode();
4267
+ process.stdin.on("keypress", onKeypress);
4268
+ });
4269
+ }
4270
+ function applySelectedProviders(args, providers) {
4271
+ if (providers.length === 0)
4272
+ return;
4273
+ args["provider"] = providers.join(",");
4274
+ }
4275
+ async function promptTheme(defaultTheme = "dark") {
4276
+ const theme = await promptSingleChoice("Theme", "Pick the rendering theme.", [
4277
+ { value: "dark", label: "Dark", description: "High-contrast dark canvas" },
4278
+ { value: "light", label: "Light", description: "Bright export with light background" }
4279
+ ], defaultTheme === "light" ? 1 : 0);
4280
+ return theme;
4281
+ }
4282
+ async function promptDateWindow() {
4283
+ const choice = await promptSingleChoice("Date Window", "Choose how much history to include.", [
4284
+ { value: "7", label: "Last 7 days", description: "Quick recent snapshot" },
4285
+ { value: "30", label: "Last 30 days", description: "Short-term trend window" },
4286
+ { value: "90", label: "Last 90 days", description: "Default overview" },
4287
+ { value: "365", label: "Last 365 days", description: "Long-range usage pattern" },
4288
+ { value: "custom", label: "Custom range", description: "Enter exact dates manually" }
4289
+ ], 2);
4290
+ if (choice !== "custom") {
4291
+ return { days: Number(choice) };
4292
+ }
4293
+ const since = await ask("Since date YYYY-MM-DD");
4294
+ const until = await ask("Until date YYYY-MM-DD (blank for today)");
4295
+ const args = { since };
4296
+ if (until)
4297
+ args["until"] = until;
4298
+ return args;
4299
+ }
4300
+ async function promptProviderSelection(title = "Provider Filter") {
4301
+ return promptMultiChoice(title, "Toggle one or more providers. Leave everything unchecked to use auto-detection.", PROVIDER_CHOICES);
4302
+ }
4303
+ async function promptOutputPath(defaultPath) {
4304
+ return ask("Output file", defaultPath);
4305
+ }
4306
+ async function promptWidth() {
4307
+ const choice = await promptSingleChoice("Terminal Width", "Choose the dashboard width.", [
4308
+ { value: "80", label: "80 columns", description: "Standard terminal width" },
4309
+ { value: "100", label: "100 columns", description: "Balanced dashboard layout" },
4310
+ { value: "120", label: "120 columns", description: "Wide dashboard layout" },
4311
+ { value: "custom", label: "Custom width", description: "Enter an exact width" }
4312
+ ], 1);
4313
+ if (choice !== "custom") {
4314
+ return Number(choice);
4315
+ }
4316
+ while (true) {
4317
+ try {
4318
+ const parsed = parsePositiveInteger(await ask("Custom width"));
4319
+ if (parsed !== null) {
4320
+ return parsed;
4321
+ }
4322
+ paint(`${HOME_CLEAR}${SHOW_CURSOR}${color("Width required", RED)}
4323
+ ${color("Enter a positive whole number to continue.", DIM)}
4324
+
4325
+ Press Enter to try again.`);
4326
+ await ask("");
4327
+ } catch (error) {
4328
+ paint(`${HOME_CLEAR}${SHOW_CURSOR}${color("Invalid width", RED)}
4329
+ ${color(error instanceof Error ? error.message : String(error), DIM)}
4330
+
4331
+ Press Enter to try again.`);
4332
+ await ask("");
4333
+ }
4334
+ }
4335
+ }
4336
+ async function promptCompareSetting() {
4337
+ const choice = await promptSingleChoice("Compare Mode", "Optionally compare the current range against an earlier period.", [
4338
+ { value: "off", label: "No compare", description: "Render a standard single-period report" },
4339
+ { value: "auto", label: "Auto compare", description: "Split the selected window automatically" },
4340
+ { value: "custom", label: "Custom compare range", description: "Provide an explicit YYYY-MM-DD..YYYY-MM-DD range" }
4341
+ ]);
4342
+ if (choice === "off")
4343
+ return null;
4344
+ if (choice === "auto")
4345
+ return "auto";
4346
+ return ask("Previous range YYYY-MM-DD..YYYY-MM-DD");
4347
+ }
4348
+ async function buildDashboardPreset() {
4349
+ const rangeArgs = await promptDateWindow();
4350
+ const providers = await promptProviderSelection();
4351
+ const width = await promptWidth();
4352
+ const noInsights = await askYesNo("Hide insights panel", false);
4353
+ const noColor2 = await askYesNo("Disable ANSI colors", false);
4354
+ const args = { ...rangeArgs };
4355
+ applySelectedProviders(args, providers);
4356
+ if (width)
4357
+ args["width"] = width;
4358
+ if (noInsights)
4359
+ args["noInsights"] = true;
4360
+ if (noColor2)
4361
+ args["noColor"] = true;
4362
+ return createRunCommand(args);
4363
+ }
4364
+ async function buildJsonPreset() {
4365
+ const rangeArgs = await promptDateWindow();
4366
+ const providers = await promptProviderSelection();
4367
+ const compare = await promptCompareSetting();
4368
+ const saveToFile = await askYesNo("Write JSON to a file", false);
4369
+ const clipboard = !saveToFile && await askYesNo("Copy JSON to clipboard after render", false);
4370
+ const args = {
4371
+ format: "json",
4372
+ ...rangeArgs
4373
+ };
4374
+ applySelectedProviders(args, providers);
4375
+ if (compare)
4376
+ args["compare"] = compare;
4377
+ if (saveToFile) {
4378
+ args["output"] = await promptOutputPath(compare ? "tokenleak-compare.json" : "tokenleak.json");
4379
+ }
4380
+ if (clipboard)
4381
+ args["clipboard"] = true;
4382
+ return createRunCommand(args);
4383
+ }
4384
+ async function buildImagePreset(format) {
4385
+ const theme = await promptTheme();
4386
+ const rangeArgs = await promptDateWindow();
4387
+ const providers = await promptProviderSelection("Provider Filter");
4388
+ const compare = await promptCompareSetting();
4389
+ const output = await promptOutputPath(`tokenleak.${format}`);
4390
+ const shouldOpen = await askYesNo("Open the file when done", true);
4391
+ const more = compare ? true : await askYesNo("Enable --more stats", format === "png");
4392
+ const args = {
4393
+ format,
4394
+ theme,
4395
+ output,
4396
+ open: shouldOpen,
4397
+ more,
4398
+ ...rangeArgs
4399
+ };
4400
+ applySelectedProviders(args, providers);
4401
+ if (compare)
4402
+ args["compare"] = compare;
4403
+ return createRunCommand(args);
4404
+ }
4405
+ async function buildComparePreset() {
4406
+ const rangeArgs = await promptDateWindow();
4407
+ const providers = await promptProviderSelection();
4408
+ const compareMode = await promptSingleChoice("Reference Period", "Choose how the earlier comparison period should be defined.", [
4409
+ { value: "auto", label: "Auto compare", description: "Split the chosen window automatically" },
4410
+ { value: "custom", label: "Custom compare range", description: "Enter an explicit prior range manually" }
4411
+ ]);
4412
+ const compare = compareMode === "custom" ? await ask("Previous range YYYY-MM-DD..YYYY-MM-DD") : "auto";
4413
+ const saveToFile = await askYesNo("Write compare output to a file", false);
4414
+ const args = {
4415
+ format: "json",
4416
+ compare,
4417
+ ...rangeArgs
4418
+ };
4419
+ applySelectedProviders(args, providers);
4420
+ if (saveToFile) {
4421
+ args["output"] = await promptOutputPath("tokenleak-compare.json");
4422
+ }
4423
+ return createRunCommand(args);
4424
+ }
4425
+ async function buildLivePreset() {
4426
+ const theme = await promptTheme();
4427
+ const rangeArgs = await promptDateWindow();
4428
+ const providers = await promptProviderSelection();
4429
+ const more = await askYesNo("Enable --more stats", true);
4430
+ const args = {
4431
+ liveServer: true,
4432
+ theme,
4433
+ more,
4434
+ ...rangeArgs
4435
+ };
4436
+ applySelectedProviders(args, providers);
4437
+ return createRunCommand(args);
4438
+ }
4439
+ async function askFormatChoice() {
4440
+ return promptSingleChoice("Output Format", "Choose the primary renderer for this command.", [
4441
+ { value: "terminal", label: "Terminal", description: "Dashboard in the current terminal" },
4442
+ { value: "json", label: "JSON", description: "Structured machine-readable output" },
4443
+ { value: "svg", label: "SVG", description: "Shareable vector export" },
4444
+ { value: "png", label: "PNG", description: "Raster export for social and docs" }
4445
+ ]);
4446
+ }
4447
+ async function buildCustomCommand() {
4448
+ const mode = await promptSingleChoice("Command Type", "Choose the command family you want to configure.", [
4449
+ { value: "run", label: "Standard command", description: "Render terminal, JSON, SVG, or PNG output" },
4450
+ { value: "live-server", label: "Live server", description: "Launch the browser dashboard locally" },
4451
+ { value: "list-providers", label: "List providers", description: "Inspect registered provider backends" }
4452
+ ]);
4453
+ if (mode === "live-server") {
4454
+ return buildLivePreset();
4455
+ }
4456
+ if (mode === "list-providers") {
4457
+ return createRunCommand({ listProviders: true });
4458
+ }
4459
+ const format = await askFormatChoice();
4460
+ const theme = format === "terminal" ? null : await promptTheme();
4461
+ const rangeArgs = await promptDateWindow();
4462
+ const providers = await promptProviderSelection();
4463
+ const compare = await promptCompareSetting();
4464
+ const width = format === "terminal" ? await promptWidth() : null;
4465
+ const output = format === "terminal" ? await ask("Output file (blank keeps stdout)") : format === "json" ? await ask("Output file (blank keeps stdout)") : await ask("Output file", `tokenleak.${format}`);
4466
+ const noColor2 = await askYesNo("Disable ANSI colors", false);
4467
+ const noInsights = format === "terminal" ? await askYesNo("Hide insights", false) : false;
4468
+ const more = await askYesNo("Enable --more stats", format === "png" || format === "svg");
4469
+ const clipboard = format !== "png" ? await askYesNo("Copy output to clipboard", false) : false;
4470
+ const open = format !== "terminal" ? await askYesNo("Open generated file", false) : false;
4471
+ const upload = format !== "png" ? await ask("Upload target [blank/gist]") : "";
4472
+ const args = {
4473
+ format,
4474
+ ...rangeArgs
4475
+ };
4476
+ if (theme)
4477
+ args["theme"] = theme;
4478
+ if (compare)
4479
+ args["compare"] = compare;
4480
+ if (width)
4481
+ args["width"] = width;
4482
+ if (output)
4483
+ args["output"] = output;
4484
+ if (noColor2)
4485
+ args["noColor"] = true;
4486
+ if (noInsights)
4487
+ args["noInsights"] = true;
4488
+ if (more)
4489
+ args["more"] = true;
4490
+ if (clipboard)
4491
+ args["clipboard"] = true;
4492
+ if (open)
4493
+ args["open"] = true;
4494
+ if (upload)
4495
+ args["upload"] = upload;
4496
+ applySelectedProviders(args, providers);
4497
+ return createRunCommand(args);
4498
+ }
4499
+ function createMenuOptions() {
4500
+ return [
4501
+ {
4502
+ digit: "1",
4503
+ title: "Launch Dashboard",
4504
+ description: "guided terminal view",
4505
+ preview: "tokenleak --days 90",
4506
+ select: buildDashboardPreset
4507
+ },
4508
+ {
4509
+ digit: "2",
4510
+ title: "Export JSON",
4511
+ description: "structured output for scripts",
4512
+ preview: "tokenleak --format json",
4513
+ select: buildJsonPreset
4514
+ },
4515
+ {
4516
+ digit: "3",
4517
+ title: "Export SVG",
4518
+ description: "shareable vector card",
4519
+ preview: "tokenleak --format svg --output tokenleak.svg",
4520
+ select: async () => buildImagePreset("svg")
4521
+ },
4522
+ {
4523
+ digit: "4",
4524
+ title: "Export PNG",
4525
+ description: "social-ready raster image",
4526
+ preview: "tokenleak --format png --output tokenleak.png --more",
4527
+ select: async () => buildImagePreset("png")
4528
+ },
4529
+ {
4530
+ digit: "5",
4531
+ title: "Compare Periods",
4532
+ description: "diff current vs previous usage",
4533
+ preview: "tokenleak --compare auto --format json",
4534
+ select: buildComparePreset
4535
+ },
4536
+ {
4537
+ digit: "6",
4538
+ title: "Start Live Server",
4539
+ description: "browser dashboard on localhost",
4540
+ preview: "tokenleak --live-server --theme dark",
4541
+ select: buildLivePreset
4542
+ },
4543
+ {
4544
+ digit: "7",
4545
+ title: "Build Custom Command",
4546
+ description: "configure flags interactively",
4547
+ preview: "tokenleak --format terminal --days 90",
4548
+ select: buildCustomCommand
4549
+ },
4550
+ {
4551
+ digit: "8",
4552
+ title: "Full Help",
4553
+ description: "examples and complete usage",
4554
+ preview: "tokenleak --help",
4555
+ select: async () => ({ type: "show-help" })
4556
+ },
4557
+ {
4558
+ digit: "9",
4559
+ title: "List Providers",
4560
+ description: "detect available registries",
4561
+ preview: "tokenleak --list-providers",
4562
+ select: async () => createRunCommand({ listProviders: true })
4563
+ },
4564
+ {
4565
+ digit: "0",
4566
+ title: "Exit",
4567
+ description: "close the launcher",
4568
+ preview: "exit",
4569
+ select: async () => ({ type: "exit" })
4570
+ }
4571
+ ];
4572
+ }
4573
+ async function waitForSingleKey() {
4574
+ return new Promise((resolve) => {
4575
+ const onKeypress = (_input, key) => {
4576
+ process.stdin.off("keypress", onKeypress);
4577
+ suspendRawMode();
4578
+ resolve(key);
4579
+ };
4580
+ resumeRawMode();
4581
+ process.stdin.on("keypress", onKeypress);
4582
+ });
4583
+ }
4584
+ async function promptForMenuCommand(context, options, state) {
4585
+ let showHelp = false;
4586
+ let resolving = false;
4587
+ return new Promise((resolve, reject) => {
4588
+ const onKeypress = async (_input, key) => {
4589
+ if (resolving) {
4590
+ return;
4591
+ }
4592
+ if (key.ctrl && key.name === "c") {
4593
+ cleanup();
4594
+ resolve({ type: "exit" });
4595
+ return;
4596
+ }
4597
+ if (showHelp) {
4598
+ if (key.name === "escape" || key.name === "return" || key.name === "enter" || key.name === "q" || key.name === "h") {
4599
+ showHelp = false;
4600
+ paint(renderLauncher(context, options, state.selectedIndex));
4601
+ }
4602
+ return;
4603
+ }
4604
+ if (key.name === "up") {
4605
+ state.selectedIndex = (state.selectedIndex - 1 + options.length) % options.length;
4606
+ paint(renderLauncher(context, options, state.selectedIndex));
4607
+ return;
4608
+ }
4609
+ if (key.name === "down") {
4610
+ state.selectedIndex = (state.selectedIndex + 1) % options.length;
4611
+ paint(renderLauncher(context, options, state.selectedIndex));
4612
+ return;
4613
+ }
4614
+ if (key.name === "h") {
4615
+ showHelp = true;
4616
+ paint(renderHelpOverlay(context.helpText, Math.max(60, (process.stdout.columns ?? 120) - 1)));
4617
+ return;
4618
+ }
4619
+ if (key.name === "q" || key.name === "escape") {
4620
+ cleanup();
4621
+ resolve({ type: "exit" });
4622
+ return;
4623
+ }
4624
+ const digit = key.sequence?.match(/^[0-9]$/)?.[0];
4625
+ if (digit) {
4626
+ const nextIndex = options.findIndex((option) => option.digit === digit);
4627
+ if (nextIndex >= 0) {
4628
+ state.selectedIndex = nextIndex;
4629
+ if (digit === "8") {
4630
+ showHelp = true;
4631
+ paint(renderHelpOverlay(context.helpText, Math.max(60, (process.stdout.columns ?? 120) - 1)));
4632
+ return;
4633
+ }
4634
+ resolving = true;
4635
+ cleanup();
4636
+ try {
4637
+ const command = await options[nextIndex].select();
4638
+ resolve(command);
4639
+ } catch (error) {
4640
+ if (isInteractiveExitError(error)) {
4641
+ resolve({ type: "exit" });
4642
+ return;
4643
+ }
4644
+ reject(error);
4645
+ }
4646
+ }
4647
+ return;
4648
+ }
4649
+ if (key.name === "return" || key.name === "enter") {
4650
+ if (options[state.selectedIndex].digit === "8") {
4651
+ showHelp = true;
4652
+ paint(renderHelpOverlay(context.helpText, Math.max(60, (process.stdout.columns ?? 120) - 1)));
4653
+ return;
4654
+ }
4655
+ resolving = true;
4656
+ cleanup();
4657
+ try {
4658
+ const command = await options[state.selectedIndex].select();
4659
+ resolve(command);
4660
+ } catch (error) {
4661
+ if (isInteractiveExitError(error)) {
4662
+ resolve({ type: "exit" });
4663
+ return;
4664
+ }
4665
+ reject(error);
4666
+ }
4667
+ }
4668
+ };
4669
+ function cleanup() {
4670
+ process.stdin.off("keypress", onKeypress);
4671
+ suspendRawMode();
4672
+ }
4673
+ paint(renderLauncher(context, options, state.selectedIndex));
4674
+ resumeRawMode();
4675
+ process.stdin.on("keypress", onKeypress);
4676
+ });
4677
+ }
4678
+ async function showExecutionResult(request, result) {
4679
+ paint(renderResult(request, result));
4680
+ const key = await waitForSingleKey();
4681
+ if (key.ctrl && key.name === "c")
4682
+ return "exit";
4683
+ if (key.name === "q" || key.name === "escape")
4684
+ return "exit";
4685
+ return "menu";
4686
+ }
4687
+ function shouldStartInteractiveCli(argv, stdinIsTTY, stdoutIsTTY) {
4688
+ return argv.length === 0 && stdinIsTTY && stdoutIsTTY;
4689
+ }
4690
+ async function startInteractiveCli(context, execute) {
4691
+ const options = createMenuOptions();
4692
+ const state = { selectedIndex: 0 };
4693
+ let interrupted = false;
4694
+ let ignoreSigint = false;
4695
+ const onSigint = () => {
4696
+ if (!ignoreSigint) {
4697
+ interrupted = true;
4698
+ }
4699
+ };
4700
+ emitKeypressEvents(process.stdin);
4701
+ enterAltScreen();
4702
+ process.on("SIGINT", onSigint);
4703
+ try {
4704
+ while (true) {
4705
+ if (interrupted) {
4706
+ return;
4707
+ }
4708
+ const command = await promptForMenuCommand(context, options, state);
4709
+ if (command.type === "exit") {
4710
+ return;
4711
+ }
4712
+ if (command.type === "show-help") {
4713
+ continue;
4714
+ }
4715
+ const startedAt = Date.now();
4716
+ let loadingFrame = 0;
4717
+ paint(renderLoading(command.request, loadingFrame, startedAt));
4718
+ const loadingTicker = setInterval(() => {
4719
+ loadingFrame += 1;
4720
+ paint(renderLoading(command.request, loadingFrame, startedAt));
4721
+ }, LOADING_TICK_MS);
4722
+ let result;
4723
+ try {
4724
+ if (command.request.executionMode === "inherit") {
4725
+ clearInterval(loadingTicker);
4726
+ leaveAltScreen();
4727
+ try {
4728
+ ignoreSigint = true;
4729
+ result = await execute(command.request);
4730
+ } finally {
4731
+ ignoreSigint = false;
4732
+ enterAltScreen();
4733
+ }
4734
+ } else {
4735
+ result = await execute(command.request);
4736
+ }
4737
+ } finally {
4738
+ clearInterval(loadingTicker);
4739
+ }
4740
+ if (interrupted) {
4741
+ return;
4742
+ }
4743
+ const next = await showExecutionResult(command.request, result);
4744
+ if (next === "exit") {
4745
+ return;
4746
+ }
4747
+ }
4748
+ } finally {
4749
+ process.off("SIGINT", onSigint);
4750
+ suspendRawMode();
4751
+ leaveAltScreen();
4752
+ }
4753
+ }
4754
+
3302
4755
  // packages/cli/src/sharing/clipboard.ts
3303
4756
  var PLATFORM_COMMANDS = {
3304
4757
  darwin: ["pbcopy"],
@@ -3400,6 +4853,248 @@ async function uploadToGist(content, filename, description) {
3400
4853
  // packages/cli/src/cli.ts
3401
4854
  var FORMAT_VALUES = ["json", "svg", "png", "terminal"];
3402
4855
  var THEME_VALUES = ["dark", "light"];
4856
+ var PROVIDER_SHORTCUTS = {
4857
+ claude: "claude-code",
4858
+ codex: "codex",
4859
+ openCode: "open-code"
4860
+ };
4861
+ var PROVIDER_ALIASES = {
4862
+ anthropic: "claude-code",
4863
+ claude: "claude-code",
4864
+ "claude-code": "claude-code",
4865
+ claudecode: "claude-code",
4866
+ codex: "codex",
4867
+ openai: "codex",
4868
+ "open-code": "open-code",
4869
+ open_code: "open-code",
4870
+ opencode: "open-code"
4871
+ };
4872
+ var PROVIDER_ALIAS_GROUPS = {
4873
+ "claude-code": ["anthropic", "claude", "claudecode"],
4874
+ codex: ["openai"],
4875
+ "open-code": ["opencode", "open_code"]
4876
+ };
4877
+ function normalizeProviderToken(token) {
4878
+ const normalized = token.trim().toLowerCase().replace(/\s+/g, "-");
4879
+ return PROVIDER_ALIASES[normalized] ?? normalized;
4880
+ }
4881
+ function getRequestedProviders(config) {
4882
+ const requested = new Set;
4883
+ if (config.provider) {
4884
+ for (const token of config.provider.split(",")) {
4885
+ const normalized = normalizeProviderToken(token);
4886
+ if (normalized) {
4887
+ requested.add(normalized);
4888
+ }
4889
+ }
4890
+ }
4891
+ if (config.claude)
4892
+ requested.add(PROVIDER_SHORTCUTS.claude);
4893
+ if (config.codex)
4894
+ requested.add(PROVIDER_SHORTCUTS.codex);
4895
+ if (config.openCode)
4896
+ requested.add(PROVIDER_SHORTCUTS.openCode);
4897
+ return requested;
4898
+ }
4899
+ function providerMatchesFilter(provider, requested) {
4900
+ if (requested.size === 0)
4901
+ return true;
4902
+ const candidates = [
4903
+ normalizeProviderToken(provider.name),
4904
+ normalizeProviderToken(provider.displayName)
4905
+ ];
4906
+ return candidates.some((candidate) => requested.has(candidate));
4907
+ }
4908
+ function buildHelpText() {
4909
+ return [
4910
+ `tokenleak ${VERSION}`,
4911
+ "Visualize AI coding assistant token usage across providers.",
4912
+ "Running `tokenleak` with no flags opens an interactive launcher in a TTY.",
4913
+ "",
4914
+ "Usage:",
4915
+ " tokenleak [flags]",
4916
+ "",
4917
+ "Provider Shortcuts:",
4918
+ " --claude Only include Claude Code",
4919
+ " --codex Only include Codex",
4920
+ " --open-code Only include OpenCode",
4921
+ " --all-providers Ignore provider filters and use every available provider",
4922
+ " --list-providers Show registered providers and aliases",
4923
+ "",
4924
+ "Flags:",
4925
+ " -f, --format <format> Output format: terminal, png, svg, json",
4926
+ " -t, --theme <theme> Theme for png/svg/live output: dark, light",
4927
+ " -s, --since <date> Start date in YYYY-MM-DD format",
4928
+ " -u, --until <date> End date in YYYY-MM-DD format",
4929
+ ` -d, --days <number> Number of trailing days to include (default: ${DEFAULT_DAYS})`,
4930
+ " -o, --output <path> Write output to a file and infer format from extension",
4931
+ " -w, --width <number> Terminal render width",
4932
+ " -p, --provider <list> Provider filter list, comma-separated",
4933
+ " --compare <range> Compare against YYYY-MM-DD..YYYY-MM-DD or auto",
4934
+ " --more Add expanded PNG/SVG stats and unlock compare cards",
4935
+ " --clipboard Copy rendered output to the clipboard",
4936
+ " --open Open the generated output file",
4937
+ " --upload <target> Upload rendered output, currently: gist",
4938
+ " -L, --live-server Start the interactive local dashboard",
4939
+ " --no-color Disable ANSI colors",
4940
+ " --no-insights Hide insights in terminal mode",
4941
+ " --help Show this help",
4942
+ " --version Show version information",
4943
+ "",
4944
+ "Examples:",
4945
+ " tokenleak",
4946
+ " tokenleak --claude --days 30",
4947
+ " tokenleak --codex --format png --output codex.png",
4948
+ " tokenleak --open-code --since 2026-01-01 --until 2026-03-01",
4949
+ " tokenleak --provider claude,codex --format svg --output usage.svg",
4950
+ " tokenleak --provider anthropic,openai",
4951
+ " tokenleak --list-providers",
4952
+ " tokenleak --compare auto --format terminal",
4953
+ " tokenleak --live-server --theme light",
4954
+ "",
4955
+ "Version:",
4956
+ ` CLI ${VERSION}`,
4957
+ ` Schema ${SCHEMA_VERSION}`,
4958
+ ""
4959
+ ].join(`
4960
+ `);
4961
+ }
4962
+ function buildVersionText() {
4963
+ return `tokenleak ${VERSION}
4964
+ schema ${SCHEMA_VERSION}
4965
+ `;
4966
+ }
4967
+ function normalizeCliArg(arg) {
4968
+ const flagMap = {
4969
+ "--all-providers": "--allProviders",
4970
+ "--list-providers": "--listProviders",
4971
+ "--open-code": "--openCode",
4972
+ "--live-server": "--liveServer",
4973
+ "--no-color": "--noColor",
4974
+ "--no-insights": "--noInsights"
4975
+ };
4976
+ return flagMap[arg] ?? arg;
4977
+ }
4978
+ function buildInteractiveSummary(cliArgs, ok, exitCode) {
4979
+ if (!ok) {
4980
+ return `Command exited with code ${exitCode}.`;
4981
+ }
4982
+ if (typeof cliArgs["output"] === "string") {
4983
+ const outputPath = cliArgs["output"];
4984
+ const format2 = String(cliArgs["format"] ?? inferFormatFromPath(outputPath) ?? "output").toUpperCase();
4985
+ return `${format2} written to ${outputPath}.`;
4986
+ }
4987
+ if (cliArgs["listProviders"]) {
4988
+ return "Provider registry loaded.";
4989
+ }
4990
+ if (cliArgs["liveServer"]) {
4991
+ return "Live dashboard stopped.";
4992
+ }
4993
+ if (cliArgs["compare"]) {
4994
+ return "Compare report generated.";
4995
+ }
4996
+ const format = String(cliArgs["format"] ?? "terminal");
4997
+ if (format === "terminal") {
4998
+ return "Terminal dashboard generated.";
4999
+ }
5000
+ return `${format.toUpperCase()} command finished successfully.`;
5001
+ }
5002
+ async function executeInteractiveCommand(request) {
5003
+ try {
5004
+ const cliPath = process.argv[1];
5005
+ if (!cliPath) {
5006
+ return {
5007
+ ok: false,
5008
+ summary: "Could not resolve the current tokenleak entrypoint.",
5009
+ stdout: "",
5010
+ stderr: "Error: process.argv[1] is missing."
5011
+ };
5012
+ }
5013
+ const command = [process.execPath, cliPath, ...buildCliArgTokens(request.args)];
5014
+ if (request.executionMode === "inherit") {
5015
+ const proc2 = Bun.spawn(command, {
5016
+ stdin: "inherit",
5017
+ stdout: "inherit",
5018
+ stderr: "inherit"
5019
+ });
5020
+ const exitCode2 = await proc2.exited;
5021
+ return {
5022
+ ok: exitCode2 === 0,
5023
+ summary: buildInteractiveSummary(request.args, exitCode2 === 0, exitCode2),
5024
+ stdout: "",
5025
+ stderr: ""
5026
+ };
5027
+ }
5028
+ const proc = Bun.spawn(command, {
5029
+ stdin: "ignore",
5030
+ stdout: "pipe",
5031
+ stderr: "pipe"
5032
+ });
5033
+ const [exitCode, stdout, stderr] = await Promise.all([
5034
+ proc.exited,
5035
+ new Response(proc.stdout).text(),
5036
+ new Response(proc.stderr).text()
5037
+ ]);
5038
+ return {
5039
+ ok: exitCode === 0,
5040
+ summary: buildInteractiveSummary(request.args, exitCode === 0, exitCode),
5041
+ stdout,
5042
+ stderr
5043
+ };
5044
+ } catch (error) {
5045
+ return {
5046
+ ok: false,
5047
+ summary: "Interactive command failed before it could finish.",
5048
+ stdout: "",
5049
+ stderr: error instanceof Error ? `Error: ${error.message}` : `Error: ${String(error)}`
5050
+ };
5051
+ }
5052
+ }
5053
+ function normalizeCliArgv(argv) {
5054
+ const normalized = argv.map(normalizeCliArg);
5055
+ const result = [];
5056
+ for (let i = 0;i < normalized.length; i++) {
5057
+ const arg = normalized[i];
5058
+ if (arg === "--provider" || arg === "-p") {
5059
+ result.push(arg);
5060
+ const providerParts = [];
5061
+ let j = i + 1;
5062
+ while (j < normalized.length) {
5063
+ const next = normalized[j];
5064
+ if (next.startsWith("-"))
5065
+ break;
5066
+ providerParts.push(next);
5067
+ j++;
5068
+ }
5069
+ if (providerParts.length > 0) {
5070
+ result.push(providerParts.join(" "));
5071
+ i = j - 1;
5072
+ }
5073
+ continue;
5074
+ }
5075
+ result.push(arg);
5076
+ }
5077
+ return result;
5078
+ }
5079
+ function registerBuiltInProviders(registry) {
5080
+ registry.register(new ClaudeCodeProvider);
5081
+ registry.register(new CodexProvider);
5082
+ registry.register(new OpenCodeProvider);
5083
+ }
5084
+ function buildProviderList(providers, availability) {
5085
+ const lines = ["Registered providers:", ""];
5086
+ for (const provider of providers) {
5087
+ const aliases = PROVIDER_ALIAS_GROUPS[provider.name] ?? [];
5088
+ const status = availability.get(provider.name) ? "available" : "unavailable";
5089
+ lines.push(`- ${provider.name} (${provider.displayName}) [${status}]`);
5090
+ if (aliases.length > 0) {
5091
+ lines.push(` aliases: ${aliases.join(", ")}`);
5092
+ }
5093
+ }
5094
+ lines.push("");
5095
+ return lines.join(`
5096
+ `);
5097
+ }
3403
5098
  function inferFormatFromPath(filePath) {
3404
5099
  const ext = filePath.split(".").pop()?.toLowerCase();
3405
5100
  switch (ext) {
@@ -3453,6 +5148,12 @@ function resolveConfig(cliArgs) {
3453
5148
  width: 80,
3454
5149
  noColor: false,
3455
5150
  noInsights: false,
5151
+ more: false,
5152
+ claude: false,
5153
+ codex: false,
5154
+ openCode: false,
5155
+ allProviders: false,
5156
+ listProviders: false,
3456
5157
  clipboard: false,
3457
5158
  open: false,
3458
5159
  liveServer: false
@@ -3471,6 +5172,8 @@ function resolveConfig(cliArgs) {
3471
5172
  merged.noColor = fileConfig.noColor;
3472
5173
  if (fileConfig.noInsights !== undefined)
3473
5174
  merged.noInsights = fileConfig.noInsights;
5175
+ if (fileConfig.more !== undefined)
5176
+ merged.more = fileConfig.more;
3474
5177
  if (envConfig.format)
3475
5178
  merged.format = envConfig.format;
3476
5179
  if (envConfig.theme)
@@ -3512,12 +5215,30 @@ function resolveConfig(cliArgs) {
3512
5215
  if (cliArgs["noInsights"] !== undefined) {
3513
5216
  result.noInsights = cliArgs["noInsights"];
3514
5217
  }
5218
+ if (cliArgs["more"] !== undefined) {
5219
+ result.more = cliArgs["more"];
5220
+ }
3515
5221
  if (cliArgs["compare"] !== undefined) {
3516
5222
  result.compare = cliArgs["compare"];
3517
5223
  }
3518
5224
  if (cliArgs["provider"] !== undefined) {
3519
5225
  result.provider = cliArgs["provider"];
3520
5226
  }
5227
+ if (cliArgs["claude"] !== undefined) {
5228
+ result.claude = cliArgs["claude"];
5229
+ }
5230
+ if (cliArgs["codex"] !== undefined) {
5231
+ result.codex = cliArgs["codex"];
5232
+ }
5233
+ if (cliArgs["openCode"] !== undefined) {
5234
+ result.openCode = cliArgs["openCode"];
5235
+ }
5236
+ if (cliArgs["allProviders"] !== undefined) {
5237
+ result.allProviders = cliArgs["allProviders"];
5238
+ }
5239
+ if (cliArgs["listProviders"] !== undefined) {
5240
+ result.listProviders = cliArgs["listProviders"];
5241
+ }
3521
5242
  if (cliArgs["clipboard"] !== undefined) {
3522
5243
  result.clipboard = cliArgs["clipboard"];
3523
5244
  }
@@ -3574,34 +5295,82 @@ async function runCompare(compareStr, currentRange, _registry, available) {
3574
5295
  loadAndAggregate(currentRange, available),
3575
5296
  loadAndAggregate(previousRange, available)
3576
5297
  ]);
3577
- return buildCompareOutput({ range: currentRange, stats: currentResult.stats }, { range: previousRange, stats: previousResult.stats });
5298
+ return {
5299
+ compareOutput: buildCompareOutput({ range: currentRange, stats: currentResult.stats }, { range: previousRange, stats: previousResult.stats }),
5300
+ currentData: currentResult.data,
5301
+ previousData: previousResult.data
5302
+ };
3578
5303
  }
3579
5304
  async function run(cliArgs) {
3580
5305
  const config = resolveConfig(cliArgs);
5306
+ if (config.allProviders && (config.provider || config.claude || config.codex || config.openCode)) {
5307
+ throw new TokenleakError("--all-providers cannot be combined with provider filters");
5308
+ }
5309
+ const registry = new ProviderRegistry;
5310
+ registerBuiltInProviders(registry);
5311
+ if (config.listProviders) {
5312
+ const providers = registry.getAll();
5313
+ const availabilityResults = await Promise.all(providers.map(async (provider) => [provider.name, await provider.isAvailable()]));
5314
+ process.stdout.write(buildProviderList(providers, new Map(availabilityResults)));
5315
+ return;
5316
+ }
3581
5317
  const dateRange = computeDateRange({
3582
5318
  since: config.since,
3583
5319
  until: config.until,
3584
5320
  days: config.days
3585
5321
  });
3586
- const registry = new ProviderRegistry;
3587
- registry.register(new ClaudeCodeProvider);
3588
- registry.register(new CodexProvider);
3589
- registry.register(new OpenCodeProvider);
3590
5322
  let available = await registry.getAvailable();
3591
- if (config.provider) {
3592
- const requested = new Set(config.provider.split(",").map((s) => s.trim().toLowerCase()));
3593
- available = available.filter((p) => requested.has(p.name.toLowerCase()) || requested.has(p.displayName.toLowerCase()));
5323
+ const requestedProviders = getRequestedProviders(config);
5324
+ if (!config.allProviders && requestedProviders.size > 0) {
5325
+ if (config.provider && (config.claude || config.codex || config.openCode)) {
5326
+ process.stderr.write(`Combining provider filters: ${Array.from(requestedProviders).join(", ")}
5327
+ `);
5328
+ }
5329
+ available = available.filter((provider) => providerMatchesFilter(provider, requestedProviders));
3594
5330
  }
3595
5331
  if (available.length === 0) {
3596
5332
  throw new TokenleakError("No provider data found");
3597
5333
  }
3598
5334
  if (config.compare) {
5335
+ const compareResult = await runCompare(config.compare, dateRange, registry, available);
5336
+ if (config.more && (config.format === "png" || config.format === "svg")) {
5337
+ const compareOutput = {
5338
+ schemaVersion: SCHEMA_VERSION,
5339
+ generated: new Date().toISOString(),
5340
+ dateRange,
5341
+ providers: compareResult.currentData,
5342
+ aggregated: compareResult.compareOutput.periodA.stats,
5343
+ more: buildMoreStats(compareResult.currentData, dateRange, {
5344
+ previousRange: compareResult.compareOutput.periodB.range,
5345
+ previousProviders: compareResult.previousData
5346
+ })
5347
+ };
5348
+ const renderer2 = getRenderer(config.format);
5349
+ const renderOptions2 = {
5350
+ format: config.format,
5351
+ theme: config.theme,
5352
+ width: config.width,
5353
+ showInsights: !config.noInsights,
5354
+ noColor: config.noColor,
5355
+ output: config.output,
5356
+ more: true
5357
+ };
5358
+ const rendered3 = await renderer2.render(compareOutput, renderOptions2);
5359
+ if (config.output) {
5360
+ const data = typeof rendered3 === "string" ? rendered3 : Buffer.from(rendered3);
5361
+ writeFileSync(config.output, data);
5362
+ } else {
5363
+ const text2 = typeof rendered3 === "string" ? rendered3 : rendered3.toString("utf-8");
5364
+ process.stdout.write(text2 + `
5365
+ `);
5366
+ }
5367
+ return;
5368
+ }
3599
5369
  if (config.format !== "json" && config.format !== "terminal") {
3600
5370
  process.stderr.write(`Warning: --compare only supports JSON output. Ignoring --format ${config.format}.
3601
5371
  `);
3602
5372
  }
3603
- const compareOutput = await runCompare(config.compare, dateRange, registry, available);
3604
- const rendered2 = JSON.stringify(compareOutput, null, 2);
5373
+ const rendered2 = JSON.stringify(compareResult.compareOutput, null, 2);
3605
5374
  if (config.output) {
3606
5375
  writeFileSync(config.output, rendered2);
3607
5376
  } else {
@@ -3628,7 +5397,8 @@ async function run(cliArgs) {
3628
5397
  generated: new Date().toISOString(),
3629
5398
  dateRange,
3630
5399
  providers: providerDataList,
3631
- aggregated: stats
5400
+ aggregated: stats,
5401
+ more: config.more ? buildMoreStats(providerDataList, dateRange) : null
3632
5402
  };
3633
5403
  if (config.liveServer) {
3634
5404
  const ignoredFlags = [];
@@ -3650,7 +5420,8 @@ async function run(cliArgs) {
3650
5420
  width: config.width,
3651
5421
  showInsights: !config.noInsights,
3652
5422
  noColor: config.noColor,
3653
- output: config.output
5423
+ output: config.output,
5424
+ more: config.more
3654
5425
  };
3655
5426
  const { port } = await startLiveServer(output, renderOptions2);
3656
5427
  await new Promise((resolve) => {
@@ -3673,7 +5444,8 @@ Shutting down server...
3673
5444
  width: config.width,
3674
5445
  showInsights: !config.noInsights,
3675
5446
  noColor: config.noColor,
3676
- output: config.output
5447
+ output: config.output,
5448
+ more: config.more
3677
5449
  };
3678
5450
  const rendered = await renderer.render(output, renderOptions);
3679
5451
  if (config.output) {
@@ -3762,6 +5534,11 @@ var main = defineCommand({
3762
5534
  description: "Hide insights panel",
3763
5535
  default: false
3764
5536
  },
5537
+ more: {
5538
+ type: "boolean",
5539
+ description: "Add expanded PNG/SVG stats and compare cards",
5540
+ default: false
5541
+ },
3765
5542
  compare: {
3766
5543
  type: "string",
3767
5544
  description: "Compare two date ranges (YYYY-MM-DD..YYYY-MM-DD)"
@@ -3771,6 +5548,31 @@ var main = defineCommand({
3771
5548
  alias: "p",
3772
5549
  description: "Filter to specific provider(s), comma-separated"
3773
5550
  },
5551
+ claude: {
5552
+ type: "boolean",
5553
+ description: "Shortcut for --provider claude-code",
5554
+ default: false
5555
+ },
5556
+ codex: {
5557
+ type: "boolean",
5558
+ description: "Shortcut for --provider codex",
5559
+ default: false
5560
+ },
5561
+ openCode: {
5562
+ type: "boolean",
5563
+ description: "Shortcut for --provider open-code",
5564
+ default: false
5565
+ },
5566
+ allProviders: {
5567
+ type: "boolean",
5568
+ description: "Ignore provider filters and use every available provider",
5569
+ default: false
5570
+ },
5571
+ listProviders: {
5572
+ type: "boolean",
5573
+ description: "List registered providers and aliases",
5574
+ default: false
5575
+ },
3774
5576
  clipboard: {
3775
5577
  type: "boolean",
3776
5578
  description: "Copy output to clipboard after rendering",
@@ -3813,10 +5615,22 @@ var main = defineCommand({
3813
5615
  cliArgs["noColor"] = true;
3814
5616
  if (args.noInsights)
3815
5617
  cliArgs["noInsights"] = true;
5618
+ if (args.more)
5619
+ cliArgs["more"] = true;
3816
5620
  if (args.compare !== undefined)
3817
5621
  cliArgs["compare"] = args.compare;
3818
5622
  if (args.provider !== undefined)
3819
5623
  cliArgs["provider"] = args.provider;
5624
+ if (args.claude)
5625
+ cliArgs["claude"] = true;
5626
+ if (args.codex)
5627
+ cliArgs["codex"] = true;
5628
+ if (args.openCode)
5629
+ cliArgs["openCode"] = true;
5630
+ if (args.allProviders)
5631
+ cliArgs["allProviders"] = true;
5632
+ if (args.listProviders)
5633
+ cliArgs["listProviders"] = true;
3820
5634
  if (args.clipboard)
3821
5635
  cliArgs["clipboard"] = true;
3822
5636
  if (args.open)
@@ -3833,11 +5647,31 @@ var main = defineCommand({
3833
5647
  });
3834
5648
  var isDirectExecution = typeof Bun !== "undefined" ? Bun.main === import.meta.path : process.argv[1] !== undefined && import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/"));
3835
5649
  if (isDirectExecution) {
3836
- runMain(main);
5650
+ const normalizedArgv = normalizeCliArgv(process.argv.slice(2));
5651
+ process.argv = [...process.argv.slice(0, 2), ...normalizedArgv];
5652
+ const argv = normalizedArgv;
5653
+ if (argv.includes("--help") || argv.includes("-h")) {
5654
+ process.stdout.write(buildHelpText());
5655
+ process.exit(0);
5656
+ }
5657
+ if (argv.includes("--version") || argv.includes("-v")) {
5658
+ process.stdout.write(buildVersionText());
5659
+ process.exit(0);
5660
+ }
5661
+ if (shouldStartInteractiveCli(argv, Boolean(process.stdin.isTTY), Boolean(process.stdout.isTTY))) {
5662
+ await startInteractiveCli({
5663
+ version: VERSION,
5664
+ helpText: buildHelpText()
5665
+ }, executeInteractiveCommand);
5666
+ } else {
5667
+ await runMain(main);
5668
+ }
3837
5669
  }
3838
5670
  export {
3839
5671
  run,
3840
5672
  resolveConfig,
5673
+ normalizeCliArgv,
3841
5674
  inferFormatFromPath,
3842
- computeDateRange
5675
+ computeDateRange,
5676
+ buildInteractiveSummary
3843
5677
  };