llm-usage-metrics 0.3.4 → 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 +61 -31
- package/dist/index.js +280 -80
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1094,11 +1094,197 @@ var CodexSourceAdapter = class {
|
|
|
1094
1094
|
}
|
|
1095
1095
|
};
|
|
1096
1096
|
|
|
1097
|
-
// src/sources/
|
|
1097
|
+
// src/sources/droid/droid-source-adapter.ts
|
|
1098
1098
|
import { readFile as readFile2 } from "fs/promises";
|
|
1099
1099
|
import os3 from "os";
|
|
1100
1100
|
import path5 from "path";
|
|
1101
|
-
|
|
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");
|
|
1102
1288
|
function parseProjectsJson(data) {
|
|
1103
1289
|
const mapping = /* @__PURE__ */ new Map();
|
|
1104
1290
|
const record = asRecord(data);
|
|
@@ -1119,9 +1305,9 @@ function parseProjectsJson(data) {
|
|
|
1119
1305
|
return mapping;
|
|
1120
1306
|
}
|
|
1121
1307
|
async function loadProjectsJson(geminiDir) {
|
|
1122
|
-
const projectsPath =
|
|
1308
|
+
const projectsPath = path6.join(geminiDir, "projects.json");
|
|
1123
1309
|
try {
|
|
1124
|
-
const content = await
|
|
1310
|
+
const content = await readFile3(projectsPath, "utf8");
|
|
1125
1311
|
const parsed = JSON.parse(content);
|
|
1126
1312
|
return parseProjectsJson(parsed);
|
|
1127
1313
|
} catch {
|
|
@@ -1129,11 +1315,11 @@ async function loadProjectsJson(geminiDir) {
|
|
|
1129
1315
|
}
|
|
1130
1316
|
}
|
|
1131
1317
|
async function discoverSessionFiles(geminiDir) {
|
|
1132
|
-
const tmpDir =
|
|
1318
|
+
const tmpDir = path6.join(geminiDir, "tmp");
|
|
1133
1319
|
const allSessionFiles = [];
|
|
1134
1320
|
const discoveredFiles = await discoverFiles(tmpDir, { extension: ".json" });
|
|
1135
1321
|
for (const filePath of discoveredFiles) {
|
|
1136
|
-
const parentDir =
|
|
1322
|
+
const parentDir = path6.basename(path6.dirname(filePath));
|
|
1137
1323
|
if (parentDir.toLowerCase() === "chats") {
|
|
1138
1324
|
allSessionFiles.push(filePath);
|
|
1139
1325
|
}
|
|
@@ -1148,9 +1334,9 @@ function resolveRepoRoot(filePath, sessionData, projectMapping) {
|
|
|
1148
1334
|
return mappedRoot;
|
|
1149
1335
|
}
|
|
1150
1336
|
}
|
|
1151
|
-
const chatsDir =
|
|
1152
|
-
const projectDir =
|
|
1153
|
-
const projectIdentifier =
|
|
1337
|
+
const chatsDir = path6.dirname(filePath);
|
|
1338
|
+
const projectDir = path6.dirname(chatsDir);
|
|
1339
|
+
const projectIdentifier = path6.basename(projectDir);
|
|
1154
1340
|
return projectMapping.get(projectIdentifier);
|
|
1155
1341
|
}
|
|
1156
1342
|
function toFiniteNumber(value) {
|
|
@@ -1197,20 +1383,6 @@ function extractTokenUsage(tokens) {
|
|
|
1197
1383
|
totalTokens
|
|
1198
1384
|
};
|
|
1199
1385
|
}
|
|
1200
|
-
function incrementSkippedReason(reasons, reason) {
|
|
1201
|
-
const current = reasons.get(reason) ?? 0;
|
|
1202
|
-
reasons.set(reason, current + 1);
|
|
1203
|
-
}
|
|
1204
|
-
function toSkippedRowReasonStats(reasons) {
|
|
1205
|
-
return [...reasons.entries()].map(([reason, count]) => ({ reason, count })).sort((left, right) => compareByCodePoint(left.reason, right.reason));
|
|
1206
|
-
}
|
|
1207
|
-
function toParseDiagnostics(events, skippedRows, skippedRowReasons) {
|
|
1208
|
-
return {
|
|
1209
|
-
events,
|
|
1210
|
-
skippedRows,
|
|
1211
|
-
skippedRowReasons: toSkippedRowReasonStats(skippedRowReasons)
|
|
1212
|
-
};
|
|
1213
|
-
}
|
|
1214
1386
|
function normalizeTimestamp2(candidate) {
|
|
1215
1387
|
if (typeof candidate !== "string" || isBlankText(candidate)) {
|
|
1216
1388
|
return void 0;
|
|
@@ -1274,7 +1446,7 @@ var GeminiSourceAdapter = class {
|
|
|
1274
1446
|
const skippedRowReasons = /* @__PURE__ */ new Map();
|
|
1275
1447
|
let sessionData;
|
|
1276
1448
|
try {
|
|
1277
|
-
const content = await
|
|
1449
|
+
const content = await readFile3(filePath, "utf8");
|
|
1278
1450
|
sessionData = JSON.parse(content);
|
|
1279
1451
|
} catch {
|
|
1280
1452
|
skippedRows++;
|
|
@@ -1287,7 +1459,7 @@ var GeminiSourceAdapter = class {
|
|
|
1287
1459
|
incrementSkippedReason(skippedRowReasons, "invalid_session_data");
|
|
1288
1460
|
return toParseDiagnostics(events, skippedRows, skippedRowReasons);
|
|
1289
1461
|
}
|
|
1290
|
-
const sessionId = asTrimmedText(sessionDataRecord.sessionId) ??
|
|
1462
|
+
const sessionId = asTrimmedText(sessionDataRecord.sessionId) ?? path6.basename(filePath, ".json");
|
|
1291
1463
|
const projectMapping = await this.getProjectMappingForParse();
|
|
1292
1464
|
const repoRoot = resolveRepoRoot(filePath, sessionDataRecord, projectMapping);
|
|
1293
1465
|
if (!Array.isArray(sessionDataRecord.messages)) {
|
|
@@ -1344,44 +1516,44 @@ var GeminiSourceAdapter = class {
|
|
|
1344
1516
|
};
|
|
1345
1517
|
|
|
1346
1518
|
// src/sources/opencode/opencode-db-path-resolver.ts
|
|
1347
|
-
import
|
|
1348
|
-
import
|
|
1519
|
+
import os5 from "os";
|
|
1520
|
+
import path7 from "path";
|
|
1349
1521
|
function deduplicate(paths) {
|
|
1350
1522
|
return [...new Set(paths)];
|
|
1351
1523
|
}
|
|
1352
1524
|
function getLinuxLikeCandidates(homeDir, env) {
|
|
1353
|
-
const xdgDataHome = env.XDG_DATA_HOME ??
|
|
1525
|
+
const xdgDataHome = env.XDG_DATA_HOME ?? path7.join(homeDir, ".local", "share");
|
|
1354
1526
|
return [
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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")
|
|
1359
1531
|
];
|
|
1360
1532
|
}
|
|
1361
1533
|
function getMacOsCandidates(homeDir) {
|
|
1362
|
-
const appSupportDir =
|
|
1534
|
+
const appSupportDir = path7.join(homeDir, "Library", "Application Support");
|
|
1363
1535
|
return [
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
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")
|
|
1368
1540
|
];
|
|
1369
1541
|
}
|
|
1370
1542
|
function getWindowsCandidates(homeDir, env) {
|
|
1371
|
-
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);
|
|
1372
1544
|
const roamingCandidates = roamingBase ? [
|
|
1373
|
-
|
|
1374
|
-
|
|
1545
|
+
path7.join(roamingBase, "opencode", "opencode.db"),
|
|
1546
|
+
path7.join(roamingBase, "opencode", "db.sqlite")
|
|
1375
1547
|
] : [];
|
|
1376
1548
|
return [
|
|
1377
1549
|
...roamingCandidates,
|
|
1378
|
-
|
|
1379
|
-
|
|
1550
|
+
path7.join(homeDir, ".opencode", "opencode.db"),
|
|
1551
|
+
path7.join(homeDir, ".opencode", "db.sqlite")
|
|
1380
1552
|
];
|
|
1381
1553
|
}
|
|
1382
1554
|
function getDefaultOpenCodeDbPathCandidates(options = {}) {
|
|
1383
1555
|
const platform = options.platform ?? process.platform;
|
|
1384
|
-
const homeDir = options.homeDir ??
|
|
1556
|
+
const homeDir = options.homeDir ?? os5.homedir();
|
|
1385
1557
|
const env = options.env ?? process.env;
|
|
1386
1558
|
switch (platform) {
|
|
1387
1559
|
case "win32":
|
|
@@ -1447,10 +1619,10 @@ async function loadNodeSqliteModule() {
|
|
|
1447
1619
|
}
|
|
1448
1620
|
|
|
1449
1621
|
// src/sources/opencode/opencode-row-parser.ts
|
|
1450
|
-
var
|
|
1451
|
-
function
|
|
1622
|
+
var UNIX_SECONDS_ABS_CUTOFF2 = 1e10;
|
|
1623
|
+
function normalizeTimestampCandidate2(candidate) {
|
|
1452
1624
|
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
1453
|
-
const timestampMs = Math.abs(candidate) <=
|
|
1625
|
+
const timestampMs = Math.abs(candidate) <= UNIX_SECONDS_ABS_CUTOFF2 ? candidate * 1e3 : candidate;
|
|
1454
1626
|
const date = new Date(timestampMs);
|
|
1455
1627
|
if (Number.isNaN(date.getTime())) {
|
|
1456
1628
|
return void 0;
|
|
@@ -1464,7 +1636,7 @@ function normalizeTimestampCandidate(candidate) {
|
|
|
1464
1636
|
}
|
|
1465
1637
|
const numericTimestamp = Number(trimmed);
|
|
1466
1638
|
if (Number.isFinite(numericTimestamp)) {
|
|
1467
|
-
return
|
|
1639
|
+
return normalizeTimestampCandidate2(numericTimestamp);
|
|
1468
1640
|
}
|
|
1469
1641
|
const date = new Date(trimmed);
|
|
1470
1642
|
if (Number.isNaN(date.getTime())) {
|
|
@@ -1482,7 +1654,7 @@ function resolveTimestamp(rowTimestamp, messagePayload) {
|
|
|
1482
1654
|
messagePayload.time_created
|
|
1483
1655
|
];
|
|
1484
1656
|
for (const candidate of timestampCandidates) {
|
|
1485
|
-
const resolved =
|
|
1657
|
+
const resolved = normalizeTimestampCandidate2(candidate);
|
|
1486
1658
|
if (resolved) {
|
|
1487
1659
|
return resolved;
|
|
1488
1660
|
}
|
|
@@ -1888,9 +2060,9 @@ var OpenCodeSourceAdapter = class {
|
|
|
1888
2060
|
};
|
|
1889
2061
|
|
|
1890
2062
|
// src/sources/pi/pi-source-adapter.ts
|
|
1891
|
-
import
|
|
1892
|
-
import
|
|
1893
|
-
var
|
|
2063
|
+
import os6 from "os";
|
|
2064
|
+
import path8 from "path";
|
|
2065
|
+
var defaultSessionsDir3 = path8.join(os6.homedir(), ".pi", "agent", "sessions");
|
|
1894
2066
|
var PI_MESSAGE_LINE_PATTERN = /"type"\s*:\s*"message"/u;
|
|
1895
2067
|
var PI_SESSION_LINE_PATTERN = /"type"\s*:\s*"session"/u;
|
|
1896
2068
|
var PI_MODEL_CHANGE_LINE_PATTERN = /"type"\s*:\s*"model_change"/u;
|
|
@@ -1898,11 +2070,11 @@ function shouldParsePiJsonlLine(lineText) {
|
|
|
1898
2070
|
return PI_MESSAGE_LINE_PATTERN.test(lineText) || PI_SESSION_LINE_PATTERN.test(lineText) || PI_MODEL_CHANGE_LINE_PATTERN.test(lineText);
|
|
1899
2071
|
}
|
|
1900
2072
|
var allowAllProviders = () => true;
|
|
1901
|
-
var
|
|
1902
|
-
function
|
|
2073
|
+
var UNIX_SECONDS_ABS_CUTOFF3 = 1e10;
|
|
2074
|
+
function normalizeTimestampCandidate3(candidate) {
|
|
1903
2075
|
let date;
|
|
1904
2076
|
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
1905
|
-
const timestampMs = Math.abs(candidate) <=
|
|
2077
|
+
const timestampMs = Math.abs(candidate) <= UNIX_SECONDS_ABS_CUTOFF3 ? candidate * 1e3 : candidate;
|
|
1906
2078
|
date = new Date(timestampMs);
|
|
1907
2079
|
} else {
|
|
1908
2080
|
const normalizedText = asTrimmedText(candidate);
|
|
@@ -1919,7 +2091,7 @@ function normalizeTimestampCandidate2(candidate) {
|
|
|
1919
2091
|
function resolveTimestamp2(line, message, state) {
|
|
1920
2092
|
const candidates = [line.timestamp, message?.timestamp, state.sessionTimestamp];
|
|
1921
2093
|
for (const candidate of candidates) {
|
|
1922
|
-
const normalizedTimestamp =
|
|
2094
|
+
const normalizedTimestamp = normalizeTimestampCandidate3(candidate);
|
|
1923
2095
|
if (normalizedTimestamp) {
|
|
1924
2096
|
return normalizedTimestamp;
|
|
1925
2097
|
}
|
|
@@ -1983,7 +2155,7 @@ function extractUsage(line, message) {
|
|
|
1983
2155
|
return extractUsageFromRecord(messageUsage);
|
|
1984
2156
|
}
|
|
1985
2157
|
function getFallbackSessionId2(filePath) {
|
|
1986
|
-
return
|
|
2158
|
+
return path8.basename(filePath, ".jsonl");
|
|
1987
2159
|
}
|
|
1988
2160
|
function resolveRepoRootFromRecord(record) {
|
|
1989
2161
|
if (!record) {
|
|
@@ -1998,7 +2170,7 @@ var PiSourceAdapter = class {
|
|
|
1998
2170
|
providerFilter;
|
|
1999
2171
|
requireSessionsDir;
|
|
2000
2172
|
constructor(options = {}) {
|
|
2001
|
-
this.sessionsDir = options.sessionsDir ??
|
|
2173
|
+
this.sessionsDir = options.sessionsDir ?? defaultSessionsDir3;
|
|
2002
2174
|
this.providerFilter = options.providerFilter ?? allowAllProviders;
|
|
2003
2175
|
this.requireSessionsDir = options.requireSessionsDir ?? false;
|
|
2004
2176
|
}
|
|
@@ -2136,6 +2308,21 @@ var sourceRegistrations = [
|
|
|
2136
2308
|
});
|
|
2137
2309
|
}
|
|
2138
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
|
+
},
|
|
2139
2326
|
{
|
|
2140
2327
|
id: "opencode",
|
|
2141
2328
|
sourceDirOverride: { kind: "unsupported", flag: "--opencode-db" },
|
|
@@ -2217,6 +2404,7 @@ function createDefaultAdapters(options) {
|
|
|
2217
2404
|
validateDirectoryOverride("--pi-dir", options.piDir);
|
|
2218
2405
|
validateDirectoryOverride("--codex-dir", options.codexDir);
|
|
2219
2406
|
validateDirectoryOverride("--gemini-dir", options.geminiDir);
|
|
2407
|
+
validateDirectoryOverride("--droid-dir", options.droidDir);
|
|
2220
2408
|
const sourceDirectoryOverrides = parseSourceDirectoryOverrides(options.sourceDir);
|
|
2221
2409
|
validateSourceDirectoryOverrideIds(sourceDirectoryOverrides);
|
|
2222
2410
|
return sourceRegistrations.map((source) => source.create(options, sourceDirectoryOverrides));
|
|
@@ -2592,7 +2780,7 @@ function aggregateEfficiency(options) {
|
|
|
2592
2780
|
// src/efficiency/git-outcome-collector.ts
|
|
2593
2781
|
import { spawn as spawn2 } from "child_process";
|
|
2594
2782
|
import { createInterface as createInterface3 } from "readline";
|
|
2595
|
-
import
|
|
2783
|
+
import path9 from "path";
|
|
2596
2784
|
import { stat as stat2 } from "fs/promises";
|
|
2597
2785
|
var GIT_COMMIT_MARKER = "";
|
|
2598
2786
|
var SHORTSTAT_PATTERN = /(\d+)\s+files?\s+changed(?:,\s+(\d+)\s+insertions?\(\+\))?(?:,\s+(\d+)\s+deletions?\(-\))?/u;
|
|
@@ -2612,13 +2800,13 @@ function resolveGitCommandFailureReason(result) {
|
|
|
2612
2800
|
}
|
|
2613
2801
|
function resolveRepoDir(repoDir) {
|
|
2614
2802
|
if (repoDir === void 0) {
|
|
2615
|
-
return
|
|
2803
|
+
return path9.resolve(process.cwd());
|
|
2616
2804
|
}
|
|
2617
2805
|
const normalizedRepoDir = repoDir.trim();
|
|
2618
2806
|
if (!normalizedRepoDir) {
|
|
2619
2807
|
throw new Error("--repo-dir must be a non-empty path");
|
|
2620
2808
|
}
|
|
2621
|
-
return
|
|
2809
|
+
return path9.resolve(normalizedRepoDir);
|
|
2622
2810
|
}
|
|
2623
2811
|
function getNodeErrorCode2(error) {
|
|
2624
2812
|
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
@@ -2990,21 +3178,21 @@ async function collectGitOutcomes(options, deps = {}) {
|
|
|
2990
3178
|
|
|
2991
3179
|
// src/efficiency/repo-attribution.ts
|
|
2992
3180
|
import { access as access2, constants as constants2, realpath } from "fs/promises";
|
|
2993
|
-
import
|
|
3181
|
+
import path10 from "path";
|
|
2994
3182
|
async function hasGitMarker(directoryPath) {
|
|
2995
3183
|
try {
|
|
2996
|
-
await access2(
|
|
3184
|
+
await access2(path10.join(directoryPath, ".git"), constants2.F_OK);
|
|
2997
3185
|
return true;
|
|
2998
3186
|
} catch {
|
|
2999
3187
|
return false;
|
|
3000
3188
|
}
|
|
3001
3189
|
}
|
|
3002
3190
|
function normalizeComparablePath(value) {
|
|
3003
|
-
const normalizedPath =
|
|
3191
|
+
const normalizedPath = path10.normalize(path10.resolve(value));
|
|
3004
3192
|
return process.platform === "win32" ? normalizedPath.toLowerCase() : normalizedPath;
|
|
3005
3193
|
}
|
|
3006
3194
|
async function resolveComparablePath(value) {
|
|
3007
|
-
const resolvedPath =
|
|
3195
|
+
const resolvedPath = path10.resolve(value);
|
|
3008
3196
|
try {
|
|
3009
3197
|
return normalizeComparablePath(await realpath(resolvedPath));
|
|
3010
3198
|
} catch {
|
|
@@ -3016,12 +3204,12 @@ async function resolveRepoRootFromPathHint(pathHint) {
|
|
|
3016
3204
|
if (!trimmedPath) {
|
|
3017
3205
|
return void 0;
|
|
3018
3206
|
}
|
|
3019
|
-
let currentPath =
|
|
3207
|
+
let currentPath = path10.resolve(trimmedPath);
|
|
3020
3208
|
for (; ; ) {
|
|
3021
3209
|
if (await hasGitMarker(currentPath)) {
|
|
3022
3210
|
return currentPath;
|
|
3023
3211
|
}
|
|
3024
|
-
const parentPath =
|
|
3212
|
+
const parentPath = path10.dirname(currentPath);
|
|
3025
3213
|
if (parentPath === currentPath) {
|
|
3026
3214
|
return void 0;
|
|
3027
3215
|
}
|
|
@@ -3275,6 +3463,12 @@ function resolveExplicitSourceIds(options, sourceFilter) {
|
|
|
3275
3463
|
if (options.codexDir) {
|
|
3276
3464
|
explicitSourceIds.add("codex");
|
|
3277
3465
|
}
|
|
3466
|
+
if (options.geminiDir) {
|
|
3467
|
+
explicitSourceIds.add("gemini");
|
|
3468
|
+
}
|
|
3469
|
+
if (options.droidDir) {
|
|
3470
|
+
explicitSourceIds.add("droid");
|
|
3471
|
+
}
|
|
3278
3472
|
if (options.opencodeDb) {
|
|
3279
3473
|
explicitSourceIds.add("opencode");
|
|
3280
3474
|
}
|
|
@@ -3342,8 +3536,8 @@ function normalizeSkippedRowReasons(value) {
|
|
|
3342
3536
|
}
|
|
3343
3537
|
|
|
3344
3538
|
// src/cli/parse-file-cache.ts
|
|
3345
|
-
import { mkdir as mkdir2, readFile as
|
|
3346
|
-
import
|
|
3539
|
+
import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
3540
|
+
import path11 from "path";
|
|
3347
3541
|
var PARSE_FILE_CACHE_VERSION = 2;
|
|
3348
3542
|
var CACHE_KEY_SEPARATOR = "\0";
|
|
3349
3543
|
function createCacheKey(source, filePath) {
|
|
@@ -3482,7 +3676,7 @@ function normalizeCacheEntry(value) {
|
|
|
3482
3676
|
};
|
|
3483
3677
|
}
|
|
3484
3678
|
function getDefaultParseFileCachePath() {
|
|
3485
|
-
return
|
|
3679
|
+
return path11.join(getUserCacheRootDir(), "llm-usage-metrics", "parse-file-cache.json");
|
|
3486
3680
|
}
|
|
3487
3681
|
var ParseFileCache = class _ParseFileCache {
|
|
3488
3682
|
constructor(cacheFilePath, limits, now) {
|
|
@@ -3568,7 +3762,7 @@ var ParseFileCache = class _ParseFileCache {
|
|
|
3568
3762
|
keptEntries.length = bestCount;
|
|
3569
3763
|
payloadText = bestPayloadText;
|
|
3570
3764
|
}
|
|
3571
|
-
await mkdir2(
|
|
3765
|
+
await mkdir2(path11.dirname(this.cacheFilePath), { recursive: true });
|
|
3572
3766
|
await writeFile2(this.cacheFilePath, payloadText, "utf8");
|
|
3573
3767
|
this.dirty = false;
|
|
3574
3768
|
}
|
|
@@ -3591,7 +3785,7 @@ var ParseFileCache = class _ParseFileCache {
|
|
|
3591
3785
|
async loadFromDisk() {
|
|
3592
3786
|
let content;
|
|
3593
3787
|
try {
|
|
3594
|
-
content = await
|
|
3788
|
+
content = await readFile4(this.cacheFilePath, "utf8");
|
|
3595
3789
|
} catch {
|
|
3596
3790
|
return;
|
|
3597
3791
|
}
|
|
@@ -3862,8 +4056,8 @@ function applyPricingToEvents(events, pricingSource) {
|
|
|
3862
4056
|
}
|
|
3863
4057
|
|
|
3864
4058
|
// src/pricing/litellm-pricing-fetcher.ts
|
|
3865
|
-
import { mkdir as mkdir3, readFile as
|
|
3866
|
-
import
|
|
4059
|
+
import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
4060
|
+
import path12 from "path";
|
|
3867
4061
|
|
|
3868
4062
|
// src/pricing/litellm-model-map.json
|
|
3869
4063
|
var litellm_model_map_default = {
|
|
@@ -4047,7 +4241,7 @@ function normalizeLitellmPricingPayload(payload) {
|
|
|
4047
4241
|
return normalizedPricing;
|
|
4048
4242
|
}
|
|
4049
4243
|
function getDefaultLiteLLMPricingCachePath() {
|
|
4050
|
-
return
|
|
4244
|
+
return path12.join(getUserCacheRootDir(), "llm-usage-metrics", "litellm-pricing-cache.json");
|
|
4051
4245
|
}
|
|
4052
4246
|
function stripProviderPrefix(model) {
|
|
4053
4247
|
const slashIndex = model.lastIndexOf("/");
|
|
@@ -4406,7 +4600,7 @@ var LiteLLMPricingFetcher = class {
|
|
|
4406
4600
|
async readCachePayload() {
|
|
4407
4601
|
let content;
|
|
4408
4602
|
try {
|
|
4409
|
-
content = await
|
|
4603
|
+
content = await readFile5(this.cacheFilePath, "utf8");
|
|
4410
4604
|
} catch {
|
|
4411
4605
|
return void 0;
|
|
4412
4606
|
}
|
|
@@ -4441,7 +4635,7 @@ var LiteLLMPricingFetcher = class {
|
|
|
4441
4635
|
};
|
|
4442
4636
|
}
|
|
4443
4637
|
async writeCache() {
|
|
4444
|
-
const directoryPath =
|
|
4638
|
+
const directoryPath = path12.dirname(this.cacheFilePath);
|
|
4445
4639
|
await mkdir3(directoryPath, { recursive: true });
|
|
4446
4640
|
const payload = {
|
|
4447
4641
|
fetchedAt: this.now(),
|
|
@@ -4615,6 +4809,12 @@ function resolveScopeNote(options) {
|
|
|
4615
4809
|
if (hasActiveTextOption(options.codexDir)) {
|
|
4616
4810
|
activeFilters.push("--codex-dir");
|
|
4617
4811
|
}
|
|
4812
|
+
if (hasActiveTextOption(options.geminiDir)) {
|
|
4813
|
+
activeFilters.push("--gemini-dir");
|
|
4814
|
+
}
|
|
4815
|
+
if (hasActiveTextOption(options.droidDir)) {
|
|
4816
|
+
activeFilters.push("--droid-dir");
|
|
4817
|
+
}
|
|
4618
4818
|
if (hasActiveTextOption(options.opencodeDb)) {
|
|
4619
4819
|
activeFilters.push("--opencode-db");
|
|
4620
4820
|
}
|
|
@@ -5847,7 +6047,7 @@ function addSharedOptions(command, options = {}) {
|
|
|
5847
6047
|
const allowedSourcesLabel = getAllowedSourcesLabel(supportedSourceIds);
|
|
5848
6048
|
const supportedSourcesSummary = `(${supportedSourceIds.length}): ${allowedSourcesLabel}`;
|
|
5849
6049
|
const includePerModelColumns = options.includePerModelColumns ?? true;
|
|
5850
|
-
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("--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(
|
|
5851
6051
|
"--source-dir <source-id=path>",
|
|
5852
6052
|
"Override source directory for directory-backed sources (repeatable)",
|
|
5853
6053
|
collectRepeatedOption,
|
|
@@ -5921,10 +6121,10 @@ function rootDescription() {
|
|
|
5921
6121
|
" $ llm-usage monthly --since 2026-01-01 --until 2026-01-31 --source pi,codex --json",
|
|
5922
6122
|
" $ llm-usage monthly --source opencode --opencode-db /path/to/opencode.db --json",
|
|
5923
6123
|
" $ llm-usage monthly --model claude --per-model-columns",
|
|
5924
|
-
" $ llm-usage daily --source-dir pi=/tmp/pi-sessions --source-dir gemini=/tmp/.gemini",
|
|
5925
|
-
" $ llm-usage daily --pi-dir /tmp/pi-sessions --gemini-dir /tmp/.gemini",
|
|
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",
|
|
5926
6126
|
" $ llm-usage efficiency weekly --repo-dir /path/to/repo --json",
|
|
5927
|
-
" $ npx --yes llm-usage-metrics daily"
|
|
6127
|
+
" $ npx --yes llm-usage-metrics@latest daily"
|
|
5928
6128
|
].join("\n");
|
|
5929
6129
|
}
|
|
5930
6130
|
function createCli(options = {}) {
|