@interactive-inc/claude-funnel 0.41.0 → 0.50.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 (55) hide show
  1. package/README.md +34 -9
  2. package/dist/bin.js +255 -256
  3. package/dist/claude-CB1WkV77.d.ts +115 -0
  4. package/dist/claude.d.ts +59 -0
  5. package/dist/claude.js +322 -0
  6. package/dist/{connector-diagnostic-log-OPpPi9V9.d.ts → connector-diagnostic-log-yTOojKUR.d.ts} +14 -14
  7. package/dist/{logger-Czli2OKh.js → connector-listener-DU54DN-f.js} +1 -9
  8. package/dist/connectors/discord.d.ts +3 -3
  9. package/dist/connectors/discord.js +2 -1
  10. package/dist/connectors/gh.d.ts +4 -3
  11. package/dist/connectors/gh.js +2 -1
  12. package/dist/connectors/schedule.d.ts +1 -1
  13. package/dist/connectors/schedule.js +2 -1
  14. package/dist/connectors/slack.d.ts +2 -2
  15. package/dist/connectors/slack.js +2 -1
  16. package/dist/discord-connector-schema-CBDyGdOI.js +21 -0
  17. package/dist/{discord-connector-schema-BeThExJp.js → discord-listener-_jSE3HsQ.js} +2 -22
  18. package/dist/file-system-BeOKXjlV.d.ts +26 -0
  19. package/dist/file-system-PWKKU7lA.js +9 -0
  20. package/dist/gateway/daemon.js +151 -152
  21. package/dist/gateway.d.ts +3 -0
  22. package/dist/gateway.js +2 -0
  23. package/dist/gh-connector-schema-eoTtHbY6.d.ts +14 -0
  24. package/dist/{gh-connector-schema-eYE4g77K.js → gh-connector-schema-o3Q1-ojL.js} +1 -176
  25. package/dist/gh-listener-DH-fClQm.js +178 -0
  26. package/dist/index-ChomoTZ5.d.ts +3404 -0
  27. package/dist/index.d.ts +11 -4214
  28. package/dist/index.js +195 -3869
  29. package/dist/local-config-json-schema-8IHjS4Q7.js +439 -0
  30. package/dist/local-config-sync-BdsrDZOu.d.ts +381 -0
  31. package/dist/local-config.d.ts +3 -0
  32. package/dist/local-config.js +3 -0
  33. package/dist/logger-BP6SisKt.js +9 -0
  34. package/dist/mcp-Dr-nIBwN.js +253 -0
  35. package/dist/memory-connector-diagnostic-log-CrW1ltLM.js +2245 -0
  36. package/dist/memory-token-prompter-B5FFCsGP.d.ts +57 -0
  37. package/dist/memory-token-prompter-CLerGsgM.js +61 -0
  38. package/dist/node-file-system-BcrmWN9I.js +48 -0
  39. package/dist/{gh-connector-schema-CQmEWzdV.d.ts → process-runner-DfniuWVU.d.ts} +1 -14
  40. package/dist/profiles-f0mNmEyP.d.ts +64 -0
  41. package/dist/profiles-wMRnjSid.js +129 -0
  42. package/dist/profiles.d.ts +2 -0
  43. package/dist/profiles.js +2 -0
  44. package/dist/schedule-connector-schema-iCI61gzU.js +31 -0
  45. package/dist/{schedule-listener-3M6WkH1Y.d.ts → schedule-listener-CUyUFFR1.d.ts} +22 -46
  46. package/dist/{schedule-connector-schema-CM-sRkac.js → schedule-listener-ePAjians.js} +3 -86
  47. package/dist/settings-reader-BSU6JyvM.d.ts +167 -0
  48. package/dist/settings-reader-DPqrpV7s.js +11 -0
  49. package/dist/settings-store-D2XSXTyt.js +186 -0
  50. package/dist/slack-connector-schema-BCNWluHM.js +32 -0
  51. package/dist/{slack-listener-9UdAn_ui.d.ts → slack-listener-Bv5xI9gC.d.ts} +31 -31
  52. package/dist/{slack-connector-schema-DDbSGPZn.js → slack-listener-ClQuHhEF.js} +2 -32
  53. package/package.json +16 -1
  54. /package/dist/{connector-adapter-VA6undzc.d.ts → connector-adapter-DKgsVuMH.d.ts} +0 -0
  55. /package/dist/{discord-connector-schema-DF4pL3Sc.d.ts → discord-connector-schema-R0Uu-3ns.d.ts} +0 -0
@@ -0,0 +1,115 @@
1
+ import { n as FunnelIdGenerator, r as ChannelConfig } from "./settings-reader-BSU6JyvM.js";
2
+ import { S as FunnelLogger } from "./connector-diagnostic-log-yTOojKUR.js";
3
+ import { r as FunnelProcessRunner } from "./process-runner-DfniuWVU.js";
4
+
5
+ //#region lib/engine/claude/channel-resolver.d.ts
6
+ type ChannelResolver = {
7
+ get(name: string): ChannelConfig | null;
8
+ getById(id: string): ChannelConfig | null;
9
+ };
10
+ //#endregion
11
+ //#region lib/engine/claude/gateway-controller.d.ts
12
+ type GatewayController = {
13
+ isRunning(): boolean;
14
+ start(options?: {
15
+ caffeinate?: boolean;
16
+ }): Promise<boolean>;
17
+ };
18
+ //#endregion
19
+ //#region lib/engine/claude/mcp-installer.d.ts
20
+ type McpInstaller = {
21
+ findInstalledName(cwd: string): string | null;
22
+ install(cwd: string): void;
23
+ };
24
+ //#endregion
25
+ //#region lib/engine/claude/process-guard.d.ts
26
+ type ProcessGuard = {
27
+ /** Returns true if a live process is already registered for this profile. */isRunning(profileId: string): boolean; /** Writes the PID file and registers an exit hook to clean it up. */
28
+ acquire(profileId: string): void; /** Removes the PID file. */
29
+ release(profileId: string): void;
30
+ };
31
+ //#endregion
32
+ //#region lib/engine/claude/session-store.d.ts
33
+ type SessionStore = {
34
+ getSessionId(profileId: string): string | null;
35
+ setSessionId(profileId: string, sessionId: string): void; /** Returns true when the session jsonl exists on disk and is non-empty. */
36
+ sessionFileExists(cwd: string, sessionId: string, env: Record<string, string>): boolean;
37
+ };
38
+ //#endregion
39
+ //#region lib/engine/claude/claude.d.ts
40
+ type LaunchOptions = {
41
+ channel: string;
42
+ cwd?: string;
43
+ userArgs?: string[];
44
+ /** Stable id of the launching profile (uuid). Keys the singleton PID file and
45
+ * the resumable session. Absent for a profile-less launch (raw `--channel`),
46
+ * which never enforces singleton-ness and never resumes. */
47
+ profileId?: string; /** Args prepended to the claude argv (typically a profile's recipe). Defaults to none. */
48
+ options?: string[]; /** Env vars layered under the launched claude process. process.env wins on collision. */
49
+ env?: Record<string, string>;
50
+ /** Whether to inject a `--session-id`/`--resume` for this profile.
51
+ * Defaults to false: resuming is opt-in and only meaningful for a profile,
52
+ * since the persisted session is owned by the profile (by id). A launch
53
+ * without a profile always starts a fresh session regardless of this flag. */
54
+ resume?: boolean;
55
+ /** Invoked synchronously after the child claude process has been spawned, with its PID.
56
+ * Useful for hosts that need to register the spawned process before it exits
57
+ * (e.g. multi-session registries that track per-claude liveness). */
58
+ onSpawned?: (pid: number) => void;
59
+ /** Whether to install the funnel MCP entry into `.mcp.json` (default: true).
60
+ * Set to false when the host already provides its own MCP server entry and
61
+ * does not need the funnel binary as an MCP endpoint. */
62
+ installMcp?: boolean;
63
+ };
64
+ type Deps = {
65
+ channels: ChannelResolver;
66
+ mcp: McpInstaller;
67
+ gateway: GatewayController;
68
+ sessions: SessionStore;
69
+ guard: ProcessGuard;
70
+ process?: FunnelProcessRunner;
71
+ idGenerator?: FunnelIdGenerator;
72
+ logger?: FunnelLogger;
73
+ };
74
+ /**
75
+ * Launches Claude Code with funnel pre-wired: ensures the gateway is running,
76
+ * installs the funnel MCP into the target repo's `.mcp.json` if missing,
77
+ * injects `FUNNEL_CHANNEL_ID` into the child env, and delegates singleton
78
+ * enforcement to a ProcessGuard.
79
+ */
80
+ declare class FunnelClaude {
81
+ private readonly channels;
82
+ private readonly mcp;
83
+ private readonly gateway;
84
+ private readonly sessions;
85
+ private readonly guard;
86
+ private readonly process;
87
+ private readonly idGenerator;
88
+ private readonly logger;
89
+ constructor(deps: Deps);
90
+ launch(options: LaunchOptions): Promise<number>;
91
+ private buildArgs;
92
+ /**
93
+ * Decides whether funnel should resume an existing claude session or start
94
+ * a freshly minted one. Backs off when the user already passed a
95
+ * session-shaping flag, since combining them would either confuse claude
96
+ * or override the explicit user intent.
97
+ *
98
+ * The session is owned by the profile (by id), not by cwd: two profiles
99
+ * pointing at the same repo each keep their own conversation, and a launch
100
+ * with no profile never resumes — so an unrelated session in the same repo
101
+ * can't bleed in. The channel never enters into it; sessions belong to the
102
+ * launch layer (profiles), keeping the transport layer ignorant of them.
103
+ *
104
+ * A persisted id is only resumed when its session jsonl still exists on
105
+ * disk. claude errors out on `--resume <id>` for a missing conversation, and
106
+ * a persisted id can outlive its jsonl (claude pruned it, or the very first
107
+ * launch was aborted after the id was written but before the jsonl
108
+ * appeared). When the file is gone we mint a fresh session instead, which
109
+ * overwrites the dangling entry — so the store self-heals.
110
+ */
111
+ private resolveSession;
112
+ private buildEnv;
113
+ }
114
+ //#endregion
115
+ export { McpInstaller as a, ProcessGuard as i, LaunchOptions as n, GatewayController as o, SessionStore as r, ChannelResolver as s, FunnelClaude as t };
@@ -0,0 +1,59 @@
1
+ import { a as McpInstaller, i as ProcessGuard, n as LaunchOptions, o as GatewayController, r as SessionStore, s as ChannelResolver, t as FunnelClaude } from "./claude-CB1WkV77.js";
2
+ import { r as FunnelProcessRunner } from "./process-runner-DfniuWVU.js";
3
+ import { n as FunnelFileSystem } from "./file-system-BeOKXjlV.js";
4
+ import { t as FunnelProfiles } from "./profiles-f0mNmEyP.js";
5
+ import { C as profileSpecSchema, S as localConfigSchema, _ as LOCAL_CONFIG_FILENAME, b as channelSpecSchema, g as ConnectorSpec, h as ChannelSpec, i as FunnelTokenPrompter, m as FunnelLocalConfig, n as FunnelLocalConfigSync, r as LocalConfigSyncResult, t as ConnectorSyncOutcome, v as LocalConfig, x as connectorSpecSchema, y as ProfileSpec } from "./local-config-sync-BdsrDZOu.js";
6
+ import { i as funnelJsonSchema, n as NodeFunnelTokenPrompter, r as FunnelLocalConfigWriter, t as MemoryFunnelTokenPrompter } from "./memory-token-prompter-B5FFCsGP.js";
7
+
8
+ //#region lib/engine/claude/file-process-guard.d.ts
9
+ type Deps$1 = {
10
+ fs?: FunnelFileSystem;
11
+ process?: FunnelProcessRunner;
12
+ dir?: string;
13
+ };
14
+ declare class FileProcessGuard implements ProcessGuard {
15
+ private readonly fs;
16
+ private readonly process;
17
+ private readonly pidDir;
18
+ constructor(deps?: Deps$1);
19
+ isRunning(profileId: string): boolean;
20
+ acquire(profileId: string): void;
21
+ release(profileId: string): void;
22
+ private pidPath;
23
+ private readPid;
24
+ }
25
+ //#endregion
26
+ //#region lib/engine/mcp/mcp.d.ts
27
+ declare const FUNNEL_MCP_COMMAND = "bun";
28
+ declare const FUNNEL_MCP_ARGS: string[];
29
+ declare const FUNNEL_MCP_NAME = "funnel";
30
+ type Deps = {
31
+ fs?: FunnelFileSystem;
32
+ };
33
+ /**
34
+ * Installs/uninstalls the funnel MCP entry into a target repository's
35
+ * `.mcp.json`. Detects an existing entry by command match so renaming is
36
+ * preserved across re-installs.
37
+ */
38
+ declare class FunnelMcp {
39
+ private readonly fs;
40
+ constructor(deps?: Deps);
41
+ install(repoPath: string): void;
42
+ uninstall(repoPath: string): void;
43
+ findInstalledName(cwd: string): string | null;
44
+ private findServerName;
45
+ private isFunnelEntry;
46
+ private readConfig;
47
+ private writeConfig;
48
+ }
49
+ //#endregion
50
+ //#region lib/engine/mcp/channel-server.d.ts
51
+ type ChannelServerOptions = {
52
+ /** Funnel home directory (settings.json + gateway.token). Defaults to ~/.funnel. */dir?: string; /** Gateway base URL. Defaults to `$FUNNEL_GATEWAY_URL` or `http://127.0.0.1:<port>`. */
53
+ gatewayUrl?: string; /** Channel id to subscribe to. Defaults to `$FUNNEL_CHANNEL_ID`. */
54
+ channelId?: string; /** Auth token. Defaults to `$FUNNEL_GATEWAY_TOKEN` then `<dir>/gateway.token`. */
55
+ token?: string;
56
+ };
57
+ declare const startChannelServer: (options?: ChannelServerOptions) => Promise<void>;
58
+ //#endregion
59
+ export { ChannelResolver, ChannelServerOptions, ChannelSpec, ConnectorSpec, ConnectorSyncOutcome, FUNNEL_MCP_ARGS, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, FileProcessGuard, FunnelClaude, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLocalConfigWriter, FunnelMcp, FunnelProfiles, FunnelTokenPrompter, GatewayController, LOCAL_CONFIG_FILENAME, LaunchOptions, LocalConfig, LocalConfigSyncResult, McpInstaller, MemoryFunnelTokenPrompter, NodeFunnelTokenPrompter, ProcessGuard, ProfileSpec, SessionStore, channelSpecSchema, connectorSpecSchema, funnelJsonSchema, localConfigSchema, profileSpecSchema, startChannelServer };
package/dist/claude.js ADDED
@@ -0,0 +1,322 @@
1
+ import { d as settingsSchema, o as resolveFunnelPort } from "./settings-store-D2XSXTyt.js";
2
+ import { a as FileProcessGuard, i as FunnelMcp, n as FUNNEL_MCP_COMMAND, o as FunnelClaude, r as FUNNEL_MCP_NAME, t as FUNNEL_MCP_ARGS } from "./mcp-Dr-nIBwN.js";
3
+ 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-8IHjS4Q7.js";
4
+ import { t as FunnelProfiles } from "./profiles-wMRnjSid.js";
5
+ import { n as FunnelLocalConfigWriter, t as MemoryFunnelTokenPrompter } from "./memory-token-prompter-CLerGsgM.js";
6
+ import { join } from "node:path";
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
10
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
12
+ //#region lib/engine/mcp/channel-subscriber.ts
13
+ const RECONNECT_DELAY = 1e3;
14
+ const MAX_RECONNECT_DELAY = 1e4;
15
+ /**
16
+ * Subscribes to the gateway WebSocket for a single channel and forwards
17
+ * incoming events to the MCP server as `notifications/claude/channel`.
18
+ * Reconnects with exponential backoff and replays missed events via `?since=<offset>`.
19
+ */
20
+ var FunnelChannelSubscriber = class {
21
+ state = {
22
+ reconnectDelay: RECONNECT_DELAY,
23
+ lastOffset: 0
24
+ };
25
+ constructor(props) {
26
+ this.props = props;
27
+ Object.freeze(this);
28
+ }
29
+ start() {
30
+ this.connect();
31
+ }
32
+ connect() {
33
+ const sinceQuery = this.state.lastOffset > 0 ? `&since=${this.state.lastOffset}` : "";
34
+ const wsUrl = `${this.props.baseUrl}${sinceQuery}`;
35
+ const ws = new WebSocket(wsUrl, this.props.protocols);
36
+ ws.addEventListener("open", () => {
37
+ this.state.reconnectDelay = RECONNECT_DELAY;
38
+ process.stderr.write(`funnel: connected (${wsUrl})\n`);
39
+ });
40
+ ws.addEventListener("message", (event) => this.handleMessage(event));
41
+ ws.addEventListener("close", () => {
42
+ process.stderr.write(`funnel: disconnected, reconnecting in ${this.state.reconnectDelay}ms\n`);
43
+ setTimeout(() => this.connect(), this.state.reconnectDelay);
44
+ this.state.reconnectDelay = Math.min(this.state.reconnectDelay * 2, MAX_RECONNECT_DELAY);
45
+ });
46
+ ws.addEventListener("error", () => {});
47
+ }
48
+ async handleMessage(event) {
49
+ try {
50
+ const payload = JSON.parse(String(event.data));
51
+ const eventType = payload.meta?.event_type ?? "unknown";
52
+ if (typeof payload.offset === "number" && payload.offset > this.state.lastOffset) this.state.lastOffset = payload.offset;
53
+ process.stderr.write(`funnel: received event (${eventType})\n`);
54
+ await this.props.server.notification({
55
+ method: "notifications/claude/channel",
56
+ params: {
57
+ content: payload.content,
58
+ meta: payload.meta
59
+ }
60
+ });
61
+ } catch (error) {
62
+ process.stderr.write(`funnel: error: ${error instanceof Error ? error.message : String(error)}\n`);
63
+ }
64
+ }
65
+ };
66
+ //#endregion
67
+ //#region lib/engine/mcp/read-channel-connectors.ts
68
+ const TOOL_CONNECTOR_TYPES = new Set([
69
+ "slack",
70
+ "gh",
71
+ "discord"
72
+ ]);
73
+ const readChannelConnectors = (dir, channelId) => {
74
+ const settingsPath = join(dir, "settings.json");
75
+ if (!existsSync(settingsPath)) return null;
76
+ const raw = JSON.parse(readFileSync(settingsPath, "utf-8"));
77
+ const parsed = settingsSchema.safeParse(raw);
78
+ if (!parsed.success) return null;
79
+ const channel = parsed.data.channels.find((c) => c.id === channelId);
80
+ if (!channel) return null;
81
+ const connectors = channel.connectors.filter((c) => TOOL_CONNECTOR_TYPES.has(c.type)).map((c) => ({
82
+ name: c.name,
83
+ type: c.type
84
+ }));
85
+ return {
86
+ channelName: channel.name,
87
+ connectors
88
+ };
89
+ };
90
+ //#endregion
91
+ //#region lib/engine/mcp/read-gateway-token.ts
92
+ const readGatewayToken = (dir) => {
93
+ const fromEnv = process.env.FUNNEL_GATEWAY_TOKEN;
94
+ if (fromEnv && fromEnv.length > 0) return fromEnv;
95
+ const path = join(dir, "gateway.token");
96
+ if (!existsSync(path)) return null;
97
+ const value = readFileSync(path, "utf-8").trim();
98
+ return value.length > 0 ? value : null;
99
+ };
100
+ //#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
+ //#region lib/engine/mcp/channel-server.ts
123
+ 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
+ const readAllChannels = (dir) => {
127
+ const settingsPath = join(dir, "settings.json");
128
+ if (!existsSync(settingsPath)) return [];
129
+ try {
130
+ const raw = JSON.parse(readFileSync(settingsPath, "utf-8"));
131
+ const parsed = settingsSchema.safeParse(raw);
132
+ if (!parsed.success) return [];
133
+ return parsed.data.channels.map((c) => ({
134
+ id: c.id,
135
+ name: c.name
136
+ }));
137
+ } catch {
138
+ return [];
139
+ }
140
+ };
141
+ const startChannelServer = async (options = {}) => {
142
+ const dir = options.dir ?? DEFAULT_FUNNEL_DIR;
143
+ const gatewayBaseUrl = options.gatewayUrl ?? process.env.FUNNEL_GATEWAY_URL ?? `http://127.0.0.1:${resolveFunnelPort()}`;
144
+ const gatewayWsUrl = `${gatewayBaseUrl.replace(/^http/, "ws")}/ws`;
145
+ const channelId = options.channelId ?? process.env.FUNNEL_CHANNEL_ID;
146
+ const channel = channelId ? readChannelConnectors(dir, channelId) : null;
147
+ const token = options.token ?? readGatewayToken(dir);
148
+ const allChannels = readAllChannels(dir);
149
+ 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
+ const server = new Server({
156
+ name: FUNNEL_MCP_NAME,
157
+ version: "1.0.0"
158
+ }, {
159
+ capabilities: {
160
+ experimental: { "claude/channel": {} },
161
+ tools: {}
162
+ },
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] };
238
+ });
239
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
240
+ const toolName = request.params.name;
241
+ if (isBuiltinTool(toolName)) return handleBuiltinTool(toolName, request.params.arguments, gatewayBaseUrl, token, allChannels);
242
+ if (!channel) throw new Error("FUNNEL_CHANNEL_ID is not set or channel not found in settings.json");
243
+ const args = request.params.arguments ?? {};
244
+ const method = typeof args.method === "string" ? args.method : "";
245
+ const path = typeof args.path === "string" ? args.path : "";
246
+ const body = args.body ?? {};
247
+ if (!method || !path) throw new Error("`method` and `path` are required");
248
+ const url = `${gatewayBaseUrl}/channels/${encodeURIComponent(channel.channelName)}/connectors/${encodeURIComponent(toolName)}/call`;
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
+ })
259
+ });
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
+ });
267
+ const transport = new StdioServerTransport();
268
+ await server.connect(transport);
269
+ if (!channelId) return;
270
+ new FunnelChannelSubscriber({
271
+ server,
272
+ baseUrl: `${gatewayWsUrl}?channel=${encodeURIComponent(channelId)}`,
273
+ protocols: token ? [`funnel.token.${token}`] : void 0
274
+ }).start();
275
+ };
276
+ const handleBuiltinTool = async (name, args, gatewayBaseUrl, token, allChannels) => {
277
+ const headers = {};
278
+ if (token) headers.authorization = `Bearer ${token}`;
279
+ if (name === "fnl_status") {
280
+ const res = await fetch(`${gatewayBaseUrl}/status`, { headers }).catch(() => null);
281
+ if (!res) return { content: [{
282
+ type: "text",
283
+ text: JSON.stringify({
284
+ running: false,
285
+ error: "gateway unreachable",
286
+ hint: "run: fnl gateway start",
287
+ knownChannels: allChannels.map((ch) => ch.name)
288
+ })
289
+ }] };
290
+ const body = await res.json();
291
+ return { content: [{
292
+ type: "text",
293
+ text: JSON.stringify(body)
294
+ }] };
295
+ }
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
+ };
321
+ //#endregion
322
+ 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 };
@@ -1,5 +1,18 @@
1
1
  import { z } from "zod";
2
2
 
3
+ //#region lib/engine/logger/logger.d.ts
4
+ /**
5
+ * Structured logger with three levels and an optional log-file path.
6
+ * Defaults to NodeFunnelLogger (appends to `<os.tmpdir()>/funnel/funnel.log`);
7
+ * MemoryFunnelLogger captures entries in memory and NoopFunnelLogger silences output.
8
+ */
9
+ declare abstract class FunnelLogger {
10
+ abstract info(message: string, meta?: Record<string, unknown>): void;
11
+ abstract warn(message: string, meta?: Record<string, unknown>): void;
12
+ abstract error(message: string, meta?: Record<string, unknown>): void;
13
+ abstract readonly file: string | null;
14
+ }
15
+ //#endregion
3
16
  //#region lib/connectors/connector-listener.d.ts
4
17
  type NotifyFn = (content: string, meta?: Record<string, string>) => Promise<void>;
5
18
  /**
@@ -19,19 +32,6 @@ declare abstract class FunnelConnectorListener {
19
32
  isAlive(): boolean;
20
33
  }
21
34
  //#endregion
22
- //#region lib/engine/logger/logger.d.ts
23
- /**
24
- * Structured logger with three levels and an optional log-file path.
25
- * Defaults to NodeFunnelLogger (appends to `<os.tmpdir()>/funnel/funnel.log`);
26
- * MemoryFunnelLogger captures entries in memory and NoopFunnelLogger silences output.
27
- */
28
- declare abstract class FunnelLogger {
29
- abstract info(message: string, meta?: Record<string, unknown>): void;
30
- abstract warn(message: string, meta?: Record<string, unknown>): void;
31
- abstract error(message: string, meta?: Record<string, unknown>): void;
32
- abstract readonly file: string | null;
33
- }
34
- //#endregion
35
35
  //#region lib/gateway/connector-diagnostic-log.d.ts
36
36
  /**
37
37
  * Points in the listener's connection lifecycle. The single source of truth
@@ -205,4 +205,4 @@ declare abstract class ConnectorDiagnosticLog {
205
205
  abstract close(): void;
206
206
  }
207
207
  //#endregion
208
- export { NotifyFn as S, connectorConnectionEventSchema as _, ConnectorConnectionStatus as a, FunnelLogger as b, ConnectorProcessedQuery as c, ConnectorRawEvent as d, ConnectorRawQuery as f, StoredRawEvent as g, StoredProcessedEvent as h, ConnectorConnectionRecord as i, ConnectorProcessedRecord as l, StoredConnectionEvent as m, ConnectorConnectionEvent as n, ConnectorDiagnosticLog as o, ConnectorRawRecord as p, ConnectorConnectionQuery as r, ConnectorProcessedEvent as s, CONNECTOR_CONNECTION_STATUSES as t, ConnectorQuery as u, connectorProcessedEventSchema as v, FunnelConnectorListener as x, connectorRawEventSchema as y };
208
+ export { FunnelLogger as S, connectorConnectionEventSchema as _, ConnectorConnectionStatus as a, FunnelConnectorListener as b, ConnectorProcessedQuery as c, ConnectorRawEvent as d, ConnectorRawQuery as f, StoredRawEvent as g, StoredProcessedEvent as h, ConnectorConnectionRecord as i, ConnectorProcessedRecord as l, StoredConnectionEvent as m, ConnectorConnectionEvent as n, ConnectorDiagnosticLog as o, ConnectorRawRecord as p, ConnectorConnectionQuery as r, ConnectorProcessedEvent as s, CONNECTOR_CONNECTION_STATUSES as t, ConnectorQuery as u, connectorProcessedEventSchema as v, NotifyFn as x, connectorRawEventSchema as y };
@@ -16,12 +16,4 @@ var FunnelConnectorListener = class {
16
16
  }
17
17
  };
18
18
  //#endregion
19
- //#region lib/engine/logger/logger.ts
20
- /**
21
- * Structured logger with three levels and an optional log-file path.
22
- * Defaults to NodeFunnelLogger (appends to `<os.tmpdir()>/funnel/funnel.log`);
23
- * MemoryFunnelLogger captures entries in memory and NoopFunnelLogger silences output.
24
- */
25
- var FunnelLogger = class {};
26
- //#endregion
27
- export { FunnelConnectorListener as n, FunnelLogger as t };
19
+ export { FunnelConnectorListener as t };
@@ -1,6 +1,6 @@
1
- import { n as FunnelConnectorAdapter, t as CallInput } from "../connector-adapter-VA6undzc.js";
2
- import { n as discordConnectorSchema, t as DiscordConnectorConfig } from "../discord-connector-schema-DF4pL3Sc.js";
3
- import { S as NotifyFn, b as FunnelLogger, o as ConnectorDiagnosticLog, x as FunnelConnectorListener } from "../connector-diagnostic-log-OPpPi9V9.js";
1
+ import { S as FunnelLogger, b as FunnelConnectorListener, o as ConnectorDiagnosticLog, x as NotifyFn } from "../connector-diagnostic-log-yTOojKUR.js";
2
+ import { n as FunnelConnectorAdapter, t as CallInput } from "../connector-adapter-DKgsVuMH.js";
3
+ import { n as discordConnectorSchema, t as DiscordConnectorConfig } from "../discord-connector-schema-R0Uu-3ns.js";
4
4
 
5
5
  //#region lib/engine/http/http-client.d.ts
6
6
  type HttpRequest = {
@@ -1,2 +1,3 @@
1
- import { i as FunnelDiscordAdapter, n as FunnelDiscordListener, r as FunnelDiscordEventProcessor, t as discordConnectorSchema } from "../discord-connector-schema-BeThExJp.js";
1
+ import { n as FunnelDiscordEventProcessor, r as FunnelDiscordAdapter, t as FunnelDiscordListener } from "../discord-listener-_jSE3HsQ.js";
2
+ import { t as discordConnectorSchema } from "../discord-connector-schema-CBDyGdOI.js";
2
3
  export { FunnelDiscordAdapter, FunnelDiscordEventProcessor, FunnelDiscordListener, discordConnectorSchema };
@@ -1,6 +1,7 @@
1
- import { n as FunnelConnectorAdapter, t as CallInput } from "../connector-adapter-VA6undzc.js";
2
- import { S as NotifyFn, b as FunnelLogger, o as ConnectorDiagnosticLog, x as FunnelConnectorListener } from "../connector-diagnostic-log-OPpPi9V9.js";
3
- import { a as FunnelProcessRunner, n as ghConnectorSchema, t as GhConnectorConfig } from "../gh-connector-schema-CQmEWzdV.js";
1
+ import { S as FunnelLogger, b as FunnelConnectorListener, o as ConnectorDiagnosticLog, x as NotifyFn } from "../connector-diagnostic-log-yTOojKUR.js";
2
+ import { r as FunnelProcessRunner } from "../process-runner-DfniuWVU.js";
3
+ import { n as FunnelConnectorAdapter, t as CallInput } from "../connector-adapter-DKgsVuMH.js";
4
+ import { n as ghConnectorSchema, t as GhConnectorConfig } from "../gh-connector-schema-eoTtHbY6.js";
4
5
 
5
6
  //#region lib/connectors/gh-adapter.d.ts
6
7
  type Deps$1 = {
@@ -1,2 +1,3 @@
1
- import { n as FunnelGhListener, r as FunnelGhAdapter, t as ghConnectorSchema } from "../gh-connector-schema-eYE4g77K.js";
1
+ import { t as ghConnectorSchema } from "../gh-connector-schema-o3Q1-ojL.js";
2
+ import { n as FunnelGhAdapter, t as FunnelGhListener } from "../gh-listener-DH-fClQm.js";
2
3
  export { FunnelGhAdapter, FunnelGhListener, ghConnectorSchema };
@@ -1,4 +1,4 @@
1
- import { c as ScheduleEntry, d as scheduleEntrySchema, l as scheduleCatchupPolicySchema, n as ScheduleOnFired, o as ScheduleCatchupPolicy, r as ScheduleStateStore, s as ScheduleConnectorConfig, t as FunnelScheduleListener, u as scheduleConnectorSchema } from "../schedule-listener-3M6WkH1Y.js";
1
+ import { a as ScheduleEntry, c as scheduleEntrySchema, i as ScheduleConnectorConfig, l as ScheduleStateStore, n as ScheduleOnFired, o as scheduleCatchupPolicySchema, r as ScheduleCatchupPolicy, s as scheduleConnectorSchema, t as FunnelScheduleListener } from "../schedule-listener-CUyUFFR1.js";
2
2
 
3
3
  //#region lib/connectors/match-cron.d.ts
4
4
  declare const matchCron: (expr: string, date: Date) => boolean;
@@ -1,2 +1,3 @@
1
- import { a as ScheduleStateStore, c as matchCron, i as FunnelScheduleListener, n as scheduleConnectorSchema, r as scheduleEntrySchema, t as scheduleCatchupPolicySchema } from "../schedule-connector-schema-CM-sRkac.js";
1
+ import { n as ScheduleStateStore, r as matchCron, t as FunnelScheduleListener } from "../schedule-listener-ePAjians.js";
2
+ import { n as scheduleConnectorSchema, r as scheduleEntrySchema, t as scheduleCatchupPolicySchema } from "../schedule-connector-schema-iCI61gzU.js";
2
3
  export { FunnelScheduleListener, ScheduleStateStore, matchCron, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema };
@@ -1,5 +1,5 @@
1
- import { n as FunnelConnectorAdapter, t as CallInput } from "../connector-adapter-VA6undzc.js";
2
- import { a as SlackProcessed, c as SlackRawEvent, d as slackConnectorSchema, i as FunnelSlackEventProcessor, l as SlackSkipReason, n as SlackOnAppCreated, o as SlackProcessedEmit, r as SlackPreprocessEvent, s as SlackProcessedSkip, t as FunnelSlackListener, u as SlackConnectorConfig } from "../slack-listener-9UdAn_ui.js";
1
+ import { n as FunnelConnectorAdapter, t as CallInput } from "../connector-adapter-DKgsVuMH.js";
2
+ import { a as slackConnectorSchema, c as SlackProcessedEmit, d as SlackSkipReason, i as SlackConnectorConfig, l as SlackProcessedSkip, n as SlackOnAppCreated, o as FunnelSlackEventProcessor, r as SlackPreprocessEvent, s as SlackProcessed, t as FunnelSlackListener, u as SlackRawEvent } from "../slack-listener-Bv5xI9gC.js";
3
3
 
4
4
  //#region lib/connectors/slack-adapter.d.ts
5
5
  type SlackWebClientLike = {
@@ -1,2 +1,3 @@
1
- import { i as FunnelSlackAdapter, n as FunnelSlackListener, r as FunnelSlackEventProcessor, t as slackConnectorSchema } from "../slack-connector-schema-DDbSGPZn.js";
1
+ import { n as FunnelSlackEventProcessor, r as FunnelSlackAdapter, t as FunnelSlackListener } from "../slack-listener-ClQuHhEF.js";
2
+ import { t as slackConnectorSchema } from "../slack-connector-schema-BCNWluHM.js";
2
3
  export { FunnelSlackAdapter, FunnelSlackEventProcessor, FunnelSlackListener, slackConnectorSchema };
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+ //#region lib/connectors/discord-connector-schema.ts
3
+ /**
4
+ * Like slack, a discord connector holds either a literal `botToken` or a
5
+ * `botTokenEnv` reference resolved from `process.env` at listener start. The
6
+ * reference form keeps the secret out of settings.json, but is only set through
7
+ * the engine API (`new Funnel(...)`); funnel.json and the `fnl` CLI produce
8
+ * literals.
9
+ */
10
+ const discordConnectorSchema = z.object({
11
+ id: z.string(),
12
+ name: z.string(),
13
+ type: z.literal("discord"),
14
+ botToken: z.string().min(10).optional(),
15
+ /** Name of the env var holding the bot token, resolved at listener start. */
16
+ botTokenEnv: z.string().optional(),
17
+ createdAt: z.string().datetime().optional(),
18
+ updatedAt: z.string().datetime().optional()
19
+ });
20
+ //#endregion
21
+ export { discordConnectorSchema as t };