codetime-cli 0.3.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 +767 -840
  2. package/package.json +2 -2
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") {
@@ -143,14 +150,11 @@ var init_fs = __esm({
143
150
  }
144
151
  });
145
152
 
146
- // src/lib/types.ts
147
- var BACKFILL_STATE_SCHEMA_VERSION = 2;
148
-
149
153
  // src/cli.ts
150
154
  import { spawn } from "node:child_process";
151
- 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";
152
156
  import os3 from "node:os";
153
- import path11 from "node:path";
157
+ import path12 from "node:path";
154
158
  import { fileURLToPath } from "node:url";
155
159
 
156
160
  // ../shared/src/index.ts
@@ -963,231 +967,17 @@ var CAC = class extends EventTarget {
963
967
  };
964
968
  var cac = (name = "") => new CAC(name);
965
969
 
966
- // src/adapters/claude-code.ts
970
+ // src/adapters/amp.ts
967
971
  import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
968
- import os from "node:os";
969
- import path5 from "node:path";
970
-
971
- // src/lib/activity.ts
972
972
  import path2 from "node:path";
973
973
 
974
- // src/lib/shell.ts
975
- import path from "node:path";
976
- function fileActivitiesFromShellCommand(command, ts, rootCwd, initialCwd) {
977
- const activities = [];
978
- let currentCwd = initialCwd;
979
- for (const segment of command.split(/\s*(?:&&|\|\||\||;)\s*/)) {
980
- const words = shellWords(segment);
981
- if (words.length === 0) {
982
- continue;
983
- }
984
- const commandName = path.basename(words[0]);
985
- if (commandName === "cd" && words[1]) {
986
- currentCwd = resolvePath(words[1], currentCwd);
987
- continue;
988
- }
989
- for (const item of shellReadTargets(words, currentCwd)) {
990
- activities.push({
991
- ts,
992
- path: displayActivityPath(item.path, rootCwd, currentCwd),
993
- operation: item.operation,
994
- confidence: "derived"
995
- });
996
- }
997
- }
998
- return activities;
999
- }
1000
- function shellReadTargets(words, cwd) {
1001
- const command = path.basename(words[0]).toLowerCase();
1002
- if (["cat", "less", "more", "nl"].includes(command)) {
1003
- return fileArgs(words.slice(1), /* @__PURE__ */ new Set()).map((item) => ({ path: item, operation: "read" }));
1004
- }
1005
- if (command === "sed") {
1006
- const args = commandArgs(words.slice(1), /* @__PURE__ */ new Set(["-e", "--expression", "-f", "--file"]));
1007
- return args.slice(1).filter(looksLikePathArg).map((item) => ({ path: item, operation: "read" }));
1008
- }
1009
- if (command === "head" || command === "tail") {
1010
- return fileArgs(words.slice(1), /* @__PURE__ */ new Set(["-n", "-c", "--lines", "--bytes"])).map((item) => ({ path: item, operation: "read" }));
1011
- }
1012
- if (["rg", "grep", "ag"].includes(command)) {
1013
- const hasPatternOption = words.includes("-e") || words.includes("--regexp") || words.includes("-f") || words.includes("--file");
1014
- const args = commandArgs(words.slice(1), /* @__PURE__ */ new Set(["-e", "--regexp", "-f", "--file", "-g", "--glob", "-t", "-T", "-A", "-B", "-C", "-m"]));
1015
- const pathArgs = hasPatternOption ? args : args.slice(1);
1016
- return pathArgs.filter(looksLikePathArg).map((item) => ({ path: item, operation: "search" }));
1017
- }
1018
- if (command === "find") {
1019
- const args = fileArgs(words.slice(1), /* @__PURE__ */ new Set());
1020
- return args.slice(0, 1).map((item) => ({ path: item, operation: "search" }));
1021
- }
1022
- if (command === "pwd" && cwd) {
1023
- return [{ path: cwd, operation: "search" }];
1024
- }
1025
- return [];
1026
- }
1027
- function fileArgs(args, optionsWithValues) {
1028
- return commandArgs(args, optionsWithValues).filter(looksLikePathArg);
1029
- }
1030
- function commandArgs(args, optionsWithValues) {
1031
- const result = [];
1032
- for (let index = 0; index < args.length; index += 1) {
1033
- const arg = args[index];
1034
- if (!arg || arg === "--") {
1035
- continue;
1036
- }
1037
- if (arg.startsWith("-")) {
1038
- if (optionsWithValues.has(arg)) {
1039
- index += 1;
1040
- }
1041
- continue;
1042
- }
1043
- result.push(arg);
1044
- }
1045
- return result;
1046
- }
1047
- function looksLikePathArg(value) {
1048
- if (!value || value.startsWith("$")) {
1049
- return false;
1050
- }
1051
- if ([">", ">>", "<", "2>", "2>>"].includes(value)) {
1052
- return false;
1053
- }
1054
- return path.isAbsolute(value) || value.includes("/") || value.includes(".");
1055
- }
1056
- function shellWords(command) {
1057
- const matches = command.match(/"([^"\\]|\\.)*"|'[^']*'|\S+/g) || [];
1058
- return matches.map((word) => {
1059
- if (word.startsWith('"') && word.endsWith('"') || word.startsWith("'") && word.endsWith("'")) {
1060
- return word.slice(1, -1);
1061
- }
1062
- return word;
1063
- });
1064
- }
1065
- function resolvePath(filePath, cwd) {
1066
- if (path.isAbsolute(filePath) || !cwd) {
1067
- return filePath;
1068
- }
1069
- return path.resolve(cwd, filePath);
1070
- }
1071
- function displayFilePath(filePath, cwd) {
1072
- if (!cwd || !path.isAbsolute(filePath)) {
1073
- return filePath;
1074
- }
1075
- const relative = path.relative(cwd, filePath);
1076
- return relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : filePath;
1077
- }
1078
- function displayActivityPath(filePath, rootCwd, currentCwd) {
1079
- if (path.isAbsolute(filePath)) {
1080
- return displayFilePath(filePath, rootCwd);
1081
- }
1082
- if (currentCwd && path.isAbsolute(currentCwd)) {
1083
- return displayFilePath(path.resolve(currentCwd, filePath), rootCwd);
1084
- }
1085
- return filePath;
1086
- }
1087
-
1088
- // src/lib/activity.ts
1089
- function operationForTool(tool) {
1090
- const normalized = (tool || "").toLowerCase();
1091
- if (["read", "notebookread", "view_image"].includes(normalized)) {
1092
- return "read";
1093
- }
1094
- if (["grep", "glob", "ls", "search", "rg"].includes(normalized)) {
1095
- return "search";
1096
- }
1097
- if (["write"].includes(normalized)) {
1098
- return "write";
1099
- }
1100
- if (["edit", "multiedit", "notebookedit", "apply_patch", "applypatch"].includes(normalized)) {
1101
- return "edit";
1102
- }
1103
- return "read";
1104
- }
1105
- function toolFileActivityType(tool) {
1106
- const readTools = ["read", "glob", "grep", "webfetch", "websearch"];
1107
- if (readTools.includes(tool.toLowerCase())) {
1108
- return "file.read";
1109
- }
1110
- return "file.changed";
1111
- }
1112
- function selectFileOperation(current, next) {
1113
- if (!current) {
1114
- return next;
1115
- }
1116
- if (isWriteOperation(current) && !isWriteOperation(next)) {
1117
- return current;
1118
- }
1119
- return next;
1120
- }
1121
- function isWriteOperation(operation) {
1122
- return operation === "create" || operation === "write" || operation === "edit" || operation === "delete";
1123
- }
1124
- function eventTypeFromFileActivities(files) {
1125
- if (files.some((file) => isWriteOperation(file.operation))) {
1126
- return "file.changed";
1127
- }
1128
- if (files.some((file) => file.operation === "search")) {
1129
- return "file.searched";
1130
- }
1131
- return "file.read";
1132
- }
1133
- function mergeFileActivity(current, next) {
1134
- return {
1135
- ts: next.ts,
1136
- path: next.path,
1137
- operation: selectFileOperation(current?.operation, next.operation),
1138
- bytesRead: sumOptional(current?.bytesRead, next.bytesRead),
1139
- bytesWritten: sumOptional(current?.bytesWritten, next.bytesWritten),
1140
- charsRead: sumOptional(current?.charsRead, next.charsRead),
1141
- charsWritten: sumOptional(current?.charsWritten, next.charsWritten),
1142
- linesRead: sumOptional(current?.linesRead, next.linesRead),
1143
- linesAdded: (current?.linesAdded || 0) + (next.linesAdded || 0),
1144
- linesRemoved: (current?.linesRemoved || 0) + (next.linesRemoved || 0)
1145
- };
1146
- }
1147
- function sumOptional(left, right) {
1148
- const sum = (left || 0) + (right || 0);
1149
- return sum || void 0;
1150
- }
1151
- function summarizeFileActivities(files) {
1152
- const linesAdded = files.reduce((total, file) => total + (file.linesAdded || 0), 0);
1153
- const linesRemoved = files.reduce((total, file) => total + (file.linesRemoved || 0), 0);
1154
- return {
1155
- linesAdded: linesAdded || void 0,
1156
- linesRemoved: linesRemoved || void 0
1157
- };
1158
- }
1159
- function addPathActivity(changes, filePath, operation, ts) {
1160
- if (!filePath) {
1161
- return;
1162
- }
1163
- const current = changes.get(filePath);
1164
- changes.set(filePath, mergeFileActivity(current, { ts, path: filePath, operation }));
1165
- }
1166
- function addResolvedPathActivity(changes, filePath, operation, ts, rootCwd, currentCwd, metrics = {}) {
1167
- if (!filePath) {
1168
- return;
1169
- }
1170
- const resolvedPath = path2.isAbsolute(filePath) ? displayFilePath(filePath, rootCwd) : currentCwd && path2.isAbsolute(currentCwd) ? displayFilePath(path2.resolve(currentCwd, filePath), rootCwd) : filePath;
1171
- changes.set(
1172
- resolvedPath,
1173
- mergeFileActivity(changes.get(resolvedPath), { ts, path: resolvedPath, operation, ...metrics })
1174
- );
1175
- }
1176
- function displayBackfillPath(filePath) {
1177
- if (!path2.isAbsolute(filePath)) {
1178
- return filePath;
1179
- }
1180
- return path2.basename(filePath);
1181
- }
1182
- function countTextLines(text) {
1183
- if (!text) {
1184
- return void 0;
1185
- }
1186
- return text.split(/\r\n|\r|\n/).length;
1187
- }
1188
-
1189
- // src/lib/adapter-helpers.ts
1190
- 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;
1191
981
 
1192
982
  // src/lib/fields.ts
1193
983
  function stringField(object, key) {
@@ -1246,75 +1036,30 @@ function numberOption(value) {
1246
1036
  return Number.isFinite(parsed) ? parsed : void 0;
1247
1037
  }
1248
1038
 
1249
- // src/lib/adapter-helpers.ts
1250
- var TURN_IDLE_MS = 6e4;
1251
- function isTurnIdle(lastEventAt) {
1252
- if (!lastEventAt) {
1253
- 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;
1254
1046
  }
1255
- const lastMs = Date.parse(lastEventAt);
1256
- if (!Number.isFinite(lastMs)) {
1257
- return false;
1047
+ }
1048
+ function timestampFrom(value) {
1049
+ if (typeof value === "string" && !Number.isNaN(Date.parse(value))) {
1050
+ return value;
1258
1051
  }
1259
- return Date.now() - lastMs > TURN_IDLE_MS;
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();
1260
1057
  }
1261
- function sessionIdFromFilePath(filePath, prefix) {
1262
- const match = path4.basename(filePath).match(/([0-9a-f]{8}-[0-9a-f-]{27,})/);
1263
- return match?.[1] || `${prefix}_${createStableHash(filePath).slice(0, 24)}`;
1264
- }
1265
- function hookHandler(agentId, statusMessage) {
1266
- return {
1267
- type: "command",
1268
- command: `codetime hook --agent ${agentId}`,
1269
- timeout: 10,
1270
- statusMessage
1271
- };
1272
- }
1273
- async function isHooksJsonInstalled(filePath, command) {
1274
- try {
1275
- const { readTextIfExists: readTextIfExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
1276
- const text = await readTextIfExists2(filePath);
1277
- if (!text) {
1278
- return false;
1279
- }
1280
- const config = JSON.parse(text);
1281
- if (!isPlainObject(config) || !isPlainObject(config.hooks)) {
1282
- return false;
1283
- }
1284
- return Object.values(config.hooks).some(
1285
- (groups) => Array.isArray(groups) && groups.some(
1286
- (group) => isPlainObject(group) && Array.isArray(group.hooks) && group.hooks.some((hook) => hook.command === command)
1287
- )
1288
- );
1289
- } catch {
1290
- return false;
1291
- }
1292
- }
1293
-
1294
- // src/lib/jsonl.ts
1295
- function parseJsonLine(line) {
1296
- try {
1297
- const parsed = JSON.parse(line);
1298
- return isPlainObject(parsed) ? parsed : void 0;
1299
- } catch {
1300
- return void 0;
1301
- }
1302
- }
1303
- function timestampFrom(value) {
1304
- if (typeof value === "string" && !Number.isNaN(Date.parse(value))) {
1305
- return value;
1306
- }
1307
- if (typeof value !== "number" || !Number.isFinite(value)) {
1308
- return void 0;
1309
- }
1310
- const millis = value > 1e10 ? value : value * 1e3;
1311
- return new Date(millis).toISOString();
1312
- }
1313
- function durationObjectToMs(duration) {
1314
- const secs = duration.secs || 0;
1315
- const nanos = duration.nanos || 0;
1316
- const durationMs = secs * 1e3 + Math.round(nanos / 1e6);
1317
- 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;
1318
1063
  }
1319
1064
  function durationMsBetween(startedAt, endedAt) {
1320
1065
  if (!startedAt || !endedAt) {
@@ -1331,14 +1076,6 @@ function msToIso(ms) {
1331
1076
  return new Date(ms).toISOString();
1332
1077
  }
1333
1078
 
1334
- // src/lib/constants.ts
1335
- var PACKAGE_VERSION = true ? "0.3.0" : "0.1.1";
1336
- var DEFAULT_API_URL = "https://codetime.dev";
1337
- var DEFAULT_BACKFILL_BATCH_SIZE = 50;
1338
- var DEFAULT_BACKFILL_BATCH_BYTES = 800 * 1024;
1339
- var ROLLUP_BUCKET_MS = 15 * 60 * 1e3;
1340
- var DEFAULT_HOOK_SYNC_MIN_INTERVAL_SECONDS = 60;
1341
-
1342
1079
  // src/lib/backfill.ts
1343
1080
  function withBackfillRefs(event, context) {
1344
1081
  const importKey = createImportKey([
@@ -1383,6 +1120,9 @@ function matchesBackfillFilters(event, options) {
1383
1120
  return true;
1384
1121
  }
1385
1122
 
1123
+ // src/adapters/amp.ts
1124
+ init_fs();
1125
+
1386
1126
  // src/lib/session-state.ts
1387
1127
  var SessionParserState = class {
1388
1128
  events = [];
@@ -1469,9 +1209,441 @@ var SessionParserState = class {
1469
1209
  }
1470
1210
  };
1471
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
+ }
1643
+
1472
1644
  // src/adapters/claude-code.ts
1473
1645
  async function parseClaudeCodeSessionFile(filePath, options) {
1474
- const text = await readFile2(filePath, "utf8");
1646
+ const text = await readFile3(filePath, "utf8");
1475
1647
  const lines = text.split("\n").filter(Boolean);
1476
1648
  const projectContext = await claudeProjectContextFromLines(filePath, lines, options);
1477
1649
  const pendingTools = /* @__PURE__ */ new Map();
@@ -1501,7 +1673,7 @@ async function parseClaudeCodeSessionFile(filePath, options) {
1501
1673
  sessionId = stringField(raw, "sessionId") || sessionId;
1502
1674
  state.sessionId = sessionId;
1503
1675
  cwd = stringField(raw, "cwd") || cwd;
1504
- project = projectContext.project || (cwd ? path5.basename(cwd) : project || claudeProjectFromFilePath(filePath, options));
1676
+ project = projectContext.project || (cwd ? path6.basename(cwd) : project || claudeProjectFromFilePath(filePath, options));
1505
1677
  if (!ts) {
1506
1678
  continue;
1507
1679
  }
@@ -1870,17 +2042,17 @@ function claudeTextStats(value) {
1870
2042
  };
1871
2043
  }
1872
2044
  async function claudeProjectContextFromLines(filePath, lines, options) {
1873
- const projectDir = path5.basename(path5.dirname(filePath));
2045
+ const projectDir = path6.basename(path6.dirname(filePath));
1874
2046
  const cwds = [];
1875
2047
  for (const line of lines) {
1876
2048
  const raw = parseJsonLine(line);
1877
2049
  const cwd = raw ? stringField(raw, "cwd") : void 0;
1878
- if (cwd && path5.isAbsolute(cwd)) {
2050
+ if (cwd && path6.isAbsolute(cwd)) {
1879
2051
  cwds.push(cwd);
1880
2052
  }
1881
2053
  }
1882
2054
  const root = await gitRootFromCwds(cwds) || claudeProjectRootFromCwds(projectDir, cwds);
1883
- const project = root ? path5.basename(root) : claudeProjectFromFilePath(filePath, options);
2055
+ const project = root ? path6.basename(root) : claudeProjectFromFilePath(filePath, options);
1884
2056
  return {
1885
2057
  project,
1886
2058
  workspaceId: createWorkspaceId({ projectName: project, repoRoot: root })
@@ -1889,15 +2061,15 @@ async function claudeProjectContextFromLines(filePath, lines, options) {
1889
2061
  async function gitRootFromCwds(cwds) {
1890
2062
  const seen = /* @__PURE__ */ new Set();
1891
2063
  for (const cwd of cwds) {
1892
- let current = path5.resolve(cwd);
2064
+ let current = path6.resolve(cwd);
1893
2065
  while (!seen.has(current)) {
1894
2066
  seen.add(current);
1895
2067
  try {
1896
- await stat2(path5.join(current, ".git"));
2068
+ await stat3(path6.join(current, ".git"));
1897
2069
  return current;
1898
2070
  } catch {
1899
2071
  }
1900
- const parent = path5.dirname(current);
2072
+ const parent = path6.dirname(current);
1901
2073
  if (parent === current) {
1902
2074
  break;
1903
2075
  }
@@ -1908,12 +2080,12 @@ async function gitRootFromCwds(cwds) {
1908
2080
  }
1909
2081
  function claudeProjectRootFromCwds(projectDir, cwds) {
1910
2082
  for (const cwd of cwds) {
1911
- let current = path5.resolve(cwd);
2083
+ let current = path6.resolve(cwd);
1912
2084
  while (true) {
1913
2085
  if (encodeClaudeProjectPath(current) === projectDir) {
1914
2086
  return current;
1915
2087
  }
1916
- const parent = path5.dirname(current);
2088
+ const parent = path6.dirname(current);
1917
2089
  if (parent === current) {
1918
2090
  break;
1919
2091
  }
@@ -1923,11 +2095,11 @@ function claudeProjectRootFromCwds(projectDir, cwds) {
1923
2095
  return void 0;
1924
2096
  }
1925
2097
  function encodeClaudeProjectPath(value) {
1926
- return path5.resolve(value).split(path5.sep).join("-");
2098
+ return path6.resolve(value).split(path6.sep).join("-");
1927
2099
  }
1928
2100
  function claudeProjectFromFilePath(filePath, options) {
1929
- const projectDir = path5.basename(path5.dirname(filePath));
1930
- 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();
1931
2103
  const homePrefix = `${encodeClaudeProjectPath(home)}-`;
1932
2104
  if (projectDir.startsWith(homePrefix)) {
1933
2105
  return projectDir.slice(homePrefix.length) || void 0;
@@ -1958,9 +2130,9 @@ function hookConfig() {
1958
2130
  function claudeConfigDir(home, env) {
1959
2131
  const override = env?.CLAUDE_CONFIG_DIR;
1960
2132
  if (override && override.trim()) {
1961
- return path5.resolve(override);
2133
+ return path6.resolve(override);
1962
2134
  }
1963
- return path5.join(home, ".claude");
2135
+ return path6.join(home, ".claude");
1964
2136
  }
1965
2137
  function createClaudeCodeAdapter() {
1966
2138
  return {
@@ -1972,71 +2144,38 @@ function createClaudeCodeAdapter() {
1972
2144
  return claudeConfigDir(home, env);
1973
2145
  },
1974
2146
  installedPath(home, env) {
1975
- return path5.join(claudeConfigDir(home, env), "settings.json");
2147
+ return path6.join(claudeConfigDir(home, env), "settings.json");
1976
2148
  },
1977
2149
  async isInstalled(home, env) {
1978
2150
  return isHooksJsonInstalled(
1979
- path5.join(claudeConfigDir(home, env), "settings.json"),
2151
+ path6.join(claudeConfigDir(home, env), "settings.json"),
1980
2152
  "codetime hook --agent claude"
1981
2153
  );
1982
2154
  },
1983
2155
  installEntries(home, env) {
1984
2156
  return [{
1985
2157
  kind: "hooks-json",
1986
- path: path5.join(claudeConfigDir(home, env), "settings.json"),
2158
+ path: path6.join(claudeConfigDir(home, env), "settings.json"),
1987
2159
  content: hookConfig()
1988
2160
  }];
1989
2161
  },
1990
2162
  sourcePaths(home, env) {
1991
2163
  const base = claudeConfigDir(home, env);
1992
2164
  return [
1993
- path5.join(base, "projects"),
1994
- path5.join(base, ".claude.json"),
1995
- path5.join(home, ".claude.json")
1996
- ];
1997
- },
1998
- parseSessionFile: parseClaudeCodeSessionFile
1999
- };
2000
- }
2001
-
2002
- // src/adapters/codex.ts
2003
- import { readFile as readFile3 } from "node:fs/promises";
2004
- import path6 from "node:path";
2005
-
2006
- // src/lib/diff.ts
2007
- function parseApplyPatch(patch, ts) {
2008
- const changes = [];
2009
- let current;
2010
- for (const line of patch.split("\n")) {
2011
- const fileMatch = line.match(/^\*\*\* (Add|Update|Delete) File: (.+)$/);
2012
- if (fileMatch) {
2013
- current = {
2014
- ts,
2015
- path: fileMatch[2].trim(),
2016
- operation: fileMatch[1] === "Add" ? "create" : fileMatch[1] === "Delete" ? "delete" : "edit",
2017
- linesAdded: 0,
2018
- linesRemoved: 0
2019
- };
2020
- changes.push(current);
2021
- continue;
2022
- }
2023
- if (!current) {
2024
- continue;
2025
- }
2026
- if (line.startsWith("+") && !line.startsWith("+++")) {
2027
- current.linesAdded = (current.linesAdded || 0) + 1;
2028
- } else if (line.startsWith("-") && !line.startsWith("---")) {
2029
- current.linesRemoved = (current.linesRemoved || 0) + 1;
2030
- }
2031
- }
2032
- return changes;
2033
- }
2034
- function patchFromCommand(command) {
2035
- if (!command || !command.includes("*** Begin Patch")) {
2036
- return void 0;
2037
- }
2038
- return command.slice(command.indexOf("*** Begin Patch"));
2165
+ path6.join(base, "projects"),
2166
+ path6.join(base, ".claude.json"),
2167
+ path6.join(home, ".claude.json")
2168
+ ];
2169
+ },
2170
+ parseSessionFile: parseClaudeCodeSessionFile
2171
+ };
2039
2172
  }
2173
+
2174
+ // src/adapters/codex.ts
2175
+ import { readFile as readFile4 } from "node:fs/promises";
2176
+ import path7 from "node:path";
2177
+
2178
+ // src/lib/diff.ts
2040
2179
  function diffStats(diff) {
2041
2180
  let linesAdded = 0;
2042
2181
  let linesRemoved = 0;
@@ -2081,9 +2220,10 @@ function fileActivitiesFromPatchChanges(changes, ts, cwd, displayFilePath3) {
2081
2220
 
2082
2221
  // src/adapters/codex.ts
2083
2222
  async function parseCodexSessionFile(filePath, options) {
2084
- const text = await readFile3(filePath, "utf8");
2223
+ const text = await readFile4(filePath, "utf8");
2085
2224
  const lines = text.split("\n").filter(Boolean);
2086
2225
  const sourcePathHash = `sha256:${createStableHash(filePath)}`;
2226
+ const serviceTier = await resolveCodexServiceTier(filePath);
2087
2227
  const events = [];
2088
2228
  let sessionId = sessionIdFromFilePath(filePath, "codex");
2089
2229
  let cwd;
@@ -2112,7 +2252,7 @@ async function parseCodexSessionFile(filePath, options) {
2112
2252
  sessionMetaLocked = true;
2113
2253
  sessionId = stringField(payload, "id") || sessionId;
2114
2254
  cwd = stringField(payload, "cwd") || cwd;
2115
- project = cwd ? path6.basename(cwd) : project;
2255
+ project = cwd ? path7.basename(cwd) : project;
2116
2256
  model = stringField(payload, "model_provider") || model;
2117
2257
  events.push(withBackfillRefs({
2118
2258
  schemaVersion: AGENT_TIME_SCHEMA_VERSION,
@@ -2132,7 +2272,7 @@ async function parseCodexSessionFile(filePath, options) {
2132
2272
  if (topType === "turn_context") {
2133
2273
  currentTurnId = stringField(payload, "turn_id") || currentTurnId;
2134
2274
  cwd = stringField(payload, "cwd") || cwd;
2135
- project = cwd ? path6.basename(cwd) : project;
2275
+ project = cwd ? path7.basename(cwd) : project;
2136
2276
  model = stringField(payload, "model") || model;
2137
2277
  continue;
2138
2278
  }
@@ -2213,7 +2353,10 @@ async function parseCodexSessionFile(filePath, options) {
2213
2353
  turnId: currentTurnId,
2214
2354
  cwd,
2215
2355
  project,
2216
- 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),
2217
2360
  confidence: "partial",
2218
2361
  metrics: usage
2219
2362
  }), { filePath, sourcePathHash, lineNumber, topType, payloadType, options }));
@@ -2402,6 +2545,50 @@ async function parseCodexSessionFile(filePath, options) {
2402
2545
  }
2403
2546
  return events.filter((event) => validateCanonicalEvent(event).valid);
2404
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
+ }
2405
2592
  function baseCodexEvent(event) {
2406
2593
  return {
2407
2594
  schemaVersion: AGENT_TIME_SCHEMA_VERSION,
@@ -2476,11 +2663,11 @@ function functionCallArguments(payload) {
2476
2663
  }
2477
2664
  }
2478
2665
  function displayFilePath2(filePath, cwd) {
2479
- if (!cwd || !path6.isAbsolute(filePath)) {
2666
+ if (!cwd || !path7.isAbsolute(filePath)) {
2480
2667
  return filePath;
2481
2668
  }
2482
- const relative = path6.relative(cwd, filePath);
2483
- 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;
2484
2671
  }
2485
2672
  var codexHandler = (msg) => hookHandler("codex", msg);
2486
2673
  function hookConfig2() {
@@ -2499,9 +2686,9 @@ function hookConfig2() {
2499
2686
  function codexHome(home, env) {
2500
2687
  const override = env?.CODEX_HOME;
2501
2688
  if (override && override.trim()) {
2502
- return path6.resolve(override);
2689
+ return path7.resolve(override);
2503
2690
  }
2504
- return path6.join(home, ".codex");
2691
+ return path7.join(home, ".codex");
2505
2692
  }
2506
2693
  function createCodexAdapter() {
2507
2694
  return {
@@ -2513,26 +2700,26 @@ function createCodexAdapter() {
2513
2700
  return codexHome(home, env);
2514
2701
  },
2515
2702
  installedPath(home, env) {
2516
- return path6.join(codexHome(home, env), "hooks.json");
2703
+ return path7.join(codexHome(home, env), "hooks.json");
2517
2704
  },
2518
2705
  async isInstalled(home, env) {
2519
2706
  return isHooksJsonInstalled(
2520
- path6.join(codexHome(home, env), "hooks.json"),
2707
+ path7.join(codexHome(home, env), "hooks.json"),
2521
2708
  "codetime hook --agent codex"
2522
2709
  );
2523
2710
  },
2524
2711
  installEntries(home, env) {
2525
2712
  return [{
2526
2713
  kind: "hooks-json",
2527
- path: path6.join(codexHome(home, env), "hooks.json"),
2714
+ path: path7.join(codexHome(home, env), "hooks.json"),
2528
2715
  content: hookConfig2()
2529
2716
  }];
2530
2717
  },
2531
2718
  sourcePaths(home, env) {
2532
2719
  const base = codexHome(home, env);
2533
2720
  return [
2534
- path6.join(base, "sessions"),
2535
- path6.join(base, "history.jsonl")
2721
+ path7.join(base, "sessions"),
2722
+ path7.join(base, "history.jsonl")
2536
2723
  ];
2537
2724
  },
2538
2725
  parseSessionFile: parseCodexSessionFile
@@ -2541,7 +2728,7 @@ function createCodexAdapter() {
2541
2728
 
2542
2729
  // src/adapters/opencode.ts
2543
2730
  import os2 from "node:os";
2544
- import path7 from "node:path";
2731
+ import path8 from "node:path";
2545
2732
  async function parseOpenCodeSessionFile(dbPath, options) {
2546
2733
  const { DatabaseSync } = await import("node:sqlite");
2547
2734
  if (!dbPath.endsWith(".db")) {
@@ -2550,13 +2737,29 @@ async function parseOpenCodeSessionFile(dbPath, options) {
2550
2737
  const db = new DatabaseSync(dbPath, { readOnly: true });
2551
2738
  const events = [];
2552
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
+ }
2553
2756
  const sessions = db.prepare(
2554
- "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`
2555
2758
  ).all();
2556
2759
  for (const session of sessions) {
2557
2760
  const sessionId = session.id;
2558
2761
  const cwd = session.directory || session.path || void 0;
2559
- const project = cwd ? path7.basename(cwd) : void 0;
2762
+ const project = cwd ? path8.basename(cwd) : void 0;
2560
2763
  const sessionTs = msToIso(session.time_created);
2561
2764
  events.push(baseOpenCodeEvent({
2562
2765
  ts: sessionTs,
@@ -2646,7 +2849,7 @@ async function parseOpenCodeSessionFile(dbPath, options) {
2646
2849
  const assistantAgent = stringField(info, "agent") || "opencode";
2647
2850
  const pathObj = objectField(info, "path");
2648
2851
  const assistantCwd = stringField(pathObj, "cwd") || cwd;
2649
- const assistantProject = assistantCwd ? path7.basename(assistantCwd) : project;
2852
+ const assistantProject = assistantCwd ? path8.basename(assistantCwd) : project;
2650
2853
  const completedTs = numberField(objectField(info, "time"), "completed");
2651
2854
  const createdTs = timeCreated;
2652
2855
  const tokens = opencodeUsageFromInfo(info);
@@ -2888,33 +3091,33 @@ function opencodeUsageFromInfo(info) {
2888
3091
  function opencodeConfigDir(home, env) {
2889
3092
  const override = env?.OPENCODE_CONFIG_DIR;
2890
3093
  if (override && override.trim()) {
2891
- return path7.resolve(override);
3094
+ return path8.resolve(override);
2892
3095
  }
2893
3096
  const xdgConfig = env?.XDG_CONFIG_HOME;
2894
3097
  if (xdgConfig && xdgConfig.trim()) {
2895
- return path7.join(path7.resolve(xdgConfig), "opencode");
3098
+ return path8.join(path8.resolve(xdgConfig), "opencode");
2896
3099
  }
2897
- return path7.join(home, ".config", "opencode");
3100
+ return path8.join(home, ".config", "opencode");
2898
3101
  }
2899
3102
  function opencodeDataCandidates(home, env) {
2900
3103
  const xdgData = env?.XDG_DATA_HOME;
2901
- const primary = xdgData && xdgData.trim() ? path7.join(path7.resolve(xdgData), "opencode", "opencode.db") : path7.join(home, ".local", "share", "opencode", "opencode.db");
2902
- return [primary, path7.join(home, ".opencode", "opencode.db")];
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")];
2903
3106
  }
2904
3107
  async function opencodeBackfillFiles(sourceRoot, home = os2.homedir(), env) {
2905
- const { stat: stat5 } = await import("node:fs/promises");
3108
+ const { stat: stat6 } = await import("node:fs/promises");
2906
3109
  if (sourceRoot) {
2907
3110
  if (!sourceRoot.endsWith(".db")) {
2908
3111
  return [];
2909
3112
  }
2910
- const info = await stat5(sourceRoot).catch(() => null);
3113
+ const info = await stat6(sourceRoot).catch(() => null);
2911
3114
  if (!info) {
2912
3115
  return [];
2913
3116
  }
2914
3117
  return [{ path: sourceRoot, modifiedAt: info.mtime.toISOString() }];
2915
3118
  }
2916
3119
  for (const candidatePath of opencodeDataCandidates(home, env)) {
2917
- const info = await stat5(candidatePath).catch(() => null);
3120
+ const info = await stat6(candidatePath).catch(() => null);
2918
3121
  if (info) {
2919
3122
  return [{ path: candidatePath, modifiedAt: info.mtime.toISOString() }];
2920
3123
  }
@@ -2977,12 +3180,12 @@ function createOpenCodeAdapter() {
2977
3180
  return opencodeConfigDir(home, env);
2978
3181
  },
2979
3182
  installedPath(home, env) {
2980
- return path7.join(opencodeConfigDir(home, env), PLUGIN_PATH);
3183
+ return path8.join(opencodeConfigDir(home, env), PLUGIN_PATH);
2981
3184
  },
2982
3185
  async isInstalled(home, env) {
2983
3186
  try {
2984
3187
  const { pathExists: pathExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
2985
- return await pathExists2(path7.join(opencodeConfigDir(home, env), 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));
2986
3189
  } catch {
2987
3190
  return false;
2988
3191
  }
@@ -2990,7 +3193,7 @@ function createOpenCodeAdapter() {
2990
3193
  installEntries(home, env) {
2991
3194
  return [{
2992
3195
  kind: "file",
2993
- path: path7.join(opencodeConfigDir(home, env), PLUGIN_PATH),
3196
+ path: path8.join(opencodeConfigDir(home, env), PLUGIN_PATH),
2994
3197
  content: opencodePluginContent()
2995
3198
  }];
2996
3199
  },
@@ -3002,10 +3205,10 @@ function createOpenCodeAdapter() {
3002
3205
  }
3003
3206
 
3004
3207
  // src/adapters/pi.ts
3005
- import { readFile as readFile4 } from "node:fs/promises";
3006
- import path8 from "node:path";
3208
+ import { readFile as readFile5 } from "node:fs/promises";
3209
+ import path9 from "node:path";
3007
3210
  async function parsePiSessionFile(filePath, options) {
3008
- const text = await readFile4(filePath, "utf8");
3211
+ const text = await readFile5(filePath, "utf8");
3009
3212
  const lines = text.split("\n").filter(Boolean);
3010
3213
  let sessionId;
3011
3214
  let cwd;
@@ -3034,7 +3237,7 @@ async function parsePiSessionFile(filePath, options) {
3034
3237
  sessionId = stringField(raw, "id") || state.sessionId;
3035
3238
  state.sessionId = sessionId || state.sessionId;
3036
3239
  cwd = stringField(raw, "cwd") || cwd;
3037
- project = cwd ? path8.basename(cwd) : project;
3240
+ project = cwd ? path9.basename(cwd) : project;
3038
3241
  continue;
3039
3242
  }
3040
3243
  if (entryType === "model_change") {
@@ -3432,16 +3635,16 @@ export default function (pi: ExtensionAPI) {
3432
3635
  function piAgentDir(home, env) {
3433
3636
  const override = env?.PI_CODING_AGENT_DIR;
3434
3637
  if (override && override.trim()) {
3435
- return path8.resolve(override);
3638
+ return path9.resolve(override);
3436
3639
  }
3437
- return path8.join(home, ".pi", "agent");
3640
+ return path9.join(home, ".pi", "agent");
3438
3641
  }
3439
3642
  function piSessionDir(home, env) {
3440
3643
  const override = env?.PI_CODING_AGENT_SESSION_DIR;
3441
3644
  if (override && override.trim()) {
3442
- return path8.resolve(override);
3645
+ return path9.resolve(override);
3443
3646
  }
3444
- return path8.join(piAgentDir(home, env), "sessions");
3647
+ return path9.join(piAgentDir(home, env), "sessions");
3445
3648
  }
3446
3649
  function createPiAdapter() {
3447
3650
  return {
@@ -3453,12 +3656,12 @@ function createPiAdapter() {
3453
3656
  return piAgentDir(home, env);
3454
3657
  },
3455
3658
  installedPath(home, env) {
3456
- return path8.join(piAgentDir(home, env), "extensions", "codetime.ts");
3659
+ return path9.join(piAgentDir(home, env), "extensions", "codetime.ts");
3457
3660
  },
3458
3661
  async isInstalled(home, env) {
3459
3662
  try {
3460
3663
  const { pathExists: pathExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
3461
- return await pathExists2(path8.join(piAgentDir(home, env), "extensions", "codetime.ts"));
3664
+ return await pathExists2(path9.join(piAgentDir(home, env), "extensions", "codetime.ts"));
3462
3665
  } catch {
3463
3666
  return false;
3464
3667
  }
@@ -3466,7 +3669,7 @@ function createPiAdapter() {
3466
3669
  installEntries(home, env) {
3467
3670
  return [{
3468
3671
  kind: "file",
3469
- path: path8.join(piAgentDir(home, env), "extensions", "codetime.ts"),
3672
+ path: path9.join(piAgentDir(home, env), "extensions", "codetime.ts"),
3470
3673
  content: piExtensionContent()
3471
3674
  }];
3472
3675
  },
@@ -3824,385 +4027,6 @@ function eventDurationMs(event) {
3824
4027
  );
3825
4028
  }
3826
4029
 
3827
- // src/hooks/dispatch.ts
3828
- import { open, readFile as readFile5, stat as stat3 } from "node:fs/promises";
3829
- import path9 from "node:path";
3830
- var TRANSCRIPT_TAIL_BYTES = 512 * 1024;
3831
- function hookEventsFromPayload(agent, payload, options, enrichment = {}, adapters) {
3832
- const eventName = stringField(payload, "hook_event_name") || "agent.operation";
3833
- const tool = stringField(payload, "tool_name");
3834
- const cwd = stringField(payload, "cwd");
3835
- const model = stringField(payload, "model") || enrichment.model;
3836
- const project = stringOption(options.project) || (cwd ? path9.basename(cwd) : void 0);
3837
- const source = toAgentSource(agent);
3838
- const ts = (/* @__PURE__ */ new Date()).toISOString();
3839
- const toolUseId = stringField(payload, "tool_use_id") || stringField(payload, "toolUseId");
3840
- const turnId = stringField(payload, "turn_id") || stringField(payload, "turnId");
3841
- const sessionId = stringField(payload, "session_id") || stringField(payload, "sessionId");
3842
- const prompt = stringField(payload, "prompt");
3843
- const fileActivities = extractFileActivities(payload, tool, ts);
3844
- const lineMetrics = summarizeFileActivities(fileActivities);
3845
- const durationMs = numberField(payload, "duration_ms") || durationObjectToMs(objectField(payload, "duration"));
3846
- const command = stringField(objectField(payload, "tool_input"), "command");
3847
- const commandHash = tool === "Bash" && command ? createStableHash(command) : void 0;
3848
- const codexUsage = adapters?.tokenUsageFromPayload?.(payload);
3849
- const baseEvent = (type, extra = {}) => ({
3850
- schemaVersion: AGENT_TIME_SCHEMA_VERSION,
3851
- ts,
3852
- source,
3853
- agent,
3854
- type,
3855
- project,
3856
- cwd,
3857
- workspaceId: createWorkspaceId({ projectName: project, repoRoot: cwd }),
3858
- sessionId,
3859
- turnId,
3860
- tool,
3861
- model,
3862
- ...extra
3863
- });
3864
- const usageEvent = (metrics, extra = {}) => {
3865
- const total = (metrics.tokensInput || 0) + (metrics.tokensOutput || 0) + (metrics.tokensReasoningOutput || 0);
3866
- if (total <= 0) {
3867
- return void 0;
3868
- }
3869
- const finalMetrics = {
3870
- ...metrics,
3871
- tokensTotal: metrics.tokensTotal || total
3872
- };
3873
- const eventBase = baseEvent("model.usage", {
3874
- operation: "model usage",
3875
- confidence: extra.confidence ?? "partial",
3876
- metrics: finalMetrics,
3877
- refs: stringRefs({
3878
- sourceId: extra.messageId,
3879
- messageId: extra.messageId
3880
- })
3881
- });
3882
- const cost = estimateEventCostUsd(eventBase);
3883
- if (cost > 0) {
3884
- eventBase.metrics = { ...finalMetrics, costUsd: cost };
3885
- }
3886
- return eventBase;
3887
- };
3888
- switch (eventName) {
3889
- case "SessionStart": {
3890
- return [baseEvent("session.started", { operation: stringField(payload, "source") || "session start" })];
3891
- }
3892
- case "SessionEnd": {
3893
- return [baseEvent("session.ended", { operation: stringField(payload, "reason") || "session end" })];
3894
- }
3895
- case "UserPromptSubmit": {
3896
- return [
3897
- baseEvent("prompt.submitted", {
3898
- operation: "prompt submitted",
3899
- metrics: { prompts: 1, promptChars: prompt?.length },
3900
- refs: stringRefs({
3901
- promptHash: prompt ? `sha256:${createStableHash(prompt)}` : void 0
3902
- })
3903
- })
3904
- ];
3905
- }
3906
- case "PreToolUse": {
3907
- return [
3908
- baseEvent("tool.started", {
3909
- operation: tool ? `${tool} started` : eventName,
3910
- metrics: { toolCalls: 1 },
3911
- refs: stringRefs({ sourceId: toolUseId, commandHash })
3912
- })
3913
- ];
3914
- }
3915
- case "PermissionRequest": {
3916
- return [
3917
- baseEvent("permission.requested", {
3918
- operation: tool ? `${tool} permission requested` : "permission requested",
3919
- refs: stringRefs({ sourceId: toolUseId, commandHash })
3920
- })
3921
- ];
3922
- }
3923
- case "PermissionDenied": {
3924
- return [
3925
- baseEvent("permission.resolved", {
3926
- operation: tool ? `${tool} permission denied` : "permission denied",
3927
- success: false,
3928
- refs: stringRefs({ sourceId: toolUseId, commandHash })
3929
- })
3930
- ];
3931
- }
3932
- case "PostToolUse": {
3933
- const events = buildHookToolResultEvents({
3934
- baseEvent,
3935
- tool,
3936
- toolUseId,
3937
- commandHash,
3938
- durationMs,
3939
- success: true,
3940
- fileActivities,
3941
- lineMetrics
3942
- });
3943
- if (codexUsage) {
3944
- const usage = usageEvent({
3945
- ...codexUsage,
3946
- modelCalls: 1,
3947
- modelDurationMs: durationMs
3948
- }, { confidence: "exact" });
3949
- if (usage) {
3950
- events.push(usage);
3951
- }
3952
- }
3953
- return events;
3954
- }
3955
- case "PostToolUseFailure": {
3956
- return buildHookToolResultEvents({
3957
- baseEvent,
3958
- tool,
3959
- toolUseId,
3960
- commandHash,
3961
- durationMs,
3962
- success: false,
3963
- fileActivities,
3964
- lineMetrics,
3965
- error: stringField(payload, "error")
3966
- });
3967
- }
3968
- case "SubagentStart": {
3969
- return [
3970
- baseEvent("subagent.started", {
3971
- operation: "subagent started",
3972
- agentInstanceId: stringField(payload, "agent_id") || stringField(payload, "agentId")
3973
- })
3974
- ];
3975
- }
3976
- case "SubagentStop": {
3977
- return [
3978
- baseEvent("subagent.ended", {
3979
- operation: "subagent completed",
3980
- agentInstanceId: stringField(payload, "agent_id") || stringField(payload, "agentId"),
3981
- success: !payload.error
3982
- })
3983
- ];
3984
- }
3985
- case "Stop": {
3986
- const turnUsage = enrichment.turnUsage || codexUsage;
3987
- const turnMetrics = { durationMs };
3988
- if (turnUsage) {
3989
- Object.assign(turnMetrics, turnUsage);
3990
- if (!turnMetrics.modelCalls && enrichment.turnUsage?.modelCalls) {
3991
- turnMetrics.modelCalls = enrichment.turnUsage.modelCalls;
3992
- }
3993
- }
3994
- const events = [
3995
- baseEvent("turn.completed", { operation: "turn completed", metrics: turnMetrics })
3996
- ];
3997
- if (turnUsage) {
3998
- const usage = usageEvent(
3999
- { ...turnUsage, modelCalls: turnMetrics.modelCalls, modelDurationMs: durationMs },
4000
- { messageId: enrichment.lastUsageMessageId, confidence: enrichment.turnUsage ? "derived" : "exact" }
4001
- );
4002
- if (usage) {
4003
- events.push(usage);
4004
- }
4005
- }
4006
- return events;
4007
- }
4008
- case "StopFailure": {
4009
- return [baseEvent("turn.failed", { operation: "turn failed" })];
4010
- }
4011
- case "PostCompact": {
4012
- return [baseEvent("context.compacted", { operation: "context compacted" })];
4013
- }
4014
- default: {
4015
- return [];
4016
- }
4017
- }
4018
- }
4019
- async function enrichFromTranscript(payload, claudeUsageFromMessage2) {
4020
- const transcriptPath = stringField(payload, "transcript_path");
4021
- if (!transcriptPath) {
4022
- return {};
4023
- }
4024
- let text;
4025
- try {
4026
- const stats = await stat3(transcriptPath);
4027
- if (stats.size <= TRANSCRIPT_TAIL_BYTES) {
4028
- text = await readFile5(transcriptPath, "utf8");
4029
- } else {
4030
- const handle = await open(transcriptPath, "r");
4031
- try {
4032
- const buffer = Buffer.alloc(TRANSCRIPT_TAIL_BYTES);
4033
- await handle.read(buffer, 0, TRANSCRIPT_TAIL_BYTES, stats.size - TRANSCRIPT_TAIL_BYTES);
4034
- text = buffer.toString("utf8");
4035
- const newlineIndex = text.indexOf("\n");
4036
- if (newlineIndex !== -1) {
4037
- text = text.slice(newlineIndex + 1);
4038
- }
4039
- } finally {
4040
- await handle.close();
4041
- }
4042
- }
4043
- } catch (error) {
4044
- if (process.env.CODETIME_DEBUG || process.env.AGENT_TIME_DEBUG) {
4045
- process.stderr.write(`[codetime] enrichFromTranscript: failed to read transcript ${transcriptPath}: ${error.message}
4046
- `);
4047
- }
4048
- return {};
4049
- }
4050
- const lines = text.split("\n");
4051
- let model;
4052
- let lastUsage;
4053
- let lastUsageMessageId;
4054
- const turnUsage = {};
4055
- let turnCalls = 0;
4056
- for (let index = lines.length - 1; index >= 0; index -= 1) {
4057
- const raw = lines[index];
4058
- if (!raw) {
4059
- continue;
4060
- }
4061
- let entry;
4062
- try {
4063
- entry = JSON.parse(raw);
4064
- } catch {
4065
- continue;
4066
- }
4067
- if (!isPlainObject(entry)) {
4068
- continue;
4069
- }
4070
- const topType = stringField(entry, "type");
4071
- const message = objectField(entry, "message");
4072
- if (topType === "user") {
4073
- if (Object.keys(message).length > 0 && isClaudeToolResultMessage2(message)) {
4074
- continue;
4075
- }
4076
- break;
4077
- }
4078
- if (topType !== "assistant") {
4079
- continue;
4080
- }
4081
- if (!model) {
4082
- model = stringField(message, "model");
4083
- }
4084
- if (!claudeUsageFromMessage2) {
4085
- continue;
4086
- }
4087
- const usage = claudeUsageFromMessage2(message);
4088
- if (!usage) {
4089
- continue;
4090
- }
4091
- if (!lastUsage) {
4092
- lastUsage = usage;
4093
- lastUsageMessageId = stringField(message, "id");
4094
- }
4095
- for (const [key, value] of Object.entries(usage)) {
4096
- if (typeof value === "number") {
4097
- turnUsage[key] = (turnUsage[key] || 0) + value;
4098
- }
4099
- }
4100
- turnCalls += 1;
4101
- }
4102
- return {
4103
- model,
4104
- lastUsage,
4105
- lastUsageMessageId,
4106
- turnUsage: turnCalls > 0 ? { ...turnUsage, modelCalls: turnCalls } : void 0
4107
- };
4108
- }
4109
- function isClaudeToolResultMessage2(message) {
4110
- return Array.isArray(message.content) ? message.content.some(
4111
- (item) => isPlainObject(item) && item.type === "tool_result"
4112
- ) : false;
4113
- }
4114
- function extractFileActivities(payload, tool, ts) {
4115
- const changes = /* @__PURE__ */ new Map();
4116
- const toolInput = objectField(payload, "tool_input");
4117
- const toolResponse = objectField(payload, "tool_response");
4118
- const operation = operationForTool2(tool);
4119
- addPathActivity(changes, stringField(toolInput, "file_path") || stringField(toolInput, "path"), operation, ts);
4120
- addPathActivity(changes, stringField(toolResponse, "filePath") || stringField(toolResponse, "file_path"), operation, ts);
4121
- for (const file of arrayField3(toolInput, "files")) {
4122
- if (typeof file === "string") {
4123
- addPathActivity(changes, file, operation, ts);
4124
- }
4125
- }
4126
- const patch = stringField(toolInput, "patch") || patchFromCommand(stringField(toolInput, "command"));
4127
- if (patch) {
4128
- for (const change of parseApplyPatch(patch, ts)) {
4129
- const current = changes.get(change.path);
4130
- changes.set(change.path, mergeFileActivity(current, change));
4131
- }
4132
- }
4133
- return [...changes.values()];
4134
- }
4135
- function operationForTool2(tool) {
4136
- const normalized = (tool || "").toLowerCase();
4137
- if (["read", "notebookread", "view_image"].includes(normalized)) {
4138
- return "read";
4139
- }
4140
- if (["grep", "glob", "ls", "search", "rg"].includes(normalized)) {
4141
- return "search";
4142
- }
4143
- if (["write"].includes(normalized)) {
4144
- return "write";
4145
- }
4146
- if (["edit", "multiedit", "notebookedit", "apply_patch", "applypatch"].includes(normalized)) {
4147
- return "edit";
4148
- }
4149
- return "read";
4150
- }
4151
- function arrayField3(object, key) {
4152
- if (!isPlainObject(object) || !Array.isArray(object[key])) {
4153
- return [];
4154
- }
4155
- return object[key];
4156
- }
4157
- function buildHookToolResultEvents(input) {
4158
- const refs = stringRefs({
4159
- sourceId: input.toolUseId,
4160
- commandHash: input.commandHash
4161
- });
4162
- const events = [
4163
- input.baseEvent(input.success ? "tool.completed" : "tool.failed", {
4164
- operation: input.tool ? `${input.tool} completed` : "tool completed",
4165
- success: input.success,
4166
- metrics: {
4167
- toolDurationMs: input.durationMs,
4168
- durationMs: input.durationMs
4169
- },
4170
- refs
4171
- })
4172
- ];
4173
- if (input.tool === "Bash") {
4174
- events.push(input.baseEvent(input.success ? "command.completed" : "command.failed", {
4175
- operation: "command completed",
4176
- success: input.success,
4177
- metrics: {
4178
- commandCalls: 1,
4179
- commandDurationMs: input.durationMs,
4180
- durationMs: input.durationMs
4181
- },
4182
- refs
4183
- }));
4184
- }
4185
- if (input.fileActivities.length > 0) {
4186
- events.push(input.baseEvent(eventTypeFromFileActivities(input.fileActivities), {
4187
- operation: input.tool ? `${input.tool} file activity` : "file activity",
4188
- success: input.success,
4189
- fileActivities: input.fileActivities,
4190
- metrics: input.lineMetrics,
4191
- refs
4192
- }));
4193
- }
4194
- return events;
4195
- }
4196
- function toAgentSource(agent) {
4197
- if (agent === "claude") {
4198
- return "claude-code";
4199
- }
4200
- if (agent === "codex" || agent === "cursor" || agent === "opencode") {
4201
- return agent;
4202
- }
4203
- return agent || "unknown";
4204
- }
4205
-
4206
4030
  // src/install/manager.ts
4207
4031
  init_fs();
4208
4032
  async function installEntry(entry, options) {
@@ -4216,7 +4040,7 @@ async function installEntry(entry, options) {
4216
4040
  });
4217
4041
  }
4218
4042
  async function mergeHooksJson(filePath, content, { dryRun, force, onWrite }) {
4219
- const { mkdir: mkdir3, writeFile: writeFile3 } = await import("node:fs/promises");
4043
+ const { mkdir: mkdir4, writeFile: writeFile3 } = await import("node:fs/promises");
4220
4044
  const pathMod = await import("node:path");
4221
4045
  if (dryRun) {
4222
4046
  onWrite(`Would merge ${filePath}`);
@@ -4237,7 +4061,7 @@ async function mergeHooksJson(filePath, content, { dryRun, force, onWrite }) {
4237
4061
  onWrite(`Already installed ${filePath}`);
4238
4062
  return;
4239
4063
  }
4240
- await mkdir3(pathMod.dirname(filePath), { recursive: true });
4064
+ await mkdir4(pathMod.dirname(filePath), { recursive: true });
4241
4065
  await writeFile3(filePath, nextText, "utf8");
4242
4066
  onWrite(`Installed ${filePath}`);
4243
4067
  }
@@ -4332,6 +4156,63 @@ function defaultMachineName() {
4332
4156
  // src/cli.ts
4333
4157
  init_fs();
4334
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
+
4335
4216
  // src/lib/progress.ts
4336
4217
  var BAR_WIDTH = 24;
4337
4218
  var ProgressBar = class {
@@ -4459,8 +4340,8 @@ function buildHeaders(token, machine) {
4459
4340
  ...machine?.platform ? { "x-machine-platform": machine.platform } : {}
4460
4341
  };
4461
4342
  }
4462
- function joinUrl(base, path12) {
4463
- return new URL(path12, base.endsWith("/") ? base : `${base}/`).toString();
4343
+ function joinUrl(base, path13) {
4344
+ return new URL(path13, base.endsWith("/") ? base : `${base}/`).toString();
4464
4345
  }
4465
4346
  async function postRollupBatch(remote, rollups, options = {}) {
4466
4347
  const response = await remote.fetchImpl(joinUrl(remote.baseUrl, "/v3/agent/ingest"), {
@@ -4524,6 +4405,9 @@ async function deleteMachine(remote, id) {
4524
4405
  return { deletedSessions: Number(body.deletedSessions) || 0 };
4525
4406
  }
4526
4407
 
4408
+ // src/lib/types.ts
4409
+ var BACKFILL_STATE_SCHEMA_VERSION = 3;
4410
+
4527
4411
  // src/cli.ts
4528
4412
  function createRegistry() {
4529
4413
  const registry = new AdapterRegistry();
@@ -4531,6 +4415,7 @@ function createRegistry() {
4531
4415
  registry.register(createClaudeCodeAdapter());
4532
4416
  registry.register(createPiAdapter());
4533
4417
  registry.register(createOpenCodeAdapter());
4418
+ registry.register(createAmpAdapter());
4534
4419
  return registry;
4535
4420
  }
4536
4421
  var defaultContext = {
@@ -4570,6 +4455,7 @@ ${helpText()}`);
4570
4455
  } catch (error) {
4571
4456
  write(ctx.stderr, `${error.message}
4572
4457
  `);
4458
+ await logError("cli", error, { argv });
4573
4459
  return 1;
4574
4460
  }
4575
4461
  }
@@ -4666,7 +4552,7 @@ async function installCommand(options, ctx, registry) {
4666
4552
  }
4667
4553
  const selectedIds = requested.length > 0 ? requested : options.all ? allAdapters.map((a) => a.id) : detected;
4668
4554
  if (selectedIds.length === 0) {
4669
- 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");
4670
4556
  return 1;
4671
4557
  }
4672
4558
  for (const adapter of allAdapters.filter((a) => selectedIds.includes(a.id))) {
@@ -4682,21 +4568,30 @@ async function installCommand(options, ctx, registry) {
4682
4568
  return 0;
4683
4569
  }
4684
4570
  async function hookCommand(options, ctx) {
4685
- const agent = requiredOption(options, "agent");
4686
- const payload = await readHookPayload(ctx.stdin);
4687
- const enrichment = await enrichFromTranscript(payload, claudeUsageFromMessage);
4688
- const events = hookEventsFromPayload(agent, payload, options, enrichment, { tokenUsageFromPayload });
4689
- assertValidEvents(events);
4690
- if (options["dry-run"]) {
4691
- 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}
4692
4592
  `);
4693
4593
  return 0;
4694
4594
  }
4695
- return syncLocalTriggerCommand({
4696
- ...options,
4697
- agent,
4698
- "min-interval": stringOption(options["min-interval"]) || String(DEFAULT_HOOK_SYNC_MIN_INTERVAL_SECONDS)
4699
- }, ctx);
4700
4595
  }
4701
4596
  async function syncLocalTriggerCommand(options, ctx, _registry) {
4702
4597
  const home = resolveHome(options, ctx);
@@ -4757,6 +4652,9 @@ async function syncLocalRunnerCommand(options, ctx, _registry) {
4757
4652
  source: "all"
4758
4653
  }, ctx);
4759
4654
  return exitCode;
4655
+ } catch (error) {
4656
+ await logError("sync-local-runner", error, { home }, home);
4657
+ throw error;
4760
4658
  } finally {
4761
4659
  const nextState = await readSyncLocalTriggerState(statePath);
4762
4660
  nextState.lastStartedAt = state.lastStartedAt;
@@ -4867,19 +4765,34 @@ async function createBackfillPlanFromOptions(options, ctx, action, registry) {
4867
4765
  }
4868
4766
  async function createBackfillEventsFromDefs(sourceDefs, options, registry, ctx, overrideFiles) {
4869
4767
  const events = [];
4768
+ const home = resolveHome(options, ctx);
4870
4769
  for (const item of sourceDefs) {
4871
4770
  const parser = registry.getParser(item.id);
4872
4771
  if (!parser) {
4873
4772
  continue;
4874
4773
  }
4875
- const sourceFiles = await listBackfillSourceFiles(item, options, ctx);
4876
- 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
+ }
4877
4784
  for (const filePath of files) {
4878
- const parsed = await parser(filePath, options);
4879
- for (const event of parsed) {
4880
- if (matchesBackfillFilters(event, options)) {
4881
- 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
+ }
4882
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
+ `);
4883
4796
  }
4884
4797
  }
4885
4798
  }
@@ -4904,11 +4817,14 @@ async function listBackfillSourceFiles(source, options, ctx) {
4904
4817
  if (source.id === "opencode") {
4905
4818
  return opencodeBackfillFiles(stringOption(options["source-root"]), resolveHome(options, ctx), ctx.env);
4906
4819
  }
4820
+ if (source.id === "amp") {
4821
+ return ampBackfillFiles(stringOption(options["source-root"]), resolveHome(options, ctx), ctx.env);
4822
+ }
4907
4823
  const roots = stringOption(options["source-root"]) ? [requiredOption(options, "source-root")] : source.paths;
4908
4824
  const fileLists = await Promise.all(roots.map((r) => listJsonlFiles(r)));
4909
4825
  const files = fileLists.flat().sort().slice(0, numberOption(options.limit) || void 0);
4910
4826
  return Promise.all(files.map(async (filePath) => {
4911
- const info = await stat4(filePath);
4827
+ const info = await stat5(filePath);
4912
4828
  return { path: filePath, modifiedAt: info.mtime.toISOString() };
4913
4829
  }));
4914
4830
  }
@@ -5049,28 +4965,48 @@ async function purgeForcedSources(sourceDefs, home, options, ctx) {
5049
4965
  async function collectCanonicalEvents(sourceDefs, registry, incrementalState, options, ctx) {
5050
4966
  const selectedFilesBySource = /* @__PURE__ */ new Map();
5051
4967
  const canonicalEvents = [];
4968
+ const home = resolveHome(options, ctx);
5052
4969
  for (const item of sourceDefs) {
5053
4970
  const parser = registry.getParser(item.id);
5054
4971
  if (!parser) {
5055
4972
  continue;
5056
4973
  }
5057
- const sourceFiles = await listBackfillSourceFiles(item, options, ctx);
5058
- 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
+ }
5059
4984
  selectedFilesBySource.set(item.id, selectedFiles);
5060
4985
  const filePaths = selectedFiles.map((f) => f.path);
5061
4986
  const sourceEvents = [];
4987
+ let sourceFailed = false;
5062
4988
  const bar = options.json ? void 0 : new ProgressBar(ctx.stdout, `${item.id.padEnd(12)}`);
5063
4989
  bar?.init(filePaths.length, `0 events`);
5064
4990
  for (let fi = 0; fi < filePaths.length; fi += 1) {
5065
- const parsed = await parser(filePaths[fi], options);
5066
- for (const event of parsed) {
5067
- if (matchesBackfillFilters(event, options)) {
5068
- sourceEvents.push(event);
4991
+ try {
4992
+ const parsed = await parser(filePaths[fi], options);
4993
+ for (const event of parsed) {
4994
+ if (matchesBackfillFilters(event, options)) {
4995
+ sourceEvents.push(event);
4996
+ }
5069
4997
  }
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
+ `);
5070
5003
  }
5071
5004
  bar?.tick(`${fi + 1}/${filePaths.length} files, ${sourceEvents.length} events`);
5072
5005
  }
5073
- bar?.finalize(`${sourceEvents.length} events`);
5006
+ bar?.finalize(`${sourceEvents.length} events${sourceFailed ? " (partial \u2014 see logs)" : ""}`);
5007
+ if (sourceFailed) {
5008
+ selectedFilesBySource.delete(item.id);
5009
+ }
5074
5010
  for (const event of sourceEvents) canonicalEvents.push(event);
5075
5011
  }
5076
5012
  return { canonicalEvents, selectedFilesBySource };
@@ -5196,13 +5132,13 @@ function selectBackfillFilesForImport(files, watermarkTs) {
5196
5132
  });
5197
5133
  }
5198
5134
  function backfillIncrementalStatePath(home) {
5199
- return path11.join(home, ".codetime", "backfill-state.json");
5135
+ return path12.join(home, ".codetime", "backfill-state.json");
5200
5136
  }
5201
5137
  function syncLocalTriggerStatePath(home) {
5202
- return path11.join(home, ".codetime", "sync-local-trigger.json");
5138
+ return path12.join(home, ".codetime", "sync-local-trigger.json");
5203
5139
  }
5204
5140
  function syncLocalTriggerLockPath(home) {
5205
- return path11.join(home, ".codetime", "sync-local-trigger.lock");
5141
+ return path12.join(home, ".codetime", "sync-local-trigger.lock");
5206
5142
  }
5207
5143
  async function readBackfillIncrementalState(home, ctx) {
5208
5144
  const statePath = backfillIncrementalStatePath(home);
@@ -5225,7 +5161,7 @@ async function readBackfillIncrementalState(home, ctx) {
5225
5161
  return { version: BACKFILL_STATE_SCHEMA_VERSION, sources: {} };
5226
5162
  }
5227
5163
  const sources = {};
5228
- for (const source of ["codex", "claude-code", "opencode", "pi"]) {
5164
+ for (const source of ["codex", "claude-code", "opencode", "pi", "amp"]) {
5229
5165
  const item = state.sources[source];
5230
5166
  if (isPlainObject(item) && typeof item.watermarkTs === "string" && !Number.isNaN(Date.parse(item.watermarkTs))) {
5231
5167
  sources[source] = { watermarkTs: item.watermarkTs };
@@ -5242,7 +5178,7 @@ async function updateBackfillIncrementalState(home, state, selectedFilesBySource
5242
5178
  state.sources[source] = { watermarkTs: latest };
5243
5179
  }
5244
5180
  const statePath = backfillIncrementalStatePath(home);
5245
- await mkdir2(path11.dirname(statePath), { recursive: true });
5181
+ await mkdir3(path12.dirname(statePath), { recursive: true });
5246
5182
  await writeFile2(statePath, `${JSON.stringify(state, null, 2)}
5247
5183
  `, "utf8");
5248
5184
  }
@@ -5270,7 +5206,7 @@ async function readSyncLocalTriggerState(statePath) {
5270
5206
  return nextState;
5271
5207
  }
5272
5208
  async function writeSyncLocalTriggerState(statePath, state) {
5273
- await mkdir2(path11.dirname(statePath), { recursive: true });
5209
+ await mkdir3(path12.dirname(statePath), { recursive: true });
5274
5210
  await writeFile2(statePath, `${JSON.stringify(state, null, 2)}
5275
5211
  `, "utf8");
5276
5212
  }
@@ -5285,7 +5221,7 @@ async function readSyncLocalLock(lockPath) {
5285
5221
  return { pid: lock.pid, startedAt: lock.startedAt };
5286
5222
  }
5287
5223
  async function writeSyncLocalLock(lockPath, lock) {
5288
- await mkdir2(path11.dirname(lockPath), { recursive: true });
5224
+ await mkdir3(path12.dirname(lockPath), { recursive: true });
5289
5225
  await writeFile2(lockPath, `${JSON.stringify(lock, null, 2)}
5290
5226
  `, "utf8");
5291
5227
  }
@@ -5345,10 +5281,10 @@ function syncLocalRunnerEntryArgs(cliPath) {
5345
5281
  if (cliPath.endsWith(".ts")) {
5346
5282
  return ["--import", "tsx", cliPath];
5347
5283
  }
5348
- return [path11.resolve(path11.dirname(cliPath), "../bin/codetime.mjs")];
5284
+ return [path12.resolve(path12.dirname(cliPath), "../bin/codetime.mjs")];
5349
5285
  }
5350
5286
  function resolveHome(options, ctx) {
5351
- return path11.resolve(stringOption(options.home) || ctx.env.HOME || os3.homedir());
5287
+ return path12.resolve(stringOption(options.home) || ctx.env.HOME || os3.homedir());
5352
5288
  }
5353
5289
  function requestedTargets(options) {
5354
5290
  const value = options.target || options.targets;
@@ -5367,15 +5303,6 @@ function requiredOption(options, name) {
5367
5303
  }
5368
5304
  return value;
5369
5305
  }
5370
- function assertValidEvent(event) {
5371
- const validation = validateCanonicalEvent(event);
5372
- if (!validation.valid) {
5373
- throw new Error(`Invalid event: ${validation.errors.join("; ")}`);
5374
- }
5375
- }
5376
- function assertValidEvents(events) {
5377
- for (const event of events) assertValidEvent(event);
5378
- }
5379
5306
  async function readHookPayload(stdin) {
5380
5307
  const text = await readAll(stdin);
5381
5308
  if (!text.trim()) {