llm-usage-metrics 0.3.2 → 0.3.3

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,
@@ -752,14 +769,14 @@ async function walkDirectory(rootDir, acc, options) {
752
769
  }
753
770
  throw error;
754
771
  }
755
- entries.sort((left, right) => left.name.localeCompare(right.name));
772
+ entries.sort((left, right) => compareByCodePoint(left.name, right.name));
756
773
  for (const entry of entries) {
757
774
  const entryPath = path3.join(rootDir, entry.name);
758
775
  if (entry.isDirectory()) {
759
776
  await walkDirectory(entryPath, acc, { allowPermissionSkip: true });
760
777
  continue;
761
778
  }
762
- if (entry.isFile() && entry.name.endsWith(".jsonl")) {
779
+ if (entry.isFile() && entry.name.toLowerCase().endsWith(".jsonl")) {
763
780
  acc.push(entryPath);
764
781
  }
765
782
  }
@@ -777,6 +794,46 @@ async function discoverJsonlFiles(rootDir) {
777
794
  return files;
778
795
  }
779
796
 
797
+ // src/utils/fs-helpers.ts
798
+ import { access, constants, stat } from "fs/promises";
799
+ async function pathExists(filePath) {
800
+ try {
801
+ await access(filePath, constants.F_OK);
802
+ return true;
803
+ } catch {
804
+ return false;
805
+ }
806
+ }
807
+ async function pathReadable(filePath) {
808
+ try {
809
+ await access(filePath, constants.R_OK);
810
+ return true;
811
+ } catch {
812
+ return false;
813
+ }
814
+ }
815
+ async function pathIsDirectory(filePath) {
816
+ try {
817
+ return (await stat(filePath)).isDirectory();
818
+ } catch {
819
+ return false;
820
+ }
821
+ }
822
+ async function pathIsFile(filePath) {
823
+ try {
824
+ return (await stat(filePath)).isFile();
825
+ } catch {
826
+ return false;
827
+ }
828
+ }
829
+ async function pathStat(filePath) {
830
+ try {
831
+ return await stat(filePath);
832
+ } catch {
833
+ return void 0;
834
+ }
835
+ }
836
+
780
837
  // src/utils/read-jsonl-objects.ts
781
838
  import { createReadStream } from "fs";
782
839
  import { createInterface as createInterface2 } from "readline";
@@ -840,6 +897,9 @@ var SESSION_META_LINE_PATTERN = /"type"\s*:\s*"session_meta"/u;
840
897
  var TURN_CONTEXT_LINE_PATTERN = /"type"\s*:\s*"turn_context"/u;
841
898
  var EVENT_MSG_LINE_PATTERN = /"type"\s*:\s*"event_msg"/u;
842
899
  var TOKEN_COUNT_LINE_PATTERN = /"type"\s*:\s*"token_count"/u;
900
+ function isBlankText(value) {
901
+ return value.trim().length === 0;
902
+ }
843
903
  function shouldParseCodexJsonlLine(lineText) {
844
904
  if (SESSION_META_LINE_PATTERN.test(lineText) || TURN_CONTEXT_LINE_PATTERN.test(lineText)) {
845
905
  return true;
@@ -902,14 +962,37 @@ function deriveDeltaUsage(info, previousTotalUsage) {
902
962
  function getFallbackSessionId(filePath) {
903
963
  return path4.basename(filePath, ".jsonl");
904
964
  }
965
+ function resolveRepoRootFromPayload(payload) {
966
+ if (!payload) {
967
+ return void 0;
968
+ }
969
+ return asTrimmedText(payload.cwd) ?? asTrimmedText(payload.repo_root) ?? asTrimmedText(payload.repoRoot) ?? asTrimmedText(payload.project_root) ?? asTrimmedText(payload.projectRoot);
970
+ }
905
971
  var CodexSourceAdapter = class {
906
972
  id = "codex";
907
973
  sessionsDir;
974
+ requireSessionsDir;
908
975
  constructor(options = {}) {
909
976
  this.sessionsDir = options.sessionsDir ?? defaultSessionsDir;
977
+ this.requireSessionsDir = options.requireSessionsDir ?? false;
910
978
  }
911
979
  async discoverFiles() {
912
- return discoverJsonlFiles(this.sessionsDir);
980
+ if (isBlankText(this.sessionsDir)) {
981
+ throw new Error("Codex sessions directory must be a non-empty path");
982
+ }
983
+ const normalizedSessionsDir = this.sessionsDir.trim();
984
+ if (this.requireSessionsDir) {
985
+ const sessionsDirStats = await pathStat(normalizedSessionsDir);
986
+ if (!sessionsDirStats) {
987
+ throw new Error(
988
+ `Codex sessions directory is missing or unreadable: ${normalizedSessionsDir}`
989
+ );
990
+ }
991
+ if (!sessionsDirStats.isDirectory()) {
992
+ throw new Error(`Codex sessions directory is not a directory: ${normalizedSessionsDir}`);
993
+ }
994
+ }
995
+ return discoverJsonlFiles(normalizedSessionsDir);
913
996
  }
914
997
  async parseFile(filePath) {
915
998
  const events = [];
@@ -924,11 +1007,13 @@ var CodexSourceAdapter = class {
924
1007
  const payload2 = asRecord(line.payload);
925
1008
  state.sessionId = asTrimmedText(payload2?.id) ?? state.sessionId;
926
1009
  state.provider = asTrimmedText(payload2?.model_provider) ?? state.provider;
1010
+ state.repoRoot = resolveRepoRootFromPayload(payload2) ?? state.repoRoot;
927
1011
  continue;
928
1012
  }
929
1013
  if (line.type === "turn_context") {
930
1014
  const payload2 = asRecord(line.payload);
931
1015
  state.model = asTrimmedText(payload2?.model) ?? state.model;
1016
+ state.repoRoot = resolveRepoRootFromPayload(payload2) ?? state.repoRoot;
932
1017
  continue;
933
1018
  }
934
1019
  if (line.type !== "event_msg") {
@@ -959,6 +1044,7 @@ var CodexSourceAdapter = class {
959
1044
  source: this.id,
960
1045
  sessionId: state.sessionId,
961
1046
  timestamp,
1047
+ repoRoot: state.repoRoot,
962
1048
  provider: state.provider,
963
1049
  model,
964
1050
  inputTokens: deltaUsage.inputTokens,
@@ -984,9 +1070,6 @@ var CodexSourceAdapter = class {
984
1070
  }
985
1071
  };
986
1072
 
987
- // src/sources/opencode/opencode-source-adapter.ts
988
- import { access, constants } from "fs/promises";
989
-
990
1073
  // src/sources/opencode/opencode-db-path-resolver.ts
991
1074
  import os3 from "os";
992
1075
  import path5 from "path";
@@ -1153,6 +1236,10 @@ function normalizeSessionIdCandidate(value) {
1153
1236
  }
1154
1237
  return asTrimmedText(value);
1155
1238
  }
1239
+ function resolveRepoRoot(messagePayload) {
1240
+ const pathPayload = asRecord(messagePayload.path);
1241
+ return asTrimmedText(pathPayload?.root) ?? asTrimmedText(pathPayload?.cwd) ?? asTrimmedText(messagePayload.cwd) ?? asTrimmedText(messagePayload.repo_root) ?? asTrimmedText(messagePayload.repoRoot) ?? asTrimmedText(messagePayload.project_root) ?? asTrimmedText(messagePayload.projectRoot);
1242
+ }
1156
1243
  function hasUsageSignal2(usageFields, explicitCost) {
1157
1244
  if (explicitCost !== void 0) {
1158
1245
  return true;
@@ -1204,6 +1291,7 @@ function parseOpenCodeMessageRows(rows, sourceId) {
1204
1291
  }
1205
1292
  const provider = asTrimmedText(payload.providerID) ?? asTrimmedText(payload.provider);
1206
1293
  const model = asTrimmedText(payload.modelID) ?? asTrimmedText(payload.model);
1294
+ const repoRoot = resolveRepoRoot(payload);
1207
1295
  const tokens = asRecord(payload.tokens);
1208
1296
  const tokenCache = asRecord(tokens?.cache);
1209
1297
  const inputTokens = toNumberLike(tokens?.input);
@@ -1233,6 +1321,7 @@ function parseOpenCodeMessageRows(rows, sourceId) {
1233
1321
  source: sourceId,
1234
1322
  sessionId,
1235
1323
  timestamp,
1324
+ repoRoot,
1236
1325
  provider,
1237
1326
  model,
1238
1327
  inputTokens,
@@ -1428,25 +1517,9 @@ function queryOpenCodeMessageRows(database) {
1428
1517
  // src/sources/opencode/opencode-source-adapter.ts
1429
1518
  var DEFAULT_BUSY_RETRY_COUNT = 2;
1430
1519
  var DEFAULT_BUSY_RETRY_DELAY_MS = 50;
1431
- function isBlankText(value) {
1520
+ function isBlankText2(value) {
1432
1521
  return value.trim().length === 0;
1433
1522
  }
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
1523
  async function sleep2(delayMs) {
1451
1524
  await new Promise((resolve) => {
1452
1525
  setTimeout(resolve, delayMs);
@@ -1458,6 +1531,7 @@ var OpenCodeSourceAdapter = class {
1458
1531
  resolveDefaultDbPaths;
1459
1532
  pathExists;
1460
1533
  pathReadable;
1534
+ pathIsFile;
1461
1535
  loadSqliteModule;
1462
1536
  maxBusyRetries;
1463
1537
  busyRetryDelayMs;
@@ -1467,6 +1541,7 @@ var OpenCodeSourceAdapter = class {
1467
1541
  this.resolveDefaultDbPaths = options.resolveDefaultDbPaths ?? getDefaultOpenCodeDbPathCandidates;
1468
1542
  this.pathExists = options.pathExists ?? pathExists;
1469
1543
  this.pathReadable = options.pathReadable ?? pathReadable;
1544
+ this.pathIsFile = options.pathIsFile ?? pathIsFile;
1470
1545
  this.loadSqliteModule = options.loadSqliteModule ?? loadNodeSqliteModule;
1471
1546
  this.maxBusyRetries = Math.max(0, options.maxBusyRetries ?? DEFAULT_BUSY_RETRY_COUNT);
1472
1547
  this.busyRetryDelayMs = Math.max(1, options.busyRetryDelayMs ?? DEFAULT_BUSY_RETRY_DELAY_MS);
@@ -1474,7 +1549,7 @@ var OpenCodeSourceAdapter = class {
1474
1549
  }
1475
1550
  async discoverFiles() {
1476
1551
  if (this.explicitDbPath !== void 0) {
1477
- if (isBlankText(this.explicitDbPath)) {
1552
+ if (isBlankText2(this.explicitDbPath)) {
1478
1553
  throw new Error("--opencode-db must be a non-empty path");
1479
1554
  }
1480
1555
  const explicitDbPath = this.explicitDbPath.trim();
@@ -1482,11 +1557,17 @@ var OpenCodeSourceAdapter = class {
1482
1557
  if (!readable) {
1483
1558
  throw new Error(`OpenCode DB path is missing or unreadable: ${explicitDbPath}`);
1484
1559
  }
1560
+ if (await this.pathExists(explicitDbPath) && !await this.pathIsFile(explicitDbPath)) {
1561
+ throw new Error(`OpenCode DB path is not a file: ${explicitDbPath}`);
1562
+ }
1485
1563
  return [explicitDbPath];
1486
1564
  }
1487
1565
  let firstUnreadableCandidatePath;
1488
1566
  for (const candidatePath of this.resolveDefaultDbPaths()) {
1489
1567
  if (await this.pathReadable(candidatePath)) {
1568
+ if (await this.pathExists(candidatePath) && !await this.pathIsFile(candidatePath)) {
1569
+ throw new Error(`OpenCode DB path is not a file: ${candidatePath}`);
1570
+ }
1490
1571
  return [candidatePath];
1491
1572
  }
1492
1573
  if (!firstUnreadableCandidatePath && await this.pathExists(candidatePath)) {
@@ -1503,7 +1584,7 @@ var OpenCodeSourceAdapter = class {
1503
1584
  return parseDiagnostics.events;
1504
1585
  }
1505
1586
  async parseFileWithDiagnostics(dbPath) {
1506
- if (isBlankText(dbPath)) {
1587
+ if (isBlankText2(dbPath)) {
1507
1588
  throw new Error("OpenCode DB path must be a non-empty path");
1508
1589
  }
1509
1590
  const normalizedDbPath = dbPath.trim();
@@ -1511,6 +1592,9 @@ var OpenCodeSourceAdapter = class {
1511
1592
  if (!readable) {
1512
1593
  throw new Error(`OpenCode DB path is unreadable: ${normalizedDbPath}`);
1513
1594
  }
1595
+ if (await this.pathExists(normalizedDbPath) && !await this.pathIsFile(normalizedDbPath)) {
1596
+ throw new Error(`OpenCode DB path is not a file: ${normalizedDbPath}`);
1597
+ }
1514
1598
  return runWithBusyRetries(() => this.parseFileOnce(normalizedDbPath), {
1515
1599
  dbPath: normalizedDbPath,
1516
1600
  maxBusyRetries: this.maxBusyRetries,
@@ -1542,6 +1626,9 @@ function shouldParsePiJsonlLine(lineText) {
1542
1626
  }
1543
1627
  var allowAllProviders = () => true;
1544
1628
  var UNIX_SECONDS_ABS_CUTOFF2 = 1e10;
1629
+ function isBlankText3(value) {
1630
+ return value.trim().length === 0;
1631
+ }
1545
1632
  function normalizeTimestampCandidate2(candidate) {
1546
1633
  let date;
1547
1634
  if (typeof candidate === "number" && Number.isFinite(candidate)) {
@@ -1582,8 +1669,34 @@ function extractUsageFromRecord(usage) {
1582
1669
  totalTokens: toNumberLike(usage.totalTokens),
1583
1670
  costUsd: toNumberLike(cost?.total)
1584
1671
  };
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;
1672
+ const toFiniteNumber = (value) => {
1673
+ if (value === null || value === void 0) {
1674
+ return void 0;
1675
+ }
1676
+ if (typeof value === "string" && value.trim().length === 0) {
1677
+ return void 0;
1678
+ }
1679
+ const parsed = typeof value === "number" ? value : Number(value);
1680
+ if (!Number.isFinite(parsed)) {
1681
+ return void 0;
1682
+ }
1683
+ return parsed;
1684
+ };
1685
+ const usageCandidates = [
1686
+ extracted.inputTokens,
1687
+ extracted.outputTokens,
1688
+ extracted.reasoningTokens,
1689
+ extracted.cacheReadTokens,
1690
+ extracted.cacheWriteTokens,
1691
+ extracted.totalTokens
1692
+ ];
1693
+ const hasPositiveUsageSignal = usageCandidates.some((value) => {
1694
+ const parsed = toFiniteNumber(value);
1695
+ return parsed !== void 0 && parsed > 0;
1696
+ });
1697
+ const explicitCost = toFiniteNumber(extracted.costUsd);
1698
+ const hasPositiveCostSignal = explicitCost !== void 0 && explicitCost > 0;
1699
+ return hasPositiveUsageSignal || hasPositiveCostSignal ? extracted : void 0;
1587
1700
  }
1588
1701
  function extractUsage(line, message) {
1589
1702
  const lineUsage = asRecord(line.usage);
@@ -1602,16 +1715,35 @@ function extractUsage(line, message) {
1602
1715
  function getFallbackSessionId2(filePath) {
1603
1716
  return path6.basename(filePath, ".jsonl");
1604
1717
  }
1718
+ function resolveRepoRootFromRecord(record) {
1719
+ if (!record) {
1720
+ return void 0;
1721
+ }
1722
+ const pathRecord = asRecord(record.path);
1723
+ return asTrimmedText(pathRecord?.root) ?? asTrimmedText(pathRecord?.cwd) ?? asTrimmedText(record.cwd) ?? asTrimmedText(record.repo_root) ?? asTrimmedText(record.repoRoot) ?? asTrimmedText(record.project_root) ?? asTrimmedText(record.projectRoot);
1724
+ }
1605
1725
  var PiSourceAdapter = class {
1606
1726
  id = "pi";
1607
1727
  sessionsDir;
1608
1728
  providerFilter;
1729
+ requireSessionsDir;
1609
1730
  constructor(options = {}) {
1610
1731
  this.sessionsDir = options.sessionsDir ?? defaultSessionsDir2;
1611
1732
  this.providerFilter = options.providerFilter ?? allowAllProviders;
1733
+ this.requireSessionsDir = options.requireSessionsDir ?? false;
1612
1734
  }
1613
1735
  async discoverFiles() {
1614
- return discoverJsonlFiles(this.sessionsDir);
1736
+ if (isBlankText3(this.sessionsDir)) {
1737
+ throw new Error("PI sessions directory must be a non-empty path");
1738
+ }
1739
+ const normalizedSessionsDir = this.sessionsDir.trim();
1740
+ if (this.requireSessionsDir && !await pathReadable(normalizedSessionsDir)) {
1741
+ throw new Error(`PI sessions directory is missing or unreadable: ${normalizedSessionsDir}`);
1742
+ }
1743
+ if (this.requireSessionsDir && !await pathIsDirectory(normalizedSessionsDir)) {
1744
+ throw new Error(`PI sessions directory is not a directory: ${normalizedSessionsDir}`);
1745
+ }
1746
+ return discoverJsonlFiles(normalizedSessionsDir);
1615
1747
  }
1616
1748
  async parseFile(filePath) {
1617
1749
  const events = [];
@@ -1622,11 +1754,13 @@ var PiSourceAdapter = class {
1622
1754
  if (line.type === "session") {
1623
1755
  state.sessionId = asTrimmedText(line.id) ?? state.sessionId;
1624
1756
  state.sessionTimestamp = asTrimmedText(line.timestamp) ?? state.sessionTimestamp;
1757
+ state.repoRoot = resolveRepoRootFromRecord(line) ?? state.repoRoot;
1625
1758
  continue;
1626
1759
  }
1627
1760
  if (line.type === "model_change") {
1628
1761
  state.provider = asTrimmedText(line.provider) ?? state.provider;
1629
1762
  state.model = asTrimmedText(line.modelId) ?? asTrimmedText(line.model) ?? state.model;
1763
+ state.repoRoot = resolveRepoRootFromRecord(line) ?? state.repoRoot;
1630
1764
  continue;
1631
1765
  }
1632
1766
  if (line.type !== "message") {
@@ -1646,12 +1780,14 @@ var PiSourceAdapter = class {
1646
1780
  continue;
1647
1781
  }
1648
1782
  const model = asTrimmedText(line.model) ?? asTrimmedText(line.modelId) ?? asTrimmedText(message?.model) ?? state.model;
1783
+ const repoRoot = resolveRepoRootFromRecord(line) ?? resolveRepoRootFromRecord(message) ?? state.repoRoot;
1649
1784
  try {
1650
1785
  events.push(
1651
1786
  createUsageEvent({
1652
1787
  source: this.id,
1653
1788
  sessionId: state.sessionId,
1654
1789
  timestamp,
1790
+ repoRoot,
1655
1791
  provider,
1656
1792
  model,
1657
1793
  ...usage
@@ -1692,16 +1828,28 @@ var sourceRegistrations = [
1692
1828
  {
1693
1829
  id: "pi",
1694
1830
  sourceDirOverride: { kind: "directory" },
1695
- create: (options, sourceDirectoryOverrides) => new PiSourceAdapter({
1696
- sessionsDir: resolveDirectoryOverride("pi", options.piDir, sourceDirectoryOverrides)
1697
- })
1831
+ create: (options, sourceDirectoryOverrides) => {
1832
+ const directoryConfig = resolveDirectoryConfig("pi", options.piDir, sourceDirectoryOverrides);
1833
+ return new PiSourceAdapter({
1834
+ sessionsDir: directoryConfig.path,
1835
+ requireSessionsDir: directoryConfig.requireExistingPath
1836
+ });
1837
+ }
1698
1838
  },
1699
1839
  {
1700
1840
  id: "codex",
1701
1841
  sourceDirOverride: { kind: "directory" },
1702
- create: (options, sourceDirectoryOverrides) => new CodexSourceAdapter({
1703
- sessionsDir: resolveDirectoryOverride("codex", options.codexDir, sourceDirectoryOverrides)
1704
- })
1842
+ create: (options, sourceDirectoryOverrides) => {
1843
+ const directoryConfig = resolveDirectoryConfig(
1844
+ "codex",
1845
+ options.codexDir,
1846
+ sourceDirectoryOverrides
1847
+ );
1848
+ return new CodexSourceAdapter({
1849
+ sessionsDir: directoryConfig.path,
1850
+ requireSessionsDir: directoryConfig.requireExistingPath
1851
+ });
1852
+ }
1705
1853
  },
1706
1854
  {
1707
1855
  id: "opencode",
@@ -1736,9 +1884,7 @@ function validateSourceDirectoryOverrideIds(sourceDirectoryOverrides) {
1736
1884
  if (unknownSourceIds.length === 0) {
1737
1885
  return;
1738
1886
  }
1739
- const allowedSourceIds = [...sourceDirSupportedIds].sort(
1740
- (left, right) => left.localeCompare(right)
1741
- );
1887
+ const allowedSourceIds = [...sourceDirSupportedIds].sort(compareByCodePoint);
1742
1888
  throw new Error(
1743
1889
  `Unknown --source-dir source id(s): ${unknownSourceIds.join(", ")}. Allowed values: ${allowedSourceIds.join(", ")}`
1744
1890
  );
@@ -1751,14 +1897,40 @@ function validateOpencodeOverride(opencodeDb) {
1751
1897
  throw new Error("--opencode-db must be a non-empty path");
1752
1898
  }
1753
1899
  }
1754
- function resolveDirectoryOverride(sourceId, explicitDirectory, sourceDirectoryOverrides) {
1755
- return explicitDirectory ?? sourceDirectoryOverrides.get(sourceId);
1900
+ function validateDirectoryOverride(optionName, value) {
1901
+ if (value === void 0) {
1902
+ return;
1903
+ }
1904
+ if (value.trim().length === 0) {
1905
+ throw new Error(`${optionName} must be a non-empty path`);
1906
+ }
1907
+ }
1908
+ function resolveDirectoryConfig(sourceId, explicitDirectory, sourceDirectoryOverrides) {
1909
+ if (explicitDirectory !== void 0) {
1910
+ return {
1911
+ path: explicitDirectory,
1912
+ requireExistingPath: true
1913
+ };
1914
+ }
1915
+ const sourceDirOverride = sourceDirectoryOverrides.get(sourceId);
1916
+ if (sourceDirOverride !== void 0) {
1917
+ return {
1918
+ path: sourceDirOverride,
1919
+ requireExistingPath: true
1920
+ };
1921
+ }
1922
+ return {
1923
+ path: void 0,
1924
+ requireExistingPath: false
1925
+ };
1756
1926
  }
1757
1927
  function getDefaultSourceIds() {
1758
1928
  return sourceRegistrations.map((source) => source.id);
1759
1929
  }
1760
1930
  function createDefaultAdapters(options) {
1761
1931
  validateOpencodeOverride(options.opencodeDb);
1932
+ validateDirectoryOverride("--pi-dir", options.piDir);
1933
+ validateDirectoryOverride("--codex-dir", options.codexDir);
1762
1934
  const sourceDirectoryOverrides = parseSourceDirectoryOverrides(options.sourceDir);
1763
1935
  validateSourceDirectoryOverrideIds(sourceDirectoryOverrides);
1764
1936
  return sourceRegistrations.map((source) => source.create(options, sourceDirectoryOverrides));
@@ -1934,7 +2106,7 @@ function aggregateUsage(events, options) {
1934
2106
  periodSources.set(event.source, rowAccumulator);
1935
2107
  addEventToAccumulator(rowAccumulator, event);
1936
2108
  }
1937
- const sortedPeriodKeys = [...periodMap.keys()].sort((left, right) => left.localeCompare(right));
2109
+ const sortedPeriodKeys = [...periodMap.keys()].sort(compareByCodePoint);
1938
2110
  const rows = [];
1939
2111
  const grandTotals = createEmptyTotals();
1940
2112
  const grandModelTotals = /* @__PURE__ */ new Map();
@@ -1992,84 +2164,710 @@ function aggregateUsage(events, options) {
1992
2164
  return rows;
1993
2165
  }
1994
2166
 
1995
- // src/config/env-var-display.ts
1996
- var ENV_VARS_TO_DISPLAY = [
1997
- { name: "LLM_USAGE_SKIP_UPDATE_CHECK", description: "skip startup update check" },
1998
- {
1999
- name: "LLM_USAGE_UPDATE_CACHE_SCOPE",
2000
- description: "update-check cache scope (global/session)"
2001
- },
2002
- { name: "LLM_USAGE_UPDATE_CACHE_SESSION_KEY", description: "update-check session cache key" },
2003
- { name: "LLM_USAGE_UPDATE_CACHE_TTL_MS", description: "update-check cache TTL" },
2004
- { name: "LLM_USAGE_UPDATE_FETCH_TIMEOUT_MS", description: "update-check fetch timeout" },
2005
- { name: "LLM_USAGE_PRICING_CACHE_TTL_MS", description: "pricing cache TTL" },
2006
- { name: "LLM_USAGE_PRICING_FETCH_TIMEOUT_MS", description: "pricing fetch timeout" },
2007
- { name: "LLM_USAGE_PARSE_MAX_PARALLEL", description: "max parallel file parsing" },
2008
- { name: "LLM_USAGE_PARSE_CACHE_ENABLED", description: "enable file parse cache" },
2009
- { name: "LLM_USAGE_PARSE_CACHE_TTL_MS", description: "file parse cache TTL" },
2010
- { name: "LLM_USAGE_PARSE_CACHE_MAX_ENTRIES", description: "file parse cache max entries" },
2011
- { name: "LLM_USAGE_PARSE_CACHE_MAX_BYTES", description: "file parse cache max bytes" }
2012
- ];
2013
- function getActiveEnvVarOverrides() {
2014
- const overrides = [];
2015
- for (const { name, description } of ENV_VARS_TO_DISPLAY) {
2016
- const value = process.env[name];
2017
- if (value !== void 0 && value !== "") {
2018
- overrides.push({ name, value, description });
2019
- }
2020
- }
2021
- return overrides;
2167
+ // src/efficiency/efficiency-row.ts
2168
+ function createEmptyEfficiencyUsageTotals() {
2169
+ return {
2170
+ inputTokens: 0,
2171
+ outputTokens: 0,
2172
+ reasoningTokens: 0,
2173
+ cacheReadTokens: 0,
2174
+ cacheWriteTokens: 0,
2175
+ totalTokens: 0,
2176
+ costUsd: 0
2177
+ };
2022
2178
  }
2023
- function formatEnvVarOverrides(overrides) {
2024
- if (overrides.length === 0) {
2025
- return [];
2026
- }
2027
- const lines = [];
2028
- lines.push("Active environment overrides:");
2029
- for (const { name, value, description } of overrides) {
2030
- lines.push(` ${name}=${value} (${description})`);
2031
- }
2032
- return lines;
2179
+ function createEmptyEfficiencyOutcomeTotals() {
2180
+ return {
2181
+ commitCount: 0,
2182
+ linesAdded: 0,
2183
+ linesDeleted: 0,
2184
+ linesChanged: 0
2185
+ };
2033
2186
  }
2034
2187
 
2035
- // src/cli/build-usage-data-diagnostics.ts
2036
- function buildUsageDiagnostics(params) {
2037
- const parseResultBySource = new Map(
2038
- params.successfulParseResults.map((result) => [result.source.toLowerCase(), result])
2039
- );
2040
- const sessionStats = params.adaptersToParse.map((adapter) => {
2041
- const parseResult = parseResultBySource.get(adapter.id.toLowerCase());
2042
- return {
2043
- source: adapter.id,
2044
- filesFound: parseResult?.filesFound ?? 0,
2045
- eventsParsed: parseResult?.events.length ?? 0
2046
- };
2047
- });
2048
- const skippedRows = params.successfulParseResults.filter((result) => result.skippedRows > 0).map((result) => ({
2049
- source: result.source,
2050
- skippedRows: result.skippedRows,
2051
- reasons: result.skippedRowReasons
2052
- }));
2188
+ // src/efficiency/aggregate-efficiency.ts
2189
+ var USD_PRECISION_SCALE2 = 1e12;
2190
+ function addUsd2(left, right) {
2191
+ return Math.round((left + right) * USD_PRECISION_SCALE2) / USD_PRECISION_SCALE2;
2192
+ }
2193
+ function toUsageTotals(row) {
2053
2194
  return {
2054
- sessionStats,
2055
- sourceFailures: params.sourceFailures,
2056
- skippedRows,
2057
- pricingOrigin: params.pricingOrigin,
2058
- activeEnvOverrides: params.activeEnvOverrides,
2059
- timezone: params.timezone
2195
+ inputTokens: row.inputTokens,
2196
+ outputTokens: row.outputTokens,
2197
+ reasoningTokens: row.reasoningTokens,
2198
+ cacheReadTokens: row.cacheReadTokens,
2199
+ cacheWriteTokens: row.cacheWriteTokens,
2200
+ totalTokens: row.totalTokens,
2201
+ costUsd: row.costUsd,
2202
+ costIncomplete: row.costIncomplete
2060
2203
  };
2061
2204
  }
2062
- function assembleUsageDataResult(rows, diagnostics) {
2205
+ function buildUsageTotalsByPeriod(usageRows) {
2206
+ const combinedByPeriod = /* @__PURE__ */ new Map();
2207
+ const sourceByPeriod = /* @__PURE__ */ new Map();
2208
+ for (const row of usageRows) {
2209
+ if (row.rowType === "grand_total") {
2210
+ continue;
2211
+ }
2212
+ if (row.rowType === "period_combined") {
2213
+ combinedByPeriod.set(row.periodKey, toUsageTotals(row));
2214
+ continue;
2215
+ }
2216
+ const existingSourceTotals = sourceByPeriod.get(row.periodKey) ?? createEmptyEfficiencyUsageTotals();
2217
+ sourceByPeriod.set(row.periodKey, addUsageTotals(existingSourceTotals, toUsageTotals(row)));
2218
+ }
2219
+ const periodKeys = /* @__PURE__ */ new Set([...combinedByPeriod.keys(), ...sourceByPeriod.keys()]);
2220
+ const usageTotalsByPeriod = /* @__PURE__ */ new Map();
2221
+ for (const periodKey of periodKeys) {
2222
+ usageTotalsByPeriod.set(
2223
+ periodKey,
2224
+ combinedByPeriod.get(periodKey) ?? sourceByPeriod.get(periodKey) ?? createEmptyEfficiencyUsageTotals()
2225
+ );
2226
+ }
2227
+ return usageTotalsByPeriod;
2228
+ }
2229
+ function addOutcomeTotals(left, right) {
2063
2230
  return {
2064
- rows,
2065
- diagnostics
2231
+ commitCount: left.commitCount + right.commitCount,
2232
+ linesAdded: left.linesAdded + right.linesAdded,
2233
+ linesDeleted: left.linesDeleted + right.linesDeleted,
2234
+ linesChanged: left.linesChanged + right.linesChanged
2066
2235
  };
2067
2236
  }
2068
-
2069
- // src/cli/build-usage-data-inputs.ts
2070
- function validateDateInput(value, flagName) {
2071
- if (!/^\d{4}-\d{2}-\d{2}$/u.test(value)) {
2072
- throw new Error(`${flagName} must use format YYYY-MM-DD`);
2237
+ function addUsageTotals(left, right) {
2238
+ const hasUnknownCost = left.costIncomplete === true && left.costUsd === void 0 || right.costIncomplete === true && right.costUsd === void 0;
2239
+ const isNeutralZeroCost = (value) => value.totalTokens === 0 && value.costUsd === 0 && value.costIncomplete !== true;
2240
+ const leftKnownCost = left.costUsd !== void 0 && !isNeutralZeroCost(left) ? left.costUsd : void 0;
2241
+ const rightKnownCost = right.costUsd !== void 0 && !isNeutralZeroCost(right) ? right.costUsd : void 0;
2242
+ let costUsd = leftKnownCost !== void 0 && rightKnownCost !== void 0 ? addUsd2(leftKnownCost, rightKnownCost) : leftKnownCost ?? rightKnownCost;
2243
+ if (hasUnknownCost && (costUsd === void 0 || costUsd === 0)) {
2244
+ costUsd = void 0;
2245
+ }
2246
+ return {
2247
+ inputTokens: left.inputTokens + right.inputTokens,
2248
+ outputTokens: left.outputTokens + right.outputTokens,
2249
+ reasoningTokens: left.reasoningTokens + right.reasoningTokens,
2250
+ cacheReadTokens: left.cacheReadTokens + right.cacheReadTokens,
2251
+ cacheWriteTokens: left.cacheWriteTokens + right.cacheWriteTokens,
2252
+ totalTokens: left.totalTokens + right.totalTokens,
2253
+ costUsd,
2254
+ costIncomplete: left.costIncomplete || right.costIncomplete ? true : void 0
2255
+ };
2256
+ }
2257
+ function computeDerivedMetrics(usage, outcomes) {
2258
+ const costUsd = usage.costUsd;
2259
+ const nonCacheTotalTokens = usage.inputTokens + usage.outputTokens + usage.reasoningTokens;
2260
+ return {
2261
+ usdPerCommit: costUsd !== void 0 && outcomes.commitCount > 0 ? costUsd / outcomes.commitCount : void 0,
2262
+ usdPer1kLinesChanged: costUsd !== void 0 && outcomes.linesChanged > 0 ? costUsd / (outcomes.linesChanged / 1e3) : void 0,
2263
+ tokensPerCommit: outcomes.commitCount > 0 ? usage.totalTokens / outcomes.commitCount : void 0,
2264
+ nonCacheTokensPerCommit: outcomes.commitCount > 0 ? nonCacheTotalTokens / outcomes.commitCount : void 0,
2265
+ commitsPerUsd: costUsd !== void 0 && costUsd > 0 ? outcomes.commitCount / costUsd : void 0
2266
+ };
2267
+ }
2268
+ function aggregateEfficiency(options) {
2269
+ const usageTotalsByPeriod = buildUsageTotalsByPeriod(options.usageRows);
2270
+ const periodKeys = [
2271
+ .../* @__PURE__ */ new Set([...usageTotalsByPeriod.keys(), ...options.periodOutcomes.keys()])
2272
+ ].sort(compareByCodePoint);
2273
+ const rows = [];
2274
+ let totalUsage = createEmptyEfficiencyUsageTotals();
2275
+ let totalOutcomes = createEmptyEfficiencyOutcomeTotals();
2276
+ for (const periodKey of periodKeys) {
2277
+ const usageTotals = usageTotalsByPeriod.get(periodKey) ?? createEmptyEfficiencyUsageTotals();
2278
+ const outcomeTotals = options.periodOutcomes.get(periodKey) ?? createEmptyEfficiencyOutcomeTotals();
2279
+ const hasUsageRow = usageTotalsByPeriod.has(periodKey);
2280
+ const hasUsageSignal3 = hasUsageRow && (usageTotals.totalTokens > 0 || usageTotals.costUsd !== void 0 || usageTotals.costIncomplete === true);
2281
+ if (outcomeTotals.commitCount === 0 || !hasUsageSignal3) {
2282
+ continue;
2283
+ }
2284
+ const derived = computeDerivedMetrics(usageTotals, outcomeTotals);
2285
+ rows.push({
2286
+ rowType: "period",
2287
+ periodKey,
2288
+ ...usageTotals,
2289
+ ...outcomeTotals,
2290
+ ...derived
2291
+ });
2292
+ totalUsage = addUsageTotals(totalUsage, usageTotals);
2293
+ totalOutcomes = addOutcomeTotals(totalOutcomes, outcomeTotals);
2294
+ }
2295
+ const finalizedTotalUsage = totalUsage.costUsd === void 0 && totalUsage.costIncomplete !== true && totalUsage.totalTokens === 0 ? { ...totalUsage, costUsd: 0 } : totalUsage;
2296
+ rows.push({
2297
+ rowType: "grand_total",
2298
+ periodKey: "ALL",
2299
+ ...finalizedTotalUsage,
2300
+ ...totalOutcomes,
2301
+ ...computeDerivedMetrics(finalizedTotalUsage, totalOutcomes)
2302
+ });
2303
+ return rows;
2304
+ }
2305
+
2306
+ // src/efficiency/git-outcome-collector.ts
2307
+ import { spawn as spawn2 } from "child_process";
2308
+ import { createInterface as createInterface3 } from "readline";
2309
+ import path7 from "path";
2310
+ import { stat as stat2 } from "fs/promises";
2311
+ var GIT_COMMIT_MARKER = "";
2312
+ var SHORTSTAT_PATTERN = /(\d+)\s+files?\s+changed(?:,\s+(\d+)\s+insertions?\(\+\))?(?:,\s+(\d+)\s+deletions?\(-\))?/u;
2313
+ function shiftDate(value, days) {
2314
+ const date = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
2315
+ if (Number.isNaN(date.getTime())) {
2316
+ throw new Error(`Invalid date value: ${value}`);
2317
+ }
2318
+ date.setUTCDate(date.getUTCDate() + days);
2319
+ return date.toISOString().slice(0, 10);
2320
+ }
2321
+ function escapeGitRegexLiteral(value) {
2322
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
2323
+ }
2324
+ function resolveGitCommandFailureReason(result) {
2325
+ return result.stderr.trim() || `git exited with code ${result.exitCode}`;
2326
+ }
2327
+ function resolveRepoDir(repoDir) {
2328
+ if (repoDir === void 0) {
2329
+ return path7.resolve(process.cwd());
2330
+ }
2331
+ const normalizedRepoDir = repoDir.trim();
2332
+ if (!normalizedRepoDir) {
2333
+ throw new Error("--repo-dir must be a non-empty path");
2334
+ }
2335
+ return path7.resolve(normalizedRepoDir);
2336
+ }
2337
+ function getNodeErrorCode2(error) {
2338
+ if (typeof error !== "object" || error === null || !("code" in error)) {
2339
+ return void 0;
2340
+ }
2341
+ const record = error;
2342
+ return typeof record.code === "string" ? record.code : void 0;
2343
+ }
2344
+ async function assertRepoDirReadable(repoDir) {
2345
+ let directoryStats;
2346
+ try {
2347
+ directoryStats = await stat2(repoDir);
2348
+ } catch (error) {
2349
+ const code = getNodeErrorCode2(error);
2350
+ if (code === "ENOENT") {
2351
+ throw new Error(`Repository path does not exist: ${repoDir}`, { cause: error });
2352
+ }
2353
+ if (code === "EACCES" || code === "EPERM") {
2354
+ throw new Error(`Repository path is unreadable: ${repoDir}`, { cause: error });
2355
+ }
2356
+ throw error;
2357
+ }
2358
+ if (!directoryStats.isDirectory()) {
2359
+ throw new Error(`Repository path is not a directory: ${repoDir}`);
2360
+ }
2361
+ }
2362
+ async function assertGitRepository(repoDir, runCommand) {
2363
+ const gitRepoResult = await runCommand(repoDir, ["rev-parse", "--is-inside-work-tree"]);
2364
+ if (gitRepoResult.exitCode === 0) {
2365
+ return;
2366
+ }
2367
+ throw new Error(`Repository is not a git repository: ${repoDir}`);
2368
+ }
2369
+ function isNoCommitHistoryFailure(result) {
2370
+ if (result.exitCode !== 128) {
2371
+ return false;
2372
+ }
2373
+ const reason = result.stderr.toLowerCase();
2374
+ 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'");
2375
+ }
2376
+ function createEmptyOutcomeCollection(repoDir, includeMergeCommits) {
2377
+ return {
2378
+ periodOutcomes: /* @__PURE__ */ new Map(),
2379
+ totalOutcomes: createEmptyEfficiencyOutcomeTotals(),
2380
+ diagnostics: {
2381
+ repoDir,
2382
+ includeMergeCommits,
2383
+ commitsCollected: 0,
2384
+ linesAdded: 0,
2385
+ linesDeleted: 0
2386
+ }
2387
+ };
2388
+ }
2389
+ function isMissingGitUserEmailError(error) {
2390
+ return error instanceof Error && error.message.startsWith("Git user.email is not configured for");
2391
+ }
2392
+ function resolveConfiguredEmailFromLines(lines) {
2393
+ return lines.map((line) => line.trim()).find((line) => line.length > 0);
2394
+ }
2395
+ function resolveEmailFromGitAuthorIdent(lines) {
2396
+ const identLine = lines.map((line) => line.trim()).find((line) => line.length > 0);
2397
+ if (!identLine) {
2398
+ return void 0;
2399
+ }
2400
+ const emailMatch = /<([^>]+)>/u.exec(identLine);
2401
+ const email = emailMatch?.[1]?.trim();
2402
+ return email && email.length > 0 ? email : void 0;
2403
+ }
2404
+ async function resolveConfiguredAuthorEmail(repoDir, runCommand) {
2405
+ const configLookupAttempts = [
2406
+ ["config", "--get", "user.email"],
2407
+ ["config", "--global", "--get", "user.email"]
2408
+ ];
2409
+ for (const args of configLookupAttempts) {
2410
+ const configResult = await runCommand(repoDir, args);
2411
+ if (configResult.exitCode === 0) {
2412
+ const configuredEmail = resolveConfiguredEmailFromLines(configResult.lines);
2413
+ if (configuredEmail) {
2414
+ return configuredEmail;
2415
+ }
2416
+ continue;
2417
+ }
2418
+ if (configResult.exitCode !== 1) {
2419
+ const reason = resolveGitCommandFailureReason(configResult);
2420
+ throw new Error(`Failed to resolve git user.email from ${repoDir}: ${reason}`);
2421
+ }
2422
+ }
2423
+ const gitAuthorIdentResult = await runCommand(repoDir, ["var", "GIT_AUTHOR_IDENT"]);
2424
+ if (gitAuthorIdentResult.exitCode === 0) {
2425
+ const authorIdentEmail = resolveEmailFromGitAuthorIdent(gitAuthorIdentResult.lines);
2426
+ if (authorIdentEmail) {
2427
+ return authorIdentEmail;
2428
+ }
2429
+ }
2430
+ throw new Error(
2431
+ `Git user.email is not configured for ${repoDir}. Run: git -C ${repoDir} config user.email "you@example.com"`
2432
+ );
2433
+ }
2434
+ function toIsoTimestamp(timestampSeconds) {
2435
+ const timestamp = new Date(timestampSeconds * 1e3);
2436
+ if (Number.isNaN(timestamp.getTime())) {
2437
+ throw new Error(`Invalid git commit timestamp: ${timestampSeconds}`);
2438
+ }
2439
+ return timestamp.toISOString();
2440
+ }
2441
+ function parseShortstatLine(line) {
2442
+ const shortstatMatch = SHORTSTAT_PATTERN.exec(line.trim());
2443
+ if (!shortstatMatch) {
2444
+ return void 0;
2445
+ }
2446
+ const linesAddedRaw = shortstatMatch[2];
2447
+ const linesDeletedRaw = shortstatMatch[3];
2448
+ return {
2449
+ linesAdded: linesAddedRaw ? Number.parseInt(linesAddedRaw, 10) : 0,
2450
+ linesDeleted: linesDeletedRaw ? Number.parseInt(linesDeletedRaw, 10) : 0
2451
+ };
2452
+ }
2453
+ function finalizeCurrentEvent(currentEvent, events, authorEmail) {
2454
+ if (!currentEvent) {
2455
+ return;
2456
+ }
2457
+ if (authorEmail && currentEvent.authorEmail.trim().toLowerCase() !== authorEmail.trim().toLowerCase()) {
2458
+ return;
2459
+ }
2460
+ const timestamp = toIsoTimestamp(currentEvent.timestampSeconds);
2461
+ events.push({
2462
+ sha: currentEvent.sha,
2463
+ timestamp,
2464
+ linesAdded: currentEvent.linesAdded,
2465
+ linesDeleted: currentEvent.linesDeleted,
2466
+ linesChanged: currentEvent.linesAdded + currentEvent.linesDeleted
2467
+ });
2468
+ }
2469
+ function parseGitLogShortstatLines(lines, authorEmail) {
2470
+ const events = [];
2471
+ let currentEvent;
2472
+ for (const line of lines) {
2473
+ if (line.startsWith(GIT_COMMIT_MARKER)) {
2474
+ const commitParts = line.slice(1).split(GIT_COMMIT_MARKER);
2475
+ const timestampPart = commitParts[0];
2476
+ const shaPart = commitParts[1];
2477
+ const authorPart = commitParts[2];
2478
+ if (commitParts.length !== 3 || !/^\d+$/u.test(timestampPart) || !/^[0-9a-f]{7,64}$/iu.test(shaPart) || authorPart.trim().length === 0) {
2479
+ throw new Error(`Malformed git commit boundary line: ${line}`);
2480
+ }
2481
+ finalizeCurrentEvent(currentEvent, events, authorEmail);
2482
+ currentEvent = {
2483
+ timestampSeconds: Number.parseInt(timestampPart, 10),
2484
+ sha: shaPart,
2485
+ authorEmail: authorPart,
2486
+ linesAdded: 0,
2487
+ linesDeleted: 0
2488
+ };
2489
+ continue;
2490
+ }
2491
+ if (!currentEvent) {
2492
+ continue;
2493
+ }
2494
+ const shortstat = parseShortstatLine(line);
2495
+ if (!shortstat) {
2496
+ continue;
2497
+ }
2498
+ currentEvent.linesAdded += shortstat.linesAdded;
2499
+ currentEvent.linesDeleted += shortstat.linesDeleted;
2500
+ }
2501
+ finalizeCurrentEvent(currentEvent, events, authorEmail);
2502
+ return events;
2503
+ }
2504
+ async function runGitCommand(repoDir, args) {
2505
+ return await new Promise((resolve, reject) => {
2506
+ const child = spawn2("git", args, {
2507
+ cwd: repoDir,
2508
+ env: {
2509
+ ...process.env,
2510
+ LC_ALL: "C",
2511
+ LANG: "C"
2512
+ },
2513
+ stdio: ["ignore", "pipe", "pipe"]
2514
+ });
2515
+ const lines = [];
2516
+ let stderr = "";
2517
+ const stdoutReader = createInterface3({ input: child.stdout });
2518
+ const stdoutPromise = (async () => {
2519
+ for await (const line of stdoutReader) {
2520
+ lines.push(line);
2521
+ }
2522
+ })();
2523
+ child.stderr.setEncoding("utf8");
2524
+ child.stderr.on("data", (chunk) => {
2525
+ stderr += chunk;
2526
+ });
2527
+ child.once("error", (error) => {
2528
+ reject(error);
2529
+ });
2530
+ child.once("close", (exitCode) => {
2531
+ void stdoutPromise.then(() => {
2532
+ resolve({
2533
+ lines,
2534
+ stderr,
2535
+ exitCode: exitCode ?? 1
2536
+ });
2537
+ }).catch((error) => {
2538
+ reject(error instanceof Error ? error : new Error(String(error)));
2539
+ });
2540
+ });
2541
+ });
2542
+ }
2543
+ function filterEventsByDateRange(events, timezone, since, until) {
2544
+ const normalizedSince = since ? shiftDate(since, 0) : void 0;
2545
+ const normalizedUntil = until ? shiftDate(until, 0) : void 0;
2546
+ return events.filter((event) => {
2547
+ const eventDate = getPeriodKey(event.timestamp, "daily", timezone);
2548
+ if (normalizedSince && eventDate < normalizedSince) {
2549
+ return false;
2550
+ }
2551
+ if (normalizedUntil && eventDate > normalizedUntil) {
2552
+ return false;
2553
+ }
2554
+ return true;
2555
+ });
2556
+ }
2557
+ function filterEventsByActiveUsageDays(events, timezone, activeUsageDays) {
2558
+ if (activeUsageDays === void 0) {
2559
+ return events;
2560
+ }
2561
+ if (activeUsageDays.size === 0) {
2562
+ return [];
2563
+ }
2564
+ return events.filter(
2565
+ (event) => activeUsageDays.has(getPeriodKey(event.timestamp, "daily", timezone))
2566
+ );
2567
+ }
2568
+ function aggregatePeriodOutcomes(events, granularity, timezone) {
2569
+ const periodOutcomes = /* @__PURE__ */ new Map();
2570
+ const totalOutcomes = createEmptyEfficiencyOutcomeTotals();
2571
+ for (const event of events) {
2572
+ const periodKey = getPeriodKey(event.timestamp, granularity, timezone);
2573
+ const periodTotals = periodOutcomes.get(periodKey) ?? createEmptyEfficiencyOutcomeTotals();
2574
+ periodTotals.commitCount += 1;
2575
+ periodTotals.linesAdded += event.linesAdded;
2576
+ periodTotals.linesDeleted += event.linesDeleted;
2577
+ periodTotals.linesChanged += event.linesChanged;
2578
+ periodOutcomes.set(periodKey, periodTotals);
2579
+ totalOutcomes.commitCount += 1;
2580
+ totalOutcomes.linesAdded += event.linesAdded;
2581
+ totalOutcomes.linesDeleted += event.linesDeleted;
2582
+ totalOutcomes.linesChanged += event.linesChanged;
2583
+ }
2584
+ return {
2585
+ periodOutcomes,
2586
+ totalOutcomes
2587
+ };
2588
+ }
2589
+ function buildGitLogArgs(options) {
2590
+ const args = [
2591
+ "log",
2592
+ `--pretty=format:${GIT_COMMIT_MARKER}%ct${GIT_COMMIT_MARKER}%H${GIT_COMMIT_MARKER}%ae`,
2593
+ "--shortstat",
2594
+ "--regexp-ignore-case",
2595
+ `--author=<${escapeGitRegexLiteral(options.authorEmail)}>`
2596
+ ];
2597
+ if (!options.includeMergeCommits) {
2598
+ args.push("--no-merges");
2599
+ }
2600
+ if (options.since) {
2601
+ args.push(`--since=${shiftDate(options.since, -1)}T00:00:00Z`);
2602
+ }
2603
+ if (options.until) {
2604
+ args.push(`--until=${shiftDate(options.until, 1)}T23:59:59Z`);
2605
+ }
2606
+ return args;
2607
+ }
2608
+ function resolveGitLogDateWindow(options) {
2609
+ if (!options.activeUsageDays || options.activeUsageDays.size === 0) {
2610
+ return {
2611
+ since: options.since,
2612
+ until: options.until
2613
+ };
2614
+ }
2615
+ let earliestUsageDay;
2616
+ let latestUsageDay;
2617
+ for (const usageDay of options.activeUsageDays) {
2618
+ if (!earliestUsageDay || usageDay < earliestUsageDay) {
2619
+ earliestUsageDay = usageDay;
2620
+ }
2621
+ if (!latestUsageDay || usageDay > latestUsageDay) {
2622
+ latestUsageDay = usageDay;
2623
+ }
2624
+ }
2625
+ return {
2626
+ since: options.since ?? earliestUsageDay,
2627
+ until: options.until ?? latestUsageDay
2628
+ };
2629
+ }
2630
+ async function collectGitOutcomes(options, deps = {}) {
2631
+ const repoDir = resolveRepoDir(options.repoDir);
2632
+ const includeMergeCommits = options.includeMergeCommits ?? false;
2633
+ const runCommand = deps.runGitCommand ?? runGitCommand;
2634
+ if (options.activeUsageDays?.size === 0) {
2635
+ return createEmptyOutcomeCollection(repoDir, includeMergeCommits);
2636
+ }
2637
+ if (!deps.runGitCommand) {
2638
+ await assertRepoDirReadable(repoDir);
2639
+ await assertGitRepository(repoDir, runCommand);
2640
+ }
2641
+ let authorEmail;
2642
+ try {
2643
+ authorEmail = await resolveConfiguredAuthorEmail(repoDir, runCommand);
2644
+ } catch (error) {
2645
+ if (!isMissingGitUserEmailError(error)) {
2646
+ throw error;
2647
+ }
2648
+ const headResult = await runCommand(repoDir, ["rev-parse", "--verify", "HEAD"]);
2649
+ if (isNoCommitHistoryFailure(headResult)) {
2650
+ return createEmptyOutcomeCollection(repoDir, includeMergeCommits);
2651
+ }
2652
+ throw error;
2653
+ }
2654
+ const gitLogDateWindow = resolveGitLogDateWindow({
2655
+ since: options.since,
2656
+ until: options.until,
2657
+ activeUsageDays: options.activeUsageDays
2658
+ });
2659
+ const gitResult = await runCommand(
2660
+ repoDir,
2661
+ buildGitLogArgs({
2662
+ since: gitLogDateWindow.since,
2663
+ until: gitLogDateWindow.until,
2664
+ includeMergeCommits,
2665
+ authorEmail
2666
+ })
2667
+ );
2668
+ if (gitResult.exitCode !== 0) {
2669
+ if (isNoCommitHistoryFailure(gitResult)) {
2670
+ return createEmptyOutcomeCollection(repoDir, includeMergeCommits);
2671
+ }
2672
+ const reason = resolveGitCommandFailureReason(gitResult);
2673
+ throw new Error(`Failed to collect git outcomes from ${repoDir}: ${reason}`);
2674
+ }
2675
+ const allEvents = parseGitLogShortstatLines(gitResult.lines, authorEmail);
2676
+ const filteredEvents = filterEventsByDateRange(
2677
+ allEvents,
2678
+ options.timezone,
2679
+ options.since,
2680
+ options.until
2681
+ );
2682
+ const usageAttributedEvents = filterEventsByActiveUsageDays(
2683
+ filteredEvents,
2684
+ options.timezone,
2685
+ options.activeUsageDays
2686
+ );
2687
+ const { periodOutcomes, totalOutcomes } = aggregatePeriodOutcomes(
2688
+ usageAttributedEvents,
2689
+ options.granularity,
2690
+ options.timezone
2691
+ );
2692
+ return {
2693
+ periodOutcomes,
2694
+ totalOutcomes,
2695
+ diagnostics: {
2696
+ repoDir,
2697
+ includeMergeCommits,
2698
+ commitsCollected: totalOutcomes.commitCount,
2699
+ linesAdded: totalOutcomes.linesAdded,
2700
+ linesDeleted: totalOutcomes.linesDeleted
2701
+ }
2702
+ };
2703
+ }
2704
+
2705
+ // src/efficiency/repo-attribution.ts
2706
+ import { access as access2, constants as constants2, realpath } from "fs/promises";
2707
+ import path8 from "path";
2708
+ async function hasGitMarker(directoryPath) {
2709
+ try {
2710
+ await access2(path8.join(directoryPath, ".git"), constants2.F_OK);
2711
+ return true;
2712
+ } catch {
2713
+ return false;
2714
+ }
2715
+ }
2716
+ function normalizeComparablePath(value) {
2717
+ const normalizedPath = path8.normalize(path8.resolve(value));
2718
+ return process.platform === "win32" ? normalizedPath.toLowerCase() : normalizedPath;
2719
+ }
2720
+ async function resolveComparablePath(value) {
2721
+ const resolvedPath = path8.resolve(value);
2722
+ try {
2723
+ return normalizeComparablePath(await realpath(resolvedPath));
2724
+ } catch {
2725
+ return normalizeComparablePath(resolvedPath);
2726
+ }
2727
+ }
2728
+ async function resolveRepoRootFromPathHint(pathHint) {
2729
+ const trimmedPath = pathHint.trim();
2730
+ if (!trimmedPath) {
2731
+ return void 0;
2732
+ }
2733
+ let currentPath = path8.resolve(trimmedPath);
2734
+ for (; ; ) {
2735
+ if (await hasGitMarker(currentPath)) {
2736
+ return currentPath;
2737
+ }
2738
+ const parentPath = path8.dirname(currentPath);
2739
+ if (parentPath === currentPath) {
2740
+ return void 0;
2741
+ }
2742
+ currentPath = parentPath;
2743
+ }
2744
+ }
2745
+ async function attributeUsageEventsToRepo(events, repoDir, resolveRepoRoot2 = resolveRepoRootFromPathHint) {
2746
+ const resolvedTargetRepoRoot = await resolveRepoRoot2(repoDir).catch(() => void 0);
2747
+ const targetRepoPath = await resolveComparablePath(resolvedTargetRepoRoot ?? repoDir);
2748
+ const rootCache = /* @__PURE__ */ new Map();
2749
+ const matchedEvents = [];
2750
+ let excludedEventCount = 0;
2751
+ let unattributedEventCount = 0;
2752
+ for (const event of events) {
2753
+ if (!event.repoRoot) {
2754
+ unattributedEventCount += 1;
2755
+ continue;
2756
+ }
2757
+ const eventRepoRoot = event.repoRoot;
2758
+ const cachedRootPromise = rootCache.get(eventRepoRoot) ?? (async () => {
2759
+ const resolvedRoot2 = await resolveRepoRoot2(eventRepoRoot).catch(() => void 0);
2760
+ if (!resolvedRoot2) {
2761
+ return void 0;
2762
+ }
2763
+ return {
2764
+ resolvedRoot: resolvedRoot2,
2765
+ comparableRoot: await resolveComparablePath(resolvedRoot2)
2766
+ };
2767
+ })();
2768
+ rootCache.set(eventRepoRoot, cachedRootPromise);
2769
+ const resolvedRoot = await cachedRootPromise;
2770
+ if (!resolvedRoot) {
2771
+ unattributedEventCount += 1;
2772
+ continue;
2773
+ }
2774
+ if (resolvedRoot.comparableRoot !== targetRepoPath) {
2775
+ excludedEventCount += 1;
2776
+ continue;
2777
+ }
2778
+ matchedEvents.push({
2779
+ ...event,
2780
+ repoRoot: resolvedRoot.resolvedRoot
2781
+ });
2782
+ }
2783
+ return {
2784
+ matchedEvents,
2785
+ matchedEventCount: matchedEvents.length,
2786
+ excludedEventCount,
2787
+ unattributedEventCount
2788
+ };
2789
+ }
2790
+
2791
+ // src/config/env-var-display.ts
2792
+ var ENV_VARS_TO_DISPLAY = [
2793
+ { name: "LLM_USAGE_SKIP_UPDATE_CHECK", description: "skip startup update check" },
2794
+ {
2795
+ name: "LLM_USAGE_UPDATE_CACHE_SCOPE",
2796
+ description: "update-check cache scope (global/session)"
2797
+ },
2798
+ { name: "LLM_USAGE_UPDATE_CACHE_SESSION_KEY", description: "update-check session cache key" },
2799
+ { name: "LLM_USAGE_UPDATE_CACHE_TTL_MS", description: "update-check cache TTL" },
2800
+ { name: "LLM_USAGE_UPDATE_FETCH_TIMEOUT_MS", description: "update-check fetch timeout" },
2801
+ { name: "LLM_USAGE_PRICING_CACHE_TTL_MS", description: "pricing cache TTL" },
2802
+ { name: "LLM_USAGE_PRICING_FETCH_TIMEOUT_MS", description: "pricing fetch timeout" },
2803
+ { name: "LLM_USAGE_PARSE_MAX_PARALLEL", description: "max parallel file parsing" },
2804
+ { name: "LLM_USAGE_PARSE_CACHE_ENABLED", description: "enable file parse cache" },
2805
+ { name: "LLM_USAGE_PARSE_CACHE_TTL_MS", description: "file parse cache TTL" },
2806
+ { name: "LLM_USAGE_PARSE_CACHE_MAX_ENTRIES", description: "file parse cache max entries" },
2807
+ { name: "LLM_USAGE_PARSE_CACHE_MAX_BYTES", description: "file parse cache max bytes" }
2808
+ ];
2809
+ function getActiveEnvVarOverrides() {
2810
+ const overrides = [];
2811
+ for (const { name, description } of ENV_VARS_TO_DISPLAY) {
2812
+ const value = process.env[name];
2813
+ if (value !== void 0 && value !== "") {
2814
+ overrides.push({ name, value, description });
2815
+ }
2816
+ }
2817
+ return overrides;
2818
+ }
2819
+ function formatEnvVarOverrides(overrides) {
2820
+ if (overrides.length === 0) {
2821
+ return [];
2822
+ }
2823
+ const lines = [];
2824
+ lines.push("Active environment overrides:");
2825
+ for (const { name, value, description } of overrides) {
2826
+ lines.push(` ${name}=${value} (${description})`);
2827
+ }
2828
+ return lines;
2829
+ }
2830
+
2831
+ // src/cli/build-usage-data-diagnostics.ts
2832
+ function buildUsageDiagnostics(params) {
2833
+ const parseResultBySource = new Map(
2834
+ params.successfulParseResults.map((result) => [result.source.toLowerCase(), result])
2835
+ );
2836
+ const sessionStats = params.adaptersToParse.map((adapter) => {
2837
+ const parseResult = parseResultBySource.get(adapter.id.toLowerCase());
2838
+ return {
2839
+ source: adapter.id,
2840
+ filesFound: parseResult?.filesFound ?? 0,
2841
+ eventsParsed: parseResult?.events.length ?? 0
2842
+ };
2843
+ });
2844
+ const skippedRows = params.successfulParseResults.filter((result) => result.skippedRows > 0).map((result) => ({
2845
+ source: result.source,
2846
+ skippedRows: result.skippedRows,
2847
+ reasons: result.skippedRowReasons
2848
+ }));
2849
+ return {
2850
+ sessionStats,
2851
+ sourceFailures: params.sourceFailures,
2852
+ skippedRows,
2853
+ pricingOrigin: params.pricingOrigin,
2854
+ pricingWarning: params.pricingWarning,
2855
+ activeEnvOverrides: params.activeEnvOverrides,
2856
+ timezone: params.timezone
2857
+ };
2858
+ }
2859
+ function assembleUsageDataResult(events, rows, diagnostics) {
2860
+ return {
2861
+ events,
2862
+ rows,
2863
+ diagnostics
2864
+ };
2865
+ }
2866
+
2867
+ // src/cli/build-usage-data-inputs.ts
2868
+ function validateDateInput(value, flagName) {
2869
+ if (!/^\d{4}-\d{2}-\d{2}$/u.test(value)) {
2870
+ throw new Error(`${flagName} must use format YYYY-MM-DD`);
2073
2871
  }
2074
2872
  const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
2075
2873
  if (Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== value) {
@@ -2121,7 +2919,7 @@ function validateSourceFilterValues(sourceFilter, availableSourceIds) {
2121
2919
  if (unknownSources.length === 0) {
2122
2920
  return;
2123
2921
  }
2124
- const allowedSources = [...availableSourceIds].sort((left, right) => left.localeCompare(right));
2922
+ const allowedSources = [...availableSourceIds].sort(compareByCodePoint);
2125
2923
  throw new Error(
2126
2924
  `Unknown --source value(s): ${unknownSources.join(", ")}. Allowed values: ${allowedSources.join(", ")}`
2127
2925
  );
@@ -2196,10 +2994,19 @@ function resolveExplicitSourceIds(options, sourceFilter) {
2196
2994
  }
2197
2995
  return explicitSourceIds;
2198
2996
  }
2997
+ function detectDefaultTimezone() {
2998
+ const detectedTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
2999
+ if (typeof detectedTimezone === "string") {
3000
+ const trimmedDetectedTimezone = detectedTimezone.trim();
3001
+ if (trimmedDetectedTimezone.length > 0) {
3002
+ return trimmedDetectedTimezone;
3003
+ }
3004
+ }
3005
+ return "UTC";
3006
+ }
2199
3007
  function normalizeBuildUsageInputs(options) {
2200
3008
  const { normalizedPricingUrl } = validateBuildOptions(options);
2201
- const timezoneInput = options.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
2202
- const timezone = timezoneInput.trim();
3009
+ const timezone = options.timezone !== void 0 ? options.timezone.trim() : detectDefaultTimezone();
2203
3010
  validateTimezone(timezone);
2204
3011
  const providerFilter = normalizeProviderFilter(options.provider);
2205
3012
  const sourceFilter = normalizeSourceFilter(options.source);
@@ -2221,7 +3028,7 @@ function selectAdaptersForParsing(adapters, sourceFilter) {
2221
3028
  }
2222
3029
 
2223
3030
  // src/cli/build-usage-data-parsing.ts
2224
- import { stat } from "fs/promises";
3031
+ import { stat as stat3 } from "fs/promises";
2225
3032
 
2226
3033
  // src/cli/normalize-skipped-row-reasons.ts
2227
3034
  function toPositiveInteger(value) {
@@ -2250,12 +3057,15 @@ function normalizeSkippedRowReasons(value) {
2250
3057
 
2251
3058
  // src/cli/parse-file-cache.ts
2252
3059
  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;
3060
+ import path9 from "path";
3061
+ var PARSE_FILE_CACHE_VERSION = 2;
2255
3062
  var CACHE_KEY_SEPARATOR = "\0";
2256
3063
  function createCacheKey(source, filePath) {
2257
3064
  return `${source}${CACHE_KEY_SEPARATOR}${filePath}`;
2258
3065
  }
3066
+ function normalizeCacheSource(source) {
3067
+ return normalizeSourceId(source)?.toLowerCase() ?? "";
3068
+ }
2259
3069
  function toNonNegativeInteger(value) {
2260
3070
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
2261
3071
  return void 0;
@@ -2268,20 +3078,32 @@ function toNonNegativeNumber2(value) {
2268
3078
  }
2269
3079
  return value;
2270
3080
  }
3081
+ function normalizeCachedTimestamp(value) {
3082
+ if (typeof value !== "string") {
3083
+ return void 0;
3084
+ }
3085
+ const normalized = value.trim();
3086
+ if (!normalized) {
3087
+ return void 0;
3088
+ }
3089
+ const timestamp = new Date(normalized);
3090
+ if (Number.isNaN(timestamp.getTime())) {
3091
+ return void 0;
3092
+ }
3093
+ return timestamp.toISOString() === normalized ? normalized : void 0;
3094
+ }
2271
3095
  function normalizeCachedUsageEvent(value) {
2272
3096
  const record = asRecord(value);
2273
3097
  if (!record) {
2274
3098
  return void 0;
2275
3099
  }
2276
- const source = normalizeSourceId(record.source);
3100
+ const source = normalizeSourceId(record.source)?.toLowerCase();
2277
3101
  const sessionId = typeof record.sessionId === "string" ? record.sessionId.trim() : "";
2278
- const timestamp = typeof record.timestamp === "string" ? record.timestamp.trim() : "";
3102
+ const timestamp = normalizeCachedTimestamp(record.timestamp);
3103
+ const repoRoot = typeof record.repoRoot === "string" ? record.repoRoot.trim() : "";
2279
3104
  if (!source || !sessionId || !timestamp) {
2280
3105
  return void 0;
2281
3106
  }
2282
- if (Number.isNaN(new Date(timestamp).getTime())) {
2283
- return void 0;
2284
- }
2285
3107
  const costMode = record.costMode === "explicit" || record.costMode === "estimated" ? record.costMode : void 0;
2286
3108
  if (!costMode) {
2287
3109
  return void 0;
@@ -2296,7 +3118,7 @@ function normalizeCachedUsageEvent(value) {
2296
3118
  return void 0;
2297
3119
  }
2298
3120
  const provider = typeof record.provider === "string" ? record.provider.trim() : "";
2299
- const model = typeof record.model === "string" ? record.model.trim() : "";
3121
+ const model = typeof record.model === "string" ? record.model.trim().toLowerCase() : "";
2300
3122
  const costUsd = toNonNegativeNumber2(record.costUsd);
2301
3123
  if (costMode === "explicit" && costUsd === void 0) {
2302
3124
  return void 0;
@@ -2305,6 +3127,7 @@ function normalizeCachedUsageEvent(value) {
2305
3127
  source,
2306
3128
  sessionId,
2307
3129
  timestamp,
3130
+ repoRoot: repoRoot || void 0,
2308
3131
  provider: provider || void 0,
2309
3132
  model: model || void 0,
2310
3133
  inputTokens,
@@ -2324,7 +3147,7 @@ function cloneUsageEvents(events) {
2324
3147
  return events.map((event) => cloneUsageEvent(event));
2325
3148
  }
2326
3149
  function cloneSkippedRowReasons(skippedRowReasons) {
2327
- return (skippedRowReasons ?? []).map((stat2) => ({ reason: stat2.reason, count: stat2.count }));
3150
+ return (skippedRowReasons ?? []).map((stat4) => ({ reason: stat4.reason, count: stat4.count }));
2328
3151
  }
2329
3152
  function normalizeCachedEvents(value) {
2330
3153
  if (!Array.isArray(value)) {
@@ -2345,7 +3168,7 @@ function normalizeCacheEntry(value) {
2345
3168
  if (!record) {
2346
3169
  return void 0;
2347
3170
  }
2348
- const source = typeof record.source === "string" ? record.source.trim() : "";
3171
+ const source = normalizeSourceId(record.source)?.toLowerCase() ?? "";
2349
3172
  const filePath = typeof record.filePath === "string" ? record.filePath.trim() : "";
2350
3173
  const cachedAt = toNonNegativeInteger(record.cachedAt);
2351
3174
  const fingerprint = asRecord(record.fingerprint);
@@ -2373,7 +3196,7 @@ function normalizeCacheEntry(value) {
2373
3196
  };
2374
3197
  }
2375
3198
  function getDefaultParseFileCachePath() {
2376
- return path7.join(getUserCacheRootDir(), "llm-usage-metrics", "parse-file-cache.json");
3199
+ return path9.join(getUserCacheRootDir(), "llm-usage-metrics", "parse-file-cache.json");
2377
3200
  }
2378
3201
  var ParseFileCache = class _ParseFileCache {
2379
3202
  constructor(cacheFilePath, limits, now) {
@@ -2393,16 +3216,20 @@ var ParseFileCache = class _ParseFileCache {
2393
3216
  return cache;
2394
3217
  }
2395
3218
  get(source, filePath, fingerprint) {
2396
- const entry = this.entriesByKey.get(createCacheKey(source, filePath));
3219
+ const normalizedSource = normalizeCacheSource(source);
3220
+ const cacheKey = createCacheKey(normalizedSource, filePath);
3221
+ const entry = this.entriesByKey.get(cacheKey);
2397
3222
  if (!entry) {
2398
3223
  return void 0;
2399
3224
  }
2400
3225
  if (entry.cachedAt + this.limits.ttlMs < this.now()) {
2401
- this.entriesByKey.delete(createCacheKey(source, filePath));
3226
+ this.entriesByKey.delete(cacheKey);
2402
3227
  this.dirty = true;
2403
3228
  return void 0;
2404
3229
  }
2405
3230
  if (entry.fingerprint.size !== fingerprint.size || entry.fingerprint.mtimeMs !== fingerprint.mtimeMs) {
3231
+ this.entriesByKey.delete(cacheKey);
3232
+ this.dirty = true;
2406
3233
  return void 0;
2407
3234
  }
2408
3235
  return {
@@ -2412,8 +3239,9 @@ var ParseFileCache = class _ParseFileCache {
2412
3239
  };
2413
3240
  }
2414
3241
  set(source, filePath, fingerprint, diagnostics) {
2415
- this.entriesByKey.set(createCacheKey(source, filePath), {
2416
- source,
3242
+ const normalizedSource = normalizeCacheSource(source);
3243
+ this.entriesByKey.set(createCacheKey(normalizedSource, filePath), {
3244
+ source: normalizedSource,
2417
3245
  filePath,
2418
3246
  fingerprint: {
2419
3247
  size: fingerprint.size,
@@ -2454,7 +3282,7 @@ var ParseFileCache = class _ParseFileCache {
2454
3282
  keptEntries.length = bestCount;
2455
3283
  payloadText = bestPayloadText;
2456
3284
  }
2457
- await mkdir2(path7.dirname(this.cacheFilePath), { recursive: true });
3285
+ await mkdir2(path9.dirname(this.cacheFilePath), { recursive: true });
2458
3286
  await writeFile2(this.cacheFilePath, payloadText, "utf8");
2459
3287
  this.dirty = false;
2460
3288
  }
@@ -2559,7 +3387,7 @@ async function parseAdapterEvents(adapter, maxParallelFileParsing, parseFileCach
2559
3387
  let parseFileDiagnostics;
2560
3388
  if (parseFileCache) {
2561
3389
  try {
2562
- const fileStat = await stat(filePath);
3390
+ const fileStat = await stat3(filePath);
2563
3391
  fileFingerprint = {
2564
3392
  size: fileStat.size,
2565
3393
  mtimeMs: fileStat.mtimeMs
@@ -2651,6 +3479,16 @@ function matchesProvider(provider, providerFilter) {
2651
3479
  }
2652
3480
  return provider?.toLowerCase().includes(providerFilter) ?? false;
2653
3481
  }
3482
+ function isEventWithinDateRange(event, timezone, since, until) {
3483
+ const eventDate = getPeriodKey(event.timestamp, "daily", timezone);
3484
+ if (since && eventDate < since) {
3485
+ return false;
3486
+ }
3487
+ if (until && eventDate > until) {
3488
+ return false;
3489
+ }
3490
+ return true;
3491
+ }
2654
3492
  function resolveModelFilterRules(events, modelFilter) {
2655
3493
  if (!modelFilter || modelFilter.length === 0) {
2656
3494
  return void 0;
@@ -2675,41 +3513,24 @@ function matchesModel(model, modelRules) {
2675
3513
  (rule) => rule.mode === "exact" ? normalizedModel === rule.value : normalizedModel.includes(rule.value)
2676
3514
  );
2677
3515
  }
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
- );
3516
+ function filterByModelRules(events, modelFilter) {
3517
+ const modelFilterRules = resolveModelFilterRules(events, modelFilter);
3518
+ return events.filter((event) => matchesModel(event.model, modelFilterRules));
2707
3519
  }
2708
3520
  function filterParsedAdapterEvents(parseResults, options) {
2709
- return filterUsageEvents(
2710
- parseResults.flatMap((result) => result.events),
2711
- options
2712
- );
3521
+ const providerAndDateFilteredEvents = [];
3522
+ for (const result of parseResults) {
3523
+ for (const event of result.events) {
3524
+ if (!matchesProvider(event.provider, options.providerFilter)) {
3525
+ continue;
3526
+ }
3527
+ if (!isEventWithinDateRange(event, options.timezone, options.since, options.until)) {
3528
+ continue;
3529
+ }
3530
+ providerAndDateFilteredEvents.push(event);
3531
+ }
3532
+ }
3533
+ return filterByModelRules(providerAndDateFilteredEvents, options.modelFilter);
2713
3534
  }
2714
3535
 
2715
3536
  // src/pricing/cost-engine.ts
@@ -2756,7 +3577,7 @@ function applyPricingToEvents(events, pricingSource) {
2756
3577
 
2757
3578
  // src/pricing/litellm-pricing-fetcher.ts
2758
3579
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
2759
- import path8 from "path";
3580
+ import path10 from "path";
2760
3581
 
2761
3582
  // src/pricing/litellm-model-map.json
2762
3583
  var litellm_model_map_default = {
@@ -2926,7 +3747,7 @@ function normalizeLitellmPricingPayload(payload) {
2926
3747
  return normalizedPricing;
2927
3748
  }
2928
3749
  function getDefaultLiteLLMPricingCachePath() {
2929
- return path8.join(getUserCacheRootDir(), "llm-usage-metrics", "litellm-pricing-cache.json");
3750
+ return path10.join(getUserCacheRootDir(), "llm-usage-metrics", "litellm-pricing-cache.json");
2930
3751
  }
2931
3752
  function stripProviderPrefix(model) {
2932
3753
  const slashIndex = model.lastIndexOf("/");
@@ -3133,7 +3954,7 @@ var LiteLLMPricingFetcher = class {
3133
3954
  if (!isProviderPrefixedMatch) {
3134
3955
  continue;
3135
3956
  }
3136
- if (!bestMatch || modelName.length < bestMatch.length || modelName.length === bestMatch.length && modelName.localeCompare(bestMatch) < 0) {
3957
+ if (!bestMatch || modelName.length < bestMatch.length || modelName.length === bestMatch.length && compareByCodePoint(modelName, bestMatch) < 0) {
3137
3958
  bestMatch = modelName;
3138
3959
  }
3139
3960
  }
@@ -3151,7 +3972,7 @@ var LiteLLMPricingFetcher = class {
3151
3972
  if (!isPrefixModelMatch(candidate, modelName)) {
3152
3973
  continue;
3153
3974
  }
3154
- if (!bestMatch || modelName.length > bestMatch.length || modelName.length === bestMatch.length && modelName.localeCompare(bestMatch) < 0) {
3975
+ if (!bestMatch || modelName.length > bestMatch.length || modelName.length === bestMatch.length && compareByCodePoint(modelName, bestMatch) < 0) {
3155
3976
  bestMatch = modelName;
3156
3977
  }
3157
3978
  }
@@ -3320,7 +4141,7 @@ var LiteLLMPricingFetcher = class {
3320
4141
  };
3321
4142
  }
3322
4143
  async writeCache() {
3323
- const directoryPath = path8.dirname(this.cacheFilePath);
4144
+ const directoryPath = path10.dirname(this.cacheFilePath);
3324
4145
  await mkdir3(directoryPath, { recursive: true });
3325
4146
  const payload = {
3326
4147
  fetchedAt: this.now(),
@@ -3383,7 +4204,21 @@ async function resolveAndApplyPricingToEvents(events, options, runtimeConfig = g
3383
4204
  pricingOrigin
3384
4205
  };
3385
4206
  }
3386
- const pricingResult = await loadPricingSource(options, runtimeConfig);
4207
+ let pricingResult;
4208
+ try {
4209
+ pricingResult = await loadPricingSource(options, runtimeConfig);
4210
+ } catch (error) {
4211
+ if (!options.ignorePricingFailures) {
4212
+ throw error;
4213
+ }
4214
+ const reason = error instanceof Error ? error.message : String(error);
4215
+ const pricingWarning = reason.trim().startsWith("Could not load") ? reason : `Could not load pricing; continuing without estimated costs: ${reason}`;
4216
+ return {
4217
+ pricedEvents: events,
4218
+ pricingOrigin,
4219
+ pricingWarning
4220
+ };
4221
+ }
3387
4222
  pricingOrigin = pricingResult.origin;
3388
4223
  return {
3389
4224
  pricedEvents: applyPricingToEvents(events, pricingResult.source),
@@ -3433,7 +4268,7 @@ async function buildUsageData(granularity, options, deps = {}) {
3433
4268
  modelFilter: normalizedInputs.modelFilter
3434
4269
  });
3435
4270
  const pricingOptions = withNormalizedPricingUrl(options, normalizedInputs.pricingUrl);
3436
- const { pricedEvents, pricingOrigin } = await resolveAndApplyPricingToEvents(
4271
+ const { pricedEvents, pricingOrigin, pricingWarning } = await resolveAndApplyPricingToEvents(
3437
4272
  filteredEvents,
3438
4273
  pricingOptions,
3439
4274
  pricingRuntimeConfig,
@@ -3449,10 +4284,114 @@ async function buildUsageData(granularity, options, deps = {}) {
3449
4284
  successfulParseResults,
3450
4285
  sourceFailures,
3451
4286
  pricingOrigin,
4287
+ pricingWarning,
3452
4288
  activeEnvOverrides: readEnvVarOverrides(),
3453
4289
  timezone: normalizedInputs.timezone
3454
4290
  });
3455
- return assembleUsageDataResult(rows, diagnostics);
4291
+ return assembleUsageDataResult(pricedEvents, rows, diagnostics);
4292
+ }
4293
+
4294
+ // src/cli/build-efficiency-data.ts
4295
+ function hasActiveRepeatedFilter(value) {
4296
+ if (!value) {
4297
+ return false;
4298
+ }
4299
+ const values = Array.isArray(value) ? value : [value];
4300
+ return values.some(
4301
+ (entry) => entry.split(",").map((candidate) => candidate.trim()).some((candidate) => candidate.length > 0)
4302
+ );
4303
+ }
4304
+ function hasActiveProviderFilter(provider) {
4305
+ return Boolean(provider?.trim());
4306
+ }
4307
+ function hasActiveTextOption(value) {
4308
+ return Boolean(value?.trim());
4309
+ }
4310
+ function resolveScopeNote(options) {
4311
+ const activeFilters = [];
4312
+ if (hasActiveTextOption(options.piDir)) {
4313
+ activeFilters.push("--pi-dir");
4314
+ }
4315
+ if (hasActiveTextOption(options.codexDir)) {
4316
+ activeFilters.push("--codex-dir");
4317
+ }
4318
+ if (hasActiveTextOption(options.opencodeDb)) {
4319
+ activeFilters.push("--opencode-db");
4320
+ }
4321
+ if (hasActiveRepeatedFilter(options.sourceDir)) {
4322
+ activeFilters.push("--source-dir");
4323
+ }
4324
+ if (hasActiveRepeatedFilter(options.source)) {
4325
+ activeFilters.push("--source");
4326
+ }
4327
+ if (hasActiveProviderFilter(options.provider)) {
4328
+ activeFilters.push("--provider");
4329
+ }
4330
+ if (hasActiveRepeatedFilter(options.model)) {
4331
+ activeFilters.push("--model");
4332
+ }
4333
+ if (activeFilters.length === 0) {
4334
+ return void 0;
4335
+ }
4336
+ return `Usage filters (${activeFilters.join(", ")}) affect commit attribution too: only commit days with matching repo-attributed usage events are counted.`;
4337
+ }
4338
+ function hasMeaningfulEfficiencyUsageSignal(event) {
4339
+ return event.totalTokens > 0 || event.costUsd !== void 0 && event.costUsd > 0;
4340
+ }
4341
+ async function buildEfficiencyData(granularity, options, deps = {}) {
4342
+ const buildUsage = deps.buildUsageData ?? buildUsageData;
4343
+ const collectOutcomes = deps.collectGitOutcomes ?? collectGitOutcomes;
4344
+ const resolveRepoRoot2 = deps.resolveRepoRoot ?? resolveRepoRootFromPathHint;
4345
+ const repoDir = options.repoDir?.trim();
4346
+ if (options.repoDir !== void 0 && !repoDir) {
4347
+ throw new Error("--repo-dir must be a non-empty path");
4348
+ }
4349
+ const usageData = await buildUsage(granularity, options);
4350
+ const attribution = await attributeUsageEventsToRepo(
4351
+ usageData.events,
4352
+ repoDir ?? process.cwd(),
4353
+ resolveRepoRoot2
4354
+ );
4355
+ const matchedEventsWithSignal = attribution.matchedEvents.filter(
4356
+ (event) => hasMeaningfulEfficiencyUsageSignal(event)
4357
+ );
4358
+ const activeUsageDays = new Set(
4359
+ matchedEventsWithSignal.map(
4360
+ (event) => getPeriodKey(event.timestamp, "daily", usageData.diagnostics.timezone)
4361
+ )
4362
+ );
4363
+ const gitOutcomes = await collectOutcomes({
4364
+ repoDir,
4365
+ granularity,
4366
+ timezone: usageData.diagnostics.timezone,
4367
+ since: options.since,
4368
+ until: options.until,
4369
+ includeMergeCommits: options.includeMergeCommits,
4370
+ activeUsageDays
4371
+ });
4372
+ const repoScopedUsageRows = aggregateUsage(matchedEventsWithSignal, {
4373
+ granularity,
4374
+ timezone: usageData.diagnostics.timezone
4375
+ });
4376
+ const rows = aggregateEfficiency({
4377
+ usageRows: repoScopedUsageRows,
4378
+ periodOutcomes: gitOutcomes.periodOutcomes
4379
+ });
4380
+ return {
4381
+ rows,
4382
+ diagnostics: {
4383
+ usage: usageData.diagnostics,
4384
+ repoDir: gitOutcomes.diagnostics.repoDir,
4385
+ includeMergeCommits: gitOutcomes.diagnostics.includeMergeCommits,
4386
+ gitCommitCount: gitOutcomes.diagnostics.commitsCollected,
4387
+ gitLinesAdded: gitOutcomes.diagnostics.linesAdded,
4388
+ gitLinesDeleted: gitOutcomes.diagnostics.linesDeleted,
4389
+ repoMatchedUsageEvents: attribution.matchedEventCount,
4390
+ repoExcludedUsageEvents: attribution.excludedEventCount,
4391
+ repoUnattributedUsageEvents: attribution.unattributedEventCount,
4392
+ scopeNote: resolveScopeNote(options)
4393
+ }
4394
+ };
3456
4395
  }
3457
4396
 
3458
4397
  // src/utils/logger.ts
@@ -3523,130 +4462,36 @@ function emitDiagnostics(diagnostics, diagnosticsLogger = logger) {
3523
4462
  switch (diagnostics.pricingOrigin) {
3524
4463
  case "offline-cache":
3525
4464
  diagnosticsLogger.info("Using cached pricing (offline mode)");
3526
- return;
4465
+ break;
3527
4466
  case "cache":
3528
4467
  diagnosticsLogger.info("Loaded pricing from cache");
3529
- return;
4468
+ break;
3530
4469
  case "network":
3531
4470
  diagnosticsLogger.info("Fetched pricing from LiteLLM");
3532
- return;
4471
+ break;
3533
4472
  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 "-";
4473
+ break;
3587
4474
  }
3588
- if (layout === "per_model_columns" && row.modelBreakdown.length > 1) {
3589
- return [...modelLines, "\u03A3 TOTAL"].join("\n");
4475
+ if (diagnostics.pricingWarning) {
4476
+ diagnosticsLogger.warn(diagnostics.pricingWarning);
3590
4477
  }
3591
- return modelLines.join("\n");
3592
4478
  }
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)));
4479
+
4480
+ // src/cli/emit-env-var-overrides.ts
4481
+ function emitEnvVarOverrides(activeEnvOverrides, diagnosticsLogger) {
4482
+ const envVarOverrideLines = formatEnvVarOverrides(activeEnvOverrides);
4483
+ if (envVarOverrideLines.length === 0) {
4484
+ return;
3600
4485
  }
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 });
4486
+ const [headerLine, ...envVarLines] = envVarOverrideLines;
4487
+ if (headerLine) {
4488
+ diagnosticsLogger.info(headerLine);
3606
4489
  }
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 }));
4490
+ for (const envVarLine of envVarLines) {
4491
+ diagnosticsLogger.dim(envVarLine);
3612
4492
  }
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
4493
  }
3646
4494
 
3647
- // src/render/report-header.ts
3648
- import pc2 from "picocolors";
3649
-
3650
4495
  // src/render/table-text-layout.ts
3651
4496
  var ansiEscapePattern = new RegExp(String.raw`\u001B\[[0-9;]*m`, "gu");
3652
4497
  var combiningMarkPattern = /\p{Mark}/u;
@@ -3811,16 +4656,50 @@ function wrapTableColumn(rows, options) {
3811
4656
  if (options.width <= 0) {
3812
4657
  throw new RangeError("wrapTableColumn width must be greater than 0");
3813
4658
  }
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
- });
4659
+ return rows.map((row) => {
4660
+ const wrappedRow = [...row];
4661
+ const cell = wrappedRow[options.columnIndex] ?? "";
4662
+ const wrappedLines = splitCellLines(cell).flatMap((line) => wrapPlainLine(line, options.width));
4663
+ wrappedRow[options.columnIndex] = wrappedLines.join("\n");
4664
+ return wrappedRow;
4665
+ });
4666
+ }
4667
+
4668
+ // src/cli/terminal-overflow-warning.ts
4669
+ function detectTerminalOverflowColumns(reportOutput, stdoutState) {
4670
+ const terminalColumns = resolveTtyColumns(stdoutState);
4671
+ if (terminalColumns === void 0) {
4672
+ return void 0;
4673
+ }
4674
+ const allLines = reportOutput.trimEnd().split("\n");
4675
+ const tableLikeLinePattern = /[│╭╮╰╯├┼┬┴┌┐└┘]|^\s*\|.*\|\s*$/u;
4676
+ const tableLines = allLines.filter((line) => tableLikeLinePattern.test(line));
4677
+ if (tableLines.length === 0) {
4678
+ return void 0;
4679
+ }
4680
+ const maxLineWidth = tableLines.reduce(
4681
+ (maxWidth, line) => Math.max(maxWidth, visibleWidth(line)),
4682
+ 0
4683
+ );
4684
+ if (maxLineWidth <= terminalColumns) {
4685
+ return void 0;
4686
+ }
4687
+ return maxLineWidth - terminalColumns;
4688
+ }
4689
+ function warnIfTerminalTableOverflows(reportOutput, warn, stdoutState = process.stdout) {
4690
+ const overflowColumns = detectTerminalOverflowColumns(reportOutput, stdoutState);
4691
+ if (overflowColumns !== void 0) {
4692
+ warn(
4693
+ `Report table is wider than terminal by ${overflowColumns} column(s). Use fullscreen/maximized terminal for better readability.`
4694
+ );
4695
+ }
3821
4696
  }
3822
4697
 
4698
+ // src/render/render-efficiency-report.ts
4699
+ import { markdownTable } from "markdown-table";
4700
+
3823
4701
  // src/render/report-header.ts
4702
+ import pc2 from "picocolors";
3824
4703
  function getBoxWidth(content) {
3825
4704
  return visibleWidth(content) + 4;
3826
4705
  }
@@ -3846,6 +4725,89 @@ function renderReportHeader(options) {
3846
4725
  return lines.join("\n");
3847
4726
  }
3848
4727
 
4728
+ // src/render/efficiency-row-cells.ts
4729
+ var efficiencyTableHeaders = [
4730
+ "Period",
4731
+ "Commits",
4732
+ "+Lines",
4733
+ "-Lines",
4734
+ "\u0394Lines",
4735
+ "Input",
4736
+ "Output",
4737
+ "Reasoning",
4738
+ "Cache Read",
4739
+ "Cache Write",
4740
+ "Total",
4741
+ "Cost",
4742
+ "$/Commit",
4743
+ "$/1k Lines",
4744
+ "All Tokens/Commit",
4745
+ "Non-Cache/Commit",
4746
+ "Commits/$"
4747
+ ];
4748
+ var integerFormatter = new Intl.NumberFormat("en-US");
4749
+ var decimalFormatter = new Intl.NumberFormat("en-US", {
4750
+ minimumFractionDigits: 2,
4751
+ maximumFractionDigits: 2
4752
+ });
4753
+ var usdFormatter = new Intl.NumberFormat("en-US", {
4754
+ style: "currency",
4755
+ currency: "USD",
4756
+ minimumFractionDigits: 2,
4757
+ maximumFractionDigits: 2
4758
+ });
4759
+ var usdRateFormatter = new Intl.NumberFormat("en-US", {
4760
+ style: "currency",
4761
+ currency: "USD",
4762
+ minimumFractionDigits: 4,
4763
+ maximumFractionDigits: 4
4764
+ });
4765
+ function formatInteger(value) {
4766
+ return integerFormatter.format(value);
4767
+ }
4768
+ function formatUsd(value, options = {}) {
4769
+ if (value === void 0) {
4770
+ return "-";
4771
+ }
4772
+ const formatted = usdFormatter.format(value);
4773
+ return options.approximate ? `~${formatted}` : formatted;
4774
+ }
4775
+ function formatUsdRate(value, options = {}) {
4776
+ if (value === void 0) {
4777
+ return "-";
4778
+ }
4779
+ const formatted = usdRateFormatter.format(value);
4780
+ return options.approximate ? `~${formatted}` : formatted;
4781
+ }
4782
+ function formatDecimal(value, options = {}) {
4783
+ if (value === void 0) {
4784
+ return "-";
4785
+ }
4786
+ const formatted = decimalFormatter.format(value);
4787
+ return options.approximate ? `~${formatted}` : formatted;
4788
+ }
4789
+ function toEfficiencyTableCells(rows) {
4790
+ return rows.map((row) => [
4791
+ row.periodKey,
4792
+ formatInteger(row.commitCount),
4793
+ formatInteger(row.linesAdded),
4794
+ formatInteger(row.linesDeleted),
4795
+ formatInteger(row.linesChanged),
4796
+ formatInteger(row.inputTokens),
4797
+ formatInteger(row.outputTokens),
4798
+ formatInteger(row.reasoningTokens),
4799
+ formatInteger(row.cacheReadTokens),
4800
+ formatInteger(row.cacheWriteTokens),
4801
+ formatInteger(row.totalTokens),
4802
+ formatUsd(row.costUsd, { approximate: row.costIncomplete }),
4803
+ formatUsdRate(row.usdPerCommit, { approximate: row.costIncomplete }),
4804
+ formatUsdRate(row.usdPer1kLinesChanged, { approximate: row.costIncomplete }),
4805
+ formatDecimal(row.tokensPerCommit),
4806
+ formatDecimal(row.nonCacheTokensPerCommit),
4807
+ formatDecimal(row.commitsPerUsd, { approximate: row.costIncomplete })
4808
+ ]);
4809
+ }
4810
+
3849
4811
  // src/render/terminal-table.ts
3850
4812
  import pc4 from "picocolors";
3851
4813
 
@@ -3924,6 +4886,96 @@ function colorizeUsageBodyRows(bodyRows, rows, options) {
3924
4886
  });
3925
4887
  }
3926
4888
 
4889
+ // src/render/row-cells.ts
4890
+ var usageTableHeaders = [
4891
+ "Period",
4892
+ "Source",
4893
+ "Models",
4894
+ "Input",
4895
+ "Output",
4896
+ "Reasoning",
4897
+ "Cache Read",
4898
+ "Cache Write",
4899
+ "Total",
4900
+ "Cost"
4901
+ ];
4902
+ var integerFormatter2 = new Intl.NumberFormat("en-US");
4903
+ var usdFormatter2 = new Intl.NumberFormat("en-US", {
4904
+ style: "currency",
4905
+ currency: "USD",
4906
+ minimumFractionDigits: 2,
4907
+ maximumFractionDigits: 2
4908
+ });
4909
+ function formatSource(row) {
4910
+ if (row.rowType === "grand_total") {
4911
+ return "TOTAL";
4912
+ }
4913
+ return row.source;
4914
+ }
4915
+ function formatTokenCount(value) {
4916
+ return integerFormatter2.format(value ?? 0);
4917
+ }
4918
+ function formatUsd2(value, options = {}) {
4919
+ if (value === void 0) {
4920
+ return "-";
4921
+ }
4922
+ const formattedUsd = usdFormatter2.format(value);
4923
+ return options.incomplete ? `~${formattedUsd}` : formattedUsd;
4924
+ }
4925
+ function buildModelLines(row) {
4926
+ if (row.modelBreakdown.length > 0) {
4927
+ return row.modelBreakdown.map((modelUsage) => `\u2022 ${modelUsage.model}`);
4928
+ }
4929
+ return row.models.map((model) => `\u2022 ${model}`);
4930
+ }
4931
+ function formatModels(row, layout) {
4932
+ const modelLines = buildModelLines(row);
4933
+ if (modelLines.length === 0) {
4934
+ return "-";
4935
+ }
4936
+ if (layout === "per_model_columns" && row.modelBreakdown.length > 1) {
4937
+ return [...modelLines, "\u03A3 TOTAL"].join("\n");
4938
+ }
4939
+ return modelLines.join("\n");
4940
+ }
4941
+ function formatModelMetric(row, selector, formatter, layout) {
4942
+ if (layout !== "per_model_columns" || row.modelBreakdown.length === 0) {
4943
+ return formatter(selector(row));
4944
+ }
4945
+ const lines = row.modelBreakdown.map((modelUsage) => formatter(selector(modelUsage)));
4946
+ if (row.modelBreakdown.length > 1) {
4947
+ lines.push(formatter(selector(row)));
4948
+ }
4949
+ return lines.join("\n");
4950
+ }
4951
+ function formatModelCostMetric(row, layout) {
4952
+ if (layout !== "per_model_columns" || row.modelBreakdown.length === 0) {
4953
+ return formatUsd2(row.costUsd, { incomplete: row.costIncomplete });
4954
+ }
4955
+ const lines = row.modelBreakdown.map(
4956
+ (modelUsage) => formatUsd2(modelUsage.costUsd, { incomplete: modelUsage.costIncomplete })
4957
+ );
4958
+ if (row.modelBreakdown.length > 1) {
4959
+ lines.push(formatUsd2(row.costUsd, { incomplete: row.costIncomplete }));
4960
+ }
4961
+ return lines.join("\n");
4962
+ }
4963
+ function toUsageTableCells(rows, options = {}) {
4964
+ const layout = options.layout ?? "compact";
4965
+ return rows.map((row) => [
4966
+ row.periodKey,
4967
+ formatSource(row),
4968
+ formatModels(row, layout),
4969
+ formatModelMetric(row, (value) => value.inputTokens, formatTokenCount, layout),
4970
+ formatModelMetric(row, (value) => value.outputTokens, formatTokenCount, layout),
4971
+ formatModelMetric(row, (value) => value.reasoningTokens, formatTokenCount, layout),
4972
+ formatModelMetric(row, (value) => value.cacheReadTokens, formatTokenCount, layout),
4973
+ formatModelMetric(row, (value) => value.cacheWriteTokens, formatTokenCount, layout),
4974
+ formatModelMetric(row, (value) => value.totalTokens, formatTokenCount, layout),
4975
+ formatModelCostMetric(row, layout)
4976
+ ]);
4977
+ }
4978
+
3927
4979
  // src/render/unicode-table.ts
3928
4980
  function getColumnAlignment(columnIndex, modelsColumnIndex2) {
3929
4981
  if (columnIndex <= modelsColumnIndex2) {
@@ -4233,8 +5285,174 @@ function renderTerminalTable(rows, options = {}) {
4233
5285
  return renderedTable;
4234
5286
  }
4235
5287
 
4236
- // src/render/render-usage-report.ts
5288
+ // src/render/render-efficiency-report.ts
4237
5289
  function getReportTitle(granularity) {
5290
+ switch (granularity) {
5291
+ case "daily":
5292
+ return "Daily Efficiency Report";
5293
+ case "weekly":
5294
+ return "Weekly Efficiency Report";
5295
+ case "monthly":
5296
+ return "Monthly Efficiency Report";
5297
+ }
5298
+ }
5299
+ function toTableSortRow(row) {
5300
+ if (row.rowType === "grand_total") {
5301
+ return {
5302
+ rowType: "grand_total",
5303
+ periodKey: "ALL",
5304
+ source: "combined",
5305
+ models: [],
5306
+ modelBreakdown: [],
5307
+ inputTokens: row.inputTokens,
5308
+ outputTokens: row.outputTokens,
5309
+ reasoningTokens: row.reasoningTokens,
5310
+ cacheReadTokens: row.cacheReadTokens,
5311
+ cacheWriteTokens: row.cacheWriteTokens,
5312
+ totalTokens: row.totalTokens,
5313
+ costUsd: row.costUsd,
5314
+ costIncomplete: row.costIncomplete
5315
+ };
5316
+ }
5317
+ return {
5318
+ rowType: "period_source",
5319
+ periodKey: row.periodKey,
5320
+ source: "combined",
5321
+ models: [],
5322
+ modelBreakdown: [],
5323
+ inputTokens: row.inputTokens,
5324
+ outputTokens: row.outputTokens,
5325
+ reasoningTokens: row.reasoningTokens,
5326
+ cacheReadTokens: row.cacheReadTokens,
5327
+ cacheWriteTokens: row.cacheWriteTokens,
5328
+ totalTokens: row.totalTokens,
5329
+ costUsd: row.costUsd,
5330
+ costIncomplete: row.costIncomplete
5331
+ };
5332
+ }
5333
+ function renderTerminalEfficiencyTable(rows) {
5334
+ const bodyRows = toEfficiencyTableCells(rows);
5335
+ const tableSortRows = rows.map((row) => toTableSortRow(row));
5336
+ const periodColumnWidth = Math.max(
5337
+ efficiencyTableHeaders[0].length,
5338
+ ...rows.map((row) => row.periodKey.length)
5339
+ );
5340
+ return renderUnicodeTable({
5341
+ headerCells: efficiencyTableHeaders,
5342
+ bodyRows,
5343
+ measureHeaderCells: efficiencyTableHeaders,
5344
+ measureBodyRows: bodyRows,
5345
+ usageRows: tableSortRows,
5346
+ tableLayout: "compact",
5347
+ modelsColumnIndex: 0,
5348
+ modelsColumnWidth: periodColumnWidth
5349
+ });
5350
+ }
5351
+ function toMarkdownSafeCell(value) {
5352
+ return value.replace(/\r?\n/gu, "<br>");
5353
+ }
5354
+ function renderMarkdownEfficiencyTable(rows) {
5355
+ const bodyRows = toEfficiencyTableCells(rows).map(
5356
+ (row) => row.map((cell) => toMarkdownSafeCell(cell))
5357
+ );
5358
+ const tableRows = [Array.from(efficiencyTableHeaders), ...bodyRows];
5359
+ const alignment2 = efficiencyTableHeaders.map((_, index) => index === 0 ? "l" : "r");
5360
+ return markdownTable(tableRows, {
5361
+ align: alignment2
5362
+ });
5363
+ }
5364
+ function renderTerminalEfficiencyReport(efficiencyData, options) {
5365
+ const outputLines = [];
5366
+ const useColor = options.useColor ?? shouldUseColorByDefault();
5367
+ outputLines.push(
5368
+ renderReportHeader({
5369
+ title: getReportTitle(options.granularity),
5370
+ useColor
5371
+ })
5372
+ );
5373
+ outputLines.push("");
5374
+ outputLines.push(renderTerminalEfficiencyTable(efficiencyData.rows));
5375
+ return outputLines.join("\n");
5376
+ }
5377
+ function renderEfficiencyReport(efficiencyData, format, options) {
5378
+ switch (format) {
5379
+ case "json":
5380
+ return JSON.stringify(efficiencyData.rows, null, 2);
5381
+ case "markdown":
5382
+ return renderMarkdownEfficiencyTable(efficiencyData.rows);
5383
+ case "terminal":
5384
+ return renderTerminalEfficiencyReport(efficiencyData, options);
5385
+ }
5386
+ }
5387
+
5388
+ // src/cli/run-efficiency-report.ts
5389
+ function validateOutputFormatOptions(options) {
5390
+ if (options.markdown && options.json) {
5391
+ throw new Error("Choose either --markdown or --json, not both");
5392
+ }
5393
+ }
5394
+ function resolveReportFormat(options) {
5395
+ if (options.json) {
5396
+ return "json";
5397
+ }
5398
+ if (options.markdown) {
5399
+ return "markdown";
5400
+ }
5401
+ return "terminal";
5402
+ }
5403
+ async function prepareEfficiencyReport(granularity, options) {
5404
+ validateOutputFormatOptions(options);
5405
+ const efficiencyData = await buildEfficiencyData(granularity, options);
5406
+ const format = resolveReportFormat(options);
5407
+ return {
5408
+ format,
5409
+ diagnostics: efficiencyData.diagnostics,
5410
+ output: renderEfficiencyReport(efficiencyData, format, {
5411
+ granularity
5412
+ })
5413
+ };
5414
+ }
5415
+ async function runEfficiencyReport(granularity, options) {
5416
+ const preparedReport = await prepareEfficiencyReport(granularity, options);
5417
+ emitDiagnostics(preparedReport.diagnostics.usage, logger);
5418
+ emitEnvVarOverrides(preparedReport.diagnostics.usage.activeEnvOverrides, logger);
5419
+ const mergeModeLabel = preparedReport.diagnostics.includeMergeCommits ? "including merge commits" : "excluding merge commits";
5420
+ logger.info(
5421
+ `Git outcomes (${mergeModeLabel}): ${preparedReport.diagnostics.gitCommitCount} commit(s), +${preparedReport.diagnostics.gitLinesAdded}/-${preparedReport.diagnostics.gitLinesDeleted} lines (${preparedReport.diagnostics.repoDir})`
5422
+ );
5423
+ logger.info(
5424
+ `Repo-attributed usage events: ${preparedReport.diagnostics.repoMatchedUsageEvents} matched, ${preparedReport.diagnostics.repoExcludedUsageEvents} excluded, ${preparedReport.diagnostics.repoUnattributedUsageEvents} unattributed`
5425
+ );
5426
+ if (preparedReport.diagnostics.scopeNote) {
5427
+ logger.warn(preparedReport.diagnostics.scopeNote);
5428
+ }
5429
+ if (preparedReport.format === "terminal") {
5430
+ warnIfTerminalTableOverflows(preparedReport.output, (message) => {
5431
+ logger.warn(message);
5432
+ });
5433
+ }
5434
+ console.log(preparedReport.output);
5435
+ }
5436
+
5437
+ // src/render/markdown-table.ts
5438
+ import { markdownTable as markdownTable2 } from "markdown-table";
5439
+ var alignment = ["l", "l", "l", "r", "r", "r", "r", "r", "r", "r"];
5440
+ function toMarkdownSafeCell2(value) {
5441
+ return value.replace(/\r?\n/gu, "<br>");
5442
+ }
5443
+ function renderMarkdownTable(rows, options = {}) {
5444
+ const tableLayout = options.tableLayout ?? "compact";
5445
+ const bodyRows = toUsageTableCells(rows, { layout: tableLayout }).map(
5446
+ (row) => row.map((cell) => toMarkdownSafeCell2(cell))
5447
+ );
5448
+ const tableRows = [Array.from(usageTableHeaders), ...bodyRows];
5449
+ return markdownTable2(tableRows, {
5450
+ align: alignment
5451
+ });
5452
+ }
5453
+
5454
+ // src/render/render-usage-report.ts
5455
+ function getReportTitle2(granularity) {
4238
5456
  switch (granularity) {
4239
5457
  case "daily":
4240
5458
  return "Daily Token Usage Report";
@@ -4246,16 +5464,11 @@ function getReportTitle(granularity) {
4246
5464
  }
4247
5465
  function renderTerminalUsageReport(usageData, options) {
4248
5466
  const outputLines = [];
4249
- const envVarOverrideLines = formatEnvVarOverrides(usageData.diagnostics.activeEnvOverrides);
4250
5467
  const useColor = options.useColor ?? shouldUseColorByDefault();
4251
5468
  const tableLayout = options.tableLayout ?? "compact";
4252
- if (envVarOverrideLines.length > 0) {
4253
- outputLines.push(...envVarOverrideLines);
4254
- outputLines.push("");
4255
- }
4256
5469
  outputLines.push(
4257
5470
  renderReportHeader({
4258
- title: getReportTitle(options.granularity),
5471
+ title: getReportTitle2(options.granularity),
4259
5472
  useColor
4260
5473
  })
4261
5474
  );
@@ -4276,12 +5489,12 @@ function renderUsageReport(usageData, format, options) {
4276
5489
  }
4277
5490
 
4278
5491
  // src/cli/run-usage-report.ts
4279
- function validateOutputFormatOptions(options) {
5492
+ function validateOutputFormatOptions2(options) {
4280
5493
  if (options.markdown && options.json) {
4281
5494
  throw new Error("Choose either --markdown or --json, not both");
4282
5495
  }
4283
5496
  }
4284
- function resolveReportFormat(options) {
5497
+ function resolveReportFormat2(options) {
4285
5498
  if (options.json) {
4286
5499
  return "json";
4287
5500
  }
@@ -4293,31 +5506,10 @@ function resolveReportFormat(options) {
4293
5506
  function resolveTableLayout(options) {
4294
5507
  return options.perModelColumns ? "per_model_columns" : "compact";
4295
5508
  }
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
5509
  async function prepareUsageReport(granularity, options) {
4318
- validateOutputFormatOptions(options);
5510
+ validateOutputFormatOptions2(options);
4319
5511
  const usageData = await buildUsageData(granularity, options);
4320
- const format = resolveReportFormat(options);
5512
+ const format = resolveReportFormat2(options);
4321
5513
  return {
4322
5514
  format,
4323
5515
  diagnostics: usageData.diagnostics,
@@ -4330,13 +5522,11 @@ async function prepareUsageReport(granularity, options) {
4330
5522
  async function runUsageReport(granularity, options) {
4331
5523
  const preparedReport = await prepareUsageReport(granularity, options);
4332
5524
  emitDiagnostics(preparedReport.diagnostics, logger);
5525
+ emitEnvVarOverrides(preparedReport.diagnostics.activeEnvOverrides, logger);
4333
5526
  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
- }
5527
+ warnIfTerminalTableOverflows(preparedReport.output, (message) => {
5528
+ logger.warn(message);
5529
+ });
4340
5530
  }
4341
5531
  console.log(preparedReport.output);
4342
5532
  }
@@ -4352,11 +5542,12 @@ function getSupportedSourceIds() {
4352
5542
  function getAllowedSourcesLabel(supportedSourceIds) {
4353
5543
  return supportedSourceIds.join(", ");
4354
5544
  }
4355
- function addSharedOptions(command) {
5545
+ function addSharedOptions(command, options = {}) {
4356
5546
  const supportedSourceIds = getSupportedSourceIds();
4357
5547
  const allowedSourcesLabel = getAllowedSourcesLabel(supportedSourceIds);
4358
5548
  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(
5549
+ const includePerModelColumns = options.includePerModelColumns ?? true;
5550
+ const configuredCommand = command.option("--pi-dir <path>", "Path to .pi sessions directory").option("--codex-dir <path>", "Path to .codex sessions directory").option("--opencode-db <path>", "Path to OpenCode SQLite DB").option(
4360
5551
  "--source-dir <source-id=path>",
4361
5552
  "Override source directory for directory-backed sources (repeatable)",
4362
5553
  collectRepeatedOption,
@@ -4371,7 +5562,14 @@ function addSharedOptions(command) {
4371
5562
  "Filter by model (repeatable/comma-separated; exact when exact match exists after source/provider/date filters, otherwise substring)",
4372
5563
  collectRepeatedOption,
4373
5564
  []
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(
5565
+ ).option("--pricing-url <url>", "Override LiteLLM pricing source URL").option("--pricing-offline", "Use cached LiteLLM pricing only (no network fetch)").option(
5566
+ "--ignore-pricing-failures",
5567
+ "Continue without estimated costs when pricing cannot be loaded"
5568
+ ).option("--markdown", "Render output as markdown table").option("--json", "Render output as JSON");
5569
+ if (!includePerModelColumns) {
5570
+ return configuredCommand;
5571
+ }
5572
+ return configuredCommand.option(
4375
5573
  "--per-model-columns",
4376
5574
  "Render per-model metrics as multiline aligned table columns (terminal/markdown)"
4377
5575
  );
@@ -4393,6 +5591,20 @@ function createCommand(granularity) {
4393
5591
  });
4394
5592
  return command;
4395
5593
  }
5594
+ function parseGranularityArgument(value) {
5595
+ const normalized = value.trim().toLowerCase();
5596
+ if (normalized === "daily" || normalized === "weekly" || normalized === "monthly") {
5597
+ return normalized;
5598
+ }
5599
+ throw new Error(`Invalid granularity: ${value}. Expected one of: daily, weekly, monthly`);
5600
+ }
5601
+ function createEfficiencyCommand() {
5602
+ const command = new Command("efficiency");
5603
+ 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) => {
5604
+ await runEfficiencyReport(granularity, options);
5605
+ });
5606
+ return command;
5607
+ }
4396
5608
  function rootDescription() {
4397
5609
  const supportedSourceIds = getSupportedSourceIds();
4398
5610
  const allowedSourcesLabel = getAllowedSourcesLabel(supportedSourceIds);
@@ -4410,12 +5622,13 @@ function rootDescription() {
4410
5622
  " $ llm-usage monthly --source opencode --opencode-db /path/to/opencode.db --json",
4411
5623
  " $ llm-usage monthly --model claude --per-model-columns",
4412
5624
  " $ llm-usage daily --source-dir pi=/tmp/pi-sessions",
5625
+ " $ llm-usage efficiency weekly --repo-dir /path/to/repo --json",
4413
5626
  " $ npx --yes llm-usage-metrics daily"
4414
5627
  ].join("\n");
4415
5628
  }
4416
5629
  function createCli(options = {}) {
4417
5630
  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"));
5631
+ 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
5632
  return program;
4420
5633
  }
4421
5634