@interactive-inc/claude-funnel 0.53.0 → 0.56.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 +3 -3
- package/dist/bin.js +1229 -486
- package/dist/claude.d.ts +22 -5
- package/dist/claude.js +455 -168
- package/dist/{connector-adapter-CePYBTgW.d.ts → connector-adapter-1PxjN-Uk.d.ts} +1 -1
- package/dist/{connector-adapter-D5Utumgz.js → connector-adapter-qwXLjQId.js} +1 -1
- package/dist/{connector-listener-DU54DN-f.js → connector-listener-CpHBecCj.js} +1 -1
- package/dist/connectors/discord.d.ts +6 -6
- package/dist/connectors/discord.js +2 -2
- package/dist/connectors/gh.d.ts +6 -6
- package/dist/connectors/gh.js +2 -2
- package/dist/connectors/schedule.d.ts +12 -2
- package/dist/connectors/schedule.js +2 -2
- package/dist/connectors/slack.d.ts +3 -3
- package/dist/connectors/slack.js +2 -2
- package/dist/{connector-diagnostic-log-yTOojKUR.d.ts → diagnostic-log-Bxe7Bbvw.d.ts} +2 -2
- package/dist/diagnostic-sql-reader-CzYgZpq2.js +83 -0
- package/dist/diagnostics.d.ts +2 -0
- package/dist/diagnostics.js +2 -0
- package/dist/{discord-connector-schema-CBDyGdOI.js → discord-connector-schema-B_N6IXLz.js} +1 -1
- package/dist/{discord-connector-schema-R0Uu-3ns.d.ts → discord-connector-schema-CPgcZkXh.d.ts} +1 -1
- package/dist/{discord-listener-_jSE3HsQ.js → discord-listener-C0MoKdQO.js} +6 -6
- package/dist/docs.d.ts +2 -0
- package/dist/docs.js +2 -0
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +2 -0
- package/dist/{file-process-guard-DMeLB6Zd.d.ts → file-process-guard-DI1742H5.d.ts} +5 -4
- package/dist/funnel-diagnostics-BpKYrMSu.js +300 -0
- package/dist/funnel-diagnostics-qWy5tPSq.d.ts +176 -0
- package/dist/funnel-docs-dXPokzr5.d.ts +18 -0
- package/dist/funnel-docs-ng5K8w4j.js +653 -0
- package/dist/funnel-doctor-BF3Rdgk0.d.ts +34 -0
- package/dist/funnel-doctor-CApCezTq.js +82 -0
- package/dist/funnel-recovery-BUBsu7WX.d.ts +101 -0
- package/dist/funnel-recovery-D9CxD5Zs.js +134 -0
- package/dist/gateway/daemon.js +838 -252
- package/dist/{gateway-base-url-ssk_He5G.js → gateway-base-url-6foMXfFf.js} +5 -5
- package/dist/gateway.d.ts +2 -2
- package/dist/gateway.js +2 -2
- package/dist/{gh-connector-schema-eoTtHbY6.d.ts → gh-connector-schema-CU1ojfIF.d.ts} +1 -1
- package/dist/{gh-connector-schema-o3Q1-ojL.js → gh-connector-schema-DUcZgN2Q.js} +1 -1
- package/dist/{gh-listener-DH-fClQm.js → gh-listener-Dsx6AmhH.js} +5 -5
- package/dist/{index-DF5VmCPJ.d.ts → index-CrngHrne.d.ts} +104 -607
- package/dist/index.d.ts +16 -11
- package/dist/index.js +509 -973
- package/dist/{local-config-json-schema-D8i-BogY.js → local-config-json-schema-DE1zkMcb.js} +12 -8
- package/dist/{local-config-sync-Cq39mT6p.d.ts → local-config-sync-B8b04LrZ.d.ts} +21 -16
- package/dist/local-config.d.ts +2 -2
- package/dist/local-config.js +2 -2
- package/dist/{memory-connector-diagnostic-log-COUWCsT_.js → memory-diagnostic-log-BbFVqDzz.js} +30 -95
- package/dist/{memory-token-prompter-CKV7VBM5.d.ts → memory-token-prompter-Lo3YRDzq.d.ts} +4 -4
- package/dist/{memory-token-prompter-Q7Snwsv2.js → memory-token-prompter-vBXxY20-.js} +2 -2
- package/dist/{profiles-f0mNmEyP.d.ts → profiles-EHTeCOqB.d.ts} +3 -2
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.js +1 -1
- package/dist/recovery.d.ts +2 -0
- package/dist/recovery.js +2 -0
- package/dist/{resolve-connector-token-BHmZLRrV.js → resolve-connector-token-CczqG_Ig.js} +1 -1
- package/dist/{schedule-connector-schema-iCI61gzU.js → schedule-connector-schema-B_xO5z5B.js} +1 -1
- package/dist/{schedule-listener-CUyUFFR1.d.ts → schedule-listener-DKh0hnkK.d.ts} +5 -5
- package/dist/{schedule-listener-ePAjians.js → schedule-listener-DP9Jhc6U.js} +14 -4
- package/dist/settings-reader-CBrgz01o.d.ts +18 -0
- package/dist/{settings-reader-BSU6JyvM.d.ts → settings-schema-zhnMIa8I.d.ts} +1 -16
- package/dist/{slack-connector-schema-BCNWluHM.js → slack-connector-schema-C1zEf4TG.js} +1 -1
- package/dist/{slack-listener-Bv5xI9gC.d.ts → slack-listener-COQA8wAZ.d.ts} +4 -4
- package/dist/{slack-listener-ClQuHhEF.js → slack-listener-DUKPcpJH.js} +7 -7
- package/dist/{mcp-QeNCBhOD.js → yaml-render-OhUN-qkS.js} +52 -34
- package/package.json +21 -1
- /package/dist/{file-system-BeOKXjlV.d.ts → file-system-Wub9Nto4.d.ts} +0 -0
- /package/dist/{process-runner-DfniuWVU.d.ts → process-runner-D5I_jhYQ.d.ts} +0 -0
- /package/dist/{profiles-wMRnjSid.js → profiles-MnXvYfZF.js} +0 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
//#endregion
|
|
2
|
+
//#region lib/services/docs/funnel-docs.ts
|
|
3
|
+
const DOCS = {
|
|
4
|
+
architecture: `funnel docs architecture — how Funnel routes events
|
|
5
|
+
|
|
6
|
+
Funnel is a hub between external sources (Slack / GitHub / Discord / cron) and
|
|
7
|
+
Claude Code agents. Events flow one way; replies flow back via MCP tools.
|
|
8
|
+
|
|
9
|
+
external sources → daemon → channel → agent (MCP)
|
|
10
|
+
↑ ← outbound replies (MCP tools)
|
|
11
|
+
|
|
12
|
+
layers (dependency direction):
|
|
13
|
+
|
|
14
|
+
engine ← connectors ← gateway ← cli
|
|
15
|
+
↖ bin.ts → funnel.ts (facade)
|
|
16
|
+
|
|
17
|
+
engine core domain (channels, profiles, settings, mcp, local-config)
|
|
18
|
+
connectors slack / gh / discord / schedule implementations
|
|
19
|
+
gateway Bun.serve hosting WS + internal HTTP, listener supervisor,
|
|
20
|
+
broadcaster, event log (SQLite by default)
|
|
21
|
+
cli argv → internal HTTP requests → Hono routes
|
|
22
|
+
|
|
23
|
+
runtime processes:
|
|
24
|
+
|
|
25
|
+
daemon long-lived gateway in its own process; spawned by fnl claude;
|
|
26
|
+
shared across Claude sessions and repos
|
|
27
|
+
in-process same gateway hosted inside the embedding process; for tests
|
|
28
|
+
and custom hosts; observe events with onEvent()
|
|
29
|
+
|
|
30
|
+
storage roots:
|
|
31
|
+
|
|
32
|
+
~/.funnel/ global daemon state (PID, token, settings)
|
|
33
|
+
~/.funnel/projects/<id>/ per-repo state when funnel.json exists
|
|
34
|
+
/tmp/funnel/ event store + connector diagnostic SQLite
|
|
35
|
+
|
|
36
|
+
key invariants:
|
|
37
|
+
|
|
38
|
+
- listener and adapter are separate one-way pipes (broadcaster sees only
|
|
39
|
+
inbound events, not outbound tool calls)
|
|
40
|
+
- per-repo state is fully isolated from global state — they never mix
|
|
41
|
+
- URL building is type-safe via channelWsUrl / gatewayLoopbackUrl helpers
|
|
42
|
+
|
|
43
|
+
related: fnl docs channels, fnl docs profiles, fnl docs mcp`,
|
|
44
|
+
channels: `funnel docs channels — what a channel is
|
|
45
|
+
|
|
46
|
+
A channel is a subscription mailbox. It owns connectors and decides how events
|
|
47
|
+
fan out to subscribers.
|
|
48
|
+
|
|
49
|
+
shape:
|
|
50
|
+
|
|
51
|
+
{ id, name, delivery, connectors[] }
|
|
52
|
+
|
|
53
|
+
delivery modes:
|
|
54
|
+
|
|
55
|
+
fanout every subscriber receives every event. Use when each subscriber
|
|
56
|
+
has its own job (multiple profiles processing the same source,
|
|
57
|
+
observer clients tapping in).
|
|
58
|
+
exclusive one event goes to one subscriber, round-robin. Use when
|
|
59
|
+
subscribers are interchangeable workers and each event must be
|
|
60
|
+
processed exactly once.
|
|
61
|
+
|
|
62
|
+
what a channel does NOT own:
|
|
63
|
+
- launch options (those live on Profile)
|
|
64
|
+
- sessions (those live on Profile)
|
|
65
|
+
- tokens (those live in per-repo settings)
|
|
66
|
+
|
|
67
|
+
operations:
|
|
68
|
+
|
|
69
|
+
fnl channels list channels
|
|
70
|
+
fnl channels <name> show one channel
|
|
71
|
+
fnl channels add <name> create a channel
|
|
72
|
+
fnl channels remove <name> delete a channel
|
|
73
|
+
fnl channels rename <old> <new> rename a channel
|
|
74
|
+
fnl channels <name> set delivery <mode> change delivery to fanout|exclusive
|
|
75
|
+
fnl channels <name> publish publish a synthetic event
|
|
76
|
+
fnl channels <name> validate sanity-check a channel
|
|
77
|
+
|
|
78
|
+
connectors live nested inside a channel — see fnl docs connectors.
|
|
79
|
+
|
|
80
|
+
related: fnl docs connectors, fnl docs profiles`,
|
|
81
|
+
claude: `funnel docs claude — launching Claude Code through funnel
|
|
82
|
+
|
|
83
|
+
\`fnl claude\` spawns Claude with the channel subscription wired in. The
|
|
84
|
+
resolution order is:
|
|
85
|
+
|
|
86
|
+
1. --help / -h print help
|
|
87
|
+
2. --profile + --channel error (mutually exclusive)
|
|
88
|
+
3. --profile <name> use named profile (global first,
|
|
89
|
+
then cwd funnel.json profiles[])
|
|
90
|
+
4. funnel.json exists + --channel <name> bind transport, no recipe
|
|
91
|
+
5. funnel.json exists, no --channel bind first channel in channels[]
|
|
92
|
+
6. no funnel.json + --channel <name> raw launch
|
|
93
|
+
7. default global profile launch
|
|
94
|
+
8. nothing matches print help
|
|
95
|
+
|
|
96
|
+
argv assembly when spawning Claude:
|
|
97
|
+
|
|
98
|
+
[profile.options] [user CLI args] [MCP server flag]
|
|
99
|
+
|
|
100
|
+
Same flag specified twice → last one wins.
|
|
101
|
+
env assembled as: profile.env merged with process.env (process.env wins).
|
|
102
|
+
|
|
103
|
+
side effects on first launch in a repo:
|
|
104
|
+
|
|
105
|
+
- writes funnel.json's top-level "id" (uuid) if missing — used to isolate
|
|
106
|
+
state under ~/.funnel/projects/<id>/
|
|
107
|
+
- installs an entry into the repo's .mcp.json (does not touch other entries)
|
|
108
|
+
- if a connector has no token, TTY-prompts and saves to per-repo settings
|
|
109
|
+
|
|
110
|
+
double-launch guard:
|
|
111
|
+
|
|
112
|
+
Same profile cannot be launched twice — protected by a PID file under
|
|
113
|
+
~/.funnel/projects/<id>/profiles/<profile-id>/pid. Stale PID files are
|
|
114
|
+
cleaned up automatically when the recorded process is gone.
|
|
115
|
+
|
|
116
|
+
related: fnl docs profiles, fnl docs mcp, fnl docs local-config`,
|
|
117
|
+
connectors: `funnel docs connectors — external service bindings
|
|
118
|
+
|
|
119
|
+
A connector is one connection to one external service. It is nested inside a
|
|
120
|
+
channel — a channel can host several connectors of different types.
|
|
121
|
+
|
|
122
|
+
types:
|
|
123
|
+
|
|
124
|
+
slack Socket Mode listener + REST adapter
|
|
125
|
+
gh GitHub polling listener + REST adapter
|
|
126
|
+
discord Discord listener + REST adapter
|
|
127
|
+
schedule cron-style tick listener (no adapter — schedule does not send out)
|
|
128
|
+
|
|
129
|
+
per-type parts:
|
|
130
|
+
|
|
131
|
+
Listener external → Funnel inbound. push (Slack), pull (GitHub), or tick
|
|
132
|
+
(Schedule cron).
|
|
133
|
+
Adapter Claude → external outbound. only on callable types.
|
|
134
|
+
Schema config validation.
|
|
135
|
+
EventProcessor shapes raw events into the channel payload.
|
|
136
|
+
|
|
137
|
+
operations (all nested under a channel):
|
|
138
|
+
|
|
139
|
+
fnl channels <ch> connectors list
|
|
140
|
+
fnl channels <ch> connectors <name> show
|
|
141
|
+
fnl channels <ch> connectors add <name> --type=… create
|
|
142
|
+
fnl channels <ch> connectors set <name> … update
|
|
143
|
+
fnl channels <ch> connectors remove <name> delete
|
|
144
|
+
fnl channels <ch> connectors rename <old> <new> rename
|
|
145
|
+
fnl channels <ch> connectors <name> request … proxy an HTTP call to the
|
|
146
|
+
adapter (e.g. send a Slack
|
|
147
|
+
message via Claude path)
|
|
148
|
+
fnl channels <ch> connectors <name> schedules … manage cron entries on a
|
|
149
|
+
schedule connector
|
|
150
|
+
|
|
151
|
+
tokens:
|
|
152
|
+
|
|
153
|
+
funnel.json must NOT contain tokens. Set them via:
|
|
154
|
+
|
|
155
|
+
fnl channels <ch> connectors set <name> --bot-token-env=SLACK_BOT_TOKEN
|
|
156
|
+
|
|
157
|
+
…or leave unset and fnl claude will TTY-prompt at first launch and persist
|
|
158
|
+
to ~/.funnel/projects/<id>/settings.json.
|
|
159
|
+
|
|
160
|
+
related: fnl docs channels, fnl docs debugging`,
|
|
161
|
+
debugging: `funnel docs debugging — diagnose and self-heal in one shot
|
|
162
|
+
|
|
163
|
+
Funnel ships with a single entry point that diagnoses every channel and, when
|
|
164
|
+
asked, applies safe self-healing fixes. Use this from the CLI (fnl doctor),
|
|
165
|
+
the MCP server (fnl_doctor tool), or the SDK (funnel.doctor.run()).
|
|
166
|
+
|
|
167
|
+
── the one command Claude needs ─────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
fnl doctor read-only diagnosis
|
|
170
|
+
fnl doctor --fix --json diagnose + apply safe fixes (idempotent)
|
|
171
|
+
fnl doctor --fix --aggressive also restart the gateway if needed
|
|
172
|
+
|
|
173
|
+
Return shape (same for CLI --json, MCP, SDK):
|
|
174
|
+
|
|
175
|
+
{ status: "ok" | "warn" | "error",
|
|
176
|
+
message: "...",
|
|
177
|
+
appliedActions: [
|
|
178
|
+
{ kind: "gateway:started" }
|
|
179
|
+
| { kind: "gateway:already-running" }
|
|
180
|
+
| { kind: "gateway:restarted" }
|
|
181
|
+
| { kind: "listener:restarted", channel, connector }
|
|
182
|
+
| { kind: "listener:skipped", channel, connector, reason }
|
|
183
|
+
],
|
|
184
|
+
remainingIssues: [
|
|
185
|
+
{ channel, diagnosis: { status, message, nextActions, rootCause } }
|
|
186
|
+
],
|
|
187
|
+
before: <diagnoseAll snapshot before fixing>,
|
|
188
|
+
after: <diagnoseAll snapshot after fixing> }
|
|
189
|
+
|
|
190
|
+
When status is "ok" you are done. Otherwise read remainingIssues for what
|
|
191
|
+
is still broken — usually a hint Claude can act on (configure a missing
|
|
192
|
+
connector, prompt the user to run \`fnl gateway start\` in a shell, etc).
|
|
193
|
+
|
|
194
|
+
── what fnl doctor --fix will and will not do ──────────────────────────────
|
|
195
|
+
|
|
196
|
+
Will (safe):
|
|
197
|
+
- start the gateway if it is down (when run as a CLI; MCP cannot do this)
|
|
198
|
+
- restart every dead listener across every channel
|
|
199
|
+
|
|
200
|
+
Will (aggressive, only with --aggressive):
|
|
201
|
+
- also restart the gateway when safe fixes are not enough
|
|
202
|
+
|
|
203
|
+
Will not:
|
|
204
|
+
- create channels / connectors / profiles
|
|
205
|
+
- rotate tokens
|
|
206
|
+
- change persistent config
|
|
207
|
+
|
|
208
|
+
── deeper inspection (rarely needed) ───────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
fnl debug events --channel <name> processed events with outcome
|
|
211
|
+
(emitted | skip:type | skip:dedup | …)
|
|
212
|
+
fnl debug dropped --channel <name> skip:* events only
|
|
213
|
+
fnl debug errors --channel <name> listener auth-failed / error events
|
|
214
|
+
fnl debug replay --channel <name> replay a past event to test a fix
|
|
215
|
+
|
|
216
|
+
fnl gateway logs daemon log stream
|
|
217
|
+
fnl gateway sql --preset recent ad-hoc SQL queries
|
|
218
|
+
|
|
219
|
+
── from inside MCP ─────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
Claude in any repo calls these tools without leaving MCP:
|
|
222
|
+
|
|
223
|
+
fnl_doctor == fnl doctor (mode: off | safe | aggressive)
|
|
224
|
+
fnl_status == lightweight snapshot
|
|
225
|
+
fnl_debug == per-channel diagnosis
|
|
226
|
+
fnl_recent_events == fnl debug events
|
|
227
|
+
fnl_dropped_events == fnl debug dropped
|
|
228
|
+
fnl_connection_errors == fnl debug errors
|
|
229
|
+
fnl_replay_event == fnl debug replay
|
|
230
|
+
fnl_docs == fnl docs
|
|
231
|
+
|
|
232
|
+
── programmable API ───────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
const funnel = new Funnel()
|
|
235
|
+
await funnel.doctor.run() // read-only diagnosis
|
|
236
|
+
await funnel.doctor.run("safe") // diagnose + safe fixes
|
|
237
|
+
await funnel.doctor.run("aggressive") // diagnose + safe + gateway restart
|
|
238
|
+
|
|
239
|
+
// Building blocks (rarely needed; doctor orchestrates them)
|
|
240
|
+
await funnel.diagnostics.diagnoseAll()
|
|
241
|
+
await funnel.recovery.ensureGatewayRunning()
|
|
242
|
+
await funnel.recovery.restartAllDeadListeners()
|
|
243
|
+
|
|
244
|
+
CLI, MCP, and the SDK share exactly one implementation — the CLI is a thin
|
|
245
|
+
presentation layer over funnel.doctor.
|
|
246
|
+
|
|
247
|
+
related: fnl docs gateway, fnl docs mcp, fnl docs recipes`,
|
|
248
|
+
gateway: `funnel docs gateway — the WebSocket + HTTP daemon
|
|
249
|
+
|
|
250
|
+
The gateway is a long-lived Bun.serve process that hosts every realtime
|
|
251
|
+
boundary in funnel. Without it: store edits work, but no events flow and
|
|
252
|
+
Claude cannot send anything outbound.
|
|
253
|
+
|
|
254
|
+
what runs inside:
|
|
255
|
+
|
|
256
|
+
WebSocket / channel subscriptions (subprotocol auth)
|
|
257
|
+
HTTP /health /status liveness and supervisor snapshot
|
|
258
|
+
HTTP /listeners* listener lifecycle
|
|
259
|
+
HTTP /channels/<n>/call outbound dispatch (Claude → adapter → external)
|
|
260
|
+
Listener Supervisor starts / stops / restarts listeners with
|
|
261
|
+
exponential backoff (cap 60s)
|
|
262
|
+
Broadcaster fans events out to WS clients and records
|
|
263
|
+
offsets to the event log
|
|
264
|
+
FunnelEventLog persistent replay log; default is
|
|
265
|
+
SqliteFunnelEventLog under /tmp/funnel/
|
|
266
|
+
|
|
267
|
+
ports:
|
|
268
|
+
|
|
269
|
+
9742 in-process / embedded (Funnel.gatewayServer())
|
|
270
|
+
9743 CLI launches (fnl claude / fnl gateway start)
|
|
271
|
+
FUNNEL_PORT overrides both
|
|
272
|
+
|
|
273
|
+
bind:
|
|
274
|
+
|
|
275
|
+
127.0.0.1 (loopback) by default — unreachable off-box.
|
|
276
|
+
FUNNEL_HOST=0.0.0.0 exposes the gateway; in that mode every privileged
|
|
277
|
+
endpoint requires a bearer token, and an unprotected start() throws.
|
|
278
|
+
|
|
279
|
+
operations:
|
|
280
|
+
|
|
281
|
+
fnl gateway overview
|
|
282
|
+
fnl gateway status running? pid? port? uptime?
|
|
283
|
+
fnl gateway start spawn daemon and wait until /health responds
|
|
284
|
+
fnl gateway stop stop daemon
|
|
285
|
+
fnl gateway restart stop + start
|
|
286
|
+
fnl gateway run run gateway in the foreground (no daemonize)
|
|
287
|
+
fnl gateway listeners list listeners with alive / event / error
|
|
288
|
+
counts
|
|
289
|
+
fnl gateway logs stream raw daemon logs
|
|
290
|
+
fnl gateway sql --preset recent query the diagnostic SQLite stores
|
|
291
|
+
|
|
292
|
+
when do you need the gateway?
|
|
293
|
+
|
|
294
|
+
needs gateway: fnl claude, MCP inbound, MCP outbound (channel call),
|
|
295
|
+
fnl debug (reads /status + /tmp/funnel/*.db)
|
|
296
|
+
no gateway: fnl channels add / remove, fnl profiles add, fnl schema
|
|
297
|
+
|
|
298
|
+
related: fnl docs debugging, fnl docs architecture`,
|
|
299
|
+
glossary: `funnel docs glossary — vocabulary reference
|
|
300
|
+
|
|
301
|
+
Channel
|
|
302
|
+
Subscription mailbox. Holds connectors and decides fan-out behavior.
|
|
303
|
+
See: fnl docs channels.
|
|
304
|
+
|
|
305
|
+
Connector
|
|
306
|
+
One binding to one external service (slack | gh | discord | schedule),
|
|
307
|
+
nested inside a channel. Has a Listener and (when callable) an Adapter.
|
|
308
|
+
See: fnl docs connectors.
|
|
309
|
+
|
|
310
|
+
Profile
|
|
311
|
+
Saved launch preset for Claude. Binds a channel and carries argv / env /
|
|
312
|
+
resume / sessionId. Not required to launch.
|
|
313
|
+
See: fnl docs profiles.
|
|
314
|
+
|
|
315
|
+
LocalConfig
|
|
316
|
+
The funnel.json file at a repo root. Declares channels + profiles for that
|
|
317
|
+
repo and gets an "id" uuid stamped on first launch.
|
|
318
|
+
See: fnl docs local-config.
|
|
319
|
+
|
|
320
|
+
Gateway
|
|
321
|
+
Long-lived Bun.serve daemon hosting WebSocket + internal HTTP + listener
|
|
322
|
+
supervisor + broadcaster. Required for any realtime flow.
|
|
323
|
+
See: fnl docs gateway.
|
|
324
|
+
|
|
325
|
+
Listener
|
|
326
|
+
External → Funnel inbound side of a connector. Push (Slack), pull (GitHub),
|
|
327
|
+
or tick (Schedule). Supervised, auto-restarted with backoff.
|
|
328
|
+
|
|
329
|
+
Adapter
|
|
330
|
+
Claude → external outbound side of a connector. Reached via MCP tool calls
|
|
331
|
+
that hit the gateway over HTTP and dispatch through the adapter.
|
|
332
|
+
|
|
333
|
+
Broadcaster
|
|
334
|
+
In-gateway component that takes notifies, records them to the event log,
|
|
335
|
+
fans them out to WS subscribers.
|
|
336
|
+
|
|
337
|
+
FunnelEventLog
|
|
338
|
+
Persistent replay log (default: SQLite). Enables resubscribers to catch up
|
|
339
|
+
from a saved offset via ?since=<N>.
|
|
340
|
+
|
|
341
|
+
Subscriber ID
|
|
342
|
+
A WS client identifier. Events with meta.target=<id> route to that
|
|
343
|
+
subscriber only — used for per-Claude targeting.
|
|
344
|
+
|
|
345
|
+
Delivery mode
|
|
346
|
+
fanout (every subscriber gets every event) or exclusive (round-robin one-
|
|
347
|
+
event-one-subscriber). Set on the channel.
|
|
348
|
+
|
|
349
|
+
MCP
|
|
350
|
+
Model Context Protocol. Funnel hosts an MCP server (fnl mcp) that Claude
|
|
351
|
+
Code launches; events flow in as notifications, outbound calls flow out as
|
|
352
|
+
tool invocations.
|
|
353
|
+
See: fnl docs mcp.
|
|
354
|
+
|
|
355
|
+
related: fnl docs architecture`,
|
|
356
|
+
"local-config": `funnel docs local-config — the per-repo funnel.json
|
|
357
|
+
|
|
358
|
+
funnel.json lives at the repo root and is committed alongside the code. It
|
|
359
|
+
declares channels (transport) and profiles (launch recipes) so a clone of the
|
|
360
|
+
repo can launch Claude with the same wiring.
|
|
361
|
+
|
|
362
|
+
shape:
|
|
363
|
+
|
|
364
|
+
LocalConfig = { id?, channels: ChannelSpec[], profiles?: ProfileSpec[] }
|
|
365
|
+
ChannelSpec = { name, connectors? }
|
|
366
|
+
ProfileSpec = { name, channel, options?, env?, resume? }
|
|
367
|
+
|
|
368
|
+
what funnel writes back to funnel.json:
|
|
369
|
+
|
|
370
|
+
Only the top-level "id" (uuid), written once on first launch. It is the
|
|
371
|
+
state-isolation key — every funnel state for this repo lives under
|
|
372
|
+
~/.funnel/projects/<id>/. Renaming the repo or moving it does not break
|
|
373
|
+
this binding.
|
|
374
|
+
|
|
375
|
+
what funnel never writes to funnel.json:
|
|
376
|
+
|
|
377
|
+
tokens. funnel.json is commit-safe. Tokens live in:
|
|
378
|
+
|
|
379
|
+
~/.funnel/projects/<id>/settings.json per-repo, set via CLI or TTY prompt
|
|
380
|
+
|
|
381
|
+
how it is read:
|
|
382
|
+
|
|
383
|
+
All CLI commands check for funnel.json in cwd. If found, FUNNEL_DIR is
|
|
384
|
+
pointed at the per-repo root before anything else loads, so routing,
|
|
385
|
+
dispatchClaude, MCP, and the daemon all share the same state.
|
|
386
|
+
|
|
387
|
+
generating the schema for editors:
|
|
388
|
+
|
|
389
|
+
fnl schema > funnel.schema.json
|
|
390
|
+
|
|
391
|
+
# then in funnel.json:
|
|
392
|
+
{ "$schema": "./funnel.schema.json", ... }
|
|
393
|
+
|
|
394
|
+
related: fnl docs profiles, fnl docs channels, fnl schema`,
|
|
395
|
+
mcp: `funnel docs mcp — the MCP server inside funnel
|
|
396
|
+
|
|
397
|
+
\`fnl mcp\` is invoked by Claude Code via .mcp.json (auto-installed when fnl
|
|
398
|
+
claude runs in a repo). It hosts two independent pipes over one stdio MCP
|
|
399
|
+
server.
|
|
400
|
+
|
|
401
|
+
inbound (events → Claude):
|
|
402
|
+
|
|
403
|
+
- WebSocket subscribes to the gateway on the channel set by FUNNEL_CHANNEL_ID
|
|
404
|
+
- forwards messages as MCP notifications under method
|
|
405
|
+
"notifications/claude/channel"
|
|
406
|
+
- capability "experimental: { claude/channel: {} }" must be enabled
|
|
407
|
+
- auto-reconnects with exponential backoff and replays missed events using
|
|
408
|
+
?since=<offset> against the gateway's persistent event log
|
|
409
|
+
|
|
410
|
+
outbound (Claude → external):
|
|
411
|
+
|
|
412
|
+
- reads the channel's connectors at startup
|
|
413
|
+
- exposes one MCP tool per connector (one tool = one connector name)
|
|
414
|
+
- tool args: { method, path, body? }
|
|
415
|
+
- tool calls become HTTP POST to the gateway's channel call endpoint with
|
|
416
|
+
Bearer auth; the JSON response is returned to Claude verbatim (no bash hop)
|
|
417
|
+
- schedule connectors are excluded (no outbound side)
|
|
418
|
+
|
|
419
|
+
built-in diagnostic tool:
|
|
420
|
+
|
|
421
|
+
- funnel_diagnose
|
|
422
|
+
returns the same structured report as \`fnl debug --all --json\`.
|
|
423
|
+
Call this when events stop arriving, when a tool call fails, or before
|
|
424
|
+
asking the user to check anything. No arguments required.
|
|
425
|
+
|
|
426
|
+
env contract:
|
|
427
|
+
|
|
428
|
+
FUNNEL_CHANNEL_ID set by launcher; without it the inbound side is no-op
|
|
429
|
+
FUNNEL_DIR state root; set by the launcher for repo-scoped runs
|
|
430
|
+
FUNNEL_PORT gateway port; default 9743 for CLI launch, 9742 for
|
|
431
|
+
embedded use
|
|
432
|
+
|
|
433
|
+
operations:
|
|
434
|
+
|
|
435
|
+
fnl mcp run as MCP server (do not invoke manually — Claude
|
|
436
|
+
launches it via .mcp.json)
|
|
437
|
+
|
|
438
|
+
related: fnl docs claude, fnl docs debugging`,
|
|
439
|
+
profiles: `funnel docs profiles — launch presets for Claude
|
|
440
|
+
|
|
441
|
+
A profile is a saved recipe for launching Claude bound to a specific channel.
|
|
442
|
+
Profiles are NOT required — \`fnl claude --channel <name>\` works without one.
|
|
443
|
+
Use profiles when you want to save options / env / resume settings.
|
|
444
|
+
|
|
445
|
+
shape:
|
|
446
|
+
|
|
447
|
+
{ id, name, path, channelId, options[], env, resume, sessionId? }
|
|
448
|
+
|
|
449
|
+
id uuid primary key; survives rename (used as the key for PID file
|
|
450
|
+
and sessionId so renaming a profile does not orphan its state)
|
|
451
|
+
name display label; what --profile <name> matches
|
|
452
|
+
path cwd to enter before spawning Claude
|
|
453
|
+
channelId the channel this profile binds
|
|
454
|
+
options argv prepended to claude (e.g. --agent, --brief, --model)
|
|
455
|
+
env env vars for Claude; process.env wins on collision
|
|
456
|
+
resume whether to reuse the saved sessionId on next launch
|
|
457
|
+
sessionId execution state — last spawned Claude session id (not config)
|
|
458
|
+
|
|
459
|
+
global vs local:
|
|
460
|
+
|
|
461
|
+
global profiles ~/.funnel/settings.json → --profile <name> always works
|
|
462
|
+
local profiles funnel.json profiles[] → per-repo recipe, only resolved
|
|
463
|
+
when cwd contains the file
|
|
464
|
+
|
|
465
|
+
mutual exclusion:
|
|
466
|
+
|
|
467
|
+
--profile and --channel are mutually exclusive. A profile already binds a
|
|
468
|
+
channel, so combining them is an error.
|
|
469
|
+
|
|
470
|
+
operations:
|
|
471
|
+
|
|
472
|
+
fnl profiles list
|
|
473
|
+
fnl profiles add <name> create
|
|
474
|
+
fnl profiles set <name> update
|
|
475
|
+
fnl profiles remove <name> delete
|
|
476
|
+
fnl profiles rename <old> <new> rename (id-stable)
|
|
477
|
+
fnl profiles <name> as-default mark as default for fnl claude
|
|
478
|
+
fnl profiles <name> run spawn Claude using this profile
|
|
479
|
+
fnl claude --profile <name> equivalent to <name> run
|
|
480
|
+
|
|
481
|
+
related: fnl docs claude, fnl docs channels`,
|
|
482
|
+
"programmable-api": `funnel docs programmable-api — Funnel as an SDK
|
|
483
|
+
|
|
484
|
+
Everything funnel does as a CLI is also a programmable API. Build your own CLI,
|
|
485
|
+
TUI, web UI, or service on top of the same engine — the CLI itself is a thin
|
|
486
|
+
Hono app over these services.
|
|
487
|
+
|
|
488
|
+
── installation ────────────────────────────────────────────────────────────
|
|
489
|
+
|
|
490
|
+
npm install @interactive-inc/claude-funnel
|
|
491
|
+
|
|
492
|
+
── facade ──────────────────────────────────────────────────────────────────
|
|
493
|
+
|
|
494
|
+
import { Funnel } from "@interactive-inc/claude-funnel"
|
|
495
|
+
|
|
496
|
+
const funnel = new Funnel() // uses ~/.funnel
|
|
497
|
+
const sandbox = Funnel.inMemory() // touches no disk / process / clock
|
|
498
|
+
|
|
499
|
+
funnel.paths // { dir, tmpDir, settings }
|
|
500
|
+
funnel.channels // CRUD on channels + nested connectors
|
|
501
|
+
funnel.profiles // CRUD on launch presets
|
|
502
|
+
funnel.localConfig // funnel.json read / write
|
|
503
|
+
funnel.localConfigSync // funnel.json → ~/.funnel sync
|
|
504
|
+
funnel.gateway // daemon lifecycle (start/stop/status)
|
|
505
|
+
funnel.gatewayToken // bearer token mint/read
|
|
506
|
+
funnel.publisher // POST /channels/:name/publish
|
|
507
|
+
funnel.listeners // listener registry control
|
|
508
|
+
funnel.claude // FunnelClaude (launch Claude Code)
|
|
509
|
+
funnel.diagnostics // read-side diagnosis (no mutation)
|
|
510
|
+
funnel.recovery // self-healing actions
|
|
511
|
+
funnel.docs // embedded documentation
|
|
512
|
+
|
|
513
|
+
── independent service classes (compose freely) ────────────────────────────
|
|
514
|
+
|
|
515
|
+
Each service depends on narrow interfaces, so you can wire them outside the
|
|
516
|
+
facade if you want a lighter-weight integration. The Funnel facade is the
|
|
517
|
+
recommended path — but it is just one composition root, not the only one.
|
|
518
|
+
|
|
519
|
+
import {
|
|
520
|
+
FunnelChannels,
|
|
521
|
+
FunnelDiagnostics,
|
|
522
|
+
FunnelRecovery,
|
|
523
|
+
FunnelDocs,
|
|
524
|
+
FunnelGatewayServer,
|
|
525
|
+
MemoryFunnelFileSystem,
|
|
526
|
+
MemoryFunnelProcessRunner,
|
|
527
|
+
MemoryFunnelClock,
|
|
528
|
+
} from "@interactive-inc/claude-funnel"
|
|
529
|
+
|
|
530
|
+
── sub-entry imports ───────────────────────────────────────────────────────
|
|
531
|
+
|
|
532
|
+
For targeted imports (smaller bundle / clearer dependency footprint):
|
|
533
|
+
|
|
534
|
+
import { FunnelGatewayServer } from "@interactive-inc/claude-funnel/gateway"
|
|
535
|
+
import { FunnelProfiles } from "@interactive-inc/claude-funnel/profiles"
|
|
536
|
+
import { FunnelLocalConfig } from "@interactive-inc/claude-funnel/local-config"
|
|
537
|
+
|
|
538
|
+
── building your own CLI ───────────────────────────────────────────────────
|
|
539
|
+
|
|
540
|
+
The Funnel CLI is a Hono app you can embed:
|
|
541
|
+
|
|
542
|
+
import { cliRoutes, toRequest } from "@interactive-inc/claude-funnel"
|
|
543
|
+
import { Funnel } from "@interactive-inc/claude-funnel"
|
|
544
|
+
|
|
545
|
+
const funnel = new Funnel()
|
|
546
|
+
const { method, url } = toRequest(process.argv.slice(2))
|
|
547
|
+
const res = await cliRoutes.request(url, { method }, { funnel })
|
|
548
|
+
|
|
549
|
+
Or skip the routing layer entirely and call the services directly:
|
|
550
|
+
|
|
551
|
+
await funnel.diagnostics.diagnoseAll()
|
|
552
|
+
await funnel.recovery.restartAllDeadListeners()
|
|
553
|
+
|
|
554
|
+
── testing ─────────────────────────────────────────────────────────────────
|
|
555
|
+
|
|
556
|
+
Every IO boundary has a Memory implementation. Wire a sandboxed Funnel for
|
|
557
|
+
fast, hermetic tests:
|
|
558
|
+
|
|
559
|
+
import { Funnel } from "@interactive-inc/claude-funnel"
|
|
560
|
+
const funnel = Funnel.inMemory()
|
|
561
|
+
|
|
562
|
+
funnel.channels.add({ name: "ops" })
|
|
563
|
+
expect(funnel.channels.list()).toHaveLength(1)
|
|
564
|
+
|
|
565
|
+
related: fnl docs architecture, fnl docs debugging, fnl docs mcp`,
|
|
566
|
+
recipes: `funnel docs recipes — common task playbooks
|
|
567
|
+
|
|
568
|
+
— bootstrap a new repo —
|
|
569
|
+
|
|
570
|
+
cd my-repo
|
|
571
|
+
fnl channels add ops
|
|
572
|
+
fnl channels ops connectors add slack-main --type=slack --bot-token-env=SLACK_BOT_TOKEN
|
|
573
|
+
fnl claude # auto-installs .mcp.json, launches Claude
|
|
574
|
+
|
|
575
|
+
— add a second Slack workspace to one channel —
|
|
576
|
+
|
|
577
|
+
fnl channels ops connectors add slack-eu --type=slack --bot-token-env=SLACK_EU_TOKEN
|
|
578
|
+
|
|
579
|
+
Both connectors push events into the same channel; subscribers see all.
|
|
580
|
+
|
|
581
|
+
— two profiles sharing one channel —
|
|
582
|
+
|
|
583
|
+
fnl channels add support
|
|
584
|
+
fnl profiles add triage --channel=support --options=--agent,triage
|
|
585
|
+
fnl profiles add resolve --channel=support --options=--agent,resolver
|
|
586
|
+
fnl claude --profile triage # in one terminal
|
|
587
|
+
fnl claude --profile resolve # in another
|
|
588
|
+
|
|
589
|
+
Pick channel delivery=exclusive so each event goes to exactly one of them.
|
|
590
|
+
|
|
591
|
+
— schedule a daily cron prompt —
|
|
592
|
+
|
|
593
|
+
fnl channels add daily
|
|
594
|
+
fnl channels daily connectors add cron --type=schedule
|
|
595
|
+
fnl channels daily connectors cron schedules add morning \\
|
|
596
|
+
--cron="0 9 * * *" --prompt="summarize yesterday's PRs"
|
|
597
|
+
|
|
598
|
+
— diagnose "events stopped arriving" —
|
|
599
|
+
|
|
600
|
+
fnl debug --all --json
|
|
601
|
+
# read diagnosis.rootCause and diagnosis.nextActions[] from the result
|
|
602
|
+
|
|
603
|
+
— replay a real event to test a code change —
|
|
604
|
+
|
|
605
|
+
fnl debug events --channel ops --limit 5
|
|
606
|
+
fnl debug replay --channel ops --seq <pick from above>
|
|
607
|
+
|
|
608
|
+
— recover from a stuck gateway —
|
|
609
|
+
|
|
610
|
+
fnl gateway logs # confirm symptoms first
|
|
611
|
+
fnl gateway restart
|
|
612
|
+
fnl debug --all --json # verify everything came back up
|
|
613
|
+
|
|
614
|
+
related: fnl docs debugging, fnl docs channels, fnl docs profiles`
|
|
615
|
+
};
|
|
616
|
+
const SUMMARIES = {
|
|
617
|
+
architecture: "how Funnel routes events end-to-end",
|
|
618
|
+
channels: "what a channel is and how delivery modes work",
|
|
619
|
+
connectors: "external service bindings nested in channels",
|
|
620
|
+
profiles: "named Claude launch presets",
|
|
621
|
+
claude: "fnl claude resolution order and argv assembly",
|
|
622
|
+
mcp: "the MCP server, inbound notifications, outbound tools",
|
|
623
|
+
gateway: "the WebSocket + HTTP daemon",
|
|
624
|
+
"local-config": "the per-repo funnel.json file",
|
|
625
|
+
debugging: "ladder for diagnosing event delivery problems",
|
|
626
|
+
"programmable-api": "Funnel as an SDK; build your own CLI/UI on the engine",
|
|
627
|
+
recipes: "common task playbooks",
|
|
628
|
+
glossary: "vocabulary reference"
|
|
629
|
+
};
|
|
630
|
+
/**
|
|
631
|
+
* Programmable docs surface — used by both the CLI (fnl docs <topic>) and the
|
|
632
|
+
* MCP / SDK consumers. Docs are embedded into the build so a Claude session
|
|
633
|
+
* can self-discover funnel's vocabulary without external network access.
|
|
634
|
+
*/
|
|
635
|
+
var FunnelDocs = class {
|
|
636
|
+
constructor() {
|
|
637
|
+
Object.freeze(this);
|
|
638
|
+
}
|
|
639
|
+
list() {
|
|
640
|
+
return Object.keys(DOCS).sort().map((name) => ({
|
|
641
|
+
name,
|
|
642
|
+
summary: SUMMARIES[name] ?? ""
|
|
643
|
+
}));
|
|
644
|
+
}
|
|
645
|
+
get(topic) {
|
|
646
|
+
return DOCS[topic] ?? null;
|
|
647
|
+
}
|
|
648
|
+
topics() {
|
|
649
|
+
return Object.keys(DOCS).sort();
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
//#endregion
|
|
653
|
+
export { FunnelDocs as t };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { c as FunnelDiagnostics, n as DiagnoseAllReport, t as ChannelDiagnosis } from "./funnel-diagnostics-qWy5tPSq.js";
|
|
2
|
+
import { n as RecoveryAction, t as FunnelRecovery } from "./funnel-recovery-BUBsu7WX.js";
|
|
3
|
+
|
|
4
|
+
//#region lib/services/doctor/funnel-doctor.d.ts
|
|
5
|
+
type Props = {
|
|
6
|
+
diagnostics: FunnelDiagnostics;
|
|
7
|
+
recovery: FunnelRecovery;
|
|
8
|
+
};
|
|
9
|
+
type DoctorFixMode = /** No mutations. Run diagnoseAll and report what would be fixed. */"off" /** Apply only idempotent, low-risk fixes — start the gateway, restart dead listeners. */ | "safe" /** Add high-impact fixes — full gateway restart when partial fixes are insufficient. */ | "aggressive";
|
|
10
|
+
type DoctorReport = {
|
|
11
|
+
/** "ok" if every channel is healthy after the run; "warn" if anything is still off; "error" if a fix step itself failed. */status: "ok" | "warn" | "error"; /** Human-readable summary suitable for stdout. */
|
|
12
|
+
message: string; /** Aggregated diagnosis after any fix pass. */
|
|
13
|
+
after: DiagnoseAllReport; /** Diagnosis before fixing, only included when fix mode is not "off". */
|
|
14
|
+
before: DiagnoseAllReport | null; /** Each recovery action that ran during the fix pass. */
|
|
15
|
+
appliedActions: RecoveryAction[]; /** Channels still unhealthy after the fix pass (or all unhealthy channels when fix is off). */
|
|
16
|
+
remainingIssues: Array<{
|
|
17
|
+
channel: string;
|
|
18
|
+
diagnosis: ChannelDiagnosis["diagnosis"];
|
|
19
|
+
}>;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* One-shot diagnose-and-fix entry point. The CLI exposes this as `fnl doctor`,
|
|
23
|
+
* the MCP server exposes it as `fnl_doctor`. Both surfaces should prefer this
|
|
24
|
+
* over chaining FunnelDiagnostics and FunnelRecovery by hand — the orchestration
|
|
25
|
+
* logic (which fixes to attempt, in what order, when to escalate) lives here.
|
|
26
|
+
*/
|
|
27
|
+
declare class FunnelDoctor {
|
|
28
|
+
private readonly props;
|
|
29
|
+
constructor(props: Props);
|
|
30
|
+
run(mode?: DoctorFixMode): Promise<DoctorReport>;
|
|
31
|
+
private buildReport;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { DoctorReport as n, FunnelDoctor as r, DoctorFixMode as t };
|