ruflo 3.42.3 → 3.42.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (479) hide show
  1. package/README.md +422 -422
  2. package/bin/ruflo.js +77 -77
  3. package/package.json +117 -117
  4. package/src/chat-ui/Dockerfile +25 -25
  5. package/src/chat-ui/patch-mcp-url-safety.sh +28 -28
  6. package/src/config/config.example.json +76 -76
  7. package/src/mcp-bridge/Dockerfile +47 -47
  8. package/src/mcp-bridge/index.js +1780 -1780
  9. package/src/mcp-bridge/mcp-stdio-kernel.js +159 -159
  10. package/src/mcp-bridge/package.json +17 -17
  11. package/src/mcp-bridge/test-harness.js +470 -470
  12. package/src/mcp-bridge/test-runtime-security.mjs +208 -208
  13. package/src/mcp-bridge/test-security-lock.js +121 -121
  14. package/src/nginx/Dockerfile +10 -10
  15. package/src/nginx/nginx.conf +67 -67
  16. package/src/nginx/static/favicon-dark.svg +4 -4
  17. package/src/nginx/static/favicon.svg +4 -4
  18. package/src/nginx/static/icon.svg +5 -5
  19. package/src/nginx/static/logo.svg +9 -9
  20. package/src/nginx/static/manifest.json +22 -22
  21. package/src/nginx/static/welcome.js +184 -184
  22. package/src/ruvocal/.claude/skills/add-model-descriptions/SKILL.md +73 -73
  23. package/src/ruvocal/.devcontainer/Dockerfile +9 -9
  24. package/src/ruvocal/.devcontainer/devcontainer.json +36 -36
  25. package/src/ruvocal/.dockerignore +16 -16
  26. package/src/ruvocal/.eslintignore +13 -13
  27. package/src/ruvocal/.eslintrc.cjs +45 -45
  28. package/src/ruvocal/.gcloudignore +18 -18
  29. package/src/ruvocal/.github/ISSUE_TEMPLATE/bug-report--chat-ui-.md +43 -43
  30. package/src/ruvocal/.github/ISSUE_TEMPLATE/config-support.md +9 -9
  31. package/src/ruvocal/.github/ISSUE_TEMPLATE/feature-request--chat-ui-.md +17 -17
  32. package/src/ruvocal/.github/ISSUE_TEMPLATE/huggingchat.md +11 -11
  33. package/src/ruvocal/.github/release.yml +16 -16
  34. package/src/ruvocal/.github/workflows/build-docs.yml +18 -18
  35. package/src/ruvocal/.github/workflows/build-image.yml +142 -142
  36. package/src/ruvocal/.github/workflows/build-pr-docs.yml +20 -20
  37. package/src/ruvocal/.github/workflows/deploy-dev.yml +63 -63
  38. package/src/ruvocal/.github/workflows/deploy-prod.yml +78 -78
  39. package/src/ruvocal/.github/workflows/lint-and-test.yml +84 -84
  40. package/src/ruvocal/.github/workflows/slugify.yaml +72 -72
  41. package/src/ruvocal/.github/workflows/trufflehog.yml +17 -17
  42. package/src/ruvocal/.github/workflows/upload-pr-documentation.yml +16 -16
  43. package/src/ruvocal/.husky/lint-stage-config.js +4 -4
  44. package/src/ruvocal/.husky/pre-commit +2 -2
  45. package/src/ruvocal/.prettierignore +14 -14
  46. package/src/ruvocal/.prettierrc +7 -7
  47. package/src/ruvocal/CLAUDE.md +126 -126
  48. package/src/ruvocal/Dockerfile +96 -96
  49. package/src/ruvocal/LICENSE +202 -202
  50. package/src/ruvocal/PRIVACY.md +41 -41
  51. package/src/ruvocal/README.md +164 -164
  52. package/src/ruvocal/chart/Chart.yaml +5 -5
  53. package/src/ruvocal/chart/env/dev.yaml +260 -260
  54. package/src/ruvocal/chart/env/prod.yaml +273 -273
  55. package/src/ruvocal/chart/templates/_helpers.tpl +22 -22
  56. package/src/ruvocal/chart/templates/config.yaml +10 -10
  57. package/src/ruvocal/chart/templates/deployment.yaml +81 -81
  58. package/src/ruvocal/chart/templates/hpa.yaml +45 -45
  59. package/src/ruvocal/chart/templates/infisical.yaml +24 -24
  60. package/src/ruvocal/chart/templates/ingress-internal.yaml +32 -32
  61. package/src/ruvocal/chart/templates/ingress.yaml +32 -32
  62. package/src/ruvocal/chart/templates/network-policy.yaml +36 -36
  63. package/src/ruvocal/chart/templates/service-account.yaml +13 -13
  64. package/src/ruvocal/chart/templates/service-monitor.yaml +17 -17
  65. package/src/ruvocal/chart/templates/service.yaml +21 -21
  66. package/src/ruvocal/chart/values.yaml +73 -73
  67. package/src/ruvocal/cloudbuild.yaml +68 -68
  68. package/src/ruvocal/config/branding.env.example +19 -19
  69. package/src/ruvocal/docker-compose.yml +21 -21
  70. package/src/ruvocal/docs/adr/ADR-029-HUGGINGFACE-CHAT-UI-CLOUD-RUN.md +1236 -1236
  71. package/src/ruvocal/docs/adr/ADR-033-RUVECTOR-RUFLO-MCP-INTEGRATION.md +111 -111
  72. package/src/ruvocal/docs/adr/ADR-034-OPTIONAL-MCP-BACKENDS.md +117 -117
  73. package/src/ruvocal/docs/adr/ADR-035-MCP-TOOL-GROUPS.md +186 -186
  74. package/src/ruvocal/docs/adr/ADR-037-AUTOPILOT-CHAT-MODE.md +1500 -1500
  75. package/src/ruvocal/docs/adr/ADR-038-RUVOCAL-FORK.md +286 -286
  76. package/src/ruvocal/docs/source/_toctree.yml +30 -30
  77. package/src/ruvocal/docs/source/configuration/common-issues.md +38 -38
  78. package/src/ruvocal/docs/source/configuration/llm-router.md +105 -105
  79. package/src/ruvocal/docs/source/configuration/mcp-tools.md +84 -84
  80. package/src/ruvocal/docs/source/configuration/metrics.md +9 -9
  81. package/src/ruvocal/docs/source/configuration/open-id.md +57 -57
  82. package/src/ruvocal/docs/source/configuration/overview.md +89 -89
  83. package/src/ruvocal/docs/source/configuration/theming.md +20 -20
  84. package/src/ruvocal/docs/source/developing/architecture.md +48 -48
  85. package/src/ruvocal/docs/source/index.md +53 -53
  86. package/src/ruvocal/docs/source/installation/docker.md +43 -43
  87. package/src/ruvocal/docs/source/installation/helm.md +43 -43
  88. package/src/ruvocal/docs/source/installation/local.md +62 -62
  89. package/src/ruvocal/entrypoint.sh +18 -18
  90. package/src/ruvocal/mcp-bridge/Dockerfile +47 -47
  91. package/src/ruvocal/mcp-bridge/cloudbuild.yaml +49 -49
  92. package/src/ruvocal/mcp-bridge/index.js +1995 -1995
  93. package/src/ruvocal/mcp-bridge/mcp-stdio-kernel.js +159 -159
  94. package/src/ruvocal/mcp-bridge/package-lock.json +762 -762
  95. package/src/ruvocal/mcp-bridge/package.json +17 -17
  96. package/src/ruvocal/mcp-bridge/test-harness.js +470 -470
  97. package/src/ruvocal/package-lock.json +11741 -11741
  98. package/src/ruvocal/package.json +121 -121
  99. package/src/ruvocal/postcss.config.js +6 -6
  100. package/src/ruvocal/rvf.manifest.json +204 -204
  101. package/src/ruvocal/scripts/config.ts +64 -64
  102. package/src/ruvocal/scripts/generate-welcome.mjs +181 -181
  103. package/src/ruvocal/scripts/populate.ts +288 -288
  104. package/src/ruvocal/scripts/samples.txt +194 -194
  105. package/src/ruvocal/scripts/setups/vitest-setup-server.ts +44 -44
  106. package/src/ruvocal/scripts/updateLocalEnv.ts +48 -48
  107. package/src/ruvocal/src/ambient.d.ts +7 -7
  108. package/src/ruvocal/src/app.d.ts +29 -29
  109. package/src/ruvocal/src/app.html +53 -53
  110. package/src/ruvocal/src/hooks.server.ts +32 -32
  111. package/src/ruvocal/src/hooks.ts +6 -6
  112. package/src/ruvocal/src/lib/APIClient.ts +148 -148
  113. package/src/ruvocal/src/lib/actions/clickOutside.ts +18 -18
  114. package/src/ruvocal/src/lib/actions/snapScrollToBottom.ts +346 -346
  115. package/src/ruvocal/src/lib/buildPrompt.ts +33 -33
  116. package/src/ruvocal/src/lib/components/AnnouncementBanner.svelte +20 -20
  117. package/src/ruvocal/src/lib/components/BackgroundGenerationPoller.svelte +168 -168
  118. package/src/ruvocal/src/lib/components/CodeBlock.svelte +73 -73
  119. package/src/ruvocal/src/lib/components/CopyToClipBoardBtn.svelte +92 -92
  120. package/src/ruvocal/src/lib/components/DeleteConversationModal.svelte +75 -75
  121. package/src/ruvocal/src/lib/components/EditConversationModal.svelte +100 -100
  122. package/src/ruvocal/src/lib/components/ExpandNavigation.svelte +22 -22
  123. package/src/ruvocal/src/lib/components/FoundationBackground.svelte +242 -242
  124. package/src/ruvocal/src/lib/components/HoverTooltip.svelte +44 -44
  125. package/src/ruvocal/src/lib/components/HtmlPreviewModal.svelte +143 -143
  126. package/src/ruvocal/src/lib/components/InfiniteScroll.svelte +50 -50
  127. package/src/ruvocal/src/lib/components/MobileNav.svelte +300 -300
  128. package/src/ruvocal/src/lib/components/Modal.svelte +115 -115
  129. package/src/ruvocal/src/lib/components/ModelCardMetadata.svelte +71 -71
  130. package/src/ruvocal/src/lib/components/NavConversationItem.svelte +151 -151
  131. package/src/ruvocal/src/lib/components/NavMenu.svelte +313 -313
  132. package/src/ruvocal/src/lib/components/Pagination.svelte +97 -97
  133. package/src/ruvocal/src/lib/components/PaginationArrow.svelte +27 -27
  134. package/src/ruvocal/src/lib/components/Portal.svelte +24 -24
  135. package/src/ruvocal/src/lib/components/RetryBtn.svelte +18 -18
  136. package/src/ruvocal/src/lib/components/RuFloUniverse.svelte +185 -185
  137. package/src/ruvocal/src/lib/components/RufloHelpModal.svelte +411 -411
  138. package/src/ruvocal/src/lib/components/ScrollToBottomBtn.svelte +47 -47
  139. package/src/ruvocal/src/lib/components/ScrollToPreviousBtn.svelte +77 -77
  140. package/src/ruvocal/src/lib/components/ShareConversationModal.svelte +182 -182
  141. package/src/ruvocal/src/lib/components/StopGeneratingBtn.svelte +69 -69
  142. package/src/ruvocal/src/lib/components/SubscribeModal.svelte +87 -87
  143. package/src/ruvocal/src/lib/components/Switch.svelte +36 -36
  144. package/src/ruvocal/src/lib/components/SystemPromptModal.svelte +44 -44
  145. package/src/ruvocal/src/lib/components/Toast.svelte +27 -27
  146. package/src/ruvocal/src/lib/components/Tooltip.svelte +30 -30
  147. package/src/ruvocal/src/lib/components/WelcomeModal.svelte +46 -46
  148. package/src/ruvocal/src/lib/components/chat/Alternatives.svelte +77 -77
  149. package/src/ruvocal/src/lib/components/chat/BlockWrapper.svelte +72 -72
  150. package/src/ruvocal/src/lib/components/chat/ChatInput.svelte +490 -490
  151. package/src/ruvocal/src/lib/components/chat/ChatIntroduction.svelte +123 -123
  152. package/src/ruvocal/src/lib/components/chat/ChatMessage.svelte +548 -548
  153. package/src/ruvocal/src/lib/components/chat/ChatWindow.svelte +1057 -1057
  154. package/src/ruvocal/src/lib/components/chat/FileDropzone.svelte +92 -92
  155. package/src/ruvocal/src/lib/components/chat/ImageLightbox.svelte +66 -66
  156. package/src/ruvocal/src/lib/components/chat/MarkdownBlock.svelte +23 -23
  157. package/src/ruvocal/src/lib/components/chat/MarkdownRenderer.svelte +69 -69
  158. package/src/ruvocal/src/lib/components/chat/MarkdownRenderer.svelte.test.ts +58 -58
  159. package/src/ruvocal/src/lib/components/chat/MessageAvatar.svelte +103 -103
  160. package/src/ruvocal/src/lib/components/chat/ModelSwitch.svelte +64 -64
  161. package/src/ruvocal/src/lib/components/chat/OpenReasoningResults.svelte +81 -81
  162. package/src/ruvocal/src/lib/components/chat/TaskGroup.svelte +88 -88
  163. package/src/ruvocal/src/lib/components/chat/ToolUpdate.svelte +273 -273
  164. package/src/ruvocal/src/lib/components/chat/UploadedFile.svelte +253 -253
  165. package/src/ruvocal/src/lib/components/chat/UrlFetchModal.svelte +203 -203
  166. package/src/ruvocal/src/lib/components/chat/VoiceRecorder.svelte +214 -214
  167. package/src/ruvocal/src/lib/components/icons/IconBurger.svelte +20 -20
  168. package/src/ruvocal/src/lib/components/icons/IconCheap.svelte +20 -20
  169. package/src/ruvocal/src/lib/components/icons/IconChevron.svelte +24 -24
  170. package/src/ruvocal/src/lib/components/icons/IconDazzled.svelte +40 -40
  171. package/src/ruvocal/src/lib/components/icons/IconFast.svelte +20 -20
  172. package/src/ruvocal/src/lib/components/icons/IconLoading.svelte +22 -22
  173. package/src/ruvocal/src/lib/components/icons/IconMCP.svelte +28 -28
  174. package/src/ruvocal/src/lib/components/icons/IconMoon.svelte +21 -21
  175. package/src/ruvocal/src/lib/components/icons/IconNew.svelte +20 -20
  176. package/src/ruvocal/src/lib/components/icons/IconOmni.svelte +90 -90
  177. package/src/ruvocal/src/lib/components/icons/IconPaperclip.svelte +24 -24
  178. package/src/ruvocal/src/lib/components/icons/IconPro.svelte +37 -37
  179. package/src/ruvocal/src/lib/components/icons/IconShare.svelte +21 -21
  180. package/src/ruvocal/src/lib/components/icons/IconSun.svelte +93 -93
  181. package/src/ruvocal/src/lib/components/icons/Logo.svelte +68 -68
  182. package/src/ruvocal/src/lib/components/icons/LogoHuggingFaceBorderless.svelte +54 -54
  183. package/src/ruvocal/src/lib/components/mcp/AddServerForm.svelte +250 -250
  184. package/src/ruvocal/src/lib/components/mcp/MCPServerManager.svelte +185 -185
  185. package/src/ruvocal/src/lib/components/mcp/ServerCard.svelte +203 -203
  186. package/src/ruvocal/src/lib/components/players/AudioPlayer.svelte +82 -82
  187. package/src/ruvocal/src/lib/components/voice/AudioWaveform.svelte +96 -96
  188. package/src/ruvocal/src/lib/components/wasm/GalleryPanel.svelte +357 -357
  189. package/src/ruvocal/src/lib/constants/mcpExamples.ts +114 -114
  190. package/src/ruvocal/src/lib/constants/mime.ts +11 -11
  191. package/src/ruvocal/src/lib/constants/pagination.ts +1 -1
  192. package/src/ruvocal/src/lib/constants/publicSepToken.ts +1 -1
  193. package/src/ruvocal/src/lib/constants/routerExamples.ts +133 -133
  194. package/src/ruvocal/src/lib/constants/rvagentPresets.ts +206 -206
  195. package/src/ruvocal/src/lib/createShareLink.ts +27 -27
  196. package/src/ruvocal/src/lib/jobs/refresh-conversation-stats.ts +297 -297
  197. package/src/ruvocal/src/lib/migrations/lock.ts +56 -56
  198. package/src/ruvocal/src/lib/migrations/migrations.spec.ts +74 -74
  199. package/src/ruvocal/src/lib/migrations/migrations.ts +109 -109
  200. package/src/ruvocal/src/lib/migrations/routines/01-update-search-assistants.ts +50 -50
  201. package/src/ruvocal/src/lib/migrations/routines/02-update-assistants-models.ts +48 -48
  202. package/src/ruvocal/src/lib/migrations/routines/04-update-message-updates.ts +151 -151
  203. package/src/ruvocal/src/lib/migrations/routines/05-update-message-files.ts +56 -56
  204. package/src/ruvocal/src/lib/migrations/routines/06-trim-message-updates.ts +56 -56
  205. package/src/ruvocal/src/lib/migrations/routines/08-update-featured-to-review.ts +32 -32
  206. package/src/ruvocal/src/lib/migrations/routines/09-delete-empty-conversations.spec.ts +214 -214
  207. package/src/ruvocal/src/lib/migrations/routines/09-delete-empty-conversations.ts +88 -88
  208. package/src/ruvocal/src/lib/migrations/routines/10-update-reports-assistantid.ts +29 -29
  209. package/src/ruvocal/src/lib/migrations/routines/index.ts +15 -15
  210. package/src/ruvocal/src/lib/server/__tests__/conversation-stop-generating.spec.ts +103 -103
  211. package/src/ruvocal/src/lib/server/abortRegistry.ts +57 -57
  212. package/src/ruvocal/src/lib/server/abortedGenerations.ts +43 -43
  213. package/src/ruvocal/src/lib/server/adminToken.ts +62 -62
  214. package/src/ruvocal/src/lib/server/api/__tests__/conversations-id.spec.ts +296 -296
  215. package/src/ruvocal/src/lib/server/api/__tests__/conversations-message.spec.ts +216 -216
  216. package/src/ruvocal/src/lib/server/api/__tests__/conversations.spec.ts +235 -235
  217. package/src/ruvocal/src/lib/server/api/__tests__/misc.spec.ts +72 -72
  218. package/src/ruvocal/src/lib/server/api/__tests__/testHelpers.ts +86 -86
  219. package/src/ruvocal/src/lib/server/api/__tests__/user-reports.spec.ts +78 -78
  220. package/src/ruvocal/src/lib/server/api/__tests__/user.spec.ts +239 -239
  221. package/src/ruvocal/src/lib/server/api/types.ts +37 -37
  222. package/src/ruvocal/src/lib/server/api/utils/requireAuth.ts +22 -22
  223. package/src/ruvocal/src/lib/server/api/utils/resolveConversation.ts +69 -69
  224. package/src/ruvocal/src/lib/server/api/utils/resolveModel.ts +27 -27
  225. package/src/ruvocal/src/lib/server/api/utils/superjsonResponse.ts +15 -15
  226. package/src/ruvocal/src/lib/server/apiToken.ts +11 -11
  227. package/src/ruvocal/src/lib/server/auth.ts +554 -554
  228. package/src/ruvocal/src/lib/server/config.ts +187 -187
  229. package/src/ruvocal/src/lib/server/conversation.ts +83 -83
  230. package/src/ruvocal/src/lib/server/database/__tests__/rvf.spec.ts +709 -709
  231. package/src/ruvocal/src/lib/server/database/postgres.ts +700 -700
  232. package/src/ruvocal/src/lib/server/database/rvf.ts +1078 -1078
  233. package/src/ruvocal/src/lib/server/database.ts +145 -145
  234. package/src/ruvocal/src/lib/server/endpoints/document.ts +68 -68
  235. package/src/ruvocal/src/lib/server/endpoints/endpoints.ts +43 -43
  236. package/src/ruvocal/src/lib/server/endpoints/images.ts +211 -211
  237. package/src/ruvocal/src/lib/server/endpoints/openai/endpointOai.ts +266 -266
  238. package/src/ruvocal/src/lib/server/endpoints/openai/openAIChatToTextGenerationStream.ts +212 -212
  239. package/src/ruvocal/src/lib/server/endpoints/openai/openAICompletionToTextGenerationStream.ts +32 -32
  240. package/src/ruvocal/src/lib/server/endpoints/preprocessMessages.ts +61 -61
  241. package/src/ruvocal/src/lib/server/exitHandler.ts +59 -59
  242. package/src/ruvocal/src/lib/server/files/downloadFile.ts +34 -34
  243. package/src/ruvocal/src/lib/server/files/uploadFile.ts +29 -29
  244. package/src/ruvocal/src/lib/server/findRepoRoot.ts +13 -13
  245. package/src/ruvocal/src/lib/server/generateFromDefaultEndpoint.ts +46 -46
  246. package/src/ruvocal/src/lib/server/hooks/error.ts +37 -37
  247. package/src/ruvocal/src/lib/server/hooks/fetch.ts +22 -22
  248. package/src/ruvocal/src/lib/server/hooks/handle.ts +250 -250
  249. package/src/ruvocal/src/lib/server/hooks/init.ts +51 -51
  250. package/src/ruvocal/src/lib/server/isURLLocal.spec.ts +31 -31
  251. package/src/ruvocal/src/lib/server/isURLLocal.ts +74 -74
  252. package/src/ruvocal/src/lib/server/logger.ts +42 -42
  253. package/src/ruvocal/src/lib/server/mcp/clientPool.spec.ts +175 -175
  254. package/src/ruvocal/src/lib/server/mcp/hf.ts +32 -32
  255. package/src/ruvocal/src/lib/server/mcp/httpClient.ts +122 -122
  256. package/src/ruvocal/src/lib/server/mcp/registry.ts +76 -76
  257. package/src/ruvocal/src/lib/server/mcp/tools.ts +196 -196
  258. package/src/ruvocal/src/lib/server/metrics.ts +255 -255
  259. package/src/ruvocal/src/lib/server/models.ts +518 -518
  260. package/src/ruvocal/src/lib/server/requestContext.ts +55 -55
  261. package/src/ruvocal/src/lib/server/router/arch.ts +230 -230
  262. package/src/ruvocal/src/lib/server/router/endpoint.ts +316 -316
  263. package/src/ruvocal/src/lib/server/router/multimodal.ts +28 -28
  264. package/src/ruvocal/src/lib/server/router/policy.ts +49 -49
  265. package/src/ruvocal/src/lib/server/router/toolsRoute.ts +51 -51
  266. package/src/ruvocal/src/lib/server/router/types.ts +21 -21
  267. package/src/ruvocal/src/lib/server/sendSlack.ts +23 -23
  268. package/src/ruvocal/src/lib/server/textGeneration/generate.ts +258 -258
  269. package/src/ruvocal/src/lib/server/textGeneration/index.ts +96 -96
  270. package/src/ruvocal/src/lib/server/textGeneration/mcp/fileRefs.ts +155 -155
  271. package/src/ruvocal/src/lib/server/textGeneration/mcp/routerResolution.ts +108 -108
  272. package/src/ruvocal/src/lib/server/textGeneration/mcp/runMcpFlow.ts +831 -831
  273. package/src/ruvocal/src/lib/server/textGeneration/mcp/toolInvocation.ts +349 -349
  274. package/src/ruvocal/src/lib/server/textGeneration/mcp/wasmTools.test.ts +633 -633
  275. package/src/ruvocal/src/lib/server/textGeneration/reasoning.ts +23 -23
  276. package/src/ruvocal/src/lib/server/textGeneration/title.ts +83 -83
  277. package/src/ruvocal/src/lib/server/textGeneration/types.ts +28 -28
  278. package/src/ruvocal/src/lib/server/textGeneration/utils/prepareFiles.ts +88 -88
  279. package/src/ruvocal/src/lib/server/textGeneration/utils/routing.ts +21 -21
  280. package/src/ruvocal/src/lib/server/textGeneration/utils/toolPrompt.ts +49 -49
  281. package/src/ruvocal/src/lib/server/urlSafety.ts +77 -77
  282. package/src/ruvocal/src/lib/server/usageLimits.ts +30 -30
  283. package/src/ruvocal/src/lib/stores/autopilotStore.svelte.ts +175 -175
  284. package/src/ruvocal/src/lib/stores/backgroundGenerations.svelte.ts +32 -32
  285. package/src/ruvocal/src/lib/stores/backgroundGenerations.ts +1 -1
  286. package/src/ruvocal/src/lib/stores/errors.ts +9 -9
  287. package/src/ruvocal/src/lib/stores/isAborted.ts +3 -3
  288. package/src/ruvocal/src/lib/stores/isPro.ts +4 -4
  289. package/src/ruvocal/src/lib/stores/loading.ts +3 -3
  290. package/src/ruvocal/src/lib/stores/mcpServers.ts +534 -534
  291. package/src/ruvocal/src/lib/stores/pendingChatInput.ts +3 -3
  292. package/src/ruvocal/src/lib/stores/pendingMessage.ts +9 -9
  293. package/src/ruvocal/src/lib/stores/settings.ts +182 -182
  294. package/src/ruvocal/src/lib/stores/shareModal.ts +13 -13
  295. package/src/ruvocal/src/lib/stores/titleUpdate.ts +8 -8
  296. package/src/ruvocal/src/lib/stores/wasmMcp.ts +472 -472
  297. package/src/ruvocal/src/lib/switchTheme.ts +124 -124
  298. package/src/ruvocal/src/lib/types/AbortedGeneration.ts +8 -8
  299. package/src/ruvocal/src/lib/types/Assistant.ts +31 -31
  300. package/src/ruvocal/src/lib/types/AssistantStats.ts +11 -11
  301. package/src/ruvocal/src/lib/types/ConfigKey.ts +4 -4
  302. package/src/ruvocal/src/lib/types/ConvSidebar.ts +9 -9
  303. package/src/ruvocal/src/lib/types/Conversation.ts +27 -27
  304. package/src/ruvocal/src/lib/types/ConversationStats.ts +13 -13
  305. package/src/ruvocal/src/lib/types/Message.ts +41 -41
  306. package/src/ruvocal/src/lib/types/MessageEvent.ts +10 -10
  307. package/src/ruvocal/src/lib/types/MessageUpdate.ts +139 -139
  308. package/src/ruvocal/src/lib/types/MigrationResult.ts +7 -7
  309. package/src/ruvocal/src/lib/types/Model.ts +23 -23
  310. package/src/ruvocal/src/lib/types/Report.ts +12 -12
  311. package/src/ruvocal/src/lib/types/Review.ts +6 -6
  312. package/src/ruvocal/src/lib/types/Semaphore.ts +19 -19
  313. package/src/ruvocal/src/lib/types/Session.ts +22 -22
  314. package/src/ruvocal/src/lib/types/Settings.ts +93 -93
  315. package/src/ruvocal/src/lib/types/SharedConversation.ts +9 -9
  316. package/src/ruvocal/src/lib/types/Template.ts +6 -6
  317. package/src/ruvocal/src/lib/types/Timestamps.ts +4 -4
  318. package/src/ruvocal/src/lib/types/TokenCache.ts +6 -6
  319. package/src/ruvocal/src/lib/types/Tool.ts +77 -77
  320. package/src/ruvocal/src/lib/types/UrlDependency.ts +5 -5
  321. package/src/ruvocal/src/lib/types/User.ts +14 -14
  322. package/src/ruvocal/src/lib/utils/PublicConfig.svelte.ts +75 -75
  323. package/src/ruvocal/src/lib/utils/auth.ts +17 -17
  324. package/src/ruvocal/src/lib/utils/chunk.ts +33 -33
  325. package/src/ruvocal/src/lib/utils/cookiesAreEnabled.ts +13 -13
  326. package/src/ruvocal/src/lib/utils/debounce.ts +17 -17
  327. package/src/ruvocal/src/lib/utils/deepestChild.ts +6 -6
  328. package/src/ruvocal/src/lib/utils/favicon.ts +21 -21
  329. package/src/ruvocal/src/lib/utils/fetchJSON.ts +23 -23
  330. package/src/ruvocal/src/lib/utils/file2base64.ts +14 -14
  331. package/src/ruvocal/src/lib/utils/formatUserCount.ts +37 -37
  332. package/src/ruvocal/src/lib/utils/generationState.spec.ts +75 -75
  333. package/src/ruvocal/src/lib/utils/generationState.ts +26 -26
  334. package/src/ruvocal/src/lib/utils/getHref.ts +41 -41
  335. package/src/ruvocal/src/lib/utils/getReturnFromGenerator.ts +7 -7
  336. package/src/ruvocal/src/lib/utils/haptics.ts +64 -64
  337. package/src/ruvocal/src/lib/utils/hashConv.ts +12 -12
  338. package/src/ruvocal/src/lib/utils/hf.ts +17 -17
  339. package/src/ruvocal/src/lib/utils/isDesktop.ts +7 -7
  340. package/src/ruvocal/src/lib/utils/isUrl.ts +8 -8
  341. package/src/ruvocal/src/lib/utils/isVirtualKeyboard.ts +16 -16
  342. package/src/ruvocal/src/lib/utils/loadAttachmentsFromUrls.ts +115 -115
  343. package/src/ruvocal/src/lib/utils/marked.spec.ts +96 -96
  344. package/src/ruvocal/src/lib/utils/marked.ts +531 -531
  345. package/src/ruvocal/src/lib/utils/mcpValidation.ts +147 -147
  346. package/src/ruvocal/src/lib/utils/mergeAsyncGenerators.ts +38 -38
  347. package/src/ruvocal/src/lib/utils/messageUpdates.spec.ts +286 -286
  348. package/src/ruvocal/src/lib/utils/messageUpdates.ts +332 -332
  349. package/src/ruvocal/src/lib/utils/mime.ts +56 -56
  350. package/src/ruvocal/src/lib/utils/models.ts +14 -14
  351. package/src/ruvocal/src/lib/utils/parseBlocks.ts +120 -120
  352. package/src/ruvocal/src/lib/utils/parseIncompleteMarkdown.ts +644 -644
  353. package/src/ruvocal/src/lib/utils/parseStringToList.ts +10 -10
  354. package/src/ruvocal/src/lib/utils/randomUuid.ts +14 -14
  355. package/src/ruvocal/src/lib/utils/searchTokens.ts +33 -33
  356. package/src/ruvocal/src/lib/utils/sha256.ts +7 -7
  357. package/src/ruvocal/src/lib/utils/stringifyError.ts +12 -12
  358. package/src/ruvocal/src/lib/utils/sum.ts +3 -3
  359. package/src/ruvocal/src/lib/utils/template.spec.ts +59 -59
  360. package/src/ruvocal/src/lib/utils/template.ts +53 -53
  361. package/src/ruvocal/src/lib/utils/timeout.ts +9 -9
  362. package/src/ruvocal/src/lib/utils/toolProgress.spec.ts +46 -46
  363. package/src/ruvocal/src/lib/utils/toolProgress.ts +11 -11
  364. package/src/ruvocal/src/lib/utils/tree/addChildren.spec.ts +102 -102
  365. package/src/ruvocal/src/lib/utils/tree/addChildren.ts +48 -48
  366. package/src/ruvocal/src/lib/utils/tree/addSibling.spec.ts +81 -81
  367. package/src/ruvocal/src/lib/utils/tree/addSibling.ts +41 -41
  368. package/src/ruvocal/src/lib/utils/tree/buildSubtree.spec.ts +110 -110
  369. package/src/ruvocal/src/lib/utils/tree/buildSubtree.ts +24 -24
  370. package/src/ruvocal/src/lib/utils/tree/convertLegacyConversation.spec.ts +31 -31
  371. package/src/ruvocal/src/lib/utils/tree/convertLegacyConversation.ts +36 -36
  372. package/src/ruvocal/src/lib/utils/tree/isMessageId.spec.ts +15 -15
  373. package/src/ruvocal/src/lib/utils/tree/isMessageId.ts +5 -5
  374. package/src/ruvocal/src/lib/utils/tree/tree.d.ts +14 -14
  375. package/src/ruvocal/src/lib/utils/tree/treeHelpers.spec.ts +167 -167
  376. package/src/ruvocal/src/lib/utils/updates.ts +39 -39
  377. package/src/ruvocal/src/lib/utils/urlParams.ts +13 -13
  378. package/src/ruvocal/src/lib/wasm/idb.ts +438 -438
  379. package/src/ruvocal/src/lib/wasm/index.ts +1213 -1213
  380. package/src/ruvocal/src/lib/wasm/tests/wasm-capabilities.test.ts +565 -565
  381. package/src/ruvocal/src/lib/wasm/wasm.worker.ts +332 -332
  382. package/src/ruvocal/src/lib/wasm/workerClient.ts +166 -166
  383. package/src/ruvocal/src/lib/workers/autopilotWorker.ts +221 -221
  384. package/src/ruvocal/src/lib/workers/detailFetchWorker.ts +100 -100
  385. package/src/ruvocal/src/lib/workers/markdownWorker.ts +61 -61
  386. package/src/ruvocal/src/routes/+error.svelte +20 -20
  387. package/src/ruvocal/src/routes/+layout.svelte +324 -324
  388. package/src/ruvocal/src/routes/+layout.ts +91 -91
  389. package/src/ruvocal/src/routes/+page.svelte +168 -168
  390. package/src/ruvocal/src/routes/.well-known/oauth-cimd/+server.ts +37 -37
  391. package/src/ruvocal/src/routes/__debug/openai/+server.ts +21 -21
  392. package/src/ruvocal/src/routes/admin/export/+server.ts +159 -159
  393. package/src/ruvocal/src/routes/admin/stats/compute/+server.ts +16 -16
  394. package/src/ruvocal/src/routes/api/conversation/[id]/+server.ts +40 -40
  395. package/src/ruvocal/src/routes/api/conversation/[id]/message/[messageId]/+server.ts +42 -42
  396. package/src/ruvocal/src/routes/api/conversations/+server.ts +48 -48
  397. package/src/ruvocal/src/routes/api/fetch-url/+server.ts +147 -147
  398. package/src/ruvocal/src/routes/api/mcp/health/+server.ts +292 -292
  399. package/src/ruvocal/src/routes/api/mcp/servers/+server.ts +32 -32
  400. package/src/ruvocal/src/routes/api/models/+server.ts +25 -25
  401. package/src/ruvocal/src/routes/api/transcribe/+server.ts +104 -104
  402. package/src/ruvocal/src/routes/api/user/+server.ts +15 -15
  403. package/src/ruvocal/src/routes/api/user/validate-token/+server.ts +20 -20
  404. package/src/ruvocal/src/routes/api/v2/conversations/+server.ts +48 -48
  405. package/src/ruvocal/src/routes/api/v2/conversations/[id]/+server.ts +94 -94
  406. package/src/ruvocal/src/routes/api/v2/conversations/[id]/message/[messageId]/+server.ts +43 -43
  407. package/src/ruvocal/src/routes/api/v2/conversations/import-share/+server.ts +23 -23
  408. package/src/ruvocal/src/routes/api/v2/debug/config/+server.ts +16 -16
  409. package/src/ruvocal/src/routes/api/v2/debug/refresh/+server.ts +30 -30
  410. package/src/ruvocal/src/routes/api/v2/export/+server.ts +196 -196
  411. package/src/ruvocal/src/routes/api/v2/feature-flags/+server.ts +14 -14
  412. package/src/ruvocal/src/routes/api/v2/models/+server.ts +38 -38
  413. package/src/ruvocal/src/routes/api/v2/models/[namespace]/+server.ts +8 -8
  414. package/src/ruvocal/src/routes/api/v2/models/[namespace]/[model]/+server.ts +8 -8
  415. package/src/ruvocal/src/routes/api/v2/models/[namespace]/[model]/subscribe/+server.ts +28 -28
  416. package/src/ruvocal/src/routes/api/v2/models/[namespace]/subscribe/+server.ts +28 -28
  417. package/src/ruvocal/src/routes/api/v2/models/old/+server.ts +7 -7
  418. package/src/ruvocal/src/routes/api/v2/models/refresh/+server.ts +33 -33
  419. package/src/ruvocal/src/routes/api/v2/public-config/+server.ts +7 -7
  420. package/src/ruvocal/src/routes/api/v2/user/+server.ts +17 -17
  421. package/src/ruvocal/src/routes/api/v2/user/billing-orgs/+server.ts +73 -73
  422. package/src/ruvocal/src/routes/api/v2/user/reports/+server.ts +17 -17
  423. package/src/ruvocal/src/routes/api/v2/user/settings/+server.ts +110 -110
  424. package/src/ruvocal/src/routes/conversation/+server.ts +115 -115
  425. package/src/ruvocal/src/routes/conversation/[id]/+page.svelte +593 -593
  426. package/src/ruvocal/src/routes/conversation/[id]/+page.ts +60 -60
  427. package/src/ruvocal/src/routes/conversation/[id]/+server.ts +740 -740
  428. package/src/ruvocal/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts +66 -66
  429. package/src/ruvocal/src/routes/conversation/[id]/share/+server.ts +69 -69
  430. package/src/ruvocal/src/routes/conversation/[id]/stop-generating/+server.ts +35 -35
  431. package/src/ruvocal/src/routes/healthcheck/+server.ts +3 -3
  432. package/src/ruvocal/src/routes/login/+server.ts +5 -5
  433. package/src/ruvocal/src/routes/login/callback/+server.ts +103 -103
  434. package/src/ruvocal/src/routes/login/callback/updateUser.spec.ts +157 -157
  435. package/src/ruvocal/src/routes/login/callback/updateUser.ts +215 -215
  436. package/src/ruvocal/src/routes/logout/+server.ts +18 -18
  437. package/src/ruvocal/src/routes/metrics/+server.ts +18 -18
  438. package/src/ruvocal/src/routes/models/+page.svelte +233 -233
  439. package/src/ruvocal/src/routes/models/[...model]/+page.svelte +161 -161
  440. package/src/ruvocal/src/routes/models/[...model]/+page.ts +14 -14
  441. package/src/ruvocal/src/routes/models/[...model]/thumbnail.png/+server.ts +64 -64
  442. package/src/ruvocal/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte +28 -28
  443. package/src/ruvocal/src/routes/privacy/+page.svelte +11 -11
  444. package/src/ruvocal/src/routes/r/[id]/+page.ts +34 -34
  445. package/src/ruvocal/src/routes/settings/(nav)/+layout.svelte +282 -282
  446. package/src/ruvocal/src/routes/settings/(nav)/+layout.ts +1 -1
  447. package/src/ruvocal/src/routes/settings/(nav)/+server.ts +59 -59
  448. package/src/ruvocal/src/routes/settings/(nav)/[...model]/+page.svelte +464 -464
  449. package/src/ruvocal/src/routes/settings/(nav)/[...model]/+page.ts +14 -14
  450. package/src/ruvocal/src/routes/settings/(nav)/application/+page.svelte +362 -362
  451. package/src/ruvocal/src/routes/settings/+layout.svelte +40 -40
  452. package/src/ruvocal/src/styles/highlight-js.css +195 -195
  453. package/src/ruvocal/src/styles/main.css +144 -144
  454. package/src/ruvocal/static/chatui/favicon-dark.svg +3 -3
  455. package/src/ruvocal/static/chatui/favicon-dev.svg +3 -3
  456. package/src/ruvocal/static/chatui/favicon.svg +3 -3
  457. package/src/ruvocal/static/chatui/icon.svg +3 -3
  458. package/src/ruvocal/static/chatui/logo.svg +7 -7
  459. package/src/ruvocal/static/chatui/manifest.json +54 -54
  460. package/src/ruvocal/static/chatui/welcome.js +184 -184
  461. package/src/ruvocal/static/huggingchat/favicon-dark.svg +4 -4
  462. package/src/ruvocal/static/huggingchat/favicon-dev.svg +4 -4
  463. package/src/ruvocal/static/huggingchat/favicon.svg +4 -4
  464. package/src/ruvocal/static/huggingchat/fulltext-logo.svg +1 -1
  465. package/src/ruvocal/static/huggingchat/icon.svg +4 -4
  466. package/src/ruvocal/static/huggingchat/logo.svg +4 -4
  467. package/src/ruvocal/static/huggingchat/manifest.json +54 -54
  468. package/src/ruvocal/static/huggingchat/routes.chat.json +226 -226
  469. package/src/ruvocal/static/robots.txt +10 -10
  470. package/src/ruvocal/static/wasm/rvagent_wasm.js +1539 -1539
  471. package/src/ruvocal/stub/@reflink/reflink/package.json +5 -5
  472. package/src/ruvocal/svelte.config.js +53 -53
  473. package/src/ruvocal/tailwind.config.cjs +30 -30
  474. package/src/ruvocal/tsconfig.json +19 -19
  475. package/src/ruvocal/vite.config.ts +87 -87
  476. package/src/scripts/deploy.sh +116 -116
  477. package/src/scripts/generate-config.js +245 -245
  478. package/src/scripts/generate-welcome.js +187 -187
  479. package/src/scripts/package-rvf.sh +116 -116
@@ -1,1995 +1,1995 @@
1
- import express from "express";
2
- import { spawn } from "child_process";
3
- import { randomUUID, timingSafeEqual } from "crypto";
4
-
5
- // =============================================================================
6
- // CONFIGURATION
7
- // =============================================================================
8
-
9
- const CLOUD_FUNCTIONS = {
10
- search: process.env.SEARCH_API_URL || null,
11
- research: process.env.RESEARCH_API_URL || null,
12
- };
13
-
14
- const PORT = parseInt(process.env.PORT || "3001", 10);
15
- const BIND_HOST = process.env.MCP_BIND_HOST || "127.0.0.1";
16
-
17
- // =============================================================================
18
- // TOOL GROUPS — Enable/disable categories of tools independently
19
- // =============================================================================
20
- // Groups map tool name prefixes from backends to logical categories.
21
- // Each group can be toggled via env var. The AI sees only enabled tools.
22
-
23
- const TOOL_GROUPS = {
24
- // --- Core (always on, built-in) ---
25
- core: {
26
- enabled: true, // cannot be disabled
27
- description: "Search, research, and guidance tools",
28
- source: "builtin",
29
- },
30
-
31
- // --- Intelligence (ruvector) ---
32
- intelligence: {
33
- enabled: process.env.MCP_GROUP_INTELLIGENCE !== "false",
34
- description: "Self-learning intelligence — routing, memory, pattern training (ruvector)",
35
- source: "ruvector",
36
- prefixes: ["hooks_"],
37
- },
38
-
39
- // --- Agents & Orchestration (ruflo) ---
40
- agents: {
41
- enabled: process.env.MCP_GROUP_AGENTS !== "false",
42
- description: "Agent lifecycle, swarm coordination, task management, workflows (ruflo)",
43
- source: "ruflo",
44
- prefixes: ["agent_", "swarm_", "task_", "session_", "hive-mind_", "workflow_", "coordination_"],
45
- },
46
-
47
- // --- Memory & Knowledge (ruflo) ---
48
- memory: {
49
- enabled: process.env.MCP_GROUP_MEMORY !== "false",
50
- description: "Vector memory, AgentDB, embeddings, semantic search (ruflo)",
51
- source: "ruflo",
52
- prefixes: ["memory_", "agentdb_", "embeddings_"],
53
- },
54
-
55
- // --- Dev Tools (ruflo) ---
56
- devtools: {
57
- enabled: process.env.MCP_GROUP_DEVTOOLS !== "false",
58
- description: "Hooks, code analysis, performance profiling, GitHub integration (ruflo)",
59
- source: "ruflo",
60
- prefixes: ["hooks_", "analyze_", "performance_", "github_", "terminal_", "config_", "system_", "progress_"],
61
- },
62
-
63
- // --- Security & Safety (ruflo) ---
64
- security: {
65
- enabled: process.env.MCP_GROUP_SECURITY === "true",
66
- description: "AI defence, PII detection, claims management, pattern transfer (ruflo)",
67
- source: "ruflo",
68
- prefixes: ["aidefence_", "claims_", "transfer_"],
69
- },
70
-
71
- // --- Browser Automation (ruflo) ---
72
- browser: {
73
- enabled: process.env.MCP_GROUP_BROWSER === "true",
74
- description: "Headless browser control — navigate, click, fill, screenshot (ruflo)",
75
- source: "ruflo",
76
- prefixes: ["browser_"],
77
- },
78
-
79
- // --- Neural & DAA (ruflo) ---
80
- neural: {
81
- enabled: process.env.MCP_GROUP_NEURAL === "true",
82
- description: "Neural network training, DAA autonomous agents, cognitive patterns (ruflo)",
83
- source: "ruflo",
84
- prefixes: ["neural_", "daa_"],
85
- },
86
-
87
- // --- Agentic Flow (agentic-flow@alpha) ---
88
- "agentic-flow": {
89
- enabled: process.env.MCP_GROUP_AGENTIC_FLOW === "true",
90
- description: "Execute 66+ specialized agents, batch code editing, AgentDB patterns (agentic-flow)",
91
- source: "agentic-flow",
92
- prefixes: ["agentic_flow_", "agent_booster_", "agentdb_"],
93
- },
94
-
95
- // --- Claude Code ---
96
- "claude-code": {
97
- enabled: process.env.MCP_GROUP_CLAUDE_CODE === "true",
98
- description: "Anthropic Claude Code — file editing, bash execution, code analysis (requires ANTHROPIC_API_KEY)",
99
- source: "claude",
100
- },
101
-
102
- // --- Gemini MCP ---
103
- gemini: {
104
- enabled: process.env.MCP_GROUP_GEMINI === "true",
105
- description: "Google Gemini conversation context, multimodal capabilities (requires GOOGLE_API_KEY)",
106
- source: "gemini-mcp",
107
- },
108
-
109
- // --- OpenAI Codex ---
110
- codex: {
111
- enabled: process.env.MCP_GROUP_CODEX === "true",
112
- description: "OpenAI Codex coding agent — code generation and execution (requires OPENAI_API_KEY)",
113
- source: "codex",
114
- },
115
- };
116
-
117
- // =============================================================================
118
- // STDIO MCP CLIENT — Connects to external MCP servers via child process
119
- // =============================================================================
120
-
121
- class StdioMcpClient {
122
- constructor(name, command, args = []) {
123
- this.name = name;
124
- this.command = command;
125
- this.args = args;
126
- this.process = null;
127
- this.tools = [];
128
- this.ready = false;
129
- this.pending = new Map();
130
- this.buffer = "";
131
- }
132
-
133
- async start() {
134
- return new Promise((resolve) => {
135
- try {
136
- this.process = spawn(this.command, this.args, {
137
- stdio: ["pipe", "pipe", "pipe"],
138
- env: { ...process.env },
139
- });
140
-
141
- this.process.stdout.on("data", (data) => this._onData(data.toString()));
142
- this.process.stderr.on("data", (data) => {
143
- const msg = data.toString().trim();
144
- if (msg && !msg.startsWith("npm WARN")) console.error(`[${this.name}] ${msg}`);
145
- });
146
- this.process.on("error", (err) => {
147
- console.error(`[${this.name}] spawn error:`, err.message);
148
- this.ready = false;
149
- resolve(false);
150
- });
151
- this.process.on("exit", (code) => {
152
- console.log(`[${this.name}] exited with code ${code}`);
153
- this.ready = false;
154
- });
155
-
156
- this._send("initialize", {
157
- protocolVersion: "2024-11-05",
158
- capabilities: {},
159
- clientInfo: { name: "mcp-bridge", version: "2.0.0" },
160
- }).then((result) => {
161
- if (result && !result.error) {
162
- this._notify("notifications/initialized", {});
163
- return this._send("tools/list", {});
164
- }
165
- return null;
166
- }).then((result) => {
167
- if (result && result.tools) {
168
- this.tools = result.tools.map(t => ({
169
- ...t,
170
- _originalName: t.name,
171
- _backend: this.name,
172
- }));
173
- this.ready = true;
174
- console.log(`[${this.name}] ${this.tools.length} tools loaded`);
175
- }
176
- resolve(this.ready);
177
- }).catch((err) => {
178
- console.error(`[${this.name}] init failed:`, err.message);
179
- resolve(false);
180
- });
181
-
182
- setTimeout(() => { if (!this.ready) resolve(false); }, 60000);
183
- } catch (err) {
184
- console.error(`[${this.name}] failed to start:`, err.message);
185
- resolve(false);
186
- }
187
- });
188
- }
189
-
190
- _onData(chunk) {
191
- this.buffer += chunk;
192
- const lines = this.buffer.split("\n");
193
- this.buffer = lines.pop() || "";
194
- for (const line of lines) {
195
- const trimmed = line.trim();
196
- if (!trimmed) continue;
197
- try {
198
- const msg = JSON.parse(trimmed);
199
- if (msg.id && this.pending.has(msg.id)) {
200
- const { resolve } = this.pending.get(msg.id);
201
- this.pending.delete(msg.id);
202
- resolve(msg.result || msg.error || {});
203
- }
204
- } catch { /* skip non-JSON */ }
205
- }
206
- }
207
-
208
- _send(method, params) {
209
- return new Promise((resolve, reject) => {
210
- if (!this.process || this.process.killed) {
211
- return reject(new Error(`${this.name} process not running`));
212
- }
213
- const id = randomUUID();
214
- const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
215
- this.pending.set(id, { resolve, reject });
216
- this.process.stdin.write(msg);
217
- // initialize is the cold-start gate for backends like ruflo/ruvector
218
- // which boot a full claude-flow / ruvector kernel — on Cloud Run with
219
- // npx fetching artifacts it can take 45-60s. Other RPC methods are
220
- // post-init and stay snappy.
221
- const timeoutMs = method === "initialize" ? 120000 : 30000;
222
- setTimeout(() => {
223
- if (this.pending.has(id)) {
224
- this.pending.delete(id);
225
- reject(new Error(`${this.name} timeout for ${method}`));
226
- }
227
- }, timeoutMs);
228
- });
229
- }
230
-
231
- _notify(method, params) {
232
- if (!this.process || this.process.killed) return;
233
- this.process.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n");
234
- }
235
-
236
- async callTool(originalName, args) {
237
- if (!this.ready) return { error: `${this.name} backend not available` };
238
- try {
239
- return await this._send("tools/call", { name: originalName, arguments: args });
240
- } catch (err) {
241
- return { error: err.message };
242
- }
243
- }
244
-
245
- stop() {
246
- if (this.process && !this.process.killed) {
247
- this.process.kill("SIGTERM");
248
- this.process = null;
249
- }
250
- this.ready = false;
251
- this.tools = [];
252
- }
253
- }
254
-
255
- // =============================================================================
256
- // BACKEND REGISTRY
257
- // =============================================================================
258
-
259
- const BACKEND_DEFS = [
260
- { name: "ruvector", command: "npx", args: ["-y", "ruvector", "mcp", "start"], groups: ["intelligence"] },
261
- { name: "ruflo", command: "npx", args: ["-y", "ruflo", "mcp", "start"], groups: ["agents", "memory", "devtools", "security", "browser", "neural"] },
262
- { name: "agentic-flow", command: "npx", args: ["-y", "agentic-flow@alpha", "mcp", "start"], groups: ["agentic-flow"] },
263
- { name: "claude", command: "claude", args: ["mcp", "serve"], groups: ["claude-code"] },
264
- { name: "gemini-mcp", command: "npx", args: ["-y", "gemini-mcp-server"], groups: ["gemini"] },
265
- { name: "codex", command: "npx", args: ["-y", "@openai/codex", "mcp-server"], groups: ["codex"] },
266
- ];
267
-
268
- const mcpBackends = new Map();
269
- let allBackendTools = []; // all tools from all backends (pre-filter)
270
-
271
- function isBackendNeeded(backendDef) {
272
- return backendDef.groups.some(g => TOOL_GROUPS[g]?.enabled);
273
- }
274
-
275
- // Filter tools from a backend based on which groups are enabled
276
- function filterToolsByGroups(tools, backendName) {
277
- const enabledGroups = Object.entries(TOOL_GROUPS)
278
- .filter(([, g]) => g.enabled && g.source === backendName);
279
-
280
- if (enabledGroups.length === 0) return [];
281
-
282
- // If any enabled group has no prefixes defined, include all tools from that backend
283
- const hasWildcard = enabledGroups.some(([, g]) => !g.prefixes);
284
- if (hasWildcard) return tools;
285
-
286
- const enabledPrefixes = enabledGroups.flatMap(([, g]) => g.prefixes || []);
287
- return tools.filter(t => enabledPrefixes.some(p => t._originalName.startsWith(p)));
288
- }
289
-
290
- // Get the final filtered tool list with namespaced names
291
- function getActiveTools() {
292
- const filtered = [];
293
- for (const [backendName, client] of mcpBackends) {
294
- const accepted = filterToolsByGroups(client.tools, backendName);
295
- for (const t of accepted) {
296
- filtered.push({ ...t, name: `${backendName}__${t._originalName}` });
297
- }
298
- }
299
- return filtered;
300
- }
301
-
302
- async function initBackends() {
303
- const needed = BACKEND_DEFS.filter(isBackendNeeded);
304
- if (needed.length === 0) return;
305
-
306
- console.log(`Starting ${needed.length} MCP backends: ${needed.map(b => b.name).join(", ")}`);
307
-
308
- await Promise.allSettled(
309
- needed.map(async (b) => {
310
- const client = new StdioMcpClient(b.name, b.command, b.args);
311
- const ok = await client.start();
312
- if (ok) {
313
- mcpBackends.set(b.name, client);
314
- } else {
315
- console.warn(`[${b.name}] failed to start`);
316
- }
317
- })
318
- );
319
-
320
- allBackendTools = getActiveTools();
321
- console.log(`MCP backends: ${mcpBackends.size} active, ${allBackendTools.length} tools (filtered by groups)`);
322
- }
323
-
324
- process.on("SIGTERM", () => { for (const [, c] of mcpBackends) c.stop(); process.exit(0); });
325
- process.on("SIGINT", () => { for (const [, c] of mcpBackends) c.stop(); process.exit(0); });
326
-
327
- // =============================================================================
328
- // BUILT-IN TOOLS (core group — always on)
329
- // =============================================================================
330
-
331
- const BUILTIN_TOOLS = [
332
- {
333
- name: "search",
334
- description: "Search your knowledge base for relevant information.",
335
- inputSchema: {
336
- type: "object",
337
- properties: {
338
- query: { type: "string", description: "Natural language search query" },
339
- limit: { type: "number", description: "Max results (default 5)", default: 5 },
340
- },
341
- required: ["query"],
342
- },
343
- },
344
- {
345
- name: "web_research",
346
- description: "Search the web, fact-check claims, compare items, or conduct deep research. Actions: 'search' (quick), 'research' (deep report), 'compare' (side-by-side), 'fact_check' (verify claims), 'goap' (comprehensive multi-step research with verification).",
347
- inputSchema: {
348
- type: "object",
349
- properties: {
350
- action: { type: "string", enum: ["search", "research", "compare", "fact_check", "goap"], description: "Research action type", default: "search" },
351
- query: { type: "string", description: "Search query or topic" },
352
- items: { type: "array", items: { type: "string" }, description: "Items to compare (for 'compare')" },
353
- claim: { type: "string", description: "Claim to verify (for 'fact_check')" },
354
- verify: { type: "boolean", description: "Verify results in goap mode", default: true },
355
- },
356
- required: ["query"],
357
- },
358
- },
359
- {
360
- name: "guidance",
361
- description: "Get instructions on how to use the available tool groups and services. Call this FIRST when unsure which tool to use, when a user asks 'what can you do?', or when you need to understand a specific tool group. Returns structured guidance for the AI on tool selection and usage patterns.",
362
- inputSchema: {
363
- type: "object",
364
- properties: {
365
- topic: {
366
- type: "string",
367
- enum: ["overview", "groups", "intelligence", "agents", "memory", "devtools", "security", "browser", "neural", "agentic-flow", "claude-code", "gemini", "codex", "tool"],
368
- description: "What to get guidance on. Use 'overview' for capabilities summary, 'groups' to see all tool groups and their status, or a specific group name for detailed usage instructions.",
369
- default: "overview",
370
- },
371
- tool_name: { type: "string", description: "Specific tool name to get detailed usage info (when topic='tool')" },
372
- },
373
- },
374
- },
375
- ];
376
-
377
- // =============================================================================
378
- // GUIDANCE ENGINE — AI-facing instruction system
379
- // =============================================================================
380
-
381
- function getGuidance(topic, toolName) {
382
- const activeGroups = Object.entries(TOOL_GROUPS).filter(([, g]) => g.enabled);
383
- const inactiveGroups = Object.entries(TOOL_GROUPS).filter(([, g]) => !g.enabled);
384
- const externalTools = getActiveTools();
385
-
386
- if (topic === "overview") {
387
- return {
388
- guidance: `# Tool Capabilities Overview
389
-
390
- You have access to ${BUILTIN_TOOLS.length + externalTools.length} tools organized into ${activeGroups.length} active groups.
391
-
392
- ## Active Groups
393
- ${activeGroups.map(([name, g]) => {
394
- const count = name === "core" ? BUILTIN_TOOLS.length : externalTools.filter(t => t.name.startsWith(g.source + "__")).length;
395
- return `- **${name}** (${count} tools) — ${g.description}`;
396
- }).join("\n")}
397
-
398
- ## Inactive Groups (can be enabled)
399
- ${inactiveGroups.map(([name, g]) => `- **${name}** — ${g.description}`).join("\n") || "None"}
400
-
401
- ## Quick Decision Guide
402
- - **Knowledge questions** → use \`search\` first, then \`web_research\` if needed
403
- - **Current events / facts** → use \`web_research\` with action 'search' or 'goap'
404
- - **Complex research** → use \`web_research\` with action 'goap' (multi-step pipeline)
405
- - **"What can you do?"** → call \`guidance\` with topic 'groups'
406
- - **Memory / recall** → use tools from the \`memory\` group
407
- - **Agent orchestration** → use tools from the \`agents\` group
408
- - **Code analysis / performance** → use tools from the \`devtools\` group
409
-
410
- ## Rules
411
- 1. Call tools FIRST, then present results conversationally
412
- 2. Never show raw JSON — synthesize results naturally
413
- 3. For complex questions, prefer GOAP pipeline (web_research action='goap')
414
- 4. Call \`guidance\` with a specific group name to learn how to use that group's tools`,
415
- topic: "overview",
416
- };
417
- }
418
-
419
- if (topic === "groups") {
420
- const groupList = Object.entries(TOOL_GROUPS).map(([name, g]) => {
421
- const status = g.enabled ? "ACTIVE" : "INACTIVE";
422
- const toolCount = name === "core" ? BUILTIN_TOOLS.length :
423
- externalTools.filter(t => {
424
- const backend = t._backend;
425
- return g.source === backend && (!g.prefixes || g.prefixes.some(p => t._originalName.startsWith(p)));
426
- }).length;
427
- return `| ${name} | ${status} | ${toolCount} | ${g.description} |`;
428
- });
429
-
430
- return {
431
- guidance: `# Tool Groups\n\n| Group | Status | Tools | Description |\n|-------|--------|-------|-------------|\n${groupList.join("\n")}`,
432
- topic: "groups",
433
- };
434
- }
435
-
436
- // Specific group guidance
437
- const groupGuides = {
438
- intelligence: `# Intelligence Group (ruvector)
439
-
440
- Self-learning intelligence tools for routing and vector memory.
441
-
442
- ## Key Tools
443
- - **ruvector__hooks_route** — Route a task to the best agent type. Call with a task description.
444
- - **ruvector__hooks_remember** — Store context/knowledge in vector memory for later recall.
445
- - **ruvector__hooks_recall** — Search vector memory semantically. Good for finding past context.
446
- - **ruvector__hooks_pretrain** — Bootstrap intelligence from a code repository.
447
- - **ruvector__hooks_build_agents** — Generate optimized agent configurations.
448
- - **ruvector__hooks_stats** — Get intelligence statistics and learning metrics.
449
-
450
- ## When to Use
451
- - Before starting complex tasks: route to find the best agent approach
452
- - To store important findings for cross-session memory
453
- - To recall previously stored patterns or solutions`,
454
-
455
- agents: `# Agents & Orchestration Group (ruflo)
456
-
457
- Multi-agent lifecycle management, swarm coordination, and task workflows.
458
-
459
- ## Key Tools
460
- - **ruflo__agent_spawn** — Create a new agent with specific capabilities
461
- - **ruflo__agent_list** — List all active agents
462
- - **ruflo__swarm_init** — Initialize a swarm with a topology (mesh, hierarchical, ring, star)
463
- - **ruflo__task_create** — Create and assign tasks
464
- - **ruflo__workflow_create** — Define multi-step workflows
465
- - **ruflo__workflow_execute** — Execute a workflow
466
- - **ruflo__hive-mind_init** — Start collective intelligence coordination
467
- - **ruflo__coordination_orchestrate** — Multi-agent coordination
468
-
469
- ## When to Use
470
- - Complex tasks requiring multiple agents working together
471
- - Pipeline workflows with sequential or parallel steps
472
- - Distributed task management`,
473
-
474
- memory: `# Memory & Knowledge Group (ruflo)
475
-
476
- Vector storage, semantic search, AgentDB pattern learning, and embeddings.
477
-
478
- ## Key Tools
479
- - **ruflo__memory_store** — Store a value with vector embedding for semantic search
480
- - **ruflo__memory_search** — Semantic search across stored memories (HNSW-indexed)
481
- - **ruflo__memory_list** — List stored memory entries
482
- - **ruflo__agentdb_pattern-store** — Store a reasoning pattern for learning
483
- - **ruflo__agentdb_pattern-search** — Search for similar reasoning patterns
484
- - **ruflo__agentdb_context-synthesize** — Synthesize context from stored memories
485
- - **ruflo__embeddings_generate** — Generate vector embeddings for text
486
- - **ruflo__embeddings_search** — Semantic similarity search
487
-
488
- ## When to Use
489
- - Persistent knowledge storage across sessions
490
- - Finding similar past solutions or patterns
491
- - Building semantic search over custom data`,
492
-
493
- devtools: `# Dev Tools Group (ruflo)
494
-
495
- Code analysis, performance profiling, GitHub integration, and terminal access.
496
-
497
- ## Key Tools
498
- - **ruflo__analyze_diff** — Analyze git diff for risk and change classification
499
- - **ruflo__performance_benchmark** — Run performance benchmarks
500
- - **ruflo__performance_bottleneck** — Detect performance bottlenecks
501
- - **ruflo__github_repo_analyze** — Analyze a GitHub repository
502
- - **ruflo__github_pr_manage** — Manage pull requests
503
- - **ruflo__terminal_execute** — Execute commands in a terminal session
504
-
505
- ## When to Use
506
- - Code review and change risk assessment
507
- - Performance analysis and optimization
508
- - GitHub repository management`,
509
-
510
- security: `# Security & Safety Group (ruflo)
511
-
512
- AI defence, PII detection, and claims-based authorization.
513
-
514
- ## Key Tools
515
- - **ruflo__aidefence_scan** — Scan text for AI manipulation attempts
516
- - **ruflo__aidefence_has_pii** — Check for PII (emails, phones, SSNs)
517
- - **ruflo__aidefence_is_safe** — Quick safety check on input
518
- - **ruflo__claims_claim** — Claim an issue for work
519
- - **ruflo__claims_board** — Visual board of all claims
520
-
521
- ## When to Use
522
- - Input validation and safety checking
523
- - PII detection before processing sensitive data
524
- - Work item management across agents`,
525
-
526
- browser: `# Browser Automation Group (ruflo)
527
-
528
- Headless browser control for web interaction and testing.
529
-
530
- ## Key Tools
531
- - **ruflo__browser_open** — Navigate to a URL
532
- - **ruflo__browser_click** — Click elements by reference
533
- - **ruflo__browser_fill** — Fill form inputs
534
- - **ruflo__browser_screenshot** — Capture page screenshots
535
- - **ruflo__browser_snapshot** — Get accessibility tree for AI parsing
536
- - **ruflo__browser_eval** — Execute JavaScript in page context
537
-
538
- ## When to Use
539
- - Web scraping and data extraction
540
- - Automated testing (E2E)
541
- - Form filling and web interaction`,
542
-
543
- neural: `# Neural & DAA Group (ruflo)
544
-
545
- Neural network operations and Decentralized Autonomous Agents.
546
-
547
- ## Key Tools
548
- - **ruflo__neural_train** — Train a neural model
549
- - **ruflo__neural_predict** — Make predictions
550
- - **ruflo__daa_agent_create** — Create an autonomous agent
551
- - **ruflo__daa_workflow_create** — Create autonomous workflows
552
- - **ruflo__daa_knowledge_share** — Share knowledge between agents
553
-
554
- ## When to Use
555
- - Pattern learning and prediction
556
- - Autonomous agent workflows
557
- - Knowledge transfer between agents`,
558
-
559
- "agentic-flow": `# Agentic Flow Group (agentic-flow@alpha)
560
-
561
- Execute 66+ specialized agents with boosted code editing and AgentDB.
562
-
563
- ## Key Tools
564
- - **agentic-flow__agentic_flow_agent** — Execute any of 66+ specialized agents
565
- - **agentic-flow__agentic_flow_list_agents** — List available agent types
566
- - **agentic-flow__agent_booster_edit_file** — 352x faster code editing
567
- - **agentic-flow__agent_booster_batch_edit** — Multi-file refactoring
568
- - **agentic-flow__agentdb_pattern_store** — Store reasoning patterns
569
- - **agentic-flow__agentdb_pattern_search** — Search similar patterns
570
-
571
- ## When to Use
572
- - Complex code generation with specialized agents
573
- - Batch code refactoring across files
574
- - Agent selection when you need the right specialist`,
575
-
576
- "claude-code": `# Claude Code Group
577
-
578
- Anthropic Claude Code MCP server — full coding agent capabilities.
579
-
580
- Requires: ANTHROPIC_API_KEY environment variable.
581
-
582
- ## Capabilities
583
- - File reading and editing
584
- - Bash command execution
585
- - Code analysis and generation
586
- - Project exploration
587
-
588
- ## When to Use
589
- - When you need a second AI perspective on code
590
- - Complex refactoring tasks
591
- - Code review and analysis`,
592
-
593
- gemini: `# Gemini MCP Group
594
-
595
- Google Gemini with conversation context management.
596
-
597
- Requires: GOOGLE_API_KEY environment variable (already set for Gemini models).
598
-
599
- ## Capabilities
600
- - Conversation context management
601
- - Multimodal processing
602
- - Google Search grounding
603
-
604
- ## When to Use
605
- - Extended context conversations
606
- - Multimodal content processing`,
607
-
608
- codex: `# Codex Group
609
-
610
- OpenAI Codex coding agent.
611
-
612
- Requires: OPENAI_API_KEY environment variable (already set for OpenAI models).
613
-
614
- ## Capabilities
615
- - Code generation and execution
616
- - Code completion
617
- - Code explanation
618
-
619
- ## When to Use
620
- - Code generation tasks
621
- - Quick code completions
622
- - Code explanation and documentation`,
623
- };
624
-
625
- if (topic === "tool" && toolName) {
626
- const allTools = [...BUILTIN_TOOLS, ...externalTools];
627
- const tool = allTools.find(t => t.name === toolName);
628
- if (tool) {
629
- const props = Object.entries(tool.inputSchema?.properties || {})
630
- .map(([k, v]) => `- **${k}** (${v.type}) — ${v.description || ""}`)
631
- .join("\n");
632
- return { guidance: `# ${tool.name}\n\n${tool.description}\n\n## Parameters\n${props}`, topic: "tool" };
633
- }
634
- return { guidance: `Tool '${toolName}' not found. Call guidance with topic='groups' to see available tools.`, topic: "tool" };
635
- }
636
-
637
- if (groupGuides[topic]) {
638
- const group = TOOL_GROUPS[topic];
639
- if (!group?.enabled) {
640
- return { guidance: `# ${topic} — INACTIVE\n\n${group?.description || ""}\n\nThis group is not enabled. Set the appropriate MCP_GROUP_* env var to "true" to activate it.`, topic };
641
- }
642
- return { guidance: groupGuides[topic], topic };
643
- }
644
-
645
- return { guidance: `Unknown topic '${topic}'. Use 'overview', 'groups', or a specific group name.`, topic };
646
- }
647
-
648
- // =============================================================================
649
- // GEMINI GROUNDED SEARCH — Uses Gemini's built-in Google Search when no
650
- // dedicated search Cloud Function is configured
651
- // =============================================================================
652
-
653
- async function geminiGroundedSearch(query, mode = "search") {
654
- const apiKey = process.env.GOOGLE_API_KEY;
655
- if (!apiKey) return { error: "No GOOGLE_API_KEY configured for search" };
656
-
657
- // Empty/missing query produces a 400 from Gemini's generateContent endpoint.
658
- // Return a structured error so the model can recover with a real query.
659
- if (!query || typeof query !== "string" || !query.trim()) {
660
- return {
661
- error: "search requires a non-empty query string",
662
- hint: "Call this tool again with { query: 'your search terms' }. For comparisons use { action: 'compare', query: 'item A vs item B' }; for fact-checking use { action: 'fact_check', claim: 'the claim text' }.",
663
- };
664
- }
665
-
666
- const model = "gemini-2.5-flash";
667
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
668
-
669
- const systemInstructions = {
670
- search: `You are a helpful search assistant. Answer the query using Google Search results. Include key facts, sources, and relevant details. Be concise but thorough.`,
671
- research: `You are a research analyst. Provide a comprehensive research report on the topic using Google Search results. Include: key findings, analysis, multiple perspectives, and source citations. Be thorough.`,
672
- compare: `You are a comparison analyst. Compare the items using Google Search results. Create a structured comparison with pros/cons, key differences, and a recommendation.`,
673
- fact_check: `You are a fact-checker. Verify the claim using Google Search results. Provide a verdict (TRUE/FALSE/PARTIALLY TRUE/UNVERIFIABLE), evidence, and sources.`,
674
- };
675
-
676
- const prompt = mode === "fact_check"
677
- ? `Fact-check this claim: "${query}"`
678
- : mode === "compare"
679
- ? `Compare these items: ${query}`
680
- : mode === "research"
681
- ? `Research this topic thoroughly: ${query}`
682
- : query;
683
-
684
- try {
685
- const resp = await fetch(url, {
686
- method: "POST",
687
- headers: { "Content-Type": "application/json" },
688
- body: JSON.stringify({
689
- system_instruction: { parts: [{ text: systemInstructions[mode] || systemInstructions.search }] },
690
- contents: [{ parts: [{ text: prompt }] }],
691
- tools: [{ google_search: {} }],
692
- generationConfig: { temperature: 0.2 },
693
- }),
694
- signal: AbortSignal.timeout(30000),
695
- });
696
-
697
- if (!resp.ok) {
698
- const errText = await resp.text();
699
- console.error(`[gemini-search] API error ${resp.status}:`, errText.substring(0, 200));
700
- return { error: `Search API error: ${resp.status}` };
701
- }
702
-
703
- const data = await resp.json();
704
- const candidate = data.candidates?.[0];
705
- const answer = candidate?.content?.parts?.map(p => p.text).filter(Boolean).join("\n") || "";
706
-
707
- // Extract grounding metadata (sources)
708
- const grounding = candidate?.groundingMetadata || {};
709
- const chunks = grounding.groundingChunks || [];
710
- const sources = chunks
711
- .filter(c => c.web)
712
- .map(c => ({ title: c.web.title || "", url: c.web.uri || "" }))
713
- .filter(s => s.url);
714
-
715
- // Extract search queries used
716
- const searchQueries = (grounding.webSearchQueries || []);
717
-
718
- return {
719
- success: true,
720
- answer,
721
- sources: sources.slice(0, 8),
722
- searchQueries,
723
- groundingMetadata: { sources, searchQueries },
724
- mode,
725
- };
726
- } catch (err) {
727
- if (err.name === "AbortError" || err.name === "TimeoutError") return { error: "Search timed out" };
728
- return { error: err.message };
729
- }
730
- }
731
-
732
- // =============================================================================
733
- // SSRF GUARD — Reject requests to private/loopback ranges (CWE-918)
734
- // =============================================================================
735
-
736
- const PRIVATE_IP_RE = /^(?:10\.|172\.(?:1[6-9]|2\d|3[01])\.|192\.168\.|127\.|0\.|::1|fc|fd)/i;
737
-
738
- function assertSafeUrl(rawUrl) {
739
- let parsed;
740
- try {
741
- parsed = new URL(rawUrl);
742
- } catch {
743
- throw new Error(`SSRF guard: invalid URL — ${rawUrl}`);
744
- }
745
- if (parsed.protocol !== "https:") {
746
- throw new Error(`SSRF guard: only HTTPS URLs are permitted, got ${parsed.protocol}`);
747
- }
748
- const host = parsed.hostname;
749
- if (PRIVATE_IP_RE.test(host) || host === "localhost" || host.endsWith(".local")) {
750
- throw new Error(`SSRF guard: private/loopback host rejected — ${host}`);
751
- }
752
- }
753
-
754
- // =============================================================================
755
- // HELPER — Call a backend Cloud Function / API
756
- // =============================================================================
757
-
758
- async function callCloudFunction(url, payload, timeoutMs = 25000) {
759
- // Validate the URL before making any network request.
760
- assertSafeUrl(url);
761
- const controller = new AbortController();
762
- const timer = setTimeout(() => controller.abort(), timeoutMs);
763
- try {
764
- const resp = await fetch(url, {
765
- method: "POST",
766
- headers: { "Content-Type": "application/json" },
767
- body: JSON.stringify(payload),
768
- signal: controller.signal,
769
- });
770
- return await resp.json();
771
- } catch (err) {
772
- if (err.name === "AbortError") return { error: "Request timed out", timeout: timeoutMs };
773
- return { error: err.message };
774
- } finally {
775
- clearTimeout(timer);
776
- }
777
- }
778
-
779
- // =============================================================================
780
- // GOAP SEARCH PIPELINE
781
- // =============================================================================
782
-
783
- async function executeGoapSearch(query, args) {
784
- const researchUrl = CLOUD_FUNCTIONS.research;
785
- if (!researchUrl) return { error: "GOAP requires a 'research' URL in CLOUD_FUNCTIONS" };
786
-
787
- const startTime = Date.now();
788
-
789
- const composeResult = await callCloudFunction(researchUrl, {
790
- action: "search",
791
- query: `Break this question into 3-4 distinct search queries that would help answer it comprehensively. Return ONLY the queries, one per line:\n\n${query}`,
792
- }, 30000);
793
-
794
- let searchQueries = [query];
795
- if (composeResult && !composeResult.error) {
796
- const answer = composeResult.result?.answer || composeResult.answer || "";
797
- const lines = answer.split("\n").map(l => l.replace(/^[\d\-\*\.\)]+\s*/, "").trim()).filter(l => l.length > 5 && l.length < 200);
798
- if (lines.length >= 2) searchQueries = lines.slice(0, 4);
799
- }
800
-
801
- const searchResults = await Promise.all(
802
- searchQueries.map(q => callCloudFunction(researchUrl, { action: "search", query: q }, 30000))
803
- );
804
-
805
- const allSources = [], allAnswers = [];
806
- for (let i = 0; i < searchResults.length; i++) {
807
- const r = searchResults[i];
808
- if (r && !r.error && r.success !== false) {
809
- const answer = r.result?.answer || r.answer || "";
810
- if (answer) allAnswers.push({ query: searchQueries[i], answer });
811
- const gm = r.result?.groundingMetadata || r.groundingMetadata || {};
812
- if (gm.sources) allSources.push(...gm.sources);
813
- }
814
- }
815
-
816
- const uniqueSources = [];
817
- const seenUrls = new Set();
818
- for (const src of allSources) {
819
- const url = src.url || src.uri || "";
820
- if (url && !seenUrls.has(url)) { seenUrls.add(url); uniqueSources.push(src); }
821
- }
822
-
823
- const synthesisInput = allAnswers.map(a => `## ${a.query}\n${a.answer}`).join("\n\n");
824
- const synthesisResult = await callCloudFunction(researchUrl, {
825
- action: "research",
826
- topic: `Synthesize these findings into a comprehensive answer to: "${query}"\n\nFindings:\n${synthesisInput}`,
827
- }, 60000);
828
-
829
- const synthesizedAnswer = synthesisResult?.result?.answer || synthesisResult?.answer || synthesisInput;
830
- const synthGm = synthesisResult?.result?.groundingMetadata || {};
831
- if (synthGm.sources) {
832
- for (const src of synthGm.sources) {
833
- const url = src.url || src.uri || "";
834
- if (url && !seenUrls.has(url)) { seenUrls.add(url); uniqueSources.push(src); }
835
- }
836
- }
837
-
838
- let verification = { verified: true, confidence: "high" };
839
- if (args.verify !== false && synthesizedAnswer.length > 100) {
840
- const vr = await callCloudFunction(researchUrl, {
841
- action: "fact_check", claim: synthesizedAnswer.substring(0, 500),
842
- }, 30000);
843
- if (vr && !vr.error && vr.result) {
844
- verification = {
845
- verified: vr.result.verdict !== "FALSE",
846
- verdict: vr.result.verdict,
847
- confidence: vr.result.confidence || "medium",
848
- details: vr.result.analysis,
849
- };
850
- }
851
- }
852
-
853
- return {
854
- answer: synthesizedAnswer, pipeline: "goap",
855
- steps: { queries_composed: searchQueries.length, searches_executed: searchResults.filter(r => !r?.error).length, sources_found: uniqueSources.length, verification },
856
- sources: uniqueSources.slice(0, 10), searchQueries, duration_ms: Date.now() - startTime,
857
- };
858
- }
859
-
860
- // =============================================================================
861
- // GOAP SEARCH PIPELINE — Gemini fallback (no Cloud Function needed)
862
- // =============================================================================
863
-
864
- async function executeGoapSearchGemini(query, args) {
865
- const startTime = Date.now();
866
-
867
- // Step 1: Decompose into sub-queries
868
- const decompose = await geminiGroundedSearch(
869
- `Break this question into 3-4 distinct search queries that would help answer it comprehensively. Return ONLY the queries, one per line:\n\n${query}`,
870
- "search"
871
- );
872
-
873
- let searchQueries = [query];
874
- if (decompose?.answer) {
875
- const lines = decompose.answer.split("\n")
876
- .map(l => l.replace(/^[\d\-\*\.\)]+\s*/, "").trim())
877
- .filter(l => l.length > 5 && l.length < 200);
878
- if (lines.length >= 2) searchQueries = lines.slice(0, 4);
879
- }
880
-
881
- // Step 2: Parallel searches
882
- const searchResults = await Promise.all(
883
- searchQueries.map(q => geminiGroundedSearch(q, "search"))
884
- );
885
-
886
- const allSources = [], allAnswers = [];
887
- for (let i = 0; i < searchResults.length; i++) {
888
- const r = searchResults[i];
889
- if (r && !r.error && r.answer) {
890
- allAnswers.push({ query: searchQueries[i], answer: r.answer });
891
- if (r.sources) allSources.push(...r.sources);
892
- }
893
- }
894
-
895
- // Dedupe sources
896
- const seenUrls = new Set();
897
- const uniqueSources = allSources.filter(s => {
898
- if (seenUrls.has(s.url)) return false;
899
- seenUrls.add(s.url);
900
- return true;
901
- });
902
-
903
- // Step 3: Synthesize
904
- const synthesisInput = allAnswers.map(a => `## ${a.query}\n${a.answer}`).join("\n\n");
905
- const synthesis = await geminiGroundedSearch(
906
- `Synthesize these findings into a comprehensive answer to: "${query}"\n\nFindings:\n${synthesisInput}`,
907
- "research"
908
- );
909
-
910
- const finalAnswer = synthesis?.answer || synthesisInput;
911
- if (synthesis?.sources) {
912
- for (const s of synthesis.sources) {
913
- if (!seenUrls.has(s.url)) { seenUrls.add(s.url); uniqueSources.push(s); }
914
- }
915
- }
916
-
917
- // Step 4: Verify if requested
918
- let verification = { verified: true, confidence: "high" };
919
- if (args.verify !== false && finalAnswer.length > 100) {
920
- const vr = await geminiGroundedSearch(finalAnswer.substring(0, 500), "fact_check");
921
- if (vr && !vr.error) {
922
- verification = { verified: true, confidence: "medium", details: vr.answer };
923
- }
924
- }
925
-
926
- return {
927
- success: true,
928
- answer: finalAnswer,
929
- pipeline: "goap-gemini",
930
- steps: {
931
- queries_composed: searchQueries.length,
932
- searches_executed: searchResults.filter(r => !r?.error).length,
933
- sources_found: uniqueSources.length,
934
- verification,
935
- },
936
- sources: uniqueSources.slice(0, 10),
937
- searchQueries,
938
- duration_ms: Date.now() - startTime,
939
- };
940
- }
941
-
942
- // =============================================================================
943
- // TOOL EXECUTOR
944
- // =============================================================================
945
-
946
- // ADR-166 §6 Phase 1d + 2a — server-side tool gate.
947
- // Enforced HERE (not just in the autopilot handler) so /mcp, /mcp/:group,
948
- // autopilot, and any future path share ONE denial gate. Was the missing
949
- // link that made the disclosed unauthenticated-RCE chain reach shell.
950
- const DANGEROUS_TOOLS = Object.freeze(new Set([
951
- "terminal_execute",
952
- "ruflo__terminal_execute",
953
- "devtools__terminal_execute",
954
- ]));
955
- function isTerminalTool(name) {
956
- return DANGEROUS_TOOLS.has(name) || /terminal_execute/i.test(name);
957
- }
958
- const MCP_ENABLE_TERMINAL = process.env.MCP_ENABLE_TERMINAL === "true";
959
-
960
- async function executeTool(name, args) {
961
- // Deny dangerous tools unless the operator explicitly opted in.
962
- // Enforced on every path (not just autopilot) — root cause of ADR-166 V2/V3.
963
- if (isTerminalTool(name) && !MCP_ENABLE_TERMINAL) {
964
- return {
965
- error:
966
- `Tool "${name}" is disabled by default. Set MCP_ENABLE_TERMINAL=true to allow.`,
967
- code: "TOOL_DISABLED",
968
- };
969
- }
970
- switch (name) {
971
- case "search": {
972
- if (CLOUD_FUNCTIONS.search) {
973
- return callCloudFunction(CLOUD_FUNCTIONS.search, { query: args.query, limit: args.limit || 5 });
974
- }
975
- // Fallback: use Gemini grounded search
976
- return geminiGroundedSearch(args.query, "search");
977
- }
978
-
979
- case "web_research": {
980
- const action = args.action || "search";
981
- if (action === "goap") {
982
- // GOAP needs research endpoint — fall back to multi-search via Gemini
983
- if (CLOUD_FUNCTIONS.research) return executeGoapSearch(args.query, args);
984
- return executeGoapSearchGemini(args.query, args);
985
- }
986
- if (CLOUD_FUNCTIONS.research) {
987
- const payload = { action };
988
- if (action === "search") payload.query = args.query;
989
- else if (action === "research") payload.topic = args.query;
990
- else if (action === "compare") payload.items = args.items;
991
- else if (action === "fact_check") payload.claim = args.claim || args.query;
992
- return callCloudFunction(CLOUD_FUNCTIONS.research, payload, 60000);
993
- }
994
- // Fallback: Gemini grounded search
995
- const mode = action === "fact_check" ? "fact_check" : action === "compare" ? "compare" : action === "research" ? "research" : "search";
996
- const query = action === "compare" ? (args.items || []).join(" vs ") : (args.claim || args.query);
997
- return geminiGroundedSearch(query, mode);
998
- }
999
-
1000
- case "guidance":
1001
- return getGuidance(args.topic || "overview", args.tool_name);
1002
-
1003
- default: {
1004
- // Route to external MCP backend
1005
- const activeTools = getActiveTools();
1006
- const extTool = activeTools.find(t => t.name === name);
1007
- if (extTool) {
1008
- const backend = mcpBackends.get(extTool._backend);
1009
- if (backend) return backend.callTool(extTool._originalName, args);
1010
- return { error: `Backend ${extTool._backend} not available` };
1011
- }
1012
- return { error: `Unknown tool: ${name}. Call 'guidance' with topic='groups' to see available tools.` };
1013
- }
1014
- }
1015
- }
1016
-
1017
- // =============================================================================
1018
- // PER-GROUP TOOL HELPERS
1019
- // =============================================================================
1020
-
1021
- // Get tools for a specific group only
1022
- function getToolsForGroup(groupName) {
1023
- const group = TOOL_GROUPS[groupName];
1024
- if (!group || !group.enabled) return [];
1025
- if (groupName === "core") return BUILTIN_TOOLS;
1026
-
1027
- const allActive = getActiveTools();
1028
- if (!group.prefixes) {
1029
- // No prefix filter — return all tools from this backend
1030
- return allActive.filter(t => t._backend === group.source);
1031
- }
1032
- return allActive.filter(t =>
1033
- t._backend === group.source && group.prefixes.some(p => t._originalName.startsWith(p))
1034
- );
1035
- }
1036
-
1037
- // Group display names for the Chat UI
1038
- const GROUP_DISPLAY_NAMES = {
1039
- core: "Core Tools",
1040
- intelligence: "Intelligence & Learning",
1041
- agents: "Agents & Orchestration",
1042
- memory: "Memory & Knowledge",
1043
- devtools: "Dev Tools & Analysis",
1044
- security: "Security & Safety",
1045
- browser: "Browser Automation",
1046
- neural: "Neural & DAA",
1047
- "agentic-flow": "Agentic Flow",
1048
- "claude-code": "Claude Code",
1049
- gemini: "Gemini",
1050
- codex: "Codex",
1051
- };
1052
-
1053
- // =============================================================================
1054
- // MCP SERVER — Multiple endpoints per group
1055
- // =============================================================================
1056
-
1057
- const app = express();
1058
- app.use(express.json({ limit: "10mb" }));
1059
-
1060
- // ---------- MCP Streamable HTTP session (#2425 djimit) ----------
1061
- // Streamable-HTTP clients (Codex/RMCP) send `DELETE /mcp` with an
1062
- // `Mcp-Session-Id` header at shutdown. We echo a stable session id back
1063
- // on every /mcp* response so those clients can attach it to the DELETE
1064
- // and to `notifications/initialized` handshakes.
1065
- const MCP_SESSION_ID = randomUUID();
1066
- app.use((req, res, next) => {
1067
- if (req.path.startsWith("/mcp")) {
1068
- res.setHeader("Mcp-Session-Id", MCP_SESSION_ID);
1069
- }
1070
- next();
1071
- });
1072
-
1073
- // ---------- CORS middleware (ADR-166 §6 Phase 3b) ----------
1074
- // MCP_CORS_ORIGIN: comma-separated allowlist (e.g. "https://a.example,https://b.example").
1075
- // unset → "*" for back-compat (loopback default is same-origin anyway)
1076
- // "*" → wildcard (explicit opt-in — same as legacy behavior)
1077
- // other → echo the request origin ONLY if it appears in the allowlist
1078
- const CORS_ALLOWLIST = (process.env.MCP_CORS_ORIGIN || "*")
1079
- .split(",").map(s => s.trim()).filter(Boolean);
1080
- const CORS_WILDCARD = CORS_ALLOWLIST.length === 1 && CORS_ALLOWLIST[0] === "*";
1081
- app.use((req, res, next) => {
1082
- const origin = req.get("origin") || "";
1083
- if (CORS_WILDCARD) {
1084
- res.setHeader("Access-Control-Allow-Origin", "*");
1085
- } else if (origin && CORS_ALLOWLIST.includes(origin)) {
1086
- res.setHeader("Access-Control-Allow-Origin", origin);
1087
- res.setHeader("Vary", "Origin");
1088
- }
1089
- // If no match, do NOT set the header — browser will block the request.
1090
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
1091
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id");
1092
- if (req.method === "OPTIONS") return res.sendStatus(204);
1093
- next();
1094
- });
1095
-
1096
- // ---------- Auth middleware ----------
1097
- // No-op in local-only mode (MCP_AUTH_TOKEN unset). Enforces 401 when token is set.
1098
- const MCP_TOKEN = process.env.MCP_AUTH_TOKEN || "";
1099
- function requireAuth(req, res, next) {
1100
- if (req.path === "/health") return next();
1101
- if (!MCP_TOKEN) return next();
1102
- const expected = `Bearer ${MCP_TOKEN}`;
1103
- const got = req.get("authorization") || "";
1104
- const ok = got.length === expected.length &&
1105
- timingSafeEqual(Buffer.from(got), Buffer.from(expected));
1106
- if (!ok) return res.status(401).json({ error: "unauthorized" });
1107
- next();
1108
- }
1109
- app.use(requireAuth);
1110
-
1111
- // ---------- Shared MCP handler ----------
1112
- function createMcpHandler(groupName) {
1113
- return async (req, res) => {
1114
- const { method, id, params } = req.body;
1115
- try {
1116
- switch (method) {
1117
- case "initialize":
1118
- return res.json({
1119
- jsonrpc: "2.0", id,
1120
- result: {
1121
- protocolVersion: "2024-11-05",
1122
- capabilities: { tools: {} },
1123
- serverInfo: { name: `mcp-bridge/${groupName}`, version: "2.0.0" },
1124
- },
1125
- });
1126
- case "tools/list": {
1127
- const tools = getToolsForGroup(groupName);
1128
- return res.json({ jsonrpc: "2.0", id, result: { tools } });
1129
- }
1130
- case "tools/call": {
1131
- const { name, arguments: toolArgs } = params;
1132
- const result = await executeTool(name, toolArgs || {});
1133
- return res.json({
1134
- jsonrpc: "2.0", id,
1135
- result: {
1136
- content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }],
1137
- },
1138
- });
1139
- }
1140
- case "notifications/initialized":
1141
- // MCP streamable-HTTP spec: notifications must return 202 Accepted
1142
- // with an empty body (no jsonrpc envelope).
1143
- return res.status(202).end();
1144
- default:
1145
- return res.json({ jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } });
1146
- }
1147
- } catch (err) {
1148
- console.error(`MCP error [${groupName}/${method}]:`, err);
1149
- return res.json({ jsonrpc: "2.0", id, error: { code: -32603, message: err.message } });
1150
- }
1151
- };
1152
- }
1153
-
1154
- function createMcpSseHandler(groupName) {
1155
- return (req, res) => {
1156
- res.setHeader("Content-Type", "text/event-stream");
1157
- res.setHeader("Cache-Control", "no-cache");
1158
- res.setHeader("Connection", "keep-alive");
1159
- res.write(`data: ${JSON.stringify({ type: "endpoint", url: `/mcp/${groupName}` })}\n\n`);
1160
- };
1161
- }
1162
-
1163
- // ---------- Register per-group endpoints ----------
1164
- for (const groupName of Object.keys(TOOL_GROUPS)) {
1165
- app.post(`/mcp/${groupName}`, createMcpHandler(groupName));
1166
- app.get(`/mcp/${groupName}`, createMcpSseHandler(groupName));
1167
- // #2425 djimit — streamable-HTTP session cleanup
1168
- app.delete(`/mcp/${groupName}`, (_, res) => res.sendStatus(204));
1169
- }
1170
-
1171
- // ---------- Catch-all /mcp — serves ALL enabled tools (backwards-compatible) ----------
1172
- app.post("/mcp", async (req, res) => {
1173
- const { method, id, params } = req.body;
1174
- try {
1175
- switch (method) {
1176
- case "initialize":
1177
- return res.json({
1178
- jsonrpc: "2.0", id,
1179
- result: {
1180
- protocolVersion: "2024-11-05",
1181
- capabilities: { tools: {} },
1182
- serverInfo: { name: "mcp-bridge", version: "2.0.0" },
1183
- },
1184
- });
1185
- case "tools/list": {
1186
- const activeTools = getActiveTools();
1187
- return res.json({ jsonrpc: "2.0", id, result: { tools: [...BUILTIN_TOOLS, ...activeTools] } });
1188
- }
1189
- case "tools/call": {
1190
- const { name, arguments: toolArgs } = params;
1191
- const result = await executeTool(name, toolArgs || {});
1192
- return res.json({
1193
- jsonrpc: "2.0", id,
1194
- result: {
1195
- content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }],
1196
- },
1197
- });
1198
- }
1199
- case "notifications/initialized":
1200
- return res.status(202).end();
1201
- default:
1202
- return res.json({ jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } });
1203
- }
1204
- } catch (err) {
1205
- console.error(`MCP error [${method}]:`, err);
1206
- return res.json({ jsonrpc: "2.0", id, error: { code: -32603, message: err.message } });
1207
- }
1208
- });
1209
-
1210
- app.get("/mcp", (req, res) => {
1211
- res.setHeader("Content-Type", "text/event-stream");
1212
- res.setHeader("Cache-Control", "no-cache");
1213
- res.setHeader("Connection", "keep-alive");
1214
- res.write(`data: ${JSON.stringify({ type: "endpoint", url: "/mcp" })}\n\n`);
1215
- });
1216
-
1217
- // #2425 djimit — streamable-HTTP session cleanup on the catch-all route.
1218
- app.delete("/mcp", (_, res) => res.sendStatus(204));
1219
-
1220
- // ---------- GET /mcp-servers — returns MCP_SERVERS JSON for Chat UI config ----------
1221
- app.get("/mcp-servers", (_, res) => {
1222
- const servers = [];
1223
- for (const [name, group] of Object.entries(TOOL_GROUPS)) {
1224
- if (!group.enabled) continue;
1225
- const tools = getToolsForGroup(name);
1226
- if (tools.length === 0) continue;
1227
- servers.push({
1228
- name: GROUP_DISPLAY_NAMES[name] || name,
1229
- url: `/mcp/${name}`,
1230
- tools: tools.length,
1231
- group: name,
1232
- });
1233
- }
1234
- res.json(servers);
1235
- });
1236
-
1237
- // =============================================================================
1238
- // CHAT COMPLETIONS PROXY
1239
- // =============================================================================
1240
-
1241
- const PROVIDER_ROUTES = {
1242
- openai: { baseURL: "https://api.openai.com/v1/chat/completions", getKey: () => process.env.OPENAI_API_KEY },
1243
- gemini: { baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", getKey: () => process.env.GOOGLE_API_KEY },
1244
- openrouter: { baseURL: "https://openrouter.ai/api/v1/chat/completions", getKey: () => process.env.OPENROUTER_API_KEY },
1245
- };
1246
-
1247
- function resolveProvider(model) {
1248
- if (typeof model === "string") {
1249
- if (model.startsWith("gemini-")) return "gemini";
1250
- if (model.includes("/")) return "openrouter";
1251
- }
1252
- return "openai";
1253
- }
1254
-
1255
- // =============================================================================
1256
- // SYSTEM PROMPT — Injected server-side into every chat completion request
1257
- // =============================================================================
1258
- // This comprehensive prompt teaches the AI how to use all 200+ MCP tools
1259
- // across 5 groups. It is injected as the first system message, ensuring
1260
- // consistent behavior regardless of what preprompt the Chat UI sends.
1261
-
1262
- function buildSystemPrompt() {
1263
- // Build dynamic group status
1264
- const enabledGroups = Object.entries(TOOL_GROUPS)
1265
- .filter(([, g]) => g.enabled)
1266
- .map(([name]) => name);
1267
-
1268
- return `You are an intelligent AI assistant with powerful tools organized into ${enabledGroups.length} active groups.
1269
-
1270
- # CRITICAL RULES
1271
- 1. Use tools proactively — NEVER guess answers, ALWAYS search first
1272
- 2. For ANY factual question, current event, or research request → call \`web_research\` IMMEDIATELY
1273
- 3. NEVER say "I don't have access to real-time information" — you DO via web_research
1274
- 4. NEVER say "I can't search the web" — you CAN via web_research
1275
- 5. Call tools FIRST, present results conversationally AFTER
1276
- 6. When multiple tools could help, call them ALL in parallel
1277
-
1278
- # Tool Groups
1279
-
1280
- Tools prefixed with backend name (e.g., \`ruflo__agent_spawn\`). Always use the full prefixed name.
1281
-
1282
- ## Group 1: Core Tools (always on)
1283
-
1284
- - **search** — Search your knowledge base (documents, workflows, how-tos).
1285
- Use for: internal knowledge, company docs, past conversations.
1286
- \`{"query": "how to process a claim"}\`
1287
-
1288
- - **web_research** — Search the internet via Google. This is your primary research tool.
1289
- ALWAYS use this for: current events, facts, comparisons, any external knowledge.
1290
- Actions:
1291
- - \`search\` — Quick web search. Default, fast. \`{"action": "search", "query": "latest news on X"}\`
1292
- - \`research\` — Deep report with synthesis. \`{"action": "research", "query": "comprehensive analysis of X"}\`
1293
- - \`compare\` — Side-by-side comparison. \`{"action": "compare", "query": "X", "items": ["A", "B"]}\`
1294
- - \`fact_check\` — Verify a claim. \`{"action": "fact_check", "query": "claim to verify"}\`
1295
- - \`goap\` — Multi-step pipeline: decomposes → parallel searches → synthesizes → verifies. BEST for important questions.
1296
- \`{"action": "goap", "query": "complex question requiring thorough research"}\`
1297
-
1298
- - **guidance** — Get help on tool groups or specific tool usage.
1299
- Topics: \`overview\`, \`groups\`, \`agents\`, \`memory\`, \`intelligence\`, \`devtools\`
1300
- \`{"topic": "overview"}\` or \`{"topic": "tool", "tool_name": "ruflo__agent_spawn"}\`
1301
-
1302
- ## Group 2: Intelligence & Learning (ruvector)
1303
- Pattern learning, routing, code analysis, and trajectory tracking. ${TOOL_GROUPS.intelligence.enabled ? "ACTIVE" : "DISABLED"}
1304
-
1305
- ### Essential Intelligence Tools:
1306
- - **ruvector__hooks_route** — Route a task to the optimal agent type. Call this FIRST for complex tasks.
1307
- \`{"task": "describe what needs to be done", "context": ["relevant info"]}\`
1308
- Returns ranked agent recommendations with confidence scores.
1309
- - **ruvector__hooks_remember** — Store a key-value pair in persistent memory for cross-session recall.
1310
- \`{"key": "pattern-name", "value": "what to remember", "namespace": "patterns"}\`
1311
- - **ruvector__hooks_recall** — Retrieve a previously stored memory by key.
1312
- - **ruvector__hooks_suggest_context** — Get contextual suggestions based on current work.
1313
- - **ruvector__hooks_swarm_recommend** — Get swarm topology recommendation for a task type.
1314
- - **ruvector__hooks_capabilities** — List all intelligence system capabilities.
1315
-
1316
- ### Code Analysis:
1317
- - **ruvector__hooks_ast_analyze** — Analyze code structure (AST) of a file.
1318
- - **ruvector__hooks_ast_complexity** — Get complexity metrics for code.
1319
- - **ruvector__hooks_security_scan** — Scan code for security vulnerabilities.
1320
- - **ruvector__hooks_diff_analyze** — Analyze a code diff for risk and impact.
1321
- - **ruvector__hooks_diff_similar** — Find similar past diffs/changes.
1322
-
1323
- ### Trajectory Learning (for multi-step tasks):
1324
- - **ruvector__hooks_trajectory_begin** — Start tracking a multi-step task for learning.
1325
- - **ruvector__hooks_trajectory_step** — Record a step in the current trajectory.
1326
- - **ruvector__hooks_trajectory_end** — End trajectory, triggering pattern extraction.
1327
-
1328
- ### Memory & Compression:
1329
- - **ruvector__hooks_compress** — Compress/summarize long text for efficient storage.
1330
- - **ruvector__hooks_rag_context** — Get RAG context for a query from stored knowledge.
1331
- - **ruvector__hooks_learn** — Force the system to learn from provided examples.
1332
- - **ruvector__hooks_batch_learn** — Learn from multiple examples at once.
1333
- - **ruvector__hooks_stats** — View learning statistics and metrics.
1334
- - **ruvector__hooks_doctor** — Run diagnostics on the intelligence system.
1335
-
1336
- ## Group 3: Agents & Orchestration (ruflo)
1337
- Spawn agents, coordinate swarms, manage tasks and workflows. ${TOOL_GROUPS.agents.enabled ? "ACTIVE" : "DISABLED"}
1338
-
1339
- ### Agent Lifecycle:
1340
- - **ruflo__agent_spawn** — Create a new specialized agent.
1341
- \`{"type": "coder|researcher|tester|reviewer|architect|security", "name": "optional-name"}\`
1342
- Agent types and when to use them:
1343
- - \`coder\` — Write code, implement features, fix bugs
1344
- - \`researcher\` — Find information, analyze documentation, investigate
1345
- - \`tester\` — Write tests, run test suites, validate behavior
1346
- - \`reviewer\` — Review code quality, security, best practices
1347
- - \`architect\` — Design systems, plan architectures, evaluate trade-offs
1348
- - \`security\` — Audit security, find vulnerabilities, recommend fixes
1349
- - **ruflo__agent_status** — Check an agent's current state. \`{"agentId": "agent-xxx"}\`
1350
- - **ruflo__agent_list** — List all active agents with their states.
1351
- - **ruflo__agent_terminate** — Stop an agent. \`{"agentId": "agent-xxx"}\`
1352
- - **ruflo__agent_health** — Health check across all agents.
1353
- - **ruflo__agent_pool** — View the agent pool and available capacity.
1354
-
1355
- ### Swarm Coordination:
1356
- - **ruflo__swarm_init** — Initialize a multi-agent swarm.
1357
- \`{"topology": "hierarchical|mesh|ring|star", "maxAgents": 8, "strategy": "balanced|specialized|adaptive"}\`
1358
- - \`hierarchical\` — Coordinator + workers, best for structured tasks (anti-drift)
1359
- - \`mesh\` — Peer-to-peer, best for collaborative work
1360
- - \`ring\` — Sequential pipeline, best for ordered processing
1361
- - \`star\` — Central hub, best for fan-out parallel work
1362
- - **ruflo__swarm_status** — Get swarm health, topology, and agent states.
1363
- - **ruflo__swarm_health** — Detailed health metrics for the swarm.
1364
- - **ruflo__swarm_shutdown** — Tear down a swarm and all its agents.
1365
-
1366
- ### Task Management:
1367
- - **ruflo__task_create** — Create a tracked task.
1368
- \`{"description": "what needs to be done", "priority": "low|normal|high|critical"}\`
1369
- - **ruflo__task_status** — Check task progress. \`{"taskId": "task-xxx"}\`
1370
- - **ruflo__task_list** — List all tasks with their statuses.
1371
- - **ruflo__task_complete** — Mark a task as done. \`{"taskId": "task-xxx"}\`
1372
- - **ruflo__task_update** — Update task details, status, or assignment.
1373
- - **ruflo__task_cancel** — Cancel a task.
1374
-
1375
- ### Workflow Orchestration:
1376
- - **ruflo__workflow_create** — Define a multi-step workflow with dependencies.
1377
- - **ruflo__workflow_execute** — Run a workflow. \`{"workflowId": "wf-xxx"}\`
1378
- - **ruflo__workflow_status** — Check workflow progress.
1379
- - **ruflo__workflow_template** — Use a pre-built workflow template.
1380
- - **ruflo__workflow_pause** / **ruflo__workflow_resume** — Control workflow execution.
1381
-
1382
- ### Hive-Mind (Distributed Consensus):
1383
- - **ruflo__hive-mind_init** — Start distributed consensus system.
1384
- - **ruflo__hive-mind_spawn** — Add an agent to the hive.
1385
- - **ruflo__hive-mind_consensus** — Run consensus vote across agents.
1386
- - **ruflo__hive-mind_broadcast** — Send message to all hive agents.
1387
- - **ruflo__hive-mind_memory** — Access shared hive memory.
1388
-
1389
- ### Coordination:
1390
- - **ruflo__coordination_topology** — View/change coordination topology.
1391
- - **ruflo__coordination_load_balance** — Distribute work across agents.
1392
- - **ruflo__coordination_orchestrate** — Orchestrate complex multi-agent tasks.
1393
- - **ruflo__coordination_sync** — Synchronize state across agents.
1394
-
1395
- ### Session Management:
1396
- - **ruflo__session_save** — Save current session state.
1397
- - **ruflo__session_restore** — Restore a previous session.
1398
- - **ruflo__session_list** — List available sessions.
1399
-
1400
- ## Group 4: Memory & Knowledge (ruflo)
1401
- Persistent memory, vector search, embeddings, and pattern storage. ${TOOL_GROUPS.memory.enabled ? "ACTIVE" : "DISABLED"}
1402
-
1403
- ### Memory Operations:
1404
- - **ruflo__memory_store** — Store data in persistent memory.
1405
- \`{"key": "my-key", "value": "data to store", "namespace": "default", "tags": ["tag1"]}\`
1406
- - **ruflo__memory_retrieve** — Get stored data by key. \`{"key": "my-key"}\`
1407
- - **ruflo__memory_search** — Semantic vector search across stored memories.
1408
- \`{"query": "what to search for", "limit": 5, "namespace": "default"}\`
1409
- - **ruflo__memory_list** — List all stored keys in a namespace.
1410
- - **ruflo__memory_delete** — Remove a stored memory.
1411
- - **ruflo__memory_stats** — View memory usage statistics.
1412
-
1413
- ### Embeddings:
1414
- - **ruflo__embeddings_generate** — Generate vector embeddings for text.
1415
- - **ruflo__embeddings_compare** — Compare semantic similarity of two texts.
1416
- - **ruflo__embeddings_search** — Search embeddings database by similarity.
1417
- - **ruflo__embeddings_neural** — Generate neural embeddings.
1418
- - **ruflo__embeddings_hyperbolic** — Generate hyperbolic embeddings for hierarchical data.
1419
-
1420
- ### AgentDB (Advanced Pattern Storage):
1421
- - **ruflo__agentdb_pattern-store** — Store a learned pattern with metadata.
1422
- \`{"pattern": "description", "category": "code|debug|architecture", "confidence": 0.9}\`
1423
- - **ruflo__agentdb_pattern-search** — Search patterns by similarity.
1424
- - **ruflo__agentdb_route** — Route a query to the most relevant stored pattern.
1425
- - **ruflo__agentdb_feedback** — Provide feedback on a pattern (reinforcement learning).
1426
- - **ruflo__agentdb_context-synthesize** — Synthesize context from multiple sources.
1427
- - **ruflo__agentdb_semantic-route** — Semantic routing based on stored knowledge.
1428
- - **ruflo__agentdb_consolidate** — Consolidate and deduplicate stored patterns.
1429
- - **ruflo__agentdb_batch** — Batch operations on patterns.
1430
- - **ruflo__agentdb_session-start** / **ruflo__agentdb_session-end** — Session tracking.
1431
- - **ruflo__agentdb_hierarchical-store** / **ruflo__agentdb_hierarchical-recall** — Hierarchical memory.
1432
-
1433
- ## Group 5: Dev Tools & Analysis (ruflo)
1434
- Performance, system health, GitHub integration, code analysis, terminal. ${TOOL_GROUPS.devtools.enabled ? "ACTIVE" : "DISABLED"}
1435
-
1436
- ### System & Performance:
1437
- - **ruflo__system_status** — System health overview.
1438
- - **ruflo__system_metrics** — Detailed performance metrics.
1439
- - **ruflo__system_health** — Health check across all subsystems.
1440
- - **ruflo__performance_report** — Generate performance report.
1441
- - **ruflo__performance_bottleneck** — Identify performance bottlenecks.
1442
- - **ruflo__performance_benchmark** — Run benchmarks.
1443
- - **ruflo__performance_optimize** — Get optimization recommendations.
1444
- - **ruflo__performance_profile** — Profile specific operations.
1445
-
1446
- ### Code Analysis:
1447
- - **ruflo__analyze_diff** — Analyze a code diff.
1448
- - **ruflo__analyze_diff-risk** — Assess risk level of changes.
1449
- - **ruflo__analyze_diff-classify** — Classify type of changes (feature, bugfix, refactor).
1450
- - **ruflo__analyze_diff-reviewers** — Suggest code reviewers.
1451
- - **ruflo__analyze_file-risk** — Assess risk of a specific file.
1452
-
1453
- ### GitHub Integration:
1454
- - **ruflo__github_repo_analyze** — Analyze a GitHub repository.
1455
- \`{"repo": "owner/repo", "analysis_type": "code_quality|performance|security"}\`
1456
- - **ruflo__github_pr_manage** — Manage pull requests (create, review, merge).
1457
- - **ruflo__github_issue_track** — Track and manage issues.
1458
- - **ruflo__github_workflow** — Manage GitHub Actions workflows.
1459
- - **ruflo__github_metrics** — Repository metrics and insights.
1460
-
1461
- ### Terminal Access:
1462
- - **ruflo__terminal_create** — Create a terminal session.
1463
- - **ruflo__terminal_execute** — Execute a command. \`{"command": "ls -la"}\`
1464
- - **ruflo__terminal_list** — List active terminals.
1465
- - **ruflo__terminal_history** — View command history.
1466
-
1467
- ### Development Hooks:
1468
- - **ruflo__hooks_pre-task** / **ruflo__hooks_post-task** — Task lifecycle hooks for learning.
1469
- - **ruflo__hooks_pre-edit** / **ruflo__hooks_post-edit** — File edit hooks.
1470
- - **ruflo__hooks_session-start** / **ruflo__hooks_session-end** — Session lifecycle.
1471
- - **ruflo__hooks_worker-dispatch** — Dispatch background workers.
1472
- Workers: \`optimize\`, \`audit\`, \`testgaps\`, \`document\`, \`map\`, \`deepdive\`, \`benchmark\`
1473
- - **ruflo__hooks_model-route** — Route to optimal AI model for a task.
1474
- - **ruflo__hooks_explain** — Explain a routing or intelligence decision.
1475
-
1476
- ### Configuration:
1477
- - **ruflo__config_get** / **ruflo__config_set** / **ruflo__config_list** — Manage settings.
1478
-
1479
- ### Progress Tracking:
1480
- - **ruflo__progress_check** — Check implementation progress.
1481
- - **ruflo__progress_summary** — Summarize overall progress.
1482
-
1483
- # Decision Framework — FOLLOW THIS EXACTLY
1484
-
1485
- 1. **ANY factual question** → \`web_research(action='search')\` IMMEDIATELY. Do NOT answer from memory.
1486
- 2. **"What is X?"** → \`web_research(action='search', query='X')\`
1487
- 3. **"Compare X vs Y"** → \`web_research(action='compare', query='X vs Y', items=['X', 'Y'])\`
1488
- 4. **"Is it true that..."** → \`web_research(action='fact_check', query='...')\`
1489
- 5. **Complex research** → \`web_research(action='goap', query='...')\` — auto multi-step pipeline
1490
- 6. **Internal docs/procedures** → \`search(query='...')\` first, then web_research if not found
1491
- 7. **Code task** → \`ruvector__hooks_route\` + \`ruflo__agent_spawn\`
1492
- 8. **Remember something** → \`ruflo__memory_store\` / \`ruflo__memory_search\`
1493
- 9. **"What can you do?"** → \`guidance(topic='overview')\`
1494
- 10. **Unknown** → \`guidance(topic='overview')\`
1495
-
1496
- IMPORTANT: For questions 1-5, ALWAYS call web_research. Never say "I don't have access to search" — you DO.
1497
-
1498
- # Execution Patterns
1499
-
1500
- ### Simple Question
1501
- \`search\` or \`web_research\` → synthesize → respond
1502
-
1503
- ### Complex Research
1504
- \`web_research(action='goap')\` → analyze → respond with citations
1505
-
1506
- ### Code Implementation
1507
- \`ruvector__hooks_route\` → \`ruflo__agent_spawn(coder)\` → track with \`ruflo__task_create\` → report
1508
-
1509
- ### Multi-Agent Analysis
1510
- \`ruflo__swarm_init(hierarchical)\` → spawn agents → coordinate → synthesize results
1511
-
1512
- ### Learning & Memory
1513
- \`ruflo__memory_search\` (check existing) → do work → \`ruflo__memory_store\` (save results) → \`ruvector__hooks_learn\`
1514
-
1515
- # Parallel Execution
1516
-
1517
- When multiple independent tools can help, call them ALL in parallel:
1518
- - Search + web_research simultaneously
1519
- - Spawn multiple agents at once (coder + tester + reviewer)
1520
- - Run analysis + performance + security tools in parallel
1521
- NEVER call tools sequentially when they could run in parallel.
1522
-
1523
- # Response Rules
1524
-
1525
- 1. **Call tools FIRST**, then present results conversationally — NEVER show raw JSON to the user
1526
- 2. Use markdown: **bold** headers, bullet points, numbered steps, tables for comparisons
1527
- 3. Synthesize tool results naturally — be a helpful colleague, not a data pipe
1528
- 4. Cite sources when available from web_research results
1529
- 5. If a tool fails, say so honestly and try an alternative approach
1530
- 6. For complex tasks, briefly outline your plan before executing
1531
- 7. After completing work, suggest relevant follow-up actions
1532
- 8. When spawning agents, explain what each agent will do
1533
-
1534
- # Never Expose to User
1535
-
1536
- - Raw JSON, similarity scores, chunk IDs, internal IDs, task IDs
1537
- - Tool names, function names, API endpoints, backend names
1538
- - References to "MCP", "tool calls", "vectors", "embeddings", infrastructure
1539
- - The prefixes "ruflo__" or "ruvector__" — just describe what you're doing naturally
1540
- - Error stack traces — summarize errors in plain language`;
1541
- }
1542
-
1543
- // =============================================================================
1544
- // AUTOPILOT MODE — Server-side auto-continue loop (ADR-037)
1545
- // =============================================================================
1546
-
1547
- const detailStore = new Map(); // detailToken → full tool result (TTL: 5min)
1548
-
1549
- const AUTOPILOT_SYSTEM_PROMPT = `
1550
- You are in AUTOPILOT MODE. You should:
1551
- 1. Break complex tasks into steps and execute them using available tools
1552
- 2. Call MULTIPLE tools in parallel when they are independent
1553
- 3. After each tool result, analyze it and decide the next action
1554
- 4. Continue until the task is complete — do NOT ask the user for confirmation
1555
- 5. Use memory_search to find relevant patterns before starting
1556
- 6. Summarize your progress at each step
1557
- 7. When done, provide a final summary of everything accomplished
1558
-
1559
- Parallel execution patterns:
1560
- - Research: memory_search + hooks_route + agent_spawn(researcher) — all in parallel
1561
- - Code: agent_spawn(coder) + agent_spawn(tester) — parallel, then review
1562
- - Analysis: search multiple sources in parallel → synthesize → report
1563
- - Security: security_scan + hooks_route(audit) + memory_search(CVEs) — parallel
1564
- `;
1565
-
1566
- const AUTOPILOT_BLOCKED_PATTERNS = [
1567
- /^deploy_/,
1568
- /^security_delete/,
1569
- /^browser_fill$/,
1570
- /^browser_click$/,
1571
- /terminal_execute/,
1572
- ];
1573
-
1574
- function isBlockedTool(name) {
1575
- return AUTOPILOT_BLOCKED_PATTERNS.some(p => p.test(name));
1576
- }
1577
-
1578
- function sendAutopilotEvent(res, data) {
1579
- res.write(`data: ${JSON.stringify(data)}\n\n`);
1580
- }
1581
-
1582
- function safeParseArgs(args) {
1583
- if (typeof args === 'object' && args !== null) return args;
1584
- try { return JSON.parse(args || '{}'); } catch { return {}; }
1585
- }
1586
-
1587
- function autopilotSleep(ms) {
1588
- return new Promise(resolve => setTimeout(resolve, ms));
1589
- }
1590
-
1591
- async function handleAutopilot(req, res, provider, body) {
1592
- const maxSteps = Math.min(parseInt(req.headers['x-autopilot-max-steps'] || '20', 10), 50);
1593
- const cooldownMs = parseInt(process.env.AUTOPILOT_COOLDOWN || '500', 10);
1594
- const stepTimeoutMs = parseInt(process.env.AUTOPILOT_STEP_TIMEOUT || '30000', 10);
1595
-
1596
- // SSE setup
1597
- res.setHeader('Content-Type', 'text/event-stream');
1598
- res.setHeader('Cache-Control', 'no-cache');
1599
- res.setHeader('Connection', 'keep-alive');
1600
- res.setHeader('X-Accel-Buffering', 'no');
1601
-
1602
- let messages = [...body.messages];
1603
- let step = 0;
1604
- let aborted = false;
1605
- let totalTasks = 0;
1606
- const startTime = Date.now();
1607
-
1608
- req.on('close', () => { aborted = true; });
1609
-
1610
- sendAutopilotEvent(res, { type: 'autopilot_start', maxSteps });
1611
-
1612
- // Get the tools list for the AI provider (OpenAI function calling format)
1613
- // Limit to BUILTIN_TOOLS + top external tools to avoid overwhelming the model
1614
- // (Gemini struggles with 100+ tool definitions)
1615
- const MAX_AUTOPILOT_TOOLS = 30;
1616
- const externalTools = getActiveTools();
1617
- // Prioritize tools from enabled high-value groups
1618
- const priorityPrefixes = ['ruvector__hooks', 'ruvector__memory', 'ruflo__memory', 'ruflo__agent'];
1619
- const prioritized = externalTools
1620
- .sort((a, b) => {
1621
- const aP = priorityPrefixes.some(p => a.name.startsWith(p)) ? 0 : 1;
1622
- const bP = priorityPrefixes.some(p => b.name.startsWith(p)) ? 0 : 1;
1623
- return aP - bP;
1624
- })
1625
- .slice(0, MAX_AUTOPILOT_TOOLS - BUILTIN_TOOLS.length);
1626
- const allTools = [...BUILTIN_TOOLS, ...prioritized];
1627
- const toolDefs = allTools.map(t => ({
1628
- type: 'function',
1629
- function: {
1630
- name: t.name,
1631
- description: t.description || '',
1632
- parameters: t.inputSchema || { type: 'object', properties: {} },
1633
- },
1634
- }));
1635
- console.log(`[autopilot] ${allTools.length} tools (${BUILTIN_TOOLS.length} builtin + ${prioritized.length} external)`);
1636
-
1637
- while (step < maxSteps && !aborted) {
1638
- // 1. Call upstream AI provider (non-streaming for tool call parsing)
1639
- const apiKey = provider.getKey();
1640
- let aiResult;
1641
- try {
1642
- const aiResponse = await fetch(provider.baseURL, {
1643
- method: 'POST',
1644
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
1645
- body: JSON.stringify({
1646
- ...body,
1647
- messages,
1648
- stream: false,
1649
- tools: toolDefs.length > 0 ? toolDefs : undefined,
1650
- }),
1651
- signal: AbortSignal.timeout(stepTimeoutMs),
1652
- });
1653
- aiResult = await aiResponse.json();
1654
- } catch (err) {
1655
- sendAutopilotEvent(res, { type: 'autopilot_error', error: `AI call failed: ${err.message}` });
1656
- break;
1657
- }
1658
-
1659
- const choice = aiResult.choices?.[0];
1660
- if (!choice) {
1661
- sendAutopilotEvent(res, { type: 'autopilot_error', error: 'No response from AI' });
1662
- break;
1663
- }
1664
-
1665
- // 2. Check for tool calls
1666
- const toolCalls = choice.message?.tool_calls;
1667
-
1668
- if (!toolCalls || toolCalls.length === 0) {
1669
- // Final text response — send it
1670
- sendAutopilotEvent(res, { type: 'autopilot_text', content: choice.message?.content || '' });
1671
- break;
1672
- }
1673
-
1674
- // 3. Execute ALL tool calls in parallel
1675
- step++;
1676
- const groupId = `g${step}`;
1677
- const taskEvents = toolCalls.map((tc, i) => ({
1678
- taskId: `t${totalTasks + i + 1}`,
1679
- tool: tc.function.name,
1680
- args: safeParseArgs(tc.function.arguments),
1681
- status: 'running',
1682
- }));
1683
- totalTasks += taskEvents.length;
1684
-
1685
- // If the AI also included text content, stream it before tools
1686
- if (choice.message?.content) {
1687
- sendAutopilotEvent(res, { type: 'autopilot_text', content: choice.message.content });
1688
- }
1689
-
1690
- // Stream group start
1691
- sendAutopilotEvent(res, { type: 'task_group_start', groupId, step, tasks: taskEvents });
1692
-
1693
- // Append assistant message to conversation
1694
- messages.push(choice.message);
1695
-
1696
- // Execute tools in parallel
1697
- const groupStart = Date.now();
1698
- const results = await Promise.allSettled(
1699
- toolCalls.map(async (tc, i) => {
1700
- const taskId = taskEvents[i].taskId;
1701
- const toolName = tc.function.name;
1702
- const toolArgs = safeParseArgs(tc.function.arguments);
1703
- const taskStart = Date.now();
1704
-
1705
- // Check blocklist
1706
- if (isBlockedTool(toolName)) {
1707
- sendAutopilotEvent(res, {
1708
- type: 'task_update', taskId, status: 'blocked',
1709
- summary: `${toolName} requires confirmation`,
1710
- duration: Date.now() - taskStart,
1711
- });
1712
- return { toolCallId: tc.id, blocked: true, toolName };
1713
- }
1714
-
1715
- try {
1716
- const result = await executeTool(toolName, toolArgs);
1717
- const resultStr = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
1718
-
1719
- // Store full detail, generate token for lazy loading
1720
- const detailToken = `dt_${taskId}_${Date.now()}`;
1721
- detailStore.set(detailToken, resultStr);
1722
-
1723
- // Stream task completion with summary only
1724
- const summary = resultStr.length > 120
1725
- ? resultStr.substring(0, 120).replace(/\n/g, ' ') + '...'
1726
- : resultStr.replace(/\n/g, ' ');
1727
-
1728
- sendAutopilotEvent(res, {
1729
- type: 'task_update', taskId, status: 'completed',
1730
- summary, duration: Date.now() - taskStart, detailToken,
1731
- });
1732
-
1733
- return { toolCallId: tc.id, content: resultStr };
1734
- } catch (err) {
1735
- sendAutopilotEvent(res, {
1736
- type: 'task_update', taskId, status: 'failed',
1737
- summary: err.message, duration: Date.now() - taskStart,
1738
- });
1739
- return { toolCallId: tc.id, content: `Error: ${err.message}` };
1740
- }
1741
- })
1742
- );
1743
-
1744
- // Stream group end
1745
- sendAutopilotEvent(res, { type: 'task_group_end', groupId, step, duration: Date.now() - groupStart });
1746
-
1747
- // Check if any tools were blocked — pause autopilot
1748
- const blockedResults = results
1749
- .filter(r => r.status === 'fulfilled' && r.value.blocked)
1750
- .map(r => r.value);
1751
- if (blockedResults.length > 0) {
1752
- sendAutopilotEvent(res, {
1753
- type: 'autopilot_paused',
1754
- reason: 'blocked_tools',
1755
- tools: blockedResults.map(b => b.toolName),
1756
- });
1757
- break;
1758
- }
1759
-
1760
- // Append tool results to messages
1761
- for (const r of results) {
1762
- if (r.status === 'fulfilled' && !r.value.blocked) {
1763
- messages.push({
1764
- role: 'tool',
1765
- tool_call_id: r.value.toolCallId,
1766
- content: r.value.content,
1767
- });
1768
- }
1769
- }
1770
-
1771
- // Cooldown to prevent runaway
1772
- await autopilotSleep(cooldownMs);
1773
- }
1774
-
1775
- if (step >= maxSteps && !aborted) {
1776
- sendAutopilotEvent(res, {
1777
- type: 'autopilot_text',
1778
- content: `\n⚠️ Autopilot reached max steps (${maxSteps}). Stopping.\n`,
1779
- });
1780
- }
1781
-
1782
- sendAutopilotEvent(res, {
1783
- type: 'autopilot_end',
1784
- totalSteps: step,
1785
- totalTasks,
1786
- duration: Date.now() - startTime,
1787
- });
1788
-
1789
- res.write('data: [DONE]\n\n');
1790
- res.end();
1791
-
1792
- // Clean up detail store after 5 minutes
1793
- const detailTTL = parseInt(process.env.AUTOPILOT_DETAIL_TTL || '300000', 10);
1794
- setTimeout(() => {
1795
- for (const [key] of detailStore) {
1796
- if (key.startsWith('dt_')) detailStore.delete(key);
1797
- }
1798
- }, detailTTL);
1799
- }
1800
-
1801
- // Lazy detail loading endpoint
1802
- app.get('/autopilot/detail/:token', (req, res) => {
1803
- const content = detailStore.get(req.params.token);
1804
- if (content) {
1805
- res.json({ content });
1806
- } else {
1807
- res.status(404).json({ error: 'Detail expired or not found' });
1808
- }
1809
- });
1810
-
1811
- // =============================================================================
1812
- // CHAT COMPLETIONS PROXY
1813
- // =============================================================================
1814
-
1815
- app.post("/chat/completions", async (req, res) => {
1816
- const model = req.body?.model;
1817
- const providerName = resolveProvider(model);
1818
- const provider = PROVIDER_ROUTES[providerName];
1819
- const apiKey = provider.getKey();
1820
-
1821
- if (!apiKey) return res.status(401).json({ error: { message: `No API key for provider: ${providerName}` } });
1822
-
1823
- // Inject comprehensive system prompt as the first message
1824
- const body = { ...req.body };
1825
- if (body.messages && Array.isArray(body.messages)) {
1826
- let systemPrompt = buildSystemPrompt();
1827
- // Add autopilot instructions if autopilot mode is active
1828
- const isAutopilot = req.headers['x-autopilot'] === 'true';
1829
- if (isAutopilot) {
1830
- systemPrompt = AUTOPILOT_SYSTEM_PROMPT + '\n\n' + systemPrompt;
1831
- }
1832
- // Prepend our system prompt before any existing messages
1833
- const hasSystemMsg = body.messages[0]?.role === "system";
1834
- if (hasSystemMsg) {
1835
- // Merge with existing system message
1836
- body.messages = [
1837
- { role: "system", content: systemPrompt + "\n\n" + body.messages[0].content },
1838
- ...body.messages.slice(1),
1839
- ];
1840
- } else {
1841
- body.messages = [{ role: "system", content: systemPrompt }, ...body.messages];
1842
- }
1843
- }
1844
-
1845
- // Route to autopilot handler if x-autopilot header is set
1846
- if (req.headers['x-autopilot'] === 'true') {
1847
- return handleAutopilot(req, res, provider, body);
1848
- }
1849
-
1850
- try {
1851
- const upstream = await fetch(provider.baseURL, {
1852
- method: "POST",
1853
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
1854
- body: JSON.stringify(body),
1855
- });
1856
-
1857
- if (!upstream.ok) {
1858
- const errBody = await upstream.text();
1859
- console.error(`Proxy error [${providerName}/${model}]: ${upstream.status} ${errBody.substring(0, 200)}`);
1860
- // Normalize all upstream errors into OpenAI-compatible format so the
1861
- // Chat UI's OpenAI SDK can parse them instead of "400 (no body)".
1862
- let errorMessage = `Upstream ${providerName} error (${upstream.status})`;
1863
- try {
1864
- const parsed = JSON.parse(errBody);
1865
- // Gemini returns [{"error": {"message": "..."}}]
1866
- if (Array.isArray(parsed) && parsed[0]?.error?.message) {
1867
- errorMessage = parsed[0].error.message;
1868
- // OpenAI/OpenRouter return {"error": {"message": "..."}}
1869
- } else if (parsed.error?.message) {
1870
- errorMessage = parsed.error.message;
1871
- }
1872
- } catch {}
1873
- return res.status(upstream.status).json({
1874
- error: { message: errorMessage, type: "upstream_error", code: upstream.status },
1875
- });
1876
- }
1877
-
1878
- res.setHeader("Content-Type", upstream.headers.get("content-type") || "application/json");
1879
-
1880
- if (req.body?.stream && upstream.body) {
1881
- const reader = upstream.body.getReader();
1882
- const decoder = new TextDecoder();
1883
- try {
1884
- while (true) {
1885
- const { done, value } = await reader.read();
1886
- if (done) break;
1887
- res.write(decoder.decode(value, { stream: true }));
1888
- }
1889
- } catch (e) { /* stream closed */ }
1890
- finally { res.end(); }
1891
- } else {
1892
- res.send(await upstream.text());
1893
- }
1894
- } catch (err) {
1895
- console.error(`Proxy error [${providerName}/${model}]:`, err.message);
1896
- res.status(502).json({ error: { message: `Upstream error: ${err.message}` } });
1897
- }
1898
- });
1899
-
1900
- // =============================================================================
1901
- // MODELS & HEALTH
1902
- // =============================================================================
1903
-
1904
- const KNOWN_MODELS = [
1905
- "gemini-2.5-pro", "gemini-2.5-flash",
1906
- "gpt-4.1", "gpt-4.1-mini", "gpt-4o", "gpt-4o-mini",
1907
- "o3-mini", "o1-mini",
1908
- ];
1909
-
1910
- app.get("/models", (_, res) => {
1911
- res.json({ object: "list", data: KNOWN_MODELS.map(id => ({
1912
- id,
1913
- object: "model",
1914
- owned_by: "system",
1915
- providers: [{ supports_tools: true }],
1916
- })) });
1917
- });
1918
-
1919
- app.get("/health", (_, res) => {
1920
- const backends = {};
1921
- for (const [name, client] of mcpBackends) {
1922
- backends[name] = { ready: client.ready, tools: client.tools.length };
1923
- }
1924
- const activeTools = getActiveTools();
1925
- const groups = {};
1926
- for (const [name, g] of Object.entries(TOOL_GROUPS)) {
1927
- groups[name] = { enabled: g.enabled, source: g.source };
1928
- }
1929
- res.json({
1930
- status: "ok", service: "mcp-bridge", version: "2.0.0",
1931
- tools: { builtin: BUILTIN_TOOLS.length, external: activeTools.length, total: BUILTIN_TOOLS.length + activeTools.length },
1932
- groups, backends,
1933
- });
1934
- });
1935
-
1936
- // GET /groups — list tool groups and their status
1937
- app.get("/groups", (_, res) => {
1938
- const activeTools = getActiveTools();
1939
- const result = {};
1940
- for (const [name, g] of Object.entries(TOOL_GROUPS)) {
1941
- const tools = name === "core" ? BUILTIN_TOOLS :
1942
- activeTools.filter(t => {
1943
- if (g.source !== t._backend) return false;
1944
- if (!g.prefixes) return true;
1945
- return g.prefixes.some(p => t._originalName.startsWith(p));
1946
- });
1947
- result[name] = {
1948
- enabled: g.enabled,
1949
- description: g.description,
1950
- tools: tools.length,
1951
- toolNames: tools.map(t => t.name).slice(0, 10),
1952
- };
1953
- }
1954
- res.json(result);
1955
- });
1956
-
1957
- // =============================================================================
1958
- // STARTUP
1959
- // =============================================================================
1960
-
1961
- async function main() {
1962
- const isPublic = BIND_HOST !== "127.0.0.1" && BIND_HOST !== "localhost";
1963
- if (isPublic && !process.env.MCP_AUTH_TOKEN) {
1964
- console.error(
1965
- "FATAL: refusing to bind a public interface without MCP_AUTH_TOKEN. " +
1966
- "Generate one with: MCP_AUTH_TOKEN=$(openssl rand -base64 32)"
1967
- );
1968
- process.exit(1);
1969
- }
1970
- app.listen(PORT, BIND_HOST, () => {
1971
- console.log(`MCP Bridge v2.0.0 on port ${PORT} (${BIND_HOST})`);
1972
- const enabled = Object.entries(TOOL_GROUPS).filter(([, g]) => g.enabled).map(([n]) => n);
1973
- console.log(`Active groups: ${enabled.join(", ")}`);
1974
- // ADR-166 §6 — startup posture banner (helps operators see the security state at boot)
1975
- console.log(
1976
- `[security] bind=${BIND_HOST} auth=${process.env.MCP_AUTH_TOKEN ? "bearer" : "off (local-only)"} ` +
1977
- `terminal=${MCP_ENABLE_TERMINAL ? "ENABLED (⚠ opt-in)" : "disabled"}`,
1978
- );
1979
- if (MCP_ENABLE_TERMINAL) {
1980
- console.warn(
1981
- "[security] WARNING: terminal_execute is enabled. This tool grants shell access " +
1982
- "inside the bridge container to any client the auth layer accepts. Ensure " +
1983
- "MCP_AUTH_TOKEN is set on any non-loopback bind. See ADR-166 §6 Phase 1d.",
1984
- );
1985
- }
1986
- });
1987
-
1988
- const anyBackendNeeded = BACKEND_DEFS.some(isBackendNeeded);
1989
- if (anyBackendNeeded) {
1990
- console.log("Initializing MCP backends...");
1991
- await initBackends();
1992
- }
1993
- }
1994
-
1995
- main().catch(err => { console.error("Fatal:", err); process.exit(1); });
1
+ import express from "express";
2
+ import { spawn } from "child_process";
3
+ import { randomUUID, timingSafeEqual } from "crypto";
4
+
5
+ // =============================================================================
6
+ // CONFIGURATION
7
+ // =============================================================================
8
+
9
+ const CLOUD_FUNCTIONS = {
10
+ search: process.env.SEARCH_API_URL || null,
11
+ research: process.env.RESEARCH_API_URL || null,
12
+ };
13
+
14
+ const PORT = parseInt(process.env.PORT || "3001", 10);
15
+ const BIND_HOST = process.env.MCP_BIND_HOST || "127.0.0.1";
16
+
17
+ // =============================================================================
18
+ // TOOL GROUPS — Enable/disable categories of tools independently
19
+ // =============================================================================
20
+ // Groups map tool name prefixes from backends to logical categories.
21
+ // Each group can be toggled via env var. The AI sees only enabled tools.
22
+
23
+ const TOOL_GROUPS = {
24
+ // --- Core (always on, built-in) ---
25
+ core: {
26
+ enabled: true, // cannot be disabled
27
+ description: "Search, research, and guidance tools",
28
+ source: "builtin",
29
+ },
30
+
31
+ // --- Intelligence (ruvector) ---
32
+ intelligence: {
33
+ enabled: process.env.MCP_GROUP_INTELLIGENCE !== "false",
34
+ description: "Self-learning intelligence — routing, memory, pattern training (ruvector)",
35
+ source: "ruvector",
36
+ prefixes: ["hooks_"],
37
+ },
38
+
39
+ // --- Agents & Orchestration (ruflo) ---
40
+ agents: {
41
+ enabled: process.env.MCP_GROUP_AGENTS !== "false",
42
+ description: "Agent lifecycle, swarm coordination, task management, workflows (ruflo)",
43
+ source: "ruflo",
44
+ prefixes: ["agent_", "swarm_", "task_", "session_", "hive-mind_", "workflow_", "coordination_"],
45
+ },
46
+
47
+ // --- Memory & Knowledge (ruflo) ---
48
+ memory: {
49
+ enabled: process.env.MCP_GROUP_MEMORY !== "false",
50
+ description: "Vector memory, AgentDB, embeddings, semantic search (ruflo)",
51
+ source: "ruflo",
52
+ prefixes: ["memory_", "agentdb_", "embeddings_"],
53
+ },
54
+
55
+ // --- Dev Tools (ruflo) ---
56
+ devtools: {
57
+ enabled: process.env.MCP_GROUP_DEVTOOLS !== "false",
58
+ description: "Hooks, code analysis, performance profiling, GitHub integration (ruflo)",
59
+ source: "ruflo",
60
+ prefixes: ["hooks_", "analyze_", "performance_", "github_", "terminal_", "config_", "system_", "progress_"],
61
+ },
62
+
63
+ // --- Security & Safety (ruflo) ---
64
+ security: {
65
+ enabled: process.env.MCP_GROUP_SECURITY === "true",
66
+ description: "AI defence, PII detection, claims management, pattern transfer (ruflo)",
67
+ source: "ruflo",
68
+ prefixes: ["aidefence_", "claims_", "transfer_"],
69
+ },
70
+
71
+ // --- Browser Automation (ruflo) ---
72
+ browser: {
73
+ enabled: process.env.MCP_GROUP_BROWSER === "true",
74
+ description: "Headless browser control — navigate, click, fill, screenshot (ruflo)",
75
+ source: "ruflo",
76
+ prefixes: ["browser_"],
77
+ },
78
+
79
+ // --- Neural & DAA (ruflo) ---
80
+ neural: {
81
+ enabled: process.env.MCP_GROUP_NEURAL === "true",
82
+ description: "Neural network training, DAA autonomous agents, cognitive patterns (ruflo)",
83
+ source: "ruflo",
84
+ prefixes: ["neural_", "daa_"],
85
+ },
86
+
87
+ // --- Agentic Flow (agentic-flow@alpha) ---
88
+ "agentic-flow": {
89
+ enabled: process.env.MCP_GROUP_AGENTIC_FLOW === "true",
90
+ description: "Execute 66+ specialized agents, batch code editing, AgentDB patterns (agentic-flow)",
91
+ source: "agentic-flow",
92
+ prefixes: ["agentic_flow_", "agent_booster_", "agentdb_"],
93
+ },
94
+
95
+ // --- Claude Code ---
96
+ "claude-code": {
97
+ enabled: process.env.MCP_GROUP_CLAUDE_CODE === "true",
98
+ description: "Anthropic Claude Code — file editing, bash execution, code analysis (requires ANTHROPIC_API_KEY)",
99
+ source: "claude",
100
+ },
101
+
102
+ // --- Gemini MCP ---
103
+ gemini: {
104
+ enabled: process.env.MCP_GROUP_GEMINI === "true",
105
+ description: "Google Gemini conversation context, multimodal capabilities (requires GOOGLE_API_KEY)",
106
+ source: "gemini-mcp",
107
+ },
108
+
109
+ // --- OpenAI Codex ---
110
+ codex: {
111
+ enabled: process.env.MCP_GROUP_CODEX === "true",
112
+ description: "OpenAI Codex coding agent — code generation and execution (requires OPENAI_API_KEY)",
113
+ source: "codex",
114
+ },
115
+ };
116
+
117
+ // =============================================================================
118
+ // STDIO MCP CLIENT — Connects to external MCP servers via child process
119
+ // =============================================================================
120
+
121
+ class StdioMcpClient {
122
+ constructor(name, command, args = []) {
123
+ this.name = name;
124
+ this.command = command;
125
+ this.args = args;
126
+ this.process = null;
127
+ this.tools = [];
128
+ this.ready = false;
129
+ this.pending = new Map();
130
+ this.buffer = "";
131
+ }
132
+
133
+ async start() {
134
+ return new Promise((resolve) => {
135
+ try {
136
+ this.process = spawn(this.command, this.args, {
137
+ stdio: ["pipe", "pipe", "pipe"],
138
+ env: { ...process.env },
139
+ });
140
+
141
+ this.process.stdout.on("data", (data) => this._onData(data.toString()));
142
+ this.process.stderr.on("data", (data) => {
143
+ const msg = data.toString().trim();
144
+ if (msg && !msg.startsWith("npm WARN")) console.error(`[${this.name}] ${msg}`);
145
+ });
146
+ this.process.on("error", (err) => {
147
+ console.error(`[${this.name}] spawn error:`, err.message);
148
+ this.ready = false;
149
+ resolve(false);
150
+ });
151
+ this.process.on("exit", (code) => {
152
+ console.log(`[${this.name}] exited with code ${code}`);
153
+ this.ready = false;
154
+ });
155
+
156
+ this._send("initialize", {
157
+ protocolVersion: "2024-11-05",
158
+ capabilities: {},
159
+ clientInfo: { name: "mcp-bridge", version: "2.0.0" },
160
+ }).then((result) => {
161
+ if (result && !result.error) {
162
+ this._notify("notifications/initialized", {});
163
+ return this._send("tools/list", {});
164
+ }
165
+ return null;
166
+ }).then((result) => {
167
+ if (result && result.tools) {
168
+ this.tools = result.tools.map(t => ({
169
+ ...t,
170
+ _originalName: t.name,
171
+ _backend: this.name,
172
+ }));
173
+ this.ready = true;
174
+ console.log(`[${this.name}] ${this.tools.length} tools loaded`);
175
+ }
176
+ resolve(this.ready);
177
+ }).catch((err) => {
178
+ console.error(`[${this.name}] init failed:`, err.message);
179
+ resolve(false);
180
+ });
181
+
182
+ setTimeout(() => { if (!this.ready) resolve(false); }, 60000);
183
+ } catch (err) {
184
+ console.error(`[${this.name}] failed to start:`, err.message);
185
+ resolve(false);
186
+ }
187
+ });
188
+ }
189
+
190
+ _onData(chunk) {
191
+ this.buffer += chunk;
192
+ const lines = this.buffer.split("\n");
193
+ this.buffer = lines.pop() || "";
194
+ for (const line of lines) {
195
+ const trimmed = line.trim();
196
+ if (!trimmed) continue;
197
+ try {
198
+ const msg = JSON.parse(trimmed);
199
+ if (msg.id && this.pending.has(msg.id)) {
200
+ const { resolve } = this.pending.get(msg.id);
201
+ this.pending.delete(msg.id);
202
+ resolve(msg.result || msg.error || {});
203
+ }
204
+ } catch { /* skip non-JSON */ }
205
+ }
206
+ }
207
+
208
+ _send(method, params) {
209
+ return new Promise((resolve, reject) => {
210
+ if (!this.process || this.process.killed) {
211
+ return reject(new Error(`${this.name} process not running`));
212
+ }
213
+ const id = randomUUID();
214
+ const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
215
+ this.pending.set(id, { resolve, reject });
216
+ this.process.stdin.write(msg);
217
+ // initialize is the cold-start gate for backends like ruflo/ruvector
218
+ // which boot a full claude-flow / ruvector kernel — on Cloud Run with
219
+ // npx fetching artifacts it can take 45-60s. Other RPC methods are
220
+ // post-init and stay snappy.
221
+ const timeoutMs = method === "initialize" ? 120000 : 30000;
222
+ setTimeout(() => {
223
+ if (this.pending.has(id)) {
224
+ this.pending.delete(id);
225
+ reject(new Error(`${this.name} timeout for ${method}`));
226
+ }
227
+ }, timeoutMs);
228
+ });
229
+ }
230
+
231
+ _notify(method, params) {
232
+ if (!this.process || this.process.killed) return;
233
+ this.process.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n");
234
+ }
235
+
236
+ async callTool(originalName, args) {
237
+ if (!this.ready) return { error: `${this.name} backend not available` };
238
+ try {
239
+ return await this._send("tools/call", { name: originalName, arguments: args });
240
+ } catch (err) {
241
+ return { error: err.message };
242
+ }
243
+ }
244
+
245
+ stop() {
246
+ if (this.process && !this.process.killed) {
247
+ this.process.kill("SIGTERM");
248
+ this.process = null;
249
+ }
250
+ this.ready = false;
251
+ this.tools = [];
252
+ }
253
+ }
254
+
255
+ // =============================================================================
256
+ // BACKEND REGISTRY
257
+ // =============================================================================
258
+
259
+ const BACKEND_DEFS = [
260
+ { name: "ruvector", command: "npx", args: ["-y", "ruvector", "mcp", "start"], groups: ["intelligence"] },
261
+ { name: "ruflo", command: "npx", args: ["-y", "ruflo", "mcp", "start"], groups: ["agents", "memory", "devtools", "security", "browser", "neural"] },
262
+ { name: "agentic-flow", command: "npx", args: ["-y", "agentic-flow@alpha", "mcp", "start"], groups: ["agentic-flow"] },
263
+ { name: "claude", command: "claude", args: ["mcp", "serve"], groups: ["claude-code"] },
264
+ { name: "gemini-mcp", command: "npx", args: ["-y", "gemini-mcp-server"], groups: ["gemini"] },
265
+ { name: "codex", command: "npx", args: ["-y", "@openai/codex", "mcp-server"], groups: ["codex"] },
266
+ ];
267
+
268
+ const mcpBackends = new Map();
269
+ let allBackendTools = []; // all tools from all backends (pre-filter)
270
+
271
+ function isBackendNeeded(backendDef) {
272
+ return backendDef.groups.some(g => TOOL_GROUPS[g]?.enabled);
273
+ }
274
+
275
+ // Filter tools from a backend based on which groups are enabled
276
+ function filterToolsByGroups(tools, backendName) {
277
+ const enabledGroups = Object.entries(TOOL_GROUPS)
278
+ .filter(([, g]) => g.enabled && g.source === backendName);
279
+
280
+ if (enabledGroups.length === 0) return [];
281
+
282
+ // If any enabled group has no prefixes defined, include all tools from that backend
283
+ const hasWildcard = enabledGroups.some(([, g]) => !g.prefixes);
284
+ if (hasWildcard) return tools;
285
+
286
+ const enabledPrefixes = enabledGroups.flatMap(([, g]) => g.prefixes || []);
287
+ return tools.filter(t => enabledPrefixes.some(p => t._originalName.startsWith(p)));
288
+ }
289
+
290
+ // Get the final filtered tool list with namespaced names
291
+ function getActiveTools() {
292
+ const filtered = [];
293
+ for (const [backendName, client] of mcpBackends) {
294
+ const accepted = filterToolsByGroups(client.tools, backendName);
295
+ for (const t of accepted) {
296
+ filtered.push({ ...t, name: `${backendName}__${t._originalName}` });
297
+ }
298
+ }
299
+ return filtered;
300
+ }
301
+
302
+ async function initBackends() {
303
+ const needed = BACKEND_DEFS.filter(isBackendNeeded);
304
+ if (needed.length === 0) return;
305
+
306
+ console.log(`Starting ${needed.length} MCP backends: ${needed.map(b => b.name).join(", ")}`);
307
+
308
+ await Promise.allSettled(
309
+ needed.map(async (b) => {
310
+ const client = new StdioMcpClient(b.name, b.command, b.args);
311
+ const ok = await client.start();
312
+ if (ok) {
313
+ mcpBackends.set(b.name, client);
314
+ } else {
315
+ console.warn(`[${b.name}] failed to start`);
316
+ }
317
+ })
318
+ );
319
+
320
+ allBackendTools = getActiveTools();
321
+ console.log(`MCP backends: ${mcpBackends.size} active, ${allBackendTools.length} tools (filtered by groups)`);
322
+ }
323
+
324
+ process.on("SIGTERM", () => { for (const [, c] of mcpBackends) c.stop(); process.exit(0); });
325
+ process.on("SIGINT", () => { for (const [, c] of mcpBackends) c.stop(); process.exit(0); });
326
+
327
+ // =============================================================================
328
+ // BUILT-IN TOOLS (core group — always on)
329
+ // =============================================================================
330
+
331
+ const BUILTIN_TOOLS = [
332
+ {
333
+ name: "search",
334
+ description: "Search your knowledge base for relevant information.",
335
+ inputSchema: {
336
+ type: "object",
337
+ properties: {
338
+ query: { type: "string", description: "Natural language search query" },
339
+ limit: { type: "number", description: "Max results (default 5)", default: 5 },
340
+ },
341
+ required: ["query"],
342
+ },
343
+ },
344
+ {
345
+ name: "web_research",
346
+ description: "Search the web, fact-check claims, compare items, or conduct deep research. Actions: 'search' (quick), 'research' (deep report), 'compare' (side-by-side), 'fact_check' (verify claims), 'goap' (comprehensive multi-step research with verification).",
347
+ inputSchema: {
348
+ type: "object",
349
+ properties: {
350
+ action: { type: "string", enum: ["search", "research", "compare", "fact_check", "goap"], description: "Research action type", default: "search" },
351
+ query: { type: "string", description: "Search query or topic" },
352
+ items: { type: "array", items: { type: "string" }, description: "Items to compare (for 'compare')" },
353
+ claim: { type: "string", description: "Claim to verify (for 'fact_check')" },
354
+ verify: { type: "boolean", description: "Verify results in goap mode", default: true },
355
+ },
356
+ required: ["query"],
357
+ },
358
+ },
359
+ {
360
+ name: "guidance",
361
+ description: "Get instructions on how to use the available tool groups and services. Call this FIRST when unsure which tool to use, when a user asks 'what can you do?', or when you need to understand a specific tool group. Returns structured guidance for the AI on tool selection and usage patterns.",
362
+ inputSchema: {
363
+ type: "object",
364
+ properties: {
365
+ topic: {
366
+ type: "string",
367
+ enum: ["overview", "groups", "intelligence", "agents", "memory", "devtools", "security", "browser", "neural", "agentic-flow", "claude-code", "gemini", "codex", "tool"],
368
+ description: "What to get guidance on. Use 'overview' for capabilities summary, 'groups' to see all tool groups and their status, or a specific group name for detailed usage instructions.",
369
+ default: "overview",
370
+ },
371
+ tool_name: { type: "string", description: "Specific tool name to get detailed usage info (when topic='tool')" },
372
+ },
373
+ },
374
+ },
375
+ ];
376
+
377
+ // =============================================================================
378
+ // GUIDANCE ENGINE — AI-facing instruction system
379
+ // =============================================================================
380
+
381
+ function getGuidance(topic, toolName) {
382
+ const activeGroups = Object.entries(TOOL_GROUPS).filter(([, g]) => g.enabled);
383
+ const inactiveGroups = Object.entries(TOOL_GROUPS).filter(([, g]) => !g.enabled);
384
+ const externalTools = getActiveTools();
385
+
386
+ if (topic === "overview") {
387
+ return {
388
+ guidance: `# Tool Capabilities Overview
389
+
390
+ You have access to ${BUILTIN_TOOLS.length + externalTools.length} tools organized into ${activeGroups.length} active groups.
391
+
392
+ ## Active Groups
393
+ ${activeGroups.map(([name, g]) => {
394
+ const count = name === "core" ? BUILTIN_TOOLS.length : externalTools.filter(t => t.name.startsWith(g.source + "__")).length;
395
+ return `- **${name}** (${count} tools) — ${g.description}`;
396
+ }).join("\n")}
397
+
398
+ ## Inactive Groups (can be enabled)
399
+ ${inactiveGroups.map(([name, g]) => `- **${name}** — ${g.description}`).join("\n") || "None"}
400
+
401
+ ## Quick Decision Guide
402
+ - **Knowledge questions** → use \`search\` first, then \`web_research\` if needed
403
+ - **Current events / facts** → use \`web_research\` with action 'search' or 'goap'
404
+ - **Complex research** → use \`web_research\` with action 'goap' (multi-step pipeline)
405
+ - **"What can you do?"** → call \`guidance\` with topic 'groups'
406
+ - **Memory / recall** → use tools from the \`memory\` group
407
+ - **Agent orchestration** → use tools from the \`agents\` group
408
+ - **Code analysis / performance** → use tools from the \`devtools\` group
409
+
410
+ ## Rules
411
+ 1. Call tools FIRST, then present results conversationally
412
+ 2. Never show raw JSON — synthesize results naturally
413
+ 3. For complex questions, prefer GOAP pipeline (web_research action='goap')
414
+ 4. Call \`guidance\` with a specific group name to learn how to use that group's tools`,
415
+ topic: "overview",
416
+ };
417
+ }
418
+
419
+ if (topic === "groups") {
420
+ const groupList = Object.entries(TOOL_GROUPS).map(([name, g]) => {
421
+ const status = g.enabled ? "ACTIVE" : "INACTIVE";
422
+ const toolCount = name === "core" ? BUILTIN_TOOLS.length :
423
+ externalTools.filter(t => {
424
+ const backend = t._backend;
425
+ return g.source === backend && (!g.prefixes || g.prefixes.some(p => t._originalName.startsWith(p)));
426
+ }).length;
427
+ return `| ${name} | ${status} | ${toolCount} | ${g.description} |`;
428
+ });
429
+
430
+ return {
431
+ guidance: `# Tool Groups\n\n| Group | Status | Tools | Description |\n|-------|--------|-------|-------------|\n${groupList.join("\n")}`,
432
+ topic: "groups",
433
+ };
434
+ }
435
+
436
+ // Specific group guidance
437
+ const groupGuides = {
438
+ intelligence: `# Intelligence Group (ruvector)
439
+
440
+ Self-learning intelligence tools for routing and vector memory.
441
+
442
+ ## Key Tools
443
+ - **ruvector__hooks_route** — Route a task to the best agent type. Call with a task description.
444
+ - **ruvector__hooks_remember** — Store context/knowledge in vector memory for later recall.
445
+ - **ruvector__hooks_recall** — Search vector memory semantically. Good for finding past context.
446
+ - **ruvector__hooks_pretrain** — Bootstrap intelligence from a code repository.
447
+ - **ruvector__hooks_build_agents** — Generate optimized agent configurations.
448
+ - **ruvector__hooks_stats** — Get intelligence statistics and learning metrics.
449
+
450
+ ## When to Use
451
+ - Before starting complex tasks: route to find the best agent approach
452
+ - To store important findings for cross-session memory
453
+ - To recall previously stored patterns or solutions`,
454
+
455
+ agents: `# Agents & Orchestration Group (ruflo)
456
+
457
+ Multi-agent lifecycle management, swarm coordination, and task workflows.
458
+
459
+ ## Key Tools
460
+ - **ruflo__agent_spawn** — Create a new agent with specific capabilities
461
+ - **ruflo__agent_list** — List all active agents
462
+ - **ruflo__swarm_init** — Initialize a swarm with a topology (mesh, hierarchical, ring, star)
463
+ - **ruflo__task_create** — Create and assign tasks
464
+ - **ruflo__workflow_create** — Define multi-step workflows
465
+ - **ruflo__workflow_execute** — Execute a workflow
466
+ - **ruflo__hive-mind_init** — Start collective intelligence coordination
467
+ - **ruflo__coordination_orchestrate** — Multi-agent coordination
468
+
469
+ ## When to Use
470
+ - Complex tasks requiring multiple agents working together
471
+ - Pipeline workflows with sequential or parallel steps
472
+ - Distributed task management`,
473
+
474
+ memory: `# Memory & Knowledge Group (ruflo)
475
+
476
+ Vector storage, semantic search, AgentDB pattern learning, and embeddings.
477
+
478
+ ## Key Tools
479
+ - **ruflo__memory_store** — Store a value with vector embedding for semantic search
480
+ - **ruflo__memory_search** — Semantic search across stored memories (HNSW-indexed)
481
+ - **ruflo__memory_list** — List stored memory entries
482
+ - **ruflo__agentdb_pattern-store** — Store a reasoning pattern for learning
483
+ - **ruflo__agentdb_pattern-search** — Search for similar reasoning patterns
484
+ - **ruflo__agentdb_context-synthesize** — Synthesize context from stored memories
485
+ - **ruflo__embeddings_generate** — Generate vector embeddings for text
486
+ - **ruflo__embeddings_search** — Semantic similarity search
487
+
488
+ ## When to Use
489
+ - Persistent knowledge storage across sessions
490
+ - Finding similar past solutions or patterns
491
+ - Building semantic search over custom data`,
492
+
493
+ devtools: `# Dev Tools Group (ruflo)
494
+
495
+ Code analysis, performance profiling, GitHub integration, and terminal access.
496
+
497
+ ## Key Tools
498
+ - **ruflo__analyze_diff** — Analyze git diff for risk and change classification
499
+ - **ruflo__performance_benchmark** — Run performance benchmarks
500
+ - **ruflo__performance_bottleneck** — Detect performance bottlenecks
501
+ - **ruflo__github_repo_analyze** — Analyze a GitHub repository
502
+ - **ruflo__github_pr_manage** — Manage pull requests
503
+ - **ruflo__terminal_execute** — Execute commands in a terminal session
504
+
505
+ ## When to Use
506
+ - Code review and change risk assessment
507
+ - Performance analysis and optimization
508
+ - GitHub repository management`,
509
+
510
+ security: `# Security & Safety Group (ruflo)
511
+
512
+ AI defence, PII detection, and claims-based authorization.
513
+
514
+ ## Key Tools
515
+ - **ruflo__aidefence_scan** — Scan text for AI manipulation attempts
516
+ - **ruflo__aidefence_has_pii** — Check for PII (emails, phones, SSNs)
517
+ - **ruflo__aidefence_is_safe** — Quick safety check on input
518
+ - **ruflo__claims_claim** — Claim an issue for work
519
+ - **ruflo__claims_board** — Visual board of all claims
520
+
521
+ ## When to Use
522
+ - Input validation and safety checking
523
+ - PII detection before processing sensitive data
524
+ - Work item management across agents`,
525
+
526
+ browser: `# Browser Automation Group (ruflo)
527
+
528
+ Headless browser control for web interaction and testing.
529
+
530
+ ## Key Tools
531
+ - **ruflo__browser_open** — Navigate to a URL
532
+ - **ruflo__browser_click** — Click elements by reference
533
+ - **ruflo__browser_fill** — Fill form inputs
534
+ - **ruflo__browser_screenshot** — Capture page screenshots
535
+ - **ruflo__browser_snapshot** — Get accessibility tree for AI parsing
536
+ - **ruflo__browser_eval** — Execute JavaScript in page context
537
+
538
+ ## When to Use
539
+ - Web scraping and data extraction
540
+ - Automated testing (E2E)
541
+ - Form filling and web interaction`,
542
+
543
+ neural: `# Neural & DAA Group (ruflo)
544
+
545
+ Neural network operations and Decentralized Autonomous Agents.
546
+
547
+ ## Key Tools
548
+ - **ruflo__neural_train** — Train a neural model
549
+ - **ruflo__neural_predict** — Make predictions
550
+ - **ruflo__daa_agent_create** — Create an autonomous agent
551
+ - **ruflo__daa_workflow_create** — Create autonomous workflows
552
+ - **ruflo__daa_knowledge_share** — Share knowledge between agents
553
+
554
+ ## When to Use
555
+ - Pattern learning and prediction
556
+ - Autonomous agent workflows
557
+ - Knowledge transfer between agents`,
558
+
559
+ "agentic-flow": `# Agentic Flow Group (agentic-flow@alpha)
560
+
561
+ Execute 66+ specialized agents with boosted code editing and AgentDB.
562
+
563
+ ## Key Tools
564
+ - **agentic-flow__agentic_flow_agent** — Execute any of 66+ specialized agents
565
+ - **agentic-flow__agentic_flow_list_agents** — List available agent types
566
+ - **agentic-flow__agent_booster_edit_file** — 352x faster code editing
567
+ - **agentic-flow__agent_booster_batch_edit** — Multi-file refactoring
568
+ - **agentic-flow__agentdb_pattern_store** — Store reasoning patterns
569
+ - **agentic-flow__agentdb_pattern_search** — Search similar patterns
570
+
571
+ ## When to Use
572
+ - Complex code generation with specialized agents
573
+ - Batch code refactoring across files
574
+ - Agent selection when you need the right specialist`,
575
+
576
+ "claude-code": `# Claude Code Group
577
+
578
+ Anthropic Claude Code MCP server — full coding agent capabilities.
579
+
580
+ Requires: ANTHROPIC_API_KEY environment variable.
581
+
582
+ ## Capabilities
583
+ - File reading and editing
584
+ - Bash command execution
585
+ - Code analysis and generation
586
+ - Project exploration
587
+
588
+ ## When to Use
589
+ - When you need a second AI perspective on code
590
+ - Complex refactoring tasks
591
+ - Code review and analysis`,
592
+
593
+ gemini: `# Gemini MCP Group
594
+
595
+ Google Gemini with conversation context management.
596
+
597
+ Requires: GOOGLE_API_KEY environment variable (already set for Gemini models).
598
+
599
+ ## Capabilities
600
+ - Conversation context management
601
+ - Multimodal processing
602
+ - Google Search grounding
603
+
604
+ ## When to Use
605
+ - Extended context conversations
606
+ - Multimodal content processing`,
607
+
608
+ codex: `# Codex Group
609
+
610
+ OpenAI Codex coding agent.
611
+
612
+ Requires: OPENAI_API_KEY environment variable (already set for OpenAI models).
613
+
614
+ ## Capabilities
615
+ - Code generation and execution
616
+ - Code completion
617
+ - Code explanation
618
+
619
+ ## When to Use
620
+ - Code generation tasks
621
+ - Quick code completions
622
+ - Code explanation and documentation`,
623
+ };
624
+
625
+ if (topic === "tool" && toolName) {
626
+ const allTools = [...BUILTIN_TOOLS, ...externalTools];
627
+ const tool = allTools.find(t => t.name === toolName);
628
+ if (tool) {
629
+ const props = Object.entries(tool.inputSchema?.properties || {})
630
+ .map(([k, v]) => `- **${k}** (${v.type}) — ${v.description || ""}`)
631
+ .join("\n");
632
+ return { guidance: `# ${tool.name}\n\n${tool.description}\n\n## Parameters\n${props}`, topic: "tool" };
633
+ }
634
+ return { guidance: `Tool '${toolName}' not found. Call guidance with topic='groups' to see available tools.`, topic: "tool" };
635
+ }
636
+
637
+ if (groupGuides[topic]) {
638
+ const group = TOOL_GROUPS[topic];
639
+ if (!group?.enabled) {
640
+ return { guidance: `# ${topic} — INACTIVE\n\n${group?.description || ""}\n\nThis group is not enabled. Set the appropriate MCP_GROUP_* env var to "true" to activate it.`, topic };
641
+ }
642
+ return { guidance: groupGuides[topic], topic };
643
+ }
644
+
645
+ return { guidance: `Unknown topic '${topic}'. Use 'overview', 'groups', or a specific group name.`, topic };
646
+ }
647
+
648
+ // =============================================================================
649
+ // GEMINI GROUNDED SEARCH — Uses Gemini's built-in Google Search when no
650
+ // dedicated search Cloud Function is configured
651
+ // =============================================================================
652
+
653
+ async function geminiGroundedSearch(query, mode = "search") {
654
+ const apiKey = process.env.GOOGLE_API_KEY;
655
+ if (!apiKey) return { error: "No GOOGLE_API_KEY configured for search" };
656
+
657
+ // Empty/missing query produces a 400 from Gemini's generateContent endpoint.
658
+ // Return a structured error so the model can recover with a real query.
659
+ if (!query || typeof query !== "string" || !query.trim()) {
660
+ return {
661
+ error: "search requires a non-empty query string",
662
+ hint: "Call this tool again with { query: 'your search terms' }. For comparisons use { action: 'compare', query: 'item A vs item B' }; for fact-checking use { action: 'fact_check', claim: 'the claim text' }.",
663
+ };
664
+ }
665
+
666
+ const model = "gemini-2.5-flash";
667
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
668
+
669
+ const systemInstructions = {
670
+ search: `You are a helpful search assistant. Answer the query using Google Search results. Include key facts, sources, and relevant details. Be concise but thorough.`,
671
+ research: `You are a research analyst. Provide a comprehensive research report on the topic using Google Search results. Include: key findings, analysis, multiple perspectives, and source citations. Be thorough.`,
672
+ compare: `You are a comparison analyst. Compare the items using Google Search results. Create a structured comparison with pros/cons, key differences, and a recommendation.`,
673
+ fact_check: `You are a fact-checker. Verify the claim using Google Search results. Provide a verdict (TRUE/FALSE/PARTIALLY TRUE/UNVERIFIABLE), evidence, and sources.`,
674
+ };
675
+
676
+ const prompt = mode === "fact_check"
677
+ ? `Fact-check this claim: "${query}"`
678
+ : mode === "compare"
679
+ ? `Compare these items: ${query}`
680
+ : mode === "research"
681
+ ? `Research this topic thoroughly: ${query}`
682
+ : query;
683
+
684
+ try {
685
+ const resp = await fetch(url, {
686
+ method: "POST",
687
+ headers: { "Content-Type": "application/json" },
688
+ body: JSON.stringify({
689
+ system_instruction: { parts: [{ text: systemInstructions[mode] || systemInstructions.search }] },
690
+ contents: [{ parts: [{ text: prompt }] }],
691
+ tools: [{ google_search: {} }],
692
+ generationConfig: { temperature: 0.2 },
693
+ }),
694
+ signal: AbortSignal.timeout(30000),
695
+ });
696
+
697
+ if (!resp.ok) {
698
+ const errText = await resp.text();
699
+ console.error(`[gemini-search] API error ${resp.status}:`, errText.substring(0, 200));
700
+ return { error: `Search API error: ${resp.status}` };
701
+ }
702
+
703
+ const data = await resp.json();
704
+ const candidate = data.candidates?.[0];
705
+ const answer = candidate?.content?.parts?.map(p => p.text).filter(Boolean).join("\n") || "";
706
+
707
+ // Extract grounding metadata (sources)
708
+ const grounding = candidate?.groundingMetadata || {};
709
+ const chunks = grounding.groundingChunks || [];
710
+ const sources = chunks
711
+ .filter(c => c.web)
712
+ .map(c => ({ title: c.web.title || "", url: c.web.uri || "" }))
713
+ .filter(s => s.url);
714
+
715
+ // Extract search queries used
716
+ const searchQueries = (grounding.webSearchQueries || []);
717
+
718
+ return {
719
+ success: true,
720
+ answer,
721
+ sources: sources.slice(0, 8),
722
+ searchQueries,
723
+ groundingMetadata: { sources, searchQueries },
724
+ mode,
725
+ };
726
+ } catch (err) {
727
+ if (err.name === "AbortError" || err.name === "TimeoutError") return { error: "Search timed out" };
728
+ return { error: err.message };
729
+ }
730
+ }
731
+
732
+ // =============================================================================
733
+ // SSRF GUARD — Reject requests to private/loopback ranges (CWE-918)
734
+ // =============================================================================
735
+
736
+ const PRIVATE_IP_RE = /^(?:10\.|172\.(?:1[6-9]|2\d|3[01])\.|192\.168\.|127\.|0\.|::1|fc|fd)/i;
737
+
738
+ function assertSafeUrl(rawUrl) {
739
+ let parsed;
740
+ try {
741
+ parsed = new URL(rawUrl);
742
+ } catch {
743
+ throw new Error(`SSRF guard: invalid URL — ${rawUrl}`);
744
+ }
745
+ if (parsed.protocol !== "https:") {
746
+ throw new Error(`SSRF guard: only HTTPS URLs are permitted, got ${parsed.protocol}`);
747
+ }
748
+ const host = parsed.hostname;
749
+ if (PRIVATE_IP_RE.test(host) || host === "localhost" || host.endsWith(".local")) {
750
+ throw new Error(`SSRF guard: private/loopback host rejected — ${host}`);
751
+ }
752
+ }
753
+
754
+ // =============================================================================
755
+ // HELPER — Call a backend Cloud Function / API
756
+ // =============================================================================
757
+
758
+ async function callCloudFunction(url, payload, timeoutMs = 25000) {
759
+ // Validate the URL before making any network request.
760
+ assertSafeUrl(url);
761
+ const controller = new AbortController();
762
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
763
+ try {
764
+ const resp = await fetch(url, {
765
+ method: "POST",
766
+ headers: { "Content-Type": "application/json" },
767
+ body: JSON.stringify(payload),
768
+ signal: controller.signal,
769
+ });
770
+ return await resp.json();
771
+ } catch (err) {
772
+ if (err.name === "AbortError") return { error: "Request timed out", timeout: timeoutMs };
773
+ return { error: err.message };
774
+ } finally {
775
+ clearTimeout(timer);
776
+ }
777
+ }
778
+
779
+ // =============================================================================
780
+ // GOAP SEARCH PIPELINE
781
+ // =============================================================================
782
+
783
+ async function executeGoapSearch(query, args) {
784
+ const researchUrl = CLOUD_FUNCTIONS.research;
785
+ if (!researchUrl) return { error: "GOAP requires a 'research' URL in CLOUD_FUNCTIONS" };
786
+
787
+ const startTime = Date.now();
788
+
789
+ const composeResult = await callCloudFunction(researchUrl, {
790
+ action: "search",
791
+ query: `Break this question into 3-4 distinct search queries that would help answer it comprehensively. Return ONLY the queries, one per line:\n\n${query}`,
792
+ }, 30000);
793
+
794
+ let searchQueries = [query];
795
+ if (composeResult && !composeResult.error) {
796
+ const answer = composeResult.result?.answer || composeResult.answer || "";
797
+ const lines = answer.split("\n").map(l => l.replace(/^[\d\-\*\.\)]+\s*/, "").trim()).filter(l => l.length > 5 && l.length < 200);
798
+ if (lines.length >= 2) searchQueries = lines.slice(0, 4);
799
+ }
800
+
801
+ const searchResults = await Promise.all(
802
+ searchQueries.map(q => callCloudFunction(researchUrl, { action: "search", query: q }, 30000))
803
+ );
804
+
805
+ const allSources = [], allAnswers = [];
806
+ for (let i = 0; i < searchResults.length; i++) {
807
+ const r = searchResults[i];
808
+ if (r && !r.error && r.success !== false) {
809
+ const answer = r.result?.answer || r.answer || "";
810
+ if (answer) allAnswers.push({ query: searchQueries[i], answer });
811
+ const gm = r.result?.groundingMetadata || r.groundingMetadata || {};
812
+ if (gm.sources) allSources.push(...gm.sources);
813
+ }
814
+ }
815
+
816
+ const uniqueSources = [];
817
+ const seenUrls = new Set();
818
+ for (const src of allSources) {
819
+ const url = src.url || src.uri || "";
820
+ if (url && !seenUrls.has(url)) { seenUrls.add(url); uniqueSources.push(src); }
821
+ }
822
+
823
+ const synthesisInput = allAnswers.map(a => `## ${a.query}\n${a.answer}`).join("\n\n");
824
+ const synthesisResult = await callCloudFunction(researchUrl, {
825
+ action: "research",
826
+ topic: `Synthesize these findings into a comprehensive answer to: "${query}"\n\nFindings:\n${synthesisInput}`,
827
+ }, 60000);
828
+
829
+ const synthesizedAnswer = synthesisResult?.result?.answer || synthesisResult?.answer || synthesisInput;
830
+ const synthGm = synthesisResult?.result?.groundingMetadata || {};
831
+ if (synthGm.sources) {
832
+ for (const src of synthGm.sources) {
833
+ const url = src.url || src.uri || "";
834
+ if (url && !seenUrls.has(url)) { seenUrls.add(url); uniqueSources.push(src); }
835
+ }
836
+ }
837
+
838
+ let verification = { verified: true, confidence: "high" };
839
+ if (args.verify !== false && synthesizedAnswer.length > 100) {
840
+ const vr = await callCloudFunction(researchUrl, {
841
+ action: "fact_check", claim: synthesizedAnswer.substring(0, 500),
842
+ }, 30000);
843
+ if (vr && !vr.error && vr.result) {
844
+ verification = {
845
+ verified: vr.result.verdict !== "FALSE",
846
+ verdict: vr.result.verdict,
847
+ confidence: vr.result.confidence || "medium",
848
+ details: vr.result.analysis,
849
+ };
850
+ }
851
+ }
852
+
853
+ return {
854
+ answer: synthesizedAnswer, pipeline: "goap",
855
+ steps: { queries_composed: searchQueries.length, searches_executed: searchResults.filter(r => !r?.error).length, sources_found: uniqueSources.length, verification },
856
+ sources: uniqueSources.slice(0, 10), searchQueries, duration_ms: Date.now() - startTime,
857
+ };
858
+ }
859
+
860
+ // =============================================================================
861
+ // GOAP SEARCH PIPELINE — Gemini fallback (no Cloud Function needed)
862
+ // =============================================================================
863
+
864
+ async function executeGoapSearchGemini(query, args) {
865
+ const startTime = Date.now();
866
+
867
+ // Step 1: Decompose into sub-queries
868
+ const decompose = await geminiGroundedSearch(
869
+ `Break this question into 3-4 distinct search queries that would help answer it comprehensively. Return ONLY the queries, one per line:\n\n${query}`,
870
+ "search"
871
+ );
872
+
873
+ let searchQueries = [query];
874
+ if (decompose?.answer) {
875
+ const lines = decompose.answer.split("\n")
876
+ .map(l => l.replace(/^[\d\-\*\.\)]+\s*/, "").trim())
877
+ .filter(l => l.length > 5 && l.length < 200);
878
+ if (lines.length >= 2) searchQueries = lines.slice(0, 4);
879
+ }
880
+
881
+ // Step 2: Parallel searches
882
+ const searchResults = await Promise.all(
883
+ searchQueries.map(q => geminiGroundedSearch(q, "search"))
884
+ );
885
+
886
+ const allSources = [], allAnswers = [];
887
+ for (let i = 0; i < searchResults.length; i++) {
888
+ const r = searchResults[i];
889
+ if (r && !r.error && r.answer) {
890
+ allAnswers.push({ query: searchQueries[i], answer: r.answer });
891
+ if (r.sources) allSources.push(...r.sources);
892
+ }
893
+ }
894
+
895
+ // Dedupe sources
896
+ const seenUrls = new Set();
897
+ const uniqueSources = allSources.filter(s => {
898
+ if (seenUrls.has(s.url)) return false;
899
+ seenUrls.add(s.url);
900
+ return true;
901
+ });
902
+
903
+ // Step 3: Synthesize
904
+ const synthesisInput = allAnswers.map(a => `## ${a.query}\n${a.answer}`).join("\n\n");
905
+ const synthesis = await geminiGroundedSearch(
906
+ `Synthesize these findings into a comprehensive answer to: "${query}"\n\nFindings:\n${synthesisInput}`,
907
+ "research"
908
+ );
909
+
910
+ const finalAnswer = synthesis?.answer || synthesisInput;
911
+ if (synthesis?.sources) {
912
+ for (const s of synthesis.sources) {
913
+ if (!seenUrls.has(s.url)) { seenUrls.add(s.url); uniqueSources.push(s); }
914
+ }
915
+ }
916
+
917
+ // Step 4: Verify if requested
918
+ let verification = { verified: true, confidence: "high" };
919
+ if (args.verify !== false && finalAnswer.length > 100) {
920
+ const vr = await geminiGroundedSearch(finalAnswer.substring(0, 500), "fact_check");
921
+ if (vr && !vr.error) {
922
+ verification = { verified: true, confidence: "medium", details: vr.answer };
923
+ }
924
+ }
925
+
926
+ return {
927
+ success: true,
928
+ answer: finalAnswer,
929
+ pipeline: "goap-gemini",
930
+ steps: {
931
+ queries_composed: searchQueries.length,
932
+ searches_executed: searchResults.filter(r => !r?.error).length,
933
+ sources_found: uniqueSources.length,
934
+ verification,
935
+ },
936
+ sources: uniqueSources.slice(0, 10),
937
+ searchQueries,
938
+ duration_ms: Date.now() - startTime,
939
+ };
940
+ }
941
+
942
+ // =============================================================================
943
+ // TOOL EXECUTOR
944
+ // =============================================================================
945
+
946
+ // ADR-166 §6 Phase 1d + 2a — server-side tool gate.
947
+ // Enforced HERE (not just in the autopilot handler) so /mcp, /mcp/:group,
948
+ // autopilot, and any future path share ONE denial gate. Was the missing
949
+ // link that made the disclosed unauthenticated-RCE chain reach shell.
950
+ const DANGEROUS_TOOLS = Object.freeze(new Set([
951
+ "terminal_execute",
952
+ "ruflo__terminal_execute",
953
+ "devtools__terminal_execute",
954
+ ]));
955
+ function isTerminalTool(name) {
956
+ return DANGEROUS_TOOLS.has(name) || /terminal_execute/i.test(name);
957
+ }
958
+ const MCP_ENABLE_TERMINAL = process.env.MCP_ENABLE_TERMINAL === "true";
959
+
960
+ async function executeTool(name, args) {
961
+ // Deny dangerous tools unless the operator explicitly opted in.
962
+ // Enforced on every path (not just autopilot) — root cause of ADR-166 V2/V3.
963
+ if (isTerminalTool(name) && !MCP_ENABLE_TERMINAL) {
964
+ return {
965
+ error:
966
+ `Tool "${name}" is disabled by default. Set MCP_ENABLE_TERMINAL=true to allow.`,
967
+ code: "TOOL_DISABLED",
968
+ };
969
+ }
970
+ switch (name) {
971
+ case "search": {
972
+ if (CLOUD_FUNCTIONS.search) {
973
+ return callCloudFunction(CLOUD_FUNCTIONS.search, { query: args.query, limit: args.limit || 5 });
974
+ }
975
+ // Fallback: use Gemini grounded search
976
+ return geminiGroundedSearch(args.query, "search");
977
+ }
978
+
979
+ case "web_research": {
980
+ const action = args.action || "search";
981
+ if (action === "goap") {
982
+ // GOAP needs research endpoint — fall back to multi-search via Gemini
983
+ if (CLOUD_FUNCTIONS.research) return executeGoapSearch(args.query, args);
984
+ return executeGoapSearchGemini(args.query, args);
985
+ }
986
+ if (CLOUD_FUNCTIONS.research) {
987
+ const payload = { action };
988
+ if (action === "search") payload.query = args.query;
989
+ else if (action === "research") payload.topic = args.query;
990
+ else if (action === "compare") payload.items = args.items;
991
+ else if (action === "fact_check") payload.claim = args.claim || args.query;
992
+ return callCloudFunction(CLOUD_FUNCTIONS.research, payload, 60000);
993
+ }
994
+ // Fallback: Gemini grounded search
995
+ const mode = action === "fact_check" ? "fact_check" : action === "compare" ? "compare" : action === "research" ? "research" : "search";
996
+ const query = action === "compare" ? (args.items || []).join(" vs ") : (args.claim || args.query);
997
+ return geminiGroundedSearch(query, mode);
998
+ }
999
+
1000
+ case "guidance":
1001
+ return getGuidance(args.topic || "overview", args.tool_name);
1002
+
1003
+ default: {
1004
+ // Route to external MCP backend
1005
+ const activeTools = getActiveTools();
1006
+ const extTool = activeTools.find(t => t.name === name);
1007
+ if (extTool) {
1008
+ const backend = mcpBackends.get(extTool._backend);
1009
+ if (backend) return backend.callTool(extTool._originalName, args);
1010
+ return { error: `Backend ${extTool._backend} not available` };
1011
+ }
1012
+ return { error: `Unknown tool: ${name}. Call 'guidance' with topic='groups' to see available tools.` };
1013
+ }
1014
+ }
1015
+ }
1016
+
1017
+ // =============================================================================
1018
+ // PER-GROUP TOOL HELPERS
1019
+ // =============================================================================
1020
+
1021
+ // Get tools for a specific group only
1022
+ function getToolsForGroup(groupName) {
1023
+ const group = TOOL_GROUPS[groupName];
1024
+ if (!group || !group.enabled) return [];
1025
+ if (groupName === "core") return BUILTIN_TOOLS;
1026
+
1027
+ const allActive = getActiveTools();
1028
+ if (!group.prefixes) {
1029
+ // No prefix filter — return all tools from this backend
1030
+ return allActive.filter(t => t._backend === group.source);
1031
+ }
1032
+ return allActive.filter(t =>
1033
+ t._backend === group.source && group.prefixes.some(p => t._originalName.startsWith(p))
1034
+ );
1035
+ }
1036
+
1037
+ // Group display names for the Chat UI
1038
+ const GROUP_DISPLAY_NAMES = {
1039
+ core: "Core Tools",
1040
+ intelligence: "Intelligence & Learning",
1041
+ agents: "Agents & Orchestration",
1042
+ memory: "Memory & Knowledge",
1043
+ devtools: "Dev Tools & Analysis",
1044
+ security: "Security & Safety",
1045
+ browser: "Browser Automation",
1046
+ neural: "Neural & DAA",
1047
+ "agentic-flow": "Agentic Flow",
1048
+ "claude-code": "Claude Code",
1049
+ gemini: "Gemini",
1050
+ codex: "Codex",
1051
+ };
1052
+
1053
+ // =============================================================================
1054
+ // MCP SERVER — Multiple endpoints per group
1055
+ // =============================================================================
1056
+
1057
+ const app = express();
1058
+ app.use(express.json({ limit: "10mb" }));
1059
+
1060
+ // ---------- MCP Streamable HTTP session (#2425 djimit) ----------
1061
+ // Streamable-HTTP clients (Codex/RMCP) send `DELETE /mcp` with an
1062
+ // `Mcp-Session-Id` header at shutdown. We echo a stable session id back
1063
+ // on every /mcp* response so those clients can attach it to the DELETE
1064
+ // and to `notifications/initialized` handshakes.
1065
+ const MCP_SESSION_ID = randomUUID();
1066
+ app.use((req, res, next) => {
1067
+ if (req.path.startsWith("/mcp")) {
1068
+ res.setHeader("Mcp-Session-Id", MCP_SESSION_ID);
1069
+ }
1070
+ next();
1071
+ });
1072
+
1073
+ // ---------- CORS middleware (ADR-166 §6 Phase 3b) ----------
1074
+ // MCP_CORS_ORIGIN: comma-separated allowlist (e.g. "https://a.example,https://b.example").
1075
+ // unset → "*" for back-compat (loopback default is same-origin anyway)
1076
+ // "*" → wildcard (explicit opt-in — same as legacy behavior)
1077
+ // other → echo the request origin ONLY if it appears in the allowlist
1078
+ const CORS_ALLOWLIST = (process.env.MCP_CORS_ORIGIN || "*")
1079
+ .split(",").map(s => s.trim()).filter(Boolean);
1080
+ const CORS_WILDCARD = CORS_ALLOWLIST.length === 1 && CORS_ALLOWLIST[0] === "*";
1081
+ app.use((req, res, next) => {
1082
+ const origin = req.get("origin") || "";
1083
+ if (CORS_WILDCARD) {
1084
+ res.setHeader("Access-Control-Allow-Origin", "*");
1085
+ } else if (origin && CORS_ALLOWLIST.includes(origin)) {
1086
+ res.setHeader("Access-Control-Allow-Origin", origin);
1087
+ res.setHeader("Vary", "Origin");
1088
+ }
1089
+ // If no match, do NOT set the header — browser will block the request.
1090
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
1091
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id");
1092
+ if (req.method === "OPTIONS") return res.sendStatus(204);
1093
+ next();
1094
+ });
1095
+
1096
+ // ---------- Auth middleware ----------
1097
+ // No-op in local-only mode (MCP_AUTH_TOKEN unset). Enforces 401 when token is set.
1098
+ const MCP_TOKEN = process.env.MCP_AUTH_TOKEN || "";
1099
+ function requireAuth(req, res, next) {
1100
+ if (req.path === "/health") return next();
1101
+ if (!MCP_TOKEN) return next();
1102
+ const expected = `Bearer ${MCP_TOKEN}`;
1103
+ const got = req.get("authorization") || "";
1104
+ const ok = got.length === expected.length &&
1105
+ timingSafeEqual(Buffer.from(got), Buffer.from(expected));
1106
+ if (!ok) return res.status(401).json({ error: "unauthorized" });
1107
+ next();
1108
+ }
1109
+ app.use(requireAuth);
1110
+
1111
+ // ---------- Shared MCP handler ----------
1112
+ function createMcpHandler(groupName) {
1113
+ return async (req, res) => {
1114
+ const { method, id, params } = req.body;
1115
+ try {
1116
+ switch (method) {
1117
+ case "initialize":
1118
+ return res.json({
1119
+ jsonrpc: "2.0", id,
1120
+ result: {
1121
+ protocolVersion: "2024-11-05",
1122
+ capabilities: { tools: {} },
1123
+ serverInfo: { name: `mcp-bridge/${groupName}`, version: "2.0.0" },
1124
+ },
1125
+ });
1126
+ case "tools/list": {
1127
+ const tools = getToolsForGroup(groupName);
1128
+ return res.json({ jsonrpc: "2.0", id, result: { tools } });
1129
+ }
1130
+ case "tools/call": {
1131
+ const { name, arguments: toolArgs } = params;
1132
+ const result = await executeTool(name, toolArgs || {});
1133
+ return res.json({
1134
+ jsonrpc: "2.0", id,
1135
+ result: {
1136
+ content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }],
1137
+ },
1138
+ });
1139
+ }
1140
+ case "notifications/initialized":
1141
+ // MCP streamable-HTTP spec: notifications must return 202 Accepted
1142
+ // with an empty body (no jsonrpc envelope).
1143
+ return res.status(202).end();
1144
+ default:
1145
+ return res.json({ jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } });
1146
+ }
1147
+ } catch (err) {
1148
+ console.error(`MCP error [${groupName}/${method}]:`, err);
1149
+ return res.json({ jsonrpc: "2.0", id, error: { code: -32603, message: err.message } });
1150
+ }
1151
+ };
1152
+ }
1153
+
1154
+ function createMcpSseHandler(groupName) {
1155
+ return (req, res) => {
1156
+ res.setHeader("Content-Type", "text/event-stream");
1157
+ res.setHeader("Cache-Control", "no-cache");
1158
+ res.setHeader("Connection", "keep-alive");
1159
+ res.write(`data: ${JSON.stringify({ type: "endpoint", url: `/mcp/${groupName}` })}\n\n`);
1160
+ };
1161
+ }
1162
+
1163
+ // ---------- Register per-group endpoints ----------
1164
+ for (const groupName of Object.keys(TOOL_GROUPS)) {
1165
+ app.post(`/mcp/${groupName}`, createMcpHandler(groupName));
1166
+ app.get(`/mcp/${groupName}`, createMcpSseHandler(groupName));
1167
+ // #2425 djimit — streamable-HTTP session cleanup
1168
+ app.delete(`/mcp/${groupName}`, (_, res) => res.sendStatus(204));
1169
+ }
1170
+
1171
+ // ---------- Catch-all /mcp — serves ALL enabled tools (backwards-compatible) ----------
1172
+ app.post("/mcp", async (req, res) => {
1173
+ const { method, id, params } = req.body;
1174
+ try {
1175
+ switch (method) {
1176
+ case "initialize":
1177
+ return res.json({
1178
+ jsonrpc: "2.0", id,
1179
+ result: {
1180
+ protocolVersion: "2024-11-05",
1181
+ capabilities: { tools: {} },
1182
+ serverInfo: { name: "mcp-bridge", version: "2.0.0" },
1183
+ },
1184
+ });
1185
+ case "tools/list": {
1186
+ const activeTools = getActiveTools();
1187
+ return res.json({ jsonrpc: "2.0", id, result: { tools: [...BUILTIN_TOOLS, ...activeTools] } });
1188
+ }
1189
+ case "tools/call": {
1190
+ const { name, arguments: toolArgs } = params;
1191
+ const result = await executeTool(name, toolArgs || {});
1192
+ return res.json({
1193
+ jsonrpc: "2.0", id,
1194
+ result: {
1195
+ content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }],
1196
+ },
1197
+ });
1198
+ }
1199
+ case "notifications/initialized":
1200
+ return res.status(202).end();
1201
+ default:
1202
+ return res.json({ jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } });
1203
+ }
1204
+ } catch (err) {
1205
+ console.error(`MCP error [${method}]:`, err);
1206
+ return res.json({ jsonrpc: "2.0", id, error: { code: -32603, message: err.message } });
1207
+ }
1208
+ });
1209
+
1210
+ app.get("/mcp", (req, res) => {
1211
+ res.setHeader("Content-Type", "text/event-stream");
1212
+ res.setHeader("Cache-Control", "no-cache");
1213
+ res.setHeader("Connection", "keep-alive");
1214
+ res.write(`data: ${JSON.stringify({ type: "endpoint", url: "/mcp" })}\n\n`);
1215
+ });
1216
+
1217
+ // #2425 djimit — streamable-HTTP session cleanup on the catch-all route.
1218
+ app.delete("/mcp", (_, res) => res.sendStatus(204));
1219
+
1220
+ // ---------- GET /mcp-servers — returns MCP_SERVERS JSON for Chat UI config ----------
1221
+ app.get("/mcp-servers", (_, res) => {
1222
+ const servers = [];
1223
+ for (const [name, group] of Object.entries(TOOL_GROUPS)) {
1224
+ if (!group.enabled) continue;
1225
+ const tools = getToolsForGroup(name);
1226
+ if (tools.length === 0) continue;
1227
+ servers.push({
1228
+ name: GROUP_DISPLAY_NAMES[name] || name,
1229
+ url: `/mcp/${name}`,
1230
+ tools: tools.length,
1231
+ group: name,
1232
+ });
1233
+ }
1234
+ res.json(servers);
1235
+ });
1236
+
1237
+ // =============================================================================
1238
+ // CHAT COMPLETIONS PROXY
1239
+ // =============================================================================
1240
+
1241
+ const PROVIDER_ROUTES = {
1242
+ openai: { baseURL: "https://api.openai.com/v1/chat/completions", getKey: () => process.env.OPENAI_API_KEY },
1243
+ gemini: { baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", getKey: () => process.env.GOOGLE_API_KEY },
1244
+ openrouter: { baseURL: "https://openrouter.ai/api/v1/chat/completions", getKey: () => process.env.OPENROUTER_API_KEY },
1245
+ };
1246
+
1247
+ function resolveProvider(model) {
1248
+ if (typeof model === "string") {
1249
+ if (model.startsWith("gemini-")) return "gemini";
1250
+ if (model.includes("/")) return "openrouter";
1251
+ }
1252
+ return "openai";
1253
+ }
1254
+
1255
+ // =============================================================================
1256
+ // SYSTEM PROMPT — Injected server-side into every chat completion request
1257
+ // =============================================================================
1258
+ // This comprehensive prompt teaches the AI how to use all 200+ MCP tools
1259
+ // across 5 groups. It is injected as the first system message, ensuring
1260
+ // consistent behavior regardless of what preprompt the Chat UI sends.
1261
+
1262
+ function buildSystemPrompt() {
1263
+ // Build dynamic group status
1264
+ const enabledGroups = Object.entries(TOOL_GROUPS)
1265
+ .filter(([, g]) => g.enabled)
1266
+ .map(([name]) => name);
1267
+
1268
+ return `You are an intelligent AI assistant with powerful tools organized into ${enabledGroups.length} active groups.
1269
+
1270
+ # CRITICAL RULES
1271
+ 1. Use tools proactively — NEVER guess answers, ALWAYS search first
1272
+ 2. For ANY factual question, current event, or research request → call \`web_research\` IMMEDIATELY
1273
+ 3. NEVER say "I don't have access to real-time information" — you DO via web_research
1274
+ 4. NEVER say "I can't search the web" — you CAN via web_research
1275
+ 5. Call tools FIRST, present results conversationally AFTER
1276
+ 6. When multiple tools could help, call them ALL in parallel
1277
+
1278
+ # Tool Groups
1279
+
1280
+ Tools prefixed with backend name (e.g., \`ruflo__agent_spawn\`). Always use the full prefixed name.
1281
+
1282
+ ## Group 1: Core Tools (always on)
1283
+
1284
+ - **search** — Search your knowledge base (documents, workflows, how-tos).
1285
+ Use for: internal knowledge, company docs, past conversations.
1286
+ \`{"query": "how to process a claim"}\`
1287
+
1288
+ - **web_research** — Search the internet via Google. This is your primary research tool.
1289
+ ALWAYS use this for: current events, facts, comparisons, any external knowledge.
1290
+ Actions:
1291
+ - \`search\` — Quick web search. Default, fast. \`{"action": "search", "query": "latest news on X"}\`
1292
+ - \`research\` — Deep report with synthesis. \`{"action": "research", "query": "comprehensive analysis of X"}\`
1293
+ - \`compare\` — Side-by-side comparison. \`{"action": "compare", "query": "X", "items": ["A", "B"]}\`
1294
+ - \`fact_check\` — Verify a claim. \`{"action": "fact_check", "query": "claim to verify"}\`
1295
+ - \`goap\` — Multi-step pipeline: decomposes → parallel searches → synthesizes → verifies. BEST for important questions.
1296
+ \`{"action": "goap", "query": "complex question requiring thorough research"}\`
1297
+
1298
+ - **guidance** — Get help on tool groups or specific tool usage.
1299
+ Topics: \`overview\`, \`groups\`, \`agents\`, \`memory\`, \`intelligence\`, \`devtools\`
1300
+ \`{"topic": "overview"}\` or \`{"topic": "tool", "tool_name": "ruflo__agent_spawn"}\`
1301
+
1302
+ ## Group 2: Intelligence & Learning (ruvector)
1303
+ Pattern learning, routing, code analysis, and trajectory tracking. ${TOOL_GROUPS.intelligence.enabled ? "ACTIVE" : "DISABLED"}
1304
+
1305
+ ### Essential Intelligence Tools:
1306
+ - **ruvector__hooks_route** — Route a task to the optimal agent type. Call this FIRST for complex tasks.
1307
+ \`{"task": "describe what needs to be done", "context": ["relevant info"]}\`
1308
+ Returns ranked agent recommendations with confidence scores.
1309
+ - **ruvector__hooks_remember** — Store a key-value pair in persistent memory for cross-session recall.
1310
+ \`{"key": "pattern-name", "value": "what to remember", "namespace": "patterns"}\`
1311
+ - **ruvector__hooks_recall** — Retrieve a previously stored memory by key.
1312
+ - **ruvector__hooks_suggest_context** — Get contextual suggestions based on current work.
1313
+ - **ruvector__hooks_swarm_recommend** — Get swarm topology recommendation for a task type.
1314
+ - **ruvector__hooks_capabilities** — List all intelligence system capabilities.
1315
+
1316
+ ### Code Analysis:
1317
+ - **ruvector__hooks_ast_analyze** — Analyze code structure (AST) of a file.
1318
+ - **ruvector__hooks_ast_complexity** — Get complexity metrics for code.
1319
+ - **ruvector__hooks_security_scan** — Scan code for security vulnerabilities.
1320
+ - **ruvector__hooks_diff_analyze** — Analyze a code diff for risk and impact.
1321
+ - **ruvector__hooks_diff_similar** — Find similar past diffs/changes.
1322
+
1323
+ ### Trajectory Learning (for multi-step tasks):
1324
+ - **ruvector__hooks_trajectory_begin** — Start tracking a multi-step task for learning.
1325
+ - **ruvector__hooks_trajectory_step** — Record a step in the current trajectory.
1326
+ - **ruvector__hooks_trajectory_end** — End trajectory, triggering pattern extraction.
1327
+
1328
+ ### Memory & Compression:
1329
+ - **ruvector__hooks_compress** — Compress/summarize long text for efficient storage.
1330
+ - **ruvector__hooks_rag_context** — Get RAG context for a query from stored knowledge.
1331
+ - **ruvector__hooks_learn** — Force the system to learn from provided examples.
1332
+ - **ruvector__hooks_batch_learn** — Learn from multiple examples at once.
1333
+ - **ruvector__hooks_stats** — View learning statistics and metrics.
1334
+ - **ruvector__hooks_doctor** — Run diagnostics on the intelligence system.
1335
+
1336
+ ## Group 3: Agents & Orchestration (ruflo)
1337
+ Spawn agents, coordinate swarms, manage tasks and workflows. ${TOOL_GROUPS.agents.enabled ? "ACTIVE" : "DISABLED"}
1338
+
1339
+ ### Agent Lifecycle:
1340
+ - **ruflo__agent_spawn** — Create a new specialized agent.
1341
+ \`{"type": "coder|researcher|tester|reviewer|architect|security", "name": "optional-name"}\`
1342
+ Agent types and when to use them:
1343
+ - \`coder\` — Write code, implement features, fix bugs
1344
+ - \`researcher\` — Find information, analyze documentation, investigate
1345
+ - \`tester\` — Write tests, run test suites, validate behavior
1346
+ - \`reviewer\` — Review code quality, security, best practices
1347
+ - \`architect\` — Design systems, plan architectures, evaluate trade-offs
1348
+ - \`security\` — Audit security, find vulnerabilities, recommend fixes
1349
+ - **ruflo__agent_status** — Check an agent's current state. \`{"agentId": "agent-xxx"}\`
1350
+ - **ruflo__agent_list** — List all active agents with their states.
1351
+ - **ruflo__agent_terminate** — Stop an agent. \`{"agentId": "agent-xxx"}\`
1352
+ - **ruflo__agent_health** — Health check across all agents.
1353
+ - **ruflo__agent_pool** — View the agent pool and available capacity.
1354
+
1355
+ ### Swarm Coordination:
1356
+ - **ruflo__swarm_init** — Initialize a multi-agent swarm.
1357
+ \`{"topology": "hierarchical|mesh|ring|star", "maxAgents": 8, "strategy": "balanced|specialized|adaptive"}\`
1358
+ - \`hierarchical\` — Coordinator + workers, best for structured tasks (anti-drift)
1359
+ - \`mesh\` — Peer-to-peer, best for collaborative work
1360
+ - \`ring\` — Sequential pipeline, best for ordered processing
1361
+ - \`star\` — Central hub, best for fan-out parallel work
1362
+ - **ruflo__swarm_status** — Get swarm health, topology, and agent states.
1363
+ - **ruflo__swarm_health** — Detailed health metrics for the swarm.
1364
+ - **ruflo__swarm_shutdown** — Tear down a swarm and all its agents.
1365
+
1366
+ ### Task Management:
1367
+ - **ruflo__task_create** — Create a tracked task.
1368
+ \`{"description": "what needs to be done", "priority": "low|normal|high|critical"}\`
1369
+ - **ruflo__task_status** — Check task progress. \`{"taskId": "task-xxx"}\`
1370
+ - **ruflo__task_list** — List all tasks with their statuses.
1371
+ - **ruflo__task_complete** — Mark a task as done. \`{"taskId": "task-xxx"}\`
1372
+ - **ruflo__task_update** — Update task details, status, or assignment.
1373
+ - **ruflo__task_cancel** — Cancel a task.
1374
+
1375
+ ### Workflow Orchestration:
1376
+ - **ruflo__workflow_create** — Define a multi-step workflow with dependencies.
1377
+ - **ruflo__workflow_execute** — Run a workflow. \`{"workflowId": "wf-xxx"}\`
1378
+ - **ruflo__workflow_status** — Check workflow progress.
1379
+ - **ruflo__workflow_template** — Use a pre-built workflow template.
1380
+ - **ruflo__workflow_pause** / **ruflo__workflow_resume** — Control workflow execution.
1381
+
1382
+ ### Hive-Mind (Distributed Consensus):
1383
+ - **ruflo__hive-mind_init** — Start distributed consensus system.
1384
+ - **ruflo__hive-mind_spawn** — Add an agent to the hive.
1385
+ - **ruflo__hive-mind_consensus** — Run consensus vote across agents.
1386
+ - **ruflo__hive-mind_broadcast** — Send message to all hive agents.
1387
+ - **ruflo__hive-mind_memory** — Access shared hive memory.
1388
+
1389
+ ### Coordination:
1390
+ - **ruflo__coordination_topology** — View/change coordination topology.
1391
+ - **ruflo__coordination_load_balance** — Distribute work across agents.
1392
+ - **ruflo__coordination_orchestrate** — Orchestrate complex multi-agent tasks.
1393
+ - **ruflo__coordination_sync** — Synchronize state across agents.
1394
+
1395
+ ### Session Management:
1396
+ - **ruflo__session_save** — Save current session state.
1397
+ - **ruflo__session_restore** — Restore a previous session.
1398
+ - **ruflo__session_list** — List available sessions.
1399
+
1400
+ ## Group 4: Memory & Knowledge (ruflo)
1401
+ Persistent memory, vector search, embeddings, and pattern storage. ${TOOL_GROUPS.memory.enabled ? "ACTIVE" : "DISABLED"}
1402
+
1403
+ ### Memory Operations:
1404
+ - **ruflo__memory_store** — Store data in persistent memory.
1405
+ \`{"key": "my-key", "value": "data to store", "namespace": "default", "tags": ["tag1"]}\`
1406
+ - **ruflo__memory_retrieve** — Get stored data by key. \`{"key": "my-key"}\`
1407
+ - **ruflo__memory_search** — Semantic vector search across stored memories.
1408
+ \`{"query": "what to search for", "limit": 5, "namespace": "default"}\`
1409
+ - **ruflo__memory_list** — List all stored keys in a namespace.
1410
+ - **ruflo__memory_delete** — Remove a stored memory.
1411
+ - **ruflo__memory_stats** — View memory usage statistics.
1412
+
1413
+ ### Embeddings:
1414
+ - **ruflo__embeddings_generate** — Generate vector embeddings for text.
1415
+ - **ruflo__embeddings_compare** — Compare semantic similarity of two texts.
1416
+ - **ruflo__embeddings_search** — Search embeddings database by similarity.
1417
+ - **ruflo__embeddings_neural** — Generate neural embeddings.
1418
+ - **ruflo__embeddings_hyperbolic** — Generate hyperbolic embeddings for hierarchical data.
1419
+
1420
+ ### AgentDB (Advanced Pattern Storage):
1421
+ - **ruflo__agentdb_pattern-store** — Store a learned pattern with metadata.
1422
+ \`{"pattern": "description", "category": "code|debug|architecture", "confidence": 0.9}\`
1423
+ - **ruflo__agentdb_pattern-search** — Search patterns by similarity.
1424
+ - **ruflo__agentdb_route** — Route a query to the most relevant stored pattern.
1425
+ - **ruflo__agentdb_feedback** — Provide feedback on a pattern (reinforcement learning).
1426
+ - **ruflo__agentdb_context-synthesize** — Synthesize context from multiple sources.
1427
+ - **ruflo__agentdb_semantic-route** — Semantic routing based on stored knowledge.
1428
+ - **ruflo__agentdb_consolidate** — Consolidate and deduplicate stored patterns.
1429
+ - **ruflo__agentdb_batch** — Batch operations on patterns.
1430
+ - **ruflo__agentdb_session-start** / **ruflo__agentdb_session-end** — Session tracking.
1431
+ - **ruflo__agentdb_hierarchical-store** / **ruflo__agentdb_hierarchical-recall** — Hierarchical memory.
1432
+
1433
+ ## Group 5: Dev Tools & Analysis (ruflo)
1434
+ Performance, system health, GitHub integration, code analysis, terminal. ${TOOL_GROUPS.devtools.enabled ? "ACTIVE" : "DISABLED"}
1435
+
1436
+ ### System & Performance:
1437
+ - **ruflo__system_status** — System health overview.
1438
+ - **ruflo__system_metrics** — Detailed performance metrics.
1439
+ - **ruflo__system_health** — Health check across all subsystems.
1440
+ - **ruflo__performance_report** — Generate performance report.
1441
+ - **ruflo__performance_bottleneck** — Identify performance bottlenecks.
1442
+ - **ruflo__performance_benchmark** — Run benchmarks.
1443
+ - **ruflo__performance_optimize** — Get optimization recommendations.
1444
+ - **ruflo__performance_profile** — Profile specific operations.
1445
+
1446
+ ### Code Analysis:
1447
+ - **ruflo__analyze_diff** — Analyze a code diff.
1448
+ - **ruflo__analyze_diff-risk** — Assess risk level of changes.
1449
+ - **ruflo__analyze_diff-classify** — Classify type of changes (feature, bugfix, refactor).
1450
+ - **ruflo__analyze_diff-reviewers** — Suggest code reviewers.
1451
+ - **ruflo__analyze_file-risk** — Assess risk of a specific file.
1452
+
1453
+ ### GitHub Integration:
1454
+ - **ruflo__github_repo_analyze** — Analyze a GitHub repository.
1455
+ \`{"repo": "owner/repo", "analysis_type": "code_quality|performance|security"}\`
1456
+ - **ruflo__github_pr_manage** — Manage pull requests (create, review, merge).
1457
+ - **ruflo__github_issue_track** — Track and manage issues.
1458
+ - **ruflo__github_workflow** — Manage GitHub Actions workflows.
1459
+ - **ruflo__github_metrics** — Repository metrics and insights.
1460
+
1461
+ ### Terminal Access:
1462
+ - **ruflo__terminal_create** — Create a terminal session.
1463
+ - **ruflo__terminal_execute** — Execute a command. \`{"command": "ls -la"}\`
1464
+ - **ruflo__terminal_list** — List active terminals.
1465
+ - **ruflo__terminal_history** — View command history.
1466
+
1467
+ ### Development Hooks:
1468
+ - **ruflo__hooks_pre-task** / **ruflo__hooks_post-task** — Task lifecycle hooks for learning.
1469
+ - **ruflo__hooks_pre-edit** / **ruflo__hooks_post-edit** — File edit hooks.
1470
+ - **ruflo__hooks_session-start** / **ruflo__hooks_session-end** — Session lifecycle.
1471
+ - **ruflo__hooks_worker-dispatch** — Dispatch background workers.
1472
+ Workers: \`optimize\`, \`audit\`, \`testgaps\`, \`document\`, \`map\`, \`deepdive\`, \`benchmark\`
1473
+ - **ruflo__hooks_model-route** — Route to optimal AI model for a task.
1474
+ - **ruflo__hooks_explain** — Explain a routing or intelligence decision.
1475
+
1476
+ ### Configuration:
1477
+ - **ruflo__config_get** / **ruflo__config_set** / **ruflo__config_list** — Manage settings.
1478
+
1479
+ ### Progress Tracking:
1480
+ - **ruflo__progress_check** — Check implementation progress.
1481
+ - **ruflo__progress_summary** — Summarize overall progress.
1482
+
1483
+ # Decision Framework — FOLLOW THIS EXACTLY
1484
+
1485
+ 1. **ANY factual question** → \`web_research(action='search')\` IMMEDIATELY. Do NOT answer from memory.
1486
+ 2. **"What is X?"** → \`web_research(action='search', query='X')\`
1487
+ 3. **"Compare X vs Y"** → \`web_research(action='compare', query='X vs Y', items=['X', 'Y'])\`
1488
+ 4. **"Is it true that..."** → \`web_research(action='fact_check', query='...')\`
1489
+ 5. **Complex research** → \`web_research(action='goap', query='...')\` — auto multi-step pipeline
1490
+ 6. **Internal docs/procedures** → \`search(query='...')\` first, then web_research if not found
1491
+ 7. **Code task** → \`ruvector__hooks_route\` + \`ruflo__agent_spawn\`
1492
+ 8. **Remember something** → \`ruflo__memory_store\` / \`ruflo__memory_search\`
1493
+ 9. **"What can you do?"** → \`guidance(topic='overview')\`
1494
+ 10. **Unknown** → \`guidance(topic='overview')\`
1495
+
1496
+ IMPORTANT: For questions 1-5, ALWAYS call web_research. Never say "I don't have access to search" — you DO.
1497
+
1498
+ # Execution Patterns
1499
+
1500
+ ### Simple Question
1501
+ \`search\` or \`web_research\` → synthesize → respond
1502
+
1503
+ ### Complex Research
1504
+ \`web_research(action='goap')\` → analyze → respond with citations
1505
+
1506
+ ### Code Implementation
1507
+ \`ruvector__hooks_route\` → \`ruflo__agent_spawn(coder)\` → track with \`ruflo__task_create\` → report
1508
+
1509
+ ### Multi-Agent Analysis
1510
+ \`ruflo__swarm_init(hierarchical)\` → spawn agents → coordinate → synthesize results
1511
+
1512
+ ### Learning & Memory
1513
+ \`ruflo__memory_search\` (check existing) → do work → \`ruflo__memory_store\` (save results) → \`ruvector__hooks_learn\`
1514
+
1515
+ # Parallel Execution
1516
+
1517
+ When multiple independent tools can help, call them ALL in parallel:
1518
+ - Search + web_research simultaneously
1519
+ - Spawn multiple agents at once (coder + tester + reviewer)
1520
+ - Run analysis + performance + security tools in parallel
1521
+ NEVER call tools sequentially when they could run in parallel.
1522
+
1523
+ # Response Rules
1524
+
1525
+ 1. **Call tools FIRST**, then present results conversationally — NEVER show raw JSON to the user
1526
+ 2. Use markdown: **bold** headers, bullet points, numbered steps, tables for comparisons
1527
+ 3. Synthesize tool results naturally — be a helpful colleague, not a data pipe
1528
+ 4. Cite sources when available from web_research results
1529
+ 5. If a tool fails, say so honestly and try an alternative approach
1530
+ 6. For complex tasks, briefly outline your plan before executing
1531
+ 7. After completing work, suggest relevant follow-up actions
1532
+ 8. When spawning agents, explain what each agent will do
1533
+
1534
+ # Never Expose to User
1535
+
1536
+ - Raw JSON, similarity scores, chunk IDs, internal IDs, task IDs
1537
+ - Tool names, function names, API endpoints, backend names
1538
+ - References to "MCP", "tool calls", "vectors", "embeddings", infrastructure
1539
+ - The prefixes "ruflo__" or "ruvector__" — just describe what you're doing naturally
1540
+ - Error stack traces — summarize errors in plain language`;
1541
+ }
1542
+
1543
+ // =============================================================================
1544
+ // AUTOPILOT MODE — Server-side auto-continue loop (ADR-037)
1545
+ // =============================================================================
1546
+
1547
+ const detailStore = new Map(); // detailToken → full tool result (TTL: 5min)
1548
+
1549
+ const AUTOPILOT_SYSTEM_PROMPT = `
1550
+ You are in AUTOPILOT MODE. You should:
1551
+ 1. Break complex tasks into steps and execute them using available tools
1552
+ 2. Call MULTIPLE tools in parallel when they are independent
1553
+ 3. After each tool result, analyze it and decide the next action
1554
+ 4. Continue until the task is complete — do NOT ask the user for confirmation
1555
+ 5. Use memory_search to find relevant patterns before starting
1556
+ 6. Summarize your progress at each step
1557
+ 7. When done, provide a final summary of everything accomplished
1558
+
1559
+ Parallel execution patterns:
1560
+ - Research: memory_search + hooks_route + agent_spawn(researcher) — all in parallel
1561
+ - Code: agent_spawn(coder) + agent_spawn(tester) — parallel, then review
1562
+ - Analysis: search multiple sources in parallel → synthesize → report
1563
+ - Security: security_scan + hooks_route(audit) + memory_search(CVEs) — parallel
1564
+ `;
1565
+
1566
+ const AUTOPILOT_BLOCKED_PATTERNS = [
1567
+ /^deploy_/,
1568
+ /^security_delete/,
1569
+ /^browser_fill$/,
1570
+ /^browser_click$/,
1571
+ /terminal_execute/,
1572
+ ];
1573
+
1574
+ function isBlockedTool(name) {
1575
+ return AUTOPILOT_BLOCKED_PATTERNS.some(p => p.test(name));
1576
+ }
1577
+
1578
+ function sendAutopilotEvent(res, data) {
1579
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
1580
+ }
1581
+
1582
+ function safeParseArgs(args) {
1583
+ if (typeof args === 'object' && args !== null) return args;
1584
+ try { return JSON.parse(args || '{}'); } catch { return {}; }
1585
+ }
1586
+
1587
+ function autopilotSleep(ms) {
1588
+ return new Promise(resolve => setTimeout(resolve, ms));
1589
+ }
1590
+
1591
+ async function handleAutopilot(req, res, provider, body) {
1592
+ const maxSteps = Math.min(parseInt(req.headers['x-autopilot-max-steps'] || '20', 10), 50);
1593
+ const cooldownMs = parseInt(process.env.AUTOPILOT_COOLDOWN || '500', 10);
1594
+ const stepTimeoutMs = parseInt(process.env.AUTOPILOT_STEP_TIMEOUT || '30000', 10);
1595
+
1596
+ // SSE setup
1597
+ res.setHeader('Content-Type', 'text/event-stream');
1598
+ res.setHeader('Cache-Control', 'no-cache');
1599
+ res.setHeader('Connection', 'keep-alive');
1600
+ res.setHeader('X-Accel-Buffering', 'no');
1601
+
1602
+ let messages = [...body.messages];
1603
+ let step = 0;
1604
+ let aborted = false;
1605
+ let totalTasks = 0;
1606
+ const startTime = Date.now();
1607
+
1608
+ req.on('close', () => { aborted = true; });
1609
+
1610
+ sendAutopilotEvent(res, { type: 'autopilot_start', maxSteps });
1611
+
1612
+ // Get the tools list for the AI provider (OpenAI function calling format)
1613
+ // Limit to BUILTIN_TOOLS + top external tools to avoid overwhelming the model
1614
+ // (Gemini struggles with 100+ tool definitions)
1615
+ const MAX_AUTOPILOT_TOOLS = 30;
1616
+ const externalTools = getActiveTools();
1617
+ // Prioritize tools from enabled high-value groups
1618
+ const priorityPrefixes = ['ruvector__hooks', 'ruvector__memory', 'ruflo__memory', 'ruflo__agent'];
1619
+ const prioritized = externalTools
1620
+ .sort((a, b) => {
1621
+ const aP = priorityPrefixes.some(p => a.name.startsWith(p)) ? 0 : 1;
1622
+ const bP = priorityPrefixes.some(p => b.name.startsWith(p)) ? 0 : 1;
1623
+ return aP - bP;
1624
+ })
1625
+ .slice(0, MAX_AUTOPILOT_TOOLS - BUILTIN_TOOLS.length);
1626
+ const allTools = [...BUILTIN_TOOLS, ...prioritized];
1627
+ const toolDefs = allTools.map(t => ({
1628
+ type: 'function',
1629
+ function: {
1630
+ name: t.name,
1631
+ description: t.description || '',
1632
+ parameters: t.inputSchema || { type: 'object', properties: {} },
1633
+ },
1634
+ }));
1635
+ console.log(`[autopilot] ${allTools.length} tools (${BUILTIN_TOOLS.length} builtin + ${prioritized.length} external)`);
1636
+
1637
+ while (step < maxSteps && !aborted) {
1638
+ // 1. Call upstream AI provider (non-streaming for tool call parsing)
1639
+ const apiKey = provider.getKey();
1640
+ let aiResult;
1641
+ try {
1642
+ const aiResponse = await fetch(provider.baseURL, {
1643
+ method: 'POST',
1644
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
1645
+ body: JSON.stringify({
1646
+ ...body,
1647
+ messages,
1648
+ stream: false,
1649
+ tools: toolDefs.length > 0 ? toolDefs : undefined,
1650
+ }),
1651
+ signal: AbortSignal.timeout(stepTimeoutMs),
1652
+ });
1653
+ aiResult = await aiResponse.json();
1654
+ } catch (err) {
1655
+ sendAutopilotEvent(res, { type: 'autopilot_error', error: `AI call failed: ${err.message}` });
1656
+ break;
1657
+ }
1658
+
1659
+ const choice = aiResult.choices?.[0];
1660
+ if (!choice) {
1661
+ sendAutopilotEvent(res, { type: 'autopilot_error', error: 'No response from AI' });
1662
+ break;
1663
+ }
1664
+
1665
+ // 2. Check for tool calls
1666
+ const toolCalls = choice.message?.tool_calls;
1667
+
1668
+ if (!toolCalls || toolCalls.length === 0) {
1669
+ // Final text response — send it
1670
+ sendAutopilotEvent(res, { type: 'autopilot_text', content: choice.message?.content || '' });
1671
+ break;
1672
+ }
1673
+
1674
+ // 3. Execute ALL tool calls in parallel
1675
+ step++;
1676
+ const groupId = `g${step}`;
1677
+ const taskEvents = toolCalls.map((tc, i) => ({
1678
+ taskId: `t${totalTasks + i + 1}`,
1679
+ tool: tc.function.name,
1680
+ args: safeParseArgs(tc.function.arguments),
1681
+ status: 'running',
1682
+ }));
1683
+ totalTasks += taskEvents.length;
1684
+
1685
+ // If the AI also included text content, stream it before tools
1686
+ if (choice.message?.content) {
1687
+ sendAutopilotEvent(res, { type: 'autopilot_text', content: choice.message.content });
1688
+ }
1689
+
1690
+ // Stream group start
1691
+ sendAutopilotEvent(res, { type: 'task_group_start', groupId, step, tasks: taskEvents });
1692
+
1693
+ // Append assistant message to conversation
1694
+ messages.push(choice.message);
1695
+
1696
+ // Execute tools in parallel
1697
+ const groupStart = Date.now();
1698
+ const results = await Promise.allSettled(
1699
+ toolCalls.map(async (tc, i) => {
1700
+ const taskId = taskEvents[i].taskId;
1701
+ const toolName = tc.function.name;
1702
+ const toolArgs = safeParseArgs(tc.function.arguments);
1703
+ const taskStart = Date.now();
1704
+
1705
+ // Check blocklist
1706
+ if (isBlockedTool(toolName)) {
1707
+ sendAutopilotEvent(res, {
1708
+ type: 'task_update', taskId, status: 'blocked',
1709
+ summary: `${toolName} requires confirmation`,
1710
+ duration: Date.now() - taskStart,
1711
+ });
1712
+ return { toolCallId: tc.id, blocked: true, toolName };
1713
+ }
1714
+
1715
+ try {
1716
+ const result = await executeTool(toolName, toolArgs);
1717
+ const resultStr = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
1718
+
1719
+ // Store full detail, generate token for lazy loading
1720
+ const detailToken = `dt_${taskId}_${Date.now()}`;
1721
+ detailStore.set(detailToken, resultStr);
1722
+
1723
+ // Stream task completion with summary only
1724
+ const summary = resultStr.length > 120
1725
+ ? resultStr.substring(0, 120).replace(/\n/g, ' ') + '...'
1726
+ : resultStr.replace(/\n/g, ' ');
1727
+
1728
+ sendAutopilotEvent(res, {
1729
+ type: 'task_update', taskId, status: 'completed',
1730
+ summary, duration: Date.now() - taskStart, detailToken,
1731
+ });
1732
+
1733
+ return { toolCallId: tc.id, content: resultStr };
1734
+ } catch (err) {
1735
+ sendAutopilotEvent(res, {
1736
+ type: 'task_update', taskId, status: 'failed',
1737
+ summary: err.message, duration: Date.now() - taskStart,
1738
+ });
1739
+ return { toolCallId: tc.id, content: `Error: ${err.message}` };
1740
+ }
1741
+ })
1742
+ );
1743
+
1744
+ // Stream group end
1745
+ sendAutopilotEvent(res, { type: 'task_group_end', groupId, step, duration: Date.now() - groupStart });
1746
+
1747
+ // Check if any tools were blocked — pause autopilot
1748
+ const blockedResults = results
1749
+ .filter(r => r.status === 'fulfilled' && r.value.blocked)
1750
+ .map(r => r.value);
1751
+ if (blockedResults.length > 0) {
1752
+ sendAutopilotEvent(res, {
1753
+ type: 'autopilot_paused',
1754
+ reason: 'blocked_tools',
1755
+ tools: blockedResults.map(b => b.toolName),
1756
+ });
1757
+ break;
1758
+ }
1759
+
1760
+ // Append tool results to messages
1761
+ for (const r of results) {
1762
+ if (r.status === 'fulfilled' && !r.value.blocked) {
1763
+ messages.push({
1764
+ role: 'tool',
1765
+ tool_call_id: r.value.toolCallId,
1766
+ content: r.value.content,
1767
+ });
1768
+ }
1769
+ }
1770
+
1771
+ // Cooldown to prevent runaway
1772
+ await autopilotSleep(cooldownMs);
1773
+ }
1774
+
1775
+ if (step >= maxSteps && !aborted) {
1776
+ sendAutopilotEvent(res, {
1777
+ type: 'autopilot_text',
1778
+ content: `\n⚠️ Autopilot reached max steps (${maxSteps}). Stopping.\n`,
1779
+ });
1780
+ }
1781
+
1782
+ sendAutopilotEvent(res, {
1783
+ type: 'autopilot_end',
1784
+ totalSteps: step,
1785
+ totalTasks,
1786
+ duration: Date.now() - startTime,
1787
+ });
1788
+
1789
+ res.write('data: [DONE]\n\n');
1790
+ res.end();
1791
+
1792
+ // Clean up detail store after 5 minutes
1793
+ const detailTTL = parseInt(process.env.AUTOPILOT_DETAIL_TTL || '300000', 10);
1794
+ setTimeout(() => {
1795
+ for (const [key] of detailStore) {
1796
+ if (key.startsWith('dt_')) detailStore.delete(key);
1797
+ }
1798
+ }, detailTTL);
1799
+ }
1800
+
1801
+ // Lazy detail loading endpoint
1802
+ app.get('/autopilot/detail/:token', (req, res) => {
1803
+ const content = detailStore.get(req.params.token);
1804
+ if (content) {
1805
+ res.json({ content });
1806
+ } else {
1807
+ res.status(404).json({ error: 'Detail expired or not found' });
1808
+ }
1809
+ });
1810
+
1811
+ // =============================================================================
1812
+ // CHAT COMPLETIONS PROXY
1813
+ // =============================================================================
1814
+
1815
+ app.post("/chat/completions", async (req, res) => {
1816
+ const model = req.body?.model;
1817
+ const providerName = resolveProvider(model);
1818
+ const provider = PROVIDER_ROUTES[providerName];
1819
+ const apiKey = provider.getKey();
1820
+
1821
+ if (!apiKey) return res.status(401).json({ error: { message: `No API key for provider: ${providerName}` } });
1822
+
1823
+ // Inject comprehensive system prompt as the first message
1824
+ const body = { ...req.body };
1825
+ if (body.messages && Array.isArray(body.messages)) {
1826
+ let systemPrompt = buildSystemPrompt();
1827
+ // Add autopilot instructions if autopilot mode is active
1828
+ const isAutopilot = req.headers['x-autopilot'] === 'true';
1829
+ if (isAutopilot) {
1830
+ systemPrompt = AUTOPILOT_SYSTEM_PROMPT + '\n\n' + systemPrompt;
1831
+ }
1832
+ // Prepend our system prompt before any existing messages
1833
+ const hasSystemMsg = body.messages[0]?.role === "system";
1834
+ if (hasSystemMsg) {
1835
+ // Merge with existing system message
1836
+ body.messages = [
1837
+ { role: "system", content: systemPrompt + "\n\n" + body.messages[0].content },
1838
+ ...body.messages.slice(1),
1839
+ ];
1840
+ } else {
1841
+ body.messages = [{ role: "system", content: systemPrompt }, ...body.messages];
1842
+ }
1843
+ }
1844
+
1845
+ // Route to autopilot handler if x-autopilot header is set
1846
+ if (req.headers['x-autopilot'] === 'true') {
1847
+ return handleAutopilot(req, res, provider, body);
1848
+ }
1849
+
1850
+ try {
1851
+ const upstream = await fetch(provider.baseURL, {
1852
+ method: "POST",
1853
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
1854
+ body: JSON.stringify(body),
1855
+ });
1856
+
1857
+ if (!upstream.ok) {
1858
+ const errBody = await upstream.text();
1859
+ console.error(`Proxy error [${providerName}/${model}]: ${upstream.status} ${errBody.substring(0, 200)}`);
1860
+ // Normalize all upstream errors into OpenAI-compatible format so the
1861
+ // Chat UI's OpenAI SDK can parse them instead of "400 (no body)".
1862
+ let errorMessage = `Upstream ${providerName} error (${upstream.status})`;
1863
+ try {
1864
+ const parsed = JSON.parse(errBody);
1865
+ // Gemini returns [{"error": {"message": "..."}}]
1866
+ if (Array.isArray(parsed) && parsed[0]?.error?.message) {
1867
+ errorMessage = parsed[0].error.message;
1868
+ // OpenAI/OpenRouter return {"error": {"message": "..."}}
1869
+ } else if (parsed.error?.message) {
1870
+ errorMessage = parsed.error.message;
1871
+ }
1872
+ } catch {}
1873
+ return res.status(upstream.status).json({
1874
+ error: { message: errorMessage, type: "upstream_error", code: upstream.status },
1875
+ });
1876
+ }
1877
+
1878
+ res.setHeader("Content-Type", upstream.headers.get("content-type") || "application/json");
1879
+
1880
+ if (req.body?.stream && upstream.body) {
1881
+ const reader = upstream.body.getReader();
1882
+ const decoder = new TextDecoder();
1883
+ try {
1884
+ while (true) {
1885
+ const { done, value } = await reader.read();
1886
+ if (done) break;
1887
+ res.write(decoder.decode(value, { stream: true }));
1888
+ }
1889
+ } catch (e) { /* stream closed */ }
1890
+ finally { res.end(); }
1891
+ } else {
1892
+ res.send(await upstream.text());
1893
+ }
1894
+ } catch (err) {
1895
+ console.error(`Proxy error [${providerName}/${model}]:`, err.message);
1896
+ res.status(502).json({ error: { message: `Upstream error: ${err.message}` } });
1897
+ }
1898
+ });
1899
+
1900
+ // =============================================================================
1901
+ // MODELS & HEALTH
1902
+ // =============================================================================
1903
+
1904
+ const KNOWN_MODELS = [
1905
+ "gemini-2.5-pro", "gemini-2.5-flash",
1906
+ "gpt-4.1", "gpt-4.1-mini", "gpt-4o", "gpt-4o-mini",
1907
+ "o3-mini", "o1-mini",
1908
+ ];
1909
+
1910
+ app.get("/models", (_, res) => {
1911
+ res.json({ object: "list", data: KNOWN_MODELS.map(id => ({
1912
+ id,
1913
+ object: "model",
1914
+ owned_by: "system",
1915
+ providers: [{ supports_tools: true }],
1916
+ })) });
1917
+ });
1918
+
1919
+ app.get("/health", (_, res) => {
1920
+ const backends = {};
1921
+ for (const [name, client] of mcpBackends) {
1922
+ backends[name] = { ready: client.ready, tools: client.tools.length };
1923
+ }
1924
+ const activeTools = getActiveTools();
1925
+ const groups = {};
1926
+ for (const [name, g] of Object.entries(TOOL_GROUPS)) {
1927
+ groups[name] = { enabled: g.enabled, source: g.source };
1928
+ }
1929
+ res.json({
1930
+ status: "ok", service: "mcp-bridge", version: "2.0.0",
1931
+ tools: { builtin: BUILTIN_TOOLS.length, external: activeTools.length, total: BUILTIN_TOOLS.length + activeTools.length },
1932
+ groups, backends,
1933
+ });
1934
+ });
1935
+
1936
+ // GET /groups — list tool groups and their status
1937
+ app.get("/groups", (_, res) => {
1938
+ const activeTools = getActiveTools();
1939
+ const result = {};
1940
+ for (const [name, g] of Object.entries(TOOL_GROUPS)) {
1941
+ const tools = name === "core" ? BUILTIN_TOOLS :
1942
+ activeTools.filter(t => {
1943
+ if (g.source !== t._backend) return false;
1944
+ if (!g.prefixes) return true;
1945
+ return g.prefixes.some(p => t._originalName.startsWith(p));
1946
+ });
1947
+ result[name] = {
1948
+ enabled: g.enabled,
1949
+ description: g.description,
1950
+ tools: tools.length,
1951
+ toolNames: tools.map(t => t.name).slice(0, 10),
1952
+ };
1953
+ }
1954
+ res.json(result);
1955
+ });
1956
+
1957
+ // =============================================================================
1958
+ // STARTUP
1959
+ // =============================================================================
1960
+
1961
+ async function main() {
1962
+ const isPublic = BIND_HOST !== "127.0.0.1" && BIND_HOST !== "localhost";
1963
+ if (isPublic && !process.env.MCP_AUTH_TOKEN) {
1964
+ console.error(
1965
+ "FATAL: refusing to bind a public interface without MCP_AUTH_TOKEN. " +
1966
+ "Generate one with: MCP_AUTH_TOKEN=$(openssl rand -base64 32)"
1967
+ );
1968
+ process.exit(1);
1969
+ }
1970
+ app.listen(PORT, BIND_HOST, () => {
1971
+ console.log(`MCP Bridge v2.0.0 on port ${PORT} (${BIND_HOST})`);
1972
+ const enabled = Object.entries(TOOL_GROUPS).filter(([, g]) => g.enabled).map(([n]) => n);
1973
+ console.log(`Active groups: ${enabled.join(", ")}`);
1974
+ // ADR-166 §6 — startup posture banner (helps operators see the security state at boot)
1975
+ console.log(
1976
+ `[security] bind=${BIND_HOST} auth=${process.env.MCP_AUTH_TOKEN ? "bearer" : "off (local-only)"} ` +
1977
+ `terminal=${MCP_ENABLE_TERMINAL ? "ENABLED (⚠ opt-in)" : "disabled"}`,
1978
+ );
1979
+ if (MCP_ENABLE_TERMINAL) {
1980
+ console.warn(
1981
+ "[security] WARNING: terminal_execute is enabled. This tool grants shell access " +
1982
+ "inside the bridge container to any client the auth layer accepts. Ensure " +
1983
+ "MCP_AUTH_TOKEN is set on any non-loopback bind. See ADR-166 §6 Phase 1d.",
1984
+ );
1985
+ }
1986
+ });
1987
+
1988
+ const anyBackendNeeded = BACKEND_DEFS.some(isBackendNeeded);
1989
+ if (anyBackendNeeded) {
1990
+ console.log("Initializing MCP backends...");
1991
+ await initBackends();
1992
+ }
1993
+ }
1994
+
1995
+ main().catch(err => { console.error("Fatal:", err); process.exit(1); });