@interactive-inc/claude-funnel 0.60.1 → 0.64.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 (88) hide show
  1. package/README.md +2 -2
  2. package/dist/bin.js +428 -761
  3. package/dist/{channels-2g_BU1N0.d.ts → channels-CRGb6B5_.d.ts} +17 -16
  4. package/dist/claude.d.ts +5 -7
  5. package/dist/claude.js +143 -36
  6. package/dist/{connector-descriptor-6SXJoszo.d.ts → connector-descriptor-BFIhyTfa.d.ts} +49 -10
  7. package/dist/connector-diagnostics-recorder-COtNEmUp.js +42 -0
  8. package/dist/connectors/discord.d.ts +31 -37
  9. package/dist/connectors/discord.js +3 -3
  10. package/dist/connectors/gh.d.ts +37 -33
  11. package/dist/connectors/gh.js +3 -3
  12. package/dist/connectors/schedule.d.ts +9 -57
  13. package/dist/connectors/schedule.js +3 -3
  14. package/dist/connectors/slack.d.ts +106 -132
  15. package/dist/connectors/slack.js +4 -3
  16. package/dist/diagnostics.d.ts +1 -1
  17. package/dist/diagnostics.js +1 -1
  18. package/dist/discord-connector-DIFkYBbi.js +250 -0
  19. package/dist/discord-connector-schema-D-bOVAKt.d.ts +22 -0
  20. package/dist/docs.js +1 -1
  21. package/dist/doctor.d.ts +1 -1
  22. package/dist/doctor.js +1 -1
  23. package/dist/{file-process-guard-C_PLxfUX.d.ts → file-process-guard-tVcgckH6.d.ts} +6 -6
  24. package/dist/{file-system-o51IsM0W.d.ts → file-system-VhwwXZbm.d.ts} +8 -0
  25. package/dist/flume-source-listener-BNyAII7N.d.ts +133 -0
  26. package/dist/{funnel-diagnostics-CSiJmPlZ.js → funnel-diagnostics-Cvk6Sk4x.js} +193 -43
  27. package/dist/{funnel-diagnostics-DpXOsCty.d.ts → funnel-diagnostics-b9ar0Ing.d.ts} +67 -5
  28. package/dist/{funnel-docs-BxXZ9Ksx.js → funnel-docs-C-ge0MuB.js} +42 -6
  29. package/dist/{funnel-doctor-CZf_0Luq.d.ts → funnel-doctor-CnRQi4kM.d.ts} +2 -2
  30. package/dist/{funnel-doctor-DiJCjHsg.js → funnel-doctor-XrI2GBH8.js} +1 -1
  31. package/dist/funnel-error-0t1MK1R6.js +75 -0
  32. package/dist/{funnel-recovery-DnLrdWO9.d.ts → funnel-recovery-CMhY8Jfk.d.ts} +1 -1
  33. package/dist/gateway/daemon.js +167 -527
  34. package/dist/gateway.d.ts +3 -3
  35. package/dist/gateway.js +3 -3
  36. package/dist/gh-connector-BUGCOEWS.js +187 -0
  37. package/dist/{gh-connector-schema-Rzwc1c1N.js → gh-connector-schema-CAqIhzGr.js} +7 -0
  38. package/dist/gh-connector-schema-DWQaB6gX.d.ts +16 -0
  39. package/dist/{index-CgY8NdMz.d.ts → index-Ds6sHhA-.d.ts} +37 -19
  40. package/dist/index.d.ts +182 -22
  41. package/dist/index.js +363 -173
  42. package/dist/{local-config-json-schema-JyLqOQNX.js → local-config-json-schema-DexV8vX3.js} +24 -4
  43. package/dist/local-config.d.ts +39 -2
  44. package/dist/local-config.js +53 -2
  45. package/dist/logger.js +1 -1
  46. package/dist/loopback-fetch-CVNuN3YZ.js +40 -0
  47. package/dist/{local-config-sync-Dh1Croqe.d.ts → memory-token-prompter-BoV8Hf-n.d.ts} +30 -3
  48. package/dist/node-file-system-BOXIHW_Q.js +174 -0
  49. package/dist/{profiles-DSzTeKQw.js → profiles-ZHLONml4.js} +49 -49
  50. package/dist/{profiles-Cy5wXQ0L.d.ts → profiles-cVZQkM69.d.ts} +3 -3
  51. package/dist/profiles.d.ts +1 -1
  52. package/dist/profiles.js +1 -1
  53. package/dist/recovery.d.ts +1 -1
  54. package/dist/recovery.js +1 -1
  55. package/dist/resolve-connector-token-DxDG9mhf.js +22 -0
  56. package/dist/{schedule-connector-L4uzg5M8.js → schedule-connector-9k3gOIgl.js} +54 -55
  57. package/dist/schedule-connector-schema-Z0RXLgPI.d.ts +49 -0
  58. package/dist/settings-reader-BNxjsxCB.d.ts +27 -0
  59. package/dist/{settings-store-CUKSeTXC.js → settings-store-C2QdOH-t.js} +23 -4
  60. package/dist/slack-connector-CxpWagbT.js +388 -0
  61. package/dist/slack-event-processor-BhCf5Wiy.d.ts +95 -0
  62. package/dist/slack-event-processor-xFDG3US0.js +176 -0
  63. package/dist/slot-fields-D-pvMgTK.js +249 -0
  64. package/dist/{memory-diagnostic-log-CI60kNfB.js → sqlite-diagnostic-log-DOTPW-tG.js} +373 -249
  65. package/dist/{yaml-render-93pX7EF7.js → yaml-render--J1_3BSA.js} +25 -21
  66. package/package.json +2 -4
  67. package/dist/discord-connector-BL36yvbL.js +0 -250
  68. package/dist/gateway-base-url-Dy4Ykuoh.js +0 -14
  69. package/dist/gh-connector-DpiixfQZ.js +0 -226
  70. package/dist/http-client-oICicjuO.d.ts +0 -18
  71. package/dist/memory-token-prompter-B4sjyaAq.d.ts +0 -57
  72. package/dist/memory-token-prompter-CZde7e6y.js +0 -61
  73. package/dist/node-file-system-Blr8pAir.js +0 -48
  74. package/dist/settings-reader-BIFB_j2f.d.ts +0 -18
  75. package/dist/slack-connector-DQIFPdBF.js +0 -484
  76. package/dist/slot-fields-CMoRpwuy.js +0 -45
  77. /package/dist/{connector-adapter-DU9Rvyec.js → connector-adapter-Dvs8N7ew.js} +0 -0
  78. /package/dist/{connector-listener-DR3aKOuK.js → connector-listener-mPGZYa8e.js} +0 -0
  79. /package/dist/{diagnostic-sql-reader-C9zR-Csp.js → diagnostic-sql-reader-oXZnWFf_.js} +0 -0
  80. /package/dist/{discord-connector-schema-B_N6IXLz.js → discord-connector-schema-B4YpWpR3.js} +0 -0
  81. /package/dist/{error-message-of-Byi4y0Uf.js → error-message-of-ColuYmAk.js} +0 -0
  82. /package/dist/{funnel-log-sqlite-sink-kqJbx2H7.js → funnel-log-sqlite-sink-DLYkY0pZ.js} +0 -0
  83. /package/dist/{funnel-recovery-BFdPjL6Z.js → funnel-recovery-DKnEutUS.js} +0 -0
  84. /package/dist/{node-http-client-lowp60Oa.js → node-http-client-u00atiKx.js} +0 -0
  85. /package/dist/{schedule-connector-schema-CfyuMCMh.js → schedule-connector-schema-DKEPZnVv.js} +0 -0
  86. /package/dist/{settings-reader-CtQ-Ix8_.js → settings-reader-9FcX3qS1.js} +0 -0
  87. /package/dist/{settings-schema-D1xcOqRu.d.ts → settings-schema-BL_c2Udm.d.ts} +0 -0
  88. /package/dist/{slack-connector-schema-C1zEf4TG.js → slack-connector-schema-Dem8to4P.js} +0 -0
@@ -0,0 +1,250 @@
1
+ import { t as NodeFunnelHttpClient } from "./node-http-client-u00atiKx.js";
2
+ import { t as errorMessageOf } from "./error-message-of-ColuYmAk.js";
3
+ import { t as discordConnectorSchema } from "./discord-connector-schema-B4YpWpR3.js";
4
+ import { t as FunnelConnectorAdapter } from "./connector-adapter-Dvs8N7ew.js";
5
+ import { t as resolveConnectorToken } from "./resolve-connector-token-DxDG9mhf.js";
6
+ import { i as resolveFlumeDeps, n as FunnelFlumeSourceListener, r as flumeLogHandler, t as slotFields } from "./slot-fields-D-pvMgTK.js";
7
+ import { FlumeDiscordGatewayIntents, FlumeDiscordSource } from "@interactive-inc/flume/discord";
8
+ //#region lib/engine/connectors/discord-adapter.ts
9
+ const DISCORD_API_BASE = "https://discord.com/api/v10";
10
+ const defaultHttp = new NodeFunnelHttpClient();
11
+ var FunnelDiscordAdapter = class extends FunnelConnectorAdapter {
12
+ token;
13
+ http;
14
+ constructor(deps) {
15
+ super();
16
+ this.token = resolveConnectorToken({
17
+ literal: deps.config.botToken,
18
+ envVar: deps.config.botTokenEnv,
19
+ env: deps.env ?? process.env,
20
+ label: `${deps.config.name}.botToken`
21
+ });
22
+ this.http = deps.http ?? defaultHttp;
23
+ Object.freeze(this);
24
+ }
25
+ async call(input) {
26
+ const method = (input.method || "GET").toUpperCase();
27
+ const path = input.path.startsWith("/") ? input.path : `/${input.path}`;
28
+ const body = input.body;
29
+ const hasBody = body !== null && typeof body === "object" && method !== "GET" && Object.keys(body).length > 0;
30
+ const res = await this.http.fetch({
31
+ method,
32
+ url: `${DISCORD_API_BASE}${path}`,
33
+ headers: {
34
+ Authorization: `Bot ${this.token}`,
35
+ "Content-Type": "application/json"
36
+ },
37
+ body: hasBody ? JSON.stringify(input.body) : void 0
38
+ });
39
+ if (!res.ok) throw new Error(`Discord API failed (${res.status}): ${await res.text()}`);
40
+ if (res.status === 204) return null;
41
+ return await res.json();
42
+ }
43
+ };
44
+ //#endregion
45
+ //#region lib/engine/connectors/discord-event-processor.ts
46
+ var FunnelDiscordEventProcessor = class {
47
+ ownUserId;
48
+ constructor(props) {
49
+ this.ownUserId = props.ownUserId;
50
+ }
51
+ process(message) {
52
+ if (message.authorIsBot) return { skip: true };
53
+ const mentioned = this.ownUserId ? message.mentionedUserIds.includes(this.ownUserId) : false;
54
+ return {
55
+ skip: false,
56
+ content: JSON.stringify(message.raw),
57
+ meta: {
58
+ event_type: "discord",
59
+ channel_id: message.channelId,
60
+ user_id: message.authorId,
61
+ mentioned: String(mentioned),
62
+ guild_id: message.guildId ?? ""
63
+ }
64
+ };
65
+ }
66
+ };
67
+ //#endregion
68
+ //#region lib/engine/connectors/discord-flume-listener.ts
69
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
70
+ const readString = (record, key) => {
71
+ const value = record[key];
72
+ return typeof value === "string" ? value : void 0;
73
+ };
74
+ /**
75
+ * Discord listener backed by `@interactive-inc/flume`'s `FlumeDiscordSource`
76
+ * (raw Gateway WebSocket + Zod). The processor layer
77
+ * (`FunnelDiscordEventProcessor`) is the application layer — self-skip,
78
+ * mention detection, meta shaping. Self-detection requires the bot's own
79
+ * user id, which Discord does not surface until READY; we read it from the
80
+ * READY payload on the first dispatch and build the processor then. Events
81
+ * seen before READY are impossible by protocol, so no event is lost.
82
+ */
83
+ var FunnelFlumeDiscordListener = class extends FunnelFlumeSourceListener {
84
+ config;
85
+ env;
86
+ flumeDeps;
87
+ signal;
88
+ processor = null;
89
+ constructor(deps) {
90
+ super({
91
+ type: "discord",
92
+ connectorId: deps.config.id,
93
+ channelId: deps.channelId ?? null,
94
+ logger: deps.logger,
95
+ diagnosticLog: deps.diagnosticLog
96
+ });
97
+ this.config = deps.config;
98
+ this.env = deps.env ?? process.env;
99
+ this.flumeDeps = deps.flumeDeps ?? {};
100
+ this.signal = deps.signal;
101
+ }
102
+ async start(notify) {
103
+ this.diagnostics.recordConnection("started", "");
104
+ let token;
105
+ try {
106
+ token = resolveConnectorToken({
107
+ literal: this.config.botToken,
108
+ envVar: this.config.botTokenEnv,
109
+ env: this.env,
110
+ label: `${this.config.name}.botToken`
111
+ });
112
+ } catch (error) {
113
+ this.diagnostics.recordConnection("auth-failed", errorMessageOf(error));
114
+ throw error;
115
+ }
116
+ const source = new FlumeDiscordSource({
117
+ token,
118
+ intents: FlumeDiscordGatewayIntents.Guilds | FlumeDiscordGatewayIntents.GuildMessages | FlumeDiscordGatewayIntents.MessageContent | FlumeDiscordGatewayIntents.DirectMessages
119
+ });
120
+ await this.runStart({
121
+ source,
122
+ onLog: flumeLogHandler(this.logger),
123
+ deps: resolveFlumeDeps(this.flumeDeps),
124
+ signal: this.signal,
125
+ onEvent: (event) => {
126
+ if (event.source !== "discord") return Promise.resolve();
127
+ return this.handleEvent(event, notify);
128
+ }
129
+ });
130
+ }
131
+ onStop() {
132
+ this.processor = null;
133
+ }
134
+ async handleEvent(event, notify) {
135
+ if (event.type === "READY") {
136
+ this.adoptReady(event.data);
137
+ return;
138
+ }
139
+ if (!this.processor) {
140
+ const skipId = crypto.randomUUID();
141
+ this.diagnostics.recordRaw(skipId, JSON.stringify(event.data));
142
+ this.diagnostics.recordProcessed(skipId, "skip:pre-ready", "");
143
+ return;
144
+ }
145
+ const data = event.data;
146
+ const author = isRecord(data.author) ? data.author : null;
147
+ const authorIsBot = author !== null && author.bot === true;
148
+ const authorId = event.meta.user_id ?? "";
149
+ const channelId = event.meta.channel_id ?? "";
150
+ const guildId = event.meta.guild_id ?? null;
151
+ const mentions = Array.isArray(data.mentions) ? data.mentions.map((m) => isRecord(m) ? readString(m, "id") ?? "" : "").filter((id) => id !== "") : [];
152
+ const eventId = crypto.randomUUID();
153
+ const rawJson = JSON.stringify(data);
154
+ this.diagnostics.recordRaw(eventId, rawJson);
155
+ const result = this.processor.process({
156
+ authorId,
157
+ authorIsBot,
158
+ channelId,
159
+ guildId,
160
+ mentionedUserIds: mentions,
161
+ raw: data
162
+ });
163
+ if (result.skip) {
164
+ this.diagnostics.recordProcessed(eventId, "skip:bot", rawJson);
165
+ this.logger?.info("discord skip", { reason: "bot author" });
166
+ return;
167
+ }
168
+ await this.deliver(notify, eventId, rawJson, result.content, result.meta);
169
+ }
170
+ adoptReady(data) {
171
+ const fromUser = isRecord(data.user) ? readString(data.user, "id") : void 0;
172
+ const fromTop = readString(data, "user_id");
173
+ const ownUserId = fromUser ?? fromTop ?? "";
174
+ if (!ownUserId) {
175
+ const skipId = crypto.randomUUID();
176
+ this.diagnostics.recordRaw(skipId, JSON.stringify(data));
177
+ this.diagnostics.recordProcessed(skipId, "skip:ready-missing-user-id", "");
178
+ this.diagnostics.recordConnection("error", "discord READY payload had neither user.id nor user_id; processor not initialized");
179
+ this.logger?.error("discord READY missing user id", { connector: this.config.name });
180
+ return;
181
+ }
182
+ this.processor = new FunnelDiscordEventProcessor({ ownUserId });
183
+ }
184
+ async deliver(notify, eventId, rawJson, content, meta) {
185
+ try {
186
+ await notify(content, meta);
187
+ } catch (error) {
188
+ this.diagnostics.recordProcessed(eventId, "emitted:delivery-failed", content || rawJson);
189
+ this.logger?.error("discord notify error", { error: errorMessageOf(error) });
190
+ return;
191
+ }
192
+ this.diagnostics.recordProcessed(eventId, "emitted", content);
193
+ }
194
+ };
195
+ //#endregion
196
+ //#region lib/engine/connectors/discord-connector.ts
197
+ /**
198
+ * Discord connector descriptor. Pass `discordConnector()` to
199
+ * `new Funnel({ connectors: [...] })` to enable the type.
200
+ *
201
+ * The listener is backed by `@interactive-inc/flume`'s `FlumeDiscordSource`
202
+ * (raw Gateway WebSocket).
203
+ */
204
+ const discordConnector = () => ({
205
+ type: "discord",
206
+ toolExposed: true,
207
+ createListener(config, deps) {
208
+ return new FunnelFlumeDiscordListener({
209
+ config: discordConnectorSchema.parse(config),
210
+ channelId: deps.channelId,
211
+ logger: deps.logger,
212
+ diagnosticLog: deps.diagnosticLog,
213
+ signal: deps.signal
214
+ });
215
+ },
216
+ createAdapter(config, deps) {
217
+ return new FunnelDiscordAdapter({
218
+ config: discordConnectorSchema.parse(config),
219
+ http: deps.http
220
+ });
221
+ },
222
+ secretTokens(config) {
223
+ return [discordConnectorSchema.parse(config).botToken].filter((token) => token !== void 0);
224
+ },
225
+ buildConfig(input, context) {
226
+ return discordConnectorSchema.parse({
227
+ id: context.id,
228
+ type: "discord",
229
+ name: input.name,
230
+ ...typeof input.botToken === "string" ? { botToken: input.botToken } : {},
231
+ ...typeof input.botTokenEnv === "string" ? { botTokenEnv: input.botTokenEnv } : {},
232
+ createdAt: context.now,
233
+ updatedAt: context.now
234
+ });
235
+ },
236
+ applyUpdate(config, fields, context) {
237
+ const current = discordConnectorSchema.parse(config);
238
+ return discordConnectorSchema.parse({
239
+ id: current.id,
240
+ name: current.name,
241
+ type: "discord",
242
+ createdAt: current.createdAt,
243
+ updatedAt: context.now,
244
+ ...slotFields("botToken", "botTokenEnv", fields, current)
245
+ });
246
+ },
247
+ operations: {}
248
+ });
249
+ //#endregion
250
+ export { FunnelDiscordAdapter as i, FunnelFlumeDiscordListener as n, FunnelDiscordEventProcessor as r, discordConnector as t };
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+
3
+ //#region lib/engine/connectors/discord-connector-schema.d.ts
4
+ /**
5
+ * Like slack, a discord connector holds either a literal `botToken` or a
6
+ * `botTokenEnv` reference resolved from `process.env` at listener start. The
7
+ * reference form keeps the secret out of settings.json, but is only set through
8
+ * the engine API (`new Funnel(...)`); funnel.json and the `fnl` CLI produce
9
+ * literals.
10
+ */
11
+ declare const discordConnectorSchema: z.ZodObject<{
12
+ id: z.ZodString;
13
+ name: z.ZodString;
14
+ type: z.ZodLiteral<"discord">;
15
+ botToken: z.ZodOptional<z.ZodString>;
16
+ botTokenEnv: z.ZodOptional<z.ZodString>;
17
+ createdAt: z.ZodOptional<z.ZodString>;
18
+ updatedAt: z.ZodOptional<z.ZodString>;
19
+ }, z.core.$strip>;
20
+ type DiscordConnectorConfig = z.infer<typeof discordConnectorSchema>;
21
+ //#endregion
22
+ export { discordConnectorSchema as n, DiscordConnectorConfig as t };
package/dist/docs.js CHANGED
@@ -1,2 +1,2 @@
1
- import { t as FunnelDocs } from "./funnel-docs-BxXZ9Ksx.js";
1
+ import { t as FunnelDocs } from "./funnel-docs-C-ge0MuB.js";
2
2
  export { FunnelDocs };
package/dist/doctor.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { n as DoctorReport, r as FunnelDoctor, t as DoctorFixMode } from "./funnel-doctor-CZf_0Luq.js";
1
+ import { n as DoctorReport, r as FunnelDoctor, t as DoctorFixMode } from "./funnel-doctor-CnRQi4kM.js";
2
2
  export { DoctorFixMode, DoctorReport, FunnelDoctor };
package/dist/doctor.js CHANGED
@@ -1,2 +1,2 @@
1
- import { t as FunnelDoctor } from "./funnel-doctor-DiJCjHsg.js";
1
+ import { t as FunnelDoctor } from "./funnel-doctor-XrI2GBH8.js";
2
2
  export { FunnelDoctor };
@@ -1,7 +1,7 @@
1
- import { t as ChannelConfig } from "./settings-schema-D1xcOqRu.js";
2
- import { n as FunnelIdGenerator } from "./settings-reader-BIFB_j2f.js";
3
- import { B as FunnelLogger, I as FunnelProcessRunner } from "./connector-descriptor-6SXJoszo.js";
4
- import { n as FunnelFileSystem } from "./file-system-o51IsM0W.js";
1
+ import { t as ChannelConfig } from "./settings-schema-BL_c2Udm.js";
2
+ import { n as FunnelIdGenerator } from "./settings-reader-BNxjsxCB.js";
3
+ import { B as FunnelProcessRunner, W as FunnelLogger } from "./connector-descriptor-BFIhyTfa.js";
4
+ import { n as FunnelFileSystem } from "./file-system-VhwwXZbm.js";
5
5
 
6
6
  //#region lib/engine/claude/channel-resolver.d.ts
7
7
  type ChannelResolver = {
@@ -136,7 +136,7 @@ type Deps = {
136
136
  process?: FunnelProcessRunner;
137
137
  dir?: string;
138
138
  };
139
- declare class FileProcessGuard implements ProcessGuard {
139
+ declare class FunnelFileProcessGuard implements ProcessGuard {
140
140
  private readonly fs;
141
141
  private readonly process;
142
142
  private readonly pidDir;
@@ -148,4 +148,4 @@ declare class FileProcessGuard implements ProcessGuard {
148
148
  private readRecord;
149
149
  }
150
150
  //#endregion
151
- export { ProcessGuard as a, ChannelResolver as c, SessionStore as i, FunnelClaude as n, McpInstaller as o, LaunchOptions as r, GatewayController as s, FileProcessGuard as t };
151
+ export { ProcessGuard as a, ChannelResolver as c, SessionStore as i, FunnelClaude as n, McpInstaller as o, LaunchOptions as r, GatewayController as s, FunnelFileProcessGuard as t };
@@ -21,6 +21,14 @@ declare abstract class FunnelFileSystem {
21
21
  }): void;
22
22
  abstract readdirSync(path: string): string[];
23
23
  abstract statSync(path: string): FileStat;
24
+ /**
25
+ * Run `fn` while holding an exclusive lock on `lockPath`. The lock file is
26
+ * created atomically (`O_EXCL`) so two processes cannot both enter. A stale
27
+ * lock whose owning pid is no longer alive is forcibly broken (this is what
28
+ * keeps a SIGKILL'd CLI command from wedging the lock forever). The Memory
29
+ * impl is a no-op because tests are single-threaded.
30
+ */
31
+ abstract withFileLock<T>(lockPath: string, fn: () => T): T;
24
32
  }
25
33
  //#endregion
26
34
  export { FunnelFileSystem as n, FileStat as t };
@@ -0,0 +1,133 @@
1
+ import { W as FunnelLogger, _ as ConnectorDiagnosticLog, g as ConnectorConnectionStatus, j as FunnelConnectorListener } from "./connector-descriptor-BFIhyTfa.js";
2
+ import { FlumeEventHandler, FlumeLogHandler, FlumeReconnectOptions, FlumeRunning, FlumeRuntimeDeps, FlumeSource, FlumeStatus } from "@interactive-inc/flume";
3
+
4
+ //#region lib/engine/connectors/connector-diagnostics-recorder.d.ts
5
+ type Props$1 = {
6
+ type: string;
7
+ connectorId: string | null;
8
+ channelId: string | null;
9
+ log: ConnectorDiagnosticLog | undefined;
10
+ };
11
+ /**
12
+ * Wraps a `ConnectorDiagnosticLog` with the per-listener axes (`type` /
13
+ * `connectorId` / `channelId`) so call sites only pass the row-specific
14
+ * fields. When no log is wired every call is a silent no-op.
15
+ */
16
+ declare class FunnelConnectorDiagnosticsRecorder {
17
+ private readonly props;
18
+ constructor(props: Props$1);
19
+ recordRaw(eventId: string, payload: string): void;
20
+ recordProcessed(eventId: string, outcome: string, payload: string): void;
21
+ recordConnection(status: ConnectorConnectionStatus, detail: string): void;
22
+ }
23
+ //#endregion
24
+ //#region lib/engine/connectors/flume-source-listener.d.ts
25
+ type Props = {
26
+ /** Funnel connector type ("slack" / "discord" / "gh") — stamped onto diagnostic rows. */type: string;
27
+ connectorId: string;
28
+ channelId: string | null;
29
+ logger?: FunnelLogger;
30
+ diagnosticLog?: ConnectorDiagnosticLog;
31
+ };
32
+ type RunStartOptions = {
33
+ source: FlumeSource; /** Typed event handler — receives the source's events only (logs are routed separately). */
34
+ onEvent: FlumeEventHandler; /** Optional log handler for everything the firehose emits (including status transitions). */
35
+ onLog?: FlumeLogHandler;
36
+ deps?: FlumeRuntimeDeps;
37
+ /**
38
+ * Optional AbortSignal forwarded to the underlying Flume. When aborted, the
39
+ * Flume auto-closes every source and resolves to `FlumeClosed`. Use this to
40
+ * propagate a host-level shutdown (SIGTERM, supervisor stop, parent timeout)
41
+ * down to the WebSocket layer without racing through `stop()`.
42
+ */
43
+ signal?: AbortSignal;
44
+ /**
45
+ * Reconnect policy override forwarded to the Flume. The base class enables
46
+ * reconnect with Flume's defaults (infinite attempts, 1s base / 30s max
47
+ * exponential backoff with jitter) so a wifi drop or upstream socket close
48
+ * is auto-recovered. Subclasses can pass a stricter `{ maxAttempts, ... }`
49
+ * or `false` to opt out of reconnect entirely. Defaults to `true`.
50
+ */
51
+ reconnect?: boolean | FlumeReconnectOptions;
52
+ };
53
+ /**
54
+ * Status event reconstructed from the firehose `status` log entry. Funnel
55
+ * keeps its own shape because Flume 0.9 collapsed the dedicated `onStatus`
56
+ * callback into the unified `onEvent` firehose — state transitions now arrive
57
+ * as `log.action === "status"` entries with `detail: { from, to, reason }`.
58
+ */
59
+ type StatusEvent = {
60
+ source: string;
61
+ status: FlumeStatus;
62
+ detail: string | null;
63
+ };
64
+ /**
65
+ * Shared lifecycle for any listener whose transport is a `FlumeSource`. Owns
66
+ * the per-listener `Flume` instance + the `FlumeRunning` handle returned by
67
+ * `open()`, the connected/alive bit, the `FlumeStatus ↔
68
+ * ConnectorConnectionStatus` mapping, and the close sequence — every Flume
69
+ * subclass plugs in only its own token resolution, source construction, and
70
+ * event dispatch around this skeleton.
71
+ *
72
+ * Flume 0.9 collapsed every observation channel into one firehose: events
73
+ * and all logs arrive through `onEvent` as a discriminated union
74
+ * (`{ kind: "event" } | { kind: "log" }`). This base class splits that back
75
+ * into the funnel-shaped trio (typed event handler, log forward, status
76
+ * mapping) so subclasses keep their per-protocol code unchanged.
77
+ */
78
+ declare abstract class FunnelFlumeSourceListener extends FunnelConnectorListener {
79
+ protected readonly logger: FunnelLogger | undefined;
80
+ protected readonly diagnostics: FunnelConnectorDiagnosticsRecorder;
81
+ protected readonly type: string;
82
+ protected running: FlumeRunning | null;
83
+ protected connected: boolean;
84
+ /**
85
+ * Flipped on by Flume's `reconnecting` status, off when the new socket
86
+ * lands on `connected` or the source gives up with `disconnected`. Used by
87
+ * `isAlive()` to treat a brief reconnect window as "still alive" so the
88
+ * supervisor does not preempt Flume's in-progress recovery with a heavier
89
+ * stop+start cycle (which would discard auth.test results and rebuild
90
+ * every per-listener state).
91
+ */
92
+ protected reconnecting: boolean;
93
+ /**
94
+ * Promise chain that serializes typed-event delivery. Flume's emitItem
95
+ * fire-and-forgets the onEvent callback (see flume.ts emitItem:
96
+ * `Promise.resolve(onEvent(item)).catch(() => {})`), so awaiting onEvent
97
+ * inside the handler does NOT pause flume's source queue — multiple event
98
+ * deliveries would race their microtask chains. Chaining each new event
99
+ * onto the previous promise's `.then(...)` guarantees per-listener
100
+ * end-to-end FIFO regardless of whether the notify path is sync or async.
101
+ */
102
+ private deliveryChain;
103
+ constructor(props: Props);
104
+ /**
105
+ * Assemble a single-source Flume, open it, and store the `FlumeRunning`
106
+ * handle. Records `error` on any `Error` returned by `flume.open()` and
107
+ * rethrows so the supervisor sees the failure.
108
+ *
109
+ * The firehose handler routes:
110
+ * - `kind: "event"` → subclass's typed `onEvent`
111
+ * - `kind: "log"` with `action === "status"` → `handleStatus()`
112
+ * - `kind: "log"` (any) → optional `onLog` handler
113
+ */
114
+ protected runStart(options: RunStartOptions): Promise<void>;
115
+ stop(): Promise<void>;
116
+ isAlive(): boolean;
117
+ /**
118
+ * Maps Flume's transport status to the connection table. `reconnecting`
119
+ * deliberately produces no row — Flume drives many transient reconnects per
120
+ * minute on a flaky network, and the row would drown the more meaningful
121
+ * `connected`/`disconnected` pair. The `reconnecting` flag still flips so
122
+ * `isAlive()` can surface it to the supervisor.
123
+ */
124
+ protected handleStatus(event: StatusEvent): void;
125
+ /**
126
+ * Hook for subclass-specific cleanup that has to run inside the stop()
127
+ * finally block (after the running handle is cleared, before the `stopped`
128
+ * row is recorded). Default is no-op.
129
+ */
130
+ protected onStop(): void;
131
+ }
132
+ //#endregion
133
+ export { FunnelFlumeSourceListener as t };