codetime-cli 0.2.0 → 0.3.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 +234 -169
- package/package.json +5 -5
package/bin/codetime.mjs
CHANGED
|
@@ -143,6 +143,9 @@ var init_fs = __esm({
|
|
|
143
143
|
}
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
// src/lib/types.ts
|
|
147
|
+
var BACKFILL_STATE_SCHEMA_VERSION = 2;
|
|
148
|
+
|
|
146
149
|
// src/cli.ts
|
|
147
150
|
import { spawn } from "node:child_process";
|
|
148
151
|
import { mkdir as mkdir2, rm, stat as stat4, writeFile as writeFile2 } from "node:fs/promises";
|
|
@@ -1329,7 +1332,7 @@ function msToIso(ms) {
|
|
|
1329
1332
|
}
|
|
1330
1333
|
|
|
1331
1334
|
// src/lib/constants.ts
|
|
1332
|
-
var PACKAGE_VERSION = true ? "0.
|
|
1335
|
+
var PACKAGE_VERSION = true ? "0.3.0" : "0.1.1";
|
|
1333
1336
|
var DEFAULT_API_URL = "https://codetime.dev";
|
|
1334
1337
|
var DEFAULT_BACKFILL_BATCH_SIZE = 50;
|
|
1335
1338
|
var DEFAULT_BACKFILL_BATCH_BYTES = 800 * 1024;
|
|
@@ -1472,6 +1475,7 @@ async function parseClaudeCodeSessionFile(filePath, options) {
|
|
|
1472
1475
|
const lines = text.split("\n").filter(Boolean);
|
|
1473
1476
|
const projectContext = await claudeProjectContextFromLines(filePath, lines, options);
|
|
1474
1477
|
const pendingTools = /* @__PURE__ */ new Map();
|
|
1478
|
+
const seenUsageKeys = /* @__PURE__ */ new Set();
|
|
1475
1479
|
let sessionId = sessionIdFromFilePath(filePath, "claude");
|
|
1476
1480
|
let cwd;
|
|
1477
1481
|
let project = projectContext.project;
|
|
@@ -1623,8 +1627,19 @@ async function parseClaudeCodeSessionFile(filePath, options) {
|
|
|
1623
1627
|
continue;
|
|
1624
1628
|
}
|
|
1625
1629
|
model = stringField(message, "model") || model;
|
|
1630
|
+
const messageId = stringField(message, "id");
|
|
1631
|
+
const requestId = stringField(raw, "requestId");
|
|
1632
|
+
const usageKey = messageId ? `${messageId}:${requestId}` : null;
|
|
1633
|
+
if (usageKey != null && seenUsageKeys.has(usageKey)) {
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
if (usageKey != null) {
|
|
1637
|
+
seenUsageKeys.add(usageKey);
|
|
1638
|
+
}
|
|
1626
1639
|
const usage = claudeUsageFromMessage(message);
|
|
1627
1640
|
if (usage) {
|
|
1641
|
+
const speed = stringField(objectField(message, "usage"), "speed");
|
|
1642
|
+
const usageModel = speed === "fast" && model ? `${model}-fast` : model;
|
|
1628
1643
|
push(baseClaudeEvent({
|
|
1629
1644
|
ts,
|
|
1630
1645
|
type: "model.usage",
|
|
@@ -1632,7 +1647,7 @@ async function parseClaudeCodeSessionFile(filePath, options) {
|
|
|
1632
1647
|
turnId: state.currentTurnId,
|
|
1633
1648
|
cwd,
|
|
1634
1649
|
project,
|
|
1635
|
-
model,
|
|
1650
|
+
model: usageModel,
|
|
1636
1651
|
confidence: "partial",
|
|
1637
1652
|
metrics: usage
|
|
1638
1653
|
}), lineNumber, topType, "usage");
|
|
@@ -1940,35 +1955,43 @@ function hookConfig() {
|
|
|
1940
1955
|
}
|
|
1941
1956
|
};
|
|
1942
1957
|
}
|
|
1958
|
+
function claudeConfigDir(home, env) {
|
|
1959
|
+
const override = env?.CLAUDE_CONFIG_DIR;
|
|
1960
|
+
if (override && override.trim()) {
|
|
1961
|
+
return path5.resolve(override);
|
|
1962
|
+
}
|
|
1963
|
+
return path5.join(home, ".claude");
|
|
1964
|
+
}
|
|
1943
1965
|
function createClaudeCodeAdapter() {
|
|
1944
|
-
const CLAUDE_PATH = ".claude";
|
|
1945
1966
|
return {
|
|
1946
1967
|
id: "claude-code",
|
|
1947
1968
|
label: "Claude Code",
|
|
1948
1969
|
agentName: "claude",
|
|
1949
1970
|
kind: "agent",
|
|
1950
|
-
detectPath(home) {
|
|
1951
|
-
return
|
|
1971
|
+
detectPath(home, env) {
|
|
1972
|
+
return claudeConfigDir(home, env);
|
|
1952
1973
|
},
|
|
1953
|
-
installedPath(home) {
|
|
1954
|
-
return path5.join(home,
|
|
1974
|
+
installedPath(home, env) {
|
|
1975
|
+
return path5.join(claudeConfigDir(home, env), "settings.json");
|
|
1955
1976
|
},
|
|
1956
|
-
async isInstalled(home) {
|
|
1977
|
+
async isInstalled(home, env) {
|
|
1957
1978
|
return isHooksJsonInstalled(
|
|
1958
|
-
path5.join(home,
|
|
1979
|
+
path5.join(claudeConfigDir(home, env), "settings.json"),
|
|
1959
1980
|
"codetime hook --agent claude"
|
|
1960
1981
|
);
|
|
1961
1982
|
},
|
|
1962
|
-
installEntries(home) {
|
|
1983
|
+
installEntries(home, env) {
|
|
1963
1984
|
return [{
|
|
1964
1985
|
kind: "hooks-json",
|
|
1965
|
-
path: path5.join(home,
|
|
1986
|
+
path: path5.join(claudeConfigDir(home, env), "settings.json"),
|
|
1966
1987
|
content: hookConfig()
|
|
1967
1988
|
}];
|
|
1968
1989
|
},
|
|
1969
|
-
sourcePaths(home) {
|
|
1990
|
+
sourcePaths(home, env) {
|
|
1991
|
+
const base = claudeConfigDir(home, env);
|
|
1970
1992
|
return [
|
|
1971
|
-
path5.join(
|
|
1993
|
+
path5.join(base, "projects"),
|
|
1994
|
+
path5.join(base, ".claude.json"),
|
|
1972
1995
|
path5.join(home, ".claude.json")
|
|
1973
1996
|
];
|
|
1974
1997
|
},
|
|
@@ -2473,36 +2496,43 @@ function hookConfig2() {
|
|
|
2473
2496
|
}
|
|
2474
2497
|
};
|
|
2475
2498
|
}
|
|
2499
|
+
function codexHome(home, env) {
|
|
2500
|
+
const override = env?.CODEX_HOME;
|
|
2501
|
+
if (override && override.trim()) {
|
|
2502
|
+
return path6.resolve(override);
|
|
2503
|
+
}
|
|
2504
|
+
return path6.join(home, ".codex");
|
|
2505
|
+
}
|
|
2476
2506
|
function createCodexAdapter() {
|
|
2477
|
-
const CODE_PATH = ".codex";
|
|
2478
2507
|
return {
|
|
2479
2508
|
id: "codex",
|
|
2480
2509
|
label: "Codex",
|
|
2481
2510
|
agentName: "codex",
|
|
2482
2511
|
kind: "agent",
|
|
2483
|
-
detectPath(home) {
|
|
2484
|
-
return
|
|
2512
|
+
detectPath(home, env) {
|
|
2513
|
+
return codexHome(home, env);
|
|
2485
2514
|
},
|
|
2486
|
-
installedPath(home) {
|
|
2487
|
-
return path6.join(home,
|
|
2515
|
+
installedPath(home, env) {
|
|
2516
|
+
return path6.join(codexHome(home, env), "hooks.json");
|
|
2488
2517
|
},
|
|
2489
|
-
async isInstalled(home) {
|
|
2518
|
+
async isInstalled(home, env) {
|
|
2490
2519
|
return isHooksJsonInstalled(
|
|
2491
|
-
path6.join(home,
|
|
2520
|
+
path6.join(codexHome(home, env), "hooks.json"),
|
|
2492
2521
|
"codetime hook --agent codex"
|
|
2493
2522
|
);
|
|
2494
2523
|
},
|
|
2495
|
-
installEntries(home) {
|
|
2524
|
+
installEntries(home, env) {
|
|
2496
2525
|
return [{
|
|
2497
2526
|
kind: "hooks-json",
|
|
2498
|
-
path: path6.join(home,
|
|
2527
|
+
path: path6.join(codexHome(home, env), "hooks.json"),
|
|
2499
2528
|
content: hookConfig2()
|
|
2500
2529
|
}];
|
|
2501
2530
|
},
|
|
2502
|
-
sourcePaths(home) {
|
|
2531
|
+
sourcePaths(home, env) {
|
|
2532
|
+
const base = codexHome(home, env);
|
|
2503
2533
|
return [
|
|
2504
|
-
path6.join(
|
|
2505
|
-
path6.join(
|
|
2534
|
+
path6.join(base, "sessions"),
|
|
2535
|
+
path6.join(base, "history.jsonl")
|
|
2506
2536
|
];
|
|
2507
2537
|
},
|
|
2508
2538
|
parseSessionFile: parseCodexSessionFile
|
|
@@ -2855,7 +2885,23 @@ function opencodeUsageFromInfo(info) {
|
|
|
2855
2885
|
tokensTotal: total
|
|
2856
2886
|
};
|
|
2857
2887
|
}
|
|
2858
|
-
|
|
2888
|
+
function opencodeConfigDir(home, env) {
|
|
2889
|
+
const override = env?.OPENCODE_CONFIG_DIR;
|
|
2890
|
+
if (override && override.trim()) {
|
|
2891
|
+
return path7.resolve(override);
|
|
2892
|
+
}
|
|
2893
|
+
const xdgConfig = env?.XDG_CONFIG_HOME;
|
|
2894
|
+
if (xdgConfig && xdgConfig.trim()) {
|
|
2895
|
+
return path7.join(path7.resolve(xdgConfig), "opencode");
|
|
2896
|
+
}
|
|
2897
|
+
return path7.join(home, ".config", "opencode");
|
|
2898
|
+
}
|
|
2899
|
+
function opencodeDataCandidates(home, env) {
|
|
2900
|
+
const xdgData = env?.XDG_DATA_HOME;
|
|
2901
|
+
const primary = xdgData && xdgData.trim() ? path7.join(path7.resolve(xdgData), "opencode", "opencode.db") : path7.join(home, ".local", "share", "opencode", "opencode.db");
|
|
2902
|
+
return [primary, path7.join(home, ".opencode", "opencode.db")];
|
|
2903
|
+
}
|
|
2904
|
+
async function opencodeBackfillFiles(sourceRoot, home = os2.homedir(), env) {
|
|
2859
2905
|
const { stat: stat5 } = await import("node:fs/promises");
|
|
2860
2906
|
if (sourceRoot) {
|
|
2861
2907
|
if (!sourceRoot.endsWith(".db")) {
|
|
@@ -2867,11 +2913,7 @@ async function opencodeBackfillFiles(sourceRoot) {
|
|
|
2867
2913
|
}
|
|
2868
2914
|
return [{ path: sourceRoot, modifiedAt: info.mtime.toISOString() }];
|
|
2869
2915
|
}
|
|
2870
|
-
const
|
|
2871
|
-
path7.join(os2.homedir(), ".local", "share", "opencode", "opencode.db"),
|
|
2872
|
-
path7.join(os2.homedir(), ".opencode", "opencode.db")
|
|
2873
|
-
];
|
|
2874
|
-
for (const candidatePath of candidates) {
|
|
2916
|
+
for (const candidatePath of opencodeDataCandidates(home, env)) {
|
|
2875
2917
|
const info = await stat5(candidatePath).catch(() => null);
|
|
2876
2918
|
if (info) {
|
|
2877
2919
|
return [{ path: candidatePath, modifiedAt: info.mtime.toISOString() }];
|
|
@@ -2925,39 +2967,35 @@ export const AgentTime = async ({ $, directory }) => {
|
|
|
2925
2967
|
`;
|
|
2926
2968
|
}
|
|
2927
2969
|
function createOpenCodeAdapter() {
|
|
2928
|
-
const OPENCODE_CONFIG = ".config/opencode";
|
|
2929
2970
|
const PLUGIN_PATH = "plugins/codetime.mjs";
|
|
2930
2971
|
return {
|
|
2931
2972
|
id: "opencode",
|
|
2932
2973
|
label: "OpenCode",
|
|
2933
2974
|
agentName: "opencode",
|
|
2934
2975
|
kind: "agent",
|
|
2935
|
-
detectPath(home) {
|
|
2936
|
-
return
|
|
2976
|
+
detectPath(home, env) {
|
|
2977
|
+
return opencodeConfigDir(home, env);
|
|
2937
2978
|
},
|
|
2938
|
-
installedPath(home) {
|
|
2939
|
-
return path7.join(home,
|
|
2979
|
+
installedPath(home, env) {
|
|
2980
|
+
return path7.join(opencodeConfigDir(home, env), PLUGIN_PATH);
|
|
2940
2981
|
},
|
|
2941
|
-
async isInstalled(home) {
|
|
2982
|
+
async isInstalled(home, env) {
|
|
2942
2983
|
try {
|
|
2943
2984
|
const { pathExists: pathExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
2944
|
-
return await pathExists2(path7.join(home,
|
|
2985
|
+
return await pathExists2(path7.join(opencodeConfigDir(home, env), PLUGIN_PATH)) || await pathExists2(path7.join(".opencode", PLUGIN_PATH));
|
|
2945
2986
|
} catch {
|
|
2946
2987
|
return false;
|
|
2947
2988
|
}
|
|
2948
2989
|
},
|
|
2949
|
-
installEntries(home) {
|
|
2990
|
+
installEntries(home, env) {
|
|
2950
2991
|
return [{
|
|
2951
2992
|
kind: "file",
|
|
2952
|
-
path: path7.join(home,
|
|
2993
|
+
path: path7.join(opencodeConfigDir(home, env), PLUGIN_PATH),
|
|
2953
2994
|
content: opencodePluginContent()
|
|
2954
2995
|
}];
|
|
2955
2996
|
},
|
|
2956
|
-
sourcePaths(home) {
|
|
2957
|
-
return
|
|
2958
|
-
path7.join(home, ".local", "share", "opencode", "opencode.db"),
|
|
2959
|
-
path7.join(home, ".opencode", "opencode.db")
|
|
2960
|
-
];
|
|
2997
|
+
sourcePaths(home, env) {
|
|
2998
|
+
return opencodeDataCandidates(home, env);
|
|
2961
2999
|
},
|
|
2962
3000
|
parseSessionFile: parseOpenCodeSessionFile
|
|
2963
3001
|
};
|
|
@@ -3391,36 +3429,49 @@ export default function (pi: ExtensionAPI) {
|
|
|
3391
3429
|
}
|
|
3392
3430
|
`;
|
|
3393
3431
|
}
|
|
3432
|
+
function piAgentDir(home, env) {
|
|
3433
|
+
const override = env?.PI_CODING_AGENT_DIR;
|
|
3434
|
+
if (override && override.trim()) {
|
|
3435
|
+
return path8.resolve(override);
|
|
3436
|
+
}
|
|
3437
|
+
return path8.join(home, ".pi", "agent");
|
|
3438
|
+
}
|
|
3439
|
+
function piSessionDir(home, env) {
|
|
3440
|
+
const override = env?.PI_CODING_AGENT_SESSION_DIR;
|
|
3441
|
+
if (override && override.trim()) {
|
|
3442
|
+
return path8.resolve(override);
|
|
3443
|
+
}
|
|
3444
|
+
return path8.join(piAgentDir(home, env), "sessions");
|
|
3445
|
+
}
|
|
3394
3446
|
function createPiAdapter() {
|
|
3395
|
-
const PI_EXTENSIONS_DIR = ".pi/agent/extensions";
|
|
3396
3447
|
return {
|
|
3397
3448
|
id: "pi",
|
|
3398
3449
|
label: "Pi",
|
|
3399
3450
|
agentName: "pi",
|
|
3400
3451
|
kind: "agent",
|
|
3401
|
-
detectPath(home) {
|
|
3402
|
-
return
|
|
3452
|
+
detectPath(home, env) {
|
|
3453
|
+
return piAgentDir(home, env);
|
|
3403
3454
|
},
|
|
3404
|
-
installedPath(home) {
|
|
3405
|
-
return path8.join(home,
|
|
3455
|
+
installedPath(home, env) {
|
|
3456
|
+
return path8.join(piAgentDir(home, env), "extensions", "codetime.ts");
|
|
3406
3457
|
},
|
|
3407
|
-
async isInstalled(home) {
|
|
3458
|
+
async isInstalled(home, env) {
|
|
3408
3459
|
try {
|
|
3409
3460
|
const { pathExists: pathExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
|
|
3410
|
-
return await pathExists2(path8.join(home,
|
|
3461
|
+
return await pathExists2(path8.join(piAgentDir(home, env), "extensions", "codetime.ts"));
|
|
3411
3462
|
} catch {
|
|
3412
3463
|
return false;
|
|
3413
3464
|
}
|
|
3414
3465
|
},
|
|
3415
|
-
installEntries(home) {
|
|
3466
|
+
installEntries(home, env) {
|
|
3416
3467
|
return [{
|
|
3417
3468
|
kind: "file",
|
|
3418
|
-
path: path8.join(home,
|
|
3469
|
+
path: path8.join(piAgentDir(home, env), "extensions", "codetime.ts"),
|
|
3419
3470
|
content: piExtensionContent()
|
|
3420
3471
|
}];
|
|
3421
3472
|
},
|
|
3422
|
-
sourcePaths(home) {
|
|
3423
|
-
return [
|
|
3473
|
+
sourcePaths(home, env) {
|
|
3474
|
+
return [piSessionDir(home, env)];
|
|
3424
3475
|
},
|
|
3425
3476
|
parseSessionFile: parsePiSessionFile
|
|
3426
3477
|
};
|
|
@@ -4569,18 +4620,19 @@ function normalizeOptions(options) {
|
|
|
4569
4620
|
}
|
|
4570
4621
|
async function detectCommand(options, ctx, registry) {
|
|
4571
4622
|
const home = resolveHome(options, ctx);
|
|
4623
|
+
const env = ctx.env;
|
|
4572
4624
|
const adapters = registry.all();
|
|
4573
4625
|
const targets = await Promise.all(adapters.map(async (adapter) => {
|
|
4574
|
-
const detected = await pathExists(adapter.detectPath(home));
|
|
4575
|
-
const installed = await adapter.isInstalled(home);
|
|
4626
|
+
const detected = await pathExists(adapter.detectPath(home, env));
|
|
4627
|
+
const installed = await adapter.isInstalled(home, env);
|
|
4576
4628
|
return {
|
|
4577
4629
|
id: adapter.id,
|
|
4578
4630
|
label: adapter.label,
|
|
4579
4631
|
kind: adapter.kind,
|
|
4580
4632
|
detected,
|
|
4581
4633
|
installed,
|
|
4582
|
-
detectPath: adapter.detectPath(home),
|
|
4583
|
-
installedPath: adapter.installedPath(home)
|
|
4634
|
+
detectPath: adapter.detectPath(home, env),
|
|
4635
|
+
installedPath: adapter.installedPath(home, env)
|
|
4584
4636
|
};
|
|
4585
4637
|
}));
|
|
4586
4638
|
if (options.json) {
|
|
@@ -4597,6 +4649,7 @@ async function detectCommand(options, ctx, registry) {
|
|
|
4597
4649
|
}
|
|
4598
4650
|
async function installCommand(options, ctx, registry) {
|
|
4599
4651
|
const home = resolveHome(options, ctx);
|
|
4652
|
+
const env = ctx.env;
|
|
4600
4653
|
const dryRun = Boolean(options["dry-run"]);
|
|
4601
4654
|
const force = Boolean(options.force);
|
|
4602
4655
|
const allAdapters = registry.all();
|
|
@@ -4607,7 +4660,7 @@ async function installCommand(options, ctx, registry) {
|
|
|
4607
4660
|
}
|
|
4608
4661
|
const detected = [];
|
|
4609
4662
|
for (const adapter of allAdapters) {
|
|
4610
|
-
if (await pathExists(adapter.detectPath(home))) {
|
|
4663
|
+
if (await pathExists(adapter.detectPath(home, env))) {
|
|
4611
4664
|
detected.push(adapter.id);
|
|
4612
4665
|
}
|
|
4613
4666
|
}
|
|
@@ -4617,7 +4670,7 @@ async function installCommand(options, ctx, registry) {
|
|
|
4617
4670
|
return 1;
|
|
4618
4671
|
}
|
|
4619
4672
|
for (const adapter of allAdapters.filter((a) => selectedIds.includes(a.id))) {
|
|
4620
|
-
for (const entry of adapter.installEntries(home)) {
|
|
4673
|
+
for (const entry of adapter.installEntries(home, env)) {
|
|
4621
4674
|
await installEntry(entry, {
|
|
4622
4675
|
dryRun,
|
|
4623
4676
|
force,
|
|
@@ -4746,13 +4799,14 @@ async function backfillCommand(options, ctx, registry) {
|
|
|
4746
4799
|
}
|
|
4747
4800
|
async function createBackfillPlanFromOptions(options, ctx, action, registry) {
|
|
4748
4801
|
const home = resolveHome(options, ctx);
|
|
4802
|
+
const env = ctx.env;
|
|
4749
4803
|
const source = normalizeBackfillSource(stringOption(options.source) || "all");
|
|
4750
|
-
const sourceDefs = source === "all" ? registry.all().map((a) => ({ id: a.id, label: a.label, paths: a.sourcePaths(home) })) : (() => {
|
|
4804
|
+
const sourceDefs = source === "all" ? registry.all().map((a) => ({ id: a.id, label: a.label, paths: a.sourcePaths(home, env) })) : (() => {
|
|
4751
4805
|
const adapter = registry.get(source);
|
|
4752
4806
|
if (!adapter) {
|
|
4753
4807
|
return [];
|
|
4754
4808
|
}
|
|
4755
|
-
return [{ id: adapter.id, label: adapter.label, paths: adapter.sourcePaths(home) }];
|
|
4809
|
+
return [{ id: adapter.id, label: adapter.label, paths: adapter.sourcePaths(home, env) }];
|
|
4756
4810
|
})();
|
|
4757
4811
|
if (sourceDefs.length === 0) {
|
|
4758
4812
|
throw new Error(`Unknown backfill source: ${source}`);
|
|
@@ -4761,7 +4815,7 @@ async function createBackfillPlanFromOptions(options, ctx, action, registry) {
|
|
|
4761
4815
|
const candidates = candidateList.flat();
|
|
4762
4816
|
let events = [];
|
|
4763
4817
|
if (action !== "discover") {
|
|
4764
|
-
const eventList = await createBackfillEventsFromDefs(sourceDefs, options, registry);
|
|
4818
|
+
const eventList = await createBackfillEventsFromDefs(sourceDefs, options, registry, ctx);
|
|
4765
4819
|
events = eventList.flat();
|
|
4766
4820
|
}
|
|
4767
4821
|
const plannedEvents = events.map((event) => ({
|
|
@@ -4811,14 +4865,14 @@ async function createBackfillPlanFromOptions(options, ctx, action, registry) {
|
|
|
4811
4865
|
privacy: "metadata only; prompt text, command text, source code, and diffs are not imported"
|
|
4812
4866
|
};
|
|
4813
4867
|
}
|
|
4814
|
-
async function createBackfillEventsFromDefs(sourceDefs, options, registry, overrideFiles) {
|
|
4868
|
+
async function createBackfillEventsFromDefs(sourceDefs, options, registry, ctx, overrideFiles) {
|
|
4815
4869
|
const events = [];
|
|
4816
4870
|
for (const item of sourceDefs) {
|
|
4817
4871
|
const parser = registry.getParser(item.id);
|
|
4818
4872
|
if (!parser) {
|
|
4819
4873
|
continue;
|
|
4820
4874
|
}
|
|
4821
|
-
const sourceFiles = await listBackfillSourceFiles(item, options);
|
|
4875
|
+
const sourceFiles = await listBackfillSourceFiles(item, options, ctx);
|
|
4822
4876
|
const files = overrideFiles ?? sourceFiles.map((f) => f.path);
|
|
4823
4877
|
for (const filePath of files) {
|
|
4824
4878
|
const parsed = await parser(filePath, options);
|
|
@@ -4846,9 +4900,9 @@ async function createBackfillCandidates(source, options) {
|
|
|
4846
4900
|
};
|
|
4847
4901
|
}));
|
|
4848
4902
|
}
|
|
4849
|
-
async function listBackfillSourceFiles(source, options) {
|
|
4903
|
+
async function listBackfillSourceFiles(source, options, ctx) {
|
|
4850
4904
|
if (source.id === "opencode") {
|
|
4851
|
-
return opencodeBackfillFiles(stringOption(options["source-root"]));
|
|
4905
|
+
return opencodeBackfillFiles(stringOption(options["source-root"]), resolveHome(options, ctx), ctx.env);
|
|
4852
4906
|
}
|
|
4853
4907
|
const roots = stringOption(options["source-root"]) ? [requiredOption(options, "source-root")] : source.paths;
|
|
4854
4908
|
const fileLists = await Promise.all(roots.map((r) => listJsonlFiles(r)));
|
|
@@ -4936,119 +4990,122 @@ async function importBackfillPlan(plan, options, ctx, registry) {
|
|
|
4936
4990
|
write(ctx.stderr, "Only Codex, Claude Code, OpenCode, and Pi backfill import are implemented.\n");
|
|
4937
4991
|
return 1;
|
|
4938
4992
|
}
|
|
4939
|
-
const
|
|
4940
|
-
const sourceDefs = allAdapters.filter((a) => supportedSources.has(a.id) && (source === "all" || a.id === source)).map((a) => ({ id: a.id, label: a.label, paths: a.sourcePaths(home) }));
|
|
4993
|
+
const sourceDefs = registry.all().filter((a) => supportedSources.has(a.id) && (source === "all" || a.id === source)).map((a) => ({ id: a.id, label: a.label, paths: a.sourcePaths(home, ctx.env) }));
|
|
4941
4994
|
if (options.force) {
|
|
4942
|
-
|
|
4943
|
-
await rm(backfillIncrementalStatePath(home), { force: true });
|
|
4944
|
-
} catch {
|
|
4945
|
-
}
|
|
4946
|
-
for (const item of sourceDefs) {
|
|
4947
|
-
try {
|
|
4948
|
-
const deleted = await deleteSessionRollupsBySourceAPI(item.id, options, ctx);
|
|
4949
|
-
if (!options.json) {
|
|
4950
|
-
write(ctx.stdout, `purged ${item.id}: ${deleted} old rollups
|
|
4951
|
-
`);
|
|
4952
|
-
}
|
|
4953
|
-
} catch (error) {
|
|
4954
|
-
debug(ctx, `Failed to purge ${item.id} rollups: ${error.message}
|
|
4955
|
-
`);
|
|
4956
|
-
}
|
|
4957
|
-
}
|
|
4995
|
+
await purgeForcedSources(sourceDefs, home, options, ctx);
|
|
4958
4996
|
}
|
|
4959
|
-
const incrementalState = shouldUseIncrementalBackfill(options) ? await readBackfillIncrementalState(home) : void 0;
|
|
4960
|
-
const selectedFilesBySource = /* @__PURE__ */ new Map();
|
|
4961
|
-
const canonicalEvents = [];
|
|
4997
|
+
const incrementalState = shouldUseIncrementalBackfill(options) ? await readBackfillIncrementalState(home, ctx) : void 0;
|
|
4962
4998
|
if (!options.json) {
|
|
4963
4999
|
write(ctx.stdout, `importRun ${plan.importRun.importRunId}
|
|
4964
5000
|
`);
|
|
4965
5001
|
write(ctx.stdout, `sources ${sourceDefs.map((s) => s.id).join(", ") || "none"}
|
|
4966
5002
|
`);
|
|
4967
5003
|
}
|
|
5004
|
+
const { canonicalEvents, selectedFilesBySource } = await collectCanonicalEvents(
|
|
5005
|
+
sourceDefs,
|
|
5006
|
+
registry,
|
|
5007
|
+
incrementalState,
|
|
5008
|
+
options,
|
|
5009
|
+
ctx
|
|
5010
|
+
);
|
|
5011
|
+
const rollups = buildSessionRollups(canonicalEvents);
|
|
5012
|
+
const counts = await uploadSessionRollups(rollups, canonicalEvents.length, options, ctx);
|
|
5013
|
+
const result = {
|
|
5014
|
+
importRunId: plan.importRun.importRunId,
|
|
5015
|
+
source,
|
|
5016
|
+
planned: rollups.length,
|
|
5017
|
+
sourceEvents: canonicalEvents.length,
|
|
5018
|
+
...counts
|
|
5019
|
+
};
|
|
5020
|
+
if (options.json) {
|
|
5021
|
+
write(ctx.stdout, `${JSON.stringify(result, null, 2)}
|
|
5022
|
+
`);
|
|
5023
|
+
}
|
|
5024
|
+
if (counts.failed === 0 && counts.conflicts === 0 && incrementalState) {
|
|
5025
|
+
await updateBackfillIncrementalState(home, incrementalState, selectedFilesBySource);
|
|
5026
|
+
}
|
|
5027
|
+
return counts.failed > 0 || counts.conflicts > 0 && !options["skip-conflicts"] ? 1 : 0;
|
|
5028
|
+
}
|
|
5029
|
+
async function purgeForcedSources(sourceDefs, home, options, ctx) {
|
|
5030
|
+
try {
|
|
5031
|
+
await rm(backfillIncrementalStatePath(home), { force: true });
|
|
5032
|
+
} catch (error) {
|
|
5033
|
+
debug(ctx, `Failed to clear backfill watermark: ${error.message}
|
|
5034
|
+
`);
|
|
5035
|
+
}
|
|
5036
|
+
for (const item of sourceDefs) {
|
|
5037
|
+
try {
|
|
5038
|
+
const deleted = await deleteSessionRollupsBySourceAPI(item.id, options, ctx);
|
|
5039
|
+
if (!options.json) {
|
|
5040
|
+
write(ctx.stdout, `purged ${item.id}: ${deleted} old rollups
|
|
5041
|
+
`);
|
|
5042
|
+
}
|
|
5043
|
+
} catch (error) {
|
|
5044
|
+
debug(ctx, `Failed to purge ${item.id} rollups: ${error.message}
|
|
5045
|
+
`);
|
|
5046
|
+
}
|
|
5047
|
+
}
|
|
5048
|
+
}
|
|
5049
|
+
async function collectCanonicalEvents(sourceDefs, registry, incrementalState, options, ctx) {
|
|
5050
|
+
const selectedFilesBySource = /* @__PURE__ */ new Map();
|
|
5051
|
+
const canonicalEvents = [];
|
|
4968
5052
|
for (const item of sourceDefs) {
|
|
4969
5053
|
const parser = registry.getParser(item.id);
|
|
4970
5054
|
if (!parser) {
|
|
4971
5055
|
continue;
|
|
4972
5056
|
}
|
|
4973
|
-
const sourceFiles = await listBackfillSourceFiles(item, options);
|
|
5057
|
+
const sourceFiles = await listBackfillSourceFiles(item, options, ctx);
|
|
4974
5058
|
const selectedFiles = selectBackfillFilesForImport(sourceFiles, incrementalState?.sources[item.id]?.watermarkTs);
|
|
4975
5059
|
selectedFilesBySource.set(item.id, selectedFiles);
|
|
4976
5060
|
const filePaths = selectedFiles.map((f) => f.path);
|
|
4977
5061
|
const sourceEvents = [];
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
}
|
|
4986
|
-
}
|
|
4987
|
-
} else {
|
|
4988
|
-
const bar = new ProgressBar(ctx.stdout, `${item.id.padEnd(12)}`);
|
|
4989
|
-
bar.init(filePaths.length, `0 events`);
|
|
4990
|
-
for (let fi = 0; fi < filePaths.length; fi += 1) {
|
|
4991
|
-
const parsed = await parser(filePaths[fi], options);
|
|
4992
|
-
for (const event of parsed) {
|
|
4993
|
-
if (matchesBackfillFilters(event, options)) {
|
|
4994
|
-
sourceEvents.push(event);
|
|
4995
|
-
}
|
|
5062
|
+
const bar = options.json ? void 0 : new ProgressBar(ctx.stdout, `${item.id.padEnd(12)}`);
|
|
5063
|
+
bar?.init(filePaths.length, `0 events`);
|
|
5064
|
+
for (let fi = 0; fi < filePaths.length; fi += 1) {
|
|
5065
|
+
const parsed = await parser(filePaths[fi], options);
|
|
5066
|
+
for (const event of parsed) {
|
|
5067
|
+
if (matchesBackfillFilters(event, options)) {
|
|
5068
|
+
sourceEvents.push(event);
|
|
4996
5069
|
}
|
|
4997
|
-
bar.tick(`${fi + 1}/${filePaths.length} files, ${sourceEvents.length} events`);
|
|
4998
5070
|
}
|
|
4999
|
-
bar
|
|
5071
|
+
bar?.tick(`${fi + 1}/${filePaths.length} files, ${sourceEvents.length} events`);
|
|
5000
5072
|
}
|
|
5073
|
+
bar?.finalize(`${sourceEvents.length} events`);
|
|
5001
5074
|
for (const event of sourceEvents) canonicalEvents.push(event);
|
|
5002
5075
|
}
|
|
5076
|
+
return { canonicalEvents, selectedFilesBySource };
|
|
5077
|
+
}
|
|
5078
|
+
async function uploadSessionRollups(rollups, eventCount, options, ctx) {
|
|
5003
5079
|
const counts = { inserted: 0, skipped: 0, conflicts: 0, failed: 0 };
|
|
5004
|
-
const rollups = buildSessionRollups(canonicalEvents);
|
|
5005
5080
|
const batchSize = Math.max(1, Math.floor(numberOption(options["batch-size"]) || DEFAULT_BACKFILL_BATCH_SIZE));
|
|
5006
5081
|
const batchBytes = Math.max(64 * 1024, Math.floor(numberOption(options["batch-bytes"]) || DEFAULT_BACKFILL_BATCH_BYTES));
|
|
5007
5082
|
const batches = packRollupBatches(rollups, batchSize, batchBytes);
|
|
5008
5083
|
const totalBatches = batches.length;
|
|
5009
5084
|
let uploadBar;
|
|
5010
5085
|
if (!options.json) {
|
|
5011
|
-
write(ctx.stdout, `rollup ${rollups.length} from ${
|
|
5086
|
+
write(ctx.stdout, `rollup ${rollups.length} from ${eventCount} events
|
|
5012
5087
|
`);
|
|
5013
5088
|
uploadBar = new ProgressBar(ctx.stdout, `upload`.padEnd(12));
|
|
5014
5089
|
uploadBar.init(totalBatches, `0/${totalBatches} batches, 0 inserted`);
|
|
5015
5090
|
}
|
|
5016
|
-
for (
|
|
5017
|
-
const batch =
|
|
5091
|
+
for (const [i, batch_] of batches.entries()) {
|
|
5092
|
+
const batch = batch_;
|
|
5018
5093
|
const batchNumber = i + 1;
|
|
5019
5094
|
try {
|
|
5020
|
-
const
|
|
5021
|
-
counts.inserted +=
|
|
5022
|
-
counts.skipped +=
|
|
5023
|
-
counts.conflicts +=
|
|
5024
|
-
counts.failed +=
|
|
5095
|
+
const result = await sendSessionRollupBatch(batch, options, ctx);
|
|
5096
|
+
counts.inserted += result.inserted;
|
|
5097
|
+
counts.skipped += result.skipped;
|
|
5098
|
+
counts.conflicts += result.conflicts;
|
|
5099
|
+
counts.failed += result.failed;
|
|
5025
5100
|
} catch (error) {
|
|
5026
5101
|
debug(ctx, `backfill rollup batch ${batchNumber}/${totalBatches} (${batch.length} rollups) failed: ${error.message}
|
|
5027
5102
|
`);
|
|
5028
5103
|
counts.failed += batch.length;
|
|
5029
5104
|
}
|
|
5030
|
-
|
|
5031
|
-
uploadBar.update(batchNumber, `${batchNumber}/${totalBatches} batches, inserted ${counts.inserted}`);
|
|
5032
|
-
}
|
|
5105
|
+
uploadBar?.update(batchNumber, `${batchNumber}/${totalBatches} batches, inserted ${counts.inserted}`);
|
|
5033
5106
|
}
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
}
|
|
5037
|
-
const result = {
|
|
5038
|
-
importRunId: plan.importRun.importRunId,
|
|
5039
|
-
source,
|
|
5040
|
-
planned: rollups.length,
|
|
5041
|
-
sourceEvents: canonicalEvents.length,
|
|
5042
|
-
...counts
|
|
5043
|
-
};
|
|
5044
|
-
if (options.json) {
|
|
5045
|
-
write(ctx.stdout, `${JSON.stringify(result, null, 2)}
|
|
5046
|
-
`);
|
|
5047
|
-
}
|
|
5048
|
-
if (counts.failed === 0 && counts.conflicts === 0 && incrementalState) {
|
|
5049
|
-
await updateBackfillIncrementalState(home, incrementalState, selectedFilesBySource);
|
|
5050
|
-
}
|
|
5051
|
-
return counts.failed > 0 || counts.conflicts > 0 && !options["skip-conflicts"] ? 1 : 0;
|
|
5107
|
+
uploadBar?.finalize(`inserted ${counts.inserted} \xB7 skipped ${counts.skipped}${counts.conflicts ? ` \xB7 conflicts ${counts.conflicts}` : ""}${counts.failed ? ` \xB7 failed ${counts.failed}` : ""}`);
|
|
5108
|
+
return counts;
|
|
5052
5109
|
}
|
|
5053
5110
|
function backfillVerifyCommand(options, ctx) {
|
|
5054
5111
|
const importRunId = stringOption(options["import-run"]);
|
|
@@ -5072,12 +5129,7 @@ ${result.message}
|
|
|
5072
5129
|
return 0;
|
|
5073
5130
|
}
|
|
5074
5131
|
async function sendSessionRollupBatch(rollups, options, ctx) {
|
|
5075
|
-
const remote =
|
|
5076
|
-
apiUrl: stringOption(options["api-url"]),
|
|
5077
|
-
token: stringOption(options.token),
|
|
5078
|
-
env: ctx.env,
|
|
5079
|
-
fetch: ctx.fetch
|
|
5080
|
-
});
|
|
5132
|
+
const remote = resolveRemoteFromOptions(options, ctx);
|
|
5081
5133
|
if (!remote) {
|
|
5082
5134
|
throw new Error("No fetch available for HTTP upload");
|
|
5083
5135
|
}
|
|
@@ -5096,12 +5148,7 @@ async function sendSessionRollupBatch(rollups, options, ctx) {
|
|
|
5096
5148
|
return result;
|
|
5097
5149
|
}
|
|
5098
5150
|
async function deleteSessionRollupsBySourceAPI(source, options, ctx) {
|
|
5099
|
-
const remote =
|
|
5100
|
-
apiUrl: stringOption(options["api-url"]),
|
|
5101
|
-
token: stringOption(options.token),
|
|
5102
|
-
env: ctx.env,
|
|
5103
|
-
fetch: ctx.fetch
|
|
5104
|
-
});
|
|
5151
|
+
const remote = resolveRemoteFromOptions(options, ctx);
|
|
5105
5152
|
if (!remote) {
|
|
5106
5153
|
throw new Error("No fetch available for HTTP delete");
|
|
5107
5154
|
}
|
|
@@ -5130,7 +5177,9 @@ function packRollupBatches(rollups, maxCount, maxBytes) {
|
|
|
5130
5177
|
current.push(rollup);
|
|
5131
5178
|
currentBytes += size;
|
|
5132
5179
|
}
|
|
5133
|
-
if (current.length > 0)
|
|
5180
|
+
if (current.length > 0) {
|
|
5181
|
+
batches.push(current);
|
|
5182
|
+
}
|
|
5134
5183
|
return batches;
|
|
5135
5184
|
}
|
|
5136
5185
|
function selectBackfillFilesForImport(files, watermarkTs) {
|
|
@@ -5155,10 +5204,25 @@ function syncLocalTriggerStatePath(home) {
|
|
|
5155
5204
|
function syncLocalTriggerLockPath(home) {
|
|
5156
5205
|
return path11.join(home, ".codetime", "sync-local-trigger.lock");
|
|
5157
5206
|
}
|
|
5158
|
-
async function readBackfillIncrementalState(home) {
|
|
5159
|
-
const
|
|
5207
|
+
async function readBackfillIncrementalState(home, ctx) {
|
|
5208
|
+
const statePath = backfillIncrementalStatePath(home);
|
|
5209
|
+
const state = await readJsonIfExists(statePath);
|
|
5210
|
+
if (state === null) {
|
|
5211
|
+
return { version: BACKFILL_STATE_SCHEMA_VERSION, sources: {} };
|
|
5212
|
+
}
|
|
5160
5213
|
if (!isPlainObject(state) || !isPlainObject(state.sources)) {
|
|
5161
|
-
|
|
5214
|
+
if (ctx) {
|
|
5215
|
+
debug(ctx, `backfill-state malformed at ${statePath}; ignoring watermarks
|
|
5216
|
+
`);
|
|
5217
|
+
}
|
|
5218
|
+
return { version: BACKFILL_STATE_SCHEMA_VERSION, sources: {} };
|
|
5219
|
+
}
|
|
5220
|
+
if (state.version !== BACKFILL_STATE_SCHEMA_VERSION) {
|
|
5221
|
+
if (ctx) {
|
|
5222
|
+
debug(ctx, `backfill-state version ${String(state.version)} at ${statePath} differs from current v${BACKFILL_STATE_SCHEMA_VERSION}; dropping watermarks so the next sync re-imports under the new parser
|
|
5223
|
+
`);
|
|
5224
|
+
}
|
|
5225
|
+
return { version: BACKFILL_STATE_SCHEMA_VERSION, sources: {} };
|
|
5162
5226
|
}
|
|
5163
5227
|
const sources = {};
|
|
5164
5228
|
for (const source of ["codex", "claude-code", "opencode", "pi"]) {
|
|
@@ -5167,7 +5231,7 @@ async function readBackfillIncrementalState(home) {
|
|
|
5167
5231
|
sources[source] = { watermarkTs: item.watermarkTs };
|
|
5168
5232
|
}
|
|
5169
5233
|
}
|
|
5170
|
-
return { version:
|
|
5234
|
+
return { version: BACKFILL_STATE_SCHEMA_VERSION, sources };
|
|
5171
5235
|
}
|
|
5172
5236
|
async function updateBackfillIncrementalState(home, state, selectedFilesBySource) {
|
|
5173
5237
|
for (const [source, files] of selectedFilesBySource.entries()) {
|
|
@@ -5339,6 +5403,15 @@ function debug(ctx, message) {
|
|
|
5339
5403
|
write(ctx.stderr, message);
|
|
5340
5404
|
}
|
|
5341
5405
|
}
|
|
5406
|
+
function resolveRemoteFromOptions(options, ctx) {
|
|
5407
|
+
return resolveRemote({
|
|
5408
|
+
apiUrl: stringOption(options["api-url"]),
|
|
5409
|
+
token: stringOption(options.token),
|
|
5410
|
+
env: ctx.env,
|
|
5411
|
+
fetch: ctx.fetch,
|
|
5412
|
+
homeOverride: stringOption(options.home)
|
|
5413
|
+
});
|
|
5414
|
+
}
|
|
5342
5415
|
function maskToken(token) {
|
|
5343
5416
|
if (token.length <= 8) {
|
|
5344
5417
|
return "***";
|
|
@@ -5399,12 +5472,7 @@ Usage: codetime token [set <token>|show|clear] [--remote <url>]
|
|
|
5399
5472
|
return 1;
|
|
5400
5473
|
}
|
|
5401
5474
|
async function machineCommand(action, options, ctx) {
|
|
5402
|
-
const remote =
|
|
5403
|
-
apiUrl: stringOption(options["api-url"]),
|
|
5404
|
-
token: stringOption(options.token),
|
|
5405
|
-
env: ctx.env,
|
|
5406
|
-
fetch: ctx.fetch
|
|
5407
|
-
});
|
|
5475
|
+
const remote = resolveRemoteFromOptions(options, ctx);
|
|
5408
5476
|
if (!remote || !remote.token) {
|
|
5409
5477
|
write(ctx.stderr, "machine command requires login (run `codetime login`).\n");
|
|
5410
5478
|
return 1;
|
|
@@ -5485,14 +5553,11 @@ Global options:
|
|
|
5485
5553
|
--token <token> Bearer token for this invocation (overrides config).
|
|
5486
5554
|
|
|
5487
5555
|
Token precedence (highest first):
|
|
5488
|
-
--token flag >
|
|
5489
|
-
(legacy alias) > saved config (~/.codetime/config.json).
|
|
5556
|
+
--token flag > CODETIME_TOKEN env > saved config (~/.codetime/config.json).
|
|
5490
5557
|
|
|
5491
5558
|
Environment:
|
|
5492
5559
|
CODETIME_API_URL Defaults to ${DEFAULT_API_URL}
|
|
5493
|
-
|
|
5494
|
-
AGENT_TIME_API_URL Legacy alias for CODETIME_API_URL
|
|
5495
|
-
AGENT_TIME_API_TOKEN Legacy alias for CODETIME_AGENT_TOKEN
|
|
5560
|
+
CODETIME_TOKEN Bearer token for the API
|
|
5496
5561
|
`;
|
|
5497
5562
|
}
|
|
5498
5563
|
export {
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codetime-cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.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": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
10
|
-
"homepage": "https://github.com/
|
|
10
|
+
"homepage": "https://github.com/codetime-dev/codetime-cli#readme",
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
|
-
"url": "git+https://github.com/
|
|
13
|
+
"url": "git+https://github.com/codetime-dev/codetime-cli.git"
|
|
14
14
|
},
|
|
15
15
|
"bugs": {
|
|
16
|
-
"url": "https://github.com/
|
|
16
|
+
"url": "https://github.com/codetime-dev/codetime-cli/issues"
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
19
19
|
"codetime",
|
|
@@ -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.3.0"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "tsc -p tsconfig.json",
|