codetime-cli 0.7.0 → 0.7.2

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 +194 -40
  2. package/package.json +1 -1
package/bin/codetime.mjs CHANGED
@@ -19,10 +19,13 @@ __export(fs_exports, {
19
19
  parseJsonFile: () => parseJsonFile,
20
20
  pathExists: () => pathExists,
21
21
  readJsonIfExists: () => readJsonIfExists,
22
+ readJsonIfExistsTolerant: () => readJsonIfExistsTolerant,
22
23
  readTextIfExists: () => readTextIfExists,
24
+ writeFileAtomic: () => writeFileAtomic,
23
25
  writeGeneratedFile: () => writeGeneratedFile
24
26
  });
25
- import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
27
+ import { randomBytes } from "node:crypto";
28
+ import { mkdir, open, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
26
29
  import path from "node:path";
27
30
  async function pathExists(filePath) {
28
31
  try {
@@ -56,6 +59,59 @@ function parseJsonFile(filePath, text) {
56
59
  throw new Error(`Could not parse JSON file ${filePath}: ${error.message}`);
57
60
  }
58
61
  }
62
+ async function readJsonIfExistsTolerant(filePath, onCorrupt) {
63
+ const text = await readTextIfExists(filePath);
64
+ if (text === null) {
65
+ return null;
66
+ }
67
+ try {
68
+ return JSON.parse(text);
69
+ } catch (error) {
70
+ onCorrupt?.(error);
71
+ return null;
72
+ }
73
+ }
74
+ async function writeFileAtomic(filePath, data, options = {}) {
75
+ const dir = path.dirname(filePath);
76
+ await mkdir(dir, { recursive: true });
77
+ const tmpPath = path.join(
78
+ dir,
79
+ `.${path.basename(filePath)}.${process.pid}.${Date.now()}.${randomBytes(6).toString("hex")}.tmp`
80
+ );
81
+ let handle;
82
+ try {
83
+ handle = await open(tmpPath, "wx", options.mode ?? 438);
84
+ await handle.writeFile(data, "utf8");
85
+ if (options.mode !== void 0) {
86
+ await handle.chmod(options.mode);
87
+ }
88
+ await handle.close();
89
+ handle = void 0;
90
+ await renameWithRetry(tmpPath, filePath);
91
+ } catch (error) {
92
+ if (handle) {
93
+ await handle.close().catch(() => {
94
+ });
95
+ }
96
+ await rm(tmpPath, { force: true }).catch(() => {
97
+ });
98
+ throw error;
99
+ }
100
+ }
101
+ async function renameWithRetry(from, to, attempts = 5) {
102
+ for (let i = 0; ; i++) {
103
+ try {
104
+ await rename(from, to);
105
+ return;
106
+ } catch (error) {
107
+ const code = error.code;
108
+ if (i >= attempts - 1 || code !== "EPERM" && code !== "EACCES" && code !== "EBUSY") {
109
+ throw error;
110
+ }
111
+ await new Promise((resolve) => setTimeout(resolve, 10 * (i + 1)));
112
+ }
113
+ }
114
+ }
59
115
  async function writeGeneratedFile(filePath, content, { dryRun, force, onWrite }) {
60
116
  if (dryRun) {
61
117
  onWrite(`Would write ${filePath}`);
@@ -152,7 +208,7 @@ var init_fs = __esm({
152
208
 
153
209
  // src/cli.ts
154
210
  import { spawn } from "node:child_process";
155
- import { mkdir as mkdir3, rm, stat as stat6, writeFile as writeFile2 } from "node:fs/promises";
211
+ import { rm as rm2, stat as stat6 } from "node:fs/promises";
156
212
  import os3 from "node:os";
157
213
  import path13 from "node:path";
158
214
  import { fileURLToPath } from "node:url";
@@ -974,7 +1030,7 @@ import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
974
1030
  import path2 from "node:path";
975
1031
 
976
1032
  // src/lib/constants.ts
977
- var PACKAGE_VERSION = true ? "0.7.0" : "0.1.1";
1033
+ var PACKAGE_VERSION = true ? "0.7.2" : "0.1.1";
978
1034
  var DEFAULT_API_URL = "https://codetime.dev";
979
1035
  var DEFAULT_BACKFILL_BATCH_SIZE = 50;
980
1036
  var DEFAULT_BACKFILL_BATCH_BYTES = 800 * 1024;
@@ -2260,6 +2316,8 @@ async function parseCodexSessionFile(filePath, options) {
2260
2316
  let turnIdAtLastUserMessage;
2261
2317
  let sessionMetaLocked = false;
2262
2318
  let lastTokenUsageKey;
2319
+ const replaySecond = detectSubagentReplaySecond(text, lines);
2320
+ let skipReplay = replaySecond !== void 0;
2263
2321
  const pendingToolCalls = /* @__PURE__ */ new Map();
2264
2322
  const turnLastEventAt = /* @__PURE__ */ new Map();
2265
2323
  const turnStartedAt = /* @__PURE__ */ new Map();
@@ -2419,6 +2477,12 @@ async function parseCodexSessionFile(filePath, options) {
2419
2477
  }
2420
2478
  const usage = tokenUsageFromPayload(payload);
2421
2479
  if (usage) {
2480
+ if (skipReplay) {
2481
+ if (ts.slice(0, 19) === replaySecond) {
2482
+ break;
2483
+ }
2484
+ skipReplay = false;
2485
+ }
2422
2486
  const usageKey = [
2423
2487
  usage.tokensInput,
2424
2488
  usage.tokensCachedInput,
@@ -2699,6 +2763,38 @@ function headlessCodexTimestamp(raw) {
2699
2763
  }
2700
2764
  return void 0;
2701
2765
  }
2766
+ function detectSubagentReplaySecond(text, lines) {
2767
+ if (!text.includes("thread_spawn")) {
2768
+ return void 0;
2769
+ }
2770
+ let firstSecond;
2771
+ for (const line of lines) {
2772
+ const raw = parseJsonLine(line);
2773
+ if (!raw || stringField(raw, "type") !== "event_msg") {
2774
+ continue;
2775
+ }
2776
+ const payload = objectField(raw, "payload");
2777
+ if (stringField(payload, "type") !== "token_count") {
2778
+ continue;
2779
+ }
2780
+ const info = objectField(payload, "info");
2781
+ const hasUsage = Object.keys(objectField(info, "last_token_usage")).length > 0 || Object.keys(objectField(info, "total_token_usage")).length > 0;
2782
+ if (!hasUsage) {
2783
+ continue;
2784
+ }
2785
+ const ts = timestampFrom(raw.timestamp) || timestampFrom(payload.timestamp);
2786
+ if (!ts) {
2787
+ continue;
2788
+ }
2789
+ const second = ts.slice(0, 19);
2790
+ if (firstSecond === void 0) {
2791
+ firstSecond = second;
2792
+ } else {
2793
+ return firstSecond === second ? firstSecond : void 0;
2794
+ }
2795
+ }
2796
+ return void 0;
2797
+ }
2702
2798
  function tokenUsageFromPayload(payload) {
2703
2799
  const info = objectField(payload, "info");
2704
2800
  const usage = objectField(info, "last_token_usage");
@@ -4516,7 +4612,7 @@ async function installEntry(entry, options) {
4516
4612
  });
4517
4613
  }
4518
4614
  async function mergeHooksJson(filePath, content, { dryRun, force, onWrite }) {
4519
- const { mkdir: mkdir4, writeFile: writeFile3 } = await import("node:fs/promises");
4615
+ const { mkdir: mkdir3, writeFile: writeFile2 } = await import("node:fs/promises");
4520
4616
  const pathMod = await import("node:path");
4521
4617
  if (dryRun) {
4522
4618
  onWrite(`Would merge ${filePath}`);
@@ -4537,8 +4633,8 @@ async function mergeHooksJson(filePath, content, { dryRun, force, onWrite }) {
4537
4633
  onWrite(`Already installed ${filePath}`);
4538
4634
  return;
4539
4635
  }
4540
- await mkdir4(pathMod.dirname(filePath), { recursive: true });
4541
- await writeFile3(filePath, nextText, "utf8");
4636
+ await mkdir3(pathMod.dirname(filePath), { recursive: true });
4637
+ await writeFile2(filePath, nextText, "utf8");
4542
4638
  onWrite(`Installed ${filePath}`);
4543
4639
  }
4544
4640
  function mergeHookObjects(existing, addition) {
@@ -4576,7 +4672,7 @@ function hookCommandFromGroup(group) {
4576
4672
 
4577
4673
  // src/lib/config.ts
4578
4674
  import { randomUUID } from "node:crypto";
4579
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4675
+ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeSync } from "node:fs";
4580
4676
  import { homedir, hostname } from "node:os";
4581
4677
  import path11 from "node:path";
4582
4678
  function configDir(home = homedir()) {
@@ -4600,30 +4696,87 @@ function readConfig(home = homedir()) {
4600
4696
  return {};
4601
4697
  }
4602
4698
  }
4603
- function writeConfig(config, home = homedir()) {
4604
- const dir = configDir(home);
4605
- if (!existsSync(dir)) {
4606
- mkdirSync(dir, { recursive: true });
4699
+ var atomicTmpCounter = 0;
4700
+ function writeFileAtomicSync(filePath, data, mode) {
4701
+ const dir = path11.dirname(filePath);
4702
+ mkdirSync(dir, { recursive: true });
4703
+ const tmp = path11.join(dir, `.${path11.basename(filePath)}.${process.pid}.${Date.now()}.${(atomicTmpCounter++).toString(36)}.tmp`);
4704
+ let fd;
4705
+ try {
4706
+ fd = openSync(tmp, "wx", mode);
4707
+ writeSync(fd, data);
4708
+ closeSync(fd);
4709
+ fd = void 0;
4710
+ chmodSync(tmp, mode);
4711
+ renameSyncWithRetry(tmp, filePath);
4712
+ } catch (error) {
4713
+ if (fd !== void 0) {
4714
+ try {
4715
+ closeSync(fd);
4716
+ } catch {
4717
+ }
4718
+ }
4719
+ try {
4720
+ unlinkSync(tmp);
4721
+ } catch {
4722
+ }
4723
+ throw error;
4724
+ }
4725
+ }
4726
+ function renameSyncWithRetry(from, to, attempts = 5) {
4727
+ for (let i = 0; ; i++) {
4728
+ try {
4729
+ renameSync(from, to);
4730
+ return;
4731
+ } catch (error) {
4732
+ const code = error.code;
4733
+ if (i >= attempts - 1 || code !== "EPERM" && code !== "EACCES" && code !== "EBUSY") {
4734
+ throw error;
4735
+ }
4736
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10 * (i + 1));
4737
+ }
4607
4738
  }
4608
- writeFileSync(configPath(home), `${JSON.stringify(config, null, 2)}
4609
- `, { mode: 384 });
4739
+ }
4740
+ function writeConfig(config, home = homedir()) {
4741
+ writeFileAtomicSync(configPath(home), `${JSON.stringify(config, null, 2)}
4742
+ `, 384);
4610
4743
  }
4611
4744
  function ensureLocalMachineId(home = homedir()) {
4612
4745
  const file = machineIdPath(home);
4613
- if (existsSync(file)) {
4614
- const value = readFileSync(file, "utf8").trim();
4615
- if (value.length > 0) {
4616
- return value;
4617
- }
4746
+ const existing = readMachineId(file);
4747
+ if (existing) {
4748
+ return existing;
4618
4749
  }
4750
+ mkdirSync(configDir(home), { recursive: true });
4619
4751
  const id = randomUUID();
4620
- const dir = configDir(home);
4621
- if (!existsSync(dir)) {
4622
- mkdirSync(dir, { recursive: true });
4752
+ try {
4753
+ const fd = openSync(file, "wx", 384);
4754
+ try {
4755
+ writeSync(fd, `${id}
4756
+ `);
4757
+ } finally {
4758
+ closeSync(fd);
4759
+ }
4760
+ return id;
4761
+ } catch (error) {
4762
+ if (error.code !== "EEXIST") {
4763
+ throw error;
4764
+ }
4765
+ const peer = readMachineId(file);
4766
+ if (peer) {
4767
+ return peer;
4768
+ }
4769
+ writeFileAtomicSync(file, `${id}
4770
+ `, 384);
4771
+ return id;
4623
4772
  }
4624
- writeFileSync(file, `${id}
4625
- `, { mode: 384 });
4626
- return id;
4773
+ }
4774
+ function readMachineId(file) {
4775
+ if (!existsSync(file)) {
4776
+ return null;
4777
+ }
4778
+ const value = readFileSync(file, "utf8").trim();
4779
+ return value.length > 0 ? value : null;
4627
4780
  }
4628
4781
  function defaultMachineName() {
4629
4782
  return hostname();
@@ -4633,7 +4786,7 @@ function defaultMachineName() {
4633
4786
  init_fs();
4634
4787
 
4635
4788
  // src/lib/logger.ts
4636
- import { appendFile, mkdir as mkdir2, rename, stat as stat5 } from "node:fs/promises";
4789
+ import { appendFile, mkdir as mkdir2, rename as rename2, stat as stat5 } from "node:fs/promises";
4637
4790
  import { homedir as homedir2 } from "node:os";
4638
4791
  import path12 from "node:path";
4639
4792
  var MAX_BYTES = 1 * 1024 * 1024;
@@ -4653,7 +4806,7 @@ async function rotateIfNeeded(file) {
4653
4806
  try {
4654
4807
  const info = await stat5(file);
4655
4808
  if (info.size > MAX_BYTES) {
4656
- await rename(file, `${file}.1`).catch(() => {
4809
+ await rename2(file, `${file}.1`).catch(() => {
4657
4810
  });
4658
4811
  }
4659
4812
  } catch {
@@ -5487,7 +5640,7 @@ async function importBackfillPlan(plan, options, ctx, registry) {
5487
5640
  }
5488
5641
  async function purgeForcedSources(sourceDefs, home, options, ctx) {
5489
5642
  try {
5490
- await rm(backfillIncrementalStatePath(home), { force: true });
5643
+ await rm2(backfillIncrementalStatePath(home), { force: true });
5491
5644
  } catch (error) {
5492
5645
  debug(ctx, `Failed to clear backfill watermark: ${error.message}
5493
5646
  `);
@@ -5685,7 +5838,11 @@ function syncLocalTriggerLockPath(home) {
5685
5838
  }
5686
5839
  async function readBackfillIncrementalState(home, ctx) {
5687
5840
  const statePath = backfillIncrementalStatePath(home);
5688
- const state = await readJsonIfExists(statePath);
5841
+ const state = await readJsonIfExistsTolerant(
5842
+ statePath,
5843
+ ctx ? (error) => debug(ctx, `backfill-state unreadable at ${statePath}; dropping watermarks and re-importing (${error.message})
5844
+ `) : void 0
5845
+ );
5689
5846
  if (state === null) {
5690
5847
  return { version: BACKFILL_STATE_SCHEMA_VERSION, sources: {} };
5691
5848
  }
@@ -5721,12 +5878,11 @@ async function updateBackfillIncrementalState(home, state, selectedFilesBySource
5721
5878
  state.sources[source] = { watermarkTs: latest };
5722
5879
  }
5723
5880
  const statePath = backfillIncrementalStatePath(home);
5724
- await mkdir3(path13.dirname(statePath), { recursive: true });
5725
- await writeFile2(statePath, `${JSON.stringify(state, null, 2)}
5726
- `, "utf8");
5881
+ await writeFileAtomic(statePath, `${JSON.stringify(state, null, 2)}
5882
+ `);
5727
5883
  }
5728
5884
  async function readSyncLocalTriggerState(statePath) {
5729
- const state = await readJsonIfExists(statePath);
5885
+ const state = await readJsonIfExistsTolerant(statePath);
5730
5886
  if (!isPlainObject(state)) {
5731
5887
  return { version: 1 };
5732
5888
  }
@@ -5749,12 +5905,11 @@ async function readSyncLocalTriggerState(statePath) {
5749
5905
  return nextState;
5750
5906
  }
5751
5907
  async function writeSyncLocalTriggerState(statePath, state) {
5752
- await mkdir3(path13.dirname(statePath), { recursive: true });
5753
- await writeFile2(statePath, `${JSON.stringify(state, null, 2)}
5754
- `, "utf8");
5908
+ await writeFileAtomic(statePath, `${JSON.stringify(state, null, 2)}
5909
+ `);
5755
5910
  }
5756
5911
  async function readSyncLocalLock(lockPath) {
5757
- const lock = await readJsonIfExists(lockPath);
5912
+ const lock = await readJsonIfExistsTolerant(lockPath);
5758
5913
  if (!isPlainObject(lock)) {
5759
5914
  return void 0;
5760
5915
  }
@@ -5764,13 +5919,12 @@ async function readSyncLocalLock(lockPath) {
5764
5919
  return { pid: lock.pid, startedAt: lock.startedAt };
5765
5920
  }
5766
5921
  async function writeSyncLocalLock(lockPath, lock) {
5767
- await mkdir3(path13.dirname(lockPath), { recursive: true });
5768
- await writeFile2(lockPath, `${JSON.stringify(lock, null, 2)}
5769
- `, "utf8");
5922
+ await writeFileAtomic(lockPath, `${JSON.stringify(lock, null, 2)}
5923
+ `);
5770
5924
  }
5771
5925
  async function clearSyncLocalLock(lockPath) {
5772
5926
  try {
5773
- await rm(lockPath, { force: true });
5927
+ await rm2(lockPath, { force: true });
5774
5928
  } catch (error) {
5775
5929
  if (error.code !== "ENOENT") {
5776
5930
  throw error;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codetime-cli",
3
3
  "type": "module",
4
- "version": "0.7.0",
4
+ "version": "0.7.2",
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": {