tokenleak 0.4.0 → 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.
- package/package.json +1 -1
- package/tokenleak.js +821 -21
package/package.json
CHANGED
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.
|
|
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"];
|
|
@@ -1650,7 +1659,13 @@ function renderHeatmap(daily, theme, options = {}) {
|
|
|
1650
1659
|
}
|
|
1651
1660
|
|
|
1652
1661
|
// packages/renderers/dist/svg/svg-renderer.js
|
|
1653
|
-
var MIN_SVG_WIDTH =
|
|
1662
|
+
var MIN_SVG_WIDTH = 1000;
|
|
1663
|
+
var MAX_STAT_VALUE_CHARS = 28;
|
|
1664
|
+
function truncateText(value, maxChars) {
|
|
1665
|
+
if (value.length <= maxChars)
|
|
1666
|
+
return value;
|
|
1667
|
+
return value.slice(0, maxChars - 1) + "\u2026";
|
|
1668
|
+
}
|
|
1654
1669
|
function renderHeaderStat(x, y, label, value, theme, align = "end") {
|
|
1655
1670
|
const anchor = align === "end" ? "end" : "start";
|
|
1656
1671
|
return group([
|
|
@@ -1682,7 +1697,7 @@ function renderBottomStat(x, y, label, value, theme) {
|
|
|
1682
1697
|
}),
|
|
1683
1698
|
text(x, y + 32, value, {
|
|
1684
1699
|
fill: theme.foreground,
|
|
1685
|
-
"font-size":
|
|
1700
|
+
"font-size": 18,
|
|
1686
1701
|
"font-family": FONT_FAMILY,
|
|
1687
1702
|
"font-weight": "700"
|
|
1688
1703
|
})
|
|
@@ -1728,21 +1743,24 @@ class SvgRenderer {
|
|
|
1728
1743
|
}
|
|
1729
1744
|
sections.push(`<line x1="${PADDING}" y1="${y}" x2="${PADDING + contentWidth}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
1730
1745
|
y += SECTION_GAP;
|
|
1731
|
-
const
|
|
1732
|
-
const
|
|
1746
|
+
const col1Width = contentWidth * 0.35;
|
|
1747
|
+
const col2Width = contentWidth * 0.35;
|
|
1748
|
+
const col3Width = contentWidth * 0.15;
|
|
1749
|
+
const col4Width = contentWidth * 0.15;
|
|
1733
1750
|
const topModel = stats.topModels.length > 0 ? stats.topModels[0] : null;
|
|
1734
|
-
const topModelLabel = topModel ? `${topModel.model} (${formatNumber(topModel.tokens)})
|
|
1751
|
+
const topModelLabel = topModel ? truncateText(`${topModel.model} (${formatNumber(topModel.tokens)})`, MAX_STAT_VALUE_CHARS) : "N/A";
|
|
1735
1752
|
sections.push(renderBottomStat(PADDING, y, "MOST USED MODEL", topModelLabel, theme));
|
|
1736
|
-
const recent30Label = stats.rolling30dTopModel ? `${stats.rolling30dTopModel} (${formatNumber(stats.rolling30dTokens)})
|
|
1737
|
-
sections.push(renderBottomStat(PADDING +
|
|
1738
|
-
sections.push(renderBottomStat(PADDING +
|
|
1739
|
-
sections.push(renderBottomStat(PADDING +
|
|
1753
|
+
const recent30Label = stats.rolling30dTopModel ? truncateText(`${stats.rolling30dTopModel} (${formatNumber(stats.rolling30dTokens)})`, MAX_STAT_VALUE_CHARS) : formatNumber(stats.rolling30dTokens);
|
|
1754
|
+
sections.push(renderBottomStat(PADDING + col1Width, y, "RECENT USE (LAST 30 DAYS)", recent30Label, theme));
|
|
1755
|
+
sections.push(renderBottomStat(PADDING + col1Width + col2Width, y, "LONGEST STREAK", `${stats.longestStreak} days`, theme));
|
|
1756
|
+
sections.push(renderBottomStat(PADDING + col1Width + col2Width + col3Width, y, "CURRENT STREAK", `${stats.currentStreak} days`, theme));
|
|
1740
1757
|
y += 56 + SECTION_GAP;
|
|
1758
|
+
const evenCardWidth = contentWidth / 4;
|
|
1741
1759
|
sections.push(`<line x1="${PADDING}" y1="${y - SECTION_GAP / 2}" x2="${PADDING + contentWidth}" y2="${y - SECTION_GAP / 2}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
1742
1760
|
sections.push(renderBottomStat(PADDING, y, "TOTAL COST", stats.totalCost >= 100 ? `$${stats.totalCost.toFixed(0)}` : `$${stats.totalCost.toFixed(2)}`, theme));
|
|
1743
|
-
sections.push(renderBottomStat(PADDING +
|
|
1744
|
-
sections.push(renderBottomStat(PADDING +
|
|
1745
|
-
sections.push(renderBottomStat(PADDING +
|
|
1761
|
+
sections.push(renderBottomStat(PADDING + evenCardWidth, y, "CACHE HIT RATE", `${(stats.cacheHitRate * 100).toFixed(1)}%`, theme));
|
|
1762
|
+
sections.push(renderBottomStat(PADDING + evenCardWidth * 2, y, "ACTIVE DAYS", `${stats.activeDays} / ${stats.totalDays}`, theme));
|
|
1763
|
+
sections.push(renderBottomStat(PADDING + evenCardWidth * 3, y, "AVG DAILY TOKENS", formatNumber(stats.averageDailyTokens), theme));
|
|
1746
1764
|
y += 56 + PADDING;
|
|
1747
1765
|
const totalHeight = y;
|
|
1748
1766
|
const svgWidth = Math.max(contentWidth + PADDING * 2, MIN_SVG_WIDTH);
|
|
@@ -1764,14 +1782,326 @@ class SvgRenderer {
|
|
|
1764
1782
|
return svgContent;
|
|
1765
1783
|
}
|
|
1766
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&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
|
+
|
|
1767
2098
|
// packages/renderers/dist/png/png-renderer.js
|
|
1768
2099
|
import sharp from "sharp";
|
|
1769
2100
|
|
|
1770
2101
|
class PngRenderer {
|
|
1771
2102
|
format = "png";
|
|
1772
|
-
svgRenderer = new SvgRenderer;
|
|
1773
2103
|
async render(output, options) {
|
|
1774
|
-
const svgString =
|
|
2104
|
+
const svgString = renderTerminalCardSvg(output, options);
|
|
1775
2105
|
const pngBuffer = await sharp(Buffer.from(svgString)).png().toBuffer();
|
|
1776
2106
|
return pngBuffer;
|
|
1777
2107
|
}
|
|
@@ -1828,7 +2158,7 @@ function intensityColor(value, max) {
|
|
|
1828
2158
|
// packages/renderers/dist/terminal/heatmap.js
|
|
1829
2159
|
var DAY_LABELS3 = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
1830
2160
|
var MONTH_LABELS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
1831
|
-
var
|
|
2161
|
+
var DAY_LABEL_WIDTH3 = 4;
|
|
1832
2162
|
var LEGEND_TEXT = "Less";
|
|
1833
2163
|
var LEGEND_TEXT_MORE = "More";
|
|
1834
2164
|
function buildUsageMap(daily) {
|
|
@@ -1859,11 +2189,11 @@ function renderTerminalHeatmap(daily, options) {
|
|
|
1859
2189
|
}
|
|
1860
2190
|
weeks.push(week);
|
|
1861
2191
|
}
|
|
1862
|
-
const availableWidth = options.width -
|
|
2192
|
+
const availableWidth = options.width - DAY_LABEL_WIDTH3;
|
|
1863
2193
|
const maxWeeks = Math.min(weeks.length, availableWidth);
|
|
1864
2194
|
const displayWeeks = weeks.slice(Math.max(0, weeks.length - maxWeeks));
|
|
1865
2195
|
const lines = [];
|
|
1866
|
-
let monthHeader = " ".repeat(
|
|
2196
|
+
let monthHeader = " ".repeat(DAY_LABEL_WIDTH3);
|
|
1867
2197
|
let lastMonth = -1;
|
|
1868
2198
|
for (const week of displayWeeks) {
|
|
1869
2199
|
const month = week[0].getMonth();
|
|
@@ -1882,7 +2212,7 @@ function renderTerminalHeatmap(daily, options) {
|
|
|
1882
2212
|
for (let dayIdx = 0;dayIdx < 7; dayIdx++) {
|
|
1883
2213
|
const label = dayIdx % 2 === 1 ? DAY_LABELS3[dayIdx] : " ";
|
|
1884
2214
|
let line = label + " ";
|
|
1885
|
-
line = line.slice(0,
|
|
2215
|
+
line = line.slice(0, DAY_LABEL_WIDTH3);
|
|
1886
2216
|
for (const week of displayWeeks) {
|
|
1887
2217
|
const date = week[dayIdx];
|
|
1888
2218
|
if (!date || date > endDate || date < startDate) {
|
|
@@ -1904,7 +2234,7 @@ function renderTerminalHeatmap(daily, options) {
|
|
|
1904
2234
|
HEATMAP_BLOCKS.DARK,
|
|
1905
2235
|
HEATMAP_BLOCKS.FULL
|
|
1906
2236
|
];
|
|
1907
|
-
const legend = `${" ".repeat(
|
|
2237
|
+
const legend = `${" ".repeat(DAY_LABEL_WIDTH3)}${LEGEND_TEXT} ${legendBlocks.join("")} ${LEGEND_TEXT_MORE}`;
|
|
1908
2238
|
lines.push(legend);
|
|
1909
2239
|
return lines.join(`
|
|
1910
2240
|
`);
|
|
@@ -2121,6 +2451,441 @@ class TerminalRenderer {
|
|
|
2121
2451
|
return renderDashboard(output, effectiveOptions);
|
|
2122
2452
|
}
|
|
2123
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()} — ${uMonth} ${u.getUTCFullYear()} · ${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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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 · ${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()">↻ 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
|
+
}
|
|
2124
2889
|
// packages/cli/src/config.ts
|
|
2125
2890
|
import { readFileSync as readFileSync2 } from "fs";
|
|
2126
2891
|
import { join as join4 } from "path";
|
|
@@ -2323,7 +3088,8 @@ function resolveConfig(cliArgs) {
|
|
|
2323
3088
|
noColor: false,
|
|
2324
3089
|
noInsights: false,
|
|
2325
3090
|
clipboard: false,
|
|
2326
|
-
open: false
|
|
3091
|
+
open: false,
|
|
3092
|
+
liveServer: false
|
|
2327
3093
|
};
|
|
2328
3094
|
if (fileConfig.format && FORMAT_VALUES.includes(fileConfig.format)) {
|
|
2329
3095
|
merged.format = fileConfig.format;
|
|
@@ -2395,6 +3161,9 @@ function resolveConfig(cliArgs) {
|
|
|
2395
3161
|
if (cliArgs["upload"] !== undefined) {
|
|
2396
3162
|
result.upload = cliArgs["upload"];
|
|
2397
3163
|
}
|
|
3164
|
+
if (cliArgs["liveServer"] !== undefined) {
|
|
3165
|
+
result.liveServer = cliArgs["liveServer"];
|
|
3166
|
+
}
|
|
2398
3167
|
return result;
|
|
2399
3168
|
}
|
|
2400
3169
|
function getRenderer(format) {
|
|
@@ -2491,6 +3260,29 @@ async function run(cliArgs) {
|
|
|
2491
3260
|
providers: providerDataList,
|
|
2492
3261
|
aggregated: stats
|
|
2493
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
|
+
}
|
|
2494
3286
|
const renderer = getRenderer(config.format);
|
|
2495
3287
|
const renderOptions = {
|
|
2496
3288
|
format: config.format,
|
|
@@ -2609,6 +3401,12 @@ var main = defineCommand({
|
|
|
2609
3401
|
upload: {
|
|
2610
3402
|
type: "string",
|
|
2611
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
|
|
2612
3410
|
}
|
|
2613
3411
|
},
|
|
2614
3412
|
async run({ args }) {
|
|
@@ -2642,6 +3440,8 @@ var main = defineCommand({
|
|
|
2642
3440
|
cliArgs["open"] = true;
|
|
2643
3441
|
if (args.upload !== undefined)
|
|
2644
3442
|
cliArgs["upload"] = args.upload;
|
|
3443
|
+
if (args.liveServer)
|
|
3444
|
+
cliArgs["liveServer"] = true;
|
|
2645
3445
|
await run(cliArgs);
|
|
2646
3446
|
} catch (error) {
|
|
2647
3447
|
handleError(error);
|