@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.
- package/README.md +2 -2
- package/dist/bin.js +428 -761
- package/dist/{channels-2g_BU1N0.d.ts → channels-B8RQPrVq.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-ClEEbuW3.d.ts} +50 -11
- 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 +71 -131
- 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-DGHxALfI.d.ts} +6 -6
- package/dist/{file-system-o51IsM0W.d.ts → file-system-VhwwXZbm.d.ts} +8 -0
- package/dist/flume-source-listener-Dim5szHG.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-DxRikYmu.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-DP_YV9xX.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-BU86fIge.js +359 -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,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 };
|