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/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, mkdir, open, rm as rm2, writeFile as writeFile2 } from "fs/promises";
12
- import path3 from "path";
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 path3.join(getUserDataRootDir(), "llm-usage-metrics", "events.db");
1012
+ return path4.join(getUserDataRootDir(), "llm-usage-metrics", "events.db");
969
1013
  }
970
1014
  function getLegacyEventStorePath() {
971
- return path3.join(getUserCacheRootDir(), "llm-usage-metrics", "events.db");
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 path3.join(path3.dirname(targetPath), "legacy-ledger-copied");
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 (!isErrorCode(error, "EEXIST")) {
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 (isErrorCode(error, "ENOENT")) {
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 = path3.dirname(filePath);
1040
- await mkdir(parentDirectory, { recursive: true, mode: 448 });
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 path5 from "path";
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 path4 from "path";
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 && path4.isAbsolute(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 path4.join(homedir, ".config");
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 path5.join(
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 path5.join(path5.dirname(configPath), "config.json");
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 (isMissingFileError(error)) {
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(`~${path5.sep}`)) {
2374
- return path5.join(os4.homedir(), value.slice(2));
2551
+ if (value.startsWith("~/") || value.startsWith(`~${path6.sep}`)) {
2552
+ return path6.join(os4.homedir(), value.slice(2));
2375
2553
  }
2376
- return path5.resolve(configDir, value);
2554
+ return path6.resolve(configDir, value);
2377
2555
  }
2378
2556
  function readPath(context, keyPath, value) {
2379
- const text = readKey(context, keyPath, value, toNonBlankString, "a non-empty path");
2380
- return text === void 0 ? void 0 : resolveConfigPath(text, context.configDir);
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 = (filePath) => readFileFromFs(filePath, "utf8")) {
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 (isMissingFileError(error)) {
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: path5.dirname(path5.resolve(configPath)),
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 { mkdir as mkdir2, readFile } from "fs/promises";
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 path6.join(getUserCacheRootDir(), "llm-usage-metrics", "update-check.json");
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 = path6.parse(baseCacheFilePath);
2840
- return path6.join(parsedPath.dir, `${parsedPath.name}.${sessionKey}${parsedPath.ext}`);
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 readFile(cacheFilePath, "utf8");
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 mkdir2(path6.dirname(cacheFilePath), { recursive: true });
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 (isBrokenPipeError(error)) {
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 Command5 } from "commander";
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 rowAccumulator = periodSources.get(event.source) ?? createRowAccumulator(includeModelBreakdown);
3579
- periodSources.set(event.source, rowAccumulator);
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 sortedSources = [...sourceMap.keys()].sort(
3594
- (left, right4) => sourceSortComparator(left, right4, sourceWeightMap)
3874
+ const sortedRowKeys = [...sourceMap.keys()].sort(
3875
+ (left, right4) => compareRowKeys(left, right4, sourceWeightMap, options.byMachine === true)
3595
3876
  );
3596
- for (const source of sortedSources) {
3597
- const accumulator = sourceMap.get(source);
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 (sortedSources.length > 1) {
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 path8 from "path";
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 path7 from "path";
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 = path7.join(rootDir, entry.name);
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(path7.basename(resolvedEntryPath), options.extension))) {
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) ?? path8.join(homeDir, ".local", "share");
4144
- return path8.join(dataHome, "amp", "threads");
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 text = asTrimmedText(value);
4148
- if (text) {
4149
- return text;
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 path10 from "path";
4678
+ import path11 from "path";
4396
4679
 
4397
4680
  // src/sources/antigravity/antigravity-path-resolver.ts
4398
4681
  import os6 from "os";
4399
- import path9 from "path";
4682
+ import path10 from "path";
4400
4683
  function resolveGeminiCliHome(options) {
4401
- return asTrimmedText(options.env.GEMINI_CLI_HOME) ?? path9.join(options.homeDir, ".gemini");
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 path9.join(resolveGeminiCliHome(resolvedOptions), "antigravity-cli", "conversations");
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: path10.basename(normalizedDbPath, path10.extname(normalizedDbPath)),
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 path13 from "path";
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 path11 from "path";
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 = path11.resolve(filePath);
5206
+ const resolvedFilePath = path12.resolve(filePath);
4924
5207
  return rootDirs.some((rootDir) => {
4925
- const relativePath = path11.relative(path11.resolve(rootDir.trim()), resolvedFilePath);
4926
- return relativePath.length > 0 && !relativePath.startsWith("..") && !path11.isAbsolute(relativePath);
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) => [path11.resolve(filePath), filePath])).values()
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 path12 from "path";
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(path12.basename(filePath));
4957
- if (!match || path12.basename(path12.dirname(filePath)) !== SUBAGENTS_DIR_NAME) {
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 path12.join(path12.dirname(filePath), `agent-${match[1]}.meta.json`);
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 = path12.dirname(filePath);
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`) ? path12.join(subagentsDir, `agent-${parentAgentId}.jsonl`) : void 0;
5258
+ return SUBAGENT_TRANSCRIPT_PATTERN.test(`agent-${parentAgentId}.jsonl`) ? path13.join(subagentsDir, `agent-${parentAgentId}.jsonl`) : void 0;
4976
5259
  }
4977
- return `${path12.dirname(subagentsDir)}.jsonl`;
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 [path13.join(configDir, "projects"), path13.join(configDir, "transcripts")];
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(path13.join(homeDir, ".claude"));
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 path13.basename(filePath, ".jsonl");
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 path15 from "path";
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 path14 from "path";
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 path14.basename(path14.dirname(filePath));
5528
+ return path15.basename(path15.dirname(filePath));
5246
5529
  }
5247
5530
  function getClineTaskHistoryPath(uiMessagesPath) {
5248
- return path14.join(path14.dirname(uiMessagesPath), "api_conversation_history.json");
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 text = asTrimmedText(entry.text);
5284
- if (!text) {
5566
+ const text2 = asTrimmedText(entry.text);
5567
+ if (!text2) {
5285
5568
  return void 0;
5286
5569
  }
5287
5570
  try {
5288
- return asRecord(JSON.parse(text));
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 path15.join(codeRoot, "User", "globalStorage", extensionId, "tasks");
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) ?? path15.join(homeDir, "AppData", "Roaming");
5409
- if (appDataRoot.endsWith(`${path15.sep}Code`) || path15.basename(appDataRoot) === "Code") {
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 path15.join(appDataRoot, "Code");
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 [path15.join(homeDir, "Library", "Application Support", "Code")];
5702
+ return [path16.join(homeDir, "Library", "Application Support", "Code")];
5420
5703
  default:
5421
- return [path15.join(homeDir, ".config", "Code")];
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 = [path15.join(homeDir, ".vscode-server", "data")];
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) => path15.basename(filePath) === "ui_messages.json");
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 path16 from "path";
5765
+ import path17 from "path";
5483
5766
  function resolveDefaultCodexSessionsDir(env = process.env, homeDir = os9.homedir()) {
5484
- return path16.join(asTrimmedText(env.CODEX_HOME) ?? path16.join(homeDir, ".codex"), "sessions");
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 path16.basename(filePath, ".jsonl");
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 path17 from "path";
6097
+ import path18 from "path";
5815
6098
  import { realpath as realpath2 } from "fs/promises";
5816
- var defaultOtelDir = path17.join(os10.homedir(), ".copilot", "otel");
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 path17.basename(filePath, ".jsonl");
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 path18 from "path";
6130
- var defaultSessionsDir = path18.join(os11.homedir(), ".factory", "sessions");
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 path18.basename(filePath, ".settings.json");
6420
+ return path19.basename(filePath, ".settings.json");
6138
6421
  }
6139
6422
  function getSiblingJsonlPath(settingsPath) {
6140
- return path18.join(path18.dirname(settingsPath), `${getSettingsSessionId(settingsPath)}.jsonl`);
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 path20 from "path";
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(text, options) {
6351
- for (const rawLine of text.split("\n")) {
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 path19 from "path";
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 path19.join(options.homeDir, ".dsh");
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 path19.join(options.homeDir, configuredHome.slice(2));
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 path19.join(resolveDshHome(resolvedOptions), "sessions");
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 = path20.basename(path20.dirname(filePath));
6719
+ const parentDirName = path21.basename(path21.dirname(filePath));
6437
6720
  if (parentDirName.startsWith("session-")) {
6438
6721
  return parentDirName;
6439
6722
  }
6440
- return path20.basename(filePath).replace(/\.zstd$/u, "");
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(path20.basename(filePath)));
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 createHash2 } from "crypto";
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 path21 from "path";
6596
- var defaultGeminiDir = path21.join(os13.homedir(), ".gemini");
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 path21.join(geminiDir, "projects.json");
6884
+ return path22.join(geminiDir, "projects.json");
6602
6885
  }
6603
6886
  function hashProjectPath(projectPath) {
6604
- return createHash2("sha256").update(projectPath).digest("hex");
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 = path21.join(geminiDir, "tmp");
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 = path21.join(tmpDir, projectEntry.name, "chats");
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 = path21.dirname(filePath);
6680
- const projectDir = path21.dirname(chatsDir);
6681
- const projectIdentifier = path21.basename(projectDir);
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) ?? path21.basename(filePath, ".json");
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 path22 from "path";
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 path22.join(rootDir, "goose", "sessions", "sessions.db");
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) ?? path22.join(homeDir, ".local", "share");
7152
+ const xdgDataHome = normalizeEnvPath3(env.XDG_DATA_HOME) ?? path23.join(homeDir, ".local", "share");
6870
7153
  return [
6871
7154
  getGooseDbPath(xdgDataHome),
6872
- path22.join(homeDir, ".local", "share", "Block", "goose", "sessions", "sessions.db")
7155
+ path23.join(homeDir, ".local", "share", "Block", "goose", "sessions", "sessions.db")
6873
7156
  ];
6874
7157
  }
6875
7158
  function getMacOsCandidates(homeDir) {
6876
7159
  return [
6877
- path22.join(homeDir, "Library", "Application Support", "goose", "sessions", "sessions.db"),
6878
- path22.join(homeDir, ".local", "share", "Block", "goose", "sessions", "sessions.db")
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 ? path22.join(userProfile, "AppData", "Roaming") : void 0);
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
- path22.join(homeDir, ".local", "share", "Block", "goose", "sessions", "sessions.db")
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 ? [path22.join(goosePathRoot, "data", "sessions", "sessions.db")] : [];
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 path23 from "path";
7098
- var defaultKimiCliSessionsDir = path23.join(os15.homedir(), ".kimi", "sessions");
7099
- var defaultKimiCodeSessionsDir = path23.join(os15.homedir(), ".kimi-code", "sessions");
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 path23.basename(path23.dirname(filePath));
7429
+ return path24.basename(path24.dirname(filePath));
7147
7430
  }
7148
7431
  function getCliConfigPath(filePath) {
7149
- const sessionsDir = path23.dirname(path23.dirname(path23.dirname(filePath)));
7150
- return path23.join(path23.dirname(sessionsDir), "config.json");
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 path23.basename(path23.dirname(path23.dirname(filePath))) === "agents";
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 = path23.dirname(filePath);
7168
- const agentsDir = path23.dirname(agentDir);
7169
- const sessionDir = path23.dirname(agentsDir);
7170
- return path23.basename(sessionDir);
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) => path23.basename(filePath) === "wire.jsonl");
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 path25 from "path";
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 path24 from "path";
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
- path24.join(directory, "opencode.db"),
7366
- ...listChannelDbFileNames(directory).map((fileName) => path24.join(directory, fileName)),
7367
- path24.join(directory, "db.sqlite")
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) ?? path24.join(homeDir, ".local", "share");
7655
+ const xdgDataHome = normalizeEnvPath4(env.XDG_DATA_HOME) ?? path25.join(homeDir, ".local", "share");
7372
7656
  return [
7373
- ...getDirectoryCandidates(path24.join(xdgDataHome, "opencode")),
7374
- ...getDirectoryCandidates(path24.join(homeDir, ".opencode"))
7657
+ ...getDirectoryCandidates(path25.join(xdgDataHome, "opencode")),
7658
+ ...getDirectoryCandidates(path25.join(homeDir, ".opencode"))
7375
7659
  ];
7376
7660
  }
7377
7661
  function getMacOsCandidates2(homeDir) {
7378
- const appSupportDir = path24.join(homeDir, "Library", "Application Support");
7662
+ const appSupportDir = path25.join(homeDir, "Library", "Application Support");
7379
7663
  return [
7380
- ...getDirectoryCandidates(path24.join(appSupportDir, "opencode")),
7381
- ...getDirectoryCandidates(path24.join(homeDir, ".opencode"))
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 ? path24.join(userProfile, "AppData", "Roaming") : void 0);
7387
- const roamingCandidates = roamingBase ? getDirectoryCandidates(path24.join(roamingBase, "opencode")) : [];
7388
- return [...roamingCandidates, ...getDirectoryCandidates(path24.join(homeDir, ".opencode"))];
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 (isBlankText2(this.explicitDbPath)) {
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 = path25.dirname(candidatePath);
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) => path25.basename(candidatePath) !== "db.sqlite"
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 (isBlankText2(dbPath)) {
8091
+ if (isBlankText(dbPath)) {
7816
8092
  return [];
7817
8093
  }
7818
8094
  return getOpenCodeParseDependencies(dbPath.trim());
7819
8095
  }
7820
8096
  async parseFileWithDiagnostics(dbPath) {
7821
- if (isBlankText2(dbPath)) {
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 path27 from "path";
8129
+ import path28 from "path";
7854
8130
 
7855
8131
  // src/sources/openclaw/openclaw-session-parser.ts
7856
- import path26 from "path";
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 path26.basename(filePath, ".jsonl");
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 = path27.join(os17.homedir(), ".openclaw", "agents");
8333
+ var defaultAgentsDir = path28.join(os17.homedir(), ".openclaw", "agents");
8058
8334
  var defaultOpenClawRootDirs = [
8059
8335
  defaultAgentsDir,
8060
- path27.join(os17.homedir(), ".clawdbot", "agents"),
8061
- path27.join(os17.homedir(), ".moltbot", "agents"),
8062
- path27.join(os17.homedir(), ".moldbot", "agents")
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 path28 from "path";
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 = path28.join(os18.homedir(), ".pi", "agent", "sessions");
8399
+ var defaultSessionsDir2 = path29.join(os18.homedir(), ".pi", "agent", "sessions");
8124
8400
  var defaultPiRootDirs = [
8125
8401
  defaultSessionsDir2,
8126
- path28.join(os18.homedir(), ".omp", "agent", "sessions")
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 ? path28.resolve(path28.dirname(filePath), parentSession) : void 0;
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 path28.basename(filePath, ".jsonl");
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 path29 from "path";
8320
- var defaultProjectsDir = path29.join(os19.homedir(), ".qwen", "projects");
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 = path29.dirname(filePath);
8369
- const projectDir = path29.dirname(chatsDir);
8370
- const projectName = path29.basename(projectDir);
8371
- const fileStem = path29.basename(filePath, ".jsonl");
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 path30 from "path";
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 = path30.resolve(filePath);
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: path38, requireExistingPath }) => ({
8514
- dir: path38,
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: path38 }) => new OpenCodeSourceAdapter({ dbPath: path38 })
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: path38 }) => new GooseSourceAdapter({ dbPath: path38 })
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 toNonNegativeInteger4(value) ?? 0;
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 = toNonNegativeInteger4(row.ingested_at);
9266
- const eventCount = toNonNegativeInteger4(row.event_count);
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 isMissingPathError(error) ? "missing" : "unknown";
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 && isMissingPathError2(error)) {
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
- ...filePaths.map((filePath) => ({
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 readFile7(filePath, "utf8");
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 { mkdir as mkdir3, readFile as readFile8 } from "fs/promises";
10603
- import path31 from "path";
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 path31.join(getUserCacheRootDir(), "llm-usage-metrics", "litellm-pricing-cache.json");
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 text = await response.text();
29668
- if (Buffer.byteLength(text, "utf8") > this.maxResponseBytes) {
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(text);
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 readFile8(this.cacheFilePath, "utf8");
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 = path31.dirname(this.cacheFilePath);
29776
- await mkdir3(directoryPath, { recursive: true });
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 includeHistory = configuredOptions.history !== false;
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 = (text) => text;
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 ?? ((text) => palette.bold(palette.green(text)));
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")} (${sourcesWithFiles.map((session) => `${session.source} ${integerFormatter3.format(session.filesFound)}`).join(", ")})`
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
- if (totalFiles > 0) {
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 path32 from "path";
34253
+ import path34 from "path";
32921
34254
  async function writeShareFile(fileName, content) {
32922
- const outputPath = path32.resolve(process.cwd(), fileName);
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(path32.delimiter).filter(Boolean);
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 = path32.join(dir, fallbackName);
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 = spawn(command, args, {
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
- (stat9, index) => renderStat({ theme, x: SHARE_MARGIN + index * statPitch, y: statsTop, size: 30, ...stat9 })
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 spawn2 } from "child_process";
33832
- import { createInterface } from "readline";
33833
- import path33 from "path";
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 path33.resolve(process.cwd());
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 path33.resolve(normalizedRepoDir);
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 = spawn2("git", args, {
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 = createInterface({ input: child.stdout });
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 path34 from "path";
35573
+ import path36 from "path";
34241
35574
  async function hasGitMarker(directoryPath) {
34242
35575
  try {
34243
- await access3(path34.join(directoryPath, ".git"), constants3.F_OK);
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 = path34.normalize(path34.resolve(value));
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 = path34.resolve(value);
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 = path34.resolve(trimmedPath);
35599
+ let currentPath = path36.resolve(trimmedPath);
34267
35600
  for (; ; ) {
34268
35601
  if (await hasGitMarker(currentPath)) {
34269
35602
  return currentPath;
34270
35603
  }
34271
- const parentPath = path34.dirname(currentPath);
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 path35 from "path";
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 ? path35.basename(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 ? path35.basename(row.repoRoot) : "(no repo)",
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) + MONTH_LABELS2[Number(month) - 1];
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) => (text) => useColor ? styler(text) : text;
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((text) => palette.bold(palette.yellow(text))),
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((text) => palette.bold(palette.green(text))) : paint(palette.green)
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 = (text) => useColor ? palette.yellow(text) : text;
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 `${MONTH_NAMES[Number(date.slice(5, 7)) - 1]} ${Number(date.slice(8, 10))}`;
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
- (stat9, index) => renderStat({
38346
+ (stat10, index) => renderStat({
37042
38347
  theme,
37043
38348
  x: SHARE_MARGIN + index * statPitch3,
37044
38349
  y: statsTop4,
37045
38350
  size: 30,
37046
- ...stat9
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
- renderGridLines(scaleMax, data.metric, theme),
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 = MONTH_NAMES2[Number(period.slice(5, 7)) - 1] ?? period;
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
- renderGridLines2(maxY, theme),
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 path36 from "path";
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 + path36.sep)) {
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.id !== "event-store");
37976
- return [...lines, "", formatSummary(sourceResults)].join("\n");
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 = selectDoctorAdapters(
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 (isMissingPathError3(error)) {
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 (isMissingPathError4(error)) {
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 (isMissingPathError4(error)) {
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 = selectPruneAdapters(configuredOptions, makeAdapters(configuredOptions));
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 text = typeof value === "string" && /^[=+\-@\t\r]/u.test(raw) ? `'${raw}` : raw;
38507
- return /[",\n\r]/u.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
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) => (text) => palette.bold(styler(text));
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, stat9) => Math.max(max, stat9.label.length), 0);
39914
+ const labelWidth = stats.reduce((max, stat10) => Math.max(max, stat10.label.length), 0);
38645
39915
  return stats.map(
38646
- (stat9) => `${paint(palette.dim)(stat9.label.padEnd(labelWidth))} ${paint(stat9.styler)(stat9.value)}`
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 = (text) => palette.bold(palette.green(text));
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 = MONTH_LABELS3[index] ?? month.month;
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) => (text) => useColor ? styler(text) : text;
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
- (stat9, index) => renderStat({ theme, x: statColumns[index], y: statsTop6, size: 32, ...stat9 })
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, mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
39896
- import path37 from "path";
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 mkdir4(path37.dirname(configPath), { recursive: true });
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 (isExistingFileError(error)) {
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-completion-command.ts
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 Command3("completion").description("Print a shell completion script (bash, zsh, or fish)").argument("<shell>", `Shell: ${COMPLETION_SHELLS.join(" | ")}`, parseShell).addHelpText(
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 Command4 } from "commander";
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: { description: "Source id, or event-store.", type: "string" },
40847
- format: { description: "Storage format of the source.", type: "string" },
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 Command4("schema").description("Print a bundled report JSON Schema").argument("[name]", `Schema name: ${schemaNames.join(", ")}`).option("--list", "List available schema names").addHelpText(
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 Command5();
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 {