codetime-cli 0.3.2 → 0.4.0

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 +465 -96
  2. package/package.json +2 -2
package/bin/codetime.mjs CHANGED
@@ -152,9 +152,9 @@ var init_fs = __esm({
152
152
 
153
153
  // src/cli.ts
154
154
  import { spawn } from "node:child_process";
155
- import { mkdir as mkdir3, rm, stat as stat5, writeFile as writeFile2 } from "node:fs/promises";
155
+ import { mkdir as mkdir3, rm, stat as stat6, writeFile as writeFile2 } from "node:fs/promises";
156
156
  import os3 from "node:os";
157
- import path12 from "node:path";
157
+ import path13 from "node:path";
158
158
  import { fileURLToPath } from "node:url";
159
159
 
160
160
  // ../shared/src/index.ts
@@ -190,6 +190,7 @@ var TELEMETRY_EVENT_TYPES = [
190
190
  "agent.operation"
191
191
  ];
192
192
  var FILE_ACTIVITY_OPERATIONS = ["read", "search", "create", "write", "edit", "delete"];
193
+ var BACKFILL_SOURCE_IDS = ["codex", "claude-code", "opencode", "pi", "amp", "gemini"];
193
194
  function createWorkspaceId(input) {
194
195
  const basis = input.repoUrl || input.repoRoot || input.projectName || "unknown";
195
196
  return `workspace_${fnv1a(basis)}`;
@@ -972,7 +973,7 @@ import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
972
973
  import path2 from "node:path";
973
974
 
974
975
  // src/lib/constants.ts
975
- var PACKAGE_VERSION = true ? "0.3.2" : "0.1.1";
976
+ var PACKAGE_VERSION = true ? "0.4.0" : "0.1.1";
976
977
  var DEFAULT_API_URL = "https://codetime.dev";
977
978
  var DEFAULT_BACKFILL_BATCH_SIZE = 50;
978
979
  var DEFAULT_BACKFILL_BATCH_BYTES = 800 * 1024;
@@ -997,6 +998,12 @@ function objectField(object, key) {
997
998
  }
998
999
  return object[key];
999
1000
  }
1001
+ function arrayField(object, key) {
1002
+ if (!isPlainObject(object) || !Array.isArray(object[key])) {
1003
+ return [];
1004
+ }
1005
+ return object[key];
1006
+ }
1000
1007
  function isPlainObject(value) {
1001
1008
  return value !== null && typeof value === "object" && !Array.isArray(value);
1002
1009
  }
@@ -1966,7 +1973,7 @@ function fileActivitiesFromClaudeToolUse(tool, input, ts, cwd) {
1966
1973
  const metrics = fileMetricsFromClaudeToolInput(tool, input);
1967
1974
  addResolvedPathActivity(changes, filePath, operation, ts, cwd, workdir, metrics);
1968
1975
  }
1969
- for (const edit of arrayField(input, "edits")) {
1976
+ for (const edit of arrayField2(input, "edits")) {
1970
1977
  if (!isPlainObject(edit)) {
1971
1978
  continue;
1972
1979
  }
@@ -2010,17 +2017,17 @@ function fileMetricsFromClaudeToolInput(tool, input) {
2010
2017
  }
2011
2018
  return {};
2012
2019
  }
2013
- function arrayField(object, key) {
2020
+ function arrayField2(object, key) {
2014
2021
  if (!isPlainObject(object) || !Array.isArray(object[key])) {
2015
2022
  return [];
2016
2023
  }
2017
2024
  return object[key];
2018
2025
  }
2019
2026
  function claudeToolUseItems(message) {
2020
- return arrayField(message, "content").filter((item) => isPlainObject(item) && item.type === "tool_use");
2027
+ return arrayField2(message, "content").filter((item) => isPlainObject(item) && item.type === "tool_use");
2021
2028
  }
2022
2029
  function claudeToolResultItems(message) {
2023
- return arrayField(message, "content").filter((item) => isPlainObject(item) && item.type === "tool_result");
2030
+ return arrayField2(message, "content").filter((item) => isPlainObject(item) && item.type === "tool_result");
2024
2031
  }
2025
2032
  function isClaudeToolResultMessage(message) {
2026
2033
  return claudeToolResultItems(message).length > 0;
@@ -2223,12 +2230,13 @@ async function parseCodexSessionFile(filePath, options) {
2223
2230
  const text = await readFile4(filePath, "utf8");
2224
2231
  const lines = text.split("\n").filter(Boolean);
2225
2232
  const sourcePathHash = `sha256:${createStableHash(filePath)}`;
2226
- const serviceTier = await resolveCodexServiceTier(filePath);
2227
2233
  const events = [];
2228
2234
  let sessionId = sessionIdFromFilePath(filePath, "codex");
2229
2235
  let cwd;
2230
2236
  let project;
2231
2237
  let model;
2238
+ let sessionStartEmitted = false;
2239
+ let serviceTier;
2232
2240
  let currentTurnId;
2233
2241
  let lastTurnIdForComplete;
2234
2242
  let turnIdAtLastUserMessage;
@@ -2267,6 +2275,7 @@ async function parseCodexSessionFile(filePath, options) {
2267
2275
  model,
2268
2276
  confidence: "exact"
2269
2277
  }, { filePath, sourcePathHash, lineNumber, topType, payloadType: "session_meta", options }));
2278
+ sessionStartEmitted = true;
2270
2279
  continue;
2271
2280
  }
2272
2281
  if (topType === "turn_context") {
@@ -2274,6 +2283,40 @@ async function parseCodexSessionFile(filePath, options) {
2274
2283
  cwd = stringField(payload, "cwd") || cwd;
2275
2284
  project = cwd ? path7.basename(cwd) : project;
2276
2285
  model = stringField(payload, "model") || model;
2286
+ const tier = stringField(payload, "service_tier");
2287
+ if (tier) {
2288
+ serviceTier = tier.toLowerCase();
2289
+ }
2290
+ continue;
2291
+ }
2292
+ if (topType === "turn.completed" || topType === "result" || topType === void 0) {
2293
+ const usage = headlessCodexUsage(raw);
2294
+ if (usage) {
2295
+ const parsedModel = headlessCodexModel(raw);
2296
+ if (parsedModel) {
2297
+ model = parsedModel;
2298
+ }
2299
+ const eventModel = parsedModel || model || "gpt-5";
2300
+ const headlessTs = headlessCodexTimestamp(raw) || ts;
2301
+ if (!sessionStartEmitted) {
2302
+ events.push(withBackfillRefs(baseCodexEvent({
2303
+ ts: headlessTs,
2304
+ type: "session.started",
2305
+ sessionId,
2306
+ model: eventModel,
2307
+ confidence: "derived"
2308
+ }), { filePath, sourcePathHash, lineNumber, topType: topType || "result", payloadType: "session", options }));
2309
+ sessionStartEmitted = true;
2310
+ }
2311
+ events.push(withBackfillRefs(baseCodexEvent({
2312
+ ts: headlessTs,
2313
+ type: "model.usage",
2314
+ sessionId,
2315
+ model: eventModel,
2316
+ confidence: "partial",
2317
+ metrics: usage
2318
+ }), { filePath, sourcePathHash, lineNumber, topType: topType || "result", payloadType: "headless", options }));
2319
+ }
2277
2320
  continue;
2278
2321
  }
2279
2322
  if (topType !== "event_msg" && topType !== "response_item") {
@@ -2333,6 +2376,11 @@ async function parseCodexSessionFile(filePath, options) {
2333
2376
  break;
2334
2377
  }
2335
2378
  case "token_count": {
2379
+ const info = objectField(payload, "info");
2380
+ const tierFromInfo = stringField(info, "service_tier") || stringField(payload, "service_tier");
2381
+ if (tierFromInfo) {
2382
+ serviceTier = tierFromInfo.toLowerCase();
2383
+ }
2336
2384
  const usage = tokenUsageFromPayload(payload);
2337
2385
  if (usage) {
2338
2386
  const usageKey = [
@@ -2545,43 +2593,6 @@ async function parseCodexSessionFile(filePath, options) {
2545
2593
  }
2546
2594
  return events.filter((event) => validateCanonicalEvent(event).valid);
2547
2595
  }
2548
- var codexServiceTierCache = /* @__PURE__ */ new Map();
2549
- async function resolveCodexServiceTier(sessionFilePath) {
2550
- const home = inferCodexHomeFromSessionPath(sessionFilePath);
2551
- if (!home) {
2552
- return void 0;
2553
- }
2554
- if (!codexServiceTierCache.has(home)) {
2555
- codexServiceTierCache.set(home, await readCodexServiceTier(home));
2556
- }
2557
- return codexServiceTierCache.get(home) ?? void 0;
2558
- }
2559
- function inferCodexHomeFromSessionPath(sessionFilePath) {
2560
- if (path7.basename(sessionFilePath) === "history.jsonl") {
2561
- return path7.dirname(sessionFilePath);
2562
- }
2563
- let dir = path7.dirname(sessionFilePath);
2564
- for (let depth = 0; depth < 10; depth += 1) {
2565
- const parent = path7.dirname(dir);
2566
- if (parent === dir) {
2567
- return void 0;
2568
- }
2569
- if (path7.basename(dir) === "sessions") {
2570
- return parent;
2571
- }
2572
- dir = parent;
2573
- }
2574
- return void 0;
2575
- }
2576
- async function readCodexServiceTier(codexHomePath) {
2577
- try {
2578
- const text = await readFile4(path7.join(codexHomePath, "config.toml"), "utf8");
2579
- const match = text.match(/(?:^|\n)\s*service_tier\s*=\s*["']?([a-z_]+)["']?/i);
2580
- return match ? match[1].toLowerCase() : null;
2581
- } catch {
2582
- return null;
2583
- }
2584
- }
2585
2596
  function rewriteCodexModelForTier(model, serviceTier) {
2586
2597
  if (!model) {
2587
2598
  return model;
@@ -2598,6 +2609,55 @@ function baseCodexEvent(event) {
2598
2609
  ...event
2599
2610
  };
2600
2611
  }
2612
+ function headlessCodexUsage(raw) {
2613
+ const usage = [
2614
+ objectField(raw, "usage"),
2615
+ objectField(objectField(raw, "data"), "usage"),
2616
+ objectField(objectField(raw, "result"), "usage"),
2617
+ objectField(objectField(raw, "response"), "usage")
2618
+ ].find((candidate) => Object.keys(candidate).length > 0);
2619
+ if (!usage) {
2620
+ return;
2621
+ }
2622
+ const input = numberField(usage, "input_tokens") ?? numberField(usage, "prompt_tokens") ?? 0;
2623
+ const output = numberField(usage, "output_tokens") ?? numberField(usage, "completion_tokens") ?? 0;
2624
+ const cached = numberField(usage, "cached_input_tokens") ?? numberField(usage, "cache_read_input_tokens") ?? numberField(usage, "cached_tokens") ?? 0;
2625
+ const reasoning = numberField(usage, "reasoning_output_tokens") ?? numberField(usage, "reasoning_tokens") ?? 0;
2626
+ const totalField = numberField(usage, "total_tokens");
2627
+ const total = totalField !== void 0 && (totalField > 0 || input + output + reasoning === 0) ? totalField : input + output + reasoning;
2628
+ if (input === 0 && cached === 0 && output === 0 && reasoning === 0 && total === 0) {
2629
+ return;
2630
+ }
2631
+ return {
2632
+ tokensInput: input || void 0,
2633
+ tokensCachedInput: cached || void 0,
2634
+ tokensOutput: output || void 0,
2635
+ tokensReasoningOutput: reasoning || void 0,
2636
+ tokensTotal: total,
2637
+ modelCalls: 1
2638
+ };
2639
+ }
2640
+ function headlessCodexModel(raw) {
2641
+ for (const source of [raw, objectField(raw, "data"), objectField(raw, "result"), objectField(raw, "response")]) {
2642
+ const model = stringField(source, "model") ?? stringField(source, "model_name");
2643
+ if (model) {
2644
+ return model;
2645
+ }
2646
+ }
2647
+ return void 0;
2648
+ }
2649
+ function headlessCodexTimestamp(raw) {
2650
+ const data = objectField(raw, "data");
2651
+ const result = objectField(raw, "result");
2652
+ const response = objectField(raw, "response");
2653
+ for (const source of [raw, data, result, response]) {
2654
+ const ts = timestampFrom(source.timestamp) ?? timestampFrom(source.created_at) ?? timestampFrom(source.createdAt);
2655
+ if (ts) {
2656
+ return ts;
2657
+ }
2658
+ }
2659
+ return void 0;
2660
+ }
2601
2661
  function tokenUsageFromPayload(payload) {
2602
2662
  const info = objectField(payload, "info");
2603
2663
  const usage = objectField(info, "last_token_usage");
@@ -2628,7 +2688,7 @@ function fileActivitiesFromFunctionCall(payload, payloadType, ts, cwd) {
2628
2688
  for (const key of ["file_path", "path", "notebook_path"]) {
2629
2689
  addResolvedPathActivity(changes, stringField(args, key), operation, ts, cwd, workdir);
2630
2690
  }
2631
- for (const file of arrayField2(args, "files")) {
2691
+ for (const file of arrayField3(args, "files")) {
2632
2692
  if (typeof file === "string") {
2633
2693
  addResolvedPathActivity(changes, file, operation, ts, cwd, workdir);
2634
2694
  }
@@ -2641,7 +2701,7 @@ function fileActivitiesFromFunctionCall(payload, payloadType, ts, cwd) {
2641
2701
  }
2642
2702
  return [...changes.values()];
2643
2703
  }
2644
- function arrayField2(object, key) {
2704
+ function arrayField3(object, key) {
2645
2705
  if (!isPlainObject(object) || !Array.isArray(object[key])) {
2646
2706
  return [];
2647
2707
  }
@@ -2726,9 +2786,314 @@ function createCodexAdapter() {
2726
2786
  };
2727
2787
  }
2728
2788
 
2789
+ // src/adapters/gemini.ts
2790
+ import { readFile as readFile5, stat as stat4 } from "node:fs/promises";
2791
+ import path8 from "node:path";
2792
+ init_fs();
2793
+ function tokenFromKeys(record, keys) {
2794
+ for (const key of keys) {
2795
+ const value = numberField(record, key);
2796
+ if (value !== void 0) {
2797
+ return Math.max(0, Math.trunc(value));
2798
+ }
2799
+ }
2800
+ return 0;
2801
+ }
2802
+ function parseTokens(value) {
2803
+ if (!isPlainObject(value)) {
2804
+ return void 0;
2805
+ }
2806
+ const totalRaw = numberField(value, "total") ?? numberField(value, "total_tokens");
2807
+ return {
2808
+ input: tokenFromKeys(value, ["input", "prompt", "input_tokens", "prompt_tokens"]),
2809
+ output: tokenFromKeys(value, ["output", "candidates", "output_tokens", "candidates_tokens"]),
2810
+ cached: tokenFromKeys(value, ["cached", "cached_tokens"]),
2811
+ thoughts: tokenFromKeys(value, ["thoughts", "reasoning", "thoughts_tokens", "reasoning_tokens"]),
2812
+ tool: tokenFromKeys(value, ["tool", "tool_tokens"]),
2813
+ total: totalRaw === void 0 ? void 0 : Math.max(0, Math.trunc(totalRaw))
2814
+ };
2815
+ }
2816
+ function subtractCachedOverlap(tokens) {
2817
+ const cacheRead = tokens.cached;
2818
+ const cachedPortion = Math.min(tokens.input, cacheRead);
2819
+ return [tokens.input - cachedPortion, cacheRead];
2820
+ }
2821
+ function normalizeSessionInput(tokens) {
2822
+ const inclusive = tokens.input + tokens.output + tokens.thoughts + tokens.tool;
2823
+ const exclusive = inclusive + tokens.cached;
2824
+ if (tokens.cached > 0 && tokens.total === inclusive && tokens.total !== exclusive) {
2825
+ return subtractCachedOverlap(tokens);
2826
+ }
2827
+ return [tokens.input, tokens.cached];
2828
+ }
2829
+ function buildUsage(model, ts, tokens, normalize, id) {
2830
+ const finalModel = model?.trim();
2831
+ if (!finalModel) {
2832
+ return void 0;
2833
+ }
2834
+ const [inputWithoutCache, cacheRead] = normalize(tokens);
2835
+ const inputTokens = inputWithoutCache + tokens.tool;
2836
+ const totalTokens = tokens.total ?? inputTokens + tokens.output + cacheRead + tokens.thoughts;
2837
+ let output = tokens.output;
2838
+ let reasoning = tokens.thoughts;
2839
+ const known = inputTokens + output + cacheRead + reasoning;
2840
+ const missing = Math.max(0, totalTokens - known);
2841
+ if (missing > 0) {
2842
+ if (output === 0) {
2843
+ output = missing;
2844
+ } else {
2845
+ reasoning += missing;
2846
+ }
2847
+ }
2848
+ if (inputTokens === 0 && output === 0 && cacheRead === 0 && reasoning === 0) {
2849
+ return void 0;
2850
+ }
2851
+ return {
2852
+ ts,
2853
+ model: finalModel,
2854
+ // codetime convention: tokensInput is cache-inclusive (see amp adapter).
2855
+ tokensInput: inputTokens + cacheRead,
2856
+ tokensOutput: output,
2857
+ tokensCacheRead: cacheRead,
2858
+ tokensReasoning: reasoning,
2859
+ tokensTotal: totalTokens,
2860
+ id
2861
+ };
2862
+ }
2863
+ function parseDirectEvent(record, modelHint, fallbackTs) {
2864
+ const tokens = parseTokens(record.tokens);
2865
+ if (!tokens) {
2866
+ return void 0;
2867
+ }
2868
+ const ts = timestampFrom(stringField(record, "timestamp")) ?? timestampFrom(stringField(record, "created_at")) ?? fallbackTs;
2869
+ const model = stringField(record, "model") ?? modelHint;
2870
+ return buildUsage(model, ts, tokens, normalizeSessionInput, stringField(record, "id"));
2871
+ }
2872
+ function parseStatsEvents(stats, modelHint, ts) {
2873
+ if (!isPlainObject(stats)) {
2874
+ return [];
2875
+ }
2876
+ const models = objectField(stats, "models");
2877
+ const perModel = Object.entries(models).filter((entry) => isPlainObject(entry[1])).map(([model, data]) => {
2878
+ const tokens2 = parseTokens(data.tokens);
2879
+ return tokens2 ? buildUsage(model, ts, tokens2, subtractCachedOverlap, void 0) : void 0;
2880
+ }).filter((usage2) => usage2 !== void 0);
2881
+ if (perModel.length > 0) {
2882
+ return perModel;
2883
+ }
2884
+ const tokens = parseTokens(stats);
2885
+ if (!tokens) {
2886
+ return [];
2887
+ }
2888
+ const usage = buildUsage(modelHint ?? "unknown", ts, tokens, subtractCachedOverlap, void 0);
2889
+ return usage ? [usage] : [];
2890
+ }
2891
+ function readStats(record) {
2892
+ if (isPlainObject(record.stats)) {
2893
+ return record.stats;
2894
+ }
2895
+ const result = record.result;
2896
+ if (isPlainObject(result) && isPlainObject(result.stats)) {
2897
+ return result.stats;
2898
+ }
2899
+ return void 0;
2900
+ }
2901
+ function parseJsonRecord(record, fileStem, fallbackTs) {
2902
+ const sessionId = stringField(record, "sessionId") ?? stringField(record, "session_id") ?? fileStem;
2903
+ const sessionTs = timestampFrom(stringField(record, "startTime")) ?? timestampFrom(stringField(record, "lastUpdated")) ?? fallbackTs;
2904
+ const messages = arrayField(record, "messages").filter(isPlainObject);
2905
+ if (messages.length > 0) {
2906
+ const usages = messages.filter((message) => stringField(message, "type") === "gemini").map((message) => parseDirectEvent(message, void 0, sessionTs)).filter((usage) => usage !== void 0);
2907
+ return { sessionId, usages };
2908
+ }
2909
+ if (stringField(record, "type") === "gemini") {
2910
+ const usage = parseDirectEvent(record, void 0, fallbackTs);
2911
+ return { sessionId, usages: usage ? [usage] : [] };
2912
+ }
2913
+ const statsTs = timestampFrom(stringField(record, "timestamp")) ?? fallbackTs;
2914
+ return {
2915
+ sessionId,
2916
+ usages: parseStatsEvents(readStats(record), stringField(record, "model"), statsTs)
2917
+ };
2918
+ }
2919
+ function parseJsonlRecords(lines, fileStem, fallbackTs) {
2920
+ let sessionId = fileStem;
2921
+ let currentModel;
2922
+ const usages = [];
2923
+ const directIndexes = /* @__PURE__ */ new Map();
2924
+ for (const line of lines) {
2925
+ const record = parseJsonLine(line);
2926
+ if (!record) {
2927
+ continue;
2928
+ }
2929
+ const sid = stringField(record, "sessionId") ?? stringField(record, "session_id");
2930
+ if (sid) {
2931
+ sessionId = sid;
2932
+ }
2933
+ const model = stringField(record, "model");
2934
+ if (model) {
2935
+ currentModel = model;
2936
+ }
2937
+ if (stringField(record, "type") === "gemini") {
2938
+ const usage = parseDirectEvent(record, currentModel, fallbackTs);
2939
+ if (!usage) {
2940
+ continue;
2941
+ }
2942
+ const id = stringField(record, "id");
2943
+ if (id === void 0) {
2944
+ usages.push(usage);
2945
+ } else {
2946
+ const existing = directIndexes.get(id);
2947
+ if (existing === void 0) {
2948
+ directIndexes.set(id, usages.length);
2949
+ usages.push(usage);
2950
+ } else {
2951
+ usages[existing] = usage;
2952
+ }
2953
+ }
2954
+ continue;
2955
+ }
2956
+ const stats = readStats(record);
2957
+ if (stats !== void 0) {
2958
+ const statsTs = timestampFrom(stringField(record, "timestamp")) ?? fallbackTs;
2959
+ usages.push(...parseStatsEvents(stats, currentModel, statsTs));
2960
+ }
2961
+ }
2962
+ return { sessionId, usages };
2963
+ }
2964
+ async function parseGeminiSessionFile(filePath, options) {
2965
+ const text = await readFile5(filePath, "utf8");
2966
+ const fileStem = path8.basename(filePath).replace(/\.jsonl?$/, "");
2967
+ const info = await stat4(filePath);
2968
+ const fallbackTs = info.mtime.toISOString();
2969
+ let parsed;
2970
+ if (filePath.endsWith(".jsonl")) {
2971
+ parsed = parseJsonlRecords(text.split("\n"), fileStem, fallbackTs);
2972
+ } else {
2973
+ let raw;
2974
+ try {
2975
+ raw = JSON.parse(text);
2976
+ } catch {
2977
+ return [];
2978
+ }
2979
+ if (!isPlainObject(raw)) {
2980
+ return [];
2981
+ }
2982
+ parsed = parseJsonRecord(raw, fileStem, fallbackTs);
2983
+ }
2984
+ if (parsed.usages.length === 0) {
2985
+ return [];
2986
+ }
2987
+ const usages = parsed.usages.sort((a, b) => a.ts.localeCompare(b.ts));
2988
+ const sessionId = `gemini_${parsed.sessionId}`;
2989
+ const state = new SessionParserState(filePath, options, (event) => baseGeminiEvent(event));
2990
+ state.sessionId = sessionId;
2991
+ state.ensureSessionStarted(usages[0].ts, 0);
2992
+ let lastTs = usages[0].ts;
2993
+ for (const [index, usage] of usages.entries()) {
2994
+ lastTs = usage.ts;
2995
+ const metrics = {
2996
+ tokensInput: usage.tokensInput || void 0,
2997
+ tokensOutput: usage.tokensOutput || void 0,
2998
+ tokensCachedInput: usage.tokensCacheRead || void 0,
2999
+ tokensCacheReadInput: usage.tokensCacheRead || void 0,
3000
+ tokensReasoningOutput: usage.tokensReasoning || void 0,
3001
+ tokensTotal: usage.tokensTotal,
3002
+ modelCalls: 1
3003
+ };
3004
+ state.push(
3005
+ baseGeminiEvent({
3006
+ ts: usage.ts,
3007
+ type: "model.usage",
3008
+ sessionId,
3009
+ model: usage.model,
3010
+ confidence: "exact",
3011
+ metrics,
3012
+ refs: stringRefs({ messageId: usage.id })
3013
+ }),
3014
+ index + 1,
3015
+ "jsonl",
3016
+ "model.usage"
3017
+ );
3018
+ }
3019
+ state.push(
3020
+ baseGeminiEvent({
3021
+ ts: lastTs,
3022
+ type: "session.ended",
3023
+ sessionId,
3024
+ confidence: "derived"
3025
+ }),
3026
+ usages.length,
3027
+ "jsonl",
3028
+ "session"
3029
+ );
3030
+ return state.events.filter((event) => matchesBackfillFilters(event, options));
3031
+ }
3032
+ function baseGeminiEvent(event) {
3033
+ return {
3034
+ schemaVersion: AGENT_TIME_SCHEMA_VERSION,
3035
+ source: "gemini",
3036
+ agent: "gemini",
3037
+ // Gemini logs carry no cwd/project metadata; pin all events to a stable
3038
+ // synthetic workspace so downstream rollups keep them grouped together.
3039
+ workspaceId: createWorkspaceId({ projectName: "gemini" }),
3040
+ ...event
3041
+ };
3042
+ }
3043
+ function geminiDataDirs(home, env) {
3044
+ const override = env?.GEMINI_DATA_DIR;
3045
+ if (override && override.trim()) {
3046
+ return override.split(",").map((entry) => entry.trim()).filter(Boolean).map((entry) => path8.resolve(entry));
3047
+ }
3048
+ return [path8.join(home, ".gemini", "tmp")];
3049
+ }
3050
+ async function geminiBackfillFiles(sourceRoot, home, env) {
3051
+ const roots = sourceRoot ? [path8.resolve(sourceRoot)] : geminiDataDirs(home, env);
3052
+ const fileLists = await Promise.all(
3053
+ roots.map((root) => listFilesByExtensions(root, [".json", ".jsonl"]))
3054
+ );
3055
+ const files = fileLists.flat().sort();
3056
+ return Promise.all(files.map(async (filePath) => {
3057
+ const info = await stat4(filePath);
3058
+ return { path: filePath, modifiedAt: info.mtime.toISOString() };
3059
+ }));
3060
+ }
3061
+ function createGeminiAdapter() {
3062
+ return {
3063
+ id: "gemini",
3064
+ label: "Gemini CLI",
3065
+ agentName: "gemini",
3066
+ kind: "agent",
3067
+ detectPath(home, env) {
3068
+ return geminiDataDirs(home, env)[0];
3069
+ },
3070
+ // Gemini has no plugin/hook surface, so there is nothing for codetime to
3071
+ // "install". Report installed = true whenever a data dir is on disk so
3072
+ // `detect` accurately shows "already covered via backfill".
3073
+ installedPath(home, env) {
3074
+ return geminiDataDirs(home, env)[0];
3075
+ },
3076
+ async isInstalled(home, env) {
3077
+ for (const dir of geminiDataDirs(home, env)) {
3078
+ if (await pathExists(dir).catch(() => false)) {
3079
+ return true;
3080
+ }
3081
+ }
3082
+ return false;
3083
+ },
3084
+ installEntries() {
3085
+ return [];
3086
+ },
3087
+ sourcePaths(home, env) {
3088
+ return geminiDataDirs(home, env);
3089
+ },
3090
+ parseSessionFile: parseGeminiSessionFile
3091
+ };
3092
+ }
3093
+
2729
3094
  // src/adapters/opencode.ts
2730
3095
  import os2 from "node:os";
2731
- import path8 from "node:path";
3096
+ import path9 from "node:path";
2732
3097
  async function parseOpenCodeSessionFile(dbPath, options) {
2733
3098
  const { DatabaseSync } = await import("node:sqlite");
2734
3099
  if (!dbPath.endsWith(".db")) {
@@ -2759,7 +3124,7 @@ async function parseOpenCodeSessionFile(dbPath, options) {
2759
3124
  for (const session of sessions) {
2760
3125
  const sessionId = session.id;
2761
3126
  const cwd = session.directory || session.path || void 0;
2762
- const project = cwd ? path8.basename(cwd) : void 0;
3127
+ const project = cwd ? path9.basename(cwd) : void 0;
2763
3128
  const sessionTs = msToIso(session.time_created);
2764
3129
  events.push(baseOpenCodeEvent({
2765
3130
  ts: sessionTs,
@@ -2849,7 +3214,7 @@ async function parseOpenCodeSessionFile(dbPath, options) {
2849
3214
  const assistantAgent = stringField(info, "agent") || "opencode";
2850
3215
  const pathObj = objectField(info, "path");
2851
3216
  const assistantCwd = stringField(pathObj, "cwd") || cwd;
2852
- const assistantProject = assistantCwd ? path8.basename(assistantCwd) : project;
3217
+ const assistantProject = assistantCwd ? path9.basename(assistantCwd) : project;
2853
3218
  const completedTs = numberField(objectField(info, "time"), "completed");
2854
3219
  const createdTs = timeCreated;
2855
3220
  const tokens = opencodeUsageFromInfo(info);
@@ -3091,33 +3456,33 @@ function opencodeUsageFromInfo(info) {
3091
3456
  function opencodeConfigDir(home, env) {
3092
3457
  const override = env?.OPENCODE_CONFIG_DIR;
3093
3458
  if (override && override.trim()) {
3094
- return path8.resolve(override);
3459
+ return path9.resolve(override);
3095
3460
  }
3096
3461
  const xdgConfig = env?.XDG_CONFIG_HOME;
3097
3462
  if (xdgConfig && xdgConfig.trim()) {
3098
- return path8.join(path8.resolve(xdgConfig), "opencode");
3463
+ return path9.join(path9.resolve(xdgConfig), "opencode");
3099
3464
  }
3100
- return path8.join(home, ".config", "opencode");
3465
+ return path9.join(home, ".config", "opencode");
3101
3466
  }
3102
3467
  function opencodeDataCandidates(home, env) {
3103
3468
  const xdgData = env?.XDG_DATA_HOME;
3104
- const primary = xdgData && xdgData.trim() ? path8.join(path8.resolve(xdgData), "opencode", "opencode.db") : path8.join(home, ".local", "share", "opencode", "opencode.db");
3105
- return [primary, path8.join(home, ".opencode", "opencode.db")];
3469
+ const primary = xdgData && xdgData.trim() ? path9.join(path9.resolve(xdgData), "opencode", "opencode.db") : path9.join(home, ".local", "share", "opencode", "opencode.db");
3470
+ return [primary, path9.join(home, ".opencode", "opencode.db")];
3106
3471
  }
3107
3472
  async function opencodeBackfillFiles(sourceRoot, home = os2.homedir(), env) {
3108
- const { stat: stat6 } = await import("node:fs/promises");
3473
+ const { stat: stat7 } = await import("node:fs/promises");
3109
3474
  if (sourceRoot) {
3110
3475
  if (!sourceRoot.endsWith(".db")) {
3111
3476
  return [];
3112
3477
  }
3113
- const info = await stat6(sourceRoot).catch(() => null);
3478
+ const info = await stat7(sourceRoot).catch(() => null);
3114
3479
  if (!info) {
3115
3480
  return [];
3116
3481
  }
3117
3482
  return [{ path: sourceRoot, modifiedAt: info.mtime.toISOString() }];
3118
3483
  }
3119
3484
  for (const candidatePath of opencodeDataCandidates(home, env)) {
3120
- const info = await stat6(candidatePath).catch(() => null);
3485
+ const info = await stat7(candidatePath).catch(() => null);
3121
3486
  if (info) {
3122
3487
  return [{ path: candidatePath, modifiedAt: info.mtime.toISOString() }];
3123
3488
  }
@@ -3180,12 +3545,12 @@ function createOpenCodeAdapter() {
3180
3545
  return opencodeConfigDir(home, env);
3181
3546
  },
3182
3547
  installedPath(home, env) {
3183
- return path8.join(opencodeConfigDir(home, env), PLUGIN_PATH);
3548
+ return path9.join(opencodeConfigDir(home, env), PLUGIN_PATH);
3184
3549
  },
3185
3550
  async isInstalled(home, env) {
3186
3551
  try {
3187
3552
  const { pathExists: pathExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
3188
- return await pathExists2(path8.join(opencodeConfigDir(home, env), PLUGIN_PATH)) || await pathExists2(path8.join(".opencode", PLUGIN_PATH));
3553
+ return await pathExists2(path9.join(opencodeConfigDir(home, env), PLUGIN_PATH)) || await pathExists2(path9.join(".opencode", PLUGIN_PATH));
3189
3554
  } catch {
3190
3555
  return false;
3191
3556
  }
@@ -3193,7 +3558,7 @@ function createOpenCodeAdapter() {
3193
3558
  installEntries(home, env) {
3194
3559
  return [{
3195
3560
  kind: "file",
3196
- path: path8.join(opencodeConfigDir(home, env), PLUGIN_PATH),
3561
+ path: path9.join(opencodeConfigDir(home, env), PLUGIN_PATH),
3197
3562
  content: opencodePluginContent()
3198
3563
  }];
3199
3564
  },
@@ -3205,10 +3570,10 @@ function createOpenCodeAdapter() {
3205
3570
  }
3206
3571
 
3207
3572
  // src/adapters/pi.ts
3208
- import { readFile as readFile5 } from "node:fs/promises";
3209
- import path9 from "node:path";
3573
+ import { readFile as readFile6 } from "node:fs/promises";
3574
+ import path10 from "node:path";
3210
3575
  async function parsePiSessionFile(filePath, options) {
3211
- const text = await readFile5(filePath, "utf8");
3576
+ const text = await readFile6(filePath, "utf8");
3212
3577
  const lines = text.split("\n").filter(Boolean);
3213
3578
  let sessionId;
3214
3579
  let cwd;
@@ -3237,7 +3602,7 @@ async function parsePiSessionFile(filePath, options) {
3237
3602
  sessionId = stringField(raw, "id") || state.sessionId;
3238
3603
  state.sessionId = sessionId || state.sessionId;
3239
3604
  cwd = stringField(raw, "cwd") || cwd;
3240
- project = cwd ? path9.basename(cwd) : project;
3605
+ project = cwd ? path10.basename(cwd) : project;
3241
3606
  continue;
3242
3607
  }
3243
3608
  if (entryType === "model_change") {
@@ -3635,16 +4000,16 @@ export default function (pi: ExtensionAPI) {
3635
4000
  function piAgentDir(home, env) {
3636
4001
  const override = env?.PI_CODING_AGENT_DIR;
3637
4002
  if (override && override.trim()) {
3638
- return path9.resolve(override);
4003
+ return path10.resolve(override);
3639
4004
  }
3640
- return path9.join(home, ".pi", "agent");
4005
+ return path10.join(home, ".pi", "agent");
3641
4006
  }
3642
4007
  function piSessionDir(home, env) {
3643
4008
  const override = env?.PI_CODING_AGENT_SESSION_DIR;
3644
4009
  if (override && override.trim()) {
3645
- return path9.resolve(override);
4010
+ return path10.resolve(override);
3646
4011
  }
3647
- return path9.join(piAgentDir(home, env), "sessions");
4012
+ return path10.join(piAgentDir(home, env), "sessions");
3648
4013
  }
3649
4014
  function createPiAdapter() {
3650
4015
  return {
@@ -3656,12 +4021,12 @@ function createPiAdapter() {
3656
4021
  return piAgentDir(home, env);
3657
4022
  },
3658
4023
  installedPath(home, env) {
3659
- return path9.join(piAgentDir(home, env), "extensions", "codetime.ts");
4024
+ return path10.join(piAgentDir(home, env), "extensions", "codetime.ts");
3660
4025
  },
3661
4026
  async isInstalled(home, env) {
3662
4027
  try {
3663
4028
  const { pathExists: pathExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
3664
- return await pathExists2(path9.join(piAgentDir(home, env), "extensions", "codetime.ts"));
4029
+ return await pathExists2(path10.join(piAgentDir(home, env), "extensions", "codetime.ts"));
3665
4030
  } catch {
3666
4031
  return false;
3667
4032
  }
@@ -3669,7 +4034,7 @@ function createPiAdapter() {
3669
4034
  installEntries(home, env) {
3670
4035
  return [{
3671
4036
  kind: "file",
3672
- path: path9.join(piAgentDir(home, env), "extensions", "codetime.ts"),
4037
+ path: path10.join(piAgentDir(home, env), "extensions", "codetime.ts"),
3673
4038
  content: piExtensionContent()
3674
4039
  }];
3675
4040
  },
@@ -4102,15 +4467,15 @@ function hookCommandFromGroup(group) {
4102
4467
  import { randomUUID } from "node:crypto";
4103
4468
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4104
4469
  import { homedir, hostname } from "node:os";
4105
- import path10 from "node:path";
4470
+ import path11 from "node:path";
4106
4471
  function configDir(home = homedir()) {
4107
- return path10.join(home, ".codetime");
4472
+ return path11.join(home, ".codetime");
4108
4473
  }
4109
4474
  function configPath(home = homedir()) {
4110
- return path10.join(configDir(home), "config.json");
4475
+ return path11.join(configDir(home), "config.json");
4111
4476
  }
4112
4477
  function machineIdPath(home = homedir()) {
4113
- return path10.join(configDir(home), "machine-id");
4478
+ return path11.join(configDir(home), "machine-id");
4114
4479
  }
4115
4480
  function readConfig(home = homedir()) {
4116
4481
  const file = configPath(home);
@@ -4157,15 +4522,15 @@ function defaultMachineName() {
4157
4522
  init_fs();
4158
4523
 
4159
4524
  // src/lib/logger.ts
4160
- import { appendFile, mkdir as mkdir2, rename, stat as stat4 } from "node:fs/promises";
4525
+ import { appendFile, mkdir as mkdir2, rename, stat as stat5 } from "node:fs/promises";
4161
4526
  import { homedir as homedir2 } from "node:os";
4162
- import path11 from "node:path";
4527
+ import path12 from "node:path";
4163
4528
  var MAX_BYTES = 1 * 1024 * 1024;
4164
4529
  function logDir(home = homedir2()) {
4165
- return path11.join(home, ".codetime", "logs");
4530
+ return path12.join(home, ".codetime", "logs");
4166
4531
  }
4167
4532
  function logPath(home = homedir2(), name = "cli.log") {
4168
- return path11.join(logDir(home), name);
4533
+ return path12.join(logDir(home), name);
4169
4534
  }
4170
4535
  function serializeError(error) {
4171
4536
  if (error instanceof Error) {
@@ -4175,7 +4540,7 @@ function serializeError(error) {
4175
4540
  }
4176
4541
  async function rotateIfNeeded(file) {
4177
4542
  try {
4178
- const info = await stat4(file);
4543
+ const info = await stat5(file);
4179
4544
  if (info.size > MAX_BYTES) {
4180
4545
  await rename(file, `${file}.1`).catch(() => {
4181
4546
  });
@@ -4340,8 +4705,8 @@ function buildHeaders(token, machine) {
4340
4705
  ...machine?.platform ? { "x-machine-platform": machine.platform } : {}
4341
4706
  };
4342
4707
  }
4343
- function joinUrl(base, path13) {
4344
- return new URL(path13, base.endsWith("/") ? base : `${base}/`).toString();
4708
+ function joinUrl(base, path14) {
4709
+ return new URL(path14, base.endsWith("/") ? base : `${base}/`).toString();
4345
4710
  }
4346
4711
  async function postRollupBatch(remote, rollups, options = {}) {
4347
4712
  const response = await remote.fetchImpl(joinUrl(remote.baseUrl, "/v3/agent/ingest"), {
@@ -4406,7 +4771,7 @@ async function deleteMachine(remote, id) {
4406
4771
  }
4407
4772
 
4408
4773
  // src/lib/types.ts
4409
- var BACKFILL_STATE_SCHEMA_VERSION = 3;
4774
+ var BACKFILL_STATE_SCHEMA_VERSION = 4;
4410
4775
 
4411
4776
  // src/cli.ts
4412
4777
  function createRegistry() {
@@ -4416,6 +4781,7 @@ function createRegistry() {
4416
4781
  registry.register(createPiAdapter());
4417
4782
  registry.register(createOpenCodeAdapter());
4418
4783
  registry.register(createAmpAdapter());
4784
+ registry.register(createGeminiAdapter());
4419
4785
  return registry;
4420
4786
  }
4421
4787
  var defaultContext = {
@@ -4676,7 +5042,7 @@ async function backfillCommand(options, ctx, registry) {
4676
5042
  }
4677
5043
  if (action === "import" && !options["dry-run"]) {
4678
5044
  const requested = normalizeBackfillSource(stringOption(options.source) || "all");
4679
- const supported = /* @__PURE__ */ new Set(["all", "codex", "claude-code", "opencode", "pi", "amp"]);
5045
+ const supported = /* @__PURE__ */ new Set(["all", ...BACKFILL_SOURCE_IDS]);
4680
5046
  if (!supported.has(requested)) {
4681
5047
  write(ctx.stderr, `Unsupported backfill source: ${requested}
4682
5048
  `);
@@ -4821,11 +5187,14 @@ async function listBackfillSourceFiles(source, options, ctx) {
4821
5187
  if (source.id === "amp") {
4822
5188
  return ampBackfillFiles(stringOption(options["source-root"]), resolveHome(options, ctx), ctx.env);
4823
5189
  }
5190
+ if (source.id === "gemini") {
5191
+ return geminiBackfillFiles(stringOption(options["source-root"]), resolveHome(options, ctx), ctx.env);
5192
+ }
4824
5193
  const roots = stringOption(options["source-root"]) ? [requiredOption(options, "source-root")] : source.paths;
4825
5194
  const fileLists = await Promise.all(roots.map((r) => listJsonlFiles(r)));
4826
5195
  const files = fileLists.flat().sort().slice(0, numberOption(options.limit) || void 0);
4827
5196
  return Promise.all(files.map(async (filePath) => {
4828
- const info = await stat5(filePath);
5197
+ const info = await stat6(filePath);
4829
5198
  return { path: filePath, modifiedAt: info.mtime.toISOString() };
4830
5199
  }));
4831
5200
  }
@@ -4901,7 +5270,7 @@ function writeBackfillPlan(plan, options, ctx) {
4901
5270
  }
4902
5271
  async function importBackfillPlan(plan, options, ctx, registry) {
4903
5272
  const source = normalizeBackfillSource(stringOption(options.source) || "all");
4904
- const supportedSources = /* @__PURE__ */ new Set(["codex", "claude-code", "opencode", "pi", "amp"]);
5273
+ const supportedSources = new Set(BACKFILL_SOURCE_IDS);
4905
5274
  const home = resolveHome(options, ctx);
4906
5275
  if (source !== "all" && !supportedSources.has(source)) {
4907
5276
  write(ctx.stderr, `Unsupported backfill source: ${source}
@@ -5134,13 +5503,13 @@ function selectBackfillFilesForImport(files, watermarkTs) {
5134
5503
  });
5135
5504
  }
5136
5505
  function backfillIncrementalStatePath(home) {
5137
- return path12.join(home, ".codetime", "backfill-state.json");
5506
+ return path13.join(home, ".codetime", "backfill-state.json");
5138
5507
  }
5139
5508
  function syncLocalTriggerStatePath(home) {
5140
- return path12.join(home, ".codetime", "sync-local-trigger.json");
5509
+ return path13.join(home, ".codetime", "sync-local-trigger.json");
5141
5510
  }
5142
5511
  function syncLocalTriggerLockPath(home) {
5143
- return path12.join(home, ".codetime", "sync-local-trigger.lock");
5512
+ return path13.join(home, ".codetime", "sync-local-trigger.lock");
5144
5513
  }
5145
5514
  async function readBackfillIncrementalState(home, ctx) {
5146
5515
  const statePath = backfillIncrementalStatePath(home);
@@ -5163,7 +5532,7 @@ async function readBackfillIncrementalState(home, ctx) {
5163
5532
  return { version: BACKFILL_STATE_SCHEMA_VERSION, sources: {} };
5164
5533
  }
5165
5534
  const sources = {};
5166
- for (const source of ["codex", "claude-code", "opencode", "pi", "amp"]) {
5535
+ for (const source of BACKFILL_SOURCE_IDS) {
5167
5536
  const item = state.sources[source];
5168
5537
  if (isPlainObject(item) && typeof item.watermarkTs === "string" && !Number.isNaN(Date.parse(item.watermarkTs))) {
5169
5538
  sources[source] = { watermarkTs: item.watermarkTs };
@@ -5180,7 +5549,7 @@ async function updateBackfillIncrementalState(home, state, selectedFilesBySource
5180
5549
  state.sources[source] = { watermarkTs: latest };
5181
5550
  }
5182
5551
  const statePath = backfillIncrementalStatePath(home);
5183
- await mkdir3(path12.dirname(statePath), { recursive: true });
5552
+ await mkdir3(path13.dirname(statePath), { recursive: true });
5184
5553
  await writeFile2(statePath, `${JSON.stringify(state, null, 2)}
5185
5554
  `, "utf8");
5186
5555
  }
@@ -5208,7 +5577,7 @@ async function readSyncLocalTriggerState(statePath) {
5208
5577
  return nextState;
5209
5578
  }
5210
5579
  async function writeSyncLocalTriggerState(statePath, state) {
5211
- await mkdir3(path12.dirname(statePath), { recursive: true });
5580
+ await mkdir3(path13.dirname(statePath), { recursive: true });
5212
5581
  await writeFile2(statePath, `${JSON.stringify(state, null, 2)}
5213
5582
  `, "utf8");
5214
5583
  }
@@ -5223,7 +5592,7 @@ async function readSyncLocalLock(lockPath) {
5223
5592
  return { pid: lock.pid, startedAt: lock.startedAt };
5224
5593
  }
5225
5594
  async function writeSyncLocalLock(lockPath, lock) {
5226
- await mkdir3(path12.dirname(lockPath), { recursive: true });
5595
+ await mkdir3(path13.dirname(lockPath), { recursive: true });
5227
5596
  await writeFile2(lockPath, `${JSON.stringify(lock, null, 2)}
5228
5597
  `, "utf8");
5229
5598
  }
@@ -5283,10 +5652,10 @@ function syncLocalRunnerEntryArgs(cliPath) {
5283
5652
  if (cliPath.endsWith(".ts")) {
5284
5653
  return ["--import", "tsx", cliPath];
5285
5654
  }
5286
- return [path12.resolve(path12.dirname(cliPath), "../bin/codetime.mjs")];
5655
+ return [path13.resolve(path13.dirname(cliPath), "../bin/codetime.mjs")];
5287
5656
  }
5288
5657
  function resolveHome(options, ctx) {
5289
- return path12.resolve(stringOption(options.home) || ctx.env.HOME || os3.homedir());
5658
+ return path13.resolve(stringOption(options.home) || ctx.env.HOME || os3.homedir());
5290
5659
  }
5291
5660
  function requestedTargets(options) {
5292
5661
  const value = options.target || options.targets;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codetime-cli",
3
3
  "type": "module",
4
- "version": "0.3.2",
4
+ "version": "0.4.0",
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": {
@@ -35,7 +35,7 @@
35
35
  "devDependencies": {
36
36
  "cac": "^7.0.0",
37
37
  "esbuild": "^0.28.0",
38
- "@codetime/shared": "0.3.2"
38
+ "@codetime/shared": "0.4.0"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsc -p tsconfig.json",