@interactive-inc/claude-funnel 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/README.md +155 -133
  2. package/dist/bin.js +1417 -0
  3. package/dist/gateway/daemon.js +513 -0
  4. package/dist/highlights-eq9cgrbb.scm +604 -0
  5. package/dist/highlights-ghv9g403.scm +205 -0
  6. package/dist/highlights-hk7bwhj4.scm +284 -0
  7. package/dist/highlights-r812a2qc.scm +150 -0
  8. package/dist/highlights-x6tmsnaa.scm +115 -0
  9. package/dist/injections-73j83es3.scm +27 -0
  10. package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  11. package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
  12. package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  13. package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  14. package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
  15. package/lib/bin.ts +78 -0
  16. package/lib/{modules → cli}/router/to-request.ts +13 -20
  17. package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +27 -0
  18. package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +40 -0
  19. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +41 -0
  20. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +22 -0
  21. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +23 -0
  22. package/lib/cli/routes/channels.$channel.connectors.$connector.ts +26 -0
  23. package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +92 -0
  24. package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +22 -0
  25. package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +63 -0
  26. package/lib/cli/routes/channels.$channel.connectors.ts +26 -0
  27. package/lib/cli/routes/channels.$channel.rename.$newName.ts +22 -0
  28. package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +34 -0
  29. package/lib/cli/routes/channels.$channel.ts +34 -0
  30. package/lib/cli/routes/channels.add.$channel.ts +33 -0
  31. package/lib/cli/routes/channels.remove.$channel.ts +20 -0
  32. package/lib/cli/routes/channels.ts +39 -0
  33. package/lib/cli/routes/claude.ts +69 -0
  34. package/lib/cli/routes/gateway.listeners.ts +41 -0
  35. package/lib/cli/routes/gateway.logs.ts +123 -0
  36. package/lib/{routes/gateway/restart.ts → cli/routes/gateway.restart.ts} +20 -5
  37. package/lib/cli/routes/gateway.run.ts +41 -0
  38. package/lib/cli/routes/gateway.start.ts +50 -0
  39. package/lib/cli/routes/gateway.status.ts +19 -0
  40. package/lib/cli/routes/gateway.stop.ts +32 -0
  41. package/lib/cli/routes/gateway.ts +55 -0
  42. package/lib/cli/routes/index.ts +202 -0
  43. package/lib/cli/routes/profiles.$profile.as-default.ts +22 -0
  44. package/lib/cli/routes/profiles.$profile.rename.$newName.ts +22 -0
  45. package/lib/cli/routes/profiles.$profile.run.ts +36 -0
  46. package/lib/cli/routes/profiles.add.$profile.ts +46 -0
  47. package/lib/cli/routes/profiles.remove.$profile.ts +20 -0
  48. package/lib/cli/routes/profiles.set.$profile.ts +46 -0
  49. package/lib/cli/routes/profiles.ts +40 -0
  50. package/lib/cli/routes/status.ts +93 -0
  51. package/lib/cli/routes/update.ts +27 -0
  52. package/lib/connectors/connector-config-schema.ts +16 -0
  53. package/lib/connectors/connector-factory.ts +94 -0
  54. package/lib/connectors/connector-listener.ts +20 -0
  55. package/lib/{modules/connectors/funnel-discord-adapter.ts → connectors/discord-adapter.ts} +6 -11
  56. package/lib/{modules/connectors → connectors}/discord-connector-schema.ts +4 -1
  57. package/lib/connectors/discord-listener.ts +111 -0
  58. package/lib/{modules/connectors/funnel-gh-adapter.ts → connectors/gh-adapter.ts} +3 -6
  59. package/lib/{modules/connectors → connectors}/gh-connector-schema.ts +4 -1
  60. package/lib/{modules/connectors/funnel-gh-listener.ts → connectors/gh-listener.ts} +45 -19
  61. package/lib/{modules/connectors → connectors}/match-cron.ts +10 -4
  62. package/lib/connectors/schedule-connector-schema.ts +33 -0
  63. package/lib/connectors/schedule-listener.ts +207 -0
  64. package/lib/connectors/schedule-state-store.ts +54 -0
  65. package/lib/connectors/slack-adapter.ts +36 -0
  66. package/lib/{modules/connectors → connectors}/slack-connector-schema.ts +4 -1
  67. package/lib/{modules/connectors/funnel-slack-event-processor.ts → connectors/slack-event-processor.ts} +15 -9
  68. package/lib/{modules/connectors/funnel-slack-listener.ts → connectors/slack-listener.ts} +33 -14
  69. package/lib/engine/channels/channels.ts +520 -0
  70. package/lib/{modules/claude/funnel-claude.ts → engine/claude/claude.ts} +28 -55
  71. package/lib/engine/claude/gateway-controller.ts +4 -0
  72. package/lib/{modules/fs/funnel-file-system.ts → engine/fs/file-system.ts} +4 -0
  73. package/lib/{modules/fs/memory-funnel-file-system.ts → engine/fs/memory-file-system.ts} +20 -3
  74. package/lib/{modules/fs/node-funnel-file-system.ts → engine/fs/node-file-system.ts} +14 -2
  75. package/lib/{modules/http/memory-funnel-http-client.ts → engine/http/memory-http-client.ts} +1 -5
  76. package/lib/{modules/http/node-funnel-http-client.ts → engine/http/node-http-client.ts} +1 -5
  77. package/lib/{modules/id/memory-funnel-id-generator.ts → engine/id/memory-id-generator.ts} +1 -1
  78. package/lib/{modules/id/node-funnel-id-generator.ts → engine/id/node-id-generator.ts} +1 -1
  79. package/lib/{modules/logger/memory-funnel-logger.ts → engine/logger/memory-logger.ts} +1 -1
  80. package/lib/{modules/logger/node-funnel-logger.ts → engine/logger/node-logger.ts} +1 -1
  81. package/lib/{modules/logger/noop-funnel-logger.ts → engine/logger/noop-logger.ts} +1 -1
  82. package/lib/engine/mcp/channel-server.ts +204 -0
  83. package/lib/{modules/mcp/funnel-mcp.ts → engine/mcp/mcp.ts} +24 -10
  84. package/lib/{modules/process/memory-funnel-process-runner.ts → engine/process/memory-process-runner.ts} +1 -1
  85. package/lib/{modules/process/node-funnel-process-runner.ts → engine/process/node-process-runner.ts} +12 -21
  86. package/lib/engine/profiles/profile-channel-checker.ts +7 -0
  87. package/lib/{modules/profiles/funnel-profiles.ts → engine/profiles/profiles.ts} +41 -43
  88. package/lib/{modules/settings/mock-funnel-settings-reader.ts → engine/settings/mock-settings-reader.ts} +4 -3
  89. package/lib/{modules/settings/funnel-settings-reader.ts → engine/settings/settings-reader.ts} +1 -1
  90. package/lib/engine/settings/settings-schema.ts +46 -0
  91. package/lib/engine/settings/settings-store.ts +110 -0
  92. package/lib/{modules/time/memory-funnel-clock.ts → engine/time/memory-clock.ts} +1 -1
  93. package/lib/{modules/time/node-funnel-clock.ts → engine/time/node-clock.ts} +1 -1
  94. package/lib/funnel.ts +83 -78
  95. package/lib/gateway/auth-middleware.ts +44 -0
  96. package/lib/gateway/broadcaster.ts +319 -0
  97. package/lib/gateway/daemon.ts +47 -0
  98. package/lib/gateway/factory.ts +10 -0
  99. package/lib/gateway/funnel-event-store.ts +155 -0
  100. package/lib/gateway/gateway-server.ts +414 -0
  101. package/lib/gateway/gateway-token.ts +79 -0
  102. package/lib/{modules/gateway/funnel-gateway.ts → gateway/gateway.ts} +27 -13
  103. package/lib/{modules/gateway → gateway}/kill-competing-slack-gateways.ts +4 -4
  104. package/lib/gateway/listener-supervisor.ts +339 -0
  105. package/lib/gateway/listeners-client.ts +128 -0
  106. package/lib/gateway/resolve-daemon-script.ts +26 -0
  107. package/lib/gateway/routes/channels.connectors.call.ts +39 -0
  108. package/lib/gateway/routes/health.ts +13 -0
  109. package/lib/gateway/routes/index.ts +24 -0
  110. package/lib/gateway/routes/listeners.list.ts +6 -0
  111. package/lib/gateway/routes/listeners.restart.ts +15 -0
  112. package/lib/gateway/routes/listeners.start.ts +15 -0
  113. package/lib/gateway/routes/listeners.stop.ts +15 -0
  114. package/lib/gateway/routes/route-deps.ts +11 -0
  115. package/lib/gateway/routes/status.ts +15 -0
  116. package/lib/gateway/routes/validator.ts +17 -0
  117. package/lib/index.ts +50 -92
  118. package/lib/logger/leuco-human-file-writer.ts +65 -0
  119. package/lib/logger/leuco-human-logger.ts +98 -0
  120. package/lib/logger/leuco-human-record.ts +16 -0
  121. package/lib/logger/leuco-human-stdout-writer.ts +26 -0
  122. package/lib/logger/leuco-human-writer.ts +14 -0
  123. package/lib/logger/leuco-logger-memory-sink.ts +67 -0
  124. package/lib/logger/leuco-logger-record.ts +13 -0
  125. package/lib/logger/leuco-logger-sink.ts +33 -0
  126. package/lib/logger/leuco-logger-sqlite-sink.ts +355 -0
  127. package/lib/logger/leuco-logger.ts +135 -0
  128. package/lib/tui/app.tsx +357 -0
  129. package/lib/tui/components/add-row.tsx +18 -0
  130. package/lib/tui/components/brand.tsx +27 -0
  131. package/lib/tui/components/card.tsx +44 -0
  132. package/lib/tui/components/detail-bar.tsx +46 -0
  133. package/lib/tui/components/editable-field.tsx +33 -0
  134. package/lib/tui/components/empty-state.tsx +11 -0
  135. package/lib/tui/components/gateway-status.tsx +66 -0
  136. package/lib/tui/components/keymap.tsx +29 -0
  137. package/lib/tui/components/menu-item.tsx +73 -0
  138. package/lib/tui/components/menu.tsx +26 -0
  139. package/lib/tui/components/panel-header.tsx +22 -0
  140. package/lib/tui/components/readonly-field.tsx +18 -0
  141. package/lib/tui/components/section-header.tsx +25 -0
  142. package/lib/tui/components/selection-accent.tsx +32 -0
  143. package/lib/tui/components/session-item.tsx +33 -0
  144. package/lib/tui/components/session-list.tsx +33 -0
  145. package/lib/tui/components/ui/hascii/accordion-item.tsx +88 -0
  146. package/lib/tui/components/ui/hascii/accordion.tsx +96 -0
  147. package/lib/tui/components/ui/hascii/alert-dialog.tsx +43 -0
  148. package/lib/tui/components/ui/hascii/badge.tsx +51 -0
  149. package/lib/tui/components/ui/hascii/breadcrumb.tsx +58 -0
  150. package/lib/tui/components/ui/hascii/button.tsx +194 -0
  151. package/lib/tui/components/ui/hascii/card-content.tsx +14 -0
  152. package/lib/tui/components/ui/hascii/card-description.tsx +13 -0
  153. package/lib/tui/components/ui/hascii/card-footer.tsx +14 -0
  154. package/lib/tui/components/ui/hascii/card-header.tsx +14 -0
  155. package/lib/tui/components/ui/hascii/card-title.tsx +13 -0
  156. package/lib/tui/components/ui/hascii/card.tsx +27 -0
  157. package/lib/tui/components/ui/hascii/checkbox.tsx +65 -0
  158. package/lib/tui/components/ui/hascii/command.tsx +159 -0
  159. package/lib/tui/components/ui/hascii/dialog-content.tsx +14 -0
  160. package/lib/tui/components/ui/hascii/dialog-description.tsx +13 -0
  161. package/lib/tui/components/ui/hascii/dialog-footer.tsx +14 -0
  162. package/lib/tui/components/ui/hascii/dialog-header.tsx +14 -0
  163. package/lib/tui/components/ui/hascii/dialog-title.tsx +13 -0
  164. package/lib/tui/components/ui/hascii/dialog.tsx +27 -0
  165. package/lib/tui/components/ui/hascii/file-tree.tsx +142 -0
  166. package/lib/tui/components/ui/hascii/focus-group.tsx +62 -0
  167. package/lib/tui/components/ui/hascii/form-item.tsx +43 -0
  168. package/lib/tui/components/ui/hascii/input-otp.tsx +86 -0
  169. package/lib/tui/components/ui/hascii/input.tsx +130 -0
  170. package/lib/tui/components/ui/hascii/pagination.tsx +105 -0
  171. package/lib/tui/components/ui/hascii/progress.tsx +28 -0
  172. package/lib/tui/components/ui/hascii/select.tsx +131 -0
  173. package/lib/tui/components/ui/hascii/separator.tsx +35 -0
  174. package/lib/tui/components/ui/hascii/sidebar-content.tsx +23 -0
  175. package/lib/tui/components/ui/hascii/sidebar-header.tsx +14 -0
  176. package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +67 -0
  177. package/lib/tui/components/ui/hascii/sidebar.tsx +24 -0
  178. package/lib/tui/components/ui/hascii/skeleton.tsx +60 -0
  179. package/lib/tui/components/ui/hascii/slider.tsx +91 -0
  180. package/lib/tui/components/ui/hascii/snackbar.tsx +75 -0
  181. package/lib/tui/components/ui/hascii/sparkline.tsx +53 -0
  182. package/lib/tui/components/ui/hascii/spinner.tsx +47 -0
  183. package/lib/tui/components/ui/hascii/stepper.tsx +54 -0
  184. package/lib/tui/components/ui/hascii/switch.tsx +66 -0
  185. package/lib/tui/components/ui/hascii/table.tsx +95 -0
  186. package/lib/tui/components/ui/hascii/tabs.tsx +59 -0
  187. package/lib/tui/components/ui/hascii/toggle-group-item.tsx +45 -0
  188. package/lib/tui/components/ui/hascii/toggle-group.tsx +99 -0
  189. package/lib/tui/components/ui/hascii/tree.tsx +104 -0
  190. package/lib/tui/components/view-shell.tsx +44 -0
  191. package/lib/tui/filter-input.tsx +33 -0
  192. package/lib/tui/hooks/hascii/use-pressable.ts +54 -0
  193. package/lib/tui/parse-comma-list.ts +14 -0
  194. package/lib/tui/profile-launcher.tsx +61 -0
  195. package/lib/tui/scrollbar-options.ts +19 -0
  196. package/lib/tui/sidebar.tsx +50 -0
  197. package/lib/tui/theme.ts +40 -0
  198. package/lib/tui/tui.tsx +20 -0
  199. package/lib/tui/types.ts +38 -0
  200. package/lib/tui/unique-name.ts +18 -0
  201. package/lib/tui/use-event-stream.ts +133 -0
  202. package/lib/tui/use-snapshot.ts +99 -0
  203. package/lib/tui/utils/hascii/form-item-context.tsx +23 -0
  204. package/lib/tui/utils/hascii/input-focus-context.tsx +31 -0
  205. package/lib/tui/utils/hascii/theme-context.tsx +26 -0
  206. package/lib/tui/utils/hascii/theme.ts +176 -0
  207. package/lib/tui/views/channels-view.tsx +108 -0
  208. package/lib/tui/views/connectors-view.tsx +164 -0
  209. package/lib/tui/views/events-view.tsx +160 -0
  210. package/lib/tui/views/listeners-view.tsx +80 -0
  211. package/lib/tui/views/profiles-view.tsx +152 -0
  212. package/package.json +50 -44
  213. package/lib/api.ts +0 -54
  214. package/lib/modules/channels/channel-connector-ref-updater.ts +0 -4
  215. package/lib/modules/channels/funnel-channels.ts +0 -160
  216. package/lib/modules/connectors/connector-config-schema.ts +0 -16
  217. package/lib/modules/connectors/connector-existence-checker.ts +0 -3
  218. package/lib/modules/connectors/funnel-callable-connector-store.ts +0 -9
  219. package/lib/modules/connectors/funnel-connector-listener.ts +0 -5
  220. package/lib/modules/connectors/funnel-connector-stores.ts +0 -52
  221. package/lib/modules/connectors/funnel-connector-type-store.ts +0 -24
  222. package/lib/modules/connectors/funnel-connectors.ts +0 -151
  223. package/lib/modules/connectors/funnel-discord-listener.ts +0 -71
  224. package/lib/modules/connectors/funnel-discord-store.ts +0 -88
  225. package/lib/modules/connectors/funnel-gh-store.ts +0 -101
  226. package/lib/modules/connectors/funnel-json-connector-store.ts +0 -100
  227. package/lib/modules/connectors/funnel-schedule-listener.ts +0 -130
  228. package/lib/modules/connectors/funnel-schedule-store.ts +0 -195
  229. package/lib/modules/connectors/funnel-slack-adapter.ts +0 -31
  230. package/lib/modules/connectors/funnel-slack-store.ts +0 -90
  231. package/lib/modules/connectors/migrate-legacy-connectors.ts +0 -81
  232. package/lib/modules/connectors/schedule-connector-schema.ts +0 -18
  233. package/lib/modules/connectors/schedule-last-fired-store.ts +0 -48
  234. package/lib/modules/gateway/daemon.ts +0 -74
  235. package/lib/modules/gateway/funnel-broadcaster.ts +0 -37
  236. package/lib/modules/gateway/funnel-event-logger.ts +0 -59
  237. package/lib/modules/gateway/funnel-gateway-server.ts +0 -241
  238. package/lib/modules/mcp/channel-server.ts +0 -76
  239. package/lib/modules/profiles/profile-channel-checker.ts +0 -3
  240. package/lib/modules/profiles/profile-channel-ref-updater.ts +0 -3
  241. package/lib/modules/repos/funnel-repositories.ts +0 -112
  242. package/lib/modules/schedule/funnel-schedule.ts +0 -39
  243. package/lib/modules/settings/funnel-settings-store.ts +0 -56
  244. package/lib/modules/settings/settings-schema.ts +0 -33
  245. package/lib/modules/tui/app.tsx +0 -44
  246. package/lib/modules/tui/tui.tsx +0 -13
  247. package/lib/routes/channels/add.help.ts +0 -3
  248. package/lib/routes/channels/add.ts +0 -21
  249. package/lib/routes/channels/connectors-attach.help.ts +0 -3
  250. package/lib/routes/channels/connectors-attach.ts +0 -17
  251. package/lib/routes/channels/connectors-detach.help.ts +0 -3
  252. package/lib/routes/channels/connectors-detach.ts +0 -17
  253. package/lib/routes/channels/group.help.ts +0 -16
  254. package/lib/routes/channels/group.ts +0 -22
  255. package/lib/routes/channels/remove.help.ts +0 -3
  256. package/lib/routes/channels/remove.ts +0 -17
  257. package/lib/routes/channels/rename.help.ts +0 -5
  258. package/lib/routes/channels/rename.ts +0 -17
  259. package/lib/routes/channels/routes.ts +0 -19
  260. package/lib/routes/channels/show.help.ts +0 -1
  261. package/lib/routes/channels/show.ts +0 -26
  262. package/lib/routes/claude/claude.help.ts +0 -16
  263. package/lib/routes/claude/claude.ts +0 -76
  264. package/lib/routes/claude/routes.ts +0 -4
  265. package/lib/routes/connectors/add.help.ts +0 -28
  266. package/lib/routes/connectors/add.ts +0 -64
  267. package/lib/routes/connectors/group.help.ts +0 -14
  268. package/lib/routes/connectors/group.ts +0 -18
  269. package/lib/routes/connectors/remove.help.ts +0 -3
  270. package/lib/routes/connectors/remove.ts +0 -17
  271. package/lib/routes/connectors/rename.help.ts +0 -5
  272. package/lib/routes/connectors/rename.ts +0 -17
  273. package/lib/routes/connectors/routes.ts +0 -23
  274. package/lib/routes/connectors/schedules-add.help.ts +0 -11
  275. package/lib/routes/connectors/schedules-add.ts +0 -33
  276. package/lib/routes/connectors/schedules-group.help.ts +0 -1
  277. package/lib/routes/connectors/schedules-group.ts +0 -38
  278. package/lib/routes/connectors/schedules-remove.help.ts +0 -3
  279. package/lib/routes/connectors/schedules-remove.ts +0 -17
  280. package/lib/routes/connectors/set.help.ts +0 -8
  281. package/lib/routes/connectors/set.ts +0 -72
  282. package/lib/routes/connectors/show.help.ts +0 -1
  283. package/lib/routes/connectors/show.ts +0 -41
  284. package/lib/routes/gateway/group.help.ts +0 -15
  285. package/lib/routes/gateway/group.ts +0 -28
  286. package/lib/routes/gateway/logs.help.ts +0 -13
  287. package/lib/routes/gateway/logs.ts +0 -102
  288. package/lib/routes/gateway/restart.help.ts +0 -10
  289. package/lib/routes/gateway/routes.ts +0 -18
  290. package/lib/routes/gateway/run.help.ts +0 -12
  291. package/lib/routes/gateway/run.ts +0 -35
  292. package/lib/routes/gateway/start.help.ts +0 -15
  293. package/lib/routes/gateway/start.ts +0 -32
  294. package/lib/routes/gateway/status.help.ts +0 -9
  295. package/lib/routes/gateway/status.ts +0 -28
  296. package/lib/routes/gateway/stop.help.ts +0 -8
  297. package/lib/routes/gateway/stop.ts +0 -21
  298. package/lib/routes/profiles/add.help.ts +0 -3
  299. package/lib/routes/profiles/add.ts +0 -33
  300. package/lib/routes/profiles/group.help.ts +0 -16
  301. package/lib/routes/profiles/group.ts +0 -25
  302. package/lib/routes/profiles/launch.help.ts +0 -4
  303. package/lib/routes/profiles/launch.ts +0 -36
  304. package/lib/routes/profiles/remove.help.ts +0 -3
  305. package/lib/routes/profiles/remove.ts +0 -17
  306. package/lib/routes/profiles/rename.help.ts +0 -5
  307. package/lib/routes/profiles/rename.ts +0 -17
  308. package/lib/routes/profiles/routes.ts +0 -18
  309. package/lib/routes/profiles/set.help.ts +0 -5
  310. package/lib/routes/profiles/set.ts +0 -32
  311. package/lib/routes/repos/add.help.ts +0 -6
  312. package/lib/routes/repos/add.ts +0 -20
  313. package/lib/routes/repos/group.help.ts +0 -11
  314. package/lib/routes/repos/group.ts +0 -18
  315. package/lib/routes/repos/remove.help.ts +0 -3
  316. package/lib/routes/repos/remove.ts +0 -17
  317. package/lib/routes/repos/rename.help.ts +0 -5
  318. package/lib/routes/repos/rename.ts +0 -17
  319. package/lib/routes/repos/routes.ts +0 -17
  320. package/lib/routes/repos/set.help.ts +0 -5
  321. package/lib/routes/repos/set.ts +0 -21
  322. package/lib/routes/repos/show.help.ts +0 -1
  323. package/lib/routes/repos/show.ts +0 -19
  324. package/lib/routes/request/discord-help.ts +0 -9
  325. package/lib/routes/request/discord.help.ts +0 -19
  326. package/lib/routes/request/discord.ts +0 -65
  327. package/lib/routes/request/group.help.ts +0 -15
  328. package/lib/routes/request/group.ts +0 -9
  329. package/lib/routes/request/routes.ts +0 -14
  330. package/lib/routes/request/slack-help.ts +0 -9
  331. package/lib/routes/request/slack.help.ts +0 -19
  332. package/lib/routes/request/slack.ts +0 -61
  333. package/lib/routes/status/routes.ts +0 -4
  334. package/lib/routes/status/status.help.ts +0 -6
  335. package/lib/routes/status/status.ts +0 -77
  336. package/lib/routes/update/routes.ts +0 -4
  337. package/lib/routes/update/update.help.ts +0 -5
  338. package/lib/routes/update/update.ts +0 -21
  339. package/lib/routes.ts +0 -40
  340. /package/lib/{factory.ts → cli/factory.ts} +0 -0
  341. /package/lib/{modules → cli}/router/query-to-cli-args.ts +0 -0
  342. /package/lib/{modules → cli}/router/validator.ts +0 -0
  343. /package/lib/{modules/connectors/funnel-connector-adapter.ts → connectors/connector-adapter.ts} +0 -0
  344. /package/lib/{modules/connectors/funnel-discord-event-processor.ts → connectors/discord-event-processor.ts} +0 -0
  345. /package/lib/{modules/http/funnel-http-client.ts → engine/http/http-client.ts} +0 -0
  346. /package/lib/{modules/id/funnel-id-generator.ts → engine/id/id-generator.ts} +0 -0
  347. /package/lib/{modules/logger/funnel-logger.ts → engine/logger/logger.ts} +0 -0
  348. /package/lib/{modules/process/funnel-process-runner.ts → engine/process/process-runner.ts} +0 -0
  349. /package/lib/{modules/time/funnel-clock.ts → engine/time/clock.ts} +0 -0
@@ -29,6 +29,12 @@ type Props = {
29
29
  now?: () => number
30
30
  }
31
31
 
32
+ const getString = (event: SlackRawEvent, key: string): string | undefined => {
33
+ const value = event[key]
34
+
35
+ return typeof value === "string" ? value : undefined
36
+ }
37
+
32
38
  export class FunnelSlackEventProcessor {
33
39
  private readonly ownBotUserId: string
34
40
  private readonly ownBotId: string
@@ -42,16 +48,16 @@ export class FunnelSlackEventProcessor {
42
48
  }
43
49
 
44
50
  process(event: SlackRawEvent): SlackProcessed {
45
- const eventType = event.type as string | undefined
51
+ const eventType = getString(event, "type")
46
52
 
47
53
  if (!eventType || !ALLOWED_EVENTS.has(eventType)) return { skip: true }
48
54
 
49
- const subtype = event.subtype as string | undefined
55
+ const subtype = getString(event, "subtype")
50
56
 
51
57
  if (!ALLOWED_SUBTYPES.has(subtype)) return { skip: true }
52
58
 
53
- const channelId = (event.channel as string) ?? ""
54
- const eventTs = (event.event_ts as string) ?? (event.ts as string) ?? ""
59
+ const channelId = getString(event, "channel") ?? ""
60
+ const eventTs = getString(event, "event_ts") ?? getString(event, "ts") ?? ""
55
61
  const dedupKey = `${channelId}:${eventTs}`
56
62
  const now = this.now()
57
63
 
@@ -63,15 +69,15 @@ export class FunnelSlackEventProcessor {
63
69
  if ((this.dedup.get(key) ?? 0) < now - DEDUP_WINDOW) this.dedup.delete(key)
64
70
  }
65
71
 
66
- const userId = event.user as string | undefined
67
- const botId = event.bot_id as string | undefined
72
+ const userId = getString(event, "user")
73
+ const botId = getString(event, "bot_id")
68
74
 
69
75
  if (userId === this.ownBotUserId) return { skip: true }
70
76
  if (botId === this.ownBotId) return { skip: true }
71
77
 
72
- const text = (event.text as string) ?? ""
78
+ const text = getString(event, "text") ?? ""
73
79
  const mentioned = text.includes(`<@${this.ownBotUserId}>`)
74
- const threadTs = (event.thread_ts as string) ?? (event.ts as string) ?? ""
80
+ const threadTs = getString(event, "thread_ts") ?? getString(event, "ts") ?? ""
75
81
 
76
82
  return {
77
83
  skip: false,
@@ -85,7 +91,7 @@ export class FunnelSlackEventProcessor {
85
91
  },
86
92
  shouldReact: mentioned,
87
93
  channel: channelId,
88
- timestamp: (event.ts as string) ?? "",
94
+ timestamp: getString(event, "ts") ?? "",
89
95
  }
90
96
  }
91
97
  }
@@ -1,12 +1,14 @@
1
1
  import { App, LogLevel } from "@slack/bolt"
2
- import {
3
- FunnelConnectorListener,
4
- type NotifyFn,
5
- } from "@/modules/connectors/funnel-connector-listener"
6
- import { FunnelSlackEventProcessor } from "@/modules/connectors/funnel-slack-event-processor"
7
- import { FunnelLogger } from "@/modules/logger/funnel-logger"
8
- import { NodeFunnelLogger } from "@/modules/logger/node-funnel-logger"
9
- import type { SlackConnectorConfig } from "@/modules/connectors/slack-connector-schema"
2
+ import { z } from "zod"
3
+ import { FunnelConnectorListener, type NotifyFn } from "@/connectors/connector-listener"
4
+ import { FunnelSlackEventProcessor } from "@/connectors/slack-event-processor"
5
+ import { FunnelLogger } from "@/engine/logger/logger"
6
+ import { NodeFunnelLogger } from "@/engine/logger/node-logger"
7
+ import type { SlackConnectorConfig } from "@/connectors/slack-connector-schema"
8
+
9
+ const middlewareArgsSchema = z.object({
10
+ event: z.record(z.string(), z.unknown()).optional(),
11
+ })
10
12
 
11
13
  type Deps = {
12
14
  config: SlackConnectorConfig
@@ -18,12 +20,12 @@ const defaultLogger = new NodeFunnelLogger()
18
20
  export class FunnelSlackListener extends FunnelConnectorListener {
19
21
  private readonly config: SlackConnectorConfig
20
22
  private readonly logger: FunnelLogger
23
+ private app: App | null = null
21
24
 
22
25
  constructor(deps: Deps) {
23
26
  super()
24
27
  this.config = deps.config
25
28
  this.logger = deps.logger ?? defaultLogger
26
- Object.freeze(this)
27
29
  }
28
30
 
29
31
  async start(notify: NotifyFn): Promise<void> {
@@ -41,13 +43,11 @@ export class FunnelSlackListener extends FunnelConnectorListener {
41
43
  })
42
44
 
43
45
  app.use(async (args) => {
44
- const event = (args as unknown as Record<string, unknown>).event as
45
- | Record<string, unknown>
46
- | undefined
46
+ const parsed = middlewareArgsSchema.safeParse(args)
47
47
 
48
- if (!event) return
48
+ if (!parsed.success || !parsed.data.event) return
49
49
 
50
- const result = processor.process(event)
50
+ const result = processor.process(parsed.data.event)
51
51
 
52
52
  if (result.skip) return
53
53
 
@@ -74,5 +74,24 @@ export class FunnelSlackListener extends FunnelConnectorListener {
74
74
  })
75
75
 
76
76
  await app.start()
77
+ this.app = app
78
+ }
79
+
80
+ async stop(): Promise<void> {
81
+ if (!this.app) return
82
+
83
+ try {
84
+ await this.app.stop()
85
+ } catch (error) {
86
+ this.logger.error("Slack stop error", {
87
+ error: error instanceof Error ? error.message : String(error),
88
+ })
89
+ } finally {
90
+ this.app = null
91
+ }
92
+ }
93
+
94
+ override isAlive(): boolean {
95
+ return this.app !== null
77
96
  }
78
97
  }
@@ -0,0 +1,520 @@
1
+ import type { CallInput } from "@/connectors/connector-adapter"
2
+ import type { ConnectorConfig } from "@/connectors/connector-config-schema"
3
+ import type { FunnelConnectorFactory } from "@/connectors/connector-factory"
4
+ import type { FunnelConnectorListener } from "@/connectors/connector-listener"
5
+ import type { DiscordConnectorConfig } from "@/connectors/discord-connector-schema"
6
+ import type { GhConnectorConfig } from "@/connectors/gh-connector-schema"
7
+ import type { ScheduleConnectorConfig, ScheduleEntry } from "@/connectors/schedule-connector-schema"
8
+ import type { SlackConnectorConfig } from "@/connectors/slack-connector-schema"
9
+ import type { ProfileChannelChecker } from "@/engine/profiles/profile-channel-checker"
10
+ import { FunnelClock } from "@/engine/time/clock"
11
+ import { NodeFunnelClock } from "@/engine/time/node-clock"
12
+ import { FunnelIdGenerator } from "@/engine/id/id-generator"
13
+ import { NodeFunnelIdGenerator } from "@/engine/id/node-id-generator"
14
+ import { FunnelSettingsReader } from "@/engine/settings/settings-reader"
15
+ import type {
16
+ ChannelConfig,
17
+ ChannelDeliveryMode,
18
+ Settings,
19
+ } from "@/engine/settings/settings-schema"
20
+
21
+ type Deps = {
22
+ store: FunnelSettingsReader
23
+ factory: FunnelConnectorFactory
24
+ profileChecker: ProfileChannelChecker
25
+ clock?: FunnelClock
26
+ idGenerator?: FunnelIdGenerator
27
+ }
28
+
29
+ export type ChannelConnectorView = ConnectorConfig & {
30
+ channelId: string
31
+ channelName: string
32
+ }
33
+
34
+ type AddConnectorInput =
35
+ | { type: "slack"; name: string; botToken: string; appToken: string }
36
+ | { type: "gh"; name: string; pollInterval?: number }
37
+ | { type: "discord"; name: string; botToken: string }
38
+ | { type: "schedule"; name: string; entries?: ScheduleEntry[] }
39
+
40
+ const defaultClock = new NodeFunnelClock()
41
+ const defaultIdGenerator = new NodeFunnelIdGenerator()
42
+
43
+ /**
44
+ * Channels own their connectors. Each channel has a stable id (UUID); the
45
+ * `name` is the human-facing label used by the CLI. Connectors live nested
46
+ * inside `channel.connectors[]`, so add/remove/rename are channel-scoped — no
47
+ * global connector namespace exists. Token uniqueness is enforced across all
48
+ * channels at add/update time so the same Slack/Discord credentials cannot
49
+ * be registered twice.
50
+ */
51
+ export class FunnelChannels {
52
+ private readonly store: FunnelSettingsReader
53
+ private readonly factory: FunnelConnectorFactory
54
+ private readonly profileChecker: ProfileChannelChecker
55
+ private readonly clock: FunnelClock
56
+ private readonly idGenerator: FunnelIdGenerator
57
+
58
+ constructor(deps: Deps) {
59
+ this.store = deps.store
60
+ this.factory = deps.factory
61
+ this.profileChecker = deps.profileChecker
62
+ this.clock = deps.clock ?? defaultClock
63
+ this.idGenerator = deps.idGenerator ?? defaultIdGenerator
64
+ Object.freeze(this)
65
+ }
66
+
67
+ list(): ChannelConfig[] {
68
+ return this.store.read().channels
69
+ }
70
+
71
+ get(name: string): ChannelConfig | null {
72
+ return this.list().find((c) => c.name === name) ?? null
73
+ }
74
+
75
+ getById(id: string): ChannelConfig | null {
76
+ return this.list().find((c) => c.id === id) ?? null
77
+ }
78
+
79
+ add(input: { name: string; delivery?: ChannelDeliveryMode }): ChannelConfig {
80
+ const settings = this.store.read()
81
+
82
+ if (settings.channels.some((c) => c.name === input.name)) {
83
+ throw new Error(`channel "${input.name}" already exists`)
84
+ }
85
+
86
+ const channel: ChannelConfig = {
87
+ id: this.idGenerator.generate(),
88
+ name: input.name,
89
+ delivery: input.delivery ?? "fanout",
90
+ connectors: [],
91
+ }
92
+
93
+ settings.channels.push(channel)
94
+ this.store.write(settings)
95
+
96
+ return channel
97
+ }
98
+
99
+ setDelivery(name: string, delivery: ChannelDeliveryMode): void {
100
+ const settings = this.store.read()
101
+ const channel = this.requireChannel(settings, name)
102
+
103
+ channel.delivery = delivery
104
+
105
+ this.store.write(settings)
106
+ }
107
+
108
+ remove(name: string): void {
109
+ const settings = this.store.read()
110
+ const index = settings.channels.findIndex((c) => c.name === name)
111
+
112
+ if (index < 0) throw new Error(`channel "${name}" not found`)
113
+
114
+ const channel = settings.channels[index]
115
+
116
+ if (channel && this.profileChecker.hasChannelRef(channel.id)) {
117
+ throw new Error(`channel "${name}" is referenced by a profile`)
118
+ }
119
+
120
+ settings.channels.splice(index, 1)
121
+ this.store.write(settings)
122
+ }
123
+
124
+ rename(oldName: string, newName: string): void {
125
+ const settings = this.store.read()
126
+ const channel = settings.channels.find((c) => c.name === oldName)
127
+
128
+ if (!channel) throw new Error(`channel "${oldName}" not found`)
129
+ if (settings.channels.some((c) => c.name === newName)) {
130
+ throw new Error(`channel "${newName}" already exists`)
131
+ }
132
+
133
+ channel.name = newName
134
+ this.store.write(settings)
135
+ }
136
+
137
+ listConnectors(channelName: string): ConnectorConfig[] {
138
+ return this.requireChannel(this.store.read(), channelName).connectors
139
+ }
140
+
141
+ getConnector(channelName: string, connectorName: string): ConnectorConfig | null {
142
+ const channel = this.get(channelName)
143
+
144
+ if (!channel) return null
145
+
146
+ return channel.connectors.find((c) => c.name === connectorName) ?? null
147
+ }
148
+
149
+ listAllConnectors(): ChannelConnectorView[] {
150
+ const out: ChannelConnectorView[] = []
151
+
152
+ for (const channel of this.list()) {
153
+ for (const connector of channel.connectors) {
154
+ out.push({ ...connector, channelId: channel.id, channelName: channel.name })
155
+ }
156
+ }
157
+
158
+ return out
159
+ }
160
+
161
+ addConnector(channelName: string, input: AddConnectorInput): ConnectorConfig {
162
+ const settings = this.store.read()
163
+ const channel = this.requireChannel(settings, channelName)
164
+
165
+ if (channel.connectors.some((c) => c.name === input.name)) {
166
+ throw new Error(`connector "${input.name}" already exists in channel "${channelName}"`)
167
+ }
168
+
169
+ const candidate = this.fromInput(input)
170
+
171
+ this.assertNoTokenCollision(settings, candidate)
172
+
173
+ channel.connectors.push(candidate)
174
+ this.store.write(settings)
175
+
176
+ return candidate
177
+ }
178
+
179
+ private fromInput(input: AddConnectorInput): ConnectorConfig {
180
+ const id = this.idGenerator.generate()
181
+ const now = this.clock.iso()
182
+ const createdAt = now
183
+ const updatedAt = now
184
+
185
+ if (input.type === "slack") {
186
+ return {
187
+ id,
188
+ type: "slack",
189
+ name: input.name,
190
+ botToken: input.botToken,
191
+ appToken: input.appToken,
192
+ createdAt,
193
+ updatedAt,
194
+ }
195
+ }
196
+
197
+ if (input.type === "gh") {
198
+ return {
199
+ id,
200
+ type: "gh",
201
+ name: input.name,
202
+ ...(input.pollInterval !== undefined ? { pollInterval: input.pollInterval } : {}),
203
+ createdAt,
204
+ updatedAt,
205
+ }
206
+ }
207
+
208
+ if (input.type === "discord") {
209
+ return {
210
+ id,
211
+ type: "discord",
212
+ name: input.name,
213
+ botToken: input.botToken,
214
+ createdAt,
215
+ updatedAt,
216
+ }
217
+ }
218
+
219
+ return {
220
+ id,
221
+ type: "schedule",
222
+ name: input.name,
223
+ entries: input.entries ?? [],
224
+ createdAt,
225
+ updatedAt,
226
+ }
227
+ }
228
+
229
+ removeConnector(channelName: string, connectorName: string): void {
230
+ const settings = this.store.read()
231
+ const channel = this.requireChannel(settings, channelName)
232
+ const index = channel.connectors.findIndex((c) => c.name === connectorName)
233
+
234
+ if (index < 0) {
235
+ throw new Error(`connector "${connectorName}" not found in channel "${channelName}"`)
236
+ }
237
+
238
+ channel.connectors.splice(index, 1)
239
+ this.store.write(settings)
240
+ }
241
+
242
+ renameConnector(channelName: string, oldName: string, newName: string): void {
243
+ const settings = this.store.read()
244
+ const channel = this.requireChannel(settings, channelName)
245
+ const connector = channel.connectors.find((c) => c.name === oldName)
246
+
247
+ if (!connector) {
248
+ throw new Error(`connector "${oldName}" not found in channel "${channelName}"`)
249
+ }
250
+
251
+ if (channel.connectors.some((c) => c.name === newName)) {
252
+ throw new Error(`connector "${newName}" already exists in channel "${channelName}"`)
253
+ }
254
+
255
+ connector.name = newName
256
+ connector.updatedAt = this.clock.iso()
257
+ this.store.write(settings)
258
+ }
259
+
260
+ updateSlackConnector(
261
+ channelName: string,
262
+ connectorName: string,
263
+ fields: { botToken?: string; appToken?: string },
264
+ ): void {
265
+ const settings = this.store.read()
266
+ const channel = this.requireChannel(settings, channelName)
267
+ const connector = this.requireSlackConnector(channel, connectorName)
268
+
269
+ const updated: SlackConnectorConfig = {
270
+ ...connector,
271
+ botToken: fields.botToken ?? connector.botToken,
272
+ appToken: fields.appToken ?? connector.appToken,
273
+ updatedAt: this.clock.iso(),
274
+ }
275
+
276
+ this.assertNoTokenCollision(settings, updated)
277
+
278
+ Object.assign(connector, updated)
279
+ this.store.write(settings)
280
+ }
281
+
282
+ updateGhConnector(
283
+ channelName: string,
284
+ connectorName: string,
285
+ fields: { pollInterval?: number },
286
+ ): void {
287
+ const settings = this.store.read()
288
+ const channel = this.requireChannel(settings, channelName)
289
+ const connector = this.requireGhConnector(channel, connectorName)
290
+
291
+ if (fields.pollInterval !== undefined) connector.pollInterval = fields.pollInterval
292
+ connector.updatedAt = this.clock.iso()
293
+
294
+ this.store.write(settings)
295
+ }
296
+
297
+ updateDiscordConnector(
298
+ channelName: string,
299
+ connectorName: string,
300
+ fields: { botToken?: string },
301
+ ): void {
302
+ const settings = this.store.read()
303
+ const channel = this.requireChannel(settings, channelName)
304
+ const connector = this.requireDiscordConnector(channel, connectorName)
305
+
306
+ const updated: DiscordConnectorConfig = {
307
+ ...connector,
308
+ botToken: fields.botToken ?? connector.botToken,
309
+ updatedAt: this.clock.iso(),
310
+ }
311
+
312
+ this.assertNoTokenCollision(settings, updated)
313
+
314
+ Object.assign(connector, updated)
315
+ this.store.write(settings)
316
+ }
317
+
318
+ listScheduleEntries(channelName: string, connectorName: string): ScheduleEntry[] {
319
+ const channel = this.requireChannel(this.store.read(), channelName)
320
+ const connector = this.requireScheduleConnector(channel, connectorName)
321
+
322
+ return connector.entries
323
+ }
324
+
325
+ addScheduleEntry(
326
+ channelName: string,
327
+ connectorName: string,
328
+ entry: Pick<ScheduleEntry, "cron" | "prompt"> &
329
+ Partial<Pick<ScheduleEntry, "id" | "enabled" | "catchupPolicy">>,
330
+ ): ScheduleEntry {
331
+ const settings = this.store.read()
332
+ const channel = this.requireChannel(settings, channelName)
333
+ const connector = this.requireScheduleConnector(channel, connectorName)
334
+
335
+ const persisted: ScheduleEntry = {
336
+ id: entry.id ?? this.idGenerator.generate(),
337
+ cron: entry.cron,
338
+ prompt: entry.prompt,
339
+ enabled: entry.enabled ?? true,
340
+ catchupPolicy: entry.catchupPolicy ?? "latest",
341
+ }
342
+
343
+ connector.entries.push(persisted)
344
+ connector.updatedAt = this.clock.iso()
345
+ this.store.write(settings)
346
+
347
+ return persisted
348
+ }
349
+
350
+ removeScheduleEntry(channelName: string, connectorName: string, id: string): void {
351
+ const settings = this.store.read()
352
+ const channel = this.requireChannel(settings, channelName)
353
+ const connector = this.requireScheduleConnector(channel, connectorName)
354
+ const index = connector.entries.findIndex((e) => e.id === id)
355
+
356
+ if (index < 0) throw new Error(`schedule entry "${id}" not found`)
357
+
358
+ connector.entries.splice(index, 1)
359
+ connector.updatedAt = this.clock.iso()
360
+ this.store.write(settings)
361
+ }
362
+
363
+ async call(channelName: string, connectorName: string, input: CallInput): Promise<unknown> {
364
+ const connector = this.getConnector(channelName, connectorName)
365
+
366
+ if (!connector) {
367
+ throw new Error(`connector "${connectorName}" not found in channel "${channelName}"`)
368
+ }
369
+
370
+ const adapter = this.factory.createAdapter(connector)
371
+
372
+ if (!adapter) {
373
+ throw new Error(`connector type "${connector.type}" does not support outbound calls`)
374
+ }
375
+
376
+ return await adapter.call(input)
377
+ }
378
+
379
+ createListener(
380
+ channelName: string,
381
+ connectorName: string,
382
+ ): { config: ConnectorConfig; channelId: string; listener: FunnelConnectorListener } | null {
383
+ const channel = this.get(channelName)
384
+
385
+ if (!channel) return null
386
+
387
+ const connector = channel.connectors.find((c) => c.name === connectorName)
388
+
389
+ if (!connector) return null
390
+
391
+ return {
392
+ config: connector,
393
+ channelId: channel.id,
394
+ listener: this.factory.createListener(channel.id, connector),
395
+ }
396
+ }
397
+
398
+ createAllListeners(): {
399
+ config: ConnectorConfig
400
+ channelId: string
401
+ channelName: string
402
+ listener: FunnelConnectorListener
403
+ }[] {
404
+ const out: {
405
+ config: ConnectorConfig
406
+ channelId: string
407
+ channelName: string
408
+ listener: FunnelConnectorListener
409
+ }[] = []
410
+
411
+ for (const channel of this.list()) {
412
+ for (const connector of channel.connectors) {
413
+ out.push({
414
+ config: connector,
415
+ channelId: channel.id,
416
+ channelName: channel.name,
417
+ listener: this.factory.createListener(channel.id, connector),
418
+ })
419
+ }
420
+ }
421
+
422
+ return out
423
+ }
424
+
425
+ private requireChannel(settings: Settings, name: string): ChannelConfig {
426
+ const channel = settings.channels.find((c) => c.name === name)
427
+
428
+ if (!channel) throw new Error(`channel "${name}" not found`)
429
+
430
+ return channel
431
+ }
432
+
433
+ private requireConnector(channel: ChannelConfig, connectorName: string): ConnectorConfig {
434
+ const connector = channel.connectors.find((c) => c.name === connectorName)
435
+
436
+ if (!connector) {
437
+ throw new Error(`connector "${connectorName}" not found in channel "${channel.name}"`)
438
+ }
439
+
440
+ return connector
441
+ }
442
+
443
+ private requireSlackConnector(
444
+ channel: ChannelConfig,
445
+ connectorName: string,
446
+ ): SlackConnectorConfig {
447
+ const connector = this.requireConnector(channel, connectorName)
448
+
449
+ if (connector.type !== "slack") {
450
+ throw new Error(`connector "${connectorName}" is type "${connector.type}", not "slack"`)
451
+ }
452
+
453
+ return connector
454
+ }
455
+
456
+ private requireGhConnector(channel: ChannelConfig, connectorName: string): GhConnectorConfig {
457
+ const connector = this.requireConnector(channel, connectorName)
458
+
459
+ if (connector.type !== "gh") {
460
+ throw new Error(`connector "${connectorName}" is type "${connector.type}", not "gh"`)
461
+ }
462
+
463
+ return connector
464
+ }
465
+
466
+ private requireDiscordConnector(
467
+ channel: ChannelConfig,
468
+ connectorName: string,
469
+ ): DiscordConnectorConfig {
470
+ const connector = this.requireConnector(channel, connectorName)
471
+
472
+ if (connector.type !== "discord") {
473
+ throw new Error(`connector "${connectorName}" is type "${connector.type}", not "discord"`)
474
+ }
475
+
476
+ return connector
477
+ }
478
+
479
+ private requireScheduleConnector(
480
+ channel: ChannelConfig,
481
+ connectorName: string,
482
+ ): ScheduleConnectorConfig {
483
+ const connector = this.requireConnector(channel, connectorName)
484
+
485
+ if (connector.type !== "schedule") {
486
+ throw new Error(`connector "${connectorName}" is type "${connector.type}", not "schedule"`)
487
+ }
488
+
489
+ return connector
490
+ }
491
+
492
+ private assertNoTokenCollision(settings: Settings, candidate: ConnectorConfig): void {
493
+ const tokens = this.tokensOf(candidate)
494
+
495
+ if (tokens.length === 0) return
496
+
497
+ for (const channel of settings.channels) {
498
+ for (const other of channel.connectors) {
499
+ if (other.id === candidate.id) continue
500
+
501
+ for (const token of this.tokensOf(other)) {
502
+ if (tokens.includes(token)) {
503
+ throw new Error(
504
+ `token already in use by connector "${other.name}" in channel "${channel.name}"`,
505
+ )
506
+ }
507
+ }
508
+ }
509
+ }
510
+ }
511
+
512
+ private tokensOf(connector: ConnectorConfig): string[] {
513
+ if (connector.type === "slack") return [connector.botToken, connector.appToken]
514
+ if (connector.type === "discord") return [connector.botToken]
515
+
516
+ return []
517
+ }
518
+ }
519
+
520
+ export type { ScheduleConnectorConfig }