@interactive-inc/claude-funnel 0.41.0 → 0.49.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/dist/index.d.ts CHANGED
@@ -69,7 +69,7 @@ type SlackListenerOptions = {
69
69
  type ScheduleListenerOptions = {
70
70
  onFired?: ScheduleOnFired;
71
71
  };
72
- type Deps$15 = {
72
+ type Deps$13 = {
73
73
  fs?: FunnelFileSystem;
74
74
  process?: FunnelProcessRunner;
75
75
  logger?: FunnelLogger;
@@ -98,7 +98,7 @@ declare class FunnelConnectorFactory {
98
98
  private readonly dir;
99
99
  private readonly slackListenerOptions;
100
100
  private readonly scheduleListenerOptions;
101
- constructor(deps?: Deps$15);
101
+ constructor(deps?: Deps$13);
102
102
  createListener(channelId: string, config: ConnectorConfig): FunnelConnectorListener;
103
103
  createAdapter(config: ConnectorConfig): FunnelConnectorAdapter | null;
104
104
  connectorDir(channelId: string, connectorId: string): string;
@@ -290,10 +290,10 @@ declare abstract class FunnelSettingsReader {
290
290
  }
291
291
  //#endregion
292
292
  //#region lib/engine/channels/channels.d.ts
293
- type Deps$14 = {
293
+ type Deps$12 = {
294
294
  store: FunnelSettingsReader;
295
295
  factory: FunnelConnectorFactory;
296
- profileChecker: ProfileChannelChecker;
296
+ profileChecker?: ProfileChannelChecker;
297
297
  clock?: FunnelClock;
298
298
  idGenerator?: FunnelIdGenerator;
299
299
  };
@@ -337,7 +337,7 @@ declare class FunnelChannels {
337
337
  private readonly profileChecker;
338
338
  private readonly clock;
339
339
  private readonly idGenerator;
340
- constructor(deps: Deps$14);
340
+ constructor(deps: Deps$12);
341
341
  list(): ChannelConfig[];
342
342
  get(name: string): ChannelConfig | null;
343
343
  getById(id: string): ChannelConfig | null;
@@ -403,9 +403,8 @@ type OnFunnelError = (error: Error, context?: Record<string, unknown>) => void;
403
403
  //#region lib/gateway/broadcaster.d.ts
404
404
  type ClientData = {
405
405
  /** Stable channel id (uuid) that the WS client subscribed to. */channel: string; /** Human-facing channel name resolved at upgrade time, kept for log readability. */
406
- channelName?: string | null; /** Connector names belonging to that channel; used by tap-all replay filtering. */
407
- connectors: string[];
408
- tapAll?: boolean; /** Routing mode resolved from channel config at upgrade time. Defaults to fanout. */
406
+ channelName?: string | null; /** Connector names belonging to that channel. */
407
+ connectors: string[]; /** Routing mode resolved from channel config at upgrade time. Defaults to fanout. */
409
408
  delivery?: "fanout" | "exclusive";
410
409
  /**
411
410
  * Opaque per-client id declared at upgrade time (`?id=<subscriberId>`). When an
@@ -433,7 +432,7 @@ type BroadcastSubscriber = (event: ReplayableEvent) => void;
433
432
  type ReplaySource = {
434
433
  loadSince(since: number): ReplayableEvent[];
435
434
  };
436
- type Deps$13 = {
435
+ type Deps$11 = {
437
436
  logger?: FunnelLogger; /** Host hook for surfacing subscriber-throw exceptions. Defaults to no-op. */
438
437
  onError?: OnFunnelError;
439
438
  maxBufferedBytes?: number;
@@ -485,7 +484,7 @@ declare class FunnelBroadcaster {
485
484
  private droppedSlowClients;
486
485
  private lastBroadcastAt;
487
486
  private latestOffset;
488
- constructor(deps?: Deps$13);
487
+ constructor(deps?: Deps$11);
489
488
  getMetrics(): BroadcasterMetrics;
490
489
  /**
491
490
  * Returns events with offset > since, filtered by the connector subscription
@@ -502,15 +501,12 @@ declare class FunnelBroadcaster {
502
501
  replaySince(since: number, data: ClientData): ReplayableEvent[];
503
502
  private matchesClient;
504
503
  /**
505
- * Returns the list of WS clients that should receive `event`. Tap=all clients always
506
- * receive (passive observation). For each per-channel group:
504
+ * Returns the list of WS clients that should receive `event`. For each per-channel group:
507
505
  * - fanout → every matching client receives
508
506
  * - exclusive → exactly one client receives, picked round-robin per channel
509
507
  *
510
- * `meta.target` narrows the regular (non-tap) recipient set first via
511
- * `matchesClient`: only the subscriber whose `subscriberId` equals `target`
512
- * stays in the running, so a targeted event reaches one named instance while
513
- * still being observable by tap=all clients.
508
+ * `meta.target` narrows the recipient set via `matchesClient`: only the subscriber
509
+ * whose `subscriberId` equals `target` receives a targeted event.
514
510
  */
515
511
  private pickRecipients;
516
512
  addClient(ws: ServerWebSocket<unknown>, data: ClientData): void;
@@ -536,7 +532,7 @@ type ConnectorRegistry = {
536
532
  } | null;
537
533
  };
538
534
  type SupervisorNotify = (channelName: string, connectorName: string, content: string, meta?: Record<string, string>) => Promise<void>;
539
- type Deps$12 = {
535
+ type Deps$10 = {
540
536
  channels: ConnectorRegistry;
541
537
  notify: SupervisorNotify;
542
538
  logger?: FunnelLogger; /** Host hook for surfacing listener lifecycle exceptions. Defaults to no-op. */
@@ -583,7 +579,7 @@ declare class FunnelListenerSupervisor {
583
579
  private readonly now;
584
580
  private healthCheckTimer;
585
581
  private healthCheckInFlight;
586
- constructor(deps: Deps$12);
582
+ constructor(deps: Deps$10);
587
583
  static keyOf(channelName: string, connectorName: string): string;
588
584
  isRunning(channelName: string, connectorName: string): boolean;
589
585
  list(): ListenerEntryStatus[];
@@ -956,6 +952,12 @@ declare function buildGatewayRoutes(): _$hono_hono_base0.HonoBase<Env$1, {
956
952
  };
957
953
  }, "/", "/channels/:channel/publish">;
958
954
  //#endregion
955
+ //#region lib/engine/claude/channel-resolver.d.ts
956
+ type ChannelResolver = {
957
+ get(name: string): ChannelConfig | null;
958
+ getById(id: string): ChannelConfig | null;
959
+ };
960
+ //#endregion
959
961
  //#region lib/engine/claude/gateway-controller.d.ts
960
962
  type GatewayController = {
961
963
  isRunning(): boolean;
@@ -964,78 +966,25 @@ type GatewayController = {
964
966
  }): Promise<boolean>;
965
967
  };
966
968
  //#endregion
967
- //#region lib/engine/mcp/mcp.d.ts
968
- declare const FUNNEL_MCP_COMMAND = "bun";
969
- declare const FUNNEL_MCP_ARGS: string[];
970
- declare const FUNNEL_MCP_NAME = "funnel";
971
- type Deps$11 = {
972
- fs?: FunnelFileSystem;
973
- };
974
- /**
975
- * Installs/uninstalls the funnel MCP entry into a target repository's
976
- * `.mcp.json`. Detects an existing entry by command match so renaming is
977
- * preserved across re-installs.
978
- */
979
- declare class FunnelMcp {
980
- private readonly fs;
981
- constructor(deps?: Deps$11);
982
- install(repoPath: string): void;
983
- uninstall(repoPath: string): void;
969
+ //#region lib/engine/claude/mcp-installer.d.ts
970
+ type McpInstaller = {
984
971
  findInstalledName(cwd: string): string | null;
985
- private findServerName;
986
- private isFunnelEntry;
987
- private readConfig;
988
- private writeConfig;
989
- }
972
+ install(cwd: string): void;
973
+ };
990
974
  //#endregion
991
- //#region lib/engine/profiles/profiles.d.ts
992
- type Deps$10 = {
993
- store: FunnelSettingsReader;
994
- idGenerator: FunnelIdGenerator;
975
+ //#region lib/engine/claude/process-guard.d.ts
976
+ type ProcessGuard = {
977
+ /** Returns true if a live process is already registered for this profile. */isRunning(profileId: string): boolean; /** Writes the PID file and registers an exit hook to clean it up. */
978
+ acquire(profileId: string): void; /** Removes the PID file. */
979
+ release(profileId: string): void;
980
+ };
981
+ //#endregion
982
+ //#region lib/engine/claude/session-store.d.ts
983
+ type SessionStore = {
984
+ getSessionId(profileId: string): string | null;
985
+ setSessionId(profileId: string, sessionId: string): void; /** Returns true when the session jsonl exists on disk and is non-empty. */
986
+ sessionFileExists(cwd: string, sessionId: string, env: Record<string, string>): boolean;
995
987
  };
996
- /**
997
- * Named launch presets for `fnl claude`. Each profile bundles a working
998
- * directory, the channel id its Claude instance subscribes to, and the launch
999
- * recipe (`options` prepended to the claude argv, `env` layered under the
1000
- * process, `resume` toggling session reuse). Implements ProfileChannelChecker
1001
- * so FunnelChannels can refuse to remove a channel that is still referenced.
1002
- *
1003
- * Each profile has a stable `id` (uuid) minted at `add`. That id is the unit
1004
- * everything internal keys on — the PID file, the resumable session id — so a
1005
- * rename never strands either. `name` is purely the CLI/TUI handle; the CRUD
1006
- * methods here take it because that is what the user types, but resolve to the
1007
- * id before touching id-keyed state. The first array entry is the default
1008
- * profile; `asDefault` reorders to put one first.
1009
- *
1010
- * `channelId` always stores the channel's stable id (uuid). CLI surfaces
1011
- * resolve channel name → id before calling `add`/`update` here.
1012
- */
1013
- declare class FunnelProfiles {
1014
- private readonly store;
1015
- private readonly idGenerator;
1016
- constructor(deps: Deps$10);
1017
- list(): ProfileConfig[];
1018
- get(name: string): ProfileConfig | null;
1019
- getById(id: string): ProfileConfig | null;
1020
- getDefault(): ProfileConfig | null;
1021
- add(input: {
1022
- name: string;
1023
- path: string;
1024
- channelId: string;
1025
- options?: string[];
1026
- env?: Record<string, string>;
1027
- resume?: boolean;
1028
- }): void;
1029
- remove(name: string): void;
1030
- rename(oldName: string, newName: string): void;
1031
- asDefault(name: string): void;
1032
- hasChannelRef(channelId: string): boolean;
1033
- /** Resumable claude session id last launched by this profile (by id), or null. */
1034
- getSessionId(id: string): string | null;
1035
- /** Records the claude session id this profile launched, overwriting any prior one. */
1036
- setSessionId(id: string, sessionId: string): void;
1037
- update(name: string, fields: Partial<Omit<ProfileConfig, "name">>): void;
1038
- }
1039
988
  //#endregion
1040
989
  //#region lib/engine/claude/claude.d.ts
1041
990
  type LaunchOptions = {
@@ -1063,41 +1012,32 @@ type LaunchOptions = {
1063
1012
  installMcp?: boolean;
1064
1013
  };
1065
1014
  type Deps$9 = {
1066
- channels: FunnelChannels;
1067
- mcp: FunnelMcp;
1015
+ channels: ChannelResolver;
1016
+ mcp: McpInstaller;
1068
1017
  gateway: GatewayController;
1069
- profiles: FunnelProfiles;
1018
+ sessions: SessionStore;
1019
+ guard: ProcessGuard;
1070
1020
  process?: FunnelProcessRunner;
1071
- fs?: FunnelFileSystem;
1072
1021
  idGenerator?: FunnelIdGenerator;
1073
1022
  logger?: FunnelLogger;
1074
- dir?: string;
1075
1023
  };
1076
1024
  /**
1077
1025
  * Launches Claude Code with funnel pre-wired: ensures the gateway is running,
1078
1026
  * installs the funnel MCP into the target repo's `.mcp.json` if missing,
1079
- * injects `FUNNEL_CHANNEL_ID` into the child env, and writes a per-profile
1080
- * PID file to enforce singleton launches.
1027
+ * injects `FUNNEL_CHANNEL_ID` into the child env, and delegates singleton
1028
+ * enforcement to a ProcessGuard.
1081
1029
  */
1082
1030
  declare class FunnelClaude {
1083
1031
  private readonly channels;
1084
1032
  private readonly mcp;
1085
1033
  private readonly gateway;
1086
- private readonly profiles;
1034
+ private readonly sessions;
1035
+ private readonly guard;
1087
1036
  private readonly process;
1088
- private readonly fs;
1089
1037
  private readonly idGenerator;
1090
1038
  private readonly logger;
1091
- private readonly pidDir;
1092
1039
  constructor(deps: Deps$9);
1093
1040
  launch(options: LaunchOptions): Promise<number>;
1094
- isRunning(profileId: string): boolean;
1095
- private pidPath;
1096
- private readPid;
1097
- private writePidFile;
1098
- private removePidFile;
1099
- private installCleanup;
1100
- private isProcessAlive;
1101
1041
  private buildArgs;
1102
1042
  /**
1103
1043
  * Decides whether funnel should resume an existing claude session or start
@@ -1119,35 +1059,10 @@ declare class FunnelClaude {
1119
1059
  * overwrites the dangling entry — so the store self-heals.
1120
1060
  */
1121
1061
  private resolveSession;
1122
- /**
1123
- * Mirrors claude's session storage path
1124
- * (`<config-dir>/projects/<cwd-with-slashes-as-dashes>/<id>.jsonl`) to check
1125
- * whether a recorded session still exists AND is non-empty. Reads the same
1126
- * `CLAUDE_CONFIG_DIR` the child will run under so the check matches reality; a
1127
- * wrong guess can only ever produce a false negative (start fresh), never a
1128
- * bad resume.
1129
- */
1130
- private sessionFileExists;
1131
1062
  private buildEnv;
1132
1063
  }
1133
1064
  //#endregion
1134
1065
  //#region lib/engine/local-config/local-config-schema.d.ts
1135
- declare const connectorSpecSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
1136
- type: z.ZodLiteral<"slack">;
1137
- name: z.ZodString;
1138
- minify: z.ZodOptional<z.ZodBoolean>;
1139
- }, z.core.$strip>, z.ZodObject<{
1140
- type: z.ZodLiteral<"discord">;
1141
- name: z.ZodString;
1142
- }, z.core.$strip>, z.ZodObject<{
1143
- type: z.ZodLiteral<"gh">;
1144
- name: z.ZodString;
1145
- pollInterval: z.ZodOptional<z.ZodNumber>;
1146
- }, z.core.$strip>, z.ZodObject<{
1147
- type: z.ZodLiteral<"schedule">;
1148
- name: z.ZodString;
1149
- }, z.core.$strip>], "type">;
1150
- type ConnectorSpec = z.infer<typeof connectorSpecSchema>;
1151
1066
  declare const channelSpecSchema: z.ZodObject<{
1152
1067
  name: z.ZodString;
1153
1068
  connectors: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -1167,14 +1082,6 @@ declare const channelSpecSchema: z.ZodObject<{
1167
1082
  }, z.core.$strip>], "type">>>;
1168
1083
  }, z.core.$strip>;
1169
1084
  type ChannelSpec = z.infer<typeof channelSpecSchema>;
1170
- declare const profileSpecSchema: z.ZodObject<{
1171
- name: z.ZodString;
1172
- channel: z.ZodString;
1173
- options: z.ZodOptional<z.ZodArray<z.ZodString>>;
1174
- env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
1175
- resume: z.ZodOptional<z.ZodBoolean>;
1176
- }, z.core.$strip>;
1177
- type ProfileSpec = z.infer<typeof profileSpecSchema>;
1178
1085
  declare const localConfigSchema: z.ZodObject<{
1179
1086
  $schema: z.ZodOptional<z.ZodString>;
1180
1087
  id: z.ZodOptional<z.ZodString>;
@@ -1205,7 +1112,6 @@ declare const localConfigSchema: z.ZodObject<{
1205
1112
  }, z.core.$strip>>>;
1206
1113
  }, z.core.$strip>;
1207
1114
  type LocalConfig = z.infer<typeof localConfigSchema>;
1208
- declare const LOCAL_CONFIG_FILENAME = "funnel.json";
1209
1115
  //#endregion
1210
1116
  //#region lib/engine/local-config/local-config.d.ts
1211
1117
  type Deps$8 = {
@@ -1291,21 +1197,64 @@ declare class FunnelLocalConfigSync {
1291
1197
  private resolveSlot;
1292
1198
  }
1293
1199
  //#endregion
1294
- //#region lib/engine/local-config/local-config-writer.d.ts
1200
+ //#region lib/engine/profiles/profiles.d.ts
1295
1201
  type Deps$6 = {
1296
- fs: FunnelFileSystem;
1202
+ store: FunnelSettingsReader;
1203
+ idGenerator: FunnelIdGenerator;
1204
+ fs?: FunnelFileSystem;
1297
1205
  };
1298
1206
  /**
1299
- * The one path that mutates the repo-committed funnel.json, and it only ever
1300
- * inserts `id`. On first launch a repo has no `id`; funnel generates one and
1301
- * writes it back here so future launches resolve the same `~/.funnel/projects/<id>/`.
1302
- * Idempotent — a no-op once `id` is present. Kept separate from the read-only
1303
- * FunnelLocalConfig so reads stay side-effect free.
1207
+ * Named launch presets for `fnl claude`. Each profile bundles a working
1208
+ * directory, the channel id its Claude instance subscribes to, and the launch
1209
+ * recipe (`options` prepended to the claude argv, `env` layered under the
1210
+ * process, `resume` toggling session reuse). Implements ProfileChannelChecker
1211
+ * so FunnelChannels can refuse to remove a channel that is still referenced.
1212
+ *
1213
+ * Each profile has a stable `id` (uuid) minted at `add`. That id is the unit
1214
+ * everything internal keys on — the PID file, the resumable session id — so a
1215
+ * rename never strands either. `name` is purely the CLI/TUI handle; the CRUD
1216
+ * methods here take it because that is what the user types, but resolve to the
1217
+ * id before touching id-keyed state. The first array entry is the default
1218
+ * profile; `asDefault` reorders to put one first.
1219
+ *
1220
+ * `channelId` always stores the channel's stable id (uuid). CLI surfaces
1221
+ * resolve channel name → id before calling `add`/`update` here.
1304
1222
  */
1305
- declare class FunnelLocalConfigWriter {
1223
+ declare class FunnelProfiles {
1224
+ private readonly store;
1225
+ private readonly idGenerator;
1306
1226
  private readonly fs;
1307
1227
  constructor(deps: Deps$6);
1308
- ensureId(cwd: string, id: string): void;
1228
+ list(): ProfileConfig[];
1229
+ get(name: string): ProfileConfig | null;
1230
+ getById(id: string): ProfileConfig | null;
1231
+ getDefault(): ProfileConfig | null;
1232
+ add(input: {
1233
+ name: string;
1234
+ path: string;
1235
+ channelId: string;
1236
+ options?: string[];
1237
+ env?: Record<string, string>;
1238
+ resume?: boolean;
1239
+ }): void;
1240
+ remove(name: string): void;
1241
+ rename(oldName: string, newName: string): void;
1242
+ asDefault(name: string): void;
1243
+ hasChannelRef(channelId: string): boolean;
1244
+ /** Resumable claude session id last launched by this profile (by id), or null. */
1245
+ getSessionId(id: string): string | null;
1246
+ /** Records the claude session id this profile launched, overwriting any prior one. */
1247
+ setSessionId(id: string, sessionId: string): void;
1248
+ /**
1249
+ * Mirrors claude's session storage path
1250
+ * (`<config-dir>/projects/<cwd-with-slashes-as-dashes>/<id>.jsonl`) to check
1251
+ * whether a recorded session still exists AND is non-empty. Reads the same
1252
+ * `CLAUDE_CONFIG_DIR` the child will run under so the check matches reality; a
1253
+ * wrong guess can only ever produce a false negative (start fresh), never a
1254
+ * bad resume.
1255
+ */
1256
+ sessionFileExists(cwd: string, sessionId: string, env: Record<string, string>): boolean;
1257
+ update(name: string, fields: Partial<Omit<ProfileConfig, "name">>): void;
1309
1258
  }
1310
1259
  //#endregion
1311
1260
  //#region lib/gateway/publish-schema.d.ts
@@ -1463,7 +1412,6 @@ declare class FunnelGateway {
1463
1412
  //#region lib/gateway/gateway-server.d.ts
1464
1413
  type Deps$3 = {
1465
1414
  channels: FunnelChannels;
1466
- settings: FunnelSettingsReader;
1467
1415
  port?: number; /** Bind address for `Bun.serve`. Defaults to `127.0.0.1` (loopback only). Set to `0.0.0.0` to expose on the network. */
1468
1416
  hostname?: string; /** SQLite event store file path. Parent directory is created on demand. Defaults to `<os.tmpdir()>/funnel/events.db`. Ignored when `eventLog` is supplied. */
1469
1417
  dbPath?: string; /** Durable replay log. Defaults to a `SqliteFunnelEventLog` at `dbPath`. Inject a `MemoryFunnelEventLog` (or any `FunnelEventLog`) to swap or disable persistence. */
@@ -1485,10 +1433,9 @@ type Deps$3 = {
1485
1433
  extraRoutes?: Hono<Env$1>;
1486
1434
  };
1487
1435
  type WsData = {
1488
- /** Stable channel id (uuid) the client subscribed to. "" for tap-all clients. */channel: string; /** Resolved channel name (for log readability). null for tap-all or unknown. */
1489
- channelName: string | null; /** Connector names belonging to that channel; used by tap-all replay filtering. */
1490
- connectors: string[];
1491
- tapAll?: boolean; /** Routing mode for this channel; resolved at upgrade time from settings. */
1436
+ /** Stable channel id (uuid) the client subscribed to. */channel: string; /** Resolved channel name (for log readability). null for unknown. */
1437
+ channelName: string | null; /** Connector names belonging to that channel. */
1438
+ connectors: string[]; /** Routing mode for this channel; resolved at upgrade time from settings. */
1492
1439
  delivery: "fanout" | "exclusive"; /** Opaque client id from `?id=<subscriberId>`; lets publishers target this client via `meta.target`. */
1493
1440
  subscriberId?: string; /** Replay any events with offset strictly greater than this on open, then resume the live stream. */
1494
1441
  since?: number;
@@ -1505,7 +1452,6 @@ type WsData = {
1505
1452
  */
1506
1453
  declare class FunnelGatewayServer {
1507
1454
  private readonly channels;
1508
- private readonly settings;
1509
1455
  private readonly port;
1510
1456
  private readonly hostname;
1511
1457
  private readonly dbPath;
@@ -1691,14 +1637,13 @@ type FunnelDebugReport = {
1691
1637
  };
1692
1638
  //#endregion
1693
1639
  //#region lib/funnel.d.ts
1694
- type Props$8 = {
1640
+ type Props$7 = {
1695
1641
  /** Settings persistence (channels with nested connectors / profiles). Defaults to a FunnelSettingsStore rooted at `dir`. */store?: FunnelSettingsReader; /** Filesystem boundary. Replace with MemoryFunnelFileSystem to sandbox all disk I/O. */
1696
1642
  fs?: FunnelFileSystem; /** Process runner used by gateway / claude / gh listener. Replace with MemoryFunnelProcessRunner for tests. */
1697
1643
  process?: FunnelProcessRunner; /** Logger flowed into every facet. Replace with MemoryFunnelLogger or NoopFunnelLogger to silence/inspect. */
1698
1644
  logger?: FunnelLogger; /** Clock used by schedule listener, gh poll watermarks, and gateway timeouts. */
1699
1645
  clock?: FunnelClock; /** ID generator for channel and connector ids. Use MemoryFunnelIdGenerator for deterministic tests. */
1700
- idGenerator?: FunnelIdGenerator; /** Prompter used by FunnelLocalConfigSync when funnel.json omits a token. Defaults to a TTY-only stdin prompter. */
1701
- tokenPrompter?: FunnelTokenPrompter; /** Funnel home directory (settings.json + per-channel/per-connector dirs). Defaults to ~/.funnel. */
1646
+ idGenerator?: FunnelIdGenerator; /** Funnel home directory (settings.json + per-channel/per-connector dirs). Defaults to ~/.funnel. */
1702
1647
  dir?: string; /** Temp / runtime directory (gateway logs and PID adjacent files). Defaults to `<os.tmpdir()>/funnel`. */
1703
1648
  tmpDir?: string;
1704
1649
  /**
@@ -1730,17 +1675,21 @@ type Props$8 = {
1730
1675
  * the default (9742) without setting FUNNEL_PORT in the environment.
1731
1676
  */
1732
1677
  port?: number;
1678
+ /**
1679
+ * Token prompter used by FunnelLocalConfigSync when funnel.json omits a token.
1680
+ * Defaults to a TTY-only stdin prompter. Inject MemoryFunnelTokenPrompter in tests.
1681
+ */
1682
+ tokenPrompter?: FunnelTokenPrompter;
1733
1683
  };
1734
1684
  /**
1735
- * Facade exposing every funnel facet as a getter.
1685
+ * Facade that wires every funnel facet together and exposes the public surface.
1736
1686
  *
1737
- * The same `Funnel` is used by the CLI and as a programmable library.
1738
- * All side-effecting boundaries (filesystem, process, logger, clock, id, paths) are
1739
- * injectable via `Props` — passing memory implementations gives a fully sandboxed
1687
+ * All side-effecting boundaries (filesystem, process, logger, clock, id, paths)
1688
+ * are injected via Props passing memory implementations gives a fully sandboxed
1740
1689
  * Funnel that touches no real disk, processes, or wall-clock time.
1741
1690
  *
1742
- * Connectors live nested inside their owning channel (channels[].connectors[]),
1743
- * so connector CRUD is reached via `funnel.channels.addConnector(...)` etc.
1691
+ * Fully immutable: all fields are resolved in the constructor and frozen.
1692
+ * No lazy initialisation — every dependency is wired at construction time.
1744
1693
  *
1745
1694
  * @example
1746
1695
  * ```ts
@@ -1751,114 +1700,57 @@ type Props$8 = {
1751
1700
  * ```
1752
1701
  */
1753
1702
  declare class Funnel {
1754
- private readonly props;
1755
- private readonly memos;
1756
- constructor(props?: Props$8);
1757
- /**
1758
- * Sandboxed Funnel wired with in-memory implementations for every IO boundary.
1759
- * Touches no real disk, processes, wall-clock time, or UUIDs — safe for tests
1760
- * and ad-hoc experiments. Override individual fields by passing them in `props`.
1761
- */
1762
- static inMemory(props?: Props$8): Funnel;
1763
- /** Resolved on-disk paths the facade will read/write when methods are called. Pure compute, not memoized. */
1764
- get paths(): {
1703
+ readonly paths: {
1765
1704
  dir: string;
1766
1705
  tmpDir: string;
1767
1706
  settings: string;
1768
1707
  };
1769
- /** Filesystem boundary. Defaults to NodeFunnelFileSystem. */
1770
- get fs(): FunnelFileSystem;
1771
- /** Process runner boundary. Defaults to NodeFunnelProcessRunner. */
1772
- get process(): FunnelProcessRunner;
1773
- /** Logger boundary. Optional — when no logger is injected, every facet's `this.logger?.x` call is a silent no-op. Production entry points (cli, daemon) inject a NodeFunnelLogger. */
1774
- get logger(): FunnelLogger | undefined;
1775
- /** Clock boundary. Defaults to NodeFunnelClock. */
1776
- get clock(): FunnelClock;
1777
- /**
1778
- * Error hook. Forwards Funnel-internal exceptions that would otherwise be
1779
- * swallowed. Defaults to a no-op when no host hook was passed.
1780
- */
1781
- get onError(): OnFunnelError;
1782
- /** ID generator boundary. Defaults to NodeFunnelIdGenerator. */
1783
- get idGenerator(): FunnelIdGenerator;
1784
- /** Settings reader. If not injected, a FunnelSettingsStore rooted at `dir` is created. */
1785
- get store(): FunnelSettingsReader;
1786
- /** Pure factory that constructs per-type listeners and adapters from connector configs. */
1787
- get factory(): FunnelConnectorFactory;
1788
- /** Channel CRUD + nested connector CRUD + schedule entries + listener/adapter dispatch. */
1789
- get channels(): FunnelChannels;
1790
- /** Launch profiles (named presets for `fnl claude`: path + sub-agent + channel id). */
1791
- get profiles(): FunnelProfiles;
1792
- /** Reads `funnel.json` from a cwd. `fnl claude` consults it before falling back to the default profile. */
1793
- get localConfig(): FunnelLocalConfig;
1794
- /** Writes the stable `id` into funnel.json on first launch so state can be scoped to `~/.funnel/projects/<id>/`. */
1795
- get localConfigWriter(): FunnelLocalConfigWriter;
1796
- /** Secret prompter. Defaults to a TTY-only stdin reader; tests inject MemoryFunnelTokenPrompter. */
1797
- get tokenPrompter(): FunnelTokenPrompter;
1798
- /** Reconciles funnel.json's channel + connectors with `~/.funnel/settings.json` on launch. */
1799
- get localConfigSync(): FunnelLocalConfigSync;
1800
- /** funnel MCP installer (writes/removes `.mcp.json` entries in target repos). */
1801
- get mcp(): FunnelMcp;
1802
- /** Launch Claude Code with a channel injected via env, MCP installed, gateway ensured. */
1803
- get claude(): FunnelClaude;
1804
- /** Gateway daemon controller (PID-file, start/stop the separate `bun daemon.ts` process). */
1805
- get gateway(): FunnelGateway;
1806
- /** Read / generate the daemon's gateway token (mode 0600 file under `dir`). */
1807
- get gatewayToken(): FunnelGatewayToken;
1808
- /**
1809
- * HTTP client for `POST /channels/:channel/publish` on the running gateway
1810
- * daemon. Use it to push arbitrary content into a channel from outside any
1811
- * connector. Returns `{ state: "offline" }` if the daemon isn't up.
1812
- */
1813
- get publisher(): FunnelChannelPublisher;
1708
+ readonly channels: FunnelChannels;
1709
+ readonly gateway: FunnelGateway;
1710
+ readonly gatewayToken: FunnelGatewayToken;
1711
+ readonly publisher: FunnelChannelPublisher;
1712
+ readonly listeners: FunnelListenersClient;
1713
+ readonly claude: FunnelClaude;
1714
+ readonly profiles: FunnelProfiles;
1715
+ readonly localConfig: FunnelLocalConfig;
1716
+ readonly localConfigSync: FunnelLocalConfigSync;
1717
+ private readonly fs;
1718
+ private readonly process;
1719
+ private readonly logger;
1720
+ private readonly clock;
1721
+ private readonly onError;
1722
+ constructor(props?: Props$7);
1814
1723
  /**
1815
- * HTTP client for listener operations on the running gateway daemon.
1816
- * Returns `{ state: "offline" }` when the daemon is offline so hot-reload
1817
- * paths stay write-only without parsing strings.
1724
+ * Sandboxed Funnel wired with in-memory implementations for every IO boundary.
1725
+ * Touches no real disk, processes, wall-clock time, or UUIDs safe for tests
1726
+ * and ad-hoc experiments. Override individual fields by passing them in `props`.
1818
1727
  */
1819
- get listeners(): FunnelListenersClient;
1728
+ static inMemory(props?: Props$7): Funnel;
1820
1729
  /**
1821
1730
  * In-process gateway server. Unlike `gateway.start()` (which spawns a daemon),
1822
1731
  * this returns a class that runs `Bun.serve` + listeners inside the current process —
1823
1732
  * useful for tests, embedding, or custom hosts.
1824
1733
  */
1825
1734
  gatewayServer(options?: {
1826
- port?: number; /** Bind address. Defaults to `127.0.0.1` (loopback only). Set to `0.0.0.0` to expose on the network. */
1735
+ port?: number;
1827
1736
  hostname?: string;
1828
1737
  dbPath?: string;
1829
- killCompetingSlack?: boolean; /** Override the auth token. Defaults to the persisted gateway.token. Pass "" to disable auth (tests). */
1830
- token?: string; /** Durable replay log. Defaults to a SqliteFunnelEventLog at dbPath; inject a MemoryFunnelEventLog (or any FunnelEventLog) to swap or disable persistence. */
1738
+ killCompetingSlack?: boolean;
1739
+ token?: string;
1831
1740
  eventLog?: FunnelEventLog;
1832
- /**
1833
- * Additional hono app mounted before the built-in gateway routes.
1834
- * Use to embed host-specific endpoints (e.g. an MCP route, custom `/api/*`).
1835
- * Host routes are mounted first; built-in `/listeners`, `/status`,
1836
- * `/channels`, `/health` are mounted after and take precedence on conflict.
1837
- */
1838
1741
  extraRoutes?: Hono<Env$1>;
1839
1742
  }): FunnelGatewayServer;
1743
+ /**
1744
+ * Run the gateway daemon in the foreground (tied to this terminal).
1745
+ * For background daemon management, use `funnel.gateway.start()` instead.
1746
+ */
1747
+ runGatewayForeground(options?: {
1748
+ caffeinate?: boolean;
1749
+ }): Promise<number>;
1840
1750
  debug(channelName?: string): Promise<FunnelDebugReport>;
1841
1751
  gatewayClient(): ReturnType<typeof hc<GatewayApp>>;
1842
1752
  }
1843
1753
  //#endregion
1844
- //#region lib/engine/mcp/channel-server.d.ts
1845
- type ChannelServerOptions = {
1846
- /** Funnel home directory (settings.json + gateway.token). Defaults to ~/.funnel. */dir?: string; /** Gateway base URL. Defaults to `$FUNNEL_GATEWAY_URL` or `http://127.0.0.1:<port>`. */
1847
- gatewayUrl?: string; /** Channel id to subscribe to. Defaults to `$FUNNEL_CHANNEL_ID`. */
1848
- channelId?: string; /** Auth token. Defaults to `$FUNNEL_GATEWAY_TOKEN` then `<dir>/gateway.token`. */
1849
- token?: string;
1850
- };
1851
- declare const startChannelServer: (options?: ChannelServerOptions) => Promise<void>;
1852
- //#endregion
1853
- //#region lib/engine/local-config/local-config-json-schema.d.ts
1854
- /**
1855
- * Generates the JSON Schema (draft 2020-12) for `funnel.json`. Useful for
1856
- * `$schema` references in committed `funnel.json` files so editors can give
1857
- * autocomplete and validation for channels[] (transport) and profiles[]
1858
- * (launch recipe) without anyone hand-maintaining a separate schema.
1859
- */
1860
- declare const funnelJsonSchema: () => Record<string, unknown>;
1861
- //#endregion
1862
1754
  //#region lib/engine/settings/settings-store.d.ts
1863
1755
  /**
1864
1756
  * Resolves the funnel home dir. Defaults to `~/.funnel`, overridable via
@@ -1928,7 +1820,7 @@ declare class NodeFunnelFileSystem extends FunnelFileSystem {
1928
1820
  }
1929
1821
  //#endregion
1930
1822
  //#region lib/engine/fs/memory-file-system.d.ts
1931
- type Props$7 = {
1823
+ type Props$6 = {
1932
1824
  dirs?: string[];
1933
1825
  files?: Record<string, string>;
1934
1826
  mtimes?: Record<string, number>;
@@ -1941,7 +1833,7 @@ declare class MemoryFunnelFileSystem extends FunnelFileSystem {
1941
1833
  private readonly mtimes;
1942
1834
  private readonly modes;
1943
1835
  private readonly now;
1944
- constructor(props?: Props$7);
1836
+ constructor(props?: Props$6);
1945
1837
  existsSync(path: string): boolean;
1946
1838
  readFileSync(path: string): string;
1947
1839
  writeFileSync(path: string, data: string): void;
@@ -2027,14 +1919,14 @@ declare class MemoryFunnelProcessRunner extends FunnelProcessRunner {
2027
1919
  }
2028
1920
  //#endregion
2029
1921
  //#region lib/engine/logger/node-logger.d.ts
2030
- type Props$6 = {
1922
+ type Props$5 = {
2031
1923
  file?: string;
2032
1924
  now?: () => Date;
2033
1925
  };
2034
1926
  declare class NodeFunnelLogger extends FunnelLogger {
2035
1927
  readonly file: string;
2036
1928
  private readonly now;
2037
- constructor(props?: Props$6);
1929
+ constructor(props?: Props$5);
2038
1930
  info(message: string, meta?: Record<string, unknown>): void;
2039
1931
  warn(message: string, meta?: Record<string, unknown>): void;
2040
1932
  error(message: string, meta?: Record<string, unknown>): void;
@@ -2070,12 +1962,12 @@ declare class NodeFunnelClock extends FunnelClock {
2070
1962
  }
2071
1963
  //#endregion
2072
1964
  //#region lib/engine/time/memory-clock.d.ts
2073
- type Props$5 = {
1965
+ type Props$4 = {
2074
1966
  start?: Date;
2075
1967
  };
2076
1968
  declare class MemoryFunnelClock extends FunnelClock {
2077
1969
  private current;
2078
- constructor(props?: Props$5);
1970
+ constructor(props?: Props$4);
2079
1971
  now(): Date;
2080
1972
  set(date: Date): void;
2081
1973
  advance(ms: number): void;
@@ -2087,41 +1979,14 @@ declare class NodeFunnelIdGenerator extends FunnelIdGenerator {
2087
1979
  }
2088
1980
  //#endregion
2089
1981
  //#region lib/engine/id/memory-id-generator.d.ts
2090
- type Props$4 = {
1982
+ type Props$3 = {
2091
1983
  prefix?: string;
2092
1984
  };
2093
1985
  declare class MemoryFunnelIdGenerator extends FunnelIdGenerator {
2094
1986
  private counter;
2095
1987
  private readonly prefix;
2096
- constructor(props?: Props$4);
2097
- generate(): string;
2098
- }
2099
- //#endregion
2100
- //#region lib/engine/token-prompter/node-token-prompter.d.ts
2101
- /**
2102
- * Reads a secret from stdin in raw mode. Echoes a `*` per byte so the user
2103
- * can see progress without exposing the token. Refuses to prompt when stdin
2104
- * is not a TTY — callers should surface the resulting error with a hint
2105
- * pointing at the corresponding env var or CLI command.
2106
- */
2107
- declare class NodeFunnelTokenPrompter extends FunnelTokenPrompter {
2108
- promptSecret(label: string): Promise<string>;
2109
- private readSecret;
2110
- }
2111
- //#endregion
2112
- //#region lib/engine/token-prompter/memory-token-prompter.d.ts
2113
- type Props$3 = {
2114
- answers?: Record<string, string>;
2115
- };
2116
- /**
2117
- * Pre-seeded answers keyed by prompt label. Tests configure the map up front;
2118
- * unmapped labels throw so the test surfaces unexpected prompts loudly.
2119
- */
2120
- declare class MemoryFunnelTokenPrompter extends FunnelTokenPrompter {
2121
- private readonly answers;
2122
- readonly asked: string[];
2123
1988
  constructor(props?: Props$3);
2124
- promptSecret(label: string): Promise<string>;
1989
+ generate(): string;
2125
1990
  }
2126
1991
  //#endregion
2127
1992
  //#region lib/gateway/sqlite-funnel-event-log.d.ts
@@ -2312,6 +2177,10 @@ declare class ConnectorDiagnosticSqlReader {
2312
2177
  type Env = {
2313
2178
  Bindings: {
2314
2179
  funnel: Funnel;
2180
+ claude: FunnelClaude;
2181
+ profiles: FunnelProfiles;
2182
+ localConfig: FunnelLocalConfig;
2183
+ localConfigSync: FunnelLocalConfigSync;
2315
2184
  };
2316
2185
  };
2317
2186
  declare const factory: _$hono_factory0.Factory<Env, string>;
@@ -2377,7 +2246,7 @@ declare const routes: _$hono_hono_base0.HonoBase<Env, {
2377
2246
  connectors: {
2378
2247
  id: string;
2379
2248
  name: string;
2380
- type: "discord" | "slack" | "gh" | "schedule";
2249
+ type: "gh" | "discord" | "slack" | "schedule";
2381
2250
  }[];
2382
2251
  }[];
2383
2252
  outputFormat: "json";
@@ -4211,4 +4080,4 @@ ${string}`;
4211
4080
  }, "/", "/update">;
4212
4081
  type CliApp = typeof routes;
4213
4082
  //#endregion
4214
- export { AliveStub, AttachOptions, BroadcastEvent, BroadcastSubscriber, CONNECTOR_CONNECTION_STATUSES, ChannelConfig, ChannelConnectorView, ChannelDeliveryMode, ChannelServerOptions, ChannelSpec, type CliApp, ConnectorConfig, ConnectorConnectionEvent, ConnectorConnectionQuery, ConnectorConnectionRecord, ConnectorConnectionStatus, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, ConnectorProcessedEvent, ConnectorProcessedQuery, ConnectorProcessedRecord, ConnectorQuery, ConnectorRawEvent, ConnectorRawQuery, ConnectorRawRecord, ConnectorSpec, ConnectorSyncOutcome, ConnectorType, DEFAULT_GATEWAY_PORT, DEFAULT_GATEWAY_TOKEN_PATH, DetachOptions, DiscordConnectorConfig, Env, FUNNEL_DIR, FUNNEL_MCP_ARGS, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, FileStat, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, type FunnelDebugReport, FunnelEvent, FunnelEventLog, FunnelEventRecord, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLocalConfigWriter, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, type GatewayApp, type GatewayEmitInput, type GatewayRouteDeps, type Env$1 as GatewayServerEnv, GhConnectorConfig, LOCAL_CONFIG_FILENAME, LaunchOptions, ListListenersResult, ListenerEntry, ListenerOpResult, LocalConfig, LocalConfigSyncResult, LogEntry, MemoryConnectorDiagnosticLog, MemoryFunnelClock, MemoryFunnelEventLog, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MemoryProcessCall, MemoryProcessHandler, MemoryProcessResponse, MemoryProcessSyncHandler, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, NotifyFn, OnFunnelError, ProcessListStub, ProcessSnapshot, ProfileConfig, ProfileSpec, PublishRequest, PublishResponse, PublishResult, ReplayableEvent, RunOptions, RunResult, SETTINGS_PATH, SETTINGS_VERSION, ScheduleCatchupPolicy, ScheduleConnectorConfig, ScheduleEntry, ScheduleListenerOptions, Settings, SlackConnectorConfig, SlackListenerOptions, SlackProcessed, SlackProcessedEmit, SlackProcessedSkip, SlackRawEvent, SlackSkipReason, SqliteConnectorDiagnosticLog, SqliteFunnelEventLog, StoredConnectionEvent, StoredProcessedEvent, StoredRawEvent, channelConfigSchema, channelDeliveryModeSchema, channelSpecSchema, routes as cliRoutes, connectorConfigSchema, connectorConnectionEventSchema, connectorProcessedEventSchema, connectorRawEventSchema, connectorSpecSchema, createSettings, discordConnectorSchema, factory, funnelEventSchema, funnelJsonSchema, ghConnectorSchema, localConfigSchema, profileConfigSchema, profileSpecSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, resolveFunnelDir, resolveFunnelPort, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, startChannelServer, toRequest };
4083
+ export { AliveStub, AttachOptions, BroadcastEvent, BroadcastSubscriber, CONNECTOR_CONNECTION_STATUSES, ChannelConfig, ChannelConnectorView, ChannelDeliveryMode, type CliApp, ConnectorConfig, ConnectorConnectionEvent, ConnectorConnectionQuery, ConnectorConnectionRecord, ConnectorConnectionStatus, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, ConnectorProcessedEvent, ConnectorProcessedQuery, ConnectorProcessedRecord, ConnectorQuery, ConnectorRawEvent, ConnectorRawQuery, ConnectorRawRecord, ConnectorType, DEFAULT_GATEWAY_PORT, DEFAULT_GATEWAY_TOKEN_PATH, DetachOptions, DiscordConnectorConfig, Env, FUNNEL_DIR, FileStat, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, type FunnelDebugReport, FunnelEvent, FunnelEventLog, FunnelEventRecord, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLogger, FunnelProcessRunner, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, type GatewayApp, type GatewayEmitInput, type GatewayRouteDeps, type Env$1 as GatewayServerEnv, GhConnectorConfig, ListListenersResult, ListenerEntry, ListenerOpResult, LogEntry, MemoryConnectorDiagnosticLog, MemoryFunnelClock, MemoryFunnelEventLog, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryProcessCall, MemoryProcessHandler, MemoryProcessResponse, MemoryProcessSyncHandler, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NoopFunnelLogger, NotifyFn, OnFunnelError, ProcessListStub, ProcessSnapshot, ProfileConfig, PublishRequest, PublishResponse, PublishResult, ReplayableEvent, RunOptions, RunResult, SETTINGS_PATH, SETTINGS_VERSION, ScheduleCatchupPolicy, ScheduleConnectorConfig, ScheduleEntry, ScheduleListenerOptions, Settings, SlackConnectorConfig, SlackListenerOptions, SlackProcessed, SlackProcessedEmit, SlackProcessedSkip, SlackRawEvent, SlackSkipReason, SqliteConnectorDiagnosticLog, SqliteFunnelEventLog, StoredConnectionEvent, StoredProcessedEvent, StoredRawEvent, channelConfigSchema, channelDeliveryModeSchema, routes as cliRoutes, connectorConfigSchema, connectorConnectionEventSchema, connectorProcessedEventSchema, connectorRawEventSchema, createSettings, discordConnectorSchema, factory, funnelEventSchema, ghConnectorSchema, profileConfigSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, resolveFunnelDir, resolveFunnelPort, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, toRequest };