@interactive-inc/claude-funnel 0.60.1 → 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-C_PLxfUX.d.ts → file-process-guard-DGHxALfI.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-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 +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-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-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
@@ -1,7 +1,8 @@
1
- import { t as NodeFunnelFileSystem } from "./node-file-system-Blr8pAir.js";
2
- import { t as FunnelConnectorListener } from "./connector-listener-DR3aKOuK.js";
3
- import { n as scheduleConnectorSchema, r as scheduleEntrySchema } from "./schedule-connector-schema-CfyuMCMh.js";
4
- import { t as errorMessageOf } from "./error-message-of-Byi4y0Uf.js";
1
+ import { t as NodeFunnelFileSystem } from "./node-file-system-BOXIHW_Q.js";
2
+ import { t as errorMessageOf } from "./error-message-of-ColuYmAk.js";
3
+ import { t as FunnelConnectorListener } from "./connector-listener-mPGZYa8e.js";
4
+ import { n as scheduleConnectorSchema, r as scheduleEntrySchema } from "./schedule-connector-schema-DKEPZnVv.js";
5
+ import { t as FunnelConnectorDiagnosticsRecorder } from "./connector-diagnostics-recorder-COtNEmUp.js";
5
6
  import { dirname, join } from "node:path";
6
7
  import { z } from "zod";
7
8
  //#region lib/engine/connectors/match-cron.ts
@@ -87,7 +88,7 @@ const defaultFs = new NodeFunnelFileSystem();
87
88
  * connectorDir) so this store does not know about the funnel directory layout
88
89
  * (`channels/<id>/connectors/<id>/state.json` lives outside this class).
89
90
  */
90
- var ScheduleStateStore = class {
91
+ var FunnelScheduleStateStore = class {
91
92
  path;
92
93
  fs;
93
94
  constructor(deps) {
@@ -116,50 +117,75 @@ const MAX_CATCHUP_MINUTES = 1440;
116
117
  var FunnelScheduleListener = class extends FunnelConnectorListener {
117
118
  config;
118
119
  lastFiredStore;
119
- channelId;
120
120
  logger;
121
- diagnosticLog;
121
+ diagnostics;
122
122
  now;
123
123
  onFired;
124
124
  timer = null;
125
125
  stopped = false;
126
+ tickScheduled = false;
126
127
  constructor(deps) {
127
128
  super();
128
129
  this.config = deps.config;
129
130
  this.lastFiredStore = deps.lastFiredStore;
130
- this.channelId = deps.channelId ?? null;
131
131
  this.logger = deps.logger;
132
- this.diagnosticLog = deps.diagnosticLog;
132
+ this.diagnostics = new FunnelConnectorDiagnosticsRecorder({
133
+ type: "schedule",
134
+ connectorId: deps.config.id,
135
+ channelId: deps.channelId ?? null,
136
+ log: deps.diagnosticLog
137
+ });
133
138
  this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
134
139
  this.onFired = deps.onFired ?? null;
135
140
  }
136
141
  async start(notify) {
137
142
  this.stopped = false;
138
- this.recordConnection("started", "");
143
+ this.tickScheduled = true;
144
+ this.diagnostics.recordConnection("started", "");
139
145
  const scheduleNext = () => {
140
146
  if (this.stopped) return;
141
147
  const date = this.now();
142
148
  const msUntilNextMinute = 6e4 - (date.getSeconds() * 1e3 + date.getMilliseconds());
143
149
  this.timer = setTimeout(async () => {
144
150
  if (this.stopped) return;
145
- await this.tick(notify);
151
+ try {
152
+ await this.tick(notify);
153
+ } catch (error) {
154
+ this.recordTickError(error);
155
+ }
146
156
  scheduleNext();
147
157
  }, msUntilNextMinute);
148
158
  this.timer.unref();
159
+ this.tickScheduled = true;
149
160
  };
150
- await this.tick(notify);
161
+ try {
162
+ await this.tick(notify);
163
+ } catch (error) {
164
+ this.recordTickError(error);
165
+ }
166
+ this.diagnostics.recordConnection("connected", "");
151
167
  scheduleNext();
152
168
  }
153
169
  async stop() {
154
170
  this.stopped = true;
171
+ this.tickScheduled = false;
155
172
  if (this.timer) {
156
173
  clearTimeout(this.timer);
157
174
  this.timer = null;
158
175
  }
159
- this.recordConnection("stopped", "");
176
+ this.diagnostics.recordConnection("disconnected", "");
177
+ this.diagnostics.recordConnection("stopped", "");
178
+ }
179
+ recordTickError(error) {
180
+ const message = errorMessageOf(error);
181
+ this.diagnostics.recordConnection("error", `tick: ${message}`);
182
+ this.logger?.error("schedule tick failed", {
183
+ connector: this.config.name,
184
+ error: message
185
+ });
160
186
  }
161
187
  isAlive() {
162
- return !this.stopped && this.timer !== null;
188
+ return !this.stopped && this.tickScheduled;
163
189
  }
164
190
  async tick(notify) {
165
191
  const now = this.truncateToMinute(this.now());
@@ -209,14 +235,20 @@ var FunnelScheduleListener = class extends FunnelConnectorListener {
209
235
  };
210
236
  if (catchup) meta.catchup = "true";
211
237
  const eventId = `${entry.id}@${firedAt.toISOString()}`;
212
- this.recordRaw(eventId, entry, firedAt, catchup);
238
+ this.diagnostics.recordRaw(eventId, JSON.stringify({
239
+ schedule_id: entry.id,
240
+ cron: entry.cron,
241
+ prompt: entry.prompt,
242
+ fired_at: firedAt.toISOString(),
243
+ catchup
244
+ }));
213
245
  try {
214
246
  await notify(entry.prompt, meta);
215
247
  } catch (error) {
216
- this.recordProcessed(eventId, entry, "emitted:delivery-failed");
248
+ this.diagnostics.recordProcessed(eventId, "emitted:delivery-failed", entry.prompt);
217
249
  throw error;
218
250
  }
219
- this.recordProcessed(eventId, entry, "emitted");
251
+ this.diagnostics.recordProcessed(eventId, "emitted", entry.prompt);
220
252
  if (this.onFired) try {
221
253
  await this.onFired(entry, firedAt);
222
254
  } catch (error) {
@@ -263,7 +295,7 @@ var FunnelScheduleListener = class extends FunnelConnectorListener {
263
295
  }
264
296
  logInvalidCron(entry, error) {
265
297
  const message = errorMessageOf(error);
266
- this.recordConnection("error", `invalid cron "${entry.cron}" (entry ${entry.id}): ${message}`);
298
+ this.diagnostics.recordConnection("error", `invalid cron "${entry.cron}" (entry ${entry.id}): ${message}`);
267
299
  this.logger?.error("invalid cron expression in schedule", {
268
300
  connector: this.config.name,
269
301
  id: entry.id,
@@ -276,40 +308,6 @@ var FunnelScheduleListener = class extends FunnelConnectorListener {
276
308
  copy.setSeconds(0, 0);
277
309
  return copy;
278
310
  }
279
- recordRaw(eventId, entry, firedAt, catchup) {
280
- this.diagnosticLog?.recordRaw({
281
- eventId,
282
- type: "schedule",
283
- connectorId: this.config.id,
284
- channelId: this.channelId,
285
- payload: JSON.stringify({
286
- schedule_id: entry.id,
287
- cron: entry.cron,
288
- prompt: entry.prompt,
289
- fired_at: firedAt.toISOString(),
290
- catchup
291
- })
292
- });
293
- }
294
- recordProcessed(eventId, entry, outcome) {
295
- this.diagnosticLog?.recordProcessed({
296
- eventId,
297
- type: "schedule",
298
- connectorId: this.config.id,
299
- channelId: this.channelId,
300
- outcome,
301
- payload: entry.prompt
302
- });
303
- }
304
- recordConnection(status, detail) {
305
- this.diagnosticLog?.recordConnection({
306
- type: "schedule",
307
- connectorId: this.config.id,
308
- channelId: this.channelId,
309
- status,
310
- detail
311
- });
312
- }
313
311
  };
314
312
  //#endregion
315
313
  //#region lib/engine/connectors/schedule-connector.ts
@@ -335,14 +333,15 @@ const scheduleConnector = (options = {}) => ({
335
333
  const parsed = scheduleConnectorSchema.parse(config);
336
334
  return new FunnelScheduleListener({
337
335
  config: parsed,
338
- lastFiredStore: new ScheduleStateStore({
336
+ lastFiredStore: new FunnelScheduleStateStore({
339
337
  path: join(deps.connectorDir(deps.channelId, parsed.id), "state.json"),
340
338
  fs: deps.fs
341
339
  }),
342
340
  channelId: deps.channelId,
343
341
  logger: deps.logger,
344
342
  diagnosticLog: deps.diagnosticLog,
345
- onFired: options.onFired
343
+ onFired: options.onFired,
344
+ now: () => deps.clock.now()
346
345
  });
347
346
  },
348
347
  createAdapter: null,
@@ -409,4 +408,4 @@ const scheduleConnector = (options = {}) => ({
409
408
  }
410
409
  });
411
410
  //#endregion
412
- export { matchCron as i, FunnelScheduleListener as n, ScheduleStateStore as r, scheduleConnector as t };
411
+ export { matchCron as i, FunnelScheduleListener as n, FunnelScheduleStateStore as r, scheduleConnector as t };
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+
3
+ //#region lib/engine/connectors/schedule-connector-schema.d.ts
4
+ /**
5
+ * Catch-up behavior when the daemon was down past one or more matching minutes.
6
+ *
7
+ * - `latest`: fire once with the most recent missed match (default; preserves prior behavior).
8
+ * - `all`: fire once per missed minute, oldest first (capped at 24 h).
9
+ * - `skip`: never fire missed matches; only fire when the current minute matches.
10
+ */
11
+ declare const scheduleCatchupPolicySchema: z.ZodEnum<{
12
+ latest: "latest";
13
+ all: "all";
14
+ skip: "skip";
15
+ }>;
16
+ type ScheduleCatchupPolicy = z.infer<typeof scheduleCatchupPolicySchema>;
17
+ declare const scheduleEntrySchema: z.ZodObject<{
18
+ id: z.ZodString;
19
+ cron: z.ZodString;
20
+ prompt: z.ZodString;
21
+ enabled: z.ZodDefault<z.ZodBoolean>;
22
+ catchupPolicy: z.ZodDefault<z.ZodEnum<{
23
+ latest: "latest";
24
+ all: "all";
25
+ skip: "skip";
26
+ }>>;
27
+ }, z.core.$strip>;
28
+ type ScheduleEntry = z.infer<typeof scheduleEntrySchema>;
29
+ declare const scheduleConnectorSchema: z.ZodObject<{
30
+ id: z.ZodString;
31
+ name: z.ZodString;
32
+ type: z.ZodLiteral<"schedule">;
33
+ entries: z.ZodDefault<z.ZodArray<z.ZodObject<{
34
+ id: z.ZodString;
35
+ cron: z.ZodString;
36
+ prompt: z.ZodString;
37
+ enabled: z.ZodDefault<z.ZodBoolean>;
38
+ catchupPolicy: z.ZodDefault<z.ZodEnum<{
39
+ latest: "latest";
40
+ all: "all";
41
+ skip: "skip";
42
+ }>>;
43
+ }, z.core.$strip>>>;
44
+ createdAt: z.ZodOptional<z.ZodString>;
45
+ updatedAt: z.ZodOptional<z.ZodString>;
46
+ }, z.core.$strip>;
47
+ type ScheduleConnectorConfig = z.infer<typeof scheduleConnectorSchema>;
48
+ //#endregion
49
+ export { scheduleConnectorSchema as a, scheduleCatchupPolicySchema as i, ScheduleConnectorConfig as n, scheduleEntrySchema as o, ScheduleEntry as r, ScheduleCatchupPolicy as t };
@@ -0,0 +1,27 @@
1
+ import { a as Settings } from "./settings-schema-BL_c2Udm.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
+ * Atomic read-modify-write. Implementations must serialize against
18
+ * concurrent processes touching the same file (the Node store does so via
19
+ * an exclusive lockfile; Memory stores are single-threaded). Engine
20
+ * classes must use `update` for any mutation that depends on prior state,
21
+ * otherwise a concurrent CLI invocation or `fnl claude` launch can lose
22
+ * the edit through a read-modify-write race.
23
+ */
24
+ abstract update<T>(mutator: (settings: Settings) => T): T;
25
+ }
26
+ //#endregion
27
+ export { FunnelIdGenerator as n, FunnelSettingsReader as t };
@@ -1,5 +1,5 @@
1
- import { t as NodeFunnelFileSystem } from "./node-file-system-Blr8pAir.js";
2
- import { n as FunnelIdGenerator, t as FunnelSettingsReader } from "./settings-reader-CtQ-Ix8_.js";
1
+ import { t as NodeFunnelFileSystem } from "./node-file-system-BOXIHW_Q.js";
2
+ import { n as FunnelIdGenerator, t as FunnelSettingsReader } from "./settings-reader-9FcX3qS1.js";
3
3
  import { dirname, join } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
  import { z } from "zod";
@@ -29,11 +29,11 @@ const baseConnectorConfigSchema = z.object({
29
29
  //#region lib/engine/settings/settings-schema.ts
30
30
  /**
31
31
  * Connectors are stored loosely here: settings validates only the common base
32
- * fields and preserves every type-specific key verbatim (`.passthrough()`).
32
+ * fields and preserves every type-specific key verbatim (`.loose()`).
33
33
  * Core does not enumerate connector types, so strict per-type validation happens
34
34
  * at the registry/descriptor layer (CRUD time), not on every settings read.
35
35
  */
36
- const storedConnectorSchema = baseConnectorConfigSchema.passthrough();
36
+ const storedConnectorSchema = baseConnectorConfigSchema.loose();
37
37
  /**
38
38
  * Routing mode when multiple WS clients are subscribed to the same channel.
39
39
  *
@@ -192,6 +192,25 @@ var FunnelSettingsStore = class extends FunnelSettingsReader {
192
192
  };
193
193
  this.fs.writeSecretFileSync(this.path, `${JSON.stringify(versioned, null, 2)}\n`);
194
194
  }
195
+ /**
196
+ * Run `mutator` against a freshly-read settings object inside an exclusive
197
+ * file lock, then persist the result. Use this instead of bare `read()` +
198
+ * `write()` for any logical edit (add channel, set token, rename profile),
199
+ * so two concurrent CLI invocations or `fnl claude` launches cannot lose
200
+ * each other's updates via a read-modify-write race. The mutator may
201
+ * mutate `settings` in place and/or return a value; the value is returned
202
+ * to the caller. A thrown error from the mutator skips the write but still
203
+ * releases the lock.
204
+ */
205
+ update(mutator) {
206
+ this.fs.mkdirSync(dirname(this.path), { recursive: true });
207
+ return this.fs.withFileLock(`${this.path}.lock`, () => {
208
+ const settings = this.read();
209
+ const result = mutator(settings);
210
+ this.write(settings);
211
+ return result;
212
+ });
213
+ }
195
214
  };
196
215
  //#endregion
197
216
  export { resolveFunnelDir as a, channelConfigSchema as c, settingsSchema as d, baseConnectorConfigSchema as f, SETTINGS_PATH as i, channelDeliveryModeSchema as l, FUNNEL_DIR as n, resolveFunnelPort as o, NodeFunnelIdGenerator as p, FunnelSettingsStore as r, SETTINGS_VERSION as s, DEFAULT_GATEWAY_PORT as t, profileConfigSchema as u };
@@ -0,0 +1,359 @@
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
+ processor = null;
123
+ botToken = "";
124
+ constructor(deps) {
125
+ super({
126
+ type: "slack",
127
+ connectorId: deps.config.id,
128
+ channelId: deps.channelId ?? null,
129
+ logger: deps.logger,
130
+ diagnosticLog: deps.diagnosticLog
131
+ });
132
+ this.config = deps.config;
133
+ this.env = deps.env ?? process.env;
134
+ this.flumeDeps = deps.flumeDeps ?? {};
135
+ this.http = deps.http ?? new NodeFunnelHttpClient();
136
+ this.signal = deps.signal;
137
+ this.preprocessEvent = deps.preprocessEvent;
138
+ }
139
+ async start(notify) {
140
+ this.diagnostics.recordConnection("started", "");
141
+ let appToken;
142
+ let botToken;
143
+ try {
144
+ appToken = resolveConnectorToken({
145
+ literal: this.config.appToken,
146
+ envVar: this.config.appTokenEnv,
147
+ env: this.env,
148
+ label: `${this.config.name}.appToken`
149
+ });
150
+ botToken = resolveConnectorToken({
151
+ literal: this.config.botToken,
152
+ envVar: this.config.botTokenEnv,
153
+ env: this.env,
154
+ label: `${this.config.name}.botToken`
155
+ });
156
+ } catch (error) {
157
+ this.diagnostics.recordConnection("auth-failed", errorMessageOf(error));
158
+ throw error;
159
+ }
160
+ this.botToken = botToken;
161
+ const auth = await this.callAuthTest();
162
+ if (!auth.ok) {
163
+ const detail = auth.error ?? "auth.test returned ok=false";
164
+ this.diagnostics.recordConnection("auth-failed", detail);
165
+ throw new FunnelAuthFailedError(this.config.name, detail);
166
+ }
167
+ this.processor = new FunnelSlackEventProcessor({
168
+ ownBotUserId: auth.user_id ?? "",
169
+ ownBotId: auth.bot_id ?? "",
170
+ minify: this.config.minify
171
+ });
172
+ const source = new FlumeSlackSource({
173
+ appToken,
174
+ botToken: this.botToken
175
+ });
176
+ await this.runStart({
177
+ source,
178
+ onLog: flumeLogHandler(this.logger),
179
+ deps: resolveFlumeDeps(this.flumeDeps),
180
+ signal: this.signal,
181
+ onEvent: (event) => {
182
+ if (event.source !== "slack") return Promise.resolve();
183
+ return this.handleEvent(event, notify);
184
+ }
185
+ });
186
+ }
187
+ onStop() {
188
+ this.processor = null;
189
+ }
190
+ async callAuthTest() {
191
+ let text;
192
+ try {
193
+ text = await (await this.http.fetch({
194
+ method: "POST",
195
+ url: AUTH_TEST_URL,
196
+ headers: {
197
+ Authorization: `Bearer ${this.botToken}`,
198
+ "Content-Type": "application/x-www-form-urlencoded"
199
+ }
200
+ })).text();
201
+ } catch (error) {
202
+ this.diagnostics.recordConnection("auth-failed", errorMessageOf(error));
203
+ throw error;
204
+ }
205
+ const parsed = authTestResponseSchema.safeParse(safeJsonParse(text));
206
+ if (!parsed.success) return {
207
+ ok: false,
208
+ error: `non-JSON auth.test response: ${text.slice(0, 200)}`
209
+ };
210
+ return parsed.data;
211
+ }
212
+ async handleEvent(event, notify) {
213
+ if (!this.processor) return;
214
+ const rawEvent = event.data.event;
215
+ if (!isSlackRawEvent(rawEvent)) {
216
+ const skipId = crypto.randomUUID();
217
+ this.diagnostics.recordRaw(skipId, JSON.stringify(event.data));
218
+ this.diagnostics.recordProcessed(skipId, "skip:non-object-event", "");
219
+ return;
220
+ }
221
+ const eventId = crypto.randomUUID();
222
+ const rawJson = JSON.stringify(rawEvent);
223
+ this.diagnostics.recordRaw(eventId, rawJson);
224
+ let preprocessed = rawEvent;
225
+ if (this.preprocessEvent) {
226
+ const next = await this.preprocessEvent(rawEvent);
227
+ if (next === null) {
228
+ this.diagnostics.recordProcessed(eventId, "skip:preprocess", rawJson);
229
+ return;
230
+ }
231
+ preprocessed = next;
232
+ }
233
+ const result = this.processor.process(preprocessed);
234
+ if (result.skip) {
235
+ this.diagnostics.recordProcessed(eventId, result.reason, rawJson);
236
+ return;
237
+ }
238
+ await this.deliver(notify, eventId, rawJson, result.content, result.meta, result.shouldReact);
239
+ }
240
+ async deliver(notify, eventId, rawJson, content, meta, shouldReact) {
241
+ try {
242
+ await notify(content, meta);
243
+ } catch (error) {
244
+ this.diagnostics.recordProcessed(eventId, "emitted:delivery-failed", content || rawJson);
245
+ this.logger?.error("slack notify error", { error: errorMessageOf(error) });
246
+ return;
247
+ }
248
+ this.diagnostics.recordProcessed(eventId, "emitted", content);
249
+ if (shouldReact) this.postReaction(meta).catch((error) => {
250
+ this.diagnostics.recordProcessed(eventId, "emitted:reaction-failed", errorMessageOf(error));
251
+ this.logger?.warn("slack reaction failed", { error: errorMessageOf(error) });
252
+ });
253
+ }
254
+ async postReaction(meta) {
255
+ const res = await this.http.fetch({
256
+ method: "POST",
257
+ url: "https://slack.com/api/reactions.add",
258
+ headers: {
259
+ Authorization: `Bearer ${this.botToken}`,
260
+ "Content-Type": "application/x-www-form-urlencoded"
261
+ },
262
+ body: new URLSearchParams({
263
+ channel: meta.channel_id ?? "",
264
+ timestamp: meta.thread_ts ?? "",
265
+ name: "eyes"
266
+ }).toString()
267
+ });
268
+ const parsed = parseSlackResponse(await res.text());
269
+ if (!parsed.ok) throw new Error(`slack reactions.add: ${parsed.error ?? `status=${res.status}`}`);
270
+ }
271
+ };
272
+ const isSlackRawEvent = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
273
+ const safeJsonParse = (text) => {
274
+ try {
275
+ return JSON.parse(text);
276
+ } catch {
277
+ return null;
278
+ }
279
+ };
280
+ const slackResponseSchema = z.object({
281
+ ok: z.boolean(),
282
+ error: z.string().optional()
283
+ });
284
+ const parseSlackResponse = (text) => {
285
+ const parsed = slackResponseSchema.safeParse(safeJsonParse(text));
286
+ if (!parsed.success) return {
287
+ ok: false,
288
+ error: `non-JSON response: ${text.slice(0, 200)}`
289
+ };
290
+ return parsed.data;
291
+ };
292
+ //#endregion
293
+ //#region lib/engine/connectors/slack-connector.ts
294
+ /**
295
+ * Slack connector descriptor. Pass `slackConnector()` to
296
+ * `new Funnel({ connectors: [...] })` to enable the type.
297
+ *
298
+ * The listener is backed by `@interactive-inc/flume`'s `FlumeSlackSource`
299
+ * (raw Socket Mode WebSocket). Only the events API envelope is delivered —
300
+ * there is no equivalent for the Bolt-style `app.action` / `app.command`
301
+ * dispatch. For HTTP-side interactivity (buttons, slash commands), run a
302
+ * separate Bolt app outside funnel; this descriptor only handles the
303
+ * incoming events firehose.
304
+ */
305
+ const slackConnector = (options = {}) => ({
306
+ type: "slack",
307
+ toolExposed: true,
308
+ createListener(config, deps) {
309
+ return new FunnelFlumeSlackListener({
310
+ config: slackConnectorSchema.parse(config),
311
+ channelId: deps.channelId,
312
+ logger: deps.logger,
313
+ diagnosticLog: deps.diagnosticLog,
314
+ http: deps.http,
315
+ signal: deps.signal,
316
+ preprocessEvent: options.preprocessEvent
317
+ });
318
+ },
319
+ createAdapter(config, deps) {
320
+ return new FunnelSlackAdapter({
321
+ config: slackConnectorSchema.parse(config),
322
+ http: deps.http
323
+ });
324
+ },
325
+ secretTokens(config) {
326
+ const parsed = slackConnectorSchema.parse(config);
327
+ return [parsed.botToken, parsed.appToken].filter((token) => token !== void 0);
328
+ },
329
+ buildConfig(input, context) {
330
+ return slackConnectorSchema.parse({
331
+ id: context.id,
332
+ type: "slack",
333
+ name: input.name,
334
+ ...typeof input.botToken === "string" ? { botToken: input.botToken } : {},
335
+ ...typeof input.appToken === "string" ? { appToken: input.appToken } : {},
336
+ ...typeof input.botTokenEnv === "string" ? { botTokenEnv: input.botTokenEnv } : {},
337
+ ...typeof input.appTokenEnv === "string" ? { appTokenEnv: input.appTokenEnv } : {},
338
+ minify: typeof input.minify === "boolean" ? input.minify : true,
339
+ createdAt: context.now,
340
+ updatedAt: context.now
341
+ });
342
+ },
343
+ applyUpdate(config, fields, context) {
344
+ const current = slackConnectorSchema.parse(config);
345
+ return slackConnectorSchema.parse({
346
+ id: current.id,
347
+ name: current.name,
348
+ type: "slack",
349
+ minify: current.minify,
350
+ createdAt: current.createdAt,
351
+ updatedAt: context.now,
352
+ ...slotFields("botToken", "botTokenEnv", fields, current),
353
+ ...slotFields("appToken", "appTokenEnv", fields, current)
354
+ });
355
+ },
356
+ operations: {}
357
+ });
358
+ //#endregion
359
+ export { FunnelFlumeSlackListener as n, FunnelSlackAdapter as r, slackConnector as t };