@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.
- package/README.md +2 -2
- package/dist/bin.js +428 -761
- package/dist/{channels-2g_BU1N0.d.ts → channels-CRGb6B5_.d.ts} +17 -16
- package/dist/claude.d.ts +5 -7
- package/dist/claude.js +143 -36
- package/dist/{connector-descriptor-6SXJoszo.d.ts → connector-descriptor-BFIhyTfa.d.ts} +49 -10
- package/dist/connector-diagnostics-recorder-COtNEmUp.js +42 -0
- package/dist/connectors/discord.d.ts +31 -37
- package/dist/connectors/discord.js +3 -3
- package/dist/connectors/gh.d.ts +37 -33
- package/dist/connectors/gh.js +3 -3
- package/dist/connectors/schedule.d.ts +9 -57
- package/dist/connectors/schedule.js +3 -3
- package/dist/connectors/slack.d.ts +106 -132
- package/dist/connectors/slack.js +4 -3
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.js +1 -1
- package/dist/discord-connector-DIFkYBbi.js +250 -0
- package/dist/discord-connector-schema-D-bOVAKt.d.ts +22 -0
- package/dist/docs.js +1 -1
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +1 -1
- package/dist/{file-process-guard-C_PLxfUX.d.ts → file-process-guard-tVcgckH6.d.ts} +6 -6
- package/dist/{file-system-o51IsM0W.d.ts → file-system-VhwwXZbm.d.ts} +8 -0
- package/dist/flume-source-listener-BNyAII7N.d.ts +133 -0
- package/dist/{funnel-diagnostics-CSiJmPlZ.js → funnel-diagnostics-Cvk6Sk4x.js} +193 -43
- package/dist/{funnel-diagnostics-DpXOsCty.d.ts → funnel-diagnostics-b9ar0Ing.d.ts} +67 -5
- package/dist/{funnel-docs-BxXZ9Ksx.js → funnel-docs-C-ge0MuB.js} +42 -6
- package/dist/{funnel-doctor-CZf_0Luq.d.ts → funnel-doctor-CnRQi4kM.d.ts} +2 -2
- package/dist/{funnel-doctor-DiJCjHsg.js → funnel-doctor-XrI2GBH8.js} +1 -1
- package/dist/funnel-error-0t1MK1R6.js +75 -0
- package/dist/{funnel-recovery-DnLrdWO9.d.ts → funnel-recovery-CMhY8Jfk.d.ts} +1 -1
- package/dist/gateway/daemon.js +167 -527
- package/dist/gateway.d.ts +3 -3
- package/dist/gateway.js +3 -3
- package/dist/gh-connector-BUGCOEWS.js +187 -0
- package/dist/{gh-connector-schema-Rzwc1c1N.js → gh-connector-schema-CAqIhzGr.js} +7 -0
- package/dist/gh-connector-schema-DWQaB6gX.d.ts +16 -0
- package/dist/{index-CgY8NdMz.d.ts → index-Ds6sHhA-.d.ts} +37 -19
- package/dist/index.d.ts +182 -22
- package/dist/index.js +363 -173
- package/dist/{local-config-json-schema-JyLqOQNX.js → local-config-json-schema-DexV8vX3.js} +24 -4
- package/dist/local-config.d.ts +39 -2
- package/dist/local-config.js +53 -2
- package/dist/logger.js +1 -1
- package/dist/loopback-fetch-CVNuN3YZ.js +40 -0
- package/dist/{local-config-sync-Dh1Croqe.d.ts → memory-token-prompter-BoV8Hf-n.d.ts} +30 -3
- package/dist/node-file-system-BOXIHW_Q.js +174 -0
- package/dist/{profiles-DSzTeKQw.js → profiles-ZHLONml4.js} +49 -49
- package/dist/{profiles-Cy5wXQ0L.d.ts → profiles-cVZQkM69.d.ts} +3 -3
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.js +1 -1
- package/dist/recovery.d.ts +1 -1
- package/dist/recovery.js +1 -1
- package/dist/resolve-connector-token-DxDG9mhf.js +22 -0
- package/dist/{schedule-connector-L4uzg5M8.js → schedule-connector-9k3gOIgl.js} +54 -55
- package/dist/schedule-connector-schema-Z0RXLgPI.d.ts +49 -0
- package/dist/settings-reader-BNxjsxCB.d.ts +27 -0
- package/dist/{settings-store-CUKSeTXC.js → settings-store-C2QdOH-t.js} +23 -4
- package/dist/slack-connector-CxpWagbT.js +388 -0
- package/dist/slack-event-processor-BhCf5Wiy.d.ts +95 -0
- package/dist/slack-event-processor-xFDG3US0.js +176 -0
- package/dist/slot-fields-D-pvMgTK.js +249 -0
- package/dist/{memory-diagnostic-log-CI60kNfB.js → sqlite-diagnostic-log-DOTPW-tG.js} +373 -249
- package/dist/{yaml-render-93pX7EF7.js → yaml-render--J1_3BSA.js} +25 -21
- package/package.json +2 -4
- package/dist/discord-connector-BL36yvbL.js +0 -250
- package/dist/gateway-base-url-Dy4Ykuoh.js +0 -14
- package/dist/gh-connector-DpiixfQZ.js +0 -226
- package/dist/http-client-oICicjuO.d.ts +0 -18
- package/dist/memory-token-prompter-B4sjyaAq.d.ts +0 -57
- package/dist/memory-token-prompter-CZde7e6y.js +0 -61
- package/dist/node-file-system-Blr8pAir.js +0 -48
- package/dist/settings-reader-BIFB_j2f.d.ts +0 -18
- package/dist/slack-connector-DQIFPdBF.js +0 -484
- package/dist/slot-fields-CMoRpwuy.js +0 -45
- /package/dist/{connector-adapter-DU9Rvyec.js → connector-adapter-Dvs8N7ew.js} +0 -0
- /package/dist/{connector-listener-DR3aKOuK.js → connector-listener-mPGZYa8e.js} +0 -0
- /package/dist/{diagnostic-sql-reader-C9zR-Csp.js → diagnostic-sql-reader-oXZnWFf_.js} +0 -0
- /package/dist/{discord-connector-schema-B_N6IXLz.js → discord-connector-schema-B4YpWpR3.js} +0 -0
- /package/dist/{error-message-of-Byi4y0Uf.js → error-message-of-ColuYmAk.js} +0 -0
- /package/dist/{funnel-log-sqlite-sink-kqJbx2H7.js → funnel-log-sqlite-sink-DLYkY0pZ.js} +0 -0
- /package/dist/{funnel-recovery-BFdPjL6Z.js → funnel-recovery-DKnEutUS.js} +0 -0
- /package/dist/{node-http-client-lowp60Oa.js → node-http-client-u00atiKx.js} +0 -0
- /package/dist/{schedule-connector-schema-CfyuMCMh.js → schedule-connector-schema-DKEPZnVv.js} +0 -0
- /package/dist/{settings-reader-CtQ-Ix8_.js → settings-reader-9FcX3qS1.js} +0 -0
- /package/dist/{settings-schema-D1xcOqRu.d.ts → settings-schema-BL_c2Udm.d.ts} +0 -0
- /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 };
|