llm-usage-metrics 0.3.3 → 0.3.5
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/README.md +63 -30
- package/dist/index.js +588 -87
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -748,7 +748,7 @@ function createUsageEvent(input) {
|
|
|
748
748
|
};
|
|
749
749
|
}
|
|
750
750
|
|
|
751
|
-
// src/utils/discover-
|
|
751
|
+
// src/utils/discover-files.ts
|
|
752
752
|
import { readdir } from "fs/promises";
|
|
753
753
|
import path3 from "path";
|
|
754
754
|
function getNodeErrorCode(error) {
|
|
@@ -759,41 +759,65 @@ function isSkippableDirectoryReadError(error) {
|
|
|
759
759
|
const code = getNodeErrorCode(error);
|
|
760
760
|
return code === "EACCES" || code === "EPERM";
|
|
761
761
|
}
|
|
762
|
+
function matchesExtension(fileName, extension) {
|
|
763
|
+
const lowerFileName = fileName.toLowerCase();
|
|
764
|
+
const lowerExtension = extension.toLowerCase();
|
|
765
|
+
return lowerFileName.endsWith(lowerExtension);
|
|
766
|
+
}
|
|
767
|
+
function normalizeExtension(extension) {
|
|
768
|
+
const normalized = extension.trim();
|
|
769
|
+
if (!normalized) {
|
|
770
|
+
throw new Error("discoverFiles extension must be a non-empty string");
|
|
771
|
+
}
|
|
772
|
+
if (!normalized.startsWith(".")) {
|
|
773
|
+
throw new Error('discoverFiles extension must start with "."');
|
|
774
|
+
}
|
|
775
|
+
return normalized;
|
|
776
|
+
}
|
|
762
777
|
async function walkDirectory(rootDir, acc, options) {
|
|
763
778
|
let entries;
|
|
764
779
|
try {
|
|
765
780
|
entries = await readdir(rootDir, { withFileTypes: true, encoding: "utf8" });
|
|
766
781
|
} catch (error) {
|
|
782
|
+
if (getNodeErrorCode(error) === "ENOENT") {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
767
785
|
if (options.allowPermissionSkip && isSkippableDirectoryReadError(error)) {
|
|
768
786
|
return;
|
|
769
787
|
}
|
|
770
788
|
throw error;
|
|
771
789
|
}
|
|
772
|
-
|
|
790
|
+
if (options.sort) {
|
|
791
|
+
entries.sort((left, right) => compareByCodePoint(left.name, right.name));
|
|
792
|
+
}
|
|
773
793
|
for (const entry of entries) {
|
|
774
794
|
const entryPath = path3.join(rootDir, entry.name);
|
|
775
|
-
if (entry.isDirectory()) {
|
|
776
|
-
await walkDirectory(entryPath, acc,
|
|
795
|
+
if (entry.isDirectory() && options.recursive) {
|
|
796
|
+
await walkDirectory(entryPath, acc, options);
|
|
777
797
|
continue;
|
|
778
798
|
}
|
|
779
|
-
if (entry.isFile() && entry.name.
|
|
799
|
+
if (entry.isFile() && matchesExtension(entry.name, options.extension)) {
|
|
780
800
|
acc.push(entryPath);
|
|
781
801
|
}
|
|
782
802
|
}
|
|
783
803
|
}
|
|
784
|
-
async function
|
|
804
|
+
async function discoverFiles(rootDir, options) {
|
|
785
805
|
const files = [];
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
}
|
|
806
|
+
const resolvedOptions = {
|
|
807
|
+
extension: normalizeExtension(options.extension),
|
|
808
|
+
recursive: options.recursive ?? true,
|
|
809
|
+
allowPermissionSkip: options.allowPermissionSkip ?? true,
|
|
810
|
+
sort: options.sort ?? true
|
|
811
|
+
};
|
|
812
|
+
await walkDirectory(rootDir, files, resolvedOptions);
|
|
794
813
|
return files;
|
|
795
814
|
}
|
|
796
815
|
|
|
816
|
+
// src/utils/discover-jsonl-files.ts
|
|
817
|
+
async function discoverJsonlFiles(rootDir) {
|
|
818
|
+
return discoverFiles(rootDir, { extension: ".jsonl" });
|
|
819
|
+
}
|
|
820
|
+
|
|
797
821
|
// src/utils/fs-helpers.ts
|
|
798
822
|
import { access, constants, stat } from "fs/promises";
|
|
799
823
|
async function pathExists(filePath) {
|
|
@@ -883,6 +907,9 @@ function asTrimmedText(value) {
|
|
|
883
907
|
const normalized = value.trim();
|
|
884
908
|
return normalized || void 0;
|
|
885
909
|
}
|
|
910
|
+
function isBlankText(value) {
|
|
911
|
+
return value.trim().length === 0;
|
|
912
|
+
}
|
|
886
913
|
function toNumberLike(value) {
|
|
887
914
|
if (value === null || value === void 0 || typeof value === "number" || typeof value === "string") {
|
|
888
915
|
return value;
|
|
@@ -897,9 +924,6 @@ var SESSION_META_LINE_PATTERN = /"type"\s*:\s*"session_meta"/u;
|
|
|
897
924
|
var TURN_CONTEXT_LINE_PATTERN = /"type"\s*:\s*"turn_context"/u;
|
|
898
925
|
var EVENT_MSG_LINE_PATTERN = /"type"\s*:\s*"event_msg"/u;
|
|
899
926
|
var TOKEN_COUNT_LINE_PATTERN = /"type"\s*:\s*"token_count"/u;
|
|
900
|
-
function isBlankText(value) {
|
|
901
|
-
return value.trim().length === 0;
|
|
902
|
-
}
|
|
903
927
|
function shouldParseCodexJsonlLine(lineText) {
|
|
904
928
|
if (SESSION_META_LINE_PATTERN.test(lineText) || TURN_CONTEXT_LINE_PATTERN.test(lineText)) {
|
|
905
929
|
return true;
|
|
@@ -1070,45 +1094,466 @@ var CodexSourceAdapter = class {
|
|
|
1070
1094
|
}
|
|
1071
1095
|
};
|
|
1072
1096
|
|
|
1073
|
-
// src/sources/
|
|
1097
|
+
// src/sources/droid/droid-source-adapter.ts
|
|
1098
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1074
1099
|
import os3 from "os";
|
|
1075
1100
|
import path5 from "path";
|
|
1101
|
+
|
|
1102
|
+
// src/sources/parse-diagnostics.ts
|
|
1103
|
+
function incrementSkippedReason(reasons, reason) {
|
|
1104
|
+
const current = reasons.get(reason) ?? 0;
|
|
1105
|
+
reasons.set(reason, current + 1);
|
|
1106
|
+
}
|
|
1107
|
+
function toSkippedRowReasonStats(reasons) {
|
|
1108
|
+
return [...reasons.entries()].map(([reason, count]) => ({ reason, count })).sort((left, right) => compareByCodePoint(left.reason, right.reason));
|
|
1109
|
+
}
|
|
1110
|
+
function toParseDiagnostics(events, skippedRows, skippedRowReasons) {
|
|
1111
|
+
return {
|
|
1112
|
+
events,
|
|
1113
|
+
skippedRows,
|
|
1114
|
+
skippedRowReasons: toSkippedRowReasonStats(skippedRowReasons)
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// src/sources/droid/droid-source-adapter.ts
|
|
1119
|
+
var defaultSessionsDir2 = path5.join(os3.homedir(), ".factory", "sessions");
|
|
1120
|
+
var DROID_SESSION_START_LINE_PATTERN = /"type"\s*:\s*"session_start"/u;
|
|
1121
|
+
var DROID_MESSAGE_LINE_PATTERN = /"type"\s*:\s*"message"/u;
|
|
1122
|
+
function shouldParseDroidJsonlLine(lineText) {
|
|
1123
|
+
return DROID_SESSION_START_LINE_PATTERN.test(lineText) || DROID_MESSAGE_LINE_PATTERN.test(lineText);
|
|
1124
|
+
}
|
|
1125
|
+
var UNIX_SECONDS_ABS_CUTOFF = 1e10;
|
|
1126
|
+
function normalizeTimestampCandidate(candidate) {
|
|
1127
|
+
let date;
|
|
1128
|
+
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
1129
|
+
const timestampMs = Math.abs(candidate) <= UNIX_SECONDS_ABS_CUTOFF ? candidate * 1e3 : candidate;
|
|
1130
|
+
date = new Date(timestampMs);
|
|
1131
|
+
} else {
|
|
1132
|
+
const normalizedText = asTrimmedText(candidate);
|
|
1133
|
+
if (!normalizedText) {
|
|
1134
|
+
return void 0;
|
|
1135
|
+
}
|
|
1136
|
+
date = new Date(normalizedText);
|
|
1137
|
+
}
|
|
1138
|
+
if (Number.isNaN(date.getTime())) {
|
|
1139
|
+
return void 0;
|
|
1140
|
+
}
|
|
1141
|
+
return date.toISOString();
|
|
1142
|
+
}
|
|
1143
|
+
function getSettingsSessionId(filePath) {
|
|
1144
|
+
return path5.basename(filePath, ".settings.json");
|
|
1145
|
+
}
|
|
1146
|
+
function getSiblingJsonlPath(settingsPath) {
|
|
1147
|
+
return path5.join(path5.dirname(settingsPath), `${getSettingsSessionId(settingsPath)}.jsonl`);
|
|
1148
|
+
}
|
|
1149
|
+
function isSessionStartRecord(line) {
|
|
1150
|
+
return asTrimmedText(line.type) === "session_start";
|
|
1151
|
+
}
|
|
1152
|
+
function isMessageRecord(line) {
|
|
1153
|
+
return asTrimmedText(line.type) === "message";
|
|
1154
|
+
}
|
|
1155
|
+
function resolveRepoRootFromSessionStart(line) {
|
|
1156
|
+
const payload = asRecord(line.session_start);
|
|
1157
|
+
return asTrimmedText(payload?.cwd);
|
|
1158
|
+
}
|
|
1159
|
+
var DroidSourceAdapter = class {
|
|
1160
|
+
id = "droid";
|
|
1161
|
+
sessionsDir;
|
|
1162
|
+
requireSessionsDir;
|
|
1163
|
+
constructor(options = {}) {
|
|
1164
|
+
this.sessionsDir = options.sessionsDir ?? defaultSessionsDir2;
|
|
1165
|
+
this.requireSessionsDir = options.requireSessionsDir ?? false;
|
|
1166
|
+
}
|
|
1167
|
+
getNormalizedSessionsDir() {
|
|
1168
|
+
if (isBlankText(this.sessionsDir)) {
|
|
1169
|
+
throw new Error("Droid sessions directory must be a non-empty path");
|
|
1170
|
+
}
|
|
1171
|
+
return this.sessionsDir.trim();
|
|
1172
|
+
}
|
|
1173
|
+
async discoverFiles() {
|
|
1174
|
+
const normalizedDir = this.getNormalizedSessionsDir();
|
|
1175
|
+
if (this.requireSessionsDir && !await pathReadable(normalizedDir)) {
|
|
1176
|
+
throw new Error(`Droid sessions directory is missing or unreadable: ${normalizedDir}`);
|
|
1177
|
+
}
|
|
1178
|
+
if (this.requireSessionsDir && !await pathIsDirectory(normalizedDir)) {
|
|
1179
|
+
throw new Error(`Droid sessions directory is not a directory: ${normalizedDir}`);
|
|
1180
|
+
}
|
|
1181
|
+
return discoverFiles(normalizedDir, { extension: ".settings.json" });
|
|
1182
|
+
}
|
|
1183
|
+
async parseFile(filePath) {
|
|
1184
|
+
const { events } = await this.parseFileWithDiagnostics(filePath);
|
|
1185
|
+
return events;
|
|
1186
|
+
}
|
|
1187
|
+
async parseFileWithDiagnostics(filePath) {
|
|
1188
|
+
const events = [];
|
|
1189
|
+
let skippedRows = 0;
|
|
1190
|
+
const skippedRowReasons = /* @__PURE__ */ new Map();
|
|
1191
|
+
let settingsJson;
|
|
1192
|
+
try {
|
|
1193
|
+
const content = await readFile2(filePath, "utf8");
|
|
1194
|
+
settingsJson = JSON.parse(content);
|
|
1195
|
+
} catch {
|
|
1196
|
+
skippedRows++;
|
|
1197
|
+
incrementSkippedReason(skippedRowReasons, "json_parse_error");
|
|
1198
|
+
return toParseDiagnostics(events, skippedRows, skippedRowReasons);
|
|
1199
|
+
}
|
|
1200
|
+
const settings = asRecord(settingsJson);
|
|
1201
|
+
if (!settings) {
|
|
1202
|
+
skippedRows++;
|
|
1203
|
+
incrementSkippedReason(skippedRowReasons, "invalid_settings_data");
|
|
1204
|
+
return toParseDiagnostics(events, skippedRows, skippedRowReasons);
|
|
1205
|
+
}
|
|
1206
|
+
const tokenUsage = asRecord(settings.tokenUsage);
|
|
1207
|
+
if (!tokenUsage) {
|
|
1208
|
+
skippedRows++;
|
|
1209
|
+
incrementSkippedReason(skippedRowReasons, "no_token_usage");
|
|
1210
|
+
return toParseDiagnostics(events, skippedRows, skippedRowReasons);
|
|
1211
|
+
}
|
|
1212
|
+
const inputTokens = normalizeNonNegativeInteger(toNumberLike(tokenUsage.inputTokens));
|
|
1213
|
+
const outputTokens = normalizeNonNegativeInteger(toNumberLike(tokenUsage.outputTokens));
|
|
1214
|
+
const reasoningTokens = normalizeNonNegativeInteger(toNumberLike(tokenUsage.thinkingTokens));
|
|
1215
|
+
const cacheReadTokens = normalizeNonNegativeInteger(toNumberLike(tokenUsage.cacheReadTokens));
|
|
1216
|
+
const cacheWriteTokens = normalizeNonNegativeInteger(
|
|
1217
|
+
toNumberLike(tokenUsage.cacheCreationTokens)
|
|
1218
|
+
);
|
|
1219
|
+
const billableTokens = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
|
|
1220
|
+
const totalTokens = billableTokens + reasoningTokens;
|
|
1221
|
+
if (billableTokens === 0) {
|
|
1222
|
+
skippedRows++;
|
|
1223
|
+
incrementSkippedReason(skippedRowReasons, "no_token_usage");
|
|
1224
|
+
return toParseDiagnostics(events, skippedRows, skippedRowReasons);
|
|
1225
|
+
}
|
|
1226
|
+
const provider = asTrimmedText(settings.providerLock);
|
|
1227
|
+
const model = asTrimmedText(settings.model);
|
|
1228
|
+
const primaryTimestamp = normalizeTimestampCandidate(settings.providerLockTimestamp);
|
|
1229
|
+
const hasValidPrimaryTimestamp = Boolean(primaryTimestamp);
|
|
1230
|
+
const jsonlPath = getSiblingJsonlPath(filePath);
|
|
1231
|
+
let repoRoot;
|
|
1232
|
+
let fallbackMessageTimestamp;
|
|
1233
|
+
try {
|
|
1234
|
+
for await (const line of readJsonlObjects(jsonlPath, {
|
|
1235
|
+
shouldParseLine: shouldParseDroidJsonlLine
|
|
1236
|
+
})) {
|
|
1237
|
+
if (!repoRoot && isSessionStartRecord(line)) {
|
|
1238
|
+
repoRoot = resolveRepoRootFromSessionStart(line) ?? repoRoot;
|
|
1239
|
+
if (hasValidPrimaryTimestamp) {
|
|
1240
|
+
break;
|
|
1241
|
+
}
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
if (!hasValidPrimaryTimestamp && isMessageRecord(line)) {
|
|
1245
|
+
fallbackMessageTimestamp = normalizeTimestampCandidate(line.timestamp);
|
|
1246
|
+
break;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
} catch {
|
|
1250
|
+
}
|
|
1251
|
+
const timestamp = primaryTimestamp ?? fallbackMessageTimestamp;
|
|
1252
|
+
if (!timestamp) {
|
|
1253
|
+
skippedRows++;
|
|
1254
|
+
incrementSkippedReason(skippedRowReasons, "invalid_timestamp");
|
|
1255
|
+
return toParseDiagnostics(events, skippedRows, skippedRowReasons);
|
|
1256
|
+
}
|
|
1257
|
+
try {
|
|
1258
|
+
events.push(
|
|
1259
|
+
createUsageEvent({
|
|
1260
|
+
source: this.id,
|
|
1261
|
+
sessionId: getSettingsSessionId(filePath),
|
|
1262
|
+
timestamp,
|
|
1263
|
+
repoRoot,
|
|
1264
|
+
provider,
|
|
1265
|
+
model,
|
|
1266
|
+
inputTokens,
|
|
1267
|
+
outputTokens,
|
|
1268
|
+
reasoningTokens,
|
|
1269
|
+
cacheReadTokens,
|
|
1270
|
+
cacheWriteTokens,
|
|
1271
|
+
totalTokens,
|
|
1272
|
+
costMode: "estimated"
|
|
1273
|
+
})
|
|
1274
|
+
);
|
|
1275
|
+
} catch {
|
|
1276
|
+
skippedRows++;
|
|
1277
|
+
incrementSkippedReason(skippedRowReasons, "event_creation_failed");
|
|
1278
|
+
}
|
|
1279
|
+
return toParseDiagnostics(events, skippedRows, skippedRowReasons);
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
// src/sources/gemini/gemini-source-adapter.ts
|
|
1284
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1285
|
+
import os4 from "os";
|
|
1286
|
+
import path6 from "path";
|
|
1287
|
+
var defaultGeminiDir = path6.join(os4.homedir(), ".gemini");
|
|
1288
|
+
function parseProjectsJson(data) {
|
|
1289
|
+
const mapping = /* @__PURE__ */ new Map();
|
|
1290
|
+
const record = asRecord(data);
|
|
1291
|
+
if (!record) {
|
|
1292
|
+
return mapping;
|
|
1293
|
+
}
|
|
1294
|
+
const projects = asRecord(record.projects);
|
|
1295
|
+
if (!projects) {
|
|
1296
|
+
return mapping;
|
|
1297
|
+
}
|
|
1298
|
+
for (const [key, value] of Object.entries(projects)) {
|
|
1299
|
+
const projectEntry = asRecord(value);
|
|
1300
|
+
const absolutePath = asTrimmedText(projectEntry?.absolutePath);
|
|
1301
|
+
if (absolutePath) {
|
|
1302
|
+
mapping.set(key, absolutePath);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return mapping;
|
|
1306
|
+
}
|
|
1307
|
+
async function loadProjectsJson(geminiDir) {
|
|
1308
|
+
const projectsPath = path6.join(geminiDir, "projects.json");
|
|
1309
|
+
try {
|
|
1310
|
+
const content = await readFile3(projectsPath, "utf8");
|
|
1311
|
+
const parsed = JSON.parse(content);
|
|
1312
|
+
return parseProjectsJson(parsed);
|
|
1313
|
+
} catch {
|
|
1314
|
+
return /* @__PURE__ */ new Map();
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
async function discoverSessionFiles(geminiDir) {
|
|
1318
|
+
const tmpDir = path6.join(geminiDir, "tmp");
|
|
1319
|
+
const allSessionFiles = [];
|
|
1320
|
+
const discoveredFiles = await discoverFiles(tmpDir, { extension: ".json" });
|
|
1321
|
+
for (const filePath of discoveredFiles) {
|
|
1322
|
+
const parentDir = path6.basename(path6.dirname(filePath));
|
|
1323
|
+
if (parentDir.toLowerCase() === "chats") {
|
|
1324
|
+
allSessionFiles.push(filePath);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
return allSessionFiles;
|
|
1328
|
+
}
|
|
1329
|
+
function resolveRepoRoot(filePath, sessionData, projectMapping) {
|
|
1330
|
+
const projectHash = asTrimmedText(sessionData.projectHash);
|
|
1331
|
+
if (projectHash) {
|
|
1332
|
+
const mappedRoot = projectMapping.get(projectHash);
|
|
1333
|
+
if (mappedRoot) {
|
|
1334
|
+
return mappedRoot;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
const chatsDir = path6.dirname(filePath);
|
|
1338
|
+
const projectDir = path6.dirname(chatsDir);
|
|
1339
|
+
const projectIdentifier = path6.basename(projectDir);
|
|
1340
|
+
return projectMapping.get(projectIdentifier);
|
|
1341
|
+
}
|
|
1342
|
+
function toFiniteNumber(value) {
|
|
1343
|
+
if (typeof value === "number") {
|
|
1344
|
+
return Number.isFinite(value) ? value : void 0;
|
|
1345
|
+
}
|
|
1346
|
+
if (typeof value !== "string") {
|
|
1347
|
+
return void 0;
|
|
1348
|
+
}
|
|
1349
|
+
const trimmed = value.trim();
|
|
1350
|
+
if (!trimmed) {
|
|
1351
|
+
return void 0;
|
|
1352
|
+
}
|
|
1353
|
+
const parsed = Number(trimmed);
|
|
1354
|
+
if (!Number.isFinite(parsed)) {
|
|
1355
|
+
return void 0;
|
|
1356
|
+
}
|
|
1357
|
+
return parsed;
|
|
1358
|
+
}
|
|
1359
|
+
function extractTokenUsage(tokens) {
|
|
1360
|
+
if (!tokens) {
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
const input = Math.max(0, toFiniteNumber(tokens.input) ?? 0);
|
|
1364
|
+
const tool = Math.max(0, toFiniteNumber(tokens.tool) ?? 0);
|
|
1365
|
+
const output = Math.max(0, toFiniteNumber(tokens.output) ?? 0);
|
|
1366
|
+
const thoughts = Math.max(0, toFiniteNumber(tokens.thoughts) ?? 0);
|
|
1367
|
+
const cached = Math.max(0, toFiniteNumber(tokens.cached) ?? 0);
|
|
1368
|
+
const inputTokens = input + tool;
|
|
1369
|
+
const outputTokens = output;
|
|
1370
|
+
const reasoningTokens = thoughts;
|
|
1371
|
+
const cacheReadTokens = cached;
|
|
1372
|
+
const declaredTotal = Math.max(0, toFiniteNumber(tokens.total) ?? 0);
|
|
1373
|
+
const componentTotal = inputTokens + outputTokens + reasoningTokens + cacheReadTokens;
|
|
1374
|
+
const totalTokens = declaredTotal > 0 ? declaredTotal : componentTotal;
|
|
1375
|
+
if (inputTokens === 0 && outputTokens === 0 && reasoningTokens === 0 && cached === 0) {
|
|
1376
|
+
return null;
|
|
1377
|
+
}
|
|
1378
|
+
return {
|
|
1379
|
+
inputTokens,
|
|
1380
|
+
outputTokens,
|
|
1381
|
+
reasoningTokens,
|
|
1382
|
+
cacheReadTokens,
|
|
1383
|
+
totalTokens
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
function normalizeTimestamp2(candidate) {
|
|
1387
|
+
if (typeof candidate !== "string" || isBlankText(candidate)) {
|
|
1388
|
+
return void 0;
|
|
1389
|
+
}
|
|
1390
|
+
const date = new Date(candidate.trim());
|
|
1391
|
+
if (Number.isNaN(date.getTime())) {
|
|
1392
|
+
return void 0;
|
|
1393
|
+
}
|
|
1394
|
+
return date.toISOString();
|
|
1395
|
+
}
|
|
1396
|
+
var GeminiSourceAdapter = class {
|
|
1397
|
+
id = "gemini";
|
|
1398
|
+
geminiDir;
|
|
1399
|
+
requireGeminiDir;
|
|
1400
|
+
projectMapping = null;
|
|
1401
|
+
constructor(options = {}) {
|
|
1402
|
+
this.geminiDir = options.geminiDir ?? defaultGeminiDir;
|
|
1403
|
+
this.requireGeminiDir = options.requireGeminiDir ?? false;
|
|
1404
|
+
}
|
|
1405
|
+
getNormalizedGeminiDir() {
|
|
1406
|
+
if (isBlankText(this.geminiDir)) {
|
|
1407
|
+
throw new Error("Gemini directory must be a non-empty path");
|
|
1408
|
+
}
|
|
1409
|
+
return this.geminiDir.trim();
|
|
1410
|
+
}
|
|
1411
|
+
async getProjectMapping(normalizedGeminiDir) {
|
|
1412
|
+
if (this.projectMapping) {
|
|
1413
|
+
return this.projectMapping;
|
|
1414
|
+
}
|
|
1415
|
+
this.projectMapping = await loadProjectsJson(normalizedGeminiDir);
|
|
1416
|
+
return this.projectMapping;
|
|
1417
|
+
}
|
|
1418
|
+
async getProjectMappingForParse() {
|
|
1419
|
+
if (this.projectMapping) {
|
|
1420
|
+
return this.projectMapping;
|
|
1421
|
+
}
|
|
1422
|
+
if (isBlankText(this.geminiDir)) {
|
|
1423
|
+
return /* @__PURE__ */ new Map();
|
|
1424
|
+
}
|
|
1425
|
+
this.projectMapping = await loadProjectsJson(this.geminiDir.trim());
|
|
1426
|
+
return this.projectMapping;
|
|
1427
|
+
}
|
|
1428
|
+
async discoverFiles() {
|
|
1429
|
+
const normalizedDir = this.getNormalizedGeminiDir();
|
|
1430
|
+
if (this.requireGeminiDir && !await pathReadable(normalizedDir)) {
|
|
1431
|
+
throw new Error(`Gemini directory is missing or unreadable: ${normalizedDir}`);
|
|
1432
|
+
}
|
|
1433
|
+
if (this.requireGeminiDir && !await pathIsDirectory(normalizedDir)) {
|
|
1434
|
+
throw new Error(`Gemini directory is not a directory: ${normalizedDir}`);
|
|
1435
|
+
}
|
|
1436
|
+
await this.getProjectMapping(normalizedDir);
|
|
1437
|
+
return discoverSessionFiles(normalizedDir);
|
|
1438
|
+
}
|
|
1439
|
+
async parseFile(filePath) {
|
|
1440
|
+
const { events } = await this.parseFileWithDiagnostics(filePath);
|
|
1441
|
+
return events;
|
|
1442
|
+
}
|
|
1443
|
+
async parseFileWithDiagnostics(filePath) {
|
|
1444
|
+
const events = [];
|
|
1445
|
+
let skippedRows = 0;
|
|
1446
|
+
const skippedRowReasons = /* @__PURE__ */ new Map();
|
|
1447
|
+
let sessionData;
|
|
1448
|
+
try {
|
|
1449
|
+
const content = await readFile3(filePath, "utf8");
|
|
1450
|
+
sessionData = JSON.parse(content);
|
|
1451
|
+
} catch {
|
|
1452
|
+
skippedRows++;
|
|
1453
|
+
incrementSkippedReason(skippedRowReasons, "json_parse_error");
|
|
1454
|
+
return toParseDiagnostics(events, skippedRows, skippedRowReasons);
|
|
1455
|
+
}
|
|
1456
|
+
const sessionDataRecord = asRecord(sessionData);
|
|
1457
|
+
if (!sessionDataRecord) {
|
|
1458
|
+
skippedRows++;
|
|
1459
|
+
incrementSkippedReason(skippedRowReasons, "invalid_session_data");
|
|
1460
|
+
return toParseDiagnostics(events, skippedRows, skippedRowReasons);
|
|
1461
|
+
}
|
|
1462
|
+
const sessionId = asTrimmedText(sessionDataRecord.sessionId) ?? path6.basename(filePath, ".json");
|
|
1463
|
+
const projectMapping = await this.getProjectMappingForParse();
|
|
1464
|
+
const repoRoot = resolveRepoRoot(filePath, sessionDataRecord, projectMapping);
|
|
1465
|
+
if (!Array.isArray(sessionDataRecord.messages)) {
|
|
1466
|
+
skippedRows++;
|
|
1467
|
+
incrementSkippedReason(skippedRowReasons, "invalid_messages_array");
|
|
1468
|
+
return toParseDiagnostics(events, skippedRows, skippedRowReasons);
|
|
1469
|
+
}
|
|
1470
|
+
const messages = sessionDataRecord.messages;
|
|
1471
|
+
for (const rawMessage of messages) {
|
|
1472
|
+
const message = asRecord(rawMessage);
|
|
1473
|
+
if (!message) {
|
|
1474
|
+
skippedRows++;
|
|
1475
|
+
incrementSkippedReason(skippedRowReasons, "invalid_message");
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
if (message.type !== "gemini") {
|
|
1479
|
+
skippedRows++;
|
|
1480
|
+
incrementSkippedReason(skippedRowReasons, "non_gemini_message");
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
const tokens = extractTokenUsage(asRecord(message.tokens));
|
|
1484
|
+
if (!tokens) {
|
|
1485
|
+
skippedRows++;
|
|
1486
|
+
incrementSkippedReason(skippedRowReasons, "no_token_usage");
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
const timestamp = normalizeTimestamp2(message.timestamp);
|
|
1490
|
+
if (!timestamp) {
|
|
1491
|
+
skippedRows++;
|
|
1492
|
+
incrementSkippedReason(skippedRowReasons, "invalid_timestamp");
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
const model = asTrimmedText(message.model);
|
|
1496
|
+
try {
|
|
1497
|
+
events.push(
|
|
1498
|
+
createUsageEvent({
|
|
1499
|
+
source: this.id,
|
|
1500
|
+
sessionId,
|
|
1501
|
+
timestamp,
|
|
1502
|
+
repoRoot,
|
|
1503
|
+
provider: "google",
|
|
1504
|
+
model,
|
|
1505
|
+
...tokens,
|
|
1506
|
+
costMode: "estimated"
|
|
1507
|
+
})
|
|
1508
|
+
);
|
|
1509
|
+
} catch {
|
|
1510
|
+
skippedRows++;
|
|
1511
|
+
incrementSkippedReason(skippedRowReasons, "event_creation_failed");
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
return toParseDiagnostics(events, skippedRows, skippedRowReasons);
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
|
|
1518
|
+
// src/sources/opencode/opencode-db-path-resolver.ts
|
|
1519
|
+
import os5 from "os";
|
|
1520
|
+
import path7 from "path";
|
|
1076
1521
|
function deduplicate(paths) {
|
|
1077
1522
|
return [...new Set(paths)];
|
|
1078
1523
|
}
|
|
1079
1524
|
function getLinuxLikeCandidates(homeDir, env) {
|
|
1080
|
-
const xdgDataHome = env.XDG_DATA_HOME ??
|
|
1525
|
+
const xdgDataHome = env.XDG_DATA_HOME ?? path7.join(homeDir, ".local", "share");
|
|
1081
1526
|
return [
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1527
|
+
path7.join(xdgDataHome, "opencode", "opencode.db"),
|
|
1528
|
+
path7.join(xdgDataHome, "opencode", "db.sqlite"),
|
|
1529
|
+
path7.join(homeDir, ".opencode", "opencode.db"),
|
|
1530
|
+
path7.join(homeDir, ".opencode", "db.sqlite")
|
|
1086
1531
|
];
|
|
1087
1532
|
}
|
|
1088
1533
|
function getMacOsCandidates(homeDir) {
|
|
1089
|
-
const appSupportDir =
|
|
1534
|
+
const appSupportDir = path7.join(homeDir, "Library", "Application Support");
|
|
1090
1535
|
return [
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1536
|
+
path7.join(appSupportDir, "opencode", "opencode.db"),
|
|
1537
|
+
path7.join(appSupportDir, "opencode", "db.sqlite"),
|
|
1538
|
+
path7.join(homeDir, ".opencode", "opencode.db"),
|
|
1539
|
+
path7.join(homeDir, ".opencode", "db.sqlite")
|
|
1095
1540
|
];
|
|
1096
1541
|
}
|
|
1097
1542
|
function getWindowsCandidates(homeDir, env) {
|
|
1098
|
-
const roamingBase = env.APPDATA ?? env.LOCALAPPDATA ?? (env.USERPROFILE ?
|
|
1543
|
+
const roamingBase = env.APPDATA ?? env.LOCALAPPDATA ?? (env.USERPROFILE ? path7.join(env.USERPROFILE, "AppData", "Roaming") : void 0);
|
|
1099
1544
|
const roamingCandidates = roamingBase ? [
|
|
1100
|
-
|
|
1101
|
-
|
|
1545
|
+
path7.join(roamingBase, "opencode", "opencode.db"),
|
|
1546
|
+
path7.join(roamingBase, "opencode", "db.sqlite")
|
|
1102
1547
|
] : [];
|
|
1103
1548
|
return [
|
|
1104
1549
|
...roamingCandidates,
|
|
1105
|
-
|
|
1106
|
-
|
|
1550
|
+
path7.join(homeDir, ".opencode", "opencode.db"),
|
|
1551
|
+
path7.join(homeDir, ".opencode", "db.sqlite")
|
|
1107
1552
|
];
|
|
1108
1553
|
}
|
|
1109
1554
|
function getDefaultOpenCodeDbPathCandidates(options = {}) {
|
|
1110
1555
|
const platform = options.platform ?? process.platform;
|
|
1111
|
-
const homeDir = options.homeDir ??
|
|
1556
|
+
const homeDir = options.homeDir ?? os5.homedir();
|
|
1112
1557
|
const env = options.env ?? process.env;
|
|
1113
1558
|
switch (platform) {
|
|
1114
1559
|
case "win32":
|
|
@@ -1174,10 +1619,10 @@ async function loadNodeSqliteModule() {
|
|
|
1174
1619
|
}
|
|
1175
1620
|
|
|
1176
1621
|
// src/sources/opencode/opencode-row-parser.ts
|
|
1177
|
-
var
|
|
1178
|
-
function
|
|
1622
|
+
var UNIX_SECONDS_ABS_CUTOFF2 = 1e10;
|
|
1623
|
+
function normalizeTimestampCandidate2(candidate) {
|
|
1179
1624
|
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
1180
|
-
const timestampMs = Math.abs(candidate) <=
|
|
1625
|
+
const timestampMs = Math.abs(candidate) <= UNIX_SECONDS_ABS_CUTOFF2 ? candidate * 1e3 : candidate;
|
|
1181
1626
|
const date = new Date(timestampMs);
|
|
1182
1627
|
if (Number.isNaN(date.getTime())) {
|
|
1183
1628
|
return void 0;
|
|
@@ -1191,7 +1636,7 @@ function normalizeTimestampCandidate(candidate) {
|
|
|
1191
1636
|
}
|
|
1192
1637
|
const numericTimestamp = Number(trimmed);
|
|
1193
1638
|
if (Number.isFinite(numericTimestamp)) {
|
|
1194
|
-
return
|
|
1639
|
+
return normalizeTimestampCandidate2(numericTimestamp);
|
|
1195
1640
|
}
|
|
1196
1641
|
const date = new Date(trimmed);
|
|
1197
1642
|
if (Number.isNaN(date.getTime())) {
|
|
@@ -1209,7 +1654,7 @@ function resolveTimestamp(rowTimestamp, messagePayload) {
|
|
|
1209
1654
|
messagePayload.time_created
|
|
1210
1655
|
];
|
|
1211
1656
|
for (const candidate of timestampCandidates) {
|
|
1212
|
-
const resolved =
|
|
1657
|
+
const resolved = normalizeTimestampCandidate2(candidate);
|
|
1213
1658
|
if (resolved) {
|
|
1214
1659
|
return resolved;
|
|
1215
1660
|
}
|
|
@@ -1236,7 +1681,7 @@ function normalizeSessionIdCandidate(value) {
|
|
|
1236
1681
|
}
|
|
1237
1682
|
return asTrimmedText(value);
|
|
1238
1683
|
}
|
|
1239
|
-
function
|
|
1684
|
+
function resolveRepoRoot2(messagePayload) {
|
|
1240
1685
|
const pathPayload = asRecord(messagePayload.path);
|
|
1241
1686
|
return asTrimmedText(pathPayload?.root) ?? asTrimmedText(pathPayload?.cwd) ?? asTrimmedText(messagePayload.cwd) ?? asTrimmedText(messagePayload.repo_root) ?? asTrimmedText(messagePayload.repoRoot) ?? asTrimmedText(messagePayload.project_root) ?? asTrimmedText(messagePayload.projectRoot);
|
|
1242
1687
|
}
|
|
@@ -1291,7 +1736,7 @@ function parseOpenCodeMessageRows(rows, sourceId) {
|
|
|
1291
1736
|
}
|
|
1292
1737
|
const provider = asTrimmedText(payload.providerID) ?? asTrimmedText(payload.provider);
|
|
1293
1738
|
const model = asTrimmedText(payload.modelID) ?? asTrimmedText(payload.model);
|
|
1294
|
-
const repoRoot =
|
|
1739
|
+
const repoRoot = resolveRepoRoot2(payload);
|
|
1295
1740
|
const tokens = asRecord(payload.tokens);
|
|
1296
1741
|
const tokenCache = asRecord(tokens?.cache);
|
|
1297
1742
|
const inputTokens = toNumberLike(tokens?.input);
|
|
@@ -1615,9 +2060,9 @@ var OpenCodeSourceAdapter = class {
|
|
|
1615
2060
|
};
|
|
1616
2061
|
|
|
1617
2062
|
// src/sources/pi/pi-source-adapter.ts
|
|
1618
|
-
import
|
|
1619
|
-
import
|
|
1620
|
-
var
|
|
2063
|
+
import os6 from "os";
|
|
2064
|
+
import path8 from "path";
|
|
2065
|
+
var defaultSessionsDir3 = path8.join(os6.homedir(), ".pi", "agent", "sessions");
|
|
1621
2066
|
var PI_MESSAGE_LINE_PATTERN = /"type"\s*:\s*"message"/u;
|
|
1622
2067
|
var PI_SESSION_LINE_PATTERN = /"type"\s*:\s*"session"/u;
|
|
1623
2068
|
var PI_MODEL_CHANGE_LINE_PATTERN = /"type"\s*:\s*"model_change"/u;
|
|
@@ -1625,14 +2070,11 @@ function shouldParsePiJsonlLine(lineText) {
|
|
|
1625
2070
|
return PI_MESSAGE_LINE_PATTERN.test(lineText) || PI_SESSION_LINE_PATTERN.test(lineText) || PI_MODEL_CHANGE_LINE_PATTERN.test(lineText);
|
|
1626
2071
|
}
|
|
1627
2072
|
var allowAllProviders = () => true;
|
|
1628
|
-
var
|
|
1629
|
-
function
|
|
1630
|
-
return value.trim().length === 0;
|
|
1631
|
-
}
|
|
1632
|
-
function normalizeTimestampCandidate2(candidate) {
|
|
2073
|
+
var UNIX_SECONDS_ABS_CUTOFF3 = 1e10;
|
|
2074
|
+
function normalizeTimestampCandidate3(candidate) {
|
|
1633
2075
|
let date;
|
|
1634
2076
|
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
1635
|
-
const timestampMs = Math.abs(candidate) <=
|
|
2077
|
+
const timestampMs = Math.abs(candidate) <= UNIX_SECONDS_ABS_CUTOFF3 ? candidate * 1e3 : candidate;
|
|
1636
2078
|
date = new Date(timestampMs);
|
|
1637
2079
|
} else {
|
|
1638
2080
|
const normalizedText = asTrimmedText(candidate);
|
|
@@ -1649,7 +2091,7 @@ function normalizeTimestampCandidate2(candidate) {
|
|
|
1649
2091
|
function resolveTimestamp2(line, message, state) {
|
|
1650
2092
|
const candidates = [line.timestamp, message?.timestamp, state.sessionTimestamp];
|
|
1651
2093
|
for (const candidate of candidates) {
|
|
1652
|
-
const normalizedTimestamp =
|
|
2094
|
+
const normalizedTimestamp = normalizeTimestampCandidate3(candidate);
|
|
1653
2095
|
if (normalizedTimestamp) {
|
|
1654
2096
|
return normalizedTimestamp;
|
|
1655
2097
|
}
|
|
@@ -1669,7 +2111,7 @@ function extractUsageFromRecord(usage) {
|
|
|
1669
2111
|
totalTokens: toNumberLike(usage.totalTokens),
|
|
1670
2112
|
costUsd: toNumberLike(cost?.total)
|
|
1671
2113
|
};
|
|
1672
|
-
const
|
|
2114
|
+
const toFiniteNumber2 = (value) => {
|
|
1673
2115
|
if (value === null || value === void 0) {
|
|
1674
2116
|
return void 0;
|
|
1675
2117
|
}
|
|
@@ -1691,10 +2133,10 @@ function extractUsageFromRecord(usage) {
|
|
|
1691
2133
|
extracted.totalTokens
|
|
1692
2134
|
];
|
|
1693
2135
|
const hasPositiveUsageSignal = usageCandidates.some((value) => {
|
|
1694
|
-
const parsed =
|
|
2136
|
+
const parsed = toFiniteNumber2(value);
|
|
1695
2137
|
return parsed !== void 0 && parsed > 0;
|
|
1696
2138
|
});
|
|
1697
|
-
const explicitCost =
|
|
2139
|
+
const explicitCost = toFiniteNumber2(extracted.costUsd);
|
|
1698
2140
|
const hasPositiveCostSignal = explicitCost !== void 0 && explicitCost > 0;
|
|
1699
2141
|
return hasPositiveUsageSignal || hasPositiveCostSignal ? extracted : void 0;
|
|
1700
2142
|
}
|
|
@@ -1713,7 +2155,7 @@ function extractUsage(line, message) {
|
|
|
1713
2155
|
return extractUsageFromRecord(messageUsage);
|
|
1714
2156
|
}
|
|
1715
2157
|
function getFallbackSessionId2(filePath) {
|
|
1716
|
-
return
|
|
2158
|
+
return path8.basename(filePath, ".jsonl");
|
|
1717
2159
|
}
|
|
1718
2160
|
function resolveRepoRootFromRecord(record) {
|
|
1719
2161
|
if (!record) {
|
|
@@ -1728,12 +2170,12 @@ var PiSourceAdapter = class {
|
|
|
1728
2170
|
providerFilter;
|
|
1729
2171
|
requireSessionsDir;
|
|
1730
2172
|
constructor(options = {}) {
|
|
1731
|
-
this.sessionsDir = options.sessionsDir ??
|
|
2173
|
+
this.sessionsDir = options.sessionsDir ?? defaultSessionsDir3;
|
|
1732
2174
|
this.providerFilter = options.providerFilter ?? allowAllProviders;
|
|
1733
2175
|
this.requireSessionsDir = options.requireSessionsDir ?? false;
|
|
1734
2176
|
}
|
|
1735
2177
|
async discoverFiles() {
|
|
1736
|
-
if (
|
|
2178
|
+
if (isBlankText(this.sessionsDir)) {
|
|
1737
2179
|
throw new Error("PI sessions directory must be a non-empty path");
|
|
1738
2180
|
}
|
|
1739
2181
|
const normalizedSessionsDir = this.sessionsDir.trim();
|
|
@@ -1851,6 +2293,36 @@ var sourceRegistrations = [
|
|
|
1851
2293
|
});
|
|
1852
2294
|
}
|
|
1853
2295
|
},
|
|
2296
|
+
{
|
|
2297
|
+
id: "gemini",
|
|
2298
|
+
sourceDirOverride: { kind: "directory" },
|
|
2299
|
+
create: (options, sourceDirectoryOverrides) => {
|
|
2300
|
+
const directoryConfig = resolveDirectoryConfig(
|
|
2301
|
+
"gemini",
|
|
2302
|
+
options.geminiDir,
|
|
2303
|
+
sourceDirectoryOverrides
|
|
2304
|
+
);
|
|
2305
|
+
return new GeminiSourceAdapter({
|
|
2306
|
+
geminiDir: directoryConfig.path,
|
|
2307
|
+
requireGeminiDir: directoryConfig.requireExistingPath
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
},
|
|
2311
|
+
{
|
|
2312
|
+
id: "droid",
|
|
2313
|
+
sourceDirOverride: { kind: "directory" },
|
|
2314
|
+
create: (options, sourceDirectoryOverrides) => {
|
|
2315
|
+
const directoryConfig = resolveDirectoryConfig(
|
|
2316
|
+
"droid",
|
|
2317
|
+
options.droidDir,
|
|
2318
|
+
sourceDirectoryOverrides
|
|
2319
|
+
);
|
|
2320
|
+
return new DroidSourceAdapter({
|
|
2321
|
+
sessionsDir: directoryConfig.path,
|
|
2322
|
+
requireSessionsDir: directoryConfig.requireExistingPath
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
},
|
|
1854
2326
|
{
|
|
1855
2327
|
id: "opencode",
|
|
1856
2328
|
sourceDirOverride: { kind: "unsupported", flag: "--opencode-db" },
|
|
@@ -1931,6 +2403,8 @@ function createDefaultAdapters(options) {
|
|
|
1931
2403
|
validateOpencodeOverride(options.opencodeDb);
|
|
1932
2404
|
validateDirectoryOverride("--pi-dir", options.piDir);
|
|
1933
2405
|
validateDirectoryOverride("--codex-dir", options.codexDir);
|
|
2406
|
+
validateDirectoryOverride("--gemini-dir", options.geminiDir);
|
|
2407
|
+
validateDirectoryOverride("--droid-dir", options.droidDir);
|
|
1934
2408
|
const sourceDirectoryOverrides = parseSourceDirectoryOverrides(options.sourceDir);
|
|
1935
2409
|
validateSourceDirectoryOverrideIds(sourceDirectoryOverrides);
|
|
1936
2410
|
return sourceRegistrations.map((source) => source.create(options, sourceDirectoryOverrides));
|
|
@@ -2306,7 +2780,7 @@ function aggregateEfficiency(options) {
|
|
|
2306
2780
|
// src/efficiency/git-outcome-collector.ts
|
|
2307
2781
|
import { spawn as spawn2 } from "child_process";
|
|
2308
2782
|
import { createInterface as createInterface3 } from "readline";
|
|
2309
|
-
import
|
|
2783
|
+
import path9 from "path";
|
|
2310
2784
|
import { stat as stat2 } from "fs/promises";
|
|
2311
2785
|
var GIT_COMMIT_MARKER = "";
|
|
2312
2786
|
var SHORTSTAT_PATTERN = /(\d+)\s+files?\s+changed(?:,\s+(\d+)\s+insertions?\(\+\))?(?:,\s+(\d+)\s+deletions?\(-\))?/u;
|
|
@@ -2326,13 +2800,13 @@ function resolveGitCommandFailureReason(result) {
|
|
|
2326
2800
|
}
|
|
2327
2801
|
function resolveRepoDir(repoDir) {
|
|
2328
2802
|
if (repoDir === void 0) {
|
|
2329
|
-
return
|
|
2803
|
+
return path9.resolve(process.cwd());
|
|
2330
2804
|
}
|
|
2331
2805
|
const normalizedRepoDir = repoDir.trim();
|
|
2332
2806
|
if (!normalizedRepoDir) {
|
|
2333
2807
|
throw new Error("--repo-dir must be a non-empty path");
|
|
2334
2808
|
}
|
|
2335
|
-
return
|
|
2809
|
+
return path9.resolve(normalizedRepoDir);
|
|
2336
2810
|
}
|
|
2337
2811
|
function getNodeErrorCode2(error) {
|
|
2338
2812
|
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
@@ -2704,21 +3178,21 @@ async function collectGitOutcomes(options, deps = {}) {
|
|
|
2704
3178
|
|
|
2705
3179
|
// src/efficiency/repo-attribution.ts
|
|
2706
3180
|
import { access as access2, constants as constants2, realpath } from "fs/promises";
|
|
2707
|
-
import
|
|
3181
|
+
import path10 from "path";
|
|
2708
3182
|
async function hasGitMarker(directoryPath) {
|
|
2709
3183
|
try {
|
|
2710
|
-
await access2(
|
|
3184
|
+
await access2(path10.join(directoryPath, ".git"), constants2.F_OK);
|
|
2711
3185
|
return true;
|
|
2712
3186
|
} catch {
|
|
2713
3187
|
return false;
|
|
2714
3188
|
}
|
|
2715
3189
|
}
|
|
2716
3190
|
function normalizeComparablePath(value) {
|
|
2717
|
-
const normalizedPath =
|
|
3191
|
+
const normalizedPath = path10.normalize(path10.resolve(value));
|
|
2718
3192
|
return process.platform === "win32" ? normalizedPath.toLowerCase() : normalizedPath;
|
|
2719
3193
|
}
|
|
2720
3194
|
async function resolveComparablePath(value) {
|
|
2721
|
-
const resolvedPath =
|
|
3195
|
+
const resolvedPath = path10.resolve(value);
|
|
2722
3196
|
try {
|
|
2723
3197
|
return normalizeComparablePath(await realpath(resolvedPath));
|
|
2724
3198
|
} catch {
|
|
@@ -2730,20 +3204,20 @@ async function resolveRepoRootFromPathHint(pathHint) {
|
|
|
2730
3204
|
if (!trimmedPath) {
|
|
2731
3205
|
return void 0;
|
|
2732
3206
|
}
|
|
2733
|
-
let currentPath =
|
|
3207
|
+
let currentPath = path10.resolve(trimmedPath);
|
|
2734
3208
|
for (; ; ) {
|
|
2735
3209
|
if (await hasGitMarker(currentPath)) {
|
|
2736
3210
|
return currentPath;
|
|
2737
3211
|
}
|
|
2738
|
-
const parentPath =
|
|
3212
|
+
const parentPath = path10.dirname(currentPath);
|
|
2739
3213
|
if (parentPath === currentPath) {
|
|
2740
3214
|
return void 0;
|
|
2741
3215
|
}
|
|
2742
3216
|
currentPath = parentPath;
|
|
2743
3217
|
}
|
|
2744
3218
|
}
|
|
2745
|
-
async function attributeUsageEventsToRepo(events, repoDir,
|
|
2746
|
-
const resolvedTargetRepoRoot = await
|
|
3219
|
+
async function attributeUsageEventsToRepo(events, repoDir, resolveRepoRoot3 = resolveRepoRootFromPathHint) {
|
|
3220
|
+
const resolvedTargetRepoRoot = await resolveRepoRoot3(repoDir).catch(() => void 0);
|
|
2747
3221
|
const targetRepoPath = await resolveComparablePath(resolvedTargetRepoRoot ?? repoDir);
|
|
2748
3222
|
const rootCache = /* @__PURE__ */ new Map();
|
|
2749
3223
|
const matchedEvents = [];
|
|
@@ -2756,7 +3230,7 @@ async function attributeUsageEventsToRepo(events, repoDir, resolveRepoRoot2 = re
|
|
|
2756
3230
|
}
|
|
2757
3231
|
const eventRepoRoot = event.repoRoot;
|
|
2758
3232
|
const cachedRootPromise = rootCache.get(eventRepoRoot) ?? (async () => {
|
|
2759
|
-
const resolvedRoot2 = await
|
|
3233
|
+
const resolvedRoot2 = await resolveRepoRoot3(eventRepoRoot).catch(() => void 0);
|
|
2760
3234
|
if (!resolvedRoot2) {
|
|
2761
3235
|
return void 0;
|
|
2762
3236
|
}
|
|
@@ -2989,6 +3463,12 @@ function resolveExplicitSourceIds(options, sourceFilter) {
|
|
|
2989
3463
|
if (options.codexDir) {
|
|
2990
3464
|
explicitSourceIds.add("codex");
|
|
2991
3465
|
}
|
|
3466
|
+
if (options.geminiDir) {
|
|
3467
|
+
explicitSourceIds.add("gemini");
|
|
3468
|
+
}
|
|
3469
|
+
if (options.droidDir) {
|
|
3470
|
+
explicitSourceIds.add("droid");
|
|
3471
|
+
}
|
|
2992
3472
|
if (options.opencodeDb) {
|
|
2993
3473
|
explicitSourceIds.add("opencode");
|
|
2994
3474
|
}
|
|
@@ -3056,8 +3536,8 @@ function normalizeSkippedRowReasons(value) {
|
|
|
3056
3536
|
}
|
|
3057
3537
|
|
|
3058
3538
|
// src/cli/parse-file-cache.ts
|
|
3059
|
-
import { mkdir as mkdir2, readFile as
|
|
3060
|
-
import
|
|
3539
|
+
import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
3540
|
+
import path11 from "path";
|
|
3061
3541
|
var PARSE_FILE_CACHE_VERSION = 2;
|
|
3062
3542
|
var CACHE_KEY_SEPARATOR = "\0";
|
|
3063
3543
|
function createCacheKey(source, filePath) {
|
|
@@ -3196,7 +3676,7 @@ function normalizeCacheEntry(value) {
|
|
|
3196
3676
|
};
|
|
3197
3677
|
}
|
|
3198
3678
|
function getDefaultParseFileCachePath() {
|
|
3199
|
-
return
|
|
3679
|
+
return path11.join(getUserCacheRootDir(), "llm-usage-metrics", "parse-file-cache.json");
|
|
3200
3680
|
}
|
|
3201
3681
|
var ParseFileCache = class _ParseFileCache {
|
|
3202
3682
|
constructor(cacheFilePath, limits, now) {
|
|
@@ -3282,7 +3762,7 @@ var ParseFileCache = class _ParseFileCache {
|
|
|
3282
3762
|
keptEntries.length = bestCount;
|
|
3283
3763
|
payloadText = bestPayloadText;
|
|
3284
3764
|
}
|
|
3285
|
-
await mkdir2(
|
|
3765
|
+
await mkdir2(path11.dirname(this.cacheFilePath), { recursive: true });
|
|
3286
3766
|
await writeFile2(this.cacheFilePath, payloadText, "utf8");
|
|
3287
3767
|
this.dirty = false;
|
|
3288
3768
|
}
|
|
@@ -3305,7 +3785,7 @@ var ParseFileCache = class _ParseFileCache {
|
|
|
3305
3785
|
async loadFromDisk() {
|
|
3306
3786
|
let content;
|
|
3307
3787
|
try {
|
|
3308
|
-
content = await
|
|
3788
|
+
content = await readFile4(this.cacheFilePath, "utf8");
|
|
3309
3789
|
} catch {
|
|
3310
3790
|
return;
|
|
3311
3791
|
}
|
|
@@ -3576,8 +4056,8 @@ function applyPricingToEvents(events, pricingSource) {
|
|
|
3576
4056
|
}
|
|
3577
4057
|
|
|
3578
4058
|
// src/pricing/litellm-pricing-fetcher.ts
|
|
3579
|
-
import { mkdir as mkdir3, readFile as
|
|
3580
|
-
import
|
|
4059
|
+
import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
4060
|
+
import path12 from "path";
|
|
3581
4061
|
|
|
3582
4062
|
// src/pricing/litellm-model-map.json
|
|
3583
4063
|
var litellm_model_map_default = {
|
|
@@ -3585,20 +4065,34 @@ var litellm_model_map_default = {
|
|
|
3585
4065
|
k2p5: "kimi-k2.5",
|
|
3586
4066
|
"kimi-k2p5": "kimi-k2.5",
|
|
3587
4067
|
"kimi-k2.5": "kimi-k2.5",
|
|
4068
|
+
"kimi-k2.5-free": "kimi-k2.5",
|
|
3588
4069
|
"moonshotai.kimi-k2.5": "kimi-k2.5",
|
|
3589
4070
|
"moonshot/kimi-k2.5": "kimi-k2.5",
|
|
4071
|
+
"gpt-5.3-codex-spark": "gpt-5.3-codex",
|
|
4072
|
+
"gemini-3-pro": "gemini-3-pro",
|
|
4073
|
+
"antigravity-gemini-3-flash": "gemini-3-flash",
|
|
4074
|
+
"antigravity-gemini-3-pro": "gemini-3-pro",
|
|
4075
|
+
"antigravity-gemini-3-pro-high": "gemini-3-pro",
|
|
4076
|
+
"minimax-m2.1": "minimax-m2.1",
|
|
4077
|
+
"minimax-m2.1-free": "minimax-m2.1",
|
|
4078
|
+
"minimax-m2.5": "minimax-m2.5",
|
|
4079
|
+
"minimax-m2.5-free": "minimax-m2.5",
|
|
3590
4080
|
"claude sonnet 4.6": "claude-sonnet-4.6",
|
|
3591
4081
|
"claude-sonnet-4.6": "claude-sonnet-4.6",
|
|
3592
4082
|
"claude-sonnet-4-6": "claude-sonnet-4.6",
|
|
3593
4083
|
"anthropic/claude-sonnet-4.6": "claude-sonnet-4.6",
|
|
3594
|
-
"anthropic.claude-sonnet-4-6": "claude-sonnet-4.6"
|
|
3595
|
-
"gpt-5.3-codex": "gpt-5.2-codex"
|
|
4084
|
+
"anthropic.claude-sonnet-4-6": "claude-sonnet-4.6"
|
|
3596
4085
|
},
|
|
3597
4086
|
notes: {
|
|
3598
|
-
"gpt-5.3-codex": "
|
|
4087
|
+
"gpt-5.3-codex-spark": "Alias to gpt-5.3-codex because upstream publishes token pricing on the gpt-5.3-codex key"
|
|
3599
4088
|
},
|
|
3600
4089
|
preferredPricingKeyByCanonicalModel: {
|
|
3601
4090
|
"kimi-k2.5": "moonshot/kimi-k2.5",
|
|
4091
|
+
"gpt-5.3-codex": "gpt-5.3-codex",
|
|
4092
|
+
"gemini-3-flash": "gemini/gemini-3-flash-preview",
|
|
4093
|
+
"gemini-3-pro": "gemini/gemini-3-pro-preview",
|
|
4094
|
+
"minimax-m2.1": "openrouter/minimax/minimax-m2.1",
|
|
4095
|
+
"minimax-m2.5": "openrouter/minimax/minimax-m2.5",
|
|
3602
4096
|
"claude-sonnet-4.6": "anthropic.claude-sonnet-4-6"
|
|
3603
4097
|
}
|
|
3604
4098
|
};
|
|
@@ -3747,7 +4241,7 @@ function normalizeLitellmPricingPayload(payload) {
|
|
|
3747
4241
|
return normalizedPricing;
|
|
3748
4242
|
}
|
|
3749
4243
|
function getDefaultLiteLLMPricingCachePath() {
|
|
3750
|
-
return
|
|
4244
|
+
return path12.join(getUserCacheRootDir(), "llm-usage-metrics", "litellm-pricing-cache.json");
|
|
3751
4245
|
}
|
|
3752
4246
|
function stripProviderPrefix(model) {
|
|
3753
4247
|
const slashIndex = model.lastIndexOf("/");
|
|
@@ -4106,7 +4600,7 @@ var LiteLLMPricingFetcher = class {
|
|
|
4106
4600
|
async readCachePayload() {
|
|
4107
4601
|
let content;
|
|
4108
4602
|
try {
|
|
4109
|
-
content = await
|
|
4603
|
+
content = await readFile5(this.cacheFilePath, "utf8");
|
|
4110
4604
|
} catch {
|
|
4111
4605
|
return void 0;
|
|
4112
4606
|
}
|
|
@@ -4141,7 +4635,7 @@ var LiteLLMPricingFetcher = class {
|
|
|
4141
4635
|
};
|
|
4142
4636
|
}
|
|
4143
4637
|
async writeCache() {
|
|
4144
|
-
const directoryPath =
|
|
4638
|
+
const directoryPath = path12.dirname(this.cacheFilePath);
|
|
4145
4639
|
await mkdir3(directoryPath, { recursive: true });
|
|
4146
4640
|
const payload = {
|
|
4147
4641
|
fetchedAt: this.now(),
|
|
@@ -4315,6 +4809,12 @@ function resolveScopeNote(options) {
|
|
|
4315
4809
|
if (hasActiveTextOption(options.codexDir)) {
|
|
4316
4810
|
activeFilters.push("--codex-dir");
|
|
4317
4811
|
}
|
|
4812
|
+
if (hasActiveTextOption(options.geminiDir)) {
|
|
4813
|
+
activeFilters.push("--gemini-dir");
|
|
4814
|
+
}
|
|
4815
|
+
if (hasActiveTextOption(options.droidDir)) {
|
|
4816
|
+
activeFilters.push("--droid-dir");
|
|
4817
|
+
}
|
|
4318
4818
|
if (hasActiveTextOption(options.opencodeDb)) {
|
|
4319
4819
|
activeFilters.push("--opencode-db");
|
|
4320
4820
|
}
|
|
@@ -4341,7 +4841,7 @@ function hasMeaningfulEfficiencyUsageSignal(event) {
|
|
|
4341
4841
|
async function buildEfficiencyData(granularity, options, deps = {}) {
|
|
4342
4842
|
const buildUsage = deps.buildUsageData ?? buildUsageData;
|
|
4343
4843
|
const collectOutcomes = deps.collectGitOutcomes ?? collectGitOutcomes;
|
|
4344
|
-
const
|
|
4844
|
+
const resolveRepoRoot3 = deps.resolveRepoRoot ?? resolveRepoRootFromPathHint;
|
|
4345
4845
|
const repoDir = options.repoDir?.trim();
|
|
4346
4846
|
if (options.repoDir !== void 0 && !repoDir) {
|
|
4347
4847
|
throw new Error("--repo-dir must be a non-empty path");
|
|
@@ -4350,7 +4850,7 @@ async function buildEfficiencyData(granularity, options, deps = {}) {
|
|
|
4350
4850
|
const attribution = await attributeUsageEventsToRepo(
|
|
4351
4851
|
usageData.events,
|
|
4352
4852
|
repoDir ?? process.cwd(),
|
|
4353
|
-
|
|
4853
|
+
resolveRepoRoot3
|
|
4354
4854
|
);
|
|
4355
4855
|
const matchedEventsWithSignal = attribution.matchedEvents.filter(
|
|
4356
4856
|
(event) => hasMeaningfulEfficiencyUsageSignal(event)
|
|
@@ -5547,7 +6047,7 @@ function addSharedOptions(command, options = {}) {
|
|
|
5547
6047
|
const allowedSourcesLabel = getAllowedSourcesLabel(supportedSourceIds);
|
|
5548
6048
|
const supportedSourcesSummary = `(${supportedSourceIds.length}): ${allowedSourcesLabel}`;
|
|
5549
6049
|
const includePerModelColumns = options.includePerModelColumns ?? true;
|
|
5550
|
-
const configuredCommand = command.option("--pi-dir <path>", "Path to .pi sessions directory").option("--codex-dir <path>", "Path to .codex sessions directory").option("--opencode-db <path>", "Path to OpenCode SQLite DB").option(
|
|
6050
|
+
const configuredCommand = command.option("--pi-dir <path>", "Path to .pi sessions directory").option("--codex-dir <path>", "Path to .codex sessions directory").option("--gemini-dir <path>", "Path to .gemini directory").option("--droid-dir <path>", "Path to Droid sessions directory").option("--opencode-db <path>", "Path to OpenCode SQLite DB").option(
|
|
5551
6051
|
"--source-dir <source-id=path>",
|
|
5552
6052
|
"Override source directory for directory-backed sources (repeatable)",
|
|
5553
6053
|
collectRepeatedOption,
|
|
@@ -5621,9 +6121,10 @@ function rootDescription() {
|
|
|
5621
6121
|
" $ llm-usage monthly --since 2026-01-01 --until 2026-01-31 --source pi,codex --json",
|
|
5622
6122
|
" $ llm-usage monthly --source opencode --opencode-db /path/to/opencode.db --json",
|
|
5623
6123
|
" $ llm-usage monthly --model claude --per-model-columns",
|
|
5624
|
-
" $ llm-usage daily --source-dir pi=/tmp/pi-sessions",
|
|
6124
|
+
" $ llm-usage daily --source-dir pi=/tmp/pi-sessions --source-dir gemini=/tmp/.gemini --source-dir droid=/tmp/droid-sessions",
|
|
6125
|
+
" $ llm-usage daily --pi-dir /tmp/pi-sessions --gemini-dir /tmp/.gemini --droid-dir /tmp/droid-sessions",
|
|
5625
6126
|
" $ llm-usage efficiency weekly --repo-dir /path/to/repo --json",
|
|
5626
|
-
" $ npx --yes llm-usage-metrics daily"
|
|
6127
|
+
" $ npx --yes llm-usage-metrics@latest daily"
|
|
5627
6128
|
].join("\n");
|
|
5628
6129
|
}
|
|
5629
6130
|
function createCli(options = {}) {
|