tokenleak 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/tokenleak.js +800 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenleak",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Visualise your AI coding-assistant token usage across providers — heatmaps, dashboards, and shareable cards.",
5
5
  "type": "module",
6
6
  "bin": {
package/tokenleak.js CHANGED
@@ -736,7 +736,7 @@ function computePreviousPeriod(current) {
736
736
  };
737
737
  }
738
738
  // packages/core/dist/index.js
739
- var VERSION = "0.4.1";
739
+ var VERSION = "0.5.0";
740
740
 
741
741
  // packages/registry/dist/models/normalizer.js
742
742
  var DATE_SUFFIX_PATTERN = /-\d{8}$/;
@@ -1526,6 +1526,15 @@ function formatNumber(n) {
1526
1526
  }
1527
1527
  return n.toFixed(0);
1528
1528
  }
1529
+ function formatCost(cost) {
1530
+ if (cost >= 100) {
1531
+ return `$${cost.toFixed(0)}`;
1532
+ }
1533
+ if (cost >= 1) {
1534
+ return `$${cost.toFixed(2)}`;
1535
+ }
1536
+ return `$${cost.toFixed(4)}`;
1537
+ }
1529
1538
 
1530
1539
  // packages/renderers/dist/svg/heatmap.js
1531
1540
  var DAY_LABELS2 = ["Mon", "", "Wed", "", "Fri", "", "Sun"];
@@ -1773,14 +1782,326 @@ class SvgRenderer {
1773
1782
  return svgContent;
1774
1783
  }
1775
1784
  }
1785
+ // packages/renderers/dist/png/terminal-card.js
1786
+ var CARD_PADDING = 48;
1787
+ var TITLEBAR_HEIGHT = 48;
1788
+ var DOT_RADIUS = 6;
1789
+ var DOT_GAP = 8;
1790
+ var CELL_SIZE2 = 16;
1791
+ var CELL_GAP2 = 4;
1792
+ var STAT_GRID_COLS = 3;
1793
+ var MODEL_BAR_HEIGHT = 8;
1794
+ var DAY_LABEL_WIDTH2 = 44;
1795
+ var MONTH_LABEL_HEIGHT2 = 24;
1796
+ var PROVIDER_SECTION_GAP = 36;
1797
+ var FONT_FAMILY2 = "'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', monospace";
1798
+ function getCardTheme(mode) {
1799
+ if (mode === "dark") {
1800
+ return {
1801
+ bg: "#0c0c0c",
1802
+ fg: "#ffffff",
1803
+ muted: "#52525b",
1804
+ border: "rgba(255,255,255,0.06)",
1805
+ accent: "#10b981",
1806
+ heatmapEmpty: "#1a1a1a",
1807
+ barTrack: "#1c1c1c",
1808
+ titlebarBorder: "rgba(255,255,255,0.06)"
1809
+ };
1810
+ }
1811
+ return {
1812
+ bg: "#fafafa",
1813
+ fg: "#18181b",
1814
+ muted: "#a1a1aa",
1815
+ border: "rgba(0,0,0,0.08)",
1816
+ accent: "#059669",
1817
+ heatmapEmpty: "#e4e4e7",
1818
+ barTrack: "#e5e5e5",
1819
+ titlebarBorder: "rgba(0,0,0,0.08)"
1820
+ };
1821
+ }
1822
+ function buildHeatmapScale(colors, isDark) {
1823
+ const [startHex, endHex] = colors.gradient;
1824
+ const s = hexToRgb(startHex);
1825
+ const e = hexToRgb(endHex);
1826
+ const opacities = isDark ? [0.15, 0.35, 0.6, 1] : [0.2, 0.4, 0.65, 1];
1827
+ return [
1828
+ "transparent",
1829
+ ...opacities.map((t) => {
1830
+ const r = Math.round(s.r + (e.r - s.r) * t);
1831
+ const g = Math.round(s.g + (e.g - s.g) * t);
1832
+ const b = Math.round(s.b + (e.b - s.b) * t);
1833
+ return rgbToHex(r, g, b);
1834
+ })
1835
+ ];
1836
+ }
1837
+ function hexToRgb(hex) {
1838
+ const h = hex.replace("#", "");
1839
+ return {
1840
+ r: parseInt(h.slice(0, 2), 16),
1841
+ g: parseInt(h.slice(2, 4), 16),
1842
+ b: parseInt(h.slice(4, 6), 16)
1843
+ };
1844
+ }
1845
+ function rgbToHex(r, g, b) {
1846
+ const toHex = (n) => n.toString(16).padStart(2, "0");
1847
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
1848
+ }
1849
+ function computeQuantiles2(values) {
1850
+ const nonZero = values.filter((v) => v > 0).sort((a, b) => a - b);
1851
+ if (nonZero.length === 0)
1852
+ return [0, 0, 0];
1853
+ const q = (p) => {
1854
+ const idx = Math.floor(p * (nonZero.length - 1));
1855
+ return nonZero[idx] ?? 0;
1856
+ };
1857
+ return [q(0.25), q(0.5), q(0.75)];
1858
+ }
1859
+ function getLevel2(tokens, quantiles) {
1860
+ if (tokens <= 0)
1861
+ return 0;
1862
+ if (tokens <= quantiles[0])
1863
+ return 1;
1864
+ if (tokens <= quantiles[1])
1865
+ return 2;
1866
+ if (tokens <= quantiles[2])
1867
+ return 3;
1868
+ return 4;
1869
+ }
1870
+ var MONTH_NAMES2 = [
1871
+ "Jan",
1872
+ "Feb",
1873
+ "Mar",
1874
+ "Apr",
1875
+ "May",
1876
+ "Jun",
1877
+ "Jul",
1878
+ "Aug",
1879
+ "Sep",
1880
+ "Oct",
1881
+ "Nov",
1882
+ "Dec"
1883
+ ];
1884
+ var MONTH_NAMES_FULL = [
1885
+ "JAN",
1886
+ "FEB",
1887
+ "MAR",
1888
+ "APR",
1889
+ "MAY",
1890
+ "JUN",
1891
+ "JUL",
1892
+ "AUG",
1893
+ "SEP",
1894
+ "OCT",
1895
+ "NOV",
1896
+ "DEC"
1897
+ ];
1898
+ function formatDateRange(since, until) {
1899
+ const s = new Date(since + "T00:00:00Z");
1900
+ const u = new Date(until + "T00:00:00Z");
1901
+ const diffMs = u.getTime() - s.getTime();
1902
+ const days = Math.round(diffMs / (1000 * 60 * 60 * 24));
1903
+ const sMonth = MONTH_NAMES_FULL[s.getUTCMonth()] ?? "";
1904
+ const uMonth = MONTH_NAMES_FULL[u.getUTCMonth()] ?? "";
1905
+ return `${sMonth} ${s.getUTCFullYear()} \u2014 ${uMonth} ${u.getUTCFullYear()} \xB7 ${days} DAYS`;
1906
+ }
1907
+ function formatPercentage(rate) {
1908
+ return `${(rate * 100).toFixed(1)}%`;
1909
+ }
1910
+ function formatStreak(n) {
1911
+ return `${n} day${n !== 1 ? "s" : ""}`;
1912
+ }
1913
+ function renderProviderHeatmap(daily, since, until, heatmapColors, emptyColor) {
1914
+ const tokenMap = new Map;
1915
+ for (const d of daily) {
1916
+ tokenMap.set(d.date, (tokenMap.get(d.date) ?? 0) + d.totalTokens);
1917
+ }
1918
+ const dates = daily.map((d) => d.date).sort();
1919
+ const endStr = until ?? dates[dates.length - 1] ?? new Date().toISOString().slice(0, 10);
1920
+ const startStr = since ?? dates[0] ?? endStr;
1921
+ const end = new Date(endStr + "T00:00:00Z");
1922
+ const start = new Date(startStr + "T00:00:00Z");
1923
+ start.setUTCDate(start.getUTCDate() - start.getUTCDay());
1924
+ const allTokens = Array.from(tokenMap.values());
1925
+ const quantiles = computeQuantiles2(allTokens);
1926
+ const cells = [];
1927
+ const monthLabels = [];
1928
+ let lastMonth = -1;
1929
+ let col = 0;
1930
+ const current = new Date(start);
1931
+ while (current <= end) {
1932
+ const row = current.getUTCDay();
1933
+ const dateStr = current.toISOString().slice(0, 10);
1934
+ const tokens = tokenMap.get(dateStr) ?? 0;
1935
+ const level = getLevel2(tokens, quantiles);
1936
+ const x = DAY_LABEL_WIDTH2 + col * (CELL_SIZE2 + CELL_GAP2);
1937
+ const y = MONTH_LABEL_HEIGHT2 + row * (CELL_SIZE2 + CELL_GAP2);
1938
+ const fill = level === 0 ? emptyColor : heatmapColors[level];
1939
+ const title = `${dateStr}: ${tokens.toLocaleString()} tokens`;
1940
+ cells.push(`<rect x="${x}" y="${y}" width="${CELL_SIZE2}" height="${CELL_SIZE2}" fill="${escapeXml(fill)}" rx="3"><title>${escapeXml(title)}</title></rect>`);
1941
+ const month = current.getUTCMonth();
1942
+ if (month !== lastMonth && row === 0) {
1943
+ lastMonth = month;
1944
+ monthLabels.push(`<text x="${x}" y="${MONTH_LABEL_HEIGHT2 - 8}" fill="__MUTED__" font-size="11" font-family="${escapeXml(FONT_FAMILY2)}">${escapeXml(MONTH_NAMES2[month] ?? "")}</text>`);
1945
+ }
1946
+ if (row === 6)
1947
+ col++;
1948
+ current.setUTCDate(current.getUTCDate() + 1);
1949
+ }
1950
+ const dayLabels = [
1951
+ { label: "Mon", row: 1 },
1952
+ { label: "Wed", row: 3 },
1953
+ { label: "Fri", row: 5 },
1954
+ { label: "Sun", row: 0 }
1955
+ ].map((d) => {
1956
+ const y = MONTH_LABEL_HEIGHT2 + d.row * (CELL_SIZE2 + CELL_GAP2) + CELL_SIZE2 - 2;
1957
+ return `<text x="0" y="${y}" fill="__MUTED__" font-size="11" font-family="${escapeXml(FONT_FAMILY2)}">${escapeXml(d.label)}</text>`;
1958
+ }).join("");
1959
+ const totalCols = col + 1;
1960
+ const gridWidth = DAY_LABEL_WIDTH2 + totalCols * (CELL_SIZE2 + CELL_GAP2);
1961
+ const height = MONTH_LABEL_HEIGHT2 + 7 * (CELL_SIZE2 + CELL_GAP2);
1962
+ const svg = [dayLabels, ...monthLabels, ...cells].join(`
1963
+ `);
1964
+ return { svg, gridWidth, height };
1965
+ }
1966
+ function renderTerminalCardSvg(output, options) {
1967
+ const theme = getCardTheme(options.theme);
1968
+ const isDark = options.theme === "dark";
1969
+ const pad = CARD_PADDING;
1970
+ const stats = output.aggregated;
1971
+ const { since, until } = output.dateRange;
1972
+ const providers = output.providers;
1973
+ const providerHeatmaps = providers.map((p) => {
1974
+ const heatmapColors = buildHeatmapScale(p.colors, isDark);
1975
+ return {
1976
+ provider: p,
1977
+ heatmap: renderProviderHeatmap(p.daily, since, until, heatmapColors, theme.heatmapEmpty),
1978
+ heatmapColors
1979
+ };
1980
+ });
1981
+ const maxHeatmapWidth = providerHeatmaps.reduce((max, ph) => Math.max(max, ph.heatmap.gridWidth), 0);
1982
+ const minContentWidth = Math.max(maxHeatmapWidth, 700);
1983
+ const cardWidth = minContentWidth + pad * 2;
1984
+ const contentWidth = cardWidth - pad * 2;
1985
+ let y = 0;
1986
+ const sections = [];
1987
+ sections.push(`<defs><style>@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&amp;display=swap');</style></defs>`);
1988
+ sections.push(`<rect width="${cardWidth}" height="__CARD_HEIGHT__" rx="12" fill="${escapeXml(theme.bg)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
1989
+ sections.push(`<clipPath id="titlebar-clip"><rect width="${cardWidth}" height="${TITLEBAR_HEIGHT}" rx="12"/></clipPath>`);
1990
+ sections.push(`<rect width="${cardWidth}" height="${TITLEBAR_HEIGHT}" fill="${escapeXml(theme.bg)}" clip-path="url(#titlebar-clip)"/>`);
1991
+ const dotY = TITLEBAR_HEIGHT / 2;
1992
+ const dotStartX = pad;
1993
+ const dots = [
1994
+ { color: "#ff5f57", cx: dotStartX },
1995
+ { color: "#febc2e", cx: dotStartX + DOT_RADIUS * 2 + DOT_GAP },
1996
+ { color: "#28c840", cx: dotStartX + (DOT_RADIUS * 2 + DOT_GAP) * 2 }
1997
+ ];
1998
+ for (const dot of dots) {
1999
+ sections.push(`<circle cx="${dot.cx}" cy="${dotY}" r="${DOT_RADIUS}" fill="${escapeXml(dot.color)}"/>`);
2000
+ }
2001
+ const titleX = dots[2].cx + DOT_RADIUS + 20;
2002
+ sections.push(`<text x="${titleX}" y="${dotY + 5}" fill="${escapeXml(theme.muted)}" font-size="13" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="500">${escapeXml("tokenleak")}</text>`);
2003
+ sections.push(`<line x1="0" y1="${TITLEBAR_HEIGHT}" x2="${cardWidth}" y2="${TITLEBAR_HEIGHT}" stroke="${escapeXml(theme.titlebarBorder)}" stroke-width="1"/>`);
2004
+ y = TITLEBAR_HEIGHT + pad * 0.6;
2005
+ sections.push(`<text x="${pad}" y="${y + 16}" font-size="15" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="500">` + `<tspan fill="${escapeXml(theme.accent)}">$</tspan>` + `<tspan fill="${escapeXml(theme.fg)}"> tokenleak</tspan>` + `<tspan fill="${escapeXml(theme.accent)}">_</tspan>` + `</text>`);
2006
+ y += 40;
2007
+ const dateRangeText = formatDateRange(since, until);
2008
+ sections.push(`<text x="${pad}" y="${y + 14}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="600" letter-spacing="2">${escapeXml(dateRangeText)}</text>`);
2009
+ y += 40;
2010
+ for (let pi = 0;pi < providerHeatmaps.length; pi++) {
2011
+ const { provider, heatmap, heatmapColors } = providerHeatmaps[pi];
2012
+ const provDotRadius = 5;
2013
+ const provColor = provider.colors.primary;
2014
+ sections.push(`<circle cx="${pad + provDotRadius}" cy="${y + 8}" r="${provDotRadius}" fill="${escapeXml(provColor)}"/>`);
2015
+ sections.push(`<text x="${pad + provDotRadius * 2 + 10}" y="${y + 13}" fill="${escapeXml(theme.fg)}" font-size="14" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="600">${escapeXml(provider.displayName)}</text>`);
2016
+ const summaryText = `${formatNumber(provider.totalTokens)} tokens \xB7 ${formatCost(provider.totalCost)}`;
2017
+ sections.push(`<text x="${cardWidth - pad}" y="${y + 13}" fill="${escapeXml(theme.muted)}" font-size="11" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="500" text-anchor="end">${escapeXml(summaryText)}</text>`);
2018
+ y += 28;
2019
+ const heatmapSvg = heatmap.svg.replace(/__MUTED__/g, escapeXml(theme.muted));
2020
+ sections.push(`<g transform="translate(${pad}, ${y})">`);
2021
+ sections.push(heatmapSvg);
2022
+ sections.push("</g>");
2023
+ y += heatmap.height;
2024
+ if (pi < providerHeatmaps.length - 1) {
2025
+ y += 12;
2026
+ sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
2027
+ y += PROVIDER_SECTION_GAP - 12;
2028
+ } else {
2029
+ y += 24;
2030
+ }
2031
+ }
2032
+ if (providers.length === 0) {
2033
+ sections.push(`<text x="${pad}" y="${y + 14}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="500">${escapeXml("No provider data")}</text>`);
2034
+ y += 32;
2035
+ }
2036
+ sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
2037
+ y += 28;
2038
+ if (providers.length > 1) {
2039
+ sections.push(`<text x="${pad}" y="${y}" fill="${escapeXml(theme.muted)}" font-size="10" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="600" letter-spacing="2">${escapeXml("OVERALL")}</text>`);
2040
+ y += 24;
2041
+ }
2042
+ const statColWidth = contentWidth / STAT_GRID_COLS;
2043
+ const statsRow1 = [
2044
+ { label: "CURRENT STREAK", value: formatStreak(stats.currentStreak), accent: true },
2045
+ { label: "LONGEST STREAK", value: formatStreak(stats.longestStreak), accent: false },
2046
+ { label: "TOTAL TOKENS", value: formatNumber(stats.totalTokens), accent: true }
2047
+ ];
2048
+ const statsRow2 = [
2049
+ { label: "TOTAL COST", value: formatCost(stats.totalCost), accent: false },
2050
+ { label: "30-DAY TOKENS", value: formatNumber(stats.rolling30dTokens), accent: false },
2051
+ { label: "CACHE HIT RATE", value: formatPercentage(stats.cacheHitRate), accent: false }
2052
+ ];
2053
+ function renderStatRow(row, startY) {
2054
+ for (let i = 0;i < row.length; i++) {
2055
+ const stat = row[i];
2056
+ const x = pad + i * statColWidth;
2057
+ sections.push(`<text x="${x}" y="${startY}" fill="${escapeXml(theme.muted)}" font-size="10" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="600" letter-spacing="1.5">${escapeXml(stat.label)}</text>`);
2058
+ const valueColor = stat.accent ? theme.accent : theme.fg;
2059
+ sections.push(`<text x="${x}" y="${startY + 28}" fill="${escapeXml(valueColor)}" font-size="22" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="700">${escapeXml(stat.value)}</text>`);
2060
+ }
2061
+ }
2062
+ renderStatRow(statsRow1, y);
2063
+ y += 60;
2064
+ renderStatRow(statsRow2, y);
2065
+ y += 60;
2066
+ y += 8;
2067
+ sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
2068
+ y += 28;
2069
+ sections.push(`<text x="${pad}" y="${y}" fill="${escapeXml(theme.muted)}" font-size="10" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="600" letter-spacing="2">${escapeXml("TOP MODELS")}</text>`);
2070
+ y += 24;
2071
+ const topModels2 = stats.topModels.slice(0, 3);
2072
+ const modelNameWidth = 200;
2073
+ const percentWidth = 60;
2074
+ const barMaxWidth = contentWidth - modelNameWidth - percentWidth - 20;
2075
+ for (const model of topModels2) {
2076
+ const barWidth = Math.max(4, model.percentage / 100 * barMaxWidth);
2077
+ sections.push(`<text x="${pad}" y="${y + MODEL_BAR_HEIGHT + 4}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="400">${escapeXml(model.model)}</text>`);
2078
+ const barX = pad + modelNameWidth;
2079
+ sections.push(`<rect x="${barX}" y="${y}" width="${barMaxWidth}" height="${MODEL_BAR_HEIGHT}" rx="4" fill="${escapeXml(theme.barTrack)}"/>`);
2080
+ const gradId = `grad-${model.model.replace(/[^a-zA-Z0-9]/g, "")}`;
2081
+ sections.push(`<defs><linearGradient id="${escapeXml(gradId)}" x1="0%" y1="0%" x2="100%" y2="0%">` + `<stop offset="0%" stop-color="${escapeXml(theme.accent)}44"/>` + `<stop offset="100%" stop-color="${escapeXml(theme.accent)}"/>` + `</linearGradient></defs>`);
2082
+ sections.push(`<rect x="${barX}" y="${y}" width="${barWidth}" height="${MODEL_BAR_HEIGHT}" rx="4" fill="url(#${escapeXml(gradId)})"/>`);
2083
+ sections.push(`<text x="${barX + barMaxWidth + 12}" y="${y + MODEL_BAR_HEIGHT + 4}" fill="${escapeXml(theme.muted)}" font-size="12" font-family="${escapeXml(FONT_FAMILY2)}" font-weight="500" text-anchor="end">${escapeXml(`${model.percentage.toFixed(0)}%`)}</text>`);
2084
+ y += 32;
2085
+ }
2086
+ y += pad * 0.5;
2087
+ const cardHeight = y;
2088
+ const svg = sections.join(`
2089
+ `).replace("__CARD_HEIGHT__", String(cardHeight));
2090
+ return [
2091
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${cardWidth}" height="${cardHeight}" viewBox="0 0 ${cardWidth} ${cardHeight}">`,
2092
+ svg,
2093
+ "</svg>"
2094
+ ].join(`
2095
+ `);
2096
+ }
2097
+
1776
2098
  // packages/renderers/dist/png/png-renderer.js
1777
2099
  import sharp from "sharp";
1778
2100
 
1779
2101
  class PngRenderer {
1780
2102
  format = "png";
1781
- svgRenderer = new SvgRenderer;
1782
2103
  async render(output, options) {
1783
- const svgString = await this.svgRenderer.render(output, options);
2104
+ const svgString = renderTerminalCardSvg(output, options);
1784
2105
  const pngBuffer = await sharp(Buffer.from(svgString)).png().toBuffer();
1785
2106
  return pngBuffer;
1786
2107
  }
@@ -1837,7 +2158,7 @@ function intensityColor(value, max) {
1837
2158
  // packages/renderers/dist/terminal/heatmap.js
1838
2159
  var DAY_LABELS3 = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1839
2160
  var MONTH_LABELS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1840
- var DAY_LABEL_WIDTH2 = 4;
2161
+ var DAY_LABEL_WIDTH3 = 4;
1841
2162
  var LEGEND_TEXT = "Less";
1842
2163
  var LEGEND_TEXT_MORE = "More";
1843
2164
  function buildUsageMap(daily) {
@@ -1868,11 +2189,11 @@ function renderTerminalHeatmap(daily, options) {
1868
2189
  }
1869
2190
  weeks.push(week);
1870
2191
  }
1871
- const availableWidth = options.width - DAY_LABEL_WIDTH2;
2192
+ const availableWidth = options.width - DAY_LABEL_WIDTH3;
1872
2193
  const maxWeeks = Math.min(weeks.length, availableWidth);
1873
2194
  const displayWeeks = weeks.slice(Math.max(0, weeks.length - maxWeeks));
1874
2195
  const lines = [];
1875
- let monthHeader = " ".repeat(DAY_LABEL_WIDTH2);
2196
+ let monthHeader = " ".repeat(DAY_LABEL_WIDTH3);
1876
2197
  let lastMonth = -1;
1877
2198
  for (const week of displayWeeks) {
1878
2199
  const month = week[0].getMonth();
@@ -1891,7 +2212,7 @@ function renderTerminalHeatmap(daily, options) {
1891
2212
  for (let dayIdx = 0;dayIdx < 7; dayIdx++) {
1892
2213
  const label = dayIdx % 2 === 1 ? DAY_LABELS3[dayIdx] : " ";
1893
2214
  let line = label + " ";
1894
- line = line.slice(0, DAY_LABEL_WIDTH2);
2215
+ line = line.slice(0, DAY_LABEL_WIDTH3);
1895
2216
  for (const week of displayWeeks) {
1896
2217
  const date = week[dayIdx];
1897
2218
  if (!date || date > endDate || date < startDate) {
@@ -1913,7 +2234,7 @@ function renderTerminalHeatmap(daily, options) {
1913
2234
  HEATMAP_BLOCKS.DARK,
1914
2235
  HEATMAP_BLOCKS.FULL
1915
2236
  ];
1916
- const legend = `${" ".repeat(DAY_LABEL_WIDTH2)}${LEGEND_TEXT} ${legendBlocks.join("")} ${LEGEND_TEXT_MORE}`;
2237
+ const legend = `${" ".repeat(DAY_LABEL_WIDTH3)}${LEGEND_TEXT} ${legendBlocks.join("")} ${LEGEND_TEXT_MORE}`;
1917
2238
  lines.push(legend);
1918
2239
  return lines.join(`
1919
2240
  `);
@@ -2130,6 +2451,441 @@ class TerminalRenderer {
2130
2451
  return renderDashboard(output, effectiveOptions);
2131
2452
  }
2132
2453
  }
2454
+ // packages/renderers/dist/live/template.js
2455
+ var MONTH_NAMES3 = [
2456
+ "Jan",
2457
+ "Feb",
2458
+ "Mar",
2459
+ "Apr",
2460
+ "May",
2461
+ "Jun",
2462
+ "Jul",
2463
+ "Aug",
2464
+ "Sep",
2465
+ "Oct",
2466
+ "Nov",
2467
+ "Dec"
2468
+ ];
2469
+ var MONTH_NAMES_FULL2 = [
2470
+ "JAN",
2471
+ "FEB",
2472
+ "MAR",
2473
+ "APR",
2474
+ "MAY",
2475
+ "JUN",
2476
+ "JUL",
2477
+ "AUG",
2478
+ "SEP",
2479
+ "OCT",
2480
+ "NOV",
2481
+ "DEC"
2482
+ ];
2483
+ function formatDateRange2(since, until) {
2484
+ const s = new Date(since + "T00:00:00Z");
2485
+ const u = new Date(until + "T00:00:00Z");
2486
+ const diffMs = u.getTime() - s.getTime();
2487
+ const days = Math.round(diffMs / (1000 * 60 * 60 * 24));
2488
+ const sMonth = MONTH_NAMES_FULL2[s.getUTCMonth()] ?? "";
2489
+ const uMonth = MONTH_NAMES_FULL2[u.getUTCMonth()] ?? "";
2490
+ return `${sMonth} ${s.getUTCFullYear()} &mdash; ${uMonth} ${u.getUTCFullYear()} &middot; ${days} DAYS`;
2491
+ }
2492
+ function formatPercentage2(rate) {
2493
+ return `${(rate * 100).toFixed(1)}%`;
2494
+ }
2495
+ function formatStreak2(n) {
2496
+ return `${n} day${n !== 1 ? "s" : ""}`;
2497
+ }
2498
+ function esc(s) {
2499
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2500
+ }
2501
+ function computeQuantiles3(values) {
2502
+ const nonZero = values.filter((v) => v > 0).sort((a, b) => a - b);
2503
+ if (nonZero.length === 0)
2504
+ return [0, 0, 0];
2505
+ const q = (p) => {
2506
+ const idx = Math.floor(p * (nonZero.length - 1));
2507
+ return nonZero[idx] ?? 0;
2508
+ };
2509
+ return [q(0.25), q(0.5), q(0.75)];
2510
+ }
2511
+ function getLevel3(tokens, quantiles) {
2512
+ if (tokens <= 0)
2513
+ return 0;
2514
+ if (tokens <= quantiles[0])
2515
+ return 1;
2516
+ if (tokens <= quantiles[1])
2517
+ return 2;
2518
+ if (tokens <= quantiles[2])
2519
+ return 3;
2520
+ return 4;
2521
+ }
2522
+ function hexToRgb2(hex) {
2523
+ const h = hex.replace("#", "");
2524
+ return {
2525
+ r: parseInt(h.slice(0, 2), 16),
2526
+ g: parseInt(h.slice(2, 4), 16),
2527
+ b: parseInt(h.slice(4, 6), 16)
2528
+ };
2529
+ }
2530
+ function rgbToHex2(r, g, b) {
2531
+ const toHex = (n) => n.toString(16).padStart(2, "0");
2532
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
2533
+ }
2534
+ function buildHeatmapScale2(colors, isDark) {
2535
+ const [startHex, endHex] = colors.gradient;
2536
+ const s = hexToRgb2(startHex);
2537
+ const e = hexToRgb2(endHex);
2538
+ const opacities = isDark ? [0.15, 0.35, 0.6, 1] : [0.2, 0.4, 0.65, 1];
2539
+ return [
2540
+ "transparent",
2541
+ ...opacities.map((t) => {
2542
+ const r = Math.round(s.r + (e.r - s.r) * t);
2543
+ const g = Math.round(s.g + (e.g - s.g) * t);
2544
+ const b = Math.round(s.b + (e.b - s.b) * t);
2545
+ return rgbToHex2(r, g, b);
2546
+ })
2547
+ ];
2548
+ }
2549
+ function buildHeatmapCells(daily, since, until) {
2550
+ const tokenMap = new Map;
2551
+ for (const d of daily) {
2552
+ tokenMap.set(d.date, (tokenMap.get(d.date) ?? 0) + d.totalTokens);
2553
+ }
2554
+ const dates = daily.map((d) => d.date).sort();
2555
+ const endStr = until ?? dates[dates.length - 1] ?? new Date().toISOString().slice(0, 10);
2556
+ const startStr = since ?? dates[0] ?? endStr;
2557
+ const end = new Date(endStr + "T00:00:00Z");
2558
+ const start = new Date(startStr + "T00:00:00Z");
2559
+ start.setUTCDate(start.getUTCDate() - start.getUTCDay());
2560
+ const allTokens = Array.from(tokenMap.values());
2561
+ const quantiles = computeQuantiles3(allTokens);
2562
+ const cells = [];
2563
+ const months = [];
2564
+ let lastMonth = -1;
2565
+ let col = 0;
2566
+ const current = new Date(start);
2567
+ while (current <= end) {
2568
+ const row = current.getUTCDay();
2569
+ const dateStr = current.toISOString().slice(0, 10);
2570
+ const tokens = tokenMap.get(dateStr) ?? 0;
2571
+ const level = getLevel3(tokens, quantiles);
2572
+ cells.push({ date: dateStr, tokens, level, row, col });
2573
+ const month = current.getUTCMonth();
2574
+ if (month !== lastMonth && row === 0) {
2575
+ lastMonth = month;
2576
+ months.push({ label: MONTH_NAMES3[month] ?? "", col });
2577
+ }
2578
+ if (row === 6)
2579
+ col++;
2580
+ current.setUTCDate(current.getUTCDate() + 1);
2581
+ }
2582
+ return { cells, months, totalCols: col + 1 };
2583
+ }
2584
+ function renderProviderHeatmapHtml(provider, since, until, isDark, emptyCell) {
2585
+ const heatmapColors = buildHeatmapScale2(provider.colors, isDark);
2586
+ const { cells, months, totalCols } = buildHeatmapCells(provider.daily, since, until);
2587
+ const cellSize = 16;
2588
+ const cellGap = 4;
2589
+ const dayLabelWidth = 44;
2590
+ const monthLabelHeight = 24;
2591
+ const heatmapWidth = dayLabelWidth + totalCols * (cellSize + cellGap);
2592
+ const heatmapHeight = monthLabelHeight + 7 * (cellSize + cellGap);
2593
+ const cellsHtml = cells.map((c) => {
2594
+ const x = dayLabelWidth + c.col * (cellSize + cellGap);
2595
+ const y = monthLabelHeight + c.row * (cellSize + cellGap);
2596
+ const fill = c.level === 0 ? emptyCell : heatmapColors[c.level];
2597
+ return `<div class="heatmap-cell" style="left:${x}px;top:${y}px;background:${fill}" data-date="${esc(c.date)}" data-tokens="${c.tokens}"></div>`;
2598
+ }).join(`
2599
+ `);
2600
+ const monthLabelsHtml = months.map((m) => {
2601
+ const x = dayLabelWidth + m.col * (cellSize + cellGap);
2602
+ return `<span class="month-label" style="left:${x}px">${esc(m.label)}</span>`;
2603
+ }).join(`
2604
+ `);
2605
+ const dayLabelsHtml = [
2606
+ { label: "Mon", row: 1 },
2607
+ { label: "Wed", row: 3 },
2608
+ { label: "Fri", row: 5 },
2609
+ { label: "Sun", row: 0 }
2610
+ ].map((d) => {
2611
+ const y = monthLabelHeight + d.row * (cellSize + cellGap) + cellSize - 2;
2612
+ return `<span class="day-label" style="top:${y - 10}px">${d.label}</span>`;
2613
+ }).join(`
2614
+ `);
2615
+ const summaryText = `${esc(formatNumber(provider.totalTokens))} tokens &middot; ${esc(formatCost(provider.totalCost))}`;
2616
+ return `<div class="provider-section" data-provider="${esc(provider.provider)}">
2617
+ <div class="provider-header">
2618
+ <div class="provider-name-row">
2619
+ <span class="provider-dot" style="background:${esc(provider.colors.primary)}"></span>
2620
+ <span class="provider-name">${esc(provider.displayName)}</span>
2621
+ </div>
2622
+ <span class="provider-summary">${summaryText}</span>
2623
+ </div>
2624
+ <div class="heatmap-container" style="width:${heatmapWidth}px;height:${heatmapHeight}px">
2625
+ ${dayLabelsHtml}
2626
+ ${monthLabelsHtml}
2627
+ ${cellsHtml}
2628
+ </div>
2629
+ </div>`;
2630
+ }
2631
+ function generateHtml(output, options) {
2632
+ const isDark = options.theme === "dark";
2633
+ const stats = output.aggregated;
2634
+ const { since, until } = output.dateRange;
2635
+ const providers = output.providers;
2636
+ const bg = isDark ? "#0c0c0c" : "#fafafa";
2637
+ const fg = isDark ? "#ffffff" : "#18181b";
2638
+ const muted = isDark ? "#52525b" : "#a1a1aa";
2639
+ const accent = isDark ? "#10b981" : "#059669";
2640
+ const border = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.08)";
2641
+ const emptyCell = isDark ? "#1a1a1a" : "#e4e4e7";
2642
+ const barTrack = isDark ? "#1c1c1c" : "#e5e5e5";
2643
+ const providerSectionsHtml = providers.map((p, i) => {
2644
+ const section = renderProviderHeatmapHtml(p, since, until, isDark, emptyCell);
2645
+ const divider2 = i < providers.length - 1 ? '<hr class="provider-divider">' : "";
2646
+ return section + divider2;
2647
+ }).join(`
2648
+ `);
2649
+ const statRows = [
2650
+ [
2651
+ { label: "CURRENT STREAK", value: esc(formatStreak2(stats.currentStreak)), accent: true },
2652
+ { label: "LONGEST STREAK", value: esc(formatStreak2(stats.longestStreak)), accent: false },
2653
+ { label: "TOTAL TOKENS", value: esc(formatNumber(stats.totalTokens)), accent: true }
2654
+ ],
2655
+ [
2656
+ { label: "TOTAL COST", value: esc(formatCost(stats.totalCost)), accent: false },
2657
+ { label: "30-DAY TOKENS", value: esc(formatNumber(stats.rolling30dTokens)), accent: false },
2658
+ { label: "CACHE HIT RATE", value: esc(formatPercentage2(stats.cacheHitRate)), accent: false }
2659
+ ]
2660
+ ];
2661
+ const statsHtml = statRows.map((row) => `<div class="stat-row">${row.map((s) => `<div class="stat"><div class="stat-label">${s.label}</div><div class="stat-value${s.accent ? " accent" : ""}">${s.value}</div></div>`).join("")}</div>`).join("");
2662
+ const topModels2 = stats.topModels.slice(0, 3);
2663
+ const modelsHtml = topModels2.map((m) => {
2664
+ const width = Math.max(2, m.percentage);
2665
+ return `<div class="model-row">
2666
+ <span class="model-name">${esc(m.model)}</span>
2667
+ <div class="model-bar-track"><div class="model-bar-fill" style="width:${width}%"></div></div>
2668
+ <span class="model-pct">${m.percentage.toFixed(0)}%</span>
2669
+ </div>`;
2670
+ }).join("");
2671
+ const overallLabel = providers.length > 1 ? '<div class="overall-label">OVERALL</div>' : "";
2672
+ return `<!DOCTYPE html>
2673
+ <html lang="en">
2674
+ <head>
2675
+ <meta charset="utf-8">
2676
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2677
+ <title>tokenleak \u2014 live dashboard</title>
2678
+ <link rel="preconnect" href="https://fonts.googleapis.com">
2679
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
2680
+ <style>
2681
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2682
+ body {
2683
+ font-family: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
2684
+ background: ${isDark ? "#000" : "#f4f4f5"};
2685
+ display: flex;
2686
+ justify-content: center;
2687
+ align-items: flex-start;
2688
+ padding: 48px 24px;
2689
+ min-height: 100vh;
2690
+ }
2691
+ .card {
2692
+ background: ${bg};
2693
+ border-radius: 12px;
2694
+ border: 1px solid ${border};
2695
+ box-shadow: 0 20px 60px -12px ${isDark ? "rgba(0,0,0,0.7)" : "rgba(0,0,0,0.15)"}, 0 0 80px ${isDark ? "rgba(16,185,129,0.1)" : "rgba(5,150,105,0.08)"};
2696
+ max-width: 900px;
2697
+ width: 100%;
2698
+ overflow: hidden;
2699
+ }
2700
+ .titlebar {
2701
+ display: flex;
2702
+ align-items: center;
2703
+ height: 48px;
2704
+ padding: 0 20px;
2705
+ border-bottom: 1px solid ${border};
2706
+ gap: 8px;
2707
+ }
2708
+ .dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
2709
+ .dot-red { background: #ff5f57; }
2710
+ .dot-yellow { background: #febc2e; }
2711
+ .dot-green { background: #28c840; }
2712
+ .titlebar-label { color: ${muted}; font-size: 13px; font-weight: 500; margin-left: 12px; }
2713
+ .content { padding: 28px 48px 48px; }
2714
+ .prompt { font-size: 15px; font-weight: 500; margin-bottom: 24px; }
2715
+ .prompt .dollar { color: ${accent}; }
2716
+ .prompt .cmd { color: ${fg}; }
2717
+ .prompt .cursor { color: ${accent}; animation: blink 1s step-end infinite; }
2718
+ @keyframes blink { 50% { opacity: 0; } }
2719
+ .date-range { color: ${muted}; font-size: 12px; font-weight: 600; letter-spacing: 2px; margin-bottom: 24px; }
2720
+
2721
+ /* Provider sections */
2722
+ .provider-section { margin-bottom: 12px; }
2723
+ .provider-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
2724
+ .provider-name-row { display: flex; align-items: center; gap: 10px; }
2725
+ .provider-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
2726
+ .provider-name { color: ${fg}; font-size: 14px; font-weight: 600; }
2727
+ .provider-summary { color: ${muted}; font-size: 11px; font-weight: 500; }
2728
+ .provider-divider { border: none; border-top: 1px solid ${border}; margin: 24px 0; }
2729
+
2730
+ .heatmap-container { position: relative; margin-bottom: 8px; }
2731
+ .heatmap-cell {
2732
+ position: absolute;
2733
+ width: 16px;
2734
+ height: 16px;
2735
+ border-radius: 3px;
2736
+ cursor: pointer;
2737
+ }
2738
+ .heatmap-cell:hover { outline: 2px solid ${accent}; outline-offset: 1px; }
2739
+ .month-label { position: absolute; top: 0; color: ${muted}; font-size: 11px; }
2740
+ .day-label { position: absolute; left: 0; color: ${muted}; font-size: 11px; }
2741
+ .tooltip {
2742
+ display: none;
2743
+ position: fixed;
2744
+ background: ${isDark ? "#1c1c1c" : "#fff"};
2745
+ color: ${fg};
2746
+ border: 1px solid ${border};
2747
+ border-radius: 6px;
2748
+ padding: 6px 10px;
2749
+ font-size: 11px;
2750
+ pointer-events: none;
2751
+ z-index: 100;
2752
+ white-space: nowrap;
2753
+ box-shadow: 0 4px 12px ${isDark ? "rgba(0,0,0,0.5)" : "rgba(0,0,0,0.1)"};
2754
+ }
2755
+
2756
+ .divider { border: none; border-top: 1px solid ${border}; margin: 0 0 28px; }
2757
+ .overall-label { color: ${muted}; font-size: 10px; font-weight: 600; letter-spacing: 2px; margin-bottom: 16px; }
2758
+ .stat-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 20px; }
2759
+ .stat-label { color: ${muted}; font-size: 10px; font-weight: 600; letter-spacing: 1.5px; margin-bottom: 8px; }
2760
+ .stat-value { color: ${fg}; font-size: 22px; font-weight: 700; }
2761
+ .stat-value.accent { color: ${accent}; }
2762
+ .models-section { margin-top: 8px; }
2763
+ .models-label { color: ${muted}; font-size: 10px; font-weight: 600; letter-spacing: 2px; margin-bottom: 16px; }
2764
+ .model-row { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
2765
+ .model-name { color: ${muted}; font-size: 12px; width: 200px; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2766
+ .model-bar-track { flex: 1; height: 8px; background: ${barTrack}; border-radius: 4px; overflow: hidden; }
2767
+ .model-bar-fill { height: 100%; border-radius: 4px; background: linear-gradient(90deg, ${accent}44, ${accent}); }
2768
+ .model-pct { color: ${muted}; font-size: 12px; width: 40px; text-align: right; flex-shrink: 0; }
2769
+ .refresh-btn {
2770
+ display: inline-flex;
2771
+ align-items: center;
2772
+ gap: 6px;
2773
+ margin-top: 24px;
2774
+ padding: 8px 16px;
2775
+ background: ${isDark ? "#1c1c1c" : "#e5e5e5"};
2776
+ color: ${muted};
2777
+ border: 1px solid ${border};
2778
+ border-radius: 6px;
2779
+ font-family: inherit;
2780
+ font-size: 12px;
2781
+ cursor: pointer;
2782
+ transition: background 0.15s;
2783
+ }
2784
+ .refresh-btn:hover { background: ${isDark ? "#262626" : "#d4d4d8"}; color: ${fg}; }
2785
+ </style>
2786
+ </head>
2787
+ <body>
2788
+ <div class="card">
2789
+ <div class="titlebar">
2790
+ <div class="dot dot-red"></div>
2791
+ <div class="dot dot-yellow"></div>
2792
+ <div class="dot dot-green"></div>
2793
+ <span class="titlebar-label">tokenleak</span>
2794
+ </div>
2795
+ <div class="content">
2796
+ <div class="prompt">
2797
+ <span class="dollar">$</span>
2798
+ <span class="cmd"> tokenleak</span>
2799
+ <span class="cursor">_</span>
2800
+ </div>
2801
+ <div class="date-range">${formatDateRange2(since, until)}</div>
2802
+ ${providerSectionsHtml}
2803
+ <hr class="divider">
2804
+ ${overallLabel}
2805
+ ${statsHtml}
2806
+ <hr class="divider" style="margin-top:8px">
2807
+ <div class="models-section">
2808
+ <div class="models-label">TOP MODELS</div>
2809
+ ${modelsHtml}
2810
+ </div>
2811
+ <button class="refresh-btn" onclick="location.reload()">&#x21bb; Refresh</button>
2812
+ </div>
2813
+ </div>
2814
+ <div class="tooltip" id="tooltip"></div>
2815
+ <script>
2816
+ const tooltip = document.getElementById('tooltip');
2817
+ document.querySelectorAll('.heatmap-cell').forEach(cell => {
2818
+ const section = cell.closest('.provider-section');
2819
+ const provider = section ? section.dataset.provider : '';
2820
+ cell.addEventListener('mouseenter', e => {
2821
+ const date = cell.dataset.date;
2822
+ const tokens = Number(cell.dataset.tokens).toLocaleString();
2823
+ tooltip.textContent = (provider ? provider + ' \u2014 ' : '') + date + ': ' + tokens + ' tokens';
2824
+ tooltip.style.display = 'block';
2825
+ });
2826
+ cell.addEventListener('mousemove', e => {
2827
+ tooltip.style.left = (e.clientX + 12) + 'px';
2828
+ tooltip.style.top = (e.clientY - 30) + 'px';
2829
+ });
2830
+ cell.addEventListener('mouseleave', () => {
2831
+ tooltip.style.display = 'none';
2832
+ });
2833
+ });
2834
+ </script>
2835
+ </body>
2836
+ </html>`;
2837
+ }
2838
+
2839
+ // packages/renderers/dist/live/live-server.js
2840
+ function tryServe(html, port) {
2841
+ try {
2842
+ const server = Bun.serve({
2843
+ port,
2844
+ fetch(_req) {
2845
+ return new Response(html, {
2846
+ headers: { "Content-Type": "text/html; charset=utf-8" }
2847
+ });
2848
+ }
2849
+ });
2850
+ return { server, error: null };
2851
+ } catch (err) {
2852
+ return { server: null, error: err };
2853
+ }
2854
+ }
2855
+ function isAddrInUse(err) {
2856
+ if (err && typeof err === "object") {
2857
+ const obj = err;
2858
+ if (obj["code"] === "EADDRINUSE")
2859
+ return true;
2860
+ }
2861
+ if (err instanceof Error) {
2862
+ const msg = err.message;
2863
+ if (msg.includes("EADDRINUSE") || msg.includes("address already in use"))
2864
+ return true;
2865
+ }
2866
+ return false;
2867
+ }
2868
+ async function startLiveServer(output, options) {
2869
+ const html = generateHtml(output, options);
2870
+ const startPort = options.port ?? 3333;
2871
+ const maxAttempts = 20;
2872
+ let port = startPort;
2873
+ for (let attempt = 0;attempt < maxAttempts; attempt++) {
2874
+ const result = tryServe(html, port);
2875
+ if (result.server) {
2876
+ const actualPort = result.server.port ?? port;
2877
+ process.stderr.write(`Server running at http://localhost:${String(actualPort)}
2878
+ `);
2879
+ return { port: actualPort, stop: () => result.server.stop(true) };
2880
+ }
2881
+ if (isAddrInUse(result.error)) {
2882
+ port++;
2883
+ continue;
2884
+ }
2885
+ throw result.error;
2886
+ }
2887
+ throw new Error(`Could not find a free port after ${maxAttempts} attempts starting from ${startPort}`);
2888
+ }
2133
2889
  // packages/cli/src/config.ts
2134
2890
  import { readFileSync as readFileSync2 } from "fs";
2135
2891
  import { join as join4 } from "path";
@@ -2332,7 +3088,8 @@ function resolveConfig(cliArgs) {
2332
3088
  noColor: false,
2333
3089
  noInsights: false,
2334
3090
  clipboard: false,
2335
- open: false
3091
+ open: false,
3092
+ liveServer: false
2336
3093
  };
2337
3094
  if (fileConfig.format && FORMAT_VALUES.includes(fileConfig.format)) {
2338
3095
  merged.format = fileConfig.format;
@@ -2404,6 +3161,9 @@ function resolveConfig(cliArgs) {
2404
3161
  if (cliArgs["upload"] !== undefined) {
2405
3162
  result.upload = cliArgs["upload"];
2406
3163
  }
3164
+ if (cliArgs["liveServer"] !== undefined) {
3165
+ result.liveServer = cliArgs["liveServer"];
3166
+ }
2407
3167
  return result;
2408
3168
  }
2409
3169
  function getRenderer(format) {
@@ -2500,6 +3260,29 @@ async function run(cliArgs) {
2500
3260
  providers: providerDataList,
2501
3261
  aggregated: stats
2502
3262
  };
3263
+ if (config.liveServer) {
3264
+ const renderOptions2 = {
3265
+ format: config.format,
3266
+ theme: config.theme,
3267
+ width: config.width,
3268
+ showInsights: !config.noInsights,
3269
+ noColor: config.noColor,
3270
+ output: config.output
3271
+ };
3272
+ const { port } = await startLiveServer(output, renderOptions2);
3273
+ await new Promise((resolve) => {
3274
+ process.on("SIGINT", () => {
3275
+ process.stderr.write(`
3276
+ Shutting down server...
3277
+ `);
3278
+ resolve();
3279
+ });
3280
+ process.on("SIGTERM", () => {
3281
+ resolve();
3282
+ });
3283
+ });
3284
+ return;
3285
+ }
2503
3286
  const renderer = getRenderer(config.format);
2504
3287
  const renderOptions = {
2505
3288
  format: config.format,
@@ -2618,6 +3401,12 @@ var main = defineCommand({
2618
3401
  upload: {
2619
3402
  type: "string",
2620
3403
  description: "Upload output to a service (supported: gist)"
3404
+ },
3405
+ liveServer: {
3406
+ type: "boolean",
3407
+ alias: "L",
3408
+ description: "Start a local server with an interactive dashboard",
3409
+ default: false
2621
3410
  }
2622
3411
  },
2623
3412
  async run({ args }) {
@@ -2651,6 +3440,8 @@ var main = defineCommand({
2651
3440
  cliArgs["open"] = true;
2652
3441
  if (args.upload !== undefined)
2653
3442
  cliArgs["upload"] = args.upload;
3443
+ if (args.liveServer)
3444
+ cliArgs["liveServer"] = true;
2654
3445
  await run(cliArgs);
2655
3446
  } catch (error) {
2656
3447
  handleError(error);