@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,36 +0,0 @@
1
- import { HTTPException } from "hono/http-exception"
2
- import { z } from "zod"
3
- import { factory } from "@/cli/factory"
4
- import { queryToCliArgs } from "@/cli/router/query-to-cli-args"
5
- import { zValidator } from "@/cli/router/validator"
6
-
7
- export const launchHelp = `funnel profiles <name> run — launch a profile (sugar for fnl claude)
8
-
9
- usage: funnel profiles <name> run [additional claude args...]
10
- funnel profiles <name> (alias)`
11
-
12
- const RESERVED_KEYS: string[] = []
13
-
14
- export const profilesLaunchHandler = factory.createHandlers(
15
- zValidator("param", z.object({ profile: z.string() })),
16
- zValidator("query", z.object({}).passthrough(), launchHelp),
17
- async (c) => {
18
- const param = c.req.valid("param")
19
- const funnel = c.var.funnel
20
- const profile = funnel.profiles.get(param.profile)
21
-
22
- if (!profile) {
23
- throw new HTTPException(404, { message: `profile "${param.profile}" not found` })
24
- }
25
-
26
- const exitCode = await funnel.claude.launch({
27
- channel: profile.channelId,
28
- cwd: profile.path,
29
- subAgent: profile.subAgent,
30
- userArgs: queryToCliArgs(c.req.url, RESERVED_KEYS),
31
- profileName: profile.name,
32
- })
33
-
34
- process.exit(exitCode)
35
- },
36
- )
@@ -1,49 +0,0 @@
1
- import { HTTPException } from "hono/http-exception"
2
- import { z } from "zod"
3
- import { factory } from "@/cli/factory"
4
- import { zValidator } from "@/cli/router/validator"
5
-
6
- export const addHelp = `funnel profiles add — add a profile
7
-
8
- usage: funnel profiles add <name> --path <path> --sub-agent <agent> --channel <channel-name> [--brief]
9
-
10
- options:
11
- --path working directory passed to claude as cwd
12
- --sub-agent sub-agent name passed to claude --agent
13
- --channel channel name (resolved to channel id internally)
14
- --brief forward --brief to claude on launch (enables SendUserMessage tool)`
15
-
16
- export const profilesAddHandler = factory.createHandlers(
17
- zValidator("param", z.object({ profile: z.string() })),
18
- zValidator(
19
- "query",
20
- z.object({
21
- path: z.string(),
22
- "sub-agent": z.string(),
23
- channel: z.string(),
24
- brief: z.coerce.boolean().optional(),
25
- }),
26
- addHelp,
27
- ),
28
- (c) => {
29
- const param = c.req.valid("param")
30
- const query = c.req.valid("query")
31
- const funnel = c.var.funnel
32
-
33
- const channel = funnel.channels.get(query.channel)
34
-
35
- if (!channel) {
36
- throw new HTTPException(400, { message: `channel "${query.channel}" not found` })
37
- }
38
-
39
- funnel.profiles.add({
40
- name: param.profile,
41
- path: query.path,
42
- subAgent: query["sub-agent"],
43
- channelId: channel.id,
44
- ...(query.brief !== undefined ? { brief: query.brief } : {}),
45
- })
46
-
47
- return c.text(`added profile "${param.profile}"`)
48
- },
49
- )
@@ -1,20 +0,0 @@
1
- import { z } from "zod"
2
- import { factory } from "@/cli/factory"
3
- import { zValidator } from "@/cli/router/validator"
4
-
5
- export const removeHelp = `funnel profiles remove — remove a profile
6
-
7
- usage: funnel profiles remove <name>`
8
-
9
- export const profilesRemoveHandler = factory.createHandlers(
10
- zValidator("param", z.object({ profile: z.string() })),
11
- zValidator("query", z.object({}), removeHelp),
12
- (c) => {
13
- const param = c.req.valid("param")
14
- const funnel = c.var.funnel
15
-
16
- funnel.profiles.remove(param.profile)
17
-
18
- return c.text(`removed profile "${param.profile}"`)
19
- },
20
- )
@@ -1,45 +0,0 @@
1
- import { HTTPException } from "hono/http-exception"
2
- import { z } from "zod"
3
- import { factory } from "@/cli/factory"
4
- import { zValidator } from "@/cli/router/validator"
5
-
6
- export const setHelp = `funnel profiles <name> set — update a profile
7
-
8
- usage: funnel profiles <name> set [--path <path>] [--sub-agent <agent>] [--channel <channel-name>] [--brief | --no-brief]`
9
-
10
- export const profilesSetHandler = factory.createHandlers(
11
- zValidator("param", z.object({ profile: z.string() })),
12
- zValidator(
13
- "query",
14
- z.object({
15
- path: z.string().optional(),
16
- "sub-agent": z.string().optional(),
17
- channel: z.string().optional(),
18
- brief: z.coerce.boolean().optional(),
19
- "no-brief": z.coerce.boolean().optional(),
20
- }),
21
- setHelp,
22
- ),
23
- (c) => {
24
- const param = c.req.valid("param")
25
- const query = c.req.valid("query")
26
- const funnel = c.var.funnel
27
-
28
- const channel = query.channel !== undefined ? funnel.channels.get(query.channel) : null
29
-
30
- if (query.channel !== undefined && !channel) {
31
- throw new HTTPException(400, { message: `channel "${query.channel}" not found` })
32
- }
33
-
34
- const brief = query["no-brief"] ? false : query.brief
35
-
36
- funnel.profiles.update(param.profile, {
37
- path: query.path,
38
- subAgent: query["sub-agent"],
39
- channelId: channel?.id,
40
- ...(brief !== undefined ? { brief } : {}),
41
- })
42
-
43
- return c.text(`updated profile "${param.profile}"`)
44
- },
45
- )
@@ -1,40 +0,0 @@
1
- import { z } from "zod"
2
- import { factory } from "@/cli/factory"
3
- import { zValidator } from "@/cli/router/validator"
4
-
5
- export const groupHelp = `funnel profiles — manage launch profiles
6
-
7
- usage: funnel profiles [subcommand]
8
-
9
- subcommands:
10
- (none) list (first entry is the default)
11
- add <name> --path <path> --sub-agent <agent> --channel <channel>
12
- <name> set [--path ...] [--sub-agent ...] [--channel ...]
13
- <name> as-default move profile to the front (becomes default)
14
- rename <old> <new> rename
15
- remove <name> remove
16
- <name> run launch (sugar for fnl claude -p <name>)
17
- <name> launch (alias for run)
18
-
19
- examples:
20
- funnel profiles add cto --path /repo/myapp --sub-agent cto --channel prod-inbox
21
- funnel profiles cto as-default
22
- funnel profiles cto run`
23
-
24
- export const profilesGroupHandler = factory.createHandlers(
25
- zValidator("query", z.object({}), groupHelp),
26
- (c) => {
27
- const funnel = c.var.funnel
28
- const profiles = funnel.profiles.list()
29
-
30
- if (profiles.length === 0) return c.text("no profiles")
31
-
32
- const lines = profiles.map((profile, index) => {
33
- const tag = index === 0 ? " (default)" : ""
34
-
35
- return `${profile.name}${tag} [path=${profile.path}, sub-agent=${profile.subAgent}, channel=${profile.channelId}]`
36
- })
37
-
38
- return c.text(lines.join("\n"))
39
- },
40
- )
@@ -1,93 +0,0 @@
1
- import { z } from "zod"
2
- import { factory } from "@/cli/factory"
3
- import { zValidator } from "@/cli/router/validator"
4
-
5
- export const statusHelp = `funnel status — show overall connection status
6
-
7
- usage: funnel status
8
-
9
- Lists configured connectors / channels / profiles, gateway running status,
10
- and active MCP WebSocket clients.`
11
-
12
- type GatewayClient = { channel: string; connectors: string[] }
13
-
14
- type GatewayStatus = {
15
- ok: boolean
16
- clients: GatewayClient[]
17
- }
18
-
19
- const isGatewayStatus = (value: unknown): value is GatewayStatus => {
20
- if (value === null || typeof value !== "object") return false
21
- if (!("clients" in value) || !Array.isArray(value.clients)) return false
22
-
23
- return value.clients.every(
24
- (client: unknown) =>
25
- typeof client === "object" &&
26
- client !== null &&
27
- "channel" in client &&
28
- typeof client.channel === "string" &&
29
- "connectors" in client &&
30
- Array.isArray(client.connectors),
31
- )
32
- }
33
-
34
- export const statusHandler = factory.createHandlers(
35
- zValidator("query", z.object({}), statusHelp),
36
- async (c) => {
37
- const funnel = c.var.funnel
38
- const channels = funnel.channels.list()
39
- const profiles = funnel.profiles.list()
40
- const gatewayStatus = funnel.gateway.getStatus()
41
-
42
- const lines: string[] = []
43
-
44
- lines.push("= funnel status =")
45
- lines.push("")
46
-
47
- lines.push(`channels: ${channels.length}`)
48
- for (const ch of channels) {
49
- const attached =
50
- ch.connectors.length > 0
51
- ? ch.connectors.map((c) => `${c.name}:${c.type}`).join(", ")
52
- : "(none)"
53
- lines.push(` - ${ch.name} [${attached}]`)
54
- }
55
- lines.push("")
56
-
57
- lines.push(`profiles: ${profiles.length}`)
58
- for (const [index, profile] of profiles.entries()) {
59
- const tag = index === 0 ? " (default)" : ""
60
- const channel = funnel.channels.getById(profile.channelId)
61
- const channelLabel = channel ? channel.name : `id:${profile.channelId}`
62
-
63
- lines.push(
64
- ` - ${profile.name}${tag} [path=${profile.path}, sub-agent=${profile.subAgent}, channel=${channelLabel}]`,
65
- )
66
- }
67
- lines.push("")
68
-
69
- if (!gatewayStatus.running) {
70
- lines.push("gateway: not running")
71
- } else {
72
- lines.push(`gateway: running (pid ${gatewayStatus.pid}, port ${gatewayStatus.port})`)
73
-
74
- const res = await fetch(`http://localhost:${gatewayStatus.port}/status`).catch(() => null)
75
-
76
- if (res && res.ok) {
77
- const body: unknown = await res.json()
78
-
79
- if (isGatewayStatus(body)) {
80
- lines.push(` clients: ${body.clients.length}`)
81
-
82
- for (const client of body.clients) {
83
- const connectorList =
84
- client.connectors.length > 0 ? client.connectors.join(", ") : "(none)"
85
- lines.push(` - channel=${client.channel || "(unset)"} [${connectorList}]`)
86
- }
87
- }
88
- }
89
- }
90
-
91
- return c.text(lines.join("\n"))
92
- },
93
- )
@@ -1,27 +0,0 @@
1
- import { HTTPException } from "hono/http-exception"
2
- import { z } from "zod"
3
- import { factory } from "@/cli/factory"
4
- import { NodeFunnelProcessRunner } from "@/engine/process/node-process-runner"
5
- import { zValidator } from "@/cli/router/validator"
6
-
7
- export const updateHelp = `funnel update — update funnel to the latest version
8
-
9
- usage: funnel update
10
-
11
- Runs "bun i -g @interactive-inc/claude-funnel".`
12
-
13
- const PACKAGE = "@interactive-inc/claude-funnel"
14
-
15
- export const updateHandler = factory.createHandlers(
16
- zValidator("query", z.object({}), updateHelp),
17
- async (c) => {
18
- const runner = new NodeFunnelProcessRunner()
19
- const exitCode = await runner.attach(["bun", "i", "-g", PACKAGE])
20
-
21
- if (exitCode !== 0) {
22
- throw new HTTPException(500, { message: `update failed (exit ${exitCode})` })
23
- }
24
-
25
- return c.text(`updated ${PACKAGE}`)
26
- },
27
- )
@@ -1,9 +0,0 @@
1
- export type CallInput = {
2
- method: string
3
- path: string
4
- body?: unknown
5
- }
6
-
7
- export abstract class FunnelConnectorAdapter {
8
- abstract call(input: CallInput): Promise<unknown>
9
- }
@@ -1,16 +0,0 @@
1
- import { z } from "zod"
2
- import { discordConnectorSchema } from "@/connectors/discord-connector-schema"
3
- import { ghConnectorSchema } from "@/connectors/gh-connector-schema"
4
- import { scheduleConnectorSchema } from "@/connectors/schedule-connector-schema"
5
- import { slackConnectorSchema } from "@/connectors/slack-connector-schema"
6
-
7
- export const connectorConfigSchema = z.discriminatedUnion("type", [
8
- slackConnectorSchema,
9
- ghConnectorSchema,
10
- discordConnectorSchema,
11
- scheduleConnectorSchema,
12
- ])
13
-
14
- export type ConnectorConfig = z.infer<typeof connectorConfigSchema>
15
-
16
- export type ConnectorType = ConnectorConfig["type"]
@@ -1,94 +0,0 @@
1
- import type { FunnelConnectorAdapter } from "@/connectors/connector-adapter"
2
- import type { ConnectorConfig } from "@/connectors/connector-config-schema"
3
- import type { FunnelConnectorListener } from "@/connectors/connector-listener"
4
- import { FunnelDiscordAdapter } from "@/connectors/discord-adapter"
5
- import { FunnelDiscordListener } from "@/connectors/discord-listener"
6
- import { FunnelGhAdapter } from "@/connectors/gh-adapter"
7
- import { FunnelGhListener } from "@/connectors/gh-listener"
8
- import { FunnelScheduleListener } from "@/connectors/schedule-listener"
9
- import { ScheduleStateStore } from "@/connectors/schedule-state-store"
10
- import { FunnelSlackAdapter } from "@/connectors/slack-adapter"
11
- import { FunnelSlackListener } from "@/connectors/slack-listener"
12
- import { FunnelFileSystem } from "@/engine/fs/file-system"
13
- import { NodeFunnelFileSystem } from "@/engine/fs/node-file-system"
14
- import { FunnelLogger } from "@/engine/logger/logger"
15
- import { NodeFunnelLogger } from "@/engine/logger/node-logger"
16
- import { FunnelProcessRunner } from "@/engine/process/process-runner"
17
- import { NodeFunnelProcessRunner } from "@/engine/process/node-process-runner"
18
- import { FUNNEL_DIR } from "@/engine/settings/settings-store"
19
- import { join } from "node:path"
20
-
21
- type Deps = {
22
- fs?: FunnelFileSystem
23
- process?: FunnelProcessRunner
24
- logger?: FunnelLogger
25
- dir?: string
26
- }
27
-
28
- const defaultFs = new NodeFunnelFileSystem()
29
- const defaultProcess = new NodeFunnelProcessRunner()
30
- const defaultLogger = new NodeFunnelLogger()
31
-
32
- /**
33
- * Pure factory for per-type listeners and adapters. The factory has no CRUD
34
- * responsibility — connector configs live inside settings.json under their
35
- * channel, and FunnelChannels passes them in by value.
36
- *
37
- * `dir` is the funnel home (defaults to ~/.funnel); per-connector state files
38
- * land at `<dir>/channels/<channel-id>/connectors/<connector-id>/state.json`.
39
- */
40
- export class FunnelConnectorFactory {
41
- private readonly fs: FunnelFileSystem
42
- private readonly process: FunnelProcessRunner
43
- private readonly logger: FunnelLogger
44
- private readonly dir: string
45
-
46
- constructor(deps: Deps = {}) {
47
- this.fs = deps.fs ?? defaultFs
48
- this.process = deps.process ?? defaultProcess
49
- this.logger = deps.logger ?? defaultLogger
50
- this.dir = deps.dir ?? FUNNEL_DIR
51
- Object.freeze(this)
52
- }
53
-
54
- createListener(channelId: string, config: ConnectorConfig): FunnelConnectorListener {
55
- if (config.type === "slack") {
56
- return new FunnelSlackListener({ config, logger: this.logger })
57
- }
58
-
59
- if (config.type === "gh") {
60
- return new FunnelGhListener({ config, process: this.process, logger: this.logger })
61
- }
62
-
63
- if (config.type === "discord") {
64
- return new FunnelDiscordListener({ config, logger: this.logger })
65
- }
66
-
67
- const lastFiredStore = new ScheduleStateStore({
68
- path: join(this.connectorDir(channelId, config.id), "state.json"),
69
- fs: this.fs,
70
- })
71
-
72
- return new FunnelScheduleListener({
73
- config,
74
- lastFiredStore,
75
- logger: this.logger,
76
- })
77
- }
78
-
79
- createAdapter(config: ConnectorConfig): FunnelConnectorAdapter | null {
80
- if (config.type === "slack") return new FunnelSlackAdapter({ config })
81
- if (config.type === "gh") return new FunnelGhAdapter({ process: this.process })
82
- if (config.type === "discord") return new FunnelDiscordAdapter({ config })
83
-
84
- return null
85
- }
86
-
87
- connectorDir(channelId: string, connectorId: string): string {
88
- return join(this.dir, "channels", channelId, "connectors", connectorId)
89
- }
90
-
91
- channelDir(channelId: string): string {
92
- return join(this.dir, "channels", channelId)
93
- }
94
- }
@@ -1,20 +0,0 @@
1
- export type NotifyFn = (content: string, meta?: Record<string, string>) => Promise<void>
2
-
3
- /**
4
- * Long-lived event source for one connector.
5
- *
6
- * `start()` opens the underlying connection (Slack Socket Mode, Discord
7
- * Gateway, GH polling, schedule tick) and pushes events through `notify`.
8
- * `stop()` releases the resources so the supervisor can recreate the listener
9
- * with new config without restarting the whole gateway. `isAlive()` lets the
10
- * supervisor periodically health-check and auto-restart dead listeners; the
11
- * default optimistic implementation is fine for poll/tick-based listeners
12
- * that self-heal.
13
- */
14
- export abstract class FunnelConnectorListener {
15
- abstract start(notify: NotifyFn): Promise<void>
16
- abstract stop(): Promise<void>
17
- isAlive(): boolean {
18
- return true
19
- }
20
- }
@@ -1,51 +0,0 @@
1
- import { FunnelConnectorAdapter, type CallInput } from "@/connectors/connector-adapter"
2
- import { FunnelHttpClient } from "@/engine/http/http-client"
3
- import { NodeFunnelHttpClient } from "@/engine/http/node-http-client"
4
- import type { DiscordConnectorConfig } from "@/connectors/discord-connector-schema"
5
-
6
- const DISCORD_API_BASE = "https://discord.com/api/v10"
7
-
8
- type Deps = {
9
- config: DiscordConnectorConfig
10
- http?: FunnelHttpClient
11
- }
12
-
13
- const defaultHttp = new NodeFunnelHttpClient()
14
-
15
- export class FunnelDiscordAdapter extends FunnelConnectorAdapter {
16
- private readonly token: string
17
- private readonly http: FunnelHttpClient
18
-
19
- constructor(deps: Deps) {
20
- super()
21
- this.token = deps.config.botToken
22
- this.http = deps.http ?? defaultHttp
23
- Object.freeze(this)
24
- }
25
-
26
- async call(input: CallInput): Promise<unknown> {
27
- const method = (input.method || "GET").toUpperCase()
28
- const path = input.path.startsWith("/") ? input.path : `/${input.path}`
29
- const body = input.body
30
- const hasBody =
31
- body !== null && typeof body === "object" && method !== "GET" && Object.keys(body).length > 0
32
-
33
- const res = await this.http.fetch({
34
- method,
35
- url: `${DISCORD_API_BASE}${path}`,
36
- headers: {
37
- Authorization: `Bot ${this.token}`,
38
- "Content-Type": "application/json",
39
- },
40
- body: hasBody ? JSON.stringify(input.body) : undefined,
41
- })
42
-
43
- if (!res.ok) {
44
- throw new Error(`Discord API failed (${res.status}): ${await res.text()}`)
45
- }
46
-
47
- if (res.status === 204) return null
48
-
49
- return await res.json()
50
- }
51
- }
@@ -1,12 +0,0 @@
1
- import { z } from "zod"
2
-
3
- export const discordConnectorSchema = z.object({
4
- id: z.string(),
5
- name: z.string(),
6
- type: z.literal("discord"),
7
- botToken: z.string().min(10),
8
- createdAt: z.string().datetime().optional(),
9
- updatedAt: z.string().datetime().optional(),
10
- })
11
-
12
- export type DiscordConnectorConfig = z.infer<typeof discordConnectorSchema>
@@ -1,48 +0,0 @@
1
- export type DiscordInboundMessage = {
2
- authorId: string
3
- authorIsBot: boolean
4
- channelId: string
5
- guildId: string | null
6
- mentionedUserIds: string[]
7
- raw: unknown
8
- }
9
-
10
- export type DiscordProcessedSkip = { skip: true }
11
-
12
- export type DiscordProcessedEmit = {
13
- skip: false
14
- content: string
15
- meta: Record<string, string>
16
- }
17
-
18
- export type DiscordProcessed = DiscordProcessedSkip | DiscordProcessedEmit
19
-
20
- type Props = {
21
- ownUserId: string
22
- }
23
-
24
- export class FunnelDiscordEventProcessor {
25
- private readonly ownUserId: string
26
-
27
- constructor(props: Props) {
28
- this.ownUserId = props.ownUserId
29
- }
30
-
31
- process(message: DiscordInboundMessage): DiscordProcessed {
32
- if (message.authorIsBot) return { skip: true }
33
-
34
- const mentioned = this.ownUserId ? message.mentionedUserIds.includes(this.ownUserId) : false
35
-
36
- return {
37
- skip: false,
38
- content: JSON.stringify(message.raw),
39
- meta: {
40
- event_type: "discord",
41
- channel_id: message.channelId,
42
- user_id: message.authorId,
43
- mentioned: String(mentioned),
44
- guild_id: message.guildId ?? "",
45
- },
46
- }
47
- }
48
- }
@@ -1,111 +0,0 @@
1
- import { Client, GatewayIntentBits, Partials } from "discord.js"
2
- import { FunnelConnectorListener, type NotifyFn } from "@/connectors/connector-listener"
3
- import { FunnelDiscordEventProcessor } from "@/connectors/discord-event-processor"
4
- import { FunnelLogger } from "@/engine/logger/logger"
5
- import { NodeFunnelLogger } from "@/engine/logger/node-logger"
6
- import type { DiscordConnectorConfig } from "@/connectors/discord-connector-schema"
7
-
8
- type Deps = {
9
- config: DiscordConnectorConfig
10
- logger?: FunnelLogger
11
- }
12
-
13
- const defaultLogger = new NodeFunnelLogger()
14
-
15
- export class FunnelDiscordListener extends FunnelConnectorListener {
16
- private readonly config: DiscordConnectorConfig
17
- private readonly logger: FunnelLogger
18
- private client: Client | null = null
19
-
20
- constructor(deps: Deps) {
21
- super()
22
- this.config = deps.config
23
- this.logger = deps.logger ?? defaultLogger
24
- }
25
-
26
- async start(notify: NotifyFn): Promise<void> {
27
- const client = new Client({
28
- intents: [
29
- GatewayIntentBits.Guilds,
30
- GatewayIntentBits.GuildMessages,
31
- GatewayIntentBits.MessageContent,
32
- GatewayIntentBits.DirectMessages,
33
- ],
34
- partials: [Partials.Channel],
35
- })
36
-
37
- client.on("messageCreate", async (message) => {
38
- const ownUserId = client.user?.id ?? ""
39
- const mentionedUserIds = [...message.mentions.users.keys()]
40
-
41
- this.logger.info("discord messageCreate", {
42
- author: message.author.id,
43
- authorIsBot: String(message.author.bot),
44
- channelId: message.channelId,
45
- guildId: message.guildId ?? "",
46
- mentions: mentionedUserIds.join(","),
47
- ownUserId,
48
- mentioned: String(mentionedUserIds.includes(ownUserId)),
49
- })
50
-
51
- const processor = new FunnelDiscordEventProcessor({ ownUserId })
52
-
53
- const result = processor.process({
54
- authorId: message.author.id,
55
- authorIsBot: message.author.bot,
56
- channelId: message.channelId,
57
- guildId: message.guildId,
58
- mentionedUserIds,
59
- raw: message.toJSON(),
60
- })
61
-
62
- if (result.skip) {
63
- this.logger.info("discord skip", { reason: "bot author" })
64
- return
65
- }
66
-
67
- try {
68
- await notify(result.content, result.meta)
69
- } catch (error) {
70
- this.logger.error("discord notify error", {
71
- error: error instanceof Error ? error.message : String(error),
72
- })
73
- }
74
- })
75
-
76
- client.on("ready", (readyClient) => {
77
- this.logger.info("discord ready", {
78
- userId: readyClient.user.id,
79
- tag: readyClient.user.tag,
80
- guilds: String(readyClient.guilds.cache.size),
81
- })
82
- })
83
-
84
- client.on("error", (error) => {
85
- this.logger.error("discord client error", {
86
- error: error instanceof Error ? error.message : String(error),
87
- })
88
- })
89
-
90
- await client.login(this.config.botToken)
91
- this.client = client
92
- }
93
-
94
- async stop(): Promise<void> {
95
- if (!this.client) return
96
-
97
- try {
98
- await this.client.destroy()
99
- } catch (error) {
100
- this.logger.error("discord stop error", {
101
- error: error instanceof Error ? error.message : String(error),
102
- })
103
- } finally {
104
- this.client = null
105
- }
106
- }
107
-
108
- override isAlive(): boolean {
109
- return this.client !== null
110
- }
111
- }