@xerg/cli 0.2.0 → 0.3.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
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync8 } from "fs";
5
4
  import { styleText as styleText2 } from "util";
6
5
 
7
6
  // src/command-display.ts
@@ -111,7 +110,6 @@ function normalizeSignal(value) {
111
110
  }
112
111
 
113
112
  // src/commands/audit.ts
114
- import { readFileSync as readFileSync6 } from "fs";
115
113
  import { rmSync as rmSync4 } from "fs";
116
114
 
117
115
  // ../core/src/cursor/usage-csv.ts
@@ -314,6 +312,7 @@ function createDetectedSource(path) {
314
312
  }
315
313
  return {
316
314
  kind: "cursor-usage-csv",
315
+ runtime: "cursor",
317
316
  path: resolvedPath,
318
317
  sizeBytes: stats.size,
319
318
  mtimeMs: stats.mtimeMs
@@ -1140,6 +1139,7 @@ function buildComparisonKey(input) {
1140
1139
  ).sort();
1141
1140
  return sha1(
1142
1141
  JSON.stringify({
1142
+ runtime: input.runtime,
1143
1143
  kinds,
1144
1144
  roots,
1145
1145
  since: normalizeSinceValue(input.since)
@@ -1250,13 +1250,31 @@ function buildFindingChanges(currentFindings, baselineFindings) {
1250
1250
  worsenedHighConfidenceWaste: sortFindingChanges(worsenedHighConfidenceWaste)
1251
1251
  };
1252
1252
  }
1253
+ function inferSummaryRuntime(summary) {
1254
+ if ("runtime" in summary && summary.runtime) {
1255
+ return summary.runtime;
1256
+ }
1257
+ if (summary.sourceFiles.some((source) => source.kind === "cursor-usage-csv")) {
1258
+ return "cursor";
1259
+ }
1260
+ return "openclaw";
1261
+ }
1253
1262
  function hydrateAuditSummary(summary) {
1263
+ const runtime = inferSummaryRuntime(summary);
1264
+ const shouldRebuildComparisonKey = !("runtime" in summary) || !summary.runtime || !summary.comparisonKey;
1265
+ const hydratedSources = summary.sourceFiles.map((source) => ({
1266
+ ...source,
1267
+ runtime: source.runtime ?? (source.kind === "cursor-usage-csv" ? "cursor" : runtime === "cursor" ? "openclaw" : runtime)
1268
+ }));
1254
1269
  return {
1255
1270
  ...summary,
1256
- comparisonKey: summary.comparisonKey ?? buildComparisonKey({
1257
- sources: summary.sourceFiles,
1271
+ runtime,
1272
+ sourceFiles: hydratedSources,
1273
+ comparisonKey: shouldRebuildComparisonKey ? buildComparisonKey({
1274
+ runtime,
1275
+ sources: hydratedSources,
1258
1276
  since: summary.since
1259
- }),
1277
+ }) : summary.comparisonKey,
1260
1278
  comparison: summary.comparison ?? null,
1261
1279
  wasteByKind: summary.wasteByKind?.length > 0 ? summary.wasteByKind : buildTaxonomyBuckets(summary.findings, "waste"),
1262
1280
  opportunityByKind: summary.opportunityByKind?.length > 0 ? summary.opportunityByKind : buildTaxonomyBuckets(summary.findings, "opportunity"),
@@ -1330,175 +1348,6 @@ function readLatestComparableAuditSummary(input) {
1330
1348
  });
1331
1349
  }
1332
1350
 
1333
- // ../core/src/detect/openclaw.ts
1334
- import { readdirSync, statSync as statSync2 } from "fs";
1335
- import { isAbsolute, join as join2, resolve as resolve2, sep } from "path";
1336
-
1337
- // ../core/src/utils/paths.ts
1338
- import { mkdirSync as mkdirSync2 } from "fs";
1339
- import { homedir } from "os";
1340
- import { join } from "path";
1341
- import { platform } from "process";
1342
- function getAppPaths() {
1343
- const home = homedir();
1344
- return platform === "darwin" ? {
1345
- data: join(home, "Library", "Application Support", "xerg"),
1346
- config: join(home, "Library", "Preferences", "xerg"),
1347
- cache: join(home, "Library", "Caches", "xerg")
1348
- } : platform === "win32" ? {
1349
- data: join(process.env.LOCALAPPDATA ?? join(home, "AppData", "Local"), "xerg", "Data"),
1350
- config: join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "xerg", "Config"),
1351
- cache: join(process.env.LOCALAPPDATA ?? join(home, "AppData", "Local"), "xerg", "Cache")
1352
- } : {
1353
- data: join(process.env.XDG_DATA_HOME ?? join(home, ".local", "share"), "xerg"),
1354
- config: join(process.env.XDG_CONFIG_HOME ?? join(home, ".config"), "xerg"),
1355
- cache: join(process.env.XDG_CACHE_HOME ?? join(home, ".cache"), "xerg")
1356
- };
1357
- }
1358
- function getDefaultDbPath() {
1359
- return join(getAppPaths().data, "xerg.db");
1360
- }
1361
- function getDefaultSessionsPattern() {
1362
- return join(homedir(), ".openclaw", "agents", "*", "sessions", "*.jsonl");
1363
- }
1364
- function getDefaultGatewayPattern() {
1365
- return "/tmp/openclaw/openclaw-*.log";
1366
- }
1367
-
1368
- // ../core/src/detect/openclaw.ts
1369
- function toDetected(path, kind) {
1370
- try {
1371
- const stats = statSync2(path);
1372
- if (!stats.isFile()) {
1373
- return null;
1374
- }
1375
- return {
1376
- kind,
1377
- path,
1378
- sizeBytes: stats.size,
1379
- mtimeMs: stats.mtimeMs
1380
- };
1381
- } catch {
1382
- return null;
1383
- }
1384
- }
1385
- async function detectOpenClawSources(options) {
1386
- const explicitSources = [];
1387
- if (options.logFile) {
1388
- const detected2 = toDetected(options.logFile, "gateway");
1389
- if (detected2) {
1390
- explicitSources.push(detected2);
1391
- }
1392
- }
1393
- if (options.sessionsDir) {
1394
- const matches = await collectGlobMatches("**/*.jsonl", {
1395
- cwd: options.sessionsDir,
1396
- resolveWith: options.sessionsDir
1397
- });
1398
- for (const match of matches) {
1399
- const detected2 = toDetected(match, "sessions");
1400
- if (detected2) {
1401
- explicitSources.push(detected2);
1402
- }
1403
- }
1404
- }
1405
- if (explicitSources.length > 0) {
1406
- return explicitSources.sort((left, right) => right.mtimeMs - left.mtimeMs);
1407
- }
1408
- const [gatewayMatches, sessionMatches] = await Promise.all([
1409
- collectGlobMatches(getDefaultGatewayPattern()),
1410
- collectGlobMatches(getDefaultSessionsPattern())
1411
- ]);
1412
- const detected = [
1413
- ...gatewayMatches.map((path) => toDetected(path, "gateway")).filter(Boolean),
1414
- ...sessionMatches.map((path) => toDetected(path, "sessions")).filter(Boolean)
1415
- ];
1416
- return detected.sort((left, right) => right.mtimeMs - left.mtimeMs);
1417
- }
1418
- async function collectGlobMatches(pattern, options) {
1419
- const baseDir = options?.cwd ? resolve2(options.cwd) : isAbsolute(pattern) ? sep : process.cwd();
1420
- const relativePattern = options?.cwd ? pattern : isAbsolute(pattern) ? pattern.slice(baseDir.length) : pattern;
1421
- const segments = relativePattern.split("/").filter(Boolean);
1422
- const matches = collectMatchesFromSegments(baseDir, segments);
1423
- return matches.map(
1424
- (match) => options?.resolveWith ? resolve2(options.resolveWith, match) : match
1425
- );
1426
- }
1427
- function collectMatchesFromSegments(currentPath, segments) {
1428
- if (segments.length === 0) {
1429
- return [currentPath];
1430
- }
1431
- const [segment, ...rest] = segments;
1432
- if (segment === "**") {
1433
- const matches2 = collectMatchesFromSegments(currentPath, rest);
1434
- for (const entry of readDirSafe(currentPath)) {
1435
- if (entry.isDirectory()) {
1436
- matches2.push(...collectMatchesFromSegments(join2(currentPath, entry.name), segments));
1437
- }
1438
- }
1439
- return matches2;
1440
- }
1441
- const matches = [];
1442
- const matcher = segmentToRegExp(segment);
1443
- for (const entry of readDirSafe(currentPath)) {
1444
- if (!matcher.test(entry.name)) {
1445
- continue;
1446
- }
1447
- const nextPath = join2(currentPath, entry.name);
1448
- if (rest.length === 0) {
1449
- matches.push(nextPath);
1450
- continue;
1451
- }
1452
- if (entry.isDirectory()) {
1453
- matches.push(...collectMatchesFromSegments(nextPath, rest));
1454
- }
1455
- }
1456
- return matches;
1457
- }
1458
- function readDirSafe(path) {
1459
- try {
1460
- return readdirSync(path, { withFileTypes: true });
1461
- } catch {
1462
- return [];
1463
- }
1464
- }
1465
- function segmentToRegExp(segment) {
1466
- const escaped = segment.replaceAll(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
1467
- return new RegExp(`^${escaped}$`);
1468
- }
1469
- async function inspectOpenClawSources(options) {
1470
- options.onProgress?.("Checking local OpenClaw defaults...");
1471
- const sources = await detectOpenClawSources(options);
1472
- const notes = [];
1473
- options.onProgress?.(
1474
- sources.length > 0 ? `Detected ${sources.length} local source file${sources.length === 1 ? "" : "s"}.` : "No local OpenClaw source files were detected."
1475
- );
1476
- if (sources.length === 0) {
1477
- notes.push("No OpenClaw gateway logs or session files were detected.");
1478
- notes.push(
1479
- "Doctor checks local defaults by default. Use --remote or --railway to inspect remote targets."
1480
- );
1481
- notes.push(
1482
- "Use --log-file or --sessions-dir if your OpenClaw data lives outside the defaults."
1483
- );
1484
- }
1485
- if (sources.some((source) => source.kind === "gateway")) {
1486
- notes.push("Gateway logs detected. These are preferred when cost metadata is present.");
1487
- }
1488
- if (sources.some((source) => source.kind === "sessions")) {
1489
- notes.push("Session transcript fallback detected. Xerg will extract usage metadata only.");
1490
- }
1491
- return {
1492
- canAudit: sources.length > 0,
1493
- sources,
1494
- defaults: {
1495
- gatewayPattern: getDefaultGatewayPattern(),
1496
- sessionsPattern: getDefaultSessionsPattern()
1497
- },
1498
- notes
1499
- };
1500
- }
1501
-
1502
1351
  // ../core/src/findings/cursor.ts
1503
1352
  function round3(value) {
1504
1353
  return Number(value.toFixed(6));
@@ -1778,10 +1627,6 @@ function buildFindings(runs) {
1778
1627
  };
1779
1628
  }
1780
1629
 
1781
- // ../core/src/normalize/openclaw.ts
1782
- import { readFileSync as readFileSync2 } from "fs";
1783
- import { basename } from "path";
1784
-
1785
1630
  // ../core/src/pricing-catalog.ts
1786
1631
  var PRICING_CATALOG = [
1787
1632
  {
@@ -1858,75 +1703,860 @@ function estimateCostUsd(provider, model, inputTokens, outputTokens) {
1858
1703
  return Number((inputCost + outputCost).toFixed(8));
1859
1704
  }
1860
1705
 
1861
- // ../core/src/utils/records.ts
1862
- function getNestedValue(input, paths) {
1863
- if (!input || typeof input !== "object") {
1706
+ // ../core/src/report/timeseries.ts
1707
+ function round5(value) {
1708
+ return Number(value.toFixed(6));
1709
+ }
1710
+ function toUtcDay(timestamp) {
1711
+ const candidate = new Date(timestamp);
1712
+ if (Number.isNaN(candidate.getTime())) {
1864
1713
  return null;
1865
1714
  }
1866
- const record = input;
1867
- for (const path of paths) {
1868
- let current = record;
1869
- for (const segment of path) {
1870
- if (!current || typeof current !== "object" || !(segment in current)) {
1871
- current = void 0;
1872
- break;
1873
- }
1874
- current = current[segment];
1875
- }
1876
- if (current !== void 0) {
1877
- return current;
1878
- }
1879
- }
1880
- return null;
1715
+ return candidate.toISOString().slice(0, 10);
1881
1716
  }
1882
- function asNumber2(value) {
1883
- if (typeof value === "number" && Number.isFinite(value)) {
1884
- return value;
1885
- }
1886
- if (typeof value === "string" && value.trim() !== "") {
1887
- const numeric = Number(value);
1888
- return Number.isFinite(numeric) ? numeric : null;
1889
- }
1890
- return null;
1717
+ function incrementUtcDay(date) {
1718
+ const candidate = /* @__PURE__ */ new Date(`${date}T00:00:00.000Z`);
1719
+ candidate.setUTCDate(candidate.getUTCDate() + 1);
1720
+ return candidate.toISOString().slice(0, 10);
1891
1721
  }
1892
- function asString(value) {
1893
- if (typeof value === "string" && value.trim() !== "") {
1894
- return value.trim();
1722
+ function buildObservedUtcDayRange(runs) {
1723
+ const days = runs.flatMap((run2) => run2.calls).map((call) => toUtcDay(call.timestamp)).filter((day) => day !== null).sort();
1724
+ if (days.length === 0) {
1725
+ return [];
1895
1726
  }
1896
- return null;
1727
+ const range = [];
1728
+ let current = days[0];
1729
+ const last = days[days.length - 1];
1730
+ while (current <= last) {
1731
+ range.push(current);
1732
+ current = incrementUtcDay(current);
1733
+ }
1734
+ return range;
1897
1735
  }
1898
- function asBoolean2(value) {
1899
- if (typeof value === "boolean") {
1900
- return value;
1736
+ function reconcileDailyTotal(rows, key, expected) {
1737
+ if (rows.length === 0) {
1738
+ return;
1901
1739
  }
1902
- if (typeof value === "string") {
1903
- return ["true", "1", "yes"].includes(value.trim().toLowerCase());
1740
+ const actual = round5(
1741
+ rows.reduce((sum, row) => sum + (typeof row[key] === "number" ? row[key] : 0), 0)
1742
+ );
1743
+ const delta = round5(expected - actual);
1744
+ if (delta === 0) {
1745
+ return;
1904
1746
  }
1905
- if (typeof value === "number") {
1906
- return value > 0;
1747
+ const last = rows[rows.length - 1];
1748
+ const current = last[key];
1749
+ if (typeof current === "number") {
1750
+ last[key] = round5(current + delta);
1907
1751
  }
1908
- return false;
1909
1752
  }
1910
- function pickMetadata(input, keys) {
1911
- const output = {};
1912
- for (const key of keys) {
1913
- const value = input[key];
1914
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1915
- output[key] = value;
1916
- }
1753
+ function buildSpendByDay(runs) {
1754
+ const days = buildObservedUtcDayRange(runs);
1755
+ if (days.length === 0) {
1756
+ return [];
1917
1757
  }
1918
- return output;
1919
- }
1920
-
1921
- // ../core/src/normalize/openclaw.ts
1922
- function parseJsonLines(path) {
1923
- const content = readFileSync2(path, "utf8");
1924
- const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1925
- const records = [];
1926
- for (const line of lines) {
1927
- try {
1928
- const parsed = JSON.parse(line);
1929
- records.push(parsed);
1758
+ const byDay = new Map(
1759
+ days.map((day) => [
1760
+ day,
1761
+ { date: day, observedSpendUsd: 0, estimatedSpendUsd: 0, callCount: 0 }
1762
+ ])
1763
+ );
1764
+ for (const run2 of runs) {
1765
+ for (const call of run2.calls) {
1766
+ const day = toUtcDay(call.timestamp);
1767
+ if (!day) {
1768
+ continue;
1769
+ }
1770
+ const bucket = byDay.get(day);
1771
+ if (!bucket) {
1772
+ continue;
1773
+ }
1774
+ bucket.callCount += 1;
1775
+ if (call.costSource === "observed") {
1776
+ bucket.observedSpendUsd += call.costUsd;
1777
+ } else if (call.costSource === "estimated") {
1778
+ bucket.estimatedSpendUsd += call.costUsd;
1779
+ }
1780
+ }
1781
+ }
1782
+ const rows = days.map((day) => {
1783
+ const bucket = byDay.get(day);
1784
+ const observedSpendUsd = round5(bucket?.observedSpendUsd ?? 0);
1785
+ const estimatedSpendUsd = round5(bucket?.estimatedSpendUsd ?? 0);
1786
+ return {
1787
+ date: day,
1788
+ observedSpendUsd,
1789
+ estimatedSpendUsd,
1790
+ spendUsd: round5(observedSpendUsd + estimatedSpendUsd),
1791
+ callCount: bucket?.callCount ?? 0
1792
+ };
1793
+ });
1794
+ reconcileDailyTotal(
1795
+ rows,
1796
+ "observedSpendUsd",
1797
+ round5(runs.reduce((sum, run2) => sum + run2.observedCostUsd, 0))
1798
+ );
1799
+ reconcileDailyTotal(
1800
+ rows,
1801
+ "estimatedSpendUsd",
1802
+ round5(runs.reduce((sum, run2) => sum + run2.estimatedCostUsd, 0))
1803
+ );
1804
+ for (const row of rows) {
1805
+ row.spendUsd = round5(row.observedSpendUsd + row.estimatedSpendUsd);
1806
+ }
1807
+ return rows;
1808
+ }
1809
+ function buildWasteByDay(wasteAttributions, days, expectedWasteUsd) {
1810
+ if (days.length === 0) {
1811
+ return [];
1812
+ }
1813
+ const byDay = new Map(days.map((day) => [day, 0]));
1814
+ for (const attribution of wasteAttributions) {
1815
+ const day = toUtcDay(attribution.timestamp);
1816
+ if (!day || !byDay.has(day)) {
1817
+ continue;
1818
+ }
1819
+ byDay.set(day, (byDay.get(day) ?? 0) + attribution.wasteUsd);
1820
+ }
1821
+ const rows = days.map((day) => ({
1822
+ date: day,
1823
+ wasteUsd: round5(byDay.get(day) ?? 0)
1824
+ }));
1825
+ reconcileDailyTotal(rows, "wasteUsd", round5(expectedWasteUsd));
1826
+ return rows;
1827
+ }
1828
+
1829
+ // ../core/src/report/summary.ts
1830
+ function buildBreakdown(items) {
1831
+ const buckets = /* @__PURE__ */ new Map();
1832
+ for (const item of items) {
1833
+ const current = buckets.get(item.key) ?? { spendUsd: 0, observedSpendUsd: 0, callCount: 0 };
1834
+ current.spendUsd += item.spendUsd;
1835
+ current.observedSpendUsd += item.observedSpendUsd;
1836
+ current.callCount += 1;
1837
+ buckets.set(item.key, current);
1838
+ }
1839
+ return Array.from(buckets.entries()).map(([key, value]) => {
1840
+ const observedShare = value.spendUsd === 0 ? 0 : value.observedSpendUsd / value.spendUsd;
1841
+ return {
1842
+ key,
1843
+ spendUsd: Number(value.spendUsd.toFixed(6)),
1844
+ callCount: value.callCount,
1845
+ observedShare: Number(observedShare.toFixed(4))
1846
+ };
1847
+ }).sort((left, right) => right.spendUsd - left.spendUsd);
1848
+ }
1849
+ function buildAuditSummary(input) {
1850
+ const callCount = input.runs.reduce((sum, run2) => sum + run2.calls.length, 0);
1851
+ const totalSpendUsd = input.runs.reduce((sum, run2) => sum + run2.totalCostUsd, 0);
1852
+ const observedSpendUsd = input.runs.reduce((sum, run2) => sum + run2.observedCostUsd, 0);
1853
+ const estimatedSpendUsd = input.runs.reduce((sum, run2) => sum + run2.estimatedCostUsd, 0);
1854
+ const wasteSpendUsd = input.findings.filter((finding) => finding.classification === "waste").reduce((sum, finding) => sum + finding.costImpactUsd, 0);
1855
+ const opportunitySpendUsd = input.findings.filter((finding) => finding.classification === "opportunity").reduce((sum, finding) => sum + finding.costImpactUsd, 0);
1856
+ const generatedAt = isoNow();
1857
+ const spendByDay = buildSpendByDay(input.runs);
1858
+ const observedDays = buildObservedUtcDayRange(input.runs);
1859
+ return {
1860
+ auditId: sha1(
1861
+ `${generatedAt}:${input.runs.length}:${input.sources.map((source) => source.path).join("|")}`
1862
+ ),
1863
+ generatedAt,
1864
+ runtime: input.runtime,
1865
+ comparisonKey: input.comparisonKeyOverride ?? buildComparisonKey({
1866
+ runtime: input.runtime,
1867
+ sources: input.sources,
1868
+ since: input.since
1869
+ }),
1870
+ comparison: null,
1871
+ since: input.since,
1872
+ runCount: input.runs.length,
1873
+ callCount,
1874
+ totalSpendUsd: Number(totalSpendUsd.toFixed(6)),
1875
+ observedSpendUsd: Number(observedSpendUsd.toFixed(6)),
1876
+ estimatedSpendUsd: Number(estimatedSpendUsd.toFixed(6)),
1877
+ wasteSpendUsd: Number(wasteSpendUsd.toFixed(6)),
1878
+ opportunitySpendUsd: Number(opportunitySpendUsd.toFixed(6)),
1879
+ structuralWasteRate: Number(
1880
+ (totalSpendUsd === 0 ? 0 : wasteSpendUsd / totalSpendUsd).toFixed(4)
1881
+ ),
1882
+ wasteByKind: buildTaxonomyBuckets(input.findings, "waste"),
1883
+ opportunityByKind: buildTaxonomyBuckets(input.findings, "opportunity"),
1884
+ spendByWorkflow: buildBreakdown(
1885
+ input.runs.map((run2) => ({
1886
+ key: run2.workflow,
1887
+ spendUsd: run2.totalCostUsd,
1888
+ observedSpendUsd: run2.observedCostUsd
1889
+ }))
1890
+ ),
1891
+ spendByModel: buildBreakdown(
1892
+ input.runs.flatMap(
1893
+ (run2) => run2.calls.map((call) => ({
1894
+ key: `${call.provider}/${call.model}`,
1895
+ spendUsd: call.costUsd,
1896
+ observedSpendUsd: call.costSource === "observed" ? call.costUsd : 0
1897
+ }))
1898
+ )
1899
+ ),
1900
+ spendByDay,
1901
+ wasteByDay: buildWasteByDay(input.wasteAttributions, observedDays, wasteSpendUsd),
1902
+ findings: input.findings,
1903
+ notes: [
1904
+ "Cost per outcome is intentionally unavailable in v0. Xerg is measuring waste intelligence only.",
1905
+ "Opportunity findings are directional recommendations, not proven waste."
1906
+ ],
1907
+ sourceFiles: input.sources,
1908
+ dbPath: input.dbPath
1909
+ };
1910
+ }
1911
+
1912
+ // ../core/src/runtime.ts
1913
+ import { basename as basename4 } from "path";
1914
+
1915
+ // ../core/src/detect/hermes.ts
1916
+ import { homedir as homedir2 } from "os";
1917
+ import { basename as basename2, join as join3 } from "path";
1918
+
1919
+ // ../core/src/normalize/hermes.ts
1920
+ import { readFileSync as readFileSync2 } from "fs";
1921
+ import { basename } from "path";
1922
+
1923
+ // ../core/src/utils/records.ts
1924
+ function getNestedValue(input, paths) {
1925
+ if (!input || typeof input !== "object") {
1926
+ return null;
1927
+ }
1928
+ const record = input;
1929
+ for (const path of paths) {
1930
+ let current = record;
1931
+ for (const segment of path) {
1932
+ if (!current || typeof current !== "object" || !(segment in current)) {
1933
+ current = void 0;
1934
+ break;
1935
+ }
1936
+ current = current[segment];
1937
+ }
1938
+ if (current !== void 0) {
1939
+ return current;
1940
+ }
1941
+ }
1942
+ return null;
1943
+ }
1944
+ function asNumber2(value) {
1945
+ if (typeof value === "number" && Number.isFinite(value)) {
1946
+ return value;
1947
+ }
1948
+ if (typeof value === "string" && value.trim() !== "") {
1949
+ const numeric = Number(value);
1950
+ return Number.isFinite(numeric) ? numeric : null;
1951
+ }
1952
+ return null;
1953
+ }
1954
+ function asString(value) {
1955
+ if (typeof value === "string" && value.trim() !== "") {
1956
+ return value.trim();
1957
+ }
1958
+ return null;
1959
+ }
1960
+ function asBoolean2(value) {
1961
+ if (typeof value === "boolean") {
1962
+ return value;
1963
+ }
1964
+ if (typeof value === "string") {
1965
+ return ["true", "1", "yes"].includes(value.trim().toLowerCase());
1966
+ }
1967
+ if (typeof value === "number") {
1968
+ return value > 0;
1969
+ }
1970
+ return false;
1971
+ }
1972
+ function pickMetadata(input, keys) {
1973
+ const output = {};
1974
+ for (const key of keys) {
1975
+ const value = input[key];
1976
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1977
+ output[key] = value;
1978
+ }
1979
+ }
1980
+ return output;
1981
+ }
1982
+
1983
+ // ../core/src/normalize/hermes.ts
1984
+ var PRICING_PROVIDER_PREFIXES = /* @__PURE__ */ new Set(["anthropic", "openai", "google", "meta"]);
1985
+ function parseJsonLine(line) {
1986
+ const trimmed = line.trim();
1987
+ if (trimmed.length === 0) {
1988
+ return null;
1989
+ }
1990
+ try {
1991
+ return JSON.parse(trimmed);
1992
+ } catch {
1993
+ }
1994
+ const jsonStart = trimmed.indexOf("{");
1995
+ const jsonEnd = trimmed.lastIndexOf("}");
1996
+ if (jsonStart < 0 || jsonEnd <= jsonStart) {
1997
+ return null;
1998
+ }
1999
+ try {
2000
+ return JSON.parse(trimmed.slice(jsonStart, jsonEnd + 1));
2001
+ } catch {
2002
+ return null;
2003
+ }
2004
+ }
2005
+ function parseJsonLines(path) {
2006
+ const content = readFileSync2(path, "utf8");
2007
+ const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
2008
+ const records = [];
2009
+ for (const line of lines) {
2010
+ const parsed = parseJsonLine(line);
2011
+ if (parsed) {
2012
+ records.push(parsed);
2013
+ }
2014
+ }
2015
+ return records;
2016
+ }
2017
+ function flattenSourceRecords(source) {
2018
+ const records = parseJsonLines(source.path);
2019
+ if (source.kind !== "sessions") {
2020
+ return records;
2021
+ }
2022
+ return records.flatMap((record) => expandSessionRecord(record));
2023
+ }
2024
+ function expandSessionRecord(record) {
2025
+ const messages = getNestedValue(record, [["messages"]]);
2026
+ if (!Array.isArray(messages)) {
2027
+ return [record];
2028
+ }
2029
+ return messages.filter((message) => Boolean(message) && typeof message === "object").map((message, index) => ({
2030
+ ...record,
2031
+ message,
2032
+ timestamp: asString(
2033
+ getNestedValue(message, [["timestamp"], ["created_at"], ["createdAt"], ["time"]])
2034
+ ) ?? asString(
2035
+ getNestedValue(record, [["timestamp"], ["created_at"], ["createdAt"], ["updated_at"]])
2036
+ ) ?? `${sourceTimestampSeed(record)}:${index}`
2037
+ }));
2038
+ }
2039
+ function sourceTimestampSeed(record) {
2040
+ return asString(getNestedValue(record, [["updated_at"], ["created_at"], ["timestamp"], ["id"]])) ?? "session";
2041
+ }
2042
+ function normalizeProviderAndModel(record) {
2043
+ const rawProvider = asString(
2044
+ getNestedValue(record, [
2045
+ ["provider"],
2046
+ ["provider_name"],
2047
+ ["runtime", "provider"],
2048
+ ["usage", "provider"],
2049
+ ["token_usage", "provider"],
2050
+ ["message", "provider"],
2051
+ ["message", "usage", "provider"]
2052
+ ])
2053
+ ) ?? "unknown";
2054
+ const rawModel = asString(
2055
+ getNestedValue(record, [
2056
+ ["model"],
2057
+ ["model_name"],
2058
+ ["runtime", "model"],
2059
+ ["usage", "model"],
2060
+ ["token_usage", "model"],
2061
+ ["message", "model"],
2062
+ ["message", "usage", "model"]
2063
+ ])
2064
+ ) ?? "unknown-model";
2065
+ if (rawModel.includes("/")) {
2066
+ const [providerPrefix, ...rest] = rawModel.split("/");
2067
+ if (PRICING_PROVIDER_PREFIXES.has(providerPrefix.toLowerCase()) && rest.length > 0) {
2068
+ return {
2069
+ provider: providerPrefix.toLowerCase(),
2070
+ model: rest.join("/")
2071
+ };
2072
+ }
2073
+ }
2074
+ return {
2075
+ provider: rawProvider.toLowerCase(),
2076
+ model: rawModel
2077
+ };
2078
+ }
2079
+ function inferWorkflow(record, sourcePath) {
2080
+ return asString(
2081
+ getNestedValue(record, [
2082
+ ["workflow"],
2083
+ ["job_name"],
2084
+ ["task_class"],
2085
+ ["taskClass"],
2086
+ ["agent_name"],
2087
+ ["agent", "name"],
2088
+ ["source"],
2089
+ ["session", "source"],
2090
+ ["session_key"],
2091
+ ["session_id"],
2092
+ ["sessionId"],
2093
+ ["title"]
2094
+ ])
2095
+ ) ?? basename(sourcePath).replace(/\.(jsonl|json|log)(\.\d+)?$/i, "");
2096
+ }
2097
+ function inferEnvironment(record) {
2098
+ return asString(getNestedValue(record, [["environment"], ["env"], ["metadata", "environment"]])) ?? "local";
2099
+ }
2100
+ function inferRunKey(record, workflow, index, sourcePath) {
2101
+ return asString(
2102
+ getNestedValue(record, [
2103
+ ["run_id"],
2104
+ ["runId"],
2105
+ ["session_id"],
2106
+ ["sessionId"],
2107
+ ["session_key"],
2108
+ ["thread_id"],
2109
+ ["threadId"],
2110
+ ["trace_id"],
2111
+ ["traceId"],
2112
+ ["conversation_id"],
2113
+ ["conversationId"],
2114
+ ["id"]
2115
+ ])
2116
+ ) ?? `${sourcePath}:${workflow}:${index}`;
2117
+ }
2118
+ function inferTaskClass(record, workflow) {
2119
+ return asString(
2120
+ getNestedValue(record, [
2121
+ ["task_class"],
2122
+ ["taskClass"],
2123
+ ["event"],
2124
+ ["source"],
2125
+ ["message", "source"]
2126
+ ])
2127
+ ) ?? workflow.toLowerCase();
2128
+ }
2129
+ function inferToolCalls(record) {
2130
+ const direct = asNumber2(
2131
+ getNestedValue(record, [
2132
+ ["tool_calls"],
2133
+ ["toolCalls"],
2134
+ ["usage", "tool_calls"],
2135
+ ["message", "usage", "tool_calls"]
2136
+ ])
2137
+ ) ?? null;
2138
+ if (direct !== null) {
2139
+ return direct;
2140
+ }
2141
+ const toolCalls = getNestedValue(record, [
2142
+ ["tool_calls"],
2143
+ ["toolCalls"],
2144
+ ["message", "tool_calls"]
2145
+ ]);
2146
+ return Array.isArray(toolCalls) ? toolCalls.length : 0;
2147
+ }
2148
+ function extractUsage(record) {
2149
+ const inputTokens = asNumber2(
2150
+ getNestedValue(record, [
2151
+ ["input_tokens"],
2152
+ ["inputTokens"],
2153
+ ["usage", "input_tokens"],
2154
+ ["usage", "inputTokens"],
2155
+ ["usage", "prompt_tokens"],
2156
+ ["token_usage", "input_tokens"],
2157
+ ["token_usage", "prompt_tokens"],
2158
+ ["token_usage", "input"],
2159
+ ["message", "usage", "input_tokens"],
2160
+ ["message", "usage", "prompt_tokens"],
2161
+ ["message", "token_usage", "input_tokens"]
2162
+ ])
2163
+ ) ?? 0;
2164
+ const outputTokens = asNumber2(
2165
+ getNestedValue(record, [
2166
+ ["output_tokens"],
2167
+ ["outputTokens"],
2168
+ ["usage", "output_tokens"],
2169
+ ["usage", "outputTokens"],
2170
+ ["usage", "completion_tokens"],
2171
+ ["token_usage", "output_tokens"],
2172
+ ["token_usage", "completion_tokens"],
2173
+ ["token_usage", "output"],
2174
+ ["message", "usage", "output_tokens"],
2175
+ ["message", "usage", "completion_tokens"],
2176
+ ["message", "token_usage", "output_tokens"]
2177
+ ])
2178
+ ) ?? 0;
2179
+ const observedCost = asNumber2(
2180
+ getNestedValue(record, [
2181
+ ["cost_usd"],
2182
+ ["costUsd"],
2183
+ ["estimated_cost_usd"],
2184
+ ["estimatedCostUsd"],
2185
+ ["total_cost_usd"],
2186
+ ["usage", "cost_usd"],
2187
+ ["usage", "costUsd"],
2188
+ ["usage", "estimated_cost_usd"],
2189
+ ["usage", "total_cost_usd"],
2190
+ ["token_usage", "cost_usd"],
2191
+ ["token_usage", "total_cost_usd"],
2192
+ ["cost", "usd"],
2193
+ ["cost", "total_usd"],
2194
+ ["pricing", "total_usd"],
2195
+ ["message", "usage", "cost_usd"],
2196
+ ["message", "cost_usd"]
2197
+ ])
2198
+ ) ?? null;
2199
+ return {
2200
+ inputTokens,
2201
+ outputTokens,
2202
+ observedCost
2203
+ };
2204
+ }
2205
+ function shouldTreatAsCall(record) {
2206
+ const { inputTokens, outputTokens, observedCost } = extractUsage(record);
2207
+ return inputTokens > 0 || outputTokens > 0 || observedCost !== null;
2208
+ }
2209
+ function buildCall2(source, record, runId, index) {
2210
+ const { provider, model } = normalizeProviderAndModel(record);
2211
+ const workflow = inferWorkflow(record, source.path);
2212
+ const { inputTokens, outputTokens, observedCost } = extractUsage(record);
2213
+ const estimatedCost = estimateCostUsd(provider, model, inputTokens, outputTokens);
2214
+ const timestamp = toIsoOrNow(
2215
+ getNestedValue(record, [
2216
+ ["timestamp"],
2217
+ ["createdAt"],
2218
+ ["created_at"],
2219
+ ["time"],
2220
+ ["updated_at"]
2221
+ ])
2222
+ );
2223
+ const attempt = asNumber2(
2224
+ getNestedValue(record, [["attempt"], ["usage", "attempt"], ["metadata", "attempt"]])
2225
+ ) ?? null;
2226
+ const iteration = asNumber2(
2227
+ getNestedValue(record, [["iteration"], ["loop_iteration"], ["metadata", "iteration"]])
2228
+ ) ?? null;
2229
+ const retries = asNumber2(getNestedValue(record, [["retries"], ["retry_count"], ["metadata", "retries"]])) ?? 0;
2230
+ const costUsd = observedCost ?? estimatedCost ?? 0;
2231
+ return {
2232
+ id: sha1(`${runId}:${source.path}:${index}:${model}:${timestamp}:${costUsd}`),
2233
+ runId,
2234
+ timestamp,
2235
+ provider,
2236
+ model,
2237
+ inputTokens,
2238
+ outputTokens,
2239
+ costUsd,
2240
+ costSource: observedCost !== null ? "observed" : estimatedCost !== null ? "estimated" : "unpriced",
2241
+ latencyMs: asNumber2(getNestedValue(record, [["latency_ms"], ["latencyMs"], ["usage", "latency_ms"]])) ?? null,
2242
+ toolCalls: inferToolCalls(record),
2243
+ retries,
2244
+ attempt,
2245
+ iteration,
2246
+ status: asString(getNestedValue(record, [["status"], ["level"], ["result"], ["error", "type"]])) ?? null,
2247
+ taskClass: inferTaskClass(record, workflow),
2248
+ cacheHit: asBoolean2(
2249
+ getNestedValue(record, [["cache_hit"], ["cacheHit"], ["usage", "cache_hit"]])
2250
+ ),
2251
+ cacheCostUsd: asNumber2(
2252
+ getNestedValue(record, [["cache_cost_usd"], ["cacheCostUsd"], ["usage", "cache_cost_usd"]])
2253
+ ) ?? null,
2254
+ metadata: pickMetadata(record, [
2255
+ "event",
2256
+ "type",
2257
+ "source",
2258
+ "session_id",
2259
+ "sessionId",
2260
+ "thread_id",
2261
+ "trace_id"
2262
+ ])
2263
+ };
2264
+ }
2265
+ function logFileHasBillableRecords(path) {
2266
+ return flattenSourceRecords({
2267
+ kind: "gateway",
2268
+ runtime: "hermes",
2269
+ path,
2270
+ sizeBytes: 0,
2271
+ mtimeMs: 0
2272
+ }).some((record) => shouldTreatAsCall(record));
2273
+ }
2274
+ function normalizeHermesSources(sources, since) {
2275
+ const cutoff = parseSince(since);
2276
+ const runsById = /* @__PURE__ */ new Map();
2277
+ for (const source of sources) {
2278
+ const records = flattenSourceRecords(source);
2279
+ records.forEach((record, index) => {
2280
+ if (!shouldTreatAsCall(record)) {
2281
+ return;
2282
+ }
2283
+ const workflow = inferWorkflow(record, source.path);
2284
+ const timestamp = toIsoOrNow(
2285
+ getNestedValue(record, [
2286
+ ["timestamp"],
2287
+ ["createdAt"],
2288
+ ["created_at"],
2289
+ ["time"],
2290
+ ["updated_at"]
2291
+ ])
2292
+ );
2293
+ if (cutoff && new Date(timestamp).getTime() < cutoff) {
2294
+ return;
2295
+ }
2296
+ const runKey = inferRunKey(record, workflow, index, source.path);
2297
+ const runId = sha1(`${source.path}:${runKey}`);
2298
+ const call = buildCall2(source, record, runId, index);
2299
+ const existing = runsById.get(runId);
2300
+ if (!existing) {
2301
+ runsById.set(runId, {
2302
+ id: runId,
2303
+ sourceKind: source.kind,
2304
+ sourcePath: source.path,
2305
+ timestamp,
2306
+ workflow,
2307
+ environment: inferEnvironment(record),
2308
+ tags: {
2309
+ sourceKind: source.kind
2310
+ },
2311
+ calls: [call],
2312
+ totalCostUsd: call.costUsd,
2313
+ totalTokens: call.inputTokens + call.outputTokens,
2314
+ observedCostUsd: call.costSource === "observed" ? call.costUsd : 0,
2315
+ estimatedCostUsd: call.costSource === "estimated" ? call.costUsd : 0
2316
+ });
2317
+ return;
2318
+ }
2319
+ existing.calls.push(call);
2320
+ existing.totalCostUsd = Number((existing.totalCostUsd + call.costUsd).toFixed(8));
2321
+ existing.totalTokens += call.inputTokens + call.outputTokens;
2322
+ existing.observedCostUsd += call.costSource === "observed" ? call.costUsd : 0;
2323
+ existing.estimatedCostUsd += call.costSource === "estimated" ? call.costUsd : 0;
2324
+ if (timestamp < existing.timestamp) {
2325
+ existing.timestamp = timestamp;
2326
+ }
2327
+ });
2328
+ }
2329
+ return Array.from(runsById.values()).sort(
2330
+ (left, right) => left.timestamp < right.timestamp ? -1 : 1
2331
+ );
2332
+ }
2333
+
2334
+ // ../core/src/utils/paths.ts
2335
+ import { mkdirSync as mkdirSync2 } from "fs";
2336
+ import { homedir } from "os";
2337
+ import { join } from "path";
2338
+ import { platform } from "process";
2339
+ function getUserHome() {
2340
+ return process.env.HOME ?? homedir();
2341
+ }
2342
+ function getAppPaths() {
2343
+ const home = getUserHome();
2344
+ return platform === "darwin" ? {
2345
+ data: join(home, "Library", "Application Support", "xerg"),
2346
+ config: join(home, "Library", "Preferences", "xerg"),
2347
+ cache: join(home, "Library", "Caches", "xerg")
2348
+ } : platform === "win32" ? {
2349
+ data: join(process.env.LOCALAPPDATA ?? join(home, "AppData", "Local"), "xerg", "Data"),
2350
+ config: join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "xerg", "Config"),
2351
+ cache: join(process.env.LOCALAPPDATA ?? join(home, "AppData", "Local"), "xerg", "Cache")
2352
+ } : {
2353
+ data: join(process.env.XDG_DATA_HOME ?? join(home, ".local", "share"), "xerg"),
2354
+ config: join(process.env.XDG_CONFIG_HOME ?? join(home, ".config"), "xerg"),
2355
+ cache: join(process.env.XDG_CACHE_HOME ?? join(home, ".cache"), "xerg")
2356
+ };
2357
+ }
2358
+ function getDefaultDbPath() {
2359
+ return join(getAppPaths().data, "xerg.db");
2360
+ }
2361
+ function getDefaultOpenClawSessionsPattern() {
2362
+ return join(getUserHome(), ".openclaw", "agents", "*", "sessions", "*.jsonl");
2363
+ }
2364
+ function getDefaultOpenClawGatewayPattern() {
2365
+ return "/tmp/openclaw/openclaw-*.log";
2366
+ }
2367
+ function getDefaultHermesSessionsPattern() {
2368
+ return join(getUserHome(), ".hermes", "sessions", "**", "*.{json,jsonl}");
2369
+ }
2370
+ function getDefaultHermesGatewayPattern() {
2371
+ return join(getUserHome(), ".hermes", "logs", "agent.log* (fallback: gateway.log*)");
2372
+ }
2373
+
2374
+ // ../core/src/detect/shared.ts
2375
+ import { readdirSync, statSync as statSync2 } from "fs";
2376
+ import { isAbsolute, join as join2, resolve as resolve2, sep } from "path";
2377
+ function toDetected(path, kind, runtime) {
2378
+ try {
2379
+ const stats = statSync2(path);
2380
+ if (!stats.isFile()) {
2381
+ return null;
2382
+ }
2383
+ return {
2384
+ kind,
2385
+ runtime,
2386
+ path,
2387
+ sizeBytes: stats.size,
2388
+ mtimeMs: stats.mtimeMs
2389
+ };
2390
+ } catch {
2391
+ return null;
2392
+ }
2393
+ }
2394
+ async function collectGlobMatches(pattern, options) {
2395
+ const baseDir = options?.cwd ? resolve2(options.cwd) : isAbsolute(pattern) ? sep : process.cwd();
2396
+ const relativePattern = options?.cwd ? pattern : isAbsolute(pattern) ? pattern.slice(baseDir.length) : pattern;
2397
+ const segments = relativePattern.split("/").filter(Boolean);
2398
+ const matches = collectMatchesFromSegments(baseDir, segments);
2399
+ return matches.map(
2400
+ (match) => options?.resolveWith ? resolve2(options.resolveWith, match) : match
2401
+ );
2402
+ }
2403
+ function collectMatchesFromSegments(currentPath, segments) {
2404
+ if (segments.length === 0) {
2405
+ return [currentPath];
2406
+ }
2407
+ const [segment, ...rest] = segments;
2408
+ if (segment === "**") {
2409
+ const matches2 = collectMatchesFromSegments(currentPath, rest);
2410
+ for (const entry of readDirSafe(currentPath)) {
2411
+ if (entry.isDirectory()) {
2412
+ matches2.push(...collectMatchesFromSegments(join2(currentPath, entry.name), segments));
2413
+ }
2414
+ }
2415
+ return matches2;
2416
+ }
2417
+ const matches = [];
2418
+ const matcher = segmentToRegExp(segment);
2419
+ for (const entry of readDirSafe(currentPath)) {
2420
+ if (!matcher.test(entry.name)) {
2421
+ continue;
2422
+ }
2423
+ const nextPath = join2(currentPath, entry.name);
2424
+ if (rest.length === 0) {
2425
+ matches.push(nextPath);
2426
+ continue;
2427
+ }
2428
+ if (entry.isDirectory()) {
2429
+ matches.push(...collectMatchesFromSegments(nextPath, rest));
2430
+ }
2431
+ }
2432
+ return matches;
2433
+ }
2434
+ function readDirSafe(path) {
2435
+ try {
2436
+ return readdirSync(path, { withFileTypes: true });
2437
+ } catch {
2438
+ return [];
2439
+ }
2440
+ }
2441
+ function segmentToRegExp(segment) {
2442
+ const escaped = segment.replaceAll(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
2443
+ return new RegExp(`^${escaped}$`);
2444
+ }
2445
+
2446
+ // ../core/src/detect/hermes.ts
2447
+ function getDefaultAgentLogPattern() {
2448
+ return join3(process.env.HOME ?? homedir2(), ".hermes", "logs", "agent.log*");
2449
+ }
2450
+ function getDefaultGatewayLogPattern() {
2451
+ return join3(process.env.HOME ?? homedir2(), ".hermes", "logs", "gateway.log*");
2452
+ }
2453
+ function getDefaultSessionsDirPattern() {
2454
+ return join3(process.env.HOME ?? homedir2(), ".hermes", "sessions", "**", "*");
2455
+ }
2456
+ function isHermesSessionTranscript(path) {
2457
+ const lowerName = basename2(path).toLowerCase();
2458
+ if (lowerName === "sessions.json") {
2459
+ return false;
2460
+ }
2461
+ return lowerName.endsWith(".jsonl") || lowerName.endsWith(".json");
2462
+ }
2463
+ async function collectHermesSessionMatches(baseDir) {
2464
+ const matches = await collectGlobMatches("**/*", {
2465
+ cwd: baseDir,
2466
+ resolveWith: baseDir
2467
+ });
2468
+ return matches.filter((match) => isHermesSessionTranscript(match));
2469
+ }
2470
+ async function pickPreferredLogFamily() {
2471
+ const [agentMatches, gatewayMatches] = await Promise.all([
2472
+ collectGlobMatches(getDefaultAgentLogPattern()),
2473
+ collectGlobMatches(getDefaultGatewayLogPattern())
2474
+ ]);
2475
+ if (agentMatches.some((path) => logFileHasBillableRecords(path))) {
2476
+ return agentMatches;
2477
+ }
2478
+ if (gatewayMatches.some((path) => logFileHasBillableRecords(path))) {
2479
+ return gatewayMatches;
2480
+ }
2481
+ return [...agentMatches, ...gatewayMatches];
2482
+ }
2483
+ async function detectHermesSources(options) {
2484
+ const explicitSources = [];
2485
+ if (options.logFile) {
2486
+ const detected2 = toDetected(options.logFile, "gateway", "hermes");
2487
+ if (detected2) {
2488
+ explicitSources.push(detected2);
2489
+ }
2490
+ }
2491
+ if (options.sessionsDir) {
2492
+ const matches = await collectHermesSessionMatches(options.sessionsDir);
2493
+ for (const match of matches) {
2494
+ const detected2 = toDetected(match, "sessions", "hermes");
2495
+ if (detected2) {
2496
+ explicitSources.push(detected2);
2497
+ }
2498
+ }
2499
+ }
2500
+ if (explicitSources.length > 0) {
2501
+ return explicitSources.sort((left, right) => right.mtimeMs - left.mtimeMs);
2502
+ }
2503
+ const [gatewayMatches, sessionMatches] = await Promise.all([
2504
+ pickPreferredLogFamily(),
2505
+ collectGlobMatches(getDefaultSessionsDirPattern())
2506
+ ]);
2507
+ const detected = [
2508
+ ...gatewayMatches.map((path) => toDetected(path, "gateway", "hermes")).filter(Boolean),
2509
+ ...sessionMatches.filter((path) => isHermesSessionTranscript(path)).map((path) => toDetected(path, "sessions", "hermes")).filter(Boolean)
2510
+ ];
2511
+ return detected.sort((left, right) => right.mtimeMs - left.mtimeMs);
2512
+ }
2513
+
2514
+ // ../core/src/detect/openclaw.ts
2515
+ async function detectOpenClawSources(options) {
2516
+ const explicitSources = [];
2517
+ if (options.logFile) {
2518
+ const detected2 = toDetected(options.logFile, "gateway", "openclaw");
2519
+ if (detected2) {
2520
+ explicitSources.push(detected2);
2521
+ }
2522
+ }
2523
+ if (options.sessionsDir) {
2524
+ const matches = await collectGlobMatches("**/*.jsonl", {
2525
+ cwd: options.sessionsDir,
2526
+ resolveWith: options.sessionsDir
2527
+ });
2528
+ for (const match of matches) {
2529
+ const detected2 = toDetected(match, "sessions", "openclaw");
2530
+ if (detected2) {
2531
+ explicitSources.push(detected2);
2532
+ }
2533
+ }
2534
+ }
2535
+ if (explicitSources.length > 0) {
2536
+ return explicitSources.sort((left, right) => right.mtimeMs - left.mtimeMs);
2537
+ }
2538
+ const [gatewayMatches, sessionMatches] = await Promise.all([
2539
+ collectGlobMatches(getDefaultOpenClawGatewayPattern()),
2540
+ collectGlobMatches(getDefaultOpenClawSessionsPattern())
2541
+ ]);
2542
+ const detected = [
2543
+ ...gatewayMatches.map((path) => toDetected(path, "gateway", "openclaw")).filter(Boolean),
2544
+ ...sessionMatches.map((path) => toDetected(path, "sessions", "openclaw")).filter(Boolean)
2545
+ ];
2546
+ return detected.sort((left, right) => right.mtimeMs - left.mtimeMs);
2547
+ }
2548
+
2549
+ // ../core/src/normalize/openclaw.ts
2550
+ import { readFileSync as readFileSync3 } from "fs";
2551
+ import { basename as basename3 } from "path";
2552
+ function parseJsonLines2(path) {
2553
+ const content = readFileSync3(path, "utf8");
2554
+ const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
2555
+ const records = [];
2556
+ for (const line of lines) {
2557
+ try {
2558
+ const parsed = JSON.parse(line);
2559
+ records.push(parsed);
1930
2560
  } catch {
1931
2561
  }
1932
2562
  }
@@ -1940,7 +2570,7 @@ function inferProvider2(record) {
1940
2570
  function inferModel(record) {
1941
2571
  return asString(getNestedValue(record, [["model"], ["message", "model"], ["usage", "model"]])) ?? "unknown-model";
1942
2572
  }
1943
- function inferWorkflow(record, sourcePath) {
2573
+ function inferWorkflow2(record, sourcePath) {
1944
2574
  return asString(
1945
2575
  getNestedValue(record, [
1946
2576
  ["workflow"],
@@ -1950,12 +2580,12 @@ function inferWorkflow(record, sourcePath) {
1950
2580
  ["agentId"],
1951
2581
  ["sessionId"]
1952
2582
  ])
1953
- ) ?? basename(sourcePath, ".jsonl");
2583
+ ) ?? basename3(sourcePath, ".jsonl");
1954
2584
  }
1955
- function inferEnvironment(record) {
2585
+ function inferEnvironment2(record) {
1956
2586
  return asString(getNestedValue(record, [["environment"], ["env"], ["metadata", "environment"]])) ?? "local";
1957
2587
  }
1958
- function inferRunKey(record, workflow, index, sourcePath) {
2588
+ function inferRunKey2(record, workflow, index, sourcePath) {
1959
2589
  return asString(
1960
2590
  getNestedValue(record, [
1961
2591
  ["run_id"],
@@ -1967,10 +2597,10 @@ function inferRunKey(record, workflow, index, sourcePath) {
1967
2597
  ])
1968
2598
  ) ?? `${sourcePath}:${workflow}:${index}`;
1969
2599
  }
1970
- function inferTaskClass(record, workflow) {
2600
+ function inferTaskClass2(record, workflow) {
1971
2601
  return asString(getNestedValue(record, [["task_class"], ["taskClass"], ["metadata", "taskClass"]])) ?? workflow.toLowerCase();
1972
2602
  }
1973
- function extractUsage(record) {
2603
+ function extractUsage2(record) {
1974
2604
  const inputTokens = asNumber2(
1975
2605
  getNestedValue(record, [
1976
2606
  ["input_tokens"],
@@ -2013,11 +2643,11 @@ function extractUsage(record) {
2013
2643
  observedCost
2014
2644
  };
2015
2645
  }
2016
- function buildCall2(source, record, runId, index) {
2646
+ function buildCall3(source, record, runId, index) {
2017
2647
  const provider = inferProvider2(record);
2018
2648
  const model = inferModel(record);
2019
- const workflow = inferWorkflow(record, source.path);
2020
- const { inputTokens, outputTokens, observedCost } = extractUsage(record);
2649
+ const workflow = inferWorkflow2(record, source.path);
2650
+ const { inputTokens, outputTokens, observedCost } = extractUsage2(record);
2021
2651
  const estimatedCost = estimateCostUsd(provider, model, inputTokens, outputTokens);
2022
2652
  const timestamp = toIsoOrNow(
2023
2653
  getNestedValue(record, [["timestamp"], ["createdAt"], ["created_at"]])
@@ -2046,7 +2676,7 @@ function buildCall2(source, record, runId, index) {
2046
2676
  attempt,
2047
2677
  iteration,
2048
2678
  status: asString(getNestedValue(record, [["status"], ["level"], ["result"], ["error", "type"]])) ?? null,
2049
- taskClass: inferTaskClass(record, workflow),
2679
+ taskClass: inferTaskClass2(record, workflow),
2050
2680
  cacheHit: asBoolean2(
2051
2681
  getNestedValue(record, [["cache_hit"], ["cacheHit"], ["usage", "cache_hit"]])
2052
2682
  ),
@@ -2056,29 +2686,29 @@ function buildCall2(source, record, runId, index) {
2056
2686
  metadata: pickMetadata(record, ["event", "type", "sessionId", "agentId"])
2057
2687
  };
2058
2688
  }
2059
- function shouldTreatAsCall(record) {
2060
- const hasUsage = extractUsage(record).inputTokens > 0 || extractUsage(record).outputTokens > 0 || extractUsage(record).observedCost !== null;
2689
+ function shouldTreatAsCall2(record) {
2690
+ const hasUsage = extractUsage2(record).inputTokens > 0 || extractUsage2(record).outputTokens > 0 || extractUsage2(record).observedCost !== null;
2061
2691
  return hasUsage;
2062
2692
  }
2063
2693
  function normalizeOpenClawSources(sources, since) {
2064
2694
  const cutoff = parseSince(since);
2065
2695
  const runsById = /* @__PURE__ */ new Map();
2066
2696
  for (const source of sources) {
2067
- const records = parseJsonLines(source.path);
2697
+ const records = parseJsonLines2(source.path);
2068
2698
  records.forEach((record, index) => {
2069
- if (!shouldTreatAsCall(record)) {
2699
+ if (!shouldTreatAsCall2(record)) {
2070
2700
  return;
2071
2701
  }
2072
- const workflow = inferWorkflow(record, source.path);
2702
+ const workflow = inferWorkflow2(record, source.path);
2073
2703
  const timestamp = toIsoOrNow(
2074
2704
  getNestedValue(record, [["timestamp"], ["createdAt"], ["created_at"]])
2075
2705
  );
2076
2706
  if (cutoff && new Date(timestamp).getTime() < cutoff) {
2077
2707
  return;
2078
2708
  }
2079
- const runKey = inferRunKey(record, workflow, index, source.path);
2709
+ const runKey = inferRunKey2(record, workflow, index, source.path);
2080
2710
  const runId = sha1(`${source.path}:${runKey}`);
2081
- const call = buildCall2(source, record, runId, index);
2711
+ const call = buildCall3(source, record, runId, index);
2082
2712
  const existing = runsById.get(runId);
2083
2713
  if (!existing) {
2084
2714
  runsById.set(runId, {
@@ -2087,7 +2717,7 @@ function normalizeOpenClawSources(sources, since) {
2087
2717
  sourcePath: source.path,
2088
2718
  timestamp,
2089
2719
  workflow,
2090
- environment: inferEnvironment(record),
2720
+ environment: inferEnvironment2(record),
2091
2721
  tags: {
2092
2722
  sourceKind: source.kind
2093
2723
  },
@@ -2111,214 +2741,281 @@ function normalizeOpenClawSources(sources, since) {
2111
2741
  });
2112
2742
  }
2113
2743
 
2114
- // ../core/src/report/timeseries.ts
2115
- function round5(value) {
2116
- return Number(value.toFixed(6));
2117
- }
2118
- function toUtcDay(timestamp) {
2119
- const candidate = new Date(timestamp);
2120
- if (Number.isNaN(candidate.getTime())) {
2121
- return null;
2744
+ // ../core/src/runtime.ts
2745
+ var RUNTIME_ADAPTERS = {
2746
+ openclaw: {
2747
+ runtime: "openclaw",
2748
+ productName: "OpenClaw",
2749
+ detectSources: detectOpenClawSources,
2750
+ normalizeSources: normalizeOpenClawSources,
2751
+ noSourceNotes: [
2752
+ "No OpenClaw gateway logs or session files were detected.",
2753
+ "Doctor checks local defaults by default. Use --log-file or --sessions-dir if your OpenClaw data lives outside the defaults.",
2754
+ "Use --remote or --railway to inspect remote OpenClaw targets."
2755
+ ],
2756
+ gatewayNote: "Gateway logs detected. These are preferred when cost metadata is present.",
2757
+ sessionNote: "Session transcript fallback detected. Xerg will extract usage metadata only.",
2758
+ noDataError: (commandPrefix) => `No OpenClaw sources were detected. Run \`${commandPrefix} doctor --runtime openclaw\` or provide --log-file / --sessions-dir.`,
2759
+ defaultPaths: () => ({
2760
+ runtime: "openclaw",
2761
+ gatewayPattern: getDefaultOpenClawGatewayPattern(),
2762
+ sessionsPattern: getDefaultOpenClawSessionsPattern()
2763
+ })
2764
+ },
2765
+ hermes: {
2766
+ runtime: "hermes",
2767
+ productName: "Hermes",
2768
+ detectSources: detectHermesSources,
2769
+ normalizeSources: normalizeHermesSources,
2770
+ noSourceNotes: [
2771
+ "No Hermes gateway logs or session files were detected.",
2772
+ "Doctor checks local defaults by default. Use --log-file or --sessions-dir if your Hermes data lives outside the defaults.",
2773
+ "Hermes remote transport is not part of this rollout yet."
2774
+ ],
2775
+ gatewayNote: "Hermes gateway logs detected. Xerg prefers agent.log entries when billable model-call records are present.",
2776
+ sessionNote: "Hermes session transcripts detected. Xerg will extract usage metadata only.",
2777
+ noDataError: (commandPrefix) => `No Hermes sources were detected. Run \`${commandPrefix} doctor --runtime hermes\` or provide --log-file / --sessions-dir.`,
2778
+ defaultPaths: () => ({
2779
+ runtime: "hermes",
2780
+ gatewayPattern: getDefaultHermesGatewayPattern(),
2781
+ sessionsPattern: getDefaultHermesSessionsPattern()
2782
+ })
2122
2783
  }
2123
- return candidate.toISOString().slice(0, 10);
2784
+ };
2785
+ function getRuntimeAdapter(runtime) {
2786
+ return RUNTIME_ADAPTERS[runtime];
2124
2787
  }
2125
- function incrementUtcDay(date) {
2126
- const candidate = /* @__PURE__ */ new Date(`${date}T00:00:00.000Z`);
2127
- candidate.setUTCDate(candidate.getUTCDate() + 1);
2128
- return candidate.toISOString().slice(0, 10);
2788
+ function hasExplicitLocalPaths(options) {
2789
+ return Boolean(options.logFile || options.sessionsDir);
2129
2790
  }
2130
- function buildObservedUtcDayRange(runs) {
2131
- const days = runs.flatMap((run2) => run2.calls).map((call) => toUtcDay(call.timestamp)).filter((day) => day !== null).sort();
2132
- if (days.length === 0) {
2133
- return [];
2791
+ function inferRuntimeFromExplicitPaths(options) {
2792
+ const paths = [options.logFile, options.sessionsDir].filter((path) => Boolean(path)).map((path) => path.replace(/\\/g, "/").toLowerCase());
2793
+ if (paths.length === 0) {
2794
+ return null;
2134
2795
  }
2135
- const range = [];
2136
- let current = days[0];
2137
- const last = days[days.length - 1];
2138
- while (current <= last) {
2139
- range.push(current);
2140
- current = incrementUtcDay(current);
2796
+ const hints = /* @__PURE__ */ new Set();
2797
+ for (const path of paths) {
2798
+ const name = basename4(path).toLowerCase();
2799
+ if (path.includes("/.openclaw/") || path.includes("/openclaw/") || path.includes("/tmp/openclaw") || name.includes("openclaw")) {
2800
+ hints.add("openclaw");
2801
+ }
2802
+ if (path.includes("/.hermes/") || path.includes("/hermes/") || path.includes("/.hermes") || name.includes("hermes") || name.startsWith("agent.log")) {
2803
+ hints.add("hermes");
2804
+ }
2141
2805
  }
2142
- return range;
2806
+ return hints.size === 1 ? Array.from(hints)[0] : null;
2143
2807
  }
2144
- function reconcileDailyTotal(rows, key, expected) {
2145
- if (rows.length === 0) {
2146
- return;
2808
+ async function probeRuntimeCandidate(adapter, options) {
2809
+ const sources = await adapter.detectSources(options);
2810
+ return {
2811
+ adapter,
2812
+ sources,
2813
+ usable: sources.length > 0
2814
+ };
2815
+ }
2816
+ function buildResolvedDoctorNotes(adapter, sources) {
2817
+ if (sources.length === 0) {
2818
+ return adapter.noSourceNotes;
2147
2819
  }
2148
- const actual = round5(
2149
- rows.reduce((sum, row) => sum + (typeof row[key] === "number" ? row[key] : 0), 0)
2150
- );
2151
- const delta = round5(expected - actual);
2152
- if (delta === 0) {
2153
- return;
2820
+ const notes = [];
2821
+ if (sources.some((source) => source.kind === "gateway")) {
2822
+ notes.push(adapter.gatewayNote);
2154
2823
  }
2155
- const last = rows[rows.length - 1];
2156
- const current = last[key];
2157
- if (typeof current === "number") {
2158
- last[key] = round5(current + delta);
2824
+ if (sources.some((source) => source.kind === "sessions")) {
2825
+ notes.push(adapter.sessionNote);
2159
2826
  }
2827
+ return notes;
2160
2828
  }
2161
- function buildSpendByDay(runs) {
2162
- const days = buildObservedUtcDayRange(runs);
2163
- if (days.length === 0) {
2164
- return [];
2165
- }
2166
- const byDay = new Map(
2167
- days.map((day) => [
2168
- day,
2169
- { date: day, observedSpendUsd: 0, estimatedSpendUsd: 0, callCount: 0 }
2170
- ])
2171
- );
2172
- for (const run2 of runs) {
2173
- for (const call of run2.calls) {
2174
- const day = toUtcDay(call.timestamp);
2175
- if (!day) {
2176
- continue;
2177
- }
2178
- const bucket = byDay.get(day);
2179
- if (!bucket) {
2180
- continue;
2181
- }
2182
- bucket.callCount += 1;
2183
- if (call.costSource === "observed") {
2184
- bucket.observedSpendUsd += call.costUsd;
2185
- } else if (call.costSource === "estimated") {
2186
- bucket.estimatedSpendUsd += call.costUsd;
2187
- }
2188
- }
2189
- }
2190
- const rows = days.map((day) => {
2191
- const bucket = byDay.get(day);
2192
- const observedSpendUsd = round5(bucket?.observedSpendUsd ?? 0);
2193
- const estimatedSpendUsd = round5(bucket?.estimatedSpendUsd ?? 0);
2829
+ function buildResolvedDoctorReport(adapter, sources) {
2830
+ return {
2831
+ canAudit: sources.length > 0,
2832
+ mode: sources.length > 0 ? "resolved" : "none",
2833
+ runtime: sources.length > 0 ? adapter.runtime : null,
2834
+ sources,
2835
+ defaults: [adapter.defaultPaths()],
2836
+ notes: buildResolvedDoctorNotes(adapter, sources)
2837
+ };
2838
+ }
2839
+ function buildAutoNoDataDoctorReport(candidates) {
2840
+ return {
2841
+ canAudit: false,
2842
+ mode: "none",
2843
+ runtime: null,
2844
+ sources: candidates.flatMap((candidate) => candidate.sources),
2845
+ defaults: Object.values(RUNTIME_ADAPTERS).map((adapter) => adapter.defaultPaths()),
2846
+ notes: [
2847
+ "No supported local runtime sources were detected.",
2848
+ "Auto-detection checked both OpenClaw and Hermes local defaults.",
2849
+ "Use --runtime openclaw or --runtime hermes with --log-file / --sessions-dir when you want to point Xerg at explicit local paths."
2850
+ ]
2851
+ };
2852
+ }
2853
+ function buildAutoAmbiguousDoctorReport(candidates) {
2854
+ return {
2855
+ canAudit: false,
2856
+ mode: "ambiguous",
2857
+ runtime: null,
2858
+ sources: candidates.flatMap((candidate) => candidate.sources),
2859
+ defaults: Object.values(RUNTIME_ADAPTERS).map((adapter) => adapter.defaultPaths()),
2860
+ notes: [
2861
+ "Both OpenClaw and Hermes local sources were detected.",
2862
+ "Re-run doctor with --runtime openclaw or --runtime hermes to choose the local runtime explicitly."
2863
+ ]
2864
+ };
2865
+ }
2866
+ function buildExplicitNoDataError(options, hintedRuntime) {
2867
+ const commandPrefix = options.commandPrefix ?? "xerg";
2868
+ if (hintedRuntime) {
2869
+ return getRuntimeAdapter(hintedRuntime).noDataError(commandPrefix);
2870
+ }
2871
+ return `No supported local runtime sources were detected. Run \`${commandPrefix} doctor\`, or use --runtime openclaw / --runtime hermes with --log-file / --sessions-dir.`;
2872
+ }
2873
+ function buildExplicitNoDataDoctorReport(candidates, hintedRuntime) {
2874
+ if (hintedRuntime) {
2875
+ const adapter = getRuntimeAdapter(hintedRuntime);
2194
2876
  return {
2195
- date: day,
2196
- observedSpendUsd,
2197
- estimatedSpendUsd,
2198
- spendUsd: round5(observedSpendUsd + estimatedSpendUsd),
2199
- callCount: bucket?.callCount ?? 0
2877
+ canAudit: false,
2878
+ mode: "none",
2879
+ runtime: null,
2880
+ sources: [],
2881
+ defaults: [adapter.defaultPaths()],
2882
+ notes: [
2883
+ `No ${adapter.productName} sources were detected from the provided local paths.`,
2884
+ `Verify --log-file / --sessions-dir and re-run doctor with --runtime ${adapter.runtime} if needed.`
2885
+ ]
2200
2886
  };
2201
- });
2202
- reconcileDailyTotal(
2203
- rows,
2204
- "observedSpendUsd",
2205
- round5(runs.reduce((sum, run2) => sum + run2.observedCostUsd, 0))
2206
- );
2207
- reconcileDailyTotal(
2208
- rows,
2209
- "estimatedSpendUsd",
2210
- round5(runs.reduce((sum, run2) => sum + run2.estimatedCostUsd, 0))
2211
- );
2212
- for (const row of rows) {
2213
- row.spendUsd = round5(row.observedSpendUsd + row.estimatedSpendUsd);
2214
2887
  }
2215
- return rows;
2888
+ return {
2889
+ canAudit: false,
2890
+ mode: "none",
2891
+ runtime: null,
2892
+ sources: candidates.flatMap((candidate) => candidate.sources),
2893
+ defaults: Object.values(RUNTIME_ADAPTERS).map((adapter) => adapter.defaultPaths()),
2894
+ notes: [
2895
+ "No supported local runtime sources were detected from the provided local paths.",
2896
+ "Verify --log-file / --sessions-dir and re-run doctor with --runtime openclaw or --runtime hermes if needed."
2897
+ ]
2898
+ };
2216
2899
  }
2217
- function buildWasteByDay(wasteAttributions, days, expectedWasteUsd) {
2218
- if (days.length === 0) {
2219
- return [];
2900
+ function getRuntimeProductName(runtime) {
2901
+ return getRuntimeAdapter(runtime).productName;
2902
+ }
2903
+ async function resolveRuntimeCandidates(options) {
2904
+ return Promise.all(
2905
+ Object.values(RUNTIME_ADAPTERS).map(
2906
+ (adapter) => probeRuntimeCandidate(adapter, options)
2907
+ )
2908
+ );
2909
+ }
2910
+ async function resolveLocalAgentRuntime(options) {
2911
+ const requestedRuntime = options.runtime ?? "auto";
2912
+ if (requestedRuntime !== "auto") {
2913
+ const adapter = getRuntimeAdapter(requestedRuntime);
2914
+ const sources = await adapter.detectSources(options);
2915
+ return {
2916
+ adapter,
2917
+ sources
2918
+ };
2220
2919
  }
2221
- const byDay = new Map(days.map((day) => [day, 0]));
2222
- for (const attribution of wasteAttributions) {
2223
- const day = toUtcDay(attribution.timestamp);
2224
- if (!day || !byDay.has(day)) {
2225
- continue;
2920
+ const candidates = await resolveRuntimeCandidates(options);
2921
+ const usableCandidates = candidates.filter((candidate) => candidate.usable);
2922
+ if (hasExplicitLocalPaths(options)) {
2923
+ const hintedRuntime = inferRuntimeFromExplicitPaths(options);
2924
+ if (hintedRuntime) {
2925
+ const hintedCandidate = usableCandidates.find(
2926
+ (candidate) => candidate.adapter.runtime === hintedRuntime
2927
+ );
2928
+ if (hintedCandidate) {
2929
+ return {
2930
+ adapter: hintedCandidate.adapter,
2931
+ sources: hintedCandidate.sources
2932
+ };
2933
+ }
2226
2934
  }
2227
- byDay.set(day, (byDay.get(day) ?? 0) + attribution.wasteUsd);
2935
+ if (usableCandidates.length === 0) {
2936
+ throw new Error(buildExplicitNoDataError(options, hintedRuntime));
2937
+ }
2938
+ if (usableCandidates.length === 1) {
2939
+ return {
2940
+ adapter: usableCandidates[0].adapter,
2941
+ sources: usableCandidates[0].sources
2942
+ };
2943
+ }
2944
+ throw new Error(
2945
+ "Could not determine whether the provided local files belong to OpenClaw or Hermes. Re-run with --runtime openclaw or --runtime hermes."
2946
+ );
2228
2947
  }
2229
- const rows = days.map((day) => ({
2230
- date: day,
2231
- wasteUsd: round5(byDay.get(day) ?? 0)
2232
- }));
2233
- reconcileDailyTotal(rows, "wasteUsd", round5(expectedWasteUsd));
2234
- return rows;
2948
+ if (usableCandidates.length === 0) {
2949
+ throw new Error(
2950
+ `No supported local runtime sources were detected. Run \`${options.commandPrefix ?? "xerg"} doctor\`, or use --runtime openclaw / --runtime hermes with --log-file / --sessions-dir.`
2951
+ );
2952
+ }
2953
+ if (usableCandidates.length > 1) {
2954
+ throw new Error(
2955
+ "Both OpenClaw and Hermes local sources were detected. Re-run with --runtime openclaw or --runtime hermes."
2956
+ );
2957
+ }
2958
+ return {
2959
+ adapter: usableCandidates[0].adapter,
2960
+ sources: usableCandidates[0].sources
2961
+ };
2235
2962
  }
2236
-
2237
- // ../core/src/report/summary.ts
2238
- function buildBreakdown(items) {
2239
- const buckets = /* @__PURE__ */ new Map();
2240
- for (const item of items) {
2241
- const current = buckets.get(item.key) ?? { spendUsd: 0, observedSpendUsd: 0, callCount: 0 };
2242
- current.spendUsd += item.spendUsd;
2243
- current.observedSpendUsd += item.observedSpendUsd;
2244
- current.callCount += 1;
2245
- buckets.set(item.key, current);
2963
+ async function doctorAgentRuntime(options) {
2964
+ const requestedRuntime = options.runtime ?? "auto";
2965
+ if (requestedRuntime !== "auto") {
2966
+ options.onProgress?.(`Checking local ${getRuntimeProductName(requestedRuntime)} defaults...`);
2967
+ const adapter = getRuntimeAdapter(requestedRuntime);
2968
+ const sources = await adapter.detectSources(options);
2969
+ options.onProgress?.(
2970
+ sources.length > 0 ? `Detected ${sources.length} local source file${sources.length === 1 ? "" : "s"}.` : `No local ${adapter.productName} source files were detected.`
2971
+ );
2972
+ return buildResolvedDoctorReport(adapter, sources);
2246
2973
  }
2247
- return Array.from(buckets.entries()).map(([key, value]) => {
2248
- const observedShare = value.spendUsd === 0 ? 0 : value.observedSpendUsd / value.spendUsd;
2974
+ options.onProgress?.("Checking local runtime defaults...");
2975
+ const candidates = await resolveRuntimeCandidates(options);
2976
+ const usableCandidates = candidates.filter((candidate) => candidate.usable);
2977
+ const detectedCount = candidates.reduce((sum, candidate) => sum + candidate.sources.length, 0);
2978
+ options.onProgress?.(
2979
+ detectedCount > 0 ? `Detected ${detectedCount} local source file${detectedCount === 1 ? "" : "s"} across supported runtimes.` : "No local runtime source files were detected."
2980
+ );
2981
+ if (hasExplicitLocalPaths(options)) {
2982
+ const hintedRuntime = inferRuntimeFromExplicitPaths(options);
2983
+ if (hintedRuntime) {
2984
+ const hintedCandidate = usableCandidates.find(
2985
+ (candidate) => candidate.adapter.runtime === hintedRuntime
2986
+ );
2987
+ if (hintedCandidate) {
2988
+ return buildResolvedDoctorReport(hintedCandidate.adapter, hintedCandidate.sources);
2989
+ }
2990
+ }
2991
+ if (usableCandidates.length === 0) {
2992
+ return buildExplicitNoDataDoctorReport(candidates, hintedRuntime);
2993
+ }
2994
+ if (usableCandidates.length === 1) {
2995
+ return buildResolvedDoctorReport(usableCandidates[0].adapter, usableCandidates[0].sources);
2996
+ }
2249
2997
  return {
2250
- key,
2251
- spendUsd: Number(value.spendUsd.toFixed(6)),
2252
- callCount: value.callCount,
2253
- observedShare: Number(observedShare.toFixed(4))
2998
+ canAudit: false,
2999
+ mode: "ambiguous",
3000
+ runtime: null,
3001
+ sources: candidates.flatMap((candidate) => candidate.sources),
3002
+ defaults: Object.values(RUNTIME_ADAPTERS).map((adapter) => adapter.defaultPaths()),
3003
+ notes: [
3004
+ "Could not determine whether the provided local files belong to OpenClaw or Hermes.",
3005
+ "Re-run doctor with --runtime openclaw or --runtime hermes to choose the local runtime explicitly."
3006
+ ]
2254
3007
  };
2255
- }).sort((left, right) => right.spendUsd - left.spendUsd);
2256
- }
2257
- function buildAuditSummary(input) {
2258
- const callCount = input.runs.reduce((sum, run2) => sum + run2.calls.length, 0);
2259
- const totalSpendUsd = input.runs.reduce((sum, run2) => sum + run2.totalCostUsd, 0);
2260
- const observedSpendUsd = input.runs.reduce((sum, run2) => sum + run2.observedCostUsd, 0);
2261
- const estimatedSpendUsd = input.runs.reduce((sum, run2) => sum + run2.estimatedCostUsd, 0);
2262
- const wasteSpendUsd = input.findings.filter((finding) => finding.classification === "waste").reduce((sum, finding) => sum + finding.costImpactUsd, 0);
2263
- const opportunitySpendUsd = input.findings.filter((finding) => finding.classification === "opportunity").reduce((sum, finding) => sum + finding.costImpactUsd, 0);
2264
- const generatedAt = isoNow();
2265
- const spendByDay = buildSpendByDay(input.runs);
2266
- const observedDays = buildObservedUtcDayRange(input.runs);
2267
- return {
2268
- auditId: sha1(
2269
- `${generatedAt}:${input.runs.length}:${input.sources.map((source) => source.path).join("|")}`
2270
- ),
2271
- generatedAt,
2272
- comparisonKey: input.comparisonKeyOverride ?? buildComparisonKey({
2273
- sources: input.sources,
2274
- since: input.since
2275
- }),
2276
- comparison: null,
2277
- since: input.since,
2278
- runCount: input.runs.length,
2279
- callCount,
2280
- totalSpendUsd: Number(totalSpendUsd.toFixed(6)),
2281
- observedSpendUsd: Number(observedSpendUsd.toFixed(6)),
2282
- estimatedSpendUsd: Number(estimatedSpendUsd.toFixed(6)),
2283
- wasteSpendUsd: Number(wasteSpendUsd.toFixed(6)),
2284
- opportunitySpendUsd: Number(opportunitySpendUsd.toFixed(6)),
2285
- structuralWasteRate: Number(
2286
- (totalSpendUsd === 0 ? 0 : wasteSpendUsd / totalSpendUsd).toFixed(4)
2287
- ),
2288
- wasteByKind: buildTaxonomyBuckets(input.findings, "waste"),
2289
- opportunityByKind: buildTaxonomyBuckets(input.findings, "opportunity"),
2290
- spendByWorkflow: buildBreakdown(
2291
- input.runs.map((run2) => ({
2292
- key: run2.workflow,
2293
- spendUsd: run2.totalCostUsd,
2294
- observedSpendUsd: run2.observedCostUsd
2295
- }))
2296
- ),
2297
- spendByModel: buildBreakdown(
2298
- input.runs.flatMap(
2299
- (run2) => run2.calls.map((call) => ({
2300
- key: `${call.provider}/${call.model}`,
2301
- spendUsd: call.costUsd,
2302
- observedSpendUsd: call.costSource === "observed" ? call.costUsd : 0
2303
- }))
2304
- )
2305
- ),
2306
- spendByDay,
2307
- wasteByDay: buildWasteByDay(input.wasteAttributions, observedDays, wasteSpendUsd),
2308
- findings: input.findings,
2309
- notes: [
2310
- "Cost per outcome is intentionally unavailable in v0. Xerg is measuring waste intelligence only.",
2311
- "Opportunity findings are directional recommendations, not proven waste."
2312
- ],
2313
- sourceFiles: input.sources,
2314
- dbPath: input.dbPath
2315
- };
3008
+ }
3009
+ if (usableCandidates.length === 0) {
3010
+ return buildAutoNoDataDoctorReport(candidates);
3011
+ }
3012
+ if (usableCandidates.length > 1) {
3013
+ return buildAutoAmbiguousDoctorReport(usableCandidates);
3014
+ }
3015
+ return buildResolvedDoctorReport(usableCandidates[0].adapter, usableCandidates[0].sources);
2316
3016
  }
2317
3017
 
2318
3018
  // ../core/src/audit.ts
2319
- async function doctorOpenClaw(options) {
2320
- return inspectOpenClawSources(options);
2321
- }
2322
3019
  async function doctorCursorUsageCsv(options) {
2323
3020
  return inspectCursorUsageCsv(options);
2324
3021
  }
@@ -2376,25 +3073,25 @@ function hasPricingCoverageChange(current, baseline) {
2376
3073
  }
2377
3074
  return (current?.pricedCallCount ?? 0) !== (baseline?.pricedCallCount ?? 0) || (current?.unpricedCallCount ?? 0) !== (baseline?.unpricedCallCount ?? 0) || (current?.pricedTokenCount ?? 0) !== (baseline?.pricedTokenCount ?? 0) || (current?.unpricedTokenCount ?? 0) !== (baseline?.unpricedTokenCount ?? 0);
2378
3075
  }
2379
- async function auditOpenClaw(options) {
2380
- options.onProgress?.("Scanning for OpenClaw source files...");
3076
+ async function auditResolvedRuntime(runtime, options, detectedSources) {
3077
+ const adapter = getRuntimeAdapter(runtime);
3078
+ options.onProgress?.(`Scanning for ${adapter.productName} source files...`);
2381
3079
  validateCompareOptions(options);
2382
- const sources = await detectOpenClawSources(options);
3080
+ const sources = detectedSources ?? await adapter.detectSources(options);
2383
3081
  if (sources.length === 0) {
2384
- options.onProgress?.("No OpenClaw source files were detected.");
2385
- throw new Error(
2386
- `No OpenClaw sources were detected. Run \`${options.commandPrefix ?? "xerg"} doctor\` or provide --log-file / --sessions-dir.`
2387
- );
3082
+ options.onProgress?.(`No ${adapter.productName} source files were detected.`);
3083
+ throw new Error(adapter.noDataError(options.commandPrefix ?? "xerg"));
2388
3084
  }
2389
3085
  options.onProgress?.(`Detected ${sources.length} source file${sources.length === 1 ? "" : "s"}.`);
2390
- options.onProgress?.("Normalizing OpenClaw source files...");
2391
- const runs = normalizeOpenClawSources(sources, options.since);
3086
+ options.onProgress?.(`Normalizing ${adapter.productName} source files...`);
3087
+ const runs = adapter.normalizeSources(sources, options.since);
2392
3088
  options.onProgress?.(`Normalized ${runs.length} run${runs.length === 1 ? "" : "s"}.`);
2393
3089
  options.onProgress?.("Computing waste and savings findings...");
2394
3090
  const { findings, wasteAttributions } = buildFindings(runs);
2395
3091
  const dbPath = options.noDb ? void 0 : options.dbPath ?? getDefaultDbPath();
2396
3092
  options.onProgress?.("Building audit summary...");
2397
3093
  const summary = buildAuditSummary({
3094
+ runtime,
2398
3095
  runs,
2399
3096
  findings,
2400
3097
  wasteAttributions,
@@ -2407,6 +3104,14 @@ async function auditOpenClaw(options) {
2407
3104
  persistLocalSnapshot(summary, runs, dbPath, options.onProgress);
2408
3105
  return summary;
2409
3106
  }
3107
+ async function auditAgentRuntime(options) {
3108
+ const runtime = options.runtime ?? "auto";
3109
+ if (runtime !== "auto") {
3110
+ return auditResolvedRuntime(runtime, options);
3111
+ }
3112
+ const resolved = await resolveLocalAgentRuntime(options);
3113
+ return auditResolvedRuntime(resolved.adapter.runtime, options, resolved.sources);
3114
+ }
2410
3115
  async function auditCursorUsageCsv(options) {
2411
3116
  options.onProgress?.("Reading Cursor usage CSV...");
2412
3117
  validateCompareOptions(options);
@@ -2430,6 +3135,7 @@ async function auditCursorUsageCsv(options) {
2430
3135
  const dbPath = options.noDb ? void 0 : options.dbPath ?? getDefaultDbPath();
2431
3136
  options.onProgress?.("Building audit summary...");
2432
3137
  const summary = buildAuditSummary({
3138
+ runtime: "cursor",
2433
3139
  runs: normalized.runs,
2434
3140
  findings,
2435
3141
  wasteAttributions,
@@ -2709,25 +3415,36 @@ function renderDailyTrendRows(spendByDay, wasteByDay) {
2709
3415
  }
2710
3416
  function renderDoctorReport(report, options) {
2711
3417
  const commandPrefix = options?.commandPrefix ?? "xerg";
2712
- const nextSteps = report.canAudit ? [] : [
3418
+ const status = report.mode === "resolved" && report.runtime ? `${report.runtime === "hermes" ? "Hermes" : "OpenClaw"} sources detected.` : report.mode === "ambiguous" ? "Multiple supported local runtimes detected." : "No supported local sources detected.";
3419
+ const nextSteps = report.canAudit ? [] : report.mode === "ambiguous" ? [
3420
+ "",
3421
+ "## Next steps",
3422
+ `- Inspect OpenClaw only: ${commandPrefix} doctor --runtime openclaw`,
3423
+ `- Inspect Hermes only: ${commandPrefix} doctor --runtime hermes`,
3424
+ `- Try explicit local paths: ${commandPrefix} doctor --runtime hermes --log-file /path/to/log --sessions-dir /path/to/sessions`
3425
+ ] : [
2713
3426
  "",
2714
3427
  "## Next steps",
2715
- `- Try explicit local paths: ${commandPrefix} doctor --log-file /path/to/openclaw.log --sessions-dir /path/to/sessions`,
2716
- `- Inspect an SSH host: ${commandPrefix} doctor --remote user@host`,
2717
- `- Inspect a Railway service: ${commandPrefix} doctor --railway`,
3428
+ `- Inspect OpenClaw locally: ${commandPrefix} doctor --runtime openclaw`,
3429
+ `- Inspect Hermes locally: ${commandPrefix} doctor --runtime hermes`,
3430
+ `- Try explicit local paths: ${commandPrefix} doctor --runtime hermes --log-file /path/to/log --sessions-dir /path/to/sessions`,
3431
+ `- Inspect an SSH host for OpenClaw: ${commandPrefix} doctor --remote user@host`,
3432
+ `- Inspect a Railway service for OpenClaw: ${commandPrefix} doctor --railway`,
2718
3433
  "- Remote audits still analyze locally after Xerg pulls the source files to your machine."
2719
3434
  ];
2720
3435
  const sections = [
2721
3436
  "# Xerg doctor",
2722
3437
  "",
2723
- report.canAudit ? "OpenClaw sources detected." : "No OpenClaw sources detected.",
3438
+ status,
2724
3439
  "",
2725
3440
  "## Defaults",
2726
- `- gateway logs: ${report.defaults.gatewayPattern}`,
2727
- `- session files: ${report.defaults.sessionsPattern}`,
3441
+ ...report.defaults.flatMap((defaults) => [
3442
+ `- ${defaults.runtime === "hermes" ? "Hermes" : "OpenClaw"} gateway logs: ${defaults.gatewayPattern}`,
3443
+ `- ${defaults.runtime === "hermes" ? "Hermes" : "OpenClaw"} session files: ${defaults.sessionsPattern}`
3444
+ ]),
2728
3445
  "",
2729
3446
  "## Sources",
2730
- ...report.sources.length > 0 ? report.sources.map((source) => `- [${source.kind}] ${source.path}`) : ["- none"],
3447
+ ...report.sources.length > 0 ? report.sources.map((source) => `- [${source.runtime}/${source.kind}] ${source.path}`) : ["- none"],
2731
3448
  "",
2732
3449
  "## Notes",
2733
3450
  ...report.notes.map((note) => `- ${note}`),
@@ -3120,17 +3837,17 @@ async function pushAudit(payload, config) {
3120
3837
  }
3121
3838
 
3122
3839
  // src/push/config.ts
3123
- import { readFileSync as readFileSync4 } from "fs";
3124
- import { homedir as homedir3 } from "os";
3125
- import { join as join4 } from "path";
3840
+ import { readFileSync as readFileSync5 } from "fs";
3841
+ import { homedir as homedir4 } from "os";
3842
+ import { join as join5 } from "path";
3126
3843
 
3127
3844
  // src/auth/credentials.ts
3128
- import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync, writeFileSync } from "fs";
3129
- import { homedir as homedir2 } from "os";
3130
- import { dirname as dirname2, join as join3 } from "path";
3845
+ import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync, writeFileSync } from "fs";
3846
+ import { homedir as homedir3 } from "os";
3847
+ import { dirname as dirname2, join as join4 } from "path";
3131
3848
  function getCredentialsPath() {
3132
- const xdgConfig = process.env.XDG_CONFIG_HOME || join3(homedir2(), ".config");
3133
- return join3(xdgConfig, "xerg", "credentials.json");
3849
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join4(homedir3(), ".config");
3850
+ return join4(xdgConfig, "xerg", "credentials.json");
3134
3851
  }
3135
3852
  function storeCredentials(token) {
3136
3853
  const credPath = getCredentialsPath();
@@ -3143,7 +3860,7 @@ function loadStoredCredentials() {
3143
3860
  const credPath = getCredentialsPath();
3144
3861
  try {
3145
3862
  if (!existsSync(credPath)) return null;
3146
- const raw = readFileSync3(credPath, "utf8");
3863
+ const raw = readFileSync4(credPath, "utf8");
3147
3864
  const parsed = JSON.parse(raw);
3148
3865
  return parsed.token || null;
3149
3866
  } catch {
@@ -3163,7 +3880,7 @@ function clearCredentials() {
3163
3880
 
3164
3881
  // src/push/config.ts
3165
3882
  var DEFAULT_API_URL = "https://api.xerg.ai";
3166
- var CONFIG_PATH = join4(homedir3(), ".xerg", "config.json");
3883
+ var CONFIG_PATH = join5(homedir4(), ".xerg", "config.json");
3167
3884
  function loadPushConfig() {
3168
3885
  const envKey = process.env.XERG_API_KEY;
3169
3886
  const envUrl = process.env.XERG_API_URL;
@@ -3174,7 +3891,7 @@ function loadPushConfig() {
3174
3891
  };
3175
3892
  }
3176
3893
  try {
3177
- const raw = readFileSync4(CONFIG_PATH, "utf8");
3894
+ const raw = readFileSync5(CONFIG_PATH, "utf8");
3178
3895
  const parsed = JSON.parse(raw);
3179
3896
  if (parsed.apiKey) {
3180
3897
  return {
@@ -3204,8 +3921,8 @@ import { hostname } from "os";
3204
3921
  import { execSync, spawnSync } from "child_process";
3205
3922
  import { createHash as createHash2 } from "crypto";
3206
3923
  import { mkdirSync as mkdirSync4, rmSync as rmSync2 } from "fs";
3207
- import { homedir as homedir4, tmpdir } from "os";
3208
- import { join as join5 } from "path";
3924
+ import { homedir as homedir5, tmpdir } from "os";
3925
+ import { join as join6 } from "path";
3209
3926
  var DEFAULT_GATEWAY_DIR = "/tmp/openclaw";
3210
3927
  var DEFAULT_SESSIONS_DIR = "~/.openclaw/agents";
3211
3928
  function hashString(input) {
@@ -3214,7 +3931,7 @@ function hashString(input) {
3214
3931
  function sshArgs(source) {
3215
3932
  const args = [];
3216
3933
  if (source.identityFile) {
3217
- const resolved = source.identityFile.replace(/^~/, homedir4());
3934
+ const resolved = source.identityFile.replace(/^~/, homedir5());
3218
3935
  args.push("-i", resolved);
3219
3936
  }
3220
3937
  args.push("-o", "BatchMode=yes", "-o", "ConnectTimeout=10");
@@ -3223,7 +3940,7 @@ function sshArgs(source) {
3223
3940
  function rsyncSshCommand(source) {
3224
3941
  const parts = ["ssh"];
3225
3942
  if (source.identityFile) {
3226
- const resolved = source.identityFile.replace(/^~/, homedir4());
3943
+ const resolved = source.identityFile.replace(/^~/, homedir5());
3227
3944
  parts.push(`-i "${resolved}"`);
3228
3945
  }
3229
3946
  parts.push("-o BatchMode=yes", "-o ConnectTimeout=10");
@@ -3311,7 +4028,7 @@ function rsyncPull(opts) {
3311
4028
  if (status !== 0 || !stdout) return false;
3312
4029
  const files = stdout.split("\n").filter(Boolean);
3313
4030
  if (files.length === 0) return false;
3314
- const tmpFile = join5(tmpdir(), `xerg-filelist-${hashString(opts.remotePath)}`);
4031
+ const tmpFile = join6(tmpdir(), `xerg-filelist-${hashString(opts.remotePath)}`);
3315
4032
  const relativePaths = files.map(
3316
4033
  (f) => f.startsWith(opts.remotePath) ? f.slice(opts.remotePath.length).replace(/^\//, "") : f
3317
4034
  );
@@ -3360,12 +4077,12 @@ function pullDirectory(opts) {
3360
4077
  }
3361
4078
  function resolveLocalPath(source, keepFiles) {
3362
4079
  if (keepFiles) {
3363
- const cacheDir = join5(homedir4(), ".xerg", "remote-cache", source.name);
4080
+ const cacheDir = join6(homedir5(), ".xerg", "remote-cache", source.name);
3364
4081
  mkdirSync4(cacheDir, { recursive: true });
3365
4082
  return cacheDir;
3366
4083
  }
3367
4084
  const hash = hashString(`${source.host}:${Date.now()}`);
3368
- const tmpPath = join5(tmpdir(), `xerg-remote-${hash}`);
4085
+ const tmpPath = join6(tmpdir(), `xerg-remote-${hash}`);
3369
4086
  mkdirSync4(tmpPath, { recursive: true });
3370
4087
  return tmpPath;
3371
4088
  }
@@ -3417,8 +4134,8 @@ async function pullRemoteFiles(opts) {
3417
4134
  useRsync ? "Local rsync detected. Xerg will prefer rsync and fall back to tar over SSH if needed." : "Local rsync not detected. Xerg will pull files with tar over SSH."
3418
4135
  );
3419
4136
  const localBase = resolveLocalPath(source, keepFiles);
3420
- const gatewayDir = join5(localBase, "gateway");
3421
- const sessionsDir = join5(localBase, "sessions");
4137
+ const gatewayDir = join6(localBase, "gateway");
4138
+ const sessionsDir = join6(localBase, "sessions");
3422
4139
  const remoteLogPath = source.logFile ?? DEFAULT_GATEWAY_DIR;
3423
4140
  const remoteSessionsPath = source.sessionsDir ?? DEFAULT_SESSIONS_DIR;
3424
4141
  const { stdout: expandedSessions } = sshExec(source, `eval echo ${remoteSessionsPath}`);
@@ -3610,8 +4327,8 @@ function formatBytes(bytes) {
3610
4327
  import { execSync as execSync2, spawnSync as spawnSync2 } from "child_process";
3611
4328
  import { createHash as createHash3 } from "crypto";
3612
4329
  import { mkdirSync as mkdirSync5, rmSync as rmSync3 } from "fs";
3613
- import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
3614
- import { join as join6 } from "path";
4330
+ import { homedir as homedir6, tmpdir as tmpdir2 } from "os";
4331
+ import { join as join7 } from "path";
3615
4332
  var DEFAULT_GATEWAY_DIR2 = "/tmp/openclaw";
3616
4333
  var DEFAULT_SESSIONS_DIR2 = "~/.openclaw/agents";
3617
4334
  var ALTERNATE_SESSION_PATHS = ["/data/.clawdbot/agents/main/sessions"];
@@ -3684,13 +4401,13 @@ function tarRailwayPull(opts) {
3684
4401
  }
3685
4402
  function resolveLocalPath2(source, keepFiles) {
3686
4403
  if (keepFiles) {
3687
- const cacheDir = join6(homedir5(), ".xerg", "remote-cache", source.name);
4404
+ const cacheDir = join7(homedir6(), ".xerg", "remote-cache", source.name);
3688
4405
  mkdirSync5(cacheDir, { recursive: true });
3689
4406
  return cacheDir;
3690
4407
  }
3691
4408
  const identity = source.railway ? `railway:${source.railway.projectId}:${Date.now()}` : `${source.name}:${Date.now()}`;
3692
4409
  const hash = hashString2(identity);
3693
- const tmpPath = join6(tmpdir2(), `xerg-remote-${hash}`);
4410
+ const tmpPath = join7(tmpdir2(), `xerg-remote-${hash}`);
3694
4411
  mkdirSync5(tmpPath, { recursive: true });
3695
4412
  return tmpPath;
3696
4413
  }
@@ -3761,8 +4478,8 @@ async function pullRemoteFilesRailway(opts) {
3761
4478
  }
3762
4479
  onProgress?.("Railway service reachable.");
3763
4480
  const localBase = resolveLocalPath2(source, keepFiles);
3764
- const gatewayDir = join6(localBase, "gateway");
3765
- const sessionsDir = join6(localBase, "sessions");
4481
+ const gatewayDir = join7(localBase, "gateway");
4482
+ const sessionsDir = join7(localBase, "sessions");
3766
4483
  const remoteLogPath = source.logFile ?? DEFAULT_GATEWAY_DIR2;
3767
4484
  onProgress?.("Checking Railway default paths for gateway logs and sessions...");
3768
4485
  const logCheck = checkRemotePath(remoteLogPath, target);
@@ -3991,13 +4708,13 @@ function formatBytes2(bytes) {
3991
4708
  }
3992
4709
 
3993
4710
  // src/transport/config.ts
3994
- import { readFileSync as readFileSync5 } from "fs";
4711
+ import { readFileSync as readFileSync6 } from "fs";
3995
4712
  import { resolve as resolve3 } from "path";
3996
4713
  function loadRemoteConfig(configPath) {
3997
4714
  const resolved = resolve3(configPath);
3998
4715
  let raw;
3999
4716
  try {
4000
- raw = readFileSync5(resolved, "utf8");
4717
+ raw = readFileSync6(resolved, "utf8");
4001
4718
  } catch {
4002
4719
  throw new Error(`Cannot read remote config at ${resolved}`);
4003
4720
  }
@@ -4071,9 +4788,10 @@ function validateRailwayEntry(entry) {
4071
4788
  // src/source-meta.ts
4072
4789
  var RAILWAY_SOURCE_ID = "OpenClaw - Railway";
4073
4790
  function buildLocalPushSourceMeta(kind, localHost = hostname()) {
4791
+ const productName = kind === "cursor" ? "Cursor" : kind === "hermes" ? "Hermes" : "OpenClaw";
4074
4792
  return {
4075
4793
  environment: "local",
4076
- sourceId: `${kind === "cursor" ? "Cursor" : "OpenClaw"} - ${localHost}`,
4794
+ sourceId: `${productName} - ${localHost}`,
4077
4795
  sourceHost: localHost
4078
4796
  };
4079
4797
  }
@@ -4092,6 +4810,9 @@ function buildRemotePushSourceMeta(source) {
4092
4810
  };
4093
4811
  }
4094
4812
  function buildCachedPushSourceMeta(summary, localHost = hostname()) {
4813
+ if (summary.runtime === "cursor") {
4814
+ return buildLocalPushSourceMeta("cursor", localHost);
4815
+ }
4095
4816
  const sourceFiles = summary.sourceFiles ?? [];
4096
4817
  const comparisonKey = summary.comparisonKey ?? "";
4097
4818
  if (sourceFiles.some((sourceFile) => sourceFile.kind === "cursor-usage-csv")) {
@@ -4112,6 +4833,9 @@ function buildCachedPushSourceMeta(summary, localHost = hostname()) {
4112
4833
  sourceHost: remoteHost
4113
4834
  };
4114
4835
  }
4836
+ if (summary.runtime === "hermes") {
4837
+ return buildLocalPushSourceMeta("hermes", localHost);
4838
+ }
4115
4839
  return buildLocalPushSourceMeta("openclaw", localHost);
4116
4840
  }
4117
4841
  function isGeneratedRailwayName(name) {
@@ -4136,11 +4860,23 @@ function resolveRemoteHost(target) {
4136
4860
  return parsed.host || target;
4137
4861
  }
4138
4862
 
4863
+ // src/version.ts
4864
+ import { readFileSync as readFileSync7 } from "fs";
4865
+ function getCliVersion() {
4866
+ try {
4867
+ const packageJsonPath = new URL("../package.json", import.meta.url);
4868
+ const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf8"));
4869
+ return packageJson.version ?? "0.0.0";
4870
+ } catch {
4871
+ return "0.0.0";
4872
+ }
4873
+ }
4874
+
4139
4875
  // src/commands/audit.ts
4140
- var NO_DATA_PATTERN = /no openclaw sources were detected/i;
4876
+ var NO_DATA_PATTERN = /no (openclaw|hermes|supported local runtime) sources? were detected/i;
4141
4877
  async function auditOrNoData(...args) {
4142
4878
  try {
4143
- return await auditOpenClaw(...args);
4879
+ return await auditAgentRuntime(...args);
4144
4880
  } catch (err) {
4145
4881
  if (err instanceof Error && NO_DATA_PATTERN.test(err.message)) {
4146
4882
  throw new NoDataError(err.message);
@@ -4153,7 +4889,9 @@ async function runAuditCommand(options) {
4153
4889
  if (options.dryRun && !options.push) {
4154
4890
  throw new Error("--dry-run requires --push.");
4155
4891
  }
4892
+ validateRuntimeOption(options.runtime);
4156
4893
  validateCursorUsageCsvOptions(options);
4894
+ validateHermesLocalOnly(options);
4157
4895
  const remoteFlags = [options.remote, options.remoteConfig, options.railway].filter(
4158
4896
  Boolean
4159
4897
  ).length;
@@ -4218,7 +4956,9 @@ async function runLocalAudit(options, logger) {
4218
4956
  checkThresholds(summary2, options);
4219
4957
  return;
4220
4958
  }
4221
- logger.verbose("Running a local audit.");
4959
+ logger.verbose(
4960
+ options.runtime ? `Running a local ${options.runtime === "hermes" ? "Hermes" : "OpenClaw"} audit.` : "Running a local runtime audit with auto-detection."
4961
+ );
4222
4962
  if (options.logFile) {
4223
4963
  logger.verbose(`Using explicit local log file: ${options.logFile}`);
4224
4964
  }
@@ -4226,6 +4966,7 @@ async function runLocalAudit(options, logger) {
4226
4966
  logger.verbose(`Using explicit local sessions directory: ${options.sessionsDir}`);
4227
4967
  }
4228
4968
  const summary = await auditOrNoData({
4969
+ runtime: options.runtime ?? "auto",
4229
4970
  logFile: options.logFile,
4230
4971
  sessionsDir: options.sessionsDir,
4231
4972
  since: options.since,
@@ -4237,16 +4978,27 @@ async function runLocalAudit(options, logger) {
4237
4978
  });
4238
4979
  renderOutput(summary, options);
4239
4980
  if (options.push) {
4240
- const meta = buildMeta(buildLocalPushSourceMeta("openclaw"));
4981
+ const meta = buildMeta(buildLocalPushSourceMeta(summary.runtime));
4241
4982
  await handlePush(summary, meta, options);
4242
4983
  }
4243
4984
  checkThresholds(summary, options);
4244
4985
  }
4986
+ function validateRuntimeOption(runtime) {
4987
+ if (!runtime) {
4988
+ return;
4989
+ }
4990
+ if (runtime !== "openclaw" && runtime !== "hermes") {
4991
+ throw new Error(
4992
+ `Unsupported runtime "${runtime}". Use --runtime openclaw or --runtime hermes.`
4993
+ );
4994
+ }
4995
+ }
4245
4996
  function validateCursorUsageCsvOptions(options) {
4246
4997
  if (!options.cursorUsageCsv) {
4247
4998
  return;
4248
4999
  }
4249
5000
  const conflicts = [
5001
+ options.runtime ? "--runtime" : null,
4250
5002
  options.logFile ? "--log-file" : null,
4251
5003
  options.sessionsDir ? "--sessions-dir" : null,
4252
5004
  options.remote ? "--remote" : null,
@@ -4263,6 +5015,27 @@ function validateCursorUsageCsvOptions(options) {
4263
5015
  throw new Error(`The --cursor-usage-csv flag cannot be combined with ${conflicts.join(", ")}.`);
4264
5016
  }
4265
5017
  }
5018
+ function validateHermesLocalOnly(options) {
5019
+ if (options.runtime !== "hermes") {
5020
+ return;
5021
+ }
5022
+ const conflicts = [
5023
+ options.remote ? "--remote" : null,
5024
+ options.remoteLogFile ? "--remote-log-file" : null,
5025
+ options.remoteSessionsDir ? "--remote-sessions-dir" : null,
5026
+ options.remoteConfig ? "--remote-config" : null,
5027
+ options.keepRemoteFiles ? "--keep-remote-files" : null,
5028
+ options.railway ? "--railway" : null,
5029
+ options.railwayProject ? "--project" : null,
5030
+ options.railwayEnvironment ? "--environment" : null,
5031
+ options.railwayService ? "--service" : null
5032
+ ].filter((flag) => flag !== null);
5033
+ if (conflicts.length > 0) {
5034
+ throw new Error(
5035
+ `Hermes remote transport is not supported yet. Remove ${conflicts.join(", ")} or switch to --runtime openclaw.`
5036
+ );
5037
+ }
5038
+ }
4266
5039
  function getComparisonKey(source) {
4267
5040
  if (source.transport === "railway") {
4268
5041
  return buildComparisonKeyForRailway(source);
@@ -4293,6 +5066,7 @@ async function runSingleRemoteAudit(source, options, logger) {
4293
5066
  try {
4294
5067
  const comparisonKeyOverride = getComparisonKey(source);
4295
5068
  const summary = await auditOrNoData({
5069
+ runtime: "openclaw",
4296
5070
  logFile: pullResult.logFile,
4297
5071
  sessionsDir: pullResult.sessionsDir,
4298
5072
  since: options.since,
@@ -4343,6 +5117,7 @@ ${errorMessages}`);
4343
5117
  for (const { source, pullResult } of results) {
4344
5118
  const comparisonKeyOverride = getComparisonKey(source);
4345
5119
  const summary = await auditOrNoData({
5120
+ runtime: "openclaw",
4346
5121
  logFile: pullResult.logFile,
4347
5122
  sessionsDir: pullResult.sessionsDir,
4348
5123
  since: options.since,
@@ -4406,18 +5181,9 @@ ${"\u2550".repeat(60)}
4406
5181
  }
4407
5182
  }
4408
5183
  }
4409
- function readCliVersion() {
4410
- try {
4411
- const packageJsonPath = new URL("../../package.json", import.meta.url);
4412
- const pkg = JSON.parse(readFileSync6(packageJsonPath, "utf8"));
4413
- return pkg.version ?? "0.0.0";
4414
- } catch {
4415
- return "0.0.0";
4416
- }
4417
- }
4418
5184
  function buildMeta(input) {
4419
5185
  return {
4420
- cliVersion: readCliVersion(),
5186
+ cliVersion: getCliVersion(),
4421
5187
  sourceId: input.sourceId,
4422
5188
  sourceHost: input.sourceHost,
4423
5189
  environment: input.environment
@@ -4492,7 +5258,9 @@ function cleanupPullResult(pullResult, keepFiles) {
4492
5258
  // src/commands/doctor.ts
4493
5259
  async function runDoctorCommand(options) {
4494
5260
  const logger = createCliLogger({ verbose: options.verbose });
5261
+ validateRuntimeOption2(options.runtime);
4495
5262
  validateCursorUsageCsvOptions2(options);
5263
+ validateHermesLocalOnly2(options);
4496
5264
  if (options.railway) {
4497
5265
  logger.verbose("Inspecting Railway audit readiness.");
4498
5266
  const railwayTarget = buildRailwayTarget2(options);
@@ -4529,14 +5297,17 @@ async function runDoctorCommand(options) {
4529
5297
  `);
4530
5298
  return;
4531
5299
  }
4532
- logger.verbose("Inspecting local OpenClaw audit readiness.");
5300
+ logger.verbose(
5301
+ options.runtime ? `Inspecting local ${options.runtime === "hermes" ? "Hermes" : "OpenClaw"} audit readiness.` : "Inspecting local runtime audit readiness."
5302
+ );
4533
5303
  if (options.logFile) {
4534
5304
  logger.verbose(`Using explicit local log file: ${options.logFile}`);
4535
5305
  }
4536
5306
  if (options.sessionsDir) {
4537
5307
  logger.verbose(`Using explicit local sessions directory: ${options.sessionsDir}`);
4538
5308
  }
4539
- const report = await doctorOpenClaw({
5309
+ const report = await doctorAgentRuntime({
5310
+ runtime: options.runtime ?? "auto",
4540
5311
  logFile: options.logFile,
4541
5312
  sessionsDir: options.sessionsDir,
4542
5313
  onProgress: logger.verbose
@@ -4544,11 +5315,22 @@ async function runDoctorCommand(options) {
4544
5315
  process.stdout.write(`${renderDoctorReport(report, { commandPrefix: options.commandPrefix })}
4545
5316
  `);
4546
5317
  }
5318
+ function validateRuntimeOption2(runtime) {
5319
+ if (!runtime) {
5320
+ return;
5321
+ }
5322
+ if (runtime !== "openclaw" && runtime !== "hermes") {
5323
+ throw new Error(
5324
+ `Unsupported runtime "${runtime}". Use --runtime openclaw or --runtime hermes.`
5325
+ );
5326
+ }
5327
+ }
4547
5328
  function validateCursorUsageCsvOptions2(options) {
4548
5329
  if (!options.cursorUsageCsv) {
4549
5330
  return;
4550
5331
  }
4551
5332
  const conflicts = [
5333
+ options.runtime ? "--runtime" : null,
4552
5334
  options.logFile ? "--log-file" : null,
4553
5335
  options.sessionsDir ? "--sessions-dir" : null,
4554
5336
  options.remote ? "--remote" : null,
@@ -4563,6 +5345,25 @@ function validateCursorUsageCsvOptions2(options) {
4563
5345
  throw new Error(`The --cursor-usage-csv flag cannot be combined with ${conflicts.join(", ")}.`);
4564
5346
  }
4565
5347
  }
5348
+ function validateHermesLocalOnly2(options) {
5349
+ if (options.runtime !== "hermes") {
5350
+ return;
5351
+ }
5352
+ const conflicts = [
5353
+ options.remote ? "--remote" : null,
5354
+ options.remoteLogFile ? "--remote-log-file" : null,
5355
+ options.remoteSessionsDir ? "--remote-sessions-dir" : null,
5356
+ options.railway ? "--railway" : null,
5357
+ options.railwayProject ? "--project" : null,
5358
+ options.railwayEnvironment ? "--environment" : null,
5359
+ options.railwayService ? "--service" : null
5360
+ ].filter((flag) => flag !== null);
5361
+ if (conflicts.length > 0) {
5362
+ throw new Error(
5363
+ `Hermes remote transport is not supported yet. Remove ${conflicts.join(", ")} or switch to --runtime openclaw.`
5364
+ );
5365
+ }
5366
+ }
4566
5367
  function buildRailwayTarget2(options) {
4567
5368
  if (options.railwayProject && options.railwayEnvironment && options.railwayService) {
4568
5369
  return {
@@ -4798,7 +5599,7 @@ function runLogoutCommand() {
4798
5599
  }
4799
5600
 
4800
5601
  // src/commands/push.ts
4801
- import { readFileSync as readFileSync7 } from "fs";
5602
+ import { readFileSync as readFileSync8 } from "fs";
4802
5603
  async function runPushCommand(options) {
4803
5604
  const payload = options.file ? loadPayloadFromFile(options.file) : loadPayloadFromCache();
4804
5605
  if (options.dryRun) {
@@ -4822,7 +5623,7 @@ async function runPushCommand(options) {
4822
5623
  function loadPayloadFromFile(filePath) {
4823
5624
  let raw;
4824
5625
  try {
4825
- raw = readFileSync7(filePath, "utf8");
5626
+ raw = readFileSync8(filePath, "utf8");
4826
5627
  } catch {
4827
5628
  throw new Error(`Cannot read file: ${filePath}`);
4828
5629
  }
@@ -4863,19 +5664,10 @@ function loadPayloadFromCache() {
4863
5664
  );
4864
5665
  return toWirePayload(latest, meta);
4865
5666
  }
4866
- function readCliVersion2() {
4867
- try {
4868
- const packageJsonPath = new URL("../../package.json", import.meta.url);
4869
- const pkg = JSON.parse(readFileSync7(packageJsonPath, "utf8"));
4870
- return pkg.version ?? "0.0.0";
4871
- } catch {
4872
- return "0.0.0";
4873
- }
4874
- }
4875
5667
  function buildMeta2(summary) {
4876
5668
  const sourceMeta = buildCachedPushSourceMeta(summary);
4877
5669
  return {
4878
- cliVersion: readCliVersion2(),
5670
+ cliVersion: getCliVersion(),
4879
5671
  sourceId: sourceMeta.sourceId,
4880
5672
  sourceHost: sourceMeta.sourceHost,
4881
5673
  environment: sourceMeta.environment
@@ -4886,14 +5678,14 @@ function buildMeta2(summary) {
4886
5678
  function renderRootHelp(version, display) {
4887
5679
  return `${display.name} ${version}
4888
5680
 
4889
- Waste intelligence for OpenClaw workflows and local Cursor usage CSVs.
5681
+ Waste intelligence for OpenClaw and Hermes workflows plus local Cursor usage CSVs.
4890
5682
 
4891
5683
  Usage:
4892
5684
  ${formatCommand("<command> [options]", display.prefix)}
4893
5685
 
4894
5686
  Commands:
4895
- audit Analyze OpenClaw logs or a local Cursor usage CSV.
4896
- doctor Inspect OpenClaw sources or a local Cursor usage CSV.
5687
+ audit Analyze OpenClaw or Hermes logs, or a local Cursor usage CSV.
5688
+ doctor Inspect OpenClaw or Hermes sources, or a local Cursor usage CSV.
4897
5689
  push Push a cached audit snapshot to the Xerg API.
4898
5690
  login Authenticate with the Xerg API via browser.
4899
5691
  logout Remove stored Xerg API credentials.
@@ -4906,14 +5698,15 @@ Global options:
4906
5698
  function renderAuditHelp(commandPrefix) {
4907
5699
  return `${formatCommand("audit", commandPrefix)}
4908
5700
 
4909
- Analyze OpenClaw logs or a local Cursor usage CSV and produce an audit report.
5701
+ Analyze OpenClaw or Hermes logs, or a local Cursor usage CSV, and produce an audit report.
4910
5702
 
4911
5703
  Usage:
4912
5704
  ${formatCommand("audit [options]", commandPrefix)}
4913
5705
 
4914
5706
  Options:
4915
- --log-file <path> Explicit OpenClaw gateway log file to analyze
4916
- --sessions-dir <path> Explicit OpenClaw sessions directory to analyze
5707
+ --runtime <name> Local runtime to inspect: openclaw or hermes
5708
+ --log-file <path> Explicit local gateway log file to analyze
5709
+ --sessions-dir <path> Explicit local sessions directory to analyze
4917
5710
  --cursor-usage-csv <path> Local Cursor usage CSV export to analyze
4918
5711
  --since <duration> Look back window such as 24h, 7d, or 30m
4919
5712
  --compare Compare this audit to the newest compatible prior local snapshot
@@ -4922,7 +5715,7 @@ Options:
4922
5715
  --db <path> Custom SQLite database path
4923
5716
  --no-db Skip local persistence
4924
5717
 
4925
- Remote options (SSH):
5718
+ Remote options (SSH, OpenClaw only):
4926
5719
  --remote <user@host> SSH target in user@host or user@host:port format
4927
5720
  --remote-log-file <path> Override the default gateway log path on the remote host
4928
5721
  --remote-sessions-dir <path> Override the default sessions directory on the remote host
@@ -4932,7 +5725,7 @@ Remote options (SSH):
4932
5725
  Prerequisites:
4933
5726
  SSH remote audits require ssh and rsync on your PATH.
4934
5727
 
4935
- Railway options:
5728
+ Railway options (OpenClaw only):
4936
5729
  --railway Audit a Railway service (uses linked project by default)
4937
5730
  --project <id> Railway project ID
4938
5731
  --environment <id> Railway environment ID
@@ -4975,25 +5768,26 @@ Authentication:
4975
5768
  function renderDoctorHelp(commandPrefix) {
4976
5769
  return `${formatCommand("doctor", commandPrefix)}
4977
5770
 
4978
- Inspect OpenClaw sources or a local Cursor usage CSV before you audit.
5771
+ Inspect OpenClaw or Hermes sources, or a local Cursor usage CSV, before you audit.
4979
5772
 
4980
5773
  Usage:
4981
5774
  ${formatCommand("doctor [options]", commandPrefix)}
4982
5775
 
4983
5776
  Options:
4984
- --log-file <path> Explicit OpenClaw gateway log file to inspect
4985
- --sessions-dir <path> Explicit OpenClaw sessions directory to inspect
5777
+ --runtime <name> Local runtime to inspect: openclaw or hermes
5778
+ --log-file <path> Explicit local gateway log file to inspect
5779
+ --sessions-dir <path> Explicit local sessions directory to inspect
4986
5780
  --cursor-usage-csv <path> Local Cursor usage CSV export to inspect
4987
5781
  --verbose Print progress updates to stderr while doctor runs
4988
5782
 
4989
- Remote options (SSH):
5783
+ Remote options (SSH, OpenClaw only):
4990
5784
  --remote <user@host> SSH target in user@host or user@host:port format
4991
5785
  --remote-log-file <path> Override the default gateway log path on the remote host
4992
5786
  --remote-sessions-dir <path> Override the default sessions directory on the remote host
4993
5787
 
4994
5788
  SSH checks require ssh and rsync on your PATH.
4995
5789
 
4996
- Railway options:
5790
+ Railway options (OpenClaw only):
4997
5791
  --railway Check a Railway service (uses linked project by default)
4998
5792
  --project <id> Railway project ID
4999
5793
  --environment <id> Railway environment ID
@@ -5006,7 +5800,7 @@ Railway options:
5006
5800
  }
5007
5801
 
5008
5802
  // src/index.ts
5009
- var VERSION = readVersion();
5803
+ var VERSION = getCliVersion();
5010
5804
  var argv = process.argv.slice(2);
5011
5805
  var commandDisplay = resolveCommandDisplay();
5012
5806
  var command = argv[0];
@@ -5077,6 +5871,10 @@ function parseAuditOptions(raw) {
5077
5871
  options.logFile = readValue(arg, argv2[index + 1]);
5078
5872
  index += 1;
5079
5873
  break;
5874
+ case "--runtime":
5875
+ options.runtime = readValue(arg, argv2[index + 1]);
5876
+ index += 1;
5877
+ break;
5080
5878
  case "--sessions-dir":
5081
5879
  options.sessionsDir = readValue(arg, argv2[index + 1]);
5082
5880
  index += 1;
@@ -5205,6 +6003,10 @@ function parseDoctorOptions(raw) {
5205
6003
  options.logFile = readValue(arg, argv2[index + 1]);
5206
6004
  index += 1;
5207
6005
  break;
6006
+ case "--runtime":
6007
+ options.runtime = readValue(arg, argv2[index + 1]);
6008
+ index += 1;
6009
+ break;
5208
6010
  case "--sessions-dir":
5209
6011
  options.sessionsDir = readValue(arg, argv2[index + 1]);
5210
6012
  index += 1;
@@ -5280,9 +6082,4 @@ function readFloat(flag, value) {
5280
6082
  function colorError(message) {
5281
6083
  return process.stderr.isTTY ? styleText2("red", message) : message;
5282
6084
  }
5283
- function readVersion() {
5284
- const packageJsonPath = new URL("../package.json", import.meta.url);
5285
- const packageJson = JSON.parse(readFileSync8(packageJsonPath, "utf8"));
5286
- return packageJson.version ?? "0.0.0";
5287
- }
5288
6085
  //# sourceMappingURL=index.js.map