@johpaz/hive 1.7.2 → 1.7.3
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 +178 -36
- package/dist/hive.js +315124 -0
- package/package.json +11 -3
- package/packages/core/src/index.ts +0 -1
- package/.dockerignore +0 -9
- package/CONTRIBUTING.md +0 -44
- package/Dockerfile +0 -67
- package/docker-compose.yml +0 -19
- package/packages/cli/package.json +0 -28
- package/packages/cli/src/commands/agent-run.ts +0 -168
- package/packages/cli/src/commands/agents.ts +0 -398
- package/packages/cli/src/commands/chat.ts +0 -142
- package/packages/cli/src/commands/config.ts +0 -50
- package/packages/cli/src/commands/cron.ts +0 -161
- package/packages/cli/src/commands/dev.ts +0 -95
- package/packages/cli/src/commands/doctor.ts +0 -133
- package/packages/cli/src/commands/gateway.ts +0 -422
- package/packages/cli/src/commands/logs.ts +0 -57
- package/packages/cli/src/commands/mcp.ts +0 -175
- package/packages/cli/src/commands/message.ts +0 -77
- package/packages/cli/src/commands/onboard.ts +0 -1696
- package/packages/cli/src/commands/security.ts +0 -144
- package/packages/cli/src/commands/service.ts +0 -50
- package/packages/cli/src/commands/sessions.ts +0 -116
- package/packages/cli/src/commands/skills.ts +0 -187
- package/packages/cli/src/commands/update.ts +0 -25
- package/packages/cli/src/index.ts +0 -190
- package/packages/cli/src/utils/token.ts +0 -6
- package/packages/code-bridge/README.md +0 -78
- package/packages/code-bridge/package.json +0 -18
- package/packages/code-bridge/src/index.ts +0 -95
- package/packages/code-bridge/src/process-manager.ts +0 -212
- package/packages/code-bridge/src/schemas.ts +0 -133
- package/packages/core/package.json +0 -55
- package/packages/core/src/agent/agent-loop.ts +0 -520
- package/packages/core/src/agent/compaction.ts +0 -183
- package/packages/core/src/agent/context-compiler.ts +0 -544
- package/packages/core/src/agent/context-guard.ts +0 -91
- package/packages/core/src/agent/conversation-store.ts +0 -193
- package/packages/core/src/agent/curator.ts +0 -158
- package/packages/core/src/agent/hooks.ts +0 -166
- package/packages/core/src/agent/llm-client.ts +0 -503
- package/packages/core/src/agent/native-tools.ts +0 -31
- package/packages/core/src/agent/playbook-selector.ts +0 -143
- package/packages/core/src/agent/prompt-builder.ts +0 -167
- package/packages/core/src/agent/providers/index.ts +0 -186
- package/packages/core/src/agent/providers.ts +0 -1
- package/packages/core/src/agent/reflector.ts +0 -200
- package/packages/core/src/agent/service.ts +0 -266
- package/packages/core/src/agent/skill-selector.ts +0 -413
- package/packages/core/src/agent/stuck-loop.ts +0 -133
- package/packages/core/src/agent/tool-selector.ts +0 -623
- package/packages/core/src/agent/tracer.ts +0 -102
- package/packages/core/src/canvas/canvas-manager.ts +0 -319
- package/packages/core/src/canvas/canvas-tools.ts +0 -420
- package/packages/core/src/canvas/emitter.ts +0 -119
- package/packages/core/src/canvas/index.ts +0 -2
- package/packages/core/src/channels/base.ts +0 -140
- package/packages/core/src/channels/discord.ts +0 -260
- package/packages/core/src/channels/index.ts +0 -7
- package/packages/core/src/channels/manager.ts +0 -383
- package/packages/core/src/channels/slack.ts +0 -287
- package/packages/core/src/channels/telegram.ts +0 -552
- package/packages/core/src/channels/webchat.ts +0 -139
- package/packages/core/src/channels/whatsapp.ts +0 -375
- package/packages/core/src/config/index.ts +0 -12
- package/packages/core/src/config/loader.ts +0 -529
- package/packages/core/src/events/agent-bus.ts +0 -460
- package/packages/core/src/events/event-bus.ts +0 -169
- package/packages/core/src/gateway/helpers/cors.ts +0 -32
- package/packages/core/src/gateway/helpers/index.ts +0 -4
- package/packages/core/src/gateway/helpers/narration.ts +0 -60
- package/packages/core/src/gateway/helpers/path.ts +0 -13
- package/packages/core/src/gateway/helpers/redact.ts +0 -61
- package/packages/core/src/gateway/index.ts +0 -5
- package/packages/core/src/gateway/initializer.ts +0 -332
- package/packages/core/src/gateway/lane-queue.ts +0 -169
- package/packages/core/src/gateway/resolver.ts +0 -108
- package/packages/core/src/gateway/router.ts +0 -124
- package/packages/core/src/gateway/routes/agents.ts +0 -187
- package/packages/core/src/gateway/routes/channels.ts +0 -203
- package/packages/core/src/gateway/routes/chat.ts +0 -241
- package/packages/core/src/gateway/routes/config.ts +0 -12
- package/packages/core/src/gateway/routes/cron.ts +0 -42
- package/packages/core/src/gateway/routes/ethics.ts +0 -46
- package/packages/core/src/gateway/routes/mcp.ts +0 -346
- package/packages/core/src/gateway/routes/models.ts +0 -93
- package/packages/core/src/gateway/routes/projects.ts +0 -179
- package/packages/core/src/gateway/routes/providers.ts +0 -192
- package/packages/core/src/gateway/routes/setup.ts +0 -267
- package/packages/core/src/gateway/routes/skills.ts +0 -70
- package/packages/core/src/gateway/routes/system.ts +0 -165
- package/packages/core/src/gateway/routes/tasks.ts +0 -44
- package/packages/core/src/gateway/routes/tools.ts +0 -35
- package/packages/core/src/gateway/routes/users.ts +0 -118
- package/packages/core/src/gateway/routes/voice.ts +0 -73
- package/packages/core/src/gateway/routes/workspace.ts +0 -281
- package/packages/core/src/gateway/server.ts +0 -1978
- package/packages/core/src/gateway/session.ts +0 -95
- package/packages/core/src/gateway/slash-commands.ts +0 -193
- package/packages/core/src/heartbeat/index.ts +0 -157
- package/packages/core/src/mcp/hot-reload.ts +0 -213
- package/packages/core/src/mcp/singleton.ts +0 -21
- package/packages/core/src/memory/index.ts +0 -1
- package/packages/core/src/memory/notes.ts +0 -68
- package/packages/core/src/plugins/api.ts +0 -128
- package/packages/core/src/plugins/index.ts +0 -2
- package/packages/core/src/plugins/loader.ts +0 -365
- package/packages/core/src/resilience/circuit-breaker.ts +0 -225
- package/packages/core/src/security/google-chat.ts +0 -269
- package/packages/core/src/security/index.ts +0 -192
- package/packages/core/src/security/pairing.ts +0 -250
- package/packages/core/src/security/rate-limit.ts +0 -270
- package/packages/core/src/security/signal.ts +0 -321
- package/packages/core/src/state/store.ts +0 -312
- package/packages/core/src/storage/crypto.ts +0 -101
- package/packages/core/src/storage/onboarding.ts +0 -1609
- package/packages/core/src/storage/schema.ts +0 -567
- package/packages/core/src/storage/seed.ts +0 -608
- package/packages/core/src/storage/sqlite.ts +0 -363
- package/packages/core/src/storage/usage.ts +0 -270
- package/packages/core/src/tools/agents/index.ts +0 -607
- package/packages/core/src/tools/bridge-events.ts +0 -26
- package/packages/core/src/tools/canvas/index.ts +0 -281
- package/packages/core/src/tools/cli/index.ts +0 -142
- package/packages/core/src/tools/codebridge/index.ts +0 -179
- package/packages/core/src/tools/core/index.ts +0 -257
- package/packages/core/src/tools/cron/index.ts +0 -373
- package/packages/core/src/tools/filesystem/fs-delete.ts +0 -78
- package/packages/core/src/tools/filesystem/fs-edit.ts +0 -106
- package/packages/core/src/tools/filesystem/fs-exists.ts +0 -63
- package/packages/core/src/tools/filesystem/fs-glob.ts +0 -108
- package/packages/core/src/tools/filesystem/fs-list.ts +0 -129
- package/packages/core/src/tools/filesystem/fs-read.ts +0 -72
- package/packages/core/src/tools/filesystem/fs-write.ts +0 -67
- package/packages/core/src/tools/filesystem/index.ts +0 -34
- package/packages/core/src/tools/filesystem/workspace-guard.ts +0 -62
- package/packages/core/src/tools/index.ts +0 -197
- package/packages/core/src/tools/projects/index.ts +0 -37
- package/packages/core/src/tools/projects/project-create.ts +0 -94
- package/packages/core/src/tools/projects/project-done.ts +0 -66
- package/packages/core/src/tools/projects/project-fail.ts +0 -66
- package/packages/core/src/tools/projects/project-list.ts +0 -96
- package/packages/core/src/tools/projects/project-update.ts +0 -72
- package/packages/core/src/tools/projects/task-create.ts +0 -68
- package/packages/core/src/tools/projects/task-evaluate.ts +0 -93
- package/packages/core/src/tools/projects/task-update.ts +0 -93
- package/packages/core/src/tools/search-knowledge/search-knowledge.ts +0 -155
- package/packages/core/src/tools/types.ts +0 -39
- package/packages/core/src/tools/voice/index.ts +0 -104
- package/packages/core/src/tools/web/browser-click.ts +0 -54
- package/packages/core/src/tools/web/browser-navigate.ts +0 -84
- package/packages/core/src/tools/web/browser-screenshot.ts +0 -54
- package/packages/core/src/tools/web/browser-type.ts +0 -60
- package/packages/core/src/tools/web/index.ts +0 -31
- package/packages/core/src/tools/web/web-fetch.ts +0 -78
- package/packages/core/src/tools/web/web-search.ts +0 -123
- package/packages/core/src/utils/benchmark.ts +0 -80
- package/packages/core/src/utils/crypto.ts +0 -73
- package/packages/core/src/utils/date.ts +0 -42
- package/packages/core/src/utils/index.ts +0 -5
- package/packages/core/src/utils/logger.ts +0 -389
- package/packages/core/src/utils/retry.ts +0 -70
- package/packages/core/src/utils/toon.ts +0 -356
- package/packages/core/src/voice/index.ts +0 -583
- package/packages/hive-ui/README.md +0 -52
- package/packages/hive-ui/components.json +0 -20
- package/packages/hive-ui/index.html +0 -30
- package/packages/hive-ui/package.json +0 -90
- package/packages/hive-ui/public/favicon.ico +0 -0
- package/packages/hive-ui/public/placeholder.svg +0 -1
- package/packages/hive-ui/src/App.tsx +0 -115
- package/packages/hive-ui/src/components/CronJobsPanel.tsx +0 -200
- package/packages/hive-ui/src/components/NavLink.tsx +0 -34
- package/packages/hive-ui/src/components/NotesPanel.tsx +0 -79
- package/packages/hive-ui/src/components/SystemMonitor.tsx +0 -270
- package/packages/hive-ui/src/components/UsageStatsPanel.tsx +0 -334
- package/packages/hive-ui/src/components/WelcomeDialog.tsx +0 -279
- package/packages/hive-ui/src/components/ui/accordion.tsx +0 -52
- package/packages/hive-ui/src/components/ui/alert-dialog.tsx +0 -104
- package/packages/hive-ui/src/components/ui/alert.tsx +0 -45
- package/packages/hive-ui/src/components/ui/aspect-ratio.tsx +0 -5
- package/packages/hive-ui/src/components/ui/avatar.tsx +0 -38
- package/packages/hive-ui/src/components/ui/badge.tsx +0 -29
- package/packages/hive-ui/src/components/ui/bee-loader.tsx +0 -68
- package/packages/hive-ui/src/components/ui/breadcrumb.tsx +0 -90
- package/packages/hive-ui/src/components/ui/button.tsx +0 -47
- package/packages/hive-ui/src/components/ui/calendar.tsx +0 -54
- package/packages/hive-ui/src/components/ui/card.tsx +0 -45
- package/packages/hive-ui/src/components/ui/carousel.tsx +0 -224
- package/packages/hive-ui/src/components/ui/chart.tsx +0 -303
- package/packages/hive-ui/src/components/ui/checkbox.tsx +0 -26
- package/packages/hive-ui/src/components/ui/collapsible.tsx +0 -9
- package/packages/hive-ui/src/components/ui/command.tsx +0 -133
- package/packages/hive-ui/src/components/ui/context-menu.tsx +0 -178
- package/packages/hive-ui/src/components/ui/dialog.tsx +0 -95
- package/packages/hive-ui/src/components/ui/drawer.tsx +0 -87
- package/packages/hive-ui/src/components/ui/dropdown-menu.tsx +0 -179
- package/packages/hive-ui/src/components/ui/form.tsx +0 -129
- package/packages/hive-ui/src/components/ui/hover-card.tsx +0 -27
- package/packages/hive-ui/src/components/ui/input-otp.tsx +0 -61
- package/packages/hive-ui/src/components/ui/input.tsx +0 -22
- package/packages/hive-ui/src/components/ui/label.tsx +0 -17
- package/packages/hive-ui/src/components/ui/menubar.tsx +0 -207
- package/packages/hive-ui/src/components/ui/navigation-menu.tsx +0 -120
- package/packages/hive-ui/src/components/ui/pagination.tsx +0 -80
- package/packages/hive-ui/src/components/ui/popover.tsx +0 -29
- package/packages/hive-ui/src/components/ui/progress.tsx +0 -23
- package/packages/hive-ui/src/components/ui/radio-group.tsx +0 -36
- package/packages/hive-ui/src/components/ui/resizable.tsx +0 -37
- package/packages/hive-ui/src/components/ui/scroll-area.tsx +0 -38
- package/packages/hive-ui/src/components/ui/select.tsx +0 -143
- package/packages/hive-ui/src/components/ui/separator.tsx +0 -20
- package/packages/hive-ui/src/components/ui/sheet.tsx +0 -107
- package/packages/hive-ui/src/components/ui/sidebar.tsx +0 -636
- package/packages/hive-ui/src/components/ui/skeleton.tsx +0 -7
- package/packages/hive-ui/src/components/ui/slider.tsx +0 -23
- package/packages/hive-ui/src/components/ui/sonner.tsx +0 -27
- package/packages/hive-ui/src/components/ui/switch.tsx +0 -27
- package/packages/hive-ui/src/components/ui/table.tsx +0 -72
- package/packages/hive-ui/src/components/ui/tabs.tsx +0 -53
- package/packages/hive-ui/src/components/ui/textarea.tsx +0 -21
- package/packages/hive-ui/src/components/ui/toast.tsx +0 -111
- package/packages/hive-ui/src/components/ui/toaster.tsx +0 -24
- package/packages/hive-ui/src/components/ui/toggle-group.tsx +0 -49
- package/packages/hive-ui/src/components/ui/toggle.tsx +0 -37
- package/packages/hive-ui/src/components/ui/tooltip.tsx +0 -28
- package/packages/hive-ui/src/components/ui/use-toast.ts +0 -3
- package/packages/hive-ui/src/hooks/use-mobile.tsx +0 -19
- package/packages/hive-ui/src/hooks/use-toast.ts +0 -186
- package/packages/hive-ui/src/hooks/useAgentConfig.ts +0 -25
- package/packages/hive-ui/src/hooks/useAgents.ts +0 -38
- package/packages/hive-ui/src/hooks/useBridge.ts +0 -38
- package/packages/hive-ui/src/hooks/useCanvas.ts +0 -24
- package/packages/hive-ui/src/hooks/useChannels.ts +0 -2
- package/packages/hive-ui/src/hooks/useEthics.ts +0 -51
- package/packages/hive-ui/src/hooks/useProviders.ts +0 -14
- package/packages/hive-ui/src/hooks/useTheme.ts +0 -29
- package/packages/hive-ui/src/hooks/useUserConfig.ts +0 -17
- package/packages/hive-ui/src/hooks/useWebSocket.ts +0 -12
- package/packages/hive-ui/src/index.css +0 -620
- package/packages/hive-ui/src/lib/api.ts +0 -100
- package/packages/hive-ui/src/lib/constants.ts +0 -6
- package/packages/hive-ui/src/lib/models.ts +0 -64
- package/packages/hive-ui/src/lib/swal.ts +0 -30
- package/packages/hive-ui/src/lib/utils.ts +0 -6
- package/packages/hive-ui/src/lib/websocket.ts +0 -7
- package/packages/hive-ui/src/main.tsx +0 -5
- package/packages/hive-ui/src/modules/agent-config/details/AgentDetailsEditor.tsx +0 -524
- package/packages/hive-ui/src/modules/agent-config/ethics/EthicsConflictDetector.tsx +0 -18
- package/packages/hive-ui/src/modules/agent-config/ethics/EthicsEditor.tsx +0 -19
- package/packages/hive-ui/src/modules/agent-config/ethics/EthicsRulesList.tsx +0 -36
- package/packages/hive-ui/src/modules/agent-config/ethics/EthicsTemplateGallery.tsx +0 -361
- package/packages/hive-ui/src/modules/agent-config/ethics/index.ts +0 -4
- package/packages/hive-ui/src/modules/agent-config/index.ts +0 -6
- package/packages/hive-ui/src/modules/agent-config/mcp/MCPServerAdd.tsx +0 -322
- package/packages/hive-ui/src/modules/agent-config/mcp/MCPServerCard.tsx +0 -93
- package/packages/hive-ui/src/modules/agent-config/mcp/MCPServerConfig.tsx +0 -427
- package/packages/hive-ui/src/modules/agent-config/mcp/MCPServerList.tsx +0 -85
- package/packages/hive-ui/src/modules/agent-config/mcp/MCPToolExplorer.tsx +0 -79
- package/packages/hive-ui/src/modules/agent-config/mcp/index.ts +0 -5
- package/packages/hive-ui/src/modules/agent-config/shared/ConfigEditorLayout.tsx +0 -30
- package/packages/hive-ui/src/modules/agent-config/shared/ConfigExporter.tsx +0 -26
- package/packages/hive-ui/src/modules/agent-config/shared/ConfigImporter.tsx +0 -25
- package/packages/hive-ui/src/modules/agent-config/shared/DiffViewer.tsx +0 -31
- package/packages/hive-ui/src/modules/agent-config/shared/MarkdownEditor.tsx +0 -32
- package/packages/hive-ui/src/modules/agent-config/shared/SaveStatusIndicator.tsx +0 -23
- package/packages/hive-ui/src/modules/agent-config/shared/ValidationPanel.tsx +0 -36
- package/packages/hive-ui/src/modules/agent-config/shared/index.ts +0 -7
- package/packages/hive-ui/src/modules/agent-config/skills/SkillCard.tsx +0 -81
- package/packages/hive-ui/src/modules/agent-config/skills/SkillConfigEditor.tsx +0 -22
- package/packages/hive-ui/src/modules/agent-config/skills/SkillCreator.tsx +0 -60
- package/packages/hive-ui/src/modules/agent-config/skills/SkillInstaller.tsx +0 -23
- package/packages/hive-ui/src/modules/agent-config/skills/SkillList.tsx +0 -72
- package/packages/hive-ui/src/modules/agent-config/skills/SkillsTab.tsx +0 -202
- package/packages/hive-ui/src/modules/agent-config/skills/index.ts +0 -5
- package/packages/hive-ui/src/modules/agent-config/tools/ToolCard.tsx +0 -27
- package/packages/hive-ui/src/modules/agent-config/tools/ToolConfigPanel.tsx +0 -22
- package/packages/hive-ui/src/modules/agent-config/tools/ToolManager.tsx +0 -266
- package/packages/hive-ui/src/modules/agent-config/tools/ToolPermissions.tsx +0 -287
- package/packages/hive-ui/src/modules/agent-config/tools/ToolRegistry.tsx +0 -84
- package/packages/hive-ui/src/modules/agent-config/tools/ToolUsageStats.tsx +0 -52
- package/packages/hive-ui/src/modules/agent-config/tools/index.ts +0 -4
- package/packages/hive-ui/src/modules/agent-config/user/ActiveAgentsList.tsx +0 -109
- package/packages/hive-ui/src/modules/agent-config/user/GlobalConfigOverview.tsx +0 -119
- package/packages/hive-ui/src/modules/agent-config/user/UserMemoryManager.tsx +0 -54
- package/packages/hive-ui/src/modules/agent-config/user/UserPreferencesForm.tsx +0 -163
- package/packages/hive-ui/src/modules/agent-config/user/UserProfileEditor.tsx +0 -261
- package/packages/hive-ui/src/modules/agent-config/user/index.ts +0 -3
- package/packages/hive-ui/src/modules/agents/AgentActivityLog.tsx +0 -25
- package/packages/hive-ui/src/modules/agents/AgentCard.tsx +0 -305
- package/packages/hive-ui/src/modules/agents/AgentCreateForm.tsx +0 -446
- package/packages/hive-ui/src/modules/agents/AgentDetail.tsx +0 -28
- package/packages/hive-ui/src/modules/agents/AgentInternalCard.tsx +0 -162
- package/packages/hive-ui/src/modules/agents/AgentList.tsx +0 -29
- package/packages/hive-ui/src/modules/agents/AgentStatusBadge.tsx +0 -34
- package/packages/hive-ui/src/modules/agents/ModelSelector.tsx +0 -151
- package/packages/hive-ui/src/modules/bridge/BridgeLogViewer.tsx +0 -61
- package/packages/hive-ui/src/modules/bridge/BridgeProcessList.tsx +0 -77
- package/packages/hive-ui/src/modules/bridge/BridgeStatus.tsx +0 -23
- package/packages/hive-ui/src/modules/bridge/BridgeTerminal.tsx +0 -7
- package/packages/hive-ui/src/modules/canvas/CanvasButton.tsx +0 -3
- package/packages/hive-ui/src/modules/canvas/CanvasChart.tsx +0 -3
- package/packages/hive-ui/src/modules/canvas/CanvasComponentMap.tsx +0 -605
- package/packages/hive-ui/src/modules/canvas/CanvasContainer.tsx +0 -360
- package/packages/hive-ui/src/modules/canvas/CanvasForm.tsx +0 -3
- package/packages/hive-ui/src/modules/canvas/CanvasMarkdown.tsx +0 -3
- package/packages/hive-ui/src/modules/canvas/CanvasTable.tsx +0 -3
- package/packages/hive-ui/src/modules/canvas/ComponentRenderer.tsx +0 -30
- package/packages/hive-ui/src/modules/canvas/DynamicRenderer.tsx +0 -3
- package/packages/hive-ui/src/modules/channels/available/AvailableChannelsGrid.tsx +0 -89
- package/packages/hive-ui/src/modules/channels/available/ChannelAuthForm.tsx +0 -33
- package/packages/hive-ui/src/modules/channels/available/ChannelSetupWizard.tsx +0 -48
- package/packages/hive-ui/src/modules/channels/available/ChannelTestConnection.tsx +0 -37
- package/packages/hive-ui/src/modules/channels/available/ChannelTypeCard.tsx +0 -30
- package/packages/hive-ui/src/modules/channels/available/ChannelWebhookConfig.tsx +0 -30
- package/packages/hive-ui/src/modules/channels/available/index.ts +0 -6
- package/packages/hive-ui/src/modules/channels/connected/ChannelCard.tsx +0 -95
- package/packages/hive-ui/src/modules/channels/connected/ChannelConfigPanel.tsx +0 -260
- package/packages/hive-ui/src/modules/channels/connected/ChannelDisconnectButton.tsx +0 -21
- package/packages/hive-ui/src/modules/channels/connected/ChannelLogsViewer.tsx +0 -42
- package/packages/hive-ui/src/modules/channels/connected/ChannelQRCode.tsx +0 -32
- package/packages/hive-ui/src/modules/channels/connected/ChannelReconnectButton.tsx +0 -16
- package/packages/hive-ui/src/modules/channels/connected/ChannelStatusBadge.tsx +0 -26
- package/packages/hive-ui/src/modules/channels/connected/ConnectedChannelsList.tsx +0 -40
- package/packages/hive-ui/src/modules/channels/connected/index.ts +0 -8
- package/packages/hive-ui/src/modules/channels/shared/ChannelCard.tsx +0 -84
- package/packages/hive-ui/src/modules/channels/shared/ChannelConfigDialog.tsx +0 -279
- package/packages/hive-ui/src/modules/channels/shared/ChannelIcon.tsx +0 -40
- package/packages/hive-ui/src/modules/channels/shared/ChannelStats.tsx +0 -37
- package/packages/hive-ui/src/modules/channels/shared/ChannelTypeBadge.tsx +0 -23
- package/packages/hive-ui/src/modules/channels/shared/ConnectionHealthIndicator.tsx +0 -20
- package/packages/hive-ui/src/modules/channels/shared/MessagePreview.tsx +0 -19
- package/packages/hive-ui/src/modules/channels/shared/index.ts +0 -5
- package/packages/hive-ui/src/modules/chat/ChatContainer.tsx +0 -268
- package/packages/hive-ui/src/modules/chat/ChatHistory.tsx +0 -101
- package/packages/hive-ui/src/modules/chat/ChatInput.tsx +0 -108
- package/packages/hive-ui/src/modules/chat/ChatMessage.tsx +0 -137
- package/packages/hive-ui/src/modules/chat/ThinkingIndicator.tsx +0 -10
- package/packages/hive-ui/src/modules/layout/AppLayout.tsx +0 -45
- package/packages/hive-ui/src/modules/layout/ConnectionStatus.tsx +0 -19
- package/packages/hive-ui/src/modules/layout/Header.tsx +0 -20
- package/packages/hive-ui/src/modules/layout/HiveSidebar.tsx +0 -173
- package/packages/hive-ui/src/modules/layout/ThemeToggle.tsx +0 -18
- package/packages/hive-ui/src/modules/providers/ProviderCard.tsx +0 -319
- package/packages/hive-ui/src/modules/providers/ProviderConfigForm.tsx +0 -146
- package/packages/hive-ui/src/modules/providers/ProviderFailoverConfig.tsx +0 -110
- package/packages/hive-ui/src/modules/providers/ProviderList.tsx +0 -33
- package/packages/hive-ui/src/modules/providers/ProviderStatusIndicator.tsx +0 -23
- package/packages/hive-ui/src/modules/providers/configs/ProviderAPIKeyManager.tsx +0 -39
- package/packages/hive-ui/src/modules/providers/configs/ProviderEndpointConfig.tsx +0 -27
- package/packages/hive-ui/src/modules/providers/configs/ProviderRateLimits.tsx +0 -37
- package/packages/hive-ui/src/modules/providers/configs/ProviderRetryPolicy.tsx +0 -46
- package/packages/hive-ui/src/modules/providers/configs/index.ts +0 -4
- package/packages/hive-ui/src/modules/providers/index.ts +0 -5
- package/packages/hive-ui/src/modules/providers/models/ModelBenchmarkBadge.tsx +0 -21
- package/packages/hive-ui/src/modules/providers/models/ModelCapabilities.tsx +0 -44
- package/packages/hive-ui/src/modules/providers/models/ModelCard.tsx +0 -36
- package/packages/hive-ui/src/modules/providers/models/ModelComparisonTable.tsx +0 -47
- package/packages/hive-ui/src/modules/providers/models/ModelList.tsx +0 -51
- package/packages/hive-ui/src/modules/providers/models/ModelPricingInfo.tsx +0 -17
- package/packages/hive-ui/src/modules/providers/models/ModelSelector.tsx +0 -32
- package/packages/hive-ui/src/modules/providers/models/index.ts +0 -7
- package/packages/hive-ui/src/pages/AgentDetailPage.tsx +0 -74
- package/packages/hive-ui/src/pages/AgentNewPage.tsx +0 -5
- package/packages/hive-ui/src/pages/AgentsPage.tsx +0 -147
- package/packages/hive-ui/src/pages/BridgePage.tsx +0 -83
- package/packages/hive-ui/src/pages/CanvasPage.tsx +0 -32
- package/packages/hive-ui/src/pages/ChannelsPage.tsx +0 -176
- package/packages/hive-ui/src/pages/DashboardPage.tsx +0 -321
- package/packages/hive-ui/src/pages/Index.tsx +0 -14
- package/packages/hive-ui/src/pages/LogsPage.tsx +0 -252
- package/packages/hive-ui/src/pages/NotFound.tsx +0 -24
- package/packages/hive-ui/src/pages/ProjectsPage.tsx +0 -241
- package/packages/hive-ui/src/pages/ProvidersPage.tsx +0 -111
- package/packages/hive-ui/src/pages/SettingsPage.tsx +0 -147
- package/packages/hive-ui/src/pages/SetupPage.tsx +0 -1177
- package/packages/hive-ui/src/pages/WebChatPage.tsx +0 -15
- package/packages/hive-ui/src/stores/agentConfigStore.ts +0 -32
- package/packages/hive-ui/src/stores/agentStore.ts +0 -5
- package/packages/hive-ui/src/stores/bridgeStore.ts +0 -237
- package/packages/hive-ui/src/stores/canvasStore.ts +0 -250
- package/packages/hive-ui/src/stores/channelStore.ts +0 -5
- package/packages/hive-ui/src/stores/chatStore.ts +0 -42
- package/packages/hive-ui/src/stores/ethicsStore.ts +0 -141
- package/packages/hive-ui/src/stores/mcpStore.ts +0 -5
- package/packages/hive-ui/src/stores/modelStore.ts +0 -2
- package/packages/hive-ui/src/stores/projectsStore.ts +0 -141
- package/packages/hive-ui/src/stores/providerStore.ts +0 -2
- package/packages/hive-ui/src/stores/skillStore.ts +0 -5
- package/packages/hive-ui/src/stores/toolStore.ts +0 -5
- package/packages/hive-ui/src/stores/useGlobalConfigStore.ts +0 -937
- package/packages/hive-ui/src/stores/useLoaderStore.ts +0 -21
- package/packages/hive-ui/src/stores/useNotesAndCronsStore.ts +0 -144
- package/packages/hive-ui/src/stores/useWebSocketStore.ts +0 -152
- package/packages/hive-ui/src/stores/useWelcomeStore.ts +0 -37
- package/packages/hive-ui/src/stores/userConfigStore.ts +0 -23
- package/packages/hive-ui/src/stores/userStore.ts +0 -82
- package/packages/hive-ui/src/test/setup.ts +0 -15
- package/packages/hive-ui/src/types/agent-config.ts +0 -33
- package/packages/hive-ui/src/types/agent.ts +0 -65
- package/packages/hive-ui/src/types/bridge.ts +0 -27
- package/packages/hive-ui/src/types/canvas.ts +0 -76
- package/packages/hive-ui/src/types/channels.ts +0 -109
- package/packages/hive-ui/src/types/chat.ts +0 -25
- package/packages/hive-ui/src/types/connections.ts +0 -17
- package/packages/hive-ui/src/types/ethics.ts +0 -41
- package/packages/hive-ui/src/types/index.ts +0 -15
- package/packages/hive-ui/src/types/mcp.ts +0 -36
- package/packages/hive-ui/src/types/notes-crons.ts +0 -31
- package/packages/hive-ui/src/types/providers.ts +0 -145
- package/packages/hive-ui/src/types/skill.ts +0 -12
- package/packages/hive-ui/src/types/tool.ts +0 -44
- package/packages/hive-ui/src/types/user.ts +0 -26
- package/packages/hive-ui/src/types/websocket.ts +0 -14
- package/packages/hive-ui/src/vite-env.d.ts +0 -1
- package/packages/mcp/package.json +0 -26
- package/packages/mcp/src/config.ts +0 -13
- package/packages/mcp/src/index.ts +0 -1
- package/packages/mcp/src/logger.ts +0 -42
- package/packages/mcp/src/manager.ts +0 -439
- package/packages/mcp/src/transports/index.ts +0 -67
- package/packages/mcp/src/transports/sse.ts +0 -241
- package/packages/mcp/src/transports/websocket.ts +0 -159
- package/packages/skills/package.json +0 -21
- package/packages/skills/src/index.ts +0 -1
- package/packages/skills/src/loader.ts +0 -346
|
@@ -1,1978 +0,0 @@
|
|
|
1
|
-
import type { Config } from "../config/loader";
|
|
2
|
-
import { loadConfig, getHiveDir } from "../config/loader";
|
|
3
|
-
import { logger, onLogEntry } from "../utils/logger";
|
|
4
|
-
import { sessionManager, parseSessionId } from "./session";
|
|
5
|
-
import { laneQueue } from "./lane-queue";
|
|
6
|
-
import {
|
|
7
|
-
type InboundMessage,
|
|
8
|
-
type OutboundMessage,
|
|
9
|
-
isSlashCommand,
|
|
10
|
-
executeSlashCommand,
|
|
11
|
-
} from "./slash-commands";
|
|
12
|
-
import { ChannelManager } from "../channels/manager";
|
|
13
|
-
import { AgentService } from "../agent/service";
|
|
14
|
-
import { AgentRunner } from "../agent/providers/index";
|
|
15
|
-
import type { IncomingMessage } from "../channels/base";
|
|
16
|
-
import { mkdirSync, rmSync, unlinkSync, watch, existsSync } from "node:fs";
|
|
17
|
-
import * as path from "node:path";
|
|
18
|
-
import { cpus as osCpus } from "node:os";
|
|
19
|
-
import { getDb, getDbPathLazy, initializeDatabase } from "../storage/sqlite";
|
|
20
|
-
import { getRecentMessages } from "../agent/conversation-store";
|
|
21
|
-
import { canvasManager } from "../canvas/canvas-manager.ts";
|
|
22
|
-
import { subscribeCanvas, unsubscribeCanvas, emitCanvas, getCanvasSnapshot } from "../canvas/emitter";
|
|
23
|
-
import { subscribeBridge, unsubscribeBridge } from "../tools/bridge-events";
|
|
24
|
-
import { randomUUID } from "crypto";
|
|
25
|
-
import { decryptConfig } from "../storage/crypto.ts";
|
|
26
|
-
import { resolveContext } from "./resolver";
|
|
27
|
-
import { getUsageStats } from "../storage/usage";
|
|
28
|
-
import { voiceService } from "../voice/index";
|
|
29
|
-
import { Benchmark } from "../utils/benchmark";
|
|
30
|
-
import { initializeGateway, type GatewayInitializationResult } from "./initializer";
|
|
31
|
-
import { handleSetupStatus, handleVerifyProvider, handleCompleteSetup } from "./routes/setup";
|
|
32
|
-
import { resolveUserId } from "../storage/onboarding";
|
|
33
|
-
import { handleGetAgents, handleCreateAgent, handleUpdateAgent } from "./routes/agents";
|
|
34
|
-
import { handleGetProviders, handleCreateProvider, handleToggleProvider, handleUpdateProvider, handleSyncProviderModels } from "./routes/providers";
|
|
35
|
-
import { handleGetUsers, handleCreateUser, handleUpdateUserSettings, handleGetUserChannels, handleLinkUserChannel } from "./routes/users";
|
|
36
|
-
import { handleGetSkills, handleActivateSkill, handleDeleteSkill, handleCreateSkill } from "./routes/skills";
|
|
37
|
-
import { handleGetEthics, handleActivateEthics, handleDeleteEthics } from "./routes/ethics";
|
|
38
|
-
import { handleGetTools, handleActivateTool } from "./routes/tools";
|
|
39
|
-
import { handleGetProjects, handleGetActiveProject, handleCreateProject, handleUpdateProject, handleGetProjectHistory, handleGetProjectDetail, handleGetProjectTasks } from "./routes/projects";
|
|
40
|
-
import { handleGetTasks, handleUpdateTask } from "./routes/tasks";
|
|
41
|
-
import { handleGetCronJobs, handleGetCronChannels, handleUpdateCronJob } from "./routes/cron";
|
|
42
|
-
import { handleGetChannels, handleGetChannelConfig, handleActivateChannel, handleDeactivateChannel, handleCreateChannel, handleGetChannelAccount, handleUpdateChannelAccount, handleDeleteChannelAccount, handleChannelAction, handleUpdateChannelSettings, handleToggleChannel } from "./routes/channels";
|
|
43
|
-
import { handleGetMcpServers, handleCreateMcpServer, handleUpdateMcpServer, handleDeleteMcpServer, handleToggleMcpServer, handleGetMCPServerTools, handleToggleMCPTool, handleDeleteMCPTool } from "./routes/mcp";
|
|
44
|
-
import { handleGetModels, handleCreateModel, handleToggleModel, handleGetModelsConfig, handleUpdateModelsConfig } from "./routes/models";
|
|
45
|
-
import { handleGetVoiceProviders, handleGetConfiguredVoiceProviders, handleTestVoice, handleGetChannelVoice, handleUpdateChannelVoice } from "./routes/voice";
|
|
46
|
-
import { handleGetActivityStats, handleGetSystemStats, handleGetUsageStats, handleSystemReload, handleApiReload } from "./routes/system";
|
|
47
|
-
import { handleGetChatHistory, handleGetCanvas, handleGetNotes, handleUpdateNote } from "./routes/chat";
|
|
48
|
-
import { handleChat as handlePostChat } from "./routes/chat";
|
|
49
|
-
import { handleGetConfig } from "./routes/config";
|
|
50
|
-
import { handleGetWorkspace, handleUpdateWorkspace, handleValidateWorkspace, handleCreateWorkspace, handleOpenWorkspace } from "./routes/workspace";
|
|
51
|
-
import { getNarration, expandPath, addCorsHeaders, redactConfig, CORS_ORIGINS } from "./helpers";
|
|
52
|
-
import { initCronScheduler, resolveBestChannel } from "../tools/cron";
|
|
53
|
-
|
|
54
|
-
const logSubscribers = new Set<string>();
|
|
55
|
-
|
|
56
|
-
// Helpers imported from ./helpers/index.ts
|
|
57
|
-
// - getNarration, TOOL_NARRATIONS
|
|
58
|
-
// - expandPath
|
|
59
|
-
// - addCorsHeaders, CORS_ORIGINS
|
|
60
|
-
// - redactConfig, redactValue
|
|
61
|
-
|
|
62
|
-
interface WebSocketData {
|
|
63
|
-
sessionId: string;
|
|
64
|
-
authenticatedAt: number;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function startGateway(config: Config): Promise<void> {
|
|
68
|
-
const host = config.gateway?.host ?? "127.0.0.1";
|
|
69
|
-
const port = config.gateway?.port ?? 18790;
|
|
70
|
-
const pidFile = expandPath(config.gateway?.pidFile ?? "~/.hive/gateway.pid");
|
|
71
|
-
|
|
72
|
-
// FIX 2 — startTime para calcular uptime en /status y /api/agents
|
|
73
|
-
const startTime = Date.now();
|
|
74
|
-
|
|
75
|
-
// CPU delta sampling — process.cpuUsage() is cumulative; we diff between calls
|
|
76
|
-
const numCores = osCpus().length || 1;
|
|
77
|
-
let lastCpuSample = process.cpuUsage();
|
|
78
|
-
let lastCpuSampleTime = Date.now();
|
|
79
|
-
const log = logger.child("gateway");
|
|
80
|
-
const mcpLog = logger.child("mcp:api");
|
|
81
|
-
|
|
82
|
-
log.info(`Starting gateway on ${host}:${port}`);
|
|
83
|
-
|
|
84
|
-
// ── Inicialización modular con manejo de errores ──────────────────────────
|
|
85
|
-
let agent: AgentService;
|
|
86
|
-
let runner: AgentRunner;
|
|
87
|
-
let channelManager: ChannelManager;
|
|
88
|
-
let dbProvider: string;
|
|
89
|
-
let dbModel: string;
|
|
90
|
-
const agentList = config.agents?.list ?? [];
|
|
91
|
-
const defaultAgent = agentList.find((a) => a.default) ?? agentList[0];
|
|
92
|
-
const workspacePath = expandPath(defaultAgent?.workspace ?? "~/.hive/workspace");
|
|
93
|
-
|
|
94
|
-
// ── Bind port immediately so parent health-check doesn't timeout ──────────
|
|
95
|
-
// The full handler is loaded via server.reload() once initialization finishes
|
|
96
|
-
let server = Bun.serve<WebSocketData>({
|
|
97
|
-
port,
|
|
98
|
-
hostname: host,
|
|
99
|
-
fetch: (_req) => Response.json({ status: "starting" }),
|
|
100
|
-
websocket: { open() { }, message() { }, close() { } },
|
|
101
|
-
});
|
|
102
|
-
log.info(`Port ${port} bound (initializing gateway...)`);
|
|
103
|
-
|
|
104
|
-
// Setup mode: no DB file OR DB exists but has no users (interrupted first run)
|
|
105
|
-
let gatewaySetupMode = !existsSync(getDbPathLazy());
|
|
106
|
-
if (!gatewaySetupMode) {
|
|
107
|
-
try {
|
|
108
|
-
initializeDatabase();
|
|
109
|
-
const count = (getDb().query("SELECT COUNT(*) as count FROM users").get() as { count: number }).count;
|
|
110
|
-
if (count === 0) gatewaySetupMode = true;
|
|
111
|
-
} catch {
|
|
112
|
-
gatewaySetupMode = true;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
// Usar el inicializador modular para todos los componentes críticos
|
|
118
|
-
const init = await initializeGateway(config, pidFile);
|
|
119
|
-
|
|
120
|
-
agent = init.agent;
|
|
121
|
-
runner = init.runner;
|
|
122
|
-
channelManager = init.channelManager;
|
|
123
|
-
dbProvider = init.provider;
|
|
124
|
-
dbModel = init.model;
|
|
125
|
-
|
|
126
|
-
if (gatewaySetupMode) {
|
|
127
|
-
log.info("🎉 Setup mode: gateway running — open http://localhost:" + port + "/setup to configure");
|
|
128
|
-
} else {
|
|
129
|
-
log.info("✅ Gateway initialization completed successfully");
|
|
130
|
-
// ── Initialize Cron Scheduler ──────────────────────────────────────────
|
|
131
|
-
initCronScheduler((sessionId, task, jobId, context) => {
|
|
132
|
-
agent.emit("cron", sessionId, task, jobId, context);
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
} catch (error) {
|
|
136
|
-
log.error(`❌ Gateway initialization failed: ${(error as Error).message}`);
|
|
137
|
-
log.error("Stack trace:", (error as Error).stack);
|
|
138
|
-
process.exit(1);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Check for insecure binding
|
|
142
|
-
if (host === "0.0.0.0" && config.security?.warnOnInsecureConfig !== false) {
|
|
143
|
-
log.warn("Gateway binding to 0.0.0.0 exposes server to all network interfaces!");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ── CRON Handler setup ─────────────────────────────────────────────────────
|
|
147
|
-
function prepareTools(agentInstance: AgentService, sessionId: string) {
|
|
148
|
-
// Tools are now handled by the native agent-loop internally
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (!gatewaySetupMode) agent.on("cron", async (sessionId: string, task: string, jobId?: string, context?: { fecha_usuario: string; hora_usuario: string }) => {
|
|
153
|
-
log.info(`[CRON] Triggered task '${task}' for session ${sessionId}, jobId: ${jobId || 'unknown'} [${context?.fecha_usuario} ${context?.hora_usuario}]`);
|
|
154
|
-
|
|
155
|
-
let notifyChannel = "webchat";
|
|
156
|
-
let notifySessionId = sessionId;
|
|
157
|
-
let taskMessage = task; // Default to task name if no taskMessage found
|
|
158
|
-
let projectId: string | null = null;
|
|
159
|
-
|
|
160
|
-
if (jobId) {
|
|
161
|
-
try {
|
|
162
|
-
const db = getDb();
|
|
163
|
-
const cronJob = db.query<any, [string]>(
|
|
164
|
-
"SELECT user_id, project_id, notify_channel_id, task_config FROM cron_jobs WHERE id = ?"
|
|
165
|
-
).get(jobId) as any;
|
|
166
|
-
|
|
167
|
-
if (cronJob) {
|
|
168
|
-
projectId = cronJob.project_id || null;
|
|
169
|
-
|
|
170
|
-
// Extract taskMessage from task_config if available
|
|
171
|
-
const taskConfig = cronJob.task_config ? JSON.parse(cronJob.task_config) : {};
|
|
172
|
-
taskMessage = taskConfig.message || task;
|
|
173
|
-
|
|
174
|
-
// If linked to a project, activate it
|
|
175
|
-
if (projectId) {
|
|
176
|
-
const project = db.query<any, [string]>(
|
|
177
|
-
"SELECT id, name, status FROM projects WHERE id = ?"
|
|
178
|
-
).get(projectId);
|
|
179
|
-
if (project) {
|
|
180
|
-
log.info(`[CRON] Activating linked project: ${project.name} (${projectId}) [status: ${project.status} → active]`);
|
|
181
|
-
db.query(
|
|
182
|
-
"UPDATE projects SET status = 'active', updated_at = unixepoch() WHERE id = ?"
|
|
183
|
-
).run(projectId);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Apply priority chain: explicit job channel → registered identity → webchat
|
|
188
|
-
notifyChannel = resolveBestChannel(cronJob.user_id, cronJob.notify_channel_id);
|
|
189
|
-
|
|
190
|
-
if (notifyChannel !== "webchat") {
|
|
191
|
-
// External channel: need the channel-specific session ID from user_identities
|
|
192
|
-
const identity = db.query<{ channel_user_id: string }, [string, string]>(
|
|
193
|
-
"SELECT channel_user_id FROM user_identities WHERE user_id = ? AND channel = ? LIMIT 1"
|
|
194
|
-
).get(cronJob.user_id, notifyChannel);
|
|
195
|
-
if (identity?.channel_user_id) {
|
|
196
|
-
notifySessionId = identity.channel_user_id;
|
|
197
|
-
} else {
|
|
198
|
-
log.warn(`[CRON] No identity found for channel '${notifyChannel}', falling back to webchat`);
|
|
199
|
-
notifyChannel = "webchat";
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (notifyChannel === "webchat") {
|
|
204
|
-
// Use any active WebChat session; fall back to user_id (works if client connects with that ID)
|
|
205
|
-
const webchatChannel = channelManager.getChannel("webchat") as any;
|
|
206
|
-
const activeWsSession = webchatChannel?.getAnyActiveSession?.() ?? cronJob.user_id;
|
|
207
|
-
notifySessionId = activeWsSession;
|
|
208
|
-
log.info(`[CRON] WebChat session resolved: ${notifySessionId}`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
} catch (e) {
|
|
212
|
-
log.warn(`[CRON] Could not fetch notify channel for job ${jobId}: ${(e as Error).message}`);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Use the resolved session for both internal processing and notifications
|
|
217
|
-
const activeSessionId = notifySessionId;
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
await channelManager.send(notifyChannel, activeSessionId, {
|
|
221
|
-
content: `⏰ Ejecutando tarea: ${task}`,
|
|
222
|
-
type: "progress"
|
|
223
|
-
});
|
|
224
|
-
} catch (notifyErr) {
|
|
225
|
-
log.warn(`[CRON] Could not send start notification: ${(notifyErr as Error).message}`);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
laneQueue.enqueue(activeSessionId, async (_t, signal) => {
|
|
229
|
-
if (signal.aborted) return;
|
|
230
|
-
try {
|
|
231
|
-
// Add cron message as 'user' role so it appears in the chat history
|
|
232
|
-
const cronMessage = `[CRON] Scheduled task triggered: ${task}.${projectId ? `\n[project_id: ${projectId}]` : ''}
|
|
233
|
-
[fecha_usuario: ${context?.fecha_usuario || ""}]
|
|
234
|
-
[hora_usuario: ${context?.hora_usuario || ""}]
|
|
235
|
-
Instruction: ${taskMessage}
|
|
236
|
-
Please execute it now.`;
|
|
237
|
-
|
|
238
|
-
const history = getRecentMessages(activeSessionId, 50);
|
|
239
|
-
const messages = [
|
|
240
|
-
...history.map((row) => ({
|
|
241
|
-
role: row.role as "user" | "assistant" | "system",
|
|
242
|
-
content: row.content,
|
|
243
|
-
})),
|
|
244
|
-
{ role: "user" as const, content: cronMessage }
|
|
245
|
-
];
|
|
246
|
-
|
|
247
|
-
const provider = dbProvider;
|
|
248
|
-
|
|
249
|
-
log.info(`[CRON] Generating response for session ${activeSessionId}...`);
|
|
250
|
-
const response = await runner.generate({
|
|
251
|
-
provider: provider as any,
|
|
252
|
-
messages,
|
|
253
|
-
rawUserMessage: cronMessage,
|
|
254
|
-
maxTokens: 4096,
|
|
255
|
-
tools: prepareTools(agent, activeSessionId),
|
|
256
|
-
maxSteps: 15,
|
|
257
|
-
threadId: activeSessionId,
|
|
258
|
-
channel: "cron",
|
|
259
|
-
onStep: async (step) => {
|
|
260
|
-
if (step.type === "text" && step.message) {
|
|
261
|
-
const trimmedMessage = (typeof step.message === "string" ? step.message : "").trim();
|
|
262
|
-
if (trimmedMessage) {
|
|
263
|
-
await channelManager.send(notifyChannel, notifySessionId, {
|
|
264
|
-
content: trimmedMessage,
|
|
265
|
-
type: "progress"
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
if (step.type === "tool_result" && step.message) {
|
|
270
|
-
try {
|
|
271
|
-
const result = JSON.parse(step.message);
|
|
272
|
-
if (result._sendToUser || result.status) {
|
|
273
|
-
const userMessage = result.message || result.status || step.message;
|
|
274
|
-
await channelManager.send(notifyChannel, notifySessionId, {
|
|
275
|
-
content: `📊 ${userMessage}`,
|
|
276
|
-
type: "progress"
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
} catch { }
|
|
280
|
-
}
|
|
281
|
-
},
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
const responseContent = response.content?.trim() || "Task completed.";
|
|
285
|
-
|
|
286
|
-
// For cron tasks, always send the final response to the notification channel
|
|
287
|
-
await channelManager.send(notifyChannel, notifySessionId, { content: responseContent });
|
|
288
|
-
|
|
289
|
-
// Also send to WebSocket if the original session is connected
|
|
290
|
-
const session = sessionManager.get(sessionId);
|
|
291
|
-
if (session?.ws) {
|
|
292
|
-
session.ws.send(JSON.stringify({ type: "message", sessionId: sessionId, content: responseContent } as OutboundMessage));
|
|
293
|
-
}
|
|
294
|
-
} catch (error) {
|
|
295
|
-
log.error(`[CRON] Error for session ${sessionId}: ${(error as Error).message}`);
|
|
296
|
-
await channelManager.send(notifyChannel, notifySessionId, {
|
|
297
|
-
content: `❌ Error en tarea programada: ${(error as Error).message}`,
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// Set up hot reload watchers
|
|
304
|
-
const watchers: Array<() => void> = [];
|
|
305
|
-
|
|
306
|
-
// Note: Context store, Ethics, Agent Loop, LLM runner, and Channel Manager
|
|
307
|
-
// are now initialized by initializeGateway() above
|
|
308
|
-
|
|
309
|
-
// Handle messages from channels (Telegram, Discord, WhatsApp, Slack)
|
|
310
|
-
if (!gatewaySetupMode) channelManager.onMessage(async (message: IncomingMessage) => {
|
|
311
|
-
log.info(`📥 Message from ${message.channel}:${message.accountId}`);
|
|
312
|
-
log.info(` Session: ${message.sessionId}`);
|
|
313
|
-
|
|
314
|
-
const voiceConfig = voiceService.getChannelVoiceConfig(message.channel);
|
|
315
|
-
let messageContent = message.content;
|
|
316
|
-
|
|
317
|
-
let preferAudioResponse = false;
|
|
318
|
-
let inputType: "text" | "audio_transcribed" = "text";
|
|
319
|
-
let sttProviderUsed: string | null = null;
|
|
320
|
-
|
|
321
|
-
if (voiceConfig.voiceEnabled && message.audio) {
|
|
322
|
-
log.info(`🎙️ Voice enabled, processing audio...`);
|
|
323
|
-
|
|
324
|
-
if (!voiceConfig.sttProvider) {
|
|
325
|
-
log.warn(`⚠️ STT provider not configured for channel ${message.channel}`);
|
|
326
|
-
await channelManager.send(message.channel, message.sessionId, {
|
|
327
|
-
content: `🎙️ Para usar notas de voz, necesitas configurar el proveedor STT en la configuración del canal. Ve a Configuración > Canales > [Tu canal] y configura "Prov. STT" (ej: groq-whisper o openai)`,
|
|
328
|
-
});
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
try {
|
|
333
|
-
const audioInput = voiceService.normalizeAudioFromChannel(message.channel, message.audio);
|
|
334
|
-
sttProviderUsed = voiceConfig.sttProvider || "groq-whisper";
|
|
335
|
-
messageContent = await voiceService.transcribe(audioInput, sttProviderUsed);
|
|
336
|
-
log.info(`📝 Transcribed: ${messageContent.substring(0, 100)}...`);
|
|
337
|
-
|
|
338
|
-
inputType = "audio_transcribed";
|
|
339
|
-
// If user sent audio and TTS is available, always respond in audio
|
|
340
|
-
preferAudioResponse = !!voiceConfig.ttsProvider;
|
|
341
|
-
|
|
342
|
-
await channelManager.send(message.channel, message.sessionId, {
|
|
343
|
-
content: `🎙️ Transcripción: ${messageContent}`,
|
|
344
|
-
type: "message"
|
|
345
|
-
});
|
|
346
|
-
} catch (error) {
|
|
347
|
-
log.error(`❌ Transcription failed: ${(error as Error).message}`);
|
|
348
|
-
await channelManager.send(message.channel, message.sessionId, {
|
|
349
|
-
content: `Error al transcribir audio: ${(error as Error).message}`,
|
|
350
|
-
});
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
log.info(` Content: ${messageContent.substring(0, 150)}${messageContent.length > 150 ? "..." : ""}`);
|
|
356
|
-
|
|
357
|
-
const { userId } = resolveContext({
|
|
358
|
-
channel: message.channel,
|
|
359
|
-
channelUserId: message.sessionId,
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
const telegramMeta = message.metadata?.telegram as { messageId?: number } | undefined;
|
|
363
|
-
const messageId = telegramMeta?.messageId?.toString();
|
|
364
|
-
await Promise.all([
|
|
365
|
-
channelManager.markAsRead(message.channel, message.sessionId, messageId),
|
|
366
|
-
channelManager.startTyping(message.channel, message.sessionId),
|
|
367
|
-
]);
|
|
368
|
-
|
|
369
|
-
// unifiedSessionId = userId del onboarding → historial y thread LangGraph unificados
|
|
370
|
-
const unifiedSessionId = userId;
|
|
371
|
-
// routingSessionId = peerId del canal → para enviar respuestas de vuelta al canal correcto
|
|
372
|
-
const routingSessionId = message.sessionId;
|
|
373
|
-
|
|
374
|
-
const userMetadata = inputType === "audio_transcribed"
|
|
375
|
-
? { input_type: "audio_transcribed", stt_provider: sttProviderUsed, channel: message.channel }
|
|
376
|
-
: { input_type: "text", channel: message.channel };
|
|
377
|
-
|
|
378
|
-
// Obtener la zona horaria del usuario para el timestamp exacto
|
|
379
|
-
const userRow = getDb()
|
|
380
|
-
.query<any, [string]>("SELECT * FROM users WHERE id = ?")
|
|
381
|
-
.get(userId);
|
|
382
|
-
const userTimezone = userRow?.timezone || "UTC";
|
|
383
|
-
const now = new Date();
|
|
384
|
-
let exactTime = "";
|
|
385
|
-
try {
|
|
386
|
-
exactTime = now.toLocaleString("en-US", {
|
|
387
|
-
timeZone: userTimezone,
|
|
388
|
-
dateStyle: "full",
|
|
389
|
-
timeStyle: "long",
|
|
390
|
-
});
|
|
391
|
-
} catch (e) {
|
|
392
|
-
exactTime = now.toISOString();
|
|
393
|
-
}
|
|
394
|
-
const messageContentWithTime = `[Timestamp: ${exactTime} (${userTimezone})]\n${messageContent}`;
|
|
395
|
-
|
|
396
|
-
const messages = [{ role: "user" as const, content: messageContentWithTime }];
|
|
397
|
-
|
|
398
|
-
try {
|
|
399
|
-
log.info(`🤖 Routing to agent loop...`);
|
|
400
|
-
|
|
401
|
-
const response = await runner.generate({
|
|
402
|
-
provider: dbProvider as any,
|
|
403
|
-
messages,
|
|
404
|
-
rawUserMessage: messageContent,
|
|
405
|
-
maxTokens: 4096,
|
|
406
|
-
tools: prepareTools(agent, unifiedSessionId),
|
|
407
|
-
maxSteps: 15,
|
|
408
|
-
threadId: unifiedSessionId,
|
|
409
|
-
userId,
|
|
410
|
-
channel: message.channel,
|
|
411
|
-
onStep: async (step) => {
|
|
412
|
-
// "text" = el agente narra lo que está pensando/haciendo antes de un tool_call
|
|
413
|
-
if (step.type === "text" && step.message) {
|
|
414
|
-
const trimmedMessage = (typeof step.message === "string" ? step.message : "").trim();
|
|
415
|
-
if (trimmedMessage) {
|
|
416
|
-
log.debug(`[NARRATION] ${trimmedMessage.substring(0, 100)}`);
|
|
417
|
-
await channelManager.send(message.channel, routingSessionId, {
|
|
418
|
-
content: trimmedMessage,
|
|
419
|
-
type: "progress",
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// "tool_call" = el agente va a ejecutar una herramienta → narrar al usuario
|
|
426
|
-
if (step.type === "tool_call" && step.toolName) {
|
|
427
|
-
const narration = getNarration(step.toolName);
|
|
428
|
-
log.debug(`[TOOL] ${step.toolName} → "${narration}"`);
|
|
429
|
-
await channelManager.send(message.channel, routingSessionId, {
|
|
430
|
-
content: narration,
|
|
431
|
-
type: "progress",
|
|
432
|
-
});
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// "tool_result" = resultado de la herramienta
|
|
437
|
-
// Solo enviamos al usuario si el resultado lo pide explícitamente
|
|
438
|
-
if (step.type === "tool_result" && step.message) {
|
|
439
|
-
try {
|
|
440
|
-
const result = JSON.parse(step.message);
|
|
441
|
-
if (result._sendToUser) {
|
|
442
|
-
const userMessage = result.message || result.status || step.message;
|
|
443
|
-
await channelManager.send(message.channel, routingSessionId, {
|
|
444
|
-
content: userMessage,
|
|
445
|
-
type: "progress",
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
} catch {
|
|
449
|
-
// No es JSON estructurado — no enviamos resultados crudos al usuario
|
|
450
|
-
}
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
},
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
const responseContent = response.content?.trim() || "";
|
|
457
|
-
if (!responseContent) {
|
|
458
|
-
log.warn(`📤 LLM response: empty — skipping send`);
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
log.info(`📤 LLM response: ${responseContent.substring(0, 100)}${responseContent.length > 100 ? "..." : ""}`);
|
|
462
|
-
|
|
463
|
-
const shouldSpeak = preferAudioResponse;
|
|
464
|
-
let responseType: "text" | "audio" = "text";
|
|
465
|
-
let ttsProviderUsed: string | null = null;
|
|
466
|
-
let ttsMimeType: string | null = null;
|
|
467
|
-
|
|
468
|
-
if (responseContent) {
|
|
469
|
-
if (shouldSpeak) {
|
|
470
|
-
if (!voiceConfig.ttsProvider) {
|
|
471
|
-
log.warn(`⚠️ TTS provider not configured, user requested audio`);
|
|
472
|
-
await channelManager.send(message.channel, routingSessionId, {
|
|
473
|
-
content: `${responseContent}\n\n🔊 Para recibir respuestas en audio, configura el proveedor TTS en Configuración > Canales > [Tu canal] (ej: elevenlabs, openai-tts)`
|
|
474
|
-
});
|
|
475
|
-
} else {
|
|
476
|
-
try {
|
|
477
|
-
log.info(`🔊 TTS enabled, synthesizing audio...`);
|
|
478
|
-
const audioOutput = await voiceService.speak(responseContent, voiceConfig.ttsProvider, voiceConfig.ttsVoiceId || undefined);
|
|
479
|
-
ttsProviderUsed = voiceConfig.ttsProvider;
|
|
480
|
-
ttsMimeType = audioOutput.mimeType;
|
|
481
|
-
responseType = "audio";
|
|
482
|
-
|
|
483
|
-
const channel = channelManager.getChannel(message.channel);
|
|
484
|
-
if (channel?.sendAudio) {
|
|
485
|
-
await channel.sendAudio(routingSessionId, audioOutput.data as Buffer, audioOutput.mimeType);
|
|
486
|
-
log.info(`✅ Audio sent to ${routingSessionId}`);
|
|
487
|
-
} else {
|
|
488
|
-
await channelManager.send(message.channel, routingSessionId, { content: responseContent });
|
|
489
|
-
}
|
|
490
|
-
} catch (error) {
|
|
491
|
-
log.error(`❌ TTS failed: ${(error as Error).message}), sending text instead`);
|
|
492
|
-
await channelManager.send(message.channel, routingSessionId, { content: responseContent });
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
} else {
|
|
496
|
-
await channelManager.send(message.channel, routingSessionId, { content: responseContent });
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
const assistantMetadata = {
|
|
501
|
-
response_type: responseType,
|
|
502
|
-
tts_provider: ttsProviderUsed,
|
|
503
|
-
mime_type: ttsMimeType,
|
|
504
|
-
channel: message.channel
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
await channelManager.stopTyping(message.channel, routingSessionId);
|
|
508
|
-
log.info(`✅ Response sent to ${routingSessionId} via ${message.channel}`);
|
|
509
|
-
} catch (error) {
|
|
510
|
-
await channelManager.stopTyping(message.channel, routingSessionId);
|
|
511
|
-
log.error(`❌ Error: ${(error as Error).message} `);
|
|
512
|
-
await channelManager.send(message.channel, routingSessionId, {
|
|
513
|
-
content: `Error: ${(error as Error).message} `,
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
// ── Auth helper ──────────────────────────────────────────────────────────
|
|
519
|
-
// En modo desarrollo (HIVE_DEV=true), no requerimos autenticación
|
|
520
|
-
const isDev = process.env.HIVE_DEV === "true" || process.env.NODE_ENV === "development";
|
|
521
|
-
|
|
522
|
-
function checkAuth(req: Request, url: URL): boolean {
|
|
523
|
-
// En modo desarrollo, permitir todo
|
|
524
|
-
if (isDev) return true;
|
|
525
|
-
|
|
526
|
-
// Read live from env so the token set during setup/complete takes effect immediately
|
|
527
|
-
const activeToken = process.env.HIVE_AUTH_TOKEN;
|
|
528
|
-
if (!activeToken) return true;
|
|
529
|
-
const authHeader = req.headers.get("authorization");
|
|
530
|
-
const provided = authHeader?.replace(/^Bearer\s+/i, "") ?? url.searchParams.get("token");
|
|
531
|
-
return provided === activeToken;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Reload with full handler now that initialization is complete
|
|
535
|
-
server.reload({
|
|
536
|
-
async fetch(req, server) {
|
|
537
|
-
const start = Date.now();
|
|
538
|
-
const url = new URL(req.url);
|
|
539
|
-
const method = req.method;
|
|
540
|
-
|
|
541
|
-
const logRequest = (status: number, duration: number) => {
|
|
542
|
-
// Skip health checks from spamming logs unless debug
|
|
543
|
-
if (url.pathname === "/health" || url.pathname === "/health/") {
|
|
544
|
-
log.debug(`${method} ${url.pathname} - ${status} (${duration}ms)`);
|
|
545
|
-
} else {
|
|
546
|
-
log.info(`${method} ${url.pathname} - ${status} (${duration}ms)`);
|
|
547
|
-
}
|
|
548
|
-
};
|
|
549
|
-
|
|
550
|
-
const handleRequest = async (): Promise<Response | undefined> => {
|
|
551
|
-
|
|
552
|
-
// ── CORS preflight ────────────────────────────────────────────────────
|
|
553
|
-
if (req.method === "OPTIONS") {
|
|
554
|
-
const origin = req.headers.get("Origin");
|
|
555
|
-
if (origin && (origin.includes("localhost") || origin.includes("127.0.0.1") || CORS_ORIGINS.some(o => origin.includes(o.replace("http://", ""))))) {
|
|
556
|
-
return new Response(null, {
|
|
557
|
-
status: 204,
|
|
558
|
-
headers: {
|
|
559
|
-
"Access-Control-Allow-Origin": origin,
|
|
560
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
561
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization, Accept, X-Requested-With",
|
|
562
|
-
"Access-Control-Allow-Credentials": "true",
|
|
563
|
-
"Access-Control-Max-Age": "86400",
|
|
564
|
-
},
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
return new Response(null, { status: 204 });
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// ── WebSocket upgrade ────────────────────────────────────────────────
|
|
571
|
-
if (url.pathname === "/ws" || url.pathname === "/ws/") {
|
|
572
|
-
// En modo desarrollo, no requerir autenticación para WebSocket
|
|
573
|
-
if (!isDev && !checkAuth(req, url)) {
|
|
574
|
-
return new Response("Unauthorized", { status: 401 });
|
|
575
|
-
}
|
|
576
|
-
const sessionId = url.searchParams.get("session") || resolveUserId({}) || "default";
|
|
577
|
-
if (!sessionId) {
|
|
578
|
-
return new Response("Missing session or user ID", { status: 400 });
|
|
579
|
-
}
|
|
580
|
-
const success = server.upgrade(req, {
|
|
581
|
-
data: { sessionId, authenticatedAt: Date.now() },
|
|
582
|
-
});
|
|
583
|
-
if (success) return undefined;
|
|
584
|
-
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// ── Bridge Events WebSocket upgrade ────────────────────────────────────
|
|
588
|
-
if (url.pathname === "/bridge-events" || url.pathname === "/bridge-events/") {
|
|
589
|
-
const sessionId = `bridge:${url.searchParams.get("sessionId") ?? (resolveUserId({}) ?? "default")}`;
|
|
590
|
-
const success = server.upgrade(req, { data: { sessionId, authenticatedAt: Date.now() } });
|
|
591
|
-
if (success) return undefined;
|
|
592
|
-
return new Response("Bridge events WebSocket upgrade failed", { status: 400 });
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// ── Canvas WebSocket upgrade ────────────────────────────────────────────
|
|
596
|
-
if (url.pathname === "/canvas" || url.pathname === "/canvas/") {
|
|
597
|
-
// En modo desarrollo, no requerir autenticación para Canvas WebSocket
|
|
598
|
-
let sessionId = url.searchParams.get("sessionId") ?? url.searchParams.get("session");
|
|
599
|
-
const defaultUserId = resolveUserId({});
|
|
600
|
-
if (!sessionId && defaultUserId) {
|
|
601
|
-
sessionId = `canvas:${defaultUserId}`;
|
|
602
|
-
}
|
|
603
|
-
if (!sessionId) {
|
|
604
|
-
return new Response("Missing session or user ID for canvas", { status: 400 });
|
|
605
|
-
}
|
|
606
|
-
const success = server.upgrade(req, {
|
|
607
|
-
data: { sessionId: sessionId.startsWith("canvas:") ? sessionId : `canvas:${sessionId}`, authenticatedAt: Date.now() },
|
|
608
|
-
});
|
|
609
|
-
if (success) return undefined;
|
|
610
|
-
return new Response("Canvas WebSocket upgrade failed", { status: 400 });
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// ── Health (must be before UI routing so it works in dev mode too) ───
|
|
614
|
-
if (url.pathname === "/health" || url.pathname === "/health/") {
|
|
615
|
-
return addCorsHeaders(Response.json({ status: "ok", pid: process.pid }), req);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// ── Dashboard / UI ────────────────────────────────────────────────────
|
|
619
|
-
// In development: UI is served by Vite on port 5173, Gateway only handles /api and /ws
|
|
620
|
-
// In production: serve static files from packages/hive-ui/dist
|
|
621
|
-
|
|
622
|
-
// Check if this is an API or WebSocket request
|
|
623
|
-
const isApiRequest = url.pathname.startsWith("/api");
|
|
624
|
-
const isWsRequest = url.pathname.startsWith("/ws");
|
|
625
|
-
const isUiRequest = url.pathname === "/ui" || url.pathname === "/ui/" || url.pathname.startsWith("/ui/") || url.pathname.startsWith("/ui?");
|
|
626
|
-
const isSetupRequest = url.pathname === "/setup" || url.pathname === "/setup/" || url.pathname.startsWith("/setup/") || url.pathname.startsWith("/setup?");
|
|
627
|
-
|
|
628
|
-
// In development mode, skip UI handling - Vite handles it directly
|
|
629
|
-
// Only serve static files from dist if they exist (production mode)
|
|
630
|
-
if (!isApiRequest && !isWsRequest) {
|
|
631
|
-
// In development: tell user to use Vite directly
|
|
632
|
-
if (isDev) {
|
|
633
|
-
return new Response(
|
|
634
|
-
"UI not available through Gateway in development.\n\n" +
|
|
635
|
-
"Use Vite directly: http://localhost:5173\n",
|
|
636
|
-
{ status: 404, headers: { "Content-Type": "text/plain" } }
|
|
637
|
-
);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// In production: serve from dist folder
|
|
641
|
-
// Priority: HIVE_UI_DIR env > ~/.hive/ui > process.cwd()/packages/hive-ui/dist
|
|
642
|
-
const uiDirFromEnv = process.env.HIVE_UI_DIR;
|
|
643
|
-
const uiDirFromHive = path.join(getHiveDir(), "ui");
|
|
644
|
-
const uiDirFromCwd = path.join(process.cwd(), "packages/hive-ui/dist");
|
|
645
|
-
const uiDir = uiDirFromEnv
|
|
646
|
-
|| (existsSync(path.join(uiDirFromHive, "index.html")) ? uiDirFromHive : uiDirFromCwd);
|
|
647
|
-
let subPath = url.pathname;
|
|
648
|
-
|
|
649
|
-
// Normalize path for /ui routes
|
|
650
|
-
if (subPath === "/ui" || subPath === "/ui/") {
|
|
651
|
-
subPath = "/index.html";
|
|
652
|
-
} else if (subPath.startsWith("/ui/")) {
|
|
653
|
-
subPath = subPath.replace(/^\/ui/, "");
|
|
654
|
-
if (!subPath) subPath = "/index.html";
|
|
655
|
-
} else if (subPath === "/") {
|
|
656
|
-
subPath = "/index.html";
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// Normalize path for /setup routes
|
|
660
|
-
if (subPath === "/setup" || subPath === "/setup/") {
|
|
661
|
-
subPath = "/index.html";
|
|
662
|
-
} else if (subPath.startsWith("/setup/")) {
|
|
663
|
-
subPath = subPath.replace(/^\/setup/, "");
|
|
664
|
-
if (!subPath) subPath = "/index.html";
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
const filePath = path.join(uiDir, subPath);
|
|
668
|
-
const uiFile = Bun.file(filePath);
|
|
669
|
-
if (await uiFile.exists()) {
|
|
670
|
-
return new Response(uiFile);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// If it's a UI route and no dist, show message
|
|
674
|
-
if (isUiRequest || isSetupRequest) {
|
|
675
|
-
return new Response(
|
|
676
|
-
"UI not found.\n\n" +
|
|
677
|
-
"Options:\n" +
|
|
678
|
-
" 1. Place the UI in ~/.hive/ui/ (copy hive-ui/dist contents there)\n" +
|
|
679
|
-
" 2. Set HIVE_UI_DIR=/path/to/ui\n" +
|
|
680
|
-
" 3. Build from source: cd packages/hive-ui && bun run build\n",
|
|
681
|
-
{ status: 404, headers: { "Content-Type": "text/plain" } }
|
|
682
|
-
);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// Handle /dashboard redirect for backwards compatibility
|
|
687
|
-
if (url.pathname.startsWith("/dashboard")) {
|
|
688
|
-
const tokenParam = url.searchParams.get("token") ? `? token = ${url.searchParams.get("token")} ` : "";
|
|
689
|
-
return Response.redirect(`/ ui${tokenParam} `, 301);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// ── Rutas que requieren autenticación ────────────────────────────────
|
|
693
|
-
if (!checkAuth(req, url)) {
|
|
694
|
-
log.warn(`[AUTH] Unauthorized request to ${url.pathname} from ${req.headers.get("origin")} `);
|
|
695
|
-
return new Response("Unauthorized", { status: 401 });
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// ── Setup API ────────────────────────────────────────────────────────
|
|
699
|
-
// GET /api/setup/status
|
|
700
|
-
if (url.pathname === "/api/setup/status" || url.pathname === "/api/setup/status/") {
|
|
701
|
-
return addCorsHeaders(await handleSetupStatus(), req)
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// POST /api/setup/verify-provider
|
|
705
|
-
if (url.pathname === "/api/setup/verify-provider" && req.method === "POST") {
|
|
706
|
-
return addCorsHeaders(await handleVerifyProvider(req), req)
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// POST /api/setup/complete
|
|
710
|
-
if (url.pathname === "/api/setup/complete" && req.method === "POST") {
|
|
711
|
-
return await handleCompleteSetup(req, config, addCorsHeaders)
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// ── Status ───────────────────────────────────────────────────────────
|
|
715
|
-
if (url.pathname === "/status" || url.pathname === "/status/") {
|
|
716
|
-
return addCorsHeaders(new Response(
|
|
717
|
-
JSON.stringify({
|
|
718
|
-
status: "ok",
|
|
719
|
-
version: "0.1.7",
|
|
720
|
-
uptime: Math.floor((Date.now() - startTime) / 1000),
|
|
721
|
-
gateway: { host, port },
|
|
722
|
-
sessions: sessionManager.list().map((s) => ({
|
|
723
|
-
id: s.id,
|
|
724
|
-
createdAt: s.createdAt,
|
|
725
|
-
messageCount: s.messageCount,
|
|
726
|
-
})),
|
|
727
|
-
channels: channelManager?.listChannels() ?? [],
|
|
728
|
-
queue: { activeSessions: 0 },
|
|
729
|
-
}),
|
|
730
|
-
{ headers: { "Content-Type": "application/json", "Cache-Control": "max-age=5" } }
|
|
731
|
-
), req);
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// ── Activity Stats ─────────────────────────────────────────────────
|
|
735
|
-
if (url.pathname === "/api/activity-stats" || url.pathname === "/api/activity-stats/") {
|
|
736
|
-
return await handleGetActivityStats(req, addCorsHeaders)
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
// ── System Stats ───────────────────────────────────────────────────
|
|
740
|
-
if (url.pathname === "/api/system-stats" || url.pathname === "/api/system-stats/") {
|
|
741
|
-
return await handleGetSystemStats(req, addCorsHeaders, startTime)
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// ── Usage Stats ─────────────────────────────────────────────────────
|
|
745
|
-
if (url.pathname === "/api/usage-stats" || url.pathname === "/api/usage-stats/") {
|
|
746
|
-
return await handleGetUsageStats(req, addCorsHeaders)
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// ── System Reload ─────────────────────────────────────────────────
|
|
750
|
-
if (url.pathname === "/api/system/reload" || url.pathname === "/api/system/reload/") {
|
|
751
|
-
return await handleSystemReload(req, addCorsHeaders)
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
// ── Config ─────────────────────────────────────────────────────────
|
|
755
|
-
if (url.pathname === "/api/config") {
|
|
756
|
-
if (req.method === "GET") {
|
|
757
|
-
return await handleGetConfig(req, addCorsHeaders, config);
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
// ── Projects API ─────────────────────────────────────────────────────
|
|
762
|
-
if ((url.pathname === "/api/projects" || url.pathname === "/api/projects/") && req.method === "GET") {
|
|
763
|
-
return await handleGetProjects(req, addCorsHeaders)
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
if (url.pathname === "/api/projects/active" && req.method === "GET") {
|
|
767
|
-
return await handleGetActiveProject(req, addCorsHeaders)
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
if (url.pathname === "/api/projects/history" && req.method === "GET") {
|
|
771
|
-
return await handleGetProjectHistory(req, addCorsHeaders)
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
const projectDetailMatch = url.pathname.match(/^\/api\/projects\/([^/]+)$/)
|
|
775
|
-
if (projectDetailMatch && req.method === "GET") {
|
|
776
|
-
const projectId = projectDetailMatch[1]
|
|
777
|
-
return await handleGetProjectDetail(req, addCorsHeaders, projectId)
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
if (projectDetailMatch && (req.method === "PATCH" || req.method === "PUT")) {
|
|
781
|
-
return await handleUpdateProject(req, addCorsHeaders)
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
const projectTasksMatch = url.pathname.match(/^\/api\/projects\/([^/]+)\/tasks$/)
|
|
785
|
-
if (projectTasksMatch && req.method === "GET") {
|
|
786
|
-
const projectId = projectTasksMatch[1]
|
|
787
|
-
return await handleGetProjectTasks(req, addCorsHeaders, projectId)
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
// POST /api/projects — crear nuevo proyecto
|
|
791
|
-
if (url.pathname === "/api/projects" && req.method === "POST") {
|
|
792
|
-
return await handleCreateProject(req, addCorsHeaders)
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// ── Tasks API ─────────────────────────────────────────────────────
|
|
796
|
-
if ((url.pathname === "/api/tasks" || url.pathname === "/api/tasks/") && req.method === "GET") {
|
|
797
|
-
return await handleGetTasks(req, addCorsHeaders)
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
const taskDetailMatch = url.pathname.match(/^\/api\/tasks\/(\d+)$/)
|
|
801
|
-
if (taskDetailMatch && req.method === "PATCH") {
|
|
802
|
-
return await handleUpdateTask(req, addCorsHeaders)
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
// ── Channels API ─────────────────────────────────────────────────────
|
|
808
|
-
if ((url.pathname === "/api/channels" || url.pathname === "/api/channels/") && req.method === "POST") {
|
|
809
|
-
const body = await req.json().catch(() => ({}));
|
|
810
|
-
const { name, accountId, config: channelConfigData } = body;
|
|
811
|
-
if (!name || !accountId || !channelConfigData) {
|
|
812
|
-
return addCorsHeaders(new Response("Missing name, accountId or config", { status: 400 }), req);
|
|
813
|
-
}
|
|
814
|
-
// Save config to YAML is handled by caller
|
|
815
|
-
config.channels = config.channels || {};
|
|
816
|
-
config.channels[name] = config.channels[name] || { enabled: true, accounts: {} };
|
|
817
|
-
const channelEntry = config.channels[name] as any;
|
|
818
|
-
channelEntry.accounts = channelEntry.accounts || {};
|
|
819
|
-
channelEntry.accounts[accountId] = channelConfigData;
|
|
820
|
-
return await handleCreateChannel(req, addCorsHeaders, channelManager);
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
const channelDetailMatch = url.pathname.match(/^\/api\/channels\/([^/]+)\/([^/]+)$/);
|
|
824
|
-
if (channelDetailMatch) {
|
|
825
|
-
const name = channelDetailMatch[1];
|
|
826
|
-
const accountId = channelDetailMatch[2];
|
|
827
|
-
|
|
828
|
-
if (req.method === "GET") {
|
|
829
|
-
return await handleGetChannelAccount(req, addCorsHeaders, name, accountId);
|
|
830
|
-
}
|
|
831
|
-
if (req.method === "PUT") {
|
|
832
|
-
const body = await req.json().catch(() => ({}));
|
|
833
|
-
if (!body.config) return new Response("Missing config", { status: 400 });
|
|
834
|
-
// Save config to YAML is handled by caller
|
|
835
|
-
config.channels = config.channels || {};
|
|
836
|
-
config.channels[name] = config.channels[name] || { enabled: true, accounts: {} };
|
|
837
|
-
const channelEntry = config.channels[name] as any;
|
|
838
|
-
channelEntry.accounts = channelEntry.accounts || {};
|
|
839
|
-
channelEntry.accounts[accountId] = body.config;
|
|
840
|
-
return await handleUpdateChannelAccount(req, addCorsHeaders, name, accountId, channelManager);
|
|
841
|
-
}
|
|
842
|
-
if (req.method === "DELETE") {
|
|
843
|
-
// Config update handled by caller
|
|
844
|
-
if (config.channels?.[name]) {
|
|
845
|
-
const channelEntry = config.channels[name] as any;
|
|
846
|
-
if (channelEntry.accounts) {
|
|
847
|
-
delete channelEntry.accounts[accountId];
|
|
848
|
-
if (Object.keys(channelEntry.accounts).length === 0) {
|
|
849
|
-
delete config.channels[name];
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
return await handleDeleteChannelAccount(req, addCorsHeaders, name, accountId, config, channelManager);
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
const channelActionMatch = url.pathname.match(
|
|
858
|
-
/^\/api\/channels\/([^/]+)\/([^/]+)\/(start|stop)$/
|
|
859
|
-
);
|
|
860
|
-
if (channelActionMatch) {
|
|
861
|
-
const [, name, accountId, action] = channelActionMatch;
|
|
862
|
-
if (req.method === "POST") {
|
|
863
|
-
return await handleChannelAction(req, addCorsHeaders, name, accountId, action as "start" | "stop", channelManager);
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
// ── Skills API ───────────────────────────────────────────────────────
|
|
868
|
-
if ((url.pathname === "/api/skills" || url.pathname === "/api/skills/") && req.method === "POST") {
|
|
869
|
-
return await handleCreateSkill(req, addCorsHeaders);
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
// ── Model Config API ─────────────────────────────────────────────────
|
|
873
|
-
if (url.pathname === "/api/config/models") {
|
|
874
|
-
if (req.method === "GET") {
|
|
875
|
-
return await handleGetModelsConfig(req, addCorsHeaders, config);
|
|
876
|
-
}
|
|
877
|
-
if (req.method === "POST") {
|
|
878
|
-
return await handleUpdateModelsConfig(req, addCorsHeaders, config, agent);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
// ── MCP API ──────────────────────────────────────────────────────────
|
|
883
|
-
// Note: Full MCP route handlers are in routes/mcp.ts
|
|
884
|
-
if (url.pathname === "/api/mcp/servers" && req.method === "GET") {
|
|
885
|
-
const mcpManager = agent.getMCPManager()
|
|
886
|
-
if (!mcpManager) {
|
|
887
|
-
log.warn(`[GET /api/mcp/servers] MCP Manager is null!`)
|
|
888
|
-
} else {
|
|
889
|
-
log.info(`[GET /api/mcp/servers] MCP Manager found`)
|
|
890
|
-
}
|
|
891
|
-
return await handleGetMcpServers(req, addCorsHeaders, mcpManager)
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
if (url.pathname === "/api/mcp/servers" && req.method === "POST") {
|
|
895
|
-
const response = await handleCreateMcpServer(req, addCorsHeaders)
|
|
896
|
-
|
|
897
|
-
// Hot reload will auto-connect the server within 2 seconds
|
|
898
|
-
// No manual connection needed
|
|
899
|
-
|
|
900
|
-
return response
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
if (url.pathname === "/api/mcp/servers" && req.method === "PUT") {
|
|
904
|
-
return await handleUpdateMcpServer(req, addCorsHeaders)
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
if (url.pathname === "/api/mcp/servers" && req.method === "DELETE") {
|
|
908
|
-
return await handleDeleteMcpServer(req, addCorsHeaders)
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
if (url.pathname.match(/^\/api\/mcp\/servers\/[^/]+\/toggle$/)) {
|
|
912
|
-
const mcpId = url.pathname.split("/")[4];
|
|
913
|
-
if (req.method === "POST") {
|
|
914
|
-
return await handleToggleMcpServer(req, addCorsHeaders, mcpId)
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
// ── Workspace API ────────────────────────────────────────────────────
|
|
919
|
-
// Validate workspace path
|
|
920
|
-
if (url.pathname === "/api/workspace/validate" && req.method === "POST") {
|
|
921
|
-
return await handleValidateWorkspace(req, addCorsHeaders);
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
// Create workspace directory
|
|
925
|
-
if (url.pathname === "/api/workspace/create" && req.method === "POST") {
|
|
926
|
-
return await handleCreateWorkspace(req, addCorsHeaders);
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
// Open workspace in file explorer
|
|
930
|
-
if (url.pathname === "/api/workspace/open" && req.method === "GET") {
|
|
931
|
-
return await handleOpenWorkspace(req, addCorsHeaders);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
// Get/Update workspace files (soul, user, ethics)
|
|
935
|
-
for (const wsType of ["soul", "user", "ethics"] as const) {
|
|
936
|
-
if (url.pathname === `/api/workspace/${wsType}`) {
|
|
937
|
-
if (req.method === "GET") {
|
|
938
|
-
return await handleGetWorkspace(req, addCorsHeaders, workspacePath, wsType);
|
|
939
|
-
}
|
|
940
|
-
if (req.method === "POST") {
|
|
941
|
-
const reloadFn = async (type: string) => {
|
|
942
|
-
if (type === "soul") agent.reloadSoul();
|
|
943
|
-
if (type === "user") agent.reloadUser();
|
|
944
|
-
if (type === "ethics") await agent.reloadEthics();
|
|
945
|
-
};
|
|
946
|
-
return await handleUpdateWorkspace(req, addCorsHeaders, workspacePath, wsType, reloadFn);
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
// ── Reload API ───────────────────────────────────────────────────────
|
|
952
|
-
if (url.pathname === "/api/reload" && req.method === "POST") {
|
|
953
|
-
return await handleApiReload(req, addCorsHeaders, agent);
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
// ── User Channel Linking API ────────────────────────────────────────────
|
|
957
|
-
if (url.pathname === "/api/user/channels" && req.method === "POST") {
|
|
958
|
-
return await handleLinkUserChannel(req, addCorsHeaders, config, log);
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
if (url.pathname === "/api/user/channels" && req.method === "GET") {
|
|
962
|
-
return await handleGetUserChannels(req, addCorsHeaders, config);
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
// ── Agents API ─────────────────────────────────────────────────────
|
|
966
|
-
if (url.pathname === "/api/agents" && req.method === "GET") {
|
|
967
|
-
return await handleGetAgents(req, addCorsHeaders)
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
if (url.pathname === "/api/agents" && req.method === "POST") {
|
|
971
|
-
return await handleCreateAgent(req, addCorsHeaders)
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
if (url.pathname.startsWith("/api/agents/") && (req.method === "PATCH" || req.method === "PUT")) {
|
|
975
|
-
return await handleUpdateAgent(req, addCorsHeaders)
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
// ── Providers API ───────────────────────────────────────────────────
|
|
979
|
-
if (url.pathname === "/api/providers" && req.method === "GET") {
|
|
980
|
-
return await handleGetProviders(req, addCorsHeaders)
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
if (url.pathname === "/api/providers" && req.method === "POST") {
|
|
984
|
-
return await handleCreateProvider(req, addCorsHeaders)
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
if (url.pathname.match(/^\/api\/providers\/[^/]+\/toggle$/) && req.method === "POST") {
|
|
988
|
-
return await handleToggleProvider(req, addCorsHeaders)
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
const providerIdMatch = url.pathname.match(/^\/api\/providers\/([^/]+)$/)
|
|
992
|
-
if (providerIdMatch && (req.method === "PUT" || req.method === "PATCH")) {
|
|
993
|
-
return await handleUpdateProvider(req, addCorsHeaders)
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
// ── Models API ───────────────────────────────────────────────────
|
|
997
|
-
// GET /api/models?provider_id=xxx - Get models filtered by provider
|
|
998
|
-
if (url.pathname === "/api/models" && req.method === "GET") {
|
|
999
|
-
return await handleGetModels(req, addCorsHeaders)
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
// POST /api/providers/:id/sync-models — sincroniza modelos desde la API local del provider
|
|
1003
|
-
const syncModelsMatch = url.pathname.match(/^\/api\/providers\/([^/]+)\/sync-models$/)
|
|
1004
|
-
if (syncModelsMatch && req.method === "POST") {
|
|
1005
|
-
const providerId = syncModelsMatch[1]
|
|
1006
|
-
return await handleSyncProviderModels(req, addCorsHeaders, providerId)
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
// POST /api/models - Create a new model
|
|
1010
|
-
if (url.pathname === "/api/models" && req.method === "POST") {
|
|
1011
|
-
return await handleCreateModel(req, addCorsHeaders)
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
if (url.pathname.match(/^\/api\/models\/[^/]+\/toggle$/) && req.method === "POST") {
|
|
1015
|
-
return await handleToggleModel(req, addCorsHeaders)
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
// ── Channels API ───────────────────────────────────────────────────
|
|
1019
|
-
if (url.pathname === "/api/channels" && req.method === "GET") {
|
|
1020
|
-
return await handleGetChannels(req, addCorsHeaders)
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
// ── Skills API ─────────────────────────────────────────────────────
|
|
1024
|
-
if (url.pathname === "/api/skills" && req.method === "GET") {
|
|
1025
|
-
return await handleGetSkills(req, addCorsHeaders)
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
if (url.pathname === "/api/skills" && req.method === "POST") {
|
|
1029
|
-
const body = await req.json().catch(() => ({}))
|
|
1030
|
-
const { name, category, tools, triggers, body: bodyContent } = body
|
|
1031
|
-
if (!name) return addCorsHeaders(new Response("Missing name", { status: 400 }), req)
|
|
1032
|
-
const id = randomUUID()
|
|
1033
|
-
getDb().query(`INSERT INTO skills(id, name, category, tools, triggers, body, version, active) VALUES(?, ?, ?, ?, ?, ?, 1, 1)`).run(id, name, category || "", tools || "", triggers || "", bodyContent || "")
|
|
1034
|
-
return addCorsHeaders(Response.json({ success: true, id }), req)
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
if (url.pathname.match(/^\/api\/skills\/[^/]+\/toggle$/) && req.method === "POST") {
|
|
1038
|
-
return await handleActivateSkill(req, addCorsHeaders)
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
// ── Tools API ────────────────────────────────────────────────────────
|
|
1042
|
-
if (url.pathname === "/api/tools" && req.method === "GET") {
|
|
1043
|
-
return await handleGetTools(req, addCorsHeaders)
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
if (url.pathname.match(/^\/api\/tools\/[^/]+\/toggle$/) && req.method === "POST") {
|
|
1047
|
-
return await handleActivateTool(req, addCorsHeaders)
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
// ── Ethics API ──────────────────────────────────────────────────────
|
|
1051
|
-
if (url.pathname === "/api/ethics" && req.method === "GET") {
|
|
1052
|
-
return await handleGetEthics(req, addCorsHeaders)
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
if (url.pathname === "/api/ethics" && req.method === "POST") {
|
|
1056
|
-
const body = await req.json().catch(() => ({}))
|
|
1057
|
-
const { name, description, content, is_default } = body
|
|
1058
|
-
if (!name || !content) return addCorsHeaders(Response.json({ success: false, error: "Missing name or content" }, { status: 400 }), req)
|
|
1059
|
-
const id = randomUUID()
|
|
1060
|
-
getDb().query(`INSERT INTO ethics(id, name, description, content, is_default, enabled, active) VALUES(?, ?, ?, ?, ?, 1, 1)`).run(id, name, description || "", content, is_default ? 1 : 0)
|
|
1061
|
-
return addCorsHeaders(Response.json({ success: true, id }), req)
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
if (url.pathname.match(/^\/api\/ethics\/[^/]+$/) && req.method === "PUT") {
|
|
1065
|
-
return await handleActivateEthics(req, addCorsHeaders)
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
if (url.pathname.match(/^\/api\/ethics\/[^/]+$/) && req.method === "DELETE") {
|
|
1069
|
-
return await handleDeleteEthics(req, addCorsHeaders)
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
// ── Users API ───────────────────────────────────────────────────────
|
|
1073
|
-
if (url.pathname === "/api/users" && req.method === "GET") {
|
|
1074
|
-
return await handleGetUsers(req, addCorsHeaders)
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
if (url.pathname === "/api/users" && req.method === "POST") {
|
|
1078
|
-
return await handleCreateUser(req, addCorsHeaders)
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
if (url.pathname === "/api/user/settings" && req.method === "PATCH") {
|
|
1082
|
-
return await handleUpdateUserSettings(req, addCorsHeaders)
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
// ── MCP Servers API ──────────────────────────────────────────────────
|
|
1086
|
-
if (url.pathname === "/api/mcp/servers" && req.method === "GET") {
|
|
1087
|
-
return await handleGetMcpServers(req, addCorsHeaders, agent.getMCPManager())
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
// GET /api/mcp/servers/:id/tools - Get tools for a specific MCP server
|
|
1091
|
-
if (url.pathname.match(/^\/api\/mcp\/servers\/([^/]+)\/tools$/) && req.method === "GET") {
|
|
1092
|
-
const serverId = url.pathname.split("/")[4];
|
|
1093
|
-
return await handleGetMCPServerTools(req, addCorsHeaders, serverId)
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
// POST /api/mcp/tools/:id/toggle - Toggle MCP tool active state
|
|
1097
|
-
if (url.pathname.match(/^\/api\/mcp\/tools\/([^/]+)\/toggle$/) && req.method === "POST") {
|
|
1098
|
-
const toolId = url.pathname.split("/")[4];
|
|
1099
|
-
return await handleToggleMCPTool(req, addCorsHeaders, toolId)
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
// DELETE /api/mcp/tools/:id - Delete MCP tool
|
|
1103
|
-
if (url.pathname.match(/^\/api\/mcp\/tools\/([^/]+)$/) && req.method === "DELETE") {
|
|
1104
|
-
const toolId = url.pathname.split("/")[4];
|
|
1105
|
-
return await handleDeleteMCPTool(req, addCorsHeaders, toolId)
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
if (url.pathname.match(/^\/api\/mcp\/servers\/([^/]+)\/toggle$/)) {
|
|
1109
|
-
const mcpName = url.pathname.split("/")[4];
|
|
1110
|
-
if (req.method === "POST") {
|
|
1111
|
-
const body = await req.json().catch(() => ({}))
|
|
1112
|
-
// Support both { active: boolean } and { action: "connect"|"disconnect" }
|
|
1113
|
-
let active = body.active
|
|
1114
|
-
if (active === undefined && body.action !== undefined) {
|
|
1115
|
-
active = body.action === "connect"
|
|
1116
|
-
}
|
|
1117
|
-
if (active === undefined) {
|
|
1118
|
-
return addCorsHeaders(Response.json({ success: false, error: "Missing active field" }, { status: 400 }), req)
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
log.info(`[MCP] Toggle connection for ${mcpName}, active=${active}`)
|
|
1122
|
-
|
|
1123
|
-
// Update DB
|
|
1124
|
-
getDb().query(`UPDATE mcp_servers SET active = ?, enabled = ? WHERE id = ?`).run(active ? 1 : 0, active ? 1 : 0, mcpName)
|
|
1125
|
-
|
|
1126
|
-
// Connect/Disconnect MCP server in real-time (no restart needed)
|
|
1127
|
-
try {
|
|
1128
|
-
const mcp = agent.getMCPManager();
|
|
1129
|
-
if (mcp) {
|
|
1130
|
-
log.info(`[MCP] Manager found, connecting ${mcpName}...`)
|
|
1131
|
-
if (active) {
|
|
1132
|
-
const server = getDb().query(`SELECT * FROM mcp_servers WHERE id = ?`).get(mcpName) as Record<string, any> | undefined;
|
|
1133
|
-
if (server) {
|
|
1134
|
-
log.info(`[MCP] Server config: transport=${server.transport}, url=${server.url}`)
|
|
1135
|
-
|
|
1136
|
-
// Build MCP server config
|
|
1137
|
-
const mcpServerConfig: any = {
|
|
1138
|
-
transport: server.transport as string,
|
|
1139
|
-
command: server.command as string | null,
|
|
1140
|
-
args: server.args ? JSON.parse(server.args as string) : [],
|
|
1141
|
-
url: server.url as string | null,
|
|
1142
|
-
enabled: true,
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
// Decrypt headers if present
|
|
1146
|
-
if (server.headers_encrypted && server.headers_iv) {
|
|
1147
|
-
try {
|
|
1148
|
-
mcpServerConfig.headers = decryptConfig(server.headers_encrypted, server.headers_iv);
|
|
1149
|
-
} catch (e) {
|
|
1150
|
-
log.warn(`Failed to decrypt headers for ${mcpName}`);
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
// Get current MCP config and add/update this server
|
|
1155
|
-
const currentConfig = (mcp as any).config || { servers: {} }
|
|
1156
|
-
const newServersConfig = { ...currentConfig.servers }
|
|
1157
|
-
newServersConfig[mcpName] = mcpServerConfig
|
|
1158
|
-
|
|
1159
|
-
// Update MCP Manager config (this will register and auto-connect the server)
|
|
1160
|
-
await mcp.updateConfig({
|
|
1161
|
-
...currentConfig,
|
|
1162
|
-
servers: newServersConfig,
|
|
1163
|
-
});
|
|
1164
|
-
|
|
1165
|
-
log.info(`[MCP] Server registered in MCP Manager`)
|
|
1166
|
-
|
|
1167
|
-
// Get tools after connection
|
|
1168
|
-
const tools = mcp.getServerTools(mcpName) || [];
|
|
1169
|
-
log.info(`[MCP] Connected! Tools: ${tools.length}`)
|
|
1170
|
-
getDb().query(`UPDATE mcp_servers SET status = ?, tools_count = ? WHERE id = ?`).run("connected", tools.length, mcpName);
|
|
1171
|
-
} else {
|
|
1172
|
-
log.error(`[MCP] Server not found in DB: ${mcpName}`)
|
|
1173
|
-
}
|
|
1174
|
-
} else {
|
|
1175
|
-
await mcp.disconnectServer(mcpName);
|
|
1176
|
-
getDb().query(`UPDATE mcp_servers SET status = ? WHERE id = ?`).run("disconnected", mcpName);
|
|
1177
|
-
}
|
|
1178
|
-
} else {
|
|
1179
|
-
log.error(`[MCP] No MCP Manager found`)
|
|
1180
|
-
}
|
|
1181
|
-
} catch (error) {
|
|
1182
|
-
log.error(`[MCP] Failed to connect ${mcpName}: ${(error as Error).message}`);
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
return addCorsHeaders(Response.json({ success: true, active, message: active ? "Servidor MCP conectado" : "Servidor MCP desconectado" }), req)
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
// Support /api/mcp/servers/{name} with POST for connect (frontend uses this)
|
|
1190
|
-
if (url.pathname.match(/^\/api\/mcp\/servers\/([^/]+)$/)) {
|
|
1191
|
-
const mcpName = url.pathname.split("/")[4];
|
|
1192
|
-
if (req.method === "POST") {
|
|
1193
|
-
const body = await req.json().catch(() => ({}))
|
|
1194
|
-
// Support both { active: boolean } and { action: "connect"|"disconnect" }
|
|
1195
|
-
let active = body.active
|
|
1196
|
-
if (active === undefined && body.action !== undefined) {
|
|
1197
|
-
active = body.action === "connect"
|
|
1198
|
-
}
|
|
1199
|
-
if (active === undefined) {
|
|
1200
|
-
return addCorsHeaders(Response.json({ success: false, error: "Missing active field" }, { status: 400 }), req)
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
// Update DB
|
|
1204
|
-
getDb().query(`UPDATE mcp_servers SET active = ?, enabled = ? WHERE id = ?`).run(active ? 1 : 0, active ? 1 : 0, mcpName)
|
|
1205
|
-
|
|
1206
|
-
// Connect/Disconnect MCP server in real-time (no restart needed)
|
|
1207
|
-
try {
|
|
1208
|
-
const mcp = agent.getMCPManager();
|
|
1209
|
-
if (mcp) {
|
|
1210
|
-
if (active) {
|
|
1211
|
-
const server = getDb().query(`SELECT * FROM mcp_servers WHERE id = ?`).get(mcpName) as Record<string, any> | undefined;
|
|
1212
|
-
if (server) {
|
|
1213
|
-
log.info(`[MCP] Server config: transport=${server.transport}, url=${server.url}`)
|
|
1214
|
-
|
|
1215
|
-
// Build MCP server config
|
|
1216
|
-
const mcpServerConfig: any = {
|
|
1217
|
-
transport: server.transport as string,
|
|
1218
|
-
command: server.command as string | null,
|
|
1219
|
-
args: server.args ? JSON.parse(server.args as string) : [],
|
|
1220
|
-
url: server.url as string | null,
|
|
1221
|
-
enabled: true,
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
// Decrypt headers if present
|
|
1225
|
-
if (server.headers_encrypted && server.headers_iv) {
|
|
1226
|
-
try {
|
|
1227
|
-
mcpServerConfig.headers = decryptConfig(server.headers_encrypted, server.headers_iv);
|
|
1228
|
-
} catch (e) {
|
|
1229
|
-
log.warn(`Failed to decrypt headers for ${mcpName}`);
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
// Get current MCP config and add/update this server
|
|
1234
|
-
const currentConfig = (mcp as any).config || { servers: {} }
|
|
1235
|
-
const newServersConfig = { ...currentConfig.servers }
|
|
1236
|
-
newServersConfig[mcpName] = mcpServerConfig
|
|
1237
|
-
|
|
1238
|
-
// Update MCP Manager config (this will register and auto-connect the server)
|
|
1239
|
-
await mcp.updateConfig({
|
|
1240
|
-
...currentConfig,
|
|
1241
|
-
servers: newServersConfig,
|
|
1242
|
-
});
|
|
1243
|
-
|
|
1244
|
-
log.info(`[MCP] Server registered in MCP Manager`)
|
|
1245
|
-
|
|
1246
|
-
// Get tools after connection
|
|
1247
|
-
const tools = mcp.getServerTools(mcpName) || [];
|
|
1248
|
-
log.info(`[MCP] Connected! Tools: ${tools.length}`)
|
|
1249
|
-
|
|
1250
|
-
// Update DB with status and tools
|
|
1251
|
-
getDb().query(`UPDATE mcp_servers SET status = ?, tools_count = ? WHERE id = ?`).run("connected", tools.length, mcpName);
|
|
1252
|
-
} else {
|
|
1253
|
-
log.error(`[MCP] Server not found in DB: ${mcpName}`)
|
|
1254
|
-
}
|
|
1255
|
-
} else {
|
|
1256
|
-
await mcp.disconnectServer(mcpName);
|
|
1257
|
-
getDb().query(`UPDATE mcp_servers SET status = ? WHERE id = ?`).run("disconnected", mcpName);
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
} catch (error) {
|
|
1261
|
-
log.error(`[MCP] Failed to connect ${mcpName}: ${(error as Error).message}`);
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
return addCorsHeaders(Response.json({ success: true, active, message: active ? "Servidor MCP conectado" : "Servidor MCP desconectado" }), req)
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
// ── Channels API ───────────────────────────────────────────────────
|
|
1269
|
-
if (url.pathname === "/api/channels" && req.method === "GET") {
|
|
1270
|
-
const channels = getDb().query("SELECT id, type, id as account_id, enabled, active, status FROM channels").all();
|
|
1271
|
-
return addCorsHeaders(Response.json({ channels }), req);
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
// PUT /api/channels/:id - Update channel settings
|
|
1275
|
-
const channelIdMatch = url.pathname.match(/^\/api\/channels\/([^/]+)$/);
|
|
1276
|
-
if (channelIdMatch && req.method === "PUT") {
|
|
1277
|
-
const channelId = channelIdMatch[1];
|
|
1278
|
-
return await handleUpdateChannelSettings(req, addCorsHeaders, channelId);
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
if (url.pathname.match(/^\/api\/channels\/[^/]+\/toggle$/)) {
|
|
1282
|
-
const channelId = url.pathname.split("/")[3];
|
|
1283
|
-
if (req.method === "POST") {
|
|
1284
|
-
return await handleToggleChannel(req, addCorsHeaders, channelId);
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
// ── Voice API ───────────────────────────────────────────────────────
|
|
1289
|
-
if (url.pathname === "/api/voice/providers" && req.method === "GET") {
|
|
1290
|
-
return await handleGetVoiceProviders(req, addCorsHeaders)
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
if (url.pathname === "/api/voice/configured-providers" && req.method === "GET") {
|
|
1294
|
-
return await handleGetConfiguredVoiceProviders(req, addCorsHeaders)
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
if (url.pathname === "/api/voice/test" && req.method === "POST") {
|
|
1298
|
-
return await handleTestVoice(req, addCorsHeaders)
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
// GET /api/channels/:id/voice - Get voice config for a channel
|
|
1302
|
-
const channelVoiceMatch = url.pathname.match(/^\/api\/channels\/([^/]+)\/voice$/)
|
|
1303
|
-
if (channelVoiceMatch && req.method === "GET") {
|
|
1304
|
-
const channelId = channelVoiceMatch[1]
|
|
1305
|
-
return await handleGetChannelVoice(req, addCorsHeaders, channelId)
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
// PATCH /api/channels/:id/voice - Update voice config for a channel
|
|
1309
|
-
if (channelVoiceMatch && req.method === "PATCH") {
|
|
1310
|
-
const channelId = channelVoiceMatch[1]
|
|
1311
|
-
return await handleUpdateChannelVoice(req, addCorsHeaders, channelId)
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
// ── Chat / Canvas / Notes API ───────────────────────────────────────
|
|
1315
|
-
if (url.pathname === "/api/chat/history" && req.method === "GET") {
|
|
1316
|
-
return await handleGetChatHistory(req, addCorsHeaders)
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
if (url.pathname === "/api/chat" && req.method === "POST") {
|
|
1320
|
-
return await handlePostChat(req, addCorsHeaders)
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
if (url.pathname === "/api/canvas" && req.method === "GET") {
|
|
1324
|
-
return await handleGetCanvas(req, addCorsHeaders)
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
if (url.pathname === "/api/notes" && req.method === "GET") {
|
|
1328
|
-
return await handleGetNotes(req, addCorsHeaders)
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
// ── Cron Jobs API ───────────────────────────────────────────────────
|
|
1332
|
-
if (url.pathname === "/api/cron-jobs" && req.method === "GET") {
|
|
1333
|
-
return await handleGetCronJobs(req, addCorsHeaders)
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
if (url.pathname === "/api/cron-jobs/channels" && req.method === "GET") {
|
|
1337
|
-
return await handleGetCronChannels(req, addCorsHeaders)
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
if (url.pathname.match(/^\/api\/cron-jobs\/[^/]+\/toggle$/) && req.method === "PATCH") {
|
|
1341
|
-
return await handleUpdateCronJob(req, addCorsHeaders)
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
return addCorsHeaders(new Response("Not Found", { status: 404 }), req)
|
|
1345
|
-
};
|
|
1346
|
-
|
|
1347
|
-
try {
|
|
1348
|
-
const response = await handleRequest();
|
|
1349
|
-
const duration = Date.now() - start;
|
|
1350
|
-
if (response) {
|
|
1351
|
-
logRequest(response.status, duration);
|
|
1352
|
-
} else {
|
|
1353
|
-
// Bun upgrade returns undefined on success
|
|
1354
|
-
log.info(`${method} ${url.pathname} - 101 Switching Protocols(${duration}ms)`);
|
|
1355
|
-
}
|
|
1356
|
-
return response;
|
|
1357
|
-
} catch (error) {
|
|
1358
|
-
const duration = Date.now() - start;
|
|
1359
|
-
log.error(`${method} ${url.pathname} - Internal Error(${duration}ms): ${(error as Error).message} `);
|
|
1360
|
-
return addCorsHeaders(Response.json({ success: false, error: (error as Error).message, message: "Error interno del servidor" }, { status: 500 }), req);
|
|
1361
|
-
}
|
|
1362
|
-
},
|
|
1363
|
-
|
|
1364
|
-
websocket: {
|
|
1365
|
-
open(ws) {
|
|
1366
|
-
const data = ws.data;
|
|
1367
|
-
const isCanvas = data.sessionId.startsWith("canvas:");
|
|
1368
|
-
const isBridge = data.sessionId.startsWith("bridge:");
|
|
1369
|
-
|
|
1370
|
-
if (isBridge) {
|
|
1371
|
-
log.info(`Bridge events client connected: ${data.sessionId}`);
|
|
1372
|
-
subscribeBridge(ws as any);
|
|
1373
|
-
ws.send(JSON.stringify({ type: "bridge:connected", sessionId: data.sessionId }));
|
|
1374
|
-
return;
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
if (isCanvas) {
|
|
1378
|
-
log.info(`Canvas session connected: ${data.sessionId} `);
|
|
1379
|
-
canvasManager.registerSession(data.sessionId, ws as any);
|
|
1380
|
-
subscribeCanvas(ws as any);
|
|
1381
|
-
ws.send(JSON.stringify({ type: "canvas:connected", sessionId: data.sessionId }));
|
|
1382
|
-
// Send initial snapshot so canvas shows current state
|
|
1383
|
-
ws.send(JSON.stringify({ type: "canvas:snapshot", data: getCanvasSnapshot() }));
|
|
1384
|
-
return;
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
log.debug(`WebSocket connected: ${data.sessionId} `);
|
|
1388
|
-
|
|
1389
|
-
sessionManager.create(data.sessionId, ws);
|
|
1390
|
-
|
|
1391
|
-
const channel = channelManager?.getChannel("webchat") as any;
|
|
1392
|
-
if (channel?.registerConnection) channel.registerConnection(ws);
|
|
1393
|
-
|
|
1394
|
-
// Send status message
|
|
1395
|
-
ws.send(JSON.stringify({
|
|
1396
|
-
type: "status",
|
|
1397
|
-
sessionId: data.sessionId,
|
|
1398
|
-
status: { state: "connected", model: `${dbProvider}/${dbModel}` },
|
|
1399
|
-
} as OutboundMessage));
|
|
1400
|
-
|
|
1401
|
-
// Send welcome message with real user data
|
|
1402
|
-
try {
|
|
1403
|
-
const db = getDb();
|
|
1404
|
-
const user = db.query("SELECT id, name, language FROM users LIMIT 1").get() as { id: string; name: string; language: string } | undefined;
|
|
1405
|
-
const agent = db.query("SELECT id, name, provider_id, model_id FROM agents WHERE role = 'coordinator' LIMIT 1").get() as { id: string; name: string; provider_id: string; model_id: string } | undefined;
|
|
1406
|
-
|
|
1407
|
-
// Get channels
|
|
1408
|
-
const channels = db.query("SELECT id FROM channels WHERE active = 1").all() as Array<{ id: string }>;
|
|
1409
|
-
|
|
1410
|
-
// Get voice config from webchat channel
|
|
1411
|
-
const voiceConfig = db.query("SELECT voice_enabled, stt_provider, tts_provider FROM channels WHERE id = 'webchat'").get() as { voice_enabled: number; stt_provider: string; tts_provider: string } | undefined;
|
|
1412
|
-
|
|
1413
|
-
// Get code bridge
|
|
1414
|
-
const codeBridge = db.query("SELECT id FROM code_bridge WHERE enabled = 1").all() as Array<{ id: string }>;
|
|
1415
|
-
|
|
1416
|
-
ws.send(JSON.stringify({
|
|
1417
|
-
type: "welcome",
|
|
1418
|
-
sessionId: user?.id || data.sessionId,
|
|
1419
|
-
user: user ? { id: user.id, name: user.name, language: user.language } : null,
|
|
1420
|
-
agent: agent ? { id: agent.id, name: agent.name, provider: agent.provider_id, model: agent.model_id } : null,
|
|
1421
|
-
channels: channels.map(c => c.id),
|
|
1422
|
-
voice: voiceConfig ? {
|
|
1423
|
-
enabled: voiceConfig.voice_enabled === 1,
|
|
1424
|
-
sttProvider: voiceConfig.stt_provider,
|
|
1425
|
-
ttsProvider: voiceConfig.tts_provider
|
|
1426
|
-
} : { enabled: false, sttProvider: null, ttsProvider: null },
|
|
1427
|
-
codeBridge: codeBridge.map(cb => cb.id)
|
|
1428
|
-
} as OutboundMessage));
|
|
1429
|
-
} catch (err) {
|
|
1430
|
-
log.error("Error sending welcome message:", err);
|
|
1431
|
-
}
|
|
1432
|
-
},
|
|
1433
|
-
|
|
1434
|
-
async message(ws, message) {
|
|
1435
|
-
const data = ws.data;
|
|
1436
|
-
|
|
1437
|
-
// Bridge events clients are read-only; ignore any messages they send
|
|
1438
|
-
if (data.sessionId.startsWith("bridge:")) return;
|
|
1439
|
-
|
|
1440
|
-
let msg: InboundMessage;
|
|
1441
|
-
try {
|
|
1442
|
-
msg = JSON.parse(message.toString()) as InboundMessage;
|
|
1443
|
-
} catch {
|
|
1444
|
-
ws.send(JSON.stringify({
|
|
1445
|
-
type: "error",
|
|
1446
|
-
sessionId: data.sessionId,
|
|
1447
|
-
error: "Invalid JSON message",
|
|
1448
|
-
} as OutboundMessage));
|
|
1449
|
-
return;
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
msg.sessionId = msg.sessionId ?? data.sessionId;
|
|
1453
|
-
sessionManager.touch(msg.sessionId);
|
|
1454
|
-
|
|
1455
|
-
if (msg.type === "ping") {
|
|
1456
|
-
ws.send(JSON.stringify({ type: "pong", sessionId: msg.sessionId } as OutboundMessage));
|
|
1457
|
-
return;
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
// Canvas subscribe
|
|
1461
|
-
if (msg.type === "canvas_subscribe") {
|
|
1462
|
-
subscribeCanvas(ws);
|
|
1463
|
-
ws.send(JSON.stringify({
|
|
1464
|
-
type: "canvas:snapshot",
|
|
1465
|
-
data: getCanvasSnapshot(),
|
|
1466
|
-
}));
|
|
1467
|
-
return;
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
// Canvas unsubscribe
|
|
1471
|
-
if (msg.type === "canvas_unsubscribe") {
|
|
1472
|
-
unsubscribeCanvas(ws);
|
|
1473
|
-
return;
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
// Canvas session - handle interactions
|
|
1477
|
-
if (data.sessionId.startsWith("canvas:")) {
|
|
1478
|
-
canvasManager.handleMessage(data.sessionId, message);
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
if (msg.type === "command" || (msg.content && isSlashCommand(msg.content))) {
|
|
1483
|
-
const result = await executeSlashCommand(msg.sessionId, msg.content ?? `/${msg.command}`, ws);
|
|
1484
|
-
if (result) {
|
|
1485
|
-
ws.send(JSON.stringify(result));
|
|
1486
|
-
return;
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
// Logs subscription
|
|
1491
|
-
if (msg.type === "logs_subscribe") {
|
|
1492
|
-
logSubscribers.add(data.sessionId);
|
|
1493
|
-
log.debug(`Session ${data.sessionId} subscribed to logs`);
|
|
1494
|
-
return;
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
if (msg.type === "logs_unsubscribe") {
|
|
1498
|
-
logSubscribers.delete(data.sessionId);
|
|
1499
|
-
log.debug(`Session ${data.sessionId} unsubscribed from logs`);
|
|
1500
|
-
return;
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
// Handle audio messages from WebChat
|
|
1504
|
-
let webchatPreferAudio = false;
|
|
1505
|
-
if (msg.type === "audio" && msg.audio) {
|
|
1506
|
-
log.info(`WebChat audio from session ${msg.sessionId}`);
|
|
1507
|
-
|
|
1508
|
-
const voiceConfig = voiceService.getChannelVoiceConfig("webchat");
|
|
1509
|
-
|
|
1510
|
-
if (!voiceConfig.voiceEnabled) {
|
|
1511
|
-
ws.send(JSON.stringify({
|
|
1512
|
-
type: "error",
|
|
1513
|
-
sessionId: msg.sessionId,
|
|
1514
|
-
error: "Voice input not enabled for this channel"
|
|
1515
|
-
} as OutboundMessage));
|
|
1516
|
-
return;
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
if (!voiceConfig.sttProvider) {
|
|
1520
|
-
ws.send(JSON.stringify({
|
|
1521
|
-
type: "message",
|
|
1522
|
-
sessionId: msg.sessionId,
|
|
1523
|
-
content: "🎙️ Para usar notas de voz, configura el proveedor STT en Configuración > Canales > WebChat (ej: groq-whisper)"
|
|
1524
|
-
} as OutboundMessage));
|
|
1525
|
-
return;
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
ws.send(JSON.stringify({
|
|
1529
|
-
type: "typing",
|
|
1530
|
-
isTyping: true,
|
|
1531
|
-
sessionId: msg.sessionId,
|
|
1532
|
-
} as OutboundMessage));
|
|
1533
|
-
|
|
1534
|
-
try {
|
|
1535
|
-
const audioInput = { type: "base64" as const, data: msg.audio, mimeType: "audio/webm" };
|
|
1536
|
-
const sttProvider = voiceConfig.sttProvider || "groq-whisper";
|
|
1537
|
-
const messageContent = await voiceService.transcribe(audioInput, sttProvider);
|
|
1538
|
-
|
|
1539
|
-
log.info(`📝 Transcribed: ${messageContent.substring(0, 100)}...`);
|
|
1540
|
-
|
|
1541
|
-
webchatPreferAudio = true;
|
|
1542
|
-
|
|
1543
|
-
ws.send(JSON.stringify({
|
|
1544
|
-
type: "message",
|
|
1545
|
-
sessionId: msg.sessionId,
|
|
1546
|
-
content: `🎙️ Transcripción: ${messageContent}`
|
|
1547
|
-
} as OutboundMessage));
|
|
1548
|
-
|
|
1549
|
-
ws.send(JSON.stringify({
|
|
1550
|
-
type: "typing",
|
|
1551
|
-
isTyping: false,
|
|
1552
|
-
sessionId: msg.sessionId,
|
|
1553
|
-
} as OutboundMessage));
|
|
1554
|
-
|
|
1555
|
-
laneQueue.enqueue(msg.sessionId, async (_task, signal) => {
|
|
1556
|
-
if (signal.aborted) {
|
|
1557
|
-
ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: msg.sessionId } as OutboundMessage));
|
|
1558
|
-
ws.send(JSON.stringify({ type: "error", sessionId: msg.sessionId, error: "Task cancelled" } as OutboundMessage));
|
|
1559
|
-
return;
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
try {
|
|
1563
|
-
const unifiedSessionId = msg.sessionId;
|
|
1564
|
-
const messages = [{ role: "user" as const, content: messageContent }];
|
|
1565
|
-
log.info(`Generating response for session ${unifiedSessionId}...`);
|
|
1566
|
-
|
|
1567
|
-
const { userId } = resolveContext({
|
|
1568
|
-
channel: "webchat",
|
|
1569
|
-
channelUserId: msg.sessionId,
|
|
1570
|
-
});
|
|
1571
|
-
|
|
1572
|
-
// Streaming: send tokens as they arrive
|
|
1573
|
-
let streamedContent = "";
|
|
1574
|
-
let messageId = crypto.randomUUID();
|
|
1575
|
-
|
|
1576
|
-
const response = await runner.generate({
|
|
1577
|
-
provider: dbProvider as any,
|
|
1578
|
-
messages,
|
|
1579
|
-
maxTokens: 4096,
|
|
1580
|
-
tools: prepareTools(agent, unifiedSessionId),
|
|
1581
|
-
maxSteps: 15,
|
|
1582
|
-
threadId: unifiedSessionId,
|
|
1583
|
-
userId,
|
|
1584
|
-
onToken: async (token: string) => {
|
|
1585
|
-
if (signal.aborted) return;
|
|
1586
|
-
streamedContent += token;
|
|
1587
|
-
// Send chunk to client
|
|
1588
|
-
ws.send(JSON.stringify({
|
|
1589
|
-
type: "message",
|
|
1590
|
-
id: messageId,
|
|
1591
|
-
sessionId: unifiedSessionId,
|
|
1592
|
-
content: token,
|
|
1593
|
-
isChunk: true,
|
|
1594
|
-
isStep: false,
|
|
1595
|
-
} as OutboundMessage));
|
|
1596
|
-
},
|
|
1597
|
-
onStep: async (step) => {
|
|
1598
|
-
if (signal.aborted) return;
|
|
1599
|
-
if (step.type === "tool_result" && step.message) {
|
|
1600
|
-
try {
|
|
1601
|
-
const result = JSON.parse(step.message);
|
|
1602
|
-
if (result._sendToUser || result.status) {
|
|
1603
|
-
const userMessage = result.message || result.status || step.message;
|
|
1604
|
-
ws.send(JSON.stringify({
|
|
1605
|
-
type: "message",
|
|
1606
|
-
sessionId: unifiedSessionId,
|
|
1607
|
-
content: `📊 ${userMessage}`,
|
|
1608
|
-
isStep: true,
|
|
1609
|
-
} as unknown as OutboundMessage));
|
|
1610
|
-
return;
|
|
1611
|
-
}
|
|
1612
|
-
} catch { }
|
|
1613
|
-
}
|
|
1614
|
-
log.debug(`[TOOL] ${step.type}: ${step.toolName || ""}`);
|
|
1615
|
-
},
|
|
1616
|
-
});
|
|
1617
|
-
|
|
1618
|
-
// Use streamed content from onToken, fallback to response.content
|
|
1619
|
-
const content = streamedContent || response.content?.trim() || "";
|
|
1620
|
-
log.info(`Response sent to session ${unifiedSessionId} (${content.length} chars)`);
|
|
1621
|
-
|
|
1622
|
-
const voiceCfg = voiceService.getChannelVoiceConfig("webchat");
|
|
1623
|
-
const shouldSpeak = webchatPreferAudio;
|
|
1624
|
-
let responseType: "text" | "audio" = "text";
|
|
1625
|
-
let ttsProviderUsed: string | null = null;
|
|
1626
|
-
let ttsMimeType: string | null = null;
|
|
1627
|
-
|
|
1628
|
-
ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: unifiedSessionId } as OutboundMessage));
|
|
1629
|
-
|
|
1630
|
-
// Don't send text message if already streamed (content came via onToken)
|
|
1631
|
-
const alreadyStreamed = streamedContent.length > 0;
|
|
1632
|
-
|
|
1633
|
-
if (content && !alreadyStreamed) {
|
|
1634
|
-
if (shouldSpeak) {
|
|
1635
|
-
if (!voiceCfg.ttsProvider) {
|
|
1636
|
-
ws.send(JSON.stringify({
|
|
1637
|
-
type: "message",
|
|
1638
|
-
sessionId: unifiedSessionId,
|
|
1639
|
-
content: `${content}\n\n🔊 Para recibir respuestas en audio, configura el proveedor TTS en Configuración > Canales > WebChat (ej: elevenlabs)`,
|
|
1640
|
-
isStep: false,
|
|
1641
|
-
} as OutboundMessage));
|
|
1642
|
-
} else {
|
|
1643
|
-
try {
|
|
1644
|
-
log.info(`🔊 TTS enabled, synthesizing audio for WebChat...`);
|
|
1645
|
-
const audioOutput = await voiceService.speak(content, voiceCfg.ttsProvider, voiceCfg.ttsVoiceId || undefined);
|
|
1646
|
-
ttsProviderUsed = voiceCfg.ttsProvider;
|
|
1647
|
-
ttsMimeType = audioOutput.mimeType;
|
|
1648
|
-
responseType = "audio";
|
|
1649
|
-
const base64Audio = (audioOutput.data as Buffer).toString("base64");
|
|
1650
|
-
ws.send(JSON.stringify({
|
|
1651
|
-
type: "audio",
|
|
1652
|
-
sessionId: unifiedSessionId,
|
|
1653
|
-
audio: base64Audio,
|
|
1654
|
-
content,
|
|
1655
|
-
mimeType: audioOutput.mimeType,
|
|
1656
|
-
} as OutboundMessage));
|
|
1657
|
-
} catch (ttsError) {
|
|
1658
|
-
log.error(`TTS failed: ${(ttsError as Error).message}), sending text instead`);
|
|
1659
|
-
ws.send(JSON.stringify({ type: "message", sessionId: unifiedSessionId, content, isStep: false } as OutboundMessage));
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
} else {
|
|
1663
|
-
ws.send(JSON.stringify({ type: "message", sessionId: unifiedSessionId, content, isStep: false } as OutboundMessage));
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
} catch (error) {
|
|
1667
|
-
ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: msg.sessionId } as OutboundMessage));
|
|
1668
|
-
ws.send(JSON.stringify({
|
|
1669
|
-
type: "error",
|
|
1670
|
-
sessionId: msg.sessionId,
|
|
1671
|
-
error: (error as Error).message,
|
|
1672
|
-
} as OutboundMessage));
|
|
1673
|
-
log.error(`Error for session ${msg.sessionId}: ${(error as Error).message}`);
|
|
1674
|
-
}
|
|
1675
|
-
});
|
|
1676
|
-
} catch (error) {
|
|
1677
|
-
ws.send(JSON.stringify({
|
|
1678
|
-
type: "typing",
|
|
1679
|
-
isTyping: false,
|
|
1680
|
-
sessionId: msg.sessionId,
|
|
1681
|
-
} as OutboundMessage));
|
|
1682
|
-
ws.send(JSON.stringify({
|
|
1683
|
-
type: "error",
|
|
1684
|
-
sessionId: msg.sessionId,
|
|
1685
|
-
error: `Transcription failed: ${(error as Error).message}`
|
|
1686
|
-
} as OutboundMessage));
|
|
1687
|
-
}
|
|
1688
|
-
return;
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
if (msg.type === "message" && msg.content) {
|
|
1692
|
-
log.info(`WebChat message from session ${msg.sessionId}: ${msg.content.substring(0, 100)}`);
|
|
1693
|
-
|
|
1694
|
-
// FIX 6 — typing indicator inmediato ANTES de encolar
|
|
1695
|
-
// El usuario ve "escribiendo..." de inmediato, no después del queue
|
|
1696
|
-
ws.send(JSON.stringify({
|
|
1697
|
-
type: "typing",
|
|
1698
|
-
isTyping: true,
|
|
1699
|
-
sessionId: msg.sessionId,
|
|
1700
|
-
} as OutboundMessage));
|
|
1701
|
-
|
|
1702
|
-
laneQueue.enqueue(msg.sessionId, async (_task, signal) => {
|
|
1703
|
-
if (signal.aborted) {
|
|
1704
|
-
ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: msg.sessionId } as OutboundMessage));
|
|
1705
|
-
ws.send(JSON.stringify({ type: "error", sessionId: msg.sessionId, error: "Task cancelled" } as OutboundMessage));
|
|
1706
|
-
return;
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
try {
|
|
1710
|
-
const unifiedSessionId = msg.sessionId;
|
|
1711
|
-
const messages = [{ role: "user" as const, content: msg.content }];
|
|
1712
|
-
log.info(`Generating response for session ${unifiedSessionId}...`);
|
|
1713
|
-
|
|
1714
|
-
const { userId } = resolveContext({
|
|
1715
|
-
channel: "webchat",
|
|
1716
|
-
channelUserId: msg.sessionId,
|
|
1717
|
-
});
|
|
1718
|
-
|
|
1719
|
-
// Streaming: send tokens as they arrive
|
|
1720
|
-
let streamedContent = "";
|
|
1721
|
-
let messageId = crypto.randomUUID();
|
|
1722
|
-
|
|
1723
|
-
const response = await runner.generate({
|
|
1724
|
-
provider: dbProvider as any,
|
|
1725
|
-
messages,
|
|
1726
|
-
maxTokens: 4096,
|
|
1727
|
-
tools: prepareTools(agent, unifiedSessionId),
|
|
1728
|
-
maxSteps: 15,
|
|
1729
|
-
threadId: unifiedSessionId,
|
|
1730
|
-
userId,
|
|
1731
|
-
onToken: async (token: string) => {
|
|
1732
|
-
if (signal.aborted) return;
|
|
1733
|
-
streamedContent += token;
|
|
1734
|
-
// Send chunk to client
|
|
1735
|
-
ws.send(JSON.stringify({
|
|
1736
|
-
type: "message",
|
|
1737
|
-
id: messageId,
|
|
1738
|
-
sessionId: unifiedSessionId,
|
|
1739
|
-
content: token,
|
|
1740
|
-
isChunk: true,
|
|
1741
|
-
isStep: false,
|
|
1742
|
-
} as OutboundMessage));
|
|
1743
|
-
},
|
|
1744
|
-
onStep: async (step) => {
|
|
1745
|
-
if (signal.aborted) return;
|
|
1746
|
-
|
|
1747
|
-
// Para tool_result, verificar si es un mensaje de progreso
|
|
1748
|
-
if (step.type === "tool_result" && step.message) {
|
|
1749
|
-
try {
|
|
1750
|
-
const result = JSON.parse(step.message);
|
|
1751
|
-
if (result._sendToUser || result.status) {
|
|
1752
|
-
const userMessage = result.message || result.status || step.message;
|
|
1753
|
-
ws.send(JSON.stringify({
|
|
1754
|
-
type: "message",
|
|
1755
|
-
sessionId: unifiedSessionId,
|
|
1756
|
-
content: `📊 ${userMessage}`,
|
|
1757
|
-
isStep: true,
|
|
1758
|
-
} as unknown as OutboundMessage));
|
|
1759
|
-
return;
|
|
1760
|
-
}
|
|
1761
|
-
} catch {
|
|
1762
|
-
// No es JSON de progreso
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
log.debug(`[TOOL] ${step.type}: ${step.toolName || ""}`);
|
|
1767
|
-
},
|
|
1768
|
-
});
|
|
1769
|
-
|
|
1770
|
-
// Use streamed content from onToken, fallback to response.content
|
|
1771
|
-
const content = streamedContent || response.content?.trim() || "";
|
|
1772
|
-
log.info(`Response sent to session ${unifiedSessionId} (${content.length} chars)`);
|
|
1773
|
-
|
|
1774
|
-
const voiceConfig = voiceService.getChannelVoiceConfig("webchat");
|
|
1775
|
-
const shouldSpeak = webchatPreferAudio;
|
|
1776
|
-
let responseType: "text" | "audio" = "text";
|
|
1777
|
-
let ttsProviderUsed: string | null = null;
|
|
1778
|
-
let ttsMimeType: string | null = null;
|
|
1779
|
-
|
|
1780
|
-
ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: unifiedSessionId } as OutboundMessage));
|
|
1781
|
-
|
|
1782
|
-
// Don't send text message if already streamed (content came via onToken)
|
|
1783
|
-
const alreadyStreamed = streamedContent.length > 0;
|
|
1784
|
-
|
|
1785
|
-
if (content && !alreadyStreamed) {
|
|
1786
|
-
if (shouldSpeak) {
|
|
1787
|
-
if (!voiceConfig.ttsProvider) {
|
|
1788
|
-
ws.send(JSON.stringify({
|
|
1789
|
-
type: "message",
|
|
1790
|
-
sessionId: unifiedSessionId,
|
|
1791
|
-
content: `${content}\n\n🔊 Para recibir respuestas en audio, configura el proveedor TTS en Configuración > Canales > WebChat (ej: elevenlabs)`,
|
|
1792
|
-
isStep: false
|
|
1793
|
-
} as OutboundMessage));
|
|
1794
|
-
} else {
|
|
1795
|
-
try {
|
|
1796
|
-
log.info(`🔊 TTS enabled, synthesizing audio for WebChat...`);
|
|
1797
|
-
const audioOutput = await voiceService.speak(content, voiceConfig.ttsProvider, voiceConfig.ttsVoiceId || undefined);
|
|
1798
|
-
ttsProviderUsed = voiceConfig.ttsProvider;
|
|
1799
|
-
ttsMimeType = audioOutput.mimeType;
|
|
1800
|
-
responseType = "audio";
|
|
1801
|
-
|
|
1802
|
-
const base64Audio = (audioOutput.data as Buffer).toString("base64");
|
|
1803
|
-
|
|
1804
|
-
ws.send(JSON.stringify({
|
|
1805
|
-
type: "audio",
|
|
1806
|
-
sessionId: unifiedSessionId,
|
|
1807
|
-
audio: base64Audio,
|
|
1808
|
-
content,
|
|
1809
|
-
mimeType: audioOutput.mimeType,
|
|
1810
|
-
} as OutboundMessage));
|
|
1811
|
-
} catch (ttsError) {
|
|
1812
|
-
log.error(`TTS failed: ${(ttsError as Error).message}), sending text instead`);
|
|
1813
|
-
ws.send(JSON.stringify({ type: "message", sessionId: unifiedSessionId, content, isStep: false } as OutboundMessage));
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
} else {
|
|
1817
|
-
ws.send(JSON.stringify({ type: "message", sessionId: unifiedSessionId, content, isStep: false } as OutboundMessage));
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
} catch (error) {
|
|
1821
|
-
const unifiedSessionId = msg.sessionId;
|
|
1822
|
-
// Detener typing aunque falle — nunca dejar el spinner infinito
|
|
1823
|
-
ws.send(JSON.stringify({ type: "typing", isTyping: false, sessionId: unifiedSessionId } as OutboundMessage));
|
|
1824
|
-
ws.send(JSON.stringify({
|
|
1825
|
-
type: "error",
|
|
1826
|
-
sessionId: unifiedSessionId,
|
|
1827
|
-
error: (error as Error).message,
|
|
1828
|
-
} as OutboundMessage));
|
|
1829
|
-
log.error(`Error for session ${unifiedSessionId}: ${(error as Error).message}`);
|
|
1830
|
-
}
|
|
1831
|
-
});
|
|
1832
|
-
|
|
1833
|
-
return;
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
ws.send(JSON.stringify({
|
|
1837
|
-
type: "error",
|
|
1838
|
-
sessionId: msg.sessionId,
|
|
1839
|
-
error: "Unknown message type",
|
|
1840
|
-
} as OutboundMessage));
|
|
1841
|
-
},
|
|
1842
|
-
|
|
1843
|
-
close(ws) {
|
|
1844
|
-
const data = ws.data;
|
|
1845
|
-
const isCanvas = data.sessionId.startsWith("canvas:");
|
|
1846
|
-
const isBridge = data.sessionId.startsWith("bridge:");
|
|
1847
|
-
|
|
1848
|
-
if (isBridge) {
|
|
1849
|
-
unsubscribeBridge(ws as any);
|
|
1850
|
-
return;
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
if (isCanvas) {
|
|
1854
|
-
canvasManager.unregisterSession(data.sessionId);
|
|
1855
|
-
unsubscribeCanvas(ws as any);
|
|
1856
|
-
return;
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
log.debug(`WebSocket disconnected: ${data.sessionId}`);
|
|
1860
|
-
logSubscribers.delete(data.sessionId);
|
|
1861
|
-
sessionManager.delete(data.sessionId);
|
|
1862
|
-
laneQueue.cancel(data.sessionId);
|
|
1863
|
-
|
|
1864
|
-
const channel = channelManager?.getChannel("webchat") as any;
|
|
1865
|
-
if (channel?.unregisterConnection) channel.unregisterConnection(data.sessionId);
|
|
1866
|
-
},
|
|
1867
|
-
},
|
|
1868
|
-
});
|
|
1869
|
-
|
|
1870
|
-
onLogEntry((entry) => {
|
|
1871
|
-
if (logSubscribers.size === 0) return;
|
|
1872
|
-
|
|
1873
|
-
const payload = JSON.stringify({
|
|
1874
|
-
type: "log",
|
|
1875
|
-
sessionId: entry.meta?.sessionId || "system",
|
|
1876
|
-
logEntry: entry,
|
|
1877
|
-
});
|
|
1878
|
-
|
|
1879
|
-
for (const sessionId of logSubscribers) {
|
|
1880
|
-
const session = sessionManager.get(sessionId);
|
|
1881
|
-
if (session?.ws && session.ws.readyState === 1) {
|
|
1882
|
-
try {
|
|
1883
|
-
session.ws.send(payload);
|
|
1884
|
-
} catch {
|
|
1885
|
-
logSubscribers.delete(sessionId);
|
|
1886
|
-
}
|
|
1887
|
-
} else {
|
|
1888
|
-
logSubscribers.delete(sessionId);
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
});
|
|
1892
|
-
|
|
1893
|
-
log.info(`Gateway started successfully`);
|
|
1894
|
-
|
|
1895
|
-
// Check if running as child process in dev mode (parent handles browser open)
|
|
1896
|
-
const isGatewayChild = process.env.HIVE_GATEWAY_CHILD === "1";
|
|
1897
|
-
|
|
1898
|
-
// Print URLs based on mode
|
|
1899
|
-
if (isDev) {
|
|
1900
|
-
// In development: UI is served by Vite on port 5173
|
|
1901
|
-
log.info(`[gateway] API: http://${host}:${port}`);
|
|
1902
|
-
log.info(`[gateway] WebSocket: ws://${host}:${port}/ws`);
|
|
1903
|
-
log.info(`[gateway] Canvas: ws://${host}:${port}/canvas`);
|
|
1904
|
-
log.info(`[gateway] Modo: desarrollo`);
|
|
1905
|
-
if (!isGatewayChild) {
|
|
1906
|
-
log.info(`🐝 Administra tu Hive aquí: http://localhost:5173`);
|
|
1907
|
-
}
|
|
1908
|
-
} else {
|
|
1909
|
-
// In production: Gateway serves UI from dist/
|
|
1910
|
-
// Check if this is first-run setup mode
|
|
1911
|
-
const isSetupMode = !existsSync(getDbPathLazy());
|
|
1912
|
-
const baseUrl = `http://${host}:${port}`;
|
|
1913
|
-
const uiUrl = isSetupMode ? `${baseUrl}/setup` : `${baseUrl}/ui`;
|
|
1914
|
-
|
|
1915
|
-
log.info(`[gateway] UI: ${uiUrl}`);
|
|
1916
|
-
log.info(`[gateway] API: http://${host}:${port}`);
|
|
1917
|
-
log.info(`[gateway] WebSocket: ws://${host}:${port}/ws`);
|
|
1918
|
-
log.info(`[gateway] Canvas: ws://${host}:${port}/canvas`);
|
|
1919
|
-
|
|
1920
|
-
log.info(isSetupMode ? `🎉 Primer arranque — abriendo wizard de configuración...` : `🐝 Administra tu Hive aquí: ${uiUrl}`);
|
|
1921
|
-
|
|
1922
|
-
// Always open browser on startup (setup and normal mode).
|
|
1923
|
-
// Set NO_BROWSER=1 to skip in headless/server environments.
|
|
1924
|
-
if (!process.env.NO_BROWSER) {
|
|
1925
|
-
try {
|
|
1926
|
-
const platform = process.platform;
|
|
1927
|
-
let shellCmd: string;
|
|
1928
|
-
if (platform === "win32") {
|
|
1929
|
-
shellCmd = `start "" "${uiUrl}"`;
|
|
1930
|
-
} else if (platform === "darwin") {
|
|
1931
|
-
shellCmd = `open "${uiUrl}"`;
|
|
1932
|
-
} else {
|
|
1933
|
-
// Linux: gio open first (GNOME/Wayland native), then xdg-open fallbacks
|
|
1934
|
-
shellCmd = `gio open "${uiUrl}" 2>/dev/null || xdg-open "${uiUrl}" 2>/dev/null || sensible-browser "${uiUrl}" 2>/dev/null || x-www-browser "${uiUrl}" 2>/dev/null || true`;
|
|
1935
|
-
}
|
|
1936
|
-
const shell = platform === "win32" ? "cmd" : "/bin/sh";
|
|
1937
|
-
const shellArg = platform === "win32" ? "/c" : "-c";
|
|
1938
|
-
// Use Bun.spawn (native Bun API) for reliable detached subprocess
|
|
1939
|
-
const proc = Bun.spawn([shell, shellArg, shellCmd], {
|
|
1940
|
-
stdout: "ignore",
|
|
1941
|
-
stderr: "ignore",
|
|
1942
|
-
stdin: "ignore",
|
|
1943
|
-
});
|
|
1944
|
-
proc.unref();
|
|
1945
|
-
} catch (err) {
|
|
1946
|
-
log.warn(`Could not open browser: ${(err as Error).message}`);
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
}
|
|
1950
|
-
if (!gatewaySetupMode) log.info(`Channels: ${channelManager.listChannels().map((c) => c.name).join(", ") || "none"}`);
|
|
1951
|
-
|
|
1952
|
-
// FIX 7 — SIGTERM desconecta MCP limpiamente antes de cerrar
|
|
1953
|
-
process.on("SIGTERM", async () => {
|
|
1954
|
-
log.info("Received SIGTERM, shutting down gracefully...");
|
|
1955
|
-
watchers.forEach((close) => close());
|
|
1956
|
-
const mcp = agent?.getMCPManager();
|
|
1957
|
-
if (mcp) {
|
|
1958
|
-
log.info("Disconnecting MCP servers...");
|
|
1959
|
-
await mcp.disconnectAll().catch(() => { });
|
|
1960
|
-
}
|
|
1961
|
-
if (channelManager) await channelManager.stopAll();
|
|
1962
|
-
server.stop();
|
|
1963
|
-
try { unlinkSync(pidFile); } catch { }
|
|
1964
|
-
process.exit(0);
|
|
1965
|
-
});
|
|
1966
|
-
|
|
1967
|
-
process.on("SIGHUP", async () => {
|
|
1968
|
-
log.info("Received SIGHUP, reloading configuration...");
|
|
1969
|
-
try {
|
|
1970
|
-
const newConfig = await loadConfig();
|
|
1971
|
-
await agent.updateConfig(newConfig);
|
|
1972
|
-
await agent.reload();
|
|
1973
|
-
log.info("Configuration reloaded successfully");
|
|
1974
|
-
} catch (error) {
|
|
1975
|
-
log.error(`Failed to reload configuration: ${(error as Error).message}`);
|
|
1976
|
-
}
|
|
1977
|
-
});
|
|
1978
|
-
}
|