@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
@@ -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 };
@@ -0,0 +1,249 @@
1
+ import { t as errorMessageOf } from "./error-message-of-ColuYmAk.js";
2
+ import { t as FunnelConnectorListener } from "./connector-listener-mPGZYa8e.js";
3
+ import { t as FunnelConnectorDiagnosticsRecorder } from "./connector-diagnostics-recorder-COtNEmUp.js";
4
+ import { Flume, createFlumeDefaultDeps } from "@interactive-inc/flume";
5
+ //#region lib/engine/connectors/flume-deps.ts
6
+ /**
7
+ * Builds the merged runtime deps Flume listeners pass to a source. Spreads the
8
+ * default IO over any partial test override and returns `undefined` when no
9
+ * override exists so Flume gets to use its own internal default and there is
10
+ * one less degree of freedom to debug.
11
+ */
12
+ const resolveFlumeDeps = (override) => {
13
+ if (!override || Object.keys(override).length === 0) return void 0;
14
+ return {
15
+ ...createFlumeDefaultDeps(),
16
+ ...override
17
+ };
18
+ };
19
+ /**
20
+ * Bridges a `FunnelLogger` into Flume's structured log stream. Returns
21
+ * `undefined` when no logger is wired so Flume's option stays cleanly absent
22
+ * instead of carrying a no-op handler.
23
+ *
24
+ * Forwards everything Flume produces: `detail` (reconnect counters, HTTP codes,
25
+ * parse offsets) is merged into the logger meta, and on errors the full stack
26
+ * is preserved — the leaf message alone is rarely enough to pinpoint a socket
27
+ * close or parse failure. Debug entries are dropped because FunnelLogger has no
28
+ * debug level and routing them to info would drown the operator log in
29
+ * heartbeats.
30
+ */
31
+ const flumeLogHandler = (logger) => {
32
+ if (!logger) return void 0;
33
+ return (log) => {
34
+ if (log.level === "debug") return;
35
+ const line = `${log.source}/${log.action}: ${log.message}`;
36
+ const meta = buildMeta(log);
37
+ if (log.level === "error") {
38
+ logger.error(line, meta);
39
+ return;
40
+ }
41
+ if (log.level === "warn") {
42
+ logger.warn(line, meta);
43
+ return;
44
+ }
45
+ logger.info(line, meta);
46
+ };
47
+ };
48
+ const buildMeta = (log) => {
49
+ const meta = { ...log.detail ?? {} };
50
+ if (log.error) {
51
+ meta.error = log.error.message;
52
+ if (log.error.stack) meta.stack = log.error.stack;
53
+ if (log.error.name && log.error.name !== "Error") meta.errorName = log.error.name;
54
+ }
55
+ return Object.keys(meta).length > 0 ? meta : void 0;
56
+ };
57
+ //#endregion
58
+ //#region lib/engine/connectors/flume-source-listener.ts
59
+ /**
60
+ * Shared lifecycle for any listener whose transport is a `FlumeSource`. Owns
61
+ * the per-listener `Flume` instance + the `FlumeRunning` handle returned by
62
+ * `open()`, the connected/alive bit, the `FlumeStatus ↔
63
+ * ConnectorConnectionStatus` mapping, and the close sequence — every Flume
64
+ * subclass plugs in only its own token resolution, source construction, and
65
+ * event dispatch around this skeleton.
66
+ *
67
+ * Flume 0.9 collapsed every observation channel into one firehose: events
68
+ * and all logs arrive through `onEvent` as a discriminated union
69
+ * (`{ kind: "event" } | { kind: "log" }`). This base class splits that back
70
+ * into the funnel-shaped trio (typed event handler, log forward, status
71
+ * mapping) so subclasses keep their per-protocol code unchanged.
72
+ */
73
+ var FunnelFlumeSourceListener = class extends FunnelConnectorListener {
74
+ logger;
75
+ diagnostics;
76
+ type;
77
+ running = null;
78
+ connected = false;
79
+ /**
80
+ * Flipped on by Flume's `reconnecting` status, off when the new socket
81
+ * lands on `connected` or the source gives up with `disconnected`. Used by
82
+ * `isAlive()` to treat a brief reconnect window as "still alive" so the
83
+ * supervisor does not preempt Flume's in-progress recovery with a heavier
84
+ * stop+start cycle (which would discard auth.test results and rebuild
85
+ * every per-listener state).
86
+ */
87
+ reconnecting = false;
88
+ /**
89
+ * Promise chain that serializes typed-event delivery. Flume's emitItem
90
+ * fire-and-forgets the onEvent callback (see flume.ts emitItem:
91
+ * `Promise.resolve(onEvent(item)).catch(() => {})`), so awaiting onEvent
92
+ * inside the handler does NOT pause flume's source queue — multiple event
93
+ * deliveries would race their microtask chains. Chaining each new event
94
+ * onto the previous promise's `.then(...)` guarantees per-listener
95
+ * end-to-end FIFO regardless of whether the notify path is sync or async.
96
+ */
97
+ deliveryChain = Promise.resolve();
98
+ constructor(props) {
99
+ super();
100
+ this.type = props.type;
101
+ this.logger = props.logger;
102
+ this.diagnostics = new FunnelConnectorDiagnosticsRecorder({
103
+ type: props.type,
104
+ connectorId: props.connectorId,
105
+ channelId: props.channelId,
106
+ log: props.diagnosticLog
107
+ });
108
+ }
109
+ /**
110
+ * Assemble a single-source Flume, open it, and store the `FlumeRunning`
111
+ * handle. Records `error` on any `Error` returned by `flume.open()` and
112
+ * rethrows so the supervisor sees the failure.
113
+ *
114
+ * The firehose handler routes:
115
+ * - `kind: "event"` → subclass's typed `onEvent`
116
+ * - `kind: "log"` with `action === "status"` → `handleStatus()`
117
+ * - `kind: "log"` (any) → optional `onLog` handler
118
+ */
119
+ async runStart(options) {
120
+ const reconnectOption = options.reconnect ?? true;
121
+ const flumeReconnect = reconnectOption === false ? void 0 : reconnectOption === true ? {} : reconnectOption;
122
+ const handleItem = (item) => {
123
+ if (item.kind === "event") {
124
+ this.deliveryChain = this.deliveryChain.catch(() => {}).then(() => Promise.resolve(options.onEvent(item.event)));
125
+ return;
126
+ }
127
+ const log = item.log;
128
+ const statusEvent = readStatusLog(log);
129
+ if (statusEvent) this.handleStatus(statusEvent);
130
+ options.onLog?.(log);
131
+ };
132
+ const flumeOptions = {
133
+ sources: [options.source],
134
+ onEvent: handleItem
135
+ };
136
+ if (options.deps) flumeOptions.deps = options.deps;
137
+ if (options.signal) flumeOptions.signal = options.signal;
138
+ if (flumeReconnect) flumeOptions.reconnect = flumeReconnect;
139
+ const result = await new Flume(flumeOptions).open();
140
+ if (result instanceof Error) {
141
+ this.diagnostics.recordConnection("error", errorMessageOf(result));
142
+ throw result;
143
+ }
144
+ this.running = result;
145
+ }
146
+ async stop() {
147
+ if (!this.running) return;
148
+ try {
149
+ await this.running.close();
150
+ this.diagnostics.recordConnection("disconnected", "");
151
+ } catch (error) {
152
+ this.diagnostics.recordConnection("error", errorMessageOf(error));
153
+ this.logger?.error(`${this.type} stop error`, { error: errorMessageOf(error) });
154
+ } finally {
155
+ this.running = null;
156
+ this.connected = false;
157
+ this.reconnecting = false;
158
+ this.deliveryChain = Promise.resolve();
159
+ this.onStop();
160
+ this.diagnostics.recordConnection("stopped", "");
161
+ }
162
+ }
163
+ isAlive() {
164
+ if (this.running === null) return false;
165
+ return this.connected || this.reconnecting;
166
+ }
167
+ /**
168
+ * Maps Flume's transport status to the connection table. `reconnecting`
169
+ * deliberately produces no row — Flume drives many transient reconnects per
170
+ * minute on a flaky network, and the row would drown the more meaningful
171
+ * `connected`/`disconnected` pair. The `reconnecting` flag still flips so
172
+ * `isAlive()` can surface it to the supervisor.
173
+ */
174
+ handleStatus(event) {
175
+ if (event.status === "connected") {
176
+ this.connected = true;
177
+ this.reconnecting = false;
178
+ this.diagnostics.recordConnection("connected", event.detail ?? "");
179
+ return;
180
+ }
181
+ if (event.status === "disconnected") {
182
+ this.connected = false;
183
+ this.reconnecting = false;
184
+ this.diagnostics.recordConnection("disconnected", event.detail ?? "");
185
+ return;
186
+ }
187
+ if (event.status === "reconnecting") {
188
+ this.connected = false;
189
+ this.reconnecting = true;
190
+ }
191
+ }
192
+ /**
193
+ * Hook for subclass-specific cleanup that has to run inside the stop()
194
+ * finally block (after the running handle is cleared, before the `stopped`
195
+ * row is recorded). Default is no-op.
196
+ */
197
+ onStop() {}
198
+ };
199
+ const STATUS_VALUES = [
200
+ "disconnected",
201
+ "connecting",
202
+ "connected",
203
+ "reconnecting"
204
+ ];
205
+ const isFlumeStatus = (value) => typeof value === "string" && STATUS_VALUES.includes(value);
206
+ /**
207
+ * Reconstructs the old `FlumeStatusEvent` shape from a `status` log entry.
208
+ * Returns `null` for anything else so the firehose pump is a single check.
209
+ * Flume 0.9 emits these as `log.action === "status"` with a structured
210
+ * `detail: { from, to, reason }` payload (see flume's FlumeStatusEmitter).
211
+ */
212
+ const readStatusLog = (log) => {
213
+ if (log.action !== "status") return null;
214
+ const detail = log.detail;
215
+ if (!detail) return null;
216
+ const to = detail.to;
217
+ if (!isFlumeStatus(to)) return null;
218
+ const reason = typeof detail.reason === "string" ? detail.reason : null;
219
+ return {
220
+ source: log.source,
221
+ status: to,
222
+ detail: reason
223
+ };
224
+ };
225
+ //#endregion
226
+ //#region lib/engine/connectors/slot-fields.ts
227
+ /**
228
+ * Resolves one token slot (e.g. botToken/botTokenEnv) for a connector update.
229
+ * The literal and the env-ref form are mutually exclusive: if `fields` supplies
230
+ * either, that form wins and the other key is omitted entirely; if it supplies
231
+ * neither, the connector's current slot is carried over unchanged. Returns a
232
+ * partial object spread into the rebuilt connector, so an omitted key is truly
233
+ * absent rather than set to undefined — switching a slot from literal to ref
234
+ * drops the stale literal instead of leaving both behind.
235
+ */
236
+ const slotFields = (literalKey, envKey, fields, current) => {
237
+ const literal = fields[literalKey];
238
+ if (typeof literal === "string") return { [literalKey]: literal };
239
+ const envVar = fields[envKey];
240
+ if (typeof envVar === "string") return { [envKey]: envVar };
241
+ const result = {};
242
+ const currentLiteral = current[literalKey];
243
+ const currentEnv = current[envKey];
244
+ if (typeof currentLiteral === "string") result[literalKey] = currentLiteral;
245
+ if (typeof currentEnv === "string") result[envKey] = currentEnv;
246
+ return result;
247
+ };
248
+ //#endregion
249
+ export { resolveFlumeDeps as i, FunnelFlumeSourceListener as n, flumeLogHandler as r, slotFields as t };