@interactive-inc/claude-funnel 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -133
- package/dist/bin.js +1417 -0
- package/dist/gateway/daemon.js +513 -0
- package/dist/highlights-eq9cgrbb.scm +604 -0
- package/dist/highlights-ghv9g403.scm +205 -0
- package/dist/highlights-hk7bwhj4.scm +284 -0
- package/dist/highlights-r812a2qc.scm +150 -0
- package/dist/highlights-x6tmsnaa.scm +115 -0
- package/dist/injections-73j83es3.scm +27 -0
- package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/lib/bin.ts +78 -0
- package/lib/{modules → cli}/router/to-request.ts +13 -20
- package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +27 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +40 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +41 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +22 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +23 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.ts +26 -0
- package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +92 -0
- package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +22 -0
- package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +63 -0
- package/lib/cli/routes/channels.$channel.connectors.ts +26 -0
- package/lib/cli/routes/channels.$channel.rename.$newName.ts +22 -0
- package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +34 -0
- package/lib/cli/routes/channels.$channel.ts +34 -0
- package/lib/cli/routes/channels.add.$channel.ts +33 -0
- package/lib/cli/routes/channels.remove.$channel.ts +20 -0
- package/lib/cli/routes/channels.ts +39 -0
- package/lib/cli/routes/claude.ts +69 -0
- package/lib/cli/routes/gateway.listeners.ts +41 -0
- package/lib/cli/routes/gateway.logs.ts +123 -0
- package/lib/{routes/gateway/restart.ts → cli/routes/gateway.restart.ts} +20 -5
- package/lib/cli/routes/gateway.run.ts +41 -0
- package/lib/cli/routes/gateway.start.ts +50 -0
- package/lib/cli/routes/gateway.status.ts +19 -0
- package/lib/cli/routes/gateway.stop.ts +32 -0
- package/lib/cli/routes/gateway.ts +55 -0
- package/lib/cli/routes/index.ts +202 -0
- package/lib/cli/routes/profiles.$profile.as-default.ts +22 -0
- package/lib/cli/routes/profiles.$profile.rename.$newName.ts +22 -0
- package/lib/cli/routes/profiles.$profile.run.ts +36 -0
- package/lib/cli/routes/profiles.add.$profile.ts +46 -0
- package/lib/cli/routes/profiles.remove.$profile.ts +20 -0
- package/lib/cli/routes/profiles.set.$profile.ts +46 -0
- package/lib/cli/routes/profiles.ts +40 -0
- package/lib/cli/routes/status.ts +93 -0
- package/lib/cli/routes/update.ts +27 -0
- package/lib/connectors/connector-config-schema.ts +16 -0
- package/lib/connectors/connector-factory.ts +94 -0
- package/lib/connectors/connector-listener.ts +20 -0
- package/lib/{modules/connectors/funnel-discord-adapter.ts → connectors/discord-adapter.ts} +6 -11
- package/lib/{modules/connectors → connectors}/discord-connector-schema.ts +4 -1
- package/lib/connectors/discord-listener.ts +111 -0
- package/lib/{modules/connectors/funnel-gh-adapter.ts → connectors/gh-adapter.ts} +3 -6
- package/lib/{modules/connectors → connectors}/gh-connector-schema.ts +4 -1
- package/lib/{modules/connectors/funnel-gh-listener.ts → connectors/gh-listener.ts} +45 -19
- package/lib/{modules/connectors → connectors}/match-cron.ts +10 -4
- package/lib/connectors/schedule-connector-schema.ts +33 -0
- package/lib/connectors/schedule-listener.ts +207 -0
- package/lib/connectors/schedule-state-store.ts +54 -0
- package/lib/connectors/slack-adapter.ts +36 -0
- package/lib/{modules/connectors → connectors}/slack-connector-schema.ts +4 -1
- package/lib/{modules/connectors/funnel-slack-event-processor.ts → connectors/slack-event-processor.ts} +15 -9
- package/lib/{modules/connectors/funnel-slack-listener.ts → connectors/slack-listener.ts} +33 -14
- package/lib/engine/channels/channels.ts +520 -0
- package/lib/{modules/claude/funnel-claude.ts → engine/claude/claude.ts} +28 -55
- package/lib/engine/claude/gateway-controller.ts +4 -0
- package/lib/{modules/fs/funnel-file-system.ts → engine/fs/file-system.ts} +4 -0
- package/lib/{modules/fs/memory-funnel-file-system.ts → engine/fs/memory-file-system.ts} +20 -3
- package/lib/{modules/fs/node-funnel-file-system.ts → engine/fs/node-file-system.ts} +14 -2
- package/lib/{modules/http/memory-funnel-http-client.ts → engine/http/memory-http-client.ts} +1 -5
- package/lib/{modules/http/node-funnel-http-client.ts → engine/http/node-http-client.ts} +1 -5
- package/lib/{modules/id/memory-funnel-id-generator.ts → engine/id/memory-id-generator.ts} +1 -1
- package/lib/{modules/id/node-funnel-id-generator.ts → engine/id/node-id-generator.ts} +1 -1
- package/lib/{modules/logger/memory-funnel-logger.ts → engine/logger/memory-logger.ts} +1 -1
- package/lib/{modules/logger/node-funnel-logger.ts → engine/logger/node-logger.ts} +1 -1
- package/lib/{modules/logger/noop-funnel-logger.ts → engine/logger/noop-logger.ts} +1 -1
- package/lib/engine/mcp/channel-server.ts +204 -0
- package/lib/{modules/mcp/funnel-mcp.ts → engine/mcp/mcp.ts} +24 -10
- package/lib/{modules/process/memory-funnel-process-runner.ts → engine/process/memory-process-runner.ts} +1 -1
- package/lib/{modules/process/node-funnel-process-runner.ts → engine/process/node-process-runner.ts} +12 -21
- package/lib/engine/profiles/profile-channel-checker.ts +7 -0
- package/lib/{modules/profiles/funnel-profiles.ts → engine/profiles/profiles.ts} +41 -43
- package/lib/{modules/settings/mock-funnel-settings-reader.ts → engine/settings/mock-settings-reader.ts} +4 -3
- package/lib/{modules/settings/funnel-settings-reader.ts → engine/settings/settings-reader.ts} +1 -1
- package/lib/engine/settings/settings-schema.ts +46 -0
- package/lib/engine/settings/settings-store.ts +110 -0
- package/lib/{modules/time/memory-funnel-clock.ts → engine/time/memory-clock.ts} +1 -1
- package/lib/{modules/time/node-funnel-clock.ts → engine/time/node-clock.ts} +1 -1
- package/lib/funnel.ts +83 -78
- package/lib/gateway/auth-middleware.ts +44 -0
- package/lib/gateway/broadcaster.ts +319 -0
- package/lib/gateway/daemon.ts +47 -0
- package/lib/gateway/factory.ts +10 -0
- package/lib/gateway/funnel-event-store.ts +155 -0
- package/lib/gateway/gateway-server.ts +414 -0
- package/lib/gateway/gateway-token.ts +79 -0
- package/lib/{modules/gateway/funnel-gateway.ts → gateway/gateway.ts} +27 -13
- package/lib/{modules/gateway → gateway}/kill-competing-slack-gateways.ts +4 -4
- package/lib/gateway/listener-supervisor.ts +339 -0
- package/lib/gateway/listeners-client.ts +128 -0
- package/lib/gateway/resolve-daemon-script.ts +26 -0
- package/lib/gateway/routes/channels.connectors.call.ts +39 -0
- package/lib/gateway/routes/health.ts +13 -0
- package/lib/gateway/routes/index.ts +24 -0
- package/lib/gateway/routes/listeners.list.ts +6 -0
- package/lib/gateway/routes/listeners.restart.ts +15 -0
- package/lib/gateway/routes/listeners.start.ts +15 -0
- package/lib/gateway/routes/listeners.stop.ts +15 -0
- package/lib/gateway/routes/route-deps.ts +11 -0
- package/lib/gateway/routes/status.ts +15 -0
- package/lib/gateway/routes/validator.ts +17 -0
- package/lib/index.ts +50 -92
- package/lib/logger/leuco-human-file-writer.ts +65 -0
- package/lib/logger/leuco-human-logger.ts +98 -0
- package/lib/logger/leuco-human-record.ts +16 -0
- package/lib/logger/leuco-human-stdout-writer.ts +26 -0
- package/lib/logger/leuco-human-writer.ts +14 -0
- package/lib/logger/leuco-logger-memory-sink.ts +67 -0
- package/lib/logger/leuco-logger-record.ts +13 -0
- package/lib/logger/leuco-logger-sink.ts +33 -0
- package/lib/logger/leuco-logger-sqlite-sink.ts +355 -0
- package/lib/logger/leuco-logger.ts +135 -0
- package/lib/tui/app.tsx +357 -0
- package/lib/tui/components/add-row.tsx +18 -0
- package/lib/tui/components/brand.tsx +27 -0
- package/lib/tui/components/card.tsx +44 -0
- package/lib/tui/components/detail-bar.tsx +46 -0
- package/lib/tui/components/editable-field.tsx +33 -0
- package/lib/tui/components/empty-state.tsx +11 -0
- package/lib/tui/components/gateway-status.tsx +66 -0
- package/lib/tui/components/keymap.tsx +29 -0
- package/lib/tui/components/menu-item.tsx +73 -0
- package/lib/tui/components/menu.tsx +26 -0
- package/lib/tui/components/panel-header.tsx +22 -0
- package/lib/tui/components/readonly-field.tsx +18 -0
- package/lib/tui/components/section-header.tsx +25 -0
- package/lib/tui/components/selection-accent.tsx +32 -0
- package/lib/tui/components/session-item.tsx +33 -0
- package/lib/tui/components/session-list.tsx +33 -0
- package/lib/tui/components/ui/hascii/accordion-item.tsx +88 -0
- package/lib/tui/components/ui/hascii/accordion.tsx +96 -0
- package/lib/tui/components/ui/hascii/alert-dialog.tsx +43 -0
- package/lib/tui/components/ui/hascii/badge.tsx +51 -0
- package/lib/tui/components/ui/hascii/breadcrumb.tsx +58 -0
- package/lib/tui/components/ui/hascii/button.tsx +194 -0
- package/lib/tui/components/ui/hascii/card-content.tsx +14 -0
- package/lib/tui/components/ui/hascii/card-description.tsx +13 -0
- package/lib/tui/components/ui/hascii/card-footer.tsx +14 -0
- package/lib/tui/components/ui/hascii/card-header.tsx +14 -0
- package/lib/tui/components/ui/hascii/card-title.tsx +13 -0
- package/lib/tui/components/ui/hascii/card.tsx +27 -0
- package/lib/tui/components/ui/hascii/checkbox.tsx +65 -0
- package/lib/tui/components/ui/hascii/command.tsx +159 -0
- package/lib/tui/components/ui/hascii/dialog-content.tsx +14 -0
- package/lib/tui/components/ui/hascii/dialog-description.tsx +13 -0
- package/lib/tui/components/ui/hascii/dialog-footer.tsx +14 -0
- package/lib/tui/components/ui/hascii/dialog-header.tsx +14 -0
- package/lib/tui/components/ui/hascii/dialog-title.tsx +13 -0
- package/lib/tui/components/ui/hascii/dialog.tsx +27 -0
- package/lib/tui/components/ui/hascii/file-tree.tsx +142 -0
- package/lib/tui/components/ui/hascii/focus-group.tsx +62 -0
- package/lib/tui/components/ui/hascii/form-item.tsx +43 -0
- package/lib/tui/components/ui/hascii/input-otp.tsx +86 -0
- package/lib/tui/components/ui/hascii/input.tsx +130 -0
- package/lib/tui/components/ui/hascii/pagination.tsx +105 -0
- package/lib/tui/components/ui/hascii/progress.tsx +28 -0
- package/lib/tui/components/ui/hascii/select.tsx +131 -0
- package/lib/tui/components/ui/hascii/separator.tsx +35 -0
- package/lib/tui/components/ui/hascii/sidebar-content.tsx +23 -0
- package/lib/tui/components/ui/hascii/sidebar-header.tsx +14 -0
- package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +67 -0
- package/lib/tui/components/ui/hascii/sidebar.tsx +24 -0
- package/lib/tui/components/ui/hascii/skeleton.tsx +60 -0
- package/lib/tui/components/ui/hascii/slider.tsx +91 -0
- package/lib/tui/components/ui/hascii/snackbar.tsx +75 -0
- package/lib/tui/components/ui/hascii/sparkline.tsx +53 -0
- package/lib/tui/components/ui/hascii/spinner.tsx +47 -0
- package/lib/tui/components/ui/hascii/stepper.tsx +54 -0
- package/lib/tui/components/ui/hascii/switch.tsx +66 -0
- package/lib/tui/components/ui/hascii/table.tsx +95 -0
- package/lib/tui/components/ui/hascii/tabs.tsx +59 -0
- package/lib/tui/components/ui/hascii/toggle-group-item.tsx +45 -0
- package/lib/tui/components/ui/hascii/toggle-group.tsx +99 -0
- package/lib/tui/components/ui/hascii/tree.tsx +104 -0
- package/lib/tui/components/view-shell.tsx +44 -0
- package/lib/tui/filter-input.tsx +33 -0
- package/lib/tui/hooks/hascii/use-pressable.ts +54 -0
- package/lib/tui/parse-comma-list.ts +14 -0
- package/lib/tui/profile-launcher.tsx +61 -0
- package/lib/tui/scrollbar-options.ts +19 -0
- package/lib/tui/sidebar.tsx +50 -0
- package/lib/tui/theme.ts +40 -0
- package/lib/tui/tui.tsx +20 -0
- package/lib/tui/types.ts +38 -0
- package/lib/tui/unique-name.ts +18 -0
- package/lib/tui/use-event-stream.ts +133 -0
- package/lib/tui/use-snapshot.ts +99 -0
- package/lib/tui/utils/hascii/form-item-context.tsx +23 -0
- package/lib/tui/utils/hascii/input-focus-context.tsx +31 -0
- package/lib/tui/utils/hascii/theme-context.tsx +26 -0
- package/lib/tui/utils/hascii/theme.ts +176 -0
- package/lib/tui/views/channels-view.tsx +108 -0
- package/lib/tui/views/connectors-view.tsx +164 -0
- package/lib/tui/views/events-view.tsx +160 -0
- package/lib/tui/views/listeners-view.tsx +80 -0
- package/lib/tui/views/profiles-view.tsx +152 -0
- package/package.json +50 -44
- package/lib/api.ts +0 -54
- package/lib/modules/channels/channel-connector-ref-updater.ts +0 -4
- package/lib/modules/channels/funnel-channels.ts +0 -160
- package/lib/modules/connectors/connector-config-schema.ts +0 -16
- package/lib/modules/connectors/connector-existence-checker.ts +0 -3
- package/lib/modules/connectors/funnel-callable-connector-store.ts +0 -9
- package/lib/modules/connectors/funnel-connector-listener.ts +0 -5
- package/lib/modules/connectors/funnel-connector-stores.ts +0 -52
- package/lib/modules/connectors/funnel-connector-type-store.ts +0 -24
- package/lib/modules/connectors/funnel-connectors.ts +0 -151
- package/lib/modules/connectors/funnel-discord-listener.ts +0 -71
- package/lib/modules/connectors/funnel-discord-store.ts +0 -88
- package/lib/modules/connectors/funnel-gh-store.ts +0 -101
- package/lib/modules/connectors/funnel-json-connector-store.ts +0 -100
- package/lib/modules/connectors/funnel-schedule-listener.ts +0 -130
- package/lib/modules/connectors/funnel-schedule-store.ts +0 -195
- package/lib/modules/connectors/funnel-slack-adapter.ts +0 -31
- package/lib/modules/connectors/funnel-slack-store.ts +0 -90
- package/lib/modules/connectors/migrate-legacy-connectors.ts +0 -81
- package/lib/modules/connectors/schedule-connector-schema.ts +0 -18
- package/lib/modules/connectors/schedule-last-fired-store.ts +0 -48
- package/lib/modules/gateway/daemon.ts +0 -74
- package/lib/modules/gateway/funnel-broadcaster.ts +0 -37
- package/lib/modules/gateway/funnel-event-logger.ts +0 -59
- package/lib/modules/gateway/funnel-gateway-server.ts +0 -241
- package/lib/modules/mcp/channel-server.ts +0 -76
- package/lib/modules/profiles/profile-channel-checker.ts +0 -3
- package/lib/modules/profiles/profile-channel-ref-updater.ts +0 -3
- package/lib/modules/repos/funnel-repositories.ts +0 -112
- package/lib/modules/schedule/funnel-schedule.ts +0 -39
- package/lib/modules/settings/funnel-settings-store.ts +0 -56
- package/lib/modules/settings/settings-schema.ts +0 -33
- package/lib/modules/tui/app.tsx +0 -44
- package/lib/modules/tui/tui.tsx +0 -13
- package/lib/routes/channels/add.help.ts +0 -3
- package/lib/routes/channels/add.ts +0 -21
- package/lib/routes/channels/connectors-attach.help.ts +0 -3
- package/lib/routes/channels/connectors-attach.ts +0 -17
- package/lib/routes/channels/connectors-detach.help.ts +0 -3
- package/lib/routes/channels/connectors-detach.ts +0 -17
- package/lib/routes/channels/group.help.ts +0 -16
- package/lib/routes/channels/group.ts +0 -22
- package/lib/routes/channels/remove.help.ts +0 -3
- package/lib/routes/channels/remove.ts +0 -17
- package/lib/routes/channels/rename.help.ts +0 -5
- package/lib/routes/channels/rename.ts +0 -17
- package/lib/routes/channels/routes.ts +0 -19
- package/lib/routes/channels/show.help.ts +0 -1
- package/lib/routes/channels/show.ts +0 -26
- package/lib/routes/claude/claude.help.ts +0 -16
- package/lib/routes/claude/claude.ts +0 -76
- package/lib/routes/claude/routes.ts +0 -4
- package/lib/routes/connectors/add.help.ts +0 -28
- package/lib/routes/connectors/add.ts +0 -64
- package/lib/routes/connectors/group.help.ts +0 -14
- package/lib/routes/connectors/group.ts +0 -18
- package/lib/routes/connectors/remove.help.ts +0 -3
- package/lib/routes/connectors/remove.ts +0 -17
- package/lib/routes/connectors/rename.help.ts +0 -5
- package/lib/routes/connectors/rename.ts +0 -17
- package/lib/routes/connectors/routes.ts +0 -23
- package/lib/routes/connectors/schedules-add.help.ts +0 -11
- package/lib/routes/connectors/schedules-add.ts +0 -33
- package/lib/routes/connectors/schedules-group.help.ts +0 -1
- package/lib/routes/connectors/schedules-group.ts +0 -38
- package/lib/routes/connectors/schedules-remove.help.ts +0 -3
- package/lib/routes/connectors/schedules-remove.ts +0 -17
- package/lib/routes/connectors/set.help.ts +0 -8
- package/lib/routes/connectors/set.ts +0 -72
- package/lib/routes/connectors/show.help.ts +0 -1
- package/lib/routes/connectors/show.ts +0 -41
- package/lib/routes/gateway/group.help.ts +0 -15
- package/lib/routes/gateway/group.ts +0 -28
- package/lib/routes/gateway/logs.help.ts +0 -13
- package/lib/routes/gateway/logs.ts +0 -102
- package/lib/routes/gateway/restart.help.ts +0 -10
- package/lib/routes/gateway/routes.ts +0 -18
- package/lib/routes/gateway/run.help.ts +0 -12
- package/lib/routes/gateway/run.ts +0 -35
- package/lib/routes/gateway/start.help.ts +0 -15
- package/lib/routes/gateway/start.ts +0 -32
- package/lib/routes/gateway/status.help.ts +0 -9
- package/lib/routes/gateway/status.ts +0 -28
- package/lib/routes/gateway/stop.help.ts +0 -8
- package/lib/routes/gateway/stop.ts +0 -21
- package/lib/routes/profiles/add.help.ts +0 -3
- package/lib/routes/profiles/add.ts +0 -33
- package/lib/routes/profiles/group.help.ts +0 -16
- package/lib/routes/profiles/group.ts +0 -25
- package/lib/routes/profiles/launch.help.ts +0 -4
- package/lib/routes/profiles/launch.ts +0 -36
- package/lib/routes/profiles/remove.help.ts +0 -3
- package/lib/routes/profiles/remove.ts +0 -17
- package/lib/routes/profiles/rename.help.ts +0 -5
- package/lib/routes/profiles/rename.ts +0 -17
- package/lib/routes/profiles/routes.ts +0 -18
- package/lib/routes/profiles/set.help.ts +0 -5
- package/lib/routes/profiles/set.ts +0 -32
- package/lib/routes/repos/add.help.ts +0 -6
- package/lib/routes/repos/add.ts +0 -20
- package/lib/routes/repos/group.help.ts +0 -11
- package/lib/routes/repos/group.ts +0 -18
- package/lib/routes/repos/remove.help.ts +0 -3
- package/lib/routes/repos/remove.ts +0 -17
- package/lib/routes/repos/rename.help.ts +0 -5
- package/lib/routes/repos/rename.ts +0 -17
- package/lib/routes/repos/routes.ts +0 -17
- package/lib/routes/repos/set.help.ts +0 -5
- package/lib/routes/repos/set.ts +0 -21
- package/lib/routes/repos/show.help.ts +0 -1
- package/lib/routes/repos/show.ts +0 -19
- package/lib/routes/request/discord-help.ts +0 -9
- package/lib/routes/request/discord.help.ts +0 -19
- package/lib/routes/request/discord.ts +0 -65
- package/lib/routes/request/group.help.ts +0 -15
- package/lib/routes/request/group.ts +0 -9
- package/lib/routes/request/routes.ts +0 -14
- package/lib/routes/request/slack-help.ts +0 -9
- package/lib/routes/request/slack.help.ts +0 -19
- package/lib/routes/request/slack.ts +0 -61
- package/lib/routes/status/routes.ts +0 -4
- package/lib/routes/status/status.help.ts +0 -6
- package/lib/routes/status/status.ts +0 -77
- package/lib/routes/update/routes.ts +0 -4
- package/lib/routes/update/update.help.ts +0 -5
- package/lib/routes/update/update.ts +0 -21
- package/lib/routes.ts +0 -40
- /package/lib/{factory.ts → cli/factory.ts} +0 -0
- /package/lib/{modules → cli}/router/query-to-cli-args.ts +0 -0
- /package/lib/{modules → cli}/router/validator.ts +0 -0
- /package/lib/{modules/connectors/funnel-connector-adapter.ts → connectors/connector-adapter.ts} +0 -0
- /package/lib/{modules/connectors/funnel-discord-event-processor.ts → connectors/discord-event-processor.ts} +0 -0
- /package/lib/{modules/http/funnel-http-client.ts → engine/http/http-client.ts} +0 -0
- /package/lib/{modules/id/funnel-id-generator.ts → engine/id/id-generator.ts} +0 -0
- /package/lib/{modules/logger/funnel-logger.ts → engine/logger/logger.ts} +0 -0
- /package/lib/{modules/process/funnel-process-runner.ts → engine/process/process-runner.ts} +0 -0
- /package/lib/{modules/time/funnel-clock.ts → engine/time/clock.ts} +0 -0
|
@@ -1,30 +1,27 @@
|
|
|
1
1
|
import { join } from "node:path"
|
|
2
|
-
import type { FunnelChannels } from "@/
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import { FunnelLogger } from "@/
|
|
7
|
-
import { NodeFunnelLogger } from "@/
|
|
8
|
-
import type { FunnelMcp } from "@/
|
|
9
|
-
import { FunnelProcessRunner } from "@/
|
|
10
|
-
import { NodeFunnelProcessRunner } from "@/
|
|
11
|
-
import
|
|
12
|
-
import { FUNNEL_DIR } from "@/modules/settings/funnel-settings-store"
|
|
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"
|
|
13
12
|
|
|
14
13
|
export type LaunchOptions = {
|
|
15
14
|
channel: string
|
|
16
|
-
|
|
15
|
+
cwd?: string
|
|
17
16
|
subAgent?: string
|
|
18
|
-
envFiles?: string[]
|
|
19
17
|
userArgs?: string[]
|
|
20
18
|
profileName?: string
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
type Deps = {
|
|
24
22
|
channels: FunnelChannels
|
|
25
|
-
repositories: FunnelRepositories
|
|
26
23
|
mcp: FunnelMcp
|
|
27
|
-
gateway:
|
|
24
|
+
gateway: GatewayController
|
|
28
25
|
process?: FunnelProcessRunner
|
|
29
26
|
fs?: FunnelFileSystem
|
|
30
27
|
logger?: FunnelLogger
|
|
@@ -43,9 +40,8 @@ const defaultLogger = new NodeFunnelLogger()
|
|
|
43
40
|
*/
|
|
44
41
|
export class FunnelClaude {
|
|
45
42
|
private readonly channels: FunnelChannels
|
|
46
|
-
private readonly repositories: FunnelRepositories
|
|
47
43
|
private readonly mcp: FunnelMcp
|
|
48
|
-
private readonly gateway:
|
|
44
|
+
private readonly gateway: GatewayController
|
|
49
45
|
private readonly process: FunnelProcessRunner
|
|
50
46
|
private readonly fs: FunnelFileSystem
|
|
51
47
|
private readonly logger: FunnelLogger
|
|
@@ -53,7 +49,6 @@ export class FunnelClaude {
|
|
|
53
49
|
|
|
54
50
|
constructor(deps: Deps) {
|
|
55
51
|
this.channels = deps.channels
|
|
56
|
-
this.repositories = deps.repositories
|
|
57
52
|
this.mcp = deps.mcp
|
|
58
53
|
this.gateway = deps.gateway
|
|
59
54
|
this.process = deps.process ?? defaultProcess
|
|
@@ -64,7 +59,7 @@ export class FunnelClaude {
|
|
|
64
59
|
}
|
|
65
60
|
|
|
66
61
|
async launch(options: LaunchOptions): Promise<number> {
|
|
67
|
-
const channel = this.channels.get(options.channel)
|
|
62
|
+
const channel = this.channels.get(options.channel) ?? this.channels.getById(options.channel)
|
|
68
63
|
|
|
69
64
|
if (!channel) {
|
|
70
65
|
throw new Error(`channel "${options.channel}" not found`)
|
|
@@ -74,9 +69,7 @@ export class FunnelClaude {
|
|
|
74
69
|
throw new Error(`profile "${options.profileName}" is already running`)
|
|
75
70
|
}
|
|
76
71
|
|
|
77
|
-
const cwd = options.
|
|
78
|
-
? this.repositories.resolvePath(options.repo)
|
|
79
|
-
: globalThis.process.cwd()
|
|
72
|
+
const cwd = options.cwd ?? globalThis.process.cwd()
|
|
80
73
|
|
|
81
74
|
if (!this.mcp.findInstalledName(cwd)) {
|
|
82
75
|
this.mcp.install(cwd)
|
|
@@ -95,11 +88,11 @@ export class FunnelClaude {
|
|
|
95
88
|
}
|
|
96
89
|
|
|
97
90
|
const claudeArgs = this.buildArgs(options, cwd)
|
|
98
|
-
const env = this.buildEnv(
|
|
91
|
+
const env = this.buildEnv(channel.id)
|
|
99
92
|
|
|
100
93
|
this.logger.info(`claude launch`, {
|
|
101
94
|
channel: options.channel,
|
|
102
|
-
|
|
95
|
+
channelId: channel.id,
|
|
103
96
|
subAgent: options.subAgent,
|
|
104
97
|
cwd,
|
|
105
98
|
})
|
|
@@ -152,11 +145,12 @@ export class FunnelClaude {
|
|
|
152
145
|
}
|
|
153
146
|
|
|
154
147
|
private installCleanup(profileName: string): void {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
148
|
+
// Default Bun behavior on SIGINT/SIGTERM is process.exit(130/143), which
|
|
149
|
+
// fires the "exit" event. Hooking only "exit" keeps the PID file cleanup
|
|
150
|
+
// running while letting the signal terminate the process normally —
|
|
151
|
+
// adding our own SIGINT handler would suppress the default exit and leave
|
|
152
|
+
// funnel hanging until claude responds.
|
|
153
|
+
globalThis.process.once("exit", () => this.removePidFile(profileName))
|
|
160
154
|
}
|
|
161
155
|
|
|
162
156
|
private isProcessAlive(pid: number): boolean {
|
|
@@ -191,35 +185,14 @@ export class FunnelClaude {
|
|
|
191
185
|
return result
|
|
192
186
|
}
|
|
193
187
|
|
|
194
|
-
private buildEnv(
|
|
195
|
-
const env: Record<string, string> = {
|
|
196
|
-
|
|
197
|
-
if (options.envFiles) {
|
|
198
|
-
for (const file of options.envFiles) {
|
|
199
|
-
const filePath = `${cwd}/${file}`
|
|
200
|
-
|
|
201
|
-
if (!this.fs.existsSync(filePath)) continue
|
|
202
|
-
|
|
203
|
-
const content = this.fs.readFileSync(filePath)
|
|
204
|
-
|
|
205
|
-
for (const line of content.split("\n")) {
|
|
206
|
-
const trimmed = line.trim()
|
|
207
|
-
|
|
208
|
-
if (!trimmed || trimmed.startsWith("#")) continue
|
|
209
|
-
|
|
210
|
-
const eqIndex = trimmed.indexOf("=")
|
|
211
|
-
|
|
212
|
-
if (eqIndex < 0) continue
|
|
213
|
-
|
|
214
|
-
const key = trimmed.slice(0, eqIndex)
|
|
215
|
-
const value = trimmed.slice(eqIndex + 1).replace(/^["']|["']$/g, "")
|
|
188
|
+
private buildEnv(channelId: string): Record<string, string> {
|
|
189
|
+
const env: Record<string, string> = {}
|
|
216
190
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
191
|
+
for (const [key, value] of Object.entries(globalThis.process.env)) {
|
|
192
|
+
if (typeof value === "string") env[key] = value
|
|
220
193
|
}
|
|
221
194
|
|
|
222
|
-
env.FUNNEL_CHANNEL_ID =
|
|
195
|
+
env.FUNNEL_CHANNEL_ID = channelId
|
|
223
196
|
|
|
224
197
|
return env
|
|
225
198
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type FileStat = {
|
|
2
2
|
mtimeMs: number
|
|
3
|
+
/** POSIX mode bits (e.g. 0o600). `null` when the underlying FS does not expose mode. */
|
|
4
|
+
mode: number | null
|
|
3
5
|
}
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -11,6 +13,8 @@ export abstract class FunnelFileSystem {
|
|
|
11
13
|
abstract existsSync(path: string): boolean
|
|
12
14
|
abstract readFileSync(path: string): string
|
|
13
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
|
|
14
18
|
abstract appendFileSync(path: string, data: string): void
|
|
15
19
|
abstract unlink(path: string): void
|
|
16
20
|
abstract mkdirSync(path: string, options?: { recursive?: boolean }): void
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import { type FileStat, FunnelFileSystem } from "@/
|
|
1
|
+
import { type FileStat, FunnelFileSystem } from "@/engine/fs/file-system"
|
|
2
2
|
|
|
3
3
|
type Props = {
|
|
4
4
|
dirs?: string[]
|
|
5
5
|
files?: Record<string, string>
|
|
6
6
|
mtimes?: Record<string, number>
|
|
7
|
+
modes?: Record<string, number>
|
|
7
8
|
now?: () => number
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
const SECRET_MODE = 0o600
|
|
12
|
+
|
|
10
13
|
export class MemoryFunnelFileSystem extends FunnelFileSystem {
|
|
11
14
|
private readonly dirs: Set<string>
|
|
12
15
|
private readonly files: Map<string, string>
|
|
13
16
|
private readonly mtimes: Map<string, number>
|
|
17
|
+
private readonly modes: Map<string, number>
|
|
14
18
|
private readonly now: () => number
|
|
15
19
|
|
|
16
20
|
constructor(props: Props = {}) {
|
|
@@ -18,6 +22,7 @@ export class MemoryFunnelFileSystem extends FunnelFileSystem {
|
|
|
18
22
|
this.dirs = new Set(props.dirs ?? [])
|
|
19
23
|
this.files = new Map(Object.entries(props.files ?? {}))
|
|
20
24
|
this.mtimes = new Map(Object.entries(props.mtimes ?? {}))
|
|
25
|
+
this.modes = new Map(Object.entries(props.modes ?? {}))
|
|
21
26
|
this.now = props.now ?? (() => Date.now())
|
|
22
27
|
}
|
|
23
28
|
|
|
@@ -34,6 +39,12 @@ export class MemoryFunnelFileSystem extends FunnelFileSystem {
|
|
|
34
39
|
this.touch(path)
|
|
35
40
|
}
|
|
36
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
|
+
|
|
37
48
|
appendFileSync(path: string, data: string): void {
|
|
38
49
|
const prev = this.files.get(path) ?? ""
|
|
39
50
|
this.files.set(path, prev + data)
|
|
@@ -43,9 +54,11 @@ export class MemoryFunnelFileSystem extends FunnelFileSystem {
|
|
|
43
54
|
unlink(path: string): void {
|
|
44
55
|
this.files.delete(path)
|
|
45
56
|
this.mtimes.delete(path)
|
|
57
|
+
this.modes.delete(path)
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
mkdirSync(path: string): void {
|
|
60
|
+
mkdirSync(path: string, options?: { recursive?: boolean }): void {
|
|
61
|
+
void options
|
|
49
62
|
this.dirs.add(path)
|
|
50
63
|
}
|
|
51
64
|
|
|
@@ -71,13 +84,17 @@ export class MemoryFunnelFileSystem extends FunnelFileSystem {
|
|
|
71
84
|
throw new Error(`not found: ${path}`)
|
|
72
85
|
}
|
|
73
86
|
|
|
74
|
-
return { mtimeMs }
|
|
87
|
+
return { mtimeMs, mode: this.modes.get(path) ?? null }
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
setMtime(path: string, mtimeMs: number): void {
|
|
78
91
|
this.mtimes.set(path, mtimeMs)
|
|
79
92
|
}
|
|
80
93
|
|
|
94
|
+
setMode(path: string, mode: number): void {
|
|
95
|
+
this.modes.set(path, mode)
|
|
96
|
+
}
|
|
97
|
+
|
|
81
98
|
private touch(path: string): void {
|
|
82
99
|
if (!this.mtimes.has(path)) this.mtimes.set(path, this.now())
|
|
83
100
|
else this.mtimes.set(path, this.now())
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
appendFileSync,
|
|
3
|
+
chmodSync,
|
|
3
4
|
existsSync,
|
|
4
5
|
mkdirSync,
|
|
5
6
|
readdirSync,
|
|
@@ -8,7 +9,9 @@ import {
|
|
|
8
9
|
unlinkSync,
|
|
9
10
|
writeFileSync,
|
|
10
11
|
} from "node:fs"
|
|
11
|
-
import { type FileStat, FunnelFileSystem } from "@/
|
|
12
|
+
import { type FileStat, FunnelFileSystem } from "@/engine/fs/file-system"
|
|
13
|
+
|
|
14
|
+
const SECRET_MODE = 0o600
|
|
12
15
|
|
|
13
16
|
export class NodeFunnelFileSystem extends FunnelFileSystem {
|
|
14
17
|
constructor() {
|
|
@@ -28,6 +31,15 @@ export class NodeFunnelFileSystem extends FunnelFileSystem {
|
|
|
28
31
|
writeFileSync(path, data)
|
|
29
32
|
}
|
|
30
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
|
+
|
|
31
43
|
appendFileSync(path: string, data: string): void {
|
|
32
44
|
appendFileSync(path, data)
|
|
33
45
|
}
|
|
@@ -51,6 +63,6 @@ export class NodeFunnelFileSystem extends FunnelFileSystem {
|
|
|
51
63
|
statSync(path: string): FileStat {
|
|
52
64
|
const stat = statSync(path)
|
|
53
65
|
|
|
54
|
-
return { mtimeMs: stat.mtimeMs }
|
|
66
|
+
return { mtimeMs: stat.mtimeMs, mode: stat.mode & 0o777 }
|
|
55
67
|
}
|
|
56
68
|
}
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
FunnelHttpClient,
|
|
3
|
-
type HttpRequest,
|
|
4
|
-
type HttpResponse,
|
|
5
|
-
} from "@/modules/http/funnel-http-client"
|
|
1
|
+
import { FunnelHttpClient, type HttpRequest, type HttpResponse } from "@/engine/http/http-client"
|
|
6
2
|
|
|
7
3
|
export type MemoryHttpResponse = {
|
|
8
4
|
status?: number
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
FunnelHttpClient,
|
|
3
|
-
type HttpRequest,
|
|
4
|
-
type HttpResponse,
|
|
5
|
-
} from "@/modules/http/funnel-http-client"
|
|
1
|
+
import { FunnelHttpClient, type HttpRequest, type HttpResponse } from "@/engine/http/http-client"
|
|
6
2
|
|
|
7
3
|
export class NodeFunnelHttpClient extends FunnelHttpClient {
|
|
8
4
|
constructor() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { appendFileSync, mkdirSync } from "node:fs"
|
|
2
2
|
import { dirname, join } from "node:path"
|
|
3
|
-
import { FunnelLogger } from "@/
|
|
3
|
+
import { FunnelLogger } from "@/engine/logger/logger"
|
|
4
4
|
|
|
5
5
|
const DEFAULT_LOG_FILE = join("/tmp/funnel", "funnel.log")
|
|
6
6
|
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
+
import { homedir } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
6
|
+
import {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ListToolsRequestSchema,
|
|
9
|
+
} from "@modelcontextprotocol/sdk/types.js"
|
|
10
|
+
import { FUNNEL_MCP_NAME } from "@/engine/mcp/mcp"
|
|
11
|
+
import { settingsSchema } from "@/engine/settings/settings-schema"
|
|
12
|
+
|
|
13
|
+
const GATEWAY_BASE_URL = process.env.FUNNEL_GATEWAY_URL ?? "http://localhost:9742"
|
|
14
|
+
const GATEWAY_WS_URL = `${GATEWAY_BASE_URL.replace(/^http/, "ws")}/ws`
|
|
15
|
+
const RECONNECT_DELAY = 1000
|
|
16
|
+
const MAX_RECONNECT_DELAY = 10000
|
|
17
|
+
const SETTINGS_PATH = join(homedir(), ".funnel", "settings.json")
|
|
18
|
+
const TOOL_CONNECTOR_TYPES = new Set(["slack", "gh", "discord"])
|
|
19
|
+
|
|
20
|
+
const readGatewayToken = (): string | null => {
|
|
21
|
+
const fromEnv = process.env.FUNNEL_GATEWAY_TOKEN
|
|
22
|
+
|
|
23
|
+
if (fromEnv && fromEnv.length > 0) return fromEnv
|
|
24
|
+
|
|
25
|
+
const path = join(homedir(), ".funnel", "gateway.token")
|
|
26
|
+
|
|
27
|
+
if (!existsSync(path)) return null
|
|
28
|
+
|
|
29
|
+
const value = readFileSync(path, "utf-8").trim()
|
|
30
|
+
|
|
31
|
+
return value.length > 0 ? value : null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const readChannelConnectors = (
|
|
35
|
+
channelId: string,
|
|
36
|
+
): { channelName: string; connectors: { name: string; type: string }[] } | null => {
|
|
37
|
+
if (!existsSync(SETTINGS_PATH)) return null
|
|
38
|
+
|
|
39
|
+
const raw = JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"))
|
|
40
|
+
const parsed = settingsSchema.safeParse(raw)
|
|
41
|
+
|
|
42
|
+
if (!parsed.success) return null
|
|
43
|
+
|
|
44
|
+
const channel = parsed.data.channels.find((c) => c.id === channelId)
|
|
45
|
+
|
|
46
|
+
if (!channel) return null
|
|
47
|
+
|
|
48
|
+
const connectors = channel.connectors
|
|
49
|
+
.filter((c) => TOOL_CONNECTOR_TYPES.has(c.type))
|
|
50
|
+
.map((c) => ({ name: c.name, type: c.type }))
|
|
51
|
+
|
|
52
|
+
return { channelName: channel.name, connectors }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const usageHintForType = (type: string): string => {
|
|
56
|
+
if (type === "slack") {
|
|
57
|
+
return "Slack Web API. method=POST path=chat.postMessage body={channel,text,thread_ts?}"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (type === "discord") {
|
|
61
|
+
return "Discord REST API. method=POST path=/channels/<id>/messages body={content,...}"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (type === "gh") {
|
|
65
|
+
return "GitHub REST via gh CLI. method=POST path=repos/owner/repo/issues/N/comments body={body}"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return "Generic adapter call."
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const startChannelServer = async (): Promise<void> => {
|
|
72
|
+
const channelId = process.env.FUNNEL_CHANNEL_ID
|
|
73
|
+
const channel = channelId ? readChannelConnectors(channelId) : null
|
|
74
|
+
const token = readGatewayToken()
|
|
75
|
+
|
|
76
|
+
const server = new Server(
|
|
77
|
+
{ name: FUNNEL_MCP_NAME, version: "1.0.0" },
|
|
78
|
+
{
|
|
79
|
+
capabilities: {
|
|
80
|
+
experimental: { "claude/channel": {} },
|
|
81
|
+
tools: {},
|
|
82
|
+
},
|
|
83
|
+
instructions: [
|
|
84
|
+
`Events arrive inside <channel source="${FUNNEL_MCP_NAME}"> tags. Use meta.event_type to discriminate.`,
|
|
85
|
+
"",
|
|
86
|
+
"To reply or act, call the connector tool exposed by this MCP (one tool per connector configured on this channel). Each tool takes { method, path, body } matching the underlying adapter's CallInput.",
|
|
87
|
+
].join("\n"),
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
92
|
+
const tools = (channel?.connectors ?? []).map((c) => ({
|
|
93
|
+
name: c.name,
|
|
94
|
+
description: `Call the "${c.name}" (${c.type}) connector. ${usageHintForType(c.type)}`,
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: "object" as const,
|
|
97
|
+
properties: {
|
|
98
|
+
method: { type: "string", description: "HTTP verb or API method (e.g. POST, chat.postMessage)" },
|
|
99
|
+
path: { type: "string", description: "API path or method name (adapter-specific)" },
|
|
100
|
+
body: { type: "object", description: "Request body / params (adapter-specific)" },
|
|
101
|
+
},
|
|
102
|
+
required: ["method", "path"],
|
|
103
|
+
},
|
|
104
|
+
}))
|
|
105
|
+
|
|
106
|
+
return { tools }
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
110
|
+
if (!channel) {
|
|
111
|
+
throw new Error("FUNNEL_CHANNEL_ID is not set or channel not found in settings.json")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const connectorName = request.params.name
|
|
115
|
+
const args = (request.params.arguments ?? {}) as Record<string, unknown>
|
|
116
|
+
const method = typeof args.method === "string" ? args.method : ""
|
|
117
|
+
const path = typeof args.path === "string" ? args.path : ""
|
|
118
|
+
const body = args.body ?? {}
|
|
119
|
+
|
|
120
|
+
if (!method || !path) {
|
|
121
|
+
throw new Error("`method` and `path` are required")
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const url = `${GATEWAY_BASE_URL}/channels/${encodeURIComponent(channel.channelName)}/connectors/${encodeURIComponent(connectorName)}/call`
|
|
125
|
+
const headers: Record<string, string> = { "content-type": "application/json" }
|
|
126
|
+
|
|
127
|
+
if (token) headers.authorization = `Bearer ${token}`
|
|
128
|
+
|
|
129
|
+
const res = await fetch(url, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers,
|
|
132
|
+
body: JSON.stringify({ method, path, body }),
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const text = await res.text()
|
|
136
|
+
|
|
137
|
+
if (!res.ok) {
|
|
138
|
+
throw new Error(`gateway call failed (${res.status}): ${text}`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
content: [{ type: "text", text }],
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const transport = new StdioServerTransport()
|
|
147
|
+
|
|
148
|
+
await server.connect(transport)
|
|
149
|
+
|
|
150
|
+
if (!channelId) return
|
|
151
|
+
|
|
152
|
+
const baseUrl = `${GATEWAY_WS_URL}?channel=${encodeURIComponent(channelId)}`
|
|
153
|
+
const protocols = token ? [`funnel.token.${token}`] : undefined
|
|
154
|
+
let reconnectDelay = RECONNECT_DELAY
|
|
155
|
+
let lastOffset = 0
|
|
156
|
+
|
|
157
|
+
const connect = () => {
|
|
158
|
+
const sinceQuery = lastOffset > 0 ? `&since=${lastOffset}` : ""
|
|
159
|
+
const wsUrl = `${baseUrl}${sinceQuery}`
|
|
160
|
+
const ws = new WebSocket(wsUrl, protocols)
|
|
161
|
+
|
|
162
|
+
ws.addEventListener("open", () => {
|
|
163
|
+
reconnectDelay = RECONNECT_DELAY
|
|
164
|
+
process.stderr.write(`funnel: connected (${wsUrl})\n`)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
ws.addEventListener("message", async (event) => {
|
|
168
|
+
try {
|
|
169
|
+
const payload = JSON.parse(String(event.data))
|
|
170
|
+
const eventType = payload.meta?.event_type ?? "unknown"
|
|
171
|
+
|
|
172
|
+
if (typeof payload.offset === "number" && payload.offset > lastOffset) {
|
|
173
|
+
lastOffset = payload.offset
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
process.stderr.write(`funnel: received event (${eventType})\n`)
|
|
177
|
+
|
|
178
|
+
await server.notification({
|
|
179
|
+
method: "notifications/claude/channel",
|
|
180
|
+
params: {
|
|
181
|
+
content: payload.content,
|
|
182
|
+
meta: payload.meta,
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
} catch (error) {
|
|
186
|
+
process.stderr.write(
|
|
187
|
+
`funnel: error: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
ws.addEventListener("close", () => {
|
|
193
|
+
process.stderr.write(`funnel: disconnected, reconnecting in ${reconnectDelay}ms\n`)
|
|
194
|
+
setTimeout(connect, reconnectDelay)
|
|
195
|
+
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
ws.addEventListener("error", () => {
|
|
199
|
+
// close handler will reconnect
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
connect()
|
|
204
|
+
}
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { join } from "node:path"
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
import { FunnelFileSystem } from "@/engine/fs/file-system"
|
|
4
|
+
import { NodeFunnelFileSystem } from "@/engine/fs/node-file-system"
|
|
4
5
|
|
|
5
6
|
export const FUNNEL_MCP_COMMAND = "funnel"
|
|
6
7
|
export const FUNNEL_MCP_NAME = "funnel"
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
command
|
|
10
|
-
args
|
|
11
|
-
}
|
|
9
|
+
const mcpEntrySchema = z.object({
|
|
10
|
+
command: z.string().optional(),
|
|
11
|
+
args: z.array(z.string()).optional(),
|
|
12
|
+
})
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
mcpServers
|
|
15
|
-
}
|
|
14
|
+
const mcpConfigSchema = z.object({
|
|
15
|
+
mcpServers: z.record(z.string(), mcpEntrySchema).optional(),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
type McpEntry = z.infer<typeof mcpEntrySchema>
|
|
19
|
+
type McpConfig = z.infer<typeof mcpConfigSchema>
|
|
16
20
|
|
|
17
21
|
type Deps = {
|
|
18
22
|
fs?: FunnelFileSystem
|
|
@@ -95,13 +99,23 @@ export class FunnelMcp {
|
|
|
95
99
|
|
|
96
100
|
if (!content) return {}
|
|
97
101
|
|
|
102
|
+
let parsed: unknown
|
|
103
|
+
|
|
98
104
|
try {
|
|
99
|
-
|
|
105
|
+
parsed = JSON.parse(content)
|
|
100
106
|
} catch (error) {
|
|
101
107
|
throw new Error(
|
|
102
108
|
`invalid .mcp.json (${mcpPath}): ${error instanceof Error ? error.message : String(error)}`,
|
|
103
109
|
)
|
|
104
110
|
}
|
|
111
|
+
|
|
112
|
+
const result = mcpConfigSchema.safeParse(parsed)
|
|
113
|
+
|
|
114
|
+
if (!result.success) {
|
|
115
|
+
throw new Error(`invalid .mcp.json (${mcpPath}): ${result.error.message}`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result.data
|
|
105
119
|
}
|
|
106
120
|
|
|
107
121
|
private writeConfig(repoPath: string, config: McpConfig): void {
|
package/lib/{modules/process/node-funnel-process-runner.ts → engine/process/node-process-runner.ts}
RENAMED
|
@@ -4,12 +4,22 @@ import {
|
|
|
4
4
|
FunnelProcessRunner,
|
|
5
5
|
type RunOptions,
|
|
6
6
|
type RunResult,
|
|
7
|
-
} from "@/
|
|
7
|
+
} from "@/engine/process/process-runner"
|
|
8
8
|
|
|
9
9
|
const toEnv = (env?: Record<string, string>): Record<string, string> | undefined => {
|
|
10
10
|
if (!env) return undefined
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
const merged: Record<string, string> = {}
|
|
13
|
+
|
|
14
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
15
|
+
if (typeof value === "string") merged[key] = value
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
for (const [key, value] of Object.entries(env)) {
|
|
19
|
+
merged[key] = value
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return merged
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
export class NodeFunnelProcessRunner extends FunnelProcessRunner {
|
|
@@ -59,25 +69,6 @@ export class NodeFunnelProcessRunner extends FunnelProcessRunner {
|
|
|
59
69
|
stdio: ["inherit", "inherit", "inherit"],
|
|
60
70
|
})
|
|
61
71
|
|
|
62
|
-
const forward = (signal: "SIGINT" | "SIGTERM") => {
|
|
63
|
-
try {
|
|
64
|
-
proc.kill(signal)
|
|
65
|
-
} catch {
|
|
66
|
-
// ignore
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
setTimeout(() => {
|
|
70
|
-
try {
|
|
71
|
-
proc.kill("SIGKILL")
|
|
72
|
-
} catch {
|
|
73
|
-
// ignore
|
|
74
|
-
}
|
|
75
|
-
}, 3000).unref()
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
process.on("SIGINT", () => forward("SIGINT"))
|
|
79
|
-
process.on("SIGTERM", () => forward("SIGTERM"))
|
|
80
|
-
|
|
81
72
|
return await proc.exited
|
|
82
73
|
}
|
|
83
74
|
|