@interactive-inc/claude-funnel 0.10.0 → 0.10.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 (228) hide show
  1. package/dist/bin.js +448 -448
  2. package/dist/connectors/slack.d.ts +1 -29
  3. package/dist/gateway/daemon.js +166 -166
  4. package/dist/index.d.ts +4 -11
  5. package/dist/index.js +133 -120
  6. package/dist/slack-event-processor-CS-bAit9.d.ts +43 -0
  7. package/package.json +1 -6
  8. package/dist/slack-connector-schema-D7zAHN8k.d.ts +0 -15
  9. package/lib/bin.ts +0 -3
  10. package/lib/cli/factory.ts +0 -10
  11. package/lib/cli/index.ts +0 -85
  12. package/lib/cli/router/query-to-cli-args.ts +0 -20
  13. package/lib/cli/router/to-request.ts +0 -113
  14. package/lib/cli/router/validator.ts +0 -27
  15. package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +0 -27
  16. package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +0 -40
  17. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +0 -41
  18. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +0 -22
  19. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +0 -23
  20. package/lib/cli/routes/channels.$channel.connectors.$connector.ts +0 -26
  21. package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +0 -92
  22. package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +0 -22
  23. package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +0 -63
  24. package/lib/cli/routes/channels.$channel.connectors.ts +0 -26
  25. package/lib/cli/routes/channels.$channel.publish.ts +0 -52
  26. package/lib/cli/routes/channels.$channel.rename.$newName.ts +0 -22
  27. package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +0 -34
  28. package/lib/cli/routes/channels.$channel.ts +0 -34
  29. package/lib/cli/routes/channels.add.$channel.ts +0 -33
  30. package/lib/cli/routes/channels.remove.$channel.ts +0 -20
  31. package/lib/cli/routes/channels.ts +0 -39
  32. package/lib/cli/routes/claude.ts +0 -70
  33. package/lib/cli/routes/gateway.listeners.ts +0 -41
  34. package/lib/cli/routes/gateway.logs.ts +0 -123
  35. package/lib/cli/routes/gateway.restart.ts +0 -50
  36. package/lib/cli/routes/gateway.run.ts +0 -41
  37. package/lib/cli/routes/gateway.start.ts +0 -50
  38. package/lib/cli/routes/gateway.status.ts +0 -19
  39. package/lib/cli/routes/gateway.stop.ts +0 -32
  40. package/lib/cli/routes/gateway.ts +0 -55
  41. package/lib/cli/routes/index.ts +0 -219
  42. package/lib/cli/routes/profiles.$profile.as-default.ts +0 -22
  43. package/lib/cli/routes/profiles.$profile.rename.$newName.ts +0 -22
  44. package/lib/cli/routes/profiles.$profile.run.ts +0 -36
  45. package/lib/cli/routes/profiles.add.$profile.ts +0 -49
  46. package/lib/cli/routes/profiles.remove.$profile.ts +0 -20
  47. package/lib/cli/routes/profiles.set.$profile.ts +0 -45
  48. package/lib/cli/routes/profiles.ts +0 -40
  49. package/lib/cli/routes/status.ts +0 -93
  50. package/lib/cli/routes/update.ts +0 -27
  51. package/lib/connectors/connector-adapter.ts +0 -9
  52. package/lib/connectors/connector-config-schema.ts +0 -16
  53. package/lib/connectors/connector-factory.ts +0 -94
  54. package/lib/connectors/connector-listener.ts +0 -20
  55. package/lib/connectors/discord-adapter.ts +0 -51
  56. package/lib/connectors/discord-connector-schema.ts +0 -12
  57. package/lib/connectors/discord-event-processor.ts +0 -48
  58. package/lib/connectors/discord-listener.ts +0 -111
  59. package/lib/connectors/discord.ts +0 -4
  60. package/lib/connectors/gh-adapter.ts +0 -48
  61. package/lib/connectors/gh-connector-schema.ts +0 -12
  62. package/lib/connectors/gh-listener.ts +0 -137
  63. package/lib/connectors/gh.ts +0 -3
  64. package/lib/connectors/match-cron.ts +0 -78
  65. package/lib/connectors/schedule-connector-schema.ts +0 -33
  66. package/lib/connectors/schedule-listener.ts +0 -207
  67. package/lib/connectors/schedule-state-store.ts +0 -54
  68. package/lib/connectors/schedule.ts +0 -4
  69. package/lib/connectors/slack-adapter.ts +0 -36
  70. package/lib/connectors/slack-connector-schema.ts +0 -13
  71. package/lib/connectors/slack-event-processor.ts +0 -97
  72. package/lib/connectors/slack-listener.ts +0 -97
  73. package/lib/connectors/slack.ts +0 -4
  74. package/lib/engine/channels/channels.ts +0 -520
  75. package/lib/engine/claude/claude.ts +0 -205
  76. package/lib/engine/claude/gateway-controller.ts +0 -4
  77. package/lib/engine/fs/file-system.ts +0 -23
  78. package/lib/engine/fs/memory-file-system.ts +0 -102
  79. package/lib/engine/fs/node-file-system.ts +0 -68
  80. package/lib/engine/http/http-client.ts +0 -17
  81. package/lib/engine/http/memory-http-client.ts +0 -36
  82. package/lib/engine/http/node-http-client.ts +0 -23
  83. package/lib/engine/id/id-generator.ts +0 -7
  84. package/lib/engine/id/memory-id-generator.ts +0 -20
  85. package/lib/engine/id/node-id-generator.ts +0 -7
  86. package/lib/engine/logger/logger.ts +0 -11
  87. package/lib/engine/logger/memory-logger.ts +0 -28
  88. package/lib/engine/logger/node-logger.ts +0 -49
  89. package/lib/engine/logger/noop-logger.ts +0 -9
  90. package/lib/engine/mcp/channel-server.ts +0 -123
  91. package/lib/engine/mcp/channel-subscriber.ts +0 -82
  92. package/lib/engine/mcp/mcp.ts +0 -126
  93. package/lib/engine/mcp/read-channel-connectors.ts +0 -34
  94. package/lib/engine/mcp/read-gateway-token.ts +0 -16
  95. package/lib/engine/mcp/usage-hint-for-type.ts +0 -15
  96. package/lib/engine/process/memory-process-runner.ts +0 -88
  97. package/lib/engine/process/node-process-runner.ts +0 -91
  98. package/lib/engine/process/process-runner.ts +0 -33
  99. package/lib/engine/profiles/profile-channel-checker.ts +0 -7
  100. package/lib/engine/profiles/profiles.ts +0 -126
  101. package/lib/engine/settings/mock-settings-reader.ts +0 -27
  102. package/lib/engine/settings/settings-reader.ts +0 -6
  103. package/lib/engine/settings/settings-schema.ts +0 -48
  104. package/lib/engine/settings/settings-store.ts +0 -110
  105. package/lib/engine/time/clock.ts +0 -15
  106. package/lib/engine/time/memory-clock.ts +0 -26
  107. package/lib/engine/time/node-clock.ts +0 -7
  108. package/lib/funnel.ts +0 -294
  109. package/lib/gateway/auth-middleware.ts +0 -44
  110. package/lib/gateway/broadcaster.ts +0 -319
  111. package/lib/gateway/channel-publisher.ts +0 -67
  112. package/lib/gateway/daemon.ts +0 -47
  113. package/lib/gateway/factory.ts +0 -10
  114. package/lib/gateway/funnel-event-store.ts +0 -155
  115. package/lib/gateway/gateway-server.ts +0 -426
  116. package/lib/gateway/gateway-token.ts +0 -79
  117. package/lib/gateway/gateway.ts +0 -209
  118. package/lib/gateway/kill-competing-slack-gateways.ts +0 -56
  119. package/lib/gateway/listener-supervisor.ts +0 -339
  120. package/lib/gateway/listeners-client.ts +0 -128
  121. package/lib/gateway/publish-schema.ts +0 -27
  122. package/lib/gateway/resolve-daemon-script.ts +0 -26
  123. package/lib/gateway/routes/channels.connectors.call.ts +0 -39
  124. package/lib/gateway/routes/channels.publish.ts +0 -44
  125. package/lib/gateway/routes/health.ts +0 -13
  126. package/lib/gateway/routes/index.ts +0 -26
  127. package/lib/gateway/routes/listeners.list.ts +0 -6
  128. package/lib/gateway/routes/listeners.restart.ts +0 -15
  129. package/lib/gateway/routes/listeners.start.ts +0 -15
  130. package/lib/gateway/routes/listeners.stop.ts +0 -15
  131. package/lib/gateway/routes/route-deps.ts +0 -19
  132. package/lib/gateway/routes/status.ts +0 -15
  133. package/lib/gateway/routes/validator.ts +0 -17
  134. package/lib/index.ts +0 -67
  135. package/lib/logger/leuco-human-file-writer.ts +0 -65
  136. package/lib/logger/leuco-human-logger.ts +0 -98
  137. package/lib/logger/leuco-human-record.ts +0 -16
  138. package/lib/logger/leuco-human-stdout-writer.ts +0 -26
  139. package/lib/logger/leuco-human-writer.ts +0 -14
  140. package/lib/logger/leuco-logger-memory-sink.ts +0 -67
  141. package/lib/logger/leuco-logger-record.ts +0 -13
  142. package/lib/logger/leuco-logger-sink.ts +0 -33
  143. package/lib/logger/leuco-logger-sqlite-sink.ts +0 -355
  144. package/lib/logger/leuco-logger.ts +0 -135
  145. package/lib/tui/app.tsx +0 -357
  146. package/lib/tui/components/add-row.tsx +0 -18
  147. package/lib/tui/components/brand.tsx +0 -27
  148. package/lib/tui/components/card.tsx +0 -44
  149. package/lib/tui/components/detail-bar.tsx +0 -46
  150. package/lib/tui/components/editable-field.tsx +0 -33
  151. package/lib/tui/components/empty-state.tsx +0 -11
  152. package/lib/tui/components/gateway-status.tsx +0 -66
  153. package/lib/tui/components/keymap.tsx +0 -29
  154. package/lib/tui/components/menu-item.tsx +0 -73
  155. package/lib/tui/components/menu.tsx +0 -26
  156. package/lib/tui/components/panel-header.tsx +0 -22
  157. package/lib/tui/components/readonly-field.tsx +0 -18
  158. package/lib/tui/components/section-header.tsx +0 -25
  159. package/lib/tui/components/selection-accent.tsx +0 -32
  160. package/lib/tui/components/session-item.tsx +0 -33
  161. package/lib/tui/components/session-list.tsx +0 -33
  162. package/lib/tui/components/ui/hascii/accordion-item.tsx +0 -88
  163. package/lib/tui/components/ui/hascii/accordion.tsx +0 -96
  164. package/lib/tui/components/ui/hascii/alert-dialog.tsx +0 -43
  165. package/lib/tui/components/ui/hascii/badge.tsx +0 -51
  166. package/lib/tui/components/ui/hascii/breadcrumb.tsx +0 -58
  167. package/lib/tui/components/ui/hascii/button.tsx +0 -194
  168. package/lib/tui/components/ui/hascii/card-content.tsx +0 -14
  169. package/lib/tui/components/ui/hascii/card-description.tsx +0 -13
  170. package/lib/tui/components/ui/hascii/card-footer.tsx +0 -14
  171. package/lib/tui/components/ui/hascii/card-header.tsx +0 -14
  172. package/lib/tui/components/ui/hascii/card-title.tsx +0 -13
  173. package/lib/tui/components/ui/hascii/card.tsx +0 -27
  174. package/lib/tui/components/ui/hascii/checkbox.tsx +0 -65
  175. package/lib/tui/components/ui/hascii/command.tsx +0 -159
  176. package/lib/tui/components/ui/hascii/dialog-content.tsx +0 -14
  177. package/lib/tui/components/ui/hascii/dialog-description.tsx +0 -13
  178. package/lib/tui/components/ui/hascii/dialog-footer.tsx +0 -14
  179. package/lib/tui/components/ui/hascii/dialog-header.tsx +0 -14
  180. package/lib/tui/components/ui/hascii/dialog-title.tsx +0 -13
  181. package/lib/tui/components/ui/hascii/dialog.tsx +0 -27
  182. package/lib/tui/components/ui/hascii/file-tree.tsx +0 -142
  183. package/lib/tui/components/ui/hascii/focus-group.tsx +0 -62
  184. package/lib/tui/components/ui/hascii/form-item.tsx +0 -43
  185. package/lib/tui/components/ui/hascii/input-otp.tsx +0 -86
  186. package/lib/tui/components/ui/hascii/input.tsx +0 -130
  187. package/lib/tui/components/ui/hascii/pagination.tsx +0 -105
  188. package/lib/tui/components/ui/hascii/progress.tsx +0 -28
  189. package/lib/tui/components/ui/hascii/select.tsx +0 -131
  190. package/lib/tui/components/ui/hascii/separator.tsx +0 -35
  191. package/lib/tui/components/ui/hascii/sidebar-content.tsx +0 -23
  192. package/lib/tui/components/ui/hascii/sidebar-header.tsx +0 -14
  193. package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +0 -67
  194. package/lib/tui/components/ui/hascii/sidebar.tsx +0 -24
  195. package/lib/tui/components/ui/hascii/skeleton.tsx +0 -60
  196. package/lib/tui/components/ui/hascii/slider.tsx +0 -91
  197. package/lib/tui/components/ui/hascii/snackbar.tsx +0 -75
  198. package/lib/tui/components/ui/hascii/sparkline.tsx +0 -53
  199. package/lib/tui/components/ui/hascii/spinner.tsx +0 -47
  200. package/lib/tui/components/ui/hascii/stepper.tsx +0 -54
  201. package/lib/tui/components/ui/hascii/switch.tsx +0 -66
  202. package/lib/tui/components/ui/hascii/table.tsx +0 -95
  203. package/lib/tui/components/ui/hascii/tabs.tsx +0 -59
  204. package/lib/tui/components/ui/hascii/toggle-group-item.tsx +0 -45
  205. package/lib/tui/components/ui/hascii/toggle-group.tsx +0 -99
  206. package/lib/tui/components/ui/hascii/tree.tsx +0 -104
  207. package/lib/tui/components/view-shell.tsx +0 -44
  208. package/lib/tui/filter-input.tsx +0 -33
  209. package/lib/tui/hooks/hascii/use-pressable.ts +0 -54
  210. package/lib/tui/parse-comma-list.ts +0 -14
  211. package/lib/tui/profile-launcher.tsx +0 -61
  212. package/lib/tui/scrollbar-options.ts +0 -19
  213. package/lib/tui/sidebar.tsx +0 -50
  214. package/lib/tui/theme.ts +0 -40
  215. package/lib/tui/tui.tsx +0 -20
  216. package/lib/tui/types.ts +0 -38
  217. package/lib/tui/unique-name.ts +0 -18
  218. package/lib/tui/use-event-stream.ts +0 -133
  219. package/lib/tui/use-snapshot.ts +0 -99
  220. package/lib/tui/utils/hascii/form-item-context.tsx +0 -23
  221. package/lib/tui/utils/hascii/input-focus-context.tsx +0 -31
  222. package/lib/tui/utils/hascii/theme-context.tsx +0 -26
  223. package/lib/tui/utils/hascii/theme.ts +0 -176
  224. package/lib/tui/views/channels-view.tsx +0 -108
  225. package/lib/tui/views/connectors-view.tsx +0 -164
  226. package/lib/tui/views/events-view.tsx +0 -160
  227. package/lib/tui/views/listeners-view.tsx +0 -80
  228. package/lib/tui/views/profiles-view.tsx +0 -152
@@ -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
- }
@@ -1,4 +0,0 @@
1
- export * from "@/connectors/discord-adapter"
2
- export * from "@/connectors/discord-connector-schema"
3
- export * from "@/connectors/discord-event-processor"
4
- export * from "@/connectors/discord-listener"
@@ -1,48 +0,0 @@
1
- import { FunnelConnectorAdapter, type CallInput } from "@/connectors/connector-adapter"
2
- import { FunnelProcessRunner } from "@/engine/process/process-runner"
3
- import { NodeFunnelProcessRunner } from "@/engine/process/node-process-runner"
4
-
5
- type Deps = {
6
- process?: FunnelProcessRunner
7
- }
8
-
9
- const defaultProcess = new NodeFunnelProcessRunner()
10
-
11
- export class FunnelGhAdapter extends FunnelConnectorAdapter {
12
- private readonly process: FunnelProcessRunner
13
-
14
- constructor(deps: Deps = {}) {
15
- super()
16
- this.process = deps.process ?? defaultProcess
17
- Object.freeze(this)
18
- }
19
-
20
- async call(input: CallInput): Promise<unknown> {
21
- const args = ["api", input.path]
22
-
23
- if (input.method && input.method.toLowerCase() !== "get") {
24
- args.push("-X", input.method.toUpperCase())
25
- }
26
-
27
- const hasBody =
28
- input.body && typeof input.body === "object" && Object.keys(input.body).length > 0
29
-
30
- if (hasBody) {
31
- args.push("--input", "-")
32
- }
33
-
34
- const result = await this.process.run(["gh", ...args], {
35
- input: hasBody ? JSON.stringify(input.body) : undefined,
36
- })
37
-
38
- if (result.exitCode !== 0) {
39
- throw new Error(`gh api failed: ${result.stderr.trim() || result.stdout.trim()}`)
40
- }
41
-
42
- try {
43
- return JSON.parse(result.stdout)
44
- } catch {
45
- return result.stdout
46
- }
47
- }
48
- }
@@ -1,12 +0,0 @@
1
- import { z } from "zod"
2
-
3
- export const ghConnectorSchema = z.object({
4
- id: z.string(),
5
- name: z.string(),
6
- type: z.literal("gh"),
7
- pollInterval: z.number().int().positive().optional(),
8
- createdAt: z.string().datetime().optional(),
9
- updatedAt: z.string().datetime().optional(),
10
- })
11
-
12
- export type GhConnectorConfig = z.infer<typeof ghConnectorSchema>
@@ -1,137 +0,0 @@
1
- import { z } from "zod"
2
- import { FunnelConnectorListener, type NotifyFn } from "@/connectors/connector-listener"
3
- import { FunnelLogger } from "@/engine/logger/logger"
4
- import { NodeFunnelLogger } from "@/engine/logger/node-logger"
5
- import { FunnelProcessRunner } from "@/engine/process/process-runner"
6
- import { NodeFunnelProcessRunner } from "@/engine/process/node-process-runner"
7
- import type { GhConnectorConfig } from "@/connectors/gh-connector-schema"
8
-
9
- const ghNotificationSchema = z.object({
10
- id: z.string(),
11
- reason: z.string(),
12
- subject: z.object({
13
- type: z.string(),
14
- url: z.string(),
15
- title: z.string(),
16
- }),
17
- repository: z.object({ full_name: z.string() }),
18
- updated_at: z.string(),
19
- })
20
-
21
- const ghNotificationsSchema = z.array(ghNotificationSchema)
22
-
23
- type GhNotification = z.infer<typeof ghNotificationSchema>
24
-
25
- type Deps = {
26
- config: GhConnectorConfig
27
- process?: FunnelProcessRunner
28
- logger?: FunnelLogger
29
- now?: () => Date
30
- }
31
-
32
- const defaultProcess = new NodeFunnelProcessRunner()
33
- const defaultLogger = new NodeFunnelLogger()
34
-
35
- const MAX_SEEN = 10000
36
- const KEEP_SEEN = 5000
37
-
38
- export class FunnelGhListener extends FunnelConnectorListener {
39
- private readonly config: GhConnectorConfig
40
- private readonly process: FunnelProcessRunner
41
- private readonly logger: FunnelLogger
42
- private readonly now: () => Date
43
- private readonly seen = new Map<string, string>()
44
- private bootstrapped = false
45
- private since: string
46
- private timer: ReturnType<typeof setInterval> | null = null
47
-
48
- constructor(deps: Deps) {
49
- super()
50
- this.config = deps.config
51
- this.process = deps.process ?? defaultProcess
52
- this.logger = deps.logger ?? defaultLogger
53
- this.now = deps.now ?? (() => new Date())
54
- this.since = this.now().toISOString()
55
- }
56
-
57
- async start(notify: NotifyFn): Promise<void> {
58
- await this.pollOnce(notify)
59
-
60
- const interval = this.config.pollInterval ?? 60
61
-
62
- this.timer = setInterval(() => void this.pollOnce(notify), interval * 1000)
63
- this.timer.unref()
64
- }
65
-
66
- async stop(): Promise<void> {
67
- if (!this.timer) return
68
-
69
- clearInterval(this.timer)
70
- this.timer = null
71
- }
72
-
73
- override isAlive(): boolean {
74
- return this.timer !== null
75
- }
76
-
77
- async pollOnce(notify: NotifyFn): Promise<void> {
78
- const nextSince = this.now().toISOString()
79
- const params = new URLSearchParams({ since: this.since, all: "false" })
80
-
81
- try {
82
- const result = await this.process.run(["gh", "api", `/notifications?${params}`])
83
-
84
- if (result.exitCode !== 0) {
85
- this.logger.error("gh poll failed", { stderr: result.stderr })
86
- return
87
- }
88
-
89
- const parsed = ghNotificationsSchema.safeParse(JSON.parse(result.stdout))
90
-
91
- if (!parsed.success) {
92
- this.logger.warn("gh response did not match schema", { error: parsed.error.message })
93
- return
94
- }
95
-
96
- const items: GhNotification[] = parsed.data
97
-
98
- for (const item of items) {
99
- if (this.seen.get(item.id) === item.updated_at) continue
100
-
101
- this.seen.set(item.id, item.updated_at)
102
-
103
- if (!this.bootstrapped) continue
104
-
105
- const meta: Record<string, string> = {
106
- event_type: "gh",
107
- reason: item.reason,
108
- subject_type: item.subject.type,
109
- subject_url: item.subject.url,
110
- repository: item.repository.full_name,
111
- thread_id: item.id,
112
- updated_at: item.updated_at,
113
- }
114
-
115
- await notify(JSON.stringify(item), meta)
116
- }
117
-
118
- if (this.seen.size > MAX_SEEN) {
119
- const toDrop = this.seen.size - KEEP_SEEN
120
- let dropped = 0
121
-
122
- for (const key of this.seen.keys()) {
123
- if (dropped >= toDrop) break
124
- this.seen.delete(key)
125
- dropped++
126
- }
127
- }
128
-
129
- this.since = nextSince
130
- this.bootstrapped = true
131
- } catch (error) {
132
- this.logger.error("gh poll error", {
133
- error: error instanceof Error ? error.message : String(error),
134
- })
135
- }
136
- }
137
- }
@@ -1,3 +0,0 @@
1
- export * from "@/connectors/gh-adapter"
2
- export * from "@/connectors/gh-connector-schema"
3
- export * from "@/connectors/gh-listener"
@@ -1,78 +0,0 @@
1
- type Field = { min: number; max: number; values: Set<number> }
2
-
3
- const parseField = (expr: string, min: number, max: number): Field => {
4
- const values = new Set<number>()
5
-
6
- for (const part of expr.split(",")) {
7
- const [rangePart, stepPart] = part.split("/")
8
- const step = stepPart ? Number(stepPart) : 1
9
-
10
- if (!Number.isFinite(step) || step <= 0) {
11
- throw new Error(`invalid cron step: "${stepPart}"`)
12
- }
13
-
14
- let lo = min
15
- let hi = max
16
-
17
- if (rangePart === "*" || rangePart === undefined || rangePart === "") {
18
- lo = min
19
- hi = max
20
- } else if (rangePart.includes("-")) {
21
- const [aStr, bStr] = rangePart.split("-")
22
- const a = Number(aStr)
23
- const b = Number(bStr)
24
-
25
- if (!Number.isFinite(a) || !Number.isFinite(b)) {
26
- throw new Error(`invalid cron range: "${rangePart}"`)
27
- }
28
-
29
- lo = a
30
- hi = b
31
- } else {
32
- const n = Number(rangePart)
33
-
34
- if (!Number.isFinite(n)) throw new Error(`invalid cron value: "${rangePart}"`)
35
-
36
- lo = n
37
- hi = stepPart ? max : n
38
- }
39
-
40
- if (lo < min || hi > max || lo > hi) {
41
- throw new Error(`cron value out of range: ${rangePart} (must be ${min}-${max})`)
42
- }
43
-
44
- for (let i = lo; i <= hi; i += step) {
45
- values.add(i)
46
- }
47
- }
48
-
49
- return { min, max, values }
50
- }
51
-
52
- export const matchCron = (expr: string, date: Date): boolean => {
53
- const parts = expr.trim().split(/\s+/)
54
-
55
- if (parts.length !== 5) {
56
- throw new Error(`cron must have 5 fields (got ${parts.length}): "${expr}"`)
57
- }
58
-
59
- const [minute, hour, dom, month, dow] = parts
60
-
61
- if (!minute || !hour || !dom || !month || !dow) {
62
- throw new Error(`cron has empty fields: "${expr}"`)
63
- }
64
-
65
- const fields = [
66
- { field: parseField(minute, 0, 59), value: date.getMinutes() },
67
- { field: parseField(hour, 0, 23), value: date.getHours() },
68
- { field: parseField(dom, 1, 31), value: date.getDate() },
69
- { field: parseField(month, 1, 12), value: date.getMonth() + 1 },
70
- { field: parseField(dow, 0, 6), value: date.getDay() },
71
- ]
72
-
73
- for (const { field, value } of fields) {
74
- if (!field.values.has(value)) return false
75
- }
76
-
77
- return true
78
- }
@@ -1,33 +0,0 @@
1
- import { z } from "zod"
2
-
3
- /**
4
- * Catch-up behavior when the daemon was down past one or more matching minutes.
5
- *
6
- * - `latest`: fire once with the most recent missed match (default; preserves prior behavior).
7
- * - `all`: fire once per missed minute, oldest first (capped at 24 h).
8
- * - `skip`: never fire missed matches; only fire when the current minute matches.
9
- */
10
- export const scheduleCatchupPolicySchema = z.enum(["latest", "all", "skip"])
11
-
12
- export type ScheduleCatchupPolicy = z.infer<typeof scheduleCatchupPolicySchema>
13
-
14
- export const scheduleEntrySchema = z.object({
15
- id: z.string(),
16
- cron: z.string(),
17
- prompt: z.string(),
18
- enabled: z.boolean().default(true),
19
- catchupPolicy: scheduleCatchupPolicySchema.default("latest"),
20
- })
21
-
22
- export type ScheduleEntry = z.infer<typeof scheduleEntrySchema>
23
-
24
- export const scheduleConnectorSchema = z.object({
25
- id: z.string(),
26
- name: z.string(),
27
- type: z.literal("schedule"),
28
- entries: z.array(scheduleEntrySchema).default([]),
29
- createdAt: z.string().datetime().optional(),
30
- updatedAt: z.string().datetime().optional(),
31
- })
32
-
33
- export type ScheduleConnectorConfig = z.infer<typeof scheduleConnectorSchema>