@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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { FunnelSettingsReader } from "@/modules/settings/funnel-settings-reader"
|
|
2
|
+
import type { ProfileConfig } from "@/modules/settings/settings-schema"
|
|
3
|
+
|
|
4
|
+
type Deps = {
|
|
5
|
+
store: FunnelSettingsReader
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class FunnelProfiles {
|
|
9
|
+
private readonly store: FunnelSettingsReader
|
|
10
|
+
|
|
11
|
+
constructor(deps: Deps) {
|
|
12
|
+
this.store = deps.store
|
|
13
|
+
Object.freeze(this)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
list(): ProfileConfig[] {
|
|
17
|
+
return this.store.read().profiles
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get(name: string): ProfileConfig | null {
|
|
21
|
+
return this.list().find((p) => p.name === name) ?? null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
add(config: ProfileConfig): void {
|
|
25
|
+
const settings = this.store.read()
|
|
26
|
+
|
|
27
|
+
if (settings.profiles.some((p) => p.name === config.name)) {
|
|
28
|
+
throw new Error(`profile "${config.name}" already exists`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!settings.channels.some((c) => c.name === config.channel)) {
|
|
32
|
+
throw new Error(`channel "${config.channel}" not found`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (config.repo && !settings.repositories.some((r) => r.name === config.repo)) {
|
|
36
|
+
throw new Error(`repo "${config.repo}" not found`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
settings.profiles.push(config)
|
|
40
|
+
|
|
41
|
+
this.store.write(settings)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
remove(name: string): void {
|
|
45
|
+
const settings = this.store.read()
|
|
46
|
+
|
|
47
|
+
const index = settings.profiles.findIndex((p) => p.name === name)
|
|
48
|
+
|
|
49
|
+
if (index < 0) throw new Error(`profile "${name}" not found`)
|
|
50
|
+
|
|
51
|
+
settings.profiles.splice(index, 1)
|
|
52
|
+
|
|
53
|
+
this.store.write(settings)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
rename(oldName: string, newName: string): void {
|
|
57
|
+
const settings = this.store.read()
|
|
58
|
+
|
|
59
|
+
const profile = settings.profiles.find((p) => p.name === oldName)
|
|
60
|
+
|
|
61
|
+
if (!profile) throw new Error(`profile "${oldName}" not found`)
|
|
62
|
+
|
|
63
|
+
if (settings.profiles.some((p) => p.name === newName)) {
|
|
64
|
+
throw new Error(`profile "${newName}" already exists`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
profile.name = newName
|
|
68
|
+
|
|
69
|
+
this.store.write(settings)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
hasChannelRef(channelName: string): boolean {
|
|
73
|
+
return this.store.read().profiles.some((p) => p.channel === channelName)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
renameChannelRef(oldName: string, newName: string): void {
|
|
77
|
+
const settings = this.store.read()
|
|
78
|
+
let changed = false
|
|
79
|
+
|
|
80
|
+
for (const profile of settings.profiles) {
|
|
81
|
+
if (profile.channel === oldName) {
|
|
82
|
+
profile.channel = newName
|
|
83
|
+
changed = true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (changed) this.store.write(settings)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
update(name: string, fields: Partial<Omit<ProfileConfig, "name">>): void {
|
|
91
|
+
const settings = this.store.read()
|
|
92
|
+
|
|
93
|
+
const profile = settings.profiles.find((p) => p.name === name)
|
|
94
|
+
|
|
95
|
+
if (!profile) throw new Error(`profile "${name}" not found`)
|
|
96
|
+
|
|
97
|
+
if (fields.channel !== undefined) {
|
|
98
|
+
if (!settings.channels.some((c) => c.name === fields.channel)) {
|
|
99
|
+
throw new Error(`channel "${fields.channel}" not found`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
profile.channel = fields.channel
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (fields.repo !== undefined) {
|
|
106
|
+
if (fields.repo && !settings.repositories.some((r) => r.name === fields.repo)) {
|
|
107
|
+
throw new Error(`repo "${fields.repo}" not found`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
profile.repo = fields.repo || undefined
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (fields.subAgent !== undefined) {
|
|
114
|
+
profile.subAgent = fields.subAgent || undefined
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (fields.envFiles !== undefined) {
|
|
118
|
+
profile.envFiles = fields.envFiles
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.store.write(settings)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { FunnelScheduleStore } from "@/modules/connectors/funnel-schedule-store"
|
|
2
|
+
import type { ScheduleEntry } from "@/modules/connectors/schedule-connector-schema"
|
|
3
|
+
|
|
4
|
+
type Deps = {
|
|
5
|
+
store: FunnelScheduleStore
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class FunnelSchedule {
|
|
9
|
+
private readonly store: FunnelScheduleStore
|
|
10
|
+
|
|
11
|
+
constructor(deps: Deps) {
|
|
12
|
+
this.store = deps.store
|
|
13
|
+
Object.freeze(this)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
listEntries(connector: string): ScheduleEntry[] {
|
|
17
|
+
const config = this.store.get(connector)
|
|
18
|
+
|
|
19
|
+
if (!config) throw new Error(`connector "${connector}" not found`)
|
|
20
|
+
|
|
21
|
+
return config.entries
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
addEntry(
|
|
25
|
+
connector: string,
|
|
26
|
+
entry: Omit<ScheduleEntry, "id"> & { id?: string },
|
|
27
|
+
): ScheduleEntry {
|
|
28
|
+
return this.store.addEntry(connector, entry)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
removeEntry(connector: string, id: string): void {
|
|
32
|
+
this.store.removeEntry(connector, id)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -2,10 +2,9 @@ import { FunnelSettingsReader } from "@/modules/settings/funnel-settings-reader"
|
|
|
2
2
|
import type { Settings } from "@/modules/settings/settings-schema"
|
|
3
3
|
|
|
4
4
|
export const createSettings = (partial: Partial<Settings> = {}): Settings => ({
|
|
5
|
-
connectors: [],
|
|
6
5
|
channels: [],
|
|
7
6
|
repositories: [],
|
|
8
|
-
|
|
7
|
+
profiles: [],
|
|
9
8
|
...partial,
|
|
10
9
|
})
|
|
11
10
|
|
|
@@ -1,38 +1,5 @@
|
|
|
1
1
|
import { z } from "zod"
|
|
2
2
|
|
|
3
|
-
export const slackConnectorSchema = z.object({
|
|
4
|
-
type: z.literal("slack"),
|
|
5
|
-
name: z.string(),
|
|
6
|
-
botToken: z.string().startsWith("xoxb-"),
|
|
7
|
-
appToken: z.string().startsWith("xapp-"),
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
export type SlackConnectorConfig = z.infer<typeof slackConnectorSchema>
|
|
11
|
-
|
|
12
|
-
export const ghConnectorSchema = z.object({
|
|
13
|
-
type: z.literal("gh"),
|
|
14
|
-
name: z.string(),
|
|
15
|
-
pollInterval: z.number().int().positive().optional(),
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
export type GhConnectorConfig = z.infer<typeof ghConnectorSchema>
|
|
19
|
-
|
|
20
|
-
export const discordConnectorSchema = z.object({
|
|
21
|
-
type: z.literal("discord"),
|
|
22
|
-
name: z.string(),
|
|
23
|
-
botToken: z.string().min(10),
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
export type DiscordConnectorConfig = z.infer<typeof discordConnectorSchema>
|
|
27
|
-
|
|
28
|
-
export const connectorConfigSchema = z.discriminatedUnion("type", [
|
|
29
|
-
slackConnectorSchema,
|
|
30
|
-
ghConnectorSchema,
|
|
31
|
-
discordConnectorSchema,
|
|
32
|
-
])
|
|
33
|
-
|
|
34
|
-
export type ConnectorConfig = z.infer<typeof connectorConfigSchema>
|
|
35
|
-
|
|
36
3
|
export const channelConfigSchema = z.object({
|
|
37
4
|
name: z.string(),
|
|
38
5
|
connectors: z.array(z.string()).default([]),
|
|
@@ -47,7 +14,7 @@ export const repositoryConfigSchema = z.object({
|
|
|
47
14
|
|
|
48
15
|
export type RepositoryConfig = z.infer<typeof repositoryConfigSchema>
|
|
49
16
|
|
|
50
|
-
export const
|
|
17
|
+
export const profileConfigSchema = z.object({
|
|
51
18
|
name: z.string(),
|
|
52
19
|
channel: z.string(),
|
|
53
20
|
repo: z.string().optional(),
|
|
@@ -55,13 +22,12 @@ export const agentConfigSchema = z.object({
|
|
|
55
22
|
envFiles: z.array(z.string()).optional(),
|
|
56
23
|
})
|
|
57
24
|
|
|
58
|
-
export type
|
|
25
|
+
export type ProfileConfig = z.infer<typeof profileConfigSchema>
|
|
59
26
|
|
|
60
27
|
export const settingsSchema = z.object({
|
|
61
|
-
connectors: z.array(connectorConfigSchema).default([]),
|
|
62
28
|
channels: z.array(channelConfigSchema).default([]),
|
|
63
29
|
repositories: z.array(repositoryConfigSchema).default([]),
|
|
64
|
-
|
|
30
|
+
profiles: z.array(profileConfigSchema).default([]),
|
|
65
31
|
})
|
|
66
32
|
|
|
67
33
|
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,9 +1,10 @@
|
|
|
1
1
|
export const help = `funnel connectors add — add a connector
|
|
2
2
|
|
|
3
3
|
usage:
|
|
4
|
-
funnel connectors add <name> --type slack
|
|
5
|
-
funnel connectors add <name> --type gh
|
|
6
|
-
funnel connectors add <name> --type discord
|
|
4
|
+
funnel connectors add <name> --type slack --bot-token xoxb-... --app-token xapp-...
|
|
5
|
+
funnel connectors add <name> --type gh [--poll-interval <seconds>]
|
|
6
|
+
funnel connectors add <name> --type discord --bot-token <discord-bot-token>
|
|
7
|
+
funnel connectors add <name> --type schedule
|
|
7
8
|
|
|
8
9
|
slack (Socket Mode):
|
|
9
10
|
--bot-token Slack Bot Token (starts with xoxb-)
|
|
@@ -16,7 +17,12 @@ gh (GitHub, gh CLI):
|
|
|
16
17
|
discord (Discord Gateway):
|
|
17
18
|
--bot-token Discord Bot Token
|
|
18
19
|
|
|
20
|
+
schedule (cron-driven prompts):
|
|
21
|
+
no extra flags at connector level — add entries with:
|
|
22
|
+
funnel connectors <name> schedules add --cron "* * * * *" --prompt "..."
|
|
23
|
+
|
|
19
24
|
examples:
|
|
20
25
|
funnel connectors add prod-slack --type slack --bot-token xoxb-... --app-token xapp-...
|
|
21
26
|
funnel connectors add my-gh --type gh --poll-interval 30
|
|
22
|
-
funnel connectors add my-discord --type discord --bot-token MTI
|
|
27
|
+
funnel connectors add my-discord --type discord --bot-token MTI...
|
|
28
|
+
funnel connectors add my-cron --type schedule`
|
|
@@ -21,6 +21,9 @@ export const connectorsAddHandler = factory.createHandlers(
|
|
|
21
21
|
type: z.literal("discord"),
|
|
22
22
|
"bot-token": z.string().min(10),
|
|
23
23
|
}),
|
|
24
|
+
z.object({
|
|
25
|
+
type: z.literal("schedule"),
|
|
26
|
+
}),
|
|
24
27
|
]),
|
|
25
28
|
help,
|
|
26
29
|
),
|
|
@@ -42,12 +45,18 @@ export const connectorsAddHandler = factory.createHandlers(
|
|
|
42
45
|
name: param.name,
|
|
43
46
|
pollInterval: query["poll-interval"] ? Number(query["poll-interval"]) : undefined,
|
|
44
47
|
})
|
|
45
|
-
} else {
|
|
48
|
+
} else if (query.type === "discord") {
|
|
46
49
|
funnel.connectors.add({
|
|
47
50
|
type: "discord",
|
|
48
51
|
name: param.name,
|
|
49
52
|
botToken: query["bot-token"],
|
|
50
53
|
})
|
|
54
|
+
} else {
|
|
55
|
+
funnel.connectors.add({
|
|
56
|
+
type: "schedule",
|
|
57
|
+
name: param.name,
|
|
58
|
+
entries: [],
|
|
59
|
+
})
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
return c.text(`added connector "${param.name}"`)
|
|
@@ -1,18 +1,22 @@
|
|
|
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"
|
|
6
|
+
import { connectorsSchedulesAddHandler } from "@/routes/connectors/schedules-add"
|
|
7
|
+
import { connectorsSchedulesGroupHandler } from "@/routes/connectors/schedules-group"
|
|
8
|
+
import { connectorsSchedulesRemoveHandler } from "@/routes/connectors/schedules-remove"
|
|
7
9
|
import { connectorsSetHandler } from "@/routes/connectors/set"
|
|
8
10
|
import { connectorsShowHandler } from "@/routes/connectors/show"
|
|
9
11
|
|
|
10
12
|
export const connectorsRoutes = factory
|
|
11
13
|
.createApp()
|
|
12
14
|
.get("/", ...connectorsGroupHandler)
|
|
13
|
-
.get("/:name/call", ...connectorsCallHandler)
|
|
14
15
|
.put("/:name/rename/:newName", ...connectorsRenameHandler)
|
|
15
16
|
.put("/rename/:name/:newName", ...connectorsRenameHandler)
|
|
17
|
+
.post("/:name/schedules", ...connectorsSchedulesAddHandler)
|
|
18
|
+
.get("/:name/schedules", ...connectorsSchedulesGroupHandler)
|
|
19
|
+
.delete("/:name/schedules/:id", ...connectorsSchedulesRemoveHandler)
|
|
16
20
|
.post("/:name", ...connectorsAddHandler)
|
|
17
21
|
.put("/:name", ...connectorsSetHandler)
|
|
18
22
|
.delete("/:name", ...connectorsRemoveHandler)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const help = `funnel connectors <name> schedules add — add a schedule entry
|
|
2
|
+
|
|
3
|
+
usage: funnel connectors <name> schedules add --cron "<expr>" --prompt "<text>" [--disabled]
|
|
4
|
+
|
|
5
|
+
options:
|
|
6
|
+
--cron 5-field cron expression (min hour dom month dow)
|
|
7
|
+
--prompt prompt text delivered to subscribing channels when the cron fires
|
|
8
|
+
--disabled create the entry in disabled state
|
|
9
|
+
|
|
10
|
+
example:
|
|
11
|
+
funnel connectors my-cron schedules add --cron "*/5 * * * *" --prompt "status check"`
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { factory } from "@/factory"
|
|
3
|
+
import { matchCron } from "@/modules/connectors/match-cron"
|
|
4
|
+
import { zValidator } from "@/modules/router/validator"
|
|
5
|
+
import { help } from "@/routes/connectors/schedules-add.help"
|
|
6
|
+
|
|
7
|
+
export const connectorsSchedulesAddHandler = factory.createHandlers(
|
|
8
|
+
zValidator("param", z.object({ name: z.string() })),
|
|
9
|
+
zValidator(
|
|
10
|
+
"query",
|
|
11
|
+
z.object({
|
|
12
|
+
cron: z.string(),
|
|
13
|
+
prompt: z.string(),
|
|
14
|
+
disabled: z.string().optional(),
|
|
15
|
+
}),
|
|
16
|
+
help,
|
|
17
|
+
),
|
|
18
|
+
(c) => {
|
|
19
|
+
const param = c.req.valid("param")
|
|
20
|
+
const query = c.req.valid("query")
|
|
21
|
+
const funnel = c.var.funnel
|
|
22
|
+
|
|
23
|
+
matchCron(query.cron, new Date())
|
|
24
|
+
|
|
25
|
+
const entry = funnel.schedule.addEntry(param.name, {
|
|
26
|
+
cron: query.cron,
|
|
27
|
+
prompt: query.prompt,
|
|
28
|
+
enabled: query.disabled !== "true",
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
return c.text(`added schedule entry "${entry.id}" to connector "${param.name}"`)
|
|
32
|
+
},
|
|
33
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const help = `funnel connectors <name> schedules — list schedule entries`
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { HTTPException } from "hono/http-exception"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
import { factory } from "@/factory"
|
|
4
|
+
import { zValidator } from "@/modules/router/validator"
|
|
5
|
+
import { help } from "@/routes/connectors/schedules-group.help"
|
|
6
|
+
|
|
7
|
+
export const connectorsSchedulesGroupHandler = factory.createHandlers(
|
|
8
|
+
zValidator("param", z.object({ name: z.string() })),
|
|
9
|
+
zValidator("query", z.object({}), help),
|
|
10
|
+
(c) => {
|
|
11
|
+
const param = c.req.valid("param")
|
|
12
|
+
const funnel = c.var.funnel
|
|
13
|
+
const connector = funnel.connectors.get(param.name)
|
|
14
|
+
|
|
15
|
+
if (!connector) {
|
|
16
|
+
throw new HTTPException(404, { message: `connector "${param.name}" not found` })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (connector.type !== "schedule") {
|
|
20
|
+
throw new HTTPException(400, {
|
|
21
|
+
message: `connector "${param.name}" is type "${connector.type}", not "schedule"`,
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const entries = funnel.schedule.listEntries(param.name)
|
|
26
|
+
|
|
27
|
+
if (entries.length === 0) return c.text("no schedule entries")
|
|
28
|
+
|
|
29
|
+
const lines: string[] = []
|
|
30
|
+
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const status = entry.enabled ? "" : " (disabled)"
|
|
33
|
+
lines.push(`${entry.id}${status} ${entry.cron} ${entry.prompt}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return c.text(lines.join("\n"))
|
|
37
|
+
},
|
|
38
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { factory } from "@/factory"
|
|
3
|
+
import { zValidator } from "@/modules/router/validator"
|
|
4
|
+
import { help } from "@/routes/connectors/schedules-remove.help"
|
|
5
|
+
|
|
6
|
+
export const connectorsSchedulesRemoveHandler = factory.createHandlers(
|
|
7
|
+
zValidator("param", z.object({ name: z.string(), id: z.string() })),
|
|
8
|
+
zValidator("query", z.object({}), help),
|
|
9
|
+
(c) => {
|
|
10
|
+
const param = c.req.valid("param")
|
|
11
|
+
const funnel = c.var.funnel
|
|
12
|
+
|
|
13
|
+
funnel.schedule.removeEntry(param.name, param.id)
|
|
14
|
+
|
|
15
|
+
return c.text(`removed schedule entry "${param.id}" from connector "${param.name}"`)
|
|
16
|
+
},
|
|
17
|
+
)
|
|
@@ -1,8 +1,28 @@
|
|
|
1
|
+
import { HTTPException } from "hono/http-exception"
|
|
1
2
|
import { z } from "zod"
|
|
2
3
|
import { factory } from "@/factory"
|
|
3
4
|
import { zValidator } from "@/modules/router/validator"
|
|
4
5
|
import { help } from "@/routes/connectors/set.help"
|
|
5
6
|
|
|
7
|
+
const SLACK_FIELDS = ["bot-token", "app-token"] as const
|
|
8
|
+
const GH_FIELDS = ["poll-interval"] as const
|
|
9
|
+
const DISCORD_FIELDS = ["bot-token"] as const
|
|
10
|
+
|
|
11
|
+
const rejectExtraneous = (
|
|
12
|
+
query: Record<string, string | undefined>,
|
|
13
|
+
allowed: ReadonlyArray<string>,
|
|
14
|
+
type: string,
|
|
15
|
+
): void => {
|
|
16
|
+
for (const key of ["bot-token", "app-token", "poll-interval"]) {
|
|
17
|
+
if (query[key] === undefined) continue
|
|
18
|
+
if (allowed.includes(key)) continue
|
|
19
|
+
|
|
20
|
+
throw new HTTPException(400, {
|
|
21
|
+
message: `connector type "${type}" does not accept --${key}`,
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
6
26
|
export const connectorsSetHandler = factory.createHandlers(
|
|
7
27
|
zValidator("param", z.object({ name: z.string() })),
|
|
8
28
|
zValidator(
|
|
@@ -19,11 +39,33 @@ export const connectorsSetHandler = factory.createHandlers(
|
|
|
19
39
|
const query = c.req.valid("query")
|
|
20
40
|
const funnel = c.var.funnel
|
|
21
41
|
|
|
22
|
-
funnel.connectors.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
42
|
+
const current = funnel.connectors.get(param.name)
|
|
43
|
+
|
|
44
|
+
if (!current) {
|
|
45
|
+
throw new HTTPException(404, { message: `connector "${param.name}" not found` })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (current.type === "slack") {
|
|
49
|
+
rejectExtraneous(query, SLACK_FIELDS, "slack")
|
|
50
|
+
funnel.connectors.updateSlack(param.name, {
|
|
51
|
+
botToken: query["bot-token"],
|
|
52
|
+
appToken: query["app-token"],
|
|
53
|
+
})
|
|
54
|
+
} else if (current.type === "gh") {
|
|
55
|
+
rejectExtraneous(query, GH_FIELDS, "gh")
|
|
56
|
+
funnel.connectors.updateGh(param.name, {
|
|
57
|
+
pollInterval: query["poll-interval"] ? Number(query["poll-interval"]) : undefined,
|
|
58
|
+
})
|
|
59
|
+
} else if (current.type === "discord") {
|
|
60
|
+
rejectExtraneous(query, DISCORD_FIELDS, "discord")
|
|
61
|
+
funnel.connectors.updateDiscord(param.name, {
|
|
62
|
+
botToken: query["bot-token"],
|
|
63
|
+
})
|
|
64
|
+
} else {
|
|
65
|
+
throw new HTTPException(400, {
|
|
66
|
+
message: `schedule connectors have no top-level fields — use schedules add/remove`,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
27
69
|
|
|
28
70
|
return c.text(`updated connector "${param.name}"`)
|
|
29
71
|
},
|