repospend 0.0.4 → 0.0.6
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 +20 -0
- package/README.md +25 -0
- package/dist/cli.js +428 -78
- package/docs/PUBLISHING.md +4 -1
- package/package.json +1 -1
- package/web-dist/assets/index-BxuOiBnq.js +265 -0
- package/web-dist/assets/index-KkDIavlL.css +1 -0
- package/web-dist/index.html +2 -2
- package/web-dist/assets/index-B8lKmhuv.js +0 -260
- package/web-dist/assets/index-CUTybR_N.css +0 -1
package/dist/cli.js
CHANGED
|
@@ -44,10 +44,14 @@ function summarize(sessions) {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
function groupByRepo(sessions) {
|
|
47
|
-
|
|
47
|
+
const groups = groupBy(sessions, (session) => session.repoRoot, (session) => session.repoName);
|
|
48
|
+
return groups.map((group) => ({
|
|
49
|
+
...group,
|
|
50
|
+
verified: sessions.some((s) => s.repoRoot === group.id && !s.warnings.includes("repo_unverified_no_git_root"))
|
|
51
|
+
}));
|
|
48
52
|
}
|
|
49
53
|
function groupByModel(sessions) {
|
|
50
|
-
return groupBy(sessions, (session) => session.model ?? "unknown-model", (session) => session.model ?? "
|
|
54
|
+
return groupBy(sessions, (session) => session.model ?? "unknown-model", (session) => session.model ?? "Model not recorded");
|
|
51
55
|
}
|
|
52
56
|
function groupBySourceApp(sessions) {
|
|
53
57
|
return groupBy(sessions, (session) => session.sourceApp, (session) => session.sourceApp);
|
|
@@ -212,6 +216,8 @@ var defaultPricing = {
|
|
|
212
216
|
"gpt-5.4-mini": { inputPerMillion: 0.75, cachedInputPerMillion: 0.075, outputPerMillion: 4.5, reasoningOutputPerMillion: 4.5 },
|
|
213
217
|
"gpt-5.4-nano": { inputPerMillion: 0.2, cachedInputPerMillion: 0.02, outputPerMillion: 1.25, reasoningOutputPerMillion: 1.25 },
|
|
214
218
|
"gpt-5.4-pro": { inputPerMillion: 30, outputPerMillion: 180, reasoningOutputPerMillion: 180 },
|
|
219
|
+
"gpt-5.3-codex": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
|
|
220
|
+
"gpt-5.3-codex-spark": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14, note: "Research preview pricing is not final in the OpenAI Codex rate card; this API-equivalent rate matches the practical ccusage/LiteLLM-style estimate." },
|
|
215
221
|
"gpt-5.2": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
|
|
216
222
|
"gpt-5.2-chat-latest": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
|
|
217
223
|
"gpt-5.2-codex": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
|
|
@@ -342,7 +348,7 @@ function filterUsage(sessions, filters) {
|
|
|
342
348
|
if (!matchesAny(session.sourceClient, filters.source)) return false;
|
|
343
349
|
if (!matchesAny(session.sourceApp, filters.sourceApp)) return false;
|
|
344
350
|
if (!matchesRepo(session, filters.repo)) return false;
|
|
345
|
-
if (!
|
|
351
|
+
if (!matchesModel(session.model, filters.model)) return false;
|
|
346
352
|
const startedAt = timestamp(session.startedAt);
|
|
347
353
|
const hasDateFilter = Boolean(filters.from || filters.to);
|
|
348
354
|
if (hasDateFilter && startedAt === void 0) return false;
|
|
@@ -363,6 +369,11 @@ function matchesRepo(session, filter) {
|
|
|
363
369
|
if (!values.length) return true;
|
|
364
370
|
return values.includes(session.repoName) || values.includes(session.repoRoot);
|
|
365
371
|
}
|
|
372
|
+
function matchesModel(value, filter) {
|
|
373
|
+
const values = filterValues(filter);
|
|
374
|
+
if (!values.length) return true;
|
|
375
|
+
return value !== void 0 ? values.includes(value) : values.includes("unknown-model");
|
|
376
|
+
}
|
|
366
377
|
function filterValues(filter) {
|
|
367
378
|
if (!filter) return [];
|
|
368
379
|
return Array.isArray(filter) ? filter.filter(Boolean) : filter.split(",").map((value) => value.trim()).filter(Boolean);
|
|
@@ -847,8 +858,12 @@ function isDashboardVisibleSession(session) {
|
|
|
847
858
|
// packages/core/src/repo.ts
|
|
848
859
|
import fs3 from "node:fs";
|
|
849
860
|
import path3 from "node:path";
|
|
861
|
+
function normalizeRoot(p) {
|
|
862
|
+
const stripped = p.startsWith("\\\\?\\") ? p.slice(4) : p;
|
|
863
|
+
return stripped.replace(/^([a-zA-Z]):/, (_, letter) => letter.toUpperCase() + ":");
|
|
864
|
+
}
|
|
850
865
|
function findGitRoot(cwd, fileSystem = fs3) {
|
|
851
|
-
let current = path3.resolve(cwd);
|
|
866
|
+
let current = normalizeRoot(path3.resolve(cwd));
|
|
852
867
|
while (true) {
|
|
853
868
|
const gitPath = path3.join(current, ".git");
|
|
854
869
|
if (fileSystem.existsSync(gitPath)) {
|
|
@@ -895,8 +910,8 @@ function safeFindGitRoot(cwd, fileSystem) {
|
|
|
895
910
|
}
|
|
896
911
|
}
|
|
897
912
|
function fallbackWorkspaceRoot(cwd) {
|
|
898
|
-
const resolved = path3.resolve(cwd || process.cwd());
|
|
899
|
-
const home = process.env.HOME ? path3.resolve(process.env.HOME) : void 0;
|
|
913
|
+
const resolved = normalizeRoot(path3.resolve(cwd || process.cwd()));
|
|
914
|
+
const home = process.env.HOME ? normalizeRoot(path3.resolve(process.env.HOME)) : void 0;
|
|
900
915
|
if (home && (resolved === home || resolved.startsWith(`${home}${path3.sep}`))) {
|
|
901
916
|
const relative = path3.relative(home, resolved).split(path3.sep).filter(Boolean);
|
|
902
917
|
const first = relative[0];
|
|
@@ -946,8 +961,9 @@ import Database from "better-sqlite3";
|
|
|
946
961
|
// packages/core/src/sources/agent-friction.ts
|
|
947
962
|
function analyzeAgentFriction(records, totalTokens) {
|
|
948
963
|
const analysis = emptyAgentFrictionAnalysis();
|
|
964
|
+
const messages = [];
|
|
949
965
|
for (const record of records) {
|
|
950
|
-
applyActionMetadata(analysis, record);
|
|
966
|
+
applyActionMetadata(analysis, record, messages);
|
|
951
967
|
}
|
|
952
968
|
finalizeCommandIssues(analysis, totalTokens);
|
|
953
969
|
return analysis;
|
|
@@ -973,14 +989,14 @@ function emptyAgentFrictionAnalysis() {
|
|
|
973
989
|
assistantMessageCount: 0
|
|
974
990
|
};
|
|
975
991
|
}
|
|
976
|
-
function applyActionMetadata(analysis, record) {
|
|
992
|
+
function applyActionMetadata(analysis, record, messages) {
|
|
977
993
|
if (!record || typeof record !== "object") return;
|
|
978
994
|
const object = record;
|
|
979
995
|
const payload = firstObject(object.payload) ?? object;
|
|
980
996
|
const type = stringValue(object.type) ?? stringValue(payload.type);
|
|
981
997
|
const role = stringValue(payload.role);
|
|
982
|
-
|
|
983
|
-
if (
|
|
998
|
+
const messageRole = payload.type === "user_message" || role === "user" ? "user" : payload.type === "agent_message" || role === "assistant" ? "assistant" : void 0;
|
|
999
|
+
if (messageRole) applyMessageCount(analysis, messages, messageRole, payload, object);
|
|
984
1000
|
const text = JSON.stringify(record).toLowerCase();
|
|
985
1001
|
const isTool = type?.includes("tool") || type?.includes("function_call") || text.includes('"tool_call"') || text.includes('"function_call"');
|
|
986
1002
|
if (isTool) analysis.toolCallCount += 1;
|
|
@@ -1140,6 +1156,64 @@ function firstObject(...values) {
|
|
|
1140
1156
|
function stringValue(value) {
|
|
1141
1157
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1142
1158
|
}
|
|
1159
|
+
function applyMessageCount(analysis, messages, role, payload, object) {
|
|
1160
|
+
const message = extractMessageText(payload) ?? extractMessageText(object);
|
|
1161
|
+
if (!message) return;
|
|
1162
|
+
const item = {
|
|
1163
|
+
role,
|
|
1164
|
+
text: compactText(message),
|
|
1165
|
+
timestamp: dateValue(payload.timestamp) ?? dateValue(object.timestamp) ?? dateValue(payload.created_at) ?? dateValue(object.created_at)
|
|
1166
|
+
};
|
|
1167
|
+
if (!item.text || isCodexStartupContext(item.text) || hasNearbyDuplicateMessage(messages, item)) return;
|
|
1168
|
+
messages.push(item);
|
|
1169
|
+
if (role === "user") analysis.userPromptCount += 1;
|
|
1170
|
+
if (role === "assistant") analysis.assistantMessageCount += 1;
|
|
1171
|
+
}
|
|
1172
|
+
function extractMessageText(record) {
|
|
1173
|
+
for (const key of ["text", "message", "content", "input"]) {
|
|
1174
|
+
const value = record[key];
|
|
1175
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
1176
|
+
if (Array.isArray(value)) {
|
|
1177
|
+
const parts = value.map((item) => {
|
|
1178
|
+
if (typeof item === "string") return item;
|
|
1179
|
+
if (item && typeof item === "object") {
|
|
1180
|
+
const object = item;
|
|
1181
|
+
return stringValue(object.text) ?? stringValue(object.content);
|
|
1182
|
+
}
|
|
1183
|
+
return void 0;
|
|
1184
|
+
}).filter((item) => Boolean(item?.trim()));
|
|
1185
|
+
if (parts.length) return parts.join("\n");
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return void 0;
|
|
1189
|
+
}
|
|
1190
|
+
function compactText(text) {
|
|
1191
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1192
|
+
}
|
|
1193
|
+
function isCodexStartupContext(text) {
|
|
1194
|
+
return text.includes("<environment_context>") && (text.includes("# AGENTS.md instructions") || text.includes("<INSTRUCTIONS>"));
|
|
1195
|
+
}
|
|
1196
|
+
function hasNearbyDuplicateMessage(items, item) {
|
|
1197
|
+
return items.some((existing) => {
|
|
1198
|
+
if (existing.role !== item.role || existing.text !== item.text) return false;
|
|
1199
|
+
if (!existing.timestamp || !item.timestamp) return true;
|
|
1200
|
+
const existingTime = new Date(existing.timestamp).getTime();
|
|
1201
|
+
const itemTime = new Date(item.timestamp).getTime();
|
|
1202
|
+
if (!Number.isFinite(existingTime) || !Number.isFinite(itemTime)) return false;
|
|
1203
|
+
return Math.abs(existingTime - itemTime) <= 5e3;
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
function dateValue(value) {
|
|
1207
|
+
if (typeof value === "number") {
|
|
1208
|
+
if (!Number.isFinite(value)) return void 0;
|
|
1209
|
+
return new Date(value > 1e10 ? value : value * 1e3).toISOString();
|
|
1210
|
+
}
|
|
1211
|
+
if (typeof value === "string") {
|
|
1212
|
+
const date = new Date(value);
|
|
1213
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
1214
|
+
}
|
|
1215
|
+
return void 0;
|
|
1216
|
+
}
|
|
1143
1217
|
|
|
1144
1218
|
// packages/core/src/sources/codex-token.ts
|
|
1145
1219
|
function aggregateTokens(records) {
|
|
@@ -1210,8 +1284,8 @@ function finalizeTokenAccumulator(accumulator) {
|
|
|
1210
1284
|
{ inputTokens: 0, cachedInputTokens: 0, outputTokens: 0, reasoningTokens: 0, totalTokens: 0 }
|
|
1211
1285
|
);
|
|
1212
1286
|
applyFinalUsage(accumulator, total);
|
|
1213
|
-
accumulator.tokenAggregationMethod = "
|
|
1214
|
-
accumulator.tokenConfidence = "
|
|
1287
|
+
accumulator.tokenAggregationMethod = "delta_sum";
|
|
1288
|
+
accumulator.tokenConfidence = "high";
|
|
1215
1289
|
return;
|
|
1216
1290
|
}
|
|
1217
1291
|
accumulator.tokenAggregationMethod = "unknown";
|
|
@@ -1222,6 +1296,10 @@ function extractRecordTokens(record) {
|
|
|
1222
1296
|
const object = record;
|
|
1223
1297
|
const payload = firstObject2(object.payload);
|
|
1224
1298
|
const payloadInfo = firstObject2(payload?.info);
|
|
1299
|
+
const codexLastUsage = firstObject2(payloadInfo?.last_token_usage);
|
|
1300
|
+
if (codexLastUsage) {
|
|
1301
|
+
return tokenUsageFromObject(codexLastUsage, false);
|
|
1302
|
+
}
|
|
1225
1303
|
const codexTotalUsage = firstObject2(payloadInfo?.total_token_usage);
|
|
1226
1304
|
if (codexTotalUsage) {
|
|
1227
1305
|
return tokenUsageFromObject(codexTotalUsage, true);
|
|
@@ -1289,12 +1367,9 @@ function scanCodex(options) {
|
|
|
1289
1367
|
if (stateExists) {
|
|
1290
1368
|
const stateSessions = readThreads(statePath, sessionFiles, options, warnings);
|
|
1291
1369
|
sessions.push(...stateSessions);
|
|
1292
|
-
if (!stateSessions.length && sessionsExists) {
|
|
1293
|
-
sessions.push(...readStandaloneSessions(sessionFiles, options));
|
|
1294
|
-
}
|
|
1295
1370
|
}
|
|
1296
|
-
if (
|
|
1297
|
-
sessions.push(...readStandaloneSessions(sessionFiles, options));
|
|
1371
|
+
if (sessionsExists) {
|
|
1372
|
+
sessions.push(...readStandaloneSessions(sessionFiles, options).filter((session) => !hasImportedSession(sessions, session)));
|
|
1298
1373
|
}
|
|
1299
1374
|
return {
|
|
1300
1375
|
source: {
|
|
@@ -1337,8 +1412,10 @@ function readThreads(statePath, sessionFiles, options, warnings) {
|
|
|
1337
1412
|
warnings.push("Codex threads table has no recognized columns.");
|
|
1338
1413
|
return [];
|
|
1339
1414
|
}
|
|
1415
|
+
const importedModels = readImportedModelMap(options.codexHome ?? path4.join(os2.homedir(), ".codex"), warnings);
|
|
1340
1416
|
const rows = db.prepare(`SELECT ${selectedColumns.map((column) => `"${column}"`).join(", ")} FROM threads`).all();
|
|
1341
|
-
|
|
1417
|
+
const sessions = rows.map((row, index) => threadToUsage(row, index, sessionFiles, options, importedModels));
|
|
1418
|
+
return attributeSubagentsToParentSurfaces(sessions, rows);
|
|
1342
1419
|
} catch (error) {
|
|
1343
1420
|
warnings.push(`Unable to read Codex SQLite state at ${statePath}: ${errorMessage(error)}`);
|
|
1344
1421
|
return [];
|
|
@@ -1346,6 +1423,37 @@ function readThreads(statePath, sessionFiles, options, warnings) {
|
|
|
1346
1423
|
db?.close();
|
|
1347
1424
|
}
|
|
1348
1425
|
}
|
|
1426
|
+
function attributeSubagentsToParentSurfaces(sessions, rows) {
|
|
1427
|
+
const sessionsById = new Map(sessions.map((session) => [session.id, session]));
|
|
1428
|
+
const parentIdsByChildId = /* @__PURE__ */ new Map();
|
|
1429
|
+
for (const row of rows) {
|
|
1430
|
+
const childId = stringValue2(row.id);
|
|
1431
|
+
const parentId = codexParentThreadId(stringValue2(row.source));
|
|
1432
|
+
if (childId && parentId) parentIdsByChildId.set(childId, parentId);
|
|
1433
|
+
}
|
|
1434
|
+
return sessions.map((session) => {
|
|
1435
|
+
if (session.sourceApp !== "Codex subagent") return session;
|
|
1436
|
+
const parent = sessionsById.get(parentIdsByChildId.get(session.id) ?? "");
|
|
1437
|
+
if (!parent || parent.sourceApp === "Codex subagent") return session;
|
|
1438
|
+
return {
|
|
1439
|
+
...session,
|
|
1440
|
+
sourceApp: parent.sourceApp,
|
|
1441
|
+
sourceAppRaw: session.sourceAppRaw,
|
|
1442
|
+
surfaceReason: `subagent attributed to parent thread ${parent.id}: ${parent.surfaceReason}`
|
|
1443
|
+
};
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
function codexParentThreadId(source) {
|
|
1447
|
+
if (!source?.trim().startsWith("{")) return void 0;
|
|
1448
|
+
try {
|
|
1449
|
+
const object = asRecord(JSON.parse(source));
|
|
1450
|
+
const subagent = asRecord(object?.subagent);
|
|
1451
|
+
const threadSpawn = asRecord(subagent?.thread_spawn);
|
|
1452
|
+
return stringValue2(threadSpawn?.parent_thread_id);
|
|
1453
|
+
} catch {
|
|
1454
|
+
return void 0;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1349
1457
|
function openCodexStateDatabase(statePath, warnings) {
|
|
1350
1458
|
try {
|
|
1351
1459
|
return new Database(statePath, { readonly: true, fileMustExist: true });
|
|
@@ -1410,24 +1518,30 @@ function isNativeSqliteMismatch(error) {
|
|
|
1410
1518
|
function exitLabel(status) {
|
|
1411
1519
|
return status === null ? "" : ` with exit code ${status}`;
|
|
1412
1520
|
}
|
|
1413
|
-
function threadToUsage(row, index, sessionFiles, options) {
|
|
1521
|
+
function threadToUsage(row, index, sessionFiles, options, importedModels = /* @__PURE__ */ new Map()) {
|
|
1522
|
+
const id = stringValue2(row.id) ?? `codex-thread-${index}`;
|
|
1414
1523
|
const sourcePath = stringValue2(row.rollout_path) ?? matchSessionPath(stringValue2(row.id), sessionFiles) ?? "";
|
|
1415
1524
|
const sessionBreakdown = sourcePath ? readSessionBreakdown(sourcePath) : emptyBreakdown();
|
|
1416
1525
|
const dbTokens = parseTokenTotal(row.tokens_used);
|
|
1417
|
-
const cwd = stringValue2(row.cwd) ?? fallbackCwdFromPath(sourcePath);
|
|
1526
|
+
const cwd = stringValue2(row.cwd) ?? sessionBreakdown.cwd ?? fallbackCwdFromPath(sourcePath);
|
|
1418
1527
|
const repo = resolveRepoInfo(cwd, options.config);
|
|
1528
|
+
const inferredModel = stringValue2(row.model) || sessionBreakdown.model ? void 0 : importedModels.get(id);
|
|
1529
|
+
const model = stringValue2(row.model) ?? sessionBreakdown.model ?? inferredModel?.model;
|
|
1530
|
+
const provider = sessionBreakdown.provider ?? inferredModel?.provider ?? stringValue2(row.model_provider);
|
|
1419
1531
|
const inputTokens = sessionBreakdown.inputTokens;
|
|
1420
1532
|
const cachedInputTokens = sessionBreakdown.cachedInputTokens;
|
|
1421
1533
|
const outputTokens = sessionBreakdown.outputTokens;
|
|
1422
1534
|
const reasoningTokens = sessionBreakdown.reasoningTokens;
|
|
1423
1535
|
const totalTokens = sessionBreakdown.totalTokens || dbTokens || inputTokens + outputTokens + reasoningTokens;
|
|
1424
1536
|
const hasPricedBreakdown = inputTokens + cachedInputTokens + outputTokens + reasoningTokens > 0;
|
|
1425
|
-
const warnings = [...repo.warnings, ...sessionBreakdown.warnings];
|
|
1537
|
+
const warnings = [...repo.warnings, ...sessionBreakdown.warnings, ...inferredModel ? ["model_inferred_from_import_source"] : []];
|
|
1426
1538
|
const threadSource = stringValue2(row.thread_source) ?? sessionBreakdown.threadSource;
|
|
1427
|
-
const sourceAppRaw = stringValue2(row.source)
|
|
1539
|
+
const sourceAppRaw = codexSourceRaw(stringValue2(row.source), sessionBreakdown.source, sessionBreakdown.originator, threadSource);
|
|
1428
1540
|
const surface = detectSurface(sourceAppRaw, threadSource);
|
|
1541
|
+
const startedAt = dateValue2(row.created_at) ?? sessionBreakdown.startedAt;
|
|
1542
|
+
const endedAt = dateValue2(row.updated_at) ?? sessionBreakdown.endedAt;
|
|
1429
1543
|
const usage = {
|
|
1430
|
-
id
|
|
1544
|
+
id,
|
|
1431
1545
|
sourceClient: "codex",
|
|
1432
1546
|
sourceApp: codexSourceAppLabel(sourceAppRaw, threadSource),
|
|
1433
1547
|
sourceAppRaw,
|
|
@@ -1437,11 +1551,11 @@ function threadToUsage(row, index, sessionFiles, options) {
|
|
|
1437
1551
|
cwd,
|
|
1438
1552
|
gitRemoteUrl: repo.gitRemoteUrl,
|
|
1439
1553
|
gitBranch: repo.gitBranch,
|
|
1440
|
-
title: stringValue2(row.title),
|
|
1441
|
-
startedAt
|
|
1442
|
-
endedAt
|
|
1443
|
-
model
|
|
1444
|
-
provider
|
|
1554
|
+
title: stringValue2(row.title) ?? sessionBreakdown.title,
|
|
1555
|
+
startedAt,
|
|
1556
|
+
endedAt,
|
|
1557
|
+
model,
|
|
1558
|
+
provider,
|
|
1445
1559
|
inputTokens,
|
|
1446
1560
|
cachedInputTokens,
|
|
1447
1561
|
outputTokens,
|
|
@@ -1456,7 +1570,7 @@ function threadToUsage(row, index, sessionFiles, options) {
|
|
|
1456
1570
|
rawTokenTotal: dbTokens || sessionBreakdown.totalTokens || void 0,
|
|
1457
1571
|
warnings,
|
|
1458
1572
|
codexHome: options.codexHome ?? path4.join(os2.homedir(), ".codex"),
|
|
1459
|
-
durationMs: durationMs(
|
|
1573
|
+
durationMs: durationMs(startedAt, endedAt),
|
|
1460
1574
|
rawEventCount: sessionBreakdown.rawEventCount,
|
|
1461
1575
|
parseStatus: sessionBreakdown.parseStatus,
|
|
1462
1576
|
parseErrors: sessionBreakdown.parseErrors,
|
|
@@ -1480,7 +1594,8 @@ function threadToUsage(row, index, sessionFiles, options) {
|
|
|
1480
1594
|
fileReadCount: sessionBreakdown.fileReadCount,
|
|
1481
1595
|
fileEditCount: sessionBreakdown.fileEditCount,
|
|
1482
1596
|
promptTimeline: sessionBreakdown.promptTimeline,
|
|
1483
|
-
sessionOutcome: inferOutcome(sessionBreakdown)
|
|
1597
|
+
sessionOutcome: inferOutcome(sessionBreakdown),
|
|
1598
|
+
compaction: sessionBreakdown.compaction
|
|
1484
1599
|
};
|
|
1485
1600
|
const cost = hasPricedBreakdown ? calculateCostUsd(usage, options.pricing) : void 0;
|
|
1486
1601
|
const costWarnings = hasPricedBreakdown ? warnings : [...warnings, "missing_token_breakdown"];
|
|
@@ -1493,26 +1608,28 @@ function threadToUsage(row, index, sessionFiles, options) {
|
|
|
1493
1608
|
function readStandaloneSessions(sessionFiles, options) {
|
|
1494
1609
|
return [...sessionFiles.entries()].map(([id, sourcePath]) => {
|
|
1495
1610
|
const breakdown = readSessionBreakdown(sourcePath);
|
|
1496
|
-
const cwd = fallbackCwdFromPath(sourcePath);
|
|
1611
|
+
const cwd = breakdown.cwd ?? fallbackCwdFromPath(sourcePath);
|
|
1497
1612
|
const repo = resolveRepoInfo(cwd, options.config);
|
|
1498
|
-
const model =
|
|
1499
|
-
const
|
|
1500
|
-
|
|
1501
|
-
|
|
1613
|
+
const model = breakdown.model;
|
|
1614
|
+
const provider = breakdown.provider ?? providerForModel(model ?? "");
|
|
1615
|
+
const sourceAppRaw = codexSourceRaw(void 0, breakdown.source, breakdown.originator, breakdown.threadSource);
|
|
1616
|
+
const surface = detectSurface(sourceAppRaw, breakdown.threadSource);
|
|
1617
|
+
const usage = {
|
|
1618
|
+
id: breakdown.id ?? id,
|
|
1502
1619
|
sourceClient: "codex",
|
|
1503
|
-
sourceApp: codexSourceAppLabel(
|
|
1504
|
-
sourceAppRaw
|
|
1620
|
+
sourceApp: codexSourceAppLabel(sourceAppRaw, breakdown.threadSource),
|
|
1621
|
+
sourceAppRaw,
|
|
1505
1622
|
sourcePath,
|
|
1506
1623
|
repoRoot: repo.repoRoot,
|
|
1507
1624
|
repoName: repo.repoName,
|
|
1508
1625
|
cwd,
|
|
1509
1626
|
gitRemoteUrl: repo.gitRemoteUrl,
|
|
1510
1627
|
gitBranch: repo.gitBranch,
|
|
1511
|
-
title:
|
|
1512
|
-
startedAt:
|
|
1513
|
-
endedAt:
|
|
1628
|
+
title: breakdown.title,
|
|
1629
|
+
startedAt: breakdown.startedAt,
|
|
1630
|
+
endedAt: breakdown.endedAt,
|
|
1514
1631
|
model,
|
|
1515
|
-
provider
|
|
1632
|
+
provider,
|
|
1516
1633
|
inputTokens: breakdown.inputTokens,
|
|
1517
1634
|
cachedInputTokens: breakdown.cachedInputTokens,
|
|
1518
1635
|
outputTokens: breakdown.outputTokens,
|
|
@@ -1525,8 +1642,9 @@ function readStandaloneSessions(sessionFiles, options) {
|
|
|
1525
1642
|
estimatedCostUsd: void 0,
|
|
1526
1643
|
messageCount: breakdown.messageCount,
|
|
1527
1644
|
rawTokenTotal: breakdown.totalTokens || void 0,
|
|
1528
|
-
warnings: [...repo.warnings, ...breakdown.warnings
|
|
1645
|
+
warnings: [...repo.warnings, ...breakdown.warnings],
|
|
1529
1646
|
codexHome: options.codexHome ?? path4.join(os2.homedir(), ".codex"),
|
|
1647
|
+
durationMs: durationMs(breakdown.startedAt, breakdown.endedAt),
|
|
1530
1648
|
rawEventCount: breakdown.rawEventCount,
|
|
1531
1649
|
parseStatus: breakdown.parseStatus,
|
|
1532
1650
|
parseErrors: breakdown.parseErrors,
|
|
@@ -1550,10 +1668,90 @@ function readStandaloneSessions(sessionFiles, options) {
|
|
|
1550
1668
|
fileReadCount: breakdown.fileReadCount,
|
|
1551
1669
|
fileEditCount: breakdown.fileEditCount,
|
|
1552
1670
|
promptTimeline: breakdown.promptTimeline,
|
|
1553
|
-
sessionOutcome: inferOutcome(breakdown)
|
|
1671
|
+
sessionOutcome: inferOutcome(breakdown),
|
|
1672
|
+
compaction: breakdown.compaction
|
|
1673
|
+
};
|
|
1674
|
+
const hasPricedBreakdown = breakdown.inputTokens + breakdown.cachedInputTokens + breakdown.outputTokens + breakdown.reasoningTokens > 0;
|
|
1675
|
+
const cost = hasPricedBreakdown ? calculateCostUsd(usage, options.pricing) : void 0;
|
|
1676
|
+
return {
|
|
1677
|
+
...usage,
|
|
1678
|
+
estimatedCostUsd: cost,
|
|
1679
|
+
warnings: cost === void 0 ? [...usage.warnings, hasPricedBreakdown ? "unknown_pricing" : "missing_token_breakdown"] : usage.warnings
|
|
1554
1680
|
};
|
|
1555
1681
|
});
|
|
1556
1682
|
}
|
|
1683
|
+
function hasImportedSession(sessions, candidate) {
|
|
1684
|
+
const candidatePath = comparableSourcePath(candidate.sourcePath);
|
|
1685
|
+
return sessions.some((session) => {
|
|
1686
|
+
if (session.id && session.id === candidate.id) return true;
|
|
1687
|
+
const sessionPath = comparableSourcePath(session.sourcePath);
|
|
1688
|
+
return Boolean(candidatePath && sessionPath && candidatePath === sessionPath);
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
function readImportedModelMap(codexHome, warnings) {
|
|
1692
|
+
const importsPath = path4.join(codexHome, "external_agent_session_imports.json");
|
|
1693
|
+
const models = /* @__PURE__ */ new Map();
|
|
1694
|
+
if (!fs4.existsSync(importsPath)) return models;
|
|
1695
|
+
let parsed;
|
|
1696
|
+
try {
|
|
1697
|
+
parsed = JSON.parse(fs4.readFileSync(importsPath, "utf8"));
|
|
1698
|
+
} catch (error) {
|
|
1699
|
+
warnings.push(`Unable to read Codex external import index at ${importsPath}: ${errorMessage(error)}`);
|
|
1700
|
+
return models;
|
|
1701
|
+
}
|
|
1702
|
+
const records = Array.isArray(asRecord(parsed)?.records) ? asRecord(parsed)?.records : [];
|
|
1703
|
+
for (const record of records) {
|
|
1704
|
+
const item = asRecord(record);
|
|
1705
|
+
const importedThreadId = stringValue2(item?.imported_thread_id);
|
|
1706
|
+
const sourcePath = normalizeVerbatimPath(stringValue2(item?.source_path));
|
|
1707
|
+
if (!importedThreadId || !sourcePath || !fs4.existsSync(sourcePath)) continue;
|
|
1708
|
+
const model = readImportedSourceModel(sourcePath, warnings);
|
|
1709
|
+
if (model) models.set(importedThreadId, { model, provider: providerForModel(model) });
|
|
1710
|
+
}
|
|
1711
|
+
return models;
|
|
1712
|
+
}
|
|
1713
|
+
function readImportedSourceModel(sourcePath, warnings) {
|
|
1714
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1715
|
+
let content;
|
|
1716
|
+
try {
|
|
1717
|
+
content = fs4.readFileSync(sourcePath, "utf8");
|
|
1718
|
+
} catch (error) {
|
|
1719
|
+
warnings.push(`Unable to read Codex external import source at ${sourcePath}: ${errorMessage(error)}`);
|
|
1720
|
+
return void 0;
|
|
1721
|
+
}
|
|
1722
|
+
for (const [index, line] of content.split(/\r?\n/).entries()) {
|
|
1723
|
+
if (!line.trim()) continue;
|
|
1724
|
+
try {
|
|
1725
|
+
const model = modelFromImportedRecord(JSON.parse(line));
|
|
1726
|
+
if (model) counts.set(model, (counts.get(model) ?? 0) + 1);
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
warnings.push(`Unable to parse Codex external import source ${sourcePath} line ${index + 1}: ${errorMessage(error)}`);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0]?.[0];
|
|
1732
|
+
}
|
|
1733
|
+
function modelFromImportedRecord(record) {
|
|
1734
|
+
const object = asRecord(record);
|
|
1735
|
+
const type = stringValue2(object?.type) ?? stringValue2(asRecord(object?.payload)?.type);
|
|
1736
|
+
const message = asRecord(object?.message) ?? asRecord(asRecord(object?.payload)?.message);
|
|
1737
|
+
const model = stringValue2(message?.model) ?? stringValue2(object?.model);
|
|
1738
|
+
if (!model || model === "<synthetic>") return void 0;
|
|
1739
|
+
if (type && type !== "assistant") return void 0;
|
|
1740
|
+
return model;
|
|
1741
|
+
}
|
|
1742
|
+
function providerForModel(model) {
|
|
1743
|
+
const normalized = model.toLowerCase();
|
|
1744
|
+
if (!normalized) return void 0;
|
|
1745
|
+
if (normalized.startsWith("claude")) return "anthropic";
|
|
1746
|
+
if (normalized.startsWith("gpt") || normalized.startsWith("o")) return "openai";
|
|
1747
|
+
return void 0;
|
|
1748
|
+
}
|
|
1749
|
+
function normalizeVerbatimPath(value) {
|
|
1750
|
+
return value?.replace(/^\\\\\?\\/, "");
|
|
1751
|
+
}
|
|
1752
|
+
function comparableSourcePath(value) {
|
|
1753
|
+
return normalizeVerbatimPath(value)?.replace(/\\/g, "/");
|
|
1754
|
+
}
|
|
1557
1755
|
function indexSessionFiles(sessionsPath, warnings) {
|
|
1558
1756
|
const files = /* @__PURE__ */ new Map();
|
|
1559
1757
|
try {
|
|
@@ -1588,11 +1786,16 @@ function readSessionBreakdown(filePath) {
|
|
|
1588
1786
|
breakdown.parseStatus = parse.errors.length ? records.length ? "partial" : "failed" : "ok";
|
|
1589
1787
|
let eventMessageCount = 0;
|
|
1590
1788
|
let responseMessageCount = 0;
|
|
1789
|
+
let lastUserMessageText;
|
|
1591
1790
|
for (const record of records) {
|
|
1791
|
+
applySessionBounds(breakdown, record);
|
|
1592
1792
|
applySessionMetadata(breakdown, record);
|
|
1593
1793
|
applyPromptTimeline(breakdown, record);
|
|
1594
1794
|
if (isCodexEventMessage(record)) eventMessageCount += 1;
|
|
1595
1795
|
if (isResponseMessage(record)) responseMessageCount += 1;
|
|
1796
|
+
const userText = extractUserMessageText(record);
|
|
1797
|
+
if (userText !== void 0) lastUserMessageText = userText;
|
|
1798
|
+
applyCompactionEvent(breakdown, record, lastUserMessageText);
|
|
1596
1799
|
}
|
|
1597
1800
|
breakdown.messageCount = eventMessageCount || responseMessageCount;
|
|
1598
1801
|
const tokenAggregation = aggregateTokens(records);
|
|
@@ -1668,6 +1871,13 @@ function matchSessionPath(id, sessionFiles) {
|
|
|
1668
1871
|
}
|
|
1669
1872
|
function emptyBreakdown() {
|
|
1670
1873
|
return {
|
|
1874
|
+
id: void 0,
|
|
1875
|
+
cwd: void 0,
|
|
1876
|
+
title: void 0,
|
|
1877
|
+
startedAt: void 0,
|
|
1878
|
+
endedAt: void 0,
|
|
1879
|
+
model: void 0,
|
|
1880
|
+
provider: void 0,
|
|
1671
1881
|
inputTokens: 0,
|
|
1672
1882
|
cachedInputTokens: 0,
|
|
1673
1883
|
outputTokens: 0,
|
|
@@ -1701,7 +1911,8 @@ function emptyBreakdown() {
|
|
|
1701
1911
|
commandIssueSamples: [],
|
|
1702
1912
|
fileReadCount: 0,
|
|
1703
1913
|
fileEditCount: 0,
|
|
1704
|
-
promptTimeline: []
|
|
1914
|
+
promptTimeline: [],
|
|
1915
|
+
compaction: { count: 0, autoCount: 0, manualCount: 0, events: [] }
|
|
1705
1916
|
};
|
|
1706
1917
|
}
|
|
1707
1918
|
function fallbackCwdFromPath(filePath) {
|
|
@@ -1711,10 +1922,26 @@ function applySessionMetadata(breakdown, record) {
|
|
|
1711
1922
|
if (!record || typeof record !== "object") return;
|
|
1712
1923
|
const object = record;
|
|
1713
1924
|
const payload = firstObject3(object.payload) ?? object;
|
|
1925
|
+
const collaborationMode = firstObject3(payload.collaboration_mode);
|
|
1926
|
+
const collaborationSettings = firstObject3(collaborationMode?.settings);
|
|
1927
|
+
breakdown.id ??= stringValue2(payload.id);
|
|
1928
|
+
breakdown.cwd ??= stringValue2(payload.cwd);
|
|
1929
|
+
breakdown.title ??= stringValue2(payload.title);
|
|
1930
|
+
breakdown.model ??= stringValue2(payload.model) ?? stringValue2(collaborationSettings?.model);
|
|
1931
|
+
breakdown.provider ??= stringValue2(payload.model_provider);
|
|
1714
1932
|
breakdown.source ??= stringValue2(payload.source);
|
|
1715
1933
|
breakdown.originator ??= stringValue2(payload.originator);
|
|
1716
1934
|
breakdown.threadSource ??= stringValue2(payload.thread_source);
|
|
1717
1935
|
}
|
|
1936
|
+
function applySessionBounds(breakdown, record) {
|
|
1937
|
+
if (!record || typeof record !== "object") return;
|
|
1938
|
+
const object = record;
|
|
1939
|
+
const payload = firstObject3(object.payload);
|
|
1940
|
+
const timestamp3 = dateValue2(object.timestamp) ?? dateValue2(payload?.timestamp);
|
|
1941
|
+
const sessionStartedAt = object.type === "session_meta" ? dateValue2(payload?.timestamp) ?? timestamp3 : timestamp3;
|
|
1942
|
+
if (sessionStartedAt && (!breakdown.startedAt || sessionStartedAt < breakdown.startedAt)) breakdown.startedAt = sessionStartedAt;
|
|
1943
|
+
if (timestamp3 && (!breakdown.endedAt || timestamp3 > breakdown.endedAt)) breakdown.endedAt = timestamp3;
|
|
1944
|
+
}
|
|
1718
1945
|
function isCodexEventMessage(record) {
|
|
1719
1946
|
if (!record || typeof record !== "object") return false;
|
|
1720
1947
|
const object = record;
|
|
@@ -1729,6 +1956,35 @@ function isResponseMessage(record) {
|
|
|
1729
1956
|
const payload = firstObject3(object.payload);
|
|
1730
1957
|
return payload?.type === "message" && (payload.role === "user" || payload.role === "assistant");
|
|
1731
1958
|
}
|
|
1959
|
+
function extractUserMessageText(record) {
|
|
1960
|
+
if (!record || typeof record !== "object") return void 0;
|
|
1961
|
+
const object = record;
|
|
1962
|
+
const payload = firstObject3(object.payload) ?? object;
|
|
1963
|
+
const payloadType = stringValue2(payload.type);
|
|
1964
|
+
const roleValue = stringValue2(payload.role);
|
|
1965
|
+
const isUser = payloadType === "user_message" || object.type === "response_item" && payloadType === "message" && roleValue === "user";
|
|
1966
|
+
if (!isUser) return void 0;
|
|
1967
|
+
return extractMessageText2(payload) ?? extractMessageText2(object);
|
|
1968
|
+
}
|
|
1969
|
+
function applyCompactionEvent(breakdown, record, lastUserMessageText) {
|
|
1970
|
+
if (!record || typeof record !== "object") return;
|
|
1971
|
+
const object = record;
|
|
1972
|
+
if (object.type !== "event_msg") return;
|
|
1973
|
+
const payload = firstObject3(object.payload);
|
|
1974
|
+
if (payload?.type !== "context_compacted") return;
|
|
1975
|
+
const trigger = isManualCompactCommand(lastUserMessageText) ? "manual" : "auto";
|
|
1976
|
+
breakdown.compaction.count += 1;
|
|
1977
|
+
if (trigger === "manual") breakdown.compaction.manualCount += 1;
|
|
1978
|
+
else breakdown.compaction.autoCount += 1;
|
|
1979
|
+
breakdown.compaction.events.push({
|
|
1980
|
+
timestamp: dateValue2(object.timestamp) ?? dateValue2(payload.timestamp),
|
|
1981
|
+
trigger
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
function isManualCompactCommand(text) {
|
|
1985
|
+
if (!text) return false;
|
|
1986
|
+
return /^\s*\/compact\b/.test(text);
|
|
1987
|
+
}
|
|
1732
1988
|
function applyPromptTimeline(breakdown, record) {
|
|
1733
1989
|
if (breakdown.promptTimeline.length >= 80 || !record || typeof record !== "object") return;
|
|
1734
1990
|
const object = record;
|
|
@@ -1737,15 +1993,19 @@ function applyPromptTimeline(breakdown, record) {
|
|
|
1737
1993
|
const roleValue = stringValue2(payload.role);
|
|
1738
1994
|
const role = payloadType === "user_message" || roleValue === "user" ? "user" : payloadType === "agent_message" || roleValue === "assistant" ? "assistant" : void 0;
|
|
1739
1995
|
if (!role) return;
|
|
1740
|
-
const text =
|
|
1996
|
+
const text = extractMessageText2(payload) ?? extractMessageText2(object);
|
|
1741
1997
|
if (!text) return;
|
|
1742
|
-
|
|
1998
|
+
const timelineText = compactTimelineText(text);
|
|
1999
|
+
if (!timelineText || isCodexStartupContext2(timelineText)) return;
|
|
2000
|
+
const item = {
|
|
1743
2001
|
role,
|
|
1744
|
-
text: normalizeTimelineText(
|
|
1745
|
-
timestamp:
|
|
1746
|
-
}
|
|
2002
|
+
text: normalizeTimelineText(timelineText),
|
|
2003
|
+
timestamp: dateValue2(payload.timestamp) ?? dateValue2(object.timestamp) ?? dateValue2(payload.created_at) ?? dateValue2(object.created_at)
|
|
2004
|
+
};
|
|
2005
|
+
if (hasNearbyDuplicatePromptTimelineItem(breakdown.promptTimeline, item)) return;
|
|
2006
|
+
breakdown.promptTimeline.push(item);
|
|
1747
2007
|
}
|
|
1748
|
-
function
|
|
2008
|
+
function extractMessageText2(record) {
|
|
1749
2009
|
for (const key of ["text", "message", "content", "input"]) {
|
|
1750
2010
|
const value = record[key];
|
|
1751
2011
|
if (typeof value === "string" && value.trim()) return value;
|
|
@@ -1763,12 +2023,33 @@ function extractMessageText(record) {
|
|
|
1763
2023
|
}
|
|
1764
2024
|
return void 0;
|
|
1765
2025
|
}
|
|
2026
|
+
function compactTimelineText(text) {
|
|
2027
|
+
return text.replace(/\s+/g, " ").trim();
|
|
2028
|
+
}
|
|
1766
2029
|
function normalizeTimelineText(text) {
|
|
1767
|
-
return text
|
|
2030
|
+
return compactTimelineText(text).slice(0, 1e3);
|
|
2031
|
+
}
|
|
2032
|
+
function isCodexStartupContext2(text) {
|
|
2033
|
+
return text.includes("<environment_context>") && (text.includes("# AGENTS.md instructions") || text.includes("<INSTRUCTIONS>"));
|
|
2034
|
+
}
|
|
2035
|
+
function hasNearbyDuplicatePromptTimelineItem(items, item) {
|
|
2036
|
+
return items.some((existing) => {
|
|
2037
|
+
if (existing.role !== item.role || existing.text !== item.text) return false;
|
|
2038
|
+
if (!existing.timestamp || !item.timestamp) return true;
|
|
2039
|
+
const existingTime = new Date(existing.timestamp).getTime();
|
|
2040
|
+
const itemTime = new Date(item.timestamp).getTime();
|
|
2041
|
+
if (!Number.isFinite(existingTime) || !Number.isFinite(itemTime)) return false;
|
|
2042
|
+
return Math.abs(existingTime - itemTime) <= 5e3;
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
function codexSourceRaw(source, sessionSource, originator, threadSource) {
|
|
2046
|
+
if (originator && /codex\s+desktop/i.test(originator)) return originator;
|
|
2047
|
+
return source ?? sessionSource ?? originator ?? threadSource;
|
|
1768
2048
|
}
|
|
1769
2049
|
function detectSurface(source, threadSource) {
|
|
1770
2050
|
const raw = `${source ?? ""} ${threadSource ?? ""}`.toLowerCase();
|
|
1771
2051
|
if (threadSource === "subagent") return { surface: "codex_exec", confidence: "medium", reason: "thread_source is subagent" };
|
|
2052
|
+
if (raw.includes("codex desktop")) return { surface: "local_agent", confidence: "high", reason: "originator metadata identifies Codex Desktop" };
|
|
1772
2053
|
if (raw.includes("vscode")) return { surface: "vscode_extension", confidence: "high", reason: "source metadata includes vscode" };
|
|
1773
2054
|
if (raw.includes("cli") || raw.includes("terminal") || raw.includes("tui")) return { surface: "terminal_cli", confidence: "high", reason: "source metadata indicates CLI/terminal" };
|
|
1774
2055
|
if (raw.includes("exec")) return { surface: "codex_exec", confidence: "medium", reason: "source metadata includes exec" };
|
|
@@ -1792,16 +2073,20 @@ function durationMs(startedAt, endedAt) {
|
|
|
1792
2073
|
function stringValue2(value) {
|
|
1793
2074
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1794
2075
|
}
|
|
2076
|
+
function asRecord(value) {
|
|
2077
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2078
|
+
}
|
|
1795
2079
|
function codexSourceAppLabel(source, threadSource) {
|
|
1796
|
-
if (threadSource === "subagent") return "Codex
|
|
2080
|
+
if (threadSource === "subagent") return "Codex subagent";
|
|
1797
2081
|
if (!source) return "Codex";
|
|
2082
|
+
if (/codex\s+desktop/i.test(source)) return "Codex Desktop";
|
|
1798
2083
|
if (source === "vscode" || source === "codex_vscode" || source === "codex-vscode") return "VS Code";
|
|
1799
2084
|
if (source === "cli" || source === "terminal" || source === "codex-tui") return "Terminal";
|
|
1800
2085
|
if (source === "codex_app" || source === "codex-app") return "Codex app";
|
|
1801
|
-
if (source.trim().startsWith("{")) return threadSource === "subagent" ? "Codex
|
|
2086
|
+
if (source.trim().startsWith("{")) return threadSource === "subagent" ? "Codex subagent" : "Codex";
|
|
1802
2087
|
return source.replace(/^codex[_-]/, "").split(/[_-]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
1803
2088
|
}
|
|
1804
|
-
function
|
|
2089
|
+
function dateValue2(value) {
|
|
1805
2090
|
if (typeof value === "number") {
|
|
1806
2091
|
return new Date(value > 1e10 ? value : value * 1e3).toISOString();
|
|
1807
2092
|
}
|
|
@@ -1831,7 +2116,9 @@ function scanClaude(options) {
|
|
|
1831
2116
|
const warnings = [];
|
|
1832
2117
|
const discovery = discoverClaudeSessionFiles(claudeHome, warnings, Boolean(options.claudeHome));
|
|
1833
2118
|
const historyExists = fs5.existsSync(historyPath);
|
|
1834
|
-
const
|
|
2119
|
+
const seenUsageKeys = /* @__PURE__ */ new Set();
|
|
2120
|
+
const orderedFiles = discovery.files.map((file) => ({ file, mtimeMs: safeMtimeMs(file.filePath) })).sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
2121
|
+
const sessions = orderedFiles.map(({ file }, index) => claudeFileToUsage(file, index, options, seenUsageKeys));
|
|
1835
2122
|
const historyEntryCount = countHistoryEntries(historyPath);
|
|
1836
2123
|
const parseFailureCount = sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).length;
|
|
1837
2124
|
const unreadableFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_session_file:"))).length;
|
|
@@ -1937,20 +2224,21 @@ function walk2(root, maxDepth, onFile, depth = 0) {
|
|
|
1937
2224
|
}
|
|
1938
2225
|
}
|
|
1939
2226
|
}
|
|
1940
|
-
function claudeFileToUsage(file, index, options) {
|
|
1941
|
-
const breakdown = readClaudeBreakdown(file.filePath);
|
|
2227
|
+
function claudeFileToUsage(file, index, options, seenUsageKeys) {
|
|
2228
|
+
const breakdown = readClaudeBreakdown(file.filePath, seenUsageKeys);
|
|
1942
2229
|
const id = breakdown.id ?? (path5.basename(file.filePath).replace(/\.jsonl$/i, "") || `claude-session-${index}`);
|
|
1943
2230
|
const fallbackCwd = inferProjectPath(file.projectDir, file.projectsPath);
|
|
1944
2231
|
const cwd = breakdown.cwd ?? fallbackCwd;
|
|
1945
2232
|
const repo = resolveRepoInfo(cwd, options.config);
|
|
1946
2233
|
const warnings = [...repo.warnings, ...breakdown.warnings];
|
|
2234
|
+
const entrypoint = breakdown.entrypoint ?? inferClaudeEntrypointFromPath(file.filePath);
|
|
1947
2235
|
if (!breakdown.cwd) warnings.push("repo_inferred_from_claude_project_dir");
|
|
1948
2236
|
const hasTokenBreakdown = breakdown.totalTokens > 0 && breakdown.usageCount > 0;
|
|
1949
2237
|
const usage = {
|
|
1950
2238
|
id,
|
|
1951
2239
|
sourceClient: "claude",
|
|
1952
|
-
sourceApp: claudeSourceAppLabel(
|
|
1953
|
-
sourceAppRaw:
|
|
2240
|
+
sourceApp: claudeSourceAppLabel(entrypoint),
|
|
2241
|
+
sourceAppRaw: entrypoint,
|
|
1954
2242
|
sourcePath: file.filePath,
|
|
1955
2243
|
repoRoot: repo.repoRoot,
|
|
1956
2244
|
repoName: repo.repoName,
|
|
@@ -1980,9 +2268,9 @@ function claudeFileToUsage(file, index, options) {
|
|
|
1980
2268
|
rawEventCount: breakdown.rawEventCount,
|
|
1981
2269
|
parseStatus: breakdown.parseStatus,
|
|
1982
2270
|
parseErrors: breakdown.parseErrors,
|
|
1983
|
-
detectedSurface: claudeSurface(
|
|
2271
|
+
detectedSurface: claudeSurface(entrypoint),
|
|
1984
2272
|
surfaceConfidence: "medium",
|
|
1985
|
-
surfaceReason:
|
|
2273
|
+
surfaceReason: entrypoint ? `Claude Code entrypoint: ${entrypoint}` : "Claude Code transcript",
|
|
1986
2274
|
userPromptCount: breakdown.userPromptCount,
|
|
1987
2275
|
assistantMessageCount: breakdown.assistantMessageCount,
|
|
1988
2276
|
toolCallCount: breakdown.toolCallCount,
|
|
@@ -2009,7 +2297,10 @@ function claudeFileToUsage(file, index, options) {
|
|
|
2009
2297
|
warnings: cost === void 0 ? [...usage.warnings, "unknown_pricing"] : usage.warnings
|
|
2010
2298
|
};
|
|
2011
2299
|
}
|
|
2012
|
-
function
|
|
2300
|
+
function inferClaudeEntrypointFromPath(filePath) {
|
|
2301
|
+
return filePath.split(path5.sep).includes("local-agent-mode-sessions") ? "local-agent" : void 0;
|
|
2302
|
+
}
|
|
2303
|
+
function readClaudeBreakdown(filePath, seenUsageKeys) {
|
|
2013
2304
|
const breakdown = emptyBreakdown2();
|
|
2014
2305
|
try {
|
|
2015
2306
|
const text = fs5.readFileSync(filePath, "utf8");
|
|
@@ -2018,7 +2309,7 @@ function readClaudeBreakdown(filePath) {
|
|
|
2018
2309
|
breakdown.parseErrors = parse.errors;
|
|
2019
2310
|
breakdown.parseStatus = parse.errors.length ? parse.records.length ? "partial" : "failed" : "ok";
|
|
2020
2311
|
for (const record of parse.records) {
|
|
2021
|
-
applyClaudeRecord(breakdown, record);
|
|
2312
|
+
applyClaudeRecord(breakdown, record, seenUsageKeys);
|
|
2022
2313
|
}
|
|
2023
2314
|
breakdown.totalTokens = breakdown.inputTokens + breakdown.outputTokens;
|
|
2024
2315
|
} catch (error) {
|
|
@@ -2029,8 +2320,8 @@ function readClaudeBreakdown(filePath) {
|
|
|
2029
2320
|
}
|
|
2030
2321
|
return breakdown;
|
|
2031
2322
|
}
|
|
2032
|
-
function applyClaudeRecord(breakdown, record) {
|
|
2033
|
-
const timestamp3 =
|
|
2323
|
+
function applyClaudeRecord(breakdown, record, seenUsageKeys) {
|
|
2324
|
+
const timestamp3 = dateValue3(record.timestamp);
|
|
2034
2325
|
if (timestamp3) {
|
|
2035
2326
|
breakdown.startedAt = minDate(breakdown.startedAt, timestamp3);
|
|
2036
2327
|
breakdown.endedAt = maxDate(breakdown.endedAt, timestamp3);
|
|
@@ -2052,11 +2343,22 @@ function applyClaudeRecord(breakdown, record) {
|
|
|
2052
2343
|
const usageTokens = usage ? usageTotal(usage) : 0;
|
|
2053
2344
|
const model = normalizeClaudeModel(message.model);
|
|
2054
2345
|
if (model && (usageTokens > 0 || !breakdown.model)) breakdown.model = model;
|
|
2055
|
-
if (usage && usageTokens > 0
|
|
2346
|
+
if (usage && usageTokens > 0 && !alreadySeenUsage(record, message, seenUsageKeys)) {
|
|
2347
|
+
applyUsage(breakdown, usage);
|
|
2348
|
+
}
|
|
2056
2349
|
applyContentSignals(breakdown, message.content);
|
|
2057
2350
|
}
|
|
2058
2351
|
if (looksFailedToolResult(record.toolUseResult)) breakdown.failedToolCallCount += 1;
|
|
2059
2352
|
}
|
|
2353
|
+
function alreadySeenUsage(record, message, seenUsageKeys) {
|
|
2354
|
+
const messageId = stringValue3(message.id);
|
|
2355
|
+
const requestId = stringValue3(record.requestId);
|
|
2356
|
+
if (!messageId || !requestId) return false;
|
|
2357
|
+
const key = `${requestId}:${messageId}`;
|
|
2358
|
+
if (seenUsageKeys.has(key)) return true;
|
|
2359
|
+
seenUsageKeys.add(key);
|
|
2360
|
+
return false;
|
|
2361
|
+
}
|
|
2060
2362
|
function applyUsage(breakdown, usage) {
|
|
2061
2363
|
const input = numberValue(usage.input_tokens);
|
|
2062
2364
|
const cacheRead = numberValue(usage.cache_read_input_tokens);
|
|
@@ -2174,13 +2476,13 @@ function inferOutcome2(breakdown) {
|
|
|
2174
2476
|
function claudeSurface(entrypoint) {
|
|
2175
2477
|
const value = entrypoint?.toLowerCase() ?? "";
|
|
2176
2478
|
if (value.includes("vscode")) return "vscode_extension";
|
|
2177
|
-
if (value.includes("local-agent")) return "local_agent";
|
|
2479
|
+
if (value.includes("local-agent") || value.includes("desktop")) return "local_agent";
|
|
2178
2480
|
return "terminal_cli";
|
|
2179
2481
|
}
|
|
2180
2482
|
function claudeSourceAppLabel(entrypoint) {
|
|
2181
2483
|
const value = entrypoint?.toLowerCase() ?? "";
|
|
2182
2484
|
if (value.includes("vscode")) return "VS Code";
|
|
2183
|
-
if (value.includes("local-agent")) return "Claude
|
|
2485
|
+
if (value.includes("local-agent") || value.includes("desktop")) return "Claude Desktop App";
|
|
2184
2486
|
if (value === "cli" || value.includes("terminal")) return "Terminal";
|
|
2185
2487
|
return "Claude Code";
|
|
2186
2488
|
}
|
|
@@ -2240,7 +2542,7 @@ function normalizeClaudeModel(value) {
|
|
|
2240
2542
|
if (!model || model === "<synthetic>") return void 0;
|
|
2241
2543
|
return model;
|
|
2242
2544
|
}
|
|
2243
|
-
function
|
|
2545
|
+
function dateValue3(value) {
|
|
2244
2546
|
if (typeof value !== "string") return void 0;
|
|
2245
2547
|
const date = new Date(value);
|
|
2246
2548
|
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
@@ -2269,6 +2571,13 @@ function looksFailedToolResult(value) {
|
|
|
2269
2571
|
function errorMessage2(error) {
|
|
2270
2572
|
return error instanceof Error ? error.message : String(error);
|
|
2271
2573
|
}
|
|
2574
|
+
function safeMtimeMs(filePath) {
|
|
2575
|
+
try {
|
|
2576
|
+
return fs5.statSync(filePath).mtimeMs;
|
|
2577
|
+
} catch {
|
|
2578
|
+
return 0;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2272
2581
|
|
|
2273
2582
|
// packages/core/src/sources/index.ts
|
|
2274
2583
|
function scanUsageSources(options) {
|
|
@@ -2293,9 +2602,9 @@ function scanUsageSources(options) {
|
|
|
2293
2602
|
import fs6 from "node:fs";
|
|
2294
2603
|
var scanCacheTtlMs = 1e4;
|
|
2295
2604
|
var scanCache;
|
|
2296
|
-
function readDashboardData(filters = {}) {
|
|
2605
|
+
function readDashboardData(filters = {}, options = {}) {
|
|
2297
2606
|
const raw = readRawUsageData();
|
|
2298
|
-
return buildDashboardSnapshot({ ...raw, filters });
|
|
2607
|
+
return buildDashboardSnapshot({ ...raw, sessions: displaySessions(raw.sessions, options), filters });
|
|
2299
2608
|
}
|
|
2300
2609
|
function clearScanCache() {
|
|
2301
2610
|
scanCache = void 0;
|
|
@@ -2332,6 +2641,11 @@ function parseFilters(query) {
|
|
|
2332
2641
|
if (to) filters.to = to;
|
|
2333
2642
|
return filters;
|
|
2334
2643
|
}
|
|
2644
|
+
function parseDashboardOptions(query) {
|
|
2645
|
+
return {
|
|
2646
|
+
splitSourceApps: query.splitSourceApps === "true" || query.splitSourceApps === "1"
|
|
2647
|
+
};
|
|
2648
|
+
}
|
|
2335
2649
|
function readPricingData() {
|
|
2336
2650
|
const config = loadConfig();
|
|
2337
2651
|
const pricingPath = resolvePricingPath(config);
|
|
@@ -2371,6 +2685,39 @@ function listQuery(value) {
|
|
|
2371
2685
|
const parsed = values.flatMap((item) => typeof item === "string" ? item.split(",") : []).map((item) => item.trim()).filter(Boolean);
|
|
2372
2686
|
return parsed.length ? parsed : void 0;
|
|
2373
2687
|
}
|
|
2688
|
+
function displaySessions(sessions, options) {
|
|
2689
|
+
if (!options.splitSourceApps) return sessions;
|
|
2690
|
+
return sessions.map((session) => {
|
|
2691
|
+
const sourceApp = sourceScopedAppLabel(session);
|
|
2692
|
+
return sourceApp === session.sourceApp ? session : { ...session, sourceApp };
|
|
2693
|
+
});
|
|
2694
|
+
}
|
|
2695
|
+
function sourceScopedAppLabel(session) {
|
|
2696
|
+
const app = session.sourceApp || fallbackSurfaceLabel(session.detectedSurface);
|
|
2697
|
+
if (!app || app === "Unknown") return sourceClientLabel(session.sourceClient);
|
|
2698
|
+
if (!shouldScopeApp(app)) return app;
|
|
2699
|
+
return `${sourceClientLabel(session.sourceClient)} on ${app}`;
|
|
2700
|
+
}
|
|
2701
|
+
function shouldScopeApp(app) {
|
|
2702
|
+
const normalized = app.toLowerCase();
|
|
2703
|
+
return normalized.includes("vs code") || normalized.includes("vscode") || normalized.includes("terminal") || normalized.includes("cli");
|
|
2704
|
+
}
|
|
2705
|
+
function sourceClientLabel(source) {
|
|
2706
|
+
if (source === "codex") return "Codex";
|
|
2707
|
+
if (source === "claude") return "Claude Code";
|
|
2708
|
+
if (source === "gemini-cli") return "Gemini CLI";
|
|
2709
|
+
if (source === "opencode") return "OpenCode";
|
|
2710
|
+
if (source === "cursor") return "Cursor";
|
|
2711
|
+
return "Unknown";
|
|
2712
|
+
}
|
|
2713
|
+
function fallbackSurfaceLabel(surface) {
|
|
2714
|
+
if (surface === "terminal_cli") return "Terminal";
|
|
2715
|
+
if (surface === "vscode_extension") return "VS Code";
|
|
2716
|
+
if (surface === "local_agent") return "Desktop local agent";
|
|
2717
|
+
if (surface === "codex_exec") return "Codex";
|
|
2718
|
+
if (surface === "codex_app_cloud") return "Codex app";
|
|
2719
|
+
return "Unknown";
|
|
2720
|
+
}
|
|
2374
2721
|
function runtimeCwd() {
|
|
2375
2722
|
return process.env.INIT_CWD || process.cwd();
|
|
2376
2723
|
}
|
|
@@ -2387,17 +2734,17 @@ var baseDate = "2026-05-18";
|
|
|
2387
2734
|
var demoPricing = {
|
|
2388
2735
|
...defaultPricing,
|
|
2389
2736
|
"gpt-5.3-codex": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
|
|
2390
|
-
"gpt-5.3-codex-spark": { inputPerMillion:
|
|
2737
|
+
"gpt-5.3-codex-spark": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
|
|
2391
2738
|
"claude-haiku-4-5-20251001": { inputPerMillion: 1, cacheCreationInputPerMillion: 1.25, cachedInputPerMillion: 0.1, outputPerMillion: 5 }
|
|
2392
2739
|
};
|
|
2393
2740
|
function demoModeEnabled() {
|
|
2394
2741
|
return process.env.REPOSPEND_DEMO_DATA === "lotr";
|
|
2395
2742
|
}
|
|
2396
|
-
function readDemoDashboardData(filters = {}) {
|
|
2743
|
+
function readDemoDashboardData(filters = {}, options = {}) {
|
|
2397
2744
|
return buildDashboardSnapshot({
|
|
2398
2745
|
sources: demoSources,
|
|
2399
2746
|
sourceStats: demoSourceStats,
|
|
2400
|
-
sessions: demoSessions,
|
|
2747
|
+
sessions: displaySessions(demoSessions, options),
|
|
2401
2748
|
filters
|
|
2402
2749
|
});
|
|
2403
2750
|
}
|
|
@@ -2655,7 +3002,7 @@ var demoInputs = [
|
|
|
2655
3002
|
repo: "rivendell-dashboard",
|
|
2656
3003
|
title: "Refactor the council-of-elrond planner",
|
|
2657
3004
|
sourceClient: "claude",
|
|
2658
|
-
sourceApp: "Claude
|
|
3005
|
+
sourceApp: "Claude Desktop App",
|
|
2659
3006
|
sourceAppRaw: "claude-desktop-local-agent",
|
|
2660
3007
|
model: "claude-sonnet-4-6",
|
|
2661
3008
|
provider: "anthropic",
|
|
@@ -2965,7 +3312,7 @@ function detectedSurfaceForApp(sourceApp) {
|
|
|
2965
3312
|
if (sourceApp === "VS Code") return "vscode_extension";
|
|
2966
3313
|
if (sourceApp === "Terminal") return "terminal_cli";
|
|
2967
3314
|
if (sourceApp === "Codex app") return "codex_app_cloud";
|
|
2968
|
-
if (sourceApp === "Claude
|
|
3315
|
+
if (sourceApp === "Claude Desktop App") return "local_agent";
|
|
2969
3316
|
return "terminal_cli";
|
|
2970
3317
|
}
|
|
2971
3318
|
function timestamp2(hour, minute) {
|
|
@@ -3185,12 +3532,15 @@ function createServer(options = {}) {
|
|
|
3185
3532
|
const app = Fastify({ logger: false });
|
|
3186
3533
|
app.get("/api/health", async () => ({ ok: true, app: "RepoSpend", version: readPackageVersion() }));
|
|
3187
3534
|
app.get("/api/dashboard", async (request) => {
|
|
3188
|
-
const
|
|
3535
|
+
const query = request.query;
|
|
3536
|
+
const filters = parseFilters(query);
|
|
3537
|
+
const dashboardOptions = parseDashboardOptions(query);
|
|
3189
3538
|
const demoMode = demoModeEnabled();
|
|
3190
|
-
const data = demoMode ? readDemoDashboardData(filters) : readDashboardData(filters);
|
|
3539
|
+
const data = demoMode ? readDemoDashboardData(filters, dashboardOptions) : readDashboardData(filters, dashboardOptions);
|
|
3191
3540
|
const rtkGain = demoMode ? readDemoRtkGain() : await readRtkGain();
|
|
3192
3541
|
return {
|
|
3193
3542
|
...data,
|
|
3543
|
+
appVersion: readPackageVersion(),
|
|
3194
3544
|
summary: { ...data.summary, skippedZeroTokenSessions: data.skippedZeroTokenSessions },
|
|
3195
3545
|
pricing: {
|
|
3196
3546
|
info: pricingInfo,
|