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/CHANGELOG.md +24 -0
- package/README.md +175 -92
- package/SECURITY.md +65 -0
- package/dist/cli.js +1216 -74
- package/docs/PUBLISHING.md +57 -0
- package/package.json +8 -3
- package/web-dist/assets/index-B8lKmhuv.js +260 -0
- package/web-dist/assets/index-CUTybR_N.css +1 -0
- package/web-dist/index.html +2 -2
- package/web-dist/assets/index-B8WOvRzk.js +0 -260
- package/web-dist/assets/index-vwATk9ov.css +0 -1
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
|
|
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 =
|
|
266
|
+
const modelPricing = findModelPricing(usage.model, pricing);
|
|
252
267
|
if (!modelPricing) {
|
|
253
268
|
return void 0;
|
|
254
269
|
}
|
|
255
|
-
const
|
|
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(
|
|
810
|
+
const allSessions = input.sessions.filter(isDashboardVisibleSession);
|
|
767
811
|
const filteredSessions = filterUsage(input.sessions, input.filters ?? {});
|
|
768
|
-
const sessions = filteredSessions.filter(
|
|
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((
|
|
947
|
-
for (const
|
|
948
|
-
repeatedCommands.set(
|
|
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
|
|
954
|
-
if (repeatedCommands.get(
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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
|
|
963
|
-
if (
|
|
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((
|
|
968
|
-
analysis.harmlessNonZeroEvents = analysis.commandIssues.filter((
|
|
969
|
-
analysis.exploratoryMisses = analysis.commandIssues.filter((
|
|
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((
|
|
978
|
-
...
|
|
979
|
-
reason: commandIssueReason(
|
|
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(
|
|
983
|
-
const severityScore =
|
|
984
|
-
const impactScore =
|
|
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(
|
|
988
|
-
if (
|
|
989
|
-
if (
|
|
990
|
-
if (
|
|
991
|
-
if (
|
|
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((
|
|
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((
|
|
1046
|
-
if (issues.some((
|
|
1047
|
-
if (issues.some((
|
|
1048
|
-
if (issues.some((
|
|
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((
|
|
1053
|
-
if (issues.some((
|
|
1054
|
-
if (issues.some((
|
|
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((
|
|
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
|
|
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 +
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
2243
|
+
const scan = scanUsageSources({ config, pricing });
|
|
1726
2244
|
const data = {
|
|
1727
|
-
sources:
|
|
1728
|
-
sourceStats:
|
|
1729
|
-
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 =
|
|
1771
|
-
|
|
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
|
|
1795
|
-
import
|
|
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
|
|
1995
|
-
const
|
|
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 =
|
|
2022
|
-
const packagedWebDist =
|
|
2023
|
-
const workspaceWebDist =
|
|
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) &&
|
|
3173
|
+
return Boolean(inputPath) && path6.isAbsolute(inputPath) && fs7.existsSync(inputPath);
|
|
2045
3174
|
}
|
|
2046
3175
|
function readPackageVersion() {
|
|
2047
|
-
const serverDist =
|
|
2048
|
-
const packagePath =
|
|
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(
|
|
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 === "
|
|
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
|
|
2120
|
-
|
|
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
|
|
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
|
}
|