@interactive-inc/claude-funnel 0.4.0 → 0.7.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 (39) hide show
  1. package/README.md +105 -5
  2. package/lib/api.ts +54 -0
  3. package/lib/funnel.ts +120 -33
  4. package/lib/modules/channels/funnel-channels.ts +5 -0
  5. package/lib/modules/claude/funnel-claude.ts +21 -9
  6. package/lib/modules/connectors/funnel-connector-stores.ts +32 -4
  7. package/lib/modules/connectors/funnel-connectors.ts +6 -0
  8. package/lib/modules/connectors/funnel-discord-listener.ts +9 -3
  9. package/lib/modules/connectors/funnel-discord-store.ts +5 -1
  10. package/lib/modules/connectors/funnel-gh-listener.ts +14 -5
  11. package/lib/modules/connectors/funnel-gh-store.ts +18 -1
  12. package/lib/modules/connectors/funnel-schedule-listener.ts +8 -2
  13. package/lib/modules/connectors/funnel-schedule-store.ts +21 -4
  14. package/lib/modules/connectors/funnel-slack-listener.ts +8 -2
  15. package/lib/modules/connectors/funnel-slack-store.ts +5 -1
  16. package/lib/modules/connectors/migrate-legacy-connectors.ts +5 -1
  17. package/lib/modules/fs/funnel-file-system.ts +5 -0
  18. package/lib/modules/gateway/daemon.ts +10 -143
  19. package/lib/modules/gateway/funnel-gateway-server.ts +241 -0
  20. package/lib/modules/gateway/funnel-gateway.ts +49 -20
  21. package/lib/modules/gateway/kill-competing-slack-gateways.ts +5 -1
  22. package/lib/modules/id/funnel-id-generator.ts +7 -0
  23. package/lib/modules/id/memory-funnel-id-generator.ts +20 -0
  24. package/lib/modules/id/node-funnel-id-generator.ts +7 -0
  25. package/lib/modules/logger/funnel-logger.ts +11 -0
  26. package/lib/modules/logger/memory-funnel-logger.ts +28 -0
  27. package/lib/modules/logger/node-funnel-logger.ts +49 -0
  28. package/lib/modules/logger/noop-funnel-logger.ts +9 -0
  29. package/lib/modules/mcp/funnel-mcp.ts +5 -0
  30. package/lib/modules/process/funnel-process-runner.ts +5 -0
  31. package/lib/modules/profiles/funnel-profiles.ts +5 -0
  32. package/lib/modules/repos/funnel-repositories.ts +5 -0
  33. package/lib/modules/schedule/funnel-schedule.ts +5 -0
  34. package/lib/modules/time/funnel-clock.ts +15 -0
  35. package/lib/modules/time/memory-funnel-clock.ts +26 -0
  36. package/lib/modules/time/node-funnel-clock.ts +7 -0
  37. package/lib/routes/gateway/logs.ts +3 -1
  38. package/package.json +16 -5
  39. package/lib/modules/logger.ts +0 -26
@@ -2,7 +2,8 @@ import {
2
2
  FunnelConnectorListener,
3
3
  type NotifyFn,
4
4
  } from "@/modules/connectors/funnel-connector-listener"
5
- import { logger } from "@/modules/logger"
5
+ import { FunnelLogger } from "@/modules/logger/funnel-logger"
6
+ import { NodeFunnelLogger } from "@/modules/logger/node-funnel-logger"
6
7
  import { FunnelProcessRunner } from "@/modules/process/funnel-process-runner"
7
8
  import { NodeFunnelProcessRunner } from "@/modules/process/node-funnel-process-runner"
8
9
  import type { GhConnectorConfig } from "@/modules/connectors/gh-connector-schema"
@@ -18,9 +19,12 @@ type GhNotification = {
18
19
  type Deps = {
19
20
  config: GhConnectorConfig
20
21
  process?: FunnelProcessRunner
22
+ logger?: FunnelLogger
23
+ now?: () => Date
21
24
  }
22
25
 
23
26
  const defaultProcess = new NodeFunnelProcessRunner()
27
+ const defaultLogger = new NodeFunnelLogger()
24
28
 
25
29
  const MAX_SEEN = 10000
26
30
  const KEEP_SEEN = 5000
@@ -28,14 +32,19 @@ const KEEP_SEEN = 5000
28
32
  export class FunnelGhListener extends FunnelConnectorListener {
29
33
  private readonly config: GhConnectorConfig
30
34
  private readonly process: FunnelProcessRunner
35
+ private readonly logger: FunnelLogger
36
+ private readonly now: () => Date
31
37
  private readonly seen = new Map<string, string>()
32
38
  private bootstrapped = false
33
- private since = new Date().toISOString()
39
+ private since: string
34
40
 
35
41
  constructor(deps: Deps) {
36
42
  super()
37
43
  this.config = deps.config
38
44
  this.process = deps.process ?? defaultProcess
45
+ this.logger = deps.logger ?? defaultLogger
46
+ this.now = deps.now ?? (() => new Date())
47
+ this.since = this.now().toISOString()
39
48
  }
40
49
 
41
50
  async start(notify: NotifyFn): Promise<void> {
@@ -47,14 +56,14 @@ export class FunnelGhListener extends FunnelConnectorListener {
47
56
  }
48
57
 
49
58
  async pollOnce(notify: NotifyFn): Promise<void> {
50
- const nextSince = new Date().toISOString()
59
+ const nextSince = this.now().toISOString()
51
60
  const params = new URLSearchParams({ since: this.since, all: "false" })
52
61
 
53
62
  try {
54
63
  const result = await this.process.run(["gh", "api", `/notifications?${params}`])
55
64
 
56
65
  if (result.exitCode !== 0) {
57
- logger.error("gh poll failed", { stderr: result.stderr })
66
+ this.logger.error("gh poll failed", { stderr: result.stderr })
58
67
  return
59
68
  }
60
69
 
@@ -94,7 +103,7 @@ export class FunnelGhListener extends FunnelConnectorListener {
94
103
  this.since = nextSince
95
104
  this.bootstrapped = true
96
105
  } catch (error) {
97
- logger.error("gh poll error", {
106
+ this.logger.error("gh poll error", {
98
107
  error: error instanceof Error ? error.message : String(error),
99
108
  })
100
109
  }
@@ -12,10 +12,16 @@ import {
12
12
  ghConnectorSchema,
13
13
  } from "@/modules/connectors/gh-connector-schema"
14
14
  import { FunnelFileSystem } from "@/modules/fs/funnel-file-system"
15
+ import type { FunnelLogger } from "@/modules/logger/funnel-logger"
16
+ import type { FunnelProcessRunner } from "@/modules/process/funnel-process-runner"
17
+ import type { FunnelClock } from "@/modules/time/funnel-clock"
15
18
 
16
19
  type Deps = {
17
20
  fs?: FunnelFileSystem
18
21
  dir?: string
22
+ process?: FunnelProcessRunner
23
+ logger?: FunnelLogger
24
+ clock?: FunnelClock
19
25
  }
20
26
 
21
27
  export type GhUpdateFields = {
@@ -25,6 +31,9 @@ export type GhUpdateFields = {
25
31
  export class FunnelGhStore extends FunnelCallableConnectorStore<GhConnectorConfig> {
26
32
  readonly type = "gh" as const
27
33
  private readonly store: FunnelJsonConnectorStore<GhConnectorConfig>
34
+ private readonly process?: FunnelProcessRunner
35
+ private readonly logger?: FunnelLogger
36
+ private readonly clock?: FunnelClock
28
37
 
29
38
  constructor(deps: Deps = {}) {
30
39
  super()
@@ -34,6 +43,9 @@ export class FunnelGhStore extends FunnelCallableConnectorStore<GhConnectorConfi
34
43
  fs: deps.fs,
35
44
  dir: deps.dir ?? DEFAULT_FUNNEL_DIR,
36
45
  })
46
+ this.process = deps.process
47
+ this.logger = deps.logger
48
+ this.clock = deps.clock
37
49
  Object.freeze(this)
38
50
  }
39
51
 
@@ -75,7 +87,12 @@ export class FunnelGhStore extends FunnelCallableConnectorStore<GhConnectorConfi
75
87
  }
76
88
 
77
89
  createListener(config: GhConnectorConfig): FunnelConnectorListener {
78
- return new FunnelGhListener({ config })
90
+ return new FunnelGhListener({
91
+ config,
92
+ process: this.process,
93
+ logger: this.logger,
94
+ now: this.clock ? () => this.clock!.now() : undefined,
95
+ })
79
96
  }
80
97
 
81
98
  createAdapter(_config: GhConnectorConfig): FunnelConnectorAdapter {
@@ -5,22 +5,27 @@ import {
5
5
  import { FunnelScheduleStore } from "@/modules/connectors/funnel-schedule-store"
6
6
  import { matchCron } from "@/modules/connectors/match-cron"
7
7
  import { ScheduleLastFiredStore } from "@/modules/connectors/schedule-last-fired-store"
8
- import { logger } from "@/modules/logger"
8
+ import { FunnelLogger } from "@/modules/logger/funnel-logger"
9
+ import { NodeFunnelLogger } from "@/modules/logger/node-funnel-logger"
9
10
  import type { ScheduleConnectorConfig } from "@/modules/connectors/schedule-connector-schema"
10
11
 
11
12
  type Deps = {
12
13
  config: ScheduleConnectorConfig
13
14
  store: FunnelScheduleStore
14
15
  lastFiredStore: ScheduleLastFiredStore
16
+ logger?: FunnelLogger
15
17
  now?: () => Date
16
18
  }
17
19
 
20
+ const defaultLogger = new NodeFunnelLogger()
21
+
18
22
  const MAX_CATCHUP_MINUTES = 60 * 24
19
23
 
20
24
  export class FunnelScheduleListener extends FunnelConnectorListener {
21
25
  private readonly config: ScheduleConnectorConfig
22
26
  private readonly store: FunnelScheduleStore
23
27
  private readonly lastFiredStore: ScheduleLastFiredStore
28
+ private readonly logger: FunnelLogger
24
29
  private readonly now: () => Date
25
30
 
26
31
  constructor(deps: Deps) {
@@ -28,6 +33,7 @@ export class FunnelScheduleListener extends FunnelConnectorListener {
28
33
  this.config = deps.config
29
34
  this.store = deps.store
30
35
  this.lastFiredStore = deps.lastFiredStore
36
+ this.logger = deps.logger ?? defaultLogger
31
37
  this.now = deps.now ?? (() => new Date())
32
38
  Object.freeze(this)
33
39
  }
@@ -103,7 +109,7 @@ export class FunnelScheduleListener extends FunnelConnectorListener {
103
109
  try {
104
110
  if (matchCron(cron, candidate)) return candidate
105
111
  } catch (error) {
106
- logger.error("invalid cron expression in schedule", {
112
+ this.logger.error("invalid cron expression in schedule", {
107
113
  connector: this.config.name,
108
114
  id: entryId,
109
115
  cron,
@@ -1,7 +1,6 @@
1
1
  import { join } from "node:path"
2
2
  import type { FunnelConnectorListener } from "@/modules/connectors/funnel-connector-listener"
3
3
  import { FunnelConnectorTypeStore } from "@/modules/connectors/funnel-connector-type-store"
4
- import { logger } from "@/modules/logger"
5
4
  import { DEFAULT_FUNNEL_DIR } from "@/modules/connectors/funnel-json-connector-store"
6
5
  import { FunnelScheduleListener } from "@/modules/connectors/funnel-schedule-listener"
7
6
  import { ScheduleLastFiredStore } from "@/modules/connectors/schedule-last-fired-store"
@@ -12,25 +11,41 @@ import {
12
11
  } from "@/modules/connectors/schedule-connector-schema"
13
12
  import { FunnelFileSystem } from "@/modules/fs/funnel-file-system"
14
13
  import { NodeFunnelFileSystem } from "@/modules/fs/node-funnel-file-system"
14
+ import { FunnelIdGenerator } from "@/modules/id/funnel-id-generator"
15
+ import { NodeFunnelIdGenerator } from "@/modules/id/node-funnel-id-generator"
16
+ import { FunnelLogger } from "@/modules/logger/funnel-logger"
17
+ import { NodeFunnelLogger } from "@/modules/logger/node-funnel-logger"
18
+ import type { FunnelClock } from "@/modules/time/funnel-clock"
15
19
 
16
20
  type Deps = {
17
21
  fs?: FunnelFileSystem
18
22
  dir?: string
23
+ logger?: FunnelLogger
24
+ idGenerator?: FunnelIdGenerator
25
+ clock?: FunnelClock
19
26
  }
20
27
 
21
28
  const defaultFs = new NodeFunnelFileSystem()
29
+ const defaultLogger = new NodeFunnelLogger()
30
+ const defaultIdGenerator = new NodeFunnelIdGenerator()
22
31
 
23
32
  export class FunnelScheduleStore extends FunnelConnectorTypeStore<ScheduleConnectorConfig> {
24
33
  readonly type = "schedule" as const
25
34
  private readonly fs: FunnelFileSystem
26
35
  private readonly baseDir: string
27
36
  private readonly dir: string
37
+ private readonly logger: FunnelLogger
38
+ private readonly idGenerator: FunnelIdGenerator
39
+ private readonly clock?: FunnelClock
28
40
 
29
41
  constructor(deps: Deps = {}) {
30
42
  super()
31
43
  this.fs = deps.fs ?? defaultFs
32
44
  this.baseDir = deps.dir ?? DEFAULT_FUNNEL_DIR
33
45
  this.dir = join(this.baseDir, "connectors", "schedule")
46
+ this.logger = deps.logger ?? defaultLogger
47
+ this.idGenerator = deps.idGenerator ?? defaultIdGenerator
48
+ this.clock = deps.clock
34
49
  Object.freeze(this)
35
50
  }
36
51
 
@@ -96,7 +111,7 @@ export class FunnelScheduleStore extends FunnelConnectorTypeStore<ScheduleConnec
96
111
  if (!this.has(name)) throw new Error(`connector "${name}" not found`)
97
112
 
98
113
  const full: ScheduleEntry = {
99
- id: entry.id ?? crypto.randomUUID(),
114
+ id: entry.id ?? this.idGenerator.generate(),
100
115
  cron: entry.cron,
101
116
  prompt: entry.prompt,
102
117
  enabled: entry.enabled ?? true,
@@ -122,6 +137,8 @@ export class FunnelScheduleStore extends FunnelConnectorTypeStore<ScheduleConnec
122
137
  config,
123
138
  store: this,
124
139
  lastFiredStore: this.createLastFiredStore(config.name),
140
+ logger: this.logger,
141
+ now: this.clock ? () => this.clock!.now() : undefined,
125
142
  })
126
143
  }
127
144
 
@@ -155,7 +172,7 @@ export class FunnelScheduleStore extends FunnelConnectorTypeStore<ScheduleConnec
155
172
  const result = scheduleEntrySchema.safeParse(parsed)
156
173
 
157
174
  if (!result.success) {
158
- logger.warn("skipping invalid schedule entry", {
175
+ this.logger.warn("skipping invalid schedule entry", {
159
176
  connector: name,
160
177
  line: lineNumber,
161
178
  issues: result.error.issues.map((iss) => `${iss.path.join(".")}: ${iss.message}`),
@@ -165,7 +182,7 @@ export class FunnelScheduleStore extends FunnelConnectorTypeStore<ScheduleConnec
165
182
 
166
183
  entries.push(result.data)
167
184
  } catch (error) {
168
- logger.warn("skipping unparseable schedule entry", {
185
+ this.logger.warn("skipping unparseable schedule entry", {
169
186
  connector: name,
170
187
  line: lineNumber,
171
188
  error: error instanceof Error ? error.message : String(error),
@@ -4,19 +4,25 @@ import {
4
4
  type NotifyFn,
5
5
  } from "@/modules/connectors/funnel-connector-listener"
6
6
  import { FunnelSlackEventProcessor } from "@/modules/connectors/funnel-slack-event-processor"
7
- import { logger } from "@/modules/logger"
7
+ import { FunnelLogger } from "@/modules/logger/funnel-logger"
8
+ import { NodeFunnelLogger } from "@/modules/logger/node-funnel-logger"
8
9
  import type { SlackConnectorConfig } from "@/modules/connectors/slack-connector-schema"
9
10
 
10
11
  type Deps = {
11
12
  config: SlackConnectorConfig
13
+ logger?: FunnelLogger
12
14
  }
13
15
 
16
+ const defaultLogger = new NodeFunnelLogger()
17
+
14
18
  export class FunnelSlackListener extends FunnelConnectorListener {
15
19
  private readonly config: SlackConnectorConfig
20
+ private readonly logger: FunnelLogger
16
21
 
17
22
  constructor(deps: Deps) {
18
23
  super()
19
24
  this.config = deps.config
25
+ this.logger = deps.logger ?? defaultLogger
20
26
  Object.freeze(this)
21
27
  }
22
28
 
@@ -62,7 +68,7 @@ export class FunnelSlackListener extends FunnelConnectorListener {
62
68
  })
63
69
 
64
70
  app.error(async (error) => {
65
- logger.error("Slack error", {
71
+ this.logger.error("Slack error", {
66
72
  error: error instanceof Error ? error.message : String(error),
67
73
  })
68
74
  })
@@ -12,10 +12,12 @@ import {
12
12
  slackConnectorSchema,
13
13
  } from "@/modules/connectors/slack-connector-schema"
14
14
  import { FunnelFileSystem } from "@/modules/fs/funnel-file-system"
15
+ import type { FunnelLogger } from "@/modules/logger/funnel-logger"
15
16
 
16
17
  type Deps = {
17
18
  fs?: FunnelFileSystem
18
19
  dir?: string
20
+ logger?: FunnelLogger
19
21
  }
20
22
 
21
23
  export type SlackUpdateFields = {
@@ -26,6 +28,7 @@ export type SlackUpdateFields = {
26
28
  export class FunnelSlackStore extends FunnelCallableConnectorStore<SlackConnectorConfig> {
27
29
  readonly type = "slack" as const
28
30
  private readonly store: FunnelJsonConnectorStore<SlackConnectorConfig>
31
+ private readonly logger?: FunnelLogger
29
32
 
30
33
  constructor(deps: Deps = {}) {
31
34
  super()
@@ -35,6 +38,7 @@ export class FunnelSlackStore extends FunnelCallableConnectorStore<SlackConnecto
35
38
  fs: deps.fs,
36
39
  dir: deps.dir ?? DEFAULT_FUNNEL_DIR,
37
40
  })
41
+ this.logger = deps.logger
38
42
  Object.freeze(this)
39
43
  }
40
44
 
@@ -77,7 +81,7 @@ export class FunnelSlackStore extends FunnelCallableConnectorStore<SlackConnecto
77
81
  }
78
82
 
79
83
  createListener(config: SlackConnectorConfig): FunnelConnectorListener {
80
- return new FunnelSlackListener({ config })
84
+ return new FunnelSlackListener({ config, logger: this.logger })
81
85
  }
82
86
 
83
87
  createAdapter(config: SlackConnectorConfig): FunnelConnectorAdapter {
@@ -4,20 +4,24 @@ import type { ConnectorStoresBundle } from "@/modules/connectors/funnel-connecto
4
4
  import { DEFAULT_FUNNEL_DIR } from "@/modules/connectors/funnel-json-connector-store"
5
5
  import { FunnelFileSystem } from "@/modules/fs/funnel-file-system"
6
6
  import { NodeFunnelFileSystem } from "@/modules/fs/node-funnel-file-system"
7
- import { logger } from "@/modules/logger"
7
+ import { FunnelLogger } from "@/modules/logger/funnel-logger"
8
+ import { NodeFunnelLogger } from "@/modules/logger/node-funnel-logger"
8
9
 
9
10
  type Props = {
10
11
  stores: ConnectorStoresBundle
11
12
  fs?: FunnelFileSystem
12
13
  dir?: string
14
+ logger?: FunnelLogger
13
15
  }
14
16
 
15
17
  const defaultFs = new NodeFunnelFileSystem()
18
+ const defaultLogger = new NodeFunnelLogger()
16
19
 
17
20
  export const migrateLegacyConnectors = (props: Props): number => {
18
21
  const fs = props.fs ?? defaultFs
19
22
  const base = props.dir ?? DEFAULT_FUNNEL_DIR
20
23
  const path = join(base, "settings.json")
24
+ const logger = props.logger ?? defaultLogger
21
25
 
22
26
  if (!fs.existsSync(path)) return 0
23
27
 
@@ -2,6 +2,11 @@ export type FileStat = {
2
2
  mtimeMs: number
3
3
  }
4
4
 
5
+ /**
6
+ * Filesystem boundary used everywhere funnel reads or writes.
7
+ * Default is NodeFunnelFileSystem (real `node:fs`); MemoryFunnelFileSystem
8
+ * provides a sandbox for tests and embedded use.
9
+ */
5
10
  export abstract class FunnelFileSystem {
6
11
  abstract existsSync(path: string): boolean
7
12
  abstract readFileSync(path: string): string
@@ -1,16 +1,12 @@
1
1
  #!/usr/bin/env bun
2
2
  import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"
3
3
  import { join } from "node:path"
4
- import type { ServerWebSocket } from "bun"
5
- import { Hono } from "hono"
6
- import { logger } from "@/modules/logger"
7
4
  import { FunnelChannels } from "@/modules/channels/funnel-channels"
8
5
  import { FunnelConnectors } from "@/modules/connectors/funnel-connectors"
9
6
  import { createConnectorStores } from "@/modules/connectors/funnel-connector-stores"
10
7
  import { migrateLegacyConnectors } from "@/modules/connectors/migrate-legacy-connectors"
11
- import { FunnelBroadcaster } from "@/modules/gateway/funnel-broadcaster"
12
- import { FunnelEventLogger } from "@/modules/gateway/funnel-event-logger"
13
- import { killCompetingSlackGateways } from "@/modules/gateway/kill-competing-slack-gateways"
8
+ import { FunnelGatewayServer } from "@/modules/gateway/funnel-gateway-server"
9
+ import { NodeFunnelLogger } from "@/modules/logger/node-funnel-logger"
14
10
  import { FunnelProfiles } from "@/modules/profiles/funnel-profiles"
15
11
  import { FUNNEL_DIR, FunnelSettingsStore } from "@/modules/settings/funnel-settings-store"
16
12
 
@@ -18,6 +14,8 @@ const PORT = Number(process.env.FUNNEL_PORT) || 9742
18
14
  const PID_FILE = join(FUNNEL_DIR, "gateway.pid")
19
15
  const LOG_DIR = "/tmp/funnel/events"
20
16
 
17
+ const logger = new NodeFunnelLogger()
18
+
21
19
  mkdirSync(FUNNEL_DIR, { recursive: true })
22
20
 
23
21
  if (existsSync(PID_FILE)) {
@@ -65,143 +63,12 @@ const connectors: FunnelConnectors = new FunnelConnectors({
65
63
  refUpdater: channels,
66
64
  })
67
65
 
68
- const eventLogger = new FunnelEventLogger({ logDir: LOG_DIR })
69
- const broadcaster = new FunnelBroadcaster()
70
- const app = new Hono()
71
-
72
- app.get("/health", (c) =>
73
- c.json({
74
- ok: true,
75
- pid: process.pid,
76
- clients: broadcaster.getClientCount(),
77
- }),
78
- )
79
-
80
- app.get("/status", (c) =>
81
- c.json({
82
- ok: true,
83
- clients: broadcaster.listChannels(),
84
- }),
85
- )
86
-
87
- const resolveConnectors = (channelName: string): string[] => {
88
- const settings = store.read()
89
- const channel = settings?.channels.find((c) => c.name === channelName)
90
-
91
- return channel?.connectors ?? []
92
- }
93
-
94
- type WsData = { channel: string; connectors: string[] }
95
-
96
- Bun.serve<WsData>({
66
+ const server = new FunnelGatewayServer({
67
+ connectors,
68
+ settings: store,
97
69
  port: PORT,
98
- fetch(request, server) {
99
- const url = new URL(request.url)
100
-
101
- if (url.pathname === "/ws" && request.headers.get("upgrade") === "websocket") {
102
- const channelName = url.searchParams.get("channel") ?? ""
103
- const connectors = channelName ? resolveConnectors(channelName) : []
104
- const data: WsData = { channel: channelName, connectors }
105
-
106
- const upgraded = server.upgrade(request, { data })
107
-
108
- if (upgraded) return undefined
109
-
110
- return new Response("WebSocket upgrade failed", { status: 400 })
111
- }
112
-
113
- return app.fetch(request)
114
- },
115
- websocket: {
116
- open(ws: ServerWebSocket<WsData>) {
117
- const data = ws.data
118
-
119
- broadcaster.addClient(ws, data)
120
-
121
- eventLogger.log("channel connected", {
122
- event_type: "system",
123
- action: "channel_connect",
124
- channel: data.channel,
125
- connectors: data.connectors.join(","),
126
- total: String(broadcaster.getClientCount()),
127
- })
128
- },
129
- close(ws: ServerWebSocket<WsData>) {
130
- broadcaster.removeClient(ws)
131
-
132
- eventLogger.log("channel disconnected", {
133
- event_type: "system",
134
- action: "channel_disconnect",
135
- total: String(broadcaster.getClientCount()),
136
- })
137
- },
138
- message(_ws: ServerWebSocket<WsData>, _message: string | Buffer) {
139
- // Future: channel → gateway messages
140
- },
141
- },
70
+ logDir: LOG_DIR,
71
+ logger,
142
72
  })
143
73
 
144
- eventLogger.log("gateway started", {
145
- event_type: "system",
146
- action: "gateway_start",
147
- port: String(PORT),
148
- pid: String(process.pid),
149
- })
150
-
151
- logger.info(`funnel gateway listening`, {
152
- url: `http://localhost:${PORT}`,
153
- websocket: `ws://localhost:${PORT}/ws`,
154
- health: `http://localhost:${PORT}/health`,
155
- })
156
-
157
- const notify = async (
158
- connectorName: string,
159
- content: string,
160
- meta?: Record<string, string>,
161
- ): Promise<void> => {
162
- const withConnector: Record<string, string> = { ...meta, connector: connectorName }
163
-
164
- eventLogger.log(content, withConnector)
165
- broadcaster.broadcast(content, withConnector)
166
- }
167
-
168
- const allConnectors = connectors.list()
169
-
170
- // Multiple Slack Socket Mode connections sharing one App Token steal DMs/mentions
171
- // from each other. Terminate other bun + gateway/bolt/slack processes first.
172
- if (allConnectors.some((c) => c.type === "slack")) {
173
- const killed = await killCompetingSlackGateways({ selfPid: process.pid })
174
-
175
- if (killed.length > 0) {
176
- eventLogger.log("killed competing Slack gateway processes", {
177
- event_type: "system",
178
- action: "kill_competing",
179
- pids: killed.join(","),
180
- })
181
- }
182
- }
183
-
184
- for (const { config, listener } of connectors.createListeners()) {
185
- const bind = (content: string, meta?: Record<string, string>) =>
186
- notify(config.name, content, meta)
187
-
188
- try {
189
- await listener.start(bind)
190
-
191
- eventLogger.log(`${config.type} listener started: ${config.name}`, {
192
- event_type: "system",
193
- action: `${config.type}_connect`,
194
- connector: config.name,
195
- })
196
-
197
- logger.info(`${config.type} listener started`, { connector: config.name })
198
- } catch (error) {
199
- logger.error(`${config.type} listener failed`, {
200
- connector: config.name,
201
- error: error instanceof Error ? error.message : String(error),
202
- })
203
- }
204
- }
205
-
206
- logger.info(`event logs: ${LOG_DIR}`)
207
- logger.info("funnel gateway running")
74
+ await server.start()