codetime-cli 0.2.0 → 0.3.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 +234 -169
  2. package/package.json +5 -5
package/bin/codetime.mjs CHANGED
@@ -143,6 +143,9 @@ var init_fs = __esm({
143
143
  }
144
144
  });
145
145
 
146
+ // src/lib/types.ts
147
+ var BACKFILL_STATE_SCHEMA_VERSION = 2;
148
+
146
149
  // src/cli.ts
147
150
  import { spawn } from "node:child_process";
148
151
  import { mkdir as mkdir2, rm, stat as stat4, writeFile as writeFile2 } from "node:fs/promises";
@@ -1329,7 +1332,7 @@ function msToIso(ms) {
1329
1332
  }
1330
1333
 
1331
1334
  // src/lib/constants.ts
1332
- var PACKAGE_VERSION = true ? "0.2.0" : "0.1.1";
1335
+ var PACKAGE_VERSION = true ? "0.3.0" : "0.1.1";
1333
1336
  var DEFAULT_API_URL = "https://codetime.dev";
1334
1337
  var DEFAULT_BACKFILL_BATCH_SIZE = 50;
1335
1338
  var DEFAULT_BACKFILL_BATCH_BYTES = 800 * 1024;
@@ -1472,6 +1475,7 @@ async function parseClaudeCodeSessionFile(filePath, options) {
1472
1475
  const lines = text.split("\n").filter(Boolean);
1473
1476
  const projectContext = await claudeProjectContextFromLines(filePath, lines, options);
1474
1477
  const pendingTools = /* @__PURE__ */ new Map();
1478
+ const seenUsageKeys = /* @__PURE__ */ new Set();
1475
1479
  let sessionId = sessionIdFromFilePath(filePath, "claude");
1476
1480
  let cwd;
1477
1481
  let project = projectContext.project;
@@ -1623,8 +1627,19 @@ async function parseClaudeCodeSessionFile(filePath, options) {
1623
1627
  continue;
1624
1628
  }
1625
1629
  model = stringField(message, "model") || model;
1630
+ const messageId = stringField(message, "id");
1631
+ const requestId = stringField(raw, "requestId");
1632
+ const usageKey = messageId ? `${messageId}:${requestId}` : null;
1633
+ if (usageKey != null && seenUsageKeys.has(usageKey)) {
1634
+ continue;
1635
+ }
1636
+ if (usageKey != null) {
1637
+ seenUsageKeys.add(usageKey);
1638
+ }
1626
1639
  const usage = claudeUsageFromMessage(message);
1627
1640
  if (usage) {
1641
+ const speed = stringField(objectField(message, "usage"), "speed");
1642
+ const usageModel = speed === "fast" && model ? `${model}-fast` : model;
1628
1643
  push(baseClaudeEvent({
1629
1644
  ts,
1630
1645
  type: "model.usage",
@@ -1632,7 +1647,7 @@ async function parseClaudeCodeSessionFile(filePath, options) {
1632
1647
  turnId: state.currentTurnId,
1633
1648
  cwd,
1634
1649
  project,
1635
- model,
1650
+ model: usageModel,
1636
1651
  confidence: "partial",
1637
1652
  metrics: usage
1638
1653
  }), lineNumber, topType, "usage");
@@ -1940,35 +1955,43 @@ function hookConfig() {
1940
1955
  }
1941
1956
  };
1942
1957
  }
1958
+ function claudeConfigDir(home, env) {
1959
+ const override = env?.CLAUDE_CONFIG_DIR;
1960
+ if (override && override.trim()) {
1961
+ return path5.resolve(override);
1962
+ }
1963
+ return path5.join(home, ".claude");
1964
+ }
1943
1965
  function createClaudeCodeAdapter() {
1944
- const CLAUDE_PATH = ".claude";
1945
1966
  return {
1946
1967
  id: "claude-code",
1947
1968
  label: "Claude Code",
1948
1969
  agentName: "claude",
1949
1970
  kind: "agent",
1950
- detectPath(home) {
1951
- return path5.join(home, CLAUDE_PATH);
1971
+ detectPath(home, env) {
1972
+ return claudeConfigDir(home, env);
1952
1973
  },
1953
- installedPath(home) {
1954
- return path5.join(home, CLAUDE_PATH, "settings.json");
1974
+ installedPath(home, env) {
1975
+ return path5.join(claudeConfigDir(home, env), "settings.json");
1955
1976
  },
1956
- async isInstalled(home) {
1977
+ async isInstalled(home, env) {
1957
1978
  return isHooksJsonInstalled(
1958
- path5.join(home, CLAUDE_PATH, "settings.json"),
1979
+ path5.join(claudeConfigDir(home, env), "settings.json"),
1959
1980
  "codetime hook --agent claude"
1960
1981
  );
1961
1982
  },
1962
- installEntries(home) {
1983
+ installEntries(home, env) {
1963
1984
  return [{
1964
1985
  kind: "hooks-json",
1965
- path: path5.join(home, CLAUDE_PATH, "settings.json"),
1986
+ path: path5.join(claudeConfigDir(home, env), "settings.json"),
1966
1987
  content: hookConfig()
1967
1988
  }];
1968
1989
  },
1969
- sourcePaths(home) {
1990
+ sourcePaths(home, env) {
1991
+ const base = claudeConfigDir(home, env);
1970
1992
  return [
1971
- path5.join(home, ".claude", "projects"),
1993
+ path5.join(base, "projects"),
1994
+ path5.join(base, ".claude.json"),
1972
1995
  path5.join(home, ".claude.json")
1973
1996
  ];
1974
1997
  },
@@ -2473,36 +2496,43 @@ function hookConfig2() {
2473
2496
  }
2474
2497
  };
2475
2498
  }
2499
+ function codexHome(home, env) {
2500
+ const override = env?.CODEX_HOME;
2501
+ if (override && override.trim()) {
2502
+ return path6.resolve(override);
2503
+ }
2504
+ return path6.join(home, ".codex");
2505
+ }
2476
2506
  function createCodexAdapter() {
2477
- const CODE_PATH = ".codex";
2478
2507
  return {
2479
2508
  id: "codex",
2480
2509
  label: "Codex",
2481
2510
  agentName: "codex",
2482
2511
  kind: "agent",
2483
- detectPath(home) {
2484
- return path6.join(home, CODE_PATH);
2512
+ detectPath(home, env) {
2513
+ return codexHome(home, env);
2485
2514
  },
2486
- installedPath(home) {
2487
- return path6.join(home, CODE_PATH, "hooks.json");
2515
+ installedPath(home, env) {
2516
+ return path6.join(codexHome(home, env), "hooks.json");
2488
2517
  },
2489
- async isInstalled(home) {
2518
+ async isInstalled(home, env) {
2490
2519
  return isHooksJsonInstalled(
2491
- path6.join(home, CODE_PATH, "hooks.json"),
2520
+ path6.join(codexHome(home, env), "hooks.json"),
2492
2521
  "codetime hook --agent codex"
2493
2522
  );
2494
2523
  },
2495
- installEntries(home) {
2524
+ installEntries(home, env) {
2496
2525
  return [{
2497
2526
  kind: "hooks-json",
2498
- path: path6.join(home, CODE_PATH, "hooks.json"),
2527
+ path: path6.join(codexHome(home, env), "hooks.json"),
2499
2528
  content: hookConfig2()
2500
2529
  }];
2501
2530
  },
2502
- sourcePaths(home) {
2531
+ sourcePaths(home, env) {
2532
+ const base = codexHome(home, env);
2503
2533
  return [
2504
- path6.join(home, ".codex", "sessions"),
2505
- path6.join(home, ".codex", "history.jsonl")
2534
+ path6.join(base, "sessions"),
2535
+ path6.join(base, "history.jsonl")
2506
2536
  ];
2507
2537
  },
2508
2538
  parseSessionFile: parseCodexSessionFile
@@ -2855,7 +2885,23 @@ function opencodeUsageFromInfo(info) {
2855
2885
  tokensTotal: total
2856
2886
  };
2857
2887
  }
2858
- async function opencodeBackfillFiles(sourceRoot) {
2888
+ function opencodeConfigDir(home, env) {
2889
+ const override = env?.OPENCODE_CONFIG_DIR;
2890
+ if (override && override.trim()) {
2891
+ return path7.resolve(override);
2892
+ }
2893
+ const xdgConfig = env?.XDG_CONFIG_HOME;
2894
+ if (xdgConfig && xdgConfig.trim()) {
2895
+ return path7.join(path7.resolve(xdgConfig), "opencode");
2896
+ }
2897
+ return path7.join(home, ".config", "opencode");
2898
+ }
2899
+ function opencodeDataCandidates(home, env) {
2900
+ const xdgData = env?.XDG_DATA_HOME;
2901
+ const primary = xdgData && xdgData.trim() ? path7.join(path7.resolve(xdgData), "opencode", "opencode.db") : path7.join(home, ".local", "share", "opencode", "opencode.db");
2902
+ return [primary, path7.join(home, ".opencode", "opencode.db")];
2903
+ }
2904
+ async function opencodeBackfillFiles(sourceRoot, home = os2.homedir(), env) {
2859
2905
  const { stat: stat5 } = await import("node:fs/promises");
2860
2906
  if (sourceRoot) {
2861
2907
  if (!sourceRoot.endsWith(".db")) {
@@ -2867,11 +2913,7 @@ async function opencodeBackfillFiles(sourceRoot) {
2867
2913
  }
2868
2914
  return [{ path: sourceRoot, modifiedAt: info.mtime.toISOString() }];
2869
2915
  }
2870
- const candidates = [
2871
- path7.join(os2.homedir(), ".local", "share", "opencode", "opencode.db"),
2872
- path7.join(os2.homedir(), ".opencode", "opencode.db")
2873
- ];
2874
- for (const candidatePath of candidates) {
2916
+ for (const candidatePath of opencodeDataCandidates(home, env)) {
2875
2917
  const info = await stat5(candidatePath).catch(() => null);
2876
2918
  if (info) {
2877
2919
  return [{ path: candidatePath, modifiedAt: info.mtime.toISOString() }];
@@ -2925,39 +2967,35 @@ export const AgentTime = async ({ $, directory }) => {
2925
2967
  `;
2926
2968
  }
2927
2969
  function createOpenCodeAdapter() {
2928
- const OPENCODE_CONFIG = ".config/opencode";
2929
2970
  const PLUGIN_PATH = "plugins/codetime.mjs";
2930
2971
  return {
2931
2972
  id: "opencode",
2932
2973
  label: "OpenCode",
2933
2974
  agentName: "opencode",
2934
2975
  kind: "agent",
2935
- detectPath(home) {
2936
- return path7.join(home, OPENCODE_CONFIG);
2976
+ detectPath(home, env) {
2977
+ return opencodeConfigDir(home, env);
2937
2978
  },
2938
- installedPath(home) {
2939
- return path7.join(home, OPENCODE_CONFIG, PLUGIN_PATH);
2979
+ installedPath(home, env) {
2980
+ return path7.join(opencodeConfigDir(home, env), PLUGIN_PATH);
2940
2981
  },
2941
- async isInstalled(home) {
2982
+ async isInstalled(home, env) {
2942
2983
  try {
2943
2984
  const { pathExists: pathExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
2944
- return await pathExists2(path7.join(home, OPENCODE_CONFIG, PLUGIN_PATH)) || await pathExists2(path7.join(".opencode", PLUGIN_PATH));
2985
+ return await pathExists2(path7.join(opencodeConfigDir(home, env), PLUGIN_PATH)) || await pathExists2(path7.join(".opencode", PLUGIN_PATH));
2945
2986
  } catch {
2946
2987
  return false;
2947
2988
  }
2948
2989
  },
2949
- installEntries(home) {
2990
+ installEntries(home, env) {
2950
2991
  return [{
2951
2992
  kind: "file",
2952
- path: path7.join(home, OPENCODE_CONFIG, PLUGIN_PATH),
2993
+ path: path7.join(opencodeConfigDir(home, env), PLUGIN_PATH),
2953
2994
  content: opencodePluginContent()
2954
2995
  }];
2955
2996
  },
2956
- sourcePaths(home) {
2957
- return [
2958
- path7.join(home, ".local", "share", "opencode", "opencode.db"),
2959
- path7.join(home, ".opencode", "opencode.db")
2960
- ];
2997
+ sourcePaths(home, env) {
2998
+ return opencodeDataCandidates(home, env);
2961
2999
  },
2962
3000
  parseSessionFile: parseOpenCodeSessionFile
2963
3001
  };
@@ -3391,36 +3429,49 @@ export default function (pi: ExtensionAPI) {
3391
3429
  }
3392
3430
  `;
3393
3431
  }
3432
+ function piAgentDir(home, env) {
3433
+ const override = env?.PI_CODING_AGENT_DIR;
3434
+ if (override && override.trim()) {
3435
+ return path8.resolve(override);
3436
+ }
3437
+ return path8.join(home, ".pi", "agent");
3438
+ }
3439
+ function piSessionDir(home, env) {
3440
+ const override = env?.PI_CODING_AGENT_SESSION_DIR;
3441
+ if (override && override.trim()) {
3442
+ return path8.resolve(override);
3443
+ }
3444
+ return path8.join(piAgentDir(home, env), "sessions");
3445
+ }
3394
3446
  function createPiAdapter() {
3395
- const PI_EXTENSIONS_DIR = ".pi/agent/extensions";
3396
3447
  return {
3397
3448
  id: "pi",
3398
3449
  label: "Pi",
3399
3450
  agentName: "pi",
3400
3451
  kind: "agent",
3401
- detectPath(home) {
3402
- return path8.join(home, ".pi", "agent");
3452
+ detectPath(home, env) {
3453
+ return piAgentDir(home, env);
3403
3454
  },
3404
- installedPath(home) {
3405
- return path8.join(home, PI_EXTENSIONS_DIR, "codetime.ts");
3455
+ installedPath(home, env) {
3456
+ return path8.join(piAgentDir(home, env), "extensions", "codetime.ts");
3406
3457
  },
3407
- async isInstalled(home) {
3458
+ async isInstalled(home, env) {
3408
3459
  try {
3409
3460
  const { pathExists: pathExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
3410
- return await pathExists2(path8.join(home, PI_EXTENSIONS_DIR, "codetime.ts"));
3461
+ return await pathExists2(path8.join(piAgentDir(home, env), "extensions", "codetime.ts"));
3411
3462
  } catch {
3412
3463
  return false;
3413
3464
  }
3414
3465
  },
3415
- installEntries(home) {
3466
+ installEntries(home, env) {
3416
3467
  return [{
3417
3468
  kind: "file",
3418
- path: path8.join(home, PI_EXTENSIONS_DIR, "codetime.ts"),
3469
+ path: path8.join(piAgentDir(home, env), "extensions", "codetime.ts"),
3419
3470
  content: piExtensionContent()
3420
3471
  }];
3421
3472
  },
3422
- sourcePaths(home) {
3423
- return [path8.join(home, ".pi", "agent", "sessions")];
3473
+ sourcePaths(home, env) {
3474
+ return [piSessionDir(home, env)];
3424
3475
  },
3425
3476
  parseSessionFile: parsePiSessionFile
3426
3477
  };
@@ -4569,18 +4620,19 @@ function normalizeOptions(options) {
4569
4620
  }
4570
4621
  async function detectCommand(options, ctx, registry) {
4571
4622
  const home = resolveHome(options, ctx);
4623
+ const env = ctx.env;
4572
4624
  const adapters = registry.all();
4573
4625
  const targets = await Promise.all(adapters.map(async (adapter) => {
4574
- const detected = await pathExists(adapter.detectPath(home));
4575
- const installed = await adapter.isInstalled(home);
4626
+ const detected = await pathExists(adapter.detectPath(home, env));
4627
+ const installed = await adapter.isInstalled(home, env);
4576
4628
  return {
4577
4629
  id: adapter.id,
4578
4630
  label: adapter.label,
4579
4631
  kind: adapter.kind,
4580
4632
  detected,
4581
4633
  installed,
4582
- detectPath: adapter.detectPath(home),
4583
- installedPath: adapter.installedPath(home)
4634
+ detectPath: adapter.detectPath(home, env),
4635
+ installedPath: adapter.installedPath(home, env)
4584
4636
  };
4585
4637
  }));
4586
4638
  if (options.json) {
@@ -4597,6 +4649,7 @@ async function detectCommand(options, ctx, registry) {
4597
4649
  }
4598
4650
  async function installCommand(options, ctx, registry) {
4599
4651
  const home = resolveHome(options, ctx);
4652
+ const env = ctx.env;
4600
4653
  const dryRun = Boolean(options["dry-run"]);
4601
4654
  const force = Boolean(options.force);
4602
4655
  const allAdapters = registry.all();
@@ -4607,7 +4660,7 @@ async function installCommand(options, ctx, registry) {
4607
4660
  }
4608
4661
  const detected = [];
4609
4662
  for (const adapter of allAdapters) {
4610
- if (await pathExists(adapter.detectPath(home))) {
4663
+ if (await pathExists(adapter.detectPath(home, env))) {
4611
4664
  detected.push(adapter.id);
4612
4665
  }
4613
4666
  }
@@ -4617,7 +4670,7 @@ async function installCommand(options, ctx, registry) {
4617
4670
  return 1;
4618
4671
  }
4619
4672
  for (const adapter of allAdapters.filter((a) => selectedIds.includes(a.id))) {
4620
- for (const entry of adapter.installEntries(home)) {
4673
+ for (const entry of adapter.installEntries(home, env)) {
4621
4674
  await installEntry(entry, {
4622
4675
  dryRun,
4623
4676
  force,
@@ -4746,13 +4799,14 @@ async function backfillCommand(options, ctx, registry) {
4746
4799
  }
4747
4800
  async function createBackfillPlanFromOptions(options, ctx, action, registry) {
4748
4801
  const home = resolveHome(options, ctx);
4802
+ const env = ctx.env;
4749
4803
  const source = normalizeBackfillSource(stringOption(options.source) || "all");
4750
- const sourceDefs = source === "all" ? registry.all().map((a) => ({ id: a.id, label: a.label, paths: a.sourcePaths(home) })) : (() => {
4804
+ const sourceDefs = source === "all" ? registry.all().map((a) => ({ id: a.id, label: a.label, paths: a.sourcePaths(home, env) })) : (() => {
4751
4805
  const adapter = registry.get(source);
4752
4806
  if (!adapter) {
4753
4807
  return [];
4754
4808
  }
4755
- return [{ id: adapter.id, label: adapter.label, paths: adapter.sourcePaths(home) }];
4809
+ return [{ id: adapter.id, label: adapter.label, paths: adapter.sourcePaths(home, env) }];
4756
4810
  })();
4757
4811
  if (sourceDefs.length === 0) {
4758
4812
  throw new Error(`Unknown backfill source: ${source}`);
@@ -4761,7 +4815,7 @@ async function createBackfillPlanFromOptions(options, ctx, action, registry) {
4761
4815
  const candidates = candidateList.flat();
4762
4816
  let events = [];
4763
4817
  if (action !== "discover") {
4764
- const eventList = await createBackfillEventsFromDefs(sourceDefs, options, registry);
4818
+ const eventList = await createBackfillEventsFromDefs(sourceDefs, options, registry, ctx);
4765
4819
  events = eventList.flat();
4766
4820
  }
4767
4821
  const plannedEvents = events.map((event) => ({
@@ -4811,14 +4865,14 @@ async function createBackfillPlanFromOptions(options, ctx, action, registry) {
4811
4865
  privacy: "metadata only; prompt text, command text, source code, and diffs are not imported"
4812
4866
  };
4813
4867
  }
4814
- async function createBackfillEventsFromDefs(sourceDefs, options, registry, overrideFiles) {
4868
+ async function createBackfillEventsFromDefs(sourceDefs, options, registry, ctx, overrideFiles) {
4815
4869
  const events = [];
4816
4870
  for (const item of sourceDefs) {
4817
4871
  const parser = registry.getParser(item.id);
4818
4872
  if (!parser) {
4819
4873
  continue;
4820
4874
  }
4821
- const sourceFiles = await listBackfillSourceFiles(item, options);
4875
+ const sourceFiles = await listBackfillSourceFiles(item, options, ctx);
4822
4876
  const files = overrideFiles ?? sourceFiles.map((f) => f.path);
4823
4877
  for (const filePath of files) {
4824
4878
  const parsed = await parser(filePath, options);
@@ -4846,9 +4900,9 @@ async function createBackfillCandidates(source, options) {
4846
4900
  };
4847
4901
  }));
4848
4902
  }
4849
- async function listBackfillSourceFiles(source, options) {
4903
+ async function listBackfillSourceFiles(source, options, ctx) {
4850
4904
  if (source.id === "opencode") {
4851
- return opencodeBackfillFiles(stringOption(options["source-root"]));
4905
+ return opencodeBackfillFiles(stringOption(options["source-root"]), resolveHome(options, ctx), ctx.env);
4852
4906
  }
4853
4907
  const roots = stringOption(options["source-root"]) ? [requiredOption(options, "source-root")] : source.paths;
4854
4908
  const fileLists = await Promise.all(roots.map((r) => listJsonlFiles(r)));
@@ -4936,119 +4990,122 @@ async function importBackfillPlan(plan, options, ctx, registry) {
4936
4990
  write(ctx.stderr, "Only Codex, Claude Code, OpenCode, and Pi backfill import are implemented.\n");
4937
4991
  return 1;
4938
4992
  }
4939
- const allAdapters = registry.all();
4940
- const sourceDefs = allAdapters.filter((a) => supportedSources.has(a.id) && (source === "all" || a.id === source)).map((a) => ({ id: a.id, label: a.label, paths: a.sourcePaths(home) }));
4993
+ 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) }));
4941
4994
  if (options.force) {
4942
- try {
4943
- await rm(backfillIncrementalStatePath(home), { force: true });
4944
- } catch {
4945
- }
4946
- for (const item of sourceDefs) {
4947
- try {
4948
- const deleted = await deleteSessionRollupsBySourceAPI(item.id, options, ctx);
4949
- if (!options.json) {
4950
- write(ctx.stdout, `purged ${item.id}: ${deleted} old rollups
4951
- `);
4952
- }
4953
- } catch (error) {
4954
- debug(ctx, `Failed to purge ${item.id} rollups: ${error.message}
4955
- `);
4956
- }
4957
- }
4995
+ await purgeForcedSources(sourceDefs, home, options, ctx);
4958
4996
  }
4959
- const incrementalState = shouldUseIncrementalBackfill(options) ? await readBackfillIncrementalState(home) : void 0;
4960
- const selectedFilesBySource = /* @__PURE__ */ new Map();
4961
- const canonicalEvents = [];
4997
+ const incrementalState = shouldUseIncrementalBackfill(options) ? await readBackfillIncrementalState(home, ctx) : void 0;
4962
4998
  if (!options.json) {
4963
4999
  write(ctx.stdout, `importRun ${plan.importRun.importRunId}
4964
5000
  `);
4965
5001
  write(ctx.stdout, `sources ${sourceDefs.map((s) => s.id).join(", ") || "none"}
4966
5002
  `);
4967
5003
  }
5004
+ const { canonicalEvents, selectedFilesBySource } = await collectCanonicalEvents(
5005
+ sourceDefs,
5006
+ registry,
5007
+ incrementalState,
5008
+ options,
5009
+ ctx
5010
+ );
5011
+ const rollups = buildSessionRollups(canonicalEvents);
5012
+ const counts = await uploadSessionRollups(rollups, canonicalEvents.length, options, ctx);
5013
+ const result = {
5014
+ importRunId: plan.importRun.importRunId,
5015
+ source,
5016
+ planned: rollups.length,
5017
+ sourceEvents: canonicalEvents.length,
5018
+ ...counts
5019
+ };
5020
+ if (options.json) {
5021
+ write(ctx.stdout, `${JSON.stringify(result, null, 2)}
5022
+ `);
5023
+ }
5024
+ if (counts.failed === 0 && counts.conflicts === 0 && incrementalState) {
5025
+ await updateBackfillIncrementalState(home, incrementalState, selectedFilesBySource);
5026
+ }
5027
+ return counts.failed > 0 || counts.conflicts > 0 && !options["skip-conflicts"] ? 1 : 0;
5028
+ }
5029
+ async function purgeForcedSources(sourceDefs, home, options, ctx) {
5030
+ try {
5031
+ await rm(backfillIncrementalStatePath(home), { force: true });
5032
+ } catch (error) {
5033
+ debug(ctx, `Failed to clear backfill watermark: ${error.message}
5034
+ `);
5035
+ }
5036
+ for (const item of sourceDefs) {
5037
+ try {
5038
+ const deleted = await deleteSessionRollupsBySourceAPI(item.id, options, ctx);
5039
+ if (!options.json) {
5040
+ write(ctx.stdout, `purged ${item.id}: ${deleted} old rollups
5041
+ `);
5042
+ }
5043
+ } catch (error) {
5044
+ debug(ctx, `Failed to purge ${item.id} rollups: ${error.message}
5045
+ `);
5046
+ }
5047
+ }
5048
+ }
5049
+ async function collectCanonicalEvents(sourceDefs, registry, incrementalState, options, ctx) {
5050
+ const selectedFilesBySource = /* @__PURE__ */ new Map();
5051
+ const canonicalEvents = [];
4968
5052
  for (const item of sourceDefs) {
4969
5053
  const parser = registry.getParser(item.id);
4970
5054
  if (!parser) {
4971
5055
  continue;
4972
5056
  }
4973
- const sourceFiles = await listBackfillSourceFiles(item, options);
5057
+ const sourceFiles = await listBackfillSourceFiles(item, options, ctx);
4974
5058
  const selectedFiles = selectBackfillFilesForImport(sourceFiles, incrementalState?.sources[item.id]?.watermarkTs);
4975
5059
  selectedFilesBySource.set(item.id, selectedFiles);
4976
5060
  const filePaths = selectedFiles.map((f) => f.path);
4977
5061
  const sourceEvents = [];
4978
- if (options.json) {
4979
- for (const filePath of filePaths) {
4980
- const parsed = await parser(filePath, options);
4981
- for (const event of parsed) {
4982
- if (matchesBackfillFilters(event, options)) {
4983
- sourceEvents.push(event);
4984
- }
4985
- }
4986
- }
4987
- } else {
4988
- const bar = new ProgressBar(ctx.stdout, `${item.id.padEnd(12)}`);
4989
- bar.init(filePaths.length, `0 events`);
4990
- for (let fi = 0; fi < filePaths.length; fi += 1) {
4991
- const parsed = await parser(filePaths[fi], options);
4992
- for (const event of parsed) {
4993
- if (matchesBackfillFilters(event, options)) {
4994
- sourceEvents.push(event);
4995
- }
5062
+ const bar = options.json ? void 0 : new ProgressBar(ctx.stdout, `${item.id.padEnd(12)}`);
5063
+ bar?.init(filePaths.length, `0 events`);
5064
+ for (let fi = 0; fi < filePaths.length; fi += 1) {
5065
+ const parsed = await parser(filePaths[fi], options);
5066
+ for (const event of parsed) {
5067
+ if (matchesBackfillFilters(event, options)) {
5068
+ sourceEvents.push(event);
4996
5069
  }
4997
- bar.tick(`${fi + 1}/${filePaths.length} files, ${sourceEvents.length} events`);
4998
5070
  }
4999
- bar.finalize(`${sourceEvents.length} events`);
5071
+ bar?.tick(`${fi + 1}/${filePaths.length} files, ${sourceEvents.length} events`);
5000
5072
  }
5073
+ bar?.finalize(`${sourceEvents.length} events`);
5001
5074
  for (const event of sourceEvents) canonicalEvents.push(event);
5002
5075
  }
5076
+ return { canonicalEvents, selectedFilesBySource };
5077
+ }
5078
+ async function uploadSessionRollups(rollups, eventCount, options, ctx) {
5003
5079
  const counts = { inserted: 0, skipped: 0, conflicts: 0, failed: 0 };
5004
- const rollups = buildSessionRollups(canonicalEvents);
5005
5080
  const batchSize = Math.max(1, Math.floor(numberOption(options["batch-size"]) || DEFAULT_BACKFILL_BATCH_SIZE));
5006
5081
  const batchBytes = Math.max(64 * 1024, Math.floor(numberOption(options["batch-bytes"]) || DEFAULT_BACKFILL_BATCH_BYTES));
5007
5082
  const batches = packRollupBatches(rollups, batchSize, batchBytes);
5008
5083
  const totalBatches = batches.length;
5009
5084
  let uploadBar;
5010
5085
  if (!options.json) {
5011
- write(ctx.stdout, `rollup ${rollups.length} from ${canonicalEvents.length} events
5086
+ write(ctx.stdout, `rollup ${rollups.length} from ${eventCount} events
5012
5087
  `);
5013
5088
  uploadBar = new ProgressBar(ctx.stdout, `upload`.padEnd(12));
5014
5089
  uploadBar.init(totalBatches, `0/${totalBatches} batches, 0 inserted`);
5015
5090
  }
5016
- for (let i = 0; i < batches.length; i += 1) {
5017
- const batch = batches[i];
5091
+ for (const [i, batch_] of batches.entries()) {
5092
+ const batch = batch_;
5018
5093
  const batchNumber = i + 1;
5019
5094
  try {
5020
- const result2 = await sendSessionRollupBatch(batch, options, ctx);
5021
- counts.inserted += result2.inserted;
5022
- counts.skipped += result2.skipped;
5023
- counts.conflicts += result2.conflicts;
5024
- counts.failed += result2.failed;
5095
+ const result = await sendSessionRollupBatch(batch, options, ctx);
5096
+ counts.inserted += result.inserted;
5097
+ counts.skipped += result.skipped;
5098
+ counts.conflicts += result.conflicts;
5099
+ counts.failed += result.failed;
5025
5100
  } catch (error) {
5026
5101
  debug(ctx, `backfill rollup batch ${batchNumber}/${totalBatches} (${batch.length} rollups) failed: ${error.message}
5027
5102
  `);
5028
5103
  counts.failed += batch.length;
5029
5104
  }
5030
- if (uploadBar) {
5031
- uploadBar.update(batchNumber, `${batchNumber}/${totalBatches} batches, inserted ${counts.inserted}`);
5032
- }
5105
+ uploadBar?.update(batchNumber, `${batchNumber}/${totalBatches} batches, inserted ${counts.inserted}`);
5033
5106
  }
5034
- if (uploadBar) {
5035
- uploadBar.finalize(`inserted ${counts.inserted} \xB7 skipped ${counts.skipped}${counts.conflicts ? ` \xB7 conflicts ${counts.conflicts}` : ""}${counts.failed ? ` \xB7 failed ${counts.failed}` : ""}`);
5036
- }
5037
- const result = {
5038
- importRunId: plan.importRun.importRunId,
5039
- source,
5040
- planned: rollups.length,
5041
- sourceEvents: canonicalEvents.length,
5042
- ...counts
5043
- };
5044
- if (options.json) {
5045
- write(ctx.stdout, `${JSON.stringify(result, null, 2)}
5046
- `);
5047
- }
5048
- if (counts.failed === 0 && counts.conflicts === 0 && incrementalState) {
5049
- await updateBackfillIncrementalState(home, incrementalState, selectedFilesBySource);
5050
- }
5051
- return counts.failed > 0 || counts.conflicts > 0 && !options["skip-conflicts"] ? 1 : 0;
5107
+ uploadBar?.finalize(`inserted ${counts.inserted} \xB7 skipped ${counts.skipped}${counts.conflicts ? ` \xB7 conflicts ${counts.conflicts}` : ""}${counts.failed ? ` \xB7 failed ${counts.failed}` : ""}`);
5108
+ return counts;
5052
5109
  }
5053
5110
  function backfillVerifyCommand(options, ctx) {
5054
5111
  const importRunId = stringOption(options["import-run"]);
@@ -5072,12 +5129,7 @@ ${result.message}
5072
5129
  return 0;
5073
5130
  }
5074
5131
  async function sendSessionRollupBatch(rollups, options, ctx) {
5075
- const remote = resolveRemote({
5076
- apiUrl: stringOption(options["api-url"]),
5077
- token: stringOption(options.token),
5078
- env: ctx.env,
5079
- fetch: ctx.fetch
5080
- });
5132
+ const remote = resolveRemoteFromOptions(options, ctx);
5081
5133
  if (!remote) {
5082
5134
  throw new Error("No fetch available for HTTP upload");
5083
5135
  }
@@ -5096,12 +5148,7 @@ async function sendSessionRollupBatch(rollups, options, ctx) {
5096
5148
  return result;
5097
5149
  }
5098
5150
  async function deleteSessionRollupsBySourceAPI(source, options, ctx) {
5099
- const remote = resolveRemote({
5100
- apiUrl: stringOption(options["api-url"]),
5101
- token: stringOption(options.token),
5102
- env: ctx.env,
5103
- fetch: ctx.fetch
5104
- });
5151
+ const remote = resolveRemoteFromOptions(options, ctx);
5105
5152
  if (!remote) {
5106
5153
  throw new Error("No fetch available for HTTP delete");
5107
5154
  }
@@ -5130,7 +5177,9 @@ function packRollupBatches(rollups, maxCount, maxBytes) {
5130
5177
  current.push(rollup);
5131
5178
  currentBytes += size;
5132
5179
  }
5133
- if (current.length > 0) batches.push(current);
5180
+ if (current.length > 0) {
5181
+ batches.push(current);
5182
+ }
5134
5183
  return batches;
5135
5184
  }
5136
5185
  function selectBackfillFilesForImport(files, watermarkTs) {
@@ -5155,10 +5204,25 @@ function syncLocalTriggerStatePath(home) {
5155
5204
  function syncLocalTriggerLockPath(home) {
5156
5205
  return path11.join(home, ".codetime", "sync-local-trigger.lock");
5157
5206
  }
5158
- async function readBackfillIncrementalState(home) {
5159
- const state = await readJsonIfExists(backfillIncrementalStatePath(home));
5207
+ async function readBackfillIncrementalState(home, ctx) {
5208
+ const statePath = backfillIncrementalStatePath(home);
5209
+ const state = await readJsonIfExists(statePath);
5210
+ if (state === null) {
5211
+ return { version: BACKFILL_STATE_SCHEMA_VERSION, sources: {} };
5212
+ }
5160
5213
  if (!isPlainObject(state) || !isPlainObject(state.sources)) {
5161
- return { version: 1, sources: {} };
5214
+ if (ctx) {
5215
+ debug(ctx, `backfill-state malformed at ${statePath}; ignoring watermarks
5216
+ `);
5217
+ }
5218
+ return { version: BACKFILL_STATE_SCHEMA_VERSION, sources: {} };
5219
+ }
5220
+ if (state.version !== BACKFILL_STATE_SCHEMA_VERSION) {
5221
+ if (ctx) {
5222
+ debug(ctx, `backfill-state version ${String(state.version)} at ${statePath} differs from current v${BACKFILL_STATE_SCHEMA_VERSION}; dropping watermarks so the next sync re-imports under the new parser
5223
+ `);
5224
+ }
5225
+ return { version: BACKFILL_STATE_SCHEMA_VERSION, sources: {} };
5162
5226
  }
5163
5227
  const sources = {};
5164
5228
  for (const source of ["codex", "claude-code", "opencode", "pi"]) {
@@ -5167,7 +5231,7 @@ async function readBackfillIncrementalState(home) {
5167
5231
  sources[source] = { watermarkTs: item.watermarkTs };
5168
5232
  }
5169
5233
  }
5170
- return { version: 1, sources };
5234
+ return { version: BACKFILL_STATE_SCHEMA_VERSION, sources };
5171
5235
  }
5172
5236
  async function updateBackfillIncrementalState(home, state, selectedFilesBySource) {
5173
5237
  for (const [source, files] of selectedFilesBySource.entries()) {
@@ -5339,6 +5403,15 @@ function debug(ctx, message) {
5339
5403
  write(ctx.stderr, message);
5340
5404
  }
5341
5405
  }
5406
+ function resolveRemoteFromOptions(options, ctx) {
5407
+ return resolveRemote({
5408
+ apiUrl: stringOption(options["api-url"]),
5409
+ token: stringOption(options.token),
5410
+ env: ctx.env,
5411
+ fetch: ctx.fetch,
5412
+ homeOverride: stringOption(options.home)
5413
+ });
5414
+ }
5342
5415
  function maskToken(token) {
5343
5416
  if (token.length <= 8) {
5344
5417
  return "***";
@@ -5399,12 +5472,7 @@ Usage: codetime token [set <token>|show|clear] [--remote <url>]
5399
5472
  return 1;
5400
5473
  }
5401
5474
  async function machineCommand(action, options, ctx) {
5402
- const remote = resolveRemote({
5403
- apiUrl: stringOption(options["api-url"]),
5404
- token: stringOption(options.token),
5405
- env: ctx.env,
5406
- fetch: ctx.fetch
5407
- });
5475
+ const remote = resolveRemoteFromOptions(options, ctx);
5408
5476
  if (!remote || !remote.token) {
5409
5477
  write(ctx.stderr, "machine command requires login (run `codetime login`).\n");
5410
5478
  return 1;
@@ -5485,14 +5553,11 @@ Global options:
5485
5553
  --token <token> Bearer token for this invocation (overrides config).
5486
5554
 
5487
5555
  Token precedence (highest first):
5488
- --token flag > CODETIME_AGENT_TOKEN env > AGENT_TIME_API_TOKEN env
5489
- (legacy alias) > saved config (~/.codetime/config.json).
5556
+ --token flag > CODETIME_TOKEN env > saved config (~/.codetime/config.json).
5490
5557
 
5491
5558
  Environment:
5492
5559
  CODETIME_API_URL Defaults to ${DEFAULT_API_URL}
5493
- CODETIME_AGENT_TOKEN Bearer token for the API
5494
- AGENT_TIME_API_URL Legacy alias for CODETIME_API_URL
5495
- AGENT_TIME_API_TOKEN Legacy alias for CODETIME_AGENT_TOKEN
5560
+ CODETIME_TOKEN Bearer token for the API
5496
5561
  `;
5497
5562
  }
5498
5563
  export {
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "codetime-cli",
3
3
  "type": "module",
4
- "version": "0.2.0",
4
+ "version": "0.3.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": {
8
8
  "access": "public"
9
9
  },
10
- "homepage": "https://github.com/jannchie/codetime-cli#readme",
10
+ "homepage": "https://github.com/codetime-dev/codetime-cli#readme",
11
11
  "repository": {
12
12
  "type": "git",
13
- "url": "git+https://github.com/jannchie/codetime-cli.git"
13
+ "url": "git+https://github.com/codetime-dev/codetime-cli.git"
14
14
  },
15
15
  "bugs": {
16
- "url": "https://github.com/jannchie/codetime-cli/issues"
16
+ "url": "https://github.com/codetime-dev/codetime-cli/issues"
17
17
  },
18
18
  "keywords": [
19
19
  "codetime",
@@ -35,7 +35,7 @@
35
35
  "devDependencies": {
36
36
  "cac": "^7.0.0",
37
37
  "esbuild": "^0.28.0",
38
- "@codetime/shared": "0.2.0"
38
+ "@codetime/shared": "0.3.0"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsc -p tsconfig.json",