codetime-cli 0.7.2 → 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 +146 -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;
|
|
@@ -2318,6 +2399,7 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2318
2399
|
let lastTokenUsageKey;
|
|
2319
2400
|
const replaySecond = detectSubagentReplaySecond(text, lines);
|
|
2320
2401
|
let skipReplay = replaySecond !== void 0;
|
|
2402
|
+
let previousTotals = { input: 0, cached: 0, output: 0, reasoning: 0, total: 0 };
|
|
2321
2403
|
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
2322
2404
|
const turnLastEventAt = /* @__PURE__ */ new Map();
|
|
2323
2405
|
const turnStartedAt = /* @__PURE__ */ new Map();
|
|
@@ -2475,7 +2557,12 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2475
2557
|
if (tierFromInfo) {
|
|
2476
2558
|
serviceTier = tierFromInfo.toLowerCase();
|
|
2477
2559
|
}
|
|
2478
|
-
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
|
+
}
|
|
2479
2566
|
if (usage) {
|
|
2480
2567
|
if (skipReplay) {
|
|
2481
2568
|
if (ts.slice(0, 19) === replaySecond) {
|
|
@@ -2735,7 +2822,9 @@ function headlessCodexUsage(raw) {
|
|
|
2735
2822
|
const billableOutput = output + reasoning;
|
|
2736
2823
|
return {
|
|
2737
2824
|
tokensInput: input || void 0,
|
|
2738
|
-
|
|
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,
|
|
2739
2828
|
tokensOutput: billableOutput || void 0,
|
|
2740
2829
|
tokensReasoningOutput: reasoning || void 0,
|
|
2741
2830
|
tokensTotal: total,
|
|
@@ -2801,15 +2890,47 @@ function tokenUsageFromPayload(payload) {
|
|
|
2801
2890
|
if (Object.keys(usage).length === 0) {
|
|
2802
2891
|
return;
|
|
2803
2892
|
}
|
|
2893
|
+
const input = numberField(usage, "input_tokens");
|
|
2894
|
+
const cached = numberField(usage, "cached_input_tokens");
|
|
2804
2895
|
return {
|
|
2805
|
-
tokensInput:
|
|
2806
|
-
|
|
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),
|
|
2807
2900
|
tokensOutput: numberField(usage, "output_tokens"),
|
|
2808
2901
|
tokensReasoningOutput: numberField(usage, "reasoning_output_tokens"),
|
|
2809
2902
|
tokensTotal: numberField(usage, "total_tokens"),
|
|
2810
2903
|
modelContextWindow: numberField(info, "model_context_window")
|
|
2811
2904
|
};
|
|
2812
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
|
+
}
|
|
2813
2934
|
function toolNameFromPayload(payload, payloadType) {
|
|
2814
2935
|
if (payloadType === "web_search_call") {
|
|
2815
2936
|
return "web_search";
|
|
@@ -3036,11 +3157,10 @@ function readStats(record) {
|
|
|
3036
3157
|
return void 0;
|
|
3037
3158
|
}
|
|
3038
3159
|
function parseJsonRecord(record, fileStem, fallbackTs) {
|
|
3039
|
-
const sessionId =
|
|
3160
|
+
const sessionId = nonEmptyStringField(record, "sessionId") ?? nonEmptyStringField(record, "session_id") ?? fileStem;
|
|
3040
3161
|
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);
|
|
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);
|
|
3044
3164
|
return { sessionId, usages };
|
|
3045
3165
|
}
|
|
3046
3166
|
if (stringField(record, "type") === "gemini") {
|
|
@@ -3063,7 +3183,7 @@ function parseJsonlRecords(lines, fileStem, fallbackTs) {
|
|
|
3063
3183
|
if (!record) {
|
|
3064
3184
|
continue;
|
|
3065
3185
|
}
|
|
3066
|
-
const sid =
|
|
3186
|
+
const sid = nonEmptyStringField(record, "sessionId") ?? nonEmptyStringField(record, "session_id");
|
|
3067
3187
|
if (sid) {
|
|
3068
3188
|
sessionId = sid;
|
|
3069
3189
|
}
|
|
@@ -3076,7 +3196,7 @@ function parseJsonlRecords(lines, fileStem, fallbackTs) {
|
|
|
3076
3196
|
if (!usage) {
|
|
3077
3197
|
continue;
|
|
3078
3198
|
}
|
|
3079
|
-
const id =
|
|
3199
|
+
const id = nonEmptyStringField(record, "id");
|
|
3080
3200
|
if (id === void 0) {
|
|
3081
3201
|
usages.push(usage);
|
|
3082
3202
|
} else {
|
|
@@ -3357,7 +3477,9 @@ async function parseOpenCodeSessionFile(dbPath, options) {
|
|
|
3357
3477
|
const createdTs = timeCreated;
|
|
3358
3478
|
const tokens = opencodeUsageFromInfo(info);
|
|
3359
3479
|
const cost = typeof info.cost === "number" && info.cost > 0 ? info.cost : void 0;
|
|
3480
|
+
let messageHadUsage = false;
|
|
3360
3481
|
if (tokens) {
|
|
3482
|
+
messageHadUsage = true;
|
|
3361
3483
|
const metrics = {
|
|
3362
3484
|
tokensInput: tokens.tokensInput,
|
|
3363
3485
|
tokensOutput: tokens.tokensOutput,
|
|
@@ -3478,7 +3600,7 @@ async function parseOpenCodeSessionFile(dbPath, options) {
|
|
|
3478
3600
|
}
|
|
3479
3601
|
}
|
|
3480
3602
|
}
|
|
3481
|
-
if (pd.type === "step-finish") {
|
|
3603
|
+
if (pd.type === "step-finish" && !messageHadUsage) {
|
|
3482
3604
|
const stepTokens = opencodeUsageFromInfo(pd);
|
|
3483
3605
|
const stepCost = typeof pd.cost === "number" && pd.cost > 0 ? pd.cost : void 0;
|
|
3484
3606
|
if (stepTokens) {
|
|
@@ -3951,7 +4073,11 @@ function piUsageFromMessage(message) {
|
|
|
3951
4073
|
const output = numberField(usage, "output") || 0;
|
|
3952
4074
|
const cacheRead = numberField(usage, "cacheRead") || 0;
|
|
3953
4075
|
const cacheWrite = numberField(usage, "cacheWrite") || 0;
|
|
3954
|
-
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;
|
|
3955
4081
|
if (totalTokens <= 0) {
|
|
3956
4082
|
return void 0;
|
|
3957
4083
|
}
|
|
@@ -3959,7 +4085,7 @@ function piUsageFromMessage(message) {
|
|
|
3959
4085
|
const costUsd = numberField(cost, "total");
|
|
3960
4086
|
return {
|
|
3961
4087
|
tokensInput: input + cacheRead + cacheWrite || void 0,
|
|
3962
|
-
tokensOutput:
|
|
4088
|
+
tokensOutput: billableOutput || void 0,
|
|
3963
4089
|
tokensCachedInput: cacheRead + cacheWrite || void 0,
|
|
3964
4090
|
tokensCacheReadInput: cacheRead || void 0,
|
|
3965
4091
|
tokensCacheCreationInput: cacheWrite || void 0,
|
package/package.json
CHANGED