tokenleak 0.4.1 → 1.0.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 +885 -26
package/package.json
CHANGED
package/tokenleak.js
CHANGED
|
@@ -663,6 +663,26 @@ function computeTotalDays(daily) {
|
|
|
663
663
|
return Math.round((last - first) / ONE_DAY_MS) + 1;
|
|
664
664
|
}
|
|
665
665
|
// packages/core/dist/aggregation/merge.js
|
|
666
|
+
function mergeModelArrays(existing, incoming) {
|
|
667
|
+
const map = new Map;
|
|
668
|
+
for (const m of existing) {
|
|
669
|
+
map.set(m.model, { ...m });
|
|
670
|
+
}
|
|
671
|
+
for (const m of incoming) {
|
|
672
|
+
const prev = map.get(m.model);
|
|
673
|
+
if (prev) {
|
|
674
|
+
prev.inputTokens += m.inputTokens;
|
|
675
|
+
prev.outputTokens += m.outputTokens;
|
|
676
|
+
prev.cacheReadTokens += m.cacheReadTokens;
|
|
677
|
+
prev.cacheWriteTokens += m.cacheWriteTokens;
|
|
678
|
+
prev.totalTokens += m.totalTokens;
|
|
679
|
+
prev.cost += m.cost;
|
|
680
|
+
} else {
|
|
681
|
+
map.set(m.model, { ...m });
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return [...map.values()];
|
|
685
|
+
}
|
|
666
686
|
function mergeProviderData(providers) {
|
|
667
687
|
const dateMap = new Map;
|
|
668
688
|
for (const provider of providers) {
|
|
@@ -675,7 +695,7 @@ function mergeProviderData(providers) {
|
|
|
675
695
|
existing.cacheWriteTokens += entry.cacheWriteTokens;
|
|
676
696
|
existing.totalTokens += entry.totalTokens;
|
|
677
697
|
existing.cost += entry.cost;
|
|
678
|
-
existing.models =
|
|
698
|
+
existing.models = mergeModelArrays(existing.models, entry.models);
|
|
679
699
|
} else {
|
|
680
700
|
dateMap.set(entry.date, {
|
|
681
701
|
date: entry.date,
|
|
@@ -736,7 +756,7 @@ function computePreviousPeriod(current) {
|
|
|
736
756
|
};
|
|
737
757
|
}
|
|
738
758
|
// packages/core/dist/index.js
|
|
739
|
-
var VERSION = "0.
|
|
759
|
+
var VERSION = "1.0.0";
|
|
740
760
|
|
|
741
761
|
// packages/registry/dist/models/normalizer.js
|
|
742
762
|
var DATE_SUFFIX_PATTERN = /-\d{8}$/;
|
|
@@ -1361,8 +1381,17 @@ function buildProviderData(records) {
|
|
|
1361
1381
|
};
|
|
1362
1382
|
}
|
|
1363
1383
|
function loadFromSqlite(dbPath, range) {
|
|
1364
|
-
|
|
1384
|
+
let db;
|
|
1385
|
+
try {
|
|
1386
|
+
db = new Database(dbPath, { readonly: true });
|
|
1387
|
+
} catch {
|
|
1388
|
+
return [];
|
|
1389
|
+
}
|
|
1365
1390
|
try {
|
|
1391
|
+
const tables = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='messages'").all();
|
|
1392
|
+
if (tables.length === 0) {
|
|
1393
|
+
return [];
|
|
1394
|
+
}
|
|
1366
1395
|
const rows = db.query("SELECT model, input_tokens, output_tokens, created_at FROM messages WHERE role = 'assistant'").all();
|
|
1367
1396
|
const records = [];
|
|
1368
1397
|
for (const row of rows) {
|
|
@@ -1377,6 +1406,8 @@ function loadFromSqlite(dbPath, range) {
|
|
|
1377
1406
|
}
|
|
1378
1407
|
}
|
|
1379
1408
|
return records;
|
|
1409
|
+
} catch {
|
|
1410
|
+
return [];
|
|
1380
1411
|
} finally {
|
|
1381
1412
|
db.close();
|
|
1382
1413
|
}
|
|
@@ -1385,24 +1416,28 @@ function loadFromJson(sessionsDir, range) {
|
|
|
1385
1416
|
const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
1386
1417
|
const records = [];
|
|
1387
1418
|
for (const file of files) {
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
}
|
|
1393
|
-
for (const msg of session.messages) {
|
|
1394
|
-
if (msg.role !== "assistant" || !msg.usage) {
|
|
1419
|
+
try {
|
|
1420
|
+
const content = readFileSync(join3(sessionsDir, file), "utf-8");
|
|
1421
|
+
const session = JSON.parse(content);
|
|
1422
|
+
if (!Array.isArray(session.messages)) {
|
|
1395
1423
|
continue;
|
|
1396
1424
|
}
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1425
|
+
for (const msg of session.messages) {
|
|
1426
|
+
if (msg.role !== "assistant" || !msg.usage) {
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
const date = extractDate2(msg.created_at);
|
|
1430
|
+
if (isInRange(date, range)) {
|
|
1431
|
+
records.push({
|
|
1432
|
+
date,
|
|
1433
|
+
model: msg.model,
|
|
1434
|
+
inputTokens: msg.usage.input_tokens,
|
|
1435
|
+
outputTokens: msg.usage.output_tokens
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1405
1438
|
}
|
|
1439
|
+
} catch {
|
|
1440
|
+
continue;
|
|
1406
1441
|
}
|
|
1407
1442
|
}
|
|
1408
1443
|
return records;
|
|
@@ -1526,6 +1561,15 @@ function formatNumber(n) {
|
|
|
1526
1561
|
}
|
|
1527
1562
|
return n.toFixed(0);
|
|
1528
1563
|
}
|
|
1564
|
+
function formatCost(cost) {
|
|
1565
|
+
if (cost >= 100) {
|
|
1566
|
+
return `$${cost.toFixed(0)}`;
|
|
1567
|
+
}
|
|
1568
|
+
if (cost >= 1) {
|
|
1569
|
+
return `$${cost.toFixed(2)}`;
|
|
1570
|
+
}
|
|
1571
|
+
return `$${cost.toFixed(4)}`;
|
|
1572
|
+
}
|
|
1529
1573
|
|
|
1530
1574
|
// packages/renderers/dist/svg/heatmap.js
|
|
1531
1575
|
var DAY_LABELS2 = ["Mon", "", "Wed", "", "Fri", "", "Sun"];
|
|
@@ -1773,14 +1817,326 @@ class SvgRenderer {
|
|
|
1773
1817
|
return svgContent;
|
|
1774
1818
|
}
|
|
1775
1819
|
}
|
|
1820
|
+
// packages/renderers/dist/png/terminal-card.js
|
|
1821
|
+
var CARD_PADDING = 48;
|
|
1822
|
+
var TITLEBAR_HEIGHT = 48;
|
|
1823
|
+
var DOT_RADIUS = 6;
|
|
1824
|
+
var DOT_GAP = 8;
|
|
1825
|
+
var CELL_SIZE2 = 16;
|
|
1826
|
+
var CELL_GAP2 = 4;
|
|
1827
|
+
var STAT_GRID_COLS = 3;
|
|
1828
|
+
var MODEL_BAR_HEIGHT = 8;
|
|
1829
|
+
var DAY_LABEL_WIDTH2 = 44;
|
|
1830
|
+
var MONTH_LABEL_HEIGHT2 = 24;
|
|
1831
|
+
var PROVIDER_SECTION_GAP = 36;
|
|
1832
|
+
var FONT_FAMILY2 = "'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', monospace";
|
|
1833
|
+
function getCardTheme(mode) {
|
|
1834
|
+
if (mode === "dark") {
|
|
1835
|
+
return {
|
|
1836
|
+
bg: "#0c0c0c",
|
|
1837
|
+
fg: "#ffffff",
|
|
1838
|
+
muted: "#52525b",
|
|
1839
|
+
border: "rgba(255,255,255,0.06)",
|
|
1840
|
+
accent: "#10b981",
|
|
1841
|
+
heatmapEmpty: "#1a1a1a",
|
|
1842
|
+
barTrack: "#1c1c1c",
|
|
1843
|
+
titlebarBorder: "rgba(255,255,255,0.06)"
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
return {
|
|
1847
|
+
bg: "#fafafa",
|
|
1848
|
+
fg: "#18181b",
|
|
1849
|
+
muted: "#a1a1aa",
|
|
1850
|
+
border: "rgba(0,0,0,0.08)",
|
|
1851
|
+
accent: "#059669",
|
|
1852
|
+
heatmapEmpty: "#e4e4e7",
|
|
1853
|
+
barTrack: "#e5e5e5",
|
|
1854
|
+
titlebarBorder: "rgba(0,0,0,0.08)"
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
function buildHeatmapScale(colors, isDark) {
|
|
1858
|
+
const [startHex, endHex] = colors.gradient;
|
|
1859
|
+
const s = hexToRgb(startHex);
|
|
1860
|
+
const e = hexToRgb(endHex);
|
|
1861
|
+
const opacities = isDark ? [0.15, 0.35, 0.6, 1] : [0.2, 0.4, 0.65, 1];
|
|
1862
|
+
return [
|
|
1863
|
+
"transparent",
|
|
1864
|
+
...opacities.map((t) => {
|
|
1865
|
+
const r = Math.round(s.r + (e.r - s.r) * t);
|
|
1866
|
+
const g = Math.round(s.g + (e.g - s.g) * t);
|
|
1867
|
+
const b = Math.round(s.b + (e.b - s.b) * t);
|
|
1868
|
+
return rgbToHex(r, g, b);
|
|
1869
|
+
})
|
|
1870
|
+
];
|
|
1871
|
+
}
|
|
1872
|
+
function hexToRgb(hex) {
|
|
1873
|
+
const h = hex.replace("#", "");
|
|
1874
|
+
return {
|
|
1875
|
+
r: parseInt(h.slice(0, 2), 16),
|
|
1876
|
+
g: parseInt(h.slice(2, 4), 16),
|
|
1877
|
+
b: parseInt(h.slice(4, 6), 16)
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
function rgbToHex(r, g, b) {
|
|
1881
|
+
const toHex = (n) => n.toString(16).padStart(2, "0");
|
|
1882
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
1883
|
+
}
|
|
1884
|
+
function computeQuantiles2(values) {
|
|
1885
|
+
const nonZero = values.filter((v) => v > 0).sort((a, b) => a - b);
|
|
1886
|
+
if (nonZero.length === 0)
|
|
1887
|
+
return [0, 0, 0];
|
|
1888
|
+
const q = (p) => {
|
|
1889
|
+
const idx = Math.floor(p * (nonZero.length - 1));
|
|
1890
|
+
return nonZero[idx] ?? 0;
|
|
1891
|
+
};
|
|
1892
|
+
return [q(0.25), q(0.5), q(0.75)];
|
|
1893
|
+
}
|
|
1894
|
+
function getLevel2(tokens, quantiles) {
|
|
1895
|
+
if (tokens <= 0)
|
|
1896
|
+
return 0;
|
|
1897
|
+
if (tokens <= quantiles[0])
|
|
1898
|
+
return 1;
|
|
1899
|
+
if (tokens <= quantiles[1])
|
|
1900
|
+
return 2;
|
|
1901
|
+
if (tokens <= quantiles[2])
|
|
1902
|
+
return 3;
|
|
1903
|
+
return 4;
|
|
1904
|
+
}
|
|
1905
|
+
var MONTH_NAMES2 = [
|
|
1906
|
+
"Jan",
|
|
1907
|
+
"Feb",
|
|
1908
|
+
"Mar",
|
|
1909
|
+
"Apr",
|
|
1910
|
+
"May",
|
|
1911
|
+
"Jun",
|
|
1912
|
+
"Jul",
|
|
1913
|
+
"Aug",
|
|
1914
|
+
"Sep",
|
|
1915
|
+
"Oct",
|
|
1916
|
+
"Nov",
|
|
1917
|
+
"Dec"
|
|
1918
|
+
];
|
|
1919
|
+
var MONTH_NAMES_FULL = [
|
|
1920
|
+
"JAN",
|
|
1921
|
+
"FEB",
|
|
1922
|
+
"MAR",
|
|
1923
|
+
"APR",
|
|
1924
|
+
"MAY",
|
|
1925
|
+
"JUN",
|
|
1926
|
+
"JUL",
|
|
1927
|
+
"AUG",
|
|
1928
|
+
"SEP",
|
|
1929
|
+
"OCT",
|
|
1930
|
+
"NOV",
|
|
1931
|
+
"DEC"
|
|
1932
|
+
];
|
|
1933
|
+
function formatDateRange(since, until) {
|
|
1934
|
+
const s = new Date(since + "T00:00:00Z");
|
|
1935
|
+
const u = new Date(until + "T00:00:00Z");
|
|
1936
|
+
const diffMs = u.getTime() - s.getTime();
|
|
1937
|
+
const days = Math.round(diffMs / (1000 * 60 * 60 * 24));
|
|
1938
|
+
const sMonth = MONTH_NAMES_FULL[s.getUTCMonth()] ?? "";
|
|
1939
|
+
const uMonth = MONTH_NAMES_FULL[u.getUTCMonth()] ?? "";
|
|
1940
|
+
return `${sMonth} ${s.getUTCFullYear()} \u2014 ${uMonth} ${u.getUTCFullYear()} \xB7 ${days} DAYS`;
|
|
1941
|
+
}
|
|
1942
|
+
function formatPercentage(rate) {
|
|
1943
|
+
return `${(rate * 100).toFixed(1)}%`;
|
|
1944
|
+
}
|
|
1945
|
+
function formatStreak(n) {
|
|
1946
|
+
return `${n} day${n !== 1 ? "s" : ""}`;
|
|
1947
|
+
}
|
|
1948
|
+
function renderProviderHeatmap(daily, since, until, heatmapColors, emptyColor) {
|
|
1949
|
+
const tokenMap = new Map;
|
|
1950
|
+
for (const d of daily) {
|
|
1951
|
+
tokenMap.set(d.date, (tokenMap.get(d.date) ?? 0) + d.totalTokens);
|
|
1952
|
+
}
|
|
1953
|
+
const dates = daily.map((d) => d.date).sort();
|
|
1954
|
+
const endStr = until ?? dates[dates.length - 1] ?? new Date().toISOString().slice(0, 10);
|
|
1955
|
+
const startStr = since ?? dates[0] ?? endStr;
|
|
1956
|
+
const end = new Date(endStr + "T00:00:00Z");
|
|
1957
|
+
const start = new Date(startStr + "T00:00:00Z");
|
|
1958
|
+
start.setUTCDate(start.getUTCDate() - start.getUTCDay());
|
|
1959
|
+
const allTokens = Array.from(tokenMap.values());
|
|
1960
|
+
const quantiles = computeQuantiles2(allTokens);
|
|
1961
|
+
const cells = [];
|
|
1962
|
+
const monthLabels = [];
|
|
1963
|
+
let lastMonth = -1;
|
|
1964
|
+
let col = 0;
|
|
1965
|
+
const current = new Date(start);
|
|
1966
|
+
while (current <= end) {
|
|
1967
|
+
const row = current.getUTCDay();
|
|
1968
|
+
const dateStr = current.toISOString().slice(0, 10);
|
|
1969
|
+
const tokens = tokenMap.get(dateStr) ?? 0;
|
|
1970
|
+
const level = getLevel2(tokens, quantiles);
|
|
1971
|
+
const x = DAY_LABEL_WIDTH2 + col * (CELL_SIZE2 + CELL_GAP2);
|
|
1972
|
+
const y = MONTH_LABEL_HEIGHT2 + row * (CELL_SIZE2 + CELL_GAP2);
|
|
1973
|
+
const fill = level === 0 ? emptyColor : heatmapColors[level];
|
|
1974
|
+
const title = `${dateStr}: ${tokens.toLocaleString()} tokens`;
|
|
1975
|
+
cells.push(`<rect x="${x}" y="${y}" width="${CELL_SIZE2}" height="${CELL_SIZE2}" fill="${escapeXml(fill)}" rx="3"><title>${escapeXml(title)}</title></rect>`);
|
|
1976
|
+
const month = current.getUTCMonth();
|
|
1977
|
+
if (month !== lastMonth && row === 0) {
|
|
1978
|
+
lastMonth = month;
|
|
1979
|
+
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>`);
|
|
1980
|
+
}
|
|
1981
|
+
if (row === 6)
|
|
1982
|
+
col++;
|
|
1983
|
+
current.setUTCDate(current.getUTCDate() + 1);
|
|
1984
|
+
}
|
|
1985
|
+
const dayLabels = [
|
|
1986
|
+
{ label: "Mon", row: 1 },
|
|
1987
|
+
{ label: "Wed", row: 3 },
|
|
1988
|
+
{ label: "Fri", row: 5 },
|
|
1989
|
+
{ label: "Sun", row: 0 }
|
|
1990
|
+
].map((d) => {
|
|
1991
|
+
const y = MONTH_LABEL_HEIGHT2 + d.row * (CELL_SIZE2 + CELL_GAP2) + CELL_SIZE2 - 2;
|
|
1992
|
+
return `<text x="0" y="${y}" fill="__MUTED__" font-size="11" font-family="${escapeXml(FONT_FAMILY2)}">${escapeXml(d.label)}</text>`;
|
|
1993
|
+
}).join("");
|
|
1994
|
+
const totalCols = col + 1;
|
|
1995
|
+
const gridWidth = DAY_LABEL_WIDTH2 + totalCols * (CELL_SIZE2 + CELL_GAP2);
|
|
1996
|
+
const height = MONTH_LABEL_HEIGHT2 + 7 * (CELL_SIZE2 + CELL_GAP2);
|
|
1997
|
+
const svg = [dayLabels, ...monthLabels, ...cells].join(`
|
|
1998
|
+
`);
|
|
1999
|
+
return { svg, gridWidth, height };
|
|
2000
|
+
}
|
|
2001
|
+
function renderTerminalCardSvg(output, options) {
|
|
2002
|
+
const theme = getCardTheme(options.theme);
|
|
2003
|
+
const isDark = options.theme === "dark";
|
|
2004
|
+
const pad = CARD_PADDING;
|
|
2005
|
+
const stats = output.aggregated;
|
|
2006
|
+
const { since, until } = output.dateRange;
|
|
2007
|
+
const providers = output.providers;
|
|
2008
|
+
const providerHeatmaps = providers.map((p) => {
|
|
2009
|
+
const heatmapColors = buildHeatmapScale(p.colors, isDark);
|
|
2010
|
+
return {
|
|
2011
|
+
provider: p,
|
|
2012
|
+
heatmap: renderProviderHeatmap(p.daily, since, until, heatmapColors, theme.heatmapEmpty),
|
|
2013
|
+
heatmapColors
|
|
2014
|
+
};
|
|
2015
|
+
});
|
|
2016
|
+
const maxHeatmapWidth = providerHeatmaps.reduce((max, ph) => Math.max(max, ph.heatmap.gridWidth), 0);
|
|
2017
|
+
const minContentWidth = Math.max(maxHeatmapWidth, 700);
|
|
2018
|
+
const cardWidth = minContentWidth + pad * 2;
|
|
2019
|
+
const contentWidth = cardWidth - pad * 2;
|
|
2020
|
+
let y = 0;
|
|
2021
|
+
const sections = [];
|
|
2022
|
+
sections.push(`<defs><style>@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap');</style></defs>`);
|
|
2023
|
+
sections.push(`<rect width="${cardWidth}" height="__CARD_HEIGHT__" rx="12" fill="${escapeXml(theme.bg)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
2024
|
+
sections.push(`<clipPath id="titlebar-clip"><rect width="${cardWidth}" height="${TITLEBAR_HEIGHT}" rx="12"/></clipPath>`);
|
|
2025
|
+
sections.push(`<rect width="${cardWidth}" height="${TITLEBAR_HEIGHT}" fill="${escapeXml(theme.bg)}" clip-path="url(#titlebar-clip)"/>`);
|
|
2026
|
+
const dotY = TITLEBAR_HEIGHT / 2;
|
|
2027
|
+
const dotStartX = pad;
|
|
2028
|
+
const dots = [
|
|
2029
|
+
{ color: "#ff5f57", cx: dotStartX },
|
|
2030
|
+
{ color: "#febc2e", cx: dotStartX + DOT_RADIUS * 2 + DOT_GAP },
|
|
2031
|
+
{ color: "#28c840", cx: dotStartX + (DOT_RADIUS * 2 + DOT_GAP) * 2 }
|
|
2032
|
+
];
|
|
2033
|
+
for (const dot of dots) {
|
|
2034
|
+
sections.push(`<circle cx="${dot.cx}" cy="${dotY}" r="${DOT_RADIUS}" fill="${escapeXml(dot.color)}"/>`);
|
|
2035
|
+
}
|
|
2036
|
+
const titleX = dots[2].cx + DOT_RADIUS + 20;
|
|
2037
|
+
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>`);
|
|
2038
|
+
sections.push(`<line x1="0" y1="${TITLEBAR_HEIGHT}" x2="${cardWidth}" y2="${TITLEBAR_HEIGHT}" stroke="${escapeXml(theme.titlebarBorder)}" stroke-width="1"/>`);
|
|
2039
|
+
y = TITLEBAR_HEIGHT + pad * 0.6;
|
|
2040
|
+
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>`);
|
|
2041
|
+
y += 40;
|
|
2042
|
+
const dateRangeText = formatDateRange(since, until);
|
|
2043
|
+
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>`);
|
|
2044
|
+
y += 40;
|
|
2045
|
+
for (let pi = 0;pi < providerHeatmaps.length; pi++) {
|
|
2046
|
+
const { provider, heatmap, heatmapColors } = providerHeatmaps[pi];
|
|
2047
|
+
const provDotRadius = 5;
|
|
2048
|
+
const provColor = provider.colors.primary;
|
|
2049
|
+
sections.push(`<circle cx="${pad + provDotRadius}" cy="${y + 8}" r="${provDotRadius}" fill="${escapeXml(provColor)}"/>`);
|
|
2050
|
+
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>`);
|
|
2051
|
+
const summaryText = `${formatNumber(provider.totalTokens)} tokens \xB7 ${formatCost(provider.totalCost)}`;
|
|
2052
|
+
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>`);
|
|
2053
|
+
y += 28;
|
|
2054
|
+
const heatmapSvg = heatmap.svg.replace(/__MUTED__/g, escapeXml(theme.muted));
|
|
2055
|
+
sections.push(`<g transform="translate(${pad}, ${y})">`);
|
|
2056
|
+
sections.push(heatmapSvg);
|
|
2057
|
+
sections.push("</g>");
|
|
2058
|
+
y += heatmap.height;
|
|
2059
|
+
if (pi < providerHeatmaps.length - 1) {
|
|
2060
|
+
y += 12;
|
|
2061
|
+
sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
2062
|
+
y += PROVIDER_SECTION_GAP - 12;
|
|
2063
|
+
} else {
|
|
2064
|
+
y += 24;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
if (providers.length === 0) {
|
|
2068
|
+
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>`);
|
|
2069
|
+
y += 32;
|
|
2070
|
+
}
|
|
2071
|
+
sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
2072
|
+
y += 28;
|
|
2073
|
+
if (providers.length > 1) {
|
|
2074
|
+
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>`);
|
|
2075
|
+
y += 24;
|
|
2076
|
+
}
|
|
2077
|
+
const statColWidth = contentWidth / STAT_GRID_COLS;
|
|
2078
|
+
const statsRow1 = [
|
|
2079
|
+
{ label: "CURRENT STREAK", value: formatStreak(stats.currentStreak), accent: true },
|
|
2080
|
+
{ label: "LONGEST STREAK", value: formatStreak(stats.longestStreak), accent: false },
|
|
2081
|
+
{ label: "TOTAL TOKENS", value: formatNumber(stats.totalTokens), accent: true }
|
|
2082
|
+
];
|
|
2083
|
+
const statsRow2 = [
|
|
2084
|
+
{ label: "TOTAL COST", value: formatCost(stats.totalCost), accent: false },
|
|
2085
|
+
{ label: "30-DAY TOKENS", value: formatNumber(stats.rolling30dTokens), accent: false },
|
|
2086
|
+
{ label: "CACHE HIT RATE", value: formatPercentage(stats.cacheHitRate), accent: false }
|
|
2087
|
+
];
|
|
2088
|
+
function renderStatRow(row, startY) {
|
|
2089
|
+
for (let i = 0;i < row.length; i++) {
|
|
2090
|
+
const stat = row[i];
|
|
2091
|
+
const x = pad + i * statColWidth;
|
|
2092
|
+
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>`);
|
|
2093
|
+
const valueColor = stat.accent ? theme.accent : theme.fg;
|
|
2094
|
+
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>`);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
renderStatRow(statsRow1, y);
|
|
2098
|
+
y += 60;
|
|
2099
|
+
renderStatRow(statsRow2, y);
|
|
2100
|
+
y += 60;
|
|
2101
|
+
y += 8;
|
|
2102
|
+
sections.push(`<line x1="${pad}" y1="${y}" x2="${cardWidth - pad}" y2="${y}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
2103
|
+
y += 28;
|
|
2104
|
+
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>`);
|
|
2105
|
+
y += 24;
|
|
2106
|
+
const topModels2 = stats.topModels.slice(0, 3);
|
|
2107
|
+
const modelNameWidth = 200;
|
|
2108
|
+
const percentWidth = 60;
|
|
2109
|
+
const barMaxWidth = contentWidth - modelNameWidth - percentWidth - 20;
|
|
2110
|
+
for (const model of topModels2) {
|
|
2111
|
+
const barWidth = Math.max(4, model.percentage / 100 * barMaxWidth);
|
|
2112
|
+
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>`);
|
|
2113
|
+
const barX = pad + modelNameWidth;
|
|
2114
|
+
sections.push(`<rect x="${barX}" y="${y}" width="${barMaxWidth}" height="${MODEL_BAR_HEIGHT}" rx="4" fill="${escapeXml(theme.barTrack)}"/>`);
|
|
2115
|
+
const gradId = `grad-${model.model.replace(/[^a-zA-Z0-9]/g, "")}`;
|
|
2116
|
+
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>`);
|
|
2117
|
+
sections.push(`<rect x="${barX}" y="${y}" width="${barWidth}" height="${MODEL_BAR_HEIGHT}" rx="4" fill="url(#${escapeXml(gradId)})"/>`);
|
|
2118
|
+
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>`);
|
|
2119
|
+
y += 32;
|
|
2120
|
+
}
|
|
2121
|
+
y += pad * 0.5;
|
|
2122
|
+
const cardHeight = y;
|
|
2123
|
+
const svg = sections.join(`
|
|
2124
|
+
`).replace("__CARD_HEIGHT__", String(cardHeight));
|
|
2125
|
+
return [
|
|
2126
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${cardWidth}" height="${cardHeight}" viewBox="0 0 ${cardWidth} ${cardHeight}">`,
|
|
2127
|
+
svg,
|
|
2128
|
+
"</svg>"
|
|
2129
|
+
].join(`
|
|
2130
|
+
`);
|
|
2131
|
+
}
|
|
2132
|
+
|
|
1776
2133
|
// packages/renderers/dist/png/png-renderer.js
|
|
1777
2134
|
import sharp from "sharp";
|
|
1778
2135
|
|
|
1779
2136
|
class PngRenderer {
|
|
1780
2137
|
format = "png";
|
|
1781
|
-
svgRenderer = new SvgRenderer;
|
|
1782
2138
|
async render(output, options) {
|
|
1783
|
-
const svgString =
|
|
2139
|
+
const svgString = renderTerminalCardSvg(output, options);
|
|
1784
2140
|
const pngBuffer = await sharp(Buffer.from(svgString)).png().toBuffer();
|
|
1785
2141
|
return pngBuffer;
|
|
1786
2142
|
}
|
|
@@ -1837,7 +2193,7 @@ function intensityColor(value, max) {
|
|
|
1837
2193
|
// packages/renderers/dist/terminal/heatmap.js
|
|
1838
2194
|
var DAY_LABELS3 = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
1839
2195
|
var MONTH_LABELS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
1840
|
-
var
|
|
2196
|
+
var DAY_LABEL_WIDTH3 = 4;
|
|
1841
2197
|
var LEGEND_TEXT = "Less";
|
|
1842
2198
|
var LEGEND_TEXT_MORE = "More";
|
|
1843
2199
|
function buildUsageMap(daily) {
|
|
@@ -1868,11 +2224,11 @@ function renderTerminalHeatmap(daily, options) {
|
|
|
1868
2224
|
}
|
|
1869
2225
|
weeks.push(week);
|
|
1870
2226
|
}
|
|
1871
|
-
const availableWidth = options.width -
|
|
2227
|
+
const availableWidth = options.width - DAY_LABEL_WIDTH3;
|
|
1872
2228
|
const maxWeeks = Math.min(weeks.length, availableWidth);
|
|
1873
2229
|
const displayWeeks = weeks.slice(Math.max(0, weeks.length - maxWeeks));
|
|
1874
2230
|
const lines = [];
|
|
1875
|
-
let monthHeader = " ".repeat(
|
|
2231
|
+
let monthHeader = " ".repeat(DAY_LABEL_WIDTH3);
|
|
1876
2232
|
let lastMonth = -1;
|
|
1877
2233
|
for (const week of displayWeeks) {
|
|
1878
2234
|
const month = week[0].getMonth();
|
|
@@ -1891,7 +2247,7 @@ function renderTerminalHeatmap(daily, options) {
|
|
|
1891
2247
|
for (let dayIdx = 0;dayIdx < 7; dayIdx++) {
|
|
1892
2248
|
const label = dayIdx % 2 === 1 ? DAY_LABELS3[dayIdx] : " ";
|
|
1893
2249
|
let line = label + " ";
|
|
1894
|
-
line = line.slice(0,
|
|
2250
|
+
line = line.slice(0, DAY_LABEL_WIDTH3);
|
|
1895
2251
|
for (const week of displayWeeks) {
|
|
1896
2252
|
const date = week[dayIdx];
|
|
1897
2253
|
if (!date || date > endDate || date < startDate) {
|
|
@@ -1913,7 +2269,7 @@ function renderTerminalHeatmap(daily, options) {
|
|
|
1913
2269
|
HEATMAP_BLOCKS.DARK,
|
|
1914
2270
|
HEATMAP_BLOCKS.FULL
|
|
1915
2271
|
];
|
|
1916
|
-
const legend = `${" ".repeat(
|
|
2272
|
+
const legend = `${" ".repeat(DAY_LABEL_WIDTH3)}${LEGEND_TEXT} ${legendBlocks.join("")} ${LEGEND_TEXT_MORE}`;
|
|
1917
2273
|
lines.push(legend);
|
|
1918
2274
|
return lines.join(`
|
|
1919
2275
|
`);
|
|
@@ -2130,6 +2486,441 @@ class TerminalRenderer {
|
|
|
2130
2486
|
return renderDashboard(output, effectiveOptions);
|
|
2131
2487
|
}
|
|
2132
2488
|
}
|
|
2489
|
+
// packages/renderers/dist/live/template.js
|
|
2490
|
+
var MONTH_NAMES3 = [
|
|
2491
|
+
"Jan",
|
|
2492
|
+
"Feb",
|
|
2493
|
+
"Mar",
|
|
2494
|
+
"Apr",
|
|
2495
|
+
"May",
|
|
2496
|
+
"Jun",
|
|
2497
|
+
"Jul",
|
|
2498
|
+
"Aug",
|
|
2499
|
+
"Sep",
|
|
2500
|
+
"Oct",
|
|
2501
|
+
"Nov",
|
|
2502
|
+
"Dec"
|
|
2503
|
+
];
|
|
2504
|
+
var MONTH_NAMES_FULL2 = [
|
|
2505
|
+
"JAN",
|
|
2506
|
+
"FEB",
|
|
2507
|
+
"MAR",
|
|
2508
|
+
"APR",
|
|
2509
|
+
"MAY",
|
|
2510
|
+
"JUN",
|
|
2511
|
+
"JUL",
|
|
2512
|
+
"AUG",
|
|
2513
|
+
"SEP",
|
|
2514
|
+
"OCT",
|
|
2515
|
+
"NOV",
|
|
2516
|
+
"DEC"
|
|
2517
|
+
];
|
|
2518
|
+
function formatDateRange2(since, until) {
|
|
2519
|
+
const s = new Date(since + "T00:00:00Z");
|
|
2520
|
+
const u = new Date(until + "T00:00:00Z");
|
|
2521
|
+
const diffMs = u.getTime() - s.getTime();
|
|
2522
|
+
const days = Math.round(diffMs / (1000 * 60 * 60 * 24));
|
|
2523
|
+
const sMonth = MONTH_NAMES_FULL2[s.getUTCMonth()] ?? "";
|
|
2524
|
+
const uMonth = MONTH_NAMES_FULL2[u.getUTCMonth()] ?? "";
|
|
2525
|
+
return `${sMonth} ${s.getUTCFullYear()} — ${uMonth} ${u.getUTCFullYear()} · ${days} DAYS`;
|
|
2526
|
+
}
|
|
2527
|
+
function formatPercentage2(rate) {
|
|
2528
|
+
return `${(rate * 100).toFixed(1)}%`;
|
|
2529
|
+
}
|
|
2530
|
+
function formatStreak2(n) {
|
|
2531
|
+
return `${n} day${n !== 1 ? "s" : ""}`;
|
|
2532
|
+
}
|
|
2533
|
+
function esc(s) {
|
|
2534
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2535
|
+
}
|
|
2536
|
+
function computeQuantiles3(values) {
|
|
2537
|
+
const nonZero = values.filter((v) => v > 0).sort((a, b) => a - b);
|
|
2538
|
+
if (nonZero.length === 0)
|
|
2539
|
+
return [0, 0, 0];
|
|
2540
|
+
const q = (p) => {
|
|
2541
|
+
const idx = Math.floor(p * (nonZero.length - 1));
|
|
2542
|
+
return nonZero[idx] ?? 0;
|
|
2543
|
+
};
|
|
2544
|
+
return [q(0.25), q(0.5), q(0.75)];
|
|
2545
|
+
}
|
|
2546
|
+
function getLevel3(tokens, quantiles) {
|
|
2547
|
+
if (tokens <= 0)
|
|
2548
|
+
return 0;
|
|
2549
|
+
if (tokens <= quantiles[0])
|
|
2550
|
+
return 1;
|
|
2551
|
+
if (tokens <= quantiles[1])
|
|
2552
|
+
return 2;
|
|
2553
|
+
if (tokens <= quantiles[2])
|
|
2554
|
+
return 3;
|
|
2555
|
+
return 4;
|
|
2556
|
+
}
|
|
2557
|
+
function hexToRgb2(hex) {
|
|
2558
|
+
const h = hex.replace("#", "");
|
|
2559
|
+
return {
|
|
2560
|
+
r: parseInt(h.slice(0, 2), 16),
|
|
2561
|
+
g: parseInt(h.slice(2, 4), 16),
|
|
2562
|
+
b: parseInt(h.slice(4, 6), 16)
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2565
|
+
function rgbToHex2(r, g, b) {
|
|
2566
|
+
const toHex = (n) => n.toString(16).padStart(2, "0");
|
|
2567
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
2568
|
+
}
|
|
2569
|
+
function buildHeatmapScale2(colors, isDark) {
|
|
2570
|
+
const [startHex, endHex] = colors.gradient;
|
|
2571
|
+
const s = hexToRgb2(startHex);
|
|
2572
|
+
const e = hexToRgb2(endHex);
|
|
2573
|
+
const opacities = isDark ? [0.15, 0.35, 0.6, 1] : [0.2, 0.4, 0.65, 1];
|
|
2574
|
+
return [
|
|
2575
|
+
"transparent",
|
|
2576
|
+
...opacities.map((t) => {
|
|
2577
|
+
const r = Math.round(s.r + (e.r - s.r) * t);
|
|
2578
|
+
const g = Math.round(s.g + (e.g - s.g) * t);
|
|
2579
|
+
const b = Math.round(s.b + (e.b - s.b) * t);
|
|
2580
|
+
return rgbToHex2(r, g, b);
|
|
2581
|
+
})
|
|
2582
|
+
];
|
|
2583
|
+
}
|
|
2584
|
+
function buildHeatmapCells(daily, since, until) {
|
|
2585
|
+
const tokenMap = new Map;
|
|
2586
|
+
for (const d of daily) {
|
|
2587
|
+
tokenMap.set(d.date, (tokenMap.get(d.date) ?? 0) + d.totalTokens);
|
|
2588
|
+
}
|
|
2589
|
+
const dates = daily.map((d) => d.date).sort();
|
|
2590
|
+
const endStr = until ?? dates[dates.length - 1] ?? new Date().toISOString().slice(0, 10);
|
|
2591
|
+
const startStr = since ?? dates[0] ?? endStr;
|
|
2592
|
+
const end = new Date(endStr + "T00:00:00Z");
|
|
2593
|
+
const start = new Date(startStr + "T00:00:00Z");
|
|
2594
|
+
start.setUTCDate(start.getUTCDate() - start.getUTCDay());
|
|
2595
|
+
const allTokens = Array.from(tokenMap.values());
|
|
2596
|
+
const quantiles = computeQuantiles3(allTokens);
|
|
2597
|
+
const cells = [];
|
|
2598
|
+
const months = [];
|
|
2599
|
+
let lastMonth = -1;
|
|
2600
|
+
let col = 0;
|
|
2601
|
+
const current = new Date(start);
|
|
2602
|
+
while (current <= end) {
|
|
2603
|
+
const row = current.getUTCDay();
|
|
2604
|
+
const dateStr = current.toISOString().slice(0, 10);
|
|
2605
|
+
const tokens = tokenMap.get(dateStr) ?? 0;
|
|
2606
|
+
const level = getLevel3(tokens, quantiles);
|
|
2607
|
+
cells.push({ date: dateStr, tokens, level, row, col });
|
|
2608
|
+
const month = current.getUTCMonth();
|
|
2609
|
+
if (month !== lastMonth && row === 0) {
|
|
2610
|
+
lastMonth = month;
|
|
2611
|
+
months.push({ label: MONTH_NAMES3[month] ?? "", col });
|
|
2612
|
+
}
|
|
2613
|
+
if (row === 6)
|
|
2614
|
+
col++;
|
|
2615
|
+
current.setUTCDate(current.getUTCDate() + 1);
|
|
2616
|
+
}
|
|
2617
|
+
return { cells, months, totalCols: col + 1 };
|
|
2618
|
+
}
|
|
2619
|
+
function renderProviderHeatmapHtml(provider, since, until, isDark, emptyCell) {
|
|
2620
|
+
const heatmapColors = buildHeatmapScale2(provider.colors, isDark);
|
|
2621
|
+
const { cells, months, totalCols } = buildHeatmapCells(provider.daily, since, until);
|
|
2622
|
+
const cellSize = 16;
|
|
2623
|
+
const cellGap = 4;
|
|
2624
|
+
const dayLabelWidth = 44;
|
|
2625
|
+
const monthLabelHeight = 24;
|
|
2626
|
+
const heatmapWidth = dayLabelWidth + totalCols * (cellSize + cellGap);
|
|
2627
|
+
const heatmapHeight = monthLabelHeight + 7 * (cellSize + cellGap);
|
|
2628
|
+
const cellsHtml = cells.map((c) => {
|
|
2629
|
+
const x = dayLabelWidth + c.col * (cellSize + cellGap);
|
|
2630
|
+
const y = monthLabelHeight + c.row * (cellSize + cellGap);
|
|
2631
|
+
const fill = c.level === 0 ? emptyCell : heatmapColors[c.level];
|
|
2632
|
+
return `<div class="heatmap-cell" style="left:${x}px;top:${y}px;background:${fill}" data-date="${esc(c.date)}" data-tokens="${c.tokens}"></div>`;
|
|
2633
|
+
}).join(`
|
|
2634
|
+
`);
|
|
2635
|
+
const monthLabelsHtml = months.map((m) => {
|
|
2636
|
+
const x = dayLabelWidth + m.col * (cellSize + cellGap);
|
|
2637
|
+
return `<span class="month-label" style="left:${x}px">${esc(m.label)}</span>`;
|
|
2638
|
+
}).join(`
|
|
2639
|
+
`);
|
|
2640
|
+
const dayLabelsHtml = [
|
|
2641
|
+
{ label: "Mon", row: 1 },
|
|
2642
|
+
{ label: "Wed", row: 3 },
|
|
2643
|
+
{ label: "Fri", row: 5 },
|
|
2644
|
+
{ label: "Sun", row: 0 }
|
|
2645
|
+
].map((d) => {
|
|
2646
|
+
const y = monthLabelHeight + d.row * (cellSize + cellGap) + cellSize - 2;
|
|
2647
|
+
return `<span class="day-label" style="top:${y - 10}px">${d.label}</span>`;
|
|
2648
|
+
}).join(`
|
|
2649
|
+
`);
|
|
2650
|
+
const summaryText = `${esc(formatNumber(provider.totalTokens))} tokens · ${esc(formatCost(provider.totalCost))}`;
|
|
2651
|
+
return `<div class="provider-section" data-provider="${esc(provider.provider)}">
|
|
2652
|
+
<div class="provider-header">
|
|
2653
|
+
<div class="provider-name-row">
|
|
2654
|
+
<span class="provider-dot" style="background:${esc(provider.colors.primary)}"></span>
|
|
2655
|
+
<span class="provider-name">${esc(provider.displayName)}</span>
|
|
2656
|
+
</div>
|
|
2657
|
+
<span class="provider-summary">${summaryText}</span>
|
|
2658
|
+
</div>
|
|
2659
|
+
<div class="heatmap-container" style="width:${heatmapWidth}px;height:${heatmapHeight}px">
|
|
2660
|
+
${dayLabelsHtml}
|
|
2661
|
+
${monthLabelsHtml}
|
|
2662
|
+
${cellsHtml}
|
|
2663
|
+
</div>
|
|
2664
|
+
</div>`;
|
|
2665
|
+
}
|
|
2666
|
+
function generateHtml(output, options) {
|
|
2667
|
+
const isDark = options.theme === "dark";
|
|
2668
|
+
const stats = output.aggregated;
|
|
2669
|
+
const { since, until } = output.dateRange;
|
|
2670
|
+
const providers = output.providers;
|
|
2671
|
+
const bg = isDark ? "#0c0c0c" : "#fafafa";
|
|
2672
|
+
const fg = isDark ? "#ffffff" : "#18181b";
|
|
2673
|
+
const muted = isDark ? "#52525b" : "#a1a1aa";
|
|
2674
|
+
const accent = isDark ? "#10b981" : "#059669";
|
|
2675
|
+
const border = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.08)";
|
|
2676
|
+
const emptyCell = isDark ? "#1a1a1a" : "#e4e4e7";
|
|
2677
|
+
const barTrack = isDark ? "#1c1c1c" : "#e5e5e5";
|
|
2678
|
+
const providerSectionsHtml = providers.map((p, i) => {
|
|
2679
|
+
const section = renderProviderHeatmapHtml(p, since, until, isDark, emptyCell);
|
|
2680
|
+
const divider2 = i < providers.length - 1 ? '<hr class="provider-divider">' : "";
|
|
2681
|
+
return section + divider2;
|
|
2682
|
+
}).join(`
|
|
2683
|
+
`);
|
|
2684
|
+
const statRows = [
|
|
2685
|
+
[
|
|
2686
|
+
{ label: "CURRENT STREAK", value: esc(formatStreak2(stats.currentStreak)), accent: true },
|
|
2687
|
+
{ label: "LONGEST STREAK", value: esc(formatStreak2(stats.longestStreak)), accent: false },
|
|
2688
|
+
{ label: "TOTAL TOKENS", value: esc(formatNumber(stats.totalTokens)), accent: true }
|
|
2689
|
+
],
|
|
2690
|
+
[
|
|
2691
|
+
{ label: "TOTAL COST", value: esc(formatCost(stats.totalCost)), accent: false },
|
|
2692
|
+
{ label: "30-DAY TOKENS", value: esc(formatNumber(stats.rolling30dTokens)), accent: false },
|
|
2693
|
+
{ label: "CACHE HIT RATE", value: esc(formatPercentage2(stats.cacheHitRate)), accent: false }
|
|
2694
|
+
]
|
|
2695
|
+
];
|
|
2696
|
+
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("");
|
|
2697
|
+
const topModels2 = stats.topModels.slice(0, 3);
|
|
2698
|
+
const modelsHtml = topModels2.map((m) => {
|
|
2699
|
+
const width = Math.max(2, m.percentage);
|
|
2700
|
+
return `<div class="model-row">
|
|
2701
|
+
<span class="model-name">${esc(m.model)}</span>
|
|
2702
|
+
<div class="model-bar-track"><div class="model-bar-fill" style="width:${width}%"></div></div>
|
|
2703
|
+
<span class="model-pct">${m.percentage.toFixed(0)}%</span>
|
|
2704
|
+
</div>`;
|
|
2705
|
+
}).join("");
|
|
2706
|
+
const overallLabel = providers.length > 1 ? '<div class="overall-label">OVERALL</div>' : "";
|
|
2707
|
+
return `<!DOCTYPE html>
|
|
2708
|
+
<html lang="en">
|
|
2709
|
+
<head>
|
|
2710
|
+
<meta charset="utf-8">
|
|
2711
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2712
|
+
<title>tokenleak \u2014 live dashboard</title>
|
|
2713
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
2714
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
2715
|
+
<style>
|
|
2716
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2717
|
+
body {
|
|
2718
|
+
font-family: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
|
|
2719
|
+
background: ${isDark ? "#000" : "#f4f4f5"};
|
|
2720
|
+
display: flex;
|
|
2721
|
+
justify-content: center;
|
|
2722
|
+
align-items: flex-start;
|
|
2723
|
+
padding: 48px 24px;
|
|
2724
|
+
min-height: 100vh;
|
|
2725
|
+
}
|
|
2726
|
+
.card {
|
|
2727
|
+
background: ${bg};
|
|
2728
|
+
border-radius: 12px;
|
|
2729
|
+
border: 1px solid ${border};
|
|
2730
|
+
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)"};
|
|
2731
|
+
max-width: 900px;
|
|
2732
|
+
width: 100%;
|
|
2733
|
+
overflow: hidden;
|
|
2734
|
+
}
|
|
2735
|
+
.titlebar {
|
|
2736
|
+
display: flex;
|
|
2737
|
+
align-items: center;
|
|
2738
|
+
height: 48px;
|
|
2739
|
+
padding: 0 20px;
|
|
2740
|
+
border-bottom: 1px solid ${border};
|
|
2741
|
+
gap: 8px;
|
|
2742
|
+
}
|
|
2743
|
+
.dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
|
|
2744
|
+
.dot-red { background: #ff5f57; }
|
|
2745
|
+
.dot-yellow { background: #febc2e; }
|
|
2746
|
+
.dot-green { background: #28c840; }
|
|
2747
|
+
.titlebar-label { color: ${muted}; font-size: 13px; font-weight: 500; margin-left: 12px; }
|
|
2748
|
+
.content { padding: 28px 48px 48px; }
|
|
2749
|
+
.prompt { font-size: 15px; font-weight: 500; margin-bottom: 24px; }
|
|
2750
|
+
.prompt .dollar { color: ${accent}; }
|
|
2751
|
+
.prompt .cmd { color: ${fg}; }
|
|
2752
|
+
.prompt .cursor { color: ${accent}; animation: blink 1s step-end infinite; }
|
|
2753
|
+
@keyframes blink { 50% { opacity: 0; } }
|
|
2754
|
+
.date-range { color: ${muted}; font-size: 12px; font-weight: 600; letter-spacing: 2px; margin-bottom: 24px; }
|
|
2755
|
+
|
|
2756
|
+
/* Provider sections */
|
|
2757
|
+
.provider-section { margin-bottom: 12px; }
|
|
2758
|
+
.provider-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
|
|
2759
|
+
.provider-name-row { display: flex; align-items: center; gap: 10px; }
|
|
2760
|
+
.provider-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
|
2761
|
+
.provider-name { color: ${fg}; font-size: 14px; font-weight: 600; }
|
|
2762
|
+
.provider-summary { color: ${muted}; font-size: 11px; font-weight: 500; }
|
|
2763
|
+
.provider-divider { border: none; border-top: 1px solid ${border}; margin: 24px 0; }
|
|
2764
|
+
|
|
2765
|
+
.heatmap-container { position: relative; margin-bottom: 8px; }
|
|
2766
|
+
.heatmap-cell {
|
|
2767
|
+
position: absolute;
|
|
2768
|
+
width: 16px;
|
|
2769
|
+
height: 16px;
|
|
2770
|
+
border-radius: 3px;
|
|
2771
|
+
cursor: pointer;
|
|
2772
|
+
}
|
|
2773
|
+
.heatmap-cell:hover { outline: 2px solid ${accent}; outline-offset: 1px; }
|
|
2774
|
+
.month-label { position: absolute; top: 0; color: ${muted}; font-size: 11px; }
|
|
2775
|
+
.day-label { position: absolute; left: 0; color: ${muted}; font-size: 11px; }
|
|
2776
|
+
.tooltip {
|
|
2777
|
+
display: none;
|
|
2778
|
+
position: fixed;
|
|
2779
|
+
background: ${isDark ? "#1c1c1c" : "#fff"};
|
|
2780
|
+
color: ${fg};
|
|
2781
|
+
border: 1px solid ${border};
|
|
2782
|
+
border-radius: 6px;
|
|
2783
|
+
padding: 6px 10px;
|
|
2784
|
+
font-size: 11px;
|
|
2785
|
+
pointer-events: none;
|
|
2786
|
+
z-index: 100;
|
|
2787
|
+
white-space: nowrap;
|
|
2788
|
+
box-shadow: 0 4px 12px ${isDark ? "rgba(0,0,0,0.5)" : "rgba(0,0,0,0.1)"};
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
.divider { border: none; border-top: 1px solid ${border}; margin: 0 0 28px; }
|
|
2792
|
+
.overall-label { color: ${muted}; font-size: 10px; font-weight: 600; letter-spacing: 2px; margin-bottom: 16px; }
|
|
2793
|
+
.stat-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 20px; }
|
|
2794
|
+
.stat-label { color: ${muted}; font-size: 10px; font-weight: 600; letter-spacing: 1.5px; margin-bottom: 8px; }
|
|
2795
|
+
.stat-value { color: ${fg}; font-size: 22px; font-weight: 700; }
|
|
2796
|
+
.stat-value.accent { color: ${accent}; }
|
|
2797
|
+
.models-section { margin-top: 8px; }
|
|
2798
|
+
.models-label { color: ${muted}; font-size: 10px; font-weight: 600; letter-spacing: 2px; margin-bottom: 16px; }
|
|
2799
|
+
.model-row { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
|
2800
|
+
.model-name { color: ${muted}; font-size: 12px; width: 200px; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2801
|
+
.model-bar-track { flex: 1; height: 8px; background: ${barTrack}; border-radius: 4px; overflow: hidden; }
|
|
2802
|
+
.model-bar-fill { height: 100%; border-radius: 4px; background: linear-gradient(90deg, ${accent}44, ${accent}); }
|
|
2803
|
+
.model-pct { color: ${muted}; font-size: 12px; width: 40px; text-align: right; flex-shrink: 0; }
|
|
2804
|
+
.refresh-btn {
|
|
2805
|
+
display: inline-flex;
|
|
2806
|
+
align-items: center;
|
|
2807
|
+
gap: 6px;
|
|
2808
|
+
margin-top: 24px;
|
|
2809
|
+
padding: 8px 16px;
|
|
2810
|
+
background: ${isDark ? "#1c1c1c" : "#e5e5e5"};
|
|
2811
|
+
color: ${muted};
|
|
2812
|
+
border: 1px solid ${border};
|
|
2813
|
+
border-radius: 6px;
|
|
2814
|
+
font-family: inherit;
|
|
2815
|
+
font-size: 12px;
|
|
2816
|
+
cursor: pointer;
|
|
2817
|
+
transition: background 0.15s;
|
|
2818
|
+
}
|
|
2819
|
+
.refresh-btn:hover { background: ${isDark ? "#262626" : "#d4d4d8"}; color: ${fg}; }
|
|
2820
|
+
</style>
|
|
2821
|
+
</head>
|
|
2822
|
+
<body>
|
|
2823
|
+
<div class="card">
|
|
2824
|
+
<div class="titlebar">
|
|
2825
|
+
<div class="dot dot-red"></div>
|
|
2826
|
+
<div class="dot dot-yellow"></div>
|
|
2827
|
+
<div class="dot dot-green"></div>
|
|
2828
|
+
<span class="titlebar-label">tokenleak</span>
|
|
2829
|
+
</div>
|
|
2830
|
+
<div class="content">
|
|
2831
|
+
<div class="prompt">
|
|
2832
|
+
<span class="dollar">$</span>
|
|
2833
|
+
<span class="cmd"> tokenleak</span>
|
|
2834
|
+
<span class="cursor">_</span>
|
|
2835
|
+
</div>
|
|
2836
|
+
<div class="date-range">${formatDateRange2(since, until)}</div>
|
|
2837
|
+
${providerSectionsHtml}
|
|
2838
|
+
<hr class="divider">
|
|
2839
|
+
${overallLabel}
|
|
2840
|
+
${statsHtml}
|
|
2841
|
+
<hr class="divider" style="margin-top:8px">
|
|
2842
|
+
<div class="models-section">
|
|
2843
|
+
<div class="models-label">TOP MODELS</div>
|
|
2844
|
+
${modelsHtml}
|
|
2845
|
+
</div>
|
|
2846
|
+
<button class="refresh-btn" onclick="location.reload()">↻ Refresh</button>
|
|
2847
|
+
</div>
|
|
2848
|
+
</div>
|
|
2849
|
+
<div class="tooltip" id="tooltip"></div>
|
|
2850
|
+
<script>
|
|
2851
|
+
const tooltip = document.getElementById('tooltip');
|
|
2852
|
+
document.querySelectorAll('.heatmap-cell').forEach(cell => {
|
|
2853
|
+
const section = cell.closest('.provider-section');
|
|
2854
|
+
const provider = section ? section.dataset.provider : '';
|
|
2855
|
+
cell.addEventListener('mouseenter', e => {
|
|
2856
|
+
const date = cell.dataset.date;
|
|
2857
|
+
const tokens = Number(cell.dataset.tokens).toLocaleString();
|
|
2858
|
+
tooltip.textContent = (provider ? provider + ' \u2014 ' : '') + date + ': ' + tokens + ' tokens';
|
|
2859
|
+
tooltip.style.display = 'block';
|
|
2860
|
+
});
|
|
2861
|
+
cell.addEventListener('mousemove', e => {
|
|
2862
|
+
tooltip.style.left = (e.clientX + 12) + 'px';
|
|
2863
|
+
tooltip.style.top = (e.clientY - 30) + 'px';
|
|
2864
|
+
});
|
|
2865
|
+
cell.addEventListener('mouseleave', () => {
|
|
2866
|
+
tooltip.style.display = 'none';
|
|
2867
|
+
});
|
|
2868
|
+
});
|
|
2869
|
+
</script>
|
|
2870
|
+
</body>
|
|
2871
|
+
</html>`;
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
// packages/renderers/dist/live/live-server.js
|
|
2875
|
+
function tryServe(html, port) {
|
|
2876
|
+
try {
|
|
2877
|
+
const server = Bun.serve({
|
|
2878
|
+
port,
|
|
2879
|
+
fetch(_req) {
|
|
2880
|
+
return new Response(html, {
|
|
2881
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
});
|
|
2885
|
+
return { server, error: null };
|
|
2886
|
+
} catch (err) {
|
|
2887
|
+
return { server: null, error: err };
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
function isAddrInUse(err) {
|
|
2891
|
+
if (err && typeof err === "object") {
|
|
2892
|
+
const obj = err;
|
|
2893
|
+
if (obj["code"] === "EADDRINUSE")
|
|
2894
|
+
return true;
|
|
2895
|
+
}
|
|
2896
|
+
if (err instanceof Error) {
|
|
2897
|
+
const msg = err.message;
|
|
2898
|
+
if (msg.includes("EADDRINUSE") || msg.includes("address already in use"))
|
|
2899
|
+
return true;
|
|
2900
|
+
}
|
|
2901
|
+
return false;
|
|
2902
|
+
}
|
|
2903
|
+
async function startLiveServer(output, options) {
|
|
2904
|
+
const html = generateHtml(output, options);
|
|
2905
|
+
const startPort = options.port ?? 3333;
|
|
2906
|
+
const maxAttempts = 20;
|
|
2907
|
+
let port = startPort;
|
|
2908
|
+
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
2909
|
+
const result = tryServe(html, port);
|
|
2910
|
+
if (result.server) {
|
|
2911
|
+
const actualPort = result.server.port ?? port;
|
|
2912
|
+
process.stderr.write(`Server running at http://localhost:${String(actualPort)}
|
|
2913
|
+
`);
|
|
2914
|
+
return { port: actualPort, stop: () => result.server.stop(true) };
|
|
2915
|
+
}
|
|
2916
|
+
if (isAddrInUse(result.error)) {
|
|
2917
|
+
port++;
|
|
2918
|
+
continue;
|
|
2919
|
+
}
|
|
2920
|
+
throw result.error;
|
|
2921
|
+
}
|
|
2922
|
+
throw new Error(`Could not find a free port after ${maxAttempts} attempts starting from ${startPort}`);
|
|
2923
|
+
}
|
|
2133
2924
|
// packages/cli/src/config.ts
|
|
2134
2925
|
import { readFileSync as readFileSync2 } from "fs";
|
|
2135
2926
|
import { join as join4 } from "path";
|
|
@@ -2307,8 +3098,21 @@ function inferFormatFromPath(filePath) {
|
|
|
2307
3098
|
return null;
|
|
2308
3099
|
}
|
|
2309
3100
|
}
|
|
3101
|
+
var DATE_FORMAT = /^\d{4}-\d{2}-\d{2}$/;
|
|
3102
|
+
function isValidDate(dateStr) {
|
|
3103
|
+
if (!DATE_FORMAT.test(dateStr))
|
|
3104
|
+
return false;
|
|
3105
|
+
const d = new Date(dateStr + "T00:00:00Z");
|
|
3106
|
+
return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === dateStr;
|
|
3107
|
+
}
|
|
2310
3108
|
function computeDateRange(args) {
|
|
2311
3109
|
const until = args.until ?? new Date().toISOString().slice(0, 10);
|
|
3110
|
+
if (args.until && !isValidDate(args.until)) {
|
|
3111
|
+
throw new TokenleakError(`Invalid --until date: "${args.until}". Use YYYY-MM-DD format.`);
|
|
3112
|
+
}
|
|
3113
|
+
if (args.since && !isValidDate(args.since)) {
|
|
3114
|
+
throw new TokenleakError(`Invalid --since date: "${args.since}". Use YYYY-MM-DD format.`);
|
|
3115
|
+
}
|
|
2312
3116
|
let since;
|
|
2313
3117
|
if (args.since) {
|
|
2314
3118
|
since = args.since;
|
|
@@ -2318,6 +3122,9 @@ function computeDateRange(args) {
|
|
|
2318
3122
|
d.setDate(d.getDate() - daysBack);
|
|
2319
3123
|
since = d.toISOString().slice(0, 10);
|
|
2320
3124
|
}
|
|
3125
|
+
if (since > until) {
|
|
3126
|
+
throw new TokenleakError(`--since (${since}) must not be after --until (${until}).`);
|
|
3127
|
+
}
|
|
2321
3128
|
return { since, until };
|
|
2322
3129
|
}
|
|
2323
3130
|
function resolveConfig(cliArgs) {
|
|
@@ -2332,7 +3139,8 @@ function resolveConfig(cliArgs) {
|
|
|
2332
3139
|
noColor: false,
|
|
2333
3140
|
noInsights: false,
|
|
2334
3141
|
clipboard: false,
|
|
2335
|
-
open: false
|
|
3142
|
+
open: false,
|
|
3143
|
+
liveServer: false
|
|
2336
3144
|
};
|
|
2337
3145
|
if (fileConfig.format && FORMAT_VALUES.includes(fileConfig.format)) {
|
|
2338
3146
|
merged.format = fileConfig.format;
|
|
@@ -2404,6 +3212,9 @@ function resolveConfig(cliArgs) {
|
|
|
2404
3212
|
if (cliArgs["upload"] !== undefined) {
|
|
2405
3213
|
result.upload = cliArgs["upload"];
|
|
2406
3214
|
}
|
|
3215
|
+
if (cliArgs["liveServer"] !== undefined) {
|
|
3216
|
+
result.liveServer = cliArgs["liveServer"];
|
|
3217
|
+
}
|
|
2407
3218
|
return result;
|
|
2408
3219
|
}
|
|
2409
3220
|
function getRenderer(format) {
|
|
@@ -2470,6 +3281,10 @@ async function run(cliArgs) {
|
|
|
2470
3281
|
throw new TokenleakError("No provider data found");
|
|
2471
3282
|
}
|
|
2472
3283
|
if (config.compare) {
|
|
3284
|
+
if (config.format !== "json" && config.format !== "terminal") {
|
|
3285
|
+
process.stderr.write(`Warning: --compare only supports JSON output. Ignoring --format ${config.format}.
|
|
3286
|
+
`);
|
|
3287
|
+
}
|
|
2473
3288
|
const compareOutput = await runCompare(config.compare, dateRange, registry, available);
|
|
2474
3289
|
const rendered2 = JSON.stringify(compareOutput, null, 2);
|
|
2475
3290
|
if (config.output) {
|
|
@@ -2500,6 +3315,42 @@ async function run(cliArgs) {
|
|
|
2500
3315
|
providers: providerDataList,
|
|
2501
3316
|
aggregated: stats
|
|
2502
3317
|
};
|
|
3318
|
+
if (config.liveServer) {
|
|
3319
|
+
const ignoredFlags = [];
|
|
3320
|
+
if (config.output)
|
|
3321
|
+
ignoredFlags.push("--output");
|
|
3322
|
+
if (config.clipboard)
|
|
3323
|
+
ignoredFlags.push("--clipboard");
|
|
3324
|
+
if (config.open)
|
|
3325
|
+
ignoredFlags.push("--open");
|
|
3326
|
+
if (config.upload)
|
|
3327
|
+
ignoredFlags.push("--upload");
|
|
3328
|
+
if (ignoredFlags.length > 0) {
|
|
3329
|
+
process.stderr.write(`Warning: ${ignoredFlags.join(", ")} ignored in --live-server mode.
|
|
3330
|
+
`);
|
|
3331
|
+
}
|
|
3332
|
+
const renderOptions2 = {
|
|
3333
|
+
format: config.format,
|
|
3334
|
+
theme: config.theme,
|
|
3335
|
+
width: config.width,
|
|
3336
|
+
showInsights: !config.noInsights,
|
|
3337
|
+
noColor: config.noColor,
|
|
3338
|
+
output: config.output
|
|
3339
|
+
};
|
|
3340
|
+
const { port } = await startLiveServer(output, renderOptions2);
|
|
3341
|
+
await new Promise((resolve) => {
|
|
3342
|
+
process.on("SIGINT", () => {
|
|
3343
|
+
process.stderr.write(`
|
|
3344
|
+
Shutting down server...
|
|
3345
|
+
`);
|
|
3346
|
+
resolve();
|
|
3347
|
+
});
|
|
3348
|
+
process.on("SIGTERM", () => {
|
|
3349
|
+
resolve();
|
|
3350
|
+
});
|
|
3351
|
+
});
|
|
3352
|
+
return;
|
|
3353
|
+
}
|
|
2503
3354
|
const renderer = getRenderer(config.format);
|
|
2504
3355
|
const renderOptions = {
|
|
2505
3356
|
format: config.format,
|
|
@@ -2618,6 +3469,12 @@ var main = defineCommand({
|
|
|
2618
3469
|
upload: {
|
|
2619
3470
|
type: "string",
|
|
2620
3471
|
description: "Upload output to a service (supported: gist)"
|
|
3472
|
+
},
|
|
3473
|
+
liveServer: {
|
|
3474
|
+
type: "boolean",
|
|
3475
|
+
alias: "L",
|
|
3476
|
+
description: "Start a local server with an interactive dashboard",
|
|
3477
|
+
default: false
|
|
2621
3478
|
}
|
|
2622
3479
|
},
|
|
2623
3480
|
async run({ args }) {
|
|
@@ -2651,6 +3508,8 @@ var main = defineCommand({
|
|
|
2651
3508
|
cliArgs["open"] = true;
|
|
2652
3509
|
if (args.upload !== undefined)
|
|
2653
3510
|
cliArgs["upload"] = args.upload;
|
|
3511
|
+
if (args.liveServer)
|
|
3512
|
+
cliArgs["liveServer"] = true;
|
|
2654
3513
|
await run(cliArgs);
|
|
2655
3514
|
} catch (error) {
|
|
2656
3515
|
handleError(error);
|