codetime-cli 0.3.2 → 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 +465 -96
- 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;
|
|
@@ -2223,12 +2230,13 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2223
2230
|
const text = await readFile4(filePath, "utf8");
|
|
2224
2231
|
const lines = text.split("\n").filter(Boolean);
|
|
2225
2232
|
const sourcePathHash = `sha256:${createStableHash(filePath)}`;
|
|
2226
|
-
const serviceTier = await resolveCodexServiceTier(filePath);
|
|
2227
2233
|
const events = [];
|
|
2228
2234
|
let sessionId = sessionIdFromFilePath(filePath, "codex");
|
|
2229
2235
|
let cwd;
|
|
2230
2236
|
let project;
|
|
2231
2237
|
let model;
|
|
2238
|
+
let sessionStartEmitted = false;
|
|
2239
|
+
let serviceTier;
|
|
2232
2240
|
let currentTurnId;
|
|
2233
2241
|
let lastTurnIdForComplete;
|
|
2234
2242
|
let turnIdAtLastUserMessage;
|
|
@@ -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") {
|
|
@@ -2274,6 +2283,40 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2274
2283
|
cwd = stringField(payload, "cwd") || cwd;
|
|
2275
2284
|
project = cwd ? path7.basename(cwd) : project;
|
|
2276
2285
|
model = stringField(payload, "model") || model;
|
|
2286
|
+
const tier = stringField(payload, "service_tier");
|
|
2287
|
+
if (tier) {
|
|
2288
|
+
serviceTier = tier.toLowerCase();
|
|
2289
|
+
}
|
|
2290
|
+
continue;
|
|
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
|
+
}
|
|
2277
2320
|
continue;
|
|
2278
2321
|
}
|
|
2279
2322
|
if (topType !== "event_msg" && topType !== "response_item") {
|
|
@@ -2333,6 +2376,11 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2333
2376
|
break;
|
|
2334
2377
|
}
|
|
2335
2378
|
case "token_count": {
|
|
2379
|
+
const info = objectField(payload, "info");
|
|
2380
|
+
const tierFromInfo = stringField(info, "service_tier") || stringField(payload, "service_tier");
|
|
2381
|
+
if (tierFromInfo) {
|
|
2382
|
+
serviceTier = tierFromInfo.toLowerCase();
|
|
2383
|
+
}
|
|
2336
2384
|
const usage = tokenUsageFromPayload(payload);
|
|
2337
2385
|
if (usage) {
|
|
2338
2386
|
const usageKey = [
|
|
@@ -2545,43 +2593,6 @@ async function parseCodexSessionFile(filePath, options) {
|
|
|
2545
2593
|
}
|
|
2546
2594
|
return events.filter((event) => validateCanonicalEvent(event).valid);
|
|
2547
2595
|
}
|
|
2548
|
-
var codexServiceTierCache = /* @__PURE__ */ new Map();
|
|
2549
|
-
async function resolveCodexServiceTier(sessionFilePath) {
|
|
2550
|
-
const home = inferCodexHomeFromSessionPath(sessionFilePath);
|
|
2551
|
-
if (!home) {
|
|
2552
|
-
return void 0;
|
|
2553
|
-
}
|
|
2554
|
-
if (!codexServiceTierCache.has(home)) {
|
|
2555
|
-
codexServiceTierCache.set(home, await readCodexServiceTier(home));
|
|
2556
|
-
}
|
|
2557
|
-
return codexServiceTierCache.get(home) ?? void 0;
|
|
2558
|
-
}
|
|
2559
|
-
function inferCodexHomeFromSessionPath(sessionFilePath) {
|
|
2560
|
-
if (path7.basename(sessionFilePath) === "history.jsonl") {
|
|
2561
|
-
return path7.dirname(sessionFilePath);
|
|
2562
|
-
}
|
|
2563
|
-
let dir = path7.dirname(sessionFilePath);
|
|
2564
|
-
for (let depth = 0; depth < 10; depth += 1) {
|
|
2565
|
-
const parent = path7.dirname(dir);
|
|
2566
|
-
if (parent === dir) {
|
|
2567
|
-
return void 0;
|
|
2568
|
-
}
|
|
2569
|
-
if (path7.basename(dir) === "sessions") {
|
|
2570
|
-
return parent;
|
|
2571
|
-
}
|
|
2572
|
-
dir = parent;
|
|
2573
|
-
}
|
|
2574
|
-
return void 0;
|
|
2575
|
-
}
|
|
2576
|
-
async function readCodexServiceTier(codexHomePath) {
|
|
2577
|
-
try {
|
|
2578
|
-
const text = await readFile4(path7.join(codexHomePath, "config.toml"), "utf8");
|
|
2579
|
-
const match = text.match(/(?:^|\n)\s*service_tier\s*=\s*["']?([a-z_]+)["']?/i);
|
|
2580
|
-
return match ? match[1].toLowerCase() : null;
|
|
2581
|
-
} catch {
|
|
2582
|
-
return null;
|
|
2583
|
-
}
|
|
2584
|
-
}
|
|
2585
2596
|
function rewriteCodexModelForTier(model, serviceTier) {
|
|
2586
2597
|
if (!model) {
|
|
2587
2598
|
return model;
|
|
@@ -2598,6 +2609,55 @@ function baseCodexEvent(event) {
|
|
|
2598
2609
|
...event
|
|
2599
2610
|
};
|
|
2600
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
|
+
}
|
|
2601
2661
|
function tokenUsageFromPayload(payload) {
|
|
2602
2662
|
const info = objectField(payload, "info");
|
|
2603
2663
|
const usage = objectField(info, "last_token_usage");
|
|
@@ -2628,7 +2688,7 @@ function fileActivitiesFromFunctionCall(payload, payloadType, ts, cwd) {
|
|
|
2628
2688
|
for (const key of ["file_path", "path", "notebook_path"]) {
|
|
2629
2689
|
addResolvedPathActivity(changes, stringField(args, key), operation, ts, cwd, workdir);
|
|
2630
2690
|
}
|
|
2631
|
-
for (const file of
|
|
2691
|
+
for (const file of arrayField3(args, "files")) {
|
|
2632
2692
|
if (typeof file === "string") {
|
|
2633
2693
|
addResolvedPathActivity(changes, file, operation, ts, cwd, workdir);
|
|
2634
2694
|
}
|
|
@@ -2641,7 +2701,7 @@ function fileActivitiesFromFunctionCall(payload, payloadType, ts, cwd) {
|
|
|
2641
2701
|
}
|
|
2642
2702
|
return [...changes.values()];
|
|
2643
2703
|
}
|
|
2644
|
-
function
|
|
2704
|
+
function arrayField3(object, key) {
|
|
2645
2705
|
if (!isPlainObject(object) || !Array.isArray(object[key])) {
|
|
2646
2706
|
return [];
|
|
2647
2707
|
}
|
|
@@ -2726,9 +2786,314 @@ function createCodexAdapter() {
|
|
|
2726
2786
|
};
|
|
2727
2787
|
}
|
|
2728
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
|
+
|
|
2729
3094
|
// src/adapters/opencode.ts
|
|
2730
3095
|
import os2 from "node:os";
|
|
2731
|
-
import
|
|
3096
|
+
import path9 from "node:path";
|
|
2732
3097
|
async function parseOpenCodeSessionFile(dbPath, options) {
|
|
2733
3098
|
const { DatabaseSync } = await import("node:sqlite");
|
|
2734
3099
|
if (!dbPath.endsWith(".db")) {
|
|
@@ -2759,7 +3124,7 @@ async function parseOpenCodeSessionFile(dbPath, options) {
|
|
|
2759
3124
|
for (const session of sessions) {
|
|
2760
3125
|
const sessionId = session.id;
|
|
2761
3126
|
const cwd = session.directory || session.path || void 0;
|
|
2762
|
-
const project = cwd ?
|
|
3127
|
+
const project = cwd ? path9.basename(cwd) : void 0;
|
|
2763
3128
|
const sessionTs = msToIso(session.time_created);
|
|
2764
3129
|
events.push(baseOpenCodeEvent({
|
|
2765
3130
|
ts: sessionTs,
|
|
@@ -2849,7 +3214,7 @@ async function parseOpenCodeSessionFile(dbPath, options) {
|
|
|
2849
3214
|
const assistantAgent = stringField(info, "agent") || "opencode";
|
|
2850
3215
|
const pathObj = objectField(info, "path");
|
|
2851
3216
|
const assistantCwd = stringField(pathObj, "cwd") || cwd;
|
|
2852
|
-
const assistantProject = assistantCwd ?
|
|
3217
|
+
const assistantProject = assistantCwd ? path9.basename(assistantCwd) : project;
|
|
2853
3218
|
const completedTs = numberField(objectField(info, "time"), "completed");
|
|
2854
3219
|
const createdTs = timeCreated;
|
|
2855
3220
|
const tokens = opencodeUsageFromInfo(info);
|
|
@@ -3091,33 +3456,33 @@ function opencodeUsageFromInfo(info) {
|
|
|
3091
3456
|
function opencodeConfigDir(home, env) {
|
|
3092
3457
|
const override = env?.OPENCODE_CONFIG_DIR;
|
|
3093
3458
|
if (override && override.trim()) {
|
|
3094
|
-
return
|
|
3459
|
+
return path9.resolve(override);
|
|
3095
3460
|
}
|
|
3096
3461
|
const xdgConfig = env?.XDG_CONFIG_HOME;
|
|
3097
3462
|
if (xdgConfig && xdgConfig.trim()) {
|
|
3098
|
-
return
|
|
3463
|
+
return path9.join(path9.resolve(xdgConfig), "opencode");
|
|
3099
3464
|
}
|
|
3100
|
-
return
|
|
3465
|
+
return path9.join(home, ".config", "opencode");
|
|
3101
3466
|
}
|
|
3102
3467
|
function opencodeDataCandidates(home, env) {
|
|
3103
3468
|
const xdgData = env?.XDG_DATA_HOME;
|
|
3104
|
-
const primary = xdgData && xdgData.trim() ?
|
|
3105
|
-
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")];
|
|
3106
3471
|
}
|
|
3107
3472
|
async function opencodeBackfillFiles(sourceRoot, home = os2.homedir(), env) {
|
|
3108
|
-
const { stat:
|
|
3473
|
+
const { stat: stat7 } = await import("node:fs/promises");
|
|
3109
3474
|
if (sourceRoot) {
|
|
3110
3475
|
if (!sourceRoot.endsWith(".db")) {
|
|
3111
3476
|
return [];
|
|
3112
3477
|
}
|
|
3113
|
-
const info = await
|
|
3478
|
+
const info = await stat7(sourceRoot).catch(() => null);
|
|
3114
3479
|
if (!info) {
|
|
3115
3480
|
return [];
|
|
3116
3481
|
}
|
|
3117
3482
|
return [{ path: sourceRoot, modifiedAt: info.mtime.toISOString() }];
|
|
3118
3483
|
}
|
|
3119
3484
|
for (const candidatePath of opencodeDataCandidates(home, env)) {
|
|
3120
|
-
const info = await
|
|
3485
|
+
const info = await stat7(candidatePath).catch(() => null);
|
|
3121
3486
|
if (info) {
|
|
3122
3487
|
return [{ path: candidatePath, modifiedAt: info.mtime.toISOString() }];
|
|
3123
3488
|
}
|
|
@@ -3180,12 +3545,12 @@ function createOpenCodeAdapter() {
|
|
|
3180
3545
|
return opencodeConfigDir(home, env);
|
|
3181
3546
|
},
|
|
3182
3547
|
installedPath(home, env) {
|
|
3183
|
-
return
|
|
3548
|
+
return path9.join(opencodeConfigDir(home, env), PLUGIN_PATH);
|
|
3184
3549
|
},
|
|
3185
3550
|
async isInstalled(home, env) {
|
|
3186
3551
|
try {
|
|
3187
3552
|
const { pathExists: pathExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
3188
|
-
return await pathExists2(
|
|
3553
|
+
return await pathExists2(path9.join(opencodeConfigDir(home, env), PLUGIN_PATH)) || await pathExists2(path9.join(".opencode", PLUGIN_PATH));
|
|
3189
3554
|
} catch {
|
|
3190
3555
|
return false;
|
|
3191
3556
|
}
|
|
@@ -3193,7 +3558,7 @@ function createOpenCodeAdapter() {
|
|
|
3193
3558
|
installEntries(home, env) {
|
|
3194
3559
|
return [{
|
|
3195
3560
|
kind: "file",
|
|
3196
|
-
path:
|
|
3561
|
+
path: path9.join(opencodeConfigDir(home, env), PLUGIN_PATH),
|
|
3197
3562
|
content: opencodePluginContent()
|
|
3198
3563
|
}];
|
|
3199
3564
|
},
|
|
@@ -3205,10 +3570,10 @@ function createOpenCodeAdapter() {
|
|
|
3205
3570
|
}
|
|
3206
3571
|
|
|
3207
3572
|
// src/adapters/pi.ts
|
|
3208
|
-
import { readFile as
|
|
3209
|
-
import
|
|
3573
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
3574
|
+
import path10 from "node:path";
|
|
3210
3575
|
async function parsePiSessionFile(filePath, options) {
|
|
3211
|
-
const text = await
|
|
3576
|
+
const text = await readFile6(filePath, "utf8");
|
|
3212
3577
|
const lines = text.split("\n").filter(Boolean);
|
|
3213
3578
|
let sessionId;
|
|
3214
3579
|
let cwd;
|
|
@@ -3237,7 +3602,7 @@ async function parsePiSessionFile(filePath, options) {
|
|
|
3237
3602
|
sessionId = stringField(raw, "id") || state.sessionId;
|
|
3238
3603
|
state.sessionId = sessionId || state.sessionId;
|
|
3239
3604
|
cwd = stringField(raw, "cwd") || cwd;
|
|
3240
|
-
project = cwd ?
|
|
3605
|
+
project = cwd ? path10.basename(cwd) : project;
|
|
3241
3606
|
continue;
|
|
3242
3607
|
}
|
|
3243
3608
|
if (entryType === "model_change") {
|
|
@@ -3635,16 +4000,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
3635
4000
|
function piAgentDir(home, env) {
|
|
3636
4001
|
const override = env?.PI_CODING_AGENT_DIR;
|
|
3637
4002
|
if (override && override.trim()) {
|
|
3638
|
-
return
|
|
4003
|
+
return path10.resolve(override);
|
|
3639
4004
|
}
|
|
3640
|
-
return
|
|
4005
|
+
return path10.join(home, ".pi", "agent");
|
|
3641
4006
|
}
|
|
3642
4007
|
function piSessionDir(home, env) {
|
|
3643
4008
|
const override = env?.PI_CODING_AGENT_SESSION_DIR;
|
|
3644
4009
|
if (override && override.trim()) {
|
|
3645
|
-
return
|
|
4010
|
+
return path10.resolve(override);
|
|
3646
4011
|
}
|
|
3647
|
-
return
|
|
4012
|
+
return path10.join(piAgentDir(home, env), "sessions");
|
|
3648
4013
|
}
|
|
3649
4014
|
function createPiAdapter() {
|
|
3650
4015
|
return {
|
|
@@ -3656,12 +4021,12 @@ function createPiAdapter() {
|
|
|
3656
4021
|
return piAgentDir(home, env);
|
|
3657
4022
|
},
|
|
3658
4023
|
installedPath(home, env) {
|
|
3659
|
-
return
|
|
4024
|
+
return path10.join(piAgentDir(home, env), "extensions", "codetime.ts");
|
|
3660
4025
|
},
|
|
3661
4026
|
async isInstalled(home, env) {
|
|
3662
4027
|
try {
|
|
3663
4028
|
const { pathExists: pathExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
3664
|
-
return await pathExists2(
|
|
4029
|
+
return await pathExists2(path10.join(piAgentDir(home, env), "extensions", "codetime.ts"));
|
|
3665
4030
|
} catch {
|
|
3666
4031
|
return false;
|
|
3667
4032
|
}
|
|
@@ -3669,7 +4034,7 @@ function createPiAdapter() {
|
|
|
3669
4034
|
installEntries(home, env) {
|
|
3670
4035
|
return [{
|
|
3671
4036
|
kind: "file",
|
|
3672
|
-
path:
|
|
4037
|
+
path: path10.join(piAgentDir(home, env), "extensions", "codetime.ts"),
|
|
3673
4038
|
content: piExtensionContent()
|
|
3674
4039
|
}];
|
|
3675
4040
|
},
|
|
@@ -4102,15 +4467,15 @@ function hookCommandFromGroup(group) {
|
|
|
4102
4467
|
import { randomUUID } from "node:crypto";
|
|
4103
4468
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4104
4469
|
import { homedir, hostname } from "node:os";
|
|
4105
|
-
import
|
|
4470
|
+
import path11 from "node:path";
|
|
4106
4471
|
function configDir(home = homedir()) {
|
|
4107
|
-
return
|
|
4472
|
+
return path11.join(home, ".codetime");
|
|
4108
4473
|
}
|
|
4109
4474
|
function configPath(home = homedir()) {
|
|
4110
|
-
return
|
|
4475
|
+
return path11.join(configDir(home), "config.json");
|
|
4111
4476
|
}
|
|
4112
4477
|
function machineIdPath(home = homedir()) {
|
|
4113
|
-
return
|
|
4478
|
+
return path11.join(configDir(home), "machine-id");
|
|
4114
4479
|
}
|
|
4115
4480
|
function readConfig(home = homedir()) {
|
|
4116
4481
|
const file = configPath(home);
|
|
@@ -4157,15 +4522,15 @@ function defaultMachineName() {
|
|
|
4157
4522
|
init_fs();
|
|
4158
4523
|
|
|
4159
4524
|
// src/lib/logger.ts
|
|
4160
|
-
import { appendFile, mkdir as mkdir2, rename, stat as
|
|
4525
|
+
import { appendFile, mkdir as mkdir2, rename, stat as stat5 } from "node:fs/promises";
|
|
4161
4526
|
import { homedir as homedir2 } from "node:os";
|
|
4162
|
-
import
|
|
4527
|
+
import path12 from "node:path";
|
|
4163
4528
|
var MAX_BYTES = 1 * 1024 * 1024;
|
|
4164
4529
|
function logDir(home = homedir2()) {
|
|
4165
|
-
return
|
|
4530
|
+
return path12.join(home, ".codetime", "logs");
|
|
4166
4531
|
}
|
|
4167
4532
|
function logPath(home = homedir2(), name = "cli.log") {
|
|
4168
|
-
return
|
|
4533
|
+
return path12.join(logDir(home), name);
|
|
4169
4534
|
}
|
|
4170
4535
|
function serializeError(error) {
|
|
4171
4536
|
if (error instanceof Error) {
|
|
@@ -4175,7 +4540,7 @@ function serializeError(error) {
|
|
|
4175
4540
|
}
|
|
4176
4541
|
async function rotateIfNeeded(file) {
|
|
4177
4542
|
try {
|
|
4178
|
-
const info = await
|
|
4543
|
+
const info = await stat5(file);
|
|
4179
4544
|
if (info.size > MAX_BYTES) {
|
|
4180
4545
|
await rename(file, `${file}.1`).catch(() => {
|
|
4181
4546
|
});
|
|
@@ -4340,8 +4705,8 @@ function buildHeaders(token, machine) {
|
|
|
4340
4705
|
...machine?.platform ? { "x-machine-platform": machine.platform } : {}
|
|
4341
4706
|
};
|
|
4342
4707
|
}
|
|
4343
|
-
function joinUrl(base,
|
|
4344
|
-
return new URL(
|
|
4708
|
+
function joinUrl(base, path14) {
|
|
4709
|
+
return new URL(path14, base.endsWith("/") ? base : `${base}/`).toString();
|
|
4345
4710
|
}
|
|
4346
4711
|
async function postRollupBatch(remote, rollups, options = {}) {
|
|
4347
4712
|
const response = await remote.fetchImpl(joinUrl(remote.baseUrl, "/v3/agent/ingest"), {
|
|
@@ -4406,7 +4771,7 @@ async function deleteMachine(remote, id) {
|
|
|
4406
4771
|
}
|
|
4407
4772
|
|
|
4408
4773
|
// src/lib/types.ts
|
|
4409
|
-
var BACKFILL_STATE_SCHEMA_VERSION =
|
|
4774
|
+
var BACKFILL_STATE_SCHEMA_VERSION = 4;
|
|
4410
4775
|
|
|
4411
4776
|
// src/cli.ts
|
|
4412
4777
|
function createRegistry() {
|
|
@@ -4416,6 +4781,7 @@ function createRegistry() {
|
|
|
4416
4781
|
registry.register(createPiAdapter());
|
|
4417
4782
|
registry.register(createOpenCodeAdapter());
|
|
4418
4783
|
registry.register(createAmpAdapter());
|
|
4784
|
+
registry.register(createGeminiAdapter());
|
|
4419
4785
|
return registry;
|
|
4420
4786
|
}
|
|
4421
4787
|
var defaultContext = {
|
|
@@ -4676,7 +5042,7 @@ async function backfillCommand(options, ctx, registry) {
|
|
|
4676
5042
|
}
|
|
4677
5043
|
if (action === "import" && !options["dry-run"]) {
|
|
4678
5044
|
const requested = normalizeBackfillSource(stringOption(options.source) || "all");
|
|
4679
|
-
const supported = /* @__PURE__ */ new Set(["all",
|
|
5045
|
+
const supported = /* @__PURE__ */ new Set(["all", ...BACKFILL_SOURCE_IDS]);
|
|
4680
5046
|
if (!supported.has(requested)) {
|
|
4681
5047
|
write(ctx.stderr, `Unsupported backfill source: ${requested}
|
|
4682
5048
|
`);
|
|
@@ -4821,11 +5187,14 @@ async function listBackfillSourceFiles(source, options, ctx) {
|
|
|
4821
5187
|
if (source.id === "amp") {
|
|
4822
5188
|
return ampBackfillFiles(stringOption(options["source-root"]), resolveHome(options, ctx), ctx.env);
|
|
4823
5189
|
}
|
|
5190
|
+
if (source.id === "gemini") {
|
|
5191
|
+
return geminiBackfillFiles(stringOption(options["source-root"]), resolveHome(options, ctx), ctx.env);
|
|
5192
|
+
}
|
|
4824
5193
|
const roots = stringOption(options["source-root"]) ? [requiredOption(options, "source-root")] : source.paths;
|
|
4825
5194
|
const fileLists = await Promise.all(roots.map((r) => listJsonlFiles(r)));
|
|
4826
5195
|
const files = fileLists.flat().sort().slice(0, numberOption(options.limit) || void 0);
|
|
4827
5196
|
return Promise.all(files.map(async (filePath) => {
|
|
4828
|
-
const info = await
|
|
5197
|
+
const info = await stat6(filePath);
|
|
4829
5198
|
return { path: filePath, modifiedAt: info.mtime.toISOString() };
|
|
4830
5199
|
}));
|
|
4831
5200
|
}
|
|
@@ -4901,7 +5270,7 @@ function writeBackfillPlan(plan, options, ctx) {
|
|
|
4901
5270
|
}
|
|
4902
5271
|
async function importBackfillPlan(plan, options, ctx, registry) {
|
|
4903
5272
|
const source = normalizeBackfillSource(stringOption(options.source) || "all");
|
|
4904
|
-
const supportedSources =
|
|
5273
|
+
const supportedSources = new Set(BACKFILL_SOURCE_IDS);
|
|
4905
5274
|
const home = resolveHome(options, ctx);
|
|
4906
5275
|
if (source !== "all" && !supportedSources.has(source)) {
|
|
4907
5276
|
write(ctx.stderr, `Unsupported backfill source: ${source}
|
|
@@ -5134,13 +5503,13 @@ function selectBackfillFilesForImport(files, watermarkTs) {
|
|
|
5134
5503
|
});
|
|
5135
5504
|
}
|
|
5136
5505
|
function backfillIncrementalStatePath(home) {
|
|
5137
|
-
return
|
|
5506
|
+
return path13.join(home, ".codetime", "backfill-state.json");
|
|
5138
5507
|
}
|
|
5139
5508
|
function syncLocalTriggerStatePath(home) {
|
|
5140
|
-
return
|
|
5509
|
+
return path13.join(home, ".codetime", "sync-local-trigger.json");
|
|
5141
5510
|
}
|
|
5142
5511
|
function syncLocalTriggerLockPath(home) {
|
|
5143
|
-
return
|
|
5512
|
+
return path13.join(home, ".codetime", "sync-local-trigger.lock");
|
|
5144
5513
|
}
|
|
5145
5514
|
async function readBackfillIncrementalState(home, ctx) {
|
|
5146
5515
|
const statePath = backfillIncrementalStatePath(home);
|
|
@@ -5163,7 +5532,7 @@ async function readBackfillIncrementalState(home, ctx) {
|
|
|
5163
5532
|
return { version: BACKFILL_STATE_SCHEMA_VERSION, sources: {} };
|
|
5164
5533
|
}
|
|
5165
5534
|
const sources = {};
|
|
5166
|
-
for (const source of
|
|
5535
|
+
for (const source of BACKFILL_SOURCE_IDS) {
|
|
5167
5536
|
const item = state.sources[source];
|
|
5168
5537
|
if (isPlainObject(item) && typeof item.watermarkTs === "string" && !Number.isNaN(Date.parse(item.watermarkTs))) {
|
|
5169
5538
|
sources[source] = { watermarkTs: item.watermarkTs };
|
|
@@ -5180,7 +5549,7 @@ async function updateBackfillIncrementalState(home, state, selectedFilesBySource
|
|
|
5180
5549
|
state.sources[source] = { watermarkTs: latest };
|
|
5181
5550
|
}
|
|
5182
5551
|
const statePath = backfillIncrementalStatePath(home);
|
|
5183
|
-
await mkdir3(
|
|
5552
|
+
await mkdir3(path13.dirname(statePath), { recursive: true });
|
|
5184
5553
|
await writeFile2(statePath, `${JSON.stringify(state, null, 2)}
|
|
5185
5554
|
`, "utf8");
|
|
5186
5555
|
}
|
|
@@ -5208,7 +5577,7 @@ async function readSyncLocalTriggerState(statePath) {
|
|
|
5208
5577
|
return nextState;
|
|
5209
5578
|
}
|
|
5210
5579
|
async function writeSyncLocalTriggerState(statePath, state) {
|
|
5211
|
-
await mkdir3(
|
|
5580
|
+
await mkdir3(path13.dirname(statePath), { recursive: true });
|
|
5212
5581
|
await writeFile2(statePath, `${JSON.stringify(state, null, 2)}
|
|
5213
5582
|
`, "utf8");
|
|
5214
5583
|
}
|
|
@@ -5223,7 +5592,7 @@ async function readSyncLocalLock(lockPath) {
|
|
|
5223
5592
|
return { pid: lock.pid, startedAt: lock.startedAt };
|
|
5224
5593
|
}
|
|
5225
5594
|
async function writeSyncLocalLock(lockPath, lock) {
|
|
5226
|
-
await mkdir3(
|
|
5595
|
+
await mkdir3(path13.dirname(lockPath), { recursive: true });
|
|
5227
5596
|
await writeFile2(lockPath, `${JSON.stringify(lock, null, 2)}
|
|
5228
5597
|
`, "utf8");
|
|
5229
5598
|
}
|
|
@@ -5283,10 +5652,10 @@ function syncLocalRunnerEntryArgs(cliPath) {
|
|
|
5283
5652
|
if (cliPath.endsWith(".ts")) {
|
|
5284
5653
|
return ["--import", "tsx", cliPath];
|
|
5285
5654
|
}
|
|
5286
|
-
return [
|
|
5655
|
+
return [path13.resolve(path13.dirname(cliPath), "../bin/codetime.mjs")];
|
|
5287
5656
|
}
|
|
5288
5657
|
function resolveHome(options, ctx) {
|
|
5289
|
-
return
|
|
5658
|
+
return path13.resolve(stringOption(options.home) || ctx.env.HOME || os3.homedir());
|
|
5290
5659
|
}
|
|
5291
5660
|
function requestedTargets(options) {
|
|
5292
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",
|