@interactive-inc/claude-funnel 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/dist/bin.js +583 -656
  2. package/dist/cli/factory.d.ts +7 -0
  3. package/dist/cli/router/query-to-cli-args.d.ts +1 -0
  4. package/dist/cli/router/to-request.d.ts +5 -0
  5. package/dist/cli/router/validator.d.ts +5 -0
  6. package/dist/cli/routes/channels.$channel.connectors.$connector.d.ts +42 -0
  7. package/dist/cli/routes/channels.$channel.connectors.$connector.rename.$newName.d.ts +46 -0
  8. package/dist/cli/routes/channels.$channel.connectors.$connector.request.d.ts +54 -0
  9. package/dist/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.d.ts +66 -0
  10. package/dist/cli/routes/channels.$channel.connectors.$connector.schedules.d.ts +42 -0
  11. package/dist/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.d.ts +46 -0
  12. package/dist/cli/routes/channels.$channel.connectors.add.$connector.d.ts +90 -0
  13. package/dist/cli/routes/channels.$channel.connectors.d.ts +38 -0
  14. package/dist/cli/routes/channels.$channel.connectors.remove.$connector.d.ts +42 -0
  15. package/dist/cli/routes/channels.$channel.connectors.set.$connector.d.ts +62 -0
  16. package/dist/cli/routes/channels.$channel.d.ts +38 -0
  17. package/dist/cli/routes/channels.$channel.rename.$newName.d.ts +42 -0
  18. package/dist/cli/routes/channels.$channel.set.delivery.$mode.d.ts +28 -0
  19. package/dist/cli/routes/channels.add.$channel.d.ts +46 -0
  20. package/dist/cli/routes/channels.d.ts +16 -0
  21. package/dist/cli/routes/channels.remove.$channel.d.ts +38 -0
  22. package/dist/cli/routes/claude.d.ts +32 -0
  23. package/dist/cli/routes/gateway.d.ts +20 -0
  24. package/dist/cli/routes/gateway.listeners.d.ts +17 -0
  25. package/dist/cli/routes/gateway.logs.d.ts +24 -0
  26. package/dist/cli/routes/gateway.restart.d.ts +24 -0
  27. package/dist/cli/routes/gateway.run.d.ts +24 -0
  28. package/dist/cli/routes/gateway.start.d.ts +24 -0
  29. package/dist/cli/routes/gateway.status.d.ts +13 -0
  30. package/dist/cli/routes/gateway.stop.d.ts +16 -0
  31. package/dist/cli/routes/index.d.ts +1222 -0
  32. package/dist/cli/routes/profiles.$profile.as-default.d.ts +38 -0
  33. package/dist/cli/routes/profiles.$profile.rename.$newName.d.ts +42 -0
  34. package/dist/cli/routes/profiles.$profile.run.d.ts +46 -0
  35. package/dist/cli/routes/profiles.add.$profile.d.ts +54 -0
  36. package/dist/cli/routes/profiles.d.ts +16 -0
  37. package/dist/cli/routes/profiles.remove.$profile.d.ts +38 -0
  38. package/dist/cli/routes/profiles.set.$profile.d.ts +54 -0
  39. package/dist/cli/routes/status.d.ts +16 -0
  40. package/dist/cli/routes/update.d.ts +16 -0
  41. package/dist/connectors/connector-adapter.d.ts +8 -0
  42. package/dist/connectors/connector-config-schema.d.ts +43 -0
  43. package/dist/connectors/connector-factory.d.ts +32 -0
  44. package/dist/connectors/connector-listener.d.ts +17 -0
  45. package/dist/connectors/discord-adapter.d.ts +14 -0
  46. package/dist/connectors/discord-connector-schema.d.ts +10 -0
  47. package/dist/connectors/discord-event-processor.d.ts +26 -0
  48. package/dist/connectors/discord-listener.d.ts +17 -0
  49. package/dist/connectors/gh-adapter.d.ts +11 -0
  50. package/dist/connectors/gh-connector-schema.d.ts +10 -0
  51. package/dist/connectors/gh-listener.d.ts +26 -0
  52. package/dist/connectors/match-cron.d.ts +1 -0
  53. package/dist/connectors/schedule-connector-schema.d.ts +45 -0
  54. package/dist/connectors/schedule-listener.d.ts +30 -0
  55. package/dist/connectors/schedule-state-store.d.ts +19 -0
  56. package/dist/connectors/slack-adapter.d.ts +15 -0
  57. package/dist/connectors/slack-connector-schema.d.ts +11 -0
  58. package/dist/connectors/slack-event-processor.d.ts +27 -0
  59. package/dist/connectors/slack-listener.d.ts +17 -0
  60. package/dist/engine/channels/channels.d.ts +106 -0
  61. package/dist/engine/claude/claude.d.ts +49 -0
  62. package/dist/engine/claude/gateway-controller.d.ts +6 -0
  63. package/dist/engine/fs/file-system.d.ts +24 -0
  64. package/dist/engine/fs/memory-file-system.d.ts +31 -0
  65. package/dist/engine/fs/node-file-system.d.ts +15 -0
  66. package/dist/engine/http/http-client.d.ts +15 -0
  67. package/dist/engine/http/memory-http-client.d.ts +12 -0
  68. package/dist/engine/http/node-http-client.d.ts +5 -0
  69. package/dist/engine/id/id-generator.d.ts +7 -0
  70. package/dist/engine/id/memory-id-generator.d.ts +11 -0
  71. package/dist/engine/id/node-id-generator.d.ts +4 -0
  72. package/dist/engine/logger/logger.d.ts +11 -0
  73. package/dist/engine/logger/memory-logger.d.ts +14 -0
  74. package/dist/engine/logger/node-logger.d.ts +15 -0
  75. package/dist/engine/logger/noop-logger.d.ts +7 -0
  76. package/dist/engine/mcp/channel-server.d.ts +1 -0
  77. package/dist/engine/mcp/mcp.d.ts +22 -0
  78. package/dist/engine/process/memory-process-runner.d.ts +43 -0
  79. package/dist/engine/process/node-process-runner.d.ts +9 -0
  80. package/dist/engine/process/process-runner.d.ts +29 -0
  81. package/dist/engine/profiles/profile-channel-checker.d.ts +7 -0
  82. package/dist/engine/profiles/profiles.d.ts +31 -0
  83. package/dist/engine/settings/mock-settings-reader.d.ts +9 -0
  84. package/dist/engine/settings/settings-reader.d.ts +5 -0
  85. package/dist/engine/settings/settings-schema.d.ts +132 -0
  86. package/dist/engine/settings/settings-store.d.ts +18 -0
  87. package/dist/engine/time/clock.d.ts +9 -0
  88. package/dist/engine/time/memory-clock.d.ts +12 -0
  89. package/dist/engine/time/node-clock.d.ts +4 -0
  90. package/dist/funnel.d.ts +95 -0
  91. package/dist/gateway/auth-middleware.d.ts +14 -0
  92. package/dist/gateway/broadcaster.d.ts +122 -0
  93. package/dist/gateway/daemon.d.ts +2 -0
  94. package/dist/gateway/daemon.js +192 -220
  95. package/dist/gateway/factory.d.ts +7 -0
  96. package/dist/gateway/funnel-event-store.d.ts +81 -0
  97. package/dist/gateway/gateway-server.d.ts +94 -0
  98. package/dist/gateway/gateway-token.d.ts +33 -0
  99. package/dist/gateway/gateway.d.ts +58 -0
  100. package/dist/gateway/kill-competing-slack-gateways.d.ts +9 -0
  101. package/dist/gateway/listener-supervisor.d.ts +85 -0
  102. package/dist/gateway/listeners-client.d.ts +53 -0
  103. package/dist/gateway/resolve-daemon-script.d.ts +11 -0
  104. package/dist/gateway/routes/channels.connectors.call.d.ts +41 -0
  105. package/dist/gateway/routes/health.d.ts +17 -0
  106. package/dist/gateway/routes/index.d.ts +209 -0
  107. package/dist/gateway/routes/listeners.list.d.ts +14 -0
  108. package/dist/gateway/routes/listeners.restart.d.ts +34 -0
  109. package/dist/gateway/routes/listeners.start.d.ts +34 -0
  110. package/dist/gateway/routes/listeners.stop.d.ts +34 -0
  111. package/dist/gateway/routes/route-deps.d.ts +10 -0
  112. package/dist/gateway/routes/status.d.ts +30 -0
  113. package/dist/gateway/routes/validator.d.ts +19 -0
  114. package/dist/index.d.ts +36 -0
  115. package/dist/index.js +3575 -0
  116. package/dist/logger/leuco-human-file-writer.d.ts +33 -0
  117. package/dist/logger/leuco-human-logger.d.ts +46 -0
  118. package/dist/logger/leuco-human-record.d.ts +15 -0
  119. package/dist/logger/leuco-human-stdout-writer.d.ts +20 -0
  120. package/dist/logger/leuco-human-writer.d.ts +13 -0
  121. package/dist/logger/leuco-logger-memory-sink.d.ts +33 -0
  122. package/dist/logger/leuco-logger-record.d.ts +13 -0
  123. package/dist/logger/leuco-logger-sink.d.ts +34 -0
  124. package/dist/logger/leuco-logger-sqlite-sink.d.ts +102 -0
  125. package/dist/logger/leuco-logger.d.ts +56 -0
  126. package/lib/index.ts +2 -0
  127. package/package.json +14 -9
@@ -0,0 +1,132 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Routing mode when multiple WS clients are subscribed to the same channel.
4
+ *
5
+ * - `fanout` (default): every connected client receives every event. Right when each
6
+ * subscriber has its own job (e.g., TUI mirrors, distinct Claude profiles each running
7
+ * their own pipeline against the same source).
8
+ * - `exclusive`: each event is delivered to exactly one connected client, picked
9
+ * round-robin per channel. Right when subscribers are interchangeable workers and you
10
+ * want each event handled once. Tap=all clients (TUI dashboard) always receive,
11
+ * regardless of mode, so they can passively observe.
12
+ */
13
+ export declare const channelDeliveryModeSchema: z.ZodEnum<{
14
+ fanout: "fanout";
15
+ exclusive: "exclusive";
16
+ }>;
17
+ export type ChannelDeliveryMode = z.infer<typeof channelDeliveryModeSchema>;
18
+ export declare const channelConfigSchema: z.ZodObject<{
19
+ id: z.ZodString;
20
+ name: z.ZodString;
21
+ delivery: z.ZodDefault<z.ZodEnum<{
22
+ fanout: "fanout";
23
+ exclusive: "exclusive";
24
+ }>>;
25
+ connectors: z.ZodDefault<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
26
+ id: z.ZodString;
27
+ name: z.ZodString;
28
+ type: z.ZodLiteral<"slack">;
29
+ botToken: z.ZodString;
30
+ appToken: z.ZodString;
31
+ createdAt: z.ZodOptional<z.ZodString>;
32
+ updatedAt: z.ZodOptional<z.ZodString>;
33
+ }, z.core.$strip>, z.ZodObject<{
34
+ id: z.ZodString;
35
+ name: z.ZodString;
36
+ type: z.ZodLiteral<"gh">;
37
+ pollInterval: z.ZodOptional<z.ZodNumber>;
38
+ createdAt: z.ZodOptional<z.ZodString>;
39
+ updatedAt: z.ZodOptional<z.ZodString>;
40
+ }, z.core.$strip>, z.ZodObject<{
41
+ id: z.ZodString;
42
+ name: z.ZodString;
43
+ type: z.ZodLiteral<"discord">;
44
+ botToken: z.ZodString;
45
+ createdAt: z.ZodOptional<z.ZodString>;
46
+ updatedAt: z.ZodOptional<z.ZodString>;
47
+ }, z.core.$strip>, z.ZodObject<{
48
+ id: z.ZodString;
49
+ name: z.ZodString;
50
+ type: z.ZodLiteral<"schedule">;
51
+ entries: z.ZodDefault<z.ZodArray<z.ZodObject<{
52
+ id: z.ZodString;
53
+ cron: z.ZodString;
54
+ prompt: z.ZodString;
55
+ enabled: z.ZodDefault<z.ZodBoolean>;
56
+ catchupPolicy: z.ZodDefault<z.ZodEnum<{
57
+ latest: "latest";
58
+ all: "all";
59
+ skip: "skip";
60
+ }>>;
61
+ }, z.core.$strip>>>;
62
+ createdAt: z.ZodOptional<z.ZodString>;
63
+ updatedAt: z.ZodOptional<z.ZodString>;
64
+ }, z.core.$strip>], "type">>>;
65
+ }, z.core.$strip>;
66
+ export type ChannelConfig = z.infer<typeof channelConfigSchema>;
67
+ export declare const profileConfigSchema: z.ZodObject<{
68
+ name: z.ZodString;
69
+ path: z.ZodString;
70
+ subAgent: z.ZodString;
71
+ channelId: z.ZodString;
72
+ }, z.core.$strip>;
73
+ export type ProfileConfig = z.infer<typeof profileConfigSchema>;
74
+ export declare const SETTINGS_VERSION = 1;
75
+ export declare const settingsSchema: z.ZodObject<{
76
+ version: z.ZodDefault<z.ZodLiteral<1>>;
77
+ channels: z.ZodDefault<z.ZodArray<z.ZodObject<{
78
+ id: z.ZodString;
79
+ name: z.ZodString;
80
+ delivery: z.ZodDefault<z.ZodEnum<{
81
+ fanout: "fanout";
82
+ exclusive: "exclusive";
83
+ }>>;
84
+ connectors: z.ZodDefault<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
85
+ id: z.ZodString;
86
+ name: z.ZodString;
87
+ type: z.ZodLiteral<"slack">;
88
+ botToken: z.ZodString;
89
+ appToken: z.ZodString;
90
+ createdAt: z.ZodOptional<z.ZodString>;
91
+ updatedAt: z.ZodOptional<z.ZodString>;
92
+ }, z.core.$strip>, z.ZodObject<{
93
+ id: z.ZodString;
94
+ name: z.ZodString;
95
+ type: z.ZodLiteral<"gh">;
96
+ pollInterval: z.ZodOptional<z.ZodNumber>;
97
+ createdAt: z.ZodOptional<z.ZodString>;
98
+ updatedAt: z.ZodOptional<z.ZodString>;
99
+ }, z.core.$strip>, z.ZodObject<{
100
+ id: z.ZodString;
101
+ name: z.ZodString;
102
+ type: z.ZodLiteral<"discord">;
103
+ botToken: z.ZodString;
104
+ createdAt: z.ZodOptional<z.ZodString>;
105
+ updatedAt: z.ZodOptional<z.ZodString>;
106
+ }, z.core.$strip>, z.ZodObject<{
107
+ id: z.ZodString;
108
+ name: z.ZodString;
109
+ type: z.ZodLiteral<"schedule">;
110
+ entries: z.ZodDefault<z.ZodArray<z.ZodObject<{
111
+ id: z.ZodString;
112
+ cron: z.ZodString;
113
+ prompt: z.ZodString;
114
+ enabled: z.ZodDefault<z.ZodBoolean>;
115
+ catchupPolicy: z.ZodDefault<z.ZodEnum<{
116
+ latest: "latest";
117
+ all: "all";
118
+ skip: "skip";
119
+ }>>;
120
+ }, z.core.$strip>>>;
121
+ createdAt: z.ZodOptional<z.ZodString>;
122
+ updatedAt: z.ZodOptional<z.ZodString>;
123
+ }, z.core.$strip>], "type">>>;
124
+ }, z.core.$strip>>>;
125
+ profiles: z.ZodDefault<z.ZodArray<z.ZodObject<{
126
+ name: z.ZodString;
127
+ path: z.ZodString;
128
+ subAgent: z.ZodString;
129
+ channelId: z.ZodString;
130
+ }, z.core.$strip>>>;
131
+ }, z.core.$strip>;
132
+ export type Settings = z.infer<typeof settingsSchema>;
@@ -0,0 +1,18 @@
1
+ import { FunnelFileSystem } from "../fs/file-system";
2
+ import { FunnelSettingsReader } from "./settings-reader";
3
+ import type { Settings } from "./settings-schema";
4
+ export declare const FUNNEL_DIR: string;
5
+ export declare const SETTINGS_PATH: string;
6
+ type Deps = {
7
+ path?: string;
8
+ fs?: FunnelFileSystem;
9
+ };
10
+ export declare class FunnelSettingsStore extends FunnelSettingsReader {
11
+ private readonly path;
12
+ private readonly fs;
13
+ constructor(deps?: Deps);
14
+ read(): Settings;
15
+ private looksLikeLegacy;
16
+ write(settings: Settings): void;
17
+ }
18
+ export {};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Time boundary. Default NodeFunnelClock returns `new Date()`; MemoryFunnelClock
3
+ * is settable and `advance(ms)`-able for deterministic schedule / timeout tests.
4
+ */
5
+ export declare abstract class FunnelClock {
6
+ abstract now(): Date;
7
+ millis(): number;
8
+ iso(): string;
9
+ }
@@ -0,0 +1,12 @@
1
+ import { FunnelClock } from "./clock";
2
+ type Props = {
3
+ start?: Date;
4
+ };
5
+ export declare class MemoryFunnelClock extends FunnelClock {
6
+ private current;
7
+ constructor(props?: Props);
8
+ now(): Date;
9
+ set(date: Date): void;
10
+ advance(ms: number): void;
11
+ }
12
+ export {};
@@ -0,0 +1,4 @@
1
+ import { FunnelClock } from "./clock";
2
+ export declare class NodeFunnelClock extends FunnelClock {
3
+ now(): Date;
4
+ }
@@ -0,0 +1,95 @@
1
+ import { FunnelConnectorFactory } from "./connectors/connector-factory";
2
+ import { FunnelChannels } from "./engine/channels/channels";
3
+ import { FunnelClaude } from "./engine/claude/claude";
4
+ import type { FunnelFileSystem } from "./engine/fs/file-system";
5
+ import type { FunnelIdGenerator } from "./engine/id/id-generator";
6
+ import { FunnelLogger } from "./engine/logger/logger";
7
+ import { FunnelMcp } from "./engine/mcp/mcp";
8
+ import { FunnelProcessRunner } from "./engine/process/process-runner";
9
+ import { FunnelProfiles } from "./engine/profiles/profiles";
10
+ import { FunnelSettingsReader } from "./engine/settings/settings-reader";
11
+ import type { FunnelClock } from "./engine/time/clock";
12
+ import { FunnelGateway } from "./gateway/gateway";
13
+ import { FunnelGatewayServer } from "./gateway/gateway-server";
14
+ import { FunnelGatewayToken } from "./gateway/gateway-token";
15
+ import { FunnelListenersClient } from "./gateway/listeners-client";
16
+ type Props = {
17
+ /** Settings persistence (channels with nested connectors / profiles). Defaults to a FunnelSettingsStore rooted at `dir`. */
18
+ store?: FunnelSettingsReader;
19
+ /** Filesystem boundary. Replace with MemoryFunnelFileSystem to sandbox all disk I/O. */
20
+ fs?: FunnelFileSystem;
21
+ /** Process runner used by gateway / claude / gh listener. Replace with MemoryFunnelProcessRunner for tests. */
22
+ process?: FunnelProcessRunner;
23
+ /** Logger flowed into every facet. Replace with MemoryFunnelLogger or NoopFunnelLogger to silence/inspect. */
24
+ logger?: FunnelLogger;
25
+ /** Clock used by schedule listener, gh poll watermarks, and gateway timeouts. */
26
+ clock?: FunnelClock;
27
+ /** ID generator for channel and connector ids. Use MemoryFunnelIdGenerator for deterministic tests. */
28
+ idGenerator?: FunnelIdGenerator;
29
+ /** Funnel home directory (settings.json + per-channel/per-connector dirs). Defaults to ~/.funnel. */
30
+ dir?: string;
31
+ /** Temp / runtime directory (gateway logs and PID adjacent files). Defaults to /tmp/funnel. */
32
+ tmpDir?: string;
33
+ };
34
+ /**
35
+ * Facade exposing every funnel facet as a getter.
36
+ *
37
+ * The same `Funnel` is used by the CLI, the TUI, and as a programmable library.
38
+ * All side-effecting boundaries (filesystem, process, logger, clock, id, paths) are
39
+ * injectable via `Props` — passing memory implementations gives a fully sandboxed
40
+ * Funnel that touches no real disk, processes, or wall-clock time.
41
+ *
42
+ * Connectors live nested inside their owning channel (channels[].connectors[]),
43
+ * so connector CRUD is reached via `funnel.channels.addConnector(...)` etc.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const funnel = new Funnel({})
48
+ * const channel = funnel.channels.add({ name: "inbox" })
49
+ * funnel.channels.addConnector("inbox", { type: "slack", name: "ops", botToken, appToken })
50
+ * await funnel.gatewayServer({ port: 9742 }).start()
51
+ * ```
52
+ */
53
+ export declare class Funnel {
54
+ private readonly props;
55
+ constructor(props?: Props);
56
+ /** Settings reader. If not injected, a FunnelSettingsStore rooted at `dir` is created. */
57
+ get store(): FunnelSettingsReader;
58
+ /** Process runner boundary. Defaults to NodeFunnelProcessRunner. */
59
+ get process(): FunnelProcessRunner;
60
+ /** Logger boundary. Defaults to NodeFunnelLogger. */
61
+ get logger(): FunnelLogger;
62
+ /** Pure factory that constructs per-type listeners and adapters from connector configs. */
63
+ get factory(): FunnelConnectorFactory;
64
+ /** Channel CRUD + nested connector CRUD + schedule entries + listener/adapter dispatch. */
65
+ get channels(): FunnelChannels;
66
+ /** Launch profiles (named presets for `fnl claude`: path + sub-agent + channel id). */
67
+ get profiles(): FunnelProfiles;
68
+ /** funnel MCP installer (writes/removes `.mcp.json` entries in target repos). */
69
+ get mcp(): FunnelMcp;
70
+ /** Launch Claude Code with a channel injected via env, MCP installed, gateway ensured. */
71
+ get claude(): FunnelClaude;
72
+ /** Gateway daemon controller (PID-file, start/stop the separate `bun daemon.ts` process). */
73
+ get gateway(): FunnelGateway;
74
+ /** Read / generate the daemon's gateway token (mode 0600 file under `dir`). */
75
+ get gatewayToken(): FunnelGatewayToken;
76
+ /**
77
+ * HTTP client for listener operations on the running gateway daemon.
78
+ * Returns `{ state: "offline" }` when the daemon is offline so hot-reload
79
+ * paths stay write-only without parsing strings.
80
+ */
81
+ get listeners(): FunnelListenersClient;
82
+ /**
83
+ * In-process gateway server. Unlike `gateway.start()` (which spawns a daemon),
84
+ * this returns a class that runs `Bun.serve` + listeners inside the current process —
85
+ * useful for tests, embedding, or custom hosts.
86
+ */
87
+ gatewayServer(options?: {
88
+ port?: number;
89
+ logDir?: string;
90
+ killCompetingSlack?: boolean;
91
+ /** Override the auth token. Defaults to the persisted gateway.token. Pass "" to disable auth (tests). */
92
+ token?: string;
93
+ }): FunnelGatewayServer;
94
+ }
95
+ export {};
@@ -0,0 +1,14 @@
1
+ import type { MiddlewareHandler } from "hono";
2
+ import type { Env } from "./factory";
3
+ type Deps = {
4
+ expected: string;
5
+ };
6
+ /**
7
+ * Verifies `Authorization: Bearer <token>` against the daemon's gateway token.
8
+ * Mounted on the routes that mutate listener state or expose detailed status.
9
+ * `/health` is intentionally left unauthenticated so the daemon manager can
10
+ * probe liveness without needing the token.
11
+ */
12
+ export declare const requireBearerToken: (deps: Deps) => MiddlewareHandler<Env>;
13
+ export declare const constantTimeEqual: (a: string, b: string) => boolean;
14
+ export {};
@@ -0,0 +1,122 @@
1
+ import type { ServerWebSocket } from "bun";
2
+ import { FunnelLogger } from "../engine/logger/logger";
3
+ type ClientData = {
4
+ /** Stable channel id (uuid) that the WS client subscribed to. */
5
+ channel: string;
6
+ /** Human-facing channel name resolved at upgrade time, kept for log readability. */
7
+ channelName?: string | null;
8
+ /** Connector names belonging to that channel; used by tap-all replay filtering. */
9
+ connectors: string[];
10
+ tapAll?: boolean;
11
+ /** Routing mode resolved from channel config at upgrade time. Defaults to fanout. */
12
+ delivery?: "fanout" | "exclusive";
13
+ };
14
+ export type BroadcastEvent = {
15
+ content: string;
16
+ meta?: Record<string, string>;
17
+ };
18
+ export type ReplayableEvent = BroadcastEvent & {
19
+ offset: number;
20
+ };
21
+ export type BroadcastSubscriber = (event: ReplayableEvent) => void;
22
+ /**
23
+ * Optional persistent replay source. Wired in by the gateway-server with
24
+ * `FunnelEventStore` (SQLite-backed) so reconnects across daemon restarts
25
+ * can recover events older than the in-memory buffer via an indexed
26
+ * `seq > since` range scan.
27
+ */
28
+ type ReplaySource = {
29
+ loadSince(since: number): ReplayableEvent[];
30
+ };
31
+ type Deps = {
32
+ logger?: FunnelLogger;
33
+ maxBufferedBytes?: number;
34
+ now?: () => number;
35
+ /** Number of recent events kept in the in-memory replay buffer. */
36
+ replayBufferSize?: number;
37
+ /** Hard byte cap on replay buffer payloads. Older events are evicted FIFO until under this cap. */
38
+ replayBufferMaxBytes?: number;
39
+ /** Persistent replay source consulted when the in-memory buffer cannot satisfy `since`. */
40
+ persistentReplay?: ReplaySource;
41
+ };
42
+ type BroadcasterMetrics = {
43
+ clients: number;
44
+ subscribers: number;
45
+ eventsBroadcast: number;
46
+ droppedSlowClients: number;
47
+ lastBroadcastAt: string | null;
48
+ /** Latest emitted offset. Clients can `?since=<offset>` to ask for events strictly after this point. */
49
+ latestOffset: number;
50
+ /** Oldest offset still held in the replay buffer. Older values cannot be replayed and trigger a full resync. */
51
+ oldestReplayableOffset: number | null;
52
+ };
53
+ /**
54
+ * In-process pub/sub for connector events.
55
+ *
56
+ * Two outbound paths:
57
+ * - WS clients connected via the gateway's `/ws` endpoint, scoped per channel
58
+ * - In-process subscribers registered via `subscribe()` (programmable API)
59
+ *
60
+ * Backpressure: if a WS client's `bufferedAmount` exceeds `maxBufferedBytes`
61
+ * (default 1 MiB), the client is closed with code 1009 and dropped from the
62
+ * registry to keep one slow consumer from blocking the daemon.
63
+ *
64
+ * Replay: every emitted event gets a strictly increasing `offset`. The latest
65
+ * `replayBufferSize` events are kept in memory; reconnecting WS clients can
66
+ * pass `?since=<offset>` and the broadcaster resends matching events before
67
+ * resuming the live stream. The in-memory ring covers short reconnects;
68
+ * older history is served from the SQLite event store wired in as
69
+ * `persistentReplay`.
70
+ */
71
+ export declare class FunnelBroadcaster {
72
+ private readonly clients;
73
+ private readonly subscribers;
74
+ private readonly logger;
75
+ private readonly maxBufferedBytes;
76
+ private readonly now;
77
+ private readonly replayBufferSize;
78
+ private readonly replayBufferMaxBytes;
79
+ private readonly replayBuffer;
80
+ private readonly persistentReplay;
81
+ private readonly exclusiveCursor;
82
+ private replayBufferBytes;
83
+ private eventsBroadcast;
84
+ private droppedSlowClients;
85
+ private lastBroadcastAt;
86
+ private latestOffset;
87
+ constructor(deps?: Deps);
88
+ getMetrics(): BroadcasterMetrics;
89
+ /**
90
+ * Returns events with offset > since, filtered by the connector subscription
91
+ * rules of `data`. Used at WS upgrade time when the client passes `?since=<offset>`.
92
+ *
93
+ * Two-tier lookup:
94
+ * 1. The in-memory ring buffer (covers short reconnects, last `replayBufferSize` events).
95
+ * 2. If `since` predates the oldest in-memory entry and a persistent replay source
96
+ * is wired in (SQLite), the gap is filled from disk. This covers reconnects across
97
+ * daemon restarts where the in-memory buffer was lost.
98
+ *
99
+ * Result is sorted ascending by offset and de-duplicated against the in-memory buffer.
100
+ */
101
+ replaySince(since: number, data: ClientData): ReplayableEvent[];
102
+ private matchesClient;
103
+ /**
104
+ * Returns the list of WS clients that should receive `event`. Tap=all clients always
105
+ * receive (passive observation). For each per-channel group:
106
+ * - fanout → every matching client receives
107
+ * - exclusive → exactly one client receives, picked round-robin per channel
108
+ */
109
+ private pickRecipients;
110
+ addClient(ws: ServerWebSocket<unknown>, data: ClientData): void;
111
+ removeClient(ws: ServerWebSocket<unknown>): void;
112
+ getClientCount(): number;
113
+ listChannels(): {
114
+ channel: string;
115
+ connectors: string[];
116
+ }[];
117
+ subscribe(handler: BroadcastSubscriber): () => void;
118
+ broadcast(content: string, meta?: Record<string, string>): ReplayableEvent;
119
+ /** Forward-seed the offset counter (used at startup from the persisted event store). */
120
+ seedLatestOffset(offset: number): void;
121
+ }
122
+ export {};
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};