llm-usage-metrics 0.3.2 → 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
@@ -138,6 +138,33 @@ function getUserCacheRootDir(env = process.env, platform = process.platform, hom
138
138
  return path.join(homedir, ".cache");
139
139
  }
140
140
 
141
+ // src/utils/compare-by-code-point.ts
142
+ function compareByCodePoint(left, right) {
143
+ if (left === right) {
144
+ return 0;
145
+ }
146
+ const leftIterator = left[Symbol.iterator]();
147
+ const rightIterator = right[Symbol.iterator]();
148
+ for (; ; ) {
149
+ const leftStep = leftIterator.next();
150
+ const rightStep = rightIterator.next();
151
+ if (leftStep.done && rightStep.done) {
152
+ return 0;
153
+ }
154
+ if (leftStep.done) {
155
+ return -1;
156
+ }
157
+ if (rightStep.done) {
158
+ return 1;
159
+ }
160
+ const leftCodePoint = leftStep.value.codePointAt(0) ?? 0;
161
+ const rightCodePoint = rightStep.value.codePointAt(0) ?? 0;
162
+ if (leftCodePoint !== rightCodePoint) {
163
+ return leftCodePoint < rightCodePoint ? -1 : 1;
164
+ }
165
+ }
166
+ }
167
+
141
168
  // src/update/version-utils.ts
142
169
  function parseVersion(value) {
143
170
  const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(
@@ -175,7 +202,7 @@ function comparePrereleaseIdentifiers(left, right) {
175
202
  if (!leftIsNumeric && rightIsNumeric) {
176
203
  return 1;
177
204
  }
178
- return left.localeCompare(right);
205
+ return compareByCodePoint(left, right);
179
206
  }
180
207
  function isPrerelease(version) {
181
208
  const parsed = parseVersion(version);
@@ -427,7 +454,10 @@ async function resolveLatestVersion(options) {
427
454
  import { spawn } from "child_process";
428
455
  import { createInterface } from "readline/promises";
429
456
  function isInteractiveSession(options) {
430
- return options.stdinIsTTY && options.stdoutIsTTY && !options.env.CI;
457
+ const ciValue = options.env.CI;
458
+ const normalizedCiValue = ciValue?.trim().toLowerCase();
459
+ const ciEnabled = normalizedCiValue !== void 0 && normalizedCiValue.length > 0 && !["0", "false", "no", "off"].includes(normalizedCiValue);
460
+ return options.stdinIsTTY && options.stdoutIsTTY && !ciEnabled;
431
461
  }
432
462
  async function defaultConfirmInstall(prompt) {
433
463
  const readline = createInterface({
@@ -496,9 +526,19 @@ async function runInteractiveInstallAndRestart(options) {
496
526
 
497
527
  // src/update/update-notifier.ts
498
528
  var UPDATE_CHECK_SKIP_ENV_VAR = "LLM_USAGE_SKIP_UPDATE_CHECK";
529
+ function isTruthyEnvFlag(value) {
530
+ if (value === void 0) {
531
+ return false;
532
+ }
533
+ const normalizedValue = value.trim().toLowerCase();
534
+ if (normalizedValue.length === 0) {
535
+ return false;
536
+ }
537
+ return ["1", "true", "yes", "on"].includes(normalizedValue);
538
+ }
499
539
  function shouldSkipUpdateCheckForArgv(argv) {
500
540
  const executableArgs = argv.slice(2);
501
- const commandNames = /* @__PURE__ */ new Set(["daily", "weekly", "monthly", "help", "version"]);
541
+ const commandNames = /* @__PURE__ */ new Set(["daily", "weekly", "monthly", "efficiency", "help", "version"]);
502
542
  if (executableArgs.length === 0) {
503
543
  return false;
504
544
  }
@@ -518,7 +558,7 @@ function isLikelyNpxExecution(argv, env) {
518
558
  return true;
519
559
  }
520
560
  const npmCommand = env.npm_command ?? "";
521
- return npmCommand === "exec";
561
+ return npmCommand === "exec" || npmCommand === "npx";
522
562
  }
523
563
  function isLikelySourceExecution(argv) {
524
564
  const executablePath = argv[1] ?? "";
@@ -539,7 +579,7 @@ function toResolveLatestVersionOptions(options, env) {
539
579
  async function checkForUpdatesAndMaybeRestart(options) {
540
580
  const env = options.env ?? process.env;
541
581
  const argv = options.argv ?? process.argv;
542
- if (env[UPDATE_CHECK_SKIP_ENV_VAR] === "1") {
582
+ if (isTruthyEnvFlag(env[UPDATE_CHECK_SKIP_ENV_VAR])) {
543
583
  return { continueExecution: true };
544
584
  }
545
585
  if (shouldSkipUpdateCheckForArgv(argv)) {
@@ -588,33 +628,6 @@ import { Command } from "commander";
588
628
  import os2 from "os";
589
629
  import path4 from "path";
590
630
 
591
- // src/utils/compare-by-code-point.ts
592
- function compareByCodePoint(left, right) {
593
- if (left === right) {
594
- return 0;
595
- }
596
- const leftIterator = left[Symbol.iterator]();
597
- const rightIterator = right[Symbol.iterator]();
598
- for (; ; ) {
599
- const leftStep = leftIterator.next();
600
- const rightStep = rightIterator.next();
601
- if (leftStep.done && rightStep.done) {
602
- return 0;
603
- }
604
- if (leftStep.done) {
605
- return -1;
606
- }
607
- if (rightStep.done) {
608
- return 1;
609
- }
610
- const leftCodePoint = leftStep.value.codePointAt(0) ?? 0;
611
- const rightCodePoint = rightStep.value.codePointAt(0) ?? 0;
612
- if (leftCodePoint !== rightCodePoint) {
613
- return leftCodePoint < rightCodePoint ? -1 : 1;
614
- }
615
- }
616
- }
617
-
618
631
  // src/domain/normalization.ts
619
632
  function normalizeNonNegativeInteger(value) {
620
633
  if (value === null || value === void 0) {
@@ -683,6 +696,9 @@ function normalizeOptionalText(value) {
683
696
  const normalized = value.trim();
684
697
  return normalized || void 0;
685
698
  }
699
+ function normalizeOptionalPath(value) {
700
+ return normalizeOptionalText(value);
701
+ }
686
702
  function normalizeOptionalModel(value) {
687
703
  const normalized = normalizeOptionalText(value);
688
704
  if (!normalized) {
@@ -718,6 +734,7 @@ function createUsageEvent(input) {
718
734
  source,
719
735
  sessionId: requireText(input.sessionId, "sessionId"),
720
736
  timestamp: normalizeTimestamp(input.timestamp),
737
+ repoRoot: normalizeOptionalPath(input.repoRoot),
721
738
  provider: normalizeOptionalText(input.provider),
722
739
  model: normalizeOptionalModel(input.model),
723
740
  inputTokens,
@@ -731,7 +748,7 @@ function createUsageEvent(input) {
731
748
  };
732
749
  }
733
750
 
734
- // src/utils/discover-jsonl-files.ts
751
+ // src/utils/discover-files.ts
735
752
  import { readdir } from "fs/promises";
736
753
  import path3 from "path";
737
754
  function getNodeErrorCode(error) {
@@ -742,39 +759,103 @@ function isSkippableDirectoryReadError(error) {
742
759
  const code = getNodeErrorCode(error);
743
760
  return code === "EACCES" || code === "EPERM";
744
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
+ }
745
777
  async function walkDirectory(rootDir, acc, options) {
746
778
  let entries;
747
779
  try {
748
780
  entries = await readdir(rootDir, { withFileTypes: true, encoding: "utf8" });
749
781
  } catch (error) {
782
+ if (getNodeErrorCode(error) === "ENOENT") {
783
+ return;
784
+ }
750
785
  if (options.allowPermissionSkip && isSkippableDirectoryReadError(error)) {
751
786
  return;
752
787
  }
753
788
  throw error;
754
789
  }
755
- entries.sort((left, right) => left.name.localeCompare(right.name));
790
+ if (options.sort) {
791
+ entries.sort((left, right) => compareByCodePoint(left.name, right.name));
792
+ }
756
793
  for (const entry of entries) {
757
794
  const entryPath = path3.join(rootDir, entry.name);
758
- if (entry.isDirectory()) {
759
- await walkDirectory(entryPath, acc, { allowPermissionSkip: true });
795
+ if (entry.isDirectory() && options.recursive) {
796
+ await walkDirectory(entryPath, acc, options);
760
797
  continue;
761
798
  }
762
- if (entry.isFile() && entry.name.endsWith(".jsonl")) {
799
+ if (entry.isFile() && matchesExtension(entry.name, options.extension)) {
763
800
  acc.push(entryPath);
764
801
  }
765
802
  }
766
803
  }
767
- async function discoverJsonlFiles(rootDir) {
804
+ async function discoverFiles(rootDir, options) {
768
805
  const files = [];
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);
813
+ return files;
814
+ }
815
+
816
+ // src/utils/discover-jsonl-files.ts
817
+ async function discoverJsonlFiles(rootDir) {
818
+ return discoverFiles(rootDir, { extension: ".jsonl" });
819
+ }
820
+
821
+ // src/utils/fs-helpers.ts
822
+ import { access, constants, stat } from "fs/promises";
823
+ async function pathExists(filePath) {
769
824
  try {
770
- await walkDirectory(rootDir, files, { allowPermissionSkip: false });
771
- } catch (error) {
772
- if (getNodeErrorCode(error) === "ENOENT") {
773
- return [];
774
- }
775
- throw error;
825
+ await access(filePath, constants.F_OK);
826
+ return true;
827
+ } catch {
828
+ return false;
829
+ }
830
+ }
831
+ async function pathReadable(filePath) {
832
+ try {
833
+ await access(filePath, constants.R_OK);
834
+ return true;
835
+ } catch {
836
+ return false;
837
+ }
838
+ }
839
+ async function pathIsDirectory(filePath) {
840
+ try {
841
+ return (await stat(filePath)).isDirectory();
842
+ } catch {
843
+ return false;
844
+ }
845
+ }
846
+ async function pathIsFile(filePath) {
847
+ try {
848
+ return (await stat(filePath)).isFile();
849
+ } catch {
850
+ return false;
851
+ }
852
+ }
853
+ async function pathStat(filePath) {
854
+ try {
855
+ return await stat(filePath);
856
+ } catch {
857
+ return void 0;
776
858
  }
777
- return files;
778
859
  }
779
860
 
780
861
  // src/utils/read-jsonl-objects.ts
@@ -826,6 +907,9 @@ function asTrimmedText(value) {
826
907
  const normalized = value.trim();
827
908
  return normalized || void 0;
828
909
  }
910
+ function isBlankText(value) {
911
+ return value.trim().length === 0;
912
+ }
829
913
  function toNumberLike(value) {
830
914
  if (value === null || value === void 0 || typeof value === "number" || typeof value === "string") {
831
915
  return value;
@@ -902,14 +986,37 @@ function deriveDeltaUsage(info, previousTotalUsage) {
902
986
  function getFallbackSessionId(filePath) {
903
987
  return path4.basename(filePath, ".jsonl");
904
988
  }
989
+ function resolveRepoRootFromPayload(payload) {
990
+ if (!payload) {
991
+ return void 0;
992
+ }
993
+ return asTrimmedText(payload.cwd) ?? asTrimmedText(payload.repo_root) ?? asTrimmedText(payload.repoRoot) ?? asTrimmedText(payload.project_root) ?? asTrimmedText(payload.projectRoot);
994
+ }
905
995
  var CodexSourceAdapter = class {
906
996
  id = "codex";
907
997
  sessionsDir;
998
+ requireSessionsDir;
908
999
  constructor(options = {}) {
909
1000
  this.sessionsDir = options.sessionsDir ?? defaultSessionsDir;
1001
+ this.requireSessionsDir = options.requireSessionsDir ?? false;
910
1002
  }
911
1003
  async discoverFiles() {
912
- return discoverJsonlFiles(this.sessionsDir);
1004
+ if (isBlankText(this.sessionsDir)) {
1005
+ throw new Error("Codex sessions directory must be a non-empty path");
1006
+ }
1007
+ const normalizedSessionsDir = this.sessionsDir.trim();
1008
+ if (this.requireSessionsDir) {
1009
+ const sessionsDirStats = await pathStat(normalizedSessionsDir);
1010
+ if (!sessionsDirStats) {
1011
+ throw new Error(
1012
+ `Codex sessions directory is missing or unreadable: ${normalizedSessionsDir}`
1013
+ );
1014
+ }
1015
+ if (!sessionsDirStats.isDirectory()) {
1016
+ throw new Error(`Codex sessions directory is not a directory: ${normalizedSessionsDir}`);
1017
+ }
1018
+ }
1019
+ return discoverJsonlFiles(normalizedSessionsDir);
913
1020
  }
914
1021
  async parseFile(filePath) {
915
1022
  const events = [];
@@ -924,11 +1031,13 @@ var CodexSourceAdapter = class {
924
1031
  const payload2 = asRecord(line.payload);
925
1032
  state.sessionId = asTrimmedText(payload2?.id) ?? state.sessionId;
926
1033
  state.provider = asTrimmedText(payload2?.model_provider) ?? state.provider;
1034
+ state.repoRoot = resolveRepoRootFromPayload(payload2) ?? state.repoRoot;
927
1035
  continue;
928
1036
  }
929
1037
  if (line.type === "turn_context") {
930
1038
  const payload2 = asRecord(line.payload);
931
1039
  state.model = asTrimmedText(payload2?.model) ?? state.model;
1040
+ state.repoRoot = resolveRepoRootFromPayload(payload2) ?? state.repoRoot;
932
1041
  continue;
933
1042
  }
934
1043
  if (line.type !== "event_msg") {
@@ -959,6 +1068,7 @@ var CodexSourceAdapter = class {
959
1068
  source: this.id,
960
1069
  sessionId: state.sessionId,
961
1070
  timestamp,
1071
+ repoRoot: state.repoRoot,
962
1072
  provider: state.provider,
963
1073
  model,
964
1074
  inputTokens: deltaUsage.inputTokens,
@@ -984,48 +1094,294 @@ var CodexSourceAdapter = class {
984
1094
  }
985
1095
  };
986
1096
 
987
- // src/sources/opencode/opencode-source-adapter.ts
988
- import { access, constants } from "fs/promises";
989
-
990
- // src/sources/opencode/opencode-db-path-resolver.ts
1097
+ // src/sources/gemini/gemini-source-adapter.ts
1098
+ import { readFile as readFile2 } from "fs/promises";
991
1099
  import os3 from "os";
992
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";
993
1349
  function deduplicate(paths) {
994
1350
  return [...new Set(paths)];
995
1351
  }
996
1352
  function getLinuxLikeCandidates(homeDir, env) {
997
- const xdgDataHome = env.XDG_DATA_HOME ?? path5.join(homeDir, ".local", "share");
1353
+ const xdgDataHome = env.XDG_DATA_HOME ?? path6.join(homeDir, ".local", "share");
998
1354
  return [
999
- path5.join(xdgDataHome, "opencode", "opencode.db"),
1000
- path5.join(xdgDataHome, "opencode", "db.sqlite"),
1001
- path5.join(homeDir, ".opencode", "opencode.db"),
1002
- 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")
1003
1359
  ];
1004
1360
  }
1005
1361
  function getMacOsCandidates(homeDir) {
1006
- const appSupportDir = path5.join(homeDir, "Library", "Application Support");
1362
+ const appSupportDir = path6.join(homeDir, "Library", "Application Support");
1007
1363
  return [
1008
- path5.join(appSupportDir, "opencode", "opencode.db"),
1009
- path5.join(appSupportDir, "opencode", "db.sqlite"),
1010
- path5.join(homeDir, ".opencode", "opencode.db"),
1011
- 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")
1012
1368
  ];
1013
1369
  }
1014
1370
  function getWindowsCandidates(homeDir, env) {
1015
- 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);
1016
1372
  const roamingCandidates = roamingBase ? [
1017
- path5.join(roamingBase, "opencode", "opencode.db"),
1018
- path5.join(roamingBase, "opencode", "db.sqlite")
1373
+ path6.join(roamingBase, "opencode", "opencode.db"),
1374
+ path6.join(roamingBase, "opencode", "db.sqlite")
1019
1375
  ] : [];
1020
1376
  return [
1021
1377
  ...roamingCandidates,
1022
- path5.join(homeDir, ".opencode", "opencode.db"),
1023
- path5.join(homeDir, ".opencode", "db.sqlite")
1378
+ path6.join(homeDir, ".opencode", "opencode.db"),
1379
+ path6.join(homeDir, ".opencode", "db.sqlite")
1024
1380
  ];
1025
1381
  }
1026
1382
  function getDefaultOpenCodeDbPathCandidates(options = {}) {
1027
1383
  const platform = options.platform ?? process.platform;
1028
- const homeDir = options.homeDir ?? os3.homedir();
1384
+ const homeDir = options.homeDir ?? os4.homedir();
1029
1385
  const env = options.env ?? process.env;
1030
1386
  switch (platform) {
1031
1387
  case "win32":
@@ -1153,6 +1509,10 @@ function normalizeSessionIdCandidate(value) {
1153
1509
  }
1154
1510
  return asTrimmedText(value);
1155
1511
  }
1512
+ function resolveRepoRoot2(messagePayload) {
1513
+ const pathPayload = asRecord(messagePayload.path);
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);
1515
+ }
1156
1516
  function hasUsageSignal2(usageFields, explicitCost) {
1157
1517
  if (explicitCost !== void 0) {
1158
1518
  return true;
@@ -1204,6 +1564,7 @@ function parseOpenCodeMessageRows(rows, sourceId) {
1204
1564
  }
1205
1565
  const provider = asTrimmedText(payload.providerID) ?? asTrimmedText(payload.provider);
1206
1566
  const model = asTrimmedText(payload.modelID) ?? asTrimmedText(payload.model);
1567
+ const repoRoot = resolveRepoRoot2(payload);
1207
1568
  const tokens = asRecord(payload.tokens);
1208
1569
  const tokenCache = asRecord(tokens?.cache);
1209
1570
  const inputTokens = toNumberLike(tokens?.input);
@@ -1233,6 +1594,7 @@ function parseOpenCodeMessageRows(rows, sourceId) {
1233
1594
  source: sourceId,
1234
1595
  sessionId,
1235
1596
  timestamp,
1597
+ repoRoot,
1236
1598
  provider,
1237
1599
  model,
1238
1600
  inputTokens,
@@ -1428,25 +1790,9 @@ function queryOpenCodeMessageRows(database) {
1428
1790
  // src/sources/opencode/opencode-source-adapter.ts
1429
1791
  var DEFAULT_BUSY_RETRY_COUNT = 2;
1430
1792
  var DEFAULT_BUSY_RETRY_DELAY_MS = 50;
1431
- function isBlankText(value) {
1793
+ function isBlankText2(value) {
1432
1794
  return value.trim().length === 0;
1433
1795
  }
1434
- async function pathExists(filePath) {
1435
- try {
1436
- await access(filePath, constants.F_OK);
1437
- return true;
1438
- } catch {
1439
- return false;
1440
- }
1441
- }
1442
- async function pathReadable(filePath) {
1443
- try {
1444
- await access(filePath, constants.R_OK);
1445
- return true;
1446
- } catch {
1447
- return false;
1448
- }
1449
- }
1450
1796
  async function sleep2(delayMs) {
1451
1797
  await new Promise((resolve) => {
1452
1798
  setTimeout(resolve, delayMs);
@@ -1458,6 +1804,7 @@ var OpenCodeSourceAdapter = class {
1458
1804
  resolveDefaultDbPaths;
1459
1805
  pathExists;
1460
1806
  pathReadable;
1807
+ pathIsFile;
1461
1808
  loadSqliteModule;
1462
1809
  maxBusyRetries;
1463
1810
  busyRetryDelayMs;
@@ -1467,6 +1814,7 @@ var OpenCodeSourceAdapter = class {
1467
1814
  this.resolveDefaultDbPaths = options.resolveDefaultDbPaths ?? getDefaultOpenCodeDbPathCandidates;
1468
1815
  this.pathExists = options.pathExists ?? pathExists;
1469
1816
  this.pathReadable = options.pathReadable ?? pathReadable;
1817
+ this.pathIsFile = options.pathIsFile ?? pathIsFile;
1470
1818
  this.loadSqliteModule = options.loadSqliteModule ?? loadNodeSqliteModule;
1471
1819
  this.maxBusyRetries = Math.max(0, options.maxBusyRetries ?? DEFAULT_BUSY_RETRY_COUNT);
1472
1820
  this.busyRetryDelayMs = Math.max(1, options.busyRetryDelayMs ?? DEFAULT_BUSY_RETRY_DELAY_MS);
@@ -1474,7 +1822,7 @@ var OpenCodeSourceAdapter = class {
1474
1822
  }
1475
1823
  async discoverFiles() {
1476
1824
  if (this.explicitDbPath !== void 0) {
1477
- if (isBlankText(this.explicitDbPath)) {
1825
+ if (isBlankText2(this.explicitDbPath)) {
1478
1826
  throw new Error("--opencode-db must be a non-empty path");
1479
1827
  }
1480
1828
  const explicitDbPath = this.explicitDbPath.trim();
@@ -1482,11 +1830,17 @@ var OpenCodeSourceAdapter = class {
1482
1830
  if (!readable) {
1483
1831
  throw new Error(`OpenCode DB path is missing or unreadable: ${explicitDbPath}`);
1484
1832
  }
1833
+ if (await this.pathExists(explicitDbPath) && !await this.pathIsFile(explicitDbPath)) {
1834
+ throw new Error(`OpenCode DB path is not a file: ${explicitDbPath}`);
1835
+ }
1485
1836
  return [explicitDbPath];
1486
1837
  }
1487
1838
  let firstUnreadableCandidatePath;
1488
1839
  for (const candidatePath of this.resolveDefaultDbPaths()) {
1489
1840
  if (await this.pathReadable(candidatePath)) {
1841
+ if (await this.pathExists(candidatePath) && !await this.pathIsFile(candidatePath)) {
1842
+ throw new Error(`OpenCode DB path is not a file: ${candidatePath}`);
1843
+ }
1490
1844
  return [candidatePath];
1491
1845
  }
1492
1846
  if (!firstUnreadableCandidatePath && await this.pathExists(candidatePath)) {
@@ -1503,7 +1857,7 @@ var OpenCodeSourceAdapter = class {
1503
1857
  return parseDiagnostics.events;
1504
1858
  }
1505
1859
  async parseFileWithDiagnostics(dbPath) {
1506
- if (isBlankText(dbPath)) {
1860
+ if (isBlankText2(dbPath)) {
1507
1861
  throw new Error("OpenCode DB path must be a non-empty path");
1508
1862
  }
1509
1863
  const normalizedDbPath = dbPath.trim();
@@ -1511,6 +1865,9 @@ var OpenCodeSourceAdapter = class {
1511
1865
  if (!readable) {
1512
1866
  throw new Error(`OpenCode DB path is unreadable: ${normalizedDbPath}`);
1513
1867
  }
1868
+ if (await this.pathExists(normalizedDbPath) && !await this.pathIsFile(normalizedDbPath)) {
1869
+ throw new Error(`OpenCode DB path is not a file: ${normalizedDbPath}`);
1870
+ }
1514
1871
  return runWithBusyRetries(() => this.parseFileOnce(normalizedDbPath), {
1515
1872
  dbPath: normalizedDbPath,
1516
1873
  maxBusyRetries: this.maxBusyRetries,
@@ -1531,9 +1888,9 @@ var OpenCodeSourceAdapter = class {
1531
1888
  };
1532
1889
 
1533
1890
  // src/sources/pi/pi-source-adapter.ts
1534
- import os4 from "os";
1535
- import path6 from "path";
1536
- 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");
1537
1894
  var PI_MESSAGE_LINE_PATTERN = /"type"\s*:\s*"message"/u;
1538
1895
  var PI_SESSION_LINE_PATTERN = /"type"\s*:\s*"session"/u;
1539
1896
  var PI_MODEL_CHANGE_LINE_PATTERN = /"type"\s*:\s*"model_change"/u;
@@ -1582,14 +1939,40 @@ function extractUsageFromRecord(usage) {
1582
1939
  totalTokens: toNumberLike(usage.totalTokens),
1583
1940
  costUsd: toNumberLike(cost?.total)
1584
1941
  };
1585
- const hasKnownUsageField = extracted.inputTokens !== void 0 || extracted.outputTokens !== void 0 || extracted.reasoningTokens !== void 0 || extracted.cacheReadTokens !== void 0 || extracted.cacheWriteTokens !== void 0 || extracted.totalTokens !== void 0 || extracted.costUsd !== void 0;
1586
- return hasKnownUsageField ? extracted : void 0;
1587
- }
1588
- function extractUsage(line, message) {
1589
- const lineUsage = asRecord(line.usage);
1590
- const messageUsage = asRecord(message?.usage);
1591
- if (lineUsage) {
1592
- const extractedLineUsage = extractUsageFromRecord(lineUsage);
1942
+ const toFiniteNumber2 = (value) => {
1943
+ if (value === null || value === void 0) {
1944
+ return void 0;
1945
+ }
1946
+ if (typeof value === "string" && value.trim().length === 0) {
1947
+ return void 0;
1948
+ }
1949
+ const parsed = typeof value === "number" ? value : Number(value);
1950
+ if (!Number.isFinite(parsed)) {
1951
+ return void 0;
1952
+ }
1953
+ return parsed;
1954
+ };
1955
+ const usageCandidates = [
1956
+ extracted.inputTokens,
1957
+ extracted.outputTokens,
1958
+ extracted.reasoningTokens,
1959
+ extracted.cacheReadTokens,
1960
+ extracted.cacheWriteTokens,
1961
+ extracted.totalTokens
1962
+ ];
1963
+ const hasPositiveUsageSignal = usageCandidates.some((value) => {
1964
+ const parsed = toFiniteNumber2(value);
1965
+ return parsed !== void 0 && parsed > 0;
1966
+ });
1967
+ const explicitCost = toFiniteNumber2(extracted.costUsd);
1968
+ const hasPositiveCostSignal = explicitCost !== void 0 && explicitCost > 0;
1969
+ return hasPositiveUsageSignal || hasPositiveCostSignal ? extracted : void 0;
1970
+ }
1971
+ function extractUsage(line, message) {
1972
+ const lineUsage = asRecord(line.usage);
1973
+ const messageUsage = asRecord(message?.usage);
1974
+ if (lineUsage) {
1975
+ const extractedLineUsage = extractUsageFromRecord(lineUsage);
1593
1976
  if (extractedLineUsage) {
1594
1977
  return extractedLineUsage;
1595
1978
  }
@@ -1600,18 +1983,37 @@ function extractUsage(line, message) {
1600
1983
  return extractUsageFromRecord(messageUsage);
1601
1984
  }
1602
1985
  function getFallbackSessionId2(filePath) {
1603
- return path6.basename(filePath, ".jsonl");
1986
+ return path7.basename(filePath, ".jsonl");
1987
+ }
1988
+ function resolveRepoRootFromRecord(record) {
1989
+ if (!record) {
1990
+ return void 0;
1991
+ }
1992
+ const pathRecord = asRecord(record.path);
1993
+ return asTrimmedText(pathRecord?.root) ?? asTrimmedText(pathRecord?.cwd) ?? asTrimmedText(record.cwd) ?? asTrimmedText(record.repo_root) ?? asTrimmedText(record.repoRoot) ?? asTrimmedText(record.project_root) ?? asTrimmedText(record.projectRoot);
1604
1994
  }
1605
1995
  var PiSourceAdapter = class {
1606
1996
  id = "pi";
1607
1997
  sessionsDir;
1608
1998
  providerFilter;
1999
+ requireSessionsDir;
1609
2000
  constructor(options = {}) {
1610
2001
  this.sessionsDir = options.sessionsDir ?? defaultSessionsDir2;
1611
2002
  this.providerFilter = options.providerFilter ?? allowAllProviders;
2003
+ this.requireSessionsDir = options.requireSessionsDir ?? false;
1612
2004
  }
1613
2005
  async discoverFiles() {
1614
- return discoverJsonlFiles(this.sessionsDir);
2006
+ if (isBlankText(this.sessionsDir)) {
2007
+ throw new Error("PI sessions directory must be a non-empty path");
2008
+ }
2009
+ const normalizedSessionsDir = this.sessionsDir.trim();
2010
+ if (this.requireSessionsDir && !await pathReadable(normalizedSessionsDir)) {
2011
+ throw new Error(`PI sessions directory is missing or unreadable: ${normalizedSessionsDir}`);
2012
+ }
2013
+ if (this.requireSessionsDir && !await pathIsDirectory(normalizedSessionsDir)) {
2014
+ throw new Error(`PI sessions directory is not a directory: ${normalizedSessionsDir}`);
2015
+ }
2016
+ return discoverJsonlFiles(normalizedSessionsDir);
1615
2017
  }
1616
2018
  async parseFile(filePath) {
1617
2019
  const events = [];
@@ -1622,11 +2024,13 @@ var PiSourceAdapter = class {
1622
2024
  if (line.type === "session") {
1623
2025
  state.sessionId = asTrimmedText(line.id) ?? state.sessionId;
1624
2026
  state.sessionTimestamp = asTrimmedText(line.timestamp) ?? state.sessionTimestamp;
2027
+ state.repoRoot = resolveRepoRootFromRecord(line) ?? state.repoRoot;
1625
2028
  continue;
1626
2029
  }
1627
2030
  if (line.type === "model_change") {
1628
2031
  state.provider = asTrimmedText(line.provider) ?? state.provider;
1629
2032
  state.model = asTrimmedText(line.modelId) ?? asTrimmedText(line.model) ?? state.model;
2033
+ state.repoRoot = resolveRepoRootFromRecord(line) ?? state.repoRoot;
1630
2034
  continue;
1631
2035
  }
1632
2036
  if (line.type !== "message") {
@@ -1646,12 +2050,14 @@ var PiSourceAdapter = class {
1646
2050
  continue;
1647
2051
  }
1648
2052
  const model = asTrimmedText(line.model) ?? asTrimmedText(line.modelId) ?? asTrimmedText(message?.model) ?? state.model;
2053
+ const repoRoot = resolveRepoRootFromRecord(line) ?? resolveRepoRootFromRecord(message) ?? state.repoRoot;
1649
2054
  try {
1650
2055
  events.push(
1651
2056
  createUsageEvent({
1652
2057
  source: this.id,
1653
2058
  sessionId: state.sessionId,
1654
2059
  timestamp,
2060
+ repoRoot,
1655
2061
  provider,
1656
2062
  model,
1657
2063
  ...usage
@@ -1692,16 +2098,43 @@ var sourceRegistrations = [
1692
2098
  {
1693
2099
  id: "pi",
1694
2100
  sourceDirOverride: { kind: "directory" },
1695
- create: (options, sourceDirectoryOverrides) => new PiSourceAdapter({
1696
- sessionsDir: resolveDirectoryOverride("pi", options.piDir, sourceDirectoryOverrides)
1697
- })
2101
+ create: (options, sourceDirectoryOverrides) => {
2102
+ const directoryConfig = resolveDirectoryConfig("pi", options.piDir, sourceDirectoryOverrides);
2103
+ return new PiSourceAdapter({
2104
+ sessionsDir: directoryConfig.path,
2105
+ requireSessionsDir: directoryConfig.requireExistingPath
2106
+ });
2107
+ }
1698
2108
  },
1699
2109
  {
1700
2110
  id: "codex",
1701
2111
  sourceDirOverride: { kind: "directory" },
1702
- create: (options, sourceDirectoryOverrides) => new CodexSourceAdapter({
1703
- sessionsDir: resolveDirectoryOverride("codex", options.codexDir, sourceDirectoryOverrides)
1704
- })
2112
+ create: (options, sourceDirectoryOverrides) => {
2113
+ const directoryConfig = resolveDirectoryConfig(
2114
+ "codex",
2115
+ options.codexDir,
2116
+ sourceDirectoryOverrides
2117
+ );
2118
+ return new CodexSourceAdapter({
2119
+ sessionsDir: directoryConfig.path,
2120
+ requireSessionsDir: directoryConfig.requireExistingPath
2121
+ });
2122
+ }
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
+ }
1705
2138
  },
1706
2139
  {
1707
2140
  id: "opencode",
@@ -1736,9 +2169,7 @@ function validateSourceDirectoryOverrideIds(sourceDirectoryOverrides) {
1736
2169
  if (unknownSourceIds.length === 0) {
1737
2170
  return;
1738
2171
  }
1739
- const allowedSourceIds = [...sourceDirSupportedIds].sort(
1740
- (left, right) => left.localeCompare(right)
1741
- );
2172
+ const allowedSourceIds = [...sourceDirSupportedIds].sort(compareByCodePoint);
1742
2173
  throw new Error(
1743
2174
  `Unknown --source-dir source id(s): ${unknownSourceIds.join(", ")}. Allowed values: ${allowedSourceIds.join(", ")}`
1744
2175
  );
@@ -1751,14 +2182,41 @@ function validateOpencodeOverride(opencodeDb) {
1751
2182
  throw new Error("--opencode-db must be a non-empty path");
1752
2183
  }
1753
2184
  }
1754
- function resolveDirectoryOverride(sourceId, explicitDirectory, sourceDirectoryOverrides) {
1755
- return explicitDirectory ?? sourceDirectoryOverrides.get(sourceId);
2185
+ function validateDirectoryOverride(optionName, value) {
2186
+ if (value === void 0) {
2187
+ return;
2188
+ }
2189
+ if (value.trim().length === 0) {
2190
+ throw new Error(`${optionName} must be a non-empty path`);
2191
+ }
2192
+ }
2193
+ function resolveDirectoryConfig(sourceId, explicitDirectory, sourceDirectoryOverrides) {
2194
+ if (explicitDirectory !== void 0) {
2195
+ return {
2196
+ path: explicitDirectory,
2197
+ requireExistingPath: true
2198
+ };
2199
+ }
2200
+ const sourceDirOverride = sourceDirectoryOverrides.get(sourceId);
2201
+ if (sourceDirOverride !== void 0) {
2202
+ return {
2203
+ path: sourceDirOverride,
2204
+ requireExistingPath: true
2205
+ };
2206
+ }
2207
+ return {
2208
+ path: void 0,
2209
+ requireExistingPath: false
2210
+ };
1756
2211
  }
1757
2212
  function getDefaultSourceIds() {
1758
2213
  return sourceRegistrations.map((source) => source.id);
1759
2214
  }
1760
2215
  function createDefaultAdapters(options) {
1761
2216
  validateOpencodeOverride(options.opencodeDb);
2217
+ validateDirectoryOverride("--pi-dir", options.piDir);
2218
+ validateDirectoryOverride("--codex-dir", options.codexDir);
2219
+ validateDirectoryOverride("--gemini-dir", options.geminiDir);
1762
2220
  const sourceDirectoryOverrides = parseSourceDirectoryOverrides(options.sourceDir);
1763
2221
  validateSourceDirectoryOverrideIds(sourceDirectoryOverrides);
1764
2222
  return sourceRegistrations.map((source) => source.create(options, sourceDirectoryOverrides));
@@ -1934,7 +2392,7 @@ function aggregateUsage(events, options) {
1934
2392
  periodSources.set(event.source, rowAccumulator);
1935
2393
  addEventToAccumulator(rowAccumulator, event);
1936
2394
  }
1937
- const sortedPeriodKeys = [...periodMap.keys()].sort((left, right) => left.localeCompare(right));
2395
+ const sortedPeriodKeys = [...periodMap.keys()].sort(compareByCodePoint);
1938
2396
  const rows = [];
1939
2397
  const grandTotals = createEmptyTotals();
1940
2398
  const grandModelTotals = /* @__PURE__ */ new Map();
@@ -1976,20 +2434,644 @@ function aggregateUsage(events, options) {
1976
2434
  modelBreakdown: toModelUsageBreakdown(periodCombinedModelTotals),
1977
2435
  ...periodCombinedTotals
1978
2436
  };
1979
- rows.push(combinedRow);
2437
+ rows.push(combinedRow);
2438
+ }
2439
+ }
2440
+ const finalizedGrandTotals = events.length === 0 && grandTotals.costUsd === void 0 && grandTotals.costIncomplete !== true ? { ...grandTotals, costUsd: 0 } : grandTotals;
2441
+ const grandTotalRow = {
2442
+ rowType: "grand_total",
2443
+ periodKey: "ALL",
2444
+ source: "combined",
2445
+ models: normalizeModelList(grandModelTotals.keys()),
2446
+ modelBreakdown: toModelUsageBreakdown(grandModelTotals),
2447
+ ...finalizedGrandTotals
2448
+ };
2449
+ rows.push(grandTotalRow);
2450
+ return rows;
2451
+ }
2452
+
2453
+ // src/efficiency/efficiency-row.ts
2454
+ function createEmptyEfficiencyUsageTotals() {
2455
+ return {
2456
+ inputTokens: 0,
2457
+ outputTokens: 0,
2458
+ reasoningTokens: 0,
2459
+ cacheReadTokens: 0,
2460
+ cacheWriteTokens: 0,
2461
+ totalTokens: 0,
2462
+ costUsd: 0
2463
+ };
2464
+ }
2465
+ function createEmptyEfficiencyOutcomeTotals() {
2466
+ return {
2467
+ commitCount: 0,
2468
+ linesAdded: 0,
2469
+ linesDeleted: 0,
2470
+ linesChanged: 0
2471
+ };
2472
+ }
2473
+
2474
+ // src/efficiency/aggregate-efficiency.ts
2475
+ var USD_PRECISION_SCALE2 = 1e12;
2476
+ function addUsd2(left, right) {
2477
+ return Math.round((left + right) * USD_PRECISION_SCALE2) / USD_PRECISION_SCALE2;
2478
+ }
2479
+ function toUsageTotals(row) {
2480
+ return {
2481
+ inputTokens: row.inputTokens,
2482
+ outputTokens: row.outputTokens,
2483
+ reasoningTokens: row.reasoningTokens,
2484
+ cacheReadTokens: row.cacheReadTokens,
2485
+ cacheWriteTokens: row.cacheWriteTokens,
2486
+ totalTokens: row.totalTokens,
2487
+ costUsd: row.costUsd,
2488
+ costIncomplete: row.costIncomplete
2489
+ };
2490
+ }
2491
+ function buildUsageTotalsByPeriod(usageRows) {
2492
+ const combinedByPeriod = /* @__PURE__ */ new Map();
2493
+ const sourceByPeriod = /* @__PURE__ */ new Map();
2494
+ for (const row of usageRows) {
2495
+ if (row.rowType === "grand_total") {
2496
+ continue;
2497
+ }
2498
+ if (row.rowType === "period_combined") {
2499
+ combinedByPeriod.set(row.periodKey, toUsageTotals(row));
2500
+ continue;
2501
+ }
2502
+ const existingSourceTotals = sourceByPeriod.get(row.periodKey) ?? createEmptyEfficiencyUsageTotals();
2503
+ sourceByPeriod.set(row.periodKey, addUsageTotals(existingSourceTotals, toUsageTotals(row)));
2504
+ }
2505
+ const periodKeys = /* @__PURE__ */ new Set([...combinedByPeriod.keys(), ...sourceByPeriod.keys()]);
2506
+ const usageTotalsByPeriod = /* @__PURE__ */ new Map();
2507
+ for (const periodKey of periodKeys) {
2508
+ usageTotalsByPeriod.set(
2509
+ periodKey,
2510
+ combinedByPeriod.get(periodKey) ?? sourceByPeriod.get(periodKey) ?? createEmptyEfficiencyUsageTotals()
2511
+ );
2512
+ }
2513
+ return usageTotalsByPeriod;
2514
+ }
2515
+ function addOutcomeTotals(left, right) {
2516
+ return {
2517
+ commitCount: left.commitCount + right.commitCount,
2518
+ linesAdded: left.linesAdded + right.linesAdded,
2519
+ linesDeleted: left.linesDeleted + right.linesDeleted,
2520
+ linesChanged: left.linesChanged + right.linesChanged
2521
+ };
2522
+ }
2523
+ function addUsageTotals(left, right) {
2524
+ const hasUnknownCost = left.costIncomplete === true && left.costUsd === void 0 || right.costIncomplete === true && right.costUsd === void 0;
2525
+ const isNeutralZeroCost = (value) => value.totalTokens === 0 && value.costUsd === 0 && value.costIncomplete !== true;
2526
+ const leftKnownCost = left.costUsd !== void 0 && !isNeutralZeroCost(left) ? left.costUsd : void 0;
2527
+ const rightKnownCost = right.costUsd !== void 0 && !isNeutralZeroCost(right) ? right.costUsd : void 0;
2528
+ let costUsd = leftKnownCost !== void 0 && rightKnownCost !== void 0 ? addUsd2(leftKnownCost, rightKnownCost) : leftKnownCost ?? rightKnownCost;
2529
+ if (hasUnknownCost && (costUsd === void 0 || costUsd === 0)) {
2530
+ costUsd = void 0;
2531
+ }
2532
+ return {
2533
+ inputTokens: left.inputTokens + right.inputTokens,
2534
+ outputTokens: left.outputTokens + right.outputTokens,
2535
+ reasoningTokens: left.reasoningTokens + right.reasoningTokens,
2536
+ cacheReadTokens: left.cacheReadTokens + right.cacheReadTokens,
2537
+ cacheWriteTokens: left.cacheWriteTokens + right.cacheWriteTokens,
2538
+ totalTokens: left.totalTokens + right.totalTokens,
2539
+ costUsd,
2540
+ costIncomplete: left.costIncomplete || right.costIncomplete ? true : void 0
2541
+ };
2542
+ }
2543
+ function computeDerivedMetrics(usage, outcomes) {
2544
+ const costUsd = usage.costUsd;
2545
+ const nonCacheTotalTokens = usage.inputTokens + usage.outputTokens + usage.reasoningTokens;
2546
+ return {
2547
+ usdPerCommit: costUsd !== void 0 && outcomes.commitCount > 0 ? costUsd / outcomes.commitCount : void 0,
2548
+ usdPer1kLinesChanged: costUsd !== void 0 && outcomes.linesChanged > 0 ? costUsd / (outcomes.linesChanged / 1e3) : void 0,
2549
+ tokensPerCommit: outcomes.commitCount > 0 ? usage.totalTokens / outcomes.commitCount : void 0,
2550
+ nonCacheTokensPerCommit: outcomes.commitCount > 0 ? nonCacheTotalTokens / outcomes.commitCount : void 0,
2551
+ commitsPerUsd: costUsd !== void 0 && costUsd > 0 ? outcomes.commitCount / costUsd : void 0
2552
+ };
2553
+ }
2554
+ function aggregateEfficiency(options) {
2555
+ const usageTotalsByPeriod = buildUsageTotalsByPeriod(options.usageRows);
2556
+ const periodKeys = [
2557
+ .../* @__PURE__ */ new Set([...usageTotalsByPeriod.keys(), ...options.periodOutcomes.keys()])
2558
+ ].sort(compareByCodePoint);
2559
+ const rows = [];
2560
+ let totalUsage = createEmptyEfficiencyUsageTotals();
2561
+ let totalOutcomes = createEmptyEfficiencyOutcomeTotals();
2562
+ for (const periodKey of periodKeys) {
2563
+ const usageTotals = usageTotalsByPeriod.get(periodKey) ?? createEmptyEfficiencyUsageTotals();
2564
+ const outcomeTotals = options.periodOutcomes.get(periodKey) ?? createEmptyEfficiencyOutcomeTotals();
2565
+ const hasUsageRow = usageTotalsByPeriod.has(periodKey);
2566
+ const hasUsageSignal3 = hasUsageRow && (usageTotals.totalTokens > 0 || usageTotals.costUsd !== void 0 || usageTotals.costIncomplete === true);
2567
+ if (outcomeTotals.commitCount === 0 || !hasUsageSignal3) {
2568
+ continue;
2569
+ }
2570
+ const derived = computeDerivedMetrics(usageTotals, outcomeTotals);
2571
+ rows.push({
2572
+ rowType: "period",
2573
+ periodKey,
2574
+ ...usageTotals,
2575
+ ...outcomeTotals,
2576
+ ...derived
2577
+ });
2578
+ totalUsage = addUsageTotals(totalUsage, usageTotals);
2579
+ totalOutcomes = addOutcomeTotals(totalOutcomes, outcomeTotals);
2580
+ }
2581
+ const finalizedTotalUsage = totalUsage.costUsd === void 0 && totalUsage.costIncomplete !== true && totalUsage.totalTokens === 0 ? { ...totalUsage, costUsd: 0 } : totalUsage;
2582
+ rows.push({
2583
+ rowType: "grand_total",
2584
+ periodKey: "ALL",
2585
+ ...finalizedTotalUsage,
2586
+ ...totalOutcomes,
2587
+ ...computeDerivedMetrics(finalizedTotalUsage, totalOutcomes)
2588
+ });
2589
+ return rows;
2590
+ }
2591
+
2592
+ // src/efficiency/git-outcome-collector.ts
2593
+ import { spawn as spawn2 } from "child_process";
2594
+ import { createInterface as createInterface3 } from "readline";
2595
+ import path8 from "path";
2596
+ import { stat as stat2 } from "fs/promises";
2597
+ var GIT_COMMIT_MARKER = "";
2598
+ var SHORTSTAT_PATTERN = /(\d+)\s+files?\s+changed(?:,\s+(\d+)\s+insertions?\(\+\))?(?:,\s+(\d+)\s+deletions?\(-\))?/u;
2599
+ function shiftDate(value, days) {
2600
+ const date = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
2601
+ if (Number.isNaN(date.getTime())) {
2602
+ throw new Error(`Invalid date value: ${value}`);
2603
+ }
2604
+ date.setUTCDate(date.getUTCDate() + days);
2605
+ return date.toISOString().slice(0, 10);
2606
+ }
2607
+ function escapeGitRegexLiteral(value) {
2608
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
2609
+ }
2610
+ function resolveGitCommandFailureReason(result) {
2611
+ return result.stderr.trim() || `git exited with code ${result.exitCode}`;
2612
+ }
2613
+ function resolveRepoDir(repoDir) {
2614
+ if (repoDir === void 0) {
2615
+ return path8.resolve(process.cwd());
2616
+ }
2617
+ const normalizedRepoDir = repoDir.trim();
2618
+ if (!normalizedRepoDir) {
2619
+ throw new Error("--repo-dir must be a non-empty path");
2620
+ }
2621
+ return path8.resolve(normalizedRepoDir);
2622
+ }
2623
+ function getNodeErrorCode2(error) {
2624
+ if (typeof error !== "object" || error === null || !("code" in error)) {
2625
+ return void 0;
2626
+ }
2627
+ const record = error;
2628
+ return typeof record.code === "string" ? record.code : void 0;
2629
+ }
2630
+ async function assertRepoDirReadable(repoDir) {
2631
+ let directoryStats;
2632
+ try {
2633
+ directoryStats = await stat2(repoDir);
2634
+ } catch (error) {
2635
+ const code = getNodeErrorCode2(error);
2636
+ if (code === "ENOENT") {
2637
+ throw new Error(`Repository path does not exist: ${repoDir}`, { cause: error });
2638
+ }
2639
+ if (code === "EACCES" || code === "EPERM") {
2640
+ throw new Error(`Repository path is unreadable: ${repoDir}`, { cause: error });
2641
+ }
2642
+ throw error;
2643
+ }
2644
+ if (!directoryStats.isDirectory()) {
2645
+ throw new Error(`Repository path is not a directory: ${repoDir}`);
2646
+ }
2647
+ }
2648
+ async function assertGitRepository(repoDir, runCommand) {
2649
+ const gitRepoResult = await runCommand(repoDir, ["rev-parse", "--is-inside-work-tree"]);
2650
+ if (gitRepoResult.exitCode === 0) {
2651
+ return;
2652
+ }
2653
+ throw new Error(`Repository is not a git repository: ${repoDir}`);
2654
+ }
2655
+ function isNoCommitHistoryFailure(result) {
2656
+ if (result.exitCode !== 128) {
2657
+ return false;
2658
+ }
2659
+ const reason = result.stderr.toLowerCase();
2660
+ return reason.includes("does not have any commits yet") || reason.includes("needed a single revision") || reason.includes("unknown revision or path not in the working tree") || reason.includes("bad revision 'head'");
2661
+ }
2662
+ function createEmptyOutcomeCollection(repoDir, includeMergeCommits) {
2663
+ return {
2664
+ periodOutcomes: /* @__PURE__ */ new Map(),
2665
+ totalOutcomes: createEmptyEfficiencyOutcomeTotals(),
2666
+ diagnostics: {
2667
+ repoDir,
2668
+ includeMergeCommits,
2669
+ commitsCollected: 0,
2670
+ linesAdded: 0,
2671
+ linesDeleted: 0
2672
+ }
2673
+ };
2674
+ }
2675
+ function isMissingGitUserEmailError(error) {
2676
+ return error instanceof Error && error.message.startsWith("Git user.email is not configured for");
2677
+ }
2678
+ function resolveConfiguredEmailFromLines(lines) {
2679
+ return lines.map((line) => line.trim()).find((line) => line.length > 0);
2680
+ }
2681
+ function resolveEmailFromGitAuthorIdent(lines) {
2682
+ const identLine = lines.map((line) => line.trim()).find((line) => line.length > 0);
2683
+ if (!identLine) {
2684
+ return void 0;
2685
+ }
2686
+ const emailMatch = /<([^>]+)>/u.exec(identLine);
2687
+ const email = emailMatch?.[1]?.trim();
2688
+ return email && email.length > 0 ? email : void 0;
2689
+ }
2690
+ async function resolveConfiguredAuthorEmail(repoDir, runCommand) {
2691
+ const configLookupAttempts = [
2692
+ ["config", "--get", "user.email"],
2693
+ ["config", "--global", "--get", "user.email"]
2694
+ ];
2695
+ for (const args of configLookupAttempts) {
2696
+ const configResult = await runCommand(repoDir, args);
2697
+ if (configResult.exitCode === 0) {
2698
+ const configuredEmail = resolveConfiguredEmailFromLines(configResult.lines);
2699
+ if (configuredEmail) {
2700
+ return configuredEmail;
2701
+ }
2702
+ continue;
2703
+ }
2704
+ if (configResult.exitCode !== 1) {
2705
+ const reason = resolveGitCommandFailureReason(configResult);
2706
+ throw new Error(`Failed to resolve git user.email from ${repoDir}: ${reason}`);
2707
+ }
2708
+ }
2709
+ const gitAuthorIdentResult = await runCommand(repoDir, ["var", "GIT_AUTHOR_IDENT"]);
2710
+ if (gitAuthorIdentResult.exitCode === 0) {
2711
+ const authorIdentEmail = resolveEmailFromGitAuthorIdent(gitAuthorIdentResult.lines);
2712
+ if (authorIdentEmail) {
2713
+ return authorIdentEmail;
2714
+ }
2715
+ }
2716
+ throw new Error(
2717
+ `Git user.email is not configured for ${repoDir}. Run: git -C ${repoDir} config user.email "you@example.com"`
2718
+ );
2719
+ }
2720
+ function toIsoTimestamp(timestampSeconds) {
2721
+ const timestamp = new Date(timestampSeconds * 1e3);
2722
+ if (Number.isNaN(timestamp.getTime())) {
2723
+ throw new Error(`Invalid git commit timestamp: ${timestampSeconds}`);
2724
+ }
2725
+ return timestamp.toISOString();
2726
+ }
2727
+ function parseShortstatLine(line) {
2728
+ const shortstatMatch = SHORTSTAT_PATTERN.exec(line.trim());
2729
+ if (!shortstatMatch) {
2730
+ return void 0;
2731
+ }
2732
+ const linesAddedRaw = shortstatMatch[2];
2733
+ const linesDeletedRaw = shortstatMatch[3];
2734
+ return {
2735
+ linesAdded: linesAddedRaw ? Number.parseInt(linesAddedRaw, 10) : 0,
2736
+ linesDeleted: linesDeletedRaw ? Number.parseInt(linesDeletedRaw, 10) : 0
2737
+ };
2738
+ }
2739
+ function finalizeCurrentEvent(currentEvent, events, authorEmail) {
2740
+ if (!currentEvent) {
2741
+ return;
2742
+ }
2743
+ if (authorEmail && currentEvent.authorEmail.trim().toLowerCase() !== authorEmail.trim().toLowerCase()) {
2744
+ return;
2745
+ }
2746
+ const timestamp = toIsoTimestamp(currentEvent.timestampSeconds);
2747
+ events.push({
2748
+ sha: currentEvent.sha,
2749
+ timestamp,
2750
+ linesAdded: currentEvent.linesAdded,
2751
+ linesDeleted: currentEvent.linesDeleted,
2752
+ linesChanged: currentEvent.linesAdded + currentEvent.linesDeleted
2753
+ });
2754
+ }
2755
+ function parseGitLogShortstatLines(lines, authorEmail) {
2756
+ const events = [];
2757
+ let currentEvent;
2758
+ for (const line of lines) {
2759
+ if (line.startsWith(GIT_COMMIT_MARKER)) {
2760
+ const commitParts = line.slice(1).split(GIT_COMMIT_MARKER);
2761
+ const timestampPart = commitParts[0];
2762
+ const shaPart = commitParts[1];
2763
+ const authorPart = commitParts[2];
2764
+ if (commitParts.length !== 3 || !/^\d+$/u.test(timestampPart) || !/^[0-9a-f]{7,64}$/iu.test(shaPart) || authorPart.trim().length === 0) {
2765
+ throw new Error(`Malformed git commit boundary line: ${line}`);
2766
+ }
2767
+ finalizeCurrentEvent(currentEvent, events, authorEmail);
2768
+ currentEvent = {
2769
+ timestampSeconds: Number.parseInt(timestampPart, 10),
2770
+ sha: shaPart,
2771
+ authorEmail: authorPart,
2772
+ linesAdded: 0,
2773
+ linesDeleted: 0
2774
+ };
2775
+ continue;
2776
+ }
2777
+ if (!currentEvent) {
2778
+ continue;
2779
+ }
2780
+ const shortstat = parseShortstatLine(line);
2781
+ if (!shortstat) {
2782
+ continue;
2783
+ }
2784
+ currentEvent.linesAdded += shortstat.linesAdded;
2785
+ currentEvent.linesDeleted += shortstat.linesDeleted;
2786
+ }
2787
+ finalizeCurrentEvent(currentEvent, events, authorEmail);
2788
+ return events;
2789
+ }
2790
+ async function runGitCommand(repoDir, args) {
2791
+ return await new Promise((resolve, reject) => {
2792
+ const child = spawn2("git", args, {
2793
+ cwd: repoDir,
2794
+ env: {
2795
+ ...process.env,
2796
+ LC_ALL: "C",
2797
+ LANG: "C"
2798
+ },
2799
+ stdio: ["ignore", "pipe", "pipe"]
2800
+ });
2801
+ const lines = [];
2802
+ let stderr = "";
2803
+ const stdoutReader = createInterface3({ input: child.stdout });
2804
+ const stdoutPromise = (async () => {
2805
+ for await (const line of stdoutReader) {
2806
+ lines.push(line);
2807
+ }
2808
+ })();
2809
+ child.stderr.setEncoding("utf8");
2810
+ child.stderr.on("data", (chunk) => {
2811
+ stderr += chunk;
2812
+ });
2813
+ child.once("error", (error) => {
2814
+ reject(error);
2815
+ });
2816
+ child.once("close", (exitCode) => {
2817
+ void stdoutPromise.then(() => {
2818
+ resolve({
2819
+ lines,
2820
+ stderr,
2821
+ exitCode: exitCode ?? 1
2822
+ });
2823
+ }).catch((error) => {
2824
+ reject(error instanceof Error ? error : new Error(String(error)));
2825
+ });
2826
+ });
2827
+ });
2828
+ }
2829
+ function filterEventsByDateRange(events, timezone, since, until) {
2830
+ const normalizedSince = since ? shiftDate(since, 0) : void 0;
2831
+ const normalizedUntil = until ? shiftDate(until, 0) : void 0;
2832
+ return events.filter((event) => {
2833
+ const eventDate = getPeriodKey(event.timestamp, "daily", timezone);
2834
+ if (normalizedSince && eventDate < normalizedSince) {
2835
+ return false;
2836
+ }
2837
+ if (normalizedUntil && eventDate > normalizedUntil) {
2838
+ return false;
2839
+ }
2840
+ return true;
2841
+ });
2842
+ }
2843
+ function filterEventsByActiveUsageDays(events, timezone, activeUsageDays) {
2844
+ if (activeUsageDays === void 0) {
2845
+ return events;
2846
+ }
2847
+ if (activeUsageDays.size === 0) {
2848
+ return [];
2849
+ }
2850
+ return events.filter(
2851
+ (event) => activeUsageDays.has(getPeriodKey(event.timestamp, "daily", timezone))
2852
+ );
2853
+ }
2854
+ function aggregatePeriodOutcomes(events, granularity, timezone) {
2855
+ const periodOutcomes = /* @__PURE__ */ new Map();
2856
+ const totalOutcomes = createEmptyEfficiencyOutcomeTotals();
2857
+ for (const event of events) {
2858
+ const periodKey = getPeriodKey(event.timestamp, granularity, timezone);
2859
+ const periodTotals = periodOutcomes.get(periodKey) ?? createEmptyEfficiencyOutcomeTotals();
2860
+ periodTotals.commitCount += 1;
2861
+ periodTotals.linesAdded += event.linesAdded;
2862
+ periodTotals.linesDeleted += event.linesDeleted;
2863
+ periodTotals.linesChanged += event.linesChanged;
2864
+ periodOutcomes.set(periodKey, periodTotals);
2865
+ totalOutcomes.commitCount += 1;
2866
+ totalOutcomes.linesAdded += event.linesAdded;
2867
+ totalOutcomes.linesDeleted += event.linesDeleted;
2868
+ totalOutcomes.linesChanged += event.linesChanged;
2869
+ }
2870
+ return {
2871
+ periodOutcomes,
2872
+ totalOutcomes
2873
+ };
2874
+ }
2875
+ function buildGitLogArgs(options) {
2876
+ const args = [
2877
+ "log",
2878
+ `--pretty=format:${GIT_COMMIT_MARKER}%ct${GIT_COMMIT_MARKER}%H${GIT_COMMIT_MARKER}%ae`,
2879
+ "--shortstat",
2880
+ "--regexp-ignore-case",
2881
+ `--author=<${escapeGitRegexLiteral(options.authorEmail)}>`
2882
+ ];
2883
+ if (!options.includeMergeCommits) {
2884
+ args.push("--no-merges");
2885
+ }
2886
+ if (options.since) {
2887
+ args.push(`--since=${shiftDate(options.since, -1)}T00:00:00Z`);
2888
+ }
2889
+ if (options.until) {
2890
+ args.push(`--until=${shiftDate(options.until, 1)}T23:59:59Z`);
2891
+ }
2892
+ return args;
2893
+ }
2894
+ function resolveGitLogDateWindow(options) {
2895
+ if (!options.activeUsageDays || options.activeUsageDays.size === 0) {
2896
+ return {
2897
+ since: options.since,
2898
+ until: options.until
2899
+ };
2900
+ }
2901
+ let earliestUsageDay;
2902
+ let latestUsageDay;
2903
+ for (const usageDay of options.activeUsageDays) {
2904
+ if (!earliestUsageDay || usageDay < earliestUsageDay) {
2905
+ earliestUsageDay = usageDay;
2906
+ }
2907
+ if (!latestUsageDay || usageDay > latestUsageDay) {
2908
+ latestUsageDay = usageDay;
2909
+ }
2910
+ }
2911
+ return {
2912
+ since: options.since ?? earliestUsageDay,
2913
+ until: options.until ?? latestUsageDay
2914
+ };
2915
+ }
2916
+ async function collectGitOutcomes(options, deps = {}) {
2917
+ const repoDir = resolveRepoDir(options.repoDir);
2918
+ const includeMergeCommits = options.includeMergeCommits ?? false;
2919
+ const runCommand = deps.runGitCommand ?? runGitCommand;
2920
+ if (options.activeUsageDays?.size === 0) {
2921
+ return createEmptyOutcomeCollection(repoDir, includeMergeCommits);
2922
+ }
2923
+ if (!deps.runGitCommand) {
2924
+ await assertRepoDirReadable(repoDir);
2925
+ await assertGitRepository(repoDir, runCommand);
2926
+ }
2927
+ let authorEmail;
2928
+ try {
2929
+ authorEmail = await resolveConfiguredAuthorEmail(repoDir, runCommand);
2930
+ } catch (error) {
2931
+ if (!isMissingGitUserEmailError(error)) {
2932
+ throw error;
2933
+ }
2934
+ const headResult = await runCommand(repoDir, ["rev-parse", "--verify", "HEAD"]);
2935
+ if (isNoCommitHistoryFailure(headResult)) {
2936
+ return createEmptyOutcomeCollection(repoDir, includeMergeCommits);
2937
+ }
2938
+ throw error;
2939
+ }
2940
+ const gitLogDateWindow = resolveGitLogDateWindow({
2941
+ since: options.since,
2942
+ until: options.until,
2943
+ activeUsageDays: options.activeUsageDays
2944
+ });
2945
+ const gitResult = await runCommand(
2946
+ repoDir,
2947
+ buildGitLogArgs({
2948
+ since: gitLogDateWindow.since,
2949
+ until: gitLogDateWindow.until,
2950
+ includeMergeCommits,
2951
+ authorEmail
2952
+ })
2953
+ );
2954
+ if (gitResult.exitCode !== 0) {
2955
+ if (isNoCommitHistoryFailure(gitResult)) {
2956
+ return createEmptyOutcomeCollection(repoDir, includeMergeCommits);
2957
+ }
2958
+ const reason = resolveGitCommandFailureReason(gitResult);
2959
+ throw new Error(`Failed to collect git outcomes from ${repoDir}: ${reason}`);
2960
+ }
2961
+ const allEvents = parseGitLogShortstatLines(gitResult.lines, authorEmail);
2962
+ const filteredEvents = filterEventsByDateRange(
2963
+ allEvents,
2964
+ options.timezone,
2965
+ options.since,
2966
+ options.until
2967
+ );
2968
+ const usageAttributedEvents = filterEventsByActiveUsageDays(
2969
+ filteredEvents,
2970
+ options.timezone,
2971
+ options.activeUsageDays
2972
+ );
2973
+ const { periodOutcomes, totalOutcomes } = aggregatePeriodOutcomes(
2974
+ usageAttributedEvents,
2975
+ options.granularity,
2976
+ options.timezone
2977
+ );
2978
+ return {
2979
+ periodOutcomes,
2980
+ totalOutcomes,
2981
+ diagnostics: {
2982
+ repoDir,
2983
+ includeMergeCommits,
2984
+ commitsCollected: totalOutcomes.commitCount,
2985
+ linesAdded: totalOutcomes.linesAdded,
2986
+ linesDeleted: totalOutcomes.linesDeleted
2987
+ }
2988
+ };
2989
+ }
2990
+
2991
+ // src/efficiency/repo-attribution.ts
2992
+ import { access as access2, constants as constants2, realpath } from "fs/promises";
2993
+ import path9 from "path";
2994
+ async function hasGitMarker(directoryPath) {
2995
+ try {
2996
+ await access2(path9.join(directoryPath, ".git"), constants2.F_OK);
2997
+ return true;
2998
+ } catch {
2999
+ return false;
3000
+ }
3001
+ }
3002
+ function normalizeComparablePath(value) {
3003
+ const normalizedPath = path9.normalize(path9.resolve(value));
3004
+ return process.platform === "win32" ? normalizedPath.toLowerCase() : normalizedPath;
3005
+ }
3006
+ async function resolveComparablePath(value) {
3007
+ const resolvedPath = path9.resolve(value);
3008
+ try {
3009
+ return normalizeComparablePath(await realpath(resolvedPath));
3010
+ } catch {
3011
+ return normalizeComparablePath(resolvedPath);
3012
+ }
3013
+ }
3014
+ async function resolveRepoRootFromPathHint(pathHint) {
3015
+ const trimmedPath = pathHint.trim();
3016
+ if (!trimmedPath) {
3017
+ return void 0;
3018
+ }
3019
+ let currentPath = path9.resolve(trimmedPath);
3020
+ for (; ; ) {
3021
+ if (await hasGitMarker(currentPath)) {
3022
+ return currentPath;
3023
+ }
3024
+ const parentPath = path9.dirname(currentPath);
3025
+ if (parentPath === currentPath) {
3026
+ return void 0;
3027
+ }
3028
+ currentPath = parentPath;
3029
+ }
3030
+ }
3031
+ async function attributeUsageEventsToRepo(events, repoDir, resolveRepoRoot3 = resolveRepoRootFromPathHint) {
3032
+ const resolvedTargetRepoRoot = await resolveRepoRoot3(repoDir).catch(() => void 0);
3033
+ const targetRepoPath = await resolveComparablePath(resolvedTargetRepoRoot ?? repoDir);
3034
+ const rootCache = /* @__PURE__ */ new Map();
3035
+ const matchedEvents = [];
3036
+ let excludedEventCount = 0;
3037
+ let unattributedEventCount = 0;
3038
+ for (const event of events) {
3039
+ if (!event.repoRoot) {
3040
+ unattributedEventCount += 1;
3041
+ continue;
3042
+ }
3043
+ const eventRepoRoot = event.repoRoot;
3044
+ const cachedRootPromise = rootCache.get(eventRepoRoot) ?? (async () => {
3045
+ const resolvedRoot2 = await resolveRepoRoot3(eventRepoRoot).catch(() => void 0);
3046
+ if (!resolvedRoot2) {
3047
+ return void 0;
3048
+ }
3049
+ return {
3050
+ resolvedRoot: resolvedRoot2,
3051
+ comparableRoot: await resolveComparablePath(resolvedRoot2)
3052
+ };
3053
+ })();
3054
+ rootCache.set(eventRepoRoot, cachedRootPromise);
3055
+ const resolvedRoot = await cachedRootPromise;
3056
+ if (!resolvedRoot) {
3057
+ unattributedEventCount += 1;
3058
+ continue;
1980
3059
  }
3060
+ if (resolvedRoot.comparableRoot !== targetRepoPath) {
3061
+ excludedEventCount += 1;
3062
+ continue;
3063
+ }
3064
+ matchedEvents.push({
3065
+ ...event,
3066
+ repoRoot: resolvedRoot.resolvedRoot
3067
+ });
1981
3068
  }
1982
- const finalizedGrandTotals = events.length === 0 && grandTotals.costUsd === void 0 && grandTotals.costIncomplete !== true ? { ...grandTotals, costUsd: 0 } : grandTotals;
1983
- const grandTotalRow = {
1984
- rowType: "grand_total",
1985
- periodKey: "ALL",
1986
- source: "combined",
1987
- models: normalizeModelList(grandModelTotals.keys()),
1988
- modelBreakdown: toModelUsageBreakdown(grandModelTotals),
1989
- ...finalizedGrandTotals
3069
+ return {
3070
+ matchedEvents,
3071
+ matchedEventCount: matchedEvents.length,
3072
+ excludedEventCount,
3073
+ unattributedEventCount
1990
3074
  };
1991
- rows.push(grandTotalRow);
1992
- return rows;
1993
3075
  }
1994
3076
 
1995
3077
  // src/config/env-var-display.ts
@@ -2055,12 +3137,14 @@ function buildUsageDiagnostics(params) {
2055
3137
  sourceFailures: params.sourceFailures,
2056
3138
  skippedRows,
2057
3139
  pricingOrigin: params.pricingOrigin,
3140
+ pricingWarning: params.pricingWarning,
2058
3141
  activeEnvOverrides: params.activeEnvOverrides,
2059
3142
  timezone: params.timezone
2060
3143
  };
2061
3144
  }
2062
- function assembleUsageDataResult(rows, diagnostics) {
3145
+ function assembleUsageDataResult(events, rows, diagnostics) {
2063
3146
  return {
3147
+ events,
2064
3148
  rows,
2065
3149
  diagnostics
2066
3150
  };
@@ -2121,7 +3205,7 @@ function validateSourceFilterValues(sourceFilter, availableSourceIds) {
2121
3205
  if (unknownSources.length === 0) {
2122
3206
  return;
2123
3207
  }
2124
- const allowedSources = [...availableSourceIds].sort((left, right) => left.localeCompare(right));
3208
+ const allowedSources = [...availableSourceIds].sort(compareByCodePoint);
2125
3209
  throw new Error(
2126
3210
  `Unknown --source value(s): ${unknownSources.join(", ")}. Allowed values: ${allowedSources.join(", ")}`
2127
3211
  );
@@ -2196,10 +3280,19 @@ function resolveExplicitSourceIds(options, sourceFilter) {
2196
3280
  }
2197
3281
  return explicitSourceIds;
2198
3282
  }
3283
+ function detectDefaultTimezone() {
3284
+ const detectedTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
3285
+ if (typeof detectedTimezone === "string") {
3286
+ const trimmedDetectedTimezone = detectedTimezone.trim();
3287
+ if (trimmedDetectedTimezone.length > 0) {
3288
+ return trimmedDetectedTimezone;
3289
+ }
3290
+ }
3291
+ return "UTC";
3292
+ }
2199
3293
  function normalizeBuildUsageInputs(options) {
2200
3294
  const { normalizedPricingUrl } = validateBuildOptions(options);
2201
- const timezoneInput = options.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
2202
- const timezone = timezoneInput.trim();
3295
+ const timezone = options.timezone !== void 0 ? options.timezone.trim() : detectDefaultTimezone();
2203
3296
  validateTimezone(timezone);
2204
3297
  const providerFilter = normalizeProviderFilter(options.provider);
2205
3298
  const sourceFilter = normalizeSourceFilter(options.source);
@@ -2221,7 +3314,7 @@ function selectAdaptersForParsing(adapters, sourceFilter) {
2221
3314
  }
2222
3315
 
2223
3316
  // src/cli/build-usage-data-parsing.ts
2224
- import { stat } from "fs/promises";
3317
+ import { stat as stat3 } from "fs/promises";
2225
3318
 
2226
3319
  // src/cli/normalize-skipped-row-reasons.ts
2227
3320
  function toPositiveInteger(value) {
@@ -2249,13 +3342,16 @@ function normalizeSkippedRowReasons(value) {
2249
3342
  }
2250
3343
 
2251
3344
  // src/cli/parse-file-cache.ts
2252
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
2253
- import path7 from "path";
2254
- var PARSE_FILE_CACHE_VERSION = 1;
3345
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
3346
+ import path10 from "path";
3347
+ var PARSE_FILE_CACHE_VERSION = 2;
2255
3348
  var CACHE_KEY_SEPARATOR = "\0";
2256
3349
  function createCacheKey(source, filePath) {
2257
3350
  return `${source}${CACHE_KEY_SEPARATOR}${filePath}`;
2258
3351
  }
3352
+ function normalizeCacheSource(source) {
3353
+ return normalizeSourceId(source)?.toLowerCase() ?? "";
3354
+ }
2259
3355
  function toNonNegativeInteger(value) {
2260
3356
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
2261
3357
  return void 0;
@@ -2268,20 +3364,32 @@ function toNonNegativeNumber2(value) {
2268
3364
  }
2269
3365
  return value;
2270
3366
  }
3367
+ function normalizeCachedTimestamp(value) {
3368
+ if (typeof value !== "string") {
3369
+ return void 0;
3370
+ }
3371
+ const normalized = value.trim();
3372
+ if (!normalized) {
3373
+ return void 0;
3374
+ }
3375
+ const timestamp = new Date(normalized);
3376
+ if (Number.isNaN(timestamp.getTime())) {
3377
+ return void 0;
3378
+ }
3379
+ return timestamp.toISOString() === normalized ? normalized : void 0;
3380
+ }
2271
3381
  function normalizeCachedUsageEvent(value) {
2272
3382
  const record = asRecord(value);
2273
3383
  if (!record) {
2274
3384
  return void 0;
2275
3385
  }
2276
- const source = normalizeSourceId(record.source);
3386
+ const source = normalizeSourceId(record.source)?.toLowerCase();
2277
3387
  const sessionId = typeof record.sessionId === "string" ? record.sessionId.trim() : "";
2278
- const timestamp = typeof record.timestamp === "string" ? record.timestamp.trim() : "";
3388
+ const timestamp = normalizeCachedTimestamp(record.timestamp);
3389
+ const repoRoot = typeof record.repoRoot === "string" ? record.repoRoot.trim() : "";
2279
3390
  if (!source || !sessionId || !timestamp) {
2280
3391
  return void 0;
2281
3392
  }
2282
- if (Number.isNaN(new Date(timestamp).getTime())) {
2283
- return void 0;
2284
- }
2285
3393
  const costMode = record.costMode === "explicit" || record.costMode === "estimated" ? record.costMode : void 0;
2286
3394
  if (!costMode) {
2287
3395
  return void 0;
@@ -2296,7 +3404,7 @@ function normalizeCachedUsageEvent(value) {
2296
3404
  return void 0;
2297
3405
  }
2298
3406
  const provider = typeof record.provider === "string" ? record.provider.trim() : "";
2299
- const model = typeof record.model === "string" ? record.model.trim() : "";
3407
+ const model = typeof record.model === "string" ? record.model.trim().toLowerCase() : "";
2300
3408
  const costUsd = toNonNegativeNumber2(record.costUsd);
2301
3409
  if (costMode === "explicit" && costUsd === void 0) {
2302
3410
  return void 0;
@@ -2305,6 +3413,7 @@ function normalizeCachedUsageEvent(value) {
2305
3413
  source,
2306
3414
  sessionId,
2307
3415
  timestamp,
3416
+ repoRoot: repoRoot || void 0,
2308
3417
  provider: provider || void 0,
2309
3418
  model: model || void 0,
2310
3419
  inputTokens,
@@ -2324,7 +3433,7 @@ function cloneUsageEvents(events) {
2324
3433
  return events.map((event) => cloneUsageEvent(event));
2325
3434
  }
2326
3435
  function cloneSkippedRowReasons(skippedRowReasons) {
2327
- return (skippedRowReasons ?? []).map((stat2) => ({ reason: stat2.reason, count: stat2.count }));
3436
+ return (skippedRowReasons ?? []).map((stat4) => ({ reason: stat4.reason, count: stat4.count }));
2328
3437
  }
2329
3438
  function normalizeCachedEvents(value) {
2330
3439
  if (!Array.isArray(value)) {
@@ -2345,7 +3454,7 @@ function normalizeCacheEntry(value) {
2345
3454
  if (!record) {
2346
3455
  return void 0;
2347
3456
  }
2348
- const source = typeof record.source === "string" ? record.source.trim() : "";
3457
+ const source = normalizeSourceId(record.source)?.toLowerCase() ?? "";
2349
3458
  const filePath = typeof record.filePath === "string" ? record.filePath.trim() : "";
2350
3459
  const cachedAt = toNonNegativeInteger(record.cachedAt);
2351
3460
  const fingerprint = asRecord(record.fingerprint);
@@ -2373,7 +3482,7 @@ function normalizeCacheEntry(value) {
2373
3482
  };
2374
3483
  }
2375
3484
  function getDefaultParseFileCachePath() {
2376
- return path7.join(getUserCacheRootDir(), "llm-usage-metrics", "parse-file-cache.json");
3485
+ return path10.join(getUserCacheRootDir(), "llm-usage-metrics", "parse-file-cache.json");
2377
3486
  }
2378
3487
  var ParseFileCache = class _ParseFileCache {
2379
3488
  constructor(cacheFilePath, limits, now) {
@@ -2393,16 +3502,20 @@ var ParseFileCache = class _ParseFileCache {
2393
3502
  return cache;
2394
3503
  }
2395
3504
  get(source, filePath, fingerprint) {
2396
- const entry = this.entriesByKey.get(createCacheKey(source, filePath));
3505
+ const normalizedSource = normalizeCacheSource(source);
3506
+ const cacheKey = createCacheKey(normalizedSource, filePath);
3507
+ const entry = this.entriesByKey.get(cacheKey);
2397
3508
  if (!entry) {
2398
3509
  return void 0;
2399
3510
  }
2400
3511
  if (entry.cachedAt + this.limits.ttlMs < this.now()) {
2401
- this.entriesByKey.delete(createCacheKey(source, filePath));
3512
+ this.entriesByKey.delete(cacheKey);
2402
3513
  this.dirty = true;
2403
3514
  return void 0;
2404
3515
  }
2405
3516
  if (entry.fingerprint.size !== fingerprint.size || entry.fingerprint.mtimeMs !== fingerprint.mtimeMs) {
3517
+ this.entriesByKey.delete(cacheKey);
3518
+ this.dirty = true;
2406
3519
  return void 0;
2407
3520
  }
2408
3521
  return {
@@ -2412,8 +3525,9 @@ var ParseFileCache = class _ParseFileCache {
2412
3525
  };
2413
3526
  }
2414
3527
  set(source, filePath, fingerprint, diagnostics) {
2415
- this.entriesByKey.set(createCacheKey(source, filePath), {
2416
- source,
3528
+ const normalizedSource = normalizeCacheSource(source);
3529
+ this.entriesByKey.set(createCacheKey(normalizedSource, filePath), {
3530
+ source: normalizedSource,
2417
3531
  filePath,
2418
3532
  fingerprint: {
2419
3533
  size: fingerprint.size,
@@ -2454,7 +3568,7 @@ var ParseFileCache = class _ParseFileCache {
2454
3568
  keptEntries.length = bestCount;
2455
3569
  payloadText = bestPayloadText;
2456
3570
  }
2457
- await mkdir2(path7.dirname(this.cacheFilePath), { recursive: true });
3571
+ await mkdir2(path10.dirname(this.cacheFilePath), { recursive: true });
2458
3572
  await writeFile2(this.cacheFilePath, payloadText, "utf8");
2459
3573
  this.dirty = false;
2460
3574
  }
@@ -2477,7 +3591,7 @@ var ParseFileCache = class _ParseFileCache {
2477
3591
  async loadFromDisk() {
2478
3592
  let content;
2479
3593
  try {
2480
- content = await readFile2(this.cacheFilePath, "utf8");
3594
+ content = await readFile3(this.cacheFilePath, "utf8");
2481
3595
  } catch {
2482
3596
  return;
2483
3597
  }
@@ -2559,7 +3673,7 @@ async function parseAdapterEvents(adapter, maxParallelFileParsing, parseFileCach
2559
3673
  let parseFileDiagnostics;
2560
3674
  if (parseFileCache) {
2561
3675
  try {
2562
- const fileStat = await stat(filePath);
3676
+ const fileStat = await stat3(filePath);
2563
3677
  fileFingerprint = {
2564
3678
  size: fileStat.size,
2565
3679
  mtimeMs: fileStat.mtimeMs
@@ -2651,6 +3765,16 @@ function matchesProvider(provider, providerFilter) {
2651
3765
  }
2652
3766
  return provider?.toLowerCase().includes(providerFilter) ?? false;
2653
3767
  }
3768
+ function isEventWithinDateRange(event, timezone, since, until) {
3769
+ const eventDate = getPeriodKey(event.timestamp, "daily", timezone);
3770
+ if (since && eventDate < since) {
3771
+ return false;
3772
+ }
3773
+ if (until && eventDate > until) {
3774
+ return false;
3775
+ }
3776
+ return true;
3777
+ }
2654
3778
  function resolveModelFilterRules(events, modelFilter) {
2655
3779
  if (!modelFilter || modelFilter.length === 0) {
2656
3780
  return void 0;
@@ -2675,41 +3799,24 @@ function matchesModel(model, modelRules) {
2675
3799
  (rule) => rule.mode === "exact" ? normalizedModel === rule.value : normalizedModel.includes(rule.value)
2676
3800
  );
2677
3801
  }
2678
- function filterEventsByDateRange(events, timezone, since, until) {
2679
- return events.filter((event) => {
2680
- const eventDate = getPeriodKey(event.timestamp, "daily", timezone);
2681
- if (since && eventDate < since) {
2682
- return false;
2683
- }
2684
- if (until && eventDate > until) {
2685
- return false;
2686
- }
2687
- return true;
2688
- });
2689
- }
2690
- function filterUsageEvents(events, options) {
2691
- const providerFilteredEvents = events.filter(
2692
- (event) => matchesProvider(event.provider, options.providerFilter)
2693
- );
2694
- const providerAndDateFilteredEvents = filterEventsByDateRange(
2695
- providerFilteredEvents,
2696
- options.timezone,
2697
- options.since,
2698
- options.until
2699
- );
2700
- const modelFilterRules = resolveModelFilterRules(
2701
- providerAndDateFilteredEvents,
2702
- options.modelFilter
2703
- );
2704
- return providerAndDateFilteredEvents.filter(
2705
- (event) => matchesModel(event.model, modelFilterRules)
2706
- );
3802
+ function filterByModelRules(events, modelFilter) {
3803
+ const modelFilterRules = resolveModelFilterRules(events, modelFilter);
3804
+ return events.filter((event) => matchesModel(event.model, modelFilterRules));
2707
3805
  }
2708
3806
  function filterParsedAdapterEvents(parseResults, options) {
2709
- return filterUsageEvents(
2710
- parseResults.flatMap((result) => result.events),
2711
- options
2712
- );
3807
+ const providerAndDateFilteredEvents = [];
3808
+ for (const result of parseResults) {
3809
+ for (const event of result.events) {
3810
+ if (!matchesProvider(event.provider, options.providerFilter)) {
3811
+ continue;
3812
+ }
3813
+ if (!isEventWithinDateRange(event, options.timezone, options.since, options.until)) {
3814
+ continue;
3815
+ }
3816
+ providerAndDateFilteredEvents.push(event);
3817
+ }
3818
+ }
3819
+ return filterByModelRules(providerAndDateFilteredEvents, options.modelFilter);
2713
3820
  }
2714
3821
 
2715
3822
  // src/pricing/cost-engine.ts
@@ -2755,8 +3862,8 @@ function applyPricingToEvents(events, pricingSource) {
2755
3862
  }
2756
3863
 
2757
3864
  // src/pricing/litellm-pricing-fetcher.ts
2758
- import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
2759
- import path8 from "path";
3865
+ import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
3866
+ import path11 from "path";
2760
3867
 
2761
3868
  // src/pricing/litellm-model-map.json
2762
3869
  var litellm_model_map_default = {
@@ -2764,20 +3871,34 @@ var litellm_model_map_default = {
2764
3871
  k2p5: "kimi-k2.5",
2765
3872
  "kimi-k2p5": "kimi-k2.5",
2766
3873
  "kimi-k2.5": "kimi-k2.5",
3874
+ "kimi-k2.5-free": "kimi-k2.5",
2767
3875
  "moonshotai.kimi-k2.5": "kimi-k2.5",
2768
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",
2769
3886
  "claude sonnet 4.6": "claude-sonnet-4.6",
2770
3887
  "claude-sonnet-4.6": "claude-sonnet-4.6",
2771
3888
  "claude-sonnet-4-6": "claude-sonnet-4.6",
2772
3889
  "anthropic/claude-sonnet-4.6": "claude-sonnet-4.6",
2773
- "anthropic.claude-sonnet-4-6": "claude-sonnet-4.6",
2774
- "gpt-5.3-codex": "gpt-5.2-codex"
3890
+ "anthropic.claude-sonnet-4-6": "claude-sonnet-4.6"
2775
3891
  },
2776
3892
  notes: {
2777
- "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"
2778
3894
  },
2779
3895
  preferredPricingKeyByCanonicalModel: {
2780
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",
2781
3902
  "claude-sonnet-4.6": "anthropic.claude-sonnet-4-6"
2782
3903
  }
2783
3904
  };
@@ -2926,7 +4047,7 @@ function normalizeLitellmPricingPayload(payload) {
2926
4047
  return normalizedPricing;
2927
4048
  }
2928
4049
  function getDefaultLiteLLMPricingCachePath() {
2929
- return path8.join(getUserCacheRootDir(), "llm-usage-metrics", "litellm-pricing-cache.json");
4050
+ return path11.join(getUserCacheRootDir(), "llm-usage-metrics", "litellm-pricing-cache.json");
2930
4051
  }
2931
4052
  function stripProviderPrefix(model) {
2932
4053
  const slashIndex = model.lastIndexOf("/");
@@ -3133,7 +4254,7 @@ var LiteLLMPricingFetcher = class {
3133
4254
  if (!isProviderPrefixedMatch) {
3134
4255
  continue;
3135
4256
  }
3136
- if (!bestMatch || modelName.length < bestMatch.length || modelName.length === bestMatch.length && modelName.localeCompare(bestMatch) < 0) {
4257
+ if (!bestMatch || modelName.length < bestMatch.length || modelName.length === bestMatch.length && compareByCodePoint(modelName, bestMatch) < 0) {
3137
4258
  bestMatch = modelName;
3138
4259
  }
3139
4260
  }
@@ -3151,7 +4272,7 @@ var LiteLLMPricingFetcher = class {
3151
4272
  if (!isPrefixModelMatch(candidate, modelName)) {
3152
4273
  continue;
3153
4274
  }
3154
- if (!bestMatch || modelName.length > bestMatch.length || modelName.length === bestMatch.length && modelName.localeCompare(bestMatch) < 0) {
4275
+ if (!bestMatch || modelName.length > bestMatch.length || modelName.length === bestMatch.length && compareByCodePoint(modelName, bestMatch) < 0) {
3155
4276
  bestMatch = modelName;
3156
4277
  }
3157
4278
  }
@@ -3285,7 +4406,7 @@ var LiteLLMPricingFetcher = class {
3285
4406
  async readCachePayload() {
3286
4407
  let content;
3287
4408
  try {
3288
- content = await readFile3(this.cacheFilePath, "utf8");
4409
+ content = await readFile4(this.cacheFilePath, "utf8");
3289
4410
  } catch {
3290
4411
  return void 0;
3291
4412
  }
@@ -3320,7 +4441,7 @@ var LiteLLMPricingFetcher = class {
3320
4441
  };
3321
4442
  }
3322
4443
  async writeCache() {
3323
- const directoryPath = path8.dirname(this.cacheFilePath);
4444
+ const directoryPath = path11.dirname(this.cacheFilePath);
3324
4445
  await mkdir3(directoryPath, { recursive: true });
3325
4446
  const payload = {
3326
4447
  fetchedAt: this.now(),
@@ -3383,7 +4504,21 @@ async function resolveAndApplyPricingToEvents(events, options, runtimeConfig = g
3383
4504
  pricingOrigin
3384
4505
  };
3385
4506
  }
3386
- const pricingResult = await loadPricingSource(options, runtimeConfig);
4507
+ let pricingResult;
4508
+ try {
4509
+ pricingResult = await loadPricingSource(options, runtimeConfig);
4510
+ } catch (error) {
4511
+ if (!options.ignorePricingFailures) {
4512
+ throw error;
4513
+ }
4514
+ const reason = error instanceof Error ? error.message : String(error);
4515
+ const pricingWarning = reason.trim().startsWith("Could not load") ? reason : `Could not load pricing; continuing without estimated costs: ${reason}`;
4516
+ return {
4517
+ pricedEvents: events,
4518
+ pricingOrigin,
4519
+ pricingWarning
4520
+ };
4521
+ }
3387
4522
  pricingOrigin = pricingResult.origin;
3388
4523
  return {
3389
4524
  pricedEvents: applyPricingToEvents(events, pricingResult.source),
@@ -3433,7 +4568,7 @@ async function buildUsageData(granularity, options, deps = {}) {
3433
4568
  modelFilter: normalizedInputs.modelFilter
3434
4569
  });
3435
4570
  const pricingOptions = withNormalizedPricingUrl(options, normalizedInputs.pricingUrl);
3436
- const { pricedEvents, pricingOrigin } = await resolveAndApplyPricingToEvents(
4571
+ const { pricedEvents, pricingOrigin, pricingWarning } = await resolveAndApplyPricingToEvents(
3437
4572
  filteredEvents,
3438
4573
  pricingOptions,
3439
4574
  pricingRuntimeConfig,
@@ -3449,10 +4584,114 @@ async function buildUsageData(granularity, options, deps = {}) {
3449
4584
  successfulParseResults,
3450
4585
  sourceFailures,
3451
4586
  pricingOrigin,
4587
+ pricingWarning,
3452
4588
  activeEnvOverrides: readEnvVarOverrides(),
3453
4589
  timezone: normalizedInputs.timezone
3454
4590
  });
3455
- return assembleUsageDataResult(rows, diagnostics);
4591
+ return assembleUsageDataResult(pricedEvents, rows, diagnostics);
4592
+ }
4593
+
4594
+ // src/cli/build-efficiency-data.ts
4595
+ function hasActiveRepeatedFilter(value) {
4596
+ if (!value) {
4597
+ return false;
4598
+ }
4599
+ const values = Array.isArray(value) ? value : [value];
4600
+ return values.some(
4601
+ (entry) => entry.split(",").map((candidate) => candidate.trim()).some((candidate) => candidate.length > 0)
4602
+ );
4603
+ }
4604
+ function hasActiveProviderFilter(provider) {
4605
+ return Boolean(provider?.trim());
4606
+ }
4607
+ function hasActiveTextOption(value) {
4608
+ return Boolean(value?.trim());
4609
+ }
4610
+ function resolveScopeNote(options) {
4611
+ const activeFilters = [];
4612
+ if (hasActiveTextOption(options.piDir)) {
4613
+ activeFilters.push("--pi-dir");
4614
+ }
4615
+ if (hasActiveTextOption(options.codexDir)) {
4616
+ activeFilters.push("--codex-dir");
4617
+ }
4618
+ if (hasActiveTextOption(options.opencodeDb)) {
4619
+ activeFilters.push("--opencode-db");
4620
+ }
4621
+ if (hasActiveRepeatedFilter(options.sourceDir)) {
4622
+ activeFilters.push("--source-dir");
4623
+ }
4624
+ if (hasActiveRepeatedFilter(options.source)) {
4625
+ activeFilters.push("--source");
4626
+ }
4627
+ if (hasActiveProviderFilter(options.provider)) {
4628
+ activeFilters.push("--provider");
4629
+ }
4630
+ if (hasActiveRepeatedFilter(options.model)) {
4631
+ activeFilters.push("--model");
4632
+ }
4633
+ if (activeFilters.length === 0) {
4634
+ return void 0;
4635
+ }
4636
+ return `Usage filters (${activeFilters.join(", ")}) affect commit attribution too: only commit days with matching repo-attributed usage events are counted.`;
4637
+ }
4638
+ function hasMeaningfulEfficiencyUsageSignal(event) {
4639
+ return event.totalTokens > 0 || event.costUsd !== void 0 && event.costUsd > 0;
4640
+ }
4641
+ async function buildEfficiencyData(granularity, options, deps = {}) {
4642
+ const buildUsage = deps.buildUsageData ?? buildUsageData;
4643
+ const collectOutcomes = deps.collectGitOutcomes ?? collectGitOutcomes;
4644
+ const resolveRepoRoot3 = deps.resolveRepoRoot ?? resolveRepoRootFromPathHint;
4645
+ const repoDir = options.repoDir?.trim();
4646
+ if (options.repoDir !== void 0 && !repoDir) {
4647
+ throw new Error("--repo-dir must be a non-empty path");
4648
+ }
4649
+ const usageData = await buildUsage(granularity, options);
4650
+ const attribution = await attributeUsageEventsToRepo(
4651
+ usageData.events,
4652
+ repoDir ?? process.cwd(),
4653
+ resolveRepoRoot3
4654
+ );
4655
+ const matchedEventsWithSignal = attribution.matchedEvents.filter(
4656
+ (event) => hasMeaningfulEfficiencyUsageSignal(event)
4657
+ );
4658
+ const activeUsageDays = new Set(
4659
+ matchedEventsWithSignal.map(
4660
+ (event) => getPeriodKey(event.timestamp, "daily", usageData.diagnostics.timezone)
4661
+ )
4662
+ );
4663
+ const gitOutcomes = await collectOutcomes({
4664
+ repoDir,
4665
+ granularity,
4666
+ timezone: usageData.diagnostics.timezone,
4667
+ since: options.since,
4668
+ until: options.until,
4669
+ includeMergeCommits: options.includeMergeCommits,
4670
+ activeUsageDays
4671
+ });
4672
+ const repoScopedUsageRows = aggregateUsage(matchedEventsWithSignal, {
4673
+ granularity,
4674
+ timezone: usageData.diagnostics.timezone
4675
+ });
4676
+ const rows = aggregateEfficiency({
4677
+ usageRows: repoScopedUsageRows,
4678
+ periodOutcomes: gitOutcomes.periodOutcomes
4679
+ });
4680
+ return {
4681
+ rows,
4682
+ diagnostics: {
4683
+ usage: usageData.diagnostics,
4684
+ repoDir: gitOutcomes.diagnostics.repoDir,
4685
+ includeMergeCommits: gitOutcomes.diagnostics.includeMergeCommits,
4686
+ gitCommitCount: gitOutcomes.diagnostics.commitsCollected,
4687
+ gitLinesAdded: gitOutcomes.diagnostics.linesAdded,
4688
+ gitLinesDeleted: gitOutcomes.diagnostics.linesDeleted,
4689
+ repoMatchedUsageEvents: attribution.matchedEventCount,
4690
+ repoExcludedUsageEvents: attribution.excludedEventCount,
4691
+ repoUnattributedUsageEvents: attribution.unattributedEventCount,
4692
+ scopeNote: resolveScopeNote(options)
4693
+ }
4694
+ };
3456
4695
  }
3457
4696
 
3458
4697
  // src/utils/logger.ts
@@ -3523,130 +4762,36 @@ function emitDiagnostics(diagnostics, diagnosticsLogger = logger) {
3523
4762
  switch (diagnostics.pricingOrigin) {
3524
4763
  case "offline-cache":
3525
4764
  diagnosticsLogger.info("Using cached pricing (offline mode)");
3526
- return;
4765
+ break;
3527
4766
  case "cache":
3528
4767
  diagnosticsLogger.info("Loaded pricing from cache");
3529
- return;
4768
+ break;
3530
4769
  case "network":
3531
4770
  diagnosticsLogger.info("Fetched pricing from LiteLLM");
3532
- return;
4771
+ break;
3533
4772
  case "none":
3534
- return;
3535
- }
3536
- }
3537
-
3538
- // src/render/markdown-table.ts
3539
- import { markdownTable } from "markdown-table";
3540
-
3541
- // src/render/row-cells.ts
3542
- var usageTableHeaders = [
3543
- "Period",
3544
- "Source",
3545
- "Models",
3546
- "Input",
3547
- "Output",
3548
- "Reasoning",
3549
- "Cache Read",
3550
- "Cache Write",
3551
- "Total",
3552
- "Cost"
3553
- ];
3554
- var integerFormatter = new Intl.NumberFormat("en-US");
3555
- var usdFormatter = new Intl.NumberFormat("en-US", {
3556
- style: "currency",
3557
- currency: "USD",
3558
- minimumFractionDigits: 2,
3559
- maximumFractionDigits: 2
3560
- });
3561
- function formatSource(row) {
3562
- if (row.rowType === "grand_total") {
3563
- return "TOTAL";
3564
- }
3565
- return row.source;
3566
- }
3567
- function formatTokenCount(value) {
3568
- return integerFormatter.format(value ?? 0);
3569
- }
3570
- function formatUsd(value, options = {}) {
3571
- if (value === void 0) {
3572
- return "-";
3573
- }
3574
- const formattedUsd = usdFormatter.format(value);
3575
- return options.incomplete ? `~${formattedUsd}` : formattedUsd;
3576
- }
3577
- function buildModelLines(row) {
3578
- if (row.modelBreakdown.length > 0) {
3579
- return row.modelBreakdown.map((modelUsage) => `\u2022 ${modelUsage.model}`);
3580
- }
3581
- return row.models.map((model) => `\u2022 ${model}`);
3582
- }
3583
- function formatModels(row, layout) {
3584
- const modelLines = buildModelLines(row);
3585
- if (modelLines.length === 0) {
3586
- return "-";
4773
+ break;
3587
4774
  }
3588
- if (layout === "per_model_columns" && row.modelBreakdown.length > 1) {
3589
- return [...modelLines, "\u03A3 TOTAL"].join("\n");
4775
+ if (diagnostics.pricingWarning) {
4776
+ diagnosticsLogger.warn(diagnostics.pricingWarning);
3590
4777
  }
3591
- return modelLines.join("\n");
3592
4778
  }
3593
- function formatModelMetric(row, selector, formatter, layout) {
3594
- if (layout !== "per_model_columns" || row.modelBreakdown.length === 0) {
3595
- return formatter(selector(row));
3596
- }
3597
- const lines = row.modelBreakdown.map((modelUsage) => formatter(selector(modelUsage)));
3598
- if (row.modelBreakdown.length > 1) {
3599
- lines.push(formatter(selector(row)));
4779
+
4780
+ // src/cli/emit-env-var-overrides.ts
4781
+ function emitEnvVarOverrides(activeEnvOverrides, diagnosticsLogger) {
4782
+ const envVarOverrideLines = formatEnvVarOverrides(activeEnvOverrides);
4783
+ if (envVarOverrideLines.length === 0) {
4784
+ return;
3600
4785
  }
3601
- return lines.join("\n");
3602
- }
3603
- function formatModelCostMetric(row, layout) {
3604
- if (layout !== "per_model_columns" || row.modelBreakdown.length === 0) {
3605
- return formatUsd(row.costUsd, { incomplete: row.costIncomplete });
4786
+ const [headerLine, ...envVarLines] = envVarOverrideLines;
4787
+ if (headerLine) {
4788
+ diagnosticsLogger.info(headerLine);
3606
4789
  }
3607
- const lines = row.modelBreakdown.map(
3608
- (modelUsage) => formatUsd(modelUsage.costUsd, { incomplete: modelUsage.costIncomplete })
3609
- );
3610
- if (row.modelBreakdown.length > 1) {
3611
- lines.push(formatUsd(row.costUsd, { incomplete: row.costIncomplete }));
4790
+ for (const envVarLine of envVarLines) {
4791
+ diagnosticsLogger.dim(envVarLine);
3612
4792
  }
3613
- return lines.join("\n");
3614
- }
3615
- function toUsageTableCells(rows, options = {}) {
3616
- const layout = options.layout ?? "compact";
3617
- return rows.map((row) => [
3618
- row.periodKey,
3619
- formatSource(row),
3620
- formatModels(row, layout),
3621
- formatModelMetric(row, (value) => value.inputTokens, formatTokenCount, layout),
3622
- formatModelMetric(row, (value) => value.outputTokens, formatTokenCount, layout),
3623
- formatModelMetric(row, (value) => value.reasoningTokens, formatTokenCount, layout),
3624
- formatModelMetric(row, (value) => value.cacheReadTokens, formatTokenCount, layout),
3625
- formatModelMetric(row, (value) => value.cacheWriteTokens, formatTokenCount, layout),
3626
- formatModelMetric(row, (value) => value.totalTokens, formatTokenCount, layout),
3627
- formatModelCostMetric(row, layout)
3628
- ]);
3629
- }
3630
-
3631
- // src/render/markdown-table.ts
3632
- var alignment = ["l", "l", "l", "r", "r", "r", "r", "r", "r", "r"];
3633
- function toMarkdownSafeCell(value) {
3634
- return value.replace(/\r?\n/gu, "<br>");
3635
- }
3636
- function renderMarkdownTable(rows, options = {}) {
3637
- const tableLayout = options.tableLayout ?? "compact";
3638
- const bodyRows = toUsageTableCells(rows, { layout: tableLayout }).map(
3639
- (row) => row.map((cell) => toMarkdownSafeCell(cell))
3640
- );
3641
- const tableRows = [Array.from(usageTableHeaders), ...bodyRows];
3642
- return markdownTable(tableRows, {
3643
- align: alignment
3644
- });
3645
4793
  }
3646
4794
 
3647
- // src/render/report-header.ts
3648
- import pc2 from "picocolors";
3649
-
3650
4795
  // src/render/table-text-layout.ts
3651
4796
  var ansiEscapePattern = new RegExp(String.raw`\u001B\[[0-9;]*m`, "gu");
3652
4797
  var combiningMarkPattern = /\p{Mark}/u;
@@ -3811,16 +4956,50 @@ function wrapTableColumn(rows, options) {
3811
4956
  if (options.width <= 0) {
3812
4957
  throw new RangeError("wrapTableColumn width must be greater than 0");
3813
4958
  }
3814
- return rows.map((row) => {
3815
- const wrappedRow = [...row];
3816
- const cell = wrappedRow[options.columnIndex] ?? "";
3817
- const wrappedLines = splitCellLines(cell).flatMap((line) => wrapPlainLine(line, options.width));
3818
- wrappedRow[options.columnIndex] = wrappedLines.join("\n");
3819
- return wrappedRow;
3820
- });
4959
+ return rows.map((row) => {
4960
+ const wrappedRow = [...row];
4961
+ const cell = wrappedRow[options.columnIndex] ?? "";
4962
+ const wrappedLines = splitCellLines(cell).flatMap((line) => wrapPlainLine(line, options.width));
4963
+ wrappedRow[options.columnIndex] = wrappedLines.join("\n");
4964
+ return wrappedRow;
4965
+ });
4966
+ }
4967
+
4968
+ // src/cli/terminal-overflow-warning.ts
4969
+ function detectTerminalOverflowColumns(reportOutput, stdoutState) {
4970
+ const terminalColumns = resolveTtyColumns(stdoutState);
4971
+ if (terminalColumns === void 0) {
4972
+ return void 0;
4973
+ }
4974
+ const allLines = reportOutput.trimEnd().split("\n");
4975
+ const tableLikeLinePattern = /[│╭╮╰╯├┼┬┴┌┐└┘]|^\s*\|.*\|\s*$/u;
4976
+ const tableLines = allLines.filter((line) => tableLikeLinePattern.test(line));
4977
+ if (tableLines.length === 0) {
4978
+ return void 0;
4979
+ }
4980
+ const maxLineWidth = tableLines.reduce(
4981
+ (maxWidth, line) => Math.max(maxWidth, visibleWidth(line)),
4982
+ 0
4983
+ );
4984
+ if (maxLineWidth <= terminalColumns) {
4985
+ return void 0;
4986
+ }
4987
+ return maxLineWidth - terminalColumns;
4988
+ }
4989
+ function warnIfTerminalTableOverflows(reportOutput, warn, stdoutState = process.stdout) {
4990
+ const overflowColumns = detectTerminalOverflowColumns(reportOutput, stdoutState);
4991
+ if (overflowColumns !== void 0) {
4992
+ warn(
4993
+ `Report table is wider than terminal by ${overflowColumns} column(s). Use fullscreen/maximized terminal for better readability.`
4994
+ );
4995
+ }
3821
4996
  }
3822
4997
 
4998
+ // src/render/render-efficiency-report.ts
4999
+ import { markdownTable } from "markdown-table";
5000
+
3823
5001
  // src/render/report-header.ts
5002
+ import pc2 from "picocolors";
3824
5003
  function getBoxWidth(content) {
3825
5004
  return visibleWidth(content) + 4;
3826
5005
  }
@@ -3846,6 +5025,89 @@ function renderReportHeader(options) {
3846
5025
  return lines.join("\n");
3847
5026
  }
3848
5027
 
5028
+ // src/render/efficiency-row-cells.ts
5029
+ var efficiencyTableHeaders = [
5030
+ "Period",
5031
+ "Commits",
5032
+ "+Lines",
5033
+ "-Lines",
5034
+ "\u0394Lines",
5035
+ "Input",
5036
+ "Output",
5037
+ "Reasoning",
5038
+ "Cache Read",
5039
+ "Cache Write",
5040
+ "Total",
5041
+ "Cost",
5042
+ "$/Commit",
5043
+ "$/1k Lines",
5044
+ "All Tokens/Commit",
5045
+ "Non-Cache/Commit",
5046
+ "Commits/$"
5047
+ ];
5048
+ var integerFormatter = new Intl.NumberFormat("en-US");
5049
+ var decimalFormatter = new Intl.NumberFormat("en-US", {
5050
+ minimumFractionDigits: 2,
5051
+ maximumFractionDigits: 2
5052
+ });
5053
+ var usdFormatter = new Intl.NumberFormat("en-US", {
5054
+ style: "currency",
5055
+ currency: "USD",
5056
+ minimumFractionDigits: 2,
5057
+ maximumFractionDigits: 2
5058
+ });
5059
+ var usdRateFormatter = new Intl.NumberFormat("en-US", {
5060
+ style: "currency",
5061
+ currency: "USD",
5062
+ minimumFractionDigits: 4,
5063
+ maximumFractionDigits: 4
5064
+ });
5065
+ function formatInteger(value) {
5066
+ return integerFormatter.format(value);
5067
+ }
5068
+ function formatUsd(value, options = {}) {
5069
+ if (value === void 0) {
5070
+ return "-";
5071
+ }
5072
+ const formatted = usdFormatter.format(value);
5073
+ return options.approximate ? `~${formatted}` : formatted;
5074
+ }
5075
+ function formatUsdRate(value, options = {}) {
5076
+ if (value === void 0) {
5077
+ return "-";
5078
+ }
5079
+ const formatted = usdRateFormatter.format(value);
5080
+ return options.approximate ? `~${formatted}` : formatted;
5081
+ }
5082
+ function formatDecimal(value, options = {}) {
5083
+ if (value === void 0) {
5084
+ return "-";
5085
+ }
5086
+ const formatted = decimalFormatter.format(value);
5087
+ return options.approximate ? `~${formatted}` : formatted;
5088
+ }
5089
+ function toEfficiencyTableCells(rows) {
5090
+ return rows.map((row) => [
5091
+ row.periodKey,
5092
+ formatInteger(row.commitCount),
5093
+ formatInteger(row.linesAdded),
5094
+ formatInteger(row.linesDeleted),
5095
+ formatInteger(row.linesChanged),
5096
+ formatInteger(row.inputTokens),
5097
+ formatInteger(row.outputTokens),
5098
+ formatInteger(row.reasoningTokens),
5099
+ formatInteger(row.cacheReadTokens),
5100
+ formatInteger(row.cacheWriteTokens),
5101
+ formatInteger(row.totalTokens),
5102
+ formatUsd(row.costUsd, { approximate: row.costIncomplete }),
5103
+ formatUsdRate(row.usdPerCommit, { approximate: row.costIncomplete }),
5104
+ formatUsdRate(row.usdPer1kLinesChanged, { approximate: row.costIncomplete }),
5105
+ formatDecimal(row.tokensPerCommit),
5106
+ formatDecimal(row.nonCacheTokensPerCommit),
5107
+ formatDecimal(row.commitsPerUsd, { approximate: row.costIncomplete })
5108
+ ]);
5109
+ }
5110
+
3849
5111
  // src/render/terminal-table.ts
3850
5112
  import pc4 from "picocolors";
3851
5113
 
@@ -3924,6 +5186,96 @@ function colorizeUsageBodyRows(bodyRows, rows, options) {
3924
5186
  });
3925
5187
  }
3926
5188
 
5189
+ // src/render/row-cells.ts
5190
+ var usageTableHeaders = [
5191
+ "Period",
5192
+ "Source",
5193
+ "Models",
5194
+ "Input",
5195
+ "Output",
5196
+ "Reasoning",
5197
+ "Cache Read",
5198
+ "Cache Write",
5199
+ "Total",
5200
+ "Cost"
5201
+ ];
5202
+ var integerFormatter2 = new Intl.NumberFormat("en-US");
5203
+ var usdFormatter2 = new Intl.NumberFormat("en-US", {
5204
+ style: "currency",
5205
+ currency: "USD",
5206
+ minimumFractionDigits: 2,
5207
+ maximumFractionDigits: 2
5208
+ });
5209
+ function formatSource(row) {
5210
+ if (row.rowType === "grand_total") {
5211
+ return "TOTAL";
5212
+ }
5213
+ return row.source;
5214
+ }
5215
+ function formatTokenCount(value) {
5216
+ return integerFormatter2.format(value ?? 0);
5217
+ }
5218
+ function formatUsd2(value, options = {}) {
5219
+ if (value === void 0) {
5220
+ return "-";
5221
+ }
5222
+ const formattedUsd = usdFormatter2.format(value);
5223
+ return options.incomplete ? `~${formattedUsd}` : formattedUsd;
5224
+ }
5225
+ function buildModelLines(row) {
5226
+ if (row.modelBreakdown.length > 0) {
5227
+ return row.modelBreakdown.map((modelUsage) => `\u2022 ${modelUsage.model}`);
5228
+ }
5229
+ return row.models.map((model) => `\u2022 ${model}`);
5230
+ }
5231
+ function formatModels(row, layout) {
5232
+ const modelLines = buildModelLines(row);
5233
+ if (modelLines.length === 0) {
5234
+ return "-";
5235
+ }
5236
+ if (layout === "per_model_columns" && row.modelBreakdown.length > 1) {
5237
+ return [...modelLines, "\u03A3 TOTAL"].join("\n");
5238
+ }
5239
+ return modelLines.join("\n");
5240
+ }
5241
+ function formatModelMetric(row, selector, formatter, layout) {
5242
+ if (layout !== "per_model_columns" || row.modelBreakdown.length === 0) {
5243
+ return formatter(selector(row));
5244
+ }
5245
+ const lines = row.modelBreakdown.map((modelUsage) => formatter(selector(modelUsage)));
5246
+ if (row.modelBreakdown.length > 1) {
5247
+ lines.push(formatter(selector(row)));
5248
+ }
5249
+ return lines.join("\n");
5250
+ }
5251
+ function formatModelCostMetric(row, layout) {
5252
+ if (layout !== "per_model_columns" || row.modelBreakdown.length === 0) {
5253
+ return formatUsd2(row.costUsd, { incomplete: row.costIncomplete });
5254
+ }
5255
+ const lines = row.modelBreakdown.map(
5256
+ (modelUsage) => formatUsd2(modelUsage.costUsd, { incomplete: modelUsage.costIncomplete })
5257
+ );
5258
+ if (row.modelBreakdown.length > 1) {
5259
+ lines.push(formatUsd2(row.costUsd, { incomplete: row.costIncomplete }));
5260
+ }
5261
+ return lines.join("\n");
5262
+ }
5263
+ function toUsageTableCells(rows, options = {}) {
5264
+ const layout = options.layout ?? "compact";
5265
+ return rows.map((row) => [
5266
+ row.periodKey,
5267
+ formatSource(row),
5268
+ formatModels(row, layout),
5269
+ formatModelMetric(row, (value) => value.inputTokens, formatTokenCount, layout),
5270
+ formatModelMetric(row, (value) => value.outputTokens, formatTokenCount, layout),
5271
+ formatModelMetric(row, (value) => value.reasoningTokens, formatTokenCount, layout),
5272
+ formatModelMetric(row, (value) => value.cacheReadTokens, formatTokenCount, layout),
5273
+ formatModelMetric(row, (value) => value.cacheWriteTokens, formatTokenCount, layout),
5274
+ formatModelMetric(row, (value) => value.totalTokens, formatTokenCount, layout),
5275
+ formatModelCostMetric(row, layout)
5276
+ ]);
5277
+ }
5278
+
3927
5279
  // src/render/unicode-table.ts
3928
5280
  function getColumnAlignment(columnIndex, modelsColumnIndex2) {
3929
5281
  if (columnIndex <= modelsColumnIndex2) {
@@ -4233,8 +5585,174 @@ function renderTerminalTable(rows, options = {}) {
4233
5585
  return renderedTable;
4234
5586
  }
4235
5587
 
4236
- // src/render/render-usage-report.ts
5588
+ // src/render/render-efficiency-report.ts
4237
5589
  function getReportTitle(granularity) {
5590
+ switch (granularity) {
5591
+ case "daily":
5592
+ return "Daily Efficiency Report";
5593
+ case "weekly":
5594
+ return "Weekly Efficiency Report";
5595
+ case "monthly":
5596
+ return "Monthly Efficiency Report";
5597
+ }
5598
+ }
5599
+ function toTableSortRow(row) {
5600
+ if (row.rowType === "grand_total") {
5601
+ return {
5602
+ rowType: "grand_total",
5603
+ periodKey: "ALL",
5604
+ source: "combined",
5605
+ models: [],
5606
+ modelBreakdown: [],
5607
+ inputTokens: row.inputTokens,
5608
+ outputTokens: row.outputTokens,
5609
+ reasoningTokens: row.reasoningTokens,
5610
+ cacheReadTokens: row.cacheReadTokens,
5611
+ cacheWriteTokens: row.cacheWriteTokens,
5612
+ totalTokens: row.totalTokens,
5613
+ costUsd: row.costUsd,
5614
+ costIncomplete: row.costIncomplete
5615
+ };
5616
+ }
5617
+ return {
5618
+ rowType: "period_source",
5619
+ periodKey: row.periodKey,
5620
+ source: "combined",
5621
+ models: [],
5622
+ modelBreakdown: [],
5623
+ inputTokens: row.inputTokens,
5624
+ outputTokens: row.outputTokens,
5625
+ reasoningTokens: row.reasoningTokens,
5626
+ cacheReadTokens: row.cacheReadTokens,
5627
+ cacheWriteTokens: row.cacheWriteTokens,
5628
+ totalTokens: row.totalTokens,
5629
+ costUsd: row.costUsd,
5630
+ costIncomplete: row.costIncomplete
5631
+ };
5632
+ }
5633
+ function renderTerminalEfficiencyTable(rows) {
5634
+ const bodyRows = toEfficiencyTableCells(rows);
5635
+ const tableSortRows = rows.map((row) => toTableSortRow(row));
5636
+ const periodColumnWidth = Math.max(
5637
+ efficiencyTableHeaders[0].length,
5638
+ ...rows.map((row) => row.periodKey.length)
5639
+ );
5640
+ return renderUnicodeTable({
5641
+ headerCells: efficiencyTableHeaders,
5642
+ bodyRows,
5643
+ measureHeaderCells: efficiencyTableHeaders,
5644
+ measureBodyRows: bodyRows,
5645
+ usageRows: tableSortRows,
5646
+ tableLayout: "compact",
5647
+ modelsColumnIndex: 0,
5648
+ modelsColumnWidth: periodColumnWidth
5649
+ });
5650
+ }
5651
+ function toMarkdownSafeCell(value) {
5652
+ return value.replace(/\r?\n/gu, "<br>");
5653
+ }
5654
+ function renderMarkdownEfficiencyTable(rows) {
5655
+ const bodyRows = toEfficiencyTableCells(rows).map(
5656
+ (row) => row.map((cell) => toMarkdownSafeCell(cell))
5657
+ );
5658
+ const tableRows = [Array.from(efficiencyTableHeaders), ...bodyRows];
5659
+ const alignment2 = efficiencyTableHeaders.map((_, index) => index === 0 ? "l" : "r");
5660
+ return markdownTable(tableRows, {
5661
+ align: alignment2
5662
+ });
5663
+ }
5664
+ function renderTerminalEfficiencyReport(efficiencyData, options) {
5665
+ const outputLines = [];
5666
+ const useColor = options.useColor ?? shouldUseColorByDefault();
5667
+ outputLines.push(
5668
+ renderReportHeader({
5669
+ title: getReportTitle(options.granularity),
5670
+ useColor
5671
+ })
5672
+ );
5673
+ outputLines.push("");
5674
+ outputLines.push(renderTerminalEfficiencyTable(efficiencyData.rows));
5675
+ return outputLines.join("\n");
5676
+ }
5677
+ function renderEfficiencyReport(efficiencyData, format, options) {
5678
+ switch (format) {
5679
+ case "json":
5680
+ return JSON.stringify(efficiencyData.rows, null, 2);
5681
+ case "markdown":
5682
+ return renderMarkdownEfficiencyTable(efficiencyData.rows);
5683
+ case "terminal":
5684
+ return renderTerminalEfficiencyReport(efficiencyData, options);
5685
+ }
5686
+ }
5687
+
5688
+ // src/cli/run-efficiency-report.ts
5689
+ function validateOutputFormatOptions(options) {
5690
+ if (options.markdown && options.json) {
5691
+ throw new Error("Choose either --markdown or --json, not both");
5692
+ }
5693
+ }
5694
+ function resolveReportFormat(options) {
5695
+ if (options.json) {
5696
+ return "json";
5697
+ }
5698
+ if (options.markdown) {
5699
+ return "markdown";
5700
+ }
5701
+ return "terminal";
5702
+ }
5703
+ async function prepareEfficiencyReport(granularity, options) {
5704
+ validateOutputFormatOptions(options);
5705
+ const efficiencyData = await buildEfficiencyData(granularity, options);
5706
+ const format = resolveReportFormat(options);
5707
+ return {
5708
+ format,
5709
+ diagnostics: efficiencyData.diagnostics,
5710
+ output: renderEfficiencyReport(efficiencyData, format, {
5711
+ granularity
5712
+ })
5713
+ };
5714
+ }
5715
+ async function runEfficiencyReport(granularity, options) {
5716
+ const preparedReport = await prepareEfficiencyReport(granularity, options);
5717
+ emitDiagnostics(preparedReport.diagnostics.usage, logger);
5718
+ emitEnvVarOverrides(preparedReport.diagnostics.usage.activeEnvOverrides, logger);
5719
+ const mergeModeLabel = preparedReport.diagnostics.includeMergeCommits ? "including merge commits" : "excluding merge commits";
5720
+ logger.info(
5721
+ `Git outcomes (${mergeModeLabel}): ${preparedReport.diagnostics.gitCommitCount} commit(s), +${preparedReport.diagnostics.gitLinesAdded}/-${preparedReport.diagnostics.gitLinesDeleted} lines (${preparedReport.diagnostics.repoDir})`
5722
+ );
5723
+ logger.info(
5724
+ `Repo-attributed usage events: ${preparedReport.diagnostics.repoMatchedUsageEvents} matched, ${preparedReport.diagnostics.repoExcludedUsageEvents} excluded, ${preparedReport.diagnostics.repoUnattributedUsageEvents} unattributed`
5725
+ );
5726
+ if (preparedReport.diagnostics.scopeNote) {
5727
+ logger.warn(preparedReport.diagnostics.scopeNote);
5728
+ }
5729
+ if (preparedReport.format === "terminal") {
5730
+ warnIfTerminalTableOverflows(preparedReport.output, (message) => {
5731
+ logger.warn(message);
5732
+ });
5733
+ }
5734
+ console.log(preparedReport.output);
5735
+ }
5736
+
5737
+ // src/render/markdown-table.ts
5738
+ import { markdownTable as markdownTable2 } from "markdown-table";
5739
+ var alignment = ["l", "l", "l", "r", "r", "r", "r", "r", "r", "r"];
5740
+ function toMarkdownSafeCell2(value) {
5741
+ return value.replace(/\r?\n/gu, "<br>");
5742
+ }
5743
+ function renderMarkdownTable(rows, options = {}) {
5744
+ const tableLayout = options.tableLayout ?? "compact";
5745
+ const bodyRows = toUsageTableCells(rows, { layout: tableLayout }).map(
5746
+ (row) => row.map((cell) => toMarkdownSafeCell2(cell))
5747
+ );
5748
+ const tableRows = [Array.from(usageTableHeaders), ...bodyRows];
5749
+ return markdownTable2(tableRows, {
5750
+ align: alignment
5751
+ });
5752
+ }
5753
+
5754
+ // src/render/render-usage-report.ts
5755
+ function getReportTitle2(granularity) {
4238
5756
  switch (granularity) {
4239
5757
  case "daily":
4240
5758
  return "Daily Token Usage Report";
@@ -4246,16 +5764,11 @@ function getReportTitle(granularity) {
4246
5764
  }
4247
5765
  function renderTerminalUsageReport(usageData, options) {
4248
5766
  const outputLines = [];
4249
- const envVarOverrideLines = formatEnvVarOverrides(usageData.diagnostics.activeEnvOverrides);
4250
5767
  const useColor = options.useColor ?? shouldUseColorByDefault();
4251
5768
  const tableLayout = options.tableLayout ?? "compact";
4252
- if (envVarOverrideLines.length > 0) {
4253
- outputLines.push(...envVarOverrideLines);
4254
- outputLines.push("");
4255
- }
4256
5769
  outputLines.push(
4257
5770
  renderReportHeader({
4258
- title: getReportTitle(options.granularity),
5771
+ title: getReportTitle2(options.granularity),
4259
5772
  useColor
4260
5773
  })
4261
5774
  );
@@ -4276,12 +5789,12 @@ function renderUsageReport(usageData, format, options) {
4276
5789
  }
4277
5790
 
4278
5791
  // src/cli/run-usage-report.ts
4279
- function validateOutputFormatOptions(options) {
5792
+ function validateOutputFormatOptions2(options) {
4280
5793
  if (options.markdown && options.json) {
4281
5794
  throw new Error("Choose either --markdown or --json, not both");
4282
5795
  }
4283
5796
  }
4284
- function resolveReportFormat(options) {
5797
+ function resolveReportFormat2(options) {
4285
5798
  if (options.json) {
4286
5799
  return "json";
4287
5800
  }
@@ -4293,31 +5806,10 @@ function resolveReportFormat(options) {
4293
5806
  function resolveTableLayout(options) {
4294
5807
  return options.perModelColumns ? "per_model_columns" : "compact";
4295
5808
  }
4296
- function detectTerminalOverflowColumns(reportOutput) {
4297
- const stdoutState = process.stdout;
4298
- const terminalColumns = resolveTtyColumns(stdoutState);
4299
- if (terminalColumns === void 0) {
4300
- return void 0;
4301
- }
4302
- const allLines = reportOutput.trimEnd().split("\n");
4303
- const tableLikeLinePattern = /[│╭╮╰╯├┼┬┴┌┐└┘]|^\s*\|.*\|\s*$/u;
4304
- const tableLines = allLines.filter((line) => tableLikeLinePattern.test(line));
4305
- if (tableLines.length === 0) {
4306
- return void 0;
4307
- }
4308
- const maxLineWidth = tableLines.reduce(
4309
- (maxWidth, line) => Math.max(maxWidth, visibleWidth(line)),
4310
- 0
4311
- );
4312
- if (maxLineWidth <= terminalColumns) {
4313
- return void 0;
4314
- }
4315
- return maxLineWidth - terminalColumns;
4316
- }
4317
5809
  async function prepareUsageReport(granularity, options) {
4318
- validateOutputFormatOptions(options);
5810
+ validateOutputFormatOptions2(options);
4319
5811
  const usageData = await buildUsageData(granularity, options);
4320
- const format = resolveReportFormat(options);
5812
+ const format = resolveReportFormat2(options);
4321
5813
  return {
4322
5814
  format,
4323
5815
  diagnostics: usageData.diagnostics,
@@ -4330,13 +5822,11 @@ async function prepareUsageReport(granularity, options) {
4330
5822
  async function runUsageReport(granularity, options) {
4331
5823
  const preparedReport = await prepareUsageReport(granularity, options);
4332
5824
  emitDiagnostics(preparedReport.diagnostics, logger);
5825
+ emitEnvVarOverrides(preparedReport.diagnostics.activeEnvOverrides, logger);
4333
5826
  if (preparedReport.format === "terminal") {
4334
- const overflowColumns = detectTerminalOverflowColumns(preparedReport.output);
4335
- if (overflowColumns !== void 0) {
4336
- logger.warn(
4337
- `Report table is wider than terminal by ${overflowColumns} column(s). Use fullscreen/maximized terminal for better readability.`
4338
- );
4339
- }
5827
+ warnIfTerminalTableOverflows(preparedReport.output, (message) => {
5828
+ logger.warn(message);
5829
+ });
4340
5830
  }
4341
5831
  console.log(preparedReport.output);
4342
5832
  }
@@ -4352,11 +5842,12 @@ function getSupportedSourceIds() {
4352
5842
  function getAllowedSourcesLabel(supportedSourceIds) {
4353
5843
  return supportedSourceIds.join(", ");
4354
5844
  }
4355
- function addSharedOptions(command) {
5845
+ function addSharedOptions(command, options = {}) {
4356
5846
  const supportedSourceIds = getSupportedSourceIds();
4357
5847
  const allowedSourcesLabel = getAllowedSourcesLabel(supportedSourceIds);
4358
5848
  const supportedSourcesSummary = `(${supportedSourceIds.length}): ${allowedSourcesLabel}`;
4359
- return 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(
5849
+ 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(
4360
5851
  "--source-dir <source-id=path>",
4361
5852
  "Override source directory for directory-backed sources (repeatable)",
4362
5853
  collectRepeatedOption,
@@ -4371,7 +5862,14 @@ function addSharedOptions(command) {
4371
5862
  "Filter by model (repeatable/comma-separated; exact when exact match exists after source/provider/date filters, otherwise substring)",
4372
5863
  collectRepeatedOption,
4373
5864
  []
4374
- ).option("--pricing-url <url>", "Override LiteLLM pricing source URL").option("--pricing-offline", "Use cached LiteLLM pricing only (no network fetch)").option("--markdown", "Render output as markdown table").option("--json", "Render output as JSON").option(
5865
+ ).option("--pricing-url <url>", "Override LiteLLM pricing source URL").option("--pricing-offline", "Use cached LiteLLM pricing only (no network fetch)").option(
5866
+ "--ignore-pricing-failures",
5867
+ "Continue without estimated costs when pricing cannot be loaded"
5868
+ ).option("--markdown", "Render output as markdown table").option("--json", "Render output as JSON");
5869
+ if (!includePerModelColumns) {
5870
+ return configuredCommand;
5871
+ }
5872
+ return configuredCommand.option(
4375
5873
  "--per-model-columns",
4376
5874
  "Render per-model metrics as multiline aligned table columns (terminal/markdown)"
4377
5875
  );
@@ -4393,6 +5891,20 @@ function createCommand(granularity) {
4393
5891
  });
4394
5892
  return command;
4395
5893
  }
5894
+ function parseGranularityArgument(value) {
5895
+ const normalized = value.trim().toLowerCase();
5896
+ if (normalized === "daily" || normalized === "weekly" || normalized === "monthly") {
5897
+ return normalized;
5898
+ }
5899
+ throw new Error(`Invalid granularity: ${value}. Expected one of: daily, weekly, monthly`);
5900
+ }
5901
+ function createEfficiencyCommand() {
5902
+ const command = new Command("efficiency");
5903
+ addSharedOptions(command, { includePerModelColumns: false }).argument("<granularity>", "Granularity: daily | weekly | monthly", parseGranularityArgument).option("--repo-dir <path>", "Path to repository for Git outcome metrics").option("--include-merge-commits", "Include merge commits in Git outcome metrics").description("Show efficiency report by correlating usage metrics with local Git outcomes").action(async (granularity, options) => {
5904
+ await runEfficiencyReport(granularity, options);
5905
+ });
5906
+ return command;
5907
+ }
4396
5908
  function rootDescription() {
4397
5909
  const supportedSourceIds = getSupportedSourceIds();
4398
5910
  const allowedSourcesLabel = getAllowedSourcesLabel(supportedSourceIds);
@@ -4409,13 +5921,15 @@ function rootDescription() {
4409
5921
  " $ llm-usage monthly --since 2026-01-01 --until 2026-01-31 --source pi,codex --json",
4410
5922
  " $ llm-usage monthly --source opencode --opencode-db /path/to/opencode.db --json",
4411
5923
  " $ llm-usage monthly --model claude --per-model-columns",
4412
- " $ 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",
5926
+ " $ llm-usage efficiency weekly --repo-dir /path/to/repo --json",
4413
5927
  " $ npx --yes llm-usage-metrics daily"
4414
5928
  ].join("\n");
4415
5929
  }
4416
5930
  function createCli(options = {}) {
4417
5931
  const program = new Command();
4418
- program.name("llm-usage").description(rootDescription()).version(options.version ?? "0.0.0").showHelpAfterError().addCommand(createCommand("daily")).addCommand(createCommand("weekly")).addCommand(createCommand("monthly"));
5932
+ program.name("llm-usage").description(rootDescription()).version(options.version ?? "0.0.0").showHelpAfterError().addCommand(createCommand("daily")).addCommand(createCommand("weekly")).addCommand(createCommand("monthly")).addCommand(createEfficiencyCommand());
4419
5933
  return program;
4420
5934
  }
4421
5935