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/dist/cli.js CHANGED
@@ -44,10 +44,14 @@ function summarize(sessions) {
44
44
  };
45
45
  }
46
46
  function groupByRepo(sessions) {
47
- return groupBy(sessions, (session) => session.repoRoot, (session) => session.repoName);
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 ?? "Unknown 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 (!matchesAny(session.model, filters.model)) return false;
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
- if (payload.type === "user_message" || role === "user") analysis.userPromptCount += 1;
983
- if (payload.type === "agent_message" || role === "assistant") analysis.assistantMessageCount += 1;
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 = "direct_usage";
1214
- accumulator.tokenConfidence = "medium";
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 (!stateExists && sessionsExists) {
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
- return rows.map((row, index) => threadToUsage(row, index, sessionFiles, options));
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) ?? sessionBreakdown.source ?? sessionBreakdown.originator ?? threadSource;
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: stringValue2(row.id) ?? `codex-thread-${index}`,
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: dateValue(row.created_at),
1442
- endedAt: dateValue(row.updated_at),
1443
- model: stringValue2(row.model),
1444
- provider: stringValue2(row.model_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(dateValue(row.created_at), dateValue(row.updated_at)),
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 = void 0;
1499
- const surface = detectSurface(breakdown.source ?? breakdown.originator, breakdown.threadSource);
1500
- return {
1501
- id,
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(breakdown.source ?? breakdown.originator, breakdown.threadSource),
1504
- sourceAppRaw: breakdown.source ?? breakdown.originator ?? breakdown.threadSource,
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: void 0,
1512
- startedAt: void 0,
1513
- endedAt: void 0,
1628
+ title: breakdown.title,
1629
+ startedAt: breakdown.startedAt,
1630
+ endedAt: breakdown.endedAt,
1514
1631
  model,
1515
- provider: void 0,
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, "unknown_pricing"],
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 = extractMessageText(payload) ?? extractMessageText(object);
1996
+ const text = extractMessageText2(payload) ?? extractMessageText2(object);
1741
1997
  if (!text) return;
1742
- breakdown.promptTimeline.push({
1998
+ const timelineText = compactTimelineText(text);
1999
+ if (!timelineText || isCodexStartupContext2(timelineText)) return;
2000
+ const item = {
1743
2001
  role,
1744
- text: normalizeTimelineText(text),
1745
- timestamp: dateValue(payload.timestamp) ?? dateValue(object.timestamp) ?? dateValue(payload.created_at) ?? dateValue(object.created_at)
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 extractMessageText(record) {
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.replace(/\s+/g, " ").trim().slice(0, 1e3);
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 app";
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 app" : "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 dateValue(value) {
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 sessions = discovery.files.map((file, index) => claudeFileToUsage(file, index, options));
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(breakdown.entrypoint),
1953
- sourceAppRaw: breakdown.entrypoint,
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(breakdown.entrypoint),
2271
+ detectedSurface: claudeSurface(entrypoint),
1984
2272
  surfaceConfidence: "medium",
1985
- surfaceReason: breakdown.entrypoint ? `Claude Code entrypoint: ${breakdown.entrypoint}` : "Claude Code transcript",
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 readClaudeBreakdown(filePath) {
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 = dateValue2(record.timestamp);
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) applyUsage(breakdown, usage);
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 desktop local agent";
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 dateValue2(value) {
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: 0.75, cachedInputPerMillion: 0.075, outputPerMillion: 4.5, reasoningOutputPerMillion: 4.5 },
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 desktop local agent",
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 desktop local agent") return "local_agent";
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 filters = parseFilters(request.query);
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,