@interactive-inc/claude-funnel 0.7.1 → 0.8.1

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 (472) hide show
  1. package/README.md +155 -133
  2. package/dist/bin.js +1344 -0
  3. package/dist/cli/factory.d.ts +7 -0
  4. package/dist/cli/router/query-to-cli-args.d.ts +1 -0
  5. package/dist/cli/router/to-request.d.ts +5 -0
  6. package/dist/cli/router/validator.d.ts +5 -0
  7. package/dist/cli/routes/channels.$channel.connectors.$connector.d.ts +42 -0
  8. package/dist/cli/routes/channels.$channel.connectors.$connector.rename.$newName.d.ts +46 -0
  9. package/dist/cli/routes/channels.$channel.connectors.$connector.request.d.ts +54 -0
  10. package/dist/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.d.ts +66 -0
  11. package/dist/cli/routes/channels.$channel.connectors.$connector.schedules.d.ts +42 -0
  12. package/dist/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.d.ts +46 -0
  13. package/dist/cli/routes/channels.$channel.connectors.add.$connector.d.ts +90 -0
  14. package/dist/cli/routes/channels.$channel.connectors.d.ts +38 -0
  15. package/dist/cli/routes/channels.$channel.connectors.remove.$connector.d.ts +42 -0
  16. package/dist/cli/routes/channels.$channel.connectors.set.$connector.d.ts +62 -0
  17. package/dist/cli/routes/channels.$channel.d.ts +38 -0
  18. package/dist/cli/routes/channels.$channel.rename.$newName.d.ts +42 -0
  19. package/dist/cli/routes/channels.$channel.set.delivery.$mode.d.ts +28 -0
  20. package/dist/cli/routes/channels.add.$channel.d.ts +46 -0
  21. package/dist/cli/routes/channels.d.ts +16 -0
  22. package/dist/cli/routes/channels.remove.$channel.d.ts +38 -0
  23. package/dist/cli/routes/claude.d.ts +32 -0
  24. package/dist/cli/routes/gateway.d.ts +20 -0
  25. package/dist/cli/routes/gateway.listeners.d.ts +17 -0
  26. package/dist/cli/routes/gateway.logs.d.ts +24 -0
  27. package/dist/cli/routes/gateway.restart.d.ts +24 -0
  28. package/dist/cli/routes/gateway.run.d.ts +24 -0
  29. package/dist/cli/routes/gateway.start.d.ts +24 -0
  30. package/dist/cli/routes/gateway.status.d.ts +13 -0
  31. package/dist/cli/routes/gateway.stop.d.ts +16 -0
  32. package/dist/cli/routes/index.d.ts +1222 -0
  33. package/dist/cli/routes/profiles.$profile.as-default.d.ts +38 -0
  34. package/dist/cli/routes/profiles.$profile.rename.$newName.d.ts +42 -0
  35. package/dist/cli/routes/profiles.$profile.run.d.ts +46 -0
  36. package/dist/cli/routes/profiles.add.$profile.d.ts +54 -0
  37. package/dist/cli/routes/profiles.d.ts +16 -0
  38. package/dist/cli/routes/profiles.remove.$profile.d.ts +38 -0
  39. package/dist/cli/routes/profiles.set.$profile.d.ts +54 -0
  40. package/dist/cli/routes/status.d.ts +16 -0
  41. package/dist/cli/routes/update.d.ts +16 -0
  42. package/dist/connectors/connector-adapter.d.ts +8 -0
  43. package/dist/connectors/connector-config-schema.d.ts +43 -0
  44. package/dist/connectors/connector-factory.d.ts +32 -0
  45. package/dist/connectors/connector-listener.d.ts +17 -0
  46. package/dist/connectors/discord-adapter.d.ts +14 -0
  47. package/dist/connectors/discord-connector-schema.d.ts +10 -0
  48. package/dist/connectors/discord-event-processor.d.ts +26 -0
  49. package/dist/connectors/discord-listener.d.ts +17 -0
  50. package/dist/connectors/gh-adapter.d.ts +11 -0
  51. package/dist/connectors/gh-connector-schema.d.ts +10 -0
  52. package/dist/connectors/gh-listener.d.ts +26 -0
  53. package/dist/connectors/match-cron.d.ts +1 -0
  54. package/dist/connectors/schedule-connector-schema.d.ts +45 -0
  55. package/dist/connectors/schedule-listener.d.ts +30 -0
  56. package/dist/connectors/schedule-state-store.d.ts +19 -0
  57. package/dist/connectors/slack-adapter.d.ts +15 -0
  58. package/dist/connectors/slack-connector-schema.d.ts +11 -0
  59. package/dist/connectors/slack-event-processor.d.ts +27 -0
  60. package/dist/connectors/slack-listener.d.ts +17 -0
  61. package/dist/engine/channels/channels.d.ts +106 -0
  62. package/dist/engine/claude/claude.d.ts +49 -0
  63. package/dist/engine/claude/gateway-controller.d.ts +6 -0
  64. package/dist/engine/fs/file-system.d.ts +24 -0
  65. package/dist/engine/fs/memory-file-system.d.ts +31 -0
  66. package/dist/engine/fs/node-file-system.d.ts +15 -0
  67. package/dist/engine/http/http-client.d.ts +15 -0
  68. package/dist/engine/http/memory-http-client.d.ts +12 -0
  69. package/dist/engine/http/node-http-client.d.ts +5 -0
  70. package/dist/engine/id/id-generator.d.ts +7 -0
  71. package/dist/engine/id/memory-id-generator.d.ts +11 -0
  72. package/dist/engine/id/node-id-generator.d.ts +4 -0
  73. package/dist/engine/logger/logger.d.ts +11 -0
  74. package/dist/engine/logger/memory-logger.d.ts +14 -0
  75. package/dist/engine/logger/node-logger.d.ts +15 -0
  76. package/dist/engine/logger/noop-logger.d.ts +7 -0
  77. package/dist/engine/mcp/channel-server.d.ts +1 -0
  78. package/dist/engine/mcp/mcp.d.ts +22 -0
  79. package/dist/engine/process/memory-process-runner.d.ts +43 -0
  80. package/dist/engine/process/node-process-runner.d.ts +9 -0
  81. package/dist/engine/process/process-runner.d.ts +29 -0
  82. package/dist/engine/profiles/profile-channel-checker.d.ts +7 -0
  83. package/dist/engine/profiles/profiles.d.ts +31 -0
  84. package/dist/engine/settings/mock-settings-reader.d.ts +9 -0
  85. package/dist/engine/settings/settings-reader.d.ts +5 -0
  86. package/dist/engine/settings/settings-schema.d.ts +132 -0
  87. package/dist/engine/settings/settings-store.d.ts +18 -0
  88. package/dist/engine/time/clock.d.ts +9 -0
  89. package/dist/engine/time/memory-clock.d.ts +12 -0
  90. package/dist/engine/time/node-clock.d.ts +4 -0
  91. package/dist/funnel.d.ts +95 -0
  92. package/dist/gateway/auth-middleware.d.ts +14 -0
  93. package/dist/gateway/broadcaster.d.ts +122 -0
  94. package/dist/gateway/daemon.d.ts +2 -0
  95. package/dist/gateway/daemon.js +485 -0
  96. package/dist/gateway/factory.d.ts +7 -0
  97. package/dist/gateway/funnel-event-store.d.ts +81 -0
  98. package/dist/gateway/gateway-server.d.ts +94 -0
  99. package/dist/gateway/gateway-token.d.ts +33 -0
  100. package/dist/gateway/gateway.d.ts +58 -0
  101. package/dist/gateway/kill-competing-slack-gateways.d.ts +9 -0
  102. package/dist/gateway/listener-supervisor.d.ts +85 -0
  103. package/dist/gateway/listeners-client.d.ts +53 -0
  104. package/dist/gateway/resolve-daemon-script.d.ts +11 -0
  105. package/dist/gateway/routes/channels.connectors.call.d.ts +41 -0
  106. package/dist/gateway/routes/health.d.ts +17 -0
  107. package/dist/gateway/routes/index.d.ts +209 -0
  108. package/dist/gateway/routes/listeners.list.d.ts +14 -0
  109. package/dist/gateway/routes/listeners.restart.d.ts +34 -0
  110. package/dist/gateway/routes/listeners.start.d.ts +34 -0
  111. package/dist/gateway/routes/listeners.stop.d.ts +34 -0
  112. package/dist/gateway/routes/route-deps.d.ts +10 -0
  113. package/dist/gateway/routes/status.d.ts +30 -0
  114. package/dist/gateway/routes/validator.d.ts +19 -0
  115. package/dist/highlights-eq9cgrbb.scm +604 -0
  116. package/dist/highlights-ghv9g403.scm +205 -0
  117. package/dist/highlights-hk7bwhj4.scm +284 -0
  118. package/dist/highlights-r812a2qc.scm +150 -0
  119. package/dist/highlights-x6tmsnaa.scm +115 -0
  120. package/dist/index.d.ts +36 -0
  121. package/dist/index.js +3575 -0
  122. package/dist/injections-73j83es3.scm +27 -0
  123. package/dist/logger/leuco-human-file-writer.d.ts +33 -0
  124. package/dist/logger/leuco-human-logger.d.ts +46 -0
  125. package/dist/logger/leuco-human-record.d.ts +15 -0
  126. package/dist/logger/leuco-human-stdout-writer.d.ts +20 -0
  127. package/dist/logger/leuco-human-writer.d.ts +13 -0
  128. package/dist/logger/leuco-logger-memory-sink.d.ts +33 -0
  129. package/dist/logger/leuco-logger-record.d.ts +13 -0
  130. package/dist/logger/leuco-logger-sink.d.ts +34 -0
  131. package/dist/logger/leuco-logger-sqlite-sink.d.ts +102 -0
  132. package/dist/logger/leuco-logger.d.ts +56 -0
  133. package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  134. package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
  135. package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  136. package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  137. package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
  138. package/lib/bin.ts +78 -0
  139. package/lib/{modules → cli}/router/to-request.ts +13 -20
  140. package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +27 -0
  141. package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +40 -0
  142. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +41 -0
  143. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +22 -0
  144. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +23 -0
  145. package/lib/cli/routes/channels.$channel.connectors.$connector.ts +26 -0
  146. package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +92 -0
  147. package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +22 -0
  148. package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +63 -0
  149. package/lib/cli/routes/channels.$channel.connectors.ts +26 -0
  150. package/lib/cli/routes/channels.$channel.rename.$newName.ts +22 -0
  151. package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +34 -0
  152. package/lib/cli/routes/channels.$channel.ts +34 -0
  153. package/lib/cli/routes/channels.add.$channel.ts +33 -0
  154. package/lib/cli/routes/channels.remove.$channel.ts +20 -0
  155. package/lib/cli/routes/channels.ts +39 -0
  156. package/lib/cli/routes/claude.ts +69 -0
  157. package/lib/cli/routes/gateway.listeners.ts +41 -0
  158. package/lib/cli/routes/gateway.logs.ts +123 -0
  159. package/lib/{routes/gateway/restart.ts → cli/routes/gateway.restart.ts} +20 -5
  160. package/lib/cli/routes/gateway.run.ts +41 -0
  161. package/lib/cli/routes/gateway.start.ts +50 -0
  162. package/lib/cli/routes/gateway.status.ts +19 -0
  163. package/lib/cli/routes/gateway.stop.ts +32 -0
  164. package/lib/cli/routes/gateway.ts +55 -0
  165. package/lib/cli/routes/index.ts +202 -0
  166. package/lib/cli/routes/profiles.$profile.as-default.ts +22 -0
  167. package/lib/cli/routes/profiles.$profile.rename.$newName.ts +22 -0
  168. package/lib/cli/routes/profiles.$profile.run.ts +36 -0
  169. package/lib/cli/routes/profiles.add.$profile.ts +46 -0
  170. package/lib/cli/routes/profiles.remove.$profile.ts +20 -0
  171. package/lib/cli/routes/profiles.set.$profile.ts +46 -0
  172. package/lib/cli/routes/profiles.ts +40 -0
  173. package/lib/cli/routes/status.ts +93 -0
  174. package/lib/cli/routes/update.ts +27 -0
  175. package/lib/connectors/connector-config-schema.ts +16 -0
  176. package/lib/connectors/connector-factory.ts +94 -0
  177. package/lib/connectors/connector-listener.ts +20 -0
  178. package/lib/{modules/connectors/funnel-discord-adapter.ts → connectors/discord-adapter.ts} +6 -11
  179. package/lib/{modules/connectors → connectors}/discord-connector-schema.ts +4 -1
  180. package/lib/connectors/discord-listener.ts +111 -0
  181. package/lib/{modules/connectors/funnel-gh-adapter.ts → connectors/gh-adapter.ts} +3 -6
  182. package/lib/{modules/connectors → connectors}/gh-connector-schema.ts +4 -1
  183. package/lib/{modules/connectors/funnel-gh-listener.ts → connectors/gh-listener.ts} +45 -19
  184. package/lib/{modules/connectors → connectors}/match-cron.ts +10 -4
  185. package/lib/connectors/schedule-connector-schema.ts +33 -0
  186. package/lib/connectors/schedule-listener.ts +207 -0
  187. package/lib/connectors/schedule-state-store.ts +54 -0
  188. package/lib/connectors/slack-adapter.ts +36 -0
  189. package/lib/{modules/connectors → connectors}/slack-connector-schema.ts +4 -1
  190. package/lib/{modules/connectors/funnel-slack-event-processor.ts → connectors/slack-event-processor.ts} +15 -9
  191. package/lib/{modules/connectors/funnel-slack-listener.ts → connectors/slack-listener.ts} +33 -14
  192. package/lib/engine/channels/channels.ts +520 -0
  193. package/lib/{modules/claude/funnel-claude.ts → engine/claude/claude.ts} +28 -55
  194. package/lib/engine/claude/gateway-controller.ts +4 -0
  195. package/lib/{modules/fs/funnel-file-system.ts → engine/fs/file-system.ts} +4 -0
  196. package/lib/{modules/fs/memory-funnel-file-system.ts → engine/fs/memory-file-system.ts} +20 -3
  197. package/lib/{modules/fs/node-funnel-file-system.ts → engine/fs/node-file-system.ts} +14 -2
  198. package/lib/{modules/http/memory-funnel-http-client.ts → engine/http/memory-http-client.ts} +1 -5
  199. package/lib/{modules/http/node-funnel-http-client.ts → engine/http/node-http-client.ts} +1 -5
  200. package/lib/{modules/id/memory-funnel-id-generator.ts → engine/id/memory-id-generator.ts} +1 -1
  201. package/lib/{modules/id/node-funnel-id-generator.ts → engine/id/node-id-generator.ts} +1 -1
  202. package/lib/{modules/logger/memory-funnel-logger.ts → engine/logger/memory-logger.ts} +1 -1
  203. package/lib/{modules/logger/node-funnel-logger.ts → engine/logger/node-logger.ts} +1 -1
  204. package/lib/{modules/logger/noop-funnel-logger.ts → engine/logger/noop-logger.ts} +1 -1
  205. package/lib/engine/mcp/channel-server.ts +204 -0
  206. package/lib/{modules/mcp/funnel-mcp.ts → engine/mcp/mcp.ts} +24 -10
  207. package/lib/{modules/process/memory-funnel-process-runner.ts → engine/process/memory-process-runner.ts} +1 -1
  208. package/lib/{modules/process/node-funnel-process-runner.ts → engine/process/node-process-runner.ts} +12 -21
  209. package/lib/engine/profiles/profile-channel-checker.ts +7 -0
  210. package/lib/{modules/profiles/funnel-profiles.ts → engine/profiles/profiles.ts} +41 -43
  211. package/lib/{modules/settings/mock-funnel-settings-reader.ts → engine/settings/mock-settings-reader.ts} +4 -3
  212. package/lib/{modules/settings/funnel-settings-reader.ts → engine/settings/settings-reader.ts} +1 -1
  213. package/lib/engine/settings/settings-schema.ts +46 -0
  214. package/lib/engine/settings/settings-store.ts +110 -0
  215. package/lib/{modules/time/memory-funnel-clock.ts → engine/time/memory-clock.ts} +1 -1
  216. package/lib/{modules/time/node-funnel-clock.ts → engine/time/node-clock.ts} +1 -1
  217. package/lib/funnel.ts +83 -78
  218. package/lib/gateway/auth-middleware.ts +44 -0
  219. package/lib/gateway/broadcaster.ts +319 -0
  220. package/lib/gateway/daemon.ts +47 -0
  221. package/lib/gateway/factory.ts +10 -0
  222. package/lib/gateway/funnel-event-store.ts +155 -0
  223. package/lib/gateway/gateway-server.ts +414 -0
  224. package/lib/gateway/gateway-token.ts +79 -0
  225. package/lib/{modules/gateway/funnel-gateway.ts → gateway/gateway.ts} +27 -13
  226. package/lib/{modules/gateway → gateway}/kill-competing-slack-gateways.ts +4 -4
  227. package/lib/gateway/listener-supervisor.ts +339 -0
  228. package/lib/gateway/listeners-client.ts +128 -0
  229. package/lib/gateway/resolve-daemon-script.ts +26 -0
  230. package/lib/gateway/routes/channels.connectors.call.ts +39 -0
  231. package/lib/gateway/routes/health.ts +13 -0
  232. package/lib/gateway/routes/index.ts +24 -0
  233. package/lib/gateway/routes/listeners.list.ts +6 -0
  234. package/lib/gateway/routes/listeners.restart.ts +15 -0
  235. package/lib/gateway/routes/listeners.start.ts +15 -0
  236. package/lib/gateway/routes/listeners.stop.ts +15 -0
  237. package/lib/gateway/routes/route-deps.ts +11 -0
  238. package/lib/gateway/routes/status.ts +15 -0
  239. package/lib/gateway/routes/validator.ts +17 -0
  240. package/lib/index.ts +52 -92
  241. package/lib/logger/leuco-human-file-writer.ts +65 -0
  242. package/lib/logger/leuco-human-logger.ts +98 -0
  243. package/lib/logger/leuco-human-record.ts +16 -0
  244. package/lib/logger/leuco-human-stdout-writer.ts +26 -0
  245. package/lib/logger/leuco-human-writer.ts +14 -0
  246. package/lib/logger/leuco-logger-memory-sink.ts +67 -0
  247. package/lib/logger/leuco-logger-record.ts +13 -0
  248. package/lib/logger/leuco-logger-sink.ts +33 -0
  249. package/lib/logger/leuco-logger-sqlite-sink.ts +355 -0
  250. package/lib/logger/leuco-logger.ts +135 -0
  251. package/lib/tui/app.tsx +357 -0
  252. package/lib/tui/components/add-row.tsx +18 -0
  253. package/lib/tui/components/brand.tsx +27 -0
  254. package/lib/tui/components/card.tsx +44 -0
  255. package/lib/tui/components/detail-bar.tsx +46 -0
  256. package/lib/tui/components/editable-field.tsx +33 -0
  257. package/lib/tui/components/empty-state.tsx +11 -0
  258. package/lib/tui/components/gateway-status.tsx +66 -0
  259. package/lib/tui/components/keymap.tsx +29 -0
  260. package/lib/tui/components/menu-item.tsx +73 -0
  261. package/lib/tui/components/menu.tsx +26 -0
  262. package/lib/tui/components/panel-header.tsx +22 -0
  263. package/lib/tui/components/readonly-field.tsx +18 -0
  264. package/lib/tui/components/section-header.tsx +25 -0
  265. package/lib/tui/components/selection-accent.tsx +32 -0
  266. package/lib/tui/components/session-item.tsx +33 -0
  267. package/lib/tui/components/session-list.tsx +33 -0
  268. package/lib/tui/components/ui/hascii/accordion-item.tsx +88 -0
  269. package/lib/tui/components/ui/hascii/accordion.tsx +96 -0
  270. package/lib/tui/components/ui/hascii/alert-dialog.tsx +43 -0
  271. package/lib/tui/components/ui/hascii/badge.tsx +51 -0
  272. package/lib/tui/components/ui/hascii/breadcrumb.tsx +58 -0
  273. package/lib/tui/components/ui/hascii/button.tsx +194 -0
  274. package/lib/tui/components/ui/hascii/card-content.tsx +14 -0
  275. package/lib/tui/components/ui/hascii/card-description.tsx +13 -0
  276. package/lib/tui/components/ui/hascii/card-footer.tsx +14 -0
  277. package/lib/tui/components/ui/hascii/card-header.tsx +14 -0
  278. package/lib/tui/components/ui/hascii/card-title.tsx +13 -0
  279. package/lib/tui/components/ui/hascii/card.tsx +27 -0
  280. package/lib/tui/components/ui/hascii/checkbox.tsx +65 -0
  281. package/lib/tui/components/ui/hascii/command.tsx +159 -0
  282. package/lib/tui/components/ui/hascii/dialog-content.tsx +14 -0
  283. package/lib/tui/components/ui/hascii/dialog-description.tsx +13 -0
  284. package/lib/tui/components/ui/hascii/dialog-footer.tsx +14 -0
  285. package/lib/tui/components/ui/hascii/dialog-header.tsx +14 -0
  286. package/lib/tui/components/ui/hascii/dialog-title.tsx +13 -0
  287. package/lib/tui/components/ui/hascii/dialog.tsx +27 -0
  288. package/lib/tui/components/ui/hascii/file-tree.tsx +142 -0
  289. package/lib/tui/components/ui/hascii/focus-group.tsx +62 -0
  290. package/lib/tui/components/ui/hascii/form-item.tsx +43 -0
  291. package/lib/tui/components/ui/hascii/input-otp.tsx +86 -0
  292. package/lib/tui/components/ui/hascii/input.tsx +130 -0
  293. package/lib/tui/components/ui/hascii/pagination.tsx +105 -0
  294. package/lib/tui/components/ui/hascii/progress.tsx +28 -0
  295. package/lib/tui/components/ui/hascii/select.tsx +131 -0
  296. package/lib/tui/components/ui/hascii/separator.tsx +35 -0
  297. package/lib/tui/components/ui/hascii/sidebar-content.tsx +23 -0
  298. package/lib/tui/components/ui/hascii/sidebar-header.tsx +14 -0
  299. package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +67 -0
  300. package/lib/tui/components/ui/hascii/sidebar.tsx +24 -0
  301. package/lib/tui/components/ui/hascii/skeleton.tsx +60 -0
  302. package/lib/tui/components/ui/hascii/slider.tsx +91 -0
  303. package/lib/tui/components/ui/hascii/snackbar.tsx +75 -0
  304. package/lib/tui/components/ui/hascii/sparkline.tsx +53 -0
  305. package/lib/tui/components/ui/hascii/spinner.tsx +47 -0
  306. package/lib/tui/components/ui/hascii/stepper.tsx +54 -0
  307. package/lib/tui/components/ui/hascii/switch.tsx +66 -0
  308. package/lib/tui/components/ui/hascii/table.tsx +95 -0
  309. package/lib/tui/components/ui/hascii/tabs.tsx +59 -0
  310. package/lib/tui/components/ui/hascii/toggle-group-item.tsx +45 -0
  311. package/lib/tui/components/ui/hascii/toggle-group.tsx +99 -0
  312. package/lib/tui/components/ui/hascii/tree.tsx +104 -0
  313. package/lib/tui/components/view-shell.tsx +44 -0
  314. package/lib/tui/filter-input.tsx +33 -0
  315. package/lib/tui/hooks/hascii/use-pressable.ts +54 -0
  316. package/lib/tui/parse-comma-list.ts +14 -0
  317. package/lib/tui/profile-launcher.tsx +61 -0
  318. package/lib/tui/scrollbar-options.ts +19 -0
  319. package/lib/tui/sidebar.tsx +50 -0
  320. package/lib/tui/theme.ts +40 -0
  321. package/lib/tui/tui.tsx +20 -0
  322. package/lib/tui/types.ts +38 -0
  323. package/lib/tui/unique-name.ts +18 -0
  324. package/lib/tui/use-event-stream.ts +133 -0
  325. package/lib/tui/use-snapshot.ts +99 -0
  326. package/lib/tui/utils/hascii/form-item-context.tsx +23 -0
  327. package/lib/tui/utils/hascii/input-focus-context.tsx +31 -0
  328. package/lib/tui/utils/hascii/theme-context.tsx +26 -0
  329. package/lib/tui/utils/hascii/theme.ts +176 -0
  330. package/lib/tui/views/channels-view.tsx +108 -0
  331. package/lib/tui/views/connectors-view.tsx +164 -0
  332. package/lib/tui/views/events-view.tsx +160 -0
  333. package/lib/tui/views/listeners-view.tsx +80 -0
  334. package/lib/tui/views/profiles-view.tsx +152 -0
  335. package/package.json +55 -44
  336. package/lib/api.ts +0 -54
  337. package/lib/modules/channels/channel-connector-ref-updater.ts +0 -4
  338. package/lib/modules/channels/funnel-channels.ts +0 -160
  339. package/lib/modules/connectors/connector-config-schema.ts +0 -16
  340. package/lib/modules/connectors/connector-existence-checker.ts +0 -3
  341. package/lib/modules/connectors/funnel-callable-connector-store.ts +0 -9
  342. package/lib/modules/connectors/funnel-connector-listener.ts +0 -5
  343. package/lib/modules/connectors/funnel-connector-stores.ts +0 -52
  344. package/lib/modules/connectors/funnel-connector-type-store.ts +0 -24
  345. package/lib/modules/connectors/funnel-connectors.ts +0 -151
  346. package/lib/modules/connectors/funnel-discord-listener.ts +0 -71
  347. package/lib/modules/connectors/funnel-discord-store.ts +0 -88
  348. package/lib/modules/connectors/funnel-gh-store.ts +0 -101
  349. package/lib/modules/connectors/funnel-json-connector-store.ts +0 -100
  350. package/lib/modules/connectors/funnel-schedule-listener.ts +0 -130
  351. package/lib/modules/connectors/funnel-schedule-store.ts +0 -195
  352. package/lib/modules/connectors/funnel-slack-adapter.ts +0 -31
  353. package/lib/modules/connectors/funnel-slack-store.ts +0 -90
  354. package/lib/modules/connectors/migrate-legacy-connectors.ts +0 -81
  355. package/lib/modules/connectors/schedule-connector-schema.ts +0 -18
  356. package/lib/modules/connectors/schedule-last-fired-store.ts +0 -48
  357. package/lib/modules/gateway/daemon.ts +0 -74
  358. package/lib/modules/gateway/funnel-broadcaster.ts +0 -37
  359. package/lib/modules/gateway/funnel-event-logger.ts +0 -59
  360. package/lib/modules/gateway/funnel-gateway-server.ts +0 -241
  361. package/lib/modules/mcp/channel-server.ts +0 -76
  362. package/lib/modules/profiles/profile-channel-checker.ts +0 -3
  363. package/lib/modules/profiles/profile-channel-ref-updater.ts +0 -3
  364. package/lib/modules/repos/funnel-repositories.ts +0 -112
  365. package/lib/modules/schedule/funnel-schedule.ts +0 -39
  366. package/lib/modules/settings/funnel-settings-store.ts +0 -56
  367. package/lib/modules/settings/settings-schema.ts +0 -33
  368. package/lib/modules/tui/app.tsx +0 -44
  369. package/lib/modules/tui/tui.tsx +0 -13
  370. package/lib/routes/channels/add.help.ts +0 -3
  371. package/lib/routes/channels/add.ts +0 -21
  372. package/lib/routes/channels/connectors-attach.help.ts +0 -3
  373. package/lib/routes/channels/connectors-attach.ts +0 -17
  374. package/lib/routes/channels/connectors-detach.help.ts +0 -3
  375. package/lib/routes/channels/connectors-detach.ts +0 -17
  376. package/lib/routes/channels/group.help.ts +0 -16
  377. package/lib/routes/channels/group.ts +0 -22
  378. package/lib/routes/channels/remove.help.ts +0 -3
  379. package/lib/routes/channels/remove.ts +0 -17
  380. package/lib/routes/channels/rename.help.ts +0 -5
  381. package/lib/routes/channels/rename.ts +0 -17
  382. package/lib/routes/channels/routes.ts +0 -19
  383. package/lib/routes/channels/show.help.ts +0 -1
  384. package/lib/routes/channels/show.ts +0 -26
  385. package/lib/routes/claude/claude.help.ts +0 -16
  386. package/lib/routes/claude/claude.ts +0 -76
  387. package/lib/routes/claude/routes.ts +0 -4
  388. package/lib/routes/connectors/add.help.ts +0 -28
  389. package/lib/routes/connectors/add.ts +0 -64
  390. package/lib/routes/connectors/group.help.ts +0 -14
  391. package/lib/routes/connectors/group.ts +0 -18
  392. package/lib/routes/connectors/remove.help.ts +0 -3
  393. package/lib/routes/connectors/remove.ts +0 -17
  394. package/lib/routes/connectors/rename.help.ts +0 -5
  395. package/lib/routes/connectors/rename.ts +0 -17
  396. package/lib/routes/connectors/routes.ts +0 -23
  397. package/lib/routes/connectors/schedules-add.help.ts +0 -11
  398. package/lib/routes/connectors/schedules-add.ts +0 -33
  399. package/lib/routes/connectors/schedules-group.help.ts +0 -1
  400. package/lib/routes/connectors/schedules-group.ts +0 -38
  401. package/lib/routes/connectors/schedules-remove.help.ts +0 -3
  402. package/lib/routes/connectors/schedules-remove.ts +0 -17
  403. package/lib/routes/connectors/set.help.ts +0 -8
  404. package/lib/routes/connectors/set.ts +0 -72
  405. package/lib/routes/connectors/show.help.ts +0 -1
  406. package/lib/routes/connectors/show.ts +0 -41
  407. package/lib/routes/gateway/group.help.ts +0 -15
  408. package/lib/routes/gateway/group.ts +0 -28
  409. package/lib/routes/gateway/logs.help.ts +0 -13
  410. package/lib/routes/gateway/logs.ts +0 -102
  411. package/lib/routes/gateway/restart.help.ts +0 -10
  412. package/lib/routes/gateway/routes.ts +0 -18
  413. package/lib/routes/gateway/run.help.ts +0 -12
  414. package/lib/routes/gateway/run.ts +0 -35
  415. package/lib/routes/gateway/start.help.ts +0 -15
  416. package/lib/routes/gateway/start.ts +0 -32
  417. package/lib/routes/gateway/status.help.ts +0 -9
  418. package/lib/routes/gateway/status.ts +0 -28
  419. package/lib/routes/gateway/stop.help.ts +0 -8
  420. package/lib/routes/gateway/stop.ts +0 -21
  421. package/lib/routes/profiles/add.help.ts +0 -3
  422. package/lib/routes/profiles/add.ts +0 -33
  423. package/lib/routes/profiles/group.help.ts +0 -16
  424. package/lib/routes/profiles/group.ts +0 -25
  425. package/lib/routes/profiles/launch.help.ts +0 -4
  426. package/lib/routes/profiles/launch.ts +0 -36
  427. package/lib/routes/profiles/remove.help.ts +0 -3
  428. package/lib/routes/profiles/remove.ts +0 -17
  429. package/lib/routes/profiles/rename.help.ts +0 -5
  430. package/lib/routes/profiles/rename.ts +0 -17
  431. package/lib/routes/profiles/routes.ts +0 -18
  432. package/lib/routes/profiles/set.help.ts +0 -5
  433. package/lib/routes/profiles/set.ts +0 -32
  434. package/lib/routes/repos/add.help.ts +0 -6
  435. package/lib/routes/repos/add.ts +0 -20
  436. package/lib/routes/repos/group.help.ts +0 -11
  437. package/lib/routes/repos/group.ts +0 -18
  438. package/lib/routes/repos/remove.help.ts +0 -3
  439. package/lib/routes/repos/remove.ts +0 -17
  440. package/lib/routes/repos/rename.help.ts +0 -5
  441. package/lib/routes/repos/rename.ts +0 -17
  442. package/lib/routes/repos/routes.ts +0 -17
  443. package/lib/routes/repos/set.help.ts +0 -5
  444. package/lib/routes/repos/set.ts +0 -21
  445. package/lib/routes/repos/show.help.ts +0 -1
  446. package/lib/routes/repos/show.ts +0 -19
  447. package/lib/routes/request/discord-help.ts +0 -9
  448. package/lib/routes/request/discord.help.ts +0 -19
  449. package/lib/routes/request/discord.ts +0 -65
  450. package/lib/routes/request/group.help.ts +0 -15
  451. package/lib/routes/request/group.ts +0 -9
  452. package/lib/routes/request/routes.ts +0 -14
  453. package/lib/routes/request/slack-help.ts +0 -9
  454. package/lib/routes/request/slack.help.ts +0 -19
  455. package/lib/routes/request/slack.ts +0 -61
  456. package/lib/routes/status/routes.ts +0 -4
  457. package/lib/routes/status/status.help.ts +0 -6
  458. package/lib/routes/status/status.ts +0 -77
  459. package/lib/routes/update/routes.ts +0 -4
  460. package/lib/routes/update/update.help.ts +0 -5
  461. package/lib/routes/update/update.ts +0 -21
  462. package/lib/routes.ts +0 -40
  463. /package/lib/{factory.ts → cli/factory.ts} +0 -0
  464. /package/lib/{modules → cli}/router/query-to-cli-args.ts +0 -0
  465. /package/lib/{modules → cli}/router/validator.ts +0 -0
  466. /package/lib/{modules/connectors/funnel-connector-adapter.ts → connectors/connector-adapter.ts} +0 -0
  467. /package/lib/{modules/connectors/funnel-discord-event-processor.ts → connectors/discord-event-processor.ts} +0 -0
  468. /package/lib/{modules/http/funnel-http-client.ts → engine/http/http-client.ts} +0 -0
  469. /package/lib/{modules/id/funnel-id-generator.ts → engine/id/id-generator.ts} +0 -0
  470. /package/lib/{modules/logger/funnel-logger.ts → engine/logger/logger.ts} +0 -0
  471. /package/lib/{modules/process/funnel-process-runner.ts → engine/process/process-runner.ts} +0 -0
  472. /package/lib/{modules/time/funnel-clock.ts → engine/time/clock.ts} +0 -0
package/dist/index.js ADDED
@@ -0,0 +1,3575 @@
1
+ // @bun
2
+ // lib/funnel.ts
3
+ import { join as join9 } from "path";
4
+
5
+ // lib/connectors/connector-adapter.ts
6
+ class FunnelConnectorAdapter {
7
+ }
8
+
9
+ // lib/engine/http/http-client.ts
10
+ class FunnelHttpClient {
11
+ }
12
+
13
+ // lib/engine/http/node-http-client.ts
14
+ class NodeFunnelHttpClient extends FunnelHttpClient {
15
+ constructor() {
16
+ super();
17
+ Object.freeze(this);
18
+ }
19
+ async fetch(request) {
20
+ const res = await globalThis.fetch(request.url, {
21
+ method: request.method,
22
+ headers: request.headers,
23
+ body: request.body
24
+ });
25
+ return {
26
+ status: res.status,
27
+ ok: res.ok,
28
+ text: () => res.text(),
29
+ json: () => res.json()
30
+ };
31
+ }
32
+ }
33
+
34
+ // lib/connectors/discord-adapter.ts
35
+ var DISCORD_API_BASE = "https://discord.com/api/v10";
36
+ var defaultHttp = new NodeFunnelHttpClient;
37
+
38
+ class FunnelDiscordAdapter extends FunnelConnectorAdapter {
39
+ token;
40
+ http;
41
+ constructor(deps) {
42
+ super();
43
+ this.token = deps.config.botToken;
44
+ this.http = deps.http ?? defaultHttp;
45
+ Object.freeze(this);
46
+ }
47
+ async call(input) {
48
+ const method = (input.method || "GET").toUpperCase();
49
+ const path = input.path.startsWith("/") ? input.path : `/${input.path}`;
50
+ const body = input.body;
51
+ const hasBody = body !== null && typeof body === "object" && method !== "GET" && Object.keys(body).length > 0;
52
+ const res = await this.http.fetch({
53
+ method,
54
+ url: `${DISCORD_API_BASE}${path}`,
55
+ headers: {
56
+ Authorization: `Bot ${this.token}`,
57
+ "Content-Type": "application/json"
58
+ },
59
+ body: hasBody ? JSON.stringify(input.body) : undefined
60
+ });
61
+ if (!res.ok) {
62
+ throw new Error(`Discord API failed (${res.status}): ${await res.text()}`);
63
+ }
64
+ if (res.status === 204)
65
+ return null;
66
+ return await res.json();
67
+ }
68
+ }
69
+
70
+ // lib/connectors/discord-listener.ts
71
+ import { Client, GatewayIntentBits, Partials } from "discord.js";
72
+
73
+ // lib/connectors/connector-listener.ts
74
+ class FunnelConnectorListener {
75
+ isAlive() {
76
+ return true;
77
+ }
78
+ }
79
+
80
+ // lib/connectors/discord-event-processor.ts
81
+ class FunnelDiscordEventProcessor {
82
+ ownUserId;
83
+ constructor(props) {
84
+ this.ownUserId = props.ownUserId;
85
+ }
86
+ process(message) {
87
+ if (message.authorIsBot)
88
+ return { skip: true };
89
+ const mentioned = this.ownUserId ? message.mentionedUserIds.includes(this.ownUserId) : false;
90
+ return {
91
+ skip: false,
92
+ content: JSON.stringify(message.raw),
93
+ meta: {
94
+ event_type: "discord",
95
+ channel_id: message.channelId,
96
+ user_id: message.authorId,
97
+ mentioned: String(mentioned),
98
+ guild_id: message.guildId ?? ""
99
+ }
100
+ };
101
+ }
102
+ }
103
+
104
+ // lib/engine/logger/node-logger.ts
105
+ import { appendFileSync, mkdirSync } from "fs";
106
+ import { dirname, join } from "path";
107
+
108
+ // lib/engine/logger/logger.ts
109
+ class FunnelLogger {
110
+ }
111
+
112
+ // lib/engine/logger/node-logger.ts
113
+ var DEFAULT_LOG_FILE = join("/tmp/funnel", "funnel.log");
114
+
115
+ class NodeFunnelLogger extends FunnelLogger {
116
+ file;
117
+ now;
118
+ constructor(props = {}) {
119
+ super();
120
+ this.file = props.file ?? DEFAULT_LOG_FILE;
121
+ this.now = props.now ?? (() => new Date);
122
+ Object.freeze(this);
123
+ }
124
+ info(message, meta) {
125
+ this.write("info", message, meta);
126
+ }
127
+ warn(message, meta) {
128
+ this.write("warn", message, meta);
129
+ }
130
+ error(message, meta) {
131
+ this.write("error", message, meta);
132
+ }
133
+ write(level, message, meta) {
134
+ mkdirSync(dirname(this.file), { recursive: true });
135
+ const entry = {
136
+ time: this.now().toISOString(),
137
+ level,
138
+ message,
139
+ ...meta ? { meta } : {}
140
+ };
141
+ appendFileSync(this.file, `${JSON.stringify(entry)}
142
+ `);
143
+ }
144
+ }
145
+
146
+ // lib/connectors/discord-listener.ts
147
+ var defaultLogger = new NodeFunnelLogger;
148
+
149
+ class FunnelDiscordListener extends FunnelConnectorListener {
150
+ config;
151
+ logger;
152
+ client = null;
153
+ constructor(deps) {
154
+ super();
155
+ this.config = deps.config;
156
+ this.logger = deps.logger ?? defaultLogger;
157
+ }
158
+ async start(notify) {
159
+ const client = new Client({
160
+ intents: [
161
+ GatewayIntentBits.Guilds,
162
+ GatewayIntentBits.GuildMessages,
163
+ GatewayIntentBits.MessageContent,
164
+ GatewayIntentBits.DirectMessages
165
+ ],
166
+ partials: [Partials.Channel]
167
+ });
168
+ client.on("messageCreate", async (message) => {
169
+ const ownUserId = client.user?.id ?? "";
170
+ const mentionedUserIds = [...message.mentions.users.keys()];
171
+ this.logger.info("discord messageCreate", {
172
+ author: message.author.id,
173
+ authorIsBot: String(message.author.bot),
174
+ channelId: message.channelId,
175
+ guildId: message.guildId ?? "",
176
+ mentions: mentionedUserIds.join(","),
177
+ ownUserId,
178
+ mentioned: String(mentionedUserIds.includes(ownUserId))
179
+ });
180
+ const processor = new FunnelDiscordEventProcessor({ ownUserId });
181
+ const result = processor.process({
182
+ authorId: message.author.id,
183
+ authorIsBot: message.author.bot,
184
+ channelId: message.channelId,
185
+ guildId: message.guildId,
186
+ mentionedUserIds,
187
+ raw: message.toJSON()
188
+ });
189
+ if (result.skip) {
190
+ this.logger.info("discord skip", { reason: "bot author" });
191
+ return;
192
+ }
193
+ try {
194
+ await notify(result.content, result.meta);
195
+ } catch (error) {
196
+ this.logger.error("discord notify error", {
197
+ error: error instanceof Error ? error.message : String(error)
198
+ });
199
+ }
200
+ });
201
+ client.on("ready", (readyClient) => {
202
+ this.logger.info("discord ready", {
203
+ userId: readyClient.user.id,
204
+ tag: readyClient.user.tag,
205
+ guilds: String(readyClient.guilds.cache.size)
206
+ });
207
+ });
208
+ client.on("error", (error) => {
209
+ this.logger.error("discord client error", {
210
+ error: error instanceof Error ? error.message : String(error)
211
+ });
212
+ });
213
+ await client.login(this.config.botToken);
214
+ this.client = client;
215
+ }
216
+ async stop() {
217
+ if (!this.client)
218
+ return;
219
+ try {
220
+ await this.client.destroy();
221
+ } catch (error) {
222
+ this.logger.error("discord stop error", {
223
+ error: error instanceof Error ? error.message : String(error)
224
+ });
225
+ } finally {
226
+ this.client = null;
227
+ }
228
+ }
229
+ isAlive() {
230
+ return this.client !== null;
231
+ }
232
+ }
233
+
234
+ // lib/engine/process/process-runner.ts
235
+ class FunnelProcessRunner {
236
+ }
237
+
238
+ // lib/engine/process/node-process-runner.ts
239
+ var toEnv = (env) => {
240
+ if (!env)
241
+ return;
242
+ const merged = {};
243
+ for (const [key, value] of Object.entries(process.env)) {
244
+ if (typeof value === "string")
245
+ merged[key] = value;
246
+ }
247
+ for (const [key, value] of Object.entries(env)) {
248
+ merged[key] = value;
249
+ }
250
+ return merged;
251
+ };
252
+
253
+ class NodeFunnelProcessRunner extends FunnelProcessRunner {
254
+ constructor() {
255
+ super();
256
+ Object.freeze(this);
257
+ }
258
+ runSync(command) {
259
+ const result = Bun.spawnSync(command, {
260
+ stdout: "pipe",
261
+ stderr: "pipe"
262
+ });
263
+ return {
264
+ exitCode: result.exitCode ?? 0,
265
+ stdout: result.stdout.toString(),
266
+ stderr: result.stderr.toString()
267
+ };
268
+ }
269
+ async run(command, options = {}) {
270
+ const proc = Bun.spawn(command, {
271
+ cwd: options.cwd,
272
+ env: toEnv(options.env),
273
+ stdin: options.input !== undefined ? "pipe" : "ignore",
274
+ stdout: "pipe",
275
+ stderr: "pipe"
276
+ });
277
+ if (options.input !== undefined && proc.stdin) {
278
+ proc.stdin.write(options.input);
279
+ proc.stdin.end();
280
+ }
281
+ const exitCode = await proc.exited;
282
+ const stdout = await new Response(proc.stdout).text();
283
+ const stderr = await new Response(proc.stderr).text();
284
+ return { exitCode, stdout, stderr };
285
+ }
286
+ async attach(command, options = {}) {
287
+ const proc = Bun.spawn(command, {
288
+ cwd: options.cwd,
289
+ env: toEnv(options.env),
290
+ stdio: ["inherit", "inherit", "inherit"]
291
+ });
292
+ return await proc.exited;
293
+ }
294
+ detach(command, options = {}) {
295
+ const proc = Bun.spawn(command, {
296
+ env: toEnv(options.env),
297
+ stdio: ["ignore", "ignore", "ignore"]
298
+ });
299
+ proc.unref();
300
+ }
301
+ kill(pid, signal = "SIGTERM") {
302
+ try {
303
+ process.kill(pid, signal);
304
+ } catch {}
305
+ }
306
+ }
307
+
308
+ // lib/connectors/gh-adapter.ts
309
+ var defaultProcess = new NodeFunnelProcessRunner;
310
+
311
+ class FunnelGhAdapter extends FunnelConnectorAdapter {
312
+ process;
313
+ constructor(deps = {}) {
314
+ super();
315
+ this.process = deps.process ?? defaultProcess;
316
+ Object.freeze(this);
317
+ }
318
+ async call(input) {
319
+ const args = ["api", input.path];
320
+ if (input.method && input.method.toLowerCase() !== "get") {
321
+ args.push("-X", input.method.toUpperCase());
322
+ }
323
+ const hasBody = input.body && typeof input.body === "object" && Object.keys(input.body).length > 0;
324
+ if (hasBody) {
325
+ args.push("--input", "-");
326
+ }
327
+ const result = await this.process.run(["gh", ...args], {
328
+ input: hasBody ? JSON.stringify(input.body) : undefined
329
+ });
330
+ if (result.exitCode !== 0) {
331
+ throw new Error(`gh api failed: ${result.stderr.trim() || result.stdout.trim()}`);
332
+ }
333
+ try {
334
+ return JSON.parse(result.stdout);
335
+ } catch {
336
+ return result.stdout;
337
+ }
338
+ }
339
+ }
340
+
341
+ // lib/connectors/gh-listener.ts
342
+ import { z } from "zod";
343
+ var ghNotificationSchema = z.object({
344
+ id: z.string(),
345
+ reason: z.string(),
346
+ subject: z.object({
347
+ type: z.string(),
348
+ url: z.string(),
349
+ title: z.string()
350
+ }),
351
+ repository: z.object({ full_name: z.string() }),
352
+ updated_at: z.string()
353
+ });
354
+ var ghNotificationsSchema = z.array(ghNotificationSchema);
355
+ var defaultProcess2 = new NodeFunnelProcessRunner;
356
+ var defaultLogger2 = new NodeFunnelLogger;
357
+ var MAX_SEEN = 1e4;
358
+ var KEEP_SEEN = 5000;
359
+
360
+ class FunnelGhListener extends FunnelConnectorListener {
361
+ config;
362
+ process;
363
+ logger;
364
+ now;
365
+ seen = new Map;
366
+ bootstrapped = false;
367
+ since;
368
+ timer = null;
369
+ constructor(deps) {
370
+ super();
371
+ this.config = deps.config;
372
+ this.process = deps.process ?? defaultProcess2;
373
+ this.logger = deps.logger ?? defaultLogger2;
374
+ this.now = deps.now ?? (() => new Date);
375
+ this.since = this.now().toISOString();
376
+ }
377
+ async start(notify) {
378
+ await this.pollOnce(notify);
379
+ const interval = this.config.pollInterval ?? 60;
380
+ this.timer = setInterval(() => void this.pollOnce(notify), interval * 1000);
381
+ this.timer.unref();
382
+ }
383
+ async stop() {
384
+ if (!this.timer)
385
+ return;
386
+ clearInterval(this.timer);
387
+ this.timer = null;
388
+ }
389
+ isAlive() {
390
+ return this.timer !== null;
391
+ }
392
+ async pollOnce(notify) {
393
+ const nextSince = this.now().toISOString();
394
+ const params = new URLSearchParams({ since: this.since, all: "false" });
395
+ try {
396
+ const result = await this.process.run(["gh", "api", `/notifications?${params}`]);
397
+ if (result.exitCode !== 0) {
398
+ this.logger.error("gh poll failed", { stderr: result.stderr });
399
+ return;
400
+ }
401
+ const parsed = ghNotificationsSchema.safeParse(JSON.parse(result.stdout));
402
+ if (!parsed.success) {
403
+ this.logger.warn("gh response did not match schema", { error: parsed.error.message });
404
+ return;
405
+ }
406
+ const items = parsed.data;
407
+ for (const item of items) {
408
+ if (this.seen.get(item.id) === item.updated_at)
409
+ continue;
410
+ this.seen.set(item.id, item.updated_at);
411
+ if (!this.bootstrapped)
412
+ continue;
413
+ const meta = {
414
+ event_type: "gh",
415
+ reason: item.reason,
416
+ subject_type: item.subject.type,
417
+ subject_url: item.subject.url,
418
+ repository: item.repository.full_name,
419
+ thread_id: item.id,
420
+ updated_at: item.updated_at
421
+ };
422
+ await notify(JSON.stringify(item), meta);
423
+ }
424
+ if (this.seen.size > MAX_SEEN) {
425
+ const toDrop = this.seen.size - KEEP_SEEN;
426
+ let dropped = 0;
427
+ for (const key of this.seen.keys()) {
428
+ if (dropped >= toDrop)
429
+ break;
430
+ this.seen.delete(key);
431
+ dropped++;
432
+ }
433
+ }
434
+ this.since = nextSince;
435
+ this.bootstrapped = true;
436
+ } catch (error) {
437
+ this.logger.error("gh poll error", {
438
+ error: error instanceof Error ? error.message : String(error)
439
+ });
440
+ }
441
+ }
442
+ }
443
+
444
+ // lib/connectors/match-cron.ts
445
+ var parseField = (expr, min, max) => {
446
+ const values = new Set;
447
+ for (const part of expr.split(",")) {
448
+ const [rangePart, stepPart] = part.split("/");
449
+ const step = stepPart ? Number(stepPart) : 1;
450
+ if (!Number.isFinite(step) || step <= 0) {
451
+ throw new Error(`invalid cron step: "${stepPart}"`);
452
+ }
453
+ let lo = min;
454
+ let hi = max;
455
+ if (rangePart === "*" || rangePart === undefined || rangePart === "") {
456
+ lo = min;
457
+ hi = max;
458
+ } else if (rangePart.includes("-")) {
459
+ const [aStr, bStr] = rangePart.split("-");
460
+ const a = Number(aStr);
461
+ const b = Number(bStr);
462
+ if (!Number.isFinite(a) || !Number.isFinite(b)) {
463
+ throw new Error(`invalid cron range: "${rangePart}"`);
464
+ }
465
+ lo = a;
466
+ hi = b;
467
+ } else {
468
+ const n = Number(rangePart);
469
+ if (!Number.isFinite(n))
470
+ throw new Error(`invalid cron value: "${rangePart}"`);
471
+ lo = n;
472
+ hi = stepPart ? max : n;
473
+ }
474
+ if (lo < min || hi > max || lo > hi) {
475
+ throw new Error(`cron value out of range: ${rangePart} (must be ${min}-${max})`);
476
+ }
477
+ for (let i = lo;i <= hi; i += step) {
478
+ values.add(i);
479
+ }
480
+ }
481
+ return { min, max, values };
482
+ };
483
+ var matchCron = (expr, date) => {
484
+ const parts = expr.trim().split(/\s+/);
485
+ if (parts.length !== 5) {
486
+ throw new Error(`cron must have 5 fields (got ${parts.length}): "${expr}"`);
487
+ }
488
+ const [minute, hour, dom, month, dow] = parts;
489
+ if (!minute || !hour || !dom || !month || !dow) {
490
+ throw new Error(`cron has empty fields: "${expr}"`);
491
+ }
492
+ const fields = [
493
+ { field: parseField(minute, 0, 59), value: date.getMinutes() },
494
+ { field: parseField(hour, 0, 23), value: date.getHours() },
495
+ { field: parseField(dom, 1, 31), value: date.getDate() },
496
+ { field: parseField(month, 1, 12), value: date.getMonth() + 1 },
497
+ { field: parseField(dow, 0, 6), value: date.getDay() }
498
+ ];
499
+ for (const { field, value } of fields) {
500
+ if (!field.values.has(value))
501
+ return false;
502
+ }
503
+ return true;
504
+ };
505
+
506
+ // lib/connectors/schedule-listener.ts
507
+ var defaultLogger3 = new NodeFunnelLogger;
508
+ var MAX_CATCHUP_MINUTES = 60 * 24;
509
+
510
+ class FunnelScheduleListener extends FunnelConnectorListener {
511
+ config;
512
+ lastFiredStore;
513
+ logger;
514
+ now;
515
+ timer = null;
516
+ stopped = false;
517
+ constructor(deps) {
518
+ super();
519
+ this.config = deps.config;
520
+ this.lastFiredStore = deps.lastFiredStore;
521
+ this.logger = deps.logger ?? defaultLogger3;
522
+ this.now = deps.now ?? (() => new Date);
523
+ }
524
+ async start(notify) {
525
+ this.stopped = false;
526
+ const scheduleNext = () => {
527
+ if (this.stopped)
528
+ return;
529
+ const date = this.now();
530
+ const msUntilNextMinute = 60000 - (date.getSeconds() * 1000 + date.getMilliseconds());
531
+ this.timer = setTimeout(async () => {
532
+ if (this.stopped)
533
+ return;
534
+ await this.tick(notify);
535
+ scheduleNext();
536
+ }, msUntilNextMinute);
537
+ this.timer.unref();
538
+ };
539
+ await this.tick(notify);
540
+ scheduleNext();
541
+ }
542
+ async stop() {
543
+ this.stopped = true;
544
+ if (this.timer) {
545
+ clearTimeout(this.timer);
546
+ this.timer = null;
547
+ }
548
+ }
549
+ isAlive() {
550
+ return !this.stopped && this.timer !== null;
551
+ }
552
+ async tick(notify) {
553
+ const now = this.truncateToMinute(this.now());
554
+ const state = this.lastFiredStore.load();
555
+ let changed = false;
556
+ for (const entry of this.config.entries) {
557
+ if (!entry.enabled)
558
+ continue;
559
+ const fired = await this.fireEntry(entry, now, state, notify);
560
+ if (fired)
561
+ changed = true;
562
+ }
563
+ if (changed)
564
+ this.lastFiredStore.save(state);
565
+ }
566
+ async fireEntry(entry, now, state, notify) {
567
+ const lastFired = state.get(entry.id);
568
+ const searchFrom = lastFired ? new Date(lastFired.getTime() + 60000) : now;
569
+ if (searchFrom.getTime() > now.getTime())
570
+ return false;
571
+ if (entry.catchupPolicy === "skip") {
572
+ try {
573
+ if (!matchCron(entry.cron, now))
574
+ return false;
575
+ } catch (error) {
576
+ this.logInvalidCron(entry, error);
577
+ return false;
578
+ }
579
+ await this.notifyOne(entry, now, notify, false);
580
+ state.set(entry.id, now);
581
+ return true;
582
+ }
583
+ if (entry.catchupPolicy === "all") {
584
+ const matches = this.findAllMatches(entry.cron, searchFrom, now, entry.id);
585
+ if (matches.length === 0)
586
+ return false;
587
+ for (const match2 of matches) {
588
+ await this.notifyOne(entry, match2, notify, match2.getTime() !== now.getTime());
589
+ }
590
+ state.set(entry.id, matches[matches.length - 1] ?? now);
591
+ return true;
592
+ }
593
+ const match = this.findMostRecentMatch(entry.cron, searchFrom, now, entry.id);
594
+ if (!match)
595
+ return false;
596
+ await this.notifyOne(entry, match, notify, match.getTime() !== now.getTime());
597
+ state.set(entry.id, match);
598
+ return true;
599
+ }
600
+ async notifyOne(entry, firedAt, notify, catchup) {
601
+ const meta = {
602
+ event_type: "schedule",
603
+ schedule_id: entry.id,
604
+ cron: entry.cron,
605
+ fired_at: firedAt.toISOString(),
606
+ catchup_policy: entry.catchupPolicy
607
+ };
608
+ if (catchup)
609
+ meta.catchup = "true";
610
+ await notify(entry.prompt, meta);
611
+ }
612
+ findMostRecentMatch(cron, from, until, entryId) {
613
+ const maxIterations = Math.min(MAX_CATCHUP_MINUTES, Math.floor((until.getTime() - from.getTime()) / 60000) + 1);
614
+ for (let i = 0;i < maxIterations; i++) {
615
+ const candidate = new Date(until.getTime() - i * 60000);
616
+ try {
617
+ if (matchCron(cron, candidate))
618
+ return candidate;
619
+ } catch (error) {
620
+ this.logInvalidCron({ id: entryId, cron }, error);
621
+ return null;
622
+ }
623
+ }
624
+ return null;
625
+ }
626
+ findAllMatches(cron, from, until, entryId) {
627
+ const maxIterations = Math.min(MAX_CATCHUP_MINUTES, Math.floor((until.getTime() - from.getTime()) / 60000) + 1);
628
+ const matches = [];
629
+ for (let i = 0;i < maxIterations; i++) {
630
+ const candidate = new Date(from.getTime() + i * 60000);
631
+ if (candidate.getTime() > until.getTime())
632
+ break;
633
+ try {
634
+ if (matchCron(cron, candidate))
635
+ matches.push(candidate);
636
+ } catch (error) {
637
+ this.logInvalidCron({ id: entryId, cron }, error);
638
+ return [];
639
+ }
640
+ }
641
+ return matches;
642
+ }
643
+ logInvalidCron(entry, error) {
644
+ this.logger.error("invalid cron expression in schedule", {
645
+ connector: this.config.name,
646
+ id: entry.id,
647
+ cron: entry.cron,
648
+ error: error instanceof Error ? error.message : String(error)
649
+ });
650
+ }
651
+ truncateToMinute(date) {
652
+ const copy = new Date(date.getTime());
653
+ copy.setSeconds(0, 0);
654
+ return copy;
655
+ }
656
+ }
657
+
658
+ // lib/connectors/schedule-state-store.ts
659
+ import { dirname as dirname2 } from "path";
660
+
661
+ // lib/engine/fs/node-file-system.ts
662
+ import {
663
+ appendFileSync as appendFileSync2,
664
+ chmodSync,
665
+ existsSync,
666
+ mkdirSync as mkdirSync2,
667
+ readdirSync,
668
+ readFileSync,
669
+ statSync,
670
+ unlinkSync,
671
+ writeFileSync
672
+ } from "fs";
673
+
674
+ // lib/engine/fs/file-system.ts
675
+ class FunnelFileSystem {
676
+ }
677
+
678
+ // lib/engine/fs/node-file-system.ts
679
+ var SECRET_MODE = 384;
680
+
681
+ class NodeFunnelFileSystem extends FunnelFileSystem {
682
+ constructor() {
683
+ super();
684
+ Object.freeze(this);
685
+ }
686
+ existsSync(path) {
687
+ return existsSync(path);
688
+ }
689
+ readFileSync(path) {
690
+ return readFileSync(path, "utf-8");
691
+ }
692
+ writeFileSync(path, data) {
693
+ writeFileSync(path, data);
694
+ }
695
+ writeSecretFileSync(path, data) {
696
+ writeFileSync(path, data, { mode: SECRET_MODE });
697
+ try {
698
+ chmodSync(path, SECRET_MODE);
699
+ } catch {}
700
+ }
701
+ appendFileSync(path, data) {
702
+ appendFileSync2(path, data);
703
+ }
704
+ unlink(path) {
705
+ try {
706
+ unlinkSync(path);
707
+ } catch {}
708
+ }
709
+ mkdirSync(path, options) {
710
+ mkdirSync2(path, { recursive: options?.recursive ?? false });
711
+ }
712
+ readdirSync(path) {
713
+ return readdirSync(path);
714
+ }
715
+ statSync(path) {
716
+ const stat = statSync(path);
717
+ return { mtimeMs: stat.mtimeMs, mode: stat.mode & 511 };
718
+ }
719
+ }
720
+
721
+ // lib/connectors/schedule-state-store.ts
722
+ var defaultFs = new NodeFunnelFileSystem;
723
+
724
+ class ScheduleStateStore {
725
+ path;
726
+ fs;
727
+ constructor(deps) {
728
+ this.path = deps.path;
729
+ this.fs = deps.fs ?? defaultFs;
730
+ Object.freeze(this);
731
+ }
732
+ load() {
733
+ const map = new Map;
734
+ if (!this.fs.existsSync(this.path))
735
+ return map;
736
+ const raw = JSON.parse(this.fs.readFileSync(this.path));
737
+ if (raw === null || typeof raw !== "object")
738
+ return map;
739
+ for (const [id, iso] of Object.entries(raw)) {
740
+ if (typeof iso === "string")
741
+ map.set(id, new Date(iso));
742
+ }
743
+ return map;
744
+ }
745
+ save(state) {
746
+ const obj = {};
747
+ for (const [id, date] of state) {
748
+ obj[id] = date.toISOString();
749
+ }
750
+ this.fs.mkdirSync(dirname2(this.path), { recursive: true });
751
+ this.fs.writeFileSync(this.path, `${JSON.stringify(obj, null, 2)}
752
+ `);
753
+ }
754
+ }
755
+
756
+ // lib/connectors/slack-adapter.ts
757
+ import { WebClient } from "@slack/web-api";
758
+ var toRecord = (value) => {
759
+ const result = {};
760
+ for (const [key, val] of Object.entries(value))
761
+ result[key] = val;
762
+ return result;
763
+ };
764
+
765
+ class FunnelSlackAdapter extends FunnelConnectorAdapter {
766
+ client;
767
+ constructor(deps) {
768
+ super();
769
+ this.client = deps.client ?? new WebClient(deps.config.botToken);
770
+ Object.freeze(this);
771
+ }
772
+ async call(input) {
773
+ const body = input.body !== null && typeof input.body === "object" ? toRecord(input.body) : {};
774
+ return await this.client.apiCall(input.path, body);
775
+ }
776
+ }
777
+
778
+ // lib/connectors/slack-listener.ts
779
+ import { App, LogLevel } from "@slack/bolt";
780
+ import { z as z2 } from "zod";
781
+
782
+ // lib/connectors/slack-event-processor.ts
783
+ var ALLOWED_EVENTS = new Set(["message", "app_mention"]);
784
+ var ALLOWED_SUBTYPES = new Set([
785
+ undefined,
786
+ "thread_broadcast",
787
+ "bot_message",
788
+ "file_share"
789
+ ]);
790
+ var DEDUP_WINDOW = 1e4;
791
+ var getString = (event, key) => {
792
+ const value = event[key];
793
+ return typeof value === "string" ? value : undefined;
794
+ };
795
+
796
+ class FunnelSlackEventProcessor {
797
+ ownBotUserId;
798
+ ownBotId;
799
+ now;
800
+ dedup = new Map;
801
+ constructor(props) {
802
+ this.ownBotUserId = props.ownBotUserId;
803
+ this.ownBotId = props.ownBotId;
804
+ this.now = props.now ?? (() => Date.now());
805
+ }
806
+ process(event) {
807
+ const eventType = getString(event, "type");
808
+ if (!eventType || !ALLOWED_EVENTS.has(eventType))
809
+ return { skip: true };
810
+ const subtype = getString(event, "subtype");
811
+ if (!ALLOWED_SUBTYPES.has(subtype))
812
+ return { skip: true };
813
+ const channelId = getString(event, "channel") ?? "";
814
+ const eventTs = getString(event, "event_ts") ?? getString(event, "ts") ?? "";
815
+ const dedupKey = `${channelId}:${eventTs}`;
816
+ const now = this.now();
817
+ if (this.dedup.has(dedupKey))
818
+ return { skip: true };
819
+ this.dedup.set(dedupKey, now);
820
+ for (const key of this.dedup.keys()) {
821
+ if ((this.dedup.get(key) ?? 0) < now - DEDUP_WINDOW)
822
+ this.dedup.delete(key);
823
+ }
824
+ const userId = getString(event, "user");
825
+ const botId = getString(event, "bot_id");
826
+ if (userId === this.ownBotUserId)
827
+ return { skip: true };
828
+ if (botId === this.ownBotId)
829
+ return { skip: true };
830
+ const text = getString(event, "text") ?? "";
831
+ const mentioned = text.includes(`<@${this.ownBotUserId}>`);
832
+ const threadTs = getString(event, "thread_ts") ?? getString(event, "ts") ?? "";
833
+ return {
834
+ skip: false,
835
+ content: JSON.stringify(event),
836
+ meta: {
837
+ event_type: "slack",
838
+ channel_id: channelId,
839
+ user_id: userId ?? "",
840
+ mentioned: String(mentioned),
841
+ thread_ts: threadTs
842
+ },
843
+ shouldReact: mentioned,
844
+ channel: channelId,
845
+ timestamp: getString(event, "ts") ?? ""
846
+ };
847
+ }
848
+ }
849
+
850
+ // lib/connectors/slack-listener.ts
851
+ var middlewareArgsSchema = z2.object({
852
+ event: z2.record(z2.string(), z2.unknown()).optional()
853
+ });
854
+ var defaultLogger4 = new NodeFunnelLogger;
855
+
856
+ class FunnelSlackListener extends FunnelConnectorListener {
857
+ config;
858
+ logger;
859
+ app = null;
860
+ constructor(deps) {
861
+ super();
862
+ this.config = deps.config;
863
+ this.logger = deps.logger ?? defaultLogger4;
864
+ }
865
+ async start(notify) {
866
+ const app = new App({
867
+ token: this.config.botToken,
868
+ appToken: this.config.appToken,
869
+ socketMode: true,
870
+ logLevel: LogLevel.ERROR
871
+ });
872
+ const authResult = await app.client.auth.test({ token: this.config.botToken });
873
+ const processor = new FunnelSlackEventProcessor({
874
+ ownBotUserId: authResult.user_id ?? "",
875
+ ownBotId: authResult.bot_id ?? ""
876
+ });
877
+ app.use(async (args) => {
878
+ const parsed = middlewareArgsSchema.safeParse(args);
879
+ if (!parsed.success || !parsed.data.event)
880
+ return;
881
+ const result = processor.process(parsed.data.event);
882
+ if (result.skip)
883
+ return;
884
+ if (result.shouldReact) {
885
+ try {
886
+ await app.client.reactions.add({
887
+ token: this.config.botToken,
888
+ channel: result.channel,
889
+ timestamp: result.timestamp,
890
+ name: "eyes"
891
+ });
892
+ } catch {}
893
+ }
894
+ await notify(result.content, result.meta);
895
+ });
896
+ app.error(async (error) => {
897
+ this.logger.error("Slack error", {
898
+ error: error instanceof Error ? error.message : String(error)
899
+ });
900
+ });
901
+ await app.start();
902
+ this.app = app;
903
+ }
904
+ async stop() {
905
+ if (!this.app)
906
+ return;
907
+ try {
908
+ await this.app.stop();
909
+ } catch (error) {
910
+ this.logger.error("Slack stop error", {
911
+ error: error instanceof Error ? error.message : String(error)
912
+ });
913
+ } finally {
914
+ this.app = null;
915
+ }
916
+ }
917
+ isAlive() {
918
+ return this.app !== null;
919
+ }
920
+ }
921
+
922
+ // lib/engine/settings/settings-store.ts
923
+ import { homedir } from "os";
924
+ import { dirname as dirname3, join as join2 } from "path";
925
+
926
+ // lib/engine/settings/settings-reader.ts
927
+ class FunnelSettingsReader {
928
+ }
929
+
930
+ // lib/engine/settings/settings-schema.ts
931
+ import { z as z8 } from "zod";
932
+
933
+ // lib/connectors/connector-config-schema.ts
934
+ import { z as z7 } from "zod";
935
+
936
+ // lib/connectors/discord-connector-schema.ts
937
+ import { z as z3 } from "zod";
938
+ var discordConnectorSchema = z3.object({
939
+ id: z3.string(),
940
+ name: z3.string(),
941
+ type: z3.literal("discord"),
942
+ botToken: z3.string().min(10),
943
+ createdAt: z3.string().datetime().optional(),
944
+ updatedAt: z3.string().datetime().optional()
945
+ });
946
+
947
+ // lib/connectors/gh-connector-schema.ts
948
+ import { z as z4 } from "zod";
949
+ var ghConnectorSchema = z4.object({
950
+ id: z4.string(),
951
+ name: z4.string(),
952
+ type: z4.literal("gh"),
953
+ pollInterval: z4.number().int().positive().optional(),
954
+ createdAt: z4.string().datetime().optional(),
955
+ updatedAt: z4.string().datetime().optional()
956
+ });
957
+
958
+ // lib/connectors/schedule-connector-schema.ts
959
+ import { z as z5 } from "zod";
960
+ var scheduleCatchupPolicySchema = z5.enum(["latest", "all", "skip"]);
961
+ var scheduleEntrySchema = z5.object({
962
+ id: z5.string(),
963
+ cron: z5.string(),
964
+ prompt: z5.string(),
965
+ enabled: z5.boolean().default(true),
966
+ catchupPolicy: scheduleCatchupPolicySchema.default("latest")
967
+ });
968
+ var scheduleConnectorSchema = z5.object({
969
+ id: z5.string(),
970
+ name: z5.string(),
971
+ type: z5.literal("schedule"),
972
+ entries: z5.array(scheduleEntrySchema).default([]),
973
+ createdAt: z5.string().datetime().optional(),
974
+ updatedAt: z5.string().datetime().optional()
975
+ });
976
+
977
+ // lib/connectors/slack-connector-schema.ts
978
+ import { z as z6 } from "zod";
979
+ var slackConnectorSchema = z6.object({
980
+ id: z6.string(),
981
+ name: z6.string(),
982
+ type: z6.literal("slack"),
983
+ botToken: z6.string().startsWith("xoxb-"),
984
+ appToken: z6.string().startsWith("xapp-"),
985
+ createdAt: z6.string().datetime().optional(),
986
+ updatedAt: z6.string().datetime().optional()
987
+ });
988
+
989
+ // lib/connectors/connector-config-schema.ts
990
+ var connectorConfigSchema = z7.discriminatedUnion("type", [
991
+ slackConnectorSchema,
992
+ ghConnectorSchema,
993
+ discordConnectorSchema,
994
+ scheduleConnectorSchema
995
+ ]);
996
+
997
+ // lib/engine/settings/settings-schema.ts
998
+ var channelDeliveryModeSchema = z8.enum(["fanout", "exclusive"]);
999
+ var channelConfigSchema = z8.object({
1000
+ id: z8.string(),
1001
+ name: z8.string(),
1002
+ delivery: channelDeliveryModeSchema.default("fanout"),
1003
+ connectors: z8.array(connectorConfigSchema).default([])
1004
+ });
1005
+ var profileConfigSchema = z8.object({
1006
+ name: z8.string(),
1007
+ path: z8.string(),
1008
+ subAgent: z8.string(),
1009
+ channelId: z8.string()
1010
+ });
1011
+ var SETTINGS_VERSION = 1;
1012
+ var settingsSchema = z8.object({
1013
+ version: z8.literal(SETTINGS_VERSION).default(SETTINGS_VERSION),
1014
+ channels: z8.array(channelConfigSchema).default([]),
1015
+ profiles: z8.array(profileConfigSchema).default([])
1016
+ });
1017
+
1018
+ // lib/engine/settings/settings-store.ts
1019
+ var FUNNEL_DIR = join2(homedir(), ".funnel");
1020
+ var SETTINGS_PATH = join2(FUNNEL_DIR, "settings.json");
1021
+ var defaultFs2 = new NodeFunnelFileSystem;
1022
+
1023
+ class FunnelSettingsStore extends FunnelSettingsReader {
1024
+ path;
1025
+ fs;
1026
+ constructor(deps = {}) {
1027
+ super();
1028
+ this.path = deps.path ?? SETTINGS_PATH;
1029
+ this.fs = deps.fs ?? defaultFs2;
1030
+ Object.freeze(this);
1031
+ }
1032
+ read() {
1033
+ if (!this.fs.existsSync(this.path)) {
1034
+ return {
1035
+ version: SETTINGS_VERSION,
1036
+ channels: [],
1037
+ profiles: []
1038
+ };
1039
+ }
1040
+ const content = this.fs.readFileSync(this.path);
1041
+ const parsed = JSON.parse(content);
1042
+ if (this.looksLikeLegacy(parsed)) {
1043
+ throw new Error(`legacy settings.json detected at ${this.path}. The schema changed (channel.connectors are now nested objects with ids; profile fields renamed). Migration is intentionally not provided. Back up and remove the old file:
1044
+ mv ${this.path} ${this.path}.bak`);
1045
+ }
1046
+ if (parsed && typeof parsed === "object" && "version" in parsed && parsed.version !== SETTINGS_VERSION) {
1047
+ throw new Error(`unsupported settings.json version (${this.path}): expected ${SETTINGS_VERSION}, got ${String(parsed.version)}`);
1048
+ }
1049
+ const result = settingsSchema.safeParse(parsed);
1050
+ if (!result.success) {
1051
+ throw new Error(`invalid settings.json (${this.path}): ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ")}`);
1052
+ }
1053
+ return result.data;
1054
+ }
1055
+ looksLikeLegacy(parsed) {
1056
+ if (!parsed || typeof parsed !== "object")
1057
+ return false;
1058
+ const obj = parsed;
1059
+ if (Array.isArray(obj.channels)) {
1060
+ for (const channel of obj.channels) {
1061
+ if (!channel || typeof channel !== "object")
1062
+ continue;
1063
+ const ch = channel;
1064
+ if (Array.isArray(ch.connectors) && ch.connectors.some((x) => typeof x === "string")) {
1065
+ return true;
1066
+ }
1067
+ if (!("id" in ch) && "name" in ch)
1068
+ return true;
1069
+ }
1070
+ }
1071
+ if (Array.isArray(obj.connectors))
1072
+ return true;
1073
+ if (Array.isArray(obj.repositories))
1074
+ return true;
1075
+ if (Array.isArray(obj.profiles)) {
1076
+ for (const profile of obj.profiles) {
1077
+ if (!profile || typeof profile !== "object")
1078
+ continue;
1079
+ const p = profile;
1080
+ if ("repository" in p || "envFiles" in p || "channel" in p && !("channelId" in p)) {
1081
+ return true;
1082
+ }
1083
+ }
1084
+ }
1085
+ return false;
1086
+ }
1087
+ write(settings) {
1088
+ this.fs.mkdirSync(dirname3(this.path), { recursive: true });
1089
+ const versioned = { ...settings, version: SETTINGS_VERSION };
1090
+ this.fs.writeFileSync(this.path, `${JSON.stringify(versioned, null, 2)}
1091
+ `);
1092
+ }
1093
+ }
1094
+
1095
+ // lib/connectors/connector-factory.ts
1096
+ import { join as join3 } from "path";
1097
+ var defaultFs3 = new NodeFunnelFileSystem;
1098
+ var defaultProcess3 = new NodeFunnelProcessRunner;
1099
+ var defaultLogger5 = new NodeFunnelLogger;
1100
+
1101
+ class FunnelConnectorFactory {
1102
+ fs;
1103
+ process;
1104
+ logger;
1105
+ dir;
1106
+ constructor(deps = {}) {
1107
+ this.fs = deps.fs ?? defaultFs3;
1108
+ this.process = deps.process ?? defaultProcess3;
1109
+ this.logger = deps.logger ?? defaultLogger5;
1110
+ this.dir = deps.dir ?? FUNNEL_DIR;
1111
+ Object.freeze(this);
1112
+ }
1113
+ createListener(channelId, config) {
1114
+ if (config.type === "slack") {
1115
+ return new FunnelSlackListener({ config, logger: this.logger });
1116
+ }
1117
+ if (config.type === "gh") {
1118
+ return new FunnelGhListener({ config, process: this.process, logger: this.logger });
1119
+ }
1120
+ if (config.type === "discord") {
1121
+ return new FunnelDiscordListener({ config, logger: this.logger });
1122
+ }
1123
+ const lastFiredStore = new ScheduleStateStore({
1124
+ path: join3(this.connectorDir(channelId, config.id), "state.json"),
1125
+ fs: this.fs
1126
+ });
1127
+ return new FunnelScheduleListener({
1128
+ config,
1129
+ lastFiredStore,
1130
+ logger: this.logger
1131
+ });
1132
+ }
1133
+ createAdapter(config) {
1134
+ if (config.type === "slack")
1135
+ return new FunnelSlackAdapter({ config });
1136
+ if (config.type === "gh")
1137
+ return new FunnelGhAdapter({ process: this.process });
1138
+ if (config.type === "discord")
1139
+ return new FunnelDiscordAdapter({ config });
1140
+ return null;
1141
+ }
1142
+ connectorDir(channelId, connectorId) {
1143
+ return join3(this.dir, "channels", channelId, "connectors", connectorId);
1144
+ }
1145
+ channelDir(channelId) {
1146
+ return join3(this.dir, "channels", channelId);
1147
+ }
1148
+ }
1149
+
1150
+ // lib/engine/time/clock.ts
1151
+ class FunnelClock {
1152
+ millis() {
1153
+ return this.now().getTime();
1154
+ }
1155
+ iso() {
1156
+ return this.now().toISOString();
1157
+ }
1158
+ }
1159
+
1160
+ // lib/engine/time/node-clock.ts
1161
+ class NodeFunnelClock extends FunnelClock {
1162
+ now() {
1163
+ return new Date;
1164
+ }
1165
+ }
1166
+
1167
+ // lib/engine/id/id-generator.ts
1168
+ class FunnelIdGenerator {
1169
+ }
1170
+
1171
+ // lib/engine/id/node-id-generator.ts
1172
+ class NodeFunnelIdGenerator extends FunnelIdGenerator {
1173
+ generate() {
1174
+ return crypto.randomUUID();
1175
+ }
1176
+ }
1177
+
1178
+ // lib/engine/channels/channels.ts
1179
+ var defaultClock = new NodeFunnelClock;
1180
+ var defaultIdGenerator = new NodeFunnelIdGenerator;
1181
+
1182
+ class FunnelChannels {
1183
+ store;
1184
+ factory;
1185
+ profileChecker;
1186
+ clock;
1187
+ idGenerator;
1188
+ constructor(deps) {
1189
+ this.store = deps.store;
1190
+ this.factory = deps.factory;
1191
+ this.profileChecker = deps.profileChecker;
1192
+ this.clock = deps.clock ?? defaultClock;
1193
+ this.idGenerator = deps.idGenerator ?? defaultIdGenerator;
1194
+ Object.freeze(this);
1195
+ }
1196
+ list() {
1197
+ return this.store.read().channels;
1198
+ }
1199
+ get(name) {
1200
+ return this.list().find((c) => c.name === name) ?? null;
1201
+ }
1202
+ getById(id) {
1203
+ return this.list().find((c) => c.id === id) ?? null;
1204
+ }
1205
+ add(input) {
1206
+ const settings = this.store.read();
1207
+ if (settings.channels.some((c) => c.name === input.name)) {
1208
+ throw new Error(`channel "${input.name}" already exists`);
1209
+ }
1210
+ const channel = {
1211
+ id: this.idGenerator.generate(),
1212
+ name: input.name,
1213
+ delivery: input.delivery ?? "fanout",
1214
+ connectors: []
1215
+ };
1216
+ settings.channels.push(channel);
1217
+ this.store.write(settings);
1218
+ return channel;
1219
+ }
1220
+ setDelivery(name, delivery) {
1221
+ const settings = this.store.read();
1222
+ const channel = this.requireChannel(settings, name);
1223
+ channel.delivery = delivery;
1224
+ this.store.write(settings);
1225
+ }
1226
+ remove(name) {
1227
+ const settings = this.store.read();
1228
+ const index = settings.channels.findIndex((c) => c.name === name);
1229
+ if (index < 0)
1230
+ throw new Error(`channel "${name}" not found`);
1231
+ const channel = settings.channels[index];
1232
+ if (channel && this.profileChecker.hasChannelRef(channel.id)) {
1233
+ throw new Error(`channel "${name}" is referenced by a profile`);
1234
+ }
1235
+ settings.channels.splice(index, 1);
1236
+ this.store.write(settings);
1237
+ }
1238
+ rename(oldName, newName) {
1239
+ const settings = this.store.read();
1240
+ const channel = settings.channels.find((c) => c.name === oldName);
1241
+ if (!channel)
1242
+ throw new Error(`channel "${oldName}" not found`);
1243
+ if (settings.channels.some((c) => c.name === newName)) {
1244
+ throw new Error(`channel "${newName}" already exists`);
1245
+ }
1246
+ channel.name = newName;
1247
+ this.store.write(settings);
1248
+ }
1249
+ listConnectors(channelName) {
1250
+ return this.requireChannel(this.store.read(), channelName).connectors;
1251
+ }
1252
+ getConnector(channelName, connectorName) {
1253
+ const channel = this.get(channelName);
1254
+ if (!channel)
1255
+ return null;
1256
+ return channel.connectors.find((c) => c.name === connectorName) ?? null;
1257
+ }
1258
+ listAllConnectors() {
1259
+ const out = [];
1260
+ for (const channel of this.list()) {
1261
+ for (const connector of channel.connectors) {
1262
+ out.push({ ...connector, channelId: channel.id, channelName: channel.name });
1263
+ }
1264
+ }
1265
+ return out;
1266
+ }
1267
+ addConnector(channelName, input) {
1268
+ const settings = this.store.read();
1269
+ const channel = this.requireChannel(settings, channelName);
1270
+ if (channel.connectors.some((c) => c.name === input.name)) {
1271
+ throw new Error(`connector "${input.name}" already exists in channel "${channelName}"`);
1272
+ }
1273
+ const candidate = this.fromInput(input);
1274
+ this.assertNoTokenCollision(settings, candidate);
1275
+ channel.connectors.push(candidate);
1276
+ this.store.write(settings);
1277
+ return candidate;
1278
+ }
1279
+ fromInput(input) {
1280
+ const id = this.idGenerator.generate();
1281
+ const now = this.clock.iso();
1282
+ const createdAt = now;
1283
+ const updatedAt = now;
1284
+ if (input.type === "slack") {
1285
+ return {
1286
+ id,
1287
+ type: "slack",
1288
+ name: input.name,
1289
+ botToken: input.botToken,
1290
+ appToken: input.appToken,
1291
+ createdAt,
1292
+ updatedAt
1293
+ };
1294
+ }
1295
+ if (input.type === "gh") {
1296
+ return {
1297
+ id,
1298
+ type: "gh",
1299
+ name: input.name,
1300
+ ...input.pollInterval !== undefined ? { pollInterval: input.pollInterval } : {},
1301
+ createdAt,
1302
+ updatedAt
1303
+ };
1304
+ }
1305
+ if (input.type === "discord") {
1306
+ return {
1307
+ id,
1308
+ type: "discord",
1309
+ name: input.name,
1310
+ botToken: input.botToken,
1311
+ createdAt,
1312
+ updatedAt
1313
+ };
1314
+ }
1315
+ return {
1316
+ id,
1317
+ type: "schedule",
1318
+ name: input.name,
1319
+ entries: input.entries ?? [],
1320
+ createdAt,
1321
+ updatedAt
1322
+ };
1323
+ }
1324
+ removeConnector(channelName, connectorName) {
1325
+ const settings = this.store.read();
1326
+ const channel = this.requireChannel(settings, channelName);
1327
+ const index = channel.connectors.findIndex((c) => c.name === connectorName);
1328
+ if (index < 0) {
1329
+ throw new Error(`connector "${connectorName}" not found in channel "${channelName}"`);
1330
+ }
1331
+ channel.connectors.splice(index, 1);
1332
+ this.store.write(settings);
1333
+ }
1334
+ renameConnector(channelName, oldName, newName) {
1335
+ const settings = this.store.read();
1336
+ const channel = this.requireChannel(settings, channelName);
1337
+ const connector = channel.connectors.find((c) => c.name === oldName);
1338
+ if (!connector) {
1339
+ throw new Error(`connector "${oldName}" not found in channel "${channelName}"`);
1340
+ }
1341
+ if (channel.connectors.some((c) => c.name === newName)) {
1342
+ throw new Error(`connector "${newName}" already exists in channel "${channelName}"`);
1343
+ }
1344
+ connector.name = newName;
1345
+ connector.updatedAt = this.clock.iso();
1346
+ this.store.write(settings);
1347
+ }
1348
+ updateSlackConnector(channelName, connectorName, fields) {
1349
+ const settings = this.store.read();
1350
+ const channel = this.requireChannel(settings, channelName);
1351
+ const connector = this.requireSlackConnector(channel, connectorName);
1352
+ const updated = {
1353
+ ...connector,
1354
+ botToken: fields.botToken ?? connector.botToken,
1355
+ appToken: fields.appToken ?? connector.appToken,
1356
+ updatedAt: this.clock.iso()
1357
+ };
1358
+ this.assertNoTokenCollision(settings, updated);
1359
+ Object.assign(connector, updated);
1360
+ this.store.write(settings);
1361
+ }
1362
+ updateGhConnector(channelName, connectorName, fields) {
1363
+ const settings = this.store.read();
1364
+ const channel = this.requireChannel(settings, channelName);
1365
+ const connector = this.requireGhConnector(channel, connectorName);
1366
+ if (fields.pollInterval !== undefined)
1367
+ connector.pollInterval = fields.pollInterval;
1368
+ connector.updatedAt = this.clock.iso();
1369
+ this.store.write(settings);
1370
+ }
1371
+ updateDiscordConnector(channelName, connectorName, fields) {
1372
+ const settings = this.store.read();
1373
+ const channel = this.requireChannel(settings, channelName);
1374
+ const connector = this.requireDiscordConnector(channel, connectorName);
1375
+ const updated = {
1376
+ ...connector,
1377
+ botToken: fields.botToken ?? connector.botToken,
1378
+ updatedAt: this.clock.iso()
1379
+ };
1380
+ this.assertNoTokenCollision(settings, updated);
1381
+ Object.assign(connector, updated);
1382
+ this.store.write(settings);
1383
+ }
1384
+ listScheduleEntries(channelName, connectorName) {
1385
+ const channel = this.requireChannel(this.store.read(), channelName);
1386
+ const connector = this.requireScheduleConnector(channel, connectorName);
1387
+ return connector.entries;
1388
+ }
1389
+ addScheduleEntry(channelName, connectorName, entry) {
1390
+ const settings = this.store.read();
1391
+ const channel = this.requireChannel(settings, channelName);
1392
+ const connector = this.requireScheduleConnector(channel, connectorName);
1393
+ const persisted = {
1394
+ id: entry.id ?? this.idGenerator.generate(),
1395
+ cron: entry.cron,
1396
+ prompt: entry.prompt,
1397
+ enabled: entry.enabled ?? true,
1398
+ catchupPolicy: entry.catchupPolicy ?? "latest"
1399
+ };
1400
+ connector.entries.push(persisted);
1401
+ connector.updatedAt = this.clock.iso();
1402
+ this.store.write(settings);
1403
+ return persisted;
1404
+ }
1405
+ removeScheduleEntry(channelName, connectorName, id) {
1406
+ const settings = this.store.read();
1407
+ const channel = this.requireChannel(settings, channelName);
1408
+ const connector = this.requireScheduleConnector(channel, connectorName);
1409
+ const index = connector.entries.findIndex((e) => e.id === id);
1410
+ if (index < 0)
1411
+ throw new Error(`schedule entry "${id}" not found`);
1412
+ connector.entries.splice(index, 1);
1413
+ connector.updatedAt = this.clock.iso();
1414
+ this.store.write(settings);
1415
+ }
1416
+ async call(channelName, connectorName, input) {
1417
+ const connector = this.getConnector(channelName, connectorName);
1418
+ if (!connector) {
1419
+ throw new Error(`connector "${connectorName}" not found in channel "${channelName}"`);
1420
+ }
1421
+ const adapter = this.factory.createAdapter(connector);
1422
+ if (!adapter) {
1423
+ throw new Error(`connector type "${connector.type}" does not support outbound calls`);
1424
+ }
1425
+ return await adapter.call(input);
1426
+ }
1427
+ createListener(channelName, connectorName) {
1428
+ const channel = this.get(channelName);
1429
+ if (!channel)
1430
+ return null;
1431
+ const connector = channel.connectors.find((c) => c.name === connectorName);
1432
+ if (!connector)
1433
+ return null;
1434
+ return {
1435
+ config: connector,
1436
+ channelId: channel.id,
1437
+ listener: this.factory.createListener(channel.id, connector)
1438
+ };
1439
+ }
1440
+ createAllListeners() {
1441
+ const out = [];
1442
+ for (const channel of this.list()) {
1443
+ for (const connector of channel.connectors) {
1444
+ out.push({
1445
+ config: connector,
1446
+ channelId: channel.id,
1447
+ channelName: channel.name,
1448
+ listener: this.factory.createListener(channel.id, connector)
1449
+ });
1450
+ }
1451
+ }
1452
+ return out;
1453
+ }
1454
+ requireChannel(settings, name) {
1455
+ const channel = settings.channels.find((c) => c.name === name);
1456
+ if (!channel)
1457
+ throw new Error(`channel "${name}" not found`);
1458
+ return channel;
1459
+ }
1460
+ requireConnector(channel, connectorName) {
1461
+ const connector = channel.connectors.find((c) => c.name === connectorName);
1462
+ if (!connector) {
1463
+ throw new Error(`connector "${connectorName}" not found in channel "${channel.name}"`);
1464
+ }
1465
+ return connector;
1466
+ }
1467
+ requireSlackConnector(channel, connectorName) {
1468
+ const connector = this.requireConnector(channel, connectorName);
1469
+ if (connector.type !== "slack") {
1470
+ throw new Error(`connector "${connectorName}" is type "${connector.type}", not "slack"`);
1471
+ }
1472
+ return connector;
1473
+ }
1474
+ requireGhConnector(channel, connectorName) {
1475
+ const connector = this.requireConnector(channel, connectorName);
1476
+ if (connector.type !== "gh") {
1477
+ throw new Error(`connector "${connectorName}" is type "${connector.type}", not "gh"`);
1478
+ }
1479
+ return connector;
1480
+ }
1481
+ requireDiscordConnector(channel, connectorName) {
1482
+ const connector = this.requireConnector(channel, connectorName);
1483
+ if (connector.type !== "discord") {
1484
+ throw new Error(`connector "${connectorName}" is type "${connector.type}", not "discord"`);
1485
+ }
1486
+ return connector;
1487
+ }
1488
+ requireScheduleConnector(channel, connectorName) {
1489
+ const connector = this.requireConnector(channel, connectorName);
1490
+ if (connector.type !== "schedule") {
1491
+ throw new Error(`connector "${connectorName}" is type "${connector.type}", not "schedule"`);
1492
+ }
1493
+ return connector;
1494
+ }
1495
+ assertNoTokenCollision(settings, candidate) {
1496
+ const tokens = this.tokensOf(candidate);
1497
+ if (tokens.length === 0)
1498
+ return;
1499
+ for (const channel of settings.channels) {
1500
+ for (const other of channel.connectors) {
1501
+ if (other.id === candidate.id)
1502
+ continue;
1503
+ for (const token of this.tokensOf(other)) {
1504
+ if (tokens.includes(token)) {
1505
+ throw new Error(`token already in use by connector "${other.name}" in channel "${channel.name}"`);
1506
+ }
1507
+ }
1508
+ }
1509
+ }
1510
+ }
1511
+ tokensOf(connector) {
1512
+ if (connector.type === "slack")
1513
+ return [connector.botToken, connector.appToken];
1514
+ if (connector.type === "discord")
1515
+ return [connector.botToken];
1516
+ return [];
1517
+ }
1518
+ }
1519
+
1520
+ // lib/engine/claude/claude.ts
1521
+ import { join as join4 } from "path";
1522
+ var defaultProcess4 = new NodeFunnelProcessRunner;
1523
+ var defaultFs4 = new NodeFunnelFileSystem;
1524
+ var defaultLogger6 = new NodeFunnelLogger;
1525
+
1526
+ class FunnelClaude {
1527
+ channels;
1528
+ mcp;
1529
+ gateway;
1530
+ process;
1531
+ fs;
1532
+ logger;
1533
+ pidDir;
1534
+ constructor(deps) {
1535
+ this.channels = deps.channels;
1536
+ this.mcp = deps.mcp;
1537
+ this.gateway = deps.gateway;
1538
+ this.process = deps.process ?? defaultProcess4;
1539
+ this.fs = deps.fs ?? defaultFs4;
1540
+ this.logger = deps.logger ?? defaultLogger6;
1541
+ this.pidDir = join4(deps.dir ?? FUNNEL_DIR, "claude");
1542
+ Object.freeze(this);
1543
+ }
1544
+ async launch(options) {
1545
+ const channel = this.channels.get(options.channel) ?? this.channels.getById(options.channel);
1546
+ if (!channel) {
1547
+ throw new Error(`channel "${options.channel}" not found`);
1548
+ }
1549
+ if (options.profileName && this.isRunning(options.profileName)) {
1550
+ throw new Error(`profile "${options.profileName}" is already running`);
1551
+ }
1552
+ const cwd = options.cwd ?? globalThis.process.cwd();
1553
+ if (!this.mcp.findInstalledName(cwd)) {
1554
+ this.mcp.install(cwd);
1555
+ this.logger.info(`added funnel MCP to .mcp.json`, { cwd });
1556
+ }
1557
+ if (!this.gateway.isRunning()) {
1558
+ this.logger.info(`starting gateway automatically`);
1559
+ await this.gateway.start();
1560
+ }
1561
+ if (options.profileName) {
1562
+ this.writePidFile(options.profileName);
1563
+ this.installCleanup(options.profileName);
1564
+ }
1565
+ const claudeArgs = this.buildArgs(options, cwd);
1566
+ const env = this.buildEnv(channel.id);
1567
+ this.logger.info(`claude launch`, {
1568
+ channel: options.channel,
1569
+ channelId: channel.id,
1570
+ subAgent: options.subAgent,
1571
+ cwd
1572
+ });
1573
+ try {
1574
+ return await this.process.attach(["claude", ...claudeArgs], { cwd, env });
1575
+ } finally {
1576
+ if (options.profileName)
1577
+ this.removePidFile(options.profileName);
1578
+ }
1579
+ }
1580
+ isRunning(profileName) {
1581
+ const pid = this.readPid(profileName);
1582
+ if (!pid)
1583
+ return false;
1584
+ return this.isProcessAlive(pid);
1585
+ }
1586
+ pidPath(profileName) {
1587
+ return join4(this.pidDir, `${profileName}.pid`);
1588
+ }
1589
+ readPid(profileName) {
1590
+ const path = this.pidPath(profileName);
1591
+ if (!this.fs.existsSync(path))
1592
+ return null;
1593
+ try {
1594
+ const content = this.fs.readFileSync(path).trim();
1595
+ const pid = Number(content);
1596
+ if (!pid || pid <= 0)
1597
+ return null;
1598
+ return pid;
1599
+ } catch {
1600
+ return null;
1601
+ }
1602
+ }
1603
+ writePidFile(profileName) {
1604
+ this.fs.mkdirSync(this.pidDir, { recursive: true });
1605
+ this.fs.writeFileSync(this.pidPath(profileName), String(globalThis.process.pid));
1606
+ }
1607
+ removePidFile(profileName) {
1608
+ const path = this.pidPath(profileName);
1609
+ if (this.fs.existsSync(path))
1610
+ this.fs.unlink(path);
1611
+ }
1612
+ installCleanup(profileName) {
1613
+ globalThis.process.once("exit", () => this.removePidFile(profileName));
1614
+ }
1615
+ isProcessAlive(pid) {
1616
+ const result = this.process.runSync(["ps", "-p", String(pid), "-o", "state="]);
1617
+ if (result.exitCode !== 0)
1618
+ return false;
1619
+ const state = result.stdout.trim();
1620
+ if (!state)
1621
+ return false;
1622
+ return !state.startsWith("Z");
1623
+ }
1624
+ buildArgs(options, cwd) {
1625
+ const result = [...options.userArgs ?? []];
1626
+ const mcpName = this.mcp.findInstalledName(cwd);
1627
+ if (mcpName && !result.includes("--dangerously-load-development-channels") && !result.includes("--channels")) {
1628
+ result.push("--dangerously-load-development-channels", `server:${mcpName}`);
1629
+ }
1630
+ if (!result.includes("--agent") && options.subAgent) {
1631
+ result.push("--agent", options.subAgent);
1632
+ }
1633
+ return result;
1634
+ }
1635
+ buildEnv(channelId) {
1636
+ const env = {};
1637
+ for (const [key, value] of Object.entries(globalThis.process.env)) {
1638
+ if (typeof value === "string")
1639
+ env[key] = value;
1640
+ }
1641
+ env.FUNNEL_CHANNEL_ID = channelId;
1642
+ return env;
1643
+ }
1644
+ }
1645
+
1646
+ // lib/engine/mcp/mcp.ts
1647
+ import { join as join5 } from "path";
1648
+ import { z as z9 } from "zod";
1649
+ var FUNNEL_MCP_COMMAND = "funnel";
1650
+ var FUNNEL_MCP_NAME = "funnel";
1651
+ var mcpEntrySchema = z9.object({
1652
+ command: z9.string().optional(),
1653
+ args: z9.array(z9.string()).optional()
1654
+ });
1655
+ var mcpConfigSchema = z9.object({
1656
+ mcpServers: z9.record(z9.string(), mcpEntrySchema).optional()
1657
+ });
1658
+ var defaultFs5 = new NodeFunnelFileSystem;
1659
+
1660
+ class FunnelMcp {
1661
+ fs;
1662
+ constructor(deps = {}) {
1663
+ this.fs = deps.fs ?? defaultFs5;
1664
+ Object.freeze(this);
1665
+ }
1666
+ install(repoPath) {
1667
+ if (!this.fs.existsSync(repoPath)) {
1668
+ throw new Error(`repository does not exist: ${repoPath}`);
1669
+ }
1670
+ const config = this.readConfig(repoPath);
1671
+ const servers = config.mcpServers ?? {};
1672
+ const existingName = this.findServerName(servers);
1673
+ const targetName = existingName ?? FUNNEL_MCP_NAME;
1674
+ servers[targetName] = {
1675
+ command: FUNNEL_MCP_COMMAND,
1676
+ args: ["mcp"]
1677
+ };
1678
+ this.writeConfig(repoPath, { ...config, mcpServers: servers });
1679
+ }
1680
+ uninstall(repoPath) {
1681
+ if (!this.fs.existsSync(repoPath))
1682
+ return;
1683
+ const config = this.readConfig(repoPath);
1684
+ const servers = config.mcpServers ?? {};
1685
+ const name = this.findServerName(servers);
1686
+ if (!name)
1687
+ return;
1688
+ const next = { ...servers };
1689
+ delete next[name];
1690
+ this.writeConfig(repoPath, { ...config, mcpServers: next });
1691
+ }
1692
+ findInstalledName(cwd) {
1693
+ const config = this.readConfig(cwd);
1694
+ return this.findServerName(config.mcpServers ?? {});
1695
+ }
1696
+ findServerName(servers) {
1697
+ for (const entry of Object.entries(servers)) {
1698
+ const name = entry[0];
1699
+ const value = entry[1];
1700
+ if (value?.command === FUNNEL_MCP_COMMAND)
1701
+ return name;
1702
+ }
1703
+ return null;
1704
+ }
1705
+ readConfig(repoPath) {
1706
+ const mcpPath = join5(repoPath, ".mcp.json");
1707
+ if (!this.fs.existsSync(mcpPath))
1708
+ return {};
1709
+ const content = this.fs.readFileSync(mcpPath).trim();
1710
+ if (!content)
1711
+ return {};
1712
+ let parsed;
1713
+ try {
1714
+ parsed = JSON.parse(content);
1715
+ } catch (error) {
1716
+ throw new Error(`invalid .mcp.json (${mcpPath}): ${error instanceof Error ? error.message : String(error)}`);
1717
+ }
1718
+ const result = mcpConfigSchema.safeParse(parsed);
1719
+ if (!result.success) {
1720
+ throw new Error(`invalid .mcp.json (${mcpPath}): ${result.error.message}`);
1721
+ }
1722
+ return result.data;
1723
+ }
1724
+ writeConfig(repoPath, config) {
1725
+ const mcpPath = join5(repoPath, ".mcp.json");
1726
+ this.fs.writeFileSync(mcpPath, `${JSON.stringify(config, null, 2)}
1727
+ `);
1728
+ }
1729
+ }
1730
+
1731
+ // lib/engine/profiles/profiles.ts
1732
+ class FunnelProfiles {
1733
+ store;
1734
+ constructor(deps) {
1735
+ this.store = deps.store;
1736
+ Object.freeze(this);
1737
+ }
1738
+ list() {
1739
+ return this.store.read().profiles;
1740
+ }
1741
+ get(name) {
1742
+ return this.list().find((p) => p.name === name) ?? null;
1743
+ }
1744
+ getDefault() {
1745
+ return this.list()[0] ?? null;
1746
+ }
1747
+ add(config) {
1748
+ const settings = this.store.read();
1749
+ if (settings.profiles.some((p) => p.name === config.name)) {
1750
+ throw new Error(`profile "${config.name}" already exists`);
1751
+ }
1752
+ if (!settings.channels.some((c) => c.id === config.channelId)) {
1753
+ throw new Error(`channel id "${config.channelId}" not found`);
1754
+ }
1755
+ settings.profiles.push(config);
1756
+ this.store.write(settings);
1757
+ }
1758
+ remove(name) {
1759
+ const settings = this.store.read();
1760
+ const index = settings.profiles.findIndex((p) => p.name === name);
1761
+ if (index < 0)
1762
+ throw new Error(`profile "${name}" not found`);
1763
+ settings.profiles.splice(index, 1);
1764
+ this.store.write(settings);
1765
+ }
1766
+ rename(oldName, newName) {
1767
+ const settings = this.store.read();
1768
+ const profile = settings.profiles.find((p) => p.name === oldName);
1769
+ if (!profile)
1770
+ throw new Error(`profile "${oldName}" not found`);
1771
+ if (settings.profiles.some((p) => p.name === newName)) {
1772
+ throw new Error(`profile "${newName}" already exists`);
1773
+ }
1774
+ profile.name = newName;
1775
+ this.store.write(settings);
1776
+ }
1777
+ asDefault(name) {
1778
+ const settings = this.store.read();
1779
+ const index = settings.profiles.findIndex((p) => p.name === name);
1780
+ if (index < 0)
1781
+ throw new Error(`profile "${name}" not found`);
1782
+ if (index === 0)
1783
+ return;
1784
+ const [profile] = settings.profiles.splice(index, 1);
1785
+ if (!profile)
1786
+ return;
1787
+ settings.profiles.unshift(profile);
1788
+ this.store.write(settings);
1789
+ }
1790
+ hasChannelRef(channelId) {
1791
+ return this.store.read().profiles.some((p) => p.channelId === channelId);
1792
+ }
1793
+ update(name, fields) {
1794
+ const settings = this.store.read();
1795
+ const profile = settings.profiles.find((p) => p.name === name);
1796
+ if (!profile)
1797
+ throw new Error(`profile "${name}" not found`);
1798
+ if (fields.channelId !== undefined) {
1799
+ if (!settings.channels.some((c) => c.id === fields.channelId)) {
1800
+ throw new Error(`channel id "${fields.channelId}" not found`);
1801
+ }
1802
+ profile.channelId = fields.channelId;
1803
+ }
1804
+ if (fields.path !== undefined)
1805
+ profile.path = fields.path;
1806
+ if (fields.subAgent !== undefined)
1807
+ profile.subAgent = fields.subAgent;
1808
+ this.store.write(settings);
1809
+ }
1810
+ }
1811
+
1812
+ // lib/gateway/gateway.ts
1813
+ import { join as join6 } from "path";
1814
+
1815
+ // lib/gateway/resolve-daemon-script.ts
1816
+ import { existsSync as existsSync2 } from "fs";
1817
+ import { resolve } from "path";
1818
+ var resolveDaemonScript = () => {
1819
+ const candidates = [
1820
+ resolve(import.meta.dir, "./daemon.ts"),
1821
+ resolve(import.meta.dir, "./daemon.js"),
1822
+ resolve(import.meta.dir, "./gateway/daemon.js")
1823
+ ];
1824
+ for (const candidate of candidates) {
1825
+ if (existsSync2(candidate))
1826
+ return candidate;
1827
+ }
1828
+ throw new Error(`daemon script not found (looked in ${candidates.join(", ")})`);
1829
+ };
1830
+
1831
+ // lib/gateway/gateway.ts
1832
+ var DEFAULT_PORT = 9742;
1833
+ var DEFAULT_TMP_DIR = "/tmp/funnel";
1834
+ var STARTUP_TIMEOUT_MS = 5000;
1835
+ var SIGTERM_TIMEOUT_MS = 2000;
1836
+ var POLL_INTERVAL_MS = 100;
1837
+ var SIGKILL_GRACE_MS = 200;
1838
+ var defaultProcess5 = new NodeFunnelProcessRunner;
1839
+ var defaultFs6 = new NodeFunnelFileSystem;
1840
+ var defaultClock2 = new NodeFunnelClock;
1841
+ var defaultSleep = (ms) => new Promise((r) => {
1842
+ setTimeout(r, ms);
1843
+ });
1844
+
1845
+ class FunnelGateway {
1846
+ process;
1847
+ fs;
1848
+ clock;
1849
+ pidFile;
1850
+ logDir;
1851
+ gatewayLog;
1852
+ tmpDir;
1853
+ port;
1854
+ sleep;
1855
+ constructor(deps = {}) {
1856
+ this.process = deps.process ?? defaultProcess5;
1857
+ this.fs = deps.fs ?? defaultFs6;
1858
+ this.clock = deps.clock ?? defaultClock2;
1859
+ const baseDir = deps.dir ?? FUNNEL_DIR;
1860
+ this.tmpDir = deps.tmpDir ?? DEFAULT_TMP_DIR;
1861
+ this.pidFile = join6(baseDir, "gateway.pid");
1862
+ this.logDir = join6(this.tmpDir, "events");
1863
+ this.gatewayLog = join6(this.tmpDir, "gateway.log");
1864
+ this.port = deps.port ?? DEFAULT_PORT;
1865
+ this.sleep = deps.sleep ?? defaultSleep;
1866
+ Object.freeze(this);
1867
+ }
1868
+ isRunning() {
1869
+ const pid = this.readPid();
1870
+ if (!pid)
1871
+ return false;
1872
+ return this.isProcessAlive(pid);
1873
+ }
1874
+ getStatus() {
1875
+ const pid = this.readPid();
1876
+ const running = pid !== null && this.isProcessAlive(pid);
1877
+ return { running, pid: running ? pid : null, port: this.port };
1878
+ }
1879
+ async start(options = {}) {
1880
+ if (this.isRunning())
1881
+ return true;
1882
+ this.fs.mkdirSync(this.tmpDir, { recursive: true });
1883
+ const gatewayScript = resolveDaemonScript();
1884
+ const command = this.buildStartCommand(gatewayScript, options);
1885
+ this.process.detach(["bash", "-c", command]);
1886
+ const deadline = Date.now() + STARTUP_TIMEOUT_MS;
1887
+ while (Date.now() < deadline) {
1888
+ if (this.isRunning())
1889
+ return true;
1890
+ await this.sleep(POLL_INTERVAL_MS);
1891
+ }
1892
+ return this.isRunning();
1893
+ }
1894
+ buildStartCommand(gatewayScript, options = {}) {
1895
+ const useCaffeinate = options.caffeinate !== false && globalThis.process.platform === "darwin";
1896
+ const prefix = useCaffeinate ? "caffeinate -i " : "";
1897
+ return `nohup ${prefix}bun ${gatewayScript} >> ${this.gatewayLog} 2>&1 &`;
1898
+ }
1899
+ async stop() {
1900
+ const pid = this.readPid();
1901
+ if (!pid)
1902
+ return true;
1903
+ if (!this.isProcessAlive(pid)) {
1904
+ this.removePid();
1905
+ return true;
1906
+ }
1907
+ try {
1908
+ this.process.kill(pid, "SIGTERM");
1909
+ } catch {
1910
+ return false;
1911
+ }
1912
+ const deadline = this.clock.millis() + SIGTERM_TIMEOUT_MS;
1913
+ while (this.clock.millis() < deadline) {
1914
+ if (!this.isProcessAlive(pid)) {
1915
+ this.removePid();
1916
+ return true;
1917
+ }
1918
+ await this.sleep(POLL_INTERVAL_MS);
1919
+ }
1920
+ try {
1921
+ this.process.kill(pid, "SIGKILL");
1922
+ } catch {}
1923
+ await this.sleep(SIGKILL_GRACE_MS);
1924
+ this.removePid();
1925
+ return !this.isProcessAlive(pid);
1926
+ }
1927
+ async restart(options = {}) {
1928
+ const wasRunning = this.isRunning();
1929
+ if (options.onlyIfRunning && !wasRunning) {
1930
+ return { ok: true, wasRunning: false, stopped: false, started: false };
1931
+ }
1932
+ const stopped = wasRunning ? await this.stop() : true;
1933
+ if (!stopped) {
1934
+ return { ok: false, wasRunning, stopped: false, started: false };
1935
+ }
1936
+ const started = await this.start({ caffeinate: options.caffeinate });
1937
+ return { ok: started, wasRunning, stopped, started };
1938
+ }
1939
+ getLogDir() {
1940
+ return this.logDir;
1941
+ }
1942
+ getGatewayLog() {
1943
+ return this.gatewayLog;
1944
+ }
1945
+ getPort() {
1946
+ return this.port;
1947
+ }
1948
+ readPid() {
1949
+ if (!this.fs.existsSync(this.pidFile))
1950
+ return null;
1951
+ try {
1952
+ const content = this.fs.readFileSync(this.pidFile).trim();
1953
+ const pid = Number(content);
1954
+ if (!pid || pid <= 0)
1955
+ return null;
1956
+ return pid;
1957
+ } catch {
1958
+ return null;
1959
+ }
1960
+ }
1961
+ removePid() {
1962
+ this.fs.unlink(this.pidFile);
1963
+ }
1964
+ isProcessAlive(pid) {
1965
+ const result = this.process.runSync(["ps", "-p", String(pid), "-o", "state="]);
1966
+ if (result.exitCode !== 0)
1967
+ return false;
1968
+ const state = result.stdout.trim();
1969
+ if (!state)
1970
+ return false;
1971
+ return !state.startsWith("Z");
1972
+ }
1973
+ }
1974
+
1975
+ // lib/gateway/gateway-server.ts
1976
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "fs";
1977
+ import { join as join7 } from "path";
1978
+
1979
+ // lib/gateway/auth-middleware.ts
1980
+ import { timingSafeEqual } from "crypto";
1981
+ var requireBearerToken = (deps) => {
1982
+ return async (c, next) => {
1983
+ const header = c.req.header("authorization") ?? "";
1984
+ const match = header.match(/^Bearer\s+(.+)$/i);
1985
+ const presented = match?.[1] ?? "";
1986
+ if (!constantTimeEqual(presented, deps.expected)) {
1987
+ return c.text("unauthorized", 401);
1988
+ }
1989
+ return await next();
1990
+ };
1991
+ };
1992
+ var constantTimeEqual = (a, b) => {
1993
+ const bufA = Buffer.from(a, "utf-8");
1994
+ const bufB = Buffer.from(b, "utf-8");
1995
+ const maxLen = Math.max(bufA.length, bufB.length, 1);
1996
+ const padA = Buffer.alloc(maxLen);
1997
+ const padB = Buffer.alloc(maxLen);
1998
+ bufA.copy(padA);
1999
+ bufB.copy(padB);
2000
+ const equal = timingSafeEqual(padA, padB);
2001
+ return equal && bufA.length === bufB.length;
2002
+ };
2003
+
2004
+ // lib/gateway/factory.ts
2005
+ import { createFactory } from "hono/factory";
2006
+ var factory = createFactory();
2007
+
2008
+ // lib/engine/logger/noop-logger.ts
2009
+ class NoopFunnelLogger extends FunnelLogger {
2010
+ file = null;
2011
+ info() {}
2012
+ warn() {}
2013
+ error() {}
2014
+ }
2015
+
2016
+ // lib/gateway/broadcaster.ts
2017
+ var byteLengthOf = (event) => {
2018
+ let bytes = Buffer.byteLength(event.content, "utf-8");
2019
+ if (event.meta) {
2020
+ for (const [k, v] of Object.entries(event.meta)) {
2021
+ bytes += Buffer.byteLength(k, "utf-8") + Buffer.byteLength(v, "utf-8");
2022
+ }
2023
+ }
2024
+ return bytes;
2025
+ };
2026
+ var DEFAULT_MAX_BUFFERED_BYTES = 1024 * 1024;
2027
+ var DEFAULT_REPLAY_BUFFER_SIZE = 200;
2028
+ var DEFAULT_REPLAY_BUFFER_MAX_BYTES = 4 * 1024 * 1024;
2029
+ var defaultLogger7 = new NoopFunnelLogger;
2030
+
2031
+ class FunnelBroadcaster {
2032
+ clients = new Map;
2033
+ subscribers = new Set;
2034
+ logger;
2035
+ maxBufferedBytes;
2036
+ now;
2037
+ replayBufferSize;
2038
+ replayBufferMaxBytes;
2039
+ replayBuffer = [];
2040
+ persistentReplay;
2041
+ exclusiveCursor = new Map;
2042
+ replayBufferBytes = 0;
2043
+ eventsBroadcast = 0;
2044
+ droppedSlowClients = 0;
2045
+ lastBroadcastAt = null;
2046
+ latestOffset = 0;
2047
+ constructor(deps = {}) {
2048
+ this.logger = deps.logger ?? defaultLogger7;
2049
+ this.maxBufferedBytes = deps.maxBufferedBytes ?? DEFAULT_MAX_BUFFERED_BYTES;
2050
+ this.now = deps.now ?? (() => Date.now());
2051
+ this.replayBufferSize = Math.max(0, deps.replayBufferSize ?? DEFAULT_REPLAY_BUFFER_SIZE);
2052
+ this.replayBufferMaxBytes = Math.max(0, deps.replayBufferMaxBytes ?? DEFAULT_REPLAY_BUFFER_MAX_BYTES);
2053
+ this.persistentReplay = deps.persistentReplay ?? null;
2054
+ }
2055
+ getMetrics() {
2056
+ return {
2057
+ clients: this.clients.size,
2058
+ subscribers: this.subscribers.size,
2059
+ eventsBroadcast: this.eventsBroadcast,
2060
+ droppedSlowClients: this.droppedSlowClients,
2061
+ lastBroadcastAt: this.lastBroadcastAt ? new Date(this.lastBroadcastAt).toISOString() : null,
2062
+ latestOffset: this.latestOffset,
2063
+ oldestReplayableOffset: this.replayBuffer[0]?.offset ?? null
2064
+ };
2065
+ }
2066
+ replaySince(since, data) {
2067
+ const oldestInMemory = this.replayBuffer[0]?.offset;
2068
+ const needFallback = this.persistentReplay && (oldestInMemory === undefined || since < oldestInMemory - 1);
2069
+ const fromMemory = this.replayBuffer.filter((event) => event.offset > since && this.matchesClient(event, data));
2070
+ if (!needFallback)
2071
+ return fromMemory;
2072
+ const persisted = this.persistentReplay ? this.persistentReplay.loadSince(since).filter((event) => this.matchesClient(event, data)) : [];
2073
+ const cutoff = oldestInMemory ?? Number.POSITIVE_INFINITY;
2074
+ const beforeMemory = persisted.filter((event) => event.offset < cutoff);
2075
+ return [...beforeMemory, ...fromMemory];
2076
+ }
2077
+ matchesClient(event, data) {
2078
+ if (data.tapAll)
2079
+ return true;
2080
+ const channelId = event.meta?.channelId;
2081
+ if (channelId && channelId !== data.channel)
2082
+ return false;
2083
+ const connector = event.meta?.connector;
2084
+ if (!connector)
2085
+ return true;
2086
+ return data.connectors.includes(connector);
2087
+ }
2088
+ pickRecipients(event) {
2089
+ const exclusiveByChannel = new Map;
2090
+ const recipients = [];
2091
+ for (const [ws, data] of this.clients) {
2092
+ if (!this.matchesClient(event, data))
2093
+ continue;
2094
+ if (data.tapAll) {
2095
+ recipients.push(ws);
2096
+ continue;
2097
+ }
2098
+ if (data.delivery === "exclusive") {
2099
+ const list = exclusiveByChannel.get(data.channel) ?? [];
2100
+ list.push(ws);
2101
+ exclusiveByChannel.set(data.channel, list);
2102
+ continue;
2103
+ }
2104
+ recipients.push(ws);
2105
+ }
2106
+ for (const [channel, candidates] of exclusiveByChannel) {
2107
+ if (candidates.length === 0)
2108
+ continue;
2109
+ const cursor = this.exclusiveCursor.get(channel) ?? 0;
2110
+ const picked = candidates[cursor % candidates.length];
2111
+ if (picked)
2112
+ recipients.push(picked);
2113
+ this.exclusiveCursor.set(channel, cursor + 1);
2114
+ }
2115
+ return recipients;
2116
+ }
2117
+ addClient(ws, data) {
2118
+ this.clients.set(ws, data);
2119
+ }
2120
+ removeClient(ws) {
2121
+ this.clients.delete(ws);
2122
+ }
2123
+ getClientCount() {
2124
+ return this.clients.size;
2125
+ }
2126
+ listChannels() {
2127
+ return [...this.clients.values()].map((d) => ({ ...d }));
2128
+ }
2129
+ subscribe(handler) {
2130
+ this.subscribers.add(handler);
2131
+ return () => {
2132
+ this.subscribers.delete(handler);
2133
+ };
2134
+ }
2135
+ broadcast(content, meta) {
2136
+ this.latestOffset += 1;
2137
+ const event = { content, meta, offset: this.latestOffset };
2138
+ const payload = JSON.stringify(event);
2139
+ const connector = meta?.connector;
2140
+ this.eventsBroadcast += 1;
2141
+ this.lastBroadcastAt = this.now();
2142
+ if (this.replayBufferSize > 0) {
2143
+ const eventBytes = byteLengthOf(event);
2144
+ this.replayBuffer.push(event);
2145
+ this.replayBufferBytes += eventBytes;
2146
+ while ((this.replayBuffer.length > this.replayBufferSize || this.replayBufferBytes > this.replayBufferMaxBytes) && this.replayBuffer.length > 0) {
2147
+ const dropped = this.replayBuffer.shift();
2148
+ if (dropped)
2149
+ this.replayBufferBytes -= byteLengthOf(dropped);
2150
+ }
2151
+ }
2152
+ const recipients = this.pickRecipients(event);
2153
+ for (const ws of recipients) {
2154
+ const buffered = ws.getBufferedAmount();
2155
+ if (buffered > this.maxBufferedBytes) {
2156
+ const data = this.clients.get(ws);
2157
+ this.logger.warn("dropping slow WS client (backpressure)", {
2158
+ channel: data?.channel,
2159
+ buffered,
2160
+ max: this.maxBufferedBytes
2161
+ });
2162
+ try {
2163
+ ws.close(1009, "backpressure");
2164
+ } catch {}
2165
+ this.clients.delete(ws);
2166
+ this.droppedSlowClients += 1;
2167
+ continue;
2168
+ }
2169
+ ws.send(payload);
2170
+ }
2171
+ for (const handler of this.subscribers) {
2172
+ try {
2173
+ handler(event);
2174
+ } catch (error) {
2175
+ this.logger.error("broadcast subscriber threw", {
2176
+ error: error instanceof Error ? error.message : String(error)
2177
+ });
2178
+ }
2179
+ }
2180
+ return event;
2181
+ }
2182
+ seedLatestOffset(offset) {
2183
+ if (offset > this.latestOffset)
2184
+ this.latestOffset = offset;
2185
+ }
2186
+ }
2187
+
2188
+ // lib/gateway/funnel-event-store.ts
2189
+ import { z as z10 } from "zod";
2190
+
2191
+ // lib/logger/leuco-logger-sqlite-sink.ts
2192
+ import { Database } from "bun:sqlite";
2193
+ var COLUMN_NAME_RE = /^[a-z_][a-z0-9_]*$/;
2194
+ var RESERVED_COLUMNS = new Set(["seq", "ts", "type", "event"]);
2195
+ var MIGRATIONS = [
2196
+ [
2197
+ "CREATE TABLE IF NOT EXISTS leuco_log (seq INTEGER PRIMARY KEY, ts INTEGER NOT NULL, type TEXT, event TEXT NOT NULL)",
2198
+ "CREATE INDEX IF NOT EXISTS idx_leuco_log_ts ON leuco_log (ts)",
2199
+ "CREATE INDEX IF NOT EXISTS idx_leuco_log_type ON leuco_log (type)"
2200
+ ]
2201
+ ];
2202
+
2203
+ class LeucoLoggerSqliteSink {
2204
+ db;
2205
+ maxRows;
2206
+ maxAgeMs;
2207
+ now;
2208
+ indexes;
2209
+ extractIndexes;
2210
+ insertStmt;
2211
+ insertWithSeqStmt;
2212
+ maxSeqStmt;
2213
+ countStmt;
2214
+ trimRowsStmt;
2215
+ trimAgeStmt;
2216
+ constructor(props) {
2217
+ this.db = new Database(props.path);
2218
+ this.db.run("PRAGMA journal_mode = WAL");
2219
+ this.migrate();
2220
+ this.maxRows = props.maxRows ?? null;
2221
+ this.maxAgeMs = props.maxAgeMs ?? null;
2222
+ this.now = props.now ?? (() => Date.now());
2223
+ this.indexes = props.indexes ?? [];
2224
+ if (this.indexes.length > 0) {
2225
+ validateIndexNames(this.indexes);
2226
+ this.extractIndexes = props.extractIndexes ?? null;
2227
+ this.syncIndexColumns();
2228
+ } else {
2229
+ this.extractIndexes = null;
2230
+ }
2231
+ const cols = ["ts", "type", "event", ...this.indexes];
2232
+ const placeholders = cols.map(() => "?").join(", ");
2233
+ this.insertStmt = this.db.prepare(`INSERT INTO leuco_log (${cols.join(", ")}) VALUES (${placeholders})`);
2234
+ const colsWithSeq = ["seq", ...cols];
2235
+ const placeholdersWithSeq = colsWithSeq.map(() => "?").join(", ");
2236
+ this.insertWithSeqStmt = this.db.prepare(`INSERT INTO leuco_log (${colsWithSeq.join(", ")}) VALUES (${placeholdersWithSeq})`);
2237
+ this.maxSeqStmt = this.db.prepare("SELECT COALESCE(MAX(seq), 0) AS max FROM leuco_log");
2238
+ this.countStmt = this.db.prepare("SELECT COUNT(*) AS n FROM leuco_log");
2239
+ this.trimRowsStmt = this.db.prepare("DELETE FROM leuco_log WHERE seq <= (SELECT seq FROM leuco_log ORDER BY seq DESC LIMIT 1 OFFSET ?)");
2240
+ this.trimAgeStmt = this.db.prepare("DELETE FROM leuco_log WHERE ts < ?");
2241
+ }
2242
+ insert(input) {
2243
+ try {
2244
+ const params = this.buildInsertParams(input.ts, input.event);
2245
+ const result = this.insertStmt.run(...params);
2246
+ const seq = Number(result.lastInsertRowid);
2247
+ this.trim();
2248
+ return { seq, ts: input.ts, event: input.event };
2249
+ } catch (e) {
2250
+ return e instanceof Error ? e : new Error(String(e));
2251
+ }
2252
+ }
2253
+ insertMany(inputs) {
2254
+ if (inputs.length === 0)
2255
+ return [];
2256
+ try {
2257
+ const records = [];
2258
+ const apply = this.db.transaction((batch) => {
2259
+ for (const input of batch) {
2260
+ const params = this.buildInsertParams(input.ts, input.event);
2261
+ const result = this.insertStmt.run(...params);
2262
+ records.push({
2263
+ seq: Number(result.lastInsertRowid),
2264
+ ts: input.ts,
2265
+ event: input.event
2266
+ });
2267
+ }
2268
+ });
2269
+ apply(inputs);
2270
+ this.trim();
2271
+ return records;
2272
+ } catch (e) {
2273
+ return e instanceof Error ? e : new Error(String(e));
2274
+ }
2275
+ }
2276
+ write(record) {
2277
+ try {
2278
+ const params = [
2279
+ record.seq,
2280
+ ...this.buildInsertParams(record.ts, record.event)
2281
+ ];
2282
+ this.insertWithSeqStmt.run(...params);
2283
+ this.trim();
2284
+ } catch (e) {
2285
+ return e instanceof Error ? e : new Error(String(e));
2286
+ }
2287
+ }
2288
+ getMaxSeq() {
2289
+ const row = this.maxSeqStmt.get();
2290
+ return row ? row.max : 0;
2291
+ }
2292
+ getRecords(props = {}) {
2293
+ const conditions = ["seq > ?"];
2294
+ const params = [props.sinceSeq ?? 0];
2295
+ if (typeof props.type === "string") {
2296
+ conditions.push("type = ?");
2297
+ params.push(props.type);
2298
+ }
2299
+ if (props.where) {
2300
+ this.appendWhereConditions(props.where, conditions, params);
2301
+ }
2302
+ const limit = props.limit ?? 1000;
2303
+ params.push(limit);
2304
+ const sql = `SELECT seq, ts, type, event FROM leuco_log WHERE ${conditions.join(" AND ")} ORDER BY seq ASC LIMIT ?`;
2305
+ const stmt = this.db.prepare(sql);
2306
+ return stmt.all(...params).map(toRecord2);
2307
+ }
2308
+ getSchemaVersion() {
2309
+ const row = this.db.prepare("PRAGMA user_version").get();
2310
+ return row?.user_version ?? 0;
2311
+ }
2312
+ close() {
2313
+ this.db.close();
2314
+ }
2315
+ buildInsertParams(ts, event) {
2316
+ const type = extractType(event);
2317
+ const json = JSON.stringify(event);
2318
+ if (this.indexes.length === 0)
2319
+ return [ts, type, json];
2320
+ const values = this.extractIndexes ? this.extractIndexes(event) : null;
2321
+ const indexParams = this.indexes.map((col) => values?.[col] ?? null);
2322
+ return [ts, type, json, ...indexParams];
2323
+ }
2324
+ appendWhereConditions(where, conditions, params) {
2325
+ const widened = where;
2326
+ for (const col of this.indexes) {
2327
+ const value = widened[col];
2328
+ if (value === undefined)
2329
+ continue;
2330
+ if (value === null) {
2331
+ conditions.push(`${col} IS NULL`);
2332
+ } else {
2333
+ conditions.push(`${col} = ?`);
2334
+ params.push(value);
2335
+ }
2336
+ }
2337
+ }
2338
+ trim() {
2339
+ if (this.maxRows !== null) {
2340
+ const row = this.countStmt.get();
2341
+ if (row && row.n > this.maxRows)
2342
+ this.trimRowsStmt.run(this.maxRows);
2343
+ }
2344
+ if (this.maxAgeMs !== null) {
2345
+ this.trimAgeStmt.run(this.now() - this.maxAgeMs);
2346
+ }
2347
+ }
2348
+ syncIndexColumns() {
2349
+ const existing = new Set(this.db.prepare("PRAGMA table_info(leuco_log)").all().map((r) => r.name));
2350
+ for (const col of this.indexes) {
2351
+ if (!existing.has(col)) {
2352
+ this.db.run(`ALTER TABLE leuco_log ADD COLUMN ${col} TEXT`);
2353
+ }
2354
+ this.db.run(`CREATE INDEX IF NOT EXISTS idx_leuco_log_${col} ON leuco_log (${col})`);
2355
+ }
2356
+ }
2357
+ migrate() {
2358
+ const row = this.db.prepare("PRAGMA user_version").get();
2359
+ const current = row?.user_version ?? 0;
2360
+ if (current >= MIGRATIONS.length)
2361
+ return;
2362
+ const pending = MIGRATIONS.slice(current);
2363
+ let version = current;
2364
+ for (const stmts of pending) {
2365
+ version += 1;
2366
+ const apply = this.db.transaction(() => {
2367
+ for (const stmt of stmts)
2368
+ this.db.run(stmt);
2369
+ this.db.run(`PRAGMA user_version = ${version}`);
2370
+ });
2371
+ apply();
2372
+ }
2373
+ }
2374
+ }
2375
+ function validateIndexNames(names) {
2376
+ for (const name of names) {
2377
+ if (!COLUMN_NAME_RE.test(name)) {
2378
+ throw new Error(`invalid index column name: ${name}`);
2379
+ }
2380
+ if (RESERVED_COLUMNS.has(name)) {
2381
+ throw new Error(`reserved index column name: ${name}`);
2382
+ }
2383
+ }
2384
+ }
2385
+ function extractType(event) {
2386
+ if (typeof event !== "object" || event === null)
2387
+ return null;
2388
+ if (!("type" in event))
2389
+ return null;
2390
+ const t = event.type;
2391
+ return typeof t === "string" ? t : null;
2392
+ }
2393
+ function toRecord2(row) {
2394
+ return {
2395
+ seq: row.seq,
2396
+ ts: row.ts,
2397
+ event: JSON.parse(row.event)
2398
+ };
2399
+ }
2400
+
2401
+ // lib/gateway/funnel-event-store.ts
2402
+ var MAX_CONTENT_CHARS = 2000;
2403
+ var funnelEventSchema = z10.object({
2404
+ type: z10.string(),
2405
+ content: z10.string(),
2406
+ channel_id: z10.string().nullable(),
2407
+ connector_id: z10.string().nullable(),
2408
+ meta: z10.record(z10.string(), z10.string()).nullable()
2409
+ });
2410
+
2411
+ class FunnelEventStore {
2412
+ sink;
2413
+ now;
2414
+ constructor(props) {
2415
+ this.now = props.now ?? (() => Date.now());
2416
+ this.sink = new LeucoLoggerSqliteSink({
2417
+ path: props.path,
2418
+ indexes: ["channel_id", "connector_id"],
2419
+ extractIndexes: (event) => ({
2420
+ channel_id: event.channel_id,
2421
+ connector_id: event.connector_id
2422
+ }),
2423
+ now: this.now,
2424
+ ...props.maxRows !== undefined ? { maxRows: props.maxRows } : {},
2425
+ ...props.maxAgeMs !== undefined ? { maxAgeMs: props.maxAgeMs } : {}
2426
+ });
2427
+ }
2428
+ record(props) {
2429
+ const event = {
2430
+ type: props.meta?.event_type ?? "unknown",
2431
+ content: truncate(props.content),
2432
+ channel_id: props.channelId,
2433
+ connector_id: props.connectorId,
2434
+ meta: props.meta
2435
+ };
2436
+ this.sink.write({ seq: props.offset, ts: this.now(), event });
2437
+ }
2438
+ loadSince(since) {
2439
+ const records = this.sink.getRecords({ sinceSeq: since });
2440
+ const out = [];
2441
+ for (const record of records) {
2442
+ out.push({
2443
+ content: record.event.content,
2444
+ meta: record.event.meta ?? undefined,
2445
+ offset: record.seq
2446
+ });
2447
+ }
2448
+ return out;
2449
+ }
2450
+ loadForChannel(props) {
2451
+ const where = {
2452
+ channel_id: props.channelId
2453
+ };
2454
+ if (props.connectorId !== undefined)
2455
+ where.connector_id = props.connectorId;
2456
+ const records = this.sink.getRecords({
2457
+ where,
2458
+ ...props.sinceSeq !== undefined ? { sinceSeq: props.sinceSeq } : {},
2459
+ ...props.limit !== undefined ? { limit: props.limit } : {}
2460
+ });
2461
+ const out = [];
2462
+ for (const record of records) {
2463
+ out.push({
2464
+ content: record.event.content,
2465
+ meta: record.event.meta ?? undefined,
2466
+ offset: record.seq
2467
+ });
2468
+ }
2469
+ return out;
2470
+ }
2471
+ findMaxOffset() {
2472
+ return this.sink.getMaxSeq();
2473
+ }
2474
+ close() {
2475
+ this.sink.close();
2476
+ }
2477
+ }
2478
+ function truncate(content) {
2479
+ if (content.length <= MAX_CONTENT_CHARS)
2480
+ return content;
2481
+ return `${content.slice(0, MAX_CONTENT_CHARS)}...`;
2482
+ }
2483
+
2484
+ // lib/gateway/listener-supervisor.ts
2485
+ var defaultLogger8 = new NodeFunnelLogger;
2486
+ var DEFAULT_HEALTH_INTERVAL_MS = 30000;
2487
+ var DEFAULT_MAX_BACKOFF_MS = 60000;
2488
+ var defaultSleep2 = (ms) => new Promise((r) => {
2489
+ setTimeout(r, ms);
2490
+ });
2491
+
2492
+ class FunnelListenerSupervisor {
2493
+ channels;
2494
+ notify;
2495
+ logger;
2496
+ running = new Map;
2497
+ failureCounts = new Map;
2498
+ stats = new Map;
2499
+ healthCheckIntervalMs;
2500
+ maxBackoffMs;
2501
+ sleep;
2502
+ now;
2503
+ healthCheckTimer = null;
2504
+ healthCheckInFlight = false;
2505
+ constructor(deps) {
2506
+ this.channels = deps.channels;
2507
+ this.notify = deps.notify;
2508
+ this.logger = deps.logger ?? defaultLogger8;
2509
+ this.healthCheckIntervalMs = deps.healthCheckIntervalMs ?? DEFAULT_HEALTH_INTERVAL_MS;
2510
+ this.maxBackoffMs = deps.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
2511
+ this.sleep = deps.sleep ?? defaultSleep2;
2512
+ this.now = deps.now ?? (() => Date.now());
2513
+ }
2514
+ static keyOf(channelName, connectorName) {
2515
+ return `${channelName}/${connectorName}`;
2516
+ }
2517
+ isRunning(channelName, connectorName) {
2518
+ return this.running.has(FunnelListenerSupervisor.keyOf(channelName, connectorName));
2519
+ }
2520
+ list() {
2521
+ return [...this.running.entries()].map(([key, entry]) => {
2522
+ const stats = this.stats.get(key);
2523
+ return {
2524
+ channelName: entry.channelName,
2525
+ channelId: entry.channelId,
2526
+ name: entry.config.name,
2527
+ type: entry.config.type,
2528
+ alive: entry.listener.isAlive(),
2529
+ events: stats?.events ?? 0,
2530
+ errors: stats?.errors ?? 0,
2531
+ failureCount: this.failureCounts.get(key) ?? 0,
2532
+ lastEventAt: stats?.lastEventAt ?? null
2533
+ };
2534
+ });
2535
+ }
2536
+ async start(channelName, connectorName) {
2537
+ const key = FunnelListenerSupervisor.keyOf(channelName, connectorName);
2538
+ if (this.running.has(key)) {
2539
+ return { ok: true, reason: "already running" };
2540
+ }
2541
+ const created = this.channels.createListener(channelName, connectorName);
2542
+ if (!created) {
2543
+ return {
2544
+ ok: false,
2545
+ reason: `connector "${connectorName}" not found in channel "${channelName}"`
2546
+ };
2547
+ }
2548
+ const bind = async (content, meta) => {
2549
+ try {
2550
+ await this.notify(channelName, connectorName, content, meta);
2551
+ this.recordEvent(key);
2552
+ } catch (error) {
2553
+ this.recordError(key);
2554
+ throw error;
2555
+ }
2556
+ };
2557
+ try {
2558
+ await created.listener.start(bind);
2559
+ this.running.set(key, {
2560
+ config: created.config,
2561
+ channelName,
2562
+ channelId: created.channelId,
2563
+ listener: created.listener
2564
+ });
2565
+ this.ensureStats(key);
2566
+ this.logger.info(`${created.config.type} listener started`, {
2567
+ channel: channelName,
2568
+ connector: connectorName
2569
+ });
2570
+ return { ok: true };
2571
+ } catch (error) {
2572
+ this.logger.error(`${created.config.type} listener failed to start`, {
2573
+ channel: channelName,
2574
+ connector: connectorName,
2575
+ error: error instanceof Error ? error.message : String(error)
2576
+ });
2577
+ return {
2578
+ ok: false,
2579
+ reason: error instanceof Error ? error.message : String(error)
2580
+ };
2581
+ }
2582
+ }
2583
+ async stop(channelName, connectorName) {
2584
+ const key = FunnelListenerSupervisor.keyOf(channelName, connectorName);
2585
+ const entry = this.running.get(key);
2586
+ if (!entry)
2587
+ return { ok: true, reason: "not running" };
2588
+ try {
2589
+ await entry.listener.stop();
2590
+ this.running.delete(key);
2591
+ this.failureCounts.delete(key);
2592
+ this.logger.info(`${entry.config.type} listener stopped`, {
2593
+ channel: channelName,
2594
+ connector: connectorName
2595
+ });
2596
+ return { ok: true };
2597
+ } catch (error) {
2598
+ this.logger.error(`${entry.config.type} listener failed to stop`, {
2599
+ channel: channelName,
2600
+ connector: connectorName,
2601
+ error: error instanceof Error ? error.message : String(error)
2602
+ });
2603
+ return {
2604
+ ok: false,
2605
+ reason: error instanceof Error ? error.message : String(error)
2606
+ };
2607
+ }
2608
+ }
2609
+ async restart(channelName, connectorName) {
2610
+ const stopped = await this.stop(channelName, connectorName);
2611
+ if (!stopped.ok)
2612
+ return stopped;
2613
+ return await this.start(channelName, connectorName);
2614
+ }
2615
+ async startAll() {
2616
+ const all = this.channels.listAllConnectors();
2617
+ for (const view of all) {
2618
+ await this.start(view.channelName, view.name);
2619
+ }
2620
+ this.startHealthCheck();
2621
+ }
2622
+ async stopAll() {
2623
+ this.stopHealthCheck();
2624
+ for (const [, entry] of [...this.running.entries()]) {
2625
+ await this.stop(entry.channelName, entry.config.name);
2626
+ }
2627
+ }
2628
+ ensureStats(key) {
2629
+ const existing = this.stats.get(key);
2630
+ if (existing)
2631
+ return existing;
2632
+ const fresh = { events: 0, errors: 0, failureCount: 0, lastEventAt: null };
2633
+ this.stats.set(key, fresh);
2634
+ return fresh;
2635
+ }
2636
+ recordEvent(key) {
2637
+ const stats = this.ensureStats(key);
2638
+ stats.events += 1;
2639
+ stats.lastEventAt = new Date(this.now()).toISOString();
2640
+ }
2641
+ recordError(key) {
2642
+ this.ensureStats(key).errors += 1;
2643
+ }
2644
+ startHealthCheck() {
2645
+ if (this.healthCheckTimer)
2646
+ return;
2647
+ this.healthCheckTimer = setInterval(() => {
2648
+ this.runHealthCheck();
2649
+ }, this.healthCheckIntervalMs);
2650
+ this.healthCheckTimer.unref();
2651
+ }
2652
+ stopHealthCheck() {
2653
+ if (!this.healthCheckTimer)
2654
+ return;
2655
+ clearInterval(this.healthCheckTimer);
2656
+ this.healthCheckTimer = null;
2657
+ }
2658
+ async runHealthCheck() {
2659
+ if (this.healthCheckInFlight)
2660
+ return;
2661
+ this.healthCheckInFlight = true;
2662
+ try {
2663
+ for (const [key, entry] of [...this.running.entries()]) {
2664
+ if (entry.listener.isAlive()) {
2665
+ this.failureCounts.delete(key);
2666
+ continue;
2667
+ }
2668
+ await this.recoverDead(entry.channelName, entry.config.name, entry.config.type);
2669
+ }
2670
+ } finally {
2671
+ this.healthCheckInFlight = false;
2672
+ }
2673
+ }
2674
+ async recoverDead(channelName, connectorName, type) {
2675
+ const key = FunnelListenerSupervisor.keyOf(channelName, connectorName);
2676
+ const failureCount = this.failureCounts.get(key) ?? 0;
2677
+ const backoffMs = Math.min(1000 * 2 ** failureCount, this.maxBackoffMs);
2678
+ this.logger.warn(`${type} listener unhealthy, restarting`, {
2679
+ channel: channelName,
2680
+ connector: connectorName,
2681
+ attempt: failureCount + 1,
2682
+ backoffMs
2683
+ });
2684
+ await this.stop(channelName, connectorName);
2685
+ await this.sleep(backoffMs);
2686
+ const result = await this.start(channelName, connectorName);
2687
+ if (result.ok) {
2688
+ this.failureCounts.delete(key);
2689
+ this.logger.info(`${type} listener recovered`, {
2690
+ channel: channelName,
2691
+ connector: connectorName
2692
+ });
2693
+ } else {
2694
+ this.failureCounts.set(key, failureCount + 1);
2695
+ }
2696
+ }
2697
+ }
2698
+
2699
+ // lib/gateway/kill-competing-slack-gateways.ts
2700
+ var defaultProcess6 = new NodeFunnelProcessRunner;
2701
+ var defaultLogger9 = new NodeFunnelLogger;
2702
+ var isBun = (args) => {
2703
+ return args.includes("bun ") || /\/bun(\s|$)/.test(args);
2704
+ };
2705
+ var looksLikeSlackGateway = (args) => {
2706
+ return /(gateway|bolt|slack)/i.test(args);
2707
+ };
2708
+ var killCompetingSlackGateways = async (props) => {
2709
+ const runner = props.process ?? defaultProcess6;
2710
+ const logger = props.logger ?? defaultLogger9;
2711
+ const result = await runner.run(["ps", "-e", "-o", "pid=,args="]);
2712
+ if (result.exitCode !== 0)
2713
+ return [];
2714
+ const killed = [];
2715
+ for (const raw of result.stdout.split(`
2716
+ `)) {
2717
+ const line = raw.trim();
2718
+ if (!line)
2719
+ continue;
2720
+ const match = /^(\d+)\s+(.+)$/.exec(line);
2721
+ if (!match)
2722
+ continue;
2723
+ const pid = Number(match[1]);
2724
+ const args = match[2];
2725
+ if (!Number.isInteger(pid) || pid <= 0)
2726
+ continue;
2727
+ if (pid === props.selfPid)
2728
+ continue;
2729
+ if (!isBun(args))
2730
+ continue;
2731
+ if (!looksLikeSlackGateway(args))
2732
+ continue;
2733
+ runner.kill(pid, "SIGTERM");
2734
+ killed.push(pid);
2735
+ logger.info("killed competing Slack gateway process", { pid, args: args.slice(0, 160) });
2736
+ }
2737
+ return killed;
2738
+ };
2739
+
2740
+ // lib/gateway/routes/channels.connectors.call.ts
2741
+ import { HTTPException } from "hono/http-exception";
2742
+ import { z as z11 } from "zod";
2743
+
2744
+ // lib/gateway/routes/validator.ts
2745
+ import { zValidator } from "@hono/zod-validator";
2746
+ var zParam = (schema) => zValidator("param", schema, (result, c) => {
2747
+ if (result.success)
2748
+ return;
2749
+ const issue = result.error.issues[0];
2750
+ const reason = issue ? `${issue.path.join(".")}: ${issue.message}` : "invalid request";
2751
+ return c.json({ ok: false, reason }, 400);
2752
+ });
2753
+
2754
+ // lib/gateway/routes/channels.connectors.call.ts
2755
+ var bodySchema = z11.object({
2756
+ method: z11.string().min(1),
2757
+ path: z11.string().min(1),
2758
+ body: z11.unknown().optional()
2759
+ });
2760
+ var channelsConnectorsCallHandler = factory.createHandlers(zParam(z11.object({ channel: z11.string().min(1), connector: z11.string().min(1) })), async (c) => {
2761
+ const param = c.req.valid("param");
2762
+ const raw = await c.req.json().catch(() => null);
2763
+ const parsed = bodySchema.safeParse(raw);
2764
+ if (!parsed.success) {
2765
+ throw new HTTPException(400, { message: parsed.error.issues[0]?.message ?? "invalid body" });
2766
+ }
2767
+ const result = await c.var.deps.channels.call(param.channel, param.connector, {
2768
+ method: parsed.data.method,
2769
+ path: parsed.data.path,
2770
+ body: parsed.data.body ?? {}
2771
+ });
2772
+ return c.json({ ok: true, result });
2773
+ });
2774
+
2775
+ // lib/gateway/routes/health.ts
2776
+ var healthHandler = factory.createHandlers((c) => {
2777
+ const deps = c.var.deps;
2778
+ return c.json({
2779
+ ok: true,
2780
+ pid: deps.selfPid,
2781
+ clients: deps.broadcaster.getClientCount(),
2782
+ listeners: deps.supervisor.list()
2783
+ });
2784
+ });
2785
+
2786
+ // lib/gateway/routes/listeners.list.ts
2787
+ var listenersListHandler = factory.createHandlers((c) => {
2788
+ return c.json({ listeners: c.var.deps.supervisor.list() });
2789
+ });
2790
+
2791
+ // lib/gateway/routes/listeners.restart.ts
2792
+ import { z as z12 } from "zod";
2793
+ var listenersRestartHandler = factory.createHandlers(zParam(z12.object({ channel: z12.string().min(1), connector: z12.string().min(1) })), async (c) => {
2794
+ const param = c.req.valid("param");
2795
+ const result = await c.var.deps.supervisor.restart(param.channel, param.connector);
2796
+ return c.json(result, result.ok ? 200 : 400);
2797
+ });
2798
+
2799
+ // lib/gateway/routes/listeners.start.ts
2800
+ import { z as z13 } from "zod";
2801
+ var listenersStartHandler = factory.createHandlers(zParam(z13.object({ channel: z13.string().min(1), connector: z13.string().min(1) })), async (c) => {
2802
+ const param = c.req.valid("param");
2803
+ const result = await c.var.deps.supervisor.start(param.channel, param.connector);
2804
+ return c.json(result, result.ok ? 200 : 400);
2805
+ });
2806
+
2807
+ // lib/gateway/routes/listeners.stop.ts
2808
+ import { z as z14 } from "zod";
2809
+ var listenersStopHandler = factory.createHandlers(zParam(z14.object({ channel: z14.string().min(1), connector: z14.string().min(1) })), async (c) => {
2810
+ const param = c.req.valid("param");
2811
+ const result = await c.var.deps.supervisor.stop(param.channel, param.connector);
2812
+ return c.json(result, result.ok ? 200 : 400);
2813
+ });
2814
+
2815
+ // lib/gateway/routes/status.ts
2816
+ var statusHandler = factory.createHandlers((c) => {
2817
+ const deps = c.var.deps;
2818
+ return c.json({
2819
+ ok: true,
2820
+ pid: deps.selfPid,
2821
+ uptimeMs: deps.uptimeMs(),
2822
+ clients: deps.broadcaster.listChannels(),
2823
+ listeners: deps.supervisor.list(),
2824
+ broadcaster: deps.broadcaster.getMetrics()
2825
+ });
2826
+ });
2827
+
2828
+ // lib/gateway/routes/index.ts
2829
+ var gatewayRoutes = factory.createApp().get("/health", ...healthHandler).get("/status", ...statusHandler).get("/listeners", ...listenersListHandler).post("/listeners/:channel/:connector/start", ...listenersStartHandler).delete("/listeners/:channel/:connector", ...listenersStopHandler).post("/listeners/:channel/:connector/restart", ...listenersRestartHandler).post("/channels/:channel/connectors/:connector/call", ...channelsConnectorsCallHandler);
2830
+
2831
+ // lib/gateway/gateway-server.ts
2832
+ var DEFAULT_PORT2 = 9742;
2833
+ var DEFAULT_LOG_DIR = "/tmp/funnel/events";
2834
+ var DB_FILENAME = "events.db";
2835
+ var defaultLogger10 = new NodeFunnelLogger;
2836
+
2837
+ class FunnelGatewayServer {
2838
+ channels;
2839
+ settings;
2840
+ port;
2841
+ logDir;
2842
+ process;
2843
+ logger;
2844
+ selfPid;
2845
+ killCompetingSlack;
2846
+ token;
2847
+ broadcaster;
2848
+ eventStore;
2849
+ supervisor;
2850
+ nowMs;
2851
+ startedAt = null;
2852
+ server = null;
2853
+ constructor(deps) {
2854
+ this.channels = deps.channels;
2855
+ this.settings = deps.settings;
2856
+ this.port = deps.port ?? DEFAULT_PORT2;
2857
+ this.logDir = deps.logDir ?? DEFAULT_LOG_DIR;
2858
+ this.process = deps.process;
2859
+ this.logger = deps.logger ?? defaultLogger10;
2860
+ this.selfPid = deps.selfPid ?? globalThis.process.pid;
2861
+ this.killCompetingSlack = deps.killCompetingSlack ?? true;
2862
+ this.token = deps.token ?? "";
2863
+ const clock = deps.clock;
2864
+ this.nowMs = clock ? () => clock.millis() : () => Date.now();
2865
+ if (!existsSync3(this.logDir))
2866
+ mkdirSync3(this.logDir, { recursive: true });
2867
+ this.eventStore = new FunnelEventStore({
2868
+ path: join7(this.logDir, DB_FILENAME),
2869
+ now: this.nowMs
2870
+ });
2871
+ this.broadcaster = new FunnelBroadcaster({
2872
+ logger: this.logger,
2873
+ now: this.nowMs,
2874
+ persistentReplay: this.eventStore
2875
+ });
2876
+ this.broadcaster.seedLatestOffset(this.eventStore.findMaxOffset());
2877
+ this.supervisor = new FunnelListenerSupervisor({
2878
+ channels: this.channels,
2879
+ logger: this.logger,
2880
+ notify: (channelName, connectorName, content, meta) => this.notify(channelName, connectorName, content, meta),
2881
+ now: this.nowMs
2882
+ });
2883
+ }
2884
+ async start() {
2885
+ if (this.server)
2886
+ return this.server;
2887
+ const app = this.buildApp();
2888
+ this.startedAt = this.nowMs();
2889
+ this.server = Bun.serve({
2890
+ port: this.port,
2891
+ development: false,
2892
+ fetch: (request, server) => this.handleFetch(request, server, app),
2893
+ websocket: {
2894
+ open: (ws) => this.handleWsOpen(ws),
2895
+ close: (ws) => this.handleWsClose(ws),
2896
+ message() {}
2897
+ }
2898
+ });
2899
+ this.logServerStarted();
2900
+ await this.bootListeners();
2901
+ return this.server;
2902
+ }
2903
+ async stop() {
2904
+ await this.supervisor.stopAll();
2905
+ if (this.server) {
2906
+ this.server.stop();
2907
+ this.server = null;
2908
+ }
2909
+ }
2910
+ getStatus() {
2911
+ return {
2912
+ clients: this.broadcaster.getClientCount(),
2913
+ channels: this.broadcaster.listChannels()
2914
+ };
2915
+ }
2916
+ getBroadcaster() {
2917
+ return this.broadcaster;
2918
+ }
2919
+ getSupervisor() {
2920
+ return this.supervisor;
2921
+ }
2922
+ getEventStore() {
2923
+ return this.eventStore;
2924
+ }
2925
+ handleFetch(request, server, app) {
2926
+ const url = new URL(request.url);
2927
+ if (url.pathname === "/ws" && request.headers.get("upgrade") === "websocket") {
2928
+ if (this.token && !this.tokenMatchesUpgrade(request)) {
2929
+ return new Response("unauthorized", { status: 401 });
2930
+ }
2931
+ const tapAll = url.searchParams.get("tap") === "all";
2932
+ const requestedChannel = tapAll ? "" : url.searchParams.get("channel") ?? "";
2933
+ const channel = !tapAll && requestedChannel ? this.resolveChannel(requestedChannel) : null;
2934
+ const channelId = tapAll ? "" : channel?.id ?? requestedChannel;
2935
+ const channelName = tapAll ? null : channel?.name ?? null;
2936
+ const connectors = channel?.connectors ?? [];
2937
+ const delivery = channel?.delivery ?? "fanout";
2938
+ const sinceRaw = url.searchParams.get("since");
2939
+ const sinceParsed = sinceRaw === null ? Number.NaN : Number.parseInt(sinceRaw, 10);
2940
+ const since = Number.isFinite(sinceParsed) && sinceParsed >= 0 ? sinceParsed : undefined;
2941
+ const upgraded = server.upgrade(request, {
2942
+ data: {
2943
+ channel: channelId,
2944
+ channelName,
2945
+ connectors,
2946
+ tapAll,
2947
+ delivery,
2948
+ since
2949
+ }
2950
+ });
2951
+ if (upgraded)
2952
+ return;
2953
+ return new Response("WebSocket upgrade failed", { status: 400 });
2954
+ }
2955
+ return app.fetch(request);
2956
+ }
2957
+ handleWsOpen(ws) {
2958
+ if (typeof ws.data.since === "number") {
2959
+ const replay = this.broadcaster.replaySince(ws.data.since, ws.data);
2960
+ for (const event of replay)
2961
+ ws.send(JSON.stringify(event));
2962
+ }
2963
+ this.broadcaster.addClient(ws, ws.data);
2964
+ if (ws.data.channelName) {
2965
+ const meta = {
2966
+ event_type: "system",
2967
+ action: "channel_connect",
2968
+ channel: ws.data.channelName,
2969
+ channelId: ws.data.channel,
2970
+ connectors: ws.data.connectors.join(","),
2971
+ total: String(this.broadcaster.getClientCount())
2972
+ };
2973
+ this.logger.info("channel connected", meta);
2974
+ } else {
2975
+ this.logger.info("tap-all client connected", {
2976
+ event_type: "system",
2977
+ action: "tap_connect",
2978
+ total: String(this.broadcaster.getClientCount())
2979
+ });
2980
+ }
2981
+ }
2982
+ handleWsClose(ws) {
2983
+ this.broadcaster.removeClient(ws);
2984
+ if (ws.data.channelName) {
2985
+ this.logger.info("channel disconnected", {
2986
+ event_type: "system",
2987
+ action: "channel_disconnect",
2988
+ channel: ws.data.channelName,
2989
+ channelId: ws.data.channel,
2990
+ total: String(this.broadcaster.getClientCount())
2991
+ });
2992
+ } else {
2993
+ this.logger.info("tap-all client disconnected", {
2994
+ event_type: "system",
2995
+ action: "tap_disconnect",
2996
+ total: String(this.broadcaster.getClientCount())
2997
+ });
2998
+ }
2999
+ }
3000
+ logServerStarted() {
3001
+ this.logger.info("gateway started", {
3002
+ event_type: "system",
3003
+ action: "gateway_start",
3004
+ port: String(this.port),
3005
+ pid: String(this.selfPid)
3006
+ });
3007
+ this.logger.info("funnel gateway listening", {
3008
+ url: `http://localhost:${this.port}`,
3009
+ websocket: `ws://localhost:${this.port}/ws`,
3010
+ health: `http://localhost:${this.port}/health`
3011
+ });
3012
+ }
3013
+ buildApp() {
3014
+ const base = factory.createApp();
3015
+ base.use((c, next) => {
3016
+ c.set("deps", {
3017
+ selfPid: this.selfPid,
3018
+ broadcaster: this.broadcaster,
3019
+ supervisor: this.supervisor,
3020
+ channels: this.channels,
3021
+ uptimeMs: () => this.startedAt ? this.nowMs() - this.startedAt : 0
3022
+ });
3023
+ return next();
3024
+ });
3025
+ if (this.token) {
3026
+ base.use("/listeners/*", requireBearerToken({ expected: this.token }));
3027
+ base.use("/status", requireBearerToken({ expected: this.token }));
3028
+ base.use("/channels/*", requireBearerToken({ expected: this.token }));
3029
+ }
3030
+ return base.route("/", gatewayRoutes);
3031
+ }
3032
+ tokenMatchesUpgrade(request) {
3033
+ const protocols = (request.headers.get("sec-websocket-protocol") ?? "").split(",").map((p) => p.trim()).filter((p) => p.length > 0);
3034
+ for (const proto of protocols) {
3035
+ if (proto.startsWith("funnel.token.") && constantTimeEqual(proto.slice("funnel.token.".length), this.token)) {
3036
+ return true;
3037
+ }
3038
+ }
3039
+ const auth = request.headers.get("authorization") ?? "";
3040
+ const match = auth.match(/^Bearer\s+(.+)$/i);
3041
+ if (match && constantTimeEqual(match[1] ?? "", this.token))
3042
+ return true;
3043
+ return false;
3044
+ }
3045
+ resolveChannel(requested) {
3046
+ const settings = this.settings.read();
3047
+ const channel = settings?.channels.find((c) => c.id === requested || c.name === requested);
3048
+ if (!channel)
3049
+ return null;
3050
+ return {
3051
+ id: channel.id,
3052
+ name: channel.name,
3053
+ connectors: channel.connectors.map((c) => c.name),
3054
+ delivery: channel.delivery
3055
+ };
3056
+ }
3057
+ async bootListeners() {
3058
+ const allConnectors = this.channels.listAllConnectors();
3059
+ if (this.killCompetingSlack && allConnectors.some((c) => c.type === "slack")) {
3060
+ const killed = await killCompetingSlackGateways({
3061
+ selfPid: this.selfPid,
3062
+ process: this.process,
3063
+ logger: this.logger
3064
+ });
3065
+ if (killed.length > 0) {
3066
+ this.logger.info("killed competing Slack gateway processes", {
3067
+ event_type: "system",
3068
+ action: "kill_competing",
3069
+ pids: killed.join(",")
3070
+ });
3071
+ }
3072
+ }
3073
+ await this.supervisor.startAll();
3074
+ for (const entry of this.supervisor.list()) {
3075
+ this.logger.info(`${entry.type} listener started: ${entry.name}`, {
3076
+ event_type: "system",
3077
+ action: `${entry.type}_connect`,
3078
+ channel: entry.channelName,
3079
+ connector: entry.name
3080
+ });
3081
+ }
3082
+ this.logger.info(`event store: ${join7(this.logDir, DB_FILENAME)}`);
3083
+ this.logger.info("funnel gateway running");
3084
+ }
3085
+ async notify(channelName, connectorName, content, meta) {
3086
+ const channelId = this.lookupChannelId(channelName);
3087
+ const connectorId = channelId ? this.lookupConnectorId(channelId, connectorName) : null;
3088
+ const enriched = {
3089
+ ...meta,
3090
+ channel: channelName,
3091
+ connector: connectorName
3092
+ };
3093
+ if (channelId)
3094
+ enriched.channelId = channelId;
3095
+ if (connectorId)
3096
+ enriched.connectorId = connectorId;
3097
+ const event = this.broadcaster.broadcast(content, enriched);
3098
+ this.eventStore.record({
3099
+ content,
3100
+ channelId: channelId ?? null,
3101
+ connectorId: connectorId ?? null,
3102
+ meta: enriched,
3103
+ offset: event.offset
3104
+ });
3105
+ }
3106
+ lookupChannelId(channelName) {
3107
+ const channel = this.settings.read().channels.find((c) => c.name === channelName);
3108
+ return channel?.id ?? null;
3109
+ }
3110
+ lookupConnectorId(channelId, connectorName) {
3111
+ const channel = this.settings.read().channels.find((c) => c.id === channelId);
3112
+ const connector = channel?.connectors.find((c) => c.name === connectorName);
3113
+ return connector?.id ?? null;
3114
+ }
3115
+ }
3116
+
3117
+ // lib/gateway/gateway-token.ts
3118
+ import { homedir as homedir2 } from "os";
3119
+ import { dirname as dirname4, join as join8 } from "path";
3120
+ var TOKEN_FILE_NAME = "gateway.token";
3121
+ var TOKEN_BYTES = 32;
3122
+ var defaultFs7 = new NodeFunnelFileSystem;
3123
+ var defaultGenerate = () => {
3124
+ const buf = new Uint8Array(TOKEN_BYTES);
3125
+ crypto.getRandomValues(buf);
3126
+ return [...buf].map((b) => b.toString(16).padStart(2, "0")).join("");
3127
+ };
3128
+
3129
+ class FunnelGatewayToken {
3130
+ fs;
3131
+ path;
3132
+ generate;
3133
+ constructor(deps = {}) {
3134
+ this.fs = deps.fs ?? defaultFs7;
3135
+ this.path = join8(deps.dir ?? FUNNEL_DIR, TOKEN_FILE_NAME);
3136
+ this.generate = deps.generate ?? defaultGenerate;
3137
+ Object.freeze(this);
3138
+ }
3139
+ read() {
3140
+ if (!this.fs.existsSync(this.path))
3141
+ return null;
3142
+ const value = this.fs.readFileSync(this.path).trim();
3143
+ return value.length > 0 ? value : null;
3144
+ }
3145
+ ensure() {
3146
+ const existing = this.read();
3147
+ if (existing)
3148
+ return existing;
3149
+ const token = this.generate();
3150
+ this.fs.mkdirSync(dirname4(this.path), { recursive: true });
3151
+ this.fs.writeSecretFileSync(this.path, `${token}
3152
+ `);
3153
+ return token;
3154
+ }
3155
+ getPath() {
3156
+ return this.path;
3157
+ }
3158
+ }
3159
+ var DEFAULT_GATEWAY_TOKEN_PATH = join8(homedir2(), ".funnel", TOKEN_FILE_NAME);
3160
+
3161
+ // lib/gateway/listeners-client.ts
3162
+ import { z as z15 } from "zod";
3163
+ var listenerEntrySchema = z15.object({
3164
+ channelName: z15.string(),
3165
+ channelId: z15.string(),
3166
+ name: z15.string(),
3167
+ type: z15.string(),
3168
+ alive: z15.boolean()
3169
+ });
3170
+ var listenersResponseSchema = z15.object({
3171
+ listeners: z15.array(listenerEntrySchema)
3172
+ });
3173
+ var opErrorBodySchema = z15.object({
3174
+ reason: z15.string().optional()
3175
+ });
3176
+ var OFFLINE = { state: "offline" };
3177
+
3178
+ class FunnelListenersClient {
3179
+ port;
3180
+ isDaemonRunning;
3181
+ getToken;
3182
+ constructor(deps) {
3183
+ this.port = deps.port;
3184
+ this.isDaemonRunning = deps.isDaemonRunning;
3185
+ this.getToken = deps.getToken ?? (() => null);
3186
+ Object.freeze(this);
3187
+ }
3188
+ async list() {
3189
+ if (!this.isDaemonRunning())
3190
+ return { state: "offline" };
3191
+ try {
3192
+ const res = await fetch(`http://localhost:${this.port}/listeners`, {
3193
+ headers: this.authHeaders()
3194
+ });
3195
+ if (!res.ok)
3196
+ return { state: "error", reason: `HTTP ${res.status}` };
3197
+ const parsed = listenersResponseSchema.safeParse(await res.json());
3198
+ if (!parsed.success) {
3199
+ return { state: "error", reason: "malformed daemon response" };
3200
+ }
3201
+ return { state: "ok", listeners: parsed.data.listeners };
3202
+ } catch (error) {
3203
+ return { state: "error", reason: error instanceof Error ? error.message : String(error) };
3204
+ }
3205
+ }
3206
+ async start(channelName, connectorName) {
3207
+ if (!this.isDaemonRunning())
3208
+ return OFFLINE;
3209
+ return await this.call("POST", `/listeners/${this.path(channelName, connectorName)}/start`);
3210
+ }
3211
+ async stop(channelName, connectorName) {
3212
+ if (!this.isDaemonRunning())
3213
+ return OFFLINE;
3214
+ return await this.call("DELETE", `/listeners/${this.path(channelName, connectorName)}`);
3215
+ }
3216
+ async restart(channelName, connectorName) {
3217
+ if (!this.isDaemonRunning())
3218
+ return OFFLINE;
3219
+ return await this.call("POST", `/listeners/${this.path(channelName, connectorName)}/restart`);
3220
+ }
3221
+ path(channelName, connectorName) {
3222
+ return `${encodeURIComponent(channelName)}/${encodeURIComponent(connectorName)}`;
3223
+ }
3224
+ authHeaders() {
3225
+ const token = this.getToken();
3226
+ return token ? { authorization: `Bearer ${token}` } : {};
3227
+ }
3228
+ async call(method, path) {
3229
+ try {
3230
+ const res = await fetch(`http://localhost:${this.port}${path}`, {
3231
+ method,
3232
+ headers: this.authHeaders()
3233
+ });
3234
+ if (!res.ok) {
3235
+ const parsed = opErrorBodySchema.safeParse(await res.json().catch(() => null));
3236
+ const reason = parsed.success ? parsed.data.reason : undefined;
3237
+ return { state: "error", reason: reason ?? `HTTP ${res.status}` };
3238
+ }
3239
+ return { state: "ok" };
3240
+ } catch (error) {
3241
+ return { state: "error", reason: error instanceof Error ? error.message : String(error) };
3242
+ }
3243
+ }
3244
+ }
3245
+
3246
+ // lib/funnel.ts
3247
+ class Funnel {
3248
+ props;
3249
+ constructor(props = {}) {
3250
+ this.props = props;
3251
+ Object.freeze(this);
3252
+ }
3253
+ get store() {
3254
+ return this.props.store ?? new FunnelSettingsStore({
3255
+ path: join9(this.props.dir ?? FUNNEL_DIR, "settings.json"),
3256
+ fs: this.props.fs
3257
+ });
3258
+ }
3259
+ get process() {
3260
+ return this.props.process ?? new NodeFunnelProcessRunner;
3261
+ }
3262
+ get logger() {
3263
+ return this.props.logger ?? new NodeFunnelLogger;
3264
+ }
3265
+ get factory() {
3266
+ return new FunnelConnectorFactory({
3267
+ fs: this.props.fs,
3268
+ process: this.props.process,
3269
+ logger: this.props.logger,
3270
+ dir: this.props.dir
3271
+ });
3272
+ }
3273
+ get channels() {
3274
+ return new FunnelChannels({
3275
+ store: this.store,
3276
+ factory: this.factory,
3277
+ profileChecker: this.profiles,
3278
+ clock: this.props.clock,
3279
+ idGenerator: this.props.idGenerator
3280
+ });
3281
+ }
3282
+ get profiles() {
3283
+ return new FunnelProfiles({ store: this.store });
3284
+ }
3285
+ get mcp() {
3286
+ return new FunnelMcp({ fs: this.props.fs });
3287
+ }
3288
+ get claude() {
3289
+ return new FunnelClaude({
3290
+ channels: this.channels,
3291
+ mcp: this.mcp,
3292
+ gateway: this.gateway,
3293
+ fs: this.props.fs,
3294
+ process: this.props.process,
3295
+ logger: this.props.logger,
3296
+ dir: this.props.dir
3297
+ });
3298
+ }
3299
+ get gateway() {
3300
+ return new FunnelGateway({
3301
+ fs: this.props.fs,
3302
+ process: this.props.process,
3303
+ clock: this.props.clock,
3304
+ dir: this.props.dir,
3305
+ tmpDir: this.props.tmpDir
3306
+ });
3307
+ }
3308
+ get gatewayToken() {
3309
+ return new FunnelGatewayToken({ fs: this.props.fs, dir: this.props.dir });
3310
+ }
3311
+ get listeners() {
3312
+ const gateway = this.gateway;
3313
+ const token = this.gatewayToken;
3314
+ return new FunnelListenersClient({
3315
+ port: gateway.getPort(),
3316
+ isDaemonRunning: () => gateway.isRunning(),
3317
+ getToken: () => token.read()
3318
+ });
3319
+ }
3320
+ gatewayServer(options = {}) {
3321
+ return new FunnelGatewayServer({
3322
+ channels: this.channels,
3323
+ settings: this.store,
3324
+ port: options.port,
3325
+ logDir: options.logDir,
3326
+ process: this.props.process,
3327
+ clock: this.props.clock,
3328
+ logger: this.props.logger,
3329
+ killCompetingSlack: options.killCompetingSlack,
3330
+ token: options.token ?? this.gatewayToken.ensure()
3331
+ });
3332
+ }
3333
+ }
3334
+ // lib/engine/settings/mock-settings-reader.ts
3335
+ var createSettings = (partial = {}) => ({
3336
+ version: SETTINGS_VERSION,
3337
+ channels: [],
3338
+ profiles: [],
3339
+ ...partial
3340
+ });
3341
+
3342
+ class MockFunnelSettingsReader extends FunnelSettingsReader {
3343
+ state;
3344
+ constructor(initial) {
3345
+ super();
3346
+ this.state = createSettings(initial);
3347
+ }
3348
+ read() {
3349
+ return this.state;
3350
+ }
3351
+ write(settings) {
3352
+ this.state = settings;
3353
+ }
3354
+ }
3355
+ // lib/engine/fs/memory-file-system.ts
3356
+ var SECRET_MODE2 = 384;
3357
+
3358
+ class MemoryFunnelFileSystem extends FunnelFileSystem {
3359
+ dirs;
3360
+ files;
3361
+ mtimes;
3362
+ modes;
3363
+ now;
3364
+ constructor(props = {}) {
3365
+ super();
3366
+ this.dirs = new Set(props.dirs ?? []);
3367
+ this.files = new Map(Object.entries(props.files ?? {}));
3368
+ this.mtimes = new Map(Object.entries(props.mtimes ?? {}));
3369
+ this.modes = new Map(Object.entries(props.modes ?? {}));
3370
+ this.now = props.now ?? (() => Date.now());
3371
+ }
3372
+ existsSync(path) {
3373
+ return this.dirs.has(path) || this.files.has(path);
3374
+ }
3375
+ readFileSync(path) {
3376
+ return this.files.get(path) ?? "";
3377
+ }
3378
+ writeFileSync(path, data) {
3379
+ this.files.set(path, data);
3380
+ this.touch(path);
3381
+ }
3382
+ writeSecretFileSync(path, data) {
3383
+ this.files.set(path, data);
3384
+ this.modes.set(path, SECRET_MODE2);
3385
+ this.touch(path);
3386
+ }
3387
+ appendFileSync(path, data) {
3388
+ const prev = this.files.get(path) ?? "";
3389
+ this.files.set(path, prev + data);
3390
+ this.touch(path);
3391
+ }
3392
+ unlink(path) {
3393
+ this.files.delete(path);
3394
+ this.mtimes.delete(path);
3395
+ this.modes.delete(path);
3396
+ }
3397
+ mkdirSync(path, options) {
3398
+ this.dirs.add(path);
3399
+ }
3400
+ readdirSync(path) {
3401
+ const prefix = path.endsWith("/") ? path : `${path}/`;
3402
+ const names = [];
3403
+ for (const file of this.files.keys()) {
3404
+ if (!file.startsWith(prefix))
3405
+ continue;
3406
+ const rest = file.slice(prefix.length);
3407
+ if (!rest.includes("/"))
3408
+ names.push(rest);
3409
+ }
3410
+ return names;
3411
+ }
3412
+ statSync(path) {
3413
+ const mtimeMs = this.mtimes.get(path);
3414
+ if (mtimeMs === undefined) {
3415
+ throw new Error(`not found: ${path}`);
3416
+ }
3417
+ return { mtimeMs, mode: this.modes.get(path) ?? null };
3418
+ }
3419
+ setMtime(path, mtimeMs) {
3420
+ this.mtimes.set(path, mtimeMs);
3421
+ }
3422
+ setMode(path, mode) {
3423
+ this.modes.set(path, mode);
3424
+ }
3425
+ touch(path) {
3426
+ if (!this.mtimes.has(path))
3427
+ this.mtimes.set(path, this.now());
3428
+ else
3429
+ this.mtimes.set(path, this.now());
3430
+ }
3431
+ }
3432
+ // lib/engine/process/memory-process-runner.ts
3433
+ var empty = { exitCode: 0, stdout: "", stderr: "" };
3434
+
3435
+ class MemoryFunnelProcessRunner extends FunnelProcessRunner {
3436
+ calls = [];
3437
+ killed = [];
3438
+ handler = () => empty;
3439
+ syncHandler = () => empty;
3440
+ on(handler) {
3441
+ this.handler = handler;
3442
+ return this;
3443
+ }
3444
+ onSync(handler) {
3445
+ this.syncHandler = handler;
3446
+ return this;
3447
+ }
3448
+ async run(command, options = {}) {
3449
+ this.calls.push({ kind: "run", command, options });
3450
+ const result = await this.handler(command);
3451
+ return {
3452
+ exitCode: result.exitCode ?? 0,
3453
+ stdout: result.stdout ?? "",
3454
+ stderr: result.stderr ?? ""
3455
+ };
3456
+ }
3457
+ runSync(command) {
3458
+ this.calls.push({ kind: "runSync", command });
3459
+ const result = this.syncHandler(command);
3460
+ return {
3461
+ exitCode: result.exitCode ?? 0,
3462
+ stdout: result.stdout ?? "",
3463
+ stderr: result.stderr ?? ""
3464
+ };
3465
+ }
3466
+ async attach(command, options = {}) {
3467
+ this.calls.push({ kind: "attach", command, options });
3468
+ const result = await this.handler(command);
3469
+ return result.exitCode ?? 0;
3470
+ }
3471
+ detach(command, options = {}) {
3472
+ this.calls.push({ kind: "detach", command, options });
3473
+ }
3474
+ kill(pid, signal = "SIGTERM") {
3475
+ this.calls.push({ kind: "kill", command: [String(pid), signal] });
3476
+ this.killed.push({ pid, signal });
3477
+ }
3478
+ }
3479
+ // lib/engine/logger/memory-logger.ts
3480
+ class MemoryFunnelLogger extends FunnelLogger {
3481
+ file = null;
3482
+ entries = [];
3483
+ info(message, meta) {
3484
+ this.entries.push({ level: "info", message, meta });
3485
+ }
3486
+ warn(message, meta) {
3487
+ this.entries.push({ level: "warn", message, meta });
3488
+ }
3489
+ error(message, meta) {
3490
+ this.entries.push({ level: "error", message, meta });
3491
+ }
3492
+ clear() {
3493
+ this.entries.length = 0;
3494
+ }
3495
+ }
3496
+ // lib/engine/time/memory-clock.ts
3497
+ class MemoryFunnelClock extends FunnelClock {
3498
+ current;
3499
+ constructor(props = {}) {
3500
+ super();
3501
+ this.current = props.start ?? new Date(0);
3502
+ }
3503
+ now() {
3504
+ return new Date(this.current.getTime());
3505
+ }
3506
+ set(date) {
3507
+ this.current = date;
3508
+ }
3509
+ advance(ms) {
3510
+ this.current = new Date(this.current.getTime() + ms);
3511
+ }
3512
+ }
3513
+ // lib/engine/id/memory-id-generator.ts
3514
+ class MemoryFunnelIdGenerator extends FunnelIdGenerator {
3515
+ counter = 0;
3516
+ prefix;
3517
+ constructor(props = {}) {
3518
+ super();
3519
+ this.prefix = props.prefix ?? "id";
3520
+ }
3521
+ generate() {
3522
+ this.counter++;
3523
+ return `${this.prefix}-${this.counter}`;
3524
+ }
3525
+ }
3526
+ export {
3527
+ slackConnectorSchema,
3528
+ settingsSchema,
3529
+ scheduleEntrySchema,
3530
+ scheduleConnectorSchema,
3531
+ scheduleCatchupPolicySchema,
3532
+ profileConfigSchema,
3533
+ funnelEventSchema,
3534
+ createSettings,
3535
+ connectorConfigSchema,
3536
+ channelDeliveryModeSchema,
3537
+ channelConfigSchema,
3538
+ SETTINGS_VERSION,
3539
+ SETTINGS_PATH,
3540
+ NoopFunnelLogger,
3541
+ NodeFunnelProcessRunner,
3542
+ NodeFunnelLogger,
3543
+ NodeFunnelIdGenerator,
3544
+ NodeFunnelFileSystem,
3545
+ NodeFunnelClock,
3546
+ MockFunnelSettingsReader,
3547
+ MemoryFunnelProcessRunner,
3548
+ MemoryFunnelLogger,
3549
+ MemoryFunnelIdGenerator,
3550
+ MemoryFunnelFileSystem,
3551
+ MemoryFunnelClock,
3552
+ FunnelSettingsStore,
3553
+ FunnelSettingsReader,
3554
+ FunnelProfiles,
3555
+ FunnelProcessRunner,
3556
+ FunnelMcp,
3557
+ FunnelLogger,
3558
+ FunnelListenersClient,
3559
+ FunnelListenerSupervisor,
3560
+ FunnelIdGenerator,
3561
+ FunnelGatewayServer,
3562
+ FunnelGateway,
3563
+ FunnelFileSystem,
3564
+ FunnelEventStore,
3565
+ FunnelConnectorListener,
3566
+ FunnelConnectorFactory,
3567
+ FunnelClock,
3568
+ FunnelClaude,
3569
+ FunnelChannels,
3570
+ FunnelBroadcaster,
3571
+ Funnel,
3572
+ FUNNEL_MCP_NAME,
3573
+ FUNNEL_MCP_COMMAND,
3574
+ FUNNEL_DIR
3575
+ };