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