@xerg/cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -10
- package/dist/index.js +1328 -531
- package/dist/index.js.map +1 -1
- package/package.json +15 -3
- package/skills/xerg/SKILL.md +16 -7
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
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2829
|
+
function buildResolvedDoctorReport(adapter, sources) {
|
|
2830
|
+
return {
|
|
2831
|
+
canAudit: sources.length > 0,
|
|
2832
|
+
mode: sources.length > 0 ? "resolved" : "none",
|
|
2833
|
+
runtime: sources.length > 0 ? adapter.runtime : null,
|
|
2834
|
+
sources,
|
|
2835
|
+
defaults: [adapter.defaultPaths()],
|
|
2836
|
+
notes: buildResolvedDoctorNotes(adapter, sources)
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
function buildAutoNoDataDoctorReport(candidates) {
|
|
2840
|
+
return {
|
|
2841
|
+
canAudit: false,
|
|
2842
|
+
mode: "none",
|
|
2843
|
+
runtime: null,
|
|
2844
|
+
sources: candidates.flatMap((candidate) => candidate.sources),
|
|
2845
|
+
defaults: Object.values(RUNTIME_ADAPTERS).map((adapter) => adapter.defaultPaths()),
|
|
2846
|
+
notes: [
|
|
2847
|
+
"No supported local runtime sources were detected.",
|
|
2848
|
+
"Auto-detection checked both OpenClaw and Hermes local defaults.",
|
|
2849
|
+
"Use --runtime openclaw or --runtime hermes with --log-file / --sessions-dir when you want to point Xerg at explicit local paths."
|
|
2850
|
+
]
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
function buildAutoAmbiguousDoctorReport(candidates) {
|
|
2854
|
+
return {
|
|
2855
|
+
canAudit: false,
|
|
2856
|
+
mode: "ambiguous",
|
|
2857
|
+
runtime: null,
|
|
2858
|
+
sources: candidates.flatMap((candidate) => candidate.sources),
|
|
2859
|
+
defaults: Object.values(RUNTIME_ADAPTERS).map((adapter) => adapter.defaultPaths()),
|
|
2860
|
+
notes: [
|
|
2861
|
+
"Both OpenClaw and Hermes local sources were detected.",
|
|
2862
|
+
"Re-run doctor with --runtime openclaw or --runtime hermes to choose the local runtime explicitly."
|
|
2863
|
+
]
|
|
2864
|
+
};
|
|
2865
|
+
}
|
|
2866
|
+
function buildExplicitNoDataError(options, hintedRuntime) {
|
|
2867
|
+
const commandPrefix = options.commandPrefix ?? "xerg";
|
|
2868
|
+
if (hintedRuntime) {
|
|
2869
|
+
return getRuntimeAdapter(hintedRuntime).noDataError(commandPrefix);
|
|
2870
|
+
}
|
|
2871
|
+
return `No supported local runtime sources were detected. Run \`${commandPrefix} doctor\`, or use --runtime openclaw / --runtime hermes with --log-file / --sessions-dir.`;
|
|
2872
|
+
}
|
|
2873
|
+
function buildExplicitNoDataDoctorReport(candidates, hintedRuntime) {
|
|
2874
|
+
if (hintedRuntime) {
|
|
2875
|
+
const adapter = getRuntimeAdapter(hintedRuntime);
|
|
2194
2876
|
return {
|
|
2195
|
-
|
|
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
|
-
|
|
2900
|
+
function getRuntimeProductName(runtime) {
|
|
2901
|
+
return getRuntimeAdapter(runtime).productName;
|
|
2902
|
+
}
|
|
2903
|
+
async function resolveRuntimeCandidates(options) {
|
|
2904
|
+
return Promise.all(
|
|
2905
|
+
Object.values(RUNTIME_ADAPTERS).map(
|
|
2906
|
+
(adapter) => probeRuntimeCandidate(adapter, options)
|
|
2907
|
+
)
|
|
2908
|
+
);
|
|
2909
|
+
}
|
|
2910
|
+
async function resolveLocalAgentRuntime(options) {
|
|
2911
|
+
const requestedRuntime = options.runtime ?? "auto";
|
|
2912
|
+
if (requestedRuntime !== "auto") {
|
|
2913
|
+
const adapter = getRuntimeAdapter(requestedRuntime);
|
|
2914
|
+
const sources = await adapter.detectSources(options);
|
|
2915
|
+
return {
|
|
2916
|
+
adapter,
|
|
2917
|
+
sources
|
|
2918
|
+
};
|
|
2220
2919
|
}
|
|
2221
|
-
const
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2920
|
+
const candidates = await resolveRuntimeCandidates(options);
|
|
2921
|
+
const usableCandidates = candidates.filter((candidate) => candidate.usable);
|
|
2922
|
+
if (hasExplicitLocalPaths(options)) {
|
|
2923
|
+
const hintedRuntime = inferRuntimeFromExplicitPaths(options);
|
|
2924
|
+
if (hintedRuntime) {
|
|
2925
|
+
const hintedCandidate = usableCandidates.find(
|
|
2926
|
+
(candidate) => candidate.adapter.runtime === hintedRuntime
|
|
2927
|
+
);
|
|
2928
|
+
if (hintedCandidate) {
|
|
2929
|
+
return {
|
|
2930
|
+
adapter: hintedCandidate.adapter,
|
|
2931
|
+
sources: hintedCandidate.sources
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2226
2934
|
}
|
|
2227
|
-
|
|
2935
|
+
if (usableCandidates.length === 0) {
|
|
2936
|
+
throw new Error(buildExplicitNoDataError(options, hintedRuntime));
|
|
2937
|
+
}
|
|
2938
|
+
if (usableCandidates.length === 1) {
|
|
2939
|
+
return {
|
|
2940
|
+
adapter: usableCandidates[0].adapter,
|
|
2941
|
+
sources: usableCandidates[0].sources
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
throw new Error(
|
|
2945
|
+
"Could not determine whether the provided local files belong to OpenClaw or Hermes. Re-run with --runtime openclaw or --runtime hermes."
|
|
2946
|
+
);
|
|
2228
2947
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2948
|
+
if (usableCandidates.length === 0) {
|
|
2949
|
+
throw new Error(
|
|
2950
|
+
`No supported local runtime sources were detected. Run \`${options.commandPrefix ?? "xerg"} doctor\`, or use --runtime openclaw / --runtime hermes with --log-file / --sessions-dir.`
|
|
2951
|
+
);
|
|
2952
|
+
}
|
|
2953
|
+
if (usableCandidates.length > 1) {
|
|
2954
|
+
throw new Error(
|
|
2955
|
+
"Both OpenClaw and Hermes local sources were detected. Re-run with --runtime openclaw or --runtime hermes."
|
|
2956
|
+
);
|
|
2957
|
+
}
|
|
2958
|
+
return {
|
|
2959
|
+
adapter: usableCandidates[0].adapter,
|
|
2960
|
+
sources: usableCandidates[0].sources
|
|
2961
|
+
};
|
|
2235
2962
|
}
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
const
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2963
|
+
async function doctorAgentRuntime(options) {
|
|
2964
|
+
const requestedRuntime = options.runtime ?? "auto";
|
|
2965
|
+
if (requestedRuntime !== "auto") {
|
|
2966
|
+
options.onProgress?.(`Checking local ${getRuntimeProductName(requestedRuntime)} defaults...`);
|
|
2967
|
+
const adapter = getRuntimeAdapter(requestedRuntime);
|
|
2968
|
+
const sources = await adapter.detectSources(options);
|
|
2969
|
+
options.onProgress?.(
|
|
2970
|
+
sources.length > 0 ? `Detected ${sources.length} local source file${sources.length === 1 ? "" : "s"}.` : `No local ${adapter.productName} source files were detected.`
|
|
2971
|
+
);
|
|
2972
|
+
return buildResolvedDoctorReport(adapter, sources);
|
|
2246
2973
|
}
|
|
2247
|
-
|
|
2248
|
-
|
|
2974
|
+
options.onProgress?.("Checking local runtime defaults...");
|
|
2975
|
+
const candidates = await resolveRuntimeCandidates(options);
|
|
2976
|
+
const usableCandidates = candidates.filter((candidate) => candidate.usable);
|
|
2977
|
+
const detectedCount = candidates.reduce((sum, candidate) => sum + candidate.sources.length, 0);
|
|
2978
|
+
options.onProgress?.(
|
|
2979
|
+
detectedCount > 0 ? `Detected ${detectedCount} local source file${detectedCount === 1 ? "" : "s"} across supported runtimes.` : "No local runtime source files were detected."
|
|
2980
|
+
);
|
|
2981
|
+
if (hasExplicitLocalPaths(options)) {
|
|
2982
|
+
const hintedRuntime = inferRuntimeFromExplicitPaths(options);
|
|
2983
|
+
if (hintedRuntime) {
|
|
2984
|
+
const hintedCandidate = usableCandidates.find(
|
|
2985
|
+
(candidate) => candidate.adapter.runtime === hintedRuntime
|
|
2986
|
+
);
|
|
2987
|
+
if (hintedCandidate) {
|
|
2988
|
+
return buildResolvedDoctorReport(hintedCandidate.adapter, hintedCandidate.sources);
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
if (usableCandidates.length === 0) {
|
|
2992
|
+
return buildExplicitNoDataDoctorReport(candidates, hintedRuntime);
|
|
2993
|
+
}
|
|
2994
|
+
if (usableCandidates.length === 1) {
|
|
2995
|
+
return buildResolvedDoctorReport(usableCandidates[0].adapter, usableCandidates[0].sources);
|
|
2996
|
+
}
|
|
2249
2997
|
return {
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2998
|
+
canAudit: false,
|
|
2999
|
+
mode: "ambiguous",
|
|
3000
|
+
runtime: null,
|
|
3001
|
+
sources: candidates.flatMap((candidate) => candidate.sources),
|
|
3002
|
+
defaults: Object.values(RUNTIME_ADAPTERS).map((adapter) => adapter.defaultPaths()),
|
|
3003
|
+
notes: [
|
|
3004
|
+
"Could not determine whether the provided local files belong to OpenClaw or Hermes.",
|
|
3005
|
+
"Re-run doctor with --runtime openclaw or --runtime hermes to choose the local runtime explicitly."
|
|
3006
|
+
]
|
|
2254
3007
|
};
|
|
2255
|
-
}
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
const opportunitySpendUsd = input.findings.filter((finding) => finding.classification === "opportunity").reduce((sum, finding) => sum + finding.costImpactUsd, 0);
|
|
2264
|
-
const generatedAt = isoNow();
|
|
2265
|
-
const spendByDay = buildSpendByDay(input.runs);
|
|
2266
|
-
const observedDays = buildObservedUtcDayRange(input.runs);
|
|
2267
|
-
return {
|
|
2268
|
-
auditId: sha1(
|
|
2269
|
-
`${generatedAt}:${input.runs.length}:${input.sources.map((source) => source.path).join("|")}`
|
|
2270
|
-
),
|
|
2271
|
-
generatedAt,
|
|
2272
|
-
comparisonKey: input.comparisonKeyOverride ?? buildComparisonKey({
|
|
2273
|
-
sources: input.sources,
|
|
2274
|
-
since: input.since
|
|
2275
|
-
}),
|
|
2276
|
-
comparison: null,
|
|
2277
|
-
since: input.since,
|
|
2278
|
-
runCount: input.runs.length,
|
|
2279
|
-
callCount,
|
|
2280
|
-
totalSpendUsd: Number(totalSpendUsd.toFixed(6)),
|
|
2281
|
-
observedSpendUsd: Number(observedSpendUsd.toFixed(6)),
|
|
2282
|
-
estimatedSpendUsd: Number(estimatedSpendUsd.toFixed(6)),
|
|
2283
|
-
wasteSpendUsd: Number(wasteSpendUsd.toFixed(6)),
|
|
2284
|
-
opportunitySpendUsd: Number(opportunitySpendUsd.toFixed(6)),
|
|
2285
|
-
structuralWasteRate: Number(
|
|
2286
|
-
(totalSpendUsd === 0 ? 0 : wasteSpendUsd / totalSpendUsd).toFixed(4)
|
|
2287
|
-
),
|
|
2288
|
-
wasteByKind: buildTaxonomyBuckets(input.findings, "waste"),
|
|
2289
|
-
opportunityByKind: buildTaxonomyBuckets(input.findings, "opportunity"),
|
|
2290
|
-
spendByWorkflow: buildBreakdown(
|
|
2291
|
-
input.runs.map((run2) => ({
|
|
2292
|
-
key: run2.workflow,
|
|
2293
|
-
spendUsd: run2.totalCostUsd,
|
|
2294
|
-
observedSpendUsd: run2.observedCostUsd
|
|
2295
|
-
}))
|
|
2296
|
-
),
|
|
2297
|
-
spendByModel: buildBreakdown(
|
|
2298
|
-
input.runs.flatMap(
|
|
2299
|
-
(run2) => run2.calls.map((call) => ({
|
|
2300
|
-
key: `${call.provider}/${call.model}`,
|
|
2301
|
-
spendUsd: call.costUsd,
|
|
2302
|
-
observedSpendUsd: call.costSource === "observed" ? call.costUsd : 0
|
|
2303
|
-
}))
|
|
2304
|
-
)
|
|
2305
|
-
),
|
|
2306
|
-
spendByDay,
|
|
2307
|
-
wasteByDay: buildWasteByDay(input.wasteAttributions, observedDays, wasteSpendUsd),
|
|
2308
|
-
findings: input.findings,
|
|
2309
|
-
notes: [
|
|
2310
|
-
"Cost per outcome is intentionally unavailable in v0. Xerg is measuring waste intelligence only.",
|
|
2311
|
-
"Opportunity findings are directional recommendations, not proven waste."
|
|
2312
|
-
],
|
|
2313
|
-
sourceFiles: input.sources,
|
|
2314
|
-
dbPath: input.dbPath
|
|
2315
|
-
};
|
|
3008
|
+
}
|
|
3009
|
+
if (usableCandidates.length === 0) {
|
|
3010
|
+
return buildAutoNoDataDoctorReport(candidates);
|
|
3011
|
+
}
|
|
3012
|
+
if (usableCandidates.length > 1) {
|
|
3013
|
+
return buildAutoAmbiguousDoctorReport(usableCandidates);
|
|
3014
|
+
}
|
|
3015
|
+
return buildResolvedDoctorReport(usableCandidates[0].adapter, usableCandidates[0].sources);
|
|
2316
3016
|
}
|
|
2317
3017
|
|
|
2318
3018
|
// ../core/src/audit.ts
|
|
2319
|
-
async function doctorOpenClaw(options) {
|
|
2320
|
-
return inspectOpenClawSources(options);
|
|
2321
|
-
}
|
|
2322
3019
|
async function doctorCursorUsageCsv(options) {
|
|
2323
3020
|
return inspectCursorUsageCsv(options);
|
|
2324
3021
|
}
|
|
@@ -2376,25 +3073,25 @@ function hasPricingCoverageChange(current, baseline) {
|
|
|
2376
3073
|
}
|
|
2377
3074
|
return (current?.pricedCallCount ?? 0) !== (baseline?.pricedCallCount ?? 0) || (current?.unpricedCallCount ?? 0) !== (baseline?.unpricedCallCount ?? 0) || (current?.pricedTokenCount ?? 0) !== (baseline?.pricedTokenCount ?? 0) || (current?.unpricedTokenCount ?? 0) !== (baseline?.unpricedTokenCount ?? 0);
|
|
2378
3075
|
}
|
|
2379
|
-
async function
|
|
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,7 +3880,7 @@ function clearCredentials() {
|
|
|
3163
3880
|
|
|
3164
3881
|
// src/push/config.ts
|
|
3165
3882
|
var DEFAULT_API_URL = "https://api.xerg.ai";
|
|
3166
|
-
var CONFIG_PATH =
|
|
3883
|
+
var CONFIG_PATH = join5(homedir4(), ".xerg", "config.json");
|
|
3167
3884
|
function loadPushConfig() {
|
|
3168
3885
|
const envKey = process.env.XERG_API_KEY;
|
|
3169
3886
|
const envUrl = process.env.XERG_API_URL;
|
|
@@ -3174,7 +3891,7 @@ function loadPushConfig() {
|
|
|
3174
3891
|
};
|
|
3175
3892
|
}
|
|
3176
3893
|
try {
|
|
3177
|
-
const raw =
|
|
3894
|
+
const raw = readFileSync5(CONFIG_PATH, "utf8");
|
|
3178
3895
|
const parsed = JSON.parse(raw);
|
|
3179
3896
|
if (parsed.apiKey) {
|
|
3180
3897
|
return {
|
|
@@ -3204,8 +3921,8 @@ import { hostname } from "os";
|
|
|
3204
3921
|
import { execSync, spawnSync } from "child_process";
|
|
3205
3922
|
import { createHash as createHash2 } from "crypto";
|
|
3206
3923
|
import { mkdirSync as mkdirSync4, rmSync as rmSync2 } from "fs";
|
|
3207
|
-
import { homedir as
|
|
3208
|
-
import { join as
|
|
3924
|
+
import { homedir as homedir5, tmpdir } from "os";
|
|
3925
|
+
import { join as join6 } from "path";
|
|
3209
3926
|
var DEFAULT_GATEWAY_DIR = "/tmp/openclaw";
|
|
3210
3927
|
var DEFAULT_SESSIONS_DIR = "~/.openclaw/agents";
|
|
3211
3928
|
function hashString(input) {
|
|
@@ -3214,7 +3931,7 @@ function hashString(input) {
|
|
|
3214
3931
|
function sshArgs(source) {
|
|
3215
3932
|
const args = [];
|
|
3216
3933
|
if (source.identityFile) {
|
|
3217
|
-
const resolved = source.identityFile.replace(/^~/,
|
|
3934
|
+
const resolved = source.identityFile.replace(/^~/, homedir5());
|
|
3218
3935
|
args.push("-i", resolved);
|
|
3219
3936
|
}
|
|
3220
3937
|
args.push("-o", "BatchMode=yes", "-o", "ConnectTimeout=10");
|
|
@@ -3223,7 +3940,7 @@ function sshArgs(source) {
|
|
|
3223
3940
|
function rsyncSshCommand(source) {
|
|
3224
3941
|
const parts = ["ssh"];
|
|
3225
3942
|
if (source.identityFile) {
|
|
3226
|
-
const resolved = source.identityFile.replace(/^~/,
|
|
3943
|
+
const resolved = source.identityFile.replace(/^~/, homedir5());
|
|
3227
3944
|
parts.push(`-i "${resolved}"`);
|
|
3228
3945
|
}
|
|
3229
3946
|
parts.push("-o BatchMode=yes", "-o ConnectTimeout=10");
|
|
@@ -3311,7 +4028,7 @@ function rsyncPull(opts) {
|
|
|
3311
4028
|
if (status !== 0 || !stdout) return false;
|
|
3312
4029
|
const files = stdout.split("\n").filter(Boolean);
|
|
3313
4030
|
if (files.length === 0) return false;
|
|
3314
|
-
const tmpFile =
|
|
4031
|
+
const tmpFile = join6(tmpdir(), `xerg-filelist-${hashString(opts.remotePath)}`);
|
|
3315
4032
|
const relativePaths = files.map(
|
|
3316
4033
|
(f) => f.startsWith(opts.remotePath) ? f.slice(opts.remotePath.length).replace(/^\//, "") : f
|
|
3317
4034
|
);
|
|
@@ -3360,12 +4077,12 @@ function pullDirectory(opts) {
|
|
|
3360
4077
|
}
|
|
3361
4078
|
function resolveLocalPath(source, keepFiles) {
|
|
3362
4079
|
if (keepFiles) {
|
|
3363
|
-
const cacheDir =
|
|
4080
|
+
const cacheDir = join6(homedir5(), ".xerg", "remote-cache", source.name);
|
|
3364
4081
|
mkdirSync4(cacheDir, { recursive: true });
|
|
3365
4082
|
return cacheDir;
|
|
3366
4083
|
}
|
|
3367
4084
|
const hash = hashString(`${source.host}:${Date.now()}`);
|
|
3368
|
-
const tmpPath =
|
|
4085
|
+
const tmpPath = join6(tmpdir(), `xerg-remote-${hash}`);
|
|
3369
4086
|
mkdirSync4(tmpPath, { recursive: true });
|
|
3370
4087
|
return tmpPath;
|
|
3371
4088
|
}
|
|
@@ -3417,8 +4134,8 @@ async function pullRemoteFiles(opts) {
|
|
|
3417
4134
|
useRsync ? "Local rsync detected. Xerg will prefer rsync and fall back to tar over SSH if needed." : "Local rsync not detected. Xerg will pull files with tar over SSH."
|
|
3418
4135
|
);
|
|
3419
4136
|
const localBase = resolveLocalPath(source, keepFiles);
|
|
3420
|
-
const gatewayDir =
|
|
3421
|
-
const sessionsDir =
|
|
4137
|
+
const gatewayDir = join6(localBase, "gateway");
|
|
4138
|
+
const sessionsDir = join6(localBase, "sessions");
|
|
3422
4139
|
const remoteLogPath = source.logFile ?? DEFAULT_GATEWAY_DIR;
|
|
3423
4140
|
const remoteSessionsPath = source.sessionsDir ?? DEFAULT_SESSIONS_DIR;
|
|
3424
4141
|
const { stdout: expandedSessions } = sshExec(source, `eval echo ${remoteSessionsPath}`);
|
|
@@ -3610,8 +4327,8 @@ function formatBytes(bytes) {
|
|
|
3610
4327
|
import { execSync as execSync2, spawnSync as spawnSync2 } from "child_process";
|
|
3611
4328
|
import { createHash as createHash3 } from "crypto";
|
|
3612
4329
|
import { mkdirSync as mkdirSync5, rmSync as rmSync3 } from "fs";
|
|
3613
|
-
import { homedir as
|
|
3614
|
-
import { join as
|
|
4330
|
+
import { homedir as homedir6, tmpdir as tmpdir2 } from "os";
|
|
4331
|
+
import { join as join7 } from "path";
|
|
3615
4332
|
var DEFAULT_GATEWAY_DIR2 = "/tmp/openclaw";
|
|
3616
4333
|
var DEFAULT_SESSIONS_DIR2 = "~/.openclaw/agents";
|
|
3617
4334
|
var ALTERNATE_SESSION_PATHS = ["/data/.clawdbot/agents/main/sessions"];
|
|
@@ -3684,13 +4401,13 @@ function tarRailwayPull(opts) {
|
|
|
3684
4401
|
}
|
|
3685
4402
|
function resolveLocalPath2(source, keepFiles) {
|
|
3686
4403
|
if (keepFiles) {
|
|
3687
|
-
const cacheDir =
|
|
4404
|
+
const cacheDir = join7(homedir6(), ".xerg", "remote-cache", source.name);
|
|
3688
4405
|
mkdirSync5(cacheDir, { recursive: true });
|
|
3689
4406
|
return cacheDir;
|
|
3690
4407
|
}
|
|
3691
4408
|
const identity = source.railway ? `railway:${source.railway.projectId}:${Date.now()}` : `${source.name}:${Date.now()}`;
|
|
3692
4409
|
const hash = hashString2(identity);
|
|
3693
|
-
const tmpPath =
|
|
4410
|
+
const tmpPath = join7(tmpdir2(), `xerg-remote-${hash}`);
|
|
3694
4411
|
mkdirSync5(tmpPath, { recursive: true });
|
|
3695
4412
|
return tmpPath;
|
|
3696
4413
|
}
|
|
@@ -3761,8 +4478,8 @@ async function pullRemoteFilesRailway(opts) {
|
|
|
3761
4478
|
}
|
|
3762
4479
|
onProgress?.("Railway service reachable.");
|
|
3763
4480
|
const localBase = resolveLocalPath2(source, keepFiles);
|
|
3764
|
-
const gatewayDir =
|
|
3765
|
-
const sessionsDir =
|
|
4481
|
+
const gatewayDir = join7(localBase, "gateway");
|
|
4482
|
+
const sessionsDir = join7(localBase, "sessions");
|
|
3766
4483
|
const remoteLogPath = source.logFile ?? DEFAULT_GATEWAY_DIR2;
|
|
3767
4484
|
onProgress?.("Checking Railway default paths for gateway logs and sessions...");
|
|
3768
4485
|
const logCheck = checkRemotePath(remoteLogPath, target);
|
|
@@ -3991,13 +4708,13 @@ function formatBytes2(bytes) {
|
|
|
3991
4708
|
}
|
|
3992
4709
|
|
|
3993
4710
|
// src/transport/config.ts
|
|
3994
|
-
import { readFileSync as
|
|
4711
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
3995
4712
|
import { resolve as resolve3 } from "path";
|
|
3996
4713
|
function loadRemoteConfig(configPath) {
|
|
3997
4714
|
const resolved = resolve3(configPath);
|
|
3998
4715
|
let raw;
|
|
3999
4716
|
try {
|
|
4000
|
-
raw =
|
|
4717
|
+
raw = readFileSync6(resolved, "utf8");
|
|
4001
4718
|
} catch {
|
|
4002
4719
|
throw new Error(`Cannot read remote config at ${resolved}`);
|
|
4003
4720
|
}
|
|
@@ -4071,9 +4788,10 @@ function validateRailwayEntry(entry) {
|
|
|
4071
4788
|
// src/source-meta.ts
|
|
4072
4789
|
var RAILWAY_SOURCE_ID = "OpenClaw - Railway";
|
|
4073
4790
|
function buildLocalPushSourceMeta(kind, localHost = hostname()) {
|
|
4791
|
+
const productName = kind === "cursor" ? "Cursor" : kind === "hermes" ? "Hermes" : "OpenClaw";
|
|
4074
4792
|
return {
|
|
4075
4793
|
environment: "local",
|
|
4076
|
-
sourceId: `${
|
|
4794
|
+
sourceId: `${productName} - ${localHost}`,
|
|
4077
4795
|
sourceHost: localHost
|
|
4078
4796
|
};
|
|
4079
4797
|
}
|
|
@@ -4092,6 +4810,9 @@ function buildRemotePushSourceMeta(source) {
|
|
|
4092
4810
|
};
|
|
4093
4811
|
}
|
|
4094
4812
|
function buildCachedPushSourceMeta(summary, localHost = hostname()) {
|
|
4813
|
+
if (summary.runtime === "cursor") {
|
|
4814
|
+
return buildLocalPushSourceMeta("cursor", localHost);
|
|
4815
|
+
}
|
|
4095
4816
|
const sourceFiles = summary.sourceFiles ?? [];
|
|
4096
4817
|
const comparisonKey = summary.comparisonKey ?? "";
|
|
4097
4818
|
if (sourceFiles.some((sourceFile) => sourceFile.kind === "cursor-usage-csv")) {
|
|
@@ -4112,6 +4833,9 @@ function buildCachedPushSourceMeta(summary, localHost = hostname()) {
|
|
|
4112
4833
|
sourceHost: remoteHost
|
|
4113
4834
|
};
|
|
4114
4835
|
}
|
|
4836
|
+
if (summary.runtime === "hermes") {
|
|
4837
|
+
return buildLocalPushSourceMeta("hermes", localHost);
|
|
4838
|
+
}
|
|
4115
4839
|
return buildLocalPushSourceMeta("openclaw", localHost);
|
|
4116
4840
|
}
|
|
4117
4841
|
function isGeneratedRailwayName(name) {
|
|
@@ -4136,11 +4860,23 @@ function resolveRemoteHost(target) {
|
|
|
4136
4860
|
return parsed.host || target;
|
|
4137
4861
|
}
|
|
4138
4862
|
|
|
4863
|
+
// src/version.ts
|
|
4864
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
4865
|
+
function getCliVersion() {
|
|
4866
|
+
try {
|
|
4867
|
+
const packageJsonPath = new URL("../package.json", import.meta.url);
|
|
4868
|
+
const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf8"));
|
|
4869
|
+
return packageJson.version ?? "0.0.0";
|
|
4870
|
+
} catch {
|
|
4871
|
+
return "0.0.0";
|
|
4872
|
+
}
|
|
4873
|
+
}
|
|
4874
|
+
|
|
4139
4875
|
// src/commands/audit.ts
|
|
4140
|
-
var NO_DATA_PATTERN = /no openclaw sources were detected/i;
|
|
4876
|
+
var NO_DATA_PATTERN = /no (openclaw|hermes|supported local runtime) sources? were detected/i;
|
|
4141
4877
|
async function auditOrNoData(...args) {
|
|
4142
4878
|
try {
|
|
4143
|
-
return await
|
|
4879
|
+
return await auditAgentRuntime(...args);
|
|
4144
4880
|
} catch (err) {
|
|
4145
4881
|
if (err instanceof Error && NO_DATA_PATTERN.test(err.message)) {
|
|
4146
4882
|
throw new NoDataError(err.message);
|
|
@@ -4153,7 +4889,9 @@ async function runAuditCommand(options) {
|
|
|
4153
4889
|
if (options.dryRun && !options.push) {
|
|
4154
4890
|
throw new Error("--dry-run requires --push.");
|
|
4155
4891
|
}
|
|
4892
|
+
validateRuntimeOption(options.runtime);
|
|
4156
4893
|
validateCursorUsageCsvOptions(options);
|
|
4894
|
+
validateHermesLocalOnly(options);
|
|
4157
4895
|
const remoteFlags = [options.remote, options.remoteConfig, options.railway].filter(
|
|
4158
4896
|
Boolean
|
|
4159
4897
|
).length;
|
|
@@ -4218,7 +4956,9 @@ async function runLocalAudit(options, logger) {
|
|
|
4218
4956
|
checkThresholds(summary2, options);
|
|
4219
4957
|
return;
|
|
4220
4958
|
}
|
|
4221
|
-
logger.verbose(
|
|
4959
|
+
logger.verbose(
|
|
4960
|
+
options.runtime ? `Running a local ${options.runtime === "hermes" ? "Hermes" : "OpenClaw"} audit.` : "Running a local runtime audit with auto-detection."
|
|
4961
|
+
);
|
|
4222
4962
|
if (options.logFile) {
|
|
4223
4963
|
logger.verbose(`Using explicit local log file: ${options.logFile}`);
|
|
4224
4964
|
}
|
|
@@ -4226,6 +4966,7 @@ async function runLocalAudit(options, logger) {
|
|
|
4226
4966
|
logger.verbose(`Using explicit local sessions directory: ${options.sessionsDir}`);
|
|
4227
4967
|
}
|
|
4228
4968
|
const summary = await auditOrNoData({
|
|
4969
|
+
runtime: options.runtime ?? "auto",
|
|
4229
4970
|
logFile: options.logFile,
|
|
4230
4971
|
sessionsDir: options.sessionsDir,
|
|
4231
4972
|
since: options.since,
|
|
@@ -4237,16 +4978,27 @@ async function runLocalAudit(options, logger) {
|
|
|
4237
4978
|
});
|
|
4238
4979
|
renderOutput(summary, options);
|
|
4239
4980
|
if (options.push) {
|
|
4240
|
-
const meta = buildMeta(buildLocalPushSourceMeta(
|
|
4981
|
+
const meta = buildMeta(buildLocalPushSourceMeta(summary.runtime));
|
|
4241
4982
|
await handlePush(summary, meta, options);
|
|
4242
4983
|
}
|
|
4243
4984
|
checkThresholds(summary, options);
|
|
4244
4985
|
}
|
|
4986
|
+
function validateRuntimeOption(runtime) {
|
|
4987
|
+
if (!runtime) {
|
|
4988
|
+
return;
|
|
4989
|
+
}
|
|
4990
|
+
if (runtime !== "openclaw" && runtime !== "hermes") {
|
|
4991
|
+
throw new Error(
|
|
4992
|
+
`Unsupported runtime "${runtime}". Use --runtime openclaw or --runtime hermes.`
|
|
4993
|
+
);
|
|
4994
|
+
}
|
|
4995
|
+
}
|
|
4245
4996
|
function validateCursorUsageCsvOptions(options) {
|
|
4246
4997
|
if (!options.cursorUsageCsv) {
|
|
4247
4998
|
return;
|
|
4248
4999
|
}
|
|
4249
5000
|
const conflicts = [
|
|
5001
|
+
options.runtime ? "--runtime" : null,
|
|
4250
5002
|
options.logFile ? "--log-file" : null,
|
|
4251
5003
|
options.sessionsDir ? "--sessions-dir" : null,
|
|
4252
5004
|
options.remote ? "--remote" : null,
|
|
@@ -4263,6 +5015,27 @@ function validateCursorUsageCsvOptions(options) {
|
|
|
4263
5015
|
throw new Error(`The --cursor-usage-csv flag cannot be combined with ${conflicts.join(", ")}.`);
|
|
4264
5016
|
}
|
|
4265
5017
|
}
|
|
5018
|
+
function validateHermesLocalOnly(options) {
|
|
5019
|
+
if (options.runtime !== "hermes") {
|
|
5020
|
+
return;
|
|
5021
|
+
}
|
|
5022
|
+
const conflicts = [
|
|
5023
|
+
options.remote ? "--remote" : null,
|
|
5024
|
+
options.remoteLogFile ? "--remote-log-file" : null,
|
|
5025
|
+
options.remoteSessionsDir ? "--remote-sessions-dir" : null,
|
|
5026
|
+
options.remoteConfig ? "--remote-config" : null,
|
|
5027
|
+
options.keepRemoteFiles ? "--keep-remote-files" : null,
|
|
5028
|
+
options.railway ? "--railway" : null,
|
|
5029
|
+
options.railwayProject ? "--project" : null,
|
|
5030
|
+
options.railwayEnvironment ? "--environment" : null,
|
|
5031
|
+
options.railwayService ? "--service" : null
|
|
5032
|
+
].filter((flag) => flag !== null);
|
|
5033
|
+
if (conflicts.length > 0) {
|
|
5034
|
+
throw new Error(
|
|
5035
|
+
`Hermes remote transport is not supported yet. Remove ${conflicts.join(", ")} or switch to --runtime openclaw.`
|
|
5036
|
+
);
|
|
5037
|
+
}
|
|
5038
|
+
}
|
|
4266
5039
|
function getComparisonKey(source) {
|
|
4267
5040
|
if (source.transport === "railway") {
|
|
4268
5041
|
return buildComparisonKeyForRailway(source);
|
|
@@ -4293,6 +5066,7 @@ async function runSingleRemoteAudit(source, options, logger) {
|
|
|
4293
5066
|
try {
|
|
4294
5067
|
const comparisonKeyOverride = getComparisonKey(source);
|
|
4295
5068
|
const summary = await auditOrNoData({
|
|
5069
|
+
runtime: "openclaw",
|
|
4296
5070
|
logFile: pullResult.logFile,
|
|
4297
5071
|
sessionsDir: pullResult.sessionsDir,
|
|
4298
5072
|
since: options.since,
|
|
@@ -4343,6 +5117,7 @@ ${errorMessages}`);
|
|
|
4343
5117
|
for (const { source, pullResult } of results) {
|
|
4344
5118
|
const comparisonKeyOverride = getComparisonKey(source);
|
|
4345
5119
|
const summary = await auditOrNoData({
|
|
5120
|
+
runtime: "openclaw",
|
|
4346
5121
|
logFile: pullResult.logFile,
|
|
4347
5122
|
sessionsDir: pullResult.sessionsDir,
|
|
4348
5123
|
since: options.since,
|
|
@@ -4406,18 +5181,9 @@ ${"\u2550".repeat(60)}
|
|
|
4406
5181
|
}
|
|
4407
5182
|
}
|
|
4408
5183
|
}
|
|
4409
|
-
function readCliVersion() {
|
|
4410
|
-
try {
|
|
4411
|
-
const packageJsonPath = new URL("../../package.json", import.meta.url);
|
|
4412
|
-
const pkg = JSON.parse(readFileSync6(packageJsonPath, "utf8"));
|
|
4413
|
-
return pkg.version ?? "0.0.0";
|
|
4414
|
-
} catch {
|
|
4415
|
-
return "0.0.0";
|
|
4416
|
-
}
|
|
4417
|
-
}
|
|
4418
5184
|
function buildMeta(input) {
|
|
4419
5185
|
return {
|
|
4420
|
-
cliVersion:
|
|
5186
|
+
cliVersion: getCliVersion(),
|
|
4421
5187
|
sourceId: input.sourceId,
|
|
4422
5188
|
sourceHost: input.sourceHost,
|
|
4423
5189
|
environment: input.environment
|
|
@@ -4492,7 +5258,9 @@ function cleanupPullResult(pullResult, keepFiles) {
|
|
|
4492
5258
|
// src/commands/doctor.ts
|
|
4493
5259
|
async function runDoctorCommand(options) {
|
|
4494
5260
|
const logger = createCliLogger({ verbose: options.verbose });
|
|
5261
|
+
validateRuntimeOption2(options.runtime);
|
|
4495
5262
|
validateCursorUsageCsvOptions2(options);
|
|
5263
|
+
validateHermesLocalOnly2(options);
|
|
4496
5264
|
if (options.railway) {
|
|
4497
5265
|
logger.verbose("Inspecting Railway audit readiness.");
|
|
4498
5266
|
const railwayTarget = buildRailwayTarget2(options);
|
|
@@ -4529,14 +5297,17 @@ async function runDoctorCommand(options) {
|
|
|
4529
5297
|
`);
|
|
4530
5298
|
return;
|
|
4531
5299
|
}
|
|
4532
|
-
logger.verbose(
|
|
5300
|
+
logger.verbose(
|
|
5301
|
+
options.runtime ? `Inspecting local ${options.runtime === "hermes" ? "Hermes" : "OpenClaw"} audit readiness.` : "Inspecting local runtime audit readiness."
|
|
5302
|
+
);
|
|
4533
5303
|
if (options.logFile) {
|
|
4534
5304
|
logger.verbose(`Using explicit local log file: ${options.logFile}`);
|
|
4535
5305
|
}
|
|
4536
5306
|
if (options.sessionsDir) {
|
|
4537
5307
|
logger.verbose(`Using explicit local sessions directory: ${options.sessionsDir}`);
|
|
4538
5308
|
}
|
|
4539
|
-
const report = await
|
|
5309
|
+
const report = await doctorAgentRuntime({
|
|
5310
|
+
runtime: options.runtime ?? "auto",
|
|
4540
5311
|
logFile: options.logFile,
|
|
4541
5312
|
sessionsDir: options.sessionsDir,
|
|
4542
5313
|
onProgress: logger.verbose
|
|
@@ -4544,11 +5315,22 @@ async function runDoctorCommand(options) {
|
|
|
4544
5315
|
process.stdout.write(`${renderDoctorReport(report, { commandPrefix: options.commandPrefix })}
|
|
4545
5316
|
`);
|
|
4546
5317
|
}
|
|
5318
|
+
function validateRuntimeOption2(runtime) {
|
|
5319
|
+
if (!runtime) {
|
|
5320
|
+
return;
|
|
5321
|
+
}
|
|
5322
|
+
if (runtime !== "openclaw" && runtime !== "hermes") {
|
|
5323
|
+
throw new Error(
|
|
5324
|
+
`Unsupported runtime "${runtime}". Use --runtime openclaw or --runtime hermes.`
|
|
5325
|
+
);
|
|
5326
|
+
}
|
|
5327
|
+
}
|
|
4547
5328
|
function validateCursorUsageCsvOptions2(options) {
|
|
4548
5329
|
if (!options.cursorUsageCsv) {
|
|
4549
5330
|
return;
|
|
4550
5331
|
}
|
|
4551
5332
|
const conflicts = [
|
|
5333
|
+
options.runtime ? "--runtime" : null,
|
|
4552
5334
|
options.logFile ? "--log-file" : null,
|
|
4553
5335
|
options.sessionsDir ? "--sessions-dir" : null,
|
|
4554
5336
|
options.remote ? "--remote" : null,
|
|
@@ -4563,6 +5345,25 @@ function validateCursorUsageCsvOptions2(options) {
|
|
|
4563
5345
|
throw new Error(`The --cursor-usage-csv flag cannot be combined with ${conflicts.join(", ")}.`);
|
|
4564
5346
|
}
|
|
4565
5347
|
}
|
|
5348
|
+
function validateHermesLocalOnly2(options) {
|
|
5349
|
+
if (options.runtime !== "hermes") {
|
|
5350
|
+
return;
|
|
5351
|
+
}
|
|
5352
|
+
const conflicts = [
|
|
5353
|
+
options.remote ? "--remote" : null,
|
|
5354
|
+
options.remoteLogFile ? "--remote-log-file" : null,
|
|
5355
|
+
options.remoteSessionsDir ? "--remote-sessions-dir" : null,
|
|
5356
|
+
options.railway ? "--railway" : null,
|
|
5357
|
+
options.railwayProject ? "--project" : null,
|
|
5358
|
+
options.railwayEnvironment ? "--environment" : null,
|
|
5359
|
+
options.railwayService ? "--service" : null
|
|
5360
|
+
].filter((flag) => flag !== null);
|
|
5361
|
+
if (conflicts.length > 0) {
|
|
5362
|
+
throw new Error(
|
|
5363
|
+
`Hermes remote transport is not supported yet. Remove ${conflicts.join(", ")} or switch to --runtime openclaw.`
|
|
5364
|
+
);
|
|
5365
|
+
}
|
|
5366
|
+
}
|
|
4566
5367
|
function buildRailwayTarget2(options) {
|
|
4567
5368
|
if (options.railwayProject && options.railwayEnvironment && options.railwayService) {
|
|
4568
5369
|
return {
|
|
@@ -4798,7 +5599,7 @@ function runLogoutCommand() {
|
|
|
4798
5599
|
}
|
|
4799
5600
|
|
|
4800
5601
|
// src/commands/push.ts
|
|
4801
|
-
import { readFileSync as
|
|
5602
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
4802
5603
|
async function runPushCommand(options) {
|
|
4803
5604
|
const payload = options.file ? loadPayloadFromFile(options.file) : loadPayloadFromCache();
|
|
4804
5605
|
if (options.dryRun) {
|
|
@@ -4822,7 +5623,7 @@ async function runPushCommand(options) {
|
|
|
4822
5623
|
function loadPayloadFromFile(filePath) {
|
|
4823
5624
|
let raw;
|
|
4824
5625
|
try {
|
|
4825
|
-
raw =
|
|
5626
|
+
raw = readFileSync8(filePath, "utf8");
|
|
4826
5627
|
} catch {
|
|
4827
5628
|
throw new Error(`Cannot read file: ${filePath}`);
|
|
4828
5629
|
}
|
|
@@ -4863,19 +5664,10 @@ function loadPayloadFromCache() {
|
|
|
4863
5664
|
);
|
|
4864
5665
|
return toWirePayload(latest, meta);
|
|
4865
5666
|
}
|
|
4866
|
-
function readCliVersion2() {
|
|
4867
|
-
try {
|
|
4868
|
-
const packageJsonPath = new URL("../../package.json", import.meta.url);
|
|
4869
|
-
const pkg = JSON.parse(readFileSync7(packageJsonPath, "utf8"));
|
|
4870
|
-
return pkg.version ?? "0.0.0";
|
|
4871
|
-
} catch {
|
|
4872
|
-
return "0.0.0";
|
|
4873
|
-
}
|
|
4874
|
-
}
|
|
4875
5667
|
function buildMeta2(summary) {
|
|
4876
5668
|
const sourceMeta = buildCachedPushSourceMeta(summary);
|
|
4877
5669
|
return {
|
|
4878
|
-
cliVersion:
|
|
5670
|
+
cliVersion: getCliVersion(),
|
|
4879
5671
|
sourceId: sourceMeta.sourceId,
|
|
4880
5672
|
sourceHost: sourceMeta.sourceHost,
|
|
4881
5673
|
environment: sourceMeta.environment
|
|
@@ -4886,14 +5678,14 @@ function buildMeta2(summary) {
|
|
|
4886
5678
|
function renderRootHelp(version, display) {
|
|
4887
5679
|
return `${display.name} ${version}
|
|
4888
5680
|
|
|
4889
|
-
Waste intelligence for OpenClaw workflows
|
|
5681
|
+
Waste intelligence for OpenClaw and Hermes workflows plus local Cursor usage CSVs.
|
|
4890
5682
|
|
|
4891
5683
|
Usage:
|
|
4892
5684
|
${formatCommand("<command> [options]", display.prefix)}
|
|
4893
5685
|
|
|
4894
5686
|
Commands:
|
|
4895
|
-
audit Analyze OpenClaw logs or a local Cursor usage CSV.
|
|
4896
|
-
doctor Inspect OpenClaw sources or a local Cursor usage CSV.
|
|
5687
|
+
audit Analyze OpenClaw or Hermes logs, or a local Cursor usage CSV.
|
|
5688
|
+
doctor Inspect OpenClaw or Hermes sources, or a local Cursor usage CSV.
|
|
4897
5689
|
push Push a cached audit snapshot to the Xerg API.
|
|
4898
5690
|
login Authenticate with the Xerg API via browser.
|
|
4899
5691
|
logout Remove stored Xerg API credentials.
|
|
@@ -4906,14 +5698,15 @@ Global options:
|
|
|
4906
5698
|
function renderAuditHelp(commandPrefix) {
|
|
4907
5699
|
return `${formatCommand("audit", commandPrefix)}
|
|
4908
5700
|
|
|
4909
|
-
Analyze OpenClaw logs or a local Cursor usage CSV and produce an audit report.
|
|
5701
|
+
Analyze OpenClaw or Hermes logs, or a local Cursor usage CSV, and produce an audit report.
|
|
4910
5702
|
|
|
4911
5703
|
Usage:
|
|
4912
5704
|
${formatCommand("audit [options]", commandPrefix)}
|
|
4913
5705
|
|
|
4914
5706
|
Options:
|
|
4915
|
-
--
|
|
4916
|
-
--
|
|
5707
|
+
--runtime <name> Local runtime to inspect: openclaw or hermes
|
|
5708
|
+
--log-file <path> Explicit local gateway log file to analyze
|
|
5709
|
+
--sessions-dir <path> Explicit local sessions directory to analyze
|
|
4917
5710
|
--cursor-usage-csv <path> Local Cursor usage CSV export to analyze
|
|
4918
5711
|
--since <duration> Look back window such as 24h, 7d, or 30m
|
|
4919
5712
|
--compare Compare this audit to the newest compatible prior local snapshot
|
|
@@ -4922,7 +5715,7 @@ Options:
|
|
|
4922
5715
|
--db <path> Custom SQLite database path
|
|
4923
5716
|
--no-db Skip local persistence
|
|
4924
5717
|
|
|
4925
|
-
Remote options (SSH):
|
|
5718
|
+
Remote options (SSH, OpenClaw only):
|
|
4926
5719
|
--remote <user@host> SSH target in user@host or user@host:port format
|
|
4927
5720
|
--remote-log-file <path> Override the default gateway log path on the remote host
|
|
4928
5721
|
--remote-sessions-dir <path> Override the default sessions directory on the remote host
|
|
@@ -4932,7 +5725,7 @@ Remote options (SSH):
|
|
|
4932
5725
|
Prerequisites:
|
|
4933
5726
|
SSH remote audits require ssh and rsync on your PATH.
|
|
4934
5727
|
|
|
4935
|
-
Railway options:
|
|
5728
|
+
Railway options (OpenClaw only):
|
|
4936
5729
|
--railway Audit a Railway service (uses linked project by default)
|
|
4937
5730
|
--project <id> Railway project ID
|
|
4938
5731
|
--environment <id> Railway environment ID
|
|
@@ -4975,25 +5768,26 @@ Authentication:
|
|
|
4975
5768
|
function renderDoctorHelp(commandPrefix) {
|
|
4976
5769
|
return `${formatCommand("doctor", commandPrefix)}
|
|
4977
5770
|
|
|
4978
|
-
Inspect OpenClaw sources or a local Cursor usage CSV before you audit.
|
|
5771
|
+
Inspect OpenClaw or Hermes sources, or a local Cursor usage CSV, before you audit.
|
|
4979
5772
|
|
|
4980
5773
|
Usage:
|
|
4981
5774
|
${formatCommand("doctor [options]", commandPrefix)}
|
|
4982
5775
|
|
|
4983
5776
|
Options:
|
|
4984
|
-
--
|
|
4985
|
-
--
|
|
5777
|
+
--runtime <name> Local runtime to inspect: openclaw or hermes
|
|
5778
|
+
--log-file <path> Explicit local gateway log file to inspect
|
|
5779
|
+
--sessions-dir <path> Explicit local sessions directory to inspect
|
|
4986
5780
|
--cursor-usage-csv <path> Local Cursor usage CSV export to inspect
|
|
4987
5781
|
--verbose Print progress updates to stderr while doctor runs
|
|
4988
5782
|
|
|
4989
|
-
Remote options (SSH):
|
|
5783
|
+
Remote options (SSH, OpenClaw only):
|
|
4990
5784
|
--remote <user@host> SSH target in user@host or user@host:port format
|
|
4991
5785
|
--remote-log-file <path> Override the default gateway log path on the remote host
|
|
4992
5786
|
--remote-sessions-dir <path> Override the default sessions directory on the remote host
|
|
4993
5787
|
|
|
4994
5788
|
SSH checks require ssh and rsync on your PATH.
|
|
4995
5789
|
|
|
4996
|
-
Railway options:
|
|
5790
|
+
Railway options (OpenClaw only):
|
|
4997
5791
|
--railway Check a Railway service (uses linked project by default)
|
|
4998
5792
|
--project <id> Railway project ID
|
|
4999
5793
|
--environment <id> Railway environment ID
|
|
@@ -5006,7 +5800,7 @@ Railway options:
|
|
|
5006
5800
|
}
|
|
5007
5801
|
|
|
5008
5802
|
// src/index.ts
|
|
5009
|
-
var VERSION =
|
|
5803
|
+
var VERSION = getCliVersion();
|
|
5010
5804
|
var argv = process.argv.slice(2);
|
|
5011
5805
|
var commandDisplay = resolveCommandDisplay();
|
|
5012
5806
|
var command = argv[0];
|
|
@@ -5077,6 +5871,10 @@ function parseAuditOptions(raw) {
|
|
|
5077
5871
|
options.logFile = readValue(arg, argv2[index + 1]);
|
|
5078
5872
|
index += 1;
|
|
5079
5873
|
break;
|
|
5874
|
+
case "--runtime":
|
|
5875
|
+
options.runtime = readValue(arg, argv2[index + 1]);
|
|
5876
|
+
index += 1;
|
|
5877
|
+
break;
|
|
5080
5878
|
case "--sessions-dir":
|
|
5081
5879
|
options.sessionsDir = readValue(arg, argv2[index + 1]);
|
|
5082
5880
|
index += 1;
|
|
@@ -5205,6 +6003,10 @@ function parseDoctorOptions(raw) {
|
|
|
5205
6003
|
options.logFile = readValue(arg, argv2[index + 1]);
|
|
5206
6004
|
index += 1;
|
|
5207
6005
|
break;
|
|
6006
|
+
case "--runtime":
|
|
6007
|
+
options.runtime = readValue(arg, argv2[index + 1]);
|
|
6008
|
+
index += 1;
|
|
6009
|
+
break;
|
|
5208
6010
|
case "--sessions-dir":
|
|
5209
6011
|
options.sessionsDir = readValue(arg, argv2[index + 1]);
|
|
5210
6012
|
index += 1;
|
|
@@ -5280,9 +6082,4 @@ function readFloat(flag, value) {
|
|
|
5280
6082
|
function colorError(message) {
|
|
5281
6083
|
return process.stderr.isTTY ? styleText2("red", message) : message;
|
|
5282
6084
|
}
|
|
5283
|
-
function readVersion() {
|
|
5284
|
-
const packageJsonPath = new URL("../package.json", import.meta.url);
|
|
5285
|
-
const packageJson = JSON.parse(readFileSync8(packageJsonPath, "utf8"));
|
|
5286
|
-
return packageJson.version ?? "0.0.0";
|
|
5287
|
-
}
|
|
5288
6085
|
//# sourceMappingURL=index.js.map
|