codetime-cli 0.3.3 → 0.4.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.
- package/bin/codetime.mjs +454 -57
- 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
|
|
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
|
|
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.
|
|
976
|
+
var PACKAGE_VERSION = true ? "0.4.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
|
|
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
|
|
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
|
|
2027
|
+
return arrayField2(message, "content").filter((item) => isPlainObject(item) && item.type === "tool_use");
|
|
2021
2028
|
}
|
|
2022
2029
|
function claudeToolResultItems(message) {
|
|
2023
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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 ?
|
|
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 ?
|
|
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
|
|
3459
|
+
return path9.resolve(override);
|
|
3067
3460
|
}
|
|
3068
3461
|
const xdgConfig = env?.XDG_CONFIG_HOME;
|
|
3069
3462
|
if (xdgConfig && xdgConfig.trim()) {
|
|
3070
|
-
return
|
|
3463
|
+
return path9.join(path9.resolve(xdgConfig), "opencode");
|
|
3071
3464
|
}
|
|
3072
|
-
return
|
|
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() ?
|
|
3077
|
-
return [primary,
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
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
|
|
3181
|
-
import
|
|
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
|
|
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 ?
|
|
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
|
|
4003
|
+
return path10.resolve(override);
|
|
3611
4004
|
}
|
|
3612
|
-
return
|
|
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
|
|
4010
|
+
return path10.resolve(override);
|
|
3618
4011
|
}
|
|
3619
|
-
return
|
|
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
|
|
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(
|
|
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:
|
|
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
|
|
4470
|
+
import path11 from "node:path";
|
|
4078
4471
|
function configDir(home = homedir()) {
|
|
4079
|
-
return
|
|
4472
|
+
return path11.join(home, ".codetime");
|
|
4080
4473
|
}
|
|
4081
4474
|
function configPath(home = homedir()) {
|
|
4082
|
-
return
|
|
4475
|
+
return path11.join(configDir(home), "config.json");
|
|
4083
4476
|
}
|
|
4084
4477
|
function machineIdPath(home = homedir()) {
|
|
4085
|
-
return
|
|
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
|
|
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
|
|
4527
|
+
import path12 from "node:path";
|
|
4135
4528
|
var MAX_BYTES = 1 * 1024 * 1024;
|
|
4136
4529
|
function logDir(home = homedir2()) {
|
|
4137
|
-
return
|
|
4530
|
+
return path12.join(home, ".codetime", "logs");
|
|
4138
4531
|
}
|
|
4139
4532
|
function logPath(home = homedir2(), name = "cli.log") {
|
|
4140
|
-
return
|
|
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
|
|
4543
|
+
const info = await stat5(file);
|
|
4151
4544
|
if (info.size > MAX_BYTES) {
|
|
4152
4545
|
await rename(file, `${file}.1`).catch(() => {
|
|
4153
4546
|
});
|
|
@@ -4312,8 +4705,8 @@ function buildHeaders(token, machine) {
|
|
|
4312
4705
|
...machine?.platform ? { "x-machine-platform": machine.platform } : {}
|
|
4313
4706
|
};
|
|
4314
4707
|
}
|
|
4315
|
-
function joinUrl(base,
|
|
4316
|
-
return new URL(
|
|
4708
|
+
function joinUrl(base, path14) {
|
|
4709
|
+
return new URL(path14, base.endsWith("/") ? base : `${base}/`).toString();
|
|
4317
4710
|
}
|
|
4318
4711
|
async function postRollupBatch(remote, rollups, options = {}) {
|
|
4319
4712
|
const response = await remote.fetchImpl(joinUrl(remote.baseUrl, "/v3/agent/ingest"), {
|
|
@@ -4388,6 +4781,7 @@ function createRegistry() {
|
|
|
4388
4781
|
registry.register(createPiAdapter());
|
|
4389
4782
|
registry.register(createOpenCodeAdapter());
|
|
4390
4783
|
registry.register(createAmpAdapter());
|
|
4784
|
+
registry.register(createGeminiAdapter());
|
|
4391
4785
|
return registry;
|
|
4392
4786
|
}
|
|
4393
4787
|
var defaultContext = {
|
|
@@ -4648,7 +5042,7 @@ async function backfillCommand(options, ctx, registry) {
|
|
|
4648
5042
|
}
|
|
4649
5043
|
if (action === "import" && !options["dry-run"]) {
|
|
4650
5044
|
const requested = normalizeBackfillSource(stringOption(options.source) || "all");
|
|
4651
|
-
const supported = /* @__PURE__ */ new Set(["all",
|
|
5045
|
+
const supported = /* @__PURE__ */ new Set(["all", ...BACKFILL_SOURCE_IDS]);
|
|
4652
5046
|
if (!supported.has(requested)) {
|
|
4653
5047
|
write(ctx.stderr, `Unsupported backfill source: ${requested}
|
|
4654
5048
|
`);
|
|
@@ -4793,11 +5187,14 @@ async function listBackfillSourceFiles(source, options, ctx) {
|
|
|
4793
5187
|
if (source.id === "amp") {
|
|
4794
5188
|
return ampBackfillFiles(stringOption(options["source-root"]), resolveHome(options, ctx), ctx.env);
|
|
4795
5189
|
}
|
|
5190
|
+
if (source.id === "gemini") {
|
|
5191
|
+
return geminiBackfillFiles(stringOption(options["source-root"]), resolveHome(options, ctx), ctx.env);
|
|
5192
|
+
}
|
|
4796
5193
|
const roots = stringOption(options["source-root"]) ? [requiredOption(options, "source-root")] : source.paths;
|
|
4797
5194
|
const fileLists = await Promise.all(roots.map((r) => listJsonlFiles(r)));
|
|
4798
5195
|
const files = fileLists.flat().sort().slice(0, numberOption(options.limit) || void 0);
|
|
4799
5196
|
return Promise.all(files.map(async (filePath) => {
|
|
4800
|
-
const info = await
|
|
5197
|
+
const info = await stat6(filePath);
|
|
4801
5198
|
return { path: filePath, modifiedAt: info.mtime.toISOString() };
|
|
4802
5199
|
}));
|
|
4803
5200
|
}
|
|
@@ -4873,7 +5270,7 @@ function writeBackfillPlan(plan, options, ctx) {
|
|
|
4873
5270
|
}
|
|
4874
5271
|
async function importBackfillPlan(plan, options, ctx, registry) {
|
|
4875
5272
|
const source = normalizeBackfillSource(stringOption(options.source) || "all");
|
|
4876
|
-
const supportedSources =
|
|
5273
|
+
const supportedSources = new Set(BACKFILL_SOURCE_IDS);
|
|
4877
5274
|
const home = resolveHome(options, ctx);
|
|
4878
5275
|
if (source !== "all" && !supportedSources.has(source)) {
|
|
4879
5276
|
write(ctx.stderr, `Unsupported backfill source: ${source}
|
|
@@ -5106,13 +5503,13 @@ function selectBackfillFilesForImport(files, watermarkTs) {
|
|
|
5106
5503
|
});
|
|
5107
5504
|
}
|
|
5108
5505
|
function backfillIncrementalStatePath(home) {
|
|
5109
|
-
return
|
|
5506
|
+
return path13.join(home, ".codetime", "backfill-state.json");
|
|
5110
5507
|
}
|
|
5111
5508
|
function syncLocalTriggerStatePath(home) {
|
|
5112
|
-
return
|
|
5509
|
+
return path13.join(home, ".codetime", "sync-local-trigger.json");
|
|
5113
5510
|
}
|
|
5114
5511
|
function syncLocalTriggerLockPath(home) {
|
|
5115
|
-
return
|
|
5512
|
+
return path13.join(home, ".codetime", "sync-local-trigger.lock");
|
|
5116
5513
|
}
|
|
5117
5514
|
async function readBackfillIncrementalState(home, ctx) {
|
|
5118
5515
|
const statePath = backfillIncrementalStatePath(home);
|
|
@@ -5135,7 +5532,7 @@ async function readBackfillIncrementalState(home, ctx) {
|
|
|
5135
5532
|
return { version: BACKFILL_STATE_SCHEMA_VERSION, sources: {} };
|
|
5136
5533
|
}
|
|
5137
5534
|
const sources = {};
|
|
5138
|
-
for (const source of
|
|
5535
|
+
for (const source of BACKFILL_SOURCE_IDS) {
|
|
5139
5536
|
const item = state.sources[source];
|
|
5140
5537
|
if (isPlainObject(item) && typeof item.watermarkTs === "string" && !Number.isNaN(Date.parse(item.watermarkTs))) {
|
|
5141
5538
|
sources[source] = { watermarkTs: item.watermarkTs };
|
|
@@ -5152,7 +5549,7 @@ async function updateBackfillIncrementalState(home, state, selectedFilesBySource
|
|
|
5152
5549
|
state.sources[source] = { watermarkTs: latest };
|
|
5153
5550
|
}
|
|
5154
5551
|
const statePath = backfillIncrementalStatePath(home);
|
|
5155
|
-
await mkdir3(
|
|
5552
|
+
await mkdir3(path13.dirname(statePath), { recursive: true });
|
|
5156
5553
|
await writeFile2(statePath, `${JSON.stringify(state, null, 2)}
|
|
5157
5554
|
`, "utf8");
|
|
5158
5555
|
}
|
|
@@ -5180,7 +5577,7 @@ async function readSyncLocalTriggerState(statePath) {
|
|
|
5180
5577
|
return nextState;
|
|
5181
5578
|
}
|
|
5182
5579
|
async function writeSyncLocalTriggerState(statePath, state) {
|
|
5183
|
-
await mkdir3(
|
|
5580
|
+
await mkdir3(path13.dirname(statePath), { recursive: true });
|
|
5184
5581
|
await writeFile2(statePath, `${JSON.stringify(state, null, 2)}
|
|
5185
5582
|
`, "utf8");
|
|
5186
5583
|
}
|
|
@@ -5195,7 +5592,7 @@ async function readSyncLocalLock(lockPath) {
|
|
|
5195
5592
|
return { pid: lock.pid, startedAt: lock.startedAt };
|
|
5196
5593
|
}
|
|
5197
5594
|
async function writeSyncLocalLock(lockPath, lock) {
|
|
5198
|
-
await mkdir3(
|
|
5595
|
+
await mkdir3(path13.dirname(lockPath), { recursive: true });
|
|
5199
5596
|
await writeFile2(lockPath, `${JSON.stringify(lock, null, 2)}
|
|
5200
5597
|
`, "utf8");
|
|
5201
5598
|
}
|
|
@@ -5255,10 +5652,10 @@ function syncLocalRunnerEntryArgs(cliPath) {
|
|
|
5255
5652
|
if (cliPath.endsWith(".ts")) {
|
|
5256
5653
|
return ["--import", "tsx", cliPath];
|
|
5257
5654
|
}
|
|
5258
|
-
return [
|
|
5655
|
+
return [path13.resolve(path13.dirname(cliPath), "../bin/codetime.mjs")];
|
|
5259
5656
|
}
|
|
5260
5657
|
function resolveHome(options, ctx) {
|
|
5261
|
-
return
|
|
5658
|
+
return path13.resolve(stringOption(options.home) || ctx.env.HOME || os3.homedir());
|
|
5262
5659
|
}
|
|
5263
5660
|
function requestedTargets(options) {
|
|
5264
5661
|
const value = options.target || options.targets;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codetime-cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.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.
|
|
38
|
+
"@codetime/shared": "0.4.0"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "tsc -p tsconfig.json",
|