llm-usage-metrics 0.3.3 → 0.3.4

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,294 @@ var CodexSourceAdapter = class {
1070
1094
  }
1071
1095
  };
1072
1096
 
1073
- // src/sources/opencode/opencode-db-path-resolver.ts
1097
+ // src/sources/gemini/gemini-source-adapter.ts
1098
+ import { readFile as readFile2 } from "fs/promises";
1074
1099
  import os3 from "os";
1075
1100
  import path5 from "path";
1101
+ var defaultGeminiDir = path5.join(os3.homedir(), ".gemini");
1102
+ function parseProjectsJson(data) {
1103
+ const mapping = /* @__PURE__ */ new Map();
1104
+ const record = asRecord(data);
1105
+ if (!record) {
1106
+ return mapping;
1107
+ }
1108
+ const projects = asRecord(record.projects);
1109
+ if (!projects) {
1110
+ return mapping;
1111
+ }
1112
+ for (const [key, value] of Object.entries(projects)) {
1113
+ const projectEntry = asRecord(value);
1114
+ const absolutePath = asTrimmedText(projectEntry?.absolutePath);
1115
+ if (absolutePath) {
1116
+ mapping.set(key, absolutePath);
1117
+ }
1118
+ }
1119
+ return mapping;
1120
+ }
1121
+ async function loadProjectsJson(geminiDir) {
1122
+ const projectsPath = path5.join(geminiDir, "projects.json");
1123
+ try {
1124
+ const content = await readFile2(projectsPath, "utf8");
1125
+ const parsed = JSON.parse(content);
1126
+ return parseProjectsJson(parsed);
1127
+ } catch {
1128
+ return /* @__PURE__ */ new Map();
1129
+ }
1130
+ }
1131
+ async function discoverSessionFiles(geminiDir) {
1132
+ const tmpDir = path5.join(geminiDir, "tmp");
1133
+ const allSessionFiles = [];
1134
+ const discoveredFiles = await discoverFiles(tmpDir, { extension: ".json" });
1135
+ for (const filePath of discoveredFiles) {
1136
+ const parentDir = path5.basename(path5.dirname(filePath));
1137
+ if (parentDir.toLowerCase() === "chats") {
1138
+ allSessionFiles.push(filePath);
1139
+ }
1140
+ }
1141
+ return allSessionFiles;
1142
+ }
1143
+ function resolveRepoRoot(filePath, sessionData, projectMapping) {
1144
+ const projectHash = asTrimmedText(sessionData.projectHash);
1145
+ if (projectHash) {
1146
+ const mappedRoot = projectMapping.get(projectHash);
1147
+ if (mappedRoot) {
1148
+ return mappedRoot;
1149
+ }
1150
+ }
1151
+ const chatsDir = path5.dirname(filePath);
1152
+ const projectDir = path5.dirname(chatsDir);
1153
+ const projectIdentifier = path5.basename(projectDir);
1154
+ return projectMapping.get(projectIdentifier);
1155
+ }
1156
+ function toFiniteNumber(value) {
1157
+ if (typeof value === "number") {
1158
+ return Number.isFinite(value) ? value : void 0;
1159
+ }
1160
+ if (typeof value !== "string") {
1161
+ return void 0;
1162
+ }
1163
+ const trimmed = value.trim();
1164
+ if (!trimmed) {
1165
+ return void 0;
1166
+ }
1167
+ const parsed = Number(trimmed);
1168
+ if (!Number.isFinite(parsed)) {
1169
+ return void 0;
1170
+ }
1171
+ return parsed;
1172
+ }
1173
+ function extractTokenUsage(tokens) {
1174
+ if (!tokens) {
1175
+ return null;
1176
+ }
1177
+ const input = Math.max(0, toFiniteNumber(tokens.input) ?? 0);
1178
+ const tool = Math.max(0, toFiniteNumber(tokens.tool) ?? 0);
1179
+ const output = Math.max(0, toFiniteNumber(tokens.output) ?? 0);
1180
+ const thoughts = Math.max(0, toFiniteNumber(tokens.thoughts) ?? 0);
1181
+ const cached = Math.max(0, toFiniteNumber(tokens.cached) ?? 0);
1182
+ const inputTokens = input + tool;
1183
+ const outputTokens = output;
1184
+ const reasoningTokens = thoughts;
1185
+ const cacheReadTokens = cached;
1186
+ const declaredTotal = Math.max(0, toFiniteNumber(tokens.total) ?? 0);
1187
+ const componentTotal = inputTokens + outputTokens + reasoningTokens + cacheReadTokens;
1188
+ const totalTokens = declaredTotal > 0 ? declaredTotal : componentTotal;
1189
+ if (inputTokens === 0 && outputTokens === 0 && reasoningTokens === 0 && cached === 0) {
1190
+ return null;
1191
+ }
1192
+ return {
1193
+ inputTokens,
1194
+ outputTokens,
1195
+ reasoningTokens,
1196
+ cacheReadTokens,
1197
+ totalTokens
1198
+ };
1199
+ }
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
+ function normalizeTimestamp2(candidate) {
1215
+ if (typeof candidate !== "string" || isBlankText(candidate)) {
1216
+ return void 0;
1217
+ }
1218
+ const date = new Date(candidate.trim());
1219
+ if (Number.isNaN(date.getTime())) {
1220
+ return void 0;
1221
+ }
1222
+ return date.toISOString();
1223
+ }
1224
+ var GeminiSourceAdapter = class {
1225
+ id = "gemini";
1226
+ geminiDir;
1227
+ requireGeminiDir;
1228
+ projectMapping = null;
1229
+ constructor(options = {}) {
1230
+ this.geminiDir = options.geminiDir ?? defaultGeminiDir;
1231
+ this.requireGeminiDir = options.requireGeminiDir ?? false;
1232
+ }
1233
+ getNormalizedGeminiDir() {
1234
+ if (isBlankText(this.geminiDir)) {
1235
+ throw new Error("Gemini directory must be a non-empty path");
1236
+ }
1237
+ return this.geminiDir.trim();
1238
+ }
1239
+ async getProjectMapping(normalizedGeminiDir) {
1240
+ if (this.projectMapping) {
1241
+ return this.projectMapping;
1242
+ }
1243
+ this.projectMapping = await loadProjectsJson(normalizedGeminiDir);
1244
+ return this.projectMapping;
1245
+ }
1246
+ async getProjectMappingForParse() {
1247
+ if (this.projectMapping) {
1248
+ return this.projectMapping;
1249
+ }
1250
+ if (isBlankText(this.geminiDir)) {
1251
+ return /* @__PURE__ */ new Map();
1252
+ }
1253
+ this.projectMapping = await loadProjectsJson(this.geminiDir.trim());
1254
+ return this.projectMapping;
1255
+ }
1256
+ async discoverFiles() {
1257
+ const normalizedDir = this.getNormalizedGeminiDir();
1258
+ if (this.requireGeminiDir && !await pathReadable(normalizedDir)) {
1259
+ throw new Error(`Gemini directory is missing or unreadable: ${normalizedDir}`);
1260
+ }
1261
+ if (this.requireGeminiDir && !await pathIsDirectory(normalizedDir)) {
1262
+ throw new Error(`Gemini directory is not a directory: ${normalizedDir}`);
1263
+ }
1264
+ await this.getProjectMapping(normalizedDir);
1265
+ return discoverSessionFiles(normalizedDir);
1266
+ }
1267
+ async parseFile(filePath) {
1268
+ const { events } = await this.parseFileWithDiagnostics(filePath);
1269
+ return events;
1270
+ }
1271
+ async parseFileWithDiagnostics(filePath) {
1272
+ const events = [];
1273
+ let skippedRows = 0;
1274
+ const skippedRowReasons = /* @__PURE__ */ new Map();
1275
+ let sessionData;
1276
+ try {
1277
+ const content = await readFile2(filePath, "utf8");
1278
+ sessionData = JSON.parse(content);
1279
+ } catch {
1280
+ skippedRows++;
1281
+ incrementSkippedReason(skippedRowReasons, "json_parse_error");
1282
+ return toParseDiagnostics(events, skippedRows, skippedRowReasons);
1283
+ }
1284
+ const sessionDataRecord = asRecord(sessionData);
1285
+ if (!sessionDataRecord) {
1286
+ skippedRows++;
1287
+ incrementSkippedReason(skippedRowReasons, "invalid_session_data");
1288
+ return toParseDiagnostics(events, skippedRows, skippedRowReasons);
1289
+ }
1290
+ const sessionId = asTrimmedText(sessionDataRecord.sessionId) ?? path5.basename(filePath, ".json");
1291
+ const projectMapping = await this.getProjectMappingForParse();
1292
+ const repoRoot = resolveRepoRoot(filePath, sessionDataRecord, projectMapping);
1293
+ if (!Array.isArray(sessionDataRecord.messages)) {
1294
+ skippedRows++;
1295
+ incrementSkippedReason(skippedRowReasons, "invalid_messages_array");
1296
+ return toParseDiagnostics(events, skippedRows, skippedRowReasons);
1297
+ }
1298
+ const messages = sessionDataRecord.messages;
1299
+ for (const rawMessage of messages) {
1300
+ const message = asRecord(rawMessage);
1301
+ if (!message) {
1302
+ skippedRows++;
1303
+ incrementSkippedReason(skippedRowReasons, "invalid_message");
1304
+ continue;
1305
+ }
1306
+ if (message.type !== "gemini") {
1307
+ skippedRows++;
1308
+ incrementSkippedReason(skippedRowReasons, "non_gemini_message");
1309
+ continue;
1310
+ }
1311
+ const tokens = extractTokenUsage(asRecord(message.tokens));
1312
+ if (!tokens) {
1313
+ skippedRows++;
1314
+ incrementSkippedReason(skippedRowReasons, "no_token_usage");
1315
+ continue;
1316
+ }
1317
+ const timestamp = normalizeTimestamp2(message.timestamp);
1318
+ if (!timestamp) {
1319
+ skippedRows++;
1320
+ incrementSkippedReason(skippedRowReasons, "invalid_timestamp");
1321
+ continue;
1322
+ }
1323
+ const model = asTrimmedText(message.model);
1324
+ try {
1325
+ events.push(
1326
+ createUsageEvent({
1327
+ source: this.id,
1328
+ sessionId,
1329
+ timestamp,
1330
+ repoRoot,
1331
+ provider: "google",
1332
+ model,
1333
+ ...tokens,
1334
+ costMode: "estimated"
1335
+ })
1336
+ );
1337
+ } catch {
1338
+ skippedRows++;
1339
+ incrementSkippedReason(skippedRowReasons, "event_creation_failed");
1340
+ }
1341
+ }
1342
+ return toParseDiagnostics(events, skippedRows, skippedRowReasons);
1343
+ }
1344
+ };
1345
+
1346
+ // src/sources/opencode/opencode-db-path-resolver.ts
1347
+ import os4 from "os";
1348
+ import path6 from "path";
1076
1349
  function deduplicate(paths) {
1077
1350
  return [...new Set(paths)];
1078
1351
  }
1079
1352
  function getLinuxLikeCandidates(homeDir, env) {
1080
- const xdgDataHome = env.XDG_DATA_HOME ?? path5.join(homeDir, ".local", "share");
1353
+ const xdgDataHome = env.XDG_DATA_HOME ?? path6.join(homeDir, ".local", "share");
1081
1354
  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")
1355
+ path6.join(xdgDataHome, "opencode", "opencode.db"),
1356
+ path6.join(xdgDataHome, "opencode", "db.sqlite"),
1357
+ path6.join(homeDir, ".opencode", "opencode.db"),
1358
+ path6.join(homeDir, ".opencode", "db.sqlite")
1086
1359
  ];
1087
1360
  }
1088
1361
  function getMacOsCandidates(homeDir) {
1089
- const appSupportDir = path5.join(homeDir, "Library", "Application Support");
1362
+ const appSupportDir = path6.join(homeDir, "Library", "Application Support");
1090
1363
  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")
1364
+ path6.join(appSupportDir, "opencode", "opencode.db"),
1365
+ path6.join(appSupportDir, "opencode", "db.sqlite"),
1366
+ path6.join(homeDir, ".opencode", "opencode.db"),
1367
+ path6.join(homeDir, ".opencode", "db.sqlite")
1095
1368
  ];
1096
1369
  }
1097
1370
  function getWindowsCandidates(homeDir, env) {
1098
- const roamingBase = env.APPDATA ?? env.LOCALAPPDATA ?? (env.USERPROFILE ? path5.join(env.USERPROFILE, "AppData", "Roaming") : void 0);
1371
+ const roamingBase = env.APPDATA ?? env.LOCALAPPDATA ?? (env.USERPROFILE ? path6.join(env.USERPROFILE, "AppData", "Roaming") : void 0);
1099
1372
  const roamingCandidates = roamingBase ? [
1100
- path5.join(roamingBase, "opencode", "opencode.db"),
1101
- path5.join(roamingBase, "opencode", "db.sqlite")
1373
+ path6.join(roamingBase, "opencode", "opencode.db"),
1374
+ path6.join(roamingBase, "opencode", "db.sqlite")
1102
1375
  ] : [];
1103
1376
  return [
1104
1377
  ...roamingCandidates,
1105
- path5.join(homeDir, ".opencode", "opencode.db"),
1106
- path5.join(homeDir, ".opencode", "db.sqlite")
1378
+ path6.join(homeDir, ".opencode", "opencode.db"),
1379
+ path6.join(homeDir, ".opencode", "db.sqlite")
1107
1380
  ];
1108
1381
  }
1109
1382
  function getDefaultOpenCodeDbPathCandidates(options = {}) {
1110
1383
  const platform = options.platform ?? process.platform;
1111
- const homeDir = options.homeDir ?? os3.homedir();
1384
+ const homeDir = options.homeDir ?? os4.homedir();
1112
1385
  const env = options.env ?? process.env;
1113
1386
  switch (platform) {
1114
1387
  case "win32":
@@ -1236,7 +1509,7 @@ function normalizeSessionIdCandidate(value) {
1236
1509
  }
1237
1510
  return asTrimmedText(value);
1238
1511
  }
1239
- function resolveRepoRoot(messagePayload) {
1512
+ function resolveRepoRoot2(messagePayload) {
1240
1513
  const pathPayload = asRecord(messagePayload.path);
1241
1514
  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
1515
  }
@@ -1291,7 +1564,7 @@ function parseOpenCodeMessageRows(rows, sourceId) {
1291
1564
  }
1292
1565
  const provider = asTrimmedText(payload.providerID) ?? asTrimmedText(payload.provider);
1293
1566
  const model = asTrimmedText(payload.modelID) ?? asTrimmedText(payload.model);
1294
- const repoRoot = resolveRepoRoot(payload);
1567
+ const repoRoot = resolveRepoRoot2(payload);
1295
1568
  const tokens = asRecord(payload.tokens);
1296
1569
  const tokenCache = asRecord(tokens?.cache);
1297
1570
  const inputTokens = toNumberLike(tokens?.input);
@@ -1615,9 +1888,9 @@ var OpenCodeSourceAdapter = class {
1615
1888
  };
1616
1889
 
1617
1890
  // 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");
1891
+ import os5 from "os";
1892
+ import path7 from "path";
1893
+ var defaultSessionsDir2 = path7.join(os5.homedir(), ".pi", "agent", "sessions");
1621
1894
  var PI_MESSAGE_LINE_PATTERN = /"type"\s*:\s*"message"/u;
1622
1895
  var PI_SESSION_LINE_PATTERN = /"type"\s*:\s*"session"/u;
1623
1896
  var PI_MODEL_CHANGE_LINE_PATTERN = /"type"\s*:\s*"model_change"/u;
@@ -1626,9 +1899,6 @@ function shouldParsePiJsonlLine(lineText) {
1626
1899
  }
1627
1900
  var allowAllProviders = () => true;
1628
1901
  var UNIX_SECONDS_ABS_CUTOFF2 = 1e10;
1629
- function isBlankText3(value) {
1630
- return value.trim().length === 0;
1631
- }
1632
1902
  function normalizeTimestampCandidate2(candidate) {
1633
1903
  let date;
1634
1904
  if (typeof candidate === "number" && Number.isFinite(candidate)) {
@@ -1669,7 +1939,7 @@ function extractUsageFromRecord(usage) {
1669
1939
  totalTokens: toNumberLike(usage.totalTokens),
1670
1940
  costUsd: toNumberLike(cost?.total)
1671
1941
  };
1672
- const toFiniteNumber = (value) => {
1942
+ const toFiniteNumber2 = (value) => {
1673
1943
  if (value === null || value === void 0) {
1674
1944
  return void 0;
1675
1945
  }
@@ -1691,10 +1961,10 @@ function extractUsageFromRecord(usage) {
1691
1961
  extracted.totalTokens
1692
1962
  ];
1693
1963
  const hasPositiveUsageSignal = usageCandidates.some((value) => {
1694
- const parsed = toFiniteNumber(value);
1964
+ const parsed = toFiniteNumber2(value);
1695
1965
  return parsed !== void 0 && parsed > 0;
1696
1966
  });
1697
- const explicitCost = toFiniteNumber(extracted.costUsd);
1967
+ const explicitCost = toFiniteNumber2(extracted.costUsd);
1698
1968
  const hasPositiveCostSignal = explicitCost !== void 0 && explicitCost > 0;
1699
1969
  return hasPositiveUsageSignal || hasPositiveCostSignal ? extracted : void 0;
1700
1970
  }
@@ -1713,7 +1983,7 @@ function extractUsage(line, message) {
1713
1983
  return extractUsageFromRecord(messageUsage);
1714
1984
  }
1715
1985
  function getFallbackSessionId2(filePath) {
1716
- return path6.basename(filePath, ".jsonl");
1986
+ return path7.basename(filePath, ".jsonl");
1717
1987
  }
1718
1988
  function resolveRepoRootFromRecord(record) {
1719
1989
  if (!record) {
@@ -1733,7 +2003,7 @@ var PiSourceAdapter = class {
1733
2003
  this.requireSessionsDir = options.requireSessionsDir ?? false;
1734
2004
  }
1735
2005
  async discoverFiles() {
1736
- if (isBlankText3(this.sessionsDir)) {
2006
+ if (isBlankText(this.sessionsDir)) {
1737
2007
  throw new Error("PI sessions directory must be a non-empty path");
1738
2008
  }
1739
2009
  const normalizedSessionsDir = this.sessionsDir.trim();
@@ -1851,6 +2121,21 @@ var sourceRegistrations = [
1851
2121
  });
1852
2122
  }
1853
2123
  },
2124
+ {
2125
+ id: "gemini",
2126
+ sourceDirOverride: { kind: "directory" },
2127
+ create: (options, sourceDirectoryOverrides) => {
2128
+ const directoryConfig = resolveDirectoryConfig(
2129
+ "gemini",
2130
+ options.geminiDir,
2131
+ sourceDirectoryOverrides
2132
+ );
2133
+ return new GeminiSourceAdapter({
2134
+ geminiDir: directoryConfig.path,
2135
+ requireGeminiDir: directoryConfig.requireExistingPath
2136
+ });
2137
+ }
2138
+ },
1854
2139
  {
1855
2140
  id: "opencode",
1856
2141
  sourceDirOverride: { kind: "unsupported", flag: "--opencode-db" },
@@ -1931,6 +2216,7 @@ function createDefaultAdapters(options) {
1931
2216
  validateOpencodeOverride(options.opencodeDb);
1932
2217
  validateDirectoryOverride("--pi-dir", options.piDir);
1933
2218
  validateDirectoryOverride("--codex-dir", options.codexDir);
2219
+ validateDirectoryOverride("--gemini-dir", options.geminiDir);
1934
2220
  const sourceDirectoryOverrides = parseSourceDirectoryOverrides(options.sourceDir);
1935
2221
  validateSourceDirectoryOverrideIds(sourceDirectoryOverrides);
1936
2222
  return sourceRegistrations.map((source) => source.create(options, sourceDirectoryOverrides));
@@ -2306,7 +2592,7 @@ function aggregateEfficiency(options) {
2306
2592
  // src/efficiency/git-outcome-collector.ts
2307
2593
  import { spawn as spawn2 } from "child_process";
2308
2594
  import { createInterface as createInterface3 } from "readline";
2309
- import path7 from "path";
2595
+ import path8 from "path";
2310
2596
  import { stat as stat2 } from "fs/promises";
2311
2597
  var GIT_COMMIT_MARKER = "";
2312
2598
  var SHORTSTAT_PATTERN = /(\d+)\s+files?\s+changed(?:,\s+(\d+)\s+insertions?\(\+\))?(?:,\s+(\d+)\s+deletions?\(-\))?/u;
@@ -2326,13 +2612,13 @@ function resolveGitCommandFailureReason(result) {
2326
2612
  }
2327
2613
  function resolveRepoDir(repoDir) {
2328
2614
  if (repoDir === void 0) {
2329
- return path7.resolve(process.cwd());
2615
+ return path8.resolve(process.cwd());
2330
2616
  }
2331
2617
  const normalizedRepoDir = repoDir.trim();
2332
2618
  if (!normalizedRepoDir) {
2333
2619
  throw new Error("--repo-dir must be a non-empty path");
2334
2620
  }
2335
- return path7.resolve(normalizedRepoDir);
2621
+ return path8.resolve(normalizedRepoDir);
2336
2622
  }
2337
2623
  function getNodeErrorCode2(error) {
2338
2624
  if (typeof error !== "object" || error === null || !("code" in error)) {
@@ -2704,21 +2990,21 @@ async function collectGitOutcomes(options, deps = {}) {
2704
2990
 
2705
2991
  // src/efficiency/repo-attribution.ts
2706
2992
  import { access as access2, constants as constants2, realpath } from "fs/promises";
2707
- import path8 from "path";
2993
+ import path9 from "path";
2708
2994
  async function hasGitMarker(directoryPath) {
2709
2995
  try {
2710
- await access2(path8.join(directoryPath, ".git"), constants2.F_OK);
2996
+ await access2(path9.join(directoryPath, ".git"), constants2.F_OK);
2711
2997
  return true;
2712
2998
  } catch {
2713
2999
  return false;
2714
3000
  }
2715
3001
  }
2716
3002
  function normalizeComparablePath(value) {
2717
- const normalizedPath = path8.normalize(path8.resolve(value));
3003
+ const normalizedPath = path9.normalize(path9.resolve(value));
2718
3004
  return process.platform === "win32" ? normalizedPath.toLowerCase() : normalizedPath;
2719
3005
  }
2720
3006
  async function resolveComparablePath(value) {
2721
- const resolvedPath = path8.resolve(value);
3007
+ const resolvedPath = path9.resolve(value);
2722
3008
  try {
2723
3009
  return normalizeComparablePath(await realpath(resolvedPath));
2724
3010
  } catch {
@@ -2730,20 +3016,20 @@ async function resolveRepoRootFromPathHint(pathHint) {
2730
3016
  if (!trimmedPath) {
2731
3017
  return void 0;
2732
3018
  }
2733
- let currentPath = path8.resolve(trimmedPath);
3019
+ let currentPath = path9.resolve(trimmedPath);
2734
3020
  for (; ; ) {
2735
3021
  if (await hasGitMarker(currentPath)) {
2736
3022
  return currentPath;
2737
3023
  }
2738
- const parentPath = path8.dirname(currentPath);
3024
+ const parentPath = path9.dirname(currentPath);
2739
3025
  if (parentPath === currentPath) {
2740
3026
  return void 0;
2741
3027
  }
2742
3028
  currentPath = parentPath;
2743
3029
  }
2744
3030
  }
2745
- async function attributeUsageEventsToRepo(events, repoDir, resolveRepoRoot2 = resolveRepoRootFromPathHint) {
2746
- const resolvedTargetRepoRoot = await resolveRepoRoot2(repoDir).catch(() => void 0);
3031
+ async function attributeUsageEventsToRepo(events, repoDir, resolveRepoRoot3 = resolveRepoRootFromPathHint) {
3032
+ const resolvedTargetRepoRoot = await resolveRepoRoot3(repoDir).catch(() => void 0);
2747
3033
  const targetRepoPath = await resolveComparablePath(resolvedTargetRepoRoot ?? repoDir);
2748
3034
  const rootCache = /* @__PURE__ */ new Map();
2749
3035
  const matchedEvents = [];
@@ -2756,7 +3042,7 @@ async function attributeUsageEventsToRepo(events, repoDir, resolveRepoRoot2 = re
2756
3042
  }
2757
3043
  const eventRepoRoot = event.repoRoot;
2758
3044
  const cachedRootPromise = rootCache.get(eventRepoRoot) ?? (async () => {
2759
- const resolvedRoot2 = await resolveRepoRoot2(eventRepoRoot).catch(() => void 0);
3045
+ const resolvedRoot2 = await resolveRepoRoot3(eventRepoRoot).catch(() => void 0);
2760
3046
  if (!resolvedRoot2) {
2761
3047
  return void 0;
2762
3048
  }
@@ -3056,8 +3342,8 @@ function normalizeSkippedRowReasons(value) {
3056
3342
  }
3057
3343
 
3058
3344
  // 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";
3345
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
3346
+ import path10 from "path";
3061
3347
  var PARSE_FILE_CACHE_VERSION = 2;
3062
3348
  var CACHE_KEY_SEPARATOR = "\0";
3063
3349
  function createCacheKey(source, filePath) {
@@ -3196,7 +3482,7 @@ function normalizeCacheEntry(value) {
3196
3482
  };
3197
3483
  }
3198
3484
  function getDefaultParseFileCachePath() {
3199
- return path9.join(getUserCacheRootDir(), "llm-usage-metrics", "parse-file-cache.json");
3485
+ return path10.join(getUserCacheRootDir(), "llm-usage-metrics", "parse-file-cache.json");
3200
3486
  }
3201
3487
  var ParseFileCache = class _ParseFileCache {
3202
3488
  constructor(cacheFilePath, limits, now) {
@@ -3282,7 +3568,7 @@ var ParseFileCache = class _ParseFileCache {
3282
3568
  keptEntries.length = bestCount;
3283
3569
  payloadText = bestPayloadText;
3284
3570
  }
3285
- await mkdir2(path9.dirname(this.cacheFilePath), { recursive: true });
3571
+ await mkdir2(path10.dirname(this.cacheFilePath), { recursive: true });
3286
3572
  await writeFile2(this.cacheFilePath, payloadText, "utf8");
3287
3573
  this.dirty = false;
3288
3574
  }
@@ -3305,7 +3591,7 @@ var ParseFileCache = class _ParseFileCache {
3305
3591
  async loadFromDisk() {
3306
3592
  let content;
3307
3593
  try {
3308
- content = await readFile2(this.cacheFilePath, "utf8");
3594
+ content = await readFile3(this.cacheFilePath, "utf8");
3309
3595
  } catch {
3310
3596
  return;
3311
3597
  }
@@ -3576,8 +3862,8 @@ function applyPricingToEvents(events, pricingSource) {
3576
3862
  }
3577
3863
 
3578
3864
  // 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";
3865
+ import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
3866
+ import path11 from "path";
3581
3867
 
3582
3868
  // src/pricing/litellm-model-map.json
3583
3869
  var litellm_model_map_default = {
@@ -3585,20 +3871,34 @@ var litellm_model_map_default = {
3585
3871
  k2p5: "kimi-k2.5",
3586
3872
  "kimi-k2p5": "kimi-k2.5",
3587
3873
  "kimi-k2.5": "kimi-k2.5",
3874
+ "kimi-k2.5-free": "kimi-k2.5",
3588
3875
  "moonshotai.kimi-k2.5": "kimi-k2.5",
3589
3876
  "moonshot/kimi-k2.5": "kimi-k2.5",
3877
+ "gpt-5.3-codex-spark": "gpt-5.3-codex",
3878
+ "gemini-3-pro": "gemini-3-pro",
3879
+ "antigravity-gemini-3-flash": "gemini-3-flash",
3880
+ "antigravity-gemini-3-pro": "gemini-3-pro",
3881
+ "antigravity-gemini-3-pro-high": "gemini-3-pro",
3882
+ "minimax-m2.1": "minimax-m2.1",
3883
+ "minimax-m2.1-free": "minimax-m2.1",
3884
+ "minimax-m2.5": "minimax-m2.5",
3885
+ "minimax-m2.5-free": "minimax-m2.5",
3590
3886
  "claude sonnet 4.6": "claude-sonnet-4.6",
3591
3887
  "claude-sonnet-4.6": "claude-sonnet-4.6",
3592
3888
  "claude-sonnet-4-6": "claude-sonnet-4.6",
3593
3889
  "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"
3890
+ "anthropic.claude-sonnet-4-6": "claude-sonnet-4.6"
3596
3891
  },
3597
3892
  notes: {
3598
- "gpt-5.3-codex": "Temporary fallback to gpt-5.2-codex until upstream publishes direct gpt-5.3-codex token pricing"
3893
+ "gpt-5.3-codex-spark": "Alias to gpt-5.3-codex because upstream publishes token pricing on the gpt-5.3-codex key"
3599
3894
  },
3600
3895
  preferredPricingKeyByCanonicalModel: {
3601
3896
  "kimi-k2.5": "moonshot/kimi-k2.5",
3897
+ "gpt-5.3-codex": "gpt-5.3-codex",
3898
+ "gemini-3-flash": "gemini/gemini-3-flash-preview",
3899
+ "gemini-3-pro": "gemini/gemini-3-pro-preview",
3900
+ "minimax-m2.1": "openrouter/minimax/minimax-m2.1",
3901
+ "minimax-m2.5": "openrouter/minimax/minimax-m2.5",
3602
3902
  "claude-sonnet-4.6": "anthropic.claude-sonnet-4-6"
3603
3903
  }
3604
3904
  };
@@ -3747,7 +4047,7 @@ function normalizeLitellmPricingPayload(payload) {
3747
4047
  return normalizedPricing;
3748
4048
  }
3749
4049
  function getDefaultLiteLLMPricingCachePath() {
3750
- return path10.join(getUserCacheRootDir(), "llm-usage-metrics", "litellm-pricing-cache.json");
4050
+ return path11.join(getUserCacheRootDir(), "llm-usage-metrics", "litellm-pricing-cache.json");
3751
4051
  }
3752
4052
  function stripProviderPrefix(model) {
3753
4053
  const slashIndex = model.lastIndexOf("/");
@@ -4106,7 +4406,7 @@ var LiteLLMPricingFetcher = class {
4106
4406
  async readCachePayload() {
4107
4407
  let content;
4108
4408
  try {
4109
- content = await readFile3(this.cacheFilePath, "utf8");
4409
+ content = await readFile4(this.cacheFilePath, "utf8");
4110
4410
  } catch {
4111
4411
  return void 0;
4112
4412
  }
@@ -4141,7 +4441,7 @@ var LiteLLMPricingFetcher = class {
4141
4441
  };
4142
4442
  }
4143
4443
  async writeCache() {
4144
- const directoryPath = path10.dirname(this.cacheFilePath);
4444
+ const directoryPath = path11.dirname(this.cacheFilePath);
4145
4445
  await mkdir3(directoryPath, { recursive: true });
4146
4446
  const payload = {
4147
4447
  fetchedAt: this.now(),
@@ -4341,7 +4641,7 @@ function hasMeaningfulEfficiencyUsageSignal(event) {
4341
4641
  async function buildEfficiencyData(granularity, options, deps = {}) {
4342
4642
  const buildUsage = deps.buildUsageData ?? buildUsageData;
4343
4643
  const collectOutcomes = deps.collectGitOutcomes ?? collectGitOutcomes;
4344
- const resolveRepoRoot2 = deps.resolveRepoRoot ?? resolveRepoRootFromPathHint;
4644
+ const resolveRepoRoot3 = deps.resolveRepoRoot ?? resolveRepoRootFromPathHint;
4345
4645
  const repoDir = options.repoDir?.trim();
4346
4646
  if (options.repoDir !== void 0 && !repoDir) {
4347
4647
  throw new Error("--repo-dir must be a non-empty path");
@@ -4350,7 +4650,7 @@ async function buildEfficiencyData(granularity, options, deps = {}) {
4350
4650
  const attribution = await attributeUsageEventsToRepo(
4351
4651
  usageData.events,
4352
4652
  repoDir ?? process.cwd(),
4353
- resolveRepoRoot2
4653
+ resolveRepoRoot3
4354
4654
  );
4355
4655
  const matchedEventsWithSignal = attribution.matchedEvents.filter(
4356
4656
  (event) => hasMeaningfulEfficiencyUsageSignal(event)
@@ -5547,7 +5847,7 @@ function addSharedOptions(command, options = {}) {
5547
5847
  const allowedSourcesLabel = getAllowedSourcesLabel(supportedSourceIds);
5548
5848
  const supportedSourcesSummary = `(${supportedSourceIds.length}): ${allowedSourcesLabel}`;
5549
5849
  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(
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(
5551
5851
  "--source-dir <source-id=path>",
5552
5852
  "Override source directory for directory-backed sources (repeatable)",
5553
5853
  collectRepeatedOption,
@@ -5621,7 +5921,8 @@ function rootDescription() {
5621
5921
  " $ llm-usage monthly --since 2026-01-01 --until 2026-01-31 --source pi,codex --json",
5622
5922
  " $ llm-usage monthly --source opencode --opencode-db /path/to/opencode.db --json",
5623
5923
  " $ llm-usage monthly --model claude --per-model-columns",
5624
- " $ llm-usage daily --source-dir pi=/tmp/pi-sessions",
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",
5625
5926
  " $ llm-usage efficiency weekly --repo-dir /path/to/repo --json",
5626
5927
  " $ npx --yes llm-usage-metrics daily"
5627
5928
  ].join("\n");