@xerg/cli 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3887,7 +3887,8 @@ function loadPushConfig() {
3887
3887
  if (envKey) {
3888
3888
  return {
3889
3889
  apiKey: envKey,
3890
- apiUrl: envUrl || DEFAULT_API_URL
3890
+ apiUrl: envUrl || DEFAULT_API_URL,
3891
+ source: "env"
3891
3892
  };
3892
3893
  }
3893
3894
  try {
@@ -3896,7 +3897,8 @@ function loadPushConfig() {
3896
3897
  if (parsed.apiKey) {
3897
3898
  return {
3898
3899
  apiKey: parsed.apiKey,
3899
- apiUrl: envUrl || parsed.apiUrl || DEFAULT_API_URL
3900
+ apiUrl: envUrl || parsed.apiUrl || DEFAULT_API_URL,
3901
+ source: "config"
3900
3902
  };
3901
3903
  }
3902
3904
  } catch {
@@ -3905,7 +3907,8 @@ function loadPushConfig() {
3905
3907
  if (storedToken) {
3906
3908
  return {
3907
3909
  apiKey: storedToken,
3908
- apiUrl: envUrl || DEFAULT_API_URL
3910
+ apiUrl: envUrl || DEFAULT_API_URL,
3911
+ source: "stored"
3909
3912
  };
3910
3913
  }
3911
3914
  throw new Error(
@@ -5255,6 +5258,348 @@ function cleanupPullResult(pullResult, keepFiles) {
5255
5258
  }
5256
5259
  }
5257
5260
 
5261
+ // src/commands/login.ts
5262
+ import { styleText } from "util";
5263
+ var DEFAULT_AUTH_URL = "https://xerg.ai/dashboard/settings";
5264
+ var DEFAULT_API_URL2 = "https://api.xerg.ai";
5265
+ var POLL_INTERVAL_MS = 2e3;
5266
+ var POLL_TIMEOUT_MS = 3e5;
5267
+ async function runLoginCommand() {
5268
+ const existing = loadStoredCredentials();
5269
+ if (existing) {
5270
+ process.stderr.write(
5271
+ `Already logged in. Credentials stored at ${getCredentialsPath()}.
5272
+ Run ${colorBold(formatCommand("logout"))} first to re-authenticate.
5273
+ `
5274
+ );
5275
+ return;
5276
+ }
5277
+ const data = await performDeviceLogin();
5278
+ storeCredentials(data.token);
5279
+ const teamInfo = data.teamName ? ` (team: ${data.teamName})` : "";
5280
+ process.stderr.write(
5281
+ `
5282
+ ${colorSuccess("Authenticated successfully")}${teamInfo}.
5283
+ Credentials saved to ${getCredentialsPath()}.
5284
+ `
5285
+ );
5286
+ }
5287
+ async function performDeviceLogin() {
5288
+ const apiUrl = process.env.XERG_API_URL || DEFAULT_API_URL2;
5289
+ const deviceCodeUrl = `${apiUrl}/v1/auth/device-code`;
5290
+ let deviceResponse;
5291
+ try {
5292
+ const res = await fetch(deviceCodeUrl, { method: "POST" });
5293
+ if (!res.ok) {
5294
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
5295
+ }
5296
+ deviceResponse = await res.json();
5297
+ } catch (err) {
5298
+ const msg = err instanceof Error ? err.message : "Unknown error";
5299
+ throw new Error(
5300
+ `Could not start device auth flow (${msg}).
5301
+
5302
+ Alternative: create an API key at ${DEFAULT_AUTH_URL}
5303
+ and set XERG_API_KEY in your environment.`
5304
+ );
5305
+ }
5306
+ const verifyUrl = deviceResponse.verificationUrl || DEFAULT_AUTH_URL;
5307
+ const pollInterval = (deviceResponse.interval || 2) * 1e3;
5308
+ process.stderr.write(
5309
+ `
5310
+ Open this URL in your browser to authenticate:
5311
+
5312
+ ${colorBold(verifyUrl)}
5313
+
5314
+ `
5315
+ );
5316
+ if (deviceResponse.userCode) {
5317
+ process.stderr.write(`Your code: ${colorBold(deviceResponse.userCode)}
5318
+
5319
+ `);
5320
+ }
5321
+ process.stderr.write("Waiting for authentication...\n");
5322
+ await openBrowser(verifyUrl);
5323
+ const tokenUrl = `${apiUrl}/v1/auth/device-token`;
5324
+ const startTime = Date.now();
5325
+ while (Date.now() - startTime < POLL_TIMEOUT_MS) {
5326
+ await sleep(Math.max(pollInterval, POLL_INTERVAL_MS));
5327
+ try {
5328
+ const res = await fetch(tokenUrl, {
5329
+ method: "POST",
5330
+ headers: { "Content-Type": "application/json" },
5331
+ body: JSON.stringify({ deviceCode: deviceResponse.deviceCode })
5332
+ });
5333
+ if (res.status === 200) {
5334
+ return await res.json();
5335
+ }
5336
+ if (res.status === 428) {
5337
+ continue;
5338
+ }
5339
+ if (res.status === 410) {
5340
+ throw new Error(`Device code expired. Please run \`${formatCommand("login")}\` again.`);
5341
+ }
5342
+ const body = await res.json().catch(() => ({}));
5343
+ throw new Error(body.error || `Unexpected response: HTTP ${res.status}`);
5344
+ } catch (err) {
5345
+ if (err instanceof Error && (err.message.includes("expired") || err.message.includes("Unexpected"))) {
5346
+ throw err;
5347
+ }
5348
+ }
5349
+ }
5350
+ throw new Error(`Authentication timed out. Please run \`${formatCommand("login")}\` again.`);
5351
+ }
5352
+ async function openBrowser(url) {
5353
+ const { exec } = await import("child_process");
5354
+ const { platform: platform2 } = await import("os");
5355
+ const commands = {
5356
+ darwin: "open",
5357
+ win32: "start",
5358
+ linux: "xdg-open"
5359
+ };
5360
+ const cmd = commands[platform2()];
5361
+ if (!cmd) return;
5362
+ return new Promise((resolve4) => {
5363
+ exec(`${cmd} ${JSON.stringify(url)}`, () => resolve4());
5364
+ });
5365
+ }
5366
+ function sleep(ms) {
5367
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
5368
+ }
5369
+ function colorBold(text) {
5370
+ return process.stderr.isTTY ? styleText("bold", text) : text;
5371
+ }
5372
+ function colorSuccess(text) {
5373
+ return process.stderr.isTTY ? styleText("green", text) : text;
5374
+ }
5375
+
5376
+ // src/cloud.ts
5377
+ function loadPushConfigOrNull() {
5378
+ try {
5379
+ return loadPushConfig();
5380
+ } catch {
5381
+ return null;
5382
+ }
5383
+ }
5384
+ async function authenticateAndLoadPushConfig() {
5385
+ const data = await performDeviceLogin();
5386
+ storeCredentials(data.token);
5387
+ const teamInfo = data.teamName ? ` (team: ${data.teamName})` : "";
5388
+ process.stderr.write(
5389
+ `
5390
+ Authenticated successfully${teamInfo}.
5391
+ Credentials saved to ${getCredentialsPath()}.
5392
+ `
5393
+ );
5394
+ return loadPushConfig();
5395
+ }
5396
+ function renderCloudDisclaimer() {
5397
+ return [
5398
+ "Xerg Cloud sync and hosted MCP are optional paid workspace features.",
5399
+ "Local audits and compare stay free, and you can keep using Xerg locally if you skip this step."
5400
+ ].join("\n");
5401
+ }
5402
+ function renderMcpCredentialSourceMessage(config) {
5403
+ if (config.source === "stored") {
5404
+ return "Using your stored login token. If hosted MCP requires a workspace API key, create one at xerg.ai/dashboard/settings and set XERG_API_KEY.";
5405
+ }
5406
+ return "Using your workspace API key.";
5407
+ }
5408
+
5409
+ // src/prompts.ts
5410
+ import { confirm, select } from "@inquirer/prompts";
5411
+ function hasPromptTty() {
5412
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
5413
+ }
5414
+ async function promptConfirm(message, defaultValue = true) {
5415
+ return confirm({
5416
+ message,
5417
+ default: defaultValue
5418
+ });
5419
+ }
5420
+ async function promptSelect(message, choices) {
5421
+ return select({
5422
+ message,
5423
+ choices
5424
+ });
5425
+ }
5426
+
5427
+ // src/commands/push.ts
5428
+ import { readFileSync as readFileSync8 } from "fs";
5429
+ async function runPushCommand(options) {
5430
+ const payload = options.file ? loadPayloadFromFile(options.file) : loadLatestCachedAuditPayload();
5431
+ if (options.dryRun) {
5432
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
5433
+ `);
5434
+ return;
5435
+ }
5436
+ const config = loadPushConfig();
5437
+ const auditId = payload.summary.auditId;
5438
+ process.stderr.write(`Pushing audit ${auditId} to ${config.apiUrl}...
5439
+ `);
5440
+ const result = await pushAudit(payload, config);
5441
+ if (result.ok) {
5442
+ process.stderr.write(`Pushed successfully (audit: ${result.auditId}).
5443
+ `);
5444
+ } else {
5445
+ const statusInfo = result.status > 0 ? ` (HTTP ${result.status})` : "";
5446
+ throw new Error(`Push failed${statusInfo}: ${result.message}`);
5447
+ }
5448
+ }
5449
+ function loadPayloadFromFile(filePath) {
5450
+ let raw;
5451
+ try {
5452
+ raw = readFileSync8(filePath, "utf8");
5453
+ } catch {
5454
+ throw new Error(`Cannot read file: ${filePath}`);
5455
+ }
5456
+ let parsed;
5457
+ try {
5458
+ parsed = JSON.parse(raw);
5459
+ } catch {
5460
+ throw new Error(`File is not valid JSON: ${filePath}`);
5461
+ }
5462
+ const payload = parsed;
5463
+ if (!payload.version || !payload.summary || !payload.meta) {
5464
+ throw new Error(
5465
+ `File does not look like an AuditPushPayload (missing version, summary, or meta): ${filePath}`
5466
+ );
5467
+ }
5468
+ return payload;
5469
+ }
5470
+ function loadLatestCachedAuditPayload() {
5471
+ const dbPath = getDefaultDbPath();
5472
+ let summaries;
5473
+ try {
5474
+ summaries = listStoredAuditSummaries(dbPath);
5475
+ } catch {
5476
+ throw new NoDataError(
5477
+ `No local audit database found. Run \`${formatCommand("audit")}\` first, or use \`${formatCommand("push --file <path>")}\`.`
5478
+ );
5479
+ }
5480
+ if (summaries.length === 0) {
5481
+ throw new NoDataError(
5482
+ `No cached audit snapshots found. Run \`${formatCommand("audit")}\` first, or use \`${formatCommand("push --file <path>")}\`.`
5483
+ );
5484
+ }
5485
+ const latest = summaries[0];
5486
+ const meta = buildMeta2(latest);
5487
+ process.stderr.write(
5488
+ `Using most recent cached audit: ${latest.auditId} (${latest.generatedAt})
5489
+ `
5490
+ );
5491
+ return toWirePayload(latest, meta);
5492
+ }
5493
+ function buildMeta2(summary) {
5494
+ const sourceMeta = buildCachedPushSourceMeta(summary);
5495
+ return {
5496
+ cliVersion: getCliVersion(),
5497
+ sourceId: sourceMeta.sourceId,
5498
+ sourceHost: sourceMeta.sourceHost,
5499
+ environment: sourceMeta.environment
5500
+ };
5501
+ }
5502
+
5503
+ // src/commands/connect.ts
5504
+ async function runConnectCommand() {
5505
+ await runConnectFlow();
5506
+ }
5507
+ async function runConnectFlow(options) {
5508
+ if (!options?.skipDisclaimer) {
5509
+ process.stderr.write(`${renderCloudDisclaimer()}
5510
+ `);
5511
+ }
5512
+ let config = loadPushConfigOrNull();
5513
+ if (config) {
5514
+ process.stderr.write("Xerg authentication detected.\n");
5515
+ } else {
5516
+ if (!hasPromptTty()) {
5517
+ process.stderr.write(
5518
+ `No Xerg authentication is configured, and ${formatCommand("connect")} needs an interactive terminal before it can start browser login.
5519
+ Run ${formatCommand("login")} from a TTY, or keep using local audits for free.
5520
+ `
5521
+ );
5522
+ process.exitCode = 1;
5523
+ return false;
5524
+ }
5525
+ const shouldLogin = await promptConfirm("Sign in to Xerg Cloud now?", true);
5526
+ if (!shouldLogin) {
5527
+ process.stderr.write(
5528
+ "Skipped Xerg Cloud setup. You can keep using local audits and compare without connecting.\n"
5529
+ );
5530
+ return false;
5531
+ }
5532
+ config = await authenticateAndLoadPushConfig();
5533
+ }
5534
+ if (!hasPromptTty()) {
5535
+ if (!options?.auditSummary) {
5536
+ process.stderr.write(
5537
+ `Non-interactive mode skips the push prompt. Run ${formatCommand("push")} when you want to sync a cached audit.
5538
+ `
5539
+ );
5540
+ } else {
5541
+ process.stderr.write(
5542
+ `Authentication is ready. Run ${formatCommand("push")} later if you want to sync this audit.
5543
+ `
5544
+ );
5545
+ }
5546
+ return true;
5547
+ }
5548
+ const shouldPush = await promptConfirm(
5549
+ options?.auditSummary ? "Push this audit to Xerg Cloud?" : "Push your latest cached audit to Xerg Cloud?",
5550
+ true
5551
+ );
5552
+ if (!shouldPush) {
5553
+ process.stderr.write(
5554
+ options?.auditSummary ? `Skipped push. Run ${formatCommand("push")} later if you want to sync a cached audit.
5555
+ ` : `Skipped push. Run ${formatCommand("push")} when you want to sync a cached audit.
5556
+ `
5557
+ );
5558
+ return true;
5559
+ }
5560
+ const payload = options?.auditSummary ? toWirePayload(options.auditSummary, buildLocalMeta(options.auditSummary)) : loadStandalonePayload();
5561
+ if (!payload) {
5562
+ return true;
5563
+ }
5564
+ await pushResolvedPayload(payload, config ?? loadPushConfig());
5565
+ return true;
5566
+ }
5567
+ function buildLocalMeta(summary) {
5568
+ const sourceMeta = buildLocalPushSourceMeta(summary.runtime);
5569
+ return {
5570
+ cliVersion: getCliVersion(),
5571
+ sourceId: sourceMeta.sourceId,
5572
+ sourceHost: sourceMeta.sourceHost,
5573
+ environment: sourceMeta.environment
5574
+ };
5575
+ }
5576
+ function loadStandalonePayload() {
5577
+ try {
5578
+ return loadLatestCachedAuditPayload();
5579
+ } catch (error) {
5580
+ if (error instanceof NoDataError || error instanceof Error && error.name === "NoDataError") {
5581
+ process.stderr.write(
5582
+ `${error instanceof Error ? error.message : "No cached audit snapshots found."}
5583
+ `
5584
+ );
5585
+ return null;
5586
+ }
5587
+ throw error;
5588
+ }
5589
+ }
5590
+ async function pushResolvedPayload(payload, config) {
5591
+ process.stderr.write(`Pushing audit ${payload.summary.auditId} to ${config.apiUrl}...
5592
+ `);
5593
+ const result = await pushAudit(payload, config);
5594
+ if (result.ok) {
5595
+ process.stderr.write(`Pushed successfully (audit: ${result.auditId}).
5596
+ `);
5597
+ return;
5598
+ }
5599
+ const statusInfo = result.status > 0 ? ` (HTTP ${result.status})` : "";
5600
+ throw new Error(`Push failed${statusInfo}: ${result.message}`);
5601
+ }
5602
+
5258
5603
  // src/commands/doctor.ts
5259
5604
  async function runDoctorCommand(options) {
5260
5605
  const logger = createCliLogger({ verbose: options.verbose });
@@ -5470,121 +5815,269 @@ function renderRailwayDoctorReport(report) {
5470
5815
  );
5471
5816
  }
5472
5817
  }
5473
- sections.push("", "## Notes", ...report.notes.map((n) => `[railway] ${n}`));
5474
- return sections.join("\n");
5818
+ sections.push("", "## Notes", ...report.notes.map((n) => `[railway] ${n}`));
5819
+ return sections.join("\n");
5820
+ }
5821
+
5822
+ // src/commands/mcp-setup.ts
5823
+ import { existsSync as existsSync2, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "fs";
5824
+ import { dirname as dirname3, join as join8 } from "path";
5825
+ var HOSTED_MCP_URL = "https://mcp.xerg.ai/mcp";
5826
+ async function runMcpSetupCommand() {
5827
+ await runMcpSetupFlow();
5828
+ }
5829
+ async function runMcpSetupFlow() {
5830
+ let config = loadPushConfigOrNull();
5831
+ if (!config) {
5832
+ process.stderr.write(`${renderCloudDisclaimer()}
5833
+ `);
5834
+ process.stderr.write("Hosted MCP requires Xerg Cloud authentication before client setup.\n");
5835
+ }
5836
+ if (!hasPromptTty()) {
5837
+ process.stderr.write(
5838
+ `${formatCommand("mcp-setup")} needs an interactive terminal so it can ask which MCP client you want to configure.
5839
+ `
5840
+ );
5841
+ process.exitCode = 1;
5842
+ return;
5843
+ }
5844
+ if (!config) {
5845
+ const shouldLogin = await promptConfirm("Authenticate with Xerg Cloud now?", true);
5846
+ if (!shouldLogin) {
5847
+ process.stderr.write(
5848
+ `Skipped hosted MCP setup. Run ${formatCommand("mcp-setup")} when you're ready.
5849
+ `
5850
+ );
5851
+ return;
5852
+ }
5853
+ config = await authenticateAndLoadPushConfig();
5854
+ }
5855
+ process.stderr.write(`${renderMcpCredentialSourceMessage(config)}
5856
+ `);
5857
+ const client = await promptSelect("Which MCP client do you want to configure?", [
5858
+ {
5859
+ name: "Cursor",
5860
+ value: "cursor",
5861
+ description: "Project-scoped or global Cursor MCP config"
5862
+ },
5863
+ {
5864
+ name: "Claude Code",
5865
+ value: "claude-code",
5866
+ description: "Project-scoped Claude Code MCP config"
5867
+ },
5868
+ {
5869
+ name: "Other",
5870
+ value: "other",
5871
+ description: "Print the hosted HTTP MCP snippet for another client"
5872
+ }
5873
+ ]);
5874
+ const snippet = JSON.stringify(buildHostedMcpConfig(config), null, 2);
5875
+ if (client === "cursor") {
5876
+ await handleCursorSetup(snippet, config);
5877
+ return;
5878
+ }
5879
+ process.stdout.write(`${snippet}
5880
+ `);
5881
+ if (client === "claude-code") {
5882
+ process.stderr.write(
5883
+ "Add this to `.mcp.json` in your project root, or import the same `mcpServers.xerg` config through Claude Code MCP settings.\n"
5884
+ );
5885
+ return;
5886
+ }
5887
+ process.stderr.write(
5888
+ `Add this as a remote HTTP MCP server in your client. Endpoint: ${HOSTED_MCP_URL}
5889
+ `
5890
+ );
5891
+ }
5892
+ async function handleCursorSetup(snippet, config) {
5893
+ const cursorDir = join8(process.cwd(), ".cursor");
5894
+ const cursorConfigPath = join8(cursorDir, "mcp.json");
5895
+ if (existsSync2(cursorDir)) {
5896
+ const shouldWrite = await promptConfirm(
5897
+ "Write a project-scoped Cursor MCP config to .cursor/mcp.json?",
5898
+ true
5899
+ );
5900
+ if (shouldWrite) {
5901
+ writeCursorConfig(cursorConfigPath, config);
5902
+ process.stderr.write(`Wrote hosted MCP config to ${cursorConfigPath}.
5903
+ `);
5904
+ return;
5905
+ }
5906
+ }
5907
+ process.stdout.write(`${snippet}
5908
+ `);
5909
+ process.stderr.write(
5910
+ "Add this to `.cursor/mcp.json` for a project-scoped Cursor config, or `~/.cursor/mcp.json` for a global Cursor config.\n"
5911
+ );
5912
+ }
5913
+ function buildHostedMcpConfig(config) {
5914
+ return {
5915
+ mcpServers: {
5916
+ xerg: {
5917
+ type: "http",
5918
+ url: HOSTED_MCP_URL,
5919
+ headers: {
5920
+ Authorization: `Bearer ${config.apiKey}`
5921
+ }
5922
+ }
5923
+ }
5924
+ };
5925
+ }
5926
+ function writeCursorConfig(filePath, config) {
5927
+ mkdirSync6(dirname3(filePath), { recursive: true });
5928
+ let parsed = {};
5929
+ if (existsSync2(filePath)) {
5930
+ try {
5931
+ parsed = JSON.parse(readFileSync9(filePath, "utf8"));
5932
+ } catch {
5933
+ throw new Error(`Cursor config is not valid JSON: ${filePath}`);
5934
+ }
5935
+ }
5936
+ const existingServers = parsed.mcpServers;
5937
+ if (existingServers && typeof existingServers !== "object") {
5938
+ throw new Error(`Cursor config has an invalid "mcpServers" value: ${filePath}`);
5939
+ }
5940
+ parsed.mcpServers = {
5941
+ ...existingServers ?? {},
5942
+ xerg: buildHostedMcpConfig(config).mcpServers.xerg
5943
+ };
5944
+ writeFileSync2(filePath, `${JSON.stringify(parsed, null, 2)}
5945
+ `);
5475
5946
  }
5476
5947
 
5477
- // src/commands/login.ts
5478
- import { styleText } from "util";
5479
- var DEFAULT_AUTH_URL = "https://xerg.ai/dashboard/settings";
5480
- var DEFAULT_API_URL2 = "https://api.xerg.ai";
5481
- var POLL_INTERVAL_MS = 2e3;
5482
- var POLL_TIMEOUT_MS = 3e5;
5483
- async function runLoginCommand() {
5484
- const existing = loadStoredCredentials();
5485
- if (existing) {
5948
+ // src/commands/init.ts
5949
+ async function runInitCommand() {
5950
+ if (!hasPromptTty()) {
5486
5951
  process.stderr.write(
5487
- `Already logged in. Credentials stored at ${getCredentialsPath()}.
5488
- Run ${colorBold(formatCommand("logout"))} first to re-authenticate.
5952
+ `${formatCommand("init")} is interactive in this release. Run ${formatCommand("audit")} directly when you need a non-interactive audit.
5489
5953
  `
5490
5954
  );
5955
+ process.exitCode = 1;
5956
+ return;
5957
+ }
5958
+ const candidates = await resolveRuntimeCandidates({ runtime: "auto" });
5959
+ const usable = candidates.filter((candidate) => candidate.usable);
5960
+ if (usable.length === 0) {
5961
+ renderNoDataGuidance();
5962
+ return;
5963
+ }
5964
+ const runtime = await chooseRuntime(usable);
5965
+ if (!runtime) {
5491
5966
  return;
5492
5967
  }
5493
- const apiUrl = process.env.XERG_API_URL || DEFAULT_API_URL2;
5494
- const deviceCodeUrl = `${apiUrl}/v1/auth/device-code`;
5495
- let deviceResponse;
5496
5968
  try {
5497
- const res = await fetch(deviceCodeUrl, { method: "POST" });
5498
- if (!res.ok) {
5499
- throw new Error(`HTTP ${res.status}: ${res.statusText}`);
5969
+ const summary = await auditAgentRuntime({
5970
+ runtime,
5971
+ commandPrefix: formatCommand("")
5972
+ });
5973
+ process.stdout.write(`${renderTerminalSummary(summary)}
5974
+ `);
5975
+ process.stderr.write(
5976
+ `
5977
+ Next: after you make a fix, run ${formatCommand("audit --compare")} to measure the delta.
5978
+ `
5979
+ );
5980
+ const existingAuth = loadPushConfigOrNull();
5981
+ process.stderr.write(
5982
+ `${existingAuth ? "Xerg Cloud authentication is already configured. You can optionally push this audit and set up hosted MCP next." : renderCloudDisclaimer()}
5983
+ `
5984
+ );
5985
+ const shouldConnect = await promptConfirm("Continue with optional Xerg Cloud setup?", true);
5986
+ if (!shouldConnect) {
5987
+ process.stderr.write(
5988
+ `Skipped Xerg Cloud setup. Run ${formatCommand("connect")} or ${formatCommand("mcp-setup")} whenever you want the hosted follow-up.
5989
+ `
5990
+ );
5991
+ return;
5500
5992
  }
5501
- deviceResponse = await res.json();
5502
- } catch (err) {
5503
- const msg = err instanceof Error ? err.message : "Unknown error";
5504
- throw new Error(
5505
- `Could not start device auth flow (${msg}).
5506
-
5507
- Alternative: create an API key at ${DEFAULT_AUTH_URL}
5508
- and set XERG_API_KEY in your environment.`
5993
+ const connected = await runConnectFlow({
5994
+ skipDisclaimer: true,
5995
+ auditSummary: summary
5996
+ });
5997
+ if (!connected) {
5998
+ return;
5999
+ }
6000
+ const shouldSetupMcp = await promptConfirm("Set up hosted MCP now?", true);
6001
+ if (!shouldSetupMcp) {
6002
+ process.stderr.write(
6003
+ `Skipped hosted MCP setup. Run ${formatCommand("mcp-setup")} when you're ready.
6004
+ `
6005
+ );
6006
+ return;
6007
+ }
6008
+ await runMcpSetupFlow();
6009
+ } catch (error) {
6010
+ const message = error instanceof Error ? error.message : "Unknown error";
6011
+ const productName = getRuntimeAdapter(runtime).productName;
6012
+ process.stderr.write(
6013
+ `${[
6014
+ `${productName} audit failed: ${message}`,
6015
+ `Try ${formatCommand(["doctor", "--runtime", runtime])} to inspect the detected paths first.`,
6016
+ `Re-run ${formatCommand("audit --verbose")} for more detail.`
6017
+ ].join("\n")}
6018
+ `
5509
6019
  );
6020
+ process.exitCode = 1;
5510
6021
  }
5511
- const verifyUrl = deviceResponse.verificationUrl || DEFAULT_AUTH_URL;
5512
- const pollInterval = (deviceResponse.interval || 2) * 1e3;
5513
- process.stderr.write(
5514
- `
5515
- Open this URL in your browser to authenticate:
5516
-
5517
- ${colorBold(verifyUrl)}
5518
-
5519
- `
5520
- );
5521
- if (deviceResponse.userCode) {
5522
- process.stderr.write(`Your code: ${colorBold(deviceResponse.userCode)}
5523
-
6022
+ }
6023
+ async function chooseRuntime(candidates) {
6024
+ if (candidates.length === 1) {
6025
+ const candidate = candidates[0];
6026
+ process.stderr.write(`${describeCandidate(candidate)}
5524
6027
  `);
5525
- }
5526
- process.stderr.write("Waiting for authentication...\n");
5527
- await openBrowser(verifyUrl);
5528
- const tokenUrl = `${apiUrl}/v1/auth/device-token`;
5529
- const startTime = Date.now();
5530
- while (Date.now() - startTime < POLL_TIMEOUT_MS) {
5531
- await sleep(Math.max(pollInterval, POLL_INTERVAL_MS));
5532
- try {
5533
- const res = await fetch(tokenUrl, {
5534
- method: "POST",
5535
- headers: { "Content-Type": "application/json" },
5536
- body: JSON.stringify({ deviceCode: deviceResponse.deviceCode })
5537
- });
5538
- if (res.status === 200) {
5539
- const data = await res.json();
5540
- storeCredentials(data.token);
5541
- const teamInfo = data.teamName ? ` (team: ${data.teamName})` : "";
5542
- process.stderr.write(
5543
- `
5544
- ${colorSuccess("Authenticated successfully")}${teamInfo}.
5545
- Credentials saved to ${getCredentialsPath()}.
6028
+ const shouldAudit = await promptConfirm(
6029
+ `Run your first ${candidate.adapter.productName} audit now?`,
6030
+ true
6031
+ );
6032
+ if (!shouldAudit) {
6033
+ process.stderr.write(
6034
+ `Skipped the first audit. Run ${formatCommand(["audit", "--runtime", candidate.adapter.runtime])} when you're ready.
5546
6035
  `
5547
- );
5548
- return;
5549
- }
5550
- if (res.status === 428) {
5551
- continue;
5552
- }
5553
- if (res.status === 410) {
5554
- throw new Error(`Device code expired. Please run \`${formatCommand("login")}\` again.`);
5555
- }
5556
- const body = await res.json().catch(() => ({}));
5557
- throw new Error(body.error || `Unexpected response: HTTP ${res.status}`);
5558
- } catch (err) {
5559
- if (err instanceof Error && (err.message.includes("expired") || err.message.includes("Unexpected"))) {
5560
- throw err;
5561
- }
6036
+ );
6037
+ return null;
5562
6038
  }
6039
+ return candidate.adapter.runtime;
5563
6040
  }
5564
- throw new Error(`Authentication timed out. Please run \`${formatCommand("login")}\` again.`);
5565
- }
5566
- async function openBrowser(url) {
5567
- const { exec } = await import("child_process");
5568
- const { platform: platform2 } = await import("os");
5569
- const commands = {
5570
- darwin: "open",
5571
- win32: "start",
5572
- linux: "xdg-open"
5573
- };
5574
- const cmd = commands[platform2()];
5575
- if (!cmd) return;
5576
- return new Promise((resolve4) => {
5577
- exec(`${cmd} ${JSON.stringify(url)}`, () => resolve4());
5578
- });
6041
+ return promptSelect("Choose the local runtime to audit first.", [
6042
+ ...candidates.map((candidate) => ({
6043
+ name: candidate.adapter.productName,
6044
+ value: candidate.adapter.runtime,
6045
+ description: describeSources(candidate)
6046
+ }))
6047
+ ]);
5579
6048
  }
5580
- function sleep(ms) {
5581
- return new Promise((resolve4) => setTimeout(resolve4, ms));
6049
+ function describeCandidate(candidate) {
6050
+ return `Found local ${candidate.adapter.productName} data (${describeSources(candidate)}).`;
5582
6051
  }
5583
- function colorBold(text) {
5584
- return process.stderr.isTTY ? styleText("bold", text) : text;
6052
+ function describeSources(candidate) {
6053
+ const kinds = new Set(candidate.sources.map((source) => source.kind));
6054
+ const details = [
6055
+ kinds.has("gateway") ? "gateway logs" : null,
6056
+ kinds.has("sessions") ? "session transcripts" : null
6057
+ ].filter((detail) => detail !== null);
6058
+ return details.join(" and ");
5585
6059
  }
5586
- function colorSuccess(text) {
5587
- return process.stderr.isTTY ? styleText("green", text) : text;
6060
+ function renderNoDataGuidance() {
6061
+ const openclawDefaults = getRuntimeAdapter("openclaw").defaultPaths();
6062
+ const hermesDefaults = getRuntimeAdapter("hermes").defaultPaths();
6063
+ process.stderr.write(
6064
+ `${[
6065
+ "No local OpenClaw or Hermes data was detected in the default locations Xerg checked.",
6066
+ "",
6067
+ "Checked defaults:",
6068
+ `- OpenClaw gateway logs: ${openclawDefaults.gatewayPattern}`,
6069
+ `- OpenClaw session transcripts: ${openclawDefaults.sessionsPattern}`,
6070
+ `- Hermes gateway logs: ${hermesDefaults.gatewayPattern}`,
6071
+ `- Hermes session transcripts: ${hermesDefaults.sessionsPattern}`,
6072
+ "",
6073
+ "Next steps:",
6074
+ `- Local OpenClaw paths: ${formatCommand("audit --runtime openclaw --log-file /path/to/openclaw.log")} or ${formatCommand("audit --runtime openclaw --sessions-dir /path/to/sessions")}`,
6075
+ `- Local Hermes paths: ${formatCommand("audit --runtime hermes --log-file ~/.hermes/logs/agent.log")} or ${formatCommand("audit --runtime hermes --sessions-dir ~/.hermes/sessions")}`,
6076
+ `- Remote OpenClaw only: ${formatCommand("audit --remote user@host")}`,
6077
+ `- Railway OpenClaw only: ${formatCommand("audit --railway")}`
6078
+ ].join("\n")}
6079
+ `
6080
+ );
5588
6081
  }
5589
6082
 
5590
6083
  // src/commands/logout.ts
@@ -5598,103 +6091,51 @@ function runLogoutCommand() {
5598
6091
  }
5599
6092
  }
5600
6093
 
5601
- // src/commands/push.ts
5602
- import { readFileSync as readFileSync8 } from "fs";
5603
- async function runPushCommand(options) {
5604
- const payload = options.file ? loadPayloadFromFile(options.file) : loadPayloadFromCache();
5605
- if (options.dryRun) {
5606
- process.stdout.write(`${JSON.stringify(payload, null, 2)}
5607
- `);
5608
- return;
5609
- }
5610
- const config = loadPushConfig();
5611
- const auditId = payload.summary.auditId;
5612
- process.stderr.write(`Pushing audit ${auditId} to ${config.apiUrl}...
5613
- `);
5614
- const result = await pushAudit(payload, config);
5615
- if (result.ok) {
5616
- process.stderr.write(`Pushed successfully (audit: ${result.auditId}).
5617
- `);
5618
- } else {
5619
- const statusInfo = result.status > 0 ? ` (HTTP ${result.status})` : "";
5620
- throw new Error(`Push failed${statusInfo}: ${result.message}`);
5621
- }
5622
- }
5623
- function loadPayloadFromFile(filePath) {
5624
- let raw;
5625
- try {
5626
- raw = readFileSync8(filePath, "utf8");
5627
- } catch {
5628
- throw new Error(`Cannot read file: ${filePath}`);
5629
- }
5630
- let parsed;
5631
- try {
5632
- parsed = JSON.parse(raw);
5633
- } catch {
5634
- throw new Error(`File is not valid JSON: ${filePath}`);
5635
- }
5636
- const payload = parsed;
5637
- if (!payload.version || !payload.summary || !payload.meta) {
5638
- throw new Error(
5639
- `File does not look like an AuditPushPayload (missing version, summary, or meta): ${filePath}`
5640
- );
5641
- }
5642
- return payload;
5643
- }
5644
- function loadPayloadFromCache() {
5645
- const dbPath = getDefaultDbPath();
5646
- let summaries;
5647
- try {
5648
- summaries = listStoredAuditSummaries(dbPath);
5649
- } catch {
5650
- throw new NoDataError(
5651
- `No local audit database found. Run \`${formatCommand("audit")}\` first, or use \`${formatCommand("push --file <path>")}\`.`
5652
- );
5653
- }
5654
- if (summaries.length === 0) {
5655
- throw new NoDataError(
5656
- `No cached audit snapshots found. Run \`${formatCommand("audit")}\` first, or use \`${formatCommand("push --file <path>")}\`.`
5657
- );
5658
- }
5659
- const latest = summaries[0];
5660
- const meta = buildMeta2(latest);
5661
- process.stderr.write(
5662
- `Using most recent cached audit: ${latest.auditId} (${latest.generatedAt})
5663
- `
5664
- );
5665
- return toWirePayload(latest, meta);
5666
- }
5667
- function buildMeta2(summary) {
5668
- const sourceMeta = buildCachedPushSourceMeta(summary);
5669
- return {
5670
- cliVersion: getCliVersion(),
5671
- sourceId: sourceMeta.sourceId,
5672
- sourceHost: sourceMeta.sourceHost,
5673
- environment: sourceMeta.environment
5674
- };
5675
- }
5676
-
5677
6094
  // src/help.ts
5678
6095
  function renderRootHelp(version, display) {
5679
6096
  return `${display.name} ${version}
5680
6097
 
5681
- Waste intelligence for OpenClaw and Hermes workflows plus local Cursor usage CSVs.
6098
+ Waste intelligence for OpenClaw and Hermes workflows.
5682
6099
 
5683
6100
  Usage:
5684
6101
  ${formatCommand("<command> [options]", display.prefix)}
5685
6102
 
5686
- Commands:
6103
+ Getting started:
6104
+ init Detect local runtimes, run a first audit, and offer optional cloud follow-up.
6105
+
6106
+ Audit and inspect:
5687
6107
  audit Analyze OpenClaw or Hermes logs, or a local Cursor usage CSV.
5688
6108
  doctor Inspect OpenClaw or Hermes sources, or a local Cursor usage CSV.
6109
+
6110
+ Cloud:
6111
+ connect Authenticate and optionally push your latest audit to Xerg Cloud.
5689
6112
  push Push a cached audit snapshot to the Xerg API.
5690
6113
  login Authenticate with the Xerg API via browser.
5691
6114
  logout Remove stored Xerg API credentials.
6115
+ mcp-setup Generate hosted MCP client configuration.
5692
6116
 
5693
6117
  Global options:
5694
6118
  -h, --help Show help
5695
6119
  -v, --version Show version
5696
6120
  `;
5697
6121
  }
6122
+ function renderInitHelp(commandPrefix) {
6123
+ return `${formatCommand("init", commandPrefix)}
6124
+
6125
+ Detect local OpenClaw or Hermes runtimes, run a first audit, and offer optional cloud follow-up.
6126
+
6127
+ Usage:
6128
+ ${formatCommand("init", commandPrefix)}
6129
+
6130
+ Notes:
6131
+ - Interactive only in v1
6132
+ - Uses local runtime auto-detection
6133
+ - Runs a first local audit with snapshot persistence enabled
6134
+ - Offers optional Xerg Cloud connect and hosted MCP setup after a successful audit
6135
+
6136
+ -h, --help Show help
6137
+ `;
6138
+ }
5698
6139
  function renderAuditHelp(commandPrefix) {
5699
6140
  return `${formatCommand("audit", commandPrefix)}
5700
6141
 
@@ -5761,7 +6202,7 @@ Options:
5761
6202
 
5762
6203
  Authentication:
5763
6204
  Set XERG_API_KEY in your environment, add "apiKey" to ~/.xerg/config.json,
5764
- or run \`${formatCommand("login", commandPrefix)}\` to authenticate via browser.
6205
+ or run \`${formatCommand("connect", commandPrefix)}\` / \`${formatCommand("login", commandPrefix)}\` to authenticate via browser.
5765
6206
  Browser login stores a token at ~/.config/xerg/credentials.json by default.
5766
6207
  `;
5767
6208
  }
@@ -5798,6 +6239,40 @@ Railway options (OpenClaw only):
5798
6239
  -h, --help Show help
5799
6240
  `;
5800
6241
  }
6242
+ function renderConnectHelp(commandPrefix) {
6243
+ return `${formatCommand("connect", commandPrefix)}
6244
+
6245
+ Authenticate with Xerg Cloud and optionally push the latest audit.
6246
+
6247
+ Usage:
6248
+ ${formatCommand("connect", commandPrefix)}
6249
+
6250
+ Notes:
6251
+ - Shows paid-workspace disclosure before hosted setup
6252
+ - Reuses existing auth from XERG_API_KEY, ~/.xerg/config.json, or stored browser login
6253
+ - Standalone non-interactive mode reports auth status and skips the push prompt
6254
+ - When called after ${formatCommand("init", commandPrefix)}, it can push the in-memory audit directly
6255
+
6256
+ -h, --help Show help
6257
+ `;
6258
+ }
6259
+ function renderMcpSetupHelp(commandPrefix) {
6260
+ return `${formatCommand("mcp-setup", commandPrefix)}
6261
+
6262
+ Generate hosted MCP client configuration for Cursor, Claude Code, or another MCP client.
6263
+
6264
+ Usage:
6265
+ ${formatCommand("mcp-setup", commandPrefix)}
6266
+
6267
+ Notes:
6268
+ - Interactive in v1 because client selection is prompt-driven
6269
+ - Uses the hosted MCP endpoint at https://mcp.xerg.ai/mcp
6270
+ - Can write a project-scoped Cursor config when .cursor/ already exists
6271
+ - Local audits and compare stay available even if you skip hosted MCP setup
6272
+
6273
+ -h, --help Show help
6274
+ `;
6275
+ }
5801
6276
 
5802
6277
  // src/index.ts
5803
6278
  var VERSION = getCliVersion();
@@ -5831,6 +6306,11 @@ async function run() {
5831
6306
  });
5832
6307
  return;
5833
6308
  }
6309
+ if (command === "init") {
6310
+ parseBareCommandOptions(argv.slice(1), renderInitHelp(commandDisplay.prefix), "init");
6311
+ await runInitCommand();
6312
+ return;
6313
+ }
5834
6314
  if (command === "doctor") {
5835
6315
  const options = parseDoctorOptions(argv.slice(1));
5836
6316
  await runDoctorCommand({
@@ -5844,6 +6324,11 @@ async function run() {
5844
6324
  await runPushCommand(options);
5845
6325
  return;
5846
6326
  }
6327
+ if (command === "connect") {
6328
+ parseBareCommandOptions(argv.slice(1), renderConnectHelp(commandDisplay.prefix), "connect");
6329
+ await runConnectCommand();
6330
+ return;
6331
+ }
5847
6332
  if (command === "login") {
5848
6333
  await runLoginCommand();
5849
6334
  return;
@@ -5852,10 +6337,31 @@ async function run() {
5852
6337
  runLogoutCommand();
5853
6338
  return;
5854
6339
  }
6340
+ if (command === "mcp-setup") {
6341
+ parseBareCommandOptions(argv.slice(1), renderMcpSetupHelp(commandDisplay.prefix), "mcp-setup");
6342
+ await runMcpSetupCommand();
6343
+ return;
6344
+ }
5855
6345
  throw new Error(
5856
6346
  `Unknown command "${command}". Run \`${formatCommand("--help", commandDisplay.prefix)}\` to see available commands.`
5857
6347
  );
5858
6348
  }
6349
+ function parseBareCommandOptions(raw, helpText, commandName) {
6350
+ const argv2 = expandEqualsArgs(raw);
6351
+ for (const arg of argv2) {
6352
+ switch (arg) {
6353
+ case "--help":
6354
+ case "-h":
6355
+ process.stdout.write(helpText);
6356
+ process.exit(0);
6357
+ break;
6358
+ default:
6359
+ throw new Error(
6360
+ `Unknown ${commandName} option "${arg}". Run \`${formatCommand([commandName, "--help"], commandDisplay.prefix)}\` for usage.`
6361
+ );
6362
+ }
6363
+ }
6364
+ }
5859
6365
  function parseAuditOptions(raw) {
5860
6366
  const argv2 = expandEqualsArgs(raw);
5861
6367
  const options = {};