@interactive-inc/claude-funnel 0.10.1 → 0.15.2
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 +116 -56
- package/dist/bin.js +532 -505
- package/dist/connectors/schedule.d.ts +2 -49
- package/dist/connectors/schedule.js +1 -1
- package/dist/connectors/slack.d.ts +4 -20
- package/dist/connectors/slack.js +1 -1
- package/dist/gateway/daemon.js +214 -212
- package/dist/index.d.ts +463 -164
- package/dist/index.js +561 -36
- package/dist/{schedule-connector-schema-CkuIQ0JQ.js → schedule-connector-schema-FxP7LPlx.js} +11 -0
- package/dist/{file-system-Co60LrmR.d.ts → schedule-listener-BPodvbld.d.ts} +56 -1
- package/dist/{slack-connector-schema-Cd22WiHB.js → slack-connector-schema-B4hsf3AY.js} +10 -1
- package/dist/slack-listener-CHj6uMY-.d.ts +74 -0
- package/funnel.schema.json +144 -0
- package/package.json +2 -1
- package/dist/slack-event-processor-CS-bAit9.d.ts +0 -43
package/README.md
CHANGED
|
@@ -1,68 +1,78 @@
|
|
|
1
1
|
[](https://www.npmjs.com/package/@interactive-inc/claude-funnel)
|
|
2
2
|
[](./LICENSE)
|
|
3
3
|
|
|
4
|
-
A hub
|
|
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
|
|
6
|
+
The command is `funnel` (or the shorthand `fnl`).
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
|
|
10
|
+
## Why funnel
|
|
11
11
|
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
|
57
|
+
Per-repo (recommended for getting started — no global install, version pinned by the repo's lock file):
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
bun add -D @interactive-inc/claude-funnel
|
|
61
|
+
bunx funnel claude # or `bunx funnel <any subcommand>`
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Global (one CLI for every repo you touch):
|
|
65
|
+
|
|
57
66
|
```bash
|
|
58
67
|
bun add -g @interactive-inc/claude-funnel
|
|
68
|
+
funnel claude # or the `fnl` shorthand
|
|
59
69
|
```
|
|
60
70
|
|
|
61
|
-
The published package
|
|
71
|
+
The published package ships the built `dist/`, so either install makes `funnel` / `fnl` available immediately — no post-install step. The rest of the README uses `fnl` for brevity; swap in `bunx funnel` if you went the per-repo route.
|
|
62
72
|
|
|
63
73
|
## Quick start
|
|
64
74
|
|
|
65
|
-
Wire
|
|
75
|
+
Wire one source to one agent:
|
|
66
76
|
|
|
67
77
|
```bash
|
|
68
78
|
fnl channels add ops
|
|
@@ -72,16 +82,59 @@ fnl gateway start
|
|
|
72
82
|
fnl claude --channel ops
|
|
73
83
|
```
|
|
74
84
|
|
|
75
|
-
|
|
85
|
+
Every event the connector sees now arrives in the running agent session, and the agent can reply via the `my-slack` MCP tool.
|
|
76
86
|
|
|
77
87
|
Save it as a profile for one-command launches:
|
|
78
88
|
|
|
79
89
|
```bash
|
|
80
90
|
fnl profiles add cto --path=/repo/myapp --sub-agent=cto --channel=ops
|
|
81
|
-
fnl claude --profile cto # cd
|
|
91
|
+
fnl claude --profile cto # cd + sub-agent + channel binding in one shot
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Or drop a `funnel.json` in the repo and `fnl claude` (no args) inside the repo will use it:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"$schema": "./node_modules/@interactive-inc/claude-funnel/funnel.schema.json",
|
|
99
|
+
"channel": "ops",
|
|
100
|
+
"options": ["--brief", "--agent", "cto"],
|
|
101
|
+
"env": {
|
|
102
|
+
"ANTHROPIC_MODEL": "claude-sonnet-4-6"
|
|
103
|
+
},
|
|
104
|
+
"connectors": [
|
|
105
|
+
{
|
|
106
|
+
"type": "slack",
|
|
107
|
+
"name": "my-slack",
|
|
108
|
+
"env": {
|
|
109
|
+
"botToken": "SLACK_BOT_TOKEN",
|
|
110
|
+
"appToken": "SLACK_APP_TOKEN"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
|
82
115
|
```
|
|
83
116
|
|
|
84
|
-
|
|
117
|
+
Only `channel` is required.
|
|
118
|
+
|
|
119
|
+
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.
|
|
120
|
+
|
|
121
|
+
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.
|
|
122
|
+
|
|
123
|
+
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.
|
|
124
|
+
|
|
125
|
+
The optional top-level `$schema` points at the JSON Schema so editors can validate and autocomplete the file. The recommended reference for repos with a local install is `./node_modules/@interactive-inc/claude-funnel/funnel.schema.json` — it works without a network round-trip and editors do not need to prompt for trust. The same file is also published at `https://interactive-inc.github.io/open-claude-funnel/funnel.schema.json` (editors usually require explicit trust on first use), and `fnl schema > funnel.schema.json` regenerates a local copy on demand.
|
|
126
|
+
|
|
127
|
+
Each token field resolves in this order:
|
|
128
|
+
|
|
129
|
+
- literal value at the field itself (e.g. `"botToken": "xoxb-..."`) — used as-is
|
|
130
|
+
- 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
|
|
131
|
+
- 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
|
|
132
|
+
|
|
133
|
+
Setting both a literal and an `env.<field>` for the same field is an error (pick one).
|
|
134
|
+
|
|
135
|
+
`funnel.json` itself is never written to — secrets stay in env vars, `.env.local`, or `~/.funnel`, never in the committed file.
|
|
136
|
+
|
|
137
|
+
Cron-driven agent runs:
|
|
85
138
|
|
|
86
139
|
```bash
|
|
87
140
|
fnl channels ops connectors add daily --type=schedule
|
|
@@ -91,14 +144,11 @@ fnl channels ops connectors daily schedules add morning \
|
|
|
91
144
|
|
|
92
145
|
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
146
|
|
|
94
|
-
Multiple
|
|
147
|
+
Multiple agents on the same source — pick the delivery mode:
|
|
95
148
|
|
|
96
149
|
```bash
|
|
97
|
-
# default:
|
|
98
|
-
fnl channels add
|
|
99
|
-
|
|
100
|
-
# worker pool — each event is handled by exactly one Claude, round-robin
|
|
101
|
-
fnl channels add ingest --delivery=exclusive
|
|
150
|
+
fnl channels add reviews # fanout (default): every agent sees every event
|
|
151
|
+
fnl channels add ingest --delivery=exclusive # exclusive: one event per agent, round-robin
|
|
102
152
|
```
|
|
103
153
|
|
|
104
154
|
## CLI surface
|
|
@@ -138,9 +188,11 @@ fnl profiles <name> as-default move to the front of the list
|
|
|
138
188
|
fnl profiles rename <old> <new>
|
|
139
189
|
fnl profiles remove <name>
|
|
140
190
|
|
|
141
|
-
fnl claude launch the default profile
|
|
191
|
+
fnl claude launch using ./funnel.json, or the default profile
|
|
142
192
|
fnl claude --profile <name> launch a named profile
|
|
143
193
|
fnl claude --channel <name> raw launch (no profile, cwd = current dir)
|
|
194
|
+
fnl claude [...] positionals and any flag other than -p / --profile / --channel
|
|
195
|
+
(e.g. --agent, --resume, -c, --model) pass through to claude
|
|
144
196
|
fnl mcp run as an MCP server (invoked from .mcp.json)
|
|
145
197
|
|
|
146
198
|
fnl gateway status (default subcommand)
|
|
@@ -150,6 +202,7 @@ fnl gateway logs [-n <N>] tail diagnostic log
|
|
|
150
202
|
fnl gateway listeners live registry (alive / dead)
|
|
151
203
|
|
|
152
204
|
fnl status overall status (channels / profiles / gateway / clients)
|
|
205
|
+
fnl schema print the JSON Schema for funnel.json (pipe to a file for editor support)
|
|
153
206
|
fnl update `bun i -g @interactive-inc/claude-funnel`
|
|
154
207
|
fnl (no args) launch the OpenTUI dashboard
|
|
155
208
|
|
|
@@ -161,7 +214,7 @@ fnl --help every subcommand has --help; verb-w
|
|
|
161
214
|
|
|
162
215
|
## Outbound calls (MCP tools per connector)
|
|
163
216
|
|
|
164
|
-
When `fnl claude` launches
|
|
217
|
+
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
218
|
|
|
166
219
|
```jsonc
|
|
167
220
|
// MCP: tools/list returns
|
|
@@ -169,7 +222,7 @@ When `fnl claude` launches Claude Code, the funnel MCP server connects to the ga
|
|
|
169
222
|
{ "name": "ops-slack", "inputSchema": { ... } }
|
|
170
223
|
{ "name": "gh-main", "inputSchema": { ... } }
|
|
171
224
|
|
|
172
|
-
//
|
|
225
|
+
// agent calls
|
|
173
226
|
tools/call name="discord" arguments={
|
|
174
227
|
"method": "POST",
|
|
175
228
|
"path": "/channels/123/messages",
|
|
@@ -177,9 +230,9 @@ tools/call name="discord" arguments={
|
|
|
177
230
|
}
|
|
178
231
|
```
|
|
179
232
|
|
|
180
|
-
|
|
233
|
+
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
234
|
|
|
182
|
-
|
|
235
|
+
To invoke a connector from outside an agent, the same path is reachable as `fnl channels <ch> connectors <c> request --method=<...> [--key=value ...]`.
|
|
183
236
|
|
|
184
237
|
## Data model
|
|
185
238
|
|
|
@@ -189,7 +242,7 @@ Channel = { id, name, delivery, connectors[] }
|
|
|
189
242
|
or `exclusive` (round-robin one client per event)
|
|
190
243
|
|
|
191
244
|
Connector =
|
|
192
|
-
| { type: "slack", name, botToken, appToken }
|
|
245
|
+
| { type: "slack", name, botToken, appToken } Slack Socket Mode
|
|
193
246
|
| { type: "gh", name, pollInterval? } GitHub (gh CLI, poll-based)
|
|
194
247
|
| { type: "discord", name, botToken } Discord Gateway
|
|
195
248
|
| { type: "schedule", name, entries[] } cron-driven; entries = { id, cron, prompt, enabled?, catchupPolicy? }
|
|
@@ -197,6 +250,13 @@ Connector =
|
|
|
197
250
|
Profile = { name, path, subAgent, channelId }
|
|
198
251
|
named launch preset; the first profile in the list is the default
|
|
199
252
|
|
|
253
|
+
LocalConfig = { channel, options?, env?, connectors? }
|
|
254
|
+
per-repo file (funnel.json) checked by `fnl claude` when no --profile / --channel is given
|
|
255
|
+
options[] is prepended to claude argv (user CLI args override); env merges under process.env;
|
|
256
|
+
connectors[] declares connectors to materialize on launch (each token field accepts a literal,
|
|
257
|
+
an env-var reference at `env.<field>` resolved from process.env and ./.env.local, or omission
|
|
258
|
+
for a TTY prompt persisted to ~/.funnel)
|
|
259
|
+
|
|
200
260
|
Settings = { channels[], profiles[] } → ~/.funnel/settings.json
|
|
201
261
|
```
|
|
202
262
|
|
|
@@ -208,7 +268,7 @@ Persistent state lives under `~/.funnel/`. Volatile logs and the event store liv
|
|
|
208
268
|
~/.funnel/
|
|
209
269
|
├── settings.json channels[] with nested connectors, profiles[]
|
|
210
270
|
├── gateway.pid daemon PID
|
|
211
|
-
├── gateway.token Bearer token for
|
|
271
|
+
├── gateway.token Bearer token for daemon HTTP / WS
|
|
212
272
|
├── claude/
|
|
213
273
|
│ └── <profile>.pid prevents double-launch of the same profile
|
|
214
274
|
└── channels/
|
|
@@ -219,23 +279,23 @@ Persistent state lives under `~/.funnel/`. Volatile logs and the event store liv
|
|
|
219
279
|
|
|
220
280
|
/tmp/funnel/
|
|
221
281
|
├── events/events.db SQLite event store with replay-by-seq
|
|
222
|
-
├── funnel.log diagnostic log (
|
|
282
|
+
├── funnel.log diagnostic log (daemon lifecycle, listener boot, connects)
|
|
223
283
|
└── gateway.log daemon stdout/stderr
|
|
224
284
|
```
|
|
225
285
|
|
|
226
|
-
Notes
|
|
286
|
+
Notes:
|
|
227
287
|
|
|
228
288
|
- 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
|
-
- `
|
|
289
|
+
- `fnl gateway logs` tails `funnel.log` and renders it as YAML.
|
|
230
290
|
|
|
231
291
|
## Environment variables
|
|
232
292
|
|
|
233
293
|
| Variable | Purpose |
|
|
234
294
|
| ---------------------- | --------------------------------------------------------------------------------------------- |
|
|
235
295
|
| `FUNNEL_CHANNEL_ID` | Injected into the child process by `fnl claude`; the funnel MCP uses it to subscribe. |
|
|
236
|
-
| `FUNNEL_PORT` |
|
|
237
|
-
| `FUNNEL_GATEWAY_URL` |
|
|
238
|
-
| `FUNNEL_GATEWAY_TOKEN` | Bearer token for the
|
|
296
|
+
| `FUNNEL_PORT` | Daemon port (default 9742). |
|
|
297
|
+
| `FUNNEL_GATEWAY_URL` | Daemon base URL used by MCP for both WS subscribe and HTTP reply (default `http://localhost:9742`). |
|
|
298
|
+
| `FUNNEL_GATEWAY_TOKEN` | Bearer token for the daemon HTTP / WS. Defaults to the contents of `~/.funnel/gateway.token`. |
|
|
239
299
|
|
|
240
300
|
## Discord bot setup
|
|
241
301
|
|
|
@@ -293,7 +353,7 @@ await server.stop()
|
|
|
293
353
|
unsubscribe()
|
|
294
354
|
```
|
|
295
355
|
|
|
296
|
-
The
|
|
356
|
+
The daemon exposes `/health`, `/status`, `/listeners*`, `/channels/:channel/connectors/:connector/call`, plus the `/ws?channel=<name>` WebSocket.
|
|
297
357
|
|
|
298
358
|
### Sandboxed Funnel
|
|
299
359
|
|
|
@@ -381,9 +441,9 @@ The published package ships a bundled library entry (`dist/index.js`) plus gener
|
|
|
381
441
|
|
|
382
442
|
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
443
|
|
|
384
|
-
Project-scoped (auto)
|
|
444
|
+
Project-scoped (auto): if you run `claude` inside this repo, the skill is picked up automatically — no install step.
|
|
385
445
|
|
|
386
|
-
Global (use the skill in any project)
|
|
446
|
+
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
447
|
|
|
388
448
|
```bash
|
|
389
449
|
# from a clone of this repo
|