@interactive-inc/claude-funnel 0.52.0 → 0.55.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 +25 -3
- package/dist/bin.js +1276 -520
- package/dist/claude.d.ts +22 -5
- package/dist/claude.js +456 -169
- package/dist/connector-adapter-1PxjN-Uk.d.ts +25 -0
- package/dist/{connector-adapter-D5Utumgz.js → connector-adapter-qwXLjQId.js} +1 -1
- package/dist/{connector-listener-DU54DN-f.js → connector-listener-CpHBecCj.js} +1 -1
- package/dist/connectors/discord.d.ts +6 -6
- package/dist/connectors/discord.js +2 -2
- package/dist/connectors/gh.d.ts +6 -6
- package/dist/connectors/gh.js +2 -2
- package/dist/connectors/schedule.d.ts +12 -2
- package/dist/connectors/schedule.js +2 -2
- package/dist/connectors/slack.d.ts +3 -3
- package/dist/connectors/slack.js +2 -2
- package/dist/{connector-diagnostic-log-yTOojKUR.d.ts → diagnostic-log-Bxe7Bbvw.d.ts} +2 -2
- package/dist/diagnostic-sql-reader-CzYgZpq2.js +83 -0
- package/dist/diagnostics.d.ts +2 -0
- package/dist/diagnostics.js +2 -0
- package/dist/{discord-connector-schema-CBDyGdOI.js → discord-connector-schema-B_N6IXLz.js} +1 -1
- package/dist/{discord-connector-schema-R0Uu-3ns.d.ts → discord-connector-schema-CPgcZkXh.d.ts} +1 -1
- package/dist/{discord-listener-_jSE3HsQ.js → discord-listener-C0MoKdQO.js} +6 -6
- package/dist/docs.d.ts +2 -0
- package/dist/docs.js +2 -0
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +2 -0
- package/dist/{file-process-guard-BgrVHe9I.d.ts → file-process-guard-DI1742H5.d.ts} +31 -15
- package/dist/funnel-diagnostics-BpKYrMSu.js +300 -0
- package/dist/funnel-diagnostics-qWy5tPSq.d.ts +176 -0
- package/dist/funnel-docs-dXPokzr5.d.ts +18 -0
- package/dist/funnel-docs-ng5K8w4j.js +653 -0
- package/dist/funnel-doctor-BF3Rdgk0.d.ts +34 -0
- package/dist/funnel-doctor-CApCezTq.js +82 -0
- package/dist/funnel-recovery-BUBsu7WX.d.ts +101 -0
- package/dist/funnel-recovery-D9CxD5Zs.js +134 -0
- package/dist/gateway/daemon.js +810 -211
- package/dist/{settings-store-D2XSXTyt.js → gateway-base-url-6foMXfFf.js} +19 -6
- package/dist/gateway.d.ts +3 -3
- package/dist/gateway.js +3 -2
- package/dist/{gh-connector-schema-eoTtHbY6.d.ts → gh-connector-schema-CU1ojfIF.d.ts} +1 -1
- package/dist/{gh-connector-schema-o3Q1-ojL.js → gh-connector-schema-DUcZgN2Q.js} +1 -1
- package/dist/{gh-listener-DH-fClQm.js → gh-listener-Dsx6AmhH.js} +5 -5
- package/dist/{index-NFs2jzCa.d.ts → index-CrngHrne.d.ts} +187 -619
- package/dist/index.d.ts +16 -11
- package/dist/index.js +512 -976
- package/dist/{local-config-json-schema-8IHjS4Q7.js → local-config-json-schema-DE1zkMcb.js} +35 -9
- package/dist/{local-config-sync-BdsrDZOu.d.ts → local-config-sync-B8b04LrZ.d.ts} +45 -25
- package/dist/local-config.d.ts +2 -2
- package/dist/local-config.js +2 -2
- package/dist/{memory-connector-diagnostic-log-CrW1ltLM.js → memory-diagnostic-log-BZ1VD80X.js} +61 -99
- package/dist/{memory-token-prompter-B5FFCsGP.d.ts → memory-token-prompter-Lo3YRDzq.d.ts} +4 -4
- package/dist/{memory-token-prompter-CLerGsgM.js → memory-token-prompter-vBXxY20-.js} +2 -2
- package/dist/{profiles-f0mNmEyP.d.ts → profiles-EHTeCOqB.d.ts} +3 -2
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.js +1 -1
- package/dist/recovery.d.ts +2 -0
- package/dist/recovery.js +2 -0
- package/dist/{resolve-connector-token-BHmZLRrV.js → resolve-connector-token-CczqG_Ig.js} +1 -1
- package/dist/{schedule-connector-schema-iCI61gzU.js → schedule-connector-schema-B_xO5z5B.js} +1 -1
- package/dist/{schedule-listener-CUyUFFR1.d.ts → schedule-listener-DKh0hnkK.d.ts} +5 -5
- package/dist/{schedule-listener-ePAjians.js → schedule-listener-DP9Jhc6U.js} +14 -4
- package/dist/settings-reader-CBrgz01o.d.ts +18 -0
- package/dist/{settings-reader-BSU6JyvM.d.ts → settings-schema-zhnMIa8I.d.ts} +1 -16
- package/dist/{slack-connector-schema-BCNWluHM.js → slack-connector-schema-C1zEf4TG.js} +1 -1
- package/dist/{slack-listener-Bv5xI9gC.d.ts → slack-listener-COQA8wAZ.d.ts} +4 -4
- package/dist/{slack-listener-ClQuHhEF.js → slack-listener-DUKPcpJH.js} +7 -7
- package/dist/{mcp-Dr-nIBwN.js → yaml-render-OhUN-qkS.js} +52 -34
- package/package.json +21 -1
- package/dist/connector-adapter-DKgsVuMH.d.ts +0 -11
- /package/dist/{file-system-BeOKXjlV.d.ts → file-system-Wub9Nto4.d.ts} +0 -0
- /package/dist/{process-runner-DfniuWVU.d.ts → process-runner-D5I_jhYQ.d.ts} +0 -0
- /package/dist/{profiles-wMRnjSid.js → profiles-MnXvYfZF.js} +0 -0
package/dist/claude.js
CHANGED
|
@@ -1,14 +1,405 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
3
|
-
import {
|
|
4
|
-
import { t as
|
|
5
|
-
import {
|
|
1
|
+
import { f as settingsSchema, s as resolveFunnelPort, t as gatewayLoopbackUrl } from "./gateway-base-url-6foMXfFf.js";
|
|
2
|
+
import { a as FunnelMcp, i as FUNNEL_MCP_NAME, n as FUNNEL_MCP_ARGS, o as FileProcessGuard, r as FUNNEL_MCP_COMMAND, s as FunnelClaude, t as renderYaml } from "./yaml-render-OhUN-qkS.js";
|
|
3
|
+
import { t as FunnelDocs } from "./funnel-docs-ng5K8w4j.js";
|
|
4
|
+
import { a as FunnelLocalConfig, c as connectorSpecSchema, i as FunnelTokenPrompter, l as localConfigSchema, n as NodeFunnelTokenPrompter, o as LOCAL_CONFIG_FILENAME, r as FunnelLocalConfigSync, s as channelSpecSchema, t as funnelJsonSchema, u as profileSpecSchema } from "./local-config-json-schema-DE1zkMcb.js";
|
|
5
|
+
import { t as FunnelProfiles } from "./profiles-MnXvYfZF.js";
|
|
6
|
+
import { n as FunnelLocalConfigWriter, t as MemoryFunnelTokenPrompter } from "./memory-token-prompter-vBXxY20-.js";
|
|
6
7
|
import { join } from "node:path";
|
|
7
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
9
|
import { homedir } from "node:os";
|
|
9
10
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10
11
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
12
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
+
//#region lib/engine/mcp/channel-server-http.ts
|
|
14
|
+
const GATEWAY_OFFLINE_HINT = "Run `fnl gateway start` in a shell (MCP cannot start the daemon)";
|
|
15
|
+
/**
|
|
16
|
+
* Pass a value through `renderYaml` into the shape the MCP transport expects.
|
|
17
|
+
* Used for offline fallbacks where we synthesise a response locally instead
|
|
18
|
+
* of forwarding gateway JSON.
|
|
19
|
+
*/
|
|
20
|
+
const yamlResult = (value) => ({ content: [{
|
|
21
|
+
type: "text",
|
|
22
|
+
text: renderYaml(value)
|
|
23
|
+
}] });
|
|
24
|
+
/**
|
|
25
|
+
* MCP error result with `isError: true` and a structured `{ error, nextAction }`
|
|
26
|
+
* body. Keeping the shape uniform lets Claude pattern-match on `nextAction`
|
|
27
|
+
* without re-parsing free-form text.
|
|
28
|
+
*/
|
|
29
|
+
const errorResult = (message, nextAction) => ({
|
|
30
|
+
content: [{
|
|
31
|
+
type: "text",
|
|
32
|
+
text: renderYaml({
|
|
33
|
+
error: message,
|
|
34
|
+
nextAction
|
|
35
|
+
})
|
|
36
|
+
}],
|
|
37
|
+
isError: true
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* Convert a gateway HTTP response (JSON wire format) into the YAML text the
|
|
41
|
+
* MCP transport hands back to Claude. YAML is what every fnl_* tool returns,
|
|
42
|
+
* matching the CLI's output and keeping the parsing surface Claude has to
|
|
43
|
+
* learn down to one.
|
|
44
|
+
*/
|
|
45
|
+
const toYamlResult = async (res) => {
|
|
46
|
+
const text = await res.text();
|
|
47
|
+
try {
|
|
48
|
+
return { content: [{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: renderYaml(JSON.parse(text))
|
|
51
|
+
}] };
|
|
52
|
+
} catch {
|
|
53
|
+
return { content: [{
|
|
54
|
+
type: "text",
|
|
55
|
+
text
|
|
56
|
+
}] };
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const getJson = async (url, headers, options = {}) => {
|
|
60
|
+
const res = await fetch(url, { headers }).catch(() => null);
|
|
61
|
+
if (!res) {
|
|
62
|
+
if (options.offlineFallback !== void 0) return yamlResult(options.offlineFallback);
|
|
63
|
+
return errorResult("gateway unreachable", GATEWAY_OFFLINE_HINT);
|
|
64
|
+
}
|
|
65
|
+
return toYamlResult(res);
|
|
66
|
+
};
|
|
67
|
+
const postJson = async (url, headers, body, options = {}) => {
|
|
68
|
+
const res = await fetch(url, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: {
|
|
71
|
+
...headers,
|
|
72
|
+
"content-type": "application/json"
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify(body)
|
|
75
|
+
}).catch(() => null);
|
|
76
|
+
if (!res) {
|
|
77
|
+
if (options.offlineFallback !== void 0) return yamlResult(options.offlineFallback);
|
|
78
|
+
return errorResult("gateway unreachable", GATEWAY_OFFLINE_HINT);
|
|
79
|
+
}
|
|
80
|
+
return toYamlResult(res);
|
|
81
|
+
};
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region lib/engine/mcp/channel-server-builtin-handler.ts
|
|
84
|
+
const docs = new FunnelDocs();
|
|
85
|
+
/**
|
|
86
|
+
* Dispatch a built-in fnl_* tool call to the right gateway endpoint (over
|
|
87
|
+
* loopback HTTP) and return the response as a YAML-text MCP result. The few
|
|
88
|
+
* read-only tools that can run without the gateway (docs lookup) bypass HTTP
|
|
89
|
+
* entirely.
|
|
90
|
+
*/
|
|
91
|
+
const handleBuiltinTool = async (deps) => {
|
|
92
|
+
const headers = {};
|
|
93
|
+
if (deps.token) headers.authorization = `Bearer ${deps.token}`;
|
|
94
|
+
const args = deps.args;
|
|
95
|
+
const channelArg = typeof args?.channel === "string" ? args.channel : null;
|
|
96
|
+
const limitArg = typeof args?.limit === "number" ? args.limit : void 0;
|
|
97
|
+
const seqArg = typeof args?.seq === "number" ? args.seq : void 0;
|
|
98
|
+
const modeArg = args?.mode === "safe" || args?.mode === "aggressive" || args?.mode === "off" ? args.mode : "off";
|
|
99
|
+
const topicArg = typeof args?.topic === "string" ? args.topic : null;
|
|
100
|
+
const base = deps.gatewayBaseUrl;
|
|
101
|
+
if (deps.name === "fnl_doctor") return postJson(`${base}/doctor`, headers, { mode: modeArg }, { offlineFallback: doctorOfflineFallback(deps.allChannels) });
|
|
102
|
+
if (deps.name === "fnl_status") return getJson(`${base}/status`, headers, { offlineFallback: {
|
|
103
|
+
running: false,
|
|
104
|
+
error: "gateway unreachable",
|
|
105
|
+
nextAction: "run `fnl gateway start` in a shell (cannot be started from MCP)",
|
|
106
|
+
knownChannels: deps.allChannels.map((ch) => ch.name)
|
|
107
|
+
} });
|
|
108
|
+
if (deps.name === "fnl_debug") return getJson(channelArg ? `${base}/diagnostics?channel=${encodeURIComponent(channelArg)}` : `${base}/diagnostics?all=true`, headers, { offlineFallback: {
|
|
109
|
+
gateway: { running: false },
|
|
110
|
+
diagnosis: {
|
|
111
|
+
status: "error",
|
|
112
|
+
message: "gateway is not running",
|
|
113
|
+
nextActions: ["fnl gateway start"],
|
|
114
|
+
rootCause: null
|
|
115
|
+
},
|
|
116
|
+
knownChannels: deps.allChannels.map((ch) => ch.name)
|
|
117
|
+
} });
|
|
118
|
+
if (deps.name === "fnl_recent_events") return getJson(`${base}/diagnostics/events?${eventListQuery(channelArg, limitArg)}`, headers);
|
|
119
|
+
if (deps.name === "fnl_dropped_events") return getJson(`${base}/diagnostics/dropped?${eventListQuery(channelArg, limitArg)}`, headers);
|
|
120
|
+
if (deps.name === "fnl_connection_errors") return getJson(`${base}/diagnostics/errors?${eventListQuery(channelArg, limitArg)}`, headers);
|
|
121
|
+
if (deps.name === "fnl_replay_event") {
|
|
122
|
+
if (!channelArg) return errorResult("channel is required", "Provide the channel name (see fnl_status)");
|
|
123
|
+
return postJson(`${base}/diagnostics/replay`, headers, {
|
|
124
|
+
channel: channelArg,
|
|
125
|
+
seq: seqArg
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (deps.name === "fnl_docs") {
|
|
129
|
+
if (!topicArg) return yamlResult({ topics: docs.list() });
|
|
130
|
+
const text = docs.get(topicArg);
|
|
131
|
+
if (text === null) return yamlResult({
|
|
132
|
+
error: `unknown topic: ${topicArg}`,
|
|
133
|
+
availableTopics: docs.topics()
|
|
134
|
+
});
|
|
135
|
+
return yamlResult({
|
|
136
|
+
topic: topicArg,
|
|
137
|
+
text
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return errorResult(`unknown built-in tool: ${deps.name}`, null);
|
|
141
|
+
};
|
|
142
|
+
const eventListQuery = (channel, limit) => {
|
|
143
|
+
const params = new URLSearchParams();
|
|
144
|
+
if (channel) params.set("channel", channel);
|
|
145
|
+
if (limit !== void 0) params.set("limit", String(limit));
|
|
146
|
+
return params.toString();
|
|
147
|
+
};
|
|
148
|
+
const doctorOfflineFallback = (allChannels) => ({
|
|
149
|
+
status: "error",
|
|
150
|
+
message: "gateway is not running and cannot be started over HTTP from the MCP side. Run `fnl gateway start` in a shell, or relaunch Claude with `fnl claude` which auto-starts the gateway.",
|
|
151
|
+
appliedActions: [],
|
|
152
|
+
remainingIssues: allChannels.map((ch) => ({
|
|
153
|
+
channel: ch.name,
|
|
154
|
+
diagnosis: {
|
|
155
|
+
status: "error",
|
|
156
|
+
message: "gateway is not running",
|
|
157
|
+
nextActions: ["fnl gateway start"],
|
|
158
|
+
rootCause: null
|
|
159
|
+
}
|
|
160
|
+
})),
|
|
161
|
+
before: null,
|
|
162
|
+
after: null
|
|
163
|
+
});
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region lib/engine/mcp/channel-server-instructions.ts
|
|
166
|
+
/**
|
|
167
|
+
* Build the MCP server's `instructions` text. Claude reads this once at session
|
|
168
|
+
* start to learn the event format and the autonomous troubleshooting loop.
|
|
169
|
+
* Pure function — kept in its own file so updating prompt content does not
|
|
170
|
+
* touch the server bootstrap.
|
|
171
|
+
*/
|
|
172
|
+
const buildChannelServerInstructions = (allChannels, currentChannelName) => {
|
|
173
|
+
return [
|
|
174
|
+
`Events arrive as notifications (method: notifications/claude/channel) with two fields:`,
|
|
175
|
+
` content — the event payload as a JSON string (parse it to read the message)`,
|
|
176
|
+
` meta — key/value strings describing the event`,
|
|
177
|
+
"",
|
|
178
|
+
"meta fields by event_type:",
|
|
179
|
+
" slack: event_type=slack channel_id=C… thread_ts=1234.5678 user_id=U… mentioned=true|false",
|
|
180
|
+
" gh: event_type=gh repository=owner/repo subject_type=Issue|PullRequest subject_url=… reason=…",
|
|
181
|
+
" discord: event_type=discord channel_id=… user_id=… guild_id=… mentioned=true|false",
|
|
182
|
+
" schedule: event_type=schedule entry_id=…",
|
|
183
|
+
"",
|
|
184
|
+
"To reply to a Slack message in the same thread, call the connector tool with:",
|
|
185
|
+
` method: POST`,
|
|
186
|
+
` path: chat.postMessage`,
|
|
187
|
+
` body: { channel: meta.channel_id, text: "your reply", thread_ts: meta.thread_ts }`,
|
|
188
|
+
"",
|
|
189
|
+
"To comment on a GitHub issue/PR (extract from subject_url in meta):",
|
|
190
|
+
` method: POST`,
|
|
191
|
+
` path: repos/<meta.repository>/issues/<number>/comments`,
|
|
192
|
+
` body: { body: "your reply" }`,
|
|
193
|
+
"",
|
|
194
|
+
"When anything seems off (events stopped, a tool failed), call fnl_doctor first:",
|
|
195
|
+
" fnl_doctor → diagnose all channels (read-only)",
|
|
196
|
+
" fnl_doctor { mode: \"safe\" } → diagnose + safely fix what can be fixed",
|
|
197
|
+
" fnl_doctor { mode: \"aggressive\" } → also restart the gateway if needed",
|
|
198
|
+
"",
|
|
199
|
+
"fnl_doctor returns { status, message, appliedActions, remainingIssues } —",
|
|
200
|
+
"if status is 'ok' you are done; otherwise read remainingIssues for what is left.",
|
|
201
|
+
"",
|
|
202
|
+
"Other diagnostic tools you can call freely:",
|
|
203
|
+
" fnl_status gateway running state, listener health snapshot",
|
|
204
|
+
" fnl_debug per-channel diagnosis (subset of fnl_doctor)",
|
|
205
|
+
" fnl_recent_events last N processed events with outcome",
|
|
206
|
+
" fnl_dropped_events events filtered out (skip:*) with reason",
|
|
207
|
+
" fnl_connection_errors listener auth-failed / error events",
|
|
208
|
+
" fnl_replay_event replay a past event to test a fix",
|
|
209
|
+
" fnl_docs embedded docs (architecture / debugging / recipes / …)",
|
|
210
|
+
"",
|
|
211
|
+
"Always prefer the built-in fnl_* tools over invoking the shell — they go directly to the",
|
|
212
|
+
"gateway over loopback HTTP and return structured JSON.",
|
|
213
|
+
allChannels.length > 0 ? [
|
|
214
|
+
"",
|
|
215
|
+
"Configured channels (use as the `channel` argument to diagnostic tools):",
|
|
216
|
+
...allChannels.map((ch) => ` ${ch.name}${ch.name === currentChannelName ? " ← this session" : ""}`)
|
|
217
|
+
].join("\n") : ""
|
|
218
|
+
].join("\n");
|
|
219
|
+
};
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region lib/engine/mcp/usage-hint-for-type.ts
|
|
222
|
+
const usageHintForType = (type) => {
|
|
223
|
+
if (type === "slack") return [
|
|
224
|
+
"Slack Web API.",
|
|
225
|
+
"To reply in the same thread: method=POST path=chat.postMessage body={ channel: meta.channel_id, text: \"...\", thread_ts: meta.thread_ts }",
|
|
226
|
+
"To react: method=POST path=reactions.add body={ channel: meta.channel_id, timestamp: meta.thread_ts, name: \"thumbsup\" }",
|
|
227
|
+
"Use meta fields from the incoming event: channel_id (Slack channel), thread_ts (thread anchor), user_id (sender)."
|
|
228
|
+
].join(" ");
|
|
229
|
+
if (type === "discord") return [
|
|
230
|
+
"Discord REST API.",
|
|
231
|
+
"To reply: method=POST path=/channels/<meta.channel_id>/messages body={ content: \"...\" }",
|
|
232
|
+
"Use meta fields: channel_id (Discord channel), user_id (sender), guild_id."
|
|
233
|
+
].join(" ");
|
|
234
|
+
if (type === "gh") return [
|
|
235
|
+
"GitHub REST via gh CLI.",
|
|
236
|
+
"To comment: method=POST path=repos/<meta.repository>/issues/<number>/comments body={ body: \"...\" }",
|
|
237
|
+
"Parse <number> from meta.subject_url. meta fields: repository (owner/repo), subject_type, subject_url, reason."
|
|
238
|
+
].join(" ");
|
|
239
|
+
return "Generic adapter call.";
|
|
240
|
+
};
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region lib/engine/mcp/channel-server-tools.ts
|
|
243
|
+
const BUILTIN_TOOL_NAMES = [
|
|
244
|
+
"fnl_doctor",
|
|
245
|
+
"fnl_status",
|
|
246
|
+
"fnl_debug",
|
|
247
|
+
"fnl_recent_events",
|
|
248
|
+
"fnl_dropped_events",
|
|
249
|
+
"fnl_connection_errors",
|
|
250
|
+
"fnl_replay_event",
|
|
251
|
+
"fnl_docs"
|
|
252
|
+
];
|
|
253
|
+
const isBuiltinTool = (name) => BUILTIN_TOOL_NAMES.includes(name);
|
|
254
|
+
/**
|
|
255
|
+
* Build the per-connector outbound tools. One tool per connector; the channel
|
|
256
|
+
* MCP server exposes them so Claude can dispatch HTTP / API calls without
|
|
257
|
+
* shelling out.
|
|
258
|
+
*/
|
|
259
|
+
const buildConnectorTools = (connectors) => connectors.map((connector) => ({
|
|
260
|
+
name: connector.name,
|
|
261
|
+
description: `Call the "${connector.name}" (${connector.type}) connector. ${usageHintForType(connector.type)}`,
|
|
262
|
+
inputSchema: {
|
|
263
|
+
type: "object",
|
|
264
|
+
properties: {
|
|
265
|
+
method: {
|
|
266
|
+
type: "string",
|
|
267
|
+
description: "HTTP verb or API method (e.g. POST, chat.postMessage)"
|
|
268
|
+
},
|
|
269
|
+
path: {
|
|
270
|
+
type: "string",
|
|
271
|
+
description: "API path or method name (adapter-specific)"
|
|
272
|
+
},
|
|
273
|
+
body: {
|
|
274
|
+
type: "object",
|
|
275
|
+
description: "Request body / params (adapter-specific)"
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
required: ["method", "path"]
|
|
279
|
+
}
|
|
280
|
+
}));
|
|
281
|
+
/**
|
|
282
|
+
* Build the built-in fnl_* tools that drive diagnosis, recovery, and docs.
|
|
283
|
+
* The list mirrors BUILTIN_TOOL_NAMES; descriptions are tuned for Claude to
|
|
284
|
+
* pick the right tool from the name + summary alone.
|
|
285
|
+
*/
|
|
286
|
+
const buildBuiltinTools = (allChannels) => {
|
|
287
|
+
const channelEnum = allChannels.length > 0 ? allChannels.map((ch) => ch.name) : void 0;
|
|
288
|
+
const channelArgSchema = channelEnum ? {
|
|
289
|
+
type: "string",
|
|
290
|
+
description: `Channel name to inspect. One of: ${channelEnum.join(", ")}. Omit to use the first available.`,
|
|
291
|
+
enum: channelEnum
|
|
292
|
+
} : {
|
|
293
|
+
type: "string",
|
|
294
|
+
description: "Channel name. Omit to use the first available."
|
|
295
|
+
};
|
|
296
|
+
return [
|
|
297
|
+
{
|
|
298
|
+
name: "fnl_doctor",
|
|
299
|
+
description: "Diagnose every channel and optionally apply safe self-healing fixes. Returns { status (ok|warn|error), message, appliedActions[], remainingIssues[], before, after }. When status is 'ok' you are done. Use this as the FIRST tool when anything seems off — it replaces the older diagnose → recover → verify loop with one call.",
|
|
300
|
+
inputSchema: {
|
|
301
|
+
type: "object",
|
|
302
|
+
properties: { mode: {
|
|
303
|
+
type: "string",
|
|
304
|
+
enum: [
|
|
305
|
+
"off",
|
|
306
|
+
"safe",
|
|
307
|
+
"aggressive"
|
|
308
|
+
],
|
|
309
|
+
description: "off (default): read-only diagnosis. safe: also start gateway if down and restart dead listeners. aggressive: also restart the gateway when safe fixes are not enough."
|
|
310
|
+
} }
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: "fnl_status",
|
|
315
|
+
description: "Return the gateway status as JSON — gateway running state, every listener's alive/dead per channel, and connected Claude WS clients. Lightweight snapshot; use fnl_doctor when you want diagnosis with next actions.",
|
|
316
|
+
inputSchema: {
|
|
317
|
+
type: "object",
|
|
318
|
+
properties: {}
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: "fnl_debug",
|
|
323
|
+
description: "Return a full channel diagnosis as JSON — gateway health, listeners, Claude WS connection, last 5 inbound events, connection errors (when listener is dead), and `diagnosis` with `status` (ok|warn|error), `message`, `nextActions`, and `rootCause`. Call this whenever events seem missing or to verify a fix worked. Omit `channel` to diagnose all channels.",
|
|
324
|
+
inputSchema: {
|
|
325
|
+
type: "object",
|
|
326
|
+
properties: { channel: {
|
|
327
|
+
...channelArgSchema,
|
|
328
|
+
description: `${channelArgSchema.description} Omit to diagnose all channels.`
|
|
329
|
+
} }
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
name: "fnl_recent_events",
|
|
334
|
+
description: "Return the last N processed events for a channel as JSON, with `outcome` (emitted | skip:type | skip:dedup | skip:subtype | skip:self-user | …), payload preview, and seq. Use this when you need to confirm whether a specific event reached the broadcaster.",
|
|
335
|
+
inputSchema: {
|
|
336
|
+
type: "object",
|
|
337
|
+
properties: {
|
|
338
|
+
channel: channelArgSchema,
|
|
339
|
+
limit: {
|
|
340
|
+
type: "number",
|
|
341
|
+
description: "Max rows (default 20)"
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: "fnl_dropped_events",
|
|
348
|
+
description: "Return events that were filtered out (outcome starts with `skip:`) for a channel. Use when fnl_debug says listeners look healthy but a particular message never reached Claude — the skip reason tells you why.",
|
|
349
|
+
inputSchema: {
|
|
350
|
+
type: "object",
|
|
351
|
+
properties: {
|
|
352
|
+
channel: channelArgSchema,
|
|
353
|
+
limit: {
|
|
354
|
+
type: "number",
|
|
355
|
+
description: "Max rows (default 20)"
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: "fnl_connection_errors",
|
|
362
|
+
description: "Return listener connection lifecycle errors (auth-failed / error). Use when a listener never connects or keeps disconnecting — the detail field carries the upstream error message.",
|
|
363
|
+
inputSchema: {
|
|
364
|
+
type: "object",
|
|
365
|
+
properties: {
|
|
366
|
+
channel: channelArgSchema,
|
|
367
|
+
limit: {
|
|
368
|
+
type: "number",
|
|
369
|
+
description: "Max rows (default 20)"
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: "fnl_replay_event",
|
|
376
|
+
description: "Re-publish a past event back into a channel so subscribers receive it again. Call after applying a fix to verify Claude handles the previously-failed event correctly. Without `seq`, replays the most recent emitted event for that channel.",
|
|
377
|
+
inputSchema: {
|
|
378
|
+
type: "object",
|
|
379
|
+
properties: {
|
|
380
|
+
channel: channelArgSchema,
|
|
381
|
+
seq: {
|
|
382
|
+
type: "number",
|
|
383
|
+
description: "Processed-table seq to replay (optional)"
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
required: ["channel"]
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
name: "fnl_docs",
|
|
391
|
+
description: "Return embedded funnel documentation. Without `topic`, returns the list of topics. With `topic`, returns the full text. Topics: architecture, channels, connectors, profiles, claude, mcp, gateway, local-config, debugging, recipes, glossary.",
|
|
392
|
+
inputSchema: {
|
|
393
|
+
type: "object",
|
|
394
|
+
properties: { topic: {
|
|
395
|
+
type: "string",
|
|
396
|
+
description: "Topic name (omit to list available topics)"
|
|
397
|
+
} }
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
];
|
|
401
|
+
};
|
|
402
|
+
//#endregion
|
|
12
403
|
//#region lib/engine/mcp/channel-subscriber.ts
|
|
13
404
|
const RECONNECT_DELAY = 1e3;
|
|
14
405
|
const MAX_RECONNECT_DELAY = 1e4;
|
|
@@ -98,31 +489,8 @@ const readGatewayToken = (dir) => {
|
|
|
98
489
|
return value.length > 0 ? value : null;
|
|
99
490
|
};
|
|
100
491
|
//#endregion
|
|
101
|
-
//#region lib/engine/mcp/usage-hint-for-type.ts
|
|
102
|
-
const usageHintForType = (type) => {
|
|
103
|
-
if (type === "slack") return [
|
|
104
|
-
"Slack Web API.",
|
|
105
|
-
"To reply in the same thread: method=POST path=chat.postMessage body={ channel: meta.channel_id, text: \"...\", thread_ts: meta.thread_ts }",
|
|
106
|
-
"To react: method=POST path=reactions.add body={ channel: meta.channel_id, timestamp: meta.thread_ts, name: \"thumbsup\" }",
|
|
107
|
-
"Use meta fields from the incoming event: channel_id (Slack channel), thread_ts (thread anchor), user_id (sender)."
|
|
108
|
-
].join(" ");
|
|
109
|
-
if (type === "discord") return [
|
|
110
|
-
"Discord REST API.",
|
|
111
|
-
"To reply: method=POST path=/channels/<meta.channel_id>/messages body={ content: \"...\" }",
|
|
112
|
-
"Use meta fields: channel_id (Discord channel), user_id (sender), guild_id."
|
|
113
|
-
].join(" ");
|
|
114
|
-
if (type === "gh") return [
|
|
115
|
-
"GitHub REST via gh CLI.",
|
|
116
|
-
"To comment: method=POST path=repos/<meta.repository>/issues/<number>/comments body={ body: \"...\" }",
|
|
117
|
-
"Parse <number> from meta.subject_url. meta fields: repository (owner/repo), subject_type, subject_url, reason."
|
|
118
|
-
].join(" ");
|
|
119
|
-
return "Generic adapter call.";
|
|
120
|
-
};
|
|
121
|
-
//#endregion
|
|
122
492
|
//#region lib/engine/mcp/channel-server.ts
|
|
123
493
|
const DEFAULT_FUNNEL_DIR = join(homedir(), ".funnel");
|
|
124
|
-
const BUILTIN_TOOL_NAMES = ["fnl_status", "fnl_debug"];
|
|
125
|
-
const isBuiltinTool = (name) => BUILTIN_TOOL_NAMES.includes(name);
|
|
126
494
|
const readAllChannels = (dir) => {
|
|
127
495
|
const settingsPath = join(dir, "settings.json");
|
|
128
496
|
if (!existsSync(settingsPath)) return [];
|
|
@@ -138,20 +506,26 @@ const readAllChannels = (dir) => {
|
|
|
138
506
|
return [];
|
|
139
507
|
}
|
|
140
508
|
};
|
|
509
|
+
/**
|
|
510
|
+
* Start the funnel MCP server over stdio. Wires:
|
|
511
|
+
*
|
|
512
|
+
* - inbound channel subscription (gateway WebSocket → notifications/claude/channel)
|
|
513
|
+
* - outbound per-connector tools (one tool per connector, dispatched via gateway HTTP)
|
|
514
|
+
* - built-in fnl_* diagnostic + recovery tools (delegated to handleBuiltinTool)
|
|
515
|
+
*
|
|
516
|
+
* Tool definitions live in channel-server-tools.ts; the instructions string in
|
|
517
|
+
* channel-server-instructions.ts; HTTP helpers in channel-server-http.ts. This
|
|
518
|
+
* file is the orchestrator — wiring only, no business logic.
|
|
519
|
+
*/
|
|
141
520
|
const startChannelServer = async (options = {}) => {
|
|
142
521
|
const dir = options.dir ?? DEFAULT_FUNNEL_DIR;
|
|
143
|
-
const gatewayBaseUrl = options.gatewayUrl ?? process.env.FUNNEL_GATEWAY_URL ??
|
|
522
|
+
const gatewayBaseUrl = options.gatewayUrl ?? process.env.FUNNEL_GATEWAY_URL ?? gatewayLoopbackUrl(resolveFunnelPort());
|
|
144
523
|
const gatewayWsUrl = `${gatewayBaseUrl.replace(/^http/, "ws")}/ws`;
|
|
145
524
|
const channelId = options.channelId ?? process.env.FUNNEL_CHANNEL_ID;
|
|
146
525
|
const channel = channelId ? readChannelConnectors(dir, channelId) : null;
|
|
147
526
|
const token = options.token ?? readGatewayToken(dir);
|
|
148
527
|
const allChannels = readAllChannels(dir);
|
|
149
528
|
const currentChannelName = channel?.channelName ?? null;
|
|
150
|
-
const channelContext = allChannels.length > 0 ? [
|
|
151
|
-
"",
|
|
152
|
-
"Configured channels (use as the `channel` argument to fnl_debug):",
|
|
153
|
-
...allChannels.map((ch) => ` ${ch.name}${ch.name === currentChannelName ? " ← this session" : ""}`)
|
|
154
|
-
].join("\n") : "";
|
|
155
529
|
const server = new Server({
|
|
156
530
|
name: FUNNEL_MCP_NAME,
|
|
157
531
|
version: "1.0.0"
|
|
@@ -160,112 +534,30 @@ const startChannelServer = async (options = {}) => {
|
|
|
160
534
|
experimental: { "claude/channel": {} },
|
|
161
535
|
tools: {}
|
|
162
536
|
},
|
|
163
|
-
instructions:
|
|
164
|
-
`Events arrive as notifications (method: notifications/claude/channel) with two fields:`,
|
|
165
|
-
` content — the event payload as a JSON string (parse it to read the message)`,
|
|
166
|
-
` meta — key/value strings describing the event`,
|
|
167
|
-
"",
|
|
168
|
-
"meta fields by event_type:",
|
|
169
|
-
" slack: event_type=slack channel_id=C… thread_ts=1234.5678 user_id=U… mentioned=true|false",
|
|
170
|
-
" gh: event_type=gh repository=owner/repo subject_type=Issue|PullRequest subject_url=… reason=…",
|
|
171
|
-
" discord: event_type=discord channel_id=… user_id=… guild_id=… mentioned=true|false",
|
|
172
|
-
" schedule: event_type=schedule entry_id=…",
|
|
173
|
-
"",
|
|
174
|
-
"To reply to a Slack message in the same thread, call the connector tool with:",
|
|
175
|
-
` method: POST`,
|
|
176
|
-
` path: chat.postMessage`,
|
|
177
|
-
` body: { channel: meta.channel_id, text: "your reply", thread_ts: meta.thread_ts }`,
|
|
178
|
-
"",
|
|
179
|
-
"To comment on a GitHub issue/PR (extract from subject_url in meta):",
|
|
180
|
-
` method: POST`,
|
|
181
|
-
` path: repos/<meta.repository>/issues/<number>/comments (parse number from meta.subject_url)`,
|
|
182
|
-
` body: { body: "your reply" }`,
|
|
183
|
-
"",
|
|
184
|
-
"Built-in diagnostic tools — call proactively when events seem missing or delayed:",
|
|
185
|
-
" fnl_status — gateway running state, all listeners alive/dead, Claude WS clients",
|
|
186
|
-
" fnl_debug — per-channel diagnosis with last 10 events, rootCause, suggestedActions",
|
|
187
|
-
" omit channel arg to diagnose all channels; check summary.suggestedActions first",
|
|
188
|
-
channelContext
|
|
189
|
-
].join("\n")
|
|
190
|
-
});
|
|
191
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
192
|
-
const connectorTools = (channel?.connectors ?? []).map((c) => ({
|
|
193
|
-
name: c.name,
|
|
194
|
-
description: `Call the "${c.name}" (${c.type}) connector. ${usageHintForType(c.type)}`,
|
|
195
|
-
inputSchema: {
|
|
196
|
-
type: "object",
|
|
197
|
-
properties: {
|
|
198
|
-
method: {
|
|
199
|
-
type: "string",
|
|
200
|
-
description: "HTTP verb or API method (e.g. POST, chat.postMessage)"
|
|
201
|
-
},
|
|
202
|
-
path: {
|
|
203
|
-
type: "string",
|
|
204
|
-
description: "API path or method name (adapter-specific)"
|
|
205
|
-
},
|
|
206
|
-
body: {
|
|
207
|
-
type: "object",
|
|
208
|
-
description: "Request body / params (adapter-specific)"
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
required: ["method", "path"]
|
|
212
|
-
}
|
|
213
|
-
}));
|
|
214
|
-
const channelEnum = allChannels.length > 0 ? allChannels.map((ch) => ch.name) : void 0;
|
|
215
|
-
const builtinTools = [{
|
|
216
|
-
name: "fnl_status",
|
|
217
|
-
description: "Return the current funnel gateway status as JSON — gateway running state, listener alive/dead per channel, and connected Claude WS clients. Call this when you need to check whether the gateway is up or why events stopped arriving.",
|
|
218
|
-
inputSchema: {
|
|
219
|
-
type: "object",
|
|
220
|
-
properties: {}
|
|
221
|
-
}
|
|
222
|
-
}, {
|
|
223
|
-
name: "fnl_debug",
|
|
224
|
-
description: "Return a full channel diagnosis as JSON — gateway health, listener state, Claude WS connection, last 10 inbound events with outcome, connectionErrors (when listener is dead), and diagnosis.rootCause. Call this first when debugging missing events. Omit `channel` to diagnose all channels at once.",
|
|
225
|
-
inputSchema: {
|
|
226
|
-
type: "object",
|
|
227
|
-
properties: { channel: channelEnum ? {
|
|
228
|
-
type: "string",
|
|
229
|
-
description: `Channel name to inspect. One of: ${channelEnum.join(", ")}. Omit to get all channels.`,
|
|
230
|
-
enum: channelEnum
|
|
231
|
-
} : {
|
|
232
|
-
type: "string",
|
|
233
|
-
description: "Channel name to inspect. Omit to get all channels."
|
|
234
|
-
} }
|
|
235
|
-
}
|
|
236
|
-
}];
|
|
237
|
-
return { tools: [...connectorTools, ...builtinTools] };
|
|
537
|
+
instructions: buildChannelServerInstructions(allChannels, currentChannelName)
|
|
238
538
|
});
|
|
539
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [...buildConnectorTools(channel?.connectors ?? []), ...buildBuiltinTools(allChannels)] }));
|
|
239
540
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
240
541
|
const toolName = request.params.name;
|
|
241
|
-
if (isBuiltinTool(toolName)) return handleBuiltinTool(
|
|
542
|
+
if (isBuiltinTool(toolName)) return handleBuiltinTool({
|
|
543
|
+
name: toolName,
|
|
544
|
+
args: request.params.arguments,
|
|
545
|
+
gatewayBaseUrl,
|
|
546
|
+
token,
|
|
547
|
+
allChannels
|
|
548
|
+
});
|
|
242
549
|
if (!channel) throw new Error("FUNNEL_CHANNEL_ID is not set or channel not found in settings.json");
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const headers = { "content-type": "application/json" };
|
|
250
|
-
if (token) headers.authorization = `Bearer ${token}`;
|
|
251
|
-
const res = await fetch(url, {
|
|
252
|
-
method: "POST",
|
|
253
|
-
headers,
|
|
254
|
-
body: JSON.stringify({
|
|
255
|
-
method,
|
|
256
|
-
path,
|
|
257
|
-
body
|
|
258
|
-
})
|
|
550
|
+
return await dispatchConnectorTool({
|
|
551
|
+
channelName: channel.channelName,
|
|
552
|
+
toolName,
|
|
553
|
+
args: request.params.arguments ?? {},
|
|
554
|
+
gatewayBaseUrl,
|
|
555
|
+
token
|
|
259
556
|
});
|
|
260
|
-
const text = await res.text();
|
|
261
|
-
if (!res.ok) throw new Error(`gateway call failed (${res.status}): ${text}`);
|
|
262
|
-
return { content: [{
|
|
263
|
-
type: "text",
|
|
264
|
-
text
|
|
265
|
-
}] };
|
|
266
557
|
});
|
|
267
558
|
const transport = new StdioServerTransport();
|
|
268
559
|
await server.connect(transport);
|
|
560
|
+
process.stderr.write(`funnel MCP ready (channel=${currentChannelName ?? "?"}); if events stop, call fnl_doctor.\n`);
|
|
269
561
|
if (!channelId) return;
|
|
270
562
|
new FunnelChannelSubscriber({
|
|
271
563
|
server,
|
|
@@ -273,50 +565,45 @@ const startChannelServer = async (options = {}) => {
|
|
|
273
565
|
protocols: token ? [`funnel.token.${token}`] : void 0
|
|
274
566
|
}).start();
|
|
275
567
|
};
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
568
|
+
const dispatchConnectorTool = async (input) => {
|
|
569
|
+
const method = typeof input.args.method === "string" ? input.args.method : "";
|
|
570
|
+
const path = typeof input.args.path === "string" ? input.args.path : "";
|
|
571
|
+
const body = input.args.body;
|
|
572
|
+
if (!method || !path) throw new Error("`method` and `path` are required");
|
|
573
|
+
const url = `${input.gatewayBaseUrl}/channels/${encodeURIComponent(input.channelName)}/connectors/${encodeURIComponent(input.toolName)}/call`;
|
|
574
|
+
const headers = { "content-type": "application/json" };
|
|
575
|
+
if (input.token) headers.authorization = `Bearer ${input.token}`;
|
|
576
|
+
const res = await fetch(url, {
|
|
577
|
+
method: "POST",
|
|
578
|
+
headers,
|
|
579
|
+
body: JSON.stringify({
|
|
580
|
+
method,
|
|
581
|
+
path,
|
|
582
|
+
body
|
|
583
|
+
})
|
|
584
|
+
});
|
|
585
|
+
const text = await res.text();
|
|
586
|
+
if (!res.ok) return {
|
|
587
|
+
content: [{
|
|
282
588
|
type: "text",
|
|
283
|
-
text:
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
hint: "run: fnl gateway start",
|
|
287
|
-
knownChannels: allChannels.map((ch) => ch.name)
|
|
589
|
+
text: renderYaml({
|
|
590
|
+
error: `gateway call failed (${res.status}): ${text}`,
|
|
591
|
+
nextAction: "Call fnl_doctor to diagnose the failure"
|
|
288
592
|
})
|
|
593
|
+
}],
|
|
594
|
+
isError: true
|
|
595
|
+
};
|
|
596
|
+
try {
|
|
597
|
+
return { content: [{
|
|
598
|
+
type: "text",
|
|
599
|
+
text: renderYaml(JSON.parse(text))
|
|
289
600
|
}] };
|
|
290
|
-
|
|
601
|
+
} catch {
|
|
291
602
|
return { content: [{
|
|
292
603
|
type: "text",
|
|
293
|
-
text
|
|
604
|
+
text
|
|
294
605
|
}] };
|
|
295
606
|
}
|
|
296
|
-
const channelArg = typeof args?.channel === "string" ? args.channel : null;
|
|
297
|
-
const url = channelArg ? `${gatewayBaseUrl}/debug?channel=${encodeURIComponent(channelArg)}` : `${gatewayBaseUrl}/debug`;
|
|
298
|
-
const res = await fetch(url, { headers }).catch(() => null);
|
|
299
|
-
if (!res) return { content: [{
|
|
300
|
-
type: "text",
|
|
301
|
-
text: JSON.stringify({
|
|
302
|
-
gateway: { running: false },
|
|
303
|
-
channels: allChannels.map((ch) => ({
|
|
304
|
-
id: ch.id,
|
|
305
|
-
name: ch.name,
|
|
306
|
-
diagnosis: {
|
|
307
|
-
status: "error",
|
|
308
|
-
message: "gateway is not running",
|
|
309
|
-
nextAction: "fnl gateway start",
|
|
310
|
-
rootCause: null
|
|
311
|
-
}
|
|
312
|
-
}))
|
|
313
|
-
})
|
|
314
|
-
}] };
|
|
315
|
-
const body = await res.json();
|
|
316
|
-
return { content: [{
|
|
317
|
-
type: "text",
|
|
318
|
-
text: JSON.stringify(body)
|
|
319
|
-
}] };
|
|
320
607
|
};
|
|
321
608
|
//#endregion
|
|
322
609
|
export { FUNNEL_MCP_ARGS, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, FileProcessGuard, FunnelClaude, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLocalConfigWriter, FunnelMcp, FunnelProfiles, FunnelTokenPrompter, LOCAL_CONFIG_FILENAME, MemoryFunnelTokenPrompter, NodeFunnelTokenPrompter, channelSpecSchema, connectorSpecSchema, funnelJsonSchema, localConfigSchema, profileSpecSchema, startChannelServer };
|