@xerg/cli 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,16 @@
1
1
  # xerg
2
2
 
3
- Audit OpenClaw agent spend, waste, and before/after improvements.
3
+ Audit OpenClaw workflows in dollars, compare fixes, and surface savings opportunities.
4
4
 
5
- Xerg audits OpenClaw workflows in dollars, not tokens. It reads your gateway logs and session transcripts, shows where money is leaking, classifies waste into five categories, and lets you re-run the same audit with `--compare` so you can see what changed after a fix.
5
+ Xerg audits OpenClaw workflows in dollars, not tokens. It reads your gateway logs and session transcripts, surfaces five spend categories across confirmed waste and savings opportunities, and lets you re-run the same audit with `--compare` so you can see exactly what changed after a fix.
6
+
7
+ Everything runs locally by default. No account is required for local audits. No data leaves your machine unless you explicitly `--push` results to the Xerg API for a team dashboard.
6
8
 
7
9
  ## 30-second quick start
8
10
 
9
11
  ```bash
10
12
  npx @xerg/cli doctor
13
+ npx @xerg/cli doctor --verbose
11
14
  npx @xerg/cli audit
12
15
  npx @xerg/cli audit --compare
13
16
  ```
@@ -112,6 +115,13 @@ xerg audit --log-file /path/to/openclaw.log
112
115
  xerg audit --sessions-dir /path/to/sessions
113
116
  ```
114
117
 
118
+ If your local machine has no OpenClaw files, inspect remote targets directly instead:
119
+
120
+ ```bash
121
+ xerg doctor --remote user@host
122
+ xerg doctor --railway
123
+ ```
124
+
115
125
  ## Authentication and config
116
126
 
117
127
  Push commands resolve credentials in this order:
@@ -140,7 +150,8 @@ Example `~/.xerg/config.json`:
140
150
 
141
151
  - Total spend by workflow and model, in dollars
142
152
  - Observed vs. estimated cost (always labeled)
143
- - Structural waste: retry, context bloat, loop, downgrade candidates, idle
153
+ - Confirmed waste: retry, loop
154
+ - Savings opportunities: context bloat, downgrade candidates, idle
144
155
  - Savings recommendations with suggested A/B tests
145
156
  - Before/after deltas on re-audit
146
157
 
@@ -158,6 +169,7 @@ Xerg v0 stores economic metadata and audit summaries locally. It does not store
158
169
  ## Troubleshooting
159
170
 
160
171
  - `better-sqlite3` is a native dependency. If install fails, retry on a supported Node version and make sure standard native build tooling is available for your platform.
172
+ - `--verbose` prints progress updates to stderr for `xerg doctor` and `xerg audit`, which helps distinguish package install time from CLI runtime.
161
173
  - If `xerg audit --remote ...` fails before pulling files, verify that both `ssh` and `rsync` are installed and reachable on your `PATH`.
162
174
  - If `xerg audit --railway` fails immediately, verify that the `railway` CLI is installed, authenticated, and can access the target project.
163
175
 
package/dist/index.js CHANGED
@@ -798,10 +798,17 @@ function segmentToRegExp(segment) {
798
798
  return new RegExp(`^${escaped}$`);
799
799
  }
800
800
  async function inspectOpenClawSources(options) {
801
+ options.onProgress?.("Checking local OpenClaw defaults...");
801
802
  const sources = await detectOpenClawSources(options);
802
803
  const notes = [];
804
+ options.onProgress?.(
805
+ sources.length > 0 ? `Detected ${sources.length} local source file${sources.length === 1 ? "" : "s"}.` : "No local OpenClaw source files were detected."
806
+ );
803
807
  if (sources.length === 0) {
804
808
  notes.push("No OpenClaw gateway logs or session files were detected.");
809
+ notes.push(
810
+ "Doctor checks local defaults by default. Use --remote or --railway to inspect remote targets."
811
+ );
805
812
  notes.push(
806
813
  "Use --log-file or --sessions-dir if your OpenClaw data lives outside the defaults."
807
814
  );
@@ -1383,6 +1390,7 @@ async function doctorOpenClaw(options) {
1383
1390
  return inspectOpenClawSources(options);
1384
1391
  }
1385
1392
  async function auditOpenClaw(options) {
1393
+ options.onProgress?.("Scanning for OpenClaw source files...");
1386
1394
  if (options.compare && options.noDb) {
1387
1395
  throw new Error(
1388
1396
  "The --compare flag needs local snapshot history. Remove --no-db or provide --db <path>."
@@ -1390,13 +1398,19 @@ async function auditOpenClaw(options) {
1390
1398
  }
1391
1399
  const sources = await detectOpenClawSources(options);
1392
1400
  if (sources.length === 0) {
1401
+ options.onProgress?.("No OpenClaw source files were detected.");
1393
1402
  throw new Error(
1394
1403
  "No OpenClaw sources were detected. Run `xerg doctor` or provide --log-file / --sessions-dir."
1395
1404
  );
1396
1405
  }
1406
+ options.onProgress?.(`Detected ${sources.length} source file${sources.length === 1 ? "" : "s"}.`);
1407
+ options.onProgress?.("Normalizing OpenClaw source files...");
1397
1408
  const runs = normalizeOpenClawSources(sources, options.since);
1409
+ options.onProgress?.(`Normalized ${runs.length} run${runs.length === 1 ? "" : "s"}.`);
1410
+ options.onProgress?.("Computing waste and savings findings...");
1398
1411
  const findings = buildFindings(runs);
1399
1412
  const dbPath = options.noDb ? void 0 : options.dbPath ?? getDefaultDbPath();
1413
+ options.onProgress?.("Building audit summary...");
1400
1414
  const summary = buildAuditSummary({
1401
1415
  runs,
1402
1416
  findings,
@@ -1406,6 +1420,7 @@ async function auditOpenClaw(options) {
1406
1420
  comparisonKeyOverride: options.comparisonKeyOverride
1407
1421
  });
1408
1422
  if (options.compare && dbPath) {
1423
+ options.onProgress?.("Looking for a comparable baseline audit...");
1409
1424
  const baseline = readLatestComparableAuditSummary({
1410
1425
  dbPath,
1411
1426
  comparisonKey: summary.comparisonKey,
@@ -1421,6 +1436,7 @@ async function auditOpenClaw(options) {
1421
1436
  }
1422
1437
  }
1423
1438
  if (dbPath) {
1439
+ options.onProgress?.(`Persisting local snapshot to ${dbPath}...`);
1424
1440
  persistAudit(
1425
1441
  {
1426
1442
  summary,
@@ -1429,6 +1445,9 @@ async function auditOpenClaw(options) {
1429
1445
  },
1430
1446
  dbPath
1431
1447
  );
1448
+ options.onProgress?.("Local snapshot stored.");
1449
+ } else {
1450
+ options.onProgress?.("Skipping local snapshot persistence (--no-db).");
1432
1451
  }
1433
1452
  return summary;
1434
1453
  }
@@ -1639,6 +1658,14 @@ function renderCompareBlock(summary) {
1639
1658
  ];
1640
1659
  }
1641
1660
  function renderDoctorReport(report) {
1661
+ const nextSteps = report.canAudit ? [] : [
1662
+ "",
1663
+ "## Next steps",
1664
+ "- Try explicit local paths: xerg doctor --log-file /path/to/openclaw.log --sessions-dir /path/to/sessions",
1665
+ "- Inspect an SSH host: xerg doctor --remote user@host",
1666
+ "- Inspect a Railway service: xerg doctor --railway",
1667
+ "- Remote audits still analyze locally after Xerg pulls the source files to your machine."
1668
+ ];
1642
1669
  const sections = [
1643
1670
  "# Xerg doctor",
1644
1671
  "",
@@ -1652,7 +1679,8 @@ function renderDoctorReport(report) {
1652
1679
  ...report.sources.length > 0 ? report.sources.map((source) => `- [${source.kind}] ${source.path}`) : ["- none"],
1653
1680
  "",
1654
1681
  "## Notes",
1655
- ...report.notes.map((note) => `- ${note}`)
1682
+ ...report.notes.map((note) => `- ${note}`),
1683
+ ...nextSteps
1656
1684
  ];
1657
1685
  return sections.join("\n");
1658
1686
  }
@@ -1811,6 +1839,23 @@ var NoDataError = class extends Error {
1811
1839
  }
1812
1840
  };
1813
1841
 
1842
+ // src/log.ts
1843
+ function createCliLogger(options) {
1844
+ return {
1845
+ info(message) {
1846
+ process.stderr.write(`${message}
1847
+ `);
1848
+ },
1849
+ verbose(message) {
1850
+ if (!options.verbose) {
1851
+ return;
1852
+ }
1853
+ process.stderr.write(`[verbose] ${message}
1854
+ `);
1855
+ }
1856
+ };
1857
+ }
1858
+
1814
1859
  // src/push/client.ts
1815
1860
  async function pushAudit(payload, config) {
1816
1861
  const url = `${config.apiUrl}/v1/audits`;
@@ -2124,14 +2169,19 @@ function buildComparisonKeyForRemote(source) {
2124
2169
  return `${source.host}:${logPath}:${sessPath}`;
2125
2170
  }
2126
2171
  async function pullRemoteFiles(opts) {
2127
- const { source, since, keepFiles = false } = opts;
2172
+ const { source, since, keepFiles = false, onProgress } = opts;
2173
+ onProgress?.(`Testing SSH connectivity to ${source.host}...`);
2128
2174
  const connectivity = testSshConnectivity(source);
2129
2175
  if (!connectivity.ok) {
2130
2176
  throw new Error(
2131
2177
  `Cannot connect to ${source.host}. Check SSH config and key access.${connectivity.error ? ` (${connectivity.error})` : ""}`
2132
2178
  );
2133
2179
  }
2180
+ onProgress?.("SSH connectivity OK.");
2134
2181
  const useRsync = isRsyncAvailable();
2182
+ onProgress?.(
2183
+ useRsync ? "Local rsync detected. Xerg will prefer rsync and fall back to tar over SSH if needed." : "Local rsync not detected. Xerg will pull files with tar over SSH."
2184
+ );
2135
2185
  const localBase = resolveLocalPath(source, keepFiles);
2136
2186
  const gatewayDir = join5(localBase, "gateway");
2137
2187
  const sessionsDir = join5(localBase, "sessions");
@@ -2139,6 +2189,7 @@ async function pullRemoteFiles(opts) {
2139
2189
  const remoteSessionsPath = source.sessionsDir ?? DEFAULT_SESSIONS_DIR;
2140
2190
  const { stdout: expandedSessions } = sshExec(source, `eval echo ${remoteSessionsPath}`);
2141
2191
  const resolvedSessionsPath = expandedSessions || remoteSessionsPath;
2192
+ onProgress?.("Checking remote default paths for gateway logs and sessions...");
2142
2193
  const { status: logPathExists } = sshExec(source, `test -e ${remoteLogPath} && echo exists`);
2143
2194
  const { status: sessPathExists } = sshExec(
2144
2195
  source,
@@ -2147,6 +2198,7 @@ async function pullRemoteFiles(opts) {
2147
2198
  let pulledLog = false;
2148
2199
  let pulledSessions = false;
2149
2200
  if (logPathExists === 0) {
2201
+ onProgress?.(`Pulling gateway logs from ${remoteLogPath}...`);
2150
2202
  const { stdout: isFile } = sshExec(source, `test -f ${remoteLogPath} && echo file`);
2151
2203
  if (isFile === "file") {
2152
2204
  const parentDir = remoteLogPath.slice(0, remoteLogPath.lastIndexOf("/")) || "/tmp";
@@ -2171,6 +2223,7 @@ async function pullRemoteFiles(opts) {
2171
2223
  }
2172
2224
  }
2173
2225
  if (sessPathExists === 0) {
2226
+ onProgress?.(`Pulling session files from ${resolvedSessionsPath}...`);
2174
2227
  pulledSessions = pullDirectory({
2175
2228
  source,
2176
2229
  remotePath: resolvedSessionsPath,
@@ -2194,11 +2247,13 @@ async function pullRemoteFiles(opts) {
2194
2247
  };
2195
2248
  if (pulledLog) result.logFile = gatewayDir;
2196
2249
  if (pulledSessions) result.sessionsDir = sessionsDir;
2250
+ onProgress?.("Remote files pulled successfully.");
2197
2251
  return result;
2198
2252
  }
2199
2253
  async function runRemoteDoctor(opts) {
2200
- const { source } = opts;
2254
+ const { source, onProgress } = opts;
2201
2255
  const notes = [];
2256
+ onProgress?.(`Testing SSH connectivity to ${source.host}...`);
2202
2257
  const connectivity = testSshConnectivity(source);
2203
2258
  if (!connectivity.ok) {
2204
2259
  return {
@@ -2222,7 +2277,9 @@ async function runRemoteDoctor(opts) {
2222
2277
  ]
2223
2278
  };
2224
2279
  }
2280
+ onProgress?.("SSH connectivity OK.");
2225
2281
  notes.push("SSH connectivity: OK");
2282
+ onProgress?.("Checking rsync availability locally and on the remote host...");
2226
2283
  const rsyncLocal = isRsyncAvailable();
2227
2284
  const rsyncRemote = isRemoteRsyncAvailable(source);
2228
2285
  notes.push(`rsync available locally: ${rsyncLocal ? "yes" : "no"}`);
@@ -2246,6 +2303,7 @@ async function runRemoteDoctor(opts) {
2246
2303
  totalBytes: Number.parseInt(sizeOut, 10) || 0
2247
2304
  };
2248
2305
  }
2306
+ onProgress?.("Inspecting remote default paths...");
2249
2307
  const gateway = checkPath(DEFAULT_GATEWAY_DIR);
2250
2308
  const sessions = checkPath(DEFAULT_SESSIONS_DIR);
2251
2309
  if (gateway.exists) {
@@ -2458,23 +2516,27 @@ function buildComparisonKeyForRailway(source) {
2458
2516
  return `railway-linked:${logPath}:${sessPath}`;
2459
2517
  }
2460
2518
  async function pullRemoteFilesRailway(opts) {
2461
- const { source, since, keepFiles = false } = opts;
2519
+ const { source, since, keepFiles = false, onProgress } = opts;
2462
2520
  const target = source.railway;
2521
+ onProgress?.("Testing Railway service connectivity...");
2463
2522
  const { status } = railwayExec("echo ok", target);
2464
2523
  if (status !== 0) {
2465
2524
  throw new Error(
2466
2525
  `Cannot reach Railway service${target ? ` (project: ${target.projectId})` : " (linked project)"}. Check railway CLI auth and service configuration.`
2467
2526
  );
2468
2527
  }
2528
+ onProgress?.("Railway service reachable.");
2469
2529
  const localBase = resolveLocalPath2(source, keepFiles);
2470
2530
  const gatewayDir = join6(localBase, "gateway");
2471
2531
  const sessionsDir = join6(localBase, "sessions");
2472
2532
  const remoteLogPath = source.logFile ?? DEFAULT_GATEWAY_DIR2;
2533
+ onProgress?.("Checking Railway default paths for gateway logs and sessions...");
2473
2534
  const logCheck = checkRemotePath(remoteLogPath, target);
2474
2535
  const resolvedSessionsPath = findSessionsPath(target, source.sessionsDir);
2475
2536
  let pulledLog = false;
2476
2537
  let pulledSessions = false;
2477
2538
  if (logCheck.exists) {
2539
+ onProgress?.(`Pulling gateway logs from ${remoteLogPath}...`);
2478
2540
  const { stdout: isFile } = railwayExec(`test -f ${remoteLogPath} && echo file`, target);
2479
2541
  if (isFile === "file") {
2480
2542
  const parentDir = remoteLogPath.slice(0, remoteLogPath.lastIndexOf("/")) || "/tmp";
@@ -2494,6 +2556,7 @@ async function pullRemoteFilesRailway(opts) {
2494
2556
  }
2495
2557
  }
2496
2558
  if (resolvedSessionsPath) {
2559
+ onProgress?.(`Pulling session files from ${resolvedSessionsPath}...`);
2497
2560
  pulledSessions = tarRailwayPull({
2498
2561
  target,
2499
2562
  remotePath: resolvedSessionsPath,
@@ -2518,12 +2581,14 @@ async function pullRemoteFilesRailway(opts) {
2518
2581
  };
2519
2582
  if (pulledLog) result.logFile = gatewayDir;
2520
2583
  if (pulledSessions) result.sessionsDir = sessionsDir;
2584
+ onProgress?.("Railway files pulled successfully.");
2521
2585
  return result;
2522
2586
  }
2523
2587
  async function runRailwayDoctor(opts) {
2524
- const { source } = opts;
2588
+ const { source, onProgress } = opts;
2525
2589
  const target = source.railway;
2526
2590
  const notes = [];
2591
+ onProgress?.("Checking whether the Railway CLI is installed...");
2527
2592
  const whichCheck = spawnSync2("which", ["railway"], { stdio: "pipe", timeout: 5e3 });
2528
2593
  const railwayCliInstalled = whichCheck.status === 0;
2529
2594
  if (!railwayCliInstalled) {
@@ -2539,6 +2604,7 @@ async function runRailwayDoctor(opts) {
2539
2604
  };
2540
2605
  }
2541
2606
  const railwayPath = whichCheck.stdout?.toString().trim() ?? "railway";
2607
+ onProgress?.("Checking Railway CLI authentication...");
2542
2608
  const versionCheck = spawnSync2("railway", ["version"], { stdio: "pipe", timeout: 1e4 });
2543
2609
  const versionStr = versionCheck.status === 0 ? versionCheck.stdout?.toString().trim() : railwayPath;
2544
2610
  notes.push(`Railway CLI: installed (${versionStr})`);
@@ -2558,6 +2624,7 @@ async function runRailwayDoctor(opts) {
2558
2624
  };
2559
2625
  }
2560
2626
  notes.push(`Authenticated as: ${railwayAuthUser}`);
2627
+ onProgress?.("Testing Railway service connectivity...");
2561
2628
  const { status: reachStatus } = railwayExec("echo ok", target);
2562
2629
  const serviceReachable = reachStatus === 0;
2563
2630
  if (!serviceReachable) {
@@ -2578,6 +2645,7 @@ async function runRailwayDoctor(opts) {
2578
2645
  };
2579
2646
  }
2580
2647
  notes.push("Service connectivity: OK");
2648
+ onProgress?.("Inspecting Railway default paths...");
2581
2649
  const gateway = checkRemotePath(DEFAULT_GATEWAY_DIR2, target);
2582
2650
  const { stdout: expandedDefault } = railwayExec(`eval echo ${DEFAULT_SESSIONS_DIR2}`, target);
2583
2651
  const resolvedDefault = expandedDefault || DEFAULT_SESSIONS_DIR2;
@@ -2770,6 +2838,7 @@ async function auditOrNoData(...args) {
2770
2838
  }
2771
2839
  }
2772
2840
  async function runAuditCommand(options) {
2841
+ const logger = createCliLogger({ verbose: options.verbose });
2773
2842
  if (options.dryRun && !options.push) {
2774
2843
  throw new Error("--dry-run requires --push.");
2775
2844
  }
@@ -2780,7 +2849,7 @@ async function runAuditCommand(options) {
2780
2849
  throw new Error("Use only one of --remote, --remote-config, or --railway.");
2781
2850
  }
2782
2851
  if (!options.remote && !options.remoteConfig && !options.railway) {
2783
- return runLocalAudit(options);
2852
+ return runLocalAudit(options, logger);
2784
2853
  }
2785
2854
  if (options.railway) {
2786
2855
  const railwayTarget = buildRailwayTarget(options);
@@ -2789,14 +2858,14 @@ async function runAuditCommand(options) {
2789
2858
  remoteLogFile: options.remoteLogFile,
2790
2859
  remoteSessionsDir: options.remoteSessionsDir
2791
2860
  });
2792
- return runSingleRemoteAudit(source2, options);
2861
+ return runSingleRemoteAudit(source2, options, logger);
2793
2862
  }
2794
2863
  if (options.remoteConfig) {
2795
2864
  const sources = loadRemoteConfig(options.remoteConfig);
2796
2865
  if (sources.length === 1) {
2797
- return runSingleRemoteAudit(sources[0], options);
2866
+ return runSingleRemoteAudit(sources[0], options, logger);
2798
2867
  }
2799
- return runMultiRemoteAudit(sources, options);
2868
+ return runMultiRemoteAudit(sources, options, logger);
2800
2869
  }
2801
2870
  const remote = options.remote;
2802
2871
  const source = buildSourceFromFlags({
@@ -2804,7 +2873,7 @@ async function runAuditCommand(options) {
2804
2873
  remoteLogFile: options.remoteLogFile,
2805
2874
  remoteSessionsDir: options.remoteSessionsDir
2806
2875
  });
2807
- return runSingleRemoteAudit(source, options);
2876
+ return runSingleRemoteAudit(source, options, logger);
2808
2877
  }
2809
2878
  function buildRailwayTarget(options) {
2810
2879
  if (options.railwayProject && options.railwayEnvironment && options.railwayService) {
@@ -2816,14 +2885,22 @@ function buildRailwayTarget(options) {
2816
2885
  }
2817
2886
  return void 0;
2818
2887
  }
2819
- async function runLocalAudit(options) {
2888
+ async function runLocalAudit(options, logger) {
2889
+ logger.verbose("Running a local audit.");
2890
+ if (options.logFile) {
2891
+ logger.verbose(`Using explicit local log file: ${options.logFile}`);
2892
+ }
2893
+ if (options.sessionsDir) {
2894
+ logger.verbose(`Using explicit local sessions directory: ${options.sessionsDir}`);
2895
+ }
2820
2896
  const summary = await auditOrNoData({
2821
2897
  logFile: options.logFile,
2822
2898
  sessionsDir: options.sessionsDir,
2823
2899
  since: options.since,
2824
2900
  compare: options.compare,
2825
2901
  dbPath: options.db,
2826
- noDb: options.noDb
2902
+ noDb: options.noDb,
2903
+ onProgress: logger.verbose
2827
2904
  });
2828
2905
  renderOutput(summary, options);
2829
2906
  if (options.push) {
@@ -2838,11 +2915,11 @@ function getComparisonKey(source) {
2838
2915
  }
2839
2916
  return buildComparisonKeyForRemote(source);
2840
2917
  }
2841
- function pullFiles(source, since, keepFiles) {
2918
+ function pullFiles(source, since, keepFiles, onProgress) {
2842
2919
  if (source.transport === "railway") {
2843
- return pullRemoteFilesRailway({ source, since, keepFiles });
2920
+ return pullRemoteFilesRailway({ source, since, keepFiles, onProgress });
2844
2921
  }
2845
- return pullRemoteFiles({ source, since, keepFiles });
2922
+ return pullRemoteFiles({ source, since, keepFiles, onProgress });
2846
2923
  }
2847
2924
  function describeSource(source) {
2848
2925
  if (source.transport === "railway") {
@@ -2853,10 +2930,15 @@ function describeSource(source) {
2853
2930
  function sourceEnvironment(source) {
2854
2931
  return source.transport === "railway" ? "railway" : "remote";
2855
2932
  }
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);
2933
+ async function runSingleRemoteAudit(source, options, logger) {
2934
+ logger.info(`Pulling files from ${describeSource(source)}...`);
2935
+ const pullResult = await pullFiles(
2936
+ source,
2937
+ options.since,
2938
+ options.keepRemoteFiles,
2939
+ logger.verbose
2940
+ );
2941
+ logger.verbose(`Files staged at ${pullResult.localPath}.`);
2860
2942
  try {
2861
2943
  const comparisonKeyOverride = getComparisonKey(source);
2862
2944
  const summary = await auditOrNoData({
@@ -2866,7 +2948,8 @@ async function runSingleRemoteAudit(source, options) {
2866
2948
  compare: options.compare,
2867
2949
  dbPath: options.db,
2868
2950
  noDb: options.noDb,
2869
- comparisonKeyOverride
2951
+ comparisonKeyOverride,
2952
+ onProgress: logger.verbose
2870
2953
  });
2871
2954
  renderOutput(summary, options);
2872
2955
  if (options.push) {
@@ -2882,14 +2965,18 @@ async function runSingleRemoteAudit(source, options) {
2882
2965
  cleanupPullResult(pullResult, options.keepRemoteFiles);
2883
2966
  }
2884
2967
  }
2885
- async function runMultiRemoteAudit(sources, options) {
2968
+ async function runMultiRemoteAudit(sources, options, logger) {
2886
2969
  const results = [];
2887
2970
  const errors = [];
2888
2971
  for (const source of sources) {
2889
- process.stderr.write(`Pulling files from ${source.name} (${describeSource(source)})...
2890
- `);
2972
+ logger.info(`Pulling files from ${source.name} (${describeSource(source)})...`);
2891
2973
  try {
2892
- const pullResult = await pullFiles(source, options.since, options.keepRemoteFiles);
2974
+ const pullResult = await pullFiles(
2975
+ source,
2976
+ options.since,
2977
+ options.keepRemoteFiles,
2978
+ logger.verbose
2979
+ );
2893
2980
  results.push({ source, pullResult });
2894
2981
  } catch (err) {
2895
2982
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -2914,7 +3001,8 @@ ${errorMessages}`);
2914
3001
  compare: options.compare,
2915
3002
  dbPath: options.db,
2916
3003
  noDb: options.noDb,
2917
- comparisonKeyOverride
3004
+ comparisonKeyOverride,
3005
+ onProgress: logger.verbose
2918
3006
  });
2919
3007
  summaries.push({ name: source.name, source, summary });
2920
3008
  }
@@ -3058,32 +3146,43 @@ function cleanupPullResult(pullResult, keepFiles) {
3058
3146
 
3059
3147
  // src/commands/doctor.ts
3060
3148
  async function runDoctorCommand(options) {
3149
+ const logger = createCliLogger({ verbose: options.verbose });
3061
3150
  if (options.railway) {
3151
+ logger.verbose("Inspecting Railway audit readiness.");
3062
3152
  const railwayTarget = buildRailwayTarget2(options);
3063
3153
  const source = buildRailwaySourceFromFlags({
3064
3154
  railway: railwayTarget,
3065
3155
  remoteLogFile: options.remoteLogFile,
3066
3156
  remoteSessionsDir: options.remoteSessionsDir
3067
3157
  });
3068
- const report2 = await runRailwayDoctor({ source });
3158
+ const report2 = await runRailwayDoctor({ source, onProgress: logger.verbose });
3069
3159
  process.stdout.write(`${renderRailwayDoctorReport(report2)}
3070
3160
  `);
3071
3161
  return;
3072
3162
  }
3073
3163
  if (options.remote) {
3164
+ logger.verbose(`Inspecting SSH audit readiness for ${options.remote}.`);
3074
3165
  const source = buildSourceFromFlags({
3075
3166
  remote: options.remote,
3076
3167
  remoteLogFile: options.remoteLogFile,
3077
3168
  remoteSessionsDir: options.remoteSessionsDir
3078
3169
  });
3079
- const report2 = await runRemoteDoctor({ source });
3170
+ const report2 = await runRemoteDoctor({ source, onProgress: logger.verbose });
3080
3171
  process.stdout.write(`${renderRemoteDoctorReport(report2)}
3081
3172
  `);
3082
3173
  return;
3083
3174
  }
3175
+ logger.verbose("Inspecting local OpenClaw audit readiness.");
3176
+ if (options.logFile) {
3177
+ logger.verbose(`Using explicit local log file: ${options.logFile}`);
3178
+ }
3179
+ if (options.sessionsDir) {
3180
+ logger.verbose(`Using explicit local sessions directory: ${options.sessionsDir}`);
3181
+ }
3084
3182
  const report = await doctorOpenClaw({
3085
3183
  logFile: options.logFile,
3086
- sessionsDir: options.sessionsDir
3184
+ sessionsDir: options.sessionsDir,
3185
+ onProgress: logger.verbose
3087
3186
  });
3088
3187
  process.stdout.write(`${renderDoctorReport(report)}
3089
3188
  `);
@@ -3534,6 +3633,9 @@ function parseAuditOptions(raw) {
3534
3633
  case "--dry-run":
3535
3634
  options.dryRun = true;
3536
3635
  break;
3636
+ case "--verbose":
3637
+ options.verbose = true;
3638
+ break;
3537
3639
  case "--fail-above-waste-rate":
3538
3640
  options.failAboveWasteRate = readFloat(arg, argv2[index + 1]);
3539
3641
  index += 1;
@@ -3618,6 +3720,9 @@ function parseDoctorOptions(raw) {
3618
3720
  options.railwayService = readValue(arg, argv2[index + 1]);
3619
3721
  index += 1;
3620
3722
  break;
3723
+ case "--verbose":
3724
+ options.verbose = true;
3725
+ break;
3621
3726
  default:
3622
3727
  throw new Error(`Unknown doctor option "${arg}". Run \`xerg doctor --help\` for usage.`);
3623
3728
  }
@@ -3709,6 +3814,7 @@ Railway options:
3709
3814
  Push options:
3710
3815
  --push Push the audit summary to the Xerg API after computing it
3711
3816
  --dry-run With --push: print the payload to stdout without sending it
3817
+ --verbose Print progress updates to stderr while the audit runs
3712
3818
 
3713
3819
  Threshold options:
3714
3820
  --fail-above-waste-rate <n> Exit with code 3 if structural waste rate exceeds threshold (e.g. 0.30)
@@ -3748,6 +3854,7 @@ Usage:
3748
3854
  Options:
3749
3855
  --log-file <path> Explicit OpenClaw gateway log file to inspect
3750
3856
  --sessions-dir <path> Explicit OpenClaw sessions directory to inspect
3857
+ --verbose Print progress updates to stderr while doctor runs
3751
3858
 
3752
3859
  Remote options (SSH):
3753
3860
  --remote <user@host> SSH target in user@host or user@host:port format