@xerg/cli 0.3.0 → 0.5.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
@@ -1280,6 +1280,7 @@ function hydrateAuditSummary(summary) {
1280
1280
  opportunityByKind: summary.opportunityByKind?.length > 0 ? summary.opportunityByKind : buildTaxonomyBuckets(summary.findings, "opportunity"),
1281
1281
  spendByDay: summary.spendByDay ?? [],
1282
1282
  wasteByDay: summary.wasteByDay ?? [],
1283
+ recommendations: summary.recommendations ?? [],
1283
1284
  notes: summary.notes ?? [],
1284
1285
  pricingCoverage: summary.pricingCoverage ?? null,
1285
1286
  cursorUsage: summary.cursorUsage ?? null
@@ -1416,6 +1417,7 @@ function buildCursorUsageFindings(runs) {
1416
1417
  summary,
1417
1418
  scope: "global",
1418
1419
  scopeId: "all",
1420
+ scopeLabel: "Cursor usage",
1419
1421
  costImpactUsd: cacheImpactUsd,
1420
1422
  details: {
1421
1423
  cacheReadShare: round3(cacheReadShare),
@@ -1443,6 +1445,7 @@ function buildCursorUsageFindings(runs) {
1443
1445
  summary: `Max mode accounts for ${(maxModeSpendShare * 100).toFixed(0)}% of billed spend across ${maxModeCalls.length} paid row${maxModeCalls.length === 1 ? "" : "s"}. This is a strong candidate for splitting work between premium and standard passes.`,
1444
1446
  scope: "global",
1445
1447
  scopeId: "all",
1448
+ scopeLabel: "Cursor usage",
1446
1449
  costImpactUsd: round3(maxModeSpendUsd * 0.2),
1447
1450
  details: {
1448
1451
  maxModeSpendUsd: round3(maxModeSpendUsd),
@@ -1502,6 +1505,7 @@ function buildFindings(runs) {
1502
1505
  summary: `${retryCandidates.length} failed call${retryCandidates.length === 1 ? "" : "s"} were followed by additional work, making their spend pure retry overhead.`,
1503
1506
  scope: "global",
1504
1507
  scopeId: "all",
1508
+ scopeLabel: "workspace",
1505
1509
  costImpactUsd: round4(retryCost),
1506
1510
  details: {
1507
1511
  failedCallCount: retryCandidates.length
@@ -1529,7 +1533,8 @@ function buildFindings(runs) {
1529
1533
  title: `Workflow "${run2.workflow}" ran beyond efficient loop bounds`,
1530
1534
  summary: `This run reached ${maxIteration} iterations. Xerg treats the spend after iteration 5 as likely loop waste.`,
1531
1535
  scope: "run",
1532
- scopeId: run2.id,
1536
+ scopeId: run2.workflow,
1537
+ scopeLabel: run2.workflow,
1533
1538
  costImpactUsd: round4(loopCost),
1534
1539
  details: {
1535
1540
  workflow: run2.workflow,
@@ -1566,6 +1571,7 @@ function buildFindings(runs) {
1566
1571
  summary: `Xerg found ${outlierRuns.length} run${outlierRuns.length === 1 ? "" : "s"} in this workflow with input token volume far above the workflow average.`,
1567
1572
  scope: "workflow",
1568
1573
  scopeId: workflow,
1574
+ scopeLabel: workflow,
1569
1575
  costImpactUsd: round4(outlierCost),
1570
1576
  details: {
1571
1577
  workflow,
@@ -1590,6 +1596,7 @@ function buildFindings(runs) {
1590
1596
  summary: "This workflow name looks like a recurring heartbeat or monitoring loop. Review whether the cadence and model tier are justified.",
1591
1597
  scope: "workflow",
1592
1598
  scopeId: workflow,
1599
+ scopeLabel: workflow,
1593
1600
  costImpactUsd: round4(idleCost),
1594
1601
  details: {
1595
1602
  workflow
@@ -1611,6 +1618,7 @@ function buildFindings(runs) {
1611
1618
  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.",
1612
1619
  scope: "workflow",
1613
1620
  scopeId: workflow,
1621
+ scopeLabel: workflow,
1614
1622
  costImpactUsd: round4(spend * 0.3),
1615
1623
  details: {
1616
1624
  workflow,
@@ -1703,6 +1711,247 @@ function estimateCostUsd(provider, model, inputTokens, outputTokens) {
1703
1711
  return Number((inputCost + outputCost).toFixed(8));
1704
1712
  }
1705
1713
 
1714
+ // ../core/src/recommendations.ts
1715
+ var PRIORITY_RANK = {
1716
+ fix_now: 2,
1717
+ test_next: 1,
1718
+ watch: 0
1719
+ };
1720
+ var CONFIDENCE_RANK = {
1721
+ high: 2,
1722
+ medium: 1,
1723
+ low: 0
1724
+ };
1725
+ function roundCurrency(value) {
1726
+ return Number(value.toFixed(6));
1727
+ }
1728
+ function roundPct(value) {
1729
+ return Number(value.toFixed(4));
1730
+ }
1731
+ function formatScopeLabel(finding) {
1732
+ if (finding.scope === "global") {
1733
+ return "workspace";
1734
+ }
1735
+ return finding.scopeLabel?.trim() || finding.scopeId;
1736
+ }
1737
+ function normalizeRecommendationScope(finding) {
1738
+ if (finding.scope === "global") {
1739
+ return "workspace";
1740
+ }
1741
+ return "workflow";
1742
+ }
1743
+ function stableScopeKeyForFinding(finding) {
1744
+ if (finding.scope === "global") {
1745
+ return "workspace";
1746
+ }
1747
+ return finding.scopeId;
1748
+ }
1749
+ function resolveScopeId(finding, scope) {
1750
+ return scope === "workspace" ? "workspace" : stableScopeKeyForFinding(finding);
1751
+ }
1752
+ function dedupKeyForFinding(finding) {
1753
+ const scope = normalizeRecommendationScope(finding);
1754
+ return `${finding.kind}:${scope}:${stableScopeKeyForFinding(finding)}`;
1755
+ }
1756
+ function recommendationIdForFinding(finding, implementationSurface) {
1757
+ const scope = normalizeRecommendationScope(finding);
1758
+ return sha1(
1759
+ `rec:v2:${finding.kind}:${scope}:${stableScopeKeyForFinding(finding)}:${implementationSurface}`
1760
+ );
1761
+ }
1762
+ function compareRecommendations(left, right) {
1763
+ const priorityDelta = PRIORITY_RANK[right.priorityBucket] - PRIORITY_RANK[left.priorityBucket];
1764
+ if (priorityDelta !== 0) {
1765
+ return priorityDelta;
1766
+ }
1767
+ if (right.estimatedSavingsUsd !== left.estimatedSavingsUsd) {
1768
+ return right.estimatedSavingsUsd - left.estimatedSavingsUsd;
1769
+ }
1770
+ const confidenceDelta = CONFIDENCE_RANK[right.confidence] - CONFIDENCE_RANK[left.confidence];
1771
+ if (confidenceDelta !== 0) {
1772
+ return confidenceDelta;
1773
+ }
1774
+ return left.title.localeCompare(right.title);
1775
+ }
1776
+ var templatesByKind = {
1777
+ "retry-waste": {
1778
+ priorityBucket: "fix_now",
1779
+ implementationSurface: "retry_policy",
1780
+ category: "structural_efficiency",
1781
+ severity: "high",
1782
+ effort: "low",
1783
+ titleFn: (finding) => `Reduce retry waste in ${formatScopeLabel(finding)}`,
1784
+ summaryFn: (finding) => `${finding.summary} This is confirmed retry overhead, so it is a fix-now issue rather than an experiment.`,
1785
+ whereToChangeFn: (finding) => `Reduce retries or add exponential backoff in the retry wrapper for ${formatScopeLabel(finding)}.`,
1786
+ validationPlanFn: () => "Ship the change, then rerun `xerg audit --compare --push` against the same source. Retry waste should drop materially on the next audit.",
1787
+ actionsFn: () => [
1788
+ "Lower max retry count.",
1789
+ "Add exponential backoff if none exists.",
1790
+ "Log or alert when retries hit the cap."
1791
+ ]
1792
+ },
1793
+ "loop-waste": {
1794
+ priorityBucket: "fix_now",
1795
+ implementationSurface: "loop_guard",
1796
+ category: "structural_efficiency",
1797
+ severity: "high",
1798
+ effort: "medium",
1799
+ titleFn: (finding) => `Cap loop depth in ${formatScopeLabel(finding)}`,
1800
+ summaryFn: (finding) => `${finding.summary} This is confirmed loop waste and should be fixed before chasing lower-confidence opportunities.`,
1801
+ whereToChangeFn: (finding) => `Cap iteration depth or add an early-exit guard in the loop controller for ${formatScopeLabel(finding)}.`,
1802
+ validationPlanFn: () => "Ship the change, then rerun `xerg audit --compare --push`. Loop waste and call volume should fall on the next audit.",
1803
+ actionsFn: () => [
1804
+ "Add a hard iteration cap.",
1805
+ "Add a no-progress exit.",
1806
+ "Emit a warning when the cap is hit."
1807
+ ]
1808
+ },
1809
+ "context-outlier": {
1810
+ priorityBucket: "test_next",
1811
+ implementationSurface: "prompt_builder",
1812
+ category: "context_hygiene",
1813
+ severity: "medium",
1814
+ effort: "medium",
1815
+ titleFn: (finding) => `Trim context in ${formatScopeLabel(finding)}`,
1816
+ summaryFn: (finding) => `${finding.summary} Treat this as a reversible optimization test rather than proven waste.`,
1817
+ whereToChangeFn: (finding) => `Trim input context in the prompt builder for ${formatScopeLabel(finding)}.`,
1818
+ validationPlanFn: () => "Ship the change, then rerun `xerg audit --compare --push`. Input tokens and context-related spend should fall.",
1819
+ actionsFn: () => [
1820
+ "Drop stale context blocks.",
1821
+ "Summarize long histories.",
1822
+ "Cap prompt size for this workflow."
1823
+ ]
1824
+ },
1825
+ "idle-spend": {
1826
+ priorityBucket: "test_next",
1827
+ implementationSurface: "scheduler",
1828
+ category: "cadence_activity",
1829
+ severity: "medium",
1830
+ effort: "low",
1831
+ titleFn: (finding) => `Review cadence for ${formatScopeLabel(finding)}`,
1832
+ summaryFn: (finding) => `${finding.summary} This is usually a scheduling decision, so validate it by lowering or gating activity instead of rewriting the workflow.`,
1833
+ whereToChangeFn: (finding) => `Lower cadence or move to event-driven triggers for ${formatScopeLabel(finding)}.`,
1834
+ validationPlanFn: () => "Ship the change, then rerun `xerg audit --compare --push`. Idle spend per day should drop.",
1835
+ actionsFn: () => [
1836
+ "Reduce poll frequency.",
1837
+ "Gate runs behind a real trigger.",
1838
+ "Disable runs during dead windows."
1839
+ ]
1840
+ },
1841
+ "candidate-downgrade": {
1842
+ priorityBucket: "test_next",
1843
+ implementationSurface: "model_routing",
1844
+ category: "model_fit",
1845
+ severity: "low",
1846
+ effort: "low",
1847
+ titleFn: (finding) => `Evaluate a cheaper model for ${formatScopeLabel(finding)}`,
1848
+ summaryFn: (finding) => `${finding.summary} This is an A/B candidate: spend may fall, but quality needs to be checked before rollout.`,
1849
+ whereToChangeFn: (finding) => `Re-map ${formatScopeLabel(finding)} to a cheaper model in the routing layer.`,
1850
+ validationPlanFn: () => "A/B the cheaper model, then rerun `xerg audit --compare --push`. Confirm spend drops without a quality regression.",
1851
+ actionsFn: () => [
1852
+ "Try a cheaper model on this workflow.",
1853
+ "Compare quality on a labeled sample.",
1854
+ "Roll out if acceptable."
1855
+ ]
1856
+ },
1857
+ "cache-carryover": {
1858
+ priorityBucket: "test_next",
1859
+ implementationSurface: "user_behavior",
1860
+ category: "context_hygiene",
1861
+ severity: "medium",
1862
+ effort: "low",
1863
+ titleFn: () => "Reset or summarize long Cursor chats",
1864
+ summaryFn: (finding) => `${finding.summary} This is about session behavior rather than code structure, so the intervention is to reset context more aggressively.`,
1865
+ whereToChangeFn: () => "Reset or summarize long Cursor chats instead of continuing the same session.",
1866
+ validationPlanFn: () => "Change session behavior, then push a new Cursor usage audit with `--compare --push`. Cache-read share should fall.",
1867
+ actionsFn: () => [
1868
+ "Summarize context into a short recall note.",
1869
+ "Start a fresh chat.",
1870
+ "Carry forward only the recall note and necessary facts."
1871
+ ]
1872
+ },
1873
+ "max-mode-concentration": {
1874
+ priorityBucket: "test_next",
1875
+ implementationSurface: "user_behavior",
1876
+ category: "model_fit",
1877
+ severity: "medium",
1878
+ effort: "low",
1879
+ titleFn: () => "Reserve max mode for the hardest Cursor turns",
1880
+ summaryFn: (finding) => `${finding.summary} The likely fix is changing mode habits or escalation rules, not modifying a prompt builder.`,
1881
+ whereToChangeFn: () => "Reserve max mode for the hardest Cursor turns; default to standard mode.",
1882
+ validationPlanFn: () => "Change your mode defaults, then push a new Cursor usage audit with `--compare --push`. Max-mode spend share should fall.",
1883
+ actionsFn: () => [
1884
+ "Start in standard mode.",
1885
+ "Escalate only when standard fails.",
1886
+ "Review any auto-escalation habit."
1887
+ ]
1888
+ }
1889
+ };
1890
+ var unknownTemplate = {
1891
+ priorityBucket: "watch",
1892
+ implementationSurface: "other",
1893
+ category: "other",
1894
+ severity: "low",
1895
+ effort: "medium",
1896
+ titleFn: (finding) => `Review ${finding.title}`,
1897
+ summaryFn: (finding) => finding.summary,
1898
+ whereToChangeFn: (finding) => `Review ${formatScopeLabel(finding)} with your operator.`,
1899
+ validationPlanFn: () => "Ship any change, then rerun `xerg audit --compare --push` to confirm impact.",
1900
+ actionsFn: () => [
1901
+ "Investigate the finding details.",
1902
+ "Decide whether to act.",
1903
+ "Re-audit after the change."
1904
+ ]
1905
+ };
1906
+ function buildSingleRecommendation(summary, finding) {
1907
+ const template = templatesByKind[finding.kind] ?? unknownTemplate;
1908
+ const scope = normalizeRecommendationScope(finding);
1909
+ const scopeId = resolveScopeId(finding, scope);
1910
+ const scopeLabel = formatScopeLabel(finding);
1911
+ const estimatedSavingsPct = summary.totalSpendUsd === 0 ? 0 : roundPct(Math.min(Math.max(finding.costImpactUsd / summary.totalSpendUsd, 0), 1));
1912
+ return {
1913
+ id: recommendationIdForFinding(finding, template.implementationSurface),
1914
+ findingId: finding.id,
1915
+ kind: finding.kind,
1916
+ title: template.titleFn(finding),
1917
+ summary: template.summaryFn(finding),
1918
+ priorityBucket: template.priorityBucket,
1919
+ recommendedOrder: 0,
1920
+ implementationSurface: template.implementationSurface,
1921
+ category: template.category,
1922
+ severity: template.severity,
1923
+ estimatedSavingsUsd: roundCurrency(finding.costImpactUsd),
1924
+ estimatedSavingsPct,
1925
+ confidence: finding.confidence,
1926
+ effort: template.effort,
1927
+ scope,
1928
+ scopeId,
1929
+ scopeLabel,
1930
+ whereToChange: template.whereToChangeFn(finding),
1931
+ validationPlan: template.validationPlanFn(finding),
1932
+ actions: template.actionsFn(finding)
1933
+ };
1934
+ }
1935
+ function buildRecommendations(summary) {
1936
+ const deduped = /* @__PURE__ */ new Map();
1937
+ for (const finding of summary.findings) {
1938
+ const recommendation = buildSingleRecommendation(summary, finding);
1939
+ const key = dedupKeyForFinding(finding);
1940
+ const existing = deduped.get(key);
1941
+ if (!existing) {
1942
+ deduped.set(key, recommendation);
1943
+ continue;
1944
+ }
1945
+ if (recommendation.estimatedSavingsUsd > existing.estimatedSavingsUsd || recommendation.estimatedSavingsUsd === existing.estimatedSavingsUsd && CONFIDENCE_RANK[recommendation.confidence] > CONFIDENCE_RANK[existing.confidence]) {
1946
+ deduped.set(key, recommendation);
1947
+ }
1948
+ }
1949
+ return [...deduped.values()].sort(compareRecommendations).map((recommendation, index) => ({
1950
+ ...recommendation,
1951
+ recommendedOrder: index + 1
1952
+ }));
1953
+ }
1954
+
1706
1955
  // ../core/src/report/timeseries.ts
1707
1956
  function round5(value) {
1708
1957
  return Number(value.toFixed(6));
@@ -1856,7 +2105,7 @@ function buildAuditSummary(input) {
1856
2105
  const generatedAt = isoNow();
1857
2106
  const spendByDay = buildSpendByDay(input.runs);
1858
2107
  const observedDays = buildObservedUtcDayRange(input.runs);
1859
- return {
2108
+ const summary = {
1860
2109
  auditId: sha1(
1861
2110
  `${generatedAt}:${input.runs.length}:${input.sources.map((source) => source.path).join("|")}`
1862
2111
  ),
@@ -1900,6 +2149,7 @@ function buildAuditSummary(input) {
1900
2149
  spendByDay,
1901
2150
  wasteByDay: buildWasteByDay(input.wasteAttributions, observedDays, wasteSpendUsd),
1902
2151
  findings: input.findings,
2152
+ recommendations: [],
1903
2153
  notes: [
1904
2154
  "Cost per outcome is intentionally unavailable in v0. Xerg is measuring waste intelligence only.",
1905
2155
  "Opportunity findings are directional recommendations, not proven waste."
@@ -1907,6 +2157,8 @@ function buildAuditSummary(input) {
1907
2157
  sourceFiles: input.sources,
1908
2158
  dbPath: input.dbPath
1909
2159
  };
2160
+ summary.recommendations = buildRecommendations(summary);
2161
+ return summary;
1910
2162
  }
1911
2163
 
1912
2164
  // ../core/src/runtime.ts
@@ -3169,109 +3421,6 @@ async function auditCursorUsageCsv(options) {
3169
3421
  return summary;
3170
3422
  }
3171
3423
 
3172
- // ../core/src/recommendations.ts
3173
- var templatesByKind = {
3174
- "retry-waste": {
3175
- actionType: "other",
3176
- titleFn: () => "Add retry backoff or reduce retry attempts",
3177
- descriptionFn: (f) => `${f.summary} Consider adding exponential backoff or reducing the maximum retry count to eliminate this overhead.`,
3178
- suggestedChangeFn: (f) => ({
3179
- strategy: "exponential-backoff",
3180
- maxRetries: 3,
3181
- failedCallCount: f.details.failedCallCount
3182
- })
3183
- },
3184
- "loop-waste": {
3185
- actionType: "other",
3186
- titleFn: (f) => `Cap iteration depth for ${extractWorkflow(f)}`,
3187
- descriptionFn: (f) => `${f.summary} Adding an iteration limit or early-exit condition would prevent runaway loops from burning spend.`,
3188
- suggestedChangeFn: (f) => ({
3189
- strategy: "iteration-cap",
3190
- suggestedMaxIterations: 5,
3191
- observedMaxIteration: f.details.maxIteration
3192
- })
3193
- },
3194
- "context-outlier": {
3195
- actionType: "prompt-trim",
3196
- titleFn: (f) => `Trim context for ${extractWorkflow(f)}`,
3197
- descriptionFn: (f) => `${f.summary} Reducing input token volume to near the workflow baseline would lower cost proportionally.`,
3198
- suggestedChangeFn: (f) => ({
3199
- strategy: "context-reduction",
3200
- averageInputTokens: f.details.averageInputTokens
3201
- })
3202
- },
3203
- "candidate-downgrade": {
3204
- actionType: "model-switch",
3205
- titleFn: (f) => `Evaluate cheaper model for ${extractWorkflow(f)}`,
3206
- descriptionFn: (f) => `${f.summary} This is an A/B test candidate \u2014 try a cheaper model on this workflow and compare quality.`,
3207
- suggestedChangeFn: () => ({
3208
- strategy: "model-downgrade",
3209
- candidates: ["claude-3-haiku", "gpt-4o-mini"]
3210
- })
3211
- },
3212
- "idle-spend": {
3213
- actionType: "other",
3214
- titleFn: (f) => `Review cadence for ${extractWorkflow(f)}`,
3215
- descriptionFn: (f) => `${f.summary} Consider reducing polling frequency or switching to an event-driven approach.`,
3216
- suggestedChangeFn: () => ({
3217
- strategy: "cadence-review"
3218
- })
3219
- },
3220
- "cache-carryover": {
3221
- actionType: "prompt-trim",
3222
- titleFn: () => "Summarize and reset long Cursor chats",
3223
- descriptionFn: (f) => `${f.summary} Create a compact recall summary, start a fresh chat, and carry forward only the facts the model actually needs.`,
3224
- suggestedChangeFn: (f) => ({
3225
- strategy: "conversation-reset",
3226
- cacheReadShare: f.details.cacheReadShare,
3227
- totalCacheReadTokens: f.details.totalCacheReadTokens
3228
- })
3229
- },
3230
- "max-mode-concentration": {
3231
- actionType: "model-switch",
3232
- titleFn: () => "Reserve max mode for the hardest Cursor turns",
3233
- descriptionFn: (f) => `${f.summary} Try a two-pass workflow: standard mode first, then escalate only the prompts that truly need max mode.`,
3234
- suggestedChangeFn: (f) => ({
3235
- strategy: "tiered-routing",
3236
- maxModeSpendShare: f.details.maxModeSpendShare,
3237
- maxModeCallCount: f.details.maxModeCallCount
3238
- })
3239
- }
3240
- };
3241
- function extractWorkflow(finding) {
3242
- const details = finding.details;
3243
- return details.workflow || finding.scopeId || "this workflow";
3244
- }
3245
- function buildSingleRecommendation(finding) {
3246
- const template = templatesByKind[finding.kind];
3247
- if (template) {
3248
- return {
3249
- id: sha1(`rec:${finding.id}:${template.actionType}`),
3250
- findingId: finding.id,
3251
- kind: finding.kind,
3252
- title: template.titleFn(finding),
3253
- description: template.descriptionFn(finding),
3254
- estimatedSavingsUsd: finding.costImpactUsd,
3255
- confidence: finding.confidence,
3256
- actionType: template.actionType,
3257
- suggestedChange: template.suggestedChangeFn?.(finding)
3258
- };
3259
- }
3260
- return {
3261
- id: sha1(`rec:${finding.id}:other`),
3262
- findingId: finding.id,
3263
- kind: finding.kind,
3264
- title: `Review: ${finding.title}`,
3265
- description: finding.summary,
3266
- estimatedSavingsUsd: finding.costImpactUsd,
3267
- confidence: finding.confidence,
3268
- actionType: "other"
3269
- };
3270
- }
3271
- function buildRecommendations(summary) {
3272
- return summary.findings.map(buildSingleRecommendation);
3273
- }
3274
-
3275
3424
  // ../core/src/report/render.ts
3276
3425
  function formatUsd(value) {
3277
3426
  return new Intl.NumberFormat("en-US", {
@@ -3330,16 +3479,6 @@ function renderTaxonomyBlock(summary) {
3330
3479
  function topFinding(summary, classification) {
3331
3480
  return summary.findings.filter((finding) => finding.classification === classification).sort((left, right) => right.costImpactUsd - left.costImpactUsd)[0];
3332
3481
  }
3333
- function topSavingsTest(summary) {
3334
- return summary.findings.filter((finding) => finding.classification === "opportunity").sort((left, right) => {
3335
- const leftPriority = left.kind === "candidate-downgrade" ? 1 : 0;
3336
- const rightPriority = right.kind === "candidate-downgrade" ? 1 : 0;
3337
- if (leftPriority !== rightPriority) {
3338
- return rightPriority - leftPriority;
3339
- }
3340
- return right.costImpactUsd - left.costImpactUsd;
3341
- })[0] ?? null;
3342
- }
3343
3482
  function renderFindingList(findings, emptyLabel) {
3344
3483
  if (findings.length === 0) {
3345
3484
  return [`- ${emptyLabel}`];
@@ -3348,6 +3487,33 @@ function renderFindingList(findings, emptyLabel) {
3348
3487
  return `- ${finding.title}: ${formatUsd(finding.costImpactUsd)} (${finding.confidence})`;
3349
3488
  });
3350
3489
  }
3490
+ function renderActionQueueRow(label, recommendation) {
3491
+ if (!recommendation) {
3492
+ return [`${label}`, "- none"];
3493
+ }
3494
+ return [
3495
+ label,
3496
+ `- ${recommendation.title} [${recommendation.scopeLabel}]: ${formatUsd(recommendation.estimatedSavingsUsd)} (${recommendation.severity})`
3497
+ ];
3498
+ }
3499
+ function renderActionQueue(summary) {
3500
+ const fixNow = summary.recommendations.find(
3501
+ (recommendation) => recommendation.priorityBucket === "fix_now"
3502
+ );
3503
+ const testNext = summary.recommendations.find(
3504
+ (recommendation) => recommendation.priorityBucket === "test_next"
3505
+ );
3506
+ const watch = summary.recommendations.find(
3507
+ (recommendation) => recommendation.priorityBucket === "watch"
3508
+ );
3509
+ return [
3510
+ "## Action queue",
3511
+ ...renderActionQueueRow("Fix now", fixNow),
3512
+ ...renderActionQueueRow("Test next", testNext),
3513
+ ...renderActionQueueRow("Watch", watch),
3514
+ "How to validate: `xerg audit --compare --push`"
3515
+ ];
3516
+ }
3351
3517
  function describeSpendDelta(delta) {
3352
3518
  return `${delta.key} (${formatUsdDelta(delta.deltaSpendUsd)})`;
3353
3519
  }
@@ -3570,6 +3736,8 @@ function renderCursorTerminalSummary(summary) {
3570
3736
  "## Findings",
3571
3737
  ...renderFindingList(summary.findings, "none detected"),
3572
3738
  "",
3739
+ ...renderActionQueue(summary),
3740
+ "",
3573
3741
  ...renderCursorCompareBlock(summary),
3574
3742
  ...summary.comparison ? [""] : [],
3575
3743
  "## Notes",
@@ -3613,6 +3781,8 @@ function renderCursorMarkdownSummary(summary) {
3613
3781
  ...summary.findings.slice(0, 10).map((finding) => {
3614
3782
  return `- **${finding.title}** (${finding.classification}, ${finding.confidence}) \u2014 ${finding.summary} Estimated impact: ${formatUsd(finding.costImpactUsd)}.`;
3615
3783
  }),
3784
+ "",
3785
+ ...renderActionQueue(summary),
3616
3786
  ...summary.comparison ? ["", ...renderCursorCompareBlock(summary)] : [],
3617
3787
  "",
3618
3788
  "## Notes",
@@ -3627,7 +3797,6 @@ function renderTerminalSummary(summary) {
3627
3797
  const opportunityFindings = summary.findings.filter(
3628
3798
  (finding) => finding.classification === "opportunity"
3629
3799
  );
3630
- const topSavings = topSavingsTest(summary);
3631
3800
  const topWaste = topFinding(summary, "waste");
3632
3801
  return [
3633
3802
  "# Xerg audit",
@@ -3656,11 +3825,8 @@ function renderTerminalSummary(summary) {
3656
3825
  "## Opportunities",
3657
3826
  ...renderFindingList(opportunityFindings, "none detected"),
3658
3827
  "",
3659
- "## First savings test",
3660
- ...topSavings ? [
3661
- `- Start with ${topSavings.title}: ${formatUsd(topSavings.costImpactUsd)} of potential impact`,
3662
- `- Why this test first: ${topSavings.summary}`
3663
- ] : ["- No savings test surfaced yet"],
3828
+ ...renderActionQueue(summary),
3829
+ "",
3664
3830
  ...topWaste ? [`- Confirmed leak to close first: ${topWaste.title}`] : ["- Confirmed leak to close first: none"],
3665
3831
  ...summary.spendByWorkflow[0] ? [`- Workflow to inspect first: ${summary.spendByWorkflow[0].key}`] : ["- Workflow to inspect first: none"],
3666
3832
  "",
@@ -3697,7 +3863,9 @@ function renderMarkdownSummary(summary) {
3697
3863
  "## Findings",
3698
3864
  ...summary.findings.slice(0, 10).map((finding) => {
3699
3865
  return `- **${finding.title}** (${finding.classification}, ${finding.confidence}) \u2014 ${finding.summary} Estimated impact: ${formatUsd(finding.costImpactUsd)}.`;
3700
- })
3866
+ }),
3867
+ "",
3868
+ ...renderActionQueue(summary)
3701
3869
  ];
3702
3870
  if (summary.comparison) {
3703
3871
  const comparison = summary.comparison;
@@ -3714,7 +3882,7 @@ function renderMarkdownSummary(summary) {
3714
3882
  }
3715
3883
 
3716
3884
  // ../schemas/dist/index.js
3717
- var AUDIT_PUSH_PAYLOAD_VERSION = 1;
3885
+ var AUDIT_PUSH_PAYLOAD_VERSION = 2;
3718
3886
 
3719
3887
  // ../core/src/wire.ts
3720
3888
  function toWireFinding(finding) {
@@ -3766,6 +3934,7 @@ function toWirePayload(summary, meta) {
3766
3934
  spendByDay: summary.spendByDay,
3767
3935
  wasteByDay: summary.wasteByDay,
3768
3936
  findings: summary.findings.map(toWireFinding),
3937
+ recommendations: summary.recommendations,
3769
3938
  notes: summary.notes,
3770
3939
  comparison: summary.comparison ? toWireComparison(summary.comparison) : null
3771
3940
  },
@@ -3887,7 +4056,8 @@ function loadPushConfig() {
3887
4056
  if (envKey) {
3888
4057
  return {
3889
4058
  apiKey: envKey,
3890
- apiUrl: envUrl || DEFAULT_API_URL
4059
+ apiUrl: envUrl || DEFAULT_API_URL,
4060
+ source: "env"
3891
4061
  };
3892
4062
  }
3893
4063
  try {
@@ -3896,7 +4066,8 @@ function loadPushConfig() {
3896
4066
  if (parsed.apiKey) {
3897
4067
  return {
3898
4068
  apiKey: parsed.apiKey,
3899
- apiUrl: envUrl || parsed.apiUrl || DEFAULT_API_URL
4069
+ apiUrl: envUrl || parsed.apiUrl || DEFAULT_API_URL,
4070
+ source: "config"
3900
4071
  };
3901
4072
  }
3902
4073
  } catch {
@@ -3905,7 +4076,8 @@ function loadPushConfig() {
3905
4076
  if (storedToken) {
3906
4077
  return {
3907
4078
  apiKey: storedToken,
3908
- apiUrl: envUrl || DEFAULT_API_URL
4079
+ apiUrl: envUrl || DEFAULT_API_URL,
4080
+ source: "stored"
3909
4081
  };
3910
4082
  }
3911
4083
  throw new Error(
@@ -5131,11 +5303,10 @@ ${errorMessages}`);
5131
5303
  summaries.push({ name: source.name, source, summary });
5132
5304
  }
5133
5305
  if (options.json) {
5134
- const output = summaries.length === 1 ? { ...summaries[0].summary, recommendations: buildRecommendations(summaries[0].summary) } : {
5306
+ const output = summaries.length === 1 ? summaries[0].summary : {
5135
5307
  sources: summaries.map((s) => ({
5136
5308
  name: s.name,
5137
- ...s.summary,
5138
- recommendations: buildRecommendations(s.summary)
5309
+ ...s.summary
5139
5310
  }))
5140
5311
  };
5141
5312
  process.stdout.write(`${JSON.stringify(output, null, 2)}
@@ -5213,9 +5384,7 @@ function renderOutput(summary, options) {
5213
5384
  return;
5214
5385
  }
5215
5386
  if (options.json) {
5216
- const recommendations = buildRecommendations(summary);
5217
- const output = { ...summary, recommendations };
5218
- process.stdout.write(`${JSON.stringify(output, null, 2)}
5387
+ process.stdout.write(`${JSON.stringify(summary, null, 2)}
5219
5388
  `);
5220
5389
  return;
5221
5390
  }
@@ -5255,103 +5424,445 @@ function cleanupPullResult(pullResult, keepFiles) {
5255
5424
  }
5256
5425
  }
5257
5426
 
5258
- // src/commands/doctor.ts
5259
- async function runDoctorCommand(options) {
5260
- const logger = createCliLogger({ verbose: options.verbose });
5261
- validateRuntimeOption2(options.runtime);
5262
- validateCursorUsageCsvOptions2(options);
5263
- validateHermesLocalOnly2(options);
5264
- if (options.railway) {
5265
- logger.verbose("Inspecting Railway audit readiness.");
5266
- const railwayTarget = buildRailwayTarget2(options);
5267
- const source = buildRailwaySourceFromFlags({
5268
- railway: railwayTarget,
5269
- remoteLogFile: options.remoteLogFile,
5270
- remoteSessionsDir: options.remoteSessionsDir
5271
- });
5272
- const report2 = await runRailwayDoctor({ source, onProgress: logger.verbose });
5273
- process.stdout.write(`${renderRailwayDoctorReport(report2)}
5274
- `);
5275
- return;
5276
- }
5277
- if (options.remote) {
5278
- logger.verbose(`Inspecting SSH audit readiness for ${options.remote}.`);
5279
- const source = buildSourceFromFlags({
5280
- remote: options.remote,
5281
- remoteLogFile: options.remoteLogFile,
5282
- remoteSessionsDir: options.remoteSessionsDir
5283
- });
5284
- const report2 = await runRemoteDoctor({ source, onProgress: logger.verbose });
5285
- process.stdout.write(`${renderRemoteDoctorReport(report2)}
5286
- `);
5287
- return;
5288
- }
5289
- if (options.cursorUsageCsv) {
5290
- logger.verbose("Inspecting local Cursor usage CSV audit readiness.");
5291
- logger.verbose(`Using Cursor usage CSV: ${options.cursorUsageCsv}`);
5292
- const report2 = await doctorCursorUsageCsv({
5293
- cursorUsageCsv: options.cursorUsageCsv,
5294
- onProgress: logger.verbose
5295
- });
5296
- process.stdout.write(`${renderCursorDoctorReport(report2)}
5297
- `);
5427
+ // src/commands/login.ts
5428
+ import { styleText } from "util";
5429
+ var DEFAULT_AUTH_URL = "https://xerg.ai/dashboard/settings";
5430
+ var DEFAULT_API_URL2 = "https://api.xerg.ai";
5431
+ var POLL_INTERVAL_MS = 2e3;
5432
+ var POLL_TIMEOUT_MS = 3e5;
5433
+ async function runLoginCommand() {
5434
+ const existing = loadStoredCredentials();
5435
+ if (existing) {
5436
+ process.stderr.write(
5437
+ `Already logged in. Credentials stored at ${getCredentialsPath()}.
5438
+ Run ${colorBold(formatCommand("logout"))} first to re-authenticate.
5439
+ `
5440
+ );
5298
5441
  return;
5299
5442
  }
5300
- logger.verbose(
5301
- options.runtime ? `Inspecting local ${options.runtime === "hermes" ? "Hermes" : "OpenClaw"} audit readiness.` : "Inspecting local runtime audit readiness."
5443
+ const data = await performDeviceLogin();
5444
+ storeCredentials(data.token);
5445
+ const teamInfo = data.teamName ? ` (team: ${data.teamName})` : "";
5446
+ process.stderr.write(
5447
+ `
5448
+ ${colorSuccess("Authenticated successfully")}${teamInfo}.
5449
+ Credentials saved to ${getCredentialsPath()}.
5450
+ `
5302
5451
  );
5303
- if (options.logFile) {
5304
- logger.verbose(`Using explicit local log file: ${options.logFile}`);
5305
- }
5306
- if (options.sessionsDir) {
5307
- logger.verbose(`Using explicit local sessions directory: ${options.sessionsDir}`);
5308
- }
5309
- const report = await doctorAgentRuntime({
5310
- runtime: options.runtime ?? "auto",
5311
- logFile: options.logFile,
5312
- sessionsDir: options.sessionsDir,
5313
- onProgress: logger.verbose
5314
- });
5315
- process.stdout.write(`${renderDoctorReport(report, { commandPrefix: options.commandPrefix })}
5316
- `);
5317
5452
  }
5318
- function validateRuntimeOption2(runtime) {
5319
- if (!runtime) {
5320
- return;
5321
- }
5322
- if (runtime !== "openclaw" && runtime !== "hermes") {
5453
+ async function performDeviceLogin() {
5454
+ const apiUrl = process.env.XERG_API_URL || DEFAULT_API_URL2;
5455
+ const deviceCodeUrl = `${apiUrl}/v1/auth/device-code`;
5456
+ let deviceResponse;
5457
+ try {
5458
+ const res = await fetch(deviceCodeUrl, { method: "POST" });
5459
+ if (!res.ok) {
5460
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
5461
+ }
5462
+ deviceResponse = await res.json();
5463
+ } catch (err) {
5464
+ const msg = err instanceof Error ? err.message : "Unknown error";
5323
5465
  throw new Error(
5324
- `Unsupported runtime "${runtime}". Use --runtime openclaw or --runtime hermes.`
5466
+ `Could not start device auth flow (${msg}).
5467
+
5468
+ Alternative: create an API key at ${DEFAULT_AUTH_URL}
5469
+ and set XERG_API_KEY in your environment.`
5325
5470
  );
5326
5471
  }
5327
- }
5328
- function validateCursorUsageCsvOptions2(options) {
5329
- if (!options.cursorUsageCsv) {
5330
- return;
5331
- }
5332
- const conflicts = [
5333
- options.runtime ? "--runtime" : null,
5334
- options.logFile ? "--log-file" : null,
5335
- options.sessionsDir ? "--sessions-dir" : null,
5336
- options.remote ? "--remote" : null,
5337
- options.remoteLogFile ? "--remote-log-file" : null,
5338
- options.remoteSessionsDir ? "--remote-sessions-dir" : null,
5339
- options.railway ? "--railway" : null,
5340
- options.railwayProject ? "--project" : null,
5341
- options.railwayEnvironment ? "--environment" : null,
5342
- options.railwayService ? "--service" : null
5343
- ].filter((flag) => flag !== null);
5344
- if (conflicts.length > 0) {
5345
- throw new Error(`The --cursor-usage-csv flag cannot be combined with ${conflicts.join(", ")}.`);
5346
- }
5347
- }
5348
- function validateHermesLocalOnly2(options) {
5349
- if (options.runtime !== "hermes") {
5350
- return;
5472
+ const verifyUrl = deviceResponse.verificationUrl || DEFAULT_AUTH_URL;
5473
+ const pollInterval = (deviceResponse.interval || 2) * 1e3;
5474
+ process.stderr.write(
5475
+ `
5476
+ Open this URL in your browser to authenticate:
5477
+
5478
+ ${colorBold(verifyUrl)}
5479
+
5480
+ `
5481
+ );
5482
+ if (deviceResponse.userCode) {
5483
+ process.stderr.write(`Your code: ${colorBold(deviceResponse.userCode)}
5484
+
5485
+ `);
5351
5486
  }
5352
- const conflicts = [
5353
- options.remote ? "--remote" : null,
5354
- options.remoteLogFile ? "--remote-log-file" : null,
5487
+ process.stderr.write("Waiting for authentication...\n");
5488
+ await openBrowser(verifyUrl);
5489
+ const tokenUrl = `${apiUrl}/v1/auth/device-token`;
5490
+ const startTime = Date.now();
5491
+ while (Date.now() - startTime < POLL_TIMEOUT_MS) {
5492
+ await sleep(Math.max(pollInterval, POLL_INTERVAL_MS));
5493
+ try {
5494
+ const res = await fetch(tokenUrl, {
5495
+ method: "POST",
5496
+ headers: { "Content-Type": "application/json" },
5497
+ body: JSON.stringify({ deviceCode: deviceResponse.deviceCode })
5498
+ });
5499
+ if (res.status === 200) {
5500
+ return await res.json();
5501
+ }
5502
+ if (res.status === 428) {
5503
+ continue;
5504
+ }
5505
+ if (res.status === 410) {
5506
+ throw new Error(`Device code expired. Please run \`${formatCommand("login")}\` again.`);
5507
+ }
5508
+ const body = await res.json().catch(() => ({}));
5509
+ throw new Error(body.error || `Unexpected response: HTTP ${res.status}`);
5510
+ } catch (err) {
5511
+ if (err instanceof Error && (err.message.includes("expired") || err.message.includes("Unexpected"))) {
5512
+ throw err;
5513
+ }
5514
+ }
5515
+ }
5516
+ throw new Error(`Authentication timed out. Please run \`${formatCommand("login")}\` again.`);
5517
+ }
5518
+ async function openBrowser(url) {
5519
+ const { exec } = await import("child_process");
5520
+ const { platform: platform2 } = await import("os");
5521
+ const commands = {
5522
+ darwin: "open",
5523
+ win32: "start",
5524
+ linux: "xdg-open"
5525
+ };
5526
+ const cmd = commands[platform2()];
5527
+ if (!cmd) return;
5528
+ return new Promise((resolve4) => {
5529
+ exec(`${cmd} ${JSON.stringify(url)}`, () => resolve4());
5530
+ });
5531
+ }
5532
+ function sleep(ms) {
5533
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
5534
+ }
5535
+ function colorBold(text) {
5536
+ return process.stderr.isTTY ? styleText("bold", text) : text;
5537
+ }
5538
+ function colorSuccess(text) {
5539
+ return process.stderr.isTTY ? styleText("green", text) : text;
5540
+ }
5541
+
5542
+ // src/cloud.ts
5543
+ function loadPushConfigOrNull() {
5544
+ try {
5545
+ return loadPushConfig();
5546
+ } catch {
5547
+ return null;
5548
+ }
5549
+ }
5550
+ async function authenticateAndLoadPushConfig() {
5551
+ const data = await performDeviceLogin();
5552
+ storeCredentials(data.token);
5553
+ const teamInfo = data.teamName ? ` (team: ${data.teamName})` : "";
5554
+ process.stderr.write(
5555
+ `
5556
+ Authenticated successfully${teamInfo}.
5557
+ Credentials saved to ${getCredentialsPath()}.
5558
+ `
5559
+ );
5560
+ return loadPushConfig();
5561
+ }
5562
+ function renderCloudDisclaimer() {
5563
+ return [
5564
+ "Xerg Cloud sync and hosted MCP are optional paid workspace features.",
5565
+ "Local audits and compare stay free, and you can keep using Xerg locally if you skip this step."
5566
+ ].join("\n");
5567
+ }
5568
+ function renderMcpCredentialSourceMessage(config) {
5569
+ if (config.source === "stored") {
5570
+ 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.";
5571
+ }
5572
+ return "Using your workspace API key.";
5573
+ }
5574
+
5575
+ // src/prompts.ts
5576
+ import { confirm, select } from "@inquirer/prompts";
5577
+ function hasPromptTty() {
5578
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
5579
+ }
5580
+ async function promptConfirm(message, defaultValue = true) {
5581
+ return confirm({
5582
+ message,
5583
+ default: defaultValue
5584
+ });
5585
+ }
5586
+ async function promptSelect(message, choices) {
5587
+ return select({
5588
+ message,
5589
+ choices
5590
+ });
5591
+ }
5592
+
5593
+ // src/commands/push.ts
5594
+ import { readFileSync as readFileSync8 } from "fs";
5595
+ async function runPushCommand(options) {
5596
+ const payload = options.file ? loadPayloadFromFile(options.file) : loadLatestCachedAuditPayload();
5597
+ if (options.dryRun) {
5598
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
5599
+ `);
5600
+ return;
5601
+ }
5602
+ const config = loadPushConfig();
5603
+ const auditId = payload.summary.auditId;
5604
+ process.stderr.write(`Pushing audit ${auditId} to ${config.apiUrl}...
5605
+ `);
5606
+ const result = await pushAudit(payload, config);
5607
+ if (result.ok) {
5608
+ process.stderr.write(`Pushed successfully (audit: ${result.auditId}).
5609
+ `);
5610
+ } else {
5611
+ const statusInfo = result.status > 0 ? ` (HTTP ${result.status})` : "";
5612
+ throw new Error(`Push failed${statusInfo}: ${result.message}`);
5613
+ }
5614
+ }
5615
+ function loadPayloadFromFile(filePath) {
5616
+ let raw;
5617
+ try {
5618
+ raw = readFileSync8(filePath, "utf8");
5619
+ } catch {
5620
+ throw new Error(`Cannot read file: ${filePath}`);
5621
+ }
5622
+ let parsed;
5623
+ try {
5624
+ parsed = JSON.parse(raw);
5625
+ } catch {
5626
+ throw new Error(`File is not valid JSON: ${filePath}`);
5627
+ }
5628
+ const payload = parsed;
5629
+ if (!payload.version || !payload.summary || !payload.meta) {
5630
+ throw new Error(
5631
+ `File does not look like an AuditPushPayload (missing version, summary, or meta): ${filePath}`
5632
+ );
5633
+ }
5634
+ return payload;
5635
+ }
5636
+ function loadLatestCachedAuditPayload() {
5637
+ const dbPath = getDefaultDbPath();
5638
+ let summaries;
5639
+ try {
5640
+ summaries = listStoredAuditSummaries(dbPath);
5641
+ } catch {
5642
+ throw new NoDataError(
5643
+ `No local audit database found. Run \`${formatCommand("audit")}\` first, or use \`${formatCommand("push --file <path>")}\`.`
5644
+ );
5645
+ }
5646
+ if (summaries.length === 0) {
5647
+ throw new NoDataError(
5648
+ `No cached audit snapshots found. Run \`${formatCommand("audit")}\` first, or use \`${formatCommand("push --file <path>")}\`.`
5649
+ );
5650
+ }
5651
+ const latest = summaries[0];
5652
+ const meta = buildMeta2(latest);
5653
+ process.stderr.write(
5654
+ `Using most recent cached audit: ${latest.auditId} (${latest.generatedAt})
5655
+ `
5656
+ );
5657
+ return toWirePayload(latest, meta);
5658
+ }
5659
+ function buildMeta2(summary) {
5660
+ const sourceMeta = buildCachedPushSourceMeta(summary);
5661
+ return {
5662
+ cliVersion: getCliVersion(),
5663
+ sourceId: sourceMeta.sourceId,
5664
+ sourceHost: sourceMeta.sourceHost,
5665
+ environment: sourceMeta.environment
5666
+ };
5667
+ }
5668
+
5669
+ // src/commands/connect.ts
5670
+ async function runConnectCommand() {
5671
+ await runConnectFlow();
5672
+ }
5673
+ async function runConnectFlow(options) {
5674
+ if (!options?.skipDisclaimer) {
5675
+ process.stderr.write(`${renderCloudDisclaimer()}
5676
+ `);
5677
+ }
5678
+ let config = loadPushConfigOrNull();
5679
+ if (config) {
5680
+ process.stderr.write("Xerg authentication detected.\n");
5681
+ } else {
5682
+ if (!hasPromptTty()) {
5683
+ process.stderr.write(
5684
+ `No Xerg authentication is configured, and ${formatCommand("connect")} needs an interactive terminal before it can start browser login.
5685
+ Run ${formatCommand("login")} from a TTY, or keep using local audits for free.
5686
+ `
5687
+ );
5688
+ process.exitCode = 1;
5689
+ return false;
5690
+ }
5691
+ const shouldLogin = await promptConfirm("Sign in to Xerg Cloud now?", true);
5692
+ if (!shouldLogin) {
5693
+ process.stderr.write(
5694
+ "Skipped Xerg Cloud setup. You can keep using local audits and compare without connecting.\n"
5695
+ );
5696
+ return false;
5697
+ }
5698
+ config = await authenticateAndLoadPushConfig();
5699
+ }
5700
+ if (!hasPromptTty()) {
5701
+ if (!options?.auditSummary) {
5702
+ process.stderr.write(
5703
+ `Non-interactive mode skips the push prompt. Run ${formatCommand("push")} when you want to sync a cached audit.
5704
+ `
5705
+ );
5706
+ } else {
5707
+ process.stderr.write(
5708
+ `Authentication is ready. Run ${formatCommand("push")} later if you want to sync this audit.
5709
+ `
5710
+ );
5711
+ }
5712
+ return true;
5713
+ }
5714
+ const shouldPush = await promptConfirm(
5715
+ options?.auditSummary ? "Push this audit to Xerg Cloud?" : "Push your latest cached audit to Xerg Cloud?",
5716
+ true
5717
+ );
5718
+ if (!shouldPush) {
5719
+ process.stderr.write(
5720
+ options?.auditSummary ? `Skipped push. Run ${formatCommand("push")} later if you want to sync a cached audit.
5721
+ ` : `Skipped push. Run ${formatCommand("push")} when you want to sync a cached audit.
5722
+ `
5723
+ );
5724
+ return true;
5725
+ }
5726
+ const payload = options?.auditSummary ? toWirePayload(options.auditSummary, buildLocalMeta(options.auditSummary)) : loadStandalonePayload();
5727
+ if (!payload) {
5728
+ return true;
5729
+ }
5730
+ await pushResolvedPayload(payload, config ?? loadPushConfig());
5731
+ return true;
5732
+ }
5733
+ function buildLocalMeta(summary) {
5734
+ const sourceMeta = buildLocalPushSourceMeta(summary.runtime);
5735
+ return {
5736
+ cliVersion: getCliVersion(),
5737
+ sourceId: sourceMeta.sourceId,
5738
+ sourceHost: sourceMeta.sourceHost,
5739
+ environment: sourceMeta.environment
5740
+ };
5741
+ }
5742
+ function loadStandalonePayload() {
5743
+ try {
5744
+ return loadLatestCachedAuditPayload();
5745
+ } catch (error) {
5746
+ if (error instanceof NoDataError || error instanceof Error && error.name === "NoDataError") {
5747
+ process.stderr.write(
5748
+ `${error instanceof Error ? error.message : "No cached audit snapshots found."}
5749
+ `
5750
+ );
5751
+ return null;
5752
+ }
5753
+ throw error;
5754
+ }
5755
+ }
5756
+ async function pushResolvedPayload(payload, config) {
5757
+ process.stderr.write(`Pushing audit ${payload.summary.auditId} to ${config.apiUrl}...
5758
+ `);
5759
+ const result = await pushAudit(payload, config);
5760
+ if (result.ok) {
5761
+ process.stderr.write(`Pushed successfully (audit: ${result.auditId}).
5762
+ `);
5763
+ return;
5764
+ }
5765
+ const statusInfo = result.status > 0 ? ` (HTTP ${result.status})` : "";
5766
+ throw new Error(`Push failed${statusInfo}: ${result.message}`);
5767
+ }
5768
+
5769
+ // src/commands/doctor.ts
5770
+ async function runDoctorCommand(options) {
5771
+ const logger = createCliLogger({ verbose: options.verbose });
5772
+ validateRuntimeOption2(options.runtime);
5773
+ validateCursorUsageCsvOptions2(options);
5774
+ validateHermesLocalOnly2(options);
5775
+ if (options.railway) {
5776
+ logger.verbose("Inspecting Railway audit readiness.");
5777
+ const railwayTarget = buildRailwayTarget2(options);
5778
+ const source = buildRailwaySourceFromFlags({
5779
+ railway: railwayTarget,
5780
+ remoteLogFile: options.remoteLogFile,
5781
+ remoteSessionsDir: options.remoteSessionsDir
5782
+ });
5783
+ const report2 = await runRailwayDoctor({ source, onProgress: logger.verbose });
5784
+ process.stdout.write(`${renderRailwayDoctorReport(report2)}
5785
+ `);
5786
+ return;
5787
+ }
5788
+ if (options.remote) {
5789
+ logger.verbose(`Inspecting SSH audit readiness for ${options.remote}.`);
5790
+ const source = buildSourceFromFlags({
5791
+ remote: options.remote,
5792
+ remoteLogFile: options.remoteLogFile,
5793
+ remoteSessionsDir: options.remoteSessionsDir
5794
+ });
5795
+ const report2 = await runRemoteDoctor({ source, onProgress: logger.verbose });
5796
+ process.stdout.write(`${renderRemoteDoctorReport(report2)}
5797
+ `);
5798
+ return;
5799
+ }
5800
+ if (options.cursorUsageCsv) {
5801
+ logger.verbose("Inspecting local Cursor usage CSV audit readiness.");
5802
+ logger.verbose(`Using Cursor usage CSV: ${options.cursorUsageCsv}`);
5803
+ const report2 = await doctorCursorUsageCsv({
5804
+ cursorUsageCsv: options.cursorUsageCsv,
5805
+ onProgress: logger.verbose
5806
+ });
5807
+ process.stdout.write(`${renderCursorDoctorReport(report2)}
5808
+ `);
5809
+ return;
5810
+ }
5811
+ logger.verbose(
5812
+ options.runtime ? `Inspecting local ${options.runtime === "hermes" ? "Hermes" : "OpenClaw"} audit readiness.` : "Inspecting local runtime audit readiness."
5813
+ );
5814
+ if (options.logFile) {
5815
+ logger.verbose(`Using explicit local log file: ${options.logFile}`);
5816
+ }
5817
+ if (options.sessionsDir) {
5818
+ logger.verbose(`Using explicit local sessions directory: ${options.sessionsDir}`);
5819
+ }
5820
+ const report = await doctorAgentRuntime({
5821
+ runtime: options.runtime ?? "auto",
5822
+ logFile: options.logFile,
5823
+ sessionsDir: options.sessionsDir,
5824
+ onProgress: logger.verbose
5825
+ });
5826
+ process.stdout.write(`${renderDoctorReport(report, { commandPrefix: options.commandPrefix })}
5827
+ `);
5828
+ }
5829
+ function validateRuntimeOption2(runtime) {
5830
+ if (!runtime) {
5831
+ return;
5832
+ }
5833
+ if (runtime !== "openclaw" && runtime !== "hermes") {
5834
+ throw new Error(
5835
+ `Unsupported runtime "${runtime}". Use --runtime openclaw or --runtime hermes.`
5836
+ );
5837
+ }
5838
+ }
5839
+ function validateCursorUsageCsvOptions2(options) {
5840
+ if (!options.cursorUsageCsv) {
5841
+ return;
5842
+ }
5843
+ const conflicts = [
5844
+ options.runtime ? "--runtime" : null,
5845
+ options.logFile ? "--log-file" : null,
5846
+ options.sessionsDir ? "--sessions-dir" : null,
5847
+ options.remote ? "--remote" : null,
5848
+ options.remoteLogFile ? "--remote-log-file" : null,
5849
+ options.remoteSessionsDir ? "--remote-sessions-dir" : null,
5850
+ options.railway ? "--railway" : null,
5851
+ options.railwayProject ? "--project" : null,
5852
+ options.railwayEnvironment ? "--environment" : null,
5853
+ options.railwayService ? "--service" : null
5854
+ ].filter((flag) => flag !== null);
5855
+ if (conflicts.length > 0) {
5856
+ throw new Error(`The --cursor-usage-csv flag cannot be combined with ${conflicts.join(", ")}.`);
5857
+ }
5858
+ }
5859
+ function validateHermesLocalOnly2(options) {
5860
+ if (options.runtime !== "hermes") {
5861
+ return;
5862
+ }
5863
+ const conflicts = [
5864
+ options.remote ? "--remote" : null,
5865
+ options.remoteLogFile ? "--remote-log-file" : null,
5355
5866
  options.remoteSessionsDir ? "--remote-sessions-dir" : null,
5356
5867
  options.railway ? "--railway" : null,
5357
5868
  options.railwayProject ? "--project" : null,
@@ -5470,121 +5981,269 @@ function renderRailwayDoctorReport(report) {
5470
5981
  );
5471
5982
  }
5472
5983
  }
5473
- sections.push("", "## Notes", ...report.notes.map((n) => `[railway] ${n}`));
5474
- return sections.join("\n");
5984
+ sections.push("", "## Notes", ...report.notes.map((n) => `[railway] ${n}`));
5985
+ return sections.join("\n");
5986
+ }
5987
+
5988
+ // src/commands/mcp-setup.ts
5989
+ import { existsSync as existsSync2, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "fs";
5990
+ import { dirname as dirname3, join as join8 } from "path";
5991
+ var HOSTED_MCP_URL = "https://mcp.xerg.ai/mcp";
5992
+ async function runMcpSetupCommand() {
5993
+ await runMcpSetupFlow();
5994
+ }
5995
+ async function runMcpSetupFlow() {
5996
+ let config = loadPushConfigOrNull();
5997
+ if (!config) {
5998
+ process.stderr.write(`${renderCloudDisclaimer()}
5999
+ `);
6000
+ process.stderr.write("Hosted MCP requires Xerg Cloud authentication before client setup.\n");
6001
+ }
6002
+ if (!hasPromptTty()) {
6003
+ process.stderr.write(
6004
+ `${formatCommand("mcp-setup")} needs an interactive terminal so it can ask which MCP client you want to configure.
6005
+ `
6006
+ );
6007
+ process.exitCode = 1;
6008
+ return;
6009
+ }
6010
+ if (!config) {
6011
+ const shouldLogin = await promptConfirm("Authenticate with Xerg Cloud now?", true);
6012
+ if (!shouldLogin) {
6013
+ process.stderr.write(
6014
+ `Skipped hosted MCP setup. Run ${formatCommand("mcp-setup")} when you're ready.
6015
+ `
6016
+ );
6017
+ return;
6018
+ }
6019
+ config = await authenticateAndLoadPushConfig();
6020
+ }
6021
+ process.stderr.write(`${renderMcpCredentialSourceMessage(config)}
6022
+ `);
6023
+ const client = await promptSelect("Which MCP client do you want to configure?", [
6024
+ {
6025
+ name: "Cursor",
6026
+ value: "cursor",
6027
+ description: "Project-scoped or global Cursor MCP config"
6028
+ },
6029
+ {
6030
+ name: "Claude Code",
6031
+ value: "claude-code",
6032
+ description: "Project-scoped Claude Code MCP config"
6033
+ },
6034
+ {
6035
+ name: "Other",
6036
+ value: "other",
6037
+ description: "Print the hosted HTTP MCP snippet for another client"
6038
+ }
6039
+ ]);
6040
+ const snippet = JSON.stringify(buildHostedMcpConfig(config), null, 2);
6041
+ if (client === "cursor") {
6042
+ await handleCursorSetup(snippet, config);
6043
+ return;
6044
+ }
6045
+ process.stdout.write(`${snippet}
6046
+ `);
6047
+ if (client === "claude-code") {
6048
+ process.stderr.write(
6049
+ "Add this to `.mcp.json` in your project root, or import the same `mcpServers.xerg` config through Claude Code MCP settings.\n"
6050
+ );
6051
+ return;
6052
+ }
6053
+ process.stderr.write(
6054
+ `Add this as a remote HTTP MCP server in your client. Endpoint: ${HOSTED_MCP_URL}
6055
+ `
6056
+ );
6057
+ }
6058
+ async function handleCursorSetup(snippet, config) {
6059
+ const cursorDir = join8(process.cwd(), ".cursor");
6060
+ const cursorConfigPath = join8(cursorDir, "mcp.json");
6061
+ if (existsSync2(cursorDir)) {
6062
+ const shouldWrite = await promptConfirm(
6063
+ "Write a project-scoped Cursor MCP config to .cursor/mcp.json?",
6064
+ true
6065
+ );
6066
+ if (shouldWrite) {
6067
+ writeCursorConfig(cursorConfigPath, config);
6068
+ process.stderr.write(`Wrote hosted MCP config to ${cursorConfigPath}.
6069
+ `);
6070
+ return;
6071
+ }
6072
+ }
6073
+ process.stdout.write(`${snippet}
6074
+ `);
6075
+ process.stderr.write(
6076
+ "Add this to `.cursor/mcp.json` for a project-scoped Cursor config, or `~/.cursor/mcp.json` for a global Cursor config.\n"
6077
+ );
6078
+ }
6079
+ function buildHostedMcpConfig(config) {
6080
+ return {
6081
+ mcpServers: {
6082
+ xerg: {
6083
+ type: "http",
6084
+ url: HOSTED_MCP_URL,
6085
+ headers: {
6086
+ Authorization: `Bearer ${config.apiKey}`
6087
+ }
6088
+ }
6089
+ }
6090
+ };
6091
+ }
6092
+ function writeCursorConfig(filePath, config) {
6093
+ mkdirSync6(dirname3(filePath), { recursive: true });
6094
+ let parsed = {};
6095
+ if (existsSync2(filePath)) {
6096
+ try {
6097
+ parsed = JSON.parse(readFileSync9(filePath, "utf8"));
6098
+ } catch {
6099
+ throw new Error(`Cursor config is not valid JSON: ${filePath}`);
6100
+ }
6101
+ }
6102
+ const existingServers = parsed.mcpServers;
6103
+ if (existingServers && typeof existingServers !== "object") {
6104
+ throw new Error(`Cursor config has an invalid "mcpServers" value: ${filePath}`);
6105
+ }
6106
+ parsed.mcpServers = {
6107
+ ...existingServers ?? {},
6108
+ xerg: buildHostedMcpConfig(config).mcpServers.xerg
6109
+ };
6110
+ writeFileSync2(filePath, `${JSON.stringify(parsed, null, 2)}
6111
+ `);
5475
6112
  }
5476
6113
 
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) {
6114
+ // src/commands/init.ts
6115
+ async function runInitCommand() {
6116
+ if (!hasPromptTty()) {
5486
6117
  process.stderr.write(
5487
- `Already logged in. Credentials stored at ${getCredentialsPath()}.
5488
- Run ${colorBold(formatCommand("logout"))} first to re-authenticate.
6118
+ `${formatCommand("init")} is interactive in this release. Run ${formatCommand("audit")} directly when you need a non-interactive audit.
5489
6119
  `
5490
6120
  );
6121
+ process.exitCode = 1;
6122
+ return;
6123
+ }
6124
+ const candidates = await resolveRuntimeCandidates({ runtime: "auto" });
6125
+ const usable = candidates.filter((candidate) => candidate.usable);
6126
+ if (usable.length === 0) {
6127
+ renderNoDataGuidance();
6128
+ return;
6129
+ }
6130
+ const runtime = await chooseRuntime(usable);
6131
+ if (!runtime) {
5491
6132
  return;
5492
6133
  }
5493
- const apiUrl = process.env.XERG_API_URL || DEFAULT_API_URL2;
5494
- const deviceCodeUrl = `${apiUrl}/v1/auth/device-code`;
5495
- let deviceResponse;
5496
6134
  try {
5497
- const res = await fetch(deviceCodeUrl, { method: "POST" });
5498
- if (!res.ok) {
5499
- throw new Error(`HTTP ${res.status}: ${res.statusText}`);
6135
+ const summary = await auditAgentRuntime({
6136
+ runtime,
6137
+ commandPrefix: formatCommand("")
6138
+ });
6139
+ process.stdout.write(`${renderTerminalSummary(summary)}
6140
+ `);
6141
+ process.stderr.write(
6142
+ `
6143
+ Next: after you make a fix, run ${formatCommand("audit --compare")} to measure the delta.
6144
+ `
6145
+ );
6146
+ const existingAuth = loadPushConfigOrNull();
6147
+ process.stderr.write(
6148
+ `${existingAuth ? "Xerg Cloud authentication is already configured. You can optionally push this audit and set up hosted MCP next." : renderCloudDisclaimer()}
6149
+ `
6150
+ );
6151
+ const shouldConnect = await promptConfirm("Continue with optional Xerg Cloud setup?", true);
6152
+ if (!shouldConnect) {
6153
+ process.stderr.write(
6154
+ `Skipped Xerg Cloud setup. Run ${formatCommand("connect")} or ${formatCommand("mcp-setup")} whenever you want the hosted follow-up.
6155
+ `
6156
+ );
6157
+ return;
5500
6158
  }
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.`
6159
+ const connected = await runConnectFlow({
6160
+ skipDisclaimer: true,
6161
+ auditSummary: summary
6162
+ });
6163
+ if (!connected) {
6164
+ return;
6165
+ }
6166
+ const shouldSetupMcp = await promptConfirm("Set up hosted MCP now?", true);
6167
+ if (!shouldSetupMcp) {
6168
+ process.stderr.write(
6169
+ `Skipped hosted MCP setup. Run ${formatCommand("mcp-setup")} when you're ready.
6170
+ `
6171
+ );
6172
+ return;
6173
+ }
6174
+ await runMcpSetupFlow();
6175
+ } catch (error) {
6176
+ const message = error instanceof Error ? error.message : "Unknown error";
6177
+ const productName = getRuntimeAdapter(runtime).productName;
6178
+ process.stderr.write(
6179
+ `${[
6180
+ `${productName} audit failed: ${message}`,
6181
+ `Try ${formatCommand(["doctor", "--runtime", runtime])} to inspect the detected paths first.`,
6182
+ `Re-run ${formatCommand("audit --verbose")} for more detail.`
6183
+ ].join("\n")}
6184
+ `
5509
6185
  );
6186
+ process.exitCode = 1;
5510
6187
  }
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
-
6188
+ }
6189
+ async function chooseRuntime(candidates) {
6190
+ if (candidates.length === 1) {
6191
+ const candidate = candidates[0];
6192
+ process.stderr.write(`${describeCandidate(candidate)}
5524
6193
  `);
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()}.
6194
+ const shouldAudit = await promptConfirm(
6195
+ `Run your first ${candidate.adapter.productName} audit now?`,
6196
+ true
6197
+ );
6198
+ if (!shouldAudit) {
6199
+ process.stderr.write(
6200
+ `Skipped the first audit. Run ${formatCommand(["audit", "--runtime", candidate.adapter.runtime])} when you're ready.
5546
6201
  `
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
- }
6202
+ );
6203
+ return null;
5562
6204
  }
6205
+ return candidate.adapter.runtime;
5563
6206
  }
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
- });
6207
+ return promptSelect("Choose the local runtime to audit first.", [
6208
+ ...candidates.map((candidate) => ({
6209
+ name: candidate.adapter.productName,
6210
+ value: candidate.adapter.runtime,
6211
+ description: describeSources(candidate)
6212
+ }))
6213
+ ]);
5579
6214
  }
5580
- function sleep(ms) {
5581
- return new Promise((resolve4) => setTimeout(resolve4, ms));
6215
+ function describeCandidate(candidate) {
6216
+ return `Found local ${candidate.adapter.productName} data (${describeSources(candidate)}).`;
5582
6217
  }
5583
- function colorBold(text) {
5584
- return process.stderr.isTTY ? styleText("bold", text) : text;
6218
+ function describeSources(candidate) {
6219
+ const kinds = new Set(candidate.sources.map((source) => source.kind));
6220
+ const details = [
6221
+ kinds.has("gateway") ? "gateway logs" : null,
6222
+ kinds.has("sessions") ? "session transcripts" : null
6223
+ ].filter((detail) => detail !== null);
6224
+ return details.join(" and ");
5585
6225
  }
5586
- function colorSuccess(text) {
5587
- return process.stderr.isTTY ? styleText("green", text) : text;
6226
+ function renderNoDataGuidance() {
6227
+ const openclawDefaults = getRuntimeAdapter("openclaw").defaultPaths();
6228
+ const hermesDefaults = getRuntimeAdapter("hermes").defaultPaths();
6229
+ process.stderr.write(
6230
+ `${[
6231
+ "No local OpenClaw or Hermes data was detected in the default locations Xerg checked.",
6232
+ "",
6233
+ "Checked defaults:",
6234
+ `- OpenClaw gateway logs: ${openclawDefaults.gatewayPattern}`,
6235
+ `- OpenClaw session transcripts: ${openclawDefaults.sessionsPattern}`,
6236
+ `- Hermes gateway logs: ${hermesDefaults.gatewayPattern}`,
6237
+ `- Hermes session transcripts: ${hermesDefaults.sessionsPattern}`,
6238
+ "",
6239
+ "Next steps:",
6240
+ `- Local OpenClaw paths: ${formatCommand("audit --runtime openclaw --log-file /path/to/openclaw.log")} or ${formatCommand("audit --runtime openclaw --sessions-dir /path/to/sessions")}`,
6241
+ `- Local Hermes paths: ${formatCommand("audit --runtime hermes --log-file ~/.hermes/logs/agent.log")} or ${formatCommand("audit --runtime hermes --sessions-dir ~/.hermes/sessions")}`,
6242
+ `- Remote OpenClaw only: ${formatCommand("audit --remote user@host")}`,
6243
+ `- Railway OpenClaw only: ${formatCommand("audit --railway")}`
6244
+ ].join("\n")}
6245
+ `
6246
+ );
5588
6247
  }
5589
6248
 
5590
6249
  // src/commands/logout.ts
@@ -5598,103 +6257,51 @@ function runLogoutCommand() {
5598
6257
  }
5599
6258
  }
5600
6259
 
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
6260
  // src/help.ts
5678
6261
  function renderRootHelp(version, display) {
5679
6262
  return `${display.name} ${version}
5680
6263
 
5681
- Waste intelligence for OpenClaw and Hermes workflows plus local Cursor usage CSVs.
6264
+ Waste intelligence for OpenClaw and Hermes workflows.
5682
6265
 
5683
6266
  Usage:
5684
6267
  ${formatCommand("<command> [options]", display.prefix)}
5685
6268
 
5686
- Commands:
6269
+ Getting started:
6270
+ init Detect local runtimes, run a first audit, and offer optional cloud follow-up.
6271
+
6272
+ Audit and inspect:
5687
6273
  audit Analyze OpenClaw or Hermes logs, or a local Cursor usage CSV.
5688
6274
  doctor Inspect OpenClaw or Hermes sources, or a local Cursor usage CSV.
6275
+
6276
+ Cloud:
6277
+ connect Authenticate and optionally push your latest audit to Xerg Cloud.
5689
6278
  push Push a cached audit snapshot to the Xerg API.
5690
6279
  login Authenticate with the Xerg API via browser.
5691
6280
  logout Remove stored Xerg API credentials.
6281
+ mcp-setup Generate hosted MCP client configuration.
5692
6282
 
5693
6283
  Global options:
5694
6284
  -h, --help Show help
5695
6285
  -v, --version Show version
5696
6286
  `;
5697
6287
  }
6288
+ function renderInitHelp(commandPrefix) {
6289
+ return `${formatCommand("init", commandPrefix)}
6290
+
6291
+ Detect local OpenClaw or Hermes runtimes, run a first audit, and offer optional cloud follow-up.
6292
+
6293
+ Usage:
6294
+ ${formatCommand("init", commandPrefix)}
6295
+
6296
+ Notes:
6297
+ - Interactive only in v1
6298
+ - Uses local runtime auto-detection
6299
+ - Runs a first local audit with snapshot persistence enabled
6300
+ - Offers optional Xerg Cloud connect and hosted MCP setup after a successful audit
6301
+
6302
+ -h, --help Show help
6303
+ `;
6304
+ }
5698
6305
  function renderAuditHelp(commandPrefix) {
5699
6306
  return `${formatCommand("audit", commandPrefix)}
5700
6307
 
@@ -5761,7 +6368,7 @@ Options:
5761
6368
 
5762
6369
  Authentication:
5763
6370
  Set XERG_API_KEY in your environment, add "apiKey" to ~/.xerg/config.json,
5764
- or run \`${formatCommand("login", commandPrefix)}\` to authenticate via browser.
6371
+ or run \`${formatCommand("connect", commandPrefix)}\` / \`${formatCommand("login", commandPrefix)}\` to authenticate via browser.
5765
6372
  Browser login stores a token at ~/.config/xerg/credentials.json by default.
5766
6373
  `;
5767
6374
  }
@@ -5798,6 +6405,40 @@ Railway options (OpenClaw only):
5798
6405
  -h, --help Show help
5799
6406
  `;
5800
6407
  }
6408
+ function renderConnectHelp(commandPrefix) {
6409
+ return `${formatCommand("connect", commandPrefix)}
6410
+
6411
+ Authenticate with Xerg Cloud and optionally push the latest audit.
6412
+
6413
+ Usage:
6414
+ ${formatCommand("connect", commandPrefix)}
6415
+
6416
+ Notes:
6417
+ - Shows paid-workspace disclosure before hosted setup
6418
+ - Reuses existing auth from XERG_API_KEY, ~/.xerg/config.json, or stored browser login
6419
+ - Standalone non-interactive mode reports auth status and skips the push prompt
6420
+ - When called after ${formatCommand("init", commandPrefix)}, it can push the in-memory audit directly
6421
+
6422
+ -h, --help Show help
6423
+ `;
6424
+ }
6425
+ function renderMcpSetupHelp(commandPrefix) {
6426
+ return `${formatCommand("mcp-setup", commandPrefix)}
6427
+
6428
+ Generate hosted MCP client configuration for Cursor, Claude Code, or another MCP client.
6429
+
6430
+ Usage:
6431
+ ${formatCommand("mcp-setup", commandPrefix)}
6432
+
6433
+ Notes:
6434
+ - Interactive in v1 because client selection is prompt-driven
6435
+ - Uses the hosted MCP endpoint at https://mcp.xerg.ai/mcp
6436
+ - Can write a project-scoped Cursor config when .cursor/ already exists
6437
+ - Local audits and compare stay available even if you skip hosted MCP setup
6438
+
6439
+ -h, --help Show help
6440
+ `;
6441
+ }
5801
6442
 
5802
6443
  // src/index.ts
5803
6444
  var VERSION = getCliVersion();
@@ -5831,6 +6472,11 @@ async function run() {
5831
6472
  });
5832
6473
  return;
5833
6474
  }
6475
+ if (command === "init") {
6476
+ parseBareCommandOptions(argv.slice(1), renderInitHelp(commandDisplay.prefix), "init");
6477
+ await runInitCommand();
6478
+ return;
6479
+ }
5834
6480
  if (command === "doctor") {
5835
6481
  const options = parseDoctorOptions(argv.slice(1));
5836
6482
  await runDoctorCommand({
@@ -5844,6 +6490,11 @@ async function run() {
5844
6490
  await runPushCommand(options);
5845
6491
  return;
5846
6492
  }
6493
+ if (command === "connect") {
6494
+ parseBareCommandOptions(argv.slice(1), renderConnectHelp(commandDisplay.prefix), "connect");
6495
+ await runConnectCommand();
6496
+ return;
6497
+ }
5847
6498
  if (command === "login") {
5848
6499
  await runLoginCommand();
5849
6500
  return;
@@ -5852,10 +6503,31 @@ async function run() {
5852
6503
  runLogoutCommand();
5853
6504
  return;
5854
6505
  }
6506
+ if (command === "mcp-setup") {
6507
+ parseBareCommandOptions(argv.slice(1), renderMcpSetupHelp(commandDisplay.prefix), "mcp-setup");
6508
+ await runMcpSetupCommand();
6509
+ return;
6510
+ }
5855
6511
  throw new Error(
5856
6512
  `Unknown command "${command}". Run \`${formatCommand("--help", commandDisplay.prefix)}\` to see available commands.`
5857
6513
  );
5858
6514
  }
6515
+ function parseBareCommandOptions(raw, helpText, commandName) {
6516
+ const argv2 = expandEqualsArgs(raw);
6517
+ for (const arg of argv2) {
6518
+ switch (arg) {
6519
+ case "--help":
6520
+ case "-h":
6521
+ process.stdout.write(helpText);
6522
+ process.exit(0);
6523
+ break;
6524
+ default:
6525
+ throw new Error(
6526
+ `Unknown ${commandName} option "${arg}". Run \`${formatCommand([commandName, "--help"], commandDisplay.prefix)}\` for usage.`
6527
+ );
6528
+ }
6529
+ }
6530
+ }
5859
6531
  function parseAuditOptions(raw) {
5860
6532
  const argv2 = expandEqualsArgs(raw);
5861
6533
  const options = {};