llm-usage-metrics 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1094,11 +1094,197 @@ var CodexSourceAdapter = class {
1094
1094
  }
1095
1095
  };
1096
1096
 
1097
- // src/sources/gemini/gemini-source-adapter.ts
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
- var defaultGeminiDir = path5.join(os3.homedir(), ".gemini");
1101
+
1102
+ // src/sources/parse-diagnostics.ts
1103
+ function incrementSkippedReason(reasons, reason) {
1104
+ const current = reasons.get(reason) ?? 0;
1105
+ reasons.set(reason, current + 1);
1106
+ }
1107
+ function toSkippedRowReasonStats(reasons) {
1108
+ return [...reasons.entries()].map(([reason, count]) => ({ reason, count })).sort((left, right) => compareByCodePoint(left.reason, right.reason));
1109
+ }
1110
+ function toParseDiagnostics(events, skippedRows, skippedRowReasons) {
1111
+ return {
1112
+ events,
1113
+ skippedRows,
1114
+ skippedRowReasons: toSkippedRowReasonStats(skippedRowReasons)
1115
+ };
1116
+ }
1117
+
1118
+ // src/sources/droid/droid-source-adapter.ts
1119
+ var defaultSessionsDir2 = path5.join(os3.homedir(), ".factory", "sessions");
1120
+ var DROID_SESSION_START_LINE_PATTERN = /"type"\s*:\s*"session_start"/u;
1121
+ var DROID_MESSAGE_LINE_PATTERN = /"type"\s*:\s*"message"/u;
1122
+ function shouldParseDroidJsonlLine(lineText) {
1123
+ return DROID_SESSION_START_LINE_PATTERN.test(lineText) || DROID_MESSAGE_LINE_PATTERN.test(lineText);
1124
+ }
1125
+ var UNIX_SECONDS_ABS_CUTOFF = 1e10;
1126
+ function normalizeTimestampCandidate(candidate) {
1127
+ let date;
1128
+ if (typeof candidate === "number" && Number.isFinite(candidate)) {
1129
+ const timestampMs = Math.abs(candidate) <= UNIX_SECONDS_ABS_CUTOFF ? candidate * 1e3 : candidate;
1130
+ date = new Date(timestampMs);
1131
+ } else {
1132
+ const normalizedText = asTrimmedText(candidate);
1133
+ if (!normalizedText) {
1134
+ return void 0;
1135
+ }
1136
+ date = new Date(normalizedText);
1137
+ }
1138
+ if (Number.isNaN(date.getTime())) {
1139
+ return void 0;
1140
+ }
1141
+ return date.toISOString();
1142
+ }
1143
+ function getSettingsSessionId(filePath) {
1144
+ return path5.basename(filePath, ".settings.json");
1145
+ }
1146
+ function getSiblingJsonlPath(settingsPath) {
1147
+ return path5.join(path5.dirname(settingsPath), `${getSettingsSessionId(settingsPath)}.jsonl`);
1148
+ }
1149
+ function isSessionStartRecord(line) {
1150
+ return asTrimmedText(line.type) === "session_start";
1151
+ }
1152
+ function isMessageRecord(line) {
1153
+ return asTrimmedText(line.type) === "message";
1154
+ }
1155
+ function resolveRepoRootFromSessionStart(line) {
1156
+ const payload = asRecord(line.session_start);
1157
+ return asTrimmedText(payload?.cwd);
1158
+ }
1159
+ var DroidSourceAdapter = class {
1160
+ id = "droid";
1161
+ sessionsDir;
1162
+ requireSessionsDir;
1163
+ constructor(options = {}) {
1164
+ this.sessionsDir = options.sessionsDir ?? defaultSessionsDir2;
1165
+ this.requireSessionsDir = options.requireSessionsDir ?? false;
1166
+ }
1167
+ getNormalizedSessionsDir() {
1168
+ if (isBlankText(this.sessionsDir)) {
1169
+ throw new Error("Droid sessions directory must be a non-empty path");
1170
+ }
1171
+ return this.sessionsDir.trim();
1172
+ }
1173
+ async discoverFiles() {
1174
+ const normalizedDir = this.getNormalizedSessionsDir();
1175
+ if (this.requireSessionsDir && !await pathReadable(normalizedDir)) {
1176
+ throw new Error(`Droid sessions directory is missing or unreadable: ${normalizedDir}`);
1177
+ }
1178
+ if (this.requireSessionsDir && !await pathIsDirectory(normalizedDir)) {
1179
+ throw new Error(`Droid sessions directory is not a directory: ${normalizedDir}`);
1180
+ }
1181
+ return discoverFiles(normalizedDir, { extension: ".settings.json" });
1182
+ }
1183
+ async parseFile(filePath) {
1184
+ const { events } = await this.parseFileWithDiagnostics(filePath);
1185
+ return events;
1186
+ }
1187
+ async parseFileWithDiagnostics(filePath) {
1188
+ const events = [];
1189
+ let skippedRows = 0;
1190
+ const skippedRowReasons = /* @__PURE__ */ new Map();
1191
+ let settingsJson;
1192
+ try {
1193
+ const content = await readFile2(filePath, "utf8");
1194
+ settingsJson = JSON.parse(content);
1195
+ } catch {
1196
+ skippedRows++;
1197
+ incrementSkippedReason(skippedRowReasons, "json_parse_error");
1198
+ return toParseDiagnostics(events, skippedRows, skippedRowReasons);
1199
+ }
1200
+ const settings = asRecord(settingsJson);
1201
+ if (!settings) {
1202
+ skippedRows++;
1203
+ incrementSkippedReason(skippedRowReasons, "invalid_settings_data");
1204
+ return toParseDiagnostics(events, skippedRows, skippedRowReasons);
1205
+ }
1206
+ const tokenUsage = asRecord(settings.tokenUsage);
1207
+ if (!tokenUsage) {
1208
+ skippedRows++;
1209
+ incrementSkippedReason(skippedRowReasons, "no_token_usage");
1210
+ return toParseDiagnostics(events, skippedRows, skippedRowReasons);
1211
+ }
1212
+ const inputTokens = normalizeNonNegativeInteger(toNumberLike(tokenUsage.inputTokens));
1213
+ const outputTokens = normalizeNonNegativeInteger(toNumberLike(tokenUsage.outputTokens));
1214
+ const reasoningTokens = normalizeNonNegativeInteger(toNumberLike(tokenUsage.thinkingTokens));
1215
+ const cacheReadTokens = normalizeNonNegativeInteger(toNumberLike(tokenUsage.cacheReadTokens));
1216
+ const cacheWriteTokens = normalizeNonNegativeInteger(
1217
+ toNumberLike(tokenUsage.cacheCreationTokens)
1218
+ );
1219
+ const billableTokens = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
1220
+ const totalTokens = billableTokens + reasoningTokens;
1221
+ if (billableTokens === 0) {
1222
+ skippedRows++;
1223
+ incrementSkippedReason(skippedRowReasons, "no_token_usage");
1224
+ return toParseDiagnostics(events, skippedRows, skippedRowReasons);
1225
+ }
1226
+ const provider = asTrimmedText(settings.providerLock);
1227
+ const model = asTrimmedText(settings.model);
1228
+ const primaryTimestamp = normalizeTimestampCandidate(settings.providerLockTimestamp);
1229
+ const hasValidPrimaryTimestamp = Boolean(primaryTimestamp);
1230
+ const jsonlPath = getSiblingJsonlPath(filePath);
1231
+ let repoRoot;
1232
+ let fallbackMessageTimestamp;
1233
+ try {
1234
+ for await (const line of readJsonlObjects(jsonlPath, {
1235
+ shouldParseLine: shouldParseDroidJsonlLine
1236
+ })) {
1237
+ if (!repoRoot && isSessionStartRecord(line)) {
1238
+ repoRoot = resolveRepoRootFromSessionStart(line) ?? repoRoot;
1239
+ if (hasValidPrimaryTimestamp) {
1240
+ break;
1241
+ }
1242
+ continue;
1243
+ }
1244
+ if (!hasValidPrimaryTimestamp && isMessageRecord(line)) {
1245
+ fallbackMessageTimestamp = normalizeTimestampCandidate(line.timestamp);
1246
+ break;
1247
+ }
1248
+ }
1249
+ } catch {
1250
+ }
1251
+ const timestamp = primaryTimestamp ?? fallbackMessageTimestamp;
1252
+ if (!timestamp) {
1253
+ skippedRows++;
1254
+ incrementSkippedReason(skippedRowReasons, "invalid_timestamp");
1255
+ return toParseDiagnostics(events, skippedRows, skippedRowReasons);
1256
+ }
1257
+ try {
1258
+ events.push(
1259
+ createUsageEvent({
1260
+ source: this.id,
1261
+ sessionId: getSettingsSessionId(filePath),
1262
+ timestamp,
1263
+ repoRoot,
1264
+ provider,
1265
+ model,
1266
+ inputTokens,
1267
+ outputTokens,
1268
+ reasoningTokens,
1269
+ cacheReadTokens,
1270
+ cacheWriteTokens,
1271
+ totalTokens,
1272
+ costMode: "estimated"
1273
+ })
1274
+ );
1275
+ } catch {
1276
+ skippedRows++;
1277
+ incrementSkippedReason(skippedRowReasons, "event_creation_failed");
1278
+ }
1279
+ return toParseDiagnostics(events, skippedRows, skippedRowReasons);
1280
+ }
1281
+ };
1282
+
1283
+ // src/sources/gemini/gemini-source-adapter.ts
1284
+ import { readFile as readFile3 } from "fs/promises";
1285
+ import os4 from "os";
1286
+ import path6 from "path";
1287
+ var defaultGeminiDir = path6.join(os4.homedir(), ".gemini");
1102
1288
  function parseProjectsJson(data) {
1103
1289
  const mapping = /* @__PURE__ */ new Map();
1104
1290
  const record = asRecord(data);
@@ -1119,9 +1305,9 @@ function parseProjectsJson(data) {
1119
1305
  return mapping;
1120
1306
  }
1121
1307
  async function loadProjectsJson(geminiDir) {
1122
- const projectsPath = path5.join(geminiDir, "projects.json");
1308
+ const projectsPath = path6.join(geminiDir, "projects.json");
1123
1309
  try {
1124
- const content = await readFile2(projectsPath, "utf8");
1310
+ const content = await readFile3(projectsPath, "utf8");
1125
1311
  const parsed = JSON.parse(content);
1126
1312
  return parseProjectsJson(parsed);
1127
1313
  } catch {
@@ -1129,11 +1315,11 @@ async function loadProjectsJson(geminiDir) {
1129
1315
  }
1130
1316
  }
1131
1317
  async function discoverSessionFiles(geminiDir) {
1132
- const tmpDir = path5.join(geminiDir, "tmp");
1318
+ const tmpDir = path6.join(geminiDir, "tmp");
1133
1319
  const allSessionFiles = [];
1134
1320
  const discoveredFiles = await discoverFiles(tmpDir, { extension: ".json" });
1135
1321
  for (const filePath of discoveredFiles) {
1136
- const parentDir = path5.basename(path5.dirname(filePath));
1322
+ const parentDir = path6.basename(path6.dirname(filePath));
1137
1323
  if (parentDir.toLowerCase() === "chats") {
1138
1324
  allSessionFiles.push(filePath);
1139
1325
  }
@@ -1148,9 +1334,9 @@ function resolveRepoRoot(filePath, sessionData, projectMapping) {
1148
1334
  return mappedRoot;
1149
1335
  }
1150
1336
  }
1151
- const chatsDir = path5.dirname(filePath);
1152
- const projectDir = path5.dirname(chatsDir);
1153
- const projectIdentifier = path5.basename(projectDir);
1337
+ const chatsDir = path6.dirname(filePath);
1338
+ const projectDir = path6.dirname(chatsDir);
1339
+ const projectIdentifier = path6.basename(projectDir);
1154
1340
  return projectMapping.get(projectIdentifier);
1155
1341
  }
1156
1342
  function toFiniteNumber(value) {
@@ -1197,20 +1383,6 @@ function extractTokenUsage(tokens) {
1197
1383
  totalTokens
1198
1384
  };
1199
1385
  }
1200
- function incrementSkippedReason(reasons, reason) {
1201
- const current = reasons.get(reason) ?? 0;
1202
- reasons.set(reason, current + 1);
1203
- }
1204
- function toSkippedRowReasonStats(reasons) {
1205
- return [...reasons.entries()].map(([reason, count]) => ({ reason, count })).sort((left, right) => compareByCodePoint(left.reason, right.reason));
1206
- }
1207
- function toParseDiagnostics(events, skippedRows, skippedRowReasons) {
1208
- return {
1209
- events,
1210
- skippedRows,
1211
- skippedRowReasons: toSkippedRowReasonStats(skippedRowReasons)
1212
- };
1213
- }
1214
1386
  function normalizeTimestamp2(candidate) {
1215
1387
  if (typeof candidate !== "string" || isBlankText(candidate)) {
1216
1388
  return void 0;
@@ -1274,7 +1446,7 @@ var GeminiSourceAdapter = class {
1274
1446
  const skippedRowReasons = /* @__PURE__ */ new Map();
1275
1447
  let sessionData;
1276
1448
  try {
1277
- const content = await readFile2(filePath, "utf8");
1449
+ const content = await readFile3(filePath, "utf8");
1278
1450
  sessionData = JSON.parse(content);
1279
1451
  } catch {
1280
1452
  skippedRows++;
@@ -1287,7 +1459,7 @@ var GeminiSourceAdapter = class {
1287
1459
  incrementSkippedReason(skippedRowReasons, "invalid_session_data");
1288
1460
  return toParseDiagnostics(events, skippedRows, skippedRowReasons);
1289
1461
  }
1290
- const sessionId = asTrimmedText(sessionDataRecord.sessionId) ?? path5.basename(filePath, ".json");
1462
+ const sessionId = asTrimmedText(sessionDataRecord.sessionId) ?? path6.basename(filePath, ".json");
1291
1463
  const projectMapping = await this.getProjectMappingForParse();
1292
1464
  const repoRoot = resolveRepoRoot(filePath, sessionDataRecord, projectMapping);
1293
1465
  if (!Array.isArray(sessionDataRecord.messages)) {
@@ -1344,44 +1516,44 @@ var GeminiSourceAdapter = class {
1344
1516
  };
1345
1517
 
1346
1518
  // src/sources/opencode/opencode-db-path-resolver.ts
1347
- import os4 from "os";
1348
- import path6 from "path";
1519
+ import os5 from "os";
1520
+ import path7 from "path";
1349
1521
  function deduplicate(paths) {
1350
1522
  return [...new Set(paths)];
1351
1523
  }
1352
1524
  function getLinuxLikeCandidates(homeDir, env) {
1353
- const xdgDataHome = env.XDG_DATA_HOME ?? path6.join(homeDir, ".local", "share");
1525
+ const xdgDataHome = env.XDG_DATA_HOME ?? path7.join(homeDir, ".local", "share");
1354
1526
  return [
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")
1527
+ path7.join(xdgDataHome, "opencode", "opencode.db"),
1528
+ path7.join(xdgDataHome, "opencode", "db.sqlite"),
1529
+ path7.join(homeDir, ".opencode", "opencode.db"),
1530
+ path7.join(homeDir, ".opencode", "db.sqlite")
1359
1531
  ];
1360
1532
  }
1361
1533
  function getMacOsCandidates(homeDir) {
1362
- const appSupportDir = path6.join(homeDir, "Library", "Application Support");
1534
+ const appSupportDir = path7.join(homeDir, "Library", "Application Support");
1363
1535
  return [
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")
1536
+ path7.join(appSupportDir, "opencode", "opencode.db"),
1537
+ path7.join(appSupportDir, "opencode", "db.sqlite"),
1538
+ path7.join(homeDir, ".opencode", "opencode.db"),
1539
+ path7.join(homeDir, ".opencode", "db.sqlite")
1368
1540
  ];
1369
1541
  }
1370
1542
  function getWindowsCandidates(homeDir, env) {
1371
- const roamingBase = env.APPDATA ?? env.LOCALAPPDATA ?? (env.USERPROFILE ? path6.join(env.USERPROFILE, "AppData", "Roaming") : void 0);
1543
+ const roamingBase = env.APPDATA ?? env.LOCALAPPDATA ?? (env.USERPROFILE ? path7.join(env.USERPROFILE, "AppData", "Roaming") : void 0);
1372
1544
  const roamingCandidates = roamingBase ? [
1373
- path6.join(roamingBase, "opencode", "opencode.db"),
1374
- path6.join(roamingBase, "opencode", "db.sqlite")
1545
+ path7.join(roamingBase, "opencode", "opencode.db"),
1546
+ path7.join(roamingBase, "opencode", "db.sqlite")
1375
1547
  ] : [];
1376
1548
  return [
1377
1549
  ...roamingCandidates,
1378
- path6.join(homeDir, ".opencode", "opencode.db"),
1379
- path6.join(homeDir, ".opencode", "db.sqlite")
1550
+ path7.join(homeDir, ".opencode", "opencode.db"),
1551
+ path7.join(homeDir, ".opencode", "db.sqlite")
1380
1552
  ];
1381
1553
  }
1382
1554
  function getDefaultOpenCodeDbPathCandidates(options = {}) {
1383
1555
  const platform = options.platform ?? process.platform;
1384
- const homeDir = options.homeDir ?? os4.homedir();
1556
+ const homeDir = options.homeDir ?? os5.homedir();
1385
1557
  const env = options.env ?? process.env;
1386
1558
  switch (platform) {
1387
1559
  case "win32":
@@ -1447,10 +1619,10 @@ async function loadNodeSqliteModule() {
1447
1619
  }
1448
1620
 
1449
1621
  // src/sources/opencode/opencode-row-parser.ts
1450
- var UNIX_SECONDS_ABS_CUTOFF = 1e10;
1451
- function normalizeTimestampCandidate(candidate) {
1622
+ var UNIX_SECONDS_ABS_CUTOFF2 = 1e10;
1623
+ function normalizeTimestampCandidate2(candidate) {
1452
1624
  if (typeof candidate === "number" && Number.isFinite(candidate)) {
1453
- const timestampMs = Math.abs(candidate) <= UNIX_SECONDS_ABS_CUTOFF ? candidate * 1e3 : candidate;
1625
+ const timestampMs = Math.abs(candidate) <= UNIX_SECONDS_ABS_CUTOFF2 ? candidate * 1e3 : candidate;
1454
1626
  const date = new Date(timestampMs);
1455
1627
  if (Number.isNaN(date.getTime())) {
1456
1628
  return void 0;
@@ -1464,7 +1636,7 @@ function normalizeTimestampCandidate(candidate) {
1464
1636
  }
1465
1637
  const numericTimestamp = Number(trimmed);
1466
1638
  if (Number.isFinite(numericTimestamp)) {
1467
- return normalizeTimestampCandidate(numericTimestamp);
1639
+ return normalizeTimestampCandidate2(numericTimestamp);
1468
1640
  }
1469
1641
  const date = new Date(trimmed);
1470
1642
  if (Number.isNaN(date.getTime())) {
@@ -1482,7 +1654,7 @@ function resolveTimestamp(rowTimestamp, messagePayload) {
1482
1654
  messagePayload.time_created
1483
1655
  ];
1484
1656
  for (const candidate of timestampCandidates) {
1485
- const resolved = normalizeTimestampCandidate(candidate);
1657
+ const resolved = normalizeTimestampCandidate2(candidate);
1486
1658
  if (resolved) {
1487
1659
  return resolved;
1488
1660
  }
@@ -1888,9 +2060,9 @@ var OpenCodeSourceAdapter = class {
1888
2060
  };
1889
2061
 
1890
2062
  // src/sources/pi/pi-source-adapter.ts
1891
- import os5 from "os";
1892
- import path7 from "path";
1893
- var defaultSessionsDir2 = path7.join(os5.homedir(), ".pi", "agent", "sessions");
2063
+ import os6 from "os";
2064
+ import path8 from "path";
2065
+ var defaultSessionsDir3 = path8.join(os6.homedir(), ".pi", "agent", "sessions");
1894
2066
  var PI_MESSAGE_LINE_PATTERN = /"type"\s*:\s*"message"/u;
1895
2067
  var PI_SESSION_LINE_PATTERN = /"type"\s*:\s*"session"/u;
1896
2068
  var PI_MODEL_CHANGE_LINE_PATTERN = /"type"\s*:\s*"model_change"/u;
@@ -1898,11 +2070,11 @@ function shouldParsePiJsonlLine(lineText) {
1898
2070
  return PI_MESSAGE_LINE_PATTERN.test(lineText) || PI_SESSION_LINE_PATTERN.test(lineText) || PI_MODEL_CHANGE_LINE_PATTERN.test(lineText);
1899
2071
  }
1900
2072
  var allowAllProviders = () => true;
1901
- var UNIX_SECONDS_ABS_CUTOFF2 = 1e10;
1902
- function normalizeTimestampCandidate2(candidate) {
2073
+ var UNIX_SECONDS_ABS_CUTOFF3 = 1e10;
2074
+ function normalizeTimestampCandidate3(candidate) {
1903
2075
  let date;
1904
2076
  if (typeof candidate === "number" && Number.isFinite(candidate)) {
1905
- const timestampMs = Math.abs(candidate) <= UNIX_SECONDS_ABS_CUTOFF2 ? candidate * 1e3 : candidate;
2077
+ const timestampMs = Math.abs(candidate) <= UNIX_SECONDS_ABS_CUTOFF3 ? candidate * 1e3 : candidate;
1906
2078
  date = new Date(timestampMs);
1907
2079
  } else {
1908
2080
  const normalizedText = asTrimmedText(candidate);
@@ -1919,7 +2091,7 @@ function normalizeTimestampCandidate2(candidate) {
1919
2091
  function resolveTimestamp2(line, message, state) {
1920
2092
  const candidates = [line.timestamp, message?.timestamp, state.sessionTimestamp];
1921
2093
  for (const candidate of candidates) {
1922
- const normalizedTimestamp = normalizeTimestampCandidate2(candidate);
2094
+ const normalizedTimestamp = normalizeTimestampCandidate3(candidate);
1923
2095
  if (normalizedTimestamp) {
1924
2096
  return normalizedTimestamp;
1925
2097
  }
@@ -1983,7 +2155,7 @@ function extractUsage(line, message) {
1983
2155
  return extractUsageFromRecord(messageUsage);
1984
2156
  }
1985
2157
  function getFallbackSessionId2(filePath) {
1986
- return path7.basename(filePath, ".jsonl");
2158
+ return path8.basename(filePath, ".jsonl");
1987
2159
  }
1988
2160
  function resolveRepoRootFromRecord(record) {
1989
2161
  if (!record) {
@@ -1998,7 +2170,7 @@ var PiSourceAdapter = class {
1998
2170
  providerFilter;
1999
2171
  requireSessionsDir;
2000
2172
  constructor(options = {}) {
2001
- this.sessionsDir = options.sessionsDir ?? defaultSessionsDir2;
2173
+ this.sessionsDir = options.sessionsDir ?? defaultSessionsDir3;
2002
2174
  this.providerFilter = options.providerFilter ?? allowAllProviders;
2003
2175
  this.requireSessionsDir = options.requireSessionsDir ?? false;
2004
2176
  }
@@ -2136,6 +2308,21 @@ var sourceRegistrations = [
2136
2308
  });
2137
2309
  }
2138
2310
  },
2311
+ {
2312
+ id: "droid",
2313
+ sourceDirOverride: { kind: "directory" },
2314
+ create: (options, sourceDirectoryOverrides) => {
2315
+ const directoryConfig = resolveDirectoryConfig(
2316
+ "droid",
2317
+ options.droidDir,
2318
+ sourceDirectoryOverrides
2319
+ );
2320
+ return new DroidSourceAdapter({
2321
+ sessionsDir: directoryConfig.path,
2322
+ requireSessionsDir: directoryConfig.requireExistingPath
2323
+ });
2324
+ }
2325
+ },
2139
2326
  {
2140
2327
  id: "opencode",
2141
2328
  sourceDirOverride: { kind: "unsupported", flag: "--opencode-db" },
@@ -2217,6 +2404,7 @@ function createDefaultAdapters(options) {
2217
2404
  validateDirectoryOverride("--pi-dir", options.piDir);
2218
2405
  validateDirectoryOverride("--codex-dir", options.codexDir);
2219
2406
  validateDirectoryOverride("--gemini-dir", options.geminiDir);
2407
+ validateDirectoryOverride("--droid-dir", options.droidDir);
2220
2408
  const sourceDirectoryOverrides = parseSourceDirectoryOverrides(options.sourceDir);
2221
2409
  validateSourceDirectoryOverrideIds(sourceDirectoryOverrides);
2222
2410
  return sourceRegistrations.map((source) => source.create(options, sourceDirectoryOverrides));
@@ -2592,7 +2780,7 @@ function aggregateEfficiency(options) {
2592
2780
  // src/efficiency/git-outcome-collector.ts
2593
2781
  import { spawn as spawn2 } from "child_process";
2594
2782
  import { createInterface as createInterface3 } from "readline";
2595
- import path8 from "path";
2783
+ import path9 from "path";
2596
2784
  import { stat as stat2 } from "fs/promises";
2597
2785
  var GIT_COMMIT_MARKER = "";
2598
2786
  var SHORTSTAT_PATTERN = /(\d+)\s+files?\s+changed(?:,\s+(\d+)\s+insertions?\(\+\))?(?:,\s+(\d+)\s+deletions?\(-\))?/u;
@@ -2612,13 +2800,13 @@ function resolveGitCommandFailureReason(result) {
2612
2800
  }
2613
2801
  function resolveRepoDir(repoDir) {
2614
2802
  if (repoDir === void 0) {
2615
- return path8.resolve(process.cwd());
2803
+ return path9.resolve(process.cwd());
2616
2804
  }
2617
2805
  const normalizedRepoDir = repoDir.trim();
2618
2806
  if (!normalizedRepoDir) {
2619
2807
  throw new Error("--repo-dir must be a non-empty path");
2620
2808
  }
2621
- return path8.resolve(normalizedRepoDir);
2809
+ return path9.resolve(normalizedRepoDir);
2622
2810
  }
2623
2811
  function getNodeErrorCode2(error) {
2624
2812
  if (typeof error !== "object" || error === null || !("code" in error)) {
@@ -2990,21 +3178,21 @@ async function collectGitOutcomes(options, deps = {}) {
2990
3178
 
2991
3179
  // src/efficiency/repo-attribution.ts
2992
3180
  import { access as access2, constants as constants2, realpath } from "fs/promises";
2993
- import path9 from "path";
3181
+ import path10 from "path";
2994
3182
  async function hasGitMarker(directoryPath) {
2995
3183
  try {
2996
- await access2(path9.join(directoryPath, ".git"), constants2.F_OK);
3184
+ await access2(path10.join(directoryPath, ".git"), constants2.F_OK);
2997
3185
  return true;
2998
3186
  } catch {
2999
3187
  return false;
3000
3188
  }
3001
3189
  }
3002
3190
  function normalizeComparablePath(value) {
3003
- const normalizedPath = path9.normalize(path9.resolve(value));
3191
+ const normalizedPath = path10.normalize(path10.resolve(value));
3004
3192
  return process.platform === "win32" ? normalizedPath.toLowerCase() : normalizedPath;
3005
3193
  }
3006
3194
  async function resolveComparablePath(value) {
3007
- const resolvedPath = path9.resolve(value);
3195
+ const resolvedPath = path10.resolve(value);
3008
3196
  try {
3009
3197
  return normalizeComparablePath(await realpath(resolvedPath));
3010
3198
  } catch {
@@ -3016,12 +3204,12 @@ async function resolveRepoRootFromPathHint(pathHint) {
3016
3204
  if (!trimmedPath) {
3017
3205
  return void 0;
3018
3206
  }
3019
- let currentPath = path9.resolve(trimmedPath);
3207
+ let currentPath = path10.resolve(trimmedPath);
3020
3208
  for (; ; ) {
3021
3209
  if (await hasGitMarker(currentPath)) {
3022
3210
  return currentPath;
3023
3211
  }
3024
- const parentPath = path9.dirname(currentPath);
3212
+ const parentPath = path10.dirname(currentPath);
3025
3213
  if (parentPath === currentPath) {
3026
3214
  return void 0;
3027
3215
  }
@@ -3275,6 +3463,12 @@ function resolveExplicitSourceIds(options, sourceFilter) {
3275
3463
  if (options.codexDir) {
3276
3464
  explicitSourceIds.add("codex");
3277
3465
  }
3466
+ if (options.geminiDir) {
3467
+ explicitSourceIds.add("gemini");
3468
+ }
3469
+ if (options.droidDir) {
3470
+ explicitSourceIds.add("droid");
3471
+ }
3278
3472
  if (options.opencodeDb) {
3279
3473
  explicitSourceIds.add("opencode");
3280
3474
  }
@@ -3342,8 +3536,8 @@ function normalizeSkippedRowReasons(value) {
3342
3536
  }
3343
3537
 
3344
3538
  // src/cli/parse-file-cache.ts
3345
- import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
3346
- import path10 from "path";
3539
+ import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
3540
+ import path11 from "path";
3347
3541
  var PARSE_FILE_CACHE_VERSION = 2;
3348
3542
  var CACHE_KEY_SEPARATOR = "\0";
3349
3543
  function createCacheKey(source, filePath) {
@@ -3482,7 +3676,7 @@ function normalizeCacheEntry(value) {
3482
3676
  };
3483
3677
  }
3484
3678
  function getDefaultParseFileCachePath() {
3485
- return path10.join(getUserCacheRootDir(), "llm-usage-metrics", "parse-file-cache.json");
3679
+ return path11.join(getUserCacheRootDir(), "llm-usage-metrics", "parse-file-cache.json");
3486
3680
  }
3487
3681
  var ParseFileCache = class _ParseFileCache {
3488
3682
  constructor(cacheFilePath, limits, now) {
@@ -3568,7 +3762,7 @@ var ParseFileCache = class _ParseFileCache {
3568
3762
  keptEntries.length = bestCount;
3569
3763
  payloadText = bestPayloadText;
3570
3764
  }
3571
- await mkdir2(path10.dirname(this.cacheFilePath), { recursive: true });
3765
+ await mkdir2(path11.dirname(this.cacheFilePath), { recursive: true });
3572
3766
  await writeFile2(this.cacheFilePath, payloadText, "utf8");
3573
3767
  this.dirty = false;
3574
3768
  }
@@ -3591,7 +3785,7 @@ var ParseFileCache = class _ParseFileCache {
3591
3785
  async loadFromDisk() {
3592
3786
  let content;
3593
3787
  try {
3594
- content = await readFile3(this.cacheFilePath, "utf8");
3788
+ content = await readFile4(this.cacheFilePath, "utf8");
3595
3789
  } catch {
3596
3790
  return;
3597
3791
  }
@@ -3862,8 +4056,8 @@ function applyPricingToEvents(events, pricingSource) {
3862
4056
  }
3863
4057
 
3864
4058
  // src/pricing/litellm-pricing-fetcher.ts
3865
- import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
3866
- import path11 from "path";
4059
+ import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
4060
+ import path12 from "path";
3867
4061
 
3868
4062
  // src/pricing/litellm-model-map.json
3869
4063
  var litellm_model_map_default = {
@@ -4047,7 +4241,7 @@ function normalizeLitellmPricingPayload(payload) {
4047
4241
  return normalizedPricing;
4048
4242
  }
4049
4243
  function getDefaultLiteLLMPricingCachePath() {
4050
- return path11.join(getUserCacheRootDir(), "llm-usage-metrics", "litellm-pricing-cache.json");
4244
+ return path12.join(getUserCacheRootDir(), "llm-usage-metrics", "litellm-pricing-cache.json");
4051
4245
  }
4052
4246
  function stripProviderPrefix(model) {
4053
4247
  const slashIndex = model.lastIndexOf("/");
@@ -4406,7 +4600,7 @@ var LiteLLMPricingFetcher = class {
4406
4600
  async readCachePayload() {
4407
4601
  let content;
4408
4602
  try {
4409
- content = await readFile4(this.cacheFilePath, "utf8");
4603
+ content = await readFile5(this.cacheFilePath, "utf8");
4410
4604
  } catch {
4411
4605
  return void 0;
4412
4606
  }
@@ -4441,7 +4635,7 @@ var LiteLLMPricingFetcher = class {
4441
4635
  };
4442
4636
  }
4443
4637
  async writeCache() {
4444
- const directoryPath = path11.dirname(this.cacheFilePath);
4638
+ const directoryPath = path12.dirname(this.cacheFilePath);
4445
4639
  await mkdir3(directoryPath, { recursive: true });
4446
4640
  const payload = {
4447
4641
  fetchedAt: this.now(),
@@ -4615,6 +4809,12 @@ function resolveScopeNote(options) {
4615
4809
  if (hasActiveTextOption(options.codexDir)) {
4616
4810
  activeFilters.push("--codex-dir");
4617
4811
  }
4812
+ if (hasActiveTextOption(options.geminiDir)) {
4813
+ activeFilters.push("--gemini-dir");
4814
+ }
4815
+ if (hasActiveTextOption(options.droidDir)) {
4816
+ activeFilters.push("--droid-dir");
4817
+ }
4618
4818
  if (hasActiveTextOption(options.opencodeDb)) {
4619
4819
  activeFilters.push("--opencode-db");
4620
4820
  }
@@ -5847,7 +6047,7 @@ function addSharedOptions(command, options = {}) {
5847
6047
  const allowedSourcesLabel = getAllowedSourcesLabel(supportedSourceIds);
5848
6048
  const supportedSourcesSummary = `(${supportedSourceIds.length}): ${allowedSourcesLabel}`;
5849
6049
  const includePerModelColumns = options.includePerModelColumns ?? true;
5850
- const configuredCommand = command.option("--pi-dir <path>", "Path to .pi sessions directory").option("--codex-dir <path>", "Path to .codex sessions directory").option("--gemini-dir <path>", "Path to .gemini directory").option("--opencode-db <path>", "Path to OpenCode SQLite DB").option(
6050
+ const configuredCommand = command.option("--pi-dir <path>", "Path to .pi sessions directory").option("--codex-dir <path>", "Path to .codex sessions directory").option("--gemini-dir <path>", "Path to .gemini directory").option("--droid-dir <path>", "Path to Droid sessions directory").option("--opencode-db <path>", "Path to OpenCode SQLite DB").option(
5851
6051
  "--source-dir <source-id=path>",
5852
6052
  "Override source directory for directory-backed sources (repeatable)",
5853
6053
  collectRepeatedOption,
@@ -5921,10 +6121,10 @@ function rootDescription() {
5921
6121
  " $ llm-usage monthly --since 2026-01-01 --until 2026-01-31 --source pi,codex --json",
5922
6122
  " $ llm-usage monthly --source opencode --opencode-db /path/to/opencode.db --json",
5923
6123
  " $ llm-usage monthly --model claude --per-model-columns",
5924
- " $ llm-usage daily --source-dir pi=/tmp/pi-sessions --source-dir gemini=/tmp/.gemini",
5925
- " $ llm-usage daily --pi-dir /tmp/pi-sessions --gemini-dir /tmp/.gemini",
6124
+ " $ llm-usage daily --source-dir pi=/tmp/pi-sessions --source-dir gemini=/tmp/.gemini --source-dir droid=/tmp/droid-sessions",
6125
+ " $ llm-usage daily --pi-dir /tmp/pi-sessions --gemini-dir /tmp/.gemini --droid-dir /tmp/droid-sessions",
5926
6126
  " $ llm-usage efficiency weekly --repo-dir /path/to/repo --json",
5927
- " $ npx --yes llm-usage-metrics daily"
6127
+ " $ npx --yes llm-usage-metrics@latest daily"
5928
6128
  ].join("\n");
5929
6129
  }
5930
6130
  function createCli(options = {}) {