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,145 @@
1
+ import bashParse from "bash-parser";
2
+ const INERT_TEXT_COMMANDS = /* @__PURE__ */ new Set(["echo", "printf", "print"]);
3
+ const RE_INVOKE_COMMANDS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "ksh", "eval"]);
4
+ const MESSAGE_TEXT_COMMANDS = /* @__PURE__ */ new Set(["git", "gh", "glab", "hg", "svn", "jj"]);
5
+ const MESSAGE_FLAGS = /* @__PURE__ */ new Set(["-m", "--message", "-b", "--body", "--description"]);
6
+ function stripMessageFlagValues(cmdName, args) {
7
+ if (!MESSAGE_TEXT_COMMANDS.has(cmdName.toLowerCase())) return args;
8
+ const out = [];
9
+ for (let i = 0; i < args.length; i++) {
10
+ const a = args[i];
11
+ if (/^--(message|body|description)=/.test(a)) continue;
12
+ if (MESSAGE_FLAGS.has(a)) {
13
+ i++;
14
+ continue;
15
+ }
16
+ out.push(a);
17
+ }
18
+ return out;
19
+ }
20
+ function walkWord(word, result) {
21
+ for (const exp of word?.expansion ?? []) {
22
+ if (exp?.type === "CommandExpansion" && typeof exp.command === "string") {
23
+ const nested = parseShellCommand(exp.command);
24
+ if (nested) {
25
+ result.commands.push(...nested);
26
+ } else {
27
+ result.incomplete = true;
28
+ }
29
+ }
30
+ }
31
+ }
32
+ function _readsStdinAsScript(cmd) {
33
+ const name = (cmd?.name?.text ?? "").toLowerCase();
34
+ if (!RE_INVOKE_COMMANDS.has(name) || name === "eval") return false;
35
+ const suffix = (cmd.suffix ?? []).filter((w) => w?.type === "Word").map((w) => w.text);
36
+ return !suffix.includes("-c");
37
+ }
38
+ function walk(node, result, depth = 0, suppressInert = true) {
39
+ if (!node || depth > 20) return;
40
+ switch (node.type) {
41
+ case "Script":
42
+ case "CompoundList":
43
+ for (const c of node.commands ?? []) walk(c, result, depth + 1, suppressInert);
44
+ return;
45
+ case "LogicalExpression":
46
+ walk(node.left, result, depth + 1, suppressInert);
47
+ walk(node.right, result, depth + 1, suppressInert);
48
+ return;
49
+ case "Pipeline": {
50
+ const pipelineSuppress = suppressInert && !(node.commands ?? []).some(_readsStdinAsScript);
51
+ for (const c of node.commands ?? []) walk(c, result, depth + 1, pipelineSuppress);
52
+ return;
53
+ }
54
+ case "Subshell":
55
+ walk(node.list, result, depth + 1, suppressInert);
56
+ return;
57
+ case "If":
58
+ walk(node.clause, result, depth + 1, suppressInert);
59
+ walk(node.then, result, depth + 1, suppressInert);
60
+ if (node.else) walk(node.else, result, depth + 1, suppressInert);
61
+ return;
62
+ case "For":
63
+ for (const w of node.wordlist ?? []) walkWord(w, result);
64
+ walk(node.do, result, depth + 1, suppressInert);
65
+ return;
66
+ case "While":
67
+ case "Until":
68
+ walk(node.clause, result, depth + 1, suppressInert);
69
+ walk(node.do, result, depth + 1, suppressInert);
70
+ return;
71
+ case "Case":
72
+ walkWord(node.clause, result);
73
+ for (const item of node.cases ?? []) {
74
+ for (const w of item.pattern ?? []) walkWord(w, result);
75
+ walk(item.body, result, depth + 1, suppressInert);
76
+ }
77
+ return;
78
+ case "Function":
79
+ walk(node.body, result, depth + 1, suppressInert);
80
+ return;
81
+ case "Command": {
82
+ const name = node.name?.text ?? "";
83
+ walkWord(node.name, result);
84
+ const prefixWords = (node.prefix ?? []).filter((w) => typeof w?.text === "string");
85
+ if (prefixWords.length > 0) {
86
+ result.commands.push({ name: "", args: prefixWords.map((w) => w.text) });
87
+ }
88
+ const argWords = (node.suffix ?? []).filter((w) => w?.type === "Word");
89
+ const args = argWords.map((w) => w.text);
90
+ for (const w of argWords) walkWord(w, result);
91
+ const isInert = suppressInert && INERT_TEXT_COMMANDS.has(name.toLowerCase());
92
+ if (isInert) {
93
+ result.inertText.push(...args);
94
+ } else {
95
+ result.commands.push({ name, args: stripMessageFlagValues(name, args) });
96
+ }
97
+ if (RE_INVOKE_COMMANDS.has(name.toLowerCase())) {
98
+ const script = name.toLowerCase() === "eval" ? args.join(" ") : args[args.indexOf("-c") + 1];
99
+ if (script) {
100
+ const nested = parseShellCommand(script);
101
+ if (nested) result.commands.push(...nested);
102
+ else result.incomplete = true;
103
+ }
104
+ }
105
+ return;
106
+ }
107
+ default:
108
+ result.incomplete = true;
109
+ }
110
+ }
111
+ function parseShellCommand(cmd) {
112
+ try {
113
+ const ast = bashParse(cmd);
114
+ const result = { commands: [], inertText: [], incomplete: false };
115
+ walk(ast, result);
116
+ if (result.incomplete) return null;
117
+ return result.commands;
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+ function analyzeCommands(commands) {
123
+ const raw = commands.join(" ");
124
+ let ast;
125
+ try {
126
+ ast = bashParse(raw);
127
+ } catch {
128
+ return { haystack: raw, commands: [] };
129
+ }
130
+ const result = { commands: [], inertText: [], incomplete: false };
131
+ walk(ast, result, 0);
132
+ if (result.incomplete) return { haystack: raw, commands: [] };
133
+ const executed = result.commands.map((c) => [c.name, ...c.args].join(" ")).join(" ");
134
+ const inertNames = raw.match(/\b(echo|printf|print)\b/gi) ?? [];
135
+ const haystack = [executed, ...inertNames].join(" ").trim() || raw;
136
+ return { haystack, commands: result.commands };
137
+ }
138
+ function buildCommandHaystack(commands) {
139
+ return analyzeCommands(commands).haystack;
140
+ }
141
+ export {
142
+ analyzeCommands,
143
+ buildCommandHaystack,
144
+ parseShellCommand
145
+ };
@@ -0,0 +1,104 @@
1
+ import logger from "../sensor/logger.js";
2
+ function webhookConfigured() {
3
+ return !!process.env.MERGEN_SIEM_WEBHOOK_URL;
4
+ }
5
+ function splunkHecConfigured() {
6
+ return !!(process.env.MERGEN_SPLUNK_HEC_URL && process.env.MERGEN_SPLUNK_HEC_TOKEN);
7
+ }
8
+ function elasticConfigured() {
9
+ return !!(process.env.MERGEN_ELASTIC_URL && process.env.MERGEN_ELASTIC_API_KEY);
10
+ }
11
+ function siemForwardingConfigured() {
12
+ return webhookConfigured() || splunkHecConfigured() || elasticConfigured();
13
+ }
14
+ function ledgerVerdictForwarded(verdict) {
15
+ const raw = process.env.MERGEN_SIEM_LEDGER_VERDICTS ?? "block,hold";
16
+ return raw.split(",").map((s) => s.trim().toLowerCase()).includes(verdict);
17
+ }
18
+ async function postWebhook(body) {
19
+ const url = process.env.MERGEN_SIEM_WEBHOOK_URL;
20
+ const headers = { "Content-Type": "application/json" };
21
+ if (process.env.MERGEN_SIEM_WEBHOOK_TOKEN) {
22
+ headers.Authorization = `Bearer ${process.env.MERGEN_SIEM_WEBHOOK_TOKEN}`;
23
+ }
24
+ const res = await fetch(url, {
25
+ method: "POST",
26
+ headers,
27
+ body: JSON.stringify(body),
28
+ signal: AbortSignal.timeout(5e3)
29
+ });
30
+ if (!res.ok) throw new Error(`SIEM webhook POST failed: HTTP ${res.status}`);
31
+ }
32
+ async function postSplunkHec(event, timeMs, sourcetype) {
33
+ const url = process.env.MERGEN_SPLUNK_HEC_URL;
34
+ const token = process.env.MERGEN_SPLUNK_HEC_TOKEN;
35
+ const res = await fetch(url, {
36
+ method: "POST",
37
+ headers: { "Content-Type": "application/json", Authorization: `Splunk ${token}` },
38
+ body: JSON.stringify({ time: Math.floor(timeMs / 1e3), sourcetype, event }),
39
+ signal: AbortSignal.timeout(5e3)
40
+ });
41
+ if (!res.ok) throw new Error(`Splunk HEC POST failed: HTTP ${res.status}`);
42
+ }
43
+ async function postElastic(event, index, timeMs) {
44
+ const base = process.env.MERGEN_ELASTIC_URL.replace(/\/$/, "");
45
+ const apiKey = process.env.MERGEN_ELASTIC_API_KEY;
46
+ const meta = JSON.stringify({ index: { _index: index } });
47
+ const doc = JSON.stringify({ "@timestamp": new Date(timeMs).toISOString(), ...event });
48
+ const body = `${meta}
49
+ ${doc}
50
+ `;
51
+ const res = await fetch(`${base}/_bulk`, {
52
+ method: "POST",
53
+ headers: {
54
+ "Content-Type": "application/x-ndjson",
55
+ Authorization: `ApiKey ${apiKey}`
56
+ },
57
+ body,
58
+ signal: AbortSignal.timeout(5e3)
59
+ });
60
+ if (!res.ok) throw new Error(`Elastic _bulk POST failed: HTTP ${res.status}`);
61
+ }
62
+ function dispatch(env) {
63
+ if (webhookConfigured()) {
64
+ postWebhook(env.webhookBody).catch((err) => {
65
+ logger.warn({ err, id: env.id }, "siem-forward: webhook delivery failed");
66
+ });
67
+ }
68
+ if (splunkHecConfigured()) {
69
+ postSplunkHec(env.splunkEvent, env.timeMs, env.splunkSourcetype).catch((err) => {
70
+ logger.warn({ err, id: env.id }, "siem-forward: Splunk HEC delivery failed");
71
+ });
72
+ }
73
+ if (elasticConfigured()) {
74
+ postElastic(env.splunkEvent, env.elasticIndex, env.timeMs).catch((err) => {
75
+ logger.warn({ err, id: env.id }, "siem-forward: Elastic delivery failed");
76
+ });
77
+ }
78
+ }
79
+ function forwardToSiem(entry) {
80
+ dispatch({
81
+ webhookBody: { source: "mergen_agent_blunder_log", ...entry },
82
+ splunkEvent: entry,
83
+ splunkSourcetype: "mergen:agent_blunder",
84
+ elasticIndex: "mergen-blunders",
85
+ timeMs: entry.recordedAt,
86
+ id: entry.id
87
+ });
88
+ }
89
+ function forwardLedgerToSiem(entry) {
90
+ if (!siemForwardingConfigured() || !ledgerVerdictForwarded(entry.verdict)) return;
91
+ dispatch({
92
+ webhookBody: { source: "mergen_action_ledger", ...entry },
93
+ splunkEvent: entry,
94
+ splunkSourcetype: "mergen:action_ledger",
95
+ elasticIndex: "mergen-ledger",
96
+ timeMs: entry.ts,
97
+ id: entry.id
98
+ });
99
+ }
100
+ export {
101
+ forwardLedgerToSiem,
102
+ forwardToSiem,
103
+ siemForwardingConfigured
104
+ };
@@ -0,0 +1,151 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { DATA_DIR } from "../sensor/paths.js";
4
+ import logger from "../sensor/logger.js";
5
+ import { incidentStore } from "../sensor/incident-store.js";
6
+ import { getStats, getRealVerdictCount, isCorpusSeeded } from "./calibration.js";
7
+ import { getOverrideSummary, getStaleOverrides } from "./override-corpus.js";
8
+ const BOT_TOKEN = process.env.MERGEN_SLACK_BOT_TOKEN ?? "";
9
+ const SLACK_CHANNEL = process.env.MERGEN_SLACK_CHANNEL ?? "";
10
+ async function _post(payload) {
11
+ if (!BOT_TOKEN || !SLACK_CHANNEL) return;
12
+ try {
13
+ const resp = await fetch("https://slack.com/api/chat.postMessage", {
14
+ method: "POST",
15
+ headers: {
16
+ "Authorization": `Bearer ${BOT_TOKEN}`,
17
+ "Content-Type": "application/json; charset=utf-8"
18
+ },
19
+ body: JSON.stringify({ channel: SLACK_CHANNEL, ...payload })
20
+ });
21
+ const json = await resp.json();
22
+ if (!json.ok) logger.warn({ error: json.error }, "slack-digest: postMessage failed");
23
+ } catch (err) {
24
+ logger.warn({ err }, "slack-digest: fetch error");
25
+ }
26
+ }
27
+ async function postDailyDigest() {
28
+ if (!BOT_TOKEN || !SLACK_CHANNEL) {
29
+ logger.warn("slack-digest: MERGEN_SLACK_BOT_TOKEN or MERGEN_SLACK_CHANNEL not set \u2014 skipping digest");
30
+ return;
31
+ }
32
+ const now = Date.now();
33
+ const since24h = now - 24 * 60 * 60 * 1e3;
34
+ const allIncidents = incidentStore.list();
35
+ const recent = allIncidents.filter((i) => i.createdAt >= since24h);
36
+ const resolved = recent.filter((i) => i.status === "resolved");
37
+ const autonomous = resolved.filter((i) => i.resolvedAutonomously);
38
+ const avgMttrMin = resolved.length > 0 ? Math.round(
39
+ resolved.reduce((sum, i) => sum + ((i.resolvedAt ?? now) - i.createdAt), 0) / resolved.length / 6e4
40
+ ) : null;
41
+ const stats = getStats();
42
+ const driftingTags = stats.filter(
43
+ (s) => s.trusted && s.trendDelta != null && s.trendDelta < -0.05
44
+ );
45
+ const realVerdicts = getRealVerdictCount();
46
+ const seeded = isCorpusSeeded();
47
+ const overrideSummary = getOverrideSummary().slice(0, 3);
48
+ const staleOverrides = getStaleOverrides(60);
49
+ const pendingRunbooks = [];
50
+ try {
51
+ const libraryFile = path.join(DATA_DIR, "runbooks", "library.json");
52
+ if (fs.existsSync(libraryFile)) {
53
+ const all = JSON.parse(fs.readFileSync(libraryFile, "utf8"));
54
+ pendingRunbooks.push(...all.filter((r) => !r.approved));
55
+ }
56
+ } catch {
57
+ }
58
+ const blocks = [
59
+ {
60
+ type: "header",
61
+ text: { type: "plain_text", text: "\u{1F4CB} Mergen Daily Digest", emoji: true }
62
+ },
63
+ {
64
+ type: "context",
65
+ elements: [{ type: "mrkdwn", text: `Last 24h \xB7 ${(/* @__PURE__ */ new Date()).toUTCString()}` }]
66
+ },
67
+ { type: "divider" }
68
+ ];
69
+ blocks.push({
70
+ type: "section",
71
+ text: {
72
+ type: "mrkdwn",
73
+ text: [
74
+ `*Incidents (last 24h)*`,
75
+ `\u2022 Total fired: *${recent.length}*`,
76
+ resolved.length > 0 ? `\u2022 Resolved: *${resolved.length}*` : "",
77
+ autonomous.length > 0 ? `\u2022 Autonomous resolution: *${autonomous.length}/${resolved.length}* (${Math.round(autonomous.length / resolved.length * 100)}%)` : "",
78
+ avgMttrMin != null ? `\u2022 Avg MTTR: *${avgMttrMin} min*` : "",
79
+ recent.length === 0 ? "_No incidents in last 24h_ \u2705" : ""
80
+ ].filter(Boolean).join("\n")
81
+ }
82
+ });
83
+ const calibLines = [
84
+ `*Calibration*`,
85
+ seeded ? `\u2022 \u26A0\uFE0F Warm-up phase \u2014 ${realVerdicts}/10 real verdicts recorded` : `\u2022 ${realVerdicts} real verdicts \u2014 confidence is environment-calibrated`,
86
+ driftingTags.length > 0 ? `\u2022 \u26A0\uFE0F Accuracy regression on: ${driftingTags.map((t) => `\`${t.tag}\``).join(", ")}` : "\u2022 All detectors stable"
87
+ ];
88
+ blocks.push({ type: "section", text: { type: "mrkdwn", text: calibLines.join("\n") } });
89
+ if (overrideSummary.length > 0) {
90
+ const overrideLines = [
91
+ `*Recent Override Patterns*`,
92
+ ...overrideSummary.map(
93
+ (o) => `\u2022 \`${o.tag}\` \u2014 ${o.dominantReason ?? "on-call-discretion"} (${o.total}\xD7)`
94
+ )
95
+ ];
96
+ blocks.push({ type: "section", text: { type: "mrkdwn", text: overrideLines.join("\n") } });
97
+ }
98
+ if (staleOverrides.length > 0) {
99
+ const topStale = staleOverrides.slice(0, 3);
100
+ const staleLines = [
101
+ `*\u26A0\uFE0F Override Corpus \u2014 ${staleOverrides.length} Stale ${staleOverrides.length === 1 ? "Entry" : "Entries"} (>60 days without review)*`,
102
+ ...topStale.map((e) => {
103
+ const age = Math.floor((Date.now() - (e.reviewedAt ?? e.recordedAt)) / 864e5);
104
+ return `\u2022 \`${e.incidentTag}\` \xB7 ${e.service} \xB7 ${e.overrideReason} \u2014 _${age}d since last review_`;
105
+ }),
106
+ staleOverrides.length > 3 ? `_\u2026and ${staleOverrides.length - 3} more. Review at_ \`GET /override-corpus/stale\`` : "",
107
+ `_Re-affirm: \`POST /overrides/{id}/review\` \xB7 Remove: \`DELETE /overrides/{id}\`_`
108
+ ].filter(Boolean);
109
+ blocks.push({ type: "section", text: { type: "mrkdwn", text: staleLines.join("\n") } });
110
+ }
111
+ if (pendingRunbooks.length > 0) {
112
+ blocks.push({
113
+ type: "section",
114
+ text: {
115
+ type: "mrkdwn",
116
+ text: [
117
+ `*Runbooks Awaiting Approval*`,
118
+ ...pendingRunbooks.map(
119
+ (r) => r ? `\u2022 \`${r.name}\` (\`${r.incidentTag}\`) \u2014 ${r.approvals.length}/${r.requiredApprovers} approvals` : ""
120
+ )
121
+ ].filter(Boolean).join("\n")
122
+ }
123
+ });
124
+ }
125
+ blocks.push({
126
+ type: "context",
127
+ elements: [{ type: "mrkdwn", text: "Mergen Operational Digest \xB7 <http://127.0.0.1:3000/dashboard|Dashboard> \xB7 <http://127.0.0.1:3000/incidents/impact-report|Impact Report>" }]
128
+ });
129
+ await _post({ blocks });
130
+ logger.info({ incidents: recent.length, resolved: resolved.length }, "slack-digest: posted daily digest");
131
+ }
132
+ function msUntilNext9amUtc() {
133
+ const now = /* @__PURE__ */ new Date();
134
+ const next = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 9, 0, 0, 0));
135
+ if (next.getTime() <= now.getTime()) next.setUTCDate(next.getUTCDate() + 1);
136
+ return next.getTime() - now.getTime();
137
+ }
138
+ function startSlackDailyDigest() {
139
+ const delay = msUntilNext9amUtc();
140
+ logger.info({ nextDigestIn: `${Math.round(delay / 6e4)}min` }, "slack-digest: scheduled daily digest at 09:00 UTC");
141
+ setTimeout(() => {
142
+ void postDailyDigest();
143
+ setInterval(() => {
144
+ void postDailyDigest();
145
+ }, 24 * 60 * 60 * 1e3).unref();
146
+ }, delay).unref();
147
+ }
148
+ export {
149
+ postDailyDigest,
150
+ startSlackDailyDigest
151
+ };
@@ -0,0 +1,217 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { DATA_DIR } from "../sensor/paths.js";
4
+ import logger from "../sensor/logger.js";
5
+ import { synthesizeRulesFromCorpus } from "./corpus-to-policy.js";
6
+ import { getStores } from "../storage/store-registry.js";
7
+ const BOT_TOKEN = process.env.MERGEN_SLACK_BOT_TOKEN ?? "";
8
+ const SLACK_CHANNEL = process.env.MERGEN_SLACK_CHANNEL ?? "";
9
+ const SYSTEM_TENANT = process.env.MERGEN_SYSTEM_TENANT_ID;
10
+ const POLL_INTERVAL_MS = 6 * 60 * 60 * 1e3;
11
+ const STATE_FILE = path.join(DATA_DIR, "slack-override-loop.json");
12
+ const POSTMORTEM_SIGNALS = [
13
+ "postmortem",
14
+ "post-mortem",
15
+ "post mortem",
16
+ "root cause",
17
+ "rca",
18
+ "fixed by",
19
+ "incident resolved",
20
+ "outage resolved",
21
+ "mitigation",
22
+ "this was caused by",
23
+ "we resolved",
24
+ "issue resolved",
25
+ "marked resolved",
26
+ "runbook",
27
+ "we decided",
28
+ "going forward",
29
+ // Negative decision patterns — often appear without a formal postmortem header
30
+ "don't",
31
+ "do not",
32
+ "avoid",
33
+ "should not",
34
+ "we learned",
35
+ "lesson learned",
36
+ "constraint:",
37
+ "action item",
38
+ "takeaway",
39
+ "going forward",
40
+ "next time"
41
+ ];
42
+ function loadState() {
43
+ try {
44
+ if (fs.existsSync(STATE_FILE)) {
45
+ return JSON.parse(fs.readFileSync(STATE_FILE, "utf8"));
46
+ }
47
+ } catch {
48
+ }
49
+ return { processedTs: [], lastPollAt: 0 };
50
+ }
51
+ function saveState(state) {
52
+ try {
53
+ fs.mkdirSync(DATA_DIR, { recursive: true });
54
+ const tmp = `${STATE_FILE}.tmp`;
55
+ state.processedTs = state.processedTs.slice(-5e3);
56
+ fs.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf8");
57
+ fs.renameSync(tmp, STATE_FILE);
58
+ } catch (err) {
59
+ logger.warn({ err }, "slack-override-loop: failed to persist state");
60
+ }
61
+ }
62
+ async function _get(path2) {
63
+ if (!BOT_TOKEN) return null;
64
+ try {
65
+ const resp = await fetch(`https://slack.com${path2}`, {
66
+ headers: { "Authorization": `Bearer ${BOT_TOKEN}` }
67
+ });
68
+ return await resp.json();
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+ async function _resolveChannel(nameOrId) {
74
+ if (/^[CG][A-Z0-9]{6,}$/i.test(nameOrId)) return nameOrId;
75
+ const clean = nameOrId.replace(/^#/, "").toLowerCase();
76
+ let cursor;
77
+ for (let page = 0; page < 3; page++) {
78
+ const qs = new URLSearchParams({
79
+ limit: "200",
80
+ types: "public_channel,private_channel",
81
+ exclude_archived: "true",
82
+ ...cursor ? { cursor } : {}
83
+ });
84
+ const r = await _get(`/api/conversations.list?${qs}`);
85
+ if (!r?.ok || !Array.isArray(r.channels)) break;
86
+ const match = r.channels.find((c) => c.name?.toLowerCase() === clean);
87
+ if (match?.id) return match.id;
88
+ if (!r.response_metadata?.next_cursor) break;
89
+ cursor = r.response_metadata.next_cursor;
90
+ }
91
+ return null;
92
+ }
93
+ async function _fetchHistory(channelId, oldest) {
94
+ const qs = new URLSearchParams({ channel: channelId, oldest, limit: "100" });
95
+ const r = await _get(`/api/conversations.history?${qs}`);
96
+ if (!r?.ok || !Array.isArray(r.messages)) return [];
97
+ return r.messages;
98
+ }
99
+ async function _fetchThread(channelId, threadTs) {
100
+ const messages = [];
101
+ let cursor;
102
+ for (let page = 0; page < 5; page++) {
103
+ const qs = new URLSearchParams({
104
+ channel: channelId,
105
+ ts: threadTs,
106
+ limit: "100",
107
+ ...cursor ? { cursor } : {}
108
+ });
109
+ const r = await _get(`/api/conversations.replies?${qs}`);
110
+ if (!r?.ok || !Array.isArray(r.messages)) break;
111
+ for (const msg of r.messages) {
112
+ if (msg.text) {
113
+ const d = new Date(parseFloat(msg.ts) * 1e3);
114
+ const time = `${d.getUTCHours().toString().padStart(2, "0")}:${d.getUTCMinutes().toString().padStart(2, "0")}`;
115
+ messages.push(`[${time}] ${msg.text.slice(0, 500)}`);
116
+ }
117
+ }
118
+ if (!r.response_metadata?.next_cursor) break;
119
+ cursor = r.response_metadata.next_cursor;
120
+ }
121
+ return messages.length > 0 ? messages.join("\n") : null;
122
+ }
123
+ async function _postSynthesizedRules(channelId, threadTs) {
124
+ if (!BOT_TOKEN) return;
125
+ const newRules = synthesizeRulesFromCorpus();
126
+ if (newRules.length === 0) return;
127
+ const lines = newRules.slice(0, 3).map(
128
+ (s) => `\u2022 *${s.rule.name}* (${s.sourceOccurrences} overrides, action: \`${s.rule.action}\`)`
129
+ );
130
+ const payload = {
131
+ channel: channelId,
132
+ thread_ts: threadTs,
133
+ text: `\u{1F4CB} Mergen detected ${newRules.length} new policy pattern${newRules.length !== 1 ? "s" : ""} from this thread.`,
134
+ blocks: [
135
+ {
136
+ type: "section",
137
+ text: {
138
+ type: "mrkdwn",
139
+ text: [
140
+ `\u{1F4CB} *Mergen detected ${newRules.length} new policy pattern${newRules.length !== 1 ? "s" : ""} from this postmortem.*`,
141
+ ``,
142
+ lines.join("\n"),
143
+ ``,
144
+ `Review and activate: \`GET /policy-suggestions\` \u2192 \`POST /policies/rules\``
145
+ ].join("\n")
146
+ }
147
+ }
148
+ ]
149
+ };
150
+ try {
151
+ await fetch("https://slack.com/api/chat.postMessage", {
152
+ method: "POST",
153
+ headers: { "Content-Type": "application/json; charset=utf-8", Authorization: `Bearer ${BOT_TOKEN}` },
154
+ body: JSON.stringify(payload),
155
+ signal: AbortSignal.timeout(8e3)
156
+ });
157
+ } catch (err) {
158
+ logger.warn({ err }, "slack-override-loop: failed to post synthesized rules notification");
159
+ }
160
+ }
161
+ async function poll() {
162
+ if (!BOT_TOKEN || !SLACK_CHANNEL) return;
163
+ const state = loadState();
164
+ const channelId = await _resolveChannel(SLACK_CHANNEL);
165
+ if (!channelId) {
166
+ logger.warn({ channel: SLACK_CHANNEL }, "slack-override-loop: could not resolve channel ID");
167
+ return;
168
+ }
169
+ const oldestTs = String((Date.now() - 7 * 60 * 60 * 1e3) / 1e3);
170
+ const messages = await _fetchHistory(channelId, oldestTs);
171
+ let newOverrides = 0;
172
+ const processedSet = new Set(state.processedTs);
173
+ for (const msg of messages) {
174
+ const ts = msg.thread_ts ?? msg.ts;
175
+ if (processedSet.has(ts)) continue;
176
+ const text = (msg.text ?? "").toLowerCase();
177
+ const isPostmortem = POSTMORTEM_SIGNALS.some((s) => text.includes(s));
178
+ if (!isPostmortem) continue;
179
+ const threadText = await _fetchThread(channelId, ts);
180
+ if (!threadText) {
181
+ processedSet.add(ts);
182
+ continue;
183
+ }
184
+ const overrides = await getStores().overrides.compileOverridesFromSlackThread(threadText, "unknown", SYSTEM_TENANT);
185
+ if (overrides.length > 0) {
186
+ newOverrides += overrides.length;
187
+ logger.info(
188
+ { count: overrides.length, tag: overrides[0].incidentTag, reason: overrides[0].overrideReason, ts },
189
+ "slack-override-loop: extracted override patterns from Slack thread"
190
+ );
191
+ void _postSynthesizedRules(channelId, ts);
192
+ }
193
+ processedSet.add(ts);
194
+ }
195
+ state.processedTs = [...processedSet];
196
+ state.lastPollAt = Date.now();
197
+ saveState(state);
198
+ if (newOverrides > 0) {
199
+ logger.info({ newOverrides }, "slack-override-loop: poll complete \u2014 new patterns added to corpus");
200
+ } else {
201
+ logger.debug("slack-override-loop: poll complete \u2014 no new patterns found");
202
+ }
203
+ }
204
+ function startSlackOverrideLoop() {
205
+ if (!BOT_TOKEN || !SLACK_CHANNEL) {
206
+ logger.warn("slack-override-loop: MERGEN_SLACK_BOT_TOKEN or MERGEN_SLACK_CHANNEL not set \u2014 loop disabled");
207
+ return;
208
+ }
209
+ void poll();
210
+ setInterval(() => {
211
+ void poll();
212
+ }, POLL_INTERVAL_MS).unref();
213
+ logger.info({ intervalHours: 6, channel: SLACK_CHANNEL }, "slack-override-loop: scheduled \u2014 scanning for postmortem patterns every 6h");
214
+ }
215
+ export {
216
+ startSlackOverrideLoop
217
+ };
@@ -0,0 +1,67 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { DATA_DIR } from "../sensor/paths.js";
4
+ import logger from "../sensor/logger.js";
5
+ const ROUTING_FILE = path.join(DATA_DIR, "slack-routing.json");
6
+ let _rules = [];
7
+ let _loaded = false;
8
+ function load() {
9
+ if (_loaded) return;
10
+ _loaded = true;
11
+ if (!fs.existsSync(ROUTING_FILE)) return;
12
+ try {
13
+ const parsed = JSON.parse(fs.readFileSync(ROUTING_FILE, "utf8"));
14
+ if (parsed?.version === 1 && Array.isArray(parsed.rules)) {
15
+ _rules = parsed.rules;
16
+ }
17
+ } catch (err) {
18
+ logger.warn({ err }, "slack-routing: failed to load config, starting empty");
19
+ }
20
+ }
21
+ function persist() {
22
+ try {
23
+ fs.mkdirSync(DATA_DIR, { recursive: true });
24
+ const tmp = `${ROUTING_FILE}.tmp.${process.pid}.${Date.now()}`;
25
+ fs.writeFileSync(tmp, JSON.stringify({ version: 1, rules: _rules }, null, 2), "utf8");
26
+ fs.renameSync(tmp, ROUTING_FILE);
27
+ } catch (err) {
28
+ logger.warn({ err }, "slack-routing: failed to persist config");
29
+ }
30
+ }
31
+ function getRules() {
32
+ load();
33
+ return [..._rules];
34
+ }
35
+ function getRoutingForService(service) {
36
+ load();
37
+ const exact = _rules.find((r) => r.service === service);
38
+ if (exact) return exact;
39
+ return _rules.find((r) => r.service === "*") ?? null;
40
+ }
41
+ function upsertRule(input) {
42
+ load();
43
+ const id = input.id ?? `rule-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
44
+ const rule = { ...input, id };
45
+ const existingIdx = _rules.findIndex((r) => r.id === id || r.service === input.service);
46
+ if (existingIdx >= 0) {
47
+ _rules[existingIdx] = rule;
48
+ } else {
49
+ _rules.push(rule);
50
+ }
51
+ persist();
52
+ return rule;
53
+ }
54
+ function deleteRule(id) {
55
+ load();
56
+ const before = _rules.length;
57
+ _rules = _rules.filter((r) => r.id !== id);
58
+ if (_rules.length === before) return false;
59
+ persist();
60
+ return true;
61
+ }
62
+ export {
63
+ deleteRule,
64
+ getRoutingForService,
65
+ getRules,
66
+ upsertRule
67
+ };