mergen-server 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. package/LICENSE +57 -0
  2. package/README.md +96 -0
  3. package/dist/__stubs__/calibration.js +471 -0
  4. package/dist/__stubs__/causal.js +601 -0
  5. package/dist/__stubs__/closed-source.js +150 -0
  6. package/dist/__stubs__/license.js +35 -0
  7. package/dist/__stubs__/plans.js +197 -0
  8. package/dist/app.js +467 -0
  9. package/dist/cli.js +250 -0
  10. package/dist/commands/agent-identity.js +79 -0
  11. package/dist/commands/gate.js +107 -0
  12. package/dist/commands/github.js +632 -0
  13. package/dist/commands/incident.js +644 -0
  14. package/dist/commands/policy.js +710 -0
  15. package/dist/commands/scan.js +243 -0
  16. package/dist/commands/setup.js +1218 -0
  17. package/dist/commands/shared.js +109 -0
  18. package/dist/commands/team.js +809 -0
  19. package/dist/datadog/blame-attribution.js +228 -0
  20. package/dist/datadog/client.js +159 -0
  21. package/dist/datadog/compactor.js +169 -0
  22. package/dist/datadog/fingerprinter.js +34 -0
  23. package/dist/datadog/incident-state.js +21 -0
  24. package/dist/datadog/line-matcher.js +56 -0
  25. package/dist/datadog/memory-store.js +399 -0
  26. package/dist/datadog/otel-trace.js +67 -0
  27. package/dist/index.js +505 -0
  28. package/dist/intelligence/action-risk.js +132 -0
  29. package/dist/intelligence/activity-feed.js +40 -0
  30. package/dist/intelligence/agent-identity.js +115 -0
  31. package/dist/intelligence/agent-pipeline.js +329 -0
  32. package/dist/intelligence/agent-profiles.js +81 -0
  33. package/dist/intelligence/ai-commit.js +16 -0
  34. package/dist/intelligence/approval-analytics.js +42 -0
  35. package/dist/intelligence/approval-events.js +5 -0
  36. package/dist/intelligence/arch-boundaries.js +94 -0
  37. package/dist/intelligence/arch-graph.js +117 -0
  38. package/dist/intelligence/autonomy.js +397 -0
  39. package/dist/intelligence/azure-broker.js +57 -0
  40. package/dist/intelligence/azure-proxy-sessions.js +49 -0
  41. package/dist/intelligence/baseline.js +1 -0
  42. package/dist/intelligence/behavior-baseline.js +134 -0
  43. package/dist/intelligence/billing.js +1 -0
  44. package/dist/intelligence/blast-radius.js +224 -0
  45. package/dist/intelligence/bypass.js +157 -0
  46. package/dist/intelligence/calibration-classifier.js +77 -0
  47. package/dist/intelligence/calibration-git-sync.js +74 -0
  48. package/dist/intelligence/calibration.js +1 -0
  49. package/dist/intelligence/cascade-detector.js +58 -0
  50. package/dist/intelligence/case-study-generator.js +126 -0
  51. package/dist/intelligence/causal-graph.js +126 -0
  52. package/dist/intelligence/causal.js +1 -0
  53. package/dist/intelligence/change-risk.js +102 -0
  54. package/dist/intelligence/compliance-report.js +134 -0
  55. package/dist/intelligence/confidence-report.js +85 -0
  56. package/dist/intelligence/corpus-to-policy.js +140 -0
  57. package/dist/intelligence/credential-broker.js +112 -0
  58. package/dist/intelligence/credential-misuse.js +39 -0
  59. package/dist/intelligence/debug-sessions.js +108 -0
  60. package/dist/intelligence/decision-escalation.js +36 -0
  61. package/dist/intelligence/decision-memory.js +47 -0
  62. package/dist/intelligence/decision-similarity.js +85 -0
  63. package/dist/intelligence/default-safety-tests.js +53 -0
  64. package/dist/intelligence/degradation-watcher.js +108 -0
  65. package/dist/intelligence/detector-plugins.js +70 -0
  66. package/dist/intelligence/detectors.js +1 -0
  67. package/dist/intelligence/device-auth.js +138 -0
  68. package/dist/intelligence/diff-size.js +42 -0
  69. package/dist/intelligence/ebpf-verify.js +45 -0
  70. package/dist/intelligence/enterprise-policy-engine.js +1122 -0
  71. package/dist/intelligence/error-fingerprint.js +1 -0
  72. package/dist/intelligence/execution-gate.js +100 -0
  73. package/dist/intelligence/execution-mode.js +22 -0
  74. package/dist/intelligence/gate-analytics.js +256 -0
  75. package/dist/intelligence/gate-decision.js +64 -0
  76. package/dist/intelligence/gcp-broker.js +50 -0
  77. package/dist/intelligence/git-adr-sync.js +207 -0
  78. package/dist/intelligence/hitl-hold.js +460 -0
  79. package/dist/intelligence/hypothesis-history.js +93 -0
  80. package/dist/intelligence/idp-identity.js +83 -0
  81. package/dist/intelligence/impl-critic.js +216 -0
  82. package/dist/intelligence/incident-autopilot.js +622 -0
  83. package/dist/intelligence/incident-replay.js +162 -0
  84. package/dist/intelligence/incident-result-cache.js +33 -0
  85. package/dist/intelligence/incident-similarity.js +58 -0
  86. package/dist/intelligence/infra-detectors.js +312 -0
  87. package/dist/intelligence/license.js +185 -0
  88. package/dist/intelligence/llm-spokesperson.js +62 -0
  89. package/dist/intelligence/mcp-prompts.js +320 -0
  90. package/dist/intelligence/mcp-resources.js +171 -0
  91. package/dist/intelligence/normalize.js +108 -0
  92. package/dist/intelligence/notifications.js +83 -0
  93. package/dist/intelligence/oidc-issuer.js +87 -0
  94. package/dist/intelligence/override-corpus.js +597 -0
  95. package/dist/intelligence/planning-gate.js +84 -0
  96. package/dist/intelligence/plans.js +223 -0
  97. package/dist/intelligence/platt-scaling.js +164 -0
  98. package/dist/intelligence/policy-proposals.js +84 -0
  99. package/dist/intelligence/policy-suggester.js +161 -0
  100. package/dist/intelligence/policy-sync.js +103 -0
  101. package/dist/intelligence/policy-test-runner.js +35 -0
  102. package/dist/intelligence/postmortem-parser.js +181 -0
  103. package/dist/intelligence/postmortem-retrieval.js +224 -0
  104. package/dist/intelligence/postmortem-store.js +596 -0
  105. package/dist/intelligence/pr-commenter.js +81 -0
  106. package/dist/intelligence/pr-policy-gate.js +38 -0
  107. package/dist/intelligence/pr-shadow-analyzer.js +229 -0
  108. package/dist/intelligence/process-tree.js +108 -0
  109. package/dist/intelligence/prompts.js +1 -0
  110. package/dist/intelligence/replay.js +38 -0
  111. package/dist/intelligence/repro-steps.js +1 -0
  112. package/dist/intelligence/resource-extractors.js +0 -0
  113. package/dist/intelligence/resource-file-context.js +138 -0
  114. package/dist/intelligence/risk-score.js +22 -0
  115. package/dist/intelligence/rollback.js +137 -0
  116. package/dist/intelligence/runbook-updater.js +143 -0
  117. package/dist/intelligence/sandbox.js +194 -0
  118. package/dist/intelligence/script-trust.js +104 -0
  119. package/dist/intelligence/session-metrics.js +67 -0
  120. package/dist/intelligence/session-threat-tracker.js +231 -0
  121. package/dist/intelligence/shadow-digest-cron.js +81 -0
  122. package/dist/intelligence/shadow-log.js +284 -0
  123. package/dist/intelligence/shell-ast.js +145 -0
  124. package/dist/intelligence/siem-forward.js +104 -0
  125. package/dist/intelligence/slack-digest.js +151 -0
  126. package/dist/intelligence/slack-override-loop.js +217 -0
  127. package/dist/intelligence/slack-routing.js +67 -0
  128. package/dist/intelligence/slack.js +900 -0
  129. package/dist/intelligence/sql-ast.js +31 -0
  130. package/dist/intelligence/team.js +1 -0
  131. package/dist/intelligence/telemetry.js +76 -0
  132. package/dist/intelligence/threshold-optimizer.js +75 -0
  133. package/dist/intelligence/token-budget.js +66 -0
  134. package/dist/intelligence/tool-guard.js +708 -0
  135. package/dist/intelligence/tool-manifest 2.js +74 -0
  136. package/dist/intelligence/tool-manifest.js +125 -0
  137. package/dist/intelligence/tools-analysis.js +857 -0
  138. package/dist/intelligence/tools-arch.js +152 -0
  139. package/dist/intelligence/tools-autonomy.js +495 -0
  140. package/dist/intelligence/tools-blast-radius.js +140 -0
  141. package/dist/intelligence/tools-browser.js +431 -0
  142. package/dist/intelligence/tools-change-timeline.js +154 -0
  143. package/dist/intelligence/tools-credentials.js +117 -0
  144. package/dist/intelligence/tools-datadog.js +233 -0
  145. package/dist/intelligence/tools-debug-sessions.js +209 -0
  146. package/dist/intelligence/tools-discovery.js +284 -0
  147. package/dist/intelligence/tools-infra.js +612 -0
  148. package/dist/intelligence/tools-intent.js +128 -0
  149. package/dist/intelligence/tools-memory.js +514 -0
  150. package/dist/intelligence/tools-runbook.js +951 -0
  151. package/dist/intelligence/tools-sessions.js +107 -0
  152. package/dist/intelligence/tools-state.js +232 -0
  153. package/dist/intelligence/tools-utility.js +430 -0
  154. package/dist/intelligence/tools-validate.js +215 -0
  155. package/dist/intelligence/tools.js +1 -0
  156. package/dist/intelligence/trace-context.js +11 -0
  157. package/dist/intelligence/unclassified-clusters.js +87 -0
  158. package/dist/intelligence/usage.js +195 -0
  159. package/dist/routes/active-authors.js +63 -0
  160. package/dist/routes/activity-feed.js +21 -0
  161. package/dist/routes/adr.js +117 -0
  162. package/dist/routes/agent-activity.js +184 -0
  163. package/dist/routes/agent-blunders.js +163 -0
  164. package/dist/routes/agents.js +175 -0
  165. package/dist/routes/api-keys.js +65 -0
  166. package/dist/routes/arch.js +75 -0
  167. package/dist/routes/audit-export.js +93 -0
  168. package/dist/routes/billing-dashboard.js +158 -0
  169. package/dist/routes/billing-outcome.js +77 -0
  170. package/dist/routes/calibration.js +305 -0
  171. package/dist/routes/ci-gate-history.js +20 -0
  172. package/dist/routes/ci-gate.js +232 -0
  173. package/dist/routes/ci.js +293 -0
  174. package/dist/routes/compliance-report.js +24 -0
  175. package/dist/routes/confidence.js +30 -0
  176. package/dist/routes/credentials.js +119 -0
  177. package/dist/routes/dashboard.js +1407 -0
  178. package/dist/routes/decision-memory.js +42 -0
  179. package/dist/routes/demo.js +844 -0
  180. package/dist/routes/device-auth-stub.js +173 -0
  181. package/dist/routes/explain-why.js +39 -0
  182. package/dist/routes/gate-analytics.js +213 -0
  183. package/dist/routes/gate.js +28 -0
  184. package/dist/routes/github-webhook.js +364 -0
  185. package/dist/routes/habituation.js +30 -0
  186. package/dist/routes/health-integrations.js +243 -0
  187. package/dist/routes/heartbeats.js +45 -0
  188. package/dist/routes/hitl.js +364 -0
  189. package/dist/routes/impact-report.js +645 -0
  190. package/dist/routes/incident-webhook.js +63 -0
  191. package/dist/routes/incidents.js +366 -0
  192. package/dist/routes/layers.js +62 -0
  193. package/dist/routes/license.js +187 -0
  194. package/dist/routes/oidc-issuer.js +30 -0
  195. package/dist/routes/onboarding.js +170 -0
  196. package/dist/routes/otel.js +67 -0
  197. package/dist/routes/otlp-receiver.js +179 -0
  198. package/dist/routes/overrides.js +241 -0
  199. package/dist/routes/pagerduty.js +258 -0
  200. package/dist/routes/policies.js +809 -0
  201. package/dist/routes/policy-nl.js +169 -0
  202. package/dist/routes/postmortem.js +102 -0
  203. package/dist/routes/pr-shadow.js +47 -0
  204. package/dist/routes/rbac.js +61 -0
  205. package/dist/routes/replay.js +17 -0
  206. package/dist/routes/risk-report.js +164 -0
  207. package/dist/routes/runbooks.js +125 -0
  208. package/dist/routes/safety-test.js +174 -0
  209. package/dist/routes/sdk.js +222 -0
  210. package/dist/routes/sensor.js +819 -0
  211. package/dist/routes/sentry.js +140 -0
  212. package/dist/routes/sessions.js +43 -0
  213. package/dist/routes/setup-ui.js +594 -0
  214. package/dist/routes/shadow-report.js +107 -0
  215. package/dist/routes/slack-routing.js +171 -0
  216. package/dist/routes/team-usage.js +71 -0
  217. package/dist/routes/telemetry.js +28 -0
  218. package/dist/routes/tenants.js +199 -0
  219. package/dist/routes/tickets.js +167 -0
  220. package/dist/routes/validate.js +13 -0
  221. package/dist/routes/war-room.js +113 -0
  222. package/dist/scripts/check-arch.js +22 -0
  223. package/dist/seeds/community-corpus.js +176 -0
  224. package/dist/seeds/corpus.js +1224 -0
  225. package/dist/sensor/action-ledger.js +327 -0
  226. package/dist/sensor/adr-store.js +140 -0
  227. package/dist/sensor/agent-blunder-store.js +370 -0
  228. package/dist/sensor/agent-context-store.js +225 -0
  229. package/dist/sensor/agent-memory-store.js +231 -0
  230. package/dist/sensor/audit-fetch.js +23 -0
  231. package/dist/sensor/audit-log.js +273 -0
  232. package/dist/sensor/buffer-schemas.js +225 -0
  233. package/dist/sensor/buffer.js +599 -0
  234. package/dist/sensor/bypass-tracker.js +93 -0
  235. package/dist/sensor/ci-gate-history.js +70 -0
  236. package/dist/sensor/cloud-auth.js +183 -0
  237. package/dist/sensor/commit-context-store.js +235 -0
  238. package/dist/sensor/docker-log-stream.js +164 -0
  239. package/dist/sensor/docker-monitor.js +122 -0
  240. package/dist/sensor/extended-buffer.js +46 -0
  241. package/dist/sensor/feedback-token.js +41 -0
  242. package/dist/sensor/file-lock.js +6 -0
  243. package/dist/sensor/fs-watcher.js +106 -0
  244. package/dist/sensor/gate-heartbeat.js +122 -0
  245. package/dist/sensor/git-suspect.js +136 -0
  246. package/dist/sensor/habituation-store.js +77 -0
  247. package/dist/sensor/heartbeat-monitor.js +133 -0
  248. package/dist/sensor/incident-store.js +340 -0
  249. package/dist/sensor/infra-normalizer.js +513 -0
  250. package/dist/sensor/ingest.js +339 -0
  251. package/dist/sensor/jest-reporter.js +52 -0
  252. package/dist/sensor/k8s-events.js +132 -0
  253. package/dist/sensor/layer2-store.js +111 -0
  254. package/dist/sensor/layer3-store.js +230 -0
  255. package/dist/sensor/layer4-store.js +147 -0
  256. package/dist/sensor/logger.js +13 -0
  257. package/dist/sensor/otel-exporter.js +161 -0
  258. package/dist/sensor/paths.js +71 -0
  259. package/dist/sensor/policy-history.js +147 -0
  260. package/dist/sensor/pr-shadow-store.js +83 -0
  261. package/dist/sensor/process-watcher.js +162 -0
  262. package/dist/sensor/rbac.js +92 -0
  263. package/dist/sensor/redact.js +114 -0
  264. package/dist/sensor/redis-store.js +191 -0
  265. package/dist/sensor/route-reachability.js +64 -0
  266. package/dist/sensor/security-utils.js +18 -0
  267. package/dist/sensor/service-graph.js +170 -0
  268. package/dist/sensor/service-topology.js +227 -0
  269. package/dist/sensor/session-history.js +74 -0
  270. package/dist/sensor/session-persist.js +38 -0
  271. package/dist/sensor/shadow-promote.js +122 -0
  272. package/dist/sensor/sourcemap.js +281 -0
  273. package/dist/sensor/sqlite-store.js +205 -0
  274. package/dist/sensor/sso.js +164 -0
  275. package/dist/sensor/vitest-reporter.js +65 -0
  276. package/dist/sensor/watcher.js +48 -0
  277. package/dist/storage/interfaces.js +0 -0
  278. package/dist/storage/pg/pg-action-ledger.js +138 -0
  279. package/dist/storage/pg/pg-approval-store.js +107 -0
  280. package/dist/storage/pg/pg-blunder-store.js +193 -0
  281. package/dist/storage/pg/pg-ci-gate-history.js +83 -0
  282. package/dist/storage/pg/pg-client.js +24 -0
  283. package/dist/storage/pg/pg-event-store.js +63 -0
  284. package/dist/storage/pg/pg-incident-store.js +190 -0
  285. package/dist/storage/pg/pg-migrations.js +26 -0
  286. package/dist/storage/pg/pg-override-corpus.js +447 -0
  287. package/dist/storage/pg/pg-shadow-log.js +129 -0
  288. package/dist/storage/sqlite/sqlite-action-ledger.js +18 -0
  289. package/dist/storage/sqlite/sqlite-approval-store.js +37 -0
  290. package/dist/storage/sqlite/sqlite-blunder-store.js +27 -0
  291. package/dist/storage/sqlite/sqlite-ci-gate-history.js +19 -0
  292. package/dist/storage/sqlite/sqlite-event-store.js +29 -0
  293. package/dist/storage/sqlite/sqlite-incident-store.js +34 -0
  294. package/dist/storage/sqlite/sqlite-override-corpus.js +75 -0
  295. package/dist/storage/sqlite/sqlite-shadow-log.js +36 -0
  296. package/dist/storage/store-factory.js +55 -0
  297. package/dist/storage/store-registry.js +31 -0
  298. package/dist/update-checker.js +107 -0
  299. package/dist/workers/autopilot-worker.js +28 -0
  300. package/dist/workers/notification-worker.js +28 -0
  301. package/dist/workers/queues.js +58 -0
  302. package/dist/workers/validation-worker.js +24 -0
  303. package/dist/workers/worker-registry.js +18 -0
  304. package/package.json +123 -0
  305. package/sdk/mergen-inject.js +260 -0
  306. package/sdk/node.js +313 -0
  307. package/sdk/vite-plugin.ts +57 -0
  308. package/sdk/webpack-plugin.js +106 -0
@@ -0,0 +1,1218 @@
1
+ import { execSync, spawn, spawnSync } from "child_process";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
3
+ import * as path from "path";
4
+ import { resolve, dirname } from "path";
5
+ import { homedir } from "os";
6
+ import { log, success, error, hr, ask, sleep, SERVER_ENTRY, CLI_ENTRY } from "./shared.js";
7
+ import { connectCommand, backfillCommand } from "./github.js";
8
+ import { guardCommand } from "./policy.js";
9
+ import { watchCommand } from "./incident.js";
10
+ async function seedBuiltinRunbooks() {
11
+ const { mkdirSync: mkd, copyFileSync, existsSync: ex } = await import("fs");
12
+ const { join: j, dirname: dirname2, resolve: res } = await import("path");
13
+ const { homedir: hd } = await import("os");
14
+ const { fileURLToPath: fileURLToPath2 } = await import("url");
15
+ const RUNBOOKS_DIR = j(hd(), ".mergen", "runbooks", "_builtin");
16
+ mkd(RUNBOOKS_DIR, { recursive: true });
17
+ const __filename = fileURLToPath2(import.meta.url);
18
+ const pkgRunbooks = res(dirname2(__filename), "..", "runbooks");
19
+ if (!ex(pkgRunbooks)) return;
20
+ const { readdirSync } = await import("fs");
21
+ let seeded = 0;
22
+ for (const f of readdirSync(pkgRunbooks)) {
23
+ if (!f.endsWith(".yaml") && !f.endsWith(".yml")) continue;
24
+ const dest = j(RUNBOOKS_DIR, f);
25
+ if (!ex(dest)) {
26
+ copyFileSync(j(pkgRunbooks, f), dest);
27
+ seeded++;
28
+ }
29
+ }
30
+ if (seeded > 0) {
31
+ log(`Seeded ${seeded} built-in runbook${seeded !== 1 ? "s" : ""} \u2192 ~/.mergen/runbooks/_builtin/`, "\u2705");
32
+ }
33
+ }
34
+ async function loginCommand() {
35
+ const { exec } = await import("child_process");
36
+ const { activateKey } = await import("../intelligence/license.js");
37
+ const { getPlan } = await import("../intelligence/plans.js");
38
+ const { runDeviceAuthorization, DeviceAuthError } = await import("../intelligence/device-auth.js");
39
+ const CAP_LABELS = {
40
+ hitlApproval: "HITL approve/deny holds",
41
+ overrideCorpusEnforce: "override-corpus enforcement",
42
+ ciGate: "CI/CD blast-radius gate",
43
+ credentialBrokering: "ephemeral multi-cloud credentials",
44
+ agentIam: "Agent IAM + SSO federation"
45
+ };
46
+ const planSummary = (planId) => {
47
+ const plan = getPlan(typeof planId === "string" ? planId : void 0);
48
+ const caps = Object.entries(plan.capabilities).filter(([, on]) => on).map(([cap]) => CAP_LABELS[cap] ?? cap);
49
+ const suffix = caps.length > 0 ? ` \u2014 unlocks ${caps.join(", ")}` : "";
50
+ return `${plan.name}${suffix}`;
51
+ };
52
+ const activateAndReport = async (key) => {
53
+ log(`Activating license key: ${key.substring(0, 12)}...`);
54
+ try {
55
+ const result = await activateKey(key);
56
+ success(`License activated! Plan: ${planSummary(result.planId)}`);
57
+ process.exit(0);
58
+ } catch (err) {
59
+ error(`Activation failed: ${err.message}`);
60
+ process.exit(1);
61
+ }
62
+ };
63
+ const args = process.argv.slice(3);
64
+ const keyIndex = args.indexOf("--key");
65
+ if (keyIndex !== -1 && args[keyIndex + 1]) {
66
+ await activateAndReport(args[keyIndex + 1]);
67
+ }
68
+ const apiUrl = process.env.MERGEN_API_URL ?? "https://api.mergen.app";
69
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
70
+ console.log("\u{1F680} Signing in to Mergen...\n");
71
+ try {
72
+ const activation = await runDeviceAuthorization(apiUrl, {
73
+ onPrompt: (code) => {
74
+ console.log(`Your device code: ${code.userCode}
75
+ `);
76
+ console.log("Opening your browser to approve this device:");
77
+ console.log(` ${code.verificationUriComplete}
78
+ `);
79
+ console.log("If it doesn\u2019t open, visit the URL above and confirm the code.");
80
+ console.log("Waiting for approval...");
81
+ exec(`${openCmd} "${code.verificationUriComplete}"`).unref();
82
+ }
83
+ });
84
+ await activateAndReport(activation.key);
85
+ } catch (err) {
86
+ if (err instanceof DeviceAuthError) {
87
+ error(`Sign-in failed: ${err.message}`);
88
+ if (err.reason === "network") {
89
+ console.log("\nThe activation server was unreachable. If you have a license key, run:");
90
+ console.log(" mergen-server login --key <your-key>");
91
+ }
92
+ } else {
93
+ error(`Sign-in failed: ${err.message}`);
94
+ }
95
+ process.exit(1);
96
+ }
97
+ }
98
+ function resolveServerEntry() {
99
+ return SERVER_ENTRY;
100
+ }
101
+ async function quickSetup(ideFlag) {
102
+ const { exec } = await import("child_process");
103
+ const { activateKey } = await import("../intelligence/license.js");
104
+ const { runDeviceAuthorization, DeviceAuthError } = await import("../intelligence/device-auth.js");
105
+ const start = performance.now();
106
+ console.log("\n\u{1F680} Mergen\n");
107
+ const nodeVersion = parseInt(process.versions.node.split(".")[0]);
108
+ if (nodeVersion < 18) {
109
+ error(`Node.js 18+ required (you have ${process.versions.node})`);
110
+ log("Install from: https://nodejs.org/");
111
+ process.exit(1);
112
+ }
113
+ success(`Node.js ${process.versions.node}`);
114
+ log("Detecting IDE...");
115
+ if (ideFlag) {
116
+ await configureIDE(ideFlag);
117
+ success(`${ideFlag} configured`);
118
+ } else {
119
+ const detected = await detectAllIDEs();
120
+ const targets = detected.length > 0 ? detected : ["claude-code", "cursor", "vscode", "windsurf"];
121
+ for (const ide of targets) {
122
+ await configureIDE(ide);
123
+ }
124
+ if (detected.length > 0) {
125
+ success(`IDE(s) configured: ${detected.join(", ")}`);
126
+ } else {
127
+ success("No IDE detected \u2014 configured all 4 (Claude Code, Cursor, VS Code, Windsurf)");
128
+ }
129
+ }
130
+ log("Signing in to mergen.app...");
131
+ const apiUrl = process.env.MERGEN_API_URL ?? "https://api.mergen.app";
132
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
133
+ try {
134
+ const activation = await runDeviceAuthorization(apiUrl, {
135
+ onPrompt: (code) => {
136
+ console.log(`
137
+ Device code: ${code.userCode}`);
138
+ console.log(" Opening browser to approve \u2014 confirm the code shown above.");
139
+ console.log(` If browser doesn't open: ${code.verificationUriComplete}
140
+ `);
141
+ exec(`${openCmd} "${code.verificationUriComplete}"`).unref();
142
+ }
143
+ });
144
+ await activateKey(activation.key);
145
+ success("Signed in");
146
+ } catch (err) {
147
+ if (err instanceof DeviceAuthError && err.reason === "network") {
148
+ log("Could not reach api.mergen.app. Continuing offline \u2014 run: mergen-server login", "\u26A0");
149
+ } else {
150
+ error(`Sign-in failed: ${err.message}`);
151
+ log("Run manually after setup: mergen-server login", "\u2139");
152
+ }
153
+ }
154
+ await seedBuiltinRunbooks();
155
+ const elapsed = ((performance.now() - start) / 1e3).toFixed(1);
156
+ hr();
157
+ success(`Setup complete in ${elapsed}s
158
+ `);
159
+ console.log("In your AI IDE, ask:");
160
+ console.log(' "What is Mergen doing right now?"');
161
+ console.log(' "Triage the latest incident"');
162
+ console.log(' "Block the command: terraform destroy --auto-approve"\n');
163
+ console.log("Add integrations (Slack, PagerDuty, GitHub):");
164
+ console.log(" mergen-server setup --full\n");
165
+ const rawArgs = process.argv.slice(3);
166
+ if (!rawArgs.includes("--no-start")) {
167
+ log("Starting server...");
168
+ process.argv = [...process.argv.slice(0, 3), "--then", "doctor"];
169
+ await startCommand();
170
+ } else {
171
+ log("Start when ready: mergen-server start", "\u2139");
172
+ }
173
+ }
174
+ async function detectAllIDEs() {
175
+ const found = [];
176
+ try {
177
+ execSync("which claude", { stdio: "ignore" });
178
+ found.push("claude-code");
179
+ } catch {
180
+ }
181
+ const cursorConfig = resolve(homedir(), ".cursor", "mcp.json");
182
+ const cursorApp = resolve("/Applications", "Cursor.app");
183
+ if (existsSync(cursorConfig) || existsSync(cursorApp)) found.push("cursor");
184
+ try {
185
+ execSync("which code", { stdio: "ignore" });
186
+ found.push("vscode");
187
+ } catch {
188
+ }
189
+ const windsurfConfig = resolve(homedir(), ".codeium", "windsurf", "mcp_config.json");
190
+ const windsurfApp = resolve("/Applications", "Windsurf.app");
191
+ if (existsSync(windsurfConfig) || existsSync(windsurfApp)) found.push("windsurf");
192
+ return found;
193
+ }
194
+ async function setupCommand() {
195
+ const rawArgs = process.argv.slice(3);
196
+ const yes = rawArgs.includes("--yes") || rawArgs.includes("-y");
197
+ const skipGitHub = yes || rawArgs.includes("--skip-github");
198
+ const showTiming = rawArgs.includes("--time");
199
+ const fullMode = rawArgs.includes("--full");
200
+ const ideFlag = rawArgs.find((a) => a.startsWith("--ide="))?.slice(6) ?? (rawArgs.includes("--ide") ? rawArgs[rawArgs.indexOf("--ide") + 1] : null);
201
+ if (!fullMode && !yes) {
202
+ await quickSetup(ideFlag);
203
+ return;
204
+ }
205
+ const _setupStart = performance.now();
206
+ const _stepTimes = [];
207
+ let _stepStart = _setupStart;
208
+ function _markStep(label) {
209
+ const ms = performance.now() - _stepStart;
210
+ _stepTimes.push({ label, ms });
211
+ _stepStart = performance.now();
212
+ }
213
+ console.log("\u{1F680} Mergen Setup Wizard\n");
214
+ if (yes) log("Non-interactive mode (--yes)\n", "\u2139");
215
+ hr();
216
+ log("Checking prerequisites...");
217
+ const nodeVersion = parseInt(process.versions.node.split(".")[0]);
218
+ if (nodeVersion < 18) {
219
+ error(`Node.js 18+ required (you have ${process.versions.node})`);
220
+ log("Install from: https://nodejs.org/");
221
+ process.exit(1);
222
+ }
223
+ success(`Node.js ${process.versions.node}`);
224
+ _markStep("Prerequisites");
225
+ log("\nDetecting IDE...");
226
+ const ide = ideFlag ?? await detectIDE();
227
+ success(`Found: ${ide}`);
228
+ _markStep("IDE detection");
229
+ log(`
230
+ Configuring ${ide}...`);
231
+ await configureIDE(ide);
232
+ success(`${ide} configured`);
233
+ _markStep("MCP configuration");
234
+ hr();
235
+ log("\nShadow mode (recommended starting point):");
236
+ console.log(" Shadow mode runs full diagnosis and posts Slack alerts but never executes fixes.");
237
+ console.log(` After 30 days you'll have a track record: "Mergen would have been correct 89% of the time."`);
238
+ console.log(" That data is what makes enabling autopilot a decision, not a leap of faith.");
239
+ if (!yes) {
240
+ const enableShadow = await ask("\nEnable shadow mode now? (y/n): ");
241
+ if (enableShadow.toLowerCase() === "y") {
242
+ process.env.MERGEN_SHADOW_MODE = "true";
243
+ const envPath = path.join(process.cwd(), ".env");
244
+ try {
245
+ const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
246
+ if (!existing.includes("MERGEN_SHADOW_MODE")) {
247
+ writeFileSync(envPath, `${existing}${existing.endsWith("\n") || existing === "" ? "" : "\n"}MERGEN_SHADOW_MODE=true
248
+ `);
249
+ success("Shadow mode enabled and written to .env");
250
+ } else {
251
+ success("Shadow mode enabled (MERGEN_SHADOW_MODE already in .env)");
252
+ }
253
+ } catch {
254
+ log("Set MERGEN_SHADOW_MODE=true in your server environment to persist this.", "\u2139");
255
+ success("Shadow mode enabled for this session");
256
+ }
257
+ } else {
258
+ log('Skipped \u2014 enable later: echo "MERGEN_SHADOW_MODE=true" >> .env', "\u2139");
259
+ }
260
+ }
261
+ hr();
262
+ log("\nLocal execution sandbox:");
263
+ console.log(" Scopes `mergen-server exec` to your workspace: blocks writes outside it and");
264
+ console.log(" denies network access by default. On macOS this uses the native sandbox-exec");
265
+ console.log(" tool \u2014 no Docker daemon, no container, near-zero startup overhead.");
266
+ async function enableSeatbeltInEnv() {
267
+ const envPath = path.join(process.cwd(), ".env");
268
+ try {
269
+ const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
270
+ if (!existing.includes("MERGEN_SANDBOX")) {
271
+ writeFileSync(envPath, `${existing}${existing.endsWith("\n") || existing === "" ? "" : "\n"}MERGEN_SANDBOX=seatbelt
272
+ `);
273
+ success("Sandbox execution enabled (MERGEN_SANDBOX=seatbelt), written to .env");
274
+ } else {
275
+ success("MERGEN_SANDBOX already set in .env \u2014 left unchanged");
276
+ }
277
+ } catch {
278
+ process.env.MERGEN_SANDBOX = "seatbelt";
279
+ success("Sandbox execution enabled for this session");
280
+ }
281
+ }
282
+ if (process.platform !== "darwin") {
283
+ log("Skipped \u2014 sandbox-exec is macOS-only. On Linux, use MERGEN_SANDBOX=docker (needs Docker) or", "\u2139");
284
+ log("MERGEN_EBPF_VERIFY=true (needs bpftrace) instead.", "\u2139");
285
+ } else if (yes) {
286
+ await enableSeatbeltInEnv();
287
+ } else {
288
+ const enableSandbox = await ask("\nEnable native sandbox execution now? (y/n): ");
289
+ if (enableSandbox.toLowerCase() === "y") {
290
+ await enableSeatbeltInEnv();
291
+ log("If a wrapped command needs to write outside the workspace (e.g. a package cache),", "\u2139");
292
+ log("set MERGEN_SANDBOX_EXTRA_WRITE_PATHS=/path/one,/path/two", "\u2139");
293
+ } else {
294
+ log('Skipped \u2014 enable later: echo "MERGEN_SANDBOX=seatbelt" >> .env', "\u2139");
295
+ }
296
+ }
297
+ _markStep("Execution sandbox");
298
+ hr();
299
+ log("\nSlack alerts (recommended \u2014 where Mergen posts incident analysis):");
300
+ console.log(" Without Slack, Mergen diagnoses silently. With Slack, every incident fires a");
301
+ console.log(" thread: root cause, confidence score, proposed fix, and HITL approve/deny buttons.");
302
+ if (!yes) {
303
+ const enableSlack = await ask("\nConnect Slack now? (y/n): ");
304
+ if (enableSlack.toLowerCase() === "y") {
305
+ console.log("");
306
+ console.log(" 1. Go to https://api.slack.com/apps \u2192 Create New App \u2192 From manifest");
307
+ console.log(" 2. Grant scopes: chat:write, chat:write.public, incoming-webhook");
308
+ console.log(" 3. Install to your workspace \u2192 copy the Bot User OAuth Token");
309
+ console.log("");
310
+ const slackToken = (await ask(" Bot token (xoxb-...): ")).trim();
311
+ const slackChannel = (await ask(" Channel for alerts (#incidents): ")).trim() || "#incidents";
312
+ let verifiedToken = slackToken;
313
+ if (verifiedToken) {
314
+ process.stdout.write(" Verifying token with Slack... ");
315
+ try {
316
+ const res = await fetch("https://slack.com/api/auth.test", {
317
+ method: "POST",
318
+ headers: { Authorization: `Bearer ${verifiedToken}` },
319
+ signal: AbortSignal.timeout(5e3)
320
+ });
321
+ const body = await res.json();
322
+ if (body.ok) {
323
+ console.log(`\u2713 (workspace: ${body.team ?? "unknown"})`);
324
+ } else {
325
+ console.log(`\u2717 (${body.error ?? "invalid token"})`);
326
+ const saveAnyway = await ask(" Token failed verification \u2014 save it anyway? (y/n): ");
327
+ if (saveAnyway.toLowerCase() !== "y") verifiedToken = "";
328
+ }
329
+ } catch (err) {
330
+ console.log(`\u2717 (${err instanceof Error ? err.message : "network error"})`);
331
+ const saveAnyway = await ask(" Could not reach Slack to verify \u2014 save it anyway? (y/n): ");
332
+ if (saveAnyway.toLowerCase() !== "y") verifiedToken = "";
333
+ }
334
+ }
335
+ if (verifiedToken) {
336
+ const envPath = path.join(process.cwd(), ".env");
337
+ try {
338
+ const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
339
+ let updated = existing;
340
+ if (!updated.includes("MERGEN_SLACK_BOT_TOKEN")) updated += `
341
+ MERGEN_SLACK_BOT_TOKEN=${verifiedToken}`;
342
+ if (!updated.includes("MERGEN_SLACK_CHANNEL")) updated += `
343
+ MERGEN_SLACK_CHANNEL=${slackChannel}`;
344
+ writeFileSync(envPath, updated.trimStart() + "\n", "utf8");
345
+ success(`Slack configured (channel: ${slackChannel}), written to .env`);
346
+ } catch {
347
+ success("Slack configured for this session");
348
+ log(`Set these in your .env to persist:
349
+ MERGEN_SLACK_BOT_TOKEN=${verifiedToken}
350
+ MERGEN_SLACK_CHANNEL=${slackChannel}`, "\u2139");
351
+ }
352
+ } else {
353
+ log("No token saved \u2014 skipped.", "\u2139");
354
+ }
355
+ } else {
356
+ log("Skipped. To add later: set MERGEN_SLACK_BOT_TOKEN and MERGEN_SLACK_CHANNEL in .env", "\u2139");
357
+ }
358
+ }
359
+ _markStep("Slack setup");
360
+ hr();
361
+ log("\nPagerDuty webhook (recommended \u2014 where incidents originate):");
362
+ console.log(" Connecting PagerDuty lets Mergen catch incident.triggered events, fetch traces,");
363
+ console.log(" and post the diagnosis to Slack. Autopilot (if enabled) runs the fix loop from here.");
364
+ if (!yes) {
365
+ const enablePD = await ask("\nConnect PagerDuty now? (y/n): ");
366
+ if (enablePD.toLowerCase() === "y") {
367
+ const port = process.env.HTTP_PORT ?? "3000";
368
+ console.log("");
369
+ console.log(" 1. PagerDuty \u2192 Service \u2192 Integrations \u2192 Add a webhook (v3)");
370
+ console.log(` 2. Endpoint URL: https://your-host:${port}/webhooks/pagerduty`);
371
+ console.log(' 3. Copy the "Webhook signing secret" shown after creation');
372
+ console.log("");
373
+ const pdSecret = (await ask(" Webhook signing secret: ")).trim();
374
+ if (pdSecret) {
375
+ if (pdSecret.length < 16) {
376
+ log("That looks short for a PagerDuty signing secret \u2014 double-check you copied the whole value.", "\u26A0");
377
+ }
378
+ const envPath = path.join(process.cwd(), ".env");
379
+ try {
380
+ const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
381
+ let updated = existing;
382
+ if (!updated.includes("MERGEN_PAGERDUTY_SECRET")) updated += `
383
+ MERGEN_PAGERDUTY_SECRET=${pdSecret}`;
384
+ writeFileSync(envPath, updated.trimStart() + "\n", "utf8");
385
+ success("PagerDuty configured, written to .env");
386
+ } catch {
387
+ success("PagerDuty configured for this session");
388
+ log(`Set this in your .env to persist:
389
+ MERGEN_PAGERDUTY_SECRET=${pdSecret}`, "\u2139");
390
+ }
391
+ log("Trigger a test event from PagerDuty once the server is running to confirm delivery end-to-end.", "\u2139");
392
+ } else {
393
+ log("No secret entered \u2014 skipped.", "\u2139");
394
+ }
395
+ } else {
396
+ log("Skipped. To add later: set MERGEN_PAGERDUTY_SECRET in .env (webhook \u2192 /webhooks/pagerduty)", "\u2139");
397
+ }
398
+ }
399
+ _markStep("PagerDuty setup");
400
+ hr();
401
+ log("\nGitHub intent archive (optional \u2014 powers explain_why):");
402
+ console.log(' Connecting GitHub populates PR history so Mergen can answer "why was this changed?"');
403
+ console.log(" Skip this if you just want incident triage \u2014 you can add it later.");
404
+ if (!skipGitHub && !yes) {
405
+ const connectGh = await ask("Connect GitHub now? (y/n): ");
406
+ if (connectGh.toLowerCase() === "y") {
407
+ await connectCommand(["github"]);
408
+ const doBackfill = await ask("\nBackfill historical PRs? Gives explain_why data on day 1. (y/n): ");
409
+ if (doBackfill.toLowerCase() === "y") {
410
+ await backfillCommand(["github"]);
411
+ }
412
+ } else {
413
+ log("Skipped. Run later: mergen-server connect github --repo <owner/repo>", "\u2139");
414
+ }
415
+ } else {
416
+ log("GitHub step skipped. Run later: mergen-server connect github --repo <owner/repo>", "\u2139");
417
+ }
418
+ _markStep("GitHub integration");
419
+ hr();
420
+ log("\nGit Pre-commit Hook (Recommended):");
421
+ console.log(" Enforces safety rules before each git commit, protecting your repo from");
422
+ console.log(" staged file errors regardless of the editor or AI autocomplete tool you use.");
423
+ if (!yes) {
424
+ const installHook = await ask("\nInstall Git pre-commit guard hook now? (y/n): ");
425
+ if (installHook.toLowerCase() === "y") {
426
+ try {
427
+ await guardCommand(["--install"]);
428
+ } catch (err) {
429
+ error(`Failed to install hook: ${err instanceof Error ? err.message : String(err)}`);
430
+ }
431
+ }
432
+ } else {
433
+ try {
434
+ await guardCommand(["--install"]);
435
+ } catch {
436
+ }
437
+ }
438
+ _markStep("Pre-commit hook");
439
+ await seedBuiltinRunbooks();
440
+ _markStep("Runbook seed");
441
+ hr();
442
+ log("\n\u2728 Setup complete!\n");
443
+ if (showTiming) {
444
+ const totalMs = performance.now() - _setupStart;
445
+ const pad = (s, n) => s + " ".repeat(Math.max(0, n - s.length));
446
+ console.log("Setup timing:");
447
+ for (const { label, ms } of _stepTimes) {
448
+ console.log(` ${pad(label + ":", 26)} ${(ms / 1e3).toFixed(1)}s`);
449
+ }
450
+ console.log(` ${"Total:".padEnd(26)} ${(totalMs / 1e3).toFixed(1)}s
451
+ `);
452
+ }
453
+ console.log("Next steps:");
454
+ console.log(" 1. Start server: mergen-server start");
455
+ console.log(" 2. Verify it works: mergen-server start --then doctor");
456
+ console.log(" (starts server, runs health checks, then keeps server running)");
457
+ console.log(" 3. Check shadow log: mergen-server shadow-report");
458
+ console.log(' 4. Enable autopilot: echo "MERGEN_AUTOPILOT=true" >> .env\n');
459
+ console.log("If PagerDuty or Datadog aren't set up yet, add to .env then restart:");
460
+ console.log(" MERGEN_PAGERDUTY_SECRET=<signing-secret> # webhook \u2192 /webhooks/pagerduty");
461
+ console.log(" DD_API_KEY=... DD_APP_KEY=... # trace fetch + validation\n");
462
+ console.log("Save your Day-1 baseline (compare at Day 30):");
463
+ console.log(" mergen-server impact-report --baseline");
464
+ console.log(" mergen-server impact-report --compare # at Day 30\n");
465
+ if (yes) {
466
+ log('Skipping "start now?" prompt in --yes mode. Run: mergen-server start', "\u2139");
467
+ return;
468
+ }
469
+ const startNow = await ask("Start server now and run doctor to verify? (y/n): ");
470
+ if (startNow.toLowerCase() === "y") {
471
+ process.argv = [...process.argv.slice(0, 3), "--then", "doctor"];
472
+ await startCommand();
473
+ }
474
+ }
475
+ async function testCommand() {
476
+ console.log("\u{1F50D} Testing Mergen installation\n");
477
+ hr();
478
+ const checks = [
479
+ { name: "Server binary", fn: checkBinary },
480
+ { name: "Server starts", fn: checkServerStarts },
481
+ { name: "Health endpoint", fn: checkHealth },
482
+ { name: "Event ingestion", fn: checkIngest },
483
+ { name: "IDE configuration", fn: checkIDEConfig }
484
+ ];
485
+ let passed = 0;
486
+ let failed = 0;
487
+ for (const check of checks) {
488
+ process.stdout.write(`${check.name}... `);
489
+ try {
490
+ await check.fn();
491
+ console.log("\u2713");
492
+ passed++;
493
+ } catch (err) {
494
+ console.log("\u2717");
495
+ error(` ${err instanceof Error ? err.message : "Unknown error"}`);
496
+ failed++;
497
+ }
498
+ }
499
+ hr();
500
+ if (failed === 0) {
501
+ success(`All checks passed (${passed}/${checks.length})`);
502
+ console.log("\n\u2728 Mergen is ready to use!\n");
503
+ } else {
504
+ error(`${failed} check(s) failed`);
505
+ console.log("\nRun: mergen-server setup");
506
+ process.exit(1);
507
+ }
508
+ }
509
+ async function ciCommand() {
510
+ const serverPath = SERVER_ENTRY;
511
+ if (!existsSync(serverPath)) {
512
+ error("Server binary not found. Run: npm run build");
513
+ process.exit(1);
514
+ }
515
+ console.log("\u{1F916} Mergen CI health check\n");
516
+ const proc = spawn("node", [serverPath], {
517
+ stdio: "pipe",
518
+ env: { ...process.env, NODE_ENV: "production", HTTP_PORT: "3099" }
519
+ });
520
+ const BASE = "http://127.0.0.1:3099";
521
+ let serverReady = false;
522
+ const stderrLines = [];
523
+ proc.stderr.on("data", (d) => {
524
+ stderrLines.push(d.toString().trim());
525
+ if (d.toString().includes("HTTP ingest listening")) {
526
+ serverReady = true;
527
+ }
528
+ });
529
+ const startDeadline = Date.now() + 1e4;
530
+ while (!serverReady && Date.now() < startDeadline) {
531
+ await sleep(200);
532
+ }
533
+ if (!serverReady) {
534
+ error("Server did not start within 10s");
535
+ console.error(stderrLines.join("\n"));
536
+ proc.kill();
537
+ process.exit(1);
538
+ }
539
+ let exitCode = 0;
540
+ try {
541
+ process.stdout.write("Health endpoint... ");
542
+ const health = await fetch(`${BASE}/health`);
543
+ if (!health.ok) throw new Error(`/health returned ${health.status}`);
544
+ const hd = await health.json();
545
+ if (hd.status !== "ok") throw new Error(`status=${hd.status}`);
546
+ console.log("\u2713");
547
+ process.stdout.write("Event ingestion... ");
548
+ const ingest = await fetch(`${BASE}/ingest`, {
549
+ method: "POST",
550
+ headers: { "Content-Type": "application/json" },
551
+ body: JSON.stringify({
552
+ type: "console",
553
+ level: "log",
554
+ args: ["mergen-ci-probe"],
555
+ url: "http://ci.test",
556
+ timestamp: Date.now()
557
+ })
558
+ });
559
+ if (!ingest.ok) throw new Error(`/ingest returned ${ingest.status}`);
560
+ console.log("\u2713");
561
+ success("All CI checks passed");
562
+ } catch (err) {
563
+ error(err instanceof Error ? err.message : String(err));
564
+ exitCode = 1;
565
+ } finally {
566
+ proc.kill();
567
+ }
568
+ process.exit(exitCode);
569
+ }
570
+ async function startCommand() {
571
+ const rawArgs = process.argv.slice(3);
572
+ const thenIdx = rawArgs.indexOf("--then");
573
+ const thenCmd = thenIdx !== -1 ? rawArgs[thenIdx + 1] : null;
574
+ const serverPath = SERVER_ENTRY;
575
+ if (!existsSync(serverPath)) {
576
+ error("Server not found. Run: mergen-server setup");
577
+ process.exit(1);
578
+ }
579
+ if (thenCmd) {
580
+ const mergenHost = process.env.MERGEN_HOST ?? "127.0.0.1";
581
+ const srv = spawn("node", [serverPath], {
582
+ stdio: "ignore",
583
+ detached: true,
584
+ env: { ...process.env, NODE_ENV: "production" }
585
+ });
586
+ srv.unref();
587
+ process.stdout.write("Starting Mergen server...");
588
+ let serverPort = 3e3;
589
+ let ready = false;
590
+ for (let attempt = 0; attempt < 20 && !ready; attempt++) {
591
+ await sleep(500);
592
+ for (let p = 3e3; p <= 3010; p++) {
593
+ try {
594
+ const r = await fetch(`http://${mergenHost}:${p}/health`, { signal: AbortSignal.timeout(300) });
595
+ if (r.ok) {
596
+ serverPort = p;
597
+ ready = true;
598
+ break;
599
+ }
600
+ } catch {
601
+ }
602
+ }
603
+ }
604
+ console.log(ready ? ` ready on :${serverPort}` : " (timeout \u2014 proceeding anyway)");
605
+ if (ready) {
606
+ try {
607
+ const mergenHost2 = process.env.MERGEN_HOST ?? "127.0.0.1";
608
+ const r = await fetch(`http://${mergenHost2}:${serverPort}/gate/heatmap`, {
609
+ signal: AbortSignal.timeout(2e3)
610
+ });
611
+ if (r.ok) {
612
+ const heatmap = await r.json();
613
+ const services = heatmap.services ?? [];
614
+ if (services.length > 0) {
615
+ console.log("");
616
+ console.log("\u2B21 Onboarding snapshot \u2014 what Mergen has observed so far:");
617
+ for (const svc of services.slice(0, 5)) {
618
+ const blast = svc.avgBlastScore != null ? ` (avg blast-radius: ${Math.round((svc.avgBlastScore ?? 0) * 100)}%)` : "";
619
+ console.log(` \u2022 ${svc.service}: ${svc.calls} gate calls${blast}`);
620
+ }
621
+ console.log(" Run: mergen-server shadow-report for the full picture");
622
+ console.log("");
623
+ }
624
+ }
625
+ } catch {
626
+ }
627
+ }
628
+ const thenParts = thenCmd.split(/\s+/);
629
+ const thenProc = spawn(thenParts[0], thenParts.slice(1), { stdio: "inherit" });
630
+ await new Promise((res) => thenProc.on("exit", () => res()));
631
+ const fgSrv = spawn("node", [serverPath], { stdio: "inherit", env: { ...process.env, NODE_ENV: "production" } });
632
+ fgSrv.on("error", (err) => {
633
+ error(`Server error: ${err.message}`);
634
+ process.exit(1);
635
+ });
636
+ fgSrv.on("exit", (code) => {
637
+ process.exit(code || 0);
638
+ });
639
+ process.on("SIGINT", () => {
640
+ fgSrv.kill("SIGINT");
641
+ });
642
+ return;
643
+ }
644
+ log("Starting Mergen server...\n");
645
+ const server = spawn("node", [serverPath], {
646
+ stdio: "inherit",
647
+ env: { ...process.env, NODE_ENV: "production" }
648
+ });
649
+ server.on("error", (err) => {
650
+ error(`Failed to start: ${err.message}`);
651
+ process.exit(1);
652
+ });
653
+ server.on("exit", (code) => {
654
+ if (code !== 0) {
655
+ error(`Server exited with code ${code}`);
656
+ }
657
+ process.exit(code || 0);
658
+ });
659
+ process.on("SIGINT", () => {
660
+ log("Stopping server...");
661
+ server.kill("SIGINT");
662
+ });
663
+ }
664
+ async function detectIDE() {
665
+ try {
666
+ execSync("which claude", { stdio: "ignore" });
667
+ return "claude-code";
668
+ } catch {
669
+ }
670
+ const cursorConfig = resolve(homedir(), ".cursor", "mcp.json");
671
+ if (existsSync(cursorConfig)) {
672
+ return "cursor";
673
+ }
674
+ try {
675
+ execSync("which code", { stdio: "ignore" });
676
+ return "vscode";
677
+ } catch {
678
+ }
679
+ const windsurfConfig = resolve(homedir(), ".codeium", "windsurf", "mcp_config.json");
680
+ if (existsSync(windsurfConfig)) {
681
+ return "windsurf";
682
+ }
683
+ return "unknown";
684
+ }
685
+ async function configureIDE(ide) {
686
+ const mcpEntry = {
687
+ command: "npx",
688
+ args: ["mergen-server", "start"]
689
+ };
690
+ function mergeMcpConfig(configPath) {
691
+ let existing = {};
692
+ if (existsSync(configPath)) {
693
+ try {
694
+ existing = JSON.parse(readFileSync(configPath, "utf8"));
695
+ } catch {
696
+ }
697
+ }
698
+ const mcpServers = existing.mcpServers ?? {};
699
+ mcpServers["mergen"] = mcpEntry;
700
+ existing.mcpServers = mcpServers;
701
+ mkdirSync(dirname(configPath), { recursive: true });
702
+ writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
703
+ }
704
+ switch (ide) {
705
+ case "claude-code":
706
+ try {
707
+ spawnSync("claude", ["mcp", "add", "mergen", "--transport", "stdio", "--", "npx", "mergen-server", "start"], {
708
+ stdio: "inherit",
709
+ shell: false
710
+ });
711
+ } catch {
712
+ log("Run manually:", "\u2139");
713
+ console.log(" claude mcp add mergen --transport stdio -- npx mergen-server start");
714
+ }
715
+ installClaudeCodePreToolUseHook();
716
+ break;
717
+ case "cursor": {
718
+ const configPath = resolve(homedir(), ".cursor", "mcp.json");
719
+ mergeMcpConfig(configPath);
720
+ log(`Cursor config updated: ${configPath}`);
721
+ break;
722
+ }
723
+ case "vscode": {
724
+ const configPath = resolve(homedir(), ".vscode", "mcp.json");
725
+ mergeMcpConfig(configPath);
726
+ log(`VS Code config updated: ${configPath}`);
727
+ break;
728
+ }
729
+ case "windsurf": {
730
+ const configPath = resolve(homedir(), ".codeium", "windsurf", "mcp_config.json");
731
+ mergeMcpConfig(configPath);
732
+ log(`Windsurf config updated: ${configPath}`);
733
+ break;
734
+ }
735
+ default:
736
+ log(`Manual setup required for ${ide}`, "\u26A0");
737
+ console.log(` Add to your IDE MCP config: { "command": "npx", "args": ["mergen-server", "start"] }`);
738
+ }
739
+ }
740
+ function installClaudeCodePreToolUseHook(settingsPath = resolve(homedir(), ".claude", "settings.json")) {
741
+ try {
742
+ let settings = {};
743
+ if (existsSync(settingsPath)) {
744
+ try {
745
+ settings = JSON.parse(readFileSync(settingsPath, "utf8"));
746
+ } catch {
747
+ log(`~/.claude/settings.json exists but is not valid JSON \u2014 skipping automatic hook install. Add it manually (see: mergen-server --help).`, "\u26A0");
748
+ return;
749
+ }
750
+ }
751
+ const hookCommand = `node "${CLI_ENTRY}" gate-check`;
752
+ const hooks = settings.hooks ?? {};
753
+ const preToolUse = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
754
+ const alreadyInstalled = preToolUse.some((entry) => {
755
+ const entryHooks = Array.isArray(entry.hooks) ? entry.hooks : [];
756
+ return entryHooks.some((h) => h.type === "command" && h.command === hookCommand);
757
+ });
758
+ if (alreadyInstalled) {
759
+ log("Claude Code PreToolUse hook already installed.");
760
+ return;
761
+ }
762
+ preToolUse.push({
763
+ matcher: "Bash",
764
+ hooks: [{ type: "command", command: hookCommand }]
765
+ });
766
+ settings.hooks = { ...hooks, PreToolUse: preToolUse };
767
+ mkdirSync(dirname(settingsPath), { recursive: true });
768
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf8");
769
+ log(`Installed a PreToolUse hook for the Bash tool \u2192 ${settingsPath}`);
770
+ log("This covers Claude Code's Bash tool specifically \u2014 it does not extend MCP gate coverage to other IDEs or raw terminal use outside Claude Code.", "\u2139");
771
+ } catch (err) {
772
+ log(`Could not install the Claude Code PreToolUse hook automatically: ${err instanceof Error ? err.message : String(err)}`, "\u26A0");
773
+ }
774
+ }
775
+ async function checkBinary() {
776
+ const serverPath = SERVER_ENTRY;
777
+ if (!existsSync(serverPath)) {
778
+ throw new Error(`Server not found at ${serverPath}`);
779
+ }
780
+ }
781
+ async function checkServerStarts() {
782
+ const serverPath = SERVER_ENTRY;
783
+ const proc = spawn("node", [serverPath], { stdio: "pipe" });
784
+ await new Promise((resolve2, reject) => {
785
+ const timeout = setTimeout(() => {
786
+ proc.kill();
787
+ reject(new Error("Server did not start within 5s"));
788
+ }, 5e3);
789
+ proc.stderr.on("data", (data) => {
790
+ if (data.toString().includes("HTTP ingest listening")) {
791
+ proc.kill();
792
+ clearTimeout(timeout);
793
+ resolve2();
794
+ }
795
+ });
796
+ proc.on("error", (err) => {
797
+ clearTimeout(timeout);
798
+ reject(err);
799
+ });
800
+ });
801
+ }
802
+ async function checkHealth() {
803
+ const serverPath = SERVER_ENTRY;
804
+ const proc = spawn("node", [serverPath], { stdio: "pipe" });
805
+ await sleep(2e3);
806
+ try {
807
+ const response = await fetch("http://127.0.0.1:3000/health");
808
+ if (!response.ok) {
809
+ throw new Error(`Health check returned ${response.status}`);
810
+ }
811
+ const data = await response.json();
812
+ if (data.status !== "ok") {
813
+ throw new Error(`Health status: ${data.status}`);
814
+ }
815
+ } finally {
816
+ proc.kill();
817
+ }
818
+ }
819
+ async function checkIngest() {
820
+ const serverPath = SERVER_ENTRY;
821
+ const proc = spawn("node", [serverPath], { stdio: "pipe" });
822
+ await sleep(2e3);
823
+ try {
824
+ const response = await fetch("http://127.0.0.1:3000/ingest", {
825
+ method: "POST",
826
+ headers: { "Content-Type": "application/json" },
827
+ body: JSON.stringify({
828
+ type: "console",
829
+ level: "log",
830
+ args: ["Mergen test"],
831
+ url: "http://test",
832
+ timestamp: Date.now()
833
+ })
834
+ });
835
+ if (!response.ok) {
836
+ throw new Error(`Ingest returned ${response.status}`);
837
+ }
838
+ } finally {
839
+ proc.kill();
840
+ }
841
+ }
842
+ async function checkIDEConfig() {
843
+ const ide = await detectIDE();
844
+ let configPath;
845
+ switch (ide) {
846
+ case "cursor":
847
+ configPath = resolve(homedir(), ".cursor", "mcp.json");
848
+ break;
849
+ case "vscode":
850
+ configPath = resolve(homedir(), ".vscode", "mcp.json");
851
+ break;
852
+ case "windsurf":
853
+ configPath = resolve(homedir(), ".codeium", "windsurf", "mcp_config.json");
854
+ break;
855
+ case "claude-code":
856
+ try {
857
+ const output = execSync("claude mcp list", { encoding: "utf8" });
858
+ if (!output.includes("mergen")) {
859
+ throw new Error("mergen not in claude mcp list");
860
+ }
861
+ return;
862
+ } catch {
863
+ throw new Error("Claude Code not configured");
864
+ }
865
+ default:
866
+ throw new Error(`Unknown IDE: ${ide}`);
867
+ }
868
+ if (!existsSync(configPath)) {
869
+ throw new Error(`Config not found: ${configPath}`);
870
+ }
871
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
872
+ if (!config.mcpServers?.mergen) {
873
+ throw new Error("mergen not in IDE config");
874
+ }
875
+ }
876
+ async function doctorCommand() {
877
+ console.log("\u{1FA7A} Mergen Doctor\n");
878
+ hr();
879
+ const checks = [];
880
+ let passed = 0;
881
+ let warned = 0;
882
+ let failed = 0;
883
+ async function runCheck(name, fn) {
884
+ process.stdout.write(` ${name}... `);
885
+ try {
886
+ const { ok, detail, fix, warn } = await fn();
887
+ if (ok) {
888
+ console.log(`\u2713 ${detail}`);
889
+ passed++;
890
+ } else if (warn) {
891
+ console.log(`\u26A0 ${detail}`);
892
+ if (fix) console.log(` \u2192 ${fix}`);
893
+ warned++;
894
+ } else {
895
+ console.log(`\u2717 ${detail}`);
896
+ if (fix) console.log(` \u2192 ${fix}`);
897
+ failed++;
898
+ }
899
+ } catch (err) {
900
+ const msg = err instanceof Error ? err.message : String(err);
901
+ console.log(`\u2717 ${msg}`);
902
+ failed++;
903
+ }
904
+ }
905
+ await runCheck("Node.js version", async () => {
906
+ const ver = parseInt(process.versions.node.split(".")[0]);
907
+ return ver >= 18 ? { ok: true, detail: `Node.js ${process.versions.node}` } : { ok: false, detail: `Node.js ${process.versions.node} (need \u226518)`, fix: "Install from https://nodejs.org/" };
908
+ });
909
+ await runCheck("Server binary", async () => {
910
+ const p = SERVER_ENTRY;
911
+ return existsSync(p) ? { ok: true, detail: p } : { ok: false, detail: "dist/index.js not found", fix: "cd server && npm run build" };
912
+ });
913
+ await runCheck("Server health", async () => {
914
+ for (let port = 3e3; port <= 3010; port++) {
915
+ try {
916
+ const r = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(1e3) });
917
+ if (r.ok) {
918
+ const d = await r.json();
919
+ return { ok: true, detail: `running on :${port} \u2014 ${d.buffered ?? 0} events buffered, ${d.errors ?? 0} errors` };
920
+ }
921
+ } catch {
922
+ }
923
+ }
924
+ return { ok: false, detail: "server not reachable on :3000\u20133010", fix: "mergen-server start" };
925
+ });
926
+ await runCheck("IDE configuration", async () => {
927
+ const ides = [];
928
+ try {
929
+ execSync("claude mcp list 2>&1", { encoding: "utf8" }).includes("mergen") && ides.push("Claude Code");
930
+ } catch {
931
+ }
932
+ const cursorCfg = resolve(homedir(), ".cursor", "mcp.json");
933
+ if (existsSync(cursorCfg)) {
934
+ try {
935
+ const c = JSON.parse(readFileSync(cursorCfg, "utf8"));
936
+ if (c?.mcpServers?.mergen) ides.push("Cursor");
937
+ } catch {
938
+ }
939
+ }
940
+ const windsurfCfg = resolve(homedir(), ".codeium", "windsurf", "mcp_config.json");
941
+ if (existsSync(windsurfCfg)) {
942
+ try {
943
+ const c = JSON.parse(readFileSync(windsurfCfg, "utf8"));
944
+ if (c?.mcpServers?.mergen) ides.push("Windsurf");
945
+ } catch {
946
+ }
947
+ }
948
+ const wsSettings = resolve(homedir(), ".vscode", "settings.json");
949
+ if (existsSync(wsSettings)) {
950
+ try {
951
+ const c = JSON.parse(readFileSync(wsSettings, "utf8"));
952
+ if (c?.["mcp.servers"]?.mergen) ides.push("VS Code");
953
+ } catch {
954
+ }
955
+ }
956
+ return ides.length > 0 ? { ok: true, detail: ides.join(", ") } : { ok: false, detail: "no IDE configured", fix: "mergen-server setup" };
957
+ });
958
+ await runCheck("Telemetry receiving", async () => {
959
+ for (let port = 3e3; port <= 3010; port++) {
960
+ try {
961
+ const r = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(800) });
962
+ if (!r.ok) continue;
963
+ const d = await r.json();
964
+ const lastMs = d.lastEventAt;
965
+ if (lastMs && Date.now() - lastMs < 24 * 60 * 60 * 1e3) {
966
+ return { ok: true, detail: `${d.buffered ?? 0} events buffered \u2014 last received ${Math.round((Date.now() - lastMs) / 1e3)}s ago` };
967
+ }
968
+ return { ok: false, warn: true, detail: "no telemetry in the last 24h", fix: "point your OTLP exporter at http://127.0.0.1:3000 \u2014 or run: mergen-server demo" };
969
+ } catch {
970
+ }
971
+ }
972
+ return { ok: false, warn: true, detail: "server not reachable \u2014 skipping telemetry check", fix: "start the server first" };
973
+ });
974
+ await runCheck("Slack integration", async () => {
975
+ const token = process.env.MERGEN_SLACK_BOT_TOKEN;
976
+ const channel = process.env.MERGEN_SLACK_CHANNEL;
977
+ if (!token) return { ok: false, warn: true, detail: "MERGEN_SLACK_BOT_TOKEN not set", fix: "set MERGEN_SLACK_BOT_TOKEN=xoxb-... for autonomous incident thread replies" };
978
+ if (!channel) return { ok: false, warn: true, detail: "MERGEN_SLACK_CHANNEL not set", fix: "set MERGEN_SLACK_CHANNEL=#incidents" };
979
+ try {
980
+ const r = await fetch("https://slack.com/api/auth.test", {
981
+ method: "POST",
982
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
983
+ signal: AbortSignal.timeout(3e3)
984
+ });
985
+ const d = await r.json();
986
+ return d.ok ? { ok: true, detail: `connected \u2014 workspace: ${d.team ?? "unknown"}, channel: ${channel}` } : { ok: false, warn: true, detail: `Slack auth failed: ${d.error}`, fix: "check MERGEN_SLACK_BOT_TOKEN \u2014 needs chat:write scope" };
987
+ } catch {
988
+ return { ok: false, warn: true, detail: "could not reach Slack API", fix: "check network connectivity" };
989
+ }
990
+ });
991
+ await runCheck("PagerDuty webhook", async () => {
992
+ for (let port = 3e3; port <= 3010; port++) {
993
+ try {
994
+ const r = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(800) });
995
+ if (r.ok) {
996
+ return { ok: true, warn: true, detail: `webhook URL: http://your-server:${port}/webhooks/pagerduty`, fix: "copy this URL into PagerDuty: Services \u2192 Integrations \u2192 Webhooks" };
997
+ }
998
+ } catch {
999
+ }
1000
+ }
1001
+ return { ok: false, warn: true, detail: "server not reachable", fix: "start the server first" };
1002
+ });
1003
+ await runCheck("Autopilot", async () => {
1004
+ const enabled = process.env.MERGEN_AUTOPILOT === "true";
1005
+ return enabled ? { ok: true, detail: "MERGEN_AUTOPILOT=true \u2014 autonomous execution enabled at \u226585% confidence" } : { ok: false, warn: true, detail: "MERGEN_AUTOPILOT not set \u2014 diagnosis-only mode", fix: "set MERGEN_AUTOPILOT=true to enable autonomous fix execution" };
1006
+ });
1007
+ console.log("\n\u2500\u2500 Optional integrations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1008
+ await runCheck("GitHub token", async () => {
1009
+ const token = process.env.GITHUB_TOKEN;
1010
+ if (!token) return { ok: false, warn: true, detail: "GITHUB_TOKEN not set", fix: "export GITHUB_TOKEN=ghp_... # required for PR commenting and backfill" };
1011
+ try {
1012
+ const r = await fetch("https://api.github.com/user", {
1013
+ headers: { Authorization: `Bearer ${token}`, "User-Agent": "mergen-doctor" },
1014
+ signal: AbortSignal.timeout(3e3)
1015
+ });
1016
+ const d = await r.json();
1017
+ return r.ok ? { ok: true, detail: `connected \u2014 user: ${d.login ?? "unknown"}` } : { ok: false, warn: true, detail: `GitHub auth failed: ${d.message ?? r.status}`, fix: "check GITHUB_TOKEN at https://github.com/settings/tokens" };
1018
+ } catch {
1019
+ return { ok: false, warn: true, detail: "could not reach GitHub API", fix: "check network connectivity" };
1020
+ }
1021
+ });
1022
+ await runCheck("GitHub webhook secret", async () => {
1023
+ const secret = process.env.GITHUB_WEBHOOK_SECRET;
1024
+ if (!secret) return { ok: false, warn: true, detail: "GITHUB_WEBHOOK_SECRET not set \u2014 PR/push events unauthenticated", fix: "run: mergen-server connect github --repo <owner/repo>" };
1025
+ return { ok: true, detail: "GITHUB_WEBHOOK_SECRET set \u2014 signature verification enabled" };
1026
+ });
1027
+ await runCheck("Datadog", async () => {
1028
+ const apiKey = process.env.DD_API_KEY;
1029
+ const appKey = process.env.DD_APP_KEY;
1030
+ if (!apiKey) return { ok: false, warn: true, detail: "DD_API_KEY not set", fix: "export DD_API_KEY=... # from https://app.datadoghq.com/organization-settings/api-keys" };
1031
+ if (!appKey) return { ok: false, warn: true, detail: "DD_APP_KEY not set", fix: "export DD_APP_KEY=... # from https://app.datadoghq.com/organization-settings/application-keys" };
1032
+ const site = process.env.DATADOG_SITE ?? "datadoghq.com";
1033
+ try {
1034
+ const r = await fetch(`https://api.${site}/api/v1/validate`, {
1035
+ headers: { "DD-API-KEY": apiKey, "DD-APPLICATION-KEY": appKey },
1036
+ signal: AbortSignal.timeout(4e3)
1037
+ });
1038
+ return r.ok ? { ok: true, detail: `connected \u2014 site: ${site}` } : { ok: false, warn: true, detail: `Datadog auth failed (${r.status})`, fix: "check DD_API_KEY and DD_APP_KEY \u2014 run: mergen-server init" };
1039
+ } catch {
1040
+ return { ok: false, warn: true, detail: `could not reach api.${site}`, fix: "check network connectivity" };
1041
+ }
1042
+ });
1043
+ await runCheck("Linear", async () => {
1044
+ const apiKey = process.env.LINEAR_API_KEY;
1045
+ const teamId = process.env.LINEAR_TEAM_ID;
1046
+ if (!apiKey) return { ok: false, warn: true, detail: "LINEAR_API_KEY not set", fix: "export LINEAR_API_KEY=lin_api_... # from https://linear.app/settings/api" };
1047
+ if (!teamId) return { ok: false, warn: true, detail: "LINEAR_TEAM_ID not set", fix: "export LINEAR_TEAM_ID=<team-id> # from https://linear.app/settings/api" };
1048
+ return { ok: true, detail: `LINEAR_API_KEY set, team: ${teamId}` };
1049
+ });
1050
+ await runCheck("Jira", async () => {
1051
+ const baseUrl = process.env.JIRA_BASE_URL;
1052
+ const email = process.env.JIRA_EMAIL;
1053
+ const token = process.env.JIRA_API_TOKEN;
1054
+ const projKey = process.env.JIRA_PROJECT_KEY;
1055
+ if (!baseUrl) return { ok: false, warn: true, detail: "JIRA_BASE_URL not set", fix: "export JIRA_BASE_URL=https://yourco.atlassian.net" };
1056
+ if (!email) return { ok: false, warn: true, detail: "JIRA_EMAIL not set", fix: "export JIRA_EMAIL=you@company.com" };
1057
+ if (!token) return { ok: false, warn: true, detail: "JIRA_API_TOKEN not set", fix: "create a token at https://id.atlassian.com/manage-profile/security/api-tokens" };
1058
+ return projKey ? { ok: true, detail: `${baseUrl} \xB7 project: ${projKey}` } : { ok: false, warn: true, detail: "JIRA_PROJECT_KEY not set", fix: "export JIRA_PROJECT_KEY=ENG # default project; can be overridden per request" };
1059
+ });
1060
+ await runCheck("Sentry webhook", async () => {
1061
+ const secret = process.env.MERGEN_SENTRY_SECRET;
1062
+ if (!secret) return { ok: false, warn: true, detail: "MERGEN_SENTRY_SECRET not set \u2014 Sentry webhooks unauthenticated", fix: "export MERGEN_SENTRY_SECRET=... # from your Sentry webhook config" };
1063
+ return { ok: true, detail: "MERGEN_SENTRY_SECRET set \u2014 signature verification enabled" };
1064
+ });
1065
+ await runCheck("Execution sandbox", async () => {
1066
+ const backend = process.env.MERGEN_SANDBOX;
1067
+ if (backend !== "docker" && backend !== "seatbelt") {
1068
+ const hint = process.platform === "darwin" ? "export MERGEN_SANDBOX=seatbelt # native macOS sandbox, near-zero overhead" : "export MERGEN_SANDBOX=docker # or MERGEN_EBPF_VERIFY=true on Linux";
1069
+ return { ok: false, warn: true, detail: "MERGEN_SANDBOX not set \u2014 mergen-server exec runs unsandboxed on the host", fix: hint };
1070
+ }
1071
+ const { isDockerAvailable, isDockerRuntimeAvailable, isMacSandboxAvailable } = await import("../intelligence/sandbox.js");
1072
+ if (backend === "docker") {
1073
+ const available2 = await isDockerAvailable();
1074
+ if (!available2) return { ok: false, warn: true, detail: "MERGEN_SANDBOX=docker but Docker is not reachable \u2014 exec falls back to unsandboxed", fix: "start Docker Desktop" };
1075
+ const runtime = process.env.MERGEN_SANDBOX_DOCKER_RUNTIME;
1076
+ if (runtime) {
1077
+ const runtimeOk = await isDockerRuntimeAvailable(runtime);
1078
+ return runtimeOk ? { ok: true, detail: `MERGEN_SANDBOX=docker \u2014 daemon reachable, runtime '${runtime}' registered` } : { ok: false, warn: true, detail: `MERGEN_SANDBOX_DOCKER_RUNTIME=${runtime} not registered with this daemon \u2014 falling back to runc`, fix: `install ${runtime} and register it as a Docker runtime` };
1079
+ }
1080
+ return { ok: true, detail: "MERGEN_SANDBOX=docker \u2014 daemon reachable (default runc runtime)" };
1081
+ }
1082
+ if (process.platform !== "darwin") {
1083
+ return { ok: false, warn: true, detail: "MERGEN_SANDBOX=seatbelt set but this is not macOS \u2014 exec falls back to unsandboxed", fix: "unset MERGEN_SANDBOX or set MERGEN_SANDBOX=docker" };
1084
+ }
1085
+ const available = await isMacSandboxAvailable();
1086
+ return available ? { ok: true, detail: "MERGEN_SANDBOX=seatbelt \u2014 sandbox-exec available" } : { ok: false, warn: true, detail: "MERGEN_SANDBOX=seatbelt but sandbox-exec is not usable on this machine \u2014 exec falls back to unsandboxed", fix: "check that /usr/bin/sandbox-exec exists and is not blocked by MDM policy" };
1087
+ });
1088
+ await runCheck("Redis persistence", async () => {
1089
+ const url = process.env.MERGEN_REDIS_URL;
1090
+ if (!url) return { ok: false, warn: true, detail: "MERGEN_REDIS_URL not set \u2014 ring buffer lost on restart", fix: "export MERGEN_REDIS_URL=redis://localhost:6379 # optional but recommended for production" };
1091
+ return { ok: true, detail: `MERGEN_REDIS_URL set \u2014 buffer persisted at ${url}` };
1092
+ });
1093
+ await runCheck("Docker containers", async () => {
1094
+ try {
1095
+ const { execSync: exec2 } = await import("child_process");
1096
+ const raw = exec2('docker ps --format "{{.Names}}" 2>/dev/null', { encoding: "utf8", timeout: 3e3 });
1097
+ const names = raw.split("\n").map((s) => s.trim()).filter(Boolean);
1098
+ if (names.length === 0) {
1099
+ return { ok: false, warn: true, detail: "Docker running but no containers found", fix: "Start your app containers, then stream logs: curl -X POST http://127.0.0.1:3000/watchers/docker" };
1100
+ }
1101
+ const label = names.slice(0, 3).join(", ") + (names.length > 3 ? ` +${names.length - 3} more` : "");
1102
+ return {
1103
+ ok: false,
1104
+ warn: true,
1105
+ detail: `${names.length} container(s) running (${label}) \u2014 not yet streaming to Mergen`,
1106
+ fix: `Activate log streaming: curl -X POST http://127.0.0.1:3000/watchers/docker`
1107
+ };
1108
+ } catch {
1109
+ return { ok: false, warn: true, detail: "Docker not detected (not installed or not running)", fix: "Install Docker if you use containers \u2014 streaming logs gives Mergen live backend context" };
1110
+ }
1111
+ });
1112
+ hr();
1113
+ const total = passed + warned + failed;
1114
+ if (failed === 0 && warned === 0) {
1115
+ success(`All ${total} checks passed \u2014 Mergen is healthy \u2728`);
1116
+ } else if (failed === 0) {
1117
+ console.log(`
1118
+ ${passed}/${total} passed, ${warned} warning(s) \u2014 Mergen is functional`);
1119
+ } else {
1120
+ console.log(`
1121
+ ${passed}/${total} passed, ${failed} failure(s)`);
1122
+ console.log("\nRun: mergen-server setup to reconfigure");
1123
+ process.exit(1);
1124
+ }
1125
+ }
1126
+ async function quickstartCommand() {
1127
+ console.log("\n\u2B21 Mergen \u2014 Quick Start\n");
1128
+ hr();
1129
+ log("From zero to your first real insight. Under 2 minutes.");
1130
+ console.log("");
1131
+ const cmd = (await ask("What command starts your app? (e.g. npm start, python app.py): ")).trim();
1132
+ if (!cmd) {
1133
+ error("No command provided.");
1134
+ process.exit(1);
1135
+ }
1136
+ const mergenHost = process.env.MERGEN_HOST ?? "127.0.0.1";
1137
+ let serverPort = 3e3;
1138
+ let serverFound = false;
1139
+ for (let p = 3e3; p <= 3010; p++) {
1140
+ try {
1141
+ const r = await fetch(`http://${mergenHost}:${p}/health`, { signal: AbortSignal.timeout(500) });
1142
+ if (r.ok) {
1143
+ serverPort = p;
1144
+ serverFound = true;
1145
+ break;
1146
+ }
1147
+ } catch {
1148
+ }
1149
+ }
1150
+ if (!serverFound) {
1151
+ const serverPath = SERVER_ENTRY;
1152
+ if (existsSync(serverPath)) {
1153
+ process.stdout.write("\nStarting Mergen server...");
1154
+ const { spawn: spawnSrv } = await import("child_process");
1155
+ const srv = spawnSrv("node", [serverPath], {
1156
+ stdio: "ignore",
1157
+ detached: true,
1158
+ env: { ...process.env, NODE_ENV: "production" }
1159
+ });
1160
+ srv.unref();
1161
+ for (let attempt = 0; attempt < 16 && !serverFound; attempt++) {
1162
+ await sleep(500);
1163
+ for (let p = 3e3; p <= 3010; p++) {
1164
+ try {
1165
+ const r = await fetch(`http://${mergenHost}:${p}/health`, { signal: AbortSignal.timeout(300) });
1166
+ if (r.ok) {
1167
+ serverPort = p;
1168
+ serverFound = true;
1169
+ break;
1170
+ }
1171
+ } catch {
1172
+ }
1173
+ }
1174
+ }
1175
+ console.log(serverFound ? ` \u2713 :${serverPort}` : " \u2717");
1176
+ }
1177
+ } else {
1178
+ success(`Server already running on :${serverPort}`);
1179
+ }
1180
+ const ide = await detectIDE().catch(() => null);
1181
+ if (ide) {
1182
+ console.log("");
1183
+ const doIde = await ask(`Add Mergen to ${ide}? Your AI IDE will see live errors without copy-paste. (y/n): `);
1184
+ if (doIde.toLowerCase() === "y") {
1185
+ await configureIDE(ide).catch(() => {
1186
+ });
1187
+ success(`${ide} configured \u2014 restart your IDE for the MCP tools to appear`);
1188
+ }
1189
+ }
1190
+ hr();
1191
+ console.log(`
1192
+ Starting: ${cmd}`);
1193
+ const div = "\u2500".repeat(58);
1194
+ console.log(div);
1195
+ log("Mergen is watching. Trigger an error in your app.");
1196
+ log("When it appears, Mergen will analyze it inline \u2014 no IDE needed.");
1197
+ log("Press Ctrl+C when done.\n");
1198
+ const parts = cmd.split(/\s+/);
1199
+ await watchCommand(["watch", ...parts]);
1200
+ }
1201
+ export {
1202
+ checkBinary,
1203
+ checkHealth,
1204
+ checkIDEConfig,
1205
+ checkIngest,
1206
+ checkServerStarts,
1207
+ ciCommand,
1208
+ configureIDE,
1209
+ detectIDE,
1210
+ doctorCommand,
1211
+ installClaudeCodePreToolUseHook,
1212
+ loginCommand,
1213
+ quickstartCommand,
1214
+ seedBuiltinRunbooks,
1215
+ setupCommand,
1216
+ startCommand,
1217
+ testCommand
1218
+ };