codetime-cli 0.7.3 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/codetime.mjs +52 -18
  2. package/package.json +1 -1
package/bin/codetime.mjs CHANGED
@@ -208,7 +208,7 @@ var init_fs = __esm({
208
208
 
209
209
  // src/cli.ts
210
210
  import { spawn } from "node:child_process";
211
- import { rm as rm2, stat as stat6 } from "node:fs/promises";
211
+ import { rm as rm2, stat as stat7 } from "node:fs/promises";
212
212
  import os3 from "node:os";
213
213
  import path13 from "node:path";
214
214
  import { fileURLToPath } from "node:url";
@@ -1030,7 +1030,7 @@ import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
1030
1030
  import path2 from "node:path";
1031
1031
 
1032
1032
  // src/lib/constants.ts
1033
- var PACKAGE_VERSION = true ? "0.7.3" : "0.1.1";
1033
+ var PACKAGE_VERSION = true ? "0.7.4" : "0.1.1";
1034
1034
  var DEFAULT_API_URL = "https://codetime.dev";
1035
1035
  var DEFAULT_BACKFILL_BATCH_SIZE = 50;
1036
1036
  var DEFAULT_BACKFILL_BATCH_BYTES = 800 * 1024;
@@ -2334,7 +2334,7 @@ function createClaudeCodeAdapter() {
2334
2334
  }
2335
2335
 
2336
2336
  // src/adapters/codex.ts
2337
- import { readFile as readFile4 } from "node:fs/promises";
2337
+ import { readFile as readFile4, stat as stat4 } from "node:fs/promises";
2338
2338
  import path7 from "node:path";
2339
2339
 
2340
2340
  // src/lib/diff.ts
@@ -2381,6 +2381,7 @@ function fileActivitiesFromPatchChanges(changes, ts, cwd, displayFilePath3) {
2381
2381
  }
2382
2382
 
2383
2383
  // src/adapters/codex.ts
2384
+ init_fs();
2384
2385
  async function parseCodexSessionFile(filePath, options) {
2385
2386
  const text = await readFile4(filePath, "utf8");
2386
2387
  const lines = text.split("\n").filter(Boolean);
@@ -3037,15 +3038,39 @@ function createCodexAdapter() {
3037
3038
  const base = codexHome(home, env);
3038
3039
  return [
3039
3040
  path7.join(base, "sessions"),
3041
+ // Codex moves old rollouts here as it rotates; include so pre-existing
3042
+ // archived history is discovered, not silently dropped. Actual file
3043
+ // listing dedups active vs archived — see codexBackfillFiles.
3044
+ path7.join(base, "archived_sessions"),
3040
3045
  path7.join(base, "history.jsonl")
3041
3046
  ];
3042
3047
  },
3043
3048
  parseSessionFile: parseCodexSessionFile
3044
3049
  };
3045
3050
  }
3051
+ async function codexBackfillFiles(sourceRoot, home, env) {
3052
+ const files = sourceRoot ? await listJsonlFiles(sourceRoot) : await codexDefaultFiles(home, env);
3053
+ return Promise.all(files.map(async (filePath) => {
3054
+ const info = await stat4(filePath);
3055
+ return { path: filePath, modifiedAt: info.mtime.toISOString() };
3056
+ }));
3057
+ }
3058
+ async function codexDefaultFiles(home, env) {
3059
+ const base = codexHome(home, env);
3060
+ const sessionsDir = path7.join(base, "sessions");
3061
+ const archivedDir = path7.join(base, "archived_sessions");
3062
+ const [activeFiles, archivedFiles, historyFiles] = await Promise.all([
3063
+ listJsonlFiles(sessionsDir),
3064
+ listJsonlFiles(archivedDir),
3065
+ listJsonlFiles(path7.join(base, "history.jsonl"))
3066
+ ]);
3067
+ const activeRelative = new Set(activeFiles.map((f) => path7.relative(sessionsDir, f)));
3068
+ const dedupedArchived = archivedFiles.filter((f) => !activeRelative.has(path7.relative(archivedDir, f)));
3069
+ return [...activeFiles, ...dedupedArchived, ...historyFiles];
3070
+ }
3046
3071
 
3047
3072
  // src/adapters/gemini.ts
3048
- import { readFile as readFile5, stat as stat4 } from "node:fs/promises";
3073
+ import { readFile as readFile5, stat as stat5 } from "node:fs/promises";
3049
3074
  import path8 from "node:path";
3050
3075
  init_fs();
3051
3076
  function tokenFromKeys(record, keys) {
@@ -3221,7 +3246,7 @@ function parseJsonlRecords(lines, fileStem, fallbackTs) {
3221
3246
  async function parseGeminiSessionFile(filePath, options) {
3222
3247
  const text = await readFile5(filePath, "utf8");
3223
3248
  const fileStem = path8.basename(filePath).replace(/\.jsonl?$/, "");
3224
- const info = await stat4(filePath);
3249
+ const info = await stat5(filePath);
3225
3250
  const fallbackTs = info.mtime.toISOString();
3226
3251
  let parsed;
3227
3252
  if (filePath.endsWith(".jsonl")) {
@@ -3312,7 +3337,7 @@ async function geminiBackfillFiles(sourceRoot, home, env) {
3312
3337
  );
3313
3338
  const files = fileLists.flat().sort();
3314
3339
  return Promise.all(files.map(async (filePath) => {
3315
- const info = await stat4(filePath);
3340
+ const info = await stat5(filePath);
3316
3341
  return { path: filePath, modifiedAt: info.mtime.toISOString() };
3317
3342
  }));
3318
3343
  }
@@ -3731,19 +3756,19 @@ function opencodeDataCandidates(home, env) {
3731
3756
  return [primary, path9.join(home, ".opencode", "opencode.db")];
3732
3757
  }
3733
3758
  async function opencodeBackfillFiles(sourceRoot, home = os2.homedir(), env) {
3734
- const { stat: stat7 } = await import("node:fs/promises");
3759
+ const { stat: stat8 } = await import("node:fs/promises");
3735
3760
  if (sourceRoot) {
3736
3761
  if (!sourceRoot.endsWith(".db")) {
3737
3762
  return [];
3738
3763
  }
3739
- const info = await stat7(sourceRoot).catch(() => null);
3764
+ const info = await stat8(sourceRoot).catch(() => null);
3740
3765
  if (!info) {
3741
3766
  return [];
3742
3767
  }
3743
3768
  return [{ path: sourceRoot, modifiedAt: info.mtime.toISOString() }];
3744
3769
  }
3745
3770
  for (const candidatePath of opencodeDataCandidates(home, env)) {
3746
- const info = await stat7(candidatePath).catch(() => null);
3771
+ const info = await stat8(candidatePath).catch(() => null);
3747
3772
  if (info) {
3748
3773
  return [{ path: candidatePath, modifiedAt: info.mtime.toISOString() }];
3749
3774
  }
@@ -4912,7 +4937,7 @@ function defaultMachineName() {
4912
4937
  init_fs();
4913
4938
 
4914
4939
  // src/lib/logger.ts
4915
- import { appendFile, mkdir as mkdir2, rename as rename2, stat as stat5 } from "node:fs/promises";
4940
+ import { appendFile, mkdir as mkdir2, rename as rename2, stat as stat6 } from "node:fs/promises";
4916
4941
  import { homedir as homedir2 } from "node:os";
4917
4942
  import path12 from "node:path";
4918
4943
  var MAX_BYTES = 1 * 1024 * 1024;
@@ -4930,7 +4955,7 @@ function serializeError(error) {
4930
4955
  }
4931
4956
  async function rotateIfNeeded(file) {
4932
4957
  try {
4933
- const info = await stat5(file);
4958
+ const info = await stat6(file);
4934
4959
  if (info.size > MAX_BYTES) {
4935
4960
  await rename2(file, `${file}.1`).catch(() => {
4936
4961
  });
@@ -5217,7 +5242,7 @@ async function deleteMachine(remote, id) {
5217
5242
  }
5218
5243
 
5219
5244
  // src/lib/types.ts
5220
- var BACKFILL_STATE_SCHEMA_VERSION = 4;
5245
+ var BACKFILL_STATE_SCHEMA_VERSION = 5;
5221
5246
 
5222
5247
  // src/cli.ts
5223
5248
  function createRegistry() {
@@ -5288,8 +5313,8 @@ function createCli(ctx, registry) {
5288
5313
  cli.command("hook", "Read agent hook JSON from stdin and report a throttled event").option("--agent <name>", "Agent name").option("--project <name>", "Project name").option("--min-interval <seconds>", "Minimum seconds between similar hook reports").action((options) => hookCommand(normalizeOptions(options), ctx));
5289
5314
  cli.command("sync-local-trigger", "Trigger one background local sync with throttle and locking").option("--min-interval <seconds>", "Minimum seconds between sync triggers").action((options) => syncLocalTriggerCommand(normalizeOptions(options), ctx, registry));
5290
5315
  cli.command("sync-local-runner", "Internal background local sync runner").option("--lock-file <path>", "Lock file for the active sync").option("--state-file <path>", "State file for trigger metadata").action((options) => syncLocalRunnerCommand(normalizeOptions(options), ctx, registry));
5291
- 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));
5292
- cli.command("sync", "Import and upload all local agent history (shorthand for `backfill import --source all`)").option("--source <source>", "Limit to one source (default: all)").option("--since <time>", "Only include history after this time").option("--until <time>", "Only include history before this time").option("--project <name>", "Project filter").option("--batch-size <count>", "Max rollups per request (also bounded by --batch-bytes)").option("--force", "Force full re-import: clear watermark and re-process all files").option("--dry-run", "Print the planned import without uploading").action((options) => {
5316
+ cli.command("backfill [action]", "Inspect local history import candidates").option("--source <source>", "Backfill source").option("--since <time>", "Only include history after this time").option("--until <time>", "Only include history before this time").option("--project <name>", "Project filter").option("--source-root <path>", "Override source history root").option("--include-source-path", "Include local source paths in output").option("--import-run <id>", "Import run id for verify/resume workflows").option("--limit <count>", "Maximum session files to parse").option("--batch-size <count>", "Max rollups per request (also bounded by --batch-bytes)").option("--batch-bytes <bytes>", "Soft byte cap for the JSON body of a single ingest POST").option("--replace", "Replace conflicting records during import (default)").option("--skip-conflicts", "Skip conflicting records instead of replacing them").option("--force", "Full re-import: re-parse every file and overwrite matching rollups (non-destructive \u2014 nothing is deleted)").option("--purge", "Before re-importing, delete THIS machine's existing rollups for the source(s). Destructive: also removes rollups whose local files are gone. Implies --force").action((action, options) => backfillCommand({ ...normalizeOptions(options), action }, ctx, registry));
5317
+ cli.command("sync", "Import and upload all local agent history (shorthand for `backfill import --source all`)").option("--source <source>", "Limit to one source (default: all)").option("--since <time>", "Only include history after this time").option("--until <time>", "Only include history before this time").option("--project <name>", "Project filter").option("--batch-size <count>", "Max rollups per request (also bounded by --batch-bytes)").option("--force", "Full re-import: re-parse every file and overwrite matching rollups (non-destructive \u2014 nothing is deleted)").option("--purge", "Before re-importing, delete THIS machine's existing rollups for the source(s). Destructive: also removes rollups whose local files are gone. Implies --force").option("--dry-run", "Print the planned import without uploading").action((options) => {
5293
5318
  const opts = normalizeOptions(options);
5294
5319
  return backfillCommand({ ...opts, action: "import", source: stringOption(opts.source) || "all" }, ctx, registry);
5295
5320
  });
@@ -5641,11 +5666,15 @@ async function listBackfillSourceFiles(source, options, ctx) {
5641
5666
  if (source.id === "gemini") {
5642
5667
  return geminiBackfillFiles(stringOption(options["source-root"]), resolveHome(options, ctx), ctx.env);
5643
5668
  }
5669
+ if (source.id === "codex") {
5670
+ const files2 = await codexBackfillFiles(stringOption(options["source-root"]), resolveHome(options, ctx), ctx.env);
5671
+ return files2.sort((a, b) => a.path.localeCompare(b.path)).slice(0, numberOption(options.limit) || void 0);
5672
+ }
5644
5673
  const roots = stringOption(options["source-root"]) ? [requiredOption(options, "source-root")] : source.paths;
5645
5674
  const fileLists = await Promise.all(roots.map((r) => listJsonlFiles(r)));
5646
5675
  const files = fileLists.flat().sort().slice(0, numberOption(options.limit) || void 0);
5647
5676
  return Promise.all(files.map(async (filePath) => {
5648
- const info = await stat6(filePath);
5677
+ const info = await stat7(filePath);
5649
5678
  return { path: filePath, modifiedAt: info.mtime.toISOString() };
5650
5679
  }));
5651
5680
  }
@@ -5729,8 +5758,11 @@ async function importBackfillPlan(plan, options, ctx, registry) {
5729
5758
  return 1;
5730
5759
  }
5731
5760
  const sourceDefs = registry.all().filter((a) => supportedSources.has(a.id) && (source === "all" || a.id === source)).map((a) => ({ id: a.id, label: a.label, paths: a.sourcePaths(home, ctx.env) }));
5732
- if (options.force) {
5733
- await purgeForcedSources(sourceDefs, home, options, ctx);
5761
+ if (options.purge) {
5762
+ await purgeSourceRollups(sourceDefs, options, ctx);
5763
+ }
5764
+ if (options.force || options.purge) {
5765
+ await clearBackfillWatermark(home, ctx);
5734
5766
  }
5735
5767
  const incrementalState = shouldUseIncrementalBackfill(options) ? await readBackfillIncrementalState(home, ctx) : void 0;
5736
5768
  if (!options.json) {
@@ -5764,13 +5796,15 @@ async function importBackfillPlan(plan, options, ctx, registry) {
5764
5796
  }
5765
5797
  return counts.failed > 0 || counts.conflicts > 0 && !options["skip-conflicts"] ? 1 : 0;
5766
5798
  }
5767
- async function purgeForcedSources(sourceDefs, home, options, ctx) {
5799
+ async function clearBackfillWatermark(home, ctx) {
5768
5800
  try {
5769
5801
  await rm2(backfillIncrementalStatePath(home), { force: true });
5770
5802
  } catch (error) {
5771
5803
  debug(ctx, `Failed to clear backfill watermark: ${error.message}
5772
5804
  `);
5773
5805
  }
5806
+ }
5807
+ async function purgeSourceRollups(sourceDefs, options, ctx) {
5774
5808
  for (const item of sourceDefs) {
5775
5809
  try {
5776
5810
  const deleted = await deleteSessionRollupsBySourceAPI(item.id, options, ctx);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codetime-cli",
3
3
  "type": "module",
4
- "version": "0.7.3",
4
+ "version": "0.7.4",
5
5
  "description": "codetime CLI — install AI-agent hooks (Claude Code, Codex, OpenCode, Pi) and report activity to codetime.dev.",
6
6
  "license": "MIT",
7
7
  "publishConfig": {