@interactive-inc/claude-funnel 0.60.0 → 0.63.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-B8RQPrVq.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-ClEEbuW3.d.ts} +50 -11
  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 +71 -131
  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-DOlCr4GF.d.ts → file-process-guard-DGHxALfI.d.ts} +8 -6
  24. package/dist/{file-system-o51IsM0W.d.ts → file-system-VhwwXZbm.d.ts} +8 -0
  25. package/dist/flume-source-listener-Dim5szHG.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-DxRikYmu.d.ts} +37 -19
  40. package/dist/index.d.ts +182 -22
  41. package/dist/index.js +365 -174
  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-DP_YV9xX.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-BU86fIge.js +359 -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-qW34NlYz.js → yaml-render--J1_3BSA.js} +28 -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
@@ -1,61 +0,0 @@
1
- import { i as FunnelTokenPrompter, o as LOCAL_CONFIG_FILENAME } from "./local-config-json-schema-JyLqOQNX.js";
2
- import { join } from "node:path";
3
- //#region lib/services/local-config/local-config-writer.ts
4
- const isRecord = (value) => {
5
- return typeof value === "object" && value !== null && !Array.isArray(value);
6
- };
7
- const withIdFirst = (config, id) => {
8
- const ordered = {};
9
- if (config.$schema !== void 0) ordered.$schema = config.$schema;
10
- ordered.id = id;
11
- for (const key of Object.keys(config)) {
12
- if (key === "$schema" || key === "id") continue;
13
- ordered[key] = config[key];
14
- }
15
- return ordered;
16
- };
17
- /**
18
- * The one path that mutates the repo-committed funnel.json, and it only ever
19
- * inserts `id`. On first launch a repo has no `id`; funnel generates one and
20
- * writes it back here so future launches resolve the same `~/.funnel/projects/<id>/`.
21
- * Idempotent — a no-op once `id` is present. Kept separate from the read-only
22
- * FunnelLocalConfig so reads stay side-effect free.
23
- */
24
- var FunnelLocalConfigWriter = class {
25
- fs;
26
- constructor(deps) {
27
- this.fs = deps.fs;
28
- Object.freeze(this);
29
- }
30
- ensureId(cwd, id) {
31
- const path = join(cwd, LOCAL_CONFIG_FILENAME);
32
- if (!this.fs.existsSync(path)) return;
33
- const parsed = JSON.parse(this.fs.readFileSync(path));
34
- if (!isRecord(parsed)) return;
35
- if (typeof parsed.id === "string" && parsed.id !== "") return;
36
- const ordered = withIdFirst(parsed, id);
37
- this.fs.writeFileSync(path, `${JSON.stringify(ordered, null, 2)}\n`);
38
- }
39
- };
40
- //#endregion
41
- //#region lib/engine/token-prompter/memory-token-prompter.ts
42
- /**
43
- * Pre-seeded answers keyed by prompt label. Tests configure the map up front;
44
- * unmapped labels throw so the test surfaces unexpected prompts loudly.
45
- */
46
- var MemoryFunnelTokenPrompter = class extends FunnelTokenPrompter {
47
- answers;
48
- asked = [];
49
- constructor(props = {}) {
50
- super();
51
- this.answers = new Map(Object.entries(props.answers ?? {}));
52
- }
53
- async promptSecret(label) {
54
- this.asked.push(label);
55
- const answer = this.answers.get(label);
56
- if (answer === void 0) throw new Error(`no answer seeded for prompt "${label}"`);
57
- return answer;
58
- }
59
- };
60
- //#endregion
61
- export { FunnelLocalConfigWriter as n, MemoryFunnelTokenPrompter as t };
@@ -1,48 +0,0 @@
1
- import { t as FunnelFileSystem } from "./file-system-Wvzc2ePY.js";
2
- import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
- //#region lib/engine/fs/node-file-system.ts
4
- const SECRET_MODE = 384;
5
- var NodeFunnelFileSystem = class extends FunnelFileSystem {
6
- constructor() {
7
- super();
8
- Object.freeze(this);
9
- }
10
- existsSync(path) {
11
- return existsSync(path);
12
- }
13
- readFileSync(path) {
14
- return readFileSync(path, "utf-8");
15
- }
16
- writeFileSync(path, data) {
17
- writeFileSync(path, data);
18
- }
19
- writeSecretFileSync(path, data) {
20
- writeFileSync(path, data, { mode: SECRET_MODE });
21
- try {
22
- chmodSync(path, SECRET_MODE);
23
- } catch {}
24
- }
25
- appendFileSync(path, data) {
26
- appendFileSync(path, data);
27
- }
28
- unlink(path) {
29
- try {
30
- unlinkSync(path);
31
- } catch {}
32
- }
33
- mkdirSync(path, options) {
34
- mkdirSync(path, { recursive: options?.recursive ?? false });
35
- }
36
- readdirSync(path) {
37
- return readdirSync(path);
38
- }
39
- statSync(path) {
40
- const stat = statSync(path);
41
- return {
42
- mtimeMs: stat.mtimeMs,
43
- mode: stat.mode & 511
44
- };
45
- }
46
- };
47
- //#endregion
48
- export { NodeFunnelFileSystem as t };
@@ -1,18 +0,0 @@
1
- import { a as Settings } from "./settings-schema-D1xcOqRu.js";
2
-
3
- //#region lib/engine/id/id-generator.d.ts
4
- /**
5
- * ID generator boundary. Default NodeFunnelIdGenerator wraps `crypto.randomUUID()`;
6
- * MemoryFunnelIdGenerator emits `<prefix>-1, <prefix>-2, ...` for deterministic tests.
7
- */
8
- declare abstract class FunnelIdGenerator {
9
- abstract generate(): string;
10
- }
11
- //#endregion
12
- //#region lib/engine/settings/settings-reader.d.ts
13
- declare abstract class FunnelSettingsReader {
14
- abstract read(): Settings;
15
- abstract write(settings: Settings): void;
16
- }
17
- //#endregion
18
- export { FunnelIdGenerator as n, FunnelSettingsReader as t };
@@ -1,484 +0,0 @@
1
- import { t as slackConnectorSchema } from "./slack-connector-schema-C1zEf4TG.js";
2
- import { t as FunnelConnectorAdapter } from "./connector-adapter-DU9Rvyec.js";
3
- import { t as FunnelConnectorListener } from "./connector-listener-DR3aKOuK.js";
4
- import { t as errorMessageOf } from "./error-message-of-Byi4y0Uf.js";
5
- import { n as resolveConnectorToken, t as slotFields } from "./slot-fields-CMoRpwuy.js";
6
- import { z } from "zod";
7
- import { WebClient } from "@slack/web-api";
8
- import { App, LogLevel, SocketModeReceiver } from "@slack/bolt";
9
- //#region lib/engine/connectors/slack-adapter.ts
10
- const toRecord = (value) => {
11
- const result = {};
12
- for (const [key, val] of Object.entries(value)) result[key] = val;
13
- return result;
14
- };
15
- /**
16
- * Recognises errors that @slack/web-api throws for Slack-side API failures
17
- * (e.g. `cant_delete_message`, `channel_not_found`, rate limits). Every such
18
- * error carries `code: "slack_webapi_*"` and a `data` field holding the raw
19
- * Slack response with `ok: false`. We unwrap to that response so the caller
20
- * receives a structured failure instead of having the gateway translate it
21
- * into an opaque HTTP 500.
22
- */
23
- const slackErrorResponse = (error) => {
24
- if (!error || typeof error !== "object") return null;
25
- if (!("code" in error)) return null;
26
- const code = error.code;
27
- if (typeof code !== "string" || !code.startsWith("slack_webapi_")) return null;
28
- if (!("data" in error)) return null;
29
- const data = error.data;
30
- if (!data || typeof data !== "object") return null;
31
- return data;
32
- };
33
- var FunnelSlackAdapter = class extends FunnelConnectorAdapter {
34
- client;
35
- constructor(deps) {
36
- super();
37
- const botToken = resolveConnectorToken({
38
- literal: deps.config.botToken,
39
- envVar: deps.config.botTokenEnv,
40
- env: deps.env ?? process.env,
41
- label: `${deps.config.name}.botToken`
42
- });
43
- this.client = deps.client ?? new WebClient(botToken);
44
- Object.freeze(this);
45
- }
46
- async call(input) {
47
- const body = input.body !== null && typeof input.body === "object" ? toRecord(input.body) : {};
48
- try {
49
- return await this.client.apiCall(input.path, body);
50
- } catch (error) {
51
- const slackResponse = slackErrorResponse(error);
52
- if (slackResponse) return slackResponse;
53
- throw error;
54
- }
55
- }
56
- async postMessage(props) {
57
- return this.call({
58
- method: "post",
59
- path: "chat.postMessage",
60
- body: {
61
- channel: props.channel,
62
- text: props.text,
63
- ...props.threadTs ? { thread_ts: props.threadTs } : {}
64
- }
65
- });
66
- }
67
- async addReaction(props) {
68
- return this.call({
69
- method: "post",
70
- path: "reactions.add",
71
- body: {
72
- channel: props.channel,
73
- timestamp: props.timestamp,
74
- name: props.name
75
- }
76
- });
77
- }
78
- async removeReaction(props) {
79
- return this.call({
80
- method: "post",
81
- path: "reactions.remove",
82
- body: {
83
- channel: props.channel,
84
- timestamp: props.timestamp,
85
- name: props.name
86
- }
87
- });
88
- }
89
- };
90
- //#endregion
91
- //#region lib/engine/connectors/minify-slack-event.ts
92
- const TOP_LEVEL_KEYS = [
93
- "type",
94
- "subtype",
95
- "user",
96
- "bot_id",
97
- "text",
98
- "ts",
99
- "thread_ts",
100
- "channel",
101
- "channel_type",
102
- "files",
103
- "attachments"
104
- ];
105
- const FILE_KEYS = [
106
- "id",
107
- "name",
108
- "mimetype",
109
- "filetype",
110
- "size",
111
- "url_private",
112
- "permalink"
113
- ];
114
- const ATTACHMENT_KEYS = [
115
- "title",
116
- "text",
117
- "fallback"
118
- ];
119
- const isRecord = (value) => {
120
- return typeof value === "object" && value !== null && !Array.isArray(value);
121
- };
122
- const pickDefined = (source, keys) => {
123
- const picked = {};
124
- for (const key of keys) if (source[key] !== void 0) picked[key] = source[key];
125
- return picked;
126
- };
127
- const hasThumbOrPreviewKey = (file) => {
128
- return Object.keys(file).some((key) => key.startsWith("thumb") || key.startsWith("preview"));
129
- };
130
- const minifyFile = (file) => {
131
- if (!isRecord(file)) return file;
132
- const minified = pickDefined(file, FILE_KEYS);
133
- if (hasThumbOrPreviewKey(file)) minified._funnel_omitted = ["thumb_*"];
134
- return minified;
135
- };
136
- const flattenRichText = (node) => {
137
- if (!isRecord(node)) return "";
138
- const text = node.text;
139
- if (typeof text === "string") return text;
140
- const elements = node.elements;
141
- if (!Array.isArray(elements)) return "";
142
- return elements.map(flattenRichText).join("");
143
- };
144
- const flattenTableRow = (row) => {
145
- if (!Array.isArray(row)) return "";
146
- return row.map(flattenRichText).join(" ");
147
- };
148
- const flattenBlock = (block) => {
149
- if (!isRecord(block)) return "";
150
- if (block.type === "table" && Array.isArray(block.rows)) return block.rows.map(flattenTableRow).join("\n");
151
- return flattenRichText(block);
152
- };
153
- const flattenBlocks = (blocks) => {
154
- return blocks.map(flattenBlock).filter((line) => line.length > 0).join("\n");
155
- };
156
- const minifyAttachment = (attachment) => {
157
- if (!isRecord(attachment)) return attachment;
158
- const minified = pickDefined(attachment, ATTACHMENT_KEYS);
159
- const blocks = attachment.blocks;
160
- if (Array.isArray(blocks)) {
161
- const flattened = flattenBlocks(blocks);
162
- const existingText = typeof minified.text === "string" ? minified.text : "";
163
- minified.text = existingText ? `${existingText}\n${flattened}` : flattened;
164
- minified._funnel_omitted = ["blocks"];
165
- }
166
- return minified;
167
- };
168
- const minifySlackEvent = (event) => {
169
- const minified = pickDefined(event, TOP_LEVEL_KEYS);
170
- if (Array.isArray(minified.files)) minified.files = minified.files.map(minifyFile);
171
- if (Array.isArray(minified.attachments)) minified.attachments = minified.attachments.map(minifyAttachment);
172
- return minified;
173
- };
174
- //#endregion
175
- //#region lib/engine/connectors/slack-event-processor.ts
176
- const ALLOWED_EVENTS = new Set(["message", "app_mention"]);
177
- const ALLOWED_SUBTYPES = new Set([
178
- void 0,
179
- "thread_broadcast",
180
- "bot_message",
181
- "file_share"
182
- ]);
183
- const DEDUP_WINDOW = 1e4;
184
- const getString = (event, key) => {
185
- const value = event[key];
186
- return typeof value === "string" ? value : void 0;
187
- };
188
- var FunnelSlackEventProcessor = class {
189
- ownBotUserId;
190
- ownBotId;
191
- minify;
192
- now;
193
- dedup = /* @__PURE__ */ new Map();
194
- constructor(props) {
195
- this.ownBotUserId = props.ownBotUserId;
196
- this.ownBotId = props.ownBotId;
197
- this.minify = props.minify ?? true;
198
- this.now = props.now ?? (() => Date.now());
199
- }
200
- process(event) {
201
- const eventType = getString(event, "type");
202
- if (!eventType || !ALLOWED_EVENTS.has(eventType)) return {
203
- skip: true,
204
- reason: "skip:type"
205
- };
206
- const subtype = getString(event, "subtype");
207
- if (!ALLOWED_SUBTYPES.has(subtype)) return {
208
- skip: true,
209
- reason: "skip:subtype"
210
- };
211
- const channelId = getString(event, "channel") ?? "";
212
- const dedupKey = `${channelId}:${getString(event, "event_ts") ?? getString(event, "ts") ?? ""}`;
213
- const now = this.now();
214
- if (this.dedup.has(dedupKey)) return {
215
- skip: true,
216
- reason: "skip:dedup"
217
- };
218
- this.dedup.set(dedupKey, now);
219
- for (const key of this.dedup.keys()) if ((this.dedup.get(key) ?? 0) < now - DEDUP_WINDOW) this.dedup.delete(key);
220
- const userId = getString(event, "user");
221
- const botId = getString(event, "bot_id");
222
- if (userId === this.ownBotUserId) return {
223
- skip: true,
224
- reason: "skip:self-user"
225
- };
226
- if (botId === this.ownBotId) return {
227
- skip: true,
228
- reason: "skip:self-bot"
229
- };
230
- const rawText = getString(event, "text") ?? "";
231
- const mentioned = rawText.includes(`<@${this.ownBotUserId}>`);
232
- const threadTs = getString(event, "thread_ts") ?? getString(event, "ts") ?? "";
233
- const ts = getString(event, "ts") ?? "";
234
- const source = eventType === "app_mention" ? "app_mention" : "message";
235
- const emitted = this.minify ? minifySlackEvent(event) : event;
236
- return {
237
- skip: false,
238
- event: {
239
- kind: "message",
240
- channel: channelId,
241
- user: userId ?? "",
242
- rawText,
243
- text: stripMention(rawText, this.ownBotUserId),
244
- threadTs,
245
- ts,
246
- isThreadRoot: threadTs === ts,
247
- mentioned,
248
- source
249
- },
250
- content: JSON.stringify(emitted),
251
- meta: {
252
- event_type: "slack",
253
- channel_id: channelId,
254
- user_id: userId ?? "",
255
- mentioned: String(mentioned),
256
- thread_ts: threadTs
257
- },
258
- shouldReact: mentioned,
259
- channel: channelId,
260
- timestamp: ts
261
- };
262
- }
263
- };
264
- const stripMention = (text, botUserId) => text.replace(new RegExp(`<@${botUserId}>`, "g"), "").trim();
265
- //#endregion
266
- //#region lib/engine/connectors/slack-listener.ts
267
- const middlewareArgsSchema = z.object({ event: z.record(z.string(), z.unknown()).optional() });
268
- var FunnelSlackListener = class extends FunnelConnectorListener {
269
- config;
270
- channelId;
271
- env;
272
- logger;
273
- diagnosticLog;
274
- onAppCreated;
275
- preprocessEvent;
276
- app = null;
277
- connected = false;
278
- constructor(deps) {
279
- super();
280
- this.config = deps.config;
281
- this.channelId = deps.channelId ?? null;
282
- this.env = deps.env ?? process.env;
283
- this.logger = deps.logger;
284
- this.diagnosticLog = deps.diagnosticLog;
285
- this.onAppCreated = deps.onAppCreated ?? null;
286
- this.preprocessEvent = deps.preprocessEvent ?? null;
287
- }
288
- async start(notify) {
289
- this.recordConnection("started", "");
290
- const botToken = resolveConnectorToken({
291
- literal: this.config.botToken,
292
- envVar: this.config.botTokenEnv,
293
- env: this.env,
294
- label: `${this.config.name}.botToken`
295
- });
296
- const receiver = new SocketModeReceiver({
297
- appToken: resolveConnectorToken({
298
- literal: this.config.appToken,
299
- envVar: this.config.appTokenEnv,
300
- env: this.env,
301
- label: `${this.config.name}.appToken`
302
- }),
303
- logLevel: LogLevel.ERROR,
304
- autoReconnectEnabled: false
305
- });
306
- receiver.client.on("connected", () => {
307
- this.connected = true;
308
- });
309
- receiver.client.on("disconnected", () => {
310
- this.connected = false;
311
- });
312
- const app = new App({
313
- token: botToken,
314
- receiver,
315
- logLevel: LogLevel.ERROR
316
- });
317
- let authResult;
318
- try {
319
- authResult = await app.client.auth.test({ token: botToken });
320
- } catch (error) {
321
- this.recordConnection("auth-failed", errorMessageOf(error));
322
- throw error;
323
- }
324
- const processor = new FunnelSlackEventProcessor({
325
- ownBotUserId: authResult.user_id ?? "",
326
- ownBotId: authResult.bot_id ?? "",
327
- minify: this.config.minify
328
- });
329
- const preprocess = this.preprocessEvent;
330
- app.use(async (args) => {
331
- const parsed = middlewareArgsSchema.safeParse(args);
332
- if (!parsed.success || !parsed.data.event) {
333
- await args.next();
334
- return;
335
- }
336
- const rawEvent = parsed.data.event;
337
- const eventId = crypto.randomUUID();
338
- this.recordRaw(eventId, rawEvent);
339
- const event = preprocess ? preprocess(rawEvent) : rawEvent;
340
- if (event === null) {
341
- this.recordProcessed(eventId, rawEvent, "skip:preprocess", "");
342
- return;
343
- }
344
- const result = processor.process(event);
345
- if (result.skip) {
346
- this.recordProcessed(eventId, event, result.reason, "");
347
- return;
348
- }
349
- try {
350
- await notify(result.content, result.meta);
351
- } catch (error) {
352
- this.recordProcessed(eventId, event, "emitted:delivery-failed", result.content);
353
- throw error;
354
- }
355
- this.recordProcessed(eventId, event, "emitted", result.content);
356
- if (result.shouldReact) try {
357
- await app.client.reactions.add({
358
- token: botToken,
359
- channel: result.channel,
360
- timestamp: result.timestamp,
361
- name: "eyes"
362
- });
363
- } catch {}
364
- });
365
- app.error(async (error) => {
366
- const message = errorMessageOf(error);
367
- this.recordConnection("error", message);
368
- this.logger?.error("Slack error", { error: message });
369
- });
370
- if (this.onAppCreated) await this.onAppCreated(app);
371
- try {
372
- await app.start();
373
- } catch (error) {
374
- this.recordConnection("error", errorMessageOf(error));
375
- throw error;
376
- }
377
- this.app = app;
378
- this.connected = true;
379
- this.recordConnection("connected", "");
380
- }
381
- async stop() {
382
- if (!this.app) return;
383
- try {
384
- await this.app.stop();
385
- this.recordConnection("disconnected", "");
386
- } catch (error) {
387
- this.recordConnection("error", errorMessageOf(error));
388
- this.logger?.error("Slack stop error", { error: errorMessageOf(error) });
389
- } finally {
390
- this.app = null;
391
- this.connected = false;
392
- this.recordConnection("stopped", "");
393
- }
394
- }
395
- isAlive() {
396
- return this.app !== null && this.connected;
397
- }
398
- recordRaw(eventId, event) {
399
- this.diagnosticLog?.recordRaw({
400
- eventId,
401
- type: "slack",
402
- connectorId: this.config.id,
403
- channelId: this.channelId,
404
- payload: JSON.stringify(event)
405
- });
406
- }
407
- recordProcessed(eventId, event, outcome, content) {
408
- this.diagnosticLog?.recordProcessed({
409
- eventId,
410
- type: "slack",
411
- connectorId: this.config.id,
412
- channelId: this.channelId,
413
- outcome,
414
- payload: content || JSON.stringify(event)
415
- });
416
- }
417
- recordConnection(status, detail) {
418
- this.diagnosticLog?.recordConnection({
419
- type: "slack",
420
- connectorId: this.config.id,
421
- channelId: this.channelId,
422
- status,
423
- detail
424
- });
425
- }
426
- };
427
- //#endregion
428
- //#region lib/engine/connectors/slack-connector.ts
429
- /**
430
- * Slack connector descriptor. Pass `slackConnector()` to
431
- * `new Funnel({ connectors: [...] })` to enable the type. Host launch hooks are
432
- * closed over here, so they need no Funnel-level option plumbing.
433
- */
434
- const slackConnector = (options = {}) => ({
435
- type: "slack",
436
- toolExposed: true,
437
- createListener(config, deps) {
438
- return new FunnelSlackListener({
439
- config: slackConnectorSchema.parse(config),
440
- channelId: deps.channelId,
441
- logger: deps.logger,
442
- diagnosticLog: deps.diagnosticLog,
443
- onAppCreated: options.onAppCreated,
444
- preprocessEvent: options.preprocessEvent
445
- });
446
- },
447
- createAdapter(config) {
448
- return new FunnelSlackAdapter({ config: slackConnectorSchema.parse(config) });
449
- },
450
- secretTokens(config) {
451
- const parsed = slackConnectorSchema.parse(config);
452
- return [parsed.botToken, parsed.appToken].filter((token) => token !== void 0);
453
- },
454
- buildConfig(input, context) {
455
- return slackConnectorSchema.parse({
456
- id: context.id,
457
- type: "slack",
458
- name: input.name,
459
- ...typeof input.botToken === "string" ? { botToken: input.botToken } : {},
460
- ...typeof input.appToken === "string" ? { appToken: input.appToken } : {},
461
- ...typeof input.botTokenEnv === "string" ? { botTokenEnv: input.botTokenEnv } : {},
462
- ...typeof input.appTokenEnv === "string" ? { appTokenEnv: input.appTokenEnv } : {},
463
- minify: typeof input.minify === "boolean" ? input.minify : true,
464
- createdAt: context.now,
465
- updatedAt: context.now
466
- });
467
- },
468
- applyUpdate(config, fields, context) {
469
- const current = slackConnectorSchema.parse(config);
470
- return slackConnectorSchema.parse({
471
- id: current.id,
472
- name: current.name,
473
- type: "slack",
474
- minify: current.minify,
475
- createdAt: current.createdAt,
476
- updatedAt: context.now,
477
- ...slotFields("botToken", "botTokenEnv", fields, current),
478
- ...slotFields("appToken", "appTokenEnv", fields, current)
479
- });
480
- },
481
- operations: {}
482
- });
483
- //#endregion
484
- export { FunnelSlackAdapter as i, FunnelSlackListener as n, FunnelSlackEventProcessor as r, slackConnector as t };
@@ -1,45 +0,0 @@
1
- //#region lib/engine/connectors/resolve-connector-token.ts
2
- /**
3
- * Resolves a connector token from either a literal value or the name of an env
4
- * var. A connector config carries one or the other per slot (see
5
- * slack-connector-schema): literals are inlined into settings.json, references
6
- * keep the secret in `process.env` and out of settings.json.
7
- *
8
- * Errors loudly when neither yields a value — a misconfigured connector should
9
- * fail at listener start, not connect with an empty token and silently never
10
- * receive events.
11
- */
12
- const resolveConnectorToken = (props) => {
13
- if (props.literal !== void 0 && props.literal !== "") return props.literal;
14
- if (props.envVar !== void 0 && props.envVar !== "") {
15
- const fromEnv = props.env[props.envVar];
16
- if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
17
- throw new Error(`${props.label} references env var "${props.envVar}" but it is not set in the environment`);
18
- }
19
- throw new Error(`${props.label} has neither a literal token nor an env var reference`);
20
- };
21
- //#endregion
22
- //#region lib/engine/connectors/slot-fields.ts
23
- /**
24
- * Resolves one token slot (e.g. botToken/botTokenEnv) for a connector update.
25
- * The literal and the env-ref form are mutually exclusive: if `fields` supplies
26
- * either, that form wins and the other key is omitted entirely; if it supplies
27
- * neither, the connector's current slot is carried over unchanged. Returns a
28
- * partial object spread into the rebuilt connector, so an omitted key is truly
29
- * absent rather than set to undefined — switching a slot from literal to ref
30
- * drops the stale literal instead of leaving both behind.
31
- */
32
- const slotFields = (literalKey, envKey, fields, current) => {
33
- const literal = fields[literalKey];
34
- if (typeof literal === "string") return { [literalKey]: literal };
35
- const envVar = fields[envKey];
36
- if (typeof envVar === "string") return { [envKey]: envVar };
37
- const result = {};
38
- const currentLiteral = current[literalKey];
39
- const currentEnv = current[envKey];
40
- if (typeof currentLiteral === "string") result[literalKey] = currentLiteral;
41
- if (typeof currentEnv === "string") result[envKey] = currentEnv;
42
- return result;
43
- };
44
- //#endregion
45
- export { resolveConnectorToken as n, slotFields as t };