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,857 @@
1
+ import { z } from "zod";
2
+ import { store } from "../sensor/buffer.js";
3
+ import { truncateToTokenBudget } from "./token-budget.js";
4
+ import { consumeCredit, getUsageSnapshot } from "./usage.js";
5
+ import { buildCausalChain } from "./causal.js";
6
+ import { buildCausalGraph } from "./causal-graph.js";
7
+ import { computeErrorFrequency, computeNetworkFrequency } from "./error-fingerprint.js";
8
+ import { computeAnomaly, getAnomalousPatterns } from "./baseline.js";
9
+ import { generateReproSteps } from "./repro-steps.js";
10
+ import { trackCall, getLastClearAt, setFirstAnalyzeAt, setLastTimeToFirstAnalysisMs } from "./tools-state.js";
11
+ import { startSession } from "./session-metrics.js";
12
+ import { getStatsForTag } from "./calibration.js";
13
+ import logger from "../sensor/logger.js";
14
+ const PLAN_TIER_DESCRIPTION = "Free plan: 10 analyze credits/month. Starter ($499/mo): 100 credits. Team ($2,500/mo): 1,000 credits. See https://mergen.app/pricing for details.";
15
+ function registerAnalyzeRuntime(server) {
16
+ _registerAnalyzeRuntime(server);
17
+ }
18
+ function _registerAnalyzeRuntime(server) {
19
+ server.registerTool(
20
+ "reconstruct_context",
21
+ {
22
+ description: "\u{1F52C} EXECUTION HISTORY \u2014 Reconstructs what happened in the runtime before a failure. Resolves stack frames to original source (with code snippets), tracks event dependencies (request \u2192 response \u2192 state mutation \u2192 crash), flags AI-generated commits in the blast radius, and produces a structured diagnosis with root-cause summary, causal path, and fix hint. Use whenever the user asks why something broke, what changed before an error, or needs context about an AI-written service they did not author. " + PLAN_TIER_DESCRIPTION,
23
+ inputSchema: {
24
+ focus: z.enum(["errors", "network", "all"]).optional().describe("Limit analysis scope (default: all)"),
25
+ since: z.number().int().optional().describe("Only analyze events after this Unix timestamp in ms"),
26
+ max_tokens: z.number().int().min(100).max(1e4).optional().describe("Soft token limit for response. Will truncate if exceeded.")
27
+ }
28
+ },
29
+ async ({ focus = "all", since, max_tokens }) => {
30
+ trackCall("reconstruct_context");
31
+ setLastTimeToFirstAnalysisMs(Date.now() - getLastClearAt());
32
+ const logs = focus === "network" ? [] : store.getLogs(200, void 0, since);
33
+ const network = focus === "errors" ? [] : store.getNetwork(200, void 0, since);
34
+ const contexts = store.getContext(20, since);
35
+ const terminal = store.getTerminalOutput(100, void 0, since);
36
+ const processExits = store.getProcessExits(20, void 0, since);
37
+ const ciEvents = store.getCIEvents(20, void 0, since);
38
+ const deployments = store.getDeployments(10, void 0, since);
39
+ let causal;
40
+ try {
41
+ causal = await Promise.race([
42
+ buildCausalChain(logs, network, contexts, since, terminal, processExits, ciEvents, deployments),
43
+ new Promise(
44
+ (_, reject) => setTimeout(() => reject(new Error("analysis timeout")), 3e4)
45
+ )
46
+ ]);
47
+ } catch (err) {
48
+ logger.warn({ err }, "reconstruct_context: causal analysis failed \u2014 returning raw telemetry");
49
+ const errorCount = logs.filter((e) => e.level === "error").length;
50
+ const netErrors = network.filter((n) => n.status >= 400 || !!n.error);
51
+ const topErrors = logs.filter((e) => e.level === "error").slice(0, 5);
52
+ return {
53
+ content: [{
54
+ type: "text",
55
+ text: [
56
+ `\u26A1 **Raw Telemetry Snapshot** (analysis unavailable \u2014 ${err instanceof Error ? err.message : "unknown error"})`,
57
+ ``,
58
+ `**${errorCount} console errors, ${netErrors.length} network failures** in window`,
59
+ topErrors.length > 0 ? `**Top errors:**
60
+ ${topErrors.map((e) => `- ${String(e.args?.[0] ?? "").slice(0, 120)}`).join("\n")}` : "",
61
+ netErrors.length > 0 ? `**Network failures:**
62
+ ${netErrors.slice(0, 5).map((n) => `- ${n.method} ${n.url} \u2192 ${n.status}`).join("\n")}` : "",
63
+ ``,
64
+ `_Manual investigation required. Retry \`reconstruct_context\` if the issue persists._`
65
+ ].filter(Boolean).join("\n")
66
+ }],
67
+ isError: true
68
+ };
69
+ }
70
+ const credit = await consumeCredit();
71
+ if (!credit.allowed) {
72
+ return {
73
+ content: [{
74
+ type: "text",
75
+ text: [
76
+ `\u26D4 Monthly limit reached on the **Free** plan.`,
77
+ ``,
78
+ `**Upgrade** at https://mergen.app/pricing for more credits.`,
79
+ ``,
80
+ `**Continue debugging with free tools:**`,
81
+ `1. \`get_incident_context\` \u2014 fetch active Datadog incident context (free)`,
82
+ `2. \`triage_incident\` \u2014 full causal analysis without credit cost`,
83
+ ``,
84
+ `Call \`triage_incident\` to continue debugging.`
85
+ ].join("\n")
86
+ }],
87
+ isError: true
88
+ };
89
+ }
90
+ setFirstAnalyzeAt(Date.now());
91
+ for (const h of causal.hypotheses) {
92
+ if (h.pid) startSession(h.pid, h.tag);
93
+ }
94
+ const usage = getUsageSnapshot();
95
+ const usageFooter = usage.included === null ? `
96
+
97
+ ---
98
+ *Credits used this month: ${usage.used} (unlimited plan)*` : `
99
+
100
+ ---
101
+ *Credits: ${usage.used} / ${usage.included} used` + (usage.overage > 0 ? ` \xB7 ${usage.overage} overage ($${(usage.estimatedOverageCents / 100).toFixed(2)} est.)` : "") + ` \xB7 resets ${new Date(usage.resetsAt).toUTCString()}*`;
102
+ const noticeBlock = credit.notice ? `
103
+
104
+ > ${credit.notice}` : "";
105
+ let hypothesisSection = "";
106
+ if (causal.hypotheses.length > 0) {
107
+ const top = causal.hypotheses[0];
108
+ const pct = Math.round((top.confidenceScore ?? 0) * 100);
109
+ const totalEvents = logs.length + network.length;
110
+ const isThin = totalEvents < 5;
111
+ const isLowConf = (top.confidenceScore ?? 1) < 0.6 || top.confidence === "LOW";
112
+ const h = [""];
113
+ if (isThin || isLowConf) {
114
+ const why = isThin ? `only ${totalEvents} event(s) in buffer \u2014 reproduce the error for a stronger signal` : `${pct}% confidence \u2014 more context needed`;
115
+ h.push(`> \u26A0\uFE0F **Tentative diagnosis** (${why}). Treat as a starting hypothesis, not a verdict.`, "");
116
+ }
117
+ const calStats = getStatsForTag(top.tag);
118
+ const calNote = calStats?.isEmpirical ? `_Confidence calibrated from ${calStats.verdicts} verdict(s) on this system \u2014 empirical._` : `_Confidence is estimated (no local verdicts yet). Call \`validate_fix\` after applying a fix to calibrate._`;
119
+ h.push(
120
+ `## Root Cause \u2014 ${top.confidence} (${pct}%)`,
121
+ calNote,
122
+ "",
123
+ top.summary,
124
+ ""
125
+ );
126
+ if (top.causalPath?.length) {
127
+ h.push("**Causal chain:**");
128
+ top.causalPath.forEach((step, i) => h.push(`${i + 1}. ${step}`));
129
+ h.push("");
130
+ }
131
+ if (top.evidence?.length) {
132
+ h.push("**Evidence:**");
133
+ top.evidence.slice(0, 3).forEach((e) => h.push(`- ${e}`));
134
+ h.push("");
135
+ }
136
+ if (top.fixHint) {
137
+ h.push(`**Fix:** ${top.fixHint}`);
138
+ h.push("");
139
+ }
140
+ if (causal.hypotheses.length > 1) {
141
+ h.push(`_${causal.hypotheses.length - 1} alternative hypothesis(es) considered and ranked lower._`);
142
+ h.push("");
143
+ }
144
+ try {
145
+ const { postmortemStore } = await import("./postmortem-store.js");
146
+ const prior = postmortemStore.getByTag(top.tag, 5).filter((pm) => pm.generatedAt < Date.now() - 6e4);
147
+ if (prior.length > 0) {
148
+ const latest = prior[0];
149
+ const daysAgo = Math.round((Date.now() - latest.generatedAt) / 864e5);
150
+ const ageStr = daysAgo === 0 ? "today" : `${daysAgo}d ago`;
151
+ const mttrStr = latest.mttrMs ? `, resolved in ${Math.round(latest.mttrMs / 6e4)}m` : "";
152
+ h.push(
153
+ `> \u{1F501} **Seen before** \u2014 ${prior.length} prior incident(s) with this pattern (most recent: ${ageStr}${mttrStr}). Call \`get_incident_history\` for past fixes.`,
154
+ ""
155
+ );
156
+ }
157
+ } catch {
158
+ }
159
+ hypothesisSection = h.join("\n");
160
+ }
161
+ const fullText = causal.contextPack + hypothesisSection + noticeBlock + usageFooter;
162
+ const { result, truncated, omitted, estimatedTokens } = truncateToTokenBudget(fullText.split("\n"), max_tokens, "\n");
163
+ if (truncated) logger.info({ tool: "reconstruct_context", omitted, estimatedTokens }, "response truncated");
164
+ return { content: [{ type: "text", text: result }] };
165
+ }
166
+ );
167
+ }
168
+ function registerAnalysisTools(server) {
169
+ server.registerTool(
170
+ "quick_check",
171
+ {
172
+ description: "\u26A1 FREE \xB7 No credit cost. Instant buffer pulse \u2014 use this constantly during development, not just when things break. Returns: error/warning/network counts, and any detected patterns (repeated failures, warning spikes, slow requests). Call this before writing code, after running the app, or whenever something feels off. For the root cause and a code fix, call reconstruct_context."
173
+ },
174
+ async () => {
175
+ trackCall("quick_check");
176
+ const errors = store.getLogs(200, "error");
177
+ const warns = store.getLogs(200, "warn");
178
+ const network = store.getNetwork(200);
179
+ const signals = store.getSignals();
180
+ const netFails = network.filter((n) => n.status >= 400 || n.status === 0 || n.error);
181
+ const lines = ["## \u26A1 Quick Check", ""];
182
+ const errLabel = errors.length === 0 ? "\u2705 0" : `\u274C ${errors.length}`;
183
+ const warnLabel = warns.length === 0 ? "\u2705 0" : `\u26A0\uFE0F ${warns.length}`;
184
+ const netLabel = netFails.length === 0 ? "\u2705 0" : `\u274C ${netFails.length}`;
185
+ lines.push("| | Count |");
186
+ lines.push("|---|---|");
187
+ lines.push(`| Console errors | ${errLabel} |`);
188
+ lines.push(`| Warnings | ${warnLabel} |`);
189
+ lines.push(`| Network failures | ${netLabel} |`);
190
+ lines.push(`| Buffer total | ${store.size()} |`);
191
+ if (errors.length > 0) {
192
+ const oldest = errors.reduce((a, b) => a.timestamp < b.timestamp ? a : b);
193
+ const newest = errors.reduce((a, b) => a.timestamp > b.timestamp ? a : b);
194
+ const fmt = (ms) => {
195
+ const s = Math.round(ms / 1e3);
196
+ if (s < 60) return `${s}s ago`;
197
+ if (s < 3600) return `${Math.round(s / 60)}m ago`;
198
+ return `${Math.round(s / 3600)}h ago`;
199
+ };
200
+ const oldestStr = fmt(Date.now() - oldest.timestamp);
201
+ const newestStr = fmt(Date.now() - newest.timestamp);
202
+ const timing = oldest.timestamp === newest.timestamp ? `first (and only) occurrence ${oldestStr}` : `first ${oldestStr} \xB7 most recent ${newestStr}`;
203
+ lines.push(`| Error window | ${timing} |`);
204
+ }
205
+ if (signals.length > 0) {
206
+ lines.push("", "### \u{1F50D} Detected patterns", "");
207
+ for (const s of signals) {
208
+ const confPct = Math.round(s.confidence * 100);
209
+ lines.push(`**${confPct}%** \u2014 ${s.message}`);
210
+ lines.push(` \u2192 **Next step:** ${s.action}`);
211
+ lines.push("");
212
+ }
213
+ lines.push("> \u{1F52C} **Root cause + fix:** call `reconstruct_context`.");
214
+ } else if (errors.length > 0) {
215
+ lines.push("", `> \u274C ${errors.length} error(s) in buffer. Call \`reconstruct_context\` for root cause + fix.`);
216
+ } else if (warns.length > 0) {
217
+ lines.push("", `> \u26A0\uFE0F ${warns.length} warning(s) in buffer. Call \`explain_warning\` to understand them before they escalate.`);
218
+ } else {
219
+ lines.push("", "> \u2705 Buffer clean \u2014 no errors, warning spikes, or repeated failures.");
220
+ }
221
+ return { content: [{ type: "text", text: lines.join("\n") }] };
222
+ }
223
+ );
224
+ server.registerTool(
225
+ "explain_warning",
226
+ {
227
+ description: "\u26A1 FREE \xB7 No credit cost. Explains the most recent console warning \u2014 what it means, why it might cause a crash later, and what to do about it. Call this proactively whenever quick_check shows warnings, without waiting for an error \u2014 warnings are cheaper to fix before they cascade. No credit cost.",
228
+ inputSchema: {
229
+ since: z.number().int().optional().describe("Only look at warnings after this Unix timestamp in ms")
230
+ }
231
+ },
232
+ async ({ since }) => {
233
+ trackCall("explain_warning");
234
+ const warns = store.getLogs(200, "warn", since);
235
+ if (warns.length === 0) {
236
+ return { content: [{ type: "text", text: "\u2705 No warnings in buffer." }] };
237
+ }
238
+ const latest = warns[warns.length - 1];
239
+ const message = latest.args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ").slice(0, 500);
240
+ const windowMs = 5e3;
241
+ const nearbyNetwork = store.getNetwork(200).filter(
242
+ (n) => Math.abs(n.timestamp - latest.timestamp) < windowMs
243
+ );
244
+ const nearbyErrors = store.getLogs(200, "error").filter(
245
+ (e) => e.timestamp > latest.timestamp
246
+ );
247
+ const lines = [
248
+ "## \u26A0\uFE0F Warning Explanation",
249
+ "",
250
+ `**Message:** \`${message}\``,
251
+ `**When:** ${new Date(latest.timestamp).toISOString()}`,
252
+ `**URL:** ${latest.url}`
253
+ ];
254
+ if (latest.stack) {
255
+ lines.push("", "<details><summary>\u{1F4CB} Stack trace</summary>", "", "```", latest.stack.slice(0, 1e3), "```", "</details>");
256
+ }
257
+ if (nearbyNetwork.length > 0) {
258
+ lines.push("", "**Nearby network activity (\xB15s):**");
259
+ for (const n of nearbyNetwork.slice(0, 3)) {
260
+ const badge = n.status >= 400 || n.status === 0 ? "\u274C" : "\u2705";
261
+ lines.push(`- ${badge} \`${n.method} ${n.url}\` \u2192 ${n.status} (${n.duration}ms)`);
262
+ }
263
+ }
264
+ if (nearbyErrors.length > 0) {
265
+ lines.push("", `> \u26A0\uFE0F **${nearbyErrors.length} error(s) fired AFTER this warning** \u2014 this warning may have been a precursor.`);
266
+ lines.push("> Call **`reconstruct_context`** for a full causal chain.");
267
+ }
268
+ if (warns.length > 1) {
269
+ lines.push("", `*${warns.length - 1} other warning(s) in buffer \u2014 showing most recent only.*`);
270
+ }
271
+ lines.push("", "---", "**Your task:** Explain what this warning means, why it could cause a crash, and the minimal fix.");
272
+ return { content: [{ type: "text", text: lines.join("\n") }] };
273
+ }
274
+ );
275
+ server.registerTool(
276
+ "session_summary",
277
+ {
278
+ description: '\u26A1 FREE \xB7 No credit cost. Summarises everything that happened in the current buffer: total errors, repeated failures, warning patterns, slow endpoints, and the top signals. Use this at the end of a debug session, when picking up work after a break, or to get a "what has been happening?" overview without running a full analysis. No credit cost.',
279
+ inputSchema: {
280
+ since: z.number().int().optional().describe("Summarise only events after this Unix timestamp in ms")
281
+ }
282
+ },
283
+ async ({ since }) => {
284
+ trackCall("session_summary");
285
+ const logs = store.getLogs(200, void 0, since);
286
+ const network = store.getNetwork(200, void 0, since);
287
+ const signals = store.getSignals();
288
+ const errors = logs.filter((e) => e.level === "error");
289
+ const warns = logs.filter((e) => e.level === "warn");
290
+ const netFails = network.filter((n) => n.status >= 400 || n.status === 0 || n.error);
291
+ const slowReqs = network.filter((n) => n.duration > 2e3);
292
+ const lines = ["## \u{1F4CA} Session Summary", ""];
293
+ lines.push("| Metric | Value |", "|---|---|");
294
+ lines.push(`| Console errors | ${errors.length} |`);
295
+ lines.push(`| Warnings | ${warns.length} |`);
296
+ lines.push(`| Network failures | ${netFails.length} |`);
297
+ lines.push(`| Slow requests (>2s)| ${slowReqs.length} |`);
298
+ lines.push(`| Total events | ${store.size()} |`, "");
299
+ if (errors.length > 0) {
300
+ const seen = /* @__PURE__ */ new Map();
301
+ for (const e of errors) {
302
+ const msg = e.args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ").split("\n")[0].slice(0, 100);
303
+ seen.set(msg, (seen.get(msg) ?? 0) + 1);
304
+ }
305
+ lines.push("### \u274C Errors", "");
306
+ for (const [msg, count] of [...seen.entries()].sort((a, b) => b[1] - a[1]))
307
+ lines.push(`- ${count > 1 ? `**(\xD7${count})**` : ""} \`${msg}\``);
308
+ lines.push("");
309
+ }
310
+ if (netFails.length > 0) {
311
+ lines.push("### \u{1F310} Failing endpoints", "");
312
+ const seen = /* @__PURE__ */ new Map();
313
+ for (const n of netFails) {
314
+ const key = `${n.method} ${n.url}`;
315
+ const prev = seen.get(key);
316
+ seen.set(key, { count: (prev?.count ?? 0) + 1, status: n.status });
317
+ }
318
+ for (const [endpoint, { count, status }] of [...seen.entries()].sort((a, b) => b[1].count - a[1].count))
319
+ lines.push(`- ${count > 1 ? `**(\xD7${count})**` : ""} \`${endpoint}\` \u2192 ${status || "NET_ERR"}`);
320
+ lines.push("");
321
+ }
322
+ if (signals.length > 0) {
323
+ lines.push("### \u{1F50D} Patterns detected", "");
324
+ for (const s of signals) {
325
+ lines.push(`- ${s.message}`);
326
+ lines.push(` \u2192 **${s.action}**`);
327
+ }
328
+ lines.push("");
329
+ }
330
+ if (errors.length > 0 || signals.length > 0) {
331
+ lines.push("> \u{1F4A1} Call **`reconstruct_context`** for root cause analysis and a fix suggestion.");
332
+ } else {
333
+ lines.push("> \u2705 Session looks clean \u2014 no significant errors or patterns detected.");
334
+ }
335
+ return { content: [{ type: "text", text: lines.join("\n") }] };
336
+ }
337
+ );
338
+ server.registerTool(
339
+ "get_system_status",
340
+ {
341
+ description: '\u26A1 FREE \xB7 No credit cost. Zero-parameter snapshot of system health across sessions. Answers "what is happening right now and what has happened recently?" by combining the live buffer (current errors, warnings, network failures) with the persistent incident history (past resolved incidents from SQLite, survives server restarts). Call this when picking up work after a break, or as the first check when something feels off. Use session_summary for a deeper buffer-only view; use get_incident_history for past incidents by fingerprint or service. For billing/plan status use get_status.'
342
+ },
343
+ async () => {
344
+ trackCall("get_status");
345
+ const errors = store.getLogs(200, "error");
346
+ const warns = store.getLogs(200, "warn");
347
+ const network = store.getNetwork(200);
348
+ const netFails = network.filter((n) => n.status >= 400 || n.status === 0 || n.error);
349
+ const signals = store.getSignals();
350
+ const lines = ["## \u2B21 Mergen Status", ""];
351
+ if (errors.length === 0 && warns.length === 0 && netFails.length === 0) {
352
+ lines.push("**Buffer:** \u2705 Clean \u2014 no errors, warnings, or network failures.");
353
+ } else {
354
+ lines.push("**Buffer:**");
355
+ if (errors.length > 0) {
356
+ const oldest = errors.reduce((a, b) => a.timestamp < b.timestamp ? a : b);
357
+ const ageMin = Math.round((Date.now() - oldest.timestamp) / 6e4);
358
+ const ageStr = ageMin < 60 ? `${ageMin}m` : `${Math.round(ageMin / 60)}h`;
359
+ lines.push(`- \u274C ${errors.length} error(s) \u2014 first appeared ${ageStr} ago`);
360
+ }
361
+ if (warns.length > 0) lines.push(`- \u26A0\uFE0F ${warns.length} warning(s)`);
362
+ if (netFails.length > 0) lines.push(`- \u{1F310} ${netFails.length} network failure(s)`);
363
+ }
364
+ if (signals.length > 0) {
365
+ lines.push("");
366
+ lines.push("**Detected patterns:**");
367
+ for (const s of signals.slice(0, 3)) {
368
+ lines.push(`- ${s.message}`);
369
+ }
370
+ lines.push("> Call `reconstruct_context` for root cause + fix.");
371
+ }
372
+ lines.push("");
373
+ try {
374
+ const { postmortemStore } = await import("./postmortem-store.js");
375
+ const recent = postmortemStore.list(5);
376
+ if (recent.length > 0) {
377
+ lines.push("**Recent incidents (persistent, survives restarts):**");
378
+ for (const pm of recent) {
379
+ const daysAgo = Math.round((Date.now() - pm.generatedAt) / 864e5);
380
+ const ageStr = daysAgo === 0 ? "today" : `${daysAgo}d ago`;
381
+ const mttr = pm.mttrMs ? ` \xB7 ${Math.round(pm.mttrMs / 6e4)}m MTTR` : "";
382
+ const how = pm.resolvedAutonomously ? "\u{1F916}" : "\u{1F464}";
383
+ lines.push(`- ${how} **${pm.tag.replace(/^infra_/, "")}** on \`${pm.service}\` \u2014 ${ageStr}${mttr}`);
384
+ }
385
+ lines.push("");
386
+ lines.push("> Call `get_incident_history` with a service name for full history and past fixes.");
387
+ } else {
388
+ lines.push("**Recent incidents:** none recorded yet \u2014 incidents appear here after resolution.");
389
+ }
390
+ } catch {
391
+ lines.push("**Recent incidents:** history store unavailable.");
392
+ }
393
+ return { content: [{ type: "text", text: lines.join("\n") }] };
394
+ }
395
+ );
396
+ server.registerTool(
397
+ "get_env_snapshot",
398
+ {
399
+ description: '\u26A1 FREE \xB7 No credit cost. Captures the current environment variables and compares against the last saved snapshot in ~/.mergen/env-snapshot.json. Surfaces any additions, removals, or value changes since the snapshot was taken. Call this when something broke after a config change, or to correlate "env changed" with the timing of an incident. Automatically redacts secrets (keys matching SECRET, TOKEN, PASSWORD, KEY, CREDENTIAL). Saves a new snapshot on every call.'
400
+ },
401
+ async () => {
402
+ trackCall("get_env_snapshot");
403
+ const { readFileSync, writeFileSync, mkdirSync } = await import("fs");
404
+ const { join } = await import("path");
405
+ const { homedir } = await import("os");
406
+ const REDACT_RE = /secret|token|password|passwd|key|credential|api_key|auth|private/i;
407
+ const sanitise = (k, v) => REDACT_RE.test(k) ? "[REDACTED]" : v.slice(0, 200);
408
+ const currentEnv = {};
409
+ for (const [k, v] of Object.entries(process.env)) {
410
+ if (v !== void 0) currentEnv[k] = sanitise(k, v);
411
+ }
412
+ const snapshotDir = join(homedir(), ".mergen");
413
+ const snapshotPath = join(snapshotDir, "env-snapshot.json");
414
+ let prior = null;
415
+ try {
416
+ prior = JSON.parse(readFileSync(snapshotPath, "utf8"));
417
+ } catch {
418
+ }
419
+ try {
420
+ mkdirSync(snapshotDir, { recursive: true });
421
+ writeFileSync(snapshotPath, JSON.stringify({ ...currentEnv, _savedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2), "utf8");
422
+ } catch {
423
+ }
424
+ const lines = ["## Environment Snapshot", ""];
425
+ if (!prior) {
426
+ lines.push("_First snapshot saved \u2014 no prior baseline to diff against._");
427
+ lines.push(`**Variables captured:** ${Object.keys(currentEnv).length}`);
428
+ lines.push("");
429
+ lines.push("Call this tool again after a config change to see the diff.");
430
+ return { content: [{ type: "text", text: lines.join("\n") }] };
431
+ }
432
+ const added = [];
433
+ const removed = [];
434
+ const changed = [];
435
+ for (const k of Object.keys(currentEnv)) {
436
+ if (k === "_savedAt") continue;
437
+ if (!(k in prior)) {
438
+ added.push(k);
439
+ } else if (prior[k] !== currentEnv[k]) {
440
+ changed.push({ key: k, was: prior[k], now: currentEnv[k] });
441
+ }
442
+ }
443
+ for (const k of Object.keys(prior)) {
444
+ if (k === "_savedAt") continue;
445
+ if (!(k in currentEnv)) removed.push(k);
446
+ }
447
+ const savedAt = prior["_savedAt"] ? ` (snapshot from ${prior["_savedAt"]})` : "";
448
+ lines.push(`**Baseline:**${savedAt}`);
449
+ lines.push("");
450
+ if (added.length === 0 && removed.length === 0 && changed.length === 0) {
451
+ lines.push("\u2705 **No changes** \u2014 environment is identical to the last snapshot.");
452
+ } else {
453
+ if (added.length > 0) {
454
+ lines.push("### Added");
455
+ for (const k of added.slice(0, 10)) lines.push(`- \`${k}\` = ${currentEnv[k]}`);
456
+ if (added.length > 10) lines.push(`_...and ${added.length - 10} more_`);
457
+ lines.push("");
458
+ }
459
+ if (removed.length > 0) {
460
+ lines.push("### Removed");
461
+ for (const k of removed.slice(0, 10)) lines.push(`- \`${k}\``);
462
+ lines.push("");
463
+ }
464
+ if (changed.length > 0) {
465
+ lines.push("### Changed");
466
+ for (const { key, was, now } of changed.slice(0, 10)) {
467
+ lines.push(`- \`${key}\`: \`${was}\` \u2192 \`${now}\``);
468
+ }
469
+ if (changed.length > 10) lines.push(`_...and ${changed.length - 10} more_`);
470
+ lines.push("");
471
+ }
472
+ lines.push("> If any of these changes correlate with when errors started, use `get_regression_start` to confirm timing.");
473
+ }
474
+ return { content: [{ type: "text", text: lines.join("\n") }] };
475
+ }
476
+ );
477
+ server.registerTool(
478
+ "get_diff_from_baseline",
479
+ {
480
+ description: '\u26A1 FREE \xB7 No credit cost. Answers "what changed since it last worked?" by splitting the error history into two windows and showing what is NEW in the recent window. Use this when a user says "this used to work" or "something changed" \u2014 it surfaces new error fingerprints, newly failing endpoints, and the timing of when they appeared. Queries persistent SQLite history so it works across server restarts. Different from reconstruct_context (which identifies root cause) and session_summary (which describes the current buffer) \u2014 this is the delta view.',
481
+ inputSchema: {
482
+ lookback_minutes: z.number().int().min(5).max(1440).optional().describe("How far back to look (default 60). The baseline is the equal-length window before that.")
483
+ }
484
+ },
485
+ async ({ lookback_minutes = 60 }) => {
486
+ trackCall("get_diff_from_baseline");
487
+ const { historyStore } = await import("../sensor/sqlite-store.js");
488
+ const { normaliseMessage } = await import("./error-fingerprint.js");
489
+ const now = Date.now();
490
+ const windowMs = lookback_minutes * 6e4;
491
+ const recentStart = now - windowMs;
492
+ const baseStart = now - windowMs * 2;
493
+ const baseErrors = historyStore.query({ since: baseStart, limit: 5e3, type: "console", level: "error" }).filter((e) => e.timestamp < recentStart);
494
+ const recentErrors = historyStore.query({ since: recentStart, limit: 5e3, type: "console", level: "error" });
495
+ const baseNetwork = store.getNetwork(500, void 0, baseStart).filter((n) => n.timestamp < recentStart && (n.status >= 400 || !!n.error));
496
+ const recentNetwork = store.getNetwork(500, void 0, recentStart).filter((n) => n.status >= 400 || !!n.error);
497
+ const baseFingerprints = new Set(
498
+ baseErrors.map((e) => normaliseMessage(e.args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")))
499
+ );
500
+ const baseEndpoints = new Set(baseNetwork.map((n) => `${n.method} ${n.url}`));
501
+ const newErrors = recentErrors.filter((e) => {
502
+ const fp = normaliseMessage(e.args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" "));
503
+ return !baseFingerprints.has(fp);
504
+ });
505
+ const newEndpoints = recentNetwork.filter((n) => !baseEndpoints.has(`${n.method} ${n.url}`));
506
+ const seen = /* @__PURE__ */ new Set();
507
+ const uniqueNewErrors = [];
508
+ for (const e of newErrors) {
509
+ const msg = e.args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
510
+ const fp = normaliseMessage(msg);
511
+ if (!seen.has(fp)) {
512
+ seen.add(fp);
513
+ const count = newErrors.filter((x) => {
514
+ const xmsg = x.args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
515
+ return normaliseMessage(xmsg) === fp;
516
+ }).length;
517
+ uniqueNewErrors.push({ fp, sample: msg.slice(0, 120), count, firstSeen: e.timestamp });
518
+ }
519
+ }
520
+ const seenEp = /* @__PURE__ */ new Set();
521
+ const uniqueNewEp = [];
522
+ for (const n of newEndpoints) {
523
+ const key = `${n.method} ${n.url}`;
524
+ if (!seenEp.has(key)) {
525
+ seenEp.add(key);
526
+ uniqueNewEp.push({ key, status: n.status, count: newEndpoints.filter((x) => `${x.method} ${x.url}` === key).length });
527
+ }
528
+ }
529
+ const fmtAgo = (ts) => {
530
+ const min = Math.round((now - ts) / 6e4);
531
+ return min < 60 ? `${min}m ago` : `${Math.round(min / 60)}h ago`;
532
+ };
533
+ const lines = [
534
+ `## Diff from Baseline \u2014 last ${lookback_minutes}m vs. ${lookback_minutes}m before that`,
535
+ "",
536
+ `**Baseline:** ${baseErrors.length} error(s), ${baseNetwork.length} network failure(s)`,
537
+ `**Recent:** ${recentErrors.length} error(s), ${recentNetwork.length} network failure(s)`,
538
+ ""
539
+ ];
540
+ if (uniqueNewErrors.length === 0 && uniqueNewEp.length === 0) {
541
+ if (recentErrors.length === 0) {
542
+ lines.push("\u2705 **No change detected** \u2014 no errors in either window.");
543
+ } else {
544
+ lines.push("\u2705 **No new patterns** \u2014 errors present but same fingerprints as baseline (not a regression, likely ongoing).");
545
+ }
546
+ } else {
547
+ if (uniqueNewErrors.length > 0) {
548
+ lines.push("### New error patterns (not in baseline)");
549
+ lines.push("");
550
+ for (const e of uniqueNewErrors.slice(0, 5)) {
551
+ lines.push(`- **\xD7${e.count}** \u2014 "${e.sample}" _(first seen ${fmtAgo(e.firstSeen)})_`);
552
+ }
553
+ lines.push("");
554
+ }
555
+ if (uniqueNewEp.length > 0) {
556
+ lines.push("### Newly failing endpoints");
557
+ lines.push("");
558
+ for (const ep of uniqueNewEp.slice(0, 5)) {
559
+ lines.push(`- **\xD7${ep.count}** \`${ep.key}\` \u2192 ${ep.status || "ERR"}`);
560
+ }
561
+ lines.push("");
562
+ }
563
+ lines.push("> \u{1F52C} Call `reconstruct_context` for root cause + fix hint on these new patterns.");
564
+ lines.push("> Call `get_regression_start` to find the exact timestamp when each pattern first appeared.");
565
+ }
566
+ return { content: [{ type: "text", text: lines.join("\n") }] };
567
+ }
568
+ );
569
+ server.registerTool(
570
+ "get_error_frequency",
571
+ {
572
+ description: 'Returns deduplicated error patterns with occurrence counts, first/last seen times, and whether each pattern is new this session. Use this to distinguish "this error fired 847 times" from "this is the first occurrence." High-count errors are fires; single occurrences are investigations.',
573
+ inputSchema: {
574
+ since: z.number().int().optional().describe("Only count errors after this Unix timestamp in ms"),
575
+ top: z.number().int().min(1).max(50).optional().describe("Return top N patterns by count (default 10)")
576
+ }
577
+ },
578
+ async ({ since, top = 10 }) => {
579
+ trackCall("get_error_frequency");
580
+ const logs = store.getLogs(200, "error", since);
581
+ const network = store.getNetwork(200, void 0, since);
582
+ const errFreq = computeErrorFrequency(logs).slice(0, top);
583
+ const netFreq = computeNetworkFrequency(network).slice(0, top);
584
+ if (errFreq.length === 0 && netFreq.length === 0) {
585
+ return { content: [{ type: "text", text: "No errors in buffer." }] };
586
+ }
587
+ const lines = ["## Error Frequency\n"];
588
+ if (errFreq.length > 0) {
589
+ lines.push("### Console Errors\n");
590
+ for (const e of errFreq) {
591
+ const age = `first: ${new Date(e.firstSeen).toISOString().slice(11, 19)}, last: ${new Date(e.lastSeen).toISOString().slice(11, 19)}`;
592
+ const newBadge = e.isNew ? " \u{1F195}" : "";
593
+ lines.push(`**\xD7${e.count}**${newBadge} \u2014 "${e.sample.slice(0, 100)}"`);
594
+ lines.push(` pattern: \`${e.fingerprint}\` | ${age}`);
595
+ lines.push("");
596
+ }
597
+ }
598
+ if (netFreq.length > 0) {
599
+ lines.push("### Network Failures\n");
600
+ for (const n of netFreq) lines.push(`**\xD7${n.count}** \u2014 ${n.sample}`);
601
+ }
602
+ return { content: [{ type: "text", text: lines.join("\n") }] };
603
+ }
604
+ );
605
+ server.registerTool(
606
+ "get_anomaly_baseline",
607
+ {
608
+ description: "Compares the current error rate to the historical baseline for this time of day and day of week. Returns whether the current rate is anomalous, the normal rate, the multiplier, and a human-readable summary. Requires MERGEN_RETENTION_HOURS \u2265 24 for a meaningful baseline \u2014 with only 1h of data it will report insufficient data.",
609
+ inputSchema: {
610
+ fingerprint: z.string().optional().describe("Specific error pattern to check (from get_error_frequency). Empty = check all errors.")
611
+ }
612
+ },
613
+ async ({ fingerprint = "" }) => {
614
+ trackCall("get_anomaly_baseline");
615
+ const { historyStore } = await import("../sensor/sqlite-store.js");
616
+ const since7d = Date.now() - 7 * 24 * 60 * 60 * 1e3;
617
+ const histEvents = historyStore.query({ since: since7d, limit: 1e4, level: "error", type: "console" });
618
+ const current = store.getLogs(200, "error", Date.now() - 60 * 60 * 1e3);
619
+ if (fingerprint) {
620
+ const result = await computeAnomaly(histEvents, current, fingerprint);
621
+ return { content: [{ type: "text", text: result.summary }] };
622
+ }
623
+ const anomalies = await getAnomalousPatterns(histEvents, current);
624
+ const overall = await computeAnomaly(histEvents, current);
625
+ const lines = ["## Anomaly Baseline\n", overall.summary, ""];
626
+ if (anomalies.length > 0) {
627
+ lines.push(`### Anomalous patterns (${anomalies.length})
628
+ `);
629
+ for (const a of anomalies.slice(0, 5)) {
630
+ const mult = isFinite(a.multiplier) ? `${a.multiplier.toFixed(1)}\xD7` : "new";
631
+ lines.push(`- **${mult} above baseline** \u2014 \`${a.fingerprint}\` (${a.currentCount} now vs ${a.normalRate.toFixed(1)} normal)`);
632
+ }
633
+ }
634
+ return { content: [{ type: "text", text: lines.join("\n") }] };
635
+ }
636
+ );
637
+ server.registerTool(
638
+ "get_regression_start",
639
+ {
640
+ description: 'Finds when an error pattern FIRST appeared and correlates it with the closest deploy or CI event before that timestamp. Call this FIRST when the user says "something broke" or "this stopped working" \u2014 it answers "when did this start and what change introduced it?" without requiring any parameters. Queries full SQLite history across server restarts, so it remembers errors from previous sessions. Works even without deploy/CI integration \u2014 always returns the first-seen timestamp and occurrence count.',
641
+ inputSchema: {
642
+ fingerprint: z.string().optional().describe("Error pattern fingerprint from get_error_frequency. If omitted, uses the most frequent current error.")
643
+ }
644
+ },
645
+ async ({ fingerprint }) => {
646
+ trackCall("get_regression_start");
647
+ const { historyStore } = await import("../sensor/sqlite-store.js");
648
+ const { normaliseMessage } = await import("./error-fingerprint.js");
649
+ const allErrors = historyStore.query({ since: 0, limit: 1e4, type: "console", level: "error" });
650
+ const currentErrors = store.getLogs(200, "error");
651
+ let fp = fingerprint;
652
+ if (!fp && currentErrors.length > 0) {
653
+ const { computeErrorFrequency: cef } = await import("./error-fingerprint.js");
654
+ const top = cef(currentErrors)[0];
655
+ fp = top?.fingerprint ?? "";
656
+ }
657
+ if (!fp) return { content: [{ type: "text", text: "No error patterns found in buffer." }] };
658
+ const matching = allErrors.filter((e) => {
659
+ const msg = e.args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
660
+ return normaliseMessage(msg) === fp;
661
+ }).sort((a, b) => a.timestamp - b.timestamp);
662
+ if (matching.length === 0) return { content: [{ type: "text", text: `No historical data for pattern: \`${fp}\`` }] };
663
+ const first = matching[0];
664
+ const firstIso = new Date(first.timestamp).toISOString();
665
+ const deploys = store.getDeployments(20).filter((d) => d.timestamp <= first.timestamp).sort((a, b) => b.timestamp - a.timestamp);
666
+ const ci = store.getCIEvents(20).filter((c) => c.timestamp <= first.timestamp).sort((a, b) => b.timestamp - a.timestamp);
667
+ const triggerDeploy = deploys[0] ?? null;
668
+ const triggerCI = ci.find((c) => c.status === "failure") ?? ci[0] ?? null;
669
+ const lines = [
670
+ `## Regression Start
671
+ `,
672
+ `**Pattern:** \`${fp}\``,
673
+ `**First seen:** ${firstIso}`,
674
+ `**Total occurrences in history:** ${matching.length}`,
675
+ ""
676
+ ];
677
+ if (triggerDeploy) {
678
+ const msBefore = first.timestamp - triggerDeploy.timestamp;
679
+ lines.push(`**Closest deploy before first occurrence:** \`${triggerDeploy.shortSha ?? triggerDeploy.sha.slice(0, 7)}\` to ${triggerDeploy.environment} (${Math.round(msBefore / 6e4)}m before error)`);
680
+ if (triggerDeploy.actor) lines.push(` Deployed by: ${triggerDeploy.actor}`);
681
+ }
682
+ if (triggerCI) {
683
+ lines.push(`**CI run before first occurrence:** ${triggerCI.job} \u2014 ${triggerCI.status}${triggerCI.failedTests?.length ? ` (${triggerCI.failedTests.length} failing tests)` : ""}`);
684
+ }
685
+ if (!triggerDeploy && !triggerCI) {
686
+ const ageMs = Date.now() - first.timestamp;
687
+ const ageMin = Math.round(ageMs / 6e4);
688
+ const ageStr = ageMin < 60 ? `${ageMin} minutes ago` : `${Math.round(ageMin / 60)} hours ago`;
689
+ lines.push(`**No deploy or CI signal found** \u2014 can't attribute this to a specific change.`);
690
+ lines.push(`What I know: this error first appeared **${ageStr}** and has fired **${matching.length} time(s)** since.`);
691
+ lines.push("");
692
+ lines.push("**To enable deploy correlation (pick one):**");
693
+ lines.push(" `mergen-server watch npm start` \u2014 captures process restarts as deploy events automatically");
694
+ lines.push(' `POST /ci {"status":"success","sha":"abc123"}` \u2014 from any CI step or deploy script');
695
+ }
696
+ const activeErrors = store.getLogs(5, "error");
697
+ if (activeErrors.length > 0 && matching.length > 0) {
698
+ lines.push("");
699
+ lines.push("> \u{1F52C} **For the causal chain and fix hint:** call `reconstruct_context`.");
700
+ lines.push("> Regression start tells you *when* \u2014 reconstruct_context tells you *why* and what to do.");
701
+ }
702
+ return { content: [{ type: "text", text: lines.join("\n") }] };
703
+ }
704
+ );
705
+ server.registerTool(
706
+ "get_repro_steps",
707
+ {
708
+ description: 'Generates draft "steps to reproduce" from the event timeline \u2014 user navigation, network calls, state changes, and the error. Ready to paste into a Jira or Linear ticket. Confidence is HIGH when \u22653 context snapshots are available.',
709
+ inputSchema: {
710
+ since: z.number().int().optional().describe("Only use events after this Unix timestamp in ms")
711
+ }
712
+ },
713
+ async ({ since }) => {
714
+ trackCall("get_repro_steps");
715
+ const logs = store.getLogs(200, void 0, since);
716
+ const network = store.getNetwork(200, void 0, since);
717
+ const contexts = store.getContext(20, since);
718
+ const repro = generateReproSteps(logs, network, contexts);
719
+ return { content: [{ type: "text", text: `## Reproduction Steps (confidence: ${repro.confidence})
720
+
721
+ ${repro.markdown}` }] };
722
+ }
723
+ );
724
+ server.registerTool(
725
+ "get_causal_graph",
726
+ {
727
+ description: "\u26A1 FREE \xB7 Returns the causal graph of the current session as structured JSON \u2014 typed nodes (error, warn, network_fail, network_ok, state, process_exit) and typed edges (TRACE_JOINED, CAUSED_BY, STATE_AT, CORRELATED_WITH, PRECEDED_BY). Model-agnostic: any consumer can traverse this graph without relying on natural-language summaries. Edge kinds are ordered by determinism: TRACE_JOINED is exact (W3C traceparent match), CAUSED_BY is detector-validated, CORRELATED_WITH is temporal proximity only. Use this to reason about event causality programmatically, build visualizations, or feed structured data to a custom pipeline \u2014 without spending reconstruct_context credits.",
728
+ inputSchema: {
729
+ since: z.number().int().optional().describe("Only include events after this Unix timestamp in ms")
730
+ }
731
+ },
732
+ async ({ since }) => {
733
+ trackCall("get_causal_graph");
734
+ const logs = store.getLogs(200, void 0, since);
735
+ const network = store.getNetwork(200, void 0, since);
736
+ const contexts = store.getContext(20, since);
737
+ const terminal = store.getTerminalOutput(100, void 0, since);
738
+ const processExits = store.getProcessExits(20, void 0, since);
739
+ const ciEvents = store.getCIEvents(20, void 0, since);
740
+ const deployments = store.getDeployments(10, void 0, since);
741
+ const causal = await buildCausalChain(logs, network, contexts, since, terminal, processExits, ciEvents, deployments);
742
+ const graph = buildCausalGraph(causal);
743
+ return { content: [{ type: "text", text: JSON.stringify(graph, null, 2) }] };
744
+ }
745
+ );
746
+ _registerAnalyzeRuntime(server);
747
+ server.registerTool(
748
+ "suggest_logging_locations",
749
+ {
750
+ description: "\u26A1 FREE \xB7 Given a hypothesis from reconstruct_context, reads your source files and suggests exactly where to add console.log statements to validate the hypothesis. Identifies function entry points, conditional branches, and error paths relevant to the diagnosis. Returns copy-pasteable console.log snippets with specific line numbers.",
751
+ inputSchema: {
752
+ hypothesis: z.string().describe('The hypothesis text from reconstruct_context (e.g. "JWT token expired before request")'),
753
+ file_path: z.string().optional().describe("Absolute or workspace-relative path to a source file to analyze. If omitted, uses recent stack frames from buffer."),
754
+ max_suggestions: z.number().int().min(1).max(20).optional().describe("Maximum number of suggestions to return (default: 5)")
755
+ }
756
+ },
757
+ async ({ hypothesis, file_path, max_suggestions = 5 }) => {
758
+ const { readFileSync, existsSync } = await import("fs");
759
+ const { resolve, basename } = await import("path");
760
+ const recentLogs = store.getLogs(50);
761
+ const frameFiles = /* @__PURE__ */ new Set();
762
+ for (const ev of recentLogs) {
763
+ if (ev.level === "error" && ev.stack) {
764
+ const matches = ev.stack.matchAll(/\(?((?:\/|\.\/|\.\.\/)[\w./\-]+\.(?:ts|tsx|js|jsx)):/g);
765
+ for (const m of matches) {
766
+ const fp = m[1];
767
+ if (!fp.includes("node_modules")) frameFiles.add(fp);
768
+ }
769
+ }
770
+ }
771
+ const candidates = file_path ? [resolve(file_path)] : Array.from(frameFiles).slice(0, 3);
772
+ if (candidates.length === 0) {
773
+ return {
774
+ content: [{
775
+ type: "text",
776
+ text: '## \u{1F50D} No source files found\n\nNo recent stack frames in the buffer and no `file_path` provided.\n\n**How to use:**\n1. Trigger the error in your app to capture stack frames\n2. Then call this tool again, or pass `file_path` directly\n\n**Example:**\n```\nsuggest_logging_locations(\n hypothesis: "JWT token expired before request",\n file_path: "src/auth/login.ts"\n)\n```'
777
+ }]
778
+ };
779
+ }
780
+ const keywords = hypothesis.toLowerCase().split(/\s+/).filter((w) => w.length > 3 && !["with", "from", "that", "this", "when", "then", "before", "after"].includes(w));
781
+ const lines = ["## \u{1F4A1} Suggested Logging Locations", "", `**Hypothesis:** ${hypothesis}`, ""];
782
+ let totalSuggestions = 0;
783
+ for (const filePath of candidates) {
784
+ if (totalSuggestions >= max_suggestions) break;
785
+ if (!existsSync(filePath)) continue;
786
+ let src;
787
+ try {
788
+ src = readFileSync(filePath, "utf8");
789
+ } catch {
790
+ continue;
791
+ }
792
+ const fileLines = src.split("\n");
793
+ const fileName = basename(filePath);
794
+ const suggestions = [];
795
+ for (let i = 0; i < fileLines.length; i++) {
796
+ const line = fileLines[i];
797
+ const lineLower = line.toLowerCase();
798
+ if (!line.trim() || line.trim().startsWith("//") || line.trim().startsWith("*") || lineLower.includes("import ")) continue;
799
+ let score = 0;
800
+ for (const kw of keywords) if (lineLower.includes(kw)) score += 2;
801
+ const isFunctionEntry = /^\s*(async\s+)?function\s+\w+|^\s*(const|let|var)\s+\w+\s*=\s*(async\s+)?\(|^\s*(async\s+)?\(\s*\)\s*=>/.test(line);
802
+ const isConditional = /^\s*(if|else if|switch)\s*\(/.test(line);
803
+ const isErrorPath = /catch\s*\(|\.catch\(|reject\(|throw\s+/.test(line);
804
+ const isReturn = /^\s*return\s+/.test(line);
805
+ const isAwait = /await\s+/.test(line);
806
+ if (isFunctionEntry) score += 3;
807
+ if (isConditional && score > 0) score += 2;
808
+ if (isErrorPath) score += 4;
809
+ if (isReturn && score > 0) score += 1;
810
+ if (isAwait && score > 0) score += 1;
811
+ if (score < 2) continue;
812
+ const indent = line.match(/^(\s*)/)?.[1] ?? "";
813
+ const context = line.trim().slice(0, 80);
814
+ let logSnippet;
815
+ if (isErrorPath) {
816
+ logSnippet = `${indent}console.error('[mergen] ${hypothesis.slice(0, 40)} \u2014 error path', { error: e ?? err, timestamp: Date.now() });`;
817
+ } else if (isConditional) {
818
+ const condMatch = line.match(/(?:if|else if)\s*\((.+)\)/);
819
+ const cond = condMatch ? condMatch[1].slice(0, 60) : "condition";
820
+ logSnippet = `${indent}console.log('[mergen] branch: ${cond}', { result: ${cond.split(/[=!<>& |]+/)[0]?.trim() ?? "value"} });`;
821
+ } else if (isFunctionEntry) {
822
+ const fnMatch = line.match(/function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=/);
823
+ const fnName = fnMatch ? fnMatch[1] || fnMatch[2] : "fn";
824
+ logSnippet = `${indent}console.log('[mergen] ${fnName} called', { args: arguments, timestamp: Date.now() });`;
825
+ } else {
826
+ logSnippet = `${indent}console.log('[mergen] checkpoint', { line: ${i + 1}, value: /* insert variable */ undefined });`;
827
+ }
828
+ suggestions.push({ lineNum: i + 1, context, logSnippet });
829
+ }
830
+ const topSuggestions = suggestions.slice(0, max_suggestions - totalSuggestions);
831
+ if (topSuggestions.length > 0) {
832
+ lines.push(`### \u{1F4C4} ${fileName}`, `\`${filePath}\``, "");
833
+ for (const s of topSuggestions) {
834
+ lines.push(`**Line ${s.lineNum}:** \`${s.context}\``);
835
+ lines.push("```typescript", `// Add BEFORE line ${s.lineNum}:`, s.logSnippet, "```", "");
836
+ totalSuggestions++;
837
+ }
838
+ }
839
+ }
840
+ if (totalSuggestions === 0) {
841
+ lines.push("No specific logging locations found in the analyzed file(s).", "");
842
+ lines.push("**Tips:**");
843
+ lines.push("- Pass a more specific `file_path` (e.g., `src/auth/login.ts`)");
844
+ lines.push("- Trigger the error in your app so stack frames are captured");
845
+ lines.push("- Try a more specific `hypothesis` with code-related keywords");
846
+ } else {
847
+ lines.push("---", '**After adding logs:** reproduce the issue, then call `get_recent_logs(level: "log")` to see the output.');
848
+ lines.push("Use `link_fix` after you resolve the issue to train the accuracy model.");
849
+ }
850
+ return { content: [{ type: "text", text: lines.join("\n") }] };
851
+ }
852
+ );
853
+ }
854
+ export {
855
+ registerAnalysisTools,
856
+ registerAnalyzeRuntime
857
+ };