codetime-cli 0.7.1 → 0.7.3
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 +186 -20
- package/package.json +1 -1
package/bin/codetime.mjs
CHANGED
|
@@ -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.3" : "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;
|
|
@@ -2316,6 +2397,9 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2316
2397
|
let turnIdAtLastUserMessage;
|
|
2317
2398
|
let sessionMetaLocked = false;
|
|
2318
2399
|
let lastTokenUsageKey;
|
|
2400
|
+
const replaySecond = detectSubagentReplaySecond(text, lines);
|
|
2401
|
+
let skipReplay = replaySecond !== void 0;
|
|
2402
|
+
let previousTotals = { input: 0, cached: 0, output: 0, reasoning: 0, total: 0 };
|
|
2319
2403
|
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
2320
2404
|
const turnLastEventAt = /* @__PURE__ */ new Map();
|
|
2321
2405
|
const turnStartedAt = /* @__PURE__ */ new Map();
|
|
@@ -2473,8 +2557,19 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2473
2557
|
if (tierFromInfo) {
|
|
2474
2558
|
serviceTier = tierFromInfo.toLowerCase();
|
|
2475
2559
|
}
|
|
2476
|
-
const
|
|
2560
|
+
const totalUsage = objectField(info, "total_token_usage");
|
|
2561
|
+
const hasTotal = Object.keys(totalUsage).length > 0;
|
|
2562
|
+
const usage = tokenUsageFromPayload(payload) ?? (hasTotal ? codexUsageDelta(totalUsage, previousTotals, info) : void 0);
|
|
2563
|
+
if (hasTotal) {
|
|
2564
|
+
previousTotals = readCodexCumulative(totalUsage);
|
|
2565
|
+
}
|
|
2477
2566
|
if (usage) {
|
|
2567
|
+
if (skipReplay) {
|
|
2568
|
+
if (ts.slice(0, 19) === replaySecond) {
|
|
2569
|
+
break;
|
|
2570
|
+
}
|
|
2571
|
+
skipReplay = false;
|
|
2572
|
+
}
|
|
2478
2573
|
const usageKey = [
|
|
2479
2574
|
usage.tokensInput,
|
|
2480
2575
|
usage.tokensCachedInput,
|
|
@@ -2727,7 +2822,9 @@ function headlessCodexUsage(raw) {
|
|
|
2727
2822
|
const billableOutput = output + reasoning;
|
|
2728
2823
|
return {
|
|
2729
2824
|
tokensInput: input || void 0,
|
|
2730
|
-
|
|
2825
|
+
// Clamp cached to input so non-cached (input - cached) never goes negative
|
|
2826
|
+
// (ccusage cached.min(input) in the headless path too).
|
|
2827
|
+
tokensCachedInput: Math.min(cached, input) || void 0,
|
|
2731
2828
|
tokensOutput: billableOutput || void 0,
|
|
2732
2829
|
tokensReasoningOutput: reasoning || void 0,
|
|
2733
2830
|
tokensTotal: total,
|
|
@@ -2755,21 +2852,85 @@ function headlessCodexTimestamp(raw) {
|
|
|
2755
2852
|
}
|
|
2756
2853
|
return void 0;
|
|
2757
2854
|
}
|
|
2855
|
+
function detectSubagentReplaySecond(text, lines) {
|
|
2856
|
+
if (!text.includes("thread_spawn")) {
|
|
2857
|
+
return void 0;
|
|
2858
|
+
}
|
|
2859
|
+
let firstSecond;
|
|
2860
|
+
for (const line of lines) {
|
|
2861
|
+
const raw = parseJsonLine(line);
|
|
2862
|
+
if (!raw || stringField(raw, "type") !== "event_msg") {
|
|
2863
|
+
continue;
|
|
2864
|
+
}
|
|
2865
|
+
const payload = objectField(raw, "payload");
|
|
2866
|
+
if (stringField(payload, "type") !== "token_count") {
|
|
2867
|
+
continue;
|
|
2868
|
+
}
|
|
2869
|
+
const info = objectField(payload, "info");
|
|
2870
|
+
const hasUsage = Object.keys(objectField(info, "last_token_usage")).length > 0 || Object.keys(objectField(info, "total_token_usage")).length > 0;
|
|
2871
|
+
if (!hasUsage) {
|
|
2872
|
+
continue;
|
|
2873
|
+
}
|
|
2874
|
+
const ts = timestampFrom(raw.timestamp) || timestampFrom(payload.timestamp);
|
|
2875
|
+
if (!ts) {
|
|
2876
|
+
continue;
|
|
2877
|
+
}
|
|
2878
|
+
const second = ts.slice(0, 19);
|
|
2879
|
+
if (firstSecond === void 0) {
|
|
2880
|
+
firstSecond = second;
|
|
2881
|
+
} else {
|
|
2882
|
+
return firstSecond === second ? firstSecond : void 0;
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
return void 0;
|
|
2886
|
+
}
|
|
2758
2887
|
function tokenUsageFromPayload(payload) {
|
|
2759
2888
|
const info = objectField(payload, "info");
|
|
2760
2889
|
const usage = objectField(info, "last_token_usage");
|
|
2761
2890
|
if (Object.keys(usage).length === 0) {
|
|
2762
2891
|
return;
|
|
2763
2892
|
}
|
|
2893
|
+
const input = numberField(usage, "input_tokens");
|
|
2894
|
+
const cached = numberField(usage, "cached_input_tokens");
|
|
2764
2895
|
return {
|
|
2765
|
-
tokensInput:
|
|
2766
|
-
|
|
2896
|
+
tokensInput: input,
|
|
2897
|
+
// Cached input can never exceed total input; clamp so the server's
|
|
2898
|
+
// non-cached = input - cached never goes negative (ccusage cached.min(input)).
|
|
2899
|
+
tokensCachedInput: cached === void 0 ? void 0 : Math.min(cached, input ?? 0),
|
|
2767
2900
|
tokensOutput: numberField(usage, "output_tokens"),
|
|
2768
2901
|
tokensReasoningOutput: numberField(usage, "reasoning_output_tokens"),
|
|
2769
2902
|
tokensTotal: numberField(usage, "total_tokens"),
|
|
2770
2903
|
modelContextWindow: numberField(info, "model_context_window")
|
|
2771
2904
|
};
|
|
2772
2905
|
}
|
|
2906
|
+
function readCodexCumulative(usage) {
|
|
2907
|
+
return {
|
|
2908
|
+
input: numberField(usage, "input_tokens") ?? 0,
|
|
2909
|
+
cached: numberField(usage, "cached_input_tokens") ?? 0,
|
|
2910
|
+
output: numberField(usage, "output_tokens") ?? 0,
|
|
2911
|
+
reasoning: numberField(usage, "reasoning_output_tokens") ?? 0,
|
|
2912
|
+
total: numberField(usage, "total_tokens") ?? 0
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
function codexUsageDelta(totalUsage, previous, info) {
|
|
2916
|
+
const cur = readCodexCumulative(totalUsage);
|
|
2917
|
+
const input = Math.max(0, cur.input - previous.input);
|
|
2918
|
+
const cached = Math.max(0, cur.cached - previous.cached);
|
|
2919
|
+
const output = Math.max(0, cur.output - previous.output);
|
|
2920
|
+
const reasoning = Math.max(0, cur.reasoning - previous.reasoning);
|
|
2921
|
+
const total = Math.max(0, cur.total - previous.total);
|
|
2922
|
+
if (input === 0 && cached === 0 && output === 0 && reasoning === 0 && total === 0) {
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
return {
|
|
2926
|
+
tokensInput: input || void 0,
|
|
2927
|
+
tokensCachedInput: Math.min(cached, input) || void 0,
|
|
2928
|
+
tokensOutput: output || void 0,
|
|
2929
|
+
tokensReasoningOutput: reasoning || void 0,
|
|
2930
|
+
tokensTotal: total || void 0,
|
|
2931
|
+
modelContextWindow: numberField(info, "model_context_window")
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2773
2934
|
function toolNameFromPayload(payload, payloadType) {
|
|
2774
2935
|
if (payloadType === "web_search_call") {
|
|
2775
2936
|
return "web_search";
|
|
@@ -2996,11 +3157,10 @@ function readStats(record) {
|
|
|
2996
3157
|
return void 0;
|
|
2997
3158
|
}
|
|
2998
3159
|
function parseJsonRecord(record, fileStem, fallbackTs) {
|
|
2999
|
-
const sessionId =
|
|
3160
|
+
const sessionId = nonEmptyStringField(record, "sessionId") ?? nonEmptyStringField(record, "session_id") ?? fileStem;
|
|
3000
3161
|
const sessionTs = timestampFrom(stringField(record, "startTime")) ?? timestampFrom(stringField(record, "lastUpdated")) ?? fallbackTs;
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
const usages = messages.filter((message) => stringField(message, "type") === "gemini").map((message) => parseDirectEvent(message, void 0, sessionTs)).filter((usage) => usage !== void 0);
|
|
3162
|
+
if (Array.isArray(record.messages)) {
|
|
3163
|
+
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);
|
|
3004
3164
|
return { sessionId, usages };
|
|
3005
3165
|
}
|
|
3006
3166
|
if (stringField(record, "type") === "gemini") {
|
|
@@ -3023,7 +3183,7 @@ function parseJsonlRecords(lines, fileStem, fallbackTs) {
|
|
|
3023
3183
|
if (!record) {
|
|
3024
3184
|
continue;
|
|
3025
3185
|
}
|
|
3026
|
-
const sid =
|
|
3186
|
+
const sid = nonEmptyStringField(record, "sessionId") ?? nonEmptyStringField(record, "session_id");
|
|
3027
3187
|
if (sid) {
|
|
3028
3188
|
sessionId = sid;
|
|
3029
3189
|
}
|
|
@@ -3036,7 +3196,7 @@ function parseJsonlRecords(lines, fileStem, fallbackTs) {
|
|
|
3036
3196
|
if (!usage) {
|
|
3037
3197
|
continue;
|
|
3038
3198
|
}
|
|
3039
|
-
const id =
|
|
3199
|
+
const id = nonEmptyStringField(record, "id");
|
|
3040
3200
|
if (id === void 0) {
|
|
3041
3201
|
usages.push(usage);
|
|
3042
3202
|
} else {
|
|
@@ -3317,7 +3477,9 @@ async function parseOpenCodeSessionFile(dbPath, options) {
|
|
|
3317
3477
|
const createdTs = timeCreated;
|
|
3318
3478
|
const tokens = opencodeUsageFromInfo(info);
|
|
3319
3479
|
const cost = typeof info.cost === "number" && info.cost > 0 ? info.cost : void 0;
|
|
3480
|
+
let messageHadUsage = false;
|
|
3320
3481
|
if (tokens) {
|
|
3482
|
+
messageHadUsage = true;
|
|
3321
3483
|
const metrics = {
|
|
3322
3484
|
tokensInput: tokens.tokensInput,
|
|
3323
3485
|
tokensOutput: tokens.tokensOutput,
|
|
@@ -3438,7 +3600,7 @@ async function parseOpenCodeSessionFile(dbPath, options) {
|
|
|
3438
3600
|
}
|
|
3439
3601
|
}
|
|
3440
3602
|
}
|
|
3441
|
-
if (pd.type === "step-finish") {
|
|
3603
|
+
if (pd.type === "step-finish" && !messageHadUsage) {
|
|
3442
3604
|
const stepTokens = opencodeUsageFromInfo(pd);
|
|
3443
3605
|
const stepCost = typeof pd.cost === "number" && pd.cost > 0 ? pd.cost : void 0;
|
|
3444
3606
|
if (stepTokens) {
|
|
@@ -3911,7 +4073,11 @@ function piUsageFromMessage(message) {
|
|
|
3911
4073
|
const output = numberField(usage, "output") || 0;
|
|
3912
4074
|
const cacheRead = numberField(usage, "cacheRead") || 0;
|
|
3913
4075
|
const cacheWrite = numberField(usage, "cacheWrite") || 0;
|
|
3914
|
-
const
|
|
4076
|
+
const explicitTotal = numberField(usage, "totalTokens") || 0;
|
|
4077
|
+
const partsSum = input + output + cacheRead + cacheWrite;
|
|
4078
|
+
const missing = Math.max(0, explicitTotal - partsSum);
|
|
4079
|
+
const billableOutput = output + missing;
|
|
4080
|
+
const totalTokens = partsSum + missing;
|
|
3915
4081
|
if (totalTokens <= 0) {
|
|
3916
4082
|
return void 0;
|
|
3917
4083
|
}
|
|
@@ -3919,7 +4085,7 @@ function piUsageFromMessage(message) {
|
|
|
3919
4085
|
const costUsd = numberField(cost, "total");
|
|
3920
4086
|
return {
|
|
3921
4087
|
tokensInput: input + cacheRead + cacheWrite || void 0,
|
|
3922
|
-
tokensOutput:
|
|
4088
|
+
tokensOutput: billableOutput || void 0,
|
|
3923
4089
|
tokensCachedInput: cacheRead + cacheWrite || void 0,
|
|
3924
4090
|
tokensCacheReadInput: cacheRead || void 0,
|
|
3925
4091
|
tokensCacheCreationInput: cacheWrite || void 0,
|
package/package.json
CHANGED