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/dist/index.js CHANGED
@@ -748,7 +748,7 @@ function createUsageEvent(input) {
748
748
  };
749
749
  }
750
750
 
751
- // src/utils/discover-jsonl-files.ts
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
- entries.sort((left, right) => compareByCodePoint(left.name, right.name));
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, { allowPermissionSkip: true });
795
+ if (entry.isDirectory() && options.recursive) {
796
+ await walkDirectory(entryPath, acc, options);
777
797
  continue;
778
798
  }
779
- if (entry.isFile() && entry.name.toLowerCase().endsWith(".jsonl")) {
799
+ if (entry.isFile() && matchesExtension(entry.name, options.extension)) {
780
800
  acc.push(entryPath);
781
801
  }
782
802
  }
783
803
  }
784
- async function discoverJsonlFiles(rootDir) {
804
+ async function discoverFiles(rootDir, options) {
785
805
  const files = [];
786
- try {
787
- await walkDirectory(rootDir, files, { allowPermissionSkip: false });
788
- } catch (error) {
789
- if (getNodeErrorCode(error) === "ENOENT") {
790
- return [];
791
- }
792
- throw error;
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/opencode/opencode-db-path-resolver.ts
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 ?? path5.join(homeDir, ".local", "share");
1525
+ const xdgDataHome = env.XDG_DATA_HOME ?? path7.join(homeDir, ".local", "share");
1081
1526
  return [
1082
- path5.join(xdgDataHome, "opencode", "opencode.db"),
1083
- path5.join(xdgDataHome, "opencode", "db.sqlite"),
1084
- path5.join(homeDir, ".opencode", "opencode.db"),
1085
- path5.join(homeDir, ".opencode", "db.sqlite")
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 = path5.join(homeDir, "Library", "Application Support");
1534
+ const appSupportDir = path7.join(homeDir, "Library", "Application Support");
1090
1535
  return [
1091
- path5.join(appSupportDir, "opencode", "opencode.db"),
1092
- path5.join(appSupportDir, "opencode", "db.sqlite"),
1093
- path5.join(homeDir, ".opencode", "opencode.db"),
1094
- path5.join(homeDir, ".opencode", "db.sqlite")
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 ? path5.join(env.USERPROFILE, "AppData", "Roaming") : void 0);
1543
+ const roamingBase = env.APPDATA ?? env.LOCALAPPDATA ?? (env.USERPROFILE ? path7.join(env.USERPROFILE, "AppData", "Roaming") : void 0);
1099
1544
  const roamingCandidates = roamingBase ? [
1100
- path5.join(roamingBase, "opencode", "opencode.db"),
1101
- path5.join(roamingBase, "opencode", "db.sqlite")
1545
+ path7.join(roamingBase, "opencode", "opencode.db"),
1546
+ path7.join(roamingBase, "opencode", "db.sqlite")
1102
1547
  ] : [];
1103
1548
  return [
1104
1549
  ...roamingCandidates,
1105
- path5.join(homeDir, ".opencode", "opencode.db"),
1106
- path5.join(homeDir, ".opencode", "db.sqlite")
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 ?? os3.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 UNIX_SECONDS_ABS_CUTOFF = 1e10;
1178
- function normalizeTimestampCandidate(candidate) {
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) <= UNIX_SECONDS_ABS_CUTOFF ? candidate * 1e3 : 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 normalizeTimestampCandidate(numericTimestamp);
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 = normalizeTimestampCandidate(candidate);
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 resolveRepoRoot(messagePayload) {
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 = resolveRepoRoot(payload);
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 os4 from "os";
1619
- import path6 from "path";
1620
- var defaultSessionsDir2 = path6.join(os4.homedir(), ".pi", "agent", "sessions");
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 UNIX_SECONDS_ABS_CUTOFF2 = 1e10;
1629
- function isBlankText3(value) {
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) <= UNIX_SECONDS_ABS_CUTOFF2 ? candidate * 1e3 : 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 = normalizeTimestampCandidate2(candidate);
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 toFiniteNumber = (value) => {
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 = toFiniteNumber(value);
2136
+ const parsed = toFiniteNumber2(value);
1695
2137
  return parsed !== void 0 && parsed > 0;
1696
2138
  });
1697
- const explicitCost = toFiniteNumber(extracted.costUsd);
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 path6.basename(filePath, ".jsonl");
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 ?? defaultSessionsDir2;
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 (isBlankText3(this.sessionsDir)) {
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 path7 from "path";
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 path7.resolve(process.cwd());
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 path7.resolve(normalizedRepoDir);
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 path8 from "path";
3181
+ import path10 from "path";
2708
3182
  async function hasGitMarker(directoryPath) {
2709
3183
  try {
2710
- await access2(path8.join(directoryPath, ".git"), constants2.F_OK);
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 = path8.normalize(path8.resolve(value));
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 = path8.resolve(value);
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 = path8.resolve(trimmedPath);
3207
+ let currentPath = path10.resolve(trimmedPath);
2734
3208
  for (; ; ) {
2735
3209
  if (await hasGitMarker(currentPath)) {
2736
3210
  return currentPath;
2737
3211
  }
2738
- const parentPath = path8.dirname(currentPath);
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, resolveRepoRoot2 = resolveRepoRootFromPathHint) {
2746
- const resolvedTargetRepoRoot = await resolveRepoRoot2(repoDir).catch(() => void 0);
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 resolveRepoRoot2(eventRepoRoot).catch(() => void 0);
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 readFile2, writeFile as writeFile2 } from "fs/promises";
3060
- import path9 from "path";
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 path9.join(getUserCacheRootDir(), "llm-usage-metrics", "parse-file-cache.json");
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(path9.dirname(this.cacheFilePath), { recursive: true });
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 readFile2(this.cacheFilePath, "utf8");
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 readFile3, writeFile as writeFile3 } from "fs/promises";
3580
- import path10 from "path";
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": "Temporary fallback to gpt-5.2-codex until upstream publishes direct gpt-5.3-codex token pricing"
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 path10.join(getUserCacheRootDir(), "llm-usage-metrics", "litellm-pricing-cache.json");
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 readFile3(this.cacheFilePath, "utf8");
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 = path10.dirname(this.cacheFilePath);
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 resolveRepoRoot2 = deps.resolveRepoRoot ?? resolveRepoRootFromPathHint;
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
- resolveRepoRoot2
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 = {}) {