@xerg/cli 0.1.10 → 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,7 +1,7 @@
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
7
  // src/command-display.ts
@@ -111,9 +111,12 @@ function normalizeSignal(value) {
111
111
  }
112
112
 
113
113
  // src/commands/audit.ts
114
- import { readFileSync as readFileSync5 } from "fs";
114
+ import { readFileSync as readFileSync6 } from "fs";
115
115
  import { rmSync as rmSync4 } from "fs";
116
- import { hostname } from "os";
116
+
117
+ // ../core/src/cursor/usage-csv.ts
118
+ import { readFileSync, statSync } from "fs";
119
+ import { resolve } from "path";
117
120
 
118
121
  // ../core/src/utils/hash.ts
119
122
  import { createHash } from "crypto";
@@ -177,6 +180,562 @@ function toIsoOrNow(value) {
177
180
  return isoNow();
178
181
  }
179
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
+
180
739
  // ../core/src/db/client.ts
181
740
  import { mkdirSync } from "fs";
182
741
  import { dirname } from "path";
@@ -536,7 +1095,7 @@ var FINDING_KIND_LABELS = {
536
1095
  "candidate-downgrade": "Downgrade candidates",
537
1096
  "idle-spend": "Idle waste"
538
1097
  };
539
- function round(value) {
1098
+ function round2(value) {
540
1099
  return Number(value.toFixed(6));
541
1100
  }
542
1101
  function normalizeSinceValue(since) {
@@ -600,7 +1159,7 @@ function buildTaxonomyBuckets(findings, classification) {
600
1159
  spendUsd: 0,
601
1160
  findingCount: 0
602
1161
  };
603
- current.spendUsd = round(current.spendUsd + finding.costImpactUsd);
1162
+ current.spendUsd = round2(current.spendUsd + finding.costImpactUsd);
604
1163
  current.findingCount += 1;
605
1164
  buckets.set(finding.kind, current);
606
1165
  }
@@ -618,9 +1177,9 @@ function buildTopSpendDeltas(currentRows, baselineRows) {
618
1177
  const currentSpendUsd = currentMap.get(key) ?? 0;
619
1178
  return {
620
1179
  key,
621
- baselineSpendUsd: round(baselineSpendUsd),
622
- currentSpendUsd: round(currentSpendUsd),
623
- deltaSpendUsd: round(currentSpendUsd - baselineSpendUsd)
1180
+ baselineSpendUsd: round2(baselineSpendUsd),
1181
+ currentSpendUsd: round2(currentSpendUsd),
1182
+ deltaSpendUsd: round2(currentSpendUsd - baselineSpendUsd)
624
1183
  };
625
1184
  }).filter((row) => row.deltaSpendUsd !== 0).sort((left, right) => Math.abs(right.deltaSpendUsd) - Math.abs(left.deltaSpendUsd)).slice(0, 3);
626
1185
  }
@@ -655,11 +1214,11 @@ function buildFindingChanges(currentFindings, baselineFindings) {
655
1214
  scope: current.scope,
656
1215
  scopeId: current.scopeId,
657
1216
  currentCostImpactUsd: current.costImpactUsd,
658
- deltaCostImpactUsd: round(current.costImpactUsd)
1217
+ deltaCostImpactUsd: round2(current.costImpactUsd)
659
1218
  });
660
1219
  continue;
661
1220
  }
662
- const deltaCostImpactUsd = round(current.costImpactUsd - baseline.costImpactUsd);
1221
+ const deltaCostImpactUsd = round2(current.costImpactUsd - baseline.costImpactUsd);
663
1222
  if (deltaCostImpactUsd > 0) {
664
1223
  worsenedHighConfidenceWaste.push({
665
1224
  kind: current.kind,
@@ -682,7 +1241,7 @@ function buildFindingChanges(currentFindings, baselineFindings) {
682
1241
  scope: baseline.scope,
683
1242
  scopeId: baseline.scopeId,
684
1243
  baselineCostImpactUsd: baseline.costImpactUsd,
685
- deltaCostImpactUsd: round(-baseline.costImpactUsd)
1244
+ deltaCostImpactUsd: round2(-baseline.costImpactUsd)
686
1245
  });
687
1246
  }
688
1247
  return {
@@ -701,7 +1260,11 @@ function hydrateAuditSummary(summary) {
701
1260
  comparison: summary.comparison ?? null,
702
1261
  wasteByKind: summary.wasteByKind?.length > 0 ? summary.wasteByKind : buildTaxonomyBuckets(summary.findings, "waste"),
703
1262
  opportunityByKind: summary.opportunityByKind?.length > 0 ? summary.opportunityByKind : buildTaxonomyBuckets(summary.findings, "opportunity"),
704
- 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
705
1268
  };
706
1269
  }
707
1270
  function buildAuditComparison(current, baseline) {
@@ -718,12 +1281,12 @@ function buildAuditComparison(current, baseline) {
718
1281
  baselineWasteSpendUsd: baseline.wasteSpendUsd,
719
1282
  baselineOpportunitySpendUsd: baseline.opportunitySpendUsd,
720
1283
  baselineStructuralWasteRate: baseline.structuralWasteRate,
721
- deltaTotalSpendUsd: round(current.totalSpendUsd - baseline.totalSpendUsd),
722
- deltaObservedSpendUsd: round(current.observedSpendUsd - baseline.observedSpendUsd),
723
- deltaEstimatedSpendUsd: round(current.estimatedSpendUsd - baseline.estimatedSpendUsd),
724
- deltaWasteSpendUsd: round(current.wasteSpendUsd - baseline.wasteSpendUsd),
725
- deltaOpportunitySpendUsd: round(current.opportunitySpendUsd - baseline.opportunitySpendUsd),
726
- 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),
727
1290
  deltaRunCount: current.runCount - baseline.runCount,
728
1291
  deltaCallCount: current.callCount - baseline.callCount,
729
1292
  workflowDeltas,
@@ -768,8 +1331,8 @@ function readLatestComparableAuditSummary(input) {
768
1331
  }
769
1332
 
770
1333
  // ../core/src/detect/openclaw.ts
771
- import { readdirSync, statSync } from "fs";
772
- 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";
773
1336
 
774
1337
  // ../core/src/utils/paths.ts
775
1338
  import { mkdirSync as mkdirSync2 } from "fs";
@@ -805,7 +1368,7 @@ function getDefaultGatewayPattern() {
805
1368
  // ../core/src/detect/openclaw.ts
806
1369
  function toDetected(path, kind) {
807
1370
  try {
808
- const stats = statSync(path);
1371
+ const stats = statSync2(path);
809
1372
  if (!stats.isFile()) {
810
1373
  return null;
811
1374
  }
@@ -853,12 +1416,12 @@ async function detectOpenClawSources(options) {
853
1416
  return detected.sort((left, right) => right.mtimeMs - left.mtimeMs);
854
1417
  }
855
1418
  async function collectGlobMatches(pattern, options) {
856
- 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();
857
1420
  const relativePattern = options?.cwd ? pattern : isAbsolute(pattern) ? pattern.slice(baseDir.length) : pattern;
858
1421
  const segments = relativePattern.split("/").filter(Boolean);
859
1422
  const matches = collectMatchesFromSegments(baseDir, segments);
860
1423
  return matches.map(
861
- (match) => options?.resolveWith ? resolve(options.resolveWith, match) : match
1424
+ (match) => options?.resolveWith ? resolve2(options.resolveWith, match) : match
862
1425
  );
863
1426
  }
864
1427
  function collectMatchesFromSegments(currentPath, segments) {
@@ -936,7 +1499,10 @@ async function inspectOpenClawSources(options) {
936
1499
  };
937
1500
  }
938
1501
 
939
- // ../core/src/findings/engine.ts
1502
+ // ../core/src/findings/cursor.ts
1503
+ function round3(value) {
1504
+ return Number(value.toFixed(6));
1505
+ }
940
1506
  function createFinding(input) {
941
1507
  return {
942
1508
  ...input,
@@ -945,11 +1511,125 @@ function createFinding(input) {
945
1511
  )
946
1512
  };
947
1513
  }
948
- 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) {
949
1628
  return Number(value.toFixed(6));
950
1629
  }
951
1630
  function buildFindings(runs) {
952
1631
  const findings = [];
1632
+ const wasteAttributions = [];
953
1633
  const allCalls = runs.flatMap((run2) => run2.calls.map((call) => ({ run: run2, call })));
954
1634
  const retryCandidates = allCalls.filter(({ call }) => {
955
1635
  const status = (call.status ?? "").toLowerCase();
@@ -957,8 +1637,15 @@ function buildFindings(runs) {
957
1637
  });
958
1638
  const retryCost = retryCandidates.reduce((sum, item) => sum + item.call.costUsd, 0);
959
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
+ );
960
1647
  findings.push(
961
- createFinding({
1648
+ createFinding2({
962
1649
  classification: "waste",
963
1650
  confidence: "high",
964
1651
  kind: "retry-waste",
@@ -966,7 +1653,7 @@ function buildFindings(runs) {
966
1653
  summary: `${retryCandidates.length} failed call${retryCandidates.length === 1 ? "" : "s"} were followed by additional work, making their spend pure retry overhead.`,
967
1654
  scope: "global",
968
1655
  scopeId: "all",
969
- costImpactUsd: round2(retryCost),
1656
+ costImpactUsd: round4(retryCost),
970
1657
  details: {
971
1658
  failedCallCount: retryCandidates.length
972
1659
  }
@@ -978,8 +1665,15 @@ function buildFindings(runs) {
978
1665
  if (maxIteration >= 7) {
979
1666
  const loopCalls = run2.calls.filter((call) => (call.iteration ?? 0) > 5);
980
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
+ );
981
1675
  findings.push(
982
- createFinding({
1676
+ createFinding2({
983
1677
  classification: "waste",
984
1678
  confidence: "high",
985
1679
  kind: "loop-waste",
@@ -987,7 +1681,7 @@ function buildFindings(runs) {
987
1681
  summary: `This run reached ${maxIteration} iterations. Xerg treats the spend after iteration 5 as likely loop waste.`,
988
1682
  scope: "run",
989
1683
  scopeId: run2.id,
990
- costImpactUsd: round2(loopCost),
1684
+ costImpactUsd: round4(loopCost),
991
1685
  details: {
992
1686
  workflow: run2.workflow,
993
1687
  maxIteration
@@ -1015,7 +1709,7 @@ function buildFindings(runs) {
1015
1709
  if (outlierRuns.length > 0) {
1016
1710
  const outlierCost = outlierRuns.reduce((sum, run2) => sum + run2.totalCostUsd, 0);
1017
1711
  findings.push(
1018
- createFinding({
1712
+ createFinding2({
1019
1713
  classification: "opportunity",
1020
1714
  confidence: "medium",
1021
1715
  kind: "context-outlier",
@@ -1023,10 +1717,10 @@ function buildFindings(runs) {
1023
1717
  summary: `Xerg found ${outlierRuns.length} run${outlierRuns.length === 1 ? "" : "s"} in this workflow with input token volume far above the workflow average.`,
1024
1718
  scope: "workflow",
1025
1719
  scopeId: workflow,
1026
- costImpactUsd: round2(outlierCost),
1720
+ costImpactUsd: round4(outlierCost),
1027
1721
  details: {
1028
1722
  workflow,
1029
- averageInputTokens: round2(average),
1723
+ averageInputTokens: round4(average),
1030
1724
  outlierRunCount: outlierRuns.length
1031
1725
  }
1032
1726
  })
@@ -1039,7 +1733,7 @@ function buildFindings(runs) {
1039
1733
  if (idleRuns.length > 0) {
1040
1734
  const idleCost = idleRuns.reduce((sum, run2) => sum + run2.totalCostUsd, 0);
1041
1735
  findings.push(
1042
- createFinding({
1736
+ createFinding2({
1043
1737
  classification: "opportunity",
1044
1738
  confidence: "medium",
1045
1739
  kind: "idle-spend",
@@ -1047,7 +1741,7 @@ function buildFindings(runs) {
1047
1741
  summary: "This workflow name looks like a recurring heartbeat or monitoring loop. Review whether the cadence and model tier are justified.",
1048
1742
  scope: "workflow",
1049
1743
  scopeId: workflow,
1050
- costImpactUsd: round2(idleCost),
1744
+ costImpactUsd: round4(idleCost),
1051
1745
  details: {
1052
1746
  workflow
1053
1747
  }
@@ -1060,7 +1754,7 @@ function buildFindings(runs) {
1060
1754
  if (downgradeCalls.length > 0) {
1061
1755
  const spend = downgradeCalls.reduce((sum, call) => sum + call.costUsd, 0);
1062
1756
  findings.push(
1063
- createFinding({
1757
+ createFinding2({
1064
1758
  classification: "opportunity",
1065
1759
  confidence: "low",
1066
1760
  kind: "candidate-downgrade",
@@ -1068,21 +1762,24 @@ function buildFindings(runs) {
1068
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.",
1069
1763
  scope: "workflow",
1070
1764
  scopeId: workflow,
1071
- costImpactUsd: round2(spend * 0.3),
1765
+ costImpactUsd: round4(spend * 0.3),
1072
1766
  details: {
1073
1767
  workflow,
1074
1768
  expensiveCallCount: downgradeCalls.length,
1075
- inspectedSpendUsd: round2(spend)
1769
+ inspectedSpendUsd: round4(spend)
1076
1770
  }
1077
1771
  })
1078
1772
  );
1079
1773
  }
1080
1774
  }
1081
- return findings.sort((left, right) => right.costImpactUsd - left.costImpactUsd);
1775
+ return {
1776
+ findings: findings.sort((left, right) => right.costImpactUsd - left.costImpactUsd),
1777
+ wasteAttributions
1778
+ };
1082
1779
  }
1083
1780
 
1084
1781
  // ../core/src/normalize/openclaw.ts
1085
- import { readFileSync } from "fs";
1782
+ import { readFileSync as readFileSync2 } from "fs";
1086
1783
  import { basename } from "path";
1087
1784
 
1088
1785
  // ../core/src/pricing-catalog.ts
@@ -1182,7 +1879,7 @@ function getNestedValue(input, paths) {
1182
1879
  }
1183
1880
  return null;
1184
1881
  }
1185
- function asNumber(value) {
1882
+ function asNumber2(value) {
1186
1883
  if (typeof value === "number" && Number.isFinite(value)) {
1187
1884
  return value;
1188
1885
  }
@@ -1198,7 +1895,7 @@ function asString(value) {
1198
1895
  }
1199
1896
  return null;
1200
1897
  }
1201
- function asBoolean(value) {
1898
+ function asBoolean2(value) {
1202
1899
  if (typeof value === "boolean") {
1203
1900
  return value;
1204
1901
  }
@@ -1223,7 +1920,7 @@ function pickMetadata(input, keys) {
1223
1920
 
1224
1921
  // ../core/src/normalize/openclaw.ts
1225
1922
  function parseJsonLines(path) {
1226
- const content = readFileSync(path, "utf8");
1923
+ const content = readFileSync2(path, "utf8");
1227
1924
  const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1228
1925
  const records = [];
1229
1926
  for (const line of lines) {
@@ -1235,7 +1932,7 @@ function parseJsonLines(path) {
1235
1932
  }
1236
1933
  return records;
1237
1934
  }
1238
- function inferProvider(record) {
1935
+ function inferProvider2(record) {
1239
1936
  return asString(
1240
1937
  getNestedValue(record, [["provider"], ["message", "provider"], ["usage", "provider"]])
1241
1938
  ) ?? "unknown";
@@ -1274,7 +1971,7 @@ function inferTaskClass(record, workflow) {
1274
1971
  return asString(getNestedValue(record, [["task_class"], ["taskClass"], ["metadata", "taskClass"]])) ?? workflow.toLowerCase();
1275
1972
  }
1276
1973
  function extractUsage(record) {
1277
- const inputTokens = asNumber(
1974
+ const inputTokens = asNumber2(
1278
1975
  getNestedValue(record, [
1279
1976
  ["input_tokens"],
1280
1977
  ["inputTokens"],
@@ -1286,7 +1983,7 @@ function extractUsage(record) {
1286
1983
  ["message", "usage", "prompt_tokens"]
1287
1984
  ])
1288
1985
  ) ?? 0;
1289
- const outputTokens = asNumber(
1986
+ const outputTokens = asNumber2(
1290
1987
  getNestedValue(record, [
1291
1988
  ["output_tokens"],
1292
1989
  ["outputTokens"],
@@ -1298,7 +1995,7 @@ function extractUsage(record) {
1298
1995
  ["message", "usage", "completion_tokens"]
1299
1996
  ])
1300
1997
  ) ?? 0;
1301
- const observedCost = asNumber(
1998
+ const observedCost = asNumber2(
1302
1999
  getNestedValue(record, [
1303
2000
  ["cost_usd"],
1304
2001
  ["costUsd"],
@@ -1316,8 +2013,8 @@ function extractUsage(record) {
1316
2013
  observedCost
1317
2014
  };
1318
2015
  }
1319
- function buildCall(source, record, runId, index) {
1320
- const provider = inferProvider(record);
2016
+ function buildCall2(source, record, runId, index) {
2017
+ const provider = inferProvider2(record);
1321
2018
  const model = inferModel(record);
1322
2019
  const workflow = inferWorkflow(record, source.path);
1323
2020
  const { inputTokens, outputTokens, observedCost } = extractUsage(record);
@@ -1325,13 +2022,13 @@ function buildCall(source, record, runId, index) {
1325
2022
  const timestamp = toIsoOrNow(
1326
2023
  getNestedValue(record, [["timestamp"], ["createdAt"], ["created_at"]])
1327
2024
  );
1328
- const attempt = asNumber(
2025
+ const attempt = asNumber2(
1329
2026
  getNestedValue(record, [["attempt"], ["usage", "attempt"], ["metadata", "attempt"]])
1330
2027
  ) ?? null;
1331
- const iteration = asNumber(
2028
+ const iteration = asNumber2(
1332
2029
  getNestedValue(record, [["iteration"], ["loop_iteration"], ["metadata", "iteration"]])
1333
2030
  ) ?? null;
1334
- const retries = asNumber(getNestedValue(record, [["retries"], ["retry_count"], ["metadata", "retries"]])) ?? 0;
2031
+ const retries = asNumber2(getNestedValue(record, [["retries"], ["retry_count"], ["metadata", "retries"]])) ?? 0;
1335
2032
  const costUsd = observedCost ?? estimatedCost ?? 0;
1336
2033
  return {
1337
2034
  id: sha1(`${runId}:${source.path}:${index}:${model}:${timestamp}:${costUsd}`),
@@ -1343,17 +2040,17 @@ function buildCall(source, record, runId, index) {
1343
2040
  outputTokens,
1344
2041
  costUsd,
1345
2042
  costSource: observedCost !== null ? "observed" : "estimated",
1346
- latencyMs: asNumber(getNestedValue(record, [["latency_ms"], ["latencyMs"], ["usage", "latency_ms"]])) ?? null,
1347
- 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,
1348
2045
  retries,
1349
2046
  attempt,
1350
2047
  iteration,
1351
2048
  status: asString(getNestedValue(record, [["status"], ["level"], ["result"], ["error", "type"]])) ?? null,
1352
2049
  taskClass: inferTaskClass(record, workflow),
1353
- cacheHit: asBoolean(
2050
+ cacheHit: asBoolean2(
1354
2051
  getNestedValue(record, [["cache_hit"], ["cacheHit"], ["usage", "cache_hit"]])
1355
2052
  ),
1356
- cacheCostUsd: asNumber(
2053
+ cacheCostUsd: asNumber2(
1357
2054
  getNestedValue(record, [["cache_cost_usd"], ["cacheCostUsd"], ["usage", "cache_cost_usd"]])
1358
2055
  ) ?? null,
1359
2056
  metadata: pickMetadata(record, ["event", "type", "sessionId", "agentId"])
@@ -1381,7 +2078,7 @@ function normalizeOpenClawSources(sources, since) {
1381
2078
  }
1382
2079
  const runKey = inferRunKey(record, workflow, index, source.path);
1383
2080
  const runId = sha1(`${source.path}:${runKey}`);
1384
- const call = buildCall(source, record, runId, index);
2081
+ const call = buildCall2(source, record, runId, index);
1385
2082
  const existing = runsById.get(runId);
1386
2083
  if (!existing) {
1387
2084
  runsById.set(runId, {
@@ -1414,6 +2111,129 @@ function normalizeOpenClawSources(sources, since) {
1414
2111
  });
1415
2112
  }
1416
2113
 
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;
2122
+ }
2123
+ return candidate.toISOString().slice(0, 10);
2124
+ }
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
+
1417
2237
  // ../core/src/report/summary.ts
1418
2238
  function buildBreakdown(items) {
1419
2239
  const buckets = /* @__PURE__ */ new Map();
@@ -1442,6 +2262,8 @@ function buildAuditSummary(input) {
1442
2262
  const wasteSpendUsd = input.findings.filter((finding) => finding.classification === "waste").reduce((sum, finding) => sum + finding.costImpactUsd, 0);
1443
2263
  const opportunitySpendUsd = input.findings.filter((finding) => finding.classification === "opportunity").reduce((sum, finding) => sum + finding.costImpactUsd, 0);
1444
2264
  const generatedAt = isoNow();
2265
+ const spendByDay = buildSpendByDay(input.runs);
2266
+ const observedDays = buildObservedUtcDayRange(input.runs);
1445
2267
  return {
1446
2268
  auditId: sha1(
1447
2269
  `${generatedAt}:${input.runs.length}:${input.sources.map((source) => source.path).join("|")}`
@@ -1481,6 +2303,8 @@ function buildAuditSummary(input) {
1481
2303
  }))
1482
2304
  )
1483
2305
  ),
2306
+ spendByDay,
2307
+ wasteByDay: buildWasteByDay(input.wasteAttributions, observedDays, wasteSpendUsd),
1484
2308
  findings: input.findings,
1485
2309
  notes: [
1486
2310
  "Cost per outcome is intentionally unavailable in v0. Xerg is measuring waste intelligence only.",
@@ -1495,66 +2319,147 @@ function buildAuditSummary(input) {
1495
2319
  async function doctorOpenClaw(options) {
1496
2320
  return inspectOpenClawSources(options);
1497
2321
  }
1498
- async function auditOpenClaw(options) {
1499
- options.onProgress?.("Scanning for OpenClaw source files...");
2322
+ async function doctorCursorUsageCsv(options) {
2323
+ return inspectCursorUsageCsv(options);
2324
+ }
2325
+ function validateCompareOptions(options) {
1500
2326
  if (options.compare && options.noDb) {
1501
2327
  throw new Error(
1502
2328
  "The --compare flag needs local snapshot history. Remove --no-db or provide --db <path>."
1503
2329
  );
1504
2330
  }
1505
- const sources = await detectOpenClawSources(options);
1506
- if (sources.length === 0) {
1507
- options.onProgress?.("No OpenClaw source files were detected.");
1508
- throw new Error(
1509
- `No OpenClaw sources were detected. Run \`${options.commandPrefix ?? "xerg"} doctor\` or provide --log-file / --sessions-dir.`
1510
- );
2331
+ }
2332
+ function maybeAttachComparison(options, dbPath, summary) {
2333
+ if (!options.compare || !dbPath) {
2334
+ return;
1511
2335
  }
1512
- options.onProgress?.(`Detected ${sources.length} source file${sources.length === 1 ? "" : "s"}.`);
1513
- options.onProgress?.("Normalizing OpenClaw source files...");
1514
- const runs = normalizeOpenClawSources(sources, options.since);
1515
- options.onProgress?.(`Normalized ${runs.length} run${runs.length === 1 ? "" : "s"}.`);
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);
2382
+ const sources = await detectOpenClawSources(options);
2383
+ if (sources.length === 0) {
2384
+ options.onProgress?.("No OpenClaw source files were detected.");
2385
+ throw new Error(
2386
+ `No OpenClaw sources were detected. Run \`${options.commandPrefix ?? "xerg"} doctor\` or provide --log-file / --sessions-dir.`
2387
+ );
2388
+ }
2389
+ options.onProgress?.(`Detected ${sources.length} source file${sources.length === 1 ? "" : "s"}.`);
2390
+ options.onProgress?.("Normalizing OpenClaw source files...");
2391
+ const runs = normalizeOpenClawSources(sources, options.since);
2392
+ options.onProgress?.(`Normalized ${runs.length} run${runs.length === 1 ? "" : "s"}.`);
1516
2393
  options.onProgress?.("Computing waste and savings findings...");
1517
- const findings = buildFindings(runs);
2394
+ const { findings, wasteAttributions } = buildFindings(runs);
1518
2395
  const dbPath = options.noDb ? void 0 : options.dbPath ?? getDefaultDbPath();
1519
2396
  options.onProgress?.("Building audit summary...");
1520
2397
  const summary = buildAuditSummary({
1521
2398
  runs,
1522
2399
  findings,
2400
+ wasteAttributions,
1523
2401
  sources,
1524
2402
  since: options.since,
1525
2403
  dbPath,
1526
2404
  comparisonKeyOverride: options.comparisonKeyOverride
1527
2405
  });
1528
- if (options.compare && dbPath) {
1529
- options.onProgress?.("Looking for a comparable baseline audit...");
1530
- const baseline = readLatestComparableAuditSummary({
1531
- dbPath,
1532
- comparisonKey: summary.comparisonKey,
1533
- currentAuditId: summary.auditId
1534
- });
1535
- if (baseline) {
1536
- summary.comparison = buildAuditComparison(summary, baseline);
1537
- } else {
1538
- summary.notes = [
1539
- ...summary.notes,
1540
- "No prior comparable audit was found. Run the same audit again after a fix to unlock before/after deltas."
1541
- ];
1542
- }
1543
- }
1544
- if (dbPath) {
1545
- options.onProgress?.(`Persisting local snapshot to ${dbPath}...`);
1546
- persistAudit(
1547
- {
1548
- summary,
1549
- runs,
1550
- pricingCatalog: PRICING_CATALOG
1551
- },
1552
- dbPath
1553
- );
1554
- options.onProgress?.("Local snapshot stored.");
1555
- } else {
1556
- options.onProgress?.("Skipping local snapshot persistence (--no-db).");
1557
- }
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);
1558
2463
  return summary;
1559
2464
  }
1560
2465
 
@@ -1605,6 +2510,26 @@ var templatesByKind = {
1605
2510
  suggestedChangeFn: () => ({
1606
2511
  strategy: "cadence-review"
1607
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
+ })
1608
2533
  }
1609
2534
  };
1610
2535
  function extractWorkflow(finding) {
@@ -1658,10 +2583,16 @@ function formatPercentDelta(value) {
1658
2583
  const sign = points > 0 ? "+" : "";
1659
2584
  return `${sign}${points.toFixed(0)} pts`;
1660
2585
  }
2586
+ function formatCount(value) {
2587
+ return new Intl.NumberFormat("en-US").format(value);
2588
+ }
1661
2589
  function formatUsdDelta(value) {
1662
2590
  const sign = value > 0 ? "+" : "";
1663
2591
  return `${sign}${formatUsd(value)}`;
1664
2592
  }
2593
+ function isCursorUsageSummary(summary) {
2594
+ return summary.sourceFiles.some((source) => source.kind === "cursor-usage-csv");
2595
+ }
1665
2596
  function topRows(rows, limit = 5) {
1666
2597
  return rows.slice(0, limit).map((row) => {
1667
2598
  return `- ${row.key}: ${formatUsd(row.spendUsd)} (${formatPercent(row.observedShare)} observed)`;
@@ -1763,6 +2694,19 @@ function renderCompareBlock(summary) {
1763
2694
  ...findingChanges.length > 0 ? findingChanges : ["- High-confidence waste changes: none"]
1764
2695
  ];
1765
2696
  }
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
+ }
1766
2710
  function renderDoctorReport(report, options) {
1767
2711
  const commandPrefix = options?.commandPrefix ?? "xerg";
1768
2712
  const nextSteps = report.canAudit ? [] : [
@@ -1791,7 +2735,177 @@ function renderDoctorReport(report, options) {
1791
2735
  ];
1792
2736
  return sections.join("\n");
1793
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
+ }
1794
2905
  function renderTerminalSummary(summary) {
2906
+ if (isCursorUsageSummary(summary)) {
2907
+ return renderCursorTerminalSummary(summary);
2908
+ }
1795
2909
  const wasteFindings = summary.findings.filter((finding) => finding.classification === "waste");
1796
2910
  const opportunityFindings = summary.findings.filter(
1797
2911
  (finding) => finding.classification === "opportunity"
@@ -1817,6 +2931,8 @@ function renderTerminalSummary(summary) {
1817
2931
  "## Top models",
1818
2932
  ...topRows(summary.spendByModel),
1819
2933
  "",
2934
+ ...renderDailyTrendRows(summary.spendByDay, summary.wasteByDay),
2935
+ ...summary.spendByDay.length > 1 ? [""] : [],
1820
2936
  "## High-confidence waste",
1821
2937
  ...renderFindingList(wasteFindings, "none detected"),
1822
2938
  "",
@@ -1838,6 +2954,9 @@ function renderTerminalSummary(summary) {
1838
2954
  ].join("\n");
1839
2955
  }
1840
2956
  function renderMarkdownSummary(summary) {
2957
+ if (isCursorUsageSummary(summary)) {
2958
+ return renderCursorMarkdownSummary(summary);
2959
+ }
1841
2960
  const lines = [
1842
2961
  "# Xerg Audit Report",
1843
2962
  "",
@@ -1855,6 +2974,9 @@ function renderMarkdownSummary(summary) {
1855
2974
  "## Top workflows",
1856
2975
  ...topRows(summary.spendByWorkflow),
1857
2976
  "",
2977
+ ...renderDailyTrendRows(summary.spendByDay, summary.wasteByDay),
2978
+ ...summary.spendByDay.length > 1 ? [""] : [],
2979
+ "",
1858
2980
  "## Findings",
1859
2981
  ...summary.findings.slice(0, 10).map((finding) => {
1860
2982
  return `- **${finding.title}** (${finding.classification}, ${finding.confidence}) \u2014 ${finding.summary} Estimated impact: ${formatUsd(finding.costImpactUsd)}.`;
@@ -1924,6 +3046,8 @@ function toWirePayload(summary, meta) {
1924
3046
  opportunityByKind: summary.opportunityByKind,
1925
3047
  spendByWorkflow: summary.spendByWorkflow,
1926
3048
  spendByModel: summary.spendByModel,
3049
+ spendByDay: summary.spendByDay,
3050
+ wasteByDay: summary.wasteByDay,
1927
3051
  findings: summary.findings.map(toWireFinding),
1928
3052
  notes: summary.notes,
1929
3053
  comparison: summary.comparison ? toWireComparison(summary.comparison) : null
@@ -1996,12 +3120,12 @@ async function pushAudit(payload, config) {
1996
3120
  }
1997
3121
 
1998
3122
  // src/push/config.ts
1999
- import { readFileSync as readFileSync3 } from "fs";
3123
+ import { readFileSync as readFileSync4 } from "fs";
2000
3124
  import { homedir as homedir3 } from "os";
2001
3125
  import { join as join4 } from "path";
2002
3126
 
2003
3127
  // src/auth/credentials.ts
2004
- 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";
2005
3129
  import { homedir as homedir2 } from "os";
2006
3130
  import { dirname as dirname2, join as join3 } from "path";
2007
3131
  function getCredentialsPath() {
@@ -2019,7 +3143,7 @@ function loadStoredCredentials() {
2019
3143
  const credPath = getCredentialsPath();
2020
3144
  try {
2021
3145
  if (!existsSync(credPath)) return null;
2022
- const raw = readFileSync2(credPath, "utf8");
3146
+ const raw = readFileSync3(credPath, "utf8");
2023
3147
  const parsed = JSON.parse(raw);
2024
3148
  return parsed.token || null;
2025
3149
  } catch {
@@ -2050,7 +3174,7 @@ function loadPushConfig() {
2050
3174
  };
2051
3175
  }
2052
3176
  try {
2053
- const raw = readFileSync3(CONFIG_PATH, "utf8");
3177
+ const raw = readFileSync4(CONFIG_PATH, "utf8");
2054
3178
  const parsed = JSON.parse(raw);
2055
3179
  if (parsed.apiKey) {
2056
3180
  return {
@@ -2073,6 +3197,9 @@ Get your key at https://xerg.ai/dashboard/settings`
2073
3197
  );
2074
3198
  }
2075
3199
 
3200
+ // src/source-meta.ts
3201
+ import { hostname } from "os";
3202
+
2076
3203
  // src/transport/ssh.ts
2077
3204
  import { execSync, spawnSync } from "child_process";
2078
3205
  import { createHash as createHash2 } from "crypto";
@@ -2864,13 +3991,13 @@ function formatBytes2(bytes) {
2864
3991
  }
2865
3992
 
2866
3993
  // src/transport/config.ts
2867
- import { readFileSync as readFileSync4 } from "fs";
2868
- import { resolve as resolve2 } from "path";
3994
+ import { readFileSync as readFileSync5 } from "fs";
3995
+ import { resolve as resolve3 } from "path";
2869
3996
  function loadRemoteConfig(configPath) {
2870
- const resolved = resolve2(configPath);
3997
+ const resolved = resolve3(configPath);
2871
3998
  let raw;
2872
3999
  try {
2873
- raw = readFileSync4(resolved, "utf8");
4000
+ raw = readFileSync5(resolved, "utf8");
2874
4001
  } catch {
2875
4002
  throw new Error(`Cannot read remote config at ${resolved}`);
2876
4003
  }
@@ -2941,6 +4068,74 @@ function validateRailwayEntry(entry) {
2941
4068
  };
2942
4069
  }
2943
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
+
2944
4139
  // src/commands/audit.ts
2945
4140
  var NO_DATA_PATTERN = /no openclaw sources were detected/i;
2946
4141
  async function auditOrNoData(...args) {
@@ -2958,6 +4153,7 @@ async function runAuditCommand(options) {
2958
4153
  if (options.dryRun && !options.push) {
2959
4154
  throw new Error("--dry-run requires --push.");
2960
4155
  }
4156
+ validateCursorUsageCsvOptions(options);
2961
4157
  const remoteFlags = [options.remote, options.remoteConfig, options.railway].filter(
2962
4158
  Boolean
2963
4159
  ).length;
@@ -3002,6 +4198,26 @@ function buildRailwayTarget(options) {
3002
4198
  return void 0;
3003
4199
  }
3004
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
+ }
3005
4221
  logger.verbose("Running a local audit.");
3006
4222
  if (options.logFile) {
3007
4223
  logger.verbose(`Using explicit local log file: ${options.logFile}`);
@@ -3021,11 +4237,32 @@ async function runLocalAudit(options, logger) {
3021
4237
  });
3022
4238
  renderOutput(summary, options);
3023
4239
  if (options.push) {
3024
- const meta = buildMeta({ environment: "local", sourceId: hostname(), sourceHost: hostname() });
4240
+ const meta = buildMeta(buildLocalPushSourceMeta("openclaw"));
3025
4241
  await handlePush(summary, meta, options);
3026
4242
  }
3027
4243
  checkThresholds(summary, options);
3028
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
+ }
3029
4266
  function getComparisonKey(source) {
3030
4267
  if (source.transport === "railway") {
3031
4268
  return buildComparisonKeyForRailway(source);
@@ -3044,9 +4281,6 @@ function describeSource(source) {
3044
4281
  }
3045
4282
  return source.host;
3046
4283
  }
3047
- function sourceEnvironment(source) {
3048
- return source.transport === "railway" ? "railway" : "remote";
3049
- }
3050
4284
  async function runSingleRemoteAudit(source, options, logger) {
3051
4285
  logger.info(`Pulling files from ${describeSource(source)}...`);
3052
4286
  const pullResult = await pullFiles(
@@ -3071,11 +4305,7 @@ async function runSingleRemoteAudit(source, options, logger) {
3071
4305
  });
3072
4306
  renderOutput(summary, options);
3073
4307
  if (options.push) {
3074
- const meta = buildMeta({
3075
- environment: sourceEnvironment(source),
3076
- sourceId: source.name,
3077
- sourceHost: source.host
3078
- });
4308
+ const meta = buildMeta(buildRemotePushSourceMeta(source));
3079
4309
  await handlePush(summary, meta, options);
3080
4310
  }
3081
4311
  checkThresholds(summary, options);
@@ -3163,11 +4393,7 @@ ${"\u2550".repeat(60)}
3163
4393
  }
3164
4394
  if (options.push) {
3165
4395
  for (const { source, summary } of summaries) {
3166
- const meta = buildMeta({
3167
- environment: sourceEnvironment(source),
3168
- sourceId: source.name,
3169
- sourceHost: source.host
3170
- });
4396
+ const meta = buildMeta(buildRemotePushSourceMeta(source));
3171
4397
  await handlePush(summary, meta, options);
3172
4398
  }
3173
4399
  }
@@ -3183,7 +4409,7 @@ ${"\u2550".repeat(60)}
3183
4409
  function readCliVersion() {
3184
4410
  try {
3185
4411
  const packageJsonPath = new URL("../../package.json", import.meta.url);
3186
- const pkg = JSON.parse(readFileSync5(packageJsonPath, "utf8"));
4412
+ const pkg = JSON.parse(readFileSync6(packageJsonPath, "utf8"));
3187
4413
  return pkg.version ?? "0.0.0";
3188
4414
  } catch {
3189
4415
  return "0.0.0";
@@ -3266,6 +4492,7 @@ function cleanupPullResult(pullResult, keepFiles) {
3266
4492
  // src/commands/doctor.ts
3267
4493
  async function runDoctorCommand(options) {
3268
4494
  const logger = createCliLogger({ verbose: options.verbose });
4495
+ validateCursorUsageCsvOptions2(options);
3269
4496
  if (options.railway) {
3270
4497
  logger.verbose("Inspecting Railway audit readiness.");
3271
4498
  const railwayTarget = buildRailwayTarget2(options);
@@ -3288,6 +4515,17 @@ async function runDoctorCommand(options) {
3288
4515
  });
3289
4516
  const report2 = await runRemoteDoctor({ source, onProgress: logger.verbose });
3290
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)}
3291
4529
  `);
3292
4530
  return;
3293
4531
  }
@@ -3306,6 +4544,25 @@ async function runDoctorCommand(options) {
3306
4544
  process.stdout.write(`${renderDoctorReport(report, { commandPrefix: options.commandPrefix })}
3307
4545
  `);
3308
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
+ }
3309
4566
  function buildRailwayTarget2(options) {
3310
4567
  if (options.railwayProject && options.railwayEnvironment && options.railwayService) {
3311
4568
  return {
@@ -3515,12 +4772,12 @@ async function openBrowser(url) {
3515
4772
  };
3516
4773
  const cmd = commands[platform2()];
3517
4774
  if (!cmd) return;
3518
- return new Promise((resolve3) => {
3519
- exec(`${cmd} ${JSON.stringify(url)}`, () => resolve3());
4775
+ return new Promise((resolve4) => {
4776
+ exec(`${cmd} ${JSON.stringify(url)}`, () => resolve4());
3520
4777
  });
3521
4778
  }
3522
4779
  function sleep(ms) {
3523
- return new Promise((resolve3) => setTimeout(resolve3, ms));
4780
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
3524
4781
  }
3525
4782
  function colorBold(text) {
3526
4783
  return process.stderr.isTTY ? styleText("bold", text) : text;
@@ -3541,8 +4798,7 @@ function runLogoutCommand() {
3541
4798
  }
3542
4799
 
3543
4800
  // src/commands/push.ts
3544
- import { readFileSync as readFileSync6 } from "fs";
3545
- import { hostname as hostname2 } from "os";
4801
+ import { readFileSync as readFileSync7 } from "fs";
3546
4802
  async function runPushCommand(options) {
3547
4803
  const payload = options.file ? loadPayloadFromFile(options.file) : loadPayloadFromCache();
3548
4804
  if (options.dryRun) {
@@ -3566,7 +4822,7 @@ async function runPushCommand(options) {
3566
4822
  function loadPayloadFromFile(filePath) {
3567
4823
  let raw;
3568
4824
  try {
3569
- raw = readFileSync6(filePath, "utf8");
4825
+ raw = readFileSync7(filePath, "utf8");
3570
4826
  } catch {
3571
4827
  throw new Error(`Cannot read file: ${filePath}`);
3572
4828
  }
@@ -3600,7 +4856,7 @@ function loadPayloadFromCache() {
3600
4856
  );
3601
4857
  }
3602
4858
  const latest = summaries[0];
3603
- const meta = buildMeta2();
4859
+ const meta = buildMeta2(latest);
3604
4860
  process.stderr.write(
3605
4861
  `Using most recent cached audit: ${latest.auditId} (${latest.generatedAt})
3606
4862
  `
@@ -3610,28 +4866,152 @@ function loadPayloadFromCache() {
3610
4866
  function readCliVersion2() {
3611
4867
  try {
3612
4868
  const packageJsonPath = new URL("../../package.json", import.meta.url);
3613
- const pkg = JSON.parse(readFileSync6(packageJsonPath, "utf8"));
4869
+ const pkg = JSON.parse(readFileSync7(packageJsonPath, "utf8"));
3614
4870
  return pkg.version ?? "0.0.0";
3615
4871
  } catch {
3616
4872
  return "0.0.0";
3617
4873
  }
3618
4874
  }
3619
- function buildMeta2() {
4875
+ function buildMeta2(summary) {
4876
+ const sourceMeta = buildCachedPushSourceMeta(summary);
3620
4877
  return {
3621
4878
  cliVersion: readCliVersion2(),
3622
- sourceId: hostname2(),
3623
- sourceHost: hostname2(),
3624
- environment: "local"
4879
+ sourceId: sourceMeta.sourceId,
4880
+ sourceHost: sourceMeta.sourceHost,
4881
+ environment: sourceMeta.environment
3625
4882
  };
3626
4883
  }
3627
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
+
3628
5008
  // src/index.ts
3629
5009
  var VERSION = readVersion();
3630
5010
  var argv = process.argv.slice(2);
3631
5011
  var commandDisplay = resolveCommandDisplay();
3632
5012
  var command = argv[0];
3633
5013
  if (!command || command === "--help" || command === "-h" || command === "help") {
3634
- process.stdout.write(renderRootHelp(commandDisplay));
5014
+ process.stdout.write(renderRootHelp(VERSION, commandDisplay));
3635
5015
  process.exit(0);
3636
5016
  }
3637
5017
  if (command === "--version" || command === "-v" || command === "version") {
@@ -3701,6 +5081,10 @@ function parseAuditOptions(raw) {
3701
5081
  options.sessionsDir = readValue(arg, argv2[index + 1]);
3702
5082
  index += 1;
3703
5083
  break;
5084
+ case "--cursor-usage-csv":
5085
+ options.cursorUsageCsv = readValue(arg, argv2[index + 1]);
5086
+ index += 1;
5087
+ break;
3704
5088
  case "--since":
3705
5089
  options.since = readValue(arg, argv2[index + 1]);
3706
5090
  index += 1;
@@ -3825,6 +5209,10 @@ function parseDoctorOptions(raw) {
3825
5209
  options.sessionsDir = readValue(arg, argv2[index + 1]);
3826
5210
  index += 1;
3827
5211
  break;
5212
+ case "--cursor-usage-csv":
5213
+ options.cursorUsageCsv = readValue(arg, argv2[index + 1]);
5214
+ index += 1;
5215
+ break;
3828
5216
  case "--remote":
3829
5217
  options.remote = readValue(arg, argv2[index + 1]);
3830
5218
  index += 1;
@@ -3889,131 +5277,12 @@ function readFloat(flag, value) {
3889
5277
  }
3890
5278
  return num;
3891
5279
  }
3892
- function renderRootHelp(display = commandDisplay) {
3893
- return `${display.name} ${VERSION}
3894
-
3895
- Waste intelligence for OpenClaw workflows.
3896
-
3897
- Usage:
3898
- ${formatCommand("<command> [options]", display.prefix)}
3899
-
3900
- Commands:
3901
- audit Analyze OpenClaw logs and produce a waste intelligence report.
3902
- doctor Inspect your machine for OpenClaw sources and audit readiness.
3903
- push Push a cached audit snapshot to the Xerg API.
3904
- login Authenticate with the Xerg API via browser.
3905
- logout Remove stored Xerg API credentials.
3906
-
3907
- Global options:
3908
- -h, --help Show help
3909
- -v, --version Show version
3910
- `;
3911
- }
3912
- function renderAuditHelp(commandPrefix = commandDisplay.prefix) {
3913
- return `${formatCommand("audit", commandPrefix)}
3914
-
3915
- Analyze OpenClaw logs and produce a waste intelligence report.
3916
-
3917
- Usage:
3918
- ${formatCommand("audit [options]", commandPrefix)}
3919
-
3920
- Options:
3921
- --log-file <path> Explicit OpenClaw gateway log file to analyze
3922
- --sessions-dir <path> Explicit OpenClaw sessions directory to analyze
3923
- --since <duration> Look back window such as 24h, 7d, or 30m
3924
- --compare Compare this audit to the newest compatible prior local snapshot
3925
- --json Render the report as JSON
3926
- --markdown Render the report as Markdown
3927
- --db <path> Custom SQLite database path
3928
- --no-db Skip local persistence
3929
-
3930
- Remote options (SSH):
3931
- --remote <user@host> SSH target in user@host or user@host:port format
3932
- --remote-log-file <path> Override the default gateway log path on the remote host
3933
- --remote-sessions-dir <path> Override the default sessions directory on the remote host
3934
- --remote-config <path> Path to a JSON file defining multiple remote sources
3935
- --keep-remote-files Retain pulled files in ~/.xerg/remote-cache/ instead of using a temp directory
3936
-
3937
- Prerequisites:
3938
- SSH remote audits require ssh and rsync on your PATH.
3939
-
3940
- Railway options:
3941
- --railway Audit a Railway service (uses linked project by default)
3942
- --project <id> Railway project ID
3943
- --environment <id> Railway environment ID
3944
- --service <id> Railway service ID
3945
-
3946
- Railway audits require the railway CLI on your PATH.
3947
-
3948
- Push options:
3949
- --push Push the audit summary to the Xerg API after computing it
3950
- --dry-run With --push: print the payload to stdout without sending it
3951
- --verbose Print progress updates to stderr while the audit runs
3952
-
3953
- Threshold options:
3954
- --fail-above-waste-rate <n> Exit with code 3 if structural waste rate exceeds threshold (e.g. 0.30)
3955
- --fail-above-waste-usd <n> Exit with code 3 if waste spend exceeds threshold in USD (e.g. 50)
3956
-
3957
- -h, --help Show help
3958
- `;
3959
- }
3960
- function renderPushHelp(commandPrefix = commandDisplay.prefix) {
3961
- return `${formatCommand("push", commandPrefix)}
3962
-
3963
- Push a cached audit snapshot to the Xerg API.
3964
-
3965
- Usage:
3966
- ${formatCommand("push [options]", commandPrefix)}
3967
-
3968
- Options:
3969
- --file <path> Push a specific snapshot file instead of the most recent cached audit
3970
- --dry-run Print the payload to stdout without sending it
3971
-
3972
- -h, --help Show help
3973
-
3974
- Authentication:
3975
- Set XERG_API_KEY in your environment, add "apiKey" to ~/.xerg/config.json,
3976
- or run \`${formatCommand("login", commandPrefix)}\` to authenticate via browser.
3977
- Browser login stores a token at ~/.config/xerg/credentials.json by default.
3978
- `;
3979
- }
3980
- function renderDoctorHelp(commandPrefix = commandDisplay.prefix) {
3981
- return `${formatCommand("doctor", commandPrefix)}
3982
-
3983
- Inspect your machine for OpenClaw sources and audit readiness.
3984
-
3985
- Usage:
3986
- ${formatCommand("doctor [options]", commandPrefix)}
3987
-
3988
- Options:
3989
- --log-file <path> Explicit OpenClaw gateway log file to inspect
3990
- --sessions-dir <path> Explicit OpenClaw sessions directory to inspect
3991
- --verbose Print progress updates to stderr while doctor runs
3992
-
3993
- Remote options (SSH):
3994
- --remote <user@host> SSH target in user@host or user@host:port format
3995
- --remote-log-file <path> Override the default gateway log path on the remote host
3996
- --remote-sessions-dir <path> Override the default sessions directory on the remote host
3997
-
3998
- SSH checks require ssh and rsync on your PATH.
3999
-
4000
- Railway options:
4001
- --railway Check a Railway service (uses linked project by default)
4002
- --project <id> Railway project ID
4003
- --environment <id> Railway environment ID
4004
- --service <id> Railway service ID
4005
-
4006
- Railway checks require the railway CLI on your PATH.
4007
-
4008
- -h, --help Show help
4009
- `;
4010
- }
4011
5280
  function colorError(message) {
4012
5281
  return process.stderr.isTTY ? styleText2("red", message) : message;
4013
5282
  }
4014
5283
  function readVersion() {
4015
5284
  const packageJsonPath = new URL("../package.json", import.meta.url);
4016
- const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf8"));
5285
+ const packageJson = JSON.parse(readFileSync8(packageJsonPath, "utf8"));
4017
5286
  return packageJson.version ?? "0.0.0";
4018
5287
  }
4019
5288
  //# sourceMappingURL=index.js.map