@interactive-inc/claude-funnel 0.3.0 → 0.4.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.
- package/README.md +71 -21
- package/lib/funnel.ts +46 -2
- package/lib/index.ts +4 -0
- package/lib/modules/channels/channel-connector-ref-updater.ts +4 -0
- package/lib/modules/channels/funnel-channels.ts +49 -7
- package/lib/modules/connectors/connector-config-schema.ts +16 -0
- package/lib/modules/connectors/connector-existence-checker.ts +3 -0
- package/lib/modules/connectors/discord-connector-schema.ts +9 -0
- package/lib/modules/connectors/funnel-callable-connector-store.ts +9 -0
- package/lib/modules/connectors/funnel-connector-stores.ts +24 -0
- package/lib/modules/connectors/funnel-connector-type-store.ts +24 -0
- package/lib/modules/connectors/funnel-connectors.ts +98 -77
- package/lib/modules/connectors/funnel-discord-adapter.ts +1 -1
- package/lib/modules/connectors/funnel-discord-listener.ts +1 -1
- package/lib/modules/connectors/funnel-discord-store.ts +84 -0
- package/lib/modules/connectors/funnel-gh-listener.ts +1 -1
- package/lib/modules/connectors/funnel-gh-store.ts +84 -0
- package/lib/modules/connectors/funnel-json-connector-store.ts +100 -0
- package/lib/modules/connectors/funnel-schedule-listener.ts +124 -0
- package/lib/modules/connectors/funnel-schedule-store.ts +178 -0
- package/lib/modules/connectors/funnel-slack-adapter.ts +1 -1
- package/lib/modules/connectors/funnel-slack-listener.ts +1 -1
- package/lib/modules/connectors/funnel-slack-store.ts +86 -0
- package/lib/modules/connectors/gh-connector-schema.ts +9 -0
- package/lib/modules/connectors/match-cron.ts +72 -0
- package/lib/modules/connectors/migrate-legacy-connectors.ts +77 -0
- package/lib/modules/connectors/schedule-connector-schema.ts +18 -0
- package/lib/modules/connectors/schedule-last-fired-store.ts +48 -0
- package/lib/modules/connectors/slack-connector-schema.ts +10 -0
- package/lib/modules/gateway/daemon.ts +30 -13
- package/lib/modules/profiles/funnel-profiles.ts +18 -0
- package/lib/modules/profiles/profile-channel-checker.ts +3 -0
- package/lib/modules/profiles/profile-channel-ref-updater.ts +3 -0
- package/lib/modules/schedule/funnel-schedule.ts +34 -0
- package/lib/modules/settings/funnel-settings-store.ts +0 -1
- package/lib/modules/settings/mock-funnel-settings-reader.ts +0 -1
- package/lib/modules/settings/settings-schema.ts +0 -34
- package/lib/routes/connectors/add.help.ts +10 -4
- package/lib/routes/connectors/add.ts +10 -1
- package/lib/routes/connectors/routes.ts +6 -0
- package/lib/routes/connectors/schedules-add.help.ts +11 -0
- package/lib/routes/connectors/schedules-add.ts +33 -0
- package/lib/routes/connectors/schedules-group.help.ts +1 -0
- package/lib/routes/connectors/schedules-group.ts +38 -0
- package/lib/routes/connectors/schedules-remove.help.ts +3 -0
- package/lib/routes/connectors/schedules-remove.ts +17 -0
- package/lib/routes/connectors/set.ts +47 -5
- package/lib/routes/connectors/show.ts +9 -0
- package/lib/routes/request/discord.ts +1 -1
- package/lib/routes/request/slack.ts +1 -1
- package/package.json +4 -4
- package/lib/modules/connectors/resolve-listener.ts +0 -13
package/README.md
CHANGED
|
@@ -3,17 +3,32 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@interactive-inc/claude-funnel)
|
|
4
4
|
[](./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
|
-
|
|
14
|
-
(
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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",
|
|
106
|
-
|
|
107
|
-
| { type: "
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
Profile = { name, channel, repo?, subAgent?, envFiles? } launch profile
|
|
156
|
+
Settings = { channels[], repositories[], profiles[] }
|
|
157
|
+
→ ~/.funnel/settings.json
|
|
112
158
|
|
|
113
|
-
|
|
114
|
-
|
|
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,22 +176,26 @@ 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
|
|
|
138
188
|
## Links
|
|
139
189
|
|
|
140
|
-
- [GitHub](https://github.com/interactive-inc/claude-funnel)
|
|
141
|
-
- [Issues](https://github.com/interactive-inc/claude-funnel/issues)
|
|
142
|
-
- Coding rules and design principles: [CLAUDE.md](https://github.com/interactive-inc/claude-funnel/blob/main/CLAUDE.md)
|
|
143
|
-
- Design notes: [`.docs/`](https://github.com/interactive-inc/claude-funnel/tree/main/.docs)
|
|
190
|
+
- [GitHub](https://github.com/interactive-inc/open-claude-funnel)
|
|
191
|
+
- [Issues](https://github.com/interactive-inc/open-claude-funnel/issues)
|
|
192
|
+
- Coding rules and design principles: [CLAUDE.md](https://github.com/interactive-inc/open-claude-funnel/blob/main/CLAUDE.md)
|
|
193
|
+
- Design notes: [`.docs/`](https://github.com/interactive-inc/open-claude-funnel/tree/main/.docs)
|
|
144
194
|
|
|
145
195
|
## Development
|
|
146
196
|
|
|
147
197
|
```bash
|
|
148
|
-
git clone https://github.com/interactive-inc/claude-funnel.git
|
|
198
|
+
git clone https://github.com/interactive-inc/open-claude-funnel.git
|
|
149
199
|
cd claude-funnel
|
|
150
200
|
bun install
|
|
151
201
|
bun link # register funnel / fnl globally
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
@@ -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 (!
|
|
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 (
|
|
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 (!
|
|
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,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 {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
64
|
+
add(config: ConnectorConfig): void {
|
|
65
|
+
if (this.has(config.name)) throw new Error(`connector "${config.name}" already exists`)
|
|
40
66
|
|
|
41
|
-
|
|
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.
|
|
71
|
+
return this.schedule.add(config)
|
|
44
72
|
}
|
|
45
73
|
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
78
|
+
updateGh(name: string, fields: GhUpdateFields): void {
|
|
79
|
+
this.gh.update(name, fields)
|
|
80
|
+
}
|
|
52
81
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
82
|
+
updateDiscord(name: string, fields: DiscordUpdateFields): void {
|
|
83
|
+
this.discord.update(name, fields)
|
|
84
|
+
}
|
|
56
85
|
|
|
57
|
-
|
|
86
|
+
remove(name: string): void {
|
|
87
|
+
const current = this.get(name)
|
|
58
88
|
|
|
59
|
-
|
|
60
|
-
const index = channel.connectors.indexOf(oldName)
|
|
89
|
+
if (!current) throw new Error(`connector "${name}" not found`)
|
|
61
90
|
|
|
62
|
-
|
|
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.
|
|
96
|
+
this.refUpdater.removeRef(name)
|
|
66
97
|
}
|
|
67
98
|
|
|
68
|
-
|
|
69
|
-
const
|
|
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 (!
|
|
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 (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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.
|
|
110
|
+
this.refUpdater.renameRef(oldName, newName)
|
|
85
111
|
}
|
|
86
112
|
|
|
87
|
-
|
|
88
|
-
const
|
|
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 (
|
|
116
|
+
if (!config) throw new Error(`slack connector "${name}" not found`)
|
|
93
117
|
|
|
94
|
-
|
|
118
|
+
return await this.slack.createAdapter(config).call(input)
|
|
119
|
+
}
|
|
95
120
|
|
|
96
|
-
|
|
97
|
-
|
|
121
|
+
async callGh(name: string, input: CallInput): Promise<unknown> {
|
|
122
|
+
const config = this.gh.get(name)
|
|
98
123
|
|
|
99
|
-
|
|
100
|
-
}
|
|
124
|
+
if (!config) throw new Error(`gh connector "${name}" not found`)
|
|
101
125
|
|
|
102
|
-
this.
|
|
126
|
+
return await this.gh.createAdapter(config).call(input)
|
|
103
127
|
}
|
|
104
128
|
|
|
105
|
-
async
|
|
106
|
-
const
|
|
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
|
|
111
|
-
return await new FunnelSlackAdapter({ config: connector }).call(input)
|
|
112
|
-
}
|
|
132
|
+
if (!config) throw new Error(`discord connector "${name}" not found`)
|
|
113
133
|
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
}
|