repospend 0.0.2 → 0.0.4
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 +28 -0
- package/README.md +175 -92
- package/SECURITY.md +65 -0
- package/dist/cli.js +1285 -76
- 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";
|
|
@@ -889,9 +936,11 @@ function parseBranch(head) {
|
|
|
889
936
|
}
|
|
890
937
|
|
|
891
938
|
// packages/core/src/sources/codex.ts
|
|
939
|
+
import { spawnSync } from "node:child_process";
|
|
892
940
|
import fs4 from "node:fs";
|
|
893
941
|
import os2 from "node:os";
|
|
894
942
|
import path4 from "node:path";
|
|
943
|
+
import { fileURLToPath } from "node:url";
|
|
895
944
|
import Database from "better-sqlite3";
|
|
896
945
|
|
|
897
946
|
// packages/core/src/sources/agent-friction.ts
|
|
@@ -943,30 +992,30 @@ function applyActionMetadata(analysis, record) {
|
|
|
943
992
|
}
|
|
944
993
|
function finalizeCommandIssues(analysis, totalTokens) {
|
|
945
994
|
const repeatedCommands = /* @__PURE__ */ new Map();
|
|
946
|
-
const repeatableIssues = analysis.commandIssues.filter((
|
|
947
|
-
for (const
|
|
948
|
-
repeatedCommands.set(
|
|
995
|
+
const repeatableIssues = analysis.commandIssues.filter((issue2) => issue2.classification !== "harmless_nonzero" && issue2.classification !== "exploratory_miss");
|
|
996
|
+
for (const issue2 of repeatableIssues) {
|
|
997
|
+
repeatedCommands.set(issue2.command, (repeatedCommands.get(issue2.command) ?? 0) + 1);
|
|
949
998
|
}
|
|
950
999
|
const repeatedFailures = [...repeatedCommands.entries()].filter(([, count2]) => count2 >= 3);
|
|
951
1000
|
if (repeatedFailures.length > 0) {
|
|
952
1001
|
analysis.repeatedFailureClusters = repeatedFailures.length;
|
|
953
|
-
for (const
|
|
954
|
-
if (repeatedCommands.get(
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1002
|
+
for (const issue2 of analysis.commandIssues) {
|
|
1003
|
+
if (repeatedCommands.get(issue2.command) >= 3 && issue2.classification !== "harmless_nonzero" && issue2.classification !== "exploratory_miss") {
|
|
1004
|
+
issue2.classification = "blocking_failure";
|
|
1005
|
+
issue2.severity = "critical";
|
|
1006
|
+
issue2.impact = "high";
|
|
958
1007
|
}
|
|
959
1008
|
}
|
|
960
1009
|
}
|
|
961
1010
|
if (totalTokens >= 1e6 && analysis.commandIssues.length >= 5) {
|
|
962
|
-
for (const
|
|
963
|
-
if (
|
|
1011
|
+
for (const issue2 of analysis.commandIssues) {
|
|
1012
|
+
if (issue2.severity === "warning") issue2.impact = "high";
|
|
964
1013
|
}
|
|
965
1014
|
}
|
|
966
1015
|
analysis.nonZeroCommandEvents = analysis.commandIssues.length;
|
|
967
|
-
analysis.importantCommandFailures = analysis.commandIssues.filter((
|
|
968
|
-
analysis.harmlessNonZeroEvents = analysis.commandIssues.filter((
|
|
969
|
-
analysis.exploratoryMisses = analysis.commandIssues.filter((
|
|
1016
|
+
analysis.importantCommandFailures = analysis.commandIssues.filter((issue2) => issue2.severity === "critical" || issue2.severity === "warning").length;
|
|
1017
|
+
analysis.harmlessNonZeroEvents = analysis.commandIssues.filter((issue2) => issue2.classification === "harmless_nonzero").length;
|
|
1018
|
+
analysis.exploratoryMisses = analysis.commandIssues.filter((issue2) => issue2.classification === "exploratory_miss").length;
|
|
970
1019
|
analysis.failedToolCallCount = analysis.importantCommandFailures;
|
|
971
1020
|
analysis.commandIssueSeverity = strongestSeverity(analysis.commandIssues);
|
|
972
1021
|
analysis.commandIssueImpact = strongestImpact(analysis.commandIssues);
|
|
@@ -974,21 +1023,21 @@ function finalizeCommandIssues(analysis, totalTokens) {
|
|
|
974
1023
|
analysis.commandIssueSamples = sampledCommandIssues(analysis.commandIssues);
|
|
975
1024
|
}
|
|
976
1025
|
function sampledCommandIssues(issues) {
|
|
977
|
-
return [...issues].sort((a, b) => issuePriority(b) - issuePriority(a)).slice(0, 12).map((
|
|
978
|
-
...
|
|
979
|
-
reason: commandIssueReason(
|
|
1026
|
+
return [...issues].sort((a, b) => issuePriority(b) - issuePriority(a)).slice(0, 12).map((issue2) => ({
|
|
1027
|
+
...issue2,
|
|
1028
|
+
reason: commandIssueReason(issue2)
|
|
980
1029
|
}));
|
|
981
1030
|
}
|
|
982
|
-
function issuePriority(
|
|
983
|
-
const severityScore =
|
|
984
|
-
const impactScore =
|
|
1031
|
+
function issuePriority(issue2) {
|
|
1032
|
+
const severityScore = issue2.severity === "critical" ? 4 : issue2.severity === "warning" ? 3 : issue2.severity === "info" ? 2 : issue2.severity === "ignored" ? 1 : 0;
|
|
1033
|
+
const impactScore = issue2.impact === "high" ? 3 : issue2.impact === "medium" ? 2 : issue2.impact === "low" ? 1 : 0;
|
|
985
1034
|
return severityScore * 10 + impactScore;
|
|
986
1035
|
}
|
|
987
|
-
function commandIssueReason(
|
|
988
|
-
if (
|
|
989
|
-
if (
|
|
990
|
-
if (
|
|
991
|
-
if (
|
|
1036
|
+
function commandIssueReason(issue2) {
|
|
1037
|
+
if (issue2.severity === "critical") return "Repeated or blocking command issue that may have stopped progress.";
|
|
1038
|
+
if (issue2.severity === "warning") return "Non-zero command event classified as important for this session.";
|
|
1039
|
+
if (issue2.classification === "exploratory_miss") return "Likely exploratory command that found no result.";
|
|
1040
|
+
if (issue2.classification === "harmless_nonzero") return "Likely harmless non-zero exit during normal shell exploration.";
|
|
992
1041
|
return "Non-zero command event with limited local context.";
|
|
993
1042
|
}
|
|
994
1043
|
function classifyCommandIssue(record, previousIssues) {
|
|
@@ -997,7 +1046,7 @@ function classifyCommandIssue(record, previousIssues) {
|
|
|
997
1046
|
const command2 = extractCommandText(record) ?? compactCommandText(raw);
|
|
998
1047
|
const category = commandCategory(text);
|
|
999
1048
|
const commandKey = normalizeCommand(command2);
|
|
1000
|
-
const repeated = previousIssues.filter((
|
|
1049
|
+
const repeated = previousIssues.filter((issue2) => issue2.command === commandKey).length + 1 >= 3;
|
|
1001
1050
|
if (isHarmlessNonZero(text)) {
|
|
1002
1051
|
return { command: commandKey, category, classification: isExploratoryMiss(text) ? "exploratory_miss" : "harmless_nonzero", severity: "ignored", impact: "low" };
|
|
1003
1052
|
}
|
|
@@ -1042,23 +1091,23 @@ function importantImpact(category) {
|
|
|
1042
1091
|
return category === "install" || category === "build" || category === "test" || category === "typecheck" || category === "deploy" ? "high" : "medium";
|
|
1043
1092
|
}
|
|
1044
1093
|
function strongestSeverity(issues) {
|
|
1045
|
-
if (issues.some((
|
|
1046
|
-
if (issues.some((
|
|
1047
|
-
if (issues.some((
|
|
1048
|
-
if (issues.some((
|
|
1094
|
+
if (issues.some((issue2) => issue2.severity === "critical")) return "critical";
|
|
1095
|
+
if (issues.some((issue2) => issue2.severity === "warning")) return "warning";
|
|
1096
|
+
if (issues.some((issue2) => issue2.severity === "info")) return "info";
|
|
1097
|
+
if (issues.some((issue2) => issue2.severity === "ignored")) return "ignored";
|
|
1049
1098
|
return "none";
|
|
1050
1099
|
}
|
|
1051
1100
|
function strongestImpact(issues) {
|
|
1052
|
-
if (issues.some((
|
|
1053
|
-
if (issues.some((
|
|
1054
|
-
if (issues.some((
|
|
1101
|
+
if (issues.some((issue2) => issue2.impact === "high")) return "high";
|
|
1102
|
+
if (issues.some((issue2) => issue2.impact === "medium")) return "medium";
|
|
1103
|
+
if (issues.some((issue2) => issue2.impact === "low")) return "low";
|
|
1055
1104
|
return "none";
|
|
1056
1105
|
}
|
|
1057
1106
|
function topFailureType(issues) {
|
|
1058
|
-
const important = issues.filter((
|
|
1107
|
+
const important = issues.filter((issue2) => issue2.severity === "critical" || issue2.severity === "warning");
|
|
1059
1108
|
const source = important.length ? important : issues;
|
|
1060
1109
|
const counts = /* @__PURE__ */ new Map();
|
|
1061
|
-
for (const
|
|
1110
|
+
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
1111
|
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
1063
1112
|
}
|
|
1064
1113
|
function extractCommandText(record) {
|
|
@@ -1221,6 +1270,7 @@ function numberFromKeys(object, keys) {
|
|
|
1221
1270
|
}
|
|
1222
1271
|
|
|
1223
1272
|
// packages/core/src/sources/codex.ts
|
|
1273
|
+
var nativeSqliteRebuildAttempted = false;
|
|
1224
1274
|
function scanCodex(options) {
|
|
1225
1275
|
const codexHome = options.codexHome ?? path4.join(os2.homedir(), ".codex");
|
|
1226
1276
|
const statePath = path4.join(codexHome, "state_5.sqlite");
|
|
@@ -1256,6 +1306,9 @@ function scanCodex(options) {
|
|
|
1256
1306
|
},
|
|
1257
1307
|
sessions,
|
|
1258
1308
|
stats: {
|
|
1309
|
+
sourceId: "codex",
|
|
1310
|
+
sourceLabel: "Codex",
|
|
1311
|
+
homePath: codexHome,
|
|
1259
1312
|
codexHome,
|
|
1260
1313
|
statePath,
|
|
1261
1314
|
sessionsPath,
|
|
@@ -1272,7 +1325,7 @@ function scanCodex(options) {
|
|
|
1272
1325
|
function readThreads(statePath, sessionFiles, options, warnings) {
|
|
1273
1326
|
let db;
|
|
1274
1327
|
try {
|
|
1275
|
-
db =
|
|
1328
|
+
db = openCodexStateDatabase(statePath, warnings);
|
|
1276
1329
|
const tableNames = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
|
|
1277
1330
|
if (!tableNames.some((table) => table.name === "threads")) {
|
|
1278
1331
|
warnings.push("Codex SQLite state does not contain a threads table.");
|
|
@@ -1293,6 +1346,70 @@ function readThreads(statePath, sessionFiles, options, warnings) {
|
|
|
1293
1346
|
db?.close();
|
|
1294
1347
|
}
|
|
1295
1348
|
}
|
|
1349
|
+
function openCodexStateDatabase(statePath, warnings) {
|
|
1350
|
+
try {
|
|
1351
|
+
return new Database(statePath, { readonly: true, fileMustExist: true });
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
if (isNativeSqliteMismatch(error) && attemptNativeSqliteRebuild(warnings)) {
|
|
1354
|
+
return new Database(statePath, { readonly: true, fileMustExist: true });
|
|
1355
|
+
}
|
|
1356
|
+
throw error;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
function attemptNativeSqliteRebuild(warnings) {
|
|
1360
|
+
if (nativeSqliteRebuildAttempted || process.env.REPOSPEND_SKIP_NATIVE_REBUILD === "1") return false;
|
|
1361
|
+
nativeSqliteRebuildAttempted = true;
|
|
1362
|
+
const packageRoot = findRepoSpendPackageRoot();
|
|
1363
|
+
if (!packageRoot) {
|
|
1364
|
+
warnings.push("Codex SQLite native module mismatch detected, but RepoSpend could not locate its package root for an automatic rebuild.");
|
|
1365
|
+
return false;
|
|
1366
|
+
}
|
|
1367
|
+
const packageManager = fs4.existsSync(path4.join(packageRoot, "pnpm-lock.yaml")) ? "pnpm" : "npm";
|
|
1368
|
+
const result = spawnSync(packageManager, ["rebuild", "better-sqlite3"], {
|
|
1369
|
+
cwd: packageRoot,
|
|
1370
|
+
encoding: "utf8",
|
|
1371
|
+
stdio: "pipe",
|
|
1372
|
+
timeout: 12e4
|
|
1373
|
+
});
|
|
1374
|
+
if (result.status === 0) return true;
|
|
1375
|
+
const detail = [result.stderr, result.stdout].filter(Boolean).join("\n").split("\n").map((line) => line.trim()).filter(Boolean).slice(-3).join(" ");
|
|
1376
|
+
warnings.push(`Codex SQLite native module mismatch detected. RepoSpend tried to rebuild better-sqlite3 automatically with ${packageManager}, but it failed${exitLabel(result.status)}.${detail ? ` ${detail}` : ""}`);
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
function findRepoSpendPackageRoot() {
|
|
1380
|
+
const starts = [
|
|
1381
|
+
process.argv[1] ? path4.dirname(fs4.realpathSync(process.argv[1])) : void 0,
|
|
1382
|
+
path4.dirname(fileURLToPath(import.meta.url)),
|
|
1383
|
+
process.cwd()
|
|
1384
|
+
].filter((item) => Boolean(item));
|
|
1385
|
+
for (const start of starts) {
|
|
1386
|
+
const root = findPackageRootNamedRepoSpend(start);
|
|
1387
|
+
if (root) return root;
|
|
1388
|
+
}
|
|
1389
|
+
return void 0;
|
|
1390
|
+
}
|
|
1391
|
+
function findPackageRootNamedRepoSpend(start) {
|
|
1392
|
+
let current = path4.resolve(start);
|
|
1393
|
+
while (true) {
|
|
1394
|
+
const packagePath = path4.join(current, "package.json");
|
|
1395
|
+
if (fs4.existsSync(packagePath)) {
|
|
1396
|
+
try {
|
|
1397
|
+
const packageJson = JSON.parse(fs4.readFileSync(packagePath, "utf8"));
|
|
1398
|
+
if (packageJson.name === "repospend") return current;
|
|
1399
|
+
} catch {
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
const parent = path4.dirname(current);
|
|
1403
|
+
if (parent === current) return void 0;
|
|
1404
|
+
current = parent;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
function isNativeSqliteMismatch(error) {
|
|
1408
|
+
return errorMessage(error).includes("NODE_MODULE_VERSION");
|
|
1409
|
+
}
|
|
1410
|
+
function exitLabel(status) {
|
|
1411
|
+
return status === null ? "" : ` with exit code ${status}`;
|
|
1412
|
+
}
|
|
1296
1413
|
function threadToUsage(row, index, sessionFiles, options) {
|
|
1297
1414
|
const sourcePath = stringValue2(row.rollout_path) ?? matchSessionPath(stringValue2(row.id), sessionFiles) ?? "";
|
|
1298
1415
|
const sessionBreakdown = sourcePath ? readSessionBreakdown(sourcePath) : emptyBreakdown();
|
|
@@ -1303,7 +1420,7 @@ function threadToUsage(row, index, sessionFiles, options) {
|
|
|
1303
1420
|
const cachedInputTokens = sessionBreakdown.cachedInputTokens;
|
|
1304
1421
|
const outputTokens = sessionBreakdown.outputTokens;
|
|
1305
1422
|
const reasoningTokens = sessionBreakdown.reasoningTokens;
|
|
1306
|
-
const totalTokens = sessionBreakdown.totalTokens || dbTokens || inputTokens +
|
|
1423
|
+
const totalTokens = sessionBreakdown.totalTokens || dbTokens || inputTokens + outputTokens + reasoningTokens;
|
|
1307
1424
|
const hasPricedBreakdown = inputTokens + cachedInputTokens + outputTokens + reasoningTokens > 0;
|
|
1308
1425
|
const warnings = [...repo.warnings, ...sessionBreakdown.warnings];
|
|
1309
1426
|
const threadSource = stringValue2(row.thread_source) ?? sessionBreakdown.threadSource;
|
|
@@ -1676,12 +1793,12 @@ function stringValue2(value) {
|
|
|
1676
1793
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1677
1794
|
}
|
|
1678
1795
|
function codexSourceAppLabel(source, threadSource) {
|
|
1679
|
-
if (threadSource === "subagent") return "Codex
|
|
1796
|
+
if (threadSource === "subagent") return "Codex app";
|
|
1680
1797
|
if (!source) return "Codex";
|
|
1681
1798
|
if (source === "vscode" || source === "codex_vscode" || source === "codex-vscode") return "VS Code";
|
|
1682
1799
|
if (source === "cli" || source === "terminal" || source === "codex-tui") return "Terminal";
|
|
1683
1800
|
if (source === "codex_app" || source === "codex-app") return "Codex app";
|
|
1684
|
-
if (source.trim().startsWith("{")) return threadSource === "subagent" ? "Codex
|
|
1801
|
+
if (source.trim().startsWith("{")) return threadSource === "subagent" ? "Codex app" : "Codex";
|
|
1685
1802
|
return source.replace(/^codex[_-]/, "").split(/[_-]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
1686
1803
|
}
|
|
1687
1804
|
function dateValue(value) {
|
|
@@ -1704,8 +1821,476 @@ function errorMessage(error) {
|
|
|
1704
1821
|
return message;
|
|
1705
1822
|
}
|
|
1706
1823
|
|
|
1707
|
-
//
|
|
1824
|
+
// packages/core/src/sources/claude.ts
|
|
1708
1825
|
import fs5 from "node:fs";
|
|
1826
|
+
import os3 from "node:os";
|
|
1827
|
+
import path5 from "node:path";
|
|
1828
|
+
function scanClaude(options) {
|
|
1829
|
+
const claudeHome = options.claudeHome ?? defaultClaudeHome();
|
|
1830
|
+
const historyPath = path5.join(claudeHome, "history.jsonl");
|
|
1831
|
+
const warnings = [];
|
|
1832
|
+
const discovery = discoverClaudeSessionFiles(claudeHome, warnings, Boolean(options.claudeHome));
|
|
1833
|
+
const historyExists = fs5.existsSync(historyPath);
|
|
1834
|
+
const sessions = discovery.files.map((file, index) => claudeFileToUsage(file, index, options));
|
|
1835
|
+
const historyEntryCount = countHistoryEntries(historyPath);
|
|
1836
|
+
const parseFailureCount = sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).length;
|
|
1837
|
+
const unreadableFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_session_file:"))).length;
|
|
1838
|
+
const malformedFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("jsonl_line_"))).length;
|
|
1839
|
+
const projectsPath = path5.join(claudeHome, "projects");
|
|
1840
|
+
if (!discovery.projectsPaths.some((candidate) => fs5.existsSync(candidate))) {
|
|
1841
|
+
warnings.push(`Claude Code projects directory not found at ${projectsPath}`);
|
|
1842
|
+
}
|
|
1843
|
+
return {
|
|
1844
|
+
source: {
|
|
1845
|
+
id: "claude",
|
|
1846
|
+
label: "Claude Code",
|
|
1847
|
+
available: discovery.files.length > 0 || historyExists,
|
|
1848
|
+
paths: unique3([...discovery.projectsPaths, historyPath, ...discovery.desktopSessionRoots]),
|
|
1849
|
+
warnings
|
|
1850
|
+
},
|
|
1851
|
+
sessions,
|
|
1852
|
+
stats: {
|
|
1853
|
+
sourceId: "claude",
|
|
1854
|
+
sourceLabel: "Claude Code",
|
|
1855
|
+
homePath: claudeHome,
|
|
1856
|
+
claudeHome,
|
|
1857
|
+
projectsPath,
|
|
1858
|
+
historyPath,
|
|
1859
|
+
projectsExists: discovery.projectsPaths.some((candidate) => fs5.existsSync(candidate)),
|
|
1860
|
+
historyExists,
|
|
1861
|
+
sessionFileCount: discovery.files.length,
|
|
1862
|
+
projectDirCount: discovery.projectDirs.size,
|
|
1863
|
+
sessionsImported: sessions.length,
|
|
1864
|
+
parseFailureCount,
|
|
1865
|
+
unreadableFileCount,
|
|
1866
|
+
malformedFileCount,
|
|
1867
|
+
historyEntryCount,
|
|
1868
|
+
lastScannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
function discoverClaudeSessionFiles(claudeHome, warnings, explicitHome) {
|
|
1873
|
+
const files = [];
|
|
1874
|
+
const projectDirs = /* @__PURE__ */ new Set();
|
|
1875
|
+
const projectsPaths = explicitHome ? [path5.join(claudeHome, "projects")] : unique3([...configuredClaudeHomes(claudeHome).map((root) => path5.join(root, "projects")), path5.join(os3.homedir(), ".config", "claude", "projects")]);
|
|
1876
|
+
for (const projectsPath of projectsPaths) {
|
|
1877
|
+
collectProjectFiles(projectsPath, files, projectDirs, warnings);
|
|
1878
|
+
}
|
|
1879
|
+
const desktopSessionRoots = explicitHome ? [] : defaultDesktopSessionRoots();
|
|
1880
|
+
for (const root of desktopSessionRoots) {
|
|
1881
|
+
collectDesktopSessionFiles(root, files, projectDirs, warnings);
|
|
1882
|
+
}
|
|
1883
|
+
return {
|
|
1884
|
+
files: uniqueSessionFiles(files),
|
|
1885
|
+
projectDirs,
|
|
1886
|
+
projectsPaths,
|
|
1887
|
+
desktopSessionRoots
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
function configuredClaudeHomes(primaryHome) {
|
|
1891
|
+
if (process.env.CLAUDE_CONFIG_DIR) {
|
|
1892
|
+
return process.env.CLAUDE_CONFIG_DIR.split(",").map((item) => item.trim()).filter(Boolean).map((item) => path5.resolve(item));
|
|
1893
|
+
}
|
|
1894
|
+
return [primaryHome];
|
|
1895
|
+
}
|
|
1896
|
+
function collectProjectFiles(projectsPath, files, projectDirs, warnings) {
|
|
1897
|
+
if (!fs5.existsSync(projectsPath)) return;
|
|
1898
|
+
try {
|
|
1899
|
+
for (const entry of fs5.readdirSync(projectsPath, { withFileTypes: true })) {
|
|
1900
|
+
if (!entry.isDirectory()) continue;
|
|
1901
|
+
const projectDir = path5.join(projectsPath, entry.name);
|
|
1902
|
+
projectDirs.add(projectDir);
|
|
1903
|
+
walk2(projectDir, 5, (filePath) => {
|
|
1904
|
+
if (filePath.endsWith(".jsonl")) files.push({ filePath, projectDir, projectsPath });
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
} catch (error) {
|
|
1908
|
+
warnings.push(`Unable to read Claude Code projects at ${projectsPath}: ${errorMessage2(error)}`);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
function collectDesktopSessionFiles(root, files, projectDirs, warnings) {
|
|
1912
|
+
if (!fs5.existsSync(root)) return;
|
|
1913
|
+
try {
|
|
1914
|
+
walk2(root, 8, (filePath) => {
|
|
1915
|
+
if (!filePath.endsWith(".jsonl")) return;
|
|
1916
|
+
const parts = filePath.split(path5.sep);
|
|
1917
|
+
const projectsIndex = parts.lastIndexOf("projects");
|
|
1918
|
+
const hasProjectSegment = projectsIndex >= 0 && projectsIndex < parts.length - 2;
|
|
1919
|
+
const projectsPath = hasProjectSegment ? parts.slice(0, projectsIndex + 1).join(path5.sep) || path5.sep : root;
|
|
1920
|
+
const projectDir = hasProjectSegment ? parts.slice(0, projectsIndex + 2).join(path5.sep) || path5.sep : path5.dirname(filePath);
|
|
1921
|
+
projectDirs.add(projectDir);
|
|
1922
|
+
files.push({ filePath, projectDir, projectsPath });
|
|
1923
|
+
});
|
|
1924
|
+
} catch (error) {
|
|
1925
|
+
warnings.push(`Unable to read Claude Code desktop sessions at ${root}: ${errorMessage2(error)}`);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
function walk2(root, maxDepth, onFile, depth = 0) {
|
|
1929
|
+
if (depth > maxDepth) return;
|
|
1930
|
+
for (const entry of fs5.readdirSync(root, { withFileTypes: true })) {
|
|
1931
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
1932
|
+
const fullPath = path5.join(root, entry.name);
|
|
1933
|
+
if (entry.isDirectory()) {
|
|
1934
|
+
walk2(fullPath, maxDepth, onFile, depth + 1);
|
|
1935
|
+
} else if (entry.isFile()) {
|
|
1936
|
+
onFile(fullPath);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
function claudeFileToUsage(file, index, options) {
|
|
1941
|
+
const breakdown = readClaudeBreakdown(file.filePath);
|
|
1942
|
+
const id = breakdown.id ?? (path5.basename(file.filePath).replace(/\.jsonl$/i, "") || `claude-session-${index}`);
|
|
1943
|
+
const fallbackCwd = inferProjectPath(file.projectDir, file.projectsPath);
|
|
1944
|
+
const cwd = breakdown.cwd ?? fallbackCwd;
|
|
1945
|
+
const repo = resolveRepoInfo(cwd, options.config);
|
|
1946
|
+
const warnings = [...repo.warnings, ...breakdown.warnings];
|
|
1947
|
+
if (!breakdown.cwd) warnings.push("repo_inferred_from_claude_project_dir");
|
|
1948
|
+
const hasTokenBreakdown = breakdown.totalTokens > 0 && breakdown.usageCount > 0;
|
|
1949
|
+
const usage = {
|
|
1950
|
+
id,
|
|
1951
|
+
sourceClient: "claude",
|
|
1952
|
+
sourceApp: claudeSourceAppLabel(breakdown.entrypoint),
|
|
1953
|
+
sourceAppRaw: breakdown.entrypoint,
|
|
1954
|
+
sourcePath: file.filePath,
|
|
1955
|
+
repoRoot: repo.repoRoot,
|
|
1956
|
+
repoName: repo.repoName,
|
|
1957
|
+
cwd,
|
|
1958
|
+
gitRemoteUrl: repo.gitRemoteUrl,
|
|
1959
|
+
gitBranch: repo.gitBranch ?? breakdown.gitBranch,
|
|
1960
|
+
title: breakdown.title,
|
|
1961
|
+
startedAt: breakdown.startedAt,
|
|
1962
|
+
endedAt: breakdown.endedAt,
|
|
1963
|
+
model: breakdown.model,
|
|
1964
|
+
provider: "anthropic",
|
|
1965
|
+
inputTokens: breakdown.inputTokens,
|
|
1966
|
+
cachedInputTokens: breakdown.cachedInputTokens,
|
|
1967
|
+
cacheCreationInputTokens: breakdown.cacheCreationInputTokens,
|
|
1968
|
+
outputTokens: breakdown.outputTokens,
|
|
1969
|
+
reasoningTokens: 0,
|
|
1970
|
+
reasoningOutputTokens: 0,
|
|
1971
|
+
totalTokens: breakdown.totalTokens,
|
|
1972
|
+
tokenAggregationMethod: hasTokenBreakdown ? "direct_usage" : "unknown",
|
|
1973
|
+
tokenConfidence: hasTokenBreakdown ? "high" : "low",
|
|
1974
|
+
tokenSnapshotCount: breakdown.usageCount,
|
|
1975
|
+
estimatedCostUsd: void 0,
|
|
1976
|
+
messageCount: breakdown.messageCount,
|
|
1977
|
+
rawTokenTotal: hasTokenBreakdown ? breakdown.totalTokens : void 0,
|
|
1978
|
+
warnings: hasTokenBreakdown ? warnings : [...warnings, "missing_token_breakdown"],
|
|
1979
|
+
durationMs: durationMs2(breakdown.startedAt, breakdown.endedAt),
|
|
1980
|
+
rawEventCount: breakdown.rawEventCount,
|
|
1981
|
+
parseStatus: breakdown.parseStatus,
|
|
1982
|
+
parseErrors: breakdown.parseErrors,
|
|
1983
|
+
detectedSurface: claudeSurface(breakdown.entrypoint),
|
|
1984
|
+
surfaceConfidence: "medium",
|
|
1985
|
+
surfaceReason: breakdown.entrypoint ? `Claude Code entrypoint: ${breakdown.entrypoint}` : "Claude Code transcript",
|
|
1986
|
+
userPromptCount: breakdown.userPromptCount,
|
|
1987
|
+
assistantMessageCount: breakdown.assistantMessageCount,
|
|
1988
|
+
toolCallCount: breakdown.toolCallCount,
|
|
1989
|
+
shellCommandCount: breakdown.shellCommandCount,
|
|
1990
|
+
failedToolCallCount: breakdown.failedToolCallCount,
|
|
1991
|
+
nonZeroCommandEvents: 0,
|
|
1992
|
+
importantCommandFailures: 0,
|
|
1993
|
+
harmlessNonZeroEvents: 0,
|
|
1994
|
+
exploratoryMisses: 0,
|
|
1995
|
+
repeatedFailureClusters: 0,
|
|
1996
|
+
commandIssueSeverity: breakdown.failedToolCallCount > 0 ? "warning" : "none",
|
|
1997
|
+
commandIssueImpact: breakdown.failedToolCallCount > 0 ? "medium" : "none",
|
|
1998
|
+
topFailureType: breakdown.failedToolCallCount > 0 ? "unknown" : void 0,
|
|
1999
|
+
commandIssueSamples: [],
|
|
2000
|
+
fileReadCount: breakdown.fileReadCount,
|
|
2001
|
+
fileEditCount: breakdown.fileEditCount,
|
|
2002
|
+
promptTimeline: breakdown.promptTimeline,
|
|
2003
|
+
sessionOutcome: inferOutcome2(breakdown)
|
|
2004
|
+
};
|
|
2005
|
+
const cost = hasTokenBreakdown ? calculateCostUsd(usage, options.pricing) : void 0;
|
|
2006
|
+
return {
|
|
2007
|
+
...usage,
|
|
2008
|
+
estimatedCostUsd: cost,
|
|
2009
|
+
warnings: cost === void 0 ? [...usage.warnings, "unknown_pricing"] : usage.warnings
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
function readClaudeBreakdown(filePath) {
|
|
2013
|
+
const breakdown = emptyBreakdown2();
|
|
2014
|
+
try {
|
|
2015
|
+
const text = fs5.readFileSync(filePath, "utf8");
|
|
2016
|
+
const parse = parseJsonLines2(text);
|
|
2017
|
+
breakdown.rawEventCount = parse.records.length;
|
|
2018
|
+
breakdown.parseErrors = parse.errors;
|
|
2019
|
+
breakdown.parseStatus = parse.errors.length ? parse.records.length ? "partial" : "failed" : "ok";
|
|
2020
|
+
for (const record of parse.records) {
|
|
2021
|
+
applyClaudeRecord(breakdown, record);
|
|
2022
|
+
}
|
|
2023
|
+
breakdown.totalTokens = breakdown.inputTokens + breakdown.outputTokens;
|
|
2024
|
+
} catch (error) {
|
|
2025
|
+
const message = `unable_to_read_session_file:${errorMessage2(error)}`;
|
|
2026
|
+
breakdown.parseStatus = "failed";
|
|
2027
|
+
breakdown.parseErrors.push(message);
|
|
2028
|
+
breakdown.warnings.push(message);
|
|
2029
|
+
}
|
|
2030
|
+
return breakdown;
|
|
2031
|
+
}
|
|
2032
|
+
function applyClaudeRecord(breakdown, record) {
|
|
2033
|
+
const timestamp3 = dateValue2(record.timestamp);
|
|
2034
|
+
if (timestamp3) {
|
|
2035
|
+
breakdown.startedAt = minDate(breakdown.startedAt, timestamp3);
|
|
2036
|
+
breakdown.endedAt = maxDate(breakdown.endedAt, timestamp3);
|
|
2037
|
+
}
|
|
2038
|
+
breakdown.id ??= stringValue3(record.sessionId);
|
|
2039
|
+
breakdown.cwd ??= stringValue3(record.cwd);
|
|
2040
|
+
breakdown.gitBranch ??= stringValue3(record.gitBranch);
|
|
2041
|
+
breakdown.entrypoint ??= stringValue3(record.entrypoint);
|
|
2042
|
+
breakdown.version ??= stringValue3(record.version);
|
|
2043
|
+
breakdown.title ??= stringValue3(record.aiTitle);
|
|
2044
|
+
const message = firstObject4(record.message);
|
|
2045
|
+
const role = stringValue3(message?.role) ?? stringValue3(record.type);
|
|
2046
|
+
if (role === "user") breakdown.userPromptCount += 1;
|
|
2047
|
+
if (role === "assistant") breakdown.assistantMessageCount += 1;
|
|
2048
|
+
if (role === "user" || role === "assistant") breakdown.messageCount += 1;
|
|
2049
|
+
if (message) {
|
|
2050
|
+
applyPromptTimeline2(breakdown, role, message.content, timestamp3);
|
|
2051
|
+
const usage = firstObject4(message.usage);
|
|
2052
|
+
const usageTokens = usage ? usageTotal(usage) : 0;
|
|
2053
|
+
const model = normalizeClaudeModel(message.model);
|
|
2054
|
+
if (model && (usageTokens > 0 || !breakdown.model)) breakdown.model = model;
|
|
2055
|
+
if (usage && usageTokens > 0) applyUsage(breakdown, usage);
|
|
2056
|
+
applyContentSignals(breakdown, message.content);
|
|
2057
|
+
}
|
|
2058
|
+
if (looksFailedToolResult(record.toolUseResult)) breakdown.failedToolCallCount += 1;
|
|
2059
|
+
}
|
|
2060
|
+
function applyUsage(breakdown, usage) {
|
|
2061
|
+
const input = numberValue(usage.input_tokens);
|
|
2062
|
+
const cacheRead = numberValue(usage.cache_read_input_tokens);
|
|
2063
|
+
const cacheCreation = numberValue(usage.cache_creation_input_tokens);
|
|
2064
|
+
const output = numberValue(usage.output_tokens);
|
|
2065
|
+
breakdown.inputTokens += input + cacheRead + cacheCreation;
|
|
2066
|
+
breakdown.cachedInputTokens += cacheRead;
|
|
2067
|
+
breakdown.cacheCreationInputTokens += cacheCreation;
|
|
2068
|
+
breakdown.outputTokens += output;
|
|
2069
|
+
breakdown.usageCount += 1;
|
|
2070
|
+
}
|
|
2071
|
+
function usageTotal(usage) {
|
|
2072
|
+
return numberValue(usage.input_tokens) + numberValue(usage.cache_read_input_tokens) + numberValue(usage.cache_creation_input_tokens) + numberValue(usage.output_tokens);
|
|
2073
|
+
}
|
|
2074
|
+
function applyContentSignals(breakdown, content) {
|
|
2075
|
+
if (!Array.isArray(content)) return;
|
|
2076
|
+
for (const item of content) {
|
|
2077
|
+
const object = firstObject4(item);
|
|
2078
|
+
if (!object || object.type !== "tool_use") continue;
|
|
2079
|
+
breakdown.toolCallCount += 1;
|
|
2080
|
+
const name = stringValue3(object.name)?.toLowerCase() ?? "";
|
|
2081
|
+
if (name === "bash") breakdown.shellCommandCount += 1;
|
|
2082
|
+
if (name === "read") breakdown.fileReadCount += 1;
|
|
2083
|
+
if (["edit", "multiedit", "write", "notebookedit"].includes(name)) breakdown.fileEditCount += 1;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
function parseJsonLines2(text) {
|
|
2087
|
+
const records = [];
|
|
2088
|
+
const errors = [];
|
|
2089
|
+
text.split(/\r?\n/).map((line, index) => ({ line: line.trim(), index })).filter(({ line }) => Boolean(line)).forEach(({ line, index }) => {
|
|
2090
|
+
try {
|
|
2091
|
+
const parsed = JSON.parse(line);
|
|
2092
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2093
|
+
records.push(parsed);
|
|
2094
|
+
}
|
|
2095
|
+
} catch (error) {
|
|
2096
|
+
errors.push(`jsonl_line_${index + 1}:${errorMessage2(error)}`);
|
|
2097
|
+
}
|
|
2098
|
+
});
|
|
2099
|
+
return { records, errors };
|
|
2100
|
+
}
|
|
2101
|
+
function emptyBreakdown2() {
|
|
2102
|
+
return {
|
|
2103
|
+
id: void 0,
|
|
2104
|
+
title: void 0,
|
|
2105
|
+
cwd: void 0,
|
|
2106
|
+
gitBranch: void 0,
|
|
2107
|
+
entrypoint: void 0,
|
|
2108
|
+
version: void 0,
|
|
2109
|
+
startedAt: void 0,
|
|
2110
|
+
endedAt: void 0,
|
|
2111
|
+
model: void 0,
|
|
2112
|
+
inputTokens: 0,
|
|
2113
|
+
cachedInputTokens: 0,
|
|
2114
|
+
cacheCreationInputTokens: 0,
|
|
2115
|
+
outputTokens: 0,
|
|
2116
|
+
totalTokens: 0,
|
|
2117
|
+
usageCount: 0,
|
|
2118
|
+
messageCount: 0,
|
|
2119
|
+
userPromptCount: 0,
|
|
2120
|
+
assistantMessageCount: 0,
|
|
2121
|
+
toolCallCount: 0,
|
|
2122
|
+
shellCommandCount: 0,
|
|
2123
|
+
failedToolCallCount: 0,
|
|
2124
|
+
fileReadCount: 0,
|
|
2125
|
+
fileEditCount: 0,
|
|
2126
|
+
rawEventCount: 0,
|
|
2127
|
+
parseStatus: "ok",
|
|
2128
|
+
parseErrors: [],
|
|
2129
|
+
promptTimeline: [],
|
|
2130
|
+
warnings: []
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
function applyPromptTimeline2(breakdown, role, content, timestamp3) {
|
|
2134
|
+
if (breakdown.promptTimeline.length >= 80 || role !== "user" && role !== "assistant") return;
|
|
2135
|
+
const text = extractClaudeMessageText(content);
|
|
2136
|
+
if (!text) return;
|
|
2137
|
+
breakdown.promptTimeline.push({
|
|
2138
|
+
role,
|
|
2139
|
+
text: normalizeTimelineText2(text),
|
|
2140
|
+
timestamp: timestamp3
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
function extractClaudeMessageText(content) {
|
|
2144
|
+
if (typeof content === "string" && content.trim()) return content;
|
|
2145
|
+
if (!Array.isArray(content)) return void 0;
|
|
2146
|
+
const parts = content.flatMap((item) => {
|
|
2147
|
+
if (typeof item === "string") return [item];
|
|
2148
|
+
const object = firstObject4(item);
|
|
2149
|
+
if (!object || object.type !== "text") return [];
|
|
2150
|
+
const text = stringValue3(object.text);
|
|
2151
|
+
return text ? [text] : [];
|
|
2152
|
+
}).filter((item) => item.trim());
|
|
2153
|
+
return parts.length ? parts.join("\n") : void 0;
|
|
2154
|
+
}
|
|
2155
|
+
function normalizeTimelineText2(text) {
|
|
2156
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 1e3);
|
|
2157
|
+
}
|
|
2158
|
+
function inferProjectPath(projectDir, projectsPath) {
|
|
2159
|
+
const name = path5.basename(projectDir);
|
|
2160
|
+
if (name.startsWith("-")) {
|
|
2161
|
+
return `${path5.sep}${name.slice(1).split("-").filter(Boolean).join(path5.sep)}`;
|
|
2162
|
+
}
|
|
2163
|
+
const relative = path5.relative(projectsPath, projectDir);
|
|
2164
|
+
return relative && !relative.startsWith("..") ? path5.join(projectsPath, relative) : projectDir;
|
|
2165
|
+
}
|
|
2166
|
+
function inferOutcome2(breakdown) {
|
|
2167
|
+
if (breakdown.parseStatus === "failed") return "failed";
|
|
2168
|
+
if (breakdown.failedToolCallCount > 0) return "partial";
|
|
2169
|
+
if (breakdown.fileEditCount > 0) return "completed";
|
|
2170
|
+
if (breakdown.shellCommandCount > 0) return "setup_debugging";
|
|
2171
|
+
if (breakdown.userPromptCount > 0 || breakdown.assistantMessageCount > 0) return "research_only";
|
|
2172
|
+
return "unknown";
|
|
2173
|
+
}
|
|
2174
|
+
function claudeSurface(entrypoint) {
|
|
2175
|
+
const value = entrypoint?.toLowerCase() ?? "";
|
|
2176
|
+
if (value.includes("vscode")) return "vscode_extension";
|
|
2177
|
+
if (value.includes("local-agent")) return "local_agent";
|
|
2178
|
+
return "terminal_cli";
|
|
2179
|
+
}
|
|
2180
|
+
function claudeSourceAppLabel(entrypoint) {
|
|
2181
|
+
const value = entrypoint?.toLowerCase() ?? "";
|
|
2182
|
+
if (value.includes("vscode")) return "VS Code";
|
|
2183
|
+
if (value.includes("local-agent")) return "Claude desktop local agent";
|
|
2184
|
+
if (value === "cli" || value.includes("terminal")) return "Terminal";
|
|
2185
|
+
return "Claude Code";
|
|
2186
|
+
}
|
|
2187
|
+
function defaultClaudeHome() {
|
|
2188
|
+
return process.env.CLAUDE_CONFIG_DIR?.split(",").map((item) => item.trim()).filter(Boolean)[0] ?? path5.join(os3.homedir(), ".claude");
|
|
2189
|
+
}
|
|
2190
|
+
function defaultDesktopSessionRoots() {
|
|
2191
|
+
const home = os3.homedir();
|
|
2192
|
+
const xdg = process.env.XDG_CONFIG_HOME || path5.join(home, ".config");
|
|
2193
|
+
return [
|
|
2194
|
+
path5.join(home, "Library", "Application Support", "Claude", "local-agent-mode-sessions"),
|
|
2195
|
+
path5.join(xdg, "Claude", "local-agent-mode-sessions")
|
|
2196
|
+
];
|
|
2197
|
+
}
|
|
2198
|
+
function uniqueSessionFiles(files) {
|
|
2199
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2200
|
+
return files.filter((file) => {
|
|
2201
|
+
const key = path5.resolve(file.filePath);
|
|
2202
|
+
if (seen.has(key)) return false;
|
|
2203
|
+
seen.add(key);
|
|
2204
|
+
return true;
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
function unique3(values) {
|
|
2208
|
+
return [...new Set(values)];
|
|
2209
|
+
}
|
|
2210
|
+
function countHistoryEntries(historyPath) {
|
|
2211
|
+
if (!fs5.existsSync(historyPath)) return 0;
|
|
2212
|
+
let entryCount = 0;
|
|
2213
|
+
try {
|
|
2214
|
+
for (const line of fs5.readFileSync(historyPath, "utf8").split(/\r?\n/).filter(Boolean)) {
|
|
2215
|
+
try {
|
|
2216
|
+
JSON.parse(line);
|
|
2217
|
+
entryCount += 1;
|
|
2218
|
+
} catch {
|
|
2219
|
+
continue;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
} catch {
|
|
2223
|
+
return entryCount;
|
|
2224
|
+
}
|
|
2225
|
+
return entryCount;
|
|
2226
|
+
}
|
|
2227
|
+
function firstObject4(value) {
|
|
2228
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2229
|
+
}
|
|
2230
|
+
function stringValue3(value) {
|
|
2231
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2232
|
+
}
|
|
2233
|
+
function numberValue(value) {
|
|
2234
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2235
|
+
if (typeof value === "string" && Number.isFinite(Number(value))) return Number(value);
|
|
2236
|
+
return 0;
|
|
2237
|
+
}
|
|
2238
|
+
function normalizeClaudeModel(value) {
|
|
2239
|
+
const model = stringValue3(value);
|
|
2240
|
+
if (!model || model === "<synthetic>") return void 0;
|
|
2241
|
+
return model;
|
|
2242
|
+
}
|
|
2243
|
+
function dateValue2(value) {
|
|
2244
|
+
if (typeof value !== "string") return void 0;
|
|
2245
|
+
const date = new Date(value);
|
|
2246
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
2247
|
+
}
|
|
2248
|
+
function minDate(current, candidate) {
|
|
2249
|
+
return !current || candidate < current ? candidate : current;
|
|
2250
|
+
}
|
|
2251
|
+
function maxDate(current, candidate) {
|
|
2252
|
+
return !current || candidate > current ? candidate : current;
|
|
2253
|
+
}
|
|
2254
|
+
function durationMs2(startedAt, endedAt) {
|
|
2255
|
+
if (!startedAt || !endedAt) return void 0;
|
|
2256
|
+
const start = new Date(startedAt).getTime();
|
|
2257
|
+
const end = new Date(endedAt).getTime();
|
|
2258
|
+
return Number.isFinite(start) && Number.isFinite(end) && end >= start ? end - start : void 0;
|
|
2259
|
+
}
|
|
2260
|
+
function looksFailedToolResult(value) {
|
|
2261
|
+
const object = firstObject4(value);
|
|
2262
|
+
if (!object) return false;
|
|
2263
|
+
if (object.is_error === true || object.error === true) return true;
|
|
2264
|
+
const exitCode = numberValue(object.exit_code ?? object.exitCode ?? object.status_code ?? object.statusCode);
|
|
2265
|
+
if (exitCode !== 0) return true;
|
|
2266
|
+
const status = stringValue3(object.status)?.toLowerCase();
|
|
2267
|
+
return status === "failed" || status === "error";
|
|
2268
|
+
}
|
|
2269
|
+
function errorMessage2(error) {
|
|
2270
|
+
return error instanceof Error ? error.message : String(error);
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
// packages/core/src/sources/index.ts
|
|
2274
|
+
function scanUsageSources(options) {
|
|
2275
|
+
const codex = scanCodex({
|
|
2276
|
+
...options.codexHome ? { codexHome: options.codexHome } : {},
|
|
2277
|
+
...options.config ? { config: options.config } : {},
|
|
2278
|
+
pricing: options.pricing
|
|
2279
|
+
});
|
|
2280
|
+
const claude = scanClaude({
|
|
2281
|
+
...options.claudeHome ? { claudeHome: options.claudeHome } : {},
|
|
2282
|
+
...options.config ? { config: options.config } : {},
|
|
2283
|
+
pricing: options.pricing
|
|
2284
|
+
});
|
|
2285
|
+
return {
|
|
2286
|
+
sources: [codex.source, claude.source],
|
|
2287
|
+
sourceStats: [codex.stats, claude.stats],
|
|
2288
|
+
sessions: [...codex.sessions, ...claude.sessions]
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
// apps/server/src/data.ts
|
|
2293
|
+
import fs6 from "node:fs";
|
|
1709
2294
|
var scanCacheTtlMs = 1e4;
|
|
1710
2295
|
var scanCache;
|
|
1711
2296
|
function readDashboardData(filters = {}) {
|
|
@@ -1722,11 +2307,11 @@ function readRawUsageData() {
|
|
|
1722
2307
|
}
|
|
1723
2308
|
const { config, warnings: configWarnings } = loadConfigWithWarnings();
|
|
1724
2309
|
const pricing = loadPricingTable(resolvePricingPath(config));
|
|
1725
|
-
const
|
|
2310
|
+
const scan = scanUsageSources({ config, pricing });
|
|
1726
2311
|
const data = {
|
|
1727
|
-
sources:
|
|
1728
|
-
sourceStats:
|
|
1729
|
-
sessions:
|
|
2312
|
+
sources: scan.sources.map((source) => ({ ...source, warnings: [...source.warnings, ...configWarnings] })),
|
|
2313
|
+
sourceStats: scan.sourceStats,
|
|
2314
|
+
sessions: scan.sessions
|
|
1730
2315
|
};
|
|
1731
2316
|
scanCache = { cwd, data, expiresAt: Date.now() + scanCacheTtlMs };
|
|
1732
2317
|
return data;
|
|
@@ -1767,8 +2352,8 @@ function writePricingData(models) {
|
|
|
1767
2352
|
}
|
|
1768
2353
|
function clearRepoSpendLocalData() {
|
|
1769
2354
|
const target = repospendHome();
|
|
1770
|
-
const removed =
|
|
1771
|
-
|
|
2355
|
+
const removed = fs6.existsSync(target);
|
|
2356
|
+
fs6.rmSync(target, { recursive: true, force: true });
|
|
1772
2357
|
clearScanCache();
|
|
1773
2358
|
return { path: target, removed };
|
|
1774
2359
|
}
|
|
@@ -1791,12 +2376,621 @@ function runtimeCwd() {
|
|
|
1791
2376
|
}
|
|
1792
2377
|
|
|
1793
2378
|
// apps/server/src/server.ts
|
|
1794
|
-
import
|
|
1795
|
-
import
|
|
1796
|
-
import { fileURLToPath } from "node:url";
|
|
2379
|
+
import fs7 from "node:fs";
|
|
2380
|
+
import path6 from "node:path";
|
|
2381
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1797
2382
|
import fastifyStatic from "@fastify/static";
|
|
1798
2383
|
import Fastify from "fastify";
|
|
1799
2384
|
|
|
2385
|
+
// apps/server/src/demo-data.ts
|
|
2386
|
+
var baseDate = "2026-05-18";
|
|
2387
|
+
var demoPricing = {
|
|
2388
|
+
...defaultPricing,
|
|
2389
|
+
"gpt-5.3-codex": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
|
|
2390
|
+
"gpt-5.3-codex-spark": { inputPerMillion: 0.75, cachedInputPerMillion: 0.075, outputPerMillion: 4.5, reasoningOutputPerMillion: 4.5 },
|
|
2391
|
+
"claude-haiku-4-5-20251001": { inputPerMillion: 1, cacheCreationInputPerMillion: 1.25, cachedInputPerMillion: 0.1, outputPerMillion: 5 }
|
|
2392
|
+
};
|
|
2393
|
+
function demoModeEnabled() {
|
|
2394
|
+
return process.env.REPOSPEND_DEMO_DATA === "lotr";
|
|
2395
|
+
}
|
|
2396
|
+
function readDemoDashboardData(filters = {}) {
|
|
2397
|
+
return buildDashboardSnapshot({
|
|
2398
|
+
sources: demoSources,
|
|
2399
|
+
sourceStats: demoSourceStats,
|
|
2400
|
+
sessions: demoSessions,
|
|
2401
|
+
filters
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
function readDemoRtkGain() {
|
|
2405
|
+
return {
|
|
2406
|
+
available: true,
|
|
2407
|
+
rtkDetected: true,
|
|
2408
|
+
rtkVersion: "rtk-demo 1.0.0",
|
|
2409
|
+
rtkCodexHookStatus: "active",
|
|
2410
|
+
rtkLastActivityAt: "2026-05-18T18:42:00.000Z",
|
|
2411
|
+
rtkDataSource: "fictional demo data",
|
|
2412
|
+
totalCommands: 248,
|
|
2413
|
+
inputTokens: "18.4M",
|
|
2414
|
+
outputTokens: "2.6M",
|
|
2415
|
+
tokensSaved: "5.8M",
|
|
2416
|
+
savedPercent: 23.9,
|
|
2417
|
+
totalExecTime: "41m",
|
|
2418
|
+
averageExecTime: "9.9s",
|
|
2419
|
+
rtkTokensSaved: "5.8M",
|
|
2420
|
+
rtkSavingsRate: 23.9,
|
|
2421
|
+
rtkCommandsProcessed: 248,
|
|
2422
|
+
rtkAverageCommandRuntime: "9.9s",
|
|
2423
|
+
rtkTopSavingsCommands: [
|
|
2424
|
+
{ command: "rg", count: 74, saved: "1.9M", averageSavedPercent: 31.2, time: "8m" },
|
|
2425
|
+
{ command: "pnpm test", count: 18, saved: "840K", averageSavedPercent: 17.4, time: "12m" },
|
|
2426
|
+
{ command: "git diff", count: 39, saved: "620K", averageSavedPercent: 26.8, time: "2m" }
|
|
2427
|
+
],
|
|
2428
|
+
rtkRecentActivity: [
|
|
2429
|
+
"rg council-of-elrond planner",
|
|
2430
|
+
"pnpm test --filter gondor-api",
|
|
2431
|
+
"git diff -- shire-mobile"
|
|
2432
|
+
],
|
|
2433
|
+
rtkDiscoverAvailable: true,
|
|
2434
|
+
rtkDiscoverError: void 0,
|
|
2435
|
+
rtkDiscoverSummary: "Total: 248 fictional command events scanned.",
|
|
2436
|
+
rtkCoverageGaps: [
|
|
2437
|
+
{ command: "cat", count: 21, rtkEquivalent: "rtk sed", status: "easy win", estimatedSavings: "410K" },
|
|
2438
|
+
{ command: "grep", count: 13, rtkEquivalent: "rtk rg", status: "easy win", estimatedSavings: "360K" }
|
|
2439
|
+
],
|
|
2440
|
+
rtkUnhandledCommands: [
|
|
2441
|
+
{ command: "palantirctl inspect", count: 5, example: "palantirctl inspect eye-of-sauron-alerts" }
|
|
2442
|
+
],
|
|
2443
|
+
topCommands: [
|
|
2444
|
+
{ command: "rg", count: 74, saved: "1.9M", averageSavedPercent: 31.2, time: "8m" },
|
|
2445
|
+
{ command: "git", count: 46, saved: "720K", averageSavedPercent: 18.3, time: "3m" },
|
|
2446
|
+
{ command: "pnpm", count: 31, saved: "1.1M", averageSavedPercent: 16.8, time: "17m" }
|
|
2447
|
+
],
|
|
2448
|
+
recentCommands: [
|
|
2449
|
+
"rtk rg auth gates-of-moria",
|
|
2450
|
+
"rtk pnpm test --filter palantir-observability",
|
|
2451
|
+
"rtk git diff -- one-ring-infra"
|
|
2452
|
+
],
|
|
2453
|
+
raw: "RepoSpend LOTR demo RTK output",
|
|
2454
|
+
error: void 0
|
|
2455
|
+
};
|
|
2456
|
+
}
|
|
2457
|
+
var demoSources = [
|
|
2458
|
+
{
|
|
2459
|
+
id: "codex",
|
|
2460
|
+
label: "Codex",
|
|
2461
|
+
available: true,
|
|
2462
|
+
paths: ["/demo/middle-earth/codex/state.sqlite", "/demo/middle-earth/codex/sessions"],
|
|
2463
|
+
warnings: []
|
|
2464
|
+
},
|
|
2465
|
+
{
|
|
2466
|
+
id: "claude",
|
|
2467
|
+
label: "Claude Code",
|
|
2468
|
+
available: true,
|
|
2469
|
+
paths: ["/demo/middle-earth/claude/projects"],
|
|
2470
|
+
warnings: []
|
|
2471
|
+
}
|
|
2472
|
+
];
|
|
2473
|
+
var demoSourceStats = [
|
|
2474
|
+
{
|
|
2475
|
+
sourceId: "codex",
|
|
2476
|
+
sourceLabel: "Codex",
|
|
2477
|
+
homePath: "/demo/middle-earth/codex",
|
|
2478
|
+
statePath: "/demo/middle-earth/codex/state.sqlite",
|
|
2479
|
+
sessionsPath: "/demo/middle-earth/codex/sessions",
|
|
2480
|
+
stateExists: true,
|
|
2481
|
+
sessionsExists: true,
|
|
2482
|
+
sessionFileCount: 15,
|
|
2483
|
+
sessionsImported: 15,
|
|
2484
|
+
parseFailureCount: 0,
|
|
2485
|
+
unreadableFileCount: 0,
|
|
2486
|
+
malformedFileCount: 0,
|
|
2487
|
+
lastScannedAt: "2026-05-18T19:00:00.000Z"
|
|
2488
|
+
},
|
|
2489
|
+
{
|
|
2490
|
+
sourceId: "claude",
|
|
2491
|
+
sourceLabel: "Claude Code",
|
|
2492
|
+
homePath: "/demo/middle-earth/claude",
|
|
2493
|
+
projectsPath: "/demo/middle-earth/claude/projects",
|
|
2494
|
+
projectsExists: true,
|
|
2495
|
+
sessionFileCount: 7,
|
|
2496
|
+
sessionsImported: 7,
|
|
2497
|
+
parseFailureCount: 0,
|
|
2498
|
+
unreadableFileCount: 0,
|
|
2499
|
+
malformedFileCount: 0,
|
|
2500
|
+
projectDirCount: 4,
|
|
2501
|
+
lastScannedAt: "2026-05-18T19:01:00.000Z"
|
|
2502
|
+
}
|
|
2503
|
+
];
|
|
2504
|
+
var demoInputs = [
|
|
2505
|
+
{
|
|
2506
|
+
id: "lotr-session-001",
|
|
2507
|
+
repo: "one-ring-infra",
|
|
2508
|
+
title: "Why is one-ring-infra spending 3x more than shire-mobile?",
|
|
2509
|
+
sourceClient: "codex",
|
|
2510
|
+
sourceApp: "Codex app",
|
|
2511
|
+
model: "GPT-5.5",
|
|
2512
|
+
provider: "openai",
|
|
2513
|
+
hour: 9,
|
|
2514
|
+
minute: 10,
|
|
2515
|
+
durationMinutes: 84,
|
|
2516
|
+
inputTokens: 285e4,
|
|
2517
|
+
cachedInputTokens: 32e4,
|
|
2518
|
+
outputTokens: 41e4,
|
|
2519
|
+
reasoningTokens: 26e4,
|
|
2520
|
+
prompts: 9,
|
|
2521
|
+
replies: 18,
|
|
2522
|
+
tools: 86,
|
|
2523
|
+
commands: 34,
|
|
2524
|
+
edits: 3,
|
|
2525
|
+
reads: 92,
|
|
2526
|
+
outcome: "completed",
|
|
2527
|
+
branch: "audit/token-fire",
|
|
2528
|
+
importantFailures: 2,
|
|
2529
|
+
harmlessNonZero: 4,
|
|
2530
|
+
repeatedClusters: 1,
|
|
2531
|
+
topFailureType: "test",
|
|
2532
|
+
warnings: ["repeated_error:test-suite"],
|
|
2533
|
+
issueSamples: [
|
|
2534
|
+
issue("pnpm test --filter one-ring-infra", "test", "warning", "medium", "Repeated failing shard while auditing token spend."),
|
|
2535
|
+
issue("terraform plan -var region=mordor", "deploy", "critical", "high", "Infrastructure plan failed before the safer dry-run path was found.")
|
|
2536
|
+
]
|
|
2537
|
+
},
|
|
2538
|
+
{
|
|
2539
|
+
id: "lotr-session-002",
|
|
2540
|
+
repo: "mordor-ci",
|
|
2541
|
+
title: "Reduce token burn in mordor-ci",
|
|
2542
|
+
sourceClient: "codex",
|
|
2543
|
+
sourceApp: "Codex app",
|
|
2544
|
+
model: "GPT-5.4",
|
|
2545
|
+
provider: "openai",
|
|
2546
|
+
hour: 10,
|
|
2547
|
+
minute: 35,
|
|
2548
|
+
durationMinutes: 62,
|
|
2549
|
+
inputTokens: 178e4,
|
|
2550
|
+
cachedInputTokens: 95e3,
|
|
2551
|
+
outputTokens: 28e4,
|
|
2552
|
+
reasoningTokens: 185e3,
|
|
2553
|
+
prompts: 7,
|
|
2554
|
+
replies: 12,
|
|
2555
|
+
tools: 58,
|
|
2556
|
+
commands: 28,
|
|
2557
|
+
edits: 2,
|
|
2558
|
+
reads: 51,
|
|
2559
|
+
outcome: "partial",
|
|
2560
|
+
branch: "ci/doom-cache",
|
|
2561
|
+
importantFailures: 3,
|
|
2562
|
+
harmlessNonZero: 1,
|
|
2563
|
+
repeatedClusters: 2,
|
|
2564
|
+
topFailureType: "build",
|
|
2565
|
+
warnings: ["low_cache_rate"],
|
|
2566
|
+
issueSamples: [
|
|
2567
|
+
issue("pnpm build --filter mordor-ci", "build", "critical", "high", "Build failed repeatedly after the Mount Doom runner image changed.")
|
|
2568
|
+
]
|
|
2569
|
+
},
|
|
2570
|
+
{
|
|
2571
|
+
id: "lotr-session-003",
|
|
2572
|
+
repo: "shire-mobile",
|
|
2573
|
+
title: "Investigate token spike after second breakfast",
|
|
2574
|
+
sourceClient: "claude",
|
|
2575
|
+
sourceApp: "Claude Code",
|
|
2576
|
+
sourceAppRaw: "claude-code",
|
|
2577
|
+
model: "claude-opus-4-7",
|
|
2578
|
+
provider: "anthropic",
|
|
2579
|
+
hour: 11,
|
|
2580
|
+
minute: 5,
|
|
2581
|
+
durationMinutes: 46,
|
|
2582
|
+
inputTokens: 142e4,
|
|
2583
|
+
cachedInputTokens: 21e4,
|
|
2584
|
+
outputTokens: 185e3,
|
|
2585
|
+
reasoningTokens: 0,
|
|
2586
|
+
prompts: 5,
|
|
2587
|
+
replies: 9,
|
|
2588
|
+
tools: 31,
|
|
2589
|
+
commands: 12,
|
|
2590
|
+
edits: 0,
|
|
2591
|
+
reads: 44,
|
|
2592
|
+
outcome: "research_only",
|
|
2593
|
+
branch: "analytics/second-breakfast",
|
|
2594
|
+
warnings: [],
|
|
2595
|
+
detectedSurface: "terminal_cli",
|
|
2596
|
+
tokenConfidence: "high"
|
|
2597
|
+
},
|
|
2598
|
+
{
|
|
2599
|
+
id: "lotr-session-004",
|
|
2600
|
+
repo: "palantir-observability",
|
|
2601
|
+
title: "Fix race condition in the palantir event stream",
|
|
2602
|
+
sourceClient: "codex",
|
|
2603
|
+
sourceApp: "VS Code",
|
|
2604
|
+
sourceAppRaw: "vscode",
|
|
2605
|
+
model: "GPT-5.4",
|
|
2606
|
+
provider: "openai",
|
|
2607
|
+
hour: 12,
|
|
2608
|
+
minute: 20,
|
|
2609
|
+
durationMinutes: 73,
|
|
2610
|
+
inputTokens: 126e4,
|
|
2611
|
+
cachedInputTokens: 455e3,
|
|
2612
|
+
outputTokens: 21e4,
|
|
2613
|
+
reasoningTokens: 17e4,
|
|
2614
|
+
prompts: 6,
|
|
2615
|
+
replies: 14,
|
|
2616
|
+
tools: 64,
|
|
2617
|
+
commands: 24,
|
|
2618
|
+
edits: 6,
|
|
2619
|
+
reads: 66,
|
|
2620
|
+
outcome: "completed",
|
|
2621
|
+
branch: "stream/palantir-race",
|
|
2622
|
+
importantFailures: 1,
|
|
2623
|
+
harmlessNonZero: 3,
|
|
2624
|
+
topFailureType: "test",
|
|
2625
|
+
issueSamples: [
|
|
2626
|
+
issue("pnpm test palantir-stream.spec.ts", "test", "warning", "medium", "One flaky stream-order assertion needed a deterministic clock.")
|
|
2627
|
+
]
|
|
2628
|
+
},
|
|
2629
|
+
{
|
|
2630
|
+
id: "lotr-session-005",
|
|
2631
|
+
repo: "gondor-api",
|
|
2632
|
+
title: "Add auth flow for gates-of-moria",
|
|
2633
|
+
sourceClient: "codex",
|
|
2634
|
+
sourceApp: "Codex app",
|
|
2635
|
+
model: "GPT-5.4-Mini",
|
|
2636
|
+
provider: "openai",
|
|
2637
|
+
hour: 13,
|
|
2638
|
+
minute: 0,
|
|
2639
|
+
durationMinutes: 58,
|
|
2640
|
+
inputTokens: 86e4,
|
|
2641
|
+
cachedInputTokens: 28e4,
|
|
2642
|
+
outputTokens: 13e4,
|
|
2643
|
+
reasoningTokens: 74e3,
|
|
2644
|
+
prompts: 5,
|
|
2645
|
+
replies: 10,
|
|
2646
|
+
tools: 42,
|
|
2647
|
+
commands: 18,
|
|
2648
|
+
edits: 8,
|
|
2649
|
+
reads: 39,
|
|
2650
|
+
outcome: "completed",
|
|
2651
|
+
branch: "auth/mellon"
|
|
2652
|
+
},
|
|
2653
|
+
{
|
|
2654
|
+
id: "lotr-session-006",
|
|
2655
|
+
repo: "rivendell-dashboard",
|
|
2656
|
+
title: "Refactor the council-of-elrond planner",
|
|
2657
|
+
sourceClient: "claude",
|
|
2658
|
+
sourceApp: "Claude desktop local agent",
|
|
2659
|
+
sourceAppRaw: "claude-desktop-local-agent",
|
|
2660
|
+
model: "claude-sonnet-4-6",
|
|
2661
|
+
provider: "anthropic",
|
|
2662
|
+
hour: 14,
|
|
2663
|
+
minute: 15,
|
|
2664
|
+
durationMinutes: 67,
|
|
2665
|
+
inputTokens: 92e4,
|
|
2666
|
+
cachedInputTokens: 36e4,
|
|
2667
|
+
outputTokens: 15e4,
|
|
2668
|
+
reasoningTokens: 0,
|
|
2669
|
+
prompts: 6,
|
|
2670
|
+
replies: 11,
|
|
2671
|
+
tools: 37,
|
|
2672
|
+
commands: 15,
|
|
2673
|
+
edits: 7,
|
|
2674
|
+
reads: 48,
|
|
2675
|
+
outcome: "completed",
|
|
2676
|
+
branch: "planner/elrond-v2"
|
|
2677
|
+
},
|
|
2678
|
+
{
|
|
2679
|
+
id: "lotr-session-007",
|
|
2680
|
+
repo: "palantir-observability",
|
|
2681
|
+
title: "Optimize observability for eye-of-sauron alerts",
|
|
2682
|
+
sourceClient: "codex",
|
|
2683
|
+
sourceApp: "Codex app",
|
|
2684
|
+
model: "GPT-5.3-Codex",
|
|
2685
|
+
provider: "openai",
|
|
2686
|
+
hour: 15,
|
|
2687
|
+
minute: 40,
|
|
2688
|
+
durationMinutes: 51,
|
|
2689
|
+
inputTokens: 74e4,
|
|
2690
|
+
cachedInputTokens: 29e4,
|
|
2691
|
+
outputTokens: 96e3,
|
|
2692
|
+
reasoningTokens: 88e3,
|
|
2693
|
+
prompts: 4,
|
|
2694
|
+
replies: 8,
|
|
2695
|
+
tools: 44,
|
|
2696
|
+
commands: 17,
|
|
2697
|
+
edits: 5,
|
|
2698
|
+
reads: 36,
|
|
2699
|
+
outcome: "completed",
|
|
2700
|
+
branch: "alerts/eye-slo",
|
|
2701
|
+
harmlessNonZero: 2,
|
|
2702
|
+
exploratoryMisses: 3
|
|
2703
|
+
},
|
|
2704
|
+
{
|
|
2705
|
+
id: "lotr-session-008",
|
|
2706
|
+
repo: "mordor-ci",
|
|
2707
|
+
title: "Clean up dead code in orc-deployment-worker",
|
|
2708
|
+
sourceClient: "codex",
|
|
2709
|
+
sourceApp: "Terminal",
|
|
2710
|
+
sourceAppRaw: "terminal",
|
|
2711
|
+
model: "GPT-5.3-Codex-Spark",
|
|
2712
|
+
provider: "openai",
|
|
2713
|
+
hour: 16,
|
|
2714
|
+
minute: 10,
|
|
2715
|
+
durationMinutes: 39,
|
|
2716
|
+
inputTokens: 54e4,
|
|
2717
|
+
cachedInputTokens: 18e4,
|
|
2718
|
+
outputTokens: 84e3,
|
|
2719
|
+
reasoningTokens: 64e3,
|
|
2720
|
+
prompts: 3,
|
|
2721
|
+
replies: 7,
|
|
2722
|
+
tools: 35,
|
|
2723
|
+
commands: 14,
|
|
2724
|
+
edits: 9,
|
|
2725
|
+
reads: 31,
|
|
2726
|
+
outcome: "completed",
|
|
2727
|
+
branch: "workers/orc-cleanup",
|
|
2728
|
+
detectedSurface: "terminal_cli"
|
|
2729
|
+
},
|
|
2730
|
+
{
|
|
2731
|
+
id: "lotr-session-009",
|
|
2732
|
+
repo: "minas-tirith-admin",
|
|
2733
|
+
title: "Tighten city-gate roles before the beacons launch",
|
|
2734
|
+
sourceClient: "claude",
|
|
2735
|
+
sourceApp: "Claude Code",
|
|
2736
|
+
sourceAppRaw: "claude-code",
|
|
2737
|
+
model: "claude-haiku-4-5-20251001",
|
|
2738
|
+
provider: "anthropic",
|
|
2739
|
+
hour: 17,
|
|
2740
|
+
minute: 5,
|
|
2741
|
+
durationMinutes: 32,
|
|
2742
|
+
inputTokens: 39e4,
|
|
2743
|
+
cachedInputTokens: 12e4,
|
|
2744
|
+
outputTokens: 61e3,
|
|
2745
|
+
reasoningTokens: 0,
|
|
2746
|
+
prompts: 4,
|
|
2747
|
+
replies: 6,
|
|
2748
|
+
tools: 25,
|
|
2749
|
+
commands: 9,
|
|
2750
|
+
edits: 5,
|
|
2751
|
+
reads: 23,
|
|
2752
|
+
outcome: "completed",
|
|
2753
|
+
branch: "rbac/beacons"
|
|
2754
|
+
},
|
|
2755
|
+
{
|
|
2756
|
+
id: "lotr-session-010",
|
|
2757
|
+
repo: "fellowship-docs",
|
|
2758
|
+
title: "Document why nobody should commit the ring config",
|
|
2759
|
+
sourceClient: "codex",
|
|
2760
|
+
sourceApp: "Codex app",
|
|
2761
|
+
model: "GPT-5.2",
|
|
2762
|
+
provider: "openai",
|
|
2763
|
+
hour: 18,
|
|
2764
|
+
minute: 25,
|
|
2765
|
+
durationMinutes: 24,
|
|
2766
|
+
inputTokens: 21e4,
|
|
2767
|
+
cachedInputTokens: 75e3,
|
|
2768
|
+
outputTokens: 38e3,
|
|
2769
|
+
reasoningTokens: 22e3,
|
|
2770
|
+
prompts: 2,
|
|
2771
|
+
replies: 5,
|
|
2772
|
+
tools: 18,
|
|
2773
|
+
commands: 6,
|
|
2774
|
+
edits: 4,
|
|
2775
|
+
reads: 16,
|
|
2776
|
+
outcome: "completed",
|
|
2777
|
+
branch: "docs/ring-config"
|
|
2778
|
+
},
|
|
2779
|
+
{
|
|
2780
|
+
id: "lotr-session-011",
|
|
2781
|
+
repo: "one-ring-infra",
|
|
2782
|
+
title: "Cap runaway apply retries in black-gate provisioner",
|
|
2783
|
+
sourceClient: "codex",
|
|
2784
|
+
sourceApp: "Terminal",
|
|
2785
|
+
sourceAppRaw: "terminal",
|
|
2786
|
+
model: "GPT-5.4",
|
|
2787
|
+
provider: "openai",
|
|
2788
|
+
hour: 18,
|
|
2789
|
+
minute: 55,
|
|
2790
|
+
durationMinutes: 44,
|
|
2791
|
+
inputTokens: 98e4,
|
|
2792
|
+
cachedInputTokens: 14e4,
|
|
2793
|
+
outputTokens: 148e3,
|
|
2794
|
+
reasoningTokens: 96e3,
|
|
2795
|
+
prompts: 4,
|
|
2796
|
+
replies: 9,
|
|
2797
|
+
tools: 41,
|
|
2798
|
+
commands: 21,
|
|
2799
|
+
edits: 4,
|
|
2800
|
+
reads: 45,
|
|
2801
|
+
outcome: "completed",
|
|
2802
|
+
branch: "infra/black-gate-retries",
|
|
2803
|
+
importantFailures: 1,
|
|
2804
|
+
harmlessNonZero: 2,
|
|
2805
|
+
topFailureType: "deploy",
|
|
2806
|
+
detectedSurface: "terminal_cli",
|
|
2807
|
+
issueSamples: [
|
|
2808
|
+
issue("terraform apply -auto-approve", "deploy", "critical", "high", "The demo caught a risky retry loop and switched to a dry-run plan.")
|
|
2809
|
+
]
|
|
2810
|
+
},
|
|
2811
|
+
{
|
|
2812
|
+
id: "lotr-session-012",
|
|
2813
|
+
repo: "rivendell-dashboard",
|
|
2814
|
+
title: "Make elvish table sorting less mysterious",
|
|
2815
|
+
sourceClient: "codex",
|
|
2816
|
+
sourceApp: "VS Code",
|
|
2817
|
+
sourceAppRaw: "vscode",
|
|
2818
|
+
model: "GPT-5.4-Mini",
|
|
2819
|
+
provider: "openai",
|
|
2820
|
+
hour: 19,
|
|
2821
|
+
minute: 20,
|
|
2822
|
+
durationMinutes: 29,
|
|
2823
|
+
inputTokens: 31e4,
|
|
2824
|
+
cachedInputTokens: 13e4,
|
|
2825
|
+
outputTokens: 49e3,
|
|
2826
|
+
reasoningTokens: 35e3,
|
|
2827
|
+
prompts: 3,
|
|
2828
|
+
replies: 6,
|
|
2829
|
+
tools: 22,
|
|
2830
|
+
commands: 8,
|
|
2831
|
+
edits: 3,
|
|
2832
|
+
reads: 18,
|
|
2833
|
+
outcome: "completed",
|
|
2834
|
+
branch: "tables/less-elvish"
|
|
2835
|
+
},
|
|
2836
|
+
{
|
|
2837
|
+
id: "lotr-session-013",
|
|
2838
|
+
repo: "gondor-api",
|
|
2839
|
+
title: "Trim noisy auth logs from white-tree callbacks",
|
|
2840
|
+
sourceClient: "codex",
|
|
2841
|
+
sourceApp: "Codex app",
|
|
2842
|
+
model: "GPT-5.3-Codex",
|
|
2843
|
+
provider: "openai",
|
|
2844
|
+
hour: 20,
|
|
2845
|
+
minute: 5,
|
|
2846
|
+
durationMinutes: 35,
|
|
2847
|
+
inputTokens: 42e4,
|
|
2848
|
+
cachedInputTokens: 16e4,
|
|
2849
|
+
outputTokens: 66e3,
|
|
2850
|
+
reasoningTokens: 54e3,
|
|
2851
|
+
prompts: 3,
|
|
2852
|
+
replies: 6,
|
|
2853
|
+
tools: 28,
|
|
2854
|
+
commands: 11,
|
|
2855
|
+
edits: 4,
|
|
2856
|
+
reads: 27,
|
|
2857
|
+
outcome: "completed",
|
|
2858
|
+
branch: "logs/white-tree"
|
|
2859
|
+
},
|
|
2860
|
+
{
|
|
2861
|
+
id: "lotr-session-014",
|
|
2862
|
+
repo: "shire-mobile",
|
|
2863
|
+
title: "Make pantry sync work offline in Bag End",
|
|
2864
|
+
sourceClient: "claude",
|
|
2865
|
+
sourceApp: "Claude Code",
|
|
2866
|
+
sourceAppRaw: "claude-code",
|
|
2867
|
+
model: "claude-haiku-4-5-20251001",
|
|
2868
|
+
provider: "anthropic",
|
|
2869
|
+
hour: 20,
|
|
2870
|
+
minute: 45,
|
|
2871
|
+
durationMinutes: 28,
|
|
2872
|
+
inputTokens: 26e4,
|
|
2873
|
+
cachedInputTokens: 9e4,
|
|
2874
|
+
outputTokens: 42e3,
|
|
2875
|
+
reasoningTokens: 0,
|
|
2876
|
+
prompts: 3,
|
|
2877
|
+
replies: 5,
|
|
2878
|
+
tools: 19,
|
|
2879
|
+
commands: 7,
|
|
2880
|
+
edits: 5,
|
|
2881
|
+
reads: 17,
|
|
2882
|
+
outcome: "completed",
|
|
2883
|
+
branch: "offline/bag-end"
|
|
2884
|
+
}
|
|
2885
|
+
];
|
|
2886
|
+
var demoSessions = demoInputs.map(toSession);
|
|
2887
|
+
function toSession(input) {
|
|
2888
|
+
const start = timestamp2(input.hour, input.minute);
|
|
2889
|
+
const end = new Date(new Date(start).getTime() + input.durationMinutes * 6e4).toISOString();
|
|
2890
|
+
const repoRoot = `/demo/middle-earth/${input.repo}`;
|
|
2891
|
+
const totalTokens = input.inputTokens + input.outputTokens + input.reasoningTokens;
|
|
2892
|
+
const importantFailures = input.importantFailures ?? 0;
|
|
2893
|
+
const harmlessNonZero = input.harmlessNonZero ?? 0;
|
|
2894
|
+
const exploratoryMisses = input.exploratoryMisses ?? 0;
|
|
2895
|
+
const repeatedClusters = input.repeatedClusters ?? 0;
|
|
2896
|
+
const failedToolCalls = importantFailures + repeatedClusters;
|
|
2897
|
+
const nonZeroCommandEvents = importantFailures + harmlessNonZero + exploratoryMisses;
|
|
2898
|
+
const cost = calculateCostUsd({
|
|
2899
|
+
model: input.model,
|
|
2900
|
+
inputTokens: input.inputTokens,
|
|
2901
|
+
cachedInputTokens: input.cachedInputTokens,
|
|
2902
|
+
outputTokens: input.outputTokens,
|
|
2903
|
+
reasoningTokens: input.reasoningTokens
|
|
2904
|
+
}, demoPricing);
|
|
2905
|
+
return {
|
|
2906
|
+
id: input.id,
|
|
2907
|
+
sourceClient: input.sourceClient,
|
|
2908
|
+
sourceApp: input.sourceApp,
|
|
2909
|
+
sourceAppRaw: input.sourceAppRaw ?? input.sourceApp.toLowerCase(),
|
|
2910
|
+
sourcePath: `/demo/middle-earth/${input.sourceClient}/sessions/${input.id}.jsonl`,
|
|
2911
|
+
repoRoot,
|
|
2912
|
+
repoName: input.repo,
|
|
2913
|
+
cwd: `${repoRoot}/worktree`,
|
|
2914
|
+
gitRemoteUrl: `https://example.invalid/middle-earth/${input.repo}.git`,
|
|
2915
|
+
gitBranch: input.branch,
|
|
2916
|
+
title: input.title,
|
|
2917
|
+
startedAt: start,
|
|
2918
|
+
endedAt: end,
|
|
2919
|
+
model: input.model,
|
|
2920
|
+
provider: input.provider,
|
|
2921
|
+
durationMs: input.durationMinutes * 6e4,
|
|
2922
|
+
rawEventCount: input.tools + input.replies + input.prompts + 8,
|
|
2923
|
+
parseStatus: "ok",
|
|
2924
|
+
parseErrors: [],
|
|
2925
|
+
detectedSurface: input.detectedSurface ?? detectedSurfaceForApp(input.sourceApp),
|
|
2926
|
+
surfaceConfidence: "high",
|
|
2927
|
+
surfaceReason: "Demo fixture includes an explicit fictional source app.",
|
|
2928
|
+
promptTimeline: [
|
|
2929
|
+
{ role: "user", text: input.title, timestamp: start },
|
|
2930
|
+
{ role: "assistant", text: demoReply(input.repo, input.edits, importantFailures), timestamp: timestamp2(input.hour, Math.min(input.minute + 7, 59)) }
|
|
2931
|
+
],
|
|
2932
|
+
sessionOutcome: input.outcome,
|
|
2933
|
+
inputTokens: input.inputTokens,
|
|
2934
|
+
cachedInputTokens: input.cachedInputTokens,
|
|
2935
|
+
outputTokens: input.outputTokens,
|
|
2936
|
+
reasoningTokens: input.reasoningTokens,
|
|
2937
|
+
reasoningOutputTokens: input.reasoningTokens,
|
|
2938
|
+
totalTokens,
|
|
2939
|
+
tokenAggregationMethod: "direct_usage",
|
|
2940
|
+
tokenConfidence: input.tokenConfidence ?? "high",
|
|
2941
|
+
tokenSnapshotCount: Math.max(2, Math.round(input.durationMinutes / 18)),
|
|
2942
|
+
estimatedCostUsd: cost,
|
|
2943
|
+
messageCount: input.prompts + input.replies,
|
|
2944
|
+
rawTokenTotal: totalTokens,
|
|
2945
|
+
warnings: input.warnings ?? [],
|
|
2946
|
+
userPromptCount: input.prompts,
|
|
2947
|
+
assistantMessageCount: input.replies,
|
|
2948
|
+
toolCallCount: input.tools,
|
|
2949
|
+
shellCommandCount: input.commands,
|
|
2950
|
+
failedToolCallCount: failedToolCalls,
|
|
2951
|
+
nonZeroCommandEvents,
|
|
2952
|
+
importantCommandFailures: importantFailures,
|
|
2953
|
+
harmlessNonZeroEvents: harmlessNonZero,
|
|
2954
|
+
exploratoryMisses,
|
|
2955
|
+
repeatedFailureClusters: repeatedClusters,
|
|
2956
|
+
commandIssueSeverity: importantFailures > 1 ? "critical" : importantFailures > 0 ? "warning" : "none",
|
|
2957
|
+
commandIssueImpact: importantFailures > 1 ? "high" : importantFailures > 0 ? "medium" : "none",
|
|
2958
|
+
topFailureType: input.topFailureType,
|
|
2959
|
+
commandIssueSamples: input.issueSamples ?? [],
|
|
2960
|
+
fileReadCount: input.reads,
|
|
2961
|
+
fileEditCount: input.edits
|
|
2962
|
+
};
|
|
2963
|
+
}
|
|
2964
|
+
function detectedSurfaceForApp(sourceApp) {
|
|
2965
|
+
if (sourceApp === "VS Code") return "vscode_extension";
|
|
2966
|
+
if (sourceApp === "Terminal") return "terminal_cli";
|
|
2967
|
+
if (sourceApp === "Codex app") return "codex_app_cloud";
|
|
2968
|
+
if (sourceApp === "Claude desktop local agent") return "local_agent";
|
|
2969
|
+
return "terminal_cli";
|
|
2970
|
+
}
|
|
2971
|
+
function timestamp2(hour, minute) {
|
|
2972
|
+
return `${baseDate}T${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}:00.000Z`;
|
|
2973
|
+
}
|
|
2974
|
+
function issue(command2, category, severity, impact, reason) {
|
|
2975
|
+
return {
|
|
2976
|
+
command: command2,
|
|
2977
|
+
category,
|
|
2978
|
+
classification: severity === "critical" ? "blocking_failure" : "warning",
|
|
2979
|
+
severity,
|
|
2980
|
+
impact,
|
|
2981
|
+
reason
|
|
2982
|
+
};
|
|
2983
|
+
}
|
|
2984
|
+
function demoReply(repo, edits, importantFailures) {
|
|
2985
|
+
if (importantFailures > 0) {
|
|
2986
|
+
return `Found the noisy path in ${repo}, separated the risky failures from ordinary exploration, and left a short list of follow-up checks.`;
|
|
2987
|
+
}
|
|
2988
|
+
if (edits === 0) {
|
|
2989
|
+
return `Mapped the spend pattern in ${repo} without changing files, which is exactly the kind of research-only trail RepoSpend should make visible.`;
|
|
2990
|
+
}
|
|
2991
|
+
return `Updated ${repo} with focused changes and enough local checks to make the token spend easy to explain.`;
|
|
2992
|
+
}
|
|
2993
|
+
|
|
1800
2994
|
// apps/server/src/rtk.ts
|
|
1801
2995
|
import { execFile } from "node:child_process";
|
|
1802
2996
|
import { promisify } from "node:util";
|
|
@@ -1991,8 +3185,10 @@ function createServer(options = {}) {
|
|
|
1991
3185
|
const app = Fastify({ logger: false });
|
|
1992
3186
|
app.get("/api/health", async () => ({ ok: true, app: "RepoSpend", version: readPackageVersion() }));
|
|
1993
3187
|
app.get("/api/dashboard", async (request) => {
|
|
1994
|
-
const
|
|
1995
|
-
const
|
|
3188
|
+
const filters = parseFilters(request.query);
|
|
3189
|
+
const demoMode = demoModeEnabled();
|
|
3190
|
+
const data = demoMode ? readDemoDashboardData(filters) : readDashboardData(filters);
|
|
3191
|
+
const rtkGain = demoMode ? readDemoRtkGain() : await readRtkGain();
|
|
1996
3192
|
return {
|
|
1997
3193
|
...data,
|
|
1998
3194
|
summary: { ...data.summary, skippedZeroTokenSessions: data.skippedZeroTokenSessions },
|
|
@@ -2018,9 +3214,9 @@ function createServer(options = {}) {
|
|
|
2018
3214
|
return exportCsv();
|
|
2019
3215
|
});
|
|
2020
3216
|
if (options.serveWeb !== false) {
|
|
2021
|
-
const serverDist =
|
|
2022
|
-
const packagedWebDist =
|
|
2023
|
-
const workspaceWebDist =
|
|
3217
|
+
const serverDist = path6.dirname(fileURLToPath2(import.meta.url));
|
|
3218
|
+
const packagedWebDist = path6.resolve(serverDist, "../web-dist");
|
|
3219
|
+
const workspaceWebDist = path6.resolve(serverDist, "../../web/dist");
|
|
2024
3220
|
const webDist = fsExists(packagedWebDist) ? packagedWebDist : workspaceWebDist;
|
|
2025
3221
|
if (!fsExists(webDist)) {
|
|
2026
3222
|
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 +3237,13 @@ function createServer(options = {}) {
|
|
|
2041
3237
|
return app;
|
|
2042
3238
|
}
|
|
2043
3239
|
function fsExists(inputPath) {
|
|
2044
|
-
return Boolean(inputPath) &&
|
|
3240
|
+
return Boolean(inputPath) && path6.isAbsolute(inputPath) && fs7.existsSync(inputPath);
|
|
2045
3241
|
}
|
|
2046
3242
|
function readPackageVersion() {
|
|
2047
|
-
const serverDist =
|
|
2048
|
-
const packagePath =
|
|
3243
|
+
const serverDist = path6.dirname(fileURLToPath2(import.meta.url));
|
|
3244
|
+
const packagePath = path6.resolve(serverDist, "../package.json");
|
|
2049
3245
|
try {
|
|
2050
|
-
const parsed = JSON.parse(
|
|
3246
|
+
const parsed = JSON.parse(fs7.readFileSync(packagePath, "utf8"));
|
|
2051
3247
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
2052
3248
|
} catch {
|
|
2053
3249
|
return "unknown";
|
|
@@ -2080,8 +3276,10 @@ function validatePricingBody(body) {
|
|
|
2080
3276
|
outputPerMillion
|
|
2081
3277
|
};
|
|
2082
3278
|
const cachedInputPerMillion = finiteNumber(row.cachedInputPerMillion);
|
|
3279
|
+
const cacheCreationInputPerMillion = finiteNumber(row.cacheCreationInputPerMillion);
|
|
2083
3280
|
const reasoningOutputPerMillion = finiteNumber(row.reasoningOutputPerMillion);
|
|
2084
3281
|
if (cachedInputPerMillion !== void 0) modelPricing.cachedInputPerMillion = cachedInputPerMillion;
|
|
3282
|
+
if (cacheCreationInputPerMillion !== void 0) modelPricing.cacheCreationInputPerMillion = cacheCreationInputPerMillion;
|
|
2085
3283
|
if (reasoningOutputPerMillion !== void 0) modelPricing.reasoningOutputPerMillion = reasoningOutputPerMillion;
|
|
2086
3284
|
if (typeof row.note === "string") modelPricing.note = row.note;
|
|
2087
3285
|
validated[model] = modelPricing;
|
|
@@ -2097,27 +3295,30 @@ function finiteNumber(value) {
|
|
|
2097
3295
|
// apps/server/src/cli.ts
|
|
2098
3296
|
var [command = "serve", ...args] = process.argv.slice(2);
|
|
2099
3297
|
try {
|
|
2100
|
-
if (command === "
|
|
3298
|
+
if (command === "--help" || command === "-h" || command === "help") {
|
|
3299
|
+
printHelp();
|
|
3300
|
+
} else if (command === "serve") {
|
|
2101
3301
|
const { url } = await startServer({ port: Number(process.env.REPOSPEND_PORT ?? 2005), serveWeb: true });
|
|
2102
3302
|
console.log(`RepoSpend dashboard: ${url}`);
|
|
2103
3303
|
openBrowser(url);
|
|
2104
3304
|
} else if (command === "scan") {
|
|
2105
|
-
const data = readDashboardData();
|
|
3305
|
+
const data = readDashboardData(cliFilters(args));
|
|
2106
3306
|
console.log(JSON.stringify({ sources: data.sources, sessionCount: data.sessions.length, summary: data.summary }, null, 2));
|
|
2107
3307
|
} else if (command === "by-repo") {
|
|
2108
|
-
printGroups(groupByRepo(readDashboardData().sessions));
|
|
3308
|
+
printGroups(groupByRepo(readDashboardData(cliFilters(args)).sessions));
|
|
2109
3309
|
} else if (command === "by-day") {
|
|
2110
|
-
printGroups(groupByDay(readDashboardData().sessions));
|
|
3310
|
+
printGroups(groupByDay(readDashboardData(cliFilters(args)).sessions));
|
|
2111
3311
|
} else if (command === "by-hour") {
|
|
2112
|
-
printGroups(groupByHour(readDashboardData().sessions));
|
|
3312
|
+
printGroups(groupByHour(readDashboardData(cliFilters(args)).sessions));
|
|
2113
3313
|
} else if (command === "by-model") {
|
|
2114
|
-
printGroups(groupByModel(readDashboardData().sessions));
|
|
3314
|
+
printGroups(groupByModel(readDashboardData(cliFilters(args)).sessions));
|
|
2115
3315
|
} else if (command === "by-app") {
|
|
2116
|
-
printGroups(groupBySourceApp(readDashboardData().sessions));
|
|
3316
|
+
printGroups(groupBySourceApp(readDashboardData(cliFilters(args)).sessions));
|
|
2117
3317
|
} else if (command === "export") {
|
|
2118
3318
|
const format = valueAfter(args, "--format") ?? "json";
|
|
2119
|
-
const
|
|
2120
|
-
|
|
3319
|
+
const filters = cliFilters(args);
|
|
3320
|
+
const sessions = readDashboardData(filters).sessions;
|
|
3321
|
+
console.log(format === "csv" ? toCsv(sessions) : JSON.stringify(sessions, null, 2));
|
|
2121
3322
|
} else {
|
|
2122
3323
|
console.error(`Unknown command: ${command}`);
|
|
2123
3324
|
printHelp();
|
|
@@ -2138,7 +3339,7 @@ function openBrowser(url) {
|
|
|
2138
3339
|
}
|
|
2139
3340
|
function printGroups(groups) {
|
|
2140
3341
|
if (!groups.length) {
|
|
2141
|
-
console.log("No usage found. Scanned
|
|
3342
|
+
console.log("No usage found. Scanned Codex and Claude Code local data paths.");
|
|
2142
3343
|
return;
|
|
2143
3344
|
}
|
|
2144
3345
|
const rows = groups.map((group) => ({
|
|
@@ -2149,6 +3350,11 @@ function printGroups(groups) {
|
|
|
2149
3350
|
}));
|
|
2150
3351
|
console.table(rows);
|
|
2151
3352
|
}
|
|
3353
|
+
function cliFilters(args2) {
|
|
3354
|
+
const source = valueAfter(args2, "--source");
|
|
3355
|
+
if (!source || source === "all") return {};
|
|
3356
|
+
return { source };
|
|
3357
|
+
}
|
|
2152
3358
|
function valueAfter(args2, flag) {
|
|
2153
3359
|
const index = args2.indexOf(flag);
|
|
2154
3360
|
return index >= 0 ? args2[index + 1] : void 0;
|
|
@@ -2163,5 +3369,8 @@ function printHelp() {
|
|
|
2163
3369
|
repospend by-model
|
|
2164
3370
|
repospend by-app
|
|
2165
3371
|
repospend export --format json
|
|
2166
|
-
repospend export --format csv
|
|
3372
|
+
repospend export --format csv
|
|
3373
|
+
|
|
3374
|
+
Options:
|
|
3375
|
+
--source all|codex|claude`);
|
|
2167
3376
|
}
|