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