@xerg/cli 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,13 +1,122 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync7 } from "fs";
4
+ import { readFileSync as readFileSync8 } from "fs";
5
5
  import { styleText as styleText2 } from "util";
6
6
 
7
+ // src/command-display.ts
8
+ var PACKAGE_NAME = "@xerg/cli";
9
+ var DEFAULT_COMMAND_PREFIX = "xerg";
10
+ function resolveCommandDisplay(context) {
11
+ const runner = detectPackageExecutor(context);
12
+ if (!runner) {
13
+ return {
14
+ prefix: DEFAULT_COMMAND_PREFIX,
15
+ name: DEFAULT_COMMAND_PREFIX
16
+ };
17
+ }
18
+ return {
19
+ prefix: `${runner} ${PACKAGE_NAME}`,
20
+ name: PACKAGE_NAME
21
+ };
22
+ }
23
+ function formatCommand(command2, commandPrefix = resolveCommandDisplay().prefix) {
24
+ const suffix = Array.isArray(command2) ? command2.join(" ") : command2;
25
+ return suffix ? `${commandPrefix} ${suffix}` : commandPrefix;
26
+ }
27
+ function detectPackageExecutor(context) {
28
+ const env = context?.env ?? process.env;
29
+ const argv2 = context?.argv ?? process.argv;
30
+ const userAgent = normalizeSignal(env.npm_config_user_agent);
31
+ const execPath = normalizeSignal(env.npm_execpath);
32
+ const argvPath = normalizeSignal(argv2[1]);
33
+ const fromArgvPath = detectRunnerFromArgvPath(argvPath);
34
+ if (fromArgvPath) {
35
+ return fromArgvPath;
36
+ }
37
+ if (looksLikeInstalledCli(argvPath)) {
38
+ return null;
39
+ }
40
+ const fromUserAgent = detectRunnerFromSignal(userAgent);
41
+ if (fromUserAgent) {
42
+ return fromUserAgent;
43
+ }
44
+ const fromExecPath = detectRunnerFromSignal(execPath);
45
+ if (fromExecPath) {
46
+ return fromExecPath;
47
+ }
48
+ if (argvPath.includes("/_npx/") || argvPath.includes("\\_npx\\")) {
49
+ return "npx";
50
+ }
51
+ if (argvPath.includes("/.yarn/") && argvPath.includes("/dlx/")) {
52
+ return "yarn dlx";
53
+ }
54
+ if (argvPath.includes("/bunx/") || argvPath.includes("\\bunx\\")) {
55
+ return "bunx";
56
+ }
57
+ if (argvPath.includes("/dlx-") || argvPath.includes("\\dlx-")) {
58
+ return "pnpm dlx";
59
+ }
60
+ if (userAgent || execPath) {
61
+ return "npx";
62
+ }
63
+ return null;
64
+ }
65
+ function detectRunnerFromArgvPath(argvPath) {
66
+ if (!argvPath) {
67
+ return null;
68
+ }
69
+ if (argvPath.includes("/_npx/") || argvPath.includes("\\_npx\\")) {
70
+ return "npx";
71
+ }
72
+ if (argvPath.includes("/.yarn/") && argvPath.includes("/dlx/")) {
73
+ return "yarn dlx";
74
+ }
75
+ if (argvPath.includes("/bunx/") || argvPath.includes("\\bunx\\")) {
76
+ return "bunx";
77
+ }
78
+ if (argvPath.includes("/dlx-") || argvPath.includes("\\dlx-")) {
79
+ return "pnpm dlx";
80
+ }
81
+ return null;
82
+ }
83
+ function looksLikeInstalledCli(argvPath) {
84
+ if (!argvPath) {
85
+ return false;
86
+ }
87
+ const normalized = argvPath.replaceAll("\\", "/");
88
+ return normalized.endsWith("/node_modules/.bin/xerg") || normalized.includes("/node_modules/@xerg/cli/dist/index.js") || normalized.endsWith("/bin/xerg");
89
+ }
90
+ function detectRunnerFromSignal(signal) {
91
+ if (!signal) {
92
+ return null;
93
+ }
94
+ const tokens = signal.split(/[^a-z0-9]+/).filter(Boolean);
95
+ if (tokens.includes("pnpm")) {
96
+ return "pnpm dlx";
97
+ }
98
+ if (tokens.includes("yarn")) {
99
+ return "yarn dlx";
100
+ }
101
+ if (tokens.includes("bun")) {
102
+ return "bunx";
103
+ }
104
+ if (tokens.includes("npm")) {
105
+ return "npx";
106
+ }
107
+ return null;
108
+ }
109
+ function normalizeSignal(value) {
110
+ return value?.trim().toLowerCase() ?? "";
111
+ }
112
+
7
113
  // src/commands/audit.ts
8
- import { readFileSync as readFileSync5 } from "fs";
114
+ import { readFileSync as readFileSync6 } from "fs";
9
115
  import { rmSync as rmSync4 } from "fs";
10
- import { hostname } from "os";
116
+
117
+ // ../core/src/cursor/usage-csv.ts
118
+ import { readFileSync, statSync } from "fs";
119
+ import { resolve } from "path";
11
120
 
12
121
  // ../core/src/utils/hash.ts
13
122
  import { createHash } from "crypto";
@@ -71,6 +180,562 @@ function toIsoOrNow(value) {
71
180
  return isoNow();
72
181
  }
73
182
 
183
+ // ../core/src/cursor/usage-csv.ts
184
+ var REQUIRED_HEADERS = [
185
+ "Date",
186
+ "Kind",
187
+ "Model",
188
+ "Max Mode",
189
+ "Input (w/ Cache Write)",
190
+ "Input (w/o Cache Write)",
191
+ "Cache Read",
192
+ "Output Tokens",
193
+ "Total Tokens",
194
+ "Cost"
195
+ ];
196
+ var CURSOR_ALIAS_PRICING = {
197
+ "claude-4.6-opus-high-thinking": {
198
+ provider: "anthropic",
199
+ canonicalModel: "claude-opus-4",
200
+ inputPer1m: 15,
201
+ outputPer1m: 75,
202
+ cacheWritePer1m: 18.75,
203
+ cachedInputPer1m: 1.5
204
+ },
205
+ "claude-4.5-sonnet": {
206
+ provider: "anthropic",
207
+ canonicalModel: "claude-sonnet-4-5",
208
+ inputPer1m: 3,
209
+ outputPer1m: 15,
210
+ cacheWritePer1m: 3.75,
211
+ cachedInputPer1m: 0.3
212
+ },
213
+ "claude-4.5-sonnet-thinking": {
214
+ provider: "anthropic",
215
+ canonicalModel: "claude-sonnet-4-5",
216
+ inputPer1m: 3,
217
+ outputPer1m: 15,
218
+ cacheWritePer1m: 3.75,
219
+ cachedInputPer1m: 0.3
220
+ },
221
+ "claude-4.5-opus-high-thinking": {
222
+ provider: "anthropic",
223
+ canonicalModel: "claude-opus-4-5",
224
+ inputPer1m: 5,
225
+ outputPer1m: 25,
226
+ cacheWritePer1m: 6.25,
227
+ cachedInputPer1m: 0.5
228
+ },
229
+ "gpt-5.1-codex": {
230
+ provider: "openai",
231
+ canonicalModel: "gpt-5.1-codex",
232
+ inputPer1m: 1.25,
233
+ outputPer1m: 10,
234
+ cacheWritePer1m: 1.25,
235
+ cachedInputPer1m: 0.125
236
+ },
237
+ "gpt-5-high-fast": {
238
+ provider: "openai",
239
+ canonicalModel: "gpt-5-high-fast",
240
+ inputPer1m: 1.25,
241
+ outputPer1m: 10,
242
+ cacheWritePer1m: 1.25,
243
+ cachedInputPer1m: 0.125
244
+ },
245
+ "gpt-5": {
246
+ provider: "openai",
247
+ canonicalModel: "gpt-5",
248
+ inputPer1m: 1.25,
249
+ outputPer1m: 10,
250
+ cacheWritePer1m: 1.25,
251
+ cachedInputPer1m: 0.125
252
+ }
253
+ };
254
+ function round(value) {
255
+ return Number(value.toFixed(6));
256
+ }
257
+ function parseCsvLine(line) {
258
+ const values = [];
259
+ let current = "";
260
+ let inQuotes = false;
261
+ for (let index = 0; index < line.length; index += 1) {
262
+ const char = line[index];
263
+ if (char === '"') {
264
+ const next = line[index + 1];
265
+ if (inQuotes && next === '"') {
266
+ current += '"';
267
+ index += 1;
268
+ } else {
269
+ inQuotes = !inQuotes;
270
+ }
271
+ continue;
272
+ }
273
+ if (char === "," && !inQuotes) {
274
+ values.push(current);
275
+ current = "";
276
+ continue;
277
+ }
278
+ current += char;
279
+ }
280
+ values.push(current);
281
+ return values.map((value) => value.trim());
282
+ }
283
+ function parseInteger(raw, column, rowNumber) {
284
+ const parsed = Number.parseInt(raw, 10);
285
+ if (!Number.isFinite(parsed) || parsed < 0) {
286
+ throw new Error(`Invalid ${column} value "${raw}" on row ${rowNumber}.`);
287
+ }
288
+ return parsed;
289
+ }
290
+ function parseTimestamp(raw, rowNumber) {
291
+ const parsed = new Date(raw);
292
+ if (Number.isNaN(parsed.getTime())) {
293
+ throw new Error(`Invalid Date value "${raw}" on row ${rowNumber}.`);
294
+ }
295
+ return parsed.toISOString();
296
+ }
297
+ function parseMaxMode(raw) {
298
+ return raw.trim().toLowerCase() === "yes";
299
+ }
300
+ function parseObservedCost(raw) {
301
+ const value = raw.trim();
302
+ if (value.length === 0 || value === "-" || value.toLowerCase() === "included") {
303
+ return null;
304
+ }
305
+ const parsed = Number.parseFloat(value);
306
+ return Number.isFinite(parsed) ? parsed : null;
307
+ }
308
+ function createDetectedSource(path) {
309
+ const resolvedPath = resolve(path);
310
+ try {
311
+ const stats = statSync(resolvedPath);
312
+ if (!stats.isFile()) {
313
+ throw new Error(`Cursor usage CSV path is not a file: ${resolvedPath}`);
314
+ }
315
+ return {
316
+ kind: "cursor-usage-csv",
317
+ path: resolvedPath,
318
+ sizeBytes: stats.size,
319
+ mtimeMs: stats.mtimeMs
320
+ };
321
+ } catch (error) {
322
+ const message = error instanceof Error ? error.message : `Cursor usage CSV not found: ${path}`;
323
+ throw new Error(message);
324
+ }
325
+ }
326
+ function validateHeaders(headers) {
327
+ const missing = REQUIRED_HEADERS.filter((header) => !headers.includes(header));
328
+ if (missing.length > 0) {
329
+ throw new Error(`Cursor usage CSV is missing required headers: ${missing.join(", ")}.`);
330
+ }
331
+ }
332
+ function parseRow(values, headers, rowNumber) {
333
+ const record = Object.fromEntries(headers.map((header, index) => [header, values[index] ?? ""]));
334
+ const costLabel = record.Cost ?? "";
335
+ return {
336
+ timestamp: parseTimestamp(record.Date ?? "", rowNumber),
337
+ kind: record.Kind ?? "",
338
+ modelAlias: record.Model ?? "",
339
+ maxMode: parseMaxMode(record["Max Mode"] ?? ""),
340
+ inputWithCacheWriteTokens: parseInteger(
341
+ record["Input (w/ Cache Write)"] ?? "",
342
+ "Input (w/ Cache Write)",
343
+ rowNumber
344
+ ),
345
+ inputWithoutCacheWriteTokens: parseInteger(
346
+ record["Input (w/o Cache Write)"] ?? "",
347
+ "Input (w/o Cache Write)",
348
+ rowNumber
349
+ ),
350
+ cacheReadTokens: parseInteger(record["Cache Read"] ?? "", "Cache Read", rowNumber),
351
+ outputTokens: parseInteger(record["Output Tokens"] ?? "", "Output Tokens", rowNumber),
352
+ totalTokens: parseInteger(record["Total Tokens"] ?? "", "Total Tokens", rowNumber),
353
+ costLabel,
354
+ observedCostUsd: parseObservedCost(costLabel)
355
+ };
356
+ }
357
+ function parseRows(lines, headers) {
358
+ const rows = [];
359
+ for (let index = 0; index < lines.length; index += 1) {
360
+ const line = lines[index];
361
+ if (!line.trim()) {
362
+ continue;
363
+ }
364
+ rows.push(parseRow(parseCsvLine(line), headers, index + 2));
365
+ }
366
+ return rows;
367
+ }
368
+ function readCursorUsageCsv(path) {
369
+ const source = createDetectedSource(path);
370
+ const content = readFileSync(source.path, "utf8");
371
+ const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
372
+ if (lines.length === 0) {
373
+ throw new Error(`Cursor usage CSV is empty: ${source.path}`);
374
+ }
375
+ const headers = parseCsvLine(lines[0]);
376
+ validateHeaders(headers);
377
+ const rows = parseRows(lines.slice(1), headers);
378
+ return {
379
+ source,
380
+ rows,
381
+ headers,
382
+ hasObservedCostRows: rows.some((row) => row.observedCostUsd !== null)
383
+ };
384
+ }
385
+ function isErroredNoCharge(kind) {
386
+ const normalized = kind.trim().toLowerCase();
387
+ return normalized.includes("errored") && normalized.includes("no charge") || normalized.includes("not charged");
388
+ }
389
+ function inferProvider(modelAlias) {
390
+ const normalized = modelAlias.trim().toLowerCase();
391
+ if (normalized.startsWith("claude-")) {
392
+ return "anthropic";
393
+ }
394
+ if (normalized.startsWith("gpt-")) {
395
+ return "openai";
396
+ }
397
+ return "cursor";
398
+ }
399
+ function buildModelKey(modelAlias, pricing) {
400
+ if (pricing) {
401
+ return `${pricing.provider}/${pricing.canonicalModel}`;
402
+ }
403
+ return `${inferProvider(modelAlias)}/${modelAlias}`;
404
+ }
405
+ function getWorkflowKey(row) {
406
+ const kind = row.kind.trim().toLowerCase();
407
+ if (kind.includes("on-demand")) {
408
+ return row.maxMode ? "on-demand / max mode" : "on-demand / standard mode";
409
+ }
410
+ if (kind.includes("included")) {
411
+ return row.maxMode ? "included / max mode" : "included / standard mode";
412
+ }
413
+ if (kind.includes("error") || kind.includes("not charged")) {
414
+ return "not charged / failed or aborted";
415
+ }
416
+ return row.maxMode ? "other / max mode" : "other / standard mode";
417
+ }
418
+ function estimateCursorRowCost(row, options) {
419
+ const pricing = CURSOR_ALIAS_PRICING[row.modelAlias.trim().toLowerCase()] ?? null;
420
+ if (isErroredNoCharge(row.kind)) {
421
+ return {
422
+ costUsd: 0,
423
+ costSource: "observed",
424
+ cacheCostUsd: 0,
425
+ cacheWriteCostUsd: 0,
426
+ pricing,
427
+ canonicalModelKey: buildModelKey(row.modelAlias, pricing)
428
+ };
429
+ }
430
+ if (options.preferObservedCost) {
431
+ if (row.observedCostUsd !== null) {
432
+ const cacheCost2 = row.cacheReadTokens > 0 && pricing?.cachedInputPer1m !== void 0 ? round(row.cacheReadTokens / 1e6 * pricing.cachedInputPer1m) : null;
433
+ const cacheWriteCost2 = row.inputWithCacheWriteTokens > 0 && pricing ? round(
434
+ row.inputWithCacheWriteTokens / 1e6 * (pricing.cacheWritePer1m ?? pricing.inputPer1m)
435
+ ) : null;
436
+ return {
437
+ costUsd: row.observedCostUsd,
438
+ costSource: "observed",
439
+ cacheCostUsd: cacheCost2,
440
+ cacheWriteCostUsd: cacheWriteCost2,
441
+ pricing,
442
+ canonicalModelKey: buildModelKey(row.modelAlias, pricing)
443
+ };
444
+ }
445
+ return {
446
+ costUsd: 0,
447
+ costSource: "observed",
448
+ cacheCostUsd: 0,
449
+ cacheWriteCostUsd: 0,
450
+ pricing,
451
+ canonicalModelKey: buildModelKey(row.modelAlias, pricing)
452
+ };
453
+ }
454
+ if (!pricing) {
455
+ return {
456
+ costUsd: 0,
457
+ costSource: "unpriced",
458
+ cacheCostUsd: null,
459
+ cacheWriteCostUsd: null,
460
+ pricing: null,
461
+ canonicalModelKey: buildModelKey(row.modelAlias, pricing)
462
+ };
463
+ }
464
+ if (row.cacheReadTokens > 0 && pricing.cachedInputPer1m === void 0) {
465
+ return {
466
+ costUsd: 0,
467
+ costSource: "unpriced",
468
+ cacheCostUsd: null,
469
+ cacheWriteCostUsd: null,
470
+ pricing: null,
471
+ canonicalModelKey: buildModelKey(row.modelAlias, pricing)
472
+ };
473
+ }
474
+ const inputCost = row.inputWithoutCacheWriteTokens / 1e6 * pricing.inputPer1m;
475
+ const cacheWriteCost = row.inputWithCacheWriteTokens / 1e6 * (pricing.cacheWritePer1m ?? pricing.inputPer1m);
476
+ const outputCost = row.outputTokens / 1e6 * pricing.outputPer1m;
477
+ const cacheCost = row.cacheReadTokens > 0 && pricing.cachedInputPer1m !== void 0 ? row.cacheReadTokens / 1e6 * pricing.cachedInputPer1m : 0;
478
+ return {
479
+ costUsd: round(inputCost + cacheWriteCost + outputCost + cacheCost),
480
+ costSource: "estimated",
481
+ cacheCostUsd: round(cacheCost),
482
+ cacheWriteCostUsd: round(cacheWriteCost),
483
+ pricing,
484
+ canonicalModelKey: buildModelKey(row.modelAlias, pricing)
485
+ };
486
+ }
487
+ function buildCall(row, source, runId, index, options) {
488
+ const cost = estimateCursorRowCost(row, options);
489
+ const totalInputTokens = Math.max(row.totalTokens - row.outputTokens, 0);
490
+ return {
491
+ cost,
492
+ call: {
493
+ id: sha1(`${runId}:${source.path}:${index}:${row.modelAlias}:${row.timestamp}`),
494
+ runId,
495
+ timestamp: row.timestamp,
496
+ provider: cost.pricing?.provider ?? inferProvider(row.modelAlias),
497
+ model: cost.pricing?.canonicalModel ?? row.modelAlias,
498
+ inputTokens: totalInputTokens,
499
+ outputTokens: row.outputTokens,
500
+ costUsd: cost.costUsd,
501
+ costSource: cost.costSource,
502
+ latencyMs: null,
503
+ toolCalls: 0,
504
+ retries: 0,
505
+ attempt: null,
506
+ iteration: null,
507
+ status: isErroredNoCharge(row.kind) ? "error" : null,
508
+ taskClass: null,
509
+ cacheHit: row.cacheReadTokens > 0,
510
+ cacheCostUsd: cost.cacheCostUsd,
511
+ metadata: {
512
+ source: "cursor-usage-csv",
513
+ kind: row.kind,
514
+ maxMode: row.maxMode,
515
+ modelAlias: row.modelAlias,
516
+ costLabel: row.costLabel,
517
+ totalTokens: row.totalTokens,
518
+ inputWithCacheWriteTokens: row.inputWithCacheWriteTokens,
519
+ inputWithoutCacheWriteTokens: row.inputWithoutCacheWriteTokens,
520
+ cacheReadTokens: row.cacheReadTokens,
521
+ pricingProvider: cost.pricing?.provider ?? null,
522
+ pricingModel: cost.pricing?.canonicalModel ?? null,
523
+ canonicalModelKey: cost.canonicalModelKey,
524
+ observedCostUsd: row.observedCostUsd,
525
+ cacheWriteCostUsd: cost.cacheWriteCostUsd
526
+ }
527
+ }
528
+ };
529
+ }
530
+ function normalizeCursorUsageCsv(input) {
531
+ const cutoff = parseSince(input.since);
532
+ const runs = [];
533
+ const modelCoverage = /* @__PURE__ */ new Map();
534
+ const modes = /* @__PURE__ */ new Map();
535
+ const models = /* @__PURE__ */ new Map();
536
+ let pricedCallCount = 0;
537
+ let unpricedCallCount = 0;
538
+ let pricedTokenCount = 0;
539
+ let unpricedTokenCount = 0;
540
+ let totalTokens = 0;
541
+ let totalOutputTokens = 0;
542
+ let totalCacheReadTokens = 0;
543
+ let totalInputWithCacheWriteTokens = 0;
544
+ let totalInputWithoutCacheWriteTokens = 0;
545
+ input.rows.forEach((row, index) => {
546
+ const timestampMs = new Date(row.timestamp).getTime();
547
+ if (cutoff && timestampMs < cutoff) {
548
+ return;
549
+ }
550
+ const workflow = getWorkflowKey(row);
551
+ const runId = sha1(`${input.source.path}:${row.timestamp}:${row.modelAlias}:${index}`);
552
+ const { call, cost } = buildCall(row, input.source, runId, index, {
553
+ preferObservedCost: input.hasObservedCostRows ?? false
554
+ });
555
+ const run2 = {
556
+ id: runId,
557
+ sourceKind: input.source.kind,
558
+ sourcePath: input.source.path,
559
+ timestamp: row.timestamp,
560
+ workflow,
561
+ environment: "local",
562
+ tags: {
563
+ sourceKind: input.source.kind,
564
+ maxMode: row.maxMode,
565
+ kind: row.kind
566
+ },
567
+ calls: [call],
568
+ totalCostUsd: call.costUsd,
569
+ totalTokens: row.totalTokens,
570
+ observedCostUsd: call.costSource === "observed" ? call.costUsd : 0,
571
+ estimatedCostUsd: call.costSource === "estimated" ? call.costUsd : 0
572
+ };
573
+ runs.push(run2);
574
+ totalTokens += row.totalTokens;
575
+ totalOutputTokens += row.outputTokens;
576
+ totalCacheReadTokens += row.cacheReadTokens;
577
+ totalInputWithCacheWriteTokens += row.inputWithCacheWriteTokens;
578
+ totalInputWithoutCacheWriteTokens += row.inputWithoutCacheWriteTokens;
579
+ const totalRowTokens = row.totalTokens;
580
+ if (cost.costSource === "unpriced") {
581
+ unpricedCallCount += 1;
582
+ unpricedTokenCount += totalRowTokens;
583
+ const current = modelCoverage.get(row.modelAlias) ?? { callCount: 0, totalTokens: 0 };
584
+ current.callCount += 1;
585
+ current.totalTokens += totalRowTokens;
586
+ modelCoverage.set(row.modelAlias, current);
587
+ } else {
588
+ pricedCallCount += 1;
589
+ pricedTokenCount += totalRowTokens;
590
+ }
591
+ const modeBucket = modes.get(workflow) ?? {
592
+ callCount: 0,
593
+ totalTokens: 0,
594
+ estimatedSpendUsd: 0
595
+ };
596
+ modeBucket.callCount += 1;
597
+ modeBucket.totalTokens += totalRowTokens;
598
+ modeBucket.estimatedSpendUsd = round(modeBucket.estimatedSpendUsd + call.costUsd);
599
+ modes.set(workflow, modeBucket);
600
+ const modelBucket = models.get(cost.canonicalModelKey) ?? {
601
+ callCount: 0,
602
+ totalTokens: 0,
603
+ estimatedSpendUsd: 0,
604
+ pricedCallCount: 0,
605
+ unpricedCallCount: 0
606
+ };
607
+ modelBucket.callCount += 1;
608
+ modelBucket.totalTokens += totalRowTokens;
609
+ modelBucket.estimatedSpendUsd = round(modelBucket.estimatedSpendUsd + call.costUsd);
610
+ if (cost.costSource === "unpriced") {
611
+ modelBucket.unpricedCallCount += 1;
612
+ } else {
613
+ modelBucket.pricedCallCount += 1;
614
+ }
615
+ models.set(cost.canonicalModelKey, modelBucket);
616
+ });
617
+ runs.sort(
618
+ (left, right) => new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime()
619
+ );
620
+ return {
621
+ runs,
622
+ pricingCoverage: {
623
+ pricedCallCount,
624
+ unpricedCallCount,
625
+ pricedTokenCount,
626
+ unpricedTokenCount,
627
+ topUnpricedModels: Array.from(modelCoverage.entries()).map(([key, value]) => ({
628
+ key,
629
+ callCount: value.callCount,
630
+ totalTokens: value.totalTokens
631
+ })).sort((left, right) => right.totalTokens - left.totalTokens).slice(0, 5)
632
+ },
633
+ cursorUsage: {
634
+ totalTokens,
635
+ totalInputTokens: Math.max(totalTokens - totalOutputTokens, 0),
636
+ totalOutputTokens,
637
+ totalCacheReadTokens,
638
+ totalInputWithCacheWriteTokens,
639
+ totalInputWithoutCacheWriteTokens,
640
+ modes: Array.from(modes.entries()).map(([key, value]) => ({
641
+ key,
642
+ callCount: value.callCount,
643
+ totalTokens: value.totalTokens,
644
+ estimatedSpendUsd: value.estimatedSpendUsd
645
+ })).sort((left, right) => right.totalTokens - left.totalTokens),
646
+ models: Array.from(models.entries()).map(([key, value]) => ({
647
+ key,
648
+ callCount: value.callCount,
649
+ totalTokens: value.totalTokens,
650
+ estimatedSpendUsd: value.estimatedSpendUsd,
651
+ pricedCallCount: value.pricedCallCount,
652
+ unpricedCallCount: value.unpricedCallCount
653
+ })).sort((left, right) => right.totalTokens - left.totalTokens)
654
+ }
655
+ };
656
+ }
657
+ function buildDoctorNotes(report) {
658
+ const notes = ["Cursor usage CSV headers validated."];
659
+ if (report.rowCount === 0) {
660
+ notes.push("The CSV contains no usage rows.");
661
+ }
662
+ if (report.pricingCoverage.unpricedCallCount > 0) {
663
+ const aliases = report.pricingCoverage.topUnpricedModels.map((model) => model.key).join(", ");
664
+ notes.push(
665
+ `Some Cursor aliases do not have full local pricing coverage: ${aliases || "unknown aliases"}.`
666
+ );
667
+ } else {
668
+ notes.push("All rows in this CSV have local pricing coverage.");
669
+ }
670
+ notes.push("Cursor CSV audits use exported usage rows rather than raw session transcripts.");
671
+ return notes;
672
+ }
673
+ async function inspectCursorUsageCsv(options) {
674
+ const filePath = options.cursorUsageCsv ? resolve(options.cursorUsageCsv) : "";
675
+ options.onProgress?.("Inspecting Cursor usage CSV...");
676
+ if (!filePath) {
677
+ return {
678
+ canAudit: false,
679
+ filePath,
680
+ source: null,
681
+ rowCount: 0,
682
+ dateRange: null,
683
+ pricingCoverage: {
684
+ pricedCallCount: 0,
685
+ unpricedCallCount: 0,
686
+ pricedTokenCount: 0,
687
+ unpricedTokenCount: 0,
688
+ topUnpricedModels: []
689
+ },
690
+ notes: ["No Cursor usage CSV path was provided."]
691
+ };
692
+ }
693
+ try {
694
+ const parsed = readCursorUsageCsv(filePath);
695
+ const normalized = normalizeCursorUsageCsv({
696
+ source: parsed.source,
697
+ rows: parsed.rows,
698
+ hasObservedCostRows: parsed.hasObservedCostRows
699
+ });
700
+ const dateRange = parsed.rows.length === 0 ? null : {
701
+ start: parsed.rows.map((row) => row.timestamp).sort((left, right) => new Date(left).getTime() - new Date(right).getTime())[0],
702
+ end: parsed.rows.map((row) => row.timestamp).sort((left, right) => new Date(left).getTime() - new Date(right).getTime()).at(-1)
703
+ };
704
+ const report = {
705
+ canAudit: true,
706
+ filePath: parsed.source.path,
707
+ source: parsed.source,
708
+ rowCount: parsed.rows.length,
709
+ dateRange,
710
+ pricingCoverage: normalized.pricingCoverage,
711
+ notes: []
712
+ };
713
+ report.notes = buildDoctorNotes(report);
714
+ options.onProgress?.(
715
+ `Cursor usage CSV is ready (${report.rowCount} row${report.rowCount === 1 ? "" : "s"}).`
716
+ );
717
+ return report;
718
+ } catch (error) {
719
+ const message = error instanceof Error ? error.message : "Unknown error";
720
+ options.onProgress?.(`Cursor usage CSV is not ready: ${message}`);
721
+ return {
722
+ canAudit: false,
723
+ filePath,
724
+ source: null,
725
+ rowCount: 0,
726
+ dateRange: null,
727
+ pricingCoverage: {
728
+ pricedCallCount: 0,
729
+ unpricedCallCount: 0,
730
+ pricedTokenCount: 0,
731
+ unpricedTokenCount: 0,
732
+ topUnpricedModels: []
733
+ },
734
+ notes: [message]
735
+ };
736
+ }
737
+ }
738
+
74
739
  // ../core/src/db/client.ts
75
740
  import { mkdirSync } from "fs";
76
741
  import { dirname } from "path";
@@ -430,7 +1095,7 @@ var FINDING_KIND_LABELS = {
430
1095
  "candidate-downgrade": "Downgrade candidates",
431
1096
  "idle-spend": "Idle waste"
432
1097
  };
433
- function round(value) {
1098
+ function round2(value) {
434
1099
  return Number(value.toFixed(6));
435
1100
  }
436
1101
  function normalizeSinceValue(since) {
@@ -494,7 +1159,7 @@ function buildTaxonomyBuckets(findings, classification) {
494
1159
  spendUsd: 0,
495
1160
  findingCount: 0
496
1161
  };
497
- current.spendUsd = round(current.spendUsd + finding.costImpactUsd);
1162
+ current.spendUsd = round2(current.spendUsd + finding.costImpactUsd);
498
1163
  current.findingCount += 1;
499
1164
  buckets.set(finding.kind, current);
500
1165
  }
@@ -512,9 +1177,9 @@ function buildTopSpendDeltas(currentRows, baselineRows) {
512
1177
  const currentSpendUsd = currentMap.get(key) ?? 0;
513
1178
  return {
514
1179
  key,
515
- baselineSpendUsd: round(baselineSpendUsd),
516
- currentSpendUsd: round(currentSpendUsd),
517
- deltaSpendUsd: round(currentSpendUsd - baselineSpendUsd)
1180
+ baselineSpendUsd: round2(baselineSpendUsd),
1181
+ currentSpendUsd: round2(currentSpendUsd),
1182
+ deltaSpendUsd: round2(currentSpendUsd - baselineSpendUsd)
518
1183
  };
519
1184
  }).filter((row) => row.deltaSpendUsd !== 0).sort((left, right) => Math.abs(right.deltaSpendUsd) - Math.abs(left.deltaSpendUsd)).slice(0, 3);
520
1185
  }
@@ -549,11 +1214,11 @@ function buildFindingChanges(currentFindings, baselineFindings) {
549
1214
  scope: current.scope,
550
1215
  scopeId: current.scopeId,
551
1216
  currentCostImpactUsd: current.costImpactUsd,
552
- deltaCostImpactUsd: round(current.costImpactUsd)
1217
+ deltaCostImpactUsd: round2(current.costImpactUsd)
553
1218
  });
554
1219
  continue;
555
1220
  }
556
- const deltaCostImpactUsd = round(current.costImpactUsd - baseline.costImpactUsd);
1221
+ const deltaCostImpactUsd = round2(current.costImpactUsd - baseline.costImpactUsd);
557
1222
  if (deltaCostImpactUsd > 0) {
558
1223
  worsenedHighConfidenceWaste.push({
559
1224
  kind: current.kind,
@@ -576,7 +1241,7 @@ function buildFindingChanges(currentFindings, baselineFindings) {
576
1241
  scope: baseline.scope,
577
1242
  scopeId: baseline.scopeId,
578
1243
  baselineCostImpactUsd: baseline.costImpactUsd,
579
- deltaCostImpactUsd: round(-baseline.costImpactUsd)
1244
+ deltaCostImpactUsd: round2(-baseline.costImpactUsd)
580
1245
  });
581
1246
  }
582
1247
  return {
@@ -595,7 +1260,11 @@ function hydrateAuditSummary(summary) {
595
1260
  comparison: summary.comparison ?? null,
596
1261
  wasteByKind: summary.wasteByKind?.length > 0 ? summary.wasteByKind : buildTaxonomyBuckets(summary.findings, "waste"),
597
1262
  opportunityByKind: summary.opportunityByKind?.length > 0 ? summary.opportunityByKind : buildTaxonomyBuckets(summary.findings, "opportunity"),
598
- notes: summary.notes ?? []
1263
+ spendByDay: summary.spendByDay ?? [],
1264
+ wasteByDay: summary.wasteByDay ?? [],
1265
+ notes: summary.notes ?? [],
1266
+ pricingCoverage: summary.pricingCoverage ?? null,
1267
+ cursorUsage: summary.cursorUsage ?? null
599
1268
  };
600
1269
  }
601
1270
  function buildAuditComparison(current, baseline) {
@@ -612,12 +1281,12 @@ function buildAuditComparison(current, baseline) {
612
1281
  baselineWasteSpendUsd: baseline.wasteSpendUsd,
613
1282
  baselineOpportunitySpendUsd: baseline.opportunitySpendUsd,
614
1283
  baselineStructuralWasteRate: baseline.structuralWasteRate,
615
- deltaTotalSpendUsd: round(current.totalSpendUsd - baseline.totalSpendUsd),
616
- deltaObservedSpendUsd: round(current.observedSpendUsd - baseline.observedSpendUsd),
617
- deltaEstimatedSpendUsd: round(current.estimatedSpendUsd - baseline.estimatedSpendUsd),
618
- deltaWasteSpendUsd: round(current.wasteSpendUsd - baseline.wasteSpendUsd),
619
- deltaOpportunitySpendUsd: round(current.opportunitySpendUsd - baseline.opportunitySpendUsd),
620
- deltaStructuralWasteRate: round(current.structuralWasteRate - baseline.structuralWasteRate),
1284
+ deltaTotalSpendUsd: round2(current.totalSpendUsd - baseline.totalSpendUsd),
1285
+ deltaObservedSpendUsd: round2(current.observedSpendUsd - baseline.observedSpendUsd),
1286
+ deltaEstimatedSpendUsd: round2(current.estimatedSpendUsd - baseline.estimatedSpendUsd),
1287
+ deltaWasteSpendUsd: round2(current.wasteSpendUsd - baseline.wasteSpendUsd),
1288
+ deltaOpportunitySpendUsd: round2(current.opportunitySpendUsd - baseline.opportunitySpendUsd),
1289
+ deltaStructuralWasteRate: round2(current.structuralWasteRate - baseline.structuralWasteRate),
621
1290
  deltaRunCount: current.runCount - baseline.runCount,
622
1291
  deltaCallCount: current.callCount - baseline.callCount,
623
1292
  workflowDeltas,
@@ -662,8 +1331,8 @@ function readLatestComparableAuditSummary(input) {
662
1331
  }
663
1332
 
664
1333
  // ../core/src/detect/openclaw.ts
665
- import { readdirSync, statSync } from "fs";
666
- import { isAbsolute, join as join2, resolve, sep } from "path";
1334
+ import { readdirSync, statSync as statSync2 } from "fs";
1335
+ import { isAbsolute, join as join2, resolve as resolve2, sep } from "path";
667
1336
 
668
1337
  // ../core/src/utils/paths.ts
669
1338
  import { mkdirSync as mkdirSync2 } from "fs";
@@ -699,7 +1368,7 @@ function getDefaultGatewayPattern() {
699
1368
  // ../core/src/detect/openclaw.ts
700
1369
  function toDetected(path, kind) {
701
1370
  try {
702
- const stats = statSync(path);
1371
+ const stats = statSync2(path);
703
1372
  if (!stats.isFile()) {
704
1373
  return null;
705
1374
  }
@@ -747,12 +1416,12 @@ async function detectOpenClawSources(options) {
747
1416
  return detected.sort((left, right) => right.mtimeMs - left.mtimeMs);
748
1417
  }
749
1418
  async function collectGlobMatches(pattern, options) {
750
- const baseDir = options?.cwd ? resolve(options.cwd) : isAbsolute(pattern) ? sep : process.cwd();
1419
+ const baseDir = options?.cwd ? resolve2(options.cwd) : isAbsolute(pattern) ? sep : process.cwd();
751
1420
  const relativePattern = options?.cwd ? pattern : isAbsolute(pattern) ? pattern.slice(baseDir.length) : pattern;
752
1421
  const segments = relativePattern.split("/").filter(Boolean);
753
1422
  const matches = collectMatchesFromSegments(baseDir, segments);
754
1423
  return matches.map(
755
- (match) => options?.resolveWith ? resolve(options.resolveWith, match) : match
1424
+ (match) => options?.resolveWith ? resolve2(options.resolveWith, match) : match
756
1425
  );
757
1426
  }
758
1427
  function collectMatchesFromSegments(currentPath, segments) {
@@ -830,7 +1499,10 @@ async function inspectOpenClawSources(options) {
830
1499
  };
831
1500
  }
832
1501
 
833
- // ../core/src/findings/engine.ts
1502
+ // ../core/src/findings/cursor.ts
1503
+ function round3(value) {
1504
+ return Number(value.toFixed(6));
1505
+ }
834
1506
  function createFinding(input) {
835
1507
  return {
836
1508
  ...input,
@@ -839,11 +1511,125 @@ function createFinding(input) {
839
1511
  )
840
1512
  };
841
1513
  }
842
- function round2(value) {
1514
+ function asNumber(value) {
1515
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
1516
+ }
1517
+ function asBoolean(value) {
1518
+ return value === true;
1519
+ }
1520
+ function buildCursorUsageFindings(runs) {
1521
+ const calls = runs.flatMap((run2) => run2.calls);
1522
+ const billableCalls = calls.filter((call) => call.costUsd > 0);
1523
+ if (billableCalls.length === 0) {
1524
+ return { findings: [], wasteAttributions: [] };
1525
+ }
1526
+ const cacheAwareCalls = billableCalls.filter((call) => {
1527
+ return asNumber(call.metadata.cacheReadTokens) > 0;
1528
+ });
1529
+ if (cacheAwareCalls.length === 0) {
1530
+ return { findings: [], wasteAttributions: [] };
1531
+ }
1532
+ const totalSpendUsd = billableCalls.reduce((sum, call) => sum + call.costUsd, 0);
1533
+ const totalInputTokens = billableCalls.reduce((sum, call) => sum + call.inputTokens, 0);
1534
+ const totalCacheReadTokens = cacheAwareCalls.reduce(
1535
+ (sum, call) => sum + asNumber(call.metadata.cacheReadTokens),
1536
+ 0
1537
+ );
1538
+ const totalCacheWriteTokens = billableCalls.reduce(
1539
+ (sum, call) => sum + asNumber(call.metadata.inputWithCacheWriteTokens),
1540
+ 0
1541
+ );
1542
+ const cacheSpendUsd = cacheAwareCalls.reduce((sum, call) => sum + (call.cacheCostUsd ?? 0), 0);
1543
+ const cacheWriteSpendUsd = billableCalls.reduce(
1544
+ (sum, call) => sum + asNumber(call.metadata.cacheWriteCostUsd),
1545
+ 0
1546
+ );
1547
+ const coveredSpendUsd = billableCalls.filter((call) => call.cacheCostUsd !== null).reduce((sum, call) => sum + call.costUsd, 0);
1548
+ const maxModeSpendUsd = billableCalls.filter((call) => asBoolean(call.metadata.maxMode)).reduce((sum, call) => sum + call.costUsd, 0);
1549
+ const cacheReadShare = totalInputTokens === 0 ? 0 : totalCacheReadTokens / totalInputTokens;
1550
+ const cacheCoverageShare = totalSpendUsd === 0 ? 0 : coveredSpendUsd / totalSpendUsd;
1551
+ const maxModeSpendShare = totalSpendUsd === 0 ? 0 : maxModeSpendUsd / totalSpendUsd;
1552
+ const cacheImpactUsd = round3(cacheSpendUsd + cacheWriteSpendUsd);
1553
+ const meetsWasteBar = cacheImpactUsd >= 25 && cacheReadShare >= 0.6 && cacheAwareCalls.length >= 20 && cacheCoverageShare >= 0.4;
1554
+ const meetsOpportunityBar = cacheImpactUsd >= 5 && cacheReadShare >= 0.35 && cacheAwareCalls.length >= 10 && cacheCoverageShare >= 0.25;
1555
+ if (!meetsWasteBar && !meetsOpportunityBar) {
1556
+ return { findings: [], wasteAttributions: [] };
1557
+ }
1558
+ const classification = meetsWasteBar ? "waste" : "opportunity";
1559
+ const confidence = cacheReadShare >= 0.8 && cacheAwareCalls.length >= 50 && cacheCoverageShare >= 0.5 ? "high" : cacheReadShare >= 0.5 && cacheAwareCalls.length >= 20 ? "medium" : "low";
1560
+ 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.`;
1561
+ const findings = [
1562
+ createFinding({
1563
+ classification,
1564
+ confidence,
1565
+ kind: "cache-carryover",
1566
+ title: classification === "waste" ? "Cached context carryover is driving avoidable spend" : "Cached context carryover looks like a strong cost-reduction opportunity",
1567
+ summary,
1568
+ scope: "global",
1569
+ scopeId: "all",
1570
+ costImpactUsd: cacheImpactUsd,
1571
+ details: {
1572
+ cacheReadShare: round3(cacheReadShare),
1573
+ cacheCoverageShare: round3(cacheCoverageShare),
1574
+ totalCacheReadTokens,
1575
+ totalCacheWriteTokens,
1576
+ billableCallCount: billableCalls.length,
1577
+ cacheAwareCallCount: cacheAwareCalls.length,
1578
+ maxModeSpendShare: round3(maxModeSpendShare),
1579
+ estimatedCacheReadSpendUsd: round3(cacheSpendUsd),
1580
+ estimatedCacheWriteSpendUsd: round3(cacheWriteSpendUsd)
1581
+ }
1582
+ })
1583
+ ];
1584
+ const maxModeCalls = billableCalls.filter((call) => asBoolean(call.metadata.maxMode));
1585
+ const maxModeCallShare = billableCalls.length === 0 ? 0 : maxModeCalls.length / billableCalls.length;
1586
+ if (maxModeSpendShare >= 0.6 && maxModeSpendUsd >= 25 && maxModeCalls.length >= 10) {
1587
+ const maxModeConfidence = maxModeSpendShare >= 0.85 && maxModeCalls.length >= 50 ? "high" : maxModeSpendShare >= 0.7 && maxModeCalls.length >= 20 ? "medium" : "low";
1588
+ findings.push(
1589
+ createFinding({
1590
+ classification: "opportunity",
1591
+ confidence: maxModeConfidence,
1592
+ kind: "max-mode-concentration",
1593
+ title: "Max mode is concentrated in the billed spend mix",
1594
+ 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.`,
1595
+ scope: "global",
1596
+ scopeId: "all",
1597
+ costImpactUsd: round3(maxModeSpendUsd * 0.2),
1598
+ details: {
1599
+ maxModeSpendUsd: round3(maxModeSpendUsd),
1600
+ maxModeSpendShare: round3(maxModeSpendShare),
1601
+ maxModeCallCount: maxModeCalls.length,
1602
+ maxModeCallShare: round3(maxModeCallShare)
1603
+ }
1604
+ })
1605
+ );
1606
+ }
1607
+ const wasteAttributions = classification === "waste" ? billableCalls.map((call) => ({
1608
+ kind: "cache-carryover",
1609
+ timestamp: call.timestamp,
1610
+ wasteUsd: round3((call.cacheCostUsd ?? 0) + asNumber(call.metadata.cacheWriteCostUsd))
1611
+ })).filter((attribution) => attribution.wasteUsd > 0) : [];
1612
+ return {
1613
+ findings: findings.sort((left, right) => right.costImpactUsd - left.costImpactUsd),
1614
+ wasteAttributions
1615
+ };
1616
+ }
1617
+
1618
+ // ../core/src/findings/engine.ts
1619
+ function createFinding2(input) {
1620
+ return {
1621
+ ...input,
1622
+ id: sha1(
1623
+ `${input.kind}:${input.scope}:${input.scopeId}:${input.title}:${input.costImpactUsd}:${input.summary}`
1624
+ )
1625
+ };
1626
+ }
1627
+ function round4(value) {
843
1628
  return Number(value.toFixed(6));
844
1629
  }
845
1630
  function buildFindings(runs) {
846
1631
  const findings = [];
1632
+ const wasteAttributions = [];
847
1633
  const allCalls = runs.flatMap((run2) => run2.calls.map((call) => ({ run: run2, call })));
848
1634
  const retryCandidates = allCalls.filter(({ call }) => {
849
1635
  const status = (call.status ?? "").toLowerCase();
@@ -851,8 +1637,15 @@ function buildFindings(runs) {
851
1637
  });
852
1638
  const retryCost = retryCandidates.reduce((sum, item) => sum + item.call.costUsd, 0);
853
1639
  if (retryCost > 0) {
1640
+ wasteAttributions.push(
1641
+ ...retryCandidates.map(({ call }) => ({
1642
+ kind: "retry-waste",
1643
+ timestamp: call.timestamp,
1644
+ wasteUsd: call.costUsd
1645
+ }))
1646
+ );
854
1647
  findings.push(
855
- createFinding({
1648
+ createFinding2({
856
1649
  classification: "waste",
857
1650
  confidence: "high",
858
1651
  kind: "retry-waste",
@@ -860,7 +1653,7 @@ function buildFindings(runs) {
860
1653
  summary: `${retryCandidates.length} failed call${retryCandidates.length === 1 ? "" : "s"} were followed by additional work, making their spend pure retry overhead.`,
861
1654
  scope: "global",
862
1655
  scopeId: "all",
863
- costImpactUsd: round2(retryCost),
1656
+ costImpactUsd: round4(retryCost),
864
1657
  details: {
865
1658
  failedCallCount: retryCandidates.length
866
1659
  }
@@ -872,8 +1665,15 @@ function buildFindings(runs) {
872
1665
  if (maxIteration >= 7) {
873
1666
  const loopCalls = run2.calls.filter((call) => (call.iteration ?? 0) > 5);
874
1667
  const loopCost = loopCalls.reduce((sum, call) => sum + call.costUsd, 0);
1668
+ wasteAttributions.push(
1669
+ ...loopCalls.map((call) => ({
1670
+ kind: "loop-waste",
1671
+ timestamp: call.timestamp,
1672
+ wasteUsd: call.costUsd
1673
+ }))
1674
+ );
875
1675
  findings.push(
876
- createFinding({
1676
+ createFinding2({
877
1677
  classification: "waste",
878
1678
  confidence: "high",
879
1679
  kind: "loop-waste",
@@ -881,7 +1681,7 @@ function buildFindings(runs) {
881
1681
  summary: `This run reached ${maxIteration} iterations. Xerg treats the spend after iteration 5 as likely loop waste.`,
882
1682
  scope: "run",
883
1683
  scopeId: run2.id,
884
- costImpactUsd: round2(loopCost),
1684
+ costImpactUsd: round4(loopCost),
885
1685
  details: {
886
1686
  workflow: run2.workflow,
887
1687
  maxIteration
@@ -909,7 +1709,7 @@ function buildFindings(runs) {
909
1709
  if (outlierRuns.length > 0) {
910
1710
  const outlierCost = outlierRuns.reduce((sum, run2) => sum + run2.totalCostUsd, 0);
911
1711
  findings.push(
912
- createFinding({
1712
+ createFinding2({
913
1713
  classification: "opportunity",
914
1714
  confidence: "medium",
915
1715
  kind: "context-outlier",
@@ -917,10 +1717,10 @@ function buildFindings(runs) {
917
1717
  summary: `Xerg found ${outlierRuns.length} run${outlierRuns.length === 1 ? "" : "s"} in this workflow with input token volume far above the workflow average.`,
918
1718
  scope: "workflow",
919
1719
  scopeId: workflow,
920
- costImpactUsd: round2(outlierCost),
1720
+ costImpactUsd: round4(outlierCost),
921
1721
  details: {
922
1722
  workflow,
923
- averageInputTokens: round2(average),
1723
+ averageInputTokens: round4(average),
924
1724
  outlierRunCount: outlierRuns.length
925
1725
  }
926
1726
  })
@@ -933,7 +1733,7 @@ function buildFindings(runs) {
933
1733
  if (idleRuns.length > 0) {
934
1734
  const idleCost = idleRuns.reduce((sum, run2) => sum + run2.totalCostUsd, 0);
935
1735
  findings.push(
936
- createFinding({
1736
+ createFinding2({
937
1737
  classification: "opportunity",
938
1738
  confidence: "medium",
939
1739
  kind: "idle-spend",
@@ -941,7 +1741,7 @@ function buildFindings(runs) {
941
1741
  summary: "This workflow name looks like a recurring heartbeat or monitoring loop. Review whether the cadence and model tier are justified.",
942
1742
  scope: "workflow",
943
1743
  scopeId: workflow,
944
- costImpactUsd: round2(idleCost),
1744
+ costImpactUsd: round4(idleCost),
945
1745
  details: {
946
1746
  workflow
947
1747
  }
@@ -954,7 +1754,7 @@ function buildFindings(runs) {
954
1754
  if (downgradeCalls.length > 0) {
955
1755
  const spend = downgradeCalls.reduce((sum, call) => sum + call.costUsd, 0);
956
1756
  findings.push(
957
- createFinding({
1757
+ createFinding2({
958
1758
  classification: "opportunity",
959
1759
  confidence: "low",
960
1760
  kind: "candidate-downgrade",
@@ -962,21 +1762,24 @@ function buildFindings(runs) {
962
1762
  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.",
963
1763
  scope: "workflow",
964
1764
  scopeId: workflow,
965
- costImpactUsd: round2(spend * 0.3),
1765
+ costImpactUsd: round4(spend * 0.3),
966
1766
  details: {
967
1767
  workflow,
968
1768
  expensiveCallCount: downgradeCalls.length,
969
- inspectedSpendUsd: round2(spend)
1769
+ inspectedSpendUsd: round4(spend)
970
1770
  }
971
1771
  })
972
1772
  );
973
1773
  }
974
1774
  }
975
- return findings.sort((left, right) => right.costImpactUsd - left.costImpactUsd);
1775
+ return {
1776
+ findings: findings.sort((left, right) => right.costImpactUsd - left.costImpactUsd),
1777
+ wasteAttributions
1778
+ };
976
1779
  }
977
1780
 
978
1781
  // ../core/src/normalize/openclaw.ts
979
- import { readFileSync } from "fs";
1782
+ import { readFileSync as readFileSync2 } from "fs";
980
1783
  import { basename } from "path";
981
1784
 
982
1785
  // ../core/src/pricing-catalog.ts
@@ -1076,7 +1879,7 @@ function getNestedValue(input, paths) {
1076
1879
  }
1077
1880
  return null;
1078
1881
  }
1079
- function asNumber(value) {
1882
+ function asNumber2(value) {
1080
1883
  if (typeof value === "number" && Number.isFinite(value)) {
1081
1884
  return value;
1082
1885
  }
@@ -1092,7 +1895,7 @@ function asString(value) {
1092
1895
  }
1093
1896
  return null;
1094
1897
  }
1095
- function asBoolean(value) {
1898
+ function asBoolean2(value) {
1096
1899
  if (typeof value === "boolean") {
1097
1900
  return value;
1098
1901
  }
@@ -1117,7 +1920,7 @@ function pickMetadata(input, keys) {
1117
1920
 
1118
1921
  // ../core/src/normalize/openclaw.ts
1119
1922
  function parseJsonLines(path) {
1120
- const content = readFileSync(path, "utf8");
1923
+ const content = readFileSync2(path, "utf8");
1121
1924
  const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1122
1925
  const records = [];
1123
1926
  for (const line of lines) {
@@ -1129,7 +1932,7 @@ function parseJsonLines(path) {
1129
1932
  }
1130
1933
  return records;
1131
1934
  }
1132
- function inferProvider(record) {
1935
+ function inferProvider2(record) {
1133
1936
  return asString(
1134
1937
  getNestedValue(record, [["provider"], ["message", "provider"], ["usage", "provider"]])
1135
1938
  ) ?? "unknown";
@@ -1168,7 +1971,7 @@ function inferTaskClass(record, workflow) {
1168
1971
  return asString(getNestedValue(record, [["task_class"], ["taskClass"], ["metadata", "taskClass"]])) ?? workflow.toLowerCase();
1169
1972
  }
1170
1973
  function extractUsage(record) {
1171
- const inputTokens = asNumber(
1974
+ const inputTokens = asNumber2(
1172
1975
  getNestedValue(record, [
1173
1976
  ["input_tokens"],
1174
1977
  ["inputTokens"],
@@ -1180,7 +1983,7 @@ function extractUsage(record) {
1180
1983
  ["message", "usage", "prompt_tokens"]
1181
1984
  ])
1182
1985
  ) ?? 0;
1183
- const outputTokens = asNumber(
1986
+ const outputTokens = asNumber2(
1184
1987
  getNestedValue(record, [
1185
1988
  ["output_tokens"],
1186
1989
  ["outputTokens"],
@@ -1192,7 +1995,7 @@ function extractUsage(record) {
1192
1995
  ["message", "usage", "completion_tokens"]
1193
1996
  ])
1194
1997
  ) ?? 0;
1195
- const observedCost = asNumber(
1998
+ const observedCost = asNumber2(
1196
1999
  getNestedValue(record, [
1197
2000
  ["cost_usd"],
1198
2001
  ["costUsd"],
@@ -1210,8 +2013,8 @@ function extractUsage(record) {
1210
2013
  observedCost
1211
2014
  };
1212
2015
  }
1213
- function buildCall(source, record, runId, index) {
1214
- const provider = inferProvider(record);
2016
+ function buildCall2(source, record, runId, index) {
2017
+ const provider = inferProvider2(record);
1215
2018
  const model = inferModel(record);
1216
2019
  const workflow = inferWorkflow(record, source.path);
1217
2020
  const { inputTokens, outputTokens, observedCost } = extractUsage(record);
@@ -1219,13 +2022,13 @@ function buildCall(source, record, runId, index) {
1219
2022
  const timestamp = toIsoOrNow(
1220
2023
  getNestedValue(record, [["timestamp"], ["createdAt"], ["created_at"]])
1221
2024
  );
1222
- const attempt = asNumber(
2025
+ const attempt = asNumber2(
1223
2026
  getNestedValue(record, [["attempt"], ["usage", "attempt"], ["metadata", "attempt"]])
1224
2027
  ) ?? null;
1225
- const iteration = asNumber(
2028
+ const iteration = asNumber2(
1226
2029
  getNestedValue(record, [["iteration"], ["loop_iteration"], ["metadata", "iteration"]])
1227
2030
  ) ?? null;
1228
- const retries = asNumber(getNestedValue(record, [["retries"], ["retry_count"], ["metadata", "retries"]])) ?? 0;
2031
+ const retries = asNumber2(getNestedValue(record, [["retries"], ["retry_count"], ["metadata", "retries"]])) ?? 0;
1229
2032
  const costUsd = observedCost ?? estimatedCost ?? 0;
1230
2033
  return {
1231
2034
  id: sha1(`${runId}:${source.path}:${index}:${model}:${timestamp}:${costUsd}`),
@@ -1237,17 +2040,17 @@ function buildCall(source, record, runId, index) {
1237
2040
  outputTokens,
1238
2041
  costUsd,
1239
2042
  costSource: observedCost !== null ? "observed" : "estimated",
1240
- latencyMs: asNumber(getNestedValue(record, [["latency_ms"], ["latencyMs"], ["usage", "latency_ms"]])) ?? null,
1241
- toolCalls: asNumber(getNestedValue(record, [["tool_calls"], ["toolCalls"], ["usage", "tool_calls"]])) ?? 0,
2043
+ latencyMs: asNumber2(getNestedValue(record, [["latency_ms"], ["latencyMs"], ["usage", "latency_ms"]])) ?? null,
2044
+ toolCalls: asNumber2(getNestedValue(record, [["tool_calls"], ["toolCalls"], ["usage", "tool_calls"]])) ?? 0,
1242
2045
  retries,
1243
2046
  attempt,
1244
2047
  iteration,
1245
2048
  status: asString(getNestedValue(record, [["status"], ["level"], ["result"], ["error", "type"]])) ?? null,
1246
2049
  taskClass: inferTaskClass(record, workflow),
1247
- cacheHit: asBoolean(
2050
+ cacheHit: asBoolean2(
1248
2051
  getNestedValue(record, [["cache_hit"], ["cacheHit"], ["usage", "cache_hit"]])
1249
2052
  ),
1250
- cacheCostUsd: asNumber(
2053
+ cacheCostUsd: asNumber2(
1251
2054
  getNestedValue(record, [["cache_cost_usd"], ["cacheCostUsd"], ["usage", "cache_cost_usd"]])
1252
2055
  ) ?? null,
1253
2056
  metadata: pickMetadata(record, ["event", "type", "sessionId", "agentId"])
@@ -1275,7 +2078,7 @@ function normalizeOpenClawSources(sources, since) {
1275
2078
  }
1276
2079
  const runKey = inferRunKey(record, workflow, index, source.path);
1277
2080
  const runId = sha1(`${source.path}:${runKey}`);
1278
- const call = buildCall(source, record, runId, index);
2081
+ const call = buildCall2(source, record, runId, index);
1279
2082
  const existing = runsById.get(runId);
1280
2083
  if (!existing) {
1281
2084
  runsById.set(runId, {
@@ -1308,34 +2111,159 @@ function normalizeOpenClawSources(sources, since) {
1308
2111
  });
1309
2112
  }
1310
2113
 
1311
- // ../core/src/report/summary.ts
1312
- function buildBreakdown(items) {
1313
- const buckets = /* @__PURE__ */ new Map();
1314
- for (const item of items) {
1315
- const current = buckets.get(item.key) ?? { spendUsd: 0, observedSpendUsd: 0, callCount: 0 };
1316
- current.spendUsd += item.spendUsd;
1317
- current.observedSpendUsd += item.observedSpendUsd;
1318
- current.callCount += 1;
1319
- buckets.set(item.key, current);
2114
+ // ../core/src/report/timeseries.ts
2115
+ function round5(value) {
2116
+ return Number(value.toFixed(6));
2117
+ }
2118
+ function toUtcDay(timestamp) {
2119
+ const candidate = new Date(timestamp);
2120
+ if (Number.isNaN(candidate.getTime())) {
2121
+ return null;
1320
2122
  }
1321
- return Array.from(buckets.entries()).map(([key, value]) => {
1322
- const observedShare = value.spendUsd === 0 ? 0 : value.observedSpendUsd / value.spendUsd;
1323
- return {
1324
- key,
1325
- spendUsd: Number(value.spendUsd.toFixed(6)),
1326
- callCount: value.callCount,
1327
- observedShare: Number(observedShare.toFixed(4))
1328
- };
1329
- }).sort((left, right) => right.spendUsd - left.spendUsd);
2123
+ return candidate.toISOString().slice(0, 10);
1330
2124
  }
1331
- function buildAuditSummary(input) {
1332
- const callCount = input.runs.reduce((sum, run2) => sum + run2.calls.length, 0);
2125
+ function incrementUtcDay(date) {
2126
+ const candidate = /* @__PURE__ */ new Date(`${date}T00:00:00.000Z`);
2127
+ candidate.setUTCDate(candidate.getUTCDate() + 1);
2128
+ return candidate.toISOString().slice(0, 10);
2129
+ }
2130
+ function buildObservedUtcDayRange(runs) {
2131
+ const days = runs.flatMap((run2) => run2.calls).map((call) => toUtcDay(call.timestamp)).filter((day) => day !== null).sort();
2132
+ if (days.length === 0) {
2133
+ return [];
2134
+ }
2135
+ const range = [];
2136
+ let current = days[0];
2137
+ const last = days[days.length - 1];
2138
+ while (current <= last) {
2139
+ range.push(current);
2140
+ current = incrementUtcDay(current);
2141
+ }
2142
+ return range;
2143
+ }
2144
+ function reconcileDailyTotal(rows, key, expected) {
2145
+ if (rows.length === 0) {
2146
+ return;
2147
+ }
2148
+ const actual = round5(
2149
+ rows.reduce((sum, row) => sum + (typeof row[key] === "number" ? row[key] : 0), 0)
2150
+ );
2151
+ const delta = round5(expected - actual);
2152
+ if (delta === 0) {
2153
+ return;
2154
+ }
2155
+ const last = rows[rows.length - 1];
2156
+ const current = last[key];
2157
+ if (typeof current === "number") {
2158
+ last[key] = round5(current + delta);
2159
+ }
2160
+ }
2161
+ function buildSpendByDay(runs) {
2162
+ const days = buildObservedUtcDayRange(runs);
2163
+ if (days.length === 0) {
2164
+ return [];
2165
+ }
2166
+ const byDay = new Map(
2167
+ days.map((day) => [
2168
+ day,
2169
+ { date: day, observedSpendUsd: 0, estimatedSpendUsd: 0, callCount: 0 }
2170
+ ])
2171
+ );
2172
+ for (const run2 of runs) {
2173
+ for (const call of run2.calls) {
2174
+ const day = toUtcDay(call.timestamp);
2175
+ if (!day) {
2176
+ continue;
2177
+ }
2178
+ const bucket = byDay.get(day);
2179
+ if (!bucket) {
2180
+ continue;
2181
+ }
2182
+ bucket.callCount += 1;
2183
+ if (call.costSource === "observed") {
2184
+ bucket.observedSpendUsd += call.costUsd;
2185
+ } else if (call.costSource === "estimated") {
2186
+ bucket.estimatedSpendUsd += call.costUsd;
2187
+ }
2188
+ }
2189
+ }
2190
+ const rows = days.map((day) => {
2191
+ const bucket = byDay.get(day);
2192
+ const observedSpendUsd = round5(bucket?.observedSpendUsd ?? 0);
2193
+ const estimatedSpendUsd = round5(bucket?.estimatedSpendUsd ?? 0);
2194
+ return {
2195
+ date: day,
2196
+ observedSpendUsd,
2197
+ estimatedSpendUsd,
2198
+ spendUsd: round5(observedSpendUsd + estimatedSpendUsd),
2199
+ callCount: bucket?.callCount ?? 0
2200
+ };
2201
+ });
2202
+ reconcileDailyTotal(
2203
+ rows,
2204
+ "observedSpendUsd",
2205
+ round5(runs.reduce((sum, run2) => sum + run2.observedCostUsd, 0))
2206
+ );
2207
+ reconcileDailyTotal(
2208
+ rows,
2209
+ "estimatedSpendUsd",
2210
+ round5(runs.reduce((sum, run2) => sum + run2.estimatedCostUsd, 0))
2211
+ );
2212
+ for (const row of rows) {
2213
+ row.spendUsd = round5(row.observedSpendUsd + row.estimatedSpendUsd);
2214
+ }
2215
+ return rows;
2216
+ }
2217
+ function buildWasteByDay(wasteAttributions, days, expectedWasteUsd) {
2218
+ if (days.length === 0) {
2219
+ return [];
2220
+ }
2221
+ const byDay = new Map(days.map((day) => [day, 0]));
2222
+ for (const attribution of wasteAttributions) {
2223
+ const day = toUtcDay(attribution.timestamp);
2224
+ if (!day || !byDay.has(day)) {
2225
+ continue;
2226
+ }
2227
+ byDay.set(day, (byDay.get(day) ?? 0) + attribution.wasteUsd);
2228
+ }
2229
+ const rows = days.map((day) => ({
2230
+ date: day,
2231
+ wasteUsd: round5(byDay.get(day) ?? 0)
2232
+ }));
2233
+ reconcileDailyTotal(rows, "wasteUsd", round5(expectedWasteUsd));
2234
+ return rows;
2235
+ }
2236
+
2237
+ // ../core/src/report/summary.ts
2238
+ function buildBreakdown(items) {
2239
+ const buckets = /* @__PURE__ */ new Map();
2240
+ for (const item of items) {
2241
+ const current = buckets.get(item.key) ?? { spendUsd: 0, observedSpendUsd: 0, callCount: 0 };
2242
+ current.spendUsd += item.spendUsd;
2243
+ current.observedSpendUsd += item.observedSpendUsd;
2244
+ current.callCount += 1;
2245
+ buckets.set(item.key, current);
2246
+ }
2247
+ return Array.from(buckets.entries()).map(([key, value]) => {
2248
+ const observedShare = value.spendUsd === 0 ? 0 : value.observedSpendUsd / value.spendUsd;
2249
+ return {
2250
+ key,
2251
+ spendUsd: Number(value.spendUsd.toFixed(6)),
2252
+ callCount: value.callCount,
2253
+ observedShare: Number(observedShare.toFixed(4))
2254
+ };
2255
+ }).sort((left, right) => right.spendUsd - left.spendUsd);
2256
+ }
2257
+ function buildAuditSummary(input) {
2258
+ const callCount = input.runs.reduce((sum, run2) => sum + run2.calls.length, 0);
1333
2259
  const totalSpendUsd = input.runs.reduce((sum, run2) => sum + run2.totalCostUsd, 0);
1334
2260
  const observedSpendUsd = input.runs.reduce((sum, run2) => sum + run2.observedCostUsd, 0);
1335
2261
  const estimatedSpendUsd = input.runs.reduce((sum, run2) => sum + run2.estimatedCostUsd, 0);
1336
2262
  const wasteSpendUsd = input.findings.filter((finding) => finding.classification === "waste").reduce((sum, finding) => sum + finding.costImpactUsd, 0);
1337
2263
  const opportunitySpendUsd = input.findings.filter((finding) => finding.classification === "opportunity").reduce((sum, finding) => sum + finding.costImpactUsd, 0);
1338
2264
  const generatedAt = isoNow();
2265
+ const spendByDay = buildSpendByDay(input.runs);
2266
+ const observedDays = buildObservedUtcDayRange(input.runs);
1339
2267
  return {
1340
2268
  auditId: sha1(
1341
2269
  `${generatedAt}:${input.runs.length}:${input.sources.map((source) => source.path).join("|")}`
@@ -1375,6 +2303,8 @@ function buildAuditSummary(input) {
1375
2303
  }))
1376
2304
  )
1377
2305
  ),
2306
+ spendByDay,
2307
+ wasteByDay: buildWasteByDay(input.wasteAttributions, observedDays, wasteSpendUsd),
1378
2308
  findings: input.findings,
1379
2309
  notes: [
1380
2310
  "Cost per outcome is intentionally unavailable in v0. Xerg is measuring waste intelligence only.",
@@ -1389,18 +2319,71 @@ function buildAuditSummary(input) {
1389
2319
  async function doctorOpenClaw(options) {
1390
2320
  return inspectOpenClawSources(options);
1391
2321
  }
1392
- async function auditOpenClaw(options) {
1393
- options.onProgress?.("Scanning for OpenClaw source files...");
2322
+ async function doctorCursorUsageCsv(options) {
2323
+ return inspectCursorUsageCsv(options);
2324
+ }
2325
+ function validateCompareOptions(options) {
1394
2326
  if (options.compare && options.noDb) {
1395
2327
  throw new Error(
1396
2328
  "The --compare flag needs local snapshot history. Remove --no-db or provide --db <path>."
1397
2329
  );
1398
2330
  }
2331
+ }
2332
+ function maybeAttachComparison(options, dbPath, summary) {
2333
+ if (!options.compare || !dbPath) {
2334
+ return;
2335
+ }
2336
+ options.onProgress?.("Looking for a comparable baseline audit...");
2337
+ const baseline = readLatestComparableAuditSummary({
2338
+ dbPath,
2339
+ comparisonKey: summary.comparisonKey,
2340
+ currentAuditId: summary.auditId
2341
+ });
2342
+ if (!baseline) {
2343
+ summary.notes = [
2344
+ ...summary.notes,
2345
+ "No prior comparable audit was found. Run the same audit again after a fix to unlock before/after deltas."
2346
+ ];
2347
+ return;
2348
+ }
2349
+ summary.comparison = buildAuditComparison(summary, baseline);
2350
+ if (hasPricingCoverageChange(summary.pricingCoverage, baseline.pricingCoverage)) {
2351
+ summary.notes = [
2352
+ ...summary.notes,
2353
+ "Pricing coverage changed versus the baseline audit. Spend deltas are directional because different Cursor aliases were priced in each run."
2354
+ ];
2355
+ }
2356
+ }
2357
+ function persistLocalSnapshot(summary, runs, dbPath, onProgress) {
2358
+ if (!dbPath) {
2359
+ onProgress?.("Skipping local snapshot persistence (--no-db).");
2360
+ return;
2361
+ }
2362
+ onProgress?.(`Persisting local snapshot to ${dbPath}...`);
2363
+ persistAudit(
2364
+ {
2365
+ summary,
2366
+ runs,
2367
+ pricingCatalog: PRICING_CATALOG
2368
+ },
2369
+ dbPath
2370
+ );
2371
+ onProgress?.("Local snapshot stored.");
2372
+ }
2373
+ function hasPricingCoverageChange(current, baseline) {
2374
+ if (!current && !baseline) {
2375
+ return false;
2376
+ }
2377
+ return (current?.pricedCallCount ?? 0) !== (baseline?.pricedCallCount ?? 0) || (current?.unpricedCallCount ?? 0) !== (baseline?.unpricedCallCount ?? 0) || (current?.pricedTokenCount ?? 0) !== (baseline?.pricedTokenCount ?? 0) || (current?.unpricedTokenCount ?? 0) !== (baseline?.unpricedTokenCount ?? 0);
2378
+ }
2379
+ async function auditOpenClaw(options) {
2380
+ options.onProgress?.("Scanning for OpenClaw source files...");
2381
+ validateCompareOptions(options);
1399
2382
  const sources = await detectOpenClawSources(options);
1400
2383
  if (sources.length === 0) {
1401
2384
  options.onProgress?.("No OpenClaw source files were detected.");
1402
2385
  throw new Error(
1403
- "No OpenClaw sources were detected. Run `xerg doctor` or provide --log-file / --sessions-dir."
2386
+ `No OpenClaw sources were detected. Run \`${options.commandPrefix ?? "xerg"} doctor\` or provide --log-file / --sessions-dir.`
1404
2387
  );
1405
2388
  }
1406
2389
  options.onProgress?.(`Detected ${sources.length} source file${sources.length === 1 ? "" : "s"}.`);
@@ -1408,47 +2391,75 @@ async function auditOpenClaw(options) {
1408
2391
  const runs = normalizeOpenClawSources(sources, options.since);
1409
2392
  options.onProgress?.(`Normalized ${runs.length} run${runs.length === 1 ? "" : "s"}.`);
1410
2393
  options.onProgress?.("Computing waste and savings findings...");
1411
- const findings = buildFindings(runs);
2394
+ const { findings, wasteAttributions } = buildFindings(runs);
1412
2395
  const dbPath = options.noDb ? void 0 : options.dbPath ?? getDefaultDbPath();
1413
2396
  options.onProgress?.("Building audit summary...");
1414
2397
  const summary = buildAuditSummary({
1415
2398
  runs,
1416
2399
  findings,
2400
+ wasteAttributions,
1417
2401
  sources,
1418
2402
  since: options.since,
1419
2403
  dbPath,
1420
2404
  comparisonKeyOverride: options.comparisonKeyOverride
1421
2405
  });
1422
- if (options.compare && dbPath) {
1423
- options.onProgress?.("Looking for a comparable baseline audit...");
1424
- const baseline = readLatestComparableAuditSummary({
1425
- dbPath,
1426
- comparisonKey: summary.comparisonKey,
1427
- currentAuditId: summary.auditId
1428
- });
1429
- if (baseline) {
1430
- summary.comparison = buildAuditComparison(summary, baseline);
1431
- } else {
1432
- summary.notes = [
1433
- ...summary.notes,
1434
- "No prior comparable audit was found. Run the same audit again after a fix to unlock before/after deltas."
1435
- ];
1436
- }
1437
- }
1438
- if (dbPath) {
1439
- options.onProgress?.(`Persisting local snapshot to ${dbPath}...`);
1440
- persistAudit(
1441
- {
1442
- summary,
1443
- runs,
1444
- pricingCatalog: PRICING_CATALOG
1445
- },
1446
- dbPath
1447
- );
1448
- options.onProgress?.("Local snapshot stored.");
1449
- } else {
1450
- options.onProgress?.("Skipping local snapshot persistence (--no-db).");
1451
- }
2406
+ maybeAttachComparison(options, dbPath, summary);
2407
+ persistLocalSnapshot(summary, runs, dbPath, options.onProgress);
2408
+ return summary;
2409
+ }
2410
+ async function auditCursorUsageCsv(options) {
2411
+ options.onProgress?.("Reading Cursor usage CSV...");
2412
+ validateCompareOptions(options);
2413
+ if (!options.cursorUsageCsv) {
2414
+ throw new Error("No Cursor usage CSV was provided. Use --cursor-usage-csv <path>.");
2415
+ }
2416
+ const parsed = readCursorUsageCsv(options.cursorUsageCsv);
2417
+ options.onProgress?.(`Loaded Cursor usage CSV: ${parsed.source.path}`);
2418
+ options.onProgress?.("Normalizing Cursor usage rows...");
2419
+ const normalized = normalizeCursorUsageCsv({
2420
+ source: parsed.source,
2421
+ rows: parsed.rows,
2422
+ hasObservedCostRows: parsed.hasObservedCostRows,
2423
+ since: options.since
2424
+ });
2425
+ options.onProgress?.(
2426
+ `Normalized ${normalized.runs.length} usage row${normalized.runs.length === 1 ? "" : "s"}.`
2427
+ );
2428
+ options.onProgress?.("Computing Cursor-specific findings...");
2429
+ const { findings, wasteAttributions } = buildCursorUsageFindings(normalized.runs);
2430
+ const dbPath = options.noDb ? void 0 : options.dbPath ?? getDefaultDbPath();
2431
+ options.onProgress?.("Building audit summary...");
2432
+ const summary = buildAuditSummary({
2433
+ runs: normalized.runs,
2434
+ findings,
2435
+ wasteAttributions,
2436
+ sources: [parsed.source],
2437
+ since: options.since,
2438
+ dbPath,
2439
+ comparisonKeyOverride: options.comparisonKeyOverride
2440
+ });
2441
+ summary.pricingCoverage = normalized.pricingCoverage;
2442
+ summary.cursorUsage = normalized.cursorUsage;
2443
+ summary.notes = [
2444
+ "Cursor CSV audits analyze exported usage rows rather than raw session transcripts.",
2445
+ "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.",
2446
+ 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."
2447
+ ];
2448
+ if (parsed.rows.length > 0 && normalized.runs.length === 0 && options.since) {
2449
+ summary.notes = [
2450
+ ...summary.notes,
2451
+ `No Cursor usage rows matched the --since window (${options.since}).`
2452
+ ];
2453
+ }
2454
+ if (normalized.pricingCoverage.unpricedCallCount > 0) {
2455
+ const aliases = normalized.pricingCoverage.topUnpricedModels.map((model) => model.key).join(", ");
2456
+ summary.notes = [
2457
+ ...summary.notes,
2458
+ `Some Cursor aliases do not have full local pricing coverage: ${aliases || "unknown aliases"}.`
2459
+ ];
2460
+ }
2461
+ maybeAttachComparison(options, dbPath, summary);
2462
+ persistLocalSnapshot(summary, normalized.runs, dbPath, options.onProgress);
1452
2463
  return summary;
1453
2464
  }
1454
2465
 
@@ -1499,6 +2510,26 @@ var templatesByKind = {
1499
2510
  suggestedChangeFn: () => ({
1500
2511
  strategy: "cadence-review"
1501
2512
  })
2513
+ },
2514
+ "cache-carryover": {
2515
+ actionType: "prompt-trim",
2516
+ titleFn: () => "Summarize and reset long Cursor chats",
2517
+ descriptionFn: (f) => `${f.summary} Create a compact recall summary, start a fresh chat, and carry forward only the facts the model actually needs.`,
2518
+ suggestedChangeFn: (f) => ({
2519
+ strategy: "conversation-reset",
2520
+ cacheReadShare: f.details.cacheReadShare,
2521
+ totalCacheReadTokens: f.details.totalCacheReadTokens
2522
+ })
2523
+ },
2524
+ "max-mode-concentration": {
2525
+ actionType: "model-switch",
2526
+ titleFn: () => "Reserve max mode for the hardest Cursor turns",
2527
+ descriptionFn: (f) => `${f.summary} Try a two-pass workflow: standard mode first, then escalate only the prompts that truly need max mode.`,
2528
+ suggestedChangeFn: (f) => ({
2529
+ strategy: "tiered-routing",
2530
+ maxModeSpendShare: f.details.maxModeSpendShare,
2531
+ maxModeCallCount: f.details.maxModeCallCount
2532
+ })
1502
2533
  }
1503
2534
  };
1504
2535
  function extractWorkflow(finding) {
@@ -1552,10 +2583,16 @@ function formatPercentDelta(value) {
1552
2583
  const sign = points > 0 ? "+" : "";
1553
2584
  return `${sign}${points.toFixed(0)} pts`;
1554
2585
  }
2586
+ function formatCount(value) {
2587
+ return new Intl.NumberFormat("en-US").format(value);
2588
+ }
1555
2589
  function formatUsdDelta(value) {
1556
2590
  const sign = value > 0 ? "+" : "";
1557
2591
  return `${sign}${formatUsd(value)}`;
1558
2592
  }
2593
+ function isCursorUsageSummary(summary) {
2594
+ return summary.sourceFiles.some((source) => source.kind === "cursor-usage-csv");
2595
+ }
1559
2596
  function topRows(rows, limit = 5) {
1560
2597
  return rows.slice(0, limit).map((row) => {
1561
2598
  return `- ${row.key}: ${formatUsd(row.spendUsd)} (${formatPercent(row.observedShare)} observed)`;
@@ -1657,13 +2694,27 @@ function renderCompareBlock(summary) {
1657
2694
  ...findingChanges.length > 0 ? findingChanges : ["- High-confidence waste changes: none"]
1658
2695
  ];
1659
2696
  }
1660
- function renderDoctorReport(report) {
2697
+ function renderDailyTrendRows(spendByDay, wasteByDay) {
2698
+ if (spendByDay.length <= 1) {
2699
+ return [];
2700
+ }
2701
+ const wasteByDate = new Map(wasteByDay.map((row) => [row.date, row.wasteUsd]));
2702
+ return [
2703
+ "## Daily trend",
2704
+ ...spendByDay.map((row) => {
2705
+ const wasteUsd = wasteByDate.get(row.date) ?? 0;
2706
+ return `- ${row.date}: ${formatUsd(row.spendUsd)} spend, ${formatUsd(wasteUsd)} waste`;
2707
+ })
2708
+ ];
2709
+ }
2710
+ function renderDoctorReport(report, options) {
2711
+ const commandPrefix = options?.commandPrefix ?? "xerg";
1661
2712
  const nextSteps = report.canAudit ? [] : [
1662
2713
  "",
1663
2714
  "## Next steps",
1664
- "- Try explicit local paths: xerg doctor --log-file /path/to/openclaw.log --sessions-dir /path/to/sessions",
1665
- "- Inspect an SSH host: xerg doctor --remote user@host",
1666
- "- Inspect a Railway service: xerg doctor --railway",
2715
+ `- Try explicit local paths: ${commandPrefix} doctor --log-file /path/to/openclaw.log --sessions-dir /path/to/sessions`,
2716
+ `- Inspect an SSH host: ${commandPrefix} doctor --remote user@host`,
2717
+ `- Inspect a Railway service: ${commandPrefix} doctor --railway`,
1667
2718
  "- Remote audits still analyze locally after Xerg pulls the source files to your machine."
1668
2719
  ];
1669
2720
  const sections = [
@@ -1684,7 +2735,177 @@ function renderDoctorReport(report) {
1684
2735
  ];
1685
2736
  return sections.join("\n");
1686
2737
  }
2738
+ function renderCursorDoctorReport(report) {
2739
+ const status = report.canAudit ? "Cursor usage CSV detected." : "Cursor usage CSV is not ready.";
2740
+ return [
2741
+ "# Xerg doctor [cursor csv]",
2742
+ "",
2743
+ status,
2744
+ "",
2745
+ `File: ${report.filePath || "(not provided)"}`,
2746
+ `Rows: ${formatCount(report.rowCount)}`,
2747
+ `Date range: ${report.dateRange ? `${report.dateRange.start} -> ${report.dateRange.end}` : "unavailable"}`,
2748
+ "",
2749
+ "## Pricing coverage",
2750
+ `- Priced rows: ${formatCount(report.pricingCoverage.pricedCallCount)}`,
2751
+ `- Unpriced rows: ${formatCount(report.pricingCoverage.unpricedCallCount)}`,
2752
+ `- Priced tokens: ${formatCount(report.pricingCoverage.pricedTokenCount)}`,
2753
+ `- Unpriced tokens: ${formatCount(report.pricingCoverage.unpricedTokenCount)}`,
2754
+ ...report.pricingCoverage.topUnpricedModels.length > 0 ? report.pricingCoverage.topUnpricedModels.map(
2755
+ (model) => `- Unpriced model: ${model.key} (${formatCount(model.totalTokens)} tokens across ${formatCount(model.callCount)} row${model.callCount === 1 ? "" : "s"})`
2756
+ ) : ["- Unpriced model: none"],
2757
+ "",
2758
+ "## Notes",
2759
+ ...report.notes.map((note) => `- ${note}`)
2760
+ ].join("\n");
2761
+ }
2762
+ function renderCursorModeRows(rows) {
2763
+ if (rows.length === 0) {
2764
+ return ["- none"];
2765
+ }
2766
+ return rows.map((row) => {
2767
+ return `- ${row.key}: ${formatCount(row.callCount)} row${row.callCount === 1 ? "" : "s"}, ${formatCount(row.totalTokens)} tokens, ${formatUsd(row.estimatedSpendUsd)} spend`;
2768
+ });
2769
+ }
2770
+ function renderCursorModelRows(rows) {
2771
+ if (rows.length === 0) {
2772
+ return ["- none"];
2773
+ }
2774
+ return rows.slice(0, 8).map((row) => {
2775
+ 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"}`;
2776
+ return `- ${row.key}: ${formatCount(row.callCount)} row${row.callCount === 1 ? "" : "s"}, ${formatCount(row.totalTokens)} tokens, ${coverage}`;
2777
+ });
2778
+ }
2779
+ function renderCursorPricingCoverage(summary) {
2780
+ const coverage = summary.pricingCoverage;
2781
+ if (!coverage) {
2782
+ return ["- Pricing coverage unavailable"];
2783
+ }
2784
+ return [
2785
+ `- Priced rows: ${formatCount(coverage.pricedCallCount)}`,
2786
+ `- Unpriced rows: ${formatCount(coverage.unpricedCallCount)}`,
2787
+ `- Priced tokens: ${formatCount(coverage.pricedTokenCount)}`,
2788
+ `- Unpriced tokens: ${formatCount(coverage.unpricedTokenCount)}`,
2789
+ ...coverage.topUnpricedModels.length > 0 ? coverage.topUnpricedModels.map(
2790
+ (model) => `- Unpriced model: ${model.key} (${formatCount(model.totalTokens)} tokens across ${formatCount(model.callCount)} row${model.callCount === 1 ? "" : "s"})`
2791
+ ) : ["- Unpriced model: none"]
2792
+ ];
2793
+ }
2794
+ function renderCursorCompareBlock(summary) {
2795
+ if (!summary.comparison) {
2796
+ return [];
2797
+ }
2798
+ const comparison = summary.comparison;
2799
+ const modeSwing = comparison.workflowDeltas[0];
2800
+ const modelSwing = comparison.modelDeltas[0];
2801
+ return [
2802
+ "## Before / after",
2803
+ `Compared against ${comparison.baselineGeneratedAt}`,
2804
+ `- Total spend: ${formatUsd(comparison.baselineTotalSpendUsd)} -> ${formatUsd(summary.totalSpendUsd)} (${formatUsdDelta(comparison.deltaTotalSpendUsd)})`,
2805
+ `- Rows analyzed: ${formatCount(comparison.baselineRunCount)} -> ${formatCount(summary.runCount)} (${comparison.deltaRunCount > 0 ? "+" : ""}${comparison.deltaRunCount})`,
2806
+ `- Usage rows with pricing: ${formatCount(summary.pricingCoverage?.pricedCallCount ?? 0)}`,
2807
+ modeSwing ? `- Mode swing to inspect: ${describeSpendDelta(modeSwing)}` : "- Mode swing to inspect: none",
2808
+ modelSwing ? `- Model swing to inspect: ${describeSpendDelta(modelSwing)}` : "- Model swing to inspect: none"
2809
+ ];
2810
+ }
2811
+ function renderCursorTerminalSummary(summary) {
2812
+ const usage = summary.cursorUsage;
2813
+ return [
2814
+ "# Xerg audit [cursor csv]",
2815
+ "",
2816
+ `Total spend: ${formatUsd(summary.totalSpendUsd)}`,
2817
+ `Observed spend: ${formatUsd(summary.observedSpendUsd)}`,
2818
+ `Estimated spend: ${formatUsd(summary.estimatedSpendUsd)}`,
2819
+ `Rows analyzed: ${formatCount(summary.runCount)}`,
2820
+ `Usage rows with pricing: ${formatCount(summary.pricingCoverage?.pricedCallCount ?? 0)} / ${formatCount(summary.runCount)}`,
2821
+ `Total tokens: ${formatCount(usage?.totalTokens ?? 0)}`,
2822
+ `Structural waste identified: ${formatUsd(summary.wasteSpendUsd)} (${formatPercent(summary.structuralWasteRate)})`,
2823
+ `Potential impact surfaced: ${formatUsd(summary.opportunitySpendUsd)}`,
2824
+ "",
2825
+ "## Token mix",
2826
+ `- Input tokens: ${formatCount(usage?.totalInputTokens ?? 0)}`,
2827
+ `- Output tokens: ${formatCount(usage?.totalOutputTokens ?? 0)}`,
2828
+ `- Cache read tokens: ${formatCount(usage?.totalCacheReadTokens ?? 0)}`,
2829
+ `- Input (cache write): ${formatCount(usage?.totalInputWithCacheWriteTokens ?? 0)}`,
2830
+ `- Input (no cache write): ${formatCount(usage?.totalInputWithoutCacheWriteTokens ?? 0)}`,
2831
+ "",
2832
+ "## Max mode usage",
2833
+ ...renderCursorModeRows(usage?.modes ?? []),
2834
+ "",
2835
+ "## Model mix",
2836
+ ...renderCursorModelRows(usage?.models ?? []),
2837
+ "",
2838
+ "## Pricing coverage",
2839
+ ...renderCursorPricingCoverage(summary),
2840
+ "",
2841
+ ...renderDailyTrendRows(summary.spendByDay, summary.wasteByDay),
2842
+ ...summary.spendByDay.length > 1 ? [""] : [],
2843
+ "## Waste taxonomy",
2844
+ "Structural waste",
2845
+ ...renderTaxonomyRows(summary.wasteByKind, "No confirmed waste buckets detected."),
2846
+ "Savings opportunities",
2847
+ ...renderTaxonomyRows(
2848
+ summary.opportunityByKind,
2849
+ "No opportunity buckets detected.",
2850
+ "(directional)"
2851
+ ),
2852
+ "",
2853
+ "## Findings",
2854
+ ...renderFindingList(summary.findings, "none detected"),
2855
+ "",
2856
+ ...renderCursorCompareBlock(summary),
2857
+ ...summary.comparison ? [""] : [],
2858
+ "## Notes",
2859
+ ...summary.notes.map((note) => `- ${note}`)
2860
+ ].join("\n");
2861
+ }
2862
+ function renderCursorMarkdownSummary(summary) {
2863
+ const usage = summary.cursorUsage;
2864
+ return [
2865
+ "# Xerg Cursor CSV Audit",
2866
+ "",
2867
+ `- Generated: ${summary.generatedAt}`,
2868
+ `- Total spend: ${formatUsd(summary.totalSpendUsd)}`,
2869
+ `- Observed spend: ${formatUsd(summary.observedSpendUsd)}`,
2870
+ `- Estimated spend: ${formatUsd(summary.estimatedSpendUsd)}`,
2871
+ `- Structural waste identified: ${formatUsd(summary.wasteSpendUsd)} (${formatPercent(summary.structuralWasteRate)})`,
2872
+ `- Potential impact surfaced: ${formatUsd(summary.opportunitySpendUsd)}`,
2873
+ `- Rows analyzed: ${formatCount(summary.runCount)}`,
2874
+ `- Usage rows with pricing: ${formatCount(summary.pricingCoverage?.pricedCallCount ?? 0)} / ${formatCount(summary.runCount)}`,
2875
+ `- Total tokens: ${formatCount(usage?.totalTokens ?? 0)}`,
2876
+ "",
2877
+ "## Token mix",
2878
+ `- Input tokens: ${formatCount(usage?.totalInputTokens ?? 0)}`,
2879
+ `- Output tokens: ${formatCount(usage?.totalOutputTokens ?? 0)}`,
2880
+ `- Cache read tokens: ${formatCount(usage?.totalCacheReadTokens ?? 0)}`,
2881
+ "",
2882
+ "## Max mode usage",
2883
+ ...renderCursorModeRows(usage?.modes ?? []),
2884
+ "",
2885
+ "## Model mix",
2886
+ ...renderCursorModelRows(usage?.models ?? []),
2887
+ "",
2888
+ "## Pricing coverage",
2889
+ ...renderCursorPricingCoverage(summary),
2890
+ "",
2891
+ ...renderDailyTrendRows(summary.spendByDay, summary.wasteByDay),
2892
+ ...summary.spendByDay.length > 1 ? [""] : [],
2893
+ ...renderTaxonomyBlock(summary),
2894
+ "",
2895
+ "## Findings",
2896
+ ...summary.findings.slice(0, 10).map((finding) => {
2897
+ return `- **${finding.title}** (${finding.classification}, ${finding.confidence}) \u2014 ${finding.summary} Estimated impact: ${formatUsd(finding.costImpactUsd)}.`;
2898
+ }),
2899
+ ...summary.comparison ? ["", ...renderCursorCompareBlock(summary)] : [],
2900
+ "",
2901
+ "## Notes",
2902
+ ...summary.notes.map((note) => `- ${note}`)
2903
+ ].join("\n");
2904
+ }
1687
2905
  function renderTerminalSummary(summary) {
2906
+ if (isCursorUsageSummary(summary)) {
2907
+ return renderCursorTerminalSummary(summary);
2908
+ }
1688
2909
  const wasteFindings = summary.findings.filter((finding) => finding.classification === "waste");
1689
2910
  const opportunityFindings = summary.findings.filter(
1690
2911
  (finding) => finding.classification === "opportunity"
@@ -1710,6 +2931,8 @@ function renderTerminalSummary(summary) {
1710
2931
  "## Top models",
1711
2932
  ...topRows(summary.spendByModel),
1712
2933
  "",
2934
+ ...renderDailyTrendRows(summary.spendByDay, summary.wasteByDay),
2935
+ ...summary.spendByDay.length > 1 ? [""] : [],
1713
2936
  "## High-confidence waste",
1714
2937
  ...renderFindingList(wasteFindings, "none detected"),
1715
2938
  "",
@@ -1731,6 +2954,9 @@ function renderTerminalSummary(summary) {
1731
2954
  ].join("\n");
1732
2955
  }
1733
2956
  function renderMarkdownSummary(summary) {
2957
+ if (isCursorUsageSummary(summary)) {
2958
+ return renderCursorMarkdownSummary(summary);
2959
+ }
1734
2960
  const lines = [
1735
2961
  "# Xerg Audit Report",
1736
2962
  "",
@@ -1748,6 +2974,9 @@ function renderMarkdownSummary(summary) {
1748
2974
  "## Top workflows",
1749
2975
  ...topRows(summary.spendByWorkflow),
1750
2976
  "",
2977
+ ...renderDailyTrendRows(summary.spendByDay, summary.wasteByDay),
2978
+ ...summary.spendByDay.length > 1 ? [""] : [],
2979
+ "",
1751
2980
  "## Findings",
1752
2981
  ...summary.findings.slice(0, 10).map((finding) => {
1753
2982
  return `- **${finding.title}** (${finding.classification}, ${finding.confidence}) \u2014 ${finding.summary} Estimated impact: ${formatUsd(finding.costImpactUsd)}.`;
@@ -1817,6 +3046,8 @@ function toWirePayload(summary, meta) {
1817
3046
  opportunityByKind: summary.opportunityByKind,
1818
3047
  spendByWorkflow: summary.spendByWorkflow,
1819
3048
  spendByModel: summary.spendByModel,
3049
+ spendByDay: summary.spendByDay,
3050
+ wasteByDay: summary.wasteByDay,
1820
3051
  findings: summary.findings.map(toWireFinding),
1821
3052
  notes: summary.notes,
1822
3053
  comparison: summary.comparison ? toWireComparison(summary.comparison) : null
@@ -1889,12 +3120,12 @@ async function pushAudit(payload, config) {
1889
3120
  }
1890
3121
 
1891
3122
  // src/push/config.ts
1892
- import { readFileSync as readFileSync3 } from "fs";
3123
+ import { readFileSync as readFileSync4 } from "fs";
1893
3124
  import { homedir as homedir3 } from "os";
1894
3125
  import { join as join4 } from "path";
1895
3126
 
1896
3127
  // src/auth/credentials.ts
1897
- import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
3128
+ import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync, writeFileSync } from "fs";
1898
3129
  import { homedir as homedir2 } from "os";
1899
3130
  import { dirname as dirname2, join as join3 } from "path";
1900
3131
  function getCredentialsPath() {
@@ -1912,7 +3143,7 @@ function loadStoredCredentials() {
1912
3143
  const credPath = getCredentialsPath();
1913
3144
  try {
1914
3145
  if (!existsSync(credPath)) return null;
1915
- const raw = readFileSync2(credPath, "utf8");
3146
+ const raw = readFileSync3(credPath, "utf8");
1916
3147
  const parsed = JSON.parse(raw);
1917
3148
  return parsed.token || null;
1918
3149
  } catch {
@@ -1943,7 +3174,7 @@ function loadPushConfig() {
1943
3174
  };
1944
3175
  }
1945
3176
  try {
1946
- const raw = readFileSync3(CONFIG_PATH, "utf8");
3177
+ const raw = readFileSync4(CONFIG_PATH, "utf8");
1947
3178
  const parsed = JSON.parse(raw);
1948
3179
  if (parsed.apiKey) {
1949
3180
  return {
@@ -1961,11 +3192,14 @@ function loadPushConfig() {
1961
3192
  };
1962
3193
  }
1963
3194
  throw new Error(
1964
- `No API key configured. Set XERG_API_KEY, add "apiKey" to ${CONFIG_PATH}, or run \`xerg login\`.
3195
+ `No API key configured. Set XERG_API_KEY, add "apiKey" to ${CONFIG_PATH}, or run \`${formatCommand("login")}\`.
1965
3196
  Get your key at https://xerg.ai/dashboard/settings`
1966
3197
  );
1967
3198
  }
1968
3199
 
3200
+ // src/source-meta.ts
3201
+ import { hostname } from "os";
3202
+
1969
3203
  // src/transport/ssh.ts
1970
3204
  import { execSync, spawnSync } from "child_process";
1971
3205
  import { createHash as createHash2 } from "crypto";
@@ -2522,7 +3756,7 @@ async function pullRemoteFilesRailway(opts) {
2522
3756
  const { status } = railwayExec("echo ok", target);
2523
3757
  if (status !== 0) {
2524
3758
  throw new Error(
2525
- `Cannot reach Railway service${target ? ` (project: ${target.projectId})` : " (linked project)"}. Check railway CLI auth and service configuration.`
3759
+ target ? `Cannot reach Railway service ${target.serviceId} (project: ${target.projectId}). Check the provided --project / --environment / --service values and confirm the Railway CLI can reach that service.` : "Cannot reach the Railway service linked to this directory. Run `railway link` here and choose the OpenClaw app service, or pass --project / --environment / --service explicitly."
2526
3760
  );
2527
3761
  }
2528
3762
  onProgress?.("Railway service reachable.");
@@ -2535,7 +3769,7 @@ async function pullRemoteFilesRailway(opts) {
2535
3769
  const resolvedSessionsPath = findSessionsPath(target, source.sessionsDir);
2536
3770
  let pulledLog = false;
2537
3771
  let pulledSessions = false;
2538
- if (logCheck.exists) {
3772
+ if (logCheck.fileCount > 0) {
2539
3773
  onProgress?.(`Pulling gateway logs from ${remoteLogPath}...`);
2540
3774
  const { stdout: isFile } = railwayExec(`test -f ${remoteLogPath} && echo file`, target);
2541
3775
  if (isFile === "file") {
@@ -2571,8 +3805,9 @@ async function pullRemoteFilesRailway(opts) {
2571
3805
  const checkedPaths = [remoteLogPath, DEFAULT_SESSIONS_DIR2, ...ALTERNATE_SESSION_PATHS].join(
2572
3806
  ", "
2573
3807
  );
3808
+ const wrongServiceHint = target ? " Verify that the selected service is the OpenClaw app, or use --remote-log-file / --remote-sessions-dir for custom paths." : " If this directory is linked to a database or sidecar instead of the OpenClaw app, run `railway link` again and choose the app service.";
2574
3809
  throw new Error(
2575
- `No OpenClaw data found on Railway service. Checked: ${checkedPaths}. Use --remote-log-file or --remote-sessions-dir to specify custom paths.`
3810
+ `No OpenClaw data found on Railway service. Checked: ${checkedPaths}. Use --remote-log-file or --remote-sessions-dir to specify custom paths.${wrongServiceHint}`
2576
3811
  );
2577
3812
  }
2578
3813
  const result = {
@@ -2635,12 +3870,12 @@ async function runRailwayDoctor(opts) {
2635
3870
  railwayAuthenticated: true,
2636
3871
  railwayAuthUser,
2637
3872
  serviceReachable: false,
2638
- serviceError: target ? `Cannot reach service ${target.serviceId}` : "Cannot reach linked service. Run: railway link",
3873
+ serviceError: target ? `Cannot reach service ${target.serviceId} in project ${target.projectId}` : "Current directory is not linked to a reachable Railway service. Run `railway link` here and choose the OpenClaw app service, or pass --project / --environment / --service.",
2639
3874
  defaultPaths: emptyDefaultPaths(),
2640
3875
  alternateSessionPaths: [],
2641
3876
  notes: [
2642
3877
  ...notes,
2643
- target ? `Service unreachable (project: ${target.projectId}, service: ${target.serviceId})` : "Service unreachable. Ensure a project is linked with: railway link"
3878
+ target ? `Service unreachable (project: ${target.projectId}, service: ${target.serviceId}). Verify the provided Railway IDs point at the OpenClaw app service.` : "Service unreachable for the current directory. Run `railway link` here and choose the OpenClaw app service, or pass explicit Railway IDs."
2644
3879
  ]
2645
3880
  };
2646
3881
  }
@@ -2700,9 +3935,11 @@ async function runRailwayDoctor(opts) {
2700
3935
  alternateSessionPaths,
2701
3936
  notes
2702
3937
  };
3938
+ let logCheck = null;
3939
+ let sessCheck = null;
2703
3940
  if (source.logFile || source.sessionsDir) {
2704
- const logCheck = source.logFile ? checkRemotePath(source.logFile, target) : null;
2705
- const sessCheck = source.sessionsDir ? checkRemotePath(source.sessionsDir, target) : null;
3941
+ logCheck = source.logFile ? checkRemotePath(source.logFile, target) : null;
3942
+ sessCheck = source.sessionsDir ? checkRemotePath(source.sessionsDir, target) : null;
2706
3943
  report.customPaths = {
2707
3944
  logFileExists: logCheck?.exists ?? false,
2708
3945
  logFilePath: source.logFile ?? "",
@@ -2725,6 +3962,12 @@ async function runRailwayDoctor(opts) {
2725
3962
  notes.push(`Custom sessions path ${source.sessionsDir}: not found`);
2726
3963
  }
2727
3964
  }
3965
+ const foundOpenClawData = gateway.fileCount > 0 || sessions.fileCount > 0 || alternateSessionPaths.some((path) => path.fileCount > 0) || (logCheck?.fileCount ?? 0) > 0 || (sessCheck?.fileCount ?? 0) > 0;
3966
+ if (!foundOpenClawData) {
3967
+ notes.push(
3968
+ target ? "No OpenClaw data was found on this Railway service. Verify that the selected service is the OpenClaw app, or use --remote-log-file / --remote-sessions-dir for custom paths." : "No OpenClaw data was found on the linked Railway service. If this directory is linked to a database or sidecar instead of the OpenClaw app, run `railway link` again and choose the app service."
3969
+ );
3970
+ }
2728
3971
  return report;
2729
3972
  }
2730
3973
  function emptyDefaultPaths() {
@@ -2748,13 +3991,13 @@ function formatBytes2(bytes) {
2748
3991
  }
2749
3992
 
2750
3993
  // src/transport/config.ts
2751
- import { readFileSync as readFileSync4 } from "fs";
2752
- import { resolve as resolve2 } from "path";
3994
+ import { readFileSync as readFileSync5 } from "fs";
3995
+ import { resolve as resolve3 } from "path";
2753
3996
  function loadRemoteConfig(configPath) {
2754
- const resolved = resolve2(configPath);
3997
+ const resolved = resolve3(configPath);
2755
3998
  let raw;
2756
3999
  try {
2757
- raw = readFileSync4(resolved, "utf8");
4000
+ raw = readFileSync5(resolved, "utf8");
2758
4001
  } catch {
2759
4002
  throw new Error(`Cannot read remote config at ${resolved}`);
2760
4003
  }
@@ -2825,6 +4068,74 @@ function validateRailwayEntry(entry) {
2825
4068
  };
2826
4069
  }
2827
4070
 
4071
+ // src/source-meta.ts
4072
+ var RAILWAY_SOURCE_ID = "OpenClaw - Railway";
4073
+ function buildLocalPushSourceMeta(kind, localHost = hostname()) {
4074
+ return {
4075
+ environment: "local",
4076
+ sourceId: `${kind === "cursor" ? "Cursor" : "OpenClaw"} - ${localHost}`,
4077
+ sourceHost: localHost
4078
+ };
4079
+ }
4080
+ function buildRemotePushSourceMeta(source) {
4081
+ if (source.transport === "railway") {
4082
+ return {
4083
+ environment: "railway",
4084
+ sourceId: isGeneratedRailwayName(source.name) ? RAILWAY_SOURCE_ID : `OpenClaw - ${source.name}`,
4085
+ sourceHost: isGeneratedRailwayName(source.host) ? "Railway" : source.host
4086
+ };
4087
+ }
4088
+ return {
4089
+ environment: "remote",
4090
+ sourceId: `OpenClaw - ${source.name}`,
4091
+ sourceHost: resolveRemoteHost(source.host)
4092
+ };
4093
+ }
4094
+ function buildCachedPushSourceMeta(summary, localHost = hostname()) {
4095
+ const sourceFiles = summary.sourceFiles ?? [];
4096
+ const comparisonKey = summary.comparisonKey ?? "";
4097
+ if (sourceFiles.some((sourceFile) => sourceFile.kind === "cursor-usage-csv")) {
4098
+ return buildLocalPushSourceMeta("cursor", localHost);
4099
+ }
4100
+ if (isRailwayComparisonKey(comparisonKey)) {
4101
+ return {
4102
+ environment: "railway",
4103
+ sourceId: RAILWAY_SOURCE_ID,
4104
+ sourceHost: "Railway"
4105
+ };
4106
+ }
4107
+ const remoteHost = parseRemoteHostFromComparisonKey(comparisonKey);
4108
+ if (remoteHost) {
4109
+ return {
4110
+ environment: "remote",
4111
+ sourceId: `OpenClaw - ${remoteHost}`,
4112
+ sourceHost: remoteHost
4113
+ };
4114
+ }
4115
+ return buildLocalPushSourceMeta("openclaw", localHost);
4116
+ }
4117
+ function isGeneratedRailwayName(name) {
4118
+ return name === "railway-linked" || /^railway-[a-z0-9]{8}$/i.test(name);
4119
+ }
4120
+ function isRailwayComparisonKey(comparisonKey) {
4121
+ return comparisonKey.startsWith("railway:") || comparisonKey.startsWith("railway-linked:");
4122
+ }
4123
+ function parseRemoteHostFromComparisonKey(comparisonKey) {
4124
+ const parts = comparisonKey.split(":");
4125
+ if (parts.length < 3) {
4126
+ return null;
4127
+ }
4128
+ const target = parts.slice(0, -2).join(":");
4129
+ if (!target) {
4130
+ return null;
4131
+ }
4132
+ return resolveRemoteHost(target);
4133
+ }
4134
+ function resolveRemoteHost(target) {
4135
+ const parsed = parseRemoteTarget(target);
4136
+ return parsed.host || target;
4137
+ }
4138
+
2828
4139
  // src/commands/audit.ts
2829
4140
  var NO_DATA_PATTERN = /no openclaw sources were detected/i;
2830
4141
  async function auditOrNoData(...args) {
@@ -2842,6 +4153,7 @@ async function runAuditCommand(options) {
2842
4153
  if (options.dryRun && !options.push) {
2843
4154
  throw new Error("--dry-run requires --push.");
2844
4155
  }
4156
+ validateCursorUsageCsvOptions(options);
2845
4157
  const remoteFlags = [options.remote, options.remoteConfig, options.railway].filter(
2846
4158
  Boolean
2847
4159
  ).length;
@@ -2886,6 +4198,26 @@ function buildRailwayTarget(options) {
2886
4198
  return void 0;
2887
4199
  }
2888
4200
  async function runLocalAudit(options, logger) {
4201
+ if (options.cursorUsageCsv) {
4202
+ logger.verbose("Running a local Cursor usage CSV audit.");
4203
+ logger.verbose(`Using Cursor usage CSV: ${options.cursorUsageCsv}`);
4204
+ const summary2 = await auditCursorUsageCsv({
4205
+ cursorUsageCsv: options.cursorUsageCsv,
4206
+ since: options.since,
4207
+ compare: options.compare,
4208
+ dbPath: options.db,
4209
+ noDb: options.noDb,
4210
+ commandPrefix: options.commandPrefix,
4211
+ onProgress: logger.verbose
4212
+ });
4213
+ renderOutput(summary2, options);
4214
+ if (options.push) {
4215
+ const meta = buildMeta(buildLocalPushSourceMeta("cursor"));
4216
+ await handlePush(summary2, meta, options);
4217
+ }
4218
+ checkThresholds(summary2, options);
4219
+ return;
4220
+ }
2889
4221
  logger.verbose("Running a local audit.");
2890
4222
  if (options.logFile) {
2891
4223
  logger.verbose(`Using explicit local log file: ${options.logFile}`);
@@ -2900,15 +4232,37 @@ async function runLocalAudit(options, logger) {
2900
4232
  compare: options.compare,
2901
4233
  dbPath: options.db,
2902
4234
  noDb: options.noDb,
4235
+ commandPrefix: options.commandPrefix,
2903
4236
  onProgress: logger.verbose
2904
4237
  });
2905
4238
  renderOutput(summary, options);
2906
4239
  if (options.push) {
2907
- const meta = buildMeta({ environment: "local", sourceId: hostname(), sourceHost: hostname() });
4240
+ const meta = buildMeta(buildLocalPushSourceMeta("openclaw"));
2908
4241
  await handlePush(summary, meta, options);
2909
4242
  }
2910
4243
  checkThresholds(summary, options);
2911
4244
  }
4245
+ function validateCursorUsageCsvOptions(options) {
4246
+ if (!options.cursorUsageCsv) {
4247
+ return;
4248
+ }
4249
+ const conflicts = [
4250
+ options.logFile ? "--log-file" : null,
4251
+ options.sessionsDir ? "--sessions-dir" : null,
4252
+ options.remote ? "--remote" : null,
4253
+ options.remoteLogFile ? "--remote-log-file" : null,
4254
+ options.remoteSessionsDir ? "--remote-sessions-dir" : null,
4255
+ options.remoteConfig ? "--remote-config" : null,
4256
+ options.keepRemoteFiles ? "--keep-remote-files" : null,
4257
+ options.railway ? "--railway" : null,
4258
+ options.railwayProject ? "--project" : null,
4259
+ options.railwayEnvironment ? "--environment" : null,
4260
+ options.railwayService ? "--service" : null
4261
+ ].filter((flag) => flag !== null);
4262
+ if (conflicts.length > 0) {
4263
+ throw new Error(`The --cursor-usage-csv flag cannot be combined with ${conflicts.join(", ")}.`);
4264
+ }
4265
+ }
2912
4266
  function getComparisonKey(source) {
2913
4267
  if (source.transport === "railway") {
2914
4268
  return buildComparisonKeyForRailway(source);
@@ -2927,9 +4281,6 @@ function describeSource(source) {
2927
4281
  }
2928
4282
  return source.host;
2929
4283
  }
2930
- function sourceEnvironment(source) {
2931
- return source.transport === "railway" ? "railway" : "remote";
2932
- }
2933
4284
  async function runSingleRemoteAudit(source, options, logger) {
2934
4285
  logger.info(`Pulling files from ${describeSource(source)}...`);
2935
4286
  const pullResult = await pullFiles(
@@ -2949,15 +4300,12 @@ async function runSingleRemoteAudit(source, options, logger) {
2949
4300
  dbPath: options.db,
2950
4301
  noDb: options.noDb,
2951
4302
  comparisonKeyOverride,
4303
+ commandPrefix: options.commandPrefix,
2952
4304
  onProgress: logger.verbose
2953
4305
  });
2954
4306
  renderOutput(summary, options);
2955
4307
  if (options.push) {
2956
- const meta = buildMeta({
2957
- environment: sourceEnvironment(source),
2958
- sourceId: source.name,
2959
- sourceHost: source.host
2960
- });
4308
+ const meta = buildMeta(buildRemotePushSourceMeta(source));
2961
4309
  await handlePush(summary, meta, options);
2962
4310
  }
2963
4311
  checkThresholds(summary, options);
@@ -3002,6 +4350,7 @@ ${errorMessages}`);
3002
4350
  dbPath: options.db,
3003
4351
  noDb: options.noDb,
3004
4352
  comparisonKeyOverride,
4353
+ commandPrefix: options.commandPrefix,
3005
4354
  onProgress: logger.verbose
3006
4355
  });
3007
4356
  summaries.push({ name: source.name, source, summary });
@@ -3044,11 +4393,7 @@ ${"\u2550".repeat(60)}
3044
4393
  }
3045
4394
  if (options.push) {
3046
4395
  for (const { source, summary } of summaries) {
3047
- const meta = buildMeta({
3048
- environment: sourceEnvironment(source),
3049
- sourceId: source.name,
3050
- sourceHost: source.host
3051
- });
4396
+ const meta = buildMeta(buildRemotePushSourceMeta(source));
3052
4397
  await handlePush(summary, meta, options);
3053
4398
  }
3054
4399
  }
@@ -3064,7 +4409,7 @@ ${"\u2550".repeat(60)}
3064
4409
  function readCliVersion() {
3065
4410
  try {
3066
4411
  const packageJsonPath = new URL("../../package.json", import.meta.url);
3067
- const pkg = JSON.parse(readFileSync5(packageJsonPath, "utf8"));
4412
+ const pkg = JSON.parse(readFileSync6(packageJsonPath, "utf8"));
3068
4413
  return pkg.version ?? "0.0.0";
3069
4414
  } catch {
3070
4415
  return "0.0.0";
@@ -3147,6 +4492,7 @@ function cleanupPullResult(pullResult, keepFiles) {
3147
4492
  // src/commands/doctor.ts
3148
4493
  async function runDoctorCommand(options) {
3149
4494
  const logger = createCliLogger({ verbose: options.verbose });
4495
+ validateCursorUsageCsvOptions2(options);
3150
4496
  if (options.railway) {
3151
4497
  logger.verbose("Inspecting Railway audit readiness.");
3152
4498
  const railwayTarget = buildRailwayTarget2(options);
@@ -3169,6 +4515,17 @@ async function runDoctorCommand(options) {
3169
4515
  });
3170
4516
  const report2 = await runRemoteDoctor({ source, onProgress: logger.verbose });
3171
4517
  process.stdout.write(`${renderRemoteDoctorReport(report2)}
4518
+ `);
4519
+ return;
4520
+ }
4521
+ if (options.cursorUsageCsv) {
4522
+ logger.verbose("Inspecting local Cursor usage CSV audit readiness.");
4523
+ logger.verbose(`Using Cursor usage CSV: ${options.cursorUsageCsv}`);
4524
+ const report2 = await doctorCursorUsageCsv({
4525
+ cursorUsageCsv: options.cursorUsageCsv,
4526
+ onProgress: logger.verbose
4527
+ });
4528
+ process.stdout.write(`${renderCursorDoctorReport(report2)}
3172
4529
  `);
3173
4530
  return;
3174
4531
  }
@@ -3184,9 +4541,28 @@ async function runDoctorCommand(options) {
3184
4541
  sessionsDir: options.sessionsDir,
3185
4542
  onProgress: logger.verbose
3186
4543
  });
3187
- process.stdout.write(`${renderDoctorReport(report)}
4544
+ process.stdout.write(`${renderDoctorReport(report, { commandPrefix: options.commandPrefix })}
3188
4545
  `);
3189
4546
  }
4547
+ function validateCursorUsageCsvOptions2(options) {
4548
+ if (!options.cursorUsageCsv) {
4549
+ return;
4550
+ }
4551
+ const conflicts = [
4552
+ options.logFile ? "--log-file" : null,
4553
+ options.sessionsDir ? "--sessions-dir" : null,
4554
+ options.remote ? "--remote" : null,
4555
+ options.remoteLogFile ? "--remote-log-file" : null,
4556
+ options.remoteSessionsDir ? "--remote-sessions-dir" : null,
4557
+ options.railway ? "--railway" : null,
4558
+ options.railwayProject ? "--project" : null,
4559
+ options.railwayEnvironment ? "--environment" : null,
4560
+ options.railwayService ? "--service" : null
4561
+ ].filter((flag) => flag !== null);
4562
+ if (conflicts.length > 0) {
4563
+ throw new Error(`The --cursor-usage-csv flag cannot be combined with ${conflicts.join(", ")}.`);
4564
+ }
4565
+ }
3190
4566
  function buildRailwayTarget2(options) {
3191
4567
  if (options.railwayProject && options.railwayEnvironment && options.railwayService) {
3192
4568
  return {
@@ -3308,7 +4684,7 @@ async function runLoginCommand() {
3308
4684
  if (existing) {
3309
4685
  process.stderr.write(
3310
4686
  `Already logged in. Credentials stored at ${getCredentialsPath()}.
3311
- Run ${colorBold("xerg logout")} first to re-authenticate.
4687
+ Run ${colorBold(formatCommand("logout"))} first to re-authenticate.
3312
4688
  `
3313
4689
  );
3314
4690
  return;
@@ -3374,7 +4750,7 @@ Credentials saved to ${getCredentialsPath()}.
3374
4750
  continue;
3375
4751
  }
3376
4752
  if (res.status === 410) {
3377
- throw new Error("Device code expired. Please run `xerg login` again.");
4753
+ throw new Error(`Device code expired. Please run \`${formatCommand("login")}\` again.`);
3378
4754
  }
3379
4755
  const body = await res.json().catch(() => ({}));
3380
4756
  throw new Error(body.error || `Unexpected response: HTTP ${res.status}`);
@@ -3384,7 +4760,7 @@ Credentials saved to ${getCredentialsPath()}.
3384
4760
  }
3385
4761
  }
3386
4762
  }
3387
- throw new Error("Authentication timed out. Please run `xerg login` again.");
4763
+ throw new Error(`Authentication timed out. Please run \`${formatCommand("login")}\` again.`);
3388
4764
  }
3389
4765
  async function openBrowser(url) {
3390
4766
  const { exec } = await import("child_process");
@@ -3396,12 +4772,12 @@ async function openBrowser(url) {
3396
4772
  };
3397
4773
  const cmd = commands[platform2()];
3398
4774
  if (!cmd) return;
3399
- return new Promise((resolve3) => {
3400
- exec(`${cmd} ${JSON.stringify(url)}`, () => resolve3());
4775
+ return new Promise((resolve4) => {
4776
+ exec(`${cmd} ${JSON.stringify(url)}`, () => resolve4());
3401
4777
  });
3402
4778
  }
3403
4779
  function sleep(ms) {
3404
- return new Promise((resolve3) => setTimeout(resolve3, ms));
4780
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
3405
4781
  }
3406
4782
  function colorBold(text) {
3407
4783
  return process.stderr.isTTY ? styleText("bold", text) : text;
@@ -3422,8 +4798,7 @@ function runLogoutCommand() {
3422
4798
  }
3423
4799
 
3424
4800
  // src/commands/push.ts
3425
- import { readFileSync as readFileSync6 } from "fs";
3426
- import { hostname as hostname2 } from "os";
4801
+ import { readFileSync as readFileSync7 } from "fs";
3427
4802
  async function runPushCommand(options) {
3428
4803
  const payload = options.file ? loadPayloadFromFile(options.file) : loadPayloadFromCache();
3429
4804
  if (options.dryRun) {
@@ -3447,7 +4822,7 @@ async function runPushCommand(options) {
3447
4822
  function loadPayloadFromFile(filePath) {
3448
4823
  let raw;
3449
4824
  try {
3450
- raw = readFileSync6(filePath, "utf8");
4825
+ raw = readFileSync7(filePath, "utf8");
3451
4826
  } catch {
3452
4827
  throw new Error(`Cannot read file: ${filePath}`);
3453
4828
  }
@@ -3472,16 +4847,16 @@ function loadPayloadFromCache() {
3472
4847
  summaries = listStoredAuditSummaries(dbPath);
3473
4848
  } catch {
3474
4849
  throw new NoDataError(
3475
- "No local audit database found. Run `xerg audit` first, or use `xerg push --file <path>`."
4850
+ `No local audit database found. Run \`${formatCommand("audit")}\` first, or use \`${formatCommand("push --file <path>")}\`.`
3476
4851
  );
3477
4852
  }
3478
4853
  if (summaries.length === 0) {
3479
4854
  throw new NoDataError(
3480
- "No cached audit snapshots found. Run `xerg audit` first, or use `xerg push --file <path>`."
4855
+ `No cached audit snapshots found. Run \`${formatCommand("audit")}\` first, or use \`${formatCommand("push --file <path>")}\`.`
3481
4856
  );
3482
4857
  }
3483
4858
  const latest = summaries[0];
3484
- const meta = buildMeta2();
4859
+ const meta = buildMeta2(latest);
3485
4860
  process.stderr.write(
3486
4861
  `Using most recent cached audit: ${latest.auditId} (${latest.generatedAt})
3487
4862
  `
@@ -3491,27 +4866,152 @@ function loadPayloadFromCache() {
3491
4866
  function readCliVersion2() {
3492
4867
  try {
3493
4868
  const packageJsonPath = new URL("../../package.json", import.meta.url);
3494
- const pkg = JSON.parse(readFileSync6(packageJsonPath, "utf8"));
4869
+ const pkg = JSON.parse(readFileSync7(packageJsonPath, "utf8"));
3495
4870
  return pkg.version ?? "0.0.0";
3496
4871
  } catch {
3497
4872
  return "0.0.0";
3498
4873
  }
3499
4874
  }
3500
- function buildMeta2() {
4875
+ function buildMeta2(summary) {
4876
+ const sourceMeta = buildCachedPushSourceMeta(summary);
3501
4877
  return {
3502
4878
  cliVersion: readCliVersion2(),
3503
- sourceId: hostname2(),
3504
- sourceHost: hostname2(),
3505
- environment: "local"
4879
+ sourceId: sourceMeta.sourceId,
4880
+ sourceHost: sourceMeta.sourceHost,
4881
+ environment: sourceMeta.environment
3506
4882
  };
3507
4883
  }
3508
4884
 
4885
+ // src/help.ts
4886
+ function renderRootHelp(version, display) {
4887
+ return `${display.name} ${version}
4888
+
4889
+ Waste intelligence for OpenClaw workflows and local Cursor usage CSVs.
4890
+
4891
+ Usage:
4892
+ ${formatCommand("<command> [options]", display.prefix)}
4893
+
4894
+ Commands:
4895
+ audit Analyze OpenClaw logs or a local Cursor usage CSV.
4896
+ doctor Inspect OpenClaw sources or a local Cursor usage CSV.
4897
+ push Push a cached audit snapshot to the Xerg API.
4898
+ login Authenticate with the Xerg API via browser.
4899
+ logout Remove stored Xerg API credentials.
4900
+
4901
+ Global options:
4902
+ -h, --help Show help
4903
+ -v, --version Show version
4904
+ `;
4905
+ }
4906
+ function renderAuditHelp(commandPrefix) {
4907
+ return `${formatCommand("audit", commandPrefix)}
4908
+
4909
+ Analyze OpenClaw logs or a local Cursor usage CSV and produce an audit report.
4910
+
4911
+ Usage:
4912
+ ${formatCommand("audit [options]", commandPrefix)}
4913
+
4914
+ Options:
4915
+ --log-file <path> Explicit OpenClaw gateway log file to analyze
4916
+ --sessions-dir <path> Explicit OpenClaw sessions directory to analyze
4917
+ --cursor-usage-csv <path> Local Cursor usage CSV export to analyze
4918
+ --since <duration> Look back window such as 24h, 7d, or 30m
4919
+ --compare Compare this audit to the newest compatible prior local snapshot
4920
+ --json Render the report as JSON
4921
+ --markdown Render the report as Markdown
4922
+ --db <path> Custom SQLite database path
4923
+ --no-db Skip local persistence
4924
+
4925
+ Remote options (SSH):
4926
+ --remote <user@host> SSH target in user@host or user@host:port format
4927
+ --remote-log-file <path> Override the default gateway log path on the remote host
4928
+ --remote-sessions-dir <path> Override the default sessions directory on the remote host
4929
+ --remote-config <path> Path to a JSON file defining multiple remote sources
4930
+ --keep-remote-files Retain pulled files in ~/.xerg/remote-cache/ instead of using a temp directory
4931
+
4932
+ Prerequisites:
4933
+ SSH remote audits require ssh and rsync on your PATH.
4934
+
4935
+ Railway options:
4936
+ --railway Audit a Railway service (uses linked project by default)
4937
+ --project <id> Railway project ID
4938
+ --environment <id> Railway environment ID
4939
+ --service <id> Railway service ID
4940
+
4941
+ Railway audits require the railway CLI on your PATH.
4942
+
4943
+ Push options:
4944
+ --push Push the audit summary to the Xerg API after computing it
4945
+ --dry-run With --push: print the payload to stdout without sending it
4946
+ --verbose Print progress updates to stderr while the audit runs
4947
+
4948
+ Threshold options:
4949
+ --fail-above-waste-rate <n> Exit with code 3 if structural waste rate exceeds threshold (e.g. 0.30)
4950
+ --fail-above-waste-usd <n> Exit with code 3 if waste spend exceeds threshold in USD (e.g. 50)
4951
+
4952
+ -h, --help Show help
4953
+ `;
4954
+ }
4955
+ function renderPushHelp(commandPrefix) {
4956
+ return `${formatCommand("push", commandPrefix)}
4957
+
4958
+ Push a cached audit snapshot to the Xerg API.
4959
+
4960
+ Usage:
4961
+ ${formatCommand("push [options]", commandPrefix)}
4962
+
4963
+ Options:
4964
+ --file <path> Push a specific snapshot file instead of the most recent cached audit
4965
+ --dry-run Print the payload to stdout without sending it
4966
+
4967
+ -h, --help Show help
4968
+
4969
+ Authentication:
4970
+ Set XERG_API_KEY in your environment, add "apiKey" to ~/.xerg/config.json,
4971
+ or run \`${formatCommand("login", commandPrefix)}\` to authenticate via browser.
4972
+ Browser login stores a token at ~/.config/xerg/credentials.json by default.
4973
+ `;
4974
+ }
4975
+ function renderDoctorHelp(commandPrefix) {
4976
+ return `${formatCommand("doctor", commandPrefix)}
4977
+
4978
+ Inspect OpenClaw sources or a local Cursor usage CSV before you audit.
4979
+
4980
+ Usage:
4981
+ ${formatCommand("doctor [options]", commandPrefix)}
4982
+
4983
+ Options:
4984
+ --log-file <path> Explicit OpenClaw gateway log file to inspect
4985
+ --sessions-dir <path> Explicit OpenClaw sessions directory to inspect
4986
+ --cursor-usage-csv <path> Local Cursor usage CSV export to inspect
4987
+ --verbose Print progress updates to stderr while doctor runs
4988
+
4989
+ Remote options (SSH):
4990
+ --remote <user@host> SSH target in user@host or user@host:port format
4991
+ --remote-log-file <path> Override the default gateway log path on the remote host
4992
+ --remote-sessions-dir <path> Override the default sessions directory on the remote host
4993
+
4994
+ SSH checks require ssh and rsync on your PATH.
4995
+
4996
+ Railway options:
4997
+ --railway Check a Railway service (uses linked project by default)
4998
+ --project <id> Railway project ID
4999
+ --environment <id> Railway environment ID
5000
+ --service <id> Railway service ID
5001
+
5002
+ Railway checks require the railway CLI on your PATH.
5003
+
5004
+ -h, --help Show help
5005
+ `;
5006
+ }
5007
+
3509
5008
  // src/index.ts
3510
5009
  var VERSION = readVersion();
3511
5010
  var argv = process.argv.slice(2);
5011
+ var commandDisplay = resolveCommandDisplay();
3512
5012
  var command = argv[0];
3513
5013
  if (!command || command === "--help" || command === "-h" || command === "help") {
3514
- process.stdout.write(renderRootHelp());
5014
+ process.stdout.write(renderRootHelp(VERSION, commandDisplay));
3515
5015
  process.exit(0);
3516
5016
  }
3517
5017
  if (command === "--version" || command === "-v" || command === "version") {
@@ -3521,7 +5021,7 @@ if (command === "--version" || command === "-v" || command === "version") {
3521
5021
  }
3522
5022
  run().catch((error) => {
3523
5023
  const message = error instanceof Error ? error.message : "Unknown error";
3524
- process.stderr.write(`${colorError(`xerg failed: ${message}`)}
5024
+ process.stderr.write(`${colorError(`${commandDisplay.name} failed: ${message}`)}
3525
5025
  `);
3526
5026
  process.exitCode = error instanceof NoDataError ? 2 : 1;
3527
5027
  });
@@ -3531,12 +5031,18 @@ async function run() {
3531
5031
  if (options.json && options.markdown) {
3532
5032
  throw new Error("Use either --json or --markdown, not both.");
3533
5033
  }
3534
- await runAuditCommand(options);
5034
+ await runAuditCommand({
5035
+ ...options,
5036
+ commandPrefix: commandDisplay.prefix
5037
+ });
3535
5038
  return;
3536
5039
  }
3537
5040
  if (command === "doctor") {
3538
5041
  const options = parseDoctorOptions(argv.slice(1));
3539
- await runDoctorCommand(options);
5042
+ await runDoctorCommand({
5043
+ ...options,
5044
+ commandPrefix: commandDisplay.prefix
5045
+ });
3540
5046
  return;
3541
5047
  }
3542
5048
  if (command === "push") {
@@ -3552,7 +5058,9 @@ async function run() {
3552
5058
  runLogoutCommand();
3553
5059
  return;
3554
5060
  }
3555
- throw new Error(`Unknown command "${command}". Run \`xerg --help\` to see available commands.`);
5061
+ throw new Error(
5062
+ `Unknown command "${command}". Run \`${formatCommand("--help", commandDisplay.prefix)}\` to see available commands.`
5063
+ );
3556
5064
  }
3557
5065
  function parseAuditOptions(raw) {
3558
5066
  const argv2 = expandEqualsArgs(raw);
@@ -3562,7 +5070,7 @@ function parseAuditOptions(raw) {
3562
5070
  switch (arg) {
3563
5071
  case "--help":
3564
5072
  case "-h":
3565
- process.stdout.write(renderAuditHelp());
5073
+ process.stdout.write(renderAuditHelp(commandDisplay.prefix));
3566
5074
  process.exit(0);
3567
5075
  break;
3568
5076
  case "--log-file":
@@ -3573,6 +5081,10 @@ function parseAuditOptions(raw) {
3573
5081
  options.sessionsDir = readValue(arg, argv2[index + 1]);
3574
5082
  index += 1;
3575
5083
  break;
5084
+ case "--cursor-usage-csv":
5085
+ options.cursorUsageCsv = readValue(arg, argv2[index + 1]);
5086
+ index += 1;
5087
+ break;
3576
5088
  case "--since":
3577
5089
  options.since = readValue(arg, argv2[index + 1]);
3578
5090
  index += 1;
@@ -3645,7 +5157,9 @@ function parseAuditOptions(raw) {
3645
5157
  index += 1;
3646
5158
  break;
3647
5159
  default:
3648
- throw new Error(`Unknown audit option "${arg}". Run \`xerg audit --help\` for usage.`);
5160
+ throw new Error(
5161
+ `Unknown audit option "${arg}". Run \`${formatCommand(["audit", "--help"], commandDisplay.prefix)}\` for usage.`
5162
+ );
3649
5163
  }
3650
5164
  }
3651
5165
  return options;
@@ -3658,7 +5172,7 @@ function parsePushOptions(raw) {
3658
5172
  switch (arg) {
3659
5173
  case "--help":
3660
5174
  case "-h":
3661
- process.stdout.write(renderPushHelp());
5175
+ process.stdout.write(renderPushHelp(commandDisplay.prefix));
3662
5176
  process.exit(0);
3663
5177
  break;
3664
5178
  case "--file":
@@ -3669,7 +5183,9 @@ function parsePushOptions(raw) {
3669
5183
  options.dryRun = true;
3670
5184
  break;
3671
5185
  default:
3672
- throw new Error(`Unknown push option "${arg}". Run \`xerg push --help\` for usage.`);
5186
+ throw new Error(
5187
+ `Unknown push option "${arg}". Run \`${formatCommand(["push", "--help"], commandDisplay.prefix)}\` for usage.`
5188
+ );
3673
5189
  }
3674
5190
  }
3675
5191
  return options;
@@ -3682,7 +5198,7 @@ function parseDoctorOptions(raw) {
3682
5198
  switch (arg) {
3683
5199
  case "--help":
3684
5200
  case "-h":
3685
- process.stdout.write(renderDoctorHelp());
5201
+ process.stdout.write(renderDoctorHelp(commandDisplay.prefix));
3686
5202
  process.exit(0);
3687
5203
  break;
3688
5204
  case "--log-file":
@@ -3693,6 +5209,10 @@ function parseDoctorOptions(raw) {
3693
5209
  options.sessionsDir = readValue(arg, argv2[index + 1]);
3694
5210
  index += 1;
3695
5211
  break;
5212
+ case "--cursor-usage-csv":
5213
+ options.cursorUsageCsv = readValue(arg, argv2[index + 1]);
5214
+ index += 1;
5215
+ break;
3696
5216
  case "--remote":
3697
5217
  options.remote = readValue(arg, argv2[index + 1]);
3698
5218
  index += 1;
@@ -3724,7 +5244,9 @@ function parseDoctorOptions(raw) {
3724
5244
  options.verbose = true;
3725
5245
  break;
3726
5246
  default:
3727
- throw new Error(`Unknown doctor option "${arg}". Run \`xerg doctor --help\` for usage.`);
5247
+ throw new Error(
5248
+ `Unknown doctor option "${arg}". Run \`${formatCommand(["doctor", "--help"], commandDisplay.prefix)}\` for usage.`
5249
+ );
3728
5250
  }
3729
5251
  }
3730
5252
  return options;
@@ -3755,131 +5277,12 @@ function readFloat(flag, value) {
3755
5277
  }
3756
5278
  return num;
3757
5279
  }
3758
- function renderRootHelp() {
3759
- return `xerg ${VERSION}
3760
-
3761
- Waste intelligence for OpenClaw workflows.
3762
-
3763
- Usage:
3764
- xerg <command> [options]
3765
-
3766
- Commands:
3767
- audit Analyze OpenClaw logs and produce a waste intelligence report.
3768
- doctor Inspect your machine for OpenClaw sources and audit readiness.
3769
- push Push a cached audit snapshot to the Xerg API.
3770
- login Authenticate with the Xerg API via browser.
3771
- logout Remove stored Xerg API credentials.
3772
-
3773
- Global options:
3774
- -h, --help Show help
3775
- -v, --version Show version
3776
- `;
3777
- }
3778
- function renderAuditHelp() {
3779
- return `xerg audit
3780
-
3781
- Analyze OpenClaw logs and produce a waste intelligence report.
3782
-
3783
- Usage:
3784
- xerg audit [options]
3785
-
3786
- Options:
3787
- --log-file <path> Explicit OpenClaw gateway log file to analyze
3788
- --sessions-dir <path> Explicit OpenClaw sessions directory to analyze
3789
- --since <duration> Look back window such as 24h, 7d, or 30m
3790
- --compare Compare this audit to the newest compatible prior local snapshot
3791
- --json Render the report as JSON
3792
- --markdown Render the report as Markdown
3793
- --db <path> Custom SQLite database path
3794
- --no-db Skip local persistence
3795
-
3796
- Remote options (SSH):
3797
- --remote <user@host> SSH target in user@host or user@host:port format
3798
- --remote-log-file <path> Override the default gateway log path on the remote host
3799
- --remote-sessions-dir <path> Override the default sessions directory on the remote host
3800
- --remote-config <path> Path to a JSON file defining multiple remote sources
3801
- --keep-remote-files Retain pulled files in ~/.xerg/remote-cache/ instead of using a temp directory
3802
-
3803
- Prerequisites:
3804
- SSH remote audits require ssh and rsync on your PATH.
3805
-
3806
- Railway options:
3807
- --railway Audit a Railway service (uses linked project by default)
3808
- --project <id> Railway project ID
3809
- --environment <id> Railway environment ID
3810
- --service <id> Railway service ID
3811
-
3812
- Railway audits require the railway CLI on your PATH.
3813
-
3814
- Push options:
3815
- --push Push the audit summary to the Xerg API after computing it
3816
- --dry-run With --push: print the payload to stdout without sending it
3817
- --verbose Print progress updates to stderr while the audit runs
3818
-
3819
- Threshold options:
3820
- --fail-above-waste-rate <n> Exit with code 3 if structural waste rate exceeds threshold (e.g. 0.30)
3821
- --fail-above-waste-usd <n> Exit with code 3 if waste spend exceeds threshold in USD (e.g. 50)
3822
-
3823
- -h, --help Show help
3824
- `;
3825
- }
3826
- function renderPushHelp() {
3827
- return `xerg push
3828
-
3829
- Push a cached audit snapshot to the Xerg API.
3830
-
3831
- Usage:
3832
- xerg push [options]
3833
-
3834
- Options:
3835
- --file <path> Push a specific snapshot file instead of the most recent cached audit
3836
- --dry-run Print the payload to stdout without sending it
3837
-
3838
- -h, --help Show help
3839
-
3840
- Authentication:
3841
- Set XERG_API_KEY in your environment, add "apiKey" to ~/.xerg/config.json,
3842
- or run \`xerg login\` to authenticate via browser.
3843
- Browser login stores a token at ~/.config/xerg/credentials.json by default.
3844
- `;
3845
- }
3846
- function renderDoctorHelp() {
3847
- return `xerg doctor
3848
-
3849
- Inspect your machine for OpenClaw sources and audit readiness.
3850
-
3851
- Usage:
3852
- xerg doctor [options]
3853
-
3854
- Options:
3855
- --log-file <path> Explicit OpenClaw gateway log file to inspect
3856
- --sessions-dir <path> Explicit OpenClaw sessions directory to inspect
3857
- --verbose Print progress updates to stderr while doctor runs
3858
-
3859
- Remote options (SSH):
3860
- --remote <user@host> SSH target in user@host or user@host:port format
3861
- --remote-log-file <path> Override the default gateway log path on the remote host
3862
- --remote-sessions-dir <path> Override the default sessions directory on the remote host
3863
-
3864
- SSH checks require ssh and rsync on your PATH.
3865
-
3866
- Railway options:
3867
- --railway Check a Railway service (uses linked project by default)
3868
- --project <id> Railway project ID
3869
- --environment <id> Railway environment ID
3870
- --service <id> Railway service ID
3871
-
3872
- Railway checks require the railway CLI on your PATH.
3873
-
3874
- -h, --help Show help
3875
- `;
3876
- }
3877
5280
  function colorError(message) {
3878
5281
  return process.stderr.isTTY ? styleText2("red", message) : message;
3879
5282
  }
3880
5283
  function readVersion() {
3881
5284
  const packageJsonPath = new URL("../package.json", import.meta.url);
3882
- const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf8"));
5285
+ const packageJson = JSON.parse(readFileSync8(packageJsonPath, "utf8"));
3883
5286
  return packageJson.version ?? "0.0.0";
3884
5287
  }
3885
5288
  //# sourceMappingURL=index.js.map