repospend 0.0.2 → 0.0.3

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/dist/cli.js CHANGED
@@ -110,6 +110,7 @@ function toCsv(sessions) {
110
110
  "provider",
111
111
  "inputTokens",
112
112
  "cachedInputTokens",
113
+ "cacheCreationInputTokens",
113
114
  "outputTokens",
114
115
  "reasoningTokens",
115
116
  "reasoningOutputTokens",
@@ -194,11 +195,15 @@ import fs from "node:fs";
194
195
  import os from "node:os";
195
196
  import path from "node:path";
196
197
  var pricingInfo = {
197
- sourceName: "OpenAI API pricing",
198
+ sourceName: "Bundled OpenAI and Claude API pricing",
198
199
  sourceUrl: "https://developers.openai.com/api/docs/pricing",
200
+ sourceUrls: [
201
+ { label: "OpenAI pricing reference", url: "https://developers.openai.com/api/docs/pricing" },
202
+ { label: "Claude pricing reference", url: "https://platform.claude.com/docs/en/about-claude/pricing" }
203
+ ],
199
204
  unit: "USD per 1M tokens",
200
205
  updatedAt: "2026-05-18",
201
- note: "RepoSpend estimates API-equivalent cost from local token counts and public API-style Standard short-context pricing. This is not your actual ChatGPT/Codex bill; subscriptions, credits, provider terms, long-context pricing, regional processing, or other billing factors can make your real cost different."
206
+ note: "RepoSpend estimates API-equivalent cost from local token counts and public API-style Standard pricing. This is not your actual bill; subscriptions, credits, provider terms, cache behavior, regional processing, or other billing factors can make your real cost different."
202
207
  };
203
208
  var defaultPricing = {
204
209
  "gpt-5.5": { inputPerMillion: 5, cachedInputPerMillion: 0.5, outputPerMillion: 30, reasoningOutputPerMillion: 30 },
@@ -220,7 +225,17 @@ var defaultPricing = {
220
225
  "gpt-5-codex": { inputPerMillion: 1.25, cachedInputPerMillion: 0.125, outputPerMillion: 10, reasoningOutputPerMillion: 10 },
221
226
  "gpt-5-pro": { inputPerMillion: 15, outputPerMillion: 120, reasoningOutputPerMillion: 120 },
222
227
  "gpt-5-mini": { inputPerMillion: 0.25, cachedInputPerMillion: 0.025, outputPerMillion: 2, reasoningOutputPerMillion: 2 },
223
- "gpt-5-nano": { inputPerMillion: 0.05, cachedInputPerMillion: 5e-3, outputPerMillion: 0.4, reasoningOutputPerMillion: 0.4 }
228
+ "gpt-5-nano": { inputPerMillion: 0.05, cachedInputPerMillion: 5e-3, outputPerMillion: 0.4, reasoningOutputPerMillion: 0.4 },
229
+ "claude-opus-4-7": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
230
+ "claude-opus-4-6": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
231
+ "claude-opus-4-5": { inputPerMillion: 5, cacheCreationInputPerMillion: 6.25, cachedInputPerMillion: 0.5, outputPerMillion: 25 },
232
+ "claude-opus-4-1": { inputPerMillion: 15, cacheCreationInputPerMillion: 18.75, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
233
+ "claude-opus-4": { inputPerMillion: 15, cacheCreationInputPerMillion: 18.75, cachedInputPerMillion: 1.5, outputPerMillion: 75 },
234
+ "claude-sonnet-4-6": { inputPerMillion: 3, cacheCreationInputPerMillion: 3.75, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
235
+ "claude-sonnet-4-5": { inputPerMillion: 3, cacheCreationInputPerMillion: 3.75, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
236
+ "claude-sonnet-4": { inputPerMillion: 3, cacheCreationInputPerMillion: 3.75, cachedInputPerMillion: 0.3, outputPerMillion: 15 },
237
+ "claude-haiku-4-5": { inputPerMillion: 1, cacheCreationInputPerMillion: 1.25, cachedInputPerMillion: 0.1, outputPerMillion: 5 },
238
+ "claude-3-5-haiku": { inputPerMillion: 0.8, cacheCreationInputPerMillion: 1, cachedInputPerMillion: 0.08, outputPerMillion: 4 }
224
239
  };
225
240
  function loadPricingTable(pricingPath) {
226
241
  if (!pricingPath) {
@@ -248,16 +263,45 @@ function calculateCostUsd(usage, pricing) {
248
263
  if (!usage.model) {
249
264
  return void 0;
250
265
  }
251
- const modelPricing = pricing[usage.model] ?? pricing[usage.model.toLowerCase()];
266
+ const modelPricing = findModelPricing(usage.model, pricing);
252
267
  if (!modelPricing) {
253
268
  return void 0;
254
269
  }
255
- const billableInput = Math.max(usage.inputTokens - usage.cachedInputTokens, 0);
270
+ const cacheCreationInput = usage.cacheCreationInputTokens ?? 0;
271
+ const billableInput = Math.max(usage.inputTokens - usage.cachedInputTokens - cacheCreationInput, 0);
256
272
  const cachedRate = modelPricing.cachedInputPerMillion ?? modelPricing.inputPerMillion;
273
+ const cacheCreationRate = modelPricing.cacheCreationInputPerMillion ?? modelPricing.inputPerMillion;
257
274
  const reasoningRate = modelPricing.reasoningOutputPerMillion ?? modelPricing.outputPerMillion;
258
- const cost = billableInput / 1e6 * modelPricing.inputPerMillion + usage.cachedInputTokens / 1e6 * cachedRate + usage.outputTokens / 1e6 * modelPricing.outputPerMillion + usage.reasoningTokens / 1e6 * reasoningRate;
275
+ const cost = billableInput / 1e6 * modelPricing.inputPerMillion + usage.cachedInputTokens / 1e6 * cachedRate + cacheCreationInput / 1e6 * cacheCreationRate + usage.outputTokens / 1e6 * modelPricing.outputPerMillion + usage.reasoningTokens / 1e6 * reasoningRate;
259
276
  return Number(cost.toFixed(6));
260
277
  }
278
+ function findModelPricing(model, pricing) {
279
+ const lower = model.toLowerCase();
280
+ const exactPricing = pricing[model] ?? pricing[lower];
281
+ if (isUsablePricing(exactPricing)) return exactPricing;
282
+ const familyPricing = claudeFamilyPricing(lower, pricing);
283
+ if (isUsablePricing(familyPricing)) return familyPricing;
284
+ return void 0;
285
+ }
286
+ function claudeFamilyPricing(model, pricing) {
287
+ const families = [
288
+ "claude-opus-4-7",
289
+ "claude-opus-4-6",
290
+ "claude-opus-4-5",
291
+ "claude-opus-4-1",
292
+ "claude-opus-4",
293
+ "claude-sonnet-4-6",
294
+ "claude-sonnet-4-5",
295
+ "claude-sonnet-4",
296
+ "claude-haiku-4-5",
297
+ "claude-3-5-haiku"
298
+ ];
299
+ const family = families.find((candidate) => model === candidate || model.startsWith(`${candidate}-`));
300
+ return family ? pricing[family] : void 0;
301
+ }
302
+ function isUsablePricing(pricing) {
303
+ return typeof pricing?.inputPerMillion === "number" && pricing.inputPerMillion > 0 && typeof pricing.outputPerMillion === "number" && pricing.outputPerMillion > 0;
304
+ }
261
305
 
262
306
  // packages/core/src/config.ts
263
307
  function configPath() {
@@ -763,9 +807,9 @@ function pluralize(noun, value) {
763
807
 
764
808
  // packages/core/src/dashboard.ts
765
809
  function buildDashboardSnapshot(input) {
766
- const allSessions = input.sessions.filter((session) => session.totalTokens > 0);
810
+ const allSessions = input.sessions.filter(isDashboardVisibleSession);
767
811
  const filteredSessions = filterUsage(input.sessions, input.filters ?? {});
768
- const sessions = filteredSessions.filter((session) => session.totalTokens > 0);
812
+ const sessions = filteredSessions.filter(isDashboardVisibleSession);
769
813
  const summary = summarize(sessions);
770
814
  const repos = buildRepoRollups(sessions);
771
815
  const sourceApps = groupBySourceApp(sessions);
@@ -796,6 +840,9 @@ function buildDashboardSnapshot(input) {
796
840
  health: buildUsageHealth({ sessions, summary, repos, sourceApps, scan })
797
841
  };
798
842
  }
843
+ function isDashboardVisibleSession(session) {
844
+ return session.totalTokens > 0 || session.sourceClient === "claude";
845
+ }
799
846
 
800
847
  // packages/core/src/repo.ts
801
848
  import fs3 from "node:fs";
@@ -943,30 +990,30 @@ function applyActionMetadata(analysis, record) {
943
990
  }
944
991
  function finalizeCommandIssues(analysis, totalTokens) {
945
992
  const repeatedCommands = /* @__PURE__ */ new Map();
946
- const repeatableIssues = analysis.commandIssues.filter((issue) => issue.classification !== "harmless_nonzero" && issue.classification !== "exploratory_miss");
947
- for (const issue of repeatableIssues) {
948
- repeatedCommands.set(issue.command, (repeatedCommands.get(issue.command) ?? 0) + 1);
993
+ const repeatableIssues = analysis.commandIssues.filter((issue2) => issue2.classification !== "harmless_nonzero" && issue2.classification !== "exploratory_miss");
994
+ for (const issue2 of repeatableIssues) {
995
+ repeatedCommands.set(issue2.command, (repeatedCommands.get(issue2.command) ?? 0) + 1);
949
996
  }
950
997
  const repeatedFailures = [...repeatedCommands.entries()].filter(([, count2]) => count2 >= 3);
951
998
  if (repeatedFailures.length > 0) {
952
999
  analysis.repeatedFailureClusters = repeatedFailures.length;
953
- for (const issue of analysis.commandIssues) {
954
- if (repeatedCommands.get(issue.command) >= 3 && issue.classification !== "harmless_nonzero" && issue.classification !== "exploratory_miss") {
955
- issue.classification = "blocking_failure";
956
- issue.severity = "critical";
957
- issue.impact = "high";
1000
+ for (const issue2 of analysis.commandIssues) {
1001
+ if (repeatedCommands.get(issue2.command) >= 3 && issue2.classification !== "harmless_nonzero" && issue2.classification !== "exploratory_miss") {
1002
+ issue2.classification = "blocking_failure";
1003
+ issue2.severity = "critical";
1004
+ issue2.impact = "high";
958
1005
  }
959
1006
  }
960
1007
  }
961
1008
  if (totalTokens >= 1e6 && analysis.commandIssues.length >= 5) {
962
- for (const issue of analysis.commandIssues) {
963
- if (issue.severity === "warning") issue.impact = "high";
1009
+ for (const issue2 of analysis.commandIssues) {
1010
+ if (issue2.severity === "warning") issue2.impact = "high";
964
1011
  }
965
1012
  }
966
1013
  analysis.nonZeroCommandEvents = analysis.commandIssues.length;
967
- analysis.importantCommandFailures = analysis.commandIssues.filter((issue) => issue.severity === "critical" || issue.severity === "warning").length;
968
- analysis.harmlessNonZeroEvents = analysis.commandIssues.filter((issue) => issue.classification === "harmless_nonzero").length;
969
- analysis.exploratoryMisses = analysis.commandIssues.filter((issue) => issue.classification === "exploratory_miss").length;
1014
+ analysis.importantCommandFailures = analysis.commandIssues.filter((issue2) => issue2.severity === "critical" || issue2.severity === "warning").length;
1015
+ analysis.harmlessNonZeroEvents = analysis.commandIssues.filter((issue2) => issue2.classification === "harmless_nonzero").length;
1016
+ analysis.exploratoryMisses = analysis.commandIssues.filter((issue2) => issue2.classification === "exploratory_miss").length;
970
1017
  analysis.failedToolCallCount = analysis.importantCommandFailures;
971
1018
  analysis.commandIssueSeverity = strongestSeverity(analysis.commandIssues);
972
1019
  analysis.commandIssueImpact = strongestImpact(analysis.commandIssues);
@@ -974,21 +1021,21 @@ function finalizeCommandIssues(analysis, totalTokens) {
974
1021
  analysis.commandIssueSamples = sampledCommandIssues(analysis.commandIssues);
975
1022
  }
976
1023
  function sampledCommandIssues(issues) {
977
- return [...issues].sort((a, b) => issuePriority(b) - issuePriority(a)).slice(0, 12).map((issue) => ({
978
- ...issue,
979
- reason: commandIssueReason(issue)
1024
+ return [...issues].sort((a, b) => issuePriority(b) - issuePriority(a)).slice(0, 12).map((issue2) => ({
1025
+ ...issue2,
1026
+ reason: commandIssueReason(issue2)
980
1027
  }));
981
1028
  }
982
- function issuePriority(issue) {
983
- const severityScore = issue.severity === "critical" ? 4 : issue.severity === "warning" ? 3 : issue.severity === "info" ? 2 : issue.severity === "ignored" ? 1 : 0;
984
- const impactScore = issue.impact === "high" ? 3 : issue.impact === "medium" ? 2 : issue.impact === "low" ? 1 : 0;
1029
+ function issuePriority(issue2) {
1030
+ const severityScore = issue2.severity === "critical" ? 4 : issue2.severity === "warning" ? 3 : issue2.severity === "info" ? 2 : issue2.severity === "ignored" ? 1 : 0;
1031
+ const impactScore = issue2.impact === "high" ? 3 : issue2.impact === "medium" ? 2 : issue2.impact === "low" ? 1 : 0;
985
1032
  return severityScore * 10 + impactScore;
986
1033
  }
987
- function commandIssueReason(issue) {
988
- if (issue.severity === "critical") return "Repeated or blocking command issue that may have stopped progress.";
989
- if (issue.severity === "warning") return "Non-zero command event classified as important for this session.";
990
- if (issue.classification === "exploratory_miss") return "Likely exploratory command that found no result.";
991
- if (issue.classification === "harmless_nonzero") return "Likely harmless non-zero exit during normal shell exploration.";
1034
+ function commandIssueReason(issue2) {
1035
+ if (issue2.severity === "critical") return "Repeated or blocking command issue that may have stopped progress.";
1036
+ if (issue2.severity === "warning") return "Non-zero command event classified as important for this session.";
1037
+ if (issue2.classification === "exploratory_miss") return "Likely exploratory command that found no result.";
1038
+ if (issue2.classification === "harmless_nonzero") return "Likely harmless non-zero exit during normal shell exploration.";
992
1039
  return "Non-zero command event with limited local context.";
993
1040
  }
994
1041
  function classifyCommandIssue(record, previousIssues) {
@@ -997,7 +1044,7 @@ function classifyCommandIssue(record, previousIssues) {
997
1044
  const command2 = extractCommandText(record) ?? compactCommandText(raw);
998
1045
  const category = commandCategory(text);
999
1046
  const commandKey = normalizeCommand(command2);
1000
- const repeated = previousIssues.filter((issue) => issue.command === commandKey).length + 1 >= 3;
1047
+ const repeated = previousIssues.filter((issue2) => issue2.command === commandKey).length + 1 >= 3;
1001
1048
  if (isHarmlessNonZero(text)) {
1002
1049
  return { command: commandKey, category, classification: isExploratoryMiss(text) ? "exploratory_miss" : "harmless_nonzero", severity: "ignored", impact: "low" };
1003
1050
  }
@@ -1042,23 +1089,23 @@ function importantImpact(category) {
1042
1089
  return category === "install" || category === "build" || category === "test" || category === "typecheck" || category === "deploy" ? "high" : "medium";
1043
1090
  }
1044
1091
  function strongestSeverity(issues) {
1045
- if (issues.some((issue) => issue.severity === "critical")) return "critical";
1046
- if (issues.some((issue) => issue.severity === "warning")) return "warning";
1047
- if (issues.some((issue) => issue.severity === "info")) return "info";
1048
- if (issues.some((issue) => issue.severity === "ignored")) return "ignored";
1092
+ if (issues.some((issue2) => issue2.severity === "critical")) return "critical";
1093
+ if (issues.some((issue2) => issue2.severity === "warning")) return "warning";
1094
+ if (issues.some((issue2) => issue2.severity === "info")) return "info";
1095
+ if (issues.some((issue2) => issue2.severity === "ignored")) return "ignored";
1049
1096
  return "none";
1050
1097
  }
1051
1098
  function strongestImpact(issues) {
1052
- if (issues.some((issue) => issue.impact === "high")) return "high";
1053
- if (issues.some((issue) => issue.impact === "medium")) return "medium";
1054
- if (issues.some((issue) => issue.impact === "low")) return "low";
1099
+ if (issues.some((issue2) => issue2.impact === "high")) return "high";
1100
+ if (issues.some((issue2) => issue2.impact === "medium")) return "medium";
1101
+ if (issues.some((issue2) => issue2.impact === "low")) return "low";
1055
1102
  return "none";
1056
1103
  }
1057
1104
  function topFailureType(issues) {
1058
- const important = issues.filter((issue) => issue.severity === "critical" || issue.severity === "warning");
1105
+ const important = issues.filter((issue2) => issue2.severity === "critical" || issue2.severity === "warning");
1059
1106
  const source = important.length ? important : issues;
1060
1107
  const counts = /* @__PURE__ */ new Map();
1061
- for (const issue of source) counts.set(issue.category === "unknown" ? issue.classification : issue.category, (counts.get(issue.category === "unknown" ? issue.classification : issue.category) ?? 0) + 1);
1108
+ for (const issue2 of source) counts.set(issue2.category === "unknown" ? issue2.classification : issue2.category, (counts.get(issue2.category === "unknown" ? issue2.classification : issue2.category) ?? 0) + 1);
1062
1109
  return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0];
1063
1110
  }
1064
1111
  function extractCommandText(record) {
@@ -1256,6 +1303,9 @@ function scanCodex(options) {
1256
1303
  },
1257
1304
  sessions,
1258
1305
  stats: {
1306
+ sourceId: "codex",
1307
+ sourceLabel: "Codex",
1308
+ homePath: codexHome,
1259
1309
  codexHome,
1260
1310
  statePath,
1261
1311
  sessionsPath,
@@ -1303,7 +1353,7 @@ function threadToUsage(row, index, sessionFiles, options) {
1303
1353
  const cachedInputTokens = sessionBreakdown.cachedInputTokens;
1304
1354
  const outputTokens = sessionBreakdown.outputTokens;
1305
1355
  const reasoningTokens = sessionBreakdown.reasoningTokens;
1306
- const totalTokens = sessionBreakdown.totalTokens || dbTokens || inputTokens + cachedInputTokens + outputTokens + reasoningTokens;
1356
+ const totalTokens = sessionBreakdown.totalTokens || dbTokens || inputTokens + outputTokens + reasoningTokens;
1307
1357
  const hasPricedBreakdown = inputTokens + cachedInputTokens + outputTokens + reasoningTokens > 0;
1308
1358
  const warnings = [...repo.warnings, ...sessionBreakdown.warnings];
1309
1359
  const threadSource = stringValue2(row.thread_source) ?? sessionBreakdown.threadSource;
@@ -1676,12 +1726,12 @@ function stringValue2(value) {
1676
1726
  return typeof value === "string" && value.length > 0 ? value : void 0;
1677
1727
  }
1678
1728
  function codexSourceAppLabel(source, threadSource) {
1679
- if (threadSource === "subagent") return "Codex subagent";
1729
+ if (threadSource === "subagent") return "Codex app";
1680
1730
  if (!source) return "Codex";
1681
1731
  if (source === "vscode" || source === "codex_vscode" || source === "codex-vscode") return "VS Code";
1682
1732
  if (source === "cli" || source === "terminal" || source === "codex-tui") return "Terminal";
1683
1733
  if (source === "codex_app" || source === "codex-app") return "Codex app";
1684
- if (source.trim().startsWith("{")) return threadSource === "subagent" ? "Codex subagent" : "Codex";
1734
+ if (source.trim().startsWith("{")) return threadSource === "subagent" ? "Codex app" : "Codex";
1685
1735
  return source.replace(/^codex[_-]/, "").split(/[_-]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
1686
1736
  }
1687
1737
  function dateValue(value) {
@@ -1704,8 +1754,476 @@ function errorMessage(error) {
1704
1754
  return message;
1705
1755
  }
1706
1756
 
1707
- // apps/server/src/data.ts
1757
+ // packages/core/src/sources/claude.ts
1708
1758
  import fs5 from "node:fs";
1759
+ import os3 from "node:os";
1760
+ import path5 from "node:path";
1761
+ function scanClaude(options) {
1762
+ const claudeHome = options.claudeHome ?? defaultClaudeHome();
1763
+ const historyPath = path5.join(claudeHome, "history.jsonl");
1764
+ const warnings = [];
1765
+ const discovery = discoverClaudeSessionFiles(claudeHome, warnings, Boolean(options.claudeHome));
1766
+ const historyExists = fs5.existsSync(historyPath);
1767
+ const sessions = discovery.files.map((file, index) => claudeFileToUsage(file, index, options));
1768
+ const historyEntryCount = countHistoryEntries(historyPath);
1769
+ const parseFailureCount = sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).length;
1770
+ const unreadableFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_session_file:"))).length;
1771
+ const malformedFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("jsonl_line_"))).length;
1772
+ const projectsPath = path5.join(claudeHome, "projects");
1773
+ if (!discovery.projectsPaths.some((candidate) => fs5.existsSync(candidate))) {
1774
+ warnings.push(`Claude Code projects directory not found at ${projectsPath}`);
1775
+ }
1776
+ return {
1777
+ source: {
1778
+ id: "claude",
1779
+ label: "Claude Code",
1780
+ available: discovery.files.length > 0 || historyExists,
1781
+ paths: unique3([...discovery.projectsPaths, historyPath, ...discovery.desktopSessionRoots]),
1782
+ warnings
1783
+ },
1784
+ sessions,
1785
+ stats: {
1786
+ sourceId: "claude",
1787
+ sourceLabel: "Claude Code",
1788
+ homePath: claudeHome,
1789
+ claudeHome,
1790
+ projectsPath,
1791
+ historyPath,
1792
+ projectsExists: discovery.projectsPaths.some((candidate) => fs5.existsSync(candidate)),
1793
+ historyExists,
1794
+ sessionFileCount: discovery.files.length,
1795
+ projectDirCount: discovery.projectDirs.size,
1796
+ sessionsImported: sessions.length,
1797
+ parseFailureCount,
1798
+ unreadableFileCount,
1799
+ malformedFileCount,
1800
+ historyEntryCount,
1801
+ lastScannedAt: (/* @__PURE__ */ new Date()).toISOString()
1802
+ }
1803
+ };
1804
+ }
1805
+ function discoverClaudeSessionFiles(claudeHome, warnings, explicitHome) {
1806
+ const files = [];
1807
+ const projectDirs = /* @__PURE__ */ new Set();
1808
+ const projectsPaths = explicitHome ? [path5.join(claudeHome, "projects")] : unique3([...configuredClaudeHomes(claudeHome).map((root) => path5.join(root, "projects")), path5.join(os3.homedir(), ".config", "claude", "projects")]);
1809
+ for (const projectsPath of projectsPaths) {
1810
+ collectProjectFiles(projectsPath, files, projectDirs, warnings);
1811
+ }
1812
+ const desktopSessionRoots = explicitHome ? [] : defaultDesktopSessionRoots();
1813
+ for (const root of desktopSessionRoots) {
1814
+ collectDesktopSessionFiles(root, files, projectDirs, warnings);
1815
+ }
1816
+ return {
1817
+ files: uniqueSessionFiles(files),
1818
+ projectDirs,
1819
+ projectsPaths,
1820
+ desktopSessionRoots
1821
+ };
1822
+ }
1823
+ function configuredClaudeHomes(primaryHome) {
1824
+ if (process.env.CLAUDE_CONFIG_DIR) {
1825
+ return process.env.CLAUDE_CONFIG_DIR.split(",").map((item) => item.trim()).filter(Boolean).map((item) => path5.resolve(item));
1826
+ }
1827
+ return [primaryHome];
1828
+ }
1829
+ function collectProjectFiles(projectsPath, files, projectDirs, warnings) {
1830
+ if (!fs5.existsSync(projectsPath)) return;
1831
+ try {
1832
+ for (const entry of fs5.readdirSync(projectsPath, { withFileTypes: true })) {
1833
+ if (!entry.isDirectory()) continue;
1834
+ const projectDir = path5.join(projectsPath, entry.name);
1835
+ projectDirs.add(projectDir);
1836
+ walk2(projectDir, 5, (filePath) => {
1837
+ if (filePath.endsWith(".jsonl")) files.push({ filePath, projectDir, projectsPath });
1838
+ });
1839
+ }
1840
+ } catch (error) {
1841
+ warnings.push(`Unable to read Claude Code projects at ${projectsPath}: ${errorMessage2(error)}`);
1842
+ }
1843
+ }
1844
+ function collectDesktopSessionFiles(root, files, projectDirs, warnings) {
1845
+ if (!fs5.existsSync(root)) return;
1846
+ try {
1847
+ walk2(root, 8, (filePath) => {
1848
+ if (!filePath.endsWith(".jsonl")) return;
1849
+ const parts = filePath.split(path5.sep);
1850
+ const projectsIndex = parts.lastIndexOf("projects");
1851
+ const hasProjectSegment = projectsIndex >= 0 && projectsIndex < parts.length - 2;
1852
+ const projectsPath = hasProjectSegment ? parts.slice(0, projectsIndex + 1).join(path5.sep) || path5.sep : root;
1853
+ const projectDir = hasProjectSegment ? parts.slice(0, projectsIndex + 2).join(path5.sep) || path5.sep : path5.dirname(filePath);
1854
+ projectDirs.add(projectDir);
1855
+ files.push({ filePath, projectDir, projectsPath });
1856
+ });
1857
+ } catch (error) {
1858
+ warnings.push(`Unable to read Claude Code desktop sessions at ${root}: ${errorMessage2(error)}`);
1859
+ }
1860
+ }
1861
+ function walk2(root, maxDepth, onFile, depth = 0) {
1862
+ if (depth > maxDepth) return;
1863
+ for (const entry of fs5.readdirSync(root, { withFileTypes: true })) {
1864
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
1865
+ const fullPath = path5.join(root, entry.name);
1866
+ if (entry.isDirectory()) {
1867
+ walk2(fullPath, maxDepth, onFile, depth + 1);
1868
+ } else if (entry.isFile()) {
1869
+ onFile(fullPath);
1870
+ }
1871
+ }
1872
+ }
1873
+ function claudeFileToUsage(file, index, options) {
1874
+ const breakdown = readClaudeBreakdown(file.filePath);
1875
+ const id = breakdown.id ?? (path5.basename(file.filePath).replace(/\.jsonl$/i, "") || `claude-session-${index}`);
1876
+ const fallbackCwd = inferProjectPath(file.projectDir, file.projectsPath);
1877
+ const cwd = breakdown.cwd ?? fallbackCwd;
1878
+ const repo = resolveRepoInfo(cwd, options.config);
1879
+ const warnings = [...repo.warnings, ...breakdown.warnings];
1880
+ if (!breakdown.cwd) warnings.push("repo_inferred_from_claude_project_dir");
1881
+ const hasTokenBreakdown = breakdown.totalTokens > 0 && breakdown.usageCount > 0;
1882
+ const usage = {
1883
+ id,
1884
+ sourceClient: "claude",
1885
+ sourceApp: claudeSourceAppLabel(breakdown.entrypoint),
1886
+ sourceAppRaw: breakdown.entrypoint,
1887
+ sourcePath: file.filePath,
1888
+ repoRoot: repo.repoRoot,
1889
+ repoName: repo.repoName,
1890
+ cwd,
1891
+ gitRemoteUrl: repo.gitRemoteUrl,
1892
+ gitBranch: repo.gitBranch ?? breakdown.gitBranch,
1893
+ title: breakdown.title,
1894
+ startedAt: breakdown.startedAt,
1895
+ endedAt: breakdown.endedAt,
1896
+ model: breakdown.model,
1897
+ provider: "anthropic",
1898
+ inputTokens: breakdown.inputTokens,
1899
+ cachedInputTokens: breakdown.cachedInputTokens,
1900
+ cacheCreationInputTokens: breakdown.cacheCreationInputTokens,
1901
+ outputTokens: breakdown.outputTokens,
1902
+ reasoningTokens: 0,
1903
+ reasoningOutputTokens: 0,
1904
+ totalTokens: breakdown.totalTokens,
1905
+ tokenAggregationMethod: hasTokenBreakdown ? "direct_usage" : "unknown",
1906
+ tokenConfidence: hasTokenBreakdown ? "high" : "low",
1907
+ tokenSnapshotCount: breakdown.usageCount,
1908
+ estimatedCostUsd: void 0,
1909
+ messageCount: breakdown.messageCount,
1910
+ rawTokenTotal: hasTokenBreakdown ? breakdown.totalTokens : void 0,
1911
+ warnings: hasTokenBreakdown ? warnings : [...warnings, "missing_token_breakdown"],
1912
+ durationMs: durationMs2(breakdown.startedAt, breakdown.endedAt),
1913
+ rawEventCount: breakdown.rawEventCount,
1914
+ parseStatus: breakdown.parseStatus,
1915
+ parseErrors: breakdown.parseErrors,
1916
+ detectedSurface: claudeSurface(breakdown.entrypoint),
1917
+ surfaceConfidence: "medium",
1918
+ surfaceReason: breakdown.entrypoint ? `Claude Code entrypoint: ${breakdown.entrypoint}` : "Claude Code transcript",
1919
+ userPromptCount: breakdown.userPromptCount,
1920
+ assistantMessageCount: breakdown.assistantMessageCount,
1921
+ toolCallCount: breakdown.toolCallCount,
1922
+ shellCommandCount: breakdown.shellCommandCount,
1923
+ failedToolCallCount: breakdown.failedToolCallCount,
1924
+ nonZeroCommandEvents: 0,
1925
+ importantCommandFailures: 0,
1926
+ harmlessNonZeroEvents: 0,
1927
+ exploratoryMisses: 0,
1928
+ repeatedFailureClusters: 0,
1929
+ commandIssueSeverity: breakdown.failedToolCallCount > 0 ? "warning" : "none",
1930
+ commandIssueImpact: breakdown.failedToolCallCount > 0 ? "medium" : "none",
1931
+ topFailureType: breakdown.failedToolCallCount > 0 ? "unknown" : void 0,
1932
+ commandIssueSamples: [],
1933
+ fileReadCount: breakdown.fileReadCount,
1934
+ fileEditCount: breakdown.fileEditCount,
1935
+ promptTimeline: breakdown.promptTimeline,
1936
+ sessionOutcome: inferOutcome2(breakdown)
1937
+ };
1938
+ const cost = hasTokenBreakdown ? calculateCostUsd(usage, options.pricing) : void 0;
1939
+ return {
1940
+ ...usage,
1941
+ estimatedCostUsd: cost,
1942
+ warnings: cost === void 0 ? [...usage.warnings, "unknown_pricing"] : usage.warnings
1943
+ };
1944
+ }
1945
+ function readClaudeBreakdown(filePath) {
1946
+ const breakdown = emptyBreakdown2();
1947
+ try {
1948
+ const text = fs5.readFileSync(filePath, "utf8");
1949
+ const parse = parseJsonLines2(text);
1950
+ breakdown.rawEventCount = parse.records.length;
1951
+ breakdown.parseErrors = parse.errors;
1952
+ breakdown.parseStatus = parse.errors.length ? parse.records.length ? "partial" : "failed" : "ok";
1953
+ for (const record of parse.records) {
1954
+ applyClaudeRecord(breakdown, record);
1955
+ }
1956
+ breakdown.totalTokens = breakdown.inputTokens + breakdown.outputTokens;
1957
+ } catch (error) {
1958
+ const message = `unable_to_read_session_file:${errorMessage2(error)}`;
1959
+ breakdown.parseStatus = "failed";
1960
+ breakdown.parseErrors.push(message);
1961
+ breakdown.warnings.push(message);
1962
+ }
1963
+ return breakdown;
1964
+ }
1965
+ function applyClaudeRecord(breakdown, record) {
1966
+ const timestamp3 = dateValue2(record.timestamp);
1967
+ if (timestamp3) {
1968
+ breakdown.startedAt = minDate(breakdown.startedAt, timestamp3);
1969
+ breakdown.endedAt = maxDate(breakdown.endedAt, timestamp3);
1970
+ }
1971
+ breakdown.id ??= stringValue3(record.sessionId);
1972
+ breakdown.cwd ??= stringValue3(record.cwd);
1973
+ breakdown.gitBranch ??= stringValue3(record.gitBranch);
1974
+ breakdown.entrypoint ??= stringValue3(record.entrypoint);
1975
+ breakdown.version ??= stringValue3(record.version);
1976
+ breakdown.title ??= stringValue3(record.aiTitle);
1977
+ const message = firstObject4(record.message);
1978
+ const role = stringValue3(message?.role) ?? stringValue3(record.type);
1979
+ if (role === "user") breakdown.userPromptCount += 1;
1980
+ if (role === "assistant") breakdown.assistantMessageCount += 1;
1981
+ if (role === "user" || role === "assistant") breakdown.messageCount += 1;
1982
+ if (message) {
1983
+ applyPromptTimeline2(breakdown, role, message.content, timestamp3);
1984
+ const usage = firstObject4(message.usage);
1985
+ const usageTokens = usage ? usageTotal(usage) : 0;
1986
+ const model = normalizeClaudeModel(message.model);
1987
+ if (model && (usageTokens > 0 || !breakdown.model)) breakdown.model = model;
1988
+ if (usage && usageTokens > 0) applyUsage(breakdown, usage);
1989
+ applyContentSignals(breakdown, message.content);
1990
+ }
1991
+ if (looksFailedToolResult(record.toolUseResult)) breakdown.failedToolCallCount += 1;
1992
+ }
1993
+ function applyUsage(breakdown, usage) {
1994
+ const input = numberValue(usage.input_tokens);
1995
+ const cacheRead = numberValue(usage.cache_read_input_tokens);
1996
+ const cacheCreation = numberValue(usage.cache_creation_input_tokens);
1997
+ const output = numberValue(usage.output_tokens);
1998
+ breakdown.inputTokens += input + cacheRead + cacheCreation;
1999
+ breakdown.cachedInputTokens += cacheRead;
2000
+ breakdown.cacheCreationInputTokens += cacheCreation;
2001
+ breakdown.outputTokens += output;
2002
+ breakdown.usageCount += 1;
2003
+ }
2004
+ function usageTotal(usage) {
2005
+ return numberValue(usage.input_tokens) + numberValue(usage.cache_read_input_tokens) + numberValue(usage.cache_creation_input_tokens) + numberValue(usage.output_tokens);
2006
+ }
2007
+ function applyContentSignals(breakdown, content) {
2008
+ if (!Array.isArray(content)) return;
2009
+ for (const item of content) {
2010
+ const object = firstObject4(item);
2011
+ if (!object || object.type !== "tool_use") continue;
2012
+ breakdown.toolCallCount += 1;
2013
+ const name = stringValue3(object.name)?.toLowerCase() ?? "";
2014
+ if (name === "bash") breakdown.shellCommandCount += 1;
2015
+ if (name === "read") breakdown.fileReadCount += 1;
2016
+ if (["edit", "multiedit", "write", "notebookedit"].includes(name)) breakdown.fileEditCount += 1;
2017
+ }
2018
+ }
2019
+ function parseJsonLines2(text) {
2020
+ const records = [];
2021
+ const errors = [];
2022
+ text.split(/\r?\n/).map((line, index) => ({ line: line.trim(), index })).filter(({ line }) => Boolean(line)).forEach(({ line, index }) => {
2023
+ try {
2024
+ const parsed = JSON.parse(line);
2025
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2026
+ records.push(parsed);
2027
+ }
2028
+ } catch (error) {
2029
+ errors.push(`jsonl_line_${index + 1}:${errorMessage2(error)}`);
2030
+ }
2031
+ });
2032
+ return { records, errors };
2033
+ }
2034
+ function emptyBreakdown2() {
2035
+ return {
2036
+ id: void 0,
2037
+ title: void 0,
2038
+ cwd: void 0,
2039
+ gitBranch: void 0,
2040
+ entrypoint: void 0,
2041
+ version: void 0,
2042
+ startedAt: void 0,
2043
+ endedAt: void 0,
2044
+ model: void 0,
2045
+ inputTokens: 0,
2046
+ cachedInputTokens: 0,
2047
+ cacheCreationInputTokens: 0,
2048
+ outputTokens: 0,
2049
+ totalTokens: 0,
2050
+ usageCount: 0,
2051
+ messageCount: 0,
2052
+ userPromptCount: 0,
2053
+ assistantMessageCount: 0,
2054
+ toolCallCount: 0,
2055
+ shellCommandCount: 0,
2056
+ failedToolCallCount: 0,
2057
+ fileReadCount: 0,
2058
+ fileEditCount: 0,
2059
+ rawEventCount: 0,
2060
+ parseStatus: "ok",
2061
+ parseErrors: [],
2062
+ promptTimeline: [],
2063
+ warnings: []
2064
+ };
2065
+ }
2066
+ function applyPromptTimeline2(breakdown, role, content, timestamp3) {
2067
+ if (breakdown.promptTimeline.length >= 80 || role !== "user" && role !== "assistant") return;
2068
+ const text = extractClaudeMessageText(content);
2069
+ if (!text) return;
2070
+ breakdown.promptTimeline.push({
2071
+ role,
2072
+ text: normalizeTimelineText2(text),
2073
+ timestamp: timestamp3
2074
+ });
2075
+ }
2076
+ function extractClaudeMessageText(content) {
2077
+ if (typeof content === "string" && content.trim()) return content;
2078
+ if (!Array.isArray(content)) return void 0;
2079
+ const parts = content.flatMap((item) => {
2080
+ if (typeof item === "string") return [item];
2081
+ const object = firstObject4(item);
2082
+ if (!object || object.type !== "text") return [];
2083
+ const text = stringValue3(object.text);
2084
+ return text ? [text] : [];
2085
+ }).filter((item) => item.trim());
2086
+ return parts.length ? parts.join("\n") : void 0;
2087
+ }
2088
+ function normalizeTimelineText2(text) {
2089
+ return text.replace(/\s+/g, " ").trim().slice(0, 1e3);
2090
+ }
2091
+ function inferProjectPath(projectDir, projectsPath) {
2092
+ const name = path5.basename(projectDir);
2093
+ if (name.startsWith("-")) {
2094
+ return `${path5.sep}${name.slice(1).split("-").filter(Boolean).join(path5.sep)}`;
2095
+ }
2096
+ const relative = path5.relative(projectsPath, projectDir);
2097
+ return relative && !relative.startsWith("..") ? path5.join(projectsPath, relative) : projectDir;
2098
+ }
2099
+ function inferOutcome2(breakdown) {
2100
+ if (breakdown.parseStatus === "failed") return "failed";
2101
+ if (breakdown.failedToolCallCount > 0) return "partial";
2102
+ if (breakdown.fileEditCount > 0) return "completed";
2103
+ if (breakdown.shellCommandCount > 0) return "setup_debugging";
2104
+ if (breakdown.userPromptCount > 0 || breakdown.assistantMessageCount > 0) return "research_only";
2105
+ return "unknown";
2106
+ }
2107
+ function claudeSurface(entrypoint) {
2108
+ const value = entrypoint?.toLowerCase() ?? "";
2109
+ if (value.includes("vscode")) return "vscode_extension";
2110
+ if (value.includes("local-agent")) return "local_agent";
2111
+ return "terminal_cli";
2112
+ }
2113
+ function claudeSourceAppLabel(entrypoint) {
2114
+ const value = entrypoint?.toLowerCase() ?? "";
2115
+ if (value.includes("vscode")) return "VS Code";
2116
+ if (value.includes("local-agent")) return "Claude desktop local agent";
2117
+ if (value === "cli" || value.includes("terminal")) return "Terminal";
2118
+ return "Claude Code";
2119
+ }
2120
+ function defaultClaudeHome() {
2121
+ return process.env.CLAUDE_CONFIG_DIR?.split(",").map((item) => item.trim()).filter(Boolean)[0] ?? path5.join(os3.homedir(), ".claude");
2122
+ }
2123
+ function defaultDesktopSessionRoots() {
2124
+ const home = os3.homedir();
2125
+ const xdg = process.env.XDG_CONFIG_HOME || path5.join(home, ".config");
2126
+ return [
2127
+ path5.join(home, "Library", "Application Support", "Claude", "local-agent-mode-sessions"),
2128
+ path5.join(xdg, "Claude", "local-agent-mode-sessions")
2129
+ ];
2130
+ }
2131
+ function uniqueSessionFiles(files) {
2132
+ const seen = /* @__PURE__ */ new Set();
2133
+ return files.filter((file) => {
2134
+ const key = path5.resolve(file.filePath);
2135
+ if (seen.has(key)) return false;
2136
+ seen.add(key);
2137
+ return true;
2138
+ });
2139
+ }
2140
+ function unique3(values) {
2141
+ return [...new Set(values)];
2142
+ }
2143
+ function countHistoryEntries(historyPath) {
2144
+ if (!fs5.existsSync(historyPath)) return 0;
2145
+ let entryCount = 0;
2146
+ try {
2147
+ for (const line of fs5.readFileSync(historyPath, "utf8").split(/\r?\n/).filter(Boolean)) {
2148
+ try {
2149
+ JSON.parse(line);
2150
+ entryCount += 1;
2151
+ } catch {
2152
+ continue;
2153
+ }
2154
+ }
2155
+ } catch {
2156
+ return entryCount;
2157
+ }
2158
+ return entryCount;
2159
+ }
2160
+ function firstObject4(value) {
2161
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2162
+ }
2163
+ function stringValue3(value) {
2164
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2165
+ }
2166
+ function numberValue(value) {
2167
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2168
+ if (typeof value === "string" && Number.isFinite(Number(value))) return Number(value);
2169
+ return 0;
2170
+ }
2171
+ function normalizeClaudeModel(value) {
2172
+ const model = stringValue3(value);
2173
+ if (!model || model === "<synthetic>") return void 0;
2174
+ return model;
2175
+ }
2176
+ function dateValue2(value) {
2177
+ if (typeof value !== "string") return void 0;
2178
+ const date = new Date(value);
2179
+ return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
2180
+ }
2181
+ function minDate(current, candidate) {
2182
+ return !current || candidate < current ? candidate : current;
2183
+ }
2184
+ function maxDate(current, candidate) {
2185
+ return !current || candidate > current ? candidate : current;
2186
+ }
2187
+ function durationMs2(startedAt, endedAt) {
2188
+ if (!startedAt || !endedAt) return void 0;
2189
+ const start = new Date(startedAt).getTime();
2190
+ const end = new Date(endedAt).getTime();
2191
+ return Number.isFinite(start) && Number.isFinite(end) && end >= start ? end - start : void 0;
2192
+ }
2193
+ function looksFailedToolResult(value) {
2194
+ const object = firstObject4(value);
2195
+ if (!object) return false;
2196
+ if (object.is_error === true || object.error === true) return true;
2197
+ const exitCode = numberValue(object.exit_code ?? object.exitCode ?? object.status_code ?? object.statusCode);
2198
+ if (exitCode !== 0) return true;
2199
+ const status = stringValue3(object.status)?.toLowerCase();
2200
+ return status === "failed" || status === "error";
2201
+ }
2202
+ function errorMessage2(error) {
2203
+ return error instanceof Error ? error.message : String(error);
2204
+ }
2205
+
2206
+ // packages/core/src/sources/index.ts
2207
+ function scanUsageSources(options) {
2208
+ const codex = scanCodex({
2209
+ ...options.codexHome ? { codexHome: options.codexHome } : {},
2210
+ ...options.config ? { config: options.config } : {},
2211
+ pricing: options.pricing
2212
+ });
2213
+ const claude = scanClaude({
2214
+ ...options.claudeHome ? { claudeHome: options.claudeHome } : {},
2215
+ ...options.config ? { config: options.config } : {},
2216
+ pricing: options.pricing
2217
+ });
2218
+ return {
2219
+ sources: [codex.source, claude.source],
2220
+ sourceStats: [codex.stats, claude.stats],
2221
+ sessions: [...codex.sessions, ...claude.sessions]
2222
+ };
2223
+ }
2224
+
2225
+ // apps/server/src/data.ts
2226
+ import fs6 from "node:fs";
1709
2227
  var scanCacheTtlMs = 1e4;
1710
2228
  var scanCache;
1711
2229
  function readDashboardData(filters = {}) {
@@ -1722,11 +2240,11 @@ function readRawUsageData() {
1722
2240
  }
1723
2241
  const { config, warnings: configWarnings } = loadConfigWithWarnings();
1724
2242
  const pricing = loadPricingTable(resolvePricingPath(config));
1725
- const codex = scanCodex({ config, pricing });
2243
+ const scan = scanUsageSources({ config, pricing });
1726
2244
  const data = {
1727
- sources: [{ ...codex.source, warnings: [...codex.source.warnings, ...configWarnings] }],
1728
- sourceStats: [codex.stats],
1729
- sessions: codex.sessions
2245
+ sources: scan.sources.map((source) => ({ ...source, warnings: [...source.warnings, ...configWarnings] })),
2246
+ sourceStats: scan.sourceStats,
2247
+ sessions: scan.sessions
1730
2248
  };
1731
2249
  scanCache = { cwd, data, expiresAt: Date.now() + scanCacheTtlMs };
1732
2250
  return data;
@@ -1767,8 +2285,8 @@ function writePricingData(models) {
1767
2285
  }
1768
2286
  function clearRepoSpendLocalData() {
1769
2287
  const target = repospendHome();
1770
- const removed = fs5.existsSync(target);
1771
- fs5.rmSync(target, { recursive: true, force: true });
2288
+ const removed = fs6.existsSync(target);
2289
+ fs6.rmSync(target, { recursive: true, force: true });
1772
2290
  clearScanCache();
1773
2291
  return { path: target, removed };
1774
2292
  }
@@ -1791,12 +2309,621 @@ function runtimeCwd() {
1791
2309
  }
1792
2310
 
1793
2311
  // apps/server/src/server.ts
1794
- import fs6 from "node:fs";
1795
- import path5 from "node:path";
2312
+ import fs7 from "node:fs";
2313
+ import path6 from "node:path";
1796
2314
  import { fileURLToPath } from "node:url";
1797
2315
  import fastifyStatic from "@fastify/static";
1798
2316
  import Fastify from "fastify";
1799
2317
 
2318
+ // apps/server/src/demo-data.ts
2319
+ var baseDate = "2026-05-18";
2320
+ var demoPricing = {
2321
+ ...defaultPricing,
2322
+ "gpt-5.3-codex": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
2323
+ "gpt-5.3-codex-spark": { inputPerMillion: 0.75, cachedInputPerMillion: 0.075, outputPerMillion: 4.5, reasoningOutputPerMillion: 4.5 },
2324
+ "claude-haiku-4-5-20251001": { inputPerMillion: 1, cacheCreationInputPerMillion: 1.25, cachedInputPerMillion: 0.1, outputPerMillion: 5 }
2325
+ };
2326
+ function demoModeEnabled() {
2327
+ return process.env.REPOSPEND_DEMO_DATA === "lotr";
2328
+ }
2329
+ function readDemoDashboardData(filters = {}) {
2330
+ return buildDashboardSnapshot({
2331
+ sources: demoSources,
2332
+ sourceStats: demoSourceStats,
2333
+ sessions: demoSessions,
2334
+ filters
2335
+ });
2336
+ }
2337
+ function readDemoRtkGain() {
2338
+ return {
2339
+ available: true,
2340
+ rtkDetected: true,
2341
+ rtkVersion: "rtk-demo 1.0.0",
2342
+ rtkCodexHookStatus: "active",
2343
+ rtkLastActivityAt: "2026-05-18T18:42:00.000Z",
2344
+ rtkDataSource: "fictional demo data",
2345
+ totalCommands: 248,
2346
+ inputTokens: "18.4M",
2347
+ outputTokens: "2.6M",
2348
+ tokensSaved: "5.8M",
2349
+ savedPercent: 23.9,
2350
+ totalExecTime: "41m",
2351
+ averageExecTime: "9.9s",
2352
+ rtkTokensSaved: "5.8M",
2353
+ rtkSavingsRate: 23.9,
2354
+ rtkCommandsProcessed: 248,
2355
+ rtkAverageCommandRuntime: "9.9s",
2356
+ rtkTopSavingsCommands: [
2357
+ { command: "rg", count: 74, saved: "1.9M", averageSavedPercent: 31.2, time: "8m" },
2358
+ { command: "pnpm test", count: 18, saved: "840K", averageSavedPercent: 17.4, time: "12m" },
2359
+ { command: "git diff", count: 39, saved: "620K", averageSavedPercent: 26.8, time: "2m" }
2360
+ ],
2361
+ rtkRecentActivity: [
2362
+ "rg council-of-elrond planner",
2363
+ "pnpm test --filter gondor-api",
2364
+ "git diff -- shire-mobile"
2365
+ ],
2366
+ rtkDiscoverAvailable: true,
2367
+ rtkDiscoverError: void 0,
2368
+ rtkDiscoverSummary: "Total: 248 fictional command events scanned.",
2369
+ rtkCoverageGaps: [
2370
+ { command: "cat", count: 21, rtkEquivalent: "rtk sed", status: "easy win", estimatedSavings: "410K" },
2371
+ { command: "grep", count: 13, rtkEquivalent: "rtk rg", status: "easy win", estimatedSavings: "360K" }
2372
+ ],
2373
+ rtkUnhandledCommands: [
2374
+ { command: "palantirctl inspect", count: 5, example: "palantirctl inspect eye-of-sauron-alerts" }
2375
+ ],
2376
+ topCommands: [
2377
+ { command: "rg", count: 74, saved: "1.9M", averageSavedPercent: 31.2, time: "8m" },
2378
+ { command: "git", count: 46, saved: "720K", averageSavedPercent: 18.3, time: "3m" },
2379
+ { command: "pnpm", count: 31, saved: "1.1M", averageSavedPercent: 16.8, time: "17m" }
2380
+ ],
2381
+ recentCommands: [
2382
+ "rtk rg auth gates-of-moria",
2383
+ "rtk pnpm test --filter palantir-observability",
2384
+ "rtk git diff -- one-ring-infra"
2385
+ ],
2386
+ raw: "RepoSpend LOTR demo RTK output",
2387
+ error: void 0
2388
+ };
2389
+ }
2390
+ var demoSources = [
2391
+ {
2392
+ id: "codex",
2393
+ label: "Codex",
2394
+ available: true,
2395
+ paths: ["/demo/middle-earth/codex/state.sqlite", "/demo/middle-earth/codex/sessions"],
2396
+ warnings: []
2397
+ },
2398
+ {
2399
+ id: "claude",
2400
+ label: "Claude Code",
2401
+ available: true,
2402
+ paths: ["/demo/middle-earth/claude/projects"],
2403
+ warnings: []
2404
+ }
2405
+ ];
2406
+ var demoSourceStats = [
2407
+ {
2408
+ sourceId: "codex",
2409
+ sourceLabel: "Codex",
2410
+ homePath: "/demo/middle-earth/codex",
2411
+ statePath: "/demo/middle-earth/codex/state.sqlite",
2412
+ sessionsPath: "/demo/middle-earth/codex/sessions",
2413
+ stateExists: true,
2414
+ sessionsExists: true,
2415
+ sessionFileCount: 15,
2416
+ sessionsImported: 15,
2417
+ parseFailureCount: 0,
2418
+ unreadableFileCount: 0,
2419
+ malformedFileCount: 0,
2420
+ lastScannedAt: "2026-05-18T19:00:00.000Z"
2421
+ },
2422
+ {
2423
+ sourceId: "claude",
2424
+ sourceLabel: "Claude Code",
2425
+ homePath: "/demo/middle-earth/claude",
2426
+ projectsPath: "/demo/middle-earth/claude/projects",
2427
+ projectsExists: true,
2428
+ sessionFileCount: 7,
2429
+ sessionsImported: 7,
2430
+ parseFailureCount: 0,
2431
+ unreadableFileCount: 0,
2432
+ malformedFileCount: 0,
2433
+ projectDirCount: 4,
2434
+ lastScannedAt: "2026-05-18T19:01:00.000Z"
2435
+ }
2436
+ ];
2437
+ var demoInputs = [
2438
+ {
2439
+ id: "lotr-session-001",
2440
+ repo: "one-ring-infra",
2441
+ title: "Why is one-ring-infra spending 3x more than shire-mobile?",
2442
+ sourceClient: "codex",
2443
+ sourceApp: "Codex app",
2444
+ model: "GPT-5.5",
2445
+ provider: "openai",
2446
+ hour: 9,
2447
+ minute: 10,
2448
+ durationMinutes: 84,
2449
+ inputTokens: 285e4,
2450
+ cachedInputTokens: 32e4,
2451
+ outputTokens: 41e4,
2452
+ reasoningTokens: 26e4,
2453
+ prompts: 9,
2454
+ replies: 18,
2455
+ tools: 86,
2456
+ commands: 34,
2457
+ edits: 3,
2458
+ reads: 92,
2459
+ outcome: "completed",
2460
+ branch: "audit/token-fire",
2461
+ importantFailures: 2,
2462
+ harmlessNonZero: 4,
2463
+ repeatedClusters: 1,
2464
+ topFailureType: "test",
2465
+ warnings: ["repeated_error:test-suite"],
2466
+ issueSamples: [
2467
+ issue("pnpm test --filter one-ring-infra", "test", "warning", "medium", "Repeated failing shard while auditing token spend."),
2468
+ issue("terraform plan -var region=mordor", "deploy", "critical", "high", "Infrastructure plan failed before the safer dry-run path was found.")
2469
+ ]
2470
+ },
2471
+ {
2472
+ id: "lotr-session-002",
2473
+ repo: "mordor-ci",
2474
+ title: "Reduce token burn in mordor-ci",
2475
+ sourceClient: "codex",
2476
+ sourceApp: "Codex app",
2477
+ model: "GPT-5.4",
2478
+ provider: "openai",
2479
+ hour: 10,
2480
+ minute: 35,
2481
+ durationMinutes: 62,
2482
+ inputTokens: 178e4,
2483
+ cachedInputTokens: 95e3,
2484
+ outputTokens: 28e4,
2485
+ reasoningTokens: 185e3,
2486
+ prompts: 7,
2487
+ replies: 12,
2488
+ tools: 58,
2489
+ commands: 28,
2490
+ edits: 2,
2491
+ reads: 51,
2492
+ outcome: "partial",
2493
+ branch: "ci/doom-cache",
2494
+ importantFailures: 3,
2495
+ harmlessNonZero: 1,
2496
+ repeatedClusters: 2,
2497
+ topFailureType: "build",
2498
+ warnings: ["low_cache_rate"],
2499
+ issueSamples: [
2500
+ issue("pnpm build --filter mordor-ci", "build", "critical", "high", "Build failed repeatedly after the Mount Doom runner image changed.")
2501
+ ]
2502
+ },
2503
+ {
2504
+ id: "lotr-session-003",
2505
+ repo: "shire-mobile",
2506
+ title: "Investigate token spike after second breakfast",
2507
+ sourceClient: "claude",
2508
+ sourceApp: "Claude Code",
2509
+ sourceAppRaw: "claude-code",
2510
+ model: "claude-opus-4-7",
2511
+ provider: "anthropic",
2512
+ hour: 11,
2513
+ minute: 5,
2514
+ durationMinutes: 46,
2515
+ inputTokens: 142e4,
2516
+ cachedInputTokens: 21e4,
2517
+ outputTokens: 185e3,
2518
+ reasoningTokens: 0,
2519
+ prompts: 5,
2520
+ replies: 9,
2521
+ tools: 31,
2522
+ commands: 12,
2523
+ edits: 0,
2524
+ reads: 44,
2525
+ outcome: "research_only",
2526
+ branch: "analytics/second-breakfast",
2527
+ warnings: [],
2528
+ detectedSurface: "terminal_cli",
2529
+ tokenConfidence: "high"
2530
+ },
2531
+ {
2532
+ id: "lotr-session-004",
2533
+ repo: "palantir-observability",
2534
+ title: "Fix race condition in the palantir event stream",
2535
+ sourceClient: "codex",
2536
+ sourceApp: "VS Code",
2537
+ sourceAppRaw: "vscode",
2538
+ model: "GPT-5.4",
2539
+ provider: "openai",
2540
+ hour: 12,
2541
+ minute: 20,
2542
+ durationMinutes: 73,
2543
+ inputTokens: 126e4,
2544
+ cachedInputTokens: 455e3,
2545
+ outputTokens: 21e4,
2546
+ reasoningTokens: 17e4,
2547
+ prompts: 6,
2548
+ replies: 14,
2549
+ tools: 64,
2550
+ commands: 24,
2551
+ edits: 6,
2552
+ reads: 66,
2553
+ outcome: "completed",
2554
+ branch: "stream/palantir-race",
2555
+ importantFailures: 1,
2556
+ harmlessNonZero: 3,
2557
+ topFailureType: "test",
2558
+ issueSamples: [
2559
+ issue("pnpm test palantir-stream.spec.ts", "test", "warning", "medium", "One flaky stream-order assertion needed a deterministic clock.")
2560
+ ]
2561
+ },
2562
+ {
2563
+ id: "lotr-session-005",
2564
+ repo: "gondor-api",
2565
+ title: "Add auth flow for gates-of-moria",
2566
+ sourceClient: "codex",
2567
+ sourceApp: "Codex app",
2568
+ model: "GPT-5.4-Mini",
2569
+ provider: "openai",
2570
+ hour: 13,
2571
+ minute: 0,
2572
+ durationMinutes: 58,
2573
+ inputTokens: 86e4,
2574
+ cachedInputTokens: 28e4,
2575
+ outputTokens: 13e4,
2576
+ reasoningTokens: 74e3,
2577
+ prompts: 5,
2578
+ replies: 10,
2579
+ tools: 42,
2580
+ commands: 18,
2581
+ edits: 8,
2582
+ reads: 39,
2583
+ outcome: "completed",
2584
+ branch: "auth/mellon"
2585
+ },
2586
+ {
2587
+ id: "lotr-session-006",
2588
+ repo: "rivendell-dashboard",
2589
+ title: "Refactor the council-of-elrond planner",
2590
+ sourceClient: "claude",
2591
+ sourceApp: "Claude desktop local agent",
2592
+ sourceAppRaw: "claude-desktop-local-agent",
2593
+ model: "claude-sonnet-4-6",
2594
+ provider: "anthropic",
2595
+ hour: 14,
2596
+ minute: 15,
2597
+ durationMinutes: 67,
2598
+ inputTokens: 92e4,
2599
+ cachedInputTokens: 36e4,
2600
+ outputTokens: 15e4,
2601
+ reasoningTokens: 0,
2602
+ prompts: 6,
2603
+ replies: 11,
2604
+ tools: 37,
2605
+ commands: 15,
2606
+ edits: 7,
2607
+ reads: 48,
2608
+ outcome: "completed",
2609
+ branch: "planner/elrond-v2"
2610
+ },
2611
+ {
2612
+ id: "lotr-session-007",
2613
+ repo: "palantir-observability",
2614
+ title: "Optimize observability for eye-of-sauron alerts",
2615
+ sourceClient: "codex",
2616
+ sourceApp: "Codex app",
2617
+ model: "GPT-5.3-Codex",
2618
+ provider: "openai",
2619
+ hour: 15,
2620
+ minute: 40,
2621
+ durationMinutes: 51,
2622
+ inputTokens: 74e4,
2623
+ cachedInputTokens: 29e4,
2624
+ outputTokens: 96e3,
2625
+ reasoningTokens: 88e3,
2626
+ prompts: 4,
2627
+ replies: 8,
2628
+ tools: 44,
2629
+ commands: 17,
2630
+ edits: 5,
2631
+ reads: 36,
2632
+ outcome: "completed",
2633
+ branch: "alerts/eye-slo",
2634
+ harmlessNonZero: 2,
2635
+ exploratoryMisses: 3
2636
+ },
2637
+ {
2638
+ id: "lotr-session-008",
2639
+ repo: "mordor-ci",
2640
+ title: "Clean up dead code in orc-deployment-worker",
2641
+ sourceClient: "codex",
2642
+ sourceApp: "Terminal",
2643
+ sourceAppRaw: "terminal",
2644
+ model: "GPT-5.3-Codex-Spark",
2645
+ provider: "openai",
2646
+ hour: 16,
2647
+ minute: 10,
2648
+ durationMinutes: 39,
2649
+ inputTokens: 54e4,
2650
+ cachedInputTokens: 18e4,
2651
+ outputTokens: 84e3,
2652
+ reasoningTokens: 64e3,
2653
+ prompts: 3,
2654
+ replies: 7,
2655
+ tools: 35,
2656
+ commands: 14,
2657
+ edits: 9,
2658
+ reads: 31,
2659
+ outcome: "completed",
2660
+ branch: "workers/orc-cleanup",
2661
+ detectedSurface: "terminal_cli"
2662
+ },
2663
+ {
2664
+ id: "lotr-session-009",
2665
+ repo: "minas-tirith-admin",
2666
+ title: "Tighten city-gate roles before the beacons launch",
2667
+ sourceClient: "claude",
2668
+ sourceApp: "Claude Code",
2669
+ sourceAppRaw: "claude-code",
2670
+ model: "claude-haiku-4-5-20251001",
2671
+ provider: "anthropic",
2672
+ hour: 17,
2673
+ minute: 5,
2674
+ durationMinutes: 32,
2675
+ inputTokens: 39e4,
2676
+ cachedInputTokens: 12e4,
2677
+ outputTokens: 61e3,
2678
+ reasoningTokens: 0,
2679
+ prompts: 4,
2680
+ replies: 6,
2681
+ tools: 25,
2682
+ commands: 9,
2683
+ edits: 5,
2684
+ reads: 23,
2685
+ outcome: "completed",
2686
+ branch: "rbac/beacons"
2687
+ },
2688
+ {
2689
+ id: "lotr-session-010",
2690
+ repo: "fellowship-docs",
2691
+ title: "Document why nobody should commit the ring config",
2692
+ sourceClient: "codex",
2693
+ sourceApp: "Codex app",
2694
+ model: "GPT-5.2",
2695
+ provider: "openai",
2696
+ hour: 18,
2697
+ minute: 25,
2698
+ durationMinutes: 24,
2699
+ inputTokens: 21e4,
2700
+ cachedInputTokens: 75e3,
2701
+ outputTokens: 38e3,
2702
+ reasoningTokens: 22e3,
2703
+ prompts: 2,
2704
+ replies: 5,
2705
+ tools: 18,
2706
+ commands: 6,
2707
+ edits: 4,
2708
+ reads: 16,
2709
+ outcome: "completed",
2710
+ branch: "docs/ring-config"
2711
+ },
2712
+ {
2713
+ id: "lotr-session-011",
2714
+ repo: "one-ring-infra",
2715
+ title: "Cap runaway apply retries in black-gate provisioner",
2716
+ sourceClient: "codex",
2717
+ sourceApp: "Terminal",
2718
+ sourceAppRaw: "terminal",
2719
+ model: "GPT-5.4",
2720
+ provider: "openai",
2721
+ hour: 18,
2722
+ minute: 55,
2723
+ durationMinutes: 44,
2724
+ inputTokens: 98e4,
2725
+ cachedInputTokens: 14e4,
2726
+ outputTokens: 148e3,
2727
+ reasoningTokens: 96e3,
2728
+ prompts: 4,
2729
+ replies: 9,
2730
+ tools: 41,
2731
+ commands: 21,
2732
+ edits: 4,
2733
+ reads: 45,
2734
+ outcome: "completed",
2735
+ branch: "infra/black-gate-retries",
2736
+ importantFailures: 1,
2737
+ harmlessNonZero: 2,
2738
+ topFailureType: "deploy",
2739
+ detectedSurface: "terminal_cli",
2740
+ issueSamples: [
2741
+ issue("terraform apply -auto-approve", "deploy", "critical", "high", "The demo caught a risky retry loop and switched to a dry-run plan.")
2742
+ ]
2743
+ },
2744
+ {
2745
+ id: "lotr-session-012",
2746
+ repo: "rivendell-dashboard",
2747
+ title: "Make elvish table sorting less mysterious",
2748
+ sourceClient: "codex",
2749
+ sourceApp: "VS Code",
2750
+ sourceAppRaw: "vscode",
2751
+ model: "GPT-5.4-Mini",
2752
+ provider: "openai",
2753
+ hour: 19,
2754
+ minute: 20,
2755
+ durationMinutes: 29,
2756
+ inputTokens: 31e4,
2757
+ cachedInputTokens: 13e4,
2758
+ outputTokens: 49e3,
2759
+ reasoningTokens: 35e3,
2760
+ prompts: 3,
2761
+ replies: 6,
2762
+ tools: 22,
2763
+ commands: 8,
2764
+ edits: 3,
2765
+ reads: 18,
2766
+ outcome: "completed",
2767
+ branch: "tables/less-elvish"
2768
+ },
2769
+ {
2770
+ id: "lotr-session-013",
2771
+ repo: "gondor-api",
2772
+ title: "Trim noisy auth logs from white-tree callbacks",
2773
+ sourceClient: "codex",
2774
+ sourceApp: "Codex app",
2775
+ model: "GPT-5.3-Codex",
2776
+ provider: "openai",
2777
+ hour: 20,
2778
+ minute: 5,
2779
+ durationMinutes: 35,
2780
+ inputTokens: 42e4,
2781
+ cachedInputTokens: 16e4,
2782
+ outputTokens: 66e3,
2783
+ reasoningTokens: 54e3,
2784
+ prompts: 3,
2785
+ replies: 6,
2786
+ tools: 28,
2787
+ commands: 11,
2788
+ edits: 4,
2789
+ reads: 27,
2790
+ outcome: "completed",
2791
+ branch: "logs/white-tree"
2792
+ },
2793
+ {
2794
+ id: "lotr-session-014",
2795
+ repo: "shire-mobile",
2796
+ title: "Make pantry sync work offline in Bag End",
2797
+ sourceClient: "claude",
2798
+ sourceApp: "Claude Code",
2799
+ sourceAppRaw: "claude-code",
2800
+ model: "claude-haiku-4-5-20251001",
2801
+ provider: "anthropic",
2802
+ hour: 20,
2803
+ minute: 45,
2804
+ durationMinutes: 28,
2805
+ inputTokens: 26e4,
2806
+ cachedInputTokens: 9e4,
2807
+ outputTokens: 42e3,
2808
+ reasoningTokens: 0,
2809
+ prompts: 3,
2810
+ replies: 5,
2811
+ tools: 19,
2812
+ commands: 7,
2813
+ edits: 5,
2814
+ reads: 17,
2815
+ outcome: "completed",
2816
+ branch: "offline/bag-end"
2817
+ }
2818
+ ];
2819
+ var demoSessions = demoInputs.map(toSession);
2820
+ function toSession(input) {
2821
+ const start = timestamp2(input.hour, input.minute);
2822
+ const end = new Date(new Date(start).getTime() + input.durationMinutes * 6e4).toISOString();
2823
+ const repoRoot = `/demo/middle-earth/${input.repo}`;
2824
+ const totalTokens = input.inputTokens + input.outputTokens + input.reasoningTokens;
2825
+ const importantFailures = input.importantFailures ?? 0;
2826
+ const harmlessNonZero = input.harmlessNonZero ?? 0;
2827
+ const exploratoryMisses = input.exploratoryMisses ?? 0;
2828
+ const repeatedClusters = input.repeatedClusters ?? 0;
2829
+ const failedToolCalls = importantFailures + repeatedClusters;
2830
+ const nonZeroCommandEvents = importantFailures + harmlessNonZero + exploratoryMisses;
2831
+ const cost = calculateCostUsd({
2832
+ model: input.model,
2833
+ inputTokens: input.inputTokens,
2834
+ cachedInputTokens: input.cachedInputTokens,
2835
+ outputTokens: input.outputTokens,
2836
+ reasoningTokens: input.reasoningTokens
2837
+ }, demoPricing);
2838
+ return {
2839
+ id: input.id,
2840
+ sourceClient: input.sourceClient,
2841
+ sourceApp: input.sourceApp,
2842
+ sourceAppRaw: input.sourceAppRaw ?? input.sourceApp.toLowerCase(),
2843
+ sourcePath: `/demo/middle-earth/${input.sourceClient}/sessions/${input.id}.jsonl`,
2844
+ repoRoot,
2845
+ repoName: input.repo,
2846
+ cwd: `${repoRoot}/worktree`,
2847
+ gitRemoteUrl: `https://example.invalid/middle-earth/${input.repo}.git`,
2848
+ gitBranch: input.branch,
2849
+ title: input.title,
2850
+ startedAt: start,
2851
+ endedAt: end,
2852
+ model: input.model,
2853
+ provider: input.provider,
2854
+ durationMs: input.durationMinutes * 6e4,
2855
+ rawEventCount: input.tools + input.replies + input.prompts + 8,
2856
+ parseStatus: "ok",
2857
+ parseErrors: [],
2858
+ detectedSurface: input.detectedSurface ?? detectedSurfaceForApp(input.sourceApp),
2859
+ surfaceConfidence: "high",
2860
+ surfaceReason: "Demo fixture includes an explicit fictional source app.",
2861
+ promptTimeline: [
2862
+ { role: "user", text: input.title, timestamp: start },
2863
+ { role: "assistant", text: demoReply(input.repo, input.edits, importantFailures), timestamp: timestamp2(input.hour, Math.min(input.minute + 7, 59)) }
2864
+ ],
2865
+ sessionOutcome: input.outcome,
2866
+ inputTokens: input.inputTokens,
2867
+ cachedInputTokens: input.cachedInputTokens,
2868
+ outputTokens: input.outputTokens,
2869
+ reasoningTokens: input.reasoningTokens,
2870
+ reasoningOutputTokens: input.reasoningTokens,
2871
+ totalTokens,
2872
+ tokenAggregationMethod: "direct_usage",
2873
+ tokenConfidence: input.tokenConfidence ?? "high",
2874
+ tokenSnapshotCount: Math.max(2, Math.round(input.durationMinutes / 18)),
2875
+ estimatedCostUsd: cost,
2876
+ messageCount: input.prompts + input.replies,
2877
+ rawTokenTotal: totalTokens,
2878
+ warnings: input.warnings ?? [],
2879
+ userPromptCount: input.prompts,
2880
+ assistantMessageCount: input.replies,
2881
+ toolCallCount: input.tools,
2882
+ shellCommandCount: input.commands,
2883
+ failedToolCallCount: failedToolCalls,
2884
+ nonZeroCommandEvents,
2885
+ importantCommandFailures: importantFailures,
2886
+ harmlessNonZeroEvents: harmlessNonZero,
2887
+ exploratoryMisses,
2888
+ repeatedFailureClusters: repeatedClusters,
2889
+ commandIssueSeverity: importantFailures > 1 ? "critical" : importantFailures > 0 ? "warning" : "none",
2890
+ commandIssueImpact: importantFailures > 1 ? "high" : importantFailures > 0 ? "medium" : "none",
2891
+ topFailureType: input.topFailureType,
2892
+ commandIssueSamples: input.issueSamples ?? [],
2893
+ fileReadCount: input.reads,
2894
+ fileEditCount: input.edits
2895
+ };
2896
+ }
2897
+ function detectedSurfaceForApp(sourceApp) {
2898
+ if (sourceApp === "VS Code") return "vscode_extension";
2899
+ if (sourceApp === "Terminal") return "terminal_cli";
2900
+ if (sourceApp === "Codex app") return "codex_app_cloud";
2901
+ if (sourceApp === "Claude desktop local agent") return "local_agent";
2902
+ return "terminal_cli";
2903
+ }
2904
+ function timestamp2(hour, minute) {
2905
+ return `${baseDate}T${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}:00.000Z`;
2906
+ }
2907
+ function issue(command2, category, severity, impact, reason) {
2908
+ return {
2909
+ command: command2,
2910
+ category,
2911
+ classification: severity === "critical" ? "blocking_failure" : "warning",
2912
+ severity,
2913
+ impact,
2914
+ reason
2915
+ };
2916
+ }
2917
+ function demoReply(repo, edits, importantFailures) {
2918
+ if (importantFailures > 0) {
2919
+ return `Found the noisy path in ${repo}, separated the risky failures from ordinary exploration, and left a short list of follow-up checks.`;
2920
+ }
2921
+ if (edits === 0) {
2922
+ return `Mapped the spend pattern in ${repo} without changing files, which is exactly the kind of research-only trail RepoSpend should make visible.`;
2923
+ }
2924
+ return `Updated ${repo} with focused changes and enough local checks to make the token spend easy to explain.`;
2925
+ }
2926
+
1800
2927
  // apps/server/src/rtk.ts
1801
2928
  import { execFile } from "node:child_process";
1802
2929
  import { promisify } from "node:util";
@@ -1991,8 +3118,10 @@ function createServer(options = {}) {
1991
3118
  const app = Fastify({ logger: false });
1992
3119
  app.get("/api/health", async () => ({ ok: true, app: "RepoSpend", version: readPackageVersion() }));
1993
3120
  app.get("/api/dashboard", async (request) => {
1994
- const data = readDashboardData(parseFilters(request.query));
1995
- const rtkGain = await readRtkGain();
3121
+ const filters = parseFilters(request.query);
3122
+ const demoMode = demoModeEnabled();
3123
+ const data = demoMode ? readDemoDashboardData(filters) : readDashboardData(filters);
3124
+ const rtkGain = demoMode ? readDemoRtkGain() : await readRtkGain();
1996
3125
  return {
1997
3126
  ...data,
1998
3127
  summary: { ...data.summary, skippedZeroTokenSessions: data.skippedZeroTokenSessions },
@@ -2018,9 +3147,9 @@ function createServer(options = {}) {
2018
3147
  return exportCsv();
2019
3148
  });
2020
3149
  if (options.serveWeb !== false) {
2021
- const serverDist = path5.dirname(fileURLToPath(import.meta.url));
2022
- const packagedWebDist = path5.resolve(serverDist, "../web-dist");
2023
- const workspaceWebDist = path5.resolve(serverDist, "../../web/dist");
3150
+ const serverDist = path6.dirname(fileURLToPath(import.meta.url));
3151
+ const packagedWebDist = path6.resolve(serverDist, "../web-dist");
3152
+ const workspaceWebDist = path6.resolve(serverDist, "../../web/dist");
2024
3153
  const webDist = fsExists(packagedWebDist) ? packagedWebDist : workspaceWebDist;
2025
3154
  if (!fsExists(webDist)) {
2026
3155
  throw new Error(`RepoSpend web assets are missing. Expected ${packagedWebDist} in the published package, or ${workspaceWebDist} during local development. Run "pnpm pack:prepare" before packaging.`);
@@ -2041,13 +3170,13 @@ function createServer(options = {}) {
2041
3170
  return app;
2042
3171
  }
2043
3172
  function fsExists(inputPath) {
2044
- return Boolean(inputPath) && path5.isAbsolute(inputPath) && fs6.existsSync(inputPath);
3173
+ return Boolean(inputPath) && path6.isAbsolute(inputPath) && fs7.existsSync(inputPath);
2045
3174
  }
2046
3175
  function readPackageVersion() {
2047
- const serverDist = path5.dirname(fileURLToPath(import.meta.url));
2048
- const packagePath = path5.resolve(serverDist, "../package.json");
3176
+ const serverDist = path6.dirname(fileURLToPath(import.meta.url));
3177
+ const packagePath = path6.resolve(serverDist, "../package.json");
2049
3178
  try {
2050
- const parsed = JSON.parse(fs6.readFileSync(packagePath, "utf8"));
3179
+ const parsed = JSON.parse(fs7.readFileSync(packagePath, "utf8"));
2051
3180
  return typeof parsed.version === "string" ? parsed.version : "unknown";
2052
3181
  } catch {
2053
3182
  return "unknown";
@@ -2080,8 +3209,10 @@ function validatePricingBody(body) {
2080
3209
  outputPerMillion
2081
3210
  };
2082
3211
  const cachedInputPerMillion = finiteNumber(row.cachedInputPerMillion);
3212
+ const cacheCreationInputPerMillion = finiteNumber(row.cacheCreationInputPerMillion);
2083
3213
  const reasoningOutputPerMillion = finiteNumber(row.reasoningOutputPerMillion);
2084
3214
  if (cachedInputPerMillion !== void 0) modelPricing.cachedInputPerMillion = cachedInputPerMillion;
3215
+ if (cacheCreationInputPerMillion !== void 0) modelPricing.cacheCreationInputPerMillion = cacheCreationInputPerMillion;
2085
3216
  if (reasoningOutputPerMillion !== void 0) modelPricing.reasoningOutputPerMillion = reasoningOutputPerMillion;
2086
3217
  if (typeof row.note === "string") modelPricing.note = row.note;
2087
3218
  validated[model] = modelPricing;
@@ -2097,27 +3228,30 @@ function finiteNumber(value) {
2097
3228
  // apps/server/src/cli.ts
2098
3229
  var [command = "serve", ...args] = process.argv.slice(2);
2099
3230
  try {
2100
- if (command === "serve") {
3231
+ if (command === "--help" || command === "-h" || command === "help") {
3232
+ printHelp();
3233
+ } else if (command === "serve") {
2101
3234
  const { url } = await startServer({ port: Number(process.env.REPOSPEND_PORT ?? 2005), serveWeb: true });
2102
3235
  console.log(`RepoSpend dashboard: ${url}`);
2103
3236
  openBrowser(url);
2104
3237
  } else if (command === "scan") {
2105
- const data = readDashboardData();
3238
+ const data = readDashboardData(cliFilters(args));
2106
3239
  console.log(JSON.stringify({ sources: data.sources, sessionCount: data.sessions.length, summary: data.summary }, null, 2));
2107
3240
  } else if (command === "by-repo") {
2108
- printGroups(groupByRepo(readDashboardData().sessions));
3241
+ printGroups(groupByRepo(readDashboardData(cliFilters(args)).sessions));
2109
3242
  } else if (command === "by-day") {
2110
- printGroups(groupByDay(readDashboardData().sessions));
3243
+ printGroups(groupByDay(readDashboardData(cliFilters(args)).sessions));
2111
3244
  } else if (command === "by-hour") {
2112
- printGroups(groupByHour(readDashboardData().sessions));
3245
+ printGroups(groupByHour(readDashboardData(cliFilters(args)).sessions));
2113
3246
  } else if (command === "by-model") {
2114
- printGroups(groupByModel(readDashboardData().sessions));
3247
+ printGroups(groupByModel(readDashboardData(cliFilters(args)).sessions));
2115
3248
  } else if (command === "by-app") {
2116
- printGroups(groupBySourceApp(readDashboardData().sessions));
3249
+ printGroups(groupBySourceApp(readDashboardData(cliFilters(args)).sessions));
2117
3250
  } else if (command === "export") {
2118
3251
  const format = valueAfter(args, "--format") ?? "json";
2119
- const sessions = readDashboardData().sessions;
2120
- console.log(format === "csv" ? toCsv(sessions) : exportJson());
3252
+ const filters = cliFilters(args);
3253
+ const sessions = readDashboardData(filters).sessions;
3254
+ console.log(format === "csv" ? toCsv(sessions) : JSON.stringify(sessions, null, 2));
2121
3255
  } else {
2122
3256
  console.error(`Unknown command: ${command}`);
2123
3257
  printHelp();
@@ -2138,7 +3272,7 @@ function openBrowser(url) {
2138
3272
  }
2139
3273
  function printGroups(groups) {
2140
3274
  if (!groups.length) {
2141
- console.log("No usage found. Scanned ~/.codex/state_5.sqlite and ~/.codex/sessions.");
3275
+ console.log("No usage found. Scanned Codex and Claude Code local data paths.");
2142
3276
  return;
2143
3277
  }
2144
3278
  const rows = groups.map((group) => ({
@@ -2149,6 +3283,11 @@ function printGroups(groups) {
2149
3283
  }));
2150
3284
  console.table(rows);
2151
3285
  }
3286
+ function cliFilters(args2) {
3287
+ const source = valueAfter(args2, "--source");
3288
+ if (!source || source === "all") return {};
3289
+ return { source };
3290
+ }
2152
3291
  function valueAfter(args2, flag) {
2153
3292
  const index = args2.indexOf(flag);
2154
3293
  return index >= 0 ? args2[index + 1] : void 0;
@@ -2163,5 +3302,8 @@ function printHelp() {
2163
3302
  repospend by-model
2164
3303
  repospend by-app
2165
3304
  repospend export --format json
2166
- repospend export --format csv`);
3305
+ repospend export --format csv
3306
+
3307
+ Options:
3308
+ --source all|codex|claude`);
2167
3309
  }