@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/README.md +70 -39
- package/dist/index.js +2050 -747
- package/dist/index.js.map +1 -1
- package/package.json +16 -3
- package/skills/xerg/SKILL.md +50 -17
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
|
-
|
|
1257
|
-
|
|
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/
|
|
1862
|
-
function
|
|
1863
|
-
|
|
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
|
-
|
|
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
|
|
1883
|
-
|
|
1884
|
-
|
|
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
|
|
1893
|
-
|
|
1894
|
-
|
|
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
|
-
|
|
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
|
|
1899
|
-
if (
|
|
1900
|
-
return
|
|
1736
|
+
function reconcileDailyTotal(rows, key, expected) {
|
|
1737
|
+
if (rows.length === 0) {
|
|
1738
|
+
return;
|
|
1901
1739
|
}
|
|
1902
|
-
|
|
1903
|
-
|
|
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
|
-
|
|
1906
|
-
|
|
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
|
|
1911
|
-
const
|
|
1912
|
-
|
|
1913
|
-
|
|
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
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
const
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
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
|
|
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
|
-
) ??
|
|
2583
|
+
) ?? basename3(sourcePath, ".jsonl");
|
|
1954
2584
|
}
|
|
1955
|
-
function
|
|
2585
|
+
function inferEnvironment2(record) {
|
|
1956
2586
|
return asString(getNestedValue(record, [["environment"], ["env"], ["metadata", "environment"]])) ?? "local";
|
|
1957
2587
|
}
|
|
1958
|
-
function
|
|
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
|
|
2600
|
+
function inferTaskClass2(record, workflow) {
|
|
1971
2601
|
return asString(getNestedValue(record, [["task_class"], ["taskClass"], ["metadata", "taskClass"]])) ?? workflow.toLowerCase();
|
|
1972
2602
|
}
|
|
1973
|
-
function
|
|
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
|
|
2646
|
+
function buildCall3(source, record, runId, index) {
|
|
2017
2647
|
const provider = inferProvider2(record);
|
|
2018
2648
|
const model = inferModel(record);
|
|
2019
|
-
const workflow =
|
|
2020
|
-
const { inputTokens, outputTokens, observedCost } =
|
|
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:
|
|
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
|
|
2060
|
-
const hasUsage =
|
|
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 =
|
|
2697
|
+
const records = parseJsonLines2(source.path);
|
|
2068
2698
|
records.forEach((record, index) => {
|
|
2069
|
-
if (!
|
|
2699
|
+
if (!shouldTreatAsCall2(record)) {
|
|
2070
2700
|
return;
|
|
2071
2701
|
}
|
|
2072
|
-
const workflow =
|
|
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 =
|
|
2709
|
+
const runKey = inferRunKey2(record, workflow, index, source.path);
|
|
2080
2710
|
const runId = sha1(`${source.path}:${runKey}`);
|
|
2081
|
-
const call =
|
|
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:
|
|
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/
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
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
|
-
|
|
2784
|
+
};
|
|
2785
|
+
function getRuntimeAdapter(runtime) {
|
|
2786
|
+
return RUNTIME_ADAPTERS[runtime];
|
|
2124
2787
|
}
|
|
2125
|
-
function
|
|
2126
|
-
|
|
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
|
|
2131
|
-
const
|
|
2132
|
-
if (
|
|
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
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
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
|
|
2806
|
+
return hints.size === 1 ? Array.from(hints)[0] : null;
|
|
2143
2807
|
}
|
|
2144
|
-
function
|
|
2145
|
-
|
|
2146
|
-
|
|
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
|
|
2149
|
-
|
|
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
|
-
|
|
2156
|
-
|
|
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
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
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
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
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
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
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
|
|
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
|
|
2218
|
-
|
|
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
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
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
|
-
|
|
2251
|
-
|
|
2252
|
-
callCount: value.callCount,
|
|
2253
|
-
observedShare: Number(observedShare.toFixed(4))
|
|
2916
|
+
adapter,
|
|
2917
|
+
sources
|
|
2254
2918
|
};
|
|
2255
|
-
}
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
)
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
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
|
|
2380
|
-
|
|
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
|
|
3080
|
+
const sources = detectedSources ?? await adapter.detectSources(options);
|
|
2383
3081
|
if (sources.length === 0) {
|
|
2384
|
-
options.onProgress?.(
|
|
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?.(
|
|
2391
|
-
const runs =
|
|
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
|
|
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
|
-
`-
|
|
2716
|
-
`- Inspect
|
|
2717
|
-
`-
|
|
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
|
-
|
|
3438
|
+
status,
|
|
2724
3439
|
"",
|
|
2725
3440
|
"## Defaults",
|
|
2726
|
-
|
|
2727
|
-
|
|
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
|
|
3124
|
-
import { homedir as
|
|
3125
|
-
import { join as
|
|
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
|
|
3129
|
-
import { homedir as
|
|
3130
|
-
import { dirname as dirname2, join as
|
|
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 ||
|
|
3133
|
-
return
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
3208
|
-
import { join as
|
|
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(/^~/,
|
|
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(/^~/,
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3421
|
-
const sessionsDir =
|
|
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
|
|
3614
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3765
|
-
const sessionsDir =
|
|
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
|
|
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 =
|
|
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: `${
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
5375
|
+
|
|
5376
|
+
// src/cloud.ts
|
|
5377
|
+
function loadPushConfigOrNull() {
|
|
4410
5378
|
try {
|
|
4411
|
-
|
|
4412
|
-
const pkg = JSON.parse(readFileSync6(packageJsonPath, "utf8"));
|
|
4413
|
-
return pkg.version ?? "0.0.0";
|
|
5379
|
+
return loadPushConfig();
|
|
4414
5380
|
} catch {
|
|
4415
|
-
return
|
|
5381
|
+
return null;
|
|
4416
5382
|
}
|
|
4417
5383
|
}
|
|
4418
|
-
function
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
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
|
-
|
|
4427
|
-
|
|
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
|
-
|
|
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
|
|
4446
|
-
|
|
4447
|
-
|
|
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
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
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
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
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
|
-
|
|
4462
|
-
`);
|
|
5468
|
+
return payload;
|
|
4463
5469
|
}
|
|
4464
|
-
function
|
|
4465
|
-
const
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
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 (
|
|
4472
|
-
|
|
4473
|
-
`
|
|
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
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
${
|
|
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
|
|
4485
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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/
|
|
4677
|
-
import {
|
|
4678
|
-
|
|
4679
|
-
var
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
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
|
-
|
|
4701
|
-
}
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
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
|
-
|
|
4721
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
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
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
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/
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
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
|
|
4810
|
-
const
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
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
|
-
|
|
4823
|
-
|
|
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
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
`
|
|
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
|
-
|
|
4842
|
-
}
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
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
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
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
|
-
|
|
4859
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
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
|
|
6098
|
+
Waste intelligence for OpenClaw and Hermes workflows.
|
|
4890
6099
|
|
|
4891
6100
|
Usage:
|
|
4892
6101
|
${formatCommand("<command> [options]", display.prefix)}
|
|
4893
6102
|
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
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
|
-
--
|
|
4916
|
-
--
|
|
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
|
-
--
|
|
4985
|
-
--
|
|
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 =
|
|
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
|