llm-usage-metrics 0.3.4 → 0.3.6

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