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