@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.
- package/README.md +106 -56
- package/dist/bin.js +557 -530
- package/dist/connectors/schedule.d.ts +2 -49
- package/dist/connectors/schedule.js +1 -1
- package/dist/connectors/slack.d.ts +4 -48
- package/dist/connectors/slack.js +1 -1
- package/dist/gateway/daemon.js +213 -211
- package/dist/index.d.ts +465 -173
- package/dist/index.js +692 -154
- package/dist/{schedule-connector-schema-CkuIQ0JQ.js → schedule-connector-schema-FxP7LPlx.js} +11 -0
- package/dist/{file-system-Co60LrmR.d.ts → schedule-listener-BPodvbld.d.ts} +56 -1
- package/dist/{slack-connector-schema-Cd22WiHB.js → slack-connector-schema-B4hsf3AY.js} +10 -1
- package/dist/slack-listener-CHj6uMY-.d.ts +74 -0
- package/package.json +2 -6
- package/schemas/funnel.schema.json +144 -0
- package/dist/slack-connector-schema-D7zAHN8k.d.ts +0 -15
- package/lib/bin.ts +0 -3
- package/lib/cli/factory.ts +0 -10
- package/lib/cli/index.ts +0 -85
- package/lib/cli/router/query-to-cli-args.ts +0 -20
- package/lib/cli/router/to-request.ts +0 -113
- package/lib/cli/router/validator.ts +0 -27
- package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +0 -27
- package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +0 -40
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +0 -41
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +0 -22
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +0 -23
- package/lib/cli/routes/channels.$channel.connectors.$connector.ts +0 -26
- package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +0 -92
- package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +0 -22
- package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +0 -63
- package/lib/cli/routes/channels.$channel.connectors.ts +0 -26
- package/lib/cli/routes/channels.$channel.publish.ts +0 -52
- package/lib/cli/routes/channels.$channel.rename.$newName.ts +0 -22
- package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +0 -34
- package/lib/cli/routes/channels.$channel.ts +0 -34
- package/lib/cli/routes/channels.add.$channel.ts +0 -33
- package/lib/cli/routes/channels.remove.$channel.ts +0 -20
- package/lib/cli/routes/channels.ts +0 -39
- package/lib/cli/routes/claude.ts +0 -70
- package/lib/cli/routes/gateway.listeners.ts +0 -41
- package/lib/cli/routes/gateway.logs.ts +0 -123
- package/lib/cli/routes/gateway.restart.ts +0 -50
- package/lib/cli/routes/gateway.run.ts +0 -41
- package/lib/cli/routes/gateway.start.ts +0 -50
- package/lib/cli/routes/gateway.status.ts +0 -19
- package/lib/cli/routes/gateway.stop.ts +0 -32
- package/lib/cli/routes/gateway.ts +0 -55
- package/lib/cli/routes/index.ts +0 -219
- package/lib/cli/routes/profiles.$profile.as-default.ts +0 -22
- package/lib/cli/routes/profiles.$profile.rename.$newName.ts +0 -22
- package/lib/cli/routes/profiles.$profile.run.ts +0 -36
- package/lib/cli/routes/profiles.add.$profile.ts +0 -49
- package/lib/cli/routes/profiles.remove.$profile.ts +0 -20
- package/lib/cli/routes/profiles.set.$profile.ts +0 -45
- package/lib/cli/routes/profiles.ts +0 -40
- package/lib/cli/routes/status.ts +0 -93
- package/lib/cli/routes/update.ts +0 -27
- package/lib/connectors/connector-adapter.ts +0 -9
- package/lib/connectors/connector-config-schema.ts +0 -16
- package/lib/connectors/connector-factory.ts +0 -94
- package/lib/connectors/connector-listener.ts +0 -20
- package/lib/connectors/discord-adapter.ts +0 -51
- package/lib/connectors/discord-connector-schema.ts +0 -12
- package/lib/connectors/discord-event-processor.ts +0 -48
- package/lib/connectors/discord-listener.ts +0 -111
- package/lib/connectors/discord.ts +0 -4
- package/lib/connectors/gh-adapter.ts +0 -48
- package/lib/connectors/gh-connector-schema.ts +0 -12
- package/lib/connectors/gh-listener.ts +0 -137
- package/lib/connectors/gh.ts +0 -3
- package/lib/connectors/match-cron.ts +0 -78
- package/lib/connectors/schedule-connector-schema.ts +0 -33
- package/lib/connectors/schedule-listener.ts +0 -207
- package/lib/connectors/schedule-state-store.ts +0 -54
- package/lib/connectors/schedule.ts +0 -4
- package/lib/connectors/slack-adapter.ts +0 -36
- package/lib/connectors/slack-connector-schema.ts +0 -13
- package/lib/connectors/slack-event-processor.ts +0 -97
- package/lib/connectors/slack-listener.ts +0 -97
- package/lib/connectors/slack.ts +0 -4
- package/lib/engine/channels/channels.ts +0 -520
- package/lib/engine/claude/claude.ts +0 -205
- package/lib/engine/claude/gateway-controller.ts +0 -4
- package/lib/engine/fs/file-system.ts +0 -23
- package/lib/engine/fs/memory-file-system.ts +0 -102
- package/lib/engine/fs/node-file-system.ts +0 -68
- package/lib/engine/http/http-client.ts +0 -17
- package/lib/engine/http/memory-http-client.ts +0 -36
- package/lib/engine/http/node-http-client.ts +0 -23
- package/lib/engine/id/id-generator.ts +0 -7
- package/lib/engine/id/memory-id-generator.ts +0 -20
- package/lib/engine/id/node-id-generator.ts +0 -7
- package/lib/engine/logger/logger.ts +0 -11
- package/lib/engine/logger/memory-logger.ts +0 -28
- package/lib/engine/logger/node-logger.ts +0 -49
- package/lib/engine/logger/noop-logger.ts +0 -9
- package/lib/engine/mcp/channel-server.ts +0 -123
- package/lib/engine/mcp/channel-subscriber.ts +0 -82
- package/lib/engine/mcp/mcp.ts +0 -126
- package/lib/engine/mcp/read-channel-connectors.ts +0 -34
- package/lib/engine/mcp/read-gateway-token.ts +0 -16
- package/lib/engine/mcp/usage-hint-for-type.ts +0 -15
- package/lib/engine/process/memory-process-runner.ts +0 -88
- package/lib/engine/process/node-process-runner.ts +0 -91
- package/lib/engine/process/process-runner.ts +0 -33
- package/lib/engine/profiles/profile-channel-checker.ts +0 -7
- package/lib/engine/profiles/profiles.ts +0 -126
- package/lib/engine/settings/mock-settings-reader.ts +0 -27
- package/lib/engine/settings/settings-reader.ts +0 -6
- package/lib/engine/settings/settings-schema.ts +0 -48
- package/lib/engine/settings/settings-store.ts +0 -110
- package/lib/engine/time/clock.ts +0 -15
- package/lib/engine/time/memory-clock.ts +0 -26
- package/lib/engine/time/node-clock.ts +0 -7
- package/lib/funnel.ts +0 -294
- package/lib/gateway/auth-middleware.ts +0 -44
- package/lib/gateway/broadcaster.ts +0 -319
- package/lib/gateway/channel-publisher.ts +0 -67
- package/lib/gateway/daemon.ts +0 -47
- package/lib/gateway/factory.ts +0 -10
- package/lib/gateway/funnel-event-store.ts +0 -155
- package/lib/gateway/gateway-server.ts +0 -426
- package/lib/gateway/gateway-token.ts +0 -79
- package/lib/gateway/gateway.ts +0 -209
- package/lib/gateway/kill-competing-slack-gateways.ts +0 -56
- package/lib/gateway/listener-supervisor.ts +0 -339
- package/lib/gateway/listeners-client.ts +0 -128
- package/lib/gateway/publish-schema.ts +0 -27
- package/lib/gateway/resolve-daemon-script.ts +0 -26
- package/lib/gateway/routes/channels.connectors.call.ts +0 -39
- package/lib/gateway/routes/channels.publish.ts +0 -44
- package/lib/gateway/routes/health.ts +0 -13
- package/lib/gateway/routes/index.ts +0 -26
- package/lib/gateway/routes/listeners.list.ts +0 -6
- package/lib/gateway/routes/listeners.restart.ts +0 -15
- package/lib/gateway/routes/listeners.start.ts +0 -15
- package/lib/gateway/routes/listeners.stop.ts +0 -15
- package/lib/gateway/routes/route-deps.ts +0 -19
- package/lib/gateway/routes/status.ts +0 -15
- package/lib/gateway/routes/validator.ts +0 -17
- package/lib/index.ts +0 -67
- package/lib/logger/leuco-human-file-writer.ts +0 -65
- package/lib/logger/leuco-human-logger.ts +0 -98
- package/lib/logger/leuco-human-record.ts +0 -16
- package/lib/logger/leuco-human-stdout-writer.ts +0 -26
- package/lib/logger/leuco-human-writer.ts +0 -14
- package/lib/logger/leuco-logger-memory-sink.ts +0 -67
- package/lib/logger/leuco-logger-record.ts +0 -13
- package/lib/logger/leuco-logger-sink.ts +0 -33
- package/lib/logger/leuco-logger-sqlite-sink.ts +0 -355
- package/lib/logger/leuco-logger.ts +0 -135
- package/lib/tui/app.tsx +0 -357
- package/lib/tui/components/add-row.tsx +0 -18
- package/lib/tui/components/brand.tsx +0 -27
- package/lib/tui/components/card.tsx +0 -44
- package/lib/tui/components/detail-bar.tsx +0 -46
- package/lib/tui/components/editable-field.tsx +0 -33
- package/lib/tui/components/empty-state.tsx +0 -11
- package/lib/tui/components/gateway-status.tsx +0 -66
- package/lib/tui/components/keymap.tsx +0 -29
- package/lib/tui/components/menu-item.tsx +0 -73
- package/lib/tui/components/menu.tsx +0 -26
- package/lib/tui/components/panel-header.tsx +0 -22
- package/lib/tui/components/readonly-field.tsx +0 -18
- package/lib/tui/components/section-header.tsx +0 -25
- package/lib/tui/components/selection-accent.tsx +0 -32
- package/lib/tui/components/session-item.tsx +0 -33
- package/lib/tui/components/session-list.tsx +0 -33
- package/lib/tui/components/ui/hascii/accordion-item.tsx +0 -88
- package/lib/tui/components/ui/hascii/accordion.tsx +0 -96
- package/lib/tui/components/ui/hascii/alert-dialog.tsx +0 -43
- package/lib/tui/components/ui/hascii/badge.tsx +0 -51
- package/lib/tui/components/ui/hascii/breadcrumb.tsx +0 -58
- package/lib/tui/components/ui/hascii/button.tsx +0 -194
- package/lib/tui/components/ui/hascii/card-content.tsx +0 -14
- package/lib/tui/components/ui/hascii/card-description.tsx +0 -13
- package/lib/tui/components/ui/hascii/card-footer.tsx +0 -14
- package/lib/tui/components/ui/hascii/card-header.tsx +0 -14
- package/lib/tui/components/ui/hascii/card-title.tsx +0 -13
- package/lib/tui/components/ui/hascii/card.tsx +0 -27
- package/lib/tui/components/ui/hascii/checkbox.tsx +0 -65
- package/lib/tui/components/ui/hascii/command.tsx +0 -159
- package/lib/tui/components/ui/hascii/dialog-content.tsx +0 -14
- package/lib/tui/components/ui/hascii/dialog-description.tsx +0 -13
- package/lib/tui/components/ui/hascii/dialog-footer.tsx +0 -14
- package/lib/tui/components/ui/hascii/dialog-header.tsx +0 -14
- package/lib/tui/components/ui/hascii/dialog-title.tsx +0 -13
- package/lib/tui/components/ui/hascii/dialog.tsx +0 -27
- package/lib/tui/components/ui/hascii/file-tree.tsx +0 -142
- package/lib/tui/components/ui/hascii/focus-group.tsx +0 -62
- package/lib/tui/components/ui/hascii/form-item.tsx +0 -43
- package/lib/tui/components/ui/hascii/input-otp.tsx +0 -86
- package/lib/tui/components/ui/hascii/input.tsx +0 -130
- package/lib/tui/components/ui/hascii/pagination.tsx +0 -105
- package/lib/tui/components/ui/hascii/progress.tsx +0 -28
- package/lib/tui/components/ui/hascii/select.tsx +0 -131
- package/lib/tui/components/ui/hascii/separator.tsx +0 -35
- package/lib/tui/components/ui/hascii/sidebar-content.tsx +0 -23
- package/lib/tui/components/ui/hascii/sidebar-header.tsx +0 -14
- package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +0 -67
- package/lib/tui/components/ui/hascii/sidebar.tsx +0 -24
- package/lib/tui/components/ui/hascii/skeleton.tsx +0 -60
- package/lib/tui/components/ui/hascii/slider.tsx +0 -91
- package/lib/tui/components/ui/hascii/snackbar.tsx +0 -75
- package/lib/tui/components/ui/hascii/sparkline.tsx +0 -53
- package/lib/tui/components/ui/hascii/spinner.tsx +0 -47
- package/lib/tui/components/ui/hascii/stepper.tsx +0 -54
- package/lib/tui/components/ui/hascii/switch.tsx +0 -66
- package/lib/tui/components/ui/hascii/table.tsx +0 -95
- package/lib/tui/components/ui/hascii/tabs.tsx +0 -59
- package/lib/tui/components/ui/hascii/toggle-group-item.tsx +0 -45
- package/lib/tui/components/ui/hascii/toggle-group.tsx +0 -99
- package/lib/tui/components/ui/hascii/tree.tsx +0 -104
- package/lib/tui/components/view-shell.tsx +0 -44
- package/lib/tui/filter-input.tsx +0 -33
- package/lib/tui/hooks/hascii/use-pressable.ts +0 -54
- package/lib/tui/parse-comma-list.ts +0 -14
- package/lib/tui/profile-launcher.tsx +0 -61
- package/lib/tui/scrollbar-options.ts +0 -19
- package/lib/tui/sidebar.tsx +0 -50
- package/lib/tui/theme.ts +0 -40
- package/lib/tui/tui.tsx +0 -20
- package/lib/tui/types.ts +0 -38
- package/lib/tui/unique-name.ts +0 -18
- package/lib/tui/use-event-stream.ts +0 -133
- package/lib/tui/use-snapshot.ts +0 -99
- package/lib/tui/utils/hascii/form-item-context.tsx +0 -23
- package/lib/tui/utils/hascii/input-focus-context.tsx +0 -31
- package/lib/tui/utils/hascii/theme-context.tsx +0 -26
- package/lib/tui/utils/hascii/theme.ts +0 -176
- package/lib/tui/views/channels-view.tsx +0 -108
- package/lib/tui/views/connectors-view.tsx +0 -164
- package/lib/tui/views/events-view.tsx +0 -160
- package/lib/tui/views/listeners-view.tsx +0 -80
- 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,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,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,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
|
-
}
|