@interactive-inc/claude-funnel 0.2.0 → 0.3.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 +18 -12
- package/lib/funnel.ts +3 -3
- package/lib/index.ts +4 -2
- package/lib/modules/channels/funnel-channels.ts +4 -4
- package/lib/modules/claude/funnel-claude.ts +79 -1
- package/lib/modules/mcp/channel-server.ts +1 -2
- package/lib/modules/{agents/funnel-agents.ts → profiles/funnel-profiles.ts} +25 -25
- package/lib/modules/repos/funnel-repositories.ts +4 -4
- package/lib/modules/router/to-request.ts +2 -5
- package/lib/modules/settings/funnel-settings-store.ts +1 -1
- package/lib/modules/settings/mock-funnel-settings-reader.ts +1 -1
- package/lib/modules/settings/settings-schema.ts +3 -3
- package/lib/routes/claude/claude.help.ts +9 -4
- package/lib/routes/claude/claude.ts +44 -7
- package/lib/routes/connectors/routes.ts +0 -2
- 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/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
|
@@ -56,7 +56,9 @@ fnl connectors <name> show details
|
|
|
56
56
|
fnl connectors <name> set [--bot-token ...] [--app-token ...]
|
|
57
57
|
fnl connectors rename <old> <new>
|
|
58
58
|
fnl connectors remove <name>
|
|
59
|
-
|
|
59
|
+
|
|
60
|
+
fnl request slack post <path> [body] --connector <name> call Slack Web API
|
|
61
|
+
fnl request discord <method> <path> [body] --connector <name> call Discord REST API
|
|
60
62
|
|
|
61
63
|
fnl channels list
|
|
62
64
|
fnl channels add <name>
|
|
@@ -66,27 +68,31 @@ fnl channels <name> connectors detach <connector>
|
|
|
66
68
|
fnl channels rename <old> <new>
|
|
67
69
|
fnl channels remove <name>
|
|
68
70
|
|
|
69
|
-
fnl
|
|
70
|
-
fnl
|
|
71
|
-
fnl
|
|
72
|
-
fnl
|
|
73
|
-
fnl
|
|
74
|
-
fnl
|
|
71
|
+
fnl profiles list launch profiles
|
|
72
|
+
fnl profiles add <name> --channel <c> [--repo <r>] [--sub-agent <s>] [--env-file <f>]
|
|
73
|
+
fnl profiles <name> run launch (sugar for fnl claude)
|
|
74
|
+
fnl profiles <name> launch (alias for run)
|
|
75
|
+
fnl profiles <name> set [--channel ...] [--repo ...] [--sub-agent ...] [--env-file ...]
|
|
76
|
+
fnl profiles rename <old> <new>
|
|
77
|
+
fnl profiles remove <name>
|
|
75
78
|
|
|
76
79
|
fnl repos list repositories (extra)
|
|
77
|
-
fnl repos add <name> --path <path>
|
|
80
|
+
fnl repos add <name> [--path <path>] register funnel MCP (path defaults to cwd)
|
|
78
81
|
fnl repos <name> show details
|
|
79
82
|
fnl repos <name> set [--path <path>]
|
|
80
83
|
fnl repos rename <old> <new>
|
|
81
84
|
fnl repos remove <name>
|
|
82
85
|
|
|
86
|
+
fnl claude launch the "default" profile
|
|
87
|
+
fnl claude --profile <name> launch a named profile
|
|
83
88
|
fnl claude --channel <c> [--repo <r>] [--sub-agent <s>] [--env-file <f>]
|
|
84
|
-
launch
|
|
89
|
+
raw launch (no profile)
|
|
85
90
|
fnl mcp run as an MCP server (invoked from .mcp.json)
|
|
86
91
|
|
|
87
92
|
fnl gateway running status
|
|
88
93
|
fnl gateway start / stop / restart / run / logs
|
|
89
|
-
fnl
|
|
94
|
+
fnl update update funnel via bun i -g
|
|
95
|
+
fnl status overall status (connectors / channels / profiles / repos / gateway)
|
|
90
96
|
|
|
91
97
|
fnl --version
|
|
92
98
|
fnl --help (every subcommand has --help)
|
|
@@ -102,9 +108,9 @@ Connector =
|
|
|
102
108
|
|
|
103
109
|
Channel = { name, connectors[] } subscription box
|
|
104
110
|
Repository = { name, path } extra
|
|
105
|
-
|
|
111
|
+
Profile = { name, channel, repo?, subAgent?, envFiles? } launch profile
|
|
106
112
|
|
|
107
|
-
Settings = { connectors[], channels[], repositories[],
|
|
113
|
+
Settings = { connectors[], channels[], repositories[], profiles[] }
|
|
108
114
|
→ ~/.funnel/settings.json
|
|
109
115
|
```
|
|
110
116
|
|
package/lib/funnel.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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"
|
|
4
3
|
import { FunnelConnectors } from "@/modules/connectors/funnel-connectors"
|
|
5
4
|
import { FunnelGateway } from "@/modules/gateway/funnel-gateway"
|
|
6
5
|
import { FunnelMcp } from "@/modules/mcp/funnel-mcp"
|
|
6
|
+
import { FunnelProfiles } from "@/modules/profiles/funnel-profiles"
|
|
7
7
|
import { FunnelRepositories } from "@/modules/repos/funnel-repositories"
|
|
8
8
|
import { FunnelSettingsReader } from "@/modules/settings/funnel-settings-reader"
|
|
9
9
|
|
|
@@ -24,8 +24,8 @@ export class Funnel {
|
|
|
24
24
|
return new FunnelChannels({ store: this.props.store })
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
get
|
|
28
|
-
return new
|
|
27
|
+
get profiles(): FunnelProfiles {
|
|
28
|
+
return new FunnelProfiles({ store: this.props.store })
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
get repositories(): FunnelRepositories {
|
package/lib/index.ts
CHANGED
|
@@ -13,13 +13,15 @@ usage: funnel [command]
|
|
|
13
13
|
|
|
14
14
|
commands:
|
|
15
15
|
(none) launch TUI
|
|
16
|
-
claude
|
|
16
|
+
claude launch Claude Code (default profile or --profile)
|
|
17
17
|
connectors manage external connections (Slack, etc.)
|
|
18
18
|
channels manage subscription boxes
|
|
19
|
-
|
|
19
|
+
profiles manage launch profiles
|
|
20
|
+
request send an outbound API call via a connector
|
|
20
21
|
repos manage repositories (extra)
|
|
21
22
|
gateway manage the gateway
|
|
22
23
|
status show overall connection status
|
|
24
|
+
update update funnel to the latest version
|
|
23
25
|
mcp run as an MCP server (invoked from .mcp.json)
|
|
24
26
|
|
|
25
27
|
options:
|
|
@@ -46,8 +46,8 @@ export class FunnelChannels {
|
|
|
46
46
|
|
|
47
47
|
if (index < 0) throw new Error(`channel "${name}" not found`)
|
|
48
48
|
|
|
49
|
-
if (settings.
|
|
50
|
-
throw new Error(`channel "${name}" is referenced by
|
|
49
|
+
if (settings.profiles.some((p) => p.channel === name)) {
|
|
50
|
+
throw new Error(`channel "${name}" is referenced by a profile`)
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
settings.channels.splice(index, 1)
|
|
@@ -68,8 +68,8 @@ export class FunnelChannels {
|
|
|
68
68
|
|
|
69
69
|
channel.name = newName
|
|
70
70
|
|
|
71
|
-
for (const
|
|
72
|
-
if (
|
|
71
|
+
for (const profile of settings.profiles) {
|
|
72
|
+
if (profile.channel === oldName) profile.channel = newName
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
this.store.write(settings)
|
|
@@ -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[] {
|
|
@@ -16,8 +16,7 @@ export const startChannelServer = async (): Promise<void> => {
|
|
|
16
16
|
instructions: [
|
|
17
17
|
`Events arrive inside <channel source="${FUNNEL_MCP_NAME}"> tags. Use meta.event_type to discriminate.`,
|
|
18
18
|
"",
|
|
19
|
-
|
|
20
|
-
'event_type="system": system event (connect / disconnect / startup, etc.).',
|
|
19
|
+
"To reply or act on an event, run `funnel request <platform> --help` via the Bash tool (e.g. `funnel request slack --help`). For general CLI usage, run `funnel --help`.",
|
|
21
20
|
].join("\n"),
|
|
22
21
|
},
|
|
23
22
|
)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { FunnelSettingsReader } from "@/modules/settings/funnel-settings-reader"
|
|
2
|
-
import type {
|
|
2
|
+
import type { ProfileConfig } from "@/modules/settings/settings-schema"
|
|
3
3
|
|
|
4
4
|
type Deps = {
|
|
5
5
|
store: FunnelSettingsReader
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export class
|
|
8
|
+
export class FunnelProfiles {
|
|
9
9
|
private readonly store: FunnelSettingsReader
|
|
10
10
|
|
|
11
11
|
constructor(deps: Deps) {
|
|
@@ -13,19 +13,19 @@ export class FunnelAgents {
|
|
|
13
13
|
Object.freeze(this)
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
list():
|
|
17
|
-
return this.store.read().
|
|
16
|
+
list(): ProfileConfig[] {
|
|
17
|
+
return this.store.read().profiles
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
get(name: string):
|
|
21
|
-
return this.list().find((
|
|
20
|
+
get(name: string): ProfileConfig | null {
|
|
21
|
+
return this.list().find((p) => p.name === name) ?? null
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
add(config:
|
|
24
|
+
add(config: ProfileConfig): void {
|
|
25
25
|
const settings = this.store.read()
|
|
26
26
|
|
|
27
|
-
if (settings.
|
|
28
|
-
throw new Error(`
|
|
27
|
+
if (settings.profiles.some((p) => p.name === config.name)) {
|
|
28
|
+
throw new Error(`profile "${config.name}" already exists`)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
if (!settings.channels.some((c) => c.name === config.channel)) {
|
|
@@ -36,7 +36,7 @@ export class FunnelAgents {
|
|
|
36
36
|
throw new Error(`repo "${config.repo}" not found`)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
settings.
|
|
39
|
+
settings.profiles.push(config)
|
|
40
40
|
|
|
41
41
|
this.store.write(settings)
|
|
42
42
|
}
|
|
@@ -44,11 +44,11 @@ export class FunnelAgents {
|
|
|
44
44
|
remove(name: string): void {
|
|
45
45
|
const settings = this.store.read()
|
|
46
46
|
|
|
47
|
-
const index = settings.
|
|
47
|
+
const index = settings.profiles.findIndex((p) => p.name === name)
|
|
48
48
|
|
|
49
|
-
if (index < 0) throw new Error(`
|
|
49
|
+
if (index < 0) throw new Error(`profile "${name}" not found`)
|
|
50
50
|
|
|
51
|
-
settings.
|
|
51
|
+
settings.profiles.splice(index, 1)
|
|
52
52
|
|
|
53
53
|
this.store.write(settings)
|
|
54
54
|
}
|
|
@@ -56,32 +56,32 @@ export class FunnelAgents {
|
|
|
56
56
|
rename(oldName: string, newName: string): void {
|
|
57
57
|
const settings = this.store.read()
|
|
58
58
|
|
|
59
|
-
const
|
|
59
|
+
const profile = settings.profiles.find((p) => p.name === oldName)
|
|
60
60
|
|
|
61
|
-
if (!
|
|
61
|
+
if (!profile) throw new Error(`profile "${oldName}" not found`)
|
|
62
62
|
|
|
63
|
-
if (settings.
|
|
64
|
-
throw new Error(`
|
|
63
|
+
if (settings.profiles.some((p) => p.name === newName)) {
|
|
64
|
+
throw new Error(`profile "${newName}" already exists`)
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
profile.name = newName
|
|
68
68
|
|
|
69
69
|
this.store.write(settings)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
update(name: string, fields: Partial<Omit<
|
|
72
|
+
update(name: string, fields: Partial<Omit<ProfileConfig, "name">>): void {
|
|
73
73
|
const settings = this.store.read()
|
|
74
74
|
|
|
75
|
-
const
|
|
75
|
+
const profile = settings.profiles.find((p) => p.name === name)
|
|
76
76
|
|
|
77
|
-
if (!
|
|
77
|
+
if (!profile) throw new Error(`profile "${name}" not found`)
|
|
78
78
|
|
|
79
79
|
if (fields.channel !== undefined) {
|
|
80
80
|
if (!settings.channels.some((c) => c.name === fields.channel)) {
|
|
81
81
|
throw new Error(`channel "${fields.channel}" not found`)
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
profile.channel = fields.channel
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
if (fields.repo !== undefined) {
|
|
@@ -89,15 +89,15 @@ export class FunnelAgents {
|
|
|
89
89
|
throw new Error(`repo "${fields.repo}" not found`)
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
profile.repo = fields.repo || undefined
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
if (fields.subAgent !== undefined) {
|
|
96
|
-
|
|
96
|
+
profile.subAgent = fields.subAgent || undefined
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
if (fields.envFiles !== undefined) {
|
|
100
|
-
|
|
100
|
+
profile.envFiles = fields.envFiles
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
this.store.write(settings)
|
|
@@ -46,8 +46,8 @@ export class FunnelRepositories {
|
|
|
46
46
|
|
|
47
47
|
if (index < 0) throw new Error(`repo "${name}" not found`)
|
|
48
48
|
|
|
49
|
-
if (settings.
|
|
50
|
-
throw new Error(`repo "${name}" is referenced by
|
|
49
|
+
if (settings.profiles.some((p) => p.repo === name)) {
|
|
50
|
+
throw new Error(`repo "${name}" is referenced by a profile`)
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const repo = settings.repositories[index]!
|
|
@@ -72,8 +72,8 @@ export class FunnelRepositories {
|
|
|
72
72
|
|
|
73
73
|
repo.name = newName
|
|
74
74
|
|
|
75
|
-
for (const
|
|
76
|
-
if (
|
|
75
|
+
for (const profile of settings.profiles) {
|
|
76
|
+
if (profile.repo === oldName) profile.repo = newName
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
this.store.write(settings)
|
|
@@ -7,7 +7,6 @@ const STRIPPED_METHOD_KEYWORDS: Record<string, string> = {
|
|
|
7
7
|
add: "POST",
|
|
8
8
|
remove: "DELETE",
|
|
9
9
|
set: "PUT",
|
|
10
|
-
update: "PUT",
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
const KEPT_METHOD_KEYWORDS: Record<string, string> = {
|
|
@@ -24,8 +23,6 @@ const isValue = (arg: string | undefined): arg is string => {
|
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
const consumeApiCall = (args: string[], i: number, params: URLSearchParams): number => {
|
|
27
|
-
params.set("method", args[i]!)
|
|
28
|
-
|
|
29
26
|
const nextPath = args[i + 1]
|
|
30
27
|
|
|
31
28
|
if (!isValue(nextPath)) return 1
|
|
@@ -99,8 +96,8 @@ export const toRequest = (args: string[]) => {
|
|
|
99
96
|
continue
|
|
100
97
|
}
|
|
101
98
|
|
|
102
|
-
if (API_CALL_METHODS.has(arg) && !params.has("
|
|
103
|
-
segments.push(
|
|
99
|
+
if (API_CALL_METHODS.has(arg) && !params.has("path")) {
|
|
100
|
+
segments.push(arg)
|
|
104
101
|
i += consumeApiCall(args, i, params)
|
|
105
102
|
continue
|
|
106
103
|
}
|
|
@@ -47,7 +47,7 @@ export const repositoryConfigSchema = z.object({
|
|
|
47
47
|
|
|
48
48
|
export type RepositoryConfig = z.infer<typeof repositoryConfigSchema>
|
|
49
49
|
|
|
50
|
-
export const
|
|
50
|
+
export const profileConfigSchema = z.object({
|
|
51
51
|
name: z.string(),
|
|
52
52
|
channel: z.string(),
|
|
53
53
|
repo: z.string().optional(),
|
|
@@ -55,13 +55,13 @@ export const agentConfigSchema = z.object({
|
|
|
55
55
|
envFiles: z.array(z.string()).optional(),
|
|
56
56
|
})
|
|
57
57
|
|
|
58
|
-
export type
|
|
58
|
+
export type ProfileConfig = z.infer<typeof profileConfigSchema>
|
|
59
59
|
|
|
60
60
|
export const settingsSchema = z.object({
|
|
61
61
|
connectors: z.array(connectorConfigSchema).default([]),
|
|
62
62
|
channels: z.array(channelConfigSchema).default([]),
|
|
63
63
|
repositories: z.array(repositoryConfigSchema).default([]),
|
|
64
|
-
|
|
64
|
+
profiles: z.array(profileConfigSchema).default([]),
|
|
65
65
|
})
|
|
66
66
|
|
|
67
67
|
export type Settings = z.infer<typeof settingsSchema>
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
export const help = `funnel claude — launch Claude Code
|
|
2
2
|
|
|
3
|
-
usage:
|
|
3
|
+
usage:
|
|
4
|
+
funnel claude launch "default" profile (or the first profile)
|
|
5
|
+
funnel claude --profile <name> launch a named profile
|
|
6
|
+
funnel claude --channel <name> [opts] raw launch (no profile)
|
|
4
7
|
|
|
5
|
-
options:
|
|
6
|
-
--
|
|
7
|
-
--
|
|
8
|
+
options (override profile / raw):
|
|
9
|
+
--profile profile name to launch
|
|
10
|
+
--channel channel name to subscribe to
|
|
11
|
+
--repo switch working directory to the named repo
|
|
8
12
|
--sub-agent sub-agent name (passed to claude --agent)
|
|
9
13
|
--env-file additional env file to load (relative path)
|
|
10
14
|
|
|
15
|
+
Any other arguments are forwarded to the claude CLI.
|
|
11
16
|
On launch the FUNNEL_CHANNEL_ID env var is set and MCP connects to the gateway.`
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
+
import { HTTPException } from "hono/http-exception"
|
|
1
2
|
import { z } from "zod"
|
|
2
3
|
import { factory } from "@/factory"
|
|
3
4
|
import { queryToCliArgs } from "@/modules/router/query-to-cli-args"
|
|
4
5
|
import { zValidator } from "@/modules/router/validator"
|
|
5
6
|
import { help } from "@/routes/claude/claude.help"
|
|
6
7
|
|
|
7
|
-
const RESERVED_KEYS = ["channel", "repo", "sub-agent", "env-file"]
|
|
8
|
+
const RESERVED_KEYS = ["profile", "channel", "repo", "sub-agent", "env-file"]
|
|
9
|
+
|
|
10
|
+
const DEFAULT_PROFILE_NAME = "default"
|
|
8
11
|
|
|
9
12
|
export const claudeHandler = factory.createHandlers(
|
|
10
13
|
zValidator(
|
|
11
14
|
"query",
|
|
12
15
|
z
|
|
13
16
|
.object({
|
|
17
|
+
profile: z.string().optional(),
|
|
14
18
|
channel: z.string().optional(),
|
|
15
19
|
repo: z.string().optional(),
|
|
16
20
|
"sub-agent": z.string().optional(),
|
|
@@ -21,17 +25,50 @@ export const claudeHandler = factory.createHandlers(
|
|
|
21
25
|
),
|
|
22
26
|
async (c) => {
|
|
23
27
|
const query = c.req.valid("query")
|
|
28
|
+
const funnel = c.var.funnel
|
|
24
29
|
|
|
25
|
-
if (
|
|
30
|
+
if (query.profile) {
|
|
31
|
+
const profile = funnel.profiles.get(query.profile)
|
|
26
32
|
|
|
27
|
-
|
|
33
|
+
if (!profile) {
|
|
34
|
+
throw new HTTPException(404, { message: `profile "${query.profile}" not found` })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const exitCode = await funnel.claude.launch({
|
|
38
|
+
channel: query.channel ?? profile.channel,
|
|
39
|
+
repo: query.repo ?? profile.repo,
|
|
40
|
+
subAgent: query["sub-agent"] ?? profile.subAgent,
|
|
41
|
+
envFiles: query["env-file"] ? [query["env-file"]] : profile.envFiles,
|
|
42
|
+
userArgs: queryToCliArgs(c.req.url, RESERVED_KEYS),
|
|
43
|
+
profileName: profile.name,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
process.exit(exitCode)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (query.channel) {
|
|
50
|
+
const exitCode = await funnel.claude.launch({
|
|
51
|
+
channel: query.channel,
|
|
52
|
+
repo: query.repo,
|
|
53
|
+
subAgent: query["sub-agent"],
|
|
54
|
+
envFiles: query["env-file"] ? [query["env-file"]] : undefined,
|
|
55
|
+
userArgs: queryToCliArgs(c.req.url, RESERVED_KEYS),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
process.exit(exitCode)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const defaultProfile = funnel.profiles.get(DEFAULT_PROFILE_NAME) ?? funnel.profiles.list()[0]
|
|
62
|
+
|
|
63
|
+
if (!defaultProfile) return c.text(help)
|
|
28
64
|
|
|
29
65
|
const exitCode = await funnel.claude.launch({
|
|
30
|
-
channel:
|
|
31
|
-
repo: query.repo,
|
|
32
|
-
subAgent: query["sub-agent"],
|
|
33
|
-
envFiles: query["env-file"] ? [query["env-file"]] :
|
|
66
|
+
channel: defaultProfile.channel,
|
|
67
|
+
repo: query.repo ?? defaultProfile.repo,
|
|
68
|
+
subAgent: query["sub-agent"] ?? defaultProfile.subAgent,
|
|
69
|
+
envFiles: query["env-file"] ? [query["env-file"]] : defaultProfile.envFiles,
|
|
34
70
|
userArgs: queryToCliArgs(c.req.url, RESERVED_KEYS),
|
|
71
|
+
profileName: defaultProfile.name,
|
|
35
72
|
})
|
|
36
73
|
|
|
37
74
|
process.exit(exitCode)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { factory } from "@/factory"
|
|
2
2
|
import { connectorsAddHandler } from "@/routes/connectors/add"
|
|
3
|
-
import { connectorsCallHandler } from "@/routes/connectors/call"
|
|
4
3
|
import { connectorsGroupHandler } from "@/routes/connectors/group"
|
|
5
4
|
import { connectorsRemoveHandler } from "@/routes/connectors/remove"
|
|
6
5
|
import { connectorsRenameHandler } from "@/routes/connectors/rename"
|
|
@@ -10,7 +9,6 @@ import { connectorsShowHandler } from "@/routes/connectors/show"
|
|
|
10
9
|
export const connectorsRoutes = factory
|
|
11
10
|
.createApp()
|
|
12
11
|
.get("/", ...connectorsGroupHandler)
|
|
13
|
-
.get("/:name/call", ...connectorsCallHandler)
|
|
14
12
|
.put("/:name/rename/:newName", ...connectorsRenameHandler)
|
|
15
13
|
.put("/rename/:name/:newName", ...connectorsRenameHandler)
|
|
16
14
|
.post("/:name", ...connectorsAddHandler)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { z } from "zod"
|
|
2
2
|
import { factory } from "@/factory"
|
|
3
3
|
import { zValidator } from "@/modules/router/validator"
|
|
4
|
-
import { help } from "@/routes/
|
|
4
|
+
import { help } from "@/routes/profiles/add.help"
|
|
5
5
|
|
|
6
|
-
export const
|
|
6
|
+
export const profilesAddHandler = factory.createHandlers(
|
|
7
7
|
zValidator("param", z.object({ name: z.string() })),
|
|
8
8
|
zValidator(
|
|
9
9
|
"query",
|
|
@@ -20,7 +20,7 @@ export const agentsAddHandler = factory.createHandlers(
|
|
|
20
20
|
const query = c.req.valid("query")
|
|
21
21
|
const funnel = c.var.funnel
|
|
22
22
|
|
|
23
|
-
funnel.
|
|
23
|
+
funnel.profiles.add({
|
|
24
24
|
name: param.name,
|
|
25
25
|
channel: query.channel,
|
|
26
26
|
repo: query.repo,
|
|
@@ -28,6 +28,6 @@ export const agentsAddHandler = factory.createHandlers(
|
|
|
28
28
|
envFiles: query["env-file"] ? [query["env-file"]] : undefined,
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
-
return c.text(`added
|
|
31
|
+
return c.text(`added profile "${param.name}"`)
|
|
32
32
|
},
|
|
33
33
|
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const help = `funnel profiles — manage launch profiles
|
|
2
|
+
|
|
3
|
+
usage: funnel profiles [subcommand]
|
|
4
|
+
|
|
5
|
+
subcommands:
|
|
6
|
+
(none) list
|
|
7
|
+
add <name> --channel <ch> [--repo <r>] [--sub-agent <s>] [--env-file <f>]
|
|
8
|
+
<name> set [--channel ...] [--repo ...] [--sub-agent ...] [--env-file ...]
|
|
9
|
+
rename <old> <new> rename
|
|
10
|
+
remove <name> remove
|
|
11
|
+
<name> run launch (sugar for fnl claude)
|
|
12
|
+
<name> launch (alias for run)
|
|
13
|
+
|
|
14
|
+
examples:
|
|
15
|
+
funnel profiles add cto --channel prod-inbox --repo myapp --sub-agent cto
|
|
16
|
+
funnel profiles cto run`
|