@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
@@ -0,0 +1,26 @@
1
+ import { existsSync } from "node:fs"
2
+ import { resolve } from "node:path"
3
+
4
+ /**
5
+ * Locate the daemon entry script. Works in both dev (running from source)
6
+ * and built mode (bundled into dist/bin.js with daemon at dist/gateway/daemon.js).
7
+ *
8
+ * The candidates cover:
9
+ * 1. dev: this helper lives at lib/gateway/, so daemon.ts is its sibling
10
+ * 2. built sibling: dist/gateway/daemon.js if the helper itself ends up at dist/gateway/
11
+ * 3. bundled: when this helper is inlined into dist/bin.js, import.meta.dir is dist/,
12
+ * and daemon.js lives at dist/gateway/daemon.js
13
+ */
14
+ export const resolveDaemonScript = (): string => {
15
+ const candidates = [
16
+ resolve(import.meta.dir, "./daemon.ts"),
17
+ resolve(import.meta.dir, "./daemon.js"),
18
+ resolve(import.meta.dir, "./gateway/daemon.js"),
19
+ ]
20
+
21
+ for (const candidate of candidates) {
22
+ if (existsSync(candidate)) return candidate
23
+ }
24
+
25
+ throw new Error(`daemon script not found (looked in ${candidates.join(", ")})`)
26
+ }
@@ -0,0 +1,39 @@
1
+ import { HTTPException } from "hono/http-exception"
2
+ import { z } from "zod"
3
+ import { factory } from "@/gateway/factory"
4
+ import { zParam } from "@/gateway/routes/validator"
5
+
6
+ const bodySchema = z.object({
7
+ method: z.string().min(1),
8
+ path: z.string().min(1),
9
+ body: z.unknown().optional(),
10
+ })
11
+
12
+ /**
13
+ * POST /channels/:channel/connectors/:connector/call
14
+ *
15
+ * Generic adapter call. Used by the funnel MCP server (running in the Claude
16
+ * Code process) to send replies/reactions/etc. without spawning a CLI
17
+ * subprocess. Mirrors the CLI's `funnel channels <c> connectors <conn> request
18
+ * --method=...` but with a structured JSON body and no shell.
19
+ */
20
+ export const channelsConnectorsCallHandler = factory.createHandlers(
21
+ zParam(z.object({ channel: z.string().min(1), connector: z.string().min(1) })),
22
+ async (c) => {
23
+ const param = c.req.valid("param")
24
+ const raw = await c.req.json().catch(() => null)
25
+ const parsed = bodySchema.safeParse(raw)
26
+
27
+ if (!parsed.success) {
28
+ throw new HTTPException(400, { message: parsed.error.issues[0]?.message ?? "invalid body" })
29
+ }
30
+
31
+ const result = await c.var.deps.channels.call(param.channel, param.connector, {
32
+ method: parsed.data.method,
33
+ path: parsed.data.path,
34
+ body: parsed.data.body ?? {},
35
+ })
36
+
37
+ return c.json({ ok: true, result })
38
+ },
39
+ )
@@ -0,0 +1,13 @@
1
+ import { factory } from "@/gateway/factory"
2
+
3
+ /** GET /health — liveness + listener registry snapshot. */
4
+ export const healthHandler = factory.createHandlers((c) => {
5
+ const deps = c.var.deps
6
+
7
+ return c.json({
8
+ ok: true,
9
+ pid: deps.selfPid,
10
+ clients: deps.broadcaster.getClientCount(),
11
+ listeners: deps.supervisor.list(),
12
+ })
13
+ })
@@ -0,0 +1,24 @@
1
+ import { factory } from "@/gateway/factory"
2
+ import { channelsConnectorsCallHandler } from "@/gateway/routes/channels.connectors.call"
3
+ import { healthHandler } from "@/gateway/routes/health"
4
+ import { listenersListHandler } from "@/gateway/routes/listeners.list"
5
+ import { listenersRestartHandler } from "@/gateway/routes/listeners.restart"
6
+ import { listenersStartHandler } from "@/gateway/routes/listeners.start"
7
+ import { listenersStopHandler } from "@/gateway/routes/listeners.stop"
8
+ import { statusHandler } from "@/gateway/routes/status"
9
+
10
+ /**
11
+ * Top-level Hono app for the gateway daemon. Mounts every HTTP endpoint flat
12
+ * (the WebSocket /ws upgrade is handled directly by `Bun.serve`). Deps come
13
+ * from the `deps` variable set by `FunnelGatewayServer`'s middleware — same
14
+ * shape as CLI's `c.var.funnel`.
15
+ */
16
+ export const gatewayRoutes = factory
17
+ .createApp()
18
+ .get("/health", ...healthHandler)
19
+ .get("/status", ...statusHandler)
20
+ .get("/listeners", ...listenersListHandler)
21
+ .post("/listeners/:channel/:connector/start", ...listenersStartHandler)
22
+ .delete("/listeners/:channel/:connector", ...listenersStopHandler)
23
+ .post("/listeners/:channel/:connector/restart", ...listenersRestartHandler)
24
+ .post("/channels/:channel/connectors/:connector/call", ...channelsConnectorsCallHandler)
@@ -0,0 +1,6 @@
1
+ import { factory } from "@/gateway/factory"
2
+
3
+ /** GET /listeners — running connector listeners with alive/dead status. */
4
+ export const listenersListHandler = factory.createHandlers((c) => {
5
+ return c.json({ listeners: c.var.deps.supervisor.list() })
6
+ })
@@ -0,0 +1,15 @@
1
+ import { z } from "zod"
2
+ import { factory } from "@/gateway/factory"
3
+ import { zParam } from "@/gateway/routes/validator"
4
+
5
+ /** POST /listeners/:channel/:connector/restart — stop + start a connector listener. */
6
+ export const listenersRestartHandler = factory.createHandlers(
7
+ zParam(z.object({ channel: z.string().min(1), connector: z.string().min(1) })),
8
+ async (c) => {
9
+ const param = c.req.valid("param")
10
+
11
+ const result = await c.var.deps.supervisor.restart(param.channel, param.connector)
12
+
13
+ return c.json(result, result.ok ? 200 : 400)
14
+ },
15
+ )
@@ -0,0 +1,15 @@
1
+ import { z } from "zod"
2
+ import { factory } from "@/gateway/factory"
3
+ import { zParam } from "@/gateway/routes/validator"
4
+
5
+ /** POST /listeners/:channel/:connector/start — start a connector listener. */
6
+ export const listenersStartHandler = factory.createHandlers(
7
+ zParam(z.object({ channel: z.string().min(1), connector: z.string().min(1) })),
8
+ async (c) => {
9
+ const param = c.req.valid("param")
10
+
11
+ const result = await c.var.deps.supervisor.start(param.channel, param.connector)
12
+
13
+ return c.json(result, result.ok ? 200 : 400)
14
+ },
15
+ )
@@ -0,0 +1,15 @@
1
+ import { z } from "zod"
2
+ import { factory } from "@/gateway/factory"
3
+ import { zParam } from "@/gateway/routes/validator"
4
+
5
+ /** DELETE /listeners/:channel/:connector — stop a connector listener. */
6
+ export const listenersStopHandler = factory.createHandlers(
7
+ zParam(z.object({ channel: z.string().min(1), connector: z.string().min(1) })),
8
+ async (c) => {
9
+ const param = c.req.valid("param")
10
+
11
+ const result = await c.var.deps.supervisor.stop(param.channel, param.connector)
12
+
13
+ return c.json(result, result.ok ? 200 : 400)
14
+ },
15
+ )
@@ -0,0 +1,11 @@
1
+ import type { FunnelChannels } from "@/engine/channels/channels"
2
+ import type { FunnelBroadcaster } from "@/gateway/broadcaster"
3
+ import type { FunnelListenerSupervisor } from "@/gateway/listener-supervisor"
4
+
5
+ export type GatewayRouteDeps = {
6
+ selfPid: number
7
+ broadcaster: FunnelBroadcaster
8
+ supervisor: FunnelListenerSupervisor
9
+ channels: FunnelChannels
10
+ uptimeMs: () => number
11
+ }
@@ -0,0 +1,15 @@
1
+ import { factory } from "@/gateway/factory"
2
+
3
+ /** GET /status — listener registry, connected channels, and broadcaster metrics. */
4
+ export const statusHandler = factory.createHandlers((c) => {
5
+ const deps = c.var.deps
6
+
7
+ return c.json({
8
+ ok: true,
9
+ pid: deps.selfPid,
10
+ uptimeMs: deps.uptimeMs(),
11
+ clients: deps.broadcaster.listChannels(),
12
+ listeners: deps.supervisor.list(),
13
+ broadcaster: deps.broadcaster.getMetrics(),
14
+ })
15
+ })
@@ -0,0 +1,17 @@
1
+ import { zValidator } from "@hono/zod-validator"
2
+ import type { ZodType } from "zod"
3
+
4
+ /**
5
+ * Path-param validator for gateway routes. On failure it answers with the same
6
+ * `{ ok: false, reason }` shape the listener routes already use, so
7
+ * `FunnelListenersClient` can surface the message without special-casing.
8
+ */
9
+ export const zParam = <T extends ZodType>(schema: T) =>
10
+ zValidator("param", schema, (result, c) => {
11
+ if (result.success) return
12
+
13
+ const issue = result.error.issues[0]
14
+ const reason = issue ? `${issue.path.join(".")}: ${issue.message}` : "invalid request"
15
+
16
+ return c.json({ ok: false, reason }, 400)
17
+ })
package/lib/index.ts CHANGED
@@ -1,92 +1,50 @@
1
- #!/usr/bin/env bun
2
- import pkg from "../package.json" with { type: "json" }
3
- import { createConnectorStores } from "@/modules/connectors/funnel-connector-stores"
4
- import { migrateLegacyConnectors } from "@/modules/connectors/migrate-legacy-connectors"
5
- import { startChannelServer } from "@/modules/mcp/channel-server"
6
- import { toRequest } from "@/modules/router/to-request"
7
- import { launchTui } from "@/modules/tui/tui"
8
- import { app } from "@/routes"
9
-
10
- process.title = "funnel"
11
-
12
- migrateLegacyConnectors({ stores: createConnectorStores() })
13
-
14
- const HELP = `funnel — Open Claude Funnel
15
-
16
- usage: funnel [command]
17
-
18
- commands:
19
- (none) launch TUI
20
- claude launch Claude Code (default profile or --profile)
21
- connectors manage external connections (Slack, etc.)
22
- channels manage subscription boxes
23
- profiles manage launch profiles
24
- request send an outbound API call via a connector
25
- repos manage repositories (extra)
26
- gateway manage the gateway
27
- status show overall connection status
28
- update update funnel to the latest version
29
- mcp run as an MCP server (invoked from .mcp.json)
30
-
31
- options:
32
- --help, -h show help
33
- --version, -v show version
34
-
35
- more: funnel <command> --help`
36
-
37
- const args = process.argv.slice(2)
38
-
39
- if (args.length === 0) {
40
- await launchTui()
41
- process.exit(0)
42
- }
43
-
44
- if (args[0] === "--version" || args[0] === "-v") {
45
- process.stdout.write(`${pkg.version}\n`)
46
- process.exit(0)
47
- }
48
-
49
- if (args[0] === "mcp") {
50
- await startChannelServer()
51
- } else {
52
- const { method, url } = toRequest(args)
53
-
54
- const parsed = new URL(url)
55
-
56
- if (parsed.searchParams.has("help")) {
57
- if (parsed.pathname === "/") {
58
- process.stdout.write(`${HELP}\n`)
59
- process.exit(0)
60
- }
61
-
62
- let res = await app.request(url, { method })
63
-
64
- if (!res.ok && method !== "GET") {
65
- res = await app.request(url, { method: "GET" })
66
- }
67
-
68
- if (!res.ok) {
69
- const group = parsed.pathname.split("/").filter(Boolean)[0]
70
-
71
- if (group) {
72
- res = await app.request(`http://localhost/${group}?help=true`, { method: "GET" })
73
- }
74
- }
75
-
76
- const text = res.ok ? await res.text() : HELP
77
- process.stdout.write(`${text}\n`)
78
- process.exit(0)
79
- }
80
-
81
- const res = await app.request(url, { method })
82
-
83
- if (!res.ok) {
84
- const text = await res.text()
85
- if (text) process.stderr.write(`${text}\n`)
86
- process.exit(1)
87
- }
88
-
89
- const body = await res.text()
90
-
91
- if (body) process.stdout.write(`${body}\n`)
92
- }
1
+ // Public API surface for the @interactive-inc/claude-funnel package.
2
+ // Organized by layer so consumers can find what they need at a glance.
3
+
4
+ // Facade
5
+ export * from "@/funnel"
6
+
7
+ // Engine domain
8
+ export * from "@/engine/channels/channels"
9
+ export * from "@/engine/claude/claude"
10
+ export * from "@/engine/mcp/mcp"
11
+ export * from "@/engine/profiles/profiles"
12
+ export * from "@/engine/settings/settings-reader"
13
+ export * from "@/engine/settings/settings-store"
14
+ export * from "@/engine/settings/mock-settings-reader"
15
+ export * from "@/engine/settings/settings-schema"
16
+
17
+ // Engine — boundaries (abstract + Node / Memory implementations)
18
+ export * from "@/engine/fs/file-system"
19
+ export * from "@/engine/fs/node-file-system"
20
+ export * from "@/engine/fs/memory-file-system"
21
+
22
+ export * from "@/engine/process/process-runner"
23
+ export * from "@/engine/process/node-process-runner"
24
+ export * from "@/engine/process/memory-process-runner"
25
+
26
+ export * from "@/engine/logger/logger"
27
+ export * from "@/engine/logger/node-logger"
28
+ export * from "@/engine/logger/memory-logger"
29
+ export * from "@/engine/logger/noop-logger"
30
+
31
+ export * from "@/engine/time/clock"
32
+ export * from "@/engine/time/node-clock"
33
+ export * from "@/engine/time/memory-clock"
34
+
35
+ export * from "@/engine/id/id-generator"
36
+ export * from "@/engine/id/node-id-generator"
37
+ export * from "@/engine/id/memory-id-generator"
38
+
39
+ // Connectors
40
+ export * from "@/connectors/connector-factory"
41
+ export * from "@/connectors/connector-config-schema"
42
+ export * from "@/connectors/schedule-connector-schema"
43
+
44
+ // Gateway
45
+ export * from "@/gateway/gateway"
46
+ export * from "@/gateway/gateway-server"
47
+ export * from "@/gateway/broadcaster"
48
+ export * from "@/gateway/funnel-event-store"
49
+ export * from "@/gateway/listener-supervisor"
50
+ export * from "@/gateway/listeners-client"
@@ -0,0 +1,65 @@
1
+ import { appendFileSync, existsSync, mkdirSync, renameSync, statSync, unlinkSync } from "node:fs"
2
+ import { dirname } from "node:path"
3
+ import type { LeucoHumanRecord } from "@/logger/leuco-human-record"
4
+ import type { LeucoHumanWriter } from "@/logger/leuco-human-writer"
5
+
6
+ type Props = {
7
+ /** Filesystem path. Parent directory is created on construct. */
8
+ path: string
9
+ /**
10
+ * Optional size cap in bytes. When the next write would push the file
11
+ * over the cap, the existing file becomes `<path>.1` (replacing any
12
+ * prior `.1`) and a fresh file takes its place. Single-keep rotation —
13
+ * a second cycle drops the previous `.1`.
14
+ */
15
+ maxBytes?: number
16
+ }
17
+
18
+ /**
19
+ * Appends one JSON line per record to a file. Optional one-keep size
20
+ * rotation. Designed for diagnostic logs a human tails (`tail -f file |
21
+ * jq`); not for replay or queries — use `LeucoLoggerSqliteSink` if you
22
+ * need indexed lookups.
23
+ *
24
+ * Writes are synchronous (`appendFileSync`), so each line is durable
25
+ * before `write` returns. Throughput matches the OS file cache; for
26
+ * high-volume logging consider buffering at the call site or using a
27
+ * different writer.
28
+ */
29
+ export class LeucoHumanFileWriter implements LeucoHumanWriter {
30
+ private readonly path: string
31
+ private readonly maxBytes: number | null
32
+
33
+ constructor(props: Props) {
34
+ this.path = props.path
35
+ this.maxBytes = props.maxBytes ?? null
36
+ this.ensureDir()
37
+ }
38
+
39
+ write(record: LeucoHumanRecord): void | Error {
40
+ try {
41
+ const line = `${JSON.stringify(record)}\n`
42
+ this.rotateIfNeeded(Buffer.byteLength(line))
43
+ appendFileSync(this.path, line)
44
+ } catch (e) {
45
+ return e instanceof Error ? e : new Error(String(e))
46
+ }
47
+ }
48
+
49
+ private ensureDir(): void {
50
+ const dir = dirname(this.path)
51
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
52
+ }
53
+
54
+ private rotateIfNeeded(incomingBytes: number): void {
55
+ if (this.maxBytes === null) return
56
+ if (!existsSync(this.path)) return
57
+
58
+ const size = statSync(this.path).size
59
+ if (size + incomingBytes <= this.maxBytes) return
60
+
61
+ const backup = `${this.path}.1`
62
+ if (existsSync(backup)) unlinkSync(backup)
63
+ renameSync(this.path, backup)
64
+ }
65
+ }
@@ -0,0 +1,98 @@
1
+ import type { LeucoHumanLevel, LeucoHumanRecord } from "@/logger/leuco-human-record"
2
+ import type { LeucoHumanWriter } from "@/logger/leuco-human-writer"
3
+
4
+ type WriteErrorHandler = (error: Error, record: LeucoHumanRecord) => void
5
+
6
+ type Props = {
7
+ /** Where records go. Use `LeucoHumanStdoutWriter`, `LeucoHumanFileWriter`, or your own. */
8
+ writer: LeucoHumanWriter
9
+ /** Minimum level to emit. Lower-rank records are dropped. Default: "info". */
10
+ level?: LeucoHumanLevel
11
+ /** Override for tests. Defaults to `Date.now`. */
12
+ now?: () => number
13
+ /** Observer for writer failures. Default: silently swallow. */
14
+ onWriteError?: WriteErrorHandler
15
+ }
16
+
17
+ const LEVEL_RANK: Record<LeucoHumanLevel, number> = {
18
+ info: 0,
19
+ warn: 1,
20
+ error: 2,
21
+ }
22
+
23
+ /**
24
+ * Human-facing diagnostic logger. The companion to `LeucoLogger`: where
25
+ * `LeucoLogger` is for schema-validated, replayable domain events,
26
+ * `LeucoHumanLogger` is for free-form info/warn/error messages destined
27
+ * for a human tailing a log or skimming during incident response.
28
+ *
29
+ * Keeping the two separate matters operationally:
30
+ * - Diagnostics typically out-volume domain events 10–1000x; mixing
31
+ * them in the same store would push events out under retention.
32
+ * - Diagnostics are unstructured by design; mixing them in would defeat
33
+ * the schema-first guarantee that makes domain events replayable.
34
+ * - Different audiences and queries (humans grep `tail -f` vs. tools
35
+ * query `WHERE seq > ?`).
36
+ *
37
+ * The writer is a port. Level gating happens here so writers receive only
38
+ * what is worth persisting. Failure isolation matches `LeucoLogger`: a
39
+ * writer that throws or returns Error is contained, surfaced via
40
+ * `onWriteError`, and never blocks the caller.
41
+ */
42
+ export class LeucoHumanLogger {
43
+ private readonly writer: LeucoHumanWriter
44
+ private readonly minRank: number
45
+ private readonly now: () => number
46
+ private readonly onWriteError: WriteErrorHandler | null
47
+
48
+ constructor(props: Props) {
49
+ this.writer = props.writer
50
+ this.minRank = LEVEL_RANK[props.level ?? "info"]
51
+ this.now = props.now ?? (() => Date.now())
52
+ this.onWriteError = props.onWriteError ?? null
53
+ }
54
+
55
+ info(message: string, meta?: Record<string, unknown>): void {
56
+ this.emit("info", message, meta)
57
+ }
58
+
59
+ warn(message: string, meta?: Record<string, unknown>): void {
60
+ this.emit("warn", message, meta)
61
+ }
62
+
63
+ error(message: string, meta?: Record<string, unknown>): void {
64
+ this.emit("error", message, meta)
65
+ }
66
+
67
+ close(): void {
68
+ if (!this.writer.close) return
69
+ try {
70
+ this.writer.close()
71
+ } catch {
72
+ // close failures are best-effort by definition
73
+ }
74
+ }
75
+
76
+ private emit(level: LeucoHumanLevel, message: string, meta?: Record<string, unknown>): void {
77
+ if (LEVEL_RANK[level] < this.minRank) return
78
+
79
+ const record: LeucoHumanRecord = {
80
+ ts: this.now(),
81
+ level,
82
+ message,
83
+ meta: meta ?? null,
84
+ }
85
+
86
+ const error = this.callWriter(record)
87
+ if (error && this.onWriteError) this.onWriteError(error, record)
88
+ }
89
+
90
+ private callWriter(record: LeucoHumanRecord): Error | null {
91
+ try {
92
+ const outcome = this.writer.write(record)
93
+ return outcome instanceof Error ? outcome : null
94
+ } catch (e) {
95
+ return e instanceof Error ? e : new Error(String(e))
96
+ }
97
+ }
98
+ }
@@ -0,0 +1,16 @@
1
+ export type LeucoHumanLevel = "info" | "warn" | "error"
2
+
3
+ /**
4
+ * One human-facing diagnostic log entry. Distinct from `LeucoLoggerRecord`
5
+ * (which wraps a schema-validated domain event) — this is the free-form,
6
+ * for-humans-tailing-a-log shape: a level, a message, and optional meta.
7
+ *
8
+ * `meta` is `null` rather than `undefined` when absent so writers can
9
+ * persist a uniform shape (no missing-key ambiguity in JSON Lines).
10
+ */
11
+ export type LeucoHumanRecord = {
12
+ ts: number
13
+ level: LeucoHumanLevel
14
+ message: string
15
+ meta: Record<string, unknown> | null
16
+ }
@@ -0,0 +1,26 @@
1
+ import type { LeucoHumanRecord } from "@/logger/leuco-human-record"
2
+ import type { LeucoHumanWriter } from "@/logger/leuco-human-writer"
3
+
4
+ type Stream = { write(s: string): void }
5
+
6
+ type Props = {
7
+ /** Override for tests. Defaults to `process.stdout`. */
8
+ out?: Stream
9
+ }
10
+
11
+ /**
12
+ * Writes one JSON line per record to stdout. Useful as the default writer
13
+ * for foreground daemons, dev runs, and short-lived processes where a
14
+ * file-backed log would be overkill.
15
+ */
16
+ export class LeucoHumanStdoutWriter implements LeucoHumanWriter {
17
+ private readonly out: Stream
18
+
19
+ constructor(props: Props = {}) {
20
+ this.out = props.out ?? process.stdout
21
+ }
22
+
23
+ write(record: LeucoHumanRecord): void {
24
+ this.out.write(`${JSON.stringify(record)}\n`)
25
+ }
26
+ }
@@ -0,0 +1,14 @@
1
+ import type { LeucoHumanRecord } from "@/logger/leuco-human-record"
2
+
3
+ /**
4
+ * Plugin port for `LeucoHumanLogger`. Writers decide where diagnostic
5
+ * records land — stdout, JSONL file, syslog, network, etc. — without the
6
+ * logger having to know about persistence shape.
7
+ *
8
+ * `write` returns `void` on success or an `Error` the logger surfaces via
9
+ * `onWriteError`. Throwing is also tolerated; the logger catches.
10
+ */
11
+ export type LeucoHumanWriter = {
12
+ write(record: LeucoHumanRecord): void | Error
13
+ close?(): void
14
+ }
@@ -0,0 +1,67 @@
1
+ import type { LeucoLoggerRecord } from "@/logger/leuco-logger-record"
2
+ import type { LeucoLoggerPrimarySink, LeucoLoggerSink } from "@/logger/leuco-logger-sink"
3
+
4
+ type Props = {
5
+ /** Hard cap on retained records. The oldest is evicted on overflow. 0 disables retention. */
6
+ capacity?: number
7
+ }
8
+
9
+ /**
10
+ * In-memory ring buffer that doubles as primary or relay. As primary it
11
+ * owns its own seq counter (single-process only — for multi-process
12
+ * safety, use `LeucoLoggerSqliteSink` as primary and place this as a
13
+ * relay). As relay it accepts whatever seq the primary assigned and
14
+ * advances its own counter to match, so `getMaxSeq` stays meaningful.
15
+ *
16
+ * Useful as a test double, as a short-window replay buffer paired with a
17
+ * persistent primary (covering reconnects without round-tripping disk),
18
+ * or as a backing store for live subscribers.
19
+ */
20
+ export class LeucoLoggerMemorySink<E> implements LeucoLoggerPrimarySink<E>, LeucoLoggerSink<E> {
21
+ private readonly capacity: number
22
+ private readonly buffer: LeucoLoggerRecord<E>[] = []
23
+ private seq = 0
24
+
25
+ constructor(props: Props = {}) {
26
+ this.capacity = Math.max(0, props.capacity ?? 1000)
27
+ }
28
+
29
+ insert(input: { ts: number; event: E }): LeucoLoggerRecord<E> {
30
+ this.seq += 1
31
+ const record: LeucoLoggerRecord<E> = {
32
+ seq: this.seq,
33
+ ts: input.ts,
34
+ event: input.event,
35
+ }
36
+ this.append(record)
37
+ return record
38
+ }
39
+
40
+ write(record: LeucoLoggerRecord<E>): void {
41
+ if (record.seq > this.seq) this.seq = record.seq
42
+ this.append(record)
43
+ }
44
+
45
+ getMaxSeq(): number {
46
+ return this.seq
47
+ }
48
+
49
+ getRecords(): ReadonlyArray<LeucoLoggerRecord<E>> {
50
+ return this.buffer
51
+ }
52
+
53
+ clear(): void {
54
+ this.buffer.length = 0
55
+ this.seq = 0
56
+ }
57
+
58
+ private append(record: LeucoLoggerRecord<E>): void {
59
+ if (this.capacity === 0) return
60
+
61
+ this.buffer.push(record)
62
+
63
+ if (this.buffer.length > this.capacity) {
64
+ this.buffer.shift()
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Wrapper that `LeucoLogger.emit` puts around every event before handing it
3
+ * to a sink. `seq` is monotonic across the lifetime of the underlying store —
4
+ * sinks persist it as the primary key so replay (and broadcaster seeding
5
+ * after restart) is an indexed range scan, not a full table walk. `ts` is
6
+ * epoch milliseconds. `event` is the caller-defined payload validated by the
7
+ * Zod schema passed to the bus.
8
+ */
9
+ export type LeucoLoggerRecord<E> = {
10
+ seq: number
11
+ ts: number
12
+ event: E
13
+ }