@xerg/cli 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,84 @@
1
1
  #!/usr/bin/env node
2
- import{readFileSync as ct}from"fs";import{styleText as ut}from"util";import{readFileSync as Ae}from"fs";import{createHash as Oe}from"crypto";function U(e){return Oe("sha1").update(e).digest("hex")}function H(e){if(!e)return null;let t=e.trim().match(/^(\d+)([mhdw])$/i);if(!t)throw new Error(`Invalid --since value "${e}". Use values like 30m, 24h, 7d, 2w.`);let n=Number(t[1]),o=t[2].toLowerCase(),r={m:60*1e3,h:3600*1e3,d:1440*60*1e3,w:10080*60*1e3};return Date.now()-n*r[o]}function R(){return new Date().toISOString()}function v(e){if(typeof e=="string"){let t=new Date(e);if(!Number.isNaN(t.getTime()))return t.toISOString()}if(typeof e=="number"){let t=new Date(e);if(!Number.isNaN(t.getTime()))return t.toISOString()}return R()}import{mkdirSync as $e}from"fs";import{dirname as Le}from"path";import _e from"better-sqlite3";var Y=`
2
+
3
+ // src/index.ts
4
+ import { readFileSync as readFileSync7 } from "fs";
5
+ import { styleText as styleText2 } from "util";
6
+
7
+ // src/commands/audit.ts
8
+ import { readFileSync as readFileSync5 } from "fs";
9
+ import { rmSync as rmSync4 } from "fs";
10
+ import { hostname } from "os";
11
+
12
+ // ../core/src/utils/hash.ts
13
+ import { createHash } from "crypto";
14
+ import { closeSync, openSync, readSync } from "fs";
15
+ function sha1(input) {
16
+ return createHash("sha1").update(input).digest("hex");
17
+ }
18
+ function sha1File(path) {
19
+ const hash = createHash("sha1");
20
+ const fd = openSync(path, "r");
21
+ const buffer = Buffer.allocUnsafe(64 * 1024);
22
+ try {
23
+ let bytesRead = 0;
24
+ do {
25
+ bytesRead = readSync(fd, buffer, 0, buffer.length, null);
26
+ if (bytesRead > 0) {
27
+ hash.update(buffer.subarray(0, bytesRead));
28
+ }
29
+ } while (bytesRead > 0);
30
+ } finally {
31
+ closeSync(fd);
32
+ }
33
+ return hash.digest("hex");
34
+ }
35
+
36
+ // ../core/src/utils/time.ts
37
+ function parseSince(value) {
38
+ if (!value) {
39
+ return null;
40
+ }
41
+ const match = value.trim().match(/^(\d+)([mhdw])$/i);
42
+ if (!match) {
43
+ throw new Error(`Invalid --since value "${value}". Use values like 30m, 24h, 7d, 2w.`);
44
+ }
45
+ const amount = Number(match[1]);
46
+ const unit = match[2].toLowerCase();
47
+ const multipliers = {
48
+ m: 60 * 1e3,
49
+ h: 60 * 60 * 1e3,
50
+ d: 24 * 60 * 60 * 1e3,
51
+ w: 7 * 24 * 60 * 60 * 1e3
52
+ };
53
+ return Date.now() - amount * multipliers[unit];
54
+ }
55
+ function isoNow() {
56
+ return (/* @__PURE__ */ new Date()).toISOString();
57
+ }
58
+ function toIsoOrNow(value) {
59
+ if (typeof value === "string") {
60
+ const candidate = new Date(value);
61
+ if (!Number.isNaN(candidate.getTime())) {
62
+ return candidate.toISOString();
63
+ }
64
+ }
65
+ if (typeof value === "number") {
66
+ const candidate = new Date(value);
67
+ if (!Number.isNaN(candidate.getTime())) {
68
+ return candidate.toISOString();
69
+ }
70
+ }
71
+ return isoNow();
72
+ }
73
+
74
+ // ../core/src/db/client.ts
75
+ import { mkdirSync } from "fs";
76
+ import { dirname } from "path";
77
+ import Database from "better-sqlite3";
78
+
79
+ // ../core/src/db/schema.ts
80
+ var SCHEMA_VERSION = 1;
81
+ var SCHEMA_SQL = `
3
82
  CREATE TABLE IF NOT EXISTS source_files (
4
83
  id TEXT PRIMARY KEY,
5
84
  path TEXT NOT NULL,
@@ -75,7 +154,96 @@ CREATE TABLE IF NOT EXISTS audit_snapshots (
75
154
  created_at TEXT NOT NULL,
76
155
  summary_json TEXT NOT NULL
77
156
  );
78
- `;function _(e){$e(Le(e),{recursive:!0});let t=new _e(e);return t.exec(Y),{sqlite:t}}function V(e,t){let{sqlite:n}=_(t),o=R(),r=e.pricingCatalog.map(s=>({...s,cachedInputPer1m:s.cachedInputPer1m??null})),a=e.summary.sourceFiles.map(s=>({id:U(`${s.path}:${s.mtimeMs}:${s.sizeBytes}`),path:s.path,kind:s.kind,fileHash:U(Ae(s.path,"utf8")),mtimeMs:Math.trunc(s.mtimeMs),sizeBytes:s.sizeBytes,importedAt:o})),i=e.runs.map(s=>({id:s.id,sourcePath:s.sourcePath,sourceKind:s.sourceKind,timestamp:s.timestamp,workflow:s.workflow,environment:s.environment,tagsJson:JSON.stringify(s.tags),totalCostUsd:s.totalCostUsd,totalTokens:s.totalTokens,observedCostUsd:s.observedCostUsd,estimatedCostUsd:s.estimatedCostUsd})),c=e.runs.flatMap(s=>s.calls.map(p=>({id:p.id,runId:p.runId,timestamp:p.timestamp,provider:p.provider,model:p.model,inputTokens:p.inputTokens,outputTokens:p.outputTokens,costUsd:p.costUsd,costSource:p.costSource,latencyMs:p.latencyMs,toolCalls:p.toolCalls,retries:p.retries,attempt:p.attempt,iteration:p.iteration,status:p.status,taskClass:p.taskClass,cacheHit:p.cacheHit,cacheCostUsd:p.cacheCostUsd,metadataJson:JSON.stringify(p.metadata)}))),u=e.summary.findings.map(s=>({id:s.id,auditId:e.summary.auditId,classification:s.classification,confidence:s.confidence,kind:s.kind,title:s.title,summary:s.summary,scope:s.scope,scopeId:s.scopeId,costImpactUsd:s.costImpactUsd,detailsJson:JSON.stringify(s.details)})),d=n.transaction(()=>{O(n,`
157
+ `;
158
+
159
+ // ../core/src/db/client.ts
160
+ function createDb(path) {
161
+ mkdirSync(dirname(path), { recursive: true });
162
+ const sqlite = new Database(path);
163
+ const currentVersion = sqlite.pragma("user_version", { simple: true });
164
+ if (currentVersion > SCHEMA_VERSION) {
165
+ sqlite.close();
166
+ throw new Error(
167
+ `Unsupported Xerg database schema version ${currentVersion}. This build supports up to ${SCHEMA_VERSION}.`
168
+ );
169
+ }
170
+ sqlite.exec(SCHEMA_SQL);
171
+ if (currentVersion < SCHEMA_VERSION) {
172
+ sqlite.pragma(`user_version = ${SCHEMA_VERSION}`);
173
+ }
174
+ return { sqlite };
175
+ }
176
+
177
+ // ../core/src/db/persist.ts
178
+ function persistAudit(audit, dbPath) {
179
+ const { sqlite } = createDb(dbPath);
180
+ const importedAt = isoNow();
181
+ const pricingRows = audit.pricingCatalog.map((entry) => ({
182
+ ...entry,
183
+ cachedInputPer1m: entry.cachedInputPer1m ?? null
184
+ }));
185
+ const sourceFileRows = audit.summary.sourceFiles.map((file) => ({
186
+ id: sha1(`${file.path}:${file.mtimeMs}:${file.sizeBytes}`),
187
+ path: file.path,
188
+ kind: file.kind,
189
+ fileHash: sha1File(file.path),
190
+ mtimeMs: Math.trunc(file.mtimeMs),
191
+ sizeBytes: file.sizeBytes,
192
+ importedAt
193
+ }));
194
+ const runRows = audit.runs.map((run2) => ({
195
+ id: run2.id,
196
+ sourcePath: run2.sourcePath,
197
+ sourceKind: run2.sourceKind,
198
+ timestamp: run2.timestamp,
199
+ workflow: run2.workflow,
200
+ environment: run2.environment,
201
+ tagsJson: JSON.stringify(run2.tags),
202
+ totalCostUsd: run2.totalCostUsd,
203
+ totalTokens: run2.totalTokens,
204
+ observedCostUsd: run2.observedCostUsd,
205
+ estimatedCostUsd: run2.estimatedCostUsd
206
+ }));
207
+ const callRows = audit.runs.flatMap(
208
+ (run2) => run2.calls.map((call) => ({
209
+ id: call.id,
210
+ runId: call.runId,
211
+ timestamp: call.timestamp,
212
+ provider: call.provider,
213
+ model: call.model,
214
+ inputTokens: call.inputTokens,
215
+ outputTokens: call.outputTokens,
216
+ costUsd: call.costUsd,
217
+ costSource: call.costSource,
218
+ latencyMs: call.latencyMs,
219
+ toolCalls: call.toolCalls,
220
+ retries: call.retries,
221
+ attempt: call.attempt,
222
+ iteration: call.iteration,
223
+ status: call.status,
224
+ taskClass: call.taskClass,
225
+ cacheHit: call.cacheHit,
226
+ cacheCostUsd: call.cacheCostUsd,
227
+ metadataJson: JSON.stringify(call.metadata)
228
+ }))
229
+ );
230
+ const findingRows = audit.summary.findings.map((finding) => ({
231
+ id: finding.id,
232
+ auditId: audit.summary.auditId,
233
+ classification: finding.classification,
234
+ confidence: finding.confidence,
235
+ kind: finding.kind,
236
+ title: finding.title,
237
+ summary: finding.summary,
238
+ scope: finding.scope,
239
+ scopeId: finding.scopeId,
240
+ costImpactUsd: finding.costImpactUsd,
241
+ detailsJson: JSON.stringify(finding.details)
242
+ }));
243
+ const persistTransaction = sqlite.transaction(() => {
244
+ insertMany(
245
+ sqlite,
246
+ `
79
247
  INSERT OR IGNORE INTO pricing_catalog (
80
248
  id,
81
249
  provider,
@@ -85,7 +253,20 @@ CREATE TABLE IF NOT EXISTS audit_snapshots (
85
253
  output_per_1m,
86
254
  cached_input_per_1m
87
255
  ) VALUES (?, ?, ?, ?, ?, ?, ?)
88
- `,r.map(s=>[s.id,s.provider,s.model,s.effectiveDate,s.inputPer1m,s.outputPer1m,s.cachedInputPer1m])),O(n,`
256
+ `,
257
+ pricingRows.map((row) => [
258
+ row.id,
259
+ row.provider,
260
+ row.model,
261
+ row.effectiveDate,
262
+ row.inputPer1m,
263
+ row.outputPer1m,
264
+ row.cachedInputPer1m
265
+ ])
266
+ );
267
+ insertMany(
268
+ sqlite,
269
+ `
89
270
  INSERT OR IGNORE INTO source_files (
90
271
  id,
91
272
  path,
@@ -95,7 +276,20 @@ CREATE TABLE IF NOT EXISTS audit_snapshots (
95
276
  size_bytes,
96
277
  imported_at
97
278
  ) VALUES (?, ?, ?, ?, ?, ?, ?)
98
- `,a.map(s=>[s.id,s.path,s.kind,s.fileHash,s.mtimeMs,s.sizeBytes,s.importedAt])),O(n,`
279
+ `,
280
+ sourceFileRows.map((row) => [
281
+ row.id,
282
+ row.path,
283
+ row.kind,
284
+ row.fileHash,
285
+ row.mtimeMs,
286
+ row.sizeBytes,
287
+ row.importedAt
288
+ ])
289
+ );
290
+ insertMany(
291
+ sqlite,
292
+ `
99
293
  INSERT OR IGNORE INTO runs (
100
294
  id,
101
295
  source_path,
@@ -109,7 +303,24 @@ CREATE TABLE IF NOT EXISTS audit_snapshots (
109
303
  observed_cost_usd,
110
304
  estimated_cost_usd
111
305
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
112
- `,i.map(s=>[s.id,s.sourcePath,s.sourceKind,s.timestamp,s.workflow,s.environment,s.tagsJson,s.totalCostUsd,s.totalTokens,s.observedCostUsd,s.estimatedCostUsd])),O(n,`
306
+ `,
307
+ runRows.map((row) => [
308
+ row.id,
309
+ row.sourcePath,
310
+ row.sourceKind,
311
+ row.timestamp,
312
+ row.workflow,
313
+ row.environment,
314
+ row.tagsJson,
315
+ row.totalCostUsd,
316
+ row.totalTokens,
317
+ row.observedCostUsd,
318
+ row.estimatedCostUsd
319
+ ])
320
+ );
321
+ insertMany(
322
+ sqlite,
323
+ `
113
324
  INSERT OR IGNORE INTO calls (
114
325
  id,
115
326
  run_id,
@@ -131,7 +342,32 @@ CREATE TABLE IF NOT EXISTS audit_snapshots (
131
342
  cache_cost_usd,
132
343
  metadata_json
133
344
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
134
- `,c.map(s=>[s.id,s.runId,s.timestamp,s.provider,s.model,s.inputTokens,s.outputTokens,s.costUsd,s.costSource,s.latencyMs??null,s.toolCalls,s.retries,s.attempt??null,s.iteration??null,s.status??null,s.taskClass??null,s.cacheHit?1:0,s.cacheCostUsd??null,s.metadataJson])),O(n,`
345
+ `,
346
+ callRows.map((row) => [
347
+ row.id,
348
+ row.runId,
349
+ row.timestamp,
350
+ row.provider,
351
+ row.model,
352
+ row.inputTokens,
353
+ row.outputTokens,
354
+ row.costUsd,
355
+ row.costSource,
356
+ row.latencyMs ?? null,
357
+ row.toolCalls,
358
+ row.retries,
359
+ row.attempt ?? null,
360
+ row.iteration ?? null,
361
+ row.status ?? null,
362
+ row.taskClass ?? null,
363
+ row.cacheHit ? 1 : 0,
364
+ row.cacheCostUsd ?? null,
365
+ row.metadataJson
366
+ ])
367
+ );
368
+ insertMany(
369
+ sqlite,
370
+ `
135
371
  INSERT OR IGNORE INTO findings (
136
372
  id,
137
373
  audit_id,
@@ -145,26 +381,3277 @@ CREATE TABLE IF NOT EXISTS audit_snapshots (
145
381
  cost_impact_usd,
146
382
  details_json
147
383
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
148
- `,u.map(s=>[s.id,s.auditId,s.classification,s.confidence,s.kind,s.title,s.summary,s.scope,s.scopeId,s.costImpactUsd,s.detailsJson])),n.prepare(`
384
+ `,
385
+ findingRows.map((row) => [
386
+ row.id,
387
+ row.auditId,
388
+ row.classification,
389
+ row.confidence,
390
+ row.kind,
391
+ row.title,
392
+ row.summary,
393
+ row.scope,
394
+ row.scopeId,
395
+ row.costImpactUsd,
396
+ row.detailsJson
397
+ ])
398
+ );
399
+ sqlite.prepare(
400
+ `
149
401
  INSERT OR IGNORE INTO audit_snapshots (
150
402
  id,
151
403
  created_at,
152
404
  summary_json
153
405
  ) VALUES (?, ?, ?)
154
- `).run(e.summary.auditId,e.summary.generatedAt,JSON.stringify(e.summary))});try{d()}finally{n.close()}}function O(e,t,n){if(n.length===0)return;let o=e.prepare(t);for(let r of n)o.run(...r)}var xe={"retry-waste":"Retry waste","context-outlier":"Context bloat","loop-waste":"Loop waste","candidate-downgrade":"Downgrade candidates","idle-spend":"Idle waste"};function w(e){return Number(e.toFixed(6))}function ve(e){if(!e)return"all";let t=e.trim().toLowerCase().match(/^(\d+)([mhdw])$/);return t?`${Number(t[1])}${t[2]}`:e.trim().toLowerCase()}function ee(e){return e.replace(/\\/g,"/")}function De(e){let t=ee(e),n="/sessions/",o=t.lastIndexOf(n);return o>=0?t.slice(0,o+n.length-1):t.slice(0,t.lastIndexOf("/"))||t}function Fe(e){let t=ee(e);return t.slice(0,t.lastIndexOf("/"))||t}function Pe(e){return xe[e]??e}function Me(e){return e.kind==="sessions"?De(e.path):Fe(e.path)}function F(e){let t=Array.from(new Set(e.sources.map(o=>o.kind))).sort(),n=Array.from(new Set(e.sources.map(o=>`${o.kind}:${Me(o)}`))).sort();return U(JSON.stringify({kinds:t,roots:n,since:ve(e.since)}))}function $(e,t){let n=new Map;for(let o of e){if(o.classification!==t)continue;let r=n.get(o.kind)??{kind:o.kind,label:Pe(o.kind),classification:t,spendUsd:0,findingCount:0};r.spendUsd=w(r.spendUsd+o.costImpactUsd),r.findingCount+=1,n.set(o.kind,r)}return Array.from(n.values()).sort((o,r)=>r.spendUsd-o.spendUsd)}function q(e){return new Map(e.map(t=>[t.key,t.spendUsd]))}function Q(e,t){let n=q(e),o=q(t);return Array.from(new Set([...n.keys(),...o.keys()])).map(a=>{let i=o.get(a)??0,c=n.get(a)??0;return{key:a,baselineSpendUsd:w(i),currentSpendUsd:w(c),deltaSpendUsd:w(c-i)}}).filter(a=>a.deltaSpendUsd!==0).sort((a,i)=>Math.abs(i.deltaSpendUsd)-Math.abs(a.deltaSpendUsd)).slice(0,3)}function Z(e){return`${e.kind}:${e.scope}:${e.scopeId}`}function D(e){return e.sort((t,n)=>Math.abs(n.deltaCostImpactUsd)-Math.abs(t.deltaCostImpactUsd))}function Be(e,t){let n=e.filter(d=>d.classification==="waste"&&d.confidence==="high"),o=t.filter(d=>d.classification==="waste"&&d.confidence==="high"),r=new Map(n.map(d=>[Z(d),d])),a=new Map(o.map(d=>[Z(d),d])),i=[],c=[],u=[];for(let[d,s]of r.entries()){let p=a.get(d);if(!p){i.push({kind:s.kind,title:s.title,scope:s.scope,scopeId:s.scopeId,currentCostImpactUsd:s.costImpactUsd,deltaCostImpactUsd:w(s.costImpactUsd)});continue}let l=w(s.costImpactUsd-p.costImpactUsd);l>0&&u.push({kind:s.kind,title:s.title,scope:s.scope,scopeId:s.scopeId,baselineCostImpactUsd:p.costImpactUsd,currentCostImpactUsd:s.costImpactUsd,deltaCostImpactUsd:l})}for(let[d,s]of a.entries())r.has(d)||c.push({kind:s.kind,title:s.title,scope:s.scope,scopeId:s.scopeId,baselineCostImpactUsd:s.costImpactUsd,deltaCostImpactUsd:w(-s.costImpactUsd)});return{newHighConfidenceWaste:D(i),resolvedHighConfidenceWaste:D(c),worsenedHighConfidenceWaste:D(u)}}function te(e){return{...e,comparisonKey:e.comparisonKey??F({sources:e.sourceFiles,since:e.since}),comparison:e.comparison??null,wasteByKind:e.wasteByKind?.length>0?e.wasteByKind:$(e.findings,"waste"),opportunityByKind:e.opportunityByKind?.length>0?e.opportunityByKind:$(e.findings,"opportunity"),notes:e.notes??[]}}function ne(e,t){let n=Q(e.spendByWorkflow,t.spendByWorkflow),o=Q(e.spendByModel,t.spendByModel);return{baselineAuditId:t.auditId,baselineGeneratedAt:t.generatedAt,baselineRunCount:t.runCount,baselineCallCount:t.callCount,baselineTotalSpendUsd:t.totalSpendUsd,baselineObservedSpendUsd:t.observedSpendUsd,baselineEstimatedSpendUsd:t.estimatedSpendUsd,baselineWasteSpendUsd:t.wasteSpendUsd,baselineOpportunitySpendUsd:t.opportunitySpendUsd,baselineStructuralWasteRate:t.structuralWasteRate,deltaTotalSpendUsd:w(e.totalSpendUsd-t.totalSpendUsd),deltaObservedSpendUsd:w(e.observedSpendUsd-t.observedSpendUsd),deltaEstimatedSpendUsd:w(e.estimatedSpendUsd-t.estimatedSpendUsd),deltaWasteSpendUsd:w(e.wasteSpendUsd-t.wasteSpendUsd),deltaOpportunitySpendUsd:w(e.opportunitySpendUsd-t.opportunitySpendUsd),deltaStructuralWasteRate:w(e.structuralWasteRate-t.structuralWasteRate),deltaRunCount:e.runCount-t.runCount,deltaCallCount:e.callCount-t.callCount,workflowDeltas:n,modelDeltas:o,findingChanges:Be(e.findings,t.findings)}}function We(e){try{return te(JSON.parse(e))}catch{return null}}function Xe(e){let{sqlite:t}=_(e);try{return t.prepare(`
155
- SELECT summary_json AS summaryJson
406
+ `
407
+ ).run(audit.summary.auditId, audit.summary.generatedAt, JSON.stringify(audit.summary));
408
+ });
409
+ try {
410
+ persistTransaction();
411
+ } finally {
412
+ sqlite.close();
413
+ }
414
+ }
415
+ function insertMany(sqlite, sql, rows) {
416
+ if (rows.length === 0) {
417
+ return;
418
+ }
419
+ const statement = sqlite.prepare(sql);
420
+ for (const row of rows) {
421
+ statement.run(...row);
422
+ }
423
+ }
424
+
425
+ // ../core/src/report/comparison.ts
426
+ var FINDING_KIND_LABELS = {
427
+ "retry-waste": "Retry waste",
428
+ "context-outlier": "Context bloat",
429
+ "loop-waste": "Loop waste",
430
+ "candidate-downgrade": "Downgrade candidates",
431
+ "idle-spend": "Idle waste"
432
+ };
433
+ function round(value) {
434
+ return Number(value.toFixed(6));
435
+ }
436
+ function normalizeSinceValue(since) {
437
+ if (!since) {
438
+ return "all";
439
+ }
440
+ const match = since.trim().toLowerCase().match(/^(\d+)([mhdw])$/);
441
+ if (!match) {
442
+ return since.trim().toLowerCase();
443
+ }
444
+ return `${Number(match[1])}${match[2]}`;
445
+ }
446
+ function normalizePath(path) {
447
+ return path.replace(/\\/g, "/");
448
+ }
449
+ function findSessionsRoot(path) {
450
+ const normalized = normalizePath(path);
451
+ const marker = "/sessions/";
452
+ const index = normalized.lastIndexOf(marker);
453
+ if (index >= 0) {
454
+ return normalized.slice(0, index + marker.length - 1);
455
+ }
456
+ return normalized.slice(0, normalized.lastIndexOf("/")) || normalized;
457
+ }
458
+ function findGatewayRoot(path) {
459
+ const normalized = normalizePath(path);
460
+ return normalized.slice(0, normalized.lastIndexOf("/")) || normalized;
461
+ }
462
+ function getFindingKindLabel(kind) {
463
+ return FINDING_KIND_LABELS[kind] ?? kind;
464
+ }
465
+ function getComparisonSourceRoot(source) {
466
+ if (source.kind === "sessions") {
467
+ return findSessionsRoot(source.path);
468
+ }
469
+ return findGatewayRoot(source.path);
470
+ }
471
+ function buildComparisonKey(input) {
472
+ const kinds = Array.from(new Set(input.sources.map((source) => source.kind))).sort();
473
+ const roots = Array.from(
474
+ new Set(input.sources.map((source) => `${source.kind}:${getComparisonSourceRoot(source)}`))
475
+ ).sort();
476
+ return sha1(
477
+ JSON.stringify({
478
+ kinds,
479
+ roots,
480
+ since: normalizeSinceValue(input.since)
481
+ })
482
+ );
483
+ }
484
+ function buildTaxonomyBuckets(findings, classification) {
485
+ const buckets = /* @__PURE__ */ new Map();
486
+ for (const finding of findings) {
487
+ if (finding.classification !== classification) {
488
+ continue;
489
+ }
490
+ const current = buckets.get(finding.kind) ?? {
491
+ kind: finding.kind,
492
+ label: getFindingKindLabel(finding.kind),
493
+ classification,
494
+ spendUsd: 0,
495
+ findingCount: 0
496
+ };
497
+ current.spendUsd = round(current.spendUsd + finding.costImpactUsd);
498
+ current.findingCount += 1;
499
+ buckets.set(finding.kind, current);
500
+ }
501
+ return Array.from(buckets.values()).sort((left, right) => right.spendUsd - left.spendUsd);
502
+ }
503
+ function toSpendMap(rows) {
504
+ return new Map(rows.map((row) => [row.key, row.spendUsd]));
505
+ }
506
+ function buildTopSpendDeltas(currentRows, baselineRows) {
507
+ const currentMap = toSpendMap(currentRows);
508
+ const baselineMap = toSpendMap(baselineRows);
509
+ const keys = Array.from(/* @__PURE__ */ new Set([...currentMap.keys(), ...baselineMap.keys()]));
510
+ return keys.map((key) => {
511
+ const baselineSpendUsd = baselineMap.get(key) ?? 0;
512
+ const currentSpendUsd = currentMap.get(key) ?? 0;
513
+ return {
514
+ key,
515
+ baselineSpendUsd: round(baselineSpendUsd),
516
+ currentSpendUsd: round(currentSpendUsd),
517
+ deltaSpendUsd: round(currentSpendUsd - baselineSpendUsd)
518
+ };
519
+ }).filter((row) => row.deltaSpendUsd !== 0).sort((left, right) => Math.abs(right.deltaSpendUsd) - Math.abs(left.deltaSpendUsd)).slice(0, 3);
520
+ }
521
+ function getFindingIdentity(finding) {
522
+ return `${finding.kind}:${finding.scope}:${finding.scopeId}`;
523
+ }
524
+ function sortFindingChanges(changes) {
525
+ return changes.sort(
526
+ (left, right) => Math.abs(right.deltaCostImpactUsd) - Math.abs(left.deltaCostImpactUsd)
527
+ );
528
+ }
529
+ function buildFindingChanges(currentFindings, baselineFindings) {
530
+ const currentWaste = currentFindings.filter(
531
+ (finding) => finding.classification === "waste" && finding.confidence === "high"
532
+ );
533
+ const baselineWaste = baselineFindings.filter(
534
+ (finding) => finding.classification === "waste" && finding.confidence === "high"
535
+ );
536
+ const currentMap = new Map(currentWaste.map((finding) => [getFindingIdentity(finding), finding]));
537
+ const baselineMap = new Map(
538
+ baselineWaste.map((finding) => [getFindingIdentity(finding), finding])
539
+ );
540
+ const newHighConfidenceWaste = [];
541
+ const resolvedHighConfidenceWaste = [];
542
+ const worsenedHighConfidenceWaste = [];
543
+ for (const [identity, current] of currentMap.entries()) {
544
+ const baseline = baselineMap.get(identity);
545
+ if (!baseline) {
546
+ newHighConfidenceWaste.push({
547
+ kind: current.kind,
548
+ title: current.title,
549
+ scope: current.scope,
550
+ scopeId: current.scopeId,
551
+ currentCostImpactUsd: current.costImpactUsd,
552
+ deltaCostImpactUsd: round(current.costImpactUsd)
553
+ });
554
+ continue;
555
+ }
556
+ const deltaCostImpactUsd = round(current.costImpactUsd - baseline.costImpactUsd);
557
+ if (deltaCostImpactUsd > 0) {
558
+ worsenedHighConfidenceWaste.push({
559
+ kind: current.kind,
560
+ title: current.title,
561
+ scope: current.scope,
562
+ scopeId: current.scopeId,
563
+ baselineCostImpactUsd: baseline.costImpactUsd,
564
+ currentCostImpactUsd: current.costImpactUsd,
565
+ deltaCostImpactUsd
566
+ });
567
+ }
568
+ }
569
+ for (const [identity, baseline] of baselineMap.entries()) {
570
+ if (currentMap.has(identity)) {
571
+ continue;
572
+ }
573
+ resolvedHighConfidenceWaste.push({
574
+ kind: baseline.kind,
575
+ title: baseline.title,
576
+ scope: baseline.scope,
577
+ scopeId: baseline.scopeId,
578
+ baselineCostImpactUsd: baseline.costImpactUsd,
579
+ deltaCostImpactUsd: round(-baseline.costImpactUsd)
580
+ });
581
+ }
582
+ return {
583
+ newHighConfidenceWaste: sortFindingChanges(newHighConfidenceWaste),
584
+ resolvedHighConfidenceWaste: sortFindingChanges(resolvedHighConfidenceWaste),
585
+ worsenedHighConfidenceWaste: sortFindingChanges(worsenedHighConfidenceWaste)
586
+ };
587
+ }
588
+ function hydrateAuditSummary(summary) {
589
+ return {
590
+ ...summary,
591
+ comparisonKey: summary.comparisonKey ?? buildComparisonKey({
592
+ sources: summary.sourceFiles,
593
+ since: summary.since
594
+ }),
595
+ comparison: summary.comparison ?? null,
596
+ wasteByKind: summary.wasteByKind?.length > 0 ? summary.wasteByKind : buildTaxonomyBuckets(summary.findings, "waste"),
597
+ opportunityByKind: summary.opportunityByKind?.length > 0 ? summary.opportunityByKind : buildTaxonomyBuckets(summary.findings, "opportunity"),
598
+ notes: summary.notes ?? []
599
+ };
600
+ }
601
+ function buildAuditComparison(current, baseline) {
602
+ const workflowDeltas = buildTopSpendDeltas(current.spendByWorkflow, baseline.spendByWorkflow);
603
+ const modelDeltas = buildTopSpendDeltas(current.spendByModel, baseline.spendByModel);
604
+ return {
605
+ baselineAuditId: baseline.auditId,
606
+ baselineGeneratedAt: baseline.generatedAt,
607
+ baselineRunCount: baseline.runCount,
608
+ baselineCallCount: baseline.callCount,
609
+ baselineTotalSpendUsd: baseline.totalSpendUsd,
610
+ baselineObservedSpendUsd: baseline.observedSpendUsd,
611
+ baselineEstimatedSpendUsd: baseline.estimatedSpendUsd,
612
+ baselineWasteSpendUsd: baseline.wasteSpendUsd,
613
+ baselineOpportunitySpendUsd: baseline.opportunitySpendUsd,
614
+ baselineStructuralWasteRate: baseline.structuralWasteRate,
615
+ deltaTotalSpendUsd: round(current.totalSpendUsd - baseline.totalSpendUsd),
616
+ deltaObservedSpendUsd: round(current.observedSpendUsd - baseline.observedSpendUsd),
617
+ deltaEstimatedSpendUsd: round(current.estimatedSpendUsd - baseline.estimatedSpendUsd),
618
+ deltaWasteSpendUsd: round(current.wasteSpendUsd - baseline.wasteSpendUsd),
619
+ deltaOpportunitySpendUsd: round(current.opportunitySpendUsd - baseline.opportunitySpendUsd),
620
+ deltaStructuralWasteRate: round(current.structuralWasteRate - baseline.structuralWasteRate),
621
+ deltaRunCount: current.runCount - baseline.runCount,
622
+ deltaCallCount: current.callCount - baseline.callCount,
623
+ workflowDeltas,
624
+ modelDeltas,
625
+ findingChanges: buildFindingChanges(current.findings, baseline.findings)
626
+ };
627
+ }
628
+
629
+ // ../core/src/db/read.ts
630
+ function parseAuditSummary(row) {
631
+ try {
632
+ return hydrateAuditSummary(JSON.parse(row.summaryJson));
633
+ } catch (error) {
634
+ const message = error instanceof Error ? error.message : "Unknown error";
635
+ process.stderr.write(`Warning: skipping unreadable audit snapshot ${row.id}: ${message}
636
+ `);
637
+ return null;
638
+ }
639
+ }
640
+ function listStoredAuditSummaries(dbPath) {
641
+ const { sqlite } = createDb(dbPath);
642
+ try {
643
+ const rows = sqlite.prepare(
644
+ `
645
+ SELECT id, summary_json AS summaryJson
156
646
  FROM audit_snapshots
157
647
  ORDER BY created_at DESC
158
- `).all().map(o=>We(o.summaryJson)).filter(o=>o!==null)}finally{t.close()}}function se(e){return Xe(e.dbPath).find(t=>e.currentAuditId&&t.auditId===e.currentAuditId?!1:t.comparisonKey===e.comparisonKey)}import{statSync as Ge}from"fs";import{glob as Je}from"fs/promises";import{resolve as Ke}from"path";import{mkdirSync as P}from"fs";import{homedir as re}from"os";import{join as h}from"path";import{platform as oe}from"process";function je(){let e=re(),t=oe==="darwin"?{data:h(e,"Library","Application Support","xerg"),config:h(e,"Library","Preferences","xerg"),cache:h(e,"Library","Caches","xerg")}:oe==="win32"?{data:h(process.env.LOCALAPPDATA??h(e,"AppData","Local"),"xerg","Data"),config:h(process.env.APPDATA??h(e,"AppData","Roaming"),"xerg","Config"),cache:h(process.env.LOCALAPPDATA??h(e,"AppData","Local"),"xerg","Cache")}:{data:h(process.env.XDG_DATA_HOME??h(e,".local","share"),"xerg"),config:h(process.env.XDG_CONFIG_HOME??h(e,".config"),"xerg"),cache:h(process.env.XDG_CACHE_HOME??h(e,".cache"),"xerg")};return P(t.data,{recursive:!0}),P(t.config,{recursive:!0}),P(t.cache,{recursive:!0}),t}function ie(){return h(je().data,"xerg.db")}function M(){return h(re(),".openclaw","agents","*","sessions","*.jsonl")}function B(){return"/tmp/openclaw/openclaw-*.log"}function A(e,t){try{let n=Ge(e);return n.isFile()?{kind:t,path:e,sizeBytes:n.size,mtimeMs:n.mtimeMs}:null}catch{return null}}async function X(e){let t=[];if(e.logFile){let a=A(e.logFile,"gateway");a&&t.push(a)}if(e.sessionsDir){let a=await W("**/*.jsonl",{cwd:e.sessionsDir,resolveWith:e.sessionsDir});for(let i of a){let c=A(i,"sessions");c&&t.push(c)}}if(t.length>0)return t.sort((a,i)=>i.mtimeMs-a.mtimeMs);let[n,o]=await Promise.all([W(B()),W(M())]);return[...n.map(a=>A(a,"gateway")).filter(Boolean),...o.map(a=>A(a,"sessions")).filter(Boolean)].sort((a,i)=>i.mtimeMs-a.mtimeMs)}async function W(e,t){let n=[];for await(let o of Je(e,{cwd:t?.cwd}))n.push(t?.resolveWith?Ke(t.resolveWith,o):o);return n}async function ae(e){let t=await X(e),n=[];return t.length===0&&(n.push("No OpenClaw gateway logs or session files were detected."),n.push("Use --log-file or --sessions-dir if your OpenClaw data lives outside the defaults.")),t.some(o=>o.kind==="gateway")&&n.push("Gateway logs detected. These are preferred when cost metadata is present."),t.some(o=>o.kind==="sessions")&&n.push("Session transcript fallback detected. Xerg will extract usage metadata only."),{canAudit:t.length>0,sources:t,defaults:{gatewayPattern:B(),sessionsPattern:M()},notes:n}}function L(e){return{...e,id:U(`${e.kind}:${e.scope}:${e.scopeId}:${e.title}:${e.costImpactUsd}:${e.summary}`)}}function b(e){return Number(e.toFixed(6))}function de(e){let t=[],o=e.flatMap(i=>i.calls.map(c=>({run:i,call:c}))).filter(({call:i})=>{let c=(i.status??"").toLowerCase();return c.includes("error")||c.includes("fail")}),r=o.reduce((i,c)=>i+c.call.costUsd,0);r>0&&t.push(L({classification:"waste",confidence:"high",kind:"retry-waste",title:"Retry waste is consuming measurable spend",summary:`${o.length} failed call${o.length===1?"":"s"} were followed by additional work, making their spend pure retry overhead.`,scope:"global",scopeId:"all",costImpactUsd:b(r),details:{failedCallCount:o.length}}));for(let i of e){let c=Math.max(...i.calls.map(u=>u.iteration??0));if(c>=7){let d=i.calls.filter(s=>(s.iteration??0)>5).reduce((s,p)=>s+p.costUsd,0);t.push(L({classification:"waste",confidence:"high",kind:"loop-waste",title:`Workflow "${i.workflow}" ran beyond efficient loop bounds`,summary:`This run reached ${c} iterations. Xerg treats the spend after iteration 5 as likely loop waste.`,scope:"run",scopeId:i.id,costImpactUsd:b(d),details:{workflow:i.workflow,maxIteration:c}}))}}let a=new Map;for(let i of e){let c=a.get(i.workflow)??[];c.push(i),a.set(i.workflow,c)}for(let[i,c]of a.entries()){if(c.length>=3){let s=c.map(g=>g.calls.reduce((k,y)=>k+y.inputTokens,0)),p=s.reduce((g,k)=>g+k,0)/s.length,l=c.filter(g=>{let k=g.calls.reduce((y,Re)=>y+Re.inputTokens,0);return k>p*1.75&&k>1500});if(l.length>0){let g=l.reduce((k,y)=>k+y.totalCostUsd,0);t.push(L({classification:"opportunity",confidence:"medium",kind:"context-outlier",title:`Context usage in "${i}" is well above its baseline`,summary:`Xerg found ${l.length} run${l.length===1?"":"s"} in this workflow with input token volume far above the workflow average.`,scope:"workflow",scopeId:i,costImpactUsd:b(g),details:{workflow:i,averageInputTokens:b(p),outlierRunCount:l.length}}))}}let u=c.filter(s=>/(heartbeat|cron|monitor|poll)/i.test(s.workflow));if(u.length>0){let s=u.reduce((p,l)=>p+l.totalCostUsd,0);t.push(L({classification:"opportunity",confidence:"medium",kind:"idle-spend",title:`Idle or monitoring spend detected in "${i}"`,summary:"This workflow name looks like a recurring heartbeat or monitoring loop. Review whether the cadence and model tier are justified.",scope:"workflow",scopeId:i,costImpactUsd:b(s),details:{workflow:i}}))}let d=c.flatMap(s=>s.calls).filter(s=>/(opus|gpt-4o|sonnet)/i.test(s.model)&&/(heartbeat|cron|monitor|summary|tag|triage)/i.test(s.taskClass??i));if(d.length>0){let s=d.reduce((p,l)=>p+l.costUsd,0);t.push(L({classification:"opportunity",confidence:"low",kind:"candidate-downgrade",title:`Candidate model downgrade opportunity in "${i}"`,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.",scope:"workflow",scopeId:i,costImpactUsd:b(s*.3),details:{workflow:i,expensiveCallCount:d.length,inspectedSpendUsd:b(s)}}))}}return t.sort((i,c)=>c.costImpactUsd-i.costImpactUsd)}import{readFileSync as He}from"fs";import{basename as Ye}from"path";var j=[{id:"anthropic-claude-haiku-4-5-2026-03-01",provider:"anthropic",model:"claude-haiku-4-5",effectiveDate:"2026-03-01",inputPer1m:.8,outputPer1m:4},{id:"anthropic-claude-sonnet-4-5-2026-03-01",provider:"anthropic",model:"claude-sonnet-4-5",effectiveDate:"2026-03-01",inputPer1m:3,outputPer1m:15},{id:"anthropic-claude-opus-4-2026-03-01",provider:"anthropic",model:"claude-opus-4",effectiveDate:"2026-03-01",inputPer1m:15,outputPer1m:75},{id:"openai-gpt-4o-2026-03-01",provider:"openai",model:"gpt-4o",effectiveDate:"2026-03-01",inputPer1m:2.5,outputPer1m:10},{id:"openai-gpt-4.1-mini-2026-03-01",provider:"openai",model:"gpt-4.1-mini",effectiveDate:"2026-03-01",inputPer1m:.4,outputPer1m:1.6},{id:"google-gemini-2.0-flash-2026-03-01",provider:"google",model:"gemini-2.0-flash",effectiveDate:"2026-03-01",inputPer1m:.35,outputPer1m:1.4},{id:"meta-llama-3.3-70b-2026-03-01",provider:"meta",model:"llama-3.3-70b-instruct",effectiveDate:"2026-03-01",inputPer1m:.9,outputPer1m:.9}];function ze(e,t){let n=e.trim().toLowerCase(),o=t.trim().toLowerCase();return j.find(r=>r.provider.toLowerCase()===n&&r.model.toLowerCase()===o)}function ce(e,t,n,o){let r=ze(e,t);if(!r)return null;let a=Math.max(n,0)/1e6*r.inputPer1m,i=Math.max(o,0)/1e6*r.outputPer1m;return Number((a+i).toFixed(8))}function f(e,t){if(!e||typeof e!="object")return null;let n=e;for(let o of t){let r=n;for(let a of o){if(!r||typeof r!="object"||!(a in r)){r=void 0;break}r=r[a]}if(r!==void 0)return r}return null}function S(e){if(typeof e=="number"&&Number.isFinite(e))return e;if(typeof e=="string"&&e.trim()!==""){let t=Number(e);return Number.isFinite(t)?t:null}return null}function C(e){return typeof e=="string"&&e.trim()!==""?e.trim():null}function ue(e){return typeof e=="boolean"?e:typeof e=="string"?["true","1","yes"].includes(e.trim().toLowerCase()):typeof e=="number"?e>0:!1}function pe(e,t){let n={};for(let o of t){let r=e[o];(typeof r=="string"||typeof r=="number"||typeof r=="boolean")&&(n[o]=r)}return n}function Ve(e){let n=He(e,"utf8").split(/\r?\n/).map(r=>r.trim()).filter(Boolean),o=[];for(let r of n)try{let a=JSON.parse(r);o.push(a)}catch{}return o}function qe(e){return C(f(e,[["provider"],["message","provider"],["usage","provider"]]))??"unknown"}function Qe(e){return C(f(e,[["model"],["message","model"],["usage","model"]]))??"unknown-model"}function le(e,t){return C(f(e,[["workflow"],["session","workflow"],["metadata","workflow"],["agent","name"],["agentId"],["sessionId"]]))??Ye(t,".jsonl")}function Ze(e){return C(f(e,[["environment"],["env"],["metadata","environment"]]))??"local"}function et(e,t,n,o){return C(f(e,[["run_id"],["runId"],["trace_id"],["traceId"],["sessionId"],["thread_id"]]))??`${o}:${t}:${n}`}function tt(e,t){return C(f(e,[["task_class"],["taskClass"],["metadata","taskClass"]]))??t.toLowerCase()}function x(e){let t=S(f(e,[["input_tokens"],["inputTokens"],["usage","input_tokens"],["usage","inputTokens"],["message","usage","input_tokens"],["message","usage","inputTokens"],["usage","prompt_tokens"],["message","usage","prompt_tokens"]]))??0,n=S(f(e,[["output_tokens"],["outputTokens"],["usage","output_tokens"],["usage","outputTokens"],["message","usage","output_tokens"],["message","usage","outputTokens"],["usage","completion_tokens"],["message","usage","completion_tokens"]]))??0,o=S(f(e,[["cost_usd"],["costUsd"],["usage","cost_usd"],["usage","costUsd"],["usage","cost","total"],["message","usage","cost","total"],["message","usage","cost_usd"],["pricing","total_usd"]]))??null;return{inputTokens:t,outputTokens:n,observedCost:o}}function nt(e,t,n,o){let r=qe(t),a=Qe(t),i=le(t,e.path),{inputTokens:c,outputTokens:u,observedCost:d}=x(t),s=ce(r,a,c,u),p=v(f(t,[["timestamp"],["createdAt"],["created_at"]])),l=S(f(t,[["attempt"],["usage","attempt"],["metadata","attempt"]]))??null,g=S(f(t,[["iteration"],["loop_iteration"],["metadata","iteration"]]))??null,k=S(f(t,[["retries"],["retry_count"],["metadata","retries"]]))??0,y=d??s??0;return{id:U(`${n}:${e.path}:${o}:${a}:${p}:${y}`),runId:n,timestamp:p,provider:r,model:a,inputTokens:c,outputTokens:u,costUsd:y,costSource:d!==null?"observed":"estimated",latencyMs:S(f(t,[["latency_ms"],["latencyMs"],["usage","latency_ms"]]))??null,toolCalls:S(f(t,[["tool_calls"],["toolCalls"],["usage","tool_calls"]]))??0,retries:k,attempt:l,iteration:g,status:C(f(t,[["status"],["level"],["result"],["error","type"]]))??null,taskClass:tt(t,i),cacheHit:ue(f(t,[["cache_hit"],["cacheHit"],["usage","cache_hit"]])),cacheCostUsd:S(f(t,[["cache_cost_usd"],["cacheCostUsd"],["usage","cache_cost_usd"]]))??null,metadata:pe(t,["event","type","sessionId","agentId"])}}function st(e){return x(e).inputTokens>0||x(e).outputTokens>0||x(e).observedCost!==null}function me(e,t){let n=H(t),o=new Map;for(let r of e)Ve(r.path).forEach((i,c)=>{if(!st(i))return;let u=le(i,r.path),d=v(f(i,[["timestamp"],["createdAt"],["created_at"]]));if(n&&new Date(d).getTime()<n)return;let s=et(i,u,c,r.path),p=U(`${r.path}:${s}`),l=nt(r,i,p,c),g=o.get(p);if(!g){o.set(p,{id:p,sourceKind:r.kind,sourcePath:r.path,timestamp:d,workflow:u,environment:Ze(i),tags:{sourceKind:r.kind},calls:[l],totalCostUsd:l.costUsd,totalTokens:l.inputTokens+l.outputTokens,observedCostUsd:l.costSource==="observed"?l.costUsd:0,estimatedCostUsd:l.costSource==="estimated"?l.costUsd:0});return}g.calls.push(l),g.totalCostUsd=Number((g.totalCostUsd+l.costUsd).toFixed(8)),g.totalTokens+=l.inputTokens+l.outputTokens,g.observedCostUsd+=l.costSource==="observed"?l.costUsd:0,g.estimatedCostUsd+=l.costSource==="estimated"?l.costUsd:0});return Array.from(o.values()).sort((r,a)=>new Date(r.timestamp).getTime()-new Date(a.timestamp).getTime())}function fe(e){let t=new Map;for(let n of e){let o=t.get(n.key)??{spendUsd:0,observedSpendUsd:0,callCount:0};o.spendUsd+=n.spendUsd,o.observedSpendUsd+=n.observedSpendUsd,o.callCount+=1,t.set(n.key,o)}return Array.from(t.entries()).map(([n,o])=>{let r=o.spendUsd===0?0:o.observedSpendUsd/o.spendUsd;return{key:n,spendUsd:Number(o.spendUsd.toFixed(6)),callCount:o.callCount,observedShare:Number(r.toFixed(4))}}).sort((n,o)=>o.spendUsd-n.spendUsd)}function ge(e){let t=e.runs.reduce((u,d)=>u+d.calls.length,0),n=e.runs.reduce((u,d)=>u+d.totalCostUsd,0),o=e.runs.reduce((u,d)=>u+d.observedCostUsd,0),r=e.runs.reduce((u,d)=>u+d.estimatedCostUsd,0),a=e.findings.filter(u=>u.classification==="waste").reduce((u,d)=>u+d.costImpactUsd,0),i=e.findings.filter(u=>u.classification==="opportunity").reduce((u,d)=>u+d.costImpactUsd,0),c=R();return{auditId:U(`${c}:${e.runs.length}:${e.sources.map(u=>u.path).join("|")}`),generatedAt:c,comparisonKey:F({sources:e.sources,since:e.since}),comparison:null,since:e.since,runCount:e.runs.length,callCount:t,totalSpendUsd:Number(n.toFixed(6)),observedSpendUsd:Number(o.toFixed(6)),estimatedSpendUsd:Number(r.toFixed(6)),wasteSpendUsd:Number(a.toFixed(6)),opportunitySpendUsd:Number(i.toFixed(6)),structuralWasteRate:Number((n===0?0:a/n).toFixed(4)),wasteByKind:$(e.findings,"waste"),opportunityByKind:$(e.findings,"opportunity"),spendByWorkflow:fe(e.runs.map(u=>({key:u.workflow,spendUsd:u.totalCostUsd,observedSpendUsd:u.observedCostUsd}))),spendByModel:fe(e.runs.flatMap(u=>u.calls.map(d=>({key:`${d.provider}/${d.model}`,spendUsd:d.costUsd,observedSpendUsd:d.costSource==="observed"?d.costUsd:0})))),findings:e.findings,notes:["Cost per outcome is intentionally unavailable in v0. Xerg is measuring waste intelligence only.","Opportunity findings are directional recommendations, not proven waste."],sourceFiles:e.sources,dbPath:e.dbPath}}async function he(e){return ae(e)}async function we(e){if(e.compare&&e.noDb)throw new Error("The --compare flag needs local snapshot history. Remove --no-db or provide --db <path>.");let t=await X(e);if(t.length===0)throw new Error("No OpenClaw sources were detected. Run `xerg doctor` or provide --log-file / --sessions-dir.");let n=me(t,e.since),o=de(n),r=e.noDb?void 0:e.dbPath??ie(),a=ge({runs:n,findings:o,sources:t,since:e.since,dbPath:r});if(e.compare&&r){let i=se({dbPath:r,comparisonKey:a.comparisonKey,currentAuditId:a.auditId});i?a.comparison=ne(a,i):a.notes=[...a.notes,"No prior comparable audit was found. Run the same audit again after a fix to unlock before/after deltas."]}return r&&V({summary:a,runs:n,pricingCatalog:j},r),a}function m(e){return new Intl.NumberFormat("en-US",{style:"currency",currency:"USD",minimumFractionDigits:e>=1?2:4,maximumFractionDigits:4}).format(e)}function N(e){return`${(e*100).toFixed(0)}%`}function Se(e){let t=e*100;return`${t>0?"+":""}${t.toFixed(0)} pts`}function I(e){return`${e>0?"+":""}${m(e)}`}function K(e,t=5){return e.slice(0,t).map(n=>`- ${n.key}: ${m(n.spendUsd)} (${N(n.observedShare)} observed)`)}function Ue(e,t,n){return e.length===0?[`- ${t}`]:e.map(o=>{let r=`${o.findingCount} finding${o.findingCount===1?"":"s"}`,a=n?` ${n}`:"";return`- ${o.label}: ${m(o.spendUsd)} across ${r}${a}`})}function Te(e){return["## Waste taxonomy","Structural waste",...Ue(e.wasteByKind,"No confirmed waste buckets detected."),"Savings opportunities",...Ue(e.opportunityByKind,"No opportunity buckets detected.","(directional)")]}function ot(e,t){return e.findings.filter(n=>n.classification===t).sort((n,o)=>o.costImpactUsd-n.costImpactUsd)[0]}function rt(e){return e.findings.filter(t=>t.classification==="opportunity").sort((t,n)=>{let o=t.kind==="candidate-downgrade"?1:0,r=n.kind==="candidate-downgrade"?1:0;return o!==r?r-o:n.costImpactUsd-t.costImpactUsd})[0]??null}function ke(e,t){return e.length===0?[`- ${t}`]:e.slice(0,5).map(n=>`- ${n.title}: ${m(n.costImpactUsd)} (${n.confidence})`)}function G(e){return`${e.key} (${I(e.deltaSpendUsd)})`}function it(e){return e.filter(t=>t.deltaSpendUsd<0).sort((t,n)=>t.deltaSpendUsd-n.deltaSpendUsd)[0]}function at(e){return e.filter(t=>t.deltaSpendUsd>0).sort((t,n)=>n.deltaSpendUsd-t.deltaSpendUsd)[0]}function J(e,t){return t==="resolved"?`- Resolved: ${e.title} (${m(e.baselineCostImpactUsd??0)})`:t==="worsened"?`- Worsened: ${e.title} (${I(e.deltaCostImpactUsd)})`:`- New: ${e.title} (${m(e.currentCostImpactUsd??0)})`}function dt(e){if(!e.comparison)return[];let t=e.comparison,n=it(t.workflowDeltas),o=at(t.workflowDeltas),r=o?.key??e.spendByWorkflow[0]?.key??null,a=[...t.findingChanges.newHighConfidenceWaste.map(i=>J(i,"new")),...t.findingChanges.resolvedHighConfidenceWaste.map(i=>J(i,"resolved")),...t.findingChanges.worsenedHighConfidenceWaste.map(i=>J(i,"worsened"))].slice(0,5);return["## Before / after",`Compared against ${t.baselineGeneratedAt}`,`- Total spend: ${m(t.baselineTotalSpendUsd)} -> ${m(e.totalSpendUsd)} (${I(t.deltaTotalSpendUsd)})`,`- Structural waste: ${m(t.baselineWasteSpendUsd)} -> ${m(e.wasteSpendUsd)} (${I(t.deltaWasteSpendUsd)})`,`- Waste rate: ${N(t.baselineStructuralWasteRate)} -> ${N(e.structuralWasteRate)} (${Se(t.deltaStructuralWasteRate)})`,`- Runs analyzed: ${t.baselineRunCount} -> ${e.runCount} (${t.deltaRunCount>0?"+":""}${t.deltaRunCount})`,`- Model calls: ${t.baselineCallCount} -> ${e.callCount} (${t.deltaCallCount>0?"+":""}${t.deltaCallCount})`,n?`- Biggest improvement: ${G(n)}`:"- Biggest improvement: none detected",o?`- Biggest regression: ${G(o)}`:"- Biggest regression: none detected",r?`- First workflow to inspect now: ${r}`:"- First workflow to inspect now: no workflow delta available",...t.modelDeltas.length>0?[`- Model swing to inspect: ${G(t.modelDeltas[0])}`]:["- Model swing to inspect: none"],...a.length>0?a:["- High-confidence waste changes: none"]]}function ye(e){return["# Xerg doctor","",e.canAudit?"OpenClaw sources detected.":"No OpenClaw sources detected.","","## Defaults",`- gateway logs: ${e.defaults.gatewayPattern}`,`- session files: ${e.defaults.sessionsPattern}`,"","## Sources",...e.sources.length>0?e.sources.map(n=>`- [${n.kind}] ${n.path}`):["- none"],"","## Notes",...e.notes.map(n=>`- ${n}`)].join(`
159
- `)}function Ce(e){let t=e.findings.filter(a=>a.classification==="waste"),n=e.findings.filter(a=>a.classification==="opportunity"),o=rt(e),r=ot(e,"waste");return["# Xerg audit","",`Total spend: ${m(e.totalSpendUsd)}`,`Observed spend: ${m(e.observedSpendUsd)}`,`Estimated spend: ${m(e.estimatedSpendUsd)}`,`Runs analyzed: ${e.runCount}`,`Model calls: ${e.callCount}`,`Structural waste identified: ${m(e.wasteSpendUsd)} (${N(e.structuralWasteRate)})`,`Potential impact surfaced: ${m(e.opportunitySpendUsd)}`,"",...Te(e),"","## Top workflows",...K(e.spendByWorkflow),"","## Top models",...K(e.spendByModel),"","## High-confidence waste",...ke(t,"none detected"),"","## Opportunities",...ke(n,"none detected"),"","## First savings test",...o?[`- Start with ${o.title}: ${m(o.costImpactUsd)} of potential impact`,`- Why this test first: ${o.summary}`]:["- No savings test surfaced yet"],...r?[`- Confirmed leak to close first: ${r.title}`]:["- Confirmed leak to close first: none"],...e.spendByWorkflow[0]?[`- Workflow to inspect first: ${e.spendByWorkflow[0].key}`]:["- Workflow to inspect first: none"],"",...dt(e),...e.comparison?[""]:[],"## Notes",...e.notes.map(a=>`- ${a}`)].join(`
160
- `)}function be(e){let t=["# Xerg Audit Report","",`- Generated: ${e.generatedAt}`,`- Total spend: ${m(e.totalSpendUsd)}`,`- Observed spend: ${m(e.observedSpendUsd)}`,`- Estimated spend: ${m(e.estimatedSpendUsd)}`,`- Structural waste identified: ${m(e.wasteSpendUsd)} (${N(e.structuralWasteRate)})`,`- Potential impact surfaced: ${m(e.opportunitySpendUsd)}`,`- Runs analyzed: ${e.runCount}`,`- Model calls: ${e.callCount}`,"",...Te(e),"","## Top workflows",...K(e.spendByWorkflow),"","## Findings",...e.findings.slice(0,10).map(n=>`- **${n.title}** (${n.classification}, ${n.confidence}) \u2014 ${n.summary} Estimated impact: ${m(n.costImpactUsd)}.`)];if(e.comparison){let n=e.comparison;t.push("","## Before / after",`- Compared against: ${n.baselineGeneratedAt}`,`- Total spend: ${m(n.baselineTotalSpendUsd)} -> ${m(e.totalSpendUsd)} (${I(n.deltaTotalSpendUsd)})`,`- Structural waste: ${m(n.baselineWasteSpendUsd)} -> ${m(e.wasteSpendUsd)} (${I(n.deltaWasteSpendUsd)})`,`- Waste rate: ${N(n.baselineStructuralWasteRate)} -> ${N(e.structuralWasteRate)} (${Se(n.deltaStructuralWasteRate)})`)}return t.join(`
161
- `)}async function Ne(e){let t=await we({logFile:e.logFile,sessionsDir:e.sessionsDir,since:e.since,compare:e.compare,dbPath:e.db,noDb:e.noDb});if(e.json){process.stdout.write(`${JSON.stringify(t,null,2)}
162
- `);return}if(e.markdown){process.stdout.write(`${be(t)}
163
- `);return}process.stdout.write(`${Ce(t)}
164
- `)}async function Ie(e){let t=await he({logFile:e.logFile,sessionsDir:e.sessionsDir});process.stdout.write(`${ye(t)}
165
- `)}var Ee=Ut(),z=process.argv.slice(2),T=z[0];(!T||T==="--help"||T==="-h"||T==="help")&&(process.stdout.write(ft()),process.exit(0));(T==="--version"||T==="-v"||T==="version")&&(process.stdout.write(`${Ee}
166
- `),process.exit(0));pt().catch(e=>{let t=e instanceof Error?e.message:"Unknown error";process.stderr.write(`${wt(`xerg failed: ${t}`)}
167
- `),process.exitCode=1});async function pt(){if(T==="audit"){let e=lt(z.slice(1));if(e.json&&e.markdown)throw new Error("Use either --json or --markdown, not both.");await Ne(e);return}if(T==="doctor"){let e=mt(z.slice(1));await Ie(e);return}throw new Error(`Unknown command "${T}". Run \`xerg --help\` to see available commands.`)}function lt(e){let t={};for(let n=0;n<e.length;n+=1){let o=e[n];switch(o){case"--help":case"-h":process.stdout.write(gt()),process.exit(0);break;case"--log-file":t.logFile=E(o,e[n+1]),n+=1;break;case"--sessions-dir":t.sessionsDir=E(o,e[n+1]),n+=1;break;case"--since":t.since=E(o,e[n+1]),n+=1;break;case"--db":t.db=E(o,e[n+1]),n+=1;break;case"--compare":t.compare=!0;break;case"--json":t.json=!0;break;case"--markdown":t.markdown=!0;break;case"--no-db":t.noDb=!0;break;default:throw new Error(`Unknown audit option "${o}". Run \`xerg audit --help\` for usage.`)}}return t}function mt(e){let t={};for(let n=0;n<e.length;n+=1){let o=e[n];switch(o){case"--help":case"-h":process.stdout.write(ht()),process.exit(0);break;case"--log-file":t.logFile=E(o,e[n+1]),n+=1;break;case"--sessions-dir":t.sessionsDir=E(o,e[n+1]),n+=1;break;default:throw new Error(`Unknown doctor option "${o}". Run \`xerg doctor --help\` for usage.`)}}return t}function E(e,t){if(!t||t.startsWith("-"))throw new Error(`The ${e} flag needs a value.`);return t}function ft(){return`xerg ${Ee}
648
+ `
649
+ ).all();
650
+ return rows.map((row) => parseAuditSummary(row)).filter((summary) => summary !== null);
651
+ } finally {
652
+ sqlite.close();
653
+ }
654
+ }
655
+ function readLatestComparableAuditSummary(input) {
656
+ return listStoredAuditSummaries(input.dbPath).find((summary) => {
657
+ if (input.currentAuditId && summary.auditId === input.currentAuditId) {
658
+ return false;
659
+ }
660
+ return summary.comparisonKey === input.comparisonKey;
661
+ });
662
+ }
663
+
664
+ // ../core/src/detect/openclaw.ts
665
+ import { readdirSync, statSync } from "fs";
666
+ import { isAbsolute, join as join2, resolve, sep } from "path";
667
+
668
+ // ../core/src/utils/paths.ts
669
+ import { mkdirSync as mkdirSync2 } from "fs";
670
+ import { homedir } from "os";
671
+ import { join } from "path";
672
+ import { platform } from "process";
673
+ function getAppPaths() {
674
+ const home = homedir();
675
+ return platform === "darwin" ? {
676
+ data: join(home, "Library", "Application Support", "xerg"),
677
+ config: join(home, "Library", "Preferences", "xerg"),
678
+ cache: join(home, "Library", "Caches", "xerg")
679
+ } : platform === "win32" ? {
680
+ data: join(process.env.LOCALAPPDATA ?? join(home, "AppData", "Local"), "xerg", "Data"),
681
+ config: join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "xerg", "Config"),
682
+ cache: join(process.env.LOCALAPPDATA ?? join(home, "AppData", "Local"), "xerg", "Cache")
683
+ } : {
684
+ data: join(process.env.XDG_DATA_HOME ?? join(home, ".local", "share"), "xerg"),
685
+ config: join(process.env.XDG_CONFIG_HOME ?? join(home, ".config"), "xerg"),
686
+ cache: join(process.env.XDG_CACHE_HOME ?? join(home, ".cache"), "xerg")
687
+ };
688
+ }
689
+ function getDefaultDbPath() {
690
+ return join(getAppPaths().data, "xerg.db");
691
+ }
692
+ function getDefaultSessionsPattern() {
693
+ return join(homedir(), ".openclaw", "agents", "*", "sessions", "*.jsonl");
694
+ }
695
+ function getDefaultGatewayPattern() {
696
+ return "/tmp/openclaw/openclaw-*.log";
697
+ }
698
+
699
+ // ../core/src/detect/openclaw.ts
700
+ function toDetected(path, kind) {
701
+ try {
702
+ const stats = statSync(path);
703
+ if (!stats.isFile()) {
704
+ return null;
705
+ }
706
+ return {
707
+ kind,
708
+ path,
709
+ sizeBytes: stats.size,
710
+ mtimeMs: stats.mtimeMs
711
+ };
712
+ } catch {
713
+ return null;
714
+ }
715
+ }
716
+ async function detectOpenClawSources(options) {
717
+ const explicitSources = [];
718
+ if (options.logFile) {
719
+ const detected2 = toDetected(options.logFile, "gateway");
720
+ if (detected2) {
721
+ explicitSources.push(detected2);
722
+ }
723
+ }
724
+ if (options.sessionsDir) {
725
+ const matches = await collectGlobMatches("**/*.jsonl", {
726
+ cwd: options.sessionsDir,
727
+ resolveWith: options.sessionsDir
728
+ });
729
+ for (const match of matches) {
730
+ const detected2 = toDetected(match, "sessions");
731
+ if (detected2) {
732
+ explicitSources.push(detected2);
733
+ }
734
+ }
735
+ }
736
+ if (explicitSources.length > 0) {
737
+ return explicitSources.sort((left, right) => right.mtimeMs - left.mtimeMs);
738
+ }
739
+ const [gatewayMatches, sessionMatches] = await Promise.all([
740
+ collectGlobMatches(getDefaultGatewayPattern()),
741
+ collectGlobMatches(getDefaultSessionsPattern())
742
+ ]);
743
+ const detected = [
744
+ ...gatewayMatches.map((path) => toDetected(path, "gateway")).filter(Boolean),
745
+ ...sessionMatches.map((path) => toDetected(path, "sessions")).filter(Boolean)
746
+ ];
747
+ return detected.sort((left, right) => right.mtimeMs - left.mtimeMs);
748
+ }
749
+ async function collectGlobMatches(pattern, options) {
750
+ const baseDir = options?.cwd ? resolve(options.cwd) : isAbsolute(pattern) ? sep : process.cwd();
751
+ const relativePattern = options?.cwd ? pattern : isAbsolute(pattern) ? pattern.slice(baseDir.length) : pattern;
752
+ const segments = relativePattern.split("/").filter(Boolean);
753
+ const matches = collectMatchesFromSegments(baseDir, segments);
754
+ return matches.map(
755
+ (match) => options?.resolveWith ? resolve(options.resolveWith, match) : match
756
+ );
757
+ }
758
+ function collectMatchesFromSegments(currentPath, segments) {
759
+ if (segments.length === 0) {
760
+ return [currentPath];
761
+ }
762
+ const [segment, ...rest] = segments;
763
+ if (segment === "**") {
764
+ const matches2 = collectMatchesFromSegments(currentPath, rest);
765
+ for (const entry of readDirSafe(currentPath)) {
766
+ if (entry.isDirectory()) {
767
+ matches2.push(...collectMatchesFromSegments(join2(currentPath, entry.name), segments));
768
+ }
769
+ }
770
+ return matches2;
771
+ }
772
+ const matches = [];
773
+ const matcher = segmentToRegExp(segment);
774
+ for (const entry of readDirSafe(currentPath)) {
775
+ if (!matcher.test(entry.name)) {
776
+ continue;
777
+ }
778
+ const nextPath = join2(currentPath, entry.name);
779
+ if (rest.length === 0) {
780
+ matches.push(nextPath);
781
+ continue;
782
+ }
783
+ if (entry.isDirectory()) {
784
+ matches.push(...collectMatchesFromSegments(nextPath, rest));
785
+ }
786
+ }
787
+ return matches;
788
+ }
789
+ function readDirSafe(path) {
790
+ try {
791
+ return readdirSync(path, { withFileTypes: true });
792
+ } catch {
793
+ return [];
794
+ }
795
+ }
796
+ function segmentToRegExp(segment) {
797
+ const escaped = segment.replaceAll(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
798
+ return new RegExp(`^${escaped}$`);
799
+ }
800
+ async function inspectOpenClawSources(options) {
801
+ const sources = await detectOpenClawSources(options);
802
+ const notes = [];
803
+ if (sources.length === 0) {
804
+ notes.push("No OpenClaw gateway logs or session files were detected.");
805
+ notes.push(
806
+ "Use --log-file or --sessions-dir if your OpenClaw data lives outside the defaults."
807
+ );
808
+ }
809
+ if (sources.some((source) => source.kind === "gateway")) {
810
+ notes.push("Gateway logs detected. These are preferred when cost metadata is present.");
811
+ }
812
+ if (sources.some((source) => source.kind === "sessions")) {
813
+ notes.push("Session transcript fallback detected. Xerg will extract usage metadata only.");
814
+ }
815
+ return {
816
+ canAudit: sources.length > 0,
817
+ sources,
818
+ defaults: {
819
+ gatewayPattern: getDefaultGatewayPattern(),
820
+ sessionsPattern: getDefaultSessionsPattern()
821
+ },
822
+ notes
823
+ };
824
+ }
825
+
826
+ // ../core/src/findings/engine.ts
827
+ function createFinding(input) {
828
+ return {
829
+ ...input,
830
+ id: sha1(
831
+ `${input.kind}:${input.scope}:${input.scopeId}:${input.title}:${input.costImpactUsd}:${input.summary}`
832
+ )
833
+ };
834
+ }
835
+ function round2(value) {
836
+ return Number(value.toFixed(6));
837
+ }
838
+ function buildFindings(runs) {
839
+ const findings = [];
840
+ const allCalls = runs.flatMap((run2) => run2.calls.map((call) => ({ run: run2, call })));
841
+ const retryCandidates = allCalls.filter(({ call }) => {
842
+ const status = (call.status ?? "").toLowerCase();
843
+ return status.includes("error") || status.includes("fail");
844
+ });
845
+ const retryCost = retryCandidates.reduce((sum, item) => sum + item.call.costUsd, 0);
846
+ if (retryCost > 0) {
847
+ findings.push(
848
+ createFinding({
849
+ classification: "waste",
850
+ confidence: "high",
851
+ kind: "retry-waste",
852
+ title: "Retry waste is consuming measurable spend",
853
+ summary: `${retryCandidates.length} failed call${retryCandidates.length === 1 ? "" : "s"} were followed by additional work, making their spend pure retry overhead.`,
854
+ scope: "global",
855
+ scopeId: "all",
856
+ costImpactUsd: round2(retryCost),
857
+ details: {
858
+ failedCallCount: retryCandidates.length
859
+ }
860
+ })
861
+ );
862
+ }
863
+ for (const run2 of runs) {
864
+ const maxIteration = Math.max(...run2.calls.map((call) => call.iteration ?? 0));
865
+ if (maxIteration >= 7) {
866
+ const loopCalls = run2.calls.filter((call) => (call.iteration ?? 0) > 5);
867
+ const loopCost = loopCalls.reduce((sum, call) => sum + call.costUsd, 0);
868
+ findings.push(
869
+ createFinding({
870
+ classification: "waste",
871
+ confidence: "high",
872
+ kind: "loop-waste",
873
+ title: `Workflow "${run2.workflow}" ran beyond efficient loop bounds`,
874
+ summary: `This run reached ${maxIteration} iterations. Xerg treats the spend after iteration 5 as likely loop waste.`,
875
+ scope: "run",
876
+ scopeId: run2.id,
877
+ costImpactUsd: round2(loopCost),
878
+ details: {
879
+ workflow: run2.workflow,
880
+ maxIteration
881
+ }
882
+ })
883
+ );
884
+ }
885
+ }
886
+ const runsByWorkflow = /* @__PURE__ */ new Map();
887
+ for (const run2 of runs) {
888
+ const bucket = runsByWorkflow.get(run2.workflow) ?? [];
889
+ bucket.push(run2);
890
+ runsByWorkflow.set(run2.workflow, bucket);
891
+ }
892
+ for (const [workflow, workflowRuns] of runsByWorkflow.entries()) {
893
+ if (workflowRuns.length >= 3) {
894
+ const totalInputs = workflowRuns.map(
895
+ (run2) => run2.calls.reduce((sum, call) => sum + call.inputTokens, 0)
896
+ );
897
+ const average = totalInputs.reduce((sum, value) => sum + value, 0) / totalInputs.length;
898
+ const outlierRuns = workflowRuns.filter((run2) => {
899
+ const tokens = run2.calls.reduce((sum, call) => sum + call.inputTokens, 0);
900
+ return tokens > average * 1.75 && tokens > 1500;
901
+ });
902
+ if (outlierRuns.length > 0) {
903
+ const outlierCost = outlierRuns.reduce((sum, run2) => sum + run2.totalCostUsd, 0);
904
+ findings.push(
905
+ createFinding({
906
+ classification: "opportunity",
907
+ confidence: "medium",
908
+ kind: "context-outlier",
909
+ title: `Context usage in "${workflow}" is well above its baseline`,
910
+ summary: `Xerg found ${outlierRuns.length} run${outlierRuns.length === 1 ? "" : "s"} in this workflow with input token volume far above the workflow average.`,
911
+ scope: "workflow",
912
+ scopeId: workflow,
913
+ costImpactUsd: round2(outlierCost),
914
+ details: {
915
+ workflow,
916
+ averageInputTokens: round2(average),
917
+ outlierRunCount: outlierRuns.length
918
+ }
919
+ })
920
+ );
921
+ }
922
+ }
923
+ const idleRuns = workflowRuns.filter(
924
+ (run2) => /(heartbeat|cron|monitor|poll)/i.test(run2.workflow)
925
+ );
926
+ if (idleRuns.length > 0) {
927
+ const idleCost = idleRuns.reduce((sum, run2) => sum + run2.totalCostUsd, 0);
928
+ findings.push(
929
+ createFinding({
930
+ classification: "opportunity",
931
+ confidence: "medium",
932
+ kind: "idle-spend",
933
+ title: `Idle or monitoring spend detected in "${workflow}"`,
934
+ summary: "This workflow name looks like a recurring heartbeat or monitoring loop. Review whether the cadence and model tier are justified.",
935
+ scope: "workflow",
936
+ scopeId: workflow,
937
+ costImpactUsd: round2(idleCost),
938
+ details: {
939
+ workflow
940
+ }
941
+ })
942
+ );
943
+ }
944
+ const downgradeCalls = workflowRuns.flatMap((run2) => run2.calls).filter((call) => {
945
+ return /(opus|gpt-4o|sonnet)/i.test(call.model) && /(heartbeat|cron|monitor|summary|tag|triage)/i.test(call.taskClass ?? workflow);
946
+ });
947
+ if (downgradeCalls.length > 0) {
948
+ const spend = downgradeCalls.reduce((sum, call) => sum + call.costUsd, 0);
949
+ findings.push(
950
+ createFinding({
951
+ classification: "opportunity",
952
+ confidence: "low",
953
+ kind: "candidate-downgrade",
954
+ title: `Candidate model downgrade opportunity in "${workflow}"`,
955
+ 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.",
956
+ scope: "workflow",
957
+ scopeId: workflow,
958
+ costImpactUsd: round2(spend * 0.3),
959
+ details: {
960
+ workflow,
961
+ expensiveCallCount: downgradeCalls.length,
962
+ inspectedSpendUsd: round2(spend)
963
+ }
964
+ })
965
+ );
966
+ }
967
+ }
968
+ return findings.sort((left, right) => right.costImpactUsd - left.costImpactUsd);
969
+ }
970
+
971
+ // ../core/src/normalize/openclaw.ts
972
+ import { readFileSync } from "fs";
973
+ import { basename } from "path";
974
+
975
+ // ../core/src/pricing-catalog.ts
976
+ var PRICING_CATALOG = [
977
+ {
978
+ id: "anthropic-claude-haiku-4-5-2026-03-01",
979
+ provider: "anthropic",
980
+ model: "claude-haiku-4-5",
981
+ effectiveDate: "2026-03-01",
982
+ inputPer1m: 0.8,
983
+ outputPer1m: 4
984
+ },
985
+ {
986
+ id: "anthropic-claude-sonnet-4-5-2026-03-01",
987
+ provider: "anthropic",
988
+ model: "claude-sonnet-4-5",
989
+ effectiveDate: "2026-03-01",
990
+ inputPer1m: 3,
991
+ outputPer1m: 15
992
+ },
993
+ {
994
+ id: "anthropic-claude-opus-4-2026-03-01",
995
+ provider: "anthropic",
996
+ model: "claude-opus-4",
997
+ effectiveDate: "2026-03-01",
998
+ inputPer1m: 15,
999
+ outputPer1m: 75
1000
+ },
1001
+ {
1002
+ id: "openai-gpt-4o-2026-03-01",
1003
+ provider: "openai",
1004
+ model: "gpt-4o",
1005
+ effectiveDate: "2026-03-01",
1006
+ inputPer1m: 2.5,
1007
+ outputPer1m: 10
1008
+ },
1009
+ {
1010
+ id: "openai-gpt-4.1-mini-2026-03-01",
1011
+ provider: "openai",
1012
+ model: "gpt-4.1-mini",
1013
+ effectiveDate: "2026-03-01",
1014
+ inputPer1m: 0.4,
1015
+ outputPer1m: 1.6
1016
+ },
1017
+ {
1018
+ id: "google-gemini-2.0-flash-2026-03-01",
1019
+ provider: "google",
1020
+ model: "gemini-2.0-flash",
1021
+ effectiveDate: "2026-03-01",
1022
+ inputPer1m: 0.35,
1023
+ outputPer1m: 1.4
1024
+ },
1025
+ {
1026
+ id: "meta-llama-3.3-70b-2026-03-01",
1027
+ provider: "meta",
1028
+ model: "llama-3.3-70b-instruct",
1029
+ effectiveDate: "2026-03-01",
1030
+ inputPer1m: 0.9,
1031
+ outputPer1m: 0.9
1032
+ }
1033
+ ];
1034
+ function getPricingEntry(provider, model) {
1035
+ const normalizedProvider = provider.trim().toLowerCase();
1036
+ const normalizedModel = model.trim().toLowerCase();
1037
+ return PRICING_CATALOG.find((entry) => {
1038
+ return entry.provider.toLowerCase() === normalizedProvider && entry.model.toLowerCase() === normalizedModel;
1039
+ });
1040
+ }
1041
+ function estimateCostUsd(provider, model, inputTokens, outputTokens) {
1042
+ const entry = getPricingEntry(provider, model);
1043
+ if (!entry) {
1044
+ return null;
1045
+ }
1046
+ const inputCost = Math.max(inputTokens, 0) / 1e6 * entry.inputPer1m;
1047
+ const outputCost = Math.max(outputTokens, 0) / 1e6 * entry.outputPer1m;
1048
+ return Number((inputCost + outputCost).toFixed(8));
1049
+ }
1050
+
1051
+ // ../core/src/utils/records.ts
1052
+ function getNestedValue(input, paths) {
1053
+ if (!input || typeof input !== "object") {
1054
+ return null;
1055
+ }
1056
+ const record = input;
1057
+ for (const path of paths) {
1058
+ let current = record;
1059
+ for (const segment of path) {
1060
+ if (!current || typeof current !== "object" || !(segment in current)) {
1061
+ current = void 0;
1062
+ break;
1063
+ }
1064
+ current = current[segment];
1065
+ }
1066
+ if (current !== void 0) {
1067
+ return current;
1068
+ }
1069
+ }
1070
+ return null;
1071
+ }
1072
+ function asNumber(value) {
1073
+ if (typeof value === "number" && Number.isFinite(value)) {
1074
+ return value;
1075
+ }
1076
+ if (typeof value === "string" && value.trim() !== "") {
1077
+ const numeric = Number(value);
1078
+ return Number.isFinite(numeric) ? numeric : null;
1079
+ }
1080
+ return null;
1081
+ }
1082
+ function asString(value) {
1083
+ if (typeof value === "string" && value.trim() !== "") {
1084
+ return value.trim();
1085
+ }
1086
+ return null;
1087
+ }
1088
+ function asBoolean(value) {
1089
+ if (typeof value === "boolean") {
1090
+ return value;
1091
+ }
1092
+ if (typeof value === "string") {
1093
+ return ["true", "1", "yes"].includes(value.trim().toLowerCase());
1094
+ }
1095
+ if (typeof value === "number") {
1096
+ return value > 0;
1097
+ }
1098
+ return false;
1099
+ }
1100
+ function pickMetadata(input, keys) {
1101
+ const output = {};
1102
+ for (const key of keys) {
1103
+ const value = input[key];
1104
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1105
+ output[key] = value;
1106
+ }
1107
+ }
1108
+ return output;
1109
+ }
1110
+
1111
+ // ../core/src/normalize/openclaw.ts
1112
+ function parseJsonLines(path) {
1113
+ const content = readFileSync(path, "utf8");
1114
+ const lines = content.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1115
+ const records = [];
1116
+ for (const line of lines) {
1117
+ try {
1118
+ const parsed = JSON.parse(line);
1119
+ records.push(parsed);
1120
+ } catch {
1121
+ }
1122
+ }
1123
+ return records;
1124
+ }
1125
+ function inferProvider(record) {
1126
+ return asString(
1127
+ getNestedValue(record, [["provider"], ["message", "provider"], ["usage", "provider"]])
1128
+ ) ?? "unknown";
1129
+ }
1130
+ function inferModel(record) {
1131
+ return asString(getNestedValue(record, [["model"], ["message", "model"], ["usage", "model"]])) ?? "unknown-model";
1132
+ }
1133
+ function inferWorkflow(record, sourcePath) {
1134
+ return asString(
1135
+ getNestedValue(record, [
1136
+ ["workflow"],
1137
+ ["session", "workflow"],
1138
+ ["metadata", "workflow"],
1139
+ ["agent", "name"],
1140
+ ["agentId"],
1141
+ ["sessionId"]
1142
+ ])
1143
+ ) ?? basename(sourcePath, ".jsonl");
1144
+ }
1145
+ function inferEnvironment(record) {
1146
+ return asString(getNestedValue(record, [["environment"], ["env"], ["metadata", "environment"]])) ?? "local";
1147
+ }
1148
+ function inferRunKey(record, workflow, index, sourcePath) {
1149
+ return asString(
1150
+ getNestedValue(record, [
1151
+ ["run_id"],
1152
+ ["runId"],
1153
+ ["trace_id"],
1154
+ ["traceId"],
1155
+ ["sessionId"],
1156
+ ["thread_id"]
1157
+ ])
1158
+ ) ?? `${sourcePath}:${workflow}:${index}`;
1159
+ }
1160
+ function inferTaskClass(record, workflow) {
1161
+ return asString(getNestedValue(record, [["task_class"], ["taskClass"], ["metadata", "taskClass"]])) ?? workflow.toLowerCase();
1162
+ }
1163
+ function extractUsage(record) {
1164
+ const inputTokens = asNumber(
1165
+ getNestedValue(record, [
1166
+ ["input_tokens"],
1167
+ ["inputTokens"],
1168
+ ["usage", "input_tokens"],
1169
+ ["usage", "inputTokens"],
1170
+ ["message", "usage", "input_tokens"],
1171
+ ["message", "usage", "inputTokens"],
1172
+ ["usage", "prompt_tokens"],
1173
+ ["message", "usage", "prompt_tokens"]
1174
+ ])
1175
+ ) ?? 0;
1176
+ const outputTokens = asNumber(
1177
+ getNestedValue(record, [
1178
+ ["output_tokens"],
1179
+ ["outputTokens"],
1180
+ ["usage", "output_tokens"],
1181
+ ["usage", "outputTokens"],
1182
+ ["message", "usage", "output_tokens"],
1183
+ ["message", "usage", "outputTokens"],
1184
+ ["usage", "completion_tokens"],
1185
+ ["message", "usage", "completion_tokens"]
1186
+ ])
1187
+ ) ?? 0;
1188
+ const observedCost = asNumber(
1189
+ getNestedValue(record, [
1190
+ ["cost_usd"],
1191
+ ["costUsd"],
1192
+ ["usage", "cost_usd"],
1193
+ ["usage", "costUsd"],
1194
+ ["usage", "cost", "total"],
1195
+ ["message", "usage", "cost", "total"],
1196
+ ["message", "usage", "cost_usd"],
1197
+ ["pricing", "total_usd"]
1198
+ ])
1199
+ ) ?? null;
1200
+ return {
1201
+ inputTokens,
1202
+ outputTokens,
1203
+ observedCost
1204
+ };
1205
+ }
1206
+ function buildCall(source, record, runId, index) {
1207
+ const provider = inferProvider(record);
1208
+ const model = inferModel(record);
1209
+ const workflow = inferWorkflow(record, source.path);
1210
+ const { inputTokens, outputTokens, observedCost } = extractUsage(record);
1211
+ const estimatedCost = estimateCostUsd(provider, model, inputTokens, outputTokens);
1212
+ const timestamp = toIsoOrNow(
1213
+ getNestedValue(record, [["timestamp"], ["createdAt"], ["created_at"]])
1214
+ );
1215
+ const attempt = asNumber(
1216
+ getNestedValue(record, [["attempt"], ["usage", "attempt"], ["metadata", "attempt"]])
1217
+ ) ?? null;
1218
+ const iteration = asNumber(
1219
+ getNestedValue(record, [["iteration"], ["loop_iteration"], ["metadata", "iteration"]])
1220
+ ) ?? null;
1221
+ const retries = asNumber(getNestedValue(record, [["retries"], ["retry_count"], ["metadata", "retries"]])) ?? 0;
1222
+ const costUsd = observedCost ?? estimatedCost ?? 0;
1223
+ return {
1224
+ id: sha1(`${runId}:${source.path}:${index}:${model}:${timestamp}:${costUsd}`),
1225
+ runId,
1226
+ timestamp,
1227
+ provider,
1228
+ model,
1229
+ inputTokens,
1230
+ outputTokens,
1231
+ costUsd,
1232
+ costSource: observedCost !== null ? "observed" : "estimated",
1233
+ latencyMs: asNumber(getNestedValue(record, [["latency_ms"], ["latencyMs"], ["usage", "latency_ms"]])) ?? null,
1234
+ toolCalls: asNumber(getNestedValue(record, [["tool_calls"], ["toolCalls"], ["usage", "tool_calls"]])) ?? 0,
1235
+ retries,
1236
+ attempt,
1237
+ iteration,
1238
+ status: asString(getNestedValue(record, [["status"], ["level"], ["result"], ["error", "type"]])) ?? null,
1239
+ taskClass: inferTaskClass(record, workflow),
1240
+ cacheHit: asBoolean(
1241
+ getNestedValue(record, [["cache_hit"], ["cacheHit"], ["usage", "cache_hit"]])
1242
+ ),
1243
+ cacheCostUsd: asNumber(
1244
+ getNestedValue(record, [["cache_cost_usd"], ["cacheCostUsd"], ["usage", "cache_cost_usd"]])
1245
+ ) ?? null,
1246
+ metadata: pickMetadata(record, ["event", "type", "sessionId", "agentId"])
1247
+ };
1248
+ }
1249
+ function shouldTreatAsCall(record) {
1250
+ const hasUsage = extractUsage(record).inputTokens > 0 || extractUsage(record).outputTokens > 0 || extractUsage(record).observedCost !== null;
1251
+ return hasUsage;
1252
+ }
1253
+ function normalizeOpenClawSources(sources, since) {
1254
+ const cutoff = parseSince(since);
1255
+ const runsById = /* @__PURE__ */ new Map();
1256
+ for (const source of sources) {
1257
+ const records = parseJsonLines(source.path);
1258
+ records.forEach((record, index) => {
1259
+ if (!shouldTreatAsCall(record)) {
1260
+ return;
1261
+ }
1262
+ const workflow = inferWorkflow(record, source.path);
1263
+ const timestamp = toIsoOrNow(
1264
+ getNestedValue(record, [["timestamp"], ["createdAt"], ["created_at"]])
1265
+ );
1266
+ if (cutoff && new Date(timestamp).getTime() < cutoff) {
1267
+ return;
1268
+ }
1269
+ const runKey = inferRunKey(record, workflow, index, source.path);
1270
+ const runId = sha1(`${source.path}:${runKey}`);
1271
+ const call = buildCall(source, record, runId, index);
1272
+ const existing = runsById.get(runId);
1273
+ if (!existing) {
1274
+ runsById.set(runId, {
1275
+ id: runId,
1276
+ sourceKind: source.kind,
1277
+ sourcePath: source.path,
1278
+ timestamp,
1279
+ workflow,
1280
+ environment: inferEnvironment(record),
1281
+ tags: {
1282
+ sourceKind: source.kind
1283
+ },
1284
+ calls: [call],
1285
+ totalCostUsd: call.costUsd,
1286
+ totalTokens: call.inputTokens + call.outputTokens,
1287
+ observedCostUsd: call.costSource === "observed" ? call.costUsd : 0,
1288
+ estimatedCostUsd: call.costSource === "estimated" ? call.costUsd : 0
1289
+ });
1290
+ return;
1291
+ }
1292
+ existing.calls.push(call);
1293
+ existing.totalCostUsd = Number((existing.totalCostUsd + call.costUsd).toFixed(8));
1294
+ existing.totalTokens += call.inputTokens + call.outputTokens;
1295
+ existing.observedCostUsd += call.costSource === "observed" ? call.costUsd : 0;
1296
+ existing.estimatedCostUsd += call.costSource === "estimated" ? call.costUsd : 0;
1297
+ });
1298
+ }
1299
+ return Array.from(runsById.values()).sort((left, right) => {
1300
+ return new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime();
1301
+ });
1302
+ }
1303
+
1304
+ // ../core/src/report/summary.ts
1305
+ function buildBreakdown(items) {
1306
+ const buckets = /* @__PURE__ */ new Map();
1307
+ for (const item of items) {
1308
+ const current = buckets.get(item.key) ?? { spendUsd: 0, observedSpendUsd: 0, callCount: 0 };
1309
+ current.spendUsd += item.spendUsd;
1310
+ current.observedSpendUsd += item.observedSpendUsd;
1311
+ current.callCount += 1;
1312
+ buckets.set(item.key, current);
1313
+ }
1314
+ return Array.from(buckets.entries()).map(([key, value]) => {
1315
+ const observedShare = value.spendUsd === 0 ? 0 : value.observedSpendUsd / value.spendUsd;
1316
+ return {
1317
+ key,
1318
+ spendUsd: Number(value.spendUsd.toFixed(6)),
1319
+ callCount: value.callCount,
1320
+ observedShare: Number(observedShare.toFixed(4))
1321
+ };
1322
+ }).sort((left, right) => right.spendUsd - left.spendUsd);
1323
+ }
1324
+ function buildAuditSummary(input) {
1325
+ const callCount = input.runs.reduce((sum, run2) => sum + run2.calls.length, 0);
1326
+ const totalSpendUsd = input.runs.reduce((sum, run2) => sum + run2.totalCostUsd, 0);
1327
+ const observedSpendUsd = input.runs.reduce((sum, run2) => sum + run2.observedCostUsd, 0);
1328
+ const estimatedSpendUsd = input.runs.reduce((sum, run2) => sum + run2.estimatedCostUsd, 0);
1329
+ const wasteSpendUsd = input.findings.filter((finding) => finding.classification === "waste").reduce((sum, finding) => sum + finding.costImpactUsd, 0);
1330
+ const opportunitySpendUsd = input.findings.filter((finding) => finding.classification === "opportunity").reduce((sum, finding) => sum + finding.costImpactUsd, 0);
1331
+ const generatedAt = isoNow();
1332
+ return {
1333
+ auditId: sha1(
1334
+ `${generatedAt}:${input.runs.length}:${input.sources.map((source) => source.path).join("|")}`
1335
+ ),
1336
+ generatedAt,
1337
+ comparisonKey: input.comparisonKeyOverride ?? buildComparisonKey({
1338
+ sources: input.sources,
1339
+ since: input.since
1340
+ }),
1341
+ comparison: null,
1342
+ since: input.since,
1343
+ runCount: input.runs.length,
1344
+ callCount,
1345
+ totalSpendUsd: Number(totalSpendUsd.toFixed(6)),
1346
+ observedSpendUsd: Number(observedSpendUsd.toFixed(6)),
1347
+ estimatedSpendUsd: Number(estimatedSpendUsd.toFixed(6)),
1348
+ wasteSpendUsd: Number(wasteSpendUsd.toFixed(6)),
1349
+ opportunitySpendUsd: Number(opportunitySpendUsd.toFixed(6)),
1350
+ structuralWasteRate: Number(
1351
+ (totalSpendUsd === 0 ? 0 : wasteSpendUsd / totalSpendUsd).toFixed(4)
1352
+ ),
1353
+ wasteByKind: buildTaxonomyBuckets(input.findings, "waste"),
1354
+ opportunityByKind: buildTaxonomyBuckets(input.findings, "opportunity"),
1355
+ spendByWorkflow: buildBreakdown(
1356
+ input.runs.map((run2) => ({
1357
+ key: run2.workflow,
1358
+ spendUsd: run2.totalCostUsd,
1359
+ observedSpendUsd: run2.observedCostUsd
1360
+ }))
1361
+ ),
1362
+ spendByModel: buildBreakdown(
1363
+ input.runs.flatMap(
1364
+ (run2) => run2.calls.map((call) => ({
1365
+ key: `${call.provider}/${call.model}`,
1366
+ spendUsd: call.costUsd,
1367
+ observedSpendUsd: call.costSource === "observed" ? call.costUsd : 0
1368
+ }))
1369
+ )
1370
+ ),
1371
+ findings: input.findings,
1372
+ notes: [
1373
+ "Cost per outcome is intentionally unavailable in v0. Xerg is measuring waste intelligence only.",
1374
+ "Opportunity findings are directional recommendations, not proven waste."
1375
+ ],
1376
+ sourceFiles: input.sources,
1377
+ dbPath: input.dbPath
1378
+ };
1379
+ }
1380
+
1381
+ // ../core/src/audit.ts
1382
+ async function doctorOpenClaw(options) {
1383
+ return inspectOpenClawSources(options);
1384
+ }
1385
+ async function auditOpenClaw(options) {
1386
+ if (options.compare && options.noDb) {
1387
+ throw new Error(
1388
+ "The --compare flag needs local snapshot history. Remove --no-db or provide --db <path>."
1389
+ );
1390
+ }
1391
+ const sources = await detectOpenClawSources(options);
1392
+ if (sources.length === 0) {
1393
+ throw new Error(
1394
+ "No OpenClaw sources were detected. Run `xerg doctor` or provide --log-file / --sessions-dir."
1395
+ );
1396
+ }
1397
+ const runs = normalizeOpenClawSources(sources, options.since);
1398
+ const findings = buildFindings(runs);
1399
+ const dbPath = options.noDb ? void 0 : options.dbPath ?? getDefaultDbPath();
1400
+ const summary = buildAuditSummary({
1401
+ runs,
1402
+ findings,
1403
+ sources,
1404
+ since: options.since,
1405
+ dbPath,
1406
+ comparisonKeyOverride: options.comparisonKeyOverride
1407
+ });
1408
+ if (options.compare && dbPath) {
1409
+ const baseline = readLatestComparableAuditSummary({
1410
+ dbPath,
1411
+ comparisonKey: summary.comparisonKey,
1412
+ currentAuditId: summary.auditId
1413
+ });
1414
+ if (baseline) {
1415
+ summary.comparison = buildAuditComparison(summary, baseline);
1416
+ } else {
1417
+ summary.notes = [
1418
+ ...summary.notes,
1419
+ "No prior comparable audit was found. Run the same audit again after a fix to unlock before/after deltas."
1420
+ ];
1421
+ }
1422
+ }
1423
+ if (dbPath) {
1424
+ persistAudit(
1425
+ {
1426
+ summary,
1427
+ runs,
1428
+ pricingCatalog: PRICING_CATALOG
1429
+ },
1430
+ dbPath
1431
+ );
1432
+ }
1433
+ return summary;
1434
+ }
1435
+
1436
+ // ../core/src/recommendations.ts
1437
+ var templatesByKind = {
1438
+ "retry-waste": {
1439
+ actionType: "other",
1440
+ titleFn: () => "Add retry backoff or reduce retry attempts",
1441
+ descriptionFn: (f) => `${f.summary} Consider adding exponential backoff or reducing the maximum retry count to eliminate this overhead.`,
1442
+ suggestedChangeFn: (f) => ({
1443
+ strategy: "exponential-backoff",
1444
+ maxRetries: 3,
1445
+ failedCallCount: f.details.failedCallCount
1446
+ })
1447
+ },
1448
+ "loop-waste": {
1449
+ actionType: "other",
1450
+ titleFn: (f) => `Cap iteration depth for ${extractWorkflow(f)}`,
1451
+ descriptionFn: (f) => `${f.summary} Adding an iteration limit or early-exit condition would prevent runaway loops from burning spend.`,
1452
+ suggestedChangeFn: (f) => ({
1453
+ strategy: "iteration-cap",
1454
+ suggestedMaxIterations: 5,
1455
+ observedMaxIteration: f.details.maxIteration
1456
+ })
1457
+ },
1458
+ "context-outlier": {
1459
+ actionType: "prompt-trim",
1460
+ titleFn: (f) => `Trim context for ${extractWorkflow(f)}`,
1461
+ descriptionFn: (f) => `${f.summary} Reducing input token volume to near the workflow baseline would lower cost proportionally.`,
1462
+ suggestedChangeFn: (f) => ({
1463
+ strategy: "context-reduction",
1464
+ averageInputTokens: f.details.averageInputTokens
1465
+ })
1466
+ },
1467
+ "candidate-downgrade": {
1468
+ actionType: "model-switch",
1469
+ titleFn: (f) => `Evaluate cheaper model for ${extractWorkflow(f)}`,
1470
+ descriptionFn: (f) => `${f.summary} This is an A/B test candidate \u2014 try a cheaper model on this workflow and compare quality.`,
1471
+ suggestedChangeFn: () => ({
1472
+ strategy: "model-downgrade",
1473
+ candidates: ["claude-3-haiku", "gpt-4o-mini"]
1474
+ })
1475
+ },
1476
+ "idle-spend": {
1477
+ actionType: "other",
1478
+ titleFn: (f) => `Review cadence for ${extractWorkflow(f)}`,
1479
+ descriptionFn: (f) => `${f.summary} Consider reducing polling frequency or switching to an event-driven approach.`,
1480
+ suggestedChangeFn: () => ({
1481
+ strategy: "cadence-review"
1482
+ })
1483
+ }
1484
+ };
1485
+ function extractWorkflow(finding) {
1486
+ const details = finding.details;
1487
+ return details.workflow || finding.scopeId || "this workflow";
1488
+ }
1489
+ function buildSingleRecommendation(finding) {
1490
+ const template = templatesByKind[finding.kind];
1491
+ if (template) {
1492
+ return {
1493
+ id: sha1(`rec:${finding.id}:${template.actionType}`),
1494
+ findingId: finding.id,
1495
+ kind: finding.kind,
1496
+ title: template.titleFn(finding),
1497
+ description: template.descriptionFn(finding),
1498
+ estimatedSavingsUsd: finding.costImpactUsd,
1499
+ confidence: finding.confidence,
1500
+ actionType: template.actionType,
1501
+ suggestedChange: template.suggestedChangeFn?.(finding)
1502
+ };
1503
+ }
1504
+ return {
1505
+ id: sha1(`rec:${finding.id}:other`),
1506
+ findingId: finding.id,
1507
+ kind: finding.kind,
1508
+ title: `Review: ${finding.title}`,
1509
+ description: finding.summary,
1510
+ estimatedSavingsUsd: finding.costImpactUsd,
1511
+ confidence: finding.confidence,
1512
+ actionType: "other"
1513
+ };
1514
+ }
1515
+ function buildRecommendations(summary) {
1516
+ return summary.findings.map(buildSingleRecommendation);
1517
+ }
1518
+
1519
+ // ../core/src/report/render.ts
1520
+ function formatUsd(value) {
1521
+ return new Intl.NumberFormat("en-US", {
1522
+ style: "currency",
1523
+ currency: "USD",
1524
+ minimumFractionDigits: value >= 1 ? 2 : 4,
1525
+ maximumFractionDigits: 4
1526
+ }).format(value);
1527
+ }
1528
+ function formatPercent(value) {
1529
+ return `${(value * 100).toFixed(0)}%`;
1530
+ }
1531
+ function formatPercentDelta(value) {
1532
+ const points = value * 100;
1533
+ const sign = points > 0 ? "+" : "";
1534
+ return `${sign}${points.toFixed(0)} pts`;
1535
+ }
1536
+ function formatUsdDelta(value) {
1537
+ const sign = value > 0 ? "+" : "";
1538
+ return `${sign}${formatUsd(value)}`;
1539
+ }
1540
+ function topRows(rows, limit = 5) {
1541
+ return rows.slice(0, limit).map((row) => {
1542
+ return `- ${row.key}: ${formatUsd(row.spendUsd)} (${formatPercent(row.observedShare)} observed)`;
1543
+ });
1544
+ }
1545
+ function renderTaxonomyRows(rows, emptyLabel, suffix) {
1546
+ if (rows.length === 0) {
1547
+ return [`- ${emptyLabel}`];
1548
+ }
1549
+ return rows.map((row) => {
1550
+ const countLabel = `${row.findingCount} finding${row.findingCount === 1 ? "" : "s"}`;
1551
+ const detail = suffix ? ` ${suffix}` : "";
1552
+ return `- ${row.label}: ${formatUsd(row.spendUsd)} across ${countLabel}${detail}`;
1553
+ });
1554
+ }
1555
+ function renderTaxonomyBlock(summary) {
1556
+ return [
1557
+ "## Waste taxonomy",
1558
+ "Structural waste",
1559
+ ...renderTaxonomyRows(summary.wasteByKind, "No confirmed waste buckets detected."),
1560
+ "Savings opportunities",
1561
+ ...renderTaxonomyRows(
1562
+ summary.opportunityByKind,
1563
+ "No opportunity buckets detected.",
1564
+ "(directional)"
1565
+ )
1566
+ ];
1567
+ }
1568
+ function topFinding(summary, classification) {
1569
+ return summary.findings.filter((finding) => finding.classification === classification).sort((left, right) => right.costImpactUsd - left.costImpactUsd)[0];
1570
+ }
1571
+ function topSavingsTest(summary) {
1572
+ return summary.findings.filter((finding) => finding.classification === "opportunity").sort((left, right) => {
1573
+ const leftPriority = left.kind === "candidate-downgrade" ? 1 : 0;
1574
+ const rightPriority = right.kind === "candidate-downgrade" ? 1 : 0;
1575
+ if (leftPriority !== rightPriority) {
1576
+ return rightPriority - leftPriority;
1577
+ }
1578
+ return right.costImpactUsd - left.costImpactUsd;
1579
+ })[0] ?? null;
1580
+ }
1581
+ function renderFindingList(findings, emptyLabel) {
1582
+ if (findings.length === 0) {
1583
+ return [`- ${emptyLabel}`];
1584
+ }
1585
+ return findings.slice(0, 5).map((finding) => {
1586
+ return `- ${finding.title}: ${formatUsd(finding.costImpactUsd)} (${finding.confidence})`;
1587
+ });
1588
+ }
1589
+ function describeSpendDelta(delta) {
1590
+ return `${delta.key} (${formatUsdDelta(delta.deltaSpendUsd)})`;
1591
+ }
1592
+ function pickBiggestImprovement(deltas) {
1593
+ return deltas.filter((delta) => delta.deltaSpendUsd < 0).sort((left, right) => left.deltaSpendUsd - right.deltaSpendUsd)[0];
1594
+ }
1595
+ function pickBiggestRegression(deltas) {
1596
+ return deltas.filter((delta) => delta.deltaSpendUsd > 0).sort((left, right) => right.deltaSpendUsd - left.deltaSpendUsd)[0];
1597
+ }
1598
+ function renderFindingChange(change, state) {
1599
+ if (state === "resolved") {
1600
+ return `- Resolved: ${change.title} (${formatUsd(change.baselineCostImpactUsd ?? 0)})`;
1601
+ }
1602
+ if (state === "worsened") {
1603
+ return `- Worsened: ${change.title} (${formatUsdDelta(change.deltaCostImpactUsd)})`;
1604
+ }
1605
+ return `- New: ${change.title} (${formatUsd(change.currentCostImpactUsd ?? 0)})`;
1606
+ }
1607
+ function renderCompareBlock(summary) {
1608
+ if (!summary.comparison) {
1609
+ return [];
1610
+ }
1611
+ const comparison = summary.comparison;
1612
+ const biggestImprovement = pickBiggestImprovement(comparison.workflowDeltas);
1613
+ const biggestRegression = pickBiggestRegression(comparison.workflowDeltas);
1614
+ const firstWorkflowToInspect = biggestRegression?.key ?? summary.spendByWorkflow[0]?.key ?? null;
1615
+ const findingChanges = [
1616
+ ...comparison.findingChanges.newHighConfidenceWaste.map(
1617
+ (change) => renderFindingChange(change, "new")
1618
+ ),
1619
+ ...comparison.findingChanges.resolvedHighConfidenceWaste.map(
1620
+ (change) => renderFindingChange(change, "resolved")
1621
+ ),
1622
+ ...comparison.findingChanges.worsenedHighConfidenceWaste.map(
1623
+ (change) => renderFindingChange(change, "worsened")
1624
+ )
1625
+ ].slice(0, 5);
1626
+ return [
1627
+ "## Before / after",
1628
+ `Compared against ${comparison.baselineGeneratedAt}`,
1629
+ `- Total spend: ${formatUsd(comparison.baselineTotalSpendUsd)} -> ${formatUsd(summary.totalSpendUsd)} (${formatUsdDelta(comparison.deltaTotalSpendUsd)})`,
1630
+ `- Structural waste: ${formatUsd(comparison.baselineWasteSpendUsd)} -> ${formatUsd(summary.wasteSpendUsd)} (${formatUsdDelta(comparison.deltaWasteSpendUsd)})`,
1631
+ `- Waste rate: ${formatPercent(comparison.baselineStructuralWasteRate)} -> ${formatPercent(summary.structuralWasteRate)} (${formatPercentDelta(comparison.deltaStructuralWasteRate)})`,
1632
+ `- Runs analyzed: ${comparison.baselineRunCount} -> ${summary.runCount} (${comparison.deltaRunCount > 0 ? "+" : ""}${comparison.deltaRunCount})`,
1633
+ `- Model calls: ${comparison.baselineCallCount} -> ${summary.callCount} (${comparison.deltaCallCount > 0 ? "+" : ""}${comparison.deltaCallCount})`,
1634
+ biggestImprovement ? `- Biggest improvement: ${describeSpendDelta(biggestImprovement)}` : "- Biggest improvement: none detected",
1635
+ biggestRegression ? `- Biggest regression: ${describeSpendDelta(biggestRegression)}` : "- Biggest regression: none detected",
1636
+ firstWorkflowToInspect ? `- First workflow to inspect now: ${firstWorkflowToInspect}` : "- First workflow to inspect now: no workflow delta available",
1637
+ ...comparison.modelDeltas.length > 0 ? [`- Model swing to inspect: ${describeSpendDelta(comparison.modelDeltas[0])}`] : ["- Model swing to inspect: none"],
1638
+ ...findingChanges.length > 0 ? findingChanges : ["- High-confidence waste changes: none"]
1639
+ ];
1640
+ }
1641
+ function renderDoctorReport(report) {
1642
+ const sections = [
1643
+ "# Xerg doctor",
1644
+ "",
1645
+ report.canAudit ? "OpenClaw sources detected." : "No OpenClaw sources detected.",
1646
+ "",
1647
+ "## Defaults",
1648
+ `- gateway logs: ${report.defaults.gatewayPattern}`,
1649
+ `- session files: ${report.defaults.sessionsPattern}`,
1650
+ "",
1651
+ "## Sources",
1652
+ ...report.sources.length > 0 ? report.sources.map((source) => `- [${source.kind}] ${source.path}`) : ["- none"],
1653
+ "",
1654
+ "## Notes",
1655
+ ...report.notes.map((note) => `- ${note}`)
1656
+ ];
1657
+ return sections.join("\n");
1658
+ }
1659
+ function renderTerminalSummary(summary) {
1660
+ const wasteFindings = summary.findings.filter((finding) => finding.classification === "waste");
1661
+ const opportunityFindings = summary.findings.filter(
1662
+ (finding) => finding.classification === "opportunity"
1663
+ );
1664
+ const topSavings = topSavingsTest(summary);
1665
+ const topWaste = topFinding(summary, "waste");
1666
+ return [
1667
+ "# Xerg audit",
1668
+ "",
1669
+ `Total spend: ${formatUsd(summary.totalSpendUsd)}`,
1670
+ `Observed spend: ${formatUsd(summary.observedSpendUsd)}`,
1671
+ `Estimated spend: ${formatUsd(summary.estimatedSpendUsd)}`,
1672
+ `Runs analyzed: ${summary.runCount}`,
1673
+ `Model calls: ${summary.callCount}`,
1674
+ `Structural waste identified: ${formatUsd(summary.wasteSpendUsd)} (${formatPercent(summary.structuralWasteRate)})`,
1675
+ `Potential impact surfaced: ${formatUsd(summary.opportunitySpendUsd)}`,
1676
+ "",
1677
+ ...renderTaxonomyBlock(summary),
1678
+ "",
1679
+ "## Top workflows",
1680
+ ...topRows(summary.spendByWorkflow),
1681
+ "",
1682
+ "## Top models",
1683
+ ...topRows(summary.spendByModel),
1684
+ "",
1685
+ "## High-confidence waste",
1686
+ ...renderFindingList(wasteFindings, "none detected"),
1687
+ "",
1688
+ "## Opportunities",
1689
+ ...renderFindingList(opportunityFindings, "none detected"),
1690
+ "",
1691
+ "## First savings test",
1692
+ ...topSavings ? [
1693
+ `- Start with ${topSavings.title}: ${formatUsd(topSavings.costImpactUsd)} of potential impact`,
1694
+ `- Why this test first: ${topSavings.summary}`
1695
+ ] : ["- No savings test surfaced yet"],
1696
+ ...topWaste ? [`- Confirmed leak to close first: ${topWaste.title}`] : ["- Confirmed leak to close first: none"],
1697
+ ...summary.spendByWorkflow[0] ? [`- Workflow to inspect first: ${summary.spendByWorkflow[0].key}`] : ["- Workflow to inspect first: none"],
1698
+ "",
1699
+ ...renderCompareBlock(summary),
1700
+ ...summary.comparison ? [""] : [],
1701
+ "## Notes",
1702
+ ...summary.notes.map((note) => `- ${note}`)
1703
+ ].join("\n");
1704
+ }
1705
+ function renderMarkdownSummary(summary) {
1706
+ const lines = [
1707
+ "# Xerg Audit Report",
1708
+ "",
1709
+ `- Generated: ${summary.generatedAt}`,
1710
+ `- Total spend: ${formatUsd(summary.totalSpendUsd)}`,
1711
+ `- Observed spend: ${formatUsd(summary.observedSpendUsd)}`,
1712
+ `- Estimated spend: ${formatUsd(summary.estimatedSpendUsd)}`,
1713
+ `- Structural waste identified: ${formatUsd(summary.wasteSpendUsd)} (${formatPercent(summary.structuralWasteRate)})`,
1714
+ `- Potential impact surfaced: ${formatUsd(summary.opportunitySpendUsd)}`,
1715
+ `- Runs analyzed: ${summary.runCount}`,
1716
+ `- Model calls: ${summary.callCount}`,
1717
+ "",
1718
+ ...renderTaxonomyBlock(summary),
1719
+ "",
1720
+ "## Top workflows",
1721
+ ...topRows(summary.spendByWorkflow),
1722
+ "",
1723
+ "## Findings",
1724
+ ...summary.findings.slice(0, 10).map((finding) => {
1725
+ return `- **${finding.title}** (${finding.classification}, ${finding.confidence}) \u2014 ${finding.summary} Estimated impact: ${formatUsd(finding.costImpactUsd)}.`;
1726
+ })
1727
+ ];
1728
+ if (summary.comparison) {
1729
+ const comparison = summary.comparison;
1730
+ lines.push(
1731
+ "",
1732
+ "## Before / after",
1733
+ `- Compared against: ${comparison.baselineGeneratedAt}`,
1734
+ `- Total spend: ${formatUsd(comparison.baselineTotalSpendUsd)} -> ${formatUsd(summary.totalSpendUsd)} (${formatUsdDelta(comparison.deltaTotalSpendUsd)})`,
1735
+ `- Structural waste: ${formatUsd(comparison.baselineWasteSpendUsd)} -> ${formatUsd(summary.wasteSpendUsd)} (${formatUsdDelta(comparison.deltaWasteSpendUsd)})`,
1736
+ `- Waste rate: ${formatPercent(comparison.baselineStructuralWasteRate)} -> ${formatPercent(summary.structuralWasteRate)} (${formatPercentDelta(comparison.deltaStructuralWasteRate)})`
1737
+ );
1738
+ }
1739
+ return lines.join("\n");
1740
+ }
1741
+
1742
+ // ../schemas/dist/index.js
1743
+ var AUDIT_PUSH_PAYLOAD_VERSION = 1;
1744
+
1745
+ // ../core/src/wire.ts
1746
+ function toWireFinding(finding) {
1747
+ return {
1748
+ id: finding.id,
1749
+ classification: finding.classification,
1750
+ confidence: finding.confidence,
1751
+ kind: finding.kind,
1752
+ title: finding.title,
1753
+ summary: finding.summary,
1754
+ scope: finding.scope,
1755
+ scopeId: finding.scopeId,
1756
+ costImpactUsd: finding.costImpactUsd
1757
+ };
1758
+ }
1759
+ function toWireComparison(comparison) {
1760
+ return {
1761
+ baselineAuditId: comparison.baselineAuditId,
1762
+ baselineGeneratedAt: comparison.baselineGeneratedAt,
1763
+ baselineTotalSpendUsd: comparison.baselineTotalSpendUsd,
1764
+ baselineWasteSpendUsd: comparison.baselineWasteSpendUsd,
1765
+ baselineStructuralWasteRate: comparison.baselineStructuralWasteRate,
1766
+ deltaTotalSpendUsd: comparison.deltaTotalSpendUsd,
1767
+ deltaWasteSpendUsd: comparison.deltaWasteSpendUsd,
1768
+ deltaStructuralWasteRate: comparison.deltaStructuralWasteRate,
1769
+ deltaRunCount: comparison.deltaRunCount,
1770
+ deltaCallCount: comparison.deltaCallCount
1771
+ };
1772
+ }
1773
+ function toWirePayload(summary, meta) {
1774
+ return {
1775
+ version: AUDIT_PUSH_PAYLOAD_VERSION,
1776
+ summary: {
1777
+ auditId: summary.auditId,
1778
+ generatedAt: summary.generatedAt,
1779
+ comparisonKey: summary.comparisonKey,
1780
+ runCount: summary.runCount,
1781
+ callCount: summary.callCount,
1782
+ totalSpendUsd: summary.totalSpendUsd,
1783
+ observedSpendUsd: summary.observedSpendUsd,
1784
+ estimatedSpendUsd: summary.estimatedSpendUsd,
1785
+ wasteSpendUsd: summary.wasteSpendUsd,
1786
+ opportunitySpendUsd: summary.opportunitySpendUsd,
1787
+ structuralWasteRate: summary.structuralWasteRate,
1788
+ wasteByKind: summary.wasteByKind,
1789
+ opportunityByKind: summary.opportunityByKind,
1790
+ spendByWorkflow: summary.spendByWorkflow,
1791
+ spendByModel: summary.spendByModel,
1792
+ findings: summary.findings.map(toWireFinding),
1793
+ notes: summary.notes,
1794
+ comparison: summary.comparison ? toWireComparison(summary.comparison) : null
1795
+ },
1796
+ meta: {
1797
+ cliVersion: meta.cliVersion,
1798
+ sourceId: meta.sourceId,
1799
+ sourceHost: meta.sourceHost,
1800
+ environment: meta.environment,
1801
+ pushedAt: isoNow()
1802
+ }
1803
+ };
1804
+ }
1805
+
1806
+ // src/errors.ts
1807
+ var NoDataError = class extends Error {
1808
+ constructor(message) {
1809
+ super(message);
1810
+ this.name = "NoDataError";
1811
+ }
1812
+ };
1813
+
1814
+ // src/push/client.ts
1815
+ async function pushAudit(payload, config) {
1816
+ const url = `${config.apiUrl}/v1/audits`;
1817
+ const body = JSON.stringify(payload);
1818
+ let response;
1819
+ try {
1820
+ response = await fetch(url, {
1821
+ method: "POST",
1822
+ headers: {
1823
+ "Content-Type": "application/json",
1824
+ Authorization: `Bearer ${config.apiKey}`
1825
+ },
1826
+ body
1827
+ });
1828
+ } catch (err) {
1829
+ const message = err instanceof Error ? err.message : "Network error";
1830
+ return { ok: false, status: 0, message: `Failed to reach ${config.apiUrl}: ${message}` };
1831
+ }
1832
+ if (response.status === 201) {
1833
+ const data = await response.json();
1834
+ return { ok: true, auditId: data.id ?? payload.summary.auditId };
1835
+ }
1836
+ let errorMessage;
1837
+ try {
1838
+ const data = await response.json();
1839
+ errorMessage = data.error || data.message || response.statusText;
1840
+ } catch {
1841
+ errorMessage = response.statusText || `HTTP ${response.status}`;
1842
+ }
1843
+ return { ok: false, status: response.status, message: errorMessage };
1844
+ }
1845
+
1846
+ // src/push/config.ts
1847
+ import { readFileSync as readFileSync3 } from "fs";
1848
+ import { homedir as homedir3 } from "os";
1849
+ import { join as join4 } from "path";
1850
+
1851
+ // src/auth/credentials.ts
1852
+ import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
1853
+ import { homedir as homedir2 } from "os";
1854
+ import { dirname as dirname2, join as join3 } from "path";
1855
+ function getCredentialsPath() {
1856
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join3(homedir2(), ".config");
1857
+ return join3(xdgConfig, "xerg", "credentials.json");
1858
+ }
1859
+ function storeCredentials(token) {
1860
+ const credPath = getCredentialsPath();
1861
+ const dir = dirname2(credPath);
1862
+ mkdirSync3(dir, { recursive: true });
1863
+ const data = { token, storedAt: (/* @__PURE__ */ new Date()).toISOString() };
1864
+ writeFileSync(credPath, JSON.stringify(data, null, 2), { mode: 384 });
1865
+ }
1866
+ function loadStoredCredentials() {
1867
+ const credPath = getCredentialsPath();
1868
+ try {
1869
+ if (!existsSync(credPath)) return null;
1870
+ const raw = readFileSync2(credPath, "utf8");
1871
+ const parsed = JSON.parse(raw);
1872
+ return parsed.token || null;
1873
+ } catch {
1874
+ return null;
1875
+ }
1876
+ }
1877
+ function clearCredentials() {
1878
+ const credPath = getCredentialsPath();
1879
+ try {
1880
+ if (!existsSync(credPath)) return false;
1881
+ rmSync(credPath);
1882
+ return true;
1883
+ } catch {
1884
+ return false;
1885
+ }
1886
+ }
1887
+
1888
+ // src/push/config.ts
1889
+ var DEFAULT_API_URL = "https://api.xerg.ai";
1890
+ var CONFIG_PATH = join4(homedir3(), ".xerg", "config.json");
1891
+ function loadPushConfig() {
1892
+ const envKey = process.env.XERG_API_KEY;
1893
+ const envUrl = process.env.XERG_API_URL;
1894
+ if (envKey) {
1895
+ return {
1896
+ apiKey: envKey,
1897
+ apiUrl: envUrl || DEFAULT_API_URL
1898
+ };
1899
+ }
1900
+ try {
1901
+ const raw = readFileSync3(CONFIG_PATH, "utf8");
1902
+ const parsed = JSON.parse(raw);
1903
+ if (parsed.apiKey) {
1904
+ return {
1905
+ apiKey: parsed.apiKey,
1906
+ apiUrl: envUrl || parsed.apiUrl || DEFAULT_API_URL
1907
+ };
1908
+ }
1909
+ } catch {
1910
+ }
1911
+ const storedToken = loadStoredCredentials();
1912
+ if (storedToken) {
1913
+ return {
1914
+ apiKey: storedToken,
1915
+ apiUrl: envUrl || DEFAULT_API_URL
1916
+ };
1917
+ }
1918
+ throw new Error(
1919
+ `No API key configured. Set XERG_API_KEY, add "apiKey" to ${CONFIG_PATH}, or run \`xerg login\`.
1920
+ Get your key at https://xerg.ai/dashboard/settings`
1921
+ );
1922
+ }
1923
+
1924
+ // src/transport/ssh.ts
1925
+ import { execSync, spawnSync } from "child_process";
1926
+ import { createHash as createHash2 } from "crypto";
1927
+ import { mkdirSync as mkdirSync4, rmSync as rmSync2 } from "fs";
1928
+ import { homedir as homedir4, tmpdir } from "os";
1929
+ import { join as join5 } from "path";
1930
+ var DEFAULT_GATEWAY_DIR = "/tmp/openclaw";
1931
+ var DEFAULT_SESSIONS_DIR = "~/.openclaw/agents";
1932
+ function hashString(input) {
1933
+ return createHash2("sha256").update(input).digest("hex").slice(0, 12);
1934
+ }
1935
+ function sshArgs(source) {
1936
+ const args = [];
1937
+ if (source.identityFile) {
1938
+ const resolved = source.identityFile.replace(/^~/, homedir4());
1939
+ args.push("-i", resolved);
1940
+ }
1941
+ args.push("-o", "BatchMode=yes", "-o", "ConnectTimeout=10");
1942
+ return args;
1943
+ }
1944
+ function rsyncSshCommand(source) {
1945
+ const parts = ["ssh"];
1946
+ if (source.identityFile) {
1947
+ const resolved = source.identityFile.replace(/^~/, homedir4());
1948
+ parts.push(`-i "${resolved}"`);
1949
+ }
1950
+ parts.push("-o BatchMode=yes", "-o ConnectTimeout=10");
1951
+ return parts.join(" ");
1952
+ }
1953
+ function isRsyncAvailable() {
1954
+ try {
1955
+ spawnSync("rsync", ["--version"], { stdio: "pipe" });
1956
+ return true;
1957
+ } catch {
1958
+ return false;
1959
+ }
1960
+ }
1961
+ function isRemoteRsyncAvailable(source) {
1962
+ try {
1963
+ const result = spawnSync("ssh", [...sshArgs(source), source.host, "which rsync"], {
1964
+ stdio: "pipe",
1965
+ timeout: 15e3
1966
+ });
1967
+ return result.status === 0;
1968
+ } catch {
1969
+ return false;
1970
+ }
1971
+ }
1972
+ function testSshConnectivity(source) {
1973
+ const result = spawnSync("ssh", [...sshArgs(source), source.host, "echo ok"], {
1974
+ stdio: "pipe",
1975
+ timeout: 15e3
1976
+ });
1977
+ if (result.status === 0) {
1978
+ return { ok: true };
1979
+ }
1980
+ const stderr = result.stderr?.toString().trim() || "Connection failed";
1981
+ return { ok: false, error: stderr };
1982
+ }
1983
+ function sshExec(source, command2) {
1984
+ const result = spawnSync("ssh", [...sshArgs(source), source.host, command2], {
1985
+ stdio: "pipe",
1986
+ timeout: 3e4
1987
+ });
1988
+ return {
1989
+ stdout: result.stdout?.toString().trim() ?? "",
1990
+ status: result.status ?? 1
1991
+ };
1992
+ }
1993
+ function buildSinceFind(since) {
1994
+ if (!since) return "";
1995
+ const match = since.trim().toLowerCase().match(/^(\d+)([mhdw])$/);
1996
+ if (!match) return "";
1997
+ const value = Number(match[1]);
1998
+ const unit = match[2];
1999
+ let minutes;
2000
+ switch (unit) {
2001
+ case "m":
2002
+ minutes = value;
2003
+ break;
2004
+ case "h":
2005
+ minutes = value * 60;
2006
+ break;
2007
+ case "d":
2008
+ minutes = value * 60 * 24;
2009
+ break;
2010
+ case "w":
2011
+ minutes = value * 60 * 24 * 7;
2012
+ break;
2013
+ default:
2014
+ return "";
2015
+ }
2016
+ return `-mmin -${minutes}`;
2017
+ }
2018
+ function rsyncPull(opts) {
2019
+ mkdirSync4(opts.localDir, { recursive: true });
2020
+ const args = ["-avz", "--timeout=30", "-e", rsyncSshCommand(opts.source)];
2021
+ if (opts.includes) {
2022
+ for (const pattern of opts.includes) {
2023
+ args.push("--include", pattern);
2024
+ }
2025
+ args.push("--exclude", "*");
2026
+ }
2027
+ if (opts.since) {
2028
+ const findArgs = buildSinceFind(opts.since);
2029
+ if (findArgs) {
2030
+ const fileListCmd = `find ${opts.remotePath} -type f ${findArgs} 2>/dev/null`;
2031
+ const { stdout, status } = sshExec(opts.source, fileListCmd);
2032
+ if (status !== 0 || !stdout) return false;
2033
+ const files = stdout.split("\n").filter(Boolean);
2034
+ if (files.length === 0) return false;
2035
+ const tmpFile = join5(tmpdir(), `xerg-filelist-${hashString(opts.remotePath)}`);
2036
+ const relativePaths = files.map(
2037
+ (f) => f.startsWith(opts.remotePath) ? f.slice(opts.remotePath.length).replace(/^\//, "") : f
2038
+ );
2039
+ execSync(`cat > ${tmpFile} << 'XERGEOF'
2040
+ ${relativePaths.join("\n")}
2041
+ XERGEOF`);
2042
+ args.push("--files-from", tmpFile);
2043
+ }
2044
+ }
2045
+ const remoteSrc = opts.remotePath.endsWith("/") ? `${opts.source.host}:${opts.remotePath}` : `${opts.source.host}:${opts.remotePath}/`;
2046
+ args.push(remoteSrc, `${opts.localDir}/`);
2047
+ const result = spawnSync("rsync", args, { stdio: "pipe", timeout: 12e4 });
2048
+ return result.status === 0;
2049
+ }
2050
+ function tarSshPull(opts) {
2051
+ mkdirSync4(opts.localDir, { recursive: true });
2052
+ const tarCmd = `tar -czf - -C ${opts.remotePath} . 2>/dev/null`;
2053
+ const sshArgsList = sshArgs(opts.source);
2054
+ const fullCmd = `ssh ${sshArgsList.map((a) => `"${a}"`).join(" ")} ${opts.source.host} '${tarCmd}' | tar -xzf - -C ${opts.localDir}`;
2055
+ try {
2056
+ execSync(fullCmd, { stdio: "pipe", timeout: 12e4 });
2057
+ return true;
2058
+ } catch {
2059
+ return false;
2060
+ }
2061
+ }
2062
+ function pullDirectory(opts) {
2063
+ if (opts.useRsync) {
2064
+ const ok = rsyncPull({
2065
+ source: opts.source,
2066
+ remotePath: opts.remotePath,
2067
+ localDir: opts.localDir,
2068
+ includes: opts.includes,
2069
+ since: opts.since
2070
+ });
2071
+ if (ok) return true;
2072
+ }
2073
+ process.stderr.write(
2074
+ opts.useRsync ? "rsync transfer failed, falling back to tar over ssh\n" : "rsync not found, using tar over ssh\n"
2075
+ );
2076
+ return tarSshPull({
2077
+ source: opts.source,
2078
+ remotePath: opts.remotePath,
2079
+ localDir: opts.localDir
2080
+ });
2081
+ }
2082
+ function resolveLocalPath(source, keepFiles) {
2083
+ if (keepFiles) {
2084
+ const cacheDir = join5(homedir4(), ".xerg", "remote-cache", source.name);
2085
+ mkdirSync4(cacheDir, { recursive: true });
2086
+ return cacheDir;
2087
+ }
2088
+ const hash = hashString(`${source.host}:${Date.now()}`);
2089
+ const tmpPath = join5(tmpdir(), `xerg-remote-${hash}`);
2090
+ mkdirSync4(tmpPath, { recursive: true });
2091
+ return tmpPath;
2092
+ }
2093
+ function parseRemoteTarget(target) {
2094
+ const portMatch = target.match(/^(.+):(\d+)$/);
2095
+ if (portMatch) {
2096
+ const userHost = portMatch[1];
2097
+ const port = portMatch[2];
2098
+ const atIndex2 = userHost.indexOf("@");
2099
+ return {
2100
+ user: atIndex2 >= 0 ? userHost.slice(0, atIndex2) : "",
2101
+ host: atIndex2 >= 0 ? userHost.slice(atIndex2 + 1) : userHost,
2102
+ port
2103
+ };
2104
+ }
2105
+ const atIndex = target.indexOf("@");
2106
+ return {
2107
+ user: atIndex >= 0 ? target.slice(0, atIndex) : "",
2108
+ host: atIndex >= 0 ? target.slice(atIndex + 1) : target
2109
+ };
2110
+ }
2111
+ function buildSourceFromFlags(opts) {
2112
+ const parsed = parseRemoteTarget(opts.remote);
2113
+ return {
2114
+ name: parsed.host,
2115
+ transport: "ssh",
2116
+ host: opts.remote,
2117
+ logFile: opts.remoteLogFile,
2118
+ sessionsDir: opts.remoteSessionsDir
2119
+ };
2120
+ }
2121
+ function buildComparisonKeyForRemote(source) {
2122
+ const logPath = source.logFile ?? DEFAULT_GATEWAY_DIR;
2123
+ const sessPath = source.sessionsDir ?? DEFAULT_SESSIONS_DIR;
2124
+ return `${source.host}:${logPath}:${sessPath}`;
2125
+ }
2126
+ async function pullRemoteFiles(opts) {
2127
+ const { source, since, keepFiles = false } = opts;
2128
+ const connectivity = testSshConnectivity(source);
2129
+ if (!connectivity.ok) {
2130
+ throw new Error(
2131
+ `Cannot connect to ${source.host}. Check SSH config and key access.${connectivity.error ? ` (${connectivity.error})` : ""}`
2132
+ );
2133
+ }
2134
+ const useRsync = isRsyncAvailable();
2135
+ const localBase = resolveLocalPath(source, keepFiles);
2136
+ const gatewayDir = join5(localBase, "gateway");
2137
+ const sessionsDir = join5(localBase, "sessions");
2138
+ const remoteLogPath = source.logFile ?? DEFAULT_GATEWAY_DIR;
2139
+ const remoteSessionsPath = source.sessionsDir ?? DEFAULT_SESSIONS_DIR;
2140
+ const { stdout: expandedSessions } = sshExec(source, `eval echo ${remoteSessionsPath}`);
2141
+ const resolvedSessionsPath = expandedSessions || remoteSessionsPath;
2142
+ const { status: logPathExists } = sshExec(source, `test -e ${remoteLogPath} && echo exists`);
2143
+ const { status: sessPathExists } = sshExec(
2144
+ source,
2145
+ `test -e ${resolvedSessionsPath} && echo exists`
2146
+ );
2147
+ let pulledLog = false;
2148
+ let pulledSessions = false;
2149
+ if (logPathExists === 0) {
2150
+ const { stdout: isFile } = sshExec(source, `test -f ${remoteLogPath} && echo file`);
2151
+ if (isFile === "file") {
2152
+ const parentDir = remoteLogPath.slice(0, remoteLogPath.lastIndexOf("/")) || "/tmp";
2153
+ const fileName = remoteLogPath.slice(remoteLogPath.lastIndexOf("/") + 1);
2154
+ pulledLog = pullDirectory({
2155
+ source,
2156
+ remotePath: parentDir,
2157
+ localDir: gatewayDir,
2158
+ includes: [fileName],
2159
+ since,
2160
+ useRsync
2161
+ });
2162
+ } else {
2163
+ pulledLog = pullDirectory({
2164
+ source,
2165
+ remotePath: remoteLogPath,
2166
+ localDir: gatewayDir,
2167
+ includes: ["openclaw-*.log", "*.log"],
2168
+ since,
2169
+ useRsync
2170
+ });
2171
+ }
2172
+ }
2173
+ if (sessPathExists === 0) {
2174
+ pulledSessions = pullDirectory({
2175
+ source,
2176
+ remotePath: resolvedSessionsPath,
2177
+ localDir: sessionsDir,
2178
+ includes: ["**/", "*.jsonl"],
2179
+ since,
2180
+ useRsync
2181
+ });
2182
+ }
2183
+ if (!pulledLog && !pulledSessions) {
2184
+ if (keepFiles) {
2185
+ rmSync2(localBase, { recursive: true, force: true });
2186
+ }
2187
+ throw new Error(
2188
+ `No OpenClaw data found at default paths on ${source.host}. Use --remote-log-file or --remote-sessions-dir.`
2189
+ );
2190
+ }
2191
+ const result = {
2192
+ localPath: localBase,
2193
+ source
2194
+ };
2195
+ if (pulledLog) result.logFile = gatewayDir;
2196
+ if (pulledSessions) result.sessionsDir = sessionsDir;
2197
+ return result;
2198
+ }
2199
+ async function runRemoteDoctor(opts) {
2200
+ const { source } = opts;
2201
+ const notes = [];
2202
+ const connectivity = testSshConnectivity(source);
2203
+ if (!connectivity.ok) {
2204
+ return {
2205
+ host: source.host,
2206
+ sshConnectivity: false,
2207
+ sshError: connectivity.error,
2208
+ rsyncAvailableLocal: false,
2209
+ rsyncAvailableRemote: false,
2210
+ defaultPaths: {
2211
+ gatewayExists: false,
2212
+ gatewayPath: DEFAULT_GATEWAY_DIR,
2213
+ gatewayFileCount: 0,
2214
+ gatewayTotalBytes: 0,
2215
+ sessionsExists: false,
2216
+ sessionsPath: DEFAULT_SESSIONS_DIR,
2217
+ sessionsFileCount: 0,
2218
+ sessionsTotalBytes: 0
2219
+ },
2220
+ notes: [
2221
+ `Cannot connect to ${source.host}. Check SSH config and key access.${connectivity.error ? ` (${connectivity.error})` : ""}`
2222
+ ]
2223
+ };
2224
+ }
2225
+ notes.push("SSH connectivity: OK");
2226
+ const rsyncLocal = isRsyncAvailable();
2227
+ const rsyncRemote = isRemoteRsyncAvailable(source);
2228
+ notes.push(`rsync available locally: ${rsyncLocal ? "yes" : "no"}`);
2229
+ notes.push(`rsync available on remote: ${rsyncRemote ? "yes" : "no"}`);
2230
+ if (!rsyncLocal || !rsyncRemote) {
2231
+ notes.push("tar over ssh fallback will be used for file transfer");
2232
+ }
2233
+ function checkPath(remotePath) {
2234
+ const { stdout: expanded } = sshExec(source, `eval echo ${remotePath}`);
2235
+ const resolved = expanded || remotePath;
2236
+ const { status: exists } = sshExec(source, `test -e ${resolved} && echo exists`);
2237
+ if (exists !== 0) {
2238
+ return { exists: false, path: resolved, fileCount: 0, totalBytes: 0 };
2239
+ }
2240
+ const { stdout: countOut } = sshExec(source, `find ${resolved} -type f 2>/dev/null | wc -l`);
2241
+ const { stdout: sizeOut } = sshExec(source, `du -sb ${resolved} 2>/dev/null | cut -f1`);
2242
+ return {
2243
+ exists: true,
2244
+ path: resolved,
2245
+ fileCount: Number.parseInt(countOut, 10) || 0,
2246
+ totalBytes: Number.parseInt(sizeOut, 10) || 0
2247
+ };
2248
+ }
2249
+ const gateway = checkPath(DEFAULT_GATEWAY_DIR);
2250
+ const sessions = checkPath(DEFAULT_SESSIONS_DIR);
2251
+ if (gateway.exists) {
2252
+ notes.push(
2253
+ `Gateway logs found at ${gateway.path}: ${gateway.fileCount} files, ${formatBytes(gateway.totalBytes)}`
2254
+ );
2255
+ } else {
2256
+ notes.push(`No gateway logs at ${gateway.path}`);
2257
+ }
2258
+ if (sessions.exists) {
2259
+ notes.push(
2260
+ `Sessions found at ${sessions.path}: ${sessions.fileCount} files, ${formatBytes(sessions.totalBytes)}`
2261
+ );
2262
+ } else {
2263
+ notes.push(`No sessions at ${sessions.path}`);
2264
+ }
2265
+ const report = {
2266
+ host: source.host,
2267
+ sshConnectivity: true,
2268
+ rsyncAvailableLocal: rsyncLocal,
2269
+ rsyncAvailableRemote: rsyncRemote,
2270
+ defaultPaths: {
2271
+ gatewayExists: gateway.exists,
2272
+ gatewayPath: gateway.path,
2273
+ gatewayFileCount: gateway.fileCount,
2274
+ gatewayTotalBytes: gateway.totalBytes,
2275
+ sessionsExists: sessions.exists,
2276
+ sessionsPath: sessions.path,
2277
+ sessionsFileCount: sessions.fileCount,
2278
+ sessionsTotalBytes: sessions.totalBytes
2279
+ },
2280
+ notes
2281
+ };
2282
+ if (source.logFile || source.sessionsDir) {
2283
+ const logCheck = source.logFile ? checkPath(source.logFile) : null;
2284
+ const sessCheck = source.sessionsDir ? checkPath(source.sessionsDir) : null;
2285
+ report.customPaths = {
2286
+ logFileExists: logCheck?.exists ?? false,
2287
+ logFilePath: source.logFile ?? "",
2288
+ logFileBytes: logCheck?.totalBytes ?? 0,
2289
+ sessionsDirExists: sessCheck?.exists ?? false,
2290
+ sessionsDirPath: source.sessionsDir ?? "",
2291
+ sessionsFileCount: sessCheck?.fileCount ?? 0,
2292
+ sessionsTotalBytes: sessCheck?.totalBytes ?? 0
2293
+ };
2294
+ if (logCheck?.exists) {
2295
+ notes.push(`Custom log path ${source.logFile}: ${formatBytes(logCheck.totalBytes)}`);
2296
+ } else if (source.logFile) {
2297
+ notes.push(`Custom log path ${source.logFile}: not found`);
2298
+ }
2299
+ if (sessCheck?.exists) {
2300
+ notes.push(
2301
+ `Custom sessions path ${source.sessionsDir}: ${sessCheck.fileCount} files, ${formatBytes(sessCheck.totalBytes)}`
2302
+ );
2303
+ } else if (source.sessionsDir) {
2304
+ notes.push(`Custom sessions path ${source.sessionsDir}: not found`);
2305
+ }
2306
+ }
2307
+ return report;
2308
+ }
2309
+ function formatBytes(bytes) {
2310
+ if (bytes === 0) return "0 B";
2311
+ const units = ["B", "KB", "MB", "GB"];
2312
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
2313
+ const value = bytes / 1024 ** i;
2314
+ return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
2315
+ }
2316
+
2317
+ // src/transport/railway.ts
2318
+ import { execSync as execSync2, spawnSync as spawnSync2 } from "child_process";
2319
+ import { createHash as createHash3 } from "crypto";
2320
+ import { mkdirSync as mkdirSync5, rmSync as rmSync3 } from "fs";
2321
+ import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
2322
+ import { join as join6 } from "path";
2323
+ var DEFAULT_GATEWAY_DIR2 = "/tmp/openclaw";
2324
+ var DEFAULT_SESSIONS_DIR2 = "~/.openclaw/agents";
2325
+ var ALTERNATE_SESSION_PATHS = ["/data/.clawdbot/agents/main/sessions"];
2326
+ function hashString2(input) {
2327
+ return createHash3("sha256").update(input).digest("hex").slice(0, 12);
2328
+ }
2329
+ function railwaySshArgs(target) {
2330
+ const args = ["ssh"];
2331
+ if (target) {
2332
+ args.push(`--project=${target.projectId}`);
2333
+ args.push(`--environment=${target.environmentId}`);
2334
+ args.push(`--service=${target.serviceId}`);
2335
+ }
2336
+ return args;
2337
+ }
2338
+ function railwayExec(command2, target) {
2339
+ const args = [...railwaySshArgs(target), command2];
2340
+ const result = spawnSync2("railway", args, { stdio: "pipe", timeout: 3e4 });
2341
+ return {
2342
+ stdout: result.stdout?.toString().trim() ?? "",
2343
+ status: result.status ?? 1
2344
+ };
2345
+ }
2346
+ function buildSinceFind2(since) {
2347
+ if (!since) return "";
2348
+ const match = since.trim().toLowerCase().match(/^(\d+)([mhdw])$/);
2349
+ if (!match) return "";
2350
+ const value = Number(match[1]);
2351
+ const unit = match[2];
2352
+ let minutes;
2353
+ switch (unit) {
2354
+ case "m":
2355
+ minutes = value;
2356
+ break;
2357
+ case "h":
2358
+ minutes = value * 60;
2359
+ break;
2360
+ case "d":
2361
+ minutes = value * 60 * 24;
2362
+ break;
2363
+ case "w":
2364
+ minutes = value * 60 * 24 * 7;
2365
+ break;
2366
+ default:
2367
+ return "";
2368
+ }
2369
+ return `-mmin -${minutes}`;
2370
+ }
2371
+ function tarRailwayPull(opts) {
2372
+ mkdirSync5(opts.localDir, { recursive: true });
2373
+ let tarCmd;
2374
+ if (opts.since) {
2375
+ const findArgs = buildSinceFind2(opts.since);
2376
+ if (findArgs) {
2377
+ tarCmd = `find ${opts.remotePath} -type f ${findArgs} -print0 2>/dev/null | tar -czf - --null -T - 2>/dev/null | base64`;
2378
+ } else {
2379
+ tarCmd = `tar -czf - -C ${opts.remotePath} . 2>/dev/null | base64`;
2380
+ }
2381
+ } else {
2382
+ tarCmd = `tar -czf - -C ${opts.remotePath} . 2>/dev/null | base64`;
2383
+ }
2384
+ const sshArgs2 = railwaySshArgs(opts.target).join(" ");
2385
+ const fullCmd = `railway ${sshArgs2} '${tarCmd}' | base64 -d | tar -xzf - -C ${opts.localDir}`;
2386
+ try {
2387
+ execSync2(fullCmd, { stdio: "pipe", timeout: 12e4 });
2388
+ return true;
2389
+ } catch {
2390
+ return false;
2391
+ }
2392
+ }
2393
+ function resolveLocalPath2(source, keepFiles) {
2394
+ if (keepFiles) {
2395
+ const cacheDir = join6(homedir5(), ".xerg", "remote-cache", source.name);
2396
+ mkdirSync5(cacheDir, { recursive: true });
2397
+ return cacheDir;
2398
+ }
2399
+ const identity = source.railway ? `railway:${source.railway.projectId}:${Date.now()}` : `${source.name}:${Date.now()}`;
2400
+ const hash = hashString2(identity);
2401
+ const tmpPath = join6(tmpdir2(), `xerg-remote-${hash}`);
2402
+ mkdirSync5(tmpPath, { recursive: true });
2403
+ return tmpPath;
2404
+ }
2405
+ function checkRemotePath(remotePath, target) {
2406
+ const { status: exists } = railwayExec(`test -e ${remotePath} && echo exists`, target);
2407
+ if (exists !== 0) {
2408
+ return { exists: false, path: remotePath, fileCount: 0, totalBytes: 0 };
2409
+ }
2410
+ const { stdout: countOut } = railwayExec(
2411
+ `find ${remotePath} -type f 2>/dev/null | wc -l`,
2412
+ target
2413
+ );
2414
+ const { stdout: sizeOut } = railwayExec(`du -sb ${remotePath} 2>/dev/null | cut -f1`, target);
2415
+ return {
2416
+ exists: true,
2417
+ path: remotePath,
2418
+ fileCount: Number.parseInt(countOut, 10) || 0,
2419
+ totalBytes: Number.parseInt(sizeOut, 10) || 0
2420
+ };
2421
+ }
2422
+ function findSessionsPath(target, customPath) {
2423
+ if (customPath) {
2424
+ const check = checkRemotePath(customPath, target);
2425
+ return check.exists ? customPath : null;
2426
+ }
2427
+ const { stdout: expandedDefault } = railwayExec(`eval echo ${DEFAULT_SESSIONS_DIR2}`, target);
2428
+ const defaultPath = expandedDefault || DEFAULT_SESSIONS_DIR2;
2429
+ const defaultCheck = checkRemotePath(defaultPath, target);
2430
+ if (defaultCheck.exists && defaultCheck.fileCount > 0) {
2431
+ return defaultPath;
2432
+ }
2433
+ for (const altPath of ALTERNATE_SESSION_PATHS) {
2434
+ const check = checkRemotePath(altPath, target);
2435
+ if (check.exists && check.fileCount > 0) {
2436
+ return altPath;
2437
+ }
2438
+ }
2439
+ return null;
2440
+ }
2441
+ function buildRailwaySourceFromFlags(opts) {
2442
+ const name = opts.railway ? `railway-${opts.railway.serviceId.slice(0, 8)}` : "railway-linked";
2443
+ return {
2444
+ name,
2445
+ transport: "railway",
2446
+ host: name,
2447
+ logFile: opts.remoteLogFile,
2448
+ sessionsDir: opts.remoteSessionsDir,
2449
+ railway: opts.railway
2450
+ };
2451
+ }
2452
+ function buildComparisonKeyForRailway(source) {
2453
+ const logPath = source.logFile ?? DEFAULT_GATEWAY_DIR2;
2454
+ const sessPath = source.sessionsDir ?? DEFAULT_SESSIONS_DIR2;
2455
+ if (source.railway) {
2456
+ return `railway:${source.railway.projectId}:${source.railway.environmentId}:${source.railway.serviceId}:${logPath}:${sessPath}`;
2457
+ }
2458
+ return `railway-linked:${logPath}:${sessPath}`;
2459
+ }
2460
+ async function pullRemoteFilesRailway(opts) {
2461
+ const { source, since, keepFiles = false } = opts;
2462
+ const target = source.railway;
2463
+ const { status } = railwayExec("echo ok", target);
2464
+ if (status !== 0) {
2465
+ throw new Error(
2466
+ `Cannot reach Railway service${target ? ` (project: ${target.projectId})` : " (linked project)"}. Check railway CLI auth and service configuration.`
2467
+ );
2468
+ }
2469
+ const localBase = resolveLocalPath2(source, keepFiles);
2470
+ const gatewayDir = join6(localBase, "gateway");
2471
+ const sessionsDir = join6(localBase, "sessions");
2472
+ const remoteLogPath = source.logFile ?? DEFAULT_GATEWAY_DIR2;
2473
+ const logCheck = checkRemotePath(remoteLogPath, target);
2474
+ const resolvedSessionsPath = findSessionsPath(target, source.sessionsDir);
2475
+ let pulledLog = false;
2476
+ let pulledSessions = false;
2477
+ if (logCheck.exists) {
2478
+ const { stdout: isFile } = railwayExec(`test -f ${remoteLogPath} && echo file`, target);
2479
+ if (isFile === "file") {
2480
+ const parentDir = remoteLogPath.slice(0, remoteLogPath.lastIndexOf("/")) || "/tmp";
2481
+ pulledLog = tarRailwayPull({
2482
+ target,
2483
+ remotePath: parentDir,
2484
+ localDir: gatewayDir,
2485
+ since
2486
+ });
2487
+ } else {
2488
+ pulledLog = tarRailwayPull({
2489
+ target,
2490
+ remotePath: remoteLogPath,
2491
+ localDir: gatewayDir,
2492
+ since
2493
+ });
2494
+ }
2495
+ }
2496
+ if (resolvedSessionsPath) {
2497
+ pulledSessions = tarRailwayPull({
2498
+ target,
2499
+ remotePath: resolvedSessionsPath,
2500
+ localDir: sessionsDir,
2501
+ since
2502
+ });
2503
+ }
2504
+ if (!pulledLog && !pulledSessions) {
2505
+ if (keepFiles) {
2506
+ rmSync3(localBase, { recursive: true, force: true });
2507
+ }
2508
+ const checkedPaths = [remoteLogPath, DEFAULT_SESSIONS_DIR2, ...ALTERNATE_SESSION_PATHS].join(
2509
+ ", "
2510
+ );
2511
+ throw new Error(
2512
+ `No OpenClaw data found on Railway service. Checked: ${checkedPaths}. Use --remote-log-file or --remote-sessions-dir to specify custom paths.`
2513
+ );
2514
+ }
2515
+ const result = {
2516
+ localPath: localBase,
2517
+ source
2518
+ };
2519
+ if (pulledLog) result.logFile = gatewayDir;
2520
+ if (pulledSessions) result.sessionsDir = sessionsDir;
2521
+ return result;
2522
+ }
2523
+ async function runRailwayDoctor(opts) {
2524
+ const { source } = opts;
2525
+ const target = source.railway;
2526
+ const notes = [];
2527
+ const whichCheck = spawnSync2("which", ["railway"], { stdio: "pipe", timeout: 5e3 });
2528
+ const railwayCliInstalled = whichCheck.status === 0;
2529
+ if (!railwayCliInstalled) {
2530
+ return {
2531
+ transport: "railway",
2532
+ name: source.name,
2533
+ railwayCliInstalled: false,
2534
+ railwayAuthenticated: false,
2535
+ serviceReachable: false,
2536
+ defaultPaths: emptyDefaultPaths(),
2537
+ alternateSessionPaths: [],
2538
+ notes: ["Railway CLI is not installed. Install it: npm i -g @railway/cli"]
2539
+ };
2540
+ }
2541
+ const railwayPath = whichCheck.stdout?.toString().trim() ?? "railway";
2542
+ const versionCheck = spawnSync2("railway", ["version"], { stdio: "pipe", timeout: 1e4 });
2543
+ const versionStr = versionCheck.status === 0 ? versionCheck.stdout?.toString().trim() : railwayPath;
2544
+ notes.push(`Railway CLI: installed (${versionStr})`);
2545
+ const whoami = spawnSync2("railway", ["whoami"], { stdio: "pipe", timeout: 1e4 });
2546
+ const railwayAuthenticated = whoami.status === 0;
2547
+ const railwayAuthUser = railwayAuthenticated ? whoami.stdout?.toString().trim() : void 0;
2548
+ if (!railwayAuthenticated) {
2549
+ return {
2550
+ transport: "railway",
2551
+ name: source.name,
2552
+ railwayCliInstalled: true,
2553
+ railwayAuthenticated: false,
2554
+ serviceReachable: false,
2555
+ defaultPaths: emptyDefaultPaths(),
2556
+ alternateSessionPaths: [],
2557
+ notes: [...notes, "Not authenticated. Run: railway login"]
2558
+ };
2559
+ }
2560
+ notes.push(`Authenticated as: ${railwayAuthUser}`);
2561
+ const { status: reachStatus } = railwayExec("echo ok", target);
2562
+ const serviceReachable = reachStatus === 0;
2563
+ if (!serviceReachable) {
2564
+ return {
2565
+ transport: "railway",
2566
+ name: source.name,
2567
+ railwayCliInstalled: true,
2568
+ railwayAuthenticated: true,
2569
+ railwayAuthUser,
2570
+ serviceReachable: false,
2571
+ serviceError: target ? `Cannot reach service ${target.serviceId}` : "Cannot reach linked service. Run: railway link",
2572
+ defaultPaths: emptyDefaultPaths(),
2573
+ alternateSessionPaths: [],
2574
+ notes: [
2575
+ ...notes,
2576
+ target ? `Service unreachable (project: ${target.projectId}, service: ${target.serviceId})` : "Service unreachable. Ensure a project is linked with: railway link"
2577
+ ]
2578
+ };
2579
+ }
2580
+ notes.push("Service connectivity: OK");
2581
+ const gateway = checkRemotePath(DEFAULT_GATEWAY_DIR2, target);
2582
+ const { stdout: expandedDefault } = railwayExec(`eval echo ${DEFAULT_SESSIONS_DIR2}`, target);
2583
+ const resolvedDefault = expandedDefault || DEFAULT_SESSIONS_DIR2;
2584
+ const sessions = checkRemotePath(resolvedDefault, target);
2585
+ if (gateway.exists) {
2586
+ notes.push(
2587
+ `Gateway logs at ${gateway.path}: ${gateway.fileCount} files, ${formatBytes2(gateway.totalBytes)}`
2588
+ );
2589
+ } else {
2590
+ notes.push(`No gateway logs at ${DEFAULT_GATEWAY_DIR2}`);
2591
+ }
2592
+ if (sessions.exists) {
2593
+ notes.push(
2594
+ `Sessions at ${sessions.path}: ${sessions.fileCount} files, ${formatBytes2(sessions.totalBytes)}`
2595
+ );
2596
+ } else {
2597
+ notes.push(`No sessions at ${resolvedDefault}`);
2598
+ }
2599
+ const alternateSessionPaths = ALTERNATE_SESSION_PATHS.map((altPath) => {
2600
+ const check = checkRemotePath(altPath, target);
2601
+ if (check.exists) {
2602
+ notes.push(
2603
+ `Alternate sessions at ${altPath}: ${check.fileCount} files, ${formatBytes2(check.totalBytes)}`
2604
+ );
2605
+ } else {
2606
+ notes.push(`No alternate sessions at ${altPath}`);
2607
+ }
2608
+ return {
2609
+ path: altPath,
2610
+ exists: check.exists,
2611
+ fileCount: check.fileCount,
2612
+ totalBytes: check.totalBytes
2613
+ };
2614
+ });
2615
+ const report = {
2616
+ transport: "railway",
2617
+ name: source.name,
2618
+ railwayCliInstalled: true,
2619
+ railwayAuthenticated: true,
2620
+ railwayAuthUser,
2621
+ serviceReachable: true,
2622
+ defaultPaths: {
2623
+ gatewayExists: gateway.exists,
2624
+ gatewayPath: gateway.path,
2625
+ gatewayFileCount: gateway.fileCount,
2626
+ gatewayTotalBytes: gateway.totalBytes,
2627
+ sessionsExists: sessions.exists,
2628
+ sessionsPath: sessions.path,
2629
+ sessionsFileCount: sessions.fileCount,
2630
+ sessionsTotalBytes: sessions.totalBytes
2631
+ },
2632
+ alternateSessionPaths,
2633
+ notes
2634
+ };
2635
+ if (source.logFile || source.sessionsDir) {
2636
+ const logCheck = source.logFile ? checkRemotePath(source.logFile, target) : null;
2637
+ const sessCheck = source.sessionsDir ? checkRemotePath(source.sessionsDir, target) : null;
2638
+ report.customPaths = {
2639
+ logFileExists: logCheck?.exists ?? false,
2640
+ logFilePath: source.logFile ?? "",
2641
+ logFileBytes: logCheck?.totalBytes ?? 0,
2642
+ sessionsDirExists: sessCheck?.exists ?? false,
2643
+ sessionsDirPath: source.sessionsDir ?? "",
2644
+ sessionsFileCount: sessCheck?.fileCount ?? 0,
2645
+ sessionsTotalBytes: sessCheck?.totalBytes ?? 0
2646
+ };
2647
+ if (logCheck?.exists) {
2648
+ notes.push(`Custom log path ${source.logFile}: ${formatBytes2(logCheck.totalBytes)}`);
2649
+ } else if (source.logFile) {
2650
+ notes.push(`Custom log path ${source.logFile}: not found`);
2651
+ }
2652
+ if (sessCheck?.exists) {
2653
+ notes.push(
2654
+ `Custom sessions path ${source.sessionsDir}: ${sessCheck.fileCount} files, ${formatBytes2(sessCheck.totalBytes)}`
2655
+ );
2656
+ } else if (source.sessionsDir) {
2657
+ notes.push(`Custom sessions path ${source.sessionsDir}: not found`);
2658
+ }
2659
+ }
2660
+ return report;
2661
+ }
2662
+ function emptyDefaultPaths() {
2663
+ return {
2664
+ gatewayExists: false,
2665
+ gatewayPath: DEFAULT_GATEWAY_DIR2,
2666
+ gatewayFileCount: 0,
2667
+ gatewayTotalBytes: 0,
2668
+ sessionsExists: false,
2669
+ sessionsPath: DEFAULT_SESSIONS_DIR2,
2670
+ sessionsFileCount: 0,
2671
+ sessionsTotalBytes: 0
2672
+ };
2673
+ }
2674
+ function formatBytes2(bytes) {
2675
+ if (bytes === 0) return "0 B";
2676
+ const units = ["B", "KB", "MB", "GB"];
2677
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
2678
+ const value = bytes / 1024 ** i;
2679
+ return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
2680
+ }
2681
+
2682
+ // src/transport/config.ts
2683
+ import { readFileSync as readFileSync4 } from "fs";
2684
+ import { resolve as resolve2 } from "path";
2685
+ function loadRemoteConfig(configPath) {
2686
+ const resolved = resolve2(configPath);
2687
+ let raw;
2688
+ try {
2689
+ raw = readFileSync4(resolved, "utf8");
2690
+ } catch {
2691
+ throw new Error(`Cannot read remote config at ${resolved}`);
2692
+ }
2693
+ let parsed;
2694
+ try {
2695
+ parsed = JSON.parse(raw);
2696
+ } catch {
2697
+ throw new Error(`Invalid JSON in remote config at ${resolved}`);
2698
+ }
2699
+ const config = parsed;
2700
+ if (!config.remotes || !Array.isArray(config.remotes)) {
2701
+ throw new Error('Remote config must have a "remotes" array');
2702
+ }
2703
+ if (config.remotes.length === 0) {
2704
+ throw new Error('Remote config "remotes" array is empty');
2705
+ }
2706
+ return config.remotes.map((entry) => validateAndNormalize(entry));
2707
+ }
2708
+ function validateAndNormalize(entry) {
2709
+ if (!entry.name || typeof entry.name !== "string") {
2710
+ throw new Error('Each remote must have a "name" string');
2711
+ }
2712
+ const transport = entry.transport ?? "ssh";
2713
+ if (transport !== "ssh" && transport !== "railway") {
2714
+ throw new Error(
2715
+ `Remote "${entry.name}" has invalid transport "${transport}". Use "ssh" or "railway".`
2716
+ );
2717
+ }
2718
+ if (transport === "railway") {
2719
+ return validateRailwayEntry(entry);
2720
+ }
2721
+ return validateSshEntry(entry);
2722
+ }
2723
+ function validateSshEntry(entry) {
2724
+ if (!entry.host || typeof entry.host !== "string") {
2725
+ throw new Error(`Remote "${entry.name}" must have a "host" string`);
2726
+ }
2727
+ return {
2728
+ name: entry.name,
2729
+ transport: "ssh",
2730
+ host: entry.host,
2731
+ logFile: entry.logFile,
2732
+ sessionsDir: entry.sessionsDir,
2733
+ identityFile: entry.identityFile
2734
+ };
2735
+ }
2736
+ function validateRailwayEntry(entry) {
2737
+ if (!entry.railway || typeof entry.railway !== "object") {
2738
+ throw new Error(`Remote "${entry.name}" with transport "railway" must have a "railway" object`);
2739
+ }
2740
+ const { projectId, environmentId, serviceId } = entry.railway;
2741
+ if (!projectId || typeof projectId !== "string") {
2742
+ throw new Error(`Remote "${entry.name}" railway config must have a "projectId" string`);
2743
+ }
2744
+ if (!environmentId || typeof environmentId !== "string") {
2745
+ throw new Error(`Remote "${entry.name}" railway config must have an "environmentId" string`);
2746
+ }
2747
+ if (!serviceId || typeof serviceId !== "string") {
2748
+ throw new Error(`Remote "${entry.name}" railway config must have a "serviceId" string`);
2749
+ }
2750
+ return {
2751
+ name: entry.name,
2752
+ transport: "railway",
2753
+ host: `railway-${serviceId.slice(0, 8)}`,
2754
+ logFile: entry.logFile,
2755
+ sessionsDir: entry.sessionsDir,
2756
+ railway: { projectId, environmentId, serviceId }
2757
+ };
2758
+ }
2759
+
2760
+ // src/commands/audit.ts
2761
+ var NO_DATA_PATTERN = /no openclaw sources were detected/i;
2762
+ async function auditOrNoData(...args) {
2763
+ try {
2764
+ return await auditOpenClaw(...args);
2765
+ } catch (err) {
2766
+ if (err instanceof Error && NO_DATA_PATTERN.test(err.message)) {
2767
+ throw new NoDataError(err.message);
2768
+ }
2769
+ throw err;
2770
+ }
2771
+ }
2772
+ async function runAuditCommand(options) {
2773
+ if (options.dryRun && !options.push) {
2774
+ throw new Error("--dry-run requires --push.");
2775
+ }
2776
+ const remoteFlags = [options.remote, options.remoteConfig, options.railway].filter(
2777
+ Boolean
2778
+ ).length;
2779
+ if (remoteFlags > 1) {
2780
+ throw new Error("Use only one of --remote, --remote-config, or --railway.");
2781
+ }
2782
+ if (!options.remote && !options.remoteConfig && !options.railway) {
2783
+ return runLocalAudit(options);
2784
+ }
2785
+ if (options.railway) {
2786
+ const railwayTarget = buildRailwayTarget(options);
2787
+ const source2 = buildRailwaySourceFromFlags({
2788
+ railway: railwayTarget,
2789
+ remoteLogFile: options.remoteLogFile,
2790
+ remoteSessionsDir: options.remoteSessionsDir
2791
+ });
2792
+ return runSingleRemoteAudit(source2, options);
2793
+ }
2794
+ if (options.remoteConfig) {
2795
+ const sources = loadRemoteConfig(options.remoteConfig);
2796
+ if (sources.length === 1) {
2797
+ return runSingleRemoteAudit(sources[0], options);
2798
+ }
2799
+ return runMultiRemoteAudit(sources, options);
2800
+ }
2801
+ const remote = options.remote;
2802
+ const source = buildSourceFromFlags({
2803
+ remote,
2804
+ remoteLogFile: options.remoteLogFile,
2805
+ remoteSessionsDir: options.remoteSessionsDir
2806
+ });
2807
+ return runSingleRemoteAudit(source, options);
2808
+ }
2809
+ function buildRailwayTarget(options) {
2810
+ if (options.railwayProject && options.railwayEnvironment && options.railwayService) {
2811
+ return {
2812
+ projectId: options.railwayProject,
2813
+ environmentId: options.railwayEnvironment,
2814
+ serviceId: options.railwayService
2815
+ };
2816
+ }
2817
+ return void 0;
2818
+ }
2819
+ async function runLocalAudit(options) {
2820
+ const summary = await auditOrNoData({
2821
+ logFile: options.logFile,
2822
+ sessionsDir: options.sessionsDir,
2823
+ since: options.since,
2824
+ compare: options.compare,
2825
+ dbPath: options.db,
2826
+ noDb: options.noDb
2827
+ });
2828
+ renderOutput(summary, options);
2829
+ if (options.push) {
2830
+ const meta = buildMeta({ environment: "local", sourceId: hostname(), sourceHost: hostname() });
2831
+ await handlePush(summary, meta, options);
2832
+ }
2833
+ checkThresholds(summary, options);
2834
+ }
2835
+ function getComparisonKey(source) {
2836
+ if (source.transport === "railway") {
2837
+ return buildComparisonKeyForRailway(source);
2838
+ }
2839
+ return buildComparisonKeyForRemote(source);
2840
+ }
2841
+ function pullFiles(source, since, keepFiles) {
2842
+ if (source.transport === "railway") {
2843
+ return pullRemoteFilesRailway({ source, since, keepFiles });
2844
+ }
2845
+ return pullRemoteFiles({ source, since, keepFiles });
2846
+ }
2847
+ function describeSource(source) {
2848
+ if (source.transport === "railway") {
2849
+ return source.railway ? `${source.name} (Railway service ${source.railway.serviceId.slice(0, 8)})` : `${source.name} (Railway linked project)`;
2850
+ }
2851
+ return source.host;
2852
+ }
2853
+ function sourceEnvironment(source) {
2854
+ return source.transport === "railway" ? "railway" : "remote";
2855
+ }
2856
+ async function runSingleRemoteAudit(source, options) {
2857
+ process.stderr.write(`Pulling files from ${describeSource(source)}...
2858
+ `);
2859
+ const pullResult = await pullFiles(source, options.since, options.keepRemoteFiles);
2860
+ try {
2861
+ const comparisonKeyOverride = getComparisonKey(source);
2862
+ const summary = await auditOrNoData({
2863
+ logFile: pullResult.logFile,
2864
+ sessionsDir: pullResult.sessionsDir,
2865
+ since: options.since,
2866
+ compare: options.compare,
2867
+ dbPath: options.db,
2868
+ noDb: options.noDb,
2869
+ comparisonKeyOverride
2870
+ });
2871
+ renderOutput(summary, options);
2872
+ if (options.push) {
2873
+ const meta = buildMeta({
2874
+ environment: sourceEnvironment(source),
2875
+ sourceId: source.name,
2876
+ sourceHost: source.host
2877
+ });
2878
+ await handlePush(summary, meta, options);
2879
+ }
2880
+ checkThresholds(summary, options);
2881
+ } finally {
2882
+ cleanupPullResult(pullResult, options.keepRemoteFiles);
2883
+ }
2884
+ }
2885
+ async function runMultiRemoteAudit(sources, options) {
2886
+ const results = [];
2887
+ const errors = [];
2888
+ for (const source of sources) {
2889
+ process.stderr.write(`Pulling files from ${source.name} (${describeSource(source)})...
2890
+ `);
2891
+ try {
2892
+ const pullResult = await pullFiles(source, options.since, options.keepRemoteFiles);
2893
+ results.push({ source, pullResult });
2894
+ } catch (err) {
2895
+ const message = err instanceof Error ? err.message : "Unknown error";
2896
+ errors.push({ source, error: message });
2897
+ process.stderr.write(` Warning: ${message}
2898
+ `);
2899
+ }
2900
+ }
2901
+ if (results.length === 0) {
2902
+ const errorMessages = errors.map((e) => ` ${e.source.name}: ${e.error}`).join("\n");
2903
+ throw new Error(`No sources could be pulled:
2904
+ ${errorMessages}`);
2905
+ }
2906
+ try {
2907
+ const summaries = [];
2908
+ for (const { source, pullResult } of results) {
2909
+ const comparisonKeyOverride = getComparisonKey(source);
2910
+ const summary = await auditOrNoData({
2911
+ logFile: pullResult.logFile,
2912
+ sessionsDir: pullResult.sessionsDir,
2913
+ since: options.since,
2914
+ compare: options.compare,
2915
+ dbPath: options.db,
2916
+ noDb: options.noDb,
2917
+ comparisonKeyOverride
2918
+ });
2919
+ summaries.push({ name: source.name, source, summary });
2920
+ }
2921
+ if (options.json) {
2922
+ const output = summaries.length === 1 ? { ...summaries[0].summary, recommendations: buildRecommendations(summaries[0].summary) } : {
2923
+ sources: summaries.map((s) => ({
2924
+ name: s.name,
2925
+ ...s.summary,
2926
+ recommendations: buildRecommendations(s.summary)
2927
+ }))
2928
+ };
2929
+ process.stdout.write(`${JSON.stringify(output, null, 2)}
2930
+ `);
2931
+ } else {
2932
+ for (const { name, summary } of summaries) {
2933
+ if (summaries.length > 1) {
2934
+ process.stdout.write(`
2935
+ ${"\u2550".repeat(60)}
2936
+ Source: ${name}
2937
+ ${"\u2550".repeat(60)}
2938
+
2939
+ `);
2940
+ }
2941
+ if (options.markdown) {
2942
+ process.stdout.write(`${renderMarkdownSummary(summary)}
2943
+ `);
2944
+ } else {
2945
+ process.stdout.write(`${renderTerminalSummary(summary)}
2946
+ `);
2947
+ }
2948
+ }
2949
+ }
2950
+ if (errors.length > 0) {
2951
+ process.stderr.write("\nSources that could not be reached:\n");
2952
+ for (const { source, error } of errors) {
2953
+ process.stderr.write(` ${source.name}: ${error}
2954
+ `);
2955
+ }
2956
+ }
2957
+ if (options.push) {
2958
+ for (const { source, summary } of summaries) {
2959
+ const meta = buildMeta({
2960
+ environment: sourceEnvironment(source),
2961
+ sourceId: source.name,
2962
+ sourceHost: source.host
2963
+ });
2964
+ await handlePush(summary, meta, options);
2965
+ }
2966
+ }
2967
+ for (const { summary } of summaries) {
2968
+ checkThresholds(summary, options);
2969
+ }
2970
+ } finally {
2971
+ for (const { pullResult } of results) {
2972
+ cleanupPullResult(pullResult, options.keepRemoteFiles);
2973
+ }
2974
+ }
2975
+ }
2976
+ function readCliVersion() {
2977
+ try {
2978
+ const packageJsonPath = new URL("../../package.json", import.meta.url);
2979
+ const pkg = JSON.parse(readFileSync5(packageJsonPath, "utf8"));
2980
+ return pkg.version ?? "0.0.0";
2981
+ } catch {
2982
+ return "0.0.0";
2983
+ }
2984
+ }
2985
+ function buildMeta(input) {
2986
+ return {
2987
+ cliVersion: readCliVersion(),
2988
+ sourceId: input.sourceId,
2989
+ sourceHost: input.sourceHost,
2990
+ environment: input.environment
2991
+ };
2992
+ }
2993
+ async function handlePush(summary, meta, options) {
2994
+ const payload = toWirePayload(summary, meta);
2995
+ if (options.dryRun) {
2996
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
2997
+ `);
2998
+ return;
2999
+ }
3000
+ const config = loadPushConfig();
3001
+ process.stderr.write(`Pushing audit ${summary.auditId} to ${config.apiUrl}...
3002
+ `);
3003
+ const result = await pushAudit(payload, config);
3004
+ if (result.ok) {
3005
+ process.stderr.write(`Pushed successfully (audit: ${result.auditId}).
3006
+ `);
3007
+ } else {
3008
+ const statusInfo = result.status > 0 ? ` (HTTP ${result.status})` : "";
3009
+ throw new Error(`Push failed${statusInfo}: ${result.message}`);
3010
+ }
3011
+ }
3012
+ function renderOutput(summary, options) {
3013
+ if (options.push && options.dryRun) {
3014
+ return;
3015
+ }
3016
+ if (options.json) {
3017
+ const recommendations = buildRecommendations(summary);
3018
+ const output = { ...summary, recommendations };
3019
+ process.stdout.write(`${JSON.stringify(output, null, 2)}
3020
+ `);
3021
+ return;
3022
+ }
3023
+ if (options.markdown) {
3024
+ process.stdout.write(`${renderMarkdownSummary(summary)}
3025
+ `);
3026
+ return;
3027
+ }
3028
+ process.stdout.write(`${renderTerminalSummary(summary)}
3029
+ `);
3030
+ }
3031
+ function checkThresholds(summary, options) {
3032
+ const breaches = [];
3033
+ if (options.failAboveWasteRate !== void 0 && summary.structuralWasteRate > options.failAboveWasteRate) {
3034
+ breaches.push(
3035
+ `Structural waste rate ${(summary.structuralWasteRate * 100).toFixed(1)}% exceeds threshold ${(options.failAboveWasteRate * 100).toFixed(1)}%`
3036
+ );
3037
+ }
3038
+ if (options.failAboveWasteUsd !== void 0 && summary.wasteSpendUsd > options.failAboveWasteUsd) {
3039
+ breaches.push(
3040
+ `Waste spend $${summary.wasteSpendUsd.toFixed(2)} exceeds threshold $${options.failAboveWasteUsd.toFixed(2)}`
3041
+ );
3042
+ }
3043
+ if (breaches.length > 0) {
3044
+ process.stderr.write(`
3045
+ Threshold exceeded:
3046
+ ${breaches.map((b) => ` ${b}`).join("\n")}
3047
+ `);
3048
+ process.exitCode = 3;
3049
+ }
3050
+ }
3051
+ function cleanupPullResult(pullResult, keepFiles) {
3052
+ if (keepFiles) return;
3053
+ try {
3054
+ rmSync4(pullResult.localPath, { recursive: true, force: true });
3055
+ } catch {
3056
+ }
3057
+ }
3058
+
3059
+ // src/commands/doctor.ts
3060
+ async function runDoctorCommand(options) {
3061
+ if (options.railway) {
3062
+ const railwayTarget = buildRailwayTarget2(options);
3063
+ const source = buildRailwaySourceFromFlags({
3064
+ railway: railwayTarget,
3065
+ remoteLogFile: options.remoteLogFile,
3066
+ remoteSessionsDir: options.remoteSessionsDir
3067
+ });
3068
+ const report2 = await runRailwayDoctor({ source });
3069
+ process.stdout.write(`${renderRailwayDoctorReport(report2)}
3070
+ `);
3071
+ return;
3072
+ }
3073
+ if (options.remote) {
3074
+ const source = buildSourceFromFlags({
3075
+ remote: options.remote,
3076
+ remoteLogFile: options.remoteLogFile,
3077
+ remoteSessionsDir: options.remoteSessionsDir
3078
+ });
3079
+ const report2 = await runRemoteDoctor({ source });
3080
+ process.stdout.write(`${renderRemoteDoctorReport(report2)}
3081
+ `);
3082
+ return;
3083
+ }
3084
+ const report = await doctorOpenClaw({
3085
+ logFile: options.logFile,
3086
+ sessionsDir: options.sessionsDir
3087
+ });
3088
+ process.stdout.write(`${renderDoctorReport(report)}
3089
+ `);
3090
+ }
3091
+ function buildRailwayTarget2(options) {
3092
+ if (options.railwayProject && options.railwayEnvironment && options.railwayService) {
3093
+ return {
3094
+ projectId: options.railwayProject,
3095
+ environmentId: options.railwayEnvironment,
3096
+ serviceId: options.railwayService
3097
+ };
3098
+ }
3099
+ return void 0;
3100
+ }
3101
+ function formatBytes3(bytes) {
3102
+ if (bytes === 0) return "0 B";
3103
+ const units = ["B", "KB", "MB", "GB"];
3104
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
3105
+ const value = bytes / 1024 ** i;
3106
+ return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
3107
+ }
3108
+ function renderRemoteDoctorReport(report) {
3109
+ const sections = [
3110
+ "# Xerg doctor [remote]",
3111
+ "",
3112
+ `Host: ${report.host}`,
3113
+ `SSH connectivity: ${report.sshConnectivity ? "OK" : "FAILED"}`
3114
+ ];
3115
+ if (!report.sshConnectivity) {
3116
+ sections.push("", ...report.notes.map((n) => `[remote] ${n}`));
3117
+ return sections.join("\n");
3118
+ }
3119
+ sections.push(
3120
+ `rsync (local): ${report.rsyncAvailableLocal ? "available" : "not found"}`,
3121
+ `rsync (remote): ${report.rsyncAvailableRemote ? "available" : "not found"}`,
3122
+ "",
3123
+ "## Default paths",
3124
+ `[remote] Gateway (${report.defaultPaths.gatewayPath}): ${report.defaultPaths.gatewayExists ? `${report.defaultPaths.gatewayFileCount} files, ${formatBytes3(report.defaultPaths.gatewayTotalBytes)}` : "not found"}`,
3125
+ `[remote] Sessions (${report.defaultPaths.sessionsPath}): ${report.defaultPaths.sessionsExists ? `${report.defaultPaths.sessionsFileCount} files, ${formatBytes3(report.defaultPaths.sessionsTotalBytes)}` : "not found"}`
3126
+ );
3127
+ if (report.customPaths) {
3128
+ sections.push("", "## Custom paths");
3129
+ if (report.customPaths.logFilePath) {
3130
+ sections.push(
3131
+ `[remote] Log file (${report.customPaths.logFilePath}): ${report.customPaths.logFileExists ? formatBytes3(report.customPaths.logFileBytes) : "not found"}`
3132
+ );
3133
+ }
3134
+ if (report.customPaths.sessionsDirPath) {
3135
+ sections.push(
3136
+ `[remote] Sessions dir (${report.customPaths.sessionsDirPath}): ${report.customPaths.sessionsDirExists ? `${report.customPaths.sessionsFileCount} files, ${formatBytes3(report.customPaths.sessionsTotalBytes)}` : "not found"}`
3137
+ );
3138
+ }
3139
+ }
3140
+ sections.push("", "## Notes", ...report.notes.map((n) => `[remote] ${n}`));
3141
+ return sections.join("\n");
3142
+ }
3143
+ function renderRailwayDoctorReport(report) {
3144
+ const sections = [
3145
+ "# Xerg doctor [railway]",
3146
+ "",
3147
+ `Source: ${report.name}`,
3148
+ `Railway CLI: ${report.railwayCliInstalled ? "installed" : "NOT INSTALLED"}`
3149
+ ];
3150
+ if (!report.railwayCliInstalled) {
3151
+ sections.push("", ...report.notes.map((n) => `[railway] ${n}`));
3152
+ return sections.join("\n");
3153
+ }
3154
+ sections.push(`Authentication: ${report.railwayAuthenticated ? "OK" : "NOT AUTHENTICATED"}`);
3155
+ if (!report.railwayAuthenticated) {
3156
+ sections.push("", ...report.notes.map((n) => `[railway] ${n}`));
3157
+ return sections.join("\n");
3158
+ }
3159
+ if (report.railwayAuthUser) {
3160
+ sections.push(`User: ${report.railwayAuthUser}`);
3161
+ }
3162
+ sections.push(`Service reachable: ${report.serviceReachable ? "OK" : "FAILED"}`);
3163
+ if (!report.serviceReachable) {
3164
+ if (report.serviceError) {
3165
+ sections.push(`Error: ${report.serviceError}`);
3166
+ }
3167
+ sections.push("", ...report.notes.map((n) => `[railway] ${n}`));
3168
+ return sections.join("\n");
3169
+ }
3170
+ sections.push(
3171
+ "",
3172
+ "## Default paths",
3173
+ `[railway] Gateway (${report.defaultPaths.gatewayPath}): ${report.defaultPaths.gatewayExists ? `${report.defaultPaths.gatewayFileCount} files, ${formatBytes3(report.defaultPaths.gatewayTotalBytes)}` : "not found"}`,
3174
+ `[railway] Sessions (${report.defaultPaths.sessionsPath}): ${report.defaultPaths.sessionsExists ? `${report.defaultPaths.sessionsFileCount} files, ${formatBytes3(report.defaultPaths.sessionsTotalBytes)}` : "not found"}`
3175
+ );
3176
+ if (report.alternateSessionPaths.length > 0) {
3177
+ sections.push("", "## Alternate session paths");
3178
+ for (const alt of report.alternateSessionPaths) {
3179
+ sections.push(
3180
+ `[railway] ${alt.path}: ${alt.exists ? `${alt.fileCount} files, ${formatBytes3(alt.totalBytes)}` : "not found"}`
3181
+ );
3182
+ }
3183
+ }
3184
+ if (report.customPaths) {
3185
+ sections.push("", "## Custom paths");
3186
+ if (report.customPaths.logFilePath) {
3187
+ sections.push(
3188
+ `[railway] Log file (${report.customPaths.logFilePath}): ${report.customPaths.logFileExists ? formatBytes3(report.customPaths.logFileBytes) : "not found"}`
3189
+ );
3190
+ }
3191
+ if (report.customPaths.sessionsDirPath) {
3192
+ sections.push(
3193
+ `[railway] Sessions dir (${report.customPaths.sessionsDirPath}): ${report.customPaths.sessionsDirExists ? `${report.customPaths.sessionsFileCount} files, ${formatBytes3(report.customPaths.sessionsTotalBytes)}` : "not found"}`
3194
+ );
3195
+ }
3196
+ }
3197
+ sections.push("", "## Notes", ...report.notes.map((n) => `[railway] ${n}`));
3198
+ return sections.join("\n");
3199
+ }
3200
+
3201
+ // src/commands/login.ts
3202
+ import { styleText } from "util";
3203
+ var DEFAULT_AUTH_URL = "https://xerg.ai/dashboard/settings";
3204
+ var DEFAULT_API_URL2 = "https://api.xerg.ai";
3205
+ var POLL_INTERVAL_MS = 2e3;
3206
+ var POLL_TIMEOUT_MS = 3e5;
3207
+ async function runLoginCommand() {
3208
+ const existing = loadStoredCredentials();
3209
+ if (existing) {
3210
+ process.stderr.write(
3211
+ `Already logged in. Credentials stored at ${getCredentialsPath()}.
3212
+ Run ${colorBold("xerg logout")} first to re-authenticate.
3213
+ `
3214
+ );
3215
+ return;
3216
+ }
3217
+ const apiUrl = process.env.XERG_API_URL || DEFAULT_API_URL2;
3218
+ const deviceCodeUrl = `${apiUrl}/v1/auth/device-code`;
3219
+ let deviceResponse;
3220
+ try {
3221
+ const res = await fetch(deviceCodeUrl, { method: "POST" });
3222
+ if (!res.ok) {
3223
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
3224
+ }
3225
+ deviceResponse = await res.json();
3226
+ } catch (err) {
3227
+ const msg = err instanceof Error ? err.message : "Unknown error";
3228
+ throw new Error(
3229
+ `Could not start device auth flow (${msg}).
3230
+
3231
+ Alternative: create an API key at ${DEFAULT_AUTH_URL}
3232
+ and set XERG_API_KEY in your environment.`
3233
+ );
3234
+ }
3235
+ const verifyUrl = deviceResponse.verificationUrl || DEFAULT_AUTH_URL;
3236
+ const pollInterval = (deviceResponse.interval || 2) * 1e3;
3237
+ process.stderr.write(
3238
+ `
3239
+ Open this URL in your browser to authenticate:
3240
+
3241
+ ${colorBold(verifyUrl)}
3242
+
3243
+ `
3244
+ );
3245
+ if (deviceResponse.userCode) {
3246
+ process.stderr.write(`Your code: ${colorBold(deviceResponse.userCode)}
3247
+
3248
+ `);
3249
+ }
3250
+ process.stderr.write("Waiting for authentication...\n");
3251
+ await openBrowser(verifyUrl);
3252
+ const tokenUrl = `${apiUrl}/v1/auth/device-token`;
3253
+ const startTime = Date.now();
3254
+ while (Date.now() - startTime < POLL_TIMEOUT_MS) {
3255
+ await sleep(Math.max(pollInterval, POLL_INTERVAL_MS));
3256
+ try {
3257
+ const res = await fetch(tokenUrl, {
3258
+ method: "POST",
3259
+ headers: { "Content-Type": "application/json" },
3260
+ body: JSON.stringify({ deviceCode: deviceResponse.deviceCode })
3261
+ });
3262
+ if (res.status === 200) {
3263
+ const data = await res.json();
3264
+ storeCredentials(data.token);
3265
+ const teamInfo = data.teamName ? ` (team: ${data.teamName})` : "";
3266
+ process.stderr.write(
3267
+ `
3268
+ ${colorSuccess("Authenticated successfully")}${teamInfo}.
3269
+ Credentials saved to ${getCredentialsPath()}.
3270
+ `
3271
+ );
3272
+ return;
3273
+ }
3274
+ if (res.status === 428) {
3275
+ continue;
3276
+ }
3277
+ if (res.status === 410) {
3278
+ throw new Error("Device code expired. Please run `xerg login` again.");
3279
+ }
3280
+ const body = await res.json().catch(() => ({}));
3281
+ throw new Error(body.error || `Unexpected response: HTTP ${res.status}`);
3282
+ } catch (err) {
3283
+ if (err instanceof Error && (err.message.includes("expired") || err.message.includes("Unexpected"))) {
3284
+ throw err;
3285
+ }
3286
+ }
3287
+ }
3288
+ throw new Error("Authentication timed out. Please run `xerg login` again.");
3289
+ }
3290
+ async function openBrowser(url) {
3291
+ const { exec } = await import("child_process");
3292
+ const { platform: platform2 } = await import("os");
3293
+ const commands = {
3294
+ darwin: "open",
3295
+ win32: "start",
3296
+ linux: "xdg-open"
3297
+ };
3298
+ const cmd = commands[platform2()];
3299
+ if (!cmd) return;
3300
+ return new Promise((resolve3) => {
3301
+ exec(`${cmd} ${JSON.stringify(url)}`, () => resolve3());
3302
+ });
3303
+ }
3304
+ function sleep(ms) {
3305
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
3306
+ }
3307
+ function colorBold(text) {
3308
+ return process.stderr.isTTY ? styleText("bold", text) : text;
3309
+ }
3310
+ function colorSuccess(text) {
3311
+ return process.stderr.isTTY ? styleText("green", text) : text;
3312
+ }
3313
+
3314
+ // src/commands/logout.ts
3315
+ function runLogoutCommand() {
3316
+ const removed = clearCredentials();
3317
+ if (removed) {
3318
+ process.stderr.write(`Credentials removed from ${getCredentialsPath()}.
3319
+ `);
3320
+ } else {
3321
+ process.stderr.write("No stored credentials found. Already logged out.\n");
3322
+ }
3323
+ }
3324
+
3325
+ // src/commands/push.ts
3326
+ import { readFileSync as readFileSync6 } from "fs";
3327
+ import { hostname as hostname2 } from "os";
3328
+ async function runPushCommand(options) {
3329
+ const payload = options.file ? loadPayloadFromFile(options.file) : loadPayloadFromCache();
3330
+ if (options.dryRun) {
3331
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
3332
+ `);
3333
+ return;
3334
+ }
3335
+ const config = loadPushConfig();
3336
+ const auditId = payload.summary.auditId;
3337
+ process.stderr.write(`Pushing audit ${auditId} to ${config.apiUrl}...
3338
+ `);
3339
+ const result = await pushAudit(payload, config);
3340
+ if (result.ok) {
3341
+ process.stderr.write(`Pushed successfully (audit: ${result.auditId}).
3342
+ `);
3343
+ } else {
3344
+ const statusInfo = result.status > 0 ? ` (HTTP ${result.status})` : "";
3345
+ throw new Error(`Push failed${statusInfo}: ${result.message}`);
3346
+ }
3347
+ }
3348
+ function loadPayloadFromFile(filePath) {
3349
+ let raw;
3350
+ try {
3351
+ raw = readFileSync6(filePath, "utf8");
3352
+ } catch {
3353
+ throw new Error(`Cannot read file: ${filePath}`);
3354
+ }
3355
+ let parsed;
3356
+ try {
3357
+ parsed = JSON.parse(raw);
3358
+ } catch {
3359
+ throw new Error(`File is not valid JSON: ${filePath}`);
3360
+ }
3361
+ const payload = parsed;
3362
+ if (!payload.version || !payload.summary || !payload.meta) {
3363
+ throw new Error(
3364
+ `File does not look like an AuditPushPayload (missing version, summary, or meta): ${filePath}`
3365
+ );
3366
+ }
3367
+ return payload;
3368
+ }
3369
+ function loadPayloadFromCache() {
3370
+ const dbPath = getDefaultDbPath();
3371
+ let summaries;
3372
+ try {
3373
+ summaries = listStoredAuditSummaries(dbPath);
3374
+ } catch {
3375
+ throw new NoDataError(
3376
+ "No local audit database found. Run `xerg audit` first, or use `xerg push --file <path>`."
3377
+ );
3378
+ }
3379
+ if (summaries.length === 0) {
3380
+ throw new NoDataError(
3381
+ "No cached audit snapshots found. Run `xerg audit` first, or use `xerg push --file <path>`."
3382
+ );
3383
+ }
3384
+ const latest = summaries[0];
3385
+ const meta = buildMeta2();
3386
+ process.stderr.write(
3387
+ `Using most recent cached audit: ${latest.auditId} (${latest.generatedAt})
3388
+ `
3389
+ );
3390
+ return toWirePayload(latest, meta);
3391
+ }
3392
+ function readCliVersion2() {
3393
+ try {
3394
+ const packageJsonPath = new URL("../../package.json", import.meta.url);
3395
+ const pkg = JSON.parse(readFileSync6(packageJsonPath, "utf8"));
3396
+ return pkg.version ?? "0.0.0";
3397
+ } catch {
3398
+ return "0.0.0";
3399
+ }
3400
+ }
3401
+ function buildMeta2() {
3402
+ return {
3403
+ cliVersion: readCliVersion2(),
3404
+ sourceId: hostname2(),
3405
+ sourceHost: hostname2(),
3406
+ environment: "local"
3407
+ };
3408
+ }
3409
+
3410
+ // src/index.ts
3411
+ var VERSION = readVersion();
3412
+ var argv = process.argv.slice(2);
3413
+ var command = argv[0];
3414
+ if (!command || command === "--help" || command === "-h" || command === "help") {
3415
+ process.stdout.write(renderRootHelp());
3416
+ process.exit(0);
3417
+ }
3418
+ if (command === "--version" || command === "-v" || command === "version") {
3419
+ process.stdout.write(`${VERSION}
3420
+ `);
3421
+ process.exit(0);
3422
+ }
3423
+ run().catch((error) => {
3424
+ const message = error instanceof Error ? error.message : "Unknown error";
3425
+ process.stderr.write(`${colorError(`xerg failed: ${message}`)}
3426
+ `);
3427
+ process.exitCode = error instanceof NoDataError ? 2 : 1;
3428
+ });
3429
+ async function run() {
3430
+ if (command === "audit") {
3431
+ const options = parseAuditOptions(argv.slice(1));
3432
+ if (options.json && options.markdown) {
3433
+ throw new Error("Use either --json or --markdown, not both.");
3434
+ }
3435
+ await runAuditCommand(options);
3436
+ return;
3437
+ }
3438
+ if (command === "doctor") {
3439
+ const options = parseDoctorOptions(argv.slice(1));
3440
+ await runDoctorCommand(options);
3441
+ return;
3442
+ }
3443
+ if (command === "push") {
3444
+ const options = parsePushOptions(argv.slice(1));
3445
+ await runPushCommand(options);
3446
+ return;
3447
+ }
3448
+ if (command === "login") {
3449
+ await runLoginCommand();
3450
+ return;
3451
+ }
3452
+ if (command === "logout") {
3453
+ runLogoutCommand();
3454
+ return;
3455
+ }
3456
+ throw new Error(`Unknown command "${command}". Run \`xerg --help\` to see available commands.`);
3457
+ }
3458
+ function parseAuditOptions(raw) {
3459
+ const argv2 = expandEqualsArgs(raw);
3460
+ const options = {};
3461
+ for (let index = 0; index < argv2.length; index += 1) {
3462
+ const arg = argv2[index];
3463
+ switch (arg) {
3464
+ case "--help":
3465
+ case "-h":
3466
+ process.stdout.write(renderAuditHelp());
3467
+ process.exit(0);
3468
+ break;
3469
+ case "--log-file":
3470
+ options.logFile = readValue(arg, argv2[index + 1]);
3471
+ index += 1;
3472
+ break;
3473
+ case "--sessions-dir":
3474
+ options.sessionsDir = readValue(arg, argv2[index + 1]);
3475
+ index += 1;
3476
+ break;
3477
+ case "--since":
3478
+ options.since = readValue(arg, argv2[index + 1]);
3479
+ index += 1;
3480
+ break;
3481
+ case "--db":
3482
+ options.db = readValue(arg, argv2[index + 1]);
3483
+ index += 1;
3484
+ break;
3485
+ case "--compare":
3486
+ options.compare = true;
3487
+ break;
3488
+ case "--json":
3489
+ options.json = true;
3490
+ break;
3491
+ case "--markdown":
3492
+ options.markdown = true;
3493
+ break;
3494
+ case "--no-db":
3495
+ options.noDb = true;
3496
+ break;
3497
+ case "--remote":
3498
+ options.remote = readValue(arg, argv2[index + 1]);
3499
+ index += 1;
3500
+ break;
3501
+ case "--remote-log-file":
3502
+ options.remoteLogFile = readValue(arg, argv2[index + 1]);
3503
+ index += 1;
3504
+ break;
3505
+ case "--remote-sessions-dir":
3506
+ options.remoteSessionsDir = readValue(arg, argv2[index + 1]);
3507
+ index += 1;
3508
+ break;
3509
+ case "--remote-config":
3510
+ options.remoteConfig = readValue(arg, argv2[index + 1]);
3511
+ index += 1;
3512
+ break;
3513
+ case "--keep-remote-files":
3514
+ options.keepRemoteFiles = true;
3515
+ break;
3516
+ case "--railway":
3517
+ options.railway = true;
3518
+ break;
3519
+ case "--project":
3520
+ options.railwayProject = readValue(arg, argv2[index + 1]);
3521
+ index += 1;
3522
+ break;
3523
+ case "--environment":
3524
+ options.railwayEnvironment = readValue(arg, argv2[index + 1]);
3525
+ index += 1;
3526
+ break;
3527
+ case "--service":
3528
+ options.railwayService = readValue(arg, argv2[index + 1]);
3529
+ index += 1;
3530
+ break;
3531
+ case "--push":
3532
+ options.push = true;
3533
+ break;
3534
+ case "--dry-run":
3535
+ options.dryRun = true;
3536
+ break;
3537
+ case "--fail-above-waste-rate":
3538
+ options.failAboveWasteRate = readFloat(arg, argv2[index + 1]);
3539
+ index += 1;
3540
+ break;
3541
+ case "--fail-above-waste-usd":
3542
+ options.failAboveWasteUsd = readFloat(arg, argv2[index + 1]);
3543
+ index += 1;
3544
+ break;
3545
+ default:
3546
+ throw new Error(`Unknown audit option "${arg}". Run \`xerg audit --help\` for usage.`);
3547
+ }
3548
+ }
3549
+ return options;
3550
+ }
3551
+ function parsePushOptions(raw) {
3552
+ const argv2 = expandEqualsArgs(raw);
3553
+ const options = {};
3554
+ for (let index = 0; index < argv2.length; index += 1) {
3555
+ const arg = argv2[index];
3556
+ switch (arg) {
3557
+ case "--help":
3558
+ case "-h":
3559
+ process.stdout.write(renderPushHelp());
3560
+ process.exit(0);
3561
+ break;
3562
+ case "--file":
3563
+ options.file = readValue(arg, argv2[index + 1]);
3564
+ index += 1;
3565
+ break;
3566
+ case "--dry-run":
3567
+ options.dryRun = true;
3568
+ break;
3569
+ default:
3570
+ throw new Error(`Unknown push option "${arg}". Run \`xerg push --help\` for usage.`);
3571
+ }
3572
+ }
3573
+ return options;
3574
+ }
3575
+ function parseDoctorOptions(raw) {
3576
+ const argv2 = expandEqualsArgs(raw);
3577
+ const options = {};
3578
+ for (let index = 0; index < argv2.length; index += 1) {
3579
+ const arg = argv2[index];
3580
+ switch (arg) {
3581
+ case "--help":
3582
+ case "-h":
3583
+ process.stdout.write(renderDoctorHelp());
3584
+ process.exit(0);
3585
+ break;
3586
+ case "--log-file":
3587
+ options.logFile = readValue(arg, argv2[index + 1]);
3588
+ index += 1;
3589
+ break;
3590
+ case "--sessions-dir":
3591
+ options.sessionsDir = readValue(arg, argv2[index + 1]);
3592
+ index += 1;
3593
+ break;
3594
+ case "--remote":
3595
+ options.remote = readValue(arg, argv2[index + 1]);
3596
+ index += 1;
3597
+ break;
3598
+ case "--remote-log-file":
3599
+ options.remoteLogFile = readValue(arg, argv2[index + 1]);
3600
+ index += 1;
3601
+ break;
3602
+ case "--remote-sessions-dir":
3603
+ options.remoteSessionsDir = readValue(arg, argv2[index + 1]);
3604
+ index += 1;
3605
+ break;
3606
+ case "--railway":
3607
+ options.railway = true;
3608
+ break;
3609
+ case "--project":
3610
+ options.railwayProject = readValue(arg, argv2[index + 1]);
3611
+ index += 1;
3612
+ break;
3613
+ case "--environment":
3614
+ options.railwayEnvironment = readValue(arg, argv2[index + 1]);
3615
+ index += 1;
3616
+ break;
3617
+ case "--service":
3618
+ options.railwayService = readValue(arg, argv2[index + 1]);
3619
+ index += 1;
3620
+ break;
3621
+ default:
3622
+ throw new Error(`Unknown doctor option "${arg}". Run \`xerg doctor --help\` for usage.`);
3623
+ }
3624
+ }
3625
+ return options;
3626
+ }
3627
+ function expandEqualsArgs(argv2) {
3628
+ const result = [];
3629
+ for (const arg of argv2) {
3630
+ const eqIndex = arg.indexOf("=");
3631
+ if (eqIndex > 0 && arg.startsWith("--")) {
3632
+ result.push(arg.slice(0, eqIndex), arg.slice(eqIndex + 1));
3633
+ } else {
3634
+ result.push(arg);
3635
+ }
3636
+ }
3637
+ return result;
3638
+ }
3639
+ function readValue(flag, value) {
3640
+ if (!value || value.startsWith("-")) {
3641
+ throw new Error(`The ${flag} flag needs a value.`);
3642
+ }
3643
+ return value;
3644
+ }
3645
+ function readFloat(flag, value) {
3646
+ const raw = readValue(flag, value);
3647
+ const num = Number.parseFloat(raw);
3648
+ if (Number.isNaN(num)) {
3649
+ throw new Error(`The ${flag} flag requires a numeric value, got "${raw}".`);
3650
+ }
3651
+ return num;
3652
+ }
3653
+ function renderRootHelp() {
3654
+ return `xerg ${VERSION}
168
3655
 
169
3656
  Waste intelligence for OpenClaw workflows.
170
3657
 
@@ -172,13 +3659,19 @@ Usage:
172
3659
  xerg <command> [options]
173
3660
 
174
3661
  Commands:
175
- audit Analyze OpenClaw logs and produce a waste intelligence report.
176
- doctor Inspect your machine for OpenClaw sources and audit readiness.
3662
+ audit Analyze OpenClaw logs and produce a waste intelligence report.
3663
+ doctor Inspect your machine for OpenClaw sources and audit readiness.
3664
+ push Push a cached audit snapshot to the Xerg API.
3665
+ login Authenticate with the Xerg API via browser.
3666
+ logout Remove stored Xerg API credentials.
177
3667
 
178
3668
  Global options:
179
3669
  -h, --help Show help
180
3670
  -v, --version Show version
181
- `}function gt(){return`xerg audit
3671
+ `;
3672
+ }
3673
+ function renderAuditHelp() {
3674
+ return `xerg audit
182
3675
 
183
3676
  Analyze OpenClaw logs and produce a waste intelligence report.
184
3677
 
@@ -186,16 +3679,66 @@ Usage:
186
3679
  xerg audit [options]
187
3680
 
188
3681
  Options:
189
- --log-file <path> Explicit OpenClaw gateway log file to analyze
190
- --sessions-dir <path> Explicit OpenClaw sessions directory to analyze
191
- --since <duration> Look back window such as 24h, 7d, or 30m
192
- --compare Compare this audit to the newest compatible prior local snapshot
193
- --json Render the report as JSON
194
- --markdown Render the report as Markdown
195
- --db <path> Custom SQLite database path
196
- --no-db Skip local persistence
197
- -h, --help Show help
198
- `}function ht(){return`xerg doctor
3682
+ --log-file <path> Explicit OpenClaw gateway log file to analyze
3683
+ --sessions-dir <path> Explicit OpenClaw sessions directory to analyze
3684
+ --since <duration> Look back window such as 24h, 7d, or 30m
3685
+ --compare Compare this audit to the newest compatible prior local snapshot
3686
+ --json Render the report as JSON
3687
+ --markdown Render the report as Markdown
3688
+ --db <path> Custom SQLite database path
3689
+ --no-db Skip local persistence
3690
+
3691
+ Remote options (SSH):
3692
+ --remote <user@host> SSH target in user@host or user@host:port format
3693
+ --remote-log-file <path> Override the default gateway log path on the remote host
3694
+ --remote-sessions-dir <path> Override the default sessions directory on the remote host
3695
+ --remote-config <path> Path to a JSON file defining multiple remote sources
3696
+ --keep-remote-files Retain pulled files in ~/.xerg/remote-cache/ instead of using a temp directory
3697
+
3698
+ Prerequisites:
3699
+ SSH remote audits require ssh and rsync on your PATH.
3700
+
3701
+ Railway options:
3702
+ --railway Audit a Railway service (uses linked project by default)
3703
+ --project <id> Railway project ID
3704
+ --environment <id> Railway environment ID
3705
+ --service <id> Railway service ID
3706
+
3707
+ Railway audits require the railway CLI on your PATH.
3708
+
3709
+ Push options:
3710
+ --push Push the audit summary to the Xerg API after computing it
3711
+ --dry-run With --push: print the payload to stdout without sending it
3712
+
3713
+ Threshold options:
3714
+ --fail-above-waste-rate <n> Exit with code 3 if structural waste rate exceeds threshold (e.g. 0.30)
3715
+ --fail-above-waste-usd <n> Exit with code 3 if waste spend exceeds threshold in USD (e.g. 50)
3716
+
3717
+ -h, --help Show help
3718
+ `;
3719
+ }
3720
+ function renderPushHelp() {
3721
+ return `xerg push
3722
+
3723
+ Push a cached audit snapshot to the Xerg API.
3724
+
3725
+ Usage:
3726
+ xerg push [options]
3727
+
3728
+ Options:
3729
+ --file <path> Push a specific snapshot file instead of the most recent cached audit
3730
+ --dry-run Print the payload to stdout without sending it
3731
+
3732
+ -h, --help Show help
3733
+
3734
+ Authentication:
3735
+ Set XERG_API_KEY in your environment, add "apiKey" to ~/.xerg/config.json,
3736
+ or run \`xerg login\` to authenticate via browser.
3737
+ Browser login stores a token at ~/.config/xerg/credentials.json by default.
3738
+ `;
3739
+ }
3740
+ function renderDoctorHelp() {
3741
+ return `xerg doctor
199
3742
 
200
3743
  Inspect your machine for OpenClaw sources and audit readiness.
201
3744
 
@@ -203,7 +3746,33 @@ Usage:
203
3746
  xerg doctor [options]
204
3747
 
205
3748
  Options:
206
- --log-file <path> Explicit OpenClaw gateway log file to inspect
207
- --sessions-dir <path> Explicit OpenClaw sessions directory to inspect
208
- -h, --help Show help
209
- `}function wt(e){return process.stderr.isTTY?ut("red",e):e}function Ut(){let e=new URL("../package.json",import.meta.url);return JSON.parse(ct(e,"utf8")).version??"0.0.0"}
3749
+ --log-file <path> Explicit OpenClaw gateway log file to inspect
3750
+ --sessions-dir <path> Explicit OpenClaw sessions directory to inspect
3751
+
3752
+ Remote options (SSH):
3753
+ --remote <user@host> SSH target in user@host or user@host:port format
3754
+ --remote-log-file <path> Override the default gateway log path on the remote host
3755
+ --remote-sessions-dir <path> Override the default sessions directory on the remote host
3756
+
3757
+ SSH checks require ssh and rsync on your PATH.
3758
+
3759
+ Railway options:
3760
+ --railway Check a Railway service (uses linked project by default)
3761
+ --project <id> Railway project ID
3762
+ --environment <id> Railway environment ID
3763
+ --service <id> Railway service ID
3764
+
3765
+ Railway checks require the railway CLI on your PATH.
3766
+
3767
+ -h, --help Show help
3768
+ `;
3769
+ }
3770
+ function colorError(message) {
3771
+ return process.stderr.isTTY ? styleText2("red", message) : message;
3772
+ }
3773
+ function readVersion() {
3774
+ const packageJsonPath = new URL("../package.json", import.meta.url);
3775
+ const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf8"));
3776
+ return packageJson.version ?? "0.0.0";
3777
+ }
3778
+ //# sourceMappingURL=index.js.map