@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,56 +0,0 @@
1
- import { FunnelLogger } from "@/engine/logger/logger"
2
- import { NodeFunnelLogger } from "@/engine/logger/node-logger"
3
- import { FunnelProcessRunner } from "@/engine/process/process-runner"
4
- import { NodeFunnelProcessRunner } from "@/engine/process/node-process-runner"
5
-
6
- type Props = {
7
- selfPid: number
8
- process?: FunnelProcessRunner
9
- logger?: FunnelLogger
10
- }
11
-
12
- const defaultProcess = new NodeFunnelProcessRunner()
13
- const defaultLogger = new NodeFunnelLogger()
14
-
15
- const isBun = (args: string): boolean => {
16
- return args.includes("bun ") || /\/bun(\s|$)/.test(args)
17
- }
18
-
19
- const looksLikeSlackGateway = (args: string): boolean => {
20
- return /(gateway|bolt|slack)/i.test(args)
21
- }
22
-
23
- export const killCompetingSlackGateways = async (props: Props): Promise<number[]> => {
24
- const runner = props.process ?? defaultProcess
25
- const logger = props.logger ?? defaultLogger
26
- const result = await runner.run(["ps", "-e", "-o", "pid=,args="])
27
-
28
- if (result.exitCode !== 0) return []
29
-
30
- const killed: number[] = []
31
-
32
- for (const raw of result.stdout.split("\n")) {
33
- const line = raw.trim()
34
-
35
- if (!line) continue
36
-
37
- const match = /^(\d+)\s+(.+)$/.exec(line)
38
-
39
- if (!match) continue
40
-
41
- const pid = Number(match[1])
42
- const args = match[2]!
43
-
44
- if (!Number.isInteger(pid) || pid <= 0) continue
45
- if (pid === props.selfPid) continue
46
- if (!isBun(args)) continue
47
- if (!looksLikeSlackGateway(args)) continue
48
-
49
- runner.kill(pid, "SIGTERM")
50
- killed.push(pid)
51
-
52
- logger.info("killed competing Slack gateway process", { pid, args: args.slice(0, 160) })
53
- }
54
-
55
- return killed
56
- }
@@ -1,339 +0,0 @@
1
- import type { ConnectorConfig } from "@/connectors/connector-config-schema"
2
- import type { FunnelConnectorListener } from "@/connectors/connector-listener"
3
- import type { ChannelConnectorView } from "@/engine/channels/channels"
4
- import { FunnelLogger } from "@/engine/logger/logger"
5
- import { NodeFunnelLogger } from "@/engine/logger/node-logger"
6
-
7
- type ConnectorRegistry = {
8
- listAllConnectors(): ChannelConnectorView[]
9
- createListener(
10
- channelName: string,
11
- connectorName: string,
12
- ): { config: ConnectorConfig; channelId: string; listener: FunnelConnectorListener } | null
13
- }
14
-
15
- type SupervisorNotify = (
16
- channelName: string,
17
- connectorName: string,
18
- content: string,
19
- meta?: Record<string, string>,
20
- ) => Promise<void>
21
-
22
- type RunningEntry = {
23
- config: ConnectorConfig
24
- channelName: string
25
- channelId: string
26
- listener: FunnelConnectorListener
27
- }
28
-
29
- type ListenerStats = {
30
- events: number
31
- errors: number
32
- failureCount: number
33
- lastEventAt: string | null
34
- }
35
-
36
- type Deps = {
37
- channels: ConnectorRegistry
38
- notify: SupervisorNotify
39
- logger?: FunnelLogger
40
- healthCheckIntervalMs?: number
41
- maxBackoffMs?: number
42
- sleep?: (ms: number) => Promise<void>
43
- now?: () => number
44
- }
45
-
46
- const defaultLogger = new NodeFunnelLogger()
47
- const DEFAULT_HEALTH_INTERVAL_MS = 30_000
48
- const DEFAULT_MAX_BACKOFF_MS = 60_000
49
-
50
- const defaultSleep = (ms: number): Promise<void> =>
51
- new Promise((r) => {
52
- setTimeout(r, ms)
53
- })
54
-
55
- type ListenerEntryStatus = {
56
- channelName: string
57
- channelId: string
58
- name: string
59
- type: ConnectorConfig["type"]
60
- alive: boolean
61
- events: number
62
- errors: number
63
- failureCount: number
64
- lastEventAt: string | null
65
- }
66
-
67
- /**
68
- * Owns the running listener instances and their lifecycle.
69
- *
70
- * Lives in the gateway process and is the only place that calls
71
- * `listener.start()` / `listener.stop()`. Each entry is keyed by
72
- * `${channelName}/${connectorName}` so the same connector name can exist in
73
- * multiple channels without colliding.
74
- *
75
- * Periodically polls each running listener's `isAlive()` and auto-restarts
76
- * dead listeners with exponential backoff (1s, 2s, 4s, ... capped). Resets
77
- * the backoff counter on successful restart.
78
- */
79
- export class FunnelListenerSupervisor {
80
- private readonly channels: ConnectorRegistry
81
- private readonly notify: SupervisorNotify
82
- private readonly logger: FunnelLogger
83
- private readonly running = new Map<string, RunningEntry>()
84
- private readonly failureCounts = new Map<string, number>()
85
- private readonly stats = new Map<string, ListenerStats>()
86
- private readonly healthCheckIntervalMs: number
87
- private readonly maxBackoffMs: number
88
- private readonly sleep: (ms: number) => Promise<void>
89
- private readonly now: () => number
90
- private healthCheckTimer: ReturnType<typeof setInterval> | null = null
91
- private healthCheckInFlight = false
92
-
93
- constructor(deps: Deps) {
94
- this.channels = deps.channels
95
- this.notify = deps.notify
96
- this.logger = deps.logger ?? defaultLogger
97
- this.healthCheckIntervalMs = deps.healthCheckIntervalMs ?? DEFAULT_HEALTH_INTERVAL_MS
98
- this.maxBackoffMs = deps.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS
99
- this.sleep = deps.sleep ?? defaultSleep
100
- this.now = deps.now ?? (() => Date.now())
101
- }
102
-
103
- static keyOf(channelName: string, connectorName: string): string {
104
- return `${channelName}/${connectorName}`
105
- }
106
-
107
- isRunning(channelName: string, connectorName: string): boolean {
108
- return this.running.has(FunnelListenerSupervisor.keyOf(channelName, connectorName))
109
- }
110
-
111
- list(): ListenerEntryStatus[] {
112
- return [...this.running.entries()].map(([key, entry]) => {
113
- const stats = this.stats.get(key)
114
-
115
- return {
116
- channelName: entry.channelName,
117
- channelId: entry.channelId,
118
- name: entry.config.name,
119
- type: entry.config.type,
120
- alive: entry.listener.isAlive(),
121
- events: stats?.events ?? 0,
122
- errors: stats?.errors ?? 0,
123
- failureCount: this.failureCounts.get(key) ?? 0,
124
- lastEventAt: stats?.lastEventAt ?? null,
125
- }
126
- })
127
- }
128
-
129
- async start(
130
- channelName: string,
131
- connectorName: string,
132
- ): Promise<{ ok: boolean; reason?: string }> {
133
- const key = FunnelListenerSupervisor.keyOf(channelName, connectorName)
134
-
135
- if (this.running.has(key)) {
136
- return { ok: true, reason: "already running" }
137
- }
138
-
139
- const created = this.channels.createListener(channelName, connectorName)
140
-
141
- if (!created) {
142
- return {
143
- ok: false,
144
- reason: `connector "${connectorName}" not found in channel "${channelName}"`,
145
- }
146
- }
147
-
148
- const bind = async (content: string, meta?: Record<string, string>) => {
149
- try {
150
- await this.notify(channelName, connectorName, content, meta)
151
- this.recordEvent(key)
152
- } catch (error) {
153
- this.recordError(key)
154
- throw error
155
- }
156
- }
157
-
158
- try {
159
- await created.listener.start(bind)
160
- this.running.set(key, {
161
- config: created.config,
162
- channelName,
163
- channelId: created.channelId,
164
- listener: created.listener,
165
- })
166
- this.ensureStats(key)
167
- this.logger.info(`${created.config.type} listener started`, {
168
- channel: channelName,
169
- connector: connectorName,
170
- })
171
-
172
- return { ok: true }
173
- } catch (error) {
174
- this.logger.error(`${created.config.type} listener failed to start`, {
175
- channel: channelName,
176
- connector: connectorName,
177
- error: error instanceof Error ? error.message : String(error),
178
- })
179
-
180
- return {
181
- ok: false,
182
- reason: error instanceof Error ? error.message : String(error),
183
- }
184
- }
185
- }
186
-
187
- async stop(
188
- channelName: string,
189
- connectorName: string,
190
- ): Promise<{ ok: boolean; reason?: string }> {
191
- const key = FunnelListenerSupervisor.keyOf(channelName, connectorName)
192
- const entry = this.running.get(key)
193
-
194
- if (!entry) return { ok: true, reason: "not running" }
195
-
196
- try {
197
- await entry.listener.stop()
198
- this.running.delete(key)
199
- this.failureCounts.delete(key)
200
- this.logger.info(`${entry.config.type} listener stopped`, {
201
- channel: channelName,
202
- connector: connectorName,
203
- })
204
-
205
- return { ok: true }
206
- } catch (error) {
207
- this.logger.error(`${entry.config.type} listener failed to stop`, {
208
- channel: channelName,
209
- connector: connectorName,
210
- error: error instanceof Error ? error.message : String(error),
211
- })
212
-
213
- return {
214
- ok: false,
215
- reason: error instanceof Error ? error.message : String(error),
216
- }
217
- }
218
- }
219
-
220
- async restart(
221
- channelName: string,
222
- connectorName: string,
223
- ): Promise<{ ok: boolean; reason?: string }> {
224
- const stopped = await this.stop(channelName, connectorName)
225
-
226
- if (!stopped.ok) return stopped
227
-
228
- return await this.start(channelName, connectorName)
229
- }
230
-
231
- async startAll(): Promise<void> {
232
- const all = this.channels.listAllConnectors()
233
-
234
- for (const view of all) {
235
- await this.start(view.channelName, view.name)
236
- }
237
-
238
- this.startHealthCheck()
239
- }
240
-
241
- async stopAll(): Promise<void> {
242
- this.stopHealthCheck()
243
-
244
- for (const [, entry] of [...this.running.entries()]) {
245
- await this.stop(entry.channelName, entry.config.name)
246
- }
247
- }
248
-
249
- private ensureStats(key: string): ListenerStats {
250
- const existing = this.stats.get(key)
251
-
252
- if (existing) return existing
253
-
254
- const fresh: ListenerStats = { events: 0, errors: 0, failureCount: 0, lastEventAt: null }
255
-
256
- this.stats.set(key, fresh)
257
-
258
- return fresh
259
- }
260
-
261
- private recordEvent(key: string): void {
262
- const stats = this.ensureStats(key)
263
-
264
- stats.events += 1
265
- stats.lastEventAt = new Date(this.now()).toISOString()
266
- }
267
-
268
- private recordError(key: string): void {
269
- this.ensureStats(key).errors += 1
270
- }
271
-
272
- private startHealthCheck(): void {
273
- if (this.healthCheckTimer) return
274
-
275
- this.healthCheckTimer = setInterval(() => {
276
- void this.runHealthCheck()
277
- }, this.healthCheckIntervalMs)
278
-
279
- this.healthCheckTimer.unref()
280
- }
281
-
282
- private stopHealthCheck(): void {
283
- if (!this.healthCheckTimer) return
284
-
285
- clearInterval(this.healthCheckTimer)
286
- this.healthCheckTimer = null
287
- }
288
-
289
- private async runHealthCheck(): Promise<void> {
290
- if (this.healthCheckInFlight) return
291
-
292
- this.healthCheckInFlight = true
293
-
294
- try {
295
- for (const [key, entry] of [...this.running.entries()]) {
296
- if (entry.listener.isAlive()) {
297
- this.failureCounts.delete(key)
298
- continue
299
- }
300
-
301
- await this.recoverDead(entry.channelName, entry.config.name, entry.config.type)
302
- }
303
- } finally {
304
- this.healthCheckInFlight = false
305
- }
306
- }
307
-
308
- private async recoverDead(
309
- channelName: string,
310
- connectorName: string,
311
- type: ConnectorConfig["type"],
312
- ): Promise<void> {
313
- const key = FunnelListenerSupervisor.keyOf(channelName, connectorName)
314
- const failureCount = this.failureCounts.get(key) ?? 0
315
- const backoffMs = Math.min(1000 * 2 ** failureCount, this.maxBackoffMs)
316
-
317
- this.logger.warn(`${type} listener unhealthy, restarting`, {
318
- channel: channelName,
319
- connector: connectorName,
320
- attempt: failureCount + 1,
321
- backoffMs,
322
- })
323
-
324
- await this.stop(channelName, connectorName)
325
- await this.sleep(backoffMs)
326
-
327
- const result = await this.start(channelName, connectorName)
328
-
329
- if (result.ok) {
330
- this.failureCounts.delete(key)
331
- this.logger.info(`${type} listener recovered`, {
332
- channel: channelName,
333
- connector: connectorName,
334
- })
335
- } else {
336
- this.failureCounts.set(key, failureCount + 1)
337
- }
338
- }
339
- }
@@ -1,128 +0,0 @@
1
- import { z } from "zod"
2
-
3
- type Deps = {
4
- port: number
5
- isDaemonRunning: () => boolean
6
- /** Returns the daemon's gateway token, or null if unavailable. Sent as `Authorization: Bearer`. */
7
- getToken?: () => string | null
8
- }
9
-
10
- const listenerEntrySchema = z.object({
11
- channelName: z.string(),
12
- channelId: z.string(),
13
- name: z.string(),
14
- type: z.string(),
15
- alive: z.boolean(),
16
- })
17
-
18
- const listenersResponseSchema = z.object({
19
- listeners: z.array(listenerEntrySchema),
20
- })
21
-
22
- const opErrorBodySchema = z.object({
23
- reason: z.string().optional(),
24
- })
25
-
26
- export type ListenerEntry = z.infer<typeof listenerEntrySchema>
27
-
28
- export type ListenerOpResult =
29
- | { state: "ok" }
30
- | { state: "offline" }
31
- | { state: "error"; reason: string }
32
-
33
- export type ListListenersResult =
34
- | { state: "ok"; listeners: ListenerEntry[] }
35
- | { state: "offline" }
36
- | { state: "error"; reason: string }
37
-
38
- const OFFLINE: ListenerOpResult = { state: "offline" }
39
-
40
- /**
41
- * HTTP client for listener operations on a running gateway daemon.
42
- *
43
- * Returns `{ state: "offline" }` when the daemon isn't running so callers
44
- * (CLI hot-reload paths) can treat that as a no-op without parsing strings.
45
- * Pair this with `FunnelGateway` (process control) for the full picture.
46
- */
47
- export class FunnelListenersClient {
48
- private readonly port: number
49
- private readonly isDaemonRunning: () => boolean
50
- private readonly getToken: () => string | null
51
-
52
- constructor(deps: Deps) {
53
- this.port = deps.port
54
- this.isDaemonRunning = deps.isDaemonRunning
55
- this.getToken = deps.getToken ?? (() => null)
56
- Object.freeze(this)
57
- }
58
-
59
- async list(): Promise<ListListenersResult> {
60
- if (!this.isDaemonRunning()) return { state: "offline" }
61
-
62
- try {
63
- const res = await fetch(`http://localhost:${this.port}/listeners`, {
64
- headers: this.authHeaders(),
65
- })
66
-
67
- if (!res.ok) return { state: "error", reason: `HTTP ${res.status}` }
68
-
69
- const parsed = listenersResponseSchema.safeParse(await res.json())
70
-
71
- if (!parsed.success) {
72
- return { state: "error", reason: "malformed daemon response" }
73
- }
74
-
75
- return { state: "ok", listeners: parsed.data.listeners }
76
- } catch (error) {
77
- return { state: "error", reason: error instanceof Error ? error.message : String(error) }
78
- }
79
- }
80
-
81
- async start(channelName: string, connectorName: string): Promise<ListenerOpResult> {
82
- if (!this.isDaemonRunning()) return OFFLINE
83
-
84
- return await this.call("POST", `/listeners/${this.path(channelName, connectorName)}/start`)
85
- }
86
-
87
- async stop(channelName: string, connectorName: string): Promise<ListenerOpResult> {
88
- if (!this.isDaemonRunning()) return OFFLINE
89
-
90
- return await this.call("DELETE", `/listeners/${this.path(channelName, connectorName)}`)
91
- }
92
-
93
- async restart(channelName: string, connectorName: string): Promise<ListenerOpResult> {
94
- if (!this.isDaemonRunning()) return OFFLINE
95
-
96
- return await this.call("POST", `/listeners/${this.path(channelName, connectorName)}/restart`)
97
- }
98
-
99
- private path(channelName: string, connectorName: string): string {
100
- return `${encodeURIComponent(channelName)}/${encodeURIComponent(connectorName)}`
101
- }
102
-
103
- private authHeaders(): Record<string, string> {
104
- const token = this.getToken()
105
-
106
- return token ? { authorization: `Bearer ${token}` } : {}
107
- }
108
-
109
- private async call(method: "POST" | "DELETE", path: string): Promise<ListenerOpResult> {
110
- try {
111
- const res = await fetch(`http://localhost:${this.port}${path}`, {
112
- method,
113
- headers: this.authHeaders(),
114
- })
115
-
116
- if (!res.ok) {
117
- const parsed = opErrorBodySchema.safeParse(await res.json().catch(() => null))
118
- const reason = parsed.success ? parsed.data.reason : undefined
119
-
120
- return { state: "error", reason: reason ?? `HTTP ${res.status}` }
121
- }
122
-
123
- return { state: "ok" }
124
- } catch (error) {
125
- return { state: "error", reason: error instanceof Error ? error.message : String(error) }
126
- }
127
- }
128
- }
@@ -1,27 +0,0 @@
1
- import { z } from "zod"
2
-
3
- /**
4
- * Shared schema for `POST /channels/:channel/publish` — used by both the
5
- * gateway route handler (input validation) and the CLI / programmable client
6
- * (request shape). The route resolves `channel` from the path; this body
7
- * covers everything else.
8
- */
9
- export const publishRequestSchema = z.object({
10
- content: z.string().min(1),
11
- meta: z.record(z.string(), z.string()).optional(),
12
- connector: z.string().min(1).optional(),
13
- })
14
-
15
- export type PublishRequest = z.infer<typeof publishRequestSchema>
16
-
17
- export const publishResponseSchema = z.object({
18
- ok: z.literal(true),
19
- offset: z.number().int().nonnegative(),
20
- })
21
-
22
- export type PublishResponse = z.infer<typeof publishResponseSchema>
23
-
24
- export type PublishResult =
25
- | { state: "ok"; offset: number }
26
- | { state: "offline" }
27
- | { state: "error"; reason: string }
@@ -1,26 +0,0 @@
1
- import { existsSync } from "node:fs"
2
- import { resolve } from "node:path"
3
-
4
- /**
5
- * Locate the daemon entry script. Works in both dev (running from source)
6
- * and built mode (bundled into dist/bin.js with daemon at dist/gateway/daemon.js).
7
- *
8
- * The candidates cover:
9
- * 1. dev: this helper lives at lib/gateway/, so daemon.ts is its sibling
10
- * 2. built sibling: dist/gateway/daemon.js if the helper itself ends up at dist/gateway/
11
- * 3. bundled: when this helper is inlined into dist/bin.js, import.meta.dir is dist/,
12
- * and daemon.js lives at dist/gateway/daemon.js
13
- */
14
- export const resolveDaemonScript = (): string => {
15
- const candidates = [
16
- resolve(import.meta.dir, "./daemon.ts"),
17
- resolve(import.meta.dir, "./daemon.js"),
18
- resolve(import.meta.dir, "./gateway/daemon.js"),
19
- ]
20
-
21
- for (const candidate of candidates) {
22
- if (existsSync(candidate)) return candidate
23
- }
24
-
25
- throw new Error(`daemon script not found (looked in ${candidates.join(", ")})`)
26
- }
@@ -1,39 +0,0 @@
1
- import { HTTPException } from "hono/http-exception"
2
- import { z } from "zod"
3
- import { factory } from "@/gateway/factory"
4
- import { zParam } from "@/gateway/routes/validator"
5
-
6
- const bodySchema = z.object({
7
- method: z.string().min(1),
8
- path: z.string().min(1),
9
- body: z.unknown().optional(),
10
- })
11
-
12
- /**
13
- * POST /channels/:channel/connectors/:connector/call
14
- *
15
- * Generic adapter call. Used by the funnel MCP server (running in the Claude
16
- * Code process) to send replies/reactions/etc. without spawning a CLI
17
- * subprocess. Mirrors the CLI's `funnel channels <c> connectors <conn> request
18
- * --method=...` but with a structured JSON body and no shell.
19
- */
20
- export const channelsConnectorsCallHandler = factory.createHandlers(
21
- zParam(z.object({ channel: z.string().min(1), connector: z.string().min(1) })),
22
- async (c) => {
23
- const param = c.req.valid("param")
24
- const raw = await c.req.json().catch(() => null)
25
- const parsed = bodySchema.safeParse(raw)
26
-
27
- if (!parsed.success) {
28
- throw new HTTPException(400, { message: parsed.error.issues[0]?.message ?? "invalid body" })
29
- }
30
-
31
- const result = await c.var.deps.channels.call(param.channel, param.connector, {
32
- method: parsed.data.method,
33
- path: parsed.data.path,
34
- body: parsed.data.body ?? {},
35
- })
36
-
37
- return c.json({ ok: true, result })
38
- },
39
- )
@@ -1,44 +0,0 @@
1
- import { zValidator } from "@hono/zod-validator"
2
- import { z } from "zod"
3
- import { factory } from "@/gateway/factory"
4
- import { publishRequestSchema } from "@/gateway/publish-schema"
5
- import type { PublishResponse } from "@/gateway/publish-schema"
6
- import { zParam } from "@/gateway/routes/validator"
7
-
8
- /**
9
- * POST /channels/:channel/publish
10
- *
11
- * Inject arbitrary content into a channel. Mirrors the connector-driven `notify`
12
- * path: events go through `broadcaster.broadcast` + `eventStore.record`, so
13
- * subscribers see them exactly as if a listener had produced them.
14
- *
15
- * Body validation is Zod-shared with the client (`publishRequestSchema`); the
16
- * response (`publishResponseSchema`) carries the assigned offset so callers can
17
- * correlate with the persistent event store.
18
- */
19
- export const channelsPublishHandler = factory.createHandlers(
20
- zParam(z.object({ channel: z.string().min(1) })),
21
- zValidator("json", publishRequestSchema, (result, c) => {
22
- if (result.success) return
23
-
24
- const issue = result.error.issues[0]
25
- const reason = issue ? `${issue.path.join(".")}: ${issue.message}` : "invalid body"
26
-
27
- return c.json({ ok: false, reason }, 400)
28
- }),
29
- (c) => {
30
- const param = c.req.valid("param")
31
- const body = c.req.valid("json")
32
-
33
- const event = c.var.deps.emit({
34
- channel: param.channel,
35
- connector: body.connector,
36
- content: body.content,
37
- meta: body.meta,
38
- })
39
-
40
- const response: PublishResponse = { ok: true, offset: event.offset }
41
-
42
- return c.json(response)
43
- },
44
- )
@@ -1,13 +0,0 @@
1
- import { factory } from "@/gateway/factory"
2
-
3
- /** GET /health — liveness + listener registry snapshot. */
4
- export const healthHandler = factory.createHandlers((c) => {
5
- const deps = c.var.deps
6
-
7
- return c.json({
8
- ok: true,
9
- pid: deps.selfPid,
10
- clients: deps.broadcaster.getClientCount(),
11
- listeners: deps.supervisor.list(),
12
- })
13
- })