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.
Files changed (75) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-build-manifest.json +58 -57
  3. package/.next/standalone/.next/app-path-routes-manifest.json +8 -8
  4. package/.next/standalone/.next/build-manifest.json +7 -7
  5. package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
  6. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  7. package/.next/standalone/.next/server/app/api/blocks/route_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/api/export/usage/route.js +1 -1
  9. package/.next/standalone/.next/server/app/api/export/usage/route_client-reference-manifest.js +1 -1
  10. package/.next/standalone/.next/server/app/api/pricing/route_client-reference-manifest.js +1 -1
  11. package/.next/standalone/.next/server/app/api/projects/route.js +1 -1
  12. package/.next/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  13. package/.next/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  14. package/.next/standalone/.next/server/app/api/scan/route_client-reference-manifest.js +1 -1
  15. package/.next/standalone/.next/server/app/api/sessions/route.js +1 -1
  16. package/.next/standalone/.next/server/app/api/sessions/route.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/api/usage/route.js +1 -1
  19. package/.next/standalone/.next/server/app/api/usage/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/usage/route_client-reference-manifest.js +1 -1
  21. package/.next/standalone/.next/server/app/models/page.js +2 -2
  22. package/.next/standalone/.next/server/app/models/page.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/models/page_client-reference-manifest.js +1 -1
  24. package/.next/standalone/.next/server/app/page.js +2 -2
  25. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  27. package/.next/standalone/.next/server/app/projects/[id]/page.js +1 -1
  28. package/.next/standalone/.next/server/app/projects/[id]/page.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/projects/[id]/page_client-reference-manifest.js +1 -1
  30. package/.next/standalone/.next/server/app/projects/page.js +1 -1
  31. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  33. package/.next/standalone/.next/server/app/sessions/[id]/page.js +2 -2
  34. package/.next/standalone/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/sessions/page.js +1 -1
  37. package/.next/standalone/.next/server/app/sessions/page.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  39. package/.next/standalone/.next/server/app/settings/page.js +1 -1
  40. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/usage/page.js +3 -3
  42. package/.next/standalone/.next/server/app/usage/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app-paths-manifest.json +8 -8
  45. package/.next/standalone/.next/server/chunks/125.js +1 -0
  46. package/.next/standalone/.next/server/chunks/567.js +2 -2
  47. package/.next/standalone/.next/server/chunks/730.js +1 -0
  48. package/.next/standalone/.next/server/edge-runtime-webpack.js +2 -0
  49. package/.next/standalone/.next/server/functions-config-manifest.json +1 -1
  50. package/.next/standalone/.next/server/middleware-build-manifest.js +1 -1
  51. package/.next/standalone/.next/server/middleware-manifest.json +28 -2
  52. package/.next/standalone/.next/server/middleware.js +14 -0
  53. package/.next/standalone/.next/server/pages/500.html +1 -1
  54. package/.next/standalone/.next/static/chunks/148-f2cba0b76260b8d3.js +1 -0
  55. package/.next/standalone/.next/static/chunks/253-dae1b01a941e7c13.js +1 -0
  56. package/.next/standalone/.next/static/chunks/app/{layout-4f3538437c5e8366.js → layout-0adb4fc0305adf29.js} +1 -1
  57. package/.next/standalone/.next/static/chunks/app/usage/page-7fcc2a2d931307d5.js +1 -0
  58. package/.next/standalone/.next/static/chunks/{main-69f0897f4e7ce710.js → main-2cb79280eaf22774.js} +1 -1
  59. package/.next/standalone/.next/static/chunks/{webpack-3fcacae817f3ffab.js → webpack-73dfee69c0ddc58e.js} +1 -1
  60. package/.next/standalone/.next/static/css/bde47638beb0c717.css +3 -0
  61. package/.next/standalone/package.json +2 -1
  62. package/.next/standalone/public/codex-logo.png +0 -0
  63. package/CHANGELOG.md +363 -0
  64. package/README.md +6 -2
  65. package/README.zh-CN.md +6 -2
  66. package/bin/cli.mjs +404 -92
  67. package/dist/mcp/server.mjs +16 -16
  68. package/dist/report/index.mjs +971 -56
  69. package/package.json +2 -1
  70. package/.next/standalone/.next/server/chunks/971.js +0 -1
  71. package/.next/standalone/.next/static/chunks/148-6c2eaf5508bfe739.js +0 -1
  72. package/.next/standalone/.next/static/chunks/app/usage/page-18fd820a3111bd5b.js +0 -1
  73. package/.next/standalone/.next/static/css/fbd2c395e5bf32cb.css +0 -3
  74. /package/.next/standalone/.next/static/{ir1LZCnQKkiNUVXLprtzh → jncTEohJB76Iq9TUm3G21}/_buildManifest.js +0 -0
  75. /package/.next/standalone/.next/static/{ir1LZCnQKkiNUVXLprtzh → jncTEohJB76Iq9TUm3G21}/_ssgManifest.js +0 -0
@@ -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.records, sources, filled);
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(allRecords, sources, o) {
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
- models: o.model ? void 0 : void 0,
1888
- // handled post-filter
1889
- projects: o.project ? void 0 : void 0
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, { label: b.label, cost: b.cost, tokens: b.totalTokens });
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(allRecords, sources, baseOpts, o);
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(allRecords, sources, base, o) {
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
- sub: s.projectName
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 wrap = (code) => (s) => enabled ? `\x1B[${code}m${s}\x1B[0m` : String(s);
2975
+ const wrap2 = (code) => (s) => enabled ? `\x1B[${code}m${s}\x1B[0m` : String(s);
2058
2976
  return {
2059
- bold: wrap("1"),
2060
- dim: wrap("2"),
2061
- cyan: wrap("36"),
2062
- green: wrap("32"),
2063
- yellow: wrap("33"),
2064
- red: wrap("31"),
2065
- blue: wrap("34"),
2066
- magenta: wrap("35"),
2067
- brand: wrap("38;2;129;140;248")
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
- "Reasoning",
2102
- c.dim(formatTokensCompact(t.reasoning)),
2103
- "Requests",
2104
- t.requests.toLocaleString()
2105
- ]);
2106
- tokenRows.push(["Total", c.bold(formatTokensCompact(t.total)), "", ""]);
2107
- } else {
2108
- tokenRows.push([
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
- truncate(r.label, 28),
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 (visibleLen(cell) > w[i]) w[i] = visibleLen(cell);
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(padEnd(lk, w[0]))} ${padEnd(lv, w[1])}`;
2173
- const right = rk ? `${c.dim(padEnd(rk, w[2]))} ${rv}` : "";
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) => visibleLen(r[i] ?? "")))
3093
+ (h, i) => Math.max(h.length, ...rows.map((r) => visibleLen2(r[i] ?? "")))
2181
3094
  );
2182
- const headLine = " " + headers.map((h, i) => rightAlign[i] ? padStart(h, widths[i]) : padEnd(h, widths[i])).map((s) => c.dim(s)).join(" ");
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] ? padStart(cell, widths[i]) : padEnd(cell, widths[i])).join(" ")
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 ANSI_RE = new RegExp(ESC + "\\[[0-9;]*m", "g");
2196
- function visibleLen(s) {
2197
- return s.replace(ANSI_RE, "").length;
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 padEnd(s, w) {
2200
- return s + " ".repeat(Math.max(0, w - visibleLen(s)));
3112
+ function padEnd2(s, w) {
3113
+ return s + " ".repeat(Math.max(0, w - visibleLen2(s)));
2201
3114
  }
2202
- function padStart(s, w) {
2203
- return " ".repeat(Math.max(0, w - visibleLen(s))) + s;
3115
+ function padStart2(s, w) {
3116
+ return " ".repeat(Math.max(0, w - visibleLen2(s))) + s;
2204
3117
  }
2205
- function truncate(s, max) {
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
  };