@interactive-inc/claude-funnel 0.10.1 → 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.
- package/README.md +106 -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/package.json +2 -1
- package/schemas/funnel.schema.json +144 -0
- package/dist/slack-event-processor-CS-bAit9.d.ts +0 -43
package/README.md
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
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
|
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
81
|
+
fnl claude --profile cto # cd + sub-agent + channel binding in one shot
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
|
|
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
|
|
137
|
+
Multiple agents on the same source — pick the delivery mode:
|
|
95
138
|
|
|
96
139
|
```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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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 }
|
|
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
|
|
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 (
|
|
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
|
-
- `
|
|
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` |
|
|
237
|
-
| `FUNNEL_GATEWAY_URL` |
|
|
238
|
-
| `FUNNEL_GATEWAY_TOKEN` | Bearer token for the
|
|
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
|
|
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)
|
|
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)
|
|
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
|