@interactive-inc/claude-funnel 0.25.2 → 0.26.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 CHANGED
@@ -34,13 +34,13 @@ external sources outbound replies
34
34
  agent (subscribes to one channel)
35
35
  ```
36
36
 
37
- Three concepts make up the model:
37
+ Two concepts make up the transport model:
38
38
 
39
39
  Channel — a named subscription box (transport only). Holds one or more connectors and a delivery mode; it does not carry launch flags. 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
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 saved launch preset. Bundles `{ path, channelId, options, env, resume }` so `fnl claude --profile cto` reproduces a known setup: which directory to launch from, which channel to bind, and the launch recipe (args prepended to the claude argv, env layered under the process, session reuse). The first profile in the list is the default.
43
+ Profile sits on top of that model as a launch convenience, not part of it you never need one to run an agent (`fnl claude --channel <name>` is enough). It is a saved launch preset bundling `{ path, channelId, options, env, resume }` so `fnl claude --profile cto` reproduces a known setup: which directory to launch from, which channel to bind, and the launch recipe (args prepended to the claude argv, env layered under the process, session reuse). A profile carries a stable uuid `id` (the unit the PID file and the resumable session key off, so renaming it strands neither); `name` is just the handle you type. The first profile in the list is the default. Because a profile already binds a channel, `--profile` and `--channel` cannot be combined.
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
 
@@ -116,13 +116,11 @@ Or drop a `funnel.json` in the repo and `fnl claude` (no args) inside the repo w
116
116
  ],
117
117
  "profiles": [
118
118
  {
119
- "name": "ops-pm",
120
119
  "channel": "ops",
121
120
  "options": ["--brief", "--agent", "pm"],
122
121
  "env": { "ANTHROPIC_MODEL": "claude-sonnet-4-6" }
123
122
  },
124
123
  {
125
- "name": "review-reviewer",
126
124
  "channel": "review",
127
125
  "options": ["--agent", "reviewer"]
128
126
  }
@@ -208,7 +206,7 @@ fnl profiles remove <name>
208
206
 
209
207
  fnl claude launch the first channel from ./funnel.json, or the default profile
210
208
  fnl claude --channel <name> with funnel.json: pick that channel; without: raw launch
211
- fnl claude --profile <name> launch a named profile (ignores funnel.json)
209
+ fnl claude --profile <name> launch a named profile (ignores funnel.json; cannot combine with --channel)
212
210
  fnl claude [...] positionals and any flag other than -p / --profile / --channel
213
211
  (e.g. --agent, --resume, -c, --model) pass through to claude
214
212
  fnl mcp run as an MCP server (invoked from .mcp.json)
@@ -265,10 +263,13 @@ Connector =
265
263
  | { type: "discord", name, botToken } Discord Gateway
266
264
  | { type: "schedule", name, entries[] } cron-driven; entries = { id, cron, prompt, enabled?, catchupPolicy? }
267
265
 
268
- Profile = { name, path, channelId, options[], env, resume }
266
+ Profile = { id, name, path, channelId, options[], env, resume, sessionId? }
269
267
  named launch preset: where to launch (path), which channel to bind, and the launch recipe —
270
268
  options[] prepends to the claude argv, env layers under the process (process.env wins on
271
- collision), resume toggles session reuse. the first profile is the default.
269
+ collision), resume toggles session reuse. the first profile is the default. id is a stable
270
+ uuid (the key the PID file and resumable session hang off, so a rename strands neither);
271
+ name is the CLI handle. sessionId is execution state, not config — the claude session this
272
+ profile last launched, written by the launcher and read back on the next resume.
272
273
 
273
274
  LocalConfig = { channels: ChannelSpec[], profiles?: ProfileSpec[] }
274
275
  per-repo file (funnel.json). channels[] required; first entry is default, --channel selects.
@@ -279,16 +280,17 @@ ChannelSpec = { name, connectors? }
279
280
  literal, an env-var reference at `env.<field>` resolved from process.env and ./.env.local, or
280
281
  omission for a TTY prompt persisted to ~/.funnel.
281
282
 
282
- ProfileSpec = { name, channel, options?, env?, resume? }
283
+ ProfileSpec = { channel, options?, env?, resume? }
283
284
  launch recipe bound to a channel by name. applied inline on launch (the first spec bound to the
284
- chosen channel); not persisted into the global profiles[] list.
285
+ chosen channel — selected by its channel binding, not by name); not persisted into the global
286
+ profiles[] list.
285
287
 
286
288
  Settings = { channels[], profiles[] } → ~/.funnel/settings.json
287
289
  ```
288
290
 
289
291
  ## File layout
290
292
 
291
- Persistent state lives under `~/.funnel/`. Volatile logs and the event store live under `/tmp/funnel/`.
293
+ Persistent state lives under `~/.funnel/`. Volatile logs and the event log live under `/tmp/funnel/`.
292
294
 
293
295
  ```
294
296
  ~/.funnel/
@@ -296,7 +298,7 @@ Persistent state lives under `~/.funnel/`. Volatile logs and the event store liv
296
298
  ├── gateway.pid daemon PID
297
299
  ├── gateway.token Bearer token for daemon HTTP / WS
298
300
  ├── claude/
299
- │ └── <profile>.pid prevents double-launch of the same profile
301
+ │ └── <profile-id>.pid prevents double-launch of the same profile (keyed by profile id)
300
302
  └── channels/
301
303
  └── <channel-id>/
302
304
  └── connectors/
@@ -304,7 +306,7 @@ Persistent state lives under `~/.funnel/`. Volatile logs and the event store liv
304
306
  └── state.json per-connector durable state (e.g. schedule lastFiredAt)
305
307
 
306
308
  /tmp/funnel/
307
- ├── events.db SQLite event store with replay-by-seq
309
+ ├── events.db SQLite event log with replay-by-offset
308
310
  ├── funnel.log diagnostic log (daemon lifecycle, listener boot, connects)
309
311
  └── gateway.log daemon stdout/stderr
310
312
  ```
@@ -372,13 +374,21 @@ Run the gateway in-process (no daemon spawn — useful for tests or embedding):
372
374
  ```ts
373
375
  const server = funnel.gatewayServer({ port: 9742 })
374
376
  await server.start() // Bun.serve (HTTP + WS) + listener supervisor
375
- const unsubscribe = server.getBroadcaster().subscribe(({ content, meta }) => {
376
- console.log(meta?.connector, content)
377
+ const unsubscribe = server.onEvent(({ content, meta }) => {
378
+ console.log(meta?.connector, content) // in-process observer for every broadcast event
377
379
  })
378
380
  await server.stop()
379
381
  unsubscribe()
380
382
  ```
381
383
 
384
+ Persistence and replay live behind the `FunnelEventLog` port. The default is a `SqliteFunnelEventLog` (durable across daemon restarts: it seeds the broadcaster's offset and serves reconnect replay). Inject `MemoryFunnelEventLog` — or any `FunnelEventLog` — to swap or disable durable replay; `onEvent` is a separate, write-only observation hook and does not replace it:
385
+
386
+ ```ts
387
+ import { MemoryFunnelEventLog } from "@interactive-inc/claude-funnel"
388
+
389
+ const server = funnel.gatewayServer({ port: 9742, eventLog: new MemoryFunnelEventLog() })
390
+ ```
391
+
382
392
  The daemon exposes `/health`, `/status`, `/listeners*`, `/channels/:channel/connectors/:connector/call`, plus the `/ws?channel=<name>` WebSocket.
383
393
 
384
394
  ### Sandboxed Funnel