@interactive-inc/claude-funnel 0.10.0 → 0.15.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 (236) hide show
  1. package/README.md +106 -56
  2. package/dist/bin.js +557 -530
  3. package/dist/connectors/schedule.d.ts +2 -49
  4. package/dist/connectors/schedule.js +1 -1
  5. package/dist/connectors/slack.d.ts +4 -48
  6. package/dist/connectors/slack.js +1 -1
  7. package/dist/gateway/daemon.js +213 -211
  8. package/dist/index.d.ts +465 -173
  9. package/dist/index.js +692 -154
  10. package/dist/{schedule-connector-schema-CkuIQ0JQ.js → schedule-connector-schema-FxP7LPlx.js} +11 -0
  11. package/dist/{file-system-Co60LrmR.d.ts → schedule-listener-BPodvbld.d.ts} +56 -1
  12. package/dist/{slack-connector-schema-Cd22WiHB.js → slack-connector-schema-B4hsf3AY.js} +10 -1
  13. package/dist/slack-listener-CHj6uMY-.d.ts +74 -0
  14. package/package.json +2 -6
  15. package/schemas/funnel.schema.json +144 -0
  16. package/dist/slack-connector-schema-D7zAHN8k.d.ts +0 -15
  17. package/lib/bin.ts +0 -3
  18. package/lib/cli/factory.ts +0 -10
  19. package/lib/cli/index.ts +0 -85
  20. package/lib/cli/router/query-to-cli-args.ts +0 -20
  21. package/lib/cli/router/to-request.ts +0 -113
  22. package/lib/cli/router/validator.ts +0 -27
  23. package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +0 -27
  24. package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +0 -40
  25. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +0 -41
  26. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +0 -22
  27. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +0 -23
  28. package/lib/cli/routes/channels.$channel.connectors.$connector.ts +0 -26
  29. package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +0 -92
  30. package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +0 -22
  31. package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +0 -63
  32. package/lib/cli/routes/channels.$channel.connectors.ts +0 -26
  33. package/lib/cli/routes/channels.$channel.publish.ts +0 -52
  34. package/lib/cli/routes/channels.$channel.rename.$newName.ts +0 -22
  35. package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +0 -34
  36. package/lib/cli/routes/channels.$channel.ts +0 -34
  37. package/lib/cli/routes/channels.add.$channel.ts +0 -33
  38. package/lib/cli/routes/channels.remove.$channel.ts +0 -20
  39. package/lib/cli/routes/channels.ts +0 -39
  40. package/lib/cli/routes/claude.ts +0 -70
  41. package/lib/cli/routes/gateway.listeners.ts +0 -41
  42. package/lib/cli/routes/gateway.logs.ts +0 -123
  43. package/lib/cli/routes/gateway.restart.ts +0 -50
  44. package/lib/cli/routes/gateway.run.ts +0 -41
  45. package/lib/cli/routes/gateway.start.ts +0 -50
  46. package/lib/cli/routes/gateway.status.ts +0 -19
  47. package/lib/cli/routes/gateway.stop.ts +0 -32
  48. package/lib/cli/routes/gateway.ts +0 -55
  49. package/lib/cli/routes/index.ts +0 -219
  50. package/lib/cli/routes/profiles.$profile.as-default.ts +0 -22
  51. package/lib/cli/routes/profiles.$profile.rename.$newName.ts +0 -22
  52. package/lib/cli/routes/profiles.$profile.run.ts +0 -36
  53. package/lib/cli/routes/profiles.add.$profile.ts +0 -49
  54. package/lib/cli/routes/profiles.remove.$profile.ts +0 -20
  55. package/lib/cli/routes/profiles.set.$profile.ts +0 -45
  56. package/lib/cli/routes/profiles.ts +0 -40
  57. package/lib/cli/routes/status.ts +0 -93
  58. package/lib/cli/routes/update.ts +0 -27
  59. package/lib/connectors/connector-adapter.ts +0 -9
  60. package/lib/connectors/connector-config-schema.ts +0 -16
  61. package/lib/connectors/connector-factory.ts +0 -94
  62. package/lib/connectors/connector-listener.ts +0 -20
  63. package/lib/connectors/discord-adapter.ts +0 -51
  64. package/lib/connectors/discord-connector-schema.ts +0 -12
  65. package/lib/connectors/discord-event-processor.ts +0 -48
  66. package/lib/connectors/discord-listener.ts +0 -111
  67. package/lib/connectors/discord.ts +0 -4
  68. package/lib/connectors/gh-adapter.ts +0 -48
  69. package/lib/connectors/gh-connector-schema.ts +0 -12
  70. package/lib/connectors/gh-listener.ts +0 -137
  71. package/lib/connectors/gh.ts +0 -3
  72. package/lib/connectors/match-cron.ts +0 -78
  73. package/lib/connectors/schedule-connector-schema.ts +0 -33
  74. package/lib/connectors/schedule-listener.ts +0 -207
  75. package/lib/connectors/schedule-state-store.ts +0 -54
  76. package/lib/connectors/schedule.ts +0 -4
  77. package/lib/connectors/slack-adapter.ts +0 -36
  78. package/lib/connectors/slack-connector-schema.ts +0 -13
  79. package/lib/connectors/slack-event-processor.ts +0 -97
  80. package/lib/connectors/slack-listener.ts +0 -97
  81. package/lib/connectors/slack.ts +0 -4
  82. package/lib/engine/channels/channels.ts +0 -520
  83. package/lib/engine/claude/claude.ts +0 -205
  84. package/lib/engine/claude/gateway-controller.ts +0 -4
  85. package/lib/engine/fs/file-system.ts +0 -23
  86. package/lib/engine/fs/memory-file-system.ts +0 -102
  87. package/lib/engine/fs/node-file-system.ts +0 -68
  88. package/lib/engine/http/http-client.ts +0 -17
  89. package/lib/engine/http/memory-http-client.ts +0 -36
  90. package/lib/engine/http/node-http-client.ts +0 -23
  91. package/lib/engine/id/id-generator.ts +0 -7
  92. package/lib/engine/id/memory-id-generator.ts +0 -20
  93. package/lib/engine/id/node-id-generator.ts +0 -7
  94. package/lib/engine/logger/logger.ts +0 -11
  95. package/lib/engine/logger/memory-logger.ts +0 -28
  96. package/lib/engine/logger/node-logger.ts +0 -49
  97. package/lib/engine/logger/noop-logger.ts +0 -9
  98. package/lib/engine/mcp/channel-server.ts +0 -123
  99. package/lib/engine/mcp/channel-subscriber.ts +0 -82
  100. package/lib/engine/mcp/mcp.ts +0 -126
  101. package/lib/engine/mcp/read-channel-connectors.ts +0 -34
  102. package/lib/engine/mcp/read-gateway-token.ts +0 -16
  103. package/lib/engine/mcp/usage-hint-for-type.ts +0 -15
  104. package/lib/engine/process/memory-process-runner.ts +0 -88
  105. package/lib/engine/process/node-process-runner.ts +0 -91
  106. package/lib/engine/process/process-runner.ts +0 -33
  107. package/lib/engine/profiles/profile-channel-checker.ts +0 -7
  108. package/lib/engine/profiles/profiles.ts +0 -126
  109. package/lib/engine/settings/mock-settings-reader.ts +0 -27
  110. package/lib/engine/settings/settings-reader.ts +0 -6
  111. package/lib/engine/settings/settings-schema.ts +0 -48
  112. package/lib/engine/settings/settings-store.ts +0 -110
  113. package/lib/engine/time/clock.ts +0 -15
  114. package/lib/engine/time/memory-clock.ts +0 -26
  115. package/lib/engine/time/node-clock.ts +0 -7
  116. package/lib/funnel.ts +0 -294
  117. package/lib/gateway/auth-middleware.ts +0 -44
  118. package/lib/gateway/broadcaster.ts +0 -319
  119. package/lib/gateway/channel-publisher.ts +0 -67
  120. package/lib/gateway/daemon.ts +0 -47
  121. package/lib/gateway/factory.ts +0 -10
  122. package/lib/gateway/funnel-event-store.ts +0 -155
  123. package/lib/gateway/gateway-server.ts +0 -426
  124. package/lib/gateway/gateway-token.ts +0 -79
  125. package/lib/gateway/gateway.ts +0 -209
  126. package/lib/gateway/kill-competing-slack-gateways.ts +0 -56
  127. package/lib/gateway/listener-supervisor.ts +0 -339
  128. package/lib/gateway/listeners-client.ts +0 -128
  129. package/lib/gateway/publish-schema.ts +0 -27
  130. package/lib/gateway/resolve-daemon-script.ts +0 -26
  131. package/lib/gateway/routes/channels.connectors.call.ts +0 -39
  132. package/lib/gateway/routes/channels.publish.ts +0 -44
  133. package/lib/gateway/routes/health.ts +0 -13
  134. package/lib/gateway/routes/index.ts +0 -26
  135. package/lib/gateway/routes/listeners.list.ts +0 -6
  136. package/lib/gateway/routes/listeners.restart.ts +0 -15
  137. package/lib/gateway/routes/listeners.start.ts +0 -15
  138. package/lib/gateway/routes/listeners.stop.ts +0 -15
  139. package/lib/gateway/routes/route-deps.ts +0 -19
  140. package/lib/gateway/routes/status.ts +0 -15
  141. package/lib/gateway/routes/validator.ts +0 -17
  142. package/lib/index.ts +0 -67
  143. package/lib/logger/leuco-human-file-writer.ts +0 -65
  144. package/lib/logger/leuco-human-logger.ts +0 -98
  145. package/lib/logger/leuco-human-record.ts +0 -16
  146. package/lib/logger/leuco-human-stdout-writer.ts +0 -26
  147. package/lib/logger/leuco-human-writer.ts +0 -14
  148. package/lib/logger/leuco-logger-memory-sink.ts +0 -67
  149. package/lib/logger/leuco-logger-record.ts +0 -13
  150. package/lib/logger/leuco-logger-sink.ts +0 -33
  151. package/lib/logger/leuco-logger-sqlite-sink.ts +0 -355
  152. package/lib/logger/leuco-logger.ts +0 -135
  153. package/lib/tui/app.tsx +0 -357
  154. package/lib/tui/components/add-row.tsx +0 -18
  155. package/lib/tui/components/brand.tsx +0 -27
  156. package/lib/tui/components/card.tsx +0 -44
  157. package/lib/tui/components/detail-bar.tsx +0 -46
  158. package/lib/tui/components/editable-field.tsx +0 -33
  159. package/lib/tui/components/empty-state.tsx +0 -11
  160. package/lib/tui/components/gateway-status.tsx +0 -66
  161. package/lib/tui/components/keymap.tsx +0 -29
  162. package/lib/tui/components/menu-item.tsx +0 -73
  163. package/lib/tui/components/menu.tsx +0 -26
  164. package/lib/tui/components/panel-header.tsx +0 -22
  165. package/lib/tui/components/readonly-field.tsx +0 -18
  166. package/lib/tui/components/section-header.tsx +0 -25
  167. package/lib/tui/components/selection-accent.tsx +0 -32
  168. package/lib/tui/components/session-item.tsx +0 -33
  169. package/lib/tui/components/session-list.tsx +0 -33
  170. package/lib/tui/components/ui/hascii/accordion-item.tsx +0 -88
  171. package/lib/tui/components/ui/hascii/accordion.tsx +0 -96
  172. package/lib/tui/components/ui/hascii/alert-dialog.tsx +0 -43
  173. package/lib/tui/components/ui/hascii/badge.tsx +0 -51
  174. package/lib/tui/components/ui/hascii/breadcrumb.tsx +0 -58
  175. package/lib/tui/components/ui/hascii/button.tsx +0 -194
  176. package/lib/tui/components/ui/hascii/card-content.tsx +0 -14
  177. package/lib/tui/components/ui/hascii/card-description.tsx +0 -13
  178. package/lib/tui/components/ui/hascii/card-footer.tsx +0 -14
  179. package/lib/tui/components/ui/hascii/card-header.tsx +0 -14
  180. package/lib/tui/components/ui/hascii/card-title.tsx +0 -13
  181. package/lib/tui/components/ui/hascii/card.tsx +0 -27
  182. package/lib/tui/components/ui/hascii/checkbox.tsx +0 -65
  183. package/lib/tui/components/ui/hascii/command.tsx +0 -159
  184. package/lib/tui/components/ui/hascii/dialog-content.tsx +0 -14
  185. package/lib/tui/components/ui/hascii/dialog-description.tsx +0 -13
  186. package/lib/tui/components/ui/hascii/dialog-footer.tsx +0 -14
  187. package/lib/tui/components/ui/hascii/dialog-header.tsx +0 -14
  188. package/lib/tui/components/ui/hascii/dialog-title.tsx +0 -13
  189. package/lib/tui/components/ui/hascii/dialog.tsx +0 -27
  190. package/lib/tui/components/ui/hascii/file-tree.tsx +0 -142
  191. package/lib/tui/components/ui/hascii/focus-group.tsx +0 -62
  192. package/lib/tui/components/ui/hascii/form-item.tsx +0 -43
  193. package/lib/tui/components/ui/hascii/input-otp.tsx +0 -86
  194. package/lib/tui/components/ui/hascii/input.tsx +0 -130
  195. package/lib/tui/components/ui/hascii/pagination.tsx +0 -105
  196. package/lib/tui/components/ui/hascii/progress.tsx +0 -28
  197. package/lib/tui/components/ui/hascii/select.tsx +0 -131
  198. package/lib/tui/components/ui/hascii/separator.tsx +0 -35
  199. package/lib/tui/components/ui/hascii/sidebar-content.tsx +0 -23
  200. package/lib/tui/components/ui/hascii/sidebar-header.tsx +0 -14
  201. package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +0 -67
  202. package/lib/tui/components/ui/hascii/sidebar.tsx +0 -24
  203. package/lib/tui/components/ui/hascii/skeleton.tsx +0 -60
  204. package/lib/tui/components/ui/hascii/slider.tsx +0 -91
  205. package/lib/tui/components/ui/hascii/snackbar.tsx +0 -75
  206. package/lib/tui/components/ui/hascii/sparkline.tsx +0 -53
  207. package/lib/tui/components/ui/hascii/spinner.tsx +0 -47
  208. package/lib/tui/components/ui/hascii/stepper.tsx +0 -54
  209. package/lib/tui/components/ui/hascii/switch.tsx +0 -66
  210. package/lib/tui/components/ui/hascii/table.tsx +0 -95
  211. package/lib/tui/components/ui/hascii/tabs.tsx +0 -59
  212. package/lib/tui/components/ui/hascii/toggle-group-item.tsx +0 -45
  213. package/lib/tui/components/ui/hascii/toggle-group.tsx +0 -99
  214. package/lib/tui/components/ui/hascii/tree.tsx +0 -104
  215. package/lib/tui/components/view-shell.tsx +0 -44
  216. package/lib/tui/filter-input.tsx +0 -33
  217. package/lib/tui/hooks/hascii/use-pressable.ts +0 -54
  218. package/lib/tui/parse-comma-list.ts +0 -14
  219. package/lib/tui/profile-launcher.tsx +0 -61
  220. package/lib/tui/scrollbar-options.ts +0 -19
  221. package/lib/tui/sidebar.tsx +0 -50
  222. package/lib/tui/theme.ts +0 -40
  223. package/lib/tui/tui.tsx +0 -20
  224. package/lib/tui/types.ts +0 -38
  225. package/lib/tui/unique-name.ts +0 -18
  226. package/lib/tui/use-event-stream.ts +0 -133
  227. package/lib/tui/use-snapshot.ts +0 -99
  228. package/lib/tui/utils/hascii/form-item-context.tsx +0 -23
  229. package/lib/tui/utils/hascii/input-focus-context.tsx +0 -31
  230. package/lib/tui/utils/hascii/theme-context.tsx +0 -26
  231. package/lib/tui/utils/hascii/theme.ts +0 -176
  232. package/lib/tui/views/channels-view.tsx +0 -108
  233. package/lib/tui/views/connectors-view.tsx +0 -164
  234. package/lib/tui/views/events-view.tsx +0 -160
  235. package/lib/tui/views/listeners-view.tsx +0 -80
  236. package/lib/tui/views/profiles-view.tsx +0 -152
package/README.md CHANGED
@@ -1,56 +1,56 @@
1
1
  [![npm](https://img.shields.io/npm/v/@interactive-inc/claude-funnel.svg)](https://www.npmjs.com/package/@interactive-inc/claude-funnel)
2
2
  [![license](https://img.shields.io/npm/l/@interactive-inc/claude-funnel.svg)](./LICENSE)
3
3
 
4
- A hub CLI that connects multiple Claude Code agents to external services (Slack / GitHub / Discord) and time-based triggers (cron). External events flow into subscription "channels" and arrive at Claude Code over MCP. Outbound API calls from Claude go back through the same connectors as MCP tools, so replying to a Slack thread or commenting on a GitHub issue does not need a bash subshell.
4
+ A hub for AI coding agents. One long-running daemon owns all external connections; agents subscribe to named channels and react to events without you wiring up shell scripts and cron entries. Outbound replies travel back through the same connectors as MCP tools, so answering a message or commenting on an issue does not need a bash subshell.
5
5
 
6
- The command is `funnel` or its shorthand `fnl`.
6
+ The command is `funnel` (or the shorthand `fnl`).
7
7
 
8
- ## Why funnel
8
+ Connectors today: Slack (Socket Mode), GitHub (poll via `gh`), Discord (Gateway), and cron schedules. Built around Claude Code; the architecture is agent-agnostic.
9
9
 
10
- A single Claude Code session is great at one repo at one moment. The moment you want it to react to things — a Slack mention, a new GitHub issue, a 9 AM standup — you end up gluing shell scripts, cron entries and `bash -c "claude ..."` together, and there is no single place that says "who is listening to what, and who is allowed to reply where."
10
+ ## Why funnel
11
11
 
12
- `funnel` is that place. You configure named subscription boxes (channels), attach connectors to them, launch Claude with a channel binding, and the daemon does the rest:
12
+ A single agent session is great at one repository at one moment. The moment you want it to react to things — a chat mention, a new issue, a 9 AM standup — you end up gluing shell scripts, cron entries, and `bash -c "agent ..."` invocations together. There is no single place that says "who is listening to what, and who is allowed to reply where."
13
13
 
14
- - The gateway daemon owns the external connections. Slack Socket Mode, the Discord Gateway, GitHub polling they connect once, from the daemon, no matter how many Claude sessions you start. Launching a second Claude does not open a second Slack socket; both sessions just subscribe to the same channel and the daemon routes events to them
15
- - Inbound events arrive as MCP notifications, so Claude reacts in the same session it is already running in
16
- - Outbound replies use MCP tools per connector, so they are essentially synchronous (no bash, no CLI cold start)
17
- - Listeners are supervised with health checks and auto-restart, so a flaky Slack connection or a crashed poller recovers on its own
18
- - Multiple Claudes can share the same channel (`fanout`) or compete for events as workers (`exclusive`) — the daemon decides who gets each event
14
+ funnel is that place. Declare named subscription boxes (channels), attach connectors to them, launch the agent with a channel binding, and the daemon handles the rest:
19
15
 
20
- If you have ever wanted "Slack-driven Claude" or "cron-driven Claude" without writing a dispatcher, this is it.
16
+ - The daemon owns the external connections. Each one connects once, no matter how many agent sessions you start. A second agent does not open a second socket; both sessions subscribe to the same channel and the daemon fans events out.
17
+ - Inbound events arrive as MCP notifications, so the agent reacts in the session it is already running in.
18
+ - Outbound replies use MCP tools per connector — essentially synchronous (no bash, no CLI cold start).
19
+ - Listeners are supervised with health checks and automatic restart; a flaky connection or crashed poller recovers on its own.
20
+ - Multiple agents can share a channel (`fanout`) or compete for events as workers (`exclusive`) — the daemon decides who gets each event.
21
21
 
22
22
  ## Concepts
23
23
 
24
24
  ```
25
- External sources Outbound calls
26
- (Slack / GitHub / Discord / cron) (Claude → MCP tools per connector)
27
-
28
-
29
- Channels (with nested per-type connectors)
30
-
31
- ▼ WebSocket
32
- Gateway daemon
33
- (port 9742: WS /ws + listener supervisor + reply API)
34
-
35
- ▼ MCP (stdio)
36
- Claude Code
25
+ external sources outbound replies
26
+ (chat / source-control / cron) (MCP tools per connector)
27
+
28
+
29
+ daemon (port 9742)
30
+ routes events into channels
31
+ serves replies through the same connectors
32
+
33
+ ▼ WebSocket / MCP (stdio)
34
+ agent (subscribes to one channel)
37
35
  ```
38
36
 
39
- Channel a named subscription box. Holds one or more connectors. Each Claude session subscribes to exactly one channel. Delivery mode is `fanout` (every subscriber sees every event; the default) or `exclusive` (round-robin one subscriber per event, for worker pools).
37
+ Three concepts make up the model:
38
+
39
+ Channel — a named subscription box. Holds one or more connectors. An agent session subscribes to exactly one channel. Delivery is `fanout` (every subscriber sees every event, the default) or `exclusive` (one event per subscriber, round-robin — for worker pools).
40
40
 
41
- Connector — a single attachment to an external source. Four types ship: `slack` (Socket Mode push), `gh` (GitHub poll via the `gh` CLI), `discord` (Gateway push), and `schedule` (cron tick). Connectors are nested inside their owning channel.
41
+ Connector — a single attachment from a channel to an external source. Four types ship today: `slack`, `gh`, `discord`, `schedule`. The first three are bidirectional (events in, replies out); `schedule` is one-way (cron ticks in).
42
42
 
43
- Profile — a named launch preset for Claude. Bundles `{ path, sub-agent, channel }` so `fnl claude --profile cto` reproduces a known setup. The first profile in the list is the default.
43
+ Profile — a saved launch preset for an agent. Bundles `{ path, sub-agent, channel }` so `fnl claude --profile cto` reproduces a known setup. The first profile in the list is the default.
44
44
 
45
- Gateway daemon the long-running process and the sole owner of external connections. Each connector connects from here exactly once; Claude sessions never open their own. Hosts the connector listeners with auto-restart, broadcasts events to subscribed clients, and serves the outbound reply API. Runs on port 9742 by default.
45
+ The daemon is where all external connections live. It runs on port 9742, supervises connectors with auto-restart, broadcasts events to subscribed agent sessions over WebSocket, and serves the reply API that MCP calls. Starting or stopping an agent never starts or stops external connections.
46
46
 
47
- MCP the bridge into Claude Code. A thin client: subscribes to one channel over WebSocket (the daemon does the real work) and surfaces one MCP tool per callable connector so Claude can call back out. Starting or stopping a Claude session does not start or stop external connections.
47
+ The MCP layer is a thin bridge into the agent. It subscribes to the bound channel over WebSocket (the daemon does the work) and exposes one tool per callable connector so the agent can reply back out.
48
48
 
49
49
  ## Requirements
50
50
 
51
51
  - [Bun](https://bun.sh) 1.3 or later
52
52
  - [Claude Code](https://docs.claude.com/en/docs/claude-code) CLI
53
- - A Slack / GitHub / Discord token or CLI, depending on which connectors you use
53
+ - A token or CLI for whichever external service you connect (Slack app, `gh` auth, Discord bot, etc.)
54
54
 
55
55
  ## Install
56
56
 
@@ -58,11 +58,11 @@ MCP — the bridge into Claude Code. A thin client: subscribes to one channel ov
58
58
  bun add -g @interactive-inc/claude-funnel
59
59
  ```
60
60
 
61
- The published package already ships the built `dist/`, so `bun add -g` makes `funnel` / `fnl` available immediately — no postinstall step.
61
+ The published package ships the built `dist/`, so `bun add -g` makes `funnel` / `fnl` available immediately — no post-install step.
62
62
 
63
63
  ## Quick start
64
64
 
65
- Wire Slack to Claude:
65
+ Wire one source to one agent:
66
66
 
67
67
  ```bash
68
68
  fnl channels add ops
@@ -72,16 +72,59 @@ fnl gateway start
72
72
  fnl claude --channel ops
73
73
  ```
74
74
 
75
- From now on every Slack event the bot can see arrives in the running Claude session, and Claude can reply via the `my-slack` MCP tool.
75
+ Every event the connector sees now arrives in the running agent session, and the agent can reply via the `my-slack` MCP tool.
76
76
 
77
77
  Save it as a profile for one-command launches:
78
78
 
79
79
  ```bash
80
80
  fnl profiles add cto --path=/repo/myapp --sub-agent=cto --channel=ops
81
- fnl claude --profile cto # cd /repo/myapp + sub-agent + channel binding
81
+ fnl claude --profile cto # cd + sub-agent + channel binding in one shot
82
82
  ```
83
83
 
84
- Cron-driven Claude:
84
+ Or drop a `funnel.json` in the repo and `fnl claude` (no args) inside the repo will use it:
85
+
86
+ ```json
87
+ {
88
+ "$schema": "https://interactive-inc.github.io/open-claude-funnel/funnel.schema.json",
89
+ "channel": "ops",
90
+ "options": ["--brief", "--agent", "cto"],
91
+ "env": {
92
+ "ANTHROPIC_MODEL": "claude-sonnet-4-6"
93
+ },
94
+ "connectors": [
95
+ {
96
+ "type": "slack",
97
+ "name": "my-slack",
98
+ "env": {
99
+ "botToken": "SLACK_BOT_TOKEN",
100
+ "appToken": "SLACK_APP_TOKEN"
101
+ }
102
+ }
103
+ ]
104
+ }
105
+ ```
106
+
107
+ Only `channel` is required.
108
+
109
+ The optional `options` array is prepended to the claude argv on every launch, before any args the user types after `fnl claude`. Use it for repo-wide claude flags (e.g. `--brief`, `--agent <name>`, `--model <name>`). User-supplied CLI args appear later in the argv so they still win on collision.
110
+
111
+ The optional top-level `env` is a `Record<string, string>` of environment variables to layer under the claude process. `process.env` from the launching shell wins on collision, so funnel.json sets defaults that the user can still override one-off via the shell.
112
+
113
+ The optional `connectors` array is treated as the source of truth for the declared channel: missing connectors are created, an existing connector that the spec references by token (not by name) is renamed in place, and connectors not declared in the spec are removed on launch. An absent `connectors` field leaves `~/.funnel` alone.
114
+
115
+ The optional top-level `$schema` points at the hosted JSON Schema (`https://interactive-inc.github.io/open-claude-funnel/funnel.schema.json`) so editors can validate and autocomplete the file. Local alternatives: the file ships in the npm package at `node_modules/@interactive-inc/claude-funnel/schemas/funnel.schema.json`, or generate one in-repo with `fnl schema > funnel.schema.json` and reference it via `./funnel.schema.json`.
116
+
117
+ Each token field resolves in this order:
118
+
119
+ - literal value at the field itself (e.g. `"botToken": "xoxb-..."`) — used as-is
120
+ - env-var name at `env.<field>` (e.g. `"env": { "botToken": "SLACK_BOT_TOKEN" }`) — looked up in `process.env`, falling back to `./.env.local` in the cwd; fails with a clear error when neither is set
121
+ - field omitted everywhere — `fnl claude` prompts on a TTY and writes the answer to `~/.funnel/settings.json`; on non-TTY stdin the launch fails so CI / agent-spawned-agent runs do not hang
122
+
123
+ Setting both a literal and an `env.<field>` for the same field is an error (pick one).
124
+
125
+ `funnel.json` itself is never written to — secrets stay in env vars, `.env.local`, or `~/.funnel`, never in the committed file.
126
+
127
+ Cron-driven agent runs:
85
128
 
86
129
  ```bash
87
130
  fnl channels ops connectors add daily --type=schedule
@@ -91,14 +134,11 @@ fnl channels ops connectors daily schedules add morning \
91
134
 
92
135
  Each tick fires the prompt into the channel. If the daemon was down at 9 AM, the next start catches up the missed slot (`meta.catchup = "true"`) for up to 24 hours.
93
136
 
94
- Multiple Claudes on the same source — pick the delivery mode:
137
+ Multiple agents on the same source — pick the delivery mode:
95
138
 
96
139
  ```bash
97
- # default: fanout — every Claude on the channel sees every event
98
- fnl channels add reviews
99
-
100
- # worker pool — each event is handled by exactly one Claude, round-robin
101
- fnl channels add ingest --delivery=exclusive
140
+ fnl channels add reviews # fanout (default): every agent sees every event
141
+ fnl channels add ingest --delivery=exclusive # exclusive: one event per agent, round-robin
102
142
  ```
103
143
 
104
144
  ## CLI surface
@@ -138,9 +178,11 @@ fnl profiles <name> as-default move to the front of the list
138
178
  fnl profiles rename <old> <new>
139
179
  fnl profiles remove <name>
140
180
 
141
- fnl claude launch the default profile
181
+ fnl claude launch using ./funnel.json, or the default profile
142
182
  fnl claude --profile <name> launch a named profile
143
183
  fnl claude --channel <name> raw launch (no profile, cwd = current dir)
184
+ fnl claude [...] positionals and any flag other than -p / --profile / --channel
185
+ (e.g. --agent, --resume, -c, --model) pass through to claude
144
186
  fnl mcp run as an MCP server (invoked from .mcp.json)
145
187
 
146
188
  fnl gateway status (default subcommand)
@@ -150,6 +192,7 @@ fnl gateway logs [-n <N>] tail diagnostic log
150
192
  fnl gateway listeners live registry (alive / dead)
151
193
 
152
194
  fnl status overall status (channels / profiles / gateway / clients)
195
+ fnl schema print the JSON Schema for funnel.json (pipe to a file for editor support)
153
196
  fnl update `bun i -g @interactive-inc/claude-funnel`
154
197
  fnl (no args) launch the OpenTUI dashboard
155
198
 
@@ -161,7 +204,7 @@ fnl --help every subcommand has --help; verb-w
161
204
 
162
205
  ## Outbound calls (MCP tools per connector)
163
206
 
164
- When `fnl claude` launches Claude Code, the funnel MCP server connects to the gateway and reads the channel's connectors from `~/.funnel/settings.json`. For every callable connector (`slack` / `discord` / `gh`; `schedule` is one-way and skipped), the MCP advertises one tool with the connector's name. Claude calls them like:
207
+ When `fnl claude` launches the agent, the funnel MCP server connects to the daemon and reads the channel's connectors from `~/.funnel/settings.json`. For every callable connector (`slack` / `discord` / `gh`; `schedule` is one-way and skipped), MCP advertises one tool with the connector's name. The agent calls them like:
165
208
 
166
209
  ```jsonc
167
210
  // MCP: tools/list returns
@@ -169,7 +212,7 @@ When `fnl claude` launches Claude Code, the funnel MCP server connects to the ga
169
212
  { "name": "ops-slack", "inputSchema": { ... } }
170
213
  { "name": "gh-main", "inputSchema": { ... } }
171
214
 
172
- // Claude calls
215
+ // agent calls
173
216
  tools/call name="discord" arguments={
174
217
  "method": "POST",
175
218
  "path": "/channels/123/messages",
@@ -177,9 +220,9 @@ tools/call name="discord" arguments={
177
220
  }
178
221
  ```
179
222
 
180
- The MCP forwards via HTTP `POST /channels/<channel>/connectors/<connector>/call` to the gateway daemon, which dispatches through the existing `FunnelChannels.call()` adapter. No bash subshell, no CLI cold start — replies are essentially synchronous.
223
+ MCP forwards via HTTP `POST /channels/<channel>/connectors/<connector>/call` to the daemon, which dispatches through the connector's adapter. No bash subshell, no CLI cold start — replies are essentially synchronous.
181
224
 
182
- If you need to invoke a connector from outside Claude, the same path is reachable as `fnl channels <ch> connectors <c> request --method=<...> [--key=value ...]`.
225
+ To invoke a connector from outside an agent, the same path is reachable as `fnl channels <ch> connectors <c> request --method=<...> [--key=value ...]`.
183
226
 
184
227
  ## Data model
185
228
 
@@ -189,7 +232,7 @@ Channel = { id, name, delivery, connectors[] }
189
232
  or `exclusive` (round-robin one client per event)
190
233
 
191
234
  Connector =
192
- | { type: "slack", name, botToken, appToken } Slack Socket Mode
235
+ | { type: "slack", name, botToken, appToken } Slack Socket Mode
193
236
  | { type: "gh", name, pollInterval? } GitHub (gh CLI, poll-based)
194
237
  | { type: "discord", name, botToken } Discord Gateway
195
238
  | { type: "schedule", name, entries[] } cron-driven; entries = { id, cron, prompt, enabled?, catchupPolicy? }
@@ -197,6 +240,13 @@ Connector =
197
240
  Profile = { name, path, subAgent, channelId }
198
241
  named launch preset; the first profile in the list is the default
199
242
 
243
+ LocalConfig = { channel, options?, env?, connectors? }
244
+ per-repo file (funnel.json) checked by `fnl claude` when no --profile / --channel is given
245
+ options[] is prepended to claude argv (user CLI args override); env merges under process.env;
246
+ connectors[] declares connectors to materialize on launch (each token field accepts a literal,
247
+ an env-var reference at `env.<field>` resolved from process.env and ./.env.local, or omission
248
+ for a TTY prompt persisted to ~/.funnel)
249
+
200
250
  Settings = { channels[], profiles[] } → ~/.funnel/settings.json
201
251
  ```
202
252
 
@@ -208,7 +258,7 @@ Persistent state lives under `~/.funnel/`. Volatile logs and the event store liv
208
258
  ~/.funnel/
209
259
  ├── settings.json channels[] with nested connectors, profiles[]
210
260
  ├── gateway.pid daemon PID
211
- ├── gateway.token Bearer token for gateway HTTP / WS
261
+ ├── gateway.token Bearer token for daemon HTTP / WS
212
262
  ├── claude/
213
263
  │ └── <profile>.pid prevents double-launch of the same profile
214
264
  └── channels/
@@ -219,23 +269,23 @@ Persistent state lives under `~/.funnel/`. Volatile logs and the event store liv
219
269
 
220
270
  /tmp/funnel/
221
271
  ├── events/events.db SQLite event store with replay-by-seq
222
- ├── funnel.log diagnostic log (gateway lifecycle, listener boot, connects)
272
+ ├── funnel.log diagnostic log (daemon lifecycle, listener boot, connects)
223
273
  └── gateway.log daemon stdout/stderr
224
274
  ```
225
275
 
226
- Notes
276
+ Notes:
227
277
 
228
278
  - Connector configuration is stored inline in `settings.json` (nested under the channel), not in a per-type directory. Per-connector durable state (e.g. `lastFiredAt` for schedule catch-up) lives under `channels/<channel-id>/connectors/<connector-id>/state.json` keyed by id, so renames do not lose state.
229
- - `funnel gateway logs` tails `funnel.log` and renders it as YAML.
279
+ - `fnl gateway logs` tails `funnel.log` and renders it as YAML.
230
280
 
231
281
  ## Environment variables
232
282
 
233
283
  | Variable | Purpose |
234
284
  | ---------------------- | --------------------------------------------------------------------------------------------- |
235
285
  | `FUNNEL_CHANNEL_ID` | Injected into the child process by `fnl claude`; the funnel MCP uses it to subscribe. |
236
- | `FUNNEL_PORT` | Gateway port (default 9742). |
237
- | `FUNNEL_GATEWAY_URL` | Gateway base URL used by MCP for both WS subscribe and HTTP reply (default `http://localhost:9742`). |
238
- | `FUNNEL_GATEWAY_TOKEN` | Bearer token for the gateway HTTP / WS. Defaults to the contents of `~/.funnel/gateway.token`. |
286
+ | `FUNNEL_PORT` | Daemon port (default 9742). |
287
+ | `FUNNEL_GATEWAY_URL` | Daemon base URL used by MCP for both WS subscribe and HTTP reply (default `http://localhost:9742`). |
288
+ | `FUNNEL_GATEWAY_TOKEN` | Bearer token for the daemon HTTP / WS. Defaults to the contents of `~/.funnel/gateway.token`. |
239
289
 
240
290
  ## Discord bot setup
241
291
 
@@ -293,7 +343,7 @@ await server.stop()
293
343
  unsubscribe()
294
344
  ```
295
345
 
296
- The gateway daemon exposes `/health`, `/status`, `/listeners*`, `/channels/:channel/connectors/:connector/call`, plus the `/ws?channel=<name>` WebSocket.
346
+ The daemon exposes `/health`, `/status`, `/listeners*`, `/channels/:channel/connectors/:connector/call`, plus the `/ws?channel=<name>` WebSocket.
297
347
 
298
348
  ### Sandboxed Funnel
299
349
 
@@ -381,9 +431,9 @@ The published package ships a bundled library entry (`dist/index.js`) plus gener
381
431
 
382
432
  This repo ships a Claude Code skill at `.claude/skills/funnel/SKILL.md`. It briefs Claude on the architecture and command groups, and tells it to defer flag-level details to `funnel <command> --help`.
383
433
 
384
- Project-scoped (auto). If you run `claude` inside this repo, the skill is picked up automatically — no install step.
434
+ Project-scoped (auto): if you run `claude` inside this repo, the skill is picked up automatically — no install step.
385
435
 
386
- Global (use the skill in any project). Claude Code does not currently provide a CLI to install skills from a remote URL, so copy the file into your personal skills directory:
436
+ Global (use the skill in any project): Claude Code does not currently provide a CLI to install skills from a remote URL, so copy the file into your personal skills directory:
387
437
 
388
438
  ```bash
389
439
  # from a clone of this repo