@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
@@ -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>
@@ -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
  }