tokenleak 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +9 -14
  2. package/package.json +1 -1
  3. package/tokenleak +1884 -45
package/tokenleak CHANGED
@@ -755,6 +755,234 @@ function computePreviousPeriod(current) {
755
755
  until: formatDateStringUtc(prevUntil)
756
756
  };
757
757
  }
758
+ // packages/core/dist/aggregation/more.js
759
+ function daysInMonth(dateString) {
760
+ const [year, month] = dateString.split("-").map(Number);
761
+ if (!year || !month) {
762
+ return 30;
763
+ }
764
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
765
+ }
766
+ function buildInputOutput(providers) {
767
+ let inputTokens = 0;
768
+ let outputTokens = 0;
769
+ for (const provider of providers) {
770
+ for (const day of provider.daily) {
771
+ inputTokens += day.inputTokens;
772
+ outputTokens += day.outputTokens;
773
+ }
774
+ }
775
+ const nonCacheTokens = inputTokens + outputTokens;
776
+ return {
777
+ inputPerOutput: outputTokens > 0 ? inputTokens / outputTokens : null,
778
+ outputPerInput: inputTokens > 0 ? outputTokens / inputTokens : null,
779
+ outputShare: nonCacheTokens > 0 ? outputTokens / nonCacheTokens : 0
780
+ };
781
+ }
782
+ function buildMonthlyBurn(providers, range) {
783
+ const monthPrefix = range.until.slice(0, 7);
784
+ const monthStart = `${monthPrefix}-01`;
785
+ const observedSince = range.since > monthStart ? range.since : monthStart;
786
+ const observedDays = Math.max(1, Math.round((Date.parse(`${range.until}T00:00:00Z`) - Date.parse(`${observedSince}T00:00:00Z`)) / 86400000) + 1);
787
+ let observedTokens = 0;
788
+ let observedCost = 0;
789
+ for (const provider of providers) {
790
+ for (const day of provider.daily) {
791
+ if (day.date >= observedSince && day.date <= range.until) {
792
+ observedTokens += day.totalTokens;
793
+ observedCost += day.cost;
794
+ }
795
+ }
796
+ }
797
+ const calendarDays = daysInMonth(range.until);
798
+ const tokensPerDay = observedTokens / observedDays;
799
+ const costPerDay = observedCost / observedDays;
800
+ return {
801
+ projectedTokens: tokensPerDay * calendarDays,
802
+ projectedCost: costPerDay * calendarDays,
803
+ observedDays,
804
+ calendarDays
805
+ };
806
+ }
807
+ function buildCacheEconomics(providers) {
808
+ let readTokens = 0;
809
+ let writeTokens = 0;
810
+ let inputTokens = 0;
811
+ for (const provider of providers) {
812
+ for (const day of provider.daily) {
813
+ readTokens += day.cacheReadTokens;
814
+ writeTokens += day.cacheWriteTokens;
815
+ inputTokens += day.inputTokens;
816
+ }
817
+ }
818
+ const readCoverage = readTokens + inputTokens > 0 ? readTokens / (readTokens + inputTokens) : 0;
819
+ return {
820
+ readTokens,
821
+ writeTokens,
822
+ readCoverage,
823
+ reuseRatio: writeTokens > 0 ? readTokens / writeTokens : null
824
+ };
825
+ }
826
+ function collectEvents(providers) {
827
+ return providers.flatMap((provider) => provider.events ?? []);
828
+ }
829
+ function buildHourOfDay(events) {
830
+ const buckets = Array.from({ length: 24 }, (_, hour) => ({
831
+ hour,
832
+ tokens: 0,
833
+ cost: 0,
834
+ count: 0
835
+ }));
836
+ for (const event of events) {
837
+ const date = new Date(event.timestamp);
838
+ if (Number.isNaN(date.getTime())) {
839
+ continue;
840
+ }
841
+ const bucket = buckets[date.getUTCHours()];
842
+ if (!bucket) {
843
+ continue;
844
+ }
845
+ bucket.tokens += event.totalTokens;
846
+ bucket.cost += event.cost;
847
+ bucket.count += 1;
848
+ }
849
+ return buckets;
850
+ }
851
+ function buildSessionMetrics(events) {
852
+ const sessions = new Map;
853
+ const projects = new Map;
854
+ for (const event of events) {
855
+ const key = event.sessionId?.trim() || `${event.provider}:${event.timestamp}`;
856
+ const timestamp = Date.parse(event.timestamp);
857
+ const safeTime = Number.isFinite(timestamp) ? timestamp : 0;
858
+ let session = sessions.get(key);
859
+ if (!session) {
860
+ session = {
861
+ label: event.projectId?.trim() || event.sessionId?.trim() || key,
862
+ tokens: 0,
863
+ cost: 0,
864
+ count: 0,
865
+ projectId: event.projectId?.trim() || undefined,
866
+ firstTimestamp: safeTime,
867
+ lastTimestamp: safeTime,
868
+ explicitDurationMs: 0,
869
+ hasExplicitDuration: false
870
+ };
871
+ sessions.set(key, session);
872
+ }
873
+ session.tokens += event.totalTokens;
874
+ session.cost += event.cost;
875
+ session.count += 1;
876
+ session.firstTimestamp = Math.min(session.firstTimestamp, safeTime);
877
+ session.lastTimestamp = Math.max(session.lastTimestamp, safeTime);
878
+ if (typeof event.durationMs === "number" && Number.isFinite(event.durationMs)) {
879
+ session.explicitDurationMs += Math.max(0, event.durationMs);
880
+ session.hasExplicitDuration = true;
881
+ }
882
+ if (event.projectId?.trim()) {
883
+ projects.set(event.projectId, (projects.get(event.projectId) ?? 0) + event.totalTokens);
884
+ }
885
+ }
886
+ const sessionEntries = [...sessions.values()];
887
+ const totalSessions = sessionEntries.length;
888
+ let totalTokens = 0;
889
+ let totalCost = 0;
890
+ let totalMessages = 0;
891
+ let durationTotal = 0;
892
+ let durationCount = 0;
893
+ let longestSession = null;
894
+ let longestSessionDuration = -1;
895
+ for (const session of sessionEntries) {
896
+ totalTokens += session.tokens;
897
+ totalCost += session.cost;
898
+ totalMessages += session.count;
899
+ const derivedDurationMs = session.hasExplicitDuration ? session.explicitDurationMs : session.lastTimestamp > session.firstTimestamp ? session.lastTimestamp - session.firstTimestamp : 0;
900
+ if (derivedDurationMs > 0) {
901
+ durationTotal += derivedDurationMs;
902
+ durationCount += 1;
903
+ }
904
+ if (derivedDurationMs > longestSessionDuration || derivedDurationMs === longestSessionDuration && (!longestSession || session.tokens > longestSession.tokens)) {
905
+ longestSessionDuration = derivedDurationMs;
906
+ longestSession = {
907
+ label: session.label,
908
+ tokens: session.tokens,
909
+ cost: session.cost,
910
+ count: session.count,
911
+ durationMs: derivedDurationMs > 0 ? derivedDurationMs : null
912
+ };
913
+ }
914
+ }
915
+ let topProject = null;
916
+ for (const [name, tokens] of projects) {
917
+ if (!topProject || tokens > topProject.tokens) {
918
+ topProject = { name, tokens };
919
+ }
920
+ }
921
+ return {
922
+ totalSessions,
923
+ averageTokens: totalSessions > 0 ? totalTokens / totalSessions : 0,
924
+ averageCost: totalSessions > 0 ? totalCost / totalSessions : 0,
925
+ averageMessages: totalSessions > 0 ? totalMessages / totalSessions : 0,
926
+ averageDurationMs: durationCount > 0 ? durationTotal / durationCount : null,
927
+ longestSession,
928
+ projectCount: projects.size,
929
+ topProject
930
+ };
931
+ }
932
+ function computeModelMixShift(currentProviders, previousProviders, limit = 5) {
933
+ const currentModelTokens = new Map;
934
+ const previousModelTokens = new Map;
935
+ let currentTotal = 0;
936
+ let previousTotal = 0;
937
+ for (const provider of currentProviders) {
938
+ for (const day of provider.daily) {
939
+ for (const model of day.models) {
940
+ currentModelTokens.set(model.model, (currentModelTokens.get(model.model) ?? 0) + model.totalTokens);
941
+ currentTotal += model.totalTokens;
942
+ }
943
+ }
944
+ }
945
+ for (const provider of previousProviders) {
946
+ for (const day of provider.daily) {
947
+ for (const model of day.models) {
948
+ previousModelTokens.set(model.model, (previousModelTokens.get(model.model) ?? 0) + model.totalTokens);
949
+ previousTotal += model.totalTokens;
950
+ }
951
+ }
952
+ }
953
+ const models = new Set([
954
+ ...currentModelTokens.keys(),
955
+ ...previousModelTokens.keys()
956
+ ]);
957
+ return [...models].map((model) => {
958
+ const currentTokens = currentModelTokens.get(model) ?? 0;
959
+ const previousTokens = previousModelTokens.get(model) ?? 0;
960
+ const currentShare = currentTotal > 0 ? currentTokens / currentTotal : 0;
961
+ const previousShare = previousTotal > 0 ? previousTokens / previousTotal : 0;
962
+ return {
963
+ model,
964
+ currentShare,
965
+ previousShare,
966
+ deltaShare: currentShare - previousShare,
967
+ currentTokens,
968
+ previousTokens
969
+ };
970
+ }).sort((a, b) => Math.abs(b.deltaShare) - Math.abs(a.deltaShare)).slice(0, limit);
971
+ }
972
+ function buildMoreStats(providers, range, compare = null) {
973
+ const events = collectEvents(providers);
974
+ return {
975
+ inputOutput: buildInputOutput(providers),
976
+ monthlyBurn: buildMonthlyBurn(providers, range),
977
+ cacheEconomics: buildCacheEconomics(providers),
978
+ hourOfDay: buildHourOfDay(events),
979
+ sessionMetrics: buildSessionMetrics(events),
980
+ compare: compare ? {
981
+ previousRange: compare.previousRange,
982
+ modelMixShift: computeModelMixShift(providers, compare.previousProviders)
983
+ } : null
984
+ };
985
+ }
758
986
  // packages/core/dist/index.js
759
987
  var VERSION = "1.0.2";
760
988
 
@@ -1022,7 +1250,7 @@ async function* splitJsonlRecords(filePath) {
1022
1250
  }
1023
1251
  // packages/registry/dist/providers/claude-code.js
1024
1252
  import { existsSync, readdirSync, statSync } from "fs";
1025
- import { join } from "path";
1253
+ import { dirname, join, relative, sep } from "path";
1026
1254
  import { homedir } from "os";
1027
1255
 
1028
1256
  // packages/registry/dist/utils.js
@@ -1101,6 +1329,7 @@ function extractUsage(record) {
1101
1329
  }
1102
1330
  return {
1103
1331
  date,
1332
+ timestamp,
1104
1333
  model,
1105
1334
  inputTokens,
1106
1335
  outputTokens,
@@ -1181,13 +1410,18 @@ class ClaudeCodeProvider {
1181
1410
  async load(range) {
1182
1411
  const files = collectJsonlFiles(this.baseDir);
1183
1412
  const allRecords = [];
1413
+ const allEvents = [];
1184
1414
  for (const file of files) {
1185
1415
  const latestRecordsByMessageId = new Map;
1186
1416
  const anonymousRecords = [];
1417
+ const relativeFile = relative(this.baseDir, file).split(sep).join("/");
1418
+ const projectId = relative(this.baseDir, dirname(file)).split(sep).join("/");
1187
1419
  try {
1188
1420
  for await (const record of splitJsonlRecords(file)) {
1189
1421
  const usage = extractUsage(record);
1190
1422
  if (usage !== null && isInRange(usage.date, range)) {
1423
+ usage.sessionId = relativeFile;
1424
+ usage.projectId = projectId;
1191
1425
  if (usage.messageId) {
1192
1426
  latestRecordsByMessageId.set(usage.messageId, usage);
1193
1427
  } else {
@@ -1201,6 +1435,24 @@ class ClaudeCodeProvider {
1201
1435
  allRecords.push(...latestRecordsByMessageId.values(), ...anonymousRecords);
1202
1436
  }
1203
1437
  const daily = buildDailyUsage(allRecords);
1438
+ for (const record of allRecords) {
1439
+ const normalizedModel = normalizeModelName(record.model);
1440
+ const cost = estimateCost(record.model, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens);
1441
+ allEvents.push({
1442
+ provider: this.name,
1443
+ timestamp: record.timestamp,
1444
+ date: record.date,
1445
+ model: normalizedModel,
1446
+ inputTokens: record.inputTokens,
1447
+ outputTokens: record.outputTokens,
1448
+ cacheReadTokens: record.cacheReadTokens,
1449
+ cacheWriteTokens: record.cacheWriteTokens,
1450
+ totalTokens: record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens,
1451
+ cost,
1452
+ sessionId: record.sessionId,
1453
+ projectId: record.projectId
1454
+ });
1455
+ }
1204
1456
  const totalTokens = daily.reduce((sum, d) => sum + d.totalTokens, 0);
1205
1457
  const totalCost = daily.reduce((sum, d) => sum + d.cost, 0);
1206
1458
  return {
@@ -1209,13 +1461,14 @@ class ClaudeCodeProvider {
1209
1461
  daily,
1210
1462
  totalTokens,
1211
1463
  totalCost,
1212
- colors: this.colors
1464
+ colors: this.colors,
1465
+ events: allEvents
1213
1466
  };
1214
1467
  }
1215
1468
  }
1216
1469
  // packages/registry/dist/providers/codex.js
1217
1470
  import { existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1218
- import { join as join2 } from "path";
1471
+ import { dirname as dirname2, join as join2, relative as relative2, sep as sep2 } from "path";
1219
1472
  import { homedir as homedir2 } from "os";
1220
1473
  var CODEX_COLORS = {
1221
1474
  primary: "#10a37f",
@@ -1373,6 +1626,7 @@ function parseTokenCountUsage(record, context) {
1373
1626
  const inputTokens = Math.max(0, usage.inputTokens - cacheReadTokens);
1374
1627
  return {
1375
1628
  date,
1629
+ timestamp,
1376
1630
  model: context.model,
1377
1631
  inputTokens,
1378
1632
  outputTokens: usage.outputTokens,
@@ -1403,6 +1657,7 @@ function parseUsageRecord(record, context) {
1403
1657
  }
1404
1658
  return {
1405
1659
  date,
1660
+ timestamp: legacyEvent.timestamp,
1406
1661
  model: compactModelDateSuffix(legacyEvent.model),
1407
1662
  inputTokens: legacyEvent.usage.input_tokens,
1408
1663
  outputTokens: legacyEvent.usage.output_tokens,
@@ -1429,11 +1684,14 @@ class CodexProvider {
1429
1684
  async load(range) {
1430
1685
  const dailyMap = new Map;
1431
1686
  const files = collectJsonlFiles2(this.sessionsDir);
1687
+ const events = [];
1432
1688
  for (const file of files) {
1433
1689
  const context = {
1434
1690
  model: "gpt-5",
1435
1691
  previousTotals: null
1436
1692
  };
1693
+ const relativeFile = relative2(this.sessionsDir, file).split(sep2).join("/");
1694
+ const projectDir = relative2(this.sessionsDir, dirname2(file)).split(sep2).join("/");
1437
1695
  try {
1438
1696
  for await (const record of splitJsonlRecords(file)) {
1439
1697
  const usage = parseUsageRecord(record, context);
@@ -1443,12 +1701,28 @@ class CodexProvider {
1443
1701
  if (!isInRange(usage.date, range)) {
1444
1702
  continue;
1445
1703
  }
1704
+ usage.sessionId = relativeFile;
1705
+ usage.projectId = projectDir === "." ? undefined : projectDir;
1446
1706
  const normalizedModel = normalizeModelName(compactModelDateSuffix(usage.model));
1447
1707
  const inputTokens = usage.inputTokens;
1448
1708
  const outputTokens = usage.outputTokens;
1449
1709
  const cacheReadTokens = usage.cacheReadTokens;
1450
1710
  const cacheWriteTokens = usage.cacheWriteTokens;
1451
1711
  const cost = estimateCost(normalizedModel, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
1712
+ events.push({
1713
+ provider: this.name,
1714
+ timestamp: usage.timestamp,
1715
+ date: usage.date,
1716
+ model: normalizedModel,
1717
+ inputTokens,
1718
+ outputTokens,
1719
+ cacheReadTokens,
1720
+ cacheWriteTokens,
1721
+ totalTokens: inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens,
1722
+ cost,
1723
+ sessionId: usage.sessionId,
1724
+ projectId: usage.projectId
1725
+ });
1452
1726
  if (!dailyMap.has(usage.date)) {
1453
1727
  dailyMap.set(usage.date, new Map);
1454
1728
  }
@@ -1503,7 +1777,8 @@ class CodexProvider {
1503
1777
  daily,
1504
1778
  totalTokens,
1505
1779
  totalCost,
1506
- colors: this.colors
1780
+ colors: this.colors,
1781
+ events
1507
1782
  };
1508
1783
  }
1509
1784
  }
@@ -1513,7 +1788,7 @@ import { join as join3 } from "path";
1513
1788
  import { homedir as homedir3 } from "os";
1514
1789
  import { Database } from "bun:sqlite";
1515
1790
  var PROVIDER_NAME = "open-code";
1516
- var DISPLAY_NAME = "Open Code";
1791
+ var DISPLAY_NAME = "OpenCode";
1517
1792
  var COLORS = {
1518
1793
  primary: "#6366f1",
1519
1794
  secondary: "#a78bfa",
@@ -1549,12 +1824,46 @@ function extractDate2(createdAt) {
1549
1824
  }
1550
1825
  return date.toISOString().slice(0, 10);
1551
1826
  }
1827
+ function toTimestampMillis(createdAt) {
1828
+ const timestamp = typeof createdAt === "number" ? createdAt : Number.isNaN(Number(createdAt)) ? Date.parse(createdAt) : Number(createdAt);
1829
+ if (!Number.isFinite(timestamp)) {
1830
+ return null;
1831
+ }
1832
+ const millis = Math.abs(timestamp) >= 1000000000000 ? timestamp : timestamp * 1000;
1833
+ return Number.isFinite(millis) ? millis : null;
1834
+ }
1835
+ function toIsoTimestamp(createdAt) {
1836
+ const millis = toTimestampMillis(createdAt);
1837
+ if (millis === null) {
1838
+ return null;
1839
+ }
1840
+ const date = new Date(millis);
1841
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
1842
+ }
1552
1843
  function getRecordCost(record) {
1553
1844
  if (typeof record.explicitCost === "number" && Number.isFinite(record.explicitCost)) {
1554
1845
  return record.explicitCost;
1555
1846
  }
1556
1847
  return estimateCost(record.model, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens);
1557
1848
  }
1849
+ function toUsageEvent(record) {
1850
+ const totalTokens = record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens;
1851
+ return {
1852
+ provider: PROVIDER_NAME,
1853
+ timestamp: record.timestamp,
1854
+ date: record.date,
1855
+ model: normalizeModelName(record.model),
1856
+ inputTokens: record.inputTokens,
1857
+ outputTokens: record.outputTokens,
1858
+ cacheReadTokens: record.cacheReadTokens,
1859
+ cacheWriteTokens: record.cacheWriteTokens,
1860
+ totalTokens,
1861
+ cost: getRecordCost(record),
1862
+ sessionId: record.sessionId,
1863
+ projectId: record.projectId,
1864
+ durationMs: record.durationMs
1865
+ };
1866
+ }
1558
1867
  function buildProviderData(records) {
1559
1868
  const byDate = new Map;
1560
1869
  for (const record of records) {
@@ -1614,7 +1923,8 @@ function buildProviderData(records) {
1614
1923
  daily,
1615
1924
  totalTokens,
1616
1925
  totalCost,
1617
- colors: COLORS
1926
+ colors: COLORS,
1927
+ events: records.map(toUsageEvent)
1618
1928
  };
1619
1929
  }
1620
1930
  function loadFromSqlite(dbPath, range) {
@@ -1629,18 +1939,21 @@ function loadFromSqlite(dbPath, range) {
1629
1939
  if (tables.length === 0) {
1630
1940
  return [];
1631
1941
  }
1632
- const rows = db.query("SELECT model, input_tokens, output_tokens, created_at FROM messages WHERE role = 'assistant'").all();
1942
+ const rows = db.query("SELECT model, session_id, input_tokens, output_tokens, created_at FROM messages WHERE role = 'assistant'").all();
1633
1943
  const records = [];
1634
1944
  for (const row of rows) {
1635
1945
  const date = extractDate2(row.created_at);
1636
- if (date && isInRange(date, range)) {
1946
+ const timestamp = toIsoTimestamp(row.created_at);
1947
+ if (date && timestamp && isInRange(date, range)) {
1637
1948
  records.push({
1638
1949
  date,
1950
+ timestamp,
1639
1951
  model: row.model,
1640
1952
  inputTokens: row.input_tokens,
1641
1953
  outputTokens: row.output_tokens,
1642
1954
  cacheReadTokens: 0,
1643
- cacheWriteTokens: 0
1955
+ cacheWriteTokens: 0,
1956
+ sessionId: row.session_id
1644
1957
  });
1645
1958
  }
1646
1959
  }
@@ -1666,14 +1979,17 @@ function loadFromLegacyJson(sessionsDir, range) {
1666
1979
  continue;
1667
1980
  }
1668
1981
  const date = extractDate2(msg.created_at);
1669
- if (date && isInRange(date, range)) {
1982
+ const timestamp = toIsoTimestamp(msg.created_at);
1983
+ if (date && timestamp && isInRange(date, range)) {
1670
1984
  records.push({
1671
1985
  date,
1986
+ timestamp,
1672
1987
  model: msg.model,
1673
1988
  inputTokens: msg.usage.input_tokens,
1674
1989
  outputTokens: msg.usage.output_tokens,
1675
1990
  cacheReadTokens: 0,
1676
- cacheWriteTokens: 0
1991
+ cacheWriteTokens: 0,
1992
+ sessionId: file
1677
1993
  });
1678
1994
  }
1679
1995
  }
@@ -1711,7 +2027,8 @@ function loadFromCurrentStorage(baseDir, range) {
1711
2027
  continue;
1712
2028
  }
1713
2029
  const date = extractDate2(createdAt);
1714
- if (!date || !isInRange(date, range)) {
2030
+ const timestamp = toIsoTimestamp(createdAt);
2031
+ if (!date || !timestamp || !isInRange(date, range)) {
1715
2032
  continue;
1716
2033
  }
1717
2034
  const inputTokens = typeof message.tokens?.input === "number" ? message.tokens.input : 0;
@@ -1720,13 +2037,21 @@ function loadFromCurrentStorage(baseDir, range) {
1720
2037
  const cacheWriteTokens = typeof message.tokens?.cache?.write === "number" ? message.tokens.cache.write : 0;
1721
2038
  const record = {
1722
2039
  date,
2040
+ timestamp,
1723
2041
  model,
1724
2042
  inputTokens,
1725
2043
  outputTokens,
1726
2044
  cacheReadTokens,
1727
2045
  cacheWriteTokens,
1728
- explicitCost: typeof message.cost === "number" ? message.cost : undefined
2046
+ explicitCost: typeof message.cost === "number" ? message.cost : undefined,
2047
+ sessionId: typeof message.sessionID === "string" && message.sessionID || sessionDir
1729
2048
  };
2049
+ const completedAt = message.time?.completed;
2050
+ const completedMs = typeof completedAt === "string" || typeof completedAt === "number" ? toTimestampMillis(completedAt) : null;
2051
+ const createdMs = Date.parse(record.timestamp);
2052
+ if (completedMs !== null && Number.isFinite(createdMs) && completedMs > createdMs) {
2053
+ record.durationMs = completedMs - createdMs;
2054
+ }
1730
2055
  const totalTokens = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
1731
2056
  if (totalTokens === 0 && !(typeof record.explicitCost === "number" && record.explicitCost > 0)) {
1732
2057
  continue;
@@ -1831,7 +2156,7 @@ var DOT_GAP = 8;
1831
2156
  var CELL_SIZE = 16;
1832
2157
  var CELL_GAP = 4;
1833
2158
  var STAT_GRID_COLS = 3;
1834
- var MODEL_BAR_HEIGHT = 8;
2159
+ var MODEL_BAR_HEIGHT = 11;
1835
2160
  var DAY_LABEL_WIDTH = 44;
1836
2161
  var MONTH_LABEL_HEIGHT = 24;
1837
2162
  var PROVIDER_SECTION_GAP = 36;
@@ -1841,24 +2166,27 @@ var MODEL_BAR_GAP = 36;
1841
2166
  var MODEL_PERCENT_WIDTH = 40;
1842
2167
 
1843
2168
  // packages/renderers/dist/png/terminal-card.js
1844
- var FONT_FAMILY = "'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', monospace";
2169
+ var FONT_FAMILY = "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif";
2170
+ var MONO_FONT_FAMILY = "'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', monospace";
1845
2171
  function getCardTheme(mode) {
1846
2172
  if (mode === "dark") {
1847
2173
  return {
1848
2174
  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)"
2175
+ fg: "#f0f0f0",
2176
+ muted: "#6b7280",
2177
+ labelFg: "#b0b8c4",
2178
+ border: "rgba(255,255,255,0.08)",
2179
+ accent: "#34d399",
2180
+ heatmapEmpty: "#1a1a22",
2181
+ barTrack: "#151520",
2182
+ titlebarBorder: "rgba(255,255,255,0.08)"
1856
2183
  };
1857
2184
  }
1858
2185
  return {
1859
2186
  bg: "#fafafa",
1860
2187
  fg: "#18181b",
1861
2188
  muted: "#a1a1aa",
2189
+ labelFg: "#71717a",
1862
2190
  border: "rgba(0,0,0,0.08)",
1863
2191
  accent: "#059669",
1864
2192
  heatmapEmpty: "#e4e4e7",
@@ -1870,7 +2198,7 @@ function buildHeatmapScale(colors, isDark) {
1870
2198
  const [startHex, endHex] = colors.gradient;
1871
2199
  const s = hexToRgb(startHex);
1872
2200
  const e = hexToRgb(endHex);
1873
- const opacities = isDark ? [0.15, 0.35, 0.6, 1] : [0.2, 0.4, 0.65, 1];
2201
+ const opacities = isDark ? [0.25, 0.5, 0.75, 1] : [0.2, 0.4, 0.65, 1];
1874
2202
  return [
1875
2203
  "transparent",
1876
2204
  ...opacities.map((t) => {
@@ -1957,6 +2285,41 @@ function formatPercentage(rate) {
1957
2285
  function formatStreak(n) {
1958
2286
  return `${n} day${n !== 1 ? "s" : ""}`;
1959
2287
  }
2288
+ function formatRatio(value, suffix = "x") {
2289
+ if (value === null || !Number.isFinite(value)) {
2290
+ return "n/a";
2291
+ }
2292
+ return `${value.toFixed(value >= 10 ? 1 : 2)}${suffix}`;
2293
+ }
2294
+ function formatPercentPoints(value) {
2295
+ const prefix = value >= 0 ? "+" : "";
2296
+ return `${prefix}${(value * 100).toFixed(1)}pp`;
2297
+ }
2298
+ function formatHour(hour) {
2299
+ return `${hour.toString().padStart(2, "0")}:00`;
2300
+ }
2301
+ function formatDuration(durationMs) {
2302
+ if (durationMs === null || durationMs === undefined || durationMs <= 0) {
2303
+ return "n/a";
2304
+ }
2305
+ const totalMinutes = Math.round(durationMs / 60000);
2306
+ if (totalMinutes < 60) {
2307
+ return `${totalMinutes}m`;
2308
+ }
2309
+ const hours = Math.floor(totalMinutes / 60);
2310
+ const minutes = totalMinutes % 60;
2311
+ return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
2312
+ }
2313
+ function truncateText(value, maxLength) {
2314
+ return value.length > maxLength ? `${value.slice(0, maxLength - 1)}\u2026` : value;
2315
+ }
2316
+ function formatSessionSummary(summary) {
2317
+ const duration = formatDuration(summary.durationMs);
2318
+ if (duration === "n/a") {
2319
+ return truncateText(summary.label, 20);
2320
+ }
2321
+ return truncateText(`${summary.label} \xB7 ${duration}`, 24);
2322
+ }
1960
2323
  function renderProviderHeatmap(daily, since, until, heatmapColors, emptyColor) {
1961
2324
  const tokenMap = new Map;
1962
2325
  for (const d of daily) {
@@ -2013,6 +2376,149 @@ function renderProviderHeatmap(daily, since, until, heatmapColors, emptyColor) {
2013
2376
  `);
2014
2377
  return { svg, gridWidth, height };
2015
2378
  }
2379
+ function renderSectionHeader(x, y, title, theme, cardAccent) {
2380
+ const parts = [];
2381
+ parts.push(`<rect x="${x}" y="${y - 8}" width="3" height="10" rx="1.5" fill="${escapeXml(cardAccent)}" opacity="0.6"/>`);
2382
+ parts.push(`<text x="${x + 12}" y="${y}" fill="${escapeXml(theme.labelFg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" letter-spacing="1.8">${escapeXml(title)}</text>`);
2383
+ return parts.join(`
2384
+ `);
2385
+ }
2386
+ function renderMetricCard(x, y, width, title, lines, theme, cardAccent) {
2387
+ const parts = [];
2388
+ const height = 38 + lines.length * 22;
2389
+ parts.push(`<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="10" fill="${escapeXml(theme.barTrack)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
2390
+ parts.push(`<text x="${x + 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="10" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" letter-spacing="1.6">${escapeXml(title)}</text>`);
2391
+ lines.forEach((line, index) => {
2392
+ const lineY = y + 48 + index * 22;
2393
+ parts.push(`<text x="${x + 18}" y="${lineY}" fill="${escapeXml(theme.fg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600">${escapeXml(line.label)}</text>`);
2394
+ parts.push(`<text x="${x + width - 18}" y="${lineY}" fill="${escapeXml(line.accent ? cardAccent : theme.fg)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" text-anchor="end">${escapeXml(line.value)}</text>`);
2395
+ });
2396
+ return parts.join(`
2397
+ `);
2398
+ }
2399
+ function buildProviderHourBuckets(providers) {
2400
+ return providers.map((p) => {
2401
+ const hours = new Array(24).fill(0);
2402
+ for (const event of p.events ?? []) {
2403
+ const date = new Date(event.timestamp);
2404
+ if (!Number.isNaN(date.getTime())) {
2405
+ const h = date.getUTCHours();
2406
+ hours[h] += event.totalTokens;
2407
+ }
2408
+ }
2409
+ return { provider: p.provider, color: p.colors.primary, hours };
2410
+ });
2411
+ }
2412
+ function renderHourOfDayChart(x, y, width, hourOfDay, theme, cardAccent, providers, isDark) {
2413
+ const chartHeight = 140;
2414
+ const innerHeight = 72;
2415
+ const baselineY = y + 92;
2416
+ const barAreaX = x + 18;
2417
+ const barAreaWidth = width - 36;
2418
+ const barGap = 4;
2419
+ const barWidth = (barAreaWidth - barGap * 23) / 24;
2420
+ const maxTokens = Math.max(...hourOfDay.map((entry) => entry.tokens), 0);
2421
+ const busiest = hourOfDay.reduce((best, entry) => best === null || entry.tokens > best.tokens ? entry : best, null);
2422
+ const isMulti = providers.length > 1;
2423
+ const providerBuckets = isMulti ? buildProviderHourBuckets(providers) : [];
2424
+ let legendSvg = "";
2425
+ if (isMulti && providerBuckets.length > 0) {
2426
+ const titleWidth = 105;
2427
+ let legendX = x + 18 + titleWidth + 12;
2428
+ const legendY = y + 22;
2429
+ for (const bucket of providerBuckets) {
2430
+ const displayName = providers.find((p) => p.provider === bucket.provider)?.displayName ?? bucket.provider;
2431
+ legendSvg += `<rect x="${legendX}" y="${legendY - 8}" width="8" height="8" rx="2" fill="${escapeXml(bucket.color)}" opacity="0.85"/>` + `<text x="${legendX + 12}" y="${legendY - 1}" fill="${escapeXml(theme.fg)}" font-size="9" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600">${escapeXml(displayName)}</text>`;
2432
+ legendX += 12 + displayName.length * 5.5 + 16;
2433
+ }
2434
+ }
2435
+ const bars = [
2436
+ `<rect x="${x}" y="${y}" width="${width}" height="${chartHeight}" rx="10" fill="${escapeXml(theme.barTrack)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`,
2437
+ `<text x="${x + 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="10" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" letter-spacing="1.6">HOUR OF DAY</text>`,
2438
+ legendSvg,
2439
+ `<text x="${x + width - 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="end">${escapeXml(busiest ? `${formatHour(busiest.hour)} peak` : "No session events")}</text>`,
2440
+ `<line x1="${barAreaX}" y1="${baselineY}" x2="${barAreaX + barAreaWidth}" y2="${baselineY}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`
2441
+ ];
2442
+ bars.push(`<defs><filter id="peakGlow" x="-50%" y="-50%" width="200%" height="200%">` + `<feGaussianBlur stdDeviation="4" result="blur"/>` + `<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>` + `</filter></defs>`);
2443
+ if (isMulti) {
2444
+ const provBaseOpacity = isDark ? "0.45" : "0.3";
2445
+ const provMidOpacity = isDark ? "0.85" : "0.75";
2446
+ for (let bi = 0;bi < providerBuckets.length; bi++) {
2447
+ const gradId = `hod-prov-${bi}`;
2448
+ bars.push(`<defs><linearGradient id="${escapeXml(gradId)}" x1="0%" y1="100%" x2="0%" y2="0%">` + `<stop offset="0%" stop-color="${escapeXml(providerBuckets[bi].color)}" stop-opacity="${provBaseOpacity}"/>` + `<stop offset="60%" stop-color="${escapeXml(providerBuckets[bi].color)}" stop-opacity="${provMidOpacity}"/>` + `<stop offset="100%" stop-color="${escapeXml(providerBuckets[bi].color)}" stop-opacity="1"/>` + `</linearGradient></defs>`);
2449
+ }
2450
+ hourOfDay.forEach((entry, index) => {
2451
+ if (entry.tokens <= 0)
2452
+ return;
2453
+ const totalRatio = entry.tokens / maxTokens;
2454
+ const totalBarHeight = Math.max(4, totalRatio * innerHeight);
2455
+ const colX = barAreaX + index * (barWidth + barGap);
2456
+ const isPeak = busiest !== null && entry.hour === busiest.hour;
2457
+ if (isPeak) {
2458
+ const topY2 = baselineY - totalBarHeight;
2459
+ bars.push(`<rect x="${colX - 2}" y="${topY2 - 2}" width="${barWidth + 4}" height="${totalBarHeight + 4}" rx="5" fill="${escapeXml(providerBuckets[0]?.color ?? cardAccent)}" opacity="0.12" filter="url(#peakGlow)"/>`);
2460
+ }
2461
+ const clipId = `hod-clip-${index}`;
2462
+ const topY = baselineY - totalBarHeight;
2463
+ bars.push(`<defs><clipPath id="${escapeXml(clipId)}">` + `<rect x="${colX}" y="${topY}" width="${barWidth}" height="${totalBarHeight}" rx="3"/>` + `</clipPath></defs>`);
2464
+ let offsetY = 0;
2465
+ for (let bi = 0;bi < providerBuckets.length; bi++) {
2466
+ const tokens = providerBuckets[bi].hours[index] ?? 0;
2467
+ if (tokens <= 0)
2468
+ continue;
2469
+ const segHeight = tokens / entry.tokens * totalBarHeight;
2470
+ const segY = baselineY - offsetY - segHeight;
2471
+ bars.push(`<rect x="${colX}" y="${segY}" width="${barWidth}" height="${segHeight}" fill="url(#hod-prov-${bi})" clip-path="url(#${escapeXml(clipId)})"/>`);
2472
+ offsetY += segHeight;
2473
+ }
2474
+ });
2475
+ } else {
2476
+ const hodGradId = "hod-bar-grad";
2477
+ const hodBaseOpacity = isDark ? "0.25" : "0.1";
2478
+ const hodMidOpacity = isDark ? "0.75" : "0.6";
2479
+ bars.push(`<defs><linearGradient id="${escapeXml(hodGradId)}" x1="0%" y1="100%" x2="0%" y2="0%">` + `<stop offset="0%" stop-color="${escapeXml(cardAccent)}" stop-opacity="${hodBaseOpacity}"/>` + `<stop offset="40%" stop-color="${escapeXml(cardAccent)}" stop-opacity="${hodMidOpacity}"/>` + `<stop offset="100%" stop-color="${escapeXml(cardAccent)}" stop-opacity="1"/>` + `</linearGradient></defs>`);
2480
+ hourOfDay.forEach((entry, index) => {
2481
+ const ratio = maxTokens > 0 ? entry.tokens / maxTokens : 0;
2482
+ const barHeight = maxTokens > 0 ? Math.max(4, ratio * innerHeight) : 4;
2483
+ const colX = barAreaX + index * (barWidth + barGap);
2484
+ const colY = baselineY - barHeight;
2485
+ const isPeak = busiest !== null && entry.hour === busiest.hour && entry.tokens > 0;
2486
+ if (isPeak) {
2487
+ bars.push(`<rect x="${colX - 2}" y="${colY - 2}" width="${barWidth + 4}" height="${barHeight + 4}" rx="5" fill="${escapeXml(cardAccent)}" opacity="0.15" filter="url(#peakGlow)"/>`);
2488
+ }
2489
+ bars.push(`<rect x="${colX}" y="${colY}" width="${barWidth}" height="${barHeight}" rx="3" fill="url(#${escapeXml(hodGradId)})" opacity="${0.35 + ratio * 0.65}"/>`);
2490
+ });
2491
+ }
2492
+ [0, 3, 6, 9, 12, 15, 18, 21].forEach((hour) => {
2493
+ const labelX = barAreaX + hour * (barWidth + barGap) + barWidth / 2;
2494
+ bars.push(`<text x="${labelX}" y="${y + 116}" fill="${escapeXml(theme.labelFg)}" font-size="9" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="middle">${escapeXml(hour.toString().padStart(2, "0"))}</text>`);
2495
+ });
2496
+ return {
2497
+ svg: bars.join(`
2498
+ `),
2499
+ height: chartHeight
2500
+ };
2501
+ }
2502
+ function renderModelMixShift(x, y, width, more, theme, cardAccent) {
2503
+ if (!more.compare || more.compare.modelMixShift.length === 0) {
2504
+ return { svg: "", height: 0 };
2505
+ }
2506
+ const rows = more.compare.modelMixShift.slice(0, 4);
2507
+ const height = 38 + rows.length * 24;
2508
+ const parts = [
2509
+ `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="10" fill="${escapeXml(theme.barTrack)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`,
2510
+ `<text x="${x + 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="10" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" letter-spacing="1.6">MODEL MIX SHIFT</text>`,
2511
+ `<text x="${x + width - 18}" y="${y + 22}" fill="${escapeXml(theme.labelFg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="end">${escapeXml(`${more.compare.previousRange.since} \u2192 ${more.compare.previousRange.until}`)}</text>`
2512
+ ];
2513
+ rows.forEach((row, index) => {
2514
+ const lineY = y + 48 + index * 24;
2515
+ parts.push(`<text x="${x + 18}" y="${lineY}" fill="${escapeXml(theme.fg)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600">${escapeXml(truncateText(row.model, 28))}</text>`);
2516
+ parts.push(`<text x="${x + width - 18}" y="${lineY}" fill="${escapeXml(row.deltaShare >= 0 ? cardAccent : "#f97316")}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" text-anchor="end">${escapeXml(formatPercentPoints(row.deltaShare))}</text>`);
2517
+ parts.push(`<text x="${x + width - 110}" y="${lineY}" fill="${escapeXml(theme.labelFg)}" font-size="11" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="end">${escapeXml(`${(row.previousShare * 100).toFixed(1)}% \u2192 ${(row.currentShare * 100).toFixed(1)}%`)}</text>`);
2518
+ });
2519
+ return { svg: parts.join(`
2520
+ `), height };
2521
+ }
2016
2522
  function renderTerminalCardSvg(output, options) {
2017
2523
  const theme = getCardTheme(options.theme);
2018
2524
  const isDark = options.theme === "dark";
@@ -2021,6 +2527,7 @@ function renderTerminalCardSvg(output, options) {
2021
2527
  const { since, until } = output.dateRange;
2022
2528
  const providers = output.providers;
2023
2529
  const cardAccent = providers.length === 1 ? providers[0]?.colors.primary ?? theme.accent : theme.accent;
2530
+ const barAccent = providers.length > 1 ? isDark ? "#c4d0e0" : "#000000" : cardAccent;
2024
2531
  const providerHeatmaps = providers.map((p) => {
2025
2532
  const heatmapColors = buildHeatmapScale(p.colors, isDark);
2026
2533
  return {
@@ -2050,20 +2557,20 @@ function renderTerminalCardSvg(output, options) {
2050
2557
  }
2051
2558
  sections.push(`<line x1="0" y1="${TITLEBAR_HEIGHT}" x2="${cardWidth}" y2="${TITLEBAR_HEIGHT}" stroke="${escapeXml(theme.titlebarBorder)}" stroke-width="1"/>`);
2052
2559
  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>`);
2560
+ sections.push(`<text x="${pad}" y="${y + 16}" font-size="15" font-family="${escapeXml(MONO_FONT_FAMILY)}" font-weight="500">` + `<tspan fill="${escapeXml(cardAccent)}">$</tspan>` + `<tspan fill="${escapeXml(theme.fg)}"> tokenleak</tspan>` + `<tspan fill="${escapeXml(cardAccent)}">_</tspan>` + `</text>`);
2054
2561
  y += 40;
2055
2562
  const dateRangeText = formatDateRange(since, until);
2056
2563
  sections.push(`<text x="${pad}" y="${y + 14}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600" letter-spacing="2">${escapeXml(dateRangeText)}</text>`);
2057
2564
  y += 40;
2058
2565
  for (let pi = 0;pi < providerHeatmaps.length; pi++) {
2059
2566
  const { provider, heatmap, heatmapColors } = providerHeatmaps[pi];
2060
- const provDotRadius = 5;
2567
+ const provDotRadius = 7;
2061
2568
  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>`);
2569
+ sections.push(`<circle cx="${pad + provDotRadius}" cy="${y + 10}" r="${provDotRadius}" fill="${escapeXml(provColor)}"/>`);
2570
+ sections.push(`<text x="${pad + provDotRadius * 2 + 12}" y="${y + 15}" fill="${escapeXml(theme.fg)}" font-size="17" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700">${escapeXml(provider.displayName)}</text>`);
2064
2571
  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;
2572
+ sections.push(`<text x="${cardWidth - pad}" y="${y + 15}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500" text-anchor="end">${escapeXml(summaryText)}</text>`);
2573
+ y += 32;
2067
2574
  const heatmapSvg = heatmap.svg.replace(/__MUTED__/g, escapeXml(theme.muted));
2068
2575
  sections.push(`<g transform="translate(${pad}, ${y})">`);
2069
2576
  sections.push(heatmapSvg);
@@ -2103,7 +2610,7 @@ function renderTerminalCardSvg(output, options) {
2103
2610
  const stat = row[i];
2104
2611
  const x = pad + i * statColWidth;
2105
2612
  sections.push(`<text x="${x}" y="${startY}" fill="${escapeXml(theme.muted)}" font-size="10" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600" letter-spacing="1.5">${escapeXml(stat.label)}</text>`);
2106
- const valueColor = stat.accent ? cardAccent : theme.fg;
2613
+ const valueColor = stat.accent && providers.length === 1 ? cardAccent : theme.fg;
2107
2614
  sections.push(`<text x="${x}" y="${startY + 28}" fill="${escapeXml(valueColor)}" font-size="22" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700">${escapeXml(stat.value)}</text>`);
2108
2615
  }
2109
2616
  }
@@ -2114,24 +2621,127 @@ function renderTerminalCardSvg(output, options) {
2114
2621
  y += 8;
2115
2622
  sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
2116
2623
  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>`);
2624
+ sections.push(renderSectionHeader(pad, y, "TOP MODELS", theme, cardAccent));
2118
2625
  y += 24;
2119
2626
  const topModels2 = stats.topModels.slice(0, 3);
2627
+ const rankWidth = 28;
2120
2628
  const modelNameWidth = MODEL_NAME_WIDTH;
2121
2629
  const barGap = MODEL_BAR_GAP;
2122
2630
  const percentX = cardWidth - pad;
2123
- const barX = pad + modelNameWidth;
2631
+ const barX = pad + rankWidth + modelNameWidth;
2124
2632
  const barMaxWidth = Math.max(48, percentX - barX - barGap);
2125
2633
  for (const [index, model] of topModels2.entries()) {
2126
2634
  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)}"/>`);
2635
+ sections.push(`<text x="${pad}" y="${y + MODEL_BAR_HEIGHT - 1}" fill="${escapeXml(cardAccent)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="700" opacity="0.7">${escapeXml(String(index + 1))}</text>`);
2636
+ sections.push(`<text x="${pad + rankWidth}" y="${y + MODEL_BAR_HEIGHT - 1}" fill="${escapeXml(theme.fg)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="500">${escapeXml(model.model)}</text>`);
2637
+ const trackColor = isDark ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.06)";
2638
+ sections.push(`<rect x="${barX}" y="${y}" width="${barMaxWidth}" height="${MODEL_BAR_HEIGHT}" rx="6" fill="${escapeXml(trackColor)}"/>`);
2129
2639
  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>`);
2640
+ sections.push(`<defs><linearGradient id="${escapeXml(gradId)}" x1="0%" y1="0%" x2="100%" y2="0%">` + `<stop offset="0%" stop-color="${escapeXml(barAccent)}" stop-opacity="${isDark ? "0.45" : "0.27"}"/>` + `<stop offset="100%" stop-color="${escapeXml(barAccent)}" stop-opacity="1"/>` + `</linearGradient></defs>`);
2641
+ sections.push(`<rect x="${barX}" y="${y}" width="${barWidth}" height="${MODEL_BAR_HEIGHT}" rx="6" fill="url(#${escapeXml(gradId)})"/>`);
2642
+ sections.push(`<text x="${percentX}" y="${y + MODEL_BAR_HEIGHT - 1}" fill="${escapeXml(theme.fg)}" font-size="12" font-family="${escapeXml(FONT_FAMILY)}" font-weight="600" text-anchor="end">${escapeXml(`${model.percentage.toFixed(0)}%`)}</text>`);
2133
2643
  y += 32;
2134
2644
  }
2645
+ if (options.more && output.more) {
2646
+ const more = output.more;
2647
+ const cardGap = 16;
2648
+ const detailCardWidth = (contentWidth - cardGap) / 2;
2649
+ y += 8;
2650
+ sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
2651
+ y += 28;
2652
+ sections.push(renderSectionHeader(pad, y, "MORE", theme, cardAccent));
2653
+ y += 24;
2654
+ const efficiencyLines = [
2655
+ {
2656
+ label: "Input / Output",
2657
+ value: more.inputOutput.inputPerOutput === null ? "n/a" : `${more.inputOutput.inputPerOutput.toFixed(2)} : 1`,
2658
+ accent: true
2659
+ },
2660
+ {
2661
+ label: "Output / Input",
2662
+ value: formatRatio(more.inputOutput.outputPerInput)
2663
+ },
2664
+ {
2665
+ label: "Output Share",
2666
+ value: formatPercentage(more.inputOutput.outputShare)
2667
+ }
2668
+ ];
2669
+ sections.push(renderMetricCard(pad, y, detailCardWidth, "INPUT / OUTPUT", efficiencyLines, theme, cardAccent));
2670
+ const burnLines = [
2671
+ {
2672
+ label: "Projected Cost",
2673
+ value: formatCost(more.monthlyBurn.projectedCost),
2674
+ accent: true
2675
+ },
2676
+ {
2677
+ label: "Projected Tokens",
2678
+ value: formatNumber(more.monthlyBurn.projectedTokens)
2679
+ },
2680
+ {
2681
+ label: "Based On",
2682
+ value: `${more.monthlyBurn.observedDays} / ${more.monthlyBurn.calendarDays} days`
2683
+ }
2684
+ ];
2685
+ sections.push(renderMetricCard(pad + detailCardWidth + cardGap, y, detailCardWidth, "PROJECTED MONTHLY BURN", burnLines, theme, cardAccent));
2686
+ y += 38 + Math.max(efficiencyLines.length, burnLines.length) * 22 + 16;
2687
+ const cacheLines = [
2688
+ {
2689
+ label: "Cache Reads",
2690
+ value: formatNumber(more.cacheEconomics.readTokens),
2691
+ accent: true
2692
+ },
2693
+ {
2694
+ label: "Cache Writes",
2695
+ value: formatNumber(more.cacheEconomics.writeTokens)
2696
+ },
2697
+ {
2698
+ label: "Read Coverage",
2699
+ value: formatPercentage(more.cacheEconomics.readCoverage)
2700
+ },
2701
+ {
2702
+ label: "Reuse Ratio",
2703
+ value: formatRatio(more.cacheEconomics.reuseRatio)
2704
+ }
2705
+ ];
2706
+ sections.push(renderMetricCard(pad, y, detailCardWidth, "CACHE ECONOMICS", cacheLines, theme, cardAccent));
2707
+ const sessionLines = [
2708
+ {
2709
+ label: "Sessions",
2710
+ value: String(more.sessionMetrics.totalSessions),
2711
+ accent: true
2712
+ },
2713
+ {
2714
+ label: "Avg Tokens",
2715
+ value: formatNumber(more.sessionMetrics.averageTokens)
2716
+ },
2717
+ {
2718
+ label: "Avg Messages",
2719
+ value: more.sessionMetrics.averageMessages.toFixed(1)
2720
+ },
2721
+ {
2722
+ label: "Avg Duration",
2723
+ value: formatDuration(more.sessionMetrics.averageDurationMs)
2724
+ },
2725
+ {
2726
+ label: "Longest Session",
2727
+ value: more.sessionMetrics.longestSession ? formatSessionSummary(more.sessionMetrics.longestSession) : "n/a"
2728
+ },
2729
+ {
2730
+ label: "Top Project",
2731
+ value: more.sessionMetrics.topProject ? truncateText(more.sessionMetrics.topProject.name, 20) : "n/a"
2732
+ }
2733
+ ];
2734
+ sections.push(renderMetricCard(pad + detailCardWidth + cardGap, y, detailCardWidth, "SESSION STATS", sessionLines, theme, cardAccent));
2735
+ y += 38 + Math.max(cacheLines.length, sessionLines.length) * 22 + 16;
2736
+ const hourChart = renderHourOfDayChart(pad, y, contentWidth, more.hourOfDay, theme, cardAccent, providers, isDark);
2737
+ sections.push(hourChart.svg);
2738
+ y += hourChart.height + 16;
2739
+ const mixShift = renderModelMixShift(pad, y, contentWidth, more, theme, cardAccent);
2740
+ if (mixShift.height > 0) {
2741
+ sections.push(mixShift.svg);
2742
+ y += mixShift.height + 12;
2743
+ }
2744
+ }
2135
2745
  y += pad * 0.5;
2136
2746
  const cardHeight = y;
2137
2747
  const svg = sections.join(`
@@ -3051,6 +3661,1097 @@ function handleError(error) {
3051
3661
  process.exit(1);
3052
3662
  }
3053
3663
 
3664
+ // packages/cli/src/flags.ts
3665
+ var CLI_FLAG_ORDER = [
3666
+ "format",
3667
+ "theme",
3668
+ "since",
3669
+ "until",
3670
+ "days",
3671
+ "output",
3672
+ "width",
3673
+ "provider",
3674
+ "compare",
3675
+ "upload",
3676
+ "claude",
3677
+ "codex",
3678
+ "openCode",
3679
+ "allProviders",
3680
+ "listProviders",
3681
+ "more",
3682
+ "clipboard",
3683
+ "open",
3684
+ "liveServer",
3685
+ "noColor",
3686
+ "noInsights"
3687
+ ];
3688
+ var CLI_FLAG_NAMES = {
3689
+ format: "--format",
3690
+ theme: "--theme",
3691
+ since: "--since",
3692
+ until: "--until",
3693
+ days: "--days",
3694
+ output: "--output",
3695
+ width: "--width",
3696
+ provider: "--provider",
3697
+ compare: "--compare",
3698
+ upload: "--upload",
3699
+ claude: "--claude",
3700
+ codex: "--codex",
3701
+ openCode: "--open-code",
3702
+ allProviders: "--all-providers",
3703
+ listProviders: "--list-providers",
3704
+ more: "--more",
3705
+ clipboard: "--clipboard",
3706
+ open: "--open",
3707
+ liveServer: "--live-server",
3708
+ noColor: "--no-color",
3709
+ noInsights: "--no-insights"
3710
+ };
3711
+ function buildCliArgTokens(cliArgs) {
3712
+ const tokens = [];
3713
+ for (const key of CLI_FLAG_ORDER) {
3714
+ const value = cliArgs[key];
3715
+ if (value === undefined || value === false || value === null) {
3716
+ continue;
3717
+ }
3718
+ const flag = CLI_FLAG_NAMES[key];
3719
+ if (!flag)
3720
+ continue;
3721
+ tokens.push(flag);
3722
+ if (value !== true) {
3723
+ tokens.push(String(value));
3724
+ }
3725
+ }
3726
+ return tokens;
3727
+ }
3728
+ function buildCliPreview(cliArgs) {
3729
+ const tokens = buildCliArgTokens(cliArgs);
3730
+ return tokens.length === 0 ? "tokenleak" : `tokenleak ${tokens.join(" ")}`;
3731
+ }
3732
+
3733
+ // packages/cli/src/interactive.ts
3734
+ import { emitKeypressEvents } from "readline";
3735
+ import { createInterface } from "readline/promises";
3736
+ var INTERACTIVE_FLAG_LINES = [
3737
+ "-f, --format <format> terminal | png | svg | json",
3738
+ "-t, --theme <theme> dark | light",
3739
+ "-s, --since <date> YYYY-MM-DD start date",
3740
+ "-u, --until <date> YYYY-MM-DD end date",
3741
+ "-d, --days <number> trailing days window",
3742
+ "-o, --output <path> write output to a file",
3743
+ "-w, --width <number> terminal render width",
3744
+ "-p, --provider <list> comma-separated providers",
3745
+ " --claude shortcut for Claude Code",
3746
+ " --codex shortcut for Codex",
3747
+ " --open-code shortcut for Open Code",
3748
+ " --all-providers ignore provider filters",
3749
+ " --list-providers show provider registry",
3750
+ " --compare <range> auto or YYYY-MM-DD..YYYY-MM-DD",
3751
+ " --more richer PNG/SVG stats",
3752
+ " --clipboard copy rendered output",
3753
+ " --open open generated file",
3754
+ " --upload <target> gist",
3755
+ "-L, --live-server local interactive dashboard",
3756
+ " --no-color disable ANSI colors",
3757
+ " --no-insights hide terminal insights",
3758
+ " --help print help",
3759
+ " --version print version"
3760
+ ];
3761
+ var ESC2 = "\x1B[";
3762
+ var RESET = `${ESC2}0m`;
3763
+ var BOLD = `${ESC2}1m`;
3764
+ var DIM = `${ESC2}2m`;
3765
+ var CYAN = `${ESC2}36m`;
3766
+ var GREEN = `${ESC2}32m`;
3767
+ var YELLOW = `${ESC2}33m`;
3768
+ var RED = `${ESC2}31m`;
3769
+ var WHITE = `${ESC2}97m`;
3770
+ var HOME_CLEAR = "\x1B[H\x1B[J";
3771
+ var HIDE_CURSOR = "\x1B[?25l";
3772
+ var SHOW_CURSOR = "\x1B[?25h";
3773
+ var ALT_SCREEN_ON = "\x1B[?1049h";
3774
+ var ALT_SCREEN_OFF = "\x1B[?1049l";
3775
+ var LOADING_TICK_MS = 120;
3776
+ function color(text2, code) {
3777
+ return `${code}${text2}${RESET}`;
3778
+ }
3779
+ function stripAnsi(text2) {
3780
+ return text2.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
3781
+ }
3782
+ function visibleLength(text2) {
3783
+ return stripAnsi(text2).length;
3784
+ }
3785
+ function padVisible(text2, width) {
3786
+ const padding = Math.max(0, width - visibleLength(text2));
3787
+ return text2 + " ".repeat(padding);
3788
+ }
3789
+ function truncateVisible(text2, width) {
3790
+ if (width <= 0)
3791
+ return "";
3792
+ const plain = stripAnsi(text2);
3793
+ if (plain.length <= width)
3794
+ return text2;
3795
+ const limit = width <= 3 ? width : width - 3;
3796
+ let visibleCount = 0;
3797
+ let index = 0;
3798
+ let result = "";
3799
+ let sawAnsi = false;
3800
+ while (index < text2.length && visibleCount < limit) {
3801
+ if (text2[index] === "\x1B") {
3802
+ const match = text2.slice(index).match(/^\x1b\[[0-9;?]*[A-Za-z]/);
3803
+ if (match) {
3804
+ result += match[0];
3805
+ index += match[0].length;
3806
+ sawAnsi = true;
3807
+ continue;
3808
+ }
3809
+ }
3810
+ result += text2[index];
3811
+ index += 1;
3812
+ visibleCount += 1;
3813
+ }
3814
+ if (width <= 3) {
3815
+ return sawAnsi ? `${result}${RESET}` : result;
3816
+ }
3817
+ return sawAnsi ? `${result}...${RESET}` : `${result}...`;
3818
+ }
3819
+ function joinColumns(left, right, totalWidth) {
3820
+ const gutter = 3;
3821
+ const leftWidth = Math.max(42, Math.min(58, Math.floor(totalWidth * 0.44)));
3822
+ const rightWidth = Math.max(36, totalWidth - leftWidth - gutter);
3823
+ const rows = Math.max(left.length, right.length);
3824
+ const lines = [];
3825
+ for (let index = 0;index < rows; index++) {
3826
+ const leftLine = truncateVisible(left[index] ?? "", leftWidth);
3827
+ const rightLine = truncateVisible(right[index] ?? "", rightWidth);
3828
+ lines.push(`${padVisible(leftLine, leftWidth)}${" ".repeat(gutter)}${rightLine}`);
3829
+ }
3830
+ return lines;
3831
+ }
3832
+ function renderRule(width) {
3833
+ return color("-".repeat(width), DIM);
3834
+ }
3835
+ function describeRequest(args) {
3836
+ const output = typeof args["output"] === "string" ? args["output"] : null;
3837
+ if (args["liveServer"]) {
3838
+ return {
3839
+ title: "Live Dashboard",
3840
+ loadingTitle: "Starting live dashboard",
3841
+ loadingDetail: "Launching the local server. Press Ctrl-C in the live view to stop it, then you will return here.",
3842
+ executionMode: "inherit"
3843
+ };
3844
+ }
3845
+ if (args["listProviders"]) {
3846
+ return {
3847
+ title: "Provider Registry",
3848
+ loadingTitle: "Loading provider registry",
3849
+ loadingDetail: "Checking registered providers and current availability.",
3850
+ executionMode: "capture"
3851
+ };
3852
+ }
3853
+ if (args["compare"]) {
3854
+ return {
3855
+ title: "Compare Report",
3856
+ loadingTitle: "Building compare report",
3857
+ loadingDetail: output ? `Computing period deltas and writing the report to ${output}.` : "Computing period deltas for the current and previous windows.",
3858
+ executionMode: "capture"
3859
+ };
3860
+ }
3861
+ switch (args["format"]) {
3862
+ case "json":
3863
+ return {
3864
+ title: "JSON Export",
3865
+ loadingTitle: "Generating JSON report",
3866
+ loadingDetail: output ? `Collecting token usage and writing JSON to ${output}.` : "Collecting token usage and building structured JSON output.",
3867
+ executionMode: "capture"
3868
+ };
3869
+ case "svg":
3870
+ return {
3871
+ title: "SVG Export",
3872
+ loadingTitle: "Rendering SVG",
3873
+ loadingDetail: output ? `Rendering a vector card and writing it to ${output}.` : "Rendering a vector card from your usage data.",
3874
+ executionMode: "capture"
3875
+ };
3876
+ case "png":
3877
+ return {
3878
+ title: "PNG Export",
3879
+ loadingTitle: "Rendering PNG",
3880
+ loadingDetail: output ? `Rendering the PNG card and writing it to ${output}. This can take a few seconds.` : "Rendering the PNG card. This can take a few seconds.",
3881
+ executionMode: "capture"
3882
+ };
3883
+ default:
3884
+ return {
3885
+ title: "Terminal Dashboard",
3886
+ loadingTitle: "Generating terminal dashboard",
3887
+ loadingDetail: "Reading provider logs and aggregating token usage.",
3888
+ executionMode: "capture"
3889
+ };
3890
+ }
3891
+ }
3892
+ function finalizeCliArgs(args) {
3893
+ const finalized = { ...args };
3894
+ const format = finalized["format"];
3895
+ if (finalized["compare"] && (format === "png" || format === "svg")) {
3896
+ finalized["more"] = true;
3897
+ }
3898
+ if (finalized["open"] && finalized["output"] === undefined && typeof format === "string") {
3899
+ if (format === "png" || format === "svg" || format === "json") {
3900
+ finalized["output"] = `tokenleak.${format}`;
3901
+ } else {
3902
+ delete finalized["open"];
3903
+ }
3904
+ }
3905
+ if (format === "png") {
3906
+ delete finalized["clipboard"];
3907
+ delete finalized["upload"];
3908
+ }
3909
+ return finalized;
3910
+ }
3911
+ function createRunCommand(args) {
3912
+ const finalizedArgs = finalizeCliArgs(args);
3913
+ return {
3914
+ type: "run",
3915
+ request: {
3916
+ args: finalizedArgs,
3917
+ preview: buildCliPreview(finalizedArgs),
3918
+ ...describeRequest(finalizedArgs)
3919
+ }
3920
+ };
3921
+ }
3922
+ function renderMenu(options, selectedIndex) {
3923
+ return options.map((option, index) => {
3924
+ const isSelected = index === selectedIndex;
3925
+ const prefix = isSelected ? color(">", GREEN) : " ";
3926
+ const number = isSelected ? color(option.digit, WHITE + BOLD) : color(option.digit, YELLOW);
3927
+ const title = isSelected ? color(option.title, WHITE + BOLD) : color(option.title, WHITE);
3928
+ const description = isSelected ? color(option.description, CYAN) : color(option.description, DIM);
3929
+ return `${prefix} [${number}] ${title} ${description}`;
3930
+ });
3931
+ }
3932
+ function renderFlagPanel() {
3933
+ return [
3934
+ color("All Flags", WHITE + BOLD),
3935
+ color("Every flag remains available while using the launcher.", DIM),
3936
+ "",
3937
+ ...INTERACTIVE_FLAG_LINES.map((line) => color(line, CYAN))
3938
+ ];
3939
+ }
3940
+ function renderMenuPanel(context, options, selectedIndex) {
3941
+ const selected = options[selectedIndex];
3942
+ return [
3943
+ color("Tokenleak Interactive Launcher", WHITE + BOLD),
3944
+ `${color(`v${context.version}`, YELLOW)} ${color("interactive command center", CYAN)}`,
3945
+ "",
3946
+ color("Arrow keys move. Number keys jump directly. Enter runs the selected action.", DIM),
3947
+ color("Commands run inside this session, so you can keep selecting without leaving tokenleak.", DIM),
3948
+ "",
3949
+ ...renderMenu(options, selectedIndex),
3950
+ "",
3951
+ color("Preview", WHITE + BOLD),
3952
+ color(selected.preview, GREEN),
3953
+ "",
3954
+ color("Keys", WHITE + BOLD),
3955
+ `${color("Up/Down", YELLOW)} move ${color("Enter", YELLOW)} run ${color("H", YELLOW)} help ${color("Q", YELLOW)} quit`,
3956
+ "",
3957
+ renderRule(44)
3958
+ ];
3959
+ }
3960
+ function renderHelpOverlay(helpText, width) {
3961
+ const lines = helpText.trimEnd().split(`
3962
+ `);
3963
+ const header = [
3964
+ color("Tokenleak Help", WHITE + BOLD),
3965
+ color("Press Enter, Escape, H, or Q to return to the launcher.", DIM),
3966
+ ""
3967
+ ];
3968
+ return `${HOME_CLEAR}${HIDE_CURSOR}${[...header, ...lines.map((line) => truncateVisible(line, width))].join(`
3969
+ `)}`;
3970
+ }
3971
+ function renderLauncher(context, options, selectedIndex) {
3972
+ const width = process.stdout.columns ?? 120;
3973
+ const menuPanel = renderMenuPanel(context, options, selectedIndex);
3974
+ const flagPanel = renderFlagPanel();
3975
+ const body = width >= 118 ? joinColumns(menuPanel, flagPanel, width) : [...menuPanel, "", ...flagPanel];
3976
+ return `${HOME_CLEAR}${HIDE_CURSOR}${body.join(`
3977
+ `)}`;
3978
+ }
3979
+ function renderProgressBar(frame, width) {
3980
+ const innerWidth = Math.max(12, width - 2);
3981
+ const headSize = Math.max(4, Math.floor(innerWidth / 5));
3982
+ const travel = Math.max(1, innerWidth - headSize);
3983
+ const cycle = travel * 2;
3984
+ const offset = frame % cycle;
3985
+ const start = offset <= travel ? offset : cycle - offset;
3986
+ const cells = Array.from({ length: innerWidth }, (_, index) => {
3987
+ if (index >= start && index < start + headSize) {
3988
+ return "=";
3989
+ }
3990
+ return "-";
3991
+ }).join("");
3992
+ return color(`[${cells}]`, CYAN);
3993
+ }
3994
+ function renderLoading(request, frame = 0, startedAt = Date.now()) {
3995
+ const elapsedSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1000));
3996
+ const progressBar = renderProgressBar(frame, 34);
3997
+ const lines = [
3998
+ color(request.loadingTitle, WHITE + BOLD),
3999
+ color(request.preview, CYAN),
4000
+ "",
4001
+ color(request.loadingDetail, DIM),
4002
+ "",
4003
+ color("Status", WHITE + BOLD),
4004
+ color("Working... stay in tokenleak while this finishes.", YELLOW),
4005
+ "",
4006
+ color("Progress", WHITE + BOLD),
4007
+ progressBar,
4008
+ color(`Elapsed ${elapsedSeconds}s`, DIM),
4009
+ "",
4010
+ renderRule(44)
4011
+ ];
4012
+ return `${HOME_CLEAR}${HIDE_CURSOR}${lines.join(`
4013
+ `)}`;
4014
+ }
4015
+ function clipOutputLines(lines, limit) {
4016
+ if (limit <= 0)
4017
+ return [];
4018
+ if (lines.length <= limit)
4019
+ return lines;
4020
+ const visible = lines.slice(0, Math.max(0, limit - 1));
4021
+ visible.push(color(`... ${lines.length - visible.length} more lines hidden`, DIM));
4022
+ return visible;
4023
+ }
4024
+ function renderOutputSection(title, content, width, maxLines) {
4025
+ const normalized = content.trimEnd();
4026
+ if (!normalized)
4027
+ return [];
4028
+ const lines = normalized.split(`
4029
+ `).map((line) => truncateVisible(line, width));
4030
+ return [
4031
+ color(title, WHITE + BOLD),
4032
+ ...clipOutputLines(lines, maxLines),
4033
+ ""
4034
+ ];
4035
+ }
4036
+ function renderResult(request, result) {
4037
+ const width = Math.max(60, (process.stdout.columns ?? 120) - 1);
4038
+ const rows = process.stdout.rows ?? 40;
4039
+ const statusColor = result.ok ? GREEN : RED;
4040
+ const statusLabel = result.ok ? "Completed" : "Failed";
4041
+ const fixedLines = 10;
4042
+ const outputBudget = Math.max(8, rows - fixedLines);
4043
+ const firstSectionLines = result.stdout.trim() && result.stderr.trim() ? Math.max(4, Math.floor(outputBudget * 0.6)) : outputBudget;
4044
+ const secondSectionLines = Math.max(4, outputBudget - firstSectionLines);
4045
+ const body = [
4046
+ color(request.title, WHITE + BOLD),
4047
+ color(request.preview, CYAN),
4048
+ "",
4049
+ `${color("Status", WHITE + BOLD)} ${color(statusLabel, statusColor)}`,
4050
+ color(result.summary, DIM),
4051
+ "",
4052
+ ...renderOutputSection("Output", result.stdout, width, firstSectionLines),
4053
+ ...renderOutputSection("Messages", result.stderr, width, secondSectionLines),
4054
+ renderRule(44),
4055
+ `${color("Enter", YELLOW)} launcher ${color("Q", YELLOW)} quit`
4056
+ ];
4057
+ return `${HOME_CLEAR}${HIDE_CURSOR}${body.join(`
4058
+ `)}`;
4059
+ }
4060
+ function enterAltScreen() {
4061
+ process.stdout.write(`${ALT_SCREEN_ON}${HOME_CLEAR}${HIDE_CURSOR}`);
4062
+ }
4063
+ function leaveAltScreen() {
4064
+ process.stdout.write(`${SHOW_CURSOR}${ALT_SCREEN_OFF}`);
4065
+ }
4066
+ function paint(content) {
4067
+ process.stdout.write(content);
4068
+ }
4069
+ function suspendRawMode() {
4070
+ if (process.stdin.isTTY) {
4071
+ process.stdin.setRawMode(false);
4072
+ }
4073
+ process.stdin.pause();
4074
+ process.stdout.write(SHOW_CURSOR);
4075
+ }
4076
+ function resumeRawMode() {
4077
+ if (process.stdin.isTTY) {
4078
+ process.stdin.setRawMode(true);
4079
+ }
4080
+ process.stdin.resume();
4081
+ process.stdout.write(HIDE_CURSOR);
4082
+ }
4083
+
4084
+ class InteractiveExitError extends Error {
4085
+ constructor() {
4086
+ super("Interactive session cancelled");
4087
+ this.name = "InteractiveExitError";
4088
+ }
4089
+ }
4090
+ var PROVIDER_CHOICES = [
4091
+ { value: "claude-code", label: "Claude Code", description: "Anthropic project logs" },
4092
+ { value: "codex", label: "Codex", description: "OpenAI session logs" },
4093
+ { value: "open-code", label: "Open Code", description: "Open Code storage and database" }
4094
+ ];
4095
+ function isInteractiveExitError(error) {
4096
+ return error instanceof InteractiveExitError;
4097
+ }
4098
+ function parsePositiveInteger(value) {
4099
+ const trimmed = value.trim();
4100
+ if (trimmed === "")
4101
+ return null;
4102
+ const parsed = Number(trimmed);
4103
+ if (!Number.isInteger(parsed) || parsed <= 0) {
4104
+ throw new Error(`Expected a positive whole number, received "${value}".`);
4105
+ }
4106
+ return parsed;
4107
+ }
4108
+ function renderChoiceScreen(title, description, options, selectedIndex, selectedValues, footer = `${color("Up/Down", YELLOW)} move ${color("Space", YELLOW)} toggle ${color("Enter", YELLOW)} confirm ${color("Ctrl-C", YELLOW)} exit`) {
4109
+ const optionLines = options.map((option, index) => {
4110
+ const isSelected = index === selectedIndex;
4111
+ const isChecked = selectedValues ? selectedValues.has(option.value) : isSelected;
4112
+ const pointer = isSelected ? color(">", GREEN) : " ";
4113
+ const checkbox = selectedValues ? isChecked ? color("[x]", GREEN) : color("[ ]", DIM) : isSelected ? color("[\u2022]", GREEN) : color("[ ]", DIM);
4114
+ const titleColor = isSelected ? WHITE + BOLD : WHITE;
4115
+ const descriptionColor = isSelected ? CYAN : DIM;
4116
+ return `${pointer} ${checkbox} ${color(option.label, titleColor)} ${color(option.description, descriptionColor)}`;
4117
+ });
4118
+ const lines = [
4119
+ color(title, WHITE + BOLD),
4120
+ color(description, DIM),
4121
+ "",
4122
+ ...optionLines,
4123
+ "",
4124
+ renderRule(44),
4125
+ footer
4126
+ ];
4127
+ return `${HOME_CLEAR}${HIDE_CURSOR}${lines.join(`
4128
+ `)}`;
4129
+ }
4130
+ async function ask(prompt, initialValue = "") {
4131
+ const readline = createInterface({
4132
+ input: process.stdin,
4133
+ output: process.stdout
4134
+ });
4135
+ let settled = false;
4136
+ return new Promise((resolve, reject) => {
4137
+ const finish = (fn) => {
4138
+ if (settled)
4139
+ return;
4140
+ settled = true;
4141
+ readline.off("SIGINT", onSigint);
4142
+ readline.close();
4143
+ fn();
4144
+ };
4145
+ const onSigint = () => {
4146
+ finish(() => reject(new InteractiveExitError));
4147
+ };
4148
+ readline.on("SIGINT", onSigint);
4149
+ const suffix = initialValue ? ` (${initialValue})` : "";
4150
+ readline.question(`${prompt}${suffix}: `).then((value) => {
4151
+ finish(() => resolve(value.trim() || initialValue));
4152
+ }).catch((error) => {
4153
+ finish(() => reject(error));
4154
+ });
4155
+ });
4156
+ }
4157
+ async function askYesNo(prompt, defaultValue = false) {
4158
+ const hint = defaultValue ? "Y/n" : "y/N";
4159
+ const value = (await ask(`${prompt} [${hint}]`)).toLowerCase();
4160
+ if (value === "")
4161
+ return defaultValue;
4162
+ return value === "y" || value === "yes";
4163
+ }
4164
+ async function promptSingleChoice(title, description, options, initialIndex = 0) {
4165
+ return new Promise((resolve, reject) => {
4166
+ let selectedIndex = Math.max(0, Math.min(initialIndex, options.length - 1));
4167
+ const onKeypress = (_input, key) => {
4168
+ if (key.ctrl && key.name === "c") {
4169
+ cleanup();
4170
+ reject(new InteractiveExitError);
4171
+ return;
4172
+ }
4173
+ if (key.name === "up") {
4174
+ selectedIndex = (selectedIndex - 1 + options.length) % options.length;
4175
+ render();
4176
+ return;
4177
+ }
4178
+ if (key.name === "down") {
4179
+ selectedIndex = (selectedIndex + 1) % options.length;
4180
+ render();
4181
+ return;
4182
+ }
4183
+ const digit = key.sequence?.match(/^[1-9]$/)?.[0];
4184
+ if (digit) {
4185
+ const index = Number(digit) - 1;
4186
+ if (index < options.length) {
4187
+ selectedIndex = index;
4188
+ cleanup();
4189
+ resolve(options[selectedIndex].value);
4190
+ }
4191
+ return;
4192
+ }
4193
+ if (key.name === "return" || key.name === "enter") {
4194
+ cleanup();
4195
+ resolve(options[selectedIndex].value);
4196
+ }
4197
+ };
4198
+ function render() {
4199
+ paint(renderChoiceScreen(title, description, options, selectedIndex, undefined, `${color("Up/Down", YELLOW)} move ${color("1-9", YELLOW)} pick ${color("Enter", YELLOW)} confirm ${color("Ctrl-C", YELLOW)} exit`));
4200
+ }
4201
+ function cleanup() {
4202
+ process.stdin.off("keypress", onKeypress);
4203
+ suspendRawMode();
4204
+ }
4205
+ render();
4206
+ resumeRawMode();
4207
+ process.stdin.on("keypress", onKeypress);
4208
+ });
4209
+ }
4210
+ async function promptMultiChoice(title, description, options, initialValues = []) {
4211
+ return new Promise((resolve, reject) => {
4212
+ let selectedIndex = 0;
4213
+ const selectedValues = new Set(initialValues);
4214
+ const onKeypress = (_input, key) => {
4215
+ if (key.ctrl && key.name === "c") {
4216
+ cleanup();
4217
+ reject(new InteractiveExitError);
4218
+ return;
4219
+ }
4220
+ if (key.name === "up") {
4221
+ selectedIndex = (selectedIndex - 1 + options.length) % options.length;
4222
+ render();
4223
+ return;
4224
+ }
4225
+ if (key.name === "down") {
4226
+ selectedIndex = (selectedIndex + 1) % options.length;
4227
+ render();
4228
+ return;
4229
+ }
4230
+ if (key.name === "space") {
4231
+ toggleSelected(selectedIndex);
4232
+ render();
4233
+ return;
4234
+ }
4235
+ const digit = key.sequence?.match(/^[1-9]$/)?.[0];
4236
+ if (digit) {
4237
+ const index = Number(digit) - 1;
4238
+ if (index < options.length) {
4239
+ selectedIndex = index;
4240
+ toggleSelected(selectedIndex);
4241
+ render();
4242
+ }
4243
+ return;
4244
+ }
4245
+ if (key.name === "return" || key.name === "enter") {
4246
+ cleanup();
4247
+ resolve(Array.from(selectedValues));
4248
+ }
4249
+ };
4250
+ function toggleSelected(index) {
4251
+ const value = options[index].value;
4252
+ if (selectedValues.has(value)) {
4253
+ selectedValues.delete(value);
4254
+ } else {
4255
+ selectedValues.add(value);
4256
+ }
4257
+ }
4258
+ function render() {
4259
+ paint(renderChoiceScreen(title, description, options, selectedIndex, selectedValues));
4260
+ }
4261
+ function cleanup() {
4262
+ process.stdin.off("keypress", onKeypress);
4263
+ suspendRawMode();
4264
+ }
4265
+ render();
4266
+ resumeRawMode();
4267
+ process.stdin.on("keypress", onKeypress);
4268
+ });
4269
+ }
4270
+ function applySelectedProviders(args, providers) {
4271
+ if (providers.length === 0)
4272
+ return;
4273
+ args["provider"] = providers.join(",");
4274
+ }
4275
+ async function promptTheme(defaultTheme = "dark") {
4276
+ const theme = await promptSingleChoice("Theme", "Pick the rendering theme.", [
4277
+ { value: "dark", label: "Dark", description: "High-contrast dark canvas" },
4278
+ { value: "light", label: "Light", description: "Bright export with light background" }
4279
+ ], defaultTheme === "light" ? 1 : 0);
4280
+ return theme;
4281
+ }
4282
+ async function promptDateWindow() {
4283
+ const choice = await promptSingleChoice("Date Window", "Choose how much history to include.", [
4284
+ { value: "7", label: "Last 7 days", description: "Quick recent snapshot" },
4285
+ { value: "30", label: "Last 30 days", description: "Short-term trend window" },
4286
+ { value: "90", label: "Last 90 days", description: "Default overview" },
4287
+ { value: "365", label: "Last 365 days", description: "Long-range usage pattern" },
4288
+ { value: "custom", label: "Custom range", description: "Enter exact dates manually" }
4289
+ ], 2);
4290
+ if (choice !== "custom") {
4291
+ return { days: Number(choice) };
4292
+ }
4293
+ const since = await ask("Since date YYYY-MM-DD");
4294
+ const until = await ask("Until date YYYY-MM-DD (blank for today)");
4295
+ const args = { since };
4296
+ if (until)
4297
+ args["until"] = until;
4298
+ return args;
4299
+ }
4300
+ async function promptProviderSelection(title = "Provider Filter") {
4301
+ return promptMultiChoice(title, "Toggle one or more providers. Leave everything unchecked to use auto-detection.", PROVIDER_CHOICES);
4302
+ }
4303
+ async function promptOutputPath(defaultPath) {
4304
+ return ask("Output file", defaultPath);
4305
+ }
4306
+ async function promptWidth() {
4307
+ const choice = await promptSingleChoice("Terminal Width", "Choose the dashboard width.", [
4308
+ { value: "80", label: "80 columns", description: "Standard terminal width" },
4309
+ { value: "100", label: "100 columns", description: "Balanced dashboard layout" },
4310
+ { value: "120", label: "120 columns", description: "Wide dashboard layout" },
4311
+ { value: "custom", label: "Custom width", description: "Enter an exact width" }
4312
+ ], 1);
4313
+ if (choice !== "custom") {
4314
+ return Number(choice);
4315
+ }
4316
+ while (true) {
4317
+ try {
4318
+ const parsed = parsePositiveInteger(await ask("Custom width"));
4319
+ if (parsed !== null) {
4320
+ return parsed;
4321
+ }
4322
+ paint(`${HOME_CLEAR}${SHOW_CURSOR}${color("Width required", RED)}
4323
+ ${color("Enter a positive whole number to continue.", DIM)}
4324
+
4325
+ Press Enter to try again.`);
4326
+ await ask("");
4327
+ } catch (error) {
4328
+ paint(`${HOME_CLEAR}${SHOW_CURSOR}${color("Invalid width", RED)}
4329
+ ${color(error instanceof Error ? error.message : String(error), DIM)}
4330
+
4331
+ Press Enter to try again.`);
4332
+ await ask("");
4333
+ }
4334
+ }
4335
+ }
4336
+ async function promptCompareSetting() {
4337
+ const choice = await promptSingleChoice("Compare Mode", "Optionally compare the current range against an earlier period.", [
4338
+ { value: "off", label: "No compare", description: "Render a standard single-period report" },
4339
+ { value: "auto", label: "Auto compare", description: "Split the selected window automatically" },
4340
+ { value: "custom", label: "Custom compare range", description: "Provide an explicit YYYY-MM-DD..YYYY-MM-DD range" }
4341
+ ]);
4342
+ if (choice === "off")
4343
+ return null;
4344
+ if (choice === "auto")
4345
+ return "auto";
4346
+ return ask("Previous range YYYY-MM-DD..YYYY-MM-DD");
4347
+ }
4348
+ async function buildDashboardPreset() {
4349
+ const rangeArgs = await promptDateWindow();
4350
+ const providers = await promptProviderSelection();
4351
+ const width = await promptWidth();
4352
+ const noInsights = await askYesNo("Hide insights panel", false);
4353
+ const noColor2 = await askYesNo("Disable ANSI colors", false);
4354
+ const args = { ...rangeArgs };
4355
+ applySelectedProviders(args, providers);
4356
+ if (width)
4357
+ args["width"] = width;
4358
+ if (noInsights)
4359
+ args["noInsights"] = true;
4360
+ if (noColor2)
4361
+ args["noColor"] = true;
4362
+ return createRunCommand(args);
4363
+ }
4364
+ async function buildJsonPreset() {
4365
+ const rangeArgs = await promptDateWindow();
4366
+ const providers = await promptProviderSelection();
4367
+ const compare = await promptCompareSetting();
4368
+ const saveToFile = await askYesNo("Write JSON to a file", false);
4369
+ const clipboard = !saveToFile && await askYesNo("Copy JSON to clipboard after render", false);
4370
+ const args = {
4371
+ format: "json",
4372
+ ...rangeArgs
4373
+ };
4374
+ applySelectedProviders(args, providers);
4375
+ if (compare)
4376
+ args["compare"] = compare;
4377
+ if (saveToFile) {
4378
+ args["output"] = await promptOutputPath(compare ? "tokenleak-compare.json" : "tokenleak.json");
4379
+ }
4380
+ if (clipboard)
4381
+ args["clipboard"] = true;
4382
+ return createRunCommand(args);
4383
+ }
4384
+ async function buildImagePreset(format) {
4385
+ const theme = await promptTheme();
4386
+ const rangeArgs = await promptDateWindow();
4387
+ const providers = await promptProviderSelection("Provider Filter");
4388
+ const compare = await promptCompareSetting();
4389
+ const output = await promptOutputPath(`tokenleak.${format}`);
4390
+ const shouldOpen = await askYesNo("Open the file when done", true);
4391
+ const more = compare ? true : await askYesNo("Enable --more stats", format === "png");
4392
+ const args = {
4393
+ format,
4394
+ theme,
4395
+ output,
4396
+ open: shouldOpen,
4397
+ more,
4398
+ ...rangeArgs
4399
+ };
4400
+ applySelectedProviders(args, providers);
4401
+ if (compare)
4402
+ args["compare"] = compare;
4403
+ return createRunCommand(args);
4404
+ }
4405
+ async function buildComparePreset() {
4406
+ const rangeArgs = await promptDateWindow();
4407
+ const providers = await promptProviderSelection();
4408
+ const compareMode = await promptSingleChoice("Reference Period", "Choose how the earlier comparison period should be defined.", [
4409
+ { value: "auto", label: "Auto compare", description: "Split the chosen window automatically" },
4410
+ { value: "custom", label: "Custom compare range", description: "Enter an explicit prior range manually" }
4411
+ ]);
4412
+ const compare = compareMode === "custom" ? await ask("Previous range YYYY-MM-DD..YYYY-MM-DD") : "auto";
4413
+ const saveToFile = await askYesNo("Write compare output to a file", false);
4414
+ const args = {
4415
+ format: "json",
4416
+ compare,
4417
+ ...rangeArgs
4418
+ };
4419
+ applySelectedProviders(args, providers);
4420
+ if (saveToFile) {
4421
+ args["output"] = await promptOutputPath("tokenleak-compare.json");
4422
+ }
4423
+ return createRunCommand(args);
4424
+ }
4425
+ async function buildLivePreset() {
4426
+ const theme = await promptTheme();
4427
+ const rangeArgs = await promptDateWindow();
4428
+ const providers = await promptProviderSelection();
4429
+ const more = await askYesNo("Enable --more stats", true);
4430
+ const args = {
4431
+ liveServer: true,
4432
+ theme,
4433
+ more,
4434
+ ...rangeArgs
4435
+ };
4436
+ applySelectedProviders(args, providers);
4437
+ return createRunCommand(args);
4438
+ }
4439
+ async function askFormatChoice() {
4440
+ return promptSingleChoice("Output Format", "Choose the primary renderer for this command.", [
4441
+ { value: "terminal", label: "Terminal", description: "Dashboard in the current terminal" },
4442
+ { value: "json", label: "JSON", description: "Structured machine-readable output" },
4443
+ { value: "svg", label: "SVG", description: "Shareable vector export" },
4444
+ { value: "png", label: "PNG", description: "Raster export for social and docs" }
4445
+ ]);
4446
+ }
4447
+ async function buildCustomCommand() {
4448
+ const mode = await promptSingleChoice("Command Type", "Choose the command family you want to configure.", [
4449
+ { value: "run", label: "Standard command", description: "Render terminal, JSON, SVG, or PNG output" },
4450
+ { value: "live-server", label: "Live server", description: "Launch the browser dashboard locally" },
4451
+ { value: "list-providers", label: "List providers", description: "Inspect registered provider backends" }
4452
+ ]);
4453
+ if (mode === "live-server") {
4454
+ return buildLivePreset();
4455
+ }
4456
+ if (mode === "list-providers") {
4457
+ return createRunCommand({ listProviders: true });
4458
+ }
4459
+ const format = await askFormatChoice();
4460
+ const theme = format === "terminal" ? null : await promptTheme();
4461
+ const rangeArgs = await promptDateWindow();
4462
+ const providers = await promptProviderSelection();
4463
+ const compare = await promptCompareSetting();
4464
+ const width = format === "terminal" ? await promptWidth() : null;
4465
+ const output = format === "terminal" ? await ask("Output file (blank keeps stdout)") : format === "json" ? await ask("Output file (blank keeps stdout)") : await ask("Output file", `tokenleak.${format}`);
4466
+ const noColor2 = await askYesNo("Disable ANSI colors", false);
4467
+ const noInsights = format === "terminal" ? await askYesNo("Hide insights", false) : false;
4468
+ const more = await askYesNo("Enable --more stats", format === "png" || format === "svg");
4469
+ const clipboard = format !== "png" ? await askYesNo("Copy output to clipboard", false) : false;
4470
+ const open = format !== "terminal" ? await askYesNo("Open generated file", false) : false;
4471
+ const upload = format !== "png" ? await ask("Upload target [blank/gist]") : "";
4472
+ const args = {
4473
+ format,
4474
+ ...rangeArgs
4475
+ };
4476
+ if (theme)
4477
+ args["theme"] = theme;
4478
+ if (compare)
4479
+ args["compare"] = compare;
4480
+ if (width)
4481
+ args["width"] = width;
4482
+ if (output)
4483
+ args["output"] = output;
4484
+ if (noColor2)
4485
+ args["noColor"] = true;
4486
+ if (noInsights)
4487
+ args["noInsights"] = true;
4488
+ if (more)
4489
+ args["more"] = true;
4490
+ if (clipboard)
4491
+ args["clipboard"] = true;
4492
+ if (open)
4493
+ args["open"] = true;
4494
+ if (upload)
4495
+ args["upload"] = upload;
4496
+ applySelectedProviders(args, providers);
4497
+ return createRunCommand(args);
4498
+ }
4499
+ function createMenuOptions() {
4500
+ return [
4501
+ {
4502
+ digit: "1",
4503
+ title: "Launch Dashboard",
4504
+ description: "guided terminal view",
4505
+ preview: "tokenleak --days 90",
4506
+ select: buildDashboardPreset
4507
+ },
4508
+ {
4509
+ digit: "2",
4510
+ title: "Export JSON",
4511
+ description: "structured output for scripts",
4512
+ preview: "tokenleak --format json",
4513
+ select: buildJsonPreset
4514
+ },
4515
+ {
4516
+ digit: "3",
4517
+ title: "Export SVG",
4518
+ description: "shareable vector card",
4519
+ preview: "tokenleak --format svg --output tokenleak.svg",
4520
+ select: async () => buildImagePreset("svg")
4521
+ },
4522
+ {
4523
+ digit: "4",
4524
+ title: "Export PNG",
4525
+ description: "social-ready raster image",
4526
+ preview: "tokenleak --format png --output tokenleak.png --more",
4527
+ select: async () => buildImagePreset("png")
4528
+ },
4529
+ {
4530
+ digit: "5",
4531
+ title: "Compare Periods",
4532
+ description: "diff current vs previous usage",
4533
+ preview: "tokenleak --compare auto --format json",
4534
+ select: buildComparePreset
4535
+ },
4536
+ {
4537
+ digit: "6",
4538
+ title: "Start Live Server",
4539
+ description: "browser dashboard on localhost",
4540
+ preview: "tokenleak --live-server --theme dark",
4541
+ select: buildLivePreset
4542
+ },
4543
+ {
4544
+ digit: "7",
4545
+ title: "Build Custom Command",
4546
+ description: "configure flags interactively",
4547
+ preview: "tokenleak --format terminal --days 90",
4548
+ select: buildCustomCommand
4549
+ },
4550
+ {
4551
+ digit: "8",
4552
+ title: "Full Help",
4553
+ description: "examples and complete usage",
4554
+ preview: "tokenleak --help",
4555
+ select: async () => ({ type: "show-help" })
4556
+ },
4557
+ {
4558
+ digit: "9",
4559
+ title: "List Providers",
4560
+ description: "detect available registries",
4561
+ preview: "tokenleak --list-providers",
4562
+ select: async () => createRunCommand({ listProviders: true })
4563
+ },
4564
+ {
4565
+ digit: "0",
4566
+ title: "Exit",
4567
+ description: "close the launcher",
4568
+ preview: "exit",
4569
+ select: async () => ({ type: "exit" })
4570
+ }
4571
+ ];
4572
+ }
4573
+ async function waitForSingleKey() {
4574
+ return new Promise((resolve) => {
4575
+ const onKeypress = (_input, key) => {
4576
+ process.stdin.off("keypress", onKeypress);
4577
+ suspendRawMode();
4578
+ resolve(key);
4579
+ };
4580
+ resumeRawMode();
4581
+ process.stdin.on("keypress", onKeypress);
4582
+ });
4583
+ }
4584
+ async function promptForMenuCommand(context, options, state) {
4585
+ let showHelp = false;
4586
+ let resolving = false;
4587
+ return new Promise((resolve, reject) => {
4588
+ const onKeypress = async (_input, key) => {
4589
+ if (resolving) {
4590
+ return;
4591
+ }
4592
+ if (key.ctrl && key.name === "c") {
4593
+ cleanup();
4594
+ resolve({ type: "exit" });
4595
+ return;
4596
+ }
4597
+ if (showHelp) {
4598
+ if (key.name === "escape" || key.name === "return" || key.name === "enter" || key.name === "q" || key.name === "h") {
4599
+ showHelp = false;
4600
+ paint(renderLauncher(context, options, state.selectedIndex));
4601
+ }
4602
+ return;
4603
+ }
4604
+ if (key.name === "up") {
4605
+ state.selectedIndex = (state.selectedIndex - 1 + options.length) % options.length;
4606
+ paint(renderLauncher(context, options, state.selectedIndex));
4607
+ return;
4608
+ }
4609
+ if (key.name === "down") {
4610
+ state.selectedIndex = (state.selectedIndex + 1) % options.length;
4611
+ paint(renderLauncher(context, options, state.selectedIndex));
4612
+ return;
4613
+ }
4614
+ if (key.name === "h") {
4615
+ showHelp = true;
4616
+ paint(renderHelpOverlay(context.helpText, Math.max(60, (process.stdout.columns ?? 120) - 1)));
4617
+ return;
4618
+ }
4619
+ if (key.name === "q" || key.name === "escape") {
4620
+ cleanup();
4621
+ resolve({ type: "exit" });
4622
+ return;
4623
+ }
4624
+ const digit = key.sequence?.match(/^[0-9]$/)?.[0];
4625
+ if (digit) {
4626
+ const nextIndex = options.findIndex((option) => option.digit === digit);
4627
+ if (nextIndex >= 0) {
4628
+ state.selectedIndex = nextIndex;
4629
+ if (digit === "8") {
4630
+ showHelp = true;
4631
+ paint(renderHelpOverlay(context.helpText, Math.max(60, (process.stdout.columns ?? 120) - 1)));
4632
+ return;
4633
+ }
4634
+ resolving = true;
4635
+ cleanup();
4636
+ try {
4637
+ const command = await options[nextIndex].select();
4638
+ resolve(command);
4639
+ } catch (error) {
4640
+ if (isInteractiveExitError(error)) {
4641
+ resolve({ type: "exit" });
4642
+ return;
4643
+ }
4644
+ reject(error);
4645
+ }
4646
+ }
4647
+ return;
4648
+ }
4649
+ if (key.name === "return" || key.name === "enter") {
4650
+ if (options[state.selectedIndex].digit === "8") {
4651
+ showHelp = true;
4652
+ paint(renderHelpOverlay(context.helpText, Math.max(60, (process.stdout.columns ?? 120) - 1)));
4653
+ return;
4654
+ }
4655
+ resolving = true;
4656
+ cleanup();
4657
+ try {
4658
+ const command = await options[state.selectedIndex].select();
4659
+ resolve(command);
4660
+ } catch (error) {
4661
+ if (isInteractiveExitError(error)) {
4662
+ resolve({ type: "exit" });
4663
+ return;
4664
+ }
4665
+ reject(error);
4666
+ }
4667
+ }
4668
+ };
4669
+ function cleanup() {
4670
+ process.stdin.off("keypress", onKeypress);
4671
+ suspendRawMode();
4672
+ }
4673
+ paint(renderLauncher(context, options, state.selectedIndex));
4674
+ resumeRawMode();
4675
+ process.stdin.on("keypress", onKeypress);
4676
+ });
4677
+ }
4678
+ async function showExecutionResult(request, result) {
4679
+ paint(renderResult(request, result));
4680
+ const key = await waitForSingleKey();
4681
+ if (key.ctrl && key.name === "c")
4682
+ return "exit";
4683
+ if (key.name === "q" || key.name === "escape")
4684
+ return "exit";
4685
+ return "menu";
4686
+ }
4687
+ function shouldStartInteractiveCli(argv, stdinIsTTY, stdoutIsTTY) {
4688
+ return argv.length === 0 && stdinIsTTY && stdoutIsTTY;
4689
+ }
4690
+ async function startInteractiveCli(context, execute) {
4691
+ const options = createMenuOptions();
4692
+ const state = { selectedIndex: 0 };
4693
+ let interrupted = false;
4694
+ let ignoreSigint = false;
4695
+ const onSigint = () => {
4696
+ if (!ignoreSigint) {
4697
+ interrupted = true;
4698
+ }
4699
+ };
4700
+ emitKeypressEvents(process.stdin);
4701
+ enterAltScreen();
4702
+ process.on("SIGINT", onSigint);
4703
+ try {
4704
+ while (true) {
4705
+ if (interrupted) {
4706
+ return;
4707
+ }
4708
+ const command = await promptForMenuCommand(context, options, state);
4709
+ if (command.type === "exit") {
4710
+ return;
4711
+ }
4712
+ if (command.type === "show-help") {
4713
+ continue;
4714
+ }
4715
+ const startedAt = Date.now();
4716
+ let loadingFrame = 0;
4717
+ paint(renderLoading(command.request, loadingFrame, startedAt));
4718
+ const loadingTicker = setInterval(() => {
4719
+ loadingFrame += 1;
4720
+ paint(renderLoading(command.request, loadingFrame, startedAt));
4721
+ }, LOADING_TICK_MS);
4722
+ let result;
4723
+ try {
4724
+ if (command.request.executionMode === "inherit") {
4725
+ clearInterval(loadingTicker);
4726
+ leaveAltScreen();
4727
+ try {
4728
+ ignoreSigint = true;
4729
+ result = await execute(command.request);
4730
+ } finally {
4731
+ ignoreSigint = false;
4732
+ enterAltScreen();
4733
+ }
4734
+ } else {
4735
+ result = await execute(command.request);
4736
+ }
4737
+ } finally {
4738
+ clearInterval(loadingTicker);
4739
+ }
4740
+ if (interrupted) {
4741
+ return;
4742
+ }
4743
+ const next = await showExecutionResult(command.request, result);
4744
+ if (next === "exit") {
4745
+ return;
4746
+ }
4747
+ }
4748
+ } finally {
4749
+ process.off("SIGINT", onSigint);
4750
+ suspendRawMode();
4751
+ leaveAltScreen();
4752
+ }
4753
+ }
4754
+
3054
4755
  // packages/cli/src/sharing/clipboard.ts
3055
4756
  var PLATFORM_COMMANDS = {
3056
4757
  darwin: ["pbcopy"],
@@ -3208,6 +4909,7 @@ function buildHelpText() {
3208
4909
  return [
3209
4910
  `tokenleak ${VERSION}`,
3210
4911
  "Visualize AI coding assistant token usage across providers.",
4912
+ "Running `tokenleak` with no flags opens an interactive launcher in a TTY.",
3211
4913
  "",
3212
4914
  "Usage:",
3213
4915
  " tokenleak [flags]",
@@ -3229,6 +4931,7 @@ function buildHelpText() {
3229
4931
  " -w, --width <number> Terminal render width",
3230
4932
  " -p, --provider <list> Provider filter list, comma-separated",
3231
4933
  " --compare <range> Compare against YYYY-MM-DD..YYYY-MM-DD or auto",
4934
+ " --more Add expanded PNG/SVG stats and unlock compare cards",
3232
4935
  " --clipboard Copy rendered output to the clipboard",
3233
4936
  " --open Open the generated output file",
3234
4937
  " --upload <target> Upload rendered output, currently: gist",
@@ -3272,6 +4975,81 @@ function normalizeCliArg(arg) {
3272
4975
  };
3273
4976
  return flagMap[arg] ?? arg;
3274
4977
  }
4978
+ function buildInteractiveSummary(cliArgs, ok, exitCode) {
4979
+ if (!ok) {
4980
+ return `Command exited with code ${exitCode}.`;
4981
+ }
4982
+ if (typeof cliArgs["output"] === "string") {
4983
+ const outputPath = cliArgs["output"];
4984
+ const format2 = String(cliArgs["format"] ?? inferFormatFromPath(outputPath) ?? "output").toUpperCase();
4985
+ return `${format2} written to ${outputPath}.`;
4986
+ }
4987
+ if (cliArgs["listProviders"]) {
4988
+ return "Provider registry loaded.";
4989
+ }
4990
+ if (cliArgs["liveServer"]) {
4991
+ return "Live dashboard stopped.";
4992
+ }
4993
+ if (cliArgs["compare"]) {
4994
+ return "Compare report generated.";
4995
+ }
4996
+ const format = String(cliArgs["format"] ?? "terminal");
4997
+ if (format === "terminal") {
4998
+ return "Terminal dashboard generated.";
4999
+ }
5000
+ return `${format.toUpperCase()} command finished successfully.`;
5001
+ }
5002
+ async function executeInteractiveCommand(request) {
5003
+ try {
5004
+ const cliPath = process.argv[1];
5005
+ if (!cliPath) {
5006
+ return {
5007
+ ok: false,
5008
+ summary: "Could not resolve the current tokenleak entrypoint.",
5009
+ stdout: "",
5010
+ stderr: "Error: process.argv[1] is missing."
5011
+ };
5012
+ }
5013
+ const command = [process.execPath, cliPath, ...buildCliArgTokens(request.args)];
5014
+ if (request.executionMode === "inherit") {
5015
+ const proc2 = Bun.spawn(command, {
5016
+ stdin: "inherit",
5017
+ stdout: "inherit",
5018
+ stderr: "inherit"
5019
+ });
5020
+ const exitCode2 = await proc2.exited;
5021
+ return {
5022
+ ok: exitCode2 === 0,
5023
+ summary: buildInteractiveSummary(request.args, exitCode2 === 0, exitCode2),
5024
+ stdout: "",
5025
+ stderr: ""
5026
+ };
5027
+ }
5028
+ const proc = Bun.spawn(command, {
5029
+ stdin: "ignore",
5030
+ stdout: "pipe",
5031
+ stderr: "pipe"
5032
+ });
5033
+ const [exitCode, stdout, stderr] = await Promise.all([
5034
+ proc.exited,
5035
+ new Response(proc.stdout).text(),
5036
+ new Response(proc.stderr).text()
5037
+ ]);
5038
+ return {
5039
+ ok: exitCode === 0,
5040
+ summary: buildInteractiveSummary(request.args, exitCode === 0, exitCode),
5041
+ stdout,
5042
+ stderr
5043
+ };
5044
+ } catch (error) {
5045
+ return {
5046
+ ok: false,
5047
+ summary: "Interactive command failed before it could finish.",
5048
+ stdout: "",
5049
+ stderr: error instanceof Error ? `Error: ${error.message}` : `Error: ${String(error)}`
5050
+ };
5051
+ }
5052
+ }
3275
5053
  function normalizeCliArgv(argv) {
3276
5054
  const normalized = argv.map(normalizeCliArg);
3277
5055
  const result = [];
@@ -3370,6 +5148,7 @@ function resolveConfig(cliArgs) {
3370
5148
  width: 80,
3371
5149
  noColor: false,
3372
5150
  noInsights: false,
5151
+ more: false,
3373
5152
  claude: false,
3374
5153
  codex: false,
3375
5154
  openCode: false,
@@ -3393,6 +5172,8 @@ function resolveConfig(cliArgs) {
3393
5172
  merged.noColor = fileConfig.noColor;
3394
5173
  if (fileConfig.noInsights !== undefined)
3395
5174
  merged.noInsights = fileConfig.noInsights;
5175
+ if (fileConfig.more !== undefined)
5176
+ merged.more = fileConfig.more;
3396
5177
  if (envConfig.format)
3397
5178
  merged.format = envConfig.format;
3398
5179
  if (envConfig.theme)
@@ -3434,6 +5215,9 @@ function resolveConfig(cliArgs) {
3434
5215
  if (cliArgs["noInsights"] !== undefined) {
3435
5216
  result.noInsights = cliArgs["noInsights"];
3436
5217
  }
5218
+ if (cliArgs["more"] !== undefined) {
5219
+ result.more = cliArgs["more"];
5220
+ }
3437
5221
  if (cliArgs["compare"] !== undefined) {
3438
5222
  result.compare = cliArgs["compare"];
3439
5223
  }
@@ -3511,7 +5295,11 @@ async function runCompare(compareStr, currentRange, _registry, available) {
3511
5295
  loadAndAggregate(currentRange, available),
3512
5296
  loadAndAggregate(previousRange, available)
3513
5297
  ]);
3514
- return buildCompareOutput({ range: currentRange, stats: currentResult.stats }, { range: previousRange, stats: previousResult.stats });
5298
+ return {
5299
+ compareOutput: buildCompareOutput({ range: currentRange, stats: currentResult.stats }, { range: previousRange, stats: previousResult.stats }),
5300
+ currentData: currentResult.data,
5301
+ previousData: previousResult.data
5302
+ };
3515
5303
  }
3516
5304
  async function run(cliArgs) {
3517
5305
  const config = resolveConfig(cliArgs);
@@ -3544,12 +5332,45 @@ async function run(cliArgs) {
3544
5332
  throw new TokenleakError("No provider data found");
3545
5333
  }
3546
5334
  if (config.compare) {
5335
+ const compareResult = await runCompare(config.compare, dateRange, registry, available);
5336
+ if (config.more && (config.format === "png" || config.format === "svg")) {
5337
+ const compareOutput = {
5338
+ schemaVersion: SCHEMA_VERSION,
5339
+ generated: new Date().toISOString(),
5340
+ dateRange,
5341
+ providers: compareResult.currentData,
5342
+ aggregated: compareResult.compareOutput.periodA.stats,
5343
+ more: buildMoreStats(compareResult.currentData, dateRange, {
5344
+ previousRange: compareResult.compareOutput.periodB.range,
5345
+ previousProviders: compareResult.previousData
5346
+ })
5347
+ };
5348
+ const renderer2 = getRenderer(config.format);
5349
+ const renderOptions2 = {
5350
+ format: config.format,
5351
+ theme: config.theme,
5352
+ width: config.width,
5353
+ showInsights: !config.noInsights,
5354
+ noColor: config.noColor,
5355
+ output: config.output,
5356
+ more: true
5357
+ };
5358
+ const rendered3 = await renderer2.render(compareOutput, renderOptions2);
5359
+ if (config.output) {
5360
+ const data = typeof rendered3 === "string" ? rendered3 : Buffer.from(rendered3);
5361
+ writeFileSync(config.output, data);
5362
+ } else {
5363
+ const text2 = typeof rendered3 === "string" ? rendered3 : rendered3.toString("utf-8");
5364
+ process.stdout.write(text2 + `
5365
+ `);
5366
+ }
5367
+ return;
5368
+ }
3547
5369
  if (config.format !== "json" && config.format !== "terminal") {
3548
5370
  process.stderr.write(`Warning: --compare only supports JSON output. Ignoring --format ${config.format}.
3549
5371
  `);
3550
5372
  }
3551
- const compareOutput = await runCompare(config.compare, dateRange, registry, available);
3552
- const rendered2 = JSON.stringify(compareOutput, null, 2);
5373
+ const rendered2 = JSON.stringify(compareResult.compareOutput, null, 2);
3553
5374
  if (config.output) {
3554
5375
  writeFileSync(config.output, rendered2);
3555
5376
  } else {
@@ -3576,7 +5397,8 @@ async function run(cliArgs) {
3576
5397
  generated: new Date().toISOString(),
3577
5398
  dateRange,
3578
5399
  providers: providerDataList,
3579
- aggregated: stats
5400
+ aggregated: stats,
5401
+ more: config.more ? buildMoreStats(providerDataList, dateRange) : null
3580
5402
  };
3581
5403
  if (config.liveServer) {
3582
5404
  const ignoredFlags = [];
@@ -3598,7 +5420,8 @@ async function run(cliArgs) {
3598
5420
  width: config.width,
3599
5421
  showInsights: !config.noInsights,
3600
5422
  noColor: config.noColor,
3601
- output: config.output
5423
+ output: config.output,
5424
+ more: config.more
3602
5425
  };
3603
5426
  const { port } = await startLiveServer(output, renderOptions2);
3604
5427
  await new Promise((resolve) => {
@@ -3621,7 +5444,8 @@ Shutting down server...
3621
5444
  width: config.width,
3622
5445
  showInsights: !config.noInsights,
3623
5446
  noColor: config.noColor,
3624
- output: config.output
5447
+ output: config.output,
5448
+ more: config.more
3625
5449
  };
3626
5450
  const rendered = await renderer.render(output, renderOptions);
3627
5451
  if (config.output) {
@@ -3710,6 +5534,11 @@ var main = defineCommand({
3710
5534
  description: "Hide insights panel",
3711
5535
  default: false
3712
5536
  },
5537
+ more: {
5538
+ type: "boolean",
5539
+ description: "Add expanded PNG/SVG stats and compare cards",
5540
+ default: false
5541
+ },
3713
5542
  compare: {
3714
5543
  type: "string",
3715
5544
  description: "Compare two date ranges (YYYY-MM-DD..YYYY-MM-DD)"
@@ -3786,6 +5615,8 @@ var main = defineCommand({
3786
5615
  cliArgs["noColor"] = true;
3787
5616
  if (args.noInsights)
3788
5617
  cliArgs["noInsights"] = true;
5618
+ if (args.more)
5619
+ cliArgs["more"] = true;
3789
5620
  if (args.compare !== undefined)
3790
5621
  cliArgs["compare"] = args.compare;
3791
5622
  if (args.provider !== undefined)
@@ -3827,12 +5658,20 @@ if (isDirectExecution) {
3827
5658
  process.stdout.write(buildVersionText());
3828
5659
  process.exit(0);
3829
5660
  }
3830
- runMain(main);
5661
+ if (shouldStartInteractiveCli(argv, Boolean(process.stdin.isTTY), Boolean(process.stdout.isTTY))) {
5662
+ await startInteractiveCli({
5663
+ version: VERSION,
5664
+ helpText: buildHelpText()
5665
+ }, executeInteractiveCommand);
5666
+ } else {
5667
+ await runMain(main);
5668
+ }
3831
5669
  }
3832
5670
  export {
3833
5671
  run,
3834
5672
  resolveConfig,
3835
5673
  normalizeCliArgv,
3836
5674
  inferFormatFromPath,
3837
- computeDateRange
5675
+ computeDateRange,
5676
+ buildInteractiveSummary
3838
5677
  };