codetime-cli 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/codetime.mjs +197 -37
- package/package.json +1 -1
package/bin/codetime.mjs
CHANGED
|
@@ -208,7 +208,7 @@ var init_fs = __esm({
|
|
|
208
208
|
|
|
209
209
|
// src/cli.ts
|
|
210
210
|
import { spawn } from "node:child_process";
|
|
211
|
-
import { rm as rm2, stat as
|
|
211
|
+
import { rm as rm2, stat as stat7 } from "node:fs/promises";
|
|
212
212
|
import os3 from "node:os";
|
|
213
213
|
import path13 from "node:path";
|
|
214
214
|
import { fileURLToPath } from "node:url";
|
|
@@ -1030,7 +1030,7 @@ import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
|
|
|
1030
1030
|
import path2 from "node:path";
|
|
1031
1031
|
|
|
1032
1032
|
// src/lib/constants.ts
|
|
1033
|
-
var PACKAGE_VERSION = true ? "0.7.
|
|
1033
|
+
var PACKAGE_VERSION = true ? "0.7.4" : "0.1.1";
|
|
1034
1034
|
var DEFAULT_API_URL = "https://codetime.dev";
|
|
1035
1035
|
var DEFAULT_BACKFILL_BATCH_SIZE = 50;
|
|
1036
1036
|
var DEFAULT_BACKFILL_BATCH_BYTES = 800 * 1024;
|
|
@@ -1046,6 +1046,10 @@ function stringField(object, key) {
|
|
|
1046
1046
|
const value = object[key];
|
|
1047
1047
|
return typeof value === "string" ? value : void 0;
|
|
1048
1048
|
}
|
|
1049
|
+
function nonEmptyStringField(object, key) {
|
|
1050
|
+
const value = stringField(object, key);
|
|
1051
|
+
return value && value.trim().length > 0 ? value : void 0;
|
|
1052
|
+
}
|
|
1049
1053
|
function numberField(object, key) {
|
|
1050
1054
|
const value = object[key];
|
|
1051
1055
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
@@ -1291,16 +1295,17 @@ async function parseAmpSessionFile(filePath, options) {
|
|
|
1291
1295
|
const messages = Array.isArray(raw.messages) ? raw.messages.filter(isPlainObject) : [];
|
|
1292
1296
|
const ledger = objectField(raw, "usageLedger");
|
|
1293
1297
|
const rawEvents = Array.isArray(ledger.events) ? ledger.events.filter(isPlainObject) : [];
|
|
1298
|
+
const state = new SessionParserState(filePath, options, (event) => baseAmpEvent(event));
|
|
1299
|
+
state.sessionId = sessionId;
|
|
1294
1300
|
if (rawEvents.length === 0) {
|
|
1295
|
-
|
|
1301
|
+
collectAmpMessageUsage(state, messages, sessionId, threadId, options);
|
|
1302
|
+
return state.events.filter((event) => matchesBackfillFilters(event, options));
|
|
1296
1303
|
}
|
|
1297
1304
|
const events = [...rawEvents].sort((a, b) => {
|
|
1298
1305
|
const ta = timestampFrom(stringField(a, "timestamp")) || "";
|
|
1299
1306
|
const tb = timestampFrom(stringField(b, "timestamp")) || "";
|
|
1300
1307
|
return ta.localeCompare(tb);
|
|
1301
1308
|
});
|
|
1302
|
-
const state = new SessionParserState(filePath, options, (event) => baseAmpEvent(event));
|
|
1303
|
-
state.sessionId = sessionId;
|
|
1304
1309
|
const firstTs = timestampFrom(stringField(events[0], "timestamp")) || (/* @__PURE__ */ new Date()).toISOString();
|
|
1305
1310
|
state.ensureSessionStarted(firstTs, 0);
|
|
1306
1311
|
let lastTs = firstTs;
|
|
@@ -1315,7 +1320,7 @@ async function parseAmpSessionFile(filePath, options) {
|
|
|
1315
1320
|
const cache = cacheTokensFor(messages, toMessageId);
|
|
1316
1321
|
const cacheRead = cache.cacheReadInputTokens;
|
|
1317
1322
|
const cacheWrite = cache.cacheCreationInputTokens;
|
|
1318
|
-
const totalTokens = input
|
|
1323
|
+
const { billableOutput, totalTokens } = foldAmpTotal(input, output, cacheRead, cacheWrite, numberField(tokens, "total") || 0);
|
|
1319
1324
|
if (totalTokens <= 0) {
|
|
1320
1325
|
continue;
|
|
1321
1326
|
}
|
|
@@ -1324,7 +1329,7 @@ async function parseAmpSessionFile(filePath, options) {
|
|
|
1324
1329
|
const fromMessageId = numberField(ev, "fromMessageId");
|
|
1325
1330
|
const metrics = {
|
|
1326
1331
|
tokensInput: input + cacheRead + cacheWrite || void 0,
|
|
1327
|
-
tokensOutput:
|
|
1332
|
+
tokensOutput: billableOutput || void 0,
|
|
1328
1333
|
tokensCachedInput: cacheRead + cacheWrite || void 0,
|
|
1329
1334
|
tokensCacheReadInput: cacheRead || void 0,
|
|
1330
1335
|
tokensCacheCreationInput: cacheWrite || void 0,
|
|
@@ -1367,6 +1372,79 @@ async function parseAmpSessionFile(filePath, options) {
|
|
|
1367
1372
|
);
|
|
1368
1373
|
return state.events.filter((event) => matchesBackfillFilters(event, options));
|
|
1369
1374
|
}
|
|
1375
|
+
function foldAmpTotal(input, output, cacheRead, cacheWrite, explicitTotal) {
|
|
1376
|
+
const partsSum = input + output + cacheRead + cacheWrite;
|
|
1377
|
+
const missing = Math.max(0, explicitTotal - partsSum);
|
|
1378
|
+
return { billableOutput: output + missing, totalTokens: partsSum + missing };
|
|
1379
|
+
}
|
|
1380
|
+
function collectAmpMessageUsage(state, messages, sessionId, threadId, _options) {
|
|
1381
|
+
const rows = [];
|
|
1382
|
+
for (const message of messages) {
|
|
1383
|
+
if (stringField(message, "role") !== "assistant") {
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
const usage = objectField(message, "usage");
|
|
1387
|
+
if (Object.keys(usage).length === 0) {
|
|
1388
|
+
continue;
|
|
1389
|
+
}
|
|
1390
|
+
const ts = timestampFrom(stringField(usage, "timestamp")) || timestampFrom(stringField(message, "timestamp"));
|
|
1391
|
+
if (!ts) {
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
const model = stringField(usage, "model") || stringField(message, "model") || void 0;
|
|
1395
|
+
const input = numberField(usage, "inputTokens") || 0;
|
|
1396
|
+
const output = numberField(usage, "outputTokens") || 0;
|
|
1397
|
+
const cacheWrite = numberField(usage, "cacheCreationInputTokens") || 0;
|
|
1398
|
+
const cacheRead = numberField(usage, "cacheReadInputTokens") || 0;
|
|
1399
|
+
const { billableOutput, totalTokens } = foldAmpTotal(input, output, cacheRead, cacheWrite, numberField(usage, "totalTokens") || 0);
|
|
1400
|
+
if (totalTokens <= 0) {
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
rows.push({ ts, model, input, billableOutput, cacheRead, cacheWrite, totalTokens });
|
|
1404
|
+
}
|
|
1405
|
+
if (rows.length === 0) {
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
rows.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
1409
|
+
state.ensureSessionStarted(rows[0].ts, 0);
|
|
1410
|
+
let lastTs = rows[0].ts;
|
|
1411
|
+
for (const [index, r] of rows.entries()) {
|
|
1412
|
+
lastTs = r.ts;
|
|
1413
|
+
state.push(
|
|
1414
|
+
baseAmpEvent({
|
|
1415
|
+
ts: r.ts,
|
|
1416
|
+
type: "model.usage",
|
|
1417
|
+
sessionId,
|
|
1418
|
+
model: r.model,
|
|
1419
|
+
confidence: "exact",
|
|
1420
|
+
metrics: {
|
|
1421
|
+
tokensInput: r.input + r.cacheRead + r.cacheWrite || void 0,
|
|
1422
|
+
tokensOutput: r.billableOutput || void 0,
|
|
1423
|
+
tokensCachedInput: r.cacheRead + r.cacheWrite || void 0,
|
|
1424
|
+
tokensCacheReadInput: r.cacheRead || void 0,
|
|
1425
|
+
tokensCacheCreationInput: r.cacheWrite || void 0,
|
|
1426
|
+
tokensTotal: r.totalTokens,
|
|
1427
|
+
modelCalls: 1
|
|
1428
|
+
},
|
|
1429
|
+
refs: stringRefs({ threadId })
|
|
1430
|
+
}),
|
|
1431
|
+
index + 1,
|
|
1432
|
+
"messages",
|
|
1433
|
+
"model.usage"
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
state.push(
|
|
1437
|
+
baseAmpEvent({
|
|
1438
|
+
ts: lastTs,
|
|
1439
|
+
type: "session.ended",
|
|
1440
|
+
sessionId,
|
|
1441
|
+
confidence: "derived"
|
|
1442
|
+
}),
|
|
1443
|
+
rows.length,
|
|
1444
|
+
"messages",
|
|
1445
|
+
"session"
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1370
1448
|
function baseAmpEvent(event) {
|
|
1371
1449
|
return {
|
|
1372
1450
|
schemaVersion: AGENT_TIME_SCHEMA_VERSION,
|
|
@@ -1863,7 +1941,10 @@ async function parseClaudeCodeSessionFile(filePath, options) {
|
|
|
1863
1941
|
if (topType !== "assistant") {
|
|
1864
1942
|
continue;
|
|
1865
1943
|
}
|
|
1866
|
-
|
|
1944
|
+
const parsedModel = stringField(message, "model");
|
|
1945
|
+
if (parsedModel && parsedModel !== "<synthetic>") {
|
|
1946
|
+
model = parsedModel;
|
|
1947
|
+
}
|
|
1867
1948
|
const messageId = stringField(message, "id");
|
|
1868
1949
|
const requestId = stringField(raw, "requestId");
|
|
1869
1950
|
const usageKey = messageId ? `${messageId}:${requestId}` : null;
|
|
@@ -2253,7 +2334,7 @@ function createClaudeCodeAdapter() {
|
|
|
2253
2334
|
}
|
|
2254
2335
|
|
|
2255
2336
|
// src/adapters/codex.ts
|
|
2256
|
-
import { readFile as readFile4 } from "node:fs/promises";
|
|
2337
|
+
import { readFile as readFile4, stat as stat4 } from "node:fs/promises";
|
|
2257
2338
|
import path7 from "node:path";
|
|
2258
2339
|
|
|
2259
2340
|
// src/lib/diff.ts
|
|
@@ -2300,6 +2381,7 @@ function fileActivitiesFromPatchChanges(changes, ts, cwd, displayFilePath3) {
|
|
|
2300
2381
|
}
|
|
2301
2382
|
|
|
2302
2383
|
// src/adapters/codex.ts
|
|
2384
|
+
init_fs();
|
|
2303
2385
|
async function parseCodexSessionFile(filePath, options) {
|
|
2304
2386
|
const text = await readFile4(filePath, "utf8");
|
|
2305
2387
|
const lines = text.split("\n").filter(Boolean);
|
|
@@ -2318,6 +2400,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2318
2400
|
let lastTokenUsageKey;
|
|
2319
2401
|
const replaySecond = detectSubagentReplaySecond(text, lines);
|
|
2320
2402
|
let skipReplay = replaySecond !== void 0;
|
|
2403
|
+
let previousTotals = { input: 0, cached: 0, output: 0, reasoning: 0, total: 0 };
|
|
2321
2404
|
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
2322
2405
|
const turnLastEventAt = /* @__PURE__ */ new Map();
|
|
2323
2406
|
const turnStartedAt = /* @__PURE__ */ new Map();
|
|
@@ -2475,7 +2558,12 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2475
2558
|
if (tierFromInfo) {
|
|
2476
2559
|
serviceTier = tierFromInfo.toLowerCase();
|
|
2477
2560
|
}
|
|
2478
|
-
const
|
|
2561
|
+
const totalUsage = objectField(info, "total_token_usage");
|
|
2562
|
+
const hasTotal = Object.keys(totalUsage).length > 0;
|
|
2563
|
+
const usage = tokenUsageFromPayload(payload) ?? (hasTotal ? codexUsageDelta(totalUsage, previousTotals, info) : void 0);
|
|
2564
|
+
if (hasTotal) {
|
|
2565
|
+
previousTotals = readCodexCumulative(totalUsage);
|
|
2566
|
+
}
|
|
2479
2567
|
if (usage) {
|
|
2480
2568
|
if (skipReplay) {
|
|
2481
2569
|
if (ts.slice(0, 19) === replaySecond) {
|
|
@@ -2735,7 +2823,9 @@ function headlessCodexUsage(raw) {
|
|
|
2735
2823
|
const billableOutput = output + reasoning;
|
|
2736
2824
|
return {
|
|
2737
2825
|
tokensInput: input || void 0,
|
|
2738
|
-
|
|
2826
|
+
// Clamp cached to input so non-cached (input - cached) never goes negative
|
|
2827
|
+
// (ccusage cached.min(input) in the headless path too).
|
|
2828
|
+
tokensCachedInput: Math.min(cached, input) || void 0,
|
|
2739
2829
|
tokensOutput: billableOutput || void 0,
|
|
2740
2830
|
tokensReasoningOutput: reasoning || void 0,
|
|
2741
2831
|
tokensTotal: total,
|
|
@@ -2801,15 +2891,47 @@ function tokenUsageFromPayload(payload) {
|
|
|
2801
2891
|
if (Object.keys(usage).length === 0) {
|
|
2802
2892
|
return;
|
|
2803
2893
|
}
|
|
2894
|
+
const input = numberField(usage, "input_tokens");
|
|
2895
|
+
const cached = numberField(usage, "cached_input_tokens");
|
|
2804
2896
|
return {
|
|
2805
|
-
tokensInput:
|
|
2806
|
-
|
|
2897
|
+
tokensInput: input,
|
|
2898
|
+
// Cached input can never exceed total input; clamp so the server's
|
|
2899
|
+
// non-cached = input - cached never goes negative (ccusage cached.min(input)).
|
|
2900
|
+
tokensCachedInput: cached === void 0 ? void 0 : Math.min(cached, input ?? 0),
|
|
2807
2901
|
tokensOutput: numberField(usage, "output_tokens"),
|
|
2808
2902
|
tokensReasoningOutput: numberField(usage, "reasoning_output_tokens"),
|
|
2809
2903
|
tokensTotal: numberField(usage, "total_tokens"),
|
|
2810
2904
|
modelContextWindow: numberField(info, "model_context_window")
|
|
2811
2905
|
};
|
|
2812
2906
|
}
|
|
2907
|
+
function readCodexCumulative(usage) {
|
|
2908
|
+
return {
|
|
2909
|
+
input: numberField(usage, "input_tokens") ?? 0,
|
|
2910
|
+
cached: numberField(usage, "cached_input_tokens") ?? 0,
|
|
2911
|
+
output: numberField(usage, "output_tokens") ?? 0,
|
|
2912
|
+
reasoning: numberField(usage, "reasoning_output_tokens") ?? 0,
|
|
2913
|
+
total: numberField(usage, "total_tokens") ?? 0
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
function codexUsageDelta(totalUsage, previous, info) {
|
|
2917
|
+
const cur = readCodexCumulative(totalUsage);
|
|
2918
|
+
const input = Math.max(0, cur.input - previous.input);
|
|
2919
|
+
const cached = Math.max(0, cur.cached - previous.cached);
|
|
2920
|
+
const output = Math.max(0, cur.output - previous.output);
|
|
2921
|
+
const reasoning = Math.max(0, cur.reasoning - previous.reasoning);
|
|
2922
|
+
const total = Math.max(0, cur.total - previous.total);
|
|
2923
|
+
if (input === 0 && cached === 0 && output === 0 && reasoning === 0 && total === 0) {
|
|
2924
|
+
return;
|
|
2925
|
+
}
|
|
2926
|
+
return {
|
|
2927
|
+
tokensInput: input || void 0,
|
|
2928
|
+
tokensCachedInput: Math.min(cached, input) || void 0,
|
|
2929
|
+
tokensOutput: output || void 0,
|
|
2930
|
+
tokensReasoningOutput: reasoning || void 0,
|
|
2931
|
+
tokensTotal: total || void 0,
|
|
2932
|
+
modelContextWindow: numberField(info, "model_context_window")
|
|
2933
|
+
};
|
|
2934
|
+
}
|
|
2813
2935
|
function toolNameFromPayload(payload, payloadType) {
|
|
2814
2936
|
if (payloadType === "web_search_call") {
|
|
2815
2937
|
return "web_search";
|
|
@@ -2916,15 +3038,39 @@ function createCodexAdapter() {
|
|
|
2916
3038
|
const base = codexHome(home, env);
|
|
2917
3039
|
return [
|
|
2918
3040
|
path7.join(base, "sessions"),
|
|
3041
|
+
// Codex moves old rollouts here as it rotates; include so pre-existing
|
|
3042
|
+
// archived history is discovered, not silently dropped. Actual file
|
|
3043
|
+
// listing dedups active vs archived — see codexBackfillFiles.
|
|
3044
|
+
path7.join(base, "archived_sessions"),
|
|
2919
3045
|
path7.join(base, "history.jsonl")
|
|
2920
3046
|
];
|
|
2921
3047
|
},
|
|
2922
3048
|
parseSessionFile: parseCodexSessionFile
|
|
2923
3049
|
};
|
|
2924
3050
|
}
|
|
3051
|
+
async function codexBackfillFiles(sourceRoot, home, env) {
|
|
3052
|
+
const files = sourceRoot ? await listJsonlFiles(sourceRoot) : await codexDefaultFiles(home, env);
|
|
3053
|
+
return Promise.all(files.map(async (filePath) => {
|
|
3054
|
+
const info = await stat4(filePath);
|
|
3055
|
+
return { path: filePath, modifiedAt: info.mtime.toISOString() };
|
|
3056
|
+
}));
|
|
3057
|
+
}
|
|
3058
|
+
async function codexDefaultFiles(home, env) {
|
|
3059
|
+
const base = codexHome(home, env);
|
|
3060
|
+
const sessionsDir = path7.join(base, "sessions");
|
|
3061
|
+
const archivedDir = path7.join(base, "archived_sessions");
|
|
3062
|
+
const [activeFiles, archivedFiles, historyFiles] = await Promise.all([
|
|
3063
|
+
listJsonlFiles(sessionsDir),
|
|
3064
|
+
listJsonlFiles(archivedDir),
|
|
3065
|
+
listJsonlFiles(path7.join(base, "history.jsonl"))
|
|
3066
|
+
]);
|
|
3067
|
+
const activeRelative = new Set(activeFiles.map((f) => path7.relative(sessionsDir, f)));
|
|
3068
|
+
const dedupedArchived = archivedFiles.filter((f) => !activeRelative.has(path7.relative(archivedDir, f)));
|
|
3069
|
+
return [...activeFiles, ...dedupedArchived, ...historyFiles];
|
|
3070
|
+
}
|
|
2925
3071
|
|
|
2926
3072
|
// src/adapters/gemini.ts
|
|
2927
|
-
import { readFile as readFile5, stat as
|
|
3073
|
+
import { readFile as readFile5, stat as stat5 } from "node:fs/promises";
|
|
2928
3074
|
import path8 from "node:path";
|
|
2929
3075
|
init_fs();
|
|
2930
3076
|
function tokenFromKeys(record, keys) {
|
|
@@ -3036,11 +3182,10 @@ function readStats(record) {
|
|
|
3036
3182
|
return void 0;
|
|
3037
3183
|
}
|
|
3038
3184
|
function parseJsonRecord(record, fileStem, fallbackTs) {
|
|
3039
|
-
const sessionId =
|
|
3185
|
+
const sessionId = nonEmptyStringField(record, "sessionId") ?? nonEmptyStringField(record, "session_id") ?? fileStem;
|
|
3040
3186
|
const sessionTs = timestampFrom(stringField(record, "startTime")) ?? timestampFrom(stringField(record, "lastUpdated")) ?? fallbackTs;
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
const usages = messages.filter((message) => stringField(message, "type") === "gemini").map((message) => parseDirectEvent(message, void 0, sessionTs)).filter((usage) => usage !== void 0);
|
|
3187
|
+
if (Array.isArray(record.messages)) {
|
|
3188
|
+
const usages = arrayField(record, "messages").filter(isPlainObject).filter((message) => stringField(message, "type") === "gemini").map((message) => parseDirectEvent(message, void 0, sessionTs)).filter((usage) => usage !== void 0);
|
|
3044
3189
|
return { sessionId, usages };
|
|
3045
3190
|
}
|
|
3046
3191
|
if (stringField(record, "type") === "gemini") {
|
|
@@ -3063,7 +3208,7 @@ function parseJsonlRecords(lines, fileStem, fallbackTs) {
|
|
|
3063
3208
|
if (!record) {
|
|
3064
3209
|
continue;
|
|
3065
3210
|
}
|
|
3066
|
-
const sid =
|
|
3211
|
+
const sid = nonEmptyStringField(record, "sessionId") ?? nonEmptyStringField(record, "session_id");
|
|
3067
3212
|
if (sid) {
|
|
3068
3213
|
sessionId = sid;
|
|
3069
3214
|
}
|
|
@@ -3076,7 +3221,7 @@ function parseJsonlRecords(lines, fileStem, fallbackTs) {
|
|
|
3076
3221
|
if (!usage) {
|
|
3077
3222
|
continue;
|
|
3078
3223
|
}
|
|
3079
|
-
const id =
|
|
3224
|
+
const id = nonEmptyStringField(record, "id");
|
|
3080
3225
|
if (id === void 0) {
|
|
3081
3226
|
usages.push(usage);
|
|
3082
3227
|
} else {
|
|
@@ -3101,7 +3246,7 @@ function parseJsonlRecords(lines, fileStem, fallbackTs) {
|
|
|
3101
3246
|
async function parseGeminiSessionFile(filePath, options) {
|
|
3102
3247
|
const text = await readFile5(filePath, "utf8");
|
|
3103
3248
|
const fileStem = path8.basename(filePath).replace(/\.jsonl?$/, "");
|
|
3104
|
-
const info = await
|
|
3249
|
+
const info = await stat5(filePath);
|
|
3105
3250
|
const fallbackTs = info.mtime.toISOString();
|
|
3106
3251
|
let parsed;
|
|
3107
3252
|
if (filePath.endsWith(".jsonl")) {
|
|
@@ -3192,7 +3337,7 @@ async function geminiBackfillFiles(sourceRoot, home, env) {
|
|
|
3192
3337
|
);
|
|
3193
3338
|
const files = fileLists.flat().sort();
|
|
3194
3339
|
return Promise.all(files.map(async (filePath) => {
|
|
3195
|
-
const info = await
|
|
3340
|
+
const info = await stat5(filePath);
|
|
3196
3341
|
return { path: filePath, modifiedAt: info.mtime.toISOString() };
|
|
3197
3342
|
}));
|
|
3198
3343
|
}
|
|
@@ -3357,7 +3502,9 @@ async function parseOpenCodeSessionFile(dbPath, options) {
|
|
|
3357
3502
|
const createdTs = timeCreated;
|
|
3358
3503
|
const tokens = opencodeUsageFromInfo(info);
|
|
3359
3504
|
const cost = typeof info.cost === "number" && info.cost > 0 ? info.cost : void 0;
|
|
3505
|
+
let messageHadUsage = false;
|
|
3360
3506
|
if (tokens) {
|
|
3507
|
+
messageHadUsage = true;
|
|
3361
3508
|
const metrics = {
|
|
3362
3509
|
tokensInput: tokens.tokensInput,
|
|
3363
3510
|
tokensOutput: tokens.tokensOutput,
|
|
@@ -3478,7 +3625,7 @@ async function parseOpenCodeSessionFile(dbPath, options) {
|
|
|
3478
3625
|
}
|
|
3479
3626
|
}
|
|
3480
3627
|
}
|
|
3481
|
-
if (pd.type === "step-finish") {
|
|
3628
|
+
if (pd.type === "step-finish" && !messageHadUsage) {
|
|
3482
3629
|
const stepTokens = opencodeUsageFromInfo(pd);
|
|
3483
3630
|
const stepCost = typeof pd.cost === "number" && pd.cost > 0 ? pd.cost : void 0;
|
|
3484
3631
|
if (stepTokens) {
|
|
@@ -3609,19 +3756,19 @@ function opencodeDataCandidates(home, env) {
|
|
|
3609
3756
|
return [primary, path9.join(home, ".opencode", "opencode.db")];
|
|
3610
3757
|
}
|
|
3611
3758
|
async function opencodeBackfillFiles(sourceRoot, home = os2.homedir(), env) {
|
|
3612
|
-
const { stat:
|
|
3759
|
+
const { stat: stat8 } = await import("node:fs/promises");
|
|
3613
3760
|
if (sourceRoot) {
|
|
3614
3761
|
if (!sourceRoot.endsWith(".db")) {
|
|
3615
3762
|
return [];
|
|
3616
3763
|
}
|
|
3617
|
-
const info = await
|
|
3764
|
+
const info = await stat8(sourceRoot).catch(() => null);
|
|
3618
3765
|
if (!info) {
|
|
3619
3766
|
return [];
|
|
3620
3767
|
}
|
|
3621
3768
|
return [{ path: sourceRoot, modifiedAt: info.mtime.toISOString() }];
|
|
3622
3769
|
}
|
|
3623
3770
|
for (const candidatePath of opencodeDataCandidates(home, env)) {
|
|
3624
|
-
const info = await
|
|
3771
|
+
const info = await stat8(candidatePath).catch(() => null);
|
|
3625
3772
|
if (info) {
|
|
3626
3773
|
return [{ path: candidatePath, modifiedAt: info.mtime.toISOString() }];
|
|
3627
3774
|
}
|
|
@@ -3951,7 +4098,11 @@ function piUsageFromMessage(message) {
|
|
|
3951
4098
|
const output = numberField(usage, "output") || 0;
|
|
3952
4099
|
const cacheRead = numberField(usage, "cacheRead") || 0;
|
|
3953
4100
|
const cacheWrite = numberField(usage, "cacheWrite") || 0;
|
|
3954
|
-
const
|
|
4101
|
+
const explicitTotal = numberField(usage, "totalTokens") || 0;
|
|
4102
|
+
const partsSum = input + output + cacheRead + cacheWrite;
|
|
4103
|
+
const missing = Math.max(0, explicitTotal - partsSum);
|
|
4104
|
+
const billableOutput = output + missing;
|
|
4105
|
+
const totalTokens = partsSum + missing;
|
|
3955
4106
|
if (totalTokens <= 0) {
|
|
3956
4107
|
return void 0;
|
|
3957
4108
|
}
|
|
@@ -3959,7 +4110,7 @@ function piUsageFromMessage(message) {
|
|
|
3959
4110
|
const costUsd = numberField(cost, "total");
|
|
3960
4111
|
return {
|
|
3961
4112
|
tokensInput: input + cacheRead + cacheWrite || void 0,
|
|
3962
|
-
tokensOutput:
|
|
4113
|
+
tokensOutput: billableOutput || void 0,
|
|
3963
4114
|
tokensCachedInput: cacheRead + cacheWrite || void 0,
|
|
3964
4115
|
tokensCacheReadInput: cacheRead || void 0,
|
|
3965
4116
|
tokensCacheCreationInput: cacheWrite || void 0,
|
|
@@ -4786,7 +4937,7 @@ function defaultMachineName() {
|
|
|
4786
4937
|
init_fs();
|
|
4787
4938
|
|
|
4788
4939
|
// src/lib/logger.ts
|
|
4789
|
-
import { appendFile, mkdir as mkdir2, rename as rename2, stat as
|
|
4940
|
+
import { appendFile, mkdir as mkdir2, rename as rename2, stat as stat6 } from "node:fs/promises";
|
|
4790
4941
|
import { homedir as homedir2 } from "node:os";
|
|
4791
4942
|
import path12 from "node:path";
|
|
4792
4943
|
var MAX_BYTES = 1 * 1024 * 1024;
|
|
@@ -4804,7 +4955,7 @@ function serializeError(error) {
|
|
|
4804
4955
|
}
|
|
4805
4956
|
async function rotateIfNeeded(file) {
|
|
4806
4957
|
try {
|
|
4807
|
-
const info = await
|
|
4958
|
+
const info = await stat6(file);
|
|
4808
4959
|
if (info.size > MAX_BYTES) {
|
|
4809
4960
|
await rename2(file, `${file}.1`).catch(() => {
|
|
4810
4961
|
});
|
|
@@ -5091,7 +5242,7 @@ async function deleteMachine(remote, id) {
|
|
|
5091
5242
|
}
|
|
5092
5243
|
|
|
5093
5244
|
// src/lib/types.ts
|
|
5094
|
-
var BACKFILL_STATE_SCHEMA_VERSION =
|
|
5245
|
+
var BACKFILL_STATE_SCHEMA_VERSION = 5;
|
|
5095
5246
|
|
|
5096
5247
|
// src/cli.ts
|
|
5097
5248
|
function createRegistry() {
|
|
@@ -5162,8 +5313,8 @@ function createCli(ctx, registry) {
|
|
|
5162
5313
|
cli.command("hook", "Read agent hook JSON from stdin and report a throttled event").option("--agent <name>", "Agent name").option("--project <name>", "Project name").option("--min-interval <seconds>", "Minimum seconds between similar hook reports").action((options) => hookCommand(normalizeOptions(options), ctx));
|
|
5163
5314
|
cli.command("sync-local-trigger", "Trigger one background local sync with throttle and locking").option("--min-interval <seconds>", "Minimum seconds between sync triggers").action((options) => syncLocalTriggerCommand(normalizeOptions(options), ctx, registry));
|
|
5164
5315
|
cli.command("sync-local-runner", "Internal background local sync runner").option("--lock-file <path>", "Lock file for the active sync").option("--state-file <path>", "State file for trigger metadata").action((options) => syncLocalRunnerCommand(normalizeOptions(options), ctx, registry));
|
|
5165
|
-
cli.command("backfill [action]", "Inspect local history import candidates").option("--source <source>", "Backfill source").option("--since <time>", "Only include history after this time").option("--until <time>", "Only include history before this time").option("--project <name>", "Project filter").option("--source-root <path>", "Override source history root").option("--include-source-path", "Include local source paths in output").option("--import-run <id>", "Import run id for verify/resume workflows").option("--limit <count>", "Maximum session files to parse").option("--batch-size <count>", "Max rollups per request (also bounded by --batch-bytes)").option("--batch-bytes <bytes>", "Soft byte cap for the JSON body of a single ingest POST").option("--replace", "Replace conflicting records during import (default)").option("--skip-conflicts", "Skip conflicting records instead of replacing them").option("--force", "
|
|
5166
|
-
cli.command("sync", "Import and upload all local agent history (shorthand for `backfill import --source all`)").option("--source <source>", "Limit to one source (default: all)").option("--since <time>", "Only include history after this time").option("--until <time>", "Only include history before this time").option("--project <name>", "Project filter").option("--batch-size <count>", "Max rollups per request (also bounded by --batch-bytes)").option("--force", "
|
|
5316
|
+
cli.command("backfill [action]", "Inspect local history import candidates").option("--source <source>", "Backfill source").option("--since <time>", "Only include history after this time").option("--until <time>", "Only include history before this time").option("--project <name>", "Project filter").option("--source-root <path>", "Override source history root").option("--include-source-path", "Include local source paths in output").option("--import-run <id>", "Import run id for verify/resume workflows").option("--limit <count>", "Maximum session files to parse").option("--batch-size <count>", "Max rollups per request (also bounded by --batch-bytes)").option("--batch-bytes <bytes>", "Soft byte cap for the JSON body of a single ingest POST").option("--replace", "Replace conflicting records during import (default)").option("--skip-conflicts", "Skip conflicting records instead of replacing them").option("--force", "Full re-import: re-parse every file and overwrite matching rollups (non-destructive \u2014 nothing is deleted)").option("--purge", "Before re-importing, delete THIS machine's existing rollups for the source(s). Destructive: also removes rollups whose local files are gone. Implies --force").action((action, options) => backfillCommand({ ...normalizeOptions(options), action }, ctx, registry));
|
|
5317
|
+
cli.command("sync", "Import and upload all local agent history (shorthand for `backfill import --source all`)").option("--source <source>", "Limit to one source (default: all)").option("--since <time>", "Only include history after this time").option("--until <time>", "Only include history before this time").option("--project <name>", "Project filter").option("--batch-size <count>", "Max rollups per request (also bounded by --batch-bytes)").option("--force", "Full re-import: re-parse every file and overwrite matching rollups (non-destructive \u2014 nothing is deleted)").option("--purge", "Before re-importing, delete THIS machine's existing rollups for the source(s). Destructive: also removes rollups whose local files are gone. Implies --force").option("--dry-run", "Print the planned import without uploading").action((options) => {
|
|
5167
5318
|
const opts = normalizeOptions(options);
|
|
5168
5319
|
return backfillCommand({ ...opts, action: "import", source: stringOption(opts.source) || "all" }, ctx, registry);
|
|
5169
5320
|
});
|
|
@@ -5515,11 +5666,15 @@ async function listBackfillSourceFiles(source, options, ctx) {
|
|
|
5515
5666
|
if (source.id === "gemini") {
|
|
5516
5667
|
return geminiBackfillFiles(stringOption(options["source-root"]), resolveHome(options, ctx), ctx.env);
|
|
5517
5668
|
}
|
|
5669
|
+
if (source.id === "codex") {
|
|
5670
|
+
const files2 = await codexBackfillFiles(stringOption(options["source-root"]), resolveHome(options, ctx), ctx.env);
|
|
5671
|
+
return files2.sort((a, b) => a.path.localeCompare(b.path)).slice(0, numberOption(options.limit) || void 0);
|
|
5672
|
+
}
|
|
5518
5673
|
const roots = stringOption(options["source-root"]) ? [requiredOption(options, "source-root")] : source.paths;
|
|
5519
5674
|
const fileLists = await Promise.all(roots.map((r) => listJsonlFiles(r)));
|
|
5520
5675
|
const files = fileLists.flat().sort().slice(0, numberOption(options.limit) || void 0);
|
|
5521
5676
|
return Promise.all(files.map(async (filePath) => {
|
|
5522
|
-
const info = await
|
|
5677
|
+
const info = await stat7(filePath);
|
|
5523
5678
|
return { path: filePath, modifiedAt: info.mtime.toISOString() };
|
|
5524
5679
|
}));
|
|
5525
5680
|
}
|
|
@@ -5603,8 +5758,11 @@ async function importBackfillPlan(plan, options, ctx, registry) {
|
|
|
5603
5758
|
return 1;
|
|
5604
5759
|
}
|
|
5605
5760
|
const sourceDefs = registry.all().filter((a) => supportedSources.has(a.id) && (source === "all" || a.id === source)).map((a) => ({ id: a.id, label: a.label, paths: a.sourcePaths(home, ctx.env) }));
|
|
5606
|
-
if (options.
|
|
5607
|
-
await
|
|
5761
|
+
if (options.purge) {
|
|
5762
|
+
await purgeSourceRollups(sourceDefs, options, ctx);
|
|
5763
|
+
}
|
|
5764
|
+
if (options.force || options.purge) {
|
|
5765
|
+
await clearBackfillWatermark(home, ctx);
|
|
5608
5766
|
}
|
|
5609
5767
|
const incrementalState = shouldUseIncrementalBackfill(options) ? await readBackfillIncrementalState(home, ctx) : void 0;
|
|
5610
5768
|
if (!options.json) {
|
|
@@ -5638,13 +5796,15 @@ async function importBackfillPlan(plan, options, ctx, registry) {
|
|
|
5638
5796
|
}
|
|
5639
5797
|
return counts.failed > 0 || counts.conflicts > 0 && !options["skip-conflicts"] ? 1 : 0;
|
|
5640
5798
|
}
|
|
5641
|
-
async function
|
|
5799
|
+
async function clearBackfillWatermark(home, ctx) {
|
|
5642
5800
|
try {
|
|
5643
5801
|
await rm2(backfillIncrementalStatePath(home), { force: true });
|
|
5644
5802
|
} catch (error) {
|
|
5645
5803
|
debug(ctx, `Failed to clear backfill watermark: ${error.message}
|
|
5646
5804
|
`);
|
|
5647
5805
|
}
|
|
5806
|
+
}
|
|
5807
|
+
async function purgeSourceRollups(sourceDefs, options, ctx) {
|
|
5648
5808
|
for (const item of sourceDefs) {
|
|
5649
5809
|
try {
|
|
5650
5810
|
const deleted = await deleteSessionRollupsBySourceAPI(item.id, options, ctx);
|
package/package.json
CHANGED