@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.
- package/README.md +179 -80
- package/dist/bin.js +693 -698
- package/dist/connector-adapter-CXB-q_XC.d.ts +11 -0
- package/dist/connector-adapter-D5Utumgz.js +4 -0
- package/dist/connectors/discord.d.ts +76 -0
- package/dist/connectors/discord.js +2 -0
- package/dist/connectors/gh.d.ts +38 -0
- package/dist/connectors/gh.js +2 -0
- package/dist/connectors/schedule.d.ts +53 -0
- package/dist/connectors/schedule.js +2 -0
- package/dist/connectors/slack.d.ts +62 -0
- package/dist/connectors/slack.js +2 -0
- package/dist/discord-connector-schema-Dww2I4zH.d.ts +14 -0
- package/dist/discord-connector-schema-ygf5Df-2.js +173 -0
- package/dist/file-system-Co60LrmR.d.ts +74 -0
- package/dist/gateway/daemon.js +243 -221
- package/dist/gh-connector-schema-2ml29MBC.js +218 -0
- package/dist/gh-connector-schema-BZFAS-p-.d.ts +45 -0
- package/dist/index.d.ts +3888 -0
- package/dist/index.js +6296 -0
- package/dist/logger-CTlXs7z4.d.ts +33 -0
- package/dist/node-logger-DQz_BGOD.js +61 -0
- package/dist/schedule-connector-schema-CkuIQ0JQ.js +325 -0
- package/dist/slack-connector-schema-Cd22WiHB.js +153 -0
- package/dist/slack-connector-schema-D7zAHN8k.d.ts +15 -0
- package/lib/bin.ts +1 -76
- package/lib/cli/index.ts +85 -0
- package/lib/cli/router/to-request.ts +1 -0
- package/lib/cli/routes/channels.$channel.publish.ts +52 -0
- package/lib/cli/routes/claude.ts +1 -0
- package/lib/cli/routes/index.ts +35 -18
- package/lib/cli/routes/profiles.add.$profile.ts +5 -2
- package/lib/cli/routes/profiles.set.$profile.ts +10 -11
- package/lib/connectors/discord.ts +4 -0
- package/lib/connectors/gh.ts +3 -0
- package/lib/connectors/schedule.ts +4 -0
- package/lib/connectors/slack.ts +4 -0
- package/lib/engine/claude/claude.ts +6 -0
- package/lib/engine/mcp/channel-server.ts +34 -115
- package/lib/engine/mcp/channel-subscriber.ts +82 -0
- package/lib/engine/mcp/read-channel-connectors.ts +34 -0
- package/lib/engine/mcp/read-gateway-token.ts +16 -0
- package/lib/engine/mcp/usage-hint-for-type.ts +15 -0
- package/lib/engine/settings/settings-schema.ts +2 -0
- package/lib/funnel.ts +162 -55
- package/lib/gateway/broadcaster.ts +1 -1
- package/lib/gateway/channel-publisher.ts +67 -0
- package/lib/gateway/gateway-server.ts +28 -16
- package/lib/gateway/publish-schema.ts +27 -0
- package/lib/gateway/routes/channels.publish.ts +44 -0
- package/lib/gateway/routes/index.ts +2 -0
- package/lib/gateway/routes/route-deps.ts +8 -0
- package/lib/index.ts +17 -0
- package/package.json +41 -25
package/README.md
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
[](https://www.npmjs.com/package/@interactive-inc/claude-funnel)
|
|
2
2
|
[](./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
|
|
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
|
-
##
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
160
|
+
`--channel` accepts the channel name (not the uuid). The CLI resolves it to a channel id before calling the engine.
|
|
127
161
|
|
|
128
|
-
##
|
|
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
|
|
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;
|
|
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
|
-
|
|
160
|
-
| { type: "
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
336
|
+
### Embedding the CLI
|
|
253
337
|
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
355
|
+
```ts
|
|
356
|
+
import { Funnel, launchTui } from "@interactive-inc/claude-funnel"
|
|
259
357
|
|
|
260
|
-
|
|
358
|
+
await launchTui(new Funnel())
|
|
359
|
+
```
|
|
261
360
|
|
|
262
|
-
###
|
|
361
|
+
### Validating connector configs
|
|
263
362
|
|
|
264
|
-
|
|
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 #
|
|
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
|
-
|
|
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.
|