@interactive-inc/claude-funnel 0.2.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.
- package/README.md +82 -26
- package/lib/funnel.ts +49 -5
- package/lib/index.ts +8 -2
- package/lib/modules/channels/channel-connector-ref-updater.ts +4 -0
- package/lib/modules/channels/funnel-channels.ts +50 -8
- package/lib/modules/claude/funnel-claude.ts +79 -1
- 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/mcp/channel-server.ts +1 -2
- package/lib/modules/profiles/funnel-profiles.ts +123 -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/repos/funnel-repositories.ts +4 -4
- package/lib/modules/router/to-request.ts +2 -5
- package/lib/modules/schedule/funnel-schedule.ts +34 -0
- package/lib/modules/settings/funnel-settings-store.ts +1 -2
- package/lib/modules/settings/mock-funnel-settings-reader.ts +1 -2
- package/lib/modules/settings/settings-schema.ts +3 -37
- package/lib/routes/claude/claude.help.ts +9 -4
- package/lib/routes/claude/claude.ts +44 -7
- package/lib/routes/connectors/add.help.ts +10 -4
- package/lib/routes/connectors/add.ts +10 -1
- package/lib/routes/connectors/routes.ts +6 -2
- 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/profiles/add.help.ts +3 -0
- package/lib/routes/{agents → profiles}/add.ts +4 -4
- package/lib/routes/profiles/group.help.ts +16 -0
- package/lib/routes/profiles/group.ts +25 -0
- package/lib/routes/profiles/launch.help.ts +4 -0
- package/lib/routes/{agents → profiles}/launch.ts +9 -8
- package/lib/routes/profiles/remove.help.ts +3 -0
- package/lib/routes/{agents → profiles}/remove.ts +4 -4
- package/lib/routes/profiles/rename.help.ts +5 -0
- package/lib/routes/{agents → profiles}/rename.ts +4 -4
- package/lib/routes/profiles/routes.ts +18 -0
- package/lib/routes/profiles/set.help.ts +5 -0
- package/lib/routes/{agents → profiles}/set.ts +4 -4
- package/lib/routes/repos/add.help.ts +3 -2
- package/lib/routes/repos/add.ts +3 -2
- package/lib/routes/repos/group.help.ts +1 -1
- package/lib/routes/request/discord-help.ts +9 -0
- package/lib/routes/request/discord.help.ts +19 -0
- package/lib/routes/request/discord.ts +65 -0
- package/lib/routes/request/group.help.ts +15 -0
- package/lib/routes/request/group.ts +9 -0
- package/lib/routes/request/routes.ts +14 -0
- package/lib/routes/request/slack-help.ts +9 -0
- package/lib/routes/request/slack.help.ts +19 -0
- package/lib/routes/{connectors/call.ts → request/slack.ts} +24 -6
- package/lib/routes/status/status.help.ts +1 -1
- package/lib/routes/status/status.ts +7 -7
- package/lib/routes/update/routes.ts +4 -0
- package/lib/routes/update/update.help.ts +5 -0
- package/lib/routes/update/update.ts +21 -0
- package/lib/routes.ts +6 -2
- package/package.json +1 -1
- package/lib/modules/agents/funnel-agents.ts +0 -105
- package/lib/modules/connectors/resolve-listener.ts +0 -13
- package/lib/routes/agents/add.help.ts +0 -3
- package/lib/routes/agents/group.help.ts +0 -13
- package/lib/routes/agents/group.ts +0 -25
- package/lib/routes/agents/launch.help.ts +0 -3
- package/lib/routes/agents/remove.help.ts +0 -3
- package/lib/routes/agents/rename.help.ts +0 -5
- package/lib/routes/agents/routes.ts +0 -17
- package/lib/routes/agents/set.help.ts +0 -5
- package/lib/routes/connectors/call.help.ts +0 -17
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,36 @@ 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
|
+
|
|
93
|
+
fnl request slack post <path> [body] --connector <name> call Slack Web API
|
|
94
|
+
fnl request discord <method> <path> [body] --connector <name> call Discord REST API
|
|
60
95
|
|
|
61
96
|
fnl channels list
|
|
62
97
|
fnl channels add <name>
|
|
@@ -66,27 +101,31 @@ fnl channels <name> connectors detach <connector>
|
|
|
66
101
|
fnl channels rename <old> <new>
|
|
67
102
|
fnl channels remove <name>
|
|
68
103
|
|
|
69
|
-
fnl
|
|
70
|
-
fnl
|
|
71
|
-
fnl
|
|
72
|
-
fnl
|
|
73
|
-
fnl
|
|
74
|
-
fnl
|
|
104
|
+
fnl profiles list launch profiles
|
|
105
|
+
fnl profiles add <name> --channel <c> [--repo <r>] [--sub-agent <s>] [--env-file <f>]
|
|
106
|
+
fnl profiles <name> run launch (sugar for fnl claude)
|
|
107
|
+
fnl profiles <name> launch (alias for run)
|
|
108
|
+
fnl profiles <name> set [--channel ...] [--repo ...] [--sub-agent ...] [--env-file ...]
|
|
109
|
+
fnl profiles rename <old> <new>
|
|
110
|
+
fnl profiles remove <name>
|
|
75
111
|
|
|
76
112
|
fnl repos list repositories (extra)
|
|
77
|
-
fnl repos add <name> --path <path>
|
|
113
|
+
fnl repos add <name> [--path <path>] register funnel MCP (path defaults to cwd)
|
|
78
114
|
fnl repos <name> show details
|
|
79
115
|
fnl repos <name> set [--path <path>]
|
|
80
116
|
fnl repos rename <old> <new>
|
|
81
117
|
fnl repos remove <name>
|
|
82
118
|
|
|
119
|
+
fnl claude launch the "default" profile
|
|
120
|
+
fnl claude --profile <name> launch a named profile
|
|
83
121
|
fnl claude --channel <c> [--repo <r>] [--sub-agent <s>] [--env-file <f>]
|
|
84
|
-
launch
|
|
122
|
+
raw launch (no profile)
|
|
85
123
|
fnl mcp run as an MCP server (invoked from .mcp.json)
|
|
86
124
|
|
|
87
125
|
fnl gateway running status
|
|
88
126
|
fnl gateway start / stop / restart / run / logs
|
|
89
|
-
fnl
|
|
127
|
+
fnl update update funnel via bun i -g
|
|
128
|
+
fnl status overall status (connectors / channels / profiles / repos / gateway)
|
|
90
129
|
|
|
91
130
|
fnl --version
|
|
92
131
|
fnl --help (every subcommand has --help)
|
|
@@ -96,16 +135,29 @@ fnl --help (every subcommand has --help)
|
|
|
96
135
|
|
|
97
136
|
```
|
|
98
137
|
Connector =
|
|
99
|
-
| { type: "slack",
|
|
100
|
-
|
|
101
|
-
| { 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
|
|
102
155
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
Agent = { name, channel, repo?, subAgent?, envFiles? } preset (extra)
|
|
156
|
+
Settings = { channels[], repositories[], profiles[] }
|
|
157
|
+
→ ~/.funnel/settings.json
|
|
106
158
|
|
|
107
|
-
|
|
108
|
-
|
|
159
|
+
Connectors are stored per type, one file per connector:
|
|
160
|
+
→ ~/.funnel/connectors/<type>/<name>.(json|jsonl)
|
|
109
161
|
```
|
|
110
162
|
|
|
111
163
|
## Discord bot setup
|
|
@@ -124,8 +176,12 @@ Settings = { connectors[], channels[], repositories[], agents[] }
|
|
|
124
176
|
|
|
125
177
|
## File layout
|
|
126
178
|
|
|
127
|
-
- 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)
|
|
128
183
|
- PID: `~/.funnel/gateway.pid`
|
|
184
|
+
- Claude PIDs: `~/.funnel/claude/<profile>.pid`
|
|
129
185
|
- Event log: `/tmp/funnel/events/*.jsonl` (auto-deleted after 30 days)
|
|
130
186
|
- Process log: `/tmp/funnel/gateway.log`
|
|
131
187
|
|
package/lib/funnel.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
import { FunnelAgents } from "@/modules/agents/funnel-agents"
|
|
2
1
|
import { FunnelChannels } from "@/modules/channels/funnel-channels"
|
|
3
2
|
import { FunnelClaude } from "@/modules/claude/funnel-claude"
|
|
3
|
+
import {
|
|
4
|
+
type ConnectorStoresBundle,
|
|
5
|
+
createConnectorStores,
|
|
6
|
+
} from "@/modules/connectors/funnel-connector-stores"
|
|
4
7
|
import { FunnelConnectors } from "@/modules/connectors/funnel-connectors"
|
|
8
|
+
import type { FunnelFileSystem } from "@/modules/fs/funnel-file-system"
|
|
5
9
|
import { FunnelGateway } from "@/modules/gateway/funnel-gateway"
|
|
6
10
|
import { FunnelMcp } from "@/modules/mcp/funnel-mcp"
|
|
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,16 +25,51 @@ 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
|
-
get
|
|
28
|
-
return new
|
|
71
|
+
get profiles(): FunnelProfiles {
|
|
72
|
+
return new FunnelProfiles({ store: this.props.store })
|
|
29
73
|
}
|
|
30
74
|
|
|
31
75
|
get repositories(): FunnelRepositories {
|
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,19 +9,23 @@ 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]
|
|
13
17
|
|
|
14
18
|
commands:
|
|
15
19
|
(none) launch TUI
|
|
16
|
-
claude
|
|
20
|
+
claude launch Claude Code (default profile or --profile)
|
|
17
21
|
connectors manage external connections (Slack, etc.)
|
|
18
22
|
channels manage subscription boxes
|
|
19
|
-
|
|
23
|
+
profiles manage launch profiles
|
|
24
|
+
request send an outbound API call via a connector
|
|
20
25
|
repos manage repositories (extra)
|
|
21
26
|
gateway manage the gateway
|
|
22
27
|
status show overall connection status
|
|
28
|
+
update update funnel to the latest version
|
|
23
29
|
mcp run as an MCP server (invoked from .mcp.json)
|
|
24
30
|
|
|
25
31
|
options:
|
|
@@ -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,8 +58,8 @@ export class FunnelChannels {
|
|
|
46
58
|
|
|
47
59
|
if (index < 0) throw new Error(`channel "${name}" not found`)
|
|
48
60
|
|
|
49
|
-
if (
|
|
50
|
-
throw new Error(`channel "${name}" is referenced by
|
|
61
|
+
if (this.profileChecker.hasChannelRef(name)) {
|
|
62
|
+
throw new Error(`channel "${name}" is referenced by a profile`)
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
settings.channels.splice(index, 1)
|
|
@@ -68,11 +80,9 @@ export class FunnelChannels {
|
|
|
68
80
|
|
|
69
81
|
channel.name = newName
|
|
70
82
|
|
|
71
|
-
for (const agent of settings.agents) {
|
|
72
|
-
if (agent.channel === oldName) agent.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
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
1
2
|
import type { FunnelChannels } from "@/modules/channels/funnel-channels"
|
|
2
3
|
import { FunnelFileSystem } from "@/modules/fs/funnel-file-system"
|
|
3
4
|
import { NodeFunnelFileSystem } from "@/modules/fs/node-funnel-file-system"
|
|
@@ -7,6 +8,9 @@ import type { FunnelMcp } from "@/modules/mcp/funnel-mcp"
|
|
|
7
8
|
import { FunnelProcessRunner } from "@/modules/process/funnel-process-runner"
|
|
8
9
|
import { NodeFunnelProcessRunner } from "@/modules/process/node-funnel-process-runner"
|
|
9
10
|
import type { FunnelRepositories } from "@/modules/repos/funnel-repositories"
|
|
11
|
+
import { FUNNEL_DIR } from "@/modules/settings/funnel-settings-store"
|
|
12
|
+
|
|
13
|
+
const CLAUDE_PID_DIR = join(FUNNEL_DIR, "claude")
|
|
10
14
|
|
|
11
15
|
export type LaunchOptions = {
|
|
12
16
|
channel: string
|
|
@@ -14,6 +18,7 @@ export type LaunchOptions = {
|
|
|
14
18
|
subAgent?: string
|
|
15
19
|
envFiles?: string[]
|
|
16
20
|
userArgs?: string[]
|
|
21
|
+
profileName?: string
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
type Deps = {
|
|
@@ -53,6 +58,10 @@ export class FunnelClaude {
|
|
|
53
58
|
throw new Error(`channel "${options.channel}" not found`)
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
if (options.profileName && this.isRunning(options.profileName)) {
|
|
62
|
+
throw new Error(`profile "${options.profileName}" is already running`)
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
const cwd = options.repo
|
|
57
66
|
? this.repositories.resolvePath(options.repo)
|
|
58
67
|
: globalThis.process.cwd()
|
|
@@ -68,6 +77,11 @@ export class FunnelClaude {
|
|
|
68
77
|
await this.gateway.start()
|
|
69
78
|
}
|
|
70
79
|
|
|
80
|
+
if (options.profileName) {
|
|
81
|
+
this.writePidFile(options.profileName)
|
|
82
|
+
this.installCleanup(options.profileName)
|
|
83
|
+
}
|
|
84
|
+
|
|
71
85
|
const claudeArgs = this.buildArgs(options, cwd)
|
|
72
86
|
const env = this.buildEnv(options, cwd)
|
|
73
87
|
|
|
@@ -78,7 +92,71 @@ export class FunnelClaude {
|
|
|
78
92
|
cwd,
|
|
79
93
|
})
|
|
80
94
|
|
|
81
|
-
|
|
95
|
+
try {
|
|
96
|
+
return await this.process.attach(["claude", ...claudeArgs], { cwd, env })
|
|
97
|
+
} finally {
|
|
98
|
+
if (options.profileName) this.removePidFile(options.profileName)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
isRunning(profileName: string): boolean {
|
|
103
|
+
const pid = this.readPid(profileName)
|
|
104
|
+
|
|
105
|
+
if (!pid) return false
|
|
106
|
+
|
|
107
|
+
return this.isProcessAlive(pid)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private pidPath(profileName: string): string {
|
|
111
|
+
return join(CLAUDE_PID_DIR, `${profileName}.pid`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private readPid(profileName: string): number | null {
|
|
115
|
+
const path = this.pidPath(profileName)
|
|
116
|
+
|
|
117
|
+
if (!this.fs.existsSync(path)) return null
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const content = this.fs.readFileSync(path).trim()
|
|
121
|
+
const pid = Number(content)
|
|
122
|
+
|
|
123
|
+
if (!pid || pid <= 0) return null
|
|
124
|
+
|
|
125
|
+
return pid
|
|
126
|
+
} catch {
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private writePidFile(profileName: string): void {
|
|
132
|
+
this.fs.mkdirSync(CLAUDE_PID_DIR, { recursive: true })
|
|
133
|
+
this.fs.writeFileSync(this.pidPath(profileName), String(globalThis.process.pid))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private removePidFile(profileName: string): void {
|
|
137
|
+
const path = this.pidPath(profileName)
|
|
138
|
+
|
|
139
|
+
if (this.fs.existsSync(path)) this.fs.unlink(path)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private installCleanup(profileName: string): void {
|
|
143
|
+
const cleanup = () => this.removePidFile(profileName)
|
|
144
|
+
|
|
145
|
+
globalThis.process.once("exit", cleanup)
|
|
146
|
+
globalThis.process.once("SIGINT", cleanup)
|
|
147
|
+
globalThis.process.once("SIGTERM", cleanup)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private isProcessAlive(pid: number): boolean {
|
|
151
|
+
const result = this.process.runSync(["ps", "-p", String(pid), "-o", "state="])
|
|
152
|
+
|
|
153
|
+
if (result.exitCode !== 0) return false
|
|
154
|
+
|
|
155
|
+
const state = result.stdout.trim()
|
|
156
|
+
|
|
157
|
+
if (!state) return false
|
|
158
|
+
|
|
159
|
+
return !state.startsWith("Z")
|
|
82
160
|
}
|
|
83
161
|
|
|
84
162
|
private buildArgs(options: LaunchOptions, cwd: string): string[] {
|
|
@@ -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
|
+
}
|