llm-usage-metrics 0.3.3 → 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 +14 -11
- package/dist/index.js +377 -76
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -748,7 +748,7 @@ function createUsageEvent(input) {
|
|
|
748
748
|
};
|
|
749
749
|
}
|
|
750
750
|
|
|
751
|
-
// src/utils/discover-
|
|
751
|
+
// src/utils/discover-files.ts
|
|
752
752
|
import { readdir } from "fs/promises";
|
|
753
753
|
import path3 from "path";
|
|
754
754
|
function getNodeErrorCode(error) {
|
|
@@ -759,41 +759,65 @@ function isSkippableDirectoryReadError(error) {
|
|
|
759
759
|
const code = getNodeErrorCode(error);
|
|
760
760
|
return code === "EACCES" || code === "EPERM";
|
|
761
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
|
+
}
|
|
762
777
|
async function walkDirectory(rootDir, acc, options) {
|
|
763
778
|
let entries;
|
|
764
779
|
try {
|
|
765
780
|
entries = await readdir(rootDir, { withFileTypes: true, encoding: "utf8" });
|
|
766
781
|
} catch (error) {
|
|
782
|
+
if (getNodeErrorCode(error) === "ENOENT") {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
767
785
|
if (options.allowPermissionSkip && isSkippableDirectoryReadError(error)) {
|
|
768
786
|
return;
|
|
769
787
|
}
|
|
770
788
|
throw error;
|
|
771
789
|
}
|
|
772
|
-
|
|
790
|
+
if (options.sort) {
|
|
791
|
+
entries.sort((left, right) => compareByCodePoint(left.name, right.name));
|
|
792
|
+
}
|
|
773
793
|
for (const entry of entries) {
|
|
774
794
|
const entryPath = path3.join(rootDir, entry.name);
|
|
775
|
-
if (entry.isDirectory()) {
|
|
776
|
-
await walkDirectory(entryPath, acc,
|
|
795
|
+
if (entry.isDirectory() && options.recursive) {
|
|
796
|
+
await walkDirectory(entryPath, acc, options);
|
|
777
797
|
continue;
|
|
778
798
|
}
|
|
779
|
-
if (entry.isFile() && entry.name.
|
|
799
|
+
if (entry.isFile() && matchesExtension(entry.name, options.extension)) {
|
|
780
800
|
acc.push(entryPath);
|
|
781
801
|
}
|
|
782
802
|
}
|
|
783
803
|
}
|
|
784
|
-
async function
|
|
804
|
+
async function discoverFiles(rootDir, options) {
|
|
785
805
|
const files = [];
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
}
|
|
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);
|
|
794
813
|
return files;
|
|
795
814
|
}
|
|
796
815
|
|
|
816
|
+
// src/utils/discover-jsonl-files.ts
|
|
817
|
+
async function discoverJsonlFiles(rootDir) {
|
|
818
|
+
return discoverFiles(rootDir, { extension: ".jsonl" });
|
|
819
|
+
}
|
|
820
|
+
|
|
797
821
|
// src/utils/fs-helpers.ts
|
|
798
822
|
import { access, constants, stat } from "fs/promises";
|
|
799
823
|
async function pathExists(filePath) {
|
|
@@ -883,6 +907,9 @@ function asTrimmedText(value) {
|
|
|
883
907
|
const normalized = value.trim();
|
|
884
908
|
return normalized || void 0;
|
|
885
909
|
}
|
|
910
|
+
function isBlankText(value) {
|
|
911
|
+
return value.trim().length === 0;
|
|
912
|
+
}
|
|
886
913
|
function toNumberLike(value) {
|
|
887
914
|
if (value === null || value === void 0 || typeof value === "number" || typeof value === "string") {
|
|
888
915
|
return value;
|
|
@@ -897,9 +924,6 @@ var SESSION_META_LINE_PATTERN = /"type"\s*:\s*"session_meta"/u;
|
|
|
897
924
|
var TURN_CONTEXT_LINE_PATTERN = /"type"\s*:\s*"turn_context"/u;
|
|
898
925
|
var EVENT_MSG_LINE_PATTERN = /"type"\s*:\s*"event_msg"/u;
|
|
899
926
|
var TOKEN_COUNT_LINE_PATTERN = /"type"\s*:\s*"token_count"/u;
|
|
900
|
-
function isBlankText(value) {
|
|
901
|
-
return value.trim().length === 0;
|
|
902
|
-
}
|
|
903
927
|
function shouldParseCodexJsonlLine(lineText) {
|
|
904
928
|
if (SESSION_META_LINE_PATTERN.test(lineText) || TURN_CONTEXT_LINE_PATTERN.test(lineText)) {
|
|
905
929
|
return true;
|
|
@@ -1070,45 +1094,294 @@ var CodexSourceAdapter = class {
|
|
|
1070
1094
|
}
|
|
1071
1095
|
};
|
|
1072
1096
|
|
|
1073
|
-
// src/sources/
|
|
1097
|
+
// src/sources/gemini/gemini-source-adapter.ts
|
|
1098
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1074
1099
|
import os3 from "os";
|
|
1075
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";
|
|
1076
1349
|
function deduplicate(paths) {
|
|
1077
1350
|
return [...new Set(paths)];
|
|
1078
1351
|
}
|
|
1079
1352
|
function getLinuxLikeCandidates(homeDir, env) {
|
|
1080
|
-
const xdgDataHome = env.XDG_DATA_HOME ??
|
|
1353
|
+
const xdgDataHome = env.XDG_DATA_HOME ?? path6.join(homeDir, ".local", "share");
|
|
1081
1354
|
return [
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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")
|
|
1086
1359
|
];
|
|
1087
1360
|
}
|
|
1088
1361
|
function getMacOsCandidates(homeDir) {
|
|
1089
|
-
const appSupportDir =
|
|
1362
|
+
const appSupportDir = path6.join(homeDir, "Library", "Application Support");
|
|
1090
1363
|
return [
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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")
|
|
1095
1368
|
];
|
|
1096
1369
|
}
|
|
1097
1370
|
function getWindowsCandidates(homeDir, env) {
|
|
1098
|
-
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);
|
|
1099
1372
|
const roamingCandidates = roamingBase ? [
|
|
1100
|
-
|
|
1101
|
-
|
|
1373
|
+
path6.join(roamingBase, "opencode", "opencode.db"),
|
|
1374
|
+
path6.join(roamingBase, "opencode", "db.sqlite")
|
|
1102
1375
|
] : [];
|
|
1103
1376
|
return [
|
|
1104
1377
|
...roamingCandidates,
|
|
1105
|
-
|
|
1106
|
-
|
|
1378
|
+
path6.join(homeDir, ".opencode", "opencode.db"),
|
|
1379
|
+
path6.join(homeDir, ".opencode", "db.sqlite")
|
|
1107
1380
|
];
|
|
1108
1381
|
}
|
|
1109
1382
|
function getDefaultOpenCodeDbPathCandidates(options = {}) {
|
|
1110
1383
|
const platform = options.platform ?? process.platform;
|
|
1111
|
-
const homeDir = options.homeDir ??
|
|
1384
|
+
const homeDir = options.homeDir ?? os4.homedir();
|
|
1112
1385
|
const env = options.env ?? process.env;
|
|
1113
1386
|
switch (platform) {
|
|
1114
1387
|
case "win32":
|
|
@@ -1236,7 +1509,7 @@ function normalizeSessionIdCandidate(value) {
|
|
|
1236
1509
|
}
|
|
1237
1510
|
return asTrimmedText(value);
|
|
1238
1511
|
}
|
|
1239
|
-
function
|
|
1512
|
+
function resolveRepoRoot2(messagePayload) {
|
|
1240
1513
|
const pathPayload = asRecord(messagePayload.path);
|
|
1241
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);
|
|
1242
1515
|
}
|
|
@@ -1291,7 +1564,7 @@ function parseOpenCodeMessageRows(rows, sourceId) {
|
|
|
1291
1564
|
}
|
|
1292
1565
|
const provider = asTrimmedText(payload.providerID) ?? asTrimmedText(payload.provider);
|
|
1293
1566
|
const model = asTrimmedText(payload.modelID) ?? asTrimmedText(payload.model);
|
|
1294
|
-
const repoRoot =
|
|
1567
|
+
const repoRoot = resolveRepoRoot2(payload);
|
|
1295
1568
|
const tokens = asRecord(payload.tokens);
|
|
1296
1569
|
const tokenCache = asRecord(tokens?.cache);
|
|
1297
1570
|
const inputTokens = toNumberLike(tokens?.input);
|
|
@@ -1615,9 +1888,9 @@ var OpenCodeSourceAdapter = class {
|
|
|
1615
1888
|
};
|
|
1616
1889
|
|
|
1617
1890
|
// src/sources/pi/pi-source-adapter.ts
|
|
1618
|
-
import
|
|
1619
|
-
import
|
|
1620
|
-
var defaultSessionsDir2 =
|
|
1891
|
+
import os5 from "os";
|
|
1892
|
+
import path7 from "path";
|
|
1893
|
+
var defaultSessionsDir2 = path7.join(os5.homedir(), ".pi", "agent", "sessions");
|
|
1621
1894
|
var PI_MESSAGE_LINE_PATTERN = /"type"\s*:\s*"message"/u;
|
|
1622
1895
|
var PI_SESSION_LINE_PATTERN = /"type"\s*:\s*"session"/u;
|
|
1623
1896
|
var PI_MODEL_CHANGE_LINE_PATTERN = /"type"\s*:\s*"model_change"/u;
|
|
@@ -1626,9 +1899,6 @@ function shouldParsePiJsonlLine(lineText) {
|
|
|
1626
1899
|
}
|
|
1627
1900
|
var allowAllProviders = () => true;
|
|
1628
1901
|
var UNIX_SECONDS_ABS_CUTOFF2 = 1e10;
|
|
1629
|
-
function isBlankText3(value) {
|
|
1630
|
-
return value.trim().length === 0;
|
|
1631
|
-
}
|
|
1632
1902
|
function normalizeTimestampCandidate2(candidate) {
|
|
1633
1903
|
let date;
|
|
1634
1904
|
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
@@ -1669,7 +1939,7 @@ function extractUsageFromRecord(usage) {
|
|
|
1669
1939
|
totalTokens: toNumberLike(usage.totalTokens),
|
|
1670
1940
|
costUsd: toNumberLike(cost?.total)
|
|
1671
1941
|
};
|
|
1672
|
-
const
|
|
1942
|
+
const toFiniteNumber2 = (value) => {
|
|
1673
1943
|
if (value === null || value === void 0) {
|
|
1674
1944
|
return void 0;
|
|
1675
1945
|
}
|
|
@@ -1691,10 +1961,10 @@ function extractUsageFromRecord(usage) {
|
|
|
1691
1961
|
extracted.totalTokens
|
|
1692
1962
|
];
|
|
1693
1963
|
const hasPositiveUsageSignal = usageCandidates.some((value) => {
|
|
1694
|
-
const parsed =
|
|
1964
|
+
const parsed = toFiniteNumber2(value);
|
|
1695
1965
|
return parsed !== void 0 && parsed > 0;
|
|
1696
1966
|
});
|
|
1697
|
-
const explicitCost =
|
|
1967
|
+
const explicitCost = toFiniteNumber2(extracted.costUsd);
|
|
1698
1968
|
const hasPositiveCostSignal = explicitCost !== void 0 && explicitCost > 0;
|
|
1699
1969
|
return hasPositiveUsageSignal || hasPositiveCostSignal ? extracted : void 0;
|
|
1700
1970
|
}
|
|
@@ -1713,7 +1983,7 @@ function extractUsage(line, message) {
|
|
|
1713
1983
|
return extractUsageFromRecord(messageUsage);
|
|
1714
1984
|
}
|
|
1715
1985
|
function getFallbackSessionId2(filePath) {
|
|
1716
|
-
return
|
|
1986
|
+
return path7.basename(filePath, ".jsonl");
|
|
1717
1987
|
}
|
|
1718
1988
|
function resolveRepoRootFromRecord(record) {
|
|
1719
1989
|
if (!record) {
|
|
@@ -1733,7 +2003,7 @@ var PiSourceAdapter = class {
|
|
|
1733
2003
|
this.requireSessionsDir = options.requireSessionsDir ?? false;
|
|
1734
2004
|
}
|
|
1735
2005
|
async discoverFiles() {
|
|
1736
|
-
if (
|
|
2006
|
+
if (isBlankText(this.sessionsDir)) {
|
|
1737
2007
|
throw new Error("PI sessions directory must be a non-empty path");
|
|
1738
2008
|
}
|
|
1739
2009
|
const normalizedSessionsDir = this.sessionsDir.trim();
|
|
@@ -1851,6 +2121,21 @@ var sourceRegistrations = [
|
|
|
1851
2121
|
});
|
|
1852
2122
|
}
|
|
1853
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
|
+
}
|
|
2138
|
+
},
|
|
1854
2139
|
{
|
|
1855
2140
|
id: "opencode",
|
|
1856
2141
|
sourceDirOverride: { kind: "unsupported", flag: "--opencode-db" },
|
|
@@ -1931,6 +2216,7 @@ function createDefaultAdapters(options) {
|
|
|
1931
2216
|
validateOpencodeOverride(options.opencodeDb);
|
|
1932
2217
|
validateDirectoryOverride("--pi-dir", options.piDir);
|
|
1933
2218
|
validateDirectoryOverride("--codex-dir", options.codexDir);
|
|
2219
|
+
validateDirectoryOverride("--gemini-dir", options.geminiDir);
|
|
1934
2220
|
const sourceDirectoryOverrides = parseSourceDirectoryOverrides(options.sourceDir);
|
|
1935
2221
|
validateSourceDirectoryOverrideIds(sourceDirectoryOverrides);
|
|
1936
2222
|
return sourceRegistrations.map((source) => source.create(options, sourceDirectoryOverrides));
|
|
@@ -2306,7 +2592,7 @@ function aggregateEfficiency(options) {
|
|
|
2306
2592
|
// src/efficiency/git-outcome-collector.ts
|
|
2307
2593
|
import { spawn as spawn2 } from "child_process";
|
|
2308
2594
|
import { createInterface as createInterface3 } from "readline";
|
|
2309
|
-
import
|
|
2595
|
+
import path8 from "path";
|
|
2310
2596
|
import { stat as stat2 } from "fs/promises";
|
|
2311
2597
|
var GIT_COMMIT_MARKER = "";
|
|
2312
2598
|
var SHORTSTAT_PATTERN = /(\d+)\s+files?\s+changed(?:,\s+(\d+)\s+insertions?\(\+\))?(?:,\s+(\d+)\s+deletions?\(-\))?/u;
|
|
@@ -2326,13 +2612,13 @@ function resolveGitCommandFailureReason(result) {
|
|
|
2326
2612
|
}
|
|
2327
2613
|
function resolveRepoDir(repoDir) {
|
|
2328
2614
|
if (repoDir === void 0) {
|
|
2329
|
-
return
|
|
2615
|
+
return path8.resolve(process.cwd());
|
|
2330
2616
|
}
|
|
2331
2617
|
const normalizedRepoDir = repoDir.trim();
|
|
2332
2618
|
if (!normalizedRepoDir) {
|
|
2333
2619
|
throw new Error("--repo-dir must be a non-empty path");
|
|
2334
2620
|
}
|
|
2335
|
-
return
|
|
2621
|
+
return path8.resolve(normalizedRepoDir);
|
|
2336
2622
|
}
|
|
2337
2623
|
function getNodeErrorCode2(error) {
|
|
2338
2624
|
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
@@ -2704,21 +2990,21 @@ async function collectGitOutcomes(options, deps = {}) {
|
|
|
2704
2990
|
|
|
2705
2991
|
// src/efficiency/repo-attribution.ts
|
|
2706
2992
|
import { access as access2, constants as constants2, realpath } from "fs/promises";
|
|
2707
|
-
import
|
|
2993
|
+
import path9 from "path";
|
|
2708
2994
|
async function hasGitMarker(directoryPath) {
|
|
2709
2995
|
try {
|
|
2710
|
-
await access2(
|
|
2996
|
+
await access2(path9.join(directoryPath, ".git"), constants2.F_OK);
|
|
2711
2997
|
return true;
|
|
2712
2998
|
} catch {
|
|
2713
2999
|
return false;
|
|
2714
3000
|
}
|
|
2715
3001
|
}
|
|
2716
3002
|
function normalizeComparablePath(value) {
|
|
2717
|
-
const normalizedPath =
|
|
3003
|
+
const normalizedPath = path9.normalize(path9.resolve(value));
|
|
2718
3004
|
return process.platform === "win32" ? normalizedPath.toLowerCase() : normalizedPath;
|
|
2719
3005
|
}
|
|
2720
3006
|
async function resolveComparablePath(value) {
|
|
2721
|
-
const resolvedPath =
|
|
3007
|
+
const resolvedPath = path9.resolve(value);
|
|
2722
3008
|
try {
|
|
2723
3009
|
return normalizeComparablePath(await realpath(resolvedPath));
|
|
2724
3010
|
} catch {
|
|
@@ -2730,20 +3016,20 @@ async function resolveRepoRootFromPathHint(pathHint) {
|
|
|
2730
3016
|
if (!trimmedPath) {
|
|
2731
3017
|
return void 0;
|
|
2732
3018
|
}
|
|
2733
|
-
let currentPath =
|
|
3019
|
+
let currentPath = path9.resolve(trimmedPath);
|
|
2734
3020
|
for (; ; ) {
|
|
2735
3021
|
if (await hasGitMarker(currentPath)) {
|
|
2736
3022
|
return currentPath;
|
|
2737
3023
|
}
|
|
2738
|
-
const parentPath =
|
|
3024
|
+
const parentPath = path9.dirname(currentPath);
|
|
2739
3025
|
if (parentPath === currentPath) {
|
|
2740
3026
|
return void 0;
|
|
2741
3027
|
}
|
|
2742
3028
|
currentPath = parentPath;
|
|
2743
3029
|
}
|
|
2744
3030
|
}
|
|
2745
|
-
async function attributeUsageEventsToRepo(events, repoDir,
|
|
2746
|
-
const resolvedTargetRepoRoot = await
|
|
3031
|
+
async function attributeUsageEventsToRepo(events, repoDir, resolveRepoRoot3 = resolveRepoRootFromPathHint) {
|
|
3032
|
+
const resolvedTargetRepoRoot = await resolveRepoRoot3(repoDir).catch(() => void 0);
|
|
2747
3033
|
const targetRepoPath = await resolveComparablePath(resolvedTargetRepoRoot ?? repoDir);
|
|
2748
3034
|
const rootCache = /* @__PURE__ */ new Map();
|
|
2749
3035
|
const matchedEvents = [];
|
|
@@ -2756,7 +3042,7 @@ async function attributeUsageEventsToRepo(events, repoDir, resolveRepoRoot2 = re
|
|
|
2756
3042
|
}
|
|
2757
3043
|
const eventRepoRoot = event.repoRoot;
|
|
2758
3044
|
const cachedRootPromise = rootCache.get(eventRepoRoot) ?? (async () => {
|
|
2759
|
-
const resolvedRoot2 = await
|
|
3045
|
+
const resolvedRoot2 = await resolveRepoRoot3(eventRepoRoot).catch(() => void 0);
|
|
2760
3046
|
if (!resolvedRoot2) {
|
|
2761
3047
|
return void 0;
|
|
2762
3048
|
}
|
|
@@ -3056,8 +3342,8 @@ function normalizeSkippedRowReasons(value) {
|
|
|
3056
3342
|
}
|
|
3057
3343
|
|
|
3058
3344
|
// src/cli/parse-file-cache.ts
|
|
3059
|
-
import { mkdir as mkdir2, readFile as
|
|
3060
|
-
import
|
|
3345
|
+
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
3346
|
+
import path10 from "path";
|
|
3061
3347
|
var PARSE_FILE_CACHE_VERSION = 2;
|
|
3062
3348
|
var CACHE_KEY_SEPARATOR = "\0";
|
|
3063
3349
|
function createCacheKey(source, filePath) {
|
|
@@ -3196,7 +3482,7 @@ function normalizeCacheEntry(value) {
|
|
|
3196
3482
|
};
|
|
3197
3483
|
}
|
|
3198
3484
|
function getDefaultParseFileCachePath() {
|
|
3199
|
-
return
|
|
3485
|
+
return path10.join(getUserCacheRootDir(), "llm-usage-metrics", "parse-file-cache.json");
|
|
3200
3486
|
}
|
|
3201
3487
|
var ParseFileCache = class _ParseFileCache {
|
|
3202
3488
|
constructor(cacheFilePath, limits, now) {
|
|
@@ -3282,7 +3568,7 @@ var ParseFileCache = class _ParseFileCache {
|
|
|
3282
3568
|
keptEntries.length = bestCount;
|
|
3283
3569
|
payloadText = bestPayloadText;
|
|
3284
3570
|
}
|
|
3285
|
-
await mkdir2(
|
|
3571
|
+
await mkdir2(path10.dirname(this.cacheFilePath), { recursive: true });
|
|
3286
3572
|
await writeFile2(this.cacheFilePath, payloadText, "utf8");
|
|
3287
3573
|
this.dirty = false;
|
|
3288
3574
|
}
|
|
@@ -3305,7 +3591,7 @@ var ParseFileCache = class _ParseFileCache {
|
|
|
3305
3591
|
async loadFromDisk() {
|
|
3306
3592
|
let content;
|
|
3307
3593
|
try {
|
|
3308
|
-
content = await
|
|
3594
|
+
content = await readFile3(this.cacheFilePath, "utf8");
|
|
3309
3595
|
} catch {
|
|
3310
3596
|
return;
|
|
3311
3597
|
}
|
|
@@ -3576,8 +3862,8 @@ function applyPricingToEvents(events, pricingSource) {
|
|
|
3576
3862
|
}
|
|
3577
3863
|
|
|
3578
3864
|
// src/pricing/litellm-pricing-fetcher.ts
|
|
3579
|
-
import { mkdir as mkdir3, readFile as
|
|
3580
|
-
import
|
|
3865
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
3866
|
+
import path11 from "path";
|
|
3581
3867
|
|
|
3582
3868
|
// src/pricing/litellm-model-map.json
|
|
3583
3869
|
var litellm_model_map_default = {
|
|
@@ -3585,20 +3871,34 @@ var litellm_model_map_default = {
|
|
|
3585
3871
|
k2p5: "kimi-k2.5",
|
|
3586
3872
|
"kimi-k2p5": "kimi-k2.5",
|
|
3587
3873
|
"kimi-k2.5": "kimi-k2.5",
|
|
3874
|
+
"kimi-k2.5-free": "kimi-k2.5",
|
|
3588
3875
|
"moonshotai.kimi-k2.5": "kimi-k2.5",
|
|
3589
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",
|
|
3590
3886
|
"claude sonnet 4.6": "claude-sonnet-4.6",
|
|
3591
3887
|
"claude-sonnet-4.6": "claude-sonnet-4.6",
|
|
3592
3888
|
"claude-sonnet-4-6": "claude-sonnet-4.6",
|
|
3593
3889
|
"anthropic/claude-sonnet-4.6": "claude-sonnet-4.6",
|
|
3594
|
-
"anthropic.claude-sonnet-4-6": "claude-sonnet-4.6"
|
|
3595
|
-
"gpt-5.3-codex": "gpt-5.2-codex"
|
|
3890
|
+
"anthropic.claude-sonnet-4-6": "claude-sonnet-4.6"
|
|
3596
3891
|
},
|
|
3597
3892
|
notes: {
|
|
3598
|
-
"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"
|
|
3599
3894
|
},
|
|
3600
3895
|
preferredPricingKeyByCanonicalModel: {
|
|
3601
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",
|
|
3602
3902
|
"claude-sonnet-4.6": "anthropic.claude-sonnet-4-6"
|
|
3603
3903
|
}
|
|
3604
3904
|
};
|
|
@@ -3747,7 +4047,7 @@ function normalizeLitellmPricingPayload(payload) {
|
|
|
3747
4047
|
return normalizedPricing;
|
|
3748
4048
|
}
|
|
3749
4049
|
function getDefaultLiteLLMPricingCachePath() {
|
|
3750
|
-
return
|
|
4050
|
+
return path11.join(getUserCacheRootDir(), "llm-usage-metrics", "litellm-pricing-cache.json");
|
|
3751
4051
|
}
|
|
3752
4052
|
function stripProviderPrefix(model) {
|
|
3753
4053
|
const slashIndex = model.lastIndexOf("/");
|
|
@@ -4106,7 +4406,7 @@ var LiteLLMPricingFetcher = class {
|
|
|
4106
4406
|
async readCachePayload() {
|
|
4107
4407
|
let content;
|
|
4108
4408
|
try {
|
|
4109
|
-
content = await
|
|
4409
|
+
content = await readFile4(this.cacheFilePath, "utf8");
|
|
4110
4410
|
} catch {
|
|
4111
4411
|
return void 0;
|
|
4112
4412
|
}
|
|
@@ -4141,7 +4441,7 @@ var LiteLLMPricingFetcher = class {
|
|
|
4141
4441
|
};
|
|
4142
4442
|
}
|
|
4143
4443
|
async writeCache() {
|
|
4144
|
-
const directoryPath =
|
|
4444
|
+
const directoryPath = path11.dirname(this.cacheFilePath);
|
|
4145
4445
|
await mkdir3(directoryPath, { recursive: true });
|
|
4146
4446
|
const payload = {
|
|
4147
4447
|
fetchedAt: this.now(),
|
|
@@ -4341,7 +4641,7 @@ function hasMeaningfulEfficiencyUsageSignal(event) {
|
|
|
4341
4641
|
async function buildEfficiencyData(granularity, options, deps = {}) {
|
|
4342
4642
|
const buildUsage = deps.buildUsageData ?? buildUsageData;
|
|
4343
4643
|
const collectOutcomes = deps.collectGitOutcomes ?? collectGitOutcomes;
|
|
4344
|
-
const
|
|
4644
|
+
const resolveRepoRoot3 = deps.resolveRepoRoot ?? resolveRepoRootFromPathHint;
|
|
4345
4645
|
const repoDir = options.repoDir?.trim();
|
|
4346
4646
|
if (options.repoDir !== void 0 && !repoDir) {
|
|
4347
4647
|
throw new Error("--repo-dir must be a non-empty path");
|
|
@@ -4350,7 +4650,7 @@ async function buildEfficiencyData(granularity, options, deps = {}) {
|
|
|
4350
4650
|
const attribution = await attributeUsageEventsToRepo(
|
|
4351
4651
|
usageData.events,
|
|
4352
4652
|
repoDir ?? process.cwd(),
|
|
4353
|
-
|
|
4653
|
+
resolveRepoRoot3
|
|
4354
4654
|
);
|
|
4355
4655
|
const matchedEventsWithSignal = attribution.matchedEvents.filter(
|
|
4356
4656
|
(event) => hasMeaningfulEfficiencyUsageSignal(event)
|
|
@@ -5547,7 +5847,7 @@ function addSharedOptions(command, options = {}) {
|
|
|
5547
5847
|
const allowedSourcesLabel = getAllowedSourcesLabel(supportedSourceIds);
|
|
5548
5848
|
const supportedSourcesSummary = `(${supportedSourceIds.length}): ${allowedSourcesLabel}`;
|
|
5549
5849
|
const includePerModelColumns = options.includePerModelColumns ?? true;
|
|
5550
|
-
const configuredCommand = command.option("--pi-dir <path>", "Path to .pi sessions directory").option("--codex-dir <path>", "Path to .codex sessions directory").option("--opencode-db <path>", "Path to OpenCode SQLite DB").option(
|
|
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(
|
|
5551
5851
|
"--source-dir <source-id=path>",
|
|
5552
5852
|
"Override source directory for directory-backed sources (repeatable)",
|
|
5553
5853
|
collectRepeatedOption,
|
|
@@ -5621,7 +5921,8 @@ function rootDescription() {
|
|
|
5621
5921
|
" $ llm-usage monthly --since 2026-01-01 --until 2026-01-31 --source pi,codex --json",
|
|
5622
5922
|
" $ llm-usage monthly --source opencode --opencode-db /path/to/opencode.db --json",
|
|
5623
5923
|
" $ llm-usage monthly --model claude --per-model-columns",
|
|
5624
|
-
" $ 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",
|
|
5625
5926
|
" $ llm-usage efficiency weekly --repo-dir /path/to/repo --json",
|
|
5626
5927
|
" $ npx --yes llm-usage-metrics daily"
|
|
5627
5928
|
].join("\n");
|