@interactive-inc/claude-funnel 0.10.0 → 0.10.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 (228) hide show
  1. package/dist/bin.js +448 -448
  2. package/dist/connectors/slack.d.ts +1 -29
  3. package/dist/gateway/daemon.js +166 -166
  4. package/dist/index.d.ts +4 -11
  5. package/dist/index.js +133 -120
  6. package/dist/slack-event-processor-CS-bAit9.d.ts +43 -0
  7. package/package.json +1 -6
  8. package/dist/slack-connector-schema-D7zAHN8k.d.ts +0 -15
  9. package/lib/bin.ts +0 -3
  10. package/lib/cli/factory.ts +0 -10
  11. package/lib/cli/index.ts +0 -85
  12. package/lib/cli/router/query-to-cli-args.ts +0 -20
  13. package/lib/cli/router/to-request.ts +0 -113
  14. package/lib/cli/router/validator.ts +0 -27
  15. package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +0 -27
  16. package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +0 -40
  17. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +0 -41
  18. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +0 -22
  19. package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +0 -23
  20. package/lib/cli/routes/channels.$channel.connectors.$connector.ts +0 -26
  21. package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +0 -92
  22. package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +0 -22
  23. package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +0 -63
  24. package/lib/cli/routes/channels.$channel.connectors.ts +0 -26
  25. package/lib/cli/routes/channels.$channel.publish.ts +0 -52
  26. package/lib/cli/routes/channels.$channel.rename.$newName.ts +0 -22
  27. package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +0 -34
  28. package/lib/cli/routes/channels.$channel.ts +0 -34
  29. package/lib/cli/routes/channels.add.$channel.ts +0 -33
  30. package/lib/cli/routes/channels.remove.$channel.ts +0 -20
  31. package/lib/cli/routes/channels.ts +0 -39
  32. package/lib/cli/routes/claude.ts +0 -70
  33. package/lib/cli/routes/gateway.listeners.ts +0 -41
  34. package/lib/cli/routes/gateway.logs.ts +0 -123
  35. package/lib/cli/routes/gateway.restart.ts +0 -50
  36. package/lib/cli/routes/gateway.run.ts +0 -41
  37. package/lib/cli/routes/gateway.start.ts +0 -50
  38. package/lib/cli/routes/gateway.status.ts +0 -19
  39. package/lib/cli/routes/gateway.stop.ts +0 -32
  40. package/lib/cli/routes/gateway.ts +0 -55
  41. package/lib/cli/routes/index.ts +0 -219
  42. package/lib/cli/routes/profiles.$profile.as-default.ts +0 -22
  43. package/lib/cli/routes/profiles.$profile.rename.$newName.ts +0 -22
  44. package/lib/cli/routes/profiles.$profile.run.ts +0 -36
  45. package/lib/cli/routes/profiles.add.$profile.ts +0 -49
  46. package/lib/cli/routes/profiles.remove.$profile.ts +0 -20
  47. package/lib/cli/routes/profiles.set.$profile.ts +0 -45
  48. package/lib/cli/routes/profiles.ts +0 -40
  49. package/lib/cli/routes/status.ts +0 -93
  50. package/lib/cli/routes/update.ts +0 -27
  51. package/lib/connectors/connector-adapter.ts +0 -9
  52. package/lib/connectors/connector-config-schema.ts +0 -16
  53. package/lib/connectors/connector-factory.ts +0 -94
  54. package/lib/connectors/connector-listener.ts +0 -20
  55. package/lib/connectors/discord-adapter.ts +0 -51
  56. package/lib/connectors/discord-connector-schema.ts +0 -12
  57. package/lib/connectors/discord-event-processor.ts +0 -48
  58. package/lib/connectors/discord-listener.ts +0 -111
  59. package/lib/connectors/discord.ts +0 -4
  60. package/lib/connectors/gh-adapter.ts +0 -48
  61. package/lib/connectors/gh-connector-schema.ts +0 -12
  62. package/lib/connectors/gh-listener.ts +0 -137
  63. package/lib/connectors/gh.ts +0 -3
  64. package/lib/connectors/match-cron.ts +0 -78
  65. package/lib/connectors/schedule-connector-schema.ts +0 -33
  66. package/lib/connectors/schedule-listener.ts +0 -207
  67. package/lib/connectors/schedule-state-store.ts +0 -54
  68. package/lib/connectors/schedule.ts +0 -4
  69. package/lib/connectors/slack-adapter.ts +0 -36
  70. package/lib/connectors/slack-connector-schema.ts +0 -13
  71. package/lib/connectors/slack-event-processor.ts +0 -97
  72. package/lib/connectors/slack-listener.ts +0 -97
  73. package/lib/connectors/slack.ts +0 -4
  74. package/lib/engine/channels/channels.ts +0 -520
  75. package/lib/engine/claude/claude.ts +0 -205
  76. package/lib/engine/claude/gateway-controller.ts +0 -4
  77. package/lib/engine/fs/file-system.ts +0 -23
  78. package/lib/engine/fs/memory-file-system.ts +0 -102
  79. package/lib/engine/fs/node-file-system.ts +0 -68
  80. package/lib/engine/http/http-client.ts +0 -17
  81. package/lib/engine/http/memory-http-client.ts +0 -36
  82. package/lib/engine/http/node-http-client.ts +0 -23
  83. package/lib/engine/id/id-generator.ts +0 -7
  84. package/lib/engine/id/memory-id-generator.ts +0 -20
  85. package/lib/engine/id/node-id-generator.ts +0 -7
  86. package/lib/engine/logger/logger.ts +0 -11
  87. package/lib/engine/logger/memory-logger.ts +0 -28
  88. package/lib/engine/logger/node-logger.ts +0 -49
  89. package/lib/engine/logger/noop-logger.ts +0 -9
  90. package/lib/engine/mcp/channel-server.ts +0 -123
  91. package/lib/engine/mcp/channel-subscriber.ts +0 -82
  92. package/lib/engine/mcp/mcp.ts +0 -126
  93. package/lib/engine/mcp/read-channel-connectors.ts +0 -34
  94. package/lib/engine/mcp/read-gateway-token.ts +0 -16
  95. package/lib/engine/mcp/usage-hint-for-type.ts +0 -15
  96. package/lib/engine/process/memory-process-runner.ts +0 -88
  97. package/lib/engine/process/node-process-runner.ts +0 -91
  98. package/lib/engine/process/process-runner.ts +0 -33
  99. package/lib/engine/profiles/profile-channel-checker.ts +0 -7
  100. package/lib/engine/profiles/profiles.ts +0 -126
  101. package/lib/engine/settings/mock-settings-reader.ts +0 -27
  102. package/lib/engine/settings/settings-reader.ts +0 -6
  103. package/lib/engine/settings/settings-schema.ts +0 -48
  104. package/lib/engine/settings/settings-store.ts +0 -110
  105. package/lib/engine/time/clock.ts +0 -15
  106. package/lib/engine/time/memory-clock.ts +0 -26
  107. package/lib/engine/time/node-clock.ts +0 -7
  108. package/lib/funnel.ts +0 -294
  109. package/lib/gateway/auth-middleware.ts +0 -44
  110. package/lib/gateway/broadcaster.ts +0 -319
  111. package/lib/gateway/channel-publisher.ts +0 -67
  112. package/lib/gateway/daemon.ts +0 -47
  113. package/lib/gateway/factory.ts +0 -10
  114. package/lib/gateway/funnel-event-store.ts +0 -155
  115. package/lib/gateway/gateway-server.ts +0 -426
  116. package/lib/gateway/gateway-token.ts +0 -79
  117. package/lib/gateway/gateway.ts +0 -209
  118. package/lib/gateway/kill-competing-slack-gateways.ts +0 -56
  119. package/lib/gateway/listener-supervisor.ts +0 -339
  120. package/lib/gateway/listeners-client.ts +0 -128
  121. package/lib/gateway/publish-schema.ts +0 -27
  122. package/lib/gateway/resolve-daemon-script.ts +0 -26
  123. package/lib/gateway/routes/channels.connectors.call.ts +0 -39
  124. package/lib/gateway/routes/channels.publish.ts +0 -44
  125. package/lib/gateway/routes/health.ts +0 -13
  126. package/lib/gateway/routes/index.ts +0 -26
  127. package/lib/gateway/routes/listeners.list.ts +0 -6
  128. package/lib/gateway/routes/listeners.restart.ts +0 -15
  129. package/lib/gateway/routes/listeners.start.ts +0 -15
  130. package/lib/gateway/routes/listeners.stop.ts +0 -15
  131. package/lib/gateway/routes/route-deps.ts +0 -19
  132. package/lib/gateway/routes/status.ts +0 -15
  133. package/lib/gateway/routes/validator.ts +0 -17
  134. package/lib/index.ts +0 -67
  135. package/lib/logger/leuco-human-file-writer.ts +0 -65
  136. package/lib/logger/leuco-human-logger.ts +0 -98
  137. package/lib/logger/leuco-human-record.ts +0 -16
  138. package/lib/logger/leuco-human-stdout-writer.ts +0 -26
  139. package/lib/logger/leuco-human-writer.ts +0 -14
  140. package/lib/logger/leuco-logger-memory-sink.ts +0 -67
  141. package/lib/logger/leuco-logger-record.ts +0 -13
  142. package/lib/logger/leuco-logger-sink.ts +0 -33
  143. package/lib/logger/leuco-logger-sqlite-sink.ts +0 -355
  144. package/lib/logger/leuco-logger.ts +0 -135
  145. package/lib/tui/app.tsx +0 -357
  146. package/lib/tui/components/add-row.tsx +0 -18
  147. package/lib/tui/components/brand.tsx +0 -27
  148. package/lib/tui/components/card.tsx +0 -44
  149. package/lib/tui/components/detail-bar.tsx +0 -46
  150. package/lib/tui/components/editable-field.tsx +0 -33
  151. package/lib/tui/components/empty-state.tsx +0 -11
  152. package/lib/tui/components/gateway-status.tsx +0 -66
  153. package/lib/tui/components/keymap.tsx +0 -29
  154. package/lib/tui/components/menu-item.tsx +0 -73
  155. package/lib/tui/components/menu.tsx +0 -26
  156. package/lib/tui/components/panel-header.tsx +0 -22
  157. package/lib/tui/components/readonly-field.tsx +0 -18
  158. package/lib/tui/components/section-header.tsx +0 -25
  159. package/lib/tui/components/selection-accent.tsx +0 -32
  160. package/lib/tui/components/session-item.tsx +0 -33
  161. package/lib/tui/components/session-list.tsx +0 -33
  162. package/lib/tui/components/ui/hascii/accordion-item.tsx +0 -88
  163. package/lib/tui/components/ui/hascii/accordion.tsx +0 -96
  164. package/lib/tui/components/ui/hascii/alert-dialog.tsx +0 -43
  165. package/lib/tui/components/ui/hascii/badge.tsx +0 -51
  166. package/lib/tui/components/ui/hascii/breadcrumb.tsx +0 -58
  167. package/lib/tui/components/ui/hascii/button.tsx +0 -194
  168. package/lib/tui/components/ui/hascii/card-content.tsx +0 -14
  169. package/lib/tui/components/ui/hascii/card-description.tsx +0 -13
  170. package/lib/tui/components/ui/hascii/card-footer.tsx +0 -14
  171. package/lib/tui/components/ui/hascii/card-header.tsx +0 -14
  172. package/lib/tui/components/ui/hascii/card-title.tsx +0 -13
  173. package/lib/tui/components/ui/hascii/card.tsx +0 -27
  174. package/lib/tui/components/ui/hascii/checkbox.tsx +0 -65
  175. package/lib/tui/components/ui/hascii/command.tsx +0 -159
  176. package/lib/tui/components/ui/hascii/dialog-content.tsx +0 -14
  177. package/lib/tui/components/ui/hascii/dialog-description.tsx +0 -13
  178. package/lib/tui/components/ui/hascii/dialog-footer.tsx +0 -14
  179. package/lib/tui/components/ui/hascii/dialog-header.tsx +0 -14
  180. package/lib/tui/components/ui/hascii/dialog-title.tsx +0 -13
  181. package/lib/tui/components/ui/hascii/dialog.tsx +0 -27
  182. package/lib/tui/components/ui/hascii/file-tree.tsx +0 -142
  183. package/lib/tui/components/ui/hascii/focus-group.tsx +0 -62
  184. package/lib/tui/components/ui/hascii/form-item.tsx +0 -43
  185. package/lib/tui/components/ui/hascii/input-otp.tsx +0 -86
  186. package/lib/tui/components/ui/hascii/input.tsx +0 -130
  187. package/lib/tui/components/ui/hascii/pagination.tsx +0 -105
  188. package/lib/tui/components/ui/hascii/progress.tsx +0 -28
  189. package/lib/tui/components/ui/hascii/select.tsx +0 -131
  190. package/lib/tui/components/ui/hascii/separator.tsx +0 -35
  191. package/lib/tui/components/ui/hascii/sidebar-content.tsx +0 -23
  192. package/lib/tui/components/ui/hascii/sidebar-header.tsx +0 -14
  193. package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +0 -67
  194. package/lib/tui/components/ui/hascii/sidebar.tsx +0 -24
  195. package/lib/tui/components/ui/hascii/skeleton.tsx +0 -60
  196. package/lib/tui/components/ui/hascii/slider.tsx +0 -91
  197. package/lib/tui/components/ui/hascii/snackbar.tsx +0 -75
  198. package/lib/tui/components/ui/hascii/sparkline.tsx +0 -53
  199. package/lib/tui/components/ui/hascii/spinner.tsx +0 -47
  200. package/lib/tui/components/ui/hascii/stepper.tsx +0 -54
  201. package/lib/tui/components/ui/hascii/switch.tsx +0 -66
  202. package/lib/tui/components/ui/hascii/table.tsx +0 -95
  203. package/lib/tui/components/ui/hascii/tabs.tsx +0 -59
  204. package/lib/tui/components/ui/hascii/toggle-group-item.tsx +0 -45
  205. package/lib/tui/components/ui/hascii/toggle-group.tsx +0 -99
  206. package/lib/tui/components/ui/hascii/tree.tsx +0 -104
  207. package/lib/tui/components/view-shell.tsx +0 -44
  208. package/lib/tui/filter-input.tsx +0 -33
  209. package/lib/tui/hooks/hascii/use-pressable.ts +0 -54
  210. package/lib/tui/parse-comma-list.ts +0 -14
  211. package/lib/tui/profile-launcher.tsx +0 -61
  212. package/lib/tui/scrollbar-options.ts +0 -19
  213. package/lib/tui/sidebar.tsx +0 -50
  214. package/lib/tui/theme.ts +0 -40
  215. package/lib/tui/tui.tsx +0 -20
  216. package/lib/tui/types.ts +0 -38
  217. package/lib/tui/unique-name.ts +0 -18
  218. package/lib/tui/use-event-stream.ts +0 -133
  219. package/lib/tui/use-snapshot.ts +0 -99
  220. package/lib/tui/utils/hascii/form-item-context.tsx +0 -23
  221. package/lib/tui/utils/hascii/input-focus-context.tsx +0 -31
  222. package/lib/tui/utils/hascii/theme-context.tsx +0 -26
  223. package/lib/tui/utils/hascii/theme.ts +0 -176
  224. package/lib/tui/views/channels-view.tsx +0 -108
  225. package/lib/tui/views/connectors-view.tsx +0 -164
  226. package/lib/tui/views/events-view.tsx +0 -160
  227. package/lib/tui/views/listeners-view.tsx +0 -80
  228. package/lib/tui/views/profiles-view.tsx +0 -152
@@ -1,205 +0,0 @@
1
- import { join } from "node:path"
2
- import type { FunnelChannels } from "@/engine/channels/channels"
3
- import type { GatewayController } from "@/engine/claude/gateway-controller"
4
- import { FunnelFileSystem } from "@/engine/fs/file-system"
5
- import { NodeFunnelFileSystem } from "@/engine/fs/node-file-system"
6
- import { FunnelLogger } from "@/engine/logger/logger"
7
- import { NodeFunnelLogger } from "@/engine/logger/node-logger"
8
- import type { FunnelMcp } from "@/engine/mcp/mcp"
9
- import { FunnelProcessRunner } from "@/engine/process/process-runner"
10
- import { NodeFunnelProcessRunner } from "@/engine/process/node-process-runner"
11
- import { FUNNEL_DIR } from "@/engine/settings/settings-store"
12
-
13
- export type LaunchOptions = {
14
- channel: string
15
- cwd?: string
16
- subAgent?: string
17
- userArgs?: string[]
18
- profileName?: string
19
- /** Forward `--brief` to claude on launch (enables the SendUserMessage tool). */
20
- brief?: boolean
21
- }
22
-
23
- type Deps = {
24
- channels: FunnelChannels
25
- mcp: FunnelMcp
26
- gateway: GatewayController
27
- process?: FunnelProcessRunner
28
- fs?: FunnelFileSystem
29
- logger?: FunnelLogger
30
- dir?: string
31
- }
32
-
33
- const defaultProcess = new NodeFunnelProcessRunner()
34
- const defaultFs = new NodeFunnelFileSystem()
35
- const defaultLogger = new NodeFunnelLogger()
36
-
37
- /**
38
- * Launches Claude Code with funnel pre-wired: ensures the gateway is running,
39
- * installs the funnel MCP into the target repo's `.mcp.json` if missing,
40
- * injects `FUNNEL_CHANNEL_ID` into the child env, and writes a per-profile
41
- * PID file to enforce singleton launches.
42
- */
43
- export class FunnelClaude {
44
- private readonly channels: FunnelChannels
45
- private readonly mcp: FunnelMcp
46
- private readonly gateway: GatewayController
47
- private readonly process: FunnelProcessRunner
48
- private readonly fs: FunnelFileSystem
49
- private readonly logger: FunnelLogger
50
- private readonly pidDir: string
51
-
52
- constructor(deps: Deps) {
53
- this.channels = deps.channels
54
- this.mcp = deps.mcp
55
- this.gateway = deps.gateway
56
- this.process = deps.process ?? defaultProcess
57
- this.fs = deps.fs ?? defaultFs
58
- this.logger = deps.logger ?? defaultLogger
59
- this.pidDir = join(deps.dir ?? FUNNEL_DIR, "claude")
60
- Object.freeze(this)
61
- }
62
-
63
- async launch(options: LaunchOptions): Promise<number> {
64
- const channel = this.channels.get(options.channel) ?? this.channels.getById(options.channel)
65
-
66
- if (!channel) {
67
- throw new Error(`channel "${options.channel}" not found`)
68
- }
69
-
70
- if (options.profileName && this.isRunning(options.profileName)) {
71
- throw new Error(`profile "${options.profileName}" is already running`)
72
- }
73
-
74
- const cwd = options.cwd ?? globalThis.process.cwd()
75
-
76
- if (!this.mcp.findInstalledName(cwd)) {
77
- this.mcp.install(cwd)
78
-
79
- this.logger.info(`added funnel MCP to .mcp.json`, { cwd })
80
- }
81
-
82
- if (!this.gateway.isRunning()) {
83
- this.logger.info(`starting gateway automatically`)
84
- await this.gateway.start()
85
- }
86
-
87
- if (options.profileName) {
88
- this.writePidFile(options.profileName)
89
- this.installCleanup(options.profileName)
90
- }
91
-
92
- const claudeArgs = this.buildArgs(options, cwd)
93
- const env = this.buildEnv(channel.id)
94
-
95
- this.logger.info(`claude launch`, {
96
- channel: options.channel,
97
- channelId: channel.id,
98
- subAgent: options.subAgent,
99
- cwd,
100
- })
101
-
102
- try {
103
- return await this.process.attach(["claude", ...claudeArgs], { cwd, env })
104
- } finally {
105
- if (options.profileName) this.removePidFile(options.profileName)
106
- }
107
- }
108
-
109
- isRunning(profileName: string): boolean {
110
- const pid = this.readPid(profileName)
111
-
112
- if (!pid) return false
113
-
114
- return this.isProcessAlive(pid)
115
- }
116
-
117
- private pidPath(profileName: string): string {
118
- return join(this.pidDir, `${profileName}.pid`)
119
- }
120
-
121
- private readPid(profileName: string): number | null {
122
- const path = this.pidPath(profileName)
123
-
124
- if (!this.fs.existsSync(path)) return null
125
-
126
- try {
127
- const content = this.fs.readFileSync(path).trim()
128
- const pid = Number(content)
129
-
130
- if (!pid || pid <= 0) return null
131
-
132
- return pid
133
- } catch {
134
- return null
135
- }
136
- }
137
-
138
- private writePidFile(profileName: string): void {
139
- this.fs.mkdirSync(this.pidDir, { recursive: true })
140
- this.fs.writeFileSync(this.pidPath(profileName), String(globalThis.process.pid))
141
- }
142
-
143
- private removePidFile(profileName: string): void {
144
- const path = this.pidPath(profileName)
145
-
146
- if (this.fs.existsSync(path)) this.fs.unlink(path)
147
- }
148
-
149
- private installCleanup(profileName: string): void {
150
- // Default Bun behavior on SIGINT/SIGTERM is process.exit(130/143), which
151
- // fires the "exit" event. Hooking only "exit" keeps the PID file cleanup
152
- // running while letting the signal terminate the process normally —
153
- // adding our own SIGINT handler would suppress the default exit and leave
154
- // funnel hanging until claude responds.
155
- globalThis.process.once("exit", () => this.removePidFile(profileName))
156
- }
157
-
158
- private isProcessAlive(pid: number): boolean {
159
- const result = this.process.runSync(["ps", "-p", String(pid), "-o", "state="])
160
-
161
- if (result.exitCode !== 0) return false
162
-
163
- const state = result.stdout.trim()
164
-
165
- if (!state) return false
166
-
167
- return !state.startsWith("Z")
168
- }
169
-
170
- private buildArgs(options: LaunchOptions, cwd: string): string[] {
171
- const result = [...(options.userArgs ?? [])]
172
-
173
- const mcpName = this.mcp.findInstalledName(cwd)
174
-
175
- if (
176
- mcpName &&
177
- !result.includes("--dangerously-load-development-channels") &&
178
- !result.includes("--channels")
179
- ) {
180
- result.push("--dangerously-load-development-channels", `server:${mcpName}`)
181
- }
182
-
183
- if (!result.includes("--agent") && options.subAgent) {
184
- result.push("--agent", options.subAgent)
185
- }
186
-
187
- if (options.brief && !result.includes("--brief")) {
188
- result.push("--brief")
189
- }
190
-
191
- return result
192
- }
193
-
194
- private buildEnv(channelId: string): Record<string, string> {
195
- const env: Record<string, string> = {}
196
-
197
- for (const [key, value] of Object.entries(globalThis.process.env)) {
198
- if (typeof value === "string") env[key] = value
199
- }
200
-
201
- env.FUNNEL_CHANNEL_ID = channelId
202
-
203
- return env
204
- }
205
- }
@@ -1,4 +0,0 @@
1
- export type GatewayController = {
2
- isRunning(): boolean
3
- start(options?: { caffeinate?: boolean }): Promise<boolean>
4
- }
@@ -1,23 +0,0 @@
1
- export type FileStat = {
2
- mtimeMs: number
3
- /** POSIX mode bits (e.g. 0o600). `null` when the underlying FS does not expose mode. */
4
- mode: number | null
5
- }
6
-
7
- /**
8
- * Filesystem boundary used everywhere funnel reads or writes.
9
- * Default is NodeFunnelFileSystem (real `node:fs`); MemoryFunnelFileSystem
10
- * provides a sandbox for tests and embedded use.
11
- */
12
- export abstract class FunnelFileSystem {
13
- abstract existsSync(path: string): boolean
14
- abstract readFileSync(path: string): string
15
- abstract writeFileSync(path: string, data: string): void
16
- /** Write `data` and ensure the resulting file is owner-only (0600). Use for tokens and any file that may contain secrets. */
17
- abstract writeSecretFileSync(path: string, data: string): void
18
- abstract appendFileSync(path: string, data: string): void
19
- abstract unlink(path: string): void
20
- abstract mkdirSync(path: string, options?: { recursive?: boolean }): void
21
- abstract readdirSync(path: string): string[]
22
- abstract statSync(path: string): FileStat
23
- }
@@ -1,102 +0,0 @@
1
- import { type FileStat, FunnelFileSystem } from "@/engine/fs/file-system"
2
-
3
- type Props = {
4
- dirs?: string[]
5
- files?: Record<string, string>
6
- mtimes?: Record<string, number>
7
- modes?: Record<string, number>
8
- now?: () => number
9
- }
10
-
11
- const SECRET_MODE = 0o600
12
-
13
- export class MemoryFunnelFileSystem extends FunnelFileSystem {
14
- private readonly dirs: Set<string>
15
- private readonly files: Map<string, string>
16
- private readonly mtimes: Map<string, number>
17
- private readonly modes: Map<string, number>
18
- private readonly now: () => number
19
-
20
- constructor(props: Props = {}) {
21
- super()
22
- this.dirs = new Set(props.dirs ?? [])
23
- this.files = new Map(Object.entries(props.files ?? {}))
24
- this.mtimes = new Map(Object.entries(props.mtimes ?? {}))
25
- this.modes = new Map(Object.entries(props.modes ?? {}))
26
- this.now = props.now ?? (() => Date.now())
27
- }
28
-
29
- existsSync(path: string): boolean {
30
- return this.dirs.has(path) || this.files.has(path)
31
- }
32
-
33
- readFileSync(path: string): string {
34
- return this.files.get(path) ?? ""
35
- }
36
-
37
- writeFileSync(path: string, data: string): void {
38
- this.files.set(path, data)
39
- this.touch(path)
40
- }
41
-
42
- writeSecretFileSync(path: string, data: string): void {
43
- this.files.set(path, data)
44
- this.modes.set(path, SECRET_MODE)
45
- this.touch(path)
46
- }
47
-
48
- appendFileSync(path: string, data: string): void {
49
- const prev = this.files.get(path) ?? ""
50
- this.files.set(path, prev + data)
51
- this.touch(path)
52
- }
53
-
54
- unlink(path: string): void {
55
- this.files.delete(path)
56
- this.mtimes.delete(path)
57
- this.modes.delete(path)
58
- }
59
-
60
- mkdirSync(path: string, options?: { recursive?: boolean }): void {
61
- void options
62
- this.dirs.add(path)
63
- }
64
-
65
- readdirSync(path: string): string[] {
66
- const prefix = path.endsWith("/") ? path : `${path}/`
67
- const names: string[] = []
68
-
69
- for (const file of this.files.keys()) {
70
- if (!file.startsWith(prefix)) continue
71
-
72
- const rest = file.slice(prefix.length)
73
-
74
- if (!rest.includes("/")) names.push(rest)
75
- }
76
-
77
- return names
78
- }
79
-
80
- statSync(path: string): FileStat {
81
- const mtimeMs = this.mtimes.get(path)
82
-
83
- if (mtimeMs === undefined) {
84
- throw new Error(`not found: ${path}`)
85
- }
86
-
87
- return { mtimeMs, mode: this.modes.get(path) ?? null }
88
- }
89
-
90
- setMtime(path: string, mtimeMs: number): void {
91
- this.mtimes.set(path, mtimeMs)
92
- }
93
-
94
- setMode(path: string, mode: number): void {
95
- this.modes.set(path, mode)
96
- }
97
-
98
- private touch(path: string): void {
99
- if (!this.mtimes.has(path)) this.mtimes.set(path, this.now())
100
- else this.mtimes.set(path, this.now())
101
- }
102
- }
@@ -1,68 +0,0 @@
1
- import {
2
- appendFileSync,
3
- chmodSync,
4
- existsSync,
5
- mkdirSync,
6
- readdirSync,
7
- readFileSync,
8
- statSync,
9
- unlinkSync,
10
- writeFileSync,
11
- } from "node:fs"
12
- import { type FileStat, FunnelFileSystem } from "@/engine/fs/file-system"
13
-
14
- const SECRET_MODE = 0o600
15
-
16
- export class NodeFunnelFileSystem extends FunnelFileSystem {
17
- constructor() {
18
- super()
19
- Object.freeze(this)
20
- }
21
-
22
- existsSync(path: string): boolean {
23
- return existsSync(path)
24
- }
25
-
26
- readFileSync(path: string): string {
27
- return readFileSync(path, "utf-8")
28
- }
29
-
30
- writeFileSync(path: string, data: string): void {
31
- writeFileSync(path, data)
32
- }
33
-
34
- writeSecretFileSync(path: string, data: string): void {
35
- writeFileSync(path, data, { mode: SECRET_MODE })
36
- try {
37
- chmodSync(path, SECRET_MODE)
38
- } catch {
39
- // ignore — best-effort tightening for files that already existed with looser perms
40
- }
41
- }
42
-
43
- appendFileSync(path: string, data: string): void {
44
- appendFileSync(path, data)
45
- }
46
-
47
- unlink(path: string): void {
48
- try {
49
- unlinkSync(path)
50
- } catch {
51
- // ignore
52
- }
53
- }
54
-
55
- mkdirSync(path: string, options?: { recursive?: boolean }): void {
56
- mkdirSync(path, { recursive: options?.recursive ?? false })
57
- }
58
-
59
- readdirSync(path: string): string[] {
60
- return readdirSync(path)
61
- }
62
-
63
- statSync(path: string): FileStat {
64
- const stat = statSync(path)
65
-
66
- return { mtimeMs: stat.mtimeMs, mode: stat.mode & 0o777 }
67
- }
68
- }
@@ -1,17 +0,0 @@
1
- export type HttpRequest = {
2
- method: string
3
- url: string
4
- headers?: Record<string, string>
5
- body?: string
6
- }
7
-
8
- export type HttpResponse = {
9
- status: number
10
- ok: boolean
11
- text(): Promise<string>
12
- json(): Promise<unknown>
13
- }
14
-
15
- export abstract class FunnelHttpClient {
16
- abstract fetch(request: HttpRequest): Promise<HttpResponse>
17
- }
@@ -1,36 +0,0 @@
1
- import { FunnelHttpClient, type HttpRequest, type HttpResponse } from "@/engine/http/http-client"
2
-
3
- export type MemoryHttpResponse = {
4
- status?: number
5
- body?: string
6
- }
7
-
8
- export type MemoryHttpHandler = (
9
- request: HttpRequest,
10
- ) => MemoryHttpResponse | Promise<MemoryHttpResponse>
11
-
12
- export class MemoryFunnelHttpClient extends FunnelHttpClient {
13
- readonly calls: HttpRequest[] = []
14
- private handler: MemoryHttpHandler = () => ({ status: 200, body: "" })
15
-
16
- on(handler: MemoryHttpHandler): this {
17
- this.handler = handler
18
-
19
- return this
20
- }
21
-
22
- async fetch(request: HttpRequest): Promise<HttpResponse> {
23
- this.calls.push(request)
24
-
25
- const response = await this.handler(request)
26
- const status = response.status ?? 200
27
- const body = response.body ?? ""
28
-
29
- return {
30
- status,
31
- ok: status >= 200 && status < 300,
32
- text: async () => body,
33
- json: async () => JSON.parse(body),
34
- }
35
- }
36
- }
@@ -1,23 +0,0 @@
1
- import { FunnelHttpClient, type HttpRequest, type HttpResponse } from "@/engine/http/http-client"
2
-
3
- export class NodeFunnelHttpClient extends FunnelHttpClient {
4
- constructor() {
5
- super()
6
- Object.freeze(this)
7
- }
8
-
9
- async fetch(request: HttpRequest): Promise<HttpResponse> {
10
- const res = await globalThis.fetch(request.url, {
11
- method: request.method,
12
- headers: request.headers,
13
- body: request.body,
14
- })
15
-
16
- return {
17
- status: res.status,
18
- ok: res.ok,
19
- text: () => res.text(),
20
- json: () => res.json(),
21
- }
22
- }
23
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * ID generator boundary. Default NodeFunnelIdGenerator wraps `crypto.randomUUID()`;
3
- * MemoryFunnelIdGenerator emits `<prefix>-1, <prefix>-2, ...` for deterministic tests.
4
- */
5
- export abstract class FunnelIdGenerator {
6
- abstract generate(): string
7
- }
@@ -1,20 +0,0 @@
1
- import { FunnelIdGenerator } from "@/engine/id/id-generator"
2
-
3
- type Props = {
4
- prefix?: string
5
- }
6
-
7
- export class MemoryFunnelIdGenerator extends FunnelIdGenerator {
8
- private counter = 0
9
- private readonly prefix: string
10
-
11
- constructor(props: Props = {}) {
12
- super()
13
- this.prefix = props.prefix ?? "id"
14
- }
15
-
16
- generate(): string {
17
- this.counter++
18
- return `${this.prefix}-${this.counter}`
19
- }
20
- }
@@ -1,7 +0,0 @@
1
- import { FunnelIdGenerator } from "@/engine/id/id-generator"
2
-
3
- export class NodeFunnelIdGenerator extends FunnelIdGenerator {
4
- generate(): string {
5
- return crypto.randomUUID()
6
- }
7
- }
@@ -1,11 +0,0 @@
1
- /**
2
- * Structured logger with three levels and an optional log-file path.
3
- * Defaults to NodeFunnelLogger (appends to /tmp/funnel/funnel.log);
4
- * MemoryFunnelLogger captures entries in memory and NoopFunnelLogger silences output.
5
- */
6
- export abstract class FunnelLogger {
7
- abstract info(message: string, meta?: Record<string, unknown>): void
8
- abstract warn(message: string, meta?: Record<string, unknown>): void
9
- abstract error(message: string, meta?: Record<string, unknown>): void
10
- abstract readonly file: string | null
11
- }
@@ -1,28 +0,0 @@
1
- import { FunnelLogger } from "@/engine/logger/logger"
2
-
3
- export type LogEntry = {
4
- level: "info" | "warn" | "error"
5
- message: string
6
- meta?: Record<string, unknown>
7
- }
8
-
9
- export class MemoryFunnelLogger extends FunnelLogger {
10
- readonly file = null
11
- readonly entries: LogEntry[] = []
12
-
13
- info(message: string, meta?: Record<string, unknown>): void {
14
- this.entries.push({ level: "info", message, meta })
15
- }
16
-
17
- warn(message: string, meta?: Record<string, unknown>): void {
18
- this.entries.push({ level: "warn", message, meta })
19
- }
20
-
21
- error(message: string, meta?: Record<string, unknown>): void {
22
- this.entries.push({ level: "error", message, meta })
23
- }
24
-
25
- clear(): void {
26
- this.entries.length = 0
27
- }
28
- }
@@ -1,49 +0,0 @@
1
- import { appendFileSync, mkdirSync } from "node:fs"
2
- import { dirname, join } from "node:path"
3
- import { FunnelLogger } from "@/engine/logger/logger"
4
-
5
- const DEFAULT_LOG_FILE = join("/tmp/funnel", "funnel.log")
6
-
7
- type Level = "info" | "warn" | "error"
8
-
9
- type Props = {
10
- file?: string
11
- now?: () => Date
12
- }
13
-
14
- export class NodeFunnelLogger extends FunnelLogger {
15
- readonly file: string
16
- private readonly now: () => Date
17
-
18
- constructor(props: Props = {}) {
19
- super()
20
- this.file = props.file ?? DEFAULT_LOG_FILE
21
- this.now = props.now ?? (() => new Date())
22
- Object.freeze(this)
23
- }
24
-
25
- info(message: string, meta?: Record<string, unknown>): void {
26
- this.write("info", message, meta)
27
- }
28
-
29
- warn(message: string, meta?: Record<string, unknown>): void {
30
- this.write("warn", message, meta)
31
- }
32
-
33
- error(message: string, meta?: Record<string, unknown>): void {
34
- this.write("error", message, meta)
35
- }
36
-
37
- private write(level: Level, message: string, meta?: Record<string, unknown>): void {
38
- mkdirSync(dirname(this.file), { recursive: true })
39
-
40
- const entry = {
41
- time: this.now().toISOString(),
42
- level,
43
- message,
44
- ...(meta ? { meta } : {}),
45
- }
46
-
47
- appendFileSync(this.file, `${JSON.stringify(entry)}\n`)
48
- }
49
- }
@@ -1,9 +0,0 @@
1
- import { FunnelLogger } from "@/engine/logger/logger"
2
-
3
- export class NoopFunnelLogger extends FunnelLogger {
4
- readonly file = null
5
-
6
- info(): void {}
7
- warn(): void {}
8
- error(): void {}
9
- }