@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
@@ -0,0 +1,20 @@
1
+ export type NotifyFn = (content: string, meta?: Record<string, string>) => Promise<void>
2
+
3
+ /**
4
+ * Long-lived event source for one connector.
5
+ *
6
+ * `start()` opens the underlying connection (Slack Socket Mode, Discord
7
+ * Gateway, GH polling, schedule tick) and pushes events through `notify`.
8
+ * `stop()` releases the resources so the supervisor can recreate the listener
9
+ * with new config without restarting the whole gateway. `isAlive()` lets the
10
+ * supervisor periodically health-check and auto-restart dead listeners; the
11
+ * default optimistic implementation is fine for poll/tick-based listeners
12
+ * that self-heal.
13
+ */
14
+ export abstract class FunnelConnectorListener {
15
+ abstract start(notify: NotifyFn): Promise<void>
16
+ abstract stop(): Promise<void>
17
+ isAlive(): boolean {
18
+ return true
19
+ }
20
+ }
@@ -1,10 +1,7 @@
1
- import {
2
- FunnelConnectorAdapter,
3
- type CallInput,
4
- } from "@/modules/connectors/funnel-connector-adapter"
5
- import { FunnelHttpClient } from "@/modules/http/funnel-http-client"
6
- import { NodeFunnelHttpClient } from "@/modules/http/node-funnel-http-client"
7
- import type { DiscordConnectorConfig } from "@/modules/connectors/discord-connector-schema"
1
+ import { FunnelConnectorAdapter, type CallInput } from "@/connectors/connector-adapter"
2
+ import { FunnelHttpClient } from "@/engine/http/http-client"
3
+ import { NodeFunnelHttpClient } from "@/engine/http/node-http-client"
4
+ import type { DiscordConnectorConfig } from "@/connectors/discord-connector-schema"
8
5
 
9
6
  const DISCORD_API_BASE = "https://discord.com/api/v10"
10
7
 
@@ -29,11 +26,9 @@ export class FunnelDiscordAdapter extends FunnelConnectorAdapter {
29
26
  async call(input: CallInput): Promise<unknown> {
30
27
  const method = (input.method || "GET").toUpperCase()
31
28
  const path = input.path.startsWith("/") ? input.path : `/${input.path}`
29
+ const body = input.body
32
30
  const hasBody =
33
- input.body &&
34
- typeof input.body === "object" &&
35
- method !== "GET" &&
36
- Object.keys(input.body as object).length > 0
31
+ body !== null && typeof body === "object" && method !== "GET" && Object.keys(body).length > 0
37
32
 
38
33
  const res = await this.http.fetch({
39
34
  method,
@@ -1,9 +1,12 @@
1
1
  import { z } from "zod"
2
2
 
3
3
  export const discordConnectorSchema = z.object({
4
- type: z.literal("discord"),
4
+ id: z.string(),
5
5
  name: z.string(),
6
+ type: z.literal("discord"),
6
7
  botToken: z.string().min(10),
8
+ createdAt: z.string().datetime().optional(),
9
+ updatedAt: z.string().datetime().optional(),
7
10
  })
8
11
 
9
12
  export type DiscordConnectorConfig = z.infer<typeof discordConnectorSchema>
@@ -0,0 +1,111 @@
1
+ import { Client, GatewayIntentBits, Partials } from "discord.js"
2
+ import { FunnelConnectorListener, type NotifyFn } from "@/connectors/connector-listener"
3
+ import { FunnelDiscordEventProcessor } from "@/connectors/discord-event-processor"
4
+ import { FunnelLogger } from "@/engine/logger/logger"
5
+ import { NodeFunnelLogger } from "@/engine/logger/node-logger"
6
+ import type { DiscordConnectorConfig } from "@/connectors/discord-connector-schema"
7
+
8
+ type Deps = {
9
+ config: DiscordConnectorConfig
10
+ logger?: FunnelLogger
11
+ }
12
+
13
+ const defaultLogger = new NodeFunnelLogger()
14
+
15
+ export class FunnelDiscordListener extends FunnelConnectorListener {
16
+ private readonly config: DiscordConnectorConfig
17
+ private readonly logger: FunnelLogger
18
+ private client: Client | null = null
19
+
20
+ constructor(deps: Deps) {
21
+ super()
22
+ this.config = deps.config
23
+ this.logger = deps.logger ?? defaultLogger
24
+ }
25
+
26
+ async start(notify: NotifyFn): Promise<void> {
27
+ const client = new Client({
28
+ intents: [
29
+ GatewayIntentBits.Guilds,
30
+ GatewayIntentBits.GuildMessages,
31
+ GatewayIntentBits.MessageContent,
32
+ GatewayIntentBits.DirectMessages,
33
+ ],
34
+ partials: [Partials.Channel],
35
+ })
36
+
37
+ client.on("messageCreate", async (message) => {
38
+ const ownUserId = client.user?.id ?? ""
39
+ const mentionedUserIds = [...message.mentions.users.keys()]
40
+
41
+ this.logger.info("discord messageCreate", {
42
+ author: message.author.id,
43
+ authorIsBot: String(message.author.bot),
44
+ channelId: message.channelId,
45
+ guildId: message.guildId ?? "",
46
+ mentions: mentionedUserIds.join(","),
47
+ ownUserId,
48
+ mentioned: String(mentionedUserIds.includes(ownUserId)),
49
+ })
50
+
51
+ const processor = new FunnelDiscordEventProcessor({ ownUserId })
52
+
53
+ const result = processor.process({
54
+ authorId: message.author.id,
55
+ authorIsBot: message.author.bot,
56
+ channelId: message.channelId,
57
+ guildId: message.guildId,
58
+ mentionedUserIds,
59
+ raw: message.toJSON(),
60
+ })
61
+
62
+ if (result.skip) {
63
+ this.logger.info("discord skip", { reason: "bot author" })
64
+ return
65
+ }
66
+
67
+ try {
68
+ await notify(result.content, result.meta)
69
+ } catch (error) {
70
+ this.logger.error("discord notify error", {
71
+ error: error instanceof Error ? error.message : String(error),
72
+ })
73
+ }
74
+ })
75
+
76
+ client.on("ready", (readyClient) => {
77
+ this.logger.info("discord ready", {
78
+ userId: readyClient.user.id,
79
+ tag: readyClient.user.tag,
80
+ guilds: String(readyClient.guilds.cache.size),
81
+ })
82
+ })
83
+
84
+ client.on("error", (error) => {
85
+ this.logger.error("discord client error", {
86
+ error: error instanceof Error ? error.message : String(error),
87
+ })
88
+ })
89
+
90
+ await client.login(this.config.botToken)
91
+ this.client = client
92
+ }
93
+
94
+ async stop(): Promise<void> {
95
+ if (!this.client) return
96
+
97
+ try {
98
+ await this.client.destroy()
99
+ } catch (error) {
100
+ this.logger.error("discord stop error", {
101
+ error: error instanceof Error ? error.message : String(error),
102
+ })
103
+ } finally {
104
+ this.client = null
105
+ }
106
+ }
107
+
108
+ override isAlive(): boolean {
109
+ return this.client !== null
110
+ }
111
+ }
@@ -1,9 +1,6 @@
1
- import {
2
- FunnelConnectorAdapter,
3
- type CallInput,
4
- } from "@/modules/connectors/funnel-connector-adapter"
5
- import { FunnelProcessRunner } from "@/modules/process/funnel-process-runner"
6
- import { NodeFunnelProcessRunner } from "@/modules/process/node-funnel-process-runner"
1
+ import { FunnelConnectorAdapter, type CallInput } from "@/connectors/connector-adapter"
2
+ import { FunnelProcessRunner } from "@/engine/process/process-runner"
3
+ import { NodeFunnelProcessRunner } from "@/engine/process/node-process-runner"
7
4
 
8
5
  type Deps = {
9
6
  process?: FunnelProcessRunner
@@ -1,9 +1,12 @@
1
1
  import { z } from "zod"
2
2
 
3
3
  export const ghConnectorSchema = z.object({
4
- type: z.literal("gh"),
4
+ id: z.string(),
5
5
  name: z.string(),
6
+ type: z.literal("gh"),
6
7
  pollInterval: z.number().int().positive().optional(),
8
+ createdAt: z.string().datetime().optional(),
9
+ updatedAt: z.string().datetime().optional(),
7
10
  })
8
11
 
9
12
  export type GhConnectorConfig = z.infer<typeof ghConnectorSchema>
@@ -1,20 +1,26 @@
1
- import {
2
- FunnelConnectorListener,
3
- type NotifyFn,
4
- } from "@/modules/connectors/funnel-connector-listener"
5
- import { FunnelLogger } from "@/modules/logger/funnel-logger"
6
- import { NodeFunnelLogger } from "@/modules/logger/node-funnel-logger"
7
- import { FunnelProcessRunner } from "@/modules/process/funnel-process-runner"
8
- import { NodeFunnelProcessRunner } from "@/modules/process/node-funnel-process-runner"
9
- import type { GhConnectorConfig } from "@/modules/connectors/gh-connector-schema"
10
-
11
- type GhNotification = {
12
- id: string
13
- reason: string
14
- subject: { type: string; url: string; title: string }
15
- repository: { full_name: string }
16
- updated_at: string
17
- }
1
+ import { z } from "zod"
2
+ import { FunnelConnectorListener, type NotifyFn } from "@/connectors/connector-listener"
3
+ import { FunnelLogger } from "@/engine/logger/logger"
4
+ import { NodeFunnelLogger } from "@/engine/logger/node-logger"
5
+ import { FunnelProcessRunner } from "@/engine/process/process-runner"
6
+ import { NodeFunnelProcessRunner } from "@/engine/process/node-process-runner"
7
+ import type { GhConnectorConfig } from "@/connectors/gh-connector-schema"
8
+
9
+ const ghNotificationSchema = z.object({
10
+ id: z.string(),
11
+ reason: z.string(),
12
+ subject: z.object({
13
+ type: z.string(),
14
+ url: z.string(),
15
+ title: z.string(),
16
+ }),
17
+ repository: z.object({ full_name: z.string() }),
18
+ updated_at: z.string(),
19
+ })
20
+
21
+ const ghNotificationsSchema = z.array(ghNotificationSchema)
22
+
23
+ type GhNotification = z.infer<typeof ghNotificationSchema>
18
24
 
19
25
  type Deps = {
20
26
  config: GhConnectorConfig
@@ -37,6 +43,7 @@ export class FunnelGhListener extends FunnelConnectorListener {
37
43
  private readonly seen = new Map<string, string>()
38
44
  private bootstrapped = false
39
45
  private since: string
46
+ private timer: ReturnType<typeof setInterval> | null = null
40
47
 
41
48
  constructor(deps: Deps) {
42
49
  super()
@@ -52,7 +59,19 @@ export class FunnelGhListener extends FunnelConnectorListener {
52
59
 
53
60
  const interval = this.config.pollInterval ?? 60
54
61
 
55
- setInterval(() => void this.pollOnce(notify), interval * 1000).unref()
62
+ this.timer = setInterval(() => void this.pollOnce(notify), interval * 1000)
63
+ this.timer.unref()
64
+ }
65
+
66
+ async stop(): Promise<void> {
67
+ if (!this.timer) return
68
+
69
+ clearInterval(this.timer)
70
+ this.timer = null
71
+ }
72
+
73
+ override isAlive(): boolean {
74
+ return this.timer !== null
56
75
  }
57
76
 
58
77
  async pollOnce(notify: NotifyFn): Promise<void> {
@@ -67,7 +86,14 @@ export class FunnelGhListener extends FunnelConnectorListener {
67
86
  return
68
87
  }
69
88
 
70
- const items = JSON.parse(result.stdout) as GhNotification[]
89
+ const parsed = ghNotificationsSchema.safeParse(JSON.parse(result.stdout))
90
+
91
+ if (!parsed.success) {
92
+ this.logger.warn("gh response did not match schema", { error: parsed.error.message })
93
+ return
94
+ }
95
+
96
+ const items: GhNotification[] = parsed.data
71
97
 
72
98
  for (const item of items) {
73
99
  if (this.seen.get(item.id) === item.updated_at) continue
@@ -18,14 +18,16 @@ const parseField = (expr: string, min: number, max: number): Field => {
18
18
  lo = min
19
19
  hi = max
20
20
  } else if (rangePart.includes("-")) {
21
- const [a, b] = rangePart.split("-").map(Number)
21
+ const [aStr, bStr] = rangePart.split("-")
22
+ const a = Number(aStr)
23
+ const b = Number(bStr)
22
24
 
23
25
  if (!Number.isFinite(a) || !Number.isFinite(b)) {
24
26
  throw new Error(`invalid cron range: "${rangePart}"`)
25
27
  }
26
28
 
27
- lo = a as number
28
- hi = b as number
29
+ lo = a
30
+ hi = b
29
31
  } else {
30
32
  const n = Number(rangePart)
31
33
 
@@ -54,7 +56,11 @@ export const matchCron = (expr: string, date: Date): boolean => {
54
56
  throw new Error(`cron must have 5 fields (got ${parts.length}): "${expr}"`)
55
57
  }
56
58
 
57
- const [minute, hour, dom, month, dow] = parts as [string, string, string, string, string]
59
+ const [minute, hour, dom, month, dow] = parts
60
+
61
+ if (!minute || !hour || !dom || !month || !dow) {
62
+ throw new Error(`cron has empty fields: "${expr}"`)
63
+ }
58
64
 
59
65
  const fields = [
60
66
  { field: parseField(minute, 0, 59), value: date.getMinutes() },
@@ -0,0 +1,33 @@
1
+ import { z } from "zod"
2
+
3
+ /**
4
+ * Catch-up behavior when the daemon was down past one or more matching minutes.
5
+ *
6
+ * - `latest`: fire once with the most recent missed match (default; preserves prior behavior).
7
+ * - `all`: fire once per missed minute, oldest first (capped at 24 h).
8
+ * - `skip`: never fire missed matches; only fire when the current minute matches.
9
+ */
10
+ export const scheduleCatchupPolicySchema = z.enum(["latest", "all", "skip"])
11
+
12
+ export type ScheduleCatchupPolicy = z.infer<typeof scheduleCatchupPolicySchema>
13
+
14
+ export const scheduleEntrySchema = z.object({
15
+ id: z.string(),
16
+ cron: z.string(),
17
+ prompt: z.string(),
18
+ enabled: z.boolean().default(true),
19
+ catchupPolicy: scheduleCatchupPolicySchema.default("latest"),
20
+ })
21
+
22
+ export type ScheduleEntry = z.infer<typeof scheduleEntrySchema>
23
+
24
+ export const scheduleConnectorSchema = z.object({
25
+ id: z.string(),
26
+ name: z.string(),
27
+ type: z.literal("schedule"),
28
+ entries: z.array(scheduleEntrySchema).default([]),
29
+ createdAt: z.string().datetime().optional(),
30
+ updatedAt: z.string().datetime().optional(),
31
+ })
32
+
33
+ export type ScheduleConnectorConfig = z.infer<typeof scheduleConnectorSchema>
@@ -0,0 +1,207 @@
1
+ import { FunnelConnectorListener, type NotifyFn } from "@/connectors/connector-listener"
2
+ import { matchCron } from "@/connectors/match-cron"
3
+ import { ScheduleStateStore } from "@/connectors/schedule-state-store"
4
+ import { FunnelLogger } from "@/engine/logger/logger"
5
+ import { NodeFunnelLogger } from "@/engine/logger/node-logger"
6
+ import type { ScheduleConnectorConfig, ScheduleEntry } from "@/connectors/schedule-connector-schema"
7
+
8
+ type Deps = {
9
+ config: ScheduleConnectorConfig
10
+ lastFiredStore: ScheduleStateStore
11
+ logger?: FunnelLogger
12
+ now?: () => Date
13
+ }
14
+
15
+ const defaultLogger = new NodeFunnelLogger()
16
+
17
+ const MAX_CATCHUP_MINUTES = 60 * 24
18
+
19
+ export class FunnelScheduleListener extends FunnelConnectorListener {
20
+ private readonly config: ScheduleConnectorConfig
21
+ private readonly lastFiredStore: ScheduleStateStore
22
+ private readonly logger: FunnelLogger
23
+ private readonly now: () => Date
24
+ private timer: ReturnType<typeof setTimeout> | null = null
25
+ private stopped = false
26
+
27
+ constructor(deps: Deps) {
28
+ super()
29
+ this.config = deps.config
30
+ this.lastFiredStore = deps.lastFiredStore
31
+ this.logger = deps.logger ?? defaultLogger
32
+ this.now = deps.now ?? (() => new Date())
33
+ }
34
+
35
+ async start(notify: NotifyFn): Promise<void> {
36
+ this.stopped = false
37
+
38
+ const scheduleNext = () => {
39
+ if (this.stopped) return
40
+
41
+ const date = this.now()
42
+ const msUntilNextMinute = 60_000 - (date.getSeconds() * 1000 + date.getMilliseconds())
43
+ this.timer = setTimeout(async () => {
44
+ if (this.stopped) return
45
+ await this.tick(notify)
46
+ scheduleNext()
47
+ }, msUntilNextMinute)
48
+
49
+ this.timer.unref()
50
+ }
51
+
52
+ await this.tick(notify)
53
+ scheduleNext()
54
+ }
55
+
56
+ async stop(): Promise<void> {
57
+ this.stopped = true
58
+
59
+ if (this.timer) {
60
+ clearTimeout(this.timer)
61
+ this.timer = null
62
+ }
63
+ }
64
+
65
+ override isAlive(): boolean {
66
+ return !this.stopped && this.timer !== null
67
+ }
68
+
69
+ async tick(notify: NotifyFn): Promise<void> {
70
+ const now = this.truncateToMinute(this.now())
71
+ const state = this.lastFiredStore.load()
72
+ let changed = false
73
+
74
+ for (const entry of this.config.entries) {
75
+ if (!entry.enabled) continue
76
+
77
+ const fired = await this.fireEntry(entry, now, state, notify)
78
+
79
+ if (fired) changed = true
80
+ }
81
+
82
+ if (changed) this.lastFiredStore.save(state)
83
+ }
84
+
85
+ private async fireEntry(
86
+ entry: ScheduleEntry,
87
+ now: Date,
88
+ state: Map<string, Date>,
89
+ notify: NotifyFn,
90
+ ): Promise<boolean> {
91
+ const lastFired = state.get(entry.id)
92
+ const searchFrom = lastFired ? new Date(lastFired.getTime() + 60_000) : now
93
+
94
+ if (searchFrom.getTime() > now.getTime()) return false
95
+
96
+ if (entry.catchupPolicy === "skip") {
97
+ try {
98
+ if (!matchCron(entry.cron, now)) return false
99
+ } catch (error) {
100
+ this.logInvalidCron(entry, error)
101
+ return false
102
+ }
103
+
104
+ await this.notifyOne(entry, now, notify, false)
105
+ state.set(entry.id, now)
106
+ return true
107
+ }
108
+
109
+ if (entry.catchupPolicy === "all") {
110
+ const matches = this.findAllMatches(entry.cron, searchFrom, now, entry.id)
111
+
112
+ if (matches.length === 0) return false
113
+
114
+ for (const match of matches) {
115
+ await this.notifyOne(entry, match, notify, match.getTime() !== now.getTime())
116
+ }
117
+
118
+ state.set(entry.id, matches[matches.length - 1] ?? now)
119
+ return true
120
+ }
121
+
122
+ const match = this.findMostRecentMatch(entry.cron, searchFrom, now, entry.id)
123
+
124
+ if (!match) return false
125
+
126
+ await this.notifyOne(entry, match, notify, match.getTime() !== now.getTime())
127
+ state.set(entry.id, match)
128
+ return true
129
+ }
130
+
131
+ private async notifyOne(
132
+ entry: ScheduleEntry,
133
+ firedAt: Date,
134
+ notify: NotifyFn,
135
+ catchup: boolean,
136
+ ): Promise<void> {
137
+ const meta: Record<string, string> = {
138
+ event_type: "schedule",
139
+ schedule_id: entry.id,
140
+ cron: entry.cron,
141
+ fired_at: firedAt.toISOString(),
142
+ catchup_policy: entry.catchupPolicy,
143
+ }
144
+
145
+ if (catchup) meta.catchup = "true"
146
+
147
+ await notify(entry.prompt, meta)
148
+ }
149
+
150
+ private findMostRecentMatch(cron: string, from: Date, until: Date, entryId: string): Date | null {
151
+ const maxIterations = Math.min(
152
+ MAX_CATCHUP_MINUTES,
153
+ Math.floor((until.getTime() - from.getTime()) / 60_000) + 1,
154
+ )
155
+
156
+ for (let i = 0; i < maxIterations; i++) {
157
+ const candidate = new Date(until.getTime() - i * 60_000)
158
+
159
+ try {
160
+ if (matchCron(cron, candidate)) return candidate
161
+ } catch (error) {
162
+ this.logInvalidCron({ id: entryId, cron } as ScheduleEntry, error)
163
+ return null
164
+ }
165
+ }
166
+
167
+ return null
168
+ }
169
+
170
+ private findAllMatches(cron: string, from: Date, until: Date, entryId: string): Date[] {
171
+ const maxIterations = Math.min(
172
+ MAX_CATCHUP_MINUTES,
173
+ Math.floor((until.getTime() - from.getTime()) / 60_000) + 1,
174
+ )
175
+ const matches: Date[] = []
176
+
177
+ for (let i = 0; i < maxIterations; i++) {
178
+ const candidate = new Date(from.getTime() + i * 60_000)
179
+
180
+ if (candidate.getTime() > until.getTime()) break
181
+
182
+ try {
183
+ if (matchCron(cron, candidate)) matches.push(candidate)
184
+ } catch (error) {
185
+ this.logInvalidCron({ id: entryId, cron } as ScheduleEntry, error)
186
+ return []
187
+ }
188
+ }
189
+
190
+ return matches
191
+ }
192
+
193
+ private logInvalidCron(entry: Pick<ScheduleEntry, "id" | "cron">, error: unknown): void {
194
+ this.logger.error("invalid cron expression in schedule", {
195
+ connector: this.config.name,
196
+ id: entry.id,
197
+ cron: entry.cron,
198
+ error: error instanceof Error ? error.message : String(error),
199
+ })
200
+ }
201
+
202
+ private truncateToMinute(date: Date): Date {
203
+ const copy = new Date(date.getTime())
204
+ copy.setSeconds(0, 0)
205
+ return copy
206
+ }
207
+ }
@@ -0,0 +1,54 @@
1
+ import { dirname } from "node:path"
2
+ import { FunnelFileSystem } from "@/engine/fs/file-system"
3
+ import { NodeFunnelFileSystem } from "@/engine/fs/node-file-system"
4
+
5
+ type Deps = {
6
+ path: string
7
+ fs?: FunnelFileSystem
8
+ }
9
+
10
+ const defaultFs = new NodeFunnelFileSystem()
11
+
12
+ /**
13
+ * Per-connector lastFiredAt persistence for the schedule listener. The path is
14
+ * passed in by FunnelConnectorFactory so this store does not know about the
15
+ * funnel directory layout (`channels/<id>/connectors/<id>/state.json` lives
16
+ * outside this class).
17
+ */
18
+ export class ScheduleStateStore {
19
+ private readonly path: string
20
+ private readonly fs: FunnelFileSystem
21
+
22
+ constructor(deps: Deps) {
23
+ this.path = deps.path
24
+ this.fs = deps.fs ?? defaultFs
25
+ Object.freeze(this)
26
+ }
27
+
28
+ load(): Map<string, Date> {
29
+ const map = new Map<string, Date>()
30
+
31
+ if (!this.fs.existsSync(this.path)) return map
32
+
33
+ const raw: unknown = JSON.parse(this.fs.readFileSync(this.path))
34
+
35
+ if (raw === null || typeof raw !== "object") return map
36
+
37
+ for (const [id, iso] of Object.entries(raw)) {
38
+ if (typeof iso === "string") map.set(id, new Date(iso))
39
+ }
40
+
41
+ return map
42
+ }
43
+
44
+ save(state: Map<string, Date>): void {
45
+ const obj: Record<string, string> = {}
46
+
47
+ for (const [id, date] of state) {
48
+ obj[id] = date.toISOString()
49
+ }
50
+
51
+ this.fs.mkdirSync(dirname(this.path), { recursive: true })
52
+ this.fs.writeFileSync(this.path, `${JSON.stringify(obj, null, 2)}\n`)
53
+ }
54
+ }
@@ -0,0 +1,36 @@
1
+ import { WebClient } from "@slack/web-api"
2
+ import { FunnelConnectorAdapter, type CallInput } from "@/connectors/connector-adapter"
3
+ import type { SlackConnectorConfig } from "@/connectors/slack-connector-schema"
4
+
5
+ export type SlackWebClientLike = {
6
+ apiCall: (method: string, options?: Record<string, unknown>) => Promise<unknown>
7
+ }
8
+
9
+ const toRecord = (value: object): Record<string, unknown> => {
10
+ const result: Record<string, unknown> = {}
11
+
12
+ for (const [key, val] of Object.entries(value)) result[key] = val
13
+
14
+ return result
15
+ }
16
+
17
+ type Deps = {
18
+ config: SlackConnectorConfig
19
+ client?: SlackWebClientLike
20
+ }
21
+
22
+ export class FunnelSlackAdapter extends FunnelConnectorAdapter {
23
+ private readonly client: SlackWebClientLike
24
+
25
+ constructor(deps: Deps) {
26
+ super()
27
+ this.client = deps.client ?? new WebClient(deps.config.botToken)
28
+ Object.freeze(this)
29
+ }
30
+
31
+ async call(input: CallInput): Promise<unknown> {
32
+ const body = input.body !== null && typeof input.body === "object" ? toRecord(input.body) : {}
33
+
34
+ return await this.client.apiCall(input.path, body)
35
+ }
36
+ }
@@ -1,10 +1,13 @@
1
1
  import { z } from "zod"
2
2
 
3
3
  export const slackConnectorSchema = z.object({
4
- type: z.literal("slack"),
4
+ id: z.string(),
5
5
  name: z.string(),
6
+ type: z.literal("slack"),
6
7
  botToken: z.string().startsWith("xoxb-"),
7
8
  appToken: z.string().startsWith("xapp-"),
9
+ createdAt: z.string().datetime().optional(),
10
+ updatedAt: z.string().datetime().optional(),
8
11
  })
9
12
 
10
13
  export type SlackConnectorConfig = z.infer<typeof slackConnectorSchema>