ruflo 3.10.36 → 3.10.37

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 (498) hide show
  1. package/README.md +416 -416
  2. package/bin/ruflo.js +77 -77
  3. package/package.json +113 -113
  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 +45 -45
  8. package/src/mcp-bridge/index.js +1692 -1692
  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/nginx/Dockerfile +10 -10
  13. package/src/nginx/nginx.conf +67 -67
  14. package/src/nginx/static/favicon-dark.svg +4 -4
  15. package/src/nginx/static/favicon.svg +4 -4
  16. package/src/nginx/static/icon.svg +5 -5
  17. package/src/nginx/static/logo.svg +9 -9
  18. package/src/nginx/static/manifest.json +22 -22
  19. package/src/nginx/static/welcome.js +184 -184
  20. package/src/ruvocal/.claude/skills/add-model-descriptions/SKILL.md +73 -73
  21. package/src/ruvocal/.devcontainer/Dockerfile +9 -9
  22. package/src/ruvocal/.devcontainer/devcontainer.json +36 -36
  23. package/src/ruvocal/.dockerignore +16 -16
  24. package/src/ruvocal/.eslintignore +13 -13
  25. package/src/ruvocal/.eslintrc.cjs +45 -45
  26. package/src/ruvocal/.gcloudignore +18 -18
  27. package/src/ruvocal/.github/ISSUE_TEMPLATE/bug-report--chat-ui-.md +43 -43
  28. package/src/ruvocal/.github/ISSUE_TEMPLATE/config-support.md +9 -9
  29. package/src/ruvocal/.github/ISSUE_TEMPLATE/feature-request--chat-ui-.md +17 -17
  30. package/src/ruvocal/.github/ISSUE_TEMPLATE/huggingchat.md +11 -11
  31. package/src/ruvocal/.github/release.yml +16 -16
  32. package/src/ruvocal/.github/workflows/build-docs.yml +18 -18
  33. package/src/ruvocal/.github/workflows/build-image.yml +142 -142
  34. package/src/ruvocal/.github/workflows/build-pr-docs.yml +20 -20
  35. package/src/ruvocal/.github/workflows/deploy-dev.yml +63 -63
  36. package/src/ruvocal/.github/workflows/deploy-prod.yml +78 -78
  37. package/src/ruvocal/.github/workflows/lint-and-test.yml +84 -84
  38. package/src/ruvocal/.github/workflows/slugify.yaml +72 -72
  39. package/src/ruvocal/.github/workflows/trufflehog.yml +17 -17
  40. package/src/ruvocal/.github/workflows/upload-pr-documentation.yml +16 -16
  41. package/src/ruvocal/.husky/lint-stage-config.js +4 -4
  42. package/src/ruvocal/.husky/pre-commit +2 -2
  43. package/src/ruvocal/.prettierignore +14 -14
  44. package/src/ruvocal/.prettierrc +7 -7
  45. package/src/ruvocal/CLAUDE.md +126 -126
  46. package/src/ruvocal/Dockerfile +96 -96
  47. package/src/ruvocal/LICENSE +202 -202
  48. package/src/ruvocal/PRIVACY.md +41 -41
  49. package/src/ruvocal/README.md +164 -164
  50. package/src/ruvocal/chart/Chart.yaml +5 -5
  51. package/src/ruvocal/chart/env/dev.yaml +260 -260
  52. package/src/ruvocal/chart/env/prod.yaml +273 -273
  53. package/src/ruvocal/chart/templates/_helpers.tpl +22 -22
  54. package/src/ruvocal/chart/templates/config.yaml +10 -10
  55. package/src/ruvocal/chart/templates/deployment.yaml +81 -81
  56. package/src/ruvocal/chart/templates/hpa.yaml +45 -45
  57. package/src/ruvocal/chart/templates/infisical.yaml +24 -24
  58. package/src/ruvocal/chart/templates/ingress-internal.yaml +32 -32
  59. package/src/ruvocal/chart/templates/ingress.yaml +32 -32
  60. package/src/ruvocal/chart/templates/network-policy.yaml +36 -36
  61. package/src/ruvocal/chart/templates/service-account.yaml +13 -13
  62. package/src/ruvocal/chart/templates/service-monitor.yaml +17 -17
  63. package/src/ruvocal/chart/templates/service.yaml +21 -21
  64. package/src/ruvocal/chart/values.yaml +73 -73
  65. package/src/ruvocal/cloudbuild.yaml +68 -68
  66. package/src/ruvocal/config/branding.env.example +19 -19
  67. package/src/ruvocal/docker-compose.yml +21 -21
  68. package/src/ruvocal/docs/adr/ADR-029-HUGGINGFACE-CHAT-UI-CLOUD-RUN.md +1236 -1236
  69. package/src/ruvocal/docs/adr/ADR-033-RUVECTOR-RUFLO-MCP-INTEGRATION.md +111 -111
  70. package/src/ruvocal/docs/adr/ADR-034-OPTIONAL-MCP-BACKENDS.md +117 -117
  71. package/src/ruvocal/docs/adr/ADR-035-MCP-TOOL-GROUPS.md +186 -186
  72. package/src/ruvocal/docs/adr/ADR-037-AUTOPILOT-CHAT-MODE.md +1500 -1500
  73. package/src/ruvocal/docs/adr/ADR-038-RUVOCAL-FORK.md +286 -286
  74. package/src/ruvocal/docs/source/_toctree.yml +30 -30
  75. package/src/ruvocal/docs/source/configuration/common-issues.md +38 -38
  76. package/src/ruvocal/docs/source/configuration/llm-router.md +105 -105
  77. package/src/ruvocal/docs/source/configuration/mcp-tools.md +84 -84
  78. package/src/ruvocal/docs/source/configuration/metrics.md +9 -9
  79. package/src/ruvocal/docs/source/configuration/open-id.md +57 -57
  80. package/src/ruvocal/docs/source/configuration/overview.md +89 -89
  81. package/src/ruvocal/docs/source/configuration/theming.md +20 -20
  82. package/src/ruvocal/docs/source/developing/architecture.md +48 -48
  83. package/src/ruvocal/docs/source/index.md +53 -53
  84. package/src/ruvocal/docs/source/installation/docker.md +43 -43
  85. package/src/ruvocal/docs/source/installation/helm.md +43 -43
  86. package/src/ruvocal/docs/source/installation/local.md +62 -62
  87. package/src/ruvocal/entrypoint.sh +18 -18
  88. package/src/ruvocal/mcp-bridge/Dockerfile +45 -45
  89. package/src/ruvocal/mcp-bridge/cloudbuild.yaml +49 -49
  90. package/src/ruvocal/mcp-bridge/index.js +1902 -1902
  91. package/src/ruvocal/mcp-bridge/mcp-stdio-kernel.js +159 -159
  92. package/src/ruvocal/mcp-bridge/package-lock.json +762 -762
  93. package/src/ruvocal/mcp-bridge/package.json +17 -17
  94. package/src/ruvocal/mcp-bridge/test-harness.js +470 -470
  95. package/src/ruvocal/package-lock.json +11741 -11741
  96. package/src/ruvocal/package.json +121 -121
  97. package/src/ruvocal/postcss.config.js +6 -6
  98. package/src/ruvocal/rvf.manifest.json +204 -204
  99. package/src/ruvocal/scripts/config.ts +64 -64
  100. package/src/ruvocal/scripts/generate-welcome.mjs +181 -181
  101. package/src/ruvocal/scripts/populate.ts +288 -288
  102. package/src/ruvocal/scripts/samples.txt +194 -194
  103. package/src/ruvocal/scripts/setups/vitest-setup-server.ts +44 -44
  104. package/src/ruvocal/scripts/updateLocalEnv.ts +48 -48
  105. package/src/ruvocal/src/ambient.d.ts +7 -7
  106. package/src/ruvocal/src/app.d.ts +29 -29
  107. package/src/ruvocal/src/app.html +53 -53
  108. package/src/ruvocal/src/hooks.server.ts +32 -32
  109. package/src/ruvocal/src/hooks.ts +6 -6
  110. package/src/ruvocal/src/lib/APIClient.ts +148 -148
  111. package/src/ruvocal/src/lib/actions/clickOutside.ts +18 -18
  112. package/src/ruvocal/src/lib/actions/snapScrollToBottom.ts +346 -346
  113. package/src/ruvocal/src/lib/buildPrompt.ts +33 -33
  114. package/src/ruvocal/src/lib/components/AnnouncementBanner.svelte +20 -20
  115. package/src/ruvocal/src/lib/components/BackgroundGenerationPoller.svelte +168 -168
  116. package/src/ruvocal/src/lib/components/CodeBlock.svelte +73 -73
  117. package/src/ruvocal/src/lib/components/CopyToClipBoardBtn.svelte +92 -92
  118. package/src/ruvocal/src/lib/components/DeleteConversationModal.svelte +75 -75
  119. package/src/ruvocal/src/lib/components/EditConversationModal.svelte +100 -100
  120. package/src/ruvocal/src/lib/components/ExpandNavigation.svelte +22 -22
  121. package/src/ruvocal/src/lib/components/FoundationBackground.svelte +242 -242
  122. package/src/ruvocal/src/lib/components/HoverTooltip.svelte +44 -44
  123. package/src/ruvocal/src/lib/components/HtmlPreviewModal.svelte +143 -143
  124. package/src/ruvocal/src/lib/components/InfiniteScroll.svelte +50 -50
  125. package/src/ruvocal/src/lib/components/MobileNav.svelte +300 -300
  126. package/src/ruvocal/src/lib/components/Modal.svelte +115 -115
  127. package/src/ruvocal/src/lib/components/ModelCardMetadata.svelte +71 -71
  128. package/src/ruvocal/src/lib/components/NavConversationItem.svelte +151 -151
  129. package/src/ruvocal/src/lib/components/NavMenu.svelte +313 -313
  130. package/src/ruvocal/src/lib/components/Pagination.svelte +97 -97
  131. package/src/ruvocal/src/lib/components/PaginationArrow.svelte +27 -27
  132. package/src/ruvocal/src/lib/components/Portal.svelte +24 -24
  133. package/src/ruvocal/src/lib/components/RetryBtn.svelte +18 -18
  134. package/src/ruvocal/src/lib/components/RuFloUniverse.svelte +185 -185
  135. package/src/ruvocal/src/lib/components/RufloHelpModal.svelte +411 -411
  136. package/src/ruvocal/src/lib/components/ScrollToBottomBtn.svelte +47 -47
  137. package/src/ruvocal/src/lib/components/ScrollToPreviousBtn.svelte +77 -77
  138. package/src/ruvocal/src/lib/components/ShareConversationModal.svelte +182 -182
  139. package/src/ruvocal/src/lib/components/StopGeneratingBtn.svelte +69 -69
  140. package/src/ruvocal/src/lib/components/SubscribeModal.svelte +87 -87
  141. package/src/ruvocal/src/lib/components/Switch.svelte +36 -36
  142. package/src/ruvocal/src/lib/components/SystemPromptModal.svelte +44 -44
  143. package/src/ruvocal/src/lib/components/Toast.svelte +27 -27
  144. package/src/ruvocal/src/lib/components/Tooltip.svelte +30 -30
  145. package/src/ruvocal/src/lib/components/WelcomeModal.svelte +46 -46
  146. package/src/ruvocal/src/lib/components/chat/Alternatives.svelte +77 -77
  147. package/src/ruvocal/src/lib/components/chat/BlockWrapper.svelte +72 -72
  148. package/src/ruvocal/src/lib/components/chat/ChatInput.svelte +490 -490
  149. package/src/ruvocal/src/lib/components/chat/ChatIntroduction.svelte +123 -123
  150. package/src/ruvocal/src/lib/components/chat/ChatMessage.svelte +548 -548
  151. package/src/ruvocal/src/lib/components/chat/ChatWindow.svelte +1057 -1057
  152. package/src/ruvocal/src/lib/components/chat/FileDropzone.svelte +92 -92
  153. package/src/ruvocal/src/lib/components/chat/ImageLightbox.svelte +66 -66
  154. package/src/ruvocal/src/lib/components/chat/MarkdownBlock.svelte +23 -23
  155. package/src/ruvocal/src/lib/components/chat/MarkdownRenderer.svelte +69 -69
  156. package/src/ruvocal/src/lib/components/chat/MarkdownRenderer.svelte.test.ts +58 -58
  157. package/src/ruvocal/src/lib/components/chat/MessageAvatar.svelte +103 -103
  158. package/src/ruvocal/src/lib/components/chat/ModelSwitch.svelte +64 -64
  159. package/src/ruvocal/src/lib/components/chat/OpenReasoningResults.svelte +81 -81
  160. package/src/ruvocal/src/lib/components/chat/TaskGroup.svelte +88 -88
  161. package/src/ruvocal/src/lib/components/chat/ToolUpdate.svelte +273 -273
  162. package/src/ruvocal/src/lib/components/chat/UploadedFile.svelte +253 -253
  163. package/src/ruvocal/src/lib/components/chat/UrlFetchModal.svelte +203 -203
  164. package/src/ruvocal/src/lib/components/chat/VoiceRecorder.svelte +214 -214
  165. package/src/ruvocal/src/lib/components/icons/IconBurger.svelte +20 -20
  166. package/src/ruvocal/src/lib/components/icons/IconCheap.svelte +20 -20
  167. package/src/ruvocal/src/lib/components/icons/IconChevron.svelte +24 -24
  168. package/src/ruvocal/src/lib/components/icons/IconDazzled.svelte +40 -40
  169. package/src/ruvocal/src/lib/components/icons/IconFast.svelte +20 -20
  170. package/src/ruvocal/src/lib/components/icons/IconLoading.svelte +22 -22
  171. package/src/ruvocal/src/lib/components/icons/IconMCP.svelte +28 -28
  172. package/src/ruvocal/src/lib/components/icons/IconMoon.svelte +21 -21
  173. package/src/ruvocal/src/lib/components/icons/IconNew.svelte +20 -20
  174. package/src/ruvocal/src/lib/components/icons/IconOmni.svelte +90 -90
  175. package/src/ruvocal/src/lib/components/icons/IconPaperclip.svelte +24 -24
  176. package/src/ruvocal/src/lib/components/icons/IconPro.svelte +37 -37
  177. package/src/ruvocal/src/lib/components/icons/IconShare.svelte +21 -21
  178. package/src/ruvocal/src/lib/components/icons/IconSun.svelte +93 -93
  179. package/src/ruvocal/src/lib/components/icons/Logo.svelte +68 -68
  180. package/src/ruvocal/src/lib/components/icons/LogoHuggingFaceBorderless.svelte +54 -54
  181. package/src/ruvocal/src/lib/components/mcp/AddServerForm.svelte +250 -250
  182. package/src/ruvocal/src/lib/components/mcp/MCPServerManager.svelte +185 -185
  183. package/src/ruvocal/src/lib/components/mcp/ServerCard.svelte +203 -203
  184. package/src/ruvocal/src/lib/components/players/AudioPlayer.svelte +82 -82
  185. package/src/ruvocal/src/lib/components/voice/AudioWaveform.svelte +96 -96
  186. package/src/ruvocal/src/lib/components/wasm/GalleryPanel.svelte +357 -357
  187. package/src/ruvocal/src/lib/constants/mcpExamples.ts +114 -114
  188. package/src/ruvocal/src/lib/constants/mime.ts +11 -11
  189. package/src/ruvocal/src/lib/constants/pagination.ts +1 -1
  190. package/src/ruvocal/src/lib/constants/publicSepToken.ts +1 -1
  191. package/src/ruvocal/src/lib/constants/routerExamples.ts +133 -133
  192. package/src/ruvocal/src/lib/constants/rvagentPresets.ts +206 -206
  193. package/src/ruvocal/src/lib/createShareLink.ts +27 -27
  194. package/src/ruvocal/src/lib/jobs/refresh-conversation-stats.ts +297 -297
  195. package/src/ruvocal/src/lib/migrations/lock.ts +56 -56
  196. package/src/ruvocal/src/lib/migrations/migrations.spec.ts +74 -74
  197. package/src/ruvocal/src/lib/migrations/migrations.ts +109 -109
  198. package/src/ruvocal/src/lib/migrations/routines/01-update-search-assistants.ts +50 -50
  199. package/src/ruvocal/src/lib/migrations/routines/02-update-assistants-models.ts +48 -48
  200. package/src/ruvocal/src/lib/migrations/routines/04-update-message-updates.ts +151 -151
  201. package/src/ruvocal/src/lib/migrations/routines/05-update-message-files.ts +56 -56
  202. package/src/ruvocal/src/lib/migrations/routines/06-trim-message-updates.ts +56 -56
  203. package/src/ruvocal/src/lib/migrations/routines/08-update-featured-to-review.ts +32 -32
  204. package/src/ruvocal/src/lib/migrations/routines/09-delete-empty-conversations.spec.ts +214 -214
  205. package/src/ruvocal/src/lib/migrations/routines/09-delete-empty-conversations.ts +88 -88
  206. package/src/ruvocal/src/lib/migrations/routines/10-update-reports-assistantid.ts +29 -29
  207. package/src/ruvocal/src/lib/migrations/routines/index.ts +15 -15
  208. package/src/ruvocal/src/lib/server/__tests__/conversation-stop-generating.spec.ts +103 -103
  209. package/src/ruvocal/src/lib/server/abortRegistry.ts +57 -57
  210. package/src/ruvocal/src/lib/server/abortedGenerations.ts +43 -43
  211. package/src/ruvocal/src/lib/server/adminToken.ts +62 -62
  212. package/src/ruvocal/src/lib/server/api/__tests__/conversations-id.spec.ts +296 -296
  213. package/src/ruvocal/src/lib/server/api/__tests__/conversations-message.spec.ts +216 -216
  214. package/src/ruvocal/src/lib/server/api/__tests__/conversations.spec.ts +235 -235
  215. package/src/ruvocal/src/lib/server/api/__tests__/misc.spec.ts +72 -72
  216. package/src/ruvocal/src/lib/server/api/__tests__/testHelpers.ts +86 -86
  217. package/src/ruvocal/src/lib/server/api/__tests__/user-reports.spec.ts +78 -78
  218. package/src/ruvocal/src/lib/server/api/__tests__/user.spec.ts +239 -239
  219. package/src/ruvocal/src/lib/server/api/types.ts +37 -37
  220. package/src/ruvocal/src/lib/server/api/utils/requireAuth.ts +22 -22
  221. package/src/ruvocal/src/lib/server/api/utils/resolveConversation.ts +69 -69
  222. package/src/ruvocal/src/lib/server/api/utils/resolveModel.ts +27 -27
  223. package/src/ruvocal/src/lib/server/api/utils/superjsonResponse.ts +15 -15
  224. package/src/ruvocal/src/lib/server/apiToken.ts +11 -11
  225. package/src/ruvocal/src/lib/server/auth.ts +554 -554
  226. package/src/ruvocal/src/lib/server/config.ts +187 -187
  227. package/src/ruvocal/src/lib/server/conversation.ts +83 -83
  228. package/src/ruvocal/src/lib/server/database/__tests__/rvf.spec.ts +709 -709
  229. package/src/ruvocal/src/lib/server/database/postgres.ts +700 -700
  230. package/src/ruvocal/src/lib/server/database/rvf.ts +1078 -1078
  231. package/src/ruvocal/src/lib/server/database.ts +145 -145
  232. package/src/ruvocal/src/lib/server/endpoints/document.ts +68 -68
  233. package/src/ruvocal/src/lib/server/endpoints/endpoints.ts +43 -43
  234. package/src/ruvocal/src/lib/server/endpoints/images.ts +211 -211
  235. package/src/ruvocal/src/lib/server/endpoints/openai/endpointOai.ts +266 -266
  236. package/src/ruvocal/src/lib/server/endpoints/openai/openAIChatToTextGenerationStream.ts +212 -212
  237. package/src/ruvocal/src/lib/server/endpoints/openai/openAICompletionToTextGenerationStream.ts +32 -32
  238. package/src/ruvocal/src/lib/server/endpoints/preprocessMessages.ts +61 -61
  239. package/src/ruvocal/src/lib/server/exitHandler.ts +59 -59
  240. package/src/ruvocal/src/lib/server/files/downloadFile.ts +34 -34
  241. package/src/ruvocal/src/lib/server/files/uploadFile.ts +29 -29
  242. package/src/ruvocal/src/lib/server/findRepoRoot.ts +13 -13
  243. package/src/ruvocal/src/lib/server/generateFromDefaultEndpoint.ts +46 -46
  244. package/src/ruvocal/src/lib/server/hooks/error.ts +37 -37
  245. package/src/ruvocal/src/lib/server/hooks/fetch.ts +22 -22
  246. package/src/ruvocal/src/lib/server/hooks/handle.ts +250 -250
  247. package/src/ruvocal/src/lib/server/hooks/init.ts +51 -51
  248. package/src/ruvocal/src/lib/server/isURLLocal.spec.ts +31 -31
  249. package/src/ruvocal/src/lib/server/isURLLocal.ts +74 -74
  250. package/src/ruvocal/src/lib/server/logger.ts +42 -42
  251. package/src/ruvocal/src/lib/server/mcp/clientPool.spec.ts +175 -175
  252. package/src/ruvocal/src/lib/server/mcp/hf.ts +32 -32
  253. package/src/ruvocal/src/lib/server/mcp/httpClient.ts +122 -122
  254. package/src/ruvocal/src/lib/server/mcp/registry.ts +76 -76
  255. package/src/ruvocal/src/lib/server/mcp/tools.ts +196 -196
  256. package/src/ruvocal/src/lib/server/metrics.ts +255 -255
  257. package/src/ruvocal/src/lib/server/models.ts +518 -518
  258. package/src/ruvocal/src/lib/server/requestContext.ts +55 -55
  259. package/src/ruvocal/src/lib/server/router/arch.ts +230 -230
  260. package/src/ruvocal/src/lib/server/router/endpoint.ts +316 -316
  261. package/src/ruvocal/src/lib/server/router/multimodal.ts +28 -28
  262. package/src/ruvocal/src/lib/server/router/policy.ts +49 -49
  263. package/src/ruvocal/src/lib/server/router/toolsRoute.ts +51 -51
  264. package/src/ruvocal/src/lib/server/router/types.ts +21 -21
  265. package/src/ruvocal/src/lib/server/sendSlack.ts +23 -23
  266. package/src/ruvocal/src/lib/server/textGeneration/generate.ts +258 -258
  267. package/src/ruvocal/src/lib/server/textGeneration/index.ts +96 -96
  268. package/src/ruvocal/src/lib/server/textGeneration/mcp/fileRefs.ts +155 -155
  269. package/src/ruvocal/src/lib/server/textGeneration/mcp/routerResolution.ts +108 -108
  270. package/src/ruvocal/src/lib/server/textGeneration/mcp/runMcpFlow.ts +831 -831
  271. package/src/ruvocal/src/lib/server/textGeneration/mcp/toolInvocation.ts +349 -349
  272. package/src/ruvocal/src/lib/server/textGeneration/mcp/wasmTools.test.ts +633 -633
  273. package/src/ruvocal/src/lib/server/textGeneration/reasoning.ts +23 -23
  274. package/src/ruvocal/src/lib/server/textGeneration/title.ts +83 -83
  275. package/src/ruvocal/src/lib/server/textGeneration/types.ts +28 -28
  276. package/src/ruvocal/src/lib/server/textGeneration/utils/prepareFiles.ts +88 -88
  277. package/src/ruvocal/src/lib/server/textGeneration/utils/routing.ts +21 -21
  278. package/src/ruvocal/src/lib/server/textGeneration/utils/toolPrompt.ts +49 -49
  279. package/src/ruvocal/src/lib/server/urlSafety.ts +77 -77
  280. package/src/ruvocal/src/lib/server/usageLimits.ts +30 -30
  281. package/src/ruvocal/src/lib/stores/autopilotStore.svelte.ts +175 -175
  282. package/src/ruvocal/src/lib/stores/backgroundGenerations.svelte.ts +32 -32
  283. package/src/ruvocal/src/lib/stores/backgroundGenerations.ts +1 -1
  284. package/src/ruvocal/src/lib/stores/errors.ts +9 -9
  285. package/src/ruvocal/src/lib/stores/isAborted.ts +3 -3
  286. package/src/ruvocal/src/lib/stores/isPro.ts +4 -4
  287. package/src/ruvocal/src/lib/stores/loading.ts +3 -3
  288. package/src/ruvocal/src/lib/stores/mcpServers.ts +534 -534
  289. package/src/ruvocal/src/lib/stores/pendingChatInput.ts +3 -3
  290. package/src/ruvocal/src/lib/stores/pendingMessage.ts +9 -9
  291. package/src/ruvocal/src/lib/stores/settings.ts +182 -182
  292. package/src/ruvocal/src/lib/stores/shareModal.ts +13 -13
  293. package/src/ruvocal/src/lib/stores/titleUpdate.ts +8 -8
  294. package/src/ruvocal/src/lib/stores/wasmMcp.ts +472 -472
  295. package/src/ruvocal/src/lib/switchTheme.ts +124 -124
  296. package/src/ruvocal/src/lib/types/AbortedGeneration.ts +8 -8
  297. package/src/ruvocal/src/lib/types/Assistant.ts +31 -31
  298. package/src/ruvocal/src/lib/types/AssistantStats.ts +11 -11
  299. package/src/ruvocal/src/lib/types/ConfigKey.ts +4 -4
  300. package/src/ruvocal/src/lib/types/ConvSidebar.ts +9 -9
  301. package/src/ruvocal/src/lib/types/Conversation.ts +27 -27
  302. package/src/ruvocal/src/lib/types/ConversationStats.ts +13 -13
  303. package/src/ruvocal/src/lib/types/Message.ts +41 -41
  304. package/src/ruvocal/src/lib/types/MessageEvent.ts +10 -10
  305. package/src/ruvocal/src/lib/types/MessageUpdate.ts +139 -139
  306. package/src/ruvocal/src/lib/types/MigrationResult.ts +7 -7
  307. package/src/ruvocal/src/lib/types/Model.ts +23 -23
  308. package/src/ruvocal/src/lib/types/Report.ts +12 -12
  309. package/src/ruvocal/src/lib/types/Review.ts +6 -6
  310. package/src/ruvocal/src/lib/types/Semaphore.ts +19 -19
  311. package/src/ruvocal/src/lib/types/Session.ts +22 -22
  312. package/src/ruvocal/src/lib/types/Settings.ts +93 -93
  313. package/src/ruvocal/src/lib/types/SharedConversation.ts +9 -9
  314. package/src/ruvocal/src/lib/types/Template.ts +6 -6
  315. package/src/ruvocal/src/lib/types/Timestamps.ts +4 -4
  316. package/src/ruvocal/src/lib/types/TokenCache.ts +6 -6
  317. package/src/ruvocal/src/lib/types/Tool.ts +77 -77
  318. package/src/ruvocal/src/lib/types/UrlDependency.ts +5 -5
  319. package/src/ruvocal/src/lib/types/User.ts +14 -14
  320. package/src/ruvocal/src/lib/utils/PublicConfig.svelte.ts +75 -75
  321. package/src/ruvocal/src/lib/utils/auth.ts +17 -17
  322. package/src/ruvocal/src/lib/utils/chunk.ts +33 -33
  323. package/src/ruvocal/src/lib/utils/cookiesAreEnabled.ts +13 -13
  324. package/src/ruvocal/src/lib/utils/debounce.ts +17 -17
  325. package/src/ruvocal/src/lib/utils/deepestChild.ts +6 -6
  326. package/src/ruvocal/src/lib/utils/favicon.ts +21 -21
  327. package/src/ruvocal/src/lib/utils/fetchJSON.ts +23 -23
  328. package/src/ruvocal/src/lib/utils/file2base64.ts +14 -14
  329. package/src/ruvocal/src/lib/utils/formatUserCount.ts +37 -37
  330. package/src/ruvocal/src/lib/utils/generationState.spec.ts +75 -75
  331. package/src/ruvocal/src/lib/utils/generationState.ts +26 -26
  332. package/src/ruvocal/src/lib/utils/getHref.ts +41 -41
  333. package/src/ruvocal/src/lib/utils/getReturnFromGenerator.ts +7 -7
  334. package/src/ruvocal/src/lib/utils/haptics.ts +64 -64
  335. package/src/ruvocal/src/lib/utils/hashConv.ts +12 -12
  336. package/src/ruvocal/src/lib/utils/hf.ts +17 -17
  337. package/src/ruvocal/src/lib/utils/isDesktop.ts +7 -7
  338. package/src/ruvocal/src/lib/utils/isUrl.ts +8 -8
  339. package/src/ruvocal/src/lib/utils/isVirtualKeyboard.ts +16 -16
  340. package/src/ruvocal/src/lib/utils/loadAttachmentsFromUrls.ts +115 -115
  341. package/src/ruvocal/src/lib/utils/marked.spec.ts +96 -96
  342. package/src/ruvocal/src/lib/utils/marked.ts +531 -531
  343. package/src/ruvocal/src/lib/utils/mcpValidation.ts +147 -147
  344. package/src/ruvocal/src/lib/utils/mergeAsyncGenerators.ts +38 -38
  345. package/src/ruvocal/src/lib/utils/messageUpdates.spec.ts +262 -262
  346. package/src/ruvocal/src/lib/utils/messageUpdates.ts +324 -324
  347. package/src/ruvocal/src/lib/utils/mime.ts +56 -56
  348. package/src/ruvocal/src/lib/utils/models.ts +14 -14
  349. package/src/ruvocal/src/lib/utils/parseBlocks.ts +120 -120
  350. package/src/ruvocal/src/lib/utils/parseIncompleteMarkdown.ts +644 -644
  351. package/src/ruvocal/src/lib/utils/parseStringToList.ts +10 -10
  352. package/src/ruvocal/src/lib/utils/randomUuid.ts +14 -14
  353. package/src/ruvocal/src/lib/utils/searchTokens.ts +33 -33
  354. package/src/ruvocal/src/lib/utils/sha256.ts +7 -7
  355. package/src/ruvocal/src/lib/utils/stringifyError.ts +12 -12
  356. package/src/ruvocal/src/lib/utils/sum.ts +3 -3
  357. package/src/ruvocal/src/lib/utils/template.spec.ts +59 -59
  358. package/src/ruvocal/src/lib/utils/template.ts +53 -53
  359. package/src/ruvocal/src/lib/utils/timeout.ts +9 -9
  360. package/src/ruvocal/src/lib/utils/toolProgress.spec.ts +46 -46
  361. package/src/ruvocal/src/lib/utils/toolProgress.ts +11 -11
  362. package/src/ruvocal/src/lib/utils/tree/addChildren.spec.ts +102 -102
  363. package/src/ruvocal/src/lib/utils/tree/addChildren.ts +48 -48
  364. package/src/ruvocal/src/lib/utils/tree/addSibling.spec.ts +81 -81
  365. package/src/ruvocal/src/lib/utils/tree/addSibling.ts +41 -41
  366. package/src/ruvocal/src/lib/utils/tree/buildSubtree.spec.ts +110 -110
  367. package/src/ruvocal/src/lib/utils/tree/buildSubtree.ts +24 -24
  368. package/src/ruvocal/src/lib/utils/tree/convertLegacyConversation.spec.ts +31 -31
  369. package/src/ruvocal/src/lib/utils/tree/convertLegacyConversation.ts +36 -36
  370. package/src/ruvocal/src/lib/utils/tree/isMessageId.spec.ts +15 -15
  371. package/src/ruvocal/src/lib/utils/tree/isMessageId.ts +5 -5
  372. package/src/ruvocal/src/lib/utils/tree/tree.d.ts +14 -14
  373. package/src/ruvocal/src/lib/utils/tree/treeHelpers.spec.ts +167 -167
  374. package/src/ruvocal/src/lib/utils/updates.ts +39 -39
  375. package/src/ruvocal/src/lib/utils/urlParams.ts +13 -13
  376. package/src/ruvocal/src/lib/wasm/idb.ts +438 -438
  377. package/src/ruvocal/src/lib/wasm/index.ts +1213 -1213
  378. package/src/ruvocal/src/lib/wasm/tests/wasm-capabilities.test.ts +565 -565
  379. package/src/ruvocal/src/lib/wasm/wasm.worker.ts +332 -332
  380. package/src/ruvocal/src/lib/wasm/workerClient.ts +166 -166
  381. package/src/ruvocal/src/lib/workers/autopilotWorker.ts +221 -221
  382. package/src/ruvocal/src/lib/workers/detailFetchWorker.ts +100 -100
  383. package/src/ruvocal/src/lib/workers/markdownWorker.ts +61 -61
  384. package/src/ruvocal/src/routes/+error.svelte +20 -20
  385. package/src/ruvocal/src/routes/+layout.svelte +324 -324
  386. package/src/ruvocal/src/routes/+layout.ts +91 -91
  387. package/src/ruvocal/src/routes/+page.svelte +168 -168
  388. package/src/ruvocal/src/routes/.well-known/oauth-cimd/+server.ts +37 -37
  389. package/src/ruvocal/src/routes/__debug/openai/+server.ts +21 -21
  390. package/src/ruvocal/src/routes/admin/export/+server.ts +159 -159
  391. package/src/ruvocal/src/routes/admin/stats/compute/+server.ts +16 -16
  392. package/src/ruvocal/src/routes/api/conversation/[id]/+server.ts +40 -40
  393. package/src/ruvocal/src/routes/api/conversation/[id]/message/[messageId]/+server.ts +42 -42
  394. package/src/ruvocal/src/routes/api/conversations/+server.ts +48 -48
  395. package/src/ruvocal/src/routes/api/fetch-url/+server.ts +147 -147
  396. package/src/ruvocal/src/routes/api/mcp/health/+server.ts +292 -292
  397. package/src/ruvocal/src/routes/api/mcp/servers/+server.ts +32 -32
  398. package/src/ruvocal/src/routes/api/models/+server.ts +25 -25
  399. package/src/ruvocal/src/routes/api/transcribe/+server.ts +104 -104
  400. package/src/ruvocal/src/routes/api/user/+server.ts +15 -15
  401. package/src/ruvocal/src/routes/api/user/validate-token/+server.ts +20 -20
  402. package/src/ruvocal/src/routes/api/v2/conversations/+server.ts +48 -48
  403. package/src/ruvocal/src/routes/api/v2/conversations/[id]/+server.ts +94 -94
  404. package/src/ruvocal/src/routes/api/v2/conversations/[id]/message/[messageId]/+server.ts +43 -43
  405. package/src/ruvocal/src/routes/api/v2/conversations/import-share/+server.ts +23 -23
  406. package/src/ruvocal/src/routes/api/v2/debug/config/+server.ts +16 -16
  407. package/src/ruvocal/src/routes/api/v2/debug/refresh/+server.ts +30 -30
  408. package/src/ruvocal/src/routes/api/v2/export/+server.ts +196 -196
  409. package/src/ruvocal/src/routes/api/v2/feature-flags/+server.ts +14 -14
  410. package/src/ruvocal/src/routes/api/v2/models/+server.ts +38 -38
  411. package/src/ruvocal/src/routes/api/v2/models/[namespace]/+server.ts +8 -8
  412. package/src/ruvocal/src/routes/api/v2/models/[namespace]/[model]/+server.ts +8 -8
  413. package/src/ruvocal/src/routes/api/v2/models/[namespace]/[model]/subscribe/+server.ts +28 -28
  414. package/src/ruvocal/src/routes/api/v2/models/[namespace]/subscribe/+server.ts +28 -28
  415. package/src/ruvocal/src/routes/api/v2/models/old/+server.ts +7 -7
  416. package/src/ruvocal/src/routes/api/v2/models/refresh/+server.ts +33 -33
  417. package/src/ruvocal/src/routes/api/v2/public-config/+server.ts +7 -7
  418. package/src/ruvocal/src/routes/api/v2/user/+server.ts +17 -17
  419. package/src/ruvocal/src/routes/api/v2/user/billing-orgs/+server.ts +73 -73
  420. package/src/ruvocal/src/routes/api/v2/user/reports/+server.ts +17 -17
  421. package/src/ruvocal/src/routes/api/v2/user/settings/+server.ts +110 -110
  422. package/src/ruvocal/src/routes/conversation/+server.ts +115 -115
  423. package/src/ruvocal/src/routes/conversation/[id]/+page.svelte +586 -586
  424. package/src/ruvocal/src/routes/conversation/[id]/+page.ts +60 -60
  425. package/src/ruvocal/src/routes/conversation/[id]/+server.ts +740 -740
  426. package/src/ruvocal/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts +66 -66
  427. package/src/ruvocal/src/routes/conversation/[id]/share/+server.ts +69 -69
  428. package/src/ruvocal/src/routes/conversation/[id]/stop-generating/+server.ts +35 -35
  429. package/src/ruvocal/src/routes/healthcheck/+server.ts +3 -3
  430. package/src/ruvocal/src/routes/login/+server.ts +5 -5
  431. package/src/ruvocal/src/routes/login/callback/+server.ts +103 -103
  432. package/src/ruvocal/src/routes/login/callback/updateUser.spec.ts +157 -157
  433. package/src/ruvocal/src/routes/login/callback/updateUser.ts +215 -215
  434. package/src/ruvocal/src/routes/logout/+server.ts +18 -18
  435. package/src/ruvocal/src/routes/metrics/+server.ts +18 -18
  436. package/src/ruvocal/src/routes/models/+page.svelte +233 -233
  437. package/src/ruvocal/src/routes/models/[...model]/+page.svelte +161 -161
  438. package/src/ruvocal/src/routes/models/[...model]/+page.ts +14 -14
  439. package/src/ruvocal/src/routes/models/[...model]/thumbnail.png/+server.ts +64 -64
  440. package/src/ruvocal/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte +28 -28
  441. package/src/ruvocal/src/routes/privacy/+page.svelte +11 -11
  442. package/src/ruvocal/src/routes/r/[id]/+page.ts +34 -34
  443. package/src/ruvocal/src/routes/settings/(nav)/+layout.svelte +282 -282
  444. package/src/ruvocal/src/routes/settings/(nav)/+layout.ts +1 -1
  445. package/src/ruvocal/src/routes/settings/(nav)/+server.ts +59 -59
  446. package/src/ruvocal/src/routes/settings/(nav)/[...model]/+page.svelte +464 -464
  447. package/src/ruvocal/src/routes/settings/(nav)/[...model]/+page.ts +14 -14
  448. package/src/ruvocal/src/routes/settings/(nav)/application/+page.svelte +362 -362
  449. package/src/ruvocal/src/routes/settings/+layout.svelte +40 -40
  450. package/src/ruvocal/src/styles/highlight-js.css +195 -195
  451. package/src/ruvocal/src/styles/main.css +144 -144
  452. package/src/ruvocal/static/chatui/favicon-dark.svg +3 -3
  453. package/src/ruvocal/static/chatui/favicon-dev.svg +3 -3
  454. package/src/ruvocal/static/chatui/favicon.svg +3 -3
  455. package/src/ruvocal/static/chatui/icon.svg +3 -3
  456. package/src/ruvocal/static/chatui/logo.svg +7 -7
  457. package/src/ruvocal/static/chatui/manifest.json +54 -54
  458. package/src/ruvocal/static/chatui/welcome.js +184 -184
  459. package/src/ruvocal/static/huggingchat/favicon-dark.svg +4 -4
  460. package/src/ruvocal/static/huggingchat/favicon-dev.svg +4 -4
  461. package/src/ruvocal/static/huggingchat/favicon.svg +4 -4
  462. package/src/ruvocal/static/huggingchat/fulltext-logo.svg +1 -1
  463. package/src/ruvocal/static/huggingchat/icon.svg +4 -4
  464. package/src/ruvocal/static/huggingchat/logo.svg +4 -4
  465. package/src/ruvocal/static/huggingchat/manifest.json +54 -54
  466. package/src/ruvocal/static/huggingchat/routes.chat.json +226 -226
  467. package/src/ruvocal/static/robots.txt +10 -10
  468. package/src/ruvocal/static/wasm/rvagent_wasm.js +1539 -1539
  469. package/src/ruvocal/stub/@reflink/reflink/package.json +5 -5
  470. package/src/ruvocal/svelte.config.js +53 -53
  471. package/src/ruvocal/tailwind.config.cjs +30 -30
  472. package/src/ruvocal/tsconfig.json +19 -19
  473. package/src/ruvocal/vite.config.ts +87 -87
  474. package/src/scripts/deploy.sh +116 -116
  475. package/src/scripts/generate-config.js +245 -245
  476. package/src/scripts/generate-welcome.js +187 -187
  477. package/src/scripts/package-rvf.sh +116 -116
  478. package/src/ruvocal/.claude-flow/daemon-state.json +0 -135
  479. package/src/ruvocal/.claude-flow/data/pending-insights.jsonl +0 -0
  480. package/src/ruvocal/.claude-flow/data/ranked-context.json +0 -5
  481. package/src/ruvocal/.claude-flow/logs/daemon.log +0 -31
  482. package/src/ruvocal/.claude-flow/logs/headless/audit_1777949411822_juxau0_prompt.log +0 -989
  483. package/src/ruvocal/.claude-flow/logs/headless/audit_1777949411822_juxau0_result.log +0 -67
  484. package/src/ruvocal/.claude-flow/logs/headless/audit_1777950042278_jvj5xq_prompt.log +0 -989
  485. package/src/ruvocal/.claude-flow/logs/headless/audit_1777950042278_jvj5xq_result.log +0 -93
  486. package/src/ruvocal/.claude-flow/logs/headless/optimize_1777949531823_yt5yc2_prompt.log +0 -1498
  487. package/src/ruvocal/.claude-flow/logs/headless/optimize_1777949531823_yt5yc2_result.log +0 -93
  488. package/src/ruvocal/.claude-flow/logs/headless/testgaps_1777949771821_elw1j4_prompt.log +0 -1498
  489. package/src/ruvocal/.claude-flow/logs/headless/testgaps_1777949771821_elw1j4_result.log +0 -100
  490. package/src/ruvocal/.claude-flow/metrics/codebase-map.json +0 -11
  491. package/src/ruvocal/.claude-flow/metrics/consolidation.json +0 -6
  492. package/src/ruvocal/.claude-flow/neural/stats.json +0 -6
  493. package/src/ruvocal/.claude-flow/sessions/current.json +0 -13
  494. package/src/ruvocal/.swarm/attestation.db +0 -0
  495. package/src/ruvocal/.swarm/hnsw.index +0 -0
  496. package/src/ruvocal/.swarm/hnsw.metadata.json +0 -1
  497. package/src/ruvocal/.swarm/memory.db +0 -0
  498. package/src/ruvocal/.swarm/schema.sql +0 -305
@@ -1,700 +1,700 @@
1
- /**
2
- * PostgreSQL adapter for RuVocal — drop-in replacement for MongoDB collections.
3
- *
4
- * Implements the MongoDB Collection interface used by HF Chat UI,
5
- * translating find/insert/update/delete/aggregate calls to SQL.
6
- *
7
- * Uses the `pg` driver with connection pooling. ObjectId fields are
8
- * mapped to UUID. Messages remain embedded in conversations as JSONB
9
- * to minimise upstream diff.
10
- */
11
-
12
- import pg from "pg";
13
- import { randomUUID } from "crypto";
14
- import { logger } from "$lib/server/logger";
15
-
16
- const { Pool } = pg;
17
-
18
- let pool: pg.Pool | null = null;
19
-
20
- export function getPool(): pg.Pool {
21
- if (!pool) {
22
- const connectionString =
23
- process.env.DATABASE_URL ||
24
- "postgresql://ruvocal:ruvocal@localhost:5432/ruvocal";
25
- pool = new Pool({
26
- connectionString,
27
- max: 20,
28
- idleTimeoutMillis: 30_000,
29
- connectionTimeoutMillis: 5_000,
30
- });
31
- pool.on("error", (err) => logger.error(err, "Postgres pool error"));
32
- }
33
- return pool;
34
- }
35
-
36
- export async function closePool(): Promise<void> {
37
- if (pool) {
38
- await pool.end();
39
- pool = null;
40
- }
41
- }
42
-
43
- // ---------------------------------------------------------------------------
44
- // ObjectId compatibility
45
- // ---------------------------------------------------------------------------
46
-
47
- /**
48
- * Minimal ObjectId stand-in that wraps a UUID string.
49
- * MongoDB's ObjectId is a 24-hex-char string; we use UUID v4 instead.
50
- */
51
- export class ObjectId {
52
- private _id: string;
53
- constructor(id?: string) {
54
- this._id = id ?? randomUUID();
55
- }
56
- toString() {
57
- return this._id;
58
- }
59
- toHexString() {
60
- return this._id;
61
- }
62
- equals(other: ObjectId | string) {
63
- const otherStr = typeof other === "string" ? other : other.toString();
64
- return this._id === otherStr;
65
- }
66
- toJSON() {
67
- return this._id;
68
- }
69
- static createFromHexString(hex: string) {
70
- return new ObjectId(hex);
71
- }
72
- }
73
-
74
- // ---------------------------------------------------------------------------
75
- // MongoDB-compatible filter → SQL WHERE
76
- // ---------------------------------------------------------------------------
77
-
78
- interface FilterOp {
79
- text: string;
80
- values: unknown[];
81
- }
82
-
83
- function filterToWhere(
84
- filter: Record<string, unknown>,
85
- startIdx = 1
86
- ): FilterOp {
87
- const clauses: string[] = [];
88
- const values: unknown[] = [];
89
- let idx = startIdx;
90
-
91
- for (const [key, val] of Object.entries(filter)) {
92
- if (key === "$or" && Array.isArray(val)) {
93
- const orClauses: string[] = [];
94
- for (const sub of val) {
95
- const r = filterToWhere(sub as Record<string, unknown>, idx);
96
- orClauses.push(`(${r.text})`);
97
- values.push(...r.values);
98
- idx += r.values.length;
99
- }
100
- clauses.push(`(${orClauses.join(" OR ")})`);
101
- continue;
102
- }
103
-
104
- if (key === "$and" && Array.isArray(val)) {
105
- for (const sub of val) {
106
- const r = filterToWhere(sub as Record<string, unknown>, idx);
107
- clauses.push(`(${r.text})`);
108
- values.push(...r.values);
109
- idx += r.values.length;
110
- }
111
- continue;
112
- }
113
-
114
- // Nested dot notation → JSONB path
115
- const col = key.includes(".") ? jsonbPath(key) : `"${snakeCase(key)}"`;
116
-
117
- if (val === null || val === undefined) {
118
- clauses.push(`${col} IS NULL`);
119
- } else if (typeof val === "object" && !Array.isArray(val) && !(val instanceof ObjectId)) {
120
- const ops = val as Record<string, unknown>;
121
- for (const [op, opVal] of Object.entries(ops)) {
122
- switch (op) {
123
- case "$exists":
124
- clauses.push(
125
- opVal ? `${col} IS NOT NULL` : `${col} IS NULL`
126
- );
127
- break;
128
- case "$gt":
129
- clauses.push(`${col} > $${idx++}`);
130
- values.push(opVal);
131
- break;
132
- case "$gte":
133
- clauses.push(`${col} >= $${idx++}`);
134
- values.push(opVal);
135
- break;
136
- case "$lt":
137
- clauses.push(`${col} < $${idx++}`);
138
- values.push(opVal);
139
- break;
140
- case "$lte":
141
- clauses.push(`${col} <= $${idx++}`);
142
- values.push(opVal);
143
- break;
144
- case "$ne":
145
- clauses.push(`${col} != $${idx++}`);
146
- values.push(opVal);
147
- break;
148
- case "$in":
149
- clauses.push(`${col} = ANY($${idx++})`);
150
- values.push(opVal);
151
- break;
152
- case "$nin":
153
- clauses.push(`${col} != ALL($${idx++})`);
154
- values.push(opVal);
155
- break;
156
- case "$regex": {
157
- const flags =
158
- ops.$options === "i" ? "~*" : "~";
159
- clauses.push(`${col}::text ${flags} $${idx++}`);
160
- values.push(opVal);
161
- break;
162
- }
163
- default:
164
- logger.warn(`Unknown filter operator: ${op}`);
165
- }
166
- }
167
- } else {
168
- const v = val instanceof ObjectId ? val.toString() : val;
169
- clauses.push(`${col} = $${idx++}`);
170
- values.push(v);
171
- }
172
- }
173
-
174
- return {
175
- text: clauses.length > 0 ? clauses.join(" AND ") : "TRUE",
176
- values,
177
- };
178
- }
179
-
180
- function snakeCase(s: string): string {
181
- // Common MongoDB field → Postgres column mappings
182
- const map: Record<string, string> = {
183
- _id: "_id",
184
- sessionId: "session_id",
185
- userId: "user_id",
186
- hfUserId: "hf_user_id",
187
- createdAt: "created_at",
188
- updatedAt: "updated_at",
189
- deletedAt: "deleted_at",
190
- expiresAt: "expires_at",
191
- deleteAt: "delete_at",
192
- conversationId: "conversation_id",
193
- assistantId: "assistant_id",
194
- createdById: "created_by_id",
195
- createdByName: "created_by_name",
196
- modelId: "model_id",
197
- userCount: "user_count",
198
- useCount: "use_count",
199
- searchTokens: "search_tokens",
200
- last24HoursCount: "last24_hours_count",
201
- last24HoursUseCount: "last24_hours_use_count",
202
- rootMessageId: "root_message_id",
203
- tokenHash: "token_hash",
204
- avatarUrl: "avatar_url",
205
- isAdmin: "is_admin",
206
- isEarlyAccess: "is_early_access",
207
- contentId: "content_id",
208
- eventType: "event_type",
209
- messageId: "message_id",
210
- dateField: "date_field",
211
- dateSpan: "date_span",
212
- dateAt: "date_at",
213
- };
214
- return map[s] ?? s.replace(/([A-Z])/g, "_$1").toLowerCase();
215
- }
216
-
217
- function jsonbPath(dotPath: string): string {
218
- const parts = dotPath.split(".");
219
- const col = `"${snakeCase(parts[0])}"`;
220
- if (parts.length === 1) return col;
221
- // JSONB deep access: data->'messages'->>'from'
222
- const jsonParts = parts.slice(1);
223
- const last = jsonParts.pop()!;
224
- let expr = col;
225
- for (const p of jsonParts) {
226
- expr += `->'${p}'`;
227
- }
228
- expr += `->>'${last}'`;
229
- return expr;
230
- }
231
-
232
- // ---------------------------------------------------------------------------
233
- // MongoDB-compatible update → SQL SET
234
- // ---------------------------------------------------------------------------
235
-
236
- interface UpdateOp {
237
- setClauses: string[];
238
- values: unknown[];
239
- }
240
-
241
- function updateToSet(
242
- update: Record<string, unknown>,
243
- startIdx: number
244
- ): UpdateOp {
245
- const setClauses: string[] = [];
246
- const values: unknown[] = [];
247
- let idx = startIdx;
248
-
249
- const setFields =
250
- (update.$set as Record<string, unknown>) ?? update;
251
-
252
- // If update has no operators, treat the whole thing as $set
253
- const hasOperators = Object.keys(update).some((k) => k.startsWith("$"));
254
- const fields = hasOperators
255
- ? (update.$set as Record<string, unknown>) ?? {}
256
- : update;
257
-
258
- for (const [key, val] of Object.entries(fields)) {
259
- if (key === "_id") continue; // never update PK
260
- const col = snakeCase(key);
261
- const v = val instanceof ObjectId ? val.toString() : val;
262
- if (typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Date)) {
263
- setClauses.push(`"${col}" = $${idx++}::jsonb`);
264
- values.push(JSON.stringify(v));
265
- } else {
266
- setClauses.push(`"${col}" = $${idx++}`);
267
- values.push(v);
268
- }
269
- }
270
-
271
- // Handle $push (append to JSONB array)
272
- if (update.$push) {
273
- for (const [key, val] of Object.entries(
274
- update.$push as Record<string, unknown>
275
- )) {
276
- const col = snakeCase(key);
277
- if (typeof val === "object" && val !== null && "$each" in (val as Record<string, unknown>)) {
278
- const each = (val as Record<string, unknown>).$each as unknown[];
279
- setClauses.push(
280
- `"${col}" = "${col}" || $${idx++}::jsonb`
281
- );
282
- values.push(JSON.stringify(each));
283
- } else {
284
- setClauses.push(
285
- `"${col}" = COALESCE("${col}", '[]'::jsonb) || $${idx++}::jsonb`
286
- );
287
- values.push(JSON.stringify([val]));
288
- }
289
- }
290
- }
291
-
292
- // Handle $inc
293
- if (update.$inc) {
294
- for (const [key, val] of Object.entries(
295
- update.$inc as Record<string, number>
296
- )) {
297
- const col = snakeCase(key);
298
- setClauses.push(`"${col}" = COALESCE("${col}", 0) + $${idx++}`);
299
- values.push(val);
300
- }
301
- }
302
-
303
- // Handle $unset
304
- if (update.$unset) {
305
- for (const key of Object.keys(update.$unset as Record<string, unknown>)) {
306
- const col = snakeCase(key);
307
- setClauses.push(`"${col}" = NULL`);
308
- }
309
- }
310
-
311
- // Always update updated_at
312
- if (!setClauses.some((c) => c.includes('"updated_at"'))) {
313
- setClauses.push(`"updated_at" = NOW()`);
314
- }
315
-
316
- return { setClauses, values };
317
- }
318
-
319
- // ---------------------------------------------------------------------------
320
- // Sort/limit/skip helpers
321
- // ---------------------------------------------------------------------------
322
-
323
- function sortToOrderBy(sort: Record<string, 1 | -1>): string {
324
- const parts = Object.entries(sort).map(([key, dir]) => {
325
- const col = key.includes(".")
326
- ? jsonbPath(key)
327
- : `"${snakeCase(key)}"`;
328
- return `${col} ${dir === -1 ? "DESC" : "ASC"}`;
329
- });
330
- return parts.length > 0 ? `ORDER BY ${parts.join(", ")}` : "";
331
- }
332
-
333
- // ---------------------------------------------------------------------------
334
- // PostgresCollection — MongoDB Collection interface
335
- // ---------------------------------------------------------------------------
336
-
337
- export interface FindOptions {
338
- sort?: Record<string, 1 | -1>;
339
- limit?: number;
340
- skip?: number;
341
- projection?: Record<string, 0 | 1>;
342
- }
343
-
344
- export class PostgresCollection<T extends Record<string, unknown>> {
345
- constructor(public readonly tableName: string) {}
346
-
347
- private get pool() {
348
- return getPool();
349
- }
350
-
351
- // Convert Postgres row (snake_case) back to camelCase for app
352
- private rowToDoc(row: Record<string, unknown>): T {
353
- // For now, return as-is — the app code uses camelCase field names
354
- // but we store snake_case. We rely on column aliases or a transform.
355
- // Since HF Chat UI accesses fields via MongoDB collection refs,
356
- // we need the row to look like a MongoDB document.
357
- const doc: Record<string, unknown> = {};
358
- for (const [key, val] of Object.entries(row)) {
359
- doc[camelCase(key)] = val;
360
- }
361
- return doc as T;
362
- }
363
-
364
- async findOne(filter: Record<string, unknown> = {}): Promise<T | null> {
365
- const w = filterToWhere(filter);
366
- const sql = `SELECT * FROM "${this.tableName}" WHERE ${w.text} LIMIT 1`;
367
- const result = await this.pool.query(sql, w.values);
368
- return result.rows.length > 0 ? this.rowToDoc(result.rows[0]) : null;
369
- }
370
-
371
- find(
372
- filter: Record<string, unknown> = {},
373
- options: FindOptions = {}
374
- ): PostgresCursor<T> {
375
- return new PostgresCursor<T>(this, filter, options);
376
- }
377
-
378
- async insertOne(
379
- doc: Partial<T> & Record<string, unknown>
380
- ): Promise<{ insertedId: ObjectId; acknowledged: boolean }> {
381
- const id = doc._id
382
- ? typeof doc._id === "string"
383
- ? doc._id
384
- : (doc._id as ObjectId).toString()
385
- : randomUUID();
386
-
387
- const entries = Object.entries(doc).filter(([k]) => k !== "_id");
388
- const cols = ["_id", ...entries.map(([k]) => `"${snakeCase(k)}"`)];
389
- const placeholders = [
390
- "$1",
391
- ...entries.map((_, i) => `$${i + 2}`),
392
- ];
393
- const values: unknown[] = [
394
- id,
395
- ...entries.map(([, v]) => {
396
- if (v instanceof ObjectId) return v.toString();
397
- if (typeof v === "object" && v !== null && !(v instanceof Date) && !Array.isArray(v))
398
- return JSON.stringify(v);
399
- if (Array.isArray(v)) return JSON.stringify(v);
400
- return v;
401
- }),
402
- ];
403
-
404
- const sql = `INSERT INTO "${this.tableName}" (${cols.join(", ")}) VALUES (${placeholders.join(", ")}) ON CONFLICT DO NOTHING RETURNING _id`;
405
- await this.pool.query(sql, values);
406
- return { insertedId: new ObjectId(id), acknowledged: true };
407
- }
408
-
409
- async insertMany(
410
- docs: Array<Partial<T> & Record<string, unknown>>
411
- ): Promise<{ insertedIds: ObjectId[]; acknowledged: boolean }> {
412
- const ids: ObjectId[] = [];
413
- for (const doc of docs) {
414
- const result = await this.insertOne(doc);
415
- ids.push(result.insertedId);
416
- }
417
- return { insertedIds: ids, acknowledged: true };
418
- }
419
-
420
- async updateOne(
421
- filter: Record<string, unknown>,
422
- update: Record<string, unknown>
423
- ): Promise<{ matchedCount: number; modifiedCount: number; acknowledged: boolean }> {
424
- const w = filterToWhere(filter);
425
- const u = updateToSet(update, w.values.length + 1);
426
- if (u.setClauses.length === 0) {
427
- return { matchedCount: 0, modifiedCount: 0, acknowledged: true };
428
- }
429
- const sql = `UPDATE "${this.tableName}" SET ${u.setClauses.join(", ")} WHERE ${w.text}`;
430
- const result = await this.pool.query(sql, [...w.values, ...u.values]);
431
- const count = result.rowCount ?? 0;
432
- return { matchedCount: count, modifiedCount: count, acknowledged: true };
433
- }
434
-
435
- async updateMany(
436
- filter: Record<string, unknown>,
437
- update: Record<string, unknown>
438
- ): Promise<{ matchedCount: number; modifiedCount: number; acknowledged: boolean }> {
439
- return this.updateOne(filter, update); // same SQL, no LIMIT 1
440
- }
441
-
442
- async deleteOne(
443
- filter: Record<string, unknown>
444
- ): Promise<{ deletedCount: number; acknowledged: boolean }> {
445
- const w = filterToWhere(filter);
446
- const sql = `DELETE FROM "${this.tableName}" WHERE ${w.text}`;
447
- const result = await this.pool.query(sql, w.values);
448
- return { deletedCount: result.rowCount ?? 0, acknowledged: true };
449
- }
450
-
451
- async deleteMany(
452
- filter: Record<string, unknown>
453
- ): Promise<{ deletedCount: number; acknowledged: boolean }> {
454
- return this.deleteOne(filter);
455
- }
456
-
457
- async countDocuments(
458
- filter: Record<string, unknown> = {}
459
- ): Promise<number> {
460
- const w = filterToWhere(filter);
461
- const sql = `SELECT COUNT(*)::int AS count FROM "${this.tableName}" WHERE ${w.text}`;
462
- const result = await this.pool.query(sql, w.values);
463
- return result.rows[0]?.count ?? 0;
464
- }
465
-
466
- async distinct(
467
- field: string,
468
- filter: Record<string, unknown> = {}
469
- ): Promise<unknown[]> {
470
- const col = `"${snakeCase(field)}"`;
471
- const w = filterToWhere(filter);
472
- const sql = `SELECT DISTINCT ${col} FROM "${this.tableName}" WHERE ${w.text}`;
473
- const result = await this.pool.query(sql, w.values);
474
- return result.rows.map((r) => r[snakeCase(field)]);
475
- }
476
-
477
- async aggregate(pipeline: Record<string, unknown>[]): Promise<T[]> {
478
- // Basic aggregation support — handle common patterns
479
- // For complex pipelines, we'd need a full translator.
480
- // For now, log a warning and return empty.
481
- logger.warn(
482
- { pipeline, table: this.tableName },
483
- "aggregate() called — basic translation only"
484
- );
485
- return [];
486
- }
487
-
488
- async createIndex(
489
- _spec: Record<string, unknown>,
490
- _options?: Record<string, unknown>
491
- ): Promise<void> {
492
- // Indexes are pre-created in the migration. This is a no-op.
493
- }
494
-
495
- async findOneAndUpdate(
496
- filter: Record<string, unknown>,
497
- update: Record<string, unknown>,
498
- options?: { upsert?: boolean; returnDocument?: "before" | "after" }
499
- ): Promise<{ value: T | null }> {
500
- if (options?.upsert) {
501
- const existing = await this.findOne(filter);
502
- if (!existing) {
503
- const doc = { ...filter, ...((update.$set as Record<string, unknown>) ?? update) };
504
- await this.insertOne(doc as Partial<T> & Record<string, unknown>);
505
- const inserted = await this.findOne(filter);
506
- return { value: inserted };
507
- }
508
- }
509
- await this.updateOne(filter, update);
510
- const updated = await this.findOne(filter);
511
- return { value: updated };
512
- }
513
-
514
- async findOneAndDelete(
515
- filter: Record<string, unknown>
516
- ): Promise<{ value: T | null }> {
517
- const doc = await this.findOne(filter);
518
- if (doc) await this.deleteOne(filter);
519
- return { value: doc };
520
- }
521
-
522
- // RuVector extension: semantic search via pgvector
523
- async semanticSearch(
524
- queryEmbedding: number[],
525
- limit = 10,
526
- filter: Record<string, unknown> = {}
527
- ): Promise<Array<T & { similarity: number }>> {
528
- const w = filterToWhere(filter);
529
- const embIdx = w.values.length + 1;
530
- const limIdx = embIdx + 1;
531
- const sql = `
532
- SELECT *, 1 - (embedding <=> $${embIdx}::vector) AS similarity
533
- FROM "${this.tableName}"
534
- WHERE ${w.text} AND embedding IS NOT NULL
535
- ORDER BY embedding <=> $${embIdx}::vector
536
- LIMIT $${limIdx}
537
- `;
538
- const result = await this.pool.query(sql, [
539
- ...w.values,
540
- `[${queryEmbedding.join(",")}]`,
541
- limit,
542
- ]);
543
- return result.rows.map((r) => ({ ...this.rowToDoc(r), similarity: r.similarity }));
544
- }
545
- }
546
-
547
- // ---------------------------------------------------------------------------
548
- // Cursor — implements MongoDB-like chaining (sort/limit/skip/toArray)
549
- // ---------------------------------------------------------------------------
550
-
551
- export class PostgresCursor<T extends Record<string, unknown>> {
552
- private _sort: Record<string, 1 | -1> = {};
553
- private _limit?: number;
554
- private _skip?: number;
555
- private _projection?: Record<string, 0 | 1>;
556
-
557
- constructor(
558
- private collection: PostgresCollection<T>,
559
- private filter: Record<string, unknown>,
560
- options: FindOptions = {}
561
- ) {
562
- if (options.sort) this._sort = options.sort;
563
- if (options.limit) this._limit = options.limit;
564
- if (options.skip) this._skip = options.skip;
565
- if (options.projection) this._projection = options.projection;
566
- }
567
-
568
- sort(spec: Record<string, 1 | -1>): this {
569
- this._sort = { ...this._sort, ...spec };
570
- return this;
571
- }
572
-
573
- limit(n: number): this {
574
- this._limit = n;
575
- return this;
576
- }
577
-
578
- skip(n: number): this {
579
- this._skip = n;
580
- return this;
581
- }
582
-
583
- project(spec: Record<string, 0 | 1>): this {
584
- this._projection = spec;
585
- return this;
586
- }
587
-
588
- async toArray(): Promise<T[]> {
589
- const w = filterToWhere(this.filter);
590
- const order = sortToOrderBy(this._sort);
591
- let sql = `SELECT * FROM "${this.collection.tableName}" WHERE ${w.text} ${order}`;
592
- const values = [...w.values];
593
- if (this._limit !== undefined) {
594
- sql += ` LIMIT $${values.length + 1}`;
595
- values.push(this._limit);
596
- }
597
- if (this._skip !== undefined) {
598
- sql += ` OFFSET $${values.length + 1}`;
599
- values.push(this._skip);
600
- }
601
- const pool = getPool();
602
- const result = await pool.query(sql, values);
603
- return result.rows.map((row) => {
604
- const doc: Record<string, unknown> = {};
605
- for (const [key, val] of Object.entries(row)) {
606
- doc[camelCase(key)] = val;
607
- }
608
- return doc as T;
609
- });
610
- }
611
-
612
- // Async iterable support
613
- async *[Symbol.asyncIterator](): AsyncGenerator<T> {
614
- const rows = await this.toArray();
615
- for (const row of rows) {
616
- yield row;
617
- }
618
- }
619
- }
620
-
621
- // ---------------------------------------------------------------------------
622
- // GridFS replacement — stores files as BYTEA in a `files` table
623
- // ---------------------------------------------------------------------------
624
-
625
- export class PostgresGridFSBucket {
626
- private readonly tableName = "files";
627
-
628
- async openUploadStream(
629
- filename: string,
630
- options?: { metadata?: Record<string, unknown>; contentType?: string }
631
- ) {
632
- const id = randomUUID();
633
- const chunks: Buffer[] = [];
634
-
635
- return {
636
- id: new ObjectId(id),
637
- write(chunk: Buffer) {
638
- chunks.push(chunk);
639
- },
640
- async end() {
641
- const data = Buffer.concat(chunks);
642
- const pool = getPool();
643
- await pool.query(
644
- `INSERT INTO files (_id, filename, content_type, length, data, metadata) VALUES ($1, $2, $3, $4, $5, $6)`,
645
- [
646
- id,
647
- filename,
648
- options?.contentType ?? "application/octet-stream",
649
- data.length,
650
- data,
651
- JSON.stringify(options?.metadata ?? {}),
652
- ]
653
- );
654
- },
655
- };
656
- }
657
-
658
- openDownloadStream(id: ObjectId | string) {
659
- const fileId = typeof id === "string" ? id : id.toString();
660
- // Return a readable-like object
661
- return {
662
- async toArray(): Promise<Buffer[]> {
663
- const pool = getPool();
664
- const result = await pool.query(
665
- `SELECT data FROM files WHERE _id = $1`,
666
- [fileId]
667
- );
668
- if (result.rows.length === 0) throw new Error("File not found");
669
- return [result.rows[0].data];
670
- },
671
- };
672
- }
673
-
674
- async delete(id: ObjectId | string) {
675
- const fileId = typeof id === "string" ? id : id.toString();
676
- const pool = getPool();
677
- await pool.query(`DELETE FROM files WHERE _id = $1`, [fileId]);
678
- }
679
-
680
- async find(filter: Record<string, unknown> = {}) {
681
- const w = filterToWhere(filter);
682
- const pool = getPool();
683
- const result = await pool.query(
684
- `SELECT _id, filename, content_type, length, metadata, created_at FROM files WHERE ${w.text}`,
685
- w.values
686
- );
687
- return {
688
- toArray: async () => result.rows,
689
- };
690
- }
691
- }
692
-
693
- // ---------------------------------------------------------------------------
694
- // Helpers
695
- // ---------------------------------------------------------------------------
696
-
697
- function camelCase(s: string): string {
698
- if (s === "_id") return "_id";
699
- return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
700
- }
1
+ /**
2
+ * PostgreSQL adapter for RuVocal — drop-in replacement for MongoDB collections.
3
+ *
4
+ * Implements the MongoDB Collection interface used by HF Chat UI,
5
+ * translating find/insert/update/delete/aggregate calls to SQL.
6
+ *
7
+ * Uses the `pg` driver with connection pooling. ObjectId fields are
8
+ * mapped to UUID. Messages remain embedded in conversations as JSONB
9
+ * to minimise upstream diff.
10
+ */
11
+
12
+ import pg from "pg";
13
+ import { randomUUID } from "crypto";
14
+ import { logger } from "$lib/server/logger";
15
+
16
+ const { Pool } = pg;
17
+
18
+ let pool: pg.Pool | null = null;
19
+
20
+ export function getPool(): pg.Pool {
21
+ if (!pool) {
22
+ const connectionString =
23
+ process.env.DATABASE_URL ||
24
+ "postgresql://ruvocal:ruvocal@localhost:5432/ruvocal";
25
+ pool = new Pool({
26
+ connectionString,
27
+ max: 20,
28
+ idleTimeoutMillis: 30_000,
29
+ connectionTimeoutMillis: 5_000,
30
+ });
31
+ pool.on("error", (err) => logger.error(err, "Postgres pool error"));
32
+ }
33
+ return pool;
34
+ }
35
+
36
+ export async function closePool(): Promise<void> {
37
+ if (pool) {
38
+ await pool.end();
39
+ pool = null;
40
+ }
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // ObjectId compatibility
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /**
48
+ * Minimal ObjectId stand-in that wraps a UUID string.
49
+ * MongoDB's ObjectId is a 24-hex-char string; we use UUID v4 instead.
50
+ */
51
+ export class ObjectId {
52
+ private _id: string;
53
+ constructor(id?: string) {
54
+ this._id = id ?? randomUUID();
55
+ }
56
+ toString() {
57
+ return this._id;
58
+ }
59
+ toHexString() {
60
+ return this._id;
61
+ }
62
+ equals(other: ObjectId | string) {
63
+ const otherStr = typeof other === "string" ? other : other.toString();
64
+ return this._id === otherStr;
65
+ }
66
+ toJSON() {
67
+ return this._id;
68
+ }
69
+ static createFromHexString(hex: string) {
70
+ return new ObjectId(hex);
71
+ }
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // MongoDB-compatible filter → SQL WHERE
76
+ // ---------------------------------------------------------------------------
77
+
78
+ interface FilterOp {
79
+ text: string;
80
+ values: unknown[];
81
+ }
82
+
83
+ function filterToWhere(
84
+ filter: Record<string, unknown>,
85
+ startIdx = 1
86
+ ): FilterOp {
87
+ const clauses: string[] = [];
88
+ const values: unknown[] = [];
89
+ let idx = startIdx;
90
+
91
+ for (const [key, val] of Object.entries(filter)) {
92
+ if (key === "$or" && Array.isArray(val)) {
93
+ const orClauses: string[] = [];
94
+ for (const sub of val) {
95
+ const r = filterToWhere(sub as Record<string, unknown>, idx);
96
+ orClauses.push(`(${r.text})`);
97
+ values.push(...r.values);
98
+ idx += r.values.length;
99
+ }
100
+ clauses.push(`(${orClauses.join(" OR ")})`);
101
+ continue;
102
+ }
103
+
104
+ if (key === "$and" && Array.isArray(val)) {
105
+ for (const sub of val) {
106
+ const r = filterToWhere(sub as Record<string, unknown>, idx);
107
+ clauses.push(`(${r.text})`);
108
+ values.push(...r.values);
109
+ idx += r.values.length;
110
+ }
111
+ continue;
112
+ }
113
+
114
+ // Nested dot notation → JSONB path
115
+ const col = key.includes(".") ? jsonbPath(key) : `"${snakeCase(key)}"`;
116
+
117
+ if (val === null || val === undefined) {
118
+ clauses.push(`${col} IS NULL`);
119
+ } else if (typeof val === "object" && !Array.isArray(val) && !(val instanceof ObjectId)) {
120
+ const ops = val as Record<string, unknown>;
121
+ for (const [op, opVal] of Object.entries(ops)) {
122
+ switch (op) {
123
+ case "$exists":
124
+ clauses.push(
125
+ opVal ? `${col} IS NOT NULL` : `${col} IS NULL`
126
+ );
127
+ break;
128
+ case "$gt":
129
+ clauses.push(`${col} > $${idx++}`);
130
+ values.push(opVal);
131
+ break;
132
+ case "$gte":
133
+ clauses.push(`${col} >= $${idx++}`);
134
+ values.push(opVal);
135
+ break;
136
+ case "$lt":
137
+ clauses.push(`${col} < $${idx++}`);
138
+ values.push(opVal);
139
+ break;
140
+ case "$lte":
141
+ clauses.push(`${col} <= $${idx++}`);
142
+ values.push(opVal);
143
+ break;
144
+ case "$ne":
145
+ clauses.push(`${col} != $${idx++}`);
146
+ values.push(opVal);
147
+ break;
148
+ case "$in":
149
+ clauses.push(`${col} = ANY($${idx++})`);
150
+ values.push(opVal);
151
+ break;
152
+ case "$nin":
153
+ clauses.push(`${col} != ALL($${idx++})`);
154
+ values.push(opVal);
155
+ break;
156
+ case "$regex": {
157
+ const flags =
158
+ ops.$options === "i" ? "~*" : "~";
159
+ clauses.push(`${col}::text ${flags} $${idx++}`);
160
+ values.push(opVal);
161
+ break;
162
+ }
163
+ default:
164
+ logger.warn(`Unknown filter operator: ${op}`);
165
+ }
166
+ }
167
+ } else {
168
+ const v = val instanceof ObjectId ? val.toString() : val;
169
+ clauses.push(`${col} = $${idx++}`);
170
+ values.push(v);
171
+ }
172
+ }
173
+
174
+ return {
175
+ text: clauses.length > 0 ? clauses.join(" AND ") : "TRUE",
176
+ values,
177
+ };
178
+ }
179
+
180
+ function snakeCase(s: string): string {
181
+ // Common MongoDB field → Postgres column mappings
182
+ const map: Record<string, string> = {
183
+ _id: "_id",
184
+ sessionId: "session_id",
185
+ userId: "user_id",
186
+ hfUserId: "hf_user_id",
187
+ createdAt: "created_at",
188
+ updatedAt: "updated_at",
189
+ deletedAt: "deleted_at",
190
+ expiresAt: "expires_at",
191
+ deleteAt: "delete_at",
192
+ conversationId: "conversation_id",
193
+ assistantId: "assistant_id",
194
+ createdById: "created_by_id",
195
+ createdByName: "created_by_name",
196
+ modelId: "model_id",
197
+ userCount: "user_count",
198
+ useCount: "use_count",
199
+ searchTokens: "search_tokens",
200
+ last24HoursCount: "last24_hours_count",
201
+ last24HoursUseCount: "last24_hours_use_count",
202
+ rootMessageId: "root_message_id",
203
+ tokenHash: "token_hash",
204
+ avatarUrl: "avatar_url",
205
+ isAdmin: "is_admin",
206
+ isEarlyAccess: "is_early_access",
207
+ contentId: "content_id",
208
+ eventType: "event_type",
209
+ messageId: "message_id",
210
+ dateField: "date_field",
211
+ dateSpan: "date_span",
212
+ dateAt: "date_at",
213
+ };
214
+ return map[s] ?? s.replace(/([A-Z])/g, "_$1").toLowerCase();
215
+ }
216
+
217
+ function jsonbPath(dotPath: string): string {
218
+ const parts = dotPath.split(".");
219
+ const col = `"${snakeCase(parts[0])}"`;
220
+ if (parts.length === 1) return col;
221
+ // JSONB deep access: data->'messages'->>'from'
222
+ const jsonParts = parts.slice(1);
223
+ const last = jsonParts.pop()!;
224
+ let expr = col;
225
+ for (const p of jsonParts) {
226
+ expr += `->'${p}'`;
227
+ }
228
+ expr += `->>'${last}'`;
229
+ return expr;
230
+ }
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // MongoDB-compatible update → SQL SET
234
+ // ---------------------------------------------------------------------------
235
+
236
+ interface UpdateOp {
237
+ setClauses: string[];
238
+ values: unknown[];
239
+ }
240
+
241
+ function updateToSet(
242
+ update: Record<string, unknown>,
243
+ startIdx: number
244
+ ): UpdateOp {
245
+ const setClauses: string[] = [];
246
+ const values: unknown[] = [];
247
+ let idx = startIdx;
248
+
249
+ const setFields =
250
+ (update.$set as Record<string, unknown>) ?? update;
251
+
252
+ // If update has no operators, treat the whole thing as $set
253
+ const hasOperators = Object.keys(update).some((k) => k.startsWith("$"));
254
+ const fields = hasOperators
255
+ ? (update.$set as Record<string, unknown>) ?? {}
256
+ : update;
257
+
258
+ for (const [key, val] of Object.entries(fields)) {
259
+ if (key === "_id") continue; // never update PK
260
+ const col = snakeCase(key);
261
+ const v = val instanceof ObjectId ? val.toString() : val;
262
+ if (typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Date)) {
263
+ setClauses.push(`"${col}" = $${idx++}::jsonb`);
264
+ values.push(JSON.stringify(v));
265
+ } else {
266
+ setClauses.push(`"${col}" = $${idx++}`);
267
+ values.push(v);
268
+ }
269
+ }
270
+
271
+ // Handle $push (append to JSONB array)
272
+ if (update.$push) {
273
+ for (const [key, val] of Object.entries(
274
+ update.$push as Record<string, unknown>
275
+ )) {
276
+ const col = snakeCase(key);
277
+ if (typeof val === "object" && val !== null && "$each" in (val as Record<string, unknown>)) {
278
+ const each = (val as Record<string, unknown>).$each as unknown[];
279
+ setClauses.push(
280
+ `"${col}" = "${col}" || $${idx++}::jsonb`
281
+ );
282
+ values.push(JSON.stringify(each));
283
+ } else {
284
+ setClauses.push(
285
+ `"${col}" = COALESCE("${col}", '[]'::jsonb) || $${idx++}::jsonb`
286
+ );
287
+ values.push(JSON.stringify([val]));
288
+ }
289
+ }
290
+ }
291
+
292
+ // Handle $inc
293
+ if (update.$inc) {
294
+ for (const [key, val] of Object.entries(
295
+ update.$inc as Record<string, number>
296
+ )) {
297
+ const col = snakeCase(key);
298
+ setClauses.push(`"${col}" = COALESCE("${col}", 0) + $${idx++}`);
299
+ values.push(val);
300
+ }
301
+ }
302
+
303
+ // Handle $unset
304
+ if (update.$unset) {
305
+ for (const key of Object.keys(update.$unset as Record<string, unknown>)) {
306
+ const col = snakeCase(key);
307
+ setClauses.push(`"${col}" = NULL`);
308
+ }
309
+ }
310
+
311
+ // Always update updated_at
312
+ if (!setClauses.some((c) => c.includes('"updated_at"'))) {
313
+ setClauses.push(`"updated_at" = NOW()`);
314
+ }
315
+
316
+ return { setClauses, values };
317
+ }
318
+
319
+ // ---------------------------------------------------------------------------
320
+ // Sort/limit/skip helpers
321
+ // ---------------------------------------------------------------------------
322
+
323
+ function sortToOrderBy(sort: Record<string, 1 | -1>): string {
324
+ const parts = Object.entries(sort).map(([key, dir]) => {
325
+ const col = key.includes(".")
326
+ ? jsonbPath(key)
327
+ : `"${snakeCase(key)}"`;
328
+ return `${col} ${dir === -1 ? "DESC" : "ASC"}`;
329
+ });
330
+ return parts.length > 0 ? `ORDER BY ${parts.join(", ")}` : "";
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // PostgresCollection — MongoDB Collection interface
335
+ // ---------------------------------------------------------------------------
336
+
337
+ export interface FindOptions {
338
+ sort?: Record<string, 1 | -1>;
339
+ limit?: number;
340
+ skip?: number;
341
+ projection?: Record<string, 0 | 1>;
342
+ }
343
+
344
+ export class PostgresCollection<T extends Record<string, unknown>> {
345
+ constructor(public readonly tableName: string) {}
346
+
347
+ private get pool() {
348
+ return getPool();
349
+ }
350
+
351
+ // Convert Postgres row (snake_case) back to camelCase for app
352
+ private rowToDoc(row: Record<string, unknown>): T {
353
+ // For now, return as-is — the app code uses camelCase field names
354
+ // but we store snake_case. We rely on column aliases or a transform.
355
+ // Since HF Chat UI accesses fields via MongoDB collection refs,
356
+ // we need the row to look like a MongoDB document.
357
+ const doc: Record<string, unknown> = {};
358
+ for (const [key, val] of Object.entries(row)) {
359
+ doc[camelCase(key)] = val;
360
+ }
361
+ return doc as T;
362
+ }
363
+
364
+ async findOne(filter: Record<string, unknown> = {}): Promise<T | null> {
365
+ const w = filterToWhere(filter);
366
+ const sql = `SELECT * FROM "${this.tableName}" WHERE ${w.text} LIMIT 1`;
367
+ const result = await this.pool.query(sql, w.values);
368
+ return result.rows.length > 0 ? this.rowToDoc(result.rows[0]) : null;
369
+ }
370
+
371
+ find(
372
+ filter: Record<string, unknown> = {},
373
+ options: FindOptions = {}
374
+ ): PostgresCursor<T> {
375
+ return new PostgresCursor<T>(this, filter, options);
376
+ }
377
+
378
+ async insertOne(
379
+ doc: Partial<T> & Record<string, unknown>
380
+ ): Promise<{ insertedId: ObjectId; acknowledged: boolean }> {
381
+ const id = doc._id
382
+ ? typeof doc._id === "string"
383
+ ? doc._id
384
+ : (doc._id as ObjectId).toString()
385
+ : randomUUID();
386
+
387
+ const entries = Object.entries(doc).filter(([k]) => k !== "_id");
388
+ const cols = ["_id", ...entries.map(([k]) => `"${snakeCase(k)}"`)];
389
+ const placeholders = [
390
+ "$1",
391
+ ...entries.map((_, i) => `$${i + 2}`),
392
+ ];
393
+ const values: unknown[] = [
394
+ id,
395
+ ...entries.map(([, v]) => {
396
+ if (v instanceof ObjectId) return v.toString();
397
+ if (typeof v === "object" && v !== null && !(v instanceof Date) && !Array.isArray(v))
398
+ return JSON.stringify(v);
399
+ if (Array.isArray(v)) return JSON.stringify(v);
400
+ return v;
401
+ }),
402
+ ];
403
+
404
+ const sql = `INSERT INTO "${this.tableName}" (${cols.join(", ")}) VALUES (${placeholders.join(", ")}) ON CONFLICT DO NOTHING RETURNING _id`;
405
+ await this.pool.query(sql, values);
406
+ return { insertedId: new ObjectId(id), acknowledged: true };
407
+ }
408
+
409
+ async insertMany(
410
+ docs: Array<Partial<T> & Record<string, unknown>>
411
+ ): Promise<{ insertedIds: ObjectId[]; acknowledged: boolean }> {
412
+ const ids: ObjectId[] = [];
413
+ for (const doc of docs) {
414
+ const result = await this.insertOne(doc);
415
+ ids.push(result.insertedId);
416
+ }
417
+ return { insertedIds: ids, acknowledged: true };
418
+ }
419
+
420
+ async updateOne(
421
+ filter: Record<string, unknown>,
422
+ update: Record<string, unknown>
423
+ ): Promise<{ matchedCount: number; modifiedCount: number; acknowledged: boolean }> {
424
+ const w = filterToWhere(filter);
425
+ const u = updateToSet(update, w.values.length + 1);
426
+ if (u.setClauses.length === 0) {
427
+ return { matchedCount: 0, modifiedCount: 0, acknowledged: true };
428
+ }
429
+ const sql = `UPDATE "${this.tableName}" SET ${u.setClauses.join(", ")} WHERE ${w.text}`;
430
+ const result = await this.pool.query(sql, [...w.values, ...u.values]);
431
+ const count = result.rowCount ?? 0;
432
+ return { matchedCount: count, modifiedCount: count, acknowledged: true };
433
+ }
434
+
435
+ async updateMany(
436
+ filter: Record<string, unknown>,
437
+ update: Record<string, unknown>
438
+ ): Promise<{ matchedCount: number; modifiedCount: number; acknowledged: boolean }> {
439
+ return this.updateOne(filter, update); // same SQL, no LIMIT 1
440
+ }
441
+
442
+ async deleteOne(
443
+ filter: Record<string, unknown>
444
+ ): Promise<{ deletedCount: number; acknowledged: boolean }> {
445
+ const w = filterToWhere(filter);
446
+ const sql = `DELETE FROM "${this.tableName}" WHERE ${w.text}`;
447
+ const result = await this.pool.query(sql, w.values);
448
+ return { deletedCount: result.rowCount ?? 0, acknowledged: true };
449
+ }
450
+
451
+ async deleteMany(
452
+ filter: Record<string, unknown>
453
+ ): Promise<{ deletedCount: number; acknowledged: boolean }> {
454
+ return this.deleteOne(filter);
455
+ }
456
+
457
+ async countDocuments(
458
+ filter: Record<string, unknown> = {}
459
+ ): Promise<number> {
460
+ const w = filterToWhere(filter);
461
+ const sql = `SELECT COUNT(*)::int AS count FROM "${this.tableName}" WHERE ${w.text}`;
462
+ const result = await this.pool.query(sql, w.values);
463
+ return result.rows[0]?.count ?? 0;
464
+ }
465
+
466
+ async distinct(
467
+ field: string,
468
+ filter: Record<string, unknown> = {}
469
+ ): Promise<unknown[]> {
470
+ const col = `"${snakeCase(field)}"`;
471
+ const w = filterToWhere(filter);
472
+ const sql = `SELECT DISTINCT ${col} FROM "${this.tableName}" WHERE ${w.text}`;
473
+ const result = await this.pool.query(sql, w.values);
474
+ return result.rows.map((r) => r[snakeCase(field)]);
475
+ }
476
+
477
+ async aggregate(pipeline: Record<string, unknown>[]): Promise<T[]> {
478
+ // Basic aggregation support — handle common patterns
479
+ // For complex pipelines, we'd need a full translator.
480
+ // For now, log a warning and return empty.
481
+ logger.warn(
482
+ { pipeline, table: this.tableName },
483
+ "aggregate() called — basic translation only"
484
+ );
485
+ return [];
486
+ }
487
+
488
+ async createIndex(
489
+ _spec: Record<string, unknown>,
490
+ _options?: Record<string, unknown>
491
+ ): Promise<void> {
492
+ // Indexes are pre-created in the migration. This is a no-op.
493
+ }
494
+
495
+ async findOneAndUpdate(
496
+ filter: Record<string, unknown>,
497
+ update: Record<string, unknown>,
498
+ options?: { upsert?: boolean; returnDocument?: "before" | "after" }
499
+ ): Promise<{ value: T | null }> {
500
+ if (options?.upsert) {
501
+ const existing = await this.findOne(filter);
502
+ if (!existing) {
503
+ const doc = { ...filter, ...((update.$set as Record<string, unknown>) ?? update) };
504
+ await this.insertOne(doc as Partial<T> & Record<string, unknown>);
505
+ const inserted = await this.findOne(filter);
506
+ return { value: inserted };
507
+ }
508
+ }
509
+ await this.updateOne(filter, update);
510
+ const updated = await this.findOne(filter);
511
+ return { value: updated };
512
+ }
513
+
514
+ async findOneAndDelete(
515
+ filter: Record<string, unknown>
516
+ ): Promise<{ value: T | null }> {
517
+ const doc = await this.findOne(filter);
518
+ if (doc) await this.deleteOne(filter);
519
+ return { value: doc };
520
+ }
521
+
522
+ // RuVector extension: semantic search via pgvector
523
+ async semanticSearch(
524
+ queryEmbedding: number[],
525
+ limit = 10,
526
+ filter: Record<string, unknown> = {}
527
+ ): Promise<Array<T & { similarity: number }>> {
528
+ const w = filterToWhere(filter);
529
+ const embIdx = w.values.length + 1;
530
+ const limIdx = embIdx + 1;
531
+ const sql = `
532
+ SELECT *, 1 - (embedding <=> $${embIdx}::vector) AS similarity
533
+ FROM "${this.tableName}"
534
+ WHERE ${w.text} AND embedding IS NOT NULL
535
+ ORDER BY embedding <=> $${embIdx}::vector
536
+ LIMIT $${limIdx}
537
+ `;
538
+ const result = await this.pool.query(sql, [
539
+ ...w.values,
540
+ `[${queryEmbedding.join(",")}]`,
541
+ limit,
542
+ ]);
543
+ return result.rows.map((r) => ({ ...this.rowToDoc(r), similarity: r.similarity }));
544
+ }
545
+ }
546
+
547
+ // ---------------------------------------------------------------------------
548
+ // Cursor — implements MongoDB-like chaining (sort/limit/skip/toArray)
549
+ // ---------------------------------------------------------------------------
550
+
551
+ export class PostgresCursor<T extends Record<string, unknown>> {
552
+ private _sort: Record<string, 1 | -1> = {};
553
+ private _limit?: number;
554
+ private _skip?: number;
555
+ private _projection?: Record<string, 0 | 1>;
556
+
557
+ constructor(
558
+ private collection: PostgresCollection<T>,
559
+ private filter: Record<string, unknown>,
560
+ options: FindOptions = {}
561
+ ) {
562
+ if (options.sort) this._sort = options.sort;
563
+ if (options.limit) this._limit = options.limit;
564
+ if (options.skip) this._skip = options.skip;
565
+ if (options.projection) this._projection = options.projection;
566
+ }
567
+
568
+ sort(spec: Record<string, 1 | -1>): this {
569
+ this._sort = { ...this._sort, ...spec };
570
+ return this;
571
+ }
572
+
573
+ limit(n: number): this {
574
+ this._limit = n;
575
+ return this;
576
+ }
577
+
578
+ skip(n: number): this {
579
+ this._skip = n;
580
+ return this;
581
+ }
582
+
583
+ project(spec: Record<string, 0 | 1>): this {
584
+ this._projection = spec;
585
+ return this;
586
+ }
587
+
588
+ async toArray(): Promise<T[]> {
589
+ const w = filterToWhere(this.filter);
590
+ const order = sortToOrderBy(this._sort);
591
+ let sql = `SELECT * FROM "${this.collection.tableName}" WHERE ${w.text} ${order}`;
592
+ const values = [...w.values];
593
+ if (this._limit !== undefined) {
594
+ sql += ` LIMIT $${values.length + 1}`;
595
+ values.push(this._limit);
596
+ }
597
+ if (this._skip !== undefined) {
598
+ sql += ` OFFSET $${values.length + 1}`;
599
+ values.push(this._skip);
600
+ }
601
+ const pool = getPool();
602
+ const result = await pool.query(sql, values);
603
+ return result.rows.map((row) => {
604
+ const doc: Record<string, unknown> = {};
605
+ for (const [key, val] of Object.entries(row)) {
606
+ doc[camelCase(key)] = val;
607
+ }
608
+ return doc as T;
609
+ });
610
+ }
611
+
612
+ // Async iterable support
613
+ async *[Symbol.asyncIterator](): AsyncGenerator<T> {
614
+ const rows = await this.toArray();
615
+ for (const row of rows) {
616
+ yield row;
617
+ }
618
+ }
619
+ }
620
+
621
+ // ---------------------------------------------------------------------------
622
+ // GridFS replacement — stores files as BYTEA in a `files` table
623
+ // ---------------------------------------------------------------------------
624
+
625
+ export class PostgresGridFSBucket {
626
+ private readonly tableName = "files";
627
+
628
+ async openUploadStream(
629
+ filename: string,
630
+ options?: { metadata?: Record<string, unknown>; contentType?: string }
631
+ ) {
632
+ const id = randomUUID();
633
+ const chunks: Buffer[] = [];
634
+
635
+ return {
636
+ id: new ObjectId(id),
637
+ write(chunk: Buffer) {
638
+ chunks.push(chunk);
639
+ },
640
+ async end() {
641
+ const data = Buffer.concat(chunks);
642
+ const pool = getPool();
643
+ await pool.query(
644
+ `INSERT INTO files (_id, filename, content_type, length, data, metadata) VALUES ($1, $2, $3, $4, $5, $6)`,
645
+ [
646
+ id,
647
+ filename,
648
+ options?.contentType ?? "application/octet-stream",
649
+ data.length,
650
+ data,
651
+ JSON.stringify(options?.metadata ?? {}),
652
+ ]
653
+ );
654
+ },
655
+ };
656
+ }
657
+
658
+ openDownloadStream(id: ObjectId | string) {
659
+ const fileId = typeof id === "string" ? id : id.toString();
660
+ // Return a readable-like object
661
+ return {
662
+ async toArray(): Promise<Buffer[]> {
663
+ const pool = getPool();
664
+ const result = await pool.query(
665
+ `SELECT data FROM files WHERE _id = $1`,
666
+ [fileId]
667
+ );
668
+ if (result.rows.length === 0) throw new Error("File not found");
669
+ return [result.rows[0].data];
670
+ },
671
+ };
672
+ }
673
+
674
+ async delete(id: ObjectId | string) {
675
+ const fileId = typeof id === "string" ? id : id.toString();
676
+ const pool = getPool();
677
+ await pool.query(`DELETE FROM files WHERE _id = $1`, [fileId]);
678
+ }
679
+
680
+ async find(filter: Record<string, unknown> = {}) {
681
+ const w = filterToWhere(filter);
682
+ const pool = getPool();
683
+ const result = await pool.query(
684
+ `SELECT _id, filename, content_type, length, metadata, created_at FROM files WHERE ${w.text}`,
685
+ w.values
686
+ );
687
+ return {
688
+ toArray: async () => result.rows,
689
+ };
690
+ }
691
+ }
692
+
693
+ // ---------------------------------------------------------------------------
694
+ // Helpers
695
+ // ---------------------------------------------------------------------------
696
+
697
+ function camelCase(s: string): string {
698
+ if (s === "_id") return "_id";
699
+ return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
700
+ }