@interactive-inc/claude-funnel 0.10.0 → 0.15.1

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 (236) hide show
  1. package/README.md +106 -56
  2. package/dist/bin.js +557 -530
  3. package/dist/connectors/schedule.d.ts +2 -49
  4. package/dist/connectors/schedule.js +1 -1
  5. package/dist/connectors/slack.d.ts +4 -48
  6. package/dist/connectors/slack.js +1 -1
  7. package/dist/gateway/daemon.js +213 -211
  8. package/dist/index.d.ts +465 -173
  9. package/dist/index.js +692 -154
  10. package/dist/{schedule-connector-schema-CkuIQ0JQ.js → schedule-connector-schema-FxP7LPlx.js} +11 -0
  11. package/dist/{file-system-Co60LrmR.d.ts → schedule-listener-BPodvbld.d.ts} +56 -1
  12. package/dist/{slack-connector-schema-Cd22WiHB.js → slack-connector-schema-B4hsf3AY.js} +10 -1
  13. package/dist/slack-listener-CHj6uMY-.d.ts +74 -0
  14. package/package.json +2 -6
  15. package/schemas/funnel.schema.json +144 -0
  16. package/dist/slack-connector-schema-D7zAHN8k.d.ts +0 -15
  17. package/lib/bin.ts +0 -3
  18. package/lib/cli/factory.ts +0 -10
  19. package/lib/cli/index.ts +0 -85
  20. package/lib/cli/router/query-to-cli-args.ts +0 -20
  21. package/lib/cli/router/to-request.ts +0 -113
  22. package/lib/cli/router/validator.ts +0 -27
  23. package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +0 -27
  24. package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +0 -40
  25. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +0 -41
  26. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +0 -22
  27. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +0 -23
  28. package/lib/cli/routes/channels.$channel.connectors.$connector.ts +0 -26
  29. package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +0 -92
  30. package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +0 -22
  31. package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +0 -63
  32. package/lib/cli/routes/channels.$channel.connectors.ts +0 -26
  33. package/lib/cli/routes/channels.$channel.publish.ts +0 -52
  34. package/lib/cli/routes/channels.$channel.rename.$newName.ts +0 -22
  35. package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +0 -34
  36. package/lib/cli/routes/channels.$channel.ts +0 -34
  37. package/lib/cli/routes/channels.add.$channel.ts +0 -33
  38. package/lib/cli/routes/channels.remove.$channel.ts +0 -20
  39. package/lib/cli/routes/channels.ts +0 -39
  40. package/lib/cli/routes/claude.ts +0 -70
  41. package/lib/cli/routes/gateway.listeners.ts +0 -41
  42. package/lib/cli/routes/gateway.logs.ts +0 -123
  43. package/lib/cli/routes/gateway.restart.ts +0 -50
  44. package/lib/cli/routes/gateway.run.ts +0 -41
  45. package/lib/cli/routes/gateway.start.ts +0 -50
  46. package/lib/cli/routes/gateway.status.ts +0 -19
  47. package/lib/cli/routes/gateway.stop.ts +0 -32
  48. package/lib/cli/routes/gateway.ts +0 -55
  49. package/lib/cli/routes/index.ts +0 -219
  50. package/lib/cli/routes/profiles.$profile.as-default.ts +0 -22
  51. package/lib/cli/routes/profiles.$profile.rename.$newName.ts +0 -22
  52. package/lib/cli/routes/profiles.$profile.run.ts +0 -36
  53. package/lib/cli/routes/profiles.add.$profile.ts +0 -49
  54. package/lib/cli/routes/profiles.remove.$profile.ts +0 -20
  55. package/lib/cli/routes/profiles.set.$profile.ts +0 -45
  56. package/lib/cli/routes/profiles.ts +0 -40
  57. package/lib/cli/routes/status.ts +0 -93
  58. package/lib/cli/routes/update.ts +0 -27
  59. package/lib/connectors/connector-adapter.ts +0 -9
  60. package/lib/connectors/connector-config-schema.ts +0 -16
  61. package/lib/connectors/connector-factory.ts +0 -94
  62. package/lib/connectors/connector-listener.ts +0 -20
  63. package/lib/connectors/discord-adapter.ts +0 -51
  64. package/lib/connectors/discord-connector-schema.ts +0 -12
  65. package/lib/connectors/discord-event-processor.ts +0 -48
  66. package/lib/connectors/discord-listener.ts +0 -111
  67. package/lib/connectors/discord.ts +0 -4
  68. package/lib/connectors/gh-adapter.ts +0 -48
  69. package/lib/connectors/gh-connector-schema.ts +0 -12
  70. package/lib/connectors/gh-listener.ts +0 -137
  71. package/lib/connectors/gh.ts +0 -3
  72. package/lib/connectors/match-cron.ts +0 -78
  73. package/lib/connectors/schedule-connector-schema.ts +0 -33
  74. package/lib/connectors/schedule-listener.ts +0 -207
  75. package/lib/connectors/schedule-state-store.ts +0 -54
  76. package/lib/connectors/schedule.ts +0 -4
  77. package/lib/connectors/slack-adapter.ts +0 -36
  78. package/lib/connectors/slack-connector-schema.ts +0 -13
  79. package/lib/connectors/slack-event-processor.ts +0 -97
  80. package/lib/connectors/slack-listener.ts +0 -97
  81. package/lib/connectors/slack.ts +0 -4
  82. package/lib/engine/channels/channels.ts +0 -520
  83. package/lib/engine/claude/claude.ts +0 -205
  84. package/lib/engine/claude/gateway-controller.ts +0 -4
  85. package/lib/engine/fs/file-system.ts +0 -23
  86. package/lib/engine/fs/memory-file-system.ts +0 -102
  87. package/lib/engine/fs/node-file-system.ts +0 -68
  88. package/lib/engine/http/http-client.ts +0 -17
  89. package/lib/engine/http/memory-http-client.ts +0 -36
  90. package/lib/engine/http/node-http-client.ts +0 -23
  91. package/lib/engine/id/id-generator.ts +0 -7
  92. package/lib/engine/id/memory-id-generator.ts +0 -20
  93. package/lib/engine/id/node-id-generator.ts +0 -7
  94. package/lib/engine/logger/logger.ts +0 -11
  95. package/lib/engine/logger/memory-logger.ts +0 -28
  96. package/lib/engine/logger/node-logger.ts +0 -49
  97. package/lib/engine/logger/noop-logger.ts +0 -9
  98. package/lib/engine/mcp/channel-server.ts +0 -123
  99. package/lib/engine/mcp/channel-subscriber.ts +0 -82
  100. package/lib/engine/mcp/mcp.ts +0 -126
  101. package/lib/engine/mcp/read-channel-connectors.ts +0 -34
  102. package/lib/engine/mcp/read-gateway-token.ts +0 -16
  103. package/lib/engine/mcp/usage-hint-for-type.ts +0 -15
  104. package/lib/engine/process/memory-process-runner.ts +0 -88
  105. package/lib/engine/process/node-process-runner.ts +0 -91
  106. package/lib/engine/process/process-runner.ts +0 -33
  107. package/lib/engine/profiles/profile-channel-checker.ts +0 -7
  108. package/lib/engine/profiles/profiles.ts +0 -126
  109. package/lib/engine/settings/mock-settings-reader.ts +0 -27
  110. package/lib/engine/settings/settings-reader.ts +0 -6
  111. package/lib/engine/settings/settings-schema.ts +0 -48
  112. package/lib/engine/settings/settings-store.ts +0 -110
  113. package/lib/engine/time/clock.ts +0 -15
  114. package/lib/engine/time/memory-clock.ts +0 -26
  115. package/lib/engine/time/node-clock.ts +0 -7
  116. package/lib/funnel.ts +0 -294
  117. package/lib/gateway/auth-middleware.ts +0 -44
  118. package/lib/gateway/broadcaster.ts +0 -319
  119. package/lib/gateway/channel-publisher.ts +0 -67
  120. package/lib/gateway/daemon.ts +0 -47
  121. package/lib/gateway/factory.ts +0 -10
  122. package/lib/gateway/funnel-event-store.ts +0 -155
  123. package/lib/gateway/gateway-server.ts +0 -426
  124. package/lib/gateway/gateway-token.ts +0 -79
  125. package/lib/gateway/gateway.ts +0 -209
  126. package/lib/gateway/kill-competing-slack-gateways.ts +0 -56
  127. package/lib/gateway/listener-supervisor.ts +0 -339
  128. package/lib/gateway/listeners-client.ts +0 -128
  129. package/lib/gateway/publish-schema.ts +0 -27
  130. package/lib/gateway/resolve-daemon-script.ts +0 -26
  131. package/lib/gateway/routes/channels.connectors.call.ts +0 -39
  132. package/lib/gateway/routes/channels.publish.ts +0 -44
  133. package/lib/gateway/routes/health.ts +0 -13
  134. package/lib/gateway/routes/index.ts +0 -26
  135. package/lib/gateway/routes/listeners.list.ts +0 -6
  136. package/lib/gateway/routes/listeners.restart.ts +0 -15
  137. package/lib/gateway/routes/listeners.start.ts +0 -15
  138. package/lib/gateway/routes/listeners.stop.ts +0 -15
  139. package/lib/gateway/routes/route-deps.ts +0 -19
  140. package/lib/gateway/routes/status.ts +0 -15
  141. package/lib/gateway/routes/validator.ts +0 -17
  142. package/lib/index.ts +0 -67
  143. package/lib/logger/leuco-human-file-writer.ts +0 -65
  144. package/lib/logger/leuco-human-logger.ts +0 -98
  145. package/lib/logger/leuco-human-record.ts +0 -16
  146. package/lib/logger/leuco-human-stdout-writer.ts +0 -26
  147. package/lib/logger/leuco-human-writer.ts +0 -14
  148. package/lib/logger/leuco-logger-memory-sink.ts +0 -67
  149. package/lib/logger/leuco-logger-record.ts +0 -13
  150. package/lib/logger/leuco-logger-sink.ts +0 -33
  151. package/lib/logger/leuco-logger-sqlite-sink.ts +0 -355
  152. package/lib/logger/leuco-logger.ts +0 -135
  153. package/lib/tui/app.tsx +0 -357
  154. package/lib/tui/components/add-row.tsx +0 -18
  155. package/lib/tui/components/brand.tsx +0 -27
  156. package/lib/tui/components/card.tsx +0 -44
  157. package/lib/tui/components/detail-bar.tsx +0 -46
  158. package/lib/tui/components/editable-field.tsx +0 -33
  159. package/lib/tui/components/empty-state.tsx +0 -11
  160. package/lib/tui/components/gateway-status.tsx +0 -66
  161. package/lib/tui/components/keymap.tsx +0 -29
  162. package/lib/tui/components/menu-item.tsx +0 -73
  163. package/lib/tui/components/menu.tsx +0 -26
  164. package/lib/tui/components/panel-header.tsx +0 -22
  165. package/lib/tui/components/readonly-field.tsx +0 -18
  166. package/lib/tui/components/section-header.tsx +0 -25
  167. package/lib/tui/components/selection-accent.tsx +0 -32
  168. package/lib/tui/components/session-item.tsx +0 -33
  169. package/lib/tui/components/session-list.tsx +0 -33
  170. package/lib/tui/components/ui/hascii/accordion-item.tsx +0 -88
  171. package/lib/tui/components/ui/hascii/accordion.tsx +0 -96
  172. package/lib/tui/components/ui/hascii/alert-dialog.tsx +0 -43
  173. package/lib/tui/components/ui/hascii/badge.tsx +0 -51
  174. package/lib/tui/components/ui/hascii/breadcrumb.tsx +0 -58
  175. package/lib/tui/components/ui/hascii/button.tsx +0 -194
  176. package/lib/tui/components/ui/hascii/card-content.tsx +0 -14
  177. package/lib/tui/components/ui/hascii/card-description.tsx +0 -13
  178. package/lib/tui/components/ui/hascii/card-footer.tsx +0 -14
  179. package/lib/tui/components/ui/hascii/card-header.tsx +0 -14
  180. package/lib/tui/components/ui/hascii/card-title.tsx +0 -13
  181. package/lib/tui/components/ui/hascii/card.tsx +0 -27
  182. package/lib/tui/components/ui/hascii/checkbox.tsx +0 -65
  183. package/lib/tui/components/ui/hascii/command.tsx +0 -159
  184. package/lib/tui/components/ui/hascii/dialog-content.tsx +0 -14
  185. package/lib/tui/components/ui/hascii/dialog-description.tsx +0 -13
  186. package/lib/tui/components/ui/hascii/dialog-footer.tsx +0 -14
  187. package/lib/tui/components/ui/hascii/dialog-header.tsx +0 -14
  188. package/lib/tui/components/ui/hascii/dialog-title.tsx +0 -13
  189. package/lib/tui/components/ui/hascii/dialog.tsx +0 -27
  190. package/lib/tui/components/ui/hascii/file-tree.tsx +0 -142
  191. package/lib/tui/components/ui/hascii/focus-group.tsx +0 -62
  192. package/lib/tui/components/ui/hascii/form-item.tsx +0 -43
  193. package/lib/tui/components/ui/hascii/input-otp.tsx +0 -86
  194. package/lib/tui/components/ui/hascii/input.tsx +0 -130
  195. package/lib/tui/components/ui/hascii/pagination.tsx +0 -105
  196. package/lib/tui/components/ui/hascii/progress.tsx +0 -28
  197. package/lib/tui/components/ui/hascii/select.tsx +0 -131
  198. package/lib/tui/components/ui/hascii/separator.tsx +0 -35
  199. package/lib/tui/components/ui/hascii/sidebar-content.tsx +0 -23
  200. package/lib/tui/components/ui/hascii/sidebar-header.tsx +0 -14
  201. package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +0 -67
  202. package/lib/tui/components/ui/hascii/sidebar.tsx +0 -24
  203. package/lib/tui/components/ui/hascii/skeleton.tsx +0 -60
  204. package/lib/tui/components/ui/hascii/slider.tsx +0 -91
  205. package/lib/tui/components/ui/hascii/snackbar.tsx +0 -75
  206. package/lib/tui/components/ui/hascii/sparkline.tsx +0 -53
  207. package/lib/tui/components/ui/hascii/spinner.tsx +0 -47
  208. package/lib/tui/components/ui/hascii/stepper.tsx +0 -54
  209. package/lib/tui/components/ui/hascii/switch.tsx +0 -66
  210. package/lib/tui/components/ui/hascii/table.tsx +0 -95
  211. package/lib/tui/components/ui/hascii/tabs.tsx +0 -59
  212. package/lib/tui/components/ui/hascii/toggle-group-item.tsx +0 -45
  213. package/lib/tui/components/ui/hascii/toggle-group.tsx +0 -99
  214. package/lib/tui/components/ui/hascii/tree.tsx +0 -104
  215. package/lib/tui/components/view-shell.tsx +0 -44
  216. package/lib/tui/filter-input.tsx +0 -33
  217. package/lib/tui/hooks/hascii/use-pressable.ts +0 -54
  218. package/lib/tui/parse-comma-list.ts +0 -14
  219. package/lib/tui/profile-launcher.tsx +0 -61
  220. package/lib/tui/scrollbar-options.ts +0 -19
  221. package/lib/tui/sidebar.tsx +0 -50
  222. package/lib/tui/theme.ts +0 -40
  223. package/lib/tui/tui.tsx +0 -20
  224. package/lib/tui/types.ts +0 -38
  225. package/lib/tui/unique-name.ts +0 -18
  226. package/lib/tui/use-event-stream.ts +0 -133
  227. package/lib/tui/use-snapshot.ts +0 -99
  228. package/lib/tui/utils/hascii/form-item-context.tsx +0 -23
  229. package/lib/tui/utils/hascii/input-focus-context.tsx +0 -31
  230. package/lib/tui/utils/hascii/theme-context.tsx +0 -26
  231. package/lib/tui/utils/hascii/theme.ts +0 -176
  232. package/lib/tui/views/channels-view.tsx +0 -108
  233. package/lib/tui/views/connectors-view.tsx +0 -164
  234. package/lib/tui/views/events-view.tsx +0 -160
  235. package/lib/tui/views/listeners-view.tsx +0 -80
  236. package/lib/tui/views/profiles-view.tsx +0 -152
@@ -1,123 +0,0 @@
1
- import { homedir } from "node:os"
2
- import { join } from "node:path"
3
- import { Server } from "@modelcontextprotocol/sdk/server/index.js"
4
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
5
- import {
6
- CallToolRequestSchema,
7
- ListToolsRequestSchema,
8
- } from "@modelcontextprotocol/sdk/types.js"
9
- import { FunnelChannelSubscriber } from "@/engine/mcp/channel-subscriber"
10
- import { FUNNEL_MCP_NAME } from "@/engine/mcp/mcp"
11
- import { readChannelConnectors } from "@/engine/mcp/read-channel-connectors"
12
- import { readGatewayToken } from "@/engine/mcp/read-gateway-token"
13
- import { usageHintForType } from "@/engine/mcp/usage-hint-for-type"
14
-
15
- const DEFAULT_FUNNEL_DIR = join(homedir(), ".funnel")
16
- const DEFAULT_GATEWAY_BASE_URL = "http://localhost:9742"
17
-
18
- export type ChannelServerOptions = {
19
- /** Funnel home directory (settings.json + gateway.token). Defaults to ~/.funnel. */
20
- dir?: string
21
- /** Gateway base URL. Defaults to `$FUNNEL_GATEWAY_URL` or `http://localhost:9742`. */
22
- gatewayUrl?: string
23
- /** Channel id to subscribe to. Defaults to `$FUNNEL_CHANNEL_ID`. */
24
- channelId?: string
25
- /** Auth token. Defaults to `$FUNNEL_GATEWAY_TOKEN` then `<dir>/gateway.token`. */
26
- token?: string
27
- }
28
-
29
- export const startChannelServer = async (
30
- options: ChannelServerOptions = {},
31
- ): Promise<void> => {
32
- const dir = options.dir ?? DEFAULT_FUNNEL_DIR
33
- const gatewayBaseUrl =
34
- options.gatewayUrl ?? process.env.FUNNEL_GATEWAY_URL ?? DEFAULT_GATEWAY_BASE_URL
35
- const gatewayWsUrl = `${gatewayBaseUrl.replace(/^http/, "ws")}/ws`
36
- const channelId = options.channelId ?? process.env.FUNNEL_CHANNEL_ID
37
- const channel = channelId ? readChannelConnectors(dir, channelId) : null
38
- const token = options.token ?? readGatewayToken(dir)
39
-
40
- const server = new Server(
41
- { name: FUNNEL_MCP_NAME, version: "1.0.0" },
42
- {
43
- capabilities: {
44
- experimental: { "claude/channel": {} },
45
- tools: {},
46
- },
47
- instructions: [
48
- `Events arrive inside <channel source="${FUNNEL_MCP_NAME}"> tags. Use meta.event_type to discriminate.`,
49
- "",
50
- "To reply or act, call the connector tool exposed by this MCP (one tool per connector configured on this channel). Each tool takes { method, path, body } matching the underlying adapter's CallInput.",
51
- ].join("\n"),
52
- },
53
- )
54
-
55
- server.setRequestHandler(ListToolsRequestSchema, async () => {
56
- const tools = (channel?.connectors ?? []).map((c) => ({
57
- name: c.name,
58
- description: `Call the "${c.name}" (${c.type}) connector. ${usageHintForType(c.type)}`,
59
- inputSchema: {
60
- type: "object" as const,
61
- properties: {
62
- method: { type: "string", description: "HTTP verb or API method (e.g. POST, chat.postMessage)" },
63
- path: { type: "string", description: "API path or method name (adapter-specific)" },
64
- body: { type: "object", description: "Request body / params (adapter-specific)" },
65
- },
66
- required: ["method", "path"],
67
- },
68
- }))
69
-
70
- return { tools }
71
- })
72
-
73
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
74
- if (!channel) {
75
- throw new Error("FUNNEL_CHANNEL_ID is not set or channel not found in settings.json")
76
- }
77
-
78
- const connectorName = request.params.name
79
- const args = (request.params.arguments ?? {}) as Record<string, unknown>
80
- const method = typeof args.method === "string" ? args.method : ""
81
- const path = typeof args.path === "string" ? args.path : ""
82
- const body = args.body ?? {}
83
-
84
- if (!method || !path) {
85
- throw new Error("`method` and `path` are required")
86
- }
87
-
88
- const url = `${gatewayBaseUrl}/channels/${encodeURIComponent(channel.channelName)}/connectors/${encodeURIComponent(connectorName)}/call`
89
- const headers: Record<string, string> = { "content-type": "application/json" }
90
-
91
- if (token) headers.authorization = `Bearer ${token}`
92
-
93
- const res = await fetch(url, {
94
- method: "POST",
95
- headers,
96
- body: JSON.stringify({ method, path, body }),
97
- })
98
-
99
- const text = await res.text()
100
-
101
- if (!res.ok) {
102
- throw new Error(`gateway call failed (${res.status}): ${text}`)
103
- }
104
-
105
- return {
106
- content: [{ type: "text", text }],
107
- }
108
- })
109
-
110
- const transport = new StdioServerTransport()
111
-
112
- await server.connect(transport)
113
-
114
- if (!channelId) return
115
-
116
- const subscriber = new FunnelChannelSubscriber({
117
- server,
118
- baseUrl: `${gatewayWsUrl}?channel=${encodeURIComponent(channelId)}`,
119
- protocols: token ? [`funnel.token.${token}`] : undefined,
120
- })
121
-
122
- subscriber.start()
123
- }
@@ -1,82 +0,0 @@
1
- import type { Server } from "@modelcontextprotocol/sdk/server/index.js"
2
-
3
- const RECONNECT_DELAY = 1000
4
- const MAX_RECONNECT_DELAY = 10000
5
-
6
- type Props = {
7
- server: Server
8
- baseUrl: string
9
- protocols: string[] | undefined
10
- }
11
-
12
- type State = {
13
- reconnectDelay: number
14
- lastOffset: number
15
- }
16
-
17
- /**
18
- * Subscribes to the gateway WebSocket for a single channel and forwards
19
- * incoming events to the MCP server as `notifications/claude/channel`.
20
- * Reconnects with exponential backoff and replays missed events via `?since=<offset>`.
21
- */
22
- export class FunnelChannelSubscriber {
23
- private readonly state: State = { reconnectDelay: RECONNECT_DELAY, lastOffset: 0 }
24
-
25
- constructor(private readonly props: Props) {
26
- Object.freeze(this)
27
- }
28
-
29
- start(): void {
30
- this.connect()
31
- }
32
-
33
- private connect(): void {
34
- const sinceQuery = this.state.lastOffset > 0 ? `&since=${this.state.lastOffset}` : ""
35
- const wsUrl = `${this.props.baseUrl}${sinceQuery}`
36
- const ws = new WebSocket(wsUrl, this.props.protocols)
37
-
38
- ws.addEventListener("open", () => {
39
- this.state.reconnectDelay = RECONNECT_DELAY
40
- process.stderr.write(`funnel: connected (${wsUrl})\n`)
41
- })
42
-
43
- ws.addEventListener("message", (event) => this.handleMessage(event))
44
-
45
- ws.addEventListener("close", () => {
46
- process.stderr.write(
47
- `funnel: disconnected, reconnecting in ${this.state.reconnectDelay}ms\n`,
48
- )
49
- setTimeout(() => this.connect(), this.state.reconnectDelay)
50
- this.state.reconnectDelay = Math.min(this.state.reconnectDelay * 2, MAX_RECONNECT_DELAY)
51
- })
52
-
53
- ws.addEventListener("error", () => {
54
- // close handler will reconnect
55
- })
56
- }
57
-
58
- private async handleMessage(event: MessageEvent): Promise<void> {
59
- try {
60
- const payload = JSON.parse(String(event.data))
61
- const eventType = payload.meta?.event_type ?? "unknown"
62
-
63
- if (typeof payload.offset === "number" && payload.offset > this.state.lastOffset) {
64
- this.state.lastOffset = payload.offset
65
- }
66
-
67
- process.stderr.write(`funnel: received event (${eventType})\n`)
68
-
69
- await this.props.server.notification({
70
- method: "notifications/claude/channel",
71
- params: {
72
- content: payload.content,
73
- meta: payload.meta,
74
- },
75
- })
76
- } catch (error) {
77
- process.stderr.write(
78
- `funnel: error: ${error instanceof Error ? error.message : String(error)}\n`,
79
- )
80
- }
81
- }
82
- }
@@ -1,126 +0,0 @@
1
- import { join } from "node:path"
2
- import { z } from "zod"
3
- import { FunnelFileSystem } from "@/engine/fs/file-system"
4
- import { NodeFunnelFileSystem } from "@/engine/fs/node-file-system"
5
-
6
- export const FUNNEL_MCP_COMMAND = "funnel"
7
- export const FUNNEL_MCP_NAME = "funnel"
8
-
9
- const mcpEntrySchema = z.object({
10
- command: z.string().optional(),
11
- args: z.array(z.string()).optional(),
12
- })
13
-
14
- const mcpConfigSchema = z.object({
15
- mcpServers: z.record(z.string(), mcpEntrySchema).optional(),
16
- })
17
-
18
- type McpEntry = z.infer<typeof mcpEntrySchema>
19
- type McpConfig = z.infer<typeof mcpConfigSchema>
20
-
21
- type Deps = {
22
- fs?: FunnelFileSystem
23
- }
24
-
25
- const defaultFs = new NodeFunnelFileSystem()
26
-
27
- /**
28
- * Installs/uninstalls the funnel MCP entry into a target repository's
29
- * `.mcp.json`. Detects an existing entry by command match so renaming is
30
- * preserved across re-installs.
31
- */
32
- export class FunnelMcp {
33
- private readonly fs: FunnelFileSystem
34
-
35
- constructor(deps: Deps = {}) {
36
- this.fs = deps.fs ?? defaultFs
37
- Object.freeze(this)
38
- }
39
-
40
- install(repoPath: string): void {
41
- if (!this.fs.existsSync(repoPath)) {
42
- throw new Error(`repository does not exist: ${repoPath}`)
43
- }
44
-
45
- const config = this.readConfig(repoPath)
46
- const servers = config.mcpServers ?? {}
47
-
48
- const existingName = this.findServerName(servers)
49
- const targetName = existingName ?? FUNNEL_MCP_NAME
50
-
51
- servers[targetName] = {
52
- command: FUNNEL_MCP_COMMAND,
53
- args: ["mcp"],
54
- }
55
-
56
- this.writeConfig(repoPath, { ...config, mcpServers: servers })
57
- }
58
-
59
- uninstall(repoPath: string): void {
60
- if (!this.fs.existsSync(repoPath)) return
61
-
62
- const config = this.readConfig(repoPath)
63
- const servers = config.mcpServers ?? {}
64
-
65
- const name = this.findServerName(servers)
66
-
67
- if (!name) return
68
-
69
- const next = { ...servers }
70
-
71
- delete next[name]
72
-
73
- this.writeConfig(repoPath, { ...config, mcpServers: next })
74
- }
75
-
76
- findInstalledName(cwd: string): string | null {
77
- const config = this.readConfig(cwd)
78
-
79
- return this.findServerName(config.mcpServers ?? {})
80
- }
81
-
82
- private findServerName(servers: Record<string, McpEntry>): string | null {
83
- for (const entry of Object.entries(servers)) {
84
- const name = entry[0]
85
- const value = entry[1]
86
-
87
- if (value?.command === FUNNEL_MCP_COMMAND) return name
88
- }
89
-
90
- return null
91
- }
92
-
93
- private readConfig(repoPath: string): McpConfig {
94
- const mcpPath = join(repoPath, ".mcp.json")
95
-
96
- if (!this.fs.existsSync(mcpPath)) return {}
97
-
98
- const content = this.fs.readFileSync(mcpPath).trim()
99
-
100
- if (!content) return {}
101
-
102
- let parsed: unknown
103
-
104
- try {
105
- parsed = JSON.parse(content)
106
- } catch (error) {
107
- throw new Error(
108
- `invalid .mcp.json (${mcpPath}): ${error instanceof Error ? error.message : String(error)}`,
109
- )
110
- }
111
-
112
- const result = mcpConfigSchema.safeParse(parsed)
113
-
114
- if (!result.success) {
115
- throw new Error(`invalid .mcp.json (${mcpPath}): ${result.error.message}`)
116
- }
117
-
118
- return result.data
119
- }
120
-
121
- private writeConfig(repoPath: string, config: McpConfig): void {
122
- const mcpPath = join(repoPath, ".mcp.json")
123
-
124
- this.fs.writeFileSync(mcpPath, `${JSON.stringify(config, null, 2)}\n`)
125
- }
126
- }
@@ -1,34 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs"
2
- import { join } from "node:path"
3
- import { settingsSchema } from "@/engine/settings/settings-schema"
4
-
5
- const TOOL_CONNECTOR_TYPES = new Set(["slack", "gh", "discord"])
6
-
7
- export type ChannelConnectorsView = {
8
- channelName: string
9
- connectors: { name: string; type: string }[]
10
- }
11
-
12
- export const readChannelConnectors = (
13
- dir: string,
14
- channelId: string,
15
- ): ChannelConnectorsView | null => {
16
- const settingsPath = join(dir, "settings.json")
17
-
18
- if (!existsSync(settingsPath)) return null
19
-
20
- const raw = JSON.parse(readFileSync(settingsPath, "utf-8"))
21
- const parsed = settingsSchema.safeParse(raw)
22
-
23
- if (!parsed.success) return null
24
-
25
- const channel = parsed.data.channels.find((c) => c.id === channelId)
26
-
27
- if (!channel) return null
28
-
29
- const connectors = channel.connectors
30
- .filter((c) => TOOL_CONNECTOR_TYPES.has(c.type))
31
- .map((c) => ({ name: c.name, type: c.type }))
32
-
33
- return { channelName: channel.name, connectors }
34
- }
@@ -1,16 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs"
2
- import { join } from "node:path"
3
-
4
- export const readGatewayToken = (dir: string): string | null => {
5
- const fromEnv = process.env.FUNNEL_GATEWAY_TOKEN
6
-
7
- if (fromEnv && fromEnv.length > 0) return fromEnv
8
-
9
- const path = join(dir, "gateway.token")
10
-
11
- if (!existsSync(path)) return null
12
-
13
- const value = readFileSync(path, "utf-8").trim()
14
-
15
- return value.length > 0 ? value : null
16
- }
@@ -1,15 +0,0 @@
1
- export const usageHintForType = (type: string): string => {
2
- if (type === "slack") {
3
- return "Slack Web API. method=POST path=chat.postMessage body={channel,text,thread_ts?}"
4
- }
5
-
6
- if (type === "discord") {
7
- return "Discord REST API. method=POST path=/channels/<id>/messages body={content,...}"
8
- }
9
-
10
- if (type === "gh") {
11
- return "GitHub REST via gh CLI. method=POST path=repos/owner/repo/issues/N/comments body={body}"
12
- }
13
-
14
- return "Generic adapter call."
15
- }
@@ -1,88 +0,0 @@
1
- import {
2
- type AttachOptions,
3
- type DetachOptions,
4
- FunnelProcessRunner,
5
- type RunOptions,
6
- type RunResult,
7
- } from "@/engine/process/process-runner"
8
-
9
- export type MemoryProcessResponse = {
10
- exitCode?: number
11
- stdout?: string
12
- stderr?: string
13
- }
14
-
15
- export type MemoryProcessHandler = (
16
- command: string[],
17
- ) => MemoryProcessResponse | Promise<MemoryProcessResponse>
18
-
19
- export type MemoryProcessSyncHandler = (command: string[]) => MemoryProcessResponse
20
-
21
- export type MemoryProcessCall =
22
- | { kind: "run"; command: string[]; options: RunOptions }
23
- | { kind: "runSync"; command: string[] }
24
- | { kind: "attach"; command: string[]; options: AttachOptions }
25
- | { kind: "detach"; command: string[]; options: DetachOptions }
26
- | { kind: "kill"; command: string[] }
27
-
28
- const empty: MemoryProcessResponse = { exitCode: 0, stdout: "", stderr: "" }
29
-
30
- export class MemoryFunnelProcessRunner extends FunnelProcessRunner {
31
- readonly calls: MemoryProcessCall[] = []
32
- readonly killed: { pid: number; signal: string }[] = []
33
- private handler: MemoryProcessHandler = () => empty
34
- private syncHandler: MemoryProcessSyncHandler = () => empty
35
-
36
- on(handler: MemoryProcessHandler): this {
37
- this.handler = handler
38
-
39
- return this
40
- }
41
-
42
- onSync(handler: MemoryProcessSyncHandler): this {
43
- this.syncHandler = handler
44
-
45
- return this
46
- }
47
-
48
- async run(command: string[], options: RunOptions = {}): Promise<RunResult> {
49
- this.calls.push({ kind: "run", command, options })
50
-
51
- const result = await this.handler(command)
52
-
53
- return {
54
- exitCode: result.exitCode ?? 0,
55
- stdout: result.stdout ?? "",
56
- stderr: result.stderr ?? "",
57
- }
58
- }
59
-
60
- runSync(command: string[]): RunResult {
61
- this.calls.push({ kind: "runSync", command })
62
-
63
- const result = this.syncHandler(command)
64
-
65
- return {
66
- exitCode: result.exitCode ?? 0,
67
- stdout: result.stdout ?? "",
68
- stderr: result.stderr ?? "",
69
- }
70
- }
71
-
72
- async attach(command: string[], options: AttachOptions = {}): Promise<number> {
73
- this.calls.push({ kind: "attach", command, options })
74
-
75
- const result = await this.handler(command)
76
-
77
- return result.exitCode ?? 0
78
- }
79
-
80
- detach(command: string[], options: DetachOptions = {}): void {
81
- this.calls.push({ kind: "detach", command, options })
82
- }
83
-
84
- kill(pid: number, signal: string = "SIGTERM"): void {
85
- this.calls.push({ kind: "kill", command: [String(pid), signal] })
86
- this.killed.push({ pid, signal })
87
- }
88
- }
@@ -1,91 +0,0 @@
1
- import {
2
- type AttachOptions,
3
- type DetachOptions,
4
- FunnelProcessRunner,
5
- type RunOptions,
6
- type RunResult,
7
- } from "@/engine/process/process-runner"
8
-
9
- const toEnv = (env?: Record<string, string>): Record<string, string> | undefined => {
10
- if (!env) return undefined
11
-
12
- const merged: Record<string, string> = {}
13
-
14
- for (const [key, value] of Object.entries(process.env)) {
15
- if (typeof value === "string") merged[key] = value
16
- }
17
-
18
- for (const [key, value] of Object.entries(env)) {
19
- merged[key] = value
20
- }
21
-
22
- return merged
23
- }
24
-
25
- export class NodeFunnelProcessRunner extends FunnelProcessRunner {
26
- constructor() {
27
- super()
28
- Object.freeze(this)
29
- }
30
-
31
- runSync(command: string[]): RunResult {
32
- const result = Bun.spawnSync(command, {
33
- stdout: "pipe",
34
- stderr: "pipe",
35
- })
36
-
37
- return {
38
- exitCode: result.exitCode ?? 0,
39
- stdout: result.stdout.toString(),
40
- stderr: result.stderr.toString(),
41
- }
42
- }
43
-
44
- async run(command: string[], options: RunOptions = {}): Promise<RunResult> {
45
- const proc = Bun.spawn(command, {
46
- cwd: options.cwd,
47
- env: toEnv(options.env),
48
- stdin: options.input !== undefined ? "pipe" : "ignore",
49
- stdout: "pipe",
50
- stderr: "pipe",
51
- })
52
-
53
- if (options.input !== undefined && proc.stdin) {
54
- proc.stdin.write(options.input)
55
- proc.stdin.end()
56
- }
57
-
58
- const exitCode = await proc.exited
59
- const stdout = await new Response(proc.stdout).text()
60
- const stderr = await new Response(proc.stderr).text()
61
-
62
- return { exitCode, stdout, stderr }
63
- }
64
-
65
- async attach(command: string[], options: AttachOptions = {}): Promise<number> {
66
- const proc = Bun.spawn(command, {
67
- cwd: options.cwd,
68
- env: toEnv(options.env),
69
- stdio: ["inherit", "inherit", "inherit"],
70
- })
71
-
72
- return await proc.exited
73
- }
74
-
75
- detach(command: string[], options: DetachOptions = {}): void {
76
- const proc = Bun.spawn(command, {
77
- env: toEnv(options.env),
78
- stdio: ["ignore", "ignore", "ignore"],
79
- })
80
-
81
- proc.unref()
82
- }
83
-
84
- kill(pid: number, signal: string = "SIGTERM"): void {
85
- try {
86
- process.kill(pid, signal)
87
- } catch {
88
- // ignore
89
- }
90
- }
91
- }
@@ -1,33 +0,0 @@
1
- export type RunOptions = {
2
- cwd?: string
3
- env?: Record<string, string>
4
- input?: string
5
- }
6
-
7
- export type RunResult = {
8
- exitCode: number
9
- stdout: string
10
- stderr: string
11
- }
12
-
13
- export type AttachOptions = {
14
- cwd?: string
15
- env?: Record<string, string>
16
- }
17
-
18
- export type DetachOptions = {
19
- env?: Record<string, string>
20
- }
21
-
22
- /**
23
- * Process boundary covering one-shot runs, sync runs, foreground attach, and
24
- * detached background spawns. Default is NodeFunnelProcessRunner (Bun.spawn);
25
- * MemoryFunnelProcessRunner records calls and lets tests stub responses.
26
- */
27
- export abstract class FunnelProcessRunner {
28
- abstract run(command: string[], options?: RunOptions): Promise<RunResult>
29
- abstract runSync(command: string[]): RunResult
30
- abstract attach(command: string[], options?: AttachOptions): Promise<number>
31
- abstract detach(command: string[], options?: DetachOptions): void
32
- abstract kill(pid: number, signal?: string): void
33
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * Read-side dependency that lets FunnelChannels ask whether a profile
3
- * references a given channel id, without depending on FunnelProfiles directly.
4
- */
5
- export type ProfileChannelChecker = {
6
- hasChannelRef(channelId: string): boolean
7
- }