llm-usage-metrics 0.9.0 → 0.10.0
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 +16 -1
- package/dist/bin.js +7 -0
- package/dist/index.js +2277 -520
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -7,9 +7,9 @@ import { isMainThread, workerData } from "worker_threads";
|
|
|
7
7
|
import { availableParallelism } from "os";
|
|
8
8
|
|
|
9
9
|
// src/persistence/event-store-administration.ts
|
|
10
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
11
|
-
import { chmod, link,
|
|
12
|
-
import
|
|
10
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
11
|
+
import { chmod, link, open, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
12
|
+
import path4 from "path";
|
|
13
13
|
|
|
14
14
|
// src/utils/as-record.ts
|
|
15
15
|
function isRecord(value) {
|
|
@@ -47,33 +47,6 @@ function normalizeSkippedRowReasons(value) {
|
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
// src/utils/compare-by-code-point.ts
|
|
51
|
-
function compareByCodePoint(left, right4) {
|
|
52
|
-
if (left === right4) {
|
|
53
|
-
return 0;
|
|
54
|
-
}
|
|
55
|
-
const leftIterator = left[Symbol.iterator]();
|
|
56
|
-
const rightIterator = right4[Symbol.iterator]();
|
|
57
|
-
for (; ; ) {
|
|
58
|
-
const leftStep = leftIterator.next();
|
|
59
|
-
const rightStep = rightIterator.next();
|
|
60
|
-
if (leftStep.done && rightStep.done) {
|
|
61
|
-
return 0;
|
|
62
|
-
}
|
|
63
|
-
if (leftStep.done) {
|
|
64
|
-
return -1;
|
|
65
|
-
}
|
|
66
|
-
if (rightStep.done) {
|
|
67
|
-
return 1;
|
|
68
|
-
}
|
|
69
|
-
const leftCodePoint = leftStep.value.codePointAt(0) ?? 0;
|
|
70
|
-
const rightCodePoint = rightStep.value.codePointAt(0) ?? 0;
|
|
71
|
-
if (leftCodePoint !== rightCodePoint) {
|
|
72
|
-
return leftCodePoint < rightCodePoint ? -1 : 1;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
50
|
// src/domain/normalization.ts
|
|
78
51
|
var CONTROL_CHARACTER_CLASS = String.raw`[\u0000-\u001F\u007F-\u009F\u061C\u200E\u200F\u2028-\u202E\u2066-\u2069]`;
|
|
79
52
|
var CONTROL_CHARACTERS_PATTERN = new RegExp(CONTROL_CHARACTER_CLASS, "gu");
|
|
@@ -115,6 +88,33 @@ function normalizeTimestamp(value) {
|
|
|
115
88
|
return date.toISOString();
|
|
116
89
|
}
|
|
117
90
|
|
|
91
|
+
// src/utils/compare-by-code-point.ts
|
|
92
|
+
function compareByCodePoint(left, right4) {
|
|
93
|
+
if (left === right4) {
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
const leftIterator = left[Symbol.iterator]();
|
|
97
|
+
const rightIterator = right4[Symbol.iterator]();
|
|
98
|
+
for (; ; ) {
|
|
99
|
+
const leftStep = leftIterator.next();
|
|
100
|
+
const rightStep = rightIterator.next();
|
|
101
|
+
if (leftStep.done && rightStep.done) {
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
if (leftStep.done) {
|
|
105
|
+
return -1;
|
|
106
|
+
}
|
|
107
|
+
if (rightStep.done) {
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
const leftCodePoint = leftStep.value.codePointAt(0) ?? 0;
|
|
111
|
+
const rightCodePoint = rightStep.value.codePointAt(0) ?? 0;
|
|
112
|
+
if (leftCodePoint !== rightCodePoint) {
|
|
113
|
+
return leftCodePoint < rightCodePoint ? -1 : 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
118
|
// src/domain/provider-normalization.ts
|
|
119
119
|
var billingProviderAliases = /* @__PURE__ */ new Map([
|
|
120
120
|
["openai-codex", "openai"],
|
|
@@ -226,6 +226,7 @@ function intersectCanonicalProviderRoots(left, right4) {
|
|
|
226
226
|
|
|
227
227
|
// src/domain/usage-event.ts
|
|
228
228
|
var USAGE_EVENT_NORMALIZATION_VERSION = 2;
|
|
229
|
+
var LOCAL_MACHINE_NAME = "local";
|
|
229
230
|
function normalizeSourceId(value) {
|
|
230
231
|
if (typeof value !== "string") {
|
|
231
232
|
return void 0;
|
|
@@ -404,7 +405,8 @@ function getUserDataRootDir(env = process.env, platform = process.platform, home
|
|
|
404
405
|
|
|
405
406
|
// src/utils/fs-helpers.ts
|
|
406
407
|
import { randomUUID } from "crypto";
|
|
407
|
-
import { access, constants, rename, rm, stat, writeFile } from "fs/promises";
|
|
408
|
+
import { access, constants, mkdir, readFile, rename, rm, stat, writeFile } from "fs/promises";
|
|
409
|
+
import path3 from "path";
|
|
408
410
|
async function pathExists(filePath) {
|
|
409
411
|
try {
|
|
410
412
|
await access(filePath, constants.F_OK);
|
|
@@ -442,6 +444,36 @@ async function pathStat(filePath) {
|
|
|
442
444
|
return void 0;
|
|
443
445
|
}
|
|
444
446
|
}
|
|
447
|
+
async function ensureDirectory(directoryPath, mode) {
|
|
448
|
+
const missing = [];
|
|
449
|
+
let current = directoryPath;
|
|
450
|
+
while (!await pathIsDirectory(current)) {
|
|
451
|
+
const parent = path3.dirname(current);
|
|
452
|
+
if (parent === current) {
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
missing.push(current);
|
|
456
|
+
current = parent;
|
|
457
|
+
}
|
|
458
|
+
for (const directory of missing.reverse()) {
|
|
459
|
+
try {
|
|
460
|
+
await mkdir(directory, { mode });
|
|
461
|
+
} catch (error) {
|
|
462
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) {
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
if (!await pathIsDirectory(directory)) {
|
|
466
|
+
throw error;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async function readRegularTextFile(filePath) {
|
|
472
|
+
if (!(await stat(filePath)).isFile()) {
|
|
473
|
+
throw new Error(`${filePath} is not a regular file`);
|
|
474
|
+
}
|
|
475
|
+
return readFile(filePath, "utf8");
|
|
476
|
+
}
|
|
445
477
|
async function writeFileAtomic(filePath, content) {
|
|
446
478
|
const tempPath = `${filePath}.${randomUUID()}.tmp`;
|
|
447
479
|
try {
|
|
@@ -480,8 +512,13 @@ function toText(value) {
|
|
|
480
512
|
const normalized = value.trim();
|
|
481
513
|
return normalized || void 0;
|
|
482
514
|
}
|
|
515
|
+
var databasesInTransaction = /* @__PURE__ */ new WeakSet();
|
|
483
516
|
function runTransaction(database, task) {
|
|
517
|
+
if (databasesInTransaction.has(database)) {
|
|
518
|
+
return task();
|
|
519
|
+
}
|
|
484
520
|
database.exec("BEGIN IMMEDIATE");
|
|
521
|
+
databasesInTransaction.add(database);
|
|
485
522
|
try {
|
|
486
523
|
const result = task();
|
|
487
524
|
database.exec("COMMIT");
|
|
@@ -489,6 +526,8 @@ function runTransaction(database, task) {
|
|
|
489
526
|
} catch (error) {
|
|
490
527
|
database.exec("ROLLBACK");
|
|
491
528
|
throw error;
|
|
529
|
+
} finally {
|
|
530
|
+
databasesInTransaction.delete(database);
|
|
492
531
|
}
|
|
493
532
|
}
|
|
494
533
|
|
|
@@ -875,6 +914,11 @@ function initializeSchema(database) {
|
|
|
875
914
|
throw new EventStoreSchemaVersionError(schemaVersion);
|
|
876
915
|
}
|
|
877
916
|
|
|
917
|
+
// src/utils/error-code.ts
|
|
918
|
+
function hasErrorCode(error, ...codes) {
|
|
919
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && codes.includes(error.code);
|
|
920
|
+
}
|
|
921
|
+
|
|
878
922
|
// src/persistence/event-store-administration.ts
|
|
879
923
|
var EVENT_STORE_OPEN_TIMEOUT_MS = 2e3;
|
|
880
924
|
var loadEventStoreSqliteModule = () => loadNodeSqliteModule("Event store");
|
|
@@ -965,16 +1009,13 @@ function stringifySkippedRowReasons(skippedRowReasons) {
|
|
|
965
1009
|
return normalizedReasons.length > 0 ? JSON.stringify(normalizedReasons) : null;
|
|
966
1010
|
}
|
|
967
1011
|
function getDefaultEventStorePath() {
|
|
968
|
-
return
|
|
1012
|
+
return path4.join(getUserDataRootDir(), "llm-usage-metrics", "events.db");
|
|
969
1013
|
}
|
|
970
1014
|
function getLegacyEventStorePath() {
|
|
971
|
-
return
|
|
972
|
-
}
|
|
973
|
-
function isErrorCode(error, code) {
|
|
974
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
1015
|
+
return path4.join(getUserCacheRootDir(), "llm-usage-metrics", "events.db");
|
|
975
1016
|
}
|
|
976
1017
|
function getLegacyCopyMarkerPath(targetPath) {
|
|
977
|
-
return
|
|
1018
|
+
return path4.join(path4.dirname(targetPath), "legacy-ledger-copied");
|
|
978
1019
|
}
|
|
979
1020
|
async function findLegacyEventStore(targetPath) {
|
|
980
1021
|
const legacyPath = getLegacyEventStorePath();
|
|
@@ -1003,7 +1044,7 @@ async function copyLegacyEventStore(targetPath, sqliteModule) {
|
|
|
1003
1044
|
try {
|
|
1004
1045
|
await link(snapshotPath, targetPath);
|
|
1005
1046
|
} catch (error) {
|
|
1006
|
-
if (!
|
|
1047
|
+
if (!hasErrorCode(error, "EEXIST")) {
|
|
1007
1048
|
throw error;
|
|
1008
1049
|
}
|
|
1009
1050
|
}
|
|
@@ -1015,6 +1056,10 @@ async function copyLegacyEventStore(targetPath, sqliteModule) {
|
|
|
1015
1056
|
`, { mode: 384 });
|
|
1016
1057
|
}
|
|
1017
1058
|
async function prepareEventStoreFile(filePath) {
|
|
1059
|
+
const existing = await pathStat(filePath);
|
|
1060
|
+
if (existing && !existing.isFile()) {
|
|
1061
|
+
throw new Error(`Event store path ${filePath} is not a regular file`);
|
|
1062
|
+
}
|
|
1018
1063
|
const fileHandle = await open(filePath, "a", 384);
|
|
1019
1064
|
try {
|
|
1020
1065
|
await chmod(filePath, 384);
|
|
@@ -1028,7 +1073,7 @@ async function restrictEventStoreFiles(filePath) {
|
|
|
1028
1073
|
try {
|
|
1029
1074
|
await chmod(sidecarPath, 384);
|
|
1030
1075
|
} catch (error) {
|
|
1031
|
-
if (
|
|
1076
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
1032
1077
|
continue;
|
|
1033
1078
|
}
|
|
1034
1079
|
throw error;
|
|
@@ -1036,8 +1081,8 @@ async function restrictEventStoreFiles(filePath) {
|
|
|
1036
1081
|
}
|
|
1037
1082
|
}
|
|
1038
1083
|
async function openEventStore(filePath = getDefaultEventStorePath(), loadSqliteModule = loadEventStoreSqliteModule) {
|
|
1039
|
-
const parentDirectory =
|
|
1040
|
-
await
|
|
1084
|
+
const parentDirectory = path4.dirname(filePath);
|
|
1085
|
+
await ensureDirectory(parentDirectory, 448);
|
|
1041
1086
|
if (filePath === getDefaultEventStorePath()) {
|
|
1042
1087
|
await chmod(parentDirectory, 448);
|
|
1043
1088
|
}
|
|
@@ -1115,6 +1160,99 @@ async function readEventStoreStoredFiles(filePath = getDefaultEventStorePath(),
|
|
|
1115
1160
|
database.close();
|
|
1116
1161
|
}
|
|
1117
1162
|
}
|
|
1163
|
+
function listStoredFileFingerprints(store) {
|
|
1164
|
+
const rows = store.database.prepare("SELECT source, file_path, fingerprint FROM files ORDER BY source ASC, file_path ASC").all();
|
|
1165
|
+
const files = [];
|
|
1166
|
+
for (const row of rows) {
|
|
1167
|
+
const source = toText(row.source);
|
|
1168
|
+
const filePath = toText(row.file_path);
|
|
1169
|
+
const fingerprint = toText(row.fingerprint);
|
|
1170
|
+
if (source && filePath && fingerprint) {
|
|
1171
|
+
files.push({ source, filePath, fingerprint });
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
return files;
|
|
1175
|
+
}
|
|
1176
|
+
function readEventStoreMeta(store, key) {
|
|
1177
|
+
return toText(store.database.prepare("SELECT value FROM meta WHERE key = ?").get(key)?.value);
|
|
1178
|
+
}
|
|
1179
|
+
function writeEventStoreMeta(store, entries) {
|
|
1180
|
+
runTransaction(store.database, () => {
|
|
1181
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
1182
|
+
if (value === void 0) {
|
|
1183
|
+
store.database.prepare("DELETE FROM meta WHERE key = ?").run(key);
|
|
1184
|
+
} else {
|
|
1185
|
+
store.database.prepare(
|
|
1186
|
+
"INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
|
|
1187
|
+
).run(key, value);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
async function readEventStoreMetaValues(filePath, loadSqliteModule = loadEventStoreSqliteModule) {
|
|
1193
|
+
const sqliteModule = await loadSqliteModule();
|
|
1194
|
+
if (!isEventStoreSqliteModule(sqliteModule)) {
|
|
1195
|
+
throw new Error("Event store requires a sqlite module with a DatabaseSync constructor");
|
|
1196
|
+
}
|
|
1197
|
+
const database = new sqliteModule.DatabaseSync(filePath, {
|
|
1198
|
+
readOnly: true,
|
|
1199
|
+
timeout: EVENT_STORE_OPEN_TIMEOUT_MS
|
|
1200
|
+
});
|
|
1201
|
+
try {
|
|
1202
|
+
return readMetaValues(database);
|
|
1203
|
+
} finally {
|
|
1204
|
+
database.close();
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
function readMetaValues(database) {
|
|
1208
|
+
const meta = /* @__PURE__ */ new Map();
|
|
1209
|
+
for (const row of database.prepare("SELECT key, value FROM meta").all()) {
|
|
1210
|
+
const key = toText(row.key);
|
|
1211
|
+
const value = toText(row.value);
|
|
1212
|
+
if (key && value) {
|
|
1213
|
+
meta.set(key, value);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return meta;
|
|
1217
|
+
}
|
|
1218
|
+
async function readEventStoreEvents(filePath, window = {}, loadSqliteModule = loadEventStoreSqliteModule) {
|
|
1219
|
+
const sqliteModule = await loadSqliteModule();
|
|
1220
|
+
if (!isEventStoreSqliteModule(sqliteModule)) {
|
|
1221
|
+
throw new Error("Event store requires a sqlite module with a DatabaseSync constructor");
|
|
1222
|
+
}
|
|
1223
|
+
const database = new sqliteModule.DatabaseSync(filePath, {
|
|
1224
|
+
readOnly: true,
|
|
1225
|
+
timeout: EVENT_STORE_OPEN_TIMEOUT_MS
|
|
1226
|
+
});
|
|
1227
|
+
try {
|
|
1228
|
+
assertSupportedSchemaVersion(database);
|
|
1229
|
+
const statement = database.prepare(
|
|
1230
|
+
[
|
|
1231
|
+
"SELECT source, session_id, timestamp, model, provider, repo_root,",
|
|
1232
|
+
" input_tokens, output_tokens, reasoning_tokens, cache_read_tokens,",
|
|
1233
|
+
" cache_write_tokens, total_tokens, cost_usd, cost_mode",
|
|
1234
|
+
"FROM events",
|
|
1235
|
+
"WHERE timestamp >= ? AND timestamp < ?",
|
|
1236
|
+
"ORDER BY source, file_path, event_index"
|
|
1237
|
+
].join("\n")
|
|
1238
|
+
);
|
|
1239
|
+
statement.setReturnArrays(true);
|
|
1240
|
+
const rows = statement.all(
|
|
1241
|
+
window.fromTimestamp ?? "",
|
|
1242
|
+
window.toTimestamp ?? "\uFFFF"
|
|
1243
|
+
);
|
|
1244
|
+
const events = [];
|
|
1245
|
+
for (const row of rows) {
|
|
1246
|
+
const event = normalizeStoredEventTuple(row);
|
|
1247
|
+
if (event) {
|
|
1248
|
+
events.push(event);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return { meta: readMetaValues(database), events };
|
|
1252
|
+
} finally {
|
|
1253
|
+
database.close();
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1118
1256
|
function deleteFileEntry(store, source, filePath) {
|
|
1119
1257
|
deleteStoredFiles(store, [{ source, filePath }]);
|
|
1120
1258
|
}
|
|
@@ -1176,6 +1314,39 @@ function readFileEvents(store, source, filePath) {
|
|
|
1176
1314
|
}
|
|
1177
1315
|
return events;
|
|
1178
1316
|
}
|
|
1317
|
+
function readDepartedFileEvents(store, source, filePath) {
|
|
1318
|
+
const normalizedSource = normalizeStoreSource(source);
|
|
1319
|
+
const normalizedFilePath = normalizeStoreFilePath(filePath);
|
|
1320
|
+
const events = [];
|
|
1321
|
+
for (const row of selectFileEventRows(store, normalizedSource, normalizedFilePath)) {
|
|
1322
|
+
const event = normalizeStoredEventTuple(row);
|
|
1323
|
+
if (!event) {
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
events.push(event);
|
|
1327
|
+
}
|
|
1328
|
+
return events;
|
|
1329
|
+
}
|
|
1330
|
+
function readStoredFileSnapshots(store, files) {
|
|
1331
|
+
const snapshots = [];
|
|
1332
|
+
store.database.exec("BEGIN");
|
|
1333
|
+
try {
|
|
1334
|
+
for (const file of files) {
|
|
1335
|
+
const source = normalizeStoreSource(file.source);
|
|
1336
|
+
const filePath = normalizeStoreFilePath(file.filePath);
|
|
1337
|
+
const events = readDepartedFileEvents(store, source, filePath);
|
|
1338
|
+
snapshots.push({
|
|
1339
|
+
source,
|
|
1340
|
+
filePath,
|
|
1341
|
+
revision: createHash2("sha256").update(JSON.stringify(events)).digest("hex").slice(0, 16),
|
|
1342
|
+
events
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
} finally {
|
|
1346
|
+
store.database.exec("COMMIT");
|
|
1347
|
+
}
|
|
1348
|
+
return snapshots;
|
|
1349
|
+
}
|
|
1179
1350
|
function countMatchingRows(store, tableName, source, filePath) {
|
|
1180
1351
|
const row = store.database.prepare(`SELECT COUNT(*) AS count FROM ${tableName} WHERE source = ? AND file_path = ?`).get(source, filePath);
|
|
1181
1352
|
return toNonNegativeInteger(row?.count) ?? 0;
|
|
@@ -1419,9 +1590,8 @@ function getEventStoreRuntimeConfig(env = process.env, config = {}) {
|
|
|
1419
1590
|
}
|
|
1420
1591
|
|
|
1421
1592
|
// src/config/user-config.ts
|
|
1422
|
-
import { readFile as readFileFromFs } from "fs/promises";
|
|
1423
1593
|
import os4 from "os";
|
|
1424
|
-
import
|
|
1594
|
+
import path6 from "path";
|
|
1425
1595
|
|
|
1426
1596
|
// node_modules/.pnpm/smol-toml@1.9.0/node_modules/smol-toml/dist/error.js
|
|
1427
1597
|
function getLineColFromPtr(string, ptr) {
|
|
@@ -2182,10 +2352,10 @@ var HAS_WELLFORMED = !!"".isWellFormed;
|
|
|
2182
2352
|
|
|
2183
2353
|
// src/utils/config-root-dir.ts
|
|
2184
2354
|
import os3 from "os";
|
|
2185
|
-
import
|
|
2355
|
+
import path5 from "path";
|
|
2186
2356
|
function getUserConfigRootDir(env = process.env, platform = process.platform, homedir = os3.homedir()) {
|
|
2187
2357
|
const xdgConfigDir = env.XDG_CONFIG_HOME;
|
|
2188
|
-
if (xdgConfigDir &&
|
|
2358
|
+
if (xdgConfigDir && path5.isAbsolute(xdgConfigDir)) {
|
|
2189
2359
|
return xdgConfigDir;
|
|
2190
2360
|
}
|
|
2191
2361
|
if (platform === "win32") {
|
|
@@ -2194,7 +2364,7 @@ function getUserConfigRootDir(env = process.env, platform = process.platform, ho
|
|
|
2194
2364
|
return appData;
|
|
2195
2365
|
}
|
|
2196
2366
|
}
|
|
2197
|
-
return
|
|
2367
|
+
return path5.join(homedir, ".config");
|
|
2198
2368
|
}
|
|
2199
2369
|
|
|
2200
2370
|
// src/config/user-config.ts
|
|
@@ -2236,6 +2406,7 @@ var USER_CONFIG_SOURCE_DIR_KEYS = [
|
|
|
2236
2406
|
var knownTopLevelKeys = [
|
|
2237
2407
|
"eventStore",
|
|
2238
2408
|
"logLevel",
|
|
2409
|
+
"machines",
|
|
2239
2410
|
"monthlyBudgetUsd",
|
|
2240
2411
|
"parseMaxParallel",
|
|
2241
2412
|
"parseWorkerMinBytes",
|
|
@@ -2256,20 +2427,30 @@ var knownPricingKeys = [
|
|
|
2256
2427
|
];
|
|
2257
2428
|
var knownEventStoreKeys = ["enabled", "path"];
|
|
2258
2429
|
var knownUpdateKeys = ["cacheTtlMs", "fetchTimeoutMs", "skipCheck"];
|
|
2430
|
+
var knownMachineKeys = ["command", "enabled", "ssh"];
|
|
2259
2431
|
var USER_CONFIG_KNOWN_KEY_PATHS = [
|
|
2260
2432
|
...knownTopLevelKeys,
|
|
2261
2433
|
...knownPricingKeys.map((key) => `pricing.${key}`),
|
|
2262
2434
|
...knownEventStoreKeys.map((key) => `eventStore.${key}`),
|
|
2263
2435
|
...knownUpdateKeys.map((key) => `update.${key}`),
|
|
2436
|
+
...knownMachineKeys.map((key) => `machines.<name>.${key}`),
|
|
2264
2437
|
...USER_CONFIG_SOURCE_DIR_KEYS.map((key) => `sourceDirs.${key}`)
|
|
2265
2438
|
];
|
|
2266
2439
|
var knownTopLevelKeySet = new Set(knownTopLevelKeys);
|
|
2267
2440
|
var knownPricingKeySet = new Set(knownPricingKeys);
|
|
2268
2441
|
var knownEventStoreKeySet = new Set(knownEventStoreKeys);
|
|
2269
2442
|
var knownUpdateKeySet = new Set(knownUpdateKeys);
|
|
2443
|
+
var knownMachineKeySet = new Set(knownMachineKeys);
|
|
2444
|
+
var MACHINE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,31}$/u;
|
|
2445
|
+
function isValidMachineName(name) {
|
|
2446
|
+
return MACHINE_NAME_PATTERN.test(name) && name !== LOCAL_MACHINE_NAME;
|
|
2447
|
+
}
|
|
2448
|
+
function isValidSshTarget(target) {
|
|
2449
|
+
return target.length > 0 && !target.startsWith("-") && !/[\s\u0000-\u001F\u007F]/u.test(target);
|
|
2450
|
+
}
|
|
2270
2451
|
var sourceDirKeySet = new Set(USER_CONFIG_SOURCE_DIR_KEYS);
|
|
2271
2452
|
function getDefaultUserConfigPath(env) {
|
|
2272
|
-
return
|
|
2453
|
+
return path6.join(
|
|
2273
2454
|
getUserConfigRootDir(env, process.platform, os4.homedir()),
|
|
2274
2455
|
"llm-usage-metrics",
|
|
2275
2456
|
"config.toml"
|
|
@@ -2282,21 +2463,18 @@ function resolveUserConfigPath(env = process.env) {
|
|
|
2282
2463
|
}
|
|
2283
2464
|
return getDefaultUserConfigPath(env);
|
|
2284
2465
|
}
|
|
2285
|
-
function isMissingFileError(error) {
|
|
2286
|
-
return asRecord(error)?.code === "ENOENT";
|
|
2287
|
-
}
|
|
2288
2466
|
function hasConfigPathOverride(env) {
|
|
2289
2467
|
return Boolean(env.LLM_USAGE_CONFIG_PATH?.trim());
|
|
2290
2468
|
}
|
|
2291
2469
|
function getLegacyJsonConfigPath(configPath) {
|
|
2292
|
-
return
|
|
2470
|
+
return path6.join(path6.dirname(configPath), "config.json");
|
|
2293
2471
|
}
|
|
2294
2472
|
async function throwIfLegacyJsonConfigExists(tomlPath, readFile9) {
|
|
2295
2473
|
const jsonPath = getLegacyJsonConfigPath(tomlPath);
|
|
2296
2474
|
try {
|
|
2297
2475
|
await readFile9(jsonPath);
|
|
2298
2476
|
} catch (error) {
|
|
2299
|
-
if (
|
|
2477
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
2300
2478
|
return;
|
|
2301
2479
|
}
|
|
2302
2480
|
throw error;
|
|
@@ -2370,14 +2548,14 @@ function resolveConfigPath(value, configDir) {
|
|
|
2370
2548
|
if (value === "~") {
|
|
2371
2549
|
return os4.homedir();
|
|
2372
2550
|
}
|
|
2373
|
-
if (value.startsWith("~/") || value.startsWith(`~${
|
|
2374
|
-
return
|
|
2551
|
+
if (value.startsWith("~/") || value.startsWith(`~${path6.sep}`)) {
|
|
2552
|
+
return path6.join(os4.homedir(), value.slice(2));
|
|
2375
2553
|
}
|
|
2376
|
-
return
|
|
2554
|
+
return path6.resolve(configDir, value);
|
|
2377
2555
|
}
|
|
2378
2556
|
function readPath(context, keyPath, value) {
|
|
2379
|
-
const
|
|
2380
|
-
return
|
|
2557
|
+
const text2 = readKey(context, keyPath, value, toNonBlankString, "a non-empty path");
|
|
2558
|
+
return text2 === void 0 ? void 0 : resolveConfigPath(text2, context.configDir);
|
|
2381
2559
|
}
|
|
2382
2560
|
function readLogLevel(value) {
|
|
2383
2561
|
if (value === "silent" || value === "warn" || value === "info" || value === "debug") {
|
|
@@ -2542,6 +2720,65 @@ function readUpdateConfig(context, value) {
|
|
|
2542
2720
|
}
|
|
2543
2721
|
return Object.keys(update).length === 0 ? void 0 : update;
|
|
2544
2722
|
}
|
|
2723
|
+
function readMachinesConfig(context, value) {
|
|
2724
|
+
const record = readTable(context, "machines", value);
|
|
2725
|
+
if (!record) {
|
|
2726
|
+
return void 0;
|
|
2727
|
+
}
|
|
2728
|
+
const machines = {};
|
|
2729
|
+
for (const [name, machineValue] of Object.entries(record)) {
|
|
2730
|
+
const keyPath = `machines.${name}`;
|
|
2731
|
+
if (!isValidMachineName(name)) {
|
|
2732
|
+
context.warnings.push(
|
|
2733
|
+
`Ignoring ${keyPath}: a machine name is 1-32 lowercase letters, digits or dashes, and not "${LOCAL_MACHINE_NAME}"`
|
|
2734
|
+
);
|
|
2735
|
+
continue;
|
|
2736
|
+
}
|
|
2737
|
+
const machineRecord = readTable(context, keyPath, machineValue);
|
|
2738
|
+
if (!machineRecord) {
|
|
2739
|
+
continue;
|
|
2740
|
+
}
|
|
2741
|
+
const ssh = readKey(
|
|
2742
|
+
context,
|
|
2743
|
+
`${keyPath}.ssh`,
|
|
2744
|
+
machineRecord.ssh,
|
|
2745
|
+
(target) => {
|
|
2746
|
+
const text2 = toNonBlankString(target);
|
|
2747
|
+
return text2 !== void 0 && isValidSshTarget(text2) ? text2 : void 0;
|
|
2748
|
+
},
|
|
2749
|
+
'an ssh destination such as "user@host" (no spaces, not starting with -)'
|
|
2750
|
+
);
|
|
2751
|
+
if (ssh === void 0) {
|
|
2752
|
+
if (isUnset(machineRecord.ssh)) {
|
|
2753
|
+
context.warnings.push(`Ignoring ${keyPath}: set ssh = "user@host"`);
|
|
2754
|
+
}
|
|
2755
|
+
continue;
|
|
2756
|
+
}
|
|
2757
|
+
const machine = { ssh };
|
|
2758
|
+
const command = readKey(
|
|
2759
|
+
context,
|
|
2760
|
+
`${keyPath}.command`,
|
|
2761
|
+
machineRecord.command,
|
|
2762
|
+
toNonBlankString,
|
|
2763
|
+
"a non-empty command"
|
|
2764
|
+
);
|
|
2765
|
+
const enabled = readKey(
|
|
2766
|
+
context,
|
|
2767
|
+
`${keyPath}.enabled`,
|
|
2768
|
+
machineRecord.enabled,
|
|
2769
|
+
toBoolean,
|
|
2770
|
+
"true or false"
|
|
2771
|
+
);
|
|
2772
|
+
if (command !== void 0) {
|
|
2773
|
+
machine.command = command;
|
|
2774
|
+
}
|
|
2775
|
+
if (enabled !== void 0) {
|
|
2776
|
+
machine.enabled = enabled;
|
|
2777
|
+
}
|
|
2778
|
+
machines[name] = machine;
|
|
2779
|
+
}
|
|
2780
|
+
return Object.keys(machines).length === 0 ? void 0 : machines;
|
|
2781
|
+
}
|
|
2545
2782
|
function readParseWorkers(value) {
|
|
2546
2783
|
if (value === "auto") {
|
|
2547
2784
|
return "auto";
|
|
@@ -2597,6 +2834,7 @@ function readConfig(context, root) {
|
|
|
2597
2834
|
PARSE_WORKER_MIN_BYTES_MAX
|
|
2598
2835
|
);
|
|
2599
2836
|
const update = readUpdateConfig(context, root.update);
|
|
2837
|
+
const machines = readMachinesConfig(context, root.machines);
|
|
2600
2838
|
if (timezone !== void 0) {
|
|
2601
2839
|
config.timezone = timezone;
|
|
2602
2840
|
}
|
|
@@ -2630,6 +2868,9 @@ function readConfig(context, root) {
|
|
|
2630
2868
|
if (update !== void 0) {
|
|
2631
2869
|
config.update = update;
|
|
2632
2870
|
}
|
|
2871
|
+
if (machines !== void 0) {
|
|
2872
|
+
config.machines = machines;
|
|
2873
|
+
}
|
|
2633
2874
|
return config;
|
|
2634
2875
|
}
|
|
2635
2876
|
function collectUnknownKeyWarnings(root) {
|
|
@@ -2638,6 +2879,14 @@ function collectUnknownKeyWarnings(root) {
|
|
|
2638
2879
|
pushUnknownNestedKeys(unknownKeys, root, "eventStore", knownEventStoreKeySet);
|
|
2639
2880
|
pushUnknownNestedKeys(unknownKeys, root, "update", knownUpdateKeySet);
|
|
2640
2881
|
pushUnknownNestedKeys(unknownKeys, root, "sourceDirs", sourceDirKeySet);
|
|
2882
|
+
for (const [name, machine] of Object.entries(asRecord(root.machines) ?? {})) {
|
|
2883
|
+
const machineRecord = asRecord(machine);
|
|
2884
|
+
if (machineRecord) {
|
|
2885
|
+
unknownKeys.push(
|
|
2886
|
+
...collectUnknownKeys(machineRecord, knownMachineKeySet, `machines.${name}.`)
|
|
2887
|
+
);
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2641
2890
|
const unknownKeyWarning = formatUnknownKeyWarning(unknownKeys);
|
|
2642
2891
|
return unknownKeyWarning === void 0 ? [] : [unknownKeyWarning];
|
|
2643
2892
|
}
|
|
@@ -2655,13 +2904,13 @@ function parseUserConfigRoot(filePath, content) {
|
|
|
2655
2904
|
}
|
|
2656
2905
|
return root;
|
|
2657
2906
|
}
|
|
2658
|
-
async function loadUserConfig(env = process.env, readFile9 =
|
|
2907
|
+
async function loadUserConfig(env = process.env, readFile9 = readRegularTextFile) {
|
|
2659
2908
|
const configPath = resolveUserConfigPath(env);
|
|
2660
2909
|
let content;
|
|
2661
2910
|
try {
|
|
2662
2911
|
content = await readFile9(configPath);
|
|
2663
2912
|
} catch (error) {
|
|
2664
|
-
if (
|
|
2913
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
2665
2914
|
if (!hasConfigPathOverride(env)) {
|
|
2666
2915
|
await throwIfLegacyJsonConfigExists(configPath, readFile9);
|
|
2667
2916
|
}
|
|
@@ -2676,7 +2925,7 @@ async function loadUserConfig(env = process.env, readFile9 = (filePath) => readF
|
|
|
2676
2925
|
}
|
|
2677
2926
|
const root = parseUserConfigRoot(configPath, content);
|
|
2678
2927
|
const context = {
|
|
2679
|
-
configDir:
|
|
2928
|
+
configDir: path6.dirname(path6.resolve(configPath)),
|
|
2680
2929
|
warnings: []
|
|
2681
2930
|
};
|
|
2682
2931
|
const config = readConfig(context, root);
|
|
@@ -2689,8 +2938,7 @@ async function loadUserConfig(env = process.env, readFile9 = (filePath) => readF
|
|
|
2689
2938
|
}
|
|
2690
2939
|
|
|
2691
2940
|
// src/update/update-cache-repository.ts
|
|
2692
|
-
import
|
|
2693
|
-
import path6 from "path";
|
|
2941
|
+
import path7 from "path";
|
|
2694
2942
|
import { setTimeout as sleepTimer } from "timers/promises";
|
|
2695
2943
|
|
|
2696
2944
|
// src/update/version-utils.ts
|
|
@@ -2813,7 +3061,7 @@ async function sleep(delayMs, signal) {
|
|
|
2813
3061
|
await sleepTimer(delayMs, void 0, { signal });
|
|
2814
3062
|
}
|
|
2815
3063
|
function getDefaultUpdateCheckCachePath() {
|
|
2816
|
-
return
|
|
3064
|
+
return path7.join(getUserCacheRootDir(), "llm-usage-metrics", "update-check.json");
|
|
2817
3065
|
}
|
|
2818
3066
|
function sanitizeCachePathFragment(value) {
|
|
2819
3067
|
return value.replace(/[^\w.-]/gu, "_");
|
|
@@ -2836,8 +3084,8 @@ function getSessionScopedCachePath(baseCacheFilePath, env, options = {}) {
|
|
|
2836
3084
|
}
|
|
2837
3085
|
const parentPid = options.parentPid ?? process.ppid;
|
|
2838
3086
|
const sessionKey = toCacheSessionKey(env[UPDATE_CHECK_CACHE_SESSION_KEY_ENV_VAR]) ?? `ppid-${parentPid}`;
|
|
2839
|
-
const parsedPath =
|
|
2840
|
-
return
|
|
3087
|
+
const parsedPath = path7.parse(baseCacheFilePath);
|
|
3088
|
+
return path7.join(parsedPath.dir, `${parsedPath.name}.${sessionKey}${parsedPath.ext}`);
|
|
2841
3089
|
}
|
|
2842
3090
|
function toNonNegativeNumber2(value) {
|
|
2843
3091
|
if (typeof value !== "number") {
|
|
@@ -2861,7 +3109,7 @@ function isCacheFresh(payload, cacheTtlMs, now) {
|
|
|
2861
3109
|
async function readUpdateCheckCachePayload(cacheFilePath) {
|
|
2862
3110
|
let content;
|
|
2863
3111
|
try {
|
|
2864
|
-
content = await
|
|
3112
|
+
content = await readRegularTextFile(cacheFilePath);
|
|
2865
3113
|
} catch {
|
|
2866
3114
|
return void 0;
|
|
2867
3115
|
}
|
|
@@ -2886,7 +3134,7 @@ async function readUpdateCheckCachePayload(cacheFilePath) {
|
|
|
2886
3134
|
};
|
|
2887
3135
|
}
|
|
2888
3136
|
async function writeUpdateCheckCachePayload(cacheFilePath, payload) {
|
|
2889
|
-
await
|
|
3137
|
+
await ensureDirectory(path7.dirname(cacheFilePath));
|
|
2890
3138
|
await writeFileAtomic(cacheFilePath, JSON.stringify(payload));
|
|
2891
3139
|
}
|
|
2892
3140
|
async function fetchLatestVersion(packageName, fetchImpl, fetchTimeoutMs, signal) {
|
|
@@ -3015,6 +3263,7 @@ function shouldSkipUpdateCheckForArgv(argv) {
|
|
|
3015
3263
|
"efficiency",
|
|
3016
3264
|
"optimize",
|
|
3017
3265
|
"statusline",
|
|
3266
|
+
"machine",
|
|
3018
3267
|
"help",
|
|
3019
3268
|
"version"
|
|
3020
3269
|
]);
|
|
@@ -3025,7 +3274,7 @@ function shouldSkipUpdateCheckForArgv(argv) {
|
|
|
3025
3274
|
return true;
|
|
3026
3275
|
}
|
|
3027
3276
|
const firstRecognizedCommand = executableArgs.find((arg) => commandNames.has(arg));
|
|
3028
|
-
return firstRecognizedCommand === "help" || firstRecognizedCommand === "version" || firstRecognizedCommand === "statusline";
|
|
3277
|
+
return firstRecognizedCommand === "help" || firstRecognizedCommand === "version" || firstRecognizedCommand === "statusline" || firstRecognizedCommand === "machine" && executableArgs[executableArgs.indexOf("machine") + 1] === "export";
|
|
3029
3278
|
}
|
|
3030
3279
|
function isLikelyNpxExecution(argv, env) {
|
|
3031
3280
|
const executablePath = argv[1] ?? "";
|
|
@@ -3149,12 +3398,9 @@ var logger = {
|
|
|
3149
3398
|
};
|
|
3150
3399
|
|
|
3151
3400
|
// src/utils/stdout-pipe-errors.ts
|
|
3152
|
-
function isBrokenPipeError(error) {
|
|
3153
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "EPIPE";
|
|
3154
|
-
}
|
|
3155
3401
|
function exitQuietlyOnBrokenPipe(stream, exit = () => process.exit()) {
|
|
3156
3402
|
stream.on("error", (error) => {
|
|
3157
|
-
if (
|
|
3403
|
+
if (hasErrorCode(error, "EPIPE")) {
|
|
3158
3404
|
exit();
|
|
3159
3405
|
return;
|
|
3160
3406
|
}
|
|
@@ -3163,7 +3409,7 @@ function exitQuietlyOnBrokenPipe(stream, exit = () => process.exit()) {
|
|
|
3163
3409
|
}
|
|
3164
3410
|
|
|
3165
3411
|
// src/cli/create-cli.ts
|
|
3166
|
-
import { Command as
|
|
3412
|
+
import { Command as Command6 } from "commander";
|
|
3167
3413
|
|
|
3168
3414
|
// src/utils/suggest-closest.ts
|
|
3169
3415
|
function editDistance(left, right4) {
|
|
@@ -3231,7 +3477,26 @@ function getDateTimeFormatter(timezone) {
|
|
|
3231
3477
|
dateTimeFormatterCache.set(timezone, formatter);
|
|
3232
3478
|
return formatter;
|
|
3233
3479
|
}
|
|
3480
|
+
var MAX_MEMOIZED_TIMESTAMPS = 1e5;
|
|
3481
|
+
var localDatePartsByTimestamp = /* @__PURE__ */ new Map();
|
|
3234
3482
|
function extractLocalDateParts(timestampIso, timezone) {
|
|
3483
|
+
let partsByTimestamp = localDatePartsByTimestamp.get(timezone);
|
|
3484
|
+
if (!partsByTimestamp) {
|
|
3485
|
+
partsByTimestamp = /* @__PURE__ */ new Map();
|
|
3486
|
+
localDatePartsByTimestamp.set(timezone, partsByTimestamp);
|
|
3487
|
+
}
|
|
3488
|
+
const cachedParts = partsByTimestamp.get(timestampIso);
|
|
3489
|
+
if (cachedParts) {
|
|
3490
|
+
return cachedParts;
|
|
3491
|
+
}
|
|
3492
|
+
const parts = parseLocalDateParts(timestampIso, timezone);
|
|
3493
|
+
if (partsByTimestamp.size >= MAX_MEMOIZED_TIMESTAMPS) {
|
|
3494
|
+
partsByTimestamp.clear();
|
|
3495
|
+
}
|
|
3496
|
+
partsByTimestamp.set(timestampIso, parts);
|
|
3497
|
+
return parts;
|
|
3498
|
+
}
|
|
3499
|
+
function parseLocalDateParts(timestampIso, timezone) {
|
|
3235
3500
|
const date = new Date(timestampIso);
|
|
3236
3501
|
const timestampMs = date.getTime();
|
|
3237
3502
|
if (Number.isNaN(timestampMs)) {
|
|
@@ -3564,6 +3829,21 @@ function sourceSortComparator(left, right4, sourceWeightMap) {
|
|
|
3564
3829
|
}
|
|
3565
3830
|
return compareByCodePoint(left, right4);
|
|
3566
3831
|
}
|
|
3832
|
+
function toMachineRowKey(source, machine) {
|
|
3833
|
+
return `${source}\0${machine}`;
|
|
3834
|
+
}
|
|
3835
|
+
function fromMachineRowKey(rowKey) {
|
|
3836
|
+
const separator = rowKey.indexOf("\0");
|
|
3837
|
+
return { source: rowKey.slice(0, separator), machine: rowKey.slice(separator + 1) };
|
|
3838
|
+
}
|
|
3839
|
+
function compareRowKeys(left, right4, sourceWeightMap, byMachine) {
|
|
3840
|
+
if (!byMachine) {
|
|
3841
|
+
return sourceSortComparator(left, right4, sourceWeightMap);
|
|
3842
|
+
}
|
|
3843
|
+
const leftKey = fromMachineRowKey(left);
|
|
3844
|
+
const rightKey = fromMachineRowKey(right4);
|
|
3845
|
+
return sourceSortComparator(leftKey.source, rightKey.source, sourceWeightMap) || Number(leftKey.machine !== LOCAL_MACHINE_NAME) - Number(rightKey.machine !== LOCAL_MACHINE_NAME) || compareByCodePoint(leftKey.machine, rightKey.machine);
|
|
3846
|
+
}
|
|
3567
3847
|
function aggregateUsage(events, options) {
|
|
3568
3848
|
const includeModelBreakdown = options.includeModelBreakdown ?? true;
|
|
3569
3849
|
const sourceWeightMap = /* @__PURE__ */ new Map();
|
|
@@ -3575,8 +3855,9 @@ function aggregateUsage(events, options) {
|
|
|
3575
3855
|
const periodKey = getPeriodKey(event.timestamp, options.granularity, options.timezone);
|
|
3576
3856
|
const periodSources = periodMap.get(periodKey) ?? /* @__PURE__ */ new Map();
|
|
3577
3857
|
periodMap.set(periodKey, periodSources);
|
|
3578
|
-
const
|
|
3579
|
-
periodSources.
|
|
3858
|
+
const rowKey = options.byMachine ? toMachineRowKey(event.source, event.machine ?? LOCAL_MACHINE_NAME) : event.source;
|
|
3859
|
+
const rowAccumulator = periodSources.get(rowKey) ?? createRowAccumulator(includeModelBreakdown);
|
|
3860
|
+
periodSources.set(rowKey, rowAccumulator);
|
|
3580
3861
|
addEventToAccumulator(rowAccumulator, event, includeModelBreakdown);
|
|
3581
3862
|
}
|
|
3582
3863
|
const sortedPeriodKeys = [...periodMap.keys()].sort(compareByCodePoint);
|
|
@@ -3590,11 +3871,12 @@ function aggregateUsage(events, options) {
|
|
|
3590
3871
|
}
|
|
3591
3872
|
const periodCombinedTotals = createEmptyTotals();
|
|
3592
3873
|
const periodCombinedModelTotals = /* @__PURE__ */ new Map();
|
|
3593
|
-
const
|
|
3594
|
-
(left, right4) =>
|
|
3874
|
+
const sortedRowKeys = [...sourceMap.keys()].sort(
|
|
3875
|
+
(left, right4) => compareRowKeys(left, right4, sourceWeightMap, options.byMachine === true)
|
|
3595
3876
|
);
|
|
3596
|
-
for (const
|
|
3597
|
-
const accumulator = sourceMap.get(
|
|
3877
|
+
for (const rowKey of sortedRowKeys) {
|
|
3878
|
+
const accumulator = sourceMap.get(rowKey);
|
|
3879
|
+
const { source, machine } = options.byMachine ? fromMachineRowKey(rowKey) : { source: rowKey, machine: void 0 };
|
|
3598
3880
|
if (!accumulator) {
|
|
3599
3881
|
continue;
|
|
3600
3882
|
}
|
|
@@ -3603,6 +3885,7 @@ function aggregateUsage(events, options) {
|
|
|
3603
3885
|
rowType: "period_source",
|
|
3604
3886
|
periodKey,
|
|
3605
3887
|
source,
|
|
3888
|
+
...machine === void 0 ? {} : { machine },
|
|
3606
3889
|
models: rankedModelUsage.models,
|
|
3607
3890
|
modelBreakdown: rankedModelUsage.modelBreakdown,
|
|
3608
3891
|
...accumulator.totals
|
|
@@ -3615,7 +3898,7 @@ function aggregateUsage(events, options) {
|
|
|
3615
3898
|
mergeModelTotals(grandModelTotals, accumulator.modelTotals);
|
|
3616
3899
|
}
|
|
3617
3900
|
}
|
|
3618
|
-
if (
|
|
3901
|
+
if (sortedRowKeys.length > 1) {
|
|
3619
3902
|
const rankedCombinedModels = includeModelBreakdown ? toRankedModelUsage(periodCombinedModelTotals) : { models: [], modelBreakdown: [] };
|
|
3620
3903
|
const combinedRow = {
|
|
3621
3904
|
rowType: "period_combined",
|
|
@@ -3871,11 +4154,11 @@ async function resolveUserConfigForOptions(options, deps = {}) {
|
|
|
3871
4154
|
|
|
3872
4155
|
// src/sources/amp/amp-source-adapter.ts
|
|
3873
4156
|
import os5 from "os";
|
|
3874
|
-
import
|
|
4157
|
+
import path9 from "path";
|
|
3875
4158
|
|
|
3876
4159
|
// src/utils/discover-files.ts
|
|
3877
4160
|
import { readdir, realpath, stat as stat2 } from "fs/promises";
|
|
3878
|
-
import
|
|
4161
|
+
import path8 from "path";
|
|
3879
4162
|
var UNRESOLVABLE_SYMLINK_ERROR_CODES = /* @__PURE__ */ new Set(["ENOENT", "ELOOP", "ENOTDIR", "ENAMETOOLONG"]);
|
|
3880
4163
|
function getNodeErrorCode(error) {
|
|
3881
4164
|
const record = asRecord(error);
|
|
@@ -3934,7 +4217,7 @@ async function walkDirectory(rootDir, acc, options, ancestryRealPaths) {
|
|
|
3934
4217
|
entries.sort((left, right4) => compareByCodePoint(left.name, right4.name));
|
|
3935
4218
|
}
|
|
3936
4219
|
for (const entry of entries) {
|
|
3937
|
-
const entryPath =
|
|
4220
|
+
const entryPath = path8.join(rootDir, entry.name);
|
|
3938
4221
|
if (entry.isSymbolicLink()) {
|
|
3939
4222
|
try {
|
|
3940
4223
|
const entryStats = await stat2(entryPath);
|
|
@@ -3943,7 +4226,7 @@ async function walkDirectory(rootDir, acc, options, ancestryRealPaths) {
|
|
|
3943
4226
|
await walkDirectory(entryPath, acc, options, nextAncestryRealPaths);
|
|
3944
4227
|
continue;
|
|
3945
4228
|
}
|
|
3946
|
-
if (entryStats.isFile() && (matchesExtension(entry.name, options.extension) || matchesExtension(
|
|
4229
|
+
if (entryStats.isFile() && (matchesExtension(entry.name, options.extension) || matchesExtension(path8.basename(resolvedEntryPath), options.extension))) {
|
|
3947
4230
|
acc.push(entryPath);
|
|
3948
4231
|
}
|
|
3949
4232
|
} catch (error) {
|
|
@@ -4140,13 +4423,13 @@ function normalizeEnvPath(value) {
|
|
|
4140
4423
|
return normalized || void 0;
|
|
4141
4424
|
}
|
|
4142
4425
|
function resolveDefaultAmpThreadsDir(env, homeDir) {
|
|
4143
|
-
const dataHome = normalizeEnvPath(env.XDG_DATA_HOME) ??
|
|
4144
|
-
return
|
|
4426
|
+
const dataHome = normalizeEnvPath(env.XDG_DATA_HOME) ?? path9.join(homeDir, ".local", "share");
|
|
4427
|
+
return path9.join(dataHome, "amp", "threads");
|
|
4145
4428
|
}
|
|
4146
4429
|
function toIdentityText(value) {
|
|
4147
|
-
const
|
|
4148
|
-
if (
|
|
4149
|
-
return
|
|
4430
|
+
const text2 = asTrimmedText(value);
|
|
4431
|
+
if (text2) {
|
|
4432
|
+
return text2;
|
|
4150
4433
|
}
|
|
4151
4434
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
4152
4435
|
return String(Math.trunc(value));
|
|
@@ -4392,20 +4675,20 @@ var AmpSourceAdapter = class {
|
|
|
4392
4675
|
};
|
|
4393
4676
|
|
|
4394
4677
|
// src/sources/antigravity/antigravity-source-adapter.ts
|
|
4395
|
-
import
|
|
4678
|
+
import path11 from "path";
|
|
4396
4679
|
|
|
4397
4680
|
// src/sources/antigravity/antigravity-path-resolver.ts
|
|
4398
4681
|
import os6 from "os";
|
|
4399
|
-
import
|
|
4682
|
+
import path10 from "path";
|
|
4400
4683
|
function resolveGeminiCliHome(options) {
|
|
4401
|
-
return asTrimmedText(options.env.GEMINI_CLI_HOME) ??
|
|
4684
|
+
return asTrimmedText(options.env.GEMINI_CLI_HOME) ?? path10.join(options.homeDir, ".gemini");
|
|
4402
4685
|
}
|
|
4403
4686
|
function getDefaultAntigravityConversationsDir(options = {}) {
|
|
4404
4687
|
const resolvedOptions = {
|
|
4405
4688
|
env: options.env ?? process.env,
|
|
4406
4689
|
homeDir: options.homeDir ?? os6.homedir()
|
|
4407
4690
|
};
|
|
4408
|
-
return
|
|
4691
|
+
return path10.join(resolveGeminiCliHome(resolvedOptions), "antigravity-cli", "conversations");
|
|
4409
4692
|
}
|
|
4410
4693
|
|
|
4411
4694
|
// src/sources/antigravity/antigravity-proto-reader.ts
|
|
@@ -4786,7 +5069,7 @@ var AntigravitySourceAdapter = class {
|
|
|
4786
5069
|
const skippedRowReasons = /* @__PURE__ */ new Map();
|
|
4787
5070
|
for (const row of rows) {
|
|
4788
5071
|
const parsedRow = parseAntigravityMetadataBlob(row.data, {
|
|
4789
|
-
sessionId:
|
|
5072
|
+
sessionId: path11.basename(normalizedDbPath, path11.extname(normalizedDbPath)),
|
|
4790
5073
|
fallbackTimestamp,
|
|
4791
5074
|
seenResponseIds
|
|
4792
5075
|
});
|
|
@@ -4810,7 +5093,7 @@ var AntigravitySourceAdapter = class {
|
|
|
4810
5093
|
|
|
4811
5094
|
// src/sources/claude/claude-source-adapter.ts
|
|
4812
5095
|
import os7 from "os";
|
|
4813
|
-
import
|
|
5096
|
+
import path14 from "path";
|
|
4814
5097
|
|
|
4815
5098
|
// src/utils/discover-jsonl-files.ts
|
|
4816
5099
|
async function discoverJsonlFiles(rootDir) {
|
|
@@ -4912,7 +5195,7 @@ function isBlankLine(lineBytes, lineStart, lineEnd) {
|
|
|
4912
5195
|
}
|
|
4913
5196
|
|
|
4914
5197
|
// src/sources/multi-root-discovery.ts
|
|
4915
|
-
import
|
|
5198
|
+
import path12 from "path";
|
|
4916
5199
|
function resolveRootDirs(overrideDir, defaultRootDirs2) {
|
|
4917
5200
|
if (overrideDir === void 0) {
|
|
4918
5201
|
return defaultRootDirs2;
|
|
@@ -4920,10 +5203,10 @@ function resolveRootDirs(overrideDir, defaultRootDirs2) {
|
|
|
4920
5203
|
return typeof overrideDir === "string" ? [overrideDir] : overrideDir;
|
|
4921
5204
|
}
|
|
4922
5205
|
function isPathWithinRoots(filePath, rootDirs) {
|
|
4923
|
-
const resolvedFilePath =
|
|
5206
|
+
const resolvedFilePath = path12.resolve(filePath);
|
|
4924
5207
|
return rootDirs.some((rootDir) => {
|
|
4925
|
-
const relativePath =
|
|
4926
|
-
return relativePath.length > 0 && !relativePath.startsWith("..") && !
|
|
5208
|
+
const relativePath = path12.relative(path12.resolve(rootDir.trim()), resolvedFilePath);
|
|
5209
|
+
return relativePath.length > 0 && !relativePath.startsWith("..") && !path12.isAbsolute(relativePath);
|
|
4927
5210
|
});
|
|
4928
5211
|
}
|
|
4929
5212
|
async function discoverFilesAcrossRoots(options) {
|
|
@@ -4942,22 +5225,22 @@ async function discoverFilesAcrossRoots(options) {
|
|
|
4942
5225
|
discoveredFiles.push(...await options.discoverInRoot(normalizedRootDir));
|
|
4943
5226
|
}
|
|
4944
5227
|
const uniqueFiles = [
|
|
4945
|
-
...new Map(discoveredFiles.map((filePath) => [
|
|
5228
|
+
...new Map(discoveredFiles.map((filePath) => [path12.resolve(filePath), filePath])).values()
|
|
4946
5229
|
];
|
|
4947
5230
|
return options.sortAcrossRoots ? uniqueFiles.sort(compareByCodePoint) : uniqueFiles;
|
|
4948
5231
|
}
|
|
4949
5232
|
|
|
4950
5233
|
// src/sources/claude/claude-fork-transcript.ts
|
|
4951
|
-
import
|
|
5234
|
+
import path13 from "path";
|
|
4952
5235
|
var SUBAGENT_TRANSCRIPT_PATTERN = /^agent-([\w-]+)\.jsonl$/u;
|
|
4953
5236
|
var SUBAGENTS_DIR_NAME = "subagents";
|
|
4954
5237
|
var CLAUDE_ASSISTANT_BYTES = Buffer.from('"assistant"');
|
|
4955
5238
|
function getClaudeSubagentMetaPath(filePath) {
|
|
4956
|
-
const match = SUBAGENT_TRANSCRIPT_PATTERN.exec(
|
|
4957
|
-
if (!match ||
|
|
5239
|
+
const match = SUBAGENT_TRANSCRIPT_PATTERN.exec(path13.basename(filePath));
|
|
5240
|
+
if (!match || path13.basename(path13.dirname(filePath)) !== SUBAGENTS_DIR_NAME) {
|
|
4958
5241
|
return void 0;
|
|
4959
5242
|
}
|
|
4960
|
-
return
|
|
5243
|
+
return path13.join(path13.dirname(filePath), `agent-${match[1]}.meta.json`);
|
|
4961
5244
|
}
|
|
4962
5245
|
async function resolveClaudeForkParentPath(filePath) {
|
|
4963
5246
|
const metaPath = getClaudeSubagentMetaPath(filePath);
|
|
@@ -4969,12 +5252,12 @@ async function resolveClaudeForkParentPath(filePath) {
|
|
|
4969
5252
|
if (meta?.isFork !== true) {
|
|
4970
5253
|
return void 0;
|
|
4971
5254
|
}
|
|
4972
|
-
const subagentsDir =
|
|
5255
|
+
const subagentsDir = path13.dirname(filePath);
|
|
4973
5256
|
const parentAgentId = asTrimmedText(meta.parentAgentId);
|
|
4974
5257
|
if (parentAgentId) {
|
|
4975
|
-
return SUBAGENT_TRANSCRIPT_PATTERN.test(`agent-${parentAgentId}.jsonl`) ?
|
|
5258
|
+
return SUBAGENT_TRANSCRIPT_PATTERN.test(`agent-${parentAgentId}.jsonl`) ? path13.join(subagentsDir, `agent-${parentAgentId}.jsonl`) : void 0;
|
|
4976
5259
|
}
|
|
4977
|
-
return `${
|
|
5260
|
+
return `${path13.dirname(subagentsDir)}.jsonl`;
|
|
4978
5261
|
}
|
|
4979
5262
|
function getClaudeMessageKey(line, message) {
|
|
4980
5263
|
const messageId = asTrimmedText(message.id);
|
|
@@ -5003,12 +5286,12 @@ async function readClaudeMessageKeys(filePath) {
|
|
|
5003
5286
|
|
|
5004
5287
|
// src/sources/claude/claude-source-adapter.ts
|
|
5005
5288
|
function getClaudeRootDirsForConfigDir(configDir) {
|
|
5006
|
-
return [
|
|
5289
|
+
return [path14.join(configDir, "projects"), path14.join(configDir, "transcripts")];
|
|
5007
5290
|
}
|
|
5008
5291
|
function resolveDefaultClaudeRootDirs(env = process.env, homeDir = os7.homedir()) {
|
|
5009
5292
|
const configDirs = (env.CLAUDE_CONFIG_DIR ?? "").split(",").map((configDir) => configDir.trim()).filter((configDir) => configDir.length > 0);
|
|
5010
5293
|
if (configDirs.length === 0) {
|
|
5011
|
-
return getClaudeRootDirsForConfigDir(
|
|
5294
|
+
return getClaudeRootDirsForConfigDir(path14.join(homeDir, ".claude"));
|
|
5012
5295
|
}
|
|
5013
5296
|
return configDirs.flatMap((configDir) => getClaudeRootDirsForConfigDir(configDir));
|
|
5014
5297
|
}
|
|
@@ -5018,7 +5301,7 @@ function shouldParseClaudeJsonlLineBytes(lineBytes) {
|
|
|
5018
5301
|
return lineBytes.includes(CLAUDE_ASSISTANT_BYTES2) && lineBytes.includes(CLAUDE_USAGE_BYTES);
|
|
5019
5302
|
}
|
|
5020
5303
|
function getFallbackSessionId(filePath) {
|
|
5021
|
-
return
|
|
5304
|
+
return path14.basename(filePath, ".jsonl");
|
|
5022
5305
|
}
|
|
5023
5306
|
function resolveProvider(message, model) {
|
|
5024
5307
|
const explicitProvider = asTrimmedText(message.provider);
|
|
@@ -5204,11 +5487,11 @@ var ClaudeSourceAdapter = class {
|
|
|
5204
5487
|
|
|
5205
5488
|
// src/sources/cline/cline-family-adapter.ts
|
|
5206
5489
|
import os8 from "os";
|
|
5207
|
-
import
|
|
5490
|
+
import path16 from "path";
|
|
5208
5491
|
|
|
5209
5492
|
// src/sources/cline/cline-task-parser.ts
|
|
5210
5493
|
import { readFile as readFile3 } from "fs/promises";
|
|
5211
|
-
import
|
|
5494
|
+
import path15 from "path";
|
|
5212
5495
|
var ENVIRONMENT_DETAILS_OPEN_TAG = "<environment_details>";
|
|
5213
5496
|
var ENVIRONMENT_DETAILS_CLOSE_TAG = "</environment_details>";
|
|
5214
5497
|
var MODEL_OPEN_TAG = "<model>";
|
|
@@ -5242,10 +5525,10 @@ function incrementContextSkippedReason2(context, reason) {
|
|
|
5242
5525
|
incrementSkippedReason(context.skippedRowReasons, reason);
|
|
5243
5526
|
}
|
|
5244
5527
|
function getTaskId(filePath) {
|
|
5245
|
-
return
|
|
5528
|
+
return path15.basename(path15.dirname(filePath));
|
|
5246
5529
|
}
|
|
5247
5530
|
function getClineTaskHistoryPath(uiMessagesPath) {
|
|
5248
|
-
return
|
|
5531
|
+
return path15.join(path15.dirname(uiMessagesPath), "api_conversation_history.json");
|
|
5249
5532
|
}
|
|
5250
5533
|
var CACHE_INCLUSIVE_INPUT_SOURCE_IDS = /* @__PURE__ */ new Set(["roocode", "kilocode"]);
|
|
5251
5534
|
function resolveInputTokens(sourceId, tokensIn, cacheReadTokens, cacheWriteTokens) {
|
|
@@ -5280,12 +5563,12 @@ function extractUsage(sourceId, payload) {
|
|
|
5280
5563
|
};
|
|
5281
5564
|
}
|
|
5282
5565
|
function parsePayload(entry) {
|
|
5283
|
-
const
|
|
5284
|
-
if (!
|
|
5566
|
+
const text2 = asTrimmedText(entry.text);
|
|
5567
|
+
if (!text2) {
|
|
5285
5568
|
return void 0;
|
|
5286
5569
|
}
|
|
5287
5570
|
try {
|
|
5288
|
-
return asRecord(JSON.parse(
|
|
5571
|
+
return asRecord(JSON.parse(text2));
|
|
5289
5572
|
} catch {
|
|
5290
5573
|
return void 0;
|
|
5291
5574
|
}
|
|
@@ -5402,23 +5685,23 @@ function normalizeEnvPath2(value) {
|
|
|
5402
5685
|
return normalized || void 0;
|
|
5403
5686
|
}
|
|
5404
5687
|
function getExtensionTasksDir(codeRoot, extensionId) {
|
|
5405
|
-
return
|
|
5688
|
+
return path16.join(codeRoot, "User", "globalStorage", extensionId, "tasks");
|
|
5406
5689
|
}
|
|
5407
5690
|
function getWindowsCodeRoot(homeDir, env) {
|
|
5408
|
-
const appDataRoot = normalizeEnvPath2(env.APPDATA) ?? normalizeEnvPath2(env.USERPROFILE) ??
|
|
5409
|
-
if (appDataRoot.endsWith(`${
|
|
5691
|
+
const appDataRoot = normalizeEnvPath2(env.APPDATA) ?? normalizeEnvPath2(env.USERPROFILE) ?? path16.join(homeDir, "AppData", "Roaming");
|
|
5692
|
+
if (appDataRoot.endsWith(`${path16.sep}Code`) || path16.basename(appDataRoot) === "Code") {
|
|
5410
5693
|
return appDataRoot;
|
|
5411
5694
|
}
|
|
5412
|
-
return
|
|
5695
|
+
return path16.join(appDataRoot, "Code");
|
|
5413
5696
|
}
|
|
5414
5697
|
function getPlatformCodeRoots(platform, homeDir, env) {
|
|
5415
5698
|
switch (platform) {
|
|
5416
5699
|
case "win32":
|
|
5417
5700
|
return [getWindowsCodeRoot(homeDir, env)];
|
|
5418
5701
|
case "darwin":
|
|
5419
|
-
return [
|
|
5702
|
+
return [path16.join(homeDir, "Library", "Application Support", "Code")];
|
|
5420
5703
|
default:
|
|
5421
|
-
return [
|
|
5704
|
+
return [path16.join(homeDir, ".config", "Code")];
|
|
5422
5705
|
}
|
|
5423
5706
|
}
|
|
5424
5707
|
function getDefaultClineTaskRootCandidates(options) {
|
|
@@ -5426,7 +5709,7 @@ function getDefaultClineTaskRootCandidates(options) {
|
|
|
5426
5709
|
const homeDir = options.homeDir ?? os8.homedir();
|
|
5427
5710
|
const env = options.env ?? process.env;
|
|
5428
5711
|
const localRoots = getPlatformCodeRoots(platform, homeDir, env);
|
|
5429
|
-
const remoteRoots = [
|
|
5712
|
+
const remoteRoots = [path16.join(homeDir, ".vscode-server", "data")];
|
|
5430
5713
|
const tasksDirs = [...localRoots, ...remoteRoots].map(
|
|
5431
5714
|
(root) => getExtensionTasksDir(root, options.extensionId)
|
|
5432
5715
|
);
|
|
@@ -5434,7 +5717,7 @@ function getDefaultClineTaskRootCandidates(options) {
|
|
|
5434
5717
|
}
|
|
5435
5718
|
async function discoverUiMessageFiles(rootDir) {
|
|
5436
5719
|
const jsonFiles = await discoverFiles(rootDir, { extension: ".json" });
|
|
5437
|
-
return jsonFiles.filter((filePath) =>
|
|
5720
|
+
return jsonFiles.filter((filePath) => path16.basename(filePath) === "ui_messages.json");
|
|
5438
5721
|
}
|
|
5439
5722
|
var ClineFamilyAdapter = class {
|
|
5440
5723
|
id;
|
|
@@ -5479,9 +5762,9 @@ function createClineFamilyAdapter(options) {
|
|
|
5479
5762
|
|
|
5480
5763
|
// src/sources/codex/codex-source-adapter.ts
|
|
5481
5764
|
import os9 from "os";
|
|
5482
|
-
import
|
|
5765
|
+
import path17 from "path";
|
|
5483
5766
|
function resolveDefaultCodexSessionsDir(env = process.env, homeDir = os9.homedir()) {
|
|
5484
|
-
return
|
|
5767
|
+
return path17.join(asTrimmedText(env.CODEX_HOME) ?? path17.join(homeDir, ".codex"), "sessions");
|
|
5485
5768
|
}
|
|
5486
5769
|
var LEGACY_CODEX_MODEL_FALLBACK = "legacy-codex-unknown";
|
|
5487
5770
|
var SESSION_META_BYTES = Buffer.from('"session_meta"');
|
|
@@ -5611,7 +5894,7 @@ function createLastUsageOnlyKey(timestamp, usage) {
|
|
|
5611
5894
|
].join(":");
|
|
5612
5895
|
}
|
|
5613
5896
|
function getFallbackSessionId2(filePath) {
|
|
5614
|
-
return
|
|
5897
|
+
return path17.basename(filePath, ".jsonl");
|
|
5615
5898
|
}
|
|
5616
5899
|
function resolveRepoRootFromPayload(payload) {
|
|
5617
5900
|
if (!payload) {
|
|
@@ -5811,9 +6094,9 @@ var CodexSourceAdapter = class {
|
|
|
5811
6094
|
|
|
5812
6095
|
// src/sources/copilot/copilot-source-adapter.ts
|
|
5813
6096
|
import os10 from "os";
|
|
5814
|
-
import
|
|
6097
|
+
import path18 from "path";
|
|
5815
6098
|
import { realpath as realpath2 } from "fs/promises";
|
|
5816
|
-
var defaultOtelDir =
|
|
6099
|
+
var defaultOtelDir = path18.join(os10.homedir(), ".copilot", "otel");
|
|
5817
6100
|
function shouldParseCopilotJsonlLine(lineText) {
|
|
5818
6101
|
return lineText.includes('"attributes"');
|
|
5819
6102
|
}
|
|
@@ -5928,7 +6211,7 @@ function hasTokenUsage(candidate) {
|
|
|
5928
6211
|
return candidate.inputTokens + candidate.outputTokens + candidate.reasoningTokens + candidate.cacheReadTokens + candidate.cacheWriteTokens + (candidate.totalTokens ?? 0) > 0;
|
|
5929
6212
|
}
|
|
5930
6213
|
function getFallbackSessionId3(filePath) {
|
|
5931
|
-
return
|
|
6214
|
+
return path18.basename(filePath, ".jsonl");
|
|
5932
6215
|
}
|
|
5933
6216
|
function extractCandidate(record, filePath, lineIndex) {
|
|
5934
6217
|
const attributes = asRecord(record.attributes);
|
|
@@ -6126,18 +6409,18 @@ var CopilotSourceAdapter = class {
|
|
|
6126
6409
|
|
|
6127
6410
|
// src/sources/droid/droid-source-adapter.ts
|
|
6128
6411
|
import os11 from "os";
|
|
6129
|
-
import
|
|
6130
|
-
var defaultSessionsDir =
|
|
6412
|
+
import path19 from "path";
|
|
6413
|
+
var defaultSessionsDir = path19.join(os11.homedir(), ".factory", "sessions");
|
|
6131
6414
|
var DROID_SESSION_START_LINE_PATTERN = /"type"\s*:\s*"session_start"/u;
|
|
6132
6415
|
var DROID_MESSAGE_LINE_PATTERN = /"type"\s*:\s*"message"/u;
|
|
6133
6416
|
function shouldParseDroidJsonlLine(lineText) {
|
|
6134
6417
|
return DROID_SESSION_START_LINE_PATTERN.test(lineText) || DROID_MESSAGE_LINE_PATTERN.test(lineText);
|
|
6135
6418
|
}
|
|
6136
6419
|
function getSettingsSessionId(filePath) {
|
|
6137
|
-
return
|
|
6420
|
+
return path19.basename(filePath, ".settings.json");
|
|
6138
6421
|
}
|
|
6139
6422
|
function getSiblingJsonlPath(settingsPath) {
|
|
6140
|
-
return
|
|
6423
|
+
return path19.join(path19.dirname(settingsPath), `${getSettingsSessionId(settingsPath)}.jsonl`);
|
|
6141
6424
|
}
|
|
6142
6425
|
function isSessionStartRecord(line) {
|
|
6143
6426
|
return asTrimmedText(line.type) === "session_start";
|
|
@@ -6286,7 +6569,7 @@ var DroidSourceAdapter = class {
|
|
|
6286
6569
|
};
|
|
6287
6570
|
|
|
6288
6571
|
// src/sources/dsh/dsh-source-adapter.ts
|
|
6289
|
-
import
|
|
6572
|
+
import path21 from "path";
|
|
6290
6573
|
|
|
6291
6574
|
// src/sources/dsh/dsh-session-log-reader.ts
|
|
6292
6575
|
import { readFile as readFile4 } from "fs/promises";
|
|
@@ -6347,8 +6630,8 @@ async function decodeFrame(frameBytes) {
|
|
|
6347
6630
|
}
|
|
6348
6631
|
return decoded;
|
|
6349
6632
|
}
|
|
6350
|
-
function* parseJsonlLines(
|
|
6351
|
-
for (const rawLine of
|
|
6633
|
+
function* parseJsonlLines(text2, options) {
|
|
6634
|
+
for (const rawLine of text2.split("\n")) {
|
|
6352
6635
|
const line = rawLine.trim();
|
|
6353
6636
|
if (line.length === 0 || !options.shouldParseLine(line)) {
|
|
6354
6637
|
continue;
|
|
@@ -6399,17 +6682,17 @@ async function* readDshSessionLogRecords(filePath, options) {
|
|
|
6399
6682
|
|
|
6400
6683
|
// src/sources/dsh/dsh-path-resolver.ts
|
|
6401
6684
|
import os12 from "os";
|
|
6402
|
-
import
|
|
6685
|
+
import path20 from "path";
|
|
6403
6686
|
function resolveDshHome(options) {
|
|
6404
6687
|
const configuredHome = asTrimmedText(options.env.DSH_HOME);
|
|
6405
6688
|
if (!configuredHome) {
|
|
6406
|
-
return
|
|
6689
|
+
return path20.join(options.homeDir, ".dsh");
|
|
6407
6690
|
}
|
|
6408
6691
|
if (configuredHome === "~") {
|
|
6409
6692
|
return options.homeDir;
|
|
6410
6693
|
}
|
|
6411
6694
|
if (configuredHome.startsWith("~/") || configuredHome.startsWith("~\\")) {
|
|
6412
|
-
return
|
|
6695
|
+
return path20.join(options.homeDir, configuredHome.slice(2));
|
|
6413
6696
|
}
|
|
6414
6697
|
return configuredHome;
|
|
6415
6698
|
}
|
|
@@ -6418,7 +6701,7 @@ function getDefaultDshSessionsDir(options = {}) {
|
|
|
6418
6701
|
env: options.env ?? process.env,
|
|
6419
6702
|
homeDir: options.homeDir ?? os12.homedir()
|
|
6420
6703
|
};
|
|
6421
|
-
return
|
|
6704
|
+
return path20.join(resolveDshHome(resolvedOptions), "sessions");
|
|
6422
6705
|
}
|
|
6423
6706
|
|
|
6424
6707
|
// src/sources/dsh/dsh-source-adapter.ts
|
|
@@ -6433,11 +6716,11 @@ function isDshSessionLogFile(fileName) {
|
|
|
6433
6716
|
return DSH_SESSION_LOG_FILENAME_PATTERN.test(fileName);
|
|
6434
6717
|
}
|
|
6435
6718
|
function getFallbackSessionId4(filePath) {
|
|
6436
|
-
const parentDirName =
|
|
6719
|
+
const parentDirName = path21.basename(path21.dirname(filePath));
|
|
6437
6720
|
if (parentDirName.startsWith("session-")) {
|
|
6438
6721
|
return parentDirName;
|
|
6439
6722
|
}
|
|
6440
|
-
return
|
|
6723
|
+
return path21.basename(filePath).replace(/\.zstd$/u, "");
|
|
6441
6724
|
}
|
|
6442
6725
|
function resolveRoute(record) {
|
|
6443
6726
|
const route = asRecord(record?.route) ?? record;
|
|
@@ -6502,7 +6785,7 @@ var DshSourceAdapter = class {
|
|
|
6502
6785
|
...await discoverFiles(rootDir, { extension: ".jsonl" }),
|
|
6503
6786
|
...await discoverFiles(rootDir, { extension: ".jsonl.zstd" })
|
|
6504
6787
|
];
|
|
6505
|
-
return discovered.filter((filePath) => isDshSessionLogFile(
|
|
6788
|
+
return discovered.filter((filePath) => isDshSessionLogFile(path21.basename(filePath)));
|
|
6506
6789
|
}
|
|
6507
6790
|
});
|
|
6508
6791
|
}
|
|
@@ -6589,19 +6872,19 @@ var DshSourceAdapter = class {
|
|
|
6589
6872
|
};
|
|
6590
6873
|
|
|
6591
6874
|
// src/sources/gemini/gemini-source-adapter.ts
|
|
6592
|
-
import { createHash as
|
|
6875
|
+
import { createHash as createHash3 } from "crypto";
|
|
6593
6876
|
import { readdir as readdir2, readFile as readFile5 } from "fs/promises";
|
|
6594
6877
|
import os13 from "os";
|
|
6595
|
-
import
|
|
6596
|
-
var defaultGeminiDir =
|
|
6878
|
+
import path22 from "path";
|
|
6879
|
+
var defaultGeminiDir = path22.join(os13.homedir(), ".gemini");
|
|
6597
6880
|
function resolveDefaultGeminiDir(env) {
|
|
6598
6881
|
return asTrimmedText(env.GEMINI_CLI_HOME) ?? defaultGeminiDir;
|
|
6599
6882
|
}
|
|
6600
6883
|
function getProjectsJsonPath(geminiDir) {
|
|
6601
|
-
return
|
|
6884
|
+
return path22.join(geminiDir, "projects.json");
|
|
6602
6885
|
}
|
|
6603
6886
|
function hashProjectPath(projectPath) {
|
|
6604
|
-
return
|
|
6887
|
+
return createHash3("sha256").update(projectPath).digest("hex");
|
|
6605
6888
|
}
|
|
6606
6889
|
function parseProjectsJson(data) {
|
|
6607
6890
|
const mapping = /* @__PURE__ */ new Map();
|
|
@@ -6645,7 +6928,7 @@ async function loadProjectsJson(geminiDir) {
|
|
|
6645
6928
|
}
|
|
6646
6929
|
}
|
|
6647
6930
|
async function discoverSessionFiles(geminiDir) {
|
|
6648
|
-
const tmpDir =
|
|
6931
|
+
const tmpDir = path22.join(geminiDir, "tmp");
|
|
6649
6932
|
let projectEntries;
|
|
6650
6933
|
try {
|
|
6651
6934
|
projectEntries = await readdir2(tmpDir, { withFileTypes: true, encoding: "utf8" });
|
|
@@ -6662,7 +6945,7 @@ async function discoverSessionFiles(geminiDir) {
|
|
|
6662
6945
|
if (!projectEntry.isDirectory() && !projectEntry.isSymbolicLink()) {
|
|
6663
6946
|
continue;
|
|
6664
6947
|
}
|
|
6665
|
-
const chatsDir =
|
|
6948
|
+
const chatsDir = path22.join(tmpDir, projectEntry.name, "chats");
|
|
6666
6949
|
const discoveredChatFiles = await discoverFiles(chatsDir, { extension: ".json" });
|
|
6667
6950
|
allSessionFiles.push(...discoveredChatFiles);
|
|
6668
6951
|
}
|
|
@@ -6676,9 +6959,9 @@ function resolveRepoRoot(filePath, sessionData, projectMapping) {
|
|
|
6676
6959
|
return mappedRoot;
|
|
6677
6960
|
}
|
|
6678
6961
|
}
|
|
6679
|
-
const chatsDir =
|
|
6680
|
-
const projectDir =
|
|
6681
|
-
const projectIdentifier =
|
|
6962
|
+
const chatsDir = path22.dirname(filePath);
|
|
6963
|
+
const projectDir = path22.dirname(chatsDir);
|
|
6964
|
+
const projectIdentifier = path22.basename(projectDir);
|
|
6682
6965
|
return projectMapping.get(projectIdentifier);
|
|
6683
6966
|
}
|
|
6684
6967
|
function toFiniteNumber3(value) {
|
|
@@ -6793,7 +7076,7 @@ var GeminiSourceAdapter = class {
|
|
|
6793
7076
|
incrementSkippedReason(skippedRowReasons, "invalid_session_data");
|
|
6794
7077
|
return toParseDiagnostics(events, skippedRows, skippedRowReasons);
|
|
6795
7078
|
}
|
|
6796
|
-
const sessionId = asTrimmedText(sessionDataRecord.sessionId) ??
|
|
7079
|
+
const sessionId = asTrimmedText(sessionDataRecord.sessionId) ?? path22.basename(filePath, ".json");
|
|
6797
7080
|
const projectMapping = await this.getProjectMappingForParse();
|
|
6798
7081
|
const repoRoot = resolveRepoRoot(filePath, sessionDataRecord, projectMapping);
|
|
6799
7082
|
if (!Array.isArray(sessionDataRecord.messages)) {
|
|
@@ -6851,7 +7134,7 @@ var GeminiSourceAdapter = class {
|
|
|
6851
7134
|
|
|
6852
7135
|
// src/sources/goose/goose-db-path-resolver.ts
|
|
6853
7136
|
import os14 from "os";
|
|
6854
|
-
import
|
|
7137
|
+
import path23 from "path";
|
|
6855
7138
|
function deduplicate(paths) {
|
|
6856
7139
|
return [...new Set(paths)];
|
|
6857
7140
|
}
|
|
@@ -6863,28 +7146,28 @@ function normalizeEnvPath3(value) {
|
|
|
6863
7146
|
return normalized || void 0;
|
|
6864
7147
|
}
|
|
6865
7148
|
function getGooseDbPath(rootDir) {
|
|
6866
|
-
return
|
|
7149
|
+
return path23.join(rootDir, "goose", "sessions", "sessions.db");
|
|
6867
7150
|
}
|
|
6868
7151
|
function getLinuxLikeCandidates(homeDir, env) {
|
|
6869
|
-
const xdgDataHome = normalizeEnvPath3(env.XDG_DATA_HOME) ??
|
|
7152
|
+
const xdgDataHome = normalizeEnvPath3(env.XDG_DATA_HOME) ?? path23.join(homeDir, ".local", "share");
|
|
6870
7153
|
return [
|
|
6871
7154
|
getGooseDbPath(xdgDataHome),
|
|
6872
|
-
|
|
7155
|
+
path23.join(homeDir, ".local", "share", "Block", "goose", "sessions", "sessions.db")
|
|
6873
7156
|
];
|
|
6874
7157
|
}
|
|
6875
7158
|
function getMacOsCandidates(homeDir) {
|
|
6876
7159
|
return [
|
|
6877
|
-
|
|
6878
|
-
|
|
7160
|
+
path23.join(homeDir, "Library", "Application Support", "goose", "sessions", "sessions.db"),
|
|
7161
|
+
path23.join(homeDir, ".local", "share", "Block", "goose", "sessions", "sessions.db")
|
|
6879
7162
|
];
|
|
6880
7163
|
}
|
|
6881
7164
|
function getWindowsCandidates(homeDir, env) {
|
|
6882
7165
|
const userProfile = normalizeEnvPath3(env.USERPROFILE);
|
|
6883
|
-
const roamingBase = normalizeEnvPath3(env.APPDATA) ?? normalizeEnvPath3(env.LOCALAPPDATA) ?? (userProfile ?
|
|
7166
|
+
const roamingBase = normalizeEnvPath3(env.APPDATA) ?? normalizeEnvPath3(env.LOCALAPPDATA) ?? (userProfile ? path23.join(userProfile, "AppData", "Roaming") : void 0);
|
|
6884
7167
|
const roamingCandidates = roamingBase ? [getGooseDbPath(roamingBase)] : [];
|
|
6885
7168
|
return [
|
|
6886
7169
|
...roamingCandidates,
|
|
6887
|
-
|
|
7170
|
+
path23.join(homeDir, ".local", "share", "Block", "goose", "sessions", "sessions.db")
|
|
6888
7171
|
];
|
|
6889
7172
|
}
|
|
6890
7173
|
function getDefaultGooseDbPathCandidates(options = {}) {
|
|
@@ -6892,7 +7175,7 @@ function getDefaultGooseDbPathCandidates(options = {}) {
|
|
|
6892
7175
|
const homeDir = options.homeDir ?? os14.homedir();
|
|
6893
7176
|
const env = options.env ?? process.env;
|
|
6894
7177
|
const goosePathRoot = normalizeEnvPath3(env.GOOSE_PATH_ROOT);
|
|
6895
|
-
const envCandidates = goosePathRoot ? [
|
|
7178
|
+
const envCandidates = goosePathRoot ? [path23.join(goosePathRoot, "data", "sessions", "sessions.db")] : [];
|
|
6896
7179
|
switch (platform) {
|
|
6897
7180
|
case "win32":
|
|
6898
7181
|
return deduplicate([...envCandidates, ...getWindowsCandidates(homeDir, env)]);
|
|
@@ -7094,9 +7377,9 @@ var GooseSourceAdapter = class {
|
|
|
7094
7377
|
// src/sources/kimi/kimi-source-adapter.ts
|
|
7095
7378
|
import { readFile as readFile6 } from "fs/promises";
|
|
7096
7379
|
import os15 from "os";
|
|
7097
|
-
import
|
|
7098
|
-
var defaultKimiCliSessionsDir =
|
|
7099
|
-
var defaultKimiCodeSessionsDir =
|
|
7380
|
+
import path24 from "path";
|
|
7381
|
+
var defaultKimiCliSessionsDir = path24.join(os15.homedir(), ".kimi", "sessions");
|
|
7382
|
+
var defaultKimiCodeSessionsDir = path24.join(os15.homedir(), ".kimi-code", "sessions");
|
|
7100
7383
|
var defaultRootDirs = [defaultKimiCliSessionsDir, defaultKimiCodeSessionsDir];
|
|
7101
7384
|
var KIMI_STATUS_UPDATE_LINE_TEXT = '"StatusUpdate"';
|
|
7102
7385
|
var KIMI_USAGE_RECORD_LINE_TEXT = '"usage.record"';
|
|
@@ -7143,14 +7426,14 @@ function incrementContextSkippedReason3(context, reason) {
|
|
|
7143
7426
|
incrementSkippedReason(context.skippedRowReasons, reason);
|
|
7144
7427
|
}
|
|
7145
7428
|
function getCliSessionId(filePath) {
|
|
7146
|
-
return
|
|
7429
|
+
return path24.basename(path24.dirname(filePath));
|
|
7147
7430
|
}
|
|
7148
7431
|
function getCliConfigPath(filePath) {
|
|
7149
|
-
const sessionsDir =
|
|
7150
|
-
return
|
|
7432
|
+
const sessionsDir = path24.dirname(path24.dirname(path24.dirname(filePath)));
|
|
7433
|
+
return path24.join(path24.dirname(sessionsDir), "config.json");
|
|
7151
7434
|
}
|
|
7152
7435
|
function isCodeWireFile(filePath) {
|
|
7153
|
-
return
|
|
7436
|
+
return path24.basename(path24.dirname(path24.dirname(filePath))) === "agents";
|
|
7154
7437
|
}
|
|
7155
7438
|
async function readCliConfigModel(filePath) {
|
|
7156
7439
|
try {
|
|
@@ -7164,10 +7447,10 @@ function getCliFallbackModel(timestamp) {
|
|
|
7164
7447
|
return Date.parse(timestamp) < KIMI_K2_6_CUTOFF_MS ? KIMI_K2_5_MODEL : KIMI_K2_6_MODEL;
|
|
7165
7448
|
}
|
|
7166
7449
|
function getCodeSessionId(filePath) {
|
|
7167
|
-
const agentDir =
|
|
7168
|
-
const agentsDir =
|
|
7169
|
-
const sessionDir =
|
|
7170
|
-
return
|
|
7450
|
+
const agentDir = path24.dirname(filePath);
|
|
7451
|
+
const agentsDir = path24.dirname(agentDir);
|
|
7452
|
+
const sessionDir = path24.dirname(agentsDir);
|
|
7453
|
+
return path24.basename(sessionDir);
|
|
7171
7454
|
}
|
|
7172
7455
|
function normalizeKimiCodeModel(model) {
|
|
7173
7456
|
const normalizedModel = asTrimmedText(model);
|
|
@@ -7268,7 +7551,7 @@ function parseUsageRecordLine(context, filePath, line) {
|
|
|
7268
7551
|
}
|
|
7269
7552
|
async function discoverWireFiles(rootDir) {
|
|
7270
7553
|
const jsonlFiles = await discoverFiles(rootDir, { extension: ".jsonl" });
|
|
7271
|
-
return jsonlFiles.filter((filePath) =>
|
|
7554
|
+
return jsonlFiles.filter((filePath) => path24.basename(filePath) === "wire.jsonl");
|
|
7272
7555
|
}
|
|
7273
7556
|
var KimiSourceAdapter = class {
|
|
7274
7557
|
id = "kimi";
|
|
@@ -7334,12 +7617,13 @@ var KimiSourceAdapter = class {
|
|
|
7334
7617
|
};
|
|
7335
7618
|
|
|
7336
7619
|
// src/sources/opencode/opencode-source-adapter.ts
|
|
7337
|
-
import
|
|
7620
|
+
import path26 from "path";
|
|
7621
|
+
import { setTimeout as sleep2 } from "timers/promises";
|
|
7338
7622
|
|
|
7339
7623
|
// src/sources/opencode/opencode-db-path-resolver.ts
|
|
7340
7624
|
import { readdirSync } from "fs";
|
|
7341
7625
|
import os16 from "os";
|
|
7342
|
-
import
|
|
7626
|
+
import path25 from "path";
|
|
7343
7627
|
var CHANNEL_DB_FILE_PATTERN = /^opencode-.+\.db$/u;
|
|
7344
7628
|
function deduplicate2(paths) {
|
|
7345
7629
|
return [...new Set(paths)];
|
|
@@ -7362,30 +7646,30 @@ function listChannelDbFileNames(directory) {
|
|
|
7362
7646
|
}
|
|
7363
7647
|
function getDirectoryCandidates(directory) {
|
|
7364
7648
|
return [
|
|
7365
|
-
|
|
7366
|
-
...listChannelDbFileNames(directory).map((fileName) =>
|
|
7367
|
-
|
|
7649
|
+
path25.join(directory, "opencode.db"),
|
|
7650
|
+
...listChannelDbFileNames(directory).map((fileName) => path25.join(directory, fileName)),
|
|
7651
|
+
path25.join(directory, "db.sqlite")
|
|
7368
7652
|
];
|
|
7369
7653
|
}
|
|
7370
7654
|
function getLinuxLikeCandidates2(homeDir, env) {
|
|
7371
|
-
const xdgDataHome = normalizeEnvPath4(env.XDG_DATA_HOME) ??
|
|
7655
|
+
const xdgDataHome = normalizeEnvPath4(env.XDG_DATA_HOME) ?? path25.join(homeDir, ".local", "share");
|
|
7372
7656
|
return [
|
|
7373
|
-
...getDirectoryCandidates(
|
|
7374
|
-
...getDirectoryCandidates(
|
|
7657
|
+
...getDirectoryCandidates(path25.join(xdgDataHome, "opencode")),
|
|
7658
|
+
...getDirectoryCandidates(path25.join(homeDir, ".opencode"))
|
|
7375
7659
|
];
|
|
7376
7660
|
}
|
|
7377
7661
|
function getMacOsCandidates2(homeDir) {
|
|
7378
|
-
const appSupportDir =
|
|
7662
|
+
const appSupportDir = path25.join(homeDir, "Library", "Application Support");
|
|
7379
7663
|
return [
|
|
7380
|
-
...getDirectoryCandidates(
|
|
7381
|
-
...getDirectoryCandidates(
|
|
7664
|
+
...getDirectoryCandidates(path25.join(appSupportDir, "opencode")),
|
|
7665
|
+
...getDirectoryCandidates(path25.join(homeDir, ".opencode"))
|
|
7382
7666
|
];
|
|
7383
7667
|
}
|
|
7384
7668
|
function getWindowsCandidates2(homeDir, env) {
|
|
7385
7669
|
const userProfile = normalizeEnvPath4(env.USERPROFILE);
|
|
7386
|
-
const roamingBase = normalizeEnvPath4(env.APPDATA) ?? normalizeEnvPath4(env.LOCALAPPDATA) ?? (userProfile ?
|
|
7387
|
-
const roamingCandidates = roamingBase ? getDirectoryCandidates(
|
|
7388
|
-
return [...roamingCandidates, ...getDirectoryCandidates(
|
|
7670
|
+
const roamingBase = normalizeEnvPath4(env.APPDATA) ?? normalizeEnvPath4(env.LOCALAPPDATA) ?? (userProfile ? path25.join(userProfile, "AppData", "Roaming") : void 0);
|
|
7671
|
+
const roamingCandidates = roamingBase ? getDirectoryCandidates(path25.join(roamingBase, "opencode")) : [];
|
|
7672
|
+
return [...roamingCandidates, ...getDirectoryCandidates(path25.join(homeDir, ".opencode"))];
|
|
7389
7673
|
}
|
|
7390
7674
|
function getDefaultOpenCodeDbPathCandidates(options = {}) {
|
|
7391
7675
|
const platform = options.platform ?? process.platform;
|
|
@@ -7724,14 +8008,6 @@ function queryOpenCodeMessageRows(database) {
|
|
|
7724
8008
|
// src/sources/opencode/opencode-source-adapter.ts
|
|
7725
8009
|
var DEFAULT_BUSY_RETRY_COUNT = 2;
|
|
7726
8010
|
var DEFAULT_BUSY_RETRY_DELAY_MS = 50;
|
|
7727
|
-
function isBlankText2(value) {
|
|
7728
|
-
return value.trim().length === 0;
|
|
7729
|
-
}
|
|
7730
|
-
async function sleep2(delayMs) {
|
|
7731
|
-
await new Promise((resolve) => {
|
|
7732
|
-
setTimeout(resolve, delayMs);
|
|
7733
|
-
});
|
|
7734
|
-
}
|
|
7735
8011
|
function getOpenCodeParseDependencies(dbPath) {
|
|
7736
8012
|
return [`${dbPath}-wal`, `${dbPath}-journal`];
|
|
7737
8013
|
}
|
|
@@ -7763,7 +8039,7 @@ var OpenCodeSourceAdapter = class {
|
|
|
7763
8039
|
}
|
|
7764
8040
|
async discoverFiles() {
|
|
7765
8041
|
if (this.explicitDbPath !== void 0) {
|
|
7766
|
-
if (
|
|
8042
|
+
if (isBlankText(this.explicitDbPath)) {
|
|
7767
8043
|
throw new Error("--opencode-db must be a non-empty path");
|
|
7768
8044
|
}
|
|
7769
8045
|
const explicitDbPath = this.explicitDbPath.trim();
|
|
@@ -7780,7 +8056,7 @@ var OpenCodeSourceAdapter = class {
|
|
|
7780
8056
|
let selectedDirectory;
|
|
7781
8057
|
const selectedPaths = [];
|
|
7782
8058
|
for (const candidatePath of this.resolveDefaultDbPaths()) {
|
|
7783
|
-
const directory =
|
|
8059
|
+
const directory = path26.dirname(candidatePath);
|
|
7784
8060
|
if (selectedDirectory !== void 0 && directory !== selectedDirectory) {
|
|
7785
8061
|
break;
|
|
7786
8062
|
}
|
|
@@ -7798,7 +8074,7 @@ var OpenCodeSourceAdapter = class {
|
|
|
7798
8074
|
}
|
|
7799
8075
|
if (selectedPaths.length > 0) {
|
|
7800
8076
|
const channelDbPaths = selectedPaths.filter(
|
|
7801
|
-
(candidatePath) =>
|
|
8077
|
+
(candidatePath) => path26.basename(candidatePath) !== "db.sqlite"
|
|
7802
8078
|
);
|
|
7803
8079
|
return channelDbPaths.length > 0 ? channelDbPaths : selectedPaths;
|
|
7804
8080
|
}
|
|
@@ -7812,13 +8088,13 @@ var OpenCodeSourceAdapter = class {
|
|
|
7812
8088
|
return parseDiagnostics.events;
|
|
7813
8089
|
}
|
|
7814
8090
|
async getParseDependencies(dbPath) {
|
|
7815
|
-
if (
|
|
8091
|
+
if (isBlankText(dbPath)) {
|
|
7816
8092
|
return [];
|
|
7817
8093
|
}
|
|
7818
8094
|
return getOpenCodeParseDependencies(dbPath.trim());
|
|
7819
8095
|
}
|
|
7820
8096
|
async parseFileWithDiagnostics(dbPath) {
|
|
7821
|
-
if (
|
|
8097
|
+
if (isBlankText(dbPath)) {
|
|
7822
8098
|
throw new Error("OpenCode DB path must be a non-empty path");
|
|
7823
8099
|
}
|
|
7824
8100
|
const normalizedDbPath = dbPath.trim();
|
|
@@ -7850,10 +8126,10 @@ var OpenCodeSourceAdapter = class {
|
|
|
7850
8126
|
|
|
7851
8127
|
// src/sources/openclaw/openclaw-source-adapter.ts
|
|
7852
8128
|
import os17 from "os";
|
|
7853
|
-
import
|
|
8129
|
+
import path28 from "path";
|
|
7854
8130
|
|
|
7855
8131
|
// src/sources/openclaw/openclaw-session-parser.ts
|
|
7856
|
-
import
|
|
8132
|
+
import path27 from "path";
|
|
7857
8133
|
var SESSION_LINE_PATTERN = /"type"\s*:\s*"session"/u;
|
|
7858
8134
|
var MESSAGE_LINE_PATTERN = /"type"\s*:\s*"message"/u;
|
|
7859
8135
|
var MODEL_CHANGE_LINE_PATTERN = /"type"\s*:\s*"model_change"/u;
|
|
@@ -7864,7 +8140,7 @@ function shouldParseOpenClawJsonlLine(lineText) {
|
|
|
7864
8140
|
return SESSION_LINE_PATTERN.test(lineText) || MESSAGE_LINE_PATTERN.test(lineText) || MODEL_CHANGE_LINE_PATTERN.test(lineText) || USAGE_LINE_PATTERN.test(lineText) || MODEL_LINE_PATTERN.test(lineText) || PROVIDER_LINE_PATTERN.test(lineText);
|
|
7865
8141
|
}
|
|
7866
8142
|
function getFallbackSessionId5(filePath) {
|
|
7867
|
-
return
|
|
8143
|
+
return path27.basename(filePath, ".jsonl");
|
|
7868
8144
|
}
|
|
7869
8145
|
function resolveRepoRootFromRecord(record) {
|
|
7870
8146
|
if (!record) {
|
|
@@ -8054,12 +8330,12 @@ async function parseOpenClawSessionFile(sourceId, filePath) {
|
|
|
8054
8330
|
}
|
|
8055
8331
|
|
|
8056
8332
|
// src/sources/openclaw/openclaw-source-adapter.ts
|
|
8057
|
-
var defaultAgentsDir =
|
|
8333
|
+
var defaultAgentsDir = path28.join(os17.homedir(), ".openclaw", "agents");
|
|
8058
8334
|
var defaultOpenClawRootDirs = [
|
|
8059
8335
|
defaultAgentsDir,
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8336
|
+
path28.join(os17.homedir(), ".clawdbot", "agents"),
|
|
8337
|
+
path28.join(os17.homedir(), ".moltbot", "agents"),
|
|
8338
|
+
path28.join(os17.homedir(), ".moldbot", "agents")
|
|
8063
8339
|
];
|
|
8064
8340
|
var OpenClawSourceAdapter = class {
|
|
8065
8341
|
id = "openclaw";
|
|
@@ -8095,7 +8371,7 @@ var OpenClawSourceAdapter = class {
|
|
|
8095
8371
|
|
|
8096
8372
|
// src/sources/pi/pi-source-adapter.ts
|
|
8097
8373
|
import os18 from "os";
|
|
8098
|
-
import
|
|
8374
|
+
import path29 from "path";
|
|
8099
8375
|
|
|
8100
8376
|
// src/utils/read-first-line.ts
|
|
8101
8377
|
import { open as open2 } from "fs/promises";
|
|
@@ -8120,10 +8396,10 @@ async function readFirstLine(filePath, maxBytes = DEFAULT_MAX_FIRST_LINE_BYTES)
|
|
|
8120
8396
|
}
|
|
8121
8397
|
|
|
8122
8398
|
// src/sources/pi/pi-source-adapter.ts
|
|
8123
|
-
var defaultSessionsDir2 =
|
|
8399
|
+
var defaultSessionsDir2 = path29.join(os18.homedir(), ".pi", "agent", "sessions");
|
|
8124
8400
|
var defaultPiRootDirs = [
|
|
8125
8401
|
defaultSessionsDir2,
|
|
8126
|
-
|
|
8402
|
+
path29.join(os18.homedir(), ".omp", "agent", "sessions")
|
|
8127
8403
|
];
|
|
8128
8404
|
var PI_MESSAGE_LINE_PATTERN = /"type"\s*:\s*"message"/u;
|
|
8129
8405
|
var PI_SESSION_LINE_PATTERN = /"type"\s*:\s*"session"/u;
|
|
@@ -8143,7 +8419,7 @@ function resolveTimestamp6(line, message, state) {
|
|
|
8143
8419
|
}
|
|
8144
8420
|
function resolveParentSessionPath(sessionLine, filePath) {
|
|
8145
8421
|
const parentSession = asTrimmedText(sessionLine.parentSession);
|
|
8146
|
-
return parentSession ?
|
|
8422
|
+
return parentSession ? path29.resolve(path29.dirname(filePath), parentSession) : void 0;
|
|
8147
8423
|
}
|
|
8148
8424
|
async function readParentSessionPath(filePath) {
|
|
8149
8425
|
const firstLine = await readFirstLine(filePath);
|
|
@@ -8203,7 +8479,7 @@ function extractUsage4(line, message) {
|
|
|
8203
8479
|
return extractUsageFromRecord2(messageUsage);
|
|
8204
8480
|
}
|
|
8205
8481
|
function getFallbackSessionId6(filePath) {
|
|
8206
|
-
return
|
|
8482
|
+
return path29.basename(filePath, ".jsonl");
|
|
8207
8483
|
}
|
|
8208
8484
|
function resolveRepoRootFromRecord2(record) {
|
|
8209
8485
|
if (!record) {
|
|
@@ -8316,8 +8592,8 @@ var PiSourceAdapter = class {
|
|
|
8316
8592
|
|
|
8317
8593
|
// src/sources/qwen/qwen-source-adapter.ts
|
|
8318
8594
|
import os19 from "os";
|
|
8319
|
-
import
|
|
8320
|
-
var defaultProjectsDir =
|
|
8595
|
+
import path30 from "path";
|
|
8596
|
+
var defaultProjectsDir = path30.join(os19.homedir(), ".qwen", "projects");
|
|
8321
8597
|
var QWEN_USAGE_LINE_TEXT = '"usageMetadata"';
|
|
8322
8598
|
function shouldParseQwenJsonlLine(lineText) {
|
|
8323
8599
|
return lineText.includes(QWEN_USAGE_LINE_TEXT);
|
|
@@ -8365,10 +8641,10 @@ function extractTokenUsage2(usageMetadata, model) {
|
|
|
8365
8641
|
};
|
|
8366
8642
|
}
|
|
8367
8643
|
function getFallbackSessionId7(filePath) {
|
|
8368
|
-
const chatsDir =
|
|
8369
|
-
const projectDir =
|
|
8370
|
-
const projectName =
|
|
8371
|
-
const fileStem =
|
|
8644
|
+
const chatsDir = path30.dirname(filePath);
|
|
8645
|
+
const projectDir = path30.dirname(chatsDir);
|
|
8646
|
+
const projectName = path30.basename(projectDir);
|
|
8647
|
+
const fileStem = path30.basename(filePath, ".jsonl");
|
|
8372
8648
|
return `${projectName}-${fileStem}`;
|
|
8373
8649
|
}
|
|
8374
8650
|
var QwenSourceAdapter = class {
|
|
@@ -8453,7 +8729,7 @@ var QwenSourceAdapter = class {
|
|
|
8453
8729
|
};
|
|
8454
8730
|
|
|
8455
8731
|
// src/sources/multi-directory-source-adapter.ts
|
|
8456
|
-
import
|
|
8732
|
+
import path31 from "path";
|
|
8457
8733
|
var MultiDirectorySourceAdapter = class {
|
|
8458
8734
|
id;
|
|
8459
8735
|
parserVersion;
|
|
@@ -8480,7 +8756,7 @@ var MultiDirectorySourceAdapter = class {
|
|
|
8480
8756
|
const adapterByFile = /* @__PURE__ */ new Map();
|
|
8481
8757
|
for (const adapter of this.adapters) {
|
|
8482
8758
|
for (const filePath of await adapter.discoverFiles()) {
|
|
8483
|
-
const resolvedPath =
|
|
8759
|
+
const resolvedPath = path31.resolve(filePath);
|
|
8484
8760
|
if (!discoveredPaths.has(resolvedPath)) {
|
|
8485
8761
|
discoveredPaths.add(resolvedPath);
|
|
8486
8762
|
adapterByFile.set(filePath, adapter);
|
|
@@ -8510,8 +8786,8 @@ var MultiDirectorySourceAdapter = class {
|
|
|
8510
8786
|
};
|
|
8511
8787
|
|
|
8512
8788
|
// src/sources/create-default-adapters.ts
|
|
8513
|
-
var dirOptions = ({ path:
|
|
8514
|
-
dir:
|
|
8789
|
+
var dirOptions = ({ path: path41, requireExistingPath }) => ({
|
|
8790
|
+
dir: path41,
|
|
8515
8791
|
requireDir: requireExistingPath
|
|
8516
8792
|
});
|
|
8517
8793
|
var sourceRegistrations = [
|
|
@@ -8560,7 +8836,7 @@ var sourceRegistrations = [
|
|
|
8560
8836
|
flag: "--opencode-db <path>",
|
|
8561
8837
|
help: "Path to OpenCode SQLite DB"
|
|
8562
8838
|
},
|
|
8563
|
-
create: ({ path:
|
|
8839
|
+
create: ({ path: path41 }) => new OpenCodeSourceAdapter({ dbPath: path41 })
|
|
8564
8840
|
},
|
|
8565
8841
|
{
|
|
8566
8842
|
id: "openclaw",
|
|
@@ -8602,7 +8878,7 @@ var sourceRegistrations = [
|
|
|
8602
8878
|
format: "sqlite",
|
|
8603
8879
|
supportsSourceDir: false,
|
|
8604
8880
|
option: { key: "gooseDb", flag: "--goose-db <path>", help: "Path to Goose SQLite DB" },
|
|
8605
|
-
create: ({ path:
|
|
8881
|
+
create: ({ path: path41 }) => new GooseSourceAdapter({ dbPath: path41 })
|
|
8606
8882
|
},
|
|
8607
8883
|
{
|
|
8608
8884
|
id: "amp",
|
|
@@ -8843,6 +9119,16 @@ function normalizeSourceFilter(source) {
|
|
|
8843
9119
|
}
|
|
8844
9120
|
return new Set(normalizedSources);
|
|
8845
9121
|
}
|
|
9122
|
+
function normalizeMachineFilter(machine) {
|
|
9123
|
+
if (!machine || Array.isArray(machine) && machine.length === 0) {
|
|
9124
|
+
return void 0;
|
|
9125
|
+
}
|
|
9126
|
+
const names = (Array.isArray(machine) ? machine : [machine]).flatMap((candidate) => candidate.split(",")).map((candidate) => candidate.trim().toLowerCase()).filter((candidate) => candidate.length > 0);
|
|
9127
|
+
if (names.length === 0) {
|
|
9128
|
+
throw new Error("--machine must contain at least one machine name");
|
|
9129
|
+
}
|
|
9130
|
+
return new Set(names);
|
|
9131
|
+
}
|
|
8846
9132
|
function normalizeModelFilter(model) {
|
|
8847
9133
|
if (!model || Array.isArray(model) && model.length === 0) {
|
|
8848
9134
|
return void 0;
|
|
@@ -8947,6 +9233,7 @@ function normalizeBuildUsageInputs(options, cliOptions = options) {
|
|
|
8947
9233
|
sourceFilter,
|
|
8948
9234
|
sourceFilterLabel: cliOptions.source === void 0 && options.source !== void 0 ? "config `sources`" : "--source",
|
|
8949
9235
|
modelFilter,
|
|
9236
|
+
machineFilter: normalizeMachineFilter(options.machine),
|
|
8950
9237
|
explicitSourceIds,
|
|
8951
9238
|
pricingUrl: normalizedPricingUrl
|
|
8952
9239
|
};
|
|
@@ -9011,6 +9298,14 @@ function throwOnExplicitSourceScopeConflicts(adapters, selectedAdapters, options
|
|
|
9011
9298
|
`Explicitly requested source(s) are incompatible with the requested ${describeExplicitScopeConstraint(options)} scope: ${incompatibleExplicitSources.join(", ")}.`
|
|
9012
9299
|
);
|
|
9013
9300
|
}
|
|
9301
|
+
function selectAdaptersBySourceFilter(adapters, source) {
|
|
9302
|
+
const sourceFilter = normalizeSourceFilter(source);
|
|
9303
|
+
validateSourceFilterValues(sourceFilter, new Set(getDefaultSourceIds()));
|
|
9304
|
+
if (!sourceFilter) {
|
|
9305
|
+
return adapters;
|
|
9306
|
+
}
|
|
9307
|
+
return adapters.filter((adapter) => sourceFilter.has(adapter.id.toLowerCase()));
|
|
9308
|
+
}
|
|
9014
9309
|
|
|
9015
9310
|
// src/cli/build-usage-data-diagnostics.ts
|
|
9016
9311
|
function buildUsageDiagnostics(params) {
|
|
@@ -9093,14 +9388,8 @@ function formatEnvVarOverrides(overrides) {
|
|
|
9093
9388
|
import { stat as stat4 } from "fs/promises";
|
|
9094
9389
|
|
|
9095
9390
|
// src/persistence/event-store-history.ts
|
|
9096
|
-
function toNonNegativeInteger4(value) {
|
|
9097
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
9098
|
-
return void 0;
|
|
9099
|
-
}
|
|
9100
|
-
return Math.trunc(value);
|
|
9101
|
-
}
|
|
9102
9391
|
function toPositiveCount(value) {
|
|
9103
|
-
return
|
|
9392
|
+
return toNonNegativeInteger(value) ?? 0;
|
|
9104
9393
|
}
|
|
9105
9394
|
function normalizeHistorySource(source) {
|
|
9106
9395
|
return normalizeSourceId(source)?.toLowerCase();
|
|
@@ -9262,8 +9551,8 @@ function readDepartedFiles(store) {
|
|
|
9262
9551
|
for (const row of rows) {
|
|
9263
9552
|
const source = toText(row.source);
|
|
9264
9553
|
const filePath = toText(row.file_path);
|
|
9265
|
-
const ingestedAt =
|
|
9266
|
-
const eventCount =
|
|
9554
|
+
const ingestedAt = toNonNegativeInteger(row.ingested_at);
|
|
9555
|
+
const eventCount = toNonNegativeInteger(row.event_count);
|
|
9267
9556
|
const newestTimestamp = toText(row.newest_timestamp);
|
|
9268
9557
|
if (!source || !filePath || ingestedAt === void 0 || eventCount === void 0) {
|
|
9269
9558
|
continue;
|
|
@@ -9413,6 +9702,7 @@ function loadHistoryEvents(store, input) {
|
|
|
9413
9702
|
const suppressedFileCount = departedFiles.length - servedFiles.length;
|
|
9414
9703
|
return {
|
|
9415
9704
|
events,
|
|
9705
|
+
servedFiles: servedFiles.map(({ source, filePath }) => ({ source, filePath })),
|
|
9416
9706
|
departedFileCount: departedFiles.length,
|
|
9417
9707
|
servedFileCount: servedFiles.length,
|
|
9418
9708
|
suppressedFileCount,
|
|
@@ -9422,15 +9712,12 @@ function loadHistoryEvents(store, input) {
|
|
|
9422
9712
|
|
|
9423
9713
|
// src/cli/history-live-files.ts
|
|
9424
9714
|
var MAX_CONCURRENT_STATS = 32;
|
|
9425
|
-
function isMissingPathError(error) {
|
|
9426
|
-
return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
9427
|
-
}
|
|
9428
9715
|
async function checkDiskPresence(filePath, statFile) {
|
|
9429
9716
|
try {
|
|
9430
9717
|
await statFile(filePath);
|
|
9431
9718
|
return "present";
|
|
9432
9719
|
} catch (error) {
|
|
9433
|
-
return
|
|
9720
|
+
return hasErrorCode(error, "ENOENT", "ENOTDIR") ? "missing" : "unknown";
|
|
9434
9721
|
}
|
|
9435
9722
|
}
|
|
9436
9723
|
async function mapWithConcurrency(inputs, concurrency, mapInput) {
|
|
@@ -9564,9 +9851,6 @@ function createParseBudget(maxParallelFileParsing) {
|
|
|
9564
9851
|
|
|
9565
9852
|
// src/cli/parse/parse-fingerprint.ts
|
|
9566
9853
|
import { stat as stat5 } from "fs/promises";
|
|
9567
|
-
function isMissingPathError2(error) {
|
|
9568
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
9569
|
-
}
|
|
9570
9854
|
async function createParseDependencyFingerprint(filePath, options) {
|
|
9571
9855
|
try {
|
|
9572
9856
|
const fileStat = await stat5(filePath);
|
|
@@ -9577,7 +9861,7 @@ async function createParseDependencyFingerprint(filePath, options) {
|
|
|
9577
9861
|
mtimeMs: fileStat.mtimeMs
|
|
9578
9862
|
};
|
|
9579
9863
|
} catch (error) {
|
|
9580
|
-
if (options.allowMissing &&
|
|
9864
|
+
if (options.allowMissing && hasErrorCode(error, "ENOENT")) {
|
|
9581
9865
|
return {
|
|
9582
9866
|
path: filePath,
|
|
9583
9867
|
exists: false
|
|
@@ -9928,6 +10212,7 @@ async function parseAdapterEvents(adapter, maxParallelFileParsing, runWithParseB
|
|
|
9928
10212
|
source: adapter.id,
|
|
9929
10213
|
events: [],
|
|
9930
10214
|
filePaths: [],
|
|
10215
|
+
parsedFilePaths: [],
|
|
9931
10216
|
filesFound: 0,
|
|
9932
10217
|
skippedRows: 0,
|
|
9933
10218
|
skippedRowReasons: []
|
|
@@ -9936,6 +10221,7 @@ async function parseAdapterEvents(adapter, maxParallelFileParsing, runWithParseB
|
|
|
9936
10221
|
const safeMaxParallelFileParsing = Number.isFinite(maxParallelFileParsing) && maxParallelFileParsing > 0 ? Math.max(1, Math.floor(maxParallelFileParsing)) : 1;
|
|
9937
10222
|
const parsedByFile = Array.from({ length: files.length }, () => []);
|
|
9938
10223
|
const skippedRowsByFile = Array.from({ length: files.length }, () => 0);
|
|
10224
|
+
const parsedFileIndexes = /* @__PURE__ */ new Set();
|
|
9939
10225
|
const skippedRowReasons = /* @__PURE__ */ new Map();
|
|
9940
10226
|
let pendingStoreWrites = [];
|
|
9941
10227
|
let pendingStoreEventCount = 0;
|
|
@@ -9959,6 +10245,7 @@ async function parseAdapterEvents(adapter, maxParallelFileParsing, runWithParseB
|
|
|
9959
10245
|
params.diagnostics.skippedRowReasons
|
|
9960
10246
|
);
|
|
9961
10247
|
parsedByFile[params.fileIndex] = params.diagnostics.events;
|
|
10248
|
+
parsedFileIndexes.add(params.fileIndex);
|
|
9962
10249
|
skippedRowsByFile[params.fileIndex] = skippedRows;
|
|
9963
10250
|
for (const reasonStat of normalizedSkippedRowReasons) {
|
|
9964
10251
|
skippedRowReasons.set(
|
|
@@ -10183,6 +10470,7 @@ async function parseAdapterEvents(adapter, maxParallelFileParsing, runWithParseB
|
|
|
10183
10470
|
source: adapter.id,
|
|
10184
10471
|
events: parsedByFile.flat(),
|
|
10185
10472
|
filePaths: files,
|
|
10473
|
+
parsedFilePaths: files.filter((_, fileIndex) => parsedFileIndexes.has(fileIndex)),
|
|
10186
10474
|
filesFound: files.length,
|
|
10187
10475
|
skippedRows: skippedRowsByFile.reduce((sum, skippedRowsCount) => sum + skippedRowsCount, 0),
|
|
10188
10476
|
skippedRowReasons: [...skippedRowReasons.entries()].map(([reason, count]) => ({ reason, count })).sort((left, right4) => compareByCodePoint(left.reason, right4.reason))
|
|
@@ -10254,17 +10542,14 @@ async function parseSelectedAdapters(adaptersToParse, maxParallelFileParsing, op
|
|
|
10254
10542
|
const sourceFailures = [];
|
|
10255
10543
|
const successfulParseResults = [];
|
|
10256
10544
|
const discoveredFiles = [];
|
|
10545
|
+
const parsedFiles = [];
|
|
10257
10546
|
for (const [index, parseResult] of parseResults.entries()) {
|
|
10258
10547
|
const source = adaptersToParse[index].id;
|
|
10259
10548
|
if (parseResult.status === "fulfilled") {
|
|
10260
|
-
const { filePaths, ...parseResultWithoutFiles } = parseResult.value;
|
|
10549
|
+
const { filePaths, parsedFilePaths, ...parseResultWithoutFiles } = parseResult.value;
|
|
10261
10550
|
successfulParseResults.push(parseResultWithoutFiles);
|
|
10262
|
-
discoveredFiles.push(
|
|
10263
|
-
|
|
10264
|
-
source,
|
|
10265
|
-
filePath
|
|
10266
|
-
}))
|
|
10267
|
-
);
|
|
10551
|
+
discoveredFiles.push(...filePaths.map((filePath) => ({ source, filePath })));
|
|
10552
|
+
parsedFiles.push(...parsedFilePaths.map((filePath) => ({ source, filePath })));
|
|
10268
10553
|
continue;
|
|
10269
10554
|
}
|
|
10270
10555
|
sourceFailures.push({ source, reason: getErrorReason(parseResult.reason) });
|
|
@@ -10272,6 +10557,7 @@ async function parseSelectedAdapters(adaptersToParse, maxParallelFileParsing, op
|
|
|
10272
10557
|
return {
|
|
10273
10558
|
successfulParseResults,
|
|
10274
10559
|
discoveredFiles,
|
|
10560
|
+
parsedFiles,
|
|
10275
10561
|
eventStoreAvailable: Boolean(options.eventStore?.enabled) && !eventStoreFailureState.disabled,
|
|
10276
10562
|
sourceFailures,
|
|
10277
10563
|
warnings: eventStoreFailureState.warning ? [eventStoreFailureState.warning] : []
|
|
@@ -10362,6 +10648,949 @@ function filterParsedAdapterEvents(parseResults, options) {
|
|
|
10362
10648
|
return filterByModelRules(providerAndDateFilteredEvents, options.modelFilter);
|
|
10363
10649
|
}
|
|
10364
10650
|
|
|
10651
|
+
// src/machines/machine-cache.ts
|
|
10652
|
+
import { rm as rm3 } from "fs/promises";
|
|
10653
|
+
import path32 from "path";
|
|
10654
|
+
|
|
10655
|
+
// src/machines/machine-export-bundle.ts
|
|
10656
|
+
var MACHINE_EXPORT_FORMAT = "llm-usage-metrics.machine-export";
|
|
10657
|
+
var MACHINE_EXPORT_VERSION = 1;
|
|
10658
|
+
function isNonEmptyText(value) {
|
|
10659
|
+
return typeof value === "string" && value.length > 0;
|
|
10660
|
+
}
|
|
10661
|
+
function toFileKey(source, filePath) {
|
|
10662
|
+
return JSON.stringify([source, filePath]);
|
|
10663
|
+
}
|
|
10664
|
+
function parseKnownFiles(text2) {
|
|
10665
|
+
let parsed;
|
|
10666
|
+
try {
|
|
10667
|
+
parsed = JSON.parse(text2);
|
|
10668
|
+
} catch (error) {
|
|
10669
|
+
throw new Error("Known files must be JSON", { cause: error });
|
|
10670
|
+
}
|
|
10671
|
+
const record = asRecord(parsed);
|
|
10672
|
+
if (record?.version !== MACHINE_EXPORT_VERSION || !Array.isArray(record.files)) {
|
|
10673
|
+
throw new Error(
|
|
10674
|
+
`Known files must be {"version":${MACHINE_EXPORT_VERSION},"files":[[source, filePath, revision], ...]}`
|
|
10675
|
+
);
|
|
10676
|
+
}
|
|
10677
|
+
const knownFiles = /* @__PURE__ */ new Map();
|
|
10678
|
+
for (const entry of record.files) {
|
|
10679
|
+
const [source, filePath, revision] = Array.isArray(entry) ? entry : [];
|
|
10680
|
+
if (!Array.isArray(entry) || entry.length !== 3 || !isNonEmptyText(source) || !isNonEmptyText(filePath) || !isNonEmptyText(revision)) {
|
|
10681
|
+
throw new Error("Each known file must be [source, filePath, revision]");
|
|
10682
|
+
}
|
|
10683
|
+
knownFiles.set(toFileKey(source, filePath), revision);
|
|
10684
|
+
}
|
|
10685
|
+
return knownFiles;
|
|
10686
|
+
}
|
|
10687
|
+
function buildMachineExportLines(header, snapshots, knownFiles = /* @__PURE__ */ new Map()) {
|
|
10688
|
+
const lines = [
|
|
10689
|
+
{ type: "header", format: MACHINE_EXPORT_FORMAT, version: MACHINE_EXPORT_VERSION, ...header }
|
|
10690
|
+
];
|
|
10691
|
+
const files = [];
|
|
10692
|
+
let eventCount = 0;
|
|
10693
|
+
for (const snapshot of snapshots) {
|
|
10694
|
+
if (snapshot.events.length === 0) {
|
|
10695
|
+
continue;
|
|
10696
|
+
}
|
|
10697
|
+
files.push([snapshot.source, snapshot.filePath, snapshot.revision]);
|
|
10698
|
+
eventCount += snapshot.events.length;
|
|
10699
|
+
if (knownFiles.get(toFileKey(snapshot.source, snapshot.filePath)) === snapshot.revision) {
|
|
10700
|
+
continue;
|
|
10701
|
+
}
|
|
10702
|
+
lines.push({
|
|
10703
|
+
type: "file",
|
|
10704
|
+
source: snapshot.source,
|
|
10705
|
+
filePath: snapshot.filePath,
|
|
10706
|
+
revision: snapshot.revision,
|
|
10707
|
+
events: snapshot.events
|
|
10708
|
+
});
|
|
10709
|
+
}
|
|
10710
|
+
lines.push({ type: "end", files, eventCount });
|
|
10711
|
+
return lines;
|
|
10712
|
+
}
|
|
10713
|
+
function parseBundleLine(line, lineNumber) {
|
|
10714
|
+
let parsed;
|
|
10715
|
+
try {
|
|
10716
|
+
parsed = JSON.parse(line);
|
|
10717
|
+
} catch (error) {
|
|
10718
|
+
throw new Error(`line ${lineNumber} is not JSON`, { cause: error });
|
|
10719
|
+
}
|
|
10720
|
+
const record = asRecord(parsed);
|
|
10721
|
+
if (!record) {
|
|
10722
|
+
throw new Error(`line ${lineNumber} is not an object`);
|
|
10723
|
+
}
|
|
10724
|
+
return record;
|
|
10725
|
+
}
|
|
10726
|
+
function readHeader(record) {
|
|
10727
|
+
if (record.type !== "header" || record.format !== MACHINE_EXPORT_FORMAT) {
|
|
10728
|
+
throw new Error("the output is not a machine export (no header line)");
|
|
10729
|
+
}
|
|
10730
|
+
if (record.version !== MACHINE_EXPORT_VERSION) {
|
|
10731
|
+
throw new Error(
|
|
10732
|
+
`it exports bundle version ${String(record.version)}, but this llm-usage-metrics reads version ${MACHINE_EXPORT_VERSION}; install the same version on both machines`
|
|
10733
|
+
);
|
|
10734
|
+
}
|
|
10735
|
+
return {
|
|
10736
|
+
type: "header",
|
|
10737
|
+
format: MACHINE_EXPORT_FORMAT,
|
|
10738
|
+
version: MACHINE_EXPORT_VERSION,
|
|
10739
|
+
cliVersion: typeof record.cliVersion === "string" ? record.cliVersion : "",
|
|
10740
|
+
hostname: typeof record.hostname === "string" ? record.hostname : "",
|
|
10741
|
+
exportedAt: typeof record.exportedAt === "string" ? record.exportedAt : ""
|
|
10742
|
+
};
|
|
10743
|
+
}
|
|
10744
|
+
function optionalText(record, key) {
|
|
10745
|
+
const value = record[key];
|
|
10746
|
+
if (value !== void 0 && typeof value !== "string") {
|
|
10747
|
+
throw new Error(`${key} is not a string`);
|
|
10748
|
+
}
|
|
10749
|
+
return value;
|
|
10750
|
+
}
|
|
10751
|
+
function optionalCount(record, key) {
|
|
10752
|
+
const value = record[key];
|
|
10753
|
+
if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value) || value < 0)) {
|
|
10754
|
+
throw new Error(`${key} is not a non-negative number`);
|
|
10755
|
+
}
|
|
10756
|
+
return value;
|
|
10757
|
+
}
|
|
10758
|
+
function toUsageEventInput(value) {
|
|
10759
|
+
const record = asRecord(value);
|
|
10760
|
+
if (!record) {
|
|
10761
|
+
throw new Error("not an object");
|
|
10762
|
+
}
|
|
10763
|
+
const costMode = record.costMode;
|
|
10764
|
+
if (costMode !== "explicit" && costMode !== "estimated") {
|
|
10765
|
+
throw new Error("costMode is not explicit or estimated");
|
|
10766
|
+
}
|
|
10767
|
+
return {
|
|
10768
|
+
source: optionalText(record, "source") ?? "",
|
|
10769
|
+
sessionId: optionalText(record, "sessionId") ?? "",
|
|
10770
|
+
timestamp: optionalText(record, "timestamp") ?? "",
|
|
10771
|
+
repoRoot: optionalText(record, "repoRoot"),
|
|
10772
|
+
provider: optionalText(record, "provider"),
|
|
10773
|
+
model: optionalText(record, "model"),
|
|
10774
|
+
inputTokens: optionalCount(record, "inputTokens"),
|
|
10775
|
+
outputTokens: optionalCount(record, "outputTokens"),
|
|
10776
|
+
reasoningTokens: optionalCount(record, "reasoningTokens"),
|
|
10777
|
+
cacheReadTokens: optionalCount(record, "cacheReadTokens"),
|
|
10778
|
+
cacheWriteTokens: optionalCount(record, "cacheWriteTokens"),
|
|
10779
|
+
totalTokens: optionalCount(record, "totalTokens"),
|
|
10780
|
+
costUsd: optionalCount(record, "costUsd"),
|
|
10781
|
+
costMode
|
|
10782
|
+
};
|
|
10783
|
+
}
|
|
10784
|
+
function readFileLine(record, lineNumber) {
|
|
10785
|
+
const { source, filePath, revision, events } = record;
|
|
10786
|
+
if (!isNonEmptyText(source) || !isNonEmptyText(filePath) || !isNonEmptyText(revision) || !Array.isArray(events) || events.length === 0) {
|
|
10787
|
+
throw new Error(`line ${lineNumber} is not a valid file line`);
|
|
10788
|
+
}
|
|
10789
|
+
return {
|
|
10790
|
+
type: "file",
|
|
10791
|
+
source,
|
|
10792
|
+
filePath,
|
|
10793
|
+
revision,
|
|
10794
|
+
events: events.map((event, index) => {
|
|
10795
|
+
try {
|
|
10796
|
+
const usageEvent = createUsageEvent(toUsageEventInput(event));
|
|
10797
|
+
if (usageEvent.source !== source) {
|
|
10798
|
+
throw new Error(`its source is ${usageEvent.source}, not ${source}`);
|
|
10799
|
+
}
|
|
10800
|
+
return usageEvent;
|
|
10801
|
+
} catch (error) {
|
|
10802
|
+
throw new Error(
|
|
10803
|
+
`line ${lineNumber} has an invalid event at index ${index}: ${getErrorReason(error)}`,
|
|
10804
|
+
{ cause: error }
|
|
10805
|
+
);
|
|
10806
|
+
}
|
|
10807
|
+
})
|
|
10808
|
+
};
|
|
10809
|
+
}
|
|
10810
|
+
function readEndLine(record, lineNumber) {
|
|
10811
|
+
const { files, eventCount } = record;
|
|
10812
|
+
if (!Array.isArray(files) || typeof eventCount !== "number" || !Number.isSafeInteger(eventCount) || eventCount < 0) {
|
|
10813
|
+
throw new Error(`line ${lineNumber} is not a valid end line`);
|
|
10814
|
+
}
|
|
10815
|
+
const fileKeys = files.map((entry) => {
|
|
10816
|
+
const [source, filePath, revision] = Array.isArray(entry) ? entry : [];
|
|
10817
|
+
if (!Array.isArray(entry) || entry.length !== 3 || !isNonEmptyText(source) || !isNonEmptyText(filePath) || !isNonEmptyText(revision)) {
|
|
10818
|
+
throw new Error(`line ${lineNumber} lists an invalid file`);
|
|
10819
|
+
}
|
|
10820
|
+
return [source, filePath, revision];
|
|
10821
|
+
});
|
|
10822
|
+
return { type: "end", files: fileKeys, eventCount };
|
|
10823
|
+
}
|
|
10824
|
+
async function readMachineExport(lines) {
|
|
10825
|
+
let header;
|
|
10826
|
+
let end;
|
|
10827
|
+
const sentFiles = [];
|
|
10828
|
+
let lineNumber = 0;
|
|
10829
|
+
for await (const line of lines) {
|
|
10830
|
+
lineNumber += 1;
|
|
10831
|
+
if (line.trim().length === 0) {
|
|
10832
|
+
continue;
|
|
10833
|
+
}
|
|
10834
|
+
if (end) {
|
|
10835
|
+
throw new Error(`line ${lineNumber} follows the end line`);
|
|
10836
|
+
}
|
|
10837
|
+
if (!header) {
|
|
10838
|
+
if (line.includes(`"${MACHINE_EXPORT_FORMAT}"`)) {
|
|
10839
|
+
header = readHeader(parseBundleLine(line, lineNumber));
|
|
10840
|
+
}
|
|
10841
|
+
continue;
|
|
10842
|
+
}
|
|
10843
|
+
const record = parseBundleLine(line, lineNumber);
|
|
10844
|
+
if (record.type === "file") {
|
|
10845
|
+
sentFiles.push(readFileLine(record, lineNumber));
|
|
10846
|
+
} else if (record.type === "end") {
|
|
10847
|
+
end = readEndLine(record, lineNumber);
|
|
10848
|
+
} else {
|
|
10849
|
+
throw new Error(`line ${lineNumber} has an unknown type`);
|
|
10850
|
+
}
|
|
10851
|
+
}
|
|
10852
|
+
if (!header) {
|
|
10853
|
+
throw new Error("the output is not a machine export (no header line)");
|
|
10854
|
+
}
|
|
10855
|
+
if (!end) {
|
|
10856
|
+
throw new Error("the export ended early (no end line)");
|
|
10857
|
+
}
|
|
10858
|
+
const listedRevisions = /* @__PURE__ */ new Map();
|
|
10859
|
+
for (const [source, filePath, revision] of end.files) {
|
|
10860
|
+
const key = toFileKey(source, filePath);
|
|
10861
|
+
if (listedRevisions.has(key)) {
|
|
10862
|
+
throw new Error(`the end line lists ${filePath} twice`);
|
|
10863
|
+
}
|
|
10864
|
+
listedRevisions.set(key, revision);
|
|
10865
|
+
}
|
|
10866
|
+
const sentKeys = /* @__PURE__ */ new Set();
|
|
10867
|
+
for (const file of sentFiles) {
|
|
10868
|
+
const key = toFileKey(file.source, file.filePath);
|
|
10869
|
+
if (sentKeys.has(key) || listedRevisions.get(key) !== file.revision) {
|
|
10870
|
+
throw new Error(`the export sent ${file.filePath} but does not list it at that revision`);
|
|
10871
|
+
}
|
|
10872
|
+
sentKeys.add(key);
|
|
10873
|
+
}
|
|
10874
|
+
return { header, sentFiles, files: end.files, eventCount: end.eventCount };
|
|
10875
|
+
}
|
|
10876
|
+
|
|
10877
|
+
// src/machines/machine-cache.ts
|
|
10878
|
+
var REVISION_PREFIX = "machine-export:";
|
|
10879
|
+
function getMachineCacheDirectory() {
|
|
10880
|
+
return path32.join(path32.dirname(getDefaultEventStorePath()), "machines");
|
|
10881
|
+
}
|
|
10882
|
+
function getMachineCachePath(name) {
|
|
10883
|
+
return path32.join(getMachineCacheDirectory(), `${name}.db`);
|
|
10884
|
+
}
|
|
10885
|
+
async function openMachineCache(name) {
|
|
10886
|
+
const cache = await openEventStore(getMachineCachePath(name));
|
|
10887
|
+
cache.database.exec("CREATE INDEX IF NOT EXISTS machine_events_timestamp ON events (timestamp)");
|
|
10888
|
+
return cache;
|
|
10889
|
+
}
|
|
10890
|
+
async function deleteMachineCache(name) {
|
|
10891
|
+
const cachePath = getMachineCachePath(name);
|
|
10892
|
+
for (const filePath of [cachePath, `${cachePath}-wal`, `${cachePath}-shm`]) {
|
|
10893
|
+
await rm3(filePath, { force: true });
|
|
10894
|
+
}
|
|
10895
|
+
}
|
|
10896
|
+
function toRevisionFingerprint(filePath, revision) {
|
|
10897
|
+
return {
|
|
10898
|
+
parserVersion: `${REVISION_PREFIX}${revision}`,
|
|
10899
|
+
dependencies: [{ path: filePath, exists: false }]
|
|
10900
|
+
};
|
|
10901
|
+
}
|
|
10902
|
+
function readRevision(fingerprint) {
|
|
10903
|
+
try {
|
|
10904
|
+
const parsed = JSON.parse(fingerprint);
|
|
10905
|
+
const parserVersion = typeof parsed === "object" && parsed !== null && "parserVersion" in parsed ? parsed.parserVersion : void 0;
|
|
10906
|
+
return typeof parserVersion === "string" && parserVersion.startsWith(REVISION_PREFIX) ? parserVersion.slice(REVISION_PREFIX.length) : void 0;
|
|
10907
|
+
} catch {
|
|
10908
|
+
return void 0;
|
|
10909
|
+
}
|
|
10910
|
+
}
|
|
10911
|
+
function readCachedFiles(cache) {
|
|
10912
|
+
const files = [];
|
|
10913
|
+
for (const file of listStoredFileFingerprints(cache)) {
|
|
10914
|
+
const revision = readRevision(file.fingerprint);
|
|
10915
|
+
if (revision) {
|
|
10916
|
+
files.push([file.source, file.filePath, revision]);
|
|
10917
|
+
}
|
|
10918
|
+
}
|
|
10919
|
+
return files;
|
|
10920
|
+
}
|
|
10921
|
+
function countCachedEvents(cache, source, filePath) {
|
|
10922
|
+
const row = cache.database.prepare("SELECT COUNT(*) AS count FROM events WHERE source = ? AND file_path = ?").get(source, filePath);
|
|
10923
|
+
return typeof row?.count === "number" ? row.count : 0;
|
|
10924
|
+
}
|
|
10925
|
+
function applyMachineExport(cache, bundle, now) {
|
|
10926
|
+
return runTransaction(cache.database, () => {
|
|
10927
|
+
const listedKeys = new Set(
|
|
10928
|
+
bundle.files.map(([source, filePath]) => toFileKey(source, filePath))
|
|
10929
|
+
);
|
|
10930
|
+
const staleFiles = readCachedFiles(cache).filter(
|
|
10931
|
+
([source, filePath]) => !listedKeys.has(toFileKey(source, filePath))
|
|
10932
|
+
);
|
|
10933
|
+
deleteStoredFiles(
|
|
10934
|
+
cache,
|
|
10935
|
+
staleFiles.map(([source, filePath]) => ({ source, filePath }))
|
|
10936
|
+
);
|
|
10937
|
+
replaceFilesEvents(
|
|
10938
|
+
cache,
|
|
10939
|
+
bundle.sentFiles.map((file) => ({
|
|
10940
|
+
source: file.source,
|
|
10941
|
+
filePath: file.filePath,
|
|
10942
|
+
fingerprint: toRevisionFingerprint(file.filePath, file.revision),
|
|
10943
|
+
events: file.events,
|
|
10944
|
+
skippedRows: 0,
|
|
10945
|
+
now
|
|
10946
|
+
}))
|
|
10947
|
+
);
|
|
10948
|
+
const cachedRevisions = new Map(
|
|
10949
|
+
readCachedFiles(cache).map(([source, filePath, revision]) => [
|
|
10950
|
+
toFileKey(source, filePath),
|
|
10951
|
+
revision
|
|
10952
|
+
])
|
|
10953
|
+
);
|
|
10954
|
+
let eventCount = 0;
|
|
10955
|
+
for (const [source, filePath, revision] of bundle.files) {
|
|
10956
|
+
if (cachedRevisions.get(toFileKey(source, filePath)) !== revision) {
|
|
10957
|
+
throw new Error(`the export skipped ${filePath}, which the cache does not hold`);
|
|
10958
|
+
}
|
|
10959
|
+
eventCount += countCachedEvents(cache, source, filePath);
|
|
10960
|
+
}
|
|
10961
|
+
if (cachedRevisions.size !== bundle.files.length || eventCount !== bundle.eventCount) {
|
|
10962
|
+
throw new Error(
|
|
10963
|
+
`the cache holds ${eventCount} event(s) after the sync, but the export counts ${bundle.eventCount}`
|
|
10964
|
+
);
|
|
10965
|
+
}
|
|
10966
|
+
writeEventStoreMeta(cache, {
|
|
10967
|
+
hostname: bundle.header.hostname || void 0,
|
|
10968
|
+
cliVersion: bundle.header.cliVersion || void 0,
|
|
10969
|
+
syncedAt: String(now),
|
|
10970
|
+
attemptedAt: String(now),
|
|
10971
|
+
lastError: void 0
|
|
10972
|
+
});
|
|
10973
|
+
return {
|
|
10974
|
+
receivedFileCount: bundle.sentFiles.length,
|
|
10975
|
+
removedFileCount: staleFiles.length,
|
|
10976
|
+
fileCount: bundle.files.length,
|
|
10977
|
+
eventCount
|
|
10978
|
+
};
|
|
10979
|
+
});
|
|
10980
|
+
}
|
|
10981
|
+
function recordMachineSyncFailure(cache, reason, now) {
|
|
10982
|
+
writeEventStoreMeta(cache, { attemptedAt: String(now), lastError: reason });
|
|
10983
|
+
}
|
|
10984
|
+
function toEpochMs(value) {
|
|
10985
|
+
const parsed = value === void 0 ? Number.NaN : Number(value);
|
|
10986
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : void 0;
|
|
10987
|
+
}
|
|
10988
|
+
function toSyncState(readMeta) {
|
|
10989
|
+
return {
|
|
10990
|
+
hostname: readMeta("hostname"),
|
|
10991
|
+
cliVersion: readMeta("cliVersion"),
|
|
10992
|
+
syncedAt: toEpochMs(readMeta("syncedAt")),
|
|
10993
|
+
attemptedAt: toEpochMs(readMeta("attemptedAt")),
|
|
10994
|
+
lastError: readMeta("lastError")
|
|
10995
|
+
};
|
|
10996
|
+
}
|
|
10997
|
+
function readMachineSyncState(cache) {
|
|
10998
|
+
return toSyncState((key) => readEventStoreMeta(cache, key));
|
|
10999
|
+
}
|
|
11000
|
+
async function readMachineCacheUsage(name, window) {
|
|
11001
|
+
const cachePath = getMachineCachePath(name);
|
|
11002
|
+
if (!await pathExists(cachePath)) {
|
|
11003
|
+
return void 0;
|
|
11004
|
+
}
|
|
11005
|
+
const { meta, events } = await readEventStoreEvents(cachePath, window);
|
|
11006
|
+
return { state: toSyncState((key) => meta.get(key)), events };
|
|
11007
|
+
}
|
|
11008
|
+
async function readMachineCacheStatus(name) {
|
|
11009
|
+
const cachePath = getMachineCachePath(name);
|
|
11010
|
+
if (!await pathExists(cachePath)) {
|
|
11011
|
+
return void 0;
|
|
11012
|
+
}
|
|
11013
|
+
const [meta, summary, storedFiles] = await Promise.all([
|
|
11014
|
+
readEventStoreMetaValues(cachePath),
|
|
11015
|
+
readEventStoreSummary(cachePath),
|
|
11016
|
+
readEventStoreStoredFiles(cachePath)
|
|
11017
|
+
]);
|
|
11018
|
+
return {
|
|
11019
|
+
state: toSyncState((key) => meta.get(key)),
|
|
11020
|
+
fileCount: storedFiles.length,
|
|
11021
|
+
eventCount: summary.eventCount
|
|
11022
|
+
};
|
|
11023
|
+
}
|
|
11024
|
+
async function readMachineSyncStateIfCached(name) {
|
|
11025
|
+
const cachePath = getMachineCachePath(name);
|
|
11026
|
+
if (!await pathExists(cachePath)) {
|
|
11027
|
+
return void 0;
|
|
11028
|
+
}
|
|
11029
|
+
const meta = await readEventStoreMetaValues(cachePath);
|
|
11030
|
+
return toSyncState((key) => meta.get(key));
|
|
11031
|
+
}
|
|
11032
|
+
function isMachineSyncDue(state, now, intervalMs) {
|
|
11033
|
+
const last = Math.max(state?.syncedAt ?? 0, state?.attemptedAt ?? 0);
|
|
11034
|
+
return now - last >= intervalMs;
|
|
11035
|
+
}
|
|
11036
|
+
function claimMachineSync(cache, now, intervalMs) {
|
|
11037
|
+
return runTransaction(cache.database, () => {
|
|
11038
|
+
if (!isMachineSyncDue(readMachineSyncState(cache), now, intervalMs)) {
|
|
11039
|
+
return false;
|
|
11040
|
+
}
|
|
11041
|
+
writeEventStoreMeta(cache, { attemptedAt: String(now) });
|
|
11042
|
+
return true;
|
|
11043
|
+
});
|
|
11044
|
+
}
|
|
11045
|
+
|
|
11046
|
+
// src/machines/merge-machine-events.ts
|
|
11047
|
+
function countHashes(events) {
|
|
11048
|
+
const counts = /* @__PURE__ */ new Map();
|
|
11049
|
+
for (const event of events) {
|
|
11050
|
+
const hash = computeEventContentHash(event);
|
|
11051
|
+
counts.set(hash, (counts.get(hash) ?? 0) + 1);
|
|
11052
|
+
}
|
|
11053
|
+
return counts;
|
|
11054
|
+
}
|
|
11055
|
+
function groupBySession(events) {
|
|
11056
|
+
const groups = /* @__PURE__ */ new Map();
|
|
11057
|
+
for (const event of events) {
|
|
11058
|
+
const key = getEventSessionKey(event);
|
|
11059
|
+
const group = groups.get(key);
|
|
11060
|
+
if (group) {
|
|
11061
|
+
group.push(event);
|
|
11062
|
+
} else {
|
|
11063
|
+
groups.set(key, [event]);
|
|
11064
|
+
}
|
|
11065
|
+
}
|
|
11066
|
+
return groups;
|
|
11067
|
+
}
|
|
11068
|
+
function mergeMachineEvents(servedEvents, machines) {
|
|
11069
|
+
const servedBySession = groupBySession(servedEvents);
|
|
11070
|
+
const servedHashesBySession = /* @__PURE__ */ new Map();
|
|
11071
|
+
const merged = [];
|
|
11072
|
+
for (const machine of machines) {
|
|
11073
|
+
const kept = [];
|
|
11074
|
+
let duplicateCount = 0;
|
|
11075
|
+
for (const [sessionKey, events] of groupBySession(machine.events)) {
|
|
11076
|
+
const served = servedBySession.get(sessionKey);
|
|
11077
|
+
if (!served) {
|
|
11078
|
+
servedBySession.set(sessionKey, [...events]);
|
|
11079
|
+
kept.push(...events);
|
|
11080
|
+
continue;
|
|
11081
|
+
}
|
|
11082
|
+
let servedHashes = servedHashesBySession.get(sessionKey);
|
|
11083
|
+
if (!servedHashes) {
|
|
11084
|
+
servedHashes = countHashes(served);
|
|
11085
|
+
servedHashesBySession.set(sessionKey, servedHashes);
|
|
11086
|
+
}
|
|
11087
|
+
const available = new Map(servedHashes);
|
|
11088
|
+
for (const event of events) {
|
|
11089
|
+
const hash = computeEventContentHash(event);
|
|
11090
|
+
const count = available.get(hash) ?? 0;
|
|
11091
|
+
if (count > 0) {
|
|
11092
|
+
available.set(hash, count - 1);
|
|
11093
|
+
duplicateCount += 1;
|
|
11094
|
+
continue;
|
|
11095
|
+
}
|
|
11096
|
+
kept.push(event);
|
|
11097
|
+
served.push(event);
|
|
11098
|
+
servedHashes.set(hash, (servedHashes.get(hash) ?? 0) + 1);
|
|
11099
|
+
}
|
|
11100
|
+
}
|
|
11101
|
+
merged.push({ name: machine.name, events: kept, duplicateCount });
|
|
11102
|
+
}
|
|
11103
|
+
return merged;
|
|
11104
|
+
}
|
|
11105
|
+
|
|
11106
|
+
// src/machines/load-machine-usage.ts
|
|
11107
|
+
function selectMachines(machines, machineFilter, options) {
|
|
11108
|
+
const configured = Object.keys(machines ?? {}).sort();
|
|
11109
|
+
if (!machineFilter) {
|
|
11110
|
+
return { includeLocal: true, names: options.customSourceDirectories ? [] : configured };
|
|
11111
|
+
}
|
|
11112
|
+
const known = /* @__PURE__ */ new Set([LOCAL_MACHINE_NAME, ...configured]);
|
|
11113
|
+
const unknown = [...machineFilter].filter((name) => !known.has(name));
|
|
11114
|
+
if (unknown.length > 0) {
|
|
11115
|
+
throw new Error(
|
|
11116
|
+
`Unknown machine(s) for --machine: ${unknown.join(", ")} (known: ${[...known].join(", ")})`
|
|
11117
|
+
);
|
|
11118
|
+
}
|
|
11119
|
+
return {
|
|
11120
|
+
includeLocal: machineFilter.has(LOCAL_MACHINE_NAME),
|
|
11121
|
+
names: configured.filter((name) => machineFilter.has(name))
|
|
11122
|
+
};
|
|
11123
|
+
}
|
|
11124
|
+
function toTimestampBound(date, offsetDays) {
|
|
11125
|
+
if (date === void 0) {
|
|
11126
|
+
return void 0;
|
|
11127
|
+
}
|
|
11128
|
+
const bound = /* @__PURE__ */ new Date(`${date}T00:00:00.000Z`);
|
|
11129
|
+
bound.setUTCDate(bound.getUTCDate() + offsetDays);
|
|
11130
|
+
return bound.toISOString();
|
|
11131
|
+
}
|
|
11132
|
+
async function loadMachineUsage(params) {
|
|
11133
|
+
const window = {
|
|
11134
|
+
fromTimestamp: toTimestampBound(params.since, -1),
|
|
11135
|
+
toTimestamp: toTimestampBound(params.until, 2)
|
|
11136
|
+
};
|
|
11137
|
+
const warnings = [];
|
|
11138
|
+
const caches = await Promise.all(
|
|
11139
|
+
params.names.map(async (name) => {
|
|
11140
|
+
try {
|
|
11141
|
+
return { name, usage: await readMachineCacheUsage(name, window) };
|
|
11142
|
+
} catch (error) {
|
|
11143
|
+
warnings.push(
|
|
11144
|
+
`Left out machine ${name}: its cache cannot be read (${getErrorReason(error)})`
|
|
11145
|
+
);
|
|
11146
|
+
return void 0;
|
|
11147
|
+
}
|
|
11148
|
+
})
|
|
11149
|
+
);
|
|
11150
|
+
const readable = caches.filter((cache) => cache !== void 0);
|
|
11151
|
+
const merged = mergeMachineEvents(
|
|
11152
|
+
params.servedEvents,
|
|
11153
|
+
readable.map(({ name, usage }) => ({
|
|
11154
|
+
name,
|
|
11155
|
+
events: (usage?.events ?? []).filter((event) => params.sources.has(event.source))
|
|
11156
|
+
}))
|
|
11157
|
+
);
|
|
11158
|
+
return {
|
|
11159
|
+
events: merged.flatMap(
|
|
11160
|
+
(machine) => machine.events.map((event) => ({ ...event, machine: machine.name }))
|
|
11161
|
+
),
|
|
11162
|
+
machines: readable.map(({ name, usage }, index) => ({
|
|
11163
|
+
name,
|
|
11164
|
+
enabled: !params.disabledNames?.has(name),
|
|
11165
|
+
state: usage?.state,
|
|
11166
|
+
duplicateCount: merged[index].duplicateCount
|
|
11167
|
+
})),
|
|
11168
|
+
warnings
|
|
11169
|
+
};
|
|
11170
|
+
}
|
|
11171
|
+
|
|
11172
|
+
// src/machines/machine-ssh.ts
|
|
11173
|
+
import { spawn } from "child_process";
|
|
11174
|
+
import { createInterface } from "readline";
|
|
11175
|
+
import { stripVTControlCharacters } from "util";
|
|
11176
|
+
var DEFAULT_MACHINE_COMMAND = "llm-usage";
|
|
11177
|
+
var MACHINE_EXPORT_TIMEOUT_MARK = "timed out";
|
|
11178
|
+
var STDERR_TAIL_BYTES = 4096;
|
|
11179
|
+
var spawnSystemSsh = (args) => spawn("ssh", args, { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
11180
|
+
function buildSshArgs(target, remoteCommand) {
|
|
11181
|
+
return [
|
|
11182
|
+
"-T",
|
|
11183
|
+
"-o",
|
|
11184
|
+
"BatchMode=yes",
|
|
11185
|
+
"-o",
|
|
11186
|
+
"ConnectTimeout=10",
|
|
11187
|
+
"-o",
|
|
11188
|
+
"ServerAliveInterval=15",
|
|
11189
|
+
"-o",
|
|
11190
|
+
"ServerAliveCountMax=3",
|
|
11191
|
+
"-o",
|
|
11192
|
+
"Compression=yes",
|
|
11193
|
+
"--",
|
|
11194
|
+
target,
|
|
11195
|
+
remoteCommand
|
|
11196
|
+
];
|
|
11197
|
+
}
|
|
11198
|
+
function buildSshExportArgs(machine) {
|
|
11199
|
+
return buildSshArgs(
|
|
11200
|
+
machine.ssh,
|
|
11201
|
+
`${machine.command ?? DEFAULT_MACHINE_COMMAND} machine export --known - --quiet`
|
|
11202
|
+
);
|
|
11203
|
+
}
|
|
11204
|
+
var LOGIN_SHELL_MARKER = "llm-usage-metrics: asking the login shell";
|
|
11205
|
+
var PROBE_OUTPUT_BYTES = 64 * 1024;
|
|
11206
|
+
var PROBE_TIMEOUT_MS = 2e4;
|
|
11207
|
+
var SAFE_PATH_PATTERN = /^\/[\w./+@-]*$/u;
|
|
11208
|
+
function ignoreStreamErrors(child) {
|
|
11209
|
+
child.stdin.on("error", () => void 0);
|
|
11210
|
+
child.stdout.on("error", () => void 0);
|
|
11211
|
+
child.stderr.on("error", () => void 0);
|
|
11212
|
+
}
|
|
11213
|
+
function readProbeValue(lines, key) {
|
|
11214
|
+
const value = lines.filter((line) => line.startsWith(`${key}=`)).at(-1)?.slice(key.length + 1);
|
|
11215
|
+
return value !== void 0 && SAFE_PATH_PATTERN.test(value) ? value : void 0;
|
|
11216
|
+
}
|
|
11217
|
+
async function detectRemoteCommand(target, options = {}) {
|
|
11218
|
+
const probe = [
|
|
11219
|
+
`command -v ${DEFAULT_MACHINE_COMMAND} && exit 0`,
|
|
11220
|
+
`echo '${LOGIN_SHELL_MARKER}'`,
|
|
11221
|
+
`"$SHELL" -lic 'echo "command=$(command -v ${DEFAULT_MACHINE_COMMAND})"; echo "node=$(node -p process.execPath)"'`
|
|
11222
|
+
].join("; ");
|
|
11223
|
+
const child = (options.spawnSsh ?? spawnSystemSsh)(buildSshArgs(target, probe));
|
|
11224
|
+
const timeoutMs = options.timeoutMs ?? PROBE_TIMEOUT_MS;
|
|
11225
|
+
const timeout = { expired: false };
|
|
11226
|
+
let output = "";
|
|
11227
|
+
const timer = setTimeout(() => {
|
|
11228
|
+
timeout.expired = true;
|
|
11229
|
+
child.kill();
|
|
11230
|
+
}, timeoutMs);
|
|
11231
|
+
ignoreStreamErrors(child);
|
|
11232
|
+
child.stdin.end();
|
|
11233
|
+
child.stdout.on("data", (chunk) => {
|
|
11234
|
+
output = (output + chunk.toString("utf8")).slice(-PROBE_OUTPUT_BYTES);
|
|
11235
|
+
});
|
|
11236
|
+
child.stderr.resume();
|
|
11237
|
+
try {
|
|
11238
|
+
await new Promise((resolve) => {
|
|
11239
|
+
child.once("error", () => {
|
|
11240
|
+
resolve();
|
|
11241
|
+
});
|
|
11242
|
+
child.once("close", () => {
|
|
11243
|
+
resolve();
|
|
11244
|
+
});
|
|
11245
|
+
});
|
|
11246
|
+
} finally {
|
|
11247
|
+
clearTimeout(timer);
|
|
11248
|
+
}
|
|
11249
|
+
if (timeout.expired) {
|
|
11250
|
+
logger.info(
|
|
11251
|
+
`Looking up llm-usage in ${target}'s login shell took over ${Math.round(timeoutMs / 1e3)}s (a startup file may wait for input); trying the default command`
|
|
11252
|
+
);
|
|
11253
|
+
return void 0;
|
|
11254
|
+
}
|
|
11255
|
+
const lines = output.split(/\r?\n/u).map((line) => line.trim());
|
|
11256
|
+
const markerIndex = lines.indexOf(LOGIN_SHELL_MARKER);
|
|
11257
|
+
const afterMarker = lines.slice(markerIndex + 1);
|
|
11258
|
+
const command = readProbeValue(afterMarker, "command");
|
|
11259
|
+
if (markerIndex === -1 || !command?.endsWith(`/${DEFAULT_MACHINE_COMMAND}`)) {
|
|
11260
|
+
return void 0;
|
|
11261
|
+
}
|
|
11262
|
+
const node = readProbeValue(afterMarker, "node");
|
|
11263
|
+
const directories = [command, node].filter((filePath) => filePath !== void 0).map((filePath) => filePath.slice(0, filePath.lastIndexOf("/")));
|
|
11264
|
+
return `env PATH=${[...new Set(directories)].join(":")}:"$PATH" ${DEFAULT_MACHINE_COMMAND}`;
|
|
11265
|
+
}
|
|
11266
|
+
function readStderrTail(chunks) {
|
|
11267
|
+
const text2 = Buffer.concat(chunks).toString("utf8").slice(-STDERR_TAIL_BYTES);
|
|
11268
|
+
return stripControlCharacters(stripVTControlCharacters(text2).replaceAll(/\s+/gu, " ")).trim();
|
|
11269
|
+
}
|
|
11270
|
+
function describeExportFailure(machine, exitCode, stderr) {
|
|
11271
|
+
const command = machine.command ?? DEFAULT_MACHINE_COMMAND;
|
|
11272
|
+
const detail = (stderr || `exit code ${String(exitCode)}`).replace(/\.$/u, "");
|
|
11273
|
+
if (exitCode === null) {
|
|
11274
|
+
return `ssh was stopped by a signal: ${detail}`;
|
|
11275
|
+
}
|
|
11276
|
+
if (exitCode === 255) {
|
|
11277
|
+
if (/permission denied|too many authentication failures/iu.test(stderr)) {
|
|
11278
|
+
return `ssh could not log in without a prompt: ${detail}. Set up key login (ssh-copy-id ${machine.ssh}) or load your key into ssh-agent.`;
|
|
11279
|
+
}
|
|
11280
|
+
if (/host key verification failed/iu.test(stderr)) {
|
|
11281
|
+
return `ssh does not know this host yet: ${detail}. Connect once with \`ssh ${machine.ssh}\` to check and accept its host key.`;
|
|
11282
|
+
}
|
|
11283
|
+
return `ssh failed: ${detail}`;
|
|
11284
|
+
}
|
|
11285
|
+
if (exitCode === 127 || /command not found|not recognized as an internal/iu.test(stderr)) {
|
|
11286
|
+
return `${command} was not found on ${machine.ssh}: ${detail}. Install llm-usage-metrics there, or give the command that starts it (machine add --command, or command in config.toml).`;
|
|
11287
|
+
}
|
|
11288
|
+
if (/unknown command 'machine'|unknown command 'export'/iu.test(stderr)) {
|
|
11289
|
+
return `${command} on ${machine.ssh} is too old to export usage: ${detail}. Update llm-usage-metrics there.`;
|
|
11290
|
+
}
|
|
11291
|
+
return `machine export failed: ${detail}`;
|
|
11292
|
+
}
|
|
11293
|
+
async function fetchMachineExport(machine, knownFiles, options = {}) {
|
|
11294
|
+
if (options.signal?.aborted) {
|
|
11295
|
+
throw new Error("stopped: the report it was for ended");
|
|
11296
|
+
}
|
|
11297
|
+
const child = (options.spawnSsh ?? spawnSystemSsh)(buildSshExportArgs(machine));
|
|
11298
|
+
const stopOnAbort = () => {
|
|
11299
|
+
child.kill();
|
|
11300
|
+
};
|
|
11301
|
+
options.signal?.addEventListener("abort", stopOnAbort, { once: true });
|
|
11302
|
+
const stderrChunks = [];
|
|
11303
|
+
const closed = new Promise((resolve, reject) => {
|
|
11304
|
+
child.once("error", reject);
|
|
11305
|
+
child.once("close", resolve);
|
|
11306
|
+
});
|
|
11307
|
+
const timeout = { expired: false };
|
|
11308
|
+
const timer = options.timeoutMs === void 0 ? void 0 : setTimeout(() => {
|
|
11309
|
+
timeout.expired = true;
|
|
11310
|
+
child.kill();
|
|
11311
|
+
}, options.timeoutMs);
|
|
11312
|
+
let stderrBytes = 0;
|
|
11313
|
+
ignoreStreamErrors(child);
|
|
11314
|
+
child.stderr.on("data", (chunk) => {
|
|
11315
|
+
stderrChunks.push(chunk);
|
|
11316
|
+
stderrBytes += chunk.length;
|
|
11317
|
+
while (stderrBytes > STDERR_TAIL_BYTES * 4 && stderrChunks.length > 1) {
|
|
11318
|
+
stderrBytes -= stderrChunks.shift()?.length ?? 0;
|
|
11319
|
+
}
|
|
11320
|
+
});
|
|
11321
|
+
child.stdin.end(JSON.stringify({ version: MACHINE_EXPORT_VERSION, files: knownFiles }));
|
|
11322
|
+
let bundle;
|
|
11323
|
+
let readError;
|
|
11324
|
+
let stoppedByUs = false;
|
|
11325
|
+
child.stdout.on("error", (error) => {
|
|
11326
|
+
readError ??= error;
|
|
11327
|
+
});
|
|
11328
|
+
try {
|
|
11329
|
+
bundle = await readMachineExport(createInterface({ input: child.stdout, crlfDelay: Infinity }));
|
|
11330
|
+
} catch (error) {
|
|
11331
|
+
readError = error;
|
|
11332
|
+
if (!child.stdout.readableEnded) {
|
|
11333
|
+
stoppedByUs = true;
|
|
11334
|
+
child.kill();
|
|
11335
|
+
}
|
|
11336
|
+
}
|
|
11337
|
+
let exitCode;
|
|
11338
|
+
try {
|
|
11339
|
+
exitCode = await closed;
|
|
11340
|
+
} catch (error) {
|
|
11341
|
+
throw new Error(`could not run ssh: ${getErrorReason(error)}`, { cause: error });
|
|
11342
|
+
} finally {
|
|
11343
|
+
clearTimeout(timer);
|
|
11344
|
+
options.signal?.removeEventListener("abort", stopOnAbort);
|
|
11345
|
+
}
|
|
11346
|
+
if (options.signal?.aborted) {
|
|
11347
|
+
throw new Error("stopped: the report it was for ended");
|
|
11348
|
+
}
|
|
11349
|
+
if (timeout.expired) {
|
|
11350
|
+
throw new Error(
|
|
11351
|
+
`no complete export within ${Math.round((options.timeoutMs ?? 0) / 1e3)}s (${MACHINE_EXPORT_TIMEOUT_MARK})`
|
|
11352
|
+
);
|
|
11353
|
+
}
|
|
11354
|
+
if (exitCode !== 0 && !stoppedByUs) {
|
|
11355
|
+
throw new Error(describeExportFailure(machine, exitCode, readStderrTail(stderrChunks)));
|
|
11356
|
+
}
|
|
11357
|
+
if (!bundle) {
|
|
11358
|
+
throw new Error(`the export from ${machine.ssh} is invalid: ${getErrorReason(readError)}`, {
|
|
11359
|
+
cause: readError
|
|
11360
|
+
});
|
|
11361
|
+
}
|
|
11362
|
+
const remoteWarnings = Buffer.concat(stderrChunks).toString("utf8").slice(-STDERR_TAIL_BYTES).split("\n").map((line) => stripControlCharacters(stripVTControlCharacters(line)).trim()).filter((line) => line.length > 0);
|
|
11363
|
+
return { bundle, remoteWarnings };
|
|
11364
|
+
}
|
|
11365
|
+
|
|
11366
|
+
// src/render/render-machines.ts
|
|
11367
|
+
var integerFormat = new Intl.NumberFormat("en-US");
|
|
11368
|
+
function formatAge(ageMs) {
|
|
11369
|
+
const minutes = Math.floor(Math.max(0, ageMs) / 6e4);
|
|
11370
|
+
if (minutes < 1) {
|
|
11371
|
+
return "just now";
|
|
11372
|
+
}
|
|
11373
|
+
if (minutes < 60) {
|
|
11374
|
+
return `${minutes} min ago`;
|
|
11375
|
+
}
|
|
11376
|
+
const hours = Math.floor(minutes / 60);
|
|
11377
|
+
if (hours < 48) {
|
|
11378
|
+
return `${hours} h ago`;
|
|
11379
|
+
}
|
|
11380
|
+
return `${Math.floor(hours / 24)} days ago`;
|
|
11381
|
+
}
|
|
11382
|
+
function formatCachedTotals(fileCount, eventCount) {
|
|
11383
|
+
return `${integerFormat.format(fileCount)} file(s), ${integerFormat.format(eventCount)} event(s)`;
|
|
11384
|
+
}
|
|
11385
|
+
function formatRemoteVersion(state) {
|
|
11386
|
+
if (!state.cliVersion) {
|
|
11387
|
+
return "";
|
|
11388
|
+
}
|
|
11389
|
+
return state.hostname ? ` from ${state.hostname} (llm-usage-metrics ${state.cliVersion})` : ` (llm-usage-metrics ${state.cliVersion})`;
|
|
11390
|
+
}
|
|
11391
|
+
function renderSyncOutcome(outcome, now) {
|
|
11392
|
+
if (outcome.ok) {
|
|
11393
|
+
const { result } = outcome;
|
|
11394
|
+
const changes = result.receivedFileCount === 0 && result.removedFileCount === 0 ? "up to date" : `${integerFormat.format(result.receivedFileCount)} file(s) updated, ${integerFormat.format(result.removedFileCount)} removed`;
|
|
11395
|
+
return [
|
|
11396
|
+
`\u2713 ${outcome.name}: ${changes}; ${formatCachedTotals(result.fileCount, result.eventCount)} cached${formatRemoteVersion(outcome.state)}`,
|
|
11397
|
+
...outcome.remoteWarnings.map((warning) => ` ${outcome.name} warned: ${warning}`)
|
|
11398
|
+
];
|
|
11399
|
+
}
|
|
11400
|
+
const cacheNote = outcome.state.syncedAt === void 0 ? "nothing cached yet" : `reports keep its usage from ${formatAge(now - outcome.state.syncedAt)}`;
|
|
11401
|
+
return [`\u2717 ${outcome.name}: ${outcome.error} (${cacheNote})`];
|
|
11402
|
+
}
|
|
11403
|
+
function describeMachineStatus(entry, now) {
|
|
11404
|
+
const { state } = entry;
|
|
11405
|
+
if (entry.machine.enabled === false) {
|
|
11406
|
+
return "disabled (cached usage stays in reports)";
|
|
11407
|
+
}
|
|
11408
|
+
if (state?.syncedAt === void 0) {
|
|
11409
|
+
return state?.lastError ? `never synced: ${state.lastError}` : "never synced";
|
|
11410
|
+
}
|
|
11411
|
+
const synced = `synced ${formatAge(now - state.syncedAt)}, ${formatCachedTotals(entry.fileCount, entry.eventCount)}`;
|
|
11412
|
+
if (state.lastError && (state.attemptedAt ?? 0) >= state.syncedAt) {
|
|
11413
|
+
return `${synced}; last attempt failed ${formatAge(now - (state.attemptedAt ?? 0))}: ${state.lastError}`;
|
|
11414
|
+
}
|
|
11415
|
+
return synced;
|
|
11416
|
+
}
|
|
11417
|
+
function renderMachineList(entries, now) {
|
|
11418
|
+
if (entries.length === 0) {
|
|
11419
|
+
return ["No machines configured.", "Add one with: llm-usage machine add <name> [user@host]"];
|
|
11420
|
+
}
|
|
11421
|
+
const nameWidth = Math.max(...entries.map((entry) => entry.name.length));
|
|
11422
|
+
const sshWidth = Math.max(...entries.map((entry) => entry.machine.ssh.length));
|
|
11423
|
+
return entries.map(
|
|
11424
|
+
(entry) => `${entry.name.padEnd(nameWidth)} ${entry.machine.ssh.padEnd(sshWidth)} ${describeMachineStatus(entry, now)}`
|
|
11425
|
+
);
|
|
11426
|
+
}
|
|
11427
|
+
function describeIncludedMachine(machine, now, localVersion) {
|
|
11428
|
+
const { state } = machine;
|
|
11429
|
+
if (!machine.enabled) {
|
|
11430
|
+
return state?.syncedAt === void 0 ? `${machine.name} (disabled, nothing cached)` : `${machine.name} (disabled, usage from ${formatAge(now - state.syncedAt)})`;
|
|
11431
|
+
}
|
|
11432
|
+
if (state?.syncedAt === void 0) {
|
|
11433
|
+
return `${machine.name} (never synced; run llm-usage sync ${machine.name})`;
|
|
11434
|
+
}
|
|
11435
|
+
const details = [`synced ${formatAge(now - state.syncedAt)}`];
|
|
11436
|
+
if (state.lastError !== void 0 && (state.attemptedAt ?? 0) >= state.syncedAt) {
|
|
11437
|
+
details.push("last sync failed");
|
|
11438
|
+
}
|
|
11439
|
+
if (state.cliVersion && state.cliVersion !== localVersion) {
|
|
11440
|
+
details.push(
|
|
11441
|
+
`llm-usage-metrics ${state.cliVersion} there; shared sessions may count twice until both run the same version`
|
|
11442
|
+
);
|
|
11443
|
+
}
|
|
11444
|
+
return `${machine.name} (${details.join(", ")})`;
|
|
11445
|
+
}
|
|
11446
|
+
function formatMachinesNote(machines, now, localVersion) {
|
|
11447
|
+
const duplicateCount = machines.reduce((sum, machine) => sum + machine.duplicateCount, 0);
|
|
11448
|
+
const duplicates = duplicateCount > 0 ? `; left out ${integerFormat.format(duplicateCount)} event(s) already counted` : "";
|
|
11449
|
+
return `Machines: ${machines.map((machine) => describeIncludedMachine(machine, now, localVersion)).join(", ")}${duplicates}.`;
|
|
11450
|
+
}
|
|
11451
|
+
var STALE_CACHE_MS = 24 * 60 * 6e4;
|
|
11452
|
+
function formatRefreshFailure(outcome, now) {
|
|
11453
|
+
if (outcome.ok) {
|
|
11454
|
+
return void 0;
|
|
11455
|
+
}
|
|
11456
|
+
const { syncedAt } = outcome.state;
|
|
11457
|
+
if (syncedAt === void 0) {
|
|
11458
|
+
return {
|
|
11459
|
+
text: `Could not sync ${outcome.name} (${outcome.error}); it has no usage cached yet.`,
|
|
11460
|
+
stale: true
|
|
11461
|
+
};
|
|
11462
|
+
}
|
|
11463
|
+
const hint = outcome.error.endsWith(`(${MACHINE_EXPORT_TIMEOUT_MARK})`) ? ` Run llm-usage sync ${outcome.name} to wait for it.` : "";
|
|
11464
|
+
return {
|
|
11465
|
+
text: `Could not sync ${outcome.name} (${outcome.error}); using its usage from ${formatAge(now - syncedAt)}.${hint}`,
|
|
11466
|
+
stale: now - syncedAt >= STALE_CACHE_MS
|
|
11467
|
+
};
|
|
11468
|
+
}
|
|
11469
|
+
|
|
11470
|
+
// src/machines/sync-machine.ts
|
|
11471
|
+
async function syncMachine(name, machine, options = {}) {
|
|
11472
|
+
const now = options.now ?? Date.now;
|
|
11473
|
+
let cache;
|
|
11474
|
+
try {
|
|
11475
|
+
cache = await openMachineCache(name);
|
|
11476
|
+
} catch (error) {
|
|
11477
|
+
return {
|
|
11478
|
+
name,
|
|
11479
|
+
ok: false,
|
|
11480
|
+
error: `cannot open its cache at ${getMachineCachePath(name)}: ${getErrorReason(error)}`,
|
|
11481
|
+
state: {}
|
|
11482
|
+
};
|
|
11483
|
+
}
|
|
11484
|
+
try {
|
|
11485
|
+
if (options.recordAttemptFirst === true) {
|
|
11486
|
+
try {
|
|
11487
|
+
writeEventStoreMeta(cache, { attemptedAt: String(now()) });
|
|
11488
|
+
} catch {
|
|
11489
|
+
}
|
|
11490
|
+
}
|
|
11491
|
+
const { bundle, remoteWarnings } = await fetchMachineExport(
|
|
11492
|
+
machine,
|
|
11493
|
+
options.full === true ? [] : readCachedFiles(cache),
|
|
11494
|
+
{ spawnSsh: options.spawnSsh, timeoutMs: options.timeoutMs, signal: options.signal }
|
|
11495
|
+
);
|
|
11496
|
+
const result = applyMachineExport(cache, bundle, now());
|
|
11497
|
+
return { name, ok: true, result, remoteWarnings, state: readMachineSyncState(cache) };
|
|
11498
|
+
} catch (error) {
|
|
11499
|
+
const reason = getErrorReason(error);
|
|
11500
|
+
let state = {};
|
|
11501
|
+
try {
|
|
11502
|
+
if (!options.signal?.aborted) {
|
|
11503
|
+
recordMachineSyncFailure(cache, reason, now());
|
|
11504
|
+
}
|
|
11505
|
+
state = readMachineSyncState(cache);
|
|
11506
|
+
} catch {
|
|
11507
|
+
}
|
|
11508
|
+
return { name, ok: false, error: reason, state };
|
|
11509
|
+
} finally {
|
|
11510
|
+
closeEventStore(cache);
|
|
11511
|
+
}
|
|
11512
|
+
}
|
|
11513
|
+
|
|
11514
|
+
// src/machines/refresh-machines.ts
|
|
11515
|
+
var MACHINE_REFRESH_INTERVAL_MS = 10 * 6e4;
|
|
11516
|
+
var MACHINE_REFRESH_TIMEOUT_MS = 3e4;
|
|
11517
|
+
async function claimDueSync(name, now) {
|
|
11518
|
+
try {
|
|
11519
|
+
if (!isMachineSyncDue(await readMachineSyncStateIfCached(name), now, MACHINE_REFRESH_INTERVAL_MS)) {
|
|
11520
|
+
return false;
|
|
11521
|
+
}
|
|
11522
|
+
} catch {
|
|
11523
|
+
}
|
|
11524
|
+
try {
|
|
11525
|
+
const cache = await openMachineCache(name);
|
|
11526
|
+
try {
|
|
11527
|
+
return claimMachineSync(cache, now, MACHINE_REFRESH_INTERVAL_MS);
|
|
11528
|
+
} finally {
|
|
11529
|
+
closeEventStore(cache);
|
|
11530
|
+
}
|
|
11531
|
+
} catch {
|
|
11532
|
+
return false;
|
|
11533
|
+
}
|
|
11534
|
+
}
|
|
11535
|
+
async function refreshDueMachines(machines, options = {}) {
|
|
11536
|
+
const now = options.now ?? Date.now;
|
|
11537
|
+
const outcomes = await Promise.all(
|
|
11538
|
+
machines.filter(({ machine }) => machine.enabled !== false).map(async ({ name, machine }) => {
|
|
11539
|
+
if (!await claimDueSync(name, now())) {
|
|
11540
|
+
return void 0;
|
|
11541
|
+
}
|
|
11542
|
+
return syncMachine(name, machine, {
|
|
11543
|
+
spawnSsh: options.spawnSsh,
|
|
11544
|
+
now,
|
|
11545
|
+
timeoutMs: options.timeoutMs ?? MACHINE_REFRESH_TIMEOUT_MS,
|
|
11546
|
+
signal: options.signal
|
|
11547
|
+
});
|
|
11548
|
+
})
|
|
11549
|
+
);
|
|
11550
|
+
return outcomes.filter((outcome) => outcome !== void 0);
|
|
11551
|
+
}
|
|
11552
|
+
|
|
11553
|
+
// src/cli/package-metadata.ts
|
|
11554
|
+
import { createRequire as createRequire2 } from "module";
|
|
11555
|
+
var DEFAULT_PACKAGE_NAME = "llm-usage-metrics";
|
|
11556
|
+
var DEFAULT_PACKAGE_VERSION = "0.0.0";
|
|
11557
|
+
var defaultPackageJsonCandidates = ["../package.json", "../../package.json"];
|
|
11558
|
+
function normalizeMetadata(candidate) {
|
|
11559
|
+
const packageJson = asRecord(candidate);
|
|
11560
|
+
if (!packageJson) {
|
|
11561
|
+
return void 0;
|
|
11562
|
+
}
|
|
11563
|
+
const packageName = typeof packageJson.name === "string" ? packageJson.name.trim() : void 0;
|
|
11564
|
+
const packageVersion = typeof packageJson.version === "string" ? packageJson.version.trim() : void 0;
|
|
11565
|
+
if (!packageName || !packageVersion) {
|
|
11566
|
+
return void 0;
|
|
11567
|
+
}
|
|
11568
|
+
return {
|
|
11569
|
+
packageName,
|
|
11570
|
+
packageVersion
|
|
11571
|
+
};
|
|
11572
|
+
}
|
|
11573
|
+
function resolvePackageMetadata(loadJson, packageJsonCandidates = defaultPackageJsonCandidates) {
|
|
11574
|
+
for (const candidatePath of packageJsonCandidates) {
|
|
11575
|
+
try {
|
|
11576
|
+
const metadata = normalizeMetadata(loadJson(candidatePath));
|
|
11577
|
+
if (metadata) {
|
|
11578
|
+
return metadata;
|
|
11579
|
+
}
|
|
11580
|
+
} catch {
|
|
11581
|
+
continue;
|
|
11582
|
+
}
|
|
11583
|
+
}
|
|
11584
|
+
return {
|
|
11585
|
+
packageName: DEFAULT_PACKAGE_NAME,
|
|
11586
|
+
packageVersion: DEFAULT_PACKAGE_VERSION
|
|
11587
|
+
};
|
|
11588
|
+
}
|
|
11589
|
+
function loadPackageMetadataFromRuntime() {
|
|
11590
|
+
const require3 = createRequire2(import.meta.url);
|
|
11591
|
+
return resolvePackageMetadata((candidatePath) => require3(candidatePath));
|
|
11592
|
+
}
|
|
11593
|
+
|
|
10365
11594
|
// src/pricing/cost-engine.ts
|
|
10366
11595
|
var ONE_MILLION = 1e6;
|
|
10367
11596
|
function estimateTokenGroupCost(tokens, per1MUsd) {
|
|
@@ -10472,7 +11701,6 @@ function applyPricingToEvents(events, pricingSource) {
|
|
|
10472
11701
|
}
|
|
10473
11702
|
|
|
10474
11703
|
// src/pricing/pricing-override-source.ts
|
|
10475
|
-
import { readFile as readFile7 } from "fs/promises";
|
|
10476
11704
|
var requiredRateKeys = ["inputPer1MUsd", "outputPer1MUsd"];
|
|
10477
11705
|
var optionalRateKeys = ["cacheReadPer1MUsd", "cacheWritePer1MUsd", "reasoningPer1MUsd"];
|
|
10478
11706
|
var knownOverrideKeys = /* @__PURE__ */ new Set([
|
|
@@ -10572,7 +11800,7 @@ function normalizeOverrideFile(payload) {
|
|
|
10572
11800
|
return overrides;
|
|
10573
11801
|
}
|
|
10574
11802
|
async function loadPricingOverrides(filePath) {
|
|
10575
|
-
const fileContents = await
|
|
11803
|
+
const fileContents = await readRegularTextFile(filePath);
|
|
10576
11804
|
const parsed = JSON.parse(fileContents);
|
|
10577
11805
|
return normalizeOverrideFile(parsed);
|
|
10578
11806
|
}
|
|
@@ -10599,8 +11827,8 @@ var PricingOverrideSource = class {
|
|
|
10599
11827
|
};
|
|
10600
11828
|
|
|
10601
11829
|
// src/pricing/litellm-pricing-fetcher.ts
|
|
10602
|
-
import
|
|
10603
|
-
import
|
|
11830
|
+
import path33 from "path";
|
|
11831
|
+
import { setTimeout as sleep3 } from "timers/promises";
|
|
10604
11832
|
|
|
10605
11833
|
// src/pricing/litellm-model-map.json
|
|
10606
11834
|
var litellm_model_map_default = {
|
|
@@ -29366,11 +30594,6 @@ function isRetryableFetchFailure2(error) {
|
|
|
29366
30594
|
}
|
|
29367
30595
|
return /timeout|timed out|network|econn|enotfound|eai_again/iu.test(error.message);
|
|
29368
30596
|
}
|
|
29369
|
-
async function sleep3(delayMs) {
|
|
29370
|
-
await new Promise((resolve) => {
|
|
29371
|
-
setTimeout(resolve, delayMs);
|
|
29372
|
-
});
|
|
29373
|
-
}
|
|
29374
30597
|
var bundledLiteLLMPricingSnapshot;
|
|
29375
30598
|
function toNonNegativeNumber3(value) {
|
|
29376
30599
|
if (typeof value === "number") {
|
|
@@ -29531,7 +30754,7 @@ function formatStaleCacheWarning(fetchedAt, fetchError) {
|
|
|
29531
30754
|
return `Pricing: the LiteLLM refresh failed (${fetchError}); using cached pricing from ${fetchedDate}.`;
|
|
29532
30755
|
}
|
|
29533
30756
|
function getDefaultLiteLLMPricingCachePath() {
|
|
29534
|
-
return
|
|
30757
|
+
return path33.join(getUserCacheRootDir(), "llm-usage-metrics", "litellm-pricing-cache.json");
|
|
29535
30758
|
}
|
|
29536
30759
|
var LiteLLMPricingFetcher = class {
|
|
29537
30760
|
sourceUrl;
|
|
@@ -29664,11 +30887,11 @@ var LiteLLMPricingFetcher = class {
|
|
|
29664
30887
|
);
|
|
29665
30888
|
}
|
|
29666
30889
|
if (!response.body) {
|
|
29667
|
-
const
|
|
29668
|
-
if (Buffer.byteLength(
|
|
30890
|
+
const text2 = await response.text();
|
|
30891
|
+
if (Buffer.byteLength(text2, "utf8") > this.maxResponseBytes) {
|
|
29669
30892
|
throw new Error(`LiteLLM pricing response exceeds ${this.maxResponseBytes} bytes`);
|
|
29670
30893
|
}
|
|
29671
|
-
return JSON.parse(
|
|
30894
|
+
return JSON.parse(text2);
|
|
29672
30895
|
}
|
|
29673
30896
|
const chunks = [];
|
|
29674
30897
|
let receivedBytes = 0;
|
|
@@ -29759,7 +30982,7 @@ var LiteLLMPricingFetcher = class {
|
|
|
29759
30982
|
async readCachePayload() {
|
|
29760
30983
|
let content;
|
|
29761
30984
|
try {
|
|
29762
|
-
content = await
|
|
30985
|
+
content = await readRegularTextFile(this.cacheFilePath);
|
|
29763
30986
|
} catch {
|
|
29764
30987
|
return void 0;
|
|
29765
30988
|
}
|
|
@@ -29772,8 +30995,8 @@ var LiteLLMPricingFetcher = class {
|
|
|
29772
30995
|
return normalizeLiteLLMCachePayload(parsedPayload);
|
|
29773
30996
|
}
|
|
29774
30997
|
async writeCache() {
|
|
29775
|
-
const directoryPath =
|
|
29776
|
-
await
|
|
30998
|
+
const directoryPath = path33.dirname(this.cacheFilePath);
|
|
30999
|
+
await ensureDirectory(directoryPath);
|
|
29777
31000
|
const payload = {
|
|
29778
31001
|
fetchedAt: this.now(),
|
|
29779
31002
|
sourceUrl: this.sourceUrl,
|
|
@@ -30199,6 +31422,26 @@ function appendHistoryEvents(parseResults, historyEvents) {
|
|
|
30199
31422
|
};
|
|
30200
31423
|
});
|
|
30201
31424
|
}
|
|
31425
|
+
function appendMachineEvents(parseResults, machineEvents) {
|
|
31426
|
+
const results = parseResults.map((result) => ({ ...result, events: [...result.events] }));
|
|
31427
|
+
const resultsBySource = new Map(results.map((result) => [result.source, result]));
|
|
31428
|
+
for (const event of machineEvents) {
|
|
31429
|
+
let result = resultsBySource.get(event.source);
|
|
31430
|
+
if (!result) {
|
|
31431
|
+
result = {
|
|
31432
|
+
source: event.source,
|
|
31433
|
+
events: [],
|
|
31434
|
+
filesFound: 0,
|
|
31435
|
+
skippedRows: 0,
|
|
31436
|
+
skippedRowReasons: []
|
|
31437
|
+
};
|
|
31438
|
+
resultsBySource.set(event.source, result);
|
|
31439
|
+
results.push(result);
|
|
31440
|
+
}
|
|
31441
|
+
result.events.push(event);
|
|
31442
|
+
}
|
|
31443
|
+
return results;
|
|
31444
|
+
}
|
|
30202
31445
|
async function buildUsageEventDataset(options, deps = {}) {
|
|
30203
31446
|
const userConfigResolution = await resolveUserConfigForOptions(options, deps);
|
|
30204
31447
|
const configuredOptions = userConfigResolution.options;
|
|
@@ -30221,7 +31464,9 @@ async function buildUsageEventDataset(options, deps = {}) {
|
|
|
30221
31464
|
]);
|
|
30222
31465
|
const historyRequested = configuredOptions.history === true;
|
|
30223
31466
|
const cliDirectorySourceIds = resolveCliDirectorySourceIds(userConfigResolution.cliOptions);
|
|
30224
|
-
const
|
|
31467
|
+
const machineSelection = selectMachines(config.machines, normalizedInputs.machineFilter, {
|
|
31468
|
+
customSourceDirectories: cliDirectorySourceIds.size > 0
|
|
31469
|
+
});
|
|
30225
31470
|
if (historyRequested && !eventStoreRuntimeConfig.enabled) {
|
|
30226
31471
|
throw new Error(
|
|
30227
31472
|
eventStoreRuntimeConfig.disabledBy === "environment" ? "--history requires the event store (unset LLM_USAGE_EVENT_STORE=0)" : "--history requires the event store (set eventStore.enabled = true in config.toml)"
|
|
@@ -30248,6 +31493,13 @@ async function buildUsageEventDataset(options, deps = {}) {
|
|
|
30248
31493
|
providerFilter: normalizedInputs.providerFilter,
|
|
30249
31494
|
modelFilter: normalizedInputs.modelFilter
|
|
30250
31495
|
});
|
|
31496
|
+
const nowMs = () => (deps.now?.() ?? /* @__PURE__ */ new Date()).getTime();
|
|
31497
|
+
const machineRefreshAbort = new AbortController();
|
|
31498
|
+
const machineRefresh = configuredOptions.sync === false ? Promise.resolve([]) : refreshDueMachines(
|
|
31499
|
+
Object.entries(config.machines ?? {}).filter(([name]) => machineSelection.names.includes(name)).map(([name, machine]) => ({ name, machine })),
|
|
31500
|
+
{ now: nowMs, spawnSsh: deps.spawnSsh, signal: machineRefreshAbort.signal }
|
|
31501
|
+
);
|
|
31502
|
+
const includeHistory = configuredOptions.history !== false;
|
|
30251
31503
|
let openedEventStore;
|
|
30252
31504
|
let dataset;
|
|
30253
31505
|
try {
|
|
@@ -30255,6 +31507,7 @@ async function buildUsageEventDataset(options, deps = {}) {
|
|
|
30255
31507
|
const {
|
|
30256
31508
|
successfulParseResults,
|
|
30257
31509
|
discoveredFiles,
|
|
31510
|
+
parsedFiles,
|
|
30258
31511
|
eventStoreAvailable,
|
|
30259
31512
|
sourceFailures,
|
|
30260
31513
|
warnings
|
|
@@ -30297,6 +31550,7 @@ async function buildUsageEventDataset(options, deps = {}) {
|
|
|
30297
31550
|
const parseWarnings = eventStoreOpenWarning ? [eventStoreOpenWarning, ...warnings] : warnings;
|
|
30298
31551
|
throwOnExplicitSourceFailures(sourceFailures, normalizedInputs.explicitSourceIds);
|
|
30299
31552
|
let parseResultsForFiltering = successfulParseResults;
|
|
31553
|
+
const ledger = eventStoreAvailable ? { path: eventStoreRuntimeConfig.path, countedFiles: [...parsedFiles] } : void 0;
|
|
30300
31554
|
const historyWarnings = [];
|
|
30301
31555
|
const historyNotes = [];
|
|
30302
31556
|
const historyStore = openedEventStore;
|
|
@@ -30325,6 +31579,7 @@ async function buildUsageEventDataset(options, deps = {}) {
|
|
|
30325
31579
|
parseResultsForFiltering,
|
|
30326
31580
|
historyResult.events
|
|
30327
31581
|
);
|
|
31582
|
+
ledger?.countedFiles.push(...historyResult.servedFiles);
|
|
30328
31583
|
if (historyRequested || historyResult.servedFileCount > 0) {
|
|
30329
31584
|
historyNotes.push(formatHistoryNote(historyResult));
|
|
30330
31585
|
}
|
|
@@ -30332,6 +31587,56 @@ async function buildUsageEventDataset(options, deps = {}) {
|
|
|
30332
31587
|
historyWarnings.push(`Event store disabled after failure: ${getErrorReason(error)}`);
|
|
30333
31588
|
}
|
|
30334
31589
|
}
|
|
31590
|
+
const machineWarnings = [];
|
|
31591
|
+
const machineNotes = [];
|
|
31592
|
+
if (!machineSelection.includeLocal) {
|
|
31593
|
+
parseResultsForFiltering = parseResultsForFiltering.map((result) => ({
|
|
31594
|
+
...result,
|
|
31595
|
+
events: []
|
|
31596
|
+
}));
|
|
31597
|
+
}
|
|
31598
|
+
const refreshOutcomes = await measureRuntimeProfileStage(
|
|
31599
|
+
runtimeProfile,
|
|
31600
|
+
"usage.dataset.machine_refresh",
|
|
31601
|
+
() => machineRefresh
|
|
31602
|
+
);
|
|
31603
|
+
for (const outcome of refreshOutcomes) {
|
|
31604
|
+
if (outcome.ok) {
|
|
31605
|
+
machineWarnings.push(
|
|
31606
|
+
...outcome.remoteWarnings.map((warning) => `${outcome.name} warned: ${warning}`)
|
|
31607
|
+
);
|
|
31608
|
+
continue;
|
|
31609
|
+
}
|
|
31610
|
+
const failure = formatRefreshFailure(outcome, nowMs());
|
|
31611
|
+
if (failure) {
|
|
31612
|
+
(failure.stale ? machineWarnings : machineNotes).push(failure.text);
|
|
31613
|
+
}
|
|
31614
|
+
}
|
|
31615
|
+
if (machineSelection.names.length > 0) {
|
|
31616
|
+
const machineUsage = await measureRuntimeProfileStage(
|
|
31617
|
+
runtimeProfile,
|
|
31618
|
+
"usage.dataset.machines",
|
|
31619
|
+
() => loadMachineUsage({
|
|
31620
|
+
names: machineSelection.names,
|
|
31621
|
+
disabledNames: new Set(
|
|
31622
|
+
machineSelection.names.filter((name) => config.machines?.[name]?.enabled === false)
|
|
31623
|
+
),
|
|
31624
|
+
servedEvents: parseResultsForFiltering.flatMap((result) => result.events),
|
|
31625
|
+
sources: new Set(adaptersToParse.map((adapter) => adapter.id)),
|
|
31626
|
+
since: configuredOptions.since,
|
|
31627
|
+
until: configuredOptions.until
|
|
31628
|
+
})
|
|
31629
|
+
);
|
|
31630
|
+
parseResultsForFiltering = appendMachineEvents(parseResultsForFiltering, machineUsage.events);
|
|
31631
|
+
machineWarnings.push(...machineUsage.warnings);
|
|
31632
|
+
machineNotes.push(
|
|
31633
|
+
formatMachinesNote(
|
|
31634
|
+
machineUsage.machines,
|
|
31635
|
+
nowMs(),
|
|
31636
|
+
loadPackageMetadataFromRuntime().packageVersion
|
|
31637
|
+
)
|
|
31638
|
+
);
|
|
31639
|
+
}
|
|
30335
31640
|
const filteredEvents = measureRuntimeProfileStageSync(
|
|
30336
31641
|
runtimeProfile,
|
|
30337
31642
|
"usage.dataset.filter_events",
|
|
@@ -30354,6 +31659,7 @@ async function buildUsageEventDataset(options, deps = {}) {
|
|
|
30354
31659
|
...userConfigResolution.loadedConfig.warnings,
|
|
30355
31660
|
...parseWarnings,
|
|
30356
31661
|
...historyWarnings,
|
|
31662
|
+
...machineWarnings,
|
|
30357
31663
|
...findUnmatchedFilterWarnings({
|
|
30358
31664
|
parseResults: parseResultsForFiltering,
|
|
30359
31665
|
// A `sources` list from config.toml is a standing default, not a typed filter.
|
|
@@ -30367,13 +31673,15 @@ async function buildUsageEventDataset(options, deps = {}) {
|
|
|
30367
31673
|
until: configuredOptions.until
|
|
30368
31674
|
})
|
|
30369
31675
|
],
|
|
30370
|
-
notes: historyNotes,
|
|
31676
|
+
notes: [...historyNotes, ...machineNotes],
|
|
30371
31677
|
filteredEvents,
|
|
31678
|
+
ledger,
|
|
30372
31679
|
pricingRuntimeConfig,
|
|
30373
31680
|
readEnvVarOverrides: deps.getActiveEnvVarOverrides ?? getActiveEnvVarOverrides
|
|
30374
31681
|
};
|
|
30375
31682
|
return dataset;
|
|
30376
31683
|
} finally {
|
|
31684
|
+
machineRefreshAbort.abort();
|
|
30377
31685
|
if (openedEventStore) {
|
|
30378
31686
|
try {
|
|
30379
31687
|
const closeStore = deps.closeEventStore ?? closeEventStore;
|
|
@@ -30465,7 +31773,8 @@ async function buildUsageData(granularity, commandOptions, commandDeps = {}) {
|
|
|
30465
31773
|
() => aggregateUsage(pricedEvents, {
|
|
30466
31774
|
granularity,
|
|
30467
31775
|
timezone: dataset.normalizedInputs.timezone,
|
|
30468
|
-
sourceOrder: dataset.adaptersToParse.map((adapter) => adapter.id)
|
|
31776
|
+
sourceOrder: dataset.adaptersToParse.map((adapter) => adapter.id),
|
|
31777
|
+
byMachine: options.byMachine === true
|
|
30469
31778
|
})
|
|
30470
31779
|
);
|
|
30471
31780
|
const diagnostics = buildUsageDiagnostics({
|
|
@@ -30767,7 +32076,7 @@ var defaultTerminalStylePalette = {
|
|
|
30767
32076
|
bold: pc3.bold,
|
|
30768
32077
|
dim: pc3.dim
|
|
30769
32078
|
};
|
|
30770
|
-
var passthroughStyler = (
|
|
32079
|
+
var passthroughStyler = (text2) => text2;
|
|
30771
32080
|
var periodColumnIndex = 0;
|
|
30772
32081
|
var sourceColumnIndex = 1;
|
|
30773
32082
|
var modelsColumnIndex = 2;
|
|
@@ -30780,7 +32089,7 @@ function styleModelsCell(cell, palette, options = {}) {
|
|
|
30780
32089
|
const emphasizePrimaryWhenSingleLine = options.emphasizePrimaryWhenSingleLine ?? false;
|
|
30781
32090
|
const primaryStyler = options.primaryStyler ?? palette.bold;
|
|
30782
32091
|
const secondaryStyler = options.secondaryStyler ?? passthroughStyler;
|
|
30783
|
-
const totalStyler = options.totalStyler ?? ((
|
|
32092
|
+
const totalStyler = options.totalStyler ?? ((text2) => palette.bold(palette.green(text2)));
|
|
30784
32093
|
const lines = splitCellLines(cell);
|
|
30785
32094
|
const modelEntryCount = lines.reduce((count, line) => {
|
|
30786
32095
|
if (line === "\u03A3 TOTAL") {
|
|
@@ -31010,7 +32319,7 @@ function formatSource(row) {
|
|
|
31010
32319
|
if (row.rowType === "grand_total") {
|
|
31011
32320
|
return "TOTAL";
|
|
31012
32321
|
}
|
|
31013
|
-
return row.source;
|
|
32322
|
+
return row.rowType === "period_source" && row.machine !== void 0 ? `${row.source} (${row.machine})` : row.source;
|
|
31014
32323
|
}
|
|
31015
32324
|
function formatFullTokenCount(value) {
|
|
31016
32325
|
return integerFormatter.format(value ?? 0);
|
|
@@ -31810,6 +33119,22 @@ function renderCompareReport(compareData, format, options = {}) {
|
|
|
31810
33119
|
}
|
|
31811
33120
|
}
|
|
31812
33121
|
|
|
33122
|
+
// src/render/month-labels.ts
|
|
33123
|
+
var MONTH_LABELS = [
|
|
33124
|
+
"Jan",
|
|
33125
|
+
"Feb",
|
|
33126
|
+
"Mar",
|
|
33127
|
+
"Apr",
|
|
33128
|
+
"May",
|
|
33129
|
+
"Jun",
|
|
33130
|
+
"Jul",
|
|
33131
|
+
"Aug",
|
|
33132
|
+
"Sep",
|
|
33133
|
+
"Oct",
|
|
33134
|
+
"Nov",
|
|
33135
|
+
"Dec"
|
|
33136
|
+
];
|
|
33137
|
+
|
|
31813
33138
|
// src/render/share-svg-theme.ts
|
|
31814
33139
|
var shareThemes = {
|
|
31815
33140
|
dark: {
|
|
@@ -31959,20 +33284,6 @@ function renderEmptyState(theme, message) {
|
|
|
31959
33284
|
attributes: 'data-empty-state="true"'
|
|
31960
33285
|
});
|
|
31961
33286
|
}
|
|
31962
|
-
var MONTH_LABELS = [
|
|
31963
|
-
"Jan",
|
|
31964
|
-
"Feb",
|
|
31965
|
-
"Mar",
|
|
31966
|
-
"Apr",
|
|
31967
|
-
"May",
|
|
31968
|
-
"Jun",
|
|
31969
|
-
"Jul",
|
|
31970
|
-
"Aug",
|
|
31971
|
-
"Sep",
|
|
31972
|
-
"Oct",
|
|
31973
|
-
"Nov",
|
|
31974
|
-
"Dec"
|
|
31975
|
-
];
|
|
31976
33287
|
var WEEKDAY_LABELS = [
|
|
31977
33288
|
{ row: 0, label: "Mon" },
|
|
31978
33289
|
{ row: 2, label: "Wed" },
|
|
@@ -32074,6 +33385,26 @@ function scaleY(value, max, top, bottom) {
|
|
|
32074
33385
|
if (max <= 0) return bottom;
|
|
32075
33386
|
return bottom - Math.max(0, value) / max * (bottom - top);
|
|
32076
33387
|
}
|
|
33388
|
+
function renderValueGridLines(scaleMax, chart, formatValue, theme) {
|
|
33389
|
+
const lines = [];
|
|
33390
|
+
for (let step = 1; step <= 3; step += 1) {
|
|
33391
|
+
const value = scaleMax / 3 * step;
|
|
33392
|
+
const y = scaleY(value, scaleMax, chart.top, chart.bottom);
|
|
33393
|
+
lines.push(
|
|
33394
|
+
`<line x1="${chart.left}" y1="${y.toFixed(2)}" x2="${chart.right}" y2="${y.toFixed(2)}" stroke="${theme.line}" stroke-width="1" stroke-dasharray="3 5"/>`,
|
|
33395
|
+
svgText(chart.left - 10, y + 4, formatValue(value), {
|
|
33396
|
+
size: 12,
|
|
33397
|
+
fill: theme.textMuted,
|
|
33398
|
+
mono: true,
|
|
33399
|
+
anchor: "end"
|
|
33400
|
+
})
|
|
33401
|
+
);
|
|
33402
|
+
}
|
|
33403
|
+
lines.push(
|
|
33404
|
+
`<line x1="${chart.left}" y1="${chart.bottom}" x2="${chart.right}" y2="${chart.bottom}" stroke="${theme.line}" stroke-width="1"/>`
|
|
33405
|
+
);
|
|
33406
|
+
return lines.join("\n");
|
|
33407
|
+
}
|
|
32077
33408
|
|
|
32078
33409
|
// src/render/render-compare-share-svg.ts
|
|
32079
33410
|
var right = SHARE_WIDTH - SHARE_MARGIN;
|
|
@@ -32666,8 +33997,9 @@ function formatSkippedEntries(split) {
|
|
|
32666
33997
|
}
|
|
32667
33998
|
function formatSummaryLine(diagnostics, totalFiles) {
|
|
32668
33999
|
const sourcesWithFiles = diagnostics.sessionStats.filter((session) => session.filesFound > 0).sort((left, right4) => right4.filesFound - left.filesFound);
|
|
34000
|
+
const fileCounts = sourcesWithFiles.map((session) => `${session.source} ${integerFormatter3.format(session.filesFound)}`).join(", ");
|
|
32669
34001
|
const parts = [
|
|
32670
|
-
`Scanned ${pluralize(totalFiles, "file")} (${
|
|
34002
|
+
totalFiles === 0 ? "Scanned no local files" : `Scanned ${pluralize(totalFiles, "file")} (${fileCounts})`
|
|
32671
34003
|
];
|
|
32672
34004
|
const pricingLabel = pricingLabels[diagnostics.pricingOrigin];
|
|
32673
34005
|
if (pricingLabel) {
|
|
@@ -32687,7 +34019,8 @@ function formatSettingsParts(diagnostics) {
|
|
|
32687
34019
|
}
|
|
32688
34020
|
function emitDiagnostics(diagnostics, diagnosticsLogger = logger) {
|
|
32689
34021
|
const totalFiles = diagnostics.sessionStats.reduce((sum, session) => sum + session.filesFound, 0);
|
|
32690
|
-
|
|
34022
|
+
const hasEvents = diagnostics.sessionStats.some((session) => session.eventsParsed > 0);
|
|
34023
|
+
if (totalFiles > 0 || hasEvents) {
|
|
32691
34024
|
diagnosticsLogger.info(formatSummaryLine(diagnostics, totalFiles));
|
|
32692
34025
|
} else {
|
|
32693
34026
|
const settings = formatSettingsParts(diagnostics);
|
|
@@ -32914,12 +34247,12 @@ ${options.svgs.light}
|
|
|
32914
34247
|
}
|
|
32915
34248
|
|
|
32916
34249
|
// src/cli/share-artifact.ts
|
|
32917
|
-
import { spawn } from "child_process";
|
|
34250
|
+
import { spawn as spawn2 } from "child_process";
|
|
32918
34251
|
import { constants as constants2 } from "fs";
|
|
32919
34252
|
import { access as access2, writeFile as writeFile3 } from "fs/promises";
|
|
32920
|
-
import
|
|
34253
|
+
import path34 from "path";
|
|
32921
34254
|
async function writeShareFile(fileName, content) {
|
|
32922
|
-
const outputPath =
|
|
34255
|
+
const outputPath = path34.resolve(process.cwd(), fileName);
|
|
32923
34256
|
await writeFile3(outputPath, content, "utf8");
|
|
32924
34257
|
return outputPath;
|
|
32925
34258
|
}
|
|
@@ -32941,10 +34274,10 @@ async function resolveBinaryPath(primaryPath, fallbackNames) {
|
|
|
32941
34274
|
if (await fileExists(primaryPath)) {
|
|
32942
34275
|
return primaryPath;
|
|
32943
34276
|
}
|
|
32944
|
-
const pathDirs = (process.env.PATH ?? "").split(
|
|
34277
|
+
const pathDirs = (process.env.PATH ?? "").split(path34.delimiter).filter(Boolean);
|
|
32945
34278
|
for (const fallbackName of fallbackNames) {
|
|
32946
34279
|
for (const dir of pathDirs) {
|
|
32947
|
-
const candidate =
|
|
34280
|
+
const candidate = path34.join(dir, fallbackName);
|
|
32948
34281
|
if (await fileExists(candidate)) {
|
|
32949
34282
|
return candidate;
|
|
32950
34283
|
}
|
|
@@ -32990,7 +34323,7 @@ async function resolveOpenCommand(filePath, platform) {
|
|
|
32990
34323
|
}
|
|
32991
34324
|
async function spawnDetached(command, args) {
|
|
32992
34325
|
await new Promise((resolve, reject) => {
|
|
32993
|
-
const child =
|
|
34326
|
+
const child = spawn2(command, args, {
|
|
32994
34327
|
detached: true,
|
|
32995
34328
|
stdio: "ignore",
|
|
32996
34329
|
windowsHide: true
|
|
@@ -33223,7 +34556,7 @@ function renderSummaryStats(allRow, theme) {
|
|
|
33223
34556
|
}
|
|
33224
34557
|
];
|
|
33225
34558
|
return stats.map(
|
|
33226
|
-
(
|
|
34559
|
+
(stat10, index) => renderStat({ theme, x: SHARE_MARGIN + index * statPitch, y: statsTop, size: 30, ...stat10 })
|
|
33227
34560
|
).join("\n");
|
|
33228
34561
|
}
|
|
33229
34562
|
function renderPanelLabel(theme, panel, label) {
|
|
@@ -33828,9 +35161,9 @@ function aggregateEfficiency(options) {
|
|
|
33828
35161
|
}
|
|
33829
35162
|
|
|
33830
35163
|
// src/efficiency/git-outcome-collector.ts
|
|
33831
|
-
import { spawn as
|
|
33832
|
-
import { createInterface } from "readline";
|
|
33833
|
-
import
|
|
35164
|
+
import { spawn as spawn3 } from "child_process";
|
|
35165
|
+
import { createInterface as createInterface2 } from "readline";
|
|
35166
|
+
import path35 from "path";
|
|
33834
35167
|
import { stat as stat6 } from "fs/promises";
|
|
33835
35168
|
var GIT_COMMIT_MARKER = "";
|
|
33836
35169
|
var SHORTSTAT_PATTERN = /(?<!\d)\d+\s+files?\s+changed(?:,\s+(\d+)\s+insertions?\(\+\))?(?:,\s+(\d+)\s+deletions?\(-\))?/u;
|
|
@@ -33850,13 +35183,13 @@ function resolveGitCommandFailureReason(result) {
|
|
|
33850
35183
|
}
|
|
33851
35184
|
function resolveRepoDir(repoDir) {
|
|
33852
35185
|
if (repoDir === void 0) {
|
|
33853
|
-
return
|
|
35186
|
+
return path35.resolve(process.cwd());
|
|
33854
35187
|
}
|
|
33855
35188
|
const normalizedRepoDir = repoDir.trim();
|
|
33856
35189
|
if (!normalizedRepoDir) {
|
|
33857
35190
|
throw new Error("--repo-dir must be a non-empty path");
|
|
33858
35191
|
}
|
|
33859
|
-
return
|
|
35192
|
+
return path35.resolve(normalizedRepoDir);
|
|
33860
35193
|
}
|
|
33861
35194
|
function getNodeErrorCode2(error) {
|
|
33862
35195
|
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
@@ -34032,7 +35365,7 @@ function parseGitLogShortstatLines(lines, authorEmail) {
|
|
|
34032
35365
|
}
|
|
34033
35366
|
async function runGitCommand(repoDir, args) {
|
|
34034
35367
|
return await new Promise((resolve, reject) => {
|
|
34035
|
-
const child =
|
|
35368
|
+
const child = spawn3("git", args, {
|
|
34036
35369
|
cwd: repoDir,
|
|
34037
35370
|
env: {
|
|
34038
35371
|
...process.env,
|
|
@@ -34043,7 +35376,7 @@ async function runGitCommand(repoDir, args) {
|
|
|
34043
35376
|
});
|
|
34044
35377
|
const lines = [];
|
|
34045
35378
|
let stderr = "";
|
|
34046
|
-
const stdoutReader =
|
|
35379
|
+
const stdoutReader = createInterface2({ input: child.stdout });
|
|
34047
35380
|
const stdoutPromise = (async () => {
|
|
34048
35381
|
for await (const line of stdoutReader) {
|
|
34049
35382
|
lines.push(line);
|
|
@@ -34237,21 +35570,21 @@ async function collectGitOutcomes(options, deps = {}) {
|
|
|
34237
35570
|
|
|
34238
35571
|
// src/efficiency/repo-attribution.ts
|
|
34239
35572
|
import { access as access3, constants as constants3, realpath as realpath3 } from "fs/promises";
|
|
34240
|
-
import
|
|
35573
|
+
import path36 from "path";
|
|
34241
35574
|
async function hasGitMarker(directoryPath) {
|
|
34242
35575
|
try {
|
|
34243
|
-
await access3(
|
|
35576
|
+
await access3(path36.join(directoryPath, ".git"), constants3.F_OK);
|
|
34244
35577
|
return true;
|
|
34245
35578
|
} catch {
|
|
34246
35579
|
return false;
|
|
34247
35580
|
}
|
|
34248
35581
|
}
|
|
34249
35582
|
function normalizeComparablePath(value) {
|
|
34250
|
-
const normalizedPath =
|
|
35583
|
+
const normalizedPath = path36.normalize(path36.resolve(value));
|
|
34251
35584
|
return process.platform === "win32" ? normalizedPath.toLowerCase() : normalizedPath;
|
|
34252
35585
|
}
|
|
34253
35586
|
async function resolveComparablePath(value) {
|
|
34254
|
-
const resolvedPath =
|
|
35587
|
+
const resolvedPath = path36.resolve(value);
|
|
34255
35588
|
try {
|
|
34256
35589
|
return normalizeComparablePath(await realpath3(resolvedPath));
|
|
34257
35590
|
} catch {
|
|
@@ -34263,12 +35596,12 @@ async function resolveRepoRootFromPathHint(pathHint) {
|
|
|
34263
35596
|
if (!trimmedPath) {
|
|
34264
35597
|
return void 0;
|
|
34265
35598
|
}
|
|
34266
|
-
let currentPath =
|
|
35599
|
+
let currentPath = path36.resolve(trimmedPath);
|
|
34267
35600
|
for (; ; ) {
|
|
34268
35601
|
if (await hasGitMarker(currentPath)) {
|
|
34269
35602
|
return currentPath;
|
|
34270
35603
|
}
|
|
34271
|
-
const parentPath =
|
|
35604
|
+
const parentPath = path36.dirname(currentPath);
|
|
34272
35605
|
if (parentPath === currentPath) {
|
|
34273
35606
|
return void 0;
|
|
34274
35607
|
}
|
|
@@ -35381,7 +36714,7 @@ async function runOptimizeReport(granularity, options) {
|
|
|
35381
36714
|
}
|
|
35382
36715
|
|
|
35383
36716
|
// src/render/render-session-report.ts
|
|
35384
|
-
import
|
|
36717
|
+
import path37 from "path";
|
|
35385
36718
|
import { markdownTable as markdownTable4 } from "markdown-table";
|
|
35386
36719
|
|
|
35387
36720
|
// src/render/format-duration.ts
|
|
@@ -35450,7 +36783,7 @@ function toSessionTableCells(row, options) {
|
|
|
35450
36783
|
return [
|
|
35451
36784
|
options.truncateSessionIds ? truncateSessionId(row.sessionId) : row.sessionId,
|
|
35452
36785
|
row.source,
|
|
35453
|
-
row.repoRoot ?
|
|
36786
|
+
row.repoRoot ? path37.basename(row.repoRoot) : "-",
|
|
35454
36787
|
getPeriodKey(row.lastActivity, "daily", options.timezone),
|
|
35455
36788
|
formatDuration(row.durationMs),
|
|
35456
36789
|
formatInteger4(row.totalTokens),
|
|
@@ -35460,7 +36793,7 @@ function toSessionTableCells(row, options) {
|
|
|
35460
36793
|
}
|
|
35461
36794
|
function toRepoTableCells(row, timezone) {
|
|
35462
36795
|
return [
|
|
35463
|
-
row.repoRoot ?
|
|
36796
|
+
row.repoRoot ? path37.basename(row.repoRoot) : "(no repo)",
|
|
35464
36797
|
formatInteger4(row.sessionCount),
|
|
35465
36798
|
getPeriodKey(row.lastActivity, "daily", timezone),
|
|
35466
36799
|
formatInteger4(row.totalTokens),
|
|
@@ -35884,20 +37217,6 @@ var LEVEL_GLYPHS = ["\xB7", "\u2591", "\u2592", "\u2593", "\u2588"];
|
|
|
35884
37217
|
var LEVELS = [0, 1, 2, 3, 4];
|
|
35885
37218
|
var ROW_LABELS = ["Mon", "", "Wed", "", "Fri", "", ""];
|
|
35886
37219
|
var ROW_LABEL_WIDTH = 4;
|
|
35887
|
-
var MONTH_LABELS2 = [
|
|
35888
|
-
"Jan",
|
|
35889
|
-
"Feb",
|
|
35890
|
-
"Mar",
|
|
35891
|
-
"Apr",
|
|
35892
|
-
"May",
|
|
35893
|
-
"Jun",
|
|
35894
|
-
"Jul",
|
|
35895
|
-
"Aug",
|
|
35896
|
-
"Sep",
|
|
35897
|
-
"Oct",
|
|
35898
|
-
"Nov",
|
|
35899
|
-
"Dec"
|
|
35900
|
-
];
|
|
35901
37220
|
var MONTH_LABEL_WIDTH = 3;
|
|
35902
37221
|
var MIN_HEATMAP_WEEKS = 13;
|
|
35903
37222
|
var LEGEND_WIDTH = "Less ".length + LEVEL_GLYPHS.length + " More".length;
|
|
@@ -35917,7 +37236,7 @@ function renderMonthLabels(weeks) {
|
|
|
35917
37236
|
weeks.forEach((week, column) => {
|
|
35918
37237
|
const month = week[0].date.slice(5, 7);
|
|
35919
37238
|
if (month !== previousMonth && (line.length === 0 || line.length < column) && column + MONTH_LABEL_WIDTH <= weeks.length) {
|
|
35920
|
-
line = line.padEnd(column) +
|
|
37239
|
+
line = line.padEnd(column) + MONTH_LABELS[Number(month) - 1];
|
|
35921
37240
|
}
|
|
35922
37241
|
previousMonth = month;
|
|
35923
37242
|
});
|
|
@@ -36011,12 +37330,12 @@ function toActivityStats(activity) {
|
|
|
36011
37330
|
}
|
|
36012
37331
|
function renderTerminalActivity(activity, options, useColor) {
|
|
36013
37332
|
const palette = options.palette ?? defaultTerminalStylePalette;
|
|
36014
|
-
const paint = (styler) => (
|
|
37333
|
+
const paint = (styler) => (text2) => useColor ? styler(text2) : text2;
|
|
36015
37334
|
const dim = paint(palette.dim);
|
|
36016
37335
|
const stats = toActivityStats(activity);
|
|
36017
37336
|
const labelWidth = Math.max(...stats.map(([label]) => label.length));
|
|
36018
37337
|
const valueStylers = [
|
|
36019
|
-
paint((
|
|
37338
|
+
paint((text2) => palette.bold(palette.yellow(text2))),
|
|
36020
37339
|
paint(palette.yellow),
|
|
36021
37340
|
paint(palette.white),
|
|
36022
37341
|
paint(palette.white)
|
|
@@ -36025,7 +37344,7 @@ function renderTerminalActivity(activity, options, useColor) {
|
|
|
36025
37344
|
activity.days,
|
|
36026
37345
|
{
|
|
36027
37346
|
dim,
|
|
36028
|
-
level: (level) => level === 0 ? dim : level >= 3 ? paint((
|
|
37347
|
+
level: (level) => level === 0 ? dim : level >= 3 ? paint((text2) => palette.bold(palette.green(text2))) : paint(palette.green)
|
|
36029
37348
|
},
|
|
36030
37349
|
options.terminalWidth ?? resolveTtyColumns(process.stdout)
|
|
36031
37350
|
);
|
|
@@ -36110,7 +37429,7 @@ function renderTerminalSummaryReport(summaryData, options) {
|
|
|
36110
37429
|
const monthNotes = toMonthNotes(summaryData);
|
|
36111
37430
|
if (monthNotes.length > 0) {
|
|
36112
37431
|
const palette = options.palette ?? defaultTerminalStylePalette;
|
|
36113
|
-
const warn = (
|
|
37432
|
+
const warn = (text2) => useColor ? palette.yellow(text2) : text2;
|
|
36114
37433
|
lines.push(
|
|
36115
37434
|
"",
|
|
36116
37435
|
...monthNotes.map((note) => note.warning ? warn(`\u26A0 ${note.text}`) : note.text)
|
|
@@ -36177,22 +37496,8 @@ var statsTop3 = 168;
|
|
|
36177
37496
|
var gridTop2 = 352;
|
|
36178
37497
|
var gridLeft2 = SHARE_MARGIN + 36;
|
|
36179
37498
|
var gridPitch = 19;
|
|
36180
|
-
var MONTH_NAMES = [
|
|
36181
|
-
"Jan",
|
|
36182
|
-
"Feb",
|
|
36183
|
-
"Mar",
|
|
36184
|
-
"Apr",
|
|
36185
|
-
"May",
|
|
36186
|
-
"Jun",
|
|
36187
|
-
"Jul",
|
|
36188
|
-
"Aug",
|
|
36189
|
-
"Sep",
|
|
36190
|
-
"Oct",
|
|
36191
|
-
"Nov",
|
|
36192
|
-
"Dec"
|
|
36193
|
-
];
|
|
36194
37499
|
function formatShortDate(date) {
|
|
36195
|
-
return `${
|
|
37500
|
+
return `${MONTH_LABELS[Number(date.slice(5, 7)) - 1]} ${Number(date.slice(8, 10))}`;
|
|
36196
37501
|
}
|
|
36197
37502
|
function findPeriod(data, key) {
|
|
36198
37503
|
return data.periods.find((period) => period.key === key);
|
|
@@ -36581,7 +37886,7 @@ async function buildStatuslineSummary(options, deps) {
|
|
|
36581
37886
|
if (options.json) {
|
|
36582
37887
|
throw new Error("--json is not supported for statusline; use llm-usage summary --json");
|
|
36583
37888
|
}
|
|
36584
|
-
return buildSummaryData({ ...options, pricingOffline: true }, deps);
|
|
37889
|
+
return buildSummaryData({ ...options, pricingOffline: true, sync: false }, deps);
|
|
36585
37890
|
}
|
|
36586
37891
|
async function runStatusline(options) {
|
|
36587
37892
|
if (!options.verbose) {
|
|
@@ -37038,35 +38343,15 @@ function renderSummaryStats2(data, theme) {
|
|
|
37038
38343
|
}
|
|
37039
38344
|
];
|
|
37040
38345
|
return stats.map(
|
|
37041
|
-
(
|
|
38346
|
+
(stat10, index) => renderStat({
|
|
37042
38347
|
theme,
|
|
37043
38348
|
x: SHARE_MARGIN + index * statPitch3,
|
|
37044
38349
|
y: statsTop4,
|
|
37045
38350
|
size: 30,
|
|
37046
|
-
...
|
|
38351
|
+
...stat10
|
|
37047
38352
|
})
|
|
37048
38353
|
).join("\n");
|
|
37049
38354
|
}
|
|
37050
|
-
function renderGridLines(scaleMax, metric, theme) {
|
|
37051
|
-
const lines = [];
|
|
37052
|
-
for (let step = 1; step <= 3; step += 1) {
|
|
37053
|
-
const value = scaleMax / 3 * step;
|
|
37054
|
-
const y = scaleY(value, scaleMax, chartTop, chartBottom);
|
|
37055
|
-
lines.push(
|
|
37056
|
-
`<line x1="${chartLeft2}" y1="${y.toFixed(2)}" x2="${chartRight2}" y2="${y.toFixed(2)}" stroke="${theme.line}" stroke-width="1" stroke-dasharray="3 5"/>`,
|
|
37057
|
-
svgText(chartLeft2 - 10, y + 4, formatMetricValue4(value, metric), {
|
|
37058
|
-
size: 12,
|
|
37059
|
-
fill: theme.textMuted,
|
|
37060
|
-
mono: true,
|
|
37061
|
-
anchor: "end"
|
|
37062
|
-
})
|
|
37063
|
-
);
|
|
37064
|
-
}
|
|
37065
|
-
lines.push(
|
|
37066
|
-
`<line x1="${chartLeft2}" y1="${chartBottom}" x2="${chartRight2}" y2="${chartBottom}" stroke="${theme.line}" stroke-width="1"/>`
|
|
37067
|
-
);
|
|
37068
|
-
return lines.join("\n");
|
|
37069
|
-
}
|
|
37070
38355
|
function renderBars(buckets, scaleMax, theme) {
|
|
37071
38356
|
const slot = (chartRight2 - chartLeft2) / buckets.length;
|
|
37072
38357
|
const barWidth = Math.max(1, Math.min(40, slot * 0.7));
|
|
@@ -37097,7 +38382,12 @@ function renderTrendsShareSvg(data, theme) {
|
|
|
37097
38382
|
const source = data.totalSeries.source === "combined" ? "all sources" : data.totalSeries.source;
|
|
37098
38383
|
const body = buckets.length === 0 ? renderEmptyState(theme, "No usage in this window") : [
|
|
37099
38384
|
renderSummaryStats2(data, theme),
|
|
37100
|
-
|
|
38385
|
+
renderValueGridLines(
|
|
38386
|
+
scaleMax,
|
|
38387
|
+
{ left: chartLeft2, right: chartRight2, top: chartTop, bottom: chartBottom },
|
|
38388
|
+
(value) => formatMetricValue4(value, data.metric),
|
|
38389
|
+
theme
|
|
38390
|
+
),
|
|
37101
38391
|
renderBars(buckets, scaleMax, theme),
|
|
37102
38392
|
renderDateLabels(buckets, theme)
|
|
37103
38393
|
].join("\n");
|
|
@@ -37670,20 +38960,6 @@ var chartBottom2 = 500;
|
|
|
37670
38960
|
var MAX_NAMED_SOURCES = 6;
|
|
37671
38961
|
var MAX_PERIOD_LABELS = 8;
|
|
37672
38962
|
var MIN_CENTERED_LABEL_SLOT = 80;
|
|
37673
|
-
var MONTH_NAMES2 = [
|
|
37674
|
-
"Jan",
|
|
37675
|
-
"Feb",
|
|
37676
|
-
"Mar",
|
|
37677
|
-
"Apr",
|
|
37678
|
-
"May",
|
|
37679
|
-
"Jun",
|
|
37680
|
-
"Jul",
|
|
37681
|
-
"Aug",
|
|
37682
|
-
"Sep",
|
|
37683
|
-
"Oct",
|
|
37684
|
-
"Nov",
|
|
37685
|
-
"Dec"
|
|
37686
|
-
];
|
|
37687
38963
|
function extractPeriodSourceRows(rows) {
|
|
37688
38964
|
return rows.filter((r) => r.rowType === "period_source");
|
|
37689
38965
|
}
|
|
@@ -37729,7 +39005,7 @@ function formatPeriodLabel(period, granularity) {
|
|
|
37729
39005
|
if (granularity === "weekly") {
|
|
37730
39006
|
return period.slice(5);
|
|
37731
39007
|
}
|
|
37732
|
-
const month =
|
|
39008
|
+
const month = MONTH_LABELS[Number(period.slice(5, 7)) - 1] ?? period;
|
|
37733
39009
|
return granularity === "daily" ? `${month} ${Number(period.slice(8, 10))}` : `${month} ${period.slice(0, 4)}`;
|
|
37734
39010
|
}
|
|
37735
39011
|
function renderLegend(series, totalTokens, theme) {
|
|
@@ -37744,26 +39020,6 @@ ${svgText(chartLeft3 - 64, y, share, { size: 15, fill: theme.textSecondary, mono
|
|
|
37744
39020
|
</g>`;
|
|
37745
39021
|
}).join("\n");
|
|
37746
39022
|
}
|
|
37747
|
-
function renderGridLines2(maxY, theme) {
|
|
37748
|
-
const lines = [];
|
|
37749
|
-
for (let step = 1; step <= 3; step += 1) {
|
|
37750
|
-
const value = maxY / 3 * step;
|
|
37751
|
-
const y = scaleY(value, maxY, chartTop2, chartBottom2);
|
|
37752
|
-
lines.push(
|
|
37753
|
-
`<line x1="${chartLeft3}" y1="${y.toFixed(2)}" x2="${chartRight3}" y2="${y.toFixed(2)}" stroke="${theme.line}" stroke-width="1" stroke-dasharray="3 5"/>`,
|
|
37754
|
-
svgText(chartLeft3 - 10, y + 4, formatCompact(value), {
|
|
37755
|
-
size: 12,
|
|
37756
|
-
fill: theme.textMuted,
|
|
37757
|
-
mono: true,
|
|
37758
|
-
anchor: "end"
|
|
37759
|
-
})
|
|
37760
|
-
);
|
|
37761
|
-
}
|
|
37762
|
-
lines.push(
|
|
37763
|
-
`<line x1="${chartLeft3}" y1="${chartBottom2}" x2="${chartRight3}" y2="${chartBottom2}" stroke="${theme.line}" stroke-width="1"/>`
|
|
37764
|
-
);
|
|
37765
|
-
return lines.join("\n");
|
|
37766
|
-
}
|
|
37767
39023
|
function renderBars2(series, periodCount, maxY) {
|
|
37768
39024
|
const slot = (chartRight3 - chartLeft3) / periodCount;
|
|
37769
39025
|
const barWidth = Math.max(1, Math.min(56, slot * 0.68));
|
|
@@ -37840,7 +39096,12 @@ function renderUsageShareSvg(usageData, granularity, theme) {
|
|
|
37840
39096
|
size: 30
|
|
37841
39097
|
}),
|
|
37842
39098
|
renderLegend(series, totalTokens, theme),
|
|
37843
|
-
|
|
39099
|
+
renderValueGridLines(
|
|
39100
|
+
maxY,
|
|
39101
|
+
{ left: chartLeft3, right: chartRight3, top: chartTop2, bottom: chartBottom2 },
|
|
39102
|
+
formatCompact,
|
|
39103
|
+
theme
|
|
39104
|
+
),
|
|
37844
39105
|
renderBars2(series, periods.length, maxY),
|
|
37845
39106
|
renderPeriodLabels(periods, granularity, theme)
|
|
37846
39107
|
].join("\n");
|
|
@@ -37889,6 +39150,9 @@ async function prepareUsageReport(granularity, options, deps = {}) {
|
|
|
37889
39150
|
if (options.compact && options.json) {
|
|
37890
39151
|
throw new Error("--compact applies to terminal and markdown tables; drop it with --json");
|
|
37891
39152
|
}
|
|
39153
|
+
if (options.byMachine && options.share) {
|
|
39154
|
+
throw new Error("--by-machine splits the table only; drop it with --share");
|
|
39155
|
+
}
|
|
37892
39156
|
},
|
|
37893
39157
|
render: (usageData, format) => {
|
|
37894
39158
|
const { output, notes } = renderUsageReportWithNotes(usageData, format, {
|
|
@@ -37923,7 +39187,7 @@ function formatByteSize(sizeBytes) {
|
|
|
37923
39187
|
|
|
37924
39188
|
// src/render/render-doctor-report.ts
|
|
37925
39189
|
import os20 from "os";
|
|
37926
|
-
import
|
|
39190
|
+
import path38 from "path";
|
|
37927
39191
|
var doctorStatusGlyphs = { ok: "\u2714", error: "\u2716" };
|
|
37928
39192
|
var doctorStateGlyphs = {
|
|
37929
39193
|
found: "\u2714",
|
|
@@ -37938,7 +39202,7 @@ var summaryStates = [
|
|
|
37938
39202
|
{ state: "not_installed", label: "not installed" }
|
|
37939
39203
|
];
|
|
37940
39204
|
function abbreviateHome(searchPath, homeDir) {
|
|
37941
|
-
if (homeDir.length === 0 || searchPath !== homeDir && !searchPath.startsWith(homeDir +
|
|
39205
|
+
if (homeDir.length === 0 || searchPath !== homeDir && !searchPath.startsWith(homeDir + path38.sep)) {
|
|
37942
39206
|
return searchPath;
|
|
37943
39207
|
}
|
|
37944
39208
|
return `~${searchPath.slice(homeDir.length)}`;
|
|
@@ -37972,15 +39236,16 @@ function renderDoctorText(results, options = {}) {
|
|
|
37972
39236
|
(searchPath) => ` ${abbreviateHome(searchPath, homeDir)}`
|
|
37973
39237
|
)
|
|
37974
39238
|
]);
|
|
37975
|
-
const sourceResults = results.filter((result) => result.
|
|
37976
|
-
|
|
39239
|
+
const sourceResults = results.filter((result) => result.state !== void 0);
|
|
39240
|
+
const hasMachines = results.some((result) => result.format === "ssh");
|
|
39241
|
+
const tip = hasMachines ? [] : [
|
|
39242
|
+
"Tip: add your other machines to every report with llm-usage machine add <name> [user@host]"
|
|
39243
|
+
];
|
|
39244
|
+
return [...lines, "", formatSummary(sourceResults), ...tip].join("\n");
|
|
37977
39245
|
}
|
|
37978
39246
|
|
|
37979
39247
|
// src/cli/run-doctor-report.ts
|
|
37980
39248
|
var USAGE_PROBE_FILE_LIMIT = 25;
|
|
37981
|
-
function isMissingPathError3(error) {
|
|
37982
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
37983
|
-
}
|
|
37984
39249
|
async function sortNewestFirst(files) {
|
|
37985
39250
|
const mtimes = await Promise.all(
|
|
37986
39251
|
files.map(async (filePath) => {
|
|
@@ -38060,18 +39325,10 @@ async function buildSourceResult(adapter, hasPathOverride) {
|
|
|
38060
39325
|
};
|
|
38061
39326
|
return { result, files };
|
|
38062
39327
|
}
|
|
38063
|
-
function selectDoctorAdapters(adapters, source) {
|
|
38064
|
-
const sourceFilter = normalizeSourceFilter(source);
|
|
38065
|
-
validateSourceFilterValues(sourceFilter, new Set(getDefaultSourceIds()));
|
|
38066
|
-
if (!sourceFilter) {
|
|
38067
|
-
return adapters;
|
|
38068
|
-
}
|
|
38069
|
-
return adapters.filter((adapter) => sourceFilter.has(adapter.id.toLowerCase()));
|
|
38070
|
-
}
|
|
38071
39328
|
async function buildDoctorResults(options, deps = {}) {
|
|
38072
39329
|
const userConfigResolution = await resolveUserConfigForOptions(options, deps);
|
|
38073
39330
|
const configuredOptions = userConfigResolution.options;
|
|
38074
|
-
const adapters =
|
|
39331
|
+
const adapters = selectAdaptersBySourceFilter(
|
|
38075
39332
|
createDefaultAdapters(configuredOptions),
|
|
38076
39333
|
configuredOptions.source
|
|
38077
39334
|
);
|
|
@@ -38101,8 +39358,42 @@ async function buildDoctorResults(options, deps = {}) {
|
|
|
38101
39358
|
)
|
|
38102
39359
|
);
|
|
38103
39360
|
}
|
|
39361
|
+
results.push(
|
|
39362
|
+
...await buildMachineDoctorResults(
|
|
39363
|
+
userConfigResolution.loadedConfig.config.machines ?? {},
|
|
39364
|
+
(deps.now ?? Date.now)()
|
|
39365
|
+
)
|
|
39366
|
+
);
|
|
38104
39367
|
return results;
|
|
38105
39368
|
}
|
|
39369
|
+
async function buildMachineDoctorResults(machines, now) {
|
|
39370
|
+
return Promise.all(
|
|
39371
|
+
Object.entries(machines).map(async ([name, machine]) => {
|
|
39372
|
+
const id = `machine:${name}`;
|
|
39373
|
+
try {
|
|
39374
|
+
const status = await readMachineCacheStatus(name);
|
|
39375
|
+
const entry = {
|
|
39376
|
+
name,
|
|
39377
|
+
machine,
|
|
39378
|
+
state: status?.state,
|
|
39379
|
+
fileCount: status?.fileCount ?? 0,
|
|
39380
|
+
eventCount: status?.eventCount ?? 0
|
|
39381
|
+
};
|
|
39382
|
+
const detail = `${machine.ssh}: ${describeMachineStatus(entry, now)}`;
|
|
39383
|
+
const { state } = entry;
|
|
39384
|
+
const lastSyncFailed = state?.lastError !== void 0 && (state.attemptedAt ?? 0) >= (state.syncedAt ?? -1);
|
|
39385
|
+
return lastSyncFailed && machine.enabled !== false ? { id, format: "ssh", status: "error", itemsFound: entry.eventCount, error: detail } : { id, format: "ssh", status: "ok", itemsFound: entry.eventCount, detail };
|
|
39386
|
+
} catch (error) {
|
|
39387
|
+
return {
|
|
39388
|
+
id,
|
|
39389
|
+
format: "ssh",
|
|
39390
|
+
status: "error",
|
|
39391
|
+
error: `cannot read its cache at ${getMachineCachePath(name)}: ${getErrorReason(error)}`
|
|
39392
|
+
};
|
|
39393
|
+
}
|
|
39394
|
+
})
|
|
39395
|
+
);
|
|
39396
|
+
}
|
|
38106
39397
|
function getUnsupportedSchemaError(schemaVersion) {
|
|
38107
39398
|
const versionLabel = schemaVersion ? `v${schemaVersion}` : "unknown";
|
|
38108
39399
|
return `Event store schema ${versionLabel} is not supported by this llm-usage-metrics version (supports v${EVENT_STORE_SCHEMA_VERSION}); upgrade llm-usage-metrics or set LLM_USAGE_EVENT_STORE=0`;
|
|
@@ -38125,7 +39416,7 @@ async function buildEventStoreDoctorResult(filePath, discoveredFilesBySource, de
|
|
|
38125
39416
|
try {
|
|
38126
39417
|
fileStats = await stat7(filePath);
|
|
38127
39418
|
} catch (error) {
|
|
38128
|
-
if (
|
|
39419
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
38129
39420
|
const legacyPath = await findLegacyEventStore(filePath);
|
|
38130
39421
|
return {
|
|
38131
39422
|
id: "event-store",
|
|
@@ -38248,9 +39539,6 @@ function renderPruneReport(result) {
|
|
|
38248
39539
|
}
|
|
38249
39540
|
|
|
38250
39541
|
// src/cli/run-prune-report.ts
|
|
38251
|
-
function isMissingPathError4(error) {
|
|
38252
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
38253
|
-
}
|
|
38254
39542
|
function assertHasSelector(options) {
|
|
38255
39543
|
if (options.suppressed || options.departedBefore !== void 0) {
|
|
38256
39544
|
return;
|
|
@@ -38271,14 +39559,6 @@ function isOlderThanUtcDate(file, departedBeforeTimestamp) {
|
|
|
38271
39559
|
const newestTimestamp = Date.parse(file.newestTimestamp);
|
|
38272
39560
|
return Number.isFinite(newestTimestamp) && newestTimestamp < departedBeforeTimestamp;
|
|
38273
39561
|
}
|
|
38274
|
-
function selectPruneAdapters(options, adapters) {
|
|
38275
|
-
const sourceFilter = normalizeSourceFilter(options.source);
|
|
38276
|
-
validateSourceFilterValues(sourceFilter, new Set(getDefaultSourceIds()));
|
|
38277
|
-
if (!sourceFilter) {
|
|
38278
|
-
return adapters;
|
|
38279
|
-
}
|
|
38280
|
-
return adapters.filter((adapter) => sourceFilter.has(adapter.id.toLowerCase()));
|
|
38281
|
-
}
|
|
38282
39562
|
async function discoverLiveFiles(adapters) {
|
|
38283
39563
|
const selectedSources = [];
|
|
38284
39564
|
const discoveredFiles = [];
|
|
@@ -38326,7 +39606,7 @@ async function fileExists2(filePath, statFile) {
|
|
|
38326
39606
|
await statFile(filePath);
|
|
38327
39607
|
return true;
|
|
38328
39608
|
} catch (error) {
|
|
38329
|
-
if (
|
|
39609
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
38330
39610
|
return false;
|
|
38331
39611
|
}
|
|
38332
39612
|
throw error;
|
|
@@ -38336,7 +39616,7 @@ async function readFileSize(filePath, statFile) {
|
|
|
38336
39616
|
try {
|
|
38337
39617
|
return (await statFile(filePath)).size;
|
|
38338
39618
|
} catch (error) {
|
|
38339
|
-
if (
|
|
39619
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
38340
39620
|
return 0;
|
|
38341
39621
|
}
|
|
38342
39622
|
throw error;
|
|
@@ -38403,7 +39683,10 @@ async function buildPruneReport(options, deps = {}) {
|
|
|
38403
39683
|
);
|
|
38404
39684
|
}
|
|
38405
39685
|
const makeAdapters = deps.createAdapters ?? createDefaultAdapters;
|
|
38406
|
-
const adapters =
|
|
39686
|
+
const adapters = selectAdaptersBySourceFilter(
|
|
39687
|
+
makeAdapters(configuredOptions),
|
|
39688
|
+
configuredOptions.source
|
|
39689
|
+
);
|
|
38407
39690
|
const { selectedSources, discoveredFiles } = await discoverLiveFiles(adapters);
|
|
38408
39691
|
const statFile = deps.statFile ?? stat8;
|
|
38409
39692
|
const storePath = eventStoreRuntimeConfig.path;
|
|
@@ -38481,7 +39764,7 @@ async function runPruneReport(options, deps = {}) {
|
|
|
38481
39764
|
|
|
38482
39765
|
// src/cli/run-events-report.ts
|
|
38483
39766
|
var eventsExportFormats = ["jsonl", "csv"];
|
|
38484
|
-
var CSV_HEADER = "source,sessionId,timestamp,repoRoot,provider,model,inputTokens,outputTokens,reasoningTokens,cacheReadTokens,cacheWriteTokens,totalTokens,costUsd,costMode";
|
|
39767
|
+
var CSV_HEADER = "source,sessionId,timestamp,repoRoot,provider,model,inputTokens,outputTokens,reasoningTokens,cacheReadTokens,cacheWriteTokens,totalTokens,costUsd,costMode,machine";
|
|
38485
39768
|
function resolveEventsFormat(options) {
|
|
38486
39769
|
if (options.json) {
|
|
38487
39770
|
throw new Error("--json is not supported for events; use --format jsonl");
|
|
@@ -38503,8 +39786,8 @@ function toCsvField(value) {
|
|
|
38503
39786
|
return "";
|
|
38504
39787
|
}
|
|
38505
39788
|
const raw = String(value);
|
|
38506
|
-
const
|
|
38507
|
-
return /[",\n\r]/u.test(
|
|
39789
|
+
const text2 = typeof value === "string" && /^[=+\-@\t\r]/u.test(raw) ? `'${raw}` : raw;
|
|
39790
|
+
return /[",\n\r]/u.test(text2) ? `"${text2.replaceAll('"', '""')}"` : text2;
|
|
38508
39791
|
}
|
|
38509
39792
|
function toCsvLine(event) {
|
|
38510
39793
|
return [
|
|
@@ -38521,7 +39804,8 @@ function toCsvLine(event) {
|
|
|
38521
39804
|
event.cacheWriteTokens,
|
|
38522
39805
|
event.totalTokens,
|
|
38523
39806
|
event.costUsd,
|
|
38524
|
-
event.costMode
|
|
39807
|
+
event.costMode,
|
|
39808
|
+
event.machine
|
|
38525
39809
|
].map(toCsvField).join(",");
|
|
38526
39810
|
}
|
|
38527
39811
|
async function runEventsReport(options, deps = {}) {
|
|
@@ -38568,20 +39852,6 @@ var wrappedReportFormats = [
|
|
|
38568
39852
|
"markdown",
|
|
38569
39853
|
"json"
|
|
38570
39854
|
];
|
|
38571
|
-
var MONTH_LABELS3 = [
|
|
38572
|
-
"Jan",
|
|
38573
|
-
"Feb",
|
|
38574
|
-
"Mar",
|
|
38575
|
-
"Apr",
|
|
38576
|
-
"May",
|
|
38577
|
-
"Jun",
|
|
38578
|
-
"Jul",
|
|
38579
|
-
"Aug",
|
|
38580
|
-
"Sep",
|
|
38581
|
-
"Oct",
|
|
38582
|
-
"Nov",
|
|
38583
|
-
"Dec"
|
|
38584
|
-
];
|
|
38585
39855
|
var INTENSITY_GLYPHS = ["\xB7", "\u2581", "\u2582", "\u2584", "\u2588"];
|
|
38586
39856
|
var TOP_MODELS_HEADERS = ["#", "Model", "Tokens", "Cost"];
|
|
38587
39857
|
var TOP_SOURCES_HEADERS = ["#", "Source", "Tokens", "Cost"];
|
|
@@ -38600,7 +39870,7 @@ function formatBusiestDay(busiestDay) {
|
|
|
38600
39870
|
return busiestDay === void 0 ? "-" : `${busiestDay.date} (${formatInteger2(busiestDay.totalTokens)} tokens)`;
|
|
38601
39871
|
}
|
|
38602
39872
|
function renderStatLines(recap, paint, palette) {
|
|
38603
|
-
const bold = (styler) => (
|
|
39873
|
+
const bold = (styler) => (text2) => palette.bold(styler(text2));
|
|
38604
39874
|
const totalSplitTokens = recap.weekdayTokens + recap.weekendTokens;
|
|
38605
39875
|
const stats = [
|
|
38606
39876
|
{ label: "Tokens", value: formatInteger2(recap.totalTokens), styler: bold(palette.cyan) },
|
|
@@ -38641,16 +39911,16 @@ function renderStatLines(recap, paint, palette) {
|
|
|
38641
39911
|
{ label: "Events", value: formatInteger2(recap.eventCount), styler: palette.white },
|
|
38642
39912
|
{ label: "Sessions", value: formatInteger2(recap.sessionCount), styler: palette.white }
|
|
38643
39913
|
);
|
|
38644
|
-
const labelWidth = stats.reduce((max,
|
|
39914
|
+
const labelWidth = stats.reduce((max, stat10) => Math.max(max, stat10.label.length), 0);
|
|
38645
39915
|
return stats.map(
|
|
38646
|
-
(
|
|
39916
|
+
(stat10) => `${paint(palette.dim)(stat10.label.padEnd(labelWidth))} ${paint(stat10.styler)(stat10.value)}`
|
|
38647
39917
|
).join("\n");
|
|
38648
39918
|
}
|
|
38649
39919
|
function renderMonthlyStrip(recap, paint, palette) {
|
|
38650
|
-
const boldGreen = (
|
|
39920
|
+
const boldGreen = (text2) => palette.bold(palette.green(text2));
|
|
38651
39921
|
return recap.monthlyIntensity.map((month, index) => {
|
|
38652
39922
|
const glyph = INTENSITY_GLYPHS[month.level] ?? "\xB7";
|
|
38653
|
-
const label =
|
|
39923
|
+
const label = MONTH_LABELS[index] ?? month.month;
|
|
38654
39924
|
const glyphStyler = month.level === 0 ? palette.dim : month.level >= 3 ? boldGreen : palette.green;
|
|
38655
39925
|
return `${paint(palette.dim)(label)} ${paint(glyphStyler)(glyph)}`;
|
|
38656
39926
|
}).join(" ");
|
|
@@ -38690,7 +39960,7 @@ function renderTopTable(headerCells, bodyRows) {
|
|
|
38690
39960
|
function renderWrappedTerminalReport(recap, options) {
|
|
38691
39961
|
const useColor = options.useColor ?? shouldUseColorByDefault();
|
|
38692
39962
|
const palette = options.palette ?? defaultTerminalStylePalette;
|
|
38693
|
-
const paint = (styler) => (
|
|
39963
|
+
const paint = (styler) => (text2) => useColor ? styler(text2) : text2;
|
|
38694
39964
|
return [
|
|
38695
39965
|
renderReportHeader({ title: `Wrapped ${recap.year}`, useColor }),
|
|
38696
39966
|
paint(palette.dim)(`${recap.from} to ${recap.to} (${recap.timezone})`),
|
|
@@ -38828,7 +40098,7 @@ function renderStats2(data, theme) {
|
|
|
38828
40098
|
}
|
|
38829
40099
|
];
|
|
38830
40100
|
return stats.map(
|
|
38831
|
-
(
|
|
40101
|
+
(stat10, index) => renderStat({ theme, x: statColumns[index], y: statsTop6, size: 32, ...stat10 })
|
|
38832
40102
|
).join("\n");
|
|
38833
40103
|
}
|
|
38834
40104
|
function renderTopList(theme, title, items, x) {
|
|
@@ -39367,7 +40637,11 @@ function registerSharedReportOptions(command, profile) {
|
|
|
39367
40637
|
configuredCommand.option(
|
|
39368
40638
|
"--history",
|
|
39369
40639
|
"include usage from files that no longer exist on disk (the default; fails if the event store is unavailable)"
|
|
39370
|
-
).option("--no-history", "leave out usage from files that no longer exist on disk")
|
|
40640
|
+
).option("--no-history", "leave out usage from files that no longer exist on disk").option(
|
|
40641
|
+
"--machine <name>",
|
|
40642
|
+
"Count only these machines (repeatable or comma-separated; local is this one; default: all)",
|
|
40643
|
+
collectRepeatedOption
|
|
40644
|
+
).option("--no-sync", "use other machines' cached usage without refreshing it over ssh");
|
|
39371
40645
|
}
|
|
39372
40646
|
if (profileConfig.includeMarkdown) {
|
|
39373
40647
|
configuredCommand.option("--markdown", "Render output as markdown table");
|
|
@@ -39532,7 +40806,7 @@ function createUsageReportDefinition(granularity) {
|
|
|
39532
40806
|
command.option(
|
|
39533
40807
|
"--compact",
|
|
39534
40808
|
"Abbreviate token counts and hide the Reasoning and Cache Write columns (terminal/markdown)"
|
|
39535
|
-
);
|
|
40809
|
+
).option("--by-machine", "Split each source row by the machine its usage came from");
|
|
39536
40810
|
command.action((options) => runUsageReport(granularity, options));
|
|
39537
40811
|
return command;
|
|
39538
40812
|
}
|
|
@@ -39892,8 +41166,8 @@ function createRootDescription() {
|
|
|
39892
41166
|
}
|
|
39893
41167
|
|
|
39894
41168
|
// src/cli/create-config-command.ts
|
|
39895
|
-
import { access as access4,
|
|
39896
|
-
import
|
|
41169
|
+
import { access as access4, writeFile as writeFile4 } from "fs/promises";
|
|
41170
|
+
import path39 from "path";
|
|
39897
41171
|
import { Command as Command2 } from "commander";
|
|
39898
41172
|
var CONFIG_TEMPLATE_HEADER = `#:schema https://ayagmar.github.io/llm-usage-metrics/config-schema.json
|
|
39899
41173
|
# llm-usage-metrics configuration. Uncomment a key to override its default.
|
|
@@ -39944,19 +41218,24 @@ ${sourceDirTemplate}
|
|
|
39944
41218
|
# skipCheck = false
|
|
39945
41219
|
# cacheTtlMs = ${UPDATE_CACHE_TTL_DEFAULT_MS}
|
|
39946
41220
|
# fetchTimeoutMs = ${UPDATE_FETCH_TIMEOUT_DEFAULT_MS}
|
|
41221
|
+
|
|
41222
|
+
# Other machines to sync usage from over ssh; each needs llm-usage-metrics
|
|
41223
|
+
# installed and ssh login without a prompt. \`llm-usage machine add\` checks
|
|
41224
|
+
# the connection and writes the table for you.
|
|
41225
|
+
# [machines.laptop]
|
|
41226
|
+
# ssh = "me@laptop.local"
|
|
41227
|
+
# command = "llm-usage"
|
|
41228
|
+
# enabled = true
|
|
39947
41229
|
`;
|
|
39948
|
-
function isExistingFileError(error) {
|
|
39949
|
-
return asRecord(error)?.code === "EEXIST";
|
|
39950
|
-
}
|
|
39951
41230
|
async function writeConfigTemplate(configPath, options) {
|
|
39952
|
-
await
|
|
41231
|
+
await ensureDirectory(path39.dirname(configPath));
|
|
39953
41232
|
try {
|
|
39954
41233
|
await writeFile4(configPath, USER_CONFIG_TEMPLATE, {
|
|
39955
41234
|
encoding: "utf8",
|
|
39956
41235
|
flag: options.force === true ? "w" : "wx"
|
|
39957
41236
|
});
|
|
39958
41237
|
} catch (error) {
|
|
39959
|
-
if (
|
|
41238
|
+
if (hasErrorCode(error, "EEXIST")) {
|
|
39960
41239
|
throw new Error(`Config file already exists: ${configPath} (use --force to overwrite)`, {
|
|
39961
41240
|
cause: error
|
|
39962
41241
|
});
|
|
@@ -40019,8 +41298,386 @@ function createConfigCommand() {
|
|
|
40019
41298
|
return configCommand;
|
|
40020
41299
|
}
|
|
40021
41300
|
|
|
40022
|
-
// src/cli/create-
|
|
41301
|
+
// src/cli/create-machine-command.ts
|
|
40023
41302
|
import { Command as Command3 } from "commander";
|
|
41303
|
+
|
|
41304
|
+
// src/cli/run-machine-export.ts
|
|
41305
|
+
import { once } from "events";
|
|
41306
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
41307
|
+
import os21 from "os";
|
|
41308
|
+
import { text } from "stream/consumers";
|
|
41309
|
+
async function readKnownFiles(known, stdin) {
|
|
41310
|
+
if (known === void 0) {
|
|
41311
|
+
return /* @__PURE__ */ new Map();
|
|
41312
|
+
}
|
|
41313
|
+
return parseKnownFiles(known === "-" ? await text(stdin) : await readFile7(known, "utf8"));
|
|
41314
|
+
}
|
|
41315
|
+
async function writeLine(stdout, line) {
|
|
41316
|
+
if (!stdout.write(`${JSON.stringify(line)}
|
|
41317
|
+
`)) {
|
|
41318
|
+
await once(stdout, "drain");
|
|
41319
|
+
}
|
|
41320
|
+
}
|
|
41321
|
+
async function runMachineExport(options, deps = {}) {
|
|
41322
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
41323
|
+
const knownFiles = await readKnownFiles(options.known, deps.stdin ?? process.stdin);
|
|
41324
|
+
const dataset = await buildUsageEventDataset({ machine: LOCAL_MACHINE_NAME }, deps);
|
|
41325
|
+
emitReportRunDiagnostics(
|
|
41326
|
+
buildUsageDiagnostics({
|
|
41327
|
+
adaptersToParse: dataset.adaptersToParse,
|
|
41328
|
+
successfulParseResults: dataset.successfulParseResults,
|
|
41329
|
+
sourceFailures: dataset.sourceFailures,
|
|
41330
|
+
pricingOrigin: "none",
|
|
41331
|
+
warnings: dataset.warnings,
|
|
41332
|
+
notes: dataset.notes,
|
|
41333
|
+
activeEnvOverrides: dataset.readEnvVarOverrides(),
|
|
41334
|
+
activeConfig: dataset.activeConfig,
|
|
41335
|
+
timezone: dataset.normalizedInputs.timezone
|
|
41336
|
+
}),
|
|
41337
|
+
{
|
|
41338
|
+
emitCommonDiagnostics: emitDiagnostics,
|
|
41339
|
+
getEnvVarOverrides: (diagnostics) => diagnostics.activeEnvOverrides,
|
|
41340
|
+
getActiveConfig: (diagnostics) => diagnostics.activeConfig
|
|
41341
|
+
}
|
|
41342
|
+
);
|
|
41343
|
+
if (!dataset.ledger) {
|
|
41344
|
+
throw new Error(
|
|
41345
|
+
"machine export reads the event store, which is disabled or failed to open; see the warning above"
|
|
41346
|
+
);
|
|
41347
|
+
}
|
|
41348
|
+
const store = await (deps.openEventStore ?? openEventStore)(dataset.ledger.path);
|
|
41349
|
+
let snapshots;
|
|
41350
|
+
try {
|
|
41351
|
+
snapshots = readStoredFileSnapshots(store, dataset.ledger.countedFiles);
|
|
41352
|
+
} finally {
|
|
41353
|
+
(deps.closeEventStore ?? closeEventStore)(store);
|
|
41354
|
+
}
|
|
41355
|
+
const lines = buildMachineExportLines(
|
|
41356
|
+
{
|
|
41357
|
+
cliVersion: loadPackageMetadataFromRuntime().packageVersion,
|
|
41358
|
+
hostname: os21.hostname(),
|
|
41359
|
+
exportedAt: (deps.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
|
|
41360
|
+
},
|
|
41361
|
+
snapshots,
|
|
41362
|
+
knownFiles
|
|
41363
|
+
);
|
|
41364
|
+
const end = lines.at(-1);
|
|
41365
|
+
const missingEventCount = dataset.filteredEvents.length - (end?.type === "end" ? end.eventCount : 0);
|
|
41366
|
+
if (missingEventCount > 0) {
|
|
41367
|
+
logger.warn(
|
|
41368
|
+
`machine export left out ${missingEventCount} event(s) that reports count but the event store does not hold`
|
|
41369
|
+
);
|
|
41370
|
+
}
|
|
41371
|
+
for (const line of lines) {
|
|
41372
|
+
await writeLine(stdout, line);
|
|
41373
|
+
}
|
|
41374
|
+
}
|
|
41375
|
+
|
|
41376
|
+
// src/machines/machine-config-file.ts
|
|
41377
|
+
import { isDeepStrictEqual } from "util";
|
|
41378
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
41379
|
+
import { readFile as readFile8, realpath as realpath4, rename as rename2, rm as rm4, stat as stat9, writeFile as writeFile5 } from "fs/promises";
|
|
41380
|
+
import path40 from "path";
|
|
41381
|
+
async function readConfigText(configPath) {
|
|
41382
|
+
try {
|
|
41383
|
+
return await readFile8(configPath, "utf8");
|
|
41384
|
+
} catch (error) {
|
|
41385
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
41386
|
+
return void 0;
|
|
41387
|
+
}
|
|
41388
|
+
throw error;
|
|
41389
|
+
}
|
|
41390
|
+
}
|
|
41391
|
+
async function loadConfigText(configPath, content) {
|
|
41392
|
+
const loaded = await loadUserConfig({ LLM_USAGE_CONFIG_PATH: configPath }, async () => content);
|
|
41393
|
+
return loaded.config;
|
|
41394
|
+
}
|
|
41395
|
+
function withoutMachine(config, name) {
|
|
41396
|
+
const machines = Object.fromEntries(
|
|
41397
|
+
Object.entries(config.machines ?? {}).filter(([machineName]) => machineName !== name)
|
|
41398
|
+
);
|
|
41399
|
+
const rest = { ...config };
|
|
41400
|
+
delete rest.machines;
|
|
41401
|
+
return Object.keys(machines).length === 0 ? rest : { ...rest, machines };
|
|
41402
|
+
}
|
|
41403
|
+
async function replaceConfigFile(configPath, content) {
|
|
41404
|
+
await ensureDirectory(path40.dirname(configPath));
|
|
41405
|
+
const targetPath = await realpath4(configPath).catch(() => configPath);
|
|
41406
|
+
const mode = (await stat9(targetPath).catch(() => void 0))?.mode;
|
|
41407
|
+
const temporaryPath = `${targetPath}.${randomUUID3()}.tmp`;
|
|
41408
|
+
try {
|
|
41409
|
+
await writeFile5(temporaryPath, content, { encoding: "utf8", mode: mode ?? 420 });
|
|
41410
|
+
await rename2(temporaryPath, targetPath);
|
|
41411
|
+
} finally {
|
|
41412
|
+
await rm4(temporaryPath, { force: true });
|
|
41413
|
+
}
|
|
41414
|
+
}
|
|
41415
|
+
async function hasMachineEntry(configPath, name) {
|
|
41416
|
+
const content = await readConfigText(configPath);
|
|
41417
|
+
try {
|
|
41418
|
+
return content !== void 0 && asRecord(asRecord(parse(content))?.machines)?.[name] !== void 0;
|
|
41419
|
+
} catch {
|
|
41420
|
+
return false;
|
|
41421
|
+
}
|
|
41422
|
+
}
|
|
41423
|
+
function formatMachineBlock(name, machine, eol = "\n") {
|
|
41424
|
+
const lines = [`[machines.${name}]`, `ssh = ${JSON.stringify(machine.ssh)}`];
|
|
41425
|
+
if (machine.command !== void 0) {
|
|
41426
|
+
lines.push(`command = ${JSON.stringify(machine.command)}`);
|
|
41427
|
+
}
|
|
41428
|
+
return `${lines.join(eol)}${eol}`;
|
|
41429
|
+
}
|
|
41430
|
+
async function addMachineToConfigFile(configPath, name, machine) {
|
|
41431
|
+
const current = await readConfigText(configPath) ?? "";
|
|
41432
|
+
const eol = current.includes("\r\n") ? "\r\n" : "\n";
|
|
41433
|
+
const separator = current.length === 0 ? "" : current.endsWith("\n") ? eol : `${eol}${eol}`;
|
|
41434
|
+
const updated = `${current}${separator}${formatMachineBlock(name, machine, eol)}`;
|
|
41435
|
+
const before = await loadConfigText(configPath, current);
|
|
41436
|
+
let after;
|
|
41437
|
+
try {
|
|
41438
|
+
after = await loadConfigText(configPath, updated);
|
|
41439
|
+
} catch {
|
|
41440
|
+
after = void 0;
|
|
41441
|
+
}
|
|
41442
|
+
if (!after || !isDeepStrictEqual(after.machines?.[name], machine) || !isDeepStrictEqual(withoutMachine(after, name), before)) {
|
|
41443
|
+
throw new Error(
|
|
41444
|
+
`Could not add [machines.${name}] to ${configPath} safely; add these lines to it by hand:
|
|
41445
|
+
${formatMachineBlock(name, machine)}`
|
|
41446
|
+
);
|
|
41447
|
+
}
|
|
41448
|
+
await replaceConfigFile(configPath, updated);
|
|
41449
|
+
}
|
|
41450
|
+
var TABLE_HEADER_PATTERN = /^\s*\[/u;
|
|
41451
|
+
function isMachineHeader(line, name) {
|
|
41452
|
+
const commentStart = line.indexOf("#");
|
|
41453
|
+
const header = (commentStart === -1 ? line : line.slice(0, commentStart)).trim();
|
|
41454
|
+
return header === `[machines.${name}]` || header === `[machines."${name}"]`;
|
|
41455
|
+
}
|
|
41456
|
+
async function removeMachineFromConfigFile(configPath, name) {
|
|
41457
|
+
const current = await readConfigText(configPath) ?? "";
|
|
41458
|
+
const lines = current.split("\n");
|
|
41459
|
+
const start = lines.findIndex((line) => isMachineHeader(line, name));
|
|
41460
|
+
const failure = new Error(
|
|
41461
|
+
`Could not remove machine ${name} from ${configPath} safely; delete its [machines.${name}] table by hand`
|
|
41462
|
+
);
|
|
41463
|
+
if (start === -1) {
|
|
41464
|
+
throw failure;
|
|
41465
|
+
}
|
|
41466
|
+
const nextTable = lines.findIndex(
|
|
41467
|
+
(line, index) => index > start && TABLE_HEADER_PATTERN.test(line)
|
|
41468
|
+
);
|
|
41469
|
+
const tableEnd = nextTable === -1 ? lines.length : nextTable;
|
|
41470
|
+
let end = start + 1;
|
|
41471
|
+
for (let index = start + 1; index < tableEnd; index += 1) {
|
|
41472
|
+
const text2 = lines[index].trim();
|
|
41473
|
+
if (text2.length > 0 && !text2.startsWith("#")) {
|
|
41474
|
+
end = index + 1;
|
|
41475
|
+
}
|
|
41476
|
+
}
|
|
41477
|
+
if (end < tableEnd && lines[end].trim() === "") {
|
|
41478
|
+
end += 1;
|
|
41479
|
+
}
|
|
41480
|
+
const isLast = lines.slice(end).every((line) => line.trim() === "");
|
|
41481
|
+
const removeFrom = isLast && start > 0 && lines[start - 1].trim() === "" ? start - 1 : start;
|
|
41482
|
+
const kept = [...lines.slice(0, removeFrom), ...lines.slice(isLast ? lines.length : end)].join(
|
|
41483
|
+
"\n"
|
|
41484
|
+
);
|
|
41485
|
+
const updated = kept.length > 0 && current.endsWith("\n") && !kept.endsWith("\n") ? `${kept}
|
|
41486
|
+
` : kept;
|
|
41487
|
+
const before = await loadConfigText(configPath, current);
|
|
41488
|
+
let after;
|
|
41489
|
+
try {
|
|
41490
|
+
after = await loadConfigText(configPath, updated);
|
|
41491
|
+
} catch {
|
|
41492
|
+
after = void 0;
|
|
41493
|
+
}
|
|
41494
|
+
if (!after || !isDeepStrictEqual(after, withoutMachine(before, name))) {
|
|
41495
|
+
throw failure;
|
|
41496
|
+
}
|
|
41497
|
+
await replaceConfigFile(configPath, updated);
|
|
41498
|
+
}
|
|
41499
|
+
|
|
41500
|
+
// src/cli/run-machine-commands.ts
|
|
41501
|
+
async function loadConfiguredMachines() {
|
|
41502
|
+
const loaded = await loadUserConfig();
|
|
41503
|
+
for (const warning of loaded.warnings) {
|
|
41504
|
+
logger.warn(warning);
|
|
41505
|
+
}
|
|
41506
|
+
return loaded.config.machines ?? {};
|
|
41507
|
+
}
|
|
41508
|
+
async function runMachineAdd(name, sshDestination, options, deps = {}) {
|
|
41509
|
+
const sshTarget = sshDestination ?? name;
|
|
41510
|
+
const print = deps.print ?? console.log;
|
|
41511
|
+
if (!isValidMachineName(name)) {
|
|
41512
|
+
const hint = /[@.:]/u.test(name) ? `; give a name first, then the destination: llm-usage machine add laptop ${name}` : "";
|
|
41513
|
+
throw new Error(
|
|
41514
|
+
`Invalid machine name "${name}": use 1-32 lowercase letters, digits or dashes (not "${LOCAL_MACHINE_NAME}")${hint}`
|
|
41515
|
+
);
|
|
41516
|
+
}
|
|
41517
|
+
if (!isValidSshTarget(sshTarget)) {
|
|
41518
|
+
throw new Error(
|
|
41519
|
+
`Invalid ssh destination "${sshTarget}": use host, user@host or an ssh_config alias`
|
|
41520
|
+
);
|
|
41521
|
+
}
|
|
41522
|
+
const configPath = resolveUserConfigPath(process.env);
|
|
41523
|
+
if (Object.hasOwn(await loadConfiguredMachines(), name)) {
|
|
41524
|
+
throw new Error(
|
|
41525
|
+
`Machine ${name} is already configured in ${configPath}; pick another name or run llm-usage machine remove ${name}`
|
|
41526
|
+
);
|
|
41527
|
+
}
|
|
41528
|
+
if (await hasMachineEntry(configPath, name)) {
|
|
41529
|
+
throw new Error(
|
|
41530
|
+
`${configPath} already has a machines.${name} entry that is not valid (see the warning above); fix or delete it first`
|
|
41531
|
+
);
|
|
41532
|
+
}
|
|
41533
|
+
let command = options.command?.trim();
|
|
41534
|
+
if (!command) {
|
|
41535
|
+
command = await detectRemoteCommand(sshTarget, { spawnSsh: deps.spawnSsh });
|
|
41536
|
+
if (command) {
|
|
41537
|
+
logger.info(
|
|
41538
|
+
`${sshTarget} finds llm-usage only in a login shell; launching it with: ${command}`
|
|
41539
|
+
);
|
|
41540
|
+
}
|
|
41541
|
+
}
|
|
41542
|
+
const machine = command ? { ssh: sshTarget, command } : { ssh: sshTarget };
|
|
41543
|
+
logger.info(
|
|
41544
|
+
`Syncing ${name} over ssh (${sshTarget}); its first export parses that machine's logs and can take a while...`
|
|
41545
|
+
);
|
|
41546
|
+
await deleteMachineCache(name);
|
|
41547
|
+
const outcome = await syncMachine(name, machine, {
|
|
41548
|
+
full: true,
|
|
41549
|
+
spawnSsh: deps.spawnSsh,
|
|
41550
|
+
now: deps.now
|
|
41551
|
+
});
|
|
41552
|
+
if (!outcome.ok) {
|
|
41553
|
+
await deleteMachineCache(name);
|
|
41554
|
+
throw new Error(`Could not add ${name}: ${outcome.error}`);
|
|
41555
|
+
}
|
|
41556
|
+
await addMachineToConfigFile(configPath, name, machine);
|
|
41557
|
+
for (const line of renderSyncOutcome(outcome, (deps.now ?? Date.now)())) {
|
|
41558
|
+
print(line);
|
|
41559
|
+
}
|
|
41560
|
+
print(
|
|
41561
|
+
`Added ${name} to ${configPath}. Reports now include its usage; llm-usage sync fetches it again.`
|
|
41562
|
+
);
|
|
41563
|
+
}
|
|
41564
|
+
async function runMachineRemove(name, deps = {}) {
|
|
41565
|
+
const print = deps.print ?? console.log;
|
|
41566
|
+
const configPath = resolveUserConfigPath(process.env);
|
|
41567
|
+
if (!Object.hasOwn(await loadConfiguredMachines(), name)) {
|
|
41568
|
+
throw new Error(`No machine named ${name} in ${configPath}`);
|
|
41569
|
+
}
|
|
41570
|
+
await removeMachineFromConfigFile(configPath, name);
|
|
41571
|
+
await deleteMachineCache(name);
|
|
41572
|
+
print(`Removed ${name} from ${configPath} and deleted its cached usage.`);
|
|
41573
|
+
}
|
|
41574
|
+
async function runMachineList(deps = {}) {
|
|
41575
|
+
const print = deps.print ?? console.log;
|
|
41576
|
+
const machines = await loadConfiguredMachines();
|
|
41577
|
+
const entries = await Promise.all(
|
|
41578
|
+
Object.entries(machines).map(async ([name, machine]) => {
|
|
41579
|
+
const status = await readMachineCacheStatus(name);
|
|
41580
|
+
return {
|
|
41581
|
+
name,
|
|
41582
|
+
machine,
|
|
41583
|
+
state: status?.state,
|
|
41584
|
+
fileCount: status?.fileCount ?? 0,
|
|
41585
|
+
eventCount: status?.eventCount ?? 0
|
|
41586
|
+
};
|
|
41587
|
+
})
|
|
41588
|
+
);
|
|
41589
|
+
for (const line of renderMachineList(entries, (deps.now ?? Date.now)())) {
|
|
41590
|
+
print(line);
|
|
41591
|
+
}
|
|
41592
|
+
}
|
|
41593
|
+
async function runSync(names, options, deps = {}) {
|
|
41594
|
+
const print = deps.print ?? console.log;
|
|
41595
|
+
const machines = await loadConfiguredMachines();
|
|
41596
|
+
const unknownNames = names.filter((name) => !Object.hasOwn(machines, name));
|
|
41597
|
+
if (unknownNames.length > 0) {
|
|
41598
|
+
const configured = Object.keys(machines);
|
|
41599
|
+
throw new Error(
|
|
41600
|
+
`Unknown machine(s): ${unknownNames.join(", ")}${configured.length > 0 ? ` (configured: ${configured.join(", ")})` : ""}`
|
|
41601
|
+
);
|
|
41602
|
+
}
|
|
41603
|
+
const selected = Object.entries(machines).filter(
|
|
41604
|
+
([name, machine]) => names.length > 0 ? names.includes(name) : machine.enabled !== false
|
|
41605
|
+
);
|
|
41606
|
+
if (selected.length === 0) {
|
|
41607
|
+
print(
|
|
41608
|
+
Object.keys(machines).length === 0 ? "No machines configured. Add one with: llm-usage machine add <name> [user@host]" : "Every machine is disabled; name one to sync it anyway."
|
|
41609
|
+
);
|
|
41610
|
+
return;
|
|
41611
|
+
}
|
|
41612
|
+
const outcomes = await Promise.all(
|
|
41613
|
+
selected.map(async ([name, machine]) => {
|
|
41614
|
+
const outcome = await syncMachine(name, machine, {
|
|
41615
|
+
full: options.full,
|
|
41616
|
+
recordAttemptFirst: true,
|
|
41617
|
+
spawnSsh: deps.spawnSsh,
|
|
41618
|
+
now: deps.now
|
|
41619
|
+
});
|
|
41620
|
+
for (const line of renderSyncOutcome(outcome, (deps.now ?? Date.now)())) {
|
|
41621
|
+
print(line);
|
|
41622
|
+
}
|
|
41623
|
+
return outcome;
|
|
41624
|
+
})
|
|
41625
|
+
);
|
|
41626
|
+
if (outcomes.some((outcome) => !outcome.ok)) {
|
|
41627
|
+
process.exitCode = 1;
|
|
41628
|
+
}
|
|
41629
|
+
}
|
|
41630
|
+
|
|
41631
|
+
// src/cli/create-machine-command.ts
|
|
41632
|
+
function createMachineCommand() {
|
|
41633
|
+
const machineCommand = new Command3("machine").description("Combine usage from your other machines over ssh").addHelpText(
|
|
41634
|
+
"after",
|
|
41635
|
+
formatHelpExamples([
|
|
41636
|
+
"llm-usage machine add laptop",
|
|
41637
|
+
"llm-usage machine list",
|
|
41638
|
+
"llm-usage machine remove laptop"
|
|
41639
|
+
])
|
|
41640
|
+
);
|
|
41641
|
+
const addCommand = new Command3("add").description(
|
|
41642
|
+
"Sync a machine over ssh and add it to config.toml; it needs llm-usage-metrics installed and ssh login without a prompt"
|
|
41643
|
+
).argument("<name>", "Name for the machine in reports (lowercase letters, digits, dashes)").argument(
|
|
41644
|
+
"[ssh-target]",
|
|
41645
|
+
"ssh destination: host, user@host, or an ssh_config alias (default: the name)"
|
|
41646
|
+
).option("--command <command>", "How llm-usage-metrics is launched there (default: llm-usage)").addHelpText(
|
|
41647
|
+
"after",
|
|
41648
|
+
formatHelpExamples([
|
|
41649
|
+
"llm-usage machine add laptop",
|
|
41650
|
+
"llm-usage machine add work me@10.0.0.5",
|
|
41651
|
+
"llm-usage machine add vps --command /home/me/.local/bin/llm-usage"
|
|
41652
|
+
])
|
|
41653
|
+
).action(
|
|
41654
|
+
(name, sshTarget, options) => runMachineAdd(name, sshTarget, options)
|
|
41655
|
+
);
|
|
41656
|
+
const listCommand = new Command3("list").description("Show each machine, when it last synced, and how much usage is cached").addHelpText("after", formatHelpExamples(["llm-usage machine list"])).action(() => runMachineList());
|
|
41657
|
+
const removeCommand = new Command3("remove").description("Remove a machine from config.toml and delete its cached usage").argument("<name>", "Machine name").addHelpText("after", formatHelpExamples(["llm-usage machine remove laptop"])).action((name) => runMachineRemove(name));
|
|
41658
|
+
const exportCommand = new Command3("export").description(
|
|
41659
|
+
"Print the usage this machine's reports count, as a versioned NDJSON bundle that sync reads"
|
|
41660
|
+
).option(
|
|
41661
|
+
"--known <path>",
|
|
41662
|
+
"Leave out files already held at the same revision, as listed in this JSON file (- for stdin)"
|
|
41663
|
+
).option("--quiet", "Suppress informational stderr output (warnings still print)").option(
|
|
41664
|
+
"--verbose",
|
|
41665
|
+
"Print detailed diagnostics on stderr (per-source counts, routine skips, config, env)"
|
|
41666
|
+
).addHelpText("after", formatHelpExamples(["llm-usage machine export > usage.ndjson"])).action((options) => runMachineExport(options));
|
|
41667
|
+
machineCommand.addCommand(addCommand);
|
|
41668
|
+
machineCommand.addCommand(listCommand);
|
|
41669
|
+
machineCommand.addCommand(removeCommand);
|
|
41670
|
+
machineCommand.addCommand(exportCommand);
|
|
41671
|
+
return machineCommand;
|
|
41672
|
+
}
|
|
41673
|
+
function createSyncCommand() {
|
|
41674
|
+
return new Command3("sync").description(
|
|
41675
|
+
"Fetch your other machines' usage now (reports also fetch it when it is 10 minutes old)"
|
|
41676
|
+
).argument("[names...]", "Machines to sync (default: every enabled machine)").option("--full", "Fetch every file again instead of only the changed ones").addHelpText("after", formatHelpExamples(["llm-usage sync", "llm-usage sync laptop --full"])).action((names, options) => runSync(names, options));
|
|
41677
|
+
}
|
|
41678
|
+
|
|
41679
|
+
// src/cli/create-completion-command.ts
|
|
41680
|
+
import { Command as Command4 } from "commander";
|
|
40024
41681
|
var COMPLETION_SHELLS = ["bash", "zsh", "fish"];
|
|
40025
41682
|
var PROGRAM_NAMES = ["llm-usage", "llm-usage-metrics"];
|
|
40026
41683
|
function isPathFlag(flag) {
|
|
@@ -40216,7 +41873,7 @@ function parseShell(value) {
|
|
|
40216
41873
|
return shell;
|
|
40217
41874
|
}
|
|
40218
41875
|
function createCompletionCommand(getProgram, argumentValues) {
|
|
40219
|
-
return new
|
|
41876
|
+
return new Command4("completion").description("Print a shell completion script (bash, zsh, or fish)").argument("<shell>", `Shell: ${COMPLETION_SHELLS.join(" | ")}`, parseShell).addHelpText(
|
|
40220
41877
|
"after",
|
|
40221
41878
|
formatHelpExamples([
|
|
40222
41879
|
"source <(llm-usage completion bash) # in ~/.bashrc",
|
|
@@ -40229,7 +41886,7 @@ function createCompletionCommand(getProgram, argumentValues) {
|
|
|
40229
41886
|
}
|
|
40230
41887
|
|
|
40231
41888
|
// src/cli/create-schema-command.ts
|
|
40232
|
-
import { Command as
|
|
41889
|
+
import { Command as Command5 } from "commander";
|
|
40233
41890
|
|
|
40234
41891
|
// schema/config.schema.json
|
|
40235
41892
|
var config_schema_default = {
|
|
@@ -40425,6 +42082,37 @@ var config_schema_default = {
|
|
|
40425
42082
|
minimum: 0,
|
|
40426
42083
|
maximum: 9007199254740991
|
|
40427
42084
|
},
|
|
42085
|
+
machines: {
|
|
42086
|
+
description: "Other machines whose usage reports include, by name. Each needs llm-usage-metrics installed and ssh access without a password prompt. Add one with `llm-usage machine add`.",
|
|
42087
|
+
type: "object",
|
|
42088
|
+
propertyNames: {
|
|
42089
|
+
pattern: "^[a-z0-9][a-z0-9-]{0,31}$",
|
|
42090
|
+
not: { const: "local" }
|
|
42091
|
+
},
|
|
42092
|
+
additionalProperties: {
|
|
42093
|
+
type: "object",
|
|
42094
|
+
additionalProperties: false,
|
|
42095
|
+
required: ["ssh"],
|
|
42096
|
+
properties: {
|
|
42097
|
+
ssh: {
|
|
42098
|
+
description: "ssh destination: host, user@host, or an alias from ~/.ssh/config.",
|
|
42099
|
+
type: "string",
|
|
42100
|
+
minLength: 1,
|
|
42101
|
+
pattern: "^[^-\\s][^\\s]*$"
|
|
42102
|
+
},
|
|
42103
|
+
command: {
|
|
42104
|
+
description: "How llm-usage-metrics is launched on that machine, e.g. a full path when a non-interactive ssh session does not find it.",
|
|
42105
|
+
type: "string",
|
|
42106
|
+
default: "llm-usage"
|
|
42107
|
+
},
|
|
42108
|
+
enabled: {
|
|
42109
|
+
description: "Set to false to keep the machine's cached usage in reports but stop syncing it.",
|
|
42110
|
+
type: "boolean",
|
|
42111
|
+
default: true
|
|
42112
|
+
}
|
|
42113
|
+
}
|
|
42114
|
+
}
|
|
42115
|
+
},
|
|
40428
42116
|
update: {
|
|
40429
42117
|
description: "Startup update-check runtime settings.",
|
|
40430
42118
|
type: "object",
|
|
@@ -40513,6 +42201,10 @@ var events_line_v1_schema_default = {
|
|
|
40513
42201
|
type: "number",
|
|
40514
42202
|
minimum: 0
|
|
40515
42203
|
},
|
|
42204
|
+
machine: {
|
|
42205
|
+
description: "The configured machine the event came from (see machine add); omitted for this machine's own usage.",
|
|
42206
|
+
type: "string"
|
|
42207
|
+
},
|
|
40516
42208
|
costMode: {
|
|
40517
42209
|
description: "Whether the cost came from the source (explicit) or from pricing (estimated).",
|
|
40518
42210
|
type: "string",
|
|
@@ -40523,6 +42215,92 @@ var events_line_v1_schema_default = {
|
|
|
40523
42215
|
unevaluatedProperties: false
|
|
40524
42216
|
};
|
|
40525
42217
|
|
|
42218
|
+
// schema/machine-export.v1.schema.json
|
|
42219
|
+
var machine_export_v1_schema_default = {
|
|
42220
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
42221
|
+
$id: "https://ayagmar.github.io/llm-usage-metrics/machine-export.v1.schema.json",
|
|
42222
|
+
title: "llm-usage-metrics machine export line (v1)",
|
|
42223
|
+
description: "One NDJSON line of `llm-usage machine export`. A bundle is a header line, zero or more file lines, then an end line; a bundle without its end line is incomplete.",
|
|
42224
|
+
oneOf: [{ $ref: "#/$defs/header" }, { $ref: "#/$defs/file" }, { $ref: "#/$defs/end" }],
|
|
42225
|
+
$defs: {
|
|
42226
|
+
header: {
|
|
42227
|
+
type: "object",
|
|
42228
|
+
properties: {
|
|
42229
|
+
type: { const: "header" },
|
|
42230
|
+
format: { const: "llm-usage-metrics.machine-export" },
|
|
42231
|
+
version: {
|
|
42232
|
+
description: "Bundle format version; readers reject versions they do not know.",
|
|
42233
|
+
const: 1
|
|
42234
|
+
},
|
|
42235
|
+
cliVersion: {
|
|
42236
|
+
description: "llm-usage-metrics version that wrote the bundle.",
|
|
42237
|
+
type: "string"
|
|
42238
|
+
},
|
|
42239
|
+
hostname: { description: "Host name of the exporting machine.", type: "string" },
|
|
42240
|
+
exportedAt: { description: "ISO 8601 time of the export.", type: "string" }
|
|
42241
|
+
},
|
|
42242
|
+
required: ["type", "format", "version", "cliVersion", "hostname", "exportedAt"],
|
|
42243
|
+
additionalProperties: false
|
|
42244
|
+
},
|
|
42245
|
+
file: {
|
|
42246
|
+
description: "The events of one counted file the reader does not hold at this revision.",
|
|
42247
|
+
type: "object",
|
|
42248
|
+
properties: {
|
|
42249
|
+
type: { const: "file" },
|
|
42250
|
+
source: { type: "string", minLength: 1 },
|
|
42251
|
+
filePath: {
|
|
42252
|
+
description: "Path of the file on the exporting machine.",
|
|
42253
|
+
type: "string",
|
|
42254
|
+
minLength: 1
|
|
42255
|
+
},
|
|
42256
|
+
revision: {
|
|
42257
|
+
description: "Opaque; changes exactly when the file's events change.",
|
|
42258
|
+
type: "string",
|
|
42259
|
+
minLength: 1
|
|
42260
|
+
},
|
|
42261
|
+
events: {
|
|
42262
|
+
description: "Unpriced usage events: costUsd is present only when the source recorded it (costMode explicit).",
|
|
42263
|
+
type: "array",
|
|
42264
|
+
minItems: 1,
|
|
42265
|
+
items: {
|
|
42266
|
+
$ref: "https://ayagmar.github.io/llm-usage-metrics/events-line.v1.schema.json"
|
|
42267
|
+
}
|
|
42268
|
+
}
|
|
42269
|
+
},
|
|
42270
|
+
required: ["type", "source", "filePath", "revision", "events"],
|
|
42271
|
+
additionalProperties: false
|
|
42272
|
+
},
|
|
42273
|
+
end: {
|
|
42274
|
+
description: "Every counted file, including those the reader already holds; a file it holds that is not listed is no longer counted.",
|
|
42275
|
+
type: "object",
|
|
42276
|
+
properties: {
|
|
42277
|
+
type: { const: "end" },
|
|
42278
|
+
files: {
|
|
42279
|
+
type: "array",
|
|
42280
|
+
items: {
|
|
42281
|
+
description: "[source, filePath, revision]",
|
|
42282
|
+
type: "array",
|
|
42283
|
+
prefixItems: [
|
|
42284
|
+
{ type: "string", minLength: 1 },
|
|
42285
|
+
{ type: "string", minLength: 1 },
|
|
42286
|
+
{ type: "string", minLength: 1 }
|
|
42287
|
+
],
|
|
42288
|
+
minItems: 3,
|
|
42289
|
+
maxItems: 3
|
|
42290
|
+
}
|
|
42291
|
+
},
|
|
42292
|
+
eventCount: {
|
|
42293
|
+
description: "Total events across all listed files.",
|
|
42294
|
+
type: "integer",
|
|
42295
|
+
minimum: 0
|
|
42296
|
+
}
|
|
42297
|
+
},
|
|
42298
|
+
required: ["type", "files", "eventCount"],
|
|
42299
|
+
additionalProperties: false
|
|
42300
|
+
}
|
|
42301
|
+
}
|
|
42302
|
+
};
|
|
42303
|
+
|
|
40526
42304
|
// schema/report-common.v1.schema.json
|
|
40527
42305
|
var report_common_v1_schema_default = {
|
|
40528
42306
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -40843,8 +42621,14 @@ var report_doctor_v1_schema_default = {
|
|
|
40843
42621
|
description: "Health of one source or the event store.",
|
|
40844
42622
|
type: "object",
|
|
40845
42623
|
properties: {
|
|
40846
|
-
id: {
|
|
40847
|
-
|
|
42624
|
+
id: {
|
|
42625
|
+
description: "Source id, event-store, or machine:<name> for a configured machine.",
|
|
42626
|
+
type: "string"
|
|
42627
|
+
},
|
|
42628
|
+
format: {
|
|
42629
|
+
description: "Storage format of the source; ssh for a machine.",
|
|
42630
|
+
type: "string"
|
|
42631
|
+
},
|
|
40848
42632
|
status: {
|
|
40849
42633
|
description: "Whether discovery ran (error: it failed). See state for whether usage was found.",
|
|
40850
42634
|
type: "string",
|
|
@@ -41782,6 +43566,10 @@ var report_usage_v1_schema_default = {
|
|
|
41782
43566
|
description: "Source id for per-source rows, or combined.",
|
|
41783
43567
|
type: "string"
|
|
41784
43568
|
},
|
|
43569
|
+
machine: {
|
|
43570
|
+
description: "With --by-machine, per-source rows only: the machine the usage came from (local for this one).",
|
|
43571
|
+
type: "string"
|
|
43572
|
+
},
|
|
41785
43573
|
models: {
|
|
41786
43574
|
description: "Model identifiers active in the row.",
|
|
41787
43575
|
type: "array",
|
|
@@ -42099,13 +43887,21 @@ var reportSchemas = {
|
|
|
42099
43887
|
var schemaDocuments = {
|
|
42100
43888
|
...reportSchemas,
|
|
42101
43889
|
"events-line": bundleCommonDefinitions(events_line_v1_schema_default),
|
|
43890
|
+
// Its events are events-line objects, so both resources are embedded.
|
|
43891
|
+
"machine-export": {
|
|
43892
|
+
...machine_export_v1_schema_default,
|
|
43893
|
+
$defs: {
|
|
43894
|
+
...machine_export_v1_schema_default.$defs,
|
|
43895
|
+
"events-line.v1": bundleCommonDefinitions(events_line_v1_schema_default)
|
|
43896
|
+
}
|
|
43897
|
+
},
|
|
42102
43898
|
config: config_schema_default
|
|
42103
43899
|
};
|
|
42104
43900
|
|
|
42105
43901
|
// src/cli/create-schema-command.ts
|
|
42106
43902
|
var schemaNames = Object.keys(schemaDocuments).sort();
|
|
42107
43903
|
function createSchemaCommand() {
|
|
42108
|
-
return new
|
|
43904
|
+
return new Command5("schema").description("Print a bundled report JSON Schema").argument("[name]", `Schema name: ${schemaNames.join(", ")}`).option("--list", "List available schema names").addHelpText(
|
|
42109
43905
|
"after",
|
|
42110
43906
|
formatHelpExamples([
|
|
42111
43907
|
"llm-usage schema --list",
|
|
@@ -42176,7 +43972,7 @@ function rejectUnknownCommand(program, args) {
|
|
|
42176
43972
|
program.error(`error: unknown command '${unknownCommand}'${hint}`);
|
|
42177
43973
|
}
|
|
42178
43974
|
function createCli(options = {}) {
|
|
42179
|
-
const program = new
|
|
43975
|
+
const program = new Command6();
|
|
42180
43976
|
program.name("llm-usage").description(createRootDescription()).version(options.version ?? "0.0.0").showHelpAfterError();
|
|
42181
43977
|
for (const command of createReportCommands()) {
|
|
42182
43978
|
const isSummary = isSummaryCommand(command);
|
|
@@ -42188,6 +43984,8 @@ function createCli(options = {}) {
|
|
|
42188
43984
|
program.addCommand(command, { isDefault: isSummary });
|
|
42189
43985
|
}
|
|
42190
43986
|
program.addCommand(createConfigCommand());
|
|
43987
|
+
program.addCommand(createMachineCommand());
|
|
43988
|
+
program.addCommand(createSyncCommand());
|
|
42191
43989
|
program.addCommand(createSchemaCommand());
|
|
42192
43990
|
program.addCommand(
|
|
42193
43991
|
createCompletionCommand(() => program, {
|
|
@@ -42211,47 +44009,6 @@ function createCli(options = {}) {
|
|
|
42211
44009
|
return program;
|
|
42212
44010
|
}
|
|
42213
44011
|
|
|
42214
|
-
// src/cli/package-metadata.ts
|
|
42215
|
-
import { createRequire as createRequire2 } from "module";
|
|
42216
|
-
var DEFAULT_PACKAGE_NAME = "llm-usage-metrics";
|
|
42217
|
-
var DEFAULT_PACKAGE_VERSION = "0.0.0";
|
|
42218
|
-
var defaultPackageJsonCandidates = ["../package.json", "../../package.json"];
|
|
42219
|
-
function normalizeMetadata(candidate) {
|
|
42220
|
-
const packageJson = asRecord(candidate);
|
|
42221
|
-
if (!packageJson) {
|
|
42222
|
-
return void 0;
|
|
42223
|
-
}
|
|
42224
|
-
const packageName = typeof packageJson.name === "string" ? packageJson.name.trim() : void 0;
|
|
42225
|
-
const packageVersion = typeof packageJson.version === "string" ? packageJson.version.trim() : void 0;
|
|
42226
|
-
if (!packageName || !packageVersion) {
|
|
42227
|
-
return void 0;
|
|
42228
|
-
}
|
|
42229
|
-
return {
|
|
42230
|
-
packageName,
|
|
42231
|
-
packageVersion
|
|
42232
|
-
};
|
|
42233
|
-
}
|
|
42234
|
-
function resolvePackageMetadata(loadJson, packageJsonCandidates = defaultPackageJsonCandidates) {
|
|
42235
|
-
for (const candidatePath of packageJsonCandidates) {
|
|
42236
|
-
try {
|
|
42237
|
-
const metadata = normalizeMetadata(loadJson(candidatePath));
|
|
42238
|
-
if (metadata) {
|
|
42239
|
-
return metadata;
|
|
42240
|
-
}
|
|
42241
|
-
} catch {
|
|
42242
|
-
continue;
|
|
42243
|
-
}
|
|
42244
|
-
}
|
|
42245
|
-
return {
|
|
42246
|
-
packageName: DEFAULT_PACKAGE_NAME,
|
|
42247
|
-
packageVersion: DEFAULT_PACKAGE_VERSION
|
|
42248
|
-
};
|
|
42249
|
-
}
|
|
42250
|
-
function loadPackageMetadataFromRuntime() {
|
|
42251
|
-
const require3 = createRequire2(import.meta.url);
|
|
42252
|
-
return resolvePackageMetadata((candidatePath) => require3(candidatePath));
|
|
42253
|
-
}
|
|
42254
|
-
|
|
42255
44012
|
// src/cli/index.ts
|
|
42256
44013
|
async function loadConfigForUpdateCheck() {
|
|
42257
44014
|
try {
|