repospend 0.0.3 → 0.0.5
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 +14 -0
- package/dist/cli.js +245 -36
- package/docs/PUBLISHING.md +4 -1
- package/package.json +1 -1
- package/web-dist/assets/index-D6eYRahs.css +1 -0
- package/web-dist/assets/{index-B8lKmhuv.js → index-DcroX3EF.js} +60 -60
- package/web-dist/index.html +2 -2
- package/web-dist/assets/index-CUTybR_N.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to RepoSpend will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.0.5
|
|
6
|
+
|
|
7
|
+
- Fix Codex token under-count on long sessions by summing per-turn `last_token_usage` instead of taking the max of `total_token_usage`, which Codex resets on each context compaction.
|
|
8
|
+
- Track Codex context compactions per session (auto vs. manual `/compact`) and surface them in the session detail panel.
|
|
9
|
+
- Filter Codex startup-context blobs (AGENTS.md/INSTRUCTIONS/environment_context) out of the prompt timeline and prompt counts.
|
|
10
|
+
- Dedupe Claude Code token usage across resumed `.jsonl` files using `requestId` + message `id`, so replayed assistant turns are not double-billed.
|
|
11
|
+
- Label Codex sub-agent sessions as "Codex subagent" instead of folding them into "Codex app".
|
|
12
|
+
- Show the running RepoSpend version in the dashboard footer.
|
|
13
|
+
- Publish a GitHub release with the matching `CHANGELOG.md` section whenever a `v*.*.*` tag is pushed.
|
|
14
|
+
|
|
15
|
+
## 0.0.4
|
|
16
|
+
|
|
17
|
+
- Automatically retry Codex SQLite access after attempting to rebuild `better-sqlite3` when an `npx` or local install has a Node native module ABI mismatch.
|
|
18
|
+
|
|
5
19
|
## 0.0.3
|
|
6
20
|
|
|
7
21
|
- Add initial Claude Code support for local JSONL session transcripts, including projects, timestamps, models, token usage when available, and source status.
|
package/dist/cli.js
CHANGED
|
@@ -212,6 +212,8 @@ var defaultPricing = {
|
|
|
212
212
|
"gpt-5.4-mini": { inputPerMillion: 0.75, cachedInputPerMillion: 0.075, outputPerMillion: 4.5, reasoningOutputPerMillion: 4.5 },
|
|
213
213
|
"gpt-5.4-nano": { inputPerMillion: 0.2, cachedInputPerMillion: 0.02, outputPerMillion: 1.25, reasoningOutputPerMillion: 1.25 },
|
|
214
214
|
"gpt-5.4-pro": { inputPerMillion: 30, outputPerMillion: 180, reasoningOutputPerMillion: 180 },
|
|
215
|
+
"gpt-5.3-codex": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
|
|
216
|
+
"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
217
|
"gpt-5.2": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
|
|
216
218
|
"gpt-5.2-chat-latest": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
|
|
217
219
|
"gpt-5.2-codex": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
|
|
@@ -936,16 +938,19 @@ function parseBranch(head) {
|
|
|
936
938
|
}
|
|
937
939
|
|
|
938
940
|
// packages/core/src/sources/codex.ts
|
|
941
|
+
import { spawnSync } from "node:child_process";
|
|
939
942
|
import fs4 from "node:fs";
|
|
940
943
|
import os2 from "node:os";
|
|
941
944
|
import path4 from "node:path";
|
|
945
|
+
import { fileURLToPath } from "node:url";
|
|
942
946
|
import Database from "better-sqlite3";
|
|
943
947
|
|
|
944
948
|
// packages/core/src/sources/agent-friction.ts
|
|
945
949
|
function analyzeAgentFriction(records, totalTokens) {
|
|
946
950
|
const analysis = emptyAgentFrictionAnalysis();
|
|
951
|
+
const messages = [];
|
|
947
952
|
for (const record of records) {
|
|
948
|
-
applyActionMetadata(analysis, record);
|
|
953
|
+
applyActionMetadata(analysis, record, messages);
|
|
949
954
|
}
|
|
950
955
|
finalizeCommandIssues(analysis, totalTokens);
|
|
951
956
|
return analysis;
|
|
@@ -971,14 +976,14 @@ function emptyAgentFrictionAnalysis() {
|
|
|
971
976
|
assistantMessageCount: 0
|
|
972
977
|
};
|
|
973
978
|
}
|
|
974
|
-
function applyActionMetadata(analysis, record) {
|
|
979
|
+
function applyActionMetadata(analysis, record, messages) {
|
|
975
980
|
if (!record || typeof record !== "object") return;
|
|
976
981
|
const object = record;
|
|
977
982
|
const payload = firstObject(object.payload) ?? object;
|
|
978
983
|
const type = stringValue(object.type) ?? stringValue(payload.type);
|
|
979
984
|
const role = stringValue(payload.role);
|
|
980
|
-
|
|
981
|
-
if (
|
|
985
|
+
const messageRole = payload.type === "user_message" || role === "user" ? "user" : payload.type === "agent_message" || role === "assistant" ? "assistant" : void 0;
|
|
986
|
+
if (messageRole) applyMessageCount(analysis, messages, messageRole, payload, object);
|
|
982
987
|
const text = JSON.stringify(record).toLowerCase();
|
|
983
988
|
const isTool = type?.includes("tool") || type?.includes("function_call") || text.includes('"tool_call"') || text.includes('"function_call"');
|
|
984
989
|
if (isTool) analysis.toolCallCount += 1;
|
|
@@ -1138,6 +1143,64 @@ function firstObject(...values) {
|
|
|
1138
1143
|
function stringValue(value) {
|
|
1139
1144
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1140
1145
|
}
|
|
1146
|
+
function applyMessageCount(analysis, messages, role, payload, object) {
|
|
1147
|
+
const message = extractMessageText(payload) ?? extractMessageText(object);
|
|
1148
|
+
if (!message) return;
|
|
1149
|
+
const item = {
|
|
1150
|
+
role,
|
|
1151
|
+
text: compactText(message),
|
|
1152
|
+
timestamp: dateValue(payload.timestamp) ?? dateValue(object.timestamp) ?? dateValue(payload.created_at) ?? dateValue(object.created_at)
|
|
1153
|
+
};
|
|
1154
|
+
if (!item.text || isCodexStartupContext(item.text) || hasNearbyDuplicateMessage(messages, item)) return;
|
|
1155
|
+
messages.push(item);
|
|
1156
|
+
if (role === "user") analysis.userPromptCount += 1;
|
|
1157
|
+
if (role === "assistant") analysis.assistantMessageCount += 1;
|
|
1158
|
+
}
|
|
1159
|
+
function extractMessageText(record) {
|
|
1160
|
+
for (const key of ["text", "message", "content", "input"]) {
|
|
1161
|
+
const value = record[key];
|
|
1162
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
1163
|
+
if (Array.isArray(value)) {
|
|
1164
|
+
const parts = value.map((item) => {
|
|
1165
|
+
if (typeof item === "string") return item;
|
|
1166
|
+
if (item && typeof item === "object") {
|
|
1167
|
+
const object = item;
|
|
1168
|
+
return stringValue(object.text) ?? stringValue(object.content);
|
|
1169
|
+
}
|
|
1170
|
+
return void 0;
|
|
1171
|
+
}).filter((item) => Boolean(item?.trim()));
|
|
1172
|
+
if (parts.length) return parts.join("\n");
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
return void 0;
|
|
1176
|
+
}
|
|
1177
|
+
function compactText(text) {
|
|
1178
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1179
|
+
}
|
|
1180
|
+
function isCodexStartupContext(text) {
|
|
1181
|
+
return text.includes("<environment_context>") && (text.includes("# AGENTS.md instructions") || text.includes("<INSTRUCTIONS>"));
|
|
1182
|
+
}
|
|
1183
|
+
function hasNearbyDuplicateMessage(items, item) {
|
|
1184
|
+
return items.some((existing) => {
|
|
1185
|
+
if (existing.role !== item.role || existing.text !== item.text) return false;
|
|
1186
|
+
if (!existing.timestamp || !item.timestamp) return true;
|
|
1187
|
+
const existingTime = new Date(existing.timestamp).getTime();
|
|
1188
|
+
const itemTime = new Date(item.timestamp).getTime();
|
|
1189
|
+
if (!Number.isFinite(existingTime) || !Number.isFinite(itemTime)) return false;
|
|
1190
|
+
return Math.abs(existingTime - itemTime) <= 5e3;
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
function dateValue(value) {
|
|
1194
|
+
if (typeof value === "number") {
|
|
1195
|
+
if (!Number.isFinite(value)) return void 0;
|
|
1196
|
+
return new Date(value > 1e10 ? value : value * 1e3).toISOString();
|
|
1197
|
+
}
|
|
1198
|
+
if (typeof value === "string") {
|
|
1199
|
+
const date = new Date(value);
|
|
1200
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
1201
|
+
}
|
|
1202
|
+
return void 0;
|
|
1203
|
+
}
|
|
1141
1204
|
|
|
1142
1205
|
// packages/core/src/sources/codex-token.ts
|
|
1143
1206
|
function aggregateTokens(records) {
|
|
@@ -1208,8 +1271,8 @@ function finalizeTokenAccumulator(accumulator) {
|
|
|
1208
1271
|
{ inputTokens: 0, cachedInputTokens: 0, outputTokens: 0, reasoningTokens: 0, totalTokens: 0 }
|
|
1209
1272
|
);
|
|
1210
1273
|
applyFinalUsage(accumulator, total);
|
|
1211
|
-
accumulator.tokenAggregationMethod = "
|
|
1212
|
-
accumulator.tokenConfidence = "
|
|
1274
|
+
accumulator.tokenAggregationMethod = "delta_sum";
|
|
1275
|
+
accumulator.tokenConfidence = "high";
|
|
1213
1276
|
return;
|
|
1214
1277
|
}
|
|
1215
1278
|
accumulator.tokenAggregationMethod = "unknown";
|
|
@@ -1220,6 +1283,10 @@ function extractRecordTokens(record) {
|
|
|
1220
1283
|
const object = record;
|
|
1221
1284
|
const payload = firstObject2(object.payload);
|
|
1222
1285
|
const payloadInfo = firstObject2(payload?.info);
|
|
1286
|
+
const codexLastUsage = firstObject2(payloadInfo?.last_token_usage);
|
|
1287
|
+
if (codexLastUsage) {
|
|
1288
|
+
return tokenUsageFromObject(codexLastUsage, false);
|
|
1289
|
+
}
|
|
1223
1290
|
const codexTotalUsage = firstObject2(payloadInfo?.total_token_usage);
|
|
1224
1291
|
if (codexTotalUsage) {
|
|
1225
1292
|
return tokenUsageFromObject(codexTotalUsage, true);
|
|
@@ -1268,6 +1335,7 @@ function numberFromKeys(object, keys) {
|
|
|
1268
1335
|
}
|
|
1269
1336
|
|
|
1270
1337
|
// packages/core/src/sources/codex.ts
|
|
1338
|
+
var nativeSqliteRebuildAttempted = false;
|
|
1271
1339
|
function scanCodex(options) {
|
|
1272
1340
|
const codexHome = options.codexHome ?? path4.join(os2.homedir(), ".codex");
|
|
1273
1341
|
const statePath = path4.join(codexHome, "state_5.sqlite");
|
|
@@ -1322,7 +1390,7 @@ function scanCodex(options) {
|
|
|
1322
1390
|
function readThreads(statePath, sessionFiles, options, warnings) {
|
|
1323
1391
|
let db;
|
|
1324
1392
|
try {
|
|
1325
|
-
db =
|
|
1393
|
+
db = openCodexStateDatabase(statePath, warnings);
|
|
1326
1394
|
const tableNames = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
|
|
1327
1395
|
if (!tableNames.some((table) => table.name === "threads")) {
|
|
1328
1396
|
warnings.push("Codex SQLite state does not contain a threads table.");
|
|
@@ -1343,6 +1411,70 @@ function readThreads(statePath, sessionFiles, options, warnings) {
|
|
|
1343
1411
|
db?.close();
|
|
1344
1412
|
}
|
|
1345
1413
|
}
|
|
1414
|
+
function openCodexStateDatabase(statePath, warnings) {
|
|
1415
|
+
try {
|
|
1416
|
+
return new Database(statePath, { readonly: true, fileMustExist: true });
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
if (isNativeSqliteMismatch(error) && attemptNativeSqliteRebuild(warnings)) {
|
|
1419
|
+
return new Database(statePath, { readonly: true, fileMustExist: true });
|
|
1420
|
+
}
|
|
1421
|
+
throw error;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
function attemptNativeSqliteRebuild(warnings) {
|
|
1425
|
+
if (nativeSqliteRebuildAttempted || process.env.REPOSPEND_SKIP_NATIVE_REBUILD === "1") return false;
|
|
1426
|
+
nativeSqliteRebuildAttempted = true;
|
|
1427
|
+
const packageRoot = findRepoSpendPackageRoot();
|
|
1428
|
+
if (!packageRoot) {
|
|
1429
|
+
warnings.push("Codex SQLite native module mismatch detected, but RepoSpend could not locate its package root for an automatic rebuild.");
|
|
1430
|
+
return false;
|
|
1431
|
+
}
|
|
1432
|
+
const packageManager = fs4.existsSync(path4.join(packageRoot, "pnpm-lock.yaml")) ? "pnpm" : "npm";
|
|
1433
|
+
const result = spawnSync(packageManager, ["rebuild", "better-sqlite3"], {
|
|
1434
|
+
cwd: packageRoot,
|
|
1435
|
+
encoding: "utf8",
|
|
1436
|
+
stdio: "pipe",
|
|
1437
|
+
timeout: 12e4
|
|
1438
|
+
});
|
|
1439
|
+
if (result.status === 0) return true;
|
|
1440
|
+
const detail = [result.stderr, result.stdout].filter(Boolean).join("\n").split("\n").map((line) => line.trim()).filter(Boolean).slice(-3).join(" ");
|
|
1441
|
+
warnings.push(`Codex SQLite native module mismatch detected. RepoSpend tried to rebuild better-sqlite3 automatically with ${packageManager}, but it failed${exitLabel(result.status)}.${detail ? ` ${detail}` : ""}`);
|
|
1442
|
+
return false;
|
|
1443
|
+
}
|
|
1444
|
+
function findRepoSpendPackageRoot() {
|
|
1445
|
+
const starts = [
|
|
1446
|
+
process.argv[1] ? path4.dirname(fs4.realpathSync(process.argv[1])) : void 0,
|
|
1447
|
+
path4.dirname(fileURLToPath(import.meta.url)),
|
|
1448
|
+
process.cwd()
|
|
1449
|
+
].filter((item) => Boolean(item));
|
|
1450
|
+
for (const start of starts) {
|
|
1451
|
+
const root = findPackageRootNamedRepoSpend(start);
|
|
1452
|
+
if (root) return root;
|
|
1453
|
+
}
|
|
1454
|
+
return void 0;
|
|
1455
|
+
}
|
|
1456
|
+
function findPackageRootNamedRepoSpend(start) {
|
|
1457
|
+
let current = path4.resolve(start);
|
|
1458
|
+
while (true) {
|
|
1459
|
+
const packagePath = path4.join(current, "package.json");
|
|
1460
|
+
if (fs4.existsSync(packagePath)) {
|
|
1461
|
+
try {
|
|
1462
|
+
const packageJson = JSON.parse(fs4.readFileSync(packagePath, "utf8"));
|
|
1463
|
+
if (packageJson.name === "repospend") return current;
|
|
1464
|
+
} catch {
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
const parent = path4.dirname(current);
|
|
1468
|
+
if (parent === current) return void 0;
|
|
1469
|
+
current = parent;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
function isNativeSqliteMismatch(error) {
|
|
1473
|
+
return errorMessage(error).includes("NODE_MODULE_VERSION");
|
|
1474
|
+
}
|
|
1475
|
+
function exitLabel(status) {
|
|
1476
|
+
return status === null ? "" : ` with exit code ${status}`;
|
|
1477
|
+
}
|
|
1346
1478
|
function threadToUsage(row, index, sessionFiles, options) {
|
|
1347
1479
|
const sourcePath = stringValue2(row.rollout_path) ?? matchSessionPath(stringValue2(row.id), sessionFiles) ?? "";
|
|
1348
1480
|
const sessionBreakdown = sourcePath ? readSessionBreakdown(sourcePath) : emptyBreakdown();
|
|
@@ -1371,8 +1503,8 @@ function threadToUsage(row, index, sessionFiles, options) {
|
|
|
1371
1503
|
gitRemoteUrl: repo.gitRemoteUrl,
|
|
1372
1504
|
gitBranch: repo.gitBranch,
|
|
1373
1505
|
title: stringValue2(row.title),
|
|
1374
|
-
startedAt:
|
|
1375
|
-
endedAt:
|
|
1506
|
+
startedAt: dateValue2(row.created_at),
|
|
1507
|
+
endedAt: dateValue2(row.updated_at),
|
|
1376
1508
|
model: stringValue2(row.model),
|
|
1377
1509
|
provider: stringValue2(row.model_provider),
|
|
1378
1510
|
inputTokens,
|
|
@@ -1389,7 +1521,7 @@ function threadToUsage(row, index, sessionFiles, options) {
|
|
|
1389
1521
|
rawTokenTotal: dbTokens || sessionBreakdown.totalTokens || void 0,
|
|
1390
1522
|
warnings,
|
|
1391
1523
|
codexHome: options.codexHome ?? path4.join(os2.homedir(), ".codex"),
|
|
1392
|
-
durationMs: durationMs(
|
|
1524
|
+
durationMs: durationMs(dateValue2(row.created_at), dateValue2(row.updated_at)),
|
|
1393
1525
|
rawEventCount: sessionBreakdown.rawEventCount,
|
|
1394
1526
|
parseStatus: sessionBreakdown.parseStatus,
|
|
1395
1527
|
parseErrors: sessionBreakdown.parseErrors,
|
|
@@ -1413,7 +1545,8 @@ function threadToUsage(row, index, sessionFiles, options) {
|
|
|
1413
1545
|
fileReadCount: sessionBreakdown.fileReadCount,
|
|
1414
1546
|
fileEditCount: sessionBreakdown.fileEditCount,
|
|
1415
1547
|
promptTimeline: sessionBreakdown.promptTimeline,
|
|
1416
|
-
sessionOutcome: inferOutcome(sessionBreakdown)
|
|
1548
|
+
sessionOutcome: inferOutcome(sessionBreakdown),
|
|
1549
|
+
compaction: sessionBreakdown.compaction
|
|
1417
1550
|
};
|
|
1418
1551
|
const cost = hasPricedBreakdown ? calculateCostUsd(usage, options.pricing) : void 0;
|
|
1419
1552
|
const costWarnings = hasPricedBreakdown ? warnings : [...warnings, "missing_token_breakdown"];
|
|
@@ -1483,7 +1616,8 @@ function readStandaloneSessions(sessionFiles, options) {
|
|
|
1483
1616
|
fileReadCount: breakdown.fileReadCount,
|
|
1484
1617
|
fileEditCount: breakdown.fileEditCount,
|
|
1485
1618
|
promptTimeline: breakdown.promptTimeline,
|
|
1486
|
-
sessionOutcome: inferOutcome(breakdown)
|
|
1619
|
+
sessionOutcome: inferOutcome(breakdown),
|
|
1620
|
+
compaction: breakdown.compaction
|
|
1487
1621
|
};
|
|
1488
1622
|
});
|
|
1489
1623
|
}
|
|
@@ -1521,11 +1655,15 @@ function readSessionBreakdown(filePath) {
|
|
|
1521
1655
|
breakdown.parseStatus = parse.errors.length ? records.length ? "partial" : "failed" : "ok";
|
|
1522
1656
|
let eventMessageCount = 0;
|
|
1523
1657
|
let responseMessageCount = 0;
|
|
1658
|
+
let lastUserMessageText;
|
|
1524
1659
|
for (const record of records) {
|
|
1525
1660
|
applySessionMetadata(breakdown, record);
|
|
1526
1661
|
applyPromptTimeline(breakdown, record);
|
|
1527
1662
|
if (isCodexEventMessage(record)) eventMessageCount += 1;
|
|
1528
1663
|
if (isResponseMessage(record)) responseMessageCount += 1;
|
|
1664
|
+
const userText = extractUserMessageText(record);
|
|
1665
|
+
if (userText !== void 0) lastUserMessageText = userText;
|
|
1666
|
+
applyCompactionEvent(breakdown, record, lastUserMessageText);
|
|
1529
1667
|
}
|
|
1530
1668
|
breakdown.messageCount = eventMessageCount || responseMessageCount;
|
|
1531
1669
|
const tokenAggregation = aggregateTokens(records);
|
|
@@ -1634,7 +1772,8 @@ function emptyBreakdown() {
|
|
|
1634
1772
|
commandIssueSamples: [],
|
|
1635
1773
|
fileReadCount: 0,
|
|
1636
1774
|
fileEditCount: 0,
|
|
1637
|
-
promptTimeline: []
|
|
1775
|
+
promptTimeline: [],
|
|
1776
|
+
compaction: { count: 0, autoCount: 0, manualCount: 0, events: [] }
|
|
1638
1777
|
};
|
|
1639
1778
|
}
|
|
1640
1779
|
function fallbackCwdFromPath(filePath) {
|
|
@@ -1662,6 +1801,35 @@ function isResponseMessage(record) {
|
|
|
1662
1801
|
const payload = firstObject3(object.payload);
|
|
1663
1802
|
return payload?.type === "message" && (payload.role === "user" || payload.role === "assistant");
|
|
1664
1803
|
}
|
|
1804
|
+
function extractUserMessageText(record) {
|
|
1805
|
+
if (!record || typeof record !== "object") return void 0;
|
|
1806
|
+
const object = record;
|
|
1807
|
+
const payload = firstObject3(object.payload) ?? object;
|
|
1808
|
+
const payloadType = stringValue2(payload.type);
|
|
1809
|
+
const roleValue = stringValue2(payload.role);
|
|
1810
|
+
const isUser = payloadType === "user_message" || object.type === "response_item" && payloadType === "message" && roleValue === "user";
|
|
1811
|
+
if (!isUser) return void 0;
|
|
1812
|
+
return extractMessageText2(payload) ?? extractMessageText2(object);
|
|
1813
|
+
}
|
|
1814
|
+
function applyCompactionEvent(breakdown, record, lastUserMessageText) {
|
|
1815
|
+
if (!record || typeof record !== "object") return;
|
|
1816
|
+
const object = record;
|
|
1817
|
+
if (object.type !== "event_msg") return;
|
|
1818
|
+
const payload = firstObject3(object.payload);
|
|
1819
|
+
if (payload?.type !== "context_compacted") return;
|
|
1820
|
+
const trigger = isManualCompactCommand(lastUserMessageText) ? "manual" : "auto";
|
|
1821
|
+
breakdown.compaction.count += 1;
|
|
1822
|
+
if (trigger === "manual") breakdown.compaction.manualCount += 1;
|
|
1823
|
+
else breakdown.compaction.autoCount += 1;
|
|
1824
|
+
breakdown.compaction.events.push({
|
|
1825
|
+
timestamp: dateValue2(object.timestamp) ?? dateValue2(payload.timestamp),
|
|
1826
|
+
trigger
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
function isManualCompactCommand(text) {
|
|
1830
|
+
if (!text) return false;
|
|
1831
|
+
return /^\s*\/compact\b/.test(text);
|
|
1832
|
+
}
|
|
1665
1833
|
function applyPromptTimeline(breakdown, record) {
|
|
1666
1834
|
if (breakdown.promptTimeline.length >= 80 || !record || typeof record !== "object") return;
|
|
1667
1835
|
const object = record;
|
|
@@ -1670,15 +1838,19 @@ function applyPromptTimeline(breakdown, record) {
|
|
|
1670
1838
|
const roleValue = stringValue2(payload.role);
|
|
1671
1839
|
const role = payloadType === "user_message" || roleValue === "user" ? "user" : payloadType === "agent_message" || roleValue === "assistant" ? "assistant" : void 0;
|
|
1672
1840
|
if (!role) return;
|
|
1673
|
-
const text =
|
|
1841
|
+
const text = extractMessageText2(payload) ?? extractMessageText2(object);
|
|
1674
1842
|
if (!text) return;
|
|
1675
|
-
|
|
1843
|
+
const timelineText = compactTimelineText(text);
|
|
1844
|
+
if (!timelineText || isCodexStartupContext2(timelineText)) return;
|
|
1845
|
+
const item = {
|
|
1676
1846
|
role,
|
|
1677
|
-
text: normalizeTimelineText(
|
|
1678
|
-
timestamp:
|
|
1679
|
-
}
|
|
1847
|
+
text: normalizeTimelineText(timelineText),
|
|
1848
|
+
timestamp: dateValue2(payload.timestamp) ?? dateValue2(object.timestamp) ?? dateValue2(payload.created_at) ?? dateValue2(object.created_at)
|
|
1849
|
+
};
|
|
1850
|
+
if (hasNearbyDuplicatePromptTimelineItem(breakdown.promptTimeline, item)) return;
|
|
1851
|
+
breakdown.promptTimeline.push(item);
|
|
1680
1852
|
}
|
|
1681
|
-
function
|
|
1853
|
+
function extractMessageText2(record) {
|
|
1682
1854
|
for (const key of ["text", "message", "content", "input"]) {
|
|
1683
1855
|
const value = record[key];
|
|
1684
1856
|
if (typeof value === "string" && value.trim()) return value;
|
|
@@ -1696,8 +1868,24 @@ function extractMessageText(record) {
|
|
|
1696
1868
|
}
|
|
1697
1869
|
return void 0;
|
|
1698
1870
|
}
|
|
1871
|
+
function compactTimelineText(text) {
|
|
1872
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1873
|
+
}
|
|
1699
1874
|
function normalizeTimelineText(text) {
|
|
1700
|
-
return text
|
|
1875
|
+
return compactTimelineText(text).slice(0, 1e3);
|
|
1876
|
+
}
|
|
1877
|
+
function isCodexStartupContext2(text) {
|
|
1878
|
+
return text.includes("<environment_context>") && (text.includes("# AGENTS.md instructions") || text.includes("<INSTRUCTIONS>"));
|
|
1879
|
+
}
|
|
1880
|
+
function hasNearbyDuplicatePromptTimelineItem(items, item) {
|
|
1881
|
+
return items.some((existing) => {
|
|
1882
|
+
if (existing.role !== item.role || existing.text !== item.text) return false;
|
|
1883
|
+
if (!existing.timestamp || !item.timestamp) return true;
|
|
1884
|
+
const existingTime = new Date(existing.timestamp).getTime();
|
|
1885
|
+
const itemTime = new Date(item.timestamp).getTime();
|
|
1886
|
+
if (!Number.isFinite(existingTime) || !Number.isFinite(itemTime)) return false;
|
|
1887
|
+
return Math.abs(existingTime - itemTime) <= 5e3;
|
|
1888
|
+
});
|
|
1701
1889
|
}
|
|
1702
1890
|
function detectSurface(source, threadSource) {
|
|
1703
1891
|
const raw = `${source ?? ""} ${threadSource ?? ""}`.toLowerCase();
|
|
@@ -1726,15 +1914,15 @@ function stringValue2(value) {
|
|
|
1726
1914
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1727
1915
|
}
|
|
1728
1916
|
function codexSourceAppLabel(source, threadSource) {
|
|
1729
|
-
if (threadSource === "subagent") return "Codex
|
|
1917
|
+
if (threadSource === "subagent") return "Codex subagent";
|
|
1730
1918
|
if (!source) return "Codex";
|
|
1731
1919
|
if (source === "vscode" || source === "codex_vscode" || source === "codex-vscode") return "VS Code";
|
|
1732
1920
|
if (source === "cli" || source === "terminal" || source === "codex-tui") return "Terminal";
|
|
1733
1921
|
if (source === "codex_app" || source === "codex-app") return "Codex app";
|
|
1734
|
-
if (source.trim().startsWith("{")) return threadSource === "subagent" ? "Codex
|
|
1922
|
+
if (source.trim().startsWith("{")) return threadSource === "subagent" ? "Codex subagent" : "Codex";
|
|
1735
1923
|
return source.replace(/^codex[_-]/, "").split(/[_-]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
1736
1924
|
}
|
|
1737
|
-
function
|
|
1925
|
+
function dateValue2(value) {
|
|
1738
1926
|
if (typeof value === "number") {
|
|
1739
1927
|
return new Date(value > 1e10 ? value : value * 1e3).toISOString();
|
|
1740
1928
|
}
|
|
@@ -1764,7 +1952,9 @@ function scanClaude(options) {
|
|
|
1764
1952
|
const warnings = [];
|
|
1765
1953
|
const discovery = discoverClaudeSessionFiles(claudeHome, warnings, Boolean(options.claudeHome));
|
|
1766
1954
|
const historyExists = fs5.existsSync(historyPath);
|
|
1767
|
-
const
|
|
1955
|
+
const seenUsageKeys = /* @__PURE__ */ new Set();
|
|
1956
|
+
const orderedFiles = discovery.files.map((file) => ({ file, mtimeMs: safeMtimeMs(file.filePath) })).sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
1957
|
+
const sessions = orderedFiles.map(({ file }, index) => claudeFileToUsage(file, index, options, seenUsageKeys));
|
|
1768
1958
|
const historyEntryCount = countHistoryEntries(historyPath);
|
|
1769
1959
|
const parseFailureCount = sessions.filter((session) => session.parseStatus === "failed" || (session.parseErrors?.length ?? 0) > 0).length;
|
|
1770
1960
|
const unreadableFileCount = sessions.filter((session) => session.parseErrors?.some((error) => error.startsWith("unable_to_read_session_file:"))).length;
|
|
@@ -1870,8 +2060,8 @@ function walk2(root, maxDepth, onFile, depth = 0) {
|
|
|
1870
2060
|
}
|
|
1871
2061
|
}
|
|
1872
2062
|
}
|
|
1873
|
-
function claudeFileToUsage(file, index, options) {
|
|
1874
|
-
const breakdown = readClaudeBreakdown(file.filePath);
|
|
2063
|
+
function claudeFileToUsage(file, index, options, seenUsageKeys) {
|
|
2064
|
+
const breakdown = readClaudeBreakdown(file.filePath, seenUsageKeys);
|
|
1875
2065
|
const id = breakdown.id ?? (path5.basename(file.filePath).replace(/\.jsonl$/i, "") || `claude-session-${index}`);
|
|
1876
2066
|
const fallbackCwd = inferProjectPath(file.projectDir, file.projectsPath);
|
|
1877
2067
|
const cwd = breakdown.cwd ?? fallbackCwd;
|
|
@@ -1942,7 +2132,7 @@ function claudeFileToUsage(file, index, options) {
|
|
|
1942
2132
|
warnings: cost === void 0 ? [...usage.warnings, "unknown_pricing"] : usage.warnings
|
|
1943
2133
|
};
|
|
1944
2134
|
}
|
|
1945
|
-
function readClaudeBreakdown(filePath) {
|
|
2135
|
+
function readClaudeBreakdown(filePath, seenUsageKeys) {
|
|
1946
2136
|
const breakdown = emptyBreakdown2();
|
|
1947
2137
|
try {
|
|
1948
2138
|
const text = fs5.readFileSync(filePath, "utf8");
|
|
@@ -1951,7 +2141,7 @@ function readClaudeBreakdown(filePath) {
|
|
|
1951
2141
|
breakdown.parseErrors = parse.errors;
|
|
1952
2142
|
breakdown.parseStatus = parse.errors.length ? parse.records.length ? "partial" : "failed" : "ok";
|
|
1953
2143
|
for (const record of parse.records) {
|
|
1954
|
-
applyClaudeRecord(breakdown, record);
|
|
2144
|
+
applyClaudeRecord(breakdown, record, seenUsageKeys);
|
|
1955
2145
|
}
|
|
1956
2146
|
breakdown.totalTokens = breakdown.inputTokens + breakdown.outputTokens;
|
|
1957
2147
|
} catch (error) {
|
|
@@ -1962,8 +2152,8 @@ function readClaudeBreakdown(filePath) {
|
|
|
1962
2152
|
}
|
|
1963
2153
|
return breakdown;
|
|
1964
2154
|
}
|
|
1965
|
-
function applyClaudeRecord(breakdown, record) {
|
|
1966
|
-
const timestamp3 =
|
|
2155
|
+
function applyClaudeRecord(breakdown, record, seenUsageKeys) {
|
|
2156
|
+
const timestamp3 = dateValue3(record.timestamp);
|
|
1967
2157
|
if (timestamp3) {
|
|
1968
2158
|
breakdown.startedAt = minDate(breakdown.startedAt, timestamp3);
|
|
1969
2159
|
breakdown.endedAt = maxDate(breakdown.endedAt, timestamp3);
|
|
@@ -1985,11 +2175,22 @@ function applyClaudeRecord(breakdown, record) {
|
|
|
1985
2175
|
const usageTokens = usage ? usageTotal(usage) : 0;
|
|
1986
2176
|
const model = normalizeClaudeModel(message.model);
|
|
1987
2177
|
if (model && (usageTokens > 0 || !breakdown.model)) breakdown.model = model;
|
|
1988
|
-
if (usage && usageTokens > 0
|
|
2178
|
+
if (usage && usageTokens > 0 && !alreadySeenUsage(record, message, seenUsageKeys)) {
|
|
2179
|
+
applyUsage(breakdown, usage);
|
|
2180
|
+
}
|
|
1989
2181
|
applyContentSignals(breakdown, message.content);
|
|
1990
2182
|
}
|
|
1991
2183
|
if (looksFailedToolResult(record.toolUseResult)) breakdown.failedToolCallCount += 1;
|
|
1992
2184
|
}
|
|
2185
|
+
function alreadySeenUsage(record, message, seenUsageKeys) {
|
|
2186
|
+
const messageId = stringValue3(message.id);
|
|
2187
|
+
const requestId = stringValue3(record.requestId);
|
|
2188
|
+
if (!messageId || !requestId) return false;
|
|
2189
|
+
const key = `${requestId}:${messageId}`;
|
|
2190
|
+
if (seenUsageKeys.has(key)) return true;
|
|
2191
|
+
seenUsageKeys.add(key);
|
|
2192
|
+
return false;
|
|
2193
|
+
}
|
|
1993
2194
|
function applyUsage(breakdown, usage) {
|
|
1994
2195
|
const input = numberValue(usage.input_tokens);
|
|
1995
2196
|
const cacheRead = numberValue(usage.cache_read_input_tokens);
|
|
@@ -2173,7 +2374,7 @@ function normalizeClaudeModel(value) {
|
|
|
2173
2374
|
if (!model || model === "<synthetic>") return void 0;
|
|
2174
2375
|
return model;
|
|
2175
2376
|
}
|
|
2176
|
-
function
|
|
2377
|
+
function dateValue3(value) {
|
|
2177
2378
|
if (typeof value !== "string") return void 0;
|
|
2178
2379
|
const date = new Date(value);
|
|
2179
2380
|
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
@@ -2202,6 +2403,13 @@ function looksFailedToolResult(value) {
|
|
|
2202
2403
|
function errorMessage2(error) {
|
|
2203
2404
|
return error instanceof Error ? error.message : String(error);
|
|
2204
2405
|
}
|
|
2406
|
+
function safeMtimeMs(filePath) {
|
|
2407
|
+
try {
|
|
2408
|
+
return fs5.statSync(filePath).mtimeMs;
|
|
2409
|
+
} catch {
|
|
2410
|
+
return 0;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2205
2413
|
|
|
2206
2414
|
// packages/core/src/sources/index.ts
|
|
2207
2415
|
function scanUsageSources(options) {
|
|
@@ -2311,7 +2519,7 @@ function runtimeCwd() {
|
|
|
2311
2519
|
// apps/server/src/server.ts
|
|
2312
2520
|
import fs7 from "node:fs";
|
|
2313
2521
|
import path6 from "node:path";
|
|
2314
|
-
import { fileURLToPath } from "node:url";
|
|
2522
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2315
2523
|
import fastifyStatic from "@fastify/static";
|
|
2316
2524
|
import Fastify from "fastify";
|
|
2317
2525
|
|
|
@@ -2320,7 +2528,7 @@ var baseDate = "2026-05-18";
|
|
|
2320
2528
|
var demoPricing = {
|
|
2321
2529
|
...defaultPricing,
|
|
2322
2530
|
"gpt-5.3-codex": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
|
|
2323
|
-
"gpt-5.3-codex-spark": { inputPerMillion:
|
|
2531
|
+
"gpt-5.3-codex-spark": { inputPerMillion: 1.75, cachedInputPerMillion: 0.175, outputPerMillion: 14, reasoningOutputPerMillion: 14 },
|
|
2324
2532
|
"claude-haiku-4-5-20251001": { inputPerMillion: 1, cacheCreationInputPerMillion: 1.25, cachedInputPerMillion: 0.1, outputPerMillion: 5 }
|
|
2325
2533
|
};
|
|
2326
2534
|
function demoModeEnabled() {
|
|
@@ -3124,6 +3332,7 @@ function createServer(options = {}) {
|
|
|
3124
3332
|
const rtkGain = demoMode ? readDemoRtkGain() : await readRtkGain();
|
|
3125
3333
|
return {
|
|
3126
3334
|
...data,
|
|
3335
|
+
appVersion: readPackageVersion(),
|
|
3127
3336
|
summary: { ...data.summary, skippedZeroTokenSessions: data.skippedZeroTokenSessions },
|
|
3128
3337
|
pricing: {
|
|
3129
3338
|
info: pricingInfo,
|
|
@@ -3147,7 +3356,7 @@ function createServer(options = {}) {
|
|
|
3147
3356
|
return exportCsv();
|
|
3148
3357
|
});
|
|
3149
3358
|
if (options.serveWeb !== false) {
|
|
3150
|
-
const serverDist = path6.dirname(
|
|
3359
|
+
const serverDist = path6.dirname(fileURLToPath2(import.meta.url));
|
|
3151
3360
|
const packagedWebDist = path6.resolve(serverDist, "../web-dist");
|
|
3152
3361
|
const workspaceWebDist = path6.resolve(serverDist, "../../web/dist");
|
|
3153
3362
|
const webDist = fsExists(packagedWebDist) ? packagedWebDist : workspaceWebDist;
|
|
@@ -3173,7 +3382,7 @@ function fsExists(inputPath) {
|
|
|
3173
3382
|
return Boolean(inputPath) && path6.isAbsolute(inputPath) && fs7.existsSync(inputPath);
|
|
3174
3383
|
}
|
|
3175
3384
|
function readPackageVersion() {
|
|
3176
|
-
const serverDist = path6.dirname(
|
|
3385
|
+
const serverDist = path6.dirname(fileURLToPath2(import.meta.url));
|
|
3177
3386
|
const packagePath = path6.resolve(serverDist, "../package.json");
|
|
3178
3387
|
try {
|
|
3179
3388
|
const parsed = JSON.parse(fs7.readFileSync(packagePath, "utf8"));
|
package/docs/PUBLISHING.md
CHANGED
|
@@ -44,7 +44,10 @@ git tag v0.0.3
|
|
|
44
44
|
git push origin main v0.0.3
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
The publish workflow runs when a `v*.*.*` tag is pushed.
|
|
47
|
+
The publish workflow runs when a `v*.*.*` tag is pushed. After npm publishes
|
|
48
|
+
successfully, the workflow also creates a GitHub release for the tag using the
|
|
49
|
+
matching section from `CHANGELOG.md`. For example, tag `v0.0.4` uses the
|
|
50
|
+
`## 0.0.4` changelog section.
|
|
48
51
|
|
|
49
52
|
## npm Trusted Publishing
|
|
50
53
|
|
package/package.json
CHANGED