ccgauge 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-build-manifest.json +58 -57
- package/.next/standalone/.next/app-path-routes-manifest.json +8 -8
- package/.next/standalone/.next/build-manifest.json +7 -7
- package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/blocks/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/export/usage/route.js +1 -1
- package/.next/standalone/.next/server/app/api/export/usage/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/pricing/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/projects/route.js +1 -1
- package/.next/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/scan/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/sessions/route.js +1 -1
- package/.next/standalone/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/usage/route.js +1 -1
- package/.next/standalone/.next/server/app/api/usage/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/usage/route_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/models/page.js +2 -2
- package/.next/standalone/.next/server/app/models/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/models/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -2
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/[id]/page.js +1 -1
- package/.next/standalone/.next/server/app/projects/[id]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/sessions/[id]/page.js +2 -2
- package/.next/standalone/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/sessions/page.js +1 -1
- package/.next/standalone/.next/server/app/sessions/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page.js +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/usage/page.js +3 -3
- package/.next/standalone/.next/server/app/usage/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/usage/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +8 -8
- package/.next/standalone/.next/server/chunks/125.js +1 -0
- package/.next/standalone/.next/server/chunks/567.js +2 -2
- package/.next/standalone/.next/server/chunks/730.js +1 -0
- package/.next/standalone/.next/server/edge-runtime-webpack.js +2 -0
- package/.next/standalone/.next/server/functions-config-manifest.json +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/.next/standalone/.next/server/middleware-manifest.json +28 -2
- package/.next/standalone/.next/server/middleware.js +14 -0
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/static/chunks/148-f2cba0b76260b8d3.js +1 -0
- package/.next/standalone/.next/static/chunks/253-dae1b01a941e7c13.js +1 -0
- package/.next/standalone/.next/static/chunks/app/{layout-4f3538437c5e8366.js → layout-0adb4fc0305adf29.js} +1 -1
- package/.next/standalone/.next/static/chunks/app/usage/page-7fcc2a2d931307d5.js +1 -0
- package/.next/standalone/.next/static/chunks/{main-69f0897f4e7ce710.js → main-2cb79280eaf22774.js} +1 -1
- package/.next/standalone/.next/static/chunks/{webpack-3fcacae817f3ffab.js → webpack-73dfee69c0ddc58e.js} +1 -1
- package/.next/standalone/.next/static/css/bde47638beb0c717.css +3 -0
- package/.next/standalone/package.json +2 -1
- package/.next/standalone/public/codex-logo.png +0 -0
- package/CHANGELOG.md +363 -0
- package/README.md +6 -2
- package/README.zh-CN.md +6 -2
- package/bin/cli.mjs +404 -92
- package/dist/mcp/server.mjs +16 -16
- package/dist/report/index.mjs +971 -56
- package/package.json +2 -1
- package/.next/standalone/.next/server/chunks/971.js +0 -1
- package/.next/standalone/.next/static/chunks/148-6c2eaf5508bfe739.js +0 -1
- package/.next/standalone/.next/static/chunks/app/usage/page-18fd820a3111bd5b.js +0 -1
- package/.next/standalone/.next/static/css/fbd2c395e5bf32cb.css +0 -3
- /package/.next/standalone/.next/static/{ir1LZCnQKkiNUVXLprtzh → jncTEohJB76Iq9TUm3G21}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{ir1LZCnQKkiNUVXLprtzh → jncTEohJB76Iq9TUm3G21}/_ssgManifest.js +0 -0
package/dist/report/index.mjs
CHANGED
|
@@ -1521,6 +1521,19 @@ function formatPct(n, frac = 1) {
|
|
|
1521
1521
|
if (!Number.isFinite(n)) return "0%";
|
|
1522
1522
|
return `${(n * 100).toFixed(frac)}%`;
|
|
1523
1523
|
}
|
|
1524
|
+
function formatDuration(ms) {
|
|
1525
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
1526
|
+
const sec = Math.floor(ms / 1e3);
|
|
1527
|
+
if (sec < 60) return `${sec}s`;
|
|
1528
|
+
const min = Math.floor(sec / 60);
|
|
1529
|
+
if (min < 60) {
|
|
1530
|
+
const s = sec % 60;
|
|
1531
|
+
return s ? `${min}m ${s}s` : `${min}m`;
|
|
1532
|
+
}
|
|
1533
|
+
const hr = Math.floor(min / 60);
|
|
1534
|
+
const m = min % 60;
|
|
1535
|
+
return m ? `${hr}h ${m}m` : `${hr}h`;
|
|
1536
|
+
}
|
|
1524
1537
|
function projectNameFromCwd(cwd) {
|
|
1525
1538
|
if (!cwd) return "(unknown)";
|
|
1526
1539
|
const trimmed = cwd.replace(/[/\\]+$/, "");
|
|
@@ -1528,6 +1541,69 @@ function projectNameFromCwd(cwd) {
|
|
|
1528
1541
|
return parts[parts.length - 1] || cwd;
|
|
1529
1542
|
}
|
|
1530
1543
|
|
|
1544
|
+
// lib/project-label.ts
|
|
1545
|
+
import { readFileSync, statSync } from "node:fs";
|
|
1546
|
+
import { basename } from "node:path";
|
|
1547
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1548
|
+
var GITDIR_PATTERN = /^(.+?)[/\\]\.git[/\\]worktrees[/\\]([^/\\]+)[/\\]?$/;
|
|
1549
|
+
var CWD_WORKTREE_PATTERN = /^(.+?)[/\\](?:\.git|\.claude)[/\\]worktrees[/\\]([^/\\]+)(?:[/\\].*)?$/;
|
|
1550
|
+
function resolveRaw(cwd) {
|
|
1551
|
+
const fallbackName = projectNameFromCwd(cwd);
|
|
1552
|
+
if (!cwd) {
|
|
1553
|
+
return { label: fallbackName, isWorktree: false, mainName: fallbackName, worktreeName: "", canonicalCwd: cwd };
|
|
1554
|
+
}
|
|
1555
|
+
const pathMatch = CWD_WORKTREE_PATTERN.exec(cwd);
|
|
1556
|
+
if (pathMatch) {
|
|
1557
|
+
const mainRepoPath = pathMatch[1];
|
|
1558
|
+
const worktreeName = pathMatch[2];
|
|
1559
|
+
const mainName = basename(mainRepoPath) || mainRepoPath;
|
|
1560
|
+
return {
|
|
1561
|
+
label: `${mainName} (${worktreeName})`,
|
|
1562
|
+
isWorktree: true,
|
|
1563
|
+
mainName,
|
|
1564
|
+
worktreeName,
|
|
1565
|
+
canonicalCwd: mainRepoPath
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
try {
|
|
1569
|
+
const gitPath = `${cwd}/.git`;
|
|
1570
|
+
const s = statSync(gitPath);
|
|
1571
|
+
if (!s.isFile()) {
|
|
1572
|
+
return { label: fallbackName, isWorktree: false, mainName: fallbackName, worktreeName: "", canonicalCwd: cwd };
|
|
1573
|
+
}
|
|
1574
|
+
const text = readFileSync(gitPath, "utf8").trim();
|
|
1575
|
+
const firstLine = text.split(/\r?\n/, 1)[0] ?? "";
|
|
1576
|
+
const m = /^gitdir:\s*(.+)$/.exec(firstLine);
|
|
1577
|
+
if (!m) {
|
|
1578
|
+
return { label: fallbackName, isWorktree: false, mainName: fallbackName, worktreeName: "", canonicalCwd: cwd };
|
|
1579
|
+
}
|
|
1580
|
+
const gitdir = m[1].trim();
|
|
1581
|
+
const wt = GITDIR_PATTERN.exec(gitdir);
|
|
1582
|
+
if (!wt) {
|
|
1583
|
+
return { label: fallbackName, isWorktree: false, mainName: fallbackName, worktreeName: "", canonicalCwd: cwd };
|
|
1584
|
+
}
|
|
1585
|
+
const mainRepoPath = wt[1];
|
|
1586
|
+
const worktreeName = wt[2];
|
|
1587
|
+
const mainName = basename(mainRepoPath) || mainRepoPath;
|
|
1588
|
+
return {
|
|
1589
|
+
label: `${mainName} (${worktreeName})`,
|
|
1590
|
+
isWorktree: true,
|
|
1591
|
+
mainName,
|
|
1592
|
+
worktreeName,
|
|
1593
|
+
canonicalCwd: mainRepoPath
|
|
1594
|
+
};
|
|
1595
|
+
} catch {
|
|
1596
|
+
return { label: fallbackName, isWorktree: false, mainName: fallbackName, worktreeName: "", canonicalCwd: cwd };
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
function resolveProjectLabel(cwd) {
|
|
1600
|
+
const cached = cache.get(cwd);
|
|
1601
|
+
if (cached) return cached.label;
|
|
1602
|
+
const r = resolveRaw(cwd);
|
|
1603
|
+
cache.set(cwd, r);
|
|
1604
|
+
return r.label;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1531
1607
|
// lib/aggregator/index.ts
|
|
1532
1608
|
var GRANULARITIES = ["hour", "day", "week", "month"];
|
|
1533
1609
|
function isGranularity(v) {
|
|
@@ -1719,6 +1795,12 @@ function aggregateBySession(records, userRecords, opts) {
|
|
|
1719
1795
|
source: rec.source,
|
|
1720
1796
|
cwd: rec.cwd,
|
|
1721
1797
|
projectName: projectNameFromCwd(rec.cwd),
|
|
1798
|
+
// Worktree-aware label so the sessions table shows the same
|
|
1799
|
+
// identifier the usage table does for the same record (e.g.
|
|
1800
|
+
// `ai-self-web (playwright)` instead of just `playwright`).
|
|
1801
|
+
// `resolveProjectLabel` caches per-cwd, so this is one fs.stat
|
|
1802
|
+
// per unique cwd across the whole aggregation.
|
|
1803
|
+
projectLabel: resolveProjectLabel(rec.cwd),
|
|
1722
1804
|
startTime: rec.timestamp,
|
|
1723
1805
|
endTime: rec.timestamp,
|
|
1724
1806
|
durationMs: 0,
|
|
@@ -1803,13 +1885,13 @@ function aggregateTotals(records, opts) {
|
|
|
1803
1885
|
}
|
|
1804
1886
|
|
|
1805
1887
|
// lib/range.ts
|
|
1806
|
-
var USAGE_RANGES = ["1d", "7d", "30d", "90d", "all"];
|
|
1888
|
+
var USAGE_RANGES = ["1d", "7d", "30d", "90d", "all", "custom"];
|
|
1807
1889
|
function isUsageRange(v) {
|
|
1808
1890
|
return typeof v === "string" && USAGE_RANGES.includes(v);
|
|
1809
1891
|
}
|
|
1810
1892
|
function rangeToDates(range) {
|
|
1811
1893
|
const now = /* @__PURE__ */ new Date();
|
|
1812
|
-
if (range === "all") return {};
|
|
1894
|
+
if (range === "all" || range === "custom") return {};
|
|
1813
1895
|
if (range === "1d") {
|
|
1814
1896
|
const from2 = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1815
1897
|
return { from: from2 };
|
|
@@ -1825,6 +1907,771 @@ function rangeToDates(range) {
|
|
|
1825
1907
|
return { from };
|
|
1826
1908
|
}
|
|
1827
1909
|
|
|
1910
|
+
// lib/turns.ts
|
|
1911
|
+
var MAX_PARENT_WALK = 5e3;
|
|
1912
|
+
function buildTurnIndex(assistants, users, parentMap) {
|
|
1913
|
+
const userTextMap = /* @__PURE__ */ new Map();
|
|
1914
|
+
for (const u of users) {
|
|
1915
|
+
if (u.isSynthetic) continue;
|
|
1916
|
+
if (u.textPreview && u.textPreview.trim()) userTextMap.set(u.uuid, u.textPreview);
|
|
1917
|
+
}
|
|
1918
|
+
const result = /* @__PURE__ */ new Map();
|
|
1919
|
+
const memo = /* @__PURE__ */ new Map();
|
|
1920
|
+
function resolve(startUuid) {
|
|
1921
|
+
const path5 = [];
|
|
1922
|
+
let cur = startUuid;
|
|
1923
|
+
let answer = null;
|
|
1924
|
+
let steps = 0;
|
|
1925
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1926
|
+
while (cur && steps++ < MAX_PARENT_WALK) {
|
|
1927
|
+
if (seen.has(cur)) break;
|
|
1928
|
+
seen.add(cur);
|
|
1929
|
+
const m = memo.get(cur);
|
|
1930
|
+
if (m) {
|
|
1931
|
+
answer = m;
|
|
1932
|
+
break;
|
|
1933
|
+
}
|
|
1934
|
+
path5.push(cur);
|
|
1935
|
+
if (userTextMap.has(cur)) {
|
|
1936
|
+
answer = cur;
|
|
1937
|
+
break;
|
|
1938
|
+
}
|
|
1939
|
+
cur = parentMap[cur] ?? null;
|
|
1940
|
+
}
|
|
1941
|
+
if (!answer) answer = startUuid;
|
|
1942
|
+
for (const id of path5) memo.set(id, answer);
|
|
1943
|
+
return answer;
|
|
1944
|
+
}
|
|
1945
|
+
for (const a of assistants) {
|
|
1946
|
+
result.set(a.uuid, resolve(a.uuid));
|
|
1947
|
+
}
|
|
1948
|
+
return result;
|
|
1949
|
+
}
|
|
1950
|
+
function summarizeTurns(records, users, parentMap) {
|
|
1951
|
+
const turnIndex = buildTurnIndex(records, users, parentMap);
|
|
1952
|
+
const out = /* @__PURE__ */ new Map();
|
|
1953
|
+
for (const r of records) {
|
|
1954
|
+
const turnId = turnIndex.get(r.uuid) ?? r.uuid;
|
|
1955
|
+
const existing = out.get(turnId);
|
|
1956
|
+
if (!existing || r.timestamp < existing.firstTimestamp) {
|
|
1957
|
+
out.set(turnId, {
|
|
1958
|
+
turnId,
|
|
1959
|
+
firstTimestamp: r.timestamp,
|
|
1960
|
+
firstModel: r.model,
|
|
1961
|
+
cwd: r.cwd,
|
|
1962
|
+
sessionId: r.sessionId,
|
|
1963
|
+
source: r.source
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
return out;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// lib/blocks/compute.ts
|
|
1971
|
+
var DEFAULT_BLOCK_WINDOW_MS = 5 * 60 * 60 * 1e3;
|
|
1972
|
+
function computeBlocks(records, windowMs = DEFAULT_BLOCK_WINDOW_MS) {
|
|
1973
|
+
if (records.length === 0) return [];
|
|
1974
|
+
const sorted = [...records].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
1975
|
+
const blocks = [];
|
|
1976
|
+
let current = null;
|
|
1977
|
+
let blockStartMs = 0;
|
|
1978
|
+
const now = Date.now();
|
|
1979
|
+
for (const rec of sorted) {
|
|
1980
|
+
const t = new Date(rec.timestamp).getTime();
|
|
1981
|
+
if (!current || t - blockStartMs >= windowMs) {
|
|
1982
|
+
blockStartMs = t;
|
|
1983
|
+
current = {
|
|
1984
|
+
id: rec.timestamp,
|
|
1985
|
+
startTime: rec.timestamp,
|
|
1986
|
+
endTime: new Date(t + windowMs).toISOString(),
|
|
1987
|
+
actualEndTime: rec.timestamp,
|
|
1988
|
+
isActive: false,
|
|
1989
|
+
inputTokens: 0,
|
|
1990
|
+
outputTokens: 0,
|
|
1991
|
+
cacheReadTokens: 0,
|
|
1992
|
+
cacheCreationTokens: 0,
|
|
1993
|
+
totalTokens: 0,
|
|
1994
|
+
cost: 0,
|
|
1995
|
+
saved: 0,
|
|
1996
|
+
models: [],
|
|
1997
|
+
requests: 0
|
|
1998
|
+
};
|
|
1999
|
+
blocks.push(current);
|
|
2000
|
+
}
|
|
2001
|
+
const cost = costOfRecord(rec);
|
|
2002
|
+
current.inputTokens += rec.usage.input_tokens;
|
|
2003
|
+
current.outputTokens += rec.usage.output_tokens;
|
|
2004
|
+
current.cacheReadTokens += rec.usage.cache_read_input_tokens;
|
|
2005
|
+
current.cacheCreationTokens += rec.usage.cache_creation_input_tokens;
|
|
2006
|
+
current.totalTokens = current.inputTokens + current.outputTokens + current.cacheReadTokens + current.cacheCreationTokens;
|
|
2007
|
+
current.cost += cost.total;
|
|
2008
|
+
current.saved += cost.saved;
|
|
2009
|
+
current.requests += 1;
|
|
2010
|
+
current.actualEndTime = rec.timestamp;
|
|
2011
|
+
if (!current.models.includes(rec.model)) current.models.push(rec.model);
|
|
2012
|
+
}
|
|
2013
|
+
for (const b of blocks) {
|
|
2014
|
+
const endMs = new Date(b.endTime).getTime();
|
|
2015
|
+
b.isActive = now < endMs;
|
|
2016
|
+
}
|
|
2017
|
+
return blocks;
|
|
2018
|
+
}
|
|
2019
|
+
function getActiveBlock(records, windowMs = DEFAULT_BLOCK_WINDOW_MS) {
|
|
2020
|
+
const blocks = computeBlocks(records, windowMs);
|
|
2021
|
+
const active = blocks.find((b) => b.isActive);
|
|
2022
|
+
return active ?? null;
|
|
2023
|
+
}
|
|
2024
|
+
function blockProgress(records, windowMs = DEFAULT_BLOCK_WINDOW_MS) {
|
|
2025
|
+
const block = getActiveBlock(records, windowMs);
|
|
2026
|
+
if (!block) {
|
|
2027
|
+
return {
|
|
2028
|
+
block: null,
|
|
2029
|
+
windowMs,
|
|
2030
|
+
elapsedMs: 0,
|
|
2031
|
+
remainingMs: 0,
|
|
2032
|
+
progress: 0,
|
|
2033
|
+
burnRatePerMin: 0,
|
|
2034
|
+
costPerMin: 0,
|
|
2035
|
+
projectedTotal: 0,
|
|
2036
|
+
projectedCost: 0
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
const now = Date.now();
|
|
2040
|
+
const startMs = new Date(block.startTime).getTime();
|
|
2041
|
+
const endMs = new Date(block.endTime).getTime();
|
|
2042
|
+
const elapsedMs = now - startMs;
|
|
2043
|
+
const remainingMs = Math.max(0, endMs - now);
|
|
2044
|
+
const progress = Math.min(1, elapsedMs / windowMs);
|
|
2045
|
+
const elapsedMin = elapsedMs / 6e4;
|
|
2046
|
+
const burnRatePerMin = elapsedMin > 0 ? block.totalTokens / elapsedMin : 0;
|
|
2047
|
+
const costPerMin = elapsedMin > 0 ? block.cost / elapsedMin : 0;
|
|
2048
|
+
const totalMin = windowMs / 6e4;
|
|
2049
|
+
return {
|
|
2050
|
+
block,
|
|
2051
|
+
windowMs,
|
|
2052
|
+
elapsedMs,
|
|
2053
|
+
remainingMs,
|
|
2054
|
+
progress,
|
|
2055
|
+
burnRatePerMin,
|
|
2056
|
+
costPerMin,
|
|
2057
|
+
projectedTotal: burnRatePerMin * totalMin,
|
|
2058
|
+
projectedCost: costPerMin * totalMin
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// lib/aggregator/activity.ts
|
|
2063
|
+
var DAY_MS = 864e5;
|
|
2064
|
+
function computeActivityStats(records, opts) {
|
|
2065
|
+
const filtered = opts.source === "all" ? records : records.filter((r) => r.source === opts.source);
|
|
2066
|
+
if (filtered.length === 0) {
|
|
2067
|
+
return {
|
|
2068
|
+
sessions: 0,
|
|
2069
|
+
messages: 0,
|
|
2070
|
+
totalTokens: 0,
|
|
2071
|
+
activeDays: 0,
|
|
2072
|
+
currentStreak: 0,
|
|
2073
|
+
longestStreak: 0,
|
|
2074
|
+
peakHour: -1,
|
|
2075
|
+
favoriteModel: null,
|
|
2076
|
+
heatmap: emptyHeatmap(),
|
|
2077
|
+
heatmapMax: 0,
|
|
2078
|
+
tokenHeatmap: emptyHeatmap(),
|
|
2079
|
+
tokensSummed: 0
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
const sessionSet = /* @__PURE__ */ new Set();
|
|
2083
|
+
const dayKeys = /* @__PURE__ */ new Set();
|
|
2084
|
+
const hourCounts = new Array(24).fill(0);
|
|
2085
|
+
const modelCounts = /* @__PURE__ */ new Map();
|
|
2086
|
+
const heatmap = emptyHeatmap();
|
|
2087
|
+
const tokenHeatmap = emptyHeatmap();
|
|
2088
|
+
let totalTokens2 = 0;
|
|
2089
|
+
let messages = 0;
|
|
2090
|
+
for (const r of filtered) {
|
|
2091
|
+
if (r.sessionId) sessionSet.add(r.sessionId);
|
|
2092
|
+
const d = new Date(r.timestamp);
|
|
2093
|
+
if (Number.isNaN(d.getTime())) continue;
|
|
2094
|
+
const dayKey = localDayKey(d);
|
|
2095
|
+
dayKeys.add(dayKey);
|
|
2096
|
+
const dow = d.getDay();
|
|
2097
|
+
const hour = d.getHours();
|
|
2098
|
+
hourCounts[hour] += 1;
|
|
2099
|
+
heatmap[dow][hour] += 1;
|
|
2100
|
+
modelCounts.set(r.model, (modelCounts.get(r.model) ?? 0) + 1);
|
|
2101
|
+
const u = r.usage;
|
|
2102
|
+
const recTokens = u.input_tokens + u.output_tokens + u.cache_read_input_tokens + u.cache_creation_input_tokens;
|
|
2103
|
+
tokenHeatmap[dow][hour] += recTokens;
|
|
2104
|
+
totalTokens2 += recTokens;
|
|
2105
|
+
messages += 1;
|
|
2106
|
+
}
|
|
2107
|
+
const peakHour = argMax(hourCounts);
|
|
2108
|
+
const favoriteModel = pickTopKey(modelCounts);
|
|
2109
|
+
const { current, longest } = computeStreaks(dayKeys, opts.streakWindowDays ?? 365);
|
|
2110
|
+
let heatmapMax = 0;
|
|
2111
|
+
for (const row of heatmap) for (const v of row) if (v > heatmapMax) heatmapMax = v;
|
|
2112
|
+
return {
|
|
2113
|
+
sessions: sessionSet.size,
|
|
2114
|
+
messages,
|
|
2115
|
+
totalTokens: totalTokens2,
|
|
2116
|
+
activeDays: dayKeys.size,
|
|
2117
|
+
currentStreak: current,
|
|
2118
|
+
longestStreak: longest,
|
|
2119
|
+
peakHour,
|
|
2120
|
+
favoriteModel,
|
|
2121
|
+
heatmap,
|
|
2122
|
+
heatmapMax,
|
|
2123
|
+
tokenHeatmap,
|
|
2124
|
+
tokensSummed: totalTokens2
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
function emptyHeatmap() {
|
|
2128
|
+
return Array.from({ length: 7 }, () => new Array(24).fill(0));
|
|
2129
|
+
}
|
|
2130
|
+
function localDayKey(d) {
|
|
2131
|
+
const yyyy = d.getFullYear();
|
|
2132
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
2133
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
2134
|
+
return `${yyyy}-${mm}-${dd}`;
|
|
2135
|
+
}
|
|
2136
|
+
function argMax(arr) {
|
|
2137
|
+
let bestIdx = -1;
|
|
2138
|
+
let best = -1;
|
|
2139
|
+
for (let i = 0; i < arr.length; i += 1) {
|
|
2140
|
+
if (arr[i] > best) {
|
|
2141
|
+
best = arr[i];
|
|
2142
|
+
bestIdx = i;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
return bestIdx;
|
|
2146
|
+
}
|
|
2147
|
+
function pickTopKey(m) {
|
|
2148
|
+
let best = null;
|
|
2149
|
+
let bestN = -1;
|
|
2150
|
+
for (const [k, v] of m) {
|
|
2151
|
+
if (v > bestN) {
|
|
2152
|
+
best = k;
|
|
2153
|
+
bestN = v;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
return best;
|
|
2157
|
+
}
|
|
2158
|
+
function computeStreaks(dayKeys, windowDays) {
|
|
2159
|
+
if (dayKeys.size === 0) return { current: 0, longest: 0 };
|
|
2160
|
+
const days = Array.from(dayKeys).map((k) => (/* @__PURE__ */ new Date(k + "T00:00:00")).getTime()).filter((t) => Number.isFinite(t)).sort((a, b) => a - b);
|
|
2161
|
+
let longest = 1;
|
|
2162
|
+
let run = 1;
|
|
2163
|
+
for (let i = 1; i < days.length; i += 1) {
|
|
2164
|
+
if (days[i] - days[i - 1] === DAY_MS) {
|
|
2165
|
+
run += 1;
|
|
2166
|
+
if (run > longest) longest = run;
|
|
2167
|
+
} else {
|
|
2168
|
+
run = 1;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
const todayMs = atMidnight(Date.now());
|
|
2172
|
+
const lastDay = days[days.length - 1];
|
|
2173
|
+
if (todayMs - lastDay > DAY_MS) {
|
|
2174
|
+
return { current: 0, longest };
|
|
2175
|
+
}
|
|
2176
|
+
let cursor = lastDay;
|
|
2177
|
+
let current = 0;
|
|
2178
|
+
let scanned = 0;
|
|
2179
|
+
const inSet = new Set(days);
|
|
2180
|
+
while (scanned < windowDays && inSet.has(cursor)) {
|
|
2181
|
+
current += 1;
|
|
2182
|
+
cursor -= DAY_MS;
|
|
2183
|
+
scanned += 1;
|
|
2184
|
+
}
|
|
2185
|
+
return { current, longest };
|
|
2186
|
+
}
|
|
2187
|
+
function atMidnight(ms) {
|
|
2188
|
+
const d = new Date(ms);
|
|
2189
|
+
d.setHours(0, 0, 0, 0);
|
|
2190
|
+
return d.getTime();
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
// lib/cli-report/ansi.ts
|
|
2194
|
+
var RGB = {
|
|
2195
|
+
brand: [129, 140, 248],
|
|
2196
|
+
// indigo-400
|
|
2197
|
+
green: [34, 197, 94],
|
|
2198
|
+
red: [239, 68, 68],
|
|
2199
|
+
yellow: [234, 179, 8],
|
|
2200
|
+
cyan: [34, 211, 238],
|
|
2201
|
+
input: [96, 165, 250],
|
|
2202
|
+
// blue-400
|
|
2203
|
+
cacheWrite: [167, 139, 250],
|
|
2204
|
+
// purple-400
|
|
2205
|
+
cacheRead: [52, 211, 153],
|
|
2206
|
+
// emerald-400
|
|
2207
|
+
output: [251, 146, 60]
|
|
2208
|
+
// orange-400
|
|
2209
|
+
};
|
|
2210
|
+
function fg(rgb, useColor) {
|
|
2211
|
+
if (!useColor) return (s) => s;
|
|
2212
|
+
const prefix = `\x1B[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m`;
|
|
2213
|
+
return (s) => `${prefix}${s}\x1B[39m`;
|
|
2214
|
+
}
|
|
2215
|
+
function wrap(open, close, useColor) {
|
|
2216
|
+
if (!useColor) return (s) => s;
|
|
2217
|
+
return (s) => `${open}${s}${close}`;
|
|
2218
|
+
}
|
|
2219
|
+
function makePalette(useColor) {
|
|
2220
|
+
return {
|
|
2221
|
+
reset: useColor ? "\x1B[0m" : "",
|
|
2222
|
+
bold: wrap("\x1B[1m", "\x1B[22m", useColor),
|
|
2223
|
+
dim: wrap("\x1B[2m", "\x1B[22m", useColor),
|
|
2224
|
+
brand: fg(RGB.brand, useColor),
|
|
2225
|
+
green: fg(RGB.green, useColor),
|
|
2226
|
+
red: fg(RGB.red, useColor),
|
|
2227
|
+
yellow: fg(RGB.yellow, useColor),
|
|
2228
|
+
cyan: fg(RGB.cyan, useColor),
|
|
2229
|
+
input: fg(RGB.input, useColor),
|
|
2230
|
+
cacheWrite: fg(RGB.cacheWrite, useColor),
|
|
2231
|
+
cacheRead: fg(RGB.cacheRead, useColor),
|
|
2232
|
+
output: fg(RGB.output, useColor)
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
var ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
2236
|
+
function visibleLen(s) {
|
|
2237
|
+
return s.replace(ANSI_RE, "").length;
|
|
2238
|
+
}
|
|
2239
|
+
function padEnd(s, w, fill = " ") {
|
|
2240
|
+
const need = w - visibleLen(s);
|
|
2241
|
+
return need > 0 ? s + fill.repeat(need) : s;
|
|
2242
|
+
}
|
|
2243
|
+
function padStart(s, w, fill = " ") {
|
|
2244
|
+
const need = w - visibleLen(s);
|
|
2245
|
+
return need > 0 ? fill.repeat(need) + s : s;
|
|
2246
|
+
}
|
|
2247
|
+
function center(s, w, fill = " ") {
|
|
2248
|
+
const need = w - visibleLen(s);
|
|
2249
|
+
if (need <= 0) return s;
|
|
2250
|
+
const l = Math.floor(need / 2);
|
|
2251
|
+
const r = need - l;
|
|
2252
|
+
return fill.repeat(l) + s + fill.repeat(r);
|
|
2253
|
+
}
|
|
2254
|
+
function truncate(s, w) {
|
|
2255
|
+
if (visibleLen(s) <= w) return s;
|
|
2256
|
+
return s.slice(0, Math.max(0, w - 1)) + "\u2026";
|
|
2257
|
+
}
|
|
2258
|
+
var HBAR_CHARS = ["", "\u258F", "\u258E", "\u258D", "\u258C", "\u258B", "\u258A", "\u2589", "\u2588"];
|
|
2259
|
+
function hbar(ratio, width, color) {
|
|
2260
|
+
if (width <= 0 || !Number.isFinite(ratio) || ratio <= 0) {
|
|
2261
|
+
return " ".repeat(Math.max(0, width));
|
|
2262
|
+
}
|
|
2263
|
+
const r = Math.min(1, ratio);
|
|
2264
|
+
const filledCells = r * width;
|
|
2265
|
+
const fullCells = Math.floor(filledCells);
|
|
2266
|
+
const remainder = filledCells - fullCells;
|
|
2267
|
+
const subIdx = Math.round(remainder * 8);
|
|
2268
|
+
let bar = "\u2588".repeat(fullCells);
|
|
2269
|
+
if (subIdx > 0 && fullCells < width) bar += HBAR_CHARS[subIdx];
|
|
2270
|
+
const out = padEnd(bar, width, " ");
|
|
2271
|
+
return color ? color(out) : out;
|
|
2272
|
+
}
|
|
2273
|
+
var SPARK = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
|
|
2274
|
+
function sparkline(values, width, color) {
|
|
2275
|
+
if (width <= 0) return "";
|
|
2276
|
+
if (values.length === 0) return color ? color("\u2500".repeat(width)) : "\u2500".repeat(width);
|
|
2277
|
+
const sampled = [];
|
|
2278
|
+
if (values.length === width) {
|
|
2279
|
+
sampled.push(...values);
|
|
2280
|
+
} else if (values.length > width) {
|
|
2281
|
+
const step = values.length / width;
|
|
2282
|
+
for (let i = 0; i < width; i += 1) {
|
|
2283
|
+
const lo = Math.floor(i * step);
|
|
2284
|
+
const hi = Math.floor((i + 1) * step);
|
|
2285
|
+
let sum = 0;
|
|
2286
|
+
let n = 0;
|
|
2287
|
+
for (let j = lo; j < Math.min(hi, values.length); j += 1) {
|
|
2288
|
+
sum += values[j];
|
|
2289
|
+
n += 1;
|
|
2290
|
+
}
|
|
2291
|
+
sampled.push(n > 0 ? sum / n : 0);
|
|
2292
|
+
}
|
|
2293
|
+
} else {
|
|
2294
|
+
const pad = width - values.length;
|
|
2295
|
+
for (let i = 0; i < pad; i += 1) sampled.push(0);
|
|
2296
|
+
sampled.push(...values);
|
|
2297
|
+
}
|
|
2298
|
+
const max = Math.max(...sampled, 0);
|
|
2299
|
+
if (max <= 0) {
|
|
2300
|
+
const flat = "\u2500".repeat(width);
|
|
2301
|
+
return color ? color(flat) : flat;
|
|
2302
|
+
}
|
|
2303
|
+
const out = sampled.map((v) => SPARK[Math.min(7, Math.max(0, Math.round(v / max * 7)))]).join("");
|
|
2304
|
+
return color ? color(out) : out;
|
|
2305
|
+
}
|
|
2306
|
+
var VBAR_CHARS = ["", "\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
|
|
2307
|
+
function stackedColumn(segments, height, maxValue) {
|
|
2308
|
+
const lines = new Array(height).fill(" ");
|
|
2309
|
+
if (height <= 0 || maxValue <= 0) return lines;
|
|
2310
|
+
const total = segments.reduce((s, x) => s + Math.max(0, x.value), 0);
|
|
2311
|
+
if (total <= 0) return lines;
|
|
2312
|
+
const totalUnits = total / maxValue * height * 8;
|
|
2313
|
+
const cap = height * 8;
|
|
2314
|
+
const used = Math.min(totalUnits, cap);
|
|
2315
|
+
let remaining = used;
|
|
2316
|
+
let cursorUnits = 0;
|
|
2317
|
+
for (const seg of segments) {
|
|
2318
|
+
if (seg.value <= 0 || remaining <= 0) continue;
|
|
2319
|
+
const segUnits = Math.min(remaining, seg.value / total * used);
|
|
2320
|
+
const startUnits = cursorUnits;
|
|
2321
|
+
const endUnits = cursorUnits + segUnits;
|
|
2322
|
+
const startRow = Math.floor(startUnits / 8);
|
|
2323
|
+
const endRow = Math.min(height - 1, Math.floor((endUnits - 1e-4) / 8));
|
|
2324
|
+
for (let row = startRow; row <= endRow; row += 1) {
|
|
2325
|
+
const rowBottom = row * 8;
|
|
2326
|
+
const rowTop = (row + 1) * 8;
|
|
2327
|
+
const fillBottom = Math.max(0, startUnits - rowBottom);
|
|
2328
|
+
const fillTop = Math.min(8, endUnits - rowBottom);
|
|
2329
|
+
let ch;
|
|
2330
|
+
if (fillTop >= 8) {
|
|
2331
|
+
ch = "\u2588";
|
|
2332
|
+
} else if (fillBottom <= 0) {
|
|
2333
|
+
const idx = Math.max(1, Math.round(fillTop));
|
|
2334
|
+
ch = VBAR_CHARS[Math.min(8, idx)] || "\u2581";
|
|
2335
|
+
} else {
|
|
2336
|
+
ch = "\u2588";
|
|
2337
|
+
}
|
|
2338
|
+
lines[height - 1 - row] = seg.color ? seg.color(ch) : ch;
|
|
2339
|
+
}
|
|
2340
|
+
remaining -= segUnits;
|
|
2341
|
+
cursorUnits = endUnits;
|
|
2342
|
+
}
|
|
2343
|
+
return lines;
|
|
2344
|
+
}
|
|
2345
|
+
function box(title, contentLines, innerWidth, c) {
|
|
2346
|
+
const titleStr = title ? ` ${title} ` : "";
|
|
2347
|
+
const tLen = visibleLen(titleStr);
|
|
2348
|
+
const beforeLen = Math.max(1, Math.floor((innerWidth - tLen) / 2));
|
|
2349
|
+
const afterLen = Math.max(1, innerWidth - tLen - beforeLen);
|
|
2350
|
+
const top = c.dim("\u250C" + "\u2500".repeat(beforeLen)) + (title ? c.bold(titleStr) : "\u2500".repeat(tLen)) + c.dim("\u2500".repeat(afterLen) + "\u2510");
|
|
2351
|
+
const bot = c.dim("\u2514" + "\u2500".repeat(innerWidth) + "\u2518");
|
|
2352
|
+
const body = contentLines.map((line) => c.dim("\u2502") + " " + padEnd(line, innerWidth - 2) + " " + c.dim("\u2502")).join("\n");
|
|
2353
|
+
return [top, body, bot].join("\n");
|
|
2354
|
+
}
|
|
2355
|
+
function twoColumns(left, right, gap = 2) {
|
|
2356
|
+
const ls = left.split("\n");
|
|
2357
|
+
const rs = right.split("\n");
|
|
2358
|
+
const h = Math.max(ls.length, rs.length);
|
|
2359
|
+
const lw = Math.max(0, ...ls.map(visibleLen));
|
|
2360
|
+
const sep = " ".repeat(Math.max(1, gap));
|
|
2361
|
+
const out = [];
|
|
2362
|
+
for (let i = 0; i < h; i += 1) {
|
|
2363
|
+
const L = padEnd(ls[i] ?? "", lw);
|
|
2364
|
+
const R = rs[i] ?? "";
|
|
2365
|
+
out.push(L + sep + R);
|
|
2366
|
+
}
|
|
2367
|
+
return out.join("\n");
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
// lib/cli-report/dash.ts
|
|
2371
|
+
function renderDash(scan, data, opts, filteredRecords) {
|
|
2372
|
+
const width = opts.width ?? process.stdout.columns ?? 100;
|
|
2373
|
+
const useColor = opts.color !== false;
|
|
2374
|
+
const c = makePalette(useColor);
|
|
2375
|
+
if (width < 80) {
|
|
2376
|
+
return [
|
|
2377
|
+
`[ccgauge] terminal width (${width}) is below the dashboard's 80-column floor.`,
|
|
2378
|
+
`Resize wider for the rich layout, or omit --dashboard for the standard report.`,
|
|
2379
|
+
"",
|
|
2380
|
+
...renderText(data, opts).split("\n")
|
|
2381
|
+
].join("\n");
|
|
2382
|
+
}
|
|
2383
|
+
const inner = width - 4;
|
|
2384
|
+
const lines = [];
|
|
2385
|
+
if (opts.banner !== false) {
|
|
2386
|
+
lines.push(...renderBanner(c, inner, data));
|
|
2387
|
+
lines.push("");
|
|
2388
|
+
}
|
|
2389
|
+
lines.push(...renderKpiTiles(c, inner, scan, data));
|
|
2390
|
+
lines.push("");
|
|
2391
|
+
if (opts.compact !== true && data.trend.length > 0) {
|
|
2392
|
+
lines.push(...renderTrend(c, inner, data));
|
|
2393
|
+
lines.push("");
|
|
2394
|
+
}
|
|
2395
|
+
lines.push(...renderBreakdowns(c, inner, data));
|
|
2396
|
+
lines.push("");
|
|
2397
|
+
lines.push(...renderHeatmap(c, inner, filteredRecords));
|
|
2398
|
+
lines.push("");
|
|
2399
|
+
lines.push(...renderFooter(c, inner, filteredRecords, data, opts));
|
|
2400
|
+
return lines.map((l) => " " + l).join("\n");
|
|
2401
|
+
}
|
|
2402
|
+
function renderBanner(c, w, data) {
|
|
2403
|
+
const title = `${c.brand(c.bold("ccgauge"))} ${c.bold("dashboard")}`;
|
|
2404
|
+
const meta = c.dim(
|
|
2405
|
+
[
|
|
2406
|
+
`range: ${data.range}`,
|
|
2407
|
+
`source: ${data.source}`,
|
|
2408
|
+
`by: ${data.by}`,
|
|
2409
|
+
`generated ${new Date(data.generatedAt).toLocaleString()}`
|
|
2410
|
+
].join(" \xB7 ")
|
|
2411
|
+
);
|
|
2412
|
+
const titlePart = ` ${title} `;
|
|
2413
|
+
const metaPart = ` ${meta} `;
|
|
2414
|
+
const fillLen = Math.max(0, w - visibleLen(titlePart) - visibleLen(metaPart) - 2);
|
|
2415
|
+
const dash = c.dim("\u2500");
|
|
2416
|
+
const left = c.dim("\u256D") + dash + titlePart + dash.repeat(Math.floor(fillLen / 2));
|
|
2417
|
+
const right = dash.repeat(fillLen - Math.floor(fillLen / 2)) + metaPart + dash + c.dim("\u256E");
|
|
2418
|
+
return [left + right];
|
|
2419
|
+
}
|
|
2420
|
+
function pickActiveBlock(records, source) {
|
|
2421
|
+
function compute(provider) {
|
|
2422
|
+
return blockProgress(
|
|
2423
|
+
records.filter((r) => r.source === provider),
|
|
2424
|
+
getProvider(provider).capabilities.blockWindowMs
|
|
2425
|
+
);
|
|
2426
|
+
}
|
|
2427
|
+
if (source === "claude") return compute("claude");
|
|
2428
|
+
if (source === "codex") return compute("codex");
|
|
2429
|
+
const c = compute("claude");
|
|
2430
|
+
if (c.block) return c;
|
|
2431
|
+
const x = compute("codex");
|
|
2432
|
+
return x.block ? x : c;
|
|
2433
|
+
}
|
|
2434
|
+
function renderKpiTiles(c, w, scan, data) {
|
|
2435
|
+
const t = data.totals;
|
|
2436
|
+
const sparkTokens = data.trend.map((b) => b.tokens);
|
|
2437
|
+
const sparkCost = data.trend.map((b) => b.cost);
|
|
2438
|
+
const sparkSaved = data.trend.map((b) => Math.max(0, b.cost - b.cost * 0.95));
|
|
2439
|
+
const sparkConvos = data.trend.map((b) => b.turns);
|
|
2440
|
+
const cacheIn = t.input + t.cacheRead + t.cacheWrite;
|
|
2441
|
+
const cacheHit = cacheIn > 0 ? t.cacheRead / cacheIn : 0;
|
|
2442
|
+
const activeBlock = pickActiveBlock(scan.records, data.source);
|
|
2443
|
+
const tiles = [
|
|
2444
|
+
{
|
|
2445
|
+
label: "Total tokens",
|
|
2446
|
+
value: c.bold(formatTokensCompact(t.total)),
|
|
2447
|
+
sub: `${t.requests.toLocaleString()} reqs \xB7 ${t.turns.toLocaleString()} convs`,
|
|
2448
|
+
spark: sparkline(sparkTokens, 12, c.input)
|
|
2449
|
+
},
|
|
2450
|
+
{
|
|
2451
|
+
label: "Cost",
|
|
2452
|
+
value: c.bold(formatUSD(t.cost)),
|
|
2453
|
+
sub: t.cost > 0 ? `${formatUSD(t.cost / Math.max(1, t.requests))} / req` : "\u2014",
|
|
2454
|
+
spark: sparkline(sparkCost, 12, c.output)
|
|
2455
|
+
},
|
|
2456
|
+
{
|
|
2457
|
+
label: "Cache saved",
|
|
2458
|
+
value: c.bold(c.green(formatUSD(t.saved))),
|
|
2459
|
+
sub: `vs full input pricing`,
|
|
2460
|
+
spark: sparkline(sparkSaved, 12, c.green)
|
|
2461
|
+
},
|
|
2462
|
+
{
|
|
2463
|
+
label: "Cache hit",
|
|
2464
|
+
value: c.bold(c.green(formatPct(cacheHit, 0))),
|
|
2465
|
+
// Render the progress bar in the sparkline slot so this tile's
|
|
2466
|
+
// row layout matches the others; the sub line then carries the
|
|
2467
|
+
// raw ratio for users who care about the exact number.
|
|
2468
|
+
spark: hbar(cacheHit, 12, c.green),
|
|
2469
|
+
sub: `${formatTokensCompact(t.cacheRead)} cached`
|
|
2470
|
+
},
|
|
2471
|
+
{
|
|
2472
|
+
label: "Conversations",
|
|
2473
|
+
value: c.bold(t.turns.toLocaleString()),
|
|
2474
|
+
sub: t.requests > 0 ? `${(t.requests / Math.max(1, t.turns)).toFixed(1)} reqs/turn` : "\u2014",
|
|
2475
|
+
spark: sparkline(sparkConvos, 12, c.cacheRead)
|
|
2476
|
+
},
|
|
2477
|
+
{
|
|
2478
|
+
label: "Active 5h block",
|
|
2479
|
+
value: activeBlock?.block ? c.bold(formatDuration(activeBlock.remainingMs)) : c.dim("idle"),
|
|
2480
|
+
spark: activeBlock?.block ? hbar(activeBlock.progress, 12, c.yellow) : void 0,
|
|
2481
|
+
sub: activeBlock?.block ? `${formatPct(activeBlock.progress, 0)} elapsed` : "no active window"
|
|
2482
|
+
}
|
|
2483
|
+
];
|
|
2484
|
+
return layoutTiles(tiles, c, w);
|
|
2485
|
+
}
|
|
2486
|
+
function layoutTiles(tiles, c, w) {
|
|
2487
|
+
const tileInner = 24;
|
|
2488
|
+
const tileOuter = tileInner + 2;
|
|
2489
|
+
const gap = 1;
|
|
2490
|
+
const perRow = Math.max(1, Math.floor((w + gap) / (tileOuter + gap)));
|
|
2491
|
+
const out = [];
|
|
2492
|
+
for (let i = 0; i < tiles.length; i += perRow) {
|
|
2493
|
+
const slice = tiles.slice(i, i + perRow);
|
|
2494
|
+
const blocks = slice.map((t) => renderTile(t, tileInner, c));
|
|
2495
|
+
out.push(...mergeHorizontally(blocks, gap).split("\n"));
|
|
2496
|
+
if (i + perRow < tiles.length) out.push("");
|
|
2497
|
+
}
|
|
2498
|
+
return out;
|
|
2499
|
+
}
|
|
2500
|
+
function renderTile(t, inner, c) {
|
|
2501
|
+
const cap = inner - 2;
|
|
2502
|
+
const body = [""];
|
|
2503
|
+
body.push(truncate(t.value, cap));
|
|
2504
|
+
body.push(t.spark ? truncate(t.spark, cap) : "");
|
|
2505
|
+
body.push(c.dim(truncate(t.sub, cap)));
|
|
2506
|
+
return box(t.label, body, inner, c);
|
|
2507
|
+
}
|
|
2508
|
+
function mergeHorizontally(blocks, gap) {
|
|
2509
|
+
const rows = blocks.map((b) => b.split("\n"));
|
|
2510
|
+
const h = Math.max(...rows.map((r) => r.length));
|
|
2511
|
+
const widths = rows.map((r) => Math.max(...r.map(visibleLen)));
|
|
2512
|
+
const sep = " ".repeat(gap);
|
|
2513
|
+
const lines = [];
|
|
2514
|
+
for (let i = 0; i < h; i += 1) {
|
|
2515
|
+
const cells = rows.map((r, idx) => padEnd(r[i] ?? "", widths[idx]));
|
|
2516
|
+
lines.push(cells.join(sep));
|
|
2517
|
+
}
|
|
2518
|
+
return lines.join("\n");
|
|
2519
|
+
}
|
|
2520
|
+
function renderTrend(c, w, data) {
|
|
2521
|
+
const trend = data.trend;
|
|
2522
|
+
const height = 10;
|
|
2523
|
+
const cellWidth = Math.max(4, Math.min(8, Math.floor((w - 8) / Math.max(1, trend.length))));
|
|
2524
|
+
const max = Math.max(...trend.map((b) => b.input + b.output + b.cacheRead + b.cacheWrite), 1);
|
|
2525
|
+
const heading = `${c.brand("\u25B8")} ${c.bold("Token usage trend")} ${c.dim(
|
|
2526
|
+
`(${data.gran} \xD7 stacked: input / cache-w / cache-r / output)`
|
|
2527
|
+
)}`;
|
|
2528
|
+
const colsPerBucket = trend.map(
|
|
2529
|
+
(b) => stackedColumn(
|
|
2530
|
+
[
|
|
2531
|
+
{ value: b.input, color: c.input },
|
|
2532
|
+
{ value: b.cacheWrite, color: c.cacheWrite },
|
|
2533
|
+
{ value: b.cacheRead, color: c.cacheRead },
|
|
2534
|
+
{ value: b.output, color: c.output }
|
|
2535
|
+
],
|
|
2536
|
+
height,
|
|
2537
|
+
max
|
|
2538
|
+
)
|
|
2539
|
+
);
|
|
2540
|
+
const yLabels = [
|
|
2541
|
+
padStart(formatTokensCompact(max), 7),
|
|
2542
|
+
padStart(formatTokensCompact(max / 2), 7),
|
|
2543
|
+
padStart("0", 7)
|
|
2544
|
+
];
|
|
2545
|
+
const yLabelRow = (row) => {
|
|
2546
|
+
if (row === 0) return yLabels[0];
|
|
2547
|
+
if (row === Math.floor((height - 1) / 2)) return yLabels[1];
|
|
2548
|
+
if (row === height - 1) return yLabels[2];
|
|
2549
|
+
return " ".repeat(7);
|
|
2550
|
+
};
|
|
2551
|
+
const lines = [heading, ""];
|
|
2552
|
+
for (let row = 0; row < height; row += 1) {
|
|
2553
|
+
const cells = colsPerBucket.map((col) => center(col[row], cellWidth));
|
|
2554
|
+
lines.push(`${c.dim(yLabelRow(row))} ${c.dim("\u2502")} ${cells.join("")}`);
|
|
2555
|
+
}
|
|
2556
|
+
const baseline = "\u2500".repeat(Math.min(w - 9, cellWidth * trend.length));
|
|
2557
|
+
lines.push(`${" ".repeat(7)} ${c.dim("\u2514")}${c.dim(baseline)}`);
|
|
2558
|
+
const sampleLabel = trend[0]?.label ?? "";
|
|
2559
|
+
const labelStep = Math.max(1, Math.ceil((sampleLabel.length + 1) / cellWidth));
|
|
2560
|
+
const xCells = trend.map((b, i) => {
|
|
2561
|
+
if (i % labelStep !== 0) return " ".repeat(cellWidth);
|
|
2562
|
+
const span = cellWidth * Math.min(labelStep, trend.length - i);
|
|
2563
|
+
return center(b.label, span);
|
|
2564
|
+
});
|
|
2565
|
+
let xLine = "";
|
|
2566
|
+
for (let i = 0; i < xCells.length; i += 1) {
|
|
2567
|
+
if (i % labelStep === 0) xLine += xCells[i];
|
|
2568
|
+
}
|
|
2569
|
+
lines.push(`${" ".repeat(8)} ${c.dim(xLine)}`);
|
|
2570
|
+
lines.push("");
|
|
2571
|
+
lines.push(
|
|
2572
|
+
`${" ".repeat(8)}${c.input("\u25CF")} input ${c.cacheWrite("\u25CF")} cache-write ${c.cacheRead("\u25CF")} cache-read ${c.output("\u25CF")} output`
|
|
2573
|
+
);
|
|
2574
|
+
return lines;
|
|
2575
|
+
}
|
|
2576
|
+
function renderBreakdowns(c, w, data) {
|
|
2577
|
+
const primary = data.breakdown.slice(0, 10);
|
|
2578
|
+
const left = renderBreakdownColumn(
|
|
2579
|
+
c,
|
|
2580
|
+
Math.floor((w - 2) / 2),
|
|
2581
|
+
`Top ${data.by}s (by cost)`,
|
|
2582
|
+
primary,
|
|
2583
|
+
data.totals.cost
|
|
2584
|
+
);
|
|
2585
|
+
const right = renderBreakdownColumn(
|
|
2586
|
+
c,
|
|
2587
|
+
Math.floor((w - 2) / 2),
|
|
2588
|
+
`Top ${data.by}s (by conversations)`,
|
|
2589
|
+
primary.slice().sort((a, b) => b.turns - a.turns).slice(0, 10),
|
|
2590
|
+
Math.max(1, ...primary.map((r) => r.turns)),
|
|
2591
|
+
"turns"
|
|
2592
|
+
);
|
|
2593
|
+
return twoColumns(left, right, 2).split("\n");
|
|
2594
|
+
}
|
|
2595
|
+
function renderBreakdownColumn(c, innerWidth, title, rows, scale, metric = "cost") {
|
|
2596
|
+
const labelW = Math.max(10, Math.min(22, innerWidth - 30));
|
|
2597
|
+
const headers = [
|
|
2598
|
+
padEnd("#", 2),
|
|
2599
|
+
padEnd(metric === "cost" ? "Item" : "Item", labelW),
|
|
2600
|
+
padStart("Conv", 5),
|
|
2601
|
+
padStart("Reqs", 6),
|
|
2602
|
+
padStart("Tokens", 8),
|
|
2603
|
+
padStart(metric === "cost" ? "Cost" : "Cost", 8)
|
|
2604
|
+
];
|
|
2605
|
+
const bodyLines = [];
|
|
2606
|
+
bodyLines.push(c.dim(headers.join(" ")));
|
|
2607
|
+
bodyLines.push(c.dim("\u2500".repeat(Math.min(innerWidth - 2, headers.join(" ").length))));
|
|
2608
|
+
if (rows.length === 0) {
|
|
2609
|
+
bodyLines.push(c.dim("(no data in this window)"));
|
|
2610
|
+
}
|
|
2611
|
+
rows.forEach((r, i) => {
|
|
2612
|
+
const lbl = truncate(r.label, labelW);
|
|
2613
|
+
bodyLines.push(
|
|
2614
|
+
[
|
|
2615
|
+
padEnd(String(i + 1), 2),
|
|
2616
|
+
padEnd(lbl, labelW),
|
|
2617
|
+
padStart(r.turns.toLocaleString(), 5),
|
|
2618
|
+
padStart(r.requests.toLocaleString(), 6),
|
|
2619
|
+
padStart(formatTokensCompact(r.tokens), 8),
|
|
2620
|
+
padStart(formatUSD(r.cost), 8)
|
|
2621
|
+
].join(" ")
|
|
2622
|
+
);
|
|
2623
|
+
const ratio = metric === "cost" ? r.cost / Math.max(1e-9, scale) : r.turns / Math.max(1e-9, scale);
|
|
2624
|
+
const barW = innerWidth - 4 - 2 - labelW;
|
|
2625
|
+
bodyLines.push(`${" ".repeat(3)}${hbar(ratio, Math.max(8, Math.min(40, barW)), c.brand)}`);
|
|
2626
|
+
});
|
|
2627
|
+
return box(title, bodyLines, innerWidth, c);
|
|
2628
|
+
}
|
|
2629
|
+
function renderHeatmap(c, w, records) {
|
|
2630
|
+
const stats = computeActivityStats(records, { source: "all" });
|
|
2631
|
+
const heat = stats.heatmap;
|
|
2632
|
+
const max = stats.heatmapMax;
|
|
2633
|
+
if (max === 0) {
|
|
2634
|
+
return [`${c.brand("\u25B8")} ${c.bold("Activity heatmap")}`, "", c.dim(" (no activity yet \u2014 start a session and re-run)")];
|
|
2635
|
+
}
|
|
2636
|
+
const dayLabels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
2637
|
+
const density = ["\xB7", "\u25A2", "\u25A3", "\u25A4", "\u25A5", "\u25A6"];
|
|
2638
|
+
const cell = (v) => {
|
|
2639
|
+
if (v === 0) return c.dim("\xB7");
|
|
2640
|
+
const idx = Math.min(density.length - 1, Math.max(1, Math.ceil(v / max * (density.length - 1))));
|
|
2641
|
+
const ch = density[idx];
|
|
2642
|
+
return idx < 3 ? c.input(ch) : idx < 5 ? c.cacheRead(ch) : c.output(ch);
|
|
2643
|
+
};
|
|
2644
|
+
const heading = `${c.brand("\u25B8")} ${c.bold("Activity heatmap")} ${c.dim("(local time, day-of-week \xD7 hour)")}`;
|
|
2645
|
+
const hourHeader = " " + Array.from({ length: 24 }, (_, h) => padStart(String(h), 2)).join(" ");
|
|
2646
|
+
const lines = [heading, "", c.dim(hourHeader)];
|
|
2647
|
+
for (let dow = 0; dow < 7; dow += 1) {
|
|
2648
|
+
const sourceDow = (dow + 1) % 7;
|
|
2649
|
+
const row = heat[sourceDow] ?? new Array(24).fill(0);
|
|
2650
|
+
const cells = row.map((v) => padStart(cell(v), 2)).join(" ");
|
|
2651
|
+
lines.push(`${c.dim(padEnd(dayLabels[dow], 4))}${cells}`);
|
|
2652
|
+
}
|
|
2653
|
+
return lines;
|
|
2654
|
+
}
|
|
2655
|
+
function renderFooter(c, w, records, data, opts) {
|
|
2656
|
+
const filters = [];
|
|
2657
|
+
if (opts.model) filters.push(`model~${opts.model}`);
|
|
2658
|
+
if (opts.project) filters.push(`project~${opts.project}`);
|
|
2659
|
+
if (data.fromIso || data.untilIso) {
|
|
2660
|
+
const range = `${data.fromIso?.slice(0, 10) ?? "\u2026"} \u2192 ${data.untilIso?.slice(0, 10) ?? "\u2026"}`;
|
|
2661
|
+
filters.push(range);
|
|
2662
|
+
}
|
|
2663
|
+
const scope = [
|
|
2664
|
+
`range: ${data.range}`,
|
|
2665
|
+
`source: ${data.source}`,
|
|
2666
|
+
`scope: ${records.length.toLocaleString()} records \xB7 ${data.totals.turns.toLocaleString()} conversations`
|
|
2667
|
+
];
|
|
2668
|
+
if (filters.length > 0) scope.push(`filters: ${filters.join(", ")}`);
|
|
2669
|
+
return [
|
|
2670
|
+
c.dim(scope.join(" \xB7 ")),
|
|
2671
|
+
c.dim(`Use \`ccgauge report\` for a CI-friendly text summary, or \`ccgauge report -d --json\` to pipe.`)
|
|
2672
|
+
];
|
|
2673
|
+
}
|
|
2674
|
+
|
|
1828
2675
|
// lib/cli-report/index.ts
|
|
1829
2676
|
var REPORT_RANGES = ["today", "1d", "7d", "30d", "90d", "all"];
|
|
1830
2677
|
var DIMS = ["model", "project", "session"];
|
|
@@ -1838,16 +2685,43 @@ var DEFAULT_REPORT = {
|
|
|
1838
2685
|
json: false,
|
|
1839
2686
|
color: true,
|
|
1840
2687
|
showTrend: true,
|
|
1841
|
-
showBreakdown: true
|
|
2688
|
+
showBreakdown: true,
|
|
2689
|
+
dashboard: false,
|
|
2690
|
+
compact: false,
|
|
2691
|
+
banner: true
|
|
1842
2692
|
};
|
|
1843
2693
|
async function runReport(opts) {
|
|
1844
2694
|
const filled = normalizeReportOptions(opts);
|
|
1845
2695
|
const scan = await getCachedScan();
|
|
1846
2696
|
const sources = filled.source === "all" ? ALL_PROVIDER_IDS : [filled.source];
|
|
1847
|
-
const data = computeReportData(scan
|
|
2697
|
+
const data = computeReportData(scan, sources, filled);
|
|
1848
2698
|
if (filled.json) return JSON.stringify(data, null, 2);
|
|
2699
|
+
if (filled.dashboard) {
|
|
2700
|
+
const filteredRecords = filterRecordsForReport(scan, sources, filled);
|
|
2701
|
+
return renderDash(scan, data, filled, filteredRecords);
|
|
2702
|
+
}
|
|
1849
2703
|
return renderText(data, filled);
|
|
1850
2704
|
}
|
|
2705
|
+
function filterRecordsForReport(scan, sources, o) {
|
|
2706
|
+
const dates = resolveRange(o);
|
|
2707
|
+
const fromIso = dates.from?.toISOString();
|
|
2708
|
+
const toIso = dates.until?.toISOString();
|
|
2709
|
+
const sourceSet = new Set(sources);
|
|
2710
|
+
const modelNeedle = o.model?.toLowerCase();
|
|
2711
|
+
const projectNeedle = o.project?.toLowerCase();
|
|
2712
|
+
return scan.records.filter((r) => {
|
|
2713
|
+
if (!sourceSet.has(r.source)) return false;
|
|
2714
|
+
if (fromIso && r.timestamp < fromIso) return false;
|
|
2715
|
+
if (toIso && r.timestamp > toIso) return false;
|
|
2716
|
+
if (modelNeedle && !r.model.toLowerCase().includes(modelNeedle)) return false;
|
|
2717
|
+
if (projectNeedle) {
|
|
2718
|
+
const cwd = (r.cwd || "").toLowerCase();
|
|
2719
|
+
const leaf = cwd.split(/[/\\]+/).pop() ?? "";
|
|
2720
|
+
if (!cwd.includes(projectNeedle) && !leaf.includes(projectNeedle)) return false;
|
|
2721
|
+
}
|
|
2722
|
+
return true;
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
1851
2725
|
function normalizeReportOptions(opts) {
|
|
1852
2726
|
const filled = { ...DEFAULT_REPORT, ...opts };
|
|
1853
2727
|
if (!isReportRange(filled.range)) {
|
|
@@ -1879,14 +2753,15 @@ function isDim(v) {
|
|
|
1879
2753
|
function invalidOptionMessage(name, value, expected) {
|
|
1880
2754
|
return `invalid ${name}: ${JSON.stringify(value)}. Expected one of: ${expected.join(", ")}`;
|
|
1881
2755
|
}
|
|
1882
|
-
function computeReportData(
|
|
2756
|
+
function computeReportData(scan, sources, o) {
|
|
2757
|
+
const allRecords = scan.records;
|
|
1883
2758
|
const dates = resolveRange(o);
|
|
1884
2759
|
const baseOpts = {
|
|
1885
2760
|
from: dates.from ?? void 0,
|
|
1886
|
-
to: dates.until ?? void 0
|
|
1887
|
-
|
|
1888
|
-
//
|
|
1889
|
-
|
|
2761
|
+
to: dates.until ?? void 0
|
|
2762
|
+
// `o.model` / `o.project` are substring patterns, not exact matches —
|
|
2763
|
+
// they're applied per-record by `withinSrcAndFilters` below, so we
|
|
2764
|
+
// deliberately leave the aggregator's exact-match filters unset.
|
|
1890
2765
|
};
|
|
1891
2766
|
const totals = {
|
|
1892
2767
|
input: 0,
|
|
@@ -1897,7 +2772,8 @@ function computeReportData(allRecords, sources, o) {
|
|
|
1897
2772
|
total: 0,
|
|
1898
2773
|
cost: 0,
|
|
1899
2774
|
saved: 0,
|
|
1900
|
-
requests: 0
|
|
2775
|
+
requests: 0,
|
|
2776
|
+
turns: 0
|
|
1901
2777
|
};
|
|
1902
2778
|
const trendBuckets = /* @__PURE__ */ new Map();
|
|
1903
2779
|
for (const source of sources) {
|
|
@@ -1913,19 +2789,41 @@ function computeReportData(allRecords, sources, o) {
|
|
|
1913
2789
|
totals.saved += t.saved;
|
|
1914
2790
|
totals.requests += t.requests;
|
|
1915
2791
|
for (const r of sourceRecs) totals.reasoning += r.usage.reasoning_tokens ?? 0;
|
|
2792
|
+
const turnSummaries = summarizeTurns(sourceRecs, scan.userRecords, scan.parentMap);
|
|
2793
|
+
totals.turns += turnSummaries.size;
|
|
2794
|
+
const turnsByKey = /* @__PURE__ */ new Map();
|
|
2795
|
+
for (const tn of turnSummaries.values()) {
|
|
2796
|
+
const { key } = bucketKey(tn.firstTimestamp, o.gran);
|
|
2797
|
+
turnsByKey.set(key, (turnsByKey.get(key) ?? 0) + 1);
|
|
2798
|
+
}
|
|
1916
2799
|
const buckets = aggregateByTime(sourceRecs, o.gran, opts);
|
|
1917
2800
|
for (const b of buckets) {
|
|
1918
2801
|
const ex = trendBuckets.get(b.key);
|
|
2802
|
+
const tnCount = turnsByKey.get(b.key) ?? 0;
|
|
1919
2803
|
if (ex) {
|
|
1920
2804
|
ex.cost += b.cost;
|
|
1921
2805
|
ex.tokens += b.totalTokens;
|
|
2806
|
+
ex.turns += tnCount;
|
|
2807
|
+
ex.input += b.inputTokens;
|
|
2808
|
+
ex.output += b.outputTokens;
|
|
2809
|
+
ex.cacheRead += b.cacheReadTokens;
|
|
2810
|
+
ex.cacheWrite += b.cacheCreationTokens;
|
|
1922
2811
|
} else {
|
|
1923
|
-
trendBuckets.set(b.key, {
|
|
2812
|
+
trendBuckets.set(b.key, {
|
|
2813
|
+
label: b.label,
|
|
2814
|
+
cost: b.cost,
|
|
2815
|
+
tokens: b.totalTokens,
|
|
2816
|
+
turns: tnCount,
|
|
2817
|
+
input: b.inputTokens,
|
|
2818
|
+
output: b.outputTokens,
|
|
2819
|
+
cacheRead: b.cacheReadTokens,
|
|
2820
|
+
cacheWrite: b.cacheCreationTokens
|
|
2821
|
+
});
|
|
1924
2822
|
}
|
|
1925
2823
|
}
|
|
1926
2824
|
}
|
|
1927
2825
|
const trend = Array.from(trendBuckets.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([, v]) => v);
|
|
1928
|
-
const breakdown = buildBreakdown(
|
|
2826
|
+
const breakdown = buildBreakdown(scan, sources, baseOpts, o);
|
|
1929
2827
|
return {
|
|
1930
2828
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1931
2829
|
range: o.range,
|
|
@@ -1939,7 +2837,8 @@ function computeReportData(allRecords, sources, o) {
|
|
|
1939
2837
|
breakdown
|
|
1940
2838
|
};
|
|
1941
2839
|
}
|
|
1942
|
-
function buildBreakdown(
|
|
2840
|
+
function buildBreakdown(scan, sources, base, o) {
|
|
2841
|
+
const allRecords = scan.records;
|
|
1943
2842
|
if (o.by === "model") {
|
|
1944
2843
|
const rows2 = [];
|
|
1945
2844
|
for (const source of sources) {
|
|
@@ -1947,11 +2846,16 @@ function buildBreakdown(allRecords, sources, base, o) {
|
|
|
1947
2846
|
const filtered = allRecords.filter((r) => withinSrcAndFilters(r, opts, o));
|
|
1948
2847
|
const models = aggregateByModel(filtered, opts);
|
|
1949
2848
|
const provider = getProvider(source);
|
|
2849
|
+
const turnsByModel = /* @__PURE__ */ new Map();
|
|
2850
|
+
for (const tn of summarizeTurns(filtered, scan.userRecords, scan.parentMap).values()) {
|
|
2851
|
+
turnsByModel.set(tn.firstModel, (turnsByModel.get(tn.firstModel) ?? 0) + 1);
|
|
2852
|
+
}
|
|
1950
2853
|
for (const m of models) {
|
|
1951
2854
|
rows2.push({
|
|
1952
2855
|
key: `${source}::${m.model}`,
|
|
1953
2856
|
label: provider.shortenModel(m.model),
|
|
1954
2857
|
requests: m.requests,
|
|
2858
|
+
turns: turnsByModel.get(m.model) ?? 0,
|
|
1955
2859
|
tokens: m.totalTokens,
|
|
1956
2860
|
cost: m.cost,
|
|
1957
2861
|
share: 0,
|
|
@@ -1968,11 +2872,17 @@ function buildBreakdown(allRecords, sources, base, o) {
|
|
|
1968
2872
|
const opts = { ...base, source };
|
|
1969
2873
|
const filtered = allRecords.filter((r) => withinSrcAndFilters(r, opts, o));
|
|
1970
2874
|
const projects = aggregateByProject(filtered, opts);
|
|
2875
|
+
const turnsByCwd = /* @__PURE__ */ new Map();
|
|
2876
|
+
for (const tn of summarizeTurns(filtered, scan.userRecords, scan.parentMap).values()) {
|
|
2877
|
+
const key = tn.cwd || "(unknown)";
|
|
2878
|
+
turnsByCwd.set(key, (turnsByCwd.get(key) ?? 0) + 1);
|
|
2879
|
+
}
|
|
1971
2880
|
for (const p of projects) {
|
|
1972
2881
|
rows2.push({
|
|
1973
2882
|
key: `${source}::${p.cwd}`,
|
|
1974
2883
|
label: p.projectName,
|
|
1975
2884
|
requests: p.requests,
|
|
2885
|
+
turns: turnsByCwd.get(p.cwd) ?? 0,
|
|
1976
2886
|
tokens: p.totalTokens,
|
|
1977
2887
|
cost: p.cost,
|
|
1978
2888
|
share: 0,
|
|
@@ -1987,15 +2897,23 @@ function buildBreakdown(allRecords, sources, base, o) {
|
|
|
1987
2897
|
const opts = { ...base, source };
|
|
1988
2898
|
const filtered = allRecords.filter((r) => withinSrcAndFilters(r, opts, o));
|
|
1989
2899
|
const sessions = aggregateBySession(filtered, [], opts);
|
|
2900
|
+
const turnsBySession = /* @__PURE__ */ new Map();
|
|
2901
|
+
for (const tn of summarizeTurns(filtered, scan.userRecords, scan.parentMap).values()) {
|
|
2902
|
+
turnsBySession.set(tn.sessionId, (turnsBySession.get(tn.sessionId) ?? 0) + 1);
|
|
2903
|
+
}
|
|
1990
2904
|
for (const s of sessions) {
|
|
1991
2905
|
rows.push({
|
|
1992
2906
|
key: `${source}::${s.sessionId}`,
|
|
1993
2907
|
label: s.title ?? s.sessionId.slice(0, 8),
|
|
1994
2908
|
requests: s.requests,
|
|
2909
|
+
turns: turnsBySession.get(s.sessionId) ?? 0,
|
|
1995
2910
|
tokens: s.totalTokens,
|
|
1996
2911
|
cost: s.cost,
|
|
1997
2912
|
share: 0,
|
|
1998
|
-
|
|
2913
|
+
// Worktree-aware so the breakdown reads
|
|
2914
|
+
// `ai-self-web (playwright)` instead of just `playwright`,
|
|
2915
|
+
// matching the dashboard's usage / sessions tables.
|
|
2916
|
+
sub: s.projectLabel
|
|
1999
2917
|
});
|
|
2000
2918
|
}
|
|
2001
2919
|
}
|
|
@@ -2054,17 +2972,17 @@ function parseReportDate(raw, boundary) {
|
|
|
2054
2972
|
return date;
|
|
2055
2973
|
}
|
|
2056
2974
|
function makeColors(enabled) {
|
|
2057
|
-
const
|
|
2975
|
+
const wrap2 = (code) => (s) => enabled ? `\x1B[${code}m${s}\x1B[0m` : String(s);
|
|
2058
2976
|
return {
|
|
2059
|
-
bold:
|
|
2060
|
-
dim:
|
|
2061
|
-
cyan:
|
|
2062
|
-
green:
|
|
2063
|
-
yellow:
|
|
2064
|
-
red:
|
|
2065
|
-
blue:
|
|
2066
|
-
magenta:
|
|
2067
|
-
brand:
|
|
2977
|
+
bold: wrap2("1"),
|
|
2978
|
+
dim: wrap2("2"),
|
|
2979
|
+
cyan: wrap2("36"),
|
|
2980
|
+
green: wrap2("32"),
|
|
2981
|
+
yellow: wrap2("33"),
|
|
2982
|
+
red: wrap2("31"),
|
|
2983
|
+
blue: wrap2("34"),
|
|
2984
|
+
magenta: wrap2("35"),
|
|
2985
|
+
brand: wrap2("38;2;129;140;248")
|
|
2068
2986
|
};
|
|
2069
2987
|
}
|
|
2070
2988
|
function renderText(d, o) {
|
|
@@ -2097,21 +3015,15 @@ function renderText(d, o) {
|
|
|
2097
3015
|
]
|
|
2098
3016
|
];
|
|
2099
3017
|
if (t.reasoning > 0) {
|
|
2100
|
-
tokenRows.push([
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
"Total",
|
|
2110
|
-
c.bold(formatTokensCompact(t.total)),
|
|
2111
|
-
"Requests",
|
|
2112
|
-
t.requests.toLocaleString()
|
|
2113
|
-
]);
|
|
2114
|
-
}
|
|
3018
|
+
tokenRows.push(["Reasoning", c.dim(formatTokensCompact(t.reasoning)), "", ""]);
|
|
3019
|
+
}
|
|
3020
|
+
tokenRows.push(["Total", c.bold(formatTokensCompact(t.total)), "", ""]);
|
|
3021
|
+
tokenRows.push([
|
|
3022
|
+
"Convos",
|
|
3023
|
+
c.bold(t.turns.toLocaleString()),
|
|
3024
|
+
"Requests",
|
|
3025
|
+
t.requests.toLocaleString()
|
|
3026
|
+
]);
|
|
2115
3027
|
lines.push(renderPairedKv(tokenRows, c));
|
|
2116
3028
|
lines.push("");
|
|
2117
3029
|
lines.push(c.brand("\u25B8") + " " + c.bold("Cost"));
|
|
@@ -2147,16 +3059,17 @@ function renderText(d, o) {
|
|
|
2147
3059
|
lines.push(
|
|
2148
3060
|
c.brand("\u25B8") + " " + c.bold(`Top ${d.breakdown.length} ${dimLabel}s`) + " " + c.dim("(by cost)")
|
|
2149
3061
|
);
|
|
2150
|
-
const headers = ["#", dimLabel, "Reqs", "Tokens", "Cost", "Share"];
|
|
3062
|
+
const headers = ["#", dimLabel, "Convos", "Reqs", "Tokens", "Cost", "Share"];
|
|
2151
3063
|
const rows = d.breakdown.map((r, i) => [
|
|
2152
3064
|
String(i + 1),
|
|
2153
|
-
|
|
3065
|
+
truncate2(r.label, 28),
|
|
3066
|
+
r.turns.toLocaleString(),
|
|
2154
3067
|
r.requests.toLocaleString(),
|
|
2155
3068
|
formatTokensCompact(r.tokens),
|
|
2156
3069
|
formatUSD(r.cost),
|
|
2157
3070
|
formatPct(r.share, 1)
|
|
2158
3071
|
]);
|
|
2159
|
-
lines.push(renderTable(headers, rows, c, [false, false, true, true, true, true]));
|
|
3072
|
+
lines.push(renderTable(headers, rows, c, [false, false, true, true, true, true, true]));
|
|
2160
3073
|
lines.push("");
|
|
2161
3074
|
}
|
|
2162
3075
|
return lines.join("\n");
|
|
@@ -2165,24 +3078,24 @@ function renderPairedKv(rows, c) {
|
|
|
2165
3078
|
const w = [0, 0, 0, 0];
|
|
2166
3079
|
for (const r of rows) for (let i = 0; i < 4; i += 1) {
|
|
2167
3080
|
const cell = r[i];
|
|
2168
|
-
if (
|
|
3081
|
+
if (visibleLen2(cell) > w[i]) w[i] = visibleLen2(cell);
|
|
2169
3082
|
}
|
|
2170
3083
|
const lines = [];
|
|
2171
3084
|
for (const [lk, lv, rk, rv] of rows) {
|
|
2172
|
-
const left = `${c.dim(
|
|
2173
|
-
const right = rk ? `${c.dim(
|
|
3085
|
+
const left = `${c.dim(padEnd2(lk, w[0]))} ${padEnd2(lv, w[1])}`;
|
|
3086
|
+
const right = rk ? `${c.dim(padEnd2(rk, w[2]))} ${rv}` : "";
|
|
2174
3087
|
lines.push(` ${left} ${right}`.replace(/\s+$/, ""));
|
|
2175
3088
|
}
|
|
2176
3089
|
return lines.join("\n");
|
|
2177
3090
|
}
|
|
2178
3091
|
function renderTable(headers, rows, c, rightAlign) {
|
|
2179
3092
|
const widths = headers.map(
|
|
2180
|
-
(h, i) => Math.max(h.length, ...rows.map((r) =>
|
|
3093
|
+
(h, i) => Math.max(h.length, ...rows.map((r) => visibleLen2(r[i] ?? "")))
|
|
2181
3094
|
);
|
|
2182
|
-
const headLine = " " + headers.map((h, i) => rightAlign[i] ?
|
|
3095
|
+
const headLine = " " + headers.map((h, i) => rightAlign[i] ? padStart2(h, widths[i]) : padEnd2(h, widths[i])).map((s) => c.dim(s)).join(" ");
|
|
2183
3096
|
const sepLine = " " + widths.map((w) => "\u2500".repeat(w)).map((s) => c.dim(s)).join(" ");
|
|
2184
3097
|
const bodyLines = rows.map(
|
|
2185
|
-
(row) => " " + row.map((cell, i) => rightAlign[i] ?
|
|
3098
|
+
(row) => " " + row.map((cell, i) => rightAlign[i] ? padStart2(cell, widths[i]) : padEnd2(cell, widths[i])).join(" ")
|
|
2186
3099
|
);
|
|
2187
3100
|
return [headLine, sepLine, ...bodyLines].join("\n");
|
|
2188
3101
|
}
|
|
@@ -2192,21 +3105,23 @@ function barString(ratio, width) {
|
|
|
2192
3105
|
return "\u2587".repeat(filled) + "\u2500".repeat(width - filled);
|
|
2193
3106
|
}
|
|
2194
3107
|
var ESC = String.fromCharCode(27);
|
|
2195
|
-
var
|
|
2196
|
-
function
|
|
2197
|
-
return s.replace(
|
|
3108
|
+
var ANSI_RE2 = new RegExp(ESC + "\\[[0-9;]*m", "g");
|
|
3109
|
+
function visibleLen2(s) {
|
|
3110
|
+
return s.replace(ANSI_RE2, "").length;
|
|
2198
3111
|
}
|
|
2199
|
-
function
|
|
2200
|
-
return s + " ".repeat(Math.max(0, w -
|
|
3112
|
+
function padEnd2(s, w) {
|
|
3113
|
+
return s + " ".repeat(Math.max(0, w - visibleLen2(s)));
|
|
2201
3114
|
}
|
|
2202
|
-
function
|
|
2203
|
-
return " ".repeat(Math.max(0, w -
|
|
3115
|
+
function padStart2(s, w) {
|
|
3116
|
+
return " ".repeat(Math.max(0, w - visibleLen2(s))) + s;
|
|
2204
3117
|
}
|
|
2205
|
-
function
|
|
3118
|
+
function truncate2(s, max) {
|
|
2206
3119
|
if (s.length <= max) return s;
|
|
2207
3120
|
return s.slice(0, max - 1) + "\u2026";
|
|
2208
3121
|
}
|
|
2209
3122
|
export {
|
|
2210
3123
|
DEFAULT_REPORT,
|
|
3124
|
+
filterRecordsForReport,
|
|
3125
|
+
renderText,
|
|
2211
3126
|
runReport
|
|
2212
3127
|
};
|