@xerg/cli 0.2.0 → 0.4.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
- }
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);
2189
2870
  }
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);
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 [];
2220
- }
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;
2226
- }
2227
- byDay.set(day, (byDay.get(day) ?? 0) + attribution.wasteUsd);
2228
- }
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;
2900
+ function getRuntimeProductName(runtime) {
2901
+ return getRuntimeAdapter(runtime).productName;
2235
2902
  }
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);
2246
- }
2247
- return Array.from(buckets.entries()).map(([key, value]) => {
2248
- const observedShare = value.spendUsd === 0 ? 0 : value.observedSpendUsd / value.spendUsd;
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);
2249
2915
  return {
2250
- key,
2251
- spendUsd: Number(value.spendUsd.toFixed(6)),
2252
- callCount: value.callCount,
2253
- observedShare: Number(observedShare.toFixed(4))
2916
+ adapter,
2917
+ sources
2254
2918
  };
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
2919
+ }
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
+ }
2934
+ }
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
+ );
2947
+ }
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
2315
2961
  };
2316
2962
  }
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);
2973
+ }
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
+ }
2997
+ return {
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
+ ]
3007
+ };
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);
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,23 +3880,25 @@ 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;
3170
3887
  if (envKey) {
3171
3888
  return {
3172
3889
  apiKey: envKey,
3173
- apiUrl: envUrl || DEFAULT_API_URL
3890
+ apiUrl: envUrl || DEFAULT_API_URL,
3891
+ source: "env"
3174
3892
  };
3175
3893
  }
3176
3894
  try {
3177
- const raw = readFileSync4(CONFIG_PATH, "utf8");
3895
+ const raw = readFileSync5(CONFIG_PATH, "utf8");
3178
3896
  const parsed = JSON.parse(raw);
3179
3897
  if (parsed.apiKey) {
3180
3898
  return {
3181
3899
  apiKey: parsed.apiKey,
3182
- apiUrl: envUrl || parsed.apiUrl || DEFAULT_API_URL
3900
+ apiUrl: envUrl || parsed.apiUrl || DEFAULT_API_URL,
3901
+ source: "config"
3183
3902
  };
3184
3903
  }
3185
3904
  } catch {
@@ -3188,7 +3907,8 @@ function loadPushConfig() {
3188
3907
  if (storedToken) {
3189
3908
  return {
3190
3909
  apiKey: storedToken,
3191
- apiUrl: envUrl || DEFAULT_API_URL
3910
+ apiUrl: envUrl || DEFAULT_API_URL,
3911
+ source: "stored"
3192
3912
  };
3193
3913
  }
3194
3914
  throw new Error(
@@ -3204,8 +3924,8 @@ import { hostname } from "os";
3204
3924
  import { execSync, spawnSync } from "child_process";
3205
3925
  import { createHash as createHash2 } from "crypto";
3206
3926
  import { mkdirSync as mkdirSync4, rmSync as rmSync2 } from "fs";
3207
- import { homedir as homedir4, tmpdir } from "os";
3208
- import { join as join5 } from "path";
3927
+ import { homedir as homedir5, tmpdir } from "os";
3928
+ import { join as join6 } from "path";
3209
3929
  var DEFAULT_GATEWAY_DIR = "/tmp/openclaw";
3210
3930
  var DEFAULT_SESSIONS_DIR = "~/.openclaw/agents";
3211
3931
  function hashString(input) {
@@ -3214,7 +3934,7 @@ function hashString(input) {
3214
3934
  function sshArgs(source) {
3215
3935
  const args = [];
3216
3936
  if (source.identityFile) {
3217
- const resolved = source.identityFile.replace(/^~/, homedir4());
3937
+ const resolved = source.identityFile.replace(/^~/, homedir5());
3218
3938
  args.push("-i", resolved);
3219
3939
  }
3220
3940
  args.push("-o", "BatchMode=yes", "-o", "ConnectTimeout=10");
@@ -3223,7 +3943,7 @@ function sshArgs(source) {
3223
3943
  function rsyncSshCommand(source) {
3224
3944
  const parts = ["ssh"];
3225
3945
  if (source.identityFile) {
3226
- const resolved = source.identityFile.replace(/^~/, homedir4());
3946
+ const resolved = source.identityFile.replace(/^~/, homedir5());
3227
3947
  parts.push(`-i "${resolved}"`);
3228
3948
  }
3229
3949
  parts.push("-o BatchMode=yes", "-o ConnectTimeout=10");
@@ -3311,7 +4031,7 @@ function rsyncPull(opts) {
3311
4031
  if (status !== 0 || !stdout) return false;
3312
4032
  const files = stdout.split("\n").filter(Boolean);
3313
4033
  if (files.length === 0) return false;
3314
- const tmpFile = join5(tmpdir(), `xerg-filelist-${hashString(opts.remotePath)}`);
4034
+ const tmpFile = join6(tmpdir(), `xerg-filelist-${hashString(opts.remotePath)}`);
3315
4035
  const relativePaths = files.map(
3316
4036
  (f) => f.startsWith(opts.remotePath) ? f.slice(opts.remotePath.length).replace(/^\//, "") : f
3317
4037
  );
@@ -3360,12 +4080,12 @@ function pullDirectory(opts) {
3360
4080
  }
3361
4081
  function resolveLocalPath(source, keepFiles) {
3362
4082
  if (keepFiles) {
3363
- const cacheDir = join5(homedir4(), ".xerg", "remote-cache", source.name);
4083
+ const cacheDir = join6(homedir5(), ".xerg", "remote-cache", source.name);
3364
4084
  mkdirSync4(cacheDir, { recursive: true });
3365
4085
  return cacheDir;
3366
4086
  }
3367
4087
  const hash = hashString(`${source.host}:${Date.now()}`);
3368
- const tmpPath = join5(tmpdir(), `xerg-remote-${hash}`);
4088
+ const tmpPath = join6(tmpdir(), `xerg-remote-${hash}`);
3369
4089
  mkdirSync4(tmpPath, { recursive: true });
3370
4090
  return tmpPath;
3371
4091
  }
@@ -3417,8 +4137,8 @@ async function pullRemoteFiles(opts) {
3417
4137
  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
4138
  );
3419
4139
  const localBase = resolveLocalPath(source, keepFiles);
3420
- const gatewayDir = join5(localBase, "gateway");
3421
- const sessionsDir = join5(localBase, "sessions");
4140
+ const gatewayDir = join6(localBase, "gateway");
4141
+ const sessionsDir = join6(localBase, "sessions");
3422
4142
  const remoteLogPath = source.logFile ?? DEFAULT_GATEWAY_DIR;
3423
4143
  const remoteSessionsPath = source.sessionsDir ?? DEFAULT_SESSIONS_DIR;
3424
4144
  const { stdout: expandedSessions } = sshExec(source, `eval echo ${remoteSessionsPath}`);
@@ -3610,8 +4330,8 @@ function formatBytes(bytes) {
3610
4330
  import { execSync as execSync2, spawnSync as spawnSync2 } from "child_process";
3611
4331
  import { createHash as createHash3 } from "crypto";
3612
4332
  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";
4333
+ import { homedir as homedir6, tmpdir as tmpdir2 } from "os";
4334
+ import { join as join7 } from "path";
3615
4335
  var DEFAULT_GATEWAY_DIR2 = "/tmp/openclaw";
3616
4336
  var DEFAULT_SESSIONS_DIR2 = "~/.openclaw/agents";
3617
4337
  var ALTERNATE_SESSION_PATHS = ["/data/.clawdbot/agents/main/sessions"];
@@ -3684,13 +4404,13 @@ function tarRailwayPull(opts) {
3684
4404
  }
3685
4405
  function resolveLocalPath2(source, keepFiles) {
3686
4406
  if (keepFiles) {
3687
- const cacheDir = join6(homedir5(), ".xerg", "remote-cache", source.name);
4407
+ const cacheDir = join7(homedir6(), ".xerg", "remote-cache", source.name);
3688
4408
  mkdirSync5(cacheDir, { recursive: true });
3689
4409
  return cacheDir;
3690
4410
  }
3691
4411
  const identity = source.railway ? `railway:${source.railway.projectId}:${Date.now()}` : `${source.name}:${Date.now()}`;
3692
4412
  const hash = hashString2(identity);
3693
- const tmpPath = join6(tmpdir2(), `xerg-remote-${hash}`);
4413
+ const tmpPath = join7(tmpdir2(), `xerg-remote-${hash}`);
3694
4414
  mkdirSync5(tmpPath, { recursive: true });
3695
4415
  return tmpPath;
3696
4416
  }
@@ -3761,8 +4481,8 @@ async function pullRemoteFilesRailway(opts) {
3761
4481
  }
3762
4482
  onProgress?.("Railway service reachable.");
3763
4483
  const localBase = resolveLocalPath2(source, keepFiles);
3764
- const gatewayDir = join6(localBase, "gateway");
3765
- const sessionsDir = join6(localBase, "sessions");
4484
+ const gatewayDir = join7(localBase, "gateway");
4485
+ const sessionsDir = join7(localBase, "sessions");
3766
4486
  const remoteLogPath = source.logFile ?? DEFAULT_GATEWAY_DIR2;
3767
4487
  onProgress?.("Checking Railway default paths for gateway logs and sessions...");
3768
4488
  const logCheck = checkRemotePath(remoteLogPath, target);
@@ -3991,13 +4711,13 @@ function formatBytes2(bytes) {
3991
4711
  }
3992
4712
 
3993
4713
  // src/transport/config.ts
3994
- import { readFileSync as readFileSync5 } from "fs";
4714
+ import { readFileSync as readFileSync6 } from "fs";
3995
4715
  import { resolve as resolve3 } from "path";
3996
4716
  function loadRemoteConfig(configPath) {
3997
4717
  const resolved = resolve3(configPath);
3998
4718
  let raw;
3999
4719
  try {
4000
- raw = readFileSync5(resolved, "utf8");
4720
+ raw = readFileSync6(resolved, "utf8");
4001
4721
  } catch {
4002
4722
  throw new Error(`Cannot read remote config at ${resolved}`);
4003
4723
  }
@@ -4071,9 +4791,10 @@ function validateRailwayEntry(entry) {
4071
4791
  // src/source-meta.ts
4072
4792
  var RAILWAY_SOURCE_ID = "OpenClaw - Railway";
4073
4793
  function buildLocalPushSourceMeta(kind, localHost = hostname()) {
4794
+ const productName = kind === "cursor" ? "Cursor" : kind === "hermes" ? "Hermes" : "OpenClaw";
4074
4795
  return {
4075
4796
  environment: "local",
4076
- sourceId: `${kind === "cursor" ? "Cursor" : "OpenClaw"} - ${localHost}`,
4797
+ sourceId: `${productName} - ${localHost}`,
4077
4798
  sourceHost: localHost
4078
4799
  };
4079
4800
  }
@@ -4092,6 +4813,9 @@ function buildRemotePushSourceMeta(source) {
4092
4813
  };
4093
4814
  }
4094
4815
  function buildCachedPushSourceMeta(summary, localHost = hostname()) {
4816
+ if (summary.runtime === "cursor") {
4817
+ return buildLocalPushSourceMeta("cursor", localHost);
4818
+ }
4095
4819
  const sourceFiles = summary.sourceFiles ?? [];
4096
4820
  const comparisonKey = summary.comparisonKey ?? "";
4097
4821
  if (sourceFiles.some((sourceFile) => sourceFile.kind === "cursor-usage-csv")) {
@@ -4112,6 +4836,9 @@ function buildCachedPushSourceMeta(summary, localHost = hostname()) {
4112
4836
  sourceHost: remoteHost
4113
4837
  };
4114
4838
  }
4839
+ if (summary.runtime === "hermes") {
4840
+ return buildLocalPushSourceMeta("hermes", localHost);
4841
+ }
4115
4842
  return buildLocalPushSourceMeta("openclaw", localHost);
4116
4843
  }
4117
4844
  function isGeneratedRailwayName(name) {
@@ -4136,11 +4863,23 @@ function resolveRemoteHost(target) {
4136
4863
  return parsed.host || target;
4137
4864
  }
4138
4865
 
4866
+ // src/version.ts
4867
+ import { readFileSync as readFileSync7 } from "fs";
4868
+ function getCliVersion() {
4869
+ try {
4870
+ const packageJsonPath = new URL("../package.json", import.meta.url);
4871
+ const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf8"));
4872
+ return packageJson.version ?? "0.0.0";
4873
+ } catch {
4874
+ return "0.0.0";
4875
+ }
4876
+ }
4877
+
4139
4878
  // src/commands/audit.ts
4140
- var NO_DATA_PATTERN = /no openclaw sources were detected/i;
4879
+ var NO_DATA_PATTERN = /no (openclaw|hermes|supported local runtime) sources? were detected/i;
4141
4880
  async function auditOrNoData(...args) {
4142
4881
  try {
4143
- return await auditOpenClaw(...args);
4882
+ return await auditAgentRuntime(...args);
4144
4883
  } catch (err) {
4145
4884
  if (err instanceof Error && NO_DATA_PATTERN.test(err.message)) {
4146
4885
  throw new NoDataError(err.message);
@@ -4153,7 +4892,9 @@ async function runAuditCommand(options) {
4153
4892
  if (options.dryRun && !options.push) {
4154
4893
  throw new Error("--dry-run requires --push.");
4155
4894
  }
4895
+ validateRuntimeOption(options.runtime);
4156
4896
  validateCursorUsageCsvOptions(options);
4897
+ validateHermesLocalOnly(options);
4157
4898
  const remoteFlags = [options.remote, options.remoteConfig, options.railway].filter(
4158
4899
  Boolean
4159
4900
  ).length;
@@ -4218,7 +4959,9 @@ async function runLocalAudit(options, logger) {
4218
4959
  checkThresholds(summary2, options);
4219
4960
  return;
4220
4961
  }
4221
- logger.verbose("Running a local audit.");
4962
+ logger.verbose(
4963
+ options.runtime ? `Running a local ${options.runtime === "hermes" ? "Hermes" : "OpenClaw"} audit.` : "Running a local runtime audit with auto-detection."
4964
+ );
4222
4965
  if (options.logFile) {
4223
4966
  logger.verbose(`Using explicit local log file: ${options.logFile}`);
4224
4967
  }
@@ -4226,6 +4969,7 @@ async function runLocalAudit(options, logger) {
4226
4969
  logger.verbose(`Using explicit local sessions directory: ${options.sessionsDir}`);
4227
4970
  }
4228
4971
  const summary = await auditOrNoData({
4972
+ runtime: options.runtime ?? "auto",
4229
4973
  logFile: options.logFile,
4230
4974
  sessionsDir: options.sessionsDir,
4231
4975
  since: options.since,
@@ -4237,16 +4981,27 @@ async function runLocalAudit(options, logger) {
4237
4981
  });
4238
4982
  renderOutput(summary, options);
4239
4983
  if (options.push) {
4240
- const meta = buildMeta(buildLocalPushSourceMeta("openclaw"));
4984
+ const meta = buildMeta(buildLocalPushSourceMeta(summary.runtime));
4241
4985
  await handlePush(summary, meta, options);
4242
4986
  }
4243
4987
  checkThresholds(summary, options);
4244
4988
  }
4989
+ function validateRuntimeOption(runtime) {
4990
+ if (!runtime) {
4991
+ return;
4992
+ }
4993
+ if (runtime !== "openclaw" && runtime !== "hermes") {
4994
+ throw new Error(
4995
+ `Unsupported runtime "${runtime}". Use --runtime openclaw or --runtime hermes.`
4996
+ );
4997
+ }
4998
+ }
4245
4999
  function validateCursorUsageCsvOptions(options) {
4246
5000
  if (!options.cursorUsageCsv) {
4247
5001
  return;
4248
5002
  }
4249
5003
  const conflicts = [
5004
+ options.runtime ? "--runtime" : null,
4250
5005
  options.logFile ? "--log-file" : null,
4251
5006
  options.sessionsDir ? "--sessions-dir" : null,
4252
5007
  options.remote ? "--remote" : null,
@@ -4263,6 +5018,27 @@ function validateCursorUsageCsvOptions(options) {
4263
5018
  throw new Error(`The --cursor-usage-csv flag cannot be combined with ${conflicts.join(", ")}.`);
4264
5019
  }
4265
5020
  }
5021
+ function validateHermesLocalOnly(options) {
5022
+ if (options.runtime !== "hermes") {
5023
+ return;
5024
+ }
5025
+ const conflicts = [
5026
+ options.remote ? "--remote" : null,
5027
+ options.remoteLogFile ? "--remote-log-file" : null,
5028
+ options.remoteSessionsDir ? "--remote-sessions-dir" : null,
5029
+ options.remoteConfig ? "--remote-config" : null,
5030
+ options.keepRemoteFiles ? "--keep-remote-files" : null,
5031
+ options.railway ? "--railway" : null,
5032
+ options.railwayProject ? "--project" : null,
5033
+ options.railwayEnvironment ? "--environment" : null,
5034
+ options.railwayService ? "--service" : null
5035
+ ].filter((flag) => flag !== null);
5036
+ if (conflicts.length > 0) {
5037
+ throw new Error(
5038
+ `Hermes remote transport is not supported yet. Remove ${conflicts.join(", ")} or switch to --runtime openclaw.`
5039
+ );
5040
+ }
5041
+ }
4266
5042
  function getComparisonKey(source) {
4267
5043
  if (source.transport === "railway") {
4268
5044
  return buildComparisonKeyForRailway(source);
@@ -4293,6 +5069,7 @@ async function runSingleRemoteAudit(source, options, logger) {
4293
5069
  try {
4294
5070
  const comparisonKeyOverride = getComparisonKey(source);
4295
5071
  const summary = await auditOrNoData({
5072
+ runtime: "openclaw",
4296
5073
  logFile: pullResult.logFile,
4297
5074
  sessionsDir: pullResult.sessionsDir,
4298
5075
  since: options.since,
@@ -4343,6 +5120,7 @@ ${errorMessages}`);
4343
5120
  for (const { source, pullResult } of results) {
4344
5121
  const comparisonKeyOverride = getComparisonKey(source);
4345
5122
  const summary = await auditOrNoData({
5123
+ runtime: "openclaw",
4346
5124
  logFile: pullResult.logFile,
4347
5125
  sessionsDir: pullResult.sessionsDir,
4348
5126
  since: options.since,
@@ -4390,48 +5168,274 @@ ${"\u2550".repeat(60)}
4390
5168
  process.stderr.write(` ${source.name}: ${error}
4391
5169
  `);
4392
5170
  }
4393
- }
4394
- if (options.push) {
4395
- for (const { source, summary } of summaries) {
4396
- const meta = buildMeta(buildRemotePushSourceMeta(source));
4397
- await handlePush(summary, meta, options);
5171
+ }
5172
+ if (options.push) {
5173
+ for (const { source, summary } of summaries) {
5174
+ const meta = buildMeta(buildRemotePushSourceMeta(source));
5175
+ await handlePush(summary, meta, options);
5176
+ }
5177
+ }
5178
+ for (const { summary } of summaries) {
5179
+ checkThresholds(summary, options);
5180
+ }
5181
+ } finally {
5182
+ for (const { pullResult } of results) {
5183
+ cleanupPullResult(pullResult, options.keepRemoteFiles);
5184
+ }
5185
+ }
5186
+ }
5187
+ function buildMeta(input) {
5188
+ return {
5189
+ cliVersion: getCliVersion(),
5190
+ sourceId: input.sourceId,
5191
+ sourceHost: input.sourceHost,
5192
+ environment: input.environment
5193
+ };
5194
+ }
5195
+ async function handlePush(summary, meta, options) {
5196
+ const payload = toWirePayload(summary, meta);
5197
+ if (options.dryRun) {
5198
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
5199
+ `);
5200
+ return;
5201
+ }
5202
+ const config = loadPushConfig();
5203
+ process.stderr.write(`Pushing audit ${summary.auditId} to ${config.apiUrl}...
5204
+ `);
5205
+ const result = await pushAudit(payload, config);
5206
+ if (result.ok) {
5207
+ process.stderr.write(`Pushed successfully (audit: ${result.auditId}).
5208
+ `);
5209
+ } else {
5210
+ const statusInfo = result.status > 0 ? ` (HTTP ${result.status})` : "";
5211
+ throw new Error(`Push failed${statusInfo}: ${result.message}`);
5212
+ }
5213
+ }
5214
+ function renderOutput(summary, options) {
5215
+ if (options.push && options.dryRun) {
5216
+ return;
5217
+ }
5218
+ if (options.json) {
5219
+ const recommendations = buildRecommendations(summary);
5220
+ const output = { ...summary, recommendations };
5221
+ process.stdout.write(`${JSON.stringify(output, null, 2)}
5222
+ `);
5223
+ return;
5224
+ }
5225
+ if (options.markdown) {
5226
+ process.stdout.write(`${renderMarkdownSummary(summary)}
5227
+ `);
5228
+ return;
5229
+ }
5230
+ process.stdout.write(`${renderTerminalSummary(summary)}
5231
+ `);
5232
+ }
5233
+ function checkThresholds(summary, options) {
5234
+ const breaches = [];
5235
+ if (options.failAboveWasteRate !== void 0 && summary.structuralWasteRate > options.failAboveWasteRate) {
5236
+ breaches.push(
5237
+ `Structural waste rate ${(summary.structuralWasteRate * 100).toFixed(1)}% exceeds threshold ${(options.failAboveWasteRate * 100).toFixed(1)}%`
5238
+ );
5239
+ }
5240
+ if (options.failAboveWasteUsd !== void 0 && summary.wasteSpendUsd > options.failAboveWasteUsd) {
5241
+ breaches.push(
5242
+ `Waste spend $${summary.wasteSpendUsd.toFixed(2)} exceeds threshold $${options.failAboveWasteUsd.toFixed(2)}`
5243
+ );
5244
+ }
5245
+ if (breaches.length > 0) {
5246
+ process.stderr.write(`
5247
+ Threshold exceeded:
5248
+ ${breaches.map((b) => ` ${b}`).join("\n")}
5249
+ `);
5250
+ process.exitCode = 3;
5251
+ }
5252
+ }
5253
+ function cleanupPullResult(pullResult, keepFiles) {
5254
+ if (keepFiles) return;
5255
+ try {
5256
+ rmSync4(pullResult.localPath, { recursive: true, force: true });
5257
+ } catch {
5258
+ }
5259
+ }
5260
+
5261
+ // src/commands/login.ts
5262
+ import { styleText } from "util";
5263
+ var DEFAULT_AUTH_URL = "https://xerg.ai/dashboard/settings";
5264
+ var DEFAULT_API_URL2 = "https://api.xerg.ai";
5265
+ var POLL_INTERVAL_MS = 2e3;
5266
+ var POLL_TIMEOUT_MS = 3e5;
5267
+ async function runLoginCommand() {
5268
+ const existing = loadStoredCredentials();
5269
+ if (existing) {
5270
+ process.stderr.write(
5271
+ `Already logged in. Credentials stored at ${getCredentialsPath()}.
5272
+ Run ${colorBold(formatCommand("logout"))} first to re-authenticate.
5273
+ `
5274
+ );
5275
+ return;
5276
+ }
5277
+ const data = await performDeviceLogin();
5278
+ storeCredentials(data.token);
5279
+ const teamInfo = data.teamName ? ` (team: ${data.teamName})` : "";
5280
+ process.stderr.write(
5281
+ `
5282
+ ${colorSuccess("Authenticated successfully")}${teamInfo}.
5283
+ Credentials saved to ${getCredentialsPath()}.
5284
+ `
5285
+ );
5286
+ }
5287
+ async function performDeviceLogin() {
5288
+ const apiUrl = process.env.XERG_API_URL || DEFAULT_API_URL2;
5289
+ const deviceCodeUrl = `${apiUrl}/v1/auth/device-code`;
5290
+ let deviceResponse;
5291
+ try {
5292
+ const res = await fetch(deviceCodeUrl, { method: "POST" });
5293
+ if (!res.ok) {
5294
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
5295
+ }
5296
+ deviceResponse = await res.json();
5297
+ } catch (err) {
5298
+ const msg = err instanceof Error ? err.message : "Unknown error";
5299
+ throw new Error(
5300
+ `Could not start device auth flow (${msg}).
5301
+
5302
+ Alternative: create an API key at ${DEFAULT_AUTH_URL}
5303
+ and set XERG_API_KEY in your environment.`
5304
+ );
5305
+ }
5306
+ const verifyUrl = deviceResponse.verificationUrl || DEFAULT_AUTH_URL;
5307
+ const pollInterval = (deviceResponse.interval || 2) * 1e3;
5308
+ process.stderr.write(
5309
+ `
5310
+ Open this URL in your browser to authenticate:
5311
+
5312
+ ${colorBold(verifyUrl)}
5313
+
5314
+ `
5315
+ );
5316
+ if (deviceResponse.userCode) {
5317
+ process.stderr.write(`Your code: ${colorBold(deviceResponse.userCode)}
5318
+
5319
+ `);
5320
+ }
5321
+ process.stderr.write("Waiting for authentication...\n");
5322
+ await openBrowser(verifyUrl);
5323
+ const tokenUrl = `${apiUrl}/v1/auth/device-token`;
5324
+ const startTime = Date.now();
5325
+ while (Date.now() - startTime < POLL_TIMEOUT_MS) {
5326
+ await sleep(Math.max(pollInterval, POLL_INTERVAL_MS));
5327
+ try {
5328
+ const res = await fetch(tokenUrl, {
5329
+ method: "POST",
5330
+ headers: { "Content-Type": "application/json" },
5331
+ body: JSON.stringify({ deviceCode: deviceResponse.deviceCode })
5332
+ });
5333
+ if (res.status === 200) {
5334
+ return await res.json();
5335
+ }
5336
+ if (res.status === 428) {
5337
+ continue;
5338
+ }
5339
+ if (res.status === 410) {
5340
+ throw new Error(`Device code expired. Please run \`${formatCommand("login")}\` again.`);
5341
+ }
5342
+ const body = await res.json().catch(() => ({}));
5343
+ throw new Error(body.error || `Unexpected response: HTTP ${res.status}`);
5344
+ } catch (err) {
5345
+ if (err instanceof Error && (err.message.includes("expired") || err.message.includes("Unexpected"))) {
5346
+ throw err;
4398
5347
  }
4399
5348
  }
4400
- for (const { summary } of summaries) {
4401
- checkThresholds(summary, options);
4402
- }
4403
- } finally {
4404
- for (const { pullResult } of results) {
4405
- cleanupPullResult(pullResult, options.keepRemoteFiles);
4406
- }
4407
5349
  }
5350
+ throw new Error(`Authentication timed out. Please run \`${formatCommand("login")}\` again.`);
5351
+ }
5352
+ async function openBrowser(url) {
5353
+ const { exec } = await import("child_process");
5354
+ const { platform: platform2 } = await import("os");
5355
+ const commands = {
5356
+ darwin: "open",
5357
+ win32: "start",
5358
+ linux: "xdg-open"
5359
+ };
5360
+ const cmd = commands[platform2()];
5361
+ if (!cmd) return;
5362
+ return new Promise((resolve4) => {
5363
+ exec(`${cmd} ${JSON.stringify(url)}`, () => resolve4());
5364
+ });
5365
+ }
5366
+ function sleep(ms) {
5367
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
5368
+ }
5369
+ function colorBold(text) {
5370
+ return process.stderr.isTTY ? styleText("bold", text) : text;
5371
+ }
5372
+ function colorSuccess(text) {
5373
+ return process.stderr.isTTY ? styleText("green", text) : text;
4408
5374
  }
4409
- function readCliVersion() {
5375
+
5376
+ // src/cloud.ts
5377
+ function loadPushConfigOrNull() {
4410
5378
  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";
5379
+ return loadPushConfig();
4414
5380
  } catch {
4415
- return "0.0.0";
5381
+ return null;
4416
5382
  }
4417
5383
  }
4418
- function buildMeta(input) {
4419
- return {
4420
- cliVersion: readCliVersion(),
4421
- sourceId: input.sourceId,
4422
- sourceHost: input.sourceHost,
4423
- environment: input.environment
4424
- };
5384
+ async function authenticateAndLoadPushConfig() {
5385
+ const data = await performDeviceLogin();
5386
+ storeCredentials(data.token);
5387
+ const teamInfo = data.teamName ? ` (team: ${data.teamName})` : "";
5388
+ process.stderr.write(
5389
+ `
5390
+ Authenticated successfully${teamInfo}.
5391
+ Credentials saved to ${getCredentialsPath()}.
5392
+ `
5393
+ );
5394
+ return loadPushConfig();
4425
5395
  }
4426
- async function handlePush(summary, meta, options) {
4427
- const payload = toWirePayload(summary, meta);
5396
+ function renderCloudDisclaimer() {
5397
+ return [
5398
+ "Xerg Cloud sync and hosted MCP are optional paid workspace features.",
5399
+ "Local audits and compare stay free, and you can keep using Xerg locally if you skip this step."
5400
+ ].join("\n");
5401
+ }
5402
+ function renderMcpCredentialSourceMessage(config) {
5403
+ if (config.source === "stored") {
5404
+ return "Using your stored login token. If hosted MCP requires a workspace API key, create one at xerg.ai/dashboard/settings and set XERG_API_KEY.";
5405
+ }
5406
+ return "Using your workspace API key.";
5407
+ }
5408
+
5409
+ // src/prompts.ts
5410
+ import { confirm, select } from "@inquirer/prompts";
5411
+ function hasPromptTty() {
5412
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
5413
+ }
5414
+ async function promptConfirm(message, defaultValue = true) {
5415
+ return confirm({
5416
+ message,
5417
+ default: defaultValue
5418
+ });
5419
+ }
5420
+ async function promptSelect(message, choices) {
5421
+ return select({
5422
+ message,
5423
+ choices
5424
+ });
5425
+ }
5426
+
5427
+ // src/commands/push.ts
5428
+ import { readFileSync as readFileSync8 } from "fs";
5429
+ async function runPushCommand(options) {
5430
+ const payload = options.file ? loadPayloadFromFile(options.file) : loadLatestCachedAuditPayload();
4428
5431
  if (options.dryRun) {
4429
5432
  process.stdout.write(`${JSON.stringify(payload, null, 2)}
4430
5433
  `);
4431
5434
  return;
4432
5435
  }
4433
5436
  const config = loadPushConfig();
4434
- process.stderr.write(`Pushing audit ${summary.auditId} to ${config.apiUrl}...
5437
+ const auditId = payload.summary.auditId;
5438
+ process.stderr.write(`Pushing audit ${auditId} to ${config.apiUrl}...
4435
5439
  `);
4436
5440
  const result = await pushAudit(payload, config);
4437
5441
  if (result.ok) {
@@ -4442,57 +5446,166 @@ async function handlePush(summary, meta, options) {
4442
5446
  throw new Error(`Push failed${statusInfo}: ${result.message}`);
4443
5447
  }
4444
5448
  }
4445
- function renderOutput(summary, options) {
4446
- if (options.push && options.dryRun) {
4447
- return;
5449
+ function loadPayloadFromFile(filePath) {
5450
+ let raw;
5451
+ try {
5452
+ raw = readFileSync8(filePath, "utf8");
5453
+ } catch {
5454
+ throw new Error(`Cannot read file: ${filePath}`);
4448
5455
  }
4449
- if (options.json) {
4450
- const recommendations = buildRecommendations(summary);
4451
- const output = { ...summary, recommendations };
4452
- process.stdout.write(`${JSON.stringify(output, null, 2)}
4453
- `);
4454
- return;
5456
+ let parsed;
5457
+ try {
5458
+ parsed = JSON.parse(raw);
5459
+ } catch {
5460
+ throw new Error(`File is not valid JSON: ${filePath}`);
4455
5461
  }
4456
- if (options.markdown) {
4457
- process.stdout.write(`${renderMarkdownSummary(summary)}
4458
- `);
4459
- return;
5462
+ const payload = parsed;
5463
+ if (!payload.version || !payload.summary || !payload.meta) {
5464
+ throw new Error(
5465
+ `File does not look like an AuditPushPayload (missing version, summary, or meta): ${filePath}`
5466
+ );
4460
5467
  }
4461
- process.stdout.write(`${renderTerminalSummary(summary)}
4462
- `);
5468
+ return payload;
4463
5469
  }
4464
- function checkThresholds(summary, options) {
4465
- const breaches = [];
4466
- if (options.failAboveWasteRate !== void 0 && summary.structuralWasteRate > options.failAboveWasteRate) {
4467
- breaches.push(
4468
- `Structural waste rate ${(summary.structuralWasteRate * 100).toFixed(1)}% exceeds threshold ${(options.failAboveWasteRate * 100).toFixed(1)}%`
5470
+ function loadLatestCachedAuditPayload() {
5471
+ const dbPath = getDefaultDbPath();
5472
+ let summaries;
5473
+ try {
5474
+ summaries = listStoredAuditSummaries(dbPath);
5475
+ } catch {
5476
+ throw new NoDataError(
5477
+ `No local audit database found. Run \`${formatCommand("audit")}\` first, or use \`${formatCommand("push --file <path>")}\`.`
4469
5478
  );
4470
5479
  }
4471
- if (options.failAboveWasteUsd !== void 0 && summary.wasteSpendUsd > options.failAboveWasteUsd) {
4472
- breaches.push(
4473
- `Waste spend $${summary.wasteSpendUsd.toFixed(2)} exceeds threshold $${options.failAboveWasteUsd.toFixed(2)}`
5480
+ if (summaries.length === 0) {
5481
+ throw new NoDataError(
5482
+ `No cached audit snapshots found. Run \`${formatCommand("audit")}\` first, or use \`${formatCommand("push --file <path>")}\`.`
4474
5483
  );
4475
5484
  }
4476
- if (breaches.length > 0) {
4477
- process.stderr.write(`
4478
- Threshold exceeded:
4479
- ${breaches.map((b) => ` ${b}`).join("\n")}
5485
+ const latest = summaries[0];
5486
+ const meta = buildMeta2(latest);
5487
+ process.stderr.write(
5488
+ `Using most recent cached audit: ${latest.auditId} (${latest.generatedAt})
5489
+ `
5490
+ );
5491
+ return toWirePayload(latest, meta);
5492
+ }
5493
+ function buildMeta2(summary) {
5494
+ const sourceMeta = buildCachedPushSourceMeta(summary);
5495
+ return {
5496
+ cliVersion: getCliVersion(),
5497
+ sourceId: sourceMeta.sourceId,
5498
+ sourceHost: sourceMeta.sourceHost,
5499
+ environment: sourceMeta.environment
5500
+ };
5501
+ }
5502
+
5503
+ // src/commands/connect.ts
5504
+ async function runConnectCommand() {
5505
+ await runConnectFlow();
5506
+ }
5507
+ async function runConnectFlow(options) {
5508
+ if (!options?.skipDisclaimer) {
5509
+ process.stderr.write(`${renderCloudDisclaimer()}
4480
5510
  `);
4481
- process.exitCode = 3;
4482
5511
  }
5512
+ let config = loadPushConfigOrNull();
5513
+ if (config) {
5514
+ process.stderr.write("Xerg authentication detected.\n");
5515
+ } else {
5516
+ if (!hasPromptTty()) {
5517
+ process.stderr.write(
5518
+ `No Xerg authentication is configured, and ${formatCommand("connect")} needs an interactive terminal before it can start browser login.
5519
+ Run ${formatCommand("login")} from a TTY, or keep using local audits for free.
5520
+ `
5521
+ );
5522
+ process.exitCode = 1;
5523
+ return false;
5524
+ }
5525
+ const shouldLogin = await promptConfirm("Sign in to Xerg Cloud now?", true);
5526
+ if (!shouldLogin) {
5527
+ process.stderr.write(
5528
+ "Skipped Xerg Cloud setup. You can keep using local audits and compare without connecting.\n"
5529
+ );
5530
+ return false;
5531
+ }
5532
+ config = await authenticateAndLoadPushConfig();
5533
+ }
5534
+ if (!hasPromptTty()) {
5535
+ if (!options?.auditSummary) {
5536
+ process.stderr.write(
5537
+ `Non-interactive mode skips the push prompt. Run ${formatCommand("push")} when you want to sync a cached audit.
5538
+ `
5539
+ );
5540
+ } else {
5541
+ process.stderr.write(
5542
+ `Authentication is ready. Run ${formatCommand("push")} later if you want to sync this audit.
5543
+ `
5544
+ );
5545
+ }
5546
+ return true;
5547
+ }
5548
+ const shouldPush = await promptConfirm(
5549
+ options?.auditSummary ? "Push this audit to Xerg Cloud?" : "Push your latest cached audit to Xerg Cloud?",
5550
+ true
5551
+ );
5552
+ if (!shouldPush) {
5553
+ process.stderr.write(
5554
+ options?.auditSummary ? `Skipped push. Run ${formatCommand("push")} later if you want to sync a cached audit.
5555
+ ` : `Skipped push. Run ${formatCommand("push")} when you want to sync a cached audit.
5556
+ `
5557
+ );
5558
+ return true;
5559
+ }
5560
+ const payload = options?.auditSummary ? toWirePayload(options.auditSummary, buildLocalMeta(options.auditSummary)) : loadStandalonePayload();
5561
+ if (!payload) {
5562
+ return true;
5563
+ }
5564
+ await pushResolvedPayload(payload, config ?? loadPushConfig());
5565
+ return true;
4483
5566
  }
4484
- function cleanupPullResult(pullResult, keepFiles) {
4485
- if (keepFiles) return;
5567
+ function buildLocalMeta(summary) {
5568
+ const sourceMeta = buildLocalPushSourceMeta(summary.runtime);
5569
+ return {
5570
+ cliVersion: getCliVersion(),
5571
+ sourceId: sourceMeta.sourceId,
5572
+ sourceHost: sourceMeta.sourceHost,
5573
+ environment: sourceMeta.environment
5574
+ };
5575
+ }
5576
+ function loadStandalonePayload() {
4486
5577
  try {
4487
- rmSync4(pullResult.localPath, { recursive: true, force: true });
4488
- } catch {
5578
+ return loadLatestCachedAuditPayload();
5579
+ } catch (error) {
5580
+ if (error instanceof NoDataError || error instanceof Error && error.name === "NoDataError") {
5581
+ process.stderr.write(
5582
+ `${error instanceof Error ? error.message : "No cached audit snapshots found."}
5583
+ `
5584
+ );
5585
+ return null;
5586
+ }
5587
+ throw error;
5588
+ }
5589
+ }
5590
+ async function pushResolvedPayload(payload, config) {
5591
+ process.stderr.write(`Pushing audit ${payload.summary.auditId} to ${config.apiUrl}...
5592
+ `);
5593
+ const result = await pushAudit(payload, config);
5594
+ if (result.ok) {
5595
+ process.stderr.write(`Pushed successfully (audit: ${result.auditId}).
5596
+ `);
5597
+ return;
4489
5598
  }
5599
+ const statusInfo = result.status > 0 ? ` (HTTP ${result.status})` : "";
5600
+ throw new Error(`Push failed${statusInfo}: ${result.message}`);
4490
5601
  }
4491
5602
 
4492
5603
  // src/commands/doctor.ts
4493
5604
  async function runDoctorCommand(options) {
4494
5605
  const logger = createCliLogger({ verbose: options.verbose });
5606
+ validateRuntimeOption2(options.runtime);
4495
5607
  validateCursorUsageCsvOptions2(options);
5608
+ validateHermesLocalOnly2(options);
4496
5609
  if (options.railway) {
4497
5610
  logger.verbose("Inspecting Railway audit readiness.");
4498
5611
  const railwayTarget = buildRailwayTarget2(options);
@@ -4529,14 +5642,17 @@ async function runDoctorCommand(options) {
4529
5642
  `);
4530
5643
  return;
4531
5644
  }
4532
- logger.verbose("Inspecting local OpenClaw audit readiness.");
5645
+ logger.verbose(
5646
+ options.runtime ? `Inspecting local ${options.runtime === "hermes" ? "Hermes" : "OpenClaw"} audit readiness.` : "Inspecting local runtime audit readiness."
5647
+ );
4533
5648
  if (options.logFile) {
4534
5649
  logger.verbose(`Using explicit local log file: ${options.logFile}`);
4535
5650
  }
4536
5651
  if (options.sessionsDir) {
4537
5652
  logger.verbose(`Using explicit local sessions directory: ${options.sessionsDir}`);
4538
5653
  }
4539
- const report = await doctorOpenClaw({
5654
+ const report = await doctorAgentRuntime({
5655
+ runtime: options.runtime ?? "auto",
4540
5656
  logFile: options.logFile,
4541
5657
  sessionsDir: options.sessionsDir,
4542
5658
  onProgress: logger.verbose
@@ -4544,11 +5660,22 @@ async function runDoctorCommand(options) {
4544
5660
  process.stdout.write(`${renderDoctorReport(report, { commandPrefix: options.commandPrefix })}
4545
5661
  `);
4546
5662
  }
5663
+ function validateRuntimeOption2(runtime) {
5664
+ if (!runtime) {
5665
+ return;
5666
+ }
5667
+ if (runtime !== "openclaw" && runtime !== "hermes") {
5668
+ throw new Error(
5669
+ `Unsupported runtime "${runtime}". Use --runtime openclaw or --runtime hermes.`
5670
+ );
5671
+ }
5672
+ }
4547
5673
  function validateCursorUsageCsvOptions2(options) {
4548
5674
  if (!options.cursorUsageCsv) {
4549
5675
  return;
4550
5676
  }
4551
5677
  const conflicts = [
5678
+ options.runtime ? "--runtime" : null,
4552
5679
  options.logFile ? "--log-file" : null,
4553
5680
  options.sessionsDir ? "--sessions-dir" : null,
4554
5681
  options.remote ? "--remote" : null,
@@ -4563,6 +5690,25 @@ function validateCursorUsageCsvOptions2(options) {
4563
5690
  throw new Error(`The --cursor-usage-csv flag cannot be combined with ${conflicts.join(", ")}.`);
4564
5691
  }
4565
5692
  }
5693
+ function validateHermesLocalOnly2(options) {
5694
+ if (options.runtime !== "hermes") {
5695
+ return;
5696
+ }
5697
+ const conflicts = [
5698
+ options.remote ? "--remote" : null,
5699
+ options.remoteLogFile ? "--remote-log-file" : null,
5700
+ options.remoteSessionsDir ? "--remote-sessions-dir" : null,
5701
+ options.railway ? "--railway" : null,
5702
+ options.railwayProject ? "--project" : null,
5703
+ options.railwayEnvironment ? "--environment" : null,
5704
+ options.railwayService ? "--service" : null
5705
+ ].filter((flag) => flag !== null);
5706
+ if (conflicts.length > 0) {
5707
+ throw new Error(
5708
+ `Hermes remote transport is not supported yet. Remove ${conflicts.join(", ")} or switch to --runtime openclaw.`
5709
+ );
5710
+ }
5711
+ }
4566
5712
  function buildRailwayTarget2(options) {
4567
5713
  if (options.railwayProject && options.railwayEnvironment && options.railwayService) {
4568
5714
  return {
@@ -4673,247 +5819,335 @@ function renderRailwayDoctorReport(report) {
4673
5819
  return sections.join("\n");
4674
5820
  }
4675
5821
 
4676
- // src/commands/login.ts
4677
- import { styleText } from "util";
4678
- var DEFAULT_AUTH_URL = "https://xerg.ai/dashboard/settings";
4679
- var DEFAULT_API_URL2 = "https://api.xerg.ai";
4680
- var POLL_INTERVAL_MS = 2e3;
4681
- var POLL_TIMEOUT_MS = 3e5;
4682
- async function runLoginCommand() {
4683
- const existing = loadStoredCredentials();
4684
- if (existing) {
5822
+ // src/commands/mcp-setup.ts
5823
+ import { existsSync as existsSync2, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "fs";
5824
+ import { dirname as dirname3, join as join8 } from "path";
5825
+ var HOSTED_MCP_URL = "https://mcp.xerg.ai/mcp";
5826
+ async function runMcpSetupCommand() {
5827
+ await runMcpSetupFlow();
5828
+ }
5829
+ async function runMcpSetupFlow() {
5830
+ let config = loadPushConfigOrNull();
5831
+ if (!config) {
5832
+ process.stderr.write(`${renderCloudDisclaimer()}
5833
+ `);
5834
+ process.stderr.write("Hosted MCP requires Xerg Cloud authentication before client setup.\n");
5835
+ }
5836
+ if (!hasPromptTty()) {
4685
5837
  process.stderr.write(
4686
- `Already logged in. Credentials stored at ${getCredentialsPath()}.
4687
- Run ${colorBold(formatCommand("logout"))} first to re-authenticate.
5838
+ `${formatCommand("mcp-setup")} needs an interactive terminal so it can ask which MCP client you want to configure.
4688
5839
  `
4689
5840
  );
5841
+ process.exitCode = 1;
4690
5842
  return;
4691
5843
  }
4692
- const apiUrl = process.env.XERG_API_URL || DEFAULT_API_URL2;
4693
- const deviceCodeUrl = `${apiUrl}/v1/auth/device-code`;
4694
- let deviceResponse;
4695
- try {
4696
- const res = await fetch(deviceCodeUrl, { method: "POST" });
4697
- if (!res.ok) {
4698
- throw new Error(`HTTP ${res.status}: ${res.statusText}`);
5844
+ if (!config) {
5845
+ const shouldLogin = await promptConfirm("Authenticate with Xerg Cloud now?", true);
5846
+ if (!shouldLogin) {
5847
+ process.stderr.write(
5848
+ `Skipped hosted MCP setup. Run ${formatCommand("mcp-setup")} when you're ready.
5849
+ `
5850
+ );
5851
+ return;
4699
5852
  }
4700
- deviceResponse = await res.json();
4701
- } catch (err) {
4702
- const msg = err instanceof Error ? err.message : "Unknown error";
4703
- throw new Error(
4704
- `Could not start device auth flow (${msg}).
4705
-
4706
- Alternative: create an API key at ${DEFAULT_AUTH_URL}
4707
- and set XERG_API_KEY in your environment.`
5853
+ config = await authenticateAndLoadPushConfig();
5854
+ }
5855
+ process.stderr.write(`${renderMcpCredentialSourceMessage(config)}
5856
+ `);
5857
+ const client = await promptSelect("Which MCP client do you want to configure?", [
5858
+ {
5859
+ name: "Cursor",
5860
+ value: "cursor",
5861
+ description: "Project-scoped or global Cursor MCP config"
5862
+ },
5863
+ {
5864
+ name: "Claude Code",
5865
+ value: "claude-code",
5866
+ description: "Project-scoped Claude Code MCP config"
5867
+ },
5868
+ {
5869
+ name: "Other",
5870
+ value: "other",
5871
+ description: "Print the hosted HTTP MCP snippet for another client"
5872
+ }
5873
+ ]);
5874
+ const snippet = JSON.stringify(buildHostedMcpConfig(config), null, 2);
5875
+ if (client === "cursor") {
5876
+ await handleCursorSetup(snippet, config);
5877
+ return;
5878
+ }
5879
+ process.stdout.write(`${snippet}
5880
+ `);
5881
+ if (client === "claude-code") {
5882
+ process.stderr.write(
5883
+ "Add this to `.mcp.json` in your project root, or import the same `mcpServers.xerg` config through Claude Code MCP settings.\n"
4708
5884
  );
5885
+ return;
4709
5886
  }
4710
- const verifyUrl = deviceResponse.verificationUrl || DEFAULT_AUTH_URL;
4711
- const pollInterval = (deviceResponse.interval || 2) * 1e3;
4712
5887
  process.stderr.write(
4713
- `
4714
- Open this URL in your browser to authenticate:
4715
-
4716
- ${colorBold(verifyUrl)}
4717
-
5888
+ `Add this as a remote HTTP MCP server in your client. Endpoint: ${HOSTED_MCP_URL}
4718
5889
  `
4719
5890
  );
4720
- if (deviceResponse.userCode) {
4721
- process.stderr.write(`Your code: ${colorBold(deviceResponse.userCode)}
4722
-
5891
+ }
5892
+ async function handleCursorSetup(snippet, config) {
5893
+ const cursorDir = join8(process.cwd(), ".cursor");
5894
+ const cursorConfigPath = join8(cursorDir, "mcp.json");
5895
+ if (existsSync2(cursorDir)) {
5896
+ const shouldWrite = await promptConfirm(
5897
+ "Write a project-scoped Cursor MCP config to .cursor/mcp.json?",
5898
+ true
5899
+ );
5900
+ if (shouldWrite) {
5901
+ writeCursorConfig(cursorConfigPath, config);
5902
+ process.stderr.write(`Wrote hosted MCP config to ${cursorConfigPath}.
4723
5903
  `);
4724
- }
4725
- process.stderr.write("Waiting for authentication...\n");
4726
- await openBrowser(verifyUrl);
4727
- const tokenUrl = `${apiUrl}/v1/auth/device-token`;
4728
- const startTime = Date.now();
4729
- while (Date.now() - startTime < POLL_TIMEOUT_MS) {
4730
- await sleep(Math.max(pollInterval, POLL_INTERVAL_MS));
4731
- try {
4732
- const res = await fetch(tokenUrl, {
4733
- method: "POST",
4734
- headers: { "Content-Type": "application/json" },
4735
- body: JSON.stringify({ deviceCode: deviceResponse.deviceCode })
4736
- });
4737
- if (res.status === 200) {
4738
- const data = await res.json();
4739
- storeCredentials(data.token);
4740
- const teamInfo = data.teamName ? ` (team: ${data.teamName})` : "";
4741
- process.stderr.write(
4742
- `
4743
- ${colorSuccess("Authenticated successfully")}${teamInfo}.
4744
- Credentials saved to ${getCredentialsPath()}.
4745
- `
4746
- );
4747
- return;
4748
- }
4749
- if (res.status === 428) {
4750
- continue;
4751
- }
4752
- if (res.status === 410) {
4753
- throw new Error(`Device code expired. Please run \`${formatCommand("login")}\` again.`);
4754
- }
4755
- const body = await res.json().catch(() => ({}));
4756
- throw new Error(body.error || `Unexpected response: HTTP ${res.status}`);
4757
- } catch (err) {
4758
- if (err instanceof Error && (err.message.includes("expired") || err.message.includes("Unexpected"))) {
4759
- throw err;
4760
- }
5904
+ return;
4761
5905
  }
4762
5906
  }
4763
- throw new Error(`Authentication timed out. Please run \`${formatCommand("login")}\` again.`);
5907
+ process.stdout.write(`${snippet}
5908
+ `);
5909
+ process.stderr.write(
5910
+ "Add this to `.cursor/mcp.json` for a project-scoped Cursor config, or `~/.cursor/mcp.json` for a global Cursor config.\n"
5911
+ );
4764
5912
  }
4765
- async function openBrowser(url) {
4766
- const { exec } = await import("child_process");
4767
- const { platform: platform2 } = await import("os");
4768
- const commands = {
4769
- darwin: "open",
4770
- win32: "start",
4771
- linux: "xdg-open"
5913
+ function buildHostedMcpConfig(config) {
5914
+ return {
5915
+ mcpServers: {
5916
+ xerg: {
5917
+ type: "http",
5918
+ url: HOSTED_MCP_URL,
5919
+ headers: {
5920
+ Authorization: `Bearer ${config.apiKey}`
5921
+ }
5922
+ }
5923
+ }
4772
5924
  };
4773
- const cmd = commands[platform2()];
4774
- if (!cmd) return;
4775
- return new Promise((resolve4) => {
4776
- exec(`${cmd} ${JSON.stringify(url)}`, () => resolve4());
4777
- });
4778
- }
4779
- function sleep(ms) {
4780
- return new Promise((resolve4) => setTimeout(resolve4, ms));
4781
- }
4782
- function colorBold(text) {
4783
- return process.stderr.isTTY ? styleText("bold", text) : text;
4784
- }
4785
- function colorSuccess(text) {
4786
- return process.stderr.isTTY ? styleText("green", text) : text;
4787
5925
  }
4788
-
4789
- // src/commands/logout.ts
4790
- function runLogoutCommand() {
4791
- const removed = clearCredentials();
4792
- if (removed) {
4793
- process.stderr.write(`Credentials removed from ${getCredentialsPath()}.
4794
- `);
4795
- } else {
4796
- process.stderr.write("No stored credentials found. Already logged out.\n");
5926
+ function writeCursorConfig(filePath, config) {
5927
+ mkdirSync6(dirname3(filePath), { recursive: true });
5928
+ let parsed = {};
5929
+ if (existsSync2(filePath)) {
5930
+ try {
5931
+ parsed = JSON.parse(readFileSync9(filePath, "utf8"));
5932
+ } catch {
5933
+ throw new Error(`Cursor config is not valid JSON: ${filePath}`);
5934
+ }
5935
+ }
5936
+ const existingServers = parsed.mcpServers;
5937
+ if (existingServers && typeof existingServers !== "object") {
5938
+ throw new Error(`Cursor config has an invalid "mcpServers" value: ${filePath}`);
4797
5939
  }
5940
+ parsed.mcpServers = {
5941
+ ...existingServers ?? {},
5942
+ xerg: buildHostedMcpConfig(config).mcpServers.xerg
5943
+ };
5944
+ writeFileSync2(filePath, `${JSON.stringify(parsed, null, 2)}
5945
+ `);
4798
5946
  }
4799
5947
 
4800
- // src/commands/push.ts
4801
- import { readFileSync as readFileSync7 } from "fs";
4802
- async function runPushCommand(options) {
4803
- const payload = options.file ? loadPayloadFromFile(options.file) : loadPayloadFromCache();
4804
- if (options.dryRun) {
4805
- process.stdout.write(`${JSON.stringify(payload, null, 2)}
4806
- `);
5948
+ // src/commands/init.ts
5949
+ async function runInitCommand() {
5950
+ if (!hasPromptTty()) {
5951
+ process.stderr.write(
5952
+ `${formatCommand("init")} is interactive in this release. Run ${formatCommand("audit")} directly when you need a non-interactive audit.
5953
+ `
5954
+ );
5955
+ process.exitCode = 1;
4807
5956
  return;
4808
5957
  }
4809
- const config = loadPushConfig();
4810
- const auditId = payload.summary.auditId;
4811
- process.stderr.write(`Pushing audit ${auditId} to ${config.apiUrl}...
4812
- `);
4813
- const result = await pushAudit(payload, config);
4814
- if (result.ok) {
4815
- process.stderr.write(`Pushed successfully (audit: ${result.auditId}).
4816
- `);
4817
- } else {
4818
- const statusInfo = result.status > 0 ? ` (HTTP ${result.status})` : "";
4819
- throw new Error(`Push failed${statusInfo}: ${result.message}`);
5958
+ const candidates = await resolveRuntimeCandidates({ runtime: "auto" });
5959
+ const usable = candidates.filter((candidate) => candidate.usable);
5960
+ if (usable.length === 0) {
5961
+ renderNoDataGuidance();
5962
+ return;
4820
5963
  }
4821
- }
4822
- function loadPayloadFromFile(filePath) {
4823
- let raw;
4824
- try {
4825
- raw = readFileSync7(filePath, "utf8");
4826
- } catch {
4827
- throw new Error(`Cannot read file: ${filePath}`);
5964
+ const runtime = await chooseRuntime(usable);
5965
+ if (!runtime) {
5966
+ return;
4828
5967
  }
4829
- let parsed;
4830
5968
  try {
4831
- parsed = JSON.parse(raw);
4832
- } catch {
4833
- throw new Error(`File is not valid JSON: ${filePath}`);
4834
- }
4835
- const payload = parsed;
4836
- if (!payload.version || !payload.summary || !payload.meta) {
4837
- throw new Error(
4838
- `File does not look like an AuditPushPayload (missing version, summary, or meta): ${filePath}`
5969
+ const summary = await auditAgentRuntime({
5970
+ runtime,
5971
+ commandPrefix: formatCommand("")
5972
+ });
5973
+ process.stdout.write(`${renderTerminalSummary(summary)}
5974
+ `);
5975
+ process.stderr.write(
5976
+ `
5977
+ Next: after you make a fix, run ${formatCommand("audit --compare")} to measure the delta.
5978
+ `
4839
5979
  );
4840
- }
4841
- return payload;
4842
- }
4843
- function loadPayloadFromCache() {
4844
- const dbPath = getDefaultDbPath();
4845
- let summaries;
4846
- try {
4847
- summaries = listStoredAuditSummaries(dbPath);
4848
- } catch {
4849
- throw new NoDataError(
4850
- `No local audit database found. Run \`${formatCommand("audit")}\` first, or use \`${formatCommand("push --file <path>")}\`.`
5980
+ const existingAuth = loadPushConfigOrNull();
5981
+ process.stderr.write(
5982
+ `${existingAuth ? "Xerg Cloud authentication is already configured. You can optionally push this audit and set up hosted MCP next." : renderCloudDisclaimer()}
5983
+ `
5984
+ );
5985
+ const shouldConnect = await promptConfirm("Continue with optional Xerg Cloud setup?", true);
5986
+ if (!shouldConnect) {
5987
+ process.stderr.write(
5988
+ `Skipped Xerg Cloud setup. Run ${formatCommand("connect")} or ${formatCommand("mcp-setup")} whenever you want the hosted follow-up.
5989
+ `
5990
+ );
5991
+ return;
5992
+ }
5993
+ const connected = await runConnectFlow({
5994
+ skipDisclaimer: true,
5995
+ auditSummary: summary
5996
+ });
5997
+ if (!connected) {
5998
+ return;
5999
+ }
6000
+ const shouldSetupMcp = await promptConfirm("Set up hosted MCP now?", true);
6001
+ if (!shouldSetupMcp) {
6002
+ process.stderr.write(
6003
+ `Skipped hosted MCP setup. Run ${formatCommand("mcp-setup")} when you're ready.
6004
+ `
6005
+ );
6006
+ return;
6007
+ }
6008
+ await runMcpSetupFlow();
6009
+ } catch (error) {
6010
+ const message = error instanceof Error ? error.message : "Unknown error";
6011
+ const productName = getRuntimeAdapter(runtime).productName;
6012
+ process.stderr.write(
6013
+ `${[
6014
+ `${productName} audit failed: ${message}`,
6015
+ `Try ${formatCommand(["doctor", "--runtime", runtime])} to inspect the detected paths first.`,
6016
+ `Re-run ${formatCommand("audit --verbose")} for more detail.`
6017
+ ].join("\n")}
6018
+ `
4851
6019
  );
6020
+ process.exitCode = 1;
4852
6021
  }
4853
- if (summaries.length === 0) {
4854
- throw new NoDataError(
4855
- `No cached audit snapshots found. Run \`${formatCommand("audit")}\` first, or use \`${formatCommand("push --file <path>")}\`.`
6022
+ }
6023
+ async function chooseRuntime(candidates) {
6024
+ if (candidates.length === 1) {
6025
+ const candidate = candidates[0];
6026
+ process.stderr.write(`${describeCandidate(candidate)}
6027
+ `);
6028
+ const shouldAudit = await promptConfirm(
6029
+ `Run your first ${candidate.adapter.productName} audit now?`,
6030
+ true
4856
6031
  );
6032
+ if (!shouldAudit) {
6033
+ process.stderr.write(
6034
+ `Skipped the first audit. Run ${formatCommand(["audit", "--runtime", candidate.adapter.runtime])} when you're ready.
6035
+ `
6036
+ );
6037
+ return null;
6038
+ }
6039
+ return candidate.adapter.runtime;
4857
6040
  }
4858
- const latest = summaries[0];
4859
- const meta = buildMeta2(latest);
6041
+ return promptSelect("Choose the local runtime to audit first.", [
6042
+ ...candidates.map((candidate) => ({
6043
+ name: candidate.adapter.productName,
6044
+ value: candidate.adapter.runtime,
6045
+ description: describeSources(candidate)
6046
+ }))
6047
+ ]);
6048
+ }
6049
+ function describeCandidate(candidate) {
6050
+ return `Found local ${candidate.adapter.productName} data (${describeSources(candidate)}).`;
6051
+ }
6052
+ function describeSources(candidate) {
6053
+ const kinds = new Set(candidate.sources.map((source) => source.kind));
6054
+ const details = [
6055
+ kinds.has("gateway") ? "gateway logs" : null,
6056
+ kinds.has("sessions") ? "session transcripts" : null
6057
+ ].filter((detail) => detail !== null);
6058
+ return details.join(" and ");
6059
+ }
6060
+ function renderNoDataGuidance() {
6061
+ const openclawDefaults = getRuntimeAdapter("openclaw").defaultPaths();
6062
+ const hermesDefaults = getRuntimeAdapter("hermes").defaultPaths();
4860
6063
  process.stderr.write(
4861
- `Using most recent cached audit: ${latest.auditId} (${latest.generatedAt})
6064
+ `${[
6065
+ "No local OpenClaw or Hermes data was detected in the default locations Xerg checked.",
6066
+ "",
6067
+ "Checked defaults:",
6068
+ `- OpenClaw gateway logs: ${openclawDefaults.gatewayPattern}`,
6069
+ `- OpenClaw session transcripts: ${openclawDefaults.sessionsPattern}`,
6070
+ `- Hermes gateway logs: ${hermesDefaults.gatewayPattern}`,
6071
+ `- Hermes session transcripts: ${hermesDefaults.sessionsPattern}`,
6072
+ "",
6073
+ "Next steps:",
6074
+ `- Local OpenClaw paths: ${formatCommand("audit --runtime openclaw --log-file /path/to/openclaw.log")} or ${formatCommand("audit --runtime openclaw --sessions-dir /path/to/sessions")}`,
6075
+ `- Local Hermes paths: ${formatCommand("audit --runtime hermes --log-file ~/.hermes/logs/agent.log")} or ${formatCommand("audit --runtime hermes --sessions-dir ~/.hermes/sessions")}`,
6076
+ `- Remote OpenClaw only: ${formatCommand("audit --remote user@host")}`,
6077
+ `- Railway OpenClaw only: ${formatCommand("audit --railway")}`
6078
+ ].join("\n")}
4862
6079
  `
4863
6080
  );
4864
- return toWirePayload(latest, meta);
4865
6081
  }
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";
6082
+
6083
+ // src/commands/logout.ts
6084
+ function runLogoutCommand() {
6085
+ const removed = clearCredentials();
6086
+ if (removed) {
6087
+ process.stderr.write(`Credentials removed from ${getCredentialsPath()}.
6088
+ `);
6089
+ } else {
6090
+ process.stderr.write("No stored credentials found. Already logged out.\n");
4873
6091
  }
4874
6092
  }
4875
- function buildMeta2(summary) {
4876
- const sourceMeta = buildCachedPushSourceMeta(summary);
4877
- return {
4878
- cliVersion: readCliVersion2(),
4879
- sourceId: sourceMeta.sourceId,
4880
- sourceHost: sourceMeta.sourceHost,
4881
- environment: sourceMeta.environment
4882
- };
4883
- }
4884
6093
 
4885
6094
  // src/help.ts
4886
6095
  function renderRootHelp(version, display) {
4887
6096
  return `${display.name} ${version}
4888
6097
 
4889
- Waste intelligence for OpenClaw workflows and local Cursor usage CSVs.
6098
+ Waste intelligence for OpenClaw and Hermes workflows.
4890
6099
 
4891
6100
  Usage:
4892
6101
  ${formatCommand("<command> [options]", display.prefix)}
4893
6102
 
4894
- Commands:
4895
- audit Analyze OpenClaw logs or a local Cursor usage CSV.
4896
- doctor Inspect OpenClaw sources or a local Cursor usage CSV.
6103
+ Getting started:
6104
+ init Detect local runtimes, run a first audit, and offer optional cloud follow-up.
6105
+
6106
+ Audit and inspect:
6107
+ audit Analyze OpenClaw or Hermes logs, or a local Cursor usage CSV.
6108
+ doctor Inspect OpenClaw or Hermes sources, or a local Cursor usage CSV.
6109
+
6110
+ Cloud:
6111
+ connect Authenticate and optionally push your latest audit to Xerg Cloud.
4897
6112
  push Push a cached audit snapshot to the Xerg API.
4898
6113
  login Authenticate with the Xerg API via browser.
4899
6114
  logout Remove stored Xerg API credentials.
6115
+ mcp-setup Generate hosted MCP client configuration.
4900
6116
 
4901
6117
  Global options:
4902
6118
  -h, --help Show help
4903
6119
  -v, --version Show version
4904
6120
  `;
4905
6121
  }
6122
+ function renderInitHelp(commandPrefix) {
6123
+ return `${formatCommand("init", commandPrefix)}
6124
+
6125
+ Detect local OpenClaw or Hermes runtimes, run a first audit, and offer optional cloud follow-up.
6126
+
6127
+ Usage:
6128
+ ${formatCommand("init", commandPrefix)}
6129
+
6130
+ Notes:
6131
+ - Interactive only in v1
6132
+ - Uses local runtime auto-detection
6133
+ - Runs a first local audit with snapshot persistence enabled
6134
+ - Offers optional Xerg Cloud connect and hosted MCP setup after a successful audit
6135
+
6136
+ -h, --help Show help
6137
+ `;
6138
+ }
4906
6139
  function renderAuditHelp(commandPrefix) {
4907
6140
  return `${formatCommand("audit", commandPrefix)}
4908
6141
 
4909
- Analyze OpenClaw logs or a local Cursor usage CSV and produce an audit report.
6142
+ Analyze OpenClaw or Hermes logs, or a local Cursor usage CSV, and produce an audit report.
4910
6143
 
4911
6144
  Usage:
4912
6145
  ${formatCommand("audit [options]", commandPrefix)}
4913
6146
 
4914
6147
  Options:
4915
- --log-file <path> Explicit OpenClaw gateway log file to analyze
4916
- --sessions-dir <path> Explicit OpenClaw sessions directory to analyze
6148
+ --runtime <name> Local runtime to inspect: openclaw or hermes
6149
+ --log-file <path> Explicit local gateway log file to analyze
6150
+ --sessions-dir <path> Explicit local sessions directory to analyze
4917
6151
  --cursor-usage-csv <path> Local Cursor usage CSV export to analyze
4918
6152
  --since <duration> Look back window such as 24h, 7d, or 30m
4919
6153
  --compare Compare this audit to the newest compatible prior local snapshot
@@ -4922,7 +6156,7 @@ Options:
4922
6156
  --db <path> Custom SQLite database path
4923
6157
  --no-db Skip local persistence
4924
6158
 
4925
- Remote options (SSH):
6159
+ Remote options (SSH, OpenClaw only):
4926
6160
  --remote <user@host> SSH target in user@host or user@host:port format
4927
6161
  --remote-log-file <path> Override the default gateway log path on the remote host
4928
6162
  --remote-sessions-dir <path> Override the default sessions directory on the remote host
@@ -4932,7 +6166,7 @@ Remote options (SSH):
4932
6166
  Prerequisites:
4933
6167
  SSH remote audits require ssh and rsync on your PATH.
4934
6168
 
4935
- Railway options:
6169
+ Railway options (OpenClaw only):
4936
6170
  --railway Audit a Railway service (uses linked project by default)
4937
6171
  --project <id> Railway project ID
4938
6172
  --environment <id> Railway environment ID
@@ -4968,32 +6202,33 @@ Options:
4968
6202
 
4969
6203
  Authentication:
4970
6204
  Set XERG_API_KEY in your environment, add "apiKey" to ~/.xerg/config.json,
4971
- or run \`${formatCommand("login", commandPrefix)}\` to authenticate via browser.
6205
+ or run \`${formatCommand("connect", commandPrefix)}\` / \`${formatCommand("login", commandPrefix)}\` to authenticate via browser.
4972
6206
  Browser login stores a token at ~/.config/xerg/credentials.json by default.
4973
6207
  `;
4974
6208
  }
4975
6209
  function renderDoctorHelp(commandPrefix) {
4976
6210
  return `${formatCommand("doctor", commandPrefix)}
4977
6211
 
4978
- Inspect OpenClaw sources or a local Cursor usage CSV before you audit.
6212
+ Inspect OpenClaw or Hermes sources, or a local Cursor usage CSV, before you audit.
4979
6213
 
4980
6214
  Usage:
4981
6215
  ${formatCommand("doctor [options]", commandPrefix)}
4982
6216
 
4983
6217
  Options:
4984
- --log-file <path> Explicit OpenClaw gateway log file to inspect
4985
- --sessions-dir <path> Explicit OpenClaw sessions directory to inspect
6218
+ --runtime <name> Local runtime to inspect: openclaw or hermes
6219
+ --log-file <path> Explicit local gateway log file to inspect
6220
+ --sessions-dir <path> Explicit local sessions directory to inspect
4986
6221
  --cursor-usage-csv <path> Local Cursor usage CSV export to inspect
4987
6222
  --verbose Print progress updates to stderr while doctor runs
4988
6223
 
4989
- Remote options (SSH):
6224
+ Remote options (SSH, OpenClaw only):
4990
6225
  --remote <user@host> SSH target in user@host or user@host:port format
4991
6226
  --remote-log-file <path> Override the default gateway log path on the remote host
4992
6227
  --remote-sessions-dir <path> Override the default sessions directory on the remote host
4993
6228
 
4994
6229
  SSH checks require ssh and rsync on your PATH.
4995
6230
 
4996
- Railway options:
6231
+ Railway options (OpenClaw only):
4997
6232
  --railway Check a Railway service (uses linked project by default)
4998
6233
  --project <id> Railway project ID
4999
6234
  --environment <id> Railway environment ID
@@ -5004,9 +6239,43 @@ Railway options:
5004
6239
  -h, --help Show help
5005
6240
  `;
5006
6241
  }
6242
+ function renderConnectHelp(commandPrefix) {
6243
+ return `${formatCommand("connect", commandPrefix)}
6244
+
6245
+ Authenticate with Xerg Cloud and optionally push the latest audit.
6246
+
6247
+ Usage:
6248
+ ${formatCommand("connect", commandPrefix)}
6249
+
6250
+ Notes:
6251
+ - Shows paid-workspace disclosure before hosted setup
6252
+ - Reuses existing auth from XERG_API_KEY, ~/.xerg/config.json, or stored browser login
6253
+ - Standalone non-interactive mode reports auth status and skips the push prompt
6254
+ - When called after ${formatCommand("init", commandPrefix)}, it can push the in-memory audit directly
6255
+
6256
+ -h, --help Show help
6257
+ `;
6258
+ }
6259
+ function renderMcpSetupHelp(commandPrefix) {
6260
+ return `${formatCommand("mcp-setup", commandPrefix)}
6261
+
6262
+ Generate hosted MCP client configuration for Cursor, Claude Code, or another MCP client.
6263
+
6264
+ Usage:
6265
+ ${formatCommand("mcp-setup", commandPrefix)}
6266
+
6267
+ Notes:
6268
+ - Interactive in v1 because client selection is prompt-driven
6269
+ - Uses the hosted MCP endpoint at https://mcp.xerg.ai/mcp
6270
+ - Can write a project-scoped Cursor config when .cursor/ already exists
6271
+ - Local audits and compare stay available even if you skip hosted MCP setup
6272
+
6273
+ -h, --help Show help
6274
+ `;
6275
+ }
5007
6276
 
5008
6277
  // src/index.ts
5009
- var VERSION = readVersion();
6278
+ var VERSION = getCliVersion();
5010
6279
  var argv = process.argv.slice(2);
5011
6280
  var commandDisplay = resolveCommandDisplay();
5012
6281
  var command = argv[0];
@@ -5037,6 +6306,11 @@ async function run() {
5037
6306
  });
5038
6307
  return;
5039
6308
  }
6309
+ if (command === "init") {
6310
+ parseBareCommandOptions(argv.slice(1), renderInitHelp(commandDisplay.prefix), "init");
6311
+ await runInitCommand();
6312
+ return;
6313
+ }
5040
6314
  if (command === "doctor") {
5041
6315
  const options = parseDoctorOptions(argv.slice(1));
5042
6316
  await runDoctorCommand({
@@ -5050,6 +6324,11 @@ async function run() {
5050
6324
  await runPushCommand(options);
5051
6325
  return;
5052
6326
  }
6327
+ if (command === "connect") {
6328
+ parseBareCommandOptions(argv.slice(1), renderConnectHelp(commandDisplay.prefix), "connect");
6329
+ await runConnectCommand();
6330
+ return;
6331
+ }
5053
6332
  if (command === "login") {
5054
6333
  await runLoginCommand();
5055
6334
  return;
@@ -5058,10 +6337,31 @@ async function run() {
5058
6337
  runLogoutCommand();
5059
6338
  return;
5060
6339
  }
6340
+ if (command === "mcp-setup") {
6341
+ parseBareCommandOptions(argv.slice(1), renderMcpSetupHelp(commandDisplay.prefix), "mcp-setup");
6342
+ await runMcpSetupCommand();
6343
+ return;
6344
+ }
5061
6345
  throw new Error(
5062
6346
  `Unknown command "${command}". Run \`${formatCommand("--help", commandDisplay.prefix)}\` to see available commands.`
5063
6347
  );
5064
6348
  }
6349
+ function parseBareCommandOptions(raw, helpText, commandName) {
6350
+ const argv2 = expandEqualsArgs(raw);
6351
+ for (const arg of argv2) {
6352
+ switch (arg) {
6353
+ case "--help":
6354
+ case "-h":
6355
+ process.stdout.write(helpText);
6356
+ process.exit(0);
6357
+ break;
6358
+ default:
6359
+ throw new Error(
6360
+ `Unknown ${commandName} option "${arg}". Run \`${formatCommand([commandName, "--help"], commandDisplay.prefix)}\` for usage.`
6361
+ );
6362
+ }
6363
+ }
6364
+ }
5065
6365
  function parseAuditOptions(raw) {
5066
6366
  const argv2 = expandEqualsArgs(raw);
5067
6367
  const options = {};
@@ -5077,6 +6377,10 @@ function parseAuditOptions(raw) {
5077
6377
  options.logFile = readValue(arg, argv2[index + 1]);
5078
6378
  index += 1;
5079
6379
  break;
6380
+ case "--runtime":
6381
+ options.runtime = readValue(arg, argv2[index + 1]);
6382
+ index += 1;
6383
+ break;
5080
6384
  case "--sessions-dir":
5081
6385
  options.sessionsDir = readValue(arg, argv2[index + 1]);
5082
6386
  index += 1;
@@ -5205,6 +6509,10 @@ function parseDoctorOptions(raw) {
5205
6509
  options.logFile = readValue(arg, argv2[index + 1]);
5206
6510
  index += 1;
5207
6511
  break;
6512
+ case "--runtime":
6513
+ options.runtime = readValue(arg, argv2[index + 1]);
6514
+ index += 1;
6515
+ break;
5208
6516
  case "--sessions-dir":
5209
6517
  options.sessionsDir = readValue(arg, argv2[index + 1]);
5210
6518
  index += 1;
@@ -5280,9 +6588,4 @@ function readFloat(flag, value) {
5280
6588
  function colorError(message) {
5281
6589
  return process.stderr.isTTY ? styleText2("red", message) : message;
5282
6590
  }
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
6591
  //# sourceMappingURL=index.js.map