codetime-cli 0.3.3 → 0.5.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 +596 -59
  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.3" : "0.1.1";
976
+ var PACKAGE_VERSION = true ? "0.5.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;
@@ -2228,6 +2235,7 @@ async function parseCodexSessionFile(filePath, options) {
2228
2235
  let cwd;
2229
2236
  let project;
2230
2237
  let model;
2238
+ let sessionStartEmitted = false;
2231
2239
  let serviceTier;
2232
2240
  let currentTurnId;
2233
2241
  let lastTurnIdForComplete;
@@ -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") {
@@ -2280,6 +2289,36 @@ async function parseCodexSessionFile(filePath, options) {
2280
2289
  }
2281
2290
  continue;
2282
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
+ }
2320
+ continue;
2321
+ }
2283
2322
  if (topType !== "event_msg" && topType !== "response_item") {
2284
2323
  continue;
2285
2324
  }
@@ -2570,6 +2609,55 @@ function baseCodexEvent(event) {
2570
2609
  ...event
2571
2610
  };
2572
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
+ }
2573
2661
  function tokenUsageFromPayload(payload) {
2574
2662
  const info = objectField(payload, "info");
2575
2663
  const usage = objectField(info, "last_token_usage");
@@ -2600,7 +2688,7 @@ function fileActivitiesFromFunctionCall(payload, payloadType, ts, cwd) {
2600
2688
  for (const key of ["file_path", "path", "notebook_path"]) {
2601
2689
  addResolvedPathActivity(changes, stringField(args, key), operation, ts, cwd, workdir);
2602
2690
  }
2603
- for (const file of arrayField2(args, "files")) {
2691
+ for (const file of arrayField3(args, "files")) {
2604
2692
  if (typeof file === "string") {
2605
2693
  addResolvedPathActivity(changes, file, operation, ts, cwd, workdir);
2606
2694
  }
@@ -2613,7 +2701,7 @@ function fileActivitiesFromFunctionCall(payload, payloadType, ts, cwd) {
2613
2701
  }
2614
2702
  return [...changes.values()];
2615
2703
  }
2616
- function arrayField2(object, key) {
2704
+ function arrayField3(object, key) {
2617
2705
  if (!isPlainObject(object) || !Array.isArray(object[key])) {
2618
2706
  return [];
2619
2707
  }
@@ -2698,9 +2786,314 @@ function createCodexAdapter() {
2698
2786
  };
2699
2787
  }
2700
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
+
2701
3094
  // src/adapters/opencode.ts
2702
3095
  import os2 from "node:os";
2703
- import path8 from "node:path";
3096
+ import path9 from "node:path";
2704
3097
  async function parseOpenCodeSessionFile(dbPath, options) {
2705
3098
  const { DatabaseSync } = await import("node:sqlite");
2706
3099
  if (!dbPath.endsWith(".db")) {
@@ -2731,7 +3124,7 @@ async function parseOpenCodeSessionFile(dbPath, options) {
2731
3124
  for (const session of sessions) {
2732
3125
  const sessionId = session.id;
2733
3126
  const cwd = session.directory || session.path || void 0;
2734
- const project = cwd ? path8.basename(cwd) : void 0;
3127
+ const project = cwd ? path9.basename(cwd) : void 0;
2735
3128
  const sessionTs = msToIso(session.time_created);
2736
3129
  events.push(baseOpenCodeEvent({
2737
3130
  ts: sessionTs,
@@ -2821,7 +3214,7 @@ async function parseOpenCodeSessionFile(dbPath, options) {
2821
3214
  const assistantAgent = stringField(info, "agent") || "opencode";
2822
3215
  const pathObj = objectField(info, "path");
2823
3216
  const assistantCwd = stringField(pathObj, "cwd") || cwd;
2824
- const assistantProject = assistantCwd ? path8.basename(assistantCwd) : project;
3217
+ const assistantProject = assistantCwd ? path9.basename(assistantCwd) : project;
2825
3218
  const completedTs = numberField(objectField(info, "time"), "completed");
2826
3219
  const createdTs = timeCreated;
2827
3220
  const tokens = opencodeUsageFromInfo(info);
@@ -3063,33 +3456,33 @@ function opencodeUsageFromInfo(info) {
3063
3456
  function opencodeConfigDir(home, env) {
3064
3457
  const override = env?.OPENCODE_CONFIG_DIR;
3065
3458
  if (override && override.trim()) {
3066
- return path8.resolve(override);
3459
+ return path9.resolve(override);
3067
3460
  }
3068
3461
  const xdgConfig = env?.XDG_CONFIG_HOME;
3069
3462
  if (xdgConfig && xdgConfig.trim()) {
3070
- return path8.join(path8.resolve(xdgConfig), "opencode");
3463
+ return path9.join(path9.resolve(xdgConfig), "opencode");
3071
3464
  }
3072
- return path8.join(home, ".config", "opencode");
3465
+ return path9.join(home, ".config", "opencode");
3073
3466
  }
3074
3467
  function opencodeDataCandidates(home, env) {
3075
3468
  const xdgData = env?.XDG_DATA_HOME;
3076
- const primary = xdgData && xdgData.trim() ? path8.join(path8.resolve(xdgData), "opencode", "opencode.db") : path8.join(home, ".local", "share", "opencode", "opencode.db");
3077
- 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")];
3078
3471
  }
3079
3472
  async function opencodeBackfillFiles(sourceRoot, home = os2.homedir(), env) {
3080
- const { stat: stat6 } = await import("node:fs/promises");
3473
+ const { stat: stat7 } = await import("node:fs/promises");
3081
3474
  if (sourceRoot) {
3082
3475
  if (!sourceRoot.endsWith(".db")) {
3083
3476
  return [];
3084
3477
  }
3085
- const info = await stat6(sourceRoot).catch(() => null);
3478
+ const info = await stat7(sourceRoot).catch(() => null);
3086
3479
  if (!info) {
3087
3480
  return [];
3088
3481
  }
3089
3482
  return [{ path: sourceRoot, modifiedAt: info.mtime.toISOString() }];
3090
3483
  }
3091
3484
  for (const candidatePath of opencodeDataCandidates(home, env)) {
3092
- const info = await stat6(candidatePath).catch(() => null);
3485
+ const info = await stat7(candidatePath).catch(() => null);
3093
3486
  if (info) {
3094
3487
  return [{ path: candidatePath, modifiedAt: info.mtime.toISOString() }];
3095
3488
  }
@@ -3152,12 +3545,12 @@ function createOpenCodeAdapter() {
3152
3545
  return opencodeConfigDir(home, env);
3153
3546
  },
3154
3547
  installedPath(home, env) {
3155
- return path8.join(opencodeConfigDir(home, env), PLUGIN_PATH);
3548
+ return path9.join(opencodeConfigDir(home, env), PLUGIN_PATH);
3156
3549
  },
3157
3550
  async isInstalled(home, env) {
3158
3551
  try {
3159
3552
  const { pathExists: pathExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
3160
- 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));
3161
3554
  } catch {
3162
3555
  return false;
3163
3556
  }
@@ -3165,7 +3558,7 @@ function createOpenCodeAdapter() {
3165
3558
  installEntries(home, env) {
3166
3559
  return [{
3167
3560
  kind: "file",
3168
- path: path8.join(opencodeConfigDir(home, env), PLUGIN_PATH),
3561
+ path: path9.join(opencodeConfigDir(home, env), PLUGIN_PATH),
3169
3562
  content: opencodePluginContent()
3170
3563
  }];
3171
3564
  },
@@ -3177,10 +3570,10 @@ function createOpenCodeAdapter() {
3177
3570
  }
3178
3571
 
3179
3572
  // src/adapters/pi.ts
3180
- import { readFile as readFile5 } from "node:fs/promises";
3181
- import path9 from "node:path";
3573
+ import { readFile as readFile6 } from "node:fs/promises";
3574
+ import path10 from "node:path";
3182
3575
  async function parsePiSessionFile(filePath, options) {
3183
- const text = await readFile5(filePath, "utf8");
3576
+ const text = await readFile6(filePath, "utf8");
3184
3577
  const lines = text.split("\n").filter(Boolean);
3185
3578
  let sessionId;
3186
3579
  let cwd;
@@ -3209,7 +3602,7 @@ async function parsePiSessionFile(filePath, options) {
3209
3602
  sessionId = stringField(raw, "id") || state.sessionId;
3210
3603
  state.sessionId = sessionId || state.sessionId;
3211
3604
  cwd = stringField(raw, "cwd") || cwd;
3212
- project = cwd ? path9.basename(cwd) : project;
3605
+ project = cwd ? path10.basename(cwd) : project;
3213
3606
  continue;
3214
3607
  }
3215
3608
  if (entryType === "model_change") {
@@ -3607,16 +4000,16 @@ export default function (pi: ExtensionAPI) {
3607
4000
  function piAgentDir(home, env) {
3608
4001
  const override = env?.PI_CODING_AGENT_DIR;
3609
4002
  if (override && override.trim()) {
3610
- return path9.resolve(override);
4003
+ return path10.resolve(override);
3611
4004
  }
3612
- return path9.join(home, ".pi", "agent");
4005
+ return path10.join(home, ".pi", "agent");
3613
4006
  }
3614
4007
  function piSessionDir(home, env) {
3615
4008
  const override = env?.PI_CODING_AGENT_SESSION_DIR;
3616
4009
  if (override && override.trim()) {
3617
- return path9.resolve(override);
4010
+ return path10.resolve(override);
3618
4011
  }
3619
- return path9.join(piAgentDir(home, env), "sessions");
4012
+ return path10.join(piAgentDir(home, env), "sessions");
3620
4013
  }
3621
4014
  function createPiAdapter() {
3622
4015
  return {
@@ -3628,12 +4021,12 @@ function createPiAdapter() {
3628
4021
  return piAgentDir(home, env);
3629
4022
  },
3630
4023
  installedPath(home, env) {
3631
- return path9.join(piAgentDir(home, env), "extensions", "codetime.ts");
4024
+ return path10.join(piAgentDir(home, env), "extensions", "codetime.ts");
3632
4025
  },
3633
4026
  async isInstalled(home, env) {
3634
4027
  try {
3635
4028
  const { pathExists: pathExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
3636
- return await pathExists2(path9.join(piAgentDir(home, env), "extensions", "codetime.ts"));
4029
+ return await pathExists2(path10.join(piAgentDir(home, env), "extensions", "codetime.ts"));
3637
4030
  } catch {
3638
4031
  return false;
3639
4032
  }
@@ -3641,7 +4034,7 @@ function createPiAdapter() {
3641
4034
  installEntries(home, env) {
3642
4035
  return [{
3643
4036
  kind: "file",
3644
- path: path9.join(piAgentDir(home, env), "extensions", "codetime.ts"),
4037
+ path: path10.join(piAgentDir(home, env), "extensions", "codetime.ts"),
3645
4038
  content: piExtensionContent()
3646
4039
  }];
3647
4040
  },
@@ -4074,15 +4467,15 @@ function hookCommandFromGroup(group) {
4074
4467
  import { randomUUID } from "node:crypto";
4075
4468
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4076
4469
  import { homedir, hostname } from "node:os";
4077
- import path10 from "node:path";
4470
+ import path11 from "node:path";
4078
4471
  function configDir(home = homedir()) {
4079
- return path10.join(home, ".codetime");
4472
+ return path11.join(home, ".codetime");
4080
4473
  }
4081
4474
  function configPath(home = homedir()) {
4082
- return path10.join(configDir(home), "config.json");
4475
+ return path11.join(configDir(home), "config.json");
4083
4476
  }
4084
4477
  function machineIdPath(home = homedir()) {
4085
- return path10.join(configDir(home), "machine-id");
4478
+ return path11.join(configDir(home), "machine-id");
4086
4479
  }
4087
4480
  function readConfig(home = homedir()) {
4088
4481
  const file = configPath(home);
@@ -4129,15 +4522,15 @@ function defaultMachineName() {
4129
4522
  init_fs();
4130
4523
 
4131
4524
  // src/lib/logger.ts
4132
- 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";
4133
4526
  import { homedir as homedir2 } from "node:os";
4134
- import path11 from "node:path";
4527
+ import path12 from "node:path";
4135
4528
  var MAX_BYTES = 1 * 1024 * 1024;
4136
4529
  function logDir(home = homedir2()) {
4137
- return path11.join(home, ".codetime", "logs");
4530
+ return path12.join(home, ".codetime", "logs");
4138
4531
  }
4139
4532
  function logPath(home = homedir2(), name = "cli.log") {
4140
- return path11.join(logDir(home), name);
4533
+ return path12.join(logDir(home), name);
4141
4534
  }
4142
4535
  function serializeError(error) {
4143
4536
  if (error instanceof Error) {
@@ -4147,7 +4540,7 @@ function serializeError(error) {
4147
4540
  }
4148
4541
  async function rotateIfNeeded(file) {
4149
4542
  try {
4150
- const info = await stat4(file);
4543
+ const info = await stat5(file);
4151
4544
  if (info.size > MAX_BYTES) {
4152
4545
  await rename(file, `${file}.1`).catch(() => {
4153
4546
  });
@@ -4185,6 +4578,40 @@ async function logError(scope, error, meta = {}, home = homedir2()) {
4185
4578
  await writeLog({ scope, error, meta, level: "error" }, home);
4186
4579
  }
4187
4580
 
4581
+ // src/lib/login.ts
4582
+ function sleep(ms) {
4583
+ return new Promise((resolve) => setTimeout(resolve, ms));
4584
+ }
4585
+ function openBrowser(ctx, url) {
4586
+ const { command, args } = browserCommand(ctx.env.CODETIME_OS ?? process.platform, url);
4587
+ try {
4588
+ const child = ctx.spawn(command, args, { detached: true, stdio: "ignore" });
4589
+ child.unref?.();
4590
+ } catch {
4591
+ }
4592
+ }
4593
+ function browserCommand(platform, url) {
4594
+ if (platform === "darwin") {
4595
+ return { command: "open", args: [url] };
4596
+ }
4597
+ if (platform === "win32") {
4598
+ return { command: "cmd", args: ["/c", "start", "", url] };
4599
+ }
4600
+ return { command: "xdg-open", args: [url] };
4601
+ }
4602
+ function isHeadless(ctx) {
4603
+ if (ctx.env.CODETIME_NO_BROWSER) {
4604
+ return true;
4605
+ }
4606
+ if (ctx.env.SSH_CONNECTION || ctx.env.SSH_TTY) {
4607
+ return true;
4608
+ }
4609
+ if (process.platform === "linux") {
4610
+ return !ctx.env.DISPLAY && !ctx.env.WAYLAND_DISPLAY;
4611
+ }
4612
+ return false;
4613
+ }
4614
+
4188
4615
  // src/lib/progress.ts
4189
4616
  var BAR_WIDTH = 24;
4190
4617
  var ProgressBar = class {
@@ -4312,8 +4739,8 @@ function buildHeaders(token, machine) {
4312
4739
  ...machine?.platform ? { "x-machine-platform": machine.platform } : {}
4313
4740
  };
4314
4741
  }
4315
- function joinUrl(base, path13) {
4316
- return new URL(path13, base.endsWith("/") ? base : `${base}/`).toString();
4742
+ function joinUrl(base, path14) {
4743
+ return new URL(path14, base.endsWith("/") ? base : `${base}/`).toString();
4317
4744
  }
4318
4745
  async function postRollupBatch(remote, rollups, options = {}) {
4319
4746
  const response = await remote.fetchImpl(joinUrl(remote.baseUrl, "/v3/agent/ingest"), {
@@ -4344,6 +4771,28 @@ async function deleteRollupsBySource(remote, source, machine) {
4344
4771
  const body = await response.json();
4345
4772
  return Number(body.deleted) || 0;
4346
4773
  }
4774
+ async function startCliLink(remote) {
4775
+ const response = await remote.fetchImpl(joinUrl(remote.baseUrl, "/v3/agent/cli/link/start"), {
4776
+ method: "POST",
4777
+ headers: buildHeaders(),
4778
+ body: "{}"
4779
+ });
4780
+ if (!response.ok) {
4781
+ throw new Error(`Failed to start login: ${response.status}`);
4782
+ }
4783
+ return await response.json();
4784
+ }
4785
+ async function pollCliLink(remote, deviceCode) {
4786
+ const response = await remote.fetchImpl(joinUrl(remote.baseUrl, "/v3/agent/cli/link/poll"), {
4787
+ method: "POST",
4788
+ headers: buildHeaders(),
4789
+ body: JSON.stringify({ deviceCode })
4790
+ });
4791
+ if (!response.ok) {
4792
+ throw new Error(`Login poll failed: ${response.status}`);
4793
+ }
4794
+ return await response.json();
4795
+ }
4347
4796
  async function listMachines(remote) {
4348
4797
  const response = await remote.fetchImpl(joinUrl(remote.baseUrl, "/v3/machines"), {
4349
4798
  headers: buildHeaders(remote.token)
@@ -4388,6 +4837,7 @@ function createRegistry() {
4388
4837
  registry.register(createPiAdapter());
4389
4838
  registry.register(createOpenCodeAdapter());
4390
4839
  registry.register(createAmpAdapter());
4840
+ registry.register(createGeminiAdapter());
4391
4841
  return registry;
4392
4842
  }
4393
4843
  var defaultContext = {
@@ -4449,6 +4899,11 @@ function createCli(ctx, registry) {
4449
4899
  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));
4450
4900
  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));
4451
4901
  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));
4902
+ 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) => {
4903
+ const opts = normalizeOptions(options);
4904
+ return backfillCommand({ ...opts, action: "import", source: stringOption(opts.source) || "all" }, ctx, registry);
4905
+ });
4906
+ cli.command("login", "Authorize this machine by signing in through your browser").option("--remote <url>", "Override API base URL for this login").option("--no-browser", "Print the login URL instead of opening a browser").action((options) => loginCommand(normalizeOptions(options), ctx));
4452
4907
  cli.command("token [action] [value]", "Set, show, or clear the persisted API token").option("--remote <url>", "Override API base URL when setting a token").action((action, value, options) => tokenCommand(action, value, normalizeOptions(options), ctx));
4453
4908
  cli.command("machine [action]", "List or rename machines (requires login)").option("--name <name>", "New display name (used by `machine rename`)").option("--id <id>", "Machine id (defaults to current machine)").action((action, options) => machineCommand(action, normalizeOptions(options), ctx));
4454
4909
  return cli;
@@ -4648,7 +5103,7 @@ async function backfillCommand(options, ctx, registry) {
4648
5103
  }
4649
5104
  if (action === "import" && !options["dry-run"]) {
4650
5105
  const requested = normalizeBackfillSource(stringOption(options.source) || "all");
4651
- const supported = /* @__PURE__ */ new Set(["all", "codex", "claude-code", "opencode", "pi", "amp"]);
5106
+ const supported = /* @__PURE__ */ new Set(["all", ...BACKFILL_SOURCE_IDS]);
4652
5107
  if (!supported.has(requested)) {
4653
5108
  write(ctx.stderr, `Unsupported backfill source: ${requested}
4654
5109
  `);
@@ -4793,11 +5248,14 @@ async function listBackfillSourceFiles(source, options, ctx) {
4793
5248
  if (source.id === "amp") {
4794
5249
  return ampBackfillFiles(stringOption(options["source-root"]), resolveHome(options, ctx), ctx.env);
4795
5250
  }
5251
+ if (source.id === "gemini") {
5252
+ return geminiBackfillFiles(stringOption(options["source-root"]), resolveHome(options, ctx), ctx.env);
5253
+ }
4796
5254
  const roots = stringOption(options["source-root"]) ? [requiredOption(options, "source-root")] : source.paths;
4797
5255
  const fileLists = await Promise.all(roots.map((r) => listJsonlFiles(r)));
4798
5256
  const files = fileLists.flat().sort().slice(0, numberOption(options.limit) || void 0);
4799
5257
  return Promise.all(files.map(async (filePath) => {
4800
- const info = await stat5(filePath);
5258
+ const info = await stat6(filePath);
4801
5259
  return { path: filePath, modifiedAt: info.mtime.toISOString() };
4802
5260
  }));
4803
5261
  }
@@ -4873,7 +5331,7 @@ function writeBackfillPlan(plan, options, ctx) {
4873
5331
  }
4874
5332
  async function importBackfillPlan(plan, options, ctx, registry) {
4875
5333
  const source = normalizeBackfillSource(stringOption(options.source) || "all");
4876
- const supportedSources = /* @__PURE__ */ new Set(["codex", "claude-code", "opencode", "pi", "amp"]);
5334
+ const supportedSources = new Set(BACKFILL_SOURCE_IDS);
4877
5335
  const home = resolveHome(options, ctx);
4878
5336
  if (source !== "all" && !supportedSources.has(source)) {
4879
5337
  write(ctx.stderr, `Unsupported backfill source: ${source}
@@ -5106,13 +5564,13 @@ function selectBackfillFilesForImport(files, watermarkTs) {
5106
5564
  });
5107
5565
  }
5108
5566
  function backfillIncrementalStatePath(home) {
5109
- return path12.join(home, ".codetime", "backfill-state.json");
5567
+ return path13.join(home, ".codetime", "backfill-state.json");
5110
5568
  }
5111
5569
  function syncLocalTriggerStatePath(home) {
5112
- return path12.join(home, ".codetime", "sync-local-trigger.json");
5570
+ return path13.join(home, ".codetime", "sync-local-trigger.json");
5113
5571
  }
5114
5572
  function syncLocalTriggerLockPath(home) {
5115
- return path12.join(home, ".codetime", "sync-local-trigger.lock");
5573
+ return path13.join(home, ".codetime", "sync-local-trigger.lock");
5116
5574
  }
5117
5575
  async function readBackfillIncrementalState(home, ctx) {
5118
5576
  const statePath = backfillIncrementalStatePath(home);
@@ -5135,7 +5593,7 @@ async function readBackfillIncrementalState(home, ctx) {
5135
5593
  return { version: BACKFILL_STATE_SCHEMA_VERSION, sources: {} };
5136
5594
  }
5137
5595
  const sources = {};
5138
- for (const source of ["codex", "claude-code", "opencode", "pi", "amp"]) {
5596
+ for (const source of BACKFILL_SOURCE_IDS) {
5139
5597
  const item = state.sources[source];
5140
5598
  if (isPlainObject(item) && typeof item.watermarkTs === "string" && !Number.isNaN(Date.parse(item.watermarkTs))) {
5141
5599
  sources[source] = { watermarkTs: item.watermarkTs };
@@ -5152,7 +5610,7 @@ async function updateBackfillIncrementalState(home, state, selectedFilesBySource
5152
5610
  state.sources[source] = { watermarkTs: latest };
5153
5611
  }
5154
5612
  const statePath = backfillIncrementalStatePath(home);
5155
- await mkdir3(path12.dirname(statePath), { recursive: true });
5613
+ await mkdir3(path13.dirname(statePath), { recursive: true });
5156
5614
  await writeFile2(statePath, `${JSON.stringify(state, null, 2)}
5157
5615
  `, "utf8");
5158
5616
  }
@@ -5180,7 +5638,7 @@ async function readSyncLocalTriggerState(statePath) {
5180
5638
  return nextState;
5181
5639
  }
5182
5640
  async function writeSyncLocalTriggerState(statePath, state) {
5183
- await mkdir3(path12.dirname(statePath), { recursive: true });
5641
+ await mkdir3(path13.dirname(statePath), { recursive: true });
5184
5642
  await writeFile2(statePath, `${JSON.stringify(state, null, 2)}
5185
5643
  `, "utf8");
5186
5644
  }
@@ -5195,7 +5653,7 @@ async function readSyncLocalLock(lockPath) {
5195
5653
  return { pid: lock.pid, startedAt: lock.startedAt };
5196
5654
  }
5197
5655
  async function writeSyncLocalLock(lockPath, lock) {
5198
- await mkdir3(path12.dirname(lockPath), { recursive: true });
5656
+ await mkdir3(path13.dirname(lockPath), { recursive: true });
5199
5657
  await writeFile2(lockPath, `${JSON.stringify(lock, null, 2)}
5200
5658
  `, "utf8");
5201
5659
  }
@@ -5255,10 +5713,10 @@ function syncLocalRunnerEntryArgs(cliPath) {
5255
5713
  if (cliPath.endsWith(".ts")) {
5256
5714
  return ["--import", "tsx", cliPath];
5257
5715
  }
5258
- return [path12.resolve(path12.dirname(cliPath), "../bin/codetime.mjs")];
5716
+ return [path13.resolve(path13.dirname(cliPath), "../bin/codetime.mjs")];
5259
5717
  }
5260
5718
  function resolveHome(options, ctx) {
5261
- return path12.resolve(stringOption(options.home) || ctx.env.HOME || os3.homedir());
5719
+ return path13.resolve(stringOption(options.home) || ctx.env.HOME || os3.homedir());
5262
5720
  }
5263
5721
  function requestedTargets(options) {
5264
5722
  const value = options.target || options.targets;
@@ -5319,6 +5777,80 @@ function maskToken(token) {
5319
5777
  }
5320
5778
  return `${token.slice(0, 3)}\u2026${token.slice(-4)}`;
5321
5779
  }
5780
+ var LOGIN_MAX_WAIT_MS = 15 * 60 * 1e3;
5781
+ async function loginCommand(options, ctx) {
5782
+ const home = resolveHome(options, ctx);
5783
+ const remoteOverride = stringOption(options.remote) || stringOption(options["api-url"]);
5784
+ const existing = readConfig(home);
5785
+ const baseUrl = (remoteOverride || ctx.env.CODETIME_API_URL || existing.remoteUrl || DEFAULT_API_URL).replace(/\/$/, "");
5786
+ const remote = resolveRemote({
5787
+ apiUrl: baseUrl,
5788
+ env: ctx.env,
5789
+ fetch: ctx.fetch,
5790
+ homeOverride: home
5791
+ });
5792
+ if (!remote) {
5793
+ write(ctx.stderr, "No fetch implementation available.\n");
5794
+ return 1;
5795
+ }
5796
+ let link;
5797
+ try {
5798
+ link = await startCliLink(remote);
5799
+ } catch (error) {
5800
+ write(ctx.stderr, `${error.message}
5801
+ `);
5802
+ return 1;
5803
+ }
5804
+ const authUrl = `${baseUrl}/cli/auth?code=${encodeURIComponent(link.userCode)}`;
5805
+ const noBrowser = options.browser === false;
5806
+ const headless = isHeadless(ctx);
5807
+ write(ctx.stdout, `
5808
+ To sign in, visit:
5809
+
5810
+ ${authUrl}
5811
+
5812
+ and confirm this code: ${link.userCode}
5813
+
5814
+ `);
5815
+ if (!noBrowser && !headless) {
5816
+ openBrowser(ctx, authUrl);
5817
+ }
5818
+ write(ctx.stdout, "Waiting for authorization\u2026\n");
5819
+ const intervalMs = Math.max(1, link.interval || 4) * 1e3;
5820
+ const deadline = Date.now() + Math.min(LOGIN_MAX_WAIT_MS, Math.max(1, link.expiresIn || 600) * 1e3);
5821
+ while (Date.now() < deadline) {
5822
+ let poll;
5823
+ try {
5824
+ poll = await pollCliLink(remote, link.deviceCode);
5825
+ } catch (error) {
5826
+ void error;
5827
+ await sleep(intervalMs);
5828
+ continue;
5829
+ }
5830
+ if (poll.status === "pending") {
5831
+ await sleep(intervalMs);
5832
+ continue;
5833
+ }
5834
+ if (poll.status === "expired") {
5835
+ write(ctx.stderr, "Login code expired before it was approved. Re-run `codetime login`.\n");
5836
+ return 1;
5837
+ }
5838
+ writeConfig({
5839
+ ...existing,
5840
+ token: poll.token,
5841
+ ...poll.userId == null ? {} : { userId: String(poll.userId) },
5842
+ // Persist the host only when explicitly overridden, matching
5843
+ // `token set` so a default-host login never clobbers an earlier
5844
+ // --remote choice.
5845
+ ...remoteOverride ? { remoteUrl: remoteOverride } : {}
5846
+ }, home);
5847
+ write(ctx.stdout, `Logged in. Token saved (${maskToken(poll.token)}).
5848
+ `);
5849
+ return 0;
5850
+ }
5851
+ write(ctx.stderr, "Timed out waiting for authorization. Re-run `codetime login`.\n");
5852
+ return 1;
5853
+ }
5322
5854
  async function tokenCommand(action, value, options, ctx) {
5323
5855
  const verb = action || "show";
5324
5856
  const home = resolveHome(options, ctx);
@@ -5430,20 +5962,25 @@ Usage:
5430
5962
  codetime detect [--json] [--home <path>]
5431
5963
  codetime install [--target codex,claude,opencode,pi] [--all] [--dry-run] [--force] [--home <path>]
5432
5964
  codetime hook --agent <name>
5965
+ codetime sync [--source <source>] [--force] [--dry-run]
5433
5966
  codetime backfill discover|plan|import|verify --source codex|claude-code|opencode|pi|all --dry-run [--json] [--batch-size <count>]
5967
+ codetime login [--no-browser] [--remote <url>]
5434
5968
  codetime token set <token>
5435
5969
  codetime token show
5436
5970
  codetime token clear
5437
5971
 
5438
5972
  Setup:
5439
- Copy your upload token from https://codetime.dev/dashboard/settings,
5440
- then run: codetime token set <token>
5973
+ Run: codetime login (signs in through your browser)
5974
+ Or copy your upload token from https://codetime.dev/dashboard/settings
5975
+ and run: codetime token set <token>
5441
5976
 
5442
5977
  Commands:
5443
5978
  detect Show supported local targets and install status.
5444
5979
  install Install integration files into detected or requested targets.
5445
5980
  hook Read agent hook JSON from stdin and report a throttled event.
5981
+ sync Import and upload all local agent history (backfill import --source all).
5446
5982
  backfill Discover local history and create metadata-only import plans.
5983
+ login Authorize this machine by signing in through your browser.
5447
5984
  token Set, show, or clear the persisted API token.
5448
5985
  machine List your machines (read-only).
5449
5986
  version Print CLI version.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codetime-cli",
3
3
  "type": "module",
4
- "version": "0.3.3",
4
+ "version": "0.5.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.3"
38
+ "@codetime/shared": "0.4.0"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsc -p tsconfig.json",