@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,35 @@
|
|
|
1
|
+
import { resolve } from "node:path"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
import { factory } from "@/factory"
|
|
4
|
+
import { zValidator } from "@/modules/router/validator"
|
|
5
|
+
import { help } from "@/routes/gateway/run.help"
|
|
6
|
+
|
|
7
|
+
export const gatewayRunHandler = factory.createHandlers(
|
|
8
|
+
zValidator(
|
|
9
|
+
"query",
|
|
10
|
+
z.object({
|
|
11
|
+
"no-caffeine": z.string().optional(),
|
|
12
|
+
}),
|
|
13
|
+
help,
|
|
14
|
+
),
|
|
15
|
+
async (c) => {
|
|
16
|
+
const query = c.req.valid("query")
|
|
17
|
+
const gatewayScript = resolve(import.meta.dir, "../../modules/gateway/daemon.ts")
|
|
18
|
+
|
|
19
|
+
const useCaffeinate = query["no-caffeine"] !== "true" && process.platform === "darwin"
|
|
20
|
+
const command = useCaffeinate
|
|
21
|
+
? ["caffeinate", "-i", "bun", gatewayScript]
|
|
22
|
+
: ["bun", gatewayScript]
|
|
23
|
+
|
|
24
|
+
const proc = Bun.spawn(command, {
|
|
25
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
process.on("SIGINT", () => proc.kill("SIGINT"))
|
|
29
|
+
process.on("SIGTERM", () => proc.kill("SIGTERM"))
|
|
30
|
+
|
|
31
|
+
const exitCode = await proc.exited
|
|
32
|
+
|
|
33
|
+
process.exit(exitCode)
|
|
34
|
+
},
|
|
35
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const help = `funnel gateway start — start the gateway in background
|
|
2
|
+
|
|
3
|
+
usage: funnel gateway start [--no-caffeine]
|
|
4
|
+
|
|
5
|
+
Daemonized with nohup, so it keeps running after the terminal is closed.
|
|
6
|
+
On macOS wraps the process with caffeinate -i by default to prevent idle sleep.
|
|
7
|
+
Use --no-caffeine to disable caffeinate.
|
|
8
|
+
|
|
9
|
+
port: 9742 (override via FUNNEL_PORT)
|
|
10
|
+
pid: ~/.funnel/gateway.pid
|
|
11
|
+
log: /tmp/funnel/gateway.log
|
|
12
|
+
|
|
13
|
+
examples:
|
|
14
|
+
funnel gateway start
|
|
15
|
+
funnel gateway start --no-caffeine`
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { factory } from "@/factory"
|
|
3
|
+
import { zValidator } from "@/modules/router/validator"
|
|
4
|
+
import { help } from "@/routes/gateway/start.help"
|
|
5
|
+
|
|
6
|
+
export const gatewayStartHandler = 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
|
+
if (funnel.gateway.isRunning()) {
|
|
19
|
+
const status = funnel.gateway.getStatus()
|
|
20
|
+
|
|
21
|
+
return c.text(`funnel gateway: already running (pid ${status.pid})`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const started = await funnel.gateway.start({
|
|
25
|
+
caffeinate: query["no-caffeine"] !== "true",
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
return started
|
|
29
|
+
? c.text("funnel gateway: started")
|
|
30
|
+
: c.text("funnel gateway: failed to start", 500)
|
|
31
|
+
},
|
|
32
|
+
)
|
|
@@ -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/status.help"
|
|
5
|
+
|
|
6
|
+
export const gatewayStatusHandler = 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,21 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { factory } from "@/factory"
|
|
3
|
+
import { zValidator } from "@/modules/router/validator"
|
|
4
|
+
import { help } from "@/routes/gateway/stop.help"
|
|
5
|
+
|
|
6
|
+
export const gatewayStopHandler = factory.createHandlers(
|
|
7
|
+
zValidator("query", z.object({}), help),
|
|
8
|
+
async (c) => {
|
|
9
|
+
const funnel = c.var.funnel
|
|
10
|
+
|
|
11
|
+
if (!funnel.gateway.isRunning()) {
|
|
12
|
+
return c.text("funnel gateway: no running process")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const stopped = await funnel.gateway.stop()
|
|
16
|
+
|
|
17
|
+
return stopped
|
|
18
|
+
? c.text("funnel gateway: stopped")
|
|
19
|
+
: c.text("funnel gateway: failed to stop", 500)
|
|
20
|
+
},
|
|
21
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { resolve } from "node:path"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
import { factory } from "@/factory"
|
|
4
|
+
import { zValidator } from "@/modules/router/validator"
|
|
5
|
+
import { help } from "@/routes/repos/add.help"
|
|
6
|
+
|
|
7
|
+
export const reposAddHandler = factory.createHandlers(
|
|
8
|
+
zValidator("param", z.object({ name: z.string() })),
|
|
9
|
+
zValidator("query", z.object({ path: z.string() }), help),
|
|
10
|
+
(c) => {
|
|
11
|
+
const param = c.req.valid("param")
|
|
12
|
+
const query = c.req.valid("query")
|
|
13
|
+
const funnel = c.var.funnel
|
|
14
|
+
|
|
15
|
+
funnel.repositories.add({ name: param.name, path: resolve(query.path) })
|
|
16
|
+
|
|
17
|
+
return c.text(`added repo "${param.name}"`)
|
|
18
|
+
},
|
|
19
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const help = `funnel repos — manage repositories (extra)
|
|
2
|
+
|
|
3
|
+
usage: funnel repos [subcommand]
|
|
4
|
+
|
|
5
|
+
subcommands:
|
|
6
|
+
(none) list
|
|
7
|
+
add <name> --path <p> add (writes funnel into .mcp.json)
|
|
8
|
+
<name> set --path <p> change path
|
|
9
|
+
rename <old> <new> rename
|
|
10
|
+
remove <name> remove
|
|
11
|
+
<name> show details`
|
|
@@ -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/repos/group.help"
|
|
5
|
+
|
|
6
|
+
export const reposGroupHandler = factory.createHandlers(
|
|
7
|
+
zValidator("query", z.object({}), help),
|
|
8
|
+
(c) => {
|
|
9
|
+
const funnel = c.var.funnel
|
|
10
|
+
const repos = funnel.repositories.list()
|
|
11
|
+
|
|
12
|
+
if (repos.length === 0) return c.text("no repos")
|
|
13
|
+
|
|
14
|
+
const lines = repos.map((repo) => `${repo.name} ${repo.path}`)
|
|
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/repos/remove.help"
|
|
5
|
+
|
|
6
|
+
export const reposRemoveHandler = 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.repositories.remove(param.name)
|
|
14
|
+
|
|
15
|
+
return c.text(`removed repo "${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/repos/rename.help"
|
|
5
|
+
|
|
6
|
+
export const reposRenameHandler = 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.repositories.rename(param.name, param["newName"])
|
|
14
|
+
|
|
15
|
+
return c.text(`renamed repo "${param.name}" to "${param["newName"]}"`)
|
|
16
|
+
},
|
|
17
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { factory } from "@/factory"
|
|
2
|
+
import { reposAddHandler } from "@/routes/repos/add"
|
|
3
|
+
import { reposGroupHandler } from "@/routes/repos/group"
|
|
4
|
+
import { reposRemoveHandler } from "@/routes/repos/remove"
|
|
5
|
+
import { reposRenameHandler } from "@/routes/repos/rename"
|
|
6
|
+
import { reposSetHandler } from "@/routes/repos/set"
|
|
7
|
+
import { reposShowHandler } from "@/routes/repos/show"
|
|
8
|
+
|
|
9
|
+
export const reposRoutes = factory
|
|
10
|
+
.createApp()
|
|
11
|
+
.get("/", ...reposGroupHandler)
|
|
12
|
+
.put("/:name/rename/:newName", ...reposRenameHandler)
|
|
13
|
+
.put("/rename/:name/:newName", ...reposRenameHandler)
|
|
14
|
+
.post("/:name", ...reposAddHandler)
|
|
15
|
+
.put("/:name", ...reposSetHandler)
|
|
16
|
+
.delete("/:name", ...reposRemoveHandler)
|
|
17
|
+
.get("/:name", ...reposShowHandler)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { resolve } from "node:path"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
import { factory } from "@/factory"
|
|
4
|
+
import { zValidator } from "@/modules/router/validator"
|
|
5
|
+
import { help } from "@/routes/repos/set.help"
|
|
6
|
+
|
|
7
|
+
export const reposSetHandler = factory.createHandlers(
|
|
8
|
+
zValidator("param", z.object({ name: z.string() })),
|
|
9
|
+
zValidator("query", z.object({ path: z.string().optional() }), help),
|
|
10
|
+
(c) => {
|
|
11
|
+
const param = c.req.valid("param")
|
|
12
|
+
const query = c.req.valid("query")
|
|
13
|
+
const funnel = c.var.funnel
|
|
14
|
+
|
|
15
|
+
funnel.repositories.update(param.name, {
|
|
16
|
+
path: query.path !== undefined ? resolve(query.path) : undefined,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
return c.text(`updated repo "${param.name}"`)
|
|
20
|
+
},
|
|
21
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const help = `funnel repos <name> — show repo details`
|
|
@@ -0,0 +1,19 @@
|
|
|
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/repos/show.help"
|
|
6
|
+
|
|
7
|
+
export const reposShowHandler = 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 repo = funnel.repositories.get(param.name)
|
|
14
|
+
|
|
15
|
+
if (!repo) throw new HTTPException(404, { message: `repo "${param.name}" not found` })
|
|
16
|
+
|
|
17
|
+
return c.text(`name: ${repo.name}\npath: ${repo.path}`)
|
|
18
|
+
},
|
|
19
|
+
)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { factory } from "@/factory"
|
|
3
|
+
import { zValidator } from "@/modules/router/validator"
|
|
4
|
+
import { help } from "@/routes/status/status.help"
|
|
5
|
+
|
|
6
|
+
type GatewayStatus = {
|
|
7
|
+
ok: boolean
|
|
8
|
+
clients: { channel: string; connectors: string[] }[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const statusHandler = factory.createHandlers(
|
|
12
|
+
zValidator("query", z.object({}), help),
|
|
13
|
+
async (c) => {
|
|
14
|
+
const funnel = c.var.funnel
|
|
15
|
+
const connectors = funnel.connectors.list()
|
|
16
|
+
const channels = funnel.channels.list()
|
|
17
|
+
const agents = funnel.agents.list()
|
|
18
|
+
const repos = funnel.repositories.list()
|
|
19
|
+
const gatewayStatus = funnel.gateway.getStatus()
|
|
20
|
+
|
|
21
|
+
const lines: string[] = []
|
|
22
|
+
|
|
23
|
+
lines.push("= funnel status =")
|
|
24
|
+
lines.push("")
|
|
25
|
+
|
|
26
|
+
lines.push(`connectors: ${connectors.length}`)
|
|
27
|
+
for (const con of connectors) {
|
|
28
|
+
lines.push(` - ${con.name} (${con.type})`)
|
|
29
|
+
}
|
|
30
|
+
lines.push("")
|
|
31
|
+
|
|
32
|
+
lines.push(`channels: ${channels.length}`)
|
|
33
|
+
for (const ch of channels) {
|
|
34
|
+
const attached = ch.connectors.length > 0 ? ch.connectors.join(", ") : "(none)"
|
|
35
|
+
lines.push(` - ${ch.name} [${attached}]`)
|
|
36
|
+
}
|
|
37
|
+
lines.push("")
|
|
38
|
+
|
|
39
|
+
lines.push(`agents: ${agents.length}`)
|
|
40
|
+
for (const agent of agents) {
|
|
41
|
+
const parts = [`channel=${agent.channel}`]
|
|
42
|
+
|
|
43
|
+
if (agent.repo) parts.push(`repo=${agent.repo}`)
|
|
44
|
+
if (agent.subAgent) parts.push(`subAgent=${agent.subAgent}`)
|
|
45
|
+
|
|
46
|
+
lines.push(` - ${agent.name} [${parts.join(", ")}]`)
|
|
47
|
+
}
|
|
48
|
+
lines.push("")
|
|
49
|
+
|
|
50
|
+
lines.push(`repos: ${repos.length}`)
|
|
51
|
+
for (const repo of repos) {
|
|
52
|
+
lines.push(` - ${repo.name} ${repo.path}`)
|
|
53
|
+
}
|
|
54
|
+
lines.push("")
|
|
55
|
+
|
|
56
|
+
if (!gatewayStatus.running) {
|
|
57
|
+
lines.push("gateway: not running")
|
|
58
|
+
} else {
|
|
59
|
+
lines.push(`gateway: running (pid ${gatewayStatus.pid}, port ${gatewayStatus.port})`)
|
|
60
|
+
|
|
61
|
+
const res = await fetch(`http://localhost:${gatewayStatus.port}/status`).catch(() => null)
|
|
62
|
+
|
|
63
|
+
if (res && res.ok) {
|
|
64
|
+
const body = (await res.json()) as GatewayStatus
|
|
65
|
+
lines.push(` clients: ${body.clients.length}`)
|
|
66
|
+
|
|
67
|
+
for (const client of body.clients) {
|
|
68
|
+
const connectorList =
|
|
69
|
+
client.connectors.length > 0 ? client.connectors.join(", ") : "(none)"
|
|
70
|
+
lines.push(` - channel=${client.channel || "(unset)"} [${connectorList}]`)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return c.text(lines.join("\n"))
|
|
76
|
+
},
|
|
77
|
+
)
|
package/lib/routes.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { HTTPException } from "hono/http-exception"
|
|
2
|
+
import { factory } from "@/factory"
|
|
3
|
+
import { Funnel } from "@/funnel"
|
|
4
|
+
import { FunnelSettingsStore } from "@/modules/settings/funnel-settings-store"
|
|
5
|
+
import { agentsRoutes } from "@/routes/agents/routes"
|
|
6
|
+
import { channelsRoutes } from "@/routes/channels/routes"
|
|
7
|
+
import { claudeRoutes } from "@/routes/claude/routes"
|
|
8
|
+
import { connectorsRoutes } from "@/routes/connectors/routes"
|
|
9
|
+
import { gatewayRoutes } from "@/routes/gateway/routes"
|
|
10
|
+
import { reposRoutes } from "@/routes/repos/routes"
|
|
11
|
+
import { statusRoutes } from "@/routes/status/routes"
|
|
12
|
+
|
|
13
|
+
const base = factory.createApp()
|
|
14
|
+
|
|
15
|
+
base.use((c, next) => {
|
|
16
|
+
c.set("funnel", new Funnel({ store: new FunnelSettingsStore() }))
|
|
17
|
+
|
|
18
|
+
return next()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
base.onError((error, c) => {
|
|
22
|
+
if (error instanceof HTTPException) {
|
|
23
|
+
return c.text(`error: ${error.message}`, error.status)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return c.text(`error: ${error instanceof Error ? error.message : String(error)}`, 400)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export const app = base
|
|
30
|
+
.route("/claude", claudeRoutes)
|
|
31
|
+
.route("/connectors", connectorsRoutes)
|
|
32
|
+
.route("/channels", channelsRoutes)
|
|
33
|
+
.route("/repos", reposRoutes)
|
|
34
|
+
.route("/agents", agentsRoutes)
|
|
35
|
+
.route("/gateway", gatewayRoutes)
|
|
36
|
+
.route("/status", statusRoutes)
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@interactive-inc/claude-funnel",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Hub CLI that routes external events (Slack / GitHub / Discord) to Claude Code agents through subscription channels over MCP.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Interactive Inc.",
|
|
7
|
+
"homepage": "https://github.com/interactive-inc/claude-funnel#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/interactive-inc/claude-funnel.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/interactive-inc/claude-funnel/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"claude",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"mcp",
|
|
19
|
+
"slack",
|
|
20
|
+
"discord",
|
|
21
|
+
"github",
|
|
22
|
+
"cli",
|
|
23
|
+
"bun"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"module": "lib/index.ts",
|
|
27
|
+
"bin": {
|
|
28
|
+
"fnl": "./lib/index.ts",
|
|
29
|
+
"funnel": "./lib/index.ts"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"lib/**/*.ts",
|
|
33
|
+
"lib/**/*.tsx",
|
|
34
|
+
"!lib/**/*.test.ts",
|
|
35
|
+
"!lib/**/*.test.tsx",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"bun": ">=1.3.0"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@hono/zod-validator": "^0.7.6",
|
|
47
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
48
|
+
"@opentui/core": "^0.1.97",
|
|
49
|
+
"@opentui/react": "^0.1.97",
|
|
50
|
+
"@slack/bolt": "^4.7.0",
|
|
51
|
+
"@slack/web-api": "^7.15.0",
|
|
52
|
+
"@tanstack/react-query": "^5.99.0",
|
|
53
|
+
"@types/react": "^19.2.14",
|
|
54
|
+
"discord.js": "^14.26.3",
|
|
55
|
+
"hono": "^4.12.12",
|
|
56
|
+
"react": "^19.2.5",
|
|
57
|
+
"yaml": "^2.8.3",
|
|
58
|
+
"zod": "^4.3.6"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/bun": "^1.3.12",
|
|
62
|
+
"@typescript/native-preview": "^7.0.0-dev.20260411.1",
|
|
63
|
+
"vite-plus": "^0.1.18"
|
|
64
|
+
}
|
|
65
|
+
}
|