llm-usage-metrics 0.3.4 → 0.3.6
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 +65 -31
- package/dist/index.js +457 -108
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1094,11 +1094,199 @@ 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
|
+
if (billableTokens === 0) {
|
|
1221
|
+
skippedRows++;
|
|
1222
|
+
incrementSkippedReason(skippedRowReasons, "no_token_usage");
|
|
1223
|
+
return toParseDiagnostics(events, skippedRows, skippedRowReasons);
|
|
1224
|
+
}
|
|
1225
|
+
const provider = asTrimmedText(settings.providerLock);
|
|
1226
|
+
const model = asTrimmedText(settings.model);
|
|
1227
|
+
const totalTokens = billableTokens;
|
|
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
|
+
if (fallbackMessageTimestamp) {
|
|
1247
|
+
break;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
} catch {
|
|
1252
|
+
}
|
|
1253
|
+
const timestamp = primaryTimestamp ?? fallbackMessageTimestamp;
|
|
1254
|
+
if (!timestamp) {
|
|
1255
|
+
skippedRows++;
|
|
1256
|
+
incrementSkippedReason(skippedRowReasons, "invalid_timestamp");
|
|
1257
|
+
return toParseDiagnostics(events, skippedRows, skippedRowReasons);
|
|
1258
|
+
}
|
|
1259
|
+
try {
|
|
1260
|
+
events.push(
|
|
1261
|
+
createUsageEvent({
|
|
1262
|
+
source: this.id,
|
|
1263
|
+
sessionId: getSettingsSessionId(filePath),
|
|
1264
|
+
timestamp,
|
|
1265
|
+
repoRoot,
|
|
1266
|
+
provider,
|
|
1267
|
+
model,
|
|
1268
|
+
inputTokens,
|
|
1269
|
+
outputTokens,
|
|
1270
|
+
reasoningTokens,
|
|
1271
|
+
cacheReadTokens,
|
|
1272
|
+
cacheWriteTokens,
|
|
1273
|
+
totalTokens,
|
|
1274
|
+
costMode: "estimated"
|
|
1275
|
+
})
|
|
1276
|
+
);
|
|
1277
|
+
} catch {
|
|
1278
|
+
skippedRows++;
|
|
1279
|
+
incrementSkippedReason(skippedRowReasons, "event_creation_failed");
|
|
1280
|
+
}
|
|
1281
|
+
return toParseDiagnostics(events, skippedRows, skippedRowReasons);
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
// src/sources/gemini/gemini-source-adapter.ts
|
|
1286
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1287
|
+
import os4 from "os";
|
|
1288
|
+
import path6 from "path";
|
|
1289
|
+
var defaultGeminiDir = path6.join(os4.homedir(), ".gemini");
|
|
1102
1290
|
function parseProjectsJson(data) {
|
|
1103
1291
|
const mapping = /* @__PURE__ */ new Map();
|
|
1104
1292
|
const record = asRecord(data);
|
|
@@ -1119,9 +1307,9 @@ function parseProjectsJson(data) {
|
|
|
1119
1307
|
return mapping;
|
|
1120
1308
|
}
|
|
1121
1309
|
async function loadProjectsJson(geminiDir) {
|
|
1122
|
-
const projectsPath =
|
|
1310
|
+
const projectsPath = path6.join(geminiDir, "projects.json");
|
|
1123
1311
|
try {
|
|
1124
|
-
const content = await
|
|
1312
|
+
const content = await readFile3(projectsPath, "utf8");
|
|
1125
1313
|
const parsed = JSON.parse(content);
|
|
1126
1314
|
return parseProjectsJson(parsed);
|
|
1127
1315
|
} catch {
|
|
@@ -1129,11 +1317,11 @@ async function loadProjectsJson(geminiDir) {
|
|
|
1129
1317
|
}
|
|
1130
1318
|
}
|
|
1131
1319
|
async function discoverSessionFiles(geminiDir) {
|
|
1132
|
-
const tmpDir =
|
|
1320
|
+
const tmpDir = path6.join(geminiDir, "tmp");
|
|
1133
1321
|
const allSessionFiles = [];
|
|
1134
1322
|
const discoveredFiles = await discoverFiles(tmpDir, { extension: ".json" });
|
|
1135
1323
|
for (const filePath of discoveredFiles) {
|
|
1136
|
-
const parentDir =
|
|
1324
|
+
const parentDir = path6.basename(path6.dirname(filePath));
|
|
1137
1325
|
if (parentDir.toLowerCase() === "chats") {
|
|
1138
1326
|
allSessionFiles.push(filePath);
|
|
1139
1327
|
}
|
|
@@ -1148,9 +1336,9 @@ function resolveRepoRoot(filePath, sessionData, projectMapping) {
|
|
|
1148
1336
|
return mappedRoot;
|
|
1149
1337
|
}
|
|
1150
1338
|
}
|
|
1151
|
-
const chatsDir =
|
|
1152
|
-
const projectDir =
|
|
1153
|
-
const projectIdentifier =
|
|
1339
|
+
const chatsDir = path6.dirname(filePath);
|
|
1340
|
+
const projectDir = path6.dirname(chatsDir);
|
|
1341
|
+
const projectIdentifier = path6.basename(projectDir);
|
|
1154
1342
|
return projectMapping.get(projectIdentifier);
|
|
1155
1343
|
}
|
|
1156
1344
|
function toFiniteNumber(value) {
|
|
@@ -1197,20 +1385,6 @@ function extractTokenUsage(tokens) {
|
|
|
1197
1385
|
totalTokens
|
|
1198
1386
|
};
|
|
1199
1387
|
}
|
|
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
1388
|
function normalizeTimestamp2(candidate) {
|
|
1215
1389
|
if (typeof candidate !== "string" || isBlankText(candidate)) {
|
|
1216
1390
|
return void 0;
|
|
@@ -1274,7 +1448,7 @@ var GeminiSourceAdapter = class {
|
|
|
1274
1448
|
const skippedRowReasons = /* @__PURE__ */ new Map();
|
|
1275
1449
|
let sessionData;
|
|
1276
1450
|
try {
|
|
1277
|
-
const content = await
|
|
1451
|
+
const content = await readFile3(filePath, "utf8");
|
|
1278
1452
|
sessionData = JSON.parse(content);
|
|
1279
1453
|
} catch {
|
|
1280
1454
|
skippedRows++;
|
|
@@ -1287,7 +1461,7 @@ var GeminiSourceAdapter = class {
|
|
|
1287
1461
|
incrementSkippedReason(skippedRowReasons, "invalid_session_data");
|
|
1288
1462
|
return toParseDiagnostics(events, skippedRows, skippedRowReasons);
|
|
1289
1463
|
}
|
|
1290
|
-
const sessionId = asTrimmedText(sessionDataRecord.sessionId) ??
|
|
1464
|
+
const sessionId = asTrimmedText(sessionDataRecord.sessionId) ?? path6.basename(filePath, ".json");
|
|
1291
1465
|
const projectMapping = await this.getProjectMappingForParse();
|
|
1292
1466
|
const repoRoot = resolveRepoRoot(filePath, sessionDataRecord, projectMapping);
|
|
1293
1467
|
if (!Array.isArray(sessionDataRecord.messages)) {
|
|
@@ -1344,44 +1518,44 @@ var GeminiSourceAdapter = class {
|
|
|
1344
1518
|
};
|
|
1345
1519
|
|
|
1346
1520
|
// src/sources/opencode/opencode-db-path-resolver.ts
|
|
1347
|
-
import
|
|
1348
|
-
import
|
|
1521
|
+
import os5 from "os";
|
|
1522
|
+
import path7 from "path";
|
|
1349
1523
|
function deduplicate(paths) {
|
|
1350
1524
|
return [...new Set(paths)];
|
|
1351
1525
|
}
|
|
1352
1526
|
function getLinuxLikeCandidates(homeDir, env) {
|
|
1353
|
-
const xdgDataHome = env.XDG_DATA_HOME ??
|
|
1527
|
+
const xdgDataHome = env.XDG_DATA_HOME ?? path7.join(homeDir, ".local", "share");
|
|
1354
1528
|
return [
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1529
|
+
path7.join(xdgDataHome, "opencode", "opencode.db"),
|
|
1530
|
+
path7.join(xdgDataHome, "opencode", "db.sqlite"),
|
|
1531
|
+
path7.join(homeDir, ".opencode", "opencode.db"),
|
|
1532
|
+
path7.join(homeDir, ".opencode", "db.sqlite")
|
|
1359
1533
|
];
|
|
1360
1534
|
}
|
|
1361
1535
|
function getMacOsCandidates(homeDir) {
|
|
1362
|
-
const appSupportDir =
|
|
1536
|
+
const appSupportDir = path7.join(homeDir, "Library", "Application Support");
|
|
1363
1537
|
return [
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1538
|
+
path7.join(appSupportDir, "opencode", "opencode.db"),
|
|
1539
|
+
path7.join(appSupportDir, "opencode", "db.sqlite"),
|
|
1540
|
+
path7.join(homeDir, ".opencode", "opencode.db"),
|
|
1541
|
+
path7.join(homeDir, ".opencode", "db.sqlite")
|
|
1368
1542
|
];
|
|
1369
1543
|
}
|
|
1370
1544
|
function getWindowsCandidates(homeDir, env) {
|
|
1371
|
-
const roamingBase = env.APPDATA ?? env.LOCALAPPDATA ?? (env.USERPROFILE ?
|
|
1545
|
+
const roamingBase = env.APPDATA ?? env.LOCALAPPDATA ?? (env.USERPROFILE ? path7.join(env.USERPROFILE, "AppData", "Roaming") : void 0);
|
|
1372
1546
|
const roamingCandidates = roamingBase ? [
|
|
1373
|
-
|
|
1374
|
-
|
|
1547
|
+
path7.join(roamingBase, "opencode", "opencode.db"),
|
|
1548
|
+
path7.join(roamingBase, "opencode", "db.sqlite")
|
|
1375
1549
|
] : [];
|
|
1376
1550
|
return [
|
|
1377
1551
|
...roamingCandidates,
|
|
1378
|
-
|
|
1379
|
-
|
|
1552
|
+
path7.join(homeDir, ".opencode", "opencode.db"),
|
|
1553
|
+
path7.join(homeDir, ".opencode", "db.sqlite")
|
|
1380
1554
|
];
|
|
1381
1555
|
}
|
|
1382
1556
|
function getDefaultOpenCodeDbPathCandidates(options = {}) {
|
|
1383
1557
|
const platform = options.platform ?? process.platform;
|
|
1384
|
-
const homeDir = options.homeDir ??
|
|
1558
|
+
const homeDir = options.homeDir ?? os5.homedir();
|
|
1385
1559
|
const env = options.env ?? process.env;
|
|
1386
1560
|
switch (platform) {
|
|
1387
1561
|
case "win32":
|
|
@@ -1447,10 +1621,10 @@ async function loadNodeSqliteModule() {
|
|
|
1447
1621
|
}
|
|
1448
1622
|
|
|
1449
1623
|
// src/sources/opencode/opencode-row-parser.ts
|
|
1450
|
-
var
|
|
1451
|
-
function
|
|
1624
|
+
var UNIX_SECONDS_ABS_CUTOFF2 = 1e10;
|
|
1625
|
+
function normalizeTimestampCandidate2(candidate) {
|
|
1452
1626
|
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
1453
|
-
const timestampMs = Math.abs(candidate) <=
|
|
1627
|
+
const timestampMs = Math.abs(candidate) <= UNIX_SECONDS_ABS_CUTOFF2 ? candidate * 1e3 : candidate;
|
|
1454
1628
|
const date = new Date(timestampMs);
|
|
1455
1629
|
if (Number.isNaN(date.getTime())) {
|
|
1456
1630
|
return void 0;
|
|
@@ -1464,7 +1638,7 @@ function normalizeTimestampCandidate(candidate) {
|
|
|
1464
1638
|
}
|
|
1465
1639
|
const numericTimestamp = Number(trimmed);
|
|
1466
1640
|
if (Number.isFinite(numericTimestamp)) {
|
|
1467
|
-
return
|
|
1641
|
+
return normalizeTimestampCandidate2(numericTimestamp);
|
|
1468
1642
|
}
|
|
1469
1643
|
const date = new Date(trimmed);
|
|
1470
1644
|
if (Number.isNaN(date.getTime())) {
|
|
@@ -1482,7 +1656,7 @@ function resolveTimestamp(rowTimestamp, messagePayload) {
|
|
|
1482
1656
|
messagePayload.time_created
|
|
1483
1657
|
];
|
|
1484
1658
|
for (const candidate of timestampCandidates) {
|
|
1485
|
-
const resolved =
|
|
1659
|
+
const resolved = normalizeTimestampCandidate2(candidate);
|
|
1486
1660
|
if (resolved) {
|
|
1487
1661
|
return resolved;
|
|
1488
1662
|
}
|
|
@@ -1888,9 +2062,9 @@ var OpenCodeSourceAdapter = class {
|
|
|
1888
2062
|
};
|
|
1889
2063
|
|
|
1890
2064
|
// src/sources/pi/pi-source-adapter.ts
|
|
1891
|
-
import
|
|
1892
|
-
import
|
|
1893
|
-
var
|
|
2065
|
+
import os6 from "os";
|
|
2066
|
+
import path8 from "path";
|
|
2067
|
+
var defaultSessionsDir3 = path8.join(os6.homedir(), ".pi", "agent", "sessions");
|
|
1894
2068
|
var PI_MESSAGE_LINE_PATTERN = /"type"\s*:\s*"message"/u;
|
|
1895
2069
|
var PI_SESSION_LINE_PATTERN = /"type"\s*:\s*"session"/u;
|
|
1896
2070
|
var PI_MODEL_CHANGE_LINE_PATTERN = /"type"\s*:\s*"model_change"/u;
|
|
@@ -1898,11 +2072,11 @@ function shouldParsePiJsonlLine(lineText) {
|
|
|
1898
2072
|
return PI_MESSAGE_LINE_PATTERN.test(lineText) || PI_SESSION_LINE_PATTERN.test(lineText) || PI_MODEL_CHANGE_LINE_PATTERN.test(lineText);
|
|
1899
2073
|
}
|
|
1900
2074
|
var allowAllProviders = () => true;
|
|
1901
|
-
var
|
|
1902
|
-
function
|
|
2075
|
+
var UNIX_SECONDS_ABS_CUTOFF3 = 1e10;
|
|
2076
|
+
function normalizeTimestampCandidate3(candidate) {
|
|
1903
2077
|
let date;
|
|
1904
2078
|
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
1905
|
-
const timestampMs = Math.abs(candidate) <=
|
|
2079
|
+
const timestampMs = Math.abs(candidate) <= UNIX_SECONDS_ABS_CUTOFF3 ? candidate * 1e3 : candidate;
|
|
1906
2080
|
date = new Date(timestampMs);
|
|
1907
2081
|
} else {
|
|
1908
2082
|
const normalizedText = asTrimmedText(candidate);
|
|
@@ -1919,7 +2093,7 @@ function normalizeTimestampCandidate2(candidate) {
|
|
|
1919
2093
|
function resolveTimestamp2(line, message, state) {
|
|
1920
2094
|
const candidates = [line.timestamp, message?.timestamp, state.sessionTimestamp];
|
|
1921
2095
|
for (const candidate of candidates) {
|
|
1922
|
-
const normalizedTimestamp =
|
|
2096
|
+
const normalizedTimestamp = normalizeTimestampCandidate3(candidate);
|
|
1923
2097
|
if (normalizedTimestamp) {
|
|
1924
2098
|
return normalizedTimestamp;
|
|
1925
2099
|
}
|
|
@@ -1983,7 +2157,7 @@ function extractUsage(line, message) {
|
|
|
1983
2157
|
return extractUsageFromRecord(messageUsage);
|
|
1984
2158
|
}
|
|
1985
2159
|
function getFallbackSessionId2(filePath) {
|
|
1986
|
-
return
|
|
2160
|
+
return path8.basename(filePath, ".jsonl");
|
|
1987
2161
|
}
|
|
1988
2162
|
function resolveRepoRootFromRecord(record) {
|
|
1989
2163
|
if (!record) {
|
|
@@ -1998,7 +2172,7 @@ var PiSourceAdapter = class {
|
|
|
1998
2172
|
providerFilter;
|
|
1999
2173
|
requireSessionsDir;
|
|
2000
2174
|
constructor(options = {}) {
|
|
2001
|
-
this.sessionsDir = options.sessionsDir ??
|
|
2175
|
+
this.sessionsDir = options.sessionsDir ?? defaultSessionsDir3;
|
|
2002
2176
|
this.providerFilter = options.providerFilter ?? allowAllProviders;
|
|
2003
2177
|
this.requireSessionsDir = options.requireSessionsDir ?? false;
|
|
2004
2178
|
}
|
|
@@ -2136,6 +2310,21 @@ var sourceRegistrations = [
|
|
|
2136
2310
|
});
|
|
2137
2311
|
}
|
|
2138
2312
|
},
|
|
2313
|
+
{
|
|
2314
|
+
id: "droid",
|
|
2315
|
+
sourceDirOverride: { kind: "directory" },
|
|
2316
|
+
create: (options, sourceDirectoryOverrides) => {
|
|
2317
|
+
const directoryConfig = resolveDirectoryConfig(
|
|
2318
|
+
"droid",
|
|
2319
|
+
options.droidDir,
|
|
2320
|
+
sourceDirectoryOverrides
|
|
2321
|
+
);
|
|
2322
|
+
return new DroidSourceAdapter({
|
|
2323
|
+
sessionsDir: directoryConfig.path,
|
|
2324
|
+
requireSessionsDir: directoryConfig.requireExistingPath
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
},
|
|
2139
2328
|
{
|
|
2140
2329
|
id: "opencode",
|
|
2141
2330
|
sourceDirOverride: { kind: "unsupported", flag: "--opencode-db" },
|
|
@@ -2217,6 +2406,7 @@ function createDefaultAdapters(options) {
|
|
|
2217
2406
|
validateDirectoryOverride("--pi-dir", options.piDir);
|
|
2218
2407
|
validateDirectoryOverride("--codex-dir", options.codexDir);
|
|
2219
2408
|
validateDirectoryOverride("--gemini-dir", options.geminiDir);
|
|
2409
|
+
validateDirectoryOverride("--droid-dir", options.droidDir);
|
|
2220
2410
|
const sourceDirectoryOverrides = parseSourceDirectoryOverrides(options.sourceDir);
|
|
2221
2411
|
validateSourceDirectoryOverrideIds(sourceDirectoryOverrides);
|
|
2222
2412
|
return sourceRegistrations.map((source) => source.create(options, sourceDirectoryOverrides));
|
|
@@ -2592,7 +2782,7 @@ function aggregateEfficiency(options) {
|
|
|
2592
2782
|
// src/efficiency/git-outcome-collector.ts
|
|
2593
2783
|
import { spawn as spawn2 } from "child_process";
|
|
2594
2784
|
import { createInterface as createInterface3 } from "readline";
|
|
2595
|
-
import
|
|
2785
|
+
import path9 from "path";
|
|
2596
2786
|
import { stat as stat2 } from "fs/promises";
|
|
2597
2787
|
var GIT_COMMIT_MARKER = "";
|
|
2598
2788
|
var SHORTSTAT_PATTERN = /(\d+)\s+files?\s+changed(?:,\s+(\d+)\s+insertions?\(\+\))?(?:,\s+(\d+)\s+deletions?\(-\))?/u;
|
|
@@ -2612,13 +2802,13 @@ function resolveGitCommandFailureReason(result) {
|
|
|
2612
2802
|
}
|
|
2613
2803
|
function resolveRepoDir(repoDir) {
|
|
2614
2804
|
if (repoDir === void 0) {
|
|
2615
|
-
return
|
|
2805
|
+
return path9.resolve(process.cwd());
|
|
2616
2806
|
}
|
|
2617
2807
|
const normalizedRepoDir = repoDir.trim();
|
|
2618
2808
|
if (!normalizedRepoDir) {
|
|
2619
2809
|
throw new Error("--repo-dir must be a non-empty path");
|
|
2620
2810
|
}
|
|
2621
|
-
return
|
|
2811
|
+
return path9.resolve(normalizedRepoDir);
|
|
2622
2812
|
}
|
|
2623
2813
|
function getNodeErrorCode2(error) {
|
|
2624
2814
|
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
@@ -2990,21 +3180,21 @@ async function collectGitOutcomes(options, deps = {}) {
|
|
|
2990
3180
|
|
|
2991
3181
|
// src/efficiency/repo-attribution.ts
|
|
2992
3182
|
import { access as access2, constants as constants2, realpath } from "fs/promises";
|
|
2993
|
-
import
|
|
3183
|
+
import path10 from "path";
|
|
2994
3184
|
async function hasGitMarker(directoryPath) {
|
|
2995
3185
|
try {
|
|
2996
|
-
await access2(
|
|
3186
|
+
await access2(path10.join(directoryPath, ".git"), constants2.F_OK);
|
|
2997
3187
|
return true;
|
|
2998
3188
|
} catch {
|
|
2999
3189
|
return false;
|
|
3000
3190
|
}
|
|
3001
3191
|
}
|
|
3002
3192
|
function normalizeComparablePath(value) {
|
|
3003
|
-
const normalizedPath =
|
|
3193
|
+
const normalizedPath = path10.normalize(path10.resolve(value));
|
|
3004
3194
|
return process.platform === "win32" ? normalizedPath.toLowerCase() : normalizedPath;
|
|
3005
3195
|
}
|
|
3006
3196
|
async function resolveComparablePath(value) {
|
|
3007
|
-
const resolvedPath =
|
|
3197
|
+
const resolvedPath = path10.resolve(value);
|
|
3008
3198
|
try {
|
|
3009
3199
|
return normalizeComparablePath(await realpath(resolvedPath));
|
|
3010
3200
|
} catch {
|
|
@@ -3016,12 +3206,12 @@ async function resolveRepoRootFromPathHint(pathHint) {
|
|
|
3016
3206
|
if (!trimmedPath) {
|
|
3017
3207
|
return void 0;
|
|
3018
3208
|
}
|
|
3019
|
-
let currentPath =
|
|
3209
|
+
let currentPath = path10.resolve(trimmedPath);
|
|
3020
3210
|
for (; ; ) {
|
|
3021
3211
|
if (await hasGitMarker(currentPath)) {
|
|
3022
3212
|
return currentPath;
|
|
3023
3213
|
}
|
|
3024
|
-
const parentPath =
|
|
3214
|
+
const parentPath = path10.dirname(currentPath);
|
|
3025
3215
|
if (parentPath === currentPath) {
|
|
3026
3216
|
return void 0;
|
|
3027
3217
|
}
|
|
@@ -3275,6 +3465,12 @@ function resolveExplicitSourceIds(options, sourceFilter) {
|
|
|
3275
3465
|
if (options.codexDir) {
|
|
3276
3466
|
explicitSourceIds.add("codex");
|
|
3277
3467
|
}
|
|
3468
|
+
if (options.geminiDir) {
|
|
3469
|
+
explicitSourceIds.add("gemini");
|
|
3470
|
+
}
|
|
3471
|
+
if (options.droidDir) {
|
|
3472
|
+
explicitSourceIds.add("droid");
|
|
3473
|
+
}
|
|
3278
3474
|
if (options.opencodeDb) {
|
|
3279
3475
|
explicitSourceIds.add("opencode");
|
|
3280
3476
|
}
|
|
@@ -3314,7 +3510,7 @@ function selectAdaptersForParsing(adapters, sourceFilter) {
|
|
|
3314
3510
|
}
|
|
3315
3511
|
|
|
3316
3512
|
// src/cli/build-usage-data-parsing.ts
|
|
3317
|
-
import { stat as
|
|
3513
|
+
import { stat as stat4 } from "fs/promises";
|
|
3318
3514
|
|
|
3319
3515
|
// src/cli/normalize-skipped-row-reasons.ts
|
|
3320
3516
|
function toPositiveInteger(value) {
|
|
@@ -3342,8 +3538,8 @@ function normalizeSkippedRowReasons(value) {
|
|
|
3342
3538
|
}
|
|
3343
3539
|
|
|
3344
3540
|
// src/cli/parse-file-cache.ts
|
|
3345
|
-
import { mkdir as mkdir2, readFile as
|
|
3346
|
-
import
|
|
3541
|
+
import { mkdir as mkdir2, readFile as readFile4, rename, rm, stat as stat3, writeFile as writeFile2 } from "fs/promises";
|
|
3542
|
+
import path11 from "path";
|
|
3347
3543
|
var PARSE_FILE_CACHE_VERSION = 2;
|
|
3348
3544
|
var CACHE_KEY_SEPARATOR = "\0";
|
|
3349
3545
|
function createCacheKey(source, filePath) {
|
|
@@ -3433,7 +3629,7 @@ function cloneUsageEvents(events) {
|
|
|
3433
3629
|
return events.map((event) => cloneUsageEvent(event));
|
|
3434
3630
|
}
|
|
3435
3631
|
function cloneSkippedRowReasons(skippedRowReasons) {
|
|
3436
|
-
return (skippedRowReasons ?? []).map((
|
|
3632
|
+
return (skippedRowReasons ?? []).map((stat5) => ({ reason: stat5.reason, count: stat5.count }));
|
|
3437
3633
|
}
|
|
3438
3634
|
function normalizeCachedEvents(value) {
|
|
3439
3635
|
if (!Array.isArray(value)) {
|
|
@@ -3482,7 +3678,22 @@ function normalizeCacheEntry(value) {
|
|
|
3482
3678
|
};
|
|
3483
3679
|
}
|
|
3484
3680
|
function getDefaultParseFileCachePath() {
|
|
3485
|
-
return
|
|
3681
|
+
return path11.join(getUserCacheRootDir(), "llm-usage-metrics", "parse-file-cache.json");
|
|
3682
|
+
}
|
|
3683
|
+
function normalizeCacheShardSource(source) {
|
|
3684
|
+
const normalizedSource = normalizeCacheSource(source);
|
|
3685
|
+
if (!normalizedSource) {
|
|
3686
|
+
return "unknown";
|
|
3687
|
+
}
|
|
3688
|
+
return normalizedSource.replace(/[^a-z0-9._-]/gu, "_");
|
|
3689
|
+
}
|
|
3690
|
+
function getSourceShardedParseFileCachePath(cacheFilePath, source) {
|
|
3691
|
+
const parsedPath = path11.parse(cacheFilePath);
|
|
3692
|
+
const sourceShard = normalizeCacheShardSource(source);
|
|
3693
|
+
if (parsedPath.ext.length > 0) {
|
|
3694
|
+
return path11.join(parsedPath.dir, `${parsedPath.name}.${sourceShard}${parsedPath.ext}`);
|
|
3695
|
+
}
|
|
3696
|
+
return path11.join(parsedPath.dir, `${parsedPath.base}.${sourceShard}`);
|
|
3486
3697
|
}
|
|
3487
3698
|
var ParseFileCache = class _ParseFileCache {
|
|
3488
3699
|
constructor(cacheFilePath, limits, now) {
|
|
@@ -3568,9 +3779,16 @@ var ParseFileCache = class _ParseFileCache {
|
|
|
3568
3779
|
keptEntries.length = bestCount;
|
|
3569
3780
|
payloadText = bestPayloadText;
|
|
3570
3781
|
}
|
|
3571
|
-
await mkdir2(
|
|
3572
|
-
|
|
3573
|
-
|
|
3782
|
+
await mkdir2(path11.dirname(this.cacheFilePath), { recursive: true });
|
|
3783
|
+
const temporaryPath = `${this.cacheFilePath}.${process.pid}.${this.now()}.tmp`;
|
|
3784
|
+
try {
|
|
3785
|
+
await writeFile2(temporaryPath, payloadText, "utf8");
|
|
3786
|
+
await rename(temporaryPath, this.cacheFilePath);
|
|
3787
|
+
this.dirty = false;
|
|
3788
|
+
} catch (error) {
|
|
3789
|
+
await rm(temporaryPath, { force: true }).catch(() => void 0);
|
|
3790
|
+
throw error;
|
|
3791
|
+
}
|
|
3574
3792
|
}
|
|
3575
3793
|
toPayload(entries) {
|
|
3576
3794
|
return {
|
|
@@ -3589,9 +3807,20 @@ var ParseFileCache = class _ParseFileCache {
|
|
|
3589
3807
|
};
|
|
3590
3808
|
}
|
|
3591
3809
|
async loadFromDisk() {
|
|
3810
|
+
let cacheFileSizeBytes;
|
|
3811
|
+
try {
|
|
3812
|
+
const cacheStat = await stat3(this.cacheFilePath);
|
|
3813
|
+
cacheFileSizeBytes = cacheStat.size;
|
|
3814
|
+
} catch {
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3817
|
+
if (cacheFileSizeBytes > this.limits.maxBytes) {
|
|
3818
|
+
this.dirty = true;
|
|
3819
|
+
return;
|
|
3820
|
+
}
|
|
3592
3821
|
let content;
|
|
3593
3822
|
try {
|
|
3594
|
-
content = await
|
|
3823
|
+
content = await readFile4(this.cacheFilePath, "utf8");
|
|
3595
3824
|
} catch {
|
|
3596
3825
|
return;
|
|
3597
3826
|
}
|
|
@@ -3614,6 +3843,7 @@ var ParseFileCache = class _ParseFileCache {
|
|
|
3614
3843
|
}
|
|
3615
3844
|
if (Buffer.byteLength(content, "utf8") > this.limits.maxBytes) {
|
|
3616
3845
|
this.dirty = true;
|
|
3846
|
+
return;
|
|
3617
3847
|
}
|
|
3618
3848
|
const entries = Array.isArray(payloadRecord.entries) ? payloadRecord.entries : [];
|
|
3619
3849
|
for (const rawEntry of entries) {
|
|
@@ -3673,7 +3903,7 @@ async function parseAdapterEvents(adapter, maxParallelFileParsing, parseFileCach
|
|
|
3673
3903
|
let parseFileDiagnostics;
|
|
3674
3904
|
if (parseFileCache) {
|
|
3675
3905
|
try {
|
|
3676
|
-
const fileStat = await
|
|
3906
|
+
const fileStat = await stat4(filePath);
|
|
3677
3907
|
fileFingerprint = {
|
|
3678
3908
|
size: fileStat.size,
|
|
3679
3909
|
mtimeMs: fileStat.mtimeMs
|
|
@@ -3714,25 +3944,44 @@ function getErrorReason(error) {
|
|
|
3714
3944
|
return String(error);
|
|
3715
3945
|
}
|
|
3716
3946
|
async function parseSelectedAdapters(adaptersToParse, maxParallelFileParsing, options = {}) {
|
|
3717
|
-
const
|
|
3718
|
-
|
|
3719
|
-
|
|
3947
|
+
const parseCacheBySource = /* @__PURE__ */ new Map();
|
|
3948
|
+
if (options.parseCache?.enabled) {
|
|
3949
|
+
const parseCacheLimits = {
|
|
3720
3950
|
ttlMs: options.parseCache.ttlMs,
|
|
3721
3951
|
maxEntries: options.parseCache.maxEntries,
|
|
3722
3952
|
maxBytes: options.parseCache.maxBytes
|
|
3723
|
-
}
|
|
3724
|
-
|
|
3725
|
-
|
|
3953
|
+
};
|
|
3954
|
+
const cacheFilePath = options.parseCacheFilePath ?? getDefaultParseFileCachePath();
|
|
3955
|
+
await Promise.all(
|
|
3956
|
+
adaptersToParse.map(async (adapter) => {
|
|
3957
|
+
const sourceId = adapter.id.toLowerCase();
|
|
3958
|
+
if (parseCacheBySource.has(sourceId)) {
|
|
3959
|
+
return;
|
|
3960
|
+
}
|
|
3961
|
+
parseCacheBySource.set(
|
|
3962
|
+
sourceId,
|
|
3963
|
+
await ParseFileCache.load({
|
|
3964
|
+
cacheFilePath: getSourceShardedParseFileCachePath(cacheFilePath, sourceId),
|
|
3965
|
+
limits: parseCacheLimits,
|
|
3966
|
+
now: options.now
|
|
3967
|
+
})
|
|
3968
|
+
);
|
|
3969
|
+
})
|
|
3970
|
+
);
|
|
3971
|
+
}
|
|
3726
3972
|
const parseResults = await Promise.allSettled(
|
|
3727
3973
|
adaptersToParse.map(
|
|
3728
|
-
(adapter) => parseAdapterEvents(
|
|
3974
|
+
(adapter) => parseAdapterEvents(
|
|
3975
|
+
adapter,
|
|
3976
|
+
maxParallelFileParsing,
|
|
3977
|
+
parseCacheBySource.get(adapter.id.toLowerCase())
|
|
3978
|
+
)
|
|
3729
3979
|
)
|
|
3730
3980
|
);
|
|
3731
|
-
if (
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
}
|
|
3981
|
+
if (parseCacheBySource.size > 0) {
|
|
3982
|
+
await Promise.allSettled(
|
|
3983
|
+
[...parseCacheBySource.values()].map(async (parseCache) => parseCache.persist())
|
|
3984
|
+
);
|
|
3736
3985
|
}
|
|
3737
3986
|
const sourceFailures = [];
|
|
3738
3987
|
const successfulParseResults = [];
|
|
@@ -3862,8 +4111,8 @@ function applyPricingToEvents(events, pricingSource) {
|
|
|
3862
4111
|
}
|
|
3863
4112
|
|
|
3864
4113
|
// src/pricing/litellm-pricing-fetcher.ts
|
|
3865
|
-
import { mkdir as mkdir3, readFile as
|
|
3866
|
-
import
|
|
4114
|
+
import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
4115
|
+
import path12 from "path";
|
|
3867
4116
|
|
|
3868
4117
|
// src/pricing/litellm-model-map.json
|
|
3869
4118
|
var litellm_model_map_default = {
|
|
@@ -4006,7 +4255,7 @@ function normalizeModelPricing(rawModelPricing) {
|
|
|
4006
4255
|
return void 0;
|
|
4007
4256
|
}
|
|
4008
4257
|
const cacheReadPerToken = toNonNegativeNumber3(rawModelPricing.cache_read_input_token_cost) ?? toNonNegativeNumber3(rawModelPricing.cache_read_input_token_cost_priority);
|
|
4009
|
-
const cacheWritePerToken = toNonNegativeNumber3(rawModelPricing.cache_creation_input_token_cost);
|
|
4258
|
+
const cacheWritePerToken = toNonNegativeNumber3(rawModelPricing.cache_creation_input_token_cost) ?? toNonNegativeNumber3(rawModelPricing.cache_creation_input_token_cost_priority);
|
|
4010
4259
|
const reasoningPerToken = toNonNegativeNumber3(rawModelPricing.output_cost_per_reasoning_token);
|
|
4011
4260
|
const modelPricing = {
|
|
4012
4261
|
inputPer1MUsd: inputPerToken * ONE_MILLION2,
|
|
@@ -4047,7 +4296,7 @@ function normalizeLitellmPricingPayload(payload) {
|
|
|
4047
4296
|
return normalizedPricing;
|
|
4048
4297
|
}
|
|
4049
4298
|
function getDefaultLiteLLMPricingCachePath() {
|
|
4050
|
-
return
|
|
4299
|
+
return path12.join(getUserCacheRootDir(), "llm-usage-metrics", "litellm-pricing-cache.json");
|
|
4051
4300
|
}
|
|
4052
4301
|
function stripProviderPrefix(model) {
|
|
4053
4302
|
const slashIndex = model.lastIndexOf("/");
|
|
@@ -4406,7 +4655,7 @@ var LiteLLMPricingFetcher = class {
|
|
|
4406
4655
|
async readCachePayload() {
|
|
4407
4656
|
let content;
|
|
4408
4657
|
try {
|
|
4409
|
-
content = await
|
|
4658
|
+
content = await readFile5(this.cacheFilePath, "utf8");
|
|
4410
4659
|
} catch {
|
|
4411
4660
|
return void 0;
|
|
4412
4661
|
}
|
|
@@ -4441,7 +4690,7 @@ var LiteLLMPricingFetcher = class {
|
|
|
4441
4690
|
};
|
|
4442
4691
|
}
|
|
4443
4692
|
async writeCache() {
|
|
4444
|
-
const directoryPath =
|
|
4693
|
+
const directoryPath = path12.dirname(this.cacheFilePath);
|
|
4445
4694
|
await mkdir3(directoryPath, { recursive: true });
|
|
4446
4695
|
const payload = {
|
|
4447
4696
|
fetchedAt: this.now(),
|
|
@@ -4615,6 +4864,12 @@ function resolveScopeNote(options) {
|
|
|
4615
4864
|
if (hasActiveTextOption(options.codexDir)) {
|
|
4616
4865
|
activeFilters.push("--codex-dir");
|
|
4617
4866
|
}
|
|
4867
|
+
if (hasActiveTextOption(options.geminiDir)) {
|
|
4868
|
+
activeFilters.push("--gemini-dir");
|
|
4869
|
+
}
|
|
4870
|
+
if (hasActiveTextOption(options.droidDir)) {
|
|
4871
|
+
activeFilters.push("--droid-dir");
|
|
4872
|
+
}
|
|
4618
4873
|
if (hasActiveTextOption(options.opencodeDb)) {
|
|
4619
4874
|
activeFilters.push("--opencode-db");
|
|
4620
4875
|
}
|
|
@@ -5586,6 +5841,8 @@ function renderTerminalTable(rows, options = {}) {
|
|
|
5586
5841
|
}
|
|
5587
5842
|
|
|
5588
5843
|
// src/render/render-efficiency-report.ts
|
|
5844
|
+
var periodColumnIndex = 0;
|
|
5845
|
+
var minimumEfficiencyColumnWidth = 1;
|
|
5589
5846
|
function getReportTitle(granularity) {
|
|
5590
5847
|
switch (granularity) {
|
|
5591
5848
|
case "daily":
|
|
@@ -5630,22 +5887,114 @@ function toTableSortRow(row) {
|
|
|
5630
5887
|
costIncomplete: row.costIncomplete
|
|
5631
5888
|
};
|
|
5632
5889
|
}
|
|
5890
|
+
function measureRenderedTableWidth(columnWidths) {
|
|
5891
|
+
if (columnWidths.length === 0) {
|
|
5892
|
+
return 0;
|
|
5893
|
+
}
|
|
5894
|
+
return columnWidths.reduce((sum, width) => sum + width, 0) + columnWidths.length * 3 + 1;
|
|
5895
|
+
}
|
|
5896
|
+
function computeColumnWidths2(headerCells, bodyRows) {
|
|
5897
|
+
const columnCount = Math.max(
|
|
5898
|
+
headerCells.length,
|
|
5899
|
+
...bodyRows.map((row) => row.length),
|
|
5900
|
+
efficiencyTableHeaders.length
|
|
5901
|
+
);
|
|
5902
|
+
const widths = Array.from({ length: columnCount }, () => 0);
|
|
5903
|
+
const measureRow = (row) => {
|
|
5904
|
+
for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
|
|
5905
|
+
for (const line of splitCellLines(row[columnIndex] ?? "")) {
|
|
5906
|
+
widths[columnIndex] = Math.max(widths[columnIndex], visibleWidth(line));
|
|
5907
|
+
}
|
|
5908
|
+
}
|
|
5909
|
+
};
|
|
5910
|
+
measureRow(headerCells);
|
|
5911
|
+
for (const row of bodyRows) {
|
|
5912
|
+
measureRow(row);
|
|
5913
|
+
}
|
|
5914
|
+
return widths;
|
|
5915
|
+
}
|
|
5916
|
+
function resolveWrappedCells(headerCells, bodyRows, widths) {
|
|
5917
|
+
let wrappedHeaderCells = [...headerCells];
|
|
5918
|
+
let wrappedBodyRows = bodyRows.map((row) => [...row]);
|
|
5919
|
+
for (let columnIndex = 0; columnIndex < widths.length; columnIndex += 1) {
|
|
5920
|
+
const columnWidth = widths[columnIndex] ?? 0;
|
|
5921
|
+
if (columnWidth <= 0) {
|
|
5922
|
+
continue;
|
|
5923
|
+
}
|
|
5924
|
+
wrappedHeaderCells = wrapTableColumn([wrappedHeaderCells], {
|
|
5925
|
+
columnIndex,
|
|
5926
|
+
width: columnWidth
|
|
5927
|
+
})[0] ?? [];
|
|
5928
|
+
wrappedBodyRows = wrapTableColumn(wrappedBodyRows, {
|
|
5929
|
+
columnIndex,
|
|
5930
|
+
width: columnWidth
|
|
5931
|
+
});
|
|
5932
|
+
}
|
|
5933
|
+
return {
|
|
5934
|
+
wrappedHeaderCells,
|
|
5935
|
+
wrappedBodyRows
|
|
5936
|
+
};
|
|
5937
|
+
}
|
|
5938
|
+
function fitTableCellsToTerminal(headerCells, bodyRows) {
|
|
5939
|
+
const naturalWidths = computeColumnWidths2(headerCells, bodyRows);
|
|
5940
|
+
const terminalWidth = resolveTtyColumns(process.stdout);
|
|
5941
|
+
if (terminalWidth === void 0 || measureRenderedTableWidth(naturalWidths) <= terminalWidth) {
|
|
5942
|
+
return {
|
|
5943
|
+
headerCells: [...headerCells],
|
|
5944
|
+
bodyRows: bodyRows.map((row) => [...row]),
|
|
5945
|
+
widths: naturalWidths
|
|
5946
|
+
};
|
|
5947
|
+
}
|
|
5948
|
+
const constrainedWidths = [...naturalWidths];
|
|
5949
|
+
let renderedTableWidth = measureRenderedTableWidth(constrainedWidths);
|
|
5950
|
+
while (renderedTableWidth > terminalWidth && constrainedWidths.some((width) => width > minimumEfficiencyColumnWidth)) {
|
|
5951
|
+
let widestIndex = -1;
|
|
5952
|
+
let widestWidth = -1;
|
|
5953
|
+
for (let columnIndex = 0; columnIndex < constrainedWidths.length; columnIndex += 1) {
|
|
5954
|
+
const columnWidth = constrainedWidths[columnIndex];
|
|
5955
|
+
if (columnWidth <= minimumEfficiencyColumnWidth || columnWidth <= widestWidth) {
|
|
5956
|
+
continue;
|
|
5957
|
+
}
|
|
5958
|
+
widestIndex = columnIndex;
|
|
5959
|
+
widestWidth = columnWidth;
|
|
5960
|
+
}
|
|
5961
|
+
if (widestIndex === -1) {
|
|
5962
|
+
break;
|
|
5963
|
+
}
|
|
5964
|
+
const overflowColumns = renderedTableWidth - terminalWidth;
|
|
5965
|
+
const maxReducibleWidth = widestWidth - minimumEfficiencyColumnWidth;
|
|
5966
|
+
const reduction = Math.min(overflowColumns, maxReducibleWidth);
|
|
5967
|
+
if (reduction <= 0) {
|
|
5968
|
+
break;
|
|
5969
|
+
}
|
|
5970
|
+
constrainedWidths[widestIndex] -= reduction;
|
|
5971
|
+
renderedTableWidth -= reduction;
|
|
5972
|
+
}
|
|
5973
|
+
const { wrappedHeaderCells, wrappedBodyRows } = resolveWrappedCells(
|
|
5974
|
+
headerCells,
|
|
5975
|
+
bodyRows,
|
|
5976
|
+
constrainedWidths
|
|
5977
|
+
);
|
|
5978
|
+
return {
|
|
5979
|
+
headerCells: wrappedHeaderCells,
|
|
5980
|
+
bodyRows: wrappedBodyRows,
|
|
5981
|
+
widths: constrainedWidths
|
|
5982
|
+
};
|
|
5983
|
+
}
|
|
5633
5984
|
function renderTerminalEfficiencyTable(rows) {
|
|
5985
|
+
const headerCells = Array.from(efficiencyTableHeaders);
|
|
5634
5986
|
const bodyRows = toEfficiencyTableCells(rows);
|
|
5635
5987
|
const tableSortRows = rows.map((row) => toTableSortRow(row));
|
|
5636
|
-
const
|
|
5637
|
-
efficiencyTableHeaders[0].length,
|
|
5638
|
-
...rows.map((row) => row.periodKey.length)
|
|
5639
|
-
);
|
|
5988
|
+
const fittedCells = fitTableCellsToTerminal(headerCells, bodyRows);
|
|
5640
5989
|
return renderUnicodeTable({
|
|
5641
|
-
headerCells:
|
|
5642
|
-
bodyRows,
|
|
5643
|
-
measureHeaderCells:
|
|
5644
|
-
measureBodyRows: bodyRows,
|
|
5990
|
+
headerCells: fittedCells.headerCells,
|
|
5991
|
+
bodyRows: fittedCells.bodyRows,
|
|
5992
|
+
measureHeaderCells: fittedCells.headerCells,
|
|
5993
|
+
measureBodyRows: fittedCells.bodyRows,
|
|
5645
5994
|
usageRows: tableSortRows,
|
|
5646
5995
|
tableLayout: "compact",
|
|
5647
|
-
modelsColumnIndex:
|
|
5648
|
-
modelsColumnWidth:
|
|
5996
|
+
modelsColumnIndex: periodColumnIndex,
|
|
5997
|
+
modelsColumnWidth: fittedCells.widths[periodColumnIndex] ?? efficiencyTableHeaders[periodColumnIndex].length
|
|
5649
5998
|
});
|
|
5650
5999
|
}
|
|
5651
6000
|
function toMarkdownSafeCell(value) {
|
|
@@ -5847,7 +6196,7 @@ function addSharedOptions(command, options = {}) {
|
|
|
5847
6196
|
const allowedSourcesLabel = getAllowedSourcesLabel(supportedSourceIds);
|
|
5848
6197
|
const supportedSourcesSummary = `(${supportedSourceIds.length}): ${allowedSourcesLabel}`;
|
|
5849
6198
|
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(
|
|
6199
|
+
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
6200
|
"--source-dir <source-id=path>",
|
|
5852
6201
|
"Override source directory for directory-backed sources (repeatable)",
|
|
5853
6202
|
collectRepeatedOption,
|
|
@@ -5921,10 +6270,10 @@ function rootDescription() {
|
|
|
5921
6270
|
" $ llm-usage monthly --since 2026-01-01 --until 2026-01-31 --source pi,codex --json",
|
|
5922
6271
|
" $ llm-usage monthly --source opencode --opencode-db /path/to/opencode.db --json",
|
|
5923
6272
|
" $ 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",
|
|
6273
|
+
" $ llm-usage daily --source-dir pi=/tmp/pi-sessions --source-dir gemini=/tmp/.gemini --source-dir droid=/tmp/droid-sessions",
|
|
6274
|
+
" $ llm-usage daily --pi-dir /tmp/pi-sessions --gemini-dir /tmp/.gemini --droid-dir /tmp/droid-sessions",
|
|
5926
6275
|
" $ llm-usage efficiency weekly --repo-dir /path/to/repo --json",
|
|
5927
|
-
" $ npx --yes llm-usage-metrics daily"
|
|
6276
|
+
" $ npx --yes llm-usage-metrics@latest daily"
|
|
5928
6277
|
].join("\n");
|
|
5929
6278
|
}
|
|
5930
6279
|
function createCli(options = {}) {
|