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.
- package/bin/codetime.mjs +767 -840
- 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
|
|
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(
|
|
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(
|
|
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 =
|
|
99
|
+
const entryPath = path.join(rootPath, entry.name);
|
|
96
100
|
if (entry.isDirectory()) {
|
|
97
|
-
return
|
|
101
|
+
return listFilesByExtensions(entryPath, extensions);
|
|
98
102
|
}
|
|
99
|
-
return entry.isFile() && entry.name.endsWith(
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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/
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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/
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
return
|
|
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
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1047
|
+
}
|
|
1048
|
+
function timestampFrom(value) {
|
|
1049
|
+
if (typeof value === "string" && !Number.isNaN(Date.parse(value))) {
|
|
1050
|
+
return value;
|
|
1258
1051
|
}
|
|
1259
|
-
|
|
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
|
|
1262
|
-
const
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
|
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 ?
|
|
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 =
|
|
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 &&
|
|
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 ?
|
|
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 =
|
|
2064
|
+
let current = path6.resolve(cwd);
|
|
1893
2065
|
while (!seen.has(current)) {
|
|
1894
2066
|
seen.add(current);
|
|
1895
2067
|
try {
|
|
1896
|
-
await
|
|
2068
|
+
await stat3(path6.join(current, ".git"));
|
|
1897
2069
|
return current;
|
|
1898
2070
|
} catch {
|
|
1899
2071
|
}
|
|
1900
|
-
const parent =
|
|
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 =
|
|
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 =
|
|
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
|
|
2098
|
+
return path6.resolve(value).split(path6.sep).join("-");
|
|
1927
2099
|
}
|
|
1928
2100
|
function claudeProjectFromFilePath(filePath, options) {
|
|
1929
|
-
const projectDir =
|
|
1930
|
-
const home = options ?
|
|
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
|
|
2133
|
+
return path6.resolve(override);
|
|
1962
2134
|
}
|
|
1963
|
-
return
|
|
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
|
|
2147
|
+
return path6.join(claudeConfigDir(home, env), "settings.json");
|
|
1976
2148
|
},
|
|
1977
2149
|
async isInstalled(home, env) {
|
|
1978
2150
|
return isHooksJsonInstalled(
|
|
1979
|
-
|
|
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:
|
|
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
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
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
|
|
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 ?
|
|
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 ?
|
|
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 || !
|
|
2666
|
+
if (!cwd || !path7.isAbsolute(filePath)) {
|
|
2480
2667
|
return filePath;
|
|
2481
2668
|
}
|
|
2482
|
-
const relative =
|
|
2483
|
-
return relative && !relative.startsWith("..") && !
|
|
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
|
|
2689
|
+
return path7.resolve(override);
|
|
2503
2690
|
}
|
|
2504
|
-
return
|
|
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
|
|
2703
|
+
return path7.join(codexHome(home, env), "hooks.json");
|
|
2517
2704
|
},
|
|
2518
2705
|
async isInstalled(home, env) {
|
|
2519
2706
|
return isHooksJsonInstalled(
|
|
2520
|
-
|
|
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:
|
|
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
|
-
|
|
2535
|
-
|
|
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
|
|
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
|
-
|
|
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 ?
|
|
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 ?
|
|
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
|
|
3094
|
+
return path8.resolve(override);
|
|
2892
3095
|
}
|
|
2893
3096
|
const xdgConfig = env?.XDG_CONFIG_HOME;
|
|
2894
3097
|
if (xdgConfig && xdgConfig.trim()) {
|
|
2895
|
-
return
|
|
3098
|
+
return path8.join(path8.resolve(xdgConfig), "opencode");
|
|
2896
3099
|
}
|
|
2897
|
-
return
|
|
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() ?
|
|
2902
|
-
return [primary,
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
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
|
|
3006
|
-
import
|
|
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
|
|
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 ?
|
|
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
|
|
3638
|
+
return path9.resolve(override);
|
|
3436
3639
|
}
|
|
3437
|
-
return
|
|
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
|
|
3645
|
+
return path9.resolve(override);
|
|
3443
3646
|
}
|
|
3444
|
-
return
|
|
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
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
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,
|
|
4463
|
-
return new URL(
|
|
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
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
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
|
-
|
|
4876
|
-
|
|
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
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
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
|
|
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
|
-
|
|
5058
|
-
|
|
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
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
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
|
|
5135
|
+
return path12.join(home, ".codetime", "backfill-state.json");
|
|
5200
5136
|
}
|
|
5201
5137
|
function syncLocalTriggerStatePath(home) {
|
|
5202
|
-
return
|
|
5138
|
+
return path12.join(home, ".codetime", "sync-local-trigger.json");
|
|
5203
5139
|
}
|
|
5204
5140
|
function syncLocalTriggerLockPath(home) {
|
|
5205
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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 [
|
|
5284
|
+
return [path12.resolve(path12.dirname(cliPath), "../bin/codetime.mjs")];
|
|
5349
5285
|
}
|
|
5350
5286
|
function resolveHome(options, ctx) {
|
|
5351
|
-
return
|
|
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()) {
|