tokenleak 1.0.2 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +9 -14
  2. package/package.json +1 -1
  3. package/tokenleak +3560 -464
package/tokenleak CHANGED
@@ -755,6 +755,235 @@ 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
+ const projectId = event.projectId?.trim() || undefined;
859
+ let session = sessions.get(key);
860
+ if (!session) {
861
+ session = {
862
+ label: projectId || event.sessionId?.trim() || key,
863
+ tokens: 0,
864
+ cost: 0,
865
+ count: 0,
866
+ projectId,
867
+ firstTimestamp: safeTime,
868
+ lastTimestamp: safeTime,
869
+ explicitDurationMs: 0,
870
+ hasExplicitDuration: false
871
+ };
872
+ sessions.set(key, session);
873
+ } else if (!session.projectId && projectId) {
874
+ session.projectId = projectId;
875
+ session.label = projectId || event.sessionId?.trim() || key;
876
+ }
877
+ session.tokens += event.totalTokens;
878
+ session.cost += event.cost;
879
+ session.count += 1;
880
+ session.firstTimestamp = Math.min(session.firstTimestamp, safeTime);
881
+ session.lastTimestamp = Math.max(session.lastTimestamp, safeTime);
882
+ if (typeof event.durationMs === "number" && Number.isFinite(event.durationMs)) {
883
+ session.explicitDurationMs += Math.max(0, event.durationMs);
884
+ session.hasExplicitDuration = true;
885
+ }
886
+ if (projectId) {
887
+ projects.set(projectId, (projects.get(projectId) ?? 0) + event.totalTokens);
888
+ }
889
+ }
890
+ const sessionEntries = [...sessions.values()];
891
+ const totalSessions = sessionEntries.length;
892
+ let totalTokens = 0;
893
+ let totalCost = 0;
894
+ let totalMessages = 0;
895
+ let durationTotal = 0;
896
+ let durationCount = 0;
897
+ let longestSession = null;
898
+ let longestSessionDuration = -1;
899
+ for (const session of sessionEntries) {
900
+ totalTokens += session.tokens;
901
+ totalCost += session.cost;
902
+ totalMessages += session.count;
903
+ const derivedDurationMs = session.hasExplicitDuration ? session.explicitDurationMs : session.lastTimestamp > session.firstTimestamp ? session.lastTimestamp - session.firstTimestamp : 0;
904
+ if (derivedDurationMs > 0) {
905
+ durationTotal += derivedDurationMs;
906
+ durationCount += 1;
907
+ }
908
+ if (derivedDurationMs > longestSessionDuration || derivedDurationMs === longestSessionDuration && (!longestSession || session.tokens > longestSession.tokens)) {
909
+ longestSessionDuration = derivedDurationMs;
910
+ longestSession = {
911
+ label: session.label,
912
+ tokens: session.tokens,
913
+ cost: session.cost,
914
+ count: session.count,
915
+ durationMs: derivedDurationMs > 0 ? derivedDurationMs : null
916
+ };
917
+ }
918
+ }
919
+ const projectBreakdown = [...projects.entries()].map(([name, tokens]) => ({ name, tokens })).sort((a, b) => b.tokens - a.tokens).slice(0, 10);
920
+ const topProject = projectBreakdown[0] ?? null;
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
+ projectBreakdown
931
+ };
932
+ }
933
+ function computeModelMixShift(currentProviders, previousProviders, limit = 5) {
934
+ const currentModelTokens = new Map;
935
+ const previousModelTokens = new Map;
936
+ let currentTotal = 0;
937
+ let previousTotal = 0;
938
+ for (const provider of currentProviders) {
939
+ for (const day of provider.daily) {
940
+ for (const model of day.models) {
941
+ currentModelTokens.set(model.model, (currentModelTokens.get(model.model) ?? 0) + model.totalTokens);
942
+ currentTotal += model.totalTokens;
943
+ }
944
+ }
945
+ }
946
+ for (const provider of previousProviders) {
947
+ for (const day of provider.daily) {
948
+ for (const model of day.models) {
949
+ previousModelTokens.set(model.model, (previousModelTokens.get(model.model) ?? 0) + model.totalTokens);
950
+ previousTotal += model.totalTokens;
951
+ }
952
+ }
953
+ }
954
+ const models = new Set([
955
+ ...currentModelTokens.keys(),
956
+ ...previousModelTokens.keys()
957
+ ]);
958
+ return [...models].map((model) => {
959
+ const currentTokens = currentModelTokens.get(model) ?? 0;
960
+ const previousTokens = previousModelTokens.get(model) ?? 0;
961
+ const currentShare = currentTotal > 0 ? currentTokens / currentTotal : 0;
962
+ const previousShare = previousTotal > 0 ? previousTokens / previousTotal : 0;
963
+ return {
964
+ model,
965
+ currentShare,
966
+ previousShare,
967
+ deltaShare: currentShare - previousShare,
968
+ currentTokens,
969
+ previousTokens
970
+ };
971
+ }).sort((a, b) => Math.abs(b.deltaShare) - Math.abs(a.deltaShare)).slice(0, limit);
972
+ }
973
+ function buildMoreStats(providers, range, compare = null) {
974
+ const events = collectEvents(providers);
975
+ return {
976
+ inputOutput: buildInputOutput(providers),
977
+ monthlyBurn: buildMonthlyBurn(providers, range),
978
+ cacheEconomics: buildCacheEconomics(providers),
979
+ hourOfDay: buildHourOfDay(events),
980
+ sessionMetrics: buildSessionMetrics(events),
981
+ compare: compare ? {
982
+ previousRange: compare.previousRange,
983
+ modelMixShift: computeModelMixShift(providers, compare.previousProviders)
984
+ } : null
985
+ };
986
+ }
758
987
  // packages/core/dist/index.js
759
988
  var VERSION = "1.0.2";
760
989
 
@@ -1022,7 +1251,7 @@ async function* splitJsonlRecords(filePath) {
1022
1251
  }
1023
1252
  // packages/registry/dist/providers/claude-code.js
1024
1253
  import { existsSync, readdirSync, statSync } from "fs";
1025
- import { join } from "path";
1254
+ import { dirname, join, relative, sep } from "path";
1026
1255
  import { homedir } from "os";
1027
1256
 
1028
1257
  // packages/registry/dist/utils.js
@@ -1101,6 +1330,7 @@ function extractUsage(record) {
1101
1330
  }
1102
1331
  return {
1103
1332
  date,
1333
+ timestamp,
1104
1334
  model,
1105
1335
  inputTokens,
1106
1336
  outputTokens,
@@ -1181,13 +1411,18 @@ class ClaudeCodeProvider {
1181
1411
  async load(range) {
1182
1412
  const files = collectJsonlFiles(this.baseDir);
1183
1413
  const allRecords = [];
1414
+ const allEvents = [];
1184
1415
  for (const file of files) {
1185
1416
  const latestRecordsByMessageId = new Map;
1186
1417
  const anonymousRecords = [];
1418
+ const relativeFile = relative(this.baseDir, file).split(sep).join("/");
1419
+ const projectId = relative(this.baseDir, dirname(file)).split(sep).join("/");
1187
1420
  try {
1188
1421
  for await (const record of splitJsonlRecords(file)) {
1189
1422
  const usage = extractUsage(record);
1190
1423
  if (usage !== null && isInRange(usage.date, range)) {
1424
+ usage.sessionId = relativeFile;
1425
+ usage.projectId = projectId;
1191
1426
  if (usage.messageId) {
1192
1427
  latestRecordsByMessageId.set(usage.messageId, usage);
1193
1428
  } else {
@@ -1201,6 +1436,24 @@ class ClaudeCodeProvider {
1201
1436
  allRecords.push(...latestRecordsByMessageId.values(), ...anonymousRecords);
1202
1437
  }
1203
1438
  const daily = buildDailyUsage(allRecords);
1439
+ for (const record of allRecords) {
1440
+ const normalizedModel = normalizeModelName(record.model);
1441
+ const cost = estimateCost(record.model, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens);
1442
+ allEvents.push({
1443
+ provider: this.name,
1444
+ timestamp: record.timestamp,
1445
+ date: record.date,
1446
+ model: normalizedModel,
1447
+ inputTokens: record.inputTokens,
1448
+ outputTokens: record.outputTokens,
1449
+ cacheReadTokens: record.cacheReadTokens,
1450
+ cacheWriteTokens: record.cacheWriteTokens,
1451
+ totalTokens: record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens,
1452
+ cost,
1453
+ sessionId: record.sessionId,
1454
+ projectId: record.projectId
1455
+ });
1456
+ }
1204
1457
  const totalTokens = daily.reduce((sum, d) => sum + d.totalTokens, 0);
1205
1458
  const totalCost = daily.reduce((sum, d) => sum + d.cost, 0);
1206
1459
  return {
@@ -1209,13 +1462,14 @@ class ClaudeCodeProvider {
1209
1462
  daily,
1210
1463
  totalTokens,
1211
1464
  totalCost,
1212
- colors: this.colors
1465
+ colors: this.colors,
1466
+ events: allEvents
1213
1467
  };
1214
1468
  }
1215
1469
  }
1216
1470
  // packages/registry/dist/providers/codex.js
1217
1471
  import { existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1218
- import { join as join2 } from "path";
1472
+ import { dirname as dirname2, join as join2, relative as relative2, sep as sep2 } from "path";
1219
1473
  import { homedir as homedir2 } from "os";
1220
1474
  var CODEX_COLORS = {
1221
1475
  primary: "#10a37f",
@@ -1373,6 +1627,7 @@ function parseTokenCountUsage(record, context) {
1373
1627
  const inputTokens = Math.max(0, usage.inputTokens - cacheReadTokens);
1374
1628
  return {
1375
1629
  date,
1630
+ timestamp,
1376
1631
  model: context.model,
1377
1632
  inputTokens,
1378
1633
  outputTokens: usage.outputTokens,
@@ -1403,6 +1658,7 @@ function parseUsageRecord(record, context) {
1403
1658
  }
1404
1659
  return {
1405
1660
  date,
1661
+ timestamp: legacyEvent.timestamp,
1406
1662
  model: compactModelDateSuffix(legacyEvent.model),
1407
1663
  inputTokens: legacyEvent.usage.input_tokens,
1408
1664
  outputTokens: legacyEvent.usage.output_tokens,
@@ -1429,11 +1685,14 @@ class CodexProvider {
1429
1685
  async load(range) {
1430
1686
  const dailyMap = new Map;
1431
1687
  const files = collectJsonlFiles2(this.sessionsDir);
1688
+ const events = [];
1432
1689
  for (const file of files) {
1433
1690
  const context = {
1434
1691
  model: "gpt-5",
1435
1692
  previousTotals: null
1436
1693
  };
1694
+ const relativeFile = relative2(this.sessionsDir, file).split(sep2).join("/");
1695
+ const projectDir = relative2(this.sessionsDir, dirname2(file)).split(sep2).join("/");
1437
1696
  try {
1438
1697
  for await (const record of splitJsonlRecords(file)) {
1439
1698
  const usage = parseUsageRecord(record, context);
@@ -1443,12 +1702,28 @@ class CodexProvider {
1443
1702
  if (!isInRange(usage.date, range)) {
1444
1703
  continue;
1445
1704
  }
1705
+ usage.sessionId = relativeFile;
1706
+ usage.projectId = projectDir === "." ? undefined : projectDir;
1446
1707
  const normalizedModel = normalizeModelName(compactModelDateSuffix(usage.model));
1447
1708
  const inputTokens = usage.inputTokens;
1448
1709
  const outputTokens = usage.outputTokens;
1449
1710
  const cacheReadTokens = usage.cacheReadTokens;
1450
1711
  const cacheWriteTokens = usage.cacheWriteTokens;
1451
1712
  const cost = estimateCost(normalizedModel, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
1713
+ events.push({
1714
+ provider: this.name,
1715
+ timestamp: usage.timestamp,
1716
+ date: usage.date,
1717
+ model: normalizedModel,
1718
+ inputTokens,
1719
+ outputTokens,
1720
+ cacheReadTokens,
1721
+ cacheWriteTokens,
1722
+ totalTokens: inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens,
1723
+ cost,
1724
+ sessionId: usage.sessionId,
1725
+ projectId: usage.projectId
1726
+ });
1452
1727
  if (!dailyMap.has(usage.date)) {
1453
1728
  dailyMap.set(usage.date, new Map);
1454
1729
  }
@@ -1503,7 +1778,8 @@ class CodexProvider {
1503
1778
  daily,
1504
1779
  totalTokens,
1505
1780
  totalCost,
1506
- colors: this.colors
1781
+ colors: this.colors,
1782
+ events
1507
1783
  };
1508
1784
  }
1509
1785
  }
@@ -1513,7 +1789,7 @@ import { join as join3 } from "path";
1513
1789
  import { homedir as homedir3 } from "os";
1514
1790
  import { Database } from "bun:sqlite";
1515
1791
  var PROVIDER_NAME = "open-code";
1516
- var DISPLAY_NAME = "Open Code";
1792
+ var DISPLAY_NAME = "OpenCode";
1517
1793
  var COLORS = {
1518
1794
  primary: "#6366f1",
1519
1795
  secondary: "#a78bfa",
@@ -1549,12 +1825,46 @@ function extractDate2(createdAt) {
1549
1825
  }
1550
1826
  return date.toISOString().slice(0, 10);
1551
1827
  }
1828
+ function toTimestampMillis(createdAt) {
1829
+ const timestamp = typeof createdAt === "number" ? createdAt : Number.isNaN(Number(createdAt)) ? Date.parse(createdAt) : Number(createdAt);
1830
+ if (!Number.isFinite(timestamp)) {
1831
+ return null;
1832
+ }
1833
+ const millis = Math.abs(timestamp) >= 1000000000000 ? timestamp : timestamp * 1000;
1834
+ return Number.isFinite(millis) ? millis : null;
1835
+ }
1836
+ function toIsoTimestamp(createdAt) {
1837
+ const millis = toTimestampMillis(createdAt);
1838
+ if (millis === null) {
1839
+ return null;
1840
+ }
1841
+ const date = new Date(millis);
1842
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
1843
+ }
1552
1844
  function getRecordCost(record) {
1553
1845
  if (typeof record.explicitCost === "number" && Number.isFinite(record.explicitCost)) {
1554
1846
  return record.explicitCost;
1555
1847
  }
1556
1848
  return estimateCost(record.model, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens);
1557
1849
  }
1850
+ function toUsageEvent(record) {
1851
+ const totalTokens = record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens;
1852
+ return {
1853
+ provider: PROVIDER_NAME,
1854
+ timestamp: record.timestamp,
1855
+ date: record.date,
1856
+ model: normalizeModelName(record.model),
1857
+ inputTokens: record.inputTokens,
1858
+ outputTokens: record.outputTokens,
1859
+ cacheReadTokens: record.cacheReadTokens,
1860
+ cacheWriteTokens: record.cacheWriteTokens,
1861
+ totalTokens,
1862
+ cost: getRecordCost(record),
1863
+ sessionId: record.sessionId,
1864
+ projectId: record.projectId,
1865
+ durationMs: record.durationMs
1866
+ };
1867
+ }
1558
1868
  function buildProviderData(records) {
1559
1869
  const byDate = new Map;
1560
1870
  for (const record of records) {
@@ -1614,7 +1924,8 @@ function buildProviderData(records) {
1614
1924
  daily,
1615
1925
  totalTokens,
1616
1926
  totalCost,
1617
- colors: COLORS
1927
+ colors: COLORS,
1928
+ events: records.map(toUsageEvent)
1618
1929
  };
1619
1930
  }
1620
1931
  function loadFromSqlite(dbPath, range) {
@@ -1629,18 +1940,21 @@ function loadFromSqlite(dbPath, range) {
1629
1940
  if (tables.length === 0) {
1630
1941
  return [];
1631
1942
  }
1632
- const rows = db.query("SELECT model, input_tokens, output_tokens, created_at FROM messages WHERE role = 'assistant'").all();
1943
+ const rows = db.query("SELECT model, session_id, input_tokens, output_tokens, created_at FROM messages WHERE role = 'assistant'").all();
1633
1944
  const records = [];
1634
1945
  for (const row of rows) {
1635
1946
  const date = extractDate2(row.created_at);
1636
- if (date && isInRange(date, range)) {
1947
+ const timestamp = toIsoTimestamp(row.created_at);
1948
+ if (date && timestamp && isInRange(date, range)) {
1637
1949
  records.push({
1638
1950
  date,
1951
+ timestamp,
1639
1952
  model: row.model,
1640
1953
  inputTokens: row.input_tokens,
1641
1954
  outputTokens: row.output_tokens,
1642
1955
  cacheReadTokens: 0,
1643
- cacheWriteTokens: 0
1956
+ cacheWriteTokens: 0,
1957
+ sessionId: row.session_id
1644
1958
  });
1645
1959
  }
1646
1960
  }
@@ -1666,14 +1980,17 @@ function loadFromLegacyJson(sessionsDir, range) {
1666
1980
  continue;
1667
1981
  }
1668
1982
  const date = extractDate2(msg.created_at);
1669
- if (date && isInRange(date, range)) {
1983
+ const timestamp = toIsoTimestamp(msg.created_at);
1984
+ if (date && timestamp && isInRange(date, range)) {
1670
1985
  records.push({
1671
1986
  date,
1987
+ timestamp,
1672
1988
  model: msg.model,
1673
1989
  inputTokens: msg.usage.input_tokens,
1674
1990
  outputTokens: msg.usage.output_tokens,
1675
1991
  cacheReadTokens: 0,
1676
- cacheWriteTokens: 0
1992
+ cacheWriteTokens: 0,
1993
+ sessionId: file
1677
1994
  });
1678
1995
  }
1679
1996
  }
@@ -1711,7 +2028,8 @@ function loadFromCurrentStorage(baseDir, range) {
1711
2028
  continue;
1712
2029
  }
1713
2030
  const date = extractDate2(createdAt);
1714
- if (!date || !isInRange(date, range)) {
2031
+ const timestamp = toIsoTimestamp(createdAt);
2032
+ if (!date || !timestamp || !isInRange(date, range)) {
1715
2033
  continue;
1716
2034
  }
1717
2035
  const inputTokens = typeof message.tokens?.input === "number" ? message.tokens.input : 0;
@@ -1720,13 +2038,21 @@ function loadFromCurrentStorage(baseDir, range) {
1720
2038
  const cacheWriteTokens = typeof message.tokens?.cache?.write === "number" ? message.tokens.cache.write : 0;
1721
2039
  const record = {
1722
2040
  date,
2041
+ timestamp,
1723
2042
  model,
1724
2043
  inputTokens,
1725
2044
  outputTokens,
1726
2045
  cacheReadTokens,
1727
2046
  cacheWriteTokens,
1728
- explicitCost: typeof message.cost === "number" ? message.cost : undefined
2047
+ explicitCost: typeof message.cost === "number" ? message.cost : undefined,
2048
+ sessionId: typeof message.sessionID === "string" && message.sessionID || sessionDir
1729
2049
  };
2050
+ const completedAt = message.time?.completed;
2051
+ const completedMs = typeof completedAt === "string" || typeof completedAt === "number" ? toTimestampMillis(completedAt) : null;
2052
+ const createdMs = Date.parse(record.timestamp);
2053
+ if (completedMs !== null && Number.isFinite(createdMs) && completedMs > createdMs) {
2054
+ record.durationMs = completedMs - createdMs;
2055
+ }
1730
2056
  const totalTokens = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
1731
2057
  if (totalTokens === 0 && !(typeof record.explicitCost === "number" && record.explicitCost > 0)) {
1732
2058
  continue;
@@ -1831,7 +2157,7 @@ var DOT_GAP = 8;
1831
2157
  var CELL_SIZE = 16;
1832
2158
  var CELL_GAP = 4;
1833
2159
  var STAT_GRID_COLS = 3;
1834
- var MODEL_BAR_HEIGHT = 8;
2160
+ var MODEL_BAR_HEIGHT = 11;
1835
2161
  var DAY_LABEL_WIDTH = 44;
1836
2162
  var MONTH_LABEL_HEIGHT = 24;
1837
2163
  var PROVIDER_SECTION_GAP = 36;
@@ -1841,24 +2167,27 @@ var MODEL_BAR_GAP = 36;
1841
2167
  var MODEL_PERCENT_WIDTH = 40;
1842
2168
 
1843
2169
  // packages/renderers/dist/png/terminal-card.js
1844
- var FONT_FAMILY = "'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', monospace";
2170
+ var FONT_FAMILY = "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif";
2171
+ var MONO_FONT_FAMILY = "'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', monospace";
1845
2172
  function getCardTheme(mode) {
1846
2173
  if (mode === "dark") {
1847
2174
  return {
1848
2175
  bg: "#09090b",
1849
- fg: "#ffffff",
1850
- muted: "#52525b",
1851
- border: "rgba(255,255,255,0.06)",
1852
- accent: "#10b981",
1853
- heatmapEmpty: "#141418",
1854
- barTrack: "#18181b",
1855
- titlebarBorder: "rgba(255,255,255,0.06)"
2176
+ fg: "#f0f0f0",
2177
+ muted: "#6b7280",
2178
+ labelFg: "#b0b8c4",
2179
+ border: "rgba(255,255,255,0.08)",
2180
+ accent: "#34d399",
2181
+ heatmapEmpty: "#1a1a22",
2182
+ barTrack: "#151520",
2183
+ titlebarBorder: "rgba(255,255,255,0.08)"
1856
2184
  };
1857
2185
  }
1858
2186
  return {
1859
2187
  bg: "#fafafa",
1860
2188
  fg: "#18181b",
1861
2189
  muted: "#a1a1aa",
2190
+ labelFg: "#71717a",
1862
2191
  border: "rgba(0,0,0,0.08)",
1863
2192
  accent: "#059669",
1864
2193
  heatmapEmpty: "#e4e4e7",
@@ -1870,7 +2199,7 @@ function buildHeatmapScale(colors, isDark) {
1870
2199
  const [startHex, endHex] = colors.gradient;
1871
2200
  const s = hexToRgb(startHex);
1872
2201
  const e = hexToRgb(endHex);
1873
- const opacities = isDark ? [0.15, 0.35, 0.6, 1] : [0.2, 0.4, 0.65, 1];
2202
+ const opacities = isDark ? [0.25, 0.5, 0.75, 1] : [0.2, 0.4, 0.65, 1];
1874
2203
  return [
1875
2204
  "transparent",
1876
2205
  ...opacities.map((t) => {
@@ -1957,6 +2286,41 @@ function formatPercentage(rate) {
1957
2286
  function formatStreak(n) {
1958
2287
  return `${n} day${n !== 1 ? "s" : ""}`;
1959
2288
  }
2289
+ function formatRatio(value, suffix = "x") {
2290
+ if (value === null || !Number.isFinite(value)) {
2291
+ return "n/a";
2292
+ }
2293
+ return `${value.toFixed(value >= 10 ? 1 : 2)}${suffix}`;
2294
+ }
2295
+ function formatPercentPoints(value) {
2296
+ const prefix = value >= 0 ? "+" : "";
2297
+ return `${prefix}${(value * 100).toFixed(1)}pp`;
2298
+ }
2299
+ function formatHour(hour) {
2300
+ return `${hour.toString().padStart(2, "0")}:00`;
2301
+ }
2302
+ function formatDuration(durationMs) {
2303
+ if (durationMs === null || durationMs === undefined || durationMs <= 0) {
2304
+ return "n/a";
2305
+ }
2306
+ const totalMinutes = Math.round(durationMs / 60000);
2307
+ if (totalMinutes < 60) {
2308
+ return `${totalMinutes}m`;
2309
+ }
2310
+ const hours = Math.floor(totalMinutes / 60);
2311
+ const minutes = totalMinutes % 60;
2312
+ return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
2313
+ }
2314
+ function truncateText(value, maxLength) {
2315
+ return value.length > maxLength ? `${value.slice(0, maxLength - 1)}\u2026` : value;
2316
+ }
2317
+ function formatSessionSummary(summary) {
2318
+ const duration = formatDuration(summary.durationMs);
2319
+ if (duration === "n/a") {
2320
+ return truncateText(summary.label, 20);
2321
+ }
2322
+ return truncateText(`${summary.label} \xB7 ${duration}`, 24);
2323
+ }
1960
2324
  function renderProviderHeatmap(daily, since, until, heatmapColors, emptyColor) {
1961
2325
  const tokenMap = new Map;
1962
2326
  for (const d of daily) {
@@ -2013,6 +2377,149 @@ function renderProviderHeatmap(daily, since, until, heatmapColors, emptyColor) {
2013
2377
  `);
2014
2378
  return { svg, gridWidth, height };
2015
2379
  }
2380
+ function renderSectionHeader(x, y, title, theme, cardAccent) {
2381
+ const parts = [];
2382
+ parts.push(`<rect x="${x}" y="${y - 8}" width="3" height="10" rx="1.5" fill="${escapeXml(cardAccent)}" opacity="0.6"/>`);
2383
+ 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>`);
2384
+ return parts.join(`
2385
+ `);
2386
+ }
2387
+ function renderMetricCard(x, y, width, title, lines, theme, cardAccent) {
2388
+ const parts = [];
2389
+ const height = 38 + lines.length * 22;
2390
+ parts.push(`<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="10" fill="${escapeXml(theme.barTrack)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
2391
+ 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>`);
2392
+ lines.forEach((line, index) => {
2393
+ const lineY = y + 48 + index * 22;
2394
+ 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>`);
2395
+ 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>`);
2396
+ });
2397
+ return parts.join(`
2398
+ `);
2399
+ }
2400
+ function buildProviderHourBuckets(providers) {
2401
+ return providers.map((p) => {
2402
+ const hours = new Array(24).fill(0);
2403
+ for (const event of p.events ?? []) {
2404
+ const date = new Date(event.timestamp);
2405
+ if (!Number.isNaN(date.getTime())) {
2406
+ const h = date.getUTCHours();
2407
+ hours[h] += event.totalTokens;
2408
+ }
2409
+ }
2410
+ return { provider: p.provider, color: p.colors.primary, hours };
2411
+ });
2412
+ }
2413
+ function renderHourOfDayChart(x, y, width, hourOfDay, theme, cardAccent, providers, isDark) {
2414
+ const chartHeight = 140;
2415
+ const innerHeight = 72;
2416
+ const baselineY = y + 92;
2417
+ const barAreaX = x + 18;
2418
+ const barAreaWidth = width - 36;
2419
+ const barGap = 4;
2420
+ const barWidth = (barAreaWidth - barGap * 23) / 24;
2421
+ const maxTokens = Math.max(...hourOfDay.map((entry) => entry.tokens), 0);
2422
+ const busiest = hourOfDay.reduce((best, entry) => best === null || entry.tokens > best.tokens ? entry : best, null);
2423
+ const isMulti = providers.length > 1;
2424
+ const providerBuckets = isMulti ? buildProviderHourBuckets(providers) : [];
2425
+ let legendSvg = "";
2426
+ if (isMulti && providerBuckets.length > 0) {
2427
+ const titleWidth = 105;
2428
+ let legendX = x + 18 + titleWidth + 12;
2429
+ const legendY = y + 22;
2430
+ for (const bucket of providerBuckets) {
2431
+ const displayName = providers.find((p) => p.provider === bucket.provider)?.displayName ?? bucket.provider;
2432
+ 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>`;
2433
+ legendX += 12 + displayName.length * 5.5 + 16;
2434
+ }
2435
+ }
2436
+ const bars = [
2437
+ `<rect x="${x}" y="${y}" width="${width}" height="${chartHeight}" rx="10" fill="${escapeXml(theme.barTrack)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`,
2438
+ `<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>`,
2439
+ legendSvg,
2440
+ `<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>`,
2441
+ `<line x1="${barAreaX}" y1="${baselineY}" x2="${barAreaX + barAreaWidth}" y2="${baselineY}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`
2442
+ ];
2443
+ 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>`);
2444
+ if (isMulti) {
2445
+ const provBaseOpacity = isDark ? "0.45" : "0.3";
2446
+ const provMidOpacity = isDark ? "0.85" : "0.75";
2447
+ for (let bi = 0;bi < providerBuckets.length; bi++) {
2448
+ const gradId = `hod-prov-${bi}`;
2449
+ 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>`);
2450
+ }
2451
+ hourOfDay.forEach((entry, index) => {
2452
+ if (entry.tokens <= 0)
2453
+ return;
2454
+ const totalRatio = entry.tokens / maxTokens;
2455
+ const totalBarHeight = Math.max(4, totalRatio * innerHeight);
2456
+ const colX = barAreaX + index * (barWidth + barGap);
2457
+ const isPeak = busiest !== null && entry.hour === busiest.hour;
2458
+ if (isPeak) {
2459
+ const topY2 = baselineY - totalBarHeight;
2460
+ 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)"/>`);
2461
+ }
2462
+ const clipId = `hod-clip-${index}`;
2463
+ const topY = baselineY - totalBarHeight;
2464
+ bars.push(`<defs><clipPath id="${escapeXml(clipId)}">` + `<rect x="${colX}" y="${topY}" width="${barWidth}" height="${totalBarHeight}" rx="3"/>` + `</clipPath></defs>`);
2465
+ let offsetY = 0;
2466
+ for (let bi = 0;bi < providerBuckets.length; bi++) {
2467
+ const tokens = providerBuckets[bi].hours[index] ?? 0;
2468
+ if (tokens <= 0)
2469
+ continue;
2470
+ const segHeight = tokens / entry.tokens * totalBarHeight;
2471
+ const segY = baselineY - offsetY - segHeight;
2472
+ bars.push(`<rect x="${colX}" y="${segY}" width="${barWidth}" height="${segHeight}" fill="url(#hod-prov-${bi})" clip-path="url(#${escapeXml(clipId)})"/>`);
2473
+ offsetY += segHeight;
2474
+ }
2475
+ });
2476
+ } else {
2477
+ const hodGradId = "hod-bar-grad";
2478
+ const hodBaseOpacity = isDark ? "0.25" : "0.1";
2479
+ const hodMidOpacity = isDark ? "0.75" : "0.6";
2480
+ 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>`);
2481
+ hourOfDay.forEach((entry, index) => {
2482
+ const ratio = maxTokens > 0 ? entry.tokens / maxTokens : 0;
2483
+ const barHeight = maxTokens > 0 ? Math.max(4, ratio * innerHeight) : 4;
2484
+ const colX = barAreaX + index * (barWidth + barGap);
2485
+ const colY = baselineY - barHeight;
2486
+ const isPeak = busiest !== null && entry.hour === busiest.hour && entry.tokens > 0;
2487
+ if (isPeak) {
2488
+ 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)"/>`);
2489
+ }
2490
+ bars.push(`<rect x="${colX}" y="${colY}" width="${barWidth}" height="${barHeight}" rx="3" fill="url(#${escapeXml(hodGradId)})" opacity="${0.35 + ratio * 0.65}"/>`);
2491
+ });
2492
+ }
2493
+ [0, 3, 6, 9, 12, 15, 18, 21].forEach((hour) => {
2494
+ const labelX = barAreaX + hour * (barWidth + barGap) + barWidth / 2;
2495
+ 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>`);
2496
+ });
2497
+ return {
2498
+ svg: bars.join(`
2499
+ `),
2500
+ height: chartHeight
2501
+ };
2502
+ }
2503
+ function renderModelMixShift(x, y, width, more, theme, cardAccent) {
2504
+ if (!more.compare || more.compare.modelMixShift.length === 0) {
2505
+ return { svg: "", height: 0 };
2506
+ }
2507
+ const rows = more.compare.modelMixShift.slice(0, 4);
2508
+ const height = 38 + rows.length * 24;
2509
+ const parts = [
2510
+ `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="10" fill="${escapeXml(theme.barTrack)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`,
2511
+ `<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>`,
2512
+ `<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>`
2513
+ ];
2514
+ rows.forEach((row, index) => {
2515
+ const lineY = y + 48 + index * 24;
2516
+ 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>`);
2517
+ 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>`);
2518
+ 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>`);
2519
+ });
2520
+ return { svg: parts.join(`
2521
+ `), height };
2522
+ }
2016
2523
  function renderTerminalCardSvg(output, options) {
2017
2524
  const theme = getCardTheme(options.theme);
2018
2525
  const isDark = options.theme === "dark";
@@ -2021,6 +2528,7 @@ function renderTerminalCardSvg(output, options) {
2021
2528
  const { since, until } = output.dateRange;
2022
2529
  const providers = output.providers;
2023
2530
  const cardAccent = providers.length === 1 ? providers[0]?.colors.primary ?? theme.accent : theme.accent;
2531
+ const barAccent = providers.length > 1 ? isDark ? "#c4d0e0" : "#000000" : cardAccent;
2024
2532
  const providerHeatmaps = providers.map((p) => {
2025
2533
  const heatmapColors = buildHeatmapScale(p.colors, isDark);
2026
2534
  return {
@@ -2050,20 +2558,20 @@ function renderTerminalCardSvg(output, options) {
2050
2558
  }
2051
2559
  sections.push(`<line x1="0" y1="${TITLEBAR_HEIGHT}" x2="${cardWidth}" y2="${TITLEBAR_HEIGHT}" stroke="${escapeXml(theme.titlebarBorder)}" stroke-width="1"/>`);
2052
2560
  y = TITLEBAR_HEIGHT + pad * 0.6;
2053
- sections.push(`<text x="${pad}" y="${y + 16}" font-size="15" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500">` + `<tspan fill="${escapeXml(cardAccent)}">$</tspan>` + `<tspan fill="${escapeXml(theme.fg)}"> tokenleak</tspan>` + `<tspan fill="${escapeXml(cardAccent)}">_</tspan>` + `</text>`);
2561
+ 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>`);
2054
2562
  y += 40;
2055
2563
  const dateRangeText = formatDateRange(since, until);
2056
2564
  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>`);
2057
2565
  y += 40;
2058
2566
  for (let pi = 0;pi < providerHeatmaps.length; pi++) {
2059
2567
  const { provider, heatmap, heatmapColors } = providerHeatmaps[pi];
2060
- const provDotRadius = 5;
2568
+ const provDotRadius = 7;
2061
2569
  const provColor = provider.colors.primary;
2062
- sections.push(`<circle cx="${pad + provDotRadius}" cy="${y + 8}" r="${provDotRadius}" fill="${escapeXml(provColor)}"/>`);
2063
- sections.push(`<text x="${pad + provDotRadius * 2 + 10}" y="${y + 13}" fill="${escapeXml(theme.fg)}" font-size="14" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600">${escapeXml(provider.displayName)}</text>`);
2570
+ sections.push(`<circle cx="${pad + provDotRadius}" cy="${y + 10}" r="${provDotRadius}" fill="${escapeXml(provColor)}"/>`);
2571
+ 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>`);
2064
2572
  const summaryText = `${formatNumber(provider.totalTokens)} tokens \xB7 ${formatCost(provider.totalCost)}`;
2065
- sections.push(`<text x="${cardWidth - pad}" y="${y + 13}" fill="${escapeXml(theme.muted)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="end">${escapeXml(summaryText)}</text>`);
2066
- y += 28;
2573
+ 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>`);
2574
+ y += 32;
2067
2575
  const heatmapSvg = heatmap.svg.replace(/__MUTED__/g, escapeXml(theme.muted));
2068
2576
  sections.push(`<g transform="translate(${pad}, ${y})">`);
2069
2577
  sections.push(heatmapSvg);
@@ -2103,7 +2611,7 @@ function renderTerminalCardSvg(output, options) {
2103
2611
  const stat = row[i];
2104
2612
  const x = pad + i * statColWidth;
2105
2613
  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>`);
2106
- const valueColor = stat.accent ? cardAccent : theme.fg;
2614
+ const valueColor = stat.accent && providers.length === 1 ? cardAccent : theme.fg;
2107
2615
  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>`);
2108
2616
  }
2109
2617
  }
@@ -2114,24 +2622,127 @@ function renderTerminalCardSvg(output, options) {
2114
2622
  y += 8;
2115
2623
  sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
2116
2624
  y += 28;
2117
- 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("TOP MODELS")}</text>`);
2625
+ sections.push(renderSectionHeader(pad, y, "TOP MODELS", theme, cardAccent));
2118
2626
  y += 24;
2119
2627
  const topModels2 = stats.topModels.slice(0, 3);
2628
+ const rankWidth = 28;
2120
2629
  const modelNameWidth = MODEL_NAME_WIDTH;
2121
2630
  const barGap = MODEL_BAR_GAP;
2122
2631
  const percentX = cardWidth - pad;
2123
- const barX = pad + modelNameWidth;
2632
+ const barX = pad + rankWidth + modelNameWidth;
2124
2633
  const barMaxWidth = Math.max(48, percentX - barX - barGap);
2125
2634
  for (const [index, model] of topModels2.entries()) {
2126
2635
  const barWidth = Math.max(4, model.percentage / 100 * barMaxWidth);
2127
- sections.push(`<text x="${pad}" y="${y + MODEL_BAR_HEIGHT - 1}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="400">${escapeXml(model.model)}</text>`);
2128
- sections.push(`<rect x="${barX}" y="${y}" width="${barMaxWidth}" height="${MODEL_BAR_HEIGHT}" rx="4" fill="${escapeXml(theme.barTrack)}"/>`);
2636
+ 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>`);
2637
+ 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>`);
2638
+ const trackColor = isDark ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.06)";
2639
+ sections.push(`<rect x="${barX}" y="${y}" width="${barMaxWidth}" height="${MODEL_BAR_HEIGHT}" rx="6" fill="${escapeXml(trackColor)}"/>`);
2129
2640
  const gradId = `grad-${index}-${model.model.replace(/[^a-zA-Z0-9]/g, "")}`;
2130
- sections.push(`<defs><linearGradient id="${escapeXml(gradId)}" x1="0%" y1="0%" x2="100%" y2="0%">` + `<stop offset="0%" stop-color="${escapeXml(cardAccent)}44"/>` + `<stop offset="100%" stop-color="${escapeXml(cardAccent)}"/>` + `</linearGradient></defs>`);
2131
- sections.push(`<rect x="${barX}" y="${y}" width="${barWidth}" height="${MODEL_BAR_HEIGHT}" rx="4" fill="url(#${escapeXml(gradId)})"/>`);
2132
- sections.push(`<text x="${percentX}" y="${y + MODEL_BAR_HEIGHT - 1}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="end">${escapeXml(`${model.percentage.toFixed(0)}%`)}</text>`);
2641
+ 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>`);
2642
+ sections.push(`<rect x="${barX}" y="${y}" width="${barWidth}" height="${MODEL_BAR_HEIGHT}" rx="6" fill="url(#${escapeXml(gradId)})"/>`);
2643
+ 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>`);
2133
2644
  y += 32;
2134
2645
  }
2646
+ if (options.more && output.more) {
2647
+ const more = output.more;
2648
+ const cardGap = 16;
2649
+ const detailCardWidth = (contentWidth - cardGap) / 2;
2650
+ y += 8;
2651
+ sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
2652
+ y += 28;
2653
+ sections.push(renderSectionHeader(pad, y, "MORE", theme, cardAccent));
2654
+ y += 24;
2655
+ const efficiencyLines = [
2656
+ {
2657
+ label: "Input / Output",
2658
+ value: more.inputOutput.inputPerOutput === null ? "n/a" : `${more.inputOutput.inputPerOutput.toFixed(2)} : 1`,
2659
+ accent: true
2660
+ },
2661
+ {
2662
+ label: "Output / Input",
2663
+ value: formatRatio(more.inputOutput.outputPerInput)
2664
+ },
2665
+ {
2666
+ label: "Output Share",
2667
+ value: formatPercentage(more.inputOutput.outputShare)
2668
+ }
2669
+ ];
2670
+ sections.push(renderMetricCard(pad, y, detailCardWidth, "INPUT / OUTPUT", efficiencyLines, theme, cardAccent));
2671
+ const burnLines = [
2672
+ {
2673
+ label: "Projected Cost",
2674
+ value: formatCost(more.monthlyBurn.projectedCost),
2675
+ accent: true
2676
+ },
2677
+ {
2678
+ label: "Projected Tokens",
2679
+ value: formatNumber(more.monthlyBurn.projectedTokens)
2680
+ },
2681
+ {
2682
+ label: "Based On",
2683
+ value: `${more.monthlyBurn.observedDays} / ${more.monthlyBurn.calendarDays} days`
2684
+ }
2685
+ ];
2686
+ sections.push(renderMetricCard(pad + detailCardWidth + cardGap, y, detailCardWidth, "PROJECTED MONTHLY BURN", burnLines, theme, cardAccent));
2687
+ y += 38 + Math.max(efficiencyLines.length, burnLines.length) * 22 + 16;
2688
+ const cacheLines = [
2689
+ {
2690
+ label: "Cache Reads",
2691
+ value: formatNumber(more.cacheEconomics.readTokens),
2692
+ accent: true
2693
+ },
2694
+ {
2695
+ label: "Cache Writes",
2696
+ value: formatNumber(more.cacheEconomics.writeTokens)
2697
+ },
2698
+ {
2699
+ label: "Read Coverage",
2700
+ value: formatPercentage(more.cacheEconomics.readCoverage)
2701
+ },
2702
+ {
2703
+ label: "Reuse Ratio",
2704
+ value: formatRatio(more.cacheEconomics.reuseRatio)
2705
+ }
2706
+ ];
2707
+ sections.push(renderMetricCard(pad, y, detailCardWidth, "CACHE ECONOMICS", cacheLines, theme, cardAccent));
2708
+ const sessionLines = [
2709
+ {
2710
+ label: "Sessions",
2711
+ value: String(more.sessionMetrics.totalSessions),
2712
+ accent: true
2713
+ },
2714
+ {
2715
+ label: "Avg Tokens",
2716
+ value: formatNumber(more.sessionMetrics.averageTokens)
2717
+ },
2718
+ {
2719
+ label: "Avg Messages",
2720
+ value: more.sessionMetrics.averageMessages.toFixed(1)
2721
+ },
2722
+ {
2723
+ label: "Avg Duration",
2724
+ value: formatDuration(more.sessionMetrics.averageDurationMs)
2725
+ },
2726
+ {
2727
+ label: "Longest Session",
2728
+ value: more.sessionMetrics.longestSession ? formatSessionSummary(more.sessionMetrics.longestSession) : "n/a"
2729
+ },
2730
+ {
2731
+ label: "Top Project",
2732
+ value: more.sessionMetrics.topProject ? truncateText(more.sessionMetrics.topProject.name, 20) : "n/a"
2733
+ }
2734
+ ];
2735
+ sections.push(renderMetricCard(pad + detailCardWidth + cardGap, y, detailCardWidth, "SESSION STATS", sessionLines, theme, cardAccent));
2736
+ y += 38 + Math.max(cacheLines.length, sessionLines.length) * 22 + 16;
2737
+ const hourChart = renderHourOfDayChart(pad, y, contentWidth, more.hourOfDay, theme, cardAccent, providers, isDark);
2738
+ sections.push(hourChart.svg);
2739
+ y += hourChart.height + 16;
2740
+ const mixShift = renderModelMixShift(pad, y, contentWidth, more, theme, cardAccent);
2741
+ if (mixShift.height > 0) {
2742
+ sections.push(mixShift.svg);
2743
+ y += mixShift.height + 12;
2744
+ }
2745
+ }
2135
2746
  y += pad * 0.5;
2136
2747
  const cardHeight = y;
2137
2748
  const svg = sections.join(`
@@ -2209,142 +2820,123 @@ function intensityColor(value, max) {
2209
2820
  return "dim";
2210
2821
  }
2211
2822
 
2212
- // packages/renderers/dist/terminal/heatmap.js
2213
- var DAY_LABELS2 = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
2214
- var MONTH_LABELS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
2215
- var DAY_LABEL_WIDTH2 = 4;
2216
- var WEEK_COLUMN_WIDTH = 2;
2217
- var LEGEND_TEXT = "Less";
2218
- var LEGEND_TEXT_MORE = "More";
2219
- function buildUsageMap(daily) {
2220
- const map = new Map;
2221
- for (const entry of daily) {
2222
- map.set(entry.date, (map.get(entry.date) ?? 0) + entry.totalTokens);
2223
- }
2224
- return map;
2225
- }
2226
- function computeQuantiles2(values) {
2227
- const nonZero = values.filter((value) => value > 0).sort((a, b) => a - b);
2228
- if (nonZero.length === 0)
2229
- return [0, 0, 0];
2230
- const quantile = (ratio) => {
2231
- const index = Math.floor(ratio * (nonZero.length - 1));
2232
- return nonZero[index] ?? 0;
2233
- };
2234
- return [quantile(0.25), quantile(0.5), quantile(0.75)];
2235
- }
2236
- function getHeatmapBlock(tokens, quantiles) {
2237
- if (tokens <= 0)
2238
- return HEATMAP_BLOCKS.EMPTY;
2239
- if (tokens <= quantiles[0])
2240
- return HEATMAP_BLOCKS.LIGHT;
2241
- if (tokens <= quantiles[1])
2242
- return HEATMAP_BLOCKS.MEDIUM;
2243
- if (tokens <= quantiles[2])
2244
- return HEATMAP_BLOCKS.DARK;
2245
- return HEATMAP_BLOCKS.FULL;
2823
+ // packages/renderers/dist/terminal/layout.js
2824
+ function stripAnsi(text2) {
2825
+ return text2.replace(/\u001B\[[0-9;?]*[A-Za-z]/g, "");
2246
2826
  }
2247
- function formatDate(date) {
2248
- const year = date.getUTCFullYear();
2249
- const month = String(date.getUTCMonth() + 1).padStart(2, "0");
2250
- const day = String(date.getUTCDate()).padStart(2, "0");
2251
- return `${year}-${month}-${day}`;
2252
- }
2253
- function buildMonthHeader(weeks) {
2254
- const header = Array.from({ length: weeks.length * WEEK_COLUMN_WIDTH }, () => " ");
2255
- let lastMonth = -1;
2256
- let nextFreeIndex = 0;
2257
- for (let weekIndex = 0;weekIndex < weeks.length; weekIndex++) {
2258
- const firstDay = weeks[weekIndex]?.[0];
2259
- if (!firstDay)
2260
- continue;
2261
- const month = firstDay.getUTCMonth();
2262
- if (month === lastMonth)
2263
- continue;
2264
- lastMonth = month;
2265
- const desiredStart = weekIndex * WEEK_COLUMN_WIDTH;
2266
- const startIndex = Math.max(desiredStart, nextFreeIndex);
2267
- const remaining = header.length - startIndex;
2268
- if (remaining <= 0)
2269
- continue;
2270
- const fullLabel = MONTH_LABELS[month] ?? "";
2271
- const label = remaining >= fullLabel.length ? fullLabel : remaining >= 2 ? fullLabel.slice(0, 2) : fullLabel.slice(0, 1);
2272
- for (let offset = 0;offset < label.length; offset++) {
2273
- header[startIndex + offset] = label[offset] ?? " ";
2274
- }
2275
- nextFreeIndex = startIndex + label.length + 1;
2276
- }
2277
- return `${" ".repeat(DAY_LABEL_WIDTH2)}${header.join("")}`;
2827
+ function visibleLength(text2) {
2828
+ return stripAnsi(text2).length;
2278
2829
  }
2279
- function buildWeeks(startDate, endDate) {
2280
- const alignedStart = new Date(startDate);
2281
- alignedStart.setUTCDate(alignedStart.getUTCDate() - alignedStart.getUTCDay());
2282
- const weeks = [];
2283
- const cursor = new Date(alignedStart);
2284
- while (cursor <= endDate) {
2285
- const week = [];
2286
- for (let day = 0;day < 7; day++) {
2287
- week.push(new Date(cursor));
2288
- cursor.setUTCDate(cursor.getUTCDate() + 1);
2289
- }
2290
- weeks.push(week);
2291
- }
2292
- return weeks;
2830
+ function padVisible(text2, width) {
2831
+ const padding = Math.max(0, width - visibleLength(text2));
2832
+ return text2 + " ".repeat(padding);
2293
2833
  }
2294
- function renderTerminalHeatmap(daily, options) {
2295
- if (daily.length === 0) {
2296
- return " No usage data available.";
2297
- }
2298
- const usageMap = buildUsageMap(daily);
2299
- const quantiles = computeQuantiles2(Array.from(usageMap.values()));
2300
- const maxTokens = Math.max(...usageMap.values(), 0);
2301
- const dates = daily.map((entry) => entry.date).sort();
2302
- const startDate = new Date(`${dates[0]}T00:00:00Z`);
2303
- const endDate = new Date(`${dates[dates.length - 1]}T00:00:00Z`);
2304
- const weeks = buildWeeks(startDate, endDate);
2305
- const availableColumns = Math.max(WEEK_COLUMN_WIDTH, options.width - DAY_LABEL_WIDTH2);
2306
- const maxWeeks = Math.max(1, Math.floor(availableColumns / WEEK_COLUMN_WIDTH));
2307
- const displayWeeks = weeks.slice(Math.max(0, weeks.length - maxWeeks));
2308
- const lines = [buildMonthHeader(displayWeeks)];
2309
- for (let dayIndex = 0;dayIndex < 7; dayIndex++) {
2310
- const label = DAY_LABELS2[dayIndex] ?? " ";
2311
- let line = `${label} `.slice(0, DAY_LABEL_WIDTH2);
2312
- for (const week of displayWeeks) {
2313
- const date = week[dayIndex];
2314
- if (!date || date < startDate || date > endDate) {
2315
- line += `${HEATMAP_BLOCKS.EMPTY} `;
2834
+ function truncateVisible(text2, width) {
2835
+ if (width <= 0)
2836
+ return "";
2837
+ const plain = stripAnsi(text2);
2838
+ if (plain.length <= width)
2839
+ return text2;
2840
+ const limit = width <= 1 ? width : width - 1;
2841
+ let visibleCount = 0;
2842
+ let index = 0;
2843
+ let result = "";
2844
+ let sawAnsi = false;
2845
+ while (index < text2.length && visibleCount < limit) {
2846
+ if (text2[index] === "\x1B") {
2847
+ const match = text2.slice(index).match(/^\u001B\[[0-9;?]*[A-Za-z]/);
2848
+ if (match) {
2849
+ result += match[0];
2850
+ index += match[0].length;
2851
+ sawAnsi = true;
2316
2852
  continue;
2317
2853
  }
2318
- const dateString = formatDate(date);
2319
- const tokens = usageMap.get(dateString) ?? 0;
2320
- const block = getHeatmapBlock(tokens, quantiles);
2321
- const color = intensityColor(tokens, maxTokens);
2322
- line += `${colorize(block, color, options.noColor)} `;
2323
2854
  }
2324
- lines.push(line.trimEnd());
2855
+ result += text2[index];
2856
+ index += 1;
2857
+ visibleCount += 1;
2325
2858
  }
2326
- const legend = `${" ".repeat(DAY_LABEL_WIDTH2)}${LEGEND_TEXT} ${[
2327
- HEATMAP_BLOCKS.EMPTY,
2328
- HEATMAP_BLOCKS.LIGHT,
2329
- HEATMAP_BLOCKS.MEDIUM,
2330
- HEATMAP_BLOCKS.DARK,
2331
- HEATMAP_BLOCKS.FULL
2332
- ].join("")} ${LEGEND_TEXT_MORE}`;
2333
- lines.push(legend);
2334
- return lines.join(`
2335
- `);
2859
+ return sawAnsi ? `${result}\u2026\x1B[0m` : `${result}\u2026`;
2860
+ }
2861
+ function renderColumns(left, right, totalWidth, leftRatio = 0.5, gutter = 3) {
2862
+ const safeWidth = Math.max(12, totalWidth);
2863
+ const leftWidth = Math.max(18, Math.floor((safeWidth - gutter) * leftRatio));
2864
+ const rightWidth = Math.max(18, safeWidth - leftWidth - gutter);
2865
+ const rows = Math.max(left.length, right.length);
2866
+ const lines = [];
2867
+ for (let index = 0;index < rows; index += 1) {
2868
+ const leftLine = truncateVisible(left[index] ?? "", leftWidth);
2869
+ const rightLine = truncateVisible(right[index] ?? "", rightWidth);
2870
+ lines.push(`${padVisible(leftLine, leftWidth)}${" ".repeat(gutter)}${rightLine}`);
2871
+ }
2872
+ return lines;
2336
2873
  }
2337
2874
 
2338
- // packages/renderers/dist/terminal/dashboard.js
2875
+ // packages/renderers/dist/terminal/compact.js
2339
2876
  var BOX_H = "\u2500";
2340
2877
  var BOX_V = "\u2502";
2341
2878
  var BOX_TL = "\u250C";
2342
2879
  var BOX_TR = "\u2510";
2343
2880
  var BOX_BL = "\u2514";
2344
2881
  var BOX_BR = "\u2518";
2345
- var DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
2346
- var BAR_CHAR = "\u2588";
2347
- var MAX_BAR_LENGTH = 20;
2882
+ function boxedHeader(title, width, noColor2) {
2883
+ const inner = Math.max(1, width - 2);
2884
+ const padded = ` ${title} `;
2885
+ const remaining = Math.max(0, inner - padded.length);
2886
+ const left = Math.floor(remaining / 2);
2887
+ const right = remaining - left;
2888
+ const titleLine = `${BOX_V}${" ".repeat(left)}${colorize(padded, "bold", noColor2)}${" ".repeat(right)}${BOX_V}`;
2889
+ return [
2890
+ `${BOX_TL}${BOX_H.repeat(inner)}${BOX_TR}`,
2891
+ titleLine,
2892
+ `${BOX_BL}${BOX_H.repeat(inner)}${BOX_BR}`
2893
+ ].join(`
2894
+ `);
2895
+ }
2896
+ function renderSummaryParts(parts, width, noColor2) {
2897
+ const left = parts.filter((_, index) => index % 2 === 0).map((part) => colorize(part, "cyan", noColor2));
2898
+ const right = parts.filter((_, index) => index % 2 === 1).map((part) => colorize(part, "green", noColor2));
2899
+ return renderColumns(left, right, Math.max(24, width - 2), 0.5, 2).map((line) => ` ${line}`).join(`
2900
+ `);
2901
+ }
2902
+ function renderCompactDashboard(model, options) {
2903
+ const width = options.width;
2904
+ const noColor2 = options.noColor;
2905
+ const lines = [
2906
+ boxedHeader("Tokenleak", width, noColor2),
2907
+ "",
2908
+ truncateVisible(` Range ${model.rangeLabel}`, width),
2909
+ "",
2910
+ renderSummaryParts(model.overview.summary, width, noColor2),
2911
+ ...model.overview.trend ? ["", truncateVisible(` Recent Trend ${model.overview.trend}`, width)] : [],
2912
+ ""
2913
+ ];
2914
+ if (model.overview.metrics.length > 0) {
2915
+ const keyMetrics = model.overview.metrics.slice(0, 4).map((metric) => {
2916
+ const text2 = `${metric.label}: ${metric.value}`;
2917
+ return ` ${truncateVisible(text2, width - 2)}`;
2918
+ });
2919
+ lines.push(...keyMetrics, "");
2920
+ }
2921
+ if (model.activeProviders.length > 0) {
2922
+ lines.push(colorize(" Providers", "bold", noColor2));
2923
+ for (const provider of model.activeProviders.slice(0, 4)) {
2924
+ const tokensMetric = provider.metrics.find((entry) => entry.label === "Total Tokens");
2925
+ const summary = `${provider.provider.displayName} ${tokensMetric?.value ?? ""} ${provider.lastActiveDate ? `| ${provider.lastActiveDate}` : ""}`.trim();
2926
+ lines.push(truncateVisible(` ${summary}`, width));
2927
+ }
2928
+ lines.push("");
2929
+ }
2930
+ if (model.inactiveProviders.length > 0) {
2931
+ lines.push(truncateVisible(` No activity: ${model.inactiveProviders.join(", ")}`, width));
2932
+ } else if (model.activeProviders.length === 0) {
2933
+ lines.push(" No provider activity in the selected range.");
2934
+ }
2935
+ return lines.filter((line, index, array) => !(line === "" && array[index - 1] === "")).join(`
2936
+ `);
2937
+ }
2938
+
2939
+ // packages/renderers/dist/terminal/dashboard-model.js
2348
2940
  function formatTokens(count) {
2349
2941
  if (count >= 1e6) {
2350
2942
  return `${(count / 1e6).toFixed(1)}M`;
@@ -2363,180 +2955,527 @@ function formatPercent(rate) {
2363
2955
  function formatSharePercent(percentage) {
2364
2956
  return `${percentage.toFixed(0)}%`;
2365
2957
  }
2366
- function divider(width) {
2367
- return BOX_H.repeat(width);
2958
+ function buildRangeLabel(output) {
2959
+ return `${output.dateRange.since} -> ${output.dateRange.until}`;
2368
2960
  }
2369
- function boxedHeader(title, width, noColor2) {
2370
- const inner = width - 2;
2371
- const padded = ` ${title} `;
2372
- const remaining = Math.max(0, inner - padded.length);
2373
- const left = Math.floor(remaining / 2);
2374
- const right = remaining - left;
2375
- const content = `${BOX_H.repeat(left)}${padded}${BOX_H.repeat(right)}`;
2376
- const top = `${BOX_TL}${BOX_H.repeat(inner)}${BOX_TR}`;
2377
- const headerLine = `${BOX_V}${colorize(content, "bold", noColor2)}${BOX_V}`;
2378
- const bottom = `${BOX_BL}${BOX_H.repeat(inner)}${BOX_BR}`;
2379
- return [top, headerLine, bottom].join(`
2380
- `);
2961
+ function buildSparkline(daily, points = 14) {
2962
+ const blocks = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
2963
+ if (daily.length === 0)
2964
+ return "\xB7".repeat(points);
2965
+ const values = daily.slice().sort((left, right) => left.date.localeCompare(right.date)).slice(-points).map((entry) => entry.totalTokens);
2966
+ const max = Math.max(...values, 0);
2967
+ if (max <= 0) {
2968
+ return "\xB7".repeat(values.length);
2969
+ }
2970
+ return values.map((value) => {
2971
+ const index = Math.min(blocks.length - 1, Math.round(value / max * (blocks.length - 1)));
2972
+ return blocks[index] ?? blocks[0];
2973
+ }).join("");
2381
2974
  }
2382
- function dayBar(tokens, maxTokens, noColor2) {
2383
- if (maxTokens <= 0)
2384
- return "";
2385
- const length = Math.round(tokens / maxTokens * MAX_BAR_LENGTH);
2386
- const bar = BAR_CHAR.repeat(length);
2387
- return colorize(bar, "green", noColor2);
2975
+ function getLastActiveDate(provider) {
2976
+ const activeDays = provider.daily.filter((entry) => entry.totalTokens > 0);
2977
+ return activeDays.at(-1)?.date ?? null;
2388
2978
  }
2389
- function renderStats(stats, width, noColor2) {
2390
- const lines = [];
2391
- const labelWidth = 20;
2392
- const entries = [
2393
- ["Current Streak", `${stats.currentStreak}d`],
2394
- ["Longest Streak", `${stats.longestStreak}d`],
2395
- ["Total Tokens", formatTokens(stats.totalTokens)],
2396
- ["Total Cost", formatCost2(stats.totalCost)],
2397
- ["30d Tokens", formatTokens(stats.rolling30dTokens)],
2398
- ["30d Cost", formatCost2(stats.rolling30dCost)],
2399
- ["7d Tokens", formatTokens(stats.rolling7dTokens)],
2400
- ["7d Cost", formatCost2(stats.rolling7dCost)],
2401
- ["Avg Daily Tokens", formatTokens(stats.averageDailyTokens)],
2402
- ["Avg Daily Cost", formatCost2(stats.averageDailyCost)],
2403
- ["Cache Hit Rate", formatPercent(stats.cacheHitRate)],
2404
- ["Active Days", `${stats.activeDays} / ${stats.totalDays}`]
2979
+ function buildMetricEntries(stats) {
2980
+ const metrics = [
2981
+ { label: "Current Streak", value: `${stats.currentStreak}d` },
2982
+ { label: "Longest Streak", value: `${stats.longestStreak}d` },
2983
+ { label: "Total Tokens", value: formatTokens(stats.totalTokens) },
2984
+ { label: "Total Cost", value: formatCost2(stats.totalCost) }
2405
2985
  ];
2406
- if (stats.peakDay) {
2407
- entries.push(["Peak Day", `${stats.peakDay.date} (${formatTokens(stats.peakDay.tokens)})`]);
2986
+ if (stats.rolling30dTokens > 0 || stats.rolling30dCost > 0) {
2987
+ metrics.push({ label: "30d Tokens", value: formatTokens(stats.rolling30dTokens) });
2988
+ metrics.push({ label: "30d Cost", value: formatCost2(stats.rolling30dCost) });
2408
2989
  }
2409
- for (const [label, value] of entries) {
2410
- const line = ` ${label.padEnd(labelWidth)} ${colorize(value, "cyan", noColor2)}`;
2411
- lines.push(line.length > width ? line.slice(0, width) : line);
2990
+ if (stats.rolling7dTokens > 0 || stats.rolling7dCost > 0) {
2991
+ metrics.push({ label: "7d Tokens", value: formatTokens(stats.rolling7dTokens) });
2992
+ metrics.push({ label: "7d Cost", value: formatCost2(stats.rolling7dCost) });
2412
2993
  }
2413
- return lines.join(`
2414
- `);
2415
- }
2416
- function renderDayOfWeek(stats, width, noColor2) {
2417
- const lines = [];
2418
- const maxTokens = Math.max(...stats.dayOfWeek.map((d) => d.tokens), 0);
2419
- for (const entry of stats.dayOfWeek) {
2420
- const label = DAY_NAMES[entry.day] ?? `Day${entry.day}`;
2421
- const bar = dayBar(entry.tokens, maxTokens, noColor2);
2422
- const tokenStr = formatTokens(entry.tokens);
2423
- const line = ` ${label} ${bar} ${tokenStr}`;
2424
- lines.push(line.length > width ? line.slice(0, width) : line);
2994
+ if (stats.activeDays > 0) {
2995
+ metrics.push({ label: "Avg Daily Tokens", value: formatTokens(stats.averageDailyTokens) });
2996
+ metrics.push({ label: "Avg Daily Cost", value: formatCost2(stats.averageDailyCost) });
2425
2997
  }
2426
- return lines.join(`
2427
- `);
2428
- }
2429
- function renderTopModels(stats, width, noColor2) {
2430
- const lines = [];
2431
- const nameWidth = Math.min(28, Math.max(16, Math.floor(width * 0.35)));
2432
- const pctWidth = 4;
2433
- const barGap = 2;
2434
- const barWidth = Math.max(8, width - nameWidth - pctWidth - 6);
2435
- for (const model of stats.topModels.slice(0, 5)) {
2436
- const pct = formatSharePercent(model.percentage);
2437
- const normalizedName = model.model.length > nameWidth ? `${model.model.slice(0, nameWidth - 1)}\u2026` : model.model;
2438
- const fillLength = Math.max(1, Math.round(model.percentage / 100 * barWidth));
2439
- const fill = colorize(BAR_CHAR.repeat(fillLength), "green", noColor2);
2440
- const track = "\u2591".repeat(Math.max(0, barWidth - fillLength));
2441
- const line = ` ${colorize(normalizedName.padEnd(nameWidth), "yellow", noColor2)}${" ".repeat(barGap)}${fill}${track}${" ".repeat(barGap)}${pct.padStart(pctWidth)}`;
2442
- lines.push(line);
2998
+ if (stats.totalTokens > 0) {
2999
+ metrics.push({ label: "Cache Hit Rate", value: formatPercent(stats.cacheHitRate) });
2443
3000
  }
2444
- return lines.join(`
2445
- `);
3001
+ metrics.push({ label: "Active Days", value: `${stats.activeDays} / ${stats.totalDays}` });
3002
+ if (stats.peakDay) {
3003
+ metrics.push({
3004
+ label: "Peak Day",
3005
+ value: `${stats.peakDay.date} (${formatTokens(stats.peakDay.tokens)})`
3006
+ });
3007
+ }
3008
+ return metrics;
2446
3009
  }
2447
- function renderInsights(stats, noColor2) {
2448
- const insights = [];
2449
- if (stats.currentStreak > 7) {
2450
- insights.push(`You have a ${stats.currentStreak}-day coding streak going!`);
3010
+ function buildProviderSummary(provider, stats) {
3011
+ const parts = [
3012
+ `${formatTokens(stats.totalTokens)} tokens`,
3013
+ formatCost2(stats.totalCost),
3014
+ `${stats.activeDays} active day${stats.activeDays === 1 ? "" : "s"}`
3015
+ ];
3016
+ const lastActiveDate = getLastActiveDate(provider);
3017
+ if (lastActiveDate) {
3018
+ parts.push(`last active ${lastActiveDate}`);
3019
+ }
3020
+ if (stats.topModels[0]) {
3021
+ parts.push(`${stats.topModels[0].model} ${formatSharePercent(stats.topModels[0].percentage)}`);
2451
3022
  }
2452
- if (stats.cacheHitRate > 0.5) {
2453
- insights.push(`Cache hit rate is ${formatPercent(stats.cacheHitRate)} - good cache reuse.`);
3023
+ return parts;
3024
+ }
3025
+ function buildInsights(stats) {
3026
+ const insights = [];
3027
+ if (stats.currentStreak >= 7) {
3028
+ insights.push(`${stats.currentStreak}-day streak is still alive.`);
2454
3029
  }
2455
- if (stats.cacheHitRate < 0.1 && stats.totalTokens > 0) {
2456
- insights.push("Cache hit rate is low - consider enabling prompt caching.");
3030
+ if (stats.cacheHitRate >= 0.5) {
3031
+ insights.push(`Cache reuse is strong at ${formatPercent(stats.cacheHitRate)}.`);
3032
+ } else if (stats.cacheHitRate < 0.1 && stats.totalTokens > 0) {
3033
+ insights.push("Cache reuse is low. There is room to improve prompt caching.");
2457
3034
  }
2458
3035
  if (stats.peakDay) {
2459
- insights.push(`Peak usage was on ${stats.peakDay.date} with ${formatTokens(stats.peakDay.tokens)} tokens.`);
3036
+ insights.push(`Peak usage hit ${formatTokens(stats.peakDay.tokens)} on ${stats.peakDay.date}.`);
2460
3037
  }
2461
- if (insights.length === 0)
2462
- return "";
2463
- return insights.map((i) => ` ${colorize("*", "green", noColor2)} ${i}`).join(`
2464
- `);
3038
+ if (stats.topModels[0] && stats.topModels[0].percentage >= 70) {
3039
+ insights.push(`${stats.topModels[0].model} dominates at ${formatSharePercent(stats.topModels[0].percentage)}.`);
3040
+ }
3041
+ return insights;
2465
3042
  }
2466
- function renderProviderSection(provider, stats, width, noColor2, showInsights) {
2467
- const sections = [];
2468
- sections.push(boxedHeader(provider.displayName, width, noColor2));
2469
- sections.push("");
2470
- sections.push(colorize(" Heatmap", "bold", noColor2));
2471
- sections.push(renderTerminalHeatmap(provider.daily, { width, noColor: noColor2 }));
2472
- sections.push("");
2473
- sections.push(colorize(" Stats", "bold", noColor2));
2474
- sections.push(renderStats(stats, width, noColor2));
2475
- if (stats.dayOfWeek.length > 0) {
2476
- sections.push("");
2477
- sections.push(colorize(" Day of Week", "bold", noColor2));
2478
- sections.push(renderDayOfWeek(stats, width, noColor2));
3043
+ function buildDayOfWeekPatterns(stats) {
3044
+ const maxTokens = Math.max(...stats.dayOfWeek.map((entry) => entry.tokens), 0);
3045
+ if (maxTokens <= 0) {
3046
+ return [];
2479
3047
  }
2480
- if (stats.topModels.length > 0) {
2481
- sections.push("");
2482
- sections.push(colorize(" Top Models", "bold", noColor2));
2483
- sections.push(renderTopModels(stats, width, noColor2));
3048
+ return stats.dayOfWeek.filter((entry) => entry.tokens > 0).map((entry) => ({
3049
+ label: entry.label,
3050
+ value: formatTokens(entry.tokens),
3051
+ share: entry.tokens / maxTokens
3052
+ }));
3053
+ }
3054
+ function buildTopModelPatterns(stats) {
3055
+ return stats.topModels.slice(0, 5).filter((entry) => entry.tokens > 0).map((entry) => ({
3056
+ label: entry.model,
3057
+ value: formatSharePercent(entry.percentage),
3058
+ share: Math.max(0.03, entry.percentage / 100)
3059
+ }));
3060
+ }
3061
+ function buildProviderModel(provider, until) {
3062
+ const stats = aggregate(provider.daily, until);
3063
+ if (stats.totalTokens <= 0) {
3064
+ return null;
3065
+ }
3066
+ return {
3067
+ provider,
3068
+ stats,
3069
+ summary: buildProviderSummary(provider, stats),
3070
+ trend: buildSparkline(provider.daily),
3071
+ metrics: buildMetricEntries(stats),
3072
+ dayOfWeek: buildDayOfWeekPatterns(stats),
3073
+ topModels: buildTopModelPatterns(stats),
3074
+ insights: buildInsights(stats),
3075
+ lastActiveDate: getLastActiveDate(provider)
3076
+ };
3077
+ }
3078
+ function buildOverview(output, activeProviders) {
3079
+ const mergedDaily = activeProviders.length > 0 ? mergeProviderData(activeProviders.map((entry) => entry.provider)) : [];
3080
+ const summary = [
3081
+ `${formatTokens(output.aggregated.totalTokens)} tokens`,
3082
+ formatCost2(output.aggregated.totalCost),
3083
+ `${activeProviders.length} active provider${activeProviders.length === 1 ? "" : "s"}`,
3084
+ `${output.aggregated.activeDays} active day${output.aggregated.activeDays === 1 ? "" : "s"}`
3085
+ ];
3086
+ const metrics = buildMetricEntries(output.aggregated);
3087
+ const maxProviderTokens = Math.max(...activeProviders.map((entry) => entry.stats.totalTokens), 0);
3088
+ const providerLeaders = activeProviders.slice().sort((left, right) => right.stats.totalTokens - left.stats.totalTokens).map((entry) => ({
3089
+ label: entry.provider.displayName,
3090
+ value: formatTokens(entry.stats.totalTokens),
3091
+ share: maxProviderTokens > 0 ? entry.stats.totalTokens / maxProviderTokens : 0
3092
+ }));
3093
+ return {
3094
+ summary,
3095
+ trend: buildSparkline(mergedDaily),
3096
+ metrics,
3097
+ providerLeaders
3098
+ };
3099
+ }
3100
+ function resolveDashboardMode(width) {
3101
+ if (width < 32)
3102
+ return "summary";
3103
+ if (width < 56)
3104
+ return "compact";
3105
+ return "full";
3106
+ }
3107
+ function buildDashboardModel(output, options) {
3108
+ const activeProviders = [];
3109
+ const inactiveProviders = [];
3110
+ for (const provider of output.providers) {
3111
+ const model = buildProviderModel(provider, output.dateRange.until);
3112
+ if (model) {
3113
+ activeProviders.push(model);
3114
+ } else {
3115
+ inactiveProviders.push(provider.displayName);
3116
+ }
3117
+ }
3118
+ return {
3119
+ mode: resolveDashboardMode(options.width),
3120
+ rangeLabel: buildRangeLabel(output),
3121
+ activeProviders,
3122
+ inactiveProviders,
3123
+ overview: buildOverview(output, activeProviders)
3124
+ };
3125
+ }
3126
+
3127
+ // packages/renderers/dist/shared/heatmap-model.js
3128
+ var MONTH_LABELS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
3129
+ function formatDate(date) {
3130
+ const year = date.getUTCFullYear();
3131
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
3132
+ const day = String(date.getUTCDate()).padStart(2, "0");
3133
+ return `${year}-${month}-${day}`;
3134
+ }
3135
+ function buildUsageMap(daily) {
3136
+ const map = new Map;
3137
+ for (const entry of daily) {
3138
+ map.set(entry.date, (map.get(entry.date) ?? 0) + entry.totalTokens);
3139
+ }
3140
+ return map;
3141
+ }
3142
+ function computeQuantiles2(values) {
3143
+ const nonZero = values.filter((value) => value > 0).sort((a, b) => a - b);
3144
+ if (nonZero.length === 0)
3145
+ return [0, 0, 0];
3146
+ const quantile = (ratio) => {
3147
+ const index = Math.floor(ratio * (nonZero.length - 1));
3148
+ return nonZero[index] ?? 0;
3149
+ };
3150
+ return [quantile(0.25), quantile(0.5), quantile(0.75)];
3151
+ }
3152
+ function getLevel2(tokens, quantiles) {
3153
+ if (tokens <= 0)
3154
+ return 0;
3155
+ if (tokens <= quantiles[0])
3156
+ return 1;
3157
+ if (tokens <= quantiles[1])
3158
+ return 2;
3159
+ if (tokens <= quantiles[2])
3160
+ return 3;
3161
+ return 4;
3162
+ }
3163
+ function alignToSunday(date) {
3164
+ const aligned = new Date(date);
3165
+ aligned.setUTCDate(aligned.getUTCDate() - aligned.getUTCDay());
3166
+ return aligned;
3167
+ }
3168
+ function buildHeatmapModel(daily, range) {
3169
+ if (daily.length === 0) {
3170
+ return null;
3171
+ }
3172
+ const dates = daily.map((entry) => entry.date).sort();
3173
+ const since = range?.since ?? dates[0];
3174
+ const until = range?.until ?? dates[dates.length - 1];
3175
+ const startDate = alignToSunday(new Date(`${since}T00:00:00Z`));
3176
+ const endDate = new Date(`${until}T00:00:00Z`);
3177
+ const usageMap = buildUsageMap(daily);
3178
+ const usageValues = Array.from(usageMap.values());
3179
+ const quantiles = computeQuantiles2(usageValues);
3180
+ const maxTokens = usageValues.reduce((max, value) => value > max ? value : max, 0);
3181
+ const weeks = [];
3182
+ const monthMarkers = [];
3183
+ let weekIndex = 0;
3184
+ let lastMonth = -1;
3185
+ for (let cursor = new Date(startDate);cursor <= endDate; weekIndex += 1) {
3186
+ const weekDays = [];
3187
+ const month = cursor.getUTCMonth();
3188
+ const year = cursor.getUTCFullYear();
3189
+ if (month !== lastMonth) {
3190
+ monthMarkers.push({
3191
+ label: MONTH_LABELS[month] ?? "",
3192
+ month,
3193
+ year,
3194
+ weekIndex
3195
+ });
3196
+ lastMonth = month;
3197
+ }
3198
+ const dayCursor = new Date(cursor);
3199
+ for (let dayIndex = 0;dayIndex < 7; dayIndex += 1) {
3200
+ const dateString = formatDate(dayCursor);
3201
+ const tokens = usageMap.get(dateString) ?? 0;
3202
+ weekDays.push({
3203
+ date: dateString,
3204
+ tokens,
3205
+ level: getLevel2(tokens, quantiles),
3206
+ dayIndex,
3207
+ weekIndex
3208
+ });
3209
+ dayCursor.setUTCDate(dayCursor.getUTCDate() + 1);
3210
+ }
3211
+ weeks.push({
3212
+ index: weekIndex,
3213
+ days: weekDays
3214
+ });
3215
+ cursor.setUTCDate(cursor.getUTCDate() + 7);
3216
+ }
3217
+ return {
3218
+ weeks,
3219
+ monthMarkers,
3220
+ maxTokens,
3221
+ since,
3222
+ until
3223
+ };
3224
+ }
3225
+
3226
+ // packages/renderers/dist/terminal/heatmap.js
3227
+ var DAY_LABELS2 = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
3228
+ var DAY_LABEL_WIDTH2 = 4;
3229
+ var WEEK_COLUMN_WIDTH = 2;
3230
+ var LEGEND_TEXT = "Less";
3231
+ var LEGEND_TEXT_MORE = "More";
3232
+ function buildMonthHeader(model, visibleStartWeek, displayWeekCount) {
3233
+ const header = Array.from({ length: displayWeekCount * WEEK_COLUMN_WIDTH }, () => " ");
3234
+ let nextFreeIndex = 0;
3235
+ let placedLabels = 0;
3236
+ const visibleMarkers = model.monthMarkers.filter((marker) => marker.weekIndex >= visibleStartWeek);
3237
+ for (const marker of visibleMarkers) {
3238
+ const startIndex = Math.max((marker.weekIndex - visibleStartWeek) * WEEK_COLUMN_WIDTH, nextFreeIndex);
3239
+ const remaining = header.length - startIndex;
3240
+ if (remaining < 3) {
3241
+ continue;
3242
+ }
3243
+ for (let offset = 0;offset < marker.label.length; offset += 1) {
3244
+ header[startIndex + offset] = marker.label[offset] ?? " ";
3245
+ }
3246
+ nextFreeIndex = startIndex + marker.label.length + 1;
3247
+ placedLabels += 1;
3248
+ }
3249
+ const line = header.some((cell) => cell !== " ") ? `${" ".repeat(DAY_LABEL_WIDTH2)}${header.join("")}` : null;
3250
+ const uniqueVisibleMonths = visibleMarkers.map((marker) => `${marker.label} ${String(marker.year)}`).filter((value, index, values) => values.indexOf(value) === index);
3251
+ const caption = placedLabels === 0 && uniqueVisibleMonths.length === 1 ? ` ${uniqueVisibleMonths[0]}` : null;
3252
+ return { caption, line };
3253
+ }
3254
+ function renderTerminalHeatmap(daily, options) {
3255
+ const model = buildHeatmapModel(daily);
3256
+ if (!model) {
3257
+ return " No usage data available in the selected range.";
3258
+ }
3259
+ const availableColumns = Math.max(WEEK_COLUMN_WIDTH, options.width - DAY_LABEL_WIDTH2);
3260
+ const maxWeeks = Math.max(1, Math.floor(availableColumns / WEEK_COLUMN_WIDTH));
3261
+ const displayWeeks = model.weeks.slice(Math.max(0, model.weeks.length - maxWeeks));
3262
+ const visibleStartWeek = model.weeks.length - displayWeeks.length;
3263
+ const { caption, line } = buildMonthHeader(model, visibleStartWeek, displayWeeks.length);
3264
+ const lines = [];
3265
+ if (caption) {
3266
+ lines.push(caption);
3267
+ }
3268
+ if (line) {
3269
+ lines.push(line);
2484
3270
  }
2485
- if (showInsights) {
2486
- const insightsText = renderInsights(stats, noColor2);
2487
- if (insightsText) {
2488
- sections.push("");
2489
- sections.push(colorize(" Insights", "bold", noColor2));
2490
- sections.push(insightsText);
3271
+ for (let dayIndex = 0;dayIndex < 7; dayIndex += 1) {
3272
+ const label = DAY_LABELS2[dayIndex] ?? " ";
3273
+ let row = `${label} `.slice(0, DAY_LABEL_WIDTH2);
3274
+ for (const week of displayWeeks) {
3275
+ const cell = week.days[dayIndex] ?? { level: 0, tokens: 0 };
3276
+ const block = [
3277
+ HEATMAP_BLOCKS.EMPTY,
3278
+ HEATMAP_BLOCKS.LIGHT,
3279
+ HEATMAP_BLOCKS.MEDIUM,
3280
+ HEATMAP_BLOCKS.DARK,
3281
+ HEATMAP_BLOCKS.FULL
3282
+ ][cell.level] ?? HEATMAP_BLOCKS.EMPTY;
3283
+ const color = intensityColor(cell.tokens, model.maxTokens);
3284
+ row += `${colorize(block, color, options.noColor)} `;
2491
3285
  }
3286
+ lines.push(row.trimEnd());
3287
+ }
3288
+ lines.push(`${" ".repeat(DAY_LABEL_WIDTH2)}${LEGEND_TEXT} ${[
3289
+ HEATMAP_BLOCKS.EMPTY,
3290
+ HEATMAP_BLOCKS.LIGHT,
3291
+ HEATMAP_BLOCKS.MEDIUM,
3292
+ HEATMAP_BLOCKS.DARK,
3293
+ HEATMAP_BLOCKS.FULL
3294
+ ].join("")} ${LEGEND_TEXT_MORE}`);
3295
+ return lines.join(`
3296
+ `);
3297
+ }
3298
+
3299
+ // packages/renderers/dist/terminal/dashboard.js
3300
+ var BOX_H2 = "\u2500";
3301
+ var BOX_V2 = "\u2502";
3302
+ var BOX_TL2 = "\u250C";
3303
+ var BOX_TR2 = "\u2510";
3304
+ var BOX_BL2 = "\u2514";
3305
+ var BOX_BR2 = "\u2518";
3306
+ var BAR_CHAR = "\u2588";
3307
+ var TRACK_CHAR = "\u2591";
3308
+ function divider(width) {
3309
+ return BOX_H2.repeat(width);
3310
+ }
3311
+ function boxedHeader2(title, width, noColor2) {
3312
+ const inner = Math.max(1, width - 2);
3313
+ const padded = ` ${title} `;
3314
+ const remaining = Math.max(0, inner - padded.length);
3315
+ const left = Math.floor(remaining / 2);
3316
+ const right = remaining - left;
3317
+ const titleLine = `${BOX_V2}${" ".repeat(left)}${colorize(padded, "bold", noColor2)}${" ".repeat(right)}${BOX_V2}`;
3318
+ return [
3319
+ `${BOX_TL2}${BOX_H2.repeat(inner)}${BOX_TR2}`,
3320
+ titleLine,
3321
+ `${BOX_BL2}${BOX_H2.repeat(inner)}${BOX_BR2}`
3322
+ ].join(`
3323
+ `);
3324
+ }
3325
+ function renderSectionTitle(title, noColor2) {
3326
+ return colorize(` ${title}`, "bold", noColor2);
3327
+ }
3328
+ function renderSummary(parts, width, noColor2) {
3329
+ if (parts.length === 0)
3330
+ return [];
3331
+ const colored = parts.map((part, index) => colorize(part, index % 2 === 0 ? "cyan" : "green", noColor2));
3332
+ const line = colored.join(colorize(" | ", "dim", noColor2));
3333
+ return [truncateVisible(` ${line}`, width)];
3334
+ }
3335
+ function renderTrend(trend, width, noColor2) {
3336
+ if (!trend)
3337
+ return [];
3338
+ return [
3339
+ truncateVisible(` ${colorize("Recent Trend", "bold", noColor2)} ${colorize(trend, "green", noColor2)}`, width)
3340
+ ];
3341
+ }
3342
+ function renderMetricLine(entry, width, noColor2) {
3343
+ const label = entry.label;
3344
+ const value = colorize(entry.value, "cyan", noColor2);
3345
+ const gutter = 2;
3346
+ const safeWidth = Math.max(12, width);
3347
+ const valueWidth = Math.min(Math.max(6, visibleLength(entry.value)), Math.max(6, Math.floor(safeWidth * 0.45)));
3348
+ const labelWidth = Math.max(4, safeWidth - valueWidth - gutter);
3349
+ return `${truncateVisible(label, labelWidth)}${" ".repeat(gutter)}${padVisible(value, valueWidth)}`;
3350
+ }
3351
+ function renderMetrics(entries, width, noColor2) {
3352
+ if (entries.length === 0)
3353
+ return [];
3354
+ const innerWidth = Math.max(16, width - 2);
3355
+ if (width >= 86 && entries.length > 3) {
3356
+ const midpoint = Math.ceil(entries.length / 2);
3357
+ const left = entries.slice(0, midpoint).map((entry) => ` ${renderMetricLine(entry, Math.floor((innerWidth - 3) / 2), noColor2)}`);
3358
+ const right = entries.slice(midpoint).map((entry) => ` ${renderMetricLine(entry, Math.floor((innerWidth - 3) / 2), noColor2)}`);
3359
+ return renderColumns(left, right, innerWidth, 0.5, 3);
3360
+ }
3361
+ return entries.map((entry) => ` ${renderMetricLine(entry, innerWidth, noColor2)}`);
3362
+ }
3363
+ function renderPatternList(title, entries, width, noColor2) {
3364
+ if (entries.length === 0)
3365
+ return [];
3366
+ const lines = [renderSectionTitle(title, noColor2)];
3367
+ const innerWidth = Math.max(18, width - 2);
3368
+ const nameWidth = Math.min(22, Math.max(8, Math.floor(innerWidth * 0.35)));
3369
+ const valueWidth = 6;
3370
+ const barWidth = Math.max(6, innerWidth - nameWidth - valueWidth - 6);
3371
+ for (const entry of entries) {
3372
+ const fillLength = Math.max(1, Math.round(entry.share * barWidth));
3373
+ const fill = colorize(BAR_CHAR.repeat(fillLength), "green", noColor2);
3374
+ const track = TRACK_CHAR.repeat(Math.max(0, barWidth - fillLength));
3375
+ const line = ` ${colorize(truncateVisible(entry.label, nameWidth).padEnd(nameWidth), "yellow", noColor2)} ${fill}${track} ${entry.value.padStart(valueWidth)}`;
3376
+ lines.push(truncateVisible(line, width));
3377
+ }
3378
+ return lines;
3379
+ }
3380
+ function renderPatternColumns(provider, width, noColor2) {
3381
+ const dayLines = renderPatternList("Day of Week", provider.dayOfWeek, Math.floor((width - 3) / 2), noColor2);
3382
+ const modelLines = renderPatternList("Top Models", provider.topModels, Math.floor((width - 3) / 2), noColor2);
3383
+ if (dayLines.length > 0 && modelLines.length > 0 && width >= 96) {
3384
+ return renderColumns(dayLines, modelLines, width - 2, 0.5, 3).map((line) => ` ${line}`);
3385
+ }
3386
+ return [...dayLines, ...dayLines.length > 0 && modelLines.length > 0 ? [""] : [], ...modelLines];
3387
+ }
3388
+ function renderInsights(insights, width, noColor2) {
3389
+ if (insights.length === 0)
3390
+ return [];
3391
+ return [
3392
+ renderSectionTitle("Insights", noColor2),
3393
+ ...insights.map((insight) => truncateVisible(` ${colorize("*", "green", noColor2)} ${insight}`, width))
3394
+ ];
3395
+ }
3396
+ function renderProviderSection(provider, width, noColor2, showInsights) {
3397
+ const sections = [
3398
+ boxedHeader2(provider.provider.displayName, width, noColor2),
3399
+ ...renderSummary(provider.summary, width, noColor2),
3400
+ ...renderTrend(provider.trend, width, noColor2),
3401
+ "",
3402
+ renderSectionTitle("Heatmap", noColor2),
3403
+ renderTerminalHeatmap(provider.provider.daily, { width: width - 2, noColor: noColor2 }),
3404
+ "",
3405
+ renderSectionTitle("Stats", noColor2),
3406
+ ...renderMetrics(provider.metrics, width, noColor2)
3407
+ ];
3408
+ const patternLines = renderPatternColumns(provider, width, noColor2);
3409
+ if (patternLines.length > 0) {
3410
+ sections.push("", ...patternLines);
3411
+ }
3412
+ const insightLines = showInsights ? renderInsights(provider.insights, width, noColor2) : [];
3413
+ if (insightLines.length > 0) {
3414
+ sections.push("", ...insightLines);
3415
+ }
3416
+ return sections.join(`
3417
+ `);
3418
+ }
3419
+ function renderOverview(model, width, noColor2) {
3420
+ const sections = [
3421
+ boxedHeader2("Overview", width, noColor2),
3422
+ ...renderSummary(model.overview.summary, width, noColor2),
3423
+ ...renderTrend(model.overview.trend, width, noColor2),
3424
+ "",
3425
+ ...renderMetrics(model.overview.metrics, width, noColor2)
3426
+ ];
3427
+ if (model.overview.providerLeaders.length > 1) {
3428
+ sections.push("", ...renderPatternList("Provider Mix", model.overview.providerLeaders, width, noColor2));
2492
3429
  }
2493
3430
  return sections.join(`
2494
3431
  `);
2495
3432
  }
2496
- function renderDashboard(output, options) {
3433
+ function renderDashboardModel(model, options) {
2497
3434
  const width = options.width;
2498
3435
  const noColor2 = options.noColor;
2499
- const sections = [];
2500
- sections.push(boxedHeader("Tokenleak", width, noColor2));
2501
- sections.push("");
2502
- if (output.providers.length === 0) {
2503
- sections.push(" No provider data available.");
3436
+ const sections = [
3437
+ boxedHeader2("Tokenleak", width, noColor2),
3438
+ ...renderSummary([model.rangeLabel, ...model.overview.summary], width, noColor2)
3439
+ ];
3440
+ if (model.activeProviders.length === 0) {
3441
+ sections.push("");
3442
+ sections.push(" No provider activity in the selected range.");
3443
+ if (model.inactiveProviders.length > 0) {
3444
+ sections.push(truncateVisible(` Checked: ${model.inactiveProviders.join(", ")}`, width));
3445
+ }
2504
3446
  return sections.join(`
2505
3447
  `);
2506
3448
  }
2507
- for (let i = 0;i < output.providers.length; i++) {
2508
- const provider = output.providers[i];
2509
- const providerStats = aggregate(provider.daily, output.dateRange.until);
2510
- sections.push(renderProviderSection(provider, providerStats, width, noColor2, options.showInsights));
2511
- if (i < output.providers.length - 1) {
2512
- sections.push("");
2513
- sections.push(divider(width));
2514
- sections.push("");
2515
- }
3449
+ sections.push("", renderOverview(model, width, noColor2));
3450
+ for (const provider of model.activeProviders) {
3451
+ const providerSection = renderProviderSection(provider, width, noColor2, options.showInsights);
3452
+ sections.push("", divider(width), "", providerSection);
2516
3453
  }
2517
- if (output.providers.length > 1) {
3454
+ if (model.inactiveProviders.length > 0) {
2518
3455
  sections.push("");
2519
- sections.push(divider(width));
2520
- sections.push("");
2521
- sections.push(boxedHeader("Overall", width, noColor2));
2522
- sections.push("");
2523
- sections.push(renderStats(output.aggregated, width, noColor2));
3456
+ sections.push(truncateVisible(` No activity in range: ${model.inactiveProviders.join(", ")}`, width));
2524
3457
  }
2525
3458
  return sections.join(`
2526
3459
  `);
2527
3460
  }
2528
3461
 
2529
3462
  // packages/renderers/dist/terminal/oneliner.js
2530
- function renderOneliner(output, _options) {
3463
+ function countActiveProviders(providers) {
3464
+ return providers.filter((provider) => provider.daily.some((entry) => entry.totalTokens > 0)).length;
3465
+ }
3466
+ function renderOneliner(output, options) {
2531
3467
  const streak = output.aggregated.currentStreak;
2532
3468
  const tokens = formatTokens(output.aggregated.totalTokens);
2533
3469
  const cost = formatCost2(output.aggregated.totalCost);
2534
- const providerCount = output.providers.length;
2535
- return `\uD83D\uDD25 ${streak}d streak | ${tokens} tokens | ${cost} | ${providerCount} provider${providerCount !== 1 ? "s" : ""}`;
3470
+ const providerCount = countActiveProviders(output.providers);
3471
+ if (providerCount === 0) {
3472
+ return `no activity | ${tokens} tokens | ${cost}`;
3473
+ }
3474
+ return `${streak}d streak | ${tokens} tokens | ${cost} | ${providerCount} active`;
2536
3475
  }
2537
3476
 
2538
3477
  // packages/renderers/dist/terminal/terminal-renderer.js
2539
- var MIN_DASHBOARD_WIDTH = 40;
3478
+ var MIN_COMPACT_WIDTH = 32;
2540
3479
 
2541
3480
  class TerminalRenderer {
2542
3481
  format = "terminal";
@@ -2545,79 +3484,494 @@ class TerminalRenderer {
2545
3484
  ...options,
2546
3485
  noColor: options.noColor
2547
3486
  };
2548
- if (effectiveOptions.width < MIN_DASHBOARD_WIDTH) {
3487
+ if (effectiveOptions.width < MIN_COMPACT_WIDTH) {
2549
3488
  return renderOneliner(output, effectiveOptions);
2550
3489
  }
2551
- return renderDashboard(output, effectiveOptions);
3490
+ const model = buildDashboardModel(output, effectiveOptions);
3491
+ if (model.mode === "compact") {
3492
+ return renderCompactDashboard(model, effectiveOptions);
3493
+ }
3494
+ return renderDashboardModel(model, effectiveOptions);
2552
3495
  }
2553
3496
  }
2554
- // packages/renderers/dist/live/template.js
2555
- var MONTH_NAMES2 = [
2556
- "Jan",
2557
- "Feb",
2558
- "Mar",
2559
- "Apr",
2560
- "May",
2561
- "Jun",
2562
- "Jul",
2563
- "Aug",
2564
- "Sep",
2565
- "Oct",
2566
- "Nov",
2567
- "Dec"
3497
+ // packages/renderers/dist/terminal/colors.js
3498
+ var ESC2 = "\x1B[";
3499
+ var RESET = `${ESC2}0m`;
3500
+ function colorize256(text2, code, noColor2) {
3501
+ if (noColor2)
3502
+ return text2;
3503
+ return `${ESC2}38;5;${code}m${text2}${RESET}`;
3504
+ }
3505
+ function bold256(text2, code, noColor2) {
3506
+ if (noColor2)
3507
+ return text2;
3508
+ return `${ESC2}1;38;5;${code}m${text2}${RESET}`;
3509
+ }
3510
+ function inverse256(text2, code, noColor2) {
3511
+ if (noColor2)
3512
+ return text2;
3513
+ return `${ESC2}7;38;5;${code}m${text2}${RESET}`;
3514
+ }
3515
+ function dim(text2, noColor2) {
3516
+ if (noColor2)
3517
+ return text2;
3518
+ return `${ESC2}2m${text2}${RESET}`;
3519
+ }
3520
+ function bold2(text2, noColor2) {
3521
+ if (noColor2)
3522
+ return text2;
3523
+ return `${ESC2}1m${text2}${RESET}`;
3524
+ }
3525
+ var DOW_COLORS = {
3526
+ Sun: 213,
3527
+ Mon: 33,
3528
+ Tue: 40,
3529
+ Wed: 208,
3530
+ Thu: 141,
3531
+ Fri: 220,
3532
+ Sat: 209
3533
+ };
3534
+ var TOD_COLORS = {
3535
+ "After midnight": 213,
3536
+ Morning: 208,
3537
+ Afternoon: 40,
3538
+ Evening: 33,
3539
+ Night: 141
3540
+ };
3541
+ var MODEL_COLORS = [
3542
+ 33,
3543
+ 40,
3544
+ 208,
3545
+ 141,
3546
+ 220,
3547
+ 209,
3548
+ 213,
3549
+ 51,
3550
+ 196,
3551
+ 118
2568
3552
  ];
2569
- var MONTH_NAMES_FULL2 = [
2570
- "JAN",
2571
- "FEB",
2572
- "MAR",
2573
- "APR",
2574
- "MAY",
2575
- "JUN",
2576
- "JUL",
2577
- "AUG",
2578
- "SEP",
2579
- "OCT",
2580
- "NOV",
2581
- "DEC"
3553
+ var PROJECT_COLORS = [
3554
+ 40,
3555
+ 33,
3556
+ 208,
3557
+ 213,
3558
+ 220,
3559
+ 141,
3560
+ 209,
3561
+ 51,
3562
+ 196,
3563
+ 118
2582
3564
  ];
2583
- function formatDateRange2(since, until) {
2584
- const s = new Date(since + "T00:00:00Z");
2585
- const u = new Date(until + "T00:00:00Z");
2586
- const diffMs = u.getTime() - s.getTime();
2587
- const days = Math.round(diffMs / (1000 * 60 * 60 * 24));
2588
- const sMonth = MONTH_NAMES_FULL2[s.getUTCMonth()] ?? "";
2589
- const uMonth = MONTH_NAMES_FULL2[u.getUTCMonth()] ?? "";
2590
- return `${sMonth} ${s.getUTCFullYear()} &mdash; ${uMonth} ${u.getUTCFullYear()} &middot; ${days} DAYS`;
2591
- }
2592
- function formatPercentage2(rate) {
2593
- return `${(rate * 100).toFixed(1)}%`;
3565
+ // packages/renderers/dist/terminal/tab-views/tab-bar.js
3566
+ var TIME_RANGES = ["7d", "30d", "90d", "365d"];
3567
+ var METRIC_TABS = ["overview", "sess", "tok", "model", "cwd", "dow", "tod"];
3568
+ var TAB_LABELS = {
3569
+ overview: "overview",
3570
+ sess: "sess",
3571
+ tok: "tok",
3572
+ model: "model",
3573
+ cwd: "cwd",
3574
+ dow: "dow",
3575
+ tod: "tod"
3576
+ };
3577
+ var ACTIVE_COLOR = 33;
3578
+ var HINT_COLOR = 220;
3579
+ function renderTabBar(activeRange, activeTab, width, noColor2) {
3580
+ const rangeParts = TIME_RANGES.map((r) => {
3581
+ if (r === activeRange) {
3582
+ return inverse256(` ${r} `, ACTIVE_COLOR, noColor2);
3583
+ }
3584
+ return dim(` ${r} `, noColor2);
3585
+ });
3586
+ const tabParts = METRIC_TABS.map((t) => {
3587
+ if (t === activeTab) {
3588
+ return inverse256(` ${TAB_LABELS[t]} `, ACTIVE_COLOR, noColor2);
3589
+ }
3590
+ return dim(` ${TAB_LABELS[t]} `, noColor2);
3591
+ });
3592
+ const separator = dim(" \u2502 ", noColor2);
3593
+ const rangeLine = ` ${rangeParts.join(" ")}${separator}${tabParts.join(" ")}`;
3594
+ const hints = [
3595
+ `${bold256("\u2190/\u2192", HINT_COLOR, noColor2)} range`,
3596
+ `${bold256("tab", HINT_COLOR, noColor2)} metric`,
3597
+ `${bold256("1-7", HINT_COLOR, noColor2)} jump`,
3598
+ `${bold256("\u2191/\u2193", HINT_COLOR, noColor2)} scroll`,
3599
+ `${bold256("q", HINT_COLOR, noColor2)} close`
3600
+ ];
3601
+ const hintLine = ` ${hints.join(dim(" \xB7 ", noColor2))}`;
3602
+ return [
3603
+ truncateVisible(rangeLine, width),
3604
+ truncateVisible(hintLine, width)
3605
+ ].join(`
3606
+ `);
2594
3607
  }
2595
- function formatStreak2(n) {
2596
- return `${n} day${n !== 1 ? "s" : ""}`;
3608
+ // packages/renderers/dist/terminal/tab-views/overview-view.js
3609
+ function renderOverviewView(output, options) {
3610
+ return renderDashboardModel(buildDashboardModel(output, options), options);
2597
3611
  }
2598
- function esc(s) {
2599
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3612
+ // packages/renderers/dist/terminal/tab-views/dow-view.js
3613
+ var BAR_CHAR2 = "\u2588";
3614
+ var TRACK_CHAR2 = "\u2591";
3615
+ var DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
3616
+ function formatTokens2(count) {
3617
+ if (count >= 1e6)
3618
+ return `${(count / 1e6).toFixed(1)}M`;
3619
+ if (count >= 1000)
3620
+ return `${(count / 1000).toFixed(0)}K`;
3621
+ return String(count);
2600
3622
  }
2601
- function computeQuantiles3(values) {
2602
- const nonZero = values.filter((v) => v > 0).sort((a, b) => a - b);
2603
- if (nonZero.length === 0)
2604
- return [0, 0, 0];
2605
- const q = (p) => {
2606
- const idx = Math.floor(p * (nonZero.length - 1));
2607
- return nonZero[idx] ?? 0;
2608
- };
2609
- return [q(0.25), q(0.5), q(0.75)];
3623
+ function formatCost3(cost) {
3624
+ return `$${cost.toFixed(2)}`;
2610
3625
  }
2611
- function getLevel2(tokens, quantiles) {
2612
- if (tokens <= 0)
2613
- return 0;
2614
- if (tokens <= quantiles[0])
2615
- return 1;
2616
- if (tokens <= quantiles[1])
2617
- return 2;
2618
- if (tokens <= quantiles[2])
2619
- return 3;
2620
- return 4;
3626
+ function renderDowView(output, width, noColor2) {
3627
+ const dow = output.aggregated.dayOfWeek;
3628
+ if (dow.length === 0) {
3629
+ return ` ${dim("No day-of-week data available.", noColor2)}`;
3630
+ }
3631
+ const maxTokens = Math.max(...dow.map((d) => d.tokens), 0);
3632
+ const totalTokens = dow.reduce((sum, d) => sum + d.tokens, 0);
3633
+ if (maxTokens <= 0) {
3634
+ return ` ${dim("No activity in the selected range.", noColor2)}`;
3635
+ }
3636
+ const lines = [bold2(" Day of Week", noColor2), ""];
3637
+ const nameWidth = 5;
3638
+ const valueWidth = 8;
3639
+ const costWidth = 10;
3640
+ const shareWidth = 6;
3641
+ const barWidth = Math.max(8, width - nameWidth - valueWidth - costWidth - shareWidth - 10);
3642
+ for (const entry of dow) {
3643
+ const label = DAY_NAMES[entry.day] ?? "???";
3644
+ const colorCode = DOW_COLORS[label] ?? 33;
3645
+ const share = totalTokens > 0 ? entry.tokens / totalTokens : 0;
3646
+ const ratio = maxTokens > 0 ? entry.tokens / maxTokens : 0;
3647
+ const fillLen = Math.max(ratio > 0 ? 1 : 0, Math.round(ratio * barWidth));
3648
+ const bar = colorize256(BAR_CHAR2.repeat(fillLen), colorCode, noColor2) + dim(TRACK_CHAR2.repeat(Math.max(0, barWidth - fillLen)), noColor2);
3649
+ const shareStr = `${(share * 100).toFixed(0)}%`.padStart(shareWidth);
3650
+ const tokStr = formatTokens2(entry.tokens).padStart(valueWidth);
3651
+ const costStr = formatCost3(entry.cost).padStart(costWidth);
3652
+ lines.push(truncateVisible(` ${colorize256(label.padEnd(nameWidth), colorCode, noColor2)} ${bar} ${shareStr} ${tokStr} ${costStr}`, width));
3653
+ }
3654
+ return lines.join(`
3655
+ `);
3656
+ }
3657
+ // packages/renderers/dist/terminal/tab-views/tod-view.js
3658
+ var BAR_CHAR3 = "\u2588";
3659
+ var TRACK_CHAR3 = "\u2591";
3660
+ var BUCKET_RANGES = [
3661
+ { label: "After midnight", hours: "0-5", start: 0, end: 5 },
3662
+ { label: "Morning", hours: "6-11", start: 6, end: 11 },
3663
+ { label: "Afternoon", hours: "12-16", start: 12, end: 16 },
3664
+ { label: "Evening", hours: "17-21", start: 17, end: 21 },
3665
+ { label: "Night", hours: "22-23", start: 22, end: 23 }
3666
+ ];
3667
+ function groupIntoBuckets(hourOfDay) {
3668
+ return BUCKET_RANGES.map(({ label, hours, start, end }) => {
3669
+ let tokens = 0;
3670
+ let cost = 0;
3671
+ let count = 0;
3672
+ for (let h = start;h <= end; h++) {
3673
+ const entry = hourOfDay[h];
3674
+ if (entry) {
3675
+ tokens += entry.tokens;
3676
+ cost += entry.cost;
3677
+ count += entry.count;
3678
+ }
3679
+ }
3680
+ return { label, hours, tokens, cost, count };
3681
+ });
3682
+ }
3683
+ function formatTokens3(n) {
3684
+ if (n >= 1e6)
3685
+ return `${(n / 1e6).toFixed(1)}M`;
3686
+ if (n >= 1000)
3687
+ return `${(n / 1000).toFixed(0)}K`;
3688
+ return String(n);
3689
+ }
3690
+ function formatCost4(cost) {
3691
+ return `$${cost.toFixed(2)}`;
3692
+ }
3693
+ function renderTodView(output, width, noColor2) {
3694
+ const hourOfDay = output.more?.hourOfDay;
3695
+ if (!hourOfDay || hourOfDay.length === 0) {
3696
+ return ` ${dim("No event-level data available for time-of-day analysis.", noColor2)}`;
3697
+ }
3698
+ const buckets = groupIntoBuckets(hourOfDay);
3699
+ const maxTokens = Math.max(...buckets.map((b) => b.tokens), 0);
3700
+ const totalTokens = buckets.reduce((sum, b) => sum + b.tokens, 0);
3701
+ if (maxTokens <= 0) {
3702
+ return ` ${dim("No activity in the selected range.", noColor2)}`;
3703
+ }
3704
+ const lines = [bold2(" Time of Day (UTC)", noColor2), ""];
3705
+ const labelWidth = 16;
3706
+ const valueWidth = 8;
3707
+ const costWidth = 10;
3708
+ const shareWidth = 6;
3709
+ const barWidth = Math.max(8, width - labelWidth - valueWidth - costWidth - shareWidth - 10);
3710
+ for (const bucket of buckets) {
3711
+ const colorCode = TOD_COLORS[bucket.label] ?? 33;
3712
+ const share = totalTokens > 0 ? bucket.tokens / totalTokens : 0;
3713
+ const ratio = maxTokens > 0 ? bucket.tokens / maxTokens : 0;
3714
+ const fillLen = Math.max(ratio > 0 ? 1 : 0, Math.round(ratio * barWidth));
3715
+ const bar = colorize256(BAR_CHAR3.repeat(fillLen), colorCode, noColor2) + dim(TRACK_CHAR3.repeat(Math.max(0, barWidth - fillLen)), noColor2);
3716
+ const shareStr = `${(share * 100).toFixed(0)}%`.padStart(shareWidth);
3717
+ const tokStr = formatTokens3(bucket.tokens).padStart(valueWidth);
3718
+ const costStr = formatCost4(bucket.cost).padStart(costWidth);
3719
+ lines.push(truncateVisible(` ${colorize256(bucket.label.padEnd(labelWidth), colorCode, noColor2)} ${bar} ${shareStr} ${tokStr} ${costStr}`, width));
3720
+ }
3721
+ lines.push("");
3722
+ lines.push(` ${dim(`Hours shown in UTC. ${buckets.reduce((s, b) => s + b.count, 0)} events total.`, noColor2)}`);
3723
+ return lines.join(`
3724
+ `);
3725
+ }
3726
+ // packages/renderers/dist/terminal/tab-views/session-view.js
3727
+ function formatTokens4(n) {
3728
+ if (n >= 1e6)
3729
+ return `${(n / 1e6).toFixed(1)}M`;
3730
+ if (n >= 1000)
3731
+ return `${(n / 1000).toFixed(0)}K`;
3732
+ return String(n);
3733
+ }
3734
+ function formatCost5(cost) {
3735
+ return `$${cost.toFixed(2)}`;
3736
+ }
3737
+ function formatDuration2(ms) {
3738
+ if (ms < 60000)
3739
+ return `${(ms / 1000).toFixed(0)}s`;
3740
+ if (ms < 3600000)
3741
+ return `${(ms / 60000).toFixed(1)}m`;
3742
+ return `${(ms / 3600000).toFixed(1)}h`;
3743
+ }
3744
+ function renderSessionView(output, width, noColor2) {
3745
+ const metrics = output.more?.sessionMetrics;
3746
+ if (!metrics || metrics.totalSessions === 0) {
3747
+ return ` ${dim("No event-level data available for session analysis.", noColor2)}`;
3748
+ }
3749
+ const lines = [bold2(" Sessions", noColor2), ""];
3750
+ const parts = [
3751
+ bold256(`${metrics.totalSessions}`, 33, noColor2) + " sessions",
3752
+ bold256(formatCost5(metrics.averageCost), 40, noColor2) + " avg/session",
3753
+ bold256(formatTokens4(metrics.averageTokens), 208, noColor2) + " avg tokens/session"
3754
+ ];
3755
+ lines.push(truncateVisible(` ${parts.join(dim(" \xB7 ", noColor2))}`, width));
3756
+ lines.push("");
3757
+ const labelWidth = 24;
3758
+ const addMetric = (label, value) => {
3759
+ lines.push(truncateVisible(` ${dim(label.padEnd(labelWidth), noColor2)} ${bold2(value, noColor2)}`, width));
3760
+ };
3761
+ addMetric("Total sessions", String(metrics.totalSessions));
3762
+ addMetric("Avg tokens/session", formatTokens4(metrics.averageTokens));
3763
+ addMetric("Avg cost/session", formatCost5(metrics.averageCost));
3764
+ addMetric("Avg messages/session", metrics.averageMessages.toFixed(1));
3765
+ if (metrics.averageDurationMs !== null) {
3766
+ addMetric("Avg duration", formatDuration2(metrics.averageDurationMs));
3767
+ }
3768
+ addMetric("Projects", String(metrics.projectCount));
3769
+ if (metrics.longestSession) {
3770
+ lines.push("");
3771
+ lines.push(` ${bold2("Longest Session", noColor2)}`);
3772
+ addMetric(" Label", metrics.longestSession.label);
3773
+ addMetric(" Tokens", formatTokens4(metrics.longestSession.tokens));
3774
+ addMetric(" Cost", formatCost5(metrics.longestSession.cost));
3775
+ addMetric(" Messages", String(metrics.longestSession.count));
3776
+ if (metrics.longestSession.durationMs !== null) {
3777
+ addMetric(" Duration", formatDuration2(metrics.longestSession.durationMs));
3778
+ }
3779
+ }
3780
+ if (metrics.topProject) {
3781
+ lines.push("");
3782
+ lines.push(truncateVisible(` ${dim("Top project:", noColor2)} ${bold256(metrics.topProject.name, 40, noColor2)} (${formatTokens4(metrics.topProject.tokens)})`, width));
3783
+ }
3784
+ return lines.join(`
3785
+ `);
3786
+ }
3787
+ // packages/renderers/dist/terminal/tab-views/model-view.js
3788
+ var BAR_CHAR4 = "\u2588";
3789
+ var TRACK_CHAR4 = "\u2591";
3790
+ function formatTokens5(n) {
3791
+ if (n >= 1e6)
3792
+ return `${(n / 1e6).toFixed(1)}M`;
3793
+ if (n >= 1000)
3794
+ return `${(n / 1000).toFixed(0)}K`;
3795
+ return String(n);
3796
+ }
3797
+ function formatCost6(cost) {
3798
+ return `$${cost.toFixed(2)}`;
3799
+ }
3800
+ function renderModelView(output, width, noColor2) {
3801
+ const models = output.aggregated.topModels;
3802
+ if (models.length === 0) {
3803
+ return ` ${dim("No model data available.", noColor2)}`;
3804
+ }
3805
+ const lines = [bold2(" Models", noColor2), ""];
3806
+ const nameWidth = Math.min(28, Math.max(12, Math.floor(width * 0.25)));
3807
+ const valueWidth = 8;
3808
+ const costWidth = 10;
3809
+ const shareWidth = 6;
3810
+ const barWidth = Math.max(8, width - nameWidth - valueWidth - costWidth - shareWidth - 10);
3811
+ const maxTokens = Math.max(...models.map((m) => m.tokens), 0);
3812
+ for (let i = 0;i < models.length; i++) {
3813
+ const model = models[i];
3814
+ const colorCode = MODEL_COLORS[i % MODEL_COLORS.length];
3815
+ const ratio = maxTokens > 0 ? model.tokens / maxTokens : 0;
3816
+ const fillLen = Math.max(ratio > 0 ? 1 : 0, Math.round(ratio * barWidth));
3817
+ const bar = colorize256(BAR_CHAR4.repeat(fillLen), colorCode, noColor2) + dim(TRACK_CHAR4.repeat(Math.max(0, barWidth - fillLen)), noColor2);
3818
+ const shareStr = `${model.percentage.toFixed(0)}%`.padStart(shareWidth);
3819
+ const tokStr = formatTokens5(model.tokens).padStart(valueWidth);
3820
+ const costStr = formatCost6(model.cost).padStart(costWidth);
3821
+ const name = model.model.length > nameWidth ? model.model.slice(0, nameWidth - 1) + "\u2026" : model.model.padEnd(nameWidth);
3822
+ lines.push(truncateVisible(` ${colorize256(name, colorCode, noColor2)} ${bar} ${shareStr} ${tokStr} ${costStr}`, width));
3823
+ }
3824
+ const io = output.more?.inputOutput;
3825
+ if (io) {
3826
+ lines.push("");
3827
+ lines.push(` ${bold2("Input / Output Ratio", noColor2)}`);
3828
+ const inputShare = 1 - io.outputShare;
3829
+ const ioBarWidth = Math.max(10, width - 20);
3830
+ const inputLen = Math.round(inputShare * ioBarWidth);
3831
+ const outputLen = ioBarWidth - inputLen;
3832
+ const ioBar = colorize256(BAR_CHAR4.repeat(inputLen), 33, noColor2) + colorize256(BAR_CHAR4.repeat(outputLen), 40, noColor2);
3833
+ lines.push(truncateVisible(` ${ioBar} ${dim(`input ${(inputShare * 100).toFixed(0)}%`, noColor2)} ${dim(`output ${(io.outputShare * 100).toFixed(0)}%`, noColor2)}`, width));
3834
+ }
3835
+ return lines.join(`
3836
+ `);
3837
+ }
3838
+ // packages/renderers/dist/terminal/tab-views/token-view.js
3839
+ var BAR_CHAR5 = "\u2588";
3840
+ function formatTokens6(n) {
3841
+ if (n >= 1e6)
3842
+ return `${(n / 1e6).toFixed(1)}M`;
3843
+ if (n >= 1000)
3844
+ return `${(n / 1000).toFixed(0)}K`;
3845
+ return String(n);
3846
+ }
3847
+ function formatCost7(cost) {
3848
+ return `$${cost.toFixed(2)}`;
3849
+ }
3850
+ function renderTokenView(output, width, noColor2) {
3851
+ const stats = output.aggregated;
3852
+ const lines = [bold2(" Tokens", noColor2), ""];
3853
+ const parts = [
3854
+ bold256(formatTokens6(stats.totalTokens), 33, noColor2) + " total",
3855
+ bold256(formatCost7(stats.totalCost), 40, noColor2) + " cost",
3856
+ bold256(`${stats.activeDays}`, 208, noColor2) + " active days"
3857
+ ];
3858
+ lines.push(truncateVisible(` ${parts.join(dim(" \xB7 ", noColor2))}`, width));
3859
+ lines.push("");
3860
+ lines.push(` ${bold2("Heatmap", noColor2)}`);
3861
+ const merged = output.providers.flatMap((p) => p.daily);
3862
+ lines.push(renderTerminalHeatmap(merged, { width: width - 2, noColor: noColor2 }));
3863
+ lines.push("");
3864
+ const io = output.more?.inputOutput;
3865
+ if (io) {
3866
+ lines.push(` ${bold2("Input / Output", noColor2)}`);
3867
+ const inputShare = 1 - io.outputShare;
3868
+ const barWidth = Math.max(10, width - 20);
3869
+ const inputLen = Math.round(inputShare * barWidth);
3870
+ const outputLen = barWidth - inputLen;
3871
+ const bar = colorize256(BAR_CHAR5.repeat(inputLen), 33, noColor2) + colorize256(BAR_CHAR5.repeat(outputLen), 40, noColor2);
3872
+ lines.push(truncateVisible(` ${bar} ${dim(`in ${(inputShare * 100).toFixed(0)}%`, noColor2)} ${dim(`out ${(io.outputShare * 100).toFixed(0)}%`, noColor2)}`, width));
3873
+ lines.push("");
3874
+ }
3875
+ const cache = output.more?.cacheEconomics;
3876
+ if (cache) {
3877
+ lines.push(` ${bold2("Cache Economics", noColor2)}`);
3878
+ const labelWidth = 20;
3879
+ const addLine = (label, value) => {
3880
+ lines.push(truncateVisible(` ${dim(label.padEnd(labelWidth), noColor2)} ${bold2(value, noColor2)}`, width));
3881
+ };
3882
+ addLine("Read tokens", formatTokens6(cache.readTokens));
3883
+ addLine("Write tokens", formatTokens6(cache.writeTokens));
3884
+ addLine("Read coverage", `${(cache.readCoverage * 100).toFixed(1)}%`);
3885
+ if (cache.reuseRatio !== null) {
3886
+ addLine("Reuse ratio", `${cache.reuseRatio.toFixed(1)}x`);
3887
+ }
3888
+ lines.push("");
3889
+ }
3890
+ const burn = output.more?.monthlyBurn;
3891
+ if (burn) {
3892
+ lines.push(` ${bold2("Monthly Burn Projection", noColor2)}`);
3893
+ const labelWidth = 20;
3894
+ lines.push(truncateVisible(` ${dim("Projected tokens".padEnd(labelWidth), noColor2)} ${bold2(formatTokens6(burn.projectedTokens), noColor2)}`, width));
3895
+ lines.push(truncateVisible(` ${dim("Projected cost".padEnd(labelWidth), noColor2)} ${bold2(formatCost7(burn.projectedCost), noColor2)}`, width));
3896
+ lines.push(truncateVisible(` ${dim("Observed days".padEnd(labelWidth), noColor2)} ${bold2(`${burn.observedDays} / ${burn.calendarDays}`, noColor2)}`, width));
3897
+ }
3898
+ return lines.join(`
3899
+ `);
3900
+ }
3901
+ // packages/renderers/dist/terminal/tab-views/cwd-view.js
3902
+ var BAR_CHAR6 = "\u2588";
3903
+ var TRACK_CHAR5 = "\u2591";
3904
+ function formatTokens7(n) {
3905
+ if (n >= 1e6)
3906
+ return `${(n / 1e6).toFixed(1)}M`;
3907
+ if (n >= 1000)
3908
+ return `${(n / 1000).toFixed(0)}K`;
3909
+ return String(n);
3910
+ }
3911
+ function renderCwdView(output, width, noColor2) {
3912
+ const breakdown = output.more?.sessionMetrics?.projectBreakdown;
3913
+ if (!breakdown || breakdown.length === 0) {
3914
+ return ` ${dim("No event-level data available for project breakdown.", noColor2)}`;
3915
+ }
3916
+ const lines = [bold2(" Projects", noColor2), ""];
3917
+ const maxTokens = Math.max(...breakdown.map((p) => p.tokens), 0);
3918
+ const totalTokens = breakdown.reduce((sum, p) => sum + p.tokens, 0);
3919
+ if (maxTokens <= 0) {
3920
+ return ` ${dim("No project activity in the selected range.", noColor2)}`;
3921
+ }
3922
+ const nameWidth = Math.min(30, Math.max(12, Math.floor(width * 0.3)));
3923
+ const valueWidth = 8;
3924
+ const shareWidth = 6;
3925
+ const barWidth = Math.max(8, width - nameWidth - valueWidth - shareWidth - 8);
3926
+ for (let i = 0;i < breakdown.length; i++) {
3927
+ const project = breakdown[i];
3928
+ const colorCode = PROJECT_COLORS[i % PROJECT_COLORS.length];
3929
+ const ratio = maxTokens > 0 ? project.tokens / maxTokens : 0;
3930
+ const share = totalTokens > 0 ? project.tokens / totalTokens : 0;
3931
+ const fillLen = Math.max(ratio > 0 ? 1 : 0, Math.round(ratio * barWidth));
3932
+ const bar = colorize256(BAR_CHAR6.repeat(fillLen), colorCode, noColor2) + dim(TRACK_CHAR5.repeat(Math.max(0, barWidth - fillLen)), noColor2);
3933
+ const shareStr = `${(share * 100).toFixed(0)}%`.padStart(shareWidth);
3934
+ const tokStr = formatTokens7(project.tokens).padStart(valueWidth);
3935
+ const name = project.name.length > nameWidth ? project.name.slice(0, nameWidth - 1) + "\u2026" : project.name.padEnd(nameWidth);
3936
+ lines.push(truncateVisible(` ${colorize256(name, colorCode, noColor2)} ${bar} ${shareStr} ${tokStr}`, width));
3937
+ }
3938
+ lines.push("");
3939
+ lines.push(` ${dim(`${breakdown.length} project${breakdown.length === 1 ? "" : "s"} shown (top 10 by tokens)`, noColor2)}`);
3940
+ return lines.join(`
3941
+ `);
3942
+ }
3943
+ // packages/renderers/dist/live/template.js
3944
+ var MONTH_NAMES_FULL2 = [
3945
+ "JAN",
3946
+ "FEB",
3947
+ "MAR",
3948
+ "APR",
3949
+ "MAY",
3950
+ "JUN",
3951
+ "JUL",
3952
+ "AUG",
3953
+ "SEP",
3954
+ "OCT",
3955
+ "NOV",
3956
+ "DEC"
3957
+ ];
3958
+ function formatDateRange2(since, until) {
3959
+ const s = new Date(since + "T00:00:00Z");
3960
+ const u = new Date(until + "T00:00:00Z");
3961
+ const diffMs = u.getTime() - s.getTime();
3962
+ const days = Math.round(diffMs / (1000 * 60 * 60 * 24));
3963
+ const sMonth = MONTH_NAMES_FULL2[s.getUTCMonth()] ?? "";
3964
+ const uMonth = MONTH_NAMES_FULL2[u.getUTCMonth()] ?? "";
3965
+ return `${sMonth} ${s.getUTCFullYear()} &mdash; ${uMonth} ${u.getUTCFullYear()} &middot; ${days} DAYS`;
3966
+ }
3967
+ function formatPercentage2(rate) {
3968
+ return `${(rate * 100).toFixed(1)}%`;
3969
+ }
3970
+ function formatStreak2(n) {
3971
+ return `${n} day${n !== 1 ? "s" : ""}`;
3972
+ }
3973
+ function esc(s) {
3974
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2621
3975
  }
2622
3976
  function hexToRgb2(hex) {
2623
3977
  const h = hex.replace("#", "");
@@ -2646,44 +4000,32 @@ function buildHeatmapScale2(colors, isDark) {
2646
4000
  })
2647
4001
  ];
2648
4002
  }
2649
- function buildHeatmapCells(daily, since, until) {
2650
- const tokenMap = new Map;
2651
- for (const d of daily) {
2652
- tokenMap.set(d.date, (tokenMap.get(d.date) ?? 0) + d.totalTokens);
4003
+ function buildHeatmapCells(provider, since, until) {
4004
+ const model = buildHeatmapModel(provider.daily, { since, until });
4005
+ if (!model) {
4006
+ return { cells: [], months: [], totalCols: 0 };
2653
4007
  }
2654
- const dates = daily.map((d) => d.date).sort();
2655
- const endStr = until ?? dates[dates.length - 1] ?? new Date().toISOString().slice(0, 10);
2656
- const startStr = since ?? dates[0] ?? endStr;
2657
- const end = new Date(endStr + "T00:00:00Z");
2658
- const start = new Date(startStr + "T00:00:00Z");
2659
- start.setUTCDate(start.getUTCDate() - start.getUTCDay());
2660
- const allTokens = Array.from(tokenMap.values());
2661
- const quantiles = computeQuantiles3(allTokens);
2662
4008
  const cells = [];
2663
4009
  const months = [];
2664
- let lastMonth = -1;
2665
- let col = 0;
2666
- const current = new Date(start);
2667
- while (current <= end) {
2668
- const row = current.getUTCDay();
2669
- const dateStr = current.toISOString().slice(0, 10);
2670
- const tokens = tokenMap.get(dateStr) ?? 0;
2671
- const level = getLevel2(tokens, quantiles);
2672
- cells.push({ date: dateStr, tokens, level, row, col });
2673
- const month = current.getUTCMonth();
2674
- if (month !== lastMonth && row === 0) {
2675
- lastMonth = month;
2676
- months.push({ label: MONTH_NAMES2[month] ?? "", col });
4010
+ for (const marker of model.monthMarkers) {
4011
+ months.push({ label: marker.label, col: marker.weekIndex });
4012
+ }
4013
+ for (const week of model.weeks) {
4014
+ for (const day of week.days) {
4015
+ cells.push({
4016
+ date: day.date,
4017
+ tokens: day.tokens,
4018
+ level: day.level,
4019
+ row: day.dayIndex,
4020
+ col: week.index
4021
+ });
2677
4022
  }
2678
- if (row === 6)
2679
- col++;
2680
- current.setUTCDate(current.getUTCDate() + 1);
2681
4023
  }
2682
- return { cells, months, totalCols: col + 1 };
4024
+ return { cells, months, totalCols: model.weeks.length };
2683
4025
  }
2684
4026
  function renderProviderHeatmapHtml(provider, since, until, isDark, emptyCell) {
2685
4027
  const heatmapColors = buildHeatmapScale2(provider.colors, isDark);
2686
- const { cells, months, totalCols } = buildHeatmapCells(provider.daily, since, until);
4028
+ const { cells, months, totalCols } = buildHeatmapCells(provider, since, until);
2687
4029
  const heatmapWidth = DAY_LABEL_WIDTH + totalCols * (CELL_SIZE + CELL_GAP);
2688
4030
  const heatmapHeight = MONTH_LABEL_HEIGHT + 7 * (CELL_SIZE + CELL_GAP);
2689
4031
  const cellsHtml = cells.map((c) => {
@@ -3001,54 +4343,1353 @@ function loadConfig() {
3001
4343
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
3002
4344
  return parsed;
3003
4345
  }
3004
- return {};
3005
- } catch {
3006
- return {};
3007
- }
4346
+ return {};
4347
+ } catch {
4348
+ return {};
4349
+ }
4350
+ }
4351
+
4352
+ // packages/cli/src/errors.ts
4353
+ class TokenleakError extends Error {
4354
+ constructor(message) {
4355
+ super(message);
4356
+ this.name = "TokenleakError";
4357
+ }
4358
+ }
4359
+ function handleError(error) {
4360
+ if (error instanceof TokenleakError) {
4361
+ process.stderr.write(`Error: ${error.message}
4362
+ `);
4363
+ } else if (error instanceof Error) {
4364
+ process.stderr.write(`Error: ${error.message}
4365
+ `);
4366
+ } else {
4367
+ process.stderr.write(`Error: ${String(error)}
4368
+ `);
4369
+ }
4370
+ process.exit(1);
4371
+ }
4372
+
4373
+ // packages/cli/src/date-range.ts
4374
+ var DATE_FORMAT = /^\d{4}-\d{2}-\d{2}$/;
4375
+ function isValidDate(dateStr) {
4376
+ if (!DATE_FORMAT.test(dateStr))
4377
+ return false;
4378
+ const d = new Date(dateStr + "T00:00:00Z");
4379
+ return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === dateStr;
4380
+ }
4381
+ function computeDateRange(args) {
4382
+ const until = args.until ?? new Date().toISOString().slice(0, 10);
4383
+ if (args.until && !isValidDate(args.until)) {
4384
+ throw new TokenleakError(`Invalid --until date: "${args.until}". Use YYYY-MM-DD format.`);
4385
+ }
4386
+ if (args.since && !isValidDate(args.since)) {
4387
+ throw new TokenleakError(`Invalid --since date: "${args.since}". Use YYYY-MM-DD format.`);
4388
+ }
4389
+ let since;
4390
+ if (args.since) {
4391
+ since = args.since;
4392
+ } else {
4393
+ const daysBack = args.days ?? DEFAULT_DAYS;
4394
+ const d = new Date(until);
4395
+ d.setDate(d.getDate() - daysBack);
4396
+ since = d.toISOString().slice(0, 10);
4397
+ }
4398
+ if (since > until) {
4399
+ throw new TokenleakError(`--since (${since}) must not be after --until (${until}).`);
4400
+ }
4401
+ return { since, until };
4402
+ }
4403
+
4404
+ // packages/cli/src/env.ts
4405
+ var VALID_FORMATS = new Set(["json", "svg", "png", "terminal"]);
4406
+ var VALID_THEMES = new Set(["dark", "light"]);
4407
+ function loadEnvOverrides() {
4408
+ const overrides = {};
4409
+ const format = process.env["TOKENLEAK_FORMAT"];
4410
+ if (format && VALID_FORMATS.has(format)) {
4411
+ overrides.format = format;
4412
+ }
4413
+ const theme = process.env["TOKENLEAK_THEME"];
4414
+ if (theme && VALID_THEMES.has(theme)) {
4415
+ overrides.theme = theme;
4416
+ }
4417
+ const days = process.env["TOKENLEAK_DAYS"];
4418
+ if (days !== undefined && days !== "") {
4419
+ const parsed = Number(days);
4420
+ if (Number.isFinite(parsed) && parsed > 0) {
4421
+ overrides.days = parsed;
4422
+ }
4423
+ }
4424
+ return overrides;
4425
+ }
4426
+
4427
+ // packages/cli/src/flags.ts
4428
+ var CLI_FLAG_ORDER = [
4429
+ "format",
4430
+ "theme",
4431
+ "since",
4432
+ "until",
4433
+ "days",
4434
+ "output",
4435
+ "width",
4436
+ "provider",
4437
+ "compare",
4438
+ "upload",
4439
+ "claude",
4440
+ "codex",
4441
+ "openCode",
4442
+ "allProviders",
4443
+ "listProviders",
4444
+ "more",
4445
+ "clipboard",
4446
+ "open",
4447
+ "liveServer",
4448
+ "noColor",
4449
+ "noInsights"
4450
+ ];
4451
+ var CLI_FLAG_NAMES = {
4452
+ format: "--format",
4453
+ theme: "--theme",
4454
+ since: "--since",
4455
+ until: "--until",
4456
+ days: "--days",
4457
+ output: "--output",
4458
+ width: "--width",
4459
+ provider: "--provider",
4460
+ compare: "--compare",
4461
+ upload: "--upload",
4462
+ claude: "--claude",
4463
+ codex: "--codex",
4464
+ openCode: "--open-code",
4465
+ allProviders: "--all-providers",
4466
+ listProviders: "--list-providers",
4467
+ more: "--more",
4468
+ clipboard: "--clipboard",
4469
+ open: "--open",
4470
+ liveServer: "--live-server",
4471
+ noColor: "--no-color",
4472
+ noInsights: "--no-insights"
4473
+ };
4474
+ function buildCliArgTokens(cliArgs) {
4475
+ const tokens = [];
4476
+ for (const key of CLI_FLAG_ORDER) {
4477
+ const value = cliArgs[key];
4478
+ if (value === undefined || value === false || value === null) {
4479
+ continue;
4480
+ }
4481
+ const flag = CLI_FLAG_NAMES[key];
4482
+ if (!flag)
4483
+ continue;
4484
+ tokens.push(flag);
4485
+ if (value !== true) {
4486
+ tokens.push(String(value));
4487
+ }
4488
+ }
4489
+ return tokens;
4490
+ }
4491
+ function buildCliPreview(cliArgs) {
4492
+ const tokens = buildCliArgTokens(cliArgs);
4493
+ return tokens.length === 0 ? "tokenleak" : `tokenleak ${tokens.join(" ")}`;
4494
+ }
4495
+
4496
+ // packages/cli/src/interactive.ts
4497
+ import { emitKeypressEvents } from "readline";
4498
+ import { createInterface } from "readline/promises";
4499
+ var INTERACTIVE_FLAG_LINES = [
4500
+ "-f, --format <format> terminal | png | svg | json",
4501
+ "-t, --theme <theme> dark | light",
4502
+ "-s, --since <date> YYYY-MM-DD start date",
4503
+ "-u, --until <date> YYYY-MM-DD end date",
4504
+ "-d, --days <number> trailing days window",
4505
+ "-o, --output <path> write output to a file",
4506
+ "-w, --width <number> terminal render width",
4507
+ "-p, --provider <list> comma-separated providers",
4508
+ " --claude shortcut for Claude Code",
4509
+ " --codex shortcut for Codex",
4510
+ " --open-code shortcut for Open Code",
4511
+ " --all-providers ignore provider filters",
4512
+ " --list-providers show provider registry",
4513
+ " --compare <range> auto or YYYY-MM-DD..YYYY-MM-DD",
4514
+ " --more richer PNG/SVG stats",
4515
+ " --clipboard copy rendered output",
4516
+ " --open open generated file",
4517
+ " --upload <target> gist",
4518
+ "-L, --live-server local interactive dashboard",
4519
+ " --no-color disable ANSI colors",
4520
+ " --no-insights hide terminal insights",
4521
+ " --help print help",
4522
+ " --version print version"
4523
+ ];
4524
+ var TAB_RANGE_DAY_COUNTS = {
4525
+ "7d": 7,
4526
+ "30d": 30,
4527
+ "90d": 90,
4528
+ "365d": 365
4529
+ };
4530
+ var DAY_MS = 24 * 60 * 60 * 1000;
4531
+ var ESC3 = "\x1B[";
4532
+ var RESET2 = `${ESC3}0m`;
4533
+ var BOLD = `${ESC3}1m`;
4534
+ var DIM = `${ESC3}2m`;
4535
+ var CYAN = `${ESC3}36m`;
4536
+ var GREEN = `${ESC3}32m`;
4537
+ var YELLOW = `${ESC3}33m`;
4538
+ var RED = `${ESC3}31m`;
4539
+ var WHITE = `${ESC3}97m`;
4540
+ var HOME_CLEAR = "\x1B[H\x1B[J";
4541
+ var HIDE_CURSOR = "\x1B[?25l";
4542
+ var SHOW_CURSOR = "\x1B[?25h";
4543
+ var ALT_SCREEN_ON = "\x1B[?1049h";
4544
+ var ALT_SCREEN_OFF = "\x1B[?1049l";
4545
+ var ALT_SCROLL_ON = "\x1B[?1007h";
4546
+ var ALT_SCROLL_OFF = "\x1B[?1007l";
4547
+ var LOADING_TICK_MS = 120;
4548
+ function color(text2, code) {
4549
+ return `${code}${text2}${RESET2}`;
4550
+ }
4551
+ function stripAnsi2(text2) {
4552
+ return text2.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
4553
+ }
4554
+ function visibleLength2(text2) {
4555
+ return stripAnsi2(text2).length;
4556
+ }
4557
+ function padVisible2(text2, width) {
4558
+ const padding = Math.max(0, width - visibleLength2(text2));
4559
+ return text2 + " ".repeat(padding);
4560
+ }
4561
+ function truncateVisible2(text2, width) {
4562
+ if (width <= 0)
4563
+ return "";
4564
+ const plain = stripAnsi2(text2);
4565
+ if (plain.length <= width)
4566
+ return text2;
4567
+ const limit = width <= 3 ? width : width - 3;
4568
+ let visibleCount = 0;
4569
+ let index = 0;
4570
+ let result = "";
4571
+ let sawAnsi = false;
4572
+ while (index < text2.length && visibleCount < limit) {
4573
+ if (text2[index] === "\x1B") {
4574
+ const match = text2.slice(index).match(/^\x1b\[[0-9;?]*[A-Za-z]/);
4575
+ if (match) {
4576
+ result += match[0];
4577
+ index += match[0].length;
4578
+ sawAnsi = true;
4579
+ continue;
4580
+ }
4581
+ }
4582
+ result += text2[index];
4583
+ index += 1;
4584
+ visibleCount += 1;
4585
+ }
4586
+ if (width <= 3) {
4587
+ return sawAnsi ? `${result}${RESET2}` : result;
4588
+ }
4589
+ return sawAnsi ? `${result}...${RESET2}` : `${result}...`;
4590
+ }
4591
+ function joinColumns(left, right, totalWidth) {
4592
+ const gutter = 3;
4593
+ const leftWidth = Math.max(42, Math.min(58, Math.floor(totalWidth * 0.44)));
4594
+ const rightWidth = Math.max(36, totalWidth - leftWidth - gutter);
4595
+ const rows = Math.max(left.length, right.length);
4596
+ const lines = [];
4597
+ for (let index = 0;index < rows; index++) {
4598
+ const leftLine = truncateVisible2(left[index] ?? "", leftWidth);
4599
+ const rightLine = truncateVisible2(right[index] ?? "", rightWidth);
4600
+ lines.push(`${padVisible2(leftLine, leftWidth)}${" ".repeat(gutter)}${rightLine}`);
4601
+ }
4602
+ return lines;
4603
+ }
4604
+ function renderRule(width) {
4605
+ return color("-".repeat(width), DIM);
4606
+ }
4607
+ function describeRequest(args) {
4608
+ const output = typeof args["output"] === "string" ? args["output"] : null;
4609
+ if (args["liveServer"]) {
4610
+ return {
4611
+ title: "Live Dashboard",
4612
+ loadingTitle: "Starting live dashboard",
4613
+ loadingDetail: "Launching the local server. Press Ctrl-C in the live view to stop it, then you will return here.",
4614
+ executionMode: "inherit"
4615
+ };
4616
+ }
4617
+ if (args["listProviders"]) {
4618
+ return {
4619
+ title: "Provider Registry",
4620
+ loadingTitle: "Loading provider registry",
4621
+ loadingDetail: "Checking registered providers and current availability.",
4622
+ executionMode: "capture"
4623
+ };
4624
+ }
4625
+ if (args["compare"]) {
4626
+ return {
4627
+ title: "Compare Report",
4628
+ loadingTitle: "Building compare report",
4629
+ loadingDetail: output ? `Computing period deltas and writing the report to ${output}.` : "Computing period deltas for the current and previous windows.",
4630
+ executionMode: "capture"
4631
+ };
4632
+ }
4633
+ switch (args["format"]) {
4634
+ case "json":
4635
+ return {
4636
+ title: "JSON Export",
4637
+ loadingTitle: "Generating JSON report",
4638
+ loadingDetail: output ? `Collecting token usage and writing JSON to ${output}.` : "Collecting token usage and building structured JSON output.",
4639
+ executionMode: "capture"
4640
+ };
4641
+ case "svg":
4642
+ return {
4643
+ title: "SVG Export",
4644
+ loadingTitle: "Rendering SVG",
4645
+ loadingDetail: output ? `Rendering a vector card and writing it to ${output}.` : "Rendering a vector card from your usage data.",
4646
+ executionMode: "capture"
4647
+ };
4648
+ case "png":
4649
+ return {
4650
+ title: "PNG Export",
4651
+ loadingTitle: "Rendering PNG",
4652
+ 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.",
4653
+ executionMode: "capture"
4654
+ };
4655
+ default:
4656
+ return {
4657
+ title: "Terminal Dashboard",
4658
+ loadingTitle: "Generating terminal dashboard",
4659
+ loadingDetail: "Reading provider logs and aggregating token usage.",
4660
+ executionMode: "capture"
4661
+ };
4662
+ }
4663
+ }
4664
+ function finalizeCliArgs(args) {
4665
+ const finalized = { ...args };
4666
+ const format = finalized["format"];
4667
+ if (finalized["compare"] && (format === "png" || format === "svg")) {
4668
+ finalized["more"] = true;
4669
+ }
4670
+ if (finalized["open"] && finalized["output"] === undefined && typeof format === "string") {
4671
+ if (format === "png" || format === "svg" || format === "json") {
4672
+ finalized["output"] = `tokenleak.${format}`;
4673
+ } else {
4674
+ delete finalized["open"];
4675
+ }
4676
+ }
4677
+ if (format === "png") {
4678
+ delete finalized["clipboard"];
4679
+ delete finalized["upload"];
4680
+ }
4681
+ return finalized;
4682
+ }
4683
+ function createRunCommand(args) {
4684
+ const finalizedArgs = finalizeCliArgs(args);
4685
+ return {
4686
+ type: "run",
4687
+ request: {
4688
+ args: finalizedArgs,
4689
+ preview: buildCliPreview(finalizedArgs),
4690
+ ...describeRequest(finalizedArgs)
4691
+ }
4692
+ };
4693
+ }
4694
+ function renderMenu(options, selectedIndex) {
4695
+ return options.map((option, index) => {
4696
+ const isSelected = index === selectedIndex;
4697
+ const prefix = isSelected ? color(">", GREEN) : " ";
4698
+ const number = isSelected ? color(option.digit, WHITE + BOLD) : color(option.digit, YELLOW);
4699
+ const title = isSelected ? color(option.title, WHITE + BOLD) : color(option.title, WHITE);
4700
+ const description = isSelected ? color(option.description, CYAN) : color(option.description, DIM);
4701
+ return `${prefix} [${number}] ${title} ${description}`;
4702
+ });
4703
+ }
4704
+ function renderFlagPanel() {
4705
+ return [
4706
+ color("All Flags", WHITE + BOLD),
4707
+ color("Every flag remains available while using the launcher.", DIM),
4708
+ "",
4709
+ ...INTERACTIVE_FLAG_LINES.map((line) => color(line, CYAN))
4710
+ ];
4711
+ }
4712
+ function renderMenuPanel(context, options, selectedIndex) {
4713
+ const selected = options[selectedIndex];
4714
+ return [
4715
+ color("Tokenleak Interactive Launcher", WHITE + BOLD),
4716
+ `${color(`v${context.version}`, YELLOW)} ${color("interactive command center", CYAN)}`,
4717
+ "",
4718
+ color("Arrow keys move. Number keys jump directly. Enter runs the selected action.", DIM),
4719
+ color("Commands run inside this session, so you can keep selecting without leaving tokenleak.", DIM),
4720
+ "",
4721
+ ...renderMenu(options, selectedIndex),
4722
+ "",
4723
+ color("Preview", WHITE + BOLD),
4724
+ color(selected.preview, GREEN),
4725
+ "",
4726
+ color("Keys", WHITE + BOLD),
4727
+ `${color("Up/Down", YELLOW)} move ${color("Enter", YELLOW)} run ${color("H", YELLOW)} help ${color("Q", YELLOW)} quit`,
4728
+ "",
4729
+ renderRule(44)
4730
+ ];
4731
+ }
4732
+ function renderHelpOverlay(helpText, width) {
4733
+ const lines = helpText.trimEnd().split(`
4734
+ `);
4735
+ const header = [
4736
+ color("Tokenleak Help", WHITE + BOLD),
4737
+ color("Press Enter, Escape, H, or Q to return to the launcher.", DIM),
4738
+ ""
4739
+ ];
4740
+ return `${HOME_CLEAR}${HIDE_CURSOR}${[...header, ...lines.map((line) => truncateVisible2(line, width))].join(`
4741
+ `)}`;
4742
+ }
4743
+ function renderLauncher(context, options, selectedIndex) {
4744
+ const width = process.stdout.columns ?? 120;
4745
+ const menuPanel = renderMenuPanel(context, options, selectedIndex);
4746
+ const flagPanel = renderFlagPanel();
4747
+ const body = width >= 118 ? joinColumns(menuPanel, flagPanel, width) : [...menuPanel, "", ...flagPanel];
4748
+ return `${HOME_CLEAR}${HIDE_CURSOR}${body.join(`
4749
+ `)}`;
4750
+ }
4751
+ function renderProgressBar(frame, width) {
4752
+ const innerWidth = Math.max(12, width - 2);
4753
+ const headSize = Math.max(4, Math.floor(innerWidth / 5));
4754
+ const travel = Math.max(1, innerWidth - headSize);
4755
+ const cycle = travel * 2;
4756
+ const offset = frame % cycle;
4757
+ const start = offset <= travel ? offset : cycle - offset;
4758
+ const cells = Array.from({ length: innerWidth }, (_, index) => {
4759
+ if (index >= start && index < start + headSize) {
4760
+ return "=";
4761
+ }
4762
+ return "-";
4763
+ }).join("");
4764
+ return color(`[${cells}]`, CYAN);
4765
+ }
4766
+ function renderLoading(request, frame = 0, startedAt = Date.now()) {
4767
+ const elapsedSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1000));
4768
+ const progressBar = renderProgressBar(frame, 34);
4769
+ const lines = [
4770
+ color(request.loadingTitle, WHITE + BOLD),
4771
+ color(request.preview, CYAN),
4772
+ "",
4773
+ color(request.loadingDetail, DIM),
4774
+ "",
4775
+ color("Status", WHITE + BOLD),
4776
+ color("Working... stay in tokenleak while this finishes.", YELLOW),
4777
+ "",
4778
+ color("Progress", WHITE + BOLD),
4779
+ progressBar,
4780
+ color(`Elapsed ${elapsedSeconds}s`, DIM),
4781
+ "",
4782
+ renderRule(44)
4783
+ ];
4784
+ return `${HOME_CLEAR}${HIDE_CURSOR}${lines.join(`
4785
+ `)}`;
4786
+ }
4787
+ function clampScrollOffset(offset, totalLines, viewportHeight) {
4788
+ const maxOffset = Math.max(0, totalLines - Math.max(1, viewportHeight));
4789
+ return Math.min(Math.max(0, offset), maxOffset);
4790
+ }
4791
+ function buildOutputSectionLines(title, content, width) {
4792
+ const normalized = content.trimEnd();
4793
+ if (!normalized)
4794
+ return [];
4795
+ const lines = normalized.split(`
4796
+ `).map((line) => truncateVisible2(line, width));
4797
+ return [
4798
+ color(title, WHITE + BOLD),
4799
+ ...lines,
4800
+ ""
4801
+ ];
4802
+ }
4803
+ function renderResult(request, result, scrollOffset = 0) {
4804
+ const width = Math.max(60, (process.stdout.columns ?? 120) - 1);
4805
+ const rows = process.stdout.rows ?? 40;
4806
+ const statusColor = result.ok ? GREEN : RED;
4807
+ const statusLabel = result.ok ? "Completed" : "Failed";
4808
+ const header = [
4809
+ color(request.title, WHITE + BOLD),
4810
+ color(request.preview, CYAN),
4811
+ "",
4812
+ `${color("Status", WHITE + BOLD)} ${color(statusLabel, statusColor)}`,
4813
+ color(result.summary, DIM),
4814
+ ""
4815
+ ];
4816
+ const contentLines = [
4817
+ ...buildOutputSectionLines("Output", result.stdout, width),
4818
+ ...buildOutputSectionLines("Messages", result.stderr, width)
4819
+ ];
4820
+ const body = contentLines.length > 0 ? contentLines : [color("No captured output for this command.", DIM), ""];
4821
+ const footer = [
4822
+ renderRule(44),
4823
+ `${color("Up/Down", YELLOW)} scroll ${color("PgUp/PgDn", YELLOW)} page ${color("Enter", YELLOW)} launcher ${color("Q", YELLOW)} quit`
4824
+ ];
4825
+ const viewportHeight = Math.max(4, rows - header.length - footer.length - 1);
4826
+ const effectiveOffset = clampScrollOffset(scrollOffset, body.length, viewportHeight);
4827
+ const visibleBody = body.slice(effectiveOffset, effectiveOffset + viewportHeight);
4828
+ const padding = Array.from({ length: Math.max(0, viewportHeight - visibleBody.length) }, () => "");
4829
+ const scrollStatus = body.length > viewportHeight ? color(`Lines ${effectiveOffset + 1}-${Math.min(body.length, effectiveOffset + viewportHeight)} of ${body.length}`, DIM) : color("All command output is visible.", DIM);
4830
+ const lines = [
4831
+ ...header,
4832
+ ...visibleBody,
4833
+ ...padding,
4834
+ scrollStatus,
4835
+ ...footer
4836
+ ];
4837
+ return `${HOME_CLEAR}${HIDE_CURSOR}${lines.join(`
4838
+ `)}`;
4839
+ }
4840
+ function enterAltScreen() {
4841
+ process.stdout.write(`${ALT_SCREEN_ON}${ALT_SCROLL_ON}${HOME_CLEAR}${HIDE_CURSOR}`);
4842
+ }
4843
+ function leaveAltScreen() {
4844
+ process.stdout.write(`${SHOW_CURSOR}${ALT_SCROLL_OFF}${ALT_SCREEN_OFF}`);
4845
+ }
4846
+ function paint(content) {
4847
+ process.stdout.write(content);
4848
+ }
4849
+ function suspendRawMode() {
4850
+ if (process.stdin.isTTY) {
4851
+ process.stdin.setRawMode(false);
4852
+ }
4853
+ process.stdin.pause();
4854
+ process.stdout.write(SHOW_CURSOR);
4855
+ }
4856
+ function resumeRawMode() {
4857
+ if (process.stdin.isTTY) {
4858
+ process.stdin.setRawMode(true);
4859
+ }
4860
+ process.stdin.resume();
4861
+ process.stdout.write(HIDE_CURSOR);
4862
+ }
4863
+
4864
+ class InteractiveExitError extends Error {
4865
+ constructor() {
4866
+ super("Interactive session cancelled");
4867
+ this.name = "InteractiveExitError";
4868
+ }
4869
+ }
4870
+ var PROVIDER_CHOICES = [
4871
+ { value: "claude-code", label: "Claude Code", description: "Anthropic project logs" },
4872
+ { value: "codex", label: "Codex", description: "OpenAI session logs" },
4873
+ { value: "open-code", label: "Open Code", description: "Open Code storage and database" }
4874
+ ];
4875
+ function isInteractiveExitError(error) {
4876
+ return error instanceof InteractiveExitError;
4877
+ }
4878
+ function parsePositiveInteger(value) {
4879
+ const trimmed = value.trim();
4880
+ if (trimmed === "")
4881
+ return null;
4882
+ const parsed = Number(trimmed);
4883
+ if (!Number.isInteger(parsed) || parsed <= 0) {
4884
+ throw new Error(`Expected a positive whole number, received "${value}".`);
4885
+ }
4886
+ return parsed;
4887
+ }
4888
+ 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`) {
4889
+ const optionLines = options.map((option, index) => {
4890
+ const isSelected = index === selectedIndex;
4891
+ const isChecked = selectedValues ? selectedValues.has(option.value) : isSelected;
4892
+ const pointer = isSelected ? color(">", GREEN) : " ";
4893
+ const checkbox = selectedValues ? isChecked ? color("[x]", GREEN) : color("[ ]", DIM) : isSelected ? color("[\u2022]", GREEN) : color("[ ]", DIM);
4894
+ const titleColor = isSelected ? WHITE + BOLD : WHITE;
4895
+ const descriptionColor = isSelected ? CYAN : DIM;
4896
+ return `${pointer} ${checkbox} ${color(option.label, titleColor)} ${color(option.description, descriptionColor)}`;
4897
+ });
4898
+ const lines = [
4899
+ color(title, WHITE + BOLD),
4900
+ color(description, DIM),
4901
+ "",
4902
+ ...optionLines,
4903
+ "",
4904
+ renderRule(44),
4905
+ footer
4906
+ ];
4907
+ return `${HOME_CLEAR}${HIDE_CURSOR}${lines.join(`
4908
+ `)}`;
4909
+ }
4910
+ async function ask(prompt, initialValue = "") {
4911
+ const readline = createInterface({
4912
+ input: process.stdin,
4913
+ output: process.stdout
4914
+ });
4915
+ let settled = false;
4916
+ return new Promise((resolve, reject) => {
4917
+ const finish = (fn) => {
4918
+ if (settled)
4919
+ return;
4920
+ settled = true;
4921
+ readline.off("SIGINT", onSigint);
4922
+ readline.close();
4923
+ fn();
4924
+ };
4925
+ const onSigint = () => {
4926
+ finish(() => reject(new InteractiveExitError));
4927
+ };
4928
+ readline.on("SIGINT", onSigint);
4929
+ const suffix = initialValue ? ` (${initialValue})` : "";
4930
+ readline.question(`${prompt}${suffix}: `).then((value) => {
4931
+ finish(() => resolve(value.trim() || initialValue));
4932
+ }).catch((error) => {
4933
+ finish(() => reject(error));
4934
+ });
4935
+ });
4936
+ }
4937
+ async function askYesNo(prompt, defaultValue = false) {
4938
+ const hint = defaultValue ? "Y/n" : "y/N";
4939
+ const value = (await ask(`${prompt} [${hint}]`)).toLowerCase();
4940
+ if (value === "")
4941
+ return defaultValue;
4942
+ return value === "y" || value === "yes";
4943
+ }
4944
+ async function promptSingleChoice(title, description, options, initialIndex = 0) {
4945
+ return new Promise((resolve, reject) => {
4946
+ let selectedIndex = Math.max(0, Math.min(initialIndex, options.length - 1));
4947
+ const onKeypress = (_input, key) => {
4948
+ if (key.ctrl && key.name === "c") {
4949
+ cleanup();
4950
+ reject(new InteractiveExitError);
4951
+ return;
4952
+ }
4953
+ if (key.name === "up") {
4954
+ selectedIndex = (selectedIndex - 1 + options.length) % options.length;
4955
+ render();
4956
+ return;
4957
+ }
4958
+ if (key.name === "down") {
4959
+ selectedIndex = (selectedIndex + 1) % options.length;
4960
+ render();
4961
+ return;
4962
+ }
4963
+ const digit = key.sequence?.match(/^[1-9]$/)?.[0];
4964
+ if (digit) {
4965
+ const index = Number(digit) - 1;
4966
+ if (index < options.length) {
4967
+ selectedIndex = index;
4968
+ cleanup();
4969
+ resolve(options[selectedIndex].value);
4970
+ }
4971
+ return;
4972
+ }
4973
+ if (key.name === "return" || key.name === "enter") {
4974
+ cleanup();
4975
+ resolve(options[selectedIndex].value);
4976
+ }
4977
+ };
4978
+ function render() {
4979
+ 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`));
4980
+ }
4981
+ function cleanup() {
4982
+ process.stdin.off("keypress", onKeypress);
4983
+ suspendRawMode();
4984
+ }
4985
+ render();
4986
+ resumeRawMode();
4987
+ process.stdin.on("keypress", onKeypress);
4988
+ });
4989
+ }
4990
+ async function promptMultiChoice(title, description, options, initialValues = []) {
4991
+ return new Promise((resolve, reject) => {
4992
+ let selectedIndex = 0;
4993
+ const selectedValues = new Set(initialValues);
4994
+ const onKeypress = (_input, key) => {
4995
+ if (key.ctrl && key.name === "c") {
4996
+ cleanup();
4997
+ reject(new InteractiveExitError);
4998
+ return;
4999
+ }
5000
+ if (key.name === "up") {
5001
+ selectedIndex = (selectedIndex - 1 + options.length) % options.length;
5002
+ render();
5003
+ return;
5004
+ }
5005
+ if (key.name === "down") {
5006
+ selectedIndex = (selectedIndex + 1) % options.length;
5007
+ render();
5008
+ return;
5009
+ }
5010
+ if (key.name === "space") {
5011
+ toggleSelected(selectedIndex);
5012
+ render();
5013
+ return;
5014
+ }
5015
+ const digit = key.sequence?.match(/^[1-9]$/)?.[0];
5016
+ if (digit) {
5017
+ const index = Number(digit) - 1;
5018
+ if (index < options.length) {
5019
+ selectedIndex = index;
5020
+ toggleSelected(selectedIndex);
5021
+ render();
5022
+ }
5023
+ return;
5024
+ }
5025
+ if (key.name === "return" || key.name === "enter") {
5026
+ cleanup();
5027
+ resolve(Array.from(selectedValues));
5028
+ }
5029
+ };
5030
+ function toggleSelected(index) {
5031
+ const value = options[index].value;
5032
+ if (selectedValues.has(value)) {
5033
+ selectedValues.delete(value);
5034
+ } else {
5035
+ selectedValues.add(value);
5036
+ }
5037
+ }
5038
+ function render() {
5039
+ paint(renderChoiceScreen(title, description, options, selectedIndex, selectedValues));
5040
+ }
5041
+ function cleanup() {
5042
+ process.stdin.off("keypress", onKeypress);
5043
+ suspendRawMode();
5044
+ }
5045
+ render();
5046
+ resumeRawMode();
5047
+ process.stdin.on("keypress", onKeypress);
5048
+ });
5049
+ }
5050
+ function applySelectedProviders(args, providers) {
5051
+ if (providers.length === 0)
5052
+ return;
5053
+ args["provider"] = providers.join(",");
5054
+ }
5055
+ async function promptTheme(defaultTheme = "dark") {
5056
+ const theme = await promptSingleChoice("Theme", "Pick the rendering theme.", [
5057
+ { value: "dark", label: "Dark", description: "High-contrast dark canvas" },
5058
+ { value: "light", label: "Light", description: "Bright export with light background" }
5059
+ ], defaultTheme === "light" ? 1 : 0);
5060
+ return theme;
5061
+ }
5062
+ async function promptDateWindow() {
5063
+ const choice = await promptSingleChoice("Date Window", "Choose how much history to include.", [
5064
+ { value: "7", label: "Last 7 days", description: "Quick recent snapshot" },
5065
+ { value: "30", label: "Last 30 days", description: "Short-term trend window" },
5066
+ { value: "90", label: "Last 90 days", description: "Default overview" },
5067
+ { value: "365", label: "Last 365 days", description: "Long-range usage pattern" },
5068
+ { value: "custom", label: "Custom range", description: "Enter exact dates manually" }
5069
+ ], 2);
5070
+ if (choice !== "custom") {
5071
+ return { days: Number(choice) };
5072
+ }
5073
+ const since = await ask("Since date YYYY-MM-DD");
5074
+ const until = await ask("Until date YYYY-MM-DD (blank for today)");
5075
+ const args = { since };
5076
+ if (until)
5077
+ args["until"] = until;
5078
+ return args;
5079
+ }
5080
+ async function promptProviderSelection(title = "Provider Filter") {
5081
+ return promptMultiChoice(title, "Toggle one or more providers. Leave everything unchecked to use auto-detection.", PROVIDER_CHOICES);
5082
+ }
5083
+ async function promptOutputPath(defaultPath) {
5084
+ return ask("Output file", defaultPath);
5085
+ }
5086
+ async function promptWidth() {
5087
+ const choice = await promptSingleChoice("Terminal Width", "Choose the dashboard width.", [
5088
+ { value: "80", label: "80 columns", description: "Standard terminal width" },
5089
+ { value: "100", label: "100 columns", description: "Balanced dashboard layout" },
5090
+ { value: "120", label: "120 columns", description: "Wide dashboard layout" },
5091
+ { value: "custom", label: "Custom width", description: "Enter an exact width" }
5092
+ ], 1);
5093
+ if (choice !== "custom") {
5094
+ return Number(choice);
5095
+ }
5096
+ while (true) {
5097
+ try {
5098
+ const parsed = parsePositiveInteger(await ask("Custom width"));
5099
+ if (parsed !== null) {
5100
+ return parsed;
5101
+ }
5102
+ paint(`${HOME_CLEAR}${SHOW_CURSOR}${color("Width required", RED)}
5103
+ ${color("Enter a positive whole number to continue.", DIM)}
5104
+
5105
+ Press Enter to try again.`);
5106
+ await ask("");
5107
+ } catch (error) {
5108
+ paint(`${HOME_CLEAR}${SHOW_CURSOR}${color("Invalid width", RED)}
5109
+ ${color(error instanceof Error ? error.message : String(error), DIM)}
5110
+
5111
+ Press Enter to try again.`);
5112
+ await ask("");
5113
+ }
5114
+ }
5115
+ }
5116
+ async function promptCompareSetting() {
5117
+ const choice = await promptSingleChoice("Compare Mode", "Optionally compare the current range against an earlier period.", [
5118
+ { value: "off", label: "No compare", description: "Render a standard single-period report" },
5119
+ { value: "auto", label: "Auto compare", description: "Split the selected window automatically" },
5120
+ { value: "custom", label: "Custom compare range", description: "Provide an explicit YYYY-MM-DD..YYYY-MM-DD range" }
5121
+ ]);
5122
+ if (choice === "off")
5123
+ return null;
5124
+ if (choice === "auto")
5125
+ return "auto";
5126
+ return ask("Previous range YYYY-MM-DD..YYYY-MM-DD");
5127
+ }
5128
+ function inferDashboardTimeRange(rangeArgs) {
5129
+ const days = typeof rangeArgs["days"] === "number" ? rangeArgs["days"] : null;
5130
+ if (days !== null) {
5131
+ if (days <= 7)
5132
+ return "7d";
5133
+ if (days <= 30)
5134
+ return "30d";
5135
+ if (days <= 90)
5136
+ return "90d";
5137
+ return "365d";
5138
+ }
5139
+ const since = typeof rangeArgs["since"] === "string" ? rangeArgs["since"].trim() : "";
5140
+ if (!since)
5141
+ return "30d";
5142
+ const rawUntil = typeof rangeArgs["until"] === "string" ? rangeArgs["until"].trim() : "";
5143
+ const until = rawUntil || new Date().toISOString().slice(0, 10);
5144
+ const sinceMs = Date.parse(`${since}T00:00:00.000Z`);
5145
+ const untilMs = Date.parse(`${until}T00:00:00.000Z`);
5146
+ if (!Number.isFinite(sinceMs) || !Number.isFinite(untilMs) || sinceMs > untilMs) {
5147
+ return "30d";
5148
+ }
5149
+ const spanDays = Math.max(1, Math.ceil((untilMs - sinceMs) / DAY_MS));
5150
+ if (spanDays <= 7)
5151
+ return "7d";
5152
+ if (spanDays <= 30)
5153
+ return "30d";
5154
+ if (spanDays <= 90)
5155
+ return "90d";
5156
+ return "365d";
5157
+ }
5158
+ function buildTabbedDashboardOptions(rangeArgs, providers, width, noInsights, noColor2) {
5159
+ const options = {
5160
+ initialTimeRange: inferDashboardTimeRange(rangeArgs),
5161
+ noColor: noColor2,
5162
+ noInsights
5163
+ };
5164
+ const rawSince = typeof rangeArgs["since"] === "string" ? rangeArgs["since"].trim() : "";
5165
+ const rawUntil = typeof rangeArgs["until"] === "string" ? rangeArgs["until"].trim() : "";
5166
+ const since = rawSince || undefined;
5167
+ const until = rawUntil || undefined;
5168
+ if (since) {
5169
+ options.initialRange = computeDateRange({ since, until });
5170
+ options.until = options.initialRange.until;
5171
+ } else if (until) {
5172
+ options.until = computeDateRange({ until }).until;
5173
+ }
5174
+ if (providers.length > 0) {
5175
+ options.providerNames = [...providers];
5176
+ }
5177
+ if (width !== null) {
5178
+ options.width = width;
5179
+ }
5180
+ return options;
5181
+ }
5182
+ function createTabbedDashboardRequest(options) {
5183
+ const args = {};
5184
+ const initialDays = options.initialTimeRange ? TAB_RANGE_DAY_COUNTS[options.initialTimeRange] : undefined;
5185
+ if (options.initialRange) {
5186
+ args["since"] = options.initialRange.since;
5187
+ args["until"] = options.initialRange.until;
5188
+ } else {
5189
+ if (initialDays !== undefined)
5190
+ args["days"] = initialDays;
5191
+ if (options.until)
5192
+ args["until"] = options.until;
5193
+ }
5194
+ if (options.providerNames && options.providerNames.length > 0) {
5195
+ args["provider"] = options.providerNames.join(",");
5196
+ }
5197
+ if (options.width !== undefined)
5198
+ args["width"] = options.width;
5199
+ if (options.noInsights)
5200
+ args["noInsights"] = true;
5201
+ if (options.noColor)
5202
+ args["noColor"] = true;
5203
+ return {
5204
+ args,
5205
+ preview: buildCliPreview(args),
5206
+ title: "Launch Dashboard",
5207
+ loadingTitle: "Starting dashboard",
5208
+ loadingDetail: "Launching the interactive terminal dashboard",
5209
+ executionMode: "inherit"
5210
+ };
5211
+ }
5212
+ async function buildDashboardPreset() {
5213
+ const rangeArgs = await promptDateWindow();
5214
+ const providers = await promptProviderSelection();
5215
+ const width = await promptWidth();
5216
+ const noInsights = await askYesNo("Hide insights panel", false);
5217
+ const noColor2 = await askYesNo("Disable ANSI colors", false);
5218
+ return {
5219
+ type: "tabbed-dashboard",
5220
+ options: buildTabbedDashboardOptions(rangeArgs, providers, width, noInsights, noColor2)
5221
+ };
5222
+ }
5223
+ async function buildJsonPreset() {
5224
+ const rangeArgs = await promptDateWindow();
5225
+ const providers = await promptProviderSelection();
5226
+ const compare = await promptCompareSetting();
5227
+ const saveToFile = await askYesNo("Write JSON to a file", false);
5228
+ const clipboard = !saveToFile && await askYesNo("Copy JSON to clipboard after render", false);
5229
+ const args = {
5230
+ format: "json",
5231
+ ...rangeArgs
5232
+ };
5233
+ applySelectedProviders(args, providers);
5234
+ if (compare)
5235
+ args["compare"] = compare;
5236
+ if (saveToFile) {
5237
+ args["output"] = await promptOutputPath(compare ? "tokenleak-compare.json" : "tokenleak.json");
5238
+ }
5239
+ if (clipboard)
5240
+ args["clipboard"] = true;
5241
+ return createRunCommand(args);
5242
+ }
5243
+ async function buildImagePreset(format) {
5244
+ const theme = await promptTheme();
5245
+ const rangeArgs = await promptDateWindow();
5246
+ const providers = await promptProviderSelection("Provider Filter");
5247
+ const compare = await promptCompareSetting();
5248
+ const output = await promptOutputPath(`tokenleak.${format}`);
5249
+ const shouldOpen = await askYesNo("Open the file when done", true);
5250
+ const more = compare ? true : await askYesNo("Enable --more stats", format === "png");
5251
+ const args = {
5252
+ format,
5253
+ theme,
5254
+ output,
5255
+ open: shouldOpen,
5256
+ more,
5257
+ ...rangeArgs
5258
+ };
5259
+ applySelectedProviders(args, providers);
5260
+ if (compare)
5261
+ args["compare"] = compare;
5262
+ return createRunCommand(args);
5263
+ }
5264
+ async function buildComparePreset() {
5265
+ const rangeArgs = await promptDateWindow();
5266
+ const providers = await promptProviderSelection();
5267
+ const compareMode = await promptSingleChoice("Reference Period", "Choose how the earlier comparison period should be defined.", [
5268
+ { value: "auto", label: "Auto compare", description: "Split the chosen window automatically" },
5269
+ { value: "custom", label: "Custom compare range", description: "Enter an explicit prior range manually" }
5270
+ ]);
5271
+ const compare = compareMode === "custom" ? await ask("Previous range YYYY-MM-DD..YYYY-MM-DD") : "auto";
5272
+ const saveToFile = await askYesNo("Write compare output to a file", false);
5273
+ const args = {
5274
+ format: "json",
5275
+ compare,
5276
+ ...rangeArgs
5277
+ };
5278
+ applySelectedProviders(args, providers);
5279
+ if (saveToFile) {
5280
+ args["output"] = await promptOutputPath("tokenleak-compare.json");
5281
+ }
5282
+ return createRunCommand(args);
5283
+ }
5284
+ async function buildLivePreset() {
5285
+ const theme = await promptTheme();
5286
+ const rangeArgs = await promptDateWindow();
5287
+ const providers = await promptProviderSelection();
5288
+ const more = await askYesNo("Enable --more stats", true);
5289
+ const args = {
5290
+ liveServer: true,
5291
+ theme,
5292
+ more,
5293
+ ...rangeArgs
5294
+ };
5295
+ applySelectedProviders(args, providers);
5296
+ return createRunCommand(args);
5297
+ }
5298
+ async function askFormatChoice() {
5299
+ return promptSingleChoice("Output Format", "Choose the primary renderer for this command.", [
5300
+ { value: "terminal", label: "Terminal", description: "Dashboard in the current terminal" },
5301
+ { value: "json", label: "JSON", description: "Structured machine-readable output" },
5302
+ { value: "svg", label: "SVG", description: "Shareable vector export" },
5303
+ { value: "png", label: "PNG", description: "Raster export for social and docs" }
5304
+ ]);
5305
+ }
5306
+ async function buildCustomCommand() {
5307
+ const mode = await promptSingleChoice("Command Type", "Choose the command family you want to configure.", [
5308
+ { value: "run", label: "Standard command", description: "Render terminal, JSON, SVG, or PNG output" },
5309
+ { value: "live-server", label: "Live server", description: "Launch the browser dashboard locally" },
5310
+ { value: "list-providers", label: "List providers", description: "Inspect registered provider backends" }
5311
+ ]);
5312
+ if (mode === "live-server") {
5313
+ return buildLivePreset();
5314
+ }
5315
+ if (mode === "list-providers") {
5316
+ return createRunCommand({ listProviders: true });
5317
+ }
5318
+ const format = await askFormatChoice();
5319
+ const theme = format === "terminal" ? null : await promptTheme();
5320
+ const rangeArgs = await promptDateWindow();
5321
+ const providers = await promptProviderSelection();
5322
+ const compare = await promptCompareSetting();
5323
+ const width = format === "terminal" ? await promptWidth() : null;
5324
+ 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}`);
5325
+ const noColor2 = await askYesNo("Disable ANSI colors", false);
5326
+ const noInsights = format === "terminal" ? await askYesNo("Hide insights", false) : false;
5327
+ const more = await askYesNo("Enable --more stats", format === "png" || format === "svg");
5328
+ const clipboard = format !== "png" ? await askYesNo("Copy output to clipboard", false) : false;
5329
+ const open = format !== "terminal" ? await askYesNo("Open generated file", false) : false;
5330
+ const upload = format !== "png" ? await ask("Upload target [blank/gist]") : "";
5331
+ const args = {
5332
+ format,
5333
+ ...rangeArgs
5334
+ };
5335
+ if (theme)
5336
+ args["theme"] = theme;
5337
+ if (compare)
5338
+ args["compare"] = compare;
5339
+ if (width)
5340
+ args["width"] = width;
5341
+ if (output)
5342
+ args["output"] = output;
5343
+ if (noColor2)
5344
+ args["noColor"] = true;
5345
+ if (noInsights)
5346
+ args["noInsights"] = true;
5347
+ if (more)
5348
+ args["more"] = true;
5349
+ if (clipboard)
5350
+ args["clipboard"] = true;
5351
+ if (open)
5352
+ args["open"] = true;
5353
+ if (upload)
5354
+ args["upload"] = upload;
5355
+ applySelectedProviders(args, providers);
5356
+ return createRunCommand(args);
5357
+ }
5358
+ function createMenuOptions() {
5359
+ return [
5360
+ {
5361
+ digit: "1",
5362
+ title: "Launch Dashboard",
5363
+ description: "guided terminal view",
5364
+ preview: "tokenleak --days 90",
5365
+ select: buildDashboardPreset
5366
+ },
5367
+ {
5368
+ digit: "2",
5369
+ title: "Export JSON",
5370
+ description: "structured output for scripts",
5371
+ preview: "tokenleak --format json",
5372
+ select: buildJsonPreset
5373
+ },
5374
+ {
5375
+ digit: "3",
5376
+ title: "Export SVG",
5377
+ description: "shareable vector card",
5378
+ preview: "tokenleak --format svg --output tokenleak.svg",
5379
+ select: async () => buildImagePreset("svg")
5380
+ },
5381
+ {
5382
+ digit: "4",
5383
+ title: "Export PNG",
5384
+ description: "social-ready raster image",
5385
+ preview: "tokenleak --format png --output tokenleak.png --more",
5386
+ select: async () => buildImagePreset("png")
5387
+ },
5388
+ {
5389
+ digit: "5",
5390
+ title: "Compare Periods",
5391
+ description: "diff current vs previous usage",
5392
+ preview: "tokenleak --compare auto --format json",
5393
+ select: buildComparePreset
5394
+ },
5395
+ {
5396
+ digit: "6",
5397
+ title: "Start Live Server",
5398
+ description: "browser dashboard on localhost",
5399
+ preview: "tokenleak --live-server --theme dark",
5400
+ select: buildLivePreset
5401
+ },
5402
+ {
5403
+ digit: "7",
5404
+ title: "Build Custom Command",
5405
+ description: "configure flags interactively",
5406
+ preview: "tokenleak --format terminal --days 90",
5407
+ select: buildCustomCommand
5408
+ },
5409
+ {
5410
+ digit: "8",
5411
+ title: "Full Help",
5412
+ description: "examples and complete usage",
5413
+ preview: "tokenleak --help",
5414
+ select: async () => ({ type: "show-help" })
5415
+ },
5416
+ {
5417
+ digit: "9",
5418
+ title: "List Providers",
5419
+ description: "detect available registries",
5420
+ preview: "tokenleak --list-providers",
5421
+ select: async () => createRunCommand({ listProviders: true })
5422
+ },
5423
+ {
5424
+ digit: "0",
5425
+ title: "Exit",
5426
+ description: "close the launcher",
5427
+ preview: "exit",
5428
+ select: async () => ({ type: "exit" })
5429
+ }
5430
+ ];
3008
5431
  }
3009
-
3010
- // packages/cli/src/env.ts
3011
- var VALID_FORMATS = new Set(["json", "svg", "png", "terminal"]);
3012
- var VALID_THEMES = new Set(["dark", "light"]);
3013
- function loadEnvOverrides() {
3014
- const overrides = {};
3015
- const format = process.env["TOKENLEAK_FORMAT"];
3016
- if (format && VALID_FORMATS.has(format)) {
3017
- overrides.format = format;
3018
- }
3019
- const theme = process.env["TOKENLEAK_THEME"];
3020
- if (theme && VALID_THEMES.has(theme)) {
3021
- overrides.theme = theme;
3022
- }
3023
- const days = process.env["TOKENLEAK_DAYS"];
3024
- if (days !== undefined && days !== "") {
3025
- const parsed = Number(days);
3026
- if (Number.isFinite(parsed) && parsed > 0) {
3027
- overrides.days = parsed;
5432
+ async function promptForMenuCommand(context, options, state) {
5433
+ let showHelp = false;
5434
+ let resolving = false;
5435
+ return new Promise((resolve, reject) => {
5436
+ const onKeypress = async (_input, key) => {
5437
+ if (resolving) {
5438
+ return;
5439
+ }
5440
+ if (key.ctrl && key.name === "c") {
5441
+ cleanup();
5442
+ resolve({ type: "exit" });
5443
+ return;
5444
+ }
5445
+ if (showHelp) {
5446
+ if (key.name === "escape" || key.name === "return" || key.name === "enter" || key.name === "q" || key.name === "h") {
5447
+ showHelp = false;
5448
+ paint(renderLauncher(context, options, state.selectedIndex));
5449
+ }
5450
+ return;
5451
+ }
5452
+ if (key.name === "up") {
5453
+ state.selectedIndex = (state.selectedIndex - 1 + options.length) % options.length;
5454
+ paint(renderLauncher(context, options, state.selectedIndex));
5455
+ return;
5456
+ }
5457
+ if (key.name === "down") {
5458
+ state.selectedIndex = (state.selectedIndex + 1) % options.length;
5459
+ paint(renderLauncher(context, options, state.selectedIndex));
5460
+ return;
5461
+ }
5462
+ if (key.name === "h") {
5463
+ showHelp = true;
5464
+ paint(renderHelpOverlay(context.helpText, Math.max(60, (process.stdout.columns ?? 120) - 1)));
5465
+ return;
5466
+ }
5467
+ if (key.name === "q" || key.name === "escape") {
5468
+ cleanup();
5469
+ resolve({ type: "exit" });
5470
+ return;
5471
+ }
5472
+ const digit = key.sequence?.match(/^[0-9]$/)?.[0];
5473
+ if (digit) {
5474
+ const nextIndex = options.findIndex((option) => option.digit === digit);
5475
+ if (nextIndex >= 0) {
5476
+ state.selectedIndex = nextIndex;
5477
+ if (digit === "8") {
5478
+ showHelp = true;
5479
+ paint(renderHelpOverlay(context.helpText, Math.max(60, (process.stdout.columns ?? 120) - 1)));
5480
+ return;
5481
+ }
5482
+ resolving = true;
5483
+ cleanup();
5484
+ try {
5485
+ const command = await options[nextIndex].select();
5486
+ resolve(command);
5487
+ } catch (error) {
5488
+ if (isInteractiveExitError(error)) {
5489
+ resolve({ type: "exit" });
5490
+ return;
5491
+ }
5492
+ reject(error);
5493
+ }
5494
+ }
5495
+ return;
5496
+ }
5497
+ if (key.name === "return" || key.name === "enter") {
5498
+ if (options[state.selectedIndex].digit === "8") {
5499
+ showHelp = true;
5500
+ paint(renderHelpOverlay(context.helpText, Math.max(60, (process.stdout.columns ?? 120) - 1)));
5501
+ return;
5502
+ }
5503
+ resolving = true;
5504
+ cleanup();
5505
+ try {
5506
+ const command = await options[state.selectedIndex].select();
5507
+ resolve(command);
5508
+ } catch (error) {
5509
+ if (isInteractiveExitError(error)) {
5510
+ resolve({ type: "exit" });
5511
+ return;
5512
+ }
5513
+ reject(error);
5514
+ }
5515
+ }
5516
+ };
5517
+ function cleanup() {
5518
+ process.stdin.off("keypress", onKeypress);
5519
+ suspendRawMode();
3028
5520
  }
3029
- }
3030
- return overrides;
5521
+ paint(renderLauncher(context, options, state.selectedIndex));
5522
+ resumeRawMode();
5523
+ process.stdin.on("keypress", onKeypress);
5524
+ });
3031
5525
  }
3032
-
3033
- // packages/cli/src/errors.ts
3034
- class TokenleakError extends Error {
3035
- constructor(message) {
3036
- super(message);
3037
- this.name = "TokenleakError";
3038
- }
5526
+ async function showExecutionResult(request, result) {
5527
+ const body = [
5528
+ ...buildOutputSectionLines("Output", result.stdout, Math.max(60, (process.stdout.columns ?? 120) - 1)),
5529
+ ...buildOutputSectionLines("Messages", result.stderr, Math.max(60, (process.stdout.columns ?? 120) - 1))
5530
+ ];
5531
+ let scrollOffset = 0;
5532
+ return new Promise((resolve) => {
5533
+ const viewportHeight = () => {
5534
+ const rows = process.stdout.rows ?? 40;
5535
+ const headerLines = 6;
5536
+ const footerLines = 3;
5537
+ return Math.max(4, rows - headerLines - footerLines);
5538
+ };
5539
+ const render = () => {
5540
+ paint(renderResult(request, result, scrollOffset));
5541
+ };
5542
+ const onKeypress = (_input, key) => {
5543
+ if (key.ctrl && key.name === "c") {
5544
+ cleanup();
5545
+ resolve("exit");
5546
+ return;
5547
+ }
5548
+ if (key.name === "q" || key.name === "escape") {
5549
+ cleanup();
5550
+ resolve("exit");
5551
+ return;
5552
+ }
5553
+ if (key.name === "return" || key.name === "enter") {
5554
+ cleanup();
5555
+ resolve("menu");
5556
+ return;
5557
+ }
5558
+ const page = viewportHeight();
5559
+ if (key.name === "up") {
5560
+ scrollOffset = clampScrollOffset(scrollOffset - 1, body.length, page);
5561
+ render();
5562
+ return;
5563
+ }
5564
+ if (key.name === "down") {
5565
+ scrollOffset = clampScrollOffset(scrollOffset + 1, body.length, page);
5566
+ render();
5567
+ return;
5568
+ }
5569
+ if (key.name === "pageup") {
5570
+ scrollOffset = clampScrollOffset(scrollOffset - page, body.length, page);
5571
+ render();
5572
+ return;
5573
+ }
5574
+ if (key.name === "pagedown") {
5575
+ scrollOffset = clampScrollOffset(scrollOffset + page, body.length, page);
5576
+ render();
5577
+ return;
5578
+ }
5579
+ if (key.name === "home") {
5580
+ scrollOffset = 0;
5581
+ render();
5582
+ return;
5583
+ }
5584
+ if (key.name === "end") {
5585
+ scrollOffset = clampScrollOffset(Number.MAX_SAFE_INTEGER, body.length, page);
5586
+ render();
5587
+ }
5588
+ };
5589
+ const cleanup = () => {
5590
+ process.stdin.off("keypress", onKeypress);
5591
+ suspendRawMode();
5592
+ };
5593
+ render();
5594
+ resumeRawMode();
5595
+ process.stdin.on("keypress", onKeypress);
5596
+ });
3039
5597
  }
3040
- function handleError(error) {
3041
- if (error instanceof TokenleakError) {
3042
- process.stderr.write(`Error: ${error.message}
3043
- `);
3044
- } else if (error instanceof Error) {
3045
- process.stderr.write(`Error: ${error.message}
3046
- `);
3047
- } else {
3048
- process.stderr.write(`Error: ${String(error)}
3049
- `);
5598
+ function shouldStartInteractiveCli(argv, stdinIsTTY, stdoutIsTTY) {
5599
+ return argv.length === 0 && stdinIsTTY && stdoutIsTTY;
5600
+ }
5601
+ async function startInteractiveCli(context, execute, launchTabbedDashboard) {
5602
+ const options = createMenuOptions();
5603
+ const state = { selectedIndex: 0 };
5604
+ let interrupted = false;
5605
+ let ignoreSigint = false;
5606
+ const onSigint = () => {
5607
+ if (!ignoreSigint) {
5608
+ interrupted = true;
5609
+ }
5610
+ };
5611
+ emitKeypressEvents(process.stdin);
5612
+ enterAltScreen();
5613
+ process.on("SIGINT", onSigint);
5614
+ try {
5615
+ while (true) {
5616
+ if (interrupted) {
5617
+ return;
5618
+ }
5619
+ const command = await promptForMenuCommand(context, options, state);
5620
+ if (command.type === "exit") {
5621
+ return;
5622
+ }
5623
+ if (command.type === "show-help") {
5624
+ continue;
5625
+ }
5626
+ if (command.type === "tabbed-dashboard") {
5627
+ if (launchTabbedDashboard) {
5628
+ const request = createTabbedDashboardRequest(command.options);
5629
+ let dashboardError = null;
5630
+ leaveAltScreen();
5631
+ try {
5632
+ ignoreSigint = true;
5633
+ await launchTabbedDashboard(command.options);
5634
+ } catch (error) {
5635
+ dashboardError = error;
5636
+ } finally {
5637
+ ignoreSigint = false;
5638
+ enterAltScreen();
5639
+ }
5640
+ if (dashboardError) {
5641
+ const message = dashboardError instanceof Error ? dashboardError.message : String(dashboardError);
5642
+ const next2 = await showExecutionResult(request, {
5643
+ ok: false,
5644
+ summary: message,
5645
+ stdout: "",
5646
+ stderr: message
5647
+ });
5648
+ if (next2 === "exit") {
5649
+ return;
5650
+ }
5651
+ }
5652
+ }
5653
+ continue;
5654
+ }
5655
+ const startedAt = Date.now();
5656
+ let loadingFrame = 0;
5657
+ paint(renderLoading(command.request, loadingFrame, startedAt));
5658
+ const loadingTicker = setInterval(() => {
5659
+ loadingFrame += 1;
5660
+ paint(renderLoading(command.request, loadingFrame, startedAt));
5661
+ }, LOADING_TICK_MS);
5662
+ let result;
5663
+ try {
5664
+ if (command.request.executionMode === "inherit") {
5665
+ clearInterval(loadingTicker);
5666
+ leaveAltScreen();
5667
+ try {
5668
+ ignoreSigint = true;
5669
+ result = await execute(command.request);
5670
+ } finally {
5671
+ ignoreSigint = false;
5672
+ enterAltScreen();
5673
+ }
5674
+ } else {
5675
+ result = await execute(command.request);
5676
+ }
5677
+ } finally {
5678
+ clearInterval(loadingTicker);
5679
+ }
5680
+ if (interrupted) {
5681
+ return;
5682
+ }
5683
+ const next = await showExecutionResult(command.request, result);
5684
+ if (next === "exit") {
5685
+ return;
5686
+ }
5687
+ }
5688
+ } finally {
5689
+ process.off("SIGINT", onSigint);
5690
+ suspendRawMode();
5691
+ leaveAltScreen();
3050
5692
  }
3051
- process.exit(1);
3052
5693
  }
3053
5694
 
3054
5695
  // packages/cli/src/sharing/clipboard.ts
@@ -3149,6 +5790,341 @@ async function uploadToGist(content, filename, description) {
3149
5790
  } catch {}
3150
5791
  }
3151
5792
  }
5793
+ // packages/cli/src/data-loader.ts
5794
+ async function loadTokenleakData(providers, range) {
5795
+ const results = await Promise.all(providers.map(async (p) => {
5796
+ try {
5797
+ return await p.load(range);
5798
+ } catch {
5799
+ return null;
5800
+ }
5801
+ }));
5802
+ const providerDataList = results.filter((r) => r !== null);
5803
+ if (providerDataList.length === 0) {
5804
+ throw new TokenleakError("No provider data found");
5805
+ }
5806
+ const mergedDaily = mergeProviderData(providerDataList);
5807
+ const stats = aggregate(mergedDaily, range.until);
5808
+ return {
5809
+ schemaVersion: SCHEMA_VERSION,
5810
+ generated: new Date().toISOString(),
5811
+ dateRange: range,
5812
+ providers: providerDataList,
5813
+ aggregated: stats,
5814
+ more: buildMoreStats(providerDataList, range)
5815
+ };
5816
+ }
5817
+
5818
+ // packages/cli/src/tabbed-dashboard.ts
5819
+ var HOME_CLEAR2 = "\x1B[H\x1B[J";
5820
+ var HIDE_CURSOR2 = "\x1B[?25l";
5821
+ var SHOW_CURSOR2 = "\x1B[?25h";
5822
+ var ALT_SCREEN_ON2 = "\x1B[?1049h";
5823
+ var ALT_SCREEN_OFF2 = "\x1B[?1049l";
5824
+ var ALT_SCROLL_ON2 = "\x1B[?1007h";
5825
+ var ALT_SCROLL_OFF2 = "\x1B[?1007l";
5826
+ var DIM2 = "\x1B[2m";
5827
+ var RESET3 = "\x1B[0m";
5828
+ var BOLD2 = "\x1B[1m";
5829
+ var YELLOW2 = "\x1B[33m";
5830
+ function timeRangeToDays(range) {
5831
+ switch (range) {
5832
+ case "7d":
5833
+ return 7;
5834
+ case "30d":
5835
+ return 30;
5836
+ case "90d":
5837
+ return 90;
5838
+ case "365d":
5839
+ return 365;
5840
+ default:
5841
+ return 30;
5842
+ }
5843
+ }
5844
+ function computeRange(range, baseUntil) {
5845
+ const until = baseUntil;
5846
+ const d = new Date(until);
5847
+ d.setDate(d.getDate() - timeRangeToDays(range));
5848
+ const since = d.toISOString().slice(0, 10);
5849
+ return { since, until };
5850
+ }
5851
+ function resolveRange(state, timeRange) {
5852
+ if (state.initialRange && timeRange === state.initialTimeRange) {
5853
+ return state.initialRange;
5854
+ }
5855
+ return computeRange(timeRange, state.baseUntil);
5856
+ }
5857
+ async function loadForRange(state, providers, timeRange) {
5858
+ const cached = state.dataCache.get(timeRange);
5859
+ if (cached)
5860
+ return cached;
5861
+ const inflight = state.inflightLoads.get(timeRange);
5862
+ if (inflight)
5863
+ return inflight;
5864
+ const range = resolveRange(state, timeRange);
5865
+ const loadPromise = loadTokenleakData(providers, range).then((output) => {
5866
+ state.dataCache.set(timeRange, output);
5867
+ return output;
5868
+ }).finally(() => {
5869
+ state.inflightLoads.delete(timeRange);
5870
+ });
5871
+ state.inflightLoads.set(timeRange, loadPromise);
5872
+ return loadPromise;
5873
+ }
5874
+ function getRenderWidth(state) {
5875
+ const terminalWidth = Math.max(40, (process.stdout.columns ?? 80) - 1);
5876
+ if (state.width === null) {
5877
+ return terminalWidth;
5878
+ }
5879
+ return Math.max(40, Math.min(terminalWidth, state.width));
5880
+ }
5881
+ function getViewportHeight(state, width, rows) {
5882
+ const headerLines = renderTabBar(state.timeRange, state.metricTab, width, state.noColor).split(`
5883
+ `).length + 2;
5884
+ const footerLines = 1;
5885
+ return Math.max(4, rows - headerLines - footerLines - 1);
5886
+ }
5887
+ function renderActiveView(output, tab, width, noColor2, noInsights) {
5888
+ const options = {
5889
+ format: "terminal",
5890
+ theme: "dark",
5891
+ width,
5892
+ showInsights: !noInsights,
5893
+ noColor: noColor2,
5894
+ output: null,
5895
+ more: true
5896
+ };
5897
+ switch (tab) {
5898
+ case "overview":
5899
+ return renderOverviewView(output, options);
5900
+ case "sess":
5901
+ return renderSessionView(output, width, noColor2);
5902
+ case "tok":
5903
+ return renderTokenView(output, width, noColor2);
5904
+ case "model":
5905
+ return renderModelView(output, width, noColor2);
5906
+ case "cwd":
5907
+ return renderCwdView(output, width, noColor2);
5908
+ case "dow":
5909
+ return renderDowView(output, width, noColor2);
5910
+ case "tod":
5911
+ return renderTodView(output, width, noColor2);
5912
+ default:
5913
+ return renderOverviewView(output, options);
5914
+ }
5915
+ }
5916
+ function renderScreen(output, state) {
5917
+ const width = getRenderWidth(state);
5918
+ const rows = process.stdout.rows ?? 40;
5919
+ const tabBar = renderTabBar(state.timeRange, state.metricTab, width, state.noColor);
5920
+ const tabBarLines = tabBar.split(`
5921
+ `);
5922
+ const rangeLabel = state.noColor ? ` ${output.dateRange.since} \u2192 ${output.dateRange.until}` : ` ${DIM2}${output.dateRange.since} \u2192 ${output.dateRange.until}${RESET3}`;
5923
+ const headerLines = [...tabBarLines, rangeLabel, ""];
5924
+ const footerLines = [""];
5925
+ const viewportHeight = getViewportHeight(state, width, rows);
5926
+ const viewContent = renderActiveView(output, state.metricTab, width, state.noColor, state.noInsights);
5927
+ const contentLines = viewContent.split(`
5928
+ `);
5929
+ const effectiveOffset = clampScrollOffset(state.scrollOffset, contentLines.length, viewportHeight);
5930
+ state.scrollOffset = effectiveOffset;
5931
+ const visibleContent = contentLines.slice(effectiveOffset, effectiveOffset + viewportHeight);
5932
+ const padding = Array.from({ length: Math.max(0, viewportHeight - visibleContent.length) }, () => "");
5933
+ const scrollInfo = contentLines.length > viewportHeight ? state.noColor ? ` Lines ${effectiveOffset + 1}-${Math.min(contentLines.length, effectiveOffset + viewportHeight)} of ${contentLines.length}` : ` ${DIM2}Lines ${effectiveOffset + 1}-${Math.min(contentLines.length, effectiveOffset + viewportHeight)} of ${contentLines.length}${RESET3}` : "";
5934
+ return `${HOME_CLEAR2}${HIDE_CURSOR2}${[
5935
+ ...headerLines,
5936
+ ...visibleContent,
5937
+ ...padding,
5938
+ scrollInfo,
5939
+ ...footerLines
5940
+ ].join(`
5941
+ `)}`;
5942
+ }
5943
+ function renderLoading2(state) {
5944
+ const width = getRenderWidth(state);
5945
+ const tabBar = renderTabBar(state.timeRange, state.metricTab, width, state.noColor);
5946
+ const loading = state.noColor ? " Loading data..." : ` ${YELLOW2}${BOLD2}Loading data...${RESET3}`;
5947
+ return `${HOME_CLEAR2}${HIDE_CURSOR2}${tabBar}
5948
+
5949
+ ${loading}`;
5950
+ }
5951
+ function enterAltScreen2() {
5952
+ process.stdout.write(`${ALT_SCREEN_ON2}${ALT_SCROLL_ON2}${HOME_CLEAR2}${HIDE_CURSOR2}`);
5953
+ }
5954
+ function leaveAltScreen2() {
5955
+ process.stdout.write(`${SHOW_CURSOR2}${ALT_SCROLL_OFF2}${ALT_SCREEN_OFF2}`);
5956
+ }
5957
+ function paint2(content) {
5958
+ process.stdout.write(content);
5959
+ }
5960
+ function suspendRawMode2() {
5961
+ if (process.stdin.isTTY) {
5962
+ process.stdin.setRawMode(false);
5963
+ }
5964
+ process.stdin.pause();
5965
+ process.stdout.write(SHOW_CURSOR2);
5966
+ }
5967
+ function resumeRawMode2() {
5968
+ if (process.stdin.isTTY) {
5969
+ process.stdin.setRawMode(true);
5970
+ }
5971
+ process.stdin.resume();
5972
+ process.stdout.write(HIDE_CURSOR2);
5973
+ }
5974
+ async function startTabbedDashboard(providers, options) {
5975
+ const state = {
5976
+ timeRange: options.initialTimeRange ?? "30d",
5977
+ initialTimeRange: options.initialTimeRange ?? "30d",
5978
+ metricTab: "overview",
5979
+ scrollOffset: 0,
5980
+ dataCache: new Map,
5981
+ inflightLoads: new Map,
5982
+ noColor: options.noColor,
5983
+ noInsights: options.noInsights ?? false,
5984
+ baseUntil: options.until ?? new Date().toISOString().slice(0, 10),
5985
+ initialRange: options.initialRange ?? null,
5986
+ width: options.width ?? null
5987
+ };
5988
+ enterAltScreen2();
5989
+ let currentOutput = null;
5990
+ let activeLoadId = 0;
5991
+ let shouldClose = false;
5992
+ const loadAndRender = async (timeRange) => {
5993
+ const loadId = ++activeLoadId;
5994
+ paint2(renderLoading2(state));
5995
+ let output;
5996
+ try {
5997
+ output = await loadForRange(state, providers, timeRange);
5998
+ } catch (error) {
5999
+ if (shouldClose || loadId !== activeLoadId || state.timeRange !== timeRange) {
6000
+ return;
6001
+ }
6002
+ throw error;
6003
+ }
6004
+ if (shouldClose || loadId !== activeLoadId || state.timeRange !== timeRange) {
6005
+ return;
6006
+ }
6007
+ currentOutput = output;
6008
+ paint2(renderScreen(currentOutput, state));
6009
+ };
6010
+ const rerender = () => {
6011
+ if (currentOutput) {
6012
+ paint2(renderScreen(currentOutput, state));
6013
+ }
6014
+ };
6015
+ const onResize = () => {
6016
+ rerender();
6017
+ };
6018
+ process.stdout.on("resize", onResize);
6019
+ try {
6020
+ await loadAndRender(state.timeRange);
6021
+ let fatalError = null;
6022
+ await new Promise((resolve) => {
6023
+ const settleFailure = (error) => {
6024
+ fatalError = error;
6025
+ cleanup();
6026
+ resolve();
6027
+ };
6028
+ const runAsyncAction = (action) => {
6029
+ action().catch((error) => {
6030
+ settleFailure(error);
6031
+ });
6032
+ };
6033
+ const onKeypress = (_input, key) => {
6034
+ if (key.ctrl && key.name === "c") {
6035
+ cleanup();
6036
+ resolve();
6037
+ return;
6038
+ }
6039
+ if (key.name === "q" || key.name === "escape") {
6040
+ cleanup();
6041
+ resolve();
6042
+ return;
6043
+ }
6044
+ if (key.name === "left") {
6045
+ const idx = TIME_RANGES.indexOf(state.timeRange);
6046
+ const newIdx = (idx - 1 + TIME_RANGES.length) % TIME_RANGES.length;
6047
+ state.timeRange = TIME_RANGES[newIdx];
6048
+ state.scrollOffset = 0;
6049
+ runAsyncAction(() => loadAndRender(state.timeRange));
6050
+ return;
6051
+ }
6052
+ if (key.name === "right") {
6053
+ const idx = TIME_RANGES.indexOf(state.timeRange);
6054
+ const newIdx = (idx + 1) % TIME_RANGES.length;
6055
+ state.timeRange = TIME_RANGES[newIdx];
6056
+ state.scrollOffset = 0;
6057
+ runAsyncAction(() => loadAndRender(state.timeRange));
6058
+ return;
6059
+ }
6060
+ if (key.name === "tab") {
6061
+ const idx = METRIC_TABS.indexOf(state.metricTab);
6062
+ const newIdx = key.shift ? (idx - 1 + METRIC_TABS.length) % METRIC_TABS.length : (idx + 1) % METRIC_TABS.length;
6063
+ state.metricTab = METRIC_TABS[newIdx];
6064
+ state.scrollOffset = 0;
6065
+ rerender();
6066
+ return;
6067
+ }
6068
+ const digit = key.sequence?.match(/^[1-7]$/)?.[0];
6069
+ if (digit) {
6070
+ const tabIdx = Number(digit) - 1;
6071
+ if (tabIdx < METRIC_TABS.length) {
6072
+ state.metricTab = METRIC_TABS[tabIdx];
6073
+ state.scrollOffset = 0;
6074
+ rerender();
6075
+ }
6076
+ return;
6077
+ }
6078
+ const rows = process.stdout.rows ?? 40;
6079
+ const viewportHeight = getViewportHeight(state, getRenderWidth(state), rows);
6080
+ if (key.name === "up") {
6081
+ state.scrollOffset = Math.max(0, state.scrollOffset - 1);
6082
+ rerender();
6083
+ return;
6084
+ }
6085
+ if (key.name === "down") {
6086
+ state.scrollOffset += 1;
6087
+ rerender();
6088
+ return;
6089
+ }
6090
+ if (key.name === "pageup") {
6091
+ state.scrollOffset = Math.max(0, state.scrollOffset - viewportHeight);
6092
+ rerender();
6093
+ return;
6094
+ }
6095
+ if (key.name === "pagedown") {
6096
+ state.scrollOffset += viewportHeight;
6097
+ rerender();
6098
+ return;
6099
+ }
6100
+ if (key.name === "home") {
6101
+ state.scrollOffset = 0;
6102
+ rerender();
6103
+ return;
6104
+ }
6105
+ if (key.name === "end") {
6106
+ state.scrollOffset = Number.MAX_SAFE_INTEGER;
6107
+ rerender();
6108
+ return;
6109
+ }
6110
+ };
6111
+ function cleanup() {
6112
+ shouldClose = true;
6113
+ process.stdin.off("keypress", onKeypress);
6114
+ suspendRawMode2();
6115
+ }
6116
+ resumeRawMode2();
6117
+ process.stdin.on("keypress", onKeypress);
6118
+ });
6119
+ if (fatalError) {
6120
+ throw fatalError;
6121
+ }
6122
+ } finally {
6123
+ process.stdout.off("resize", onResize);
6124
+ leaveAltScreen2();
6125
+ }
6126
+ }
6127
+
3152
6128
  // packages/cli/src/cli.ts
3153
6129
  var FORMAT_VALUES = ["json", "svg", "png", "terminal"];
3154
6130
  var THEME_VALUES = ["dark", "light"];
@@ -3208,6 +6184,7 @@ function buildHelpText() {
3208
6184
  return [
3209
6185
  `tokenleak ${VERSION}`,
3210
6186
  "Visualize AI coding assistant token usage across providers.",
6187
+ "Running `tokenleak` with no flags opens an interactive launcher in a TTY.",
3211
6188
  "",
3212
6189
  "Usage:",
3213
6190
  " tokenleak [flags]",
@@ -3229,6 +6206,7 @@ function buildHelpText() {
3229
6206
  " -w, --width <number> Terminal render width",
3230
6207
  " -p, --provider <list> Provider filter list, comma-separated",
3231
6208
  " --compare <range> Compare against YYYY-MM-DD..YYYY-MM-DD or auto",
6209
+ " --more Add expanded PNG/SVG stats and unlock compare cards",
3232
6210
  " --clipboard Copy rendered output to the clipboard",
3233
6211
  " --open Open the generated output file",
3234
6212
  " --upload <target> Upload rendered output, currently: gist",
@@ -3272,6 +6250,81 @@ function normalizeCliArg(arg) {
3272
6250
  };
3273
6251
  return flagMap[arg] ?? arg;
3274
6252
  }
6253
+ function buildInteractiveSummary(cliArgs, ok, exitCode) {
6254
+ if (!ok) {
6255
+ return `Command exited with code ${exitCode}.`;
6256
+ }
6257
+ if (typeof cliArgs["output"] === "string") {
6258
+ const outputPath = cliArgs["output"];
6259
+ const format2 = String(cliArgs["format"] ?? inferFormatFromPath(outputPath) ?? "output").toUpperCase();
6260
+ return `${format2} written to ${outputPath}.`;
6261
+ }
6262
+ if (cliArgs["listProviders"]) {
6263
+ return "Provider registry loaded.";
6264
+ }
6265
+ if (cliArgs["liveServer"]) {
6266
+ return "Live dashboard stopped.";
6267
+ }
6268
+ if (cliArgs["compare"]) {
6269
+ return "Compare report generated.";
6270
+ }
6271
+ const format = String(cliArgs["format"] ?? "terminal");
6272
+ if (format === "terminal") {
6273
+ return "Terminal dashboard generated.";
6274
+ }
6275
+ return `${format.toUpperCase()} command finished successfully.`;
6276
+ }
6277
+ async function executeInteractiveCommand(request) {
6278
+ try {
6279
+ const cliPath = process.argv[1];
6280
+ if (!cliPath) {
6281
+ return {
6282
+ ok: false,
6283
+ summary: "Could not resolve the current tokenleak entrypoint.",
6284
+ stdout: "",
6285
+ stderr: "Error: process.argv[1] is missing."
6286
+ };
6287
+ }
6288
+ const command = [process.execPath, cliPath, ...buildCliArgTokens(request.args)];
6289
+ if (request.executionMode === "inherit") {
6290
+ const proc2 = Bun.spawn(command, {
6291
+ stdin: "inherit",
6292
+ stdout: "inherit",
6293
+ stderr: "inherit"
6294
+ });
6295
+ const exitCode2 = await proc2.exited;
6296
+ return {
6297
+ ok: exitCode2 === 0,
6298
+ summary: buildInteractiveSummary(request.args, exitCode2 === 0, exitCode2),
6299
+ stdout: "",
6300
+ stderr: ""
6301
+ };
6302
+ }
6303
+ const proc = Bun.spawn(command, {
6304
+ stdin: "ignore",
6305
+ stdout: "pipe",
6306
+ stderr: "pipe"
6307
+ });
6308
+ const [exitCode, stdout, stderr] = await Promise.all([
6309
+ proc.exited,
6310
+ new Response(proc.stdout).text(),
6311
+ new Response(proc.stderr).text()
6312
+ ]);
6313
+ return {
6314
+ ok: exitCode === 0,
6315
+ summary: buildInteractiveSummary(request.args, exitCode === 0, exitCode),
6316
+ stdout,
6317
+ stderr
6318
+ };
6319
+ } catch (error) {
6320
+ return {
6321
+ ok: false,
6322
+ summary: "Interactive command failed before it could finish.",
6323
+ stdout: "",
6324
+ stderr: error instanceof Error ? `Error: ${error.message}` : `Error: ${String(error)}`
6325
+ };
6326
+ }
6327
+ }
3275
6328
  function normalizeCliArgv(argv) {
3276
6329
  const normalized = argv.map(normalizeCliArg);
3277
6330
  const result = [];
@@ -3330,35 +6383,6 @@ function inferFormatFromPath(filePath) {
3330
6383
  return null;
3331
6384
  }
3332
6385
  }
3333
- var DATE_FORMAT = /^\d{4}-\d{2}-\d{2}$/;
3334
- function isValidDate(dateStr) {
3335
- if (!DATE_FORMAT.test(dateStr))
3336
- return false;
3337
- const d = new Date(dateStr + "T00:00:00Z");
3338
- return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === dateStr;
3339
- }
3340
- function computeDateRange(args) {
3341
- const until = args.until ?? new Date().toISOString().slice(0, 10);
3342
- if (args.until && !isValidDate(args.until)) {
3343
- throw new TokenleakError(`Invalid --until date: "${args.until}". Use YYYY-MM-DD format.`);
3344
- }
3345
- if (args.since && !isValidDate(args.since)) {
3346
- throw new TokenleakError(`Invalid --since date: "${args.since}". Use YYYY-MM-DD format.`);
3347
- }
3348
- let since;
3349
- if (args.since) {
3350
- since = args.since;
3351
- } else {
3352
- const daysBack = args.days ?? DEFAULT_DAYS;
3353
- const d = new Date(until);
3354
- d.setDate(d.getDate() - daysBack);
3355
- since = d.toISOString().slice(0, 10);
3356
- }
3357
- if (since > until) {
3358
- throw new TokenleakError(`--since (${since}) must not be after --until (${until}).`);
3359
- }
3360
- return { since, until };
3361
- }
3362
6386
  function resolveConfig(cliArgs) {
3363
6387
  const fileConfig = loadConfig();
3364
6388
  const envConfig = loadEnvOverrides();
@@ -3370,6 +6394,7 @@ function resolveConfig(cliArgs) {
3370
6394
  width: 80,
3371
6395
  noColor: false,
3372
6396
  noInsights: false,
6397
+ more: false,
3373
6398
  claude: false,
3374
6399
  codex: false,
3375
6400
  openCode: false,
@@ -3393,6 +6418,8 @@ function resolveConfig(cliArgs) {
3393
6418
  merged.noColor = fileConfig.noColor;
3394
6419
  if (fileConfig.noInsights !== undefined)
3395
6420
  merged.noInsights = fileConfig.noInsights;
6421
+ if (fileConfig.more !== undefined)
6422
+ merged.more = fileConfig.more;
3396
6423
  if (envConfig.format)
3397
6424
  merged.format = envConfig.format;
3398
6425
  if (envConfig.theme)
@@ -3434,6 +6461,9 @@ function resolveConfig(cliArgs) {
3434
6461
  if (cliArgs["noInsights"] !== undefined) {
3435
6462
  result.noInsights = cliArgs["noInsights"];
3436
6463
  }
6464
+ if (cliArgs["more"] !== undefined) {
6465
+ result.more = cliArgs["more"];
6466
+ }
3437
6467
  if (cliArgs["compare"] !== undefined) {
3438
6468
  result.compare = cliArgs["compare"];
3439
6469
  }
@@ -3511,7 +6541,11 @@ async function runCompare(compareStr, currentRange, _registry, available) {
3511
6541
  loadAndAggregate(currentRange, available),
3512
6542
  loadAndAggregate(previousRange, available)
3513
6543
  ]);
3514
- return buildCompareOutput({ range: currentRange, stats: currentResult.stats }, { range: previousRange, stats: previousResult.stats });
6544
+ return {
6545
+ compareOutput: buildCompareOutput({ range: currentRange, stats: currentResult.stats }, { range: previousRange, stats: previousResult.stats }),
6546
+ currentData: currentResult.data,
6547
+ previousData: previousResult.data
6548
+ };
3515
6549
  }
3516
6550
  async function run(cliArgs) {
3517
6551
  const config = resolveConfig(cliArgs);
@@ -3544,12 +6578,45 @@ async function run(cliArgs) {
3544
6578
  throw new TokenleakError("No provider data found");
3545
6579
  }
3546
6580
  if (config.compare) {
6581
+ const compareResult = await runCompare(config.compare, dateRange, registry, available);
6582
+ if (config.more && (config.format === "png" || config.format === "svg")) {
6583
+ const compareOutput = {
6584
+ schemaVersion: SCHEMA_VERSION,
6585
+ generated: new Date().toISOString(),
6586
+ dateRange,
6587
+ providers: compareResult.currentData,
6588
+ aggregated: compareResult.compareOutput.periodA.stats,
6589
+ more: buildMoreStats(compareResult.currentData, dateRange, {
6590
+ previousRange: compareResult.compareOutput.periodB.range,
6591
+ previousProviders: compareResult.previousData
6592
+ })
6593
+ };
6594
+ const renderer2 = getRenderer(config.format);
6595
+ const renderOptions2 = {
6596
+ format: config.format,
6597
+ theme: config.theme,
6598
+ width: config.width,
6599
+ showInsights: !config.noInsights,
6600
+ noColor: config.noColor,
6601
+ output: config.output,
6602
+ more: true
6603
+ };
6604
+ const rendered3 = await renderer2.render(compareOutput, renderOptions2);
6605
+ if (config.output) {
6606
+ const data = typeof rendered3 === "string" ? rendered3 : Buffer.from(rendered3);
6607
+ writeFileSync(config.output, data);
6608
+ } else {
6609
+ const text2 = typeof rendered3 === "string" ? rendered3 : rendered3.toString("utf-8");
6610
+ process.stdout.write(text2 + `
6611
+ `);
6612
+ }
6613
+ return;
6614
+ }
3547
6615
  if (config.format !== "json" && config.format !== "terminal") {
3548
6616
  process.stderr.write(`Warning: --compare only supports JSON output. Ignoring --format ${config.format}.
3549
6617
  `);
3550
6618
  }
3551
- const compareOutput = await runCompare(config.compare, dateRange, registry, available);
3552
- const rendered2 = JSON.stringify(compareOutput, null, 2);
6619
+ const rendered2 = JSON.stringify(compareResult.compareOutput, null, 2);
3553
6620
  if (config.output) {
3554
6621
  writeFileSync(config.output, rendered2);
3555
6622
  } else {
@@ -3576,7 +6643,8 @@ async function run(cliArgs) {
3576
6643
  generated: new Date().toISOString(),
3577
6644
  dateRange,
3578
6645
  providers: providerDataList,
3579
- aggregated: stats
6646
+ aggregated: stats,
6647
+ more: config.more ? buildMoreStats(providerDataList, dateRange) : null
3580
6648
  };
3581
6649
  if (config.liveServer) {
3582
6650
  const ignoredFlags = [];
@@ -3598,7 +6666,8 @@ async function run(cliArgs) {
3598
6666
  width: config.width,
3599
6667
  showInsights: !config.noInsights,
3600
6668
  noColor: config.noColor,
3601
- output: config.output
6669
+ output: config.output,
6670
+ more: config.more
3602
6671
  };
3603
6672
  const { port } = await startLiveServer(output, renderOptions2);
3604
6673
  await new Promise((resolve) => {
@@ -3621,7 +6690,8 @@ Shutting down server...
3621
6690
  width: config.width,
3622
6691
  showInsights: !config.noInsights,
3623
6692
  noColor: config.noColor,
3624
- output: config.output
6693
+ output: config.output,
6694
+ more: config.more
3625
6695
  };
3626
6696
  const rendered = await renderer.render(output, renderOptions);
3627
6697
  if (config.output) {
@@ -3710,6 +6780,11 @@ var main = defineCommand({
3710
6780
  description: "Hide insights panel",
3711
6781
  default: false
3712
6782
  },
6783
+ more: {
6784
+ type: "boolean",
6785
+ description: "Add expanded PNG/SVG stats and compare cards",
6786
+ default: false
6787
+ },
3713
6788
  compare: {
3714
6789
  type: "string",
3715
6790
  description: "Compare two date ranges (YYYY-MM-DD..YYYY-MM-DD)"
@@ -3786,6 +6861,8 @@ var main = defineCommand({
3786
6861
  cliArgs["noColor"] = true;
3787
6862
  if (args.noInsights)
3788
6863
  cliArgs["noInsights"] = true;
6864
+ if (args.more)
6865
+ cliArgs["more"] = true;
3789
6866
  if (args.compare !== undefined)
3790
6867
  cliArgs["compare"] = args.compare;
3791
6868
  if (args.provider !== undefined)
@@ -3827,12 +6904,31 @@ if (isDirectExecution) {
3827
6904
  process.stdout.write(buildVersionText());
3828
6905
  process.exit(0);
3829
6906
  }
3830
- runMain(main);
6907
+ if (shouldStartInteractiveCli(argv, Boolean(process.stdin.isTTY), Boolean(process.stdout.isTTY))) {
6908
+ const registry = new ProviderRegistry;
6909
+ registerBuiltInProviders(registry);
6910
+ const available = await registry.getAvailable();
6911
+ const launchTabbed = async (opts) => {
6912
+ const requested = new Set(opts.providerNames ?? []);
6913
+ const scopedProviders = requested.size > 0 ? available.filter((provider) => providerMatchesFilter(provider, requested)) : available;
6914
+ if (scopedProviders.length === 0) {
6915
+ throw new TokenleakError("No provider data found");
6916
+ }
6917
+ await startTabbedDashboard(scopedProviders, opts);
6918
+ };
6919
+ await startInteractiveCli({
6920
+ version: VERSION,
6921
+ helpText: buildHelpText()
6922
+ }, executeInteractiveCommand, launchTabbed);
6923
+ } else {
6924
+ await runMain(main);
6925
+ }
3831
6926
  }
3832
6927
  export {
3833
6928
  run,
3834
6929
  resolveConfig,
3835
6930
  normalizeCliArgv,
3836
6931
  inferFormatFromPath,
3837
- computeDateRange
6932
+ computeDateRange,
6933
+ buildInteractiveSummary
3838
6934
  };