codetime-cli 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/codetime.mjs +1010 -1018
  2. package/package.json +5 -5
package/bin/codetime.mjs CHANGED
@@ -14,6 +14,7 @@ var fs_exports = {};
14
14
  __export(fs_exports, {
15
15
  GENERATED_MARKER: () => GENERATED_MARKER,
16
16
  countDirectoryEntries: () => countDirectoryEntries,
17
+ listFilesByExtensions: () => listFilesByExtensions,
17
18
  listJsonlFiles: () => listJsonlFiles,
18
19
  parseJsonFile: () => parseJsonFile,
19
20
  pathExists: () => pathExists,
@@ -22,7 +23,7 @@ __export(fs_exports, {
22
23
  writeGeneratedFile: () => writeGeneratedFile
23
24
  });
24
25
  import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
25
- import path3 from "node:path";
26
+ import path from "node:path";
26
27
  async function pathExists(filePath) {
27
28
  try {
28
29
  await stat(filePath);
@@ -70,11 +71,14 @@ async function writeGeneratedFile(filePath, content, { dryRun, force, onWrite })
70
71
  `Refusing to overwrite non-codetime file: ${filePath}. Re-run with --force if this is intentional.`
71
72
  );
72
73
  }
73
- await mkdir(path3.dirname(filePath), { recursive: true });
74
+ await mkdir(path.dirname(filePath), { recursive: true });
74
75
  await writeFile(filePath, content, "utf8");
75
76
  onWrite(`Installed ${filePath}`);
76
77
  }
77
78
  async function listJsonlFiles(rootPath) {
79
+ return listFilesByExtensions(rootPath, [".jsonl"]);
80
+ }
81
+ async function listFilesByExtensions(rootPath, extensions) {
78
82
  let info;
79
83
  try {
80
84
  info = await stat(rootPath);
@@ -85,18 +89,18 @@ async function listJsonlFiles(rootPath) {
85
89
  throw error;
86
90
  }
87
91
  if (info.isFile()) {
88
- return rootPath.endsWith(".jsonl") ? [rootPath] : [];
92
+ return extensions.some((ext) => rootPath.endsWith(ext)) ? [rootPath] : [];
89
93
  }
90
94
  if (!info.isDirectory()) {
91
95
  return [];
92
96
  }
93
97
  const entries = await readdir(rootPath, { withFileTypes: true });
94
98
  const files = await Promise.all(entries.map(async (entry) => {
95
- const entryPath = path3.join(rootPath, entry.name);
99
+ const entryPath = path.join(rootPath, entry.name);
96
100
  if (entry.isDirectory()) {
97
- return listJsonlFiles(entryPath);
101
+ return listFilesByExtensions(entryPath, extensions);
98
102
  }
99
- return entry.isFile() && entry.name.endsWith(".jsonl") ? [entryPath] : [];
103
+ return entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext)) ? [entryPath] : [];
100
104
  }));
101
105
  return files.flat().sort();
102
106
  }
@@ -108,6 +112,9 @@ async function countDirectoryEntries(candidatePath) {
108
112
  const text = await readFile(candidatePath, "utf8");
109
113
  return text.split("\n").filter(Boolean).length;
110
114
  }
115
+ if (candidatePath.endsWith(".json")) {
116
+ return 1;
117
+ }
111
118
  if (candidatePath.endsWith(".db")) {
112
119
  try {
113
120
  const { DatabaseSync } = await import("node:sqlite");
@@ -126,7 +133,7 @@ async function countDirectoryEntries(candidatePath) {
126
133
  }
127
134
  return 1;
128
135
  }
129
- const files = await listJsonlFiles(candidatePath);
136
+ const files = await listFilesByExtensions(candidatePath, [".jsonl", ".json"]);
130
137
  return files.length;
131
138
  } catch (error) {
132
139
  if (error.code === "ENOENT") {
@@ -145,9 +152,9 @@ var init_fs = __esm({
145
152
 
146
153
  // src/cli.ts
147
154
  import { spawn } from "node:child_process";
148
- import { mkdir as mkdir2, rm, stat as stat4, writeFile as writeFile2 } from "node:fs/promises";
155
+ import { mkdir as mkdir3, rm, stat as stat5, writeFile as writeFile2 } from "node:fs/promises";
149
156
  import os3 from "node:os";
150
- import path11 from "node:path";
157
+ import path12 from "node:path";
151
158
  import { fileURLToPath } from "node:url";
152
159
 
153
160
  // ../shared/src/index.ts
@@ -960,231 +967,17 @@ var CAC = class extends EventTarget {
960
967
  };
961
968
  var cac = (name = "") => new CAC(name);
962
969
 
963
- // src/adapters/claude-code.ts
970
+ // src/adapters/amp.ts
964
971
  import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
965
- import os from "node:os";
966
- import path5 from "node:path";
967
-
968
- // src/lib/activity.ts
969
972
  import path2 from "node:path";
970
973
 
971
- // src/lib/shell.ts
972
- import path from "node:path";
973
- function fileActivitiesFromShellCommand(command, ts, rootCwd, initialCwd) {
974
- const activities = [];
975
- let currentCwd = initialCwd;
976
- for (const segment of command.split(/\s*(?:&&|\|\||\||;)\s*/)) {
977
- const words = shellWords(segment);
978
- if (words.length === 0) {
979
- continue;
980
- }
981
- const commandName = path.basename(words[0]);
982
- if (commandName === "cd" && words[1]) {
983
- currentCwd = resolvePath(words[1], currentCwd);
984
- continue;
985
- }
986
- for (const item of shellReadTargets(words, currentCwd)) {
987
- activities.push({
988
- ts,
989
- path: displayActivityPath(item.path, rootCwd, currentCwd),
990
- operation: item.operation,
991
- confidence: "derived"
992
- });
993
- }
994
- }
995
- return activities;
996
- }
997
- function shellReadTargets(words, cwd) {
998
- const command = path.basename(words[0]).toLowerCase();
999
- if (["cat", "less", "more", "nl"].includes(command)) {
1000
- return fileArgs(words.slice(1), /* @__PURE__ */ new Set()).map((item) => ({ path: item, operation: "read" }));
1001
- }
1002
- if (command === "sed") {
1003
- const args = commandArgs(words.slice(1), /* @__PURE__ */ new Set(["-e", "--expression", "-f", "--file"]));
1004
- return args.slice(1).filter(looksLikePathArg).map((item) => ({ path: item, operation: "read" }));
1005
- }
1006
- if (command === "head" || command === "tail") {
1007
- return fileArgs(words.slice(1), /* @__PURE__ */ new Set(["-n", "-c", "--lines", "--bytes"])).map((item) => ({ path: item, operation: "read" }));
1008
- }
1009
- if (["rg", "grep", "ag"].includes(command)) {
1010
- const hasPatternOption = words.includes("-e") || words.includes("--regexp") || words.includes("-f") || words.includes("--file");
1011
- const args = commandArgs(words.slice(1), /* @__PURE__ */ new Set(["-e", "--regexp", "-f", "--file", "-g", "--glob", "-t", "-T", "-A", "-B", "-C", "-m"]));
1012
- const pathArgs = hasPatternOption ? args : args.slice(1);
1013
- return pathArgs.filter(looksLikePathArg).map((item) => ({ path: item, operation: "search" }));
1014
- }
1015
- if (command === "find") {
1016
- const args = fileArgs(words.slice(1), /* @__PURE__ */ new Set());
1017
- return args.slice(0, 1).map((item) => ({ path: item, operation: "search" }));
1018
- }
1019
- if (command === "pwd" && cwd) {
1020
- return [{ path: cwd, operation: "search" }];
1021
- }
1022
- return [];
1023
- }
1024
- function fileArgs(args, optionsWithValues) {
1025
- return commandArgs(args, optionsWithValues).filter(looksLikePathArg);
1026
- }
1027
- function commandArgs(args, optionsWithValues) {
1028
- const result = [];
1029
- for (let index = 0; index < args.length; index += 1) {
1030
- const arg = args[index];
1031
- if (!arg || arg === "--") {
1032
- continue;
1033
- }
1034
- if (arg.startsWith("-")) {
1035
- if (optionsWithValues.has(arg)) {
1036
- index += 1;
1037
- }
1038
- continue;
1039
- }
1040
- result.push(arg);
1041
- }
1042
- return result;
1043
- }
1044
- function looksLikePathArg(value) {
1045
- if (!value || value.startsWith("$")) {
1046
- return false;
1047
- }
1048
- if ([">", ">>", "<", "2>", "2>>"].includes(value)) {
1049
- return false;
1050
- }
1051
- return path.isAbsolute(value) || value.includes("/") || value.includes(".");
1052
- }
1053
- function shellWords(command) {
1054
- const matches = command.match(/"([^"\\]|\\.)*"|'[^']*'|\S+/g) || [];
1055
- return matches.map((word) => {
1056
- if (word.startsWith('"') && word.endsWith('"') || word.startsWith("'") && word.endsWith("'")) {
1057
- return word.slice(1, -1);
1058
- }
1059
- return word;
1060
- });
1061
- }
1062
- function resolvePath(filePath, cwd) {
1063
- if (path.isAbsolute(filePath) || !cwd) {
1064
- return filePath;
1065
- }
1066
- return path.resolve(cwd, filePath);
1067
- }
1068
- function displayFilePath(filePath, cwd) {
1069
- if (!cwd || !path.isAbsolute(filePath)) {
1070
- return filePath;
1071
- }
1072
- const relative = path.relative(cwd, filePath);
1073
- return relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : filePath;
1074
- }
1075
- function displayActivityPath(filePath, rootCwd, currentCwd) {
1076
- if (path.isAbsolute(filePath)) {
1077
- return displayFilePath(filePath, rootCwd);
1078
- }
1079
- if (currentCwd && path.isAbsolute(currentCwd)) {
1080
- return displayFilePath(path.resolve(currentCwd, filePath), rootCwd);
1081
- }
1082
- return filePath;
1083
- }
1084
-
1085
- // src/lib/activity.ts
1086
- function operationForTool(tool) {
1087
- const normalized = (tool || "").toLowerCase();
1088
- if (["read", "notebookread", "view_image"].includes(normalized)) {
1089
- return "read";
1090
- }
1091
- if (["grep", "glob", "ls", "search", "rg"].includes(normalized)) {
1092
- return "search";
1093
- }
1094
- if (["write"].includes(normalized)) {
1095
- return "write";
1096
- }
1097
- if (["edit", "multiedit", "notebookedit", "apply_patch", "applypatch"].includes(normalized)) {
1098
- return "edit";
1099
- }
1100
- return "read";
1101
- }
1102
- function toolFileActivityType(tool) {
1103
- const readTools = ["read", "glob", "grep", "webfetch", "websearch"];
1104
- if (readTools.includes(tool.toLowerCase())) {
1105
- return "file.read";
1106
- }
1107
- return "file.changed";
1108
- }
1109
- function selectFileOperation(current, next) {
1110
- if (!current) {
1111
- return next;
1112
- }
1113
- if (isWriteOperation(current) && !isWriteOperation(next)) {
1114
- return current;
1115
- }
1116
- return next;
1117
- }
1118
- function isWriteOperation(operation) {
1119
- return operation === "create" || operation === "write" || operation === "edit" || operation === "delete";
1120
- }
1121
- function eventTypeFromFileActivities(files) {
1122
- if (files.some((file) => isWriteOperation(file.operation))) {
1123
- return "file.changed";
1124
- }
1125
- if (files.some((file) => file.operation === "search")) {
1126
- return "file.searched";
1127
- }
1128
- return "file.read";
1129
- }
1130
- function mergeFileActivity(current, next) {
1131
- return {
1132
- ts: next.ts,
1133
- path: next.path,
1134
- operation: selectFileOperation(current?.operation, next.operation),
1135
- bytesRead: sumOptional(current?.bytesRead, next.bytesRead),
1136
- bytesWritten: sumOptional(current?.bytesWritten, next.bytesWritten),
1137
- charsRead: sumOptional(current?.charsRead, next.charsRead),
1138
- charsWritten: sumOptional(current?.charsWritten, next.charsWritten),
1139
- linesRead: sumOptional(current?.linesRead, next.linesRead),
1140
- linesAdded: (current?.linesAdded || 0) + (next.linesAdded || 0),
1141
- linesRemoved: (current?.linesRemoved || 0) + (next.linesRemoved || 0)
1142
- };
1143
- }
1144
- function sumOptional(left, right) {
1145
- const sum = (left || 0) + (right || 0);
1146
- return sum || void 0;
1147
- }
1148
- function summarizeFileActivities(files) {
1149
- const linesAdded = files.reduce((total, file) => total + (file.linesAdded || 0), 0);
1150
- const linesRemoved = files.reduce((total, file) => total + (file.linesRemoved || 0), 0);
1151
- return {
1152
- linesAdded: linesAdded || void 0,
1153
- linesRemoved: linesRemoved || void 0
1154
- };
1155
- }
1156
- function addPathActivity(changes, filePath, operation, ts) {
1157
- if (!filePath) {
1158
- return;
1159
- }
1160
- const current = changes.get(filePath);
1161
- changes.set(filePath, mergeFileActivity(current, { ts, path: filePath, operation }));
1162
- }
1163
- function addResolvedPathActivity(changes, filePath, operation, ts, rootCwd, currentCwd, metrics = {}) {
1164
- if (!filePath) {
1165
- return;
1166
- }
1167
- const resolvedPath = path2.isAbsolute(filePath) ? displayFilePath(filePath, rootCwd) : currentCwd && path2.isAbsolute(currentCwd) ? displayFilePath(path2.resolve(currentCwd, filePath), rootCwd) : filePath;
1168
- changes.set(
1169
- resolvedPath,
1170
- mergeFileActivity(changes.get(resolvedPath), { ts, path: resolvedPath, operation, ...metrics })
1171
- );
1172
- }
1173
- function displayBackfillPath(filePath) {
1174
- if (!path2.isAbsolute(filePath)) {
1175
- return filePath;
1176
- }
1177
- return path2.basename(filePath);
1178
- }
1179
- function countTextLines(text) {
1180
- if (!text) {
1181
- return void 0;
1182
- }
1183
- return text.split(/\r\n|\r|\n/).length;
1184
- }
1185
-
1186
- // src/lib/adapter-helpers.ts
1187
- import path4 from "node:path";
974
+ // src/lib/constants.ts
975
+ var PACKAGE_VERSION = true ? "0.3.1" : "0.1.1";
976
+ var DEFAULT_API_URL = "https://codetime.dev";
977
+ var DEFAULT_BACKFILL_BATCH_SIZE = 50;
978
+ var DEFAULT_BACKFILL_BATCH_BYTES = 800 * 1024;
979
+ var ROLLUP_BUCKET_MS = 15 * 60 * 1e3;
980
+ var DEFAULT_HOOK_SYNC_MIN_INTERVAL_SECONDS = 60;
1188
981
 
1189
982
  // src/lib/fields.ts
1190
983
  function stringField(object, key) {
@@ -1243,75 +1036,30 @@ function numberOption(value) {
1243
1036
  return Number.isFinite(parsed) ? parsed : void 0;
1244
1037
  }
1245
1038
 
1246
- // src/lib/adapter-helpers.ts
1247
- var TURN_IDLE_MS = 6e4;
1248
- function isTurnIdle(lastEventAt) {
1249
- if (!lastEventAt) {
1250
- return false;
1251
- }
1252
- const lastMs = Date.parse(lastEventAt);
1253
- if (!Number.isFinite(lastMs)) {
1254
- return false;
1039
+ // src/lib/jsonl.ts
1040
+ function parseJsonLine(line) {
1041
+ try {
1042
+ const parsed = JSON.parse(line);
1043
+ return isPlainObject(parsed) ? parsed : void 0;
1044
+ } catch {
1045
+ return void 0;
1255
1046
  }
1256
- return Date.now() - lastMs > TURN_IDLE_MS;
1257
1047
  }
1258
- function sessionIdFromFilePath(filePath, prefix) {
1259
- const match = path4.basename(filePath).match(/([0-9a-f]{8}-[0-9a-f-]{27,})/);
1260
- return match?.[1] || `${prefix}_${createStableHash(filePath).slice(0, 24)}`;
1048
+ function timestampFrom(value) {
1049
+ if (typeof value === "string" && !Number.isNaN(Date.parse(value))) {
1050
+ return value;
1051
+ }
1052
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1053
+ return void 0;
1054
+ }
1055
+ const millis = value > 1e10 ? value : value * 1e3;
1056
+ return new Date(millis).toISOString();
1261
1057
  }
1262
- function hookHandler(agentId, statusMessage) {
1263
- return {
1264
- type: "command",
1265
- command: `codetime hook --agent ${agentId}`,
1266
- timeout: 10,
1267
- statusMessage
1268
- };
1269
- }
1270
- async function isHooksJsonInstalled(filePath, command) {
1271
- try {
1272
- const { readTextIfExists: readTextIfExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
1273
- const text = await readTextIfExists2(filePath);
1274
- if (!text) {
1275
- return false;
1276
- }
1277
- const config = JSON.parse(text);
1278
- if (!isPlainObject(config) || !isPlainObject(config.hooks)) {
1279
- return false;
1280
- }
1281
- return Object.values(config.hooks).some(
1282
- (groups) => Array.isArray(groups) && groups.some(
1283
- (group) => isPlainObject(group) && Array.isArray(group.hooks) && group.hooks.some((hook) => hook.command === command)
1284
- )
1285
- );
1286
- } catch {
1287
- return false;
1288
- }
1289
- }
1290
-
1291
- // src/lib/jsonl.ts
1292
- function parseJsonLine(line) {
1293
- try {
1294
- const parsed = JSON.parse(line);
1295
- return isPlainObject(parsed) ? parsed : void 0;
1296
- } catch {
1297
- return void 0;
1298
- }
1299
- }
1300
- function timestampFrom(value) {
1301
- if (typeof value === "string" && !Number.isNaN(Date.parse(value))) {
1302
- return value;
1303
- }
1304
- if (typeof value !== "number" || !Number.isFinite(value)) {
1305
- return void 0;
1306
- }
1307
- const millis = value > 1e10 ? value : value * 1e3;
1308
- return new Date(millis).toISOString();
1309
- }
1310
- function durationObjectToMs(duration) {
1311
- const secs = duration.secs || 0;
1312
- const nanos = duration.nanos || 0;
1313
- const durationMs = secs * 1e3 + Math.round(nanos / 1e6);
1314
- return durationMs || void 0;
1058
+ function durationObjectToMs(duration) {
1059
+ const secs = duration.secs || 0;
1060
+ const nanos = duration.nanos || 0;
1061
+ const durationMs = secs * 1e3 + Math.round(nanos / 1e6);
1062
+ return durationMs || void 0;
1315
1063
  }
1316
1064
  function durationMsBetween(startedAt, endedAt) {
1317
1065
  if (!startedAt || !endedAt) {
@@ -1328,14 +1076,6 @@ function msToIso(ms) {
1328
1076
  return new Date(ms).toISOString();
1329
1077
  }
1330
1078
 
1331
- // src/lib/constants.ts
1332
- var PACKAGE_VERSION = true ? "0.2.0" : "0.1.1";
1333
- var DEFAULT_API_URL = "https://codetime.dev";
1334
- var DEFAULT_BACKFILL_BATCH_SIZE = 50;
1335
- var DEFAULT_BACKFILL_BATCH_BYTES = 800 * 1024;
1336
- var ROLLUP_BUCKET_MS = 15 * 60 * 1e3;
1337
- var DEFAULT_HOOK_SYNC_MIN_INTERVAL_SECONDS = 60;
1338
-
1339
1079
  // src/lib/backfill.ts
1340
1080
  function withBackfillRefs(event, context) {
1341
1081
  const importKey = createImportKey([
@@ -1380,6 +1120,9 @@ function matchesBackfillFilters(event, options) {
1380
1120
  return true;
1381
1121
  }
1382
1122
 
1123
+ // src/adapters/amp.ts
1124
+ init_fs();
1125
+
1383
1126
  // src/lib/session-state.ts
1384
1127
  var SessionParserState = class {
1385
1128
  events = [];
@@ -1464,14 +1207,447 @@ var SessionParserState = class {
1464
1207
  sessionId: event.sessionId ?? sessionId
1465
1208
  };
1466
1209
  }
1467
- };
1210
+ };
1211
+
1212
+ // src/adapters/amp.ts
1213
+ async function parseAmpSessionFile(filePath, options) {
1214
+ const text = await readFile2(filePath, "utf8");
1215
+ let raw;
1216
+ try {
1217
+ raw = JSON.parse(text);
1218
+ } catch {
1219
+ return [];
1220
+ }
1221
+ if (!isPlainObject(raw)) {
1222
+ return [];
1223
+ }
1224
+ const threadId = stringField(raw, "id") || path2.basename(filePath, ".json");
1225
+ const sessionId = `amp_${threadId}`;
1226
+ const messages = Array.isArray(raw.messages) ? raw.messages.filter(isPlainObject) : [];
1227
+ const ledger = objectField(raw, "usageLedger");
1228
+ const rawEvents = Array.isArray(ledger.events) ? ledger.events.filter(isPlainObject) : [];
1229
+ if (rawEvents.length === 0) {
1230
+ return [];
1231
+ }
1232
+ const events = [...rawEvents].sort((a, b) => {
1233
+ const ta = timestampFrom(stringField(a, "timestamp")) || "";
1234
+ const tb = timestampFrom(stringField(b, "timestamp")) || "";
1235
+ return ta.localeCompare(tb);
1236
+ });
1237
+ const state = new SessionParserState(filePath, options, (event) => baseAmpEvent(event));
1238
+ state.sessionId = sessionId;
1239
+ const firstTs = timestampFrom(stringField(events[0], "timestamp")) || (/* @__PURE__ */ new Date()).toISOString();
1240
+ state.ensureSessionStarted(firstTs, 0);
1241
+ let lastTs = firstTs;
1242
+ for (const [index, ev] of events.entries()) {
1243
+ const ts = timestampFrom(stringField(ev, "timestamp")) || lastTs;
1244
+ lastTs = ts;
1245
+ const model = stringField(ev, "model") || void 0;
1246
+ const tokens = objectField(ev, "tokens");
1247
+ const input = numberField(tokens, "input") || 0;
1248
+ const output = numberField(tokens, "output") || 0;
1249
+ const toMessageId = numberField(ev, "toMessageId");
1250
+ const cache = cacheTokensFor(messages, toMessageId);
1251
+ const cacheRead = cache.cacheReadInputTokens;
1252
+ const cacheWrite = cache.cacheCreationInputTokens;
1253
+ const totalTokens = input + output + cacheRead + cacheWrite;
1254
+ if (totalTokens <= 0) {
1255
+ continue;
1256
+ }
1257
+ const credits = numberField(ev, "credits");
1258
+ const operationType = stringField(ev, "operationType");
1259
+ const fromMessageId = numberField(ev, "fromMessageId");
1260
+ const metrics = {
1261
+ tokensInput: input + cacheRead + cacheWrite || void 0,
1262
+ tokensOutput: output || void 0,
1263
+ tokensCachedInput: cacheRead + cacheWrite || void 0,
1264
+ tokensCacheReadInput: cacheRead || void 0,
1265
+ tokensCacheCreationInput: cacheWrite || void 0,
1266
+ tokensTotal: totalTokens,
1267
+ modelCalls: 1
1268
+ };
1269
+ state.push(
1270
+ baseAmpEvent({
1271
+ ts,
1272
+ type: "model.usage",
1273
+ sessionId,
1274
+ model,
1275
+ confidence: "exact",
1276
+ metrics,
1277
+ refs: stringRefs({
1278
+ threadId,
1279
+ operationType,
1280
+ fromMessageId: fromMessageId === void 0 ? void 0 : String(fromMessageId),
1281
+ toMessageId: toMessageId === void 0 ? void 0 : String(toMessageId),
1282
+ // Credits are Amp's proprietary billing unit; preserved here so the
1283
+ // backend can correlate token counts with what the user is charged.
1284
+ credits: typeof credits === "number" && Number.isFinite(credits) ? String(credits) : void 0
1285
+ })
1286
+ }),
1287
+ index + 1,
1288
+ "ledger",
1289
+ "model.usage"
1290
+ );
1291
+ }
1292
+ state.push(
1293
+ baseAmpEvent({
1294
+ ts: lastTs,
1295
+ type: "session.ended",
1296
+ sessionId,
1297
+ confidence: "derived"
1298
+ }),
1299
+ events.length,
1300
+ "ledger",
1301
+ "session"
1302
+ );
1303
+ return state.events.filter((event) => matchesBackfillFilters(event, options));
1304
+ }
1305
+ function baseAmpEvent(event) {
1306
+ return {
1307
+ schemaVersion: AGENT_TIME_SCHEMA_VERSION,
1308
+ source: "amp",
1309
+ agent: "amp",
1310
+ // Amp threads carry no cwd/project metadata; pin all events to a stable
1311
+ // synthetic workspace so downstream rollups keep them grouped together.
1312
+ workspaceId: createWorkspaceId({ projectName: "amp" }),
1313
+ ...event
1314
+ };
1315
+ }
1316
+ function cacheTokensFor(messages, toMessageId) {
1317
+ if (toMessageId === void 0) {
1318
+ return { cacheReadInputTokens: 0, cacheCreationInputTokens: 0 };
1319
+ }
1320
+ const match = messages.find(
1321
+ (message) => stringField(message, "role") === "assistant" && numberField(message, "messageId") === toMessageId
1322
+ );
1323
+ if (!match) {
1324
+ return { cacheReadInputTokens: 0, cacheCreationInputTokens: 0 };
1325
+ }
1326
+ const usage = objectField(match, "usage");
1327
+ return {
1328
+ cacheReadInputTokens: numberField(usage, "cacheReadInputTokens") || 0,
1329
+ cacheCreationInputTokens: numberField(usage, "cacheCreationInputTokens") || 0
1330
+ };
1331
+ }
1332
+ function ampDataDir(home, env) {
1333
+ const override = env?.AMP_DATA_DIR;
1334
+ if (override && override.trim()) {
1335
+ return path2.resolve(override);
1336
+ }
1337
+ return path2.join(home, ".local", "share", "amp");
1338
+ }
1339
+ function ampThreadsDir(home, env) {
1340
+ return path2.join(ampDataDir(home, env), "threads");
1341
+ }
1342
+ async function ampBackfillFiles(sourceRoot, home, env) {
1343
+ const root = sourceRoot ? path2.resolve(sourceRoot) : ampThreadsDir(home, env);
1344
+ const files = await listFilesByExtensions(root, [".json"]);
1345
+ return Promise.all(files.map(async (filePath) => {
1346
+ const info = await stat2(filePath);
1347
+ return { path: filePath, modifiedAt: info.mtime.toISOString() };
1348
+ }));
1349
+ }
1350
+ function createAmpAdapter() {
1351
+ return {
1352
+ id: "amp",
1353
+ label: "Amp",
1354
+ agentName: "amp",
1355
+ kind: "agent",
1356
+ detectPath(home, env) {
1357
+ return ampDataDir(home, env);
1358
+ },
1359
+ // Amp has no plugin/hook surface, so there is nothing for codetime to
1360
+ // "install". Report installed = true whenever the threads dir is on disk
1361
+ // so `detect` accurately shows "already covered via backfill".
1362
+ installedPath(home, env) {
1363
+ return ampThreadsDir(home, env);
1364
+ },
1365
+ async isInstalled(home, env) {
1366
+ try {
1367
+ return await pathExists(ampThreadsDir(home, env));
1368
+ } catch {
1369
+ return false;
1370
+ }
1371
+ },
1372
+ installEntries() {
1373
+ return [];
1374
+ },
1375
+ sourcePaths(home, env) {
1376
+ return [ampThreadsDir(home, env)];
1377
+ },
1378
+ parseSessionFile: parseAmpSessionFile
1379
+ };
1380
+ }
1381
+
1382
+ // src/adapters/claude-code.ts
1383
+ import { readFile as readFile3, stat as stat3 } from "node:fs/promises";
1384
+ import os from "node:os";
1385
+ import path6 from "node:path";
1386
+
1387
+ // src/lib/activity.ts
1388
+ import path4 from "node:path";
1389
+
1390
+ // src/lib/shell.ts
1391
+ import path3 from "node:path";
1392
+ function fileActivitiesFromShellCommand(command, ts, rootCwd, initialCwd) {
1393
+ const activities = [];
1394
+ let currentCwd = initialCwd;
1395
+ for (const segment of command.split(/\s*(?:&&|\|\||\||;)\s*/)) {
1396
+ const words = shellWords(segment);
1397
+ if (words.length === 0) {
1398
+ continue;
1399
+ }
1400
+ const commandName = path3.basename(words[0]);
1401
+ if (commandName === "cd" && words[1]) {
1402
+ currentCwd = resolvePath(words[1], currentCwd);
1403
+ continue;
1404
+ }
1405
+ for (const item of shellReadTargets(words, currentCwd)) {
1406
+ activities.push({
1407
+ ts,
1408
+ path: displayActivityPath(item.path, rootCwd, currentCwd),
1409
+ operation: item.operation,
1410
+ confidence: "derived"
1411
+ });
1412
+ }
1413
+ }
1414
+ return activities;
1415
+ }
1416
+ function shellReadTargets(words, cwd) {
1417
+ const command = path3.basename(words[0]).toLowerCase();
1418
+ if (["cat", "less", "more", "nl"].includes(command)) {
1419
+ return fileArgs(words.slice(1), /* @__PURE__ */ new Set()).map((item) => ({ path: item, operation: "read" }));
1420
+ }
1421
+ if (command === "sed") {
1422
+ const args = commandArgs(words.slice(1), /* @__PURE__ */ new Set(["-e", "--expression", "-f", "--file"]));
1423
+ return args.slice(1).filter(looksLikePathArg).map((item) => ({ path: item, operation: "read" }));
1424
+ }
1425
+ if (command === "head" || command === "tail") {
1426
+ return fileArgs(words.slice(1), /* @__PURE__ */ new Set(["-n", "-c", "--lines", "--bytes"])).map((item) => ({ path: item, operation: "read" }));
1427
+ }
1428
+ if (["rg", "grep", "ag"].includes(command)) {
1429
+ const hasPatternOption = words.includes("-e") || words.includes("--regexp") || words.includes("-f") || words.includes("--file");
1430
+ const args = commandArgs(words.slice(1), /* @__PURE__ */ new Set(["-e", "--regexp", "-f", "--file", "-g", "--glob", "-t", "-T", "-A", "-B", "-C", "-m"]));
1431
+ const pathArgs = hasPatternOption ? args : args.slice(1);
1432
+ return pathArgs.filter(looksLikePathArg).map((item) => ({ path: item, operation: "search" }));
1433
+ }
1434
+ if (command === "find") {
1435
+ const args = fileArgs(words.slice(1), /* @__PURE__ */ new Set());
1436
+ return args.slice(0, 1).map((item) => ({ path: item, operation: "search" }));
1437
+ }
1438
+ if (command === "pwd" && cwd) {
1439
+ return [{ path: cwd, operation: "search" }];
1440
+ }
1441
+ return [];
1442
+ }
1443
+ function fileArgs(args, optionsWithValues) {
1444
+ return commandArgs(args, optionsWithValues).filter(looksLikePathArg);
1445
+ }
1446
+ function commandArgs(args, optionsWithValues) {
1447
+ const result = [];
1448
+ for (let index = 0; index < args.length; index += 1) {
1449
+ const arg = args[index];
1450
+ if (!arg || arg === "--") {
1451
+ continue;
1452
+ }
1453
+ if (arg.startsWith("-")) {
1454
+ if (optionsWithValues.has(arg)) {
1455
+ index += 1;
1456
+ }
1457
+ continue;
1458
+ }
1459
+ result.push(arg);
1460
+ }
1461
+ return result;
1462
+ }
1463
+ function looksLikePathArg(value) {
1464
+ if (!value || value.startsWith("$")) {
1465
+ return false;
1466
+ }
1467
+ if ([">", ">>", "<", "2>", "2>>"].includes(value)) {
1468
+ return false;
1469
+ }
1470
+ return path3.isAbsolute(value) || value.includes("/") || value.includes(".");
1471
+ }
1472
+ function shellWords(command) {
1473
+ const matches = command.match(/"([^"\\]|\\.)*"|'[^']*'|\S+/g) || [];
1474
+ return matches.map((word) => {
1475
+ if (word.startsWith('"') && word.endsWith('"') || word.startsWith("'") && word.endsWith("'")) {
1476
+ return word.slice(1, -1);
1477
+ }
1478
+ return word;
1479
+ });
1480
+ }
1481
+ function resolvePath(filePath, cwd) {
1482
+ if (path3.isAbsolute(filePath) || !cwd) {
1483
+ return filePath;
1484
+ }
1485
+ return path3.resolve(cwd, filePath);
1486
+ }
1487
+ function displayFilePath(filePath, cwd) {
1488
+ if (!cwd || !path3.isAbsolute(filePath)) {
1489
+ return filePath;
1490
+ }
1491
+ const relative = path3.relative(cwd, filePath);
1492
+ return relative && !relative.startsWith("..") && !path3.isAbsolute(relative) ? relative : filePath;
1493
+ }
1494
+ function displayActivityPath(filePath, rootCwd, currentCwd) {
1495
+ if (path3.isAbsolute(filePath)) {
1496
+ return displayFilePath(filePath, rootCwd);
1497
+ }
1498
+ if (currentCwd && path3.isAbsolute(currentCwd)) {
1499
+ return displayFilePath(path3.resolve(currentCwd, filePath), rootCwd);
1500
+ }
1501
+ return filePath;
1502
+ }
1503
+
1504
+ // src/lib/activity.ts
1505
+ function operationForTool(tool) {
1506
+ const normalized = (tool || "").toLowerCase();
1507
+ if (["read", "notebookread", "view_image"].includes(normalized)) {
1508
+ return "read";
1509
+ }
1510
+ if (["grep", "glob", "ls", "search", "rg"].includes(normalized)) {
1511
+ return "search";
1512
+ }
1513
+ if (["write"].includes(normalized)) {
1514
+ return "write";
1515
+ }
1516
+ if (["edit", "multiedit", "notebookedit", "apply_patch", "applypatch"].includes(normalized)) {
1517
+ return "edit";
1518
+ }
1519
+ return "read";
1520
+ }
1521
+ function toolFileActivityType(tool) {
1522
+ const readTools = ["read", "glob", "grep", "webfetch", "websearch"];
1523
+ if (readTools.includes(tool.toLowerCase())) {
1524
+ return "file.read";
1525
+ }
1526
+ return "file.changed";
1527
+ }
1528
+ function selectFileOperation(current, next) {
1529
+ if (!current) {
1530
+ return next;
1531
+ }
1532
+ if (isWriteOperation(current) && !isWriteOperation(next)) {
1533
+ return current;
1534
+ }
1535
+ return next;
1536
+ }
1537
+ function isWriteOperation(operation) {
1538
+ return operation === "create" || operation === "write" || operation === "edit" || operation === "delete";
1539
+ }
1540
+ function eventTypeFromFileActivities(files) {
1541
+ if (files.some((file) => isWriteOperation(file.operation))) {
1542
+ return "file.changed";
1543
+ }
1544
+ if (files.some((file) => file.operation === "search")) {
1545
+ return "file.searched";
1546
+ }
1547
+ return "file.read";
1548
+ }
1549
+ function mergeFileActivity(current, next) {
1550
+ return {
1551
+ ts: next.ts,
1552
+ path: next.path,
1553
+ operation: selectFileOperation(current?.operation, next.operation),
1554
+ bytesRead: sumOptional(current?.bytesRead, next.bytesRead),
1555
+ bytesWritten: sumOptional(current?.bytesWritten, next.bytesWritten),
1556
+ charsRead: sumOptional(current?.charsRead, next.charsRead),
1557
+ charsWritten: sumOptional(current?.charsWritten, next.charsWritten),
1558
+ linesRead: sumOptional(current?.linesRead, next.linesRead),
1559
+ linesAdded: (current?.linesAdded || 0) + (next.linesAdded || 0),
1560
+ linesRemoved: (current?.linesRemoved || 0) + (next.linesRemoved || 0)
1561
+ };
1562
+ }
1563
+ function sumOptional(left, right) {
1564
+ const sum = (left || 0) + (right || 0);
1565
+ return sum || void 0;
1566
+ }
1567
+ function summarizeFileActivities(files) {
1568
+ const linesAdded = files.reduce((total, file) => total + (file.linesAdded || 0), 0);
1569
+ const linesRemoved = files.reduce((total, file) => total + (file.linesRemoved || 0), 0);
1570
+ return {
1571
+ linesAdded: linesAdded || void 0,
1572
+ linesRemoved: linesRemoved || void 0
1573
+ };
1574
+ }
1575
+ function addResolvedPathActivity(changes, filePath, operation, ts, rootCwd, currentCwd, metrics = {}) {
1576
+ if (!filePath) {
1577
+ return;
1578
+ }
1579
+ const resolvedPath = path4.isAbsolute(filePath) ? displayFilePath(filePath, rootCwd) : currentCwd && path4.isAbsolute(currentCwd) ? displayFilePath(path4.resolve(currentCwd, filePath), rootCwd) : filePath;
1580
+ changes.set(
1581
+ resolvedPath,
1582
+ mergeFileActivity(changes.get(resolvedPath), { ts, path: resolvedPath, operation, ...metrics })
1583
+ );
1584
+ }
1585
+ function displayBackfillPath(filePath) {
1586
+ if (!path4.isAbsolute(filePath)) {
1587
+ return filePath;
1588
+ }
1589
+ return path4.basename(filePath);
1590
+ }
1591
+ function countTextLines(text) {
1592
+ if (!text) {
1593
+ return void 0;
1594
+ }
1595
+ return text.split(/\r\n|\r|\n/).length;
1596
+ }
1597
+
1598
+ // src/lib/adapter-helpers.ts
1599
+ import path5 from "node:path";
1600
+ var TURN_IDLE_MS = 6e4;
1601
+ function isTurnIdle(lastEventAt) {
1602
+ if (!lastEventAt) {
1603
+ return false;
1604
+ }
1605
+ const lastMs = Date.parse(lastEventAt);
1606
+ if (!Number.isFinite(lastMs)) {
1607
+ return false;
1608
+ }
1609
+ return Date.now() - lastMs > TURN_IDLE_MS;
1610
+ }
1611
+ function sessionIdFromFilePath(filePath, prefix) {
1612
+ const match = path5.basename(filePath).match(/([0-9a-f]{8}-[0-9a-f-]{27,})/);
1613
+ return match?.[1] || `${prefix}_${createStableHash(filePath).slice(0, 24)}`;
1614
+ }
1615
+ function hookHandler(agentId, statusMessage) {
1616
+ return {
1617
+ type: "command",
1618
+ command: `codetime hook --agent ${agentId}`,
1619
+ timeout: 10,
1620
+ statusMessage
1621
+ };
1622
+ }
1623
+ async function isHooksJsonInstalled(filePath, command) {
1624
+ try {
1625
+ const { readTextIfExists: readTextIfExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
1626
+ const text = await readTextIfExists2(filePath);
1627
+ if (!text) {
1628
+ return false;
1629
+ }
1630
+ const config = JSON.parse(text);
1631
+ if (!isPlainObject(config) || !isPlainObject(config.hooks)) {
1632
+ return false;
1633
+ }
1634
+ return Object.values(config.hooks).some(
1635
+ (groups) => Array.isArray(groups) && groups.some(
1636
+ (group) => isPlainObject(group) && Array.isArray(group.hooks) && group.hooks.some((hook) => hook.command === command)
1637
+ )
1638
+ );
1639
+ } catch {
1640
+ return false;
1641
+ }
1642
+ }
1468
1643
 
1469
1644
  // src/adapters/claude-code.ts
1470
1645
  async function parseClaudeCodeSessionFile(filePath, options) {
1471
- const text = await readFile2(filePath, "utf8");
1646
+ const text = await readFile3(filePath, "utf8");
1472
1647
  const lines = text.split("\n").filter(Boolean);
1473
1648
  const projectContext = await claudeProjectContextFromLines(filePath, lines, options);
1474
1649
  const pendingTools = /* @__PURE__ */ new Map();
1650
+ const seenUsageKeys = /* @__PURE__ */ new Set();
1475
1651
  let sessionId = sessionIdFromFilePath(filePath, "claude");
1476
1652
  let cwd;
1477
1653
  let project = projectContext.project;
@@ -1497,7 +1673,7 @@ async function parseClaudeCodeSessionFile(filePath, options) {
1497
1673
  sessionId = stringField(raw, "sessionId") || sessionId;
1498
1674
  state.sessionId = sessionId;
1499
1675
  cwd = stringField(raw, "cwd") || cwd;
1500
- project = projectContext.project || (cwd ? path5.basename(cwd) : project || claudeProjectFromFilePath(filePath, options));
1676
+ project = projectContext.project || (cwd ? path6.basename(cwd) : project || claudeProjectFromFilePath(filePath, options));
1501
1677
  if (!ts) {
1502
1678
  continue;
1503
1679
  }
@@ -1623,8 +1799,19 @@ async function parseClaudeCodeSessionFile(filePath, options) {
1623
1799
  continue;
1624
1800
  }
1625
1801
  model = stringField(message, "model") || model;
1802
+ const messageId = stringField(message, "id");
1803
+ const requestId = stringField(raw, "requestId");
1804
+ const usageKey = messageId ? `${messageId}:${requestId}` : null;
1805
+ if (usageKey != null && seenUsageKeys.has(usageKey)) {
1806
+ continue;
1807
+ }
1808
+ if (usageKey != null) {
1809
+ seenUsageKeys.add(usageKey);
1810
+ }
1626
1811
  const usage = claudeUsageFromMessage(message);
1627
1812
  if (usage) {
1813
+ const speed = stringField(objectField(message, "usage"), "speed");
1814
+ const usageModel = speed === "fast" && model ? `${model}-fast` : model;
1628
1815
  push(baseClaudeEvent({
1629
1816
  ts,
1630
1817
  type: "model.usage",
@@ -1632,7 +1819,7 @@ async function parseClaudeCodeSessionFile(filePath, options) {
1632
1819
  turnId: state.currentTurnId,
1633
1820
  cwd,
1634
1821
  project,
1635
- model,
1822
+ model: usageModel,
1636
1823
  confidence: "partial",
1637
1824
  metrics: usage
1638
1825
  }), lineNumber, topType, "usage");
@@ -1855,17 +2042,17 @@ function claudeTextStats(value) {
1855
2042
  };
1856
2043
  }
1857
2044
  async function claudeProjectContextFromLines(filePath, lines, options) {
1858
- const projectDir = path5.basename(path5.dirname(filePath));
2045
+ const projectDir = path6.basename(path6.dirname(filePath));
1859
2046
  const cwds = [];
1860
2047
  for (const line of lines) {
1861
2048
  const raw = parseJsonLine(line);
1862
2049
  const cwd = raw ? stringField(raw, "cwd") : void 0;
1863
- if (cwd && path5.isAbsolute(cwd)) {
2050
+ if (cwd && path6.isAbsolute(cwd)) {
1864
2051
  cwds.push(cwd);
1865
2052
  }
1866
2053
  }
1867
2054
  const root = await gitRootFromCwds(cwds) || claudeProjectRootFromCwds(projectDir, cwds);
1868
- const project = root ? path5.basename(root) : claudeProjectFromFilePath(filePath, options);
2055
+ const project = root ? path6.basename(root) : claudeProjectFromFilePath(filePath, options);
1869
2056
  return {
1870
2057
  project,
1871
2058
  workspaceId: createWorkspaceId({ projectName: project, repoRoot: root })
@@ -1874,15 +2061,15 @@ async function claudeProjectContextFromLines(filePath, lines, options) {
1874
2061
  async function gitRootFromCwds(cwds) {
1875
2062
  const seen = /* @__PURE__ */ new Set();
1876
2063
  for (const cwd of cwds) {
1877
- let current = path5.resolve(cwd);
2064
+ let current = path6.resolve(cwd);
1878
2065
  while (!seen.has(current)) {
1879
2066
  seen.add(current);
1880
2067
  try {
1881
- await stat2(path5.join(current, ".git"));
2068
+ await stat3(path6.join(current, ".git"));
1882
2069
  return current;
1883
2070
  } catch {
1884
2071
  }
1885
- const parent = path5.dirname(current);
2072
+ const parent = path6.dirname(current);
1886
2073
  if (parent === current) {
1887
2074
  break;
1888
2075
  }
@@ -1893,12 +2080,12 @@ async function gitRootFromCwds(cwds) {
1893
2080
  }
1894
2081
  function claudeProjectRootFromCwds(projectDir, cwds) {
1895
2082
  for (const cwd of cwds) {
1896
- let current = path5.resolve(cwd);
2083
+ let current = path6.resolve(cwd);
1897
2084
  while (true) {
1898
2085
  if (encodeClaudeProjectPath(current) === projectDir) {
1899
2086
  return current;
1900
2087
  }
1901
- const parent = path5.dirname(current);
2088
+ const parent = path6.dirname(current);
1902
2089
  if (parent === current) {
1903
2090
  break;
1904
2091
  }
@@ -1908,11 +2095,11 @@ function claudeProjectRootFromCwds(projectDir, cwds) {
1908
2095
  return void 0;
1909
2096
  }
1910
2097
  function encodeClaudeProjectPath(value) {
1911
- return path5.resolve(value).split(path5.sep).join("-");
2098
+ return path6.resolve(value).split(path6.sep).join("-");
1912
2099
  }
1913
2100
  function claudeProjectFromFilePath(filePath, options) {
1914
- const projectDir = path5.basename(path5.dirname(filePath));
1915
- const home = options ? path5.resolve(stringOption(options.home) || os.homedir()) : os.homedir();
2101
+ const projectDir = path6.basename(path6.dirname(filePath));
2102
+ const home = options ? path6.resolve(stringOption(options.home) || os.homedir()) : os.homedir();
1916
2103
  const homePrefix = `${encodeClaudeProjectPath(home)}-`;
1917
2104
  if (projectDir.startsWith(homePrefix)) {
1918
2105
  return projectDir.slice(homePrefix.length) || void 0;
@@ -1940,36 +2127,44 @@ function hookConfig() {
1940
2127
  }
1941
2128
  };
1942
2129
  }
2130
+ function claudeConfigDir(home, env) {
2131
+ const override = env?.CLAUDE_CONFIG_DIR;
2132
+ if (override && override.trim()) {
2133
+ return path6.resolve(override);
2134
+ }
2135
+ return path6.join(home, ".claude");
2136
+ }
1943
2137
  function createClaudeCodeAdapter() {
1944
- const CLAUDE_PATH = ".claude";
1945
2138
  return {
1946
2139
  id: "claude-code",
1947
2140
  label: "Claude Code",
1948
2141
  agentName: "claude",
1949
2142
  kind: "agent",
1950
- detectPath(home) {
1951
- return path5.join(home, CLAUDE_PATH);
2143
+ detectPath(home, env) {
2144
+ return claudeConfigDir(home, env);
1952
2145
  },
1953
- installedPath(home) {
1954
- return path5.join(home, CLAUDE_PATH, "settings.json");
2146
+ installedPath(home, env) {
2147
+ return path6.join(claudeConfigDir(home, env), "settings.json");
1955
2148
  },
1956
- async isInstalled(home) {
2149
+ async isInstalled(home, env) {
1957
2150
  return isHooksJsonInstalled(
1958
- path5.join(home, CLAUDE_PATH, "settings.json"),
2151
+ path6.join(claudeConfigDir(home, env), "settings.json"),
1959
2152
  "codetime hook --agent claude"
1960
2153
  );
1961
2154
  },
1962
- installEntries(home) {
2155
+ installEntries(home, env) {
1963
2156
  return [{
1964
2157
  kind: "hooks-json",
1965
- path: path5.join(home, CLAUDE_PATH, "settings.json"),
2158
+ path: path6.join(claudeConfigDir(home, env), "settings.json"),
1966
2159
  content: hookConfig()
1967
2160
  }];
1968
2161
  },
1969
- sourcePaths(home) {
2162
+ sourcePaths(home, env) {
2163
+ const base = claudeConfigDir(home, env);
1970
2164
  return [
1971
- path5.join(home, ".claude", "projects"),
1972
- path5.join(home, ".claude.json")
2165
+ path6.join(base, "projects"),
2166
+ path6.join(base, ".claude.json"),
2167
+ path6.join(home, ".claude.json")
1973
2168
  ];
1974
2169
  },
1975
2170
  parseSessionFile: parseClaudeCodeSessionFile
@@ -1977,43 +2172,10 @@ function createClaudeCodeAdapter() {
1977
2172
  }
1978
2173
 
1979
2174
  // src/adapters/codex.ts
1980
- import { readFile as readFile3 } from "node:fs/promises";
1981
- import path6 from "node:path";
2175
+ import { readFile as readFile4 } from "node:fs/promises";
2176
+ import path7 from "node:path";
1982
2177
 
1983
2178
  // src/lib/diff.ts
1984
- function parseApplyPatch(patch, ts) {
1985
- const changes = [];
1986
- let current;
1987
- for (const line of patch.split("\n")) {
1988
- const fileMatch = line.match(/^\*\*\* (Add|Update|Delete) File: (.+)$/);
1989
- if (fileMatch) {
1990
- current = {
1991
- ts,
1992
- path: fileMatch[2].trim(),
1993
- operation: fileMatch[1] === "Add" ? "create" : fileMatch[1] === "Delete" ? "delete" : "edit",
1994
- linesAdded: 0,
1995
- linesRemoved: 0
1996
- };
1997
- changes.push(current);
1998
- continue;
1999
- }
2000
- if (!current) {
2001
- continue;
2002
- }
2003
- if (line.startsWith("+") && !line.startsWith("+++")) {
2004
- current.linesAdded = (current.linesAdded || 0) + 1;
2005
- } else if (line.startsWith("-") && !line.startsWith("---")) {
2006
- current.linesRemoved = (current.linesRemoved || 0) + 1;
2007
- }
2008
- }
2009
- return changes;
2010
- }
2011
- function patchFromCommand(command) {
2012
- if (!command || !command.includes("*** Begin Patch")) {
2013
- return void 0;
2014
- }
2015
- return command.slice(command.indexOf("*** Begin Patch"));
2016
- }
2017
2179
  function diffStats(diff) {
2018
2180
  let linesAdded = 0;
2019
2181
  let linesRemoved = 0;
@@ -2058,9 +2220,10 @@ function fileActivitiesFromPatchChanges(changes, ts, cwd, displayFilePath3) {
2058
2220
 
2059
2221
  // src/adapters/codex.ts
2060
2222
  async function parseCodexSessionFile(filePath, options) {
2061
- const text = await readFile3(filePath, "utf8");
2223
+ const text = await readFile4(filePath, "utf8");
2062
2224
  const lines = text.split("\n").filter(Boolean);
2063
2225
  const sourcePathHash = `sha256:${createStableHash(filePath)}`;
2226
+ const serviceTier = await resolveCodexServiceTier(filePath);
2064
2227
  const events = [];
2065
2228
  let sessionId = sessionIdFromFilePath(filePath, "codex");
2066
2229
  let cwd;
@@ -2089,7 +2252,7 @@ async function parseCodexSessionFile(filePath, options) {
2089
2252
  sessionMetaLocked = true;
2090
2253
  sessionId = stringField(payload, "id") || sessionId;
2091
2254
  cwd = stringField(payload, "cwd") || cwd;
2092
- project = cwd ? path6.basename(cwd) : project;
2255
+ project = cwd ? path7.basename(cwd) : project;
2093
2256
  model = stringField(payload, "model_provider") || model;
2094
2257
  events.push(withBackfillRefs({
2095
2258
  schemaVersion: AGENT_TIME_SCHEMA_VERSION,
@@ -2109,7 +2272,7 @@ async function parseCodexSessionFile(filePath, options) {
2109
2272
  if (topType === "turn_context") {
2110
2273
  currentTurnId = stringField(payload, "turn_id") || currentTurnId;
2111
2274
  cwd = stringField(payload, "cwd") || cwd;
2112
- project = cwd ? path6.basename(cwd) : project;
2275
+ project = cwd ? path7.basename(cwd) : project;
2113
2276
  model = stringField(payload, "model") || model;
2114
2277
  continue;
2115
2278
  }
@@ -2190,7 +2353,10 @@ async function parseCodexSessionFile(filePath, options) {
2190
2353
  turnId: currentTurnId,
2191
2354
  cwd,
2192
2355
  project,
2193
- model,
2356
+ // Only the model.usage event carries the -fast suffix; tool and
2357
+ // turn events keep the bare model so other queries (e.g. "what
2358
+ // model was the user on") don't show tier-specific names.
2359
+ model: rewriteCodexModelForTier(model, serviceTier),
2194
2360
  confidence: "partial",
2195
2361
  metrics: usage
2196
2362
  }), { filePath, sourcePathHash, lineNumber, topType, payloadType, options }));
@@ -2379,6 +2545,50 @@ async function parseCodexSessionFile(filePath, options) {
2379
2545
  }
2380
2546
  return events.filter((event) => validateCanonicalEvent(event).valid);
2381
2547
  }
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
+ function rewriteCodexModelForTier(model, serviceTier) {
2586
+ if (!model) {
2587
+ return model;
2588
+ }
2589
+ const isFastTier = serviceTier === "fast" || serviceTier === "priority";
2590
+ return isFastTier ? `${model}-fast` : model;
2591
+ }
2382
2592
  function baseCodexEvent(event) {
2383
2593
  return {
2384
2594
  schemaVersion: AGENT_TIME_SCHEMA_VERSION,
@@ -2453,11 +2663,11 @@ function functionCallArguments(payload) {
2453
2663
  }
2454
2664
  }
2455
2665
  function displayFilePath2(filePath, cwd) {
2456
- if (!cwd || !path6.isAbsolute(filePath)) {
2666
+ if (!cwd || !path7.isAbsolute(filePath)) {
2457
2667
  return filePath;
2458
2668
  }
2459
- const relative = path6.relative(cwd, filePath);
2460
- return relative && !relative.startsWith("..") && !path6.isAbsolute(relative) ? relative : filePath;
2669
+ const relative = path7.relative(cwd, filePath);
2670
+ return relative && !relative.startsWith("..") && !path7.isAbsolute(relative) ? relative : filePath;
2461
2671
  }
2462
2672
  var codexHandler = (msg) => hookHandler("codex", msg);
2463
2673
  function hookConfig2() {
@@ -2473,36 +2683,43 @@ function hookConfig2() {
2473
2683
  }
2474
2684
  };
2475
2685
  }
2686
+ function codexHome(home, env) {
2687
+ const override = env?.CODEX_HOME;
2688
+ if (override && override.trim()) {
2689
+ return path7.resolve(override);
2690
+ }
2691
+ return path7.join(home, ".codex");
2692
+ }
2476
2693
  function createCodexAdapter() {
2477
- const CODE_PATH = ".codex";
2478
2694
  return {
2479
2695
  id: "codex",
2480
2696
  label: "Codex",
2481
2697
  agentName: "codex",
2482
2698
  kind: "agent",
2483
- detectPath(home) {
2484
- return path6.join(home, CODE_PATH);
2699
+ detectPath(home, env) {
2700
+ return codexHome(home, env);
2485
2701
  },
2486
- installedPath(home) {
2487
- return path6.join(home, CODE_PATH, "hooks.json");
2702
+ installedPath(home, env) {
2703
+ return path7.join(codexHome(home, env), "hooks.json");
2488
2704
  },
2489
- async isInstalled(home) {
2705
+ async isInstalled(home, env) {
2490
2706
  return isHooksJsonInstalled(
2491
- path6.join(home, CODE_PATH, "hooks.json"),
2707
+ path7.join(codexHome(home, env), "hooks.json"),
2492
2708
  "codetime hook --agent codex"
2493
2709
  );
2494
2710
  },
2495
- installEntries(home) {
2711
+ installEntries(home, env) {
2496
2712
  return [{
2497
2713
  kind: "hooks-json",
2498
- path: path6.join(home, CODE_PATH, "hooks.json"),
2714
+ path: path7.join(codexHome(home, env), "hooks.json"),
2499
2715
  content: hookConfig2()
2500
2716
  }];
2501
2717
  },
2502
- sourcePaths(home) {
2718
+ sourcePaths(home, env) {
2719
+ const base = codexHome(home, env);
2503
2720
  return [
2504
- path6.join(home, ".codex", "sessions"),
2505
- path6.join(home, ".codex", "history.jsonl")
2721
+ path7.join(base, "sessions"),
2722
+ path7.join(base, "history.jsonl")
2506
2723
  ];
2507
2724
  },
2508
2725
  parseSessionFile: parseCodexSessionFile
@@ -2511,7 +2728,7 @@ function createCodexAdapter() {
2511
2728
 
2512
2729
  // src/adapters/opencode.ts
2513
2730
  import os2 from "node:os";
2514
- import path7 from "node:path";
2731
+ import path8 from "node:path";
2515
2732
  async function parseOpenCodeSessionFile(dbPath, options) {
2516
2733
  const { DatabaseSync } = await import("node:sqlite");
2517
2734
  if (!dbPath.endsWith(".db")) {
@@ -2520,13 +2737,29 @@ async function parseOpenCodeSessionFile(dbPath, options) {
2520
2737
  const db = new DatabaseSync(dbPath, { readOnly: true });
2521
2738
  const events = [];
2522
2739
  try {
2740
+ const sessionCols = new Set(
2741
+ db.prepare("PRAGMA table_info(session)").all().map((row) => row.name)
2742
+ );
2743
+ const hasDirectory = sessionCols.has("directory");
2744
+ const hasPath = sessionCols.has("path");
2745
+ const hasArchived = sessionCols.has("time_archived");
2746
+ const selectCols = ["id", "title", "time_created"];
2747
+ if (hasDirectory) {
2748
+ selectCols.push("directory");
2749
+ }
2750
+ if (hasPath) {
2751
+ selectCols.push("path");
2752
+ }
2753
+ if (hasArchived) {
2754
+ selectCols.push("time_archived");
2755
+ }
2523
2756
  const sessions = db.prepare(
2524
- "SELECT id, directory, path, title, time_created, time_archived FROM session WHERE time_created IS NOT NULL ORDER BY time_created"
2757
+ `SELECT ${selectCols.join(", ")} FROM session WHERE time_created IS NOT NULL ORDER BY time_created`
2525
2758
  ).all();
2526
2759
  for (const session of sessions) {
2527
2760
  const sessionId = session.id;
2528
2761
  const cwd = session.directory || session.path || void 0;
2529
- const project = cwd ? path7.basename(cwd) : void 0;
2762
+ const project = cwd ? path8.basename(cwd) : void 0;
2530
2763
  const sessionTs = msToIso(session.time_created);
2531
2764
  events.push(baseOpenCodeEvent({
2532
2765
  ts: sessionTs,
@@ -2616,7 +2849,7 @@ async function parseOpenCodeSessionFile(dbPath, options) {
2616
2849
  const assistantAgent = stringField(info, "agent") || "opencode";
2617
2850
  const pathObj = objectField(info, "path");
2618
2851
  const assistantCwd = stringField(pathObj, "cwd") || cwd;
2619
- const assistantProject = assistantCwd ? path7.basename(assistantCwd) : project;
2852
+ const assistantProject = assistantCwd ? path8.basename(assistantCwd) : project;
2620
2853
  const completedTs = numberField(objectField(info, "time"), "completed");
2621
2854
  const createdTs = timeCreated;
2622
2855
  const tokens = opencodeUsageFromInfo(info);
@@ -2855,24 +3088,36 @@ function opencodeUsageFromInfo(info) {
2855
3088
  tokensTotal: total
2856
3089
  };
2857
3090
  }
2858
- async function opencodeBackfillFiles(sourceRoot) {
2859
- const { stat: stat5 } = await import("node:fs/promises");
3091
+ function opencodeConfigDir(home, env) {
3092
+ const override = env?.OPENCODE_CONFIG_DIR;
3093
+ if (override && override.trim()) {
3094
+ return path8.resolve(override);
3095
+ }
3096
+ const xdgConfig = env?.XDG_CONFIG_HOME;
3097
+ if (xdgConfig && xdgConfig.trim()) {
3098
+ return path8.join(path8.resolve(xdgConfig), "opencode");
3099
+ }
3100
+ return path8.join(home, ".config", "opencode");
3101
+ }
3102
+ function opencodeDataCandidates(home, env) {
3103
+ const xdgData = env?.XDG_DATA_HOME;
3104
+ const primary = xdgData && xdgData.trim() ? path8.join(path8.resolve(xdgData), "opencode", "opencode.db") : path8.join(home, ".local", "share", "opencode", "opencode.db");
3105
+ return [primary, path8.join(home, ".opencode", "opencode.db")];
3106
+ }
3107
+ async function opencodeBackfillFiles(sourceRoot, home = os2.homedir(), env) {
3108
+ const { stat: stat6 } = await import("node:fs/promises");
2860
3109
  if (sourceRoot) {
2861
3110
  if (!sourceRoot.endsWith(".db")) {
2862
3111
  return [];
2863
3112
  }
2864
- const info = await stat5(sourceRoot).catch(() => null);
3113
+ const info = await stat6(sourceRoot).catch(() => null);
2865
3114
  if (!info) {
2866
3115
  return [];
2867
3116
  }
2868
3117
  return [{ path: sourceRoot, modifiedAt: info.mtime.toISOString() }];
2869
3118
  }
2870
- const candidates = [
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) {
2875
- const info = await stat5(candidatePath).catch(() => null);
3119
+ for (const candidatePath of opencodeDataCandidates(home, env)) {
3120
+ const info = await stat6(candidatePath).catch(() => null);
2876
3121
  if (info) {
2877
3122
  return [{ path: candidatePath, modifiedAt: info.mtime.toISOString() }];
2878
3123
  }
@@ -2925,49 +3170,45 @@ export const AgentTime = async ({ $, directory }) => {
2925
3170
  `;
2926
3171
  }
2927
3172
  function createOpenCodeAdapter() {
2928
- const OPENCODE_CONFIG = ".config/opencode";
2929
3173
  const PLUGIN_PATH = "plugins/codetime.mjs";
2930
3174
  return {
2931
3175
  id: "opencode",
2932
3176
  label: "OpenCode",
2933
3177
  agentName: "opencode",
2934
3178
  kind: "agent",
2935
- detectPath(home) {
2936
- return path7.join(home, OPENCODE_CONFIG);
3179
+ detectPath(home, env) {
3180
+ return opencodeConfigDir(home, env);
2937
3181
  },
2938
- installedPath(home) {
2939
- return path7.join(home, OPENCODE_CONFIG, PLUGIN_PATH);
3182
+ installedPath(home, env) {
3183
+ return path8.join(opencodeConfigDir(home, env), PLUGIN_PATH);
2940
3184
  },
2941
- async isInstalled(home) {
3185
+ async isInstalled(home, env) {
2942
3186
  try {
2943
3187
  const { pathExists: pathExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
2944
- return await pathExists2(path7.join(home, OPENCODE_CONFIG, PLUGIN_PATH)) || await pathExists2(path7.join(".opencode", PLUGIN_PATH));
3188
+ return await pathExists2(path8.join(opencodeConfigDir(home, env), PLUGIN_PATH)) || await pathExists2(path8.join(".opencode", PLUGIN_PATH));
2945
3189
  } catch {
2946
3190
  return false;
2947
3191
  }
2948
3192
  },
2949
- installEntries(home) {
3193
+ installEntries(home, env) {
2950
3194
  return [{
2951
3195
  kind: "file",
2952
- path: path7.join(home, OPENCODE_CONFIG, PLUGIN_PATH),
3196
+ path: path8.join(opencodeConfigDir(home, env), PLUGIN_PATH),
2953
3197
  content: opencodePluginContent()
2954
3198
  }];
2955
3199
  },
2956
- sourcePaths(home) {
2957
- return [
2958
- path7.join(home, ".local", "share", "opencode", "opencode.db"),
2959
- path7.join(home, ".opencode", "opencode.db")
2960
- ];
3200
+ sourcePaths(home, env) {
3201
+ return opencodeDataCandidates(home, env);
2961
3202
  },
2962
3203
  parseSessionFile: parseOpenCodeSessionFile
2963
3204
  };
2964
3205
  }
2965
3206
 
2966
3207
  // src/adapters/pi.ts
2967
- import { readFile as readFile4 } from "node:fs/promises";
2968
- import path8 from "node:path";
3208
+ import { readFile as readFile5 } from "node:fs/promises";
3209
+ import path9 from "node:path";
2969
3210
  async function parsePiSessionFile(filePath, options) {
2970
- const text = await readFile4(filePath, "utf8");
3211
+ const text = await readFile5(filePath, "utf8");
2971
3212
  const lines = text.split("\n").filter(Boolean);
2972
3213
  let sessionId;
2973
3214
  let cwd;
@@ -2996,7 +3237,7 @@ async function parsePiSessionFile(filePath, options) {
2996
3237
  sessionId = stringField(raw, "id") || state.sessionId;
2997
3238
  state.sessionId = sessionId || state.sessionId;
2998
3239
  cwd = stringField(raw, "cwd") || cwd;
2999
- project = cwd ? path8.basename(cwd) : project;
3240
+ project = cwd ? path9.basename(cwd) : project;
3000
3241
  continue;
3001
3242
  }
3002
3243
  if (entryType === "model_change") {
@@ -3391,36 +3632,49 @@ export default function (pi: ExtensionAPI) {
3391
3632
  }
3392
3633
  `;
3393
3634
  }
3635
+ function piAgentDir(home, env) {
3636
+ const override = env?.PI_CODING_AGENT_DIR;
3637
+ if (override && override.trim()) {
3638
+ return path9.resolve(override);
3639
+ }
3640
+ return path9.join(home, ".pi", "agent");
3641
+ }
3642
+ function piSessionDir(home, env) {
3643
+ const override = env?.PI_CODING_AGENT_SESSION_DIR;
3644
+ if (override && override.trim()) {
3645
+ return path9.resolve(override);
3646
+ }
3647
+ return path9.join(piAgentDir(home, env), "sessions");
3648
+ }
3394
3649
  function createPiAdapter() {
3395
- const PI_EXTENSIONS_DIR = ".pi/agent/extensions";
3396
3650
  return {
3397
3651
  id: "pi",
3398
3652
  label: "Pi",
3399
3653
  agentName: "pi",
3400
3654
  kind: "agent",
3401
- detectPath(home) {
3402
- return path8.join(home, ".pi", "agent");
3655
+ detectPath(home, env) {
3656
+ return piAgentDir(home, env);
3403
3657
  },
3404
- installedPath(home) {
3405
- return path8.join(home, PI_EXTENSIONS_DIR, "codetime.ts");
3658
+ installedPath(home, env) {
3659
+ return path9.join(piAgentDir(home, env), "extensions", "codetime.ts");
3406
3660
  },
3407
- async isInstalled(home) {
3661
+ async isInstalled(home, env) {
3408
3662
  try {
3409
3663
  const { pathExists: pathExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
3410
- return await pathExists2(path8.join(home, PI_EXTENSIONS_DIR, "codetime.ts"));
3664
+ return await pathExists2(path9.join(piAgentDir(home, env), "extensions", "codetime.ts"));
3411
3665
  } catch {
3412
3666
  return false;
3413
3667
  }
3414
3668
  },
3415
- installEntries(home) {
3669
+ installEntries(home, env) {
3416
3670
  return [{
3417
3671
  kind: "file",
3418
- path: path8.join(home, PI_EXTENSIONS_DIR, "codetime.ts"),
3672
+ path: path9.join(piAgentDir(home, env), "extensions", "codetime.ts"),
3419
3673
  content: piExtensionContent()
3420
3674
  }];
3421
3675
  },
3422
- sourcePaths(home) {
3423
- return [path8.join(home, ".pi", "agent", "sessions")];
3676
+ sourcePaths(home, env) {
3677
+ return [piSessionDir(home, env)];
3424
3678
  },
3425
3679
  parseSessionFile: parsePiSessionFile
3426
3680
  };
@@ -3699,457 +3953,78 @@ function buildSessionRollup(rollupKey, events) {
3699
3953
  fileRollup.linesAdded += fileLinesAdded;
3700
3954
  fileRollup.linesRemoved += fileLinesRemoved;
3701
3955
  if ((file.ts || event.ts) > fileRollup.lastTouchedAt) {
3702
- fileRollup.lastTouchedAt = file.ts || event.ts;
3703
- }
3704
- fileRollups.set(pathHash, fileRollup);
3705
- }
3706
- timeBuckets.set(bucketTs, bucket);
3707
- }
3708
- const baseRollup = {
3709
- rollupKey,
3710
- payloadHash: "",
3711
- source: first.source,
3712
- project,
3713
- sessionId,
3714
- agent,
3715
- startedAt,
3716
- lastEventAt,
3717
- eventCount: ordered.length,
3718
- promptCount,
3719
- turnCount,
3720
- toolCallCount,
3721
- commandCallCount,
3722
- inputTokens,
3723
- cachedInputTokens,
3724
- cacheCreationInputTokens,
3725
- cacheReadInputTokens,
3726
- outputTokens,
3727
- reasoningOutputTokens,
3728
- totalTokens,
3729
- linesAdded,
3730
- linesRemoved,
3731
- durationMs: Math.max(0, Date.parse(lastEventAt) - Date.parse(startedAt)),
3732
- timeBuckets: [...timeBuckets.values()].sort((a, b) => a.ts.localeCompare(b.ts)),
3733
- modelRollups: [...modelRollups.values()].sort((a, b) => b.callCount - a.callCount || a.model.localeCompare(b.model)),
3734
- toolRollups: [...toolRollups.values()].sort((a, b) => b.callCount - a.callCount || a.tool.localeCompare(b.tool)),
3735
- fileRollups: [...fileRollups.values()].sort((a, b) => b.writes - a.writes || b.reads - a.reads || a.displayPath.localeCompare(b.displayPath)),
3736
- turnRollups: [...turnRollups.values()].map((rollup) => ({
3737
- ...rollup,
3738
- durationMs: Math.max(0, Date.parse(rollup.lastEventAt) - Date.parse(rollup.startedAt))
3739
- })).sort((a, b) => a.startedAt.localeCompare(b.startedAt))
3740
- };
3741
- return {
3742
- ...baseRollup,
3743
- payloadHash: createPayloadHash(baseRollup)
3744
- };
3745
- }
3746
- function floorRollupBucket(ts) {
3747
- const ms = Date.parse(ts);
3748
- if (!Number.isFinite(ms)) {
3749
- return ts;
3750
- }
3751
- return new Date(Math.floor(ms / ROLLUP_BUCKET_MS) * ROLLUP_BUCKET_MS).toISOString();
3752
- }
3753
- function totalTokensFromEvent(event) {
3754
- const explicit = event.metrics?.tokensTotal;
3755
- if (typeof explicit === "number" && explicit > 0) {
3756
- return explicit;
3757
- }
3758
- return Math.max(0, event.metrics?.tokensInput || 0) + Math.max(0, event.metrics?.tokensOutput || 0) + Math.max(0, event.metrics?.tokensReasoningOutput || 0);
3759
- }
3760
- function lineStatsFromEvent(event) {
3761
- const files = event.fileActivities || [];
3762
- const fileLinesAdded = files.reduce((total, f) => total + (f.linesAdded || 0), 0);
3763
- const fileLinesRemoved = files.reduce((total, f) => total + (f.linesRemoved || 0), 0);
3764
- return {
3765
- linesAdded: Math.max(fileLinesAdded, event.metrics?.linesAdded || 0),
3766
- linesRemoved: Math.max(fileLinesRemoved, event.metrics?.linesRemoved || 0)
3767
- };
3768
- }
3769
- function eventDurationMs(event) {
3770
- return Math.max(
3771
- 0,
3772
- event.metrics?.durationMs || event.metrics?.commandDurationMs || event.metrics?.toolDurationMs || event.metrics?.modelDurationMs || 0
3773
- );
3774
- }
3775
-
3776
- // src/hooks/dispatch.ts
3777
- import { open, readFile as readFile5, stat as stat3 } from "node:fs/promises";
3778
- import path9 from "node:path";
3779
- var TRANSCRIPT_TAIL_BYTES = 512 * 1024;
3780
- function hookEventsFromPayload(agent, payload, options, enrichment = {}, adapters) {
3781
- const eventName = stringField(payload, "hook_event_name") || "agent.operation";
3782
- const tool = stringField(payload, "tool_name");
3783
- const cwd = stringField(payload, "cwd");
3784
- const model = stringField(payload, "model") || enrichment.model;
3785
- const project = stringOption(options.project) || (cwd ? path9.basename(cwd) : void 0);
3786
- const source = toAgentSource(agent);
3787
- const ts = (/* @__PURE__ */ new Date()).toISOString();
3788
- const toolUseId = stringField(payload, "tool_use_id") || stringField(payload, "toolUseId");
3789
- const turnId = stringField(payload, "turn_id") || stringField(payload, "turnId");
3790
- const sessionId = stringField(payload, "session_id") || stringField(payload, "sessionId");
3791
- const prompt = stringField(payload, "prompt");
3792
- const fileActivities = extractFileActivities(payload, tool, ts);
3793
- const lineMetrics = summarizeFileActivities(fileActivities);
3794
- const durationMs = numberField(payload, "duration_ms") || durationObjectToMs(objectField(payload, "duration"));
3795
- const command = stringField(objectField(payload, "tool_input"), "command");
3796
- const commandHash = tool === "Bash" && command ? createStableHash(command) : void 0;
3797
- const codexUsage = adapters?.tokenUsageFromPayload?.(payload);
3798
- const baseEvent = (type, extra = {}) => ({
3799
- schemaVersion: AGENT_TIME_SCHEMA_VERSION,
3800
- ts,
3801
- source,
3802
- agent,
3803
- type,
3804
- project,
3805
- cwd,
3806
- workspaceId: createWorkspaceId({ projectName: project, repoRoot: cwd }),
3807
- sessionId,
3808
- turnId,
3809
- tool,
3810
- model,
3811
- ...extra
3812
- });
3813
- const usageEvent = (metrics, extra = {}) => {
3814
- const total = (metrics.tokensInput || 0) + (metrics.tokensOutput || 0) + (metrics.tokensReasoningOutput || 0);
3815
- if (total <= 0) {
3816
- return void 0;
3817
- }
3818
- const finalMetrics = {
3819
- ...metrics,
3820
- tokensTotal: metrics.tokensTotal || total
3821
- };
3822
- const eventBase = baseEvent("model.usage", {
3823
- operation: "model usage",
3824
- confidence: extra.confidence ?? "partial",
3825
- metrics: finalMetrics,
3826
- refs: stringRefs({
3827
- sourceId: extra.messageId,
3828
- messageId: extra.messageId
3829
- })
3830
- });
3831
- const cost = estimateEventCostUsd(eventBase);
3832
- if (cost > 0) {
3833
- eventBase.metrics = { ...finalMetrics, costUsd: cost };
3834
- }
3835
- return eventBase;
3836
- };
3837
- switch (eventName) {
3838
- case "SessionStart": {
3839
- return [baseEvent("session.started", { operation: stringField(payload, "source") || "session start" })];
3840
- }
3841
- case "SessionEnd": {
3842
- return [baseEvent("session.ended", { operation: stringField(payload, "reason") || "session end" })];
3843
- }
3844
- case "UserPromptSubmit": {
3845
- return [
3846
- baseEvent("prompt.submitted", {
3847
- operation: "prompt submitted",
3848
- metrics: { prompts: 1, promptChars: prompt?.length },
3849
- refs: stringRefs({
3850
- promptHash: prompt ? `sha256:${createStableHash(prompt)}` : void 0
3851
- })
3852
- })
3853
- ];
3854
- }
3855
- case "PreToolUse": {
3856
- return [
3857
- baseEvent("tool.started", {
3858
- operation: tool ? `${tool} started` : eventName,
3859
- metrics: { toolCalls: 1 },
3860
- refs: stringRefs({ sourceId: toolUseId, commandHash })
3861
- })
3862
- ];
3863
- }
3864
- case "PermissionRequest": {
3865
- return [
3866
- baseEvent("permission.requested", {
3867
- operation: tool ? `${tool} permission requested` : "permission requested",
3868
- refs: stringRefs({ sourceId: toolUseId, commandHash })
3869
- })
3870
- ];
3871
- }
3872
- case "PermissionDenied": {
3873
- return [
3874
- baseEvent("permission.resolved", {
3875
- operation: tool ? `${tool} permission denied` : "permission denied",
3876
- success: false,
3877
- refs: stringRefs({ sourceId: toolUseId, commandHash })
3878
- })
3879
- ];
3880
- }
3881
- case "PostToolUse": {
3882
- const events = buildHookToolResultEvents({
3883
- baseEvent,
3884
- tool,
3885
- toolUseId,
3886
- commandHash,
3887
- durationMs,
3888
- success: true,
3889
- fileActivities,
3890
- lineMetrics
3891
- });
3892
- if (codexUsage) {
3893
- const usage = usageEvent({
3894
- ...codexUsage,
3895
- modelCalls: 1,
3896
- modelDurationMs: durationMs
3897
- }, { confidence: "exact" });
3898
- if (usage) {
3899
- events.push(usage);
3900
- }
3901
- }
3902
- return events;
3903
- }
3904
- case "PostToolUseFailure": {
3905
- return buildHookToolResultEvents({
3906
- baseEvent,
3907
- tool,
3908
- toolUseId,
3909
- commandHash,
3910
- durationMs,
3911
- success: false,
3912
- fileActivities,
3913
- lineMetrics,
3914
- error: stringField(payload, "error")
3915
- });
3916
- }
3917
- case "SubagentStart": {
3918
- return [
3919
- baseEvent("subagent.started", {
3920
- operation: "subagent started",
3921
- agentInstanceId: stringField(payload, "agent_id") || stringField(payload, "agentId")
3922
- })
3923
- ];
3924
- }
3925
- case "SubagentStop": {
3926
- return [
3927
- baseEvent("subagent.ended", {
3928
- operation: "subagent completed",
3929
- agentInstanceId: stringField(payload, "agent_id") || stringField(payload, "agentId"),
3930
- success: !payload.error
3931
- })
3932
- ];
3933
- }
3934
- case "Stop": {
3935
- const turnUsage = enrichment.turnUsage || codexUsage;
3936
- const turnMetrics = { durationMs };
3937
- if (turnUsage) {
3938
- Object.assign(turnMetrics, turnUsage);
3939
- if (!turnMetrics.modelCalls && enrichment.turnUsage?.modelCalls) {
3940
- turnMetrics.modelCalls = enrichment.turnUsage.modelCalls;
3941
- }
3942
- }
3943
- const events = [
3944
- baseEvent("turn.completed", { operation: "turn completed", metrics: turnMetrics })
3945
- ];
3946
- if (turnUsage) {
3947
- const usage = usageEvent(
3948
- { ...turnUsage, modelCalls: turnMetrics.modelCalls, modelDurationMs: durationMs },
3949
- { messageId: enrichment.lastUsageMessageId, confidence: enrichment.turnUsage ? "derived" : "exact" }
3950
- );
3951
- if (usage) {
3952
- events.push(usage);
3953
- }
3954
- }
3955
- return events;
3956
- }
3957
- case "StopFailure": {
3958
- return [baseEvent("turn.failed", { operation: "turn failed" })];
3959
- }
3960
- case "PostCompact": {
3961
- return [baseEvent("context.compacted", { operation: "context compacted" })];
3962
- }
3963
- default: {
3964
- return [];
3965
- }
3966
- }
3967
- }
3968
- async function enrichFromTranscript(payload, claudeUsageFromMessage2) {
3969
- const transcriptPath = stringField(payload, "transcript_path");
3970
- if (!transcriptPath) {
3971
- return {};
3972
- }
3973
- let text;
3974
- try {
3975
- const stats = await stat3(transcriptPath);
3976
- if (stats.size <= TRANSCRIPT_TAIL_BYTES) {
3977
- text = await readFile5(transcriptPath, "utf8");
3978
- } else {
3979
- const handle = await open(transcriptPath, "r");
3980
- try {
3981
- const buffer = Buffer.alloc(TRANSCRIPT_TAIL_BYTES);
3982
- await handle.read(buffer, 0, TRANSCRIPT_TAIL_BYTES, stats.size - TRANSCRIPT_TAIL_BYTES);
3983
- text = buffer.toString("utf8");
3984
- const newlineIndex = text.indexOf("\n");
3985
- if (newlineIndex !== -1) {
3986
- text = text.slice(newlineIndex + 1);
3987
- }
3988
- } finally {
3989
- await handle.close();
3990
- }
3991
- }
3992
- } catch (error) {
3993
- if (process.env.CODETIME_DEBUG || process.env.AGENT_TIME_DEBUG) {
3994
- process.stderr.write(`[codetime] enrichFromTranscript: failed to read transcript ${transcriptPath}: ${error.message}
3995
- `);
3996
- }
3997
- return {};
3998
- }
3999
- const lines = text.split("\n");
4000
- let model;
4001
- let lastUsage;
4002
- let lastUsageMessageId;
4003
- const turnUsage = {};
4004
- let turnCalls = 0;
4005
- for (let index = lines.length - 1; index >= 0; index -= 1) {
4006
- const raw = lines[index];
4007
- if (!raw) {
4008
- continue;
4009
- }
4010
- let entry;
4011
- try {
4012
- entry = JSON.parse(raw);
4013
- } catch {
4014
- continue;
4015
- }
4016
- if (!isPlainObject(entry)) {
4017
- continue;
4018
- }
4019
- const topType = stringField(entry, "type");
4020
- const message = objectField(entry, "message");
4021
- if (topType === "user") {
4022
- if (Object.keys(message).length > 0 && isClaudeToolResultMessage2(message)) {
4023
- continue;
4024
- }
4025
- break;
4026
- }
4027
- if (topType !== "assistant") {
4028
- continue;
4029
- }
4030
- if (!model) {
4031
- model = stringField(message, "model");
4032
- }
4033
- if (!claudeUsageFromMessage2) {
4034
- continue;
4035
- }
4036
- const usage = claudeUsageFromMessage2(message);
4037
- if (!usage) {
4038
- continue;
4039
- }
4040
- if (!lastUsage) {
4041
- lastUsage = usage;
4042
- lastUsageMessageId = stringField(message, "id");
4043
- }
4044
- for (const [key, value] of Object.entries(usage)) {
4045
- if (typeof value === "number") {
4046
- turnUsage[key] = (turnUsage[key] || 0) + value;
3956
+ fileRollup.lastTouchedAt = file.ts || event.ts;
4047
3957
  }
3958
+ fileRollups.set(pathHash, fileRollup);
4048
3959
  }
4049
- turnCalls += 1;
3960
+ timeBuckets.set(bucketTs, bucket);
4050
3961
  }
3962
+ const baseRollup = {
3963
+ rollupKey,
3964
+ payloadHash: "",
3965
+ source: first.source,
3966
+ project,
3967
+ sessionId,
3968
+ agent,
3969
+ startedAt,
3970
+ lastEventAt,
3971
+ eventCount: ordered.length,
3972
+ promptCount,
3973
+ turnCount,
3974
+ toolCallCount,
3975
+ commandCallCount,
3976
+ inputTokens,
3977
+ cachedInputTokens,
3978
+ cacheCreationInputTokens,
3979
+ cacheReadInputTokens,
3980
+ outputTokens,
3981
+ reasoningOutputTokens,
3982
+ totalTokens,
3983
+ linesAdded,
3984
+ linesRemoved,
3985
+ durationMs: Math.max(0, Date.parse(lastEventAt) - Date.parse(startedAt)),
3986
+ timeBuckets: [...timeBuckets.values()].sort((a, b) => a.ts.localeCompare(b.ts)),
3987
+ modelRollups: [...modelRollups.values()].sort((a, b) => b.callCount - a.callCount || a.model.localeCompare(b.model)),
3988
+ toolRollups: [...toolRollups.values()].sort((a, b) => b.callCount - a.callCount || a.tool.localeCompare(b.tool)),
3989
+ fileRollups: [...fileRollups.values()].sort((a, b) => b.writes - a.writes || b.reads - a.reads || a.displayPath.localeCompare(b.displayPath)),
3990
+ turnRollups: [...turnRollups.values()].map((rollup) => ({
3991
+ ...rollup,
3992
+ durationMs: Math.max(0, Date.parse(rollup.lastEventAt) - Date.parse(rollup.startedAt))
3993
+ })).sort((a, b) => a.startedAt.localeCompare(b.startedAt))
3994
+ };
4051
3995
  return {
4052
- model,
4053
- lastUsage,
4054
- lastUsageMessageId,
4055
- turnUsage: turnCalls > 0 ? { ...turnUsage, modelCalls: turnCalls } : void 0
3996
+ ...baseRollup,
3997
+ payloadHash: createPayloadHash(baseRollup)
4056
3998
  };
4057
3999
  }
4058
- function isClaudeToolResultMessage2(message) {
4059
- return Array.isArray(message.content) ? message.content.some(
4060
- (item) => isPlainObject(item) && item.type === "tool_result"
4061
- ) : false;
4062
- }
4063
- function extractFileActivities(payload, tool, ts) {
4064
- const changes = /* @__PURE__ */ new Map();
4065
- const toolInput = objectField(payload, "tool_input");
4066
- const toolResponse = objectField(payload, "tool_response");
4067
- const operation = operationForTool2(tool);
4068
- addPathActivity(changes, stringField(toolInput, "file_path") || stringField(toolInput, "path"), operation, ts);
4069
- addPathActivity(changes, stringField(toolResponse, "filePath") || stringField(toolResponse, "file_path"), operation, ts);
4070
- for (const file of arrayField3(toolInput, "files")) {
4071
- if (typeof file === "string") {
4072
- addPathActivity(changes, file, operation, ts);
4073
- }
4074
- }
4075
- const patch = stringField(toolInput, "patch") || patchFromCommand(stringField(toolInput, "command"));
4076
- if (patch) {
4077
- for (const change of parseApplyPatch(patch, ts)) {
4078
- const current = changes.get(change.path);
4079
- changes.set(change.path, mergeFileActivity(current, change));
4080
- }
4081
- }
4082
- return [...changes.values()];
4083
- }
4084
- function operationForTool2(tool) {
4085
- const normalized = (tool || "").toLowerCase();
4086
- if (["read", "notebookread", "view_image"].includes(normalized)) {
4087
- return "read";
4088
- }
4089
- if (["grep", "glob", "ls", "search", "rg"].includes(normalized)) {
4090
- return "search";
4091
- }
4092
- if (["write"].includes(normalized)) {
4093
- return "write";
4094
- }
4095
- if (["edit", "multiedit", "notebookedit", "apply_patch", "applypatch"].includes(normalized)) {
4096
- return "edit";
4000
+ function floorRollupBucket(ts) {
4001
+ const ms = Date.parse(ts);
4002
+ if (!Number.isFinite(ms)) {
4003
+ return ts;
4097
4004
  }
4098
- return "read";
4005
+ return new Date(Math.floor(ms / ROLLUP_BUCKET_MS) * ROLLUP_BUCKET_MS).toISOString();
4099
4006
  }
4100
- function arrayField3(object, key) {
4101
- if (!isPlainObject(object) || !Array.isArray(object[key])) {
4102
- return [];
4007
+ function totalTokensFromEvent(event) {
4008
+ const explicit = event.metrics?.tokensTotal;
4009
+ if (typeof explicit === "number" && explicit > 0) {
4010
+ return explicit;
4103
4011
  }
4104
- return object[key];
4012
+ return Math.max(0, event.metrics?.tokensInput || 0) + Math.max(0, event.metrics?.tokensOutput || 0) + Math.max(0, event.metrics?.tokensReasoningOutput || 0);
4105
4013
  }
4106
- function buildHookToolResultEvents(input) {
4107
- const refs = stringRefs({
4108
- sourceId: input.toolUseId,
4109
- commandHash: input.commandHash
4110
- });
4111
- const events = [
4112
- input.baseEvent(input.success ? "tool.completed" : "tool.failed", {
4113
- operation: input.tool ? `${input.tool} completed` : "tool completed",
4114
- success: input.success,
4115
- metrics: {
4116
- toolDurationMs: input.durationMs,
4117
- durationMs: input.durationMs
4118
- },
4119
- refs
4120
- })
4121
- ];
4122
- if (input.tool === "Bash") {
4123
- events.push(input.baseEvent(input.success ? "command.completed" : "command.failed", {
4124
- operation: "command completed",
4125
- success: input.success,
4126
- metrics: {
4127
- commandCalls: 1,
4128
- commandDurationMs: input.durationMs,
4129
- durationMs: input.durationMs
4130
- },
4131
- refs
4132
- }));
4133
- }
4134
- if (input.fileActivities.length > 0) {
4135
- events.push(input.baseEvent(eventTypeFromFileActivities(input.fileActivities), {
4136
- operation: input.tool ? `${input.tool} file activity` : "file activity",
4137
- success: input.success,
4138
- fileActivities: input.fileActivities,
4139
- metrics: input.lineMetrics,
4140
- refs
4141
- }));
4142
- }
4143
- return events;
4014
+ function lineStatsFromEvent(event) {
4015
+ const files = event.fileActivities || [];
4016
+ const fileLinesAdded = files.reduce((total, f) => total + (f.linesAdded || 0), 0);
4017
+ const fileLinesRemoved = files.reduce((total, f) => total + (f.linesRemoved || 0), 0);
4018
+ return {
4019
+ linesAdded: Math.max(fileLinesAdded, event.metrics?.linesAdded || 0),
4020
+ linesRemoved: Math.max(fileLinesRemoved, event.metrics?.linesRemoved || 0)
4021
+ };
4144
4022
  }
4145
- function toAgentSource(agent) {
4146
- if (agent === "claude") {
4147
- return "claude-code";
4148
- }
4149
- if (agent === "codex" || agent === "cursor" || agent === "opencode") {
4150
- return agent;
4151
- }
4152
- return agent || "unknown";
4023
+ function eventDurationMs(event) {
4024
+ return Math.max(
4025
+ 0,
4026
+ event.metrics?.durationMs || event.metrics?.commandDurationMs || event.metrics?.toolDurationMs || event.metrics?.modelDurationMs || 0
4027
+ );
4153
4028
  }
4154
4029
 
4155
4030
  // src/install/manager.ts
@@ -4165,7 +4040,7 @@ async function installEntry(entry, options) {
4165
4040
  });
4166
4041
  }
4167
4042
  async function mergeHooksJson(filePath, content, { dryRun, force, onWrite }) {
4168
- const { mkdir: mkdir3, writeFile: writeFile3 } = await import("node:fs/promises");
4043
+ const { mkdir: mkdir4, writeFile: writeFile3 } = await import("node:fs/promises");
4169
4044
  const pathMod = await import("node:path");
4170
4045
  if (dryRun) {
4171
4046
  onWrite(`Would merge ${filePath}`);
@@ -4186,7 +4061,7 @@ async function mergeHooksJson(filePath, content, { dryRun, force, onWrite }) {
4186
4061
  onWrite(`Already installed ${filePath}`);
4187
4062
  return;
4188
4063
  }
4189
- await mkdir3(pathMod.dirname(filePath), { recursive: true });
4064
+ await mkdir4(pathMod.dirname(filePath), { recursive: true });
4190
4065
  await writeFile3(filePath, nextText, "utf8");
4191
4066
  onWrite(`Installed ${filePath}`);
4192
4067
  }
@@ -4281,6 +4156,63 @@ function defaultMachineName() {
4281
4156
  // src/cli.ts
4282
4157
  init_fs();
4283
4158
 
4159
+ // src/lib/logger.ts
4160
+ import { appendFile, mkdir as mkdir2, rename, stat as stat4 } from "node:fs/promises";
4161
+ import { homedir as homedir2 } from "node:os";
4162
+ import path11 from "node:path";
4163
+ var MAX_BYTES = 1 * 1024 * 1024;
4164
+ function logDir(home = homedir2()) {
4165
+ return path11.join(home, ".codetime", "logs");
4166
+ }
4167
+ function logPath(home = homedir2(), name = "cli.log") {
4168
+ return path11.join(logDir(home), name);
4169
+ }
4170
+ function serializeError(error) {
4171
+ if (error instanceof Error) {
4172
+ return { message: error.message, stack: error.stack, name: error.name };
4173
+ }
4174
+ return { value: String(error) };
4175
+ }
4176
+ async function rotateIfNeeded(file) {
4177
+ try {
4178
+ const info = await stat4(file);
4179
+ if (info.size > MAX_BYTES) {
4180
+ await rename(file, `${file}.1`).catch(() => {
4181
+ });
4182
+ }
4183
+ } catch {
4184
+ }
4185
+ }
4186
+ async function writeLog(entry, home = homedir2(), fileName = "cli.log") {
4187
+ try {
4188
+ const dir = logDir(home);
4189
+ await mkdir2(dir, { recursive: true });
4190
+ const file = logPath(home, fileName);
4191
+ await rotateIfNeeded(file);
4192
+ const record = {
4193
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
4194
+ level: entry.level || (entry.error ? "error" : "info"),
4195
+ scope: entry.scope,
4196
+ pid: process.pid
4197
+ };
4198
+ if (entry.message) {
4199
+ record.message = entry.message;
4200
+ }
4201
+ if (entry.meta) {
4202
+ Object.assign(record, entry.meta);
4203
+ }
4204
+ if (entry.error !== void 0) {
4205
+ record.error = serializeError(entry.error);
4206
+ }
4207
+ await appendFile(file, `${JSON.stringify(record)}
4208
+ `);
4209
+ } catch {
4210
+ }
4211
+ }
4212
+ async function logError(scope, error, meta = {}, home = homedir2()) {
4213
+ await writeLog({ scope, error, meta, level: "error" }, home);
4214
+ }
4215
+
4284
4216
  // src/lib/progress.ts
4285
4217
  var BAR_WIDTH = 24;
4286
4218
  var ProgressBar = class {
@@ -4408,8 +4340,8 @@ function buildHeaders(token, machine) {
4408
4340
  ...machine?.platform ? { "x-machine-platform": machine.platform } : {}
4409
4341
  };
4410
4342
  }
4411
- function joinUrl(base, path12) {
4412
- return new URL(path12, base.endsWith("/") ? base : `${base}/`).toString();
4343
+ function joinUrl(base, path13) {
4344
+ return new URL(path13, base.endsWith("/") ? base : `${base}/`).toString();
4413
4345
  }
4414
4346
  async function postRollupBatch(remote, rollups, options = {}) {
4415
4347
  const response = await remote.fetchImpl(joinUrl(remote.baseUrl, "/v3/agent/ingest"), {
@@ -4473,6 +4405,9 @@ async function deleteMachine(remote, id) {
4473
4405
  return { deletedSessions: Number(body.deletedSessions) || 0 };
4474
4406
  }
4475
4407
 
4408
+ // src/lib/types.ts
4409
+ var BACKFILL_STATE_SCHEMA_VERSION = 3;
4410
+
4476
4411
  // src/cli.ts
4477
4412
  function createRegistry() {
4478
4413
  const registry = new AdapterRegistry();
@@ -4480,6 +4415,7 @@ function createRegistry() {
4480
4415
  registry.register(createClaudeCodeAdapter());
4481
4416
  registry.register(createPiAdapter());
4482
4417
  registry.register(createOpenCodeAdapter());
4418
+ registry.register(createAmpAdapter());
4483
4419
  return registry;
4484
4420
  }
4485
4421
  var defaultContext = {
@@ -4519,6 +4455,7 @@ ${helpText()}`);
4519
4455
  } catch (error) {
4520
4456
  write(ctx.stderr, `${error.message}
4521
4457
  `);
4458
+ await logError("cli", error, { argv });
4522
4459
  return 1;
4523
4460
  }
4524
4461
  }
@@ -4569,18 +4506,19 @@ function normalizeOptions(options) {
4569
4506
  }
4570
4507
  async function detectCommand(options, ctx, registry) {
4571
4508
  const home = resolveHome(options, ctx);
4509
+ const env = ctx.env;
4572
4510
  const adapters = registry.all();
4573
4511
  const targets = await Promise.all(adapters.map(async (adapter) => {
4574
- const detected = await pathExists(adapter.detectPath(home));
4575
- const installed = await adapter.isInstalled(home);
4512
+ const detected = await pathExists(adapter.detectPath(home, env));
4513
+ const installed = await adapter.isInstalled(home, env);
4576
4514
  return {
4577
4515
  id: adapter.id,
4578
4516
  label: adapter.label,
4579
4517
  kind: adapter.kind,
4580
4518
  detected,
4581
4519
  installed,
4582
- detectPath: adapter.detectPath(home),
4583
- installedPath: adapter.installedPath(home)
4520
+ detectPath: adapter.detectPath(home, env),
4521
+ installedPath: adapter.installedPath(home, env)
4584
4522
  };
4585
4523
  }));
4586
4524
  if (options.json) {
@@ -4597,6 +4535,7 @@ async function detectCommand(options, ctx, registry) {
4597
4535
  }
4598
4536
  async function installCommand(options, ctx, registry) {
4599
4537
  const home = resolveHome(options, ctx);
4538
+ const env = ctx.env;
4600
4539
  const dryRun = Boolean(options["dry-run"]);
4601
4540
  const force = Boolean(options.force);
4602
4541
  const allAdapters = registry.all();
@@ -4607,17 +4546,17 @@ async function installCommand(options, ctx, registry) {
4607
4546
  }
4608
4547
  const detected = [];
4609
4548
  for (const adapter of allAdapters) {
4610
- if (await pathExists(adapter.detectPath(home))) {
4549
+ if (await pathExists(adapter.detectPath(home, env))) {
4611
4550
  detected.push(adapter.id);
4612
4551
  }
4613
4552
  }
4614
4553
  const selectedIds = requested.length > 0 ? requested : options.all ? allAdapters.map((a) => a.id) : detected;
4615
4554
  if (selectedIds.length === 0) {
4616
- write(ctx.stderr, "No supported local targets were detected. Use --target codex,claude,opencode,pi or --all to create them.\n");
4555
+ write(ctx.stderr, "No supported local targets were detected. Use --target codex,claude,opencode,pi,amp or --all to create them.\n");
4617
4556
  return 1;
4618
4557
  }
4619
4558
  for (const adapter of allAdapters.filter((a) => selectedIds.includes(a.id))) {
4620
- for (const entry of adapter.installEntries(home)) {
4559
+ for (const entry of adapter.installEntries(home, env)) {
4621
4560
  await installEntry(entry, {
4622
4561
  dryRun,
4623
4562
  force,
@@ -4629,21 +4568,30 @@ async function installCommand(options, ctx, registry) {
4629
4568
  return 0;
4630
4569
  }
4631
4570
  async function hookCommand(options, ctx) {
4632
- const agent = requiredOption(options, "agent");
4633
- const payload = await readHookPayload(ctx.stdin);
4634
- const enrichment = await enrichFromTranscript(payload, claudeUsageFromMessage);
4635
- const events = hookEventsFromPayload(agent, payload, options, enrichment, { tokenUsageFromPayload });
4636
- assertValidEvents(events);
4637
- if (options["dry-run"]) {
4638
- write(ctx.stdout, `${JSON.stringify(events.length === 1 ? events[0] : events, null, 2)}
4571
+ const home = resolveHome(options, ctx);
4572
+ try {
4573
+ const agent = requiredOption(options, "agent");
4574
+ const payload = await readHookPayload(ctx.stdin);
4575
+ if (options["dry-run"]) {
4576
+ write(ctx.stdout, `${JSON.stringify({
4577
+ agent,
4578
+ received: payload,
4579
+ wouldTrigger: "backfill"
4580
+ }, null, 2)}
4581
+ `);
4582
+ return 0;
4583
+ }
4584
+ return await syncLocalTriggerCommand({
4585
+ ...options,
4586
+ agent,
4587
+ "min-interval": stringOption(options["min-interval"]) || String(DEFAULT_HOOK_SYNC_MIN_INTERVAL_SECONDS)
4588
+ }, ctx);
4589
+ } catch (error) {
4590
+ await logError("hook", error, { agent: stringOption(options.agent) }, home);
4591
+ debug(ctx, `[codetime] hook failed: ${error.message}
4639
4592
  `);
4640
4593
  return 0;
4641
4594
  }
4642
- return syncLocalTriggerCommand({
4643
- ...options,
4644
- agent,
4645
- "min-interval": stringOption(options["min-interval"]) || String(DEFAULT_HOOK_SYNC_MIN_INTERVAL_SECONDS)
4646
- }, ctx);
4647
4595
  }
4648
4596
  async function syncLocalTriggerCommand(options, ctx, _registry) {
4649
4597
  const home = resolveHome(options, ctx);
@@ -4704,6 +4652,9 @@ async function syncLocalRunnerCommand(options, ctx, _registry) {
4704
4652
  source: "all"
4705
4653
  }, ctx);
4706
4654
  return exitCode;
4655
+ } catch (error) {
4656
+ await logError("sync-local-runner", error, { home }, home);
4657
+ throw error;
4707
4658
  } finally {
4708
4659
  const nextState = await readSyncLocalTriggerState(statePath);
4709
4660
  nextState.lastStartedAt = state.lastStartedAt;
@@ -4746,13 +4697,14 @@ async function backfillCommand(options, ctx, registry) {
4746
4697
  }
4747
4698
  async function createBackfillPlanFromOptions(options, ctx, action, registry) {
4748
4699
  const home = resolveHome(options, ctx);
4700
+ const env = ctx.env;
4749
4701
  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) })) : (() => {
4702
+ const sourceDefs = source === "all" ? registry.all().map((a) => ({ id: a.id, label: a.label, paths: a.sourcePaths(home, env) })) : (() => {
4751
4703
  const adapter = registry.get(source);
4752
4704
  if (!adapter) {
4753
4705
  return [];
4754
4706
  }
4755
- return [{ id: adapter.id, label: adapter.label, paths: adapter.sourcePaths(home) }];
4707
+ return [{ id: adapter.id, label: adapter.label, paths: adapter.sourcePaths(home, env) }];
4756
4708
  })();
4757
4709
  if (sourceDefs.length === 0) {
4758
4710
  throw new Error(`Unknown backfill source: ${source}`);
@@ -4761,7 +4713,7 @@ async function createBackfillPlanFromOptions(options, ctx, action, registry) {
4761
4713
  const candidates = candidateList.flat();
4762
4714
  let events = [];
4763
4715
  if (action !== "discover") {
4764
- const eventList = await createBackfillEventsFromDefs(sourceDefs, options, registry);
4716
+ const eventList = await createBackfillEventsFromDefs(sourceDefs, options, registry, ctx);
4765
4717
  events = eventList.flat();
4766
4718
  }
4767
4719
  const plannedEvents = events.map((event) => ({
@@ -4811,21 +4763,36 @@ async function createBackfillPlanFromOptions(options, ctx, action, registry) {
4811
4763
  privacy: "metadata only; prompt text, command text, source code, and diffs are not imported"
4812
4764
  };
4813
4765
  }
4814
- async function createBackfillEventsFromDefs(sourceDefs, options, registry, overrideFiles) {
4766
+ async function createBackfillEventsFromDefs(sourceDefs, options, registry, ctx, overrideFiles) {
4815
4767
  const events = [];
4768
+ const home = resolveHome(options, ctx);
4816
4769
  for (const item of sourceDefs) {
4817
4770
  const parser = registry.getParser(item.id);
4818
4771
  if (!parser) {
4819
4772
  continue;
4820
4773
  }
4821
- const sourceFiles = await listBackfillSourceFiles(item, options);
4822
- const files = overrideFiles ?? sourceFiles.map((f) => f.path);
4774
+ let files;
4775
+ try {
4776
+ const sourceFiles = await listBackfillSourceFiles(item, options, ctx);
4777
+ files = overrideFiles ?? sourceFiles.map((f) => f.path);
4778
+ } catch (error) {
4779
+ await logError("backfill.listFiles", error, { source: item.id, phase: "plan" }, home);
4780
+ debug(ctx, `[codetime] skip ${item.id} in plan: list files failed: ${error.message}
4781
+ `);
4782
+ continue;
4783
+ }
4823
4784
  for (const filePath of files) {
4824
- const parsed = await parser(filePath, options);
4825
- for (const event of parsed) {
4826
- if (matchesBackfillFilters(event, options)) {
4827
- events.push(event);
4785
+ try {
4786
+ const parsed = await parser(filePath, options);
4787
+ for (const event of parsed) {
4788
+ if (matchesBackfillFilters(event, options)) {
4789
+ events.push(event);
4790
+ }
4828
4791
  }
4792
+ } catch (error) {
4793
+ await logError("backfill.parse", error, { source: item.id, file: filePath, phase: "plan" }, home);
4794
+ debug(ctx, `[codetime] skip ${item.id} file ${filePath} in plan: ${error.message}
4795
+ `);
4829
4796
  }
4830
4797
  }
4831
4798
  }
@@ -4846,15 +4813,18 @@ async function createBackfillCandidates(source, options) {
4846
4813
  };
4847
4814
  }));
4848
4815
  }
4849
- async function listBackfillSourceFiles(source, options) {
4816
+ async function listBackfillSourceFiles(source, options, ctx) {
4850
4817
  if (source.id === "opencode") {
4851
- return opencodeBackfillFiles(stringOption(options["source-root"]));
4818
+ return opencodeBackfillFiles(stringOption(options["source-root"]), resolveHome(options, ctx), ctx.env);
4819
+ }
4820
+ if (source.id === "amp") {
4821
+ return ampBackfillFiles(stringOption(options["source-root"]), resolveHome(options, ctx), ctx.env);
4852
4822
  }
4853
4823
  const roots = stringOption(options["source-root"]) ? [requiredOption(options, "source-root")] : source.paths;
4854
4824
  const fileLists = await Promise.all(roots.map((r) => listJsonlFiles(r)));
4855
4825
  const files = fileLists.flat().sort().slice(0, numberOption(options.limit) || void 0);
4856
4826
  return Promise.all(files.map(async (filePath) => {
4857
- const info = await stat4(filePath);
4827
+ const info = await stat5(filePath);
4858
4828
  return { path: filePath, modifiedAt: info.mtime.toISOString() };
4859
4829
  }));
4860
4830
  }
@@ -4936,119 +4906,142 @@ async function importBackfillPlan(plan, options, ctx, registry) {
4936
4906
  write(ctx.stderr, "Only Codex, Claude Code, OpenCode, and Pi backfill import are implemented.\n");
4937
4907
  return 1;
4938
4908
  }
4939
- const allAdapters = registry.all();
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) }));
4909
+ 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
4910
  if (options.force) {
4942
- try {
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
- }
4911
+ await purgeForcedSources(sourceDefs, home, options, ctx);
4958
4912
  }
4959
- const incrementalState = shouldUseIncrementalBackfill(options) ? await readBackfillIncrementalState(home) : void 0;
4960
- const selectedFilesBySource = /* @__PURE__ */ new Map();
4961
- const canonicalEvents = [];
4913
+ const incrementalState = shouldUseIncrementalBackfill(options) ? await readBackfillIncrementalState(home, ctx) : void 0;
4962
4914
  if (!options.json) {
4963
4915
  write(ctx.stdout, `importRun ${plan.importRun.importRunId}
4964
4916
  `);
4965
4917
  write(ctx.stdout, `sources ${sourceDefs.map((s) => s.id).join(", ") || "none"}
4966
4918
  `);
4967
4919
  }
4920
+ const { canonicalEvents, selectedFilesBySource } = await collectCanonicalEvents(
4921
+ sourceDefs,
4922
+ registry,
4923
+ incrementalState,
4924
+ options,
4925
+ ctx
4926
+ );
4927
+ const rollups = buildSessionRollups(canonicalEvents);
4928
+ const counts = await uploadSessionRollups(rollups, canonicalEvents.length, options, ctx);
4929
+ const result = {
4930
+ importRunId: plan.importRun.importRunId,
4931
+ source,
4932
+ planned: rollups.length,
4933
+ sourceEvents: canonicalEvents.length,
4934
+ ...counts
4935
+ };
4936
+ if (options.json) {
4937
+ write(ctx.stdout, `${JSON.stringify(result, null, 2)}
4938
+ `);
4939
+ }
4940
+ if (counts.failed === 0 && counts.conflicts === 0 && incrementalState) {
4941
+ await updateBackfillIncrementalState(home, incrementalState, selectedFilesBySource);
4942
+ }
4943
+ return counts.failed > 0 || counts.conflicts > 0 && !options["skip-conflicts"] ? 1 : 0;
4944
+ }
4945
+ async function purgeForcedSources(sourceDefs, home, options, ctx) {
4946
+ try {
4947
+ await rm(backfillIncrementalStatePath(home), { force: true });
4948
+ } catch (error) {
4949
+ debug(ctx, `Failed to clear backfill watermark: ${error.message}
4950
+ `);
4951
+ }
4952
+ for (const item of sourceDefs) {
4953
+ try {
4954
+ const deleted = await deleteSessionRollupsBySourceAPI(item.id, options, ctx);
4955
+ if (!options.json) {
4956
+ write(ctx.stdout, `purged ${item.id}: ${deleted} old rollups
4957
+ `);
4958
+ }
4959
+ } catch (error) {
4960
+ debug(ctx, `Failed to purge ${item.id} rollups: ${error.message}
4961
+ `);
4962
+ }
4963
+ }
4964
+ }
4965
+ async function collectCanonicalEvents(sourceDefs, registry, incrementalState, options, ctx) {
4966
+ const selectedFilesBySource = /* @__PURE__ */ new Map();
4967
+ const canonicalEvents = [];
4968
+ const home = resolveHome(options, ctx);
4968
4969
  for (const item of sourceDefs) {
4969
4970
  const parser = registry.getParser(item.id);
4970
4971
  if (!parser) {
4971
4972
  continue;
4972
4973
  }
4973
- const sourceFiles = await listBackfillSourceFiles(item, options);
4974
- const selectedFiles = selectBackfillFilesForImport(sourceFiles, incrementalState?.sources[item.id]?.watermarkTs);
4974
+ let selectedFiles;
4975
+ try {
4976
+ const sourceFiles = await listBackfillSourceFiles(item, options, ctx);
4977
+ selectedFiles = selectBackfillFilesForImport(sourceFiles, incrementalState?.sources[item.id]?.watermarkTs);
4978
+ } catch (error) {
4979
+ await logError("backfill.listFiles", error, { source: item.id }, home);
4980
+ debug(ctx, `[codetime] skip ${item.id}: list files failed: ${error.message}
4981
+ `);
4982
+ continue;
4983
+ }
4975
4984
  selectedFilesBySource.set(item.id, selectedFiles);
4976
4985
  const filePaths = selectedFiles.map((f) => f.path);
4977
4986
  const sourceEvents = [];
4978
- if (options.json) {
4979
- for (const filePath of filePaths) {
4980
- const parsed = await parser(filePath, options);
4981
- for (const event of parsed) {
4982
- if (matchesBackfillFilters(event, options)) {
4983
- sourceEvents.push(event);
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) {
4987
+ let sourceFailed = false;
4988
+ const bar = options.json ? void 0 : 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
+ try {
4991
4992
  const parsed = await parser(filePaths[fi], options);
4992
4993
  for (const event of parsed) {
4993
4994
  if (matchesBackfillFilters(event, options)) {
4994
4995
  sourceEvents.push(event);
4995
4996
  }
4996
4997
  }
4997
- bar.tick(`${fi + 1}/${filePaths.length} files, ${sourceEvents.length} events`);
4998
+ } catch (error) {
4999
+ sourceFailed = true;
5000
+ await logError("backfill.parse", error, { source: item.id, file: filePaths[fi] }, home);
5001
+ debug(ctx, `[codetime] skip ${item.id} file ${filePaths[fi]}: ${error.message}
5002
+ `);
4998
5003
  }
4999
- bar.finalize(`${sourceEvents.length} events`);
5004
+ bar?.tick(`${fi + 1}/${filePaths.length} files, ${sourceEvents.length} events`);
5005
+ }
5006
+ bar?.finalize(`${sourceEvents.length} events${sourceFailed ? " (partial \u2014 see logs)" : ""}`);
5007
+ if (sourceFailed) {
5008
+ selectedFilesBySource.delete(item.id);
5000
5009
  }
5001
5010
  for (const event of sourceEvents) canonicalEvents.push(event);
5002
5011
  }
5012
+ return { canonicalEvents, selectedFilesBySource };
5013
+ }
5014
+ async function uploadSessionRollups(rollups, eventCount, options, ctx) {
5003
5015
  const counts = { inserted: 0, skipped: 0, conflicts: 0, failed: 0 };
5004
- const rollups = buildSessionRollups(canonicalEvents);
5005
5016
  const batchSize = Math.max(1, Math.floor(numberOption(options["batch-size"]) || DEFAULT_BACKFILL_BATCH_SIZE));
5006
5017
  const batchBytes = Math.max(64 * 1024, Math.floor(numberOption(options["batch-bytes"]) || DEFAULT_BACKFILL_BATCH_BYTES));
5007
5018
  const batches = packRollupBatches(rollups, batchSize, batchBytes);
5008
5019
  const totalBatches = batches.length;
5009
5020
  let uploadBar;
5010
5021
  if (!options.json) {
5011
- write(ctx.stdout, `rollup ${rollups.length} from ${canonicalEvents.length} events
5022
+ write(ctx.stdout, `rollup ${rollups.length} from ${eventCount} events
5012
5023
  `);
5013
5024
  uploadBar = new ProgressBar(ctx.stdout, `upload`.padEnd(12));
5014
5025
  uploadBar.init(totalBatches, `0/${totalBatches} batches, 0 inserted`);
5015
5026
  }
5016
- for (let i = 0; i < batches.length; i += 1) {
5017
- const batch = batches[i];
5027
+ for (const [i, batch_] of batches.entries()) {
5028
+ const batch = batch_;
5018
5029
  const batchNumber = i + 1;
5019
5030
  try {
5020
- const result2 = await sendSessionRollupBatch(batch, options, ctx);
5021
- counts.inserted += result2.inserted;
5022
- counts.skipped += result2.skipped;
5023
- counts.conflicts += result2.conflicts;
5024
- counts.failed += result2.failed;
5031
+ const result = await sendSessionRollupBatch(batch, options, ctx);
5032
+ counts.inserted += result.inserted;
5033
+ counts.skipped += result.skipped;
5034
+ counts.conflicts += result.conflicts;
5035
+ counts.failed += result.failed;
5025
5036
  } catch (error) {
5026
5037
  debug(ctx, `backfill rollup batch ${batchNumber}/${totalBatches} (${batch.length} rollups) failed: ${error.message}
5027
5038
  `);
5028
5039
  counts.failed += batch.length;
5029
5040
  }
5030
- if (uploadBar) {
5031
- uploadBar.update(batchNumber, `${batchNumber}/${totalBatches} batches, inserted ${counts.inserted}`);
5032
- }
5033
- }
5034
- if (uploadBar) {
5035
- uploadBar.finalize(`inserted ${counts.inserted} \xB7 skipped ${counts.skipped}${counts.conflicts ? ` \xB7 conflicts ${counts.conflicts}` : ""}${counts.failed ? ` \xB7 failed ${counts.failed}` : ""}`);
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);
5041
+ uploadBar?.update(batchNumber, `${batchNumber}/${totalBatches} batches, inserted ${counts.inserted}`);
5050
5042
  }
5051
- return counts.failed > 0 || counts.conflicts > 0 && !options["skip-conflicts"] ? 1 : 0;
5043
+ uploadBar?.finalize(`inserted ${counts.inserted} \xB7 skipped ${counts.skipped}${counts.conflicts ? ` \xB7 conflicts ${counts.conflicts}` : ""}${counts.failed ? ` \xB7 failed ${counts.failed}` : ""}`);
5044
+ return counts;
5052
5045
  }
5053
5046
  function backfillVerifyCommand(options, ctx) {
5054
5047
  const importRunId = stringOption(options["import-run"]);
@@ -5072,12 +5065,7 @@ ${result.message}
5072
5065
  return 0;
5073
5066
  }
5074
5067
  async function sendSessionRollupBatch(rollups, options, ctx) {
5075
- const remote = resolveRemote({
5076
- apiUrl: stringOption(options["api-url"]),
5077
- token: stringOption(options.token),
5078
- env: ctx.env,
5079
- fetch: ctx.fetch
5080
- });
5068
+ const remote = resolveRemoteFromOptions(options, ctx);
5081
5069
  if (!remote) {
5082
5070
  throw new Error("No fetch available for HTTP upload");
5083
5071
  }
@@ -5096,12 +5084,7 @@ async function sendSessionRollupBatch(rollups, options, ctx) {
5096
5084
  return result;
5097
5085
  }
5098
5086
  async function deleteSessionRollupsBySourceAPI(source, options, ctx) {
5099
- const remote = resolveRemote({
5100
- apiUrl: stringOption(options["api-url"]),
5101
- token: stringOption(options.token),
5102
- env: ctx.env,
5103
- fetch: ctx.fetch
5104
- });
5087
+ const remote = resolveRemoteFromOptions(options, ctx);
5105
5088
  if (!remote) {
5106
5089
  throw new Error("No fetch available for HTTP delete");
5107
5090
  }
@@ -5130,7 +5113,9 @@ function packRollupBatches(rollups, maxCount, maxBytes) {
5130
5113
  current.push(rollup);
5131
5114
  currentBytes += size;
5132
5115
  }
5133
- if (current.length > 0) batches.push(current);
5116
+ if (current.length > 0) {
5117
+ batches.push(current);
5118
+ }
5134
5119
  return batches;
5135
5120
  }
5136
5121
  function selectBackfillFilesForImport(files, watermarkTs) {
@@ -5147,27 +5132,42 @@ function selectBackfillFilesForImport(files, watermarkTs) {
5147
5132
  });
5148
5133
  }
5149
5134
  function backfillIncrementalStatePath(home) {
5150
- return path11.join(home, ".codetime", "backfill-state.json");
5135
+ return path12.join(home, ".codetime", "backfill-state.json");
5151
5136
  }
5152
5137
  function syncLocalTriggerStatePath(home) {
5153
- return path11.join(home, ".codetime", "sync-local-trigger.json");
5138
+ return path12.join(home, ".codetime", "sync-local-trigger.json");
5154
5139
  }
5155
5140
  function syncLocalTriggerLockPath(home) {
5156
- return path11.join(home, ".codetime", "sync-local-trigger.lock");
5141
+ return path12.join(home, ".codetime", "sync-local-trigger.lock");
5157
5142
  }
5158
- async function readBackfillIncrementalState(home) {
5159
- const state = await readJsonIfExists(backfillIncrementalStatePath(home));
5143
+ async function readBackfillIncrementalState(home, ctx) {
5144
+ const statePath = backfillIncrementalStatePath(home);
5145
+ const state = await readJsonIfExists(statePath);
5146
+ if (state === null) {
5147
+ return { version: BACKFILL_STATE_SCHEMA_VERSION, sources: {} };
5148
+ }
5160
5149
  if (!isPlainObject(state) || !isPlainObject(state.sources)) {
5161
- return { version: 1, sources: {} };
5150
+ if (ctx) {
5151
+ debug(ctx, `backfill-state malformed at ${statePath}; ignoring watermarks
5152
+ `);
5153
+ }
5154
+ return { version: BACKFILL_STATE_SCHEMA_VERSION, sources: {} };
5155
+ }
5156
+ if (state.version !== BACKFILL_STATE_SCHEMA_VERSION) {
5157
+ if (ctx) {
5158
+ 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
5159
+ `);
5160
+ }
5161
+ return { version: BACKFILL_STATE_SCHEMA_VERSION, sources: {} };
5162
5162
  }
5163
5163
  const sources = {};
5164
- for (const source of ["codex", "claude-code", "opencode", "pi"]) {
5164
+ for (const source of ["codex", "claude-code", "opencode", "pi", "amp"]) {
5165
5165
  const item = state.sources[source];
5166
5166
  if (isPlainObject(item) && typeof item.watermarkTs === "string" && !Number.isNaN(Date.parse(item.watermarkTs))) {
5167
5167
  sources[source] = { watermarkTs: item.watermarkTs };
5168
5168
  }
5169
5169
  }
5170
- return { version: 1, sources };
5170
+ return { version: BACKFILL_STATE_SCHEMA_VERSION, sources };
5171
5171
  }
5172
5172
  async function updateBackfillIncrementalState(home, state, selectedFilesBySource) {
5173
5173
  for (const [source, files] of selectedFilesBySource.entries()) {
@@ -5178,7 +5178,7 @@ async function updateBackfillIncrementalState(home, state, selectedFilesBySource
5178
5178
  state.sources[source] = { watermarkTs: latest };
5179
5179
  }
5180
5180
  const statePath = backfillIncrementalStatePath(home);
5181
- await mkdir2(path11.dirname(statePath), { recursive: true });
5181
+ await mkdir3(path12.dirname(statePath), { recursive: true });
5182
5182
  await writeFile2(statePath, `${JSON.stringify(state, null, 2)}
5183
5183
  `, "utf8");
5184
5184
  }
@@ -5206,7 +5206,7 @@ async function readSyncLocalTriggerState(statePath) {
5206
5206
  return nextState;
5207
5207
  }
5208
5208
  async function writeSyncLocalTriggerState(statePath, state) {
5209
- await mkdir2(path11.dirname(statePath), { recursive: true });
5209
+ await mkdir3(path12.dirname(statePath), { recursive: true });
5210
5210
  await writeFile2(statePath, `${JSON.stringify(state, null, 2)}
5211
5211
  `, "utf8");
5212
5212
  }
@@ -5221,7 +5221,7 @@ async function readSyncLocalLock(lockPath) {
5221
5221
  return { pid: lock.pid, startedAt: lock.startedAt };
5222
5222
  }
5223
5223
  async function writeSyncLocalLock(lockPath, lock) {
5224
- await mkdir2(path11.dirname(lockPath), { recursive: true });
5224
+ await mkdir3(path12.dirname(lockPath), { recursive: true });
5225
5225
  await writeFile2(lockPath, `${JSON.stringify(lock, null, 2)}
5226
5226
  `, "utf8");
5227
5227
  }
@@ -5281,10 +5281,10 @@ function syncLocalRunnerEntryArgs(cliPath) {
5281
5281
  if (cliPath.endsWith(".ts")) {
5282
5282
  return ["--import", "tsx", cliPath];
5283
5283
  }
5284
- return [path11.resolve(path11.dirname(cliPath), "../bin/codetime.mjs")];
5284
+ return [path12.resolve(path12.dirname(cliPath), "../bin/codetime.mjs")];
5285
5285
  }
5286
5286
  function resolveHome(options, ctx) {
5287
- return path11.resolve(stringOption(options.home) || ctx.env.HOME || os3.homedir());
5287
+ return path12.resolve(stringOption(options.home) || ctx.env.HOME || os3.homedir());
5288
5288
  }
5289
5289
  function requestedTargets(options) {
5290
5290
  const value = options.target || options.targets;
@@ -5303,15 +5303,6 @@ function requiredOption(options, name) {
5303
5303
  }
5304
5304
  return value;
5305
5305
  }
5306
- function assertValidEvent(event) {
5307
- const validation = validateCanonicalEvent(event);
5308
- if (!validation.valid) {
5309
- throw new Error(`Invalid event: ${validation.errors.join("; ")}`);
5310
- }
5311
- }
5312
- function assertValidEvents(events) {
5313
- for (const event of events) assertValidEvent(event);
5314
- }
5315
5306
  async function readHookPayload(stdin) {
5316
5307
  const text = await readAll(stdin);
5317
5308
  if (!text.trim()) {
@@ -5339,6 +5330,15 @@ function debug(ctx, message) {
5339
5330
  write(ctx.stderr, message);
5340
5331
  }
5341
5332
  }
5333
+ function resolveRemoteFromOptions(options, ctx) {
5334
+ return resolveRemote({
5335
+ apiUrl: stringOption(options["api-url"]),
5336
+ token: stringOption(options.token),
5337
+ env: ctx.env,
5338
+ fetch: ctx.fetch,
5339
+ homeOverride: stringOption(options.home)
5340
+ });
5341
+ }
5342
5342
  function maskToken(token) {
5343
5343
  if (token.length <= 8) {
5344
5344
  return "***";
@@ -5399,12 +5399,7 @@ Usage: codetime token [set <token>|show|clear] [--remote <url>]
5399
5399
  return 1;
5400
5400
  }
5401
5401
  async function machineCommand(action, options, ctx) {
5402
- const remote = resolveRemote({
5403
- apiUrl: stringOption(options["api-url"]),
5404
- token: stringOption(options.token),
5405
- env: ctx.env,
5406
- fetch: ctx.fetch
5407
- });
5402
+ const remote = resolveRemoteFromOptions(options, ctx);
5408
5403
  if (!remote || !remote.token) {
5409
5404
  write(ctx.stderr, "machine command requires login (run `codetime login`).\n");
5410
5405
  return 1;
@@ -5485,14 +5480,11 @@ Global options:
5485
5480
  --token <token> Bearer token for this invocation (overrides config).
5486
5481
 
5487
5482
  Token precedence (highest first):
5488
- --token flag > CODETIME_AGENT_TOKEN env > AGENT_TIME_API_TOKEN env
5489
- (legacy alias) > saved config (~/.codetime/config.json).
5483
+ --token flag > CODETIME_TOKEN env > saved config (~/.codetime/config.json).
5490
5484
 
5491
5485
  Environment:
5492
5486
  CODETIME_API_URL Defaults to ${DEFAULT_API_URL}
5493
- CODETIME_AGENT_TOKEN Bearer token for the API
5494
- AGENT_TIME_API_URL Legacy alias for CODETIME_API_URL
5495
- AGENT_TIME_API_TOKEN Legacy alias for CODETIME_AGENT_TOKEN
5487
+ CODETIME_TOKEN Bearer token for the API
5496
5488
  `;
5497
5489
  }
5498
5490
  export {