@xerg/cli 0.1.10 → 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 +21 -12
- package/dist/index.js +2644 -578
- 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 readFileSync7 } from "fs";
|
|
5
4
|
import { styleText as styleText2 } from "util";
|
|
6
5
|
|
|
7
6
|
// src/command-display.ts
|
|
@@ -111,9 +110,11 @@ function normalizeSignal(value) {
|
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
// src/commands/audit.ts
|
|
114
|
-
import { readFileSync as readFileSync5 } from "fs";
|
|
115
113
|
import { rmSync as rmSync4 } from "fs";
|
|
116
|
-
|
|
114
|
+
|
|
115
|
+
// ../core/src/cursor/usage-csv.ts
|
|
116
|
+
import { readFileSync, statSync } from "fs";
|
|
117
|
+
import { resolve } from "path";
|
|
117
118
|
|
|
118
119
|
// ../core/src/utils/hash.ts
|
|
119
120
|
import { createHash } from "crypto";
|
|
@@ -177,6 +178,563 @@ function toIsoOrNow(value) {
|
|
|
177
178
|
return isoNow();
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
// ../core/src/cursor/usage-csv.ts
|
|
182
|
+
var REQUIRED_HEADERS = [
|
|
183
|
+
"Date",
|
|
184
|
+
"Kind",
|
|
185
|
+
"Model",
|
|
186
|
+
"Max Mode",
|
|
187
|
+
"Input (w/ Cache Write)",
|
|
188
|
+
"Input (w/o Cache Write)",
|
|
189
|
+
"Cache Read",
|
|
190
|
+
"Output Tokens",
|
|
191
|
+
"Total Tokens",
|
|
192
|
+
"Cost"
|
|
193
|
+
];
|
|
194
|
+
var CURSOR_ALIAS_PRICING = {
|
|
195
|
+
"claude-4.6-opus-high-thinking": {
|
|
196
|
+
provider: "anthropic",
|
|
197
|
+
canonicalModel: "claude-opus-4",
|
|
198
|
+
inputPer1m: 15,
|
|
199
|
+
outputPer1m: 75,
|
|
200
|
+
cacheWritePer1m: 18.75,
|
|
201
|
+
cachedInputPer1m: 1.5
|
|
202
|
+
},
|
|
203
|
+
"claude-4.5-sonnet": {
|
|
204
|
+
provider: "anthropic",
|
|
205
|
+
canonicalModel: "claude-sonnet-4-5",
|
|
206
|
+
inputPer1m: 3,
|
|
207
|
+
outputPer1m: 15,
|
|
208
|
+
cacheWritePer1m: 3.75,
|
|
209
|
+
cachedInputPer1m: 0.3
|
|
210
|
+
},
|
|
211
|
+
"claude-4.5-sonnet-thinking": {
|
|
212
|
+
provider: "anthropic",
|
|
213
|
+
canonicalModel: "claude-sonnet-4-5",
|
|
214
|
+
inputPer1m: 3,
|
|
215
|
+
outputPer1m: 15,
|
|
216
|
+
cacheWritePer1m: 3.75,
|
|
217
|
+
cachedInputPer1m: 0.3
|
|
218
|
+
},
|
|
219
|
+
"claude-4.5-opus-high-thinking": {
|
|
220
|
+
provider: "anthropic",
|
|
221
|
+
canonicalModel: "claude-opus-4-5",
|
|
222
|
+
inputPer1m: 5,
|
|
223
|
+
outputPer1m: 25,
|
|
224
|
+
cacheWritePer1m: 6.25,
|
|
225
|
+
cachedInputPer1m: 0.5
|
|
226
|
+
},
|
|
227
|
+
"gpt-5.1-codex": {
|
|
228
|
+
provider: "openai",
|
|
229
|
+
canonicalModel: "gpt-5.1-codex",
|
|
230
|
+
inputPer1m: 1.25,
|
|
231
|
+
outputPer1m: 10,
|
|
232
|
+
cacheWritePer1m: 1.25,
|
|
233
|
+
cachedInputPer1m: 0.125
|
|
234
|
+
},
|
|
235
|
+
"gpt-5-high-fast": {
|
|
236
|
+
provider: "openai",
|
|
237
|
+
canonicalModel: "gpt-5-high-fast",
|
|
238
|
+
inputPer1m: 1.25,
|
|
239
|
+
outputPer1m: 10,
|
|
240
|
+
cacheWritePer1m: 1.25,
|
|
241
|
+
cachedInputPer1m: 0.125
|
|
242
|
+
},
|
|
243
|
+
"gpt-5": {
|
|
244
|
+
provider: "openai",
|
|
245
|
+
canonicalModel: "gpt-5",
|
|
246
|
+
inputPer1m: 1.25,
|
|
247
|
+
outputPer1m: 10,
|
|
248
|
+
cacheWritePer1m: 1.25,
|
|
249
|
+
cachedInputPer1m: 0.125
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
function round(value) {
|
|
253
|
+
return Number(value.toFixed(6));
|
|
254
|
+
}
|
|
255
|
+
function parseCsvLine(line) {
|
|
256
|
+
const values = [];
|
|
257
|
+
let current = "";
|
|
258
|
+
let inQuotes = false;
|
|
259
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
260
|
+
const char = line[index];
|
|
261
|
+
if (char === '"') {
|
|
262
|
+
const next = line[index + 1];
|
|
263
|
+
if (inQuotes && next === '"') {
|
|
264
|
+
current += '"';
|
|
265
|
+
index += 1;
|
|
266
|
+
} else {
|
|
267
|
+
inQuotes = !inQuotes;
|
|
268
|
+
}
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (char === "," && !inQuotes) {
|
|
272
|
+
values.push(current);
|
|
273
|
+
current = "";
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
current += char;
|
|
277
|
+
}
|
|
278
|
+
values.push(current);
|
|
279
|
+
return values.map((value) => value.trim());
|
|
280
|
+
}
|
|
281
|
+
function parseInteger(raw, column, rowNumber) {
|
|
282
|
+
const parsed = Number.parseInt(raw, 10);
|
|
283
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
284
|
+
throw new Error(`Invalid ${column} value "${raw}" on row ${rowNumber}.`);
|
|
285
|
+
}
|
|
286
|
+
return parsed;
|
|
287
|
+
}
|
|
288
|
+
function parseTimestamp(raw, rowNumber) {
|
|
289
|
+
const parsed = new Date(raw);
|
|
290
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
291
|
+
throw new Error(`Invalid Date value "${raw}" on row ${rowNumber}.`);
|
|
292
|
+
}
|
|
293
|
+
return parsed.toISOString();
|
|
294
|
+
}
|
|
295
|
+
function parseMaxMode(raw) {
|
|
296
|
+
return raw.trim().toLowerCase() === "yes";
|
|
297
|
+
}
|
|
298
|
+
function parseObservedCost(raw) {
|
|
299
|
+
const value = raw.trim();
|
|
300
|
+
if (value.length === 0 || value === "-" || value.toLowerCase() === "included") {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
const parsed = Number.parseFloat(value);
|
|
304
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
305
|
+
}
|
|
306
|
+
function createDetectedSource(path) {
|
|
307
|
+
const resolvedPath = resolve(path);
|
|
308
|
+
try {
|
|
309
|
+
const stats = statSync(resolvedPath);
|
|
310
|
+
if (!stats.isFile()) {
|
|
311
|
+
throw new Error(`Cursor usage CSV path is not a file: ${resolvedPath}`);
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
kind: "cursor-usage-csv",
|
|
315
|
+
runtime: "cursor",
|
|
316
|
+
path: resolvedPath,
|
|
317
|
+
sizeBytes: stats.size,
|
|
318
|
+
mtimeMs: stats.mtimeMs
|
|
319
|
+
};
|
|
320
|
+
} catch (error) {
|
|
321
|
+
const message = error instanceof Error ? error.message : `Cursor usage CSV not found: ${path}`;
|
|
322
|
+
throw new Error(message);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function validateHeaders(headers) {
|
|
326
|
+
const missing = REQUIRED_HEADERS.filter((header) => !headers.includes(header));
|
|
327
|
+
if (missing.length > 0) {
|
|
328
|
+
throw new Error(`Cursor usage CSV is missing required headers: ${missing.join(", ")}.`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function parseRow(values, headers, rowNumber) {
|
|
332
|
+
const record = Object.fromEntries(headers.map((header, index) => [header, values[index] ?? ""]));
|
|
333
|
+
const costLabel = record.Cost ?? "";
|
|
334
|
+
return {
|
|
335
|
+
timestamp: parseTimestamp(record.Date ?? "", rowNumber),
|
|
336
|
+
kind: record.Kind ?? "",
|
|
337
|
+
modelAlias: record.Model ?? "",
|
|
338
|
+
maxMode: parseMaxMode(record["Max Mode"] ?? ""),
|
|
339
|
+
inputWithCacheWriteTokens: parseInteger(
|
|
340
|
+
record["Input (w/ Cache Write)"] ?? "",
|
|
341
|
+
"Input (w/ Cache Write)",
|
|
342
|
+
rowNumber
|
|
343
|
+
),
|
|
344
|
+
inputWithoutCacheWriteTokens: parseInteger(
|
|
345
|
+
record["Input (w/o Cache Write)"] ?? "",
|
|
346
|
+
"Input (w/o Cache Write)",
|
|
347
|
+
rowNumber
|
|
348
|
+
),
|
|
349
|
+
cacheReadTokens: parseInteger(record["Cache Read"] ?? "", "Cache Read", rowNumber),
|
|
350
|
+
outputTokens: parseInteger(record["Output Tokens"] ?? "", "Output Tokens", rowNumber),
|
|
351
|
+
totalTokens: parseInteger(record["Total Tokens"] ?? "", "Total Tokens", rowNumber),
|
|
352
|
+
costLabel,
|
|
353
|
+
observedCostUsd: parseObservedCost(costLabel)
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function parseRows(lines, headers) {
|
|
357
|
+
const rows = [];
|
|
358
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
359
|
+
const line = lines[index];
|
|
360
|
+
if (!line.trim()) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
rows.push(parseRow(parseCsvLine(line), headers, index + 2));
|
|
364
|
+
}
|
|
365
|
+
return rows;
|
|
366
|
+
}
|
|
367
|
+
function readCursorUsageCsv(path) {
|
|
368
|
+
const source = createDetectedSource(path);
|
|
369
|
+
const content = readFileSync(source.path, "utf8");
|
|
370
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
371
|
+
if (lines.length === 0) {
|
|
372
|
+
throw new Error(`Cursor usage CSV is empty: ${source.path}`);
|
|
373
|
+
}
|
|
374
|
+
const headers = parseCsvLine(lines[0]);
|
|
375
|
+
validateHeaders(headers);
|
|
376
|
+
const rows = parseRows(lines.slice(1), headers);
|
|
377
|
+
return {
|
|
378
|
+
source,
|
|
379
|
+
rows,
|
|
380
|
+
headers,
|
|
381
|
+
hasObservedCostRows: rows.some((row) => row.observedCostUsd !== null)
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function isErroredNoCharge(kind) {
|
|
385
|
+
const normalized = kind.trim().toLowerCase();
|
|
386
|
+
return normalized.includes("errored") && normalized.includes("no charge") || normalized.includes("not charged");
|
|
387
|
+
}
|
|
388
|
+
function inferProvider(modelAlias) {
|
|
389
|
+
const normalized = modelAlias.trim().toLowerCase();
|
|
390
|
+
if (normalized.startsWith("claude-")) {
|
|
391
|
+
return "anthropic";
|
|
392
|
+
}
|
|
393
|
+
if (normalized.startsWith("gpt-")) {
|
|
394
|
+
return "openai";
|
|
395
|
+
}
|
|
396
|
+
return "cursor";
|
|
397
|
+
}
|
|
398
|
+
function buildModelKey(modelAlias, pricing) {
|
|
399
|
+
if (pricing) {
|
|
400
|
+
return `${pricing.provider}/${pricing.canonicalModel}`;
|
|
401
|
+
}
|
|
402
|
+
return `${inferProvider(modelAlias)}/${modelAlias}`;
|
|
403
|
+
}
|
|
404
|
+
function getWorkflowKey(row) {
|
|
405
|
+
const kind = row.kind.trim().toLowerCase();
|
|
406
|
+
if (kind.includes("on-demand")) {
|
|
407
|
+
return row.maxMode ? "on-demand / max mode" : "on-demand / standard mode";
|
|
408
|
+
}
|
|
409
|
+
if (kind.includes("included")) {
|
|
410
|
+
return row.maxMode ? "included / max mode" : "included / standard mode";
|
|
411
|
+
}
|
|
412
|
+
if (kind.includes("error") || kind.includes("not charged")) {
|
|
413
|
+
return "not charged / failed or aborted";
|
|
414
|
+
}
|
|
415
|
+
return row.maxMode ? "other / max mode" : "other / standard mode";
|
|
416
|
+
}
|
|
417
|
+
function estimateCursorRowCost(row, options) {
|
|
418
|
+
const pricing = CURSOR_ALIAS_PRICING[row.modelAlias.trim().toLowerCase()] ?? null;
|
|
419
|
+
if (isErroredNoCharge(row.kind)) {
|
|
420
|
+
return {
|
|
421
|
+
costUsd: 0,
|
|
422
|
+
costSource: "observed",
|
|
423
|
+
cacheCostUsd: 0,
|
|
424
|
+
cacheWriteCostUsd: 0,
|
|
425
|
+
pricing,
|
|
426
|
+
canonicalModelKey: buildModelKey(row.modelAlias, pricing)
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
if (options.preferObservedCost) {
|
|
430
|
+
if (row.observedCostUsd !== null) {
|
|
431
|
+
const cacheCost2 = row.cacheReadTokens > 0 && pricing?.cachedInputPer1m !== void 0 ? round(row.cacheReadTokens / 1e6 * pricing.cachedInputPer1m) : null;
|
|
432
|
+
const cacheWriteCost2 = row.inputWithCacheWriteTokens > 0 && pricing ? round(
|
|
433
|
+
row.inputWithCacheWriteTokens / 1e6 * (pricing.cacheWritePer1m ?? pricing.inputPer1m)
|
|
434
|
+
) : null;
|
|
435
|
+
return {
|
|
436
|
+
costUsd: row.observedCostUsd,
|
|
437
|
+
costSource: "observed",
|
|
438
|
+
cacheCostUsd: cacheCost2,
|
|
439
|
+
cacheWriteCostUsd: cacheWriteCost2,
|
|
440
|
+
pricing,
|
|
441
|
+
canonicalModelKey: buildModelKey(row.modelAlias, pricing)
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
costUsd: 0,
|
|
446
|
+
costSource: "observed",
|
|
447
|
+
cacheCostUsd: 0,
|
|
448
|
+
cacheWriteCostUsd: 0,
|
|
449
|
+
pricing,
|
|
450
|
+
canonicalModelKey: buildModelKey(row.modelAlias, pricing)
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
if (!pricing) {
|
|
454
|
+
return {
|
|
455
|
+
costUsd: 0,
|
|
456
|
+
costSource: "unpriced",
|
|
457
|
+
cacheCostUsd: null,
|
|
458
|
+
cacheWriteCostUsd: null,
|
|
459
|
+
pricing: null,
|
|
460
|
+
canonicalModelKey: buildModelKey(row.modelAlias, pricing)
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
if (row.cacheReadTokens > 0 && pricing.cachedInputPer1m === void 0) {
|
|
464
|
+
return {
|
|
465
|
+
costUsd: 0,
|
|
466
|
+
costSource: "unpriced",
|
|
467
|
+
cacheCostUsd: null,
|
|
468
|
+
cacheWriteCostUsd: null,
|
|
469
|
+
pricing: null,
|
|
470
|
+
canonicalModelKey: buildModelKey(row.modelAlias, pricing)
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
const inputCost = row.inputWithoutCacheWriteTokens / 1e6 * pricing.inputPer1m;
|
|
474
|
+
const cacheWriteCost = row.inputWithCacheWriteTokens / 1e6 * (pricing.cacheWritePer1m ?? pricing.inputPer1m);
|
|
475
|
+
const outputCost = row.outputTokens / 1e6 * pricing.outputPer1m;
|
|
476
|
+
const cacheCost = row.cacheReadTokens > 0 && pricing.cachedInputPer1m !== void 0 ? row.cacheReadTokens / 1e6 * pricing.cachedInputPer1m : 0;
|
|
477
|
+
return {
|
|
478
|
+
costUsd: round(inputCost + cacheWriteCost + outputCost + cacheCost),
|
|
479
|
+
costSource: "estimated",
|
|
480
|
+
cacheCostUsd: round(cacheCost),
|
|
481
|
+
cacheWriteCostUsd: round(cacheWriteCost),
|
|
482
|
+
pricing,
|
|
483
|
+
canonicalModelKey: buildModelKey(row.modelAlias, pricing)
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function buildCall(row, source, runId, index, options) {
|
|
487
|
+
const cost = estimateCursorRowCost(row, options);
|
|
488
|
+
const totalInputTokens = Math.max(row.totalTokens - row.outputTokens, 0);
|
|
489
|
+
return {
|
|
490
|
+
cost,
|
|
491
|
+
call: {
|
|
492
|
+
id: sha1(`${runId}:${source.path}:${index}:${row.modelAlias}:${row.timestamp}`),
|
|
493
|
+
runId,
|
|
494
|
+
timestamp: row.timestamp,
|
|
495
|
+
provider: cost.pricing?.provider ?? inferProvider(row.modelAlias),
|
|
496
|
+
model: cost.pricing?.canonicalModel ?? row.modelAlias,
|
|
497
|
+
inputTokens: totalInputTokens,
|
|
498
|
+
outputTokens: row.outputTokens,
|
|
499
|
+
costUsd: cost.costUsd,
|
|
500
|
+
costSource: cost.costSource,
|
|
501
|
+
latencyMs: null,
|
|
502
|
+
toolCalls: 0,
|
|
503
|
+
retries: 0,
|
|
504
|
+
attempt: null,
|
|
505
|
+
iteration: null,
|
|
506
|
+
status: isErroredNoCharge(row.kind) ? "error" : null,
|
|
507
|
+
taskClass: null,
|
|
508
|
+
cacheHit: row.cacheReadTokens > 0,
|
|
509
|
+
cacheCostUsd: cost.cacheCostUsd,
|
|
510
|
+
metadata: {
|
|
511
|
+
source: "cursor-usage-csv",
|
|
512
|
+
kind: row.kind,
|
|
513
|
+
maxMode: row.maxMode,
|
|
514
|
+
modelAlias: row.modelAlias,
|
|
515
|
+
costLabel: row.costLabel,
|
|
516
|
+
totalTokens: row.totalTokens,
|
|
517
|
+
inputWithCacheWriteTokens: row.inputWithCacheWriteTokens,
|
|
518
|
+
inputWithoutCacheWriteTokens: row.inputWithoutCacheWriteTokens,
|
|
519
|
+
cacheReadTokens: row.cacheReadTokens,
|
|
520
|
+
pricingProvider: cost.pricing?.provider ?? null,
|
|
521
|
+
pricingModel: cost.pricing?.canonicalModel ?? null,
|
|
522
|
+
canonicalModelKey: cost.canonicalModelKey,
|
|
523
|
+
observedCostUsd: row.observedCostUsd,
|
|
524
|
+
cacheWriteCostUsd: cost.cacheWriteCostUsd
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function normalizeCursorUsageCsv(input) {
|
|
530
|
+
const cutoff = parseSince(input.since);
|
|
531
|
+
const runs = [];
|
|
532
|
+
const modelCoverage = /* @__PURE__ */ new Map();
|
|
533
|
+
const modes = /* @__PURE__ */ new Map();
|
|
534
|
+
const models = /* @__PURE__ */ new Map();
|
|
535
|
+
let pricedCallCount = 0;
|
|
536
|
+
let unpricedCallCount = 0;
|
|
537
|
+
let pricedTokenCount = 0;
|
|
538
|
+
let unpricedTokenCount = 0;
|
|
539
|
+
let totalTokens = 0;
|
|
540
|
+
let totalOutputTokens = 0;
|
|
541
|
+
let totalCacheReadTokens = 0;
|
|
542
|
+
let totalInputWithCacheWriteTokens = 0;
|
|
543
|
+
let totalInputWithoutCacheWriteTokens = 0;
|
|
544
|
+
input.rows.forEach((row, index) => {
|
|
545
|
+
const timestampMs = new Date(row.timestamp).getTime();
|
|
546
|
+
if (cutoff && timestampMs < cutoff) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const workflow = getWorkflowKey(row);
|
|
550
|
+
const runId = sha1(`${input.source.path}:${row.timestamp}:${row.modelAlias}:${index}`);
|
|
551
|
+
const { call, cost } = buildCall(row, input.source, runId, index, {
|
|
552
|
+
preferObservedCost: input.hasObservedCostRows ?? false
|
|
553
|
+
});
|
|
554
|
+
const run2 = {
|
|
555
|
+
id: runId,
|
|
556
|
+
sourceKind: input.source.kind,
|
|
557
|
+
sourcePath: input.source.path,
|
|
558
|
+
timestamp: row.timestamp,
|
|
559
|
+
workflow,
|
|
560
|
+
environment: "local",
|
|
561
|
+
tags: {
|
|
562
|
+
sourceKind: input.source.kind,
|
|
563
|
+
maxMode: row.maxMode,
|
|
564
|
+
kind: row.kind
|
|
565
|
+
},
|
|
566
|
+
calls: [call],
|
|
567
|
+
totalCostUsd: call.costUsd,
|
|
568
|
+
totalTokens: row.totalTokens,
|
|
569
|
+
observedCostUsd: call.costSource === "observed" ? call.costUsd : 0,
|
|
570
|
+
estimatedCostUsd: call.costSource === "estimated" ? call.costUsd : 0
|
|
571
|
+
};
|
|
572
|
+
runs.push(run2);
|
|
573
|
+
totalTokens += row.totalTokens;
|
|
574
|
+
totalOutputTokens += row.outputTokens;
|
|
575
|
+
totalCacheReadTokens += row.cacheReadTokens;
|
|
576
|
+
totalInputWithCacheWriteTokens += row.inputWithCacheWriteTokens;
|
|
577
|
+
totalInputWithoutCacheWriteTokens += row.inputWithoutCacheWriteTokens;
|
|
578
|
+
const totalRowTokens = row.totalTokens;
|
|
579
|
+
if (cost.costSource === "unpriced") {
|
|
580
|
+
unpricedCallCount += 1;
|
|
581
|
+
unpricedTokenCount += totalRowTokens;
|
|
582
|
+
const current = modelCoverage.get(row.modelAlias) ?? { callCount: 0, totalTokens: 0 };
|
|
583
|
+
current.callCount += 1;
|
|
584
|
+
current.totalTokens += totalRowTokens;
|
|
585
|
+
modelCoverage.set(row.modelAlias, current);
|
|
586
|
+
} else {
|
|
587
|
+
pricedCallCount += 1;
|
|
588
|
+
pricedTokenCount += totalRowTokens;
|
|
589
|
+
}
|
|
590
|
+
const modeBucket = modes.get(workflow) ?? {
|
|
591
|
+
callCount: 0,
|
|
592
|
+
totalTokens: 0,
|
|
593
|
+
estimatedSpendUsd: 0
|
|
594
|
+
};
|
|
595
|
+
modeBucket.callCount += 1;
|
|
596
|
+
modeBucket.totalTokens += totalRowTokens;
|
|
597
|
+
modeBucket.estimatedSpendUsd = round(modeBucket.estimatedSpendUsd + call.costUsd);
|
|
598
|
+
modes.set(workflow, modeBucket);
|
|
599
|
+
const modelBucket = models.get(cost.canonicalModelKey) ?? {
|
|
600
|
+
callCount: 0,
|
|
601
|
+
totalTokens: 0,
|
|
602
|
+
estimatedSpendUsd: 0,
|
|
603
|
+
pricedCallCount: 0,
|
|
604
|
+
unpricedCallCount: 0
|
|
605
|
+
};
|
|
606
|
+
modelBucket.callCount += 1;
|
|
607
|
+
modelBucket.totalTokens += totalRowTokens;
|
|
608
|
+
modelBucket.estimatedSpendUsd = round(modelBucket.estimatedSpendUsd + call.costUsd);
|
|
609
|
+
if (cost.costSource === "unpriced") {
|
|
610
|
+
modelBucket.unpricedCallCount += 1;
|
|
611
|
+
} else {
|
|
612
|
+
modelBucket.pricedCallCount += 1;
|
|
613
|
+
}
|
|
614
|
+
models.set(cost.canonicalModelKey, modelBucket);
|
|
615
|
+
});
|
|
616
|
+
runs.sort(
|
|
617
|
+
(left, right) => new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime()
|
|
618
|
+
);
|
|
619
|
+
return {
|
|
620
|
+
runs,
|
|
621
|
+
pricingCoverage: {
|
|
622
|
+
pricedCallCount,
|
|
623
|
+
unpricedCallCount,
|
|
624
|
+
pricedTokenCount,
|
|
625
|
+
unpricedTokenCount,
|
|
626
|
+
topUnpricedModels: Array.from(modelCoverage.entries()).map(([key, value]) => ({
|
|
627
|
+
key,
|
|
628
|
+
callCount: value.callCount,
|
|
629
|
+
totalTokens: value.totalTokens
|
|
630
|
+
})).sort((left, right) => right.totalTokens - left.totalTokens).slice(0, 5)
|
|
631
|
+
},
|
|
632
|
+
cursorUsage: {
|
|
633
|
+
totalTokens,
|
|
634
|
+
totalInputTokens: Math.max(totalTokens - totalOutputTokens, 0),
|
|
635
|
+
totalOutputTokens,
|
|
636
|
+
totalCacheReadTokens,
|
|
637
|
+
totalInputWithCacheWriteTokens,
|
|
638
|
+
totalInputWithoutCacheWriteTokens,
|
|
639
|
+
modes: Array.from(modes.entries()).map(([key, value]) => ({
|
|
640
|
+
key,
|
|
641
|
+
callCount: value.callCount,
|
|
642
|
+
totalTokens: value.totalTokens,
|
|
643
|
+
estimatedSpendUsd: value.estimatedSpendUsd
|
|
644
|
+
})).sort((left, right) => right.totalTokens - left.totalTokens),
|
|
645
|
+
models: Array.from(models.entries()).map(([key, value]) => ({
|
|
646
|
+
key,
|
|
647
|
+
callCount: value.callCount,
|
|
648
|
+
totalTokens: value.totalTokens,
|
|
649
|
+
estimatedSpendUsd: value.estimatedSpendUsd,
|
|
650
|
+
pricedCallCount: value.pricedCallCount,
|
|
651
|
+
unpricedCallCount: value.unpricedCallCount
|
|
652
|
+
})).sort((left, right) => right.totalTokens - left.totalTokens)
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
function buildDoctorNotes(report) {
|
|
657
|
+
const notes = ["Cursor usage CSV headers validated."];
|
|
658
|
+
if (report.rowCount === 0) {
|
|
659
|
+
notes.push("The CSV contains no usage rows.");
|
|
660
|
+
}
|
|
661
|
+
if (report.pricingCoverage.unpricedCallCount > 0) {
|
|
662
|
+
const aliases = report.pricingCoverage.topUnpricedModels.map((model) => model.key).join(", ");
|
|
663
|
+
notes.push(
|
|
664
|
+
`Some Cursor aliases do not have full local pricing coverage: ${aliases || "unknown aliases"}.`
|
|
665
|
+
);
|
|
666
|
+
} else {
|
|
667
|
+
notes.push("All rows in this CSV have local pricing coverage.");
|
|
668
|
+
}
|
|
669
|
+
notes.push("Cursor CSV audits use exported usage rows rather than raw session transcripts.");
|
|
670
|
+
return notes;
|
|
671
|
+
}
|
|
672
|
+
async function inspectCursorUsageCsv(options) {
|
|
673
|
+
const filePath = options.cursorUsageCsv ? resolve(options.cursorUsageCsv) : "";
|
|
674
|
+
options.onProgress?.("Inspecting Cursor usage CSV...");
|
|
675
|
+
if (!filePath) {
|
|
676
|
+
return {
|
|
677
|
+
canAudit: false,
|
|
678
|
+
filePath,
|
|
679
|
+
source: null,
|
|
680
|
+
rowCount: 0,
|
|
681
|
+
dateRange: null,
|
|
682
|
+
pricingCoverage: {
|
|
683
|
+
pricedCallCount: 0,
|
|
684
|
+
unpricedCallCount: 0,
|
|
685
|
+
pricedTokenCount: 0,
|
|
686
|
+
unpricedTokenCount: 0,
|
|
687
|
+
topUnpricedModels: []
|
|
688
|
+
},
|
|
689
|
+
notes: ["No Cursor usage CSV path was provided."]
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
try {
|
|
693
|
+
const parsed = readCursorUsageCsv(filePath);
|
|
694
|
+
const normalized = normalizeCursorUsageCsv({
|
|
695
|
+
source: parsed.source,
|
|
696
|
+
rows: parsed.rows,
|
|
697
|
+
hasObservedCostRows: parsed.hasObservedCostRows
|
|
698
|
+
});
|
|
699
|
+
const dateRange = parsed.rows.length === 0 ? null : {
|
|
700
|
+
start: parsed.rows.map((row) => row.timestamp).sort((left, right) => new Date(left).getTime() - new Date(right).getTime())[0],
|
|
701
|
+
end: parsed.rows.map((row) => row.timestamp).sort((left, right) => new Date(left).getTime() - new Date(right).getTime()).at(-1)
|
|
702
|
+
};
|
|
703
|
+
const report = {
|
|
704
|
+
canAudit: true,
|
|
705
|
+
filePath: parsed.source.path,
|
|
706
|
+
source: parsed.source,
|
|
707
|
+
rowCount: parsed.rows.length,
|
|
708
|
+
dateRange,
|
|
709
|
+
pricingCoverage: normalized.pricingCoverage,
|
|
710
|
+
notes: []
|
|
711
|
+
};
|
|
712
|
+
report.notes = buildDoctorNotes(report);
|
|
713
|
+
options.onProgress?.(
|
|
714
|
+
`Cursor usage CSV is ready (${report.rowCount} row${report.rowCount === 1 ? "" : "s"}).`
|
|
715
|
+
);
|
|
716
|
+
return report;
|
|
717
|
+
} catch (error) {
|
|
718
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
719
|
+
options.onProgress?.(`Cursor usage CSV is not ready: ${message}`);
|
|
720
|
+
return {
|
|
721
|
+
canAudit: false,
|
|
722
|
+
filePath,
|
|
723
|
+
source: null,
|
|
724
|
+
rowCount: 0,
|
|
725
|
+
dateRange: null,
|
|
726
|
+
pricingCoverage: {
|
|
727
|
+
pricedCallCount: 0,
|
|
728
|
+
unpricedCallCount: 0,
|
|
729
|
+
pricedTokenCount: 0,
|
|
730
|
+
unpricedTokenCount: 0,
|
|
731
|
+
topUnpricedModels: []
|
|
732
|
+
},
|
|
733
|
+
notes: [message]
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
180
738
|
// ../core/src/db/client.ts
|
|
181
739
|
import { mkdirSync } from "fs";
|
|
182
740
|
import { dirname } from "path";
|
|
@@ -536,7 +1094,7 @@ var FINDING_KIND_LABELS = {
|
|
|
536
1094
|
"candidate-downgrade": "Downgrade candidates",
|
|
537
1095
|
"idle-spend": "Idle waste"
|
|
538
1096
|
};
|
|
539
|
-
function
|
|
1097
|
+
function round2(value) {
|
|
540
1098
|
return Number(value.toFixed(6));
|
|
541
1099
|
}
|
|
542
1100
|
function normalizeSinceValue(since) {
|
|
@@ -581,6 +1139,7 @@ function buildComparisonKey(input) {
|
|
|
581
1139
|
).sort();
|
|
582
1140
|
return sha1(
|
|
583
1141
|
JSON.stringify({
|
|
1142
|
+
runtime: input.runtime,
|
|
584
1143
|
kinds,
|
|
585
1144
|
roots,
|
|
586
1145
|
since: normalizeSinceValue(input.since)
|
|
@@ -600,7 +1159,7 @@ function buildTaxonomyBuckets(findings, classification) {
|
|
|
600
1159
|
spendUsd: 0,
|
|
601
1160
|
findingCount: 0
|
|
602
1161
|
};
|
|
603
|
-
current.spendUsd =
|
|
1162
|
+
current.spendUsd = round2(current.spendUsd + finding.costImpactUsd);
|
|
604
1163
|
current.findingCount += 1;
|
|
605
1164
|
buckets.set(finding.kind, current);
|
|
606
1165
|
}
|
|
@@ -618,9 +1177,9 @@ function buildTopSpendDeltas(currentRows, baselineRows) {
|
|
|
618
1177
|
const currentSpendUsd = currentMap.get(key) ?? 0;
|
|
619
1178
|
return {
|
|
620
1179
|
key,
|
|
621
|
-
baselineSpendUsd:
|
|
622
|
-
currentSpendUsd:
|
|
623
|
-
deltaSpendUsd:
|
|
1180
|
+
baselineSpendUsd: round2(baselineSpendUsd),
|
|
1181
|
+
currentSpendUsd: round2(currentSpendUsd),
|
|
1182
|
+
deltaSpendUsd: round2(currentSpendUsd - baselineSpendUsd)
|
|
624
1183
|
};
|
|
625
1184
|
}).filter((row) => row.deltaSpendUsd !== 0).sort((left, right) => Math.abs(right.deltaSpendUsd) - Math.abs(left.deltaSpendUsd)).slice(0, 3);
|
|
626
1185
|
}
|
|
@@ -655,11 +1214,11 @@ function buildFindingChanges(currentFindings, baselineFindings) {
|
|
|
655
1214
|
scope: current.scope,
|
|
656
1215
|
scopeId: current.scopeId,
|
|
657
1216
|
currentCostImpactUsd: current.costImpactUsd,
|
|
658
|
-
deltaCostImpactUsd:
|
|
1217
|
+
deltaCostImpactUsd: round2(current.costImpactUsd)
|
|
659
1218
|
});
|
|
660
1219
|
continue;
|
|
661
1220
|
}
|
|
662
|
-
const deltaCostImpactUsd =
|
|
1221
|
+
const deltaCostImpactUsd = round2(current.costImpactUsd - baseline.costImpactUsd);
|
|
663
1222
|
if (deltaCostImpactUsd > 0) {
|
|
664
1223
|
worsenedHighConfidenceWaste.push({
|
|
665
1224
|
kind: current.kind,
|
|
@@ -682,7 +1241,7 @@ function buildFindingChanges(currentFindings, baselineFindings) {
|
|
|
682
1241
|
scope: baseline.scope,
|
|
683
1242
|
scopeId: baseline.scopeId,
|
|
684
1243
|
baselineCostImpactUsd: baseline.costImpactUsd,
|
|
685
|
-
deltaCostImpactUsd:
|
|
1244
|
+
deltaCostImpactUsd: round2(-baseline.costImpactUsd)
|
|
686
1245
|
});
|
|
687
1246
|
}
|
|
688
1247
|
return {
|
|
@@ -691,17 +1250,39 @@ function buildFindingChanges(currentFindings, baselineFindings) {
|
|
|
691
1250
|
worsenedHighConfidenceWaste: sortFindingChanges(worsenedHighConfidenceWaste)
|
|
692
1251
|
};
|
|
693
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
|
+
}
|
|
694
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
|
+
}));
|
|
695
1269
|
return {
|
|
696
1270
|
...summary,
|
|
697
|
-
|
|
698
|
-
|
|
1271
|
+
runtime,
|
|
1272
|
+
sourceFiles: hydratedSources,
|
|
1273
|
+
comparisonKey: shouldRebuildComparisonKey ? buildComparisonKey({
|
|
1274
|
+
runtime,
|
|
1275
|
+
sources: hydratedSources,
|
|
699
1276
|
since: summary.since
|
|
700
|
-
}),
|
|
1277
|
+
}) : summary.comparisonKey,
|
|
701
1278
|
comparison: summary.comparison ?? null,
|
|
702
1279
|
wasteByKind: summary.wasteByKind?.length > 0 ? summary.wasteByKind : buildTaxonomyBuckets(summary.findings, "waste"),
|
|
703
1280
|
opportunityByKind: summary.opportunityByKind?.length > 0 ? summary.opportunityByKind : buildTaxonomyBuckets(summary.findings, "opportunity"),
|
|
704
|
-
|
|
1281
|
+
spendByDay: summary.spendByDay ?? [],
|
|
1282
|
+
wasteByDay: summary.wasteByDay ?? [],
|
|
1283
|
+
notes: summary.notes ?? [],
|
|
1284
|
+
pricingCoverage: summary.pricingCoverage ?? null,
|
|
1285
|
+
cursorUsage: summary.cursorUsage ?? null
|
|
705
1286
|
};
|
|
706
1287
|
}
|
|
707
1288
|
function buildAuditComparison(current, baseline) {
|
|
@@ -718,12 +1299,12 @@ function buildAuditComparison(current, baseline) {
|
|
|
718
1299
|
baselineWasteSpendUsd: baseline.wasteSpendUsd,
|
|
719
1300
|
baselineOpportunitySpendUsd: baseline.opportunitySpendUsd,
|
|
720
1301
|
baselineStructuralWasteRate: baseline.structuralWasteRate,
|
|
721
|
-
deltaTotalSpendUsd:
|
|
722
|
-
deltaObservedSpendUsd:
|
|
723
|
-
deltaEstimatedSpendUsd:
|
|
724
|
-
deltaWasteSpendUsd:
|
|
725
|
-
deltaOpportunitySpendUsd:
|
|
726
|
-
deltaStructuralWasteRate:
|
|
1302
|
+
deltaTotalSpendUsd: round2(current.totalSpendUsd - baseline.totalSpendUsd),
|
|
1303
|
+
deltaObservedSpendUsd: round2(current.observedSpendUsd - baseline.observedSpendUsd),
|
|
1304
|
+
deltaEstimatedSpendUsd: round2(current.estimatedSpendUsd - baseline.estimatedSpendUsd),
|
|
1305
|
+
deltaWasteSpendUsd: round2(current.wasteSpendUsd - baseline.wasteSpendUsd),
|
|
1306
|
+
deltaOpportunitySpendUsd: round2(current.opportunitySpendUsd - baseline.opportunitySpendUsd),
|
|
1307
|
+
deltaStructuralWasteRate: round2(current.structuralWasteRate - baseline.structuralWasteRate),
|
|
727
1308
|
deltaRunCount: current.runCount - baseline.runCount,
|
|
728
1309
|
deltaCallCount: current.callCount - baseline.callCount,
|
|
729
1310
|
workflowDeltas,
|
|
@@ -767,189 +1348,137 @@ function readLatestComparableAuditSummary(input) {
|
|
|
767
1348
|
});
|
|
768
1349
|
}
|
|
769
1350
|
|
|
770
|
-
// ../core/src/
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
// ../core/src/utils/paths.ts
|
|
775
|
-
import { mkdirSync as mkdirSync2 } from "fs";
|
|
776
|
-
import { homedir } from "os";
|
|
777
|
-
import { join } from "path";
|
|
778
|
-
import { platform } from "process";
|
|
779
|
-
function getAppPaths() {
|
|
780
|
-
const home = homedir();
|
|
781
|
-
return platform === "darwin" ? {
|
|
782
|
-
data: join(home, "Library", "Application Support", "xerg"),
|
|
783
|
-
config: join(home, "Library", "Preferences", "xerg"),
|
|
784
|
-
cache: join(home, "Library", "Caches", "xerg")
|
|
785
|
-
} : platform === "win32" ? {
|
|
786
|
-
data: join(process.env.LOCALAPPDATA ?? join(home, "AppData", "Local"), "xerg", "Data"),
|
|
787
|
-
config: join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "xerg", "Config"),
|
|
788
|
-
cache: join(process.env.LOCALAPPDATA ?? join(home, "AppData", "Local"), "xerg", "Cache")
|
|
789
|
-
} : {
|
|
790
|
-
data: join(process.env.XDG_DATA_HOME ?? join(home, ".local", "share"), "xerg"),
|
|
791
|
-
config: join(process.env.XDG_CONFIG_HOME ?? join(home, ".config"), "xerg"),
|
|
792
|
-
cache: join(process.env.XDG_CACHE_HOME ?? join(home, ".cache"), "xerg")
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
function getDefaultDbPath() {
|
|
796
|
-
return join(getAppPaths().data, "xerg.db");
|
|
1351
|
+
// ../core/src/findings/cursor.ts
|
|
1352
|
+
function round3(value) {
|
|
1353
|
+
return Number(value.toFixed(6));
|
|
797
1354
|
}
|
|
798
|
-
function
|
|
799
|
-
return
|
|
1355
|
+
function createFinding(input) {
|
|
1356
|
+
return {
|
|
1357
|
+
...input,
|
|
1358
|
+
id: sha1(
|
|
1359
|
+
`${input.kind}:${input.scope}:${input.scopeId}:${input.title}:${input.costImpactUsd}:${input.summary}`
|
|
1360
|
+
)
|
|
1361
|
+
};
|
|
800
1362
|
}
|
|
801
|
-
function
|
|
802
|
-
return "
|
|
1363
|
+
function asNumber(value) {
|
|
1364
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
803
1365
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
function toDetected(path, kind) {
|
|
807
|
-
try {
|
|
808
|
-
const stats = statSync(path);
|
|
809
|
-
if (!stats.isFile()) {
|
|
810
|
-
return null;
|
|
811
|
-
}
|
|
812
|
-
return {
|
|
813
|
-
kind,
|
|
814
|
-
path,
|
|
815
|
-
sizeBytes: stats.size,
|
|
816
|
-
mtimeMs: stats.mtimeMs
|
|
817
|
-
};
|
|
818
|
-
} catch {
|
|
819
|
-
return null;
|
|
820
|
-
}
|
|
1366
|
+
function asBoolean(value) {
|
|
1367
|
+
return value === true;
|
|
821
1368
|
}
|
|
822
|
-
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
explicitSources.push(detected2);
|
|
828
|
-
}
|
|
1369
|
+
function buildCursorUsageFindings(runs) {
|
|
1370
|
+
const calls = runs.flatMap((run2) => run2.calls);
|
|
1371
|
+
const billableCalls = calls.filter((call) => call.costUsd > 0);
|
|
1372
|
+
if (billableCalls.length === 0) {
|
|
1373
|
+
return { findings: [], wasteAttributions: [] };
|
|
829
1374
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1375
|
+
const cacheAwareCalls = billableCalls.filter((call) => {
|
|
1376
|
+
return asNumber(call.metadata.cacheReadTokens) > 0;
|
|
1377
|
+
});
|
|
1378
|
+
if (cacheAwareCalls.length === 0) {
|
|
1379
|
+
return { findings: [], wasteAttributions: [] };
|
|
1380
|
+
}
|
|
1381
|
+
const totalSpendUsd = billableCalls.reduce((sum, call) => sum + call.costUsd, 0);
|
|
1382
|
+
const totalInputTokens = billableCalls.reduce((sum, call) => sum + call.inputTokens, 0);
|
|
1383
|
+
const totalCacheReadTokens = cacheAwareCalls.reduce(
|
|
1384
|
+
(sum, call) => sum + asNumber(call.metadata.cacheReadTokens),
|
|
1385
|
+
0
|
|
1386
|
+
);
|
|
1387
|
+
const totalCacheWriteTokens = billableCalls.reduce(
|
|
1388
|
+
(sum, call) => sum + asNumber(call.metadata.inputWithCacheWriteTokens),
|
|
1389
|
+
0
|
|
1390
|
+
);
|
|
1391
|
+
const cacheSpendUsd = cacheAwareCalls.reduce((sum, call) => sum + (call.cacheCostUsd ?? 0), 0);
|
|
1392
|
+
const cacheWriteSpendUsd = billableCalls.reduce(
|
|
1393
|
+
(sum, call) => sum + asNumber(call.metadata.cacheWriteCostUsd),
|
|
1394
|
+
0
|
|
1395
|
+
);
|
|
1396
|
+
const coveredSpendUsd = billableCalls.filter((call) => call.cacheCostUsd !== null).reduce((sum, call) => sum + call.costUsd, 0);
|
|
1397
|
+
const maxModeSpendUsd = billableCalls.filter((call) => asBoolean(call.metadata.maxMode)).reduce((sum, call) => sum + call.costUsd, 0);
|
|
1398
|
+
const cacheReadShare = totalInputTokens === 0 ? 0 : totalCacheReadTokens / totalInputTokens;
|
|
1399
|
+
const cacheCoverageShare = totalSpendUsd === 0 ? 0 : coveredSpendUsd / totalSpendUsd;
|
|
1400
|
+
const maxModeSpendShare = totalSpendUsd === 0 ? 0 : maxModeSpendUsd / totalSpendUsd;
|
|
1401
|
+
const cacheImpactUsd = round3(cacheSpendUsd + cacheWriteSpendUsd);
|
|
1402
|
+
const meetsWasteBar = cacheImpactUsd >= 25 && cacheReadShare >= 0.6 && cacheAwareCalls.length >= 20 && cacheCoverageShare >= 0.4;
|
|
1403
|
+
const meetsOpportunityBar = cacheImpactUsd >= 5 && cacheReadShare >= 0.35 && cacheAwareCalls.length >= 10 && cacheCoverageShare >= 0.25;
|
|
1404
|
+
if (!meetsWasteBar && !meetsOpportunityBar) {
|
|
1405
|
+
return { findings: [], wasteAttributions: [] };
|
|
1406
|
+
}
|
|
1407
|
+
const classification = meetsWasteBar ? "waste" : "opportunity";
|
|
1408
|
+
const confidence = cacheReadShare >= 0.8 && cacheAwareCalls.length >= 50 && cacheCoverageShare >= 0.5 ? "high" : cacheReadShare >= 0.5 && cacheAwareCalls.length >= 20 ? "medium" : "low";
|
|
1409
|
+
const summary = classification === "waste" ? `Xerg estimated ${cacheImpactUsd.toFixed(2)} USD of billed spend was driven by repeatedly replaying cached context across ${cacheAwareCalls.length} paid row${cacheAwareCalls.length === 1 ? "" : "s"}. This pattern is consistent with long chats carrying more history than needed.` : `Xerg estimated ${cacheImpactUsd.toFixed(2)} USD of billed spend was tied to cached context replay across ${cacheAwareCalls.length} paid row${cacheAwareCalls.length === 1 ? "" : "s"}. Summarizing and resetting long chats could reduce this carryover cost.`;
|
|
1410
|
+
const findings = [
|
|
1411
|
+
createFinding({
|
|
1412
|
+
classification,
|
|
1413
|
+
confidence,
|
|
1414
|
+
kind: "cache-carryover",
|
|
1415
|
+
title: classification === "waste" ? "Cached context carryover is driving avoidable spend" : "Cached context carryover looks like a strong cost-reduction opportunity",
|
|
1416
|
+
summary,
|
|
1417
|
+
scope: "global",
|
|
1418
|
+
scopeId: "all",
|
|
1419
|
+
costImpactUsd: cacheImpactUsd,
|
|
1420
|
+
details: {
|
|
1421
|
+
cacheReadShare: round3(cacheReadShare),
|
|
1422
|
+
cacheCoverageShare: round3(cacheCoverageShare),
|
|
1423
|
+
totalCacheReadTokens,
|
|
1424
|
+
totalCacheWriteTokens,
|
|
1425
|
+
billableCallCount: billableCalls.length,
|
|
1426
|
+
cacheAwareCallCount: cacheAwareCalls.length,
|
|
1427
|
+
maxModeSpendShare: round3(maxModeSpendShare),
|
|
1428
|
+
estimatedCacheReadSpendUsd: round3(cacheSpendUsd),
|
|
1429
|
+
estimatedCacheWriteSpendUsd: round3(cacheWriteSpendUsd)
|
|
839
1430
|
}
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
if (explicitSources.length > 0) {
|
|
843
|
-
return explicitSources.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
844
|
-
}
|
|
845
|
-
const [gatewayMatches, sessionMatches] = await Promise.all([
|
|
846
|
-
collectGlobMatches(getDefaultGatewayPattern()),
|
|
847
|
-
collectGlobMatches(getDefaultSessionsPattern())
|
|
848
|
-
]);
|
|
849
|
-
const detected = [
|
|
850
|
-
...gatewayMatches.map((path) => toDetected(path, "gateway")).filter(Boolean),
|
|
851
|
-
...sessionMatches.map((path) => toDetected(path, "sessions")).filter(Boolean)
|
|
1431
|
+
})
|
|
852
1432
|
];
|
|
853
|
-
|
|
1433
|
+
const maxModeCalls = billableCalls.filter((call) => asBoolean(call.metadata.maxMode));
|
|
1434
|
+
const maxModeCallShare = billableCalls.length === 0 ? 0 : maxModeCalls.length / billableCalls.length;
|
|
1435
|
+
if (maxModeSpendShare >= 0.6 && maxModeSpendUsd >= 25 && maxModeCalls.length >= 10) {
|
|
1436
|
+
const maxModeConfidence = maxModeSpendShare >= 0.85 && maxModeCalls.length >= 50 ? "high" : maxModeSpendShare >= 0.7 && maxModeCalls.length >= 20 ? "medium" : "low";
|
|
1437
|
+
findings.push(
|
|
1438
|
+
createFinding({
|
|
1439
|
+
classification: "opportunity",
|
|
1440
|
+
confidence: maxModeConfidence,
|
|
1441
|
+
kind: "max-mode-concentration",
|
|
1442
|
+
title: "Max mode is concentrated in the billed spend mix",
|
|
1443
|
+
summary: `Max mode accounts for ${(maxModeSpendShare * 100).toFixed(0)}% of billed spend across ${maxModeCalls.length} paid row${maxModeCalls.length === 1 ? "" : "s"}. This is a strong candidate for splitting work between premium and standard passes.`,
|
|
1444
|
+
scope: "global",
|
|
1445
|
+
scopeId: "all",
|
|
1446
|
+
costImpactUsd: round3(maxModeSpendUsd * 0.2),
|
|
1447
|
+
details: {
|
|
1448
|
+
maxModeSpendUsd: round3(maxModeSpendUsd),
|
|
1449
|
+
maxModeSpendShare: round3(maxModeSpendShare),
|
|
1450
|
+
maxModeCallCount: maxModeCalls.length,
|
|
1451
|
+
maxModeCallShare: round3(maxModeCallShare)
|
|
1452
|
+
}
|
|
1453
|
+
})
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
const wasteAttributions = classification === "waste" ? billableCalls.map((call) => ({
|
|
1457
|
+
kind: "cache-carryover",
|
|
1458
|
+
timestamp: call.timestamp,
|
|
1459
|
+
wasteUsd: round3((call.cacheCostUsd ?? 0) + asNumber(call.metadata.cacheWriteCostUsd))
|
|
1460
|
+
})).filter((attribution) => attribution.wasteUsd > 0) : [];
|
|
1461
|
+
return {
|
|
1462
|
+
findings: findings.sort((left, right) => right.costImpactUsd - left.costImpactUsd),
|
|
1463
|
+
wasteAttributions
|
|
1464
|
+
};
|
|
854
1465
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1466
|
+
|
|
1467
|
+
// ../core/src/findings/engine.ts
|
|
1468
|
+
function createFinding2(input) {
|
|
1469
|
+
return {
|
|
1470
|
+
...input,
|
|
1471
|
+
id: sha1(
|
|
1472
|
+
`${input.kind}:${input.scope}:${input.scopeId}:${input.title}:${input.costImpactUsd}:${input.summary}`
|
|
1473
|
+
)
|
|
1474
|
+
};
|
|
863
1475
|
}
|
|
864
|
-
function
|
|
865
|
-
if (segments.length === 0) {
|
|
866
|
-
return [currentPath];
|
|
867
|
-
}
|
|
868
|
-
const [segment, ...rest] = segments;
|
|
869
|
-
if (segment === "**") {
|
|
870
|
-
const matches2 = collectMatchesFromSegments(currentPath, rest);
|
|
871
|
-
for (const entry of readDirSafe(currentPath)) {
|
|
872
|
-
if (entry.isDirectory()) {
|
|
873
|
-
matches2.push(...collectMatchesFromSegments(join2(currentPath, entry.name), segments));
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
return matches2;
|
|
877
|
-
}
|
|
878
|
-
const matches = [];
|
|
879
|
-
const matcher = segmentToRegExp(segment);
|
|
880
|
-
for (const entry of readDirSafe(currentPath)) {
|
|
881
|
-
if (!matcher.test(entry.name)) {
|
|
882
|
-
continue;
|
|
883
|
-
}
|
|
884
|
-
const nextPath = join2(currentPath, entry.name);
|
|
885
|
-
if (rest.length === 0) {
|
|
886
|
-
matches.push(nextPath);
|
|
887
|
-
continue;
|
|
888
|
-
}
|
|
889
|
-
if (entry.isDirectory()) {
|
|
890
|
-
matches.push(...collectMatchesFromSegments(nextPath, rest));
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
return matches;
|
|
894
|
-
}
|
|
895
|
-
function readDirSafe(path) {
|
|
896
|
-
try {
|
|
897
|
-
return readdirSync(path, { withFileTypes: true });
|
|
898
|
-
} catch {
|
|
899
|
-
return [];
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
function segmentToRegExp(segment) {
|
|
903
|
-
const escaped = segment.replaceAll(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
|
|
904
|
-
return new RegExp(`^${escaped}$`);
|
|
905
|
-
}
|
|
906
|
-
async function inspectOpenClawSources(options) {
|
|
907
|
-
options.onProgress?.("Checking local OpenClaw defaults...");
|
|
908
|
-
const sources = await detectOpenClawSources(options);
|
|
909
|
-
const notes = [];
|
|
910
|
-
options.onProgress?.(
|
|
911
|
-
sources.length > 0 ? `Detected ${sources.length} local source file${sources.length === 1 ? "" : "s"}.` : "No local OpenClaw source files were detected."
|
|
912
|
-
);
|
|
913
|
-
if (sources.length === 0) {
|
|
914
|
-
notes.push("No OpenClaw gateway logs or session files were detected.");
|
|
915
|
-
notes.push(
|
|
916
|
-
"Doctor checks local defaults by default. Use --remote or --railway to inspect remote targets."
|
|
917
|
-
);
|
|
918
|
-
notes.push(
|
|
919
|
-
"Use --log-file or --sessions-dir if your OpenClaw data lives outside the defaults."
|
|
920
|
-
);
|
|
921
|
-
}
|
|
922
|
-
if (sources.some((source) => source.kind === "gateway")) {
|
|
923
|
-
notes.push("Gateway logs detected. These are preferred when cost metadata is present.");
|
|
924
|
-
}
|
|
925
|
-
if (sources.some((source) => source.kind === "sessions")) {
|
|
926
|
-
notes.push("Session transcript fallback detected. Xerg will extract usage metadata only.");
|
|
927
|
-
}
|
|
928
|
-
return {
|
|
929
|
-
canAudit: sources.length > 0,
|
|
930
|
-
sources,
|
|
931
|
-
defaults: {
|
|
932
|
-
gatewayPattern: getDefaultGatewayPattern(),
|
|
933
|
-
sessionsPattern: getDefaultSessionsPattern()
|
|
934
|
-
},
|
|
935
|
-
notes
|
|
936
|
-
};
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// ../core/src/findings/engine.ts
|
|
940
|
-
function createFinding(input) {
|
|
941
|
-
return {
|
|
942
|
-
...input,
|
|
943
|
-
id: sha1(
|
|
944
|
-
`${input.kind}:${input.scope}:${input.scopeId}:${input.title}:${input.costImpactUsd}:${input.summary}`
|
|
945
|
-
)
|
|
946
|
-
};
|
|
947
|
-
}
|
|
948
|
-
function round2(value) {
|
|
1476
|
+
function round4(value) {
|
|
949
1477
|
return Number(value.toFixed(6));
|
|
950
1478
|
}
|
|
951
1479
|
function buildFindings(runs) {
|
|
952
1480
|
const findings = [];
|
|
1481
|
+
const wasteAttributions = [];
|
|
953
1482
|
const allCalls = runs.flatMap((run2) => run2.calls.map((call) => ({ run: run2, call })));
|
|
954
1483
|
const retryCandidates = allCalls.filter(({ call }) => {
|
|
955
1484
|
const status = (call.status ?? "").toLowerCase();
|
|
@@ -957,8 +1486,15 @@ function buildFindings(runs) {
|
|
|
957
1486
|
});
|
|
958
1487
|
const retryCost = retryCandidates.reduce((sum, item) => sum + item.call.costUsd, 0);
|
|
959
1488
|
if (retryCost > 0) {
|
|
1489
|
+
wasteAttributions.push(
|
|
1490
|
+
...retryCandidates.map(({ call }) => ({
|
|
1491
|
+
kind: "retry-waste",
|
|
1492
|
+
timestamp: call.timestamp,
|
|
1493
|
+
wasteUsd: call.costUsd
|
|
1494
|
+
}))
|
|
1495
|
+
);
|
|
960
1496
|
findings.push(
|
|
961
|
-
|
|
1497
|
+
createFinding2({
|
|
962
1498
|
classification: "waste",
|
|
963
1499
|
confidence: "high",
|
|
964
1500
|
kind: "retry-waste",
|
|
@@ -966,7 +1502,7 @@ function buildFindings(runs) {
|
|
|
966
1502
|
summary: `${retryCandidates.length} failed call${retryCandidates.length === 1 ? "" : "s"} were followed by additional work, making their spend pure retry overhead.`,
|
|
967
1503
|
scope: "global",
|
|
968
1504
|
scopeId: "all",
|
|
969
|
-
costImpactUsd:
|
|
1505
|
+
costImpactUsd: round4(retryCost),
|
|
970
1506
|
details: {
|
|
971
1507
|
failedCallCount: retryCandidates.length
|
|
972
1508
|
}
|
|
@@ -978,8 +1514,15 @@ function buildFindings(runs) {
|
|
|
978
1514
|
if (maxIteration >= 7) {
|
|
979
1515
|
const loopCalls = run2.calls.filter((call) => (call.iteration ?? 0) > 5);
|
|
980
1516
|
const loopCost = loopCalls.reduce((sum, call) => sum + call.costUsd, 0);
|
|
1517
|
+
wasteAttributions.push(
|
|
1518
|
+
...loopCalls.map((call) => ({
|
|
1519
|
+
kind: "loop-waste",
|
|
1520
|
+
timestamp: call.timestamp,
|
|
1521
|
+
wasteUsd: call.costUsd
|
|
1522
|
+
}))
|
|
1523
|
+
);
|
|
981
1524
|
findings.push(
|
|
982
|
-
|
|
1525
|
+
createFinding2({
|
|
983
1526
|
classification: "waste",
|
|
984
1527
|
confidence: "high",
|
|
985
1528
|
kind: "loop-waste",
|
|
@@ -987,7 +1530,7 @@ function buildFindings(runs) {
|
|
|
987
1530
|
summary: `This run reached ${maxIteration} iterations. Xerg treats the spend after iteration 5 as likely loop waste.`,
|
|
988
1531
|
scope: "run",
|
|
989
1532
|
scopeId: run2.id,
|
|
990
|
-
costImpactUsd:
|
|
1533
|
+
costImpactUsd: round4(loopCost),
|
|
991
1534
|
details: {
|
|
992
1535
|
workflow: run2.workflow,
|
|
993
1536
|
maxIteration
|
|
@@ -1015,7 +1558,7 @@ function buildFindings(runs) {
|
|
|
1015
1558
|
if (outlierRuns.length > 0) {
|
|
1016
1559
|
const outlierCost = outlierRuns.reduce((sum, run2) => sum + run2.totalCostUsd, 0);
|
|
1017
1560
|
findings.push(
|
|
1018
|
-
|
|
1561
|
+
createFinding2({
|
|
1019
1562
|
classification: "opportunity",
|
|
1020
1563
|
confidence: "medium",
|
|
1021
1564
|
kind: "context-outlier",
|
|
@@ -1023,10 +1566,10 @@ function buildFindings(runs) {
|
|
|
1023
1566
|
summary: `Xerg found ${outlierRuns.length} run${outlierRuns.length === 1 ? "" : "s"} in this workflow with input token volume far above the workflow average.`,
|
|
1024
1567
|
scope: "workflow",
|
|
1025
1568
|
scopeId: workflow,
|
|
1026
|
-
costImpactUsd:
|
|
1569
|
+
costImpactUsd: round4(outlierCost),
|
|
1027
1570
|
details: {
|
|
1028
1571
|
workflow,
|
|
1029
|
-
averageInputTokens:
|
|
1572
|
+
averageInputTokens: round4(average),
|
|
1030
1573
|
outlierRunCount: outlierRuns.length
|
|
1031
1574
|
}
|
|
1032
1575
|
})
|
|
@@ -1039,7 +1582,7 @@ function buildFindings(runs) {
|
|
|
1039
1582
|
if (idleRuns.length > 0) {
|
|
1040
1583
|
const idleCost = idleRuns.reduce((sum, run2) => sum + run2.totalCostUsd, 0);
|
|
1041
1584
|
findings.push(
|
|
1042
|
-
|
|
1585
|
+
createFinding2({
|
|
1043
1586
|
classification: "opportunity",
|
|
1044
1587
|
confidence: "medium",
|
|
1045
1588
|
kind: "idle-spend",
|
|
@@ -1047,7 +1590,7 @@ function buildFindings(runs) {
|
|
|
1047
1590
|
summary: "This workflow name looks like a recurring heartbeat or monitoring loop. Review whether the cadence and model tier are justified.",
|
|
1048
1591
|
scope: "workflow",
|
|
1049
1592
|
scopeId: workflow,
|
|
1050
|
-
costImpactUsd:
|
|
1593
|
+
costImpactUsd: round4(idleCost),
|
|
1051
1594
|
details: {
|
|
1052
1595
|
workflow
|
|
1053
1596
|
}
|
|
@@ -1060,7 +1603,7 @@ function buildFindings(runs) {
|
|
|
1060
1603
|
if (downgradeCalls.length > 0) {
|
|
1061
1604
|
const spend = downgradeCalls.reduce((sum, call) => sum + call.costUsd, 0);
|
|
1062
1605
|
findings.push(
|
|
1063
|
-
|
|
1606
|
+
createFinding2({
|
|
1064
1607
|
classification: "opportunity",
|
|
1065
1608
|
confidence: "low",
|
|
1066
1609
|
kind: "candidate-downgrade",
|
|
@@ -1068,23 +1611,22 @@ function buildFindings(runs) {
|
|
|
1068
1611
|
summary: "An expensive model is being used on a workflow that looks operationally simple. Treat this as an A/B test candidate, not proven waste.",
|
|
1069
1612
|
scope: "workflow",
|
|
1070
1613
|
scopeId: workflow,
|
|
1071
|
-
costImpactUsd:
|
|
1614
|
+
costImpactUsd: round4(spend * 0.3),
|
|
1072
1615
|
details: {
|
|
1073
1616
|
workflow,
|
|
1074
1617
|
expensiveCallCount: downgradeCalls.length,
|
|
1075
|
-
inspectedSpendUsd:
|
|
1618
|
+
inspectedSpendUsd: round4(spend)
|
|
1076
1619
|
}
|
|
1077
1620
|
})
|
|
1078
1621
|
);
|
|
1079
1622
|
}
|
|
1080
1623
|
}
|
|
1081
|
-
return
|
|
1624
|
+
return {
|
|
1625
|
+
findings: findings.sort((left, right) => right.costImpactUsd - left.costImpactUsd),
|
|
1626
|
+
wasteAttributions
|
|
1627
|
+
};
|
|
1082
1628
|
}
|
|
1083
1629
|
|
|
1084
|
-
// ../core/src/normalize/openclaw.ts
|
|
1085
|
-
import { readFileSync } from "fs";
|
|
1086
|
-
import { basename } from "path";
|
|
1087
|
-
|
|
1088
1630
|
// ../core/src/pricing-catalog.ts
|
|
1089
1631
|
var PRICING_CATALOG = [
|
|
1090
1632
|
{
|
|
@@ -1161,6 +1703,223 @@ function estimateCostUsd(provider, model, inputTokens, outputTokens) {
|
|
|
1161
1703
|
return Number((inputCost + outputCost).toFixed(8));
|
|
1162
1704
|
}
|
|
1163
1705
|
|
|
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())) {
|
|
1713
|
+
return null;
|
|
1714
|
+
}
|
|
1715
|
+
return candidate.toISOString().slice(0, 10);
|
|
1716
|
+
}
|
|
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);
|
|
1721
|
+
}
|
|
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 [];
|
|
1726
|
+
}
|
|
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;
|
|
1735
|
+
}
|
|
1736
|
+
function reconcileDailyTotal(rows, key, expected) {
|
|
1737
|
+
if (rows.length === 0) {
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
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;
|
|
1746
|
+
}
|
|
1747
|
+
const last = rows[rows.length - 1];
|
|
1748
|
+
const current = last[key];
|
|
1749
|
+
if (typeof current === "number") {
|
|
1750
|
+
last[key] = round5(current + delta);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
function buildSpendByDay(runs) {
|
|
1754
|
+
const days = buildObservedUtcDayRange(runs);
|
|
1755
|
+
if (days.length === 0) {
|
|
1756
|
+
return [];
|
|
1757
|
+
}
|
|
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
|
+
|
|
1164
1923
|
// ../core/src/utils/records.ts
|
|
1165
1924
|
function getNestedValue(input, paths) {
|
|
1166
1925
|
if (!input || typeof input !== "object") {
|
|
@@ -1182,7 +1941,7 @@ function getNestedValue(input, paths) {
|
|
|
1182
1941
|
}
|
|
1183
1942
|
return null;
|
|
1184
1943
|
}
|
|
1185
|
-
function
|
|
1944
|
+
function asNumber2(value) {
|
|
1186
1945
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1187
1946
|
return value;
|
|
1188
1947
|
}
|
|
@@ -1198,7 +1957,7 @@ function asString(value) {
|
|
|
1198
1957
|
}
|
|
1199
1958
|
return null;
|
|
1200
1959
|
}
|
|
1201
|
-
function
|
|
1960
|
+
function asBoolean2(value) {
|
|
1202
1961
|
if (typeof value === "boolean") {
|
|
1203
1962
|
return value;
|
|
1204
1963
|
}
|
|
@@ -1221,39 +1980,119 @@ function pickMetadata(input, keys) {
|
|
|
1221
1980
|
return output;
|
|
1222
1981
|
}
|
|
1223
1982
|
|
|
1224
|
-
// ../core/src/normalize/
|
|
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
|
+
}
|
|
1225
2005
|
function parseJsonLines(path) {
|
|
1226
|
-
const content =
|
|
2006
|
+
const content = readFileSync2(path, "utf8");
|
|
1227
2007
|
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1228
2008
|
const records = [];
|
|
1229
2009
|
for (const line of lines) {
|
|
1230
|
-
|
|
1231
|
-
|
|
2010
|
+
const parsed = parseJsonLine(line);
|
|
2011
|
+
if (parsed) {
|
|
1232
2012
|
records.push(parsed);
|
|
1233
|
-
} catch {
|
|
1234
2013
|
}
|
|
1235
2014
|
}
|
|
1236
2015
|
return records;
|
|
1237
2016
|
}
|
|
1238
|
-
function
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
+
}));
|
|
1242
2038
|
}
|
|
1243
|
-
function
|
|
1244
|
-
return asString(getNestedValue(record, [["
|
|
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
|
+
};
|
|
1245
2078
|
}
|
|
1246
2079
|
function inferWorkflow(record, sourcePath) {
|
|
1247
2080
|
return asString(
|
|
1248
2081
|
getNestedValue(record, [
|
|
1249
2082
|
["workflow"],
|
|
1250
|
-
["
|
|
1251
|
-
["
|
|
2083
|
+
["job_name"],
|
|
2084
|
+
["task_class"],
|
|
2085
|
+
["taskClass"],
|
|
2086
|
+
["agent_name"],
|
|
1252
2087
|
["agent", "name"],
|
|
1253
|
-
["
|
|
1254
|
-
["
|
|
2088
|
+
["source"],
|
|
2089
|
+
["session", "source"],
|
|
2090
|
+
["session_key"],
|
|
2091
|
+
["session_id"],
|
|
2092
|
+
["sessionId"],
|
|
2093
|
+
["title"]
|
|
1255
2094
|
])
|
|
1256
|
-
) ?? basename(sourcePath, "
|
|
2095
|
+
) ?? basename(sourcePath).replace(/\.(jsonl|json|log)(\.\d+)?$/i, "");
|
|
1257
2096
|
}
|
|
1258
2097
|
function inferEnvironment(record) {
|
|
1259
2098
|
return asString(getNestedValue(record, [["environment"], ["env"], ["metadata", "environment"]])) ?? "local";
|
|
@@ -1263,18 +2102,506 @@ function inferRunKey(record, workflow, index, sourcePath) {
|
|
|
1263
2102
|
getNestedValue(record, [
|
|
1264
2103
|
["run_id"],
|
|
1265
2104
|
["runId"],
|
|
2105
|
+
["session_id"],
|
|
2106
|
+
["sessionId"],
|
|
2107
|
+
["session_key"],
|
|
2108
|
+
["thread_id"],
|
|
2109
|
+
["threadId"],
|
|
1266
2110
|
["trace_id"],
|
|
1267
2111
|
["traceId"],
|
|
1268
|
-
["
|
|
1269
|
-
["
|
|
2112
|
+
["conversation_id"],
|
|
2113
|
+
["conversationId"],
|
|
2114
|
+
["id"]
|
|
1270
2115
|
])
|
|
1271
2116
|
) ?? `${sourcePath}:${workflow}:${index}`;
|
|
1272
2117
|
}
|
|
1273
2118
|
function inferTaskClass(record, workflow) {
|
|
1274
|
-
return asString(
|
|
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;
|
|
1275
2147
|
}
|
|
1276
2148
|
function extractUsage(record) {
|
|
1277
|
-
const inputTokens =
|
|
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);
|
|
2560
|
+
} catch {
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
return records;
|
|
2564
|
+
}
|
|
2565
|
+
function inferProvider2(record) {
|
|
2566
|
+
return asString(
|
|
2567
|
+
getNestedValue(record, [["provider"], ["message", "provider"], ["usage", "provider"]])
|
|
2568
|
+
) ?? "unknown";
|
|
2569
|
+
}
|
|
2570
|
+
function inferModel(record) {
|
|
2571
|
+
return asString(getNestedValue(record, [["model"], ["message", "model"], ["usage", "model"]])) ?? "unknown-model";
|
|
2572
|
+
}
|
|
2573
|
+
function inferWorkflow2(record, sourcePath) {
|
|
2574
|
+
return asString(
|
|
2575
|
+
getNestedValue(record, [
|
|
2576
|
+
["workflow"],
|
|
2577
|
+
["session", "workflow"],
|
|
2578
|
+
["metadata", "workflow"],
|
|
2579
|
+
["agent", "name"],
|
|
2580
|
+
["agentId"],
|
|
2581
|
+
["sessionId"]
|
|
2582
|
+
])
|
|
2583
|
+
) ?? basename3(sourcePath, ".jsonl");
|
|
2584
|
+
}
|
|
2585
|
+
function inferEnvironment2(record) {
|
|
2586
|
+
return asString(getNestedValue(record, [["environment"], ["env"], ["metadata", "environment"]])) ?? "local";
|
|
2587
|
+
}
|
|
2588
|
+
function inferRunKey2(record, workflow, index, sourcePath) {
|
|
2589
|
+
return asString(
|
|
2590
|
+
getNestedValue(record, [
|
|
2591
|
+
["run_id"],
|
|
2592
|
+
["runId"],
|
|
2593
|
+
["trace_id"],
|
|
2594
|
+
["traceId"],
|
|
2595
|
+
["sessionId"],
|
|
2596
|
+
["thread_id"]
|
|
2597
|
+
])
|
|
2598
|
+
) ?? `${sourcePath}:${workflow}:${index}`;
|
|
2599
|
+
}
|
|
2600
|
+
function inferTaskClass2(record, workflow) {
|
|
2601
|
+
return asString(getNestedValue(record, [["task_class"], ["taskClass"], ["metadata", "taskClass"]])) ?? workflow.toLowerCase();
|
|
2602
|
+
}
|
|
2603
|
+
function extractUsage2(record) {
|
|
2604
|
+
const inputTokens = asNumber2(
|
|
1278
2605
|
getNestedValue(record, [
|
|
1279
2606
|
["input_tokens"],
|
|
1280
2607
|
["inputTokens"],
|
|
@@ -1286,7 +2613,7 @@ function extractUsage(record) {
|
|
|
1286
2613
|
["message", "usage", "prompt_tokens"]
|
|
1287
2614
|
])
|
|
1288
2615
|
) ?? 0;
|
|
1289
|
-
const outputTokens =
|
|
2616
|
+
const outputTokens = asNumber2(
|
|
1290
2617
|
getNestedValue(record, [
|
|
1291
2618
|
["output_tokens"],
|
|
1292
2619
|
["outputTokens"],
|
|
@@ -1298,7 +2625,7 @@ function extractUsage(record) {
|
|
|
1298
2625
|
["message", "usage", "completion_tokens"]
|
|
1299
2626
|
])
|
|
1300
2627
|
) ?? 0;
|
|
1301
|
-
const observedCost =
|
|
2628
|
+
const observedCost = asNumber2(
|
|
1302
2629
|
getNestedValue(record, [
|
|
1303
2630
|
["cost_usd"],
|
|
1304
2631
|
["costUsd"],
|
|
@@ -1316,22 +2643,22 @@ function extractUsage(record) {
|
|
|
1316
2643
|
observedCost
|
|
1317
2644
|
};
|
|
1318
2645
|
}
|
|
1319
|
-
function
|
|
1320
|
-
const provider =
|
|
2646
|
+
function buildCall3(source, record, runId, index) {
|
|
2647
|
+
const provider = inferProvider2(record);
|
|
1321
2648
|
const model = inferModel(record);
|
|
1322
|
-
const workflow =
|
|
1323
|
-
const { inputTokens, outputTokens, observedCost } =
|
|
2649
|
+
const workflow = inferWorkflow2(record, source.path);
|
|
2650
|
+
const { inputTokens, outputTokens, observedCost } = extractUsage2(record);
|
|
1324
2651
|
const estimatedCost = estimateCostUsd(provider, model, inputTokens, outputTokens);
|
|
1325
2652
|
const timestamp = toIsoOrNow(
|
|
1326
2653
|
getNestedValue(record, [["timestamp"], ["createdAt"], ["created_at"]])
|
|
1327
2654
|
);
|
|
1328
|
-
const attempt =
|
|
2655
|
+
const attempt = asNumber2(
|
|
1329
2656
|
getNestedValue(record, [["attempt"], ["usage", "attempt"], ["metadata", "attempt"]])
|
|
1330
2657
|
) ?? null;
|
|
1331
|
-
const iteration =
|
|
2658
|
+
const iteration = asNumber2(
|
|
1332
2659
|
getNestedValue(record, [["iteration"], ["loop_iteration"], ["metadata", "iteration"]])
|
|
1333
2660
|
) ?? null;
|
|
1334
|
-
const retries =
|
|
2661
|
+
const retries = asNumber2(getNestedValue(record, [["retries"], ["retry_count"], ["metadata", "retries"]])) ?? 0;
|
|
1335
2662
|
const costUsd = observedCost ?? estimatedCost ?? 0;
|
|
1336
2663
|
return {
|
|
1337
2664
|
id: sha1(`${runId}:${source.path}:${index}:${model}:${timestamp}:${costUsd}`),
|
|
@@ -1343,45 +2670,45 @@ function buildCall(source, record, runId, index) {
|
|
|
1343
2670
|
outputTokens,
|
|
1344
2671
|
costUsd,
|
|
1345
2672
|
costSource: observedCost !== null ? "observed" : "estimated",
|
|
1346
|
-
latencyMs:
|
|
1347
|
-
toolCalls:
|
|
2673
|
+
latencyMs: asNumber2(getNestedValue(record, [["latency_ms"], ["latencyMs"], ["usage", "latency_ms"]])) ?? null,
|
|
2674
|
+
toolCalls: asNumber2(getNestedValue(record, [["tool_calls"], ["toolCalls"], ["usage", "tool_calls"]])) ?? 0,
|
|
1348
2675
|
retries,
|
|
1349
2676
|
attempt,
|
|
1350
2677
|
iteration,
|
|
1351
2678
|
status: asString(getNestedValue(record, [["status"], ["level"], ["result"], ["error", "type"]])) ?? null,
|
|
1352
|
-
taskClass:
|
|
1353
|
-
cacheHit:
|
|
2679
|
+
taskClass: inferTaskClass2(record, workflow),
|
|
2680
|
+
cacheHit: asBoolean2(
|
|
1354
2681
|
getNestedValue(record, [["cache_hit"], ["cacheHit"], ["usage", "cache_hit"]])
|
|
1355
2682
|
),
|
|
1356
|
-
cacheCostUsd:
|
|
2683
|
+
cacheCostUsd: asNumber2(
|
|
1357
2684
|
getNestedValue(record, [["cache_cost_usd"], ["cacheCostUsd"], ["usage", "cache_cost_usd"]])
|
|
1358
2685
|
) ?? null,
|
|
1359
2686
|
metadata: pickMetadata(record, ["event", "type", "sessionId", "agentId"])
|
|
1360
2687
|
};
|
|
1361
2688
|
}
|
|
1362
|
-
function
|
|
1363
|
-
const hasUsage =
|
|
2689
|
+
function shouldTreatAsCall2(record) {
|
|
2690
|
+
const hasUsage = extractUsage2(record).inputTokens > 0 || extractUsage2(record).outputTokens > 0 || extractUsage2(record).observedCost !== null;
|
|
1364
2691
|
return hasUsage;
|
|
1365
2692
|
}
|
|
1366
2693
|
function normalizeOpenClawSources(sources, since) {
|
|
1367
2694
|
const cutoff = parseSince(since);
|
|
1368
2695
|
const runsById = /* @__PURE__ */ new Map();
|
|
1369
2696
|
for (const source of sources) {
|
|
1370
|
-
const records =
|
|
2697
|
+
const records = parseJsonLines2(source.path);
|
|
1371
2698
|
records.forEach((record, index) => {
|
|
1372
|
-
if (!
|
|
2699
|
+
if (!shouldTreatAsCall2(record)) {
|
|
1373
2700
|
return;
|
|
1374
2701
|
}
|
|
1375
|
-
const workflow =
|
|
2702
|
+
const workflow = inferWorkflow2(record, source.path);
|
|
1376
2703
|
const timestamp = toIsoOrNow(
|
|
1377
2704
|
getNestedValue(record, [["timestamp"], ["createdAt"], ["created_at"]])
|
|
1378
2705
|
);
|
|
1379
2706
|
if (cutoff && new Date(timestamp).getTime() < cutoff) {
|
|
1380
2707
|
return;
|
|
1381
2708
|
}
|
|
1382
|
-
const runKey =
|
|
2709
|
+
const runKey = inferRunKey2(record, workflow, index, source.path);
|
|
1383
2710
|
const runId = sha1(`${source.path}:${runKey}`);
|
|
1384
|
-
const call =
|
|
2711
|
+
const call = buildCall3(source, record, runId, index);
|
|
1385
2712
|
const existing = runsById.get(runId);
|
|
1386
2713
|
if (!existing) {
|
|
1387
2714
|
runsById.set(runId, {
|
|
@@ -1390,7 +2717,7 @@ function normalizeOpenClawSources(sources, since) {
|
|
|
1390
2717
|
sourcePath: source.path,
|
|
1391
2718
|
timestamp,
|
|
1392
2719
|
workflow,
|
|
1393
|
-
environment:
|
|
2720
|
+
environment: inferEnvironment2(record),
|
|
1394
2721
|
tags: {
|
|
1395
2722
|
sourceKind: source.kind
|
|
1396
2723
|
},
|
|
@@ -1414,147 +2741,431 @@ function normalizeOpenClawSources(sources, since) {
|
|
|
1414
2741
|
});
|
|
1415
2742
|
}
|
|
1416
2743
|
|
|
1417
|
-
// ../core/src/
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
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
|
+
})
|
|
1426
2783
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
2784
|
+
};
|
|
2785
|
+
function getRuntimeAdapter(runtime) {
|
|
2786
|
+
return RUNTIME_ADAPTERS[runtime];
|
|
2787
|
+
}
|
|
2788
|
+
function hasExplicitLocalPaths(options) {
|
|
2789
|
+
return Boolean(options.logFile || options.sessionsDir);
|
|
2790
|
+
}
|
|
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;
|
|
2795
|
+
}
|
|
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
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
return hints.size === 1 ? Array.from(hints)[0] : null;
|
|
2807
|
+
}
|
|
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;
|
|
2819
|
+
}
|
|
2820
|
+
const notes = [];
|
|
2821
|
+
if (sources.some((source) => source.kind === "gateway")) {
|
|
2822
|
+
notes.push(adapter.gatewayNote);
|
|
2823
|
+
}
|
|
2824
|
+
if (sources.some((source) => source.kind === "sessions")) {
|
|
2825
|
+
notes.push(adapter.sessionNote);
|
|
2826
|
+
}
|
|
2827
|
+
return notes;
|
|
2828
|
+
}
|
|
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);
|
|
1429
2876
|
return {
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
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
|
+
]
|
|
1434
2886
|
};
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
function buildAuditSummary(input) {
|
|
1438
|
-
const callCount = input.runs.reduce((sum, run2) => sum + run2.calls.length, 0);
|
|
1439
|
-
const totalSpendUsd = input.runs.reduce((sum, run2) => sum + run2.totalCostUsd, 0);
|
|
1440
|
-
const observedSpendUsd = input.runs.reduce((sum, run2) => sum + run2.observedCostUsd, 0);
|
|
1441
|
-
const estimatedSpendUsd = input.runs.reduce((sum, run2) => sum + run2.estimatedCostUsd, 0);
|
|
1442
|
-
const wasteSpendUsd = input.findings.filter((finding) => finding.classification === "waste").reduce((sum, finding) => sum + finding.costImpactUsd, 0);
|
|
1443
|
-
const opportunitySpendUsd = input.findings.filter((finding) => finding.classification === "opportunity").reduce((sum, finding) => sum + finding.costImpactUsd, 0);
|
|
1444
|
-
const generatedAt = isoNow();
|
|
2887
|
+
}
|
|
1445
2888
|
return {
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
sources: input.sources,
|
|
1452
|
-
since: input.since
|
|
1453
|
-
}),
|
|
1454
|
-
comparison: null,
|
|
1455
|
-
since: input.since,
|
|
1456
|
-
runCount: input.runs.length,
|
|
1457
|
-
callCount,
|
|
1458
|
-
totalSpendUsd: Number(totalSpendUsd.toFixed(6)),
|
|
1459
|
-
observedSpendUsd: Number(observedSpendUsd.toFixed(6)),
|
|
1460
|
-
estimatedSpendUsd: Number(estimatedSpendUsd.toFixed(6)),
|
|
1461
|
-
wasteSpendUsd: Number(wasteSpendUsd.toFixed(6)),
|
|
1462
|
-
opportunitySpendUsd: Number(opportunitySpendUsd.toFixed(6)),
|
|
1463
|
-
structuralWasteRate: Number(
|
|
1464
|
-
(totalSpendUsd === 0 ? 0 : wasteSpendUsd / totalSpendUsd).toFixed(4)
|
|
1465
|
-
),
|
|
1466
|
-
wasteByKind: buildTaxonomyBuckets(input.findings, "waste"),
|
|
1467
|
-
opportunityByKind: buildTaxonomyBuckets(input.findings, "opportunity"),
|
|
1468
|
-
spendByWorkflow: buildBreakdown(
|
|
1469
|
-
input.runs.map((run2) => ({
|
|
1470
|
-
key: run2.workflow,
|
|
1471
|
-
spendUsd: run2.totalCostUsd,
|
|
1472
|
-
observedSpendUsd: run2.observedCostUsd
|
|
1473
|
-
}))
|
|
1474
|
-
),
|
|
1475
|
-
spendByModel: buildBreakdown(
|
|
1476
|
-
input.runs.flatMap(
|
|
1477
|
-
(run2) => run2.calls.map((call) => ({
|
|
1478
|
-
key: `${call.provider}/${call.model}`,
|
|
1479
|
-
spendUsd: call.costUsd,
|
|
1480
|
-
observedSpendUsd: call.costSource === "observed" ? call.costUsd : 0
|
|
1481
|
-
}))
|
|
1482
|
-
)
|
|
1483
|
-
),
|
|
1484
|
-
findings: input.findings,
|
|
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()),
|
|
1485
2894
|
notes: [
|
|
1486
|
-
"
|
|
1487
|
-
"
|
|
1488
|
-
]
|
|
1489
|
-
|
|
1490
|
-
|
|
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
|
+
};
|
|
2899
|
+
}
|
|
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
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
const candidates = await resolveRuntimeCandidates(options);
|
|
2921
|
+
const usableCandidates = candidates.filter((candidate) => candidate.usable);
|
|
2922
|
+
if (hasExplicitLocalPaths(options)) {
|
|
2923
|
+
const hintedRuntime = inferRuntimeFromExplicitPaths(options);
|
|
2924
|
+
if (hintedRuntime) {
|
|
2925
|
+
const hintedCandidate = usableCandidates.find(
|
|
2926
|
+
(candidate) => candidate.adapter.runtime === hintedRuntime
|
|
2927
|
+
);
|
|
2928
|
+
if (hintedCandidate) {
|
|
2929
|
+
return {
|
|
2930
|
+
adapter: hintedCandidate.adapter,
|
|
2931
|
+
sources: hintedCandidate.sources
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
if (usableCandidates.length === 0) {
|
|
2936
|
+
throw new Error(buildExplicitNoDataError(options, hintedRuntime));
|
|
2937
|
+
}
|
|
2938
|
+
if (usableCandidates.length === 1) {
|
|
2939
|
+
return {
|
|
2940
|
+
adapter: usableCandidates[0].adapter,
|
|
2941
|
+
sources: usableCandidates[0].sources
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
throw new Error(
|
|
2945
|
+
"Could not determine whether the provided local files belong to OpenClaw or Hermes. Re-run with --runtime openclaw or --runtime hermes."
|
|
2946
|
+
);
|
|
2947
|
+
}
|
|
2948
|
+
if (usableCandidates.length === 0) {
|
|
2949
|
+
throw new Error(
|
|
2950
|
+
`No supported local runtime sources were detected. Run \`${options.commandPrefix ?? "xerg"} doctor\`, or use --runtime openclaw / --runtime hermes with --log-file / --sessions-dir.`
|
|
2951
|
+
);
|
|
2952
|
+
}
|
|
2953
|
+
if (usableCandidates.length > 1) {
|
|
2954
|
+
throw new Error(
|
|
2955
|
+
"Both OpenClaw and Hermes local sources were detected. Re-run with --runtime openclaw or --runtime hermes."
|
|
2956
|
+
);
|
|
2957
|
+
}
|
|
2958
|
+
return {
|
|
2959
|
+
adapter: usableCandidates[0].adapter,
|
|
2960
|
+
sources: usableCandidates[0].sources
|
|
1491
2961
|
};
|
|
1492
2962
|
}
|
|
2963
|
+
async function doctorAgentRuntime(options) {
|
|
2964
|
+
const requestedRuntime = options.runtime ?? "auto";
|
|
2965
|
+
if (requestedRuntime !== "auto") {
|
|
2966
|
+
options.onProgress?.(`Checking local ${getRuntimeProductName(requestedRuntime)} defaults...`);
|
|
2967
|
+
const adapter = getRuntimeAdapter(requestedRuntime);
|
|
2968
|
+
const sources = await adapter.detectSources(options);
|
|
2969
|
+
options.onProgress?.(
|
|
2970
|
+
sources.length > 0 ? `Detected ${sources.length} local source file${sources.length === 1 ? "" : "s"}.` : `No local ${adapter.productName} source files were detected.`
|
|
2971
|
+
);
|
|
2972
|
+
return buildResolvedDoctorReport(adapter, sources);
|
|
2973
|
+
}
|
|
2974
|
+
options.onProgress?.("Checking local runtime defaults...");
|
|
2975
|
+
const candidates = await resolveRuntimeCandidates(options);
|
|
2976
|
+
const usableCandidates = candidates.filter((candidate) => candidate.usable);
|
|
2977
|
+
const detectedCount = candidates.reduce((sum, candidate) => sum + candidate.sources.length, 0);
|
|
2978
|
+
options.onProgress?.(
|
|
2979
|
+
detectedCount > 0 ? `Detected ${detectedCount} local source file${detectedCount === 1 ? "" : "s"} across supported runtimes.` : "No local runtime source files were detected."
|
|
2980
|
+
);
|
|
2981
|
+
if (hasExplicitLocalPaths(options)) {
|
|
2982
|
+
const hintedRuntime = inferRuntimeFromExplicitPaths(options);
|
|
2983
|
+
if (hintedRuntime) {
|
|
2984
|
+
const hintedCandidate = usableCandidates.find(
|
|
2985
|
+
(candidate) => candidate.adapter.runtime === hintedRuntime
|
|
2986
|
+
);
|
|
2987
|
+
if (hintedCandidate) {
|
|
2988
|
+
return buildResolvedDoctorReport(hintedCandidate.adapter, hintedCandidate.sources);
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
if (usableCandidates.length === 0) {
|
|
2992
|
+
return buildExplicitNoDataDoctorReport(candidates, hintedRuntime);
|
|
2993
|
+
}
|
|
2994
|
+
if (usableCandidates.length === 1) {
|
|
2995
|
+
return buildResolvedDoctorReport(usableCandidates[0].adapter, usableCandidates[0].sources);
|
|
2996
|
+
}
|
|
2997
|
+
return {
|
|
2998
|
+
canAudit: false,
|
|
2999
|
+
mode: "ambiguous",
|
|
3000
|
+
runtime: null,
|
|
3001
|
+
sources: candidates.flatMap((candidate) => candidate.sources),
|
|
3002
|
+
defaults: Object.values(RUNTIME_ADAPTERS).map((adapter) => adapter.defaultPaths()),
|
|
3003
|
+
notes: [
|
|
3004
|
+
"Could not determine whether the provided local files belong to OpenClaw or Hermes.",
|
|
3005
|
+
"Re-run doctor with --runtime openclaw or --runtime hermes to choose the local runtime explicitly."
|
|
3006
|
+
]
|
|
3007
|
+
};
|
|
3008
|
+
}
|
|
3009
|
+
if (usableCandidates.length === 0) {
|
|
3010
|
+
return buildAutoNoDataDoctorReport(candidates);
|
|
3011
|
+
}
|
|
3012
|
+
if (usableCandidates.length > 1) {
|
|
3013
|
+
return buildAutoAmbiguousDoctorReport(usableCandidates);
|
|
3014
|
+
}
|
|
3015
|
+
return buildResolvedDoctorReport(usableCandidates[0].adapter, usableCandidates[0].sources);
|
|
3016
|
+
}
|
|
1493
3017
|
|
|
1494
3018
|
// ../core/src/audit.ts
|
|
1495
|
-
async function
|
|
1496
|
-
return
|
|
3019
|
+
async function doctorCursorUsageCsv(options) {
|
|
3020
|
+
return inspectCursorUsageCsv(options);
|
|
1497
3021
|
}
|
|
1498
|
-
|
|
1499
|
-
options.onProgress?.("Scanning for OpenClaw source files...");
|
|
3022
|
+
function validateCompareOptions(options) {
|
|
1500
3023
|
if (options.compare && options.noDb) {
|
|
1501
3024
|
throw new Error(
|
|
1502
3025
|
"The --compare flag needs local snapshot history. Remove --no-db or provide --db <path>."
|
|
1503
3026
|
);
|
|
1504
3027
|
}
|
|
1505
|
-
|
|
3028
|
+
}
|
|
3029
|
+
function maybeAttachComparison(options, dbPath, summary) {
|
|
3030
|
+
if (!options.compare || !dbPath) {
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
options.onProgress?.("Looking for a comparable baseline audit...");
|
|
3034
|
+
const baseline = readLatestComparableAuditSummary({
|
|
3035
|
+
dbPath,
|
|
3036
|
+
comparisonKey: summary.comparisonKey,
|
|
3037
|
+
currentAuditId: summary.auditId
|
|
3038
|
+
});
|
|
3039
|
+
if (!baseline) {
|
|
3040
|
+
summary.notes = [
|
|
3041
|
+
...summary.notes,
|
|
3042
|
+
"No prior comparable audit was found. Run the same audit again after a fix to unlock before/after deltas."
|
|
3043
|
+
];
|
|
3044
|
+
return;
|
|
3045
|
+
}
|
|
3046
|
+
summary.comparison = buildAuditComparison(summary, baseline);
|
|
3047
|
+
if (hasPricingCoverageChange(summary.pricingCoverage, baseline.pricingCoverage)) {
|
|
3048
|
+
summary.notes = [
|
|
3049
|
+
...summary.notes,
|
|
3050
|
+
"Pricing coverage changed versus the baseline audit. Spend deltas are directional because different Cursor aliases were priced in each run."
|
|
3051
|
+
];
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
function persistLocalSnapshot(summary, runs, dbPath, onProgress) {
|
|
3055
|
+
if (!dbPath) {
|
|
3056
|
+
onProgress?.("Skipping local snapshot persistence (--no-db).");
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
onProgress?.(`Persisting local snapshot to ${dbPath}...`);
|
|
3060
|
+
persistAudit(
|
|
3061
|
+
{
|
|
3062
|
+
summary,
|
|
3063
|
+
runs,
|
|
3064
|
+
pricingCatalog: PRICING_CATALOG
|
|
3065
|
+
},
|
|
3066
|
+
dbPath
|
|
3067
|
+
);
|
|
3068
|
+
onProgress?.("Local snapshot stored.");
|
|
3069
|
+
}
|
|
3070
|
+
function hasPricingCoverageChange(current, baseline) {
|
|
3071
|
+
if (!current && !baseline) {
|
|
3072
|
+
return false;
|
|
3073
|
+
}
|
|
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);
|
|
3075
|
+
}
|
|
3076
|
+
async function auditResolvedRuntime(runtime, options, detectedSources) {
|
|
3077
|
+
const adapter = getRuntimeAdapter(runtime);
|
|
3078
|
+
options.onProgress?.(`Scanning for ${adapter.productName} source files...`);
|
|
3079
|
+
validateCompareOptions(options);
|
|
3080
|
+
const sources = detectedSources ?? await adapter.detectSources(options);
|
|
1506
3081
|
if (sources.length === 0) {
|
|
1507
|
-
options.onProgress?.(
|
|
1508
|
-
throw new Error(
|
|
1509
|
-
`No OpenClaw sources were detected. Run \`${options.commandPrefix ?? "xerg"} doctor\` or provide --log-file / --sessions-dir.`
|
|
1510
|
-
);
|
|
3082
|
+
options.onProgress?.(`No ${adapter.productName} source files were detected.`);
|
|
3083
|
+
throw new Error(adapter.noDataError(options.commandPrefix ?? "xerg"));
|
|
1511
3084
|
}
|
|
1512
3085
|
options.onProgress?.(`Detected ${sources.length} source file${sources.length === 1 ? "" : "s"}.`);
|
|
1513
|
-
options.onProgress?.(
|
|
1514
|
-
const runs =
|
|
3086
|
+
options.onProgress?.(`Normalizing ${adapter.productName} source files...`);
|
|
3087
|
+
const runs = adapter.normalizeSources(sources, options.since);
|
|
1515
3088
|
options.onProgress?.(`Normalized ${runs.length} run${runs.length === 1 ? "" : "s"}.`);
|
|
1516
3089
|
options.onProgress?.("Computing waste and savings findings...");
|
|
1517
|
-
const findings = buildFindings(runs);
|
|
3090
|
+
const { findings, wasteAttributions } = buildFindings(runs);
|
|
1518
3091
|
const dbPath = options.noDb ? void 0 : options.dbPath ?? getDefaultDbPath();
|
|
1519
3092
|
options.onProgress?.("Building audit summary...");
|
|
1520
3093
|
const summary = buildAuditSummary({
|
|
3094
|
+
runtime,
|
|
1521
3095
|
runs,
|
|
1522
3096
|
findings,
|
|
3097
|
+
wasteAttributions,
|
|
1523
3098
|
sources,
|
|
1524
3099
|
since: options.since,
|
|
1525
3100
|
dbPath,
|
|
1526
3101
|
comparisonKeyOverride: options.comparisonKeyOverride
|
|
1527
3102
|
});
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
3103
|
+
maybeAttachComparison(options, dbPath, summary);
|
|
3104
|
+
persistLocalSnapshot(summary, runs, dbPath, options.onProgress);
|
|
3105
|
+
return summary;
|
|
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
|
+
}
|
|
3115
|
+
async function auditCursorUsageCsv(options) {
|
|
3116
|
+
options.onProgress?.("Reading Cursor usage CSV...");
|
|
3117
|
+
validateCompareOptions(options);
|
|
3118
|
+
if (!options.cursorUsageCsv) {
|
|
3119
|
+
throw new Error("No Cursor usage CSV was provided. Use --cursor-usage-csv <path>.");
|
|
3120
|
+
}
|
|
3121
|
+
const parsed = readCursorUsageCsv(options.cursorUsageCsv);
|
|
3122
|
+
options.onProgress?.(`Loaded Cursor usage CSV: ${parsed.source.path}`);
|
|
3123
|
+
options.onProgress?.("Normalizing Cursor usage rows...");
|
|
3124
|
+
const normalized = normalizeCursorUsageCsv({
|
|
3125
|
+
source: parsed.source,
|
|
3126
|
+
rows: parsed.rows,
|
|
3127
|
+
hasObservedCostRows: parsed.hasObservedCostRows,
|
|
3128
|
+
since: options.since
|
|
3129
|
+
});
|
|
3130
|
+
options.onProgress?.(
|
|
3131
|
+
`Normalized ${normalized.runs.length} usage row${normalized.runs.length === 1 ? "" : "s"}.`
|
|
3132
|
+
);
|
|
3133
|
+
options.onProgress?.("Computing Cursor-specific findings...");
|
|
3134
|
+
const { findings, wasteAttributions } = buildCursorUsageFindings(normalized.runs);
|
|
3135
|
+
const dbPath = options.noDb ? void 0 : options.dbPath ?? getDefaultDbPath();
|
|
3136
|
+
options.onProgress?.("Building audit summary...");
|
|
3137
|
+
const summary = buildAuditSummary({
|
|
3138
|
+
runtime: "cursor",
|
|
3139
|
+
runs: normalized.runs,
|
|
3140
|
+
findings,
|
|
3141
|
+
wasteAttributions,
|
|
3142
|
+
sources: [parsed.source],
|
|
3143
|
+
since: options.since,
|
|
3144
|
+
dbPath,
|
|
3145
|
+
comparisonKeyOverride: options.comparisonKeyOverride
|
|
3146
|
+
});
|
|
3147
|
+
summary.pricingCoverage = normalized.pricingCoverage;
|
|
3148
|
+
summary.cursorUsage = normalized.cursorUsage;
|
|
3149
|
+
summary.notes = [
|
|
3150
|
+
"Cursor CSV audits analyze exported usage rows rather than raw session transcripts.",
|
|
3151
|
+
"OpenClaw-specific retry, loop, and session-level workflow findings are still unavailable for Cursor CSV inputs because the export does not include retries, iterations, or real workflow IDs.",
|
|
3152
|
+
parsed.hasObservedCostRows ? "Numeric Cost values were used as observed spend. Included rows are treated as zero billed spend in this export mode." : "This CSV did not include numeric Cost values, so spend was estimated from local model pricing where possible."
|
|
3153
|
+
];
|
|
3154
|
+
if (parsed.rows.length > 0 && normalized.runs.length === 0 && options.since) {
|
|
3155
|
+
summary.notes = [
|
|
3156
|
+
...summary.notes,
|
|
3157
|
+
`No Cursor usage rows matched the --since window (${options.since}).`
|
|
3158
|
+
];
|
|
3159
|
+
}
|
|
3160
|
+
if (normalized.pricingCoverage.unpricedCallCount > 0) {
|
|
3161
|
+
const aliases = normalized.pricingCoverage.topUnpricedModels.map((model) => model.key).join(", ");
|
|
3162
|
+
summary.notes = [
|
|
3163
|
+
...summary.notes,
|
|
3164
|
+
`Some Cursor aliases do not have full local pricing coverage: ${aliases || "unknown aliases"}.`
|
|
3165
|
+
];
|
|
3166
|
+
}
|
|
3167
|
+
maybeAttachComparison(options, dbPath, summary);
|
|
3168
|
+
persistLocalSnapshot(summary, normalized.runs, dbPath, options.onProgress);
|
|
1558
3169
|
return summary;
|
|
1559
3170
|
}
|
|
1560
3171
|
|
|
@@ -1605,6 +3216,26 @@ var templatesByKind = {
|
|
|
1605
3216
|
suggestedChangeFn: () => ({
|
|
1606
3217
|
strategy: "cadence-review"
|
|
1607
3218
|
})
|
|
3219
|
+
},
|
|
3220
|
+
"cache-carryover": {
|
|
3221
|
+
actionType: "prompt-trim",
|
|
3222
|
+
titleFn: () => "Summarize and reset long Cursor chats",
|
|
3223
|
+
descriptionFn: (f) => `${f.summary} Create a compact recall summary, start a fresh chat, and carry forward only the facts the model actually needs.`,
|
|
3224
|
+
suggestedChangeFn: (f) => ({
|
|
3225
|
+
strategy: "conversation-reset",
|
|
3226
|
+
cacheReadShare: f.details.cacheReadShare,
|
|
3227
|
+
totalCacheReadTokens: f.details.totalCacheReadTokens
|
|
3228
|
+
})
|
|
3229
|
+
},
|
|
3230
|
+
"max-mode-concentration": {
|
|
3231
|
+
actionType: "model-switch",
|
|
3232
|
+
titleFn: () => "Reserve max mode for the hardest Cursor turns",
|
|
3233
|
+
descriptionFn: (f) => `${f.summary} Try a two-pass workflow: standard mode first, then escalate only the prompts that truly need max mode.`,
|
|
3234
|
+
suggestedChangeFn: (f) => ({
|
|
3235
|
+
strategy: "tiered-routing",
|
|
3236
|
+
maxModeSpendShare: f.details.maxModeSpendShare,
|
|
3237
|
+
maxModeCallCount: f.details.maxModeCallCount
|
|
3238
|
+
})
|
|
1608
3239
|
}
|
|
1609
3240
|
};
|
|
1610
3241
|
function extractWorkflow(finding) {
|
|
@@ -1658,10 +3289,16 @@ function formatPercentDelta(value) {
|
|
|
1658
3289
|
const sign = points > 0 ? "+" : "";
|
|
1659
3290
|
return `${sign}${points.toFixed(0)} pts`;
|
|
1660
3291
|
}
|
|
3292
|
+
function formatCount(value) {
|
|
3293
|
+
return new Intl.NumberFormat("en-US").format(value);
|
|
3294
|
+
}
|
|
1661
3295
|
function formatUsdDelta(value) {
|
|
1662
3296
|
const sign = value > 0 ? "+" : "";
|
|
1663
3297
|
return `${sign}${formatUsd(value)}`;
|
|
1664
3298
|
}
|
|
3299
|
+
function isCursorUsageSummary(summary) {
|
|
3300
|
+
return summary.sourceFiles.some((source) => source.kind === "cursor-usage-csv");
|
|
3301
|
+
}
|
|
1665
3302
|
function topRows(rows, limit = 5) {
|
|
1666
3303
|
return rows.slice(0, limit).map((row) => {
|
|
1667
3304
|
return `- ${row.key}: ${formatUsd(row.spendUsd)} (${formatPercent(row.observedShare)} observed)`;
|
|
@@ -1763,27 +3400,51 @@ function renderCompareBlock(summary) {
|
|
|
1763
3400
|
...findingChanges.length > 0 ? findingChanges : ["- High-confidence waste changes: none"]
|
|
1764
3401
|
];
|
|
1765
3402
|
}
|
|
3403
|
+
function renderDailyTrendRows(spendByDay, wasteByDay) {
|
|
3404
|
+
if (spendByDay.length <= 1) {
|
|
3405
|
+
return [];
|
|
3406
|
+
}
|
|
3407
|
+
const wasteByDate = new Map(wasteByDay.map((row) => [row.date, row.wasteUsd]));
|
|
3408
|
+
return [
|
|
3409
|
+
"## Daily trend",
|
|
3410
|
+
...spendByDay.map((row) => {
|
|
3411
|
+
const wasteUsd = wasteByDate.get(row.date) ?? 0;
|
|
3412
|
+
return `- ${row.date}: ${formatUsd(row.spendUsd)} spend, ${formatUsd(wasteUsd)} waste`;
|
|
3413
|
+
})
|
|
3414
|
+
];
|
|
3415
|
+
}
|
|
1766
3416
|
function renderDoctorReport(report, options) {
|
|
1767
3417
|
const commandPrefix = options?.commandPrefix ?? "xerg";
|
|
1768
|
-
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
|
+
] : [
|
|
1769
3426
|
"",
|
|
1770
3427
|
"## Next steps",
|
|
1771
|
-
`-
|
|
1772
|
-
`- Inspect
|
|
1773
|
-
`-
|
|
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`,
|
|
1774
3433
|
"- Remote audits still analyze locally after Xerg pulls the source files to your machine."
|
|
1775
3434
|
];
|
|
1776
3435
|
const sections = [
|
|
1777
3436
|
"# Xerg doctor",
|
|
1778
3437
|
"",
|
|
1779
|
-
|
|
3438
|
+
status,
|
|
1780
3439
|
"",
|
|
1781
3440
|
"## Defaults",
|
|
1782
|
-
|
|
1783
|
-
|
|
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
|
+
]),
|
|
1784
3445
|
"",
|
|
1785
3446
|
"## Sources",
|
|
1786
|
-
...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"],
|
|
1787
3448
|
"",
|
|
1788
3449
|
"## Notes",
|
|
1789
3450
|
...report.notes.map((note) => `- ${note}`),
|
|
@@ -1791,7 +3452,177 @@ function renderDoctorReport(report, options) {
|
|
|
1791
3452
|
];
|
|
1792
3453
|
return sections.join("\n");
|
|
1793
3454
|
}
|
|
3455
|
+
function renderCursorDoctorReport(report) {
|
|
3456
|
+
const status = report.canAudit ? "Cursor usage CSV detected." : "Cursor usage CSV is not ready.";
|
|
3457
|
+
return [
|
|
3458
|
+
"# Xerg doctor [cursor csv]",
|
|
3459
|
+
"",
|
|
3460
|
+
status,
|
|
3461
|
+
"",
|
|
3462
|
+
`File: ${report.filePath || "(not provided)"}`,
|
|
3463
|
+
`Rows: ${formatCount(report.rowCount)}`,
|
|
3464
|
+
`Date range: ${report.dateRange ? `${report.dateRange.start} -> ${report.dateRange.end}` : "unavailable"}`,
|
|
3465
|
+
"",
|
|
3466
|
+
"## Pricing coverage",
|
|
3467
|
+
`- Priced rows: ${formatCount(report.pricingCoverage.pricedCallCount)}`,
|
|
3468
|
+
`- Unpriced rows: ${formatCount(report.pricingCoverage.unpricedCallCount)}`,
|
|
3469
|
+
`- Priced tokens: ${formatCount(report.pricingCoverage.pricedTokenCount)}`,
|
|
3470
|
+
`- Unpriced tokens: ${formatCount(report.pricingCoverage.unpricedTokenCount)}`,
|
|
3471
|
+
...report.pricingCoverage.topUnpricedModels.length > 0 ? report.pricingCoverage.topUnpricedModels.map(
|
|
3472
|
+
(model) => `- Unpriced model: ${model.key} (${formatCount(model.totalTokens)} tokens across ${formatCount(model.callCount)} row${model.callCount === 1 ? "" : "s"})`
|
|
3473
|
+
) : ["- Unpriced model: none"],
|
|
3474
|
+
"",
|
|
3475
|
+
"## Notes",
|
|
3476
|
+
...report.notes.map((note) => `- ${note}`)
|
|
3477
|
+
].join("\n");
|
|
3478
|
+
}
|
|
3479
|
+
function renderCursorModeRows(rows) {
|
|
3480
|
+
if (rows.length === 0) {
|
|
3481
|
+
return ["- none"];
|
|
3482
|
+
}
|
|
3483
|
+
return rows.map((row) => {
|
|
3484
|
+
return `- ${row.key}: ${formatCount(row.callCount)} row${row.callCount === 1 ? "" : "s"}, ${formatCount(row.totalTokens)} tokens, ${formatUsd(row.estimatedSpendUsd)} spend`;
|
|
3485
|
+
});
|
|
3486
|
+
}
|
|
3487
|
+
function renderCursorModelRows(rows) {
|
|
3488
|
+
if (rows.length === 0) {
|
|
3489
|
+
return ["- none"];
|
|
3490
|
+
}
|
|
3491
|
+
return rows.slice(0, 8).map((row) => {
|
|
3492
|
+
const coverage = row.unpricedCallCount === 0 ? `${formatUsd(row.estimatedSpendUsd)} spend` : row.pricedCallCount === 0 ? "unpriced" : `${formatUsd(row.estimatedSpendUsd)} spend, ${formatCount(row.unpricedCallCount)} unpriced row${row.unpricedCallCount === 1 ? "" : "s"}`;
|
|
3493
|
+
return `- ${row.key}: ${formatCount(row.callCount)} row${row.callCount === 1 ? "" : "s"}, ${formatCount(row.totalTokens)} tokens, ${coverage}`;
|
|
3494
|
+
});
|
|
3495
|
+
}
|
|
3496
|
+
function renderCursorPricingCoverage(summary) {
|
|
3497
|
+
const coverage = summary.pricingCoverage;
|
|
3498
|
+
if (!coverage) {
|
|
3499
|
+
return ["- Pricing coverage unavailable"];
|
|
3500
|
+
}
|
|
3501
|
+
return [
|
|
3502
|
+
`- Priced rows: ${formatCount(coverage.pricedCallCount)}`,
|
|
3503
|
+
`- Unpriced rows: ${formatCount(coverage.unpricedCallCount)}`,
|
|
3504
|
+
`- Priced tokens: ${formatCount(coverage.pricedTokenCount)}`,
|
|
3505
|
+
`- Unpriced tokens: ${formatCount(coverage.unpricedTokenCount)}`,
|
|
3506
|
+
...coverage.topUnpricedModels.length > 0 ? coverage.topUnpricedModels.map(
|
|
3507
|
+
(model) => `- Unpriced model: ${model.key} (${formatCount(model.totalTokens)} tokens across ${formatCount(model.callCount)} row${model.callCount === 1 ? "" : "s"})`
|
|
3508
|
+
) : ["- Unpriced model: none"]
|
|
3509
|
+
];
|
|
3510
|
+
}
|
|
3511
|
+
function renderCursorCompareBlock(summary) {
|
|
3512
|
+
if (!summary.comparison) {
|
|
3513
|
+
return [];
|
|
3514
|
+
}
|
|
3515
|
+
const comparison = summary.comparison;
|
|
3516
|
+
const modeSwing = comparison.workflowDeltas[0];
|
|
3517
|
+
const modelSwing = comparison.modelDeltas[0];
|
|
3518
|
+
return [
|
|
3519
|
+
"## Before / after",
|
|
3520
|
+
`Compared against ${comparison.baselineGeneratedAt}`,
|
|
3521
|
+
`- Total spend: ${formatUsd(comparison.baselineTotalSpendUsd)} -> ${formatUsd(summary.totalSpendUsd)} (${formatUsdDelta(comparison.deltaTotalSpendUsd)})`,
|
|
3522
|
+
`- Rows analyzed: ${formatCount(comparison.baselineRunCount)} -> ${formatCount(summary.runCount)} (${comparison.deltaRunCount > 0 ? "+" : ""}${comparison.deltaRunCount})`,
|
|
3523
|
+
`- Usage rows with pricing: ${formatCount(summary.pricingCoverage?.pricedCallCount ?? 0)}`,
|
|
3524
|
+
modeSwing ? `- Mode swing to inspect: ${describeSpendDelta(modeSwing)}` : "- Mode swing to inspect: none",
|
|
3525
|
+
modelSwing ? `- Model swing to inspect: ${describeSpendDelta(modelSwing)}` : "- Model swing to inspect: none"
|
|
3526
|
+
];
|
|
3527
|
+
}
|
|
3528
|
+
function renderCursorTerminalSummary(summary) {
|
|
3529
|
+
const usage = summary.cursorUsage;
|
|
3530
|
+
return [
|
|
3531
|
+
"# Xerg audit [cursor csv]",
|
|
3532
|
+
"",
|
|
3533
|
+
`Total spend: ${formatUsd(summary.totalSpendUsd)}`,
|
|
3534
|
+
`Observed spend: ${formatUsd(summary.observedSpendUsd)}`,
|
|
3535
|
+
`Estimated spend: ${formatUsd(summary.estimatedSpendUsd)}`,
|
|
3536
|
+
`Rows analyzed: ${formatCount(summary.runCount)}`,
|
|
3537
|
+
`Usage rows with pricing: ${formatCount(summary.pricingCoverage?.pricedCallCount ?? 0)} / ${formatCount(summary.runCount)}`,
|
|
3538
|
+
`Total tokens: ${formatCount(usage?.totalTokens ?? 0)}`,
|
|
3539
|
+
`Structural waste identified: ${formatUsd(summary.wasteSpendUsd)} (${formatPercent(summary.structuralWasteRate)})`,
|
|
3540
|
+
`Potential impact surfaced: ${formatUsd(summary.opportunitySpendUsd)}`,
|
|
3541
|
+
"",
|
|
3542
|
+
"## Token mix",
|
|
3543
|
+
`- Input tokens: ${formatCount(usage?.totalInputTokens ?? 0)}`,
|
|
3544
|
+
`- Output tokens: ${formatCount(usage?.totalOutputTokens ?? 0)}`,
|
|
3545
|
+
`- Cache read tokens: ${formatCount(usage?.totalCacheReadTokens ?? 0)}`,
|
|
3546
|
+
`- Input (cache write): ${formatCount(usage?.totalInputWithCacheWriteTokens ?? 0)}`,
|
|
3547
|
+
`- Input (no cache write): ${formatCount(usage?.totalInputWithoutCacheWriteTokens ?? 0)}`,
|
|
3548
|
+
"",
|
|
3549
|
+
"## Max mode usage",
|
|
3550
|
+
...renderCursorModeRows(usage?.modes ?? []),
|
|
3551
|
+
"",
|
|
3552
|
+
"## Model mix",
|
|
3553
|
+
...renderCursorModelRows(usage?.models ?? []),
|
|
3554
|
+
"",
|
|
3555
|
+
"## Pricing coverage",
|
|
3556
|
+
...renderCursorPricingCoverage(summary),
|
|
3557
|
+
"",
|
|
3558
|
+
...renderDailyTrendRows(summary.spendByDay, summary.wasteByDay),
|
|
3559
|
+
...summary.spendByDay.length > 1 ? [""] : [],
|
|
3560
|
+
"## Waste taxonomy",
|
|
3561
|
+
"Structural waste",
|
|
3562
|
+
...renderTaxonomyRows(summary.wasteByKind, "No confirmed waste buckets detected."),
|
|
3563
|
+
"Savings opportunities",
|
|
3564
|
+
...renderTaxonomyRows(
|
|
3565
|
+
summary.opportunityByKind,
|
|
3566
|
+
"No opportunity buckets detected.",
|
|
3567
|
+
"(directional)"
|
|
3568
|
+
),
|
|
3569
|
+
"",
|
|
3570
|
+
"## Findings",
|
|
3571
|
+
...renderFindingList(summary.findings, "none detected"),
|
|
3572
|
+
"",
|
|
3573
|
+
...renderCursorCompareBlock(summary),
|
|
3574
|
+
...summary.comparison ? [""] : [],
|
|
3575
|
+
"## Notes",
|
|
3576
|
+
...summary.notes.map((note) => `- ${note}`)
|
|
3577
|
+
].join("\n");
|
|
3578
|
+
}
|
|
3579
|
+
function renderCursorMarkdownSummary(summary) {
|
|
3580
|
+
const usage = summary.cursorUsage;
|
|
3581
|
+
return [
|
|
3582
|
+
"# Xerg Cursor CSV Audit",
|
|
3583
|
+
"",
|
|
3584
|
+
`- Generated: ${summary.generatedAt}`,
|
|
3585
|
+
`- Total spend: ${formatUsd(summary.totalSpendUsd)}`,
|
|
3586
|
+
`- Observed spend: ${formatUsd(summary.observedSpendUsd)}`,
|
|
3587
|
+
`- Estimated spend: ${formatUsd(summary.estimatedSpendUsd)}`,
|
|
3588
|
+
`- Structural waste identified: ${formatUsd(summary.wasteSpendUsd)} (${formatPercent(summary.structuralWasteRate)})`,
|
|
3589
|
+
`- Potential impact surfaced: ${formatUsd(summary.opportunitySpendUsd)}`,
|
|
3590
|
+
`- Rows analyzed: ${formatCount(summary.runCount)}`,
|
|
3591
|
+
`- Usage rows with pricing: ${formatCount(summary.pricingCoverage?.pricedCallCount ?? 0)} / ${formatCount(summary.runCount)}`,
|
|
3592
|
+
`- Total tokens: ${formatCount(usage?.totalTokens ?? 0)}`,
|
|
3593
|
+
"",
|
|
3594
|
+
"## Token mix",
|
|
3595
|
+
`- Input tokens: ${formatCount(usage?.totalInputTokens ?? 0)}`,
|
|
3596
|
+
`- Output tokens: ${formatCount(usage?.totalOutputTokens ?? 0)}`,
|
|
3597
|
+
`- Cache read tokens: ${formatCount(usage?.totalCacheReadTokens ?? 0)}`,
|
|
3598
|
+
"",
|
|
3599
|
+
"## Max mode usage",
|
|
3600
|
+
...renderCursorModeRows(usage?.modes ?? []),
|
|
3601
|
+
"",
|
|
3602
|
+
"## Model mix",
|
|
3603
|
+
...renderCursorModelRows(usage?.models ?? []),
|
|
3604
|
+
"",
|
|
3605
|
+
"## Pricing coverage",
|
|
3606
|
+
...renderCursorPricingCoverage(summary),
|
|
3607
|
+
"",
|
|
3608
|
+
...renderDailyTrendRows(summary.spendByDay, summary.wasteByDay),
|
|
3609
|
+
...summary.spendByDay.length > 1 ? [""] : [],
|
|
3610
|
+
...renderTaxonomyBlock(summary),
|
|
3611
|
+
"",
|
|
3612
|
+
"## Findings",
|
|
3613
|
+
...summary.findings.slice(0, 10).map((finding) => {
|
|
3614
|
+
return `- **${finding.title}** (${finding.classification}, ${finding.confidence}) \u2014 ${finding.summary} Estimated impact: ${formatUsd(finding.costImpactUsd)}.`;
|
|
3615
|
+
}),
|
|
3616
|
+
...summary.comparison ? ["", ...renderCursorCompareBlock(summary)] : [],
|
|
3617
|
+
"",
|
|
3618
|
+
"## Notes",
|
|
3619
|
+
...summary.notes.map((note) => `- ${note}`)
|
|
3620
|
+
].join("\n");
|
|
3621
|
+
}
|
|
1794
3622
|
function renderTerminalSummary(summary) {
|
|
3623
|
+
if (isCursorUsageSummary(summary)) {
|
|
3624
|
+
return renderCursorTerminalSummary(summary);
|
|
3625
|
+
}
|
|
1795
3626
|
const wasteFindings = summary.findings.filter((finding) => finding.classification === "waste");
|
|
1796
3627
|
const opportunityFindings = summary.findings.filter(
|
|
1797
3628
|
(finding) => finding.classification === "opportunity"
|
|
@@ -1817,6 +3648,8 @@ function renderTerminalSummary(summary) {
|
|
|
1817
3648
|
"## Top models",
|
|
1818
3649
|
...topRows(summary.spendByModel),
|
|
1819
3650
|
"",
|
|
3651
|
+
...renderDailyTrendRows(summary.spendByDay, summary.wasteByDay),
|
|
3652
|
+
...summary.spendByDay.length > 1 ? [""] : [],
|
|
1820
3653
|
"## High-confidence waste",
|
|
1821
3654
|
...renderFindingList(wasteFindings, "none detected"),
|
|
1822
3655
|
"",
|
|
@@ -1838,6 +3671,9 @@ function renderTerminalSummary(summary) {
|
|
|
1838
3671
|
].join("\n");
|
|
1839
3672
|
}
|
|
1840
3673
|
function renderMarkdownSummary(summary) {
|
|
3674
|
+
if (isCursorUsageSummary(summary)) {
|
|
3675
|
+
return renderCursorMarkdownSummary(summary);
|
|
3676
|
+
}
|
|
1841
3677
|
const lines = [
|
|
1842
3678
|
"# Xerg Audit Report",
|
|
1843
3679
|
"",
|
|
@@ -1855,6 +3691,9 @@ function renderMarkdownSummary(summary) {
|
|
|
1855
3691
|
"## Top workflows",
|
|
1856
3692
|
...topRows(summary.spendByWorkflow),
|
|
1857
3693
|
"",
|
|
3694
|
+
...renderDailyTrendRows(summary.spendByDay, summary.wasteByDay),
|
|
3695
|
+
...summary.spendByDay.length > 1 ? [""] : [],
|
|
3696
|
+
"",
|
|
1858
3697
|
"## Findings",
|
|
1859
3698
|
...summary.findings.slice(0, 10).map((finding) => {
|
|
1860
3699
|
return `- **${finding.title}** (${finding.classification}, ${finding.confidence}) \u2014 ${finding.summary} Estimated impact: ${formatUsd(finding.costImpactUsd)}.`;
|
|
@@ -1924,6 +3763,8 @@ function toWirePayload(summary, meta) {
|
|
|
1924
3763
|
opportunityByKind: summary.opportunityByKind,
|
|
1925
3764
|
spendByWorkflow: summary.spendByWorkflow,
|
|
1926
3765
|
spendByModel: summary.spendByModel,
|
|
3766
|
+
spendByDay: summary.spendByDay,
|
|
3767
|
+
wasteByDay: summary.wasteByDay,
|
|
1927
3768
|
findings: summary.findings.map(toWireFinding),
|
|
1928
3769
|
notes: summary.notes,
|
|
1929
3770
|
comparison: summary.comparison ? toWireComparison(summary.comparison) : null
|
|
@@ -1996,17 +3837,17 @@ async function pushAudit(payload, config) {
|
|
|
1996
3837
|
}
|
|
1997
3838
|
|
|
1998
3839
|
// src/push/config.ts
|
|
1999
|
-
import { readFileSync as
|
|
2000
|
-
import { homedir as
|
|
2001
|
-
import { join as
|
|
3840
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
3841
|
+
import { homedir as homedir4 } from "os";
|
|
3842
|
+
import { join as join5 } from "path";
|
|
2002
3843
|
|
|
2003
3844
|
// src/auth/credentials.ts
|
|
2004
|
-
import { existsSync, mkdirSync as mkdirSync3, readFileSync as
|
|
2005
|
-
import { homedir as
|
|
2006
|
-
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";
|
|
2007
3848
|
function getCredentialsPath() {
|
|
2008
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ||
|
|
2009
|
-
return
|
|
3849
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME || join4(homedir3(), ".config");
|
|
3850
|
+
return join4(xdgConfig, "xerg", "credentials.json");
|
|
2010
3851
|
}
|
|
2011
3852
|
function storeCredentials(token) {
|
|
2012
3853
|
const credPath = getCredentialsPath();
|
|
@@ -2019,7 +3860,7 @@ function loadStoredCredentials() {
|
|
|
2019
3860
|
const credPath = getCredentialsPath();
|
|
2020
3861
|
try {
|
|
2021
3862
|
if (!existsSync(credPath)) return null;
|
|
2022
|
-
const raw =
|
|
3863
|
+
const raw = readFileSync4(credPath, "utf8");
|
|
2023
3864
|
const parsed = JSON.parse(raw);
|
|
2024
3865
|
return parsed.token || null;
|
|
2025
3866
|
} catch {
|
|
@@ -2039,7 +3880,7 @@ function clearCredentials() {
|
|
|
2039
3880
|
|
|
2040
3881
|
// src/push/config.ts
|
|
2041
3882
|
var DEFAULT_API_URL = "https://api.xerg.ai";
|
|
2042
|
-
var CONFIG_PATH =
|
|
3883
|
+
var CONFIG_PATH = join5(homedir4(), ".xerg", "config.json");
|
|
2043
3884
|
function loadPushConfig() {
|
|
2044
3885
|
const envKey = process.env.XERG_API_KEY;
|
|
2045
3886
|
const envUrl = process.env.XERG_API_URL;
|
|
@@ -2050,7 +3891,7 @@ function loadPushConfig() {
|
|
|
2050
3891
|
};
|
|
2051
3892
|
}
|
|
2052
3893
|
try {
|
|
2053
|
-
const raw =
|
|
3894
|
+
const raw = readFileSync5(CONFIG_PATH, "utf8");
|
|
2054
3895
|
const parsed = JSON.parse(raw);
|
|
2055
3896
|
if (parsed.apiKey) {
|
|
2056
3897
|
return {
|
|
@@ -2073,12 +3914,15 @@ Get your key at https://xerg.ai/dashboard/settings`
|
|
|
2073
3914
|
);
|
|
2074
3915
|
}
|
|
2075
3916
|
|
|
3917
|
+
// src/source-meta.ts
|
|
3918
|
+
import { hostname } from "os";
|
|
3919
|
+
|
|
2076
3920
|
// src/transport/ssh.ts
|
|
2077
3921
|
import { execSync, spawnSync } from "child_process";
|
|
2078
3922
|
import { createHash as createHash2 } from "crypto";
|
|
2079
3923
|
import { mkdirSync as mkdirSync4, rmSync as rmSync2 } from "fs";
|
|
2080
|
-
import { homedir as
|
|
2081
|
-
import { join as
|
|
3924
|
+
import { homedir as homedir5, tmpdir } from "os";
|
|
3925
|
+
import { join as join6 } from "path";
|
|
2082
3926
|
var DEFAULT_GATEWAY_DIR = "/tmp/openclaw";
|
|
2083
3927
|
var DEFAULT_SESSIONS_DIR = "~/.openclaw/agents";
|
|
2084
3928
|
function hashString(input) {
|
|
@@ -2087,7 +3931,7 @@ function hashString(input) {
|
|
|
2087
3931
|
function sshArgs(source) {
|
|
2088
3932
|
const args = [];
|
|
2089
3933
|
if (source.identityFile) {
|
|
2090
|
-
const resolved = source.identityFile.replace(/^~/,
|
|
3934
|
+
const resolved = source.identityFile.replace(/^~/, homedir5());
|
|
2091
3935
|
args.push("-i", resolved);
|
|
2092
3936
|
}
|
|
2093
3937
|
args.push("-o", "BatchMode=yes", "-o", "ConnectTimeout=10");
|
|
@@ -2096,7 +3940,7 @@ function sshArgs(source) {
|
|
|
2096
3940
|
function rsyncSshCommand(source) {
|
|
2097
3941
|
const parts = ["ssh"];
|
|
2098
3942
|
if (source.identityFile) {
|
|
2099
|
-
const resolved = source.identityFile.replace(/^~/,
|
|
3943
|
+
const resolved = source.identityFile.replace(/^~/, homedir5());
|
|
2100
3944
|
parts.push(`-i "${resolved}"`);
|
|
2101
3945
|
}
|
|
2102
3946
|
parts.push("-o BatchMode=yes", "-o ConnectTimeout=10");
|
|
@@ -2184,7 +4028,7 @@ function rsyncPull(opts) {
|
|
|
2184
4028
|
if (status !== 0 || !stdout) return false;
|
|
2185
4029
|
const files = stdout.split("\n").filter(Boolean);
|
|
2186
4030
|
if (files.length === 0) return false;
|
|
2187
|
-
const tmpFile =
|
|
4031
|
+
const tmpFile = join6(tmpdir(), `xerg-filelist-${hashString(opts.remotePath)}`);
|
|
2188
4032
|
const relativePaths = files.map(
|
|
2189
4033
|
(f) => f.startsWith(opts.remotePath) ? f.slice(opts.remotePath.length).replace(/^\//, "") : f
|
|
2190
4034
|
);
|
|
@@ -2233,12 +4077,12 @@ function pullDirectory(opts) {
|
|
|
2233
4077
|
}
|
|
2234
4078
|
function resolveLocalPath(source, keepFiles) {
|
|
2235
4079
|
if (keepFiles) {
|
|
2236
|
-
const cacheDir =
|
|
4080
|
+
const cacheDir = join6(homedir5(), ".xerg", "remote-cache", source.name);
|
|
2237
4081
|
mkdirSync4(cacheDir, { recursive: true });
|
|
2238
4082
|
return cacheDir;
|
|
2239
4083
|
}
|
|
2240
4084
|
const hash = hashString(`${source.host}:${Date.now()}`);
|
|
2241
|
-
const tmpPath =
|
|
4085
|
+
const tmpPath = join6(tmpdir(), `xerg-remote-${hash}`);
|
|
2242
4086
|
mkdirSync4(tmpPath, { recursive: true });
|
|
2243
4087
|
return tmpPath;
|
|
2244
4088
|
}
|
|
@@ -2290,8 +4134,8 @@ async function pullRemoteFiles(opts) {
|
|
|
2290
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."
|
|
2291
4135
|
);
|
|
2292
4136
|
const localBase = resolveLocalPath(source, keepFiles);
|
|
2293
|
-
const gatewayDir =
|
|
2294
|
-
const sessionsDir =
|
|
4137
|
+
const gatewayDir = join6(localBase, "gateway");
|
|
4138
|
+
const sessionsDir = join6(localBase, "sessions");
|
|
2295
4139
|
const remoteLogPath = source.logFile ?? DEFAULT_GATEWAY_DIR;
|
|
2296
4140
|
const remoteSessionsPath = source.sessionsDir ?? DEFAULT_SESSIONS_DIR;
|
|
2297
4141
|
const { stdout: expandedSessions } = sshExec(source, `eval echo ${remoteSessionsPath}`);
|
|
@@ -2483,8 +4327,8 @@ function formatBytes(bytes) {
|
|
|
2483
4327
|
import { execSync as execSync2, spawnSync as spawnSync2 } from "child_process";
|
|
2484
4328
|
import { createHash as createHash3 } from "crypto";
|
|
2485
4329
|
import { mkdirSync as mkdirSync5, rmSync as rmSync3 } from "fs";
|
|
2486
|
-
import { homedir as
|
|
2487
|
-
import { join as
|
|
4330
|
+
import { homedir as homedir6, tmpdir as tmpdir2 } from "os";
|
|
4331
|
+
import { join as join7 } from "path";
|
|
2488
4332
|
var DEFAULT_GATEWAY_DIR2 = "/tmp/openclaw";
|
|
2489
4333
|
var DEFAULT_SESSIONS_DIR2 = "~/.openclaw/agents";
|
|
2490
4334
|
var ALTERNATE_SESSION_PATHS = ["/data/.clawdbot/agents/main/sessions"];
|
|
@@ -2557,13 +4401,13 @@ function tarRailwayPull(opts) {
|
|
|
2557
4401
|
}
|
|
2558
4402
|
function resolveLocalPath2(source, keepFiles) {
|
|
2559
4403
|
if (keepFiles) {
|
|
2560
|
-
const cacheDir =
|
|
4404
|
+
const cacheDir = join7(homedir6(), ".xerg", "remote-cache", source.name);
|
|
2561
4405
|
mkdirSync5(cacheDir, { recursive: true });
|
|
2562
4406
|
return cacheDir;
|
|
2563
4407
|
}
|
|
2564
4408
|
const identity = source.railway ? `railway:${source.railway.projectId}:${Date.now()}` : `${source.name}:${Date.now()}`;
|
|
2565
4409
|
const hash = hashString2(identity);
|
|
2566
|
-
const tmpPath =
|
|
4410
|
+
const tmpPath = join7(tmpdir2(), `xerg-remote-${hash}`);
|
|
2567
4411
|
mkdirSync5(tmpPath, { recursive: true });
|
|
2568
4412
|
return tmpPath;
|
|
2569
4413
|
}
|
|
@@ -2634,8 +4478,8 @@ async function pullRemoteFilesRailway(opts) {
|
|
|
2634
4478
|
}
|
|
2635
4479
|
onProgress?.("Railway service reachable.");
|
|
2636
4480
|
const localBase = resolveLocalPath2(source, keepFiles);
|
|
2637
|
-
const gatewayDir =
|
|
2638
|
-
const sessionsDir =
|
|
4481
|
+
const gatewayDir = join7(localBase, "gateway");
|
|
4482
|
+
const sessionsDir = join7(localBase, "sessions");
|
|
2639
4483
|
const remoteLogPath = source.logFile ?? DEFAULT_GATEWAY_DIR2;
|
|
2640
4484
|
onProgress?.("Checking Railway default paths for gateway logs and sessions...");
|
|
2641
4485
|
const logCheck = checkRemotePath(remoteLogPath, target);
|
|
@@ -2864,13 +4708,13 @@ function formatBytes2(bytes) {
|
|
|
2864
4708
|
}
|
|
2865
4709
|
|
|
2866
4710
|
// src/transport/config.ts
|
|
2867
|
-
import { readFileSync as
|
|
2868
|
-
import { resolve as
|
|
4711
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
4712
|
+
import { resolve as resolve3 } from "path";
|
|
2869
4713
|
function loadRemoteConfig(configPath) {
|
|
2870
|
-
const resolved =
|
|
4714
|
+
const resolved = resolve3(configPath);
|
|
2871
4715
|
let raw;
|
|
2872
4716
|
try {
|
|
2873
|
-
raw =
|
|
4717
|
+
raw = readFileSync6(resolved, "utf8");
|
|
2874
4718
|
} catch {
|
|
2875
4719
|
throw new Error(`Cannot read remote config at ${resolved}`);
|
|
2876
4720
|
}
|
|
@@ -2941,11 +4785,98 @@ function validateRailwayEntry(entry) {
|
|
|
2941
4785
|
};
|
|
2942
4786
|
}
|
|
2943
4787
|
|
|
4788
|
+
// src/source-meta.ts
|
|
4789
|
+
var RAILWAY_SOURCE_ID = "OpenClaw - Railway";
|
|
4790
|
+
function buildLocalPushSourceMeta(kind, localHost = hostname()) {
|
|
4791
|
+
const productName = kind === "cursor" ? "Cursor" : kind === "hermes" ? "Hermes" : "OpenClaw";
|
|
4792
|
+
return {
|
|
4793
|
+
environment: "local",
|
|
4794
|
+
sourceId: `${productName} - ${localHost}`,
|
|
4795
|
+
sourceHost: localHost
|
|
4796
|
+
};
|
|
4797
|
+
}
|
|
4798
|
+
function buildRemotePushSourceMeta(source) {
|
|
4799
|
+
if (source.transport === "railway") {
|
|
4800
|
+
return {
|
|
4801
|
+
environment: "railway",
|
|
4802
|
+
sourceId: isGeneratedRailwayName(source.name) ? RAILWAY_SOURCE_ID : `OpenClaw - ${source.name}`,
|
|
4803
|
+
sourceHost: isGeneratedRailwayName(source.host) ? "Railway" : source.host
|
|
4804
|
+
};
|
|
4805
|
+
}
|
|
4806
|
+
return {
|
|
4807
|
+
environment: "remote",
|
|
4808
|
+
sourceId: `OpenClaw - ${source.name}`,
|
|
4809
|
+
sourceHost: resolveRemoteHost(source.host)
|
|
4810
|
+
};
|
|
4811
|
+
}
|
|
4812
|
+
function buildCachedPushSourceMeta(summary, localHost = hostname()) {
|
|
4813
|
+
if (summary.runtime === "cursor") {
|
|
4814
|
+
return buildLocalPushSourceMeta("cursor", localHost);
|
|
4815
|
+
}
|
|
4816
|
+
const sourceFiles = summary.sourceFiles ?? [];
|
|
4817
|
+
const comparisonKey = summary.comparisonKey ?? "";
|
|
4818
|
+
if (sourceFiles.some((sourceFile) => sourceFile.kind === "cursor-usage-csv")) {
|
|
4819
|
+
return buildLocalPushSourceMeta("cursor", localHost);
|
|
4820
|
+
}
|
|
4821
|
+
if (isRailwayComparisonKey(comparisonKey)) {
|
|
4822
|
+
return {
|
|
4823
|
+
environment: "railway",
|
|
4824
|
+
sourceId: RAILWAY_SOURCE_ID,
|
|
4825
|
+
sourceHost: "Railway"
|
|
4826
|
+
};
|
|
4827
|
+
}
|
|
4828
|
+
const remoteHost = parseRemoteHostFromComparisonKey(comparisonKey);
|
|
4829
|
+
if (remoteHost) {
|
|
4830
|
+
return {
|
|
4831
|
+
environment: "remote",
|
|
4832
|
+
sourceId: `OpenClaw - ${remoteHost}`,
|
|
4833
|
+
sourceHost: remoteHost
|
|
4834
|
+
};
|
|
4835
|
+
}
|
|
4836
|
+
if (summary.runtime === "hermes") {
|
|
4837
|
+
return buildLocalPushSourceMeta("hermes", localHost);
|
|
4838
|
+
}
|
|
4839
|
+
return buildLocalPushSourceMeta("openclaw", localHost);
|
|
4840
|
+
}
|
|
4841
|
+
function isGeneratedRailwayName(name) {
|
|
4842
|
+
return name === "railway-linked" || /^railway-[a-z0-9]{8}$/i.test(name);
|
|
4843
|
+
}
|
|
4844
|
+
function isRailwayComparisonKey(comparisonKey) {
|
|
4845
|
+
return comparisonKey.startsWith("railway:") || comparisonKey.startsWith("railway-linked:");
|
|
4846
|
+
}
|
|
4847
|
+
function parseRemoteHostFromComparisonKey(comparisonKey) {
|
|
4848
|
+
const parts = comparisonKey.split(":");
|
|
4849
|
+
if (parts.length < 3) {
|
|
4850
|
+
return null;
|
|
4851
|
+
}
|
|
4852
|
+
const target = parts.slice(0, -2).join(":");
|
|
4853
|
+
if (!target) {
|
|
4854
|
+
return null;
|
|
4855
|
+
}
|
|
4856
|
+
return resolveRemoteHost(target);
|
|
4857
|
+
}
|
|
4858
|
+
function resolveRemoteHost(target) {
|
|
4859
|
+
const parsed = parseRemoteTarget(target);
|
|
4860
|
+
return parsed.host || target;
|
|
4861
|
+
}
|
|
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
|
+
|
|
2944
4875
|
// src/commands/audit.ts
|
|
2945
|
-
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;
|
|
2946
4877
|
async function auditOrNoData(...args) {
|
|
2947
4878
|
try {
|
|
2948
|
-
return await
|
|
4879
|
+
return await auditAgentRuntime(...args);
|
|
2949
4880
|
} catch (err) {
|
|
2950
4881
|
if (err instanceof Error && NO_DATA_PATTERN.test(err.message)) {
|
|
2951
4882
|
throw new NoDataError(err.message);
|
|
@@ -2958,6 +4889,9 @@ async function runAuditCommand(options) {
|
|
|
2958
4889
|
if (options.dryRun && !options.push) {
|
|
2959
4890
|
throw new Error("--dry-run requires --push.");
|
|
2960
4891
|
}
|
|
4892
|
+
validateRuntimeOption(options.runtime);
|
|
4893
|
+
validateCursorUsageCsvOptions(options);
|
|
4894
|
+
validateHermesLocalOnly(options);
|
|
2961
4895
|
const remoteFlags = [options.remote, options.remoteConfig, options.railway].filter(
|
|
2962
4896
|
Boolean
|
|
2963
4897
|
).length;
|
|
@@ -3002,7 +4936,29 @@ function buildRailwayTarget(options) {
|
|
|
3002
4936
|
return void 0;
|
|
3003
4937
|
}
|
|
3004
4938
|
async function runLocalAudit(options, logger) {
|
|
3005
|
-
|
|
4939
|
+
if (options.cursorUsageCsv) {
|
|
4940
|
+
logger.verbose("Running a local Cursor usage CSV audit.");
|
|
4941
|
+
logger.verbose(`Using Cursor usage CSV: ${options.cursorUsageCsv}`);
|
|
4942
|
+
const summary2 = await auditCursorUsageCsv({
|
|
4943
|
+
cursorUsageCsv: options.cursorUsageCsv,
|
|
4944
|
+
since: options.since,
|
|
4945
|
+
compare: options.compare,
|
|
4946
|
+
dbPath: options.db,
|
|
4947
|
+
noDb: options.noDb,
|
|
4948
|
+
commandPrefix: options.commandPrefix,
|
|
4949
|
+
onProgress: logger.verbose
|
|
4950
|
+
});
|
|
4951
|
+
renderOutput(summary2, options);
|
|
4952
|
+
if (options.push) {
|
|
4953
|
+
const meta = buildMeta(buildLocalPushSourceMeta("cursor"));
|
|
4954
|
+
await handlePush(summary2, meta, options);
|
|
4955
|
+
}
|
|
4956
|
+
checkThresholds(summary2, options);
|
|
4957
|
+
return;
|
|
4958
|
+
}
|
|
4959
|
+
logger.verbose(
|
|
4960
|
+
options.runtime ? `Running a local ${options.runtime === "hermes" ? "Hermes" : "OpenClaw"} audit.` : "Running a local runtime audit with auto-detection."
|
|
4961
|
+
);
|
|
3006
4962
|
if (options.logFile) {
|
|
3007
4963
|
logger.verbose(`Using explicit local log file: ${options.logFile}`);
|
|
3008
4964
|
}
|
|
@@ -3010,6 +4966,7 @@ async function runLocalAudit(options, logger) {
|
|
|
3010
4966
|
logger.verbose(`Using explicit local sessions directory: ${options.sessionsDir}`);
|
|
3011
4967
|
}
|
|
3012
4968
|
const summary = await auditOrNoData({
|
|
4969
|
+
runtime: options.runtime ?? "auto",
|
|
3013
4970
|
logFile: options.logFile,
|
|
3014
4971
|
sessionsDir: options.sessionsDir,
|
|
3015
4972
|
since: options.since,
|
|
@@ -3021,10 +4978,63 @@ async function runLocalAudit(options, logger) {
|
|
|
3021
4978
|
});
|
|
3022
4979
|
renderOutput(summary, options);
|
|
3023
4980
|
if (options.push) {
|
|
3024
|
-
const meta = buildMeta(
|
|
4981
|
+
const meta = buildMeta(buildLocalPushSourceMeta(summary.runtime));
|
|
3025
4982
|
await handlePush(summary, meta, options);
|
|
3026
4983
|
}
|
|
3027
|
-
checkThresholds(summary, options);
|
|
4984
|
+
checkThresholds(summary, options);
|
|
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
|
+
}
|
|
4996
|
+
function validateCursorUsageCsvOptions(options) {
|
|
4997
|
+
if (!options.cursorUsageCsv) {
|
|
4998
|
+
return;
|
|
4999
|
+
}
|
|
5000
|
+
const conflicts = [
|
|
5001
|
+
options.runtime ? "--runtime" : null,
|
|
5002
|
+
options.logFile ? "--log-file" : null,
|
|
5003
|
+
options.sessionsDir ? "--sessions-dir" : null,
|
|
5004
|
+
options.remote ? "--remote" : null,
|
|
5005
|
+
options.remoteLogFile ? "--remote-log-file" : null,
|
|
5006
|
+
options.remoteSessionsDir ? "--remote-sessions-dir" : null,
|
|
5007
|
+
options.remoteConfig ? "--remote-config" : null,
|
|
5008
|
+
options.keepRemoteFiles ? "--keep-remote-files" : null,
|
|
5009
|
+
options.railway ? "--railway" : null,
|
|
5010
|
+
options.railwayProject ? "--project" : null,
|
|
5011
|
+
options.railwayEnvironment ? "--environment" : null,
|
|
5012
|
+
options.railwayService ? "--service" : null
|
|
5013
|
+
].filter((flag) => flag !== null);
|
|
5014
|
+
if (conflicts.length > 0) {
|
|
5015
|
+
throw new Error(`The --cursor-usage-csv flag cannot be combined with ${conflicts.join(", ")}.`);
|
|
5016
|
+
}
|
|
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
|
+
}
|
|
3028
5038
|
}
|
|
3029
5039
|
function getComparisonKey(source) {
|
|
3030
5040
|
if (source.transport === "railway") {
|
|
@@ -3044,9 +5054,6 @@ function describeSource(source) {
|
|
|
3044
5054
|
}
|
|
3045
5055
|
return source.host;
|
|
3046
5056
|
}
|
|
3047
|
-
function sourceEnvironment(source) {
|
|
3048
|
-
return source.transport === "railway" ? "railway" : "remote";
|
|
3049
|
-
}
|
|
3050
5057
|
async function runSingleRemoteAudit(source, options, logger) {
|
|
3051
5058
|
logger.info(`Pulling files from ${describeSource(source)}...`);
|
|
3052
5059
|
const pullResult = await pullFiles(
|
|
@@ -3059,6 +5066,7 @@ async function runSingleRemoteAudit(source, options, logger) {
|
|
|
3059
5066
|
try {
|
|
3060
5067
|
const comparisonKeyOverride = getComparisonKey(source);
|
|
3061
5068
|
const summary = await auditOrNoData({
|
|
5069
|
+
runtime: "openclaw",
|
|
3062
5070
|
logFile: pullResult.logFile,
|
|
3063
5071
|
sessionsDir: pullResult.sessionsDir,
|
|
3064
5072
|
since: options.since,
|
|
@@ -3071,11 +5079,7 @@ async function runSingleRemoteAudit(source, options, logger) {
|
|
|
3071
5079
|
});
|
|
3072
5080
|
renderOutput(summary, options);
|
|
3073
5081
|
if (options.push) {
|
|
3074
|
-
const meta = buildMeta(
|
|
3075
|
-
environment: sourceEnvironment(source),
|
|
3076
|
-
sourceId: source.name,
|
|
3077
|
-
sourceHost: source.host
|
|
3078
|
-
});
|
|
5082
|
+
const meta = buildMeta(buildRemotePushSourceMeta(source));
|
|
3079
5083
|
await handlePush(summary, meta, options);
|
|
3080
5084
|
}
|
|
3081
5085
|
checkThresholds(summary, options);
|
|
@@ -3113,6 +5117,7 @@ ${errorMessages}`);
|
|
|
3113
5117
|
for (const { source, pullResult } of results) {
|
|
3114
5118
|
const comparisonKeyOverride = getComparisonKey(source);
|
|
3115
5119
|
const summary = await auditOrNoData({
|
|
5120
|
+
runtime: "openclaw",
|
|
3116
5121
|
logFile: pullResult.logFile,
|
|
3117
5122
|
sessionsDir: pullResult.sessionsDir,
|
|
3118
5123
|
since: options.since,
|
|
@@ -3163,11 +5168,7 @@ ${"\u2550".repeat(60)}
|
|
|
3163
5168
|
}
|
|
3164
5169
|
if (options.push) {
|
|
3165
5170
|
for (const { source, summary } of summaries) {
|
|
3166
|
-
const meta = buildMeta(
|
|
3167
|
-
environment: sourceEnvironment(source),
|
|
3168
|
-
sourceId: source.name,
|
|
3169
|
-
sourceHost: source.host
|
|
3170
|
-
});
|
|
5171
|
+
const meta = buildMeta(buildRemotePushSourceMeta(source));
|
|
3171
5172
|
await handlePush(summary, meta, options);
|
|
3172
5173
|
}
|
|
3173
5174
|
}
|
|
@@ -3180,18 +5181,9 @@ ${"\u2550".repeat(60)}
|
|
|
3180
5181
|
}
|
|
3181
5182
|
}
|
|
3182
5183
|
}
|
|
3183
|
-
function readCliVersion() {
|
|
3184
|
-
try {
|
|
3185
|
-
const packageJsonPath = new URL("../../package.json", import.meta.url);
|
|
3186
|
-
const pkg = JSON.parse(readFileSync5(packageJsonPath, "utf8"));
|
|
3187
|
-
return pkg.version ?? "0.0.0";
|
|
3188
|
-
} catch {
|
|
3189
|
-
return "0.0.0";
|
|
3190
|
-
}
|
|
3191
|
-
}
|
|
3192
5184
|
function buildMeta(input) {
|
|
3193
5185
|
return {
|
|
3194
|
-
cliVersion:
|
|
5186
|
+
cliVersion: getCliVersion(),
|
|
3195
5187
|
sourceId: input.sourceId,
|
|
3196
5188
|
sourceHost: input.sourceHost,
|
|
3197
5189
|
environment: input.environment
|
|
@@ -3266,6 +5258,9 @@ function cleanupPullResult(pullResult, keepFiles) {
|
|
|
3266
5258
|
// src/commands/doctor.ts
|
|
3267
5259
|
async function runDoctorCommand(options) {
|
|
3268
5260
|
const logger = createCliLogger({ verbose: options.verbose });
|
|
5261
|
+
validateRuntimeOption2(options.runtime);
|
|
5262
|
+
validateCursorUsageCsvOptions2(options);
|
|
5263
|
+
validateHermesLocalOnly2(options);
|
|
3269
5264
|
if (options.railway) {
|
|
3270
5265
|
logger.verbose("Inspecting Railway audit readiness.");
|
|
3271
5266
|
const railwayTarget = buildRailwayTarget2(options);
|
|
@@ -3291,14 +5286,28 @@ async function runDoctorCommand(options) {
|
|
|
3291
5286
|
`);
|
|
3292
5287
|
return;
|
|
3293
5288
|
}
|
|
3294
|
-
|
|
5289
|
+
if (options.cursorUsageCsv) {
|
|
5290
|
+
logger.verbose("Inspecting local Cursor usage CSV audit readiness.");
|
|
5291
|
+
logger.verbose(`Using Cursor usage CSV: ${options.cursorUsageCsv}`);
|
|
5292
|
+
const report2 = await doctorCursorUsageCsv({
|
|
5293
|
+
cursorUsageCsv: options.cursorUsageCsv,
|
|
5294
|
+
onProgress: logger.verbose
|
|
5295
|
+
});
|
|
5296
|
+
process.stdout.write(`${renderCursorDoctorReport(report2)}
|
|
5297
|
+
`);
|
|
5298
|
+
return;
|
|
5299
|
+
}
|
|
5300
|
+
logger.verbose(
|
|
5301
|
+
options.runtime ? `Inspecting local ${options.runtime === "hermes" ? "Hermes" : "OpenClaw"} audit readiness.` : "Inspecting local runtime audit readiness."
|
|
5302
|
+
);
|
|
3295
5303
|
if (options.logFile) {
|
|
3296
5304
|
logger.verbose(`Using explicit local log file: ${options.logFile}`);
|
|
3297
5305
|
}
|
|
3298
5306
|
if (options.sessionsDir) {
|
|
3299
5307
|
logger.verbose(`Using explicit local sessions directory: ${options.sessionsDir}`);
|
|
3300
5308
|
}
|
|
3301
|
-
const report = await
|
|
5309
|
+
const report = await doctorAgentRuntime({
|
|
5310
|
+
runtime: options.runtime ?? "auto",
|
|
3302
5311
|
logFile: options.logFile,
|
|
3303
5312
|
sessionsDir: options.sessionsDir,
|
|
3304
5313
|
onProgress: logger.verbose
|
|
@@ -3306,6 +5315,55 @@ async function runDoctorCommand(options) {
|
|
|
3306
5315
|
process.stdout.write(`${renderDoctorReport(report, { commandPrefix: options.commandPrefix })}
|
|
3307
5316
|
`);
|
|
3308
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
|
+
}
|
|
5328
|
+
function validateCursorUsageCsvOptions2(options) {
|
|
5329
|
+
if (!options.cursorUsageCsv) {
|
|
5330
|
+
return;
|
|
5331
|
+
}
|
|
5332
|
+
const conflicts = [
|
|
5333
|
+
options.runtime ? "--runtime" : null,
|
|
5334
|
+
options.logFile ? "--log-file" : null,
|
|
5335
|
+
options.sessionsDir ? "--sessions-dir" : null,
|
|
5336
|
+
options.remote ? "--remote" : null,
|
|
5337
|
+
options.remoteLogFile ? "--remote-log-file" : null,
|
|
5338
|
+
options.remoteSessionsDir ? "--remote-sessions-dir" : null,
|
|
5339
|
+
options.railway ? "--railway" : null,
|
|
5340
|
+
options.railwayProject ? "--project" : null,
|
|
5341
|
+
options.railwayEnvironment ? "--environment" : null,
|
|
5342
|
+
options.railwayService ? "--service" : null
|
|
5343
|
+
].filter((flag) => flag !== null);
|
|
5344
|
+
if (conflicts.length > 0) {
|
|
5345
|
+
throw new Error(`The --cursor-usage-csv flag cannot be combined with ${conflicts.join(", ")}.`);
|
|
5346
|
+
}
|
|
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
|
+
}
|
|
3309
5367
|
function buildRailwayTarget2(options) {
|
|
3310
5368
|
if (options.railwayProject && options.railwayEnvironment && options.railwayService) {
|
|
3311
5369
|
return {
|
|
@@ -3515,12 +5573,12 @@ async function openBrowser(url) {
|
|
|
3515
5573
|
};
|
|
3516
5574
|
const cmd = commands[platform2()];
|
|
3517
5575
|
if (!cmd) return;
|
|
3518
|
-
return new Promise((
|
|
3519
|
-
exec(`${cmd} ${JSON.stringify(url)}`, () =>
|
|
5576
|
+
return new Promise((resolve4) => {
|
|
5577
|
+
exec(`${cmd} ${JSON.stringify(url)}`, () => resolve4());
|
|
3520
5578
|
});
|
|
3521
5579
|
}
|
|
3522
5580
|
function sleep(ms) {
|
|
3523
|
-
return new Promise((
|
|
5581
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
3524
5582
|
}
|
|
3525
5583
|
function colorBold(text) {
|
|
3526
5584
|
return process.stderr.isTTY ? styleText("bold", text) : text;
|
|
@@ -3541,8 +5599,7 @@ function runLogoutCommand() {
|
|
|
3541
5599
|
}
|
|
3542
5600
|
|
|
3543
5601
|
// src/commands/push.ts
|
|
3544
|
-
import { readFileSync as
|
|
3545
|
-
import { hostname as hostname2 } from "os";
|
|
5602
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
3546
5603
|
async function runPushCommand(options) {
|
|
3547
5604
|
const payload = options.file ? loadPayloadFromFile(options.file) : loadPayloadFromCache();
|
|
3548
5605
|
if (options.dryRun) {
|
|
@@ -3566,7 +5623,7 @@ async function runPushCommand(options) {
|
|
|
3566
5623
|
function loadPayloadFromFile(filePath) {
|
|
3567
5624
|
let raw;
|
|
3568
5625
|
try {
|
|
3569
|
-
raw =
|
|
5626
|
+
raw = readFileSync8(filePath, "utf8");
|
|
3570
5627
|
} catch {
|
|
3571
5628
|
throw new Error(`Cannot read file: ${filePath}`);
|
|
3572
5629
|
}
|
|
@@ -3600,38 +5657,155 @@ function loadPayloadFromCache() {
|
|
|
3600
5657
|
);
|
|
3601
5658
|
}
|
|
3602
5659
|
const latest = summaries[0];
|
|
3603
|
-
const meta = buildMeta2();
|
|
5660
|
+
const meta = buildMeta2(latest);
|
|
3604
5661
|
process.stderr.write(
|
|
3605
5662
|
`Using most recent cached audit: ${latest.auditId} (${latest.generatedAt})
|
|
3606
5663
|
`
|
|
3607
5664
|
);
|
|
3608
5665
|
return toWirePayload(latest, meta);
|
|
3609
5666
|
}
|
|
3610
|
-
function
|
|
3611
|
-
|
|
3612
|
-
const packageJsonPath = new URL("../../package.json", import.meta.url);
|
|
3613
|
-
const pkg = JSON.parse(readFileSync6(packageJsonPath, "utf8"));
|
|
3614
|
-
return pkg.version ?? "0.0.0";
|
|
3615
|
-
} catch {
|
|
3616
|
-
return "0.0.0";
|
|
3617
|
-
}
|
|
3618
|
-
}
|
|
3619
|
-
function buildMeta2() {
|
|
5667
|
+
function buildMeta2(summary) {
|
|
5668
|
+
const sourceMeta = buildCachedPushSourceMeta(summary);
|
|
3620
5669
|
return {
|
|
3621
|
-
cliVersion:
|
|
3622
|
-
sourceId:
|
|
3623
|
-
sourceHost:
|
|
3624
|
-
environment:
|
|
5670
|
+
cliVersion: getCliVersion(),
|
|
5671
|
+
sourceId: sourceMeta.sourceId,
|
|
5672
|
+
sourceHost: sourceMeta.sourceHost,
|
|
5673
|
+
environment: sourceMeta.environment
|
|
3625
5674
|
};
|
|
3626
5675
|
}
|
|
3627
5676
|
|
|
5677
|
+
// src/help.ts
|
|
5678
|
+
function renderRootHelp(version, display) {
|
|
5679
|
+
return `${display.name} ${version}
|
|
5680
|
+
|
|
5681
|
+
Waste intelligence for OpenClaw and Hermes workflows plus local Cursor usage CSVs.
|
|
5682
|
+
|
|
5683
|
+
Usage:
|
|
5684
|
+
${formatCommand("<command> [options]", display.prefix)}
|
|
5685
|
+
|
|
5686
|
+
Commands:
|
|
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.
|
|
5689
|
+
push Push a cached audit snapshot to the Xerg API.
|
|
5690
|
+
login Authenticate with the Xerg API via browser.
|
|
5691
|
+
logout Remove stored Xerg API credentials.
|
|
5692
|
+
|
|
5693
|
+
Global options:
|
|
5694
|
+
-h, --help Show help
|
|
5695
|
+
-v, --version Show version
|
|
5696
|
+
`;
|
|
5697
|
+
}
|
|
5698
|
+
function renderAuditHelp(commandPrefix) {
|
|
5699
|
+
return `${formatCommand("audit", commandPrefix)}
|
|
5700
|
+
|
|
5701
|
+
Analyze OpenClaw or Hermes logs, or a local Cursor usage CSV, and produce an audit report.
|
|
5702
|
+
|
|
5703
|
+
Usage:
|
|
5704
|
+
${formatCommand("audit [options]", commandPrefix)}
|
|
5705
|
+
|
|
5706
|
+
Options:
|
|
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
|
|
5710
|
+
--cursor-usage-csv <path> Local Cursor usage CSV export to analyze
|
|
5711
|
+
--since <duration> Look back window such as 24h, 7d, or 30m
|
|
5712
|
+
--compare Compare this audit to the newest compatible prior local snapshot
|
|
5713
|
+
--json Render the report as JSON
|
|
5714
|
+
--markdown Render the report as Markdown
|
|
5715
|
+
--db <path> Custom SQLite database path
|
|
5716
|
+
--no-db Skip local persistence
|
|
5717
|
+
|
|
5718
|
+
Remote options (SSH, OpenClaw only):
|
|
5719
|
+
--remote <user@host> SSH target in user@host or user@host:port format
|
|
5720
|
+
--remote-log-file <path> Override the default gateway log path on the remote host
|
|
5721
|
+
--remote-sessions-dir <path> Override the default sessions directory on the remote host
|
|
5722
|
+
--remote-config <path> Path to a JSON file defining multiple remote sources
|
|
5723
|
+
--keep-remote-files Retain pulled files in ~/.xerg/remote-cache/ instead of using a temp directory
|
|
5724
|
+
|
|
5725
|
+
Prerequisites:
|
|
5726
|
+
SSH remote audits require ssh and rsync on your PATH.
|
|
5727
|
+
|
|
5728
|
+
Railway options (OpenClaw only):
|
|
5729
|
+
--railway Audit a Railway service (uses linked project by default)
|
|
5730
|
+
--project <id> Railway project ID
|
|
5731
|
+
--environment <id> Railway environment ID
|
|
5732
|
+
--service <id> Railway service ID
|
|
5733
|
+
|
|
5734
|
+
Railway audits require the railway CLI on your PATH.
|
|
5735
|
+
|
|
5736
|
+
Push options:
|
|
5737
|
+
--push Push the audit summary to the Xerg API after computing it
|
|
5738
|
+
--dry-run With --push: print the payload to stdout without sending it
|
|
5739
|
+
--verbose Print progress updates to stderr while the audit runs
|
|
5740
|
+
|
|
5741
|
+
Threshold options:
|
|
5742
|
+
--fail-above-waste-rate <n> Exit with code 3 if structural waste rate exceeds threshold (e.g. 0.30)
|
|
5743
|
+
--fail-above-waste-usd <n> Exit with code 3 if waste spend exceeds threshold in USD (e.g. 50)
|
|
5744
|
+
|
|
5745
|
+
-h, --help Show help
|
|
5746
|
+
`;
|
|
5747
|
+
}
|
|
5748
|
+
function renderPushHelp(commandPrefix) {
|
|
5749
|
+
return `${formatCommand("push", commandPrefix)}
|
|
5750
|
+
|
|
5751
|
+
Push a cached audit snapshot to the Xerg API.
|
|
5752
|
+
|
|
5753
|
+
Usage:
|
|
5754
|
+
${formatCommand("push [options]", commandPrefix)}
|
|
5755
|
+
|
|
5756
|
+
Options:
|
|
5757
|
+
--file <path> Push a specific snapshot file instead of the most recent cached audit
|
|
5758
|
+
--dry-run Print the payload to stdout without sending it
|
|
5759
|
+
|
|
5760
|
+
-h, --help Show help
|
|
5761
|
+
|
|
5762
|
+
Authentication:
|
|
5763
|
+
Set XERG_API_KEY in your environment, add "apiKey" to ~/.xerg/config.json,
|
|
5764
|
+
or run \`${formatCommand("login", commandPrefix)}\` to authenticate via browser.
|
|
5765
|
+
Browser login stores a token at ~/.config/xerg/credentials.json by default.
|
|
5766
|
+
`;
|
|
5767
|
+
}
|
|
5768
|
+
function renderDoctorHelp(commandPrefix) {
|
|
5769
|
+
return `${formatCommand("doctor", commandPrefix)}
|
|
5770
|
+
|
|
5771
|
+
Inspect OpenClaw or Hermes sources, or a local Cursor usage CSV, before you audit.
|
|
5772
|
+
|
|
5773
|
+
Usage:
|
|
5774
|
+
${formatCommand("doctor [options]", commandPrefix)}
|
|
5775
|
+
|
|
5776
|
+
Options:
|
|
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
|
|
5780
|
+
--cursor-usage-csv <path> Local Cursor usage CSV export to inspect
|
|
5781
|
+
--verbose Print progress updates to stderr while doctor runs
|
|
5782
|
+
|
|
5783
|
+
Remote options (SSH, OpenClaw only):
|
|
5784
|
+
--remote <user@host> SSH target in user@host or user@host:port format
|
|
5785
|
+
--remote-log-file <path> Override the default gateway log path on the remote host
|
|
5786
|
+
--remote-sessions-dir <path> Override the default sessions directory on the remote host
|
|
5787
|
+
|
|
5788
|
+
SSH checks require ssh and rsync on your PATH.
|
|
5789
|
+
|
|
5790
|
+
Railway options (OpenClaw only):
|
|
5791
|
+
--railway Check a Railway service (uses linked project by default)
|
|
5792
|
+
--project <id> Railway project ID
|
|
5793
|
+
--environment <id> Railway environment ID
|
|
5794
|
+
--service <id> Railway service ID
|
|
5795
|
+
|
|
5796
|
+
Railway checks require the railway CLI on your PATH.
|
|
5797
|
+
|
|
5798
|
+
-h, --help Show help
|
|
5799
|
+
`;
|
|
5800
|
+
}
|
|
5801
|
+
|
|
3628
5802
|
// src/index.ts
|
|
3629
|
-
var VERSION =
|
|
5803
|
+
var VERSION = getCliVersion();
|
|
3630
5804
|
var argv = process.argv.slice(2);
|
|
3631
5805
|
var commandDisplay = resolveCommandDisplay();
|
|
3632
5806
|
var command = argv[0];
|
|
3633
5807
|
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
3634
|
-
process.stdout.write(renderRootHelp(commandDisplay));
|
|
5808
|
+
process.stdout.write(renderRootHelp(VERSION, commandDisplay));
|
|
3635
5809
|
process.exit(0);
|
|
3636
5810
|
}
|
|
3637
5811
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
@@ -3697,10 +5871,18 @@ function parseAuditOptions(raw) {
|
|
|
3697
5871
|
options.logFile = readValue(arg, argv2[index + 1]);
|
|
3698
5872
|
index += 1;
|
|
3699
5873
|
break;
|
|
5874
|
+
case "--runtime":
|
|
5875
|
+
options.runtime = readValue(arg, argv2[index + 1]);
|
|
5876
|
+
index += 1;
|
|
5877
|
+
break;
|
|
3700
5878
|
case "--sessions-dir":
|
|
3701
5879
|
options.sessionsDir = readValue(arg, argv2[index + 1]);
|
|
3702
5880
|
index += 1;
|
|
3703
5881
|
break;
|
|
5882
|
+
case "--cursor-usage-csv":
|
|
5883
|
+
options.cursorUsageCsv = readValue(arg, argv2[index + 1]);
|
|
5884
|
+
index += 1;
|
|
5885
|
+
break;
|
|
3704
5886
|
case "--since":
|
|
3705
5887
|
options.since = readValue(arg, argv2[index + 1]);
|
|
3706
5888
|
index += 1;
|
|
@@ -3821,10 +6003,18 @@ function parseDoctorOptions(raw) {
|
|
|
3821
6003
|
options.logFile = readValue(arg, argv2[index + 1]);
|
|
3822
6004
|
index += 1;
|
|
3823
6005
|
break;
|
|
6006
|
+
case "--runtime":
|
|
6007
|
+
options.runtime = readValue(arg, argv2[index + 1]);
|
|
6008
|
+
index += 1;
|
|
6009
|
+
break;
|
|
3824
6010
|
case "--sessions-dir":
|
|
3825
6011
|
options.sessionsDir = readValue(arg, argv2[index + 1]);
|
|
3826
6012
|
index += 1;
|
|
3827
6013
|
break;
|
|
6014
|
+
case "--cursor-usage-csv":
|
|
6015
|
+
options.cursorUsageCsv = readValue(arg, argv2[index + 1]);
|
|
6016
|
+
index += 1;
|
|
6017
|
+
break;
|
|
3828
6018
|
case "--remote":
|
|
3829
6019
|
options.remote = readValue(arg, argv2[index + 1]);
|
|
3830
6020
|
index += 1;
|
|
@@ -3889,131 +6079,7 @@ function readFloat(flag, value) {
|
|
|
3889
6079
|
}
|
|
3890
6080
|
return num;
|
|
3891
6081
|
}
|
|
3892
|
-
function renderRootHelp(display = commandDisplay) {
|
|
3893
|
-
return `${display.name} ${VERSION}
|
|
3894
|
-
|
|
3895
|
-
Waste intelligence for OpenClaw workflows.
|
|
3896
|
-
|
|
3897
|
-
Usage:
|
|
3898
|
-
${formatCommand("<command> [options]", display.prefix)}
|
|
3899
|
-
|
|
3900
|
-
Commands:
|
|
3901
|
-
audit Analyze OpenClaw logs and produce a waste intelligence report.
|
|
3902
|
-
doctor Inspect your machine for OpenClaw sources and audit readiness.
|
|
3903
|
-
push Push a cached audit snapshot to the Xerg API.
|
|
3904
|
-
login Authenticate with the Xerg API via browser.
|
|
3905
|
-
logout Remove stored Xerg API credentials.
|
|
3906
|
-
|
|
3907
|
-
Global options:
|
|
3908
|
-
-h, --help Show help
|
|
3909
|
-
-v, --version Show version
|
|
3910
|
-
`;
|
|
3911
|
-
}
|
|
3912
|
-
function renderAuditHelp(commandPrefix = commandDisplay.prefix) {
|
|
3913
|
-
return `${formatCommand("audit", commandPrefix)}
|
|
3914
|
-
|
|
3915
|
-
Analyze OpenClaw logs and produce a waste intelligence report.
|
|
3916
|
-
|
|
3917
|
-
Usage:
|
|
3918
|
-
${formatCommand("audit [options]", commandPrefix)}
|
|
3919
|
-
|
|
3920
|
-
Options:
|
|
3921
|
-
--log-file <path> Explicit OpenClaw gateway log file to analyze
|
|
3922
|
-
--sessions-dir <path> Explicit OpenClaw sessions directory to analyze
|
|
3923
|
-
--since <duration> Look back window such as 24h, 7d, or 30m
|
|
3924
|
-
--compare Compare this audit to the newest compatible prior local snapshot
|
|
3925
|
-
--json Render the report as JSON
|
|
3926
|
-
--markdown Render the report as Markdown
|
|
3927
|
-
--db <path> Custom SQLite database path
|
|
3928
|
-
--no-db Skip local persistence
|
|
3929
|
-
|
|
3930
|
-
Remote options (SSH):
|
|
3931
|
-
--remote <user@host> SSH target in user@host or user@host:port format
|
|
3932
|
-
--remote-log-file <path> Override the default gateway log path on the remote host
|
|
3933
|
-
--remote-sessions-dir <path> Override the default sessions directory on the remote host
|
|
3934
|
-
--remote-config <path> Path to a JSON file defining multiple remote sources
|
|
3935
|
-
--keep-remote-files Retain pulled files in ~/.xerg/remote-cache/ instead of using a temp directory
|
|
3936
|
-
|
|
3937
|
-
Prerequisites:
|
|
3938
|
-
SSH remote audits require ssh and rsync on your PATH.
|
|
3939
|
-
|
|
3940
|
-
Railway options:
|
|
3941
|
-
--railway Audit a Railway service (uses linked project by default)
|
|
3942
|
-
--project <id> Railway project ID
|
|
3943
|
-
--environment <id> Railway environment ID
|
|
3944
|
-
--service <id> Railway service ID
|
|
3945
|
-
|
|
3946
|
-
Railway audits require the railway CLI on your PATH.
|
|
3947
|
-
|
|
3948
|
-
Push options:
|
|
3949
|
-
--push Push the audit summary to the Xerg API after computing it
|
|
3950
|
-
--dry-run With --push: print the payload to stdout without sending it
|
|
3951
|
-
--verbose Print progress updates to stderr while the audit runs
|
|
3952
|
-
|
|
3953
|
-
Threshold options:
|
|
3954
|
-
--fail-above-waste-rate <n> Exit with code 3 if structural waste rate exceeds threshold (e.g. 0.30)
|
|
3955
|
-
--fail-above-waste-usd <n> Exit with code 3 if waste spend exceeds threshold in USD (e.g. 50)
|
|
3956
|
-
|
|
3957
|
-
-h, --help Show help
|
|
3958
|
-
`;
|
|
3959
|
-
}
|
|
3960
|
-
function renderPushHelp(commandPrefix = commandDisplay.prefix) {
|
|
3961
|
-
return `${formatCommand("push", commandPrefix)}
|
|
3962
|
-
|
|
3963
|
-
Push a cached audit snapshot to the Xerg API.
|
|
3964
|
-
|
|
3965
|
-
Usage:
|
|
3966
|
-
${formatCommand("push [options]", commandPrefix)}
|
|
3967
|
-
|
|
3968
|
-
Options:
|
|
3969
|
-
--file <path> Push a specific snapshot file instead of the most recent cached audit
|
|
3970
|
-
--dry-run Print the payload to stdout without sending it
|
|
3971
|
-
|
|
3972
|
-
-h, --help Show help
|
|
3973
|
-
|
|
3974
|
-
Authentication:
|
|
3975
|
-
Set XERG_API_KEY in your environment, add "apiKey" to ~/.xerg/config.json,
|
|
3976
|
-
or run \`${formatCommand("login", commandPrefix)}\` to authenticate via browser.
|
|
3977
|
-
Browser login stores a token at ~/.config/xerg/credentials.json by default.
|
|
3978
|
-
`;
|
|
3979
|
-
}
|
|
3980
|
-
function renderDoctorHelp(commandPrefix = commandDisplay.prefix) {
|
|
3981
|
-
return `${formatCommand("doctor", commandPrefix)}
|
|
3982
|
-
|
|
3983
|
-
Inspect your machine for OpenClaw sources and audit readiness.
|
|
3984
|
-
|
|
3985
|
-
Usage:
|
|
3986
|
-
${formatCommand("doctor [options]", commandPrefix)}
|
|
3987
|
-
|
|
3988
|
-
Options:
|
|
3989
|
-
--log-file <path> Explicit OpenClaw gateway log file to inspect
|
|
3990
|
-
--sessions-dir <path> Explicit OpenClaw sessions directory to inspect
|
|
3991
|
-
--verbose Print progress updates to stderr while doctor runs
|
|
3992
|
-
|
|
3993
|
-
Remote options (SSH):
|
|
3994
|
-
--remote <user@host> SSH target in user@host or user@host:port format
|
|
3995
|
-
--remote-log-file <path> Override the default gateway log path on the remote host
|
|
3996
|
-
--remote-sessions-dir <path> Override the default sessions directory on the remote host
|
|
3997
|
-
|
|
3998
|
-
SSH checks require ssh and rsync on your PATH.
|
|
3999
|
-
|
|
4000
|
-
Railway options:
|
|
4001
|
-
--railway Check a Railway service (uses linked project by default)
|
|
4002
|
-
--project <id> Railway project ID
|
|
4003
|
-
--environment <id> Railway environment ID
|
|
4004
|
-
--service <id> Railway service ID
|
|
4005
|
-
|
|
4006
|
-
Railway checks require the railway CLI on your PATH.
|
|
4007
|
-
|
|
4008
|
-
-h, --help Show help
|
|
4009
|
-
`;
|
|
4010
|
-
}
|
|
4011
6082
|
function colorError(message) {
|
|
4012
6083
|
return process.stderr.isTTY ? styleText2("red", message) : message;
|
|
4013
6084
|
}
|
|
4014
|
-
function readVersion() {
|
|
4015
|
-
const packageJsonPath = new URL("../package.json", import.meta.url);
|
|
4016
|
-
const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf8"));
|
|
4017
|
-
return packageJson.version ?? "0.0.0";
|
|
4018
|
-
}
|
|
4019
6085
|
//# sourceMappingURL=index.js.map
|