ruflo 3.42.2 → 3.42.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,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
+ }