@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.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/lib/factory.ts +10 -0
  4. package/lib/funnel.ts +51 -0
  5. package/lib/index.ts +86 -0
  6. package/lib/modules/agents/funnel-agents.ts +105 -0
  7. package/lib/modules/channels/funnel-channels.ts +113 -0
  8. package/lib/modules/claude/funnel-claude.ts +136 -0
  9. package/lib/modules/connectors/funnel-connector-adapter.ts +9 -0
  10. package/lib/modules/connectors/funnel-connector-listener.ts +5 -0
  11. package/lib/modules/connectors/funnel-connectors.ts +124 -0
  12. package/lib/modules/connectors/funnel-discord-adapter.ts +56 -0
  13. package/lib/modules/connectors/funnel-discord-event-processor.ts +48 -0
  14. package/lib/modules/connectors/funnel-discord-listener.ts +65 -0
  15. package/lib/modules/connectors/funnel-gh-adapter.ts +51 -0
  16. package/lib/modules/connectors/funnel-gh-listener.ts +102 -0
  17. package/lib/modules/connectors/funnel-slack-adapter.ts +31 -0
  18. package/lib/modules/connectors/funnel-slack-event-processor.ts +91 -0
  19. package/lib/modules/connectors/funnel-slack-listener.ts +72 -0
  20. package/lib/modules/connectors/resolve-listener.ts +13 -0
  21. package/lib/modules/fs/funnel-file-system.ts +14 -0
  22. package/lib/modules/fs/memory-funnel-file-system.ts +85 -0
  23. package/lib/modules/fs/node-funnel-file-system.ts +56 -0
  24. package/lib/modules/gateway/daemon.ts +190 -0
  25. package/lib/modules/gateway/funnel-broadcaster.ts +37 -0
  26. package/lib/modules/gateway/funnel-event-logger.ts +59 -0
  27. package/lib/modules/gateway/funnel-gateway.ts +166 -0
  28. package/lib/modules/gateway/kill-competing-slack-gateways.ts +52 -0
  29. package/lib/modules/http/funnel-http-client.ts +17 -0
  30. package/lib/modules/http/memory-funnel-http-client.ts +40 -0
  31. package/lib/modules/http/node-funnel-http-client.ts +27 -0
  32. package/lib/modules/logger.ts +26 -0
  33. package/lib/modules/mcp/channel-server.ts +77 -0
  34. package/lib/modules/mcp/funnel-mcp.ts +107 -0
  35. package/lib/modules/process/funnel-process-runner.ts +28 -0
  36. package/lib/modules/process/memory-funnel-process-runner.ts +88 -0
  37. package/lib/modules/process/node-funnel-process-runner.ts +100 -0
  38. package/lib/modules/repos/funnel-repositories.ts +107 -0
  39. package/lib/modules/router/query-to-cli-args.ts +20 -0
  40. package/lib/modules/router/to-request.ts +122 -0
  41. package/lib/modules/router/validator.ts +27 -0
  42. package/lib/modules/settings/funnel-settings-reader.ts +6 -0
  43. package/lib/modules/settings/funnel-settings-store.ts +57 -0
  44. package/lib/modules/settings/mock-funnel-settings-reader.ts +27 -0
  45. package/lib/modules/settings/settings-schema.ts +67 -0
  46. package/lib/modules/tui/app.tsx +44 -0
  47. package/lib/modules/tui/tui.tsx +13 -0
  48. package/lib/routes/agents/add.help.ts +3 -0
  49. package/lib/routes/agents/add.ts +33 -0
  50. package/lib/routes/agents/group.help.ts +13 -0
  51. package/lib/routes/agents/group.ts +25 -0
  52. package/lib/routes/agents/launch.help.ts +3 -0
  53. package/lib/routes/agents/launch.ts +35 -0
  54. package/lib/routes/agents/remove.help.ts +3 -0
  55. package/lib/routes/agents/remove.ts +17 -0
  56. package/lib/routes/agents/rename.help.ts +5 -0
  57. package/lib/routes/agents/rename.ts +17 -0
  58. package/lib/routes/agents/routes.ts +17 -0
  59. package/lib/routes/agents/set.help.ts +5 -0
  60. package/lib/routes/agents/set.ts +32 -0
  61. package/lib/routes/channels/add.help.ts +3 -0
  62. package/lib/routes/channels/add.ts +21 -0
  63. package/lib/routes/channels/connectors-attach.help.ts +3 -0
  64. package/lib/routes/channels/connectors-attach.ts +17 -0
  65. package/lib/routes/channels/connectors-detach.help.ts +3 -0
  66. package/lib/routes/channels/connectors-detach.ts +17 -0
  67. package/lib/routes/channels/group.help.ts +16 -0
  68. package/lib/routes/channels/group.ts +22 -0
  69. package/lib/routes/channels/remove.help.ts +3 -0
  70. package/lib/routes/channels/remove.ts +17 -0
  71. package/lib/routes/channels/rename.help.ts +5 -0
  72. package/lib/routes/channels/rename.ts +17 -0
  73. package/lib/routes/channels/routes.ts +19 -0
  74. package/lib/routes/channels/show.help.ts +1 -0
  75. package/lib/routes/channels/show.ts +26 -0
  76. package/lib/routes/claude/claude.help.ts +11 -0
  77. package/lib/routes/claude/claude.ts +39 -0
  78. package/lib/routes/claude/routes.ts +4 -0
  79. package/lib/routes/connectors/add.help.ts +22 -0
  80. package/lib/routes/connectors/add.ts +55 -0
  81. package/lib/routes/connectors/call.help.ts +17 -0
  82. package/lib/routes/connectors/call.ts +43 -0
  83. package/lib/routes/connectors/group.help.ts +14 -0
  84. package/lib/routes/connectors/group.ts +18 -0
  85. package/lib/routes/connectors/remove.help.ts +3 -0
  86. package/lib/routes/connectors/remove.ts +17 -0
  87. package/lib/routes/connectors/rename.help.ts +5 -0
  88. package/lib/routes/connectors/rename.ts +17 -0
  89. package/lib/routes/connectors/routes.ts +19 -0
  90. package/lib/routes/connectors/set.help.ts +8 -0
  91. package/lib/routes/connectors/set.ts +30 -0
  92. package/lib/routes/connectors/show.help.ts +1 -0
  93. package/lib/routes/connectors/show.ts +32 -0
  94. package/lib/routes/gateway/group.help.ts +15 -0
  95. package/lib/routes/gateway/group.ts +28 -0
  96. package/lib/routes/gateway/logs.help.ts +13 -0
  97. package/lib/routes/gateway/logs.ts +100 -0
  98. package/lib/routes/gateway/restart.help.ts +10 -0
  99. package/lib/routes/gateway/restart.ts +35 -0
  100. package/lib/routes/gateway/routes.ts +18 -0
  101. package/lib/routes/gateway/run.help.ts +12 -0
  102. package/lib/routes/gateway/run.ts +35 -0
  103. package/lib/routes/gateway/start.help.ts +15 -0
  104. package/lib/routes/gateway/start.ts +32 -0
  105. package/lib/routes/gateway/status.help.ts +9 -0
  106. package/lib/routes/gateway/status.ts +28 -0
  107. package/lib/routes/gateway/stop.help.ts +8 -0
  108. package/lib/routes/gateway/stop.ts +21 -0
  109. package/lib/routes/repos/add.help.ts +5 -0
  110. package/lib/routes/repos/add.ts +19 -0
  111. package/lib/routes/repos/group.help.ts +11 -0
  112. package/lib/routes/repos/group.ts +18 -0
  113. package/lib/routes/repos/remove.help.ts +3 -0
  114. package/lib/routes/repos/remove.ts +17 -0
  115. package/lib/routes/repos/rename.help.ts +5 -0
  116. package/lib/routes/repos/rename.ts +17 -0
  117. package/lib/routes/repos/routes.ts +17 -0
  118. package/lib/routes/repos/set.help.ts +5 -0
  119. package/lib/routes/repos/set.ts +21 -0
  120. package/lib/routes/repos/show.help.ts +1 -0
  121. package/lib/routes/repos/show.ts +19 -0
  122. package/lib/routes/status/routes.ts +4 -0
  123. package/lib/routes/status/status.help.ts +6 -0
  124. package/lib/routes/status/status.ts +77 -0
  125. package/lib/routes.ts +36 -0
  126. package/package.json +65 -0
@@ -0,0 +1,27 @@
1
+ import { zValidator as zv } from "@hono/zod-validator"
2
+ import { HTTPException } from "hono/http-exception"
3
+ import type { ZodType } from "zod"
4
+
5
+ export const zValidator = <Target extends "param" | "query" | "json", T extends ZodType>(
6
+ target: Target,
7
+ schema: T,
8
+ helpText?: string,
9
+ ) =>
10
+ zv(target, schema, (result, c) => {
11
+ if (helpText && c.req.query("help")) {
12
+ return c.text(helpText)
13
+ }
14
+
15
+ if (result.success) return
16
+
17
+ const issue = result.error.issues[0]
18
+
19
+ if (!issue) {
20
+ throw new HTTPException(400, { message: "invalid request" })
21
+ }
22
+
23
+ const path = issue.path.join(".")
24
+ const message = path ? `${path}: ${issue.message}` : issue.message
25
+
26
+ throw new HTTPException(400, { message })
27
+ })
@@ -0,0 +1,6 @@
1
+ import type { Settings } from "@/modules/settings/settings-schema"
2
+
3
+ export abstract class FunnelSettingsReader {
4
+ abstract read(): Settings
5
+ abstract write(settings: Settings): void
6
+ }
@@ -0,0 +1,57 @@
1
+ import { homedir } from "node:os"
2
+ import { dirname, join } from "node:path"
3
+ import { FunnelFileSystem } from "@/modules/fs/funnel-file-system"
4
+ import { NodeFunnelFileSystem } from "@/modules/fs/node-funnel-file-system"
5
+ import { FunnelSettingsReader } from "@/modules/settings/funnel-settings-reader"
6
+ import { settingsSchema } from "@/modules/settings/settings-schema"
7
+ import type { Settings } from "@/modules/settings/settings-schema"
8
+
9
+ export const FUNNEL_DIR = join(homedir(), ".funnel")
10
+ export const SETTINGS_PATH = join(FUNNEL_DIR, "settings.json")
11
+
12
+ type Deps = {
13
+ path?: string
14
+ fs?: FunnelFileSystem
15
+ }
16
+
17
+ const defaultFs = new NodeFunnelFileSystem()
18
+
19
+ export class FunnelSettingsStore extends FunnelSettingsReader {
20
+ private readonly path: string
21
+ private readonly fs: FunnelFileSystem
22
+
23
+ constructor(deps: Deps = {}) {
24
+ super()
25
+ this.path = deps.path ?? SETTINGS_PATH
26
+ this.fs = deps.fs ?? defaultFs
27
+ Object.freeze(this)
28
+ }
29
+
30
+ read(): Settings {
31
+ if (!this.fs.existsSync(this.path)) {
32
+ return {
33
+ connectors: [],
34
+ channels: [],
35
+ repositories: [],
36
+ agents: [],
37
+ }
38
+ }
39
+
40
+ const content = this.fs.readFileSync(this.path)
41
+ const parsed = JSON.parse(content)
42
+ const result = settingsSchema.safeParse(parsed)
43
+
44
+ if (!result.success) {
45
+ throw new Error(
46
+ `invalid settings.json (${this.path}): ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ")}`,
47
+ )
48
+ }
49
+
50
+ return result.data
51
+ }
52
+
53
+ write(settings: Settings): void {
54
+ this.fs.mkdirSync(dirname(this.path), { recursive: true })
55
+ this.fs.writeFileSync(this.path, `${JSON.stringify(settings, null, 2)}\n`)
56
+ }
57
+ }
@@ -0,0 +1,27 @@
1
+ import { FunnelSettingsReader } from "@/modules/settings/funnel-settings-reader"
2
+ import type { Settings } from "@/modules/settings/settings-schema"
3
+
4
+ export const createSettings = (partial: Partial<Settings> = {}): Settings => ({
5
+ connectors: [],
6
+ channels: [],
7
+ repositories: [],
8
+ agents: [],
9
+ ...partial,
10
+ })
11
+
12
+ export class MockFunnelSettingsReader extends FunnelSettingsReader {
13
+ private state: Settings
14
+
15
+ constructor(initial?: Partial<Settings>) {
16
+ super()
17
+ this.state = createSettings(initial)
18
+ }
19
+
20
+ read(): Settings {
21
+ return this.state
22
+ }
23
+
24
+ write(settings: Settings): void {
25
+ this.state = settings
26
+ }
27
+ }
@@ -0,0 +1,67 @@
1
+ import { z } from "zod"
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
+ export const channelConfigSchema = z.object({
37
+ name: z.string(),
38
+ connectors: z.array(z.string()).default([]),
39
+ })
40
+
41
+ export type ChannelConfig = z.infer<typeof channelConfigSchema>
42
+
43
+ export const repositoryConfigSchema = z.object({
44
+ name: z.string(),
45
+ path: z.string(),
46
+ })
47
+
48
+ export type RepositoryConfig = z.infer<typeof repositoryConfigSchema>
49
+
50
+ export const agentConfigSchema = z.object({
51
+ name: z.string(),
52
+ channel: z.string(),
53
+ repo: z.string().optional(),
54
+ subAgent: z.string().optional(),
55
+ envFiles: z.array(z.string()).optional(),
56
+ })
57
+
58
+ export type AgentConfig = z.infer<typeof agentConfigSchema>
59
+
60
+ export const settingsSchema = z.object({
61
+ connectors: z.array(connectorConfigSchema).default([]),
62
+ channels: z.array(channelConfigSchema).default([]),
63
+ repositories: z.array(repositoryConfigSchema).default([]),
64
+ agents: z.array(agentConfigSchema).default([]),
65
+ })
66
+
67
+ export type Settings = z.infer<typeof settingsSchema>
@@ -0,0 +1,44 @@
1
+ import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
2
+
3
+ const zinc = {
4
+ 50: "#fafafa",
5
+ 600: "#52525b",
6
+ 950: "#09090b",
7
+ } as const
8
+
9
+ const LABEL = "funnel"
10
+
11
+ export function App() {
12
+ const renderer = useRenderer()
13
+
14
+ const dimensions = useTerminalDimensions()
15
+
16
+ useKeyboard((key) => {
17
+ if (key.name === "q" || key.name === "escape" || (key.ctrl && key.name === "c")) {
18
+ renderer.destroy()
19
+ }
20
+ })
21
+
22
+ const marginLeft = Math.max(0, Math.floor((dimensions.width - LABEL.length) / 2))
23
+
24
+ const marginTop = Math.max(0, Math.floor(dimensions.height / 2) - 1)
25
+
26
+ return (
27
+ <box
28
+ style={{
29
+ width: "100%",
30
+ height: "100%",
31
+ flexDirection: "column",
32
+ backgroundColor: zinc[950],
33
+ }}
34
+ >
35
+ <box style={{ height: marginTop, backgroundColor: zinc[950] }} />
36
+ <text fg={zinc[50]} bg={zinc[950]} style={{ marginLeft }}>
37
+ {LABEL}
38
+ </text>
39
+ <text fg={zinc[600]} bg={zinc[950]} position="absolute" bottom={0}>
40
+ press q to quit
41
+ </text>
42
+ </box>
43
+ )
44
+ }
@@ -0,0 +1,13 @@
1
+ import { createCliRenderer } from "@opentui/core"
2
+ import { createRoot } from "@opentui/react"
3
+ import { App } from "@/modules/tui/app"
4
+
5
+ export async function launchTui(): Promise<void> {
6
+ const renderer = await createCliRenderer()
7
+
8
+ createRoot(renderer).render(<App />)
9
+
10
+ await new Promise<void>((resolve) => {
11
+ renderer.once("destroy", () => resolve())
12
+ })
13
+ }
@@ -0,0 +1,3 @@
1
+ export const help = `funnel agents add — add an agent preset
2
+
3
+ usage: funnel agents add <name> --channel <ch> [--repo <r>] [--sub-agent <s>] [--env-file <f>]`
@@ -0,0 +1,33 @@
1
+ import { z } from "zod"
2
+ import { factory } from "@/factory"
3
+ import { zValidator } from "@/modules/router/validator"
4
+ import { help } from "@/routes/agents/add.help"
5
+
6
+ export const agentsAddHandler = factory.createHandlers(
7
+ zValidator("param", z.object({ name: z.string() })),
8
+ zValidator(
9
+ "query",
10
+ z.object({
11
+ channel: z.string(),
12
+ repo: z.string().optional(),
13
+ "sub-agent": z.string().optional(),
14
+ "env-file": 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
+ funnel.agents.add({
24
+ name: param.name,
25
+ channel: query.channel,
26
+ repo: query.repo,
27
+ subAgent: query["sub-agent"],
28
+ envFiles: query["env-file"] ? [query["env-file"]] : undefined,
29
+ })
30
+
31
+ return c.text(`added agent "${param.name}"`)
32
+ },
33
+ )
@@ -0,0 +1,13 @@
1
+ export const help = `funnel agents — agent presets (extra)
2
+
3
+ usage: funnel agents [subcommand]
4
+
5
+ subcommands:
6
+ (none) list
7
+ add <name> --channel <ch> [--repo <r>] [--sub-agent <s>] [--env-file <f>]
8
+ remove <name> remove
9
+ <name> launch (sugar for fnl claude)
10
+
11
+ examples:
12
+ funnel agents add cto --channel prod-inbox --repo myapp --sub-agent cto
13
+ funnel agents cto`
@@ -0,0 +1,25 @@
1
+ import { z } from "zod"
2
+ import { factory } from "@/factory"
3
+ import { zValidator } from "@/modules/router/validator"
4
+ import { help } from "@/routes/agents/group.help"
5
+
6
+ export const agentsGroupHandler = factory.createHandlers(
7
+ zValidator("query", z.object({}), help),
8
+ (c) => {
9
+ const funnel = c.var.funnel
10
+ const agents = funnel.agents.list()
11
+
12
+ if (agents.length === 0) return c.text("no agents")
13
+
14
+ const lines = agents.map((agent) => {
15
+ const parts = [`channel=${agent.channel}`]
16
+
17
+ if (agent.repo) parts.push(`repo=${agent.repo}`)
18
+ if (agent.subAgent) parts.push(`subAgent=${agent.subAgent}`)
19
+
20
+ return `${agent.name} [${parts.join(", ")}]`
21
+ })
22
+
23
+ return c.text(lines.join("\n"))
24
+ },
25
+ )
@@ -0,0 +1,3 @@
1
+ export const help = `funnel agents <name> — launch an agent (sugar for fnl claude)
2
+
3
+ usage: funnel agents <name> [additional claude args...]`
@@ -0,0 +1,35 @@
1
+ import { HTTPException } from "hono/http-exception"
2
+ import { z } from "zod"
3
+ import { factory } from "@/factory"
4
+ import { queryToCliArgs } from "@/modules/router/query-to-cli-args"
5
+ import { zValidator } from "@/modules/router/validator"
6
+ import { help } from "@/routes/agents/launch.help"
7
+
8
+ const RESERVED_KEYS = ["channel", "repo", "sub-agent", "env-file"]
9
+
10
+ export const agentsLaunchHandler = factory.createHandlers(
11
+ zValidator("param", z.object({ name: z.string() })),
12
+ zValidator("query", z.object({}).passthrough(), help),
13
+ async (c) => {
14
+ const param = c.req.valid("param")
15
+ const funnel = c.var.funnel
16
+ const agent = funnel.agents.get(param.name)
17
+
18
+ if (!agent) throw new HTTPException(404, { message: `agent "${param.name}" not found` })
19
+
20
+ const overrideChannel = c.req.query("channel")
21
+ const overrideRepo = c.req.query("repo")
22
+ const overrideSubAgent = c.req.query("sub-agent")
23
+ const overrideEnvFile = c.req.query("env-file")
24
+
25
+ const exitCode = await funnel.claude.launch({
26
+ channel: overrideChannel ?? agent.channel,
27
+ repo: overrideRepo ?? agent.repo,
28
+ subAgent: overrideSubAgent ?? agent.subAgent,
29
+ envFiles: overrideEnvFile ? [overrideEnvFile] : agent.envFiles,
30
+ userArgs: queryToCliArgs(c.req.url, RESERVED_KEYS),
31
+ })
32
+
33
+ process.exit(exitCode)
34
+ },
35
+ )
@@ -0,0 +1,3 @@
1
+ export const help = `funnel agents remove — remove an agent preset
2
+
3
+ usage: funnel agents remove <name>`
@@ -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/agents/remove.help"
5
+
6
+ export const agentsRemoveHandler = 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.agents.remove(param.name)
14
+
15
+ return c.text(`removed agent "${param.name}"`)
16
+ },
17
+ )
@@ -0,0 +1,5 @@
1
+ export const help = `funnel agents rename — rename an agent preset
2
+
3
+ usage:
4
+ funnel agents rename <old> <new>
5
+ funnel agents <old> rename <new>`
@@ -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/agents/rename.help"
5
+
6
+ export const agentsRenameHandler = 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.agents.rename(param.name, param["newName"])
14
+
15
+ return c.text(`renamed agent "${param.name}" to "${param["newName"]}"`)
16
+ },
17
+ )
@@ -0,0 +1,17 @@
1
+ import { factory } from "@/factory"
2
+ import { agentsAddHandler } from "@/routes/agents/add"
3
+ import { agentsGroupHandler } from "@/routes/agents/group"
4
+ import { agentsLaunchHandler } from "@/routes/agents/launch"
5
+ import { agentsRemoveHandler } from "@/routes/agents/remove"
6
+ import { agentsRenameHandler } from "@/routes/agents/rename"
7
+ import { agentsSetHandler } from "@/routes/agents/set"
8
+
9
+ export const agentsRoutes = factory
10
+ .createApp()
11
+ .get("/", ...agentsGroupHandler)
12
+ .put("/:name/rename/:newName", ...agentsRenameHandler)
13
+ .put("/rename/:name/:newName", ...agentsRenameHandler)
14
+ .post("/:name", ...agentsAddHandler)
15
+ .put("/:name", ...agentsSetHandler)
16
+ .delete("/:name", ...agentsRemoveHandler)
17
+ .get("/:name", ...agentsLaunchHandler)
@@ -0,0 +1,5 @@
1
+ export const help = `funnel agents <name> set — update an agent preset
2
+
3
+ usage: funnel agents <name> set [--channel <ch>] [--repo <r>] [--sub-agent <s>] [--env-file <f>]
4
+
5
+ pass an empty string to --repo / --sub-agent to unset them.`
@@ -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/agents/set.help"
5
+
6
+ export const agentsSetHandler = factory.createHandlers(
7
+ zValidator("param", z.object({ name: z.string() })),
8
+ zValidator(
9
+ "query",
10
+ z.object({
11
+ channel: z.string().optional(),
12
+ repo: z.string().optional(),
13
+ "sub-agent": z.string().optional(),
14
+ "env-file": 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
+ funnel.agents.update(param.name, {
24
+ channel: query.channel,
25
+ repo: query.repo,
26
+ subAgent: query["sub-agent"],
27
+ envFiles: query["env-file"] !== undefined ? [query["env-file"]] : undefined,
28
+ })
29
+
30
+ return c.text(`updated agent "${param.name}"`)
31
+ },
32
+ )
@@ -0,0 +1,3 @@
1
+ export const help = `funnel channels add — add a channel
2
+
3
+ usage: funnel channels add <name> [--connector <c>]...`
@@ -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/channels/add.help"
5
+
6
+ export const channelsAddHandler = factory.createHandlers(
7
+ zValidator("param", z.object({ name: z.string() })),
8
+ zValidator("query", z.object({ connector: z.string().optional() }), help),
9
+ (c) => {
10
+ const param = c.req.valid("param")
11
+ const query = c.req.valid("query")
12
+ const funnel = c.var.funnel
13
+
14
+ funnel.channels.add({
15
+ name: param.name,
16
+ connectors: query.connector ? [query.connector] : [],
17
+ })
18
+
19
+ return c.text(`added channel "${param.name}"`)
20
+ },
21
+ )
@@ -0,0 +1,3 @@
1
+ export const help = `funnel channels <name> connectors attach — subscribe to a connector
2
+
3
+ usage: funnel channels <name> connectors attach <connector>`
@@ -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/channels/connectors-attach.help"
5
+
6
+ export const channelsConnectorsAttachHandler = factory.createHandlers(
7
+ zValidator("param", z.object({ name: z.string(), connector: 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.channels.attachConnector(param.name, param.connector)
14
+
15
+ return c.text(`attached connector "${param.connector}" to channel "${param.name}"`)
16
+ },
17
+ )
@@ -0,0 +1,3 @@
1
+ export const help = `funnel channels <name> connectors detach — unsubscribe from a connector
2
+
3
+ usage: funnel channels <name> connectors detach <connector>`
@@ -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/channels/connectors-detach.help"
5
+
6
+ export const channelsConnectorsDetachHandler = factory.createHandlers(
7
+ zValidator("param", z.object({ name: z.string(), connector: 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.channels.detachConnector(param.name, param.connector)
14
+
15
+ return c.text(`detached connector "${param.connector}" from channel "${param.name}"`)
16
+ },
17
+ )
@@ -0,0 +1,16 @@
1
+ export const help = `funnel channels — manage subscription boxes
2
+
3
+ usage: funnel channels [subcommand]
4
+
5
+ subcommands:
6
+ (none) list
7
+ add <name> add
8
+ remove <name> remove
9
+ <name> show details
10
+ <name> connectors attach <c> subscribe to a connector
11
+ <name> connectors detach <c> unsubscribe from a connector
12
+
13
+ examples:
14
+ funnel channels add prod-inbox
15
+ funnel channels prod-inbox connectors attach prod-slack
16
+ funnel channels prod-inbox`
@@ -0,0 +1,22 @@
1
+ import { z } from "zod"
2
+ import { factory } from "@/factory"
3
+ import { zValidator } from "@/modules/router/validator"
4
+ import { help } from "@/routes/channels/group.help"
5
+
6
+ export const channelsGroupHandler = factory.createHandlers(
7
+ zValidator("query", z.object({}), help),
8
+ (c) => {
9
+ const funnel = c.var.funnel
10
+ const channels = funnel.channels.list()
11
+
12
+ if (channels.length === 0) return c.text("no channels")
13
+
14
+ const lines = channels.map((ch) => {
15
+ const connectors = ch.connectors.length > 0 ? ch.connectors.join(", ") : "(none)"
16
+
17
+ return `${ch.name} [${connectors}]`
18
+ })
19
+
20
+ return c.text(lines.join("\n"))
21
+ },
22
+ )
@@ -0,0 +1,3 @@
1
+ export const help = `funnel channels remove — remove a channel
2
+
3
+ usage: funnel channels remove <name>`
@@ -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/channels/remove.help"
5
+
6
+ export const channelsRemoveHandler = 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.channels.remove(param.name)
14
+
15
+ return c.text(`removed channel "${param.name}"`)
16
+ },
17
+ )
@@ -0,0 +1,5 @@
1
+ export const help = `funnel channels rename — rename a channel
2
+
3
+ usage:
4
+ funnel channels rename <old> <new>
5
+ funnel channels <old> rename <new>`
@@ -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/channels/rename.help"
5
+
6
+ export const channelsRenameHandler = 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.channels.rename(param.name, param["newName"])
14
+
15
+ return c.text(`renamed channel "${param.name}" to "${param["newName"]}"`)
16
+ },
17
+ )
@@ -0,0 +1,19 @@
1
+ import { factory } from "@/factory"
2
+ import { channelsAddHandler } from "@/routes/channels/add"
3
+ import { channelsConnectorsAttachHandler } from "@/routes/channels/connectors-attach"
4
+ import { channelsConnectorsDetachHandler } from "@/routes/channels/connectors-detach"
5
+ import { channelsGroupHandler } from "@/routes/channels/group"
6
+ import { channelsRemoveHandler } from "@/routes/channels/remove"
7
+ import { channelsRenameHandler } from "@/routes/channels/rename"
8
+ import { channelsShowHandler } from "@/routes/channels/show"
9
+
10
+ export const channelsRoutes = factory
11
+ .createApp()
12
+ .get("/", ...channelsGroupHandler)
13
+ .put("/:name/rename/:newName", ...channelsRenameHandler)
14
+ .put("/rename/:name/:newName", ...channelsRenameHandler)
15
+ .put("/:name/connectors/attach/:connector", ...channelsConnectorsAttachHandler)
16
+ .delete("/:name/connectors/detach/:connector", ...channelsConnectorsDetachHandler)
17
+ .post("/:name", ...channelsAddHandler)
18
+ .delete("/:name", ...channelsRemoveHandler)
19
+ .get("/:name", ...channelsShowHandler)