@interactive-inc/claude-funnel 0.53.0 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +3 -3
  2. package/dist/bin.js +1276 -520
  3. package/dist/claude.d.ts +22 -5
  4. package/dist/claude.js +455 -168
  5. package/dist/{connector-adapter-CePYBTgW.d.ts → connector-adapter-1PxjN-Uk.d.ts} +1 -1
  6. package/dist/{connector-adapter-D5Utumgz.js → connector-adapter-qwXLjQId.js} +1 -1
  7. package/dist/{connector-listener-DU54DN-f.js → connector-listener-CpHBecCj.js} +1 -1
  8. package/dist/connectors/discord.d.ts +6 -6
  9. package/dist/connectors/discord.js +2 -2
  10. package/dist/connectors/gh.d.ts +6 -6
  11. package/dist/connectors/gh.js +2 -2
  12. package/dist/connectors/schedule.d.ts +12 -2
  13. package/dist/connectors/schedule.js +2 -2
  14. package/dist/connectors/slack.d.ts +3 -3
  15. package/dist/connectors/slack.js +2 -2
  16. package/dist/{connector-diagnostic-log-yTOojKUR.d.ts → diagnostic-log-Bxe7Bbvw.d.ts} +2 -2
  17. package/dist/diagnostic-sql-reader-CzYgZpq2.js +83 -0
  18. package/dist/diagnostics.d.ts +2 -0
  19. package/dist/diagnostics.js +2 -0
  20. package/dist/{discord-connector-schema-CBDyGdOI.js → discord-connector-schema-B_N6IXLz.js} +1 -1
  21. package/dist/{discord-connector-schema-R0Uu-3ns.d.ts → discord-connector-schema-CPgcZkXh.d.ts} +1 -1
  22. package/dist/{discord-listener-_jSE3HsQ.js → discord-listener-C0MoKdQO.js} +6 -6
  23. package/dist/docs.d.ts +2 -0
  24. package/dist/docs.js +2 -0
  25. package/dist/doctor.d.ts +2 -0
  26. package/dist/doctor.js +2 -0
  27. package/dist/{file-process-guard-DMeLB6Zd.d.ts → file-process-guard-DI1742H5.d.ts} +5 -4
  28. package/dist/funnel-diagnostics-BpKYrMSu.js +300 -0
  29. package/dist/funnel-diagnostics-qWy5tPSq.d.ts +176 -0
  30. package/dist/funnel-docs-dXPokzr5.d.ts +18 -0
  31. package/dist/funnel-docs-ng5K8w4j.js +653 -0
  32. package/dist/funnel-doctor-BF3Rdgk0.d.ts +34 -0
  33. package/dist/funnel-doctor-CApCezTq.js +82 -0
  34. package/dist/funnel-recovery-BUBsu7WX.d.ts +101 -0
  35. package/dist/funnel-recovery-D9CxD5Zs.js +134 -0
  36. package/dist/gateway/daemon.js +810 -211
  37. package/dist/{gateway-base-url-ssk_He5G.js → gateway-base-url-6foMXfFf.js} +5 -5
  38. package/dist/gateway.d.ts +2 -2
  39. package/dist/gateway.js +2 -2
  40. package/dist/{gh-connector-schema-eoTtHbY6.d.ts → gh-connector-schema-CU1ojfIF.d.ts} +1 -1
  41. package/dist/{gh-connector-schema-o3Q1-ojL.js → gh-connector-schema-DUcZgN2Q.js} +1 -1
  42. package/dist/{gh-listener-DH-fClQm.js → gh-listener-Dsx6AmhH.js} +5 -5
  43. package/dist/{index-DF5VmCPJ.d.ts → index-CrngHrne.d.ts} +104 -607
  44. package/dist/index.d.ts +16 -11
  45. package/dist/index.js +508 -972
  46. package/dist/{local-config-json-schema-D8i-BogY.js → local-config-json-schema-DE1zkMcb.js} +12 -8
  47. package/dist/{local-config-sync-Cq39mT6p.d.ts → local-config-sync-B8b04LrZ.d.ts} +21 -16
  48. package/dist/local-config.d.ts +2 -2
  49. package/dist/local-config.js +2 -2
  50. package/dist/{memory-connector-diagnostic-log-COUWCsT_.js → memory-diagnostic-log-BZ1VD80X.js} +26 -95
  51. package/dist/{memory-token-prompter-CKV7VBM5.d.ts → memory-token-prompter-Lo3YRDzq.d.ts} +4 -4
  52. package/dist/{memory-token-prompter-Q7Snwsv2.js → memory-token-prompter-vBXxY20-.js} +2 -2
  53. package/dist/{profiles-f0mNmEyP.d.ts → profiles-EHTeCOqB.d.ts} +3 -2
  54. package/dist/profiles.d.ts +1 -1
  55. package/dist/profiles.js +1 -1
  56. package/dist/recovery.d.ts +2 -0
  57. package/dist/recovery.js +2 -0
  58. package/dist/{resolve-connector-token-BHmZLRrV.js → resolve-connector-token-CczqG_Ig.js} +1 -1
  59. package/dist/{schedule-connector-schema-iCI61gzU.js → schedule-connector-schema-B_xO5z5B.js} +1 -1
  60. package/dist/{schedule-listener-CUyUFFR1.d.ts → schedule-listener-DKh0hnkK.d.ts} +5 -5
  61. package/dist/{schedule-listener-ePAjians.js → schedule-listener-DP9Jhc6U.js} +14 -4
  62. package/dist/settings-reader-CBrgz01o.d.ts +18 -0
  63. package/dist/{settings-reader-BSU6JyvM.d.ts → settings-schema-zhnMIa8I.d.ts} +1 -16
  64. package/dist/{slack-connector-schema-BCNWluHM.js → slack-connector-schema-C1zEf4TG.js} +1 -1
  65. package/dist/{slack-listener-Bv5xI9gC.d.ts → slack-listener-COQA8wAZ.d.ts} +4 -4
  66. package/dist/{slack-listener-ClQuHhEF.js → slack-listener-DUKPcpJH.js} +7 -7
  67. package/dist/{mcp-QeNCBhOD.js → yaml-render-OhUN-qkS.js} +52 -34
  68. package/package.json +21 -1
  69. /package/dist/{file-system-BeOKXjlV.d.ts → file-system-Wub9Nto4.d.ts} +0 -0
  70. /package/dist/{process-runner-DfniuWVU.d.ts → process-runner-D5I_jhYQ.d.ts} +0 -0
  71. /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 };