@interactive-inc/claude-funnel 0.3.0 → 0.4.0

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 (52) hide show
  1. package/README.md +66 -16
  2. package/lib/funnel.ts +46 -2
  3. package/lib/index.ts +4 -0
  4. package/lib/modules/channels/channel-connector-ref-updater.ts +4 -0
  5. package/lib/modules/channels/funnel-channels.ts +49 -7
  6. package/lib/modules/connectors/connector-config-schema.ts +16 -0
  7. package/lib/modules/connectors/connector-existence-checker.ts +3 -0
  8. package/lib/modules/connectors/discord-connector-schema.ts +9 -0
  9. package/lib/modules/connectors/funnel-callable-connector-store.ts +9 -0
  10. package/lib/modules/connectors/funnel-connector-stores.ts +24 -0
  11. package/lib/modules/connectors/funnel-connector-type-store.ts +24 -0
  12. package/lib/modules/connectors/funnel-connectors.ts +98 -77
  13. package/lib/modules/connectors/funnel-discord-adapter.ts +1 -1
  14. package/lib/modules/connectors/funnel-discord-listener.ts +1 -1
  15. package/lib/modules/connectors/funnel-discord-store.ts +84 -0
  16. package/lib/modules/connectors/funnel-gh-listener.ts +1 -1
  17. package/lib/modules/connectors/funnel-gh-store.ts +84 -0
  18. package/lib/modules/connectors/funnel-json-connector-store.ts +100 -0
  19. package/lib/modules/connectors/funnel-schedule-listener.ts +124 -0
  20. package/lib/modules/connectors/funnel-schedule-store.ts +178 -0
  21. package/lib/modules/connectors/funnel-slack-adapter.ts +1 -1
  22. package/lib/modules/connectors/funnel-slack-listener.ts +1 -1
  23. package/lib/modules/connectors/funnel-slack-store.ts +86 -0
  24. package/lib/modules/connectors/gh-connector-schema.ts +9 -0
  25. package/lib/modules/connectors/match-cron.ts +72 -0
  26. package/lib/modules/connectors/migrate-legacy-connectors.ts +77 -0
  27. package/lib/modules/connectors/schedule-connector-schema.ts +18 -0
  28. package/lib/modules/connectors/schedule-last-fired-store.ts +48 -0
  29. package/lib/modules/connectors/slack-connector-schema.ts +10 -0
  30. package/lib/modules/gateway/daemon.ts +30 -13
  31. package/lib/modules/profiles/funnel-profiles.ts +18 -0
  32. package/lib/modules/profiles/profile-channel-checker.ts +3 -0
  33. package/lib/modules/profiles/profile-channel-ref-updater.ts +3 -0
  34. package/lib/modules/schedule/funnel-schedule.ts +34 -0
  35. package/lib/modules/settings/funnel-settings-store.ts +0 -1
  36. package/lib/modules/settings/mock-funnel-settings-reader.ts +0 -1
  37. package/lib/modules/settings/settings-schema.ts +0 -34
  38. package/lib/routes/connectors/add.help.ts +10 -4
  39. package/lib/routes/connectors/add.ts +10 -1
  40. package/lib/routes/connectors/routes.ts +6 -0
  41. package/lib/routes/connectors/schedules-add.help.ts +11 -0
  42. package/lib/routes/connectors/schedules-add.ts +33 -0
  43. package/lib/routes/connectors/schedules-group.help.ts +1 -0
  44. package/lib/routes/connectors/schedules-group.ts +38 -0
  45. package/lib/routes/connectors/schedules-remove.help.ts +3 -0
  46. package/lib/routes/connectors/schedules-remove.ts +17 -0
  47. package/lib/routes/connectors/set.ts +47 -5
  48. package/lib/routes/connectors/show.ts +9 -0
  49. package/lib/routes/request/discord.ts +1 -1
  50. package/lib/routes/request/slack.ts +1 -1
  51. package/package.json +1 -1
  52. package/lib/modules/connectors/resolve-listener.ts +0 -13
package/README.md CHANGED
@@ -3,17 +3,32 @@
3
3
  [![npm](https://img.shields.io/npm/v/@interactive-inc/claude-funnel.svg)](https://www.npmjs.com/package/@interactive-inc/claude-funnel)
4
4
  [![license](https://img.shields.io/npm/l/@interactive-inc/claude-funnel.svg)](./LICENSE)
5
5
 
6
- A hub CLI that connects multiple Claude Code agents to external services (Slack / GitHub / Discord). External events flow through subscription "channels" into Claude Code sessions, and outbound API calls from Claude are funneled through the same connectors.
6
+ A hub CLI that connects multiple Claude Code agents to external services (Slack / GitHub / Discord) and time-based triggers (cron). External events flow through subscription "channels" into Claude Code sessions, and outbound API calls from Claude are funneled through the same connectors.
7
7
 
8
8
  The command is `funnel` or its shorthand `fnl`.
9
9
 
10
10
  ## Overview
11
11
 
12
12
  ```
13
- Slack/others Connectors Channels Claude Code
14
- (external APIs) ─→ (funnel) ─→ (subscription router) ──WS/MCP─→ (received as <channel> tags)
15
-
16
- funnel MCP server (funnel mcp)
13
+ External sources
14
+ (Slack / GitHub / Discord / cron)
15
+
16
+
17
+ Connectors
18
+ (per-type stores)
19
+
20
+
21
+ Channels
22
+ (subscription router)
23
+
24
+ ▼ WebSocket
25
+ Gateway daemon
26
+
27
+ ▼ MCP (stdio)
28
+ Claude Code
29
+ (events surfaced as <channel> tags;
30
+ outbound calls go back through the
31
+ same connectors via funnel MCP)
17
32
  ```
18
33
 
19
34
  ## Requirements
@@ -47,16 +62,34 @@ fnl gateway start
47
62
  fnl claude --channel my-inbox
48
63
  ```
49
64
 
65
+ Schedule (cron) trigger:
66
+
67
+ ```bash
68
+ # Register a schedule connector and a cron entry
69
+ fnl connectors add daily --type schedule
70
+ fnl connectors daily schedules add --cron "0 9 * * *" --prompt "morning standup"
71
+
72
+ # Attach it to a channel just like any other connector
73
+ fnl channels my-inbox connectors attach daily
74
+ ```
75
+
50
76
  ## Commands
51
77
 
52
78
  ```
53
79
  fnl connectors list
54
- fnl connectors add <name> --type <t> --bot-token <t> --app-token <t>
80
+ fnl connectors add <name> --type slack --bot-token xoxb-... --app-token xapp-...
81
+ fnl connectors add <name> --type gh [--poll-interval <sec>]
82
+ fnl connectors add <name> --type discord --bot-token <token>
83
+ fnl connectors add <name> --type schedule
55
84
  fnl connectors <name> show details
56
- fnl connectors <name> set [--bot-token ...] [--app-token ...]
85
+ fnl connectors <name> set [--bot-token ...] [--app-token ...] [--poll-interval ...]
57
86
  fnl connectors rename <old> <new>
58
87
  fnl connectors remove <name>
59
88
 
89
+ fnl connectors <name> schedules list cron entries
90
+ fnl connectors <name> schedules add --cron "<expr>" --prompt "<text>" [--disabled]
91
+ fnl connectors <name> schedules remove <id>
92
+
60
93
  fnl request slack post <path> [body] --connector <name> call Slack Web API
61
94
  fnl request discord <method> <path> [body] --connector <name> call Discord REST API
62
95
 
@@ -102,16 +135,29 @@ fnl --help (every subcommand has --help)
102
135
 
103
136
  ```
104
137
  Connector =
105
- | { type: "slack", name, botToken, appToken } Slack Socket Mode
106
- | { type: "gh", name, pollInterval? } GitHub (gh CLI)
107
- | { type: "discord", name, botToken } Discord Gateway
138
+ | { type: "slack", name, botToken, appToken }
139
+ Slack Socket Mode
140
+ | { type: "gh", name, pollInterval? }
141
+ GitHub (gh CLI)
142
+ | { type: "discord", name, botToken }
143
+ Discord Gateway
144
+ | { type: "schedule", name, entries[] }
145
+ cron-driven, entries = { id, cron, prompt, enabled }
146
+
147
+ Channel = { name, connectors[] }
148
+ subscription box
149
+
150
+ Repository = { name, path }
151
+ extra
152
+
153
+ Profile = { name, channel, repo?, subAgent?, envFiles? }
154
+ launch profile
108
155
 
109
- Channel = { name, connectors[] } subscription box
110
- Repository = { name, path } extra
111
- Profile = { name, channel, repo?, subAgent?, envFiles? } launch profile
156
+ Settings = { channels[], repositories[], profiles[] }
157
+ ~/.funnel/settings.json
112
158
 
113
- Settings = { connectors[], channels[], repositories[], profiles[] }
114
- → ~/.funnel/settings.json
159
+ Connectors are stored per type, one file per connector:
160
+ → ~/.funnel/connectors/<type>/<name>.(json|jsonl)
115
161
  ```
116
162
 
117
163
  ## Discord bot setup
@@ -130,8 +176,12 @@ Settings = { connectors[], channels[], repositories[], profiles[] }
130
176
 
131
177
  ## File layout
132
178
 
133
- - Config: `~/.funnel/settings.json`
179
+ - Config: `~/.funnel/settings.json` (channels / repositories / profiles)
180
+ - Connectors: `~/.funnel/connectors/<type>/<name>.(json|jsonl)`
181
+ - `slack/<name>.json`, `gh/<name>.json`, `discord/<name>.json`
182
+ - `schedule/<name>.jsonl` (one entry per line) and `schedule/<name>.state.json` (last-fired timestamps for catch-up)
134
183
  - PID: `~/.funnel/gateway.pid`
184
+ - Claude PIDs: `~/.funnel/claude/<profile>.pid`
135
185
  - Event log: `/tmp/funnel/events/*.jsonl` (auto-deleted after 30 days)
136
186
  - Process log: `/tmp/funnel/gateway.log`
137
187
 
package/lib/funnel.ts CHANGED
@@ -1,14 +1,23 @@
1
1
  import { FunnelChannels } from "@/modules/channels/funnel-channels"
2
2
  import { FunnelClaude } from "@/modules/claude/funnel-claude"
3
+ import {
4
+ type ConnectorStoresBundle,
5
+ createConnectorStores,
6
+ } from "@/modules/connectors/funnel-connector-stores"
3
7
  import { FunnelConnectors } from "@/modules/connectors/funnel-connectors"
8
+ import type { FunnelFileSystem } from "@/modules/fs/funnel-file-system"
4
9
  import { FunnelGateway } from "@/modules/gateway/funnel-gateway"
5
10
  import { FunnelMcp } from "@/modules/mcp/funnel-mcp"
6
11
  import { FunnelProfiles } from "@/modules/profiles/funnel-profiles"
7
12
  import { FunnelRepositories } from "@/modules/repos/funnel-repositories"
13
+ import { FunnelSchedule } from "@/modules/schedule/funnel-schedule"
8
14
  import { FunnelSettingsReader } from "@/modules/settings/funnel-settings-reader"
9
15
 
10
16
  type Props = {
11
17
  store: FunnelSettingsReader
18
+ fs?: FunnelFileSystem
19
+ dir?: string
20
+ connectorStores?: ConnectorStoresBundle
12
21
  }
13
22
 
14
23
  export class Funnel {
@@ -16,12 +25,47 @@ export class Funnel {
16
25
  Object.freeze(this)
17
26
  }
18
27
 
28
+ get stores(): ConnectorStoresBundle {
29
+ return (
30
+ this.props.connectorStores ??
31
+ createConnectorStores({ fs: this.props.fs, dir: this.props.dir })
32
+ )
33
+ }
34
+
19
35
  get connectors(): FunnelConnectors {
20
- return new FunnelConnectors({ store: this.props.store })
36
+ const stores = this.stores
37
+ const profiles = this.profiles
38
+ const channels: FunnelChannels = new FunnelChannels({
39
+ store: this.props.store,
40
+ connectorChecker: { has: (name) => connectors.has(name) },
41
+ profileChecker: profiles,
42
+ profileRefUpdater: profiles,
43
+ })
44
+ const connectors: FunnelConnectors = new FunnelConnectors({
45
+ ...stores,
46
+ refUpdater: channels,
47
+ })
48
+ return connectors
21
49
  }
22
50
 
23
51
  get channels(): FunnelChannels {
24
- return new FunnelChannels({ store: this.props.store })
52
+ const stores = this.stores
53
+ const profiles = this.profiles
54
+ const channels: FunnelChannels = new FunnelChannels({
55
+ store: this.props.store,
56
+ connectorChecker: { has: (name) => connectors.has(name) },
57
+ profileChecker: profiles,
58
+ profileRefUpdater: profiles,
59
+ })
60
+ const connectors: FunnelConnectors = new FunnelConnectors({
61
+ ...stores,
62
+ refUpdater: channels,
63
+ })
64
+ return channels
65
+ }
66
+
67
+ get schedule(): FunnelSchedule {
68
+ return new FunnelSchedule({ store: this.stores.schedule })
25
69
  }
26
70
 
27
71
  get profiles(): FunnelProfiles {
package/lib/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import pkg from "../package.json" with { type: "json" }
3
+ import { createConnectorStores } from "@/modules/connectors/funnel-connector-stores"
4
+ import { migrateLegacyConnectors } from "@/modules/connectors/migrate-legacy-connectors"
3
5
  import { startChannelServer } from "@/modules/mcp/channel-server"
4
6
  import { toRequest } from "@/modules/router/to-request"
5
7
  import { launchTui } from "@/modules/tui/tui"
@@ -7,6 +9,8 @@ import { app } from "@/routes"
7
9
 
8
10
  process.title = "funnel"
9
11
 
12
+ migrateLegacyConnectors({ stores: createConnectorStores() })
13
+
10
14
  const HELP = `funnel — Open Claude Funnel
11
15
 
12
16
  usage: funnel [command]
@@ -0,0 +1,4 @@
1
+ export type ChannelConnectorRefUpdater = {
2
+ renameRef(oldName: string, newName: string): void
3
+ removeRef(connectorName: string): void
4
+ }
@@ -1,15 +1,27 @@
1
+ import type { ConnectorExistenceChecker } from "@/modules/connectors/connector-existence-checker"
2
+ import type { ProfileChannelChecker } from "@/modules/profiles/profile-channel-checker"
3
+ import type { ProfileChannelRefUpdater } from "@/modules/profiles/profile-channel-ref-updater"
1
4
  import { FunnelSettingsReader } from "@/modules/settings/funnel-settings-reader"
2
5
  import type { ChannelConfig } from "@/modules/settings/settings-schema"
3
6
 
4
7
  type Deps = {
5
8
  store: FunnelSettingsReader
9
+ connectorChecker: ConnectorExistenceChecker
10
+ profileChecker: ProfileChannelChecker
11
+ profileRefUpdater: ProfileChannelRefUpdater
6
12
  }
7
13
 
8
14
  export class FunnelChannels {
9
15
  private readonly store: FunnelSettingsReader
16
+ private readonly connectorChecker: ConnectorExistenceChecker
17
+ private readonly profileChecker: ProfileChannelChecker
18
+ private readonly profileRefUpdater: ProfileChannelRefUpdater
10
19
 
11
20
  constructor(deps: Deps) {
12
21
  this.store = deps.store
22
+ this.connectorChecker = deps.connectorChecker
23
+ this.profileChecker = deps.profileChecker
24
+ this.profileRefUpdater = deps.profileRefUpdater
13
25
  Object.freeze(this)
14
26
  }
15
27
 
@@ -29,7 +41,7 @@ export class FunnelChannels {
29
41
  }
30
42
 
31
43
  for (const connectorName of config.connectors) {
32
- if (!settings.connectors.some((c) => c.name === connectorName)) {
44
+ if (!this.connectorChecker.has(connectorName)) {
33
45
  throw new Error(`connector "${connectorName}" not found`)
34
46
  }
35
47
  }
@@ -46,7 +58,7 @@ export class FunnelChannels {
46
58
 
47
59
  if (index < 0) throw new Error(`channel "${name}" not found`)
48
60
 
49
- if (settings.profiles.some((p) => p.channel === name)) {
61
+ if (this.profileChecker.hasChannelRef(name)) {
50
62
  throw new Error(`channel "${name}" is referenced by a profile`)
51
63
  }
52
64
 
@@ -68,11 +80,9 @@ export class FunnelChannels {
68
80
 
69
81
  channel.name = newName
70
82
 
71
- for (const profile of settings.profiles) {
72
- if (profile.channel === oldName) profile.channel = newName
73
- }
74
-
75
83
  this.store.write(settings)
84
+
85
+ this.profileRefUpdater.renameChannelRef(oldName, newName)
76
86
  }
77
87
 
78
88
  attachConnector(name: string, connectorName: string): void {
@@ -82,7 +92,7 @@ export class FunnelChannels {
82
92
 
83
93
  if (!channel) throw new Error(`channel "${name}" not found`)
84
94
 
85
- if (!settings.connectors.some((c) => c.name === connectorName)) {
95
+ if (!this.connectorChecker.has(connectorName)) {
86
96
  throw new Error(`connector "${connectorName}" not found`)
87
97
  }
88
98
 
@@ -110,4 +120,36 @@ export class FunnelChannels {
110
120
 
111
121
  this.store.write(settings)
112
122
  }
123
+
124
+ renameRef(oldName: string, newName: string): void {
125
+ const settings = this.store.read()
126
+ let changed = false
127
+
128
+ for (const channel of settings.channels) {
129
+ const i = channel.connectors.indexOf(oldName)
130
+
131
+ if (i >= 0) {
132
+ channel.connectors[i] = newName
133
+ changed = true
134
+ }
135
+ }
136
+
137
+ if (changed) this.store.write(settings)
138
+ }
139
+
140
+ removeRef(connectorName: string): void {
141
+ const settings = this.store.read()
142
+ let changed = false
143
+
144
+ for (const channel of settings.channels) {
145
+ const i = channel.connectors.indexOf(connectorName)
146
+
147
+ if (i >= 0) {
148
+ channel.connectors.splice(i, 1)
149
+ changed = true
150
+ }
151
+ }
152
+
153
+ if (changed) this.store.write(settings)
154
+ }
113
155
  }
@@ -0,0 +1,16 @@
1
+ import { z } from "zod"
2
+ import { discordConnectorSchema } from "@/modules/connectors/discord-connector-schema"
3
+ import { ghConnectorSchema } from "@/modules/connectors/gh-connector-schema"
4
+ import { scheduleConnectorSchema } from "@/modules/connectors/schedule-connector-schema"
5
+ import { slackConnectorSchema } from "@/modules/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"]
@@ -0,0 +1,3 @@
1
+ export type ConnectorExistenceChecker = {
2
+ has(name: string): boolean
3
+ }
@@ -0,0 +1,9 @@
1
+ import { z } from "zod"
2
+
3
+ export const discordConnectorSchema = z.object({
4
+ type: z.literal("discord"),
5
+ name: z.string(),
6
+ botToken: z.string().min(10),
7
+ })
8
+
9
+ export type DiscordConnectorConfig = z.infer<typeof discordConnectorSchema>
@@ -0,0 +1,9 @@
1
+ import type { FunnelConnectorAdapter } from "@/modules/connectors/funnel-connector-adapter"
2
+ import { FunnelConnectorTypeStore } from "@/modules/connectors/funnel-connector-type-store"
3
+ import type { ConnectorConfig } from "@/modules/connectors/connector-config-schema"
4
+
5
+ export abstract class FunnelCallableConnectorStore<
6
+ TConfig extends ConnectorConfig,
7
+ > extends FunnelConnectorTypeStore<TConfig> {
8
+ abstract createAdapter(config: TConfig): FunnelConnectorAdapter
9
+ }
@@ -0,0 +1,24 @@
1
+ import { FunnelDiscordStore } from "@/modules/connectors/funnel-discord-store"
2
+ import { FunnelGhStore } from "@/modules/connectors/funnel-gh-store"
3
+ import { FunnelScheduleStore } from "@/modules/connectors/funnel-schedule-store"
4
+ import { FunnelSlackStore } from "@/modules/connectors/funnel-slack-store"
5
+ import { FunnelFileSystem } from "@/modules/fs/funnel-file-system"
6
+
7
+ export type ConnectorStoresBundle = {
8
+ slack: FunnelSlackStore
9
+ gh: FunnelGhStore
10
+ discord: FunnelDiscordStore
11
+ schedule: FunnelScheduleStore
12
+ }
13
+
14
+ type Deps = {
15
+ fs?: FunnelFileSystem
16
+ dir?: string
17
+ }
18
+
19
+ export const createConnectorStores = (deps: Deps = {}): ConnectorStoresBundle => ({
20
+ slack: new FunnelSlackStore(deps),
21
+ gh: new FunnelGhStore(deps),
22
+ discord: new FunnelDiscordStore(deps),
23
+ schedule: new FunnelScheduleStore(deps),
24
+ })
@@ -0,0 +1,24 @@
1
+ import type { FunnelConnectorListener } from "@/modules/connectors/funnel-connector-listener"
2
+ import type { ConnectorConfig } from "@/modules/connectors/connector-config-schema"
3
+
4
+ export abstract class FunnelConnectorTypeStore<TConfig extends ConnectorConfig> {
5
+ abstract readonly type: TConfig["type"]
6
+
7
+ abstract list(): TConfig[]
8
+
9
+ abstract get(name: string): TConfig | null
10
+
11
+ abstract has(name: string): boolean
12
+
13
+ abstract add(config: TConfig): void
14
+
15
+ abstract remove(name: string): void
16
+
17
+ abstract rename(oldName: string, newName: string): void
18
+
19
+ abstract createListener(config: TConfig): FunnelConnectorListener
20
+
21
+ createAllListeners(): { config: TConfig; listener: FunnelConnectorListener }[] {
22
+ return this.list().map((config) => ({ config, listener: this.createListener(config) }))
23
+ }
24
+ }
@@ -1,124 +1,145 @@
1
+ import type { ChannelConnectorRefUpdater } from "@/modules/channels/channel-connector-ref-updater"
2
+ import type { ConnectorConfig } from "@/modules/connectors/connector-config-schema"
1
3
  import type { CallInput } from "@/modules/connectors/funnel-connector-adapter"
2
- import { FunnelDiscordAdapter } from "@/modules/connectors/funnel-discord-adapter"
3
- import { FunnelGhAdapter } from "@/modules/connectors/funnel-gh-adapter"
4
- import { FunnelSlackAdapter } from "@/modules/connectors/funnel-slack-adapter"
5
- import { FunnelSettingsReader } from "@/modules/settings/funnel-settings-reader"
6
- import type { ConnectorConfig } from "@/modules/settings/settings-schema"
4
+ import type { FunnelConnectorListener } from "@/modules/connectors/funnel-connector-listener"
5
+ import type {
6
+ DiscordUpdateFields,
7
+ FunnelDiscordStore,
8
+ } from "@/modules/connectors/funnel-discord-store"
9
+ import type { FunnelGhStore, GhUpdateFields } from "@/modules/connectors/funnel-gh-store"
10
+ import type { FunnelScheduleStore } from "@/modules/connectors/funnel-schedule-store"
11
+ import type { FunnelSlackStore, SlackUpdateFields } from "@/modules/connectors/funnel-slack-store"
7
12
 
8
13
  type Deps = {
9
- store: FunnelSettingsReader
10
- }
11
-
12
- type UpdateFields = {
13
- botToken?: string
14
- appToken?: string
15
- pollInterval?: number
14
+ slack: FunnelSlackStore
15
+ gh: FunnelGhStore
16
+ discord: FunnelDiscordStore
17
+ schedule: FunnelScheduleStore
18
+ refUpdater: ChannelConnectorRefUpdater
16
19
  }
17
20
 
18
21
  export class FunnelConnectors {
19
- private readonly store: FunnelSettingsReader
22
+ private readonly slack: FunnelSlackStore
23
+ private readonly gh: FunnelGhStore
24
+ private readonly discord: FunnelDiscordStore
25
+ private readonly schedule: FunnelScheduleStore
26
+ private readonly refUpdater: ChannelConnectorRefUpdater
20
27
 
21
28
  constructor(deps: Deps) {
22
- this.store = deps.store
29
+ this.slack = deps.slack
30
+ this.gh = deps.gh
31
+ this.discord = deps.discord
32
+ this.schedule = deps.schedule
33
+ this.refUpdater = deps.refUpdater
23
34
  Object.freeze(this)
24
35
  }
25
36
 
26
37
  list(): ConnectorConfig[] {
27
- return this.store.read().connectors
38
+ return [
39
+ ...this.slack.list(),
40
+ ...this.gh.list(),
41
+ ...this.discord.list(),
42
+ ...this.schedule.list(),
43
+ ]
28
44
  }
29
45
 
30
46
  get(name: string): ConnectorConfig | null {
31
- return this.list().find((c) => c.name === name) ?? null
47
+ return (
48
+ this.slack.get(name) ??
49
+ this.gh.get(name) ??
50
+ this.discord.get(name) ??
51
+ this.schedule.get(name)
52
+ )
32
53
  }
33
54
 
34
- add(config: ConnectorConfig): void {
35
- const settings = this.store.read()
55
+ has(name: string): boolean {
56
+ return (
57
+ this.slack.has(name) ||
58
+ this.gh.has(name) ||
59
+ this.discord.has(name) ||
60
+ this.schedule.has(name)
61
+ )
62
+ }
36
63
 
37
- if (settings.connectors.some((c) => c.name === config.name)) {
38
- throw new Error(`connector "${config.name}" already exists`)
39
- }
64
+ add(config: ConnectorConfig): void {
65
+ if (this.has(config.name)) throw new Error(`connector "${config.name}" already exists`)
40
66
 
41
- settings.connectors.push(config)
67
+ if (config.type === "slack") return this.slack.add(config)
68
+ if (config.type === "gh") return this.gh.add(config)
69
+ if (config.type === "discord") return this.discord.add(config)
42
70
 
43
- this.store.write(settings)
71
+ return this.schedule.add(config)
44
72
  }
45
73
 
46
- rename(oldName: string, newName: string): void {
47
- const settings = this.store.read()
48
-
49
- const connector = settings.connectors.find((c) => c.name === oldName)
74
+ updateSlack(name: string, fields: SlackUpdateFields): void {
75
+ this.slack.update(name, fields)
76
+ }
50
77
 
51
- if (!connector) throw new Error(`connector "${oldName}" not found`)
78
+ updateGh(name: string, fields: GhUpdateFields): void {
79
+ this.gh.update(name, fields)
80
+ }
52
81
 
53
- if (settings.connectors.some((c) => c.name === newName)) {
54
- throw new Error(`connector "${newName}" already exists`)
55
- }
82
+ updateDiscord(name: string, fields: DiscordUpdateFields): void {
83
+ this.discord.update(name, fields)
84
+ }
56
85
 
57
- connector.name = newName
86
+ remove(name: string): void {
87
+ const current = this.get(name)
58
88
 
59
- for (const channel of settings.channels) {
60
- const index = channel.connectors.indexOf(oldName)
89
+ if (!current) throw new Error(`connector "${name}" not found`)
61
90
 
62
- if (index >= 0) channel.connectors[index] = newName
63
- }
91
+ if (current.type === "slack") this.slack.remove(name)
92
+ else if (current.type === "gh") this.gh.remove(name)
93
+ else if (current.type === "discord") this.discord.remove(name)
94
+ else this.schedule.remove(name)
64
95
 
65
- this.store.write(settings)
96
+ this.refUpdater.removeRef(name)
66
97
  }
67
98
 
68
- update(name: string, fields: UpdateFields): void {
69
- const settings = this.store.read()
70
-
71
- const connector = settings.connectors.find((c) => c.name === name)
99
+ rename(oldName: string, newName: string): void {
100
+ const current = this.get(oldName)
72
101
 
73
- if (!connector) throw new Error(`connector "${name}" not found`)
102
+ if (!current) throw new Error(`connector "${oldName}" not found`)
103
+ if (this.has(newName)) throw new Error(`connector "${newName}" already exists`)
74
104
 
75
- if (connector.type === "slack") {
76
- if (fields.botToken !== undefined) connector.botToken = fields.botToken
77
- if (fields.appToken !== undefined) connector.appToken = fields.appToken
78
- } else if (connector.type === "gh") {
79
- if (fields.pollInterval !== undefined) connector.pollInterval = fields.pollInterval
80
- } else if (connector.type === "discord") {
81
- if (fields.botToken !== undefined) connector.botToken = fields.botToken
82
- }
105
+ if (current.type === "slack") this.slack.rename(oldName, newName)
106
+ else if (current.type === "gh") this.gh.rename(oldName, newName)
107
+ else if (current.type === "discord") this.discord.rename(oldName, newName)
108
+ else this.schedule.rename(oldName, newName)
83
109
 
84
- this.store.write(settings)
110
+ this.refUpdater.renameRef(oldName, newName)
85
111
  }
86
112
 
87
- remove(name: string): void {
88
- const settings = this.store.read()
89
-
90
- const index = settings.connectors.findIndex((c) => c.name === name)
113
+ async callSlack(name: string, input: CallInput): Promise<unknown> {
114
+ const config = this.slack.get(name)
91
115
 
92
- if (index < 0) throw new Error(`connector "${name}" not found`)
116
+ if (!config) throw new Error(`slack connector "${name}" not found`)
93
117
 
94
- settings.connectors.splice(index, 1)
118
+ return await this.slack.createAdapter(config).call(input)
119
+ }
95
120
 
96
- for (const channel of settings.channels) {
97
- const ci = channel.connectors.indexOf(name)
121
+ async callGh(name: string, input: CallInput): Promise<unknown> {
122
+ const config = this.gh.get(name)
98
123
 
99
- if (ci >= 0) channel.connectors.splice(ci, 1)
100
- }
124
+ if (!config) throw new Error(`gh connector "${name}" not found`)
101
125
 
102
- this.store.write(settings)
126
+ return await this.gh.createAdapter(config).call(input)
103
127
  }
104
128
 
105
- async call(name: string, input: CallInput): Promise<unknown> {
106
- const connector = this.get(name)
107
-
108
- if (!connector) throw new Error(`connector "${name}" not found`)
129
+ async callDiscord(name: string, input: CallInput): Promise<unknown> {
130
+ const config = this.discord.get(name)
109
131
 
110
- if (connector.type === "slack") {
111
- return await new FunnelSlackAdapter({ config: connector }).call(input)
112
- }
132
+ if (!config) throw new Error(`discord connector "${name}" not found`)
113
133
 
114
- if (connector.type === "gh") {
115
- return await new FunnelGhAdapter().call(input)
116
- }
117
-
118
- if (connector.type === "discord") {
119
- return await new FunnelDiscordAdapter({ config: connector }).call(input)
120
- }
134
+ return await this.discord.createAdapter(config).call(input)
135
+ }
121
136
 
122
- throw new Error(`unsupported connector type: ${(connector as { type: string }).type}`)
137
+ createListeners(): { config: ConnectorConfig; listener: FunnelConnectorListener }[] {
138
+ return [
139
+ ...this.slack.createAllListeners(),
140
+ ...this.gh.createAllListeners(),
141
+ ...this.discord.createAllListeners(),
142
+ ...this.schedule.createAllListeners(),
143
+ ]
123
144
  }
124
145
  }
@@ -4,7 +4,7 @@ import {
4
4
  } from "@/modules/connectors/funnel-connector-adapter"
5
5
  import { FunnelHttpClient } from "@/modules/http/funnel-http-client"
6
6
  import { NodeFunnelHttpClient } from "@/modules/http/node-funnel-http-client"
7
- import type { DiscordConnectorConfig } from "@/modules/settings/settings-schema"
7
+ import type { DiscordConnectorConfig } from "@/modules/connectors/discord-connector-schema"
8
8
 
9
9
  const DISCORD_API_BASE = "https://discord.com/api/v10"
10
10
 
@@ -5,7 +5,7 @@ import {
5
5
  } from "@/modules/connectors/funnel-connector-listener"
6
6
  import { FunnelDiscordEventProcessor } from "@/modules/connectors/funnel-discord-event-processor"
7
7
  import { logger } from "@/modules/logger"
8
- import type { DiscordConnectorConfig } from "@/modules/settings/settings-schema"
8
+ import type { DiscordConnectorConfig } from "@/modules/connectors/discord-connector-schema"
9
9
 
10
10
  type Deps = {
11
11
  config: DiscordConnectorConfig