@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,16 +1,20 @@
1
- import { type FileStat, FunnelFileSystem } from "@/modules/fs/funnel-file-system"
1
+ import { type FileStat, FunnelFileSystem } from "@/engine/fs/file-system"
2
2
 
3
3
  type Props = {
4
4
  dirs?: string[]
5
5
  files?: Record<string, string>
6
6
  mtimes?: Record<string, number>
7
+ modes?: Record<string, number>
7
8
  now?: () => number
8
9
  }
9
10
 
11
+ const SECRET_MODE = 0o600
12
+
10
13
  export class MemoryFunnelFileSystem extends FunnelFileSystem {
11
14
  private readonly dirs: Set<string>
12
15
  private readonly files: Map<string, string>
13
16
  private readonly mtimes: Map<string, number>
17
+ private readonly modes: Map<string, number>
14
18
  private readonly now: () => number
15
19
 
16
20
  constructor(props: Props = {}) {
@@ -18,6 +22,7 @@ export class MemoryFunnelFileSystem extends FunnelFileSystem {
18
22
  this.dirs = new Set(props.dirs ?? [])
19
23
  this.files = new Map(Object.entries(props.files ?? {}))
20
24
  this.mtimes = new Map(Object.entries(props.mtimes ?? {}))
25
+ this.modes = new Map(Object.entries(props.modes ?? {}))
21
26
  this.now = props.now ?? (() => Date.now())
22
27
  }
23
28
 
@@ -34,6 +39,12 @@ export class MemoryFunnelFileSystem extends FunnelFileSystem {
34
39
  this.touch(path)
35
40
  }
36
41
 
42
+ writeSecretFileSync(path: string, data: string): void {
43
+ this.files.set(path, data)
44
+ this.modes.set(path, SECRET_MODE)
45
+ this.touch(path)
46
+ }
47
+
37
48
  appendFileSync(path: string, data: string): void {
38
49
  const prev = this.files.get(path) ?? ""
39
50
  this.files.set(path, prev + data)
@@ -43,9 +54,11 @@ export class MemoryFunnelFileSystem extends FunnelFileSystem {
43
54
  unlink(path: string): void {
44
55
  this.files.delete(path)
45
56
  this.mtimes.delete(path)
57
+ this.modes.delete(path)
46
58
  }
47
59
 
48
- mkdirSync(path: string): void {
60
+ mkdirSync(path: string, options?: { recursive?: boolean }): void {
61
+ void options
49
62
  this.dirs.add(path)
50
63
  }
51
64
 
@@ -71,13 +84,17 @@ export class MemoryFunnelFileSystem extends FunnelFileSystem {
71
84
  throw new Error(`not found: ${path}`)
72
85
  }
73
86
 
74
- return { mtimeMs }
87
+ return { mtimeMs, mode: this.modes.get(path) ?? null }
75
88
  }
76
89
 
77
90
  setMtime(path: string, mtimeMs: number): void {
78
91
  this.mtimes.set(path, mtimeMs)
79
92
  }
80
93
 
94
+ setMode(path: string, mode: number): void {
95
+ this.modes.set(path, mode)
96
+ }
97
+
81
98
  private touch(path: string): void {
82
99
  if (!this.mtimes.has(path)) this.mtimes.set(path, this.now())
83
100
  else this.mtimes.set(path, this.now())
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  appendFileSync,
3
+ chmodSync,
3
4
  existsSync,
4
5
  mkdirSync,
5
6
  readdirSync,
@@ -8,7 +9,9 @@ import {
8
9
  unlinkSync,
9
10
  writeFileSync,
10
11
  } from "node:fs"
11
- import { type FileStat, FunnelFileSystem } from "@/modules/fs/funnel-file-system"
12
+ import { type FileStat, FunnelFileSystem } from "@/engine/fs/file-system"
13
+
14
+ const SECRET_MODE = 0o600
12
15
 
13
16
  export class NodeFunnelFileSystem extends FunnelFileSystem {
14
17
  constructor() {
@@ -28,6 +31,15 @@ export class NodeFunnelFileSystem extends FunnelFileSystem {
28
31
  writeFileSync(path, data)
29
32
  }
30
33
 
34
+ writeSecretFileSync(path: string, data: string): void {
35
+ writeFileSync(path, data, { mode: SECRET_MODE })
36
+ try {
37
+ chmodSync(path, SECRET_MODE)
38
+ } catch {
39
+ // ignore — best-effort tightening for files that already existed with looser perms
40
+ }
41
+ }
42
+
31
43
  appendFileSync(path: string, data: string): void {
32
44
  appendFileSync(path, data)
33
45
  }
@@ -51,6 +63,6 @@ export class NodeFunnelFileSystem extends FunnelFileSystem {
51
63
  statSync(path: string): FileStat {
52
64
  const stat = statSync(path)
53
65
 
54
- return { mtimeMs: stat.mtimeMs }
66
+ return { mtimeMs: stat.mtimeMs, mode: stat.mode & 0o777 }
55
67
  }
56
68
  }
@@ -1,8 +1,4 @@
1
- import {
2
- FunnelHttpClient,
3
- type HttpRequest,
4
- type HttpResponse,
5
- } from "@/modules/http/funnel-http-client"
1
+ import { FunnelHttpClient, type HttpRequest, type HttpResponse } from "@/engine/http/http-client"
6
2
 
7
3
  export type MemoryHttpResponse = {
8
4
  status?: number
@@ -1,8 +1,4 @@
1
- import {
2
- FunnelHttpClient,
3
- type HttpRequest,
4
- type HttpResponse,
5
- } from "@/modules/http/funnel-http-client"
1
+ import { FunnelHttpClient, type HttpRequest, type HttpResponse } from "@/engine/http/http-client"
6
2
 
7
3
  export class NodeFunnelHttpClient extends FunnelHttpClient {
8
4
  constructor() {
@@ -1,4 +1,4 @@
1
- import { FunnelIdGenerator } from "@/modules/id/funnel-id-generator"
1
+ import { FunnelIdGenerator } from "@/engine/id/id-generator"
2
2
 
3
3
  type Props = {
4
4
  prefix?: string
@@ -1,4 +1,4 @@
1
- import { FunnelIdGenerator } from "@/modules/id/funnel-id-generator"
1
+ import { FunnelIdGenerator } from "@/engine/id/id-generator"
2
2
 
3
3
  export class NodeFunnelIdGenerator extends FunnelIdGenerator {
4
4
  generate(): string {
@@ -1,4 +1,4 @@
1
- import { FunnelLogger } from "@/modules/logger/funnel-logger"
1
+ import { FunnelLogger } from "@/engine/logger/logger"
2
2
 
3
3
  export type LogEntry = {
4
4
  level: "info" | "warn" | "error"
@@ -1,6 +1,6 @@
1
1
  import { appendFileSync, mkdirSync } from "node:fs"
2
2
  import { dirname, join } from "node:path"
3
- import { FunnelLogger } from "@/modules/logger/funnel-logger"
3
+ import { FunnelLogger } from "@/engine/logger/logger"
4
4
 
5
5
  const DEFAULT_LOG_FILE = join("/tmp/funnel", "funnel.log")
6
6
 
@@ -1,4 +1,4 @@
1
- import { FunnelLogger } from "@/modules/logger/funnel-logger"
1
+ import { FunnelLogger } from "@/engine/logger/logger"
2
2
 
3
3
  export class NoopFunnelLogger extends FunnelLogger {
4
4
  readonly file = null
@@ -0,0 +1,204 @@
1
+ import { existsSync, readFileSync } from "node:fs"
2
+ import { homedir } from "node:os"
3
+ import { join } from "node:path"
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js"
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
6
+ import {
7
+ CallToolRequestSchema,
8
+ ListToolsRequestSchema,
9
+ } from "@modelcontextprotocol/sdk/types.js"
10
+ import { FUNNEL_MCP_NAME } from "@/engine/mcp/mcp"
11
+ import { settingsSchema } from "@/engine/settings/settings-schema"
12
+
13
+ const GATEWAY_BASE_URL = process.env.FUNNEL_GATEWAY_URL ?? "http://localhost:9742"
14
+ const GATEWAY_WS_URL = `${GATEWAY_BASE_URL.replace(/^http/, "ws")}/ws`
15
+ const RECONNECT_DELAY = 1000
16
+ const MAX_RECONNECT_DELAY = 10000
17
+ const SETTINGS_PATH = join(homedir(), ".funnel", "settings.json")
18
+ const TOOL_CONNECTOR_TYPES = new Set(["slack", "gh", "discord"])
19
+
20
+ const readGatewayToken = (): string | null => {
21
+ const fromEnv = process.env.FUNNEL_GATEWAY_TOKEN
22
+
23
+ if (fromEnv && fromEnv.length > 0) return fromEnv
24
+
25
+ const path = join(homedir(), ".funnel", "gateway.token")
26
+
27
+ if (!existsSync(path)) return null
28
+
29
+ const value = readFileSync(path, "utf-8").trim()
30
+
31
+ return value.length > 0 ? value : null
32
+ }
33
+
34
+ const readChannelConnectors = (
35
+ channelId: string,
36
+ ): { channelName: string; connectors: { name: string; type: string }[] } | null => {
37
+ if (!existsSync(SETTINGS_PATH)) return null
38
+
39
+ const raw = JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"))
40
+ const parsed = settingsSchema.safeParse(raw)
41
+
42
+ if (!parsed.success) return null
43
+
44
+ const channel = parsed.data.channels.find((c) => c.id === channelId)
45
+
46
+ if (!channel) return null
47
+
48
+ const connectors = channel.connectors
49
+ .filter((c) => TOOL_CONNECTOR_TYPES.has(c.type))
50
+ .map((c) => ({ name: c.name, type: c.type }))
51
+
52
+ return { channelName: channel.name, connectors }
53
+ }
54
+
55
+ const usageHintForType = (type: string): string => {
56
+ if (type === "slack") {
57
+ return "Slack Web API. method=POST path=chat.postMessage body={channel,text,thread_ts?}"
58
+ }
59
+
60
+ if (type === "discord") {
61
+ return "Discord REST API. method=POST path=/channels/<id>/messages body={content,...}"
62
+ }
63
+
64
+ if (type === "gh") {
65
+ return "GitHub REST via gh CLI. method=POST path=repos/owner/repo/issues/N/comments body={body}"
66
+ }
67
+
68
+ return "Generic adapter call."
69
+ }
70
+
71
+ export const startChannelServer = async (): Promise<void> => {
72
+ const channelId = process.env.FUNNEL_CHANNEL_ID
73
+ const channel = channelId ? readChannelConnectors(channelId) : null
74
+ const token = readGatewayToken()
75
+
76
+ const server = new Server(
77
+ { name: FUNNEL_MCP_NAME, version: "1.0.0" },
78
+ {
79
+ capabilities: {
80
+ experimental: { "claude/channel": {} },
81
+ tools: {},
82
+ },
83
+ instructions: [
84
+ `Events arrive inside <channel source="${FUNNEL_MCP_NAME}"> tags. Use meta.event_type to discriminate.`,
85
+ "",
86
+ "To reply or act, call the connector tool exposed by this MCP (one tool per connector configured on this channel). Each tool takes { method, path, body } matching the underlying adapter's CallInput.",
87
+ ].join("\n"),
88
+ },
89
+ )
90
+
91
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
92
+ const tools = (channel?.connectors ?? []).map((c) => ({
93
+ name: c.name,
94
+ description: `Call the "${c.name}" (${c.type}) connector. ${usageHintForType(c.type)}`,
95
+ inputSchema: {
96
+ type: "object" as const,
97
+ properties: {
98
+ method: { type: "string", description: "HTTP verb or API method (e.g. POST, chat.postMessage)" },
99
+ path: { type: "string", description: "API path or method name (adapter-specific)" },
100
+ body: { type: "object", description: "Request body / params (adapter-specific)" },
101
+ },
102
+ required: ["method", "path"],
103
+ },
104
+ }))
105
+
106
+ return { tools }
107
+ })
108
+
109
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
110
+ if (!channel) {
111
+ throw new Error("FUNNEL_CHANNEL_ID is not set or channel not found in settings.json")
112
+ }
113
+
114
+ const connectorName = request.params.name
115
+ const args = (request.params.arguments ?? {}) as Record<string, unknown>
116
+ const method = typeof args.method === "string" ? args.method : ""
117
+ const path = typeof args.path === "string" ? args.path : ""
118
+ const body = args.body ?? {}
119
+
120
+ if (!method || !path) {
121
+ throw new Error("`method` and `path` are required")
122
+ }
123
+
124
+ const url = `${GATEWAY_BASE_URL}/channels/${encodeURIComponent(channel.channelName)}/connectors/${encodeURIComponent(connectorName)}/call`
125
+ const headers: Record<string, string> = { "content-type": "application/json" }
126
+
127
+ if (token) headers.authorization = `Bearer ${token}`
128
+
129
+ const res = await fetch(url, {
130
+ method: "POST",
131
+ headers,
132
+ body: JSON.stringify({ method, path, body }),
133
+ })
134
+
135
+ const text = await res.text()
136
+
137
+ if (!res.ok) {
138
+ throw new Error(`gateway call failed (${res.status}): ${text}`)
139
+ }
140
+
141
+ return {
142
+ content: [{ type: "text", text }],
143
+ }
144
+ })
145
+
146
+ const transport = new StdioServerTransport()
147
+
148
+ await server.connect(transport)
149
+
150
+ if (!channelId) return
151
+
152
+ const baseUrl = `${GATEWAY_WS_URL}?channel=${encodeURIComponent(channelId)}`
153
+ const protocols = token ? [`funnel.token.${token}`] : undefined
154
+ let reconnectDelay = RECONNECT_DELAY
155
+ let lastOffset = 0
156
+
157
+ const connect = () => {
158
+ const sinceQuery = lastOffset > 0 ? `&since=${lastOffset}` : ""
159
+ const wsUrl = `${baseUrl}${sinceQuery}`
160
+ const ws = new WebSocket(wsUrl, protocols)
161
+
162
+ ws.addEventListener("open", () => {
163
+ reconnectDelay = RECONNECT_DELAY
164
+ process.stderr.write(`funnel: connected (${wsUrl})\n`)
165
+ })
166
+
167
+ ws.addEventListener("message", async (event) => {
168
+ try {
169
+ const payload = JSON.parse(String(event.data))
170
+ const eventType = payload.meta?.event_type ?? "unknown"
171
+
172
+ if (typeof payload.offset === "number" && payload.offset > lastOffset) {
173
+ lastOffset = payload.offset
174
+ }
175
+
176
+ process.stderr.write(`funnel: received event (${eventType})\n`)
177
+
178
+ await server.notification({
179
+ method: "notifications/claude/channel",
180
+ params: {
181
+ content: payload.content,
182
+ meta: payload.meta,
183
+ },
184
+ })
185
+ } catch (error) {
186
+ process.stderr.write(
187
+ `funnel: error: ${error instanceof Error ? error.message : String(error)}\n`,
188
+ )
189
+ }
190
+ })
191
+
192
+ ws.addEventListener("close", () => {
193
+ process.stderr.write(`funnel: disconnected, reconnecting in ${reconnectDelay}ms\n`)
194
+ setTimeout(connect, reconnectDelay)
195
+ reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY)
196
+ })
197
+
198
+ ws.addEventListener("error", () => {
199
+ // close handler will reconnect
200
+ })
201
+ }
202
+
203
+ connect()
204
+ }
@@ -1,18 +1,22 @@
1
1
  import { join } from "node:path"
2
- import { FunnelFileSystem } from "@/modules/fs/funnel-file-system"
3
- import { NodeFunnelFileSystem } from "@/modules/fs/node-funnel-file-system"
2
+ import { z } from "zod"
3
+ import { FunnelFileSystem } from "@/engine/fs/file-system"
4
+ import { NodeFunnelFileSystem } from "@/engine/fs/node-file-system"
4
5
 
5
6
  export const FUNNEL_MCP_COMMAND = "funnel"
6
7
  export const FUNNEL_MCP_NAME = "funnel"
7
8
 
8
- type McpEntry = {
9
- command?: string
10
- args?: string[]
11
- }
9
+ const mcpEntrySchema = z.object({
10
+ command: z.string().optional(),
11
+ args: z.array(z.string()).optional(),
12
+ })
12
13
 
13
- type McpConfig = {
14
- mcpServers?: Record<string, McpEntry>
15
- }
14
+ const mcpConfigSchema = z.object({
15
+ mcpServers: z.record(z.string(), mcpEntrySchema).optional(),
16
+ })
17
+
18
+ type McpEntry = z.infer<typeof mcpEntrySchema>
19
+ type McpConfig = z.infer<typeof mcpConfigSchema>
16
20
 
17
21
  type Deps = {
18
22
  fs?: FunnelFileSystem
@@ -95,13 +99,23 @@ export class FunnelMcp {
95
99
 
96
100
  if (!content) return {}
97
101
 
102
+ let parsed: unknown
103
+
98
104
  try {
99
- return JSON.parse(content) as McpConfig
105
+ parsed = JSON.parse(content)
100
106
  } catch (error) {
101
107
  throw new Error(
102
108
  `invalid .mcp.json (${mcpPath}): ${error instanceof Error ? error.message : String(error)}`,
103
109
  )
104
110
  }
111
+
112
+ const result = mcpConfigSchema.safeParse(parsed)
113
+
114
+ if (!result.success) {
115
+ throw new Error(`invalid .mcp.json (${mcpPath}): ${result.error.message}`)
116
+ }
117
+
118
+ return result.data
105
119
  }
106
120
 
107
121
  private writeConfig(repoPath: string, config: McpConfig): void {
@@ -4,7 +4,7 @@ import {
4
4
  FunnelProcessRunner,
5
5
  type RunOptions,
6
6
  type RunResult,
7
- } from "@/modules/process/funnel-process-runner"
7
+ } from "@/engine/process/process-runner"
8
8
 
9
9
  export type MemoryProcessResponse = {
10
10
  exitCode?: number
@@ -4,12 +4,22 @@ import {
4
4
  FunnelProcessRunner,
5
5
  type RunOptions,
6
6
  type RunResult,
7
- } from "@/modules/process/funnel-process-runner"
7
+ } from "@/engine/process/process-runner"
8
8
 
9
9
  const toEnv = (env?: Record<string, string>): Record<string, string> | undefined => {
10
10
  if (!env) return undefined
11
11
 
12
- return { ...(process.env as Record<string, string>), ...env }
12
+ const merged: Record<string, string> = {}
13
+
14
+ for (const [key, value] of Object.entries(process.env)) {
15
+ if (typeof value === "string") merged[key] = value
16
+ }
17
+
18
+ for (const [key, value] of Object.entries(env)) {
19
+ merged[key] = value
20
+ }
21
+
22
+ return merged
13
23
  }
14
24
 
15
25
  export class NodeFunnelProcessRunner extends FunnelProcessRunner {
@@ -59,25 +69,6 @@ export class NodeFunnelProcessRunner extends FunnelProcessRunner {
59
69
  stdio: ["inherit", "inherit", "inherit"],
60
70
  })
61
71
 
62
- const forward = (signal: "SIGINT" | "SIGTERM") => {
63
- try {
64
- proc.kill(signal)
65
- } catch {
66
- // ignore
67
- }
68
-
69
- setTimeout(() => {
70
- try {
71
- proc.kill("SIGKILL")
72
- } catch {
73
- // ignore
74
- }
75
- }, 3000).unref()
76
- }
77
-
78
- process.on("SIGINT", () => forward("SIGINT"))
79
- process.on("SIGTERM", () => forward("SIGTERM"))
80
-
81
72
  return await proc.exited
82
73
  }
83
74
 
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Read-side dependency that lets FunnelChannels ask whether a profile
3
+ * references a given channel id, without depending on FunnelProfiles directly.
4
+ */
5
+ export type ProfileChannelChecker = {
6
+ hasChannelRef(channelId: string): boolean
7
+ }
@@ -1,14 +1,21 @@
1
- import { FunnelSettingsReader } from "@/modules/settings/funnel-settings-reader"
2
- import type { ProfileConfig } from "@/modules/settings/settings-schema"
1
+ import { FunnelSettingsReader } from "@/engine/settings/settings-reader"
2
+ import type { ProfileConfig } from "@/engine/settings/settings-schema"
3
3
 
4
4
  type Deps = {
5
5
  store: FunnelSettingsReader
6
6
  }
7
7
 
8
8
  /**
9
- * Named launch presets for `fnl claude` (channel + repo + sub-agent + env files).
10
- * Implements ProfileChannelChecker / ProfileChannelRefUpdater so FunnelChannels
11
- * can validate references without depending on the FunnelProfiles concrete type.
9
+ * Named launch presets for `fnl claude`. Each profile bundles a working
10
+ * directory, a sub-agent name, and the channel id its Claude instance will
11
+ * subscribe to. Implements ProfileChannelChecker so FunnelChannels can refuse
12
+ * to remove a channel that is still referenced.
13
+ *
14
+ * The first entry in the persisted array is treated as the default profile;
15
+ * `asDefault` reorders the array to put a named profile first.
16
+ *
17
+ * `channelId` always stores the channel's stable id (uuid). CLI surfaces
18
+ * resolve channel name → id before calling `add`/`update` here.
12
19
  */
13
20
  export class FunnelProfiles {
14
21
  private readonly store: FunnelSettingsReader
@@ -26,6 +33,10 @@ export class FunnelProfiles {
26
33
  return this.list().find((p) => p.name === name) ?? null
27
34
  }
28
35
 
36
+ getDefault(): ProfileConfig | null {
37
+ return this.list()[0] ?? null
38
+ }
39
+
29
40
  add(config: ProfileConfig): void {
30
41
  const settings = this.store.read()
31
42
 
@@ -33,12 +44,8 @@ export class FunnelProfiles {
33
44
  throw new Error(`profile "${config.name}" already exists`)
34
45
  }
35
46
 
36
- if (!settings.channels.some((c) => c.name === config.channel)) {
37
- throw new Error(`channel "${config.channel}" not found`)
38
- }
39
-
40
- if (config.repo && !settings.repositories.some((r) => r.name === config.repo)) {
41
- throw new Error(`repo "${config.repo}" not found`)
47
+ if (!settings.channels.some((c) => c.id === config.channelId)) {
48
+ throw new Error(`channel id "${config.channelId}" not found`)
42
49
  }
43
50
 
44
51
  settings.profiles.push(config)
@@ -74,22 +81,26 @@ export class FunnelProfiles {
74
81
  this.store.write(settings)
75
82
  }
76
83
 
77
- hasChannelRef(channelName: string): boolean {
78
- return this.store.read().profiles.some((p) => p.channel === channelName)
79
- }
80
-
81
- renameChannelRef(oldName: string, newName: string): void {
84
+ asDefault(name: string): void {
82
85
  const settings = this.store.read()
83
- let changed = false
84
86
 
85
- for (const profile of settings.profiles) {
86
- if (profile.channel === oldName) {
87
- profile.channel = newName
88
- changed = true
89
- }
90
- }
87
+ const index = settings.profiles.findIndex((p) => p.name === name)
88
+
89
+ if (index < 0) throw new Error(`profile "${name}" not found`)
90
+
91
+ if (index === 0) return
92
+
93
+ const [profile] = settings.profiles.splice(index, 1)
91
94
 
92
- if (changed) this.store.write(settings)
95
+ if (!profile) return
96
+
97
+ settings.profiles.unshift(profile)
98
+
99
+ this.store.write(settings)
100
+ }
101
+
102
+ hasChannelRef(channelId: string): boolean {
103
+ return this.store.read().profiles.some((p) => p.channelId === channelId)
93
104
  }
94
105
 
95
106
  update(name: string, fields: Partial<Omit<ProfileConfig, "name">>): void {
@@ -99,29 +110,16 @@ export class FunnelProfiles {
99
110
 
100
111
  if (!profile) throw new Error(`profile "${name}" not found`)
101
112
 
102
- if (fields.channel !== undefined) {
103
- if (!settings.channels.some((c) => c.name === fields.channel)) {
104
- throw new Error(`channel "${fields.channel}" not found`)
105
- }
106
-
107
- profile.channel = fields.channel
108
- }
109
-
110
- if (fields.repo !== undefined) {
111
- if (fields.repo && !settings.repositories.some((r) => r.name === fields.repo)) {
112
- throw new Error(`repo "${fields.repo}" not found`)
113
+ if (fields.channelId !== undefined) {
114
+ if (!settings.channels.some((c) => c.id === fields.channelId)) {
115
+ throw new Error(`channel id "${fields.channelId}" not found`)
113
116
  }
114
117
 
115
- profile.repo = fields.repo || undefined
118
+ profile.channelId = fields.channelId
116
119
  }
117
120
 
118
- if (fields.subAgent !== undefined) {
119
- profile.subAgent = fields.subAgent || undefined
120
- }
121
-
122
- if (fields.envFiles !== undefined) {
123
- profile.envFiles = fields.envFiles
124
- }
121
+ if (fields.path !== undefined) profile.path = fields.path
122
+ if (fields.subAgent !== undefined) profile.subAgent = fields.subAgent
125
123
 
126
124
  this.store.write(settings)
127
125
  }
@@ -1,9 +1,10 @@
1
- import { FunnelSettingsReader } from "@/modules/settings/funnel-settings-reader"
2
- import type { Settings } from "@/modules/settings/settings-schema"
1
+ import { FunnelSettingsReader } from "@/engine/settings/settings-reader"
2
+ import { SETTINGS_VERSION } from "@/engine/settings/settings-schema"
3
+ import type { Settings } from "@/engine/settings/settings-schema"
3
4
 
4
5
  export const createSettings = (partial: Partial<Settings> = {}): Settings => ({
6
+ version: SETTINGS_VERSION,
5
7
  channels: [],
6
- repositories: [],
7
8
  profiles: [],
8
9
  ...partial,
9
10
  })
@@ -1,4 +1,4 @@
1
- import type { Settings } from "@/modules/settings/settings-schema"
1
+ import type { Settings } from "@/engine/settings/settings-schema"
2
2
 
3
3
  export abstract class FunnelSettingsReader {
4
4
  abstract read(): Settings