@interactive-inc/claude-funnel 0.4.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 +233 -111
  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} +57 -22
  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} +39 -14
  69. package/lib/engine/channels/channels.ts +520 -0
  70. package/lib/{modules/claude/funnel-claude.ts → engine/claude/claude.ts} +47 -62
  71. package/lib/engine/claude/gateway-controller.ts +4 -0
  72. package/lib/{modules/fs/funnel-file-system.ts → engine/fs/file-system.ts} +9 -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/engine/id/id-generator.ts +7 -0
  78. package/lib/engine/id/memory-id-generator.ts +20 -0
  79. package/lib/engine/id/node-id-generator.ts +7 -0
  80. package/lib/engine/logger/logger.ts +11 -0
  81. package/lib/engine/logger/memory-logger.ts +28 -0
  82. package/lib/engine/logger/node-logger.ts +49 -0
  83. package/lib/engine/logger/noop-logger.ts +9 -0
  84. package/lib/engine/mcp/channel-server.ts +204 -0
  85. package/lib/{modules/mcp/funnel-mcp.ts → engine/mcp/mcp.ts} +29 -10
  86. package/lib/{modules/process/memory-funnel-process-runner.ts → engine/process/memory-process-runner.ts} +1 -1
  87. package/lib/{modules/process/node-funnel-process-runner.ts → engine/process/node-process-runner.ts} +12 -21
  88. package/lib/{modules/process/funnel-process-runner.ts → engine/process/process-runner.ts} +5 -0
  89. package/lib/engine/profiles/profile-channel-checker.ts +7 -0
  90. package/lib/engine/profiles/profiles.ts +126 -0
  91. package/lib/{modules/settings/mock-funnel-settings-reader.ts → engine/settings/mock-settings-reader.ts} +4 -3
  92. package/lib/{modules/settings/funnel-settings-reader.ts → engine/settings/settings-reader.ts} +1 -1
  93. package/lib/engine/settings/settings-schema.ts +46 -0
  94. package/lib/engine/settings/settings-store.ts +110 -0
  95. package/lib/engine/time/clock.ts +15 -0
  96. package/lib/engine/time/memory-clock.ts +26 -0
  97. package/lib/engine/time/node-clock.ts +7 -0
  98. package/lib/funnel.ts +148 -56
  99. package/lib/gateway/auth-middleware.ts +44 -0
  100. package/lib/gateway/broadcaster.ts +319 -0
  101. package/lib/gateway/daemon.ts +47 -0
  102. package/lib/gateway/factory.ts +10 -0
  103. package/lib/gateway/funnel-event-store.ts +155 -0
  104. package/lib/gateway/gateway-server.ts +414 -0
  105. package/lib/gateway/gateway-token.ts +79 -0
  106. package/lib/{modules/gateway/funnel-gateway.ts → gateway/gateway.ts} +70 -27
  107. package/lib/{modules/gateway → gateway}/kill-competing-slack-gateways.ts +7 -3
  108. package/lib/gateway/listener-supervisor.ts +339 -0
  109. package/lib/gateway/listeners-client.ts +128 -0
  110. package/lib/gateway/resolve-daemon-script.ts +26 -0
  111. package/lib/gateway/routes/channels.connectors.call.ts +39 -0
  112. package/lib/gateway/routes/health.ts +13 -0
  113. package/lib/gateway/routes/index.ts +24 -0
  114. package/lib/gateway/routes/listeners.list.ts +6 -0
  115. package/lib/gateway/routes/listeners.restart.ts +15 -0
  116. package/lib/gateway/routes/listeners.start.ts +15 -0
  117. package/lib/gateway/routes/listeners.stop.ts +15 -0
  118. package/lib/gateway/routes/route-deps.ts +11 -0
  119. package/lib/gateway/routes/status.ts +15 -0
  120. package/lib/gateway/routes/validator.ts +17 -0
  121. package/lib/index.ts +50 -92
  122. package/lib/logger/leuco-human-file-writer.ts +65 -0
  123. package/lib/logger/leuco-human-logger.ts +98 -0
  124. package/lib/logger/leuco-human-record.ts +16 -0
  125. package/lib/logger/leuco-human-stdout-writer.ts +26 -0
  126. package/lib/logger/leuco-human-writer.ts +14 -0
  127. package/lib/logger/leuco-logger-memory-sink.ts +67 -0
  128. package/lib/logger/leuco-logger-record.ts +13 -0
  129. package/lib/logger/leuco-logger-sink.ts +33 -0
  130. package/lib/logger/leuco-logger-sqlite-sink.ts +355 -0
  131. package/lib/logger/leuco-logger.ts +135 -0
  132. package/lib/tui/app.tsx +357 -0
  133. package/lib/tui/components/add-row.tsx +18 -0
  134. package/lib/tui/components/brand.tsx +27 -0
  135. package/lib/tui/components/card.tsx +44 -0
  136. package/lib/tui/components/detail-bar.tsx +46 -0
  137. package/lib/tui/components/editable-field.tsx +33 -0
  138. package/lib/tui/components/empty-state.tsx +11 -0
  139. package/lib/tui/components/gateway-status.tsx +66 -0
  140. package/lib/tui/components/keymap.tsx +29 -0
  141. package/lib/tui/components/menu-item.tsx +73 -0
  142. package/lib/tui/components/menu.tsx +26 -0
  143. package/lib/tui/components/panel-header.tsx +22 -0
  144. package/lib/tui/components/readonly-field.tsx +18 -0
  145. package/lib/tui/components/section-header.tsx +25 -0
  146. package/lib/tui/components/selection-accent.tsx +32 -0
  147. package/lib/tui/components/session-item.tsx +33 -0
  148. package/lib/tui/components/session-list.tsx +33 -0
  149. package/lib/tui/components/ui/hascii/accordion-item.tsx +88 -0
  150. package/lib/tui/components/ui/hascii/accordion.tsx +96 -0
  151. package/lib/tui/components/ui/hascii/alert-dialog.tsx +43 -0
  152. package/lib/tui/components/ui/hascii/badge.tsx +51 -0
  153. package/lib/tui/components/ui/hascii/breadcrumb.tsx +58 -0
  154. package/lib/tui/components/ui/hascii/button.tsx +194 -0
  155. package/lib/tui/components/ui/hascii/card-content.tsx +14 -0
  156. package/lib/tui/components/ui/hascii/card-description.tsx +13 -0
  157. package/lib/tui/components/ui/hascii/card-footer.tsx +14 -0
  158. package/lib/tui/components/ui/hascii/card-header.tsx +14 -0
  159. package/lib/tui/components/ui/hascii/card-title.tsx +13 -0
  160. package/lib/tui/components/ui/hascii/card.tsx +27 -0
  161. package/lib/tui/components/ui/hascii/checkbox.tsx +65 -0
  162. package/lib/tui/components/ui/hascii/command.tsx +159 -0
  163. package/lib/tui/components/ui/hascii/dialog-content.tsx +14 -0
  164. package/lib/tui/components/ui/hascii/dialog-description.tsx +13 -0
  165. package/lib/tui/components/ui/hascii/dialog-footer.tsx +14 -0
  166. package/lib/tui/components/ui/hascii/dialog-header.tsx +14 -0
  167. package/lib/tui/components/ui/hascii/dialog-title.tsx +13 -0
  168. package/lib/tui/components/ui/hascii/dialog.tsx +27 -0
  169. package/lib/tui/components/ui/hascii/file-tree.tsx +142 -0
  170. package/lib/tui/components/ui/hascii/focus-group.tsx +62 -0
  171. package/lib/tui/components/ui/hascii/form-item.tsx +43 -0
  172. package/lib/tui/components/ui/hascii/input-otp.tsx +86 -0
  173. package/lib/tui/components/ui/hascii/input.tsx +130 -0
  174. package/lib/tui/components/ui/hascii/pagination.tsx +105 -0
  175. package/lib/tui/components/ui/hascii/progress.tsx +28 -0
  176. package/lib/tui/components/ui/hascii/select.tsx +131 -0
  177. package/lib/tui/components/ui/hascii/separator.tsx +35 -0
  178. package/lib/tui/components/ui/hascii/sidebar-content.tsx +23 -0
  179. package/lib/tui/components/ui/hascii/sidebar-header.tsx +14 -0
  180. package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +67 -0
  181. package/lib/tui/components/ui/hascii/sidebar.tsx +24 -0
  182. package/lib/tui/components/ui/hascii/skeleton.tsx +60 -0
  183. package/lib/tui/components/ui/hascii/slider.tsx +91 -0
  184. package/lib/tui/components/ui/hascii/snackbar.tsx +75 -0
  185. package/lib/tui/components/ui/hascii/sparkline.tsx +53 -0
  186. package/lib/tui/components/ui/hascii/spinner.tsx +47 -0
  187. package/lib/tui/components/ui/hascii/stepper.tsx +54 -0
  188. package/lib/tui/components/ui/hascii/switch.tsx +66 -0
  189. package/lib/tui/components/ui/hascii/table.tsx +95 -0
  190. package/lib/tui/components/ui/hascii/tabs.tsx +59 -0
  191. package/lib/tui/components/ui/hascii/toggle-group-item.tsx +45 -0
  192. package/lib/tui/components/ui/hascii/toggle-group.tsx +99 -0
  193. package/lib/tui/components/ui/hascii/tree.tsx +104 -0
  194. package/lib/tui/components/view-shell.tsx +44 -0
  195. package/lib/tui/filter-input.tsx +33 -0
  196. package/lib/tui/hooks/hascii/use-pressable.ts +54 -0
  197. package/lib/tui/parse-comma-list.ts +14 -0
  198. package/lib/tui/profile-launcher.tsx +61 -0
  199. package/lib/tui/scrollbar-options.ts +19 -0
  200. package/lib/tui/sidebar.tsx +50 -0
  201. package/lib/tui/theme.ts +40 -0
  202. package/lib/tui/tui.tsx +20 -0
  203. package/lib/tui/types.ts +38 -0
  204. package/lib/tui/unique-name.ts +18 -0
  205. package/lib/tui/use-event-stream.ts +133 -0
  206. package/lib/tui/use-snapshot.ts +99 -0
  207. package/lib/tui/utils/hascii/form-item-context.tsx +23 -0
  208. package/lib/tui/utils/hascii/input-focus-context.tsx +31 -0
  209. package/lib/tui/utils/hascii/theme-context.tsx +26 -0
  210. package/lib/tui/utils/hascii/theme.ts +176 -0
  211. package/lib/tui/views/channels-view.tsx +108 -0
  212. package/lib/tui/views/connectors-view.tsx +164 -0
  213. package/lib/tui/views/events-view.tsx +160 -0
  214. package/lib/tui/views/listeners-view.tsx +80 -0
  215. package/lib/tui/views/profiles-view.tsx +152 -0
  216. package/package.json +51 -34
  217. package/lib/modules/channels/channel-connector-ref-updater.ts +0 -4
  218. package/lib/modules/channels/funnel-channels.ts +0 -155
  219. package/lib/modules/connectors/connector-config-schema.ts +0 -16
  220. package/lib/modules/connectors/connector-existence-checker.ts +0 -3
  221. package/lib/modules/connectors/funnel-callable-connector-store.ts +0 -9
  222. package/lib/modules/connectors/funnel-connector-listener.ts +0 -5
  223. package/lib/modules/connectors/funnel-connector-stores.ts +0 -24
  224. package/lib/modules/connectors/funnel-connector-type-store.ts +0 -24
  225. package/lib/modules/connectors/funnel-connectors.ts +0 -145
  226. package/lib/modules/connectors/funnel-discord-listener.ts +0 -65
  227. package/lib/modules/connectors/funnel-discord-store.ts +0 -84
  228. package/lib/modules/connectors/funnel-gh-store.ts +0 -84
  229. package/lib/modules/connectors/funnel-json-connector-store.ts +0 -100
  230. package/lib/modules/connectors/funnel-schedule-listener.ts +0 -124
  231. package/lib/modules/connectors/funnel-schedule-store.ts +0 -178
  232. package/lib/modules/connectors/funnel-slack-adapter.ts +0 -31
  233. package/lib/modules/connectors/funnel-slack-store.ts +0 -86
  234. package/lib/modules/connectors/migrate-legacy-connectors.ts +0 -77
  235. package/lib/modules/connectors/schedule-connector-schema.ts +0 -18
  236. package/lib/modules/connectors/schedule-last-fired-store.ts +0 -48
  237. package/lib/modules/gateway/daemon.ts +0 -207
  238. package/lib/modules/gateway/funnel-broadcaster.ts +0 -37
  239. package/lib/modules/gateway/funnel-event-logger.ts +0 -59
  240. package/lib/modules/logger.ts +0 -26
  241. package/lib/modules/mcp/channel-server.ts +0 -76
  242. package/lib/modules/profiles/funnel-profiles.ts +0 -123
  243. package/lib/modules/profiles/profile-channel-checker.ts +0 -3
  244. package/lib/modules/profiles/profile-channel-ref-updater.ts +0 -3
  245. package/lib/modules/repos/funnel-repositories.ts +0 -107
  246. package/lib/modules/schedule/funnel-schedule.ts +0 -34
  247. package/lib/modules/settings/funnel-settings-store.ts +0 -56
  248. package/lib/modules/settings/settings-schema.ts +0 -33
  249. package/lib/modules/tui/app.tsx +0 -44
  250. package/lib/modules/tui/tui.tsx +0 -13
  251. package/lib/routes/channels/add.help.ts +0 -3
  252. package/lib/routes/channels/add.ts +0 -21
  253. package/lib/routes/channels/connectors-attach.help.ts +0 -3
  254. package/lib/routes/channels/connectors-attach.ts +0 -17
  255. package/lib/routes/channels/connectors-detach.help.ts +0 -3
  256. package/lib/routes/channels/connectors-detach.ts +0 -17
  257. package/lib/routes/channels/group.help.ts +0 -16
  258. package/lib/routes/channels/group.ts +0 -22
  259. package/lib/routes/channels/remove.help.ts +0 -3
  260. package/lib/routes/channels/remove.ts +0 -17
  261. package/lib/routes/channels/rename.help.ts +0 -5
  262. package/lib/routes/channels/rename.ts +0 -17
  263. package/lib/routes/channels/routes.ts +0 -19
  264. package/lib/routes/channels/show.help.ts +0 -1
  265. package/lib/routes/channels/show.ts +0 -26
  266. package/lib/routes/claude/claude.help.ts +0 -16
  267. package/lib/routes/claude/claude.ts +0 -76
  268. package/lib/routes/claude/routes.ts +0 -4
  269. package/lib/routes/connectors/add.help.ts +0 -28
  270. package/lib/routes/connectors/add.ts +0 -64
  271. package/lib/routes/connectors/group.help.ts +0 -14
  272. package/lib/routes/connectors/group.ts +0 -18
  273. package/lib/routes/connectors/remove.help.ts +0 -3
  274. package/lib/routes/connectors/remove.ts +0 -17
  275. package/lib/routes/connectors/rename.help.ts +0 -5
  276. package/lib/routes/connectors/rename.ts +0 -17
  277. package/lib/routes/connectors/routes.ts +0 -23
  278. package/lib/routes/connectors/schedules-add.help.ts +0 -11
  279. package/lib/routes/connectors/schedules-add.ts +0 -33
  280. package/lib/routes/connectors/schedules-group.help.ts +0 -1
  281. package/lib/routes/connectors/schedules-group.ts +0 -38
  282. package/lib/routes/connectors/schedules-remove.help.ts +0 -3
  283. package/lib/routes/connectors/schedules-remove.ts +0 -17
  284. package/lib/routes/connectors/set.help.ts +0 -8
  285. package/lib/routes/connectors/set.ts +0 -72
  286. package/lib/routes/connectors/show.help.ts +0 -1
  287. package/lib/routes/connectors/show.ts +0 -41
  288. package/lib/routes/gateway/group.help.ts +0 -15
  289. package/lib/routes/gateway/group.ts +0 -28
  290. package/lib/routes/gateway/logs.help.ts +0 -13
  291. package/lib/routes/gateway/logs.ts +0 -100
  292. package/lib/routes/gateway/restart.help.ts +0 -10
  293. package/lib/routes/gateway/routes.ts +0 -18
  294. package/lib/routes/gateway/run.help.ts +0 -12
  295. package/lib/routes/gateway/run.ts +0 -35
  296. package/lib/routes/gateway/start.help.ts +0 -15
  297. package/lib/routes/gateway/start.ts +0 -32
  298. package/lib/routes/gateway/status.help.ts +0 -9
  299. package/lib/routes/gateway/status.ts +0 -28
  300. package/lib/routes/gateway/stop.help.ts +0 -8
  301. package/lib/routes/gateway/stop.ts +0 -21
  302. package/lib/routes/profiles/add.help.ts +0 -3
  303. package/lib/routes/profiles/add.ts +0 -33
  304. package/lib/routes/profiles/group.help.ts +0 -16
  305. package/lib/routes/profiles/group.ts +0 -25
  306. package/lib/routes/profiles/launch.help.ts +0 -4
  307. package/lib/routes/profiles/launch.ts +0 -36
  308. package/lib/routes/profiles/remove.help.ts +0 -3
  309. package/lib/routes/profiles/remove.ts +0 -17
  310. package/lib/routes/profiles/rename.help.ts +0 -5
  311. package/lib/routes/profiles/rename.ts +0 -17
  312. package/lib/routes/profiles/routes.ts +0 -18
  313. package/lib/routes/profiles/set.help.ts +0 -5
  314. package/lib/routes/profiles/set.ts +0 -32
  315. package/lib/routes/repos/add.help.ts +0 -6
  316. package/lib/routes/repos/add.ts +0 -20
  317. package/lib/routes/repos/group.help.ts +0 -11
  318. package/lib/routes/repos/group.ts +0 -18
  319. package/lib/routes/repos/remove.help.ts +0 -3
  320. package/lib/routes/repos/remove.ts +0 -17
  321. package/lib/routes/repos/rename.help.ts +0 -5
  322. package/lib/routes/repos/rename.ts +0 -17
  323. package/lib/routes/repos/routes.ts +0 -17
  324. package/lib/routes/repos/set.help.ts +0 -5
  325. package/lib/routes/repos/set.ts +0 -21
  326. package/lib/routes/repos/show.help.ts +0 -1
  327. package/lib/routes/repos/show.ts +0 -19
  328. package/lib/routes/request/discord-help.ts +0 -9
  329. package/lib/routes/request/discord.help.ts +0 -19
  330. package/lib/routes/request/discord.ts +0 -65
  331. package/lib/routes/request/group.help.ts +0 -15
  332. package/lib/routes/request/group.ts +0 -9
  333. package/lib/routes/request/routes.ts +0 -14
  334. package/lib/routes/request/slack-help.ts +0 -9
  335. package/lib/routes/request/slack.help.ts +0 -19
  336. package/lib/routes/request/slack.ts +0 -61
  337. package/lib/routes/status/routes.ts +0 -4
  338. package/lib/routes/status/status.help.ts +0 -6
  339. package/lib/routes/status/status.ts +0 -77
  340. package/lib/routes/update/routes.ts +0 -4
  341. package/lib/routes/update/update.help.ts +0 -5
  342. package/lib/routes/update/update.ts +0 -21
  343. package/lib/routes.ts +0 -40
  344. /package/lib/{factory.ts → cli/factory.ts} +0 -0
  345. /package/lib/{modules → cli}/router/query-to-cli-args.ts +0 -0
  346. /package/lib/{modules → cli}/router/validator.ts +0 -0
  347. /package/lib/{modules/connectors/funnel-connector-adapter.ts → connectors/connector-adapter.ts} +0 -0
  348. /package/lib/{modules/connectors/funnel-discord-event-processor.ts → connectors/discord-event-processor.ts} +0 -0
  349. /package/lib/{modules/http/funnel-http-client.ts → engine/http/http-client.ts} +0 -0
@@ -1,31 +1,65 @@
1
- import { join, resolve } from "node:path"
2
- import { FunnelFileSystem } from "@/modules/fs/funnel-file-system"
3
- import { NodeFunnelFileSystem } from "@/modules/fs/node-funnel-file-system"
4
- import { FunnelProcessRunner } from "@/modules/process/funnel-process-runner"
5
- import { NodeFunnelProcessRunner } from "@/modules/process/node-funnel-process-runner"
6
- import { FUNNEL_DIR } from "@/modules/settings/funnel-settings-store"
1
+ import { join } from "node:path"
2
+ import { FunnelFileSystem } from "@/engine/fs/file-system"
3
+ import { resolveDaemonScript } from "@/gateway/resolve-daemon-script"
4
+ import { NodeFunnelFileSystem } from "@/engine/fs/node-file-system"
5
+ import { FunnelProcessRunner } from "@/engine/process/process-runner"
6
+ import { NodeFunnelProcessRunner } from "@/engine/process/node-process-runner"
7
+ import { FUNNEL_DIR } from "@/engine/settings/settings-store"
8
+ import { FunnelClock } from "@/engine/time/clock"
9
+ import { NodeFunnelClock } from "@/engine/time/node-clock"
7
10
 
8
11
  const DEFAULT_PORT = 9742
9
- const PID_FILE = join(FUNNEL_DIR, "gateway.pid")
10
- const LOG_DIR = "/tmp/funnel/events"
11
- const GATEWAY_LOG = "/tmp/funnel/gateway.log"
12
- const TMP_DIR = "/tmp/funnel"
12
+ const DEFAULT_TMP_DIR = "/tmp/funnel"
13
+ const STARTUP_TIMEOUT_MS = 5000
14
+ const SIGTERM_TIMEOUT_MS = 2000
15
+ const POLL_INTERVAL_MS = 100
16
+ const SIGKILL_GRACE_MS = 200
13
17
 
14
18
  type Deps = {
15
19
  process?: FunnelProcessRunner
16
20
  fs?: FunnelFileSystem
21
+ clock?: FunnelClock
22
+ dir?: string
23
+ tmpDir?: string
24
+ port?: number
25
+ sleep?: (ms: number) => Promise<void>
17
26
  }
18
27
 
19
28
  const defaultProcess = new NodeFunnelProcessRunner()
20
29
  const defaultFs = new NodeFunnelFileSystem()
21
-
30
+ const defaultClock = new NodeFunnelClock()
31
+ const defaultSleep = (ms: number): Promise<void> =>
32
+ new Promise((r) => {
33
+ setTimeout(r, ms)
34
+ })
35
+
36
+ /**
37
+ * Manages the gateway daemon as a separate process via PID file.
38
+ * Use `start()` to spawn `bun daemon.ts` in the background and `stop()` to
39
+ * terminate it. For an in-process gateway, use `Funnel.gatewayServer` instead.
40
+ */
22
41
  export class FunnelGateway {
23
42
  private readonly process: FunnelProcessRunner
24
43
  private readonly fs: FunnelFileSystem
44
+ private readonly clock: FunnelClock
45
+ private readonly pidFile: string
46
+ private readonly logDir: string
47
+ private readonly gatewayLog: string
48
+ private readonly tmpDir: string
49
+ private readonly port: number
50
+ private readonly sleep: (ms: number) => Promise<void>
25
51
 
26
52
  constructor(deps: Deps = {}) {
27
53
  this.process = deps.process ?? defaultProcess
28
54
  this.fs = deps.fs ?? defaultFs
55
+ this.clock = deps.clock ?? defaultClock
56
+ const baseDir = deps.dir ?? FUNNEL_DIR
57
+ this.tmpDir = deps.tmpDir ?? DEFAULT_TMP_DIR
58
+ this.pidFile = join(baseDir, "gateway.pid")
59
+ this.logDir = join(this.tmpDir, "events")
60
+ this.gatewayLog = join(this.tmpDir, "gateway.log")
61
+ this.port = deps.port ?? DEFAULT_PORT
62
+ this.sleep = deps.sleep ?? defaultSleep
29
63
  Object.freeze(this)
30
64
  }
31
65
 
@@ -41,20 +75,25 @@ export class FunnelGateway {
41
75
  const pid = this.readPid()
42
76
  const running = pid !== null && this.isProcessAlive(pid)
43
77
 
44
- return { running, pid: running ? pid : null, port: DEFAULT_PORT }
78
+ return { running, pid: running ? pid : null, port: this.port }
45
79
  }
46
80
 
47
81
  async start(options: { caffeinate?: boolean } = {}): Promise<boolean> {
48
82
  if (this.isRunning()) return true
49
83
 
50
- this.fs.mkdirSync(TMP_DIR, { recursive: true })
84
+ this.fs.mkdirSync(this.tmpDir, { recursive: true })
51
85
 
52
- const gatewayScript = resolve(import.meta.dir, "./daemon.ts")
86
+ const gatewayScript = resolveDaemonScript()
53
87
  const command = this.buildStartCommand(gatewayScript, options)
54
88
 
55
89
  this.process.detach(["bash", "-c", command])
56
90
 
57
- await Bun.sleep(800)
91
+ const deadline = Date.now() + STARTUP_TIMEOUT_MS
92
+
93
+ while (Date.now() < deadline) {
94
+ if (this.isRunning()) return true
95
+ await this.sleep(POLL_INTERVAL_MS)
96
+ }
58
97
 
59
98
  return this.isRunning()
60
99
  }
@@ -63,7 +102,7 @@ export class FunnelGateway {
63
102
  const useCaffeinate = options.caffeinate !== false && globalThis.process.platform === "darwin"
64
103
  const prefix = useCaffeinate ? "caffeinate -i " : ""
65
104
 
66
- return `nohup ${prefix}bun ${gatewayScript} >> ${GATEWAY_LOG} 2>&1 &`
105
+ return `nohup ${prefix}bun ${gatewayScript} >> ${this.gatewayLog} 2>&1 &`
67
106
  }
68
107
 
69
108
  async stop(): Promise<boolean> {
@@ -77,29 +116,29 @@ export class FunnelGateway {
77
116
  }
78
117
 
79
118
  try {
80
- globalThis.process.kill(pid, "SIGTERM")
119
+ this.process.kill(pid, "SIGTERM")
81
120
  } catch {
82
121
  return false
83
122
  }
84
123
 
85
- const deadline = Date.now() + 2000
124
+ const deadline = this.clock.millis() + SIGTERM_TIMEOUT_MS
86
125
 
87
- while (Date.now() < deadline) {
126
+ while (this.clock.millis() < deadline) {
88
127
  if (!this.isProcessAlive(pid)) {
89
128
  this.removePid()
90
129
  return true
91
130
  }
92
131
 
93
- await Bun.sleep(100)
132
+ await this.sleep(POLL_INTERVAL_MS)
94
133
  }
95
134
 
96
135
  try {
97
- globalThis.process.kill(pid, "SIGKILL")
136
+ this.process.kill(pid, "SIGKILL")
98
137
  } catch {
99
138
  // ignore
100
139
  }
101
140
 
102
- await Bun.sleep(200)
141
+ await this.sleep(SIGKILL_GRACE_MS)
103
142
  this.removePid()
104
143
 
105
144
  return !this.isProcessAlive(pid)
@@ -126,18 +165,22 @@ export class FunnelGateway {
126
165
  }
127
166
 
128
167
  getLogDir(): string {
129
- return LOG_DIR
168
+ return this.logDir
130
169
  }
131
170
 
132
171
  getGatewayLog(): string {
133
- return GATEWAY_LOG
172
+ return this.gatewayLog
173
+ }
174
+
175
+ getPort(): number {
176
+ return this.port
134
177
  }
135
178
 
136
179
  private readPid(): number | null {
137
- if (!this.fs.existsSync(PID_FILE)) return null
180
+ if (!this.fs.existsSync(this.pidFile)) return null
138
181
 
139
182
  try {
140
- const content = this.fs.readFileSync(PID_FILE).trim()
183
+ const content = this.fs.readFileSync(this.pidFile).trim()
141
184
  const pid = Number(content)
142
185
 
143
186
  if (!pid || pid <= 0) return null
@@ -149,7 +192,7 @@ export class FunnelGateway {
149
192
  }
150
193
 
151
194
  private removePid(): void {
152
- this.fs.unlink(PID_FILE)
195
+ this.fs.unlink(this.pidFile)
153
196
  }
154
197
 
155
198
  private isProcessAlive(pid: number): boolean {
@@ -1,13 +1,16 @@
1
- import { logger } from "@/modules/logger"
2
- import { FunnelProcessRunner } from "@/modules/process/funnel-process-runner"
3
- import { NodeFunnelProcessRunner } from "@/modules/process/node-funnel-process-runner"
1
+ import { FunnelLogger } from "@/engine/logger/logger"
2
+ import { NodeFunnelLogger } from "@/engine/logger/node-logger"
3
+ import { FunnelProcessRunner } from "@/engine/process/process-runner"
4
+ import { NodeFunnelProcessRunner } from "@/engine/process/node-process-runner"
4
5
 
5
6
  type Props = {
6
7
  selfPid: number
7
8
  process?: FunnelProcessRunner
9
+ logger?: FunnelLogger
8
10
  }
9
11
 
10
12
  const defaultProcess = new NodeFunnelProcessRunner()
13
+ const defaultLogger = new NodeFunnelLogger()
11
14
 
12
15
  const isBun = (args: string): boolean => {
13
16
  return args.includes("bun ") || /\/bun(\s|$)/.test(args)
@@ -19,6 +22,7 @@ const looksLikeSlackGateway = (args: string): boolean => {
19
22
 
20
23
  export const killCompetingSlackGateways = async (props: Props): Promise<number[]> => {
21
24
  const runner = props.process ?? defaultProcess
25
+ const logger = props.logger ?? defaultLogger
22
26
  const result = await runner.run(["ps", "-e", "-o", "pid=,args="])
23
27
 
24
28
  if (result.exitCode !== 0) return []
@@ -0,0 +1,339 @@
1
+ import type { ConnectorConfig } from "@/connectors/connector-config-schema"
2
+ import type { FunnelConnectorListener } from "@/connectors/connector-listener"
3
+ import type { ChannelConnectorView } from "@/engine/channels/channels"
4
+ import { FunnelLogger } from "@/engine/logger/logger"
5
+ import { NodeFunnelLogger } from "@/engine/logger/node-logger"
6
+
7
+ type ConnectorRegistry = {
8
+ listAllConnectors(): ChannelConnectorView[]
9
+ createListener(
10
+ channelName: string,
11
+ connectorName: string,
12
+ ): { config: ConnectorConfig; channelId: string; listener: FunnelConnectorListener } | null
13
+ }
14
+
15
+ type SupervisorNotify = (
16
+ channelName: string,
17
+ connectorName: string,
18
+ content: string,
19
+ meta?: Record<string, string>,
20
+ ) => Promise<void>
21
+
22
+ type RunningEntry = {
23
+ config: ConnectorConfig
24
+ channelName: string
25
+ channelId: string
26
+ listener: FunnelConnectorListener
27
+ }
28
+
29
+ type ListenerStats = {
30
+ events: number
31
+ errors: number
32
+ failureCount: number
33
+ lastEventAt: string | null
34
+ }
35
+
36
+ type Deps = {
37
+ channels: ConnectorRegistry
38
+ notify: SupervisorNotify
39
+ logger?: FunnelLogger
40
+ healthCheckIntervalMs?: number
41
+ maxBackoffMs?: number
42
+ sleep?: (ms: number) => Promise<void>
43
+ now?: () => number
44
+ }
45
+
46
+ const defaultLogger = new NodeFunnelLogger()
47
+ const DEFAULT_HEALTH_INTERVAL_MS = 30_000
48
+ const DEFAULT_MAX_BACKOFF_MS = 60_000
49
+
50
+ const defaultSleep = (ms: number): Promise<void> =>
51
+ new Promise((r) => {
52
+ setTimeout(r, ms)
53
+ })
54
+
55
+ type ListenerEntryStatus = {
56
+ channelName: string
57
+ channelId: string
58
+ name: string
59
+ type: ConnectorConfig["type"]
60
+ alive: boolean
61
+ events: number
62
+ errors: number
63
+ failureCount: number
64
+ lastEventAt: string | null
65
+ }
66
+
67
+ /**
68
+ * Owns the running listener instances and their lifecycle.
69
+ *
70
+ * Lives in the gateway process and is the only place that calls
71
+ * `listener.start()` / `listener.stop()`. Each entry is keyed by
72
+ * `${channelName}/${connectorName}` so the same connector name can exist in
73
+ * multiple channels without colliding.
74
+ *
75
+ * Periodically polls each running listener's `isAlive()` and auto-restarts
76
+ * dead listeners with exponential backoff (1s, 2s, 4s, ... capped). Resets
77
+ * the backoff counter on successful restart.
78
+ */
79
+ export class FunnelListenerSupervisor {
80
+ private readonly channels: ConnectorRegistry
81
+ private readonly notify: SupervisorNotify
82
+ private readonly logger: FunnelLogger
83
+ private readonly running = new Map<string, RunningEntry>()
84
+ private readonly failureCounts = new Map<string, number>()
85
+ private readonly stats = new Map<string, ListenerStats>()
86
+ private readonly healthCheckIntervalMs: number
87
+ private readonly maxBackoffMs: number
88
+ private readonly sleep: (ms: number) => Promise<void>
89
+ private readonly now: () => number
90
+ private healthCheckTimer: ReturnType<typeof setInterval> | null = null
91
+ private healthCheckInFlight = false
92
+
93
+ constructor(deps: Deps) {
94
+ this.channels = deps.channels
95
+ this.notify = deps.notify
96
+ this.logger = deps.logger ?? defaultLogger
97
+ this.healthCheckIntervalMs = deps.healthCheckIntervalMs ?? DEFAULT_HEALTH_INTERVAL_MS
98
+ this.maxBackoffMs = deps.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS
99
+ this.sleep = deps.sleep ?? defaultSleep
100
+ this.now = deps.now ?? (() => Date.now())
101
+ }
102
+
103
+ static keyOf(channelName: string, connectorName: string): string {
104
+ return `${channelName}/${connectorName}`
105
+ }
106
+
107
+ isRunning(channelName: string, connectorName: string): boolean {
108
+ return this.running.has(FunnelListenerSupervisor.keyOf(channelName, connectorName))
109
+ }
110
+
111
+ list(): ListenerEntryStatus[] {
112
+ return [...this.running.entries()].map(([key, entry]) => {
113
+ const stats = this.stats.get(key)
114
+
115
+ return {
116
+ channelName: entry.channelName,
117
+ channelId: entry.channelId,
118
+ name: entry.config.name,
119
+ type: entry.config.type,
120
+ alive: entry.listener.isAlive(),
121
+ events: stats?.events ?? 0,
122
+ errors: stats?.errors ?? 0,
123
+ failureCount: this.failureCounts.get(key) ?? 0,
124
+ lastEventAt: stats?.lastEventAt ?? null,
125
+ }
126
+ })
127
+ }
128
+
129
+ async start(
130
+ channelName: string,
131
+ connectorName: string,
132
+ ): Promise<{ ok: boolean; reason?: string }> {
133
+ const key = FunnelListenerSupervisor.keyOf(channelName, connectorName)
134
+
135
+ if (this.running.has(key)) {
136
+ return { ok: true, reason: "already running" }
137
+ }
138
+
139
+ const created = this.channels.createListener(channelName, connectorName)
140
+
141
+ if (!created) {
142
+ return {
143
+ ok: false,
144
+ reason: `connector "${connectorName}" not found in channel "${channelName}"`,
145
+ }
146
+ }
147
+
148
+ const bind = async (content: string, meta?: Record<string, string>) => {
149
+ try {
150
+ await this.notify(channelName, connectorName, content, meta)
151
+ this.recordEvent(key)
152
+ } catch (error) {
153
+ this.recordError(key)
154
+ throw error
155
+ }
156
+ }
157
+
158
+ try {
159
+ await created.listener.start(bind)
160
+ this.running.set(key, {
161
+ config: created.config,
162
+ channelName,
163
+ channelId: created.channelId,
164
+ listener: created.listener,
165
+ })
166
+ this.ensureStats(key)
167
+ this.logger.info(`${created.config.type} listener started`, {
168
+ channel: channelName,
169
+ connector: connectorName,
170
+ })
171
+
172
+ return { ok: true }
173
+ } catch (error) {
174
+ this.logger.error(`${created.config.type} listener failed to start`, {
175
+ channel: channelName,
176
+ connector: connectorName,
177
+ error: error instanceof Error ? error.message : String(error),
178
+ })
179
+
180
+ return {
181
+ ok: false,
182
+ reason: error instanceof Error ? error.message : String(error),
183
+ }
184
+ }
185
+ }
186
+
187
+ async stop(
188
+ channelName: string,
189
+ connectorName: string,
190
+ ): Promise<{ ok: boolean; reason?: string }> {
191
+ const key = FunnelListenerSupervisor.keyOf(channelName, connectorName)
192
+ const entry = this.running.get(key)
193
+
194
+ if (!entry) return { ok: true, reason: "not running" }
195
+
196
+ try {
197
+ await entry.listener.stop()
198
+ this.running.delete(key)
199
+ this.failureCounts.delete(key)
200
+ this.logger.info(`${entry.config.type} listener stopped`, {
201
+ channel: channelName,
202
+ connector: connectorName,
203
+ })
204
+
205
+ return { ok: true }
206
+ } catch (error) {
207
+ this.logger.error(`${entry.config.type} listener failed to stop`, {
208
+ channel: channelName,
209
+ connector: connectorName,
210
+ error: error instanceof Error ? error.message : String(error),
211
+ })
212
+
213
+ return {
214
+ ok: false,
215
+ reason: error instanceof Error ? error.message : String(error),
216
+ }
217
+ }
218
+ }
219
+
220
+ async restart(
221
+ channelName: string,
222
+ connectorName: string,
223
+ ): Promise<{ ok: boolean; reason?: string }> {
224
+ const stopped = await this.stop(channelName, connectorName)
225
+
226
+ if (!stopped.ok) return stopped
227
+
228
+ return await this.start(channelName, connectorName)
229
+ }
230
+
231
+ async startAll(): Promise<void> {
232
+ const all = this.channels.listAllConnectors()
233
+
234
+ for (const view of all) {
235
+ await this.start(view.channelName, view.name)
236
+ }
237
+
238
+ this.startHealthCheck()
239
+ }
240
+
241
+ async stopAll(): Promise<void> {
242
+ this.stopHealthCheck()
243
+
244
+ for (const [, entry] of [...this.running.entries()]) {
245
+ await this.stop(entry.channelName, entry.config.name)
246
+ }
247
+ }
248
+
249
+ private ensureStats(key: string): ListenerStats {
250
+ const existing = this.stats.get(key)
251
+
252
+ if (existing) return existing
253
+
254
+ const fresh: ListenerStats = { events: 0, errors: 0, failureCount: 0, lastEventAt: null }
255
+
256
+ this.stats.set(key, fresh)
257
+
258
+ return fresh
259
+ }
260
+
261
+ private recordEvent(key: string): void {
262
+ const stats = this.ensureStats(key)
263
+
264
+ stats.events += 1
265
+ stats.lastEventAt = new Date(this.now()).toISOString()
266
+ }
267
+
268
+ private recordError(key: string): void {
269
+ this.ensureStats(key).errors += 1
270
+ }
271
+
272
+ private startHealthCheck(): void {
273
+ if (this.healthCheckTimer) return
274
+
275
+ this.healthCheckTimer = setInterval(() => {
276
+ void this.runHealthCheck()
277
+ }, this.healthCheckIntervalMs)
278
+
279
+ this.healthCheckTimer.unref()
280
+ }
281
+
282
+ private stopHealthCheck(): void {
283
+ if (!this.healthCheckTimer) return
284
+
285
+ clearInterval(this.healthCheckTimer)
286
+ this.healthCheckTimer = null
287
+ }
288
+
289
+ private async runHealthCheck(): Promise<void> {
290
+ if (this.healthCheckInFlight) return
291
+
292
+ this.healthCheckInFlight = true
293
+
294
+ try {
295
+ for (const [key, entry] of [...this.running.entries()]) {
296
+ if (entry.listener.isAlive()) {
297
+ this.failureCounts.delete(key)
298
+ continue
299
+ }
300
+
301
+ await this.recoverDead(entry.channelName, entry.config.name, entry.config.type)
302
+ }
303
+ } finally {
304
+ this.healthCheckInFlight = false
305
+ }
306
+ }
307
+
308
+ private async recoverDead(
309
+ channelName: string,
310
+ connectorName: string,
311
+ type: ConnectorConfig["type"],
312
+ ): Promise<void> {
313
+ const key = FunnelListenerSupervisor.keyOf(channelName, connectorName)
314
+ const failureCount = this.failureCounts.get(key) ?? 0
315
+ const backoffMs = Math.min(1000 * 2 ** failureCount, this.maxBackoffMs)
316
+
317
+ this.logger.warn(`${type} listener unhealthy, restarting`, {
318
+ channel: channelName,
319
+ connector: connectorName,
320
+ attempt: failureCount + 1,
321
+ backoffMs,
322
+ })
323
+
324
+ await this.stop(channelName, connectorName)
325
+ await this.sleep(backoffMs)
326
+
327
+ const result = await this.start(channelName, connectorName)
328
+
329
+ if (result.ok) {
330
+ this.failureCounts.delete(key)
331
+ this.logger.info(`${type} listener recovered`, {
332
+ channel: channelName,
333
+ connector: connectorName,
334
+ })
335
+ } else {
336
+ this.failureCounts.set(key, failureCount + 1)
337
+ }
338
+ }
339
+ }
@@ -0,0 +1,128 @@
1
+ import { z } from "zod"
2
+
3
+ type Deps = {
4
+ port: number
5
+ isDaemonRunning: () => boolean
6
+ /** Returns the daemon's gateway token, or null if unavailable. Sent as `Authorization: Bearer`. */
7
+ getToken?: () => string | null
8
+ }
9
+
10
+ const listenerEntrySchema = z.object({
11
+ channelName: z.string(),
12
+ channelId: z.string(),
13
+ name: z.string(),
14
+ type: z.string(),
15
+ alive: z.boolean(),
16
+ })
17
+
18
+ const listenersResponseSchema = z.object({
19
+ listeners: z.array(listenerEntrySchema),
20
+ })
21
+
22
+ const opErrorBodySchema = z.object({
23
+ reason: z.string().optional(),
24
+ })
25
+
26
+ export type ListenerEntry = z.infer<typeof listenerEntrySchema>
27
+
28
+ export type ListenerOpResult =
29
+ | { state: "ok" }
30
+ | { state: "offline" }
31
+ | { state: "error"; reason: string }
32
+
33
+ export type ListListenersResult =
34
+ | { state: "ok"; listeners: ListenerEntry[] }
35
+ | { state: "offline" }
36
+ | { state: "error"; reason: string }
37
+
38
+ const OFFLINE: ListenerOpResult = { state: "offline" }
39
+
40
+ /**
41
+ * HTTP client for listener operations on a running gateway daemon.
42
+ *
43
+ * Returns `{ state: "offline" }` when the daemon isn't running so callers
44
+ * (CLI hot-reload paths) can treat that as a no-op without parsing strings.
45
+ * Pair this with `FunnelGateway` (process control) for the full picture.
46
+ */
47
+ export class FunnelListenersClient {
48
+ private readonly port: number
49
+ private readonly isDaemonRunning: () => boolean
50
+ private readonly getToken: () => string | null
51
+
52
+ constructor(deps: Deps) {
53
+ this.port = deps.port
54
+ this.isDaemonRunning = deps.isDaemonRunning
55
+ this.getToken = deps.getToken ?? (() => null)
56
+ Object.freeze(this)
57
+ }
58
+
59
+ async list(): Promise<ListListenersResult> {
60
+ if (!this.isDaemonRunning()) return { state: "offline" }
61
+
62
+ try {
63
+ const res = await fetch(`http://localhost:${this.port}/listeners`, {
64
+ headers: this.authHeaders(),
65
+ })
66
+
67
+ if (!res.ok) return { state: "error", reason: `HTTP ${res.status}` }
68
+
69
+ const parsed = listenersResponseSchema.safeParse(await res.json())
70
+
71
+ if (!parsed.success) {
72
+ return { state: "error", reason: "malformed daemon response" }
73
+ }
74
+
75
+ return { state: "ok", listeners: parsed.data.listeners }
76
+ } catch (error) {
77
+ return { state: "error", reason: error instanceof Error ? error.message : String(error) }
78
+ }
79
+ }
80
+
81
+ async start(channelName: string, connectorName: string): Promise<ListenerOpResult> {
82
+ if (!this.isDaemonRunning()) return OFFLINE
83
+
84
+ return await this.call("POST", `/listeners/${this.path(channelName, connectorName)}/start`)
85
+ }
86
+
87
+ async stop(channelName: string, connectorName: string): Promise<ListenerOpResult> {
88
+ if (!this.isDaemonRunning()) return OFFLINE
89
+
90
+ return await this.call("DELETE", `/listeners/${this.path(channelName, connectorName)}`)
91
+ }
92
+
93
+ async restart(channelName: string, connectorName: string): Promise<ListenerOpResult> {
94
+ if (!this.isDaemonRunning()) return OFFLINE
95
+
96
+ return await this.call("POST", `/listeners/${this.path(channelName, connectorName)}/restart`)
97
+ }
98
+
99
+ private path(channelName: string, connectorName: string): string {
100
+ return `${encodeURIComponent(channelName)}/${encodeURIComponent(connectorName)}`
101
+ }
102
+
103
+ private authHeaders(): Record<string, string> {
104
+ const token = this.getToken()
105
+
106
+ return token ? { authorization: `Bearer ${token}` } : {}
107
+ }
108
+
109
+ private async call(method: "POST" | "DELETE", path: string): Promise<ListenerOpResult> {
110
+ try {
111
+ const res = await fetch(`http://localhost:${this.port}${path}`, {
112
+ method,
113
+ headers: this.authHeaders(),
114
+ })
115
+
116
+ if (!res.ok) {
117
+ const parsed = opErrorBodySchema.safeParse(await res.json().catch(() => null))
118
+ const reason = parsed.success ? parsed.data.reason : undefined
119
+
120
+ return { state: "error", reason: reason ?? `HTTP ${res.status}` }
121
+ }
122
+
123
+ return { state: "ok" }
124
+ } catch (error) {
125
+ return { state: "error", reason: error instanceof Error ? error.message : String(error) }
126
+ }
127
+ }
128
+ }