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/README.md +95 -0
- package/dist/index.js +1576 -363
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
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
|
|
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
|
-
|
|
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]
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
1586
|
-
|
|
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
|
-
|
|
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) =>
|
|
1696
|
-
|
|
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) =>
|
|
1703
|
-
|
|
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
|
|
1755
|
-
|
|
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(
|
|
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/
|
|
1996
|
-
|
|
1997
|
-
{
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
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
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
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/
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
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
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
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
|
|
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
|
-
|
|
2065
|
-
|
|
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
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
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(
|
|
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
|
|
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
|
|
2254
|
-
var PARSE_FILE_CACHE_VERSION =
|
|
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 =
|
|
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((
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
2416
|
-
|
|
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(
|
|
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
|
|
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
|
|
2679
|
-
|
|
2680
|
-
|
|
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
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
4465
|
+
break;
|
|
3527
4466
|
case "cache":
|
|
3528
4467
|
diagnosticsLogger.info("Loaded pricing from cache");
|
|
3529
|
-
|
|
4468
|
+
break;
|
|
3530
4469
|
case "network":
|
|
3531
4470
|
diagnosticsLogger.info("Fetched pricing from LiteLLM");
|
|
3532
|
-
|
|
4471
|
+
break;
|
|
3533
4472
|
case "none":
|
|
3534
|
-
|
|
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 (
|
|
3589
|
-
|
|
4475
|
+
if (diagnostics.pricingWarning) {
|
|
4476
|
+
diagnosticsLogger.warn(diagnostics.pricingWarning);
|
|
3590
4477
|
}
|
|
3591
|
-
return modelLines.join("\n");
|
|
3592
4478
|
}
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
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
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
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
|
|
3608
|
-
(
|
|
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-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
5510
|
+
validateOutputFormatOptions2(options);
|
|
4319
5511
|
const usageData = await buildUsageData(granularity, options);
|
|
4320
|
-
const format =
|
|
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
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|