@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/README.md +5 -4
- package/dist/index.js +1524 -255
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
|
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
|
|
114
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
115
115
|
import { rmSync as rmSync4 } from "fs";
|
|
116
|
-
|
|
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
|
|
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 =
|
|
1162
|
+
current.spendUsd = round2(current.spendUsd + finding.costImpactUsd);
|
|
604
1163
|
current.findingCount += 1;
|
|
605
1164
|
buckets.set(finding.kind, current);
|
|
606
1165
|
}
|
|
@@ -618,9 +1177,9 @@ function buildTopSpendDeltas(currentRows, baselineRows) {
|
|
|
618
1177
|
const currentSpendUsd = currentMap.get(key) ?? 0;
|
|
619
1178
|
return {
|
|
620
1179
|
key,
|
|
621
|
-
baselineSpendUsd:
|
|
622
|
-
currentSpendUsd:
|
|
623
|
-
deltaSpendUsd:
|
|
1180
|
+
baselineSpendUsd: round2(baselineSpendUsd),
|
|
1181
|
+
currentSpendUsd: round2(currentSpendUsd),
|
|
1182
|
+
deltaSpendUsd: round2(currentSpendUsd - baselineSpendUsd)
|
|
624
1183
|
};
|
|
625
1184
|
}).filter((row) => row.deltaSpendUsd !== 0).sort((left, right) => Math.abs(right.deltaSpendUsd) - Math.abs(left.deltaSpendUsd)).slice(0, 3);
|
|
626
1185
|
}
|
|
@@ -655,11 +1214,11 @@ function buildFindingChanges(currentFindings, baselineFindings) {
|
|
|
655
1214
|
scope: current.scope,
|
|
656
1215
|
scopeId: current.scopeId,
|
|
657
1216
|
currentCostImpactUsd: current.costImpactUsd,
|
|
658
|
-
deltaCostImpactUsd:
|
|
1217
|
+
deltaCostImpactUsd: round2(current.costImpactUsd)
|
|
659
1218
|
});
|
|
660
1219
|
continue;
|
|
661
1220
|
}
|
|
662
|
-
const deltaCostImpactUsd =
|
|
1221
|
+
const deltaCostImpactUsd = round2(current.costImpactUsd - baseline.costImpactUsd);
|
|
663
1222
|
if (deltaCostImpactUsd > 0) {
|
|
664
1223
|
worsenedHighConfidenceWaste.push({
|
|
665
1224
|
kind: current.kind,
|
|
@@ -682,7 +1241,7 @@ function buildFindingChanges(currentFindings, baselineFindings) {
|
|
|
682
1241
|
scope: baseline.scope,
|
|
683
1242
|
scopeId: baseline.scopeId,
|
|
684
1243
|
baselineCostImpactUsd: baseline.costImpactUsd,
|
|
685
|
-
deltaCostImpactUsd:
|
|
1244
|
+
deltaCostImpactUsd: round2(-baseline.costImpactUsd)
|
|
686
1245
|
});
|
|
687
1246
|
}
|
|
688
1247
|
return {
|
|
@@ -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
|
-
|
|
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:
|
|
722
|
-
deltaObservedSpendUsd:
|
|
723
|
-
deltaEstimatedSpendUsd:
|
|
724
|
-
deltaWasteSpendUsd:
|
|
725
|
-
deltaOpportunitySpendUsd:
|
|
726
|
-
deltaStructuralWasteRate:
|
|
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 =
|
|
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 ?
|
|
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 ?
|
|
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/
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
1720
|
+
costImpactUsd: round4(outlierCost),
|
|
1027
1721
|
details: {
|
|
1028
1722
|
workflow,
|
|
1029
|
-
averageInputTokens:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
1765
|
+
costImpactUsd: round4(spend * 0.3),
|
|
1072
1766
|
details: {
|
|
1073
1767
|
workflow,
|
|
1074
1768
|
expensiveCallCount: downgradeCalls.length,
|
|
1075
|
-
inspectedSpendUsd:
|
|
1769
|
+
inspectedSpendUsd: round4(spend)
|
|
1076
1770
|
}
|
|
1077
1771
|
})
|
|
1078
1772
|
);
|
|
1079
1773
|
}
|
|
1080
1774
|
}
|
|
1081
|
-
return
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
1320
|
-
const provider =
|
|
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 =
|
|
2025
|
+
const attempt = asNumber2(
|
|
1329
2026
|
getNestedValue(record, [["attempt"], ["usage", "attempt"], ["metadata", "attempt"]])
|
|
1330
2027
|
) ?? null;
|
|
1331
|
-
const iteration =
|
|
2028
|
+
const iteration = asNumber2(
|
|
1332
2029
|
getNestedValue(record, [["iteration"], ["loop_iteration"], ["metadata", "iteration"]])
|
|
1333
2030
|
) ?? null;
|
|
1334
|
-
const retries =
|
|
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:
|
|
1347
|
-
toolCalls:
|
|
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:
|
|
2050
|
+
cacheHit: asBoolean2(
|
|
1354
2051
|
getNestedValue(record, [["cache_hit"], ["cacheHit"], ["usage", "cache_hit"]])
|
|
1355
2052
|
),
|
|
1356
|
-
cacheCostUsd:
|
|
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 =
|
|
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
|
|
1499
|
-
options
|
|
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
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
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?.(
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
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
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
options.
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
2868
|
-
import { resolve as
|
|
3994
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
3995
|
+
import { resolve as resolve3 } from "path";
|
|
2869
3996
|
function loadRemoteConfig(configPath) {
|
|
2870
|
-
const resolved =
|
|
3997
|
+
const resolved = resolve3(configPath);
|
|
2871
3998
|
let raw;
|
|
2872
3999
|
try {
|
|
2873
|
-
raw =
|
|
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(
|
|
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(
|
|
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((
|
|
3519
|
-
exec(`${cmd} ${JSON.stringify(url)}`, () =>
|
|
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((
|
|
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
|
|
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 =
|
|
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(
|
|
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:
|
|
3623
|
-
sourceHost:
|
|
3624
|
-
environment:
|
|
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(
|
|
5285
|
+
const packageJson = JSON.parse(readFileSync8(packageJsonPath, "utf8"));
|
|
4017
5286
|
return packageJson.version ?? "0.0.0";
|
|
4018
5287
|
}
|
|
4019
5288
|
//# sourceMappingURL=index.js.map
|