@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,388 @@
1
+ import { t as NodeFunnelHttpClient } from "./node-http-client-u00atiKx.js";
2
+ import { t as FunnelAuthFailedError } from "./funnel-error-0t1MK1R6.js";
3
+ import { t as errorMessageOf } from "./error-message-of-ColuYmAk.js";
4
+ import { t as slackConnectorSchema } from "./slack-connector-schema-Dem8to4P.js";
5
+ import { t as FunnelConnectorAdapter } from "./connector-adapter-Dvs8N7ew.js";
6
+ import { t as FunnelSlackEventProcessor } from "./slack-event-processor-xFDG3US0.js";
7
+ import { t as resolveConnectorToken } from "./resolve-connector-token-DxDG9mhf.js";
8
+ import { i as resolveFlumeDeps, n as FunnelFlumeSourceListener, r as flumeLogHandler, t as slotFields } from "./slot-fields-D-pvMgTK.js";
9
+ import { z } from "zod";
10
+ import { FlumeSlackSource } from "@interactive-inc/flume/slack";
11
+ //#region lib/engine/connectors/slack-adapter.ts
12
+ const SLACK_API_BASE = "https://slack.com/api/";
13
+ const toRecord = (value) => {
14
+ const result = {};
15
+ for (const [key, val] of Object.entries(value)) result[key] = val;
16
+ return result;
17
+ };
18
+ /**
19
+ * Slack Web API adapter over the injected `FunnelHttpClient`. `call()` posts
20
+ * to `https://slack.com/api/<method>` with `Authorization: Bearer <botToken>`
21
+ * and returns the parsed JSON body verbatim — Slack signals failures with
22
+ * `{ ok: false, error: "..." }` in a 200 response, so we surface that body
23
+ * unchanged and let the caller inspect `ok`.
24
+ */
25
+ var FunnelSlackAdapter = class extends FunnelConnectorAdapter {
26
+ token;
27
+ http;
28
+ constructor(deps) {
29
+ super();
30
+ this.token = resolveConnectorToken({
31
+ literal: deps.config.botToken,
32
+ envVar: deps.config.botTokenEnv,
33
+ env: deps.env ?? process.env,
34
+ label: `${deps.config.name}.botToken`
35
+ });
36
+ this.http = deps.http ?? new NodeFunnelHttpClient();
37
+ Object.freeze(this);
38
+ }
39
+ async call(input) {
40
+ const url = `${SLACK_API_BASE}${input.path}`;
41
+ const body = input.body !== null && typeof input.body === "object" ? toRecord(input.body) : {};
42
+ const form = new URLSearchParams();
43
+ for (const [key, value] of Object.entries(body)) form.set(key, typeof value === "string" ? value : JSON.stringify(value));
44
+ const text = await (await this.http.fetch({
45
+ method: "POST",
46
+ url,
47
+ headers: {
48
+ Authorization: `Bearer ${this.token}`,
49
+ "Content-Type": "application/x-www-form-urlencoded"
50
+ },
51
+ body: form.toString()
52
+ })).text();
53
+ try {
54
+ return JSON.parse(text);
55
+ } catch {
56
+ return {
57
+ ok: false,
58
+ error: `non-JSON response: ${text.slice(0, 200)}`
59
+ };
60
+ }
61
+ }
62
+ async postMessage(props) {
63
+ return this.call({
64
+ method: "post",
65
+ path: "chat.postMessage",
66
+ body: {
67
+ channel: props.channel,
68
+ text: props.text,
69
+ ...props.threadTs ? { thread_ts: props.threadTs } : {}
70
+ }
71
+ });
72
+ }
73
+ async addReaction(props) {
74
+ return this.call({
75
+ method: "post",
76
+ path: "reactions.add",
77
+ body: {
78
+ channel: props.channel,
79
+ timestamp: props.timestamp,
80
+ name: props.name
81
+ }
82
+ });
83
+ }
84
+ async removeReaction(props) {
85
+ return this.call({
86
+ method: "post",
87
+ path: "reactions.remove",
88
+ body: {
89
+ channel: props.channel,
90
+ timestamp: props.timestamp,
91
+ name: props.name
92
+ }
93
+ });
94
+ }
95
+ };
96
+ //#endregion
97
+ //#region lib/engine/connectors/slack-flume-listener.ts
98
+ const authTestResponseSchema = z.object({
99
+ ok: z.boolean(),
100
+ user_id: z.string().optional(),
101
+ bot_id: z.string().optional(),
102
+ error: z.string().optional()
103
+ });
104
+ const AUTH_TEST_URL = "https://slack.com/api/auth.test";
105
+ /**
106
+ * Slack listener backed by `@interactive-inc/flume`'s `FlumeSlackSource` (raw
107
+ * Socket Mode WebSocket + Zod). The processor layer
108
+ * (`FunnelSlackEventProcessor`) is the application layer — self-skip, mention
109
+ * detection, dedup, minify. Self-detection needs `auth.test` to learn the
110
+ * bot's own user/bot id, which the listener calls once at start using the
111
+ * bot token. Flume delivers the events API envelope and nothing else; Bolt's
112
+ * `app.action` / `app.command` / `preprocessEvent` hooks have no equivalent
113
+ * here and must be re-implemented against Slack's HTTP endpoints if needed.
114
+ */
115
+ var FunnelFlumeSlackListener = class extends FunnelFlumeSourceListener {
116
+ config;
117
+ env;
118
+ flumeDeps;
119
+ http;
120
+ signal;
121
+ preprocessEvent;
122
+ onInteractive;
123
+ processor = null;
124
+ botToken = "";
125
+ constructor(deps) {
126
+ super({
127
+ type: "slack",
128
+ connectorId: deps.config.id,
129
+ channelId: deps.channelId ?? null,
130
+ logger: deps.logger,
131
+ diagnosticLog: deps.diagnosticLog
132
+ });
133
+ this.config = deps.config;
134
+ this.env = deps.env ?? process.env;
135
+ this.flumeDeps = deps.flumeDeps ?? {};
136
+ this.http = deps.http ?? new NodeFunnelHttpClient();
137
+ this.signal = deps.signal;
138
+ this.preprocessEvent = deps.preprocessEvent;
139
+ this.onInteractive = deps.onInteractive;
140
+ }
141
+ async start(notify) {
142
+ this.diagnostics.recordConnection("started", "");
143
+ let appToken;
144
+ let botToken;
145
+ try {
146
+ appToken = resolveConnectorToken({
147
+ literal: this.config.appToken,
148
+ envVar: this.config.appTokenEnv,
149
+ env: this.env,
150
+ label: `${this.config.name}.appToken`
151
+ });
152
+ botToken = resolveConnectorToken({
153
+ literal: this.config.botToken,
154
+ envVar: this.config.botTokenEnv,
155
+ env: this.env,
156
+ label: `${this.config.name}.botToken`
157
+ });
158
+ } catch (error) {
159
+ this.diagnostics.recordConnection("auth-failed", errorMessageOf(error));
160
+ throw error;
161
+ }
162
+ this.botToken = botToken;
163
+ const auth = await this.callAuthTest();
164
+ if (!auth.ok) {
165
+ const detail = auth.error ?? "auth.test returned ok=false";
166
+ this.diagnostics.recordConnection("auth-failed", detail);
167
+ throw new FunnelAuthFailedError(this.config.name, detail);
168
+ }
169
+ this.processor = new FunnelSlackEventProcessor({
170
+ ownBotUserId: auth.user_id ?? "",
171
+ ownBotId: auth.bot_id ?? "",
172
+ minify: this.config.minify
173
+ });
174
+ const source = new FlumeSlackSource({
175
+ appToken,
176
+ botToken: this.botToken
177
+ });
178
+ await this.runStart({
179
+ source,
180
+ onLog: flumeLogHandler(this.logger),
181
+ deps: resolveFlumeDeps(this.flumeDeps),
182
+ signal: this.signal,
183
+ onEvent: (event) => {
184
+ if (event.source !== "slack") return Promise.resolve();
185
+ return this.handleEvent(event, notify);
186
+ }
187
+ });
188
+ }
189
+ onStop() {
190
+ this.processor = null;
191
+ }
192
+ async handleInteractive(payload) {
193
+ const eventId = crypto.randomUUID();
194
+ const rawJson = JSON.stringify(payload);
195
+ this.diagnostics.recordRaw(eventId, rawJson);
196
+ if (!this.onInteractive) {
197
+ this.diagnostics.recordProcessed(eventId, "skip:no-interactive-handler", "");
198
+ return;
199
+ }
200
+ const subtype = typeof payload.type === "string" ? payload.type : "unknown";
201
+ try {
202
+ await this.onInteractive(payload);
203
+ this.diagnostics.recordProcessed(eventId, `interactive:${subtype}`, "");
204
+ } catch (error) {
205
+ const message = errorMessageOf(error);
206
+ this.diagnostics.recordProcessed(eventId, `interactive-error:${subtype}`, message);
207
+ this.logger?.error(`slack interactive handler error (${subtype})`, { error: message });
208
+ }
209
+ }
210
+ async callAuthTest() {
211
+ let text;
212
+ try {
213
+ text = await (await this.http.fetch({
214
+ method: "POST",
215
+ url: AUTH_TEST_URL,
216
+ headers: {
217
+ Authorization: `Bearer ${this.botToken}`,
218
+ "Content-Type": "application/x-www-form-urlencoded"
219
+ }
220
+ })).text();
221
+ } catch (error) {
222
+ this.diagnostics.recordConnection("auth-failed", errorMessageOf(error));
223
+ throw error;
224
+ }
225
+ const parsed = authTestResponseSchema.safeParse(safeJsonParse(text));
226
+ if (!parsed.success) return {
227
+ ok: false,
228
+ error: `non-JSON auth.test response: ${text.slice(0, 200)}`
229
+ };
230
+ return parsed.data;
231
+ }
232
+ async handleEvent(event, notify) {
233
+ if (!this.processor) return;
234
+ if (event.type === "interactive") {
235
+ await this.handleInteractive(event.data);
236
+ return;
237
+ }
238
+ const rawEvent = event.data.event;
239
+ if (!isSlackRawEvent(rawEvent)) {
240
+ const skipId = crypto.randomUUID();
241
+ this.diagnostics.recordRaw(skipId, JSON.stringify(event.data));
242
+ this.diagnostics.recordProcessed(skipId, "skip:non-object-event", "");
243
+ return;
244
+ }
245
+ const eventId = crypto.randomUUID();
246
+ const rawJson = JSON.stringify(rawEvent);
247
+ this.diagnostics.recordRaw(eventId, rawJson);
248
+ let preprocessed = rawEvent;
249
+ if (this.preprocessEvent) {
250
+ const next = await this.preprocessEvent(rawEvent);
251
+ if (next === null) {
252
+ this.diagnostics.recordProcessed(eventId, "skip:preprocess", rawJson);
253
+ return;
254
+ }
255
+ preprocessed = next;
256
+ }
257
+ const result = this.processor.process(preprocessed);
258
+ if (result.skip) {
259
+ this.diagnostics.recordProcessed(eventId, result.reason, rawJson);
260
+ return;
261
+ }
262
+ await this.deliver(notify, eventId, rawJson, result.content, result.meta, result.shouldReact);
263
+ }
264
+ async deliver(notify, eventId, rawJson, content, meta, shouldReact) {
265
+ try {
266
+ await notify(content, meta);
267
+ } catch (error) {
268
+ this.diagnostics.recordProcessed(eventId, "emitted:delivery-failed", content || rawJson);
269
+ this.logger?.error("slack notify error", { error: errorMessageOf(error) });
270
+ return;
271
+ }
272
+ this.diagnostics.recordProcessed(eventId, "emitted", content);
273
+ if (shouldReact) this.postReaction(meta).catch((error) => {
274
+ this.diagnostics.recordProcessed(eventId, "emitted:reaction-failed", errorMessageOf(error));
275
+ this.logger?.warn("slack reaction failed", { error: errorMessageOf(error) });
276
+ });
277
+ }
278
+ async postReaction(meta) {
279
+ const res = await this.http.fetch({
280
+ method: "POST",
281
+ url: "https://slack.com/api/reactions.add",
282
+ headers: {
283
+ Authorization: `Bearer ${this.botToken}`,
284
+ "Content-Type": "application/x-www-form-urlencoded"
285
+ },
286
+ body: new URLSearchParams({
287
+ channel: meta.channel_id ?? "",
288
+ timestamp: meta.thread_ts ?? "",
289
+ name: "eyes"
290
+ }).toString()
291
+ });
292
+ const parsed = parseSlackResponse(await res.text());
293
+ if (!parsed.ok) throw new Error(`slack reactions.add: ${parsed.error ?? `status=${res.status}`}`);
294
+ }
295
+ };
296
+ const isSlackRawEvent = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
297
+ const safeJsonParse = (text) => {
298
+ try {
299
+ return JSON.parse(text);
300
+ } catch {
301
+ return null;
302
+ }
303
+ };
304
+ const slackResponseSchema = z.object({
305
+ ok: z.boolean(),
306
+ error: z.string().optional()
307
+ });
308
+ const parseSlackResponse = (text) => {
309
+ const parsed = slackResponseSchema.safeParse(safeJsonParse(text));
310
+ if (!parsed.success) return {
311
+ ok: false,
312
+ error: `non-JSON response: ${text.slice(0, 200)}`
313
+ };
314
+ return parsed.data;
315
+ };
316
+ //#endregion
317
+ //#region lib/engine/connectors/slack-connector.ts
318
+ /**
319
+ * Slack connector descriptor. Pass `slackConnector()` to
320
+ * `new Funnel({ connectors: [...] })` to enable the type.
321
+ *
322
+ * The listener is backed by `@interactive-inc/flume`'s `FlumeSlackSource`
323
+ * (raw Socket Mode WebSocket). Both the `events_api` envelope (messages,
324
+ * mentions, reactions, …) and the `interactive` envelope (block actions,
325
+ * view submissions, message actions, shortcuts) are delivered — the former
326
+ * runs through the funnel processor and emits notifications, the latter is
327
+ * handed to the optional `onInteractive` host hook (funnel auto-acks the
328
+ * envelope, so the host can respond via the Slack web API at its leisure).
329
+ * Slash commands (`slash_commands` envelope) and Bolt's middleware chain
330
+ * have no equivalent here yet — wire those via the Slack HTTP endpoints if
331
+ * needed.
332
+ */
333
+ const slackConnector = (options = {}) => ({
334
+ type: "slack",
335
+ toolExposed: true,
336
+ createListener(config, deps) {
337
+ return new FunnelFlumeSlackListener({
338
+ config: slackConnectorSchema.parse(config),
339
+ channelId: deps.channelId,
340
+ logger: deps.logger,
341
+ diagnosticLog: deps.diagnosticLog,
342
+ http: deps.http,
343
+ signal: deps.signal,
344
+ preprocessEvent: options.preprocessEvent,
345
+ onInteractive: options.onInteractive
346
+ });
347
+ },
348
+ createAdapter(config, deps) {
349
+ return new FunnelSlackAdapter({
350
+ config: slackConnectorSchema.parse(config),
351
+ http: deps.http
352
+ });
353
+ },
354
+ secretTokens(config) {
355
+ const parsed = slackConnectorSchema.parse(config);
356
+ return [parsed.botToken, parsed.appToken].filter((token) => token !== void 0);
357
+ },
358
+ buildConfig(input, context) {
359
+ return slackConnectorSchema.parse({
360
+ id: context.id,
361
+ type: "slack",
362
+ name: input.name,
363
+ ...typeof input.botToken === "string" ? { botToken: input.botToken } : {},
364
+ ...typeof input.appToken === "string" ? { appToken: input.appToken } : {},
365
+ ...typeof input.botTokenEnv === "string" ? { botTokenEnv: input.botTokenEnv } : {},
366
+ ...typeof input.appTokenEnv === "string" ? { appTokenEnv: input.appTokenEnv } : {},
367
+ minify: typeof input.minify === "boolean" ? input.minify : true,
368
+ createdAt: context.now,
369
+ updatedAt: context.now
370
+ });
371
+ },
372
+ applyUpdate(config, fields, context) {
373
+ const current = slackConnectorSchema.parse(config);
374
+ return slackConnectorSchema.parse({
375
+ id: current.id,
376
+ name: current.name,
377
+ type: "slack",
378
+ minify: current.minify,
379
+ createdAt: current.createdAt,
380
+ updatedAt: context.now,
381
+ ...slotFields("botToken", "botTokenEnv", fields, current),
382
+ ...slotFields("appToken", "appTokenEnv", fields, current)
383
+ });
384
+ },
385
+ operations: {}
386
+ });
387
+ //#endregion
388
+ export { FunnelFlumeSlackListener as n, FunnelSlackAdapter as r, slackConnector as t };
@@ -0,0 +1,95 @@
1
+ import { z } from "zod";
2
+
3
+ //#region lib/engine/connectors/slack-connector-schema.d.ts
4
+ /**
5
+ * A slack connector resolves its tokens one of two ways, set at sync time:
6
+ *
7
+ * - literal: `botToken` / `appToken` hold the real `xoxb-`/`xapp-` secret
8
+ * (set by a `fnl channels` command or a TTY prompt at launch).
9
+ * - by reference: `botTokenEnv` / `appTokenEnv` hold the *name* of an env var.
10
+ * The secret never lands in settings.json; the listener resolves it from
11
+ * `process.env` at start. This form is only set through the engine API
12
+ * (`new Funnel(...)`) — funnel.json and the `fnl` CLI produce literals.
13
+ *
14
+ * Both are optional at the schema level (a discriminated-union member can't
15
+ * carry a cross-field refine); the listener requires exactly one resolved
16
+ * token per slot and errors loudly otherwise.
17
+ */
18
+ declare const slackConnectorSchema: z.ZodObject<{
19
+ id: z.ZodString;
20
+ name: z.ZodString;
21
+ type: z.ZodLiteral<"slack">;
22
+ botToken: z.ZodOptional<z.ZodString>;
23
+ appToken: z.ZodOptional<z.ZodString>;
24
+ botTokenEnv: z.ZodOptional<z.ZodString>;
25
+ appTokenEnv: z.ZodOptional<z.ZodString>;
26
+ minify: z.ZodDefault<z.ZodBoolean>;
27
+ createdAt: z.ZodOptional<z.ZodString>;
28
+ updatedAt: z.ZodOptional<z.ZodString>;
29
+ }, z.core.$strip>;
30
+ type SlackConnectorConfig = z.infer<typeof slackConnectorSchema>;
31
+ //#endregion
32
+ //#region lib/engine/connectors/slack-event-types.d.ts
33
+ type SlackMessageEvent = {
34
+ kind: "message";
35
+ channel: string;
36
+ user: string;
37
+ rawText: string;
38
+ text: string;
39
+ threadTs: string;
40
+ ts: string;
41
+ isThreadRoot: boolean;
42
+ mentioned: boolean;
43
+ source: "app_mention" | "message";
44
+ };
45
+ type SlackReactionEvent = {
46
+ kind: "reaction_added" | "reaction_removed";
47
+ channel: string;
48
+ user: string;
49
+ emoji: string;
50
+ targetTs: string;
51
+ targetUser: string | null;
52
+ };
53
+ type SlackEvent = SlackMessageEvent | SlackReactionEvent;
54
+ //#endregion
55
+ //#region lib/engine/connectors/slack-event-processor.d.ts
56
+ type SlackRawEvent = Record<string, unknown>;
57
+ /**
58
+ * Why the processor dropped an event. Mirrored verbatim into the diagnostic
59
+ * log's processed `outcome` column so "Slack delivered it but no notification arrived" is
60
+ * traceable to the exact gate that dropped it. The listener may additionally
61
+ * record `skip:preprocess` for events a host preprocessor dropped before the
62
+ * processor ran — that gate is outside this type.
63
+ */
64
+ type SlackSkipReason = "skip:type" | "skip:subtype" | "skip:dedup" | "skip:self-user" | "skip:self-bot";
65
+ type SlackProcessedSkip = {
66
+ skip: true;
67
+ reason: SlackSkipReason;
68
+ };
69
+ type SlackProcessedEmit = {
70
+ skip: false;
71
+ event: SlackEvent;
72
+ content: string;
73
+ meta: Record<string, string>;
74
+ shouldReact: boolean;
75
+ channel: string;
76
+ timestamp: string;
77
+ };
78
+ type SlackProcessed = SlackProcessedSkip | SlackProcessedEmit;
79
+ type Props = {
80
+ ownBotUserId: string;
81
+ ownBotId: string;
82
+ minify?: boolean;
83
+ now?: () => number;
84
+ };
85
+ declare class FunnelSlackEventProcessor {
86
+ private readonly ownBotUserId;
87
+ private readonly ownBotId;
88
+ private readonly minify;
89
+ private readonly now;
90
+ private readonly dedup;
91
+ constructor(props: Props);
92
+ process(event: SlackRawEvent): SlackProcessed;
93
+ }
94
+ //#endregion
95
+ export { SlackRawEvent as a, SlackMessageEvent as c, slackConnectorSchema as d, SlackProcessedSkip as i, SlackReactionEvent as l, SlackProcessed as n, SlackSkipReason as o, SlackProcessedEmit as r, SlackEvent as s, FunnelSlackEventProcessor as t, SlackConnectorConfig as u };
@@ -0,0 +1,176 @@
1
+ //#region lib/engine/connectors/minify-slack-event.ts
2
+ const TOP_LEVEL_KEYS = [
3
+ "type",
4
+ "subtype",
5
+ "user",
6
+ "bot_id",
7
+ "text",
8
+ "ts",
9
+ "thread_ts",
10
+ "channel",
11
+ "channel_type",
12
+ "files",
13
+ "attachments"
14
+ ];
15
+ const FILE_KEYS = [
16
+ "id",
17
+ "name",
18
+ "mimetype",
19
+ "filetype",
20
+ "size",
21
+ "url_private",
22
+ "permalink"
23
+ ];
24
+ const ATTACHMENT_KEYS = [
25
+ "title",
26
+ "text",
27
+ "fallback"
28
+ ];
29
+ const isRecord = (value) => {
30
+ return typeof value === "object" && value !== null && !Array.isArray(value);
31
+ };
32
+ const pickDefined = (source, keys) => {
33
+ const picked = {};
34
+ for (const key of keys) if (source[key] !== void 0) picked[key] = source[key];
35
+ return picked;
36
+ };
37
+ const hasThumbOrPreviewKey = (file) => {
38
+ return Object.keys(file).some((key) => key.startsWith("thumb") || key.startsWith("preview"));
39
+ };
40
+ const minifyFile = (file) => {
41
+ if (!isRecord(file)) return file;
42
+ const minified = pickDefined(file, FILE_KEYS);
43
+ if (hasThumbOrPreviewKey(file)) minified._funnel_omitted = ["thumb_*"];
44
+ return minified;
45
+ };
46
+ const flattenRichText = (node) => {
47
+ if (!isRecord(node)) return "";
48
+ const text = node.text;
49
+ if (typeof text === "string") return text;
50
+ const elements = node.elements;
51
+ if (!Array.isArray(elements)) return "";
52
+ return elements.map(flattenRichText).join("");
53
+ };
54
+ const flattenTableRow = (row) => {
55
+ if (!Array.isArray(row)) return "";
56
+ return row.map(flattenRichText).join(" ");
57
+ };
58
+ const flattenBlock = (block) => {
59
+ if (!isRecord(block)) return "";
60
+ if (block.type === "table" && Array.isArray(block.rows)) return block.rows.map(flattenTableRow).join("\n");
61
+ return flattenRichText(block);
62
+ };
63
+ const flattenBlocks = (blocks) => {
64
+ return blocks.map(flattenBlock).filter((line) => line.length > 0).join("\n");
65
+ };
66
+ const minifyAttachment = (attachment) => {
67
+ if (!isRecord(attachment)) return attachment;
68
+ const minified = pickDefined(attachment, ATTACHMENT_KEYS);
69
+ const blocks = attachment.blocks;
70
+ if (Array.isArray(blocks)) {
71
+ const flattened = flattenBlocks(blocks);
72
+ const existingText = typeof minified.text === "string" ? minified.text : "";
73
+ minified.text = existingText ? `${existingText}\n${flattened}` : flattened;
74
+ minified._funnel_omitted = ["blocks"];
75
+ }
76
+ return minified;
77
+ };
78
+ const minifySlackEvent = (event) => {
79
+ const minified = pickDefined(event, TOP_LEVEL_KEYS);
80
+ if (Array.isArray(minified.files)) minified.files = minified.files.map(minifyFile);
81
+ if (Array.isArray(minified.attachments)) minified.attachments = minified.attachments.map(minifyAttachment);
82
+ return minified;
83
+ };
84
+ //#endregion
85
+ //#region lib/engine/connectors/slack-event-processor.ts
86
+ const ALLOWED_EVENTS = new Set(["message", "app_mention"]);
87
+ const ALLOWED_SUBTYPES = new Set([
88
+ void 0,
89
+ "thread_broadcast",
90
+ "bot_message",
91
+ "file_share"
92
+ ]);
93
+ const DEDUP_WINDOW = 1e4;
94
+ const getString = (event, key) => {
95
+ const value = event[key];
96
+ return typeof value === "string" ? value : void 0;
97
+ };
98
+ var FunnelSlackEventProcessor = class {
99
+ ownBotUserId;
100
+ ownBotId;
101
+ minify;
102
+ now;
103
+ dedup = /* @__PURE__ */ new Map();
104
+ constructor(props) {
105
+ this.ownBotUserId = props.ownBotUserId;
106
+ this.ownBotId = props.ownBotId;
107
+ this.minify = props.minify ?? true;
108
+ this.now = props.now ?? (() => Date.now());
109
+ }
110
+ process(event) {
111
+ const eventType = getString(event, "type");
112
+ if (!eventType || !ALLOWED_EVENTS.has(eventType)) return {
113
+ skip: true,
114
+ reason: "skip:type"
115
+ };
116
+ const subtype = getString(event, "subtype");
117
+ if (!ALLOWED_SUBTYPES.has(subtype)) return {
118
+ skip: true,
119
+ reason: "skip:subtype"
120
+ };
121
+ const channelId = getString(event, "channel") ?? "";
122
+ const dedupKey = `${channelId}:${getString(event, "event_ts") ?? getString(event, "ts") ?? ""}`;
123
+ const now = this.now();
124
+ if (this.dedup.has(dedupKey)) return {
125
+ skip: true,
126
+ reason: "skip:dedup"
127
+ };
128
+ this.dedup.set(dedupKey, now);
129
+ for (const key of this.dedup.keys()) if ((this.dedup.get(key) ?? 0) < now - DEDUP_WINDOW) this.dedup.delete(key);
130
+ const userId = getString(event, "user");
131
+ const botId = getString(event, "bot_id");
132
+ if (userId === this.ownBotUserId) return {
133
+ skip: true,
134
+ reason: "skip:self-user"
135
+ };
136
+ if (botId === this.ownBotId) return {
137
+ skip: true,
138
+ reason: "skip:self-bot"
139
+ };
140
+ const rawText = getString(event, "text") ?? "";
141
+ const mentioned = rawText.includes(`<@${this.ownBotUserId}>`);
142
+ const threadTs = getString(event, "thread_ts") ?? getString(event, "ts") ?? "";
143
+ const ts = getString(event, "ts") ?? "";
144
+ const source = eventType === "app_mention" ? "app_mention" : "message";
145
+ const emitted = this.minify ? minifySlackEvent(event) : event;
146
+ return {
147
+ skip: false,
148
+ event: {
149
+ kind: "message",
150
+ channel: channelId,
151
+ user: userId ?? "",
152
+ rawText,
153
+ text: stripMention(rawText, this.ownBotUserId),
154
+ threadTs,
155
+ ts,
156
+ isThreadRoot: threadTs === ts,
157
+ mentioned,
158
+ source
159
+ },
160
+ content: JSON.stringify(emitted),
161
+ meta: {
162
+ event_type: "slack",
163
+ channel_id: channelId,
164
+ user_id: userId ?? "",
165
+ mentioned: String(mentioned),
166
+ thread_ts: threadTs
167
+ },
168
+ shouldReact: mentioned,
169
+ channel: channelId,
170
+ timestamp: ts
171
+ };
172
+ }
173
+ };
174
+ const stripMention = (text, botUserId) => text.replace(new RegExp(`<@${botUserId}>`, "g"), "").trim();
175
+ //#endregion
176
+ export { FunnelSlackEventProcessor as t };