@interactive-inc/claude-funnel 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/README.md +155 -133
  2. package/dist/bin.js +1417 -0
  3. package/dist/gateway/daemon.js +513 -0
  4. package/dist/highlights-eq9cgrbb.scm +604 -0
  5. package/dist/highlights-ghv9g403.scm +205 -0
  6. package/dist/highlights-hk7bwhj4.scm +284 -0
  7. package/dist/highlights-r812a2qc.scm +150 -0
  8. package/dist/highlights-x6tmsnaa.scm +115 -0
  9. package/dist/injections-73j83es3.scm +27 -0
  10. package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  11. package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
  12. package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  13. package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  14. package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
  15. package/lib/bin.ts +78 -0
  16. package/lib/{modules → cli}/router/to-request.ts +13 -20
  17. package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +27 -0
  18. package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +40 -0
  19. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +41 -0
  20. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +22 -0
  21. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +23 -0
  22. package/lib/cli/routes/channels.$channel.connectors.$connector.ts +26 -0
  23. package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +92 -0
  24. package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +22 -0
  25. package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +63 -0
  26. package/lib/cli/routes/channels.$channel.connectors.ts +26 -0
  27. package/lib/cli/routes/channels.$channel.rename.$newName.ts +22 -0
  28. package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +34 -0
  29. package/lib/cli/routes/channels.$channel.ts +34 -0
  30. package/lib/cli/routes/channels.add.$channel.ts +33 -0
  31. package/lib/cli/routes/channels.remove.$channel.ts +20 -0
  32. package/lib/cli/routes/channels.ts +39 -0
  33. package/lib/cli/routes/claude.ts +69 -0
  34. package/lib/cli/routes/gateway.listeners.ts +41 -0
  35. package/lib/cli/routes/gateway.logs.ts +123 -0
  36. package/lib/{routes/gateway/restart.ts → cli/routes/gateway.restart.ts} +20 -5
  37. package/lib/cli/routes/gateway.run.ts +41 -0
  38. package/lib/cli/routes/gateway.start.ts +50 -0
  39. package/lib/cli/routes/gateway.status.ts +19 -0
  40. package/lib/cli/routes/gateway.stop.ts +32 -0
  41. package/lib/cli/routes/gateway.ts +55 -0
  42. package/lib/cli/routes/index.ts +202 -0
  43. package/lib/cli/routes/profiles.$profile.as-default.ts +22 -0
  44. package/lib/cli/routes/profiles.$profile.rename.$newName.ts +22 -0
  45. package/lib/cli/routes/profiles.$profile.run.ts +36 -0
  46. package/lib/cli/routes/profiles.add.$profile.ts +46 -0
  47. package/lib/cli/routes/profiles.remove.$profile.ts +20 -0
  48. package/lib/cli/routes/profiles.set.$profile.ts +46 -0
  49. package/lib/cli/routes/profiles.ts +40 -0
  50. package/lib/cli/routes/status.ts +93 -0
  51. package/lib/cli/routes/update.ts +27 -0
  52. package/lib/connectors/connector-config-schema.ts +16 -0
  53. package/lib/connectors/connector-factory.ts +94 -0
  54. package/lib/connectors/connector-listener.ts +20 -0
  55. package/lib/{modules/connectors/funnel-discord-adapter.ts → connectors/discord-adapter.ts} +6 -11
  56. package/lib/{modules/connectors → connectors}/discord-connector-schema.ts +4 -1
  57. package/lib/connectors/discord-listener.ts +111 -0
  58. package/lib/{modules/connectors/funnel-gh-adapter.ts → connectors/gh-adapter.ts} +3 -6
  59. package/lib/{modules/connectors → connectors}/gh-connector-schema.ts +4 -1
  60. package/lib/{modules/connectors/funnel-gh-listener.ts → connectors/gh-listener.ts} +45 -19
  61. package/lib/{modules/connectors → connectors}/match-cron.ts +10 -4
  62. package/lib/connectors/schedule-connector-schema.ts +33 -0
  63. package/lib/connectors/schedule-listener.ts +207 -0
  64. package/lib/connectors/schedule-state-store.ts +54 -0
  65. package/lib/connectors/slack-adapter.ts +36 -0
  66. package/lib/{modules/connectors → connectors}/slack-connector-schema.ts +4 -1
  67. package/lib/{modules/connectors/funnel-slack-event-processor.ts → connectors/slack-event-processor.ts} +15 -9
  68. package/lib/{modules/connectors/funnel-slack-listener.ts → connectors/slack-listener.ts} +33 -14
  69. package/lib/engine/channels/channels.ts +520 -0
  70. package/lib/{modules/claude/funnel-claude.ts → engine/claude/claude.ts} +28 -55
  71. package/lib/engine/claude/gateway-controller.ts +4 -0
  72. package/lib/{modules/fs/funnel-file-system.ts → engine/fs/file-system.ts} +4 -0
  73. package/lib/{modules/fs/memory-funnel-file-system.ts → engine/fs/memory-file-system.ts} +20 -3
  74. package/lib/{modules/fs/node-funnel-file-system.ts → engine/fs/node-file-system.ts} +14 -2
  75. package/lib/{modules/http/memory-funnel-http-client.ts → engine/http/memory-http-client.ts} +1 -5
  76. package/lib/{modules/http/node-funnel-http-client.ts → engine/http/node-http-client.ts} +1 -5
  77. package/lib/{modules/id/memory-funnel-id-generator.ts → engine/id/memory-id-generator.ts} +1 -1
  78. package/lib/{modules/id/node-funnel-id-generator.ts → engine/id/node-id-generator.ts} +1 -1
  79. package/lib/{modules/logger/memory-funnel-logger.ts → engine/logger/memory-logger.ts} +1 -1
  80. package/lib/{modules/logger/node-funnel-logger.ts → engine/logger/node-logger.ts} +1 -1
  81. package/lib/{modules/logger/noop-funnel-logger.ts → engine/logger/noop-logger.ts} +1 -1
  82. package/lib/engine/mcp/channel-server.ts +204 -0
  83. package/lib/{modules/mcp/funnel-mcp.ts → engine/mcp/mcp.ts} +24 -10
  84. package/lib/{modules/process/memory-funnel-process-runner.ts → engine/process/memory-process-runner.ts} +1 -1
  85. package/lib/{modules/process/node-funnel-process-runner.ts → engine/process/node-process-runner.ts} +12 -21
  86. package/lib/engine/profiles/profile-channel-checker.ts +7 -0
  87. package/lib/{modules/profiles/funnel-profiles.ts → engine/profiles/profiles.ts} +41 -43
  88. package/lib/{modules/settings/mock-funnel-settings-reader.ts → engine/settings/mock-settings-reader.ts} +4 -3
  89. package/lib/{modules/settings/funnel-settings-reader.ts → engine/settings/settings-reader.ts} +1 -1
  90. package/lib/engine/settings/settings-schema.ts +46 -0
  91. package/lib/engine/settings/settings-store.ts +110 -0
  92. package/lib/{modules/time/memory-funnel-clock.ts → engine/time/memory-clock.ts} +1 -1
  93. package/lib/{modules/time/node-funnel-clock.ts → engine/time/node-clock.ts} +1 -1
  94. package/lib/funnel.ts +83 -78
  95. package/lib/gateway/auth-middleware.ts +44 -0
  96. package/lib/gateway/broadcaster.ts +319 -0
  97. package/lib/gateway/daemon.ts +47 -0
  98. package/lib/gateway/factory.ts +10 -0
  99. package/lib/gateway/funnel-event-store.ts +155 -0
  100. package/lib/gateway/gateway-server.ts +414 -0
  101. package/lib/gateway/gateway-token.ts +79 -0
  102. package/lib/{modules/gateway/funnel-gateway.ts → gateway/gateway.ts} +27 -13
  103. package/lib/{modules/gateway → gateway}/kill-competing-slack-gateways.ts +4 -4
  104. package/lib/gateway/listener-supervisor.ts +339 -0
  105. package/lib/gateway/listeners-client.ts +128 -0
  106. package/lib/gateway/resolve-daemon-script.ts +26 -0
  107. package/lib/gateway/routes/channels.connectors.call.ts +39 -0
  108. package/lib/gateway/routes/health.ts +13 -0
  109. package/lib/gateway/routes/index.ts +24 -0
  110. package/lib/gateway/routes/listeners.list.ts +6 -0
  111. package/lib/gateway/routes/listeners.restart.ts +15 -0
  112. package/lib/gateway/routes/listeners.start.ts +15 -0
  113. package/lib/gateway/routes/listeners.stop.ts +15 -0
  114. package/lib/gateway/routes/route-deps.ts +11 -0
  115. package/lib/gateway/routes/status.ts +15 -0
  116. package/lib/gateway/routes/validator.ts +17 -0
  117. package/lib/index.ts +50 -92
  118. package/lib/logger/leuco-human-file-writer.ts +65 -0
  119. package/lib/logger/leuco-human-logger.ts +98 -0
  120. package/lib/logger/leuco-human-record.ts +16 -0
  121. package/lib/logger/leuco-human-stdout-writer.ts +26 -0
  122. package/lib/logger/leuco-human-writer.ts +14 -0
  123. package/lib/logger/leuco-logger-memory-sink.ts +67 -0
  124. package/lib/logger/leuco-logger-record.ts +13 -0
  125. package/lib/logger/leuco-logger-sink.ts +33 -0
  126. package/lib/logger/leuco-logger-sqlite-sink.ts +355 -0
  127. package/lib/logger/leuco-logger.ts +135 -0
  128. package/lib/tui/app.tsx +357 -0
  129. package/lib/tui/components/add-row.tsx +18 -0
  130. package/lib/tui/components/brand.tsx +27 -0
  131. package/lib/tui/components/card.tsx +44 -0
  132. package/lib/tui/components/detail-bar.tsx +46 -0
  133. package/lib/tui/components/editable-field.tsx +33 -0
  134. package/lib/tui/components/empty-state.tsx +11 -0
  135. package/lib/tui/components/gateway-status.tsx +66 -0
  136. package/lib/tui/components/keymap.tsx +29 -0
  137. package/lib/tui/components/menu-item.tsx +73 -0
  138. package/lib/tui/components/menu.tsx +26 -0
  139. package/lib/tui/components/panel-header.tsx +22 -0
  140. package/lib/tui/components/readonly-field.tsx +18 -0
  141. package/lib/tui/components/section-header.tsx +25 -0
  142. package/lib/tui/components/selection-accent.tsx +32 -0
  143. package/lib/tui/components/session-item.tsx +33 -0
  144. package/lib/tui/components/session-list.tsx +33 -0
  145. package/lib/tui/components/ui/hascii/accordion-item.tsx +88 -0
  146. package/lib/tui/components/ui/hascii/accordion.tsx +96 -0
  147. package/lib/tui/components/ui/hascii/alert-dialog.tsx +43 -0
  148. package/lib/tui/components/ui/hascii/badge.tsx +51 -0
  149. package/lib/tui/components/ui/hascii/breadcrumb.tsx +58 -0
  150. package/lib/tui/components/ui/hascii/button.tsx +194 -0
  151. package/lib/tui/components/ui/hascii/card-content.tsx +14 -0
  152. package/lib/tui/components/ui/hascii/card-description.tsx +13 -0
  153. package/lib/tui/components/ui/hascii/card-footer.tsx +14 -0
  154. package/lib/tui/components/ui/hascii/card-header.tsx +14 -0
  155. package/lib/tui/components/ui/hascii/card-title.tsx +13 -0
  156. package/lib/tui/components/ui/hascii/card.tsx +27 -0
  157. package/lib/tui/components/ui/hascii/checkbox.tsx +65 -0
  158. package/lib/tui/components/ui/hascii/command.tsx +159 -0
  159. package/lib/tui/components/ui/hascii/dialog-content.tsx +14 -0
  160. package/lib/tui/components/ui/hascii/dialog-description.tsx +13 -0
  161. package/lib/tui/components/ui/hascii/dialog-footer.tsx +14 -0
  162. package/lib/tui/components/ui/hascii/dialog-header.tsx +14 -0
  163. package/lib/tui/components/ui/hascii/dialog-title.tsx +13 -0
  164. package/lib/tui/components/ui/hascii/dialog.tsx +27 -0
  165. package/lib/tui/components/ui/hascii/file-tree.tsx +142 -0
  166. package/lib/tui/components/ui/hascii/focus-group.tsx +62 -0
  167. package/lib/tui/components/ui/hascii/form-item.tsx +43 -0
  168. package/lib/tui/components/ui/hascii/input-otp.tsx +86 -0
  169. package/lib/tui/components/ui/hascii/input.tsx +130 -0
  170. package/lib/tui/components/ui/hascii/pagination.tsx +105 -0
  171. package/lib/tui/components/ui/hascii/progress.tsx +28 -0
  172. package/lib/tui/components/ui/hascii/select.tsx +131 -0
  173. package/lib/tui/components/ui/hascii/separator.tsx +35 -0
  174. package/lib/tui/components/ui/hascii/sidebar-content.tsx +23 -0
  175. package/lib/tui/components/ui/hascii/sidebar-header.tsx +14 -0
  176. package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +67 -0
  177. package/lib/tui/components/ui/hascii/sidebar.tsx +24 -0
  178. package/lib/tui/components/ui/hascii/skeleton.tsx +60 -0
  179. package/lib/tui/components/ui/hascii/slider.tsx +91 -0
  180. package/lib/tui/components/ui/hascii/snackbar.tsx +75 -0
  181. package/lib/tui/components/ui/hascii/sparkline.tsx +53 -0
  182. package/lib/tui/components/ui/hascii/spinner.tsx +47 -0
  183. package/lib/tui/components/ui/hascii/stepper.tsx +54 -0
  184. package/lib/tui/components/ui/hascii/switch.tsx +66 -0
  185. package/lib/tui/components/ui/hascii/table.tsx +95 -0
  186. package/lib/tui/components/ui/hascii/tabs.tsx +59 -0
  187. package/lib/tui/components/ui/hascii/toggle-group-item.tsx +45 -0
  188. package/lib/tui/components/ui/hascii/toggle-group.tsx +99 -0
  189. package/lib/tui/components/ui/hascii/tree.tsx +104 -0
  190. package/lib/tui/components/view-shell.tsx +44 -0
  191. package/lib/tui/filter-input.tsx +33 -0
  192. package/lib/tui/hooks/hascii/use-pressable.ts +54 -0
  193. package/lib/tui/parse-comma-list.ts +14 -0
  194. package/lib/tui/profile-launcher.tsx +61 -0
  195. package/lib/tui/scrollbar-options.ts +19 -0
  196. package/lib/tui/sidebar.tsx +50 -0
  197. package/lib/tui/theme.ts +40 -0
  198. package/lib/tui/tui.tsx +20 -0
  199. package/lib/tui/types.ts +38 -0
  200. package/lib/tui/unique-name.ts +18 -0
  201. package/lib/tui/use-event-stream.ts +133 -0
  202. package/lib/tui/use-snapshot.ts +99 -0
  203. package/lib/tui/utils/hascii/form-item-context.tsx +23 -0
  204. package/lib/tui/utils/hascii/input-focus-context.tsx +31 -0
  205. package/lib/tui/utils/hascii/theme-context.tsx +26 -0
  206. package/lib/tui/utils/hascii/theme.ts +176 -0
  207. package/lib/tui/views/channels-view.tsx +108 -0
  208. package/lib/tui/views/connectors-view.tsx +164 -0
  209. package/lib/tui/views/events-view.tsx +160 -0
  210. package/lib/tui/views/listeners-view.tsx +80 -0
  211. package/lib/tui/views/profiles-view.tsx +152 -0
  212. package/package.json +50 -44
  213. package/lib/api.ts +0 -54
  214. package/lib/modules/channels/channel-connector-ref-updater.ts +0 -4
  215. package/lib/modules/channels/funnel-channels.ts +0 -160
  216. package/lib/modules/connectors/connector-config-schema.ts +0 -16
  217. package/lib/modules/connectors/connector-existence-checker.ts +0 -3
  218. package/lib/modules/connectors/funnel-callable-connector-store.ts +0 -9
  219. package/lib/modules/connectors/funnel-connector-listener.ts +0 -5
  220. package/lib/modules/connectors/funnel-connector-stores.ts +0 -52
  221. package/lib/modules/connectors/funnel-connector-type-store.ts +0 -24
  222. package/lib/modules/connectors/funnel-connectors.ts +0 -151
  223. package/lib/modules/connectors/funnel-discord-listener.ts +0 -71
  224. package/lib/modules/connectors/funnel-discord-store.ts +0 -88
  225. package/lib/modules/connectors/funnel-gh-store.ts +0 -101
  226. package/lib/modules/connectors/funnel-json-connector-store.ts +0 -100
  227. package/lib/modules/connectors/funnel-schedule-listener.ts +0 -130
  228. package/lib/modules/connectors/funnel-schedule-store.ts +0 -195
  229. package/lib/modules/connectors/funnel-slack-adapter.ts +0 -31
  230. package/lib/modules/connectors/funnel-slack-store.ts +0 -90
  231. package/lib/modules/connectors/migrate-legacy-connectors.ts +0 -81
  232. package/lib/modules/connectors/schedule-connector-schema.ts +0 -18
  233. package/lib/modules/connectors/schedule-last-fired-store.ts +0 -48
  234. package/lib/modules/gateway/daemon.ts +0 -74
  235. package/lib/modules/gateway/funnel-broadcaster.ts +0 -37
  236. package/lib/modules/gateway/funnel-event-logger.ts +0 -59
  237. package/lib/modules/gateway/funnel-gateway-server.ts +0 -241
  238. package/lib/modules/mcp/channel-server.ts +0 -76
  239. package/lib/modules/profiles/profile-channel-checker.ts +0 -3
  240. package/lib/modules/profiles/profile-channel-ref-updater.ts +0 -3
  241. package/lib/modules/repos/funnel-repositories.ts +0 -112
  242. package/lib/modules/schedule/funnel-schedule.ts +0 -39
  243. package/lib/modules/settings/funnel-settings-store.ts +0 -56
  244. package/lib/modules/settings/settings-schema.ts +0 -33
  245. package/lib/modules/tui/app.tsx +0 -44
  246. package/lib/modules/tui/tui.tsx +0 -13
  247. package/lib/routes/channels/add.help.ts +0 -3
  248. package/lib/routes/channels/add.ts +0 -21
  249. package/lib/routes/channels/connectors-attach.help.ts +0 -3
  250. package/lib/routes/channels/connectors-attach.ts +0 -17
  251. package/lib/routes/channels/connectors-detach.help.ts +0 -3
  252. package/lib/routes/channels/connectors-detach.ts +0 -17
  253. package/lib/routes/channels/group.help.ts +0 -16
  254. package/lib/routes/channels/group.ts +0 -22
  255. package/lib/routes/channels/remove.help.ts +0 -3
  256. package/lib/routes/channels/remove.ts +0 -17
  257. package/lib/routes/channels/rename.help.ts +0 -5
  258. package/lib/routes/channels/rename.ts +0 -17
  259. package/lib/routes/channels/routes.ts +0 -19
  260. package/lib/routes/channels/show.help.ts +0 -1
  261. package/lib/routes/channels/show.ts +0 -26
  262. package/lib/routes/claude/claude.help.ts +0 -16
  263. package/lib/routes/claude/claude.ts +0 -76
  264. package/lib/routes/claude/routes.ts +0 -4
  265. package/lib/routes/connectors/add.help.ts +0 -28
  266. package/lib/routes/connectors/add.ts +0 -64
  267. package/lib/routes/connectors/group.help.ts +0 -14
  268. package/lib/routes/connectors/group.ts +0 -18
  269. package/lib/routes/connectors/remove.help.ts +0 -3
  270. package/lib/routes/connectors/remove.ts +0 -17
  271. package/lib/routes/connectors/rename.help.ts +0 -5
  272. package/lib/routes/connectors/rename.ts +0 -17
  273. package/lib/routes/connectors/routes.ts +0 -23
  274. package/lib/routes/connectors/schedules-add.help.ts +0 -11
  275. package/lib/routes/connectors/schedules-add.ts +0 -33
  276. package/lib/routes/connectors/schedules-group.help.ts +0 -1
  277. package/lib/routes/connectors/schedules-group.ts +0 -38
  278. package/lib/routes/connectors/schedules-remove.help.ts +0 -3
  279. package/lib/routes/connectors/schedules-remove.ts +0 -17
  280. package/lib/routes/connectors/set.help.ts +0 -8
  281. package/lib/routes/connectors/set.ts +0 -72
  282. package/lib/routes/connectors/show.help.ts +0 -1
  283. package/lib/routes/connectors/show.ts +0 -41
  284. package/lib/routes/gateway/group.help.ts +0 -15
  285. package/lib/routes/gateway/group.ts +0 -28
  286. package/lib/routes/gateway/logs.help.ts +0 -13
  287. package/lib/routes/gateway/logs.ts +0 -102
  288. package/lib/routes/gateway/restart.help.ts +0 -10
  289. package/lib/routes/gateway/routes.ts +0 -18
  290. package/lib/routes/gateway/run.help.ts +0 -12
  291. package/lib/routes/gateway/run.ts +0 -35
  292. package/lib/routes/gateway/start.help.ts +0 -15
  293. package/lib/routes/gateway/start.ts +0 -32
  294. package/lib/routes/gateway/status.help.ts +0 -9
  295. package/lib/routes/gateway/status.ts +0 -28
  296. package/lib/routes/gateway/stop.help.ts +0 -8
  297. package/lib/routes/gateway/stop.ts +0 -21
  298. package/lib/routes/profiles/add.help.ts +0 -3
  299. package/lib/routes/profiles/add.ts +0 -33
  300. package/lib/routes/profiles/group.help.ts +0 -16
  301. package/lib/routes/profiles/group.ts +0 -25
  302. package/lib/routes/profiles/launch.help.ts +0 -4
  303. package/lib/routes/profiles/launch.ts +0 -36
  304. package/lib/routes/profiles/remove.help.ts +0 -3
  305. package/lib/routes/profiles/remove.ts +0 -17
  306. package/lib/routes/profiles/rename.help.ts +0 -5
  307. package/lib/routes/profiles/rename.ts +0 -17
  308. package/lib/routes/profiles/routes.ts +0 -18
  309. package/lib/routes/profiles/set.help.ts +0 -5
  310. package/lib/routes/profiles/set.ts +0 -32
  311. package/lib/routes/repos/add.help.ts +0 -6
  312. package/lib/routes/repos/add.ts +0 -20
  313. package/lib/routes/repos/group.help.ts +0 -11
  314. package/lib/routes/repos/group.ts +0 -18
  315. package/lib/routes/repos/remove.help.ts +0 -3
  316. package/lib/routes/repos/remove.ts +0 -17
  317. package/lib/routes/repos/rename.help.ts +0 -5
  318. package/lib/routes/repos/rename.ts +0 -17
  319. package/lib/routes/repos/routes.ts +0 -17
  320. package/lib/routes/repos/set.help.ts +0 -5
  321. package/lib/routes/repos/set.ts +0 -21
  322. package/lib/routes/repos/show.help.ts +0 -1
  323. package/lib/routes/repos/show.ts +0 -19
  324. package/lib/routes/request/discord-help.ts +0 -9
  325. package/lib/routes/request/discord.help.ts +0 -19
  326. package/lib/routes/request/discord.ts +0 -65
  327. package/lib/routes/request/group.help.ts +0 -15
  328. package/lib/routes/request/group.ts +0 -9
  329. package/lib/routes/request/routes.ts +0 -14
  330. package/lib/routes/request/slack-help.ts +0 -9
  331. package/lib/routes/request/slack.help.ts +0 -19
  332. package/lib/routes/request/slack.ts +0 -61
  333. package/lib/routes/status/routes.ts +0 -4
  334. package/lib/routes/status/status.help.ts +0 -6
  335. package/lib/routes/status/status.ts +0 -77
  336. package/lib/routes/update/routes.ts +0 -4
  337. package/lib/routes/update/update.help.ts +0 -5
  338. package/lib/routes/update/update.ts +0 -21
  339. package/lib/routes.ts +0 -40
  340. /package/lib/{factory.ts → cli/factory.ts} +0 -0
  341. /package/lib/{modules → cli}/router/query-to-cli-args.ts +0 -0
  342. /package/lib/{modules → cli}/router/validator.ts +0 -0
  343. /package/lib/{modules/connectors/funnel-connector-adapter.ts → connectors/connector-adapter.ts} +0 -0
  344. /package/lib/{modules/connectors/funnel-discord-event-processor.ts → connectors/discord-event-processor.ts} +0 -0
  345. /package/lib/{modules/http/funnel-http-client.ts → engine/http/http-client.ts} +0 -0
  346. /package/lib/{modules/id/funnel-id-generator.ts → engine/id/id-generator.ts} +0 -0
  347. /package/lib/{modules/logger/funnel-logger.ts → engine/logger/logger.ts} +0 -0
  348. /package/lib/{modules/process/funnel-process-runner.ts → engine/process/process-runner.ts} +0 -0
  349. /package/lib/{modules/time/funnel-clock.ts → engine/time/clock.ts} +0 -0
@@ -0,0 +1,108 @@
1
+ /** @jsxImportSource @opentui/react */
2
+ import { AddRow } from "@/tui/components/add-row"
3
+ import { Card } from "@/tui/components/card"
4
+ import { EditableField } from "@/tui/components/editable-field"
5
+ import { EmptyState } from "@/tui/components/empty-state"
6
+ import { PanelHeader } from "@/tui/components/panel-header"
7
+ import { ReadonlyField } from "@/tui/components/readonly-field"
8
+ import { ViewShell } from "@/tui/components/view-shell"
9
+ import type { Snapshot } from "@/tui/types"
10
+ import { uniqueName } from "@/tui/unique-name"
11
+ import type { Funnel } from "@/funnel"
12
+
13
+ type Props = {
14
+ snapshot: Snapshot
15
+ funnel: Funnel
16
+ refresh: () => void
17
+ focusedKey: string | null
18
+ setFocusedKey: (key: string | null) => void
19
+ }
20
+
21
+ type Channel = Snapshot["channels"][number]
22
+
23
+ const fieldKey = (name: string, field: string): string => `channels::${name}::${field}`
24
+
25
+ /**
26
+ * Channel inspector — one Card per channel. Connectors live nested inside the
27
+ * channel and are managed in the connectors view; here only the channel's
28
+ * name and id (read-only) are shown along with a count of nested connectors.
29
+ */
30
+ export function ChannelsView(props: Props) {
31
+ const channels = props.snapshot.channels
32
+
33
+ const commit = (channel: Channel, field: string, raw: string): void => {
34
+ try {
35
+ if (field === "name") {
36
+ const next = raw.trim()
37
+
38
+ if (next && next !== channel.name) props.funnel.channels.rename(channel.name, next)
39
+ }
40
+ } catch (error) {
41
+ props.funnel.logger.error(error instanceof Error ? error.message : String(error))
42
+ }
43
+
44
+ props.setFocusedKey(null)
45
+ props.refresh()
46
+ }
47
+
48
+ const removeChannel = (name: string): void => {
49
+ try {
50
+ props.funnel.channels.remove(name)
51
+ } catch (error) {
52
+ props.funnel.logger.error(error instanceof Error ? error.message : String(error))
53
+ }
54
+
55
+ props.setFocusedKey(null)
56
+ props.refresh()
57
+ }
58
+
59
+ const addChannel = (): void => {
60
+ const name = uniqueName(
61
+ channels.map((c) => c.name),
62
+ "channel",
63
+ )
64
+
65
+ try {
66
+ const created = props.funnel.channels.add({ name })
67
+ props.setFocusedKey(fieldKey(created.name, "name"))
68
+ } catch (error) {
69
+ props.funnel.logger.error(error instanceof Error ? error.message : String(error))
70
+ }
71
+
72
+ props.refresh()
73
+ }
74
+
75
+ return (
76
+ <ViewShell>
77
+ <PanelHeader label="channels" count={channels.length} />
78
+
79
+ {channels.length === 0 ? (
80
+ <EmptyState message="(none — use the button below to add one)" />
81
+ ) : (
82
+ channels.map((channel) => (
83
+ <Card key={channel.id} title={channel.name} onDelete={() => removeChannel(channel.name)}>
84
+ <EditableField
85
+ label="name"
86
+ initialValue={channel.name}
87
+ focused={props.focusedKey === fieldKey(channel.name, "name")}
88
+ onFocus={() => props.setFocusedKey(fieldKey(channel.name, "name"))}
89
+ onCommit={(raw) => commit(channel, "name", raw)}
90
+ />
91
+ <ReadonlyField label="id" value={channel.id} />
92
+ <ReadonlyField label="delivery" value={channel.delivery} />
93
+ <ReadonlyField
94
+ label="connectors"
95
+ value={
96
+ channel.connectors.length > 0
97
+ ? channel.connectors.map((c) => `${c.name}:${c.type}`).join(", ")
98
+ : "(none)"
99
+ }
100
+ />
101
+ </Card>
102
+ ))
103
+ )}
104
+
105
+ <AddRow label="add channel" onClick={addChannel} />
106
+ </ViewShell>
107
+ )
108
+ }
@@ -0,0 +1,164 @@
1
+ import { AddRow } from "@/tui/components/add-row"
2
+ import { Card } from "@/tui/components/card"
3
+ import { EmptyState } from "@/tui/components/empty-state"
4
+ import { PanelHeader } from "@/tui/components/panel-header"
5
+ import { ReadonlyField } from "@/tui/components/readonly-field"
6
+ import { HasciiButton } from "@/tui/components/ui/hascii/button"
7
+ import { ViewShell } from "@/tui/components/view-shell"
8
+ import { funnel } from "@/tui/theme"
9
+ import type { Snapshot } from "@/tui/types"
10
+ import { uniqueName } from "@/tui/unique-name"
11
+ import type { Funnel } from "@/funnel"
12
+
13
+ type Props = {
14
+ snapshot: Snapshot
15
+ funnel: Funnel
16
+ refresh: () => void
17
+ focusedKey: string | null
18
+ setFocusedKey: (key: string | null) => void
19
+ }
20
+
21
+ type Connector = Snapshot["connectors"][number]
22
+ type ConnectorType = Connector["type"]
23
+
24
+ const formatTimestamp = (iso: string | undefined): string => {
25
+ if (!iso) return "—"
26
+
27
+ const d = new Date(iso)
28
+
29
+ if (Number.isNaN(d.getTime())) return "—"
30
+
31
+ const yyyy = d.getFullYear()
32
+ const mm = String(d.getMonth() + 1).padStart(2, "0")
33
+ const dd = String(d.getDate()).padStart(2, "0")
34
+ const hh = String(d.getHours()).padStart(2, "0")
35
+ const mn = String(d.getMinutes()).padStart(2, "0")
36
+
37
+ return `${yyyy}-${mm}-${dd} ${hh}:${mn}`
38
+ }
39
+
40
+ /**
41
+ * Channel-scoped connector inspector. Reads `funnel.channels.listAllConnectors()`
42
+ * (already flattened with channelName / channelId tags) and lets the user delete
43
+ * each connector or quickly add a new one to the first available channel via the
44
+ * AddRow buttons. Editing values is intentionally read-only — token / pollInterval
45
+ * mutation belongs to `fnl channels <ch> connectors set <conn> ...` because the
46
+ * same connector name can exist in multiple channels and inline edits would have
47
+ * to disambiguate.
48
+ */
49
+ export function ConnectorsView(props: Props) {
50
+ const connectors = props.snapshot.connectors
51
+ const channels = props.snapshot.channels
52
+ const targetChannel = channels[0] ?? null
53
+
54
+ const logError = (error: unknown): void => {
55
+ props.funnel.logger.error(error instanceof Error ? error.message : String(error))
56
+ }
57
+
58
+ const removeConnector = (connector: Connector): void => {
59
+ props.funnel.listeners.stop(connector.channelName, connector.name).catch(logError)
60
+
61
+ try {
62
+ props.funnel.channels.removeConnector(connector.channelName, connector.name)
63
+ } catch (error) {
64
+ logError(error)
65
+ }
66
+
67
+ props.refresh()
68
+ }
69
+
70
+ const addConnector = (type: ConnectorType): void => {
71
+ if (!targetChannel) {
72
+ logError(new Error("add a channel first before creating a connector"))
73
+
74
+ return
75
+ }
76
+
77
+ const existingNames = connectors
78
+ .filter((c) => c.channelId === targetChannel.id)
79
+ .map((c) => c.name)
80
+ const name = uniqueName(existingNames, type)
81
+
82
+ try {
83
+ if (type === "slack") {
84
+ props.funnel.channels.addConnector(targetChannel.name, {
85
+ type: "slack",
86
+ name,
87
+ botToken: "xoxb-PLACEHOLDER",
88
+ appToken: "xapp-PLACEHOLDER",
89
+ })
90
+ } else if (type === "gh") {
91
+ props.funnel.channels.addConnector(targetChannel.name, { type: "gh", name })
92
+ } else if (type === "discord") {
93
+ props.funnel.channels.addConnector(targetChannel.name, {
94
+ type: "discord",
95
+ name,
96
+ botToken: "PLACEHOLDER-PLACEHOLDER",
97
+ })
98
+ } else {
99
+ props.funnel.channels.addConnector(targetChannel.name, { type: "schedule", name })
100
+ }
101
+
102
+ props.funnel.listeners.start(targetChannel.name, name).catch(logError)
103
+ } catch (error) {
104
+ logError(error)
105
+ }
106
+
107
+ props.refresh()
108
+ }
109
+
110
+ return (
111
+ <ViewShell>
112
+ <PanelHeader label="connectors" count={connectors.length} />
113
+
114
+ {connectors.length === 0 ? (
115
+ <EmptyState message="(none — add via the buttons below or `fnl channels <ch> connectors add ...`)" />
116
+ ) : (
117
+ connectors.map((connector) => (
118
+ <Card key={`${connector.channelId}::${connector.id}`} title={connector.name}>
119
+ <ReadonlyField label="channel" value={connector.channelName} />
120
+ <ReadonlyField label="type" value={connector.type} />
121
+ <ReadonlyField label="id" value={connector.id} />
122
+ {connector.type === "slack" ? (
123
+ <>
124
+ <ReadonlyField label="bot-token" value={connector.botToken} />
125
+ <ReadonlyField label="app-token" value={connector.appToken} />
126
+ </>
127
+ ) : null}
128
+ {connector.type === "gh" ? (
129
+ <ReadonlyField label="poll" value={String(connector.pollInterval ?? 60)} />
130
+ ) : null}
131
+ {connector.type === "discord" ? (
132
+ <ReadonlyField label="bot-token" value={connector.botToken} />
133
+ ) : null}
134
+ {connector.type === "schedule" ? (
135
+ <ReadonlyField label="entries" value={String(connector.entries.length)} />
136
+ ) : null}
137
+ <text fg={funnel.faint}>{`created ${formatTimestamp(connector.createdAt)}`}</text>
138
+ <box style={{ flexDirection: "row", justifyContent: "space-between" }}>
139
+ <text fg={funnel.faint}>{`updated ${formatTimestamp(connector.updatedAt)}`}</text>
140
+ <HasciiButton
141
+ variant="destructive"
142
+ size="sm"
143
+ onPress={() => removeConnector(connector)}
144
+ >
145
+ delete
146
+ </HasciiButton>
147
+ </box>
148
+ </Card>
149
+ ))
150
+ )}
151
+
152
+ {targetChannel ? (
153
+ <text fg={funnel.faint}>{`add target channel: ${targetChannel.name}`}</text>
154
+ ) : (
155
+ <text fg={funnel.warn}>add a channel first to enable the buttons below</text>
156
+ )}
157
+
158
+ <AddRow label="add slack" onClick={() => addConnector("slack")} />
159
+ <AddRow label="add gh" onClick={() => addConnector("gh")} />
160
+ <AddRow label="add discord" onClick={() => addConnector("discord")} />
161
+ <AddRow label="add schedule" onClick={() => addConnector("schedule")} />
162
+ </ViewShell>
163
+ )
164
+ }
@@ -0,0 +1,160 @@
1
+ /** @jsxImportSource @opentui/react */
2
+ import { DetailBar } from "@/tui/components/detail-bar"
3
+ import { HasciiSeparator } from "@/tui/components/ui/hascii/separator"
4
+ import { EmptyState } from "@/tui/components/empty-state"
5
+ import { Keymap } from "@/tui/components/keymap"
6
+ import { PanelHeader } from "@/tui/components/panel-header"
7
+ import { ViewShell } from "@/tui/components/view-shell"
8
+ import { funnel } from "@/tui/theme"
9
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
10
+ import type { StreamEvent, StreamStatus } from "@/tui/types"
11
+
12
+ type Props = {
13
+ events: StreamEvent[]
14
+ filter: string
15
+ selectedIndex: number
16
+ streamStatus: StreamStatus
17
+ }
18
+
19
+ const streamLabel = (status: StreamStatus): string => {
20
+ if (status === "open") return "live"
21
+ if (status === "connecting") return "connecting…"
22
+ if (status === "closed") return "reconnecting…"
23
+
24
+ return "offline"
25
+ }
26
+
27
+ const formatTime = (ms: number): string => {
28
+ const date = new Date(ms)
29
+ const hh = String(date.getHours()).padStart(2, "0")
30
+ const mm = String(date.getMinutes()).padStart(2, "0")
31
+ const ss = String(date.getSeconds()).padStart(2, "0")
32
+
33
+ return `${hh}:${mm}:${ss}`
34
+ }
35
+
36
+ const truncate = (value: string, max: number): string => {
37
+ const flat = value.replace(/\s+/g, " ").trim()
38
+
39
+ if (flat.length <= max) return flat
40
+
41
+ return `${flat.slice(0, max - 1)}…`
42
+ }
43
+
44
+ const matches = (event: StreamEvent, filter: string): boolean => {
45
+ if (!filter) return true
46
+
47
+ const needle = filter.toLowerCase()
48
+ const haystack = [
49
+ event.content,
50
+ event.meta.connector ?? "",
51
+ event.meta.event_type ?? "",
52
+ event.meta.channel ?? "",
53
+ ]
54
+ .join(" ")
55
+ .toLowerCase()
56
+
57
+ return haystack.includes(needle)
58
+ }
59
+
60
+ const tryParseJson = (value: string): unknown => {
61
+ try {
62
+ return JSON.parse(value)
63
+ } catch {
64
+ return value
65
+ }
66
+ }
67
+
68
+ const formatJson = (value: unknown): string => {
69
+ try {
70
+ return JSON.stringify(value, null, 2)
71
+ } catch {
72
+ return String(value)
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Live event stream + detail of the selected event.
78
+ *
79
+ * The events list lives inside `ViewShell` (padded canvas) while the
80
+ * detail strip is a sibling `DetailBar` so its background spans the
81
+ * full main column edge-to-edge and reads as a distinct elevated
82
+ * stratum below the list.
83
+ */
84
+ export function EventsView(props: Props) {
85
+ const theme = useHasciiTheme()
86
+ const visible = props.events.filter((event) => matches(event, props.filter))
87
+ const selected = visible[props.selectedIndex] ?? null
88
+
89
+ return (
90
+ <box style={{ flexDirection: "column", flexGrow: 1 }}>
91
+ <ViewShell>
92
+ <PanelHeader
93
+ label="events"
94
+ count={visible.length}
95
+ hint={[
96
+ streamLabel(props.streamStatus),
97
+ `${props.events.length} total`,
98
+ props.filter ? `/${props.filter}/` : null,
99
+ ]
100
+ .filter((part): part is string => part !== null)
101
+ .join(" · ")}
102
+ />
103
+
104
+ {visible.length === 0 ? (
105
+ <EmptyState message="(no events yet — waiting for the first one)" />
106
+ ) : (
107
+ visible.map((event, index) => {
108
+ const isSelected = index === props.selectedIndex
109
+ const connector = event.meta.connector ?? "system"
110
+ const eventType = event.meta.event_type ?? "?"
111
+
112
+ return (
113
+ <text key={event.id} bg={isSelected ? theme.color.muted : undefined}>
114
+ <span fg={theme.color.mutedForeground}>{formatTime(event.receivedAt)}</span>
115
+ <span fg={funnel.faint}> </span>
116
+ <span fg={theme.color.mutedForeground}>{eventType.padEnd(8)}</span>
117
+ <span fg={funnel.faint}>{" · "}</span>
118
+ <span fg={isSelected ? theme.color.foreground : theme.color.foreground}>
119
+ {connector.padEnd(14)}
120
+ </span>
121
+ <span fg={funnel.faint}> </span>
122
+ <span fg={isSelected ? theme.color.foreground : theme.color.mutedForeground}>
123
+ {truncate(event.content, 80)}
124
+ </span>
125
+ </text>
126
+ )
127
+ })
128
+ )}
129
+
130
+ <Keymap
131
+ hints={[
132
+ { key: "j/k", label: "select" },
133
+ { key: "/", label: "filter" },
134
+ ]}
135
+ />
136
+ </ViewShell>
137
+
138
+ <DetailBar>
139
+ <PanelHeader label="detail" />
140
+
141
+ {!selected ? (
142
+ <EmptyState message="(select an event with j/k to inspect)" />
143
+ ) : (
144
+ <>
145
+ <text>
146
+ <span fg={theme.color.mutedForeground}>meta: </span>
147
+ <span fg={theme.color.foreground}>
148
+ {Object.entries(selected.meta)
149
+ .map(([key, value]) => `${key}=${value}`)
150
+ .join(" ")}
151
+ </span>
152
+ </text>
153
+ <HasciiSeparator />
154
+ <text fg={theme.color.foreground}>{formatJson(tryParseJson(selected.content))}</text>
155
+ </>
156
+ )}
157
+ </DetailBar>
158
+ </box>
159
+ )
160
+ }
@@ -0,0 +1,80 @@
1
+ /** @jsxImportSource @opentui/react */
2
+ import { Card } from "@/tui/components/card"
3
+ import { EmptyState } from "@/tui/components/empty-state"
4
+ import { Keymap } from "@/tui/components/keymap"
5
+ import { PanelHeader } from "@/tui/components/panel-header"
6
+ import { ViewShell } from "@/tui/components/view-shell"
7
+ import { funnel } from "@/tui/theme"
8
+ import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
9
+ import type { Snapshot, StreamEvent } from "@/tui/types"
10
+
11
+ type Props = {
12
+ snapshot: Snapshot
13
+ events: StreamEvent[]
14
+ selectedIndex: number
15
+ busy: boolean
16
+ }
17
+
18
+ const eventCountBy = (events: StreamEvent[], connectorName: string): number => {
19
+ let count = 0
20
+
21
+ for (const event of events) {
22
+ if (event.meta.connector === connectorName) count += 1
23
+ }
24
+
25
+ return count
26
+ }
27
+
28
+ /**
29
+ * Listener registry — one Card per listener. The Card title shows the
30
+ * listener's name; inside, a single status line carries the alive
31
+ * dot, the connector type, and the event count. Cursor selection is
32
+ * shown via the Card's `selected` accent. Listeners are runtime
33
+ * entities derived from connectors, so there is no add path here —
34
+ * register / remove a connector instead.
35
+ */
36
+ export function ListenersView(props: Props) {
37
+ const theme = useHasciiTheme()
38
+ const listeners = props.snapshot.listeners
39
+
40
+ return (
41
+ <ViewShell>
42
+ <PanelHeader
43
+ label="listeners"
44
+ count={listeners.length}
45
+ hint={props.busy ? "working…" : undefined}
46
+ />
47
+
48
+ {!props.snapshot.daemonReachable ? (
49
+ <EmptyState message="(gateway daemon offline — press G to start it)" />
50
+ ) : listeners.length === 0 ? (
51
+ <EmptyState message="(no listeners — register a connector first)" />
52
+ ) : (
53
+ listeners.map((entry, index) => {
54
+ const aliveColor = entry.alive ? funnel.alive : funnel.dead
55
+ const count = eventCountBy(props.events, entry.name)
56
+
57
+ return (
58
+ <Card key={entry.name} title={entry.name} selected={index === props.selectedIndex}>
59
+ <text>
60
+ <span fg={aliveColor}>{entry.alive ? "●" : "○"}</span>
61
+ <span fg={funnel.faint}> </span>
62
+ <span fg={theme.color.mutedForeground}>{entry.type}</span>
63
+ {count > 0 ? <span fg={theme.color.mutedForeground}>{` ${count}↓`}</span> : null}
64
+ </text>
65
+ </Card>
66
+ )
67
+ })
68
+ )}
69
+
70
+ <Keymap
71
+ hints={[
72
+ { key: "j/k", label: "select" },
73
+ { key: "s", label: "start" },
74
+ { key: "x", label: "stop" },
75
+ { key: "R", label: "restart" },
76
+ ]}
77
+ />
78
+ </ViewShell>
79
+ )
80
+ }
@@ -0,0 +1,152 @@
1
+ /** @jsxImportSource @opentui/react */
2
+ import { AddRow } from "@/tui/components/add-row"
3
+ import { Card } from "@/tui/components/card"
4
+ import { EditableField } from "@/tui/components/editable-field"
5
+ import { EmptyState } from "@/tui/components/empty-state"
6
+ import { Keymap } from "@/tui/components/keymap"
7
+ import { PanelHeader } from "@/tui/components/panel-header"
8
+ import { ViewShell } from "@/tui/components/view-shell"
9
+ import type { Snapshot } from "@/tui/types"
10
+ import { uniqueName } from "@/tui/unique-name"
11
+ import type { Funnel } from "@/funnel"
12
+
13
+ type Props = {
14
+ snapshot: Snapshot
15
+ selectedIndex: number
16
+ funnel: Funnel
17
+ refresh: () => void
18
+ focusedKey: string | null
19
+ setFocusedKey: (key: string | null) => void
20
+ }
21
+
22
+ type Profile = Snapshot["profiles"][number]
23
+
24
+ const fieldKey = (name: string, field: string): string => `profiles::${name}::${field}`
25
+
26
+ /**
27
+ * Profile list — one Card per profile. Selection (j/k cursor) shows the
28
+ * `▏` primary rule via the Card's `selected` prop; pressing `c`
29
+ * launches Claude Code with the selected profile.
30
+ *
31
+ * `+ add profile` at the foot creates a new profile pointed at the
32
+ * first existing channel (or an empty string if there are none, which
33
+ * the user must then edit before launching).
34
+ */
35
+ export function ProfilesView(props: Props) {
36
+ const profiles = props.snapshot.profiles
37
+ const channels = props.snapshot.channels
38
+
39
+ const commit = (profile: Profile, field: string, raw: string): void => {
40
+ try {
41
+ if (field === "name") {
42
+ const next = raw.trim()
43
+
44
+ if (next && next !== profile.name) props.funnel.profiles.rename(profile.name, next)
45
+ } else if (field === "channel") {
46
+ const next = raw.trim()
47
+
48
+ if (next) props.funnel.profiles.update(profile.name, { channelId: next })
49
+ } else if (field === "path") {
50
+ const next = raw.trim()
51
+
52
+ if (next) props.funnel.profiles.update(profile.name, { path: next })
53
+ } else if (field === "sub-agent") {
54
+ const next = raw.trim()
55
+
56
+ if (next) props.funnel.profiles.update(profile.name, { subAgent: next })
57
+ }
58
+ } catch (error) {
59
+ props.funnel.logger.error(error instanceof Error ? error.message : String(error))
60
+ }
61
+
62
+ props.setFocusedKey(null)
63
+ props.refresh()
64
+ }
65
+
66
+ const removeProfile = (name: string): void => {
67
+ try {
68
+ props.funnel.profiles.remove(name)
69
+ } catch (error) {
70
+ props.funnel.logger.error(error instanceof Error ? error.message : String(error))
71
+ }
72
+
73
+ props.setFocusedKey(null)
74
+ props.refresh()
75
+ }
76
+
77
+ const addProfile = (): void => {
78
+ const name = uniqueName(
79
+ profiles.map((p) => p.name),
80
+ "profile",
81
+ )
82
+ const channelId = channels[0]?.name ?? ""
83
+
84
+ try {
85
+ props.funnel.profiles.add({ name, path: "", subAgent: "", channelId })
86
+ props.setFocusedKey(fieldKey(name, "name"))
87
+ } catch (error) {
88
+ props.funnel.logger.error(error instanceof Error ? error.message : String(error))
89
+ }
90
+
91
+ props.refresh()
92
+ }
93
+
94
+ return (
95
+ <ViewShell>
96
+ <PanelHeader label="profiles" count={profiles.length} />
97
+
98
+ {profiles.length === 0 ? (
99
+ <EmptyState message="(none — use the button below to add one)" />
100
+ ) : (
101
+ profiles.map((profile, index) => (
102
+ <Card
103
+ key={profile.name}
104
+ title={profile.name}
105
+ selected={index === props.selectedIndex}
106
+ onDelete={() => removeProfile(profile.name)}
107
+ >
108
+ <EditableField
109
+ label="name"
110
+ initialValue={profile.name}
111
+ focused={props.focusedKey === fieldKey(profile.name, "name")}
112
+ onFocus={() => props.setFocusedKey(fieldKey(profile.name, "name"))}
113
+ onCommit={(raw) => commit(profile, "name", raw)}
114
+ />
115
+ <EditableField
116
+ label="path"
117
+ initialValue={profile.path}
118
+ focused={props.focusedKey === fieldKey(profile.name, "path")}
119
+ onFocus={() => props.setFocusedKey(fieldKey(profile.name, "path"))}
120
+ onCommit={(raw) => commit(profile, "path", raw)}
121
+ placeholder="repository path"
122
+ />
123
+ <EditableField
124
+ label="sub-agent"
125
+ initialValue={profile.subAgent}
126
+ focused={props.focusedKey === fieldKey(profile.name, "sub-agent")}
127
+ onFocus={() => props.setFocusedKey(fieldKey(profile.name, "sub-agent"))}
128
+ onCommit={(raw) => commit(profile, "sub-agent", raw)}
129
+ placeholder="claude --agent value"
130
+ />
131
+ <EditableField
132
+ label="channel"
133
+ initialValue={profile.channelId}
134
+ focused={props.focusedKey === fieldKey(profile.name, "channel")}
135
+ onFocus={() => props.setFocusedKey(fieldKey(profile.name, "channel"))}
136
+ onCommit={(raw) => commit(profile, "channel", raw)}
137
+ />
138
+ </Card>
139
+ ))
140
+ )}
141
+
142
+ <AddRow label="add profile" onClick={addProfile} />
143
+
144
+ <Keymap
145
+ hints={[
146
+ { key: "j/k", label: "select" },
147
+ { key: "c", label: "launch" },
148
+ ]}
149
+ />
150
+ </ViewShell>
151
+ )
152
+ }