@interactive-inc/claude-funnel 0.10.0 → 0.15.1

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 (236) hide show
  1. package/README.md +106 -56
  2. package/dist/bin.js +557 -530
  3. package/dist/connectors/schedule.d.ts +2 -49
  4. package/dist/connectors/schedule.js +1 -1
  5. package/dist/connectors/slack.d.ts +4 -48
  6. package/dist/connectors/slack.js +1 -1
  7. package/dist/gateway/daemon.js +213 -211
  8. package/dist/index.d.ts +465 -173
  9. package/dist/index.js +692 -154
  10. package/dist/{schedule-connector-schema-CkuIQ0JQ.js → schedule-connector-schema-FxP7LPlx.js} +11 -0
  11. package/dist/{file-system-Co60LrmR.d.ts → schedule-listener-BPodvbld.d.ts} +56 -1
  12. package/dist/{slack-connector-schema-Cd22WiHB.js → slack-connector-schema-B4hsf3AY.js} +10 -1
  13. package/dist/slack-listener-CHj6uMY-.d.ts +74 -0
  14. package/package.json +2 -6
  15. package/schemas/funnel.schema.json +144 -0
  16. package/dist/slack-connector-schema-D7zAHN8k.d.ts +0 -15
  17. package/lib/bin.ts +0 -3
  18. package/lib/cli/factory.ts +0 -10
  19. package/lib/cli/index.ts +0 -85
  20. package/lib/cli/router/query-to-cli-args.ts +0 -20
  21. package/lib/cli/router/to-request.ts +0 -113
  22. package/lib/cli/router/validator.ts +0 -27
  23. package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +0 -27
  24. package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +0 -40
  25. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +0 -41
  26. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +0 -22
  27. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +0 -23
  28. package/lib/cli/routes/channels.$channel.connectors.$connector.ts +0 -26
  29. package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +0 -92
  30. package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +0 -22
  31. package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +0 -63
  32. package/lib/cli/routes/channels.$channel.connectors.ts +0 -26
  33. package/lib/cli/routes/channels.$channel.publish.ts +0 -52
  34. package/lib/cli/routes/channels.$channel.rename.$newName.ts +0 -22
  35. package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +0 -34
  36. package/lib/cli/routes/channels.$channel.ts +0 -34
  37. package/lib/cli/routes/channels.add.$channel.ts +0 -33
  38. package/lib/cli/routes/channels.remove.$channel.ts +0 -20
  39. package/lib/cli/routes/channels.ts +0 -39
  40. package/lib/cli/routes/claude.ts +0 -70
  41. package/lib/cli/routes/gateway.listeners.ts +0 -41
  42. package/lib/cli/routes/gateway.logs.ts +0 -123
  43. package/lib/cli/routes/gateway.restart.ts +0 -50
  44. package/lib/cli/routes/gateway.run.ts +0 -41
  45. package/lib/cli/routes/gateway.start.ts +0 -50
  46. package/lib/cli/routes/gateway.status.ts +0 -19
  47. package/lib/cli/routes/gateway.stop.ts +0 -32
  48. package/lib/cli/routes/gateway.ts +0 -55
  49. package/lib/cli/routes/index.ts +0 -219
  50. package/lib/cli/routes/profiles.$profile.as-default.ts +0 -22
  51. package/lib/cli/routes/profiles.$profile.rename.$newName.ts +0 -22
  52. package/lib/cli/routes/profiles.$profile.run.ts +0 -36
  53. package/lib/cli/routes/profiles.add.$profile.ts +0 -49
  54. package/lib/cli/routes/profiles.remove.$profile.ts +0 -20
  55. package/lib/cli/routes/profiles.set.$profile.ts +0 -45
  56. package/lib/cli/routes/profiles.ts +0 -40
  57. package/lib/cli/routes/status.ts +0 -93
  58. package/lib/cli/routes/update.ts +0 -27
  59. package/lib/connectors/connector-adapter.ts +0 -9
  60. package/lib/connectors/connector-config-schema.ts +0 -16
  61. package/lib/connectors/connector-factory.ts +0 -94
  62. package/lib/connectors/connector-listener.ts +0 -20
  63. package/lib/connectors/discord-adapter.ts +0 -51
  64. package/lib/connectors/discord-connector-schema.ts +0 -12
  65. package/lib/connectors/discord-event-processor.ts +0 -48
  66. package/lib/connectors/discord-listener.ts +0 -111
  67. package/lib/connectors/discord.ts +0 -4
  68. package/lib/connectors/gh-adapter.ts +0 -48
  69. package/lib/connectors/gh-connector-schema.ts +0 -12
  70. package/lib/connectors/gh-listener.ts +0 -137
  71. package/lib/connectors/gh.ts +0 -3
  72. package/lib/connectors/match-cron.ts +0 -78
  73. package/lib/connectors/schedule-connector-schema.ts +0 -33
  74. package/lib/connectors/schedule-listener.ts +0 -207
  75. package/lib/connectors/schedule-state-store.ts +0 -54
  76. package/lib/connectors/schedule.ts +0 -4
  77. package/lib/connectors/slack-adapter.ts +0 -36
  78. package/lib/connectors/slack-connector-schema.ts +0 -13
  79. package/lib/connectors/slack-event-processor.ts +0 -97
  80. package/lib/connectors/slack-listener.ts +0 -97
  81. package/lib/connectors/slack.ts +0 -4
  82. package/lib/engine/channels/channels.ts +0 -520
  83. package/lib/engine/claude/claude.ts +0 -205
  84. package/lib/engine/claude/gateway-controller.ts +0 -4
  85. package/lib/engine/fs/file-system.ts +0 -23
  86. package/lib/engine/fs/memory-file-system.ts +0 -102
  87. package/lib/engine/fs/node-file-system.ts +0 -68
  88. package/lib/engine/http/http-client.ts +0 -17
  89. package/lib/engine/http/memory-http-client.ts +0 -36
  90. package/lib/engine/http/node-http-client.ts +0 -23
  91. package/lib/engine/id/id-generator.ts +0 -7
  92. package/lib/engine/id/memory-id-generator.ts +0 -20
  93. package/lib/engine/id/node-id-generator.ts +0 -7
  94. package/lib/engine/logger/logger.ts +0 -11
  95. package/lib/engine/logger/memory-logger.ts +0 -28
  96. package/lib/engine/logger/node-logger.ts +0 -49
  97. package/lib/engine/logger/noop-logger.ts +0 -9
  98. package/lib/engine/mcp/channel-server.ts +0 -123
  99. package/lib/engine/mcp/channel-subscriber.ts +0 -82
  100. package/lib/engine/mcp/mcp.ts +0 -126
  101. package/lib/engine/mcp/read-channel-connectors.ts +0 -34
  102. package/lib/engine/mcp/read-gateway-token.ts +0 -16
  103. package/lib/engine/mcp/usage-hint-for-type.ts +0 -15
  104. package/lib/engine/process/memory-process-runner.ts +0 -88
  105. package/lib/engine/process/node-process-runner.ts +0 -91
  106. package/lib/engine/process/process-runner.ts +0 -33
  107. package/lib/engine/profiles/profile-channel-checker.ts +0 -7
  108. package/lib/engine/profiles/profiles.ts +0 -126
  109. package/lib/engine/settings/mock-settings-reader.ts +0 -27
  110. package/lib/engine/settings/settings-reader.ts +0 -6
  111. package/lib/engine/settings/settings-schema.ts +0 -48
  112. package/lib/engine/settings/settings-store.ts +0 -110
  113. package/lib/engine/time/clock.ts +0 -15
  114. package/lib/engine/time/memory-clock.ts +0 -26
  115. package/lib/engine/time/node-clock.ts +0 -7
  116. package/lib/funnel.ts +0 -294
  117. package/lib/gateway/auth-middleware.ts +0 -44
  118. package/lib/gateway/broadcaster.ts +0 -319
  119. package/lib/gateway/channel-publisher.ts +0 -67
  120. package/lib/gateway/daemon.ts +0 -47
  121. package/lib/gateway/factory.ts +0 -10
  122. package/lib/gateway/funnel-event-store.ts +0 -155
  123. package/lib/gateway/gateway-server.ts +0 -426
  124. package/lib/gateway/gateway-token.ts +0 -79
  125. package/lib/gateway/gateway.ts +0 -209
  126. package/lib/gateway/kill-competing-slack-gateways.ts +0 -56
  127. package/lib/gateway/listener-supervisor.ts +0 -339
  128. package/lib/gateway/listeners-client.ts +0 -128
  129. package/lib/gateway/publish-schema.ts +0 -27
  130. package/lib/gateway/resolve-daemon-script.ts +0 -26
  131. package/lib/gateway/routes/channels.connectors.call.ts +0 -39
  132. package/lib/gateway/routes/channels.publish.ts +0 -44
  133. package/lib/gateway/routes/health.ts +0 -13
  134. package/lib/gateway/routes/index.ts +0 -26
  135. package/lib/gateway/routes/listeners.list.ts +0 -6
  136. package/lib/gateway/routes/listeners.restart.ts +0 -15
  137. package/lib/gateway/routes/listeners.start.ts +0 -15
  138. package/lib/gateway/routes/listeners.stop.ts +0 -15
  139. package/lib/gateway/routes/route-deps.ts +0 -19
  140. package/lib/gateway/routes/status.ts +0 -15
  141. package/lib/gateway/routes/validator.ts +0 -17
  142. package/lib/index.ts +0 -67
  143. package/lib/logger/leuco-human-file-writer.ts +0 -65
  144. package/lib/logger/leuco-human-logger.ts +0 -98
  145. package/lib/logger/leuco-human-record.ts +0 -16
  146. package/lib/logger/leuco-human-stdout-writer.ts +0 -26
  147. package/lib/logger/leuco-human-writer.ts +0 -14
  148. package/lib/logger/leuco-logger-memory-sink.ts +0 -67
  149. package/lib/logger/leuco-logger-record.ts +0 -13
  150. package/lib/logger/leuco-logger-sink.ts +0 -33
  151. package/lib/logger/leuco-logger-sqlite-sink.ts +0 -355
  152. package/lib/logger/leuco-logger.ts +0 -135
  153. package/lib/tui/app.tsx +0 -357
  154. package/lib/tui/components/add-row.tsx +0 -18
  155. package/lib/tui/components/brand.tsx +0 -27
  156. package/lib/tui/components/card.tsx +0 -44
  157. package/lib/tui/components/detail-bar.tsx +0 -46
  158. package/lib/tui/components/editable-field.tsx +0 -33
  159. package/lib/tui/components/empty-state.tsx +0 -11
  160. package/lib/tui/components/gateway-status.tsx +0 -66
  161. package/lib/tui/components/keymap.tsx +0 -29
  162. package/lib/tui/components/menu-item.tsx +0 -73
  163. package/lib/tui/components/menu.tsx +0 -26
  164. package/lib/tui/components/panel-header.tsx +0 -22
  165. package/lib/tui/components/readonly-field.tsx +0 -18
  166. package/lib/tui/components/section-header.tsx +0 -25
  167. package/lib/tui/components/selection-accent.tsx +0 -32
  168. package/lib/tui/components/session-item.tsx +0 -33
  169. package/lib/tui/components/session-list.tsx +0 -33
  170. package/lib/tui/components/ui/hascii/accordion-item.tsx +0 -88
  171. package/lib/tui/components/ui/hascii/accordion.tsx +0 -96
  172. package/lib/tui/components/ui/hascii/alert-dialog.tsx +0 -43
  173. package/lib/tui/components/ui/hascii/badge.tsx +0 -51
  174. package/lib/tui/components/ui/hascii/breadcrumb.tsx +0 -58
  175. package/lib/tui/components/ui/hascii/button.tsx +0 -194
  176. package/lib/tui/components/ui/hascii/card-content.tsx +0 -14
  177. package/lib/tui/components/ui/hascii/card-description.tsx +0 -13
  178. package/lib/tui/components/ui/hascii/card-footer.tsx +0 -14
  179. package/lib/tui/components/ui/hascii/card-header.tsx +0 -14
  180. package/lib/tui/components/ui/hascii/card-title.tsx +0 -13
  181. package/lib/tui/components/ui/hascii/card.tsx +0 -27
  182. package/lib/tui/components/ui/hascii/checkbox.tsx +0 -65
  183. package/lib/tui/components/ui/hascii/command.tsx +0 -159
  184. package/lib/tui/components/ui/hascii/dialog-content.tsx +0 -14
  185. package/lib/tui/components/ui/hascii/dialog-description.tsx +0 -13
  186. package/lib/tui/components/ui/hascii/dialog-footer.tsx +0 -14
  187. package/lib/tui/components/ui/hascii/dialog-header.tsx +0 -14
  188. package/lib/tui/components/ui/hascii/dialog-title.tsx +0 -13
  189. package/lib/tui/components/ui/hascii/dialog.tsx +0 -27
  190. package/lib/tui/components/ui/hascii/file-tree.tsx +0 -142
  191. package/lib/tui/components/ui/hascii/focus-group.tsx +0 -62
  192. package/lib/tui/components/ui/hascii/form-item.tsx +0 -43
  193. package/lib/tui/components/ui/hascii/input-otp.tsx +0 -86
  194. package/lib/tui/components/ui/hascii/input.tsx +0 -130
  195. package/lib/tui/components/ui/hascii/pagination.tsx +0 -105
  196. package/lib/tui/components/ui/hascii/progress.tsx +0 -28
  197. package/lib/tui/components/ui/hascii/select.tsx +0 -131
  198. package/lib/tui/components/ui/hascii/separator.tsx +0 -35
  199. package/lib/tui/components/ui/hascii/sidebar-content.tsx +0 -23
  200. package/lib/tui/components/ui/hascii/sidebar-header.tsx +0 -14
  201. package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +0 -67
  202. package/lib/tui/components/ui/hascii/sidebar.tsx +0 -24
  203. package/lib/tui/components/ui/hascii/skeleton.tsx +0 -60
  204. package/lib/tui/components/ui/hascii/slider.tsx +0 -91
  205. package/lib/tui/components/ui/hascii/snackbar.tsx +0 -75
  206. package/lib/tui/components/ui/hascii/sparkline.tsx +0 -53
  207. package/lib/tui/components/ui/hascii/spinner.tsx +0 -47
  208. package/lib/tui/components/ui/hascii/stepper.tsx +0 -54
  209. package/lib/tui/components/ui/hascii/switch.tsx +0 -66
  210. package/lib/tui/components/ui/hascii/table.tsx +0 -95
  211. package/lib/tui/components/ui/hascii/tabs.tsx +0 -59
  212. package/lib/tui/components/ui/hascii/toggle-group-item.tsx +0 -45
  213. package/lib/tui/components/ui/hascii/toggle-group.tsx +0 -99
  214. package/lib/tui/components/ui/hascii/tree.tsx +0 -104
  215. package/lib/tui/components/view-shell.tsx +0 -44
  216. package/lib/tui/filter-input.tsx +0 -33
  217. package/lib/tui/hooks/hascii/use-pressable.ts +0 -54
  218. package/lib/tui/parse-comma-list.ts +0 -14
  219. package/lib/tui/profile-launcher.tsx +0 -61
  220. package/lib/tui/scrollbar-options.ts +0 -19
  221. package/lib/tui/sidebar.tsx +0 -50
  222. package/lib/tui/theme.ts +0 -40
  223. package/lib/tui/tui.tsx +0 -20
  224. package/lib/tui/types.ts +0 -38
  225. package/lib/tui/unique-name.ts +0 -18
  226. package/lib/tui/use-event-stream.ts +0 -133
  227. package/lib/tui/use-snapshot.ts +0 -99
  228. package/lib/tui/utils/hascii/form-item-context.tsx +0 -23
  229. package/lib/tui/utils/hascii/input-focus-context.tsx +0 -31
  230. package/lib/tui/utils/hascii/theme-context.tsx +0 -26
  231. package/lib/tui/utils/hascii/theme.ts +0 -176
  232. package/lib/tui/views/channels-view.tsx +0 -108
  233. package/lib/tui/views/connectors-view.tsx +0 -164
  234. package/lib/tui/views/events-view.tsx +0 -160
  235. package/lib/tui/views/listeners-view.tsx +0 -80
  236. package/lib/tui/views/profiles-view.tsx +0 -152
@@ -1,126 +0,0 @@
1
- import { FunnelSettingsReader } from "@/engine/settings/settings-reader"
2
- import type { ProfileConfig } from "@/engine/settings/settings-schema"
3
-
4
- type Deps = {
5
- store: FunnelSettingsReader
6
- }
7
-
8
- /**
9
- * Named launch presets for `fnl claude`. Each profile bundles a working
10
- * directory, a sub-agent name, and the channel id its Claude instance will
11
- * subscribe to. Implements ProfileChannelChecker so FunnelChannels can refuse
12
- * to remove a channel that is still referenced.
13
- *
14
- * The first entry in the persisted array is treated as the default profile;
15
- * `asDefault` reorders the array to put a named profile first.
16
- *
17
- * `channelId` always stores the channel's stable id (uuid). CLI surfaces
18
- * resolve channel name → id before calling `add`/`update` here.
19
- */
20
- export class FunnelProfiles {
21
- private readonly store: FunnelSettingsReader
22
-
23
- constructor(deps: Deps) {
24
- this.store = deps.store
25
- Object.freeze(this)
26
- }
27
-
28
- list(): ProfileConfig[] {
29
- return this.store.read().profiles
30
- }
31
-
32
- get(name: string): ProfileConfig | null {
33
- return this.list().find((p) => p.name === name) ?? null
34
- }
35
-
36
- getDefault(): ProfileConfig | null {
37
- return this.list()[0] ?? null
38
- }
39
-
40
- add(config: ProfileConfig): void {
41
- const settings = this.store.read()
42
-
43
- if (settings.profiles.some((p) => p.name === config.name)) {
44
- throw new Error(`profile "${config.name}" already exists`)
45
- }
46
-
47
- if (!settings.channels.some((c) => c.id === config.channelId)) {
48
- throw new Error(`channel id "${config.channelId}" not found`)
49
- }
50
-
51
- settings.profiles.push(config)
52
-
53
- this.store.write(settings)
54
- }
55
-
56
- remove(name: string): void {
57
- const settings = this.store.read()
58
-
59
- const index = settings.profiles.findIndex((p) => p.name === name)
60
-
61
- if (index < 0) throw new Error(`profile "${name}" not found`)
62
-
63
- settings.profiles.splice(index, 1)
64
-
65
- this.store.write(settings)
66
- }
67
-
68
- rename(oldName: string, newName: string): void {
69
- const settings = this.store.read()
70
-
71
- const profile = settings.profiles.find((p) => p.name === oldName)
72
-
73
- if (!profile) throw new Error(`profile "${oldName}" not found`)
74
-
75
- if (settings.profiles.some((p) => p.name === newName)) {
76
- throw new Error(`profile "${newName}" already exists`)
77
- }
78
-
79
- profile.name = newName
80
-
81
- this.store.write(settings)
82
- }
83
-
84
- asDefault(name: string): void {
85
- const settings = this.store.read()
86
-
87
- const index = settings.profiles.findIndex((p) => p.name === name)
88
-
89
- if (index < 0) throw new Error(`profile "${name}" not found`)
90
-
91
- if (index === 0) return
92
-
93
- const [profile] = settings.profiles.splice(index, 1)
94
-
95
- if (!profile) return
96
-
97
- settings.profiles.unshift(profile)
98
-
99
- this.store.write(settings)
100
- }
101
-
102
- hasChannelRef(channelId: string): boolean {
103
- return this.store.read().profiles.some((p) => p.channelId === channelId)
104
- }
105
-
106
- update(name: string, fields: Partial<Omit<ProfileConfig, "name">>): void {
107
- const settings = this.store.read()
108
-
109
- const profile = settings.profiles.find((p) => p.name === name)
110
-
111
- if (!profile) throw new Error(`profile "${name}" not found`)
112
-
113
- if (fields.channelId !== undefined) {
114
- if (!settings.channels.some((c) => c.id === fields.channelId)) {
115
- throw new Error(`channel id "${fields.channelId}" not found`)
116
- }
117
-
118
- profile.channelId = fields.channelId
119
- }
120
-
121
- if (fields.path !== undefined) profile.path = fields.path
122
- if (fields.subAgent !== undefined) profile.subAgent = fields.subAgent
123
-
124
- this.store.write(settings)
125
- }
126
- }
@@ -1,27 +0,0 @@
1
- import { FunnelSettingsReader } from "@/engine/settings/settings-reader"
2
- import { SETTINGS_VERSION } from "@/engine/settings/settings-schema"
3
- import type { Settings } from "@/engine/settings/settings-schema"
4
-
5
- export const createSettings = (partial: Partial<Settings> = {}): Settings => ({
6
- version: SETTINGS_VERSION,
7
- channels: [],
8
- profiles: [],
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
- }
@@ -1,6 +0,0 @@
1
- import type { Settings } from "@/engine/settings/settings-schema"
2
-
3
- export abstract class FunnelSettingsReader {
4
- abstract read(): Settings
5
- abstract write(settings: Settings): void
6
- }
@@ -1,48 +0,0 @@
1
- import { z } from "zod"
2
- import { connectorConfigSchema } from "@/connectors/connector-config-schema"
3
-
4
- /**
5
- * Routing mode when multiple WS clients are subscribed to the same channel.
6
- *
7
- * - `fanout` (default): every connected client receives every event. Right when each
8
- * subscriber has its own job (e.g., TUI mirrors, distinct Claude profiles each running
9
- * their own pipeline against the same source).
10
- * - `exclusive`: each event is delivered to exactly one connected client, picked
11
- * round-robin per channel. Right when subscribers are interchangeable workers and you
12
- * want each event handled once. Tap=all clients (TUI dashboard) always receive,
13
- * regardless of mode, so they can passively observe.
14
- */
15
- export const channelDeliveryModeSchema = z.enum(["fanout", "exclusive"])
16
-
17
- export type ChannelDeliveryMode = z.infer<typeof channelDeliveryModeSchema>
18
-
19
- export const channelConfigSchema = z.object({
20
- id: z.string(),
21
- name: z.string(),
22
- delivery: channelDeliveryModeSchema.default("fanout"),
23
- connectors: z.array(connectorConfigSchema).default([]),
24
- })
25
-
26
- export type ChannelConfig = z.infer<typeof channelConfigSchema>
27
-
28
- export const profileConfigSchema = z.object({
29
- name: z.string(),
30
- path: z.string(),
31
- subAgent: z.string(),
32
- channelId: z.string(),
33
- /** Forwards `--brief` to claude on launch (enables the SendUserMessage tool). */
34
- brief: z.boolean().optional(),
35
- })
36
-
37
- export type ProfileConfig = z.infer<typeof profileConfigSchema>
38
-
39
- export const SETTINGS_VERSION = 1
40
-
41
- export const settingsSchema = z.object({
42
- /** Schema version. New files always write the current version; older files without one are read as v1. */
43
- version: z.literal(SETTINGS_VERSION).default(SETTINGS_VERSION),
44
- channels: z.array(channelConfigSchema).default([]),
45
- profiles: z.array(profileConfigSchema).default([]),
46
- })
47
-
48
- export type Settings = z.infer<typeof settingsSchema>
@@ -1,110 +0,0 @@
1
- import { homedir } from "node:os"
2
- import { dirname, join } from "node:path"
3
- import { FunnelFileSystem } from "@/engine/fs/file-system"
4
- import { NodeFunnelFileSystem } from "@/engine/fs/node-file-system"
5
- import { FunnelSettingsReader } from "@/engine/settings/settings-reader"
6
- import { SETTINGS_VERSION, settingsSchema } from "@/engine/settings/settings-schema"
7
- import type { Settings } from "@/engine/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
- version: SETTINGS_VERSION,
34
- channels: [],
35
- profiles: [],
36
- }
37
- }
38
-
39
- const content = this.fs.readFileSync(this.path)
40
- const parsed: unknown = JSON.parse(content)
41
-
42
- if (this.looksLikeLegacy(parsed)) {
43
- throw new Error(
44
- `legacy settings.json detected at ${this.path}. The schema changed (channel.connectors are now nested objects with ids; profile fields renamed). Migration is intentionally not provided. Back up and remove the old file:\n mv ${this.path} ${this.path}.bak`,
45
- )
46
- }
47
-
48
- if (
49
- parsed &&
50
- typeof parsed === "object" &&
51
- "version" in parsed &&
52
- parsed.version !== SETTINGS_VERSION
53
- ) {
54
- throw new Error(
55
- `unsupported settings.json version (${this.path}): expected ${SETTINGS_VERSION}, got ${String(parsed.version)}`,
56
- )
57
- }
58
-
59
- const result = settingsSchema.safeParse(parsed)
60
-
61
- if (!result.success) {
62
- throw new Error(
63
- `invalid settings.json (${this.path}): ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ")}`,
64
- )
65
- }
66
-
67
- return result.data
68
- }
69
-
70
- private looksLikeLegacy(parsed: unknown): boolean {
71
- if (!parsed || typeof parsed !== "object") return false
72
-
73
- const obj = parsed as Record<string, unknown>
74
-
75
- if (Array.isArray(obj.channels)) {
76
- for (const channel of obj.channels) {
77
- if (!channel || typeof channel !== "object") continue
78
- const ch = channel as Record<string, unknown>
79
-
80
- if (Array.isArray(ch.connectors) && ch.connectors.some((x) => typeof x === "string")) {
81
- return true
82
- }
83
-
84
- if (!("id" in ch) && "name" in ch) return true
85
- }
86
- }
87
-
88
- if (Array.isArray(obj.connectors)) return true
89
- if (Array.isArray(obj.repositories)) return true
90
-
91
- if (Array.isArray(obj.profiles)) {
92
- for (const profile of obj.profiles) {
93
- if (!profile || typeof profile !== "object") continue
94
- const p = profile as Record<string, unknown>
95
-
96
- if ("repository" in p || "envFiles" in p || ("channel" in p && !("channelId" in p))) {
97
- return true
98
- }
99
- }
100
- }
101
-
102
- return false
103
- }
104
-
105
- write(settings: Settings): void {
106
- this.fs.mkdirSync(dirname(this.path), { recursive: true })
107
- const versioned: Settings = { ...settings, version: SETTINGS_VERSION }
108
- this.fs.writeFileSync(this.path, `${JSON.stringify(versioned, null, 2)}\n`)
109
- }
110
- }
@@ -1,15 +0,0 @@
1
- /**
2
- * Time boundary. Default NodeFunnelClock returns `new Date()`; MemoryFunnelClock
3
- * is settable and `advance(ms)`-able for deterministic schedule / timeout tests.
4
- */
5
- export abstract class FunnelClock {
6
- abstract now(): Date
7
-
8
- millis(): number {
9
- return this.now().getTime()
10
- }
11
-
12
- iso(): string {
13
- return this.now().toISOString()
14
- }
15
- }
@@ -1,26 +0,0 @@
1
- import { FunnelClock } from "@/engine/time/clock"
2
-
3
- type Props = {
4
- start?: Date
5
- }
6
-
7
- export class MemoryFunnelClock extends FunnelClock {
8
- private current: Date
9
-
10
- constructor(props: Props = {}) {
11
- super()
12
- this.current = props.start ?? new Date(0)
13
- }
14
-
15
- now(): Date {
16
- return new Date(this.current.getTime())
17
- }
18
-
19
- set(date: Date): void {
20
- this.current = date
21
- }
22
-
23
- advance(ms: number): void {
24
- this.current = new Date(this.current.getTime() + ms)
25
- }
26
- }
@@ -1,7 +0,0 @@
1
- import { FunnelClock } from "@/engine/time/clock"
2
-
3
- export class NodeFunnelClock extends FunnelClock {
4
- now(): Date {
5
- return new Date()
6
- }
7
- }
package/lib/funnel.ts DELETED
@@ -1,294 +0,0 @@
1
- import { join } from "node:path"
2
- import { FunnelConnectorFactory } from "@/connectors/connector-factory"
3
- import { FunnelChannels } from "@/engine/channels/channels"
4
- import { FunnelClaude } from "@/engine/claude/claude"
5
- import { FunnelFileSystem } from "@/engine/fs/file-system"
6
- import { MemoryFunnelFileSystem } from "@/engine/fs/memory-file-system"
7
- import { NodeFunnelFileSystem } from "@/engine/fs/node-file-system"
8
- import { FunnelIdGenerator } from "@/engine/id/id-generator"
9
- import { MemoryFunnelIdGenerator } from "@/engine/id/memory-id-generator"
10
- import { NodeFunnelIdGenerator } from "@/engine/id/node-id-generator"
11
- import { FunnelLogger } from "@/engine/logger/logger"
12
- import { MemoryFunnelLogger } from "@/engine/logger/memory-logger"
13
- import { NodeFunnelLogger } from "@/engine/logger/node-logger"
14
- import { FunnelMcp } from "@/engine/mcp/mcp"
15
- import { FunnelProcessRunner } from "@/engine/process/process-runner"
16
- import { MemoryFunnelProcessRunner } from "@/engine/process/memory-process-runner"
17
- import { NodeFunnelProcessRunner } from "@/engine/process/node-process-runner"
18
- import { FunnelProfiles } from "@/engine/profiles/profiles"
19
- import { MockFunnelSettingsReader } from "@/engine/settings/mock-settings-reader"
20
- import { FunnelSettingsReader } from "@/engine/settings/settings-reader"
21
- import { FUNNEL_DIR, FunnelSettingsStore } from "@/engine/settings/settings-store"
22
- import { FunnelClock } from "@/engine/time/clock"
23
- import { MemoryFunnelClock } from "@/engine/time/memory-clock"
24
- import { NodeFunnelClock } from "@/engine/time/node-clock"
25
- import { FunnelChannelPublisher } from "@/gateway/channel-publisher"
26
- import { FunnelGateway } from "@/gateway/gateway"
27
- import { FunnelGatewayServer } from "@/gateway/gateway-server"
28
- import { FunnelGatewayToken } from "@/gateway/gateway-token"
29
- import { FunnelListenersClient } from "@/gateway/listeners-client"
30
-
31
- const DEFAULT_TMP_DIR = "/tmp/funnel"
32
- const SANDBOX_DIR = "/sandbox/.funnel"
33
- const SANDBOX_TMP_DIR = "/sandbox/tmp"
34
-
35
- type Props = {
36
- /** Settings persistence (channels with nested connectors / profiles). Defaults to a FunnelSettingsStore rooted at `dir`. */
37
- store?: FunnelSettingsReader
38
- /** Filesystem boundary. Replace with MemoryFunnelFileSystem to sandbox all disk I/O. */
39
- fs?: FunnelFileSystem
40
- /** Process runner used by gateway / claude / gh listener. Replace with MemoryFunnelProcessRunner for tests. */
41
- process?: FunnelProcessRunner
42
- /** Logger flowed into every facet. Replace with MemoryFunnelLogger or NoopFunnelLogger to silence/inspect. */
43
- logger?: FunnelLogger
44
- /** Clock used by schedule listener, gh poll watermarks, and gateway timeouts. */
45
- clock?: FunnelClock
46
- /** ID generator for channel and connector ids. Use MemoryFunnelIdGenerator for deterministic tests. */
47
- idGenerator?: FunnelIdGenerator
48
- /** Funnel home directory (settings.json + per-channel/per-connector dirs). Defaults to ~/.funnel. */
49
- dir?: string
50
- /** Temp / runtime directory (gateway logs and PID adjacent files). Defaults to /tmp/funnel. */
51
- tmpDir?: string
52
- }
53
-
54
- /**
55
- * Facade exposing every funnel facet as a getter.
56
- *
57
- * The same `Funnel` is used by the CLI, the TUI, and as a programmable library.
58
- * All side-effecting boundaries (filesystem, process, logger, clock, id, paths) are
59
- * injectable via `Props` — passing memory implementations gives a fully sandboxed
60
- * Funnel that touches no real disk, processes, or wall-clock time.
61
- *
62
- * Connectors live nested inside their owning channel (channels[].connectors[]),
63
- * so connector CRUD is reached via `funnel.channels.addConnector(...)` etc.
64
- *
65
- * @example
66
- * ```ts
67
- * const funnel = new Funnel({})
68
- * const channel = funnel.channels.add({ name: "inbox" })
69
- * funnel.channels.addConnector("inbox", { type: "slack", name: "ops", botToken, appToken })
70
- * await funnel.gatewayServer({ port: 9742 }).start()
71
- * ```
72
- */
73
- export class Funnel {
74
- private readonly cache = new Map<string, unknown>()
75
-
76
- constructor(private readonly props: Props = {}) {
77
- Object.freeze(this)
78
- }
79
-
80
- /**
81
- * Sandboxed Funnel wired with in-memory implementations for every IO boundary.
82
- * Touches no real disk, processes, wall-clock time, or UUIDs — safe for tests
83
- * and ad-hoc experiments. Override individual fields by passing them in `props`.
84
- */
85
- static inMemory(props: Props = {}): Funnel {
86
- return new Funnel({
87
- store: props.store ?? new MockFunnelSettingsReader(),
88
- fs: props.fs ?? new MemoryFunnelFileSystem(),
89
- process: props.process ?? new MemoryFunnelProcessRunner(),
90
- logger: props.logger ?? new MemoryFunnelLogger(),
91
- clock: props.clock ?? new MemoryFunnelClock(),
92
- idGenerator: props.idGenerator ?? new MemoryFunnelIdGenerator(),
93
- dir: props.dir ?? SANDBOX_DIR,
94
- tmpDir: props.tmpDir ?? SANDBOX_TMP_DIR,
95
- })
96
- }
97
-
98
- private memo<T>(key: string, build: () => T): T {
99
- if (this.cache.has(key)) return this.cache.get(key) as T
100
-
101
- const value = build()
102
- this.cache.set(key, value)
103
-
104
- return value
105
- }
106
-
107
- /** Resolved on-disk paths the facade will read/write when methods are called. Pure compute, not memoized. */
108
- get paths(): { dir: string; tmpDir: string; settings: string } {
109
- const dir = this.props.dir ?? FUNNEL_DIR
110
- const tmpDir = this.props.tmpDir ?? DEFAULT_TMP_DIR
111
-
112
- return { dir, tmpDir, settings: join(dir, "settings.json") }
113
- }
114
-
115
- /** Filesystem boundary. Defaults to NodeFunnelFileSystem. */
116
- get fs(): FunnelFileSystem {
117
- return this.memo("fs", () => this.props.fs ?? new NodeFunnelFileSystem())
118
- }
119
-
120
- /** Process runner boundary. Defaults to NodeFunnelProcessRunner. */
121
- get process(): FunnelProcessRunner {
122
- return this.memo("process", () => this.props.process ?? new NodeFunnelProcessRunner())
123
- }
124
-
125
- /** Logger boundary. Defaults to NodeFunnelLogger. */
126
- get logger(): FunnelLogger {
127
- return this.memo("logger", () => this.props.logger ?? new NodeFunnelLogger())
128
- }
129
-
130
- /** Clock boundary. Defaults to NodeFunnelClock. */
131
- get clock(): FunnelClock {
132
- return this.memo("clock", () => this.props.clock ?? new NodeFunnelClock())
133
- }
134
-
135
- /** ID generator boundary. Defaults to NodeFunnelIdGenerator. */
136
- get idGenerator(): FunnelIdGenerator {
137
- return this.memo("idGenerator", () => this.props.idGenerator ?? new NodeFunnelIdGenerator())
138
- }
139
-
140
- /** Settings reader. If not injected, a FunnelSettingsStore rooted at `dir` is created. */
141
- get store(): FunnelSettingsReader {
142
- return this.memo(
143
- "store",
144
- () =>
145
- this.props.store ??
146
- new FunnelSettingsStore({
147
- path: this.paths.settings,
148
- fs: this.fs,
149
- }),
150
- )
151
- }
152
-
153
- /** Pure factory that constructs per-type listeners and adapters from connector configs. */
154
- get factory(): FunnelConnectorFactory {
155
- return this.memo(
156
- "factory",
157
- () =>
158
- new FunnelConnectorFactory({
159
- fs: this.fs,
160
- process: this.process,
161
- logger: this.logger,
162
- dir: this.paths.dir,
163
- }),
164
- )
165
- }
166
-
167
- /** Channel CRUD + nested connector CRUD + schedule entries + listener/adapter dispatch. */
168
- get channels(): FunnelChannels {
169
- return this.memo(
170
- "channels",
171
- () =>
172
- new FunnelChannels({
173
- store: this.store,
174
- factory: this.factory,
175
- profileChecker: this.profiles,
176
- clock: this.clock,
177
- idGenerator: this.idGenerator,
178
- }),
179
- )
180
- }
181
-
182
- /** Launch profiles (named presets for `fnl claude`: path + sub-agent + channel id). */
183
- get profiles(): FunnelProfiles {
184
- return this.memo("profiles", () => new FunnelProfiles({ store: this.store }))
185
- }
186
-
187
- /** funnel MCP installer (writes/removes `.mcp.json` entries in target repos). */
188
- get mcp(): FunnelMcp {
189
- return this.memo("mcp", () => new FunnelMcp({ fs: this.fs }))
190
- }
191
-
192
- /** Launch Claude Code with a channel injected via env, MCP installed, gateway ensured. */
193
- get claude(): FunnelClaude {
194
- return this.memo(
195
- "claude",
196
- () =>
197
- new FunnelClaude({
198
- channels: this.channels,
199
- mcp: this.mcp,
200
- gateway: this.gateway,
201
- fs: this.fs,
202
- process: this.process,
203
- logger: this.logger,
204
- dir: this.paths.dir,
205
- }),
206
- )
207
- }
208
-
209
- /** Gateway daemon controller (PID-file, start/stop the separate `bun daemon.ts` process). */
210
- get gateway(): FunnelGateway {
211
- return this.memo(
212
- "gateway",
213
- () =>
214
- new FunnelGateway({
215
- fs: this.fs,
216
- process: this.process,
217
- clock: this.clock,
218
- dir: this.paths.dir,
219
- tmpDir: this.paths.tmpDir,
220
- }),
221
- )
222
- }
223
-
224
- /** Read / generate the daemon's gateway token (mode 0600 file under `dir`). */
225
- get gatewayToken(): FunnelGatewayToken {
226
- return this.memo(
227
- "gatewayToken",
228
- () => new FunnelGatewayToken({ fs: this.fs, dir: this.paths.dir }),
229
- )
230
- }
231
-
232
- /**
233
- * HTTP client for `POST /channels/:channel/publish` on the running gateway
234
- * daemon. Use it to push arbitrary content into a channel from outside any
235
- * connector. Returns `{ state: "offline" }` if the daemon isn't up.
236
- */
237
- get publisher(): FunnelChannelPublisher {
238
- return this.memo("publisher", () => {
239
- const gateway = this.gateway
240
- const token = this.gatewayToken
241
-
242
- return new FunnelChannelPublisher({
243
- port: gateway.getPort(),
244
- isDaemonRunning: () => gateway.isRunning(),
245
- getToken: () => token.read(),
246
- })
247
- })
248
- }
249
-
250
- /**
251
- * HTTP client for listener operations on the running gateway daemon.
252
- * Returns `{ state: "offline" }` when the daemon is offline so hot-reload
253
- * paths stay write-only without parsing strings.
254
- */
255
- get listeners(): FunnelListenersClient {
256
- return this.memo("listeners", () => {
257
- const gateway = this.gateway
258
- const token = this.gatewayToken
259
-
260
- return new FunnelListenersClient({
261
- port: gateway.getPort(),
262
- isDaemonRunning: () => gateway.isRunning(),
263
- getToken: () => token.read(),
264
- })
265
- })
266
- }
267
-
268
- /**
269
- * In-process gateway server. Unlike `gateway.start()` (which spawns a daemon),
270
- * this returns a class that runs `Bun.serve` + listeners inside the current process —
271
- * useful for tests, embedding, or custom hosts.
272
- */
273
- gatewayServer(
274
- options: {
275
- port?: number
276
- logDir?: string
277
- killCompetingSlack?: boolean
278
- /** Override the auth token. Defaults to the persisted gateway.token. Pass "" to disable auth (tests). */
279
- token?: string
280
- } = {},
281
- ): FunnelGatewayServer {
282
- return new FunnelGatewayServer({
283
- channels: this.channels,
284
- settings: this.store,
285
- port: options.port,
286
- logDir: options.logDir,
287
- process: this.process,
288
- clock: this.clock,
289
- logger: this.logger,
290
- killCompetingSlack: options.killCompetingSlack,
291
- token: options.token ?? this.gatewayToken.ensure(),
292
- })
293
- }
294
- }