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.
- package/bin/codetime.mjs +194 -40
- 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 {
|
|
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 {
|
|
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.
|
|
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:
|
|
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
|
|
4541
|
-
await
|
|
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,
|
|
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
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
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
|
-
|
|
4609
|
-
|
|
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
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
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
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
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
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5725
|
-
|
|
5726
|
-
`, "utf8");
|
|
5881
|
+
await writeFileAtomic(statePath, `${JSON.stringify(state, null, 2)}
|
|
5882
|
+
`);
|
|
5727
5883
|
}
|
|
5728
5884
|
async function readSyncLocalTriggerState(statePath) {
|
|
5729
|
-
const state = await
|
|
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
|
|
5753
|
-
|
|
5754
|
-
`, "utf8");
|
|
5908
|
+
await writeFileAtomic(statePath, `${JSON.stringify(state, null, 2)}
|
|
5909
|
+
`);
|
|
5755
5910
|
}
|
|
5756
5911
|
async function readSyncLocalLock(lockPath) {
|
|
5757
|
-
const lock = await
|
|
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
|
|
5768
|
-
|
|
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
|
|
5927
|
+
await rm2(lockPath, { force: true });
|
|
5774
5928
|
} catch (error) {
|
|
5775
5929
|
if (error.code !== "ENOENT") {
|
|
5776
5930
|
throw error;
|
package/package.json
CHANGED