@interactive-inc/claude-funnel 0.8.0 → 0.10.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 (54) hide show
  1. package/README.md +179 -80
  2. package/dist/bin.js +693 -698
  3. package/dist/connector-adapter-CXB-q_XC.d.ts +11 -0
  4. package/dist/connector-adapter-D5Utumgz.js +4 -0
  5. package/dist/connectors/discord.d.ts +76 -0
  6. package/dist/connectors/discord.js +2 -0
  7. package/dist/connectors/gh.d.ts +38 -0
  8. package/dist/connectors/gh.js +2 -0
  9. package/dist/connectors/schedule.d.ts +53 -0
  10. package/dist/connectors/schedule.js +2 -0
  11. package/dist/connectors/slack.d.ts +62 -0
  12. package/dist/connectors/slack.js +2 -0
  13. package/dist/discord-connector-schema-Dww2I4zH.d.ts +14 -0
  14. package/dist/discord-connector-schema-ygf5Df-2.js +173 -0
  15. package/dist/file-system-Co60LrmR.d.ts +74 -0
  16. package/dist/gateway/daemon.js +243 -221
  17. package/dist/gh-connector-schema-2ml29MBC.js +218 -0
  18. package/dist/gh-connector-schema-BZFAS-p-.d.ts +45 -0
  19. package/dist/index.d.ts +3888 -0
  20. package/dist/index.js +6296 -0
  21. package/dist/logger-CTlXs7z4.d.ts +33 -0
  22. package/dist/node-logger-DQz_BGOD.js +61 -0
  23. package/dist/schedule-connector-schema-CkuIQ0JQ.js +325 -0
  24. package/dist/slack-connector-schema-Cd22WiHB.js +153 -0
  25. package/dist/slack-connector-schema-D7zAHN8k.d.ts +15 -0
  26. package/lib/bin.ts +1 -76
  27. package/lib/cli/index.ts +85 -0
  28. package/lib/cli/router/to-request.ts +1 -0
  29. package/lib/cli/routes/channels.$channel.publish.ts +52 -0
  30. package/lib/cli/routes/claude.ts +1 -0
  31. package/lib/cli/routes/index.ts +35 -18
  32. package/lib/cli/routes/profiles.add.$profile.ts +5 -2
  33. package/lib/cli/routes/profiles.set.$profile.ts +10 -11
  34. package/lib/connectors/discord.ts +4 -0
  35. package/lib/connectors/gh.ts +3 -0
  36. package/lib/connectors/schedule.ts +4 -0
  37. package/lib/connectors/slack.ts +4 -0
  38. package/lib/engine/claude/claude.ts +6 -0
  39. package/lib/engine/mcp/channel-server.ts +34 -115
  40. package/lib/engine/mcp/channel-subscriber.ts +82 -0
  41. package/lib/engine/mcp/read-channel-connectors.ts +34 -0
  42. package/lib/engine/mcp/read-gateway-token.ts +16 -0
  43. package/lib/engine/mcp/usage-hint-for-type.ts +15 -0
  44. package/lib/engine/settings/settings-schema.ts +2 -0
  45. package/lib/funnel.ts +162 -55
  46. package/lib/gateway/broadcaster.ts +1 -1
  47. package/lib/gateway/channel-publisher.ts +67 -0
  48. package/lib/gateway/gateway-server.ts +28 -16
  49. package/lib/gateway/publish-schema.ts +27 -0
  50. package/lib/gateway/routes/channels.publish.ts +44 -0
  51. package/lib/gateway/routes/index.ts +2 -0
  52. package/lib/gateway/routes/route-deps.ts +8 -0
  53. package/lib/index.ts +17 -0
  54. package/package.json +41 -25
package/README.md CHANGED
@@ -1,11 +1,25 @@
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 through subscription "channels" into Claude Code sessions, and outbound API calls from Claude are funneled through the same connectors as MCP tools.
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.
5
5
 
6
6
  The command is `funnel` or its shorthand `fnl`.
7
7
 
8
- ## Overview
8
+ ## Why funnel
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."
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:
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
19
+
20
+ If you have ever wanted "Slack-driven Claude" or "cron-driven Claude" without writing a dispatcher, this is it.
21
+
22
+ ## Concepts
9
23
 
10
24
  ```
11
25
  External sources Outbound calls
@@ -20,14 +34,21 @@ External sources Outbound calls
20
34
 
21
35
  ▼ MCP (stdio)
22
36
  Claude Code
23
- (events arrive as <channel> notifications;
24
- one MCP tool is exposed per configured connector
25
- so Claude can reply / send / call APIs without bash)
26
37
  ```
27
38
 
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).
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.
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.
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.
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.
48
+
28
49
  ## Requirements
29
50
 
30
- - [Bun](https://bun.sh) 1.3 or later (runtime — used at install time to build the CLI bundle and at runtime to execute it)
51
+ - [Bun](https://bun.sh) 1.3 or later
31
52
  - [Claude Code](https://docs.claude.com/en/docs/claude-code) CLI
32
53
  - A Slack / GitHub / Discord token or CLI, depending on which connectors you use
33
54
 
@@ -37,39 +58,52 @@ External sources Outbound calls
37
58
  bun add -g @interactive-inc/claude-funnel
38
59
  ```
39
60
 
40
- `postinstall` runs `bun run build` to produce a single bundled CLI at `dist/bin.js`. After install, `funnel` and `fnl` are available globally.
61
+ The published package already ships the built `dist/`, so `bun add -g` makes `funnel` / `fnl` available immediately — no postinstall step.
41
62
 
42
63
  ## Quick start
43
64
 
65
+ Wire Slack to Claude:
66
+
44
67
  ```bash
45
- # Create a subscription box (channel) and attach a connector
46
68
  fnl channels add ops
47
69
  fnl channels ops connectors add my-slack --type=slack \
48
70
  --bot-token=xoxb-... --app-token=xapp-...
49
-
50
- # Start the gateway (connects to Slack Socket Mode and surfaces events)
51
71
  fnl gateway start
52
-
53
- # Launch Claude with a raw channel binding (no profile required)
54
72
  fnl claude --channel ops
73
+ ```
55
74
 
56
- # Or save it as a profile and launch by name
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.
76
+
77
+ Save it as a profile for one-command launches:
78
+
79
+ ```bash
57
80
  fnl profiles add cto --path=/repo/myapp --sub-agent=cto --channel=ops
58
- fnl claude --profile cto
81
+ fnl claude --profile cto # cd /repo/myapp + sub-agent + channel binding
59
82
  ```
60
83
 
61
- Schedule (cron) trigger:
84
+ Cron-driven Claude:
62
85
 
63
86
  ```bash
64
- # A schedule connector contains many cron entries
65
87
  fnl channels ops connectors add daily --type=schedule
66
88
  fnl channels ops connectors daily schedules add morning \
67
89
  --cron="0 9 * * *" --prompt="morning standup"
68
90
  ```
69
91
 
92
+ 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
+
94
+ Multiple Claudes on the same source — pick the delivery mode:
95
+
96
+ ```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
102
+ ```
103
+
70
104
  ## CLI surface
71
105
 
72
- Connectors live nested inside their owning channel. Every CLI verb (`add` / `set` / `remove` / `rename` / `as-default` / `request`) maps to `POST` plus the verb in the URL — there is no method-stripping, so the same word stays visible in both shell and HTTP form. Read paths (no verb) stay `GET`.
106
+ Connectors live nested inside their owning channel. Every write verb (`add` / `set` / `remove` / `rename` / `as-default` / `request`) maps to `POST` plus the verb in the URL — the same word stays visible in shell and HTTP form. Read paths stay `GET`.
73
107
 
74
108
  ```text
75
109
  fnl channels list
@@ -123,11 +157,11 @@ fnl --version
123
157
  fnl --help every subcommand has --help; verb-without-arg also returns help
124
158
  ```
125
159
 
126
- `--channel` accepts the channel **name** (not the uuid). The CLI resolves it to a channel id before calling the engine.
160
+ `--channel` accepts the channel name (not the uuid). The CLI resolves it to a channel id before calling the engine.
127
161
 
128
- ## Reply path: MCP tools per connector
162
+ ## Outbound calls (MCP tools per connector)
129
163
 
130
- 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 can call them like:
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:
131
165
 
132
166
  ```jsonc
133
167
  // MCP: tools/list returns
@@ -151,36 +185,75 @@ If you need to invoke a connector from outside Claude, the same path is reachabl
151
185
 
152
186
  ```
153
187
  Channel = { id, name, delivery, connectors[] }
154
- subscription box; `delivery` is `fanout` (default; every WS client sees every event)
188
+ subscription box; delivery is `fanout` (every WS client sees every event)
155
189
  or `exclusive` (round-robin one client per event)
156
190
 
157
191
  Connector =
158
- | { type: "slack", name, botToken, appToken }
159
- Slack Socket Mode
160
- | { type: "gh", name, pollInterval? }
161
- GitHub (gh CLI, poll-based)
162
- | { type: "discord", name, botToken }
163
- Discord Gateway
164
- | { type: "schedule", name, entries[] }
165
- cron-driven; entries = { id, cron, prompt, enabled?, catchupPolicy? }
192
+ | { type: "slack", name, botToken, appToken } Slack Socket Mode
193
+ | { type: "gh", name, pollInterval? } GitHub (gh CLI, poll-based)
194
+ | { type: "discord", name, botToken } Discord Gateway
195
+ | { type: "schedule", name, entries[] } cron-driven; entries = { id, cron, prompt, enabled?, catchupPolicy? }
166
196
 
167
197
  Profile = { name, path, subAgent, channelId }
168
198
  named launch preset; the first profile in the list is the default
169
199
 
170
- Settings = { channels[], profiles[] }
171
- → ~/.funnel/settings.json
200
+ Settings = { channels[], profiles[] } → ~/.funnel/settings.json
201
+ ```
202
+
203
+ ## File layout
204
+
205
+ Persistent state lives under `~/.funnel/`. Volatile logs and the event store live under `/tmp/funnel/`.
206
+
207
+ ```
208
+ ~/.funnel/
209
+ ├── settings.json channels[] with nested connectors, profiles[]
210
+ ├── gateway.pid daemon PID
211
+ ├── gateway.token Bearer token for gateway HTTP / WS
212
+ ├── claude/
213
+ │ └── <profile>.pid prevents double-launch of the same profile
214
+ └── channels/
215
+ └── <channel-id>/
216
+ └── connectors/
217
+ └── <connector-id>/
218
+ └── state.json per-connector durable state (e.g. schedule lastFiredAt)
219
+
220
+ /tmp/funnel/
221
+ ├── events/events.db SQLite event store with replay-by-seq
222
+ ├── funnel.log diagnostic log (gateway lifecycle, listener boot, connects)
223
+ └── gateway.log daemon stdout/stderr
172
224
  ```
173
225
 
174
- Connectors are stored per type, one file per connector, under `~/.funnel/connectors/<type>/<name>.(json|jsonl)` so adding or retiring a type is contained to its own subdirectory.
226
+ Notes
227
+
228
+ - 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.
230
+
231
+ ## Environment variables
232
+
233
+ | Variable | Purpose |
234
+ | ---------------------- | --------------------------------------------------------------------------------------------- |
235
+ | `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`. |
239
+
240
+ ## Discord bot setup
241
+
242
+ - Create a bot in the Discord Developer Portal and obtain its token
243
+ - Enable `Message Content Intent` under Privileged Gateway Intents
244
+ - Invite the bot via OAuth2 → URL Generator with the `bot` scope and `View Channels` / `Send Messages` / `Read Message History` permissions
175
245
 
176
246
  ## Programmable API (Bun)
177
247
 
178
- `funnel` is also usable as a library — the same `Funnel` facade the CLI uses is exported from the package root, with no CLI side effects.
248
+ `funnel` is also usable as a library — the same `Funnel` facade the CLI uses is exported from the package root. The constructor is fully lazy: `new Funnel()` records its props and freezes; no disk / process / network access happens until a method is called.
179
249
 
180
250
  ```ts
181
251
  import { Funnel } from "@interactive-inc/claude-funnel"
182
252
 
183
- const funnel = new Funnel() // defaults to ~/.funnel + the local filesystem
253
+ const funnel = new Funnel() // defaults to ~/.funnel + /tmp/funnel on the local filesystem
254
+
255
+ funnel.paths
256
+ // → { dir: "/Users/you/.funnel", tmpDir: "/tmp/funnel", settings: "/Users/you/.funnel/settings.json" }
184
257
 
185
258
  const channel = funnel.channels.add({ name: "inbox" })
186
259
 
@@ -194,7 +267,7 @@ funnel.channels.addConnector("inbox", {
194
267
  for (const c of funnel.channels.list()) console.log(c.name, c.connectors.length)
195
268
  ```
196
269
 
197
- Every facet — `channels` / `profiles` / `gateway` / `gatewayServer` / `gatewayToken` / `listeners` / `mcp` / `claude` / `factory` / `store` / `process` / `logger` — is reachable from the same instance:
270
+ Every facet — `channels` / `profiles` / `gateway` / `gatewayServer` / `gatewayToken` / `listeners` / `mcp` / `claude` / `factory` / `store` / `process` / `logger` / `paths` — is reachable from the same instance:
198
271
 
199
272
  ```ts
200
273
  funnel.gateway.getStatus() // { running, pid, port }
@@ -220,9 +293,20 @@ await server.stop()
220
293
  unsubscribe()
221
294
  ```
222
295
 
223
- The gateway daemon exposes `/health`, `/status`, `/listeners*`, `/channels/:channel/connectors/:connector/call`, plus the `/ws?channel=<name>` WebSocket. There is no SPA; for a visual operator view, run `fnl` with no arguments to launch the OpenTUI dashboard.
296
+ The gateway daemon exposes `/health`, `/status`, `/listeners*`, `/channels/:channel/connectors/:connector/call`, plus the `/ws?channel=<name>` WebSocket.
297
+
298
+ ### Sandboxed Funnel
224
299
 
225
- Every side-effecting boundary is a DI seam. For tests / sandbox use, swap them all with the in-memory implementations and Funnel will not touch real disk, real processes, real time, or real UUIDs:
300
+ `Funnel.inMemory()` returns a Funnel pre-wired with Memory implementations for every IO boundary useful for tests and ad-hoc experiments. Pass any subset of `props` to override individual seams:
301
+
302
+ ```ts
303
+ import { Funnel } from "@interactive-inc/claude-funnel"
304
+
305
+ const funnel = Funnel.inMemory() // touches no real disk, processes, clock, or UUIDs
306
+ funnel.channels.add({ name: "inbox" }) // mutates the in-memory store
307
+ ```
308
+
309
+ The longhand form (for fine-grained control) is still available:
226
310
 
227
311
  ```ts
228
312
  import {
@@ -247,21 +331,59 @@ const funnel = new Funnel({
247
331
  })
248
332
  ```
249
333
 
250
- Available abstractions (each has `Funnel*` interface, `Node*` default, and `Memory*` for tests): `FunnelFileSystem`, `FunnelProcessRunner`, `FunnelLogger`, `FunnelClock`, `FunnelIdGenerator`. Plus `NoopFunnelLogger` for silent operation.
334
+ Available abstractions (each has `Funnel*` interface, `Node*` default, and `Memory*` for tests): `FunnelFileSystem`, `FunnelProcessRunner`, `FunnelLogger`, `FunnelClock`, `FunnelIdGenerator`. Plus `NoopFunnelLogger` for silent operation and `MockFunnelSettingsReader` for an in-memory settings store.
251
335
 
252
- The published package ships both TypeScript sources (under `lib/`) for library consumers and a single bundled CLI (`dist/bin.js`) which is what the `fnl` / `funnel` bin entries point to. Importing `@interactive-inc/claude-funnel/bin` resolves to the bundled CLI entry — only do this if you're embedding the CLI rather than the library.
336
+ ### Embedding the CLI
253
337
 
254
- ## Claude Code skill
338
+ The same Hono app that backs `fnl` is published as `createCliApp(funnel)` — pass any `Funnel` instance to bind a custom store / boundaries to the routes. The pair `toRequest` (argv → request) and `queryToCliArgs` (URL search params → CLI flags) lets you drive the app programmatically:
255
339
 
256
- 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`.
340
+ ```ts
341
+ import { Funnel, createCliApp, toRequest } from "@interactive-inc/claude-funnel"
342
+
343
+ const app = createCliApp(Funnel.inMemory())
344
+ const { method, url } = toRequest(["channels", "add", "inbox"])
345
+ const res = await app.request(url, { method })
346
+ console.log(await res.text())
347
+ ```
348
+
349
+ `cliApp` is the same app pre-wired to `new Funnel()` for callers who just want the default. The middleware sets the chosen Funnel onto `c.var.funnel`; the matching `Env` type is exported for composing custom routes that share the same context variable.
350
+
351
+ ### Launching the TUI
352
+
353
+ `launchTui(funnel)` boots the OpenTUI dashboard against any `Funnel` instance — pass `Funnel.inMemory()` to drive it against a fake state, or your production funnel for a live view.
257
354
 
258
- ### Project-scoped (auto)
355
+ ```ts
356
+ import { Funnel, launchTui } from "@interactive-inc/claude-funnel"
259
357
 
260
- If you run `claude` inside this repo, the skill is picked up automatically — no install step.
358
+ await launchTui(new Funnel())
359
+ ```
261
360
 
262
- ### Global (use the skill in any project)
361
+ ### Validating connector configs
263
362
 
264
- Claude Code does not currently provide a CLI to install skills from a remote URL, so copy the file into your personal skills directory:
363
+ Each connector type publishes its Zod schema, so consumers can parse external configs (JSON files, API payloads, etc.) before handing them to `addConnector`. The discriminated union `connectorConfigSchema` covers the whole set.
364
+
365
+ ```ts
366
+ import {
367
+ connectorConfigSchema,
368
+ slackConnectorSchema,
369
+ type SlackConnectorConfig,
370
+ } from "@interactive-inc/claude-funnel"
371
+
372
+ const slack: SlackConnectorConfig = slackConnectorSchema.parse(json)
373
+ const any = connectorConfigSchema.parse(json) // narrows by `type`
374
+ ```
375
+
376
+ ### Packaging
377
+
378
+ The published package ships a bundled library entry (`dist/index.js`) plus generated declarations (`dist/**/*.d.ts`), so consumers do not need a matching tsconfig paths setup to resolve `@/...` imports. The `fnl` / `funnel` bin entries point to a separately bundled `dist/bin.js`. Import `@interactive-inc/claude-funnel/bin` only if you are embedding the CLI binary rather than the library.
379
+
380
+ ## Claude Code skill
381
+
382
+ 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
+
384
+ Project-scoped (auto). If you run `claude` inside this repo, the skill is picked up automatically — no install step.
385
+
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:
265
387
 
266
388
  ```bash
267
389
  # from a clone of this repo
@@ -279,52 +401,29 @@ curl -fsSL https://raw.githubusercontent.com/interactive-inc/open-claude-funnel/
279
401
 
280
402
  After this, Claude Code will load the skill in any session.
281
403
 
282
- ## Discord bot setup
283
-
284
- - Create a bot in the Discord Developer Portal and obtain its token
285
- - Enable `Message Content Intent` under Privileged Gateway Intents
286
- - Invite the bot via OAuth2 → URL Generator with the `bot` scope and `View Channels` / `Send Messages` / `Read Message History` permissions
287
-
288
- ## Environment variables
289
-
290
- | Variable | Purpose |
291
- | ---------------------- | --------------------------------------------------------------------------------------------- |
292
- | `FUNNEL_CHANNEL_ID` | Injected into the child process by `fnl claude`; the funnel MCP uses it to subscribe. |
293
- | `FUNNEL_PORT` | Gateway port (default 9742). |
294
- | `FUNNEL_GATEWAY_URL` | Gateway base URL used by MCP for both WS subscribe and HTTP reply (default `http://localhost:9742`). |
295
- | `FUNNEL_GATEWAY_TOKEN` | Bearer token for the gateway HTTP / WS. Defaults to the contents of `~/.funnel/gateway.token`. |
296
-
297
- ## File layout
298
-
299
- - Config: `~/.funnel/settings.json` (channels with nested connectors / profiles)
300
- - Connectors: `~/.funnel/connectors/<type>/<name>.(json|jsonl)`
301
- - `slack/<name>.json`, `gh/<name>.json`, `discord/<name>.json`
302
- - `schedule/<name>.jsonl` (one entry per line) and `schedule/<name>.state.json` (last-fired timestamps for catch-up)
303
- - Gateway PID: `~/.funnel/gateway.pid`, token: `~/.funnel/gateway.token`
304
- - Claude PIDs: `~/.funnel/claude/<profile>.pid`
305
- - Event store: `/tmp/funnel/events/events.db` (SQLite; broadcaster events with replay-by-seq)
306
- - Diagnostic log: `/tmp/funnel/funnel.log` (gateway lifecycle, connect/disconnect, listener boot — what `funnel gateway logs` tails as YAML)
307
- - Process log: `/tmp/funnel/gateway.log` (daemon stdout/stderr)
308
-
309
- ## Links
310
-
311
- - [GitHub](https://github.com/interactive-inc/open-claude-funnel)
312
- - [Issues](https://github.com/interactive-inc/open-claude-funnel/issues)
313
- - Coding rules and design principles: [CLAUDE.md](https://github.com/interactive-inc/open-claude-funnel/blob/main/CLAUDE.md)
314
-
315
404
  ## Development
316
405
 
317
406
  ```bash
318
407
  git clone https://github.com/interactive-inc/open-claude-funnel.git
319
408
  cd open-claude-funnel
320
- bun install # postinstall builds dist/bin.js
409
+ bun install # install deps (no auto-build)
410
+ make build # produce dist/ — run this once after install
321
411
  bun link # symlinks fnl / funnel → dist/bin.js
322
- bun run build # rebuild after editing the cli
412
+ make build # rebuild library + CLI after editing
413
+ make build-lib # library only (vp pack)
414
+ make build-bin # CLI / daemon only (bun build --minify)
415
+ make clean # remove dist/
323
416
  bun test # run tests
324
417
  bunx tsc -b # type check
325
418
  bun lib/bin.ts ... # run the cli from source (no build) for fast iteration
326
419
  ```
327
420
 
421
+ ## Links
422
+
423
+ - [GitHub](https://github.com/interactive-inc/open-claude-funnel)
424
+ - [Issues](https://github.com/interactive-inc/open-claude-funnel/issues)
425
+ - Coding rules and design principles: [CLAUDE.md](https://github.com/interactive-inc/open-claude-funnel/blob/main/CLAUDE.md)
426
+
328
427
  ## License
329
428
 
330
429
  MIT © Interactive Inc.