@interactive-inc/claude-funnel 0.2.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/LICENSE +21 -0
- package/README.md +152 -0
- package/lib/factory.ts +10 -0
- package/lib/funnel.ts +51 -0
- package/lib/index.ts +86 -0
- package/lib/modules/agents/funnel-agents.ts +105 -0
- package/lib/modules/channels/funnel-channels.ts +113 -0
- package/lib/modules/claude/funnel-claude.ts +136 -0
- package/lib/modules/connectors/funnel-connector-adapter.ts +9 -0
- package/lib/modules/connectors/funnel-connector-listener.ts +5 -0
- package/lib/modules/connectors/funnel-connectors.ts +124 -0
- package/lib/modules/connectors/funnel-discord-adapter.ts +56 -0
- package/lib/modules/connectors/funnel-discord-event-processor.ts +48 -0
- package/lib/modules/connectors/funnel-discord-listener.ts +65 -0
- package/lib/modules/connectors/funnel-gh-adapter.ts +51 -0
- package/lib/modules/connectors/funnel-gh-listener.ts +102 -0
- package/lib/modules/connectors/funnel-slack-adapter.ts +31 -0
- package/lib/modules/connectors/funnel-slack-event-processor.ts +91 -0
- package/lib/modules/connectors/funnel-slack-listener.ts +72 -0
- package/lib/modules/connectors/resolve-listener.ts +13 -0
- package/lib/modules/fs/funnel-file-system.ts +14 -0
- package/lib/modules/fs/memory-funnel-file-system.ts +85 -0
- package/lib/modules/fs/node-funnel-file-system.ts +56 -0
- package/lib/modules/gateway/daemon.ts +190 -0
- package/lib/modules/gateway/funnel-broadcaster.ts +37 -0
- package/lib/modules/gateway/funnel-event-logger.ts +59 -0
- package/lib/modules/gateway/funnel-gateway.ts +166 -0
- package/lib/modules/gateway/kill-competing-slack-gateways.ts +52 -0
- package/lib/modules/http/funnel-http-client.ts +17 -0
- package/lib/modules/http/memory-funnel-http-client.ts +40 -0
- package/lib/modules/http/node-funnel-http-client.ts +27 -0
- package/lib/modules/logger.ts +26 -0
- package/lib/modules/mcp/channel-server.ts +77 -0
- package/lib/modules/mcp/funnel-mcp.ts +107 -0
- package/lib/modules/process/funnel-process-runner.ts +28 -0
- package/lib/modules/process/memory-funnel-process-runner.ts +88 -0
- package/lib/modules/process/node-funnel-process-runner.ts +100 -0
- package/lib/modules/repos/funnel-repositories.ts +107 -0
- package/lib/modules/router/query-to-cli-args.ts +20 -0
- package/lib/modules/router/to-request.ts +122 -0
- package/lib/modules/router/validator.ts +27 -0
- package/lib/modules/settings/funnel-settings-reader.ts +6 -0
- package/lib/modules/settings/funnel-settings-store.ts +57 -0
- package/lib/modules/settings/mock-funnel-settings-reader.ts +27 -0
- package/lib/modules/settings/settings-schema.ts +67 -0
- package/lib/modules/tui/app.tsx +44 -0
- package/lib/modules/tui/tui.tsx +13 -0
- package/lib/routes/agents/add.help.ts +3 -0
- package/lib/routes/agents/add.ts +33 -0
- package/lib/routes/agents/group.help.ts +13 -0
- package/lib/routes/agents/group.ts +25 -0
- package/lib/routes/agents/launch.help.ts +3 -0
- package/lib/routes/agents/launch.ts +35 -0
- package/lib/routes/agents/remove.help.ts +3 -0
- package/lib/routes/agents/remove.ts +17 -0
- package/lib/routes/agents/rename.help.ts +5 -0
- package/lib/routes/agents/rename.ts +17 -0
- package/lib/routes/agents/routes.ts +17 -0
- package/lib/routes/agents/set.help.ts +5 -0
- package/lib/routes/agents/set.ts +32 -0
- package/lib/routes/channels/add.help.ts +3 -0
- package/lib/routes/channels/add.ts +21 -0
- package/lib/routes/channels/connectors-attach.help.ts +3 -0
- package/lib/routes/channels/connectors-attach.ts +17 -0
- package/lib/routes/channels/connectors-detach.help.ts +3 -0
- package/lib/routes/channels/connectors-detach.ts +17 -0
- package/lib/routes/channels/group.help.ts +16 -0
- package/lib/routes/channels/group.ts +22 -0
- package/lib/routes/channels/remove.help.ts +3 -0
- package/lib/routes/channels/remove.ts +17 -0
- package/lib/routes/channels/rename.help.ts +5 -0
- package/lib/routes/channels/rename.ts +17 -0
- package/lib/routes/channels/routes.ts +19 -0
- package/lib/routes/channels/show.help.ts +1 -0
- package/lib/routes/channels/show.ts +26 -0
- package/lib/routes/claude/claude.help.ts +11 -0
- package/lib/routes/claude/claude.ts +39 -0
- package/lib/routes/claude/routes.ts +4 -0
- package/lib/routes/connectors/add.help.ts +22 -0
- package/lib/routes/connectors/add.ts +55 -0
- package/lib/routes/connectors/call.help.ts +17 -0
- package/lib/routes/connectors/call.ts +43 -0
- package/lib/routes/connectors/group.help.ts +14 -0
- package/lib/routes/connectors/group.ts +18 -0
- package/lib/routes/connectors/remove.help.ts +3 -0
- package/lib/routes/connectors/remove.ts +17 -0
- package/lib/routes/connectors/rename.help.ts +5 -0
- package/lib/routes/connectors/rename.ts +17 -0
- package/lib/routes/connectors/routes.ts +19 -0
- package/lib/routes/connectors/set.help.ts +8 -0
- package/lib/routes/connectors/set.ts +30 -0
- package/lib/routes/connectors/show.help.ts +1 -0
- package/lib/routes/connectors/show.ts +32 -0
- package/lib/routes/gateway/group.help.ts +15 -0
- package/lib/routes/gateway/group.ts +28 -0
- package/lib/routes/gateway/logs.help.ts +13 -0
- package/lib/routes/gateway/logs.ts +100 -0
- package/lib/routes/gateway/restart.help.ts +10 -0
- package/lib/routes/gateway/restart.ts +35 -0
- package/lib/routes/gateway/routes.ts +18 -0
- package/lib/routes/gateway/run.help.ts +12 -0
- package/lib/routes/gateway/run.ts +35 -0
- package/lib/routes/gateway/start.help.ts +15 -0
- package/lib/routes/gateway/start.ts +32 -0
- package/lib/routes/gateway/status.help.ts +9 -0
- package/lib/routes/gateway/status.ts +28 -0
- package/lib/routes/gateway/stop.help.ts +8 -0
- package/lib/routes/gateway/stop.ts +21 -0
- package/lib/routes/repos/add.help.ts +5 -0
- package/lib/routes/repos/add.ts +19 -0
- package/lib/routes/repos/group.help.ts +11 -0
- package/lib/routes/repos/group.ts +18 -0
- package/lib/routes/repos/remove.help.ts +3 -0
- package/lib/routes/repos/remove.ts +17 -0
- package/lib/routes/repos/rename.help.ts +5 -0
- package/lib/routes/repos/rename.ts +17 -0
- package/lib/routes/repos/routes.ts +17 -0
- package/lib/routes/repos/set.help.ts +5 -0
- package/lib/routes/repos/set.ts +21 -0
- package/lib/routes/repos/show.help.ts +1 -0
- package/lib/routes/repos/show.ts +19 -0
- package/lib/routes/status/routes.ts +4 -0
- package/lib/routes/status/status.help.ts +6 -0
- package/lib/routes/status/status.ts +77 -0
- package/lib/routes.ts +36 -0
- package/package.json +65 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const help = `funnel channels <name> — show channel details`
|
|
@@ -0,0 +1,26 @@
|
|
|
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/channels/show.help"
|
|
6
|
+
|
|
7
|
+
export const channelsShowHandler = 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 channel = funnel.channels.get(param.name)
|
|
14
|
+
|
|
15
|
+
if (!channel) {
|
|
16
|
+
throw new HTTPException(404, { message: `channel "${param.name}" not found` })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const lines = [
|
|
20
|
+
`name: ${channel.name}`,
|
|
21
|
+
`connectors: ${channel.connectors.length > 0 ? channel.connectors.join(", ") : "(none)"}`,
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
return c.text(lines.join("\n"))
|
|
25
|
+
},
|
|
26
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const help = `funnel claude — launch Claude Code
|
|
2
|
+
|
|
3
|
+
usage: funnel claude --channel <name> [--repo <name>] [--sub-agent <name>] [--env-file <file>] [additional claude args...]
|
|
4
|
+
|
|
5
|
+
options:
|
|
6
|
+
--channel channel name to subscribe to (required)
|
|
7
|
+
--repo switch working directory to the named repo (extra)
|
|
8
|
+
--sub-agent sub-agent name (passed to claude --agent)
|
|
9
|
+
--env-file additional env file to load (relative path)
|
|
10
|
+
|
|
11
|
+
On launch the FUNNEL_CHANNEL_ID env var is set and MCP connects to the gateway.`
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { factory } from "@/factory"
|
|
3
|
+
import { queryToCliArgs } from "@/modules/router/query-to-cli-args"
|
|
4
|
+
import { zValidator } from "@/modules/router/validator"
|
|
5
|
+
import { help } from "@/routes/claude/claude.help"
|
|
6
|
+
|
|
7
|
+
const RESERVED_KEYS = ["channel", "repo", "sub-agent", "env-file"]
|
|
8
|
+
|
|
9
|
+
export const claudeHandler = factory.createHandlers(
|
|
10
|
+
zValidator(
|
|
11
|
+
"query",
|
|
12
|
+
z
|
|
13
|
+
.object({
|
|
14
|
+
channel: z.string().optional(),
|
|
15
|
+
repo: z.string().optional(),
|
|
16
|
+
"sub-agent": z.string().optional(),
|
|
17
|
+
"env-file": z.string().optional(),
|
|
18
|
+
})
|
|
19
|
+
.passthrough(),
|
|
20
|
+
help,
|
|
21
|
+
),
|
|
22
|
+
async (c) => {
|
|
23
|
+
const query = c.req.valid("query")
|
|
24
|
+
|
|
25
|
+
if (!query.channel) return c.text(help)
|
|
26
|
+
|
|
27
|
+
const funnel = c.var.funnel
|
|
28
|
+
|
|
29
|
+
const exitCode = await funnel.claude.launch({
|
|
30
|
+
channel: query.channel,
|
|
31
|
+
repo: query.repo,
|
|
32
|
+
subAgent: query["sub-agent"],
|
|
33
|
+
envFiles: query["env-file"] ? [query["env-file"]] : undefined,
|
|
34
|
+
userArgs: queryToCliArgs(c.req.url, RESERVED_KEYS),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
process.exit(exitCode)
|
|
38
|
+
},
|
|
39
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const help = `funnel connectors add — add a connector
|
|
2
|
+
|
|
3
|
+
usage:
|
|
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
|
+
|
|
8
|
+
slack (Socket Mode):
|
|
9
|
+
--bot-token Slack Bot Token (starts with xoxb-)
|
|
10
|
+
--app-token Slack App Token (starts with xapp-)
|
|
11
|
+
|
|
12
|
+
gh (GitHub, gh CLI):
|
|
13
|
+
--poll-interval polling interval for /notifications (seconds, default 60)
|
|
14
|
+
note: uses the gh CLI (must be authenticated via gh auth login); no token required
|
|
15
|
+
|
|
16
|
+
discord (Discord Gateway):
|
|
17
|
+
--bot-token Discord Bot Token
|
|
18
|
+
|
|
19
|
+
examples:
|
|
20
|
+
funnel connectors add prod-slack --type slack --bot-token xoxb-... --app-token xapp-...
|
|
21
|
+
funnel connectors add my-gh --type gh --poll-interval 30
|
|
22
|
+
funnel connectors add my-discord --type discord --bot-token MTI...`
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { factory } from "@/factory"
|
|
3
|
+
import { zValidator } from "@/modules/router/validator"
|
|
4
|
+
import { help } from "@/routes/connectors/add.help"
|
|
5
|
+
|
|
6
|
+
export const connectorsAddHandler = factory.createHandlers(
|
|
7
|
+
zValidator("param", z.object({ name: z.string() })),
|
|
8
|
+
zValidator(
|
|
9
|
+
"query",
|
|
10
|
+
z.discriminatedUnion("type", [
|
|
11
|
+
z.object({
|
|
12
|
+
type: z.literal("slack"),
|
|
13
|
+
"bot-token": z.string().startsWith("xoxb-"),
|
|
14
|
+
"app-token": z.string().startsWith("xapp-"),
|
|
15
|
+
}),
|
|
16
|
+
z.object({
|
|
17
|
+
type: z.literal("gh"),
|
|
18
|
+
"poll-interval": z.string().optional(),
|
|
19
|
+
}),
|
|
20
|
+
z.object({
|
|
21
|
+
type: z.literal("discord"),
|
|
22
|
+
"bot-token": z.string().min(10),
|
|
23
|
+
}),
|
|
24
|
+
]),
|
|
25
|
+
help,
|
|
26
|
+
),
|
|
27
|
+
(c) => {
|
|
28
|
+
const param = c.req.valid("param")
|
|
29
|
+
const query = c.req.valid("query")
|
|
30
|
+
const funnel = c.var.funnel
|
|
31
|
+
|
|
32
|
+
if (query.type === "slack") {
|
|
33
|
+
funnel.connectors.add({
|
|
34
|
+
type: "slack",
|
|
35
|
+
name: param.name,
|
|
36
|
+
botToken: query["bot-token"],
|
|
37
|
+
appToken: query["app-token"],
|
|
38
|
+
})
|
|
39
|
+
} else if (query.type === "gh") {
|
|
40
|
+
funnel.connectors.add({
|
|
41
|
+
type: "gh",
|
|
42
|
+
name: param.name,
|
|
43
|
+
pollInterval: query["poll-interval"] ? Number(query["poll-interval"]) : undefined,
|
|
44
|
+
})
|
|
45
|
+
} else {
|
|
46
|
+
funnel.connectors.add({
|
|
47
|
+
type: "discord",
|
|
48
|
+
name: param.name,
|
|
49
|
+
botToken: query["bot-token"],
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return c.text(`added connector "${param.name}"`)
|
|
54
|
+
},
|
|
55
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const help = `funnel connectors <name> <method> <path> [body] — call a connector API
|
|
2
|
+
|
|
3
|
+
usage:
|
|
4
|
+
funnel connectors <name> <method> <path> [<json-body>]
|
|
5
|
+
|
|
6
|
+
<method>:
|
|
7
|
+
get / post / put / patch / delete / head / options
|
|
8
|
+
|
|
9
|
+
Slack examples (every API is posted via POST internally):
|
|
10
|
+
funnel connectors my-slack post chat.postMessage '{"channel":"D...","text":"hi"}'
|
|
11
|
+
funnel connectors my-slack post chat.update '{"channel":"D...","ts":"...","text":"edit"}'
|
|
12
|
+
funnel connectors my-slack post chat.delete '{"channel":"D...","ts":"..."}'
|
|
13
|
+
funnel connectors my-slack post users.info '{"user":"U..."}'
|
|
14
|
+
|
|
15
|
+
Discord examples (per HTTP method):
|
|
16
|
+
funnel connectors my-discord post channels/C/messages '{"content":"hi"}'
|
|
17
|
+
funnel connectors my-discord delete channels/C/messages/M`
|
|
@@ -0,0 +1,43 @@
|
|
|
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/call.help"
|
|
6
|
+
|
|
7
|
+
const parseBody = (raw: string | undefined): unknown => {
|
|
8
|
+
if (!raw) return {}
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(raw)
|
|
12
|
+
} catch (error) {
|
|
13
|
+
throw new HTTPException(400, {
|
|
14
|
+
message: `body is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const connectorsCallHandler = factory.createHandlers(
|
|
20
|
+
zValidator("param", z.object({ name: z.string() })),
|
|
21
|
+
zValidator(
|
|
22
|
+
"query",
|
|
23
|
+
z.object({
|
|
24
|
+
method: z.string(),
|
|
25
|
+
path: z.string(),
|
|
26
|
+
body: z.string().optional(),
|
|
27
|
+
}),
|
|
28
|
+
help,
|
|
29
|
+
),
|
|
30
|
+
async (c) => {
|
|
31
|
+
const param = c.req.valid("param")
|
|
32
|
+
const query = c.req.valid("query")
|
|
33
|
+
const funnel = c.var.funnel
|
|
34
|
+
|
|
35
|
+
const result = await funnel.connectors.call(param.name, {
|
|
36
|
+
method: query.method,
|
|
37
|
+
path: query.path,
|
|
38
|
+
body: parseBody(query.body),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return c.text(JSON.stringify(result, null, 2))
|
|
42
|
+
},
|
|
43
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const help = `funnel connectors — manage external connections (Slack, etc.)
|
|
2
|
+
|
|
3
|
+
usage: funnel connectors [subcommand]
|
|
4
|
+
|
|
5
|
+
subcommands:
|
|
6
|
+
(none) list
|
|
7
|
+
add <name> --type slack --bot-token <t> --app-token <t>
|
|
8
|
+
remove <name>
|
|
9
|
+
<name> show details
|
|
10
|
+
|
|
11
|
+
examples:
|
|
12
|
+
funnel connectors add prod-slack --type slack --bot-token xoxb-... --app-token xapp-...
|
|
13
|
+
funnel connectors
|
|
14
|
+
funnel connectors remove prod-slack`
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { factory } from "@/factory"
|
|
3
|
+
import { zValidator } from "@/modules/router/validator"
|
|
4
|
+
import { help } from "@/routes/connectors/group.help"
|
|
5
|
+
|
|
6
|
+
export const connectorsGroupHandler = factory.createHandlers(
|
|
7
|
+
zValidator("query", z.object({}), help),
|
|
8
|
+
(c) => {
|
|
9
|
+
const funnel = c.var.funnel
|
|
10
|
+
const connectors = funnel.connectors.list()
|
|
11
|
+
|
|
12
|
+
if (connectors.length === 0) return c.text("no connectors")
|
|
13
|
+
|
|
14
|
+
const lines = connectors.map((con) => `${con.name} (${con.type})`)
|
|
15
|
+
|
|
16
|
+
return c.text(lines.join("\n"))
|
|
17
|
+
},
|
|
18
|
+
)
|
|
@@ -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/remove.help"
|
|
5
|
+
|
|
6
|
+
export const connectorsRemoveHandler = factory.createHandlers(
|
|
7
|
+
zValidator("param", z.object({ name: 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.connectors.remove(param.name)
|
|
14
|
+
|
|
15
|
+
return c.text(`removed connector "${param.name}"`)
|
|
16
|
+
},
|
|
17
|
+
)
|
|
@@ -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/rename.help"
|
|
5
|
+
|
|
6
|
+
export const connectorsRenameHandler = factory.createHandlers(
|
|
7
|
+
zValidator("param", z.object({ name: z.string(), newName: 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.connectors.rename(param.name, param["newName"])
|
|
14
|
+
|
|
15
|
+
return c.text(`renamed connector "${param.name}" to "${param["newName"]}"`)
|
|
16
|
+
},
|
|
17
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { factory } from "@/factory"
|
|
2
|
+
import { connectorsAddHandler } from "@/routes/connectors/add"
|
|
3
|
+
import { connectorsCallHandler } from "@/routes/connectors/call"
|
|
4
|
+
import { connectorsGroupHandler } from "@/routes/connectors/group"
|
|
5
|
+
import { connectorsRemoveHandler } from "@/routes/connectors/remove"
|
|
6
|
+
import { connectorsRenameHandler } from "@/routes/connectors/rename"
|
|
7
|
+
import { connectorsSetHandler } from "@/routes/connectors/set"
|
|
8
|
+
import { connectorsShowHandler } from "@/routes/connectors/show"
|
|
9
|
+
|
|
10
|
+
export const connectorsRoutes = factory
|
|
11
|
+
.createApp()
|
|
12
|
+
.get("/", ...connectorsGroupHandler)
|
|
13
|
+
.get("/:name/call", ...connectorsCallHandler)
|
|
14
|
+
.put("/:name/rename/:newName", ...connectorsRenameHandler)
|
|
15
|
+
.put("/rename/:name/:newName", ...connectorsRenameHandler)
|
|
16
|
+
.post("/:name", ...connectorsAddHandler)
|
|
17
|
+
.put("/:name", ...connectorsSetHandler)
|
|
18
|
+
.delete("/:name", ...connectorsRemoveHandler)
|
|
19
|
+
.get("/:name", ...connectorsShowHandler)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const help = `funnel connectors <name> set — update a connector
|
|
2
|
+
|
|
3
|
+
usage: funnel connectors <name> set [--bot-token ...] [--app-token ...] [--poll-interval ...]
|
|
4
|
+
|
|
5
|
+
fields available per type:
|
|
6
|
+
slack : --bot-token / --app-token
|
|
7
|
+
gh : --poll-interval
|
|
8
|
+
discord : --bot-token`
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { factory } from "@/factory"
|
|
3
|
+
import { zValidator } from "@/modules/router/validator"
|
|
4
|
+
import { help } from "@/routes/connectors/set.help"
|
|
5
|
+
|
|
6
|
+
export const connectorsSetHandler = factory.createHandlers(
|
|
7
|
+
zValidator("param", z.object({ name: z.string() })),
|
|
8
|
+
zValidator(
|
|
9
|
+
"query",
|
|
10
|
+
z.object({
|
|
11
|
+
"bot-token": z.string().optional(),
|
|
12
|
+
"app-token": z.string().optional(),
|
|
13
|
+
"poll-interval": z.string().optional(),
|
|
14
|
+
}),
|
|
15
|
+
help,
|
|
16
|
+
),
|
|
17
|
+
(c) => {
|
|
18
|
+
const param = c.req.valid("param")
|
|
19
|
+
const query = c.req.valid("query")
|
|
20
|
+
const funnel = c.var.funnel
|
|
21
|
+
|
|
22
|
+
funnel.connectors.update(param.name, {
|
|
23
|
+
botToken: query["bot-token"],
|
|
24
|
+
appToken: query["app-token"],
|
|
25
|
+
pollInterval: query["poll-interval"] ? Number(query["poll-interval"]) : undefined,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
return c.text(`updated connector "${param.name}"`)
|
|
29
|
+
},
|
|
30
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const help = `funnel connectors <name> — show connector details`
|
|
@@ -0,0 +1,32 @@
|
|
|
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/show.help"
|
|
6
|
+
|
|
7
|
+
export const connectorsShowHandler = 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
|
+
const lines: string[] = [`name: ${connector.name}`, `type: ${connector.type}`]
|
|
20
|
+
|
|
21
|
+
if (connector.type === "slack") {
|
|
22
|
+
lines.push(`botToken: ${connector.botToken.slice(0, 8)}...`)
|
|
23
|
+
lines.push(`appToken: ${connector.appToken.slice(0, 8)}...`)
|
|
24
|
+
} else if (connector.type === "gh") {
|
|
25
|
+
lines.push(`pollInterval: ${connector.pollInterval ?? 60}s`)
|
|
26
|
+
} else if (connector.type === "discord") {
|
|
27
|
+
lines.push(`botToken: ${connector.botToken.slice(0, 8)}...`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return c.text(lines.join("\n"))
|
|
31
|
+
},
|
|
32
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const help = `funnel gateway — manage the gateway
|
|
2
|
+
|
|
3
|
+
usage: funnel gateway [subcommand]
|
|
4
|
+
|
|
5
|
+
subcommands:
|
|
6
|
+
status show running status (default)
|
|
7
|
+
start start in background
|
|
8
|
+
stop stop
|
|
9
|
+
restart stop then start
|
|
10
|
+
run start in foreground (for developers)
|
|
11
|
+
logs [-n <N>] show event logs
|
|
12
|
+
|
|
13
|
+
examples:
|
|
14
|
+
funnel gateway check status
|
|
15
|
+
funnel gateway restart restart`
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { factory } from "@/factory"
|
|
3
|
+
import { zValidator } from "@/modules/router/validator"
|
|
4
|
+
import { help } from "@/routes/gateway/group.help"
|
|
5
|
+
|
|
6
|
+
export const gatewayGroupHandler = factory.createHandlers(
|
|
7
|
+
zValidator("query", z.object({}), help),
|
|
8
|
+
async (c) => {
|
|
9
|
+
const funnel = c.var.funnel
|
|
10
|
+
const status = funnel.gateway.getStatus()
|
|
11
|
+
|
|
12
|
+
if (!status.running) {
|
|
13
|
+
return c.text("funnel gateway: not running", 503)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const res = await fetch(`http://localhost:${status.port}/health`).catch(() => null)
|
|
17
|
+
|
|
18
|
+
if (!res) {
|
|
19
|
+
return c.text(`funnel gateway: running (pid ${status.pid}) — health check failed`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const health = (await res.json()) as Record<string, unknown>
|
|
23
|
+
|
|
24
|
+
return c.text(
|
|
25
|
+
`funnel gateway: running (pid ${status.pid})\n port: ${status.port}\n clients: ${health.clients ?? 0}`,
|
|
26
|
+
)
|
|
27
|
+
},
|
|
28
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const help = `funnel gateway logs — tail event logs
|
|
2
|
+
|
|
3
|
+
usage: funnel gateway logs [-n <N>]
|
|
4
|
+
|
|
5
|
+
options:
|
|
6
|
+
-n <N> number of trailing lines to show (default: 20)
|
|
7
|
+
|
|
8
|
+
Tails the latest /tmp/funnel/events/*.jsonl file. Exit with SIGINT.
|
|
9
|
+
Output is formatted as YAML.
|
|
10
|
+
|
|
11
|
+
examples:
|
|
12
|
+
funnel gateway logs
|
|
13
|
+
funnel gateway logs -n 100`
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { stringify } from "yaml"
|
|
4
|
+
import { z } from "zod"
|
|
5
|
+
import { factory } from "@/factory"
|
|
6
|
+
import { logger } from "@/modules/logger"
|
|
7
|
+
import { zValidator } from "@/modules/router/validator"
|
|
8
|
+
import { help } from "@/routes/gateway/logs.help"
|
|
9
|
+
|
|
10
|
+
const tryParseJson = (s: string): unknown => {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(s)
|
|
13
|
+
} catch {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const gatewayLogsHandler = factory.createHandlers(
|
|
19
|
+
zValidator(
|
|
20
|
+
"query",
|
|
21
|
+
z.object({
|
|
22
|
+
n: z.string().optional(),
|
|
23
|
+
}),
|
|
24
|
+
help,
|
|
25
|
+
),
|
|
26
|
+
async (c) => {
|
|
27
|
+
const query = c.req.valid("query")
|
|
28
|
+
const funnel = c.var.funnel
|
|
29
|
+
const logDir = funnel.gateway.getLogDir()
|
|
30
|
+
|
|
31
|
+
const files = readdirSync(logDir)
|
|
32
|
+
.filter((name) => name.endsWith(".jsonl"))
|
|
33
|
+
.sort()
|
|
34
|
+
|
|
35
|
+
if (files.length === 0) {
|
|
36
|
+
return c.text("no logs")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const latestFile = join(logDir, files[files.length - 1]!)
|
|
40
|
+
const lineCount = query.n ? Number(query.n) : 20
|
|
41
|
+
|
|
42
|
+
const tail = Bun.spawn(["tail", "-f", "-n", String(lineCount), latestFile], {
|
|
43
|
+
stdout: "pipe",
|
|
44
|
+
stderr: "inherit",
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const forward = (signal: "SIGINT" | "SIGTERM") => {
|
|
48
|
+
tail.kill(signal)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
process.on("SIGINT", () => forward("SIGINT"))
|
|
52
|
+
process.on("SIGTERM", () => forward("SIGTERM"))
|
|
53
|
+
|
|
54
|
+
const reader = tail.stdout.getReader()
|
|
55
|
+
const decoder = new TextDecoder()
|
|
56
|
+
let buffer = ""
|
|
57
|
+
|
|
58
|
+
logger.info("gateway.logs tail start", { file: latestFile })
|
|
59
|
+
|
|
60
|
+
while (true) {
|
|
61
|
+
const result = await reader.read()
|
|
62
|
+
|
|
63
|
+
if (result.done) break
|
|
64
|
+
|
|
65
|
+
buffer += decoder.decode(result.value, { stream: true })
|
|
66
|
+
|
|
67
|
+
const lines = buffer.split("\n")
|
|
68
|
+
buffer = lines.pop() ?? ""
|
|
69
|
+
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
if (!line.trim()) continue
|
|
72
|
+
|
|
73
|
+
const entry = tryParseJson(line) as {
|
|
74
|
+
timestamp: string
|
|
75
|
+
eventType: string
|
|
76
|
+
meta?: unknown
|
|
77
|
+
content: string
|
|
78
|
+
} | null
|
|
79
|
+
|
|
80
|
+
if (!entry) {
|
|
81
|
+
process.stdout.write(`${line}\n`)
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const parsedContent = tryParseJson(entry.content) ?? entry.content
|
|
86
|
+
const output = {
|
|
87
|
+
time: entry.timestamp,
|
|
88
|
+
type: entry.eventType,
|
|
89
|
+
...(entry.meta ? { meta: entry.meta } : {}),
|
|
90
|
+
content: parsedContent,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
process.stdout.write(`---\n${stringify(output)}`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await tail.exited
|
|
98
|
+
process.exit(0)
|
|
99
|
+
},
|
|
100
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const help = `funnel gateway restart — restart the gateway
|
|
2
|
+
|
|
3
|
+
usage: funnel gateway restart [--no-caffeine]
|
|
4
|
+
|
|
5
|
+
Stops the running process then starts it again in background.
|
|
6
|
+
On macOS wraps with caffeinate -i by default. Use --no-caffeine to disable.
|
|
7
|
+
|
|
8
|
+
examples:
|
|
9
|
+
funnel gateway restart
|
|
10
|
+
funnel gateway restart --no-caffeine`
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { factory } from "@/factory"
|
|
3
|
+
import { zValidator } from "@/modules/router/validator"
|
|
4
|
+
import { help } from "@/routes/gateway/restart.help"
|
|
5
|
+
|
|
6
|
+
export const gatewayRestartHandler = factory.createHandlers(
|
|
7
|
+
zValidator(
|
|
8
|
+
"query",
|
|
9
|
+
z.object({
|
|
10
|
+
"no-caffeine": z.string().optional(),
|
|
11
|
+
}),
|
|
12
|
+
help,
|
|
13
|
+
),
|
|
14
|
+
async (c) => {
|
|
15
|
+
const query = c.req.valid("query")
|
|
16
|
+
const funnel = c.var.funnel
|
|
17
|
+
|
|
18
|
+
const result = await funnel.gateway.restart({
|
|
19
|
+
caffeinate: query["no-caffeine"] !== "true",
|
|
20
|
+
})
|
|
21
|
+
const lines: string[] = []
|
|
22
|
+
|
|
23
|
+
if (result.wasRunning) {
|
|
24
|
+
lines.push(result.stopped ? "funnel gateway: stopped" : "funnel gateway: failed to stop")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (result.stopped) {
|
|
28
|
+
lines.push(result.started ? "funnel gateway: started" : "funnel gateway: failed to start")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const body = lines.join("\n")
|
|
32
|
+
|
|
33
|
+
return result.ok ? c.text(body) : c.text(body, 500)
|
|
34
|
+
},
|
|
35
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { factory } from "@/factory"
|
|
2
|
+
import { gatewayGroupHandler } from "@/routes/gateway/group"
|
|
3
|
+
import { gatewayLogsHandler } from "@/routes/gateway/logs"
|
|
4
|
+
import { gatewayRestartHandler } from "@/routes/gateway/restart"
|
|
5
|
+
import { gatewayRunHandler } from "@/routes/gateway/run"
|
|
6
|
+
import { gatewayStartHandler } from "@/routes/gateway/start"
|
|
7
|
+
import { gatewayStatusHandler } from "@/routes/gateway/status"
|
|
8
|
+
import { gatewayStopHandler } from "@/routes/gateway/stop"
|
|
9
|
+
|
|
10
|
+
export const gatewayRoutes = factory
|
|
11
|
+
.createApp()
|
|
12
|
+
.get("/", ...gatewayGroupHandler)
|
|
13
|
+
.get("/status", ...gatewayStatusHandler)
|
|
14
|
+
.get("/start", ...gatewayStartHandler)
|
|
15
|
+
.get("/stop", ...gatewayStopHandler)
|
|
16
|
+
.get("/restart", ...gatewayRestartHandler)
|
|
17
|
+
.get("/run", ...gatewayRunHandler)
|
|
18
|
+
.get("/logs", ...gatewayLogsHandler)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const help = `funnel gateway run — run the gateway in foreground
|
|
2
|
+
|
|
3
|
+
usage: funnel gateway run [--no-caffeine]
|
|
4
|
+
|
|
5
|
+
For developers. The process is tied to the current terminal and exits on SIGINT / SIGTERM.
|
|
6
|
+
On macOS wraps with caffeinate -i by default. Use --no-caffeine to disable.
|
|
7
|
+
|
|
8
|
+
For normal usage prefer funnel gateway start.
|
|
9
|
+
|
|
10
|
+
examples:
|
|
11
|
+
funnel gateway run
|
|
12
|
+
funnel gateway run --no-caffeine`
|