@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/README.md +65 -36
- package/dist/index.js +1069 -397
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/skills/xerg/SKILL.md +48 -20
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.
|
|
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
|
-
|
|
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
|
-
|
|
3660
|
-
|
|
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 =
|
|
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 ?
|
|
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
|
-
|
|
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/
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
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
|
-
|
|
5301
|
-
|
|
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
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
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
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
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/
|
|
5478
|
-
|
|
5479
|
-
|
|
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
|
-
|
|
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
|
|
5498
|
-
|
|
5499
|
-
|
|
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
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
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
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
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
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
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
|
|
5581
|
-
return
|
|
6215
|
+
function describeCandidate(candidate) {
|
|
6216
|
+
return `Found local ${candidate.adapter.productName} data (${describeSources(candidate)}).`;
|
|
5582
6217
|
}
|
|
5583
|
-
function
|
|
5584
|
-
|
|
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
|
|
5587
|
-
|
|
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
|
|
6264
|
+
Waste intelligence for OpenClaw and Hermes workflows.
|
|
5682
6265
|
|
|
5683
6266
|
Usage:
|
|
5684
6267
|
${formatCommand("<command> [options]", display.prefix)}
|
|
5685
6268
|
|
|
5686
|
-
|
|
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 = {};
|