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.
Files changed (2) hide show
  1. package/bin/codetime.mjs +197 -37
  2. 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 stat6 } from "node:fs/promises";
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.2" : "0.1.1";
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
- return [];
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 + output + cacheRead + cacheWrite;
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: output || void 0,
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
- model = stringField(message, "model") || model;
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 usage = tokenUsageFromPayload(payload);
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
- tokensCachedInput: cached || void 0,
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: numberField(usage, "input_tokens"),
2806
- tokensCachedInput: numberField(usage, "cached_input_tokens"),
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 stat4 } from "node:fs/promises";
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 = stringField(record, "sessionId") ?? stringField(record, "session_id") ?? fileStem;
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
- const messages = arrayField(record, "messages").filter(isPlainObject);
3042
- if (messages.length > 0) {
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 = stringField(record, "sessionId") ?? stringField(record, "session_id");
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 = stringField(record, "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 stat4(filePath);
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 stat4(filePath);
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: stat7 } = await import("node:fs/promises");
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 stat7(sourceRoot).catch(() => null);
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 stat7(candidatePath).catch(() => null);
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 totalTokens = numberField(usage, "totalTokens") || input + output + cacheRead + cacheWrite;
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: output || void 0,
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 stat5 } from "node:fs/promises";
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 stat5(file);
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 = 4;
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", "Force full re-import: clear watermark and re-process all files").action((action, options) => backfillCommand({ ...normalizeOptions(options), action }, ctx, registry));
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", "Force full re-import: clear watermark and re-process all files").option("--dry-run", "Print the planned import without uploading").action((options) => {
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 stat6(filePath);
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.force) {
5607
- await purgeForcedSources(sourceDefs, home, options, ctx);
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 purgeForcedSources(sourceDefs, home, options, ctx) {
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codetime-cli",
3
3
  "type": "module",
4
- "version": "0.7.2",
4
+ "version": "0.7.4",
5
5
  "description": "codetime CLI — install AI-agent hooks (Claude Code, Codex, OpenCode, Pi) and report activity to codetime.dev.",
6
6
  "license": "MIT",
7
7
  "publishConfig": {