ruflo 3.10.45 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (498) hide show
  1. package/README.md +412 -412
  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/.claude-flow/daemon-state.json +135 -0
  22. package/src/ruvocal/.claude-flow/data/pending-insights.jsonl +0 -0
  23. package/src/ruvocal/.claude-flow/data/ranked-context.json +5 -0
  24. package/src/ruvocal/.claude-flow/logs/daemon.log +31 -0
  25. package/src/ruvocal/.claude-flow/logs/headless/audit_1777949411822_juxau0_prompt.log +989 -0
  26. package/src/ruvocal/.claude-flow/logs/headless/audit_1777949411822_juxau0_result.log +67 -0
  27. package/src/ruvocal/.claude-flow/logs/headless/audit_1777950042278_jvj5xq_prompt.log +989 -0
  28. package/src/ruvocal/.claude-flow/logs/headless/audit_1777950042278_jvj5xq_result.log +93 -0
  29. package/src/ruvocal/.claude-flow/logs/headless/optimize_1777949531823_yt5yc2_prompt.log +1498 -0
  30. package/src/ruvocal/.claude-flow/logs/headless/optimize_1777949531823_yt5yc2_result.log +93 -0
  31. package/src/ruvocal/.claude-flow/logs/headless/testgaps_1777949771821_elw1j4_prompt.log +1498 -0
  32. package/src/ruvocal/.claude-flow/logs/headless/testgaps_1777949771821_elw1j4_result.log +100 -0
  33. package/src/ruvocal/.claude-flow/metrics/codebase-map.json +11 -0
  34. package/src/ruvocal/.claude-flow/metrics/consolidation.json +6 -0
  35. package/src/ruvocal/.claude-flow/neural/stats.json +6 -0
  36. package/src/ruvocal/.claude-flow/sessions/current.json +13 -0
  37. package/src/ruvocal/.devcontainer/Dockerfile +9 -9
  38. package/src/ruvocal/.devcontainer/devcontainer.json +36 -36
  39. package/src/ruvocal/.dockerignore +16 -16
  40. package/src/ruvocal/.eslintignore +13 -13
  41. package/src/ruvocal/.eslintrc.cjs +45 -45
  42. package/src/ruvocal/.gcloudignore +18 -18
  43. package/src/ruvocal/.github/ISSUE_TEMPLATE/bug-report--chat-ui-.md +43 -43
  44. package/src/ruvocal/.github/ISSUE_TEMPLATE/config-support.md +9 -9
  45. package/src/ruvocal/.github/ISSUE_TEMPLATE/feature-request--chat-ui-.md +17 -17
  46. package/src/ruvocal/.github/ISSUE_TEMPLATE/huggingchat.md +11 -11
  47. package/src/ruvocal/.github/release.yml +16 -16
  48. package/src/ruvocal/.github/workflows/build-docs.yml +18 -18
  49. package/src/ruvocal/.github/workflows/build-image.yml +142 -142
  50. package/src/ruvocal/.github/workflows/build-pr-docs.yml +20 -20
  51. package/src/ruvocal/.github/workflows/deploy-dev.yml +63 -63
  52. package/src/ruvocal/.github/workflows/deploy-prod.yml +78 -78
  53. package/src/ruvocal/.github/workflows/lint-and-test.yml +84 -84
  54. package/src/ruvocal/.github/workflows/slugify.yaml +72 -72
  55. package/src/ruvocal/.github/workflows/trufflehog.yml +17 -17
  56. package/src/ruvocal/.github/workflows/upload-pr-documentation.yml +16 -16
  57. package/src/ruvocal/.husky/lint-stage-config.js +4 -4
  58. package/src/ruvocal/.husky/pre-commit +2 -2
  59. package/src/ruvocal/.prettierignore +14 -14
  60. package/src/ruvocal/.prettierrc +7 -7
  61. package/src/ruvocal/.swarm/attestation.db +0 -0
  62. package/src/ruvocal/.swarm/hnsw.index +0 -0
  63. package/src/ruvocal/.swarm/hnsw.metadata.json +1 -0
  64. package/src/ruvocal/.swarm/memory.db +0 -0
  65. package/src/ruvocal/.swarm/schema.sql +305 -0
  66. package/src/ruvocal/CLAUDE.md +126 -126
  67. package/src/ruvocal/Dockerfile +96 -96
  68. package/src/ruvocal/LICENSE +202 -202
  69. package/src/ruvocal/PRIVACY.md +41 -41
  70. package/src/ruvocal/README.md +164 -164
  71. package/src/ruvocal/chart/Chart.yaml +5 -5
  72. package/src/ruvocal/chart/env/dev.yaml +260 -260
  73. package/src/ruvocal/chart/env/prod.yaml +273 -273
  74. package/src/ruvocal/chart/templates/_helpers.tpl +22 -22
  75. package/src/ruvocal/chart/templates/config.yaml +10 -10
  76. package/src/ruvocal/chart/templates/deployment.yaml +81 -81
  77. package/src/ruvocal/chart/templates/hpa.yaml +45 -45
  78. package/src/ruvocal/chart/templates/infisical.yaml +24 -24
  79. package/src/ruvocal/chart/templates/ingress-internal.yaml +32 -32
  80. package/src/ruvocal/chart/templates/ingress.yaml +32 -32
  81. package/src/ruvocal/chart/templates/network-policy.yaml +36 -36
  82. package/src/ruvocal/chart/templates/service-account.yaml +13 -13
  83. package/src/ruvocal/chart/templates/service-monitor.yaml +17 -17
  84. package/src/ruvocal/chart/templates/service.yaml +21 -21
  85. package/src/ruvocal/chart/values.yaml +73 -73
  86. package/src/ruvocal/cloudbuild.yaml +68 -68
  87. package/src/ruvocal/config/branding.env.example +19 -19
  88. package/src/ruvocal/docker-compose.yml +21 -21
  89. package/src/ruvocal/docs/adr/ADR-029-HUGGINGFACE-CHAT-UI-CLOUD-RUN.md +1236 -1236
  90. package/src/ruvocal/docs/adr/ADR-033-RUVECTOR-RUFLO-MCP-INTEGRATION.md +111 -111
  91. package/src/ruvocal/docs/adr/ADR-034-OPTIONAL-MCP-BACKENDS.md +117 -117
  92. package/src/ruvocal/docs/adr/ADR-035-MCP-TOOL-GROUPS.md +186 -186
  93. package/src/ruvocal/docs/adr/ADR-037-AUTOPILOT-CHAT-MODE.md +1500 -1500
  94. package/src/ruvocal/docs/adr/ADR-038-RUVOCAL-FORK.md +286 -286
  95. package/src/ruvocal/docs/source/_toctree.yml +30 -30
  96. package/src/ruvocal/docs/source/configuration/common-issues.md +38 -38
  97. package/src/ruvocal/docs/source/configuration/llm-router.md +105 -105
  98. package/src/ruvocal/docs/source/configuration/mcp-tools.md +84 -84
  99. package/src/ruvocal/docs/source/configuration/metrics.md +9 -9
  100. package/src/ruvocal/docs/source/configuration/open-id.md +57 -57
  101. package/src/ruvocal/docs/source/configuration/overview.md +89 -89
  102. package/src/ruvocal/docs/source/configuration/theming.md +20 -20
  103. package/src/ruvocal/docs/source/developing/architecture.md +48 -48
  104. package/src/ruvocal/docs/source/index.md +53 -53
  105. package/src/ruvocal/docs/source/installation/docker.md +43 -43
  106. package/src/ruvocal/docs/source/installation/helm.md +43 -43
  107. package/src/ruvocal/docs/source/installation/local.md +62 -62
  108. package/src/ruvocal/entrypoint.sh +18 -18
  109. package/src/ruvocal/mcp-bridge/Dockerfile +45 -45
  110. package/src/ruvocal/mcp-bridge/cloudbuild.yaml +49 -49
  111. package/src/ruvocal/mcp-bridge/index.js +1902 -1902
  112. package/src/ruvocal/mcp-bridge/mcp-stdio-kernel.js +159 -159
  113. package/src/ruvocal/mcp-bridge/package-lock.json +762 -762
  114. package/src/ruvocal/mcp-bridge/package.json +17 -17
  115. package/src/ruvocal/mcp-bridge/test-harness.js +470 -470
  116. package/src/ruvocal/package-lock.json +11741 -11741
  117. package/src/ruvocal/package.json +121 -121
  118. package/src/ruvocal/postcss.config.js +6 -6
  119. package/src/ruvocal/rvf.manifest.json +204 -204
  120. package/src/ruvocal/scripts/config.ts +64 -64
  121. package/src/ruvocal/scripts/generate-welcome.mjs +181 -181
  122. package/src/ruvocal/scripts/populate.ts +288 -288
  123. package/src/ruvocal/scripts/samples.txt +194 -194
  124. package/src/ruvocal/scripts/setups/vitest-setup-server.ts +44 -44
  125. package/src/ruvocal/scripts/updateLocalEnv.ts +48 -48
  126. package/src/ruvocal/src/ambient.d.ts +7 -7
  127. package/src/ruvocal/src/app.d.ts +29 -29
  128. package/src/ruvocal/src/app.html +53 -53
  129. package/src/ruvocal/src/hooks.server.ts +32 -32
  130. package/src/ruvocal/src/hooks.ts +6 -6
  131. package/src/ruvocal/src/lib/APIClient.ts +148 -148
  132. package/src/ruvocal/src/lib/actions/clickOutside.ts +18 -18
  133. package/src/ruvocal/src/lib/actions/snapScrollToBottom.ts +346 -346
  134. package/src/ruvocal/src/lib/buildPrompt.ts +33 -33
  135. package/src/ruvocal/src/lib/components/AnnouncementBanner.svelte +20 -20
  136. package/src/ruvocal/src/lib/components/BackgroundGenerationPoller.svelte +168 -168
  137. package/src/ruvocal/src/lib/components/CodeBlock.svelte +73 -73
  138. package/src/ruvocal/src/lib/components/CopyToClipBoardBtn.svelte +92 -92
  139. package/src/ruvocal/src/lib/components/DeleteConversationModal.svelte +75 -75
  140. package/src/ruvocal/src/lib/components/EditConversationModal.svelte +100 -100
  141. package/src/ruvocal/src/lib/components/ExpandNavigation.svelte +22 -22
  142. package/src/ruvocal/src/lib/components/FoundationBackground.svelte +242 -242
  143. package/src/ruvocal/src/lib/components/HoverTooltip.svelte +44 -44
  144. package/src/ruvocal/src/lib/components/HtmlPreviewModal.svelte +143 -143
  145. package/src/ruvocal/src/lib/components/InfiniteScroll.svelte +50 -50
  146. package/src/ruvocal/src/lib/components/MobileNav.svelte +300 -300
  147. package/src/ruvocal/src/lib/components/Modal.svelte +115 -115
  148. package/src/ruvocal/src/lib/components/ModelCardMetadata.svelte +71 -71
  149. package/src/ruvocal/src/lib/components/NavConversationItem.svelte +151 -151
  150. package/src/ruvocal/src/lib/components/NavMenu.svelte +313 -313
  151. package/src/ruvocal/src/lib/components/Pagination.svelte +97 -97
  152. package/src/ruvocal/src/lib/components/PaginationArrow.svelte +27 -27
  153. package/src/ruvocal/src/lib/components/Portal.svelte +24 -24
  154. package/src/ruvocal/src/lib/components/RetryBtn.svelte +18 -18
  155. package/src/ruvocal/src/lib/components/RuFloUniverse.svelte +185 -185
  156. package/src/ruvocal/src/lib/components/RufloHelpModal.svelte +411 -411
  157. package/src/ruvocal/src/lib/components/ScrollToBottomBtn.svelte +47 -47
  158. package/src/ruvocal/src/lib/components/ScrollToPreviousBtn.svelte +77 -77
  159. package/src/ruvocal/src/lib/components/ShareConversationModal.svelte +182 -182
  160. package/src/ruvocal/src/lib/components/StopGeneratingBtn.svelte +69 -69
  161. package/src/ruvocal/src/lib/components/SubscribeModal.svelte +87 -87
  162. package/src/ruvocal/src/lib/components/Switch.svelte +36 -36
  163. package/src/ruvocal/src/lib/components/SystemPromptModal.svelte +44 -44
  164. package/src/ruvocal/src/lib/components/Toast.svelte +27 -27
  165. package/src/ruvocal/src/lib/components/Tooltip.svelte +30 -30
  166. package/src/ruvocal/src/lib/components/WelcomeModal.svelte +46 -46
  167. package/src/ruvocal/src/lib/components/chat/Alternatives.svelte +77 -77
  168. package/src/ruvocal/src/lib/components/chat/BlockWrapper.svelte +72 -72
  169. package/src/ruvocal/src/lib/components/chat/ChatInput.svelte +490 -490
  170. package/src/ruvocal/src/lib/components/chat/ChatIntroduction.svelte +123 -123
  171. package/src/ruvocal/src/lib/components/chat/ChatMessage.svelte +548 -548
  172. package/src/ruvocal/src/lib/components/chat/ChatWindow.svelte +1057 -1057
  173. package/src/ruvocal/src/lib/components/chat/FileDropzone.svelte +92 -92
  174. package/src/ruvocal/src/lib/components/chat/ImageLightbox.svelte +66 -66
  175. package/src/ruvocal/src/lib/components/chat/MarkdownBlock.svelte +23 -23
  176. package/src/ruvocal/src/lib/components/chat/MarkdownRenderer.svelte +69 -69
  177. package/src/ruvocal/src/lib/components/chat/MarkdownRenderer.svelte.test.ts +58 -58
  178. package/src/ruvocal/src/lib/components/chat/MessageAvatar.svelte +103 -103
  179. package/src/ruvocal/src/lib/components/chat/ModelSwitch.svelte +64 -64
  180. package/src/ruvocal/src/lib/components/chat/OpenReasoningResults.svelte +81 -81
  181. package/src/ruvocal/src/lib/components/chat/TaskGroup.svelte +88 -88
  182. package/src/ruvocal/src/lib/components/chat/ToolUpdate.svelte +273 -273
  183. package/src/ruvocal/src/lib/components/chat/UploadedFile.svelte +253 -253
  184. package/src/ruvocal/src/lib/components/chat/UrlFetchModal.svelte +203 -203
  185. package/src/ruvocal/src/lib/components/chat/VoiceRecorder.svelte +214 -214
  186. package/src/ruvocal/src/lib/components/icons/IconBurger.svelte +20 -20
  187. package/src/ruvocal/src/lib/components/icons/IconCheap.svelte +20 -20
  188. package/src/ruvocal/src/lib/components/icons/IconChevron.svelte +24 -24
  189. package/src/ruvocal/src/lib/components/icons/IconDazzled.svelte +40 -40
  190. package/src/ruvocal/src/lib/components/icons/IconFast.svelte +20 -20
  191. package/src/ruvocal/src/lib/components/icons/IconLoading.svelte +22 -22
  192. package/src/ruvocal/src/lib/components/icons/IconMCP.svelte +28 -28
  193. package/src/ruvocal/src/lib/components/icons/IconMoon.svelte +21 -21
  194. package/src/ruvocal/src/lib/components/icons/IconNew.svelte +20 -20
  195. package/src/ruvocal/src/lib/components/icons/IconOmni.svelte +90 -90
  196. package/src/ruvocal/src/lib/components/icons/IconPaperclip.svelte +24 -24
  197. package/src/ruvocal/src/lib/components/icons/IconPro.svelte +37 -37
  198. package/src/ruvocal/src/lib/components/icons/IconShare.svelte +21 -21
  199. package/src/ruvocal/src/lib/components/icons/IconSun.svelte +93 -93
  200. package/src/ruvocal/src/lib/components/icons/Logo.svelte +68 -68
  201. package/src/ruvocal/src/lib/components/icons/LogoHuggingFaceBorderless.svelte +54 -54
  202. package/src/ruvocal/src/lib/components/mcp/AddServerForm.svelte +250 -250
  203. package/src/ruvocal/src/lib/components/mcp/MCPServerManager.svelte +185 -185
  204. package/src/ruvocal/src/lib/components/mcp/ServerCard.svelte +203 -203
  205. package/src/ruvocal/src/lib/components/players/AudioPlayer.svelte +82 -82
  206. package/src/ruvocal/src/lib/components/voice/AudioWaveform.svelte +96 -96
  207. package/src/ruvocal/src/lib/components/wasm/GalleryPanel.svelte +357 -357
  208. package/src/ruvocal/src/lib/constants/mcpExamples.ts +114 -114
  209. package/src/ruvocal/src/lib/constants/mime.ts +11 -11
  210. package/src/ruvocal/src/lib/constants/pagination.ts +1 -1
  211. package/src/ruvocal/src/lib/constants/publicSepToken.ts +1 -1
  212. package/src/ruvocal/src/lib/constants/routerExamples.ts +133 -133
  213. package/src/ruvocal/src/lib/constants/rvagentPresets.ts +206 -206
  214. package/src/ruvocal/src/lib/createShareLink.ts +27 -27
  215. package/src/ruvocal/src/lib/jobs/refresh-conversation-stats.ts +297 -297
  216. package/src/ruvocal/src/lib/migrations/lock.ts +56 -56
  217. package/src/ruvocal/src/lib/migrations/migrations.spec.ts +74 -74
  218. package/src/ruvocal/src/lib/migrations/migrations.ts +109 -109
  219. package/src/ruvocal/src/lib/migrations/routines/01-update-search-assistants.ts +50 -50
  220. package/src/ruvocal/src/lib/migrations/routines/02-update-assistants-models.ts +48 -48
  221. package/src/ruvocal/src/lib/migrations/routines/04-update-message-updates.ts +151 -151
  222. package/src/ruvocal/src/lib/migrations/routines/05-update-message-files.ts +56 -56
  223. package/src/ruvocal/src/lib/migrations/routines/06-trim-message-updates.ts +56 -56
  224. package/src/ruvocal/src/lib/migrations/routines/08-update-featured-to-review.ts +32 -32
  225. package/src/ruvocal/src/lib/migrations/routines/09-delete-empty-conversations.spec.ts +214 -214
  226. package/src/ruvocal/src/lib/migrations/routines/09-delete-empty-conversations.ts +88 -88
  227. package/src/ruvocal/src/lib/migrations/routines/10-update-reports-assistantid.ts +29 -29
  228. package/src/ruvocal/src/lib/migrations/routines/index.ts +15 -15
  229. package/src/ruvocal/src/lib/server/__tests__/conversation-stop-generating.spec.ts +103 -103
  230. package/src/ruvocal/src/lib/server/abortRegistry.ts +57 -57
  231. package/src/ruvocal/src/lib/server/abortedGenerations.ts +43 -43
  232. package/src/ruvocal/src/lib/server/adminToken.ts +62 -62
  233. package/src/ruvocal/src/lib/server/api/__tests__/conversations-id.spec.ts +296 -296
  234. package/src/ruvocal/src/lib/server/api/__tests__/conversations-message.spec.ts +216 -216
  235. package/src/ruvocal/src/lib/server/api/__tests__/conversations.spec.ts +235 -235
  236. package/src/ruvocal/src/lib/server/api/__tests__/misc.spec.ts +72 -72
  237. package/src/ruvocal/src/lib/server/api/__tests__/testHelpers.ts +86 -86
  238. package/src/ruvocal/src/lib/server/api/__tests__/user-reports.spec.ts +78 -78
  239. package/src/ruvocal/src/lib/server/api/__tests__/user.spec.ts +239 -239
  240. package/src/ruvocal/src/lib/server/api/types.ts +37 -37
  241. package/src/ruvocal/src/lib/server/api/utils/requireAuth.ts +22 -22
  242. package/src/ruvocal/src/lib/server/api/utils/resolveConversation.ts +69 -69
  243. package/src/ruvocal/src/lib/server/api/utils/resolveModel.ts +27 -27
  244. package/src/ruvocal/src/lib/server/api/utils/superjsonResponse.ts +15 -15
  245. package/src/ruvocal/src/lib/server/apiToken.ts +11 -11
  246. package/src/ruvocal/src/lib/server/auth.ts +554 -554
  247. package/src/ruvocal/src/lib/server/config.ts +187 -187
  248. package/src/ruvocal/src/lib/server/conversation.ts +83 -83
  249. package/src/ruvocal/src/lib/server/database/__tests__/rvf.spec.ts +709 -709
  250. package/src/ruvocal/src/lib/server/database/postgres.ts +700 -700
  251. package/src/ruvocal/src/lib/server/database/rvf.ts +1078 -1078
  252. package/src/ruvocal/src/lib/server/database.ts +145 -145
  253. package/src/ruvocal/src/lib/server/endpoints/document.ts +68 -68
  254. package/src/ruvocal/src/lib/server/endpoints/endpoints.ts +43 -43
  255. package/src/ruvocal/src/lib/server/endpoints/images.ts +211 -211
  256. package/src/ruvocal/src/lib/server/endpoints/openai/endpointOai.ts +266 -266
  257. package/src/ruvocal/src/lib/server/endpoints/openai/openAIChatToTextGenerationStream.ts +212 -212
  258. package/src/ruvocal/src/lib/server/endpoints/openai/openAICompletionToTextGenerationStream.ts +32 -32
  259. package/src/ruvocal/src/lib/server/endpoints/preprocessMessages.ts +61 -61
  260. package/src/ruvocal/src/lib/server/exitHandler.ts +59 -59
  261. package/src/ruvocal/src/lib/server/files/downloadFile.ts +34 -34
  262. package/src/ruvocal/src/lib/server/files/uploadFile.ts +29 -29
  263. package/src/ruvocal/src/lib/server/findRepoRoot.ts +13 -13
  264. package/src/ruvocal/src/lib/server/generateFromDefaultEndpoint.ts +46 -46
  265. package/src/ruvocal/src/lib/server/hooks/error.ts +37 -37
  266. package/src/ruvocal/src/lib/server/hooks/fetch.ts +22 -22
  267. package/src/ruvocal/src/lib/server/hooks/handle.ts +250 -250
  268. package/src/ruvocal/src/lib/server/hooks/init.ts +51 -51
  269. package/src/ruvocal/src/lib/server/isURLLocal.spec.ts +31 -31
  270. package/src/ruvocal/src/lib/server/isURLLocal.ts +74 -74
  271. package/src/ruvocal/src/lib/server/logger.ts +42 -42
  272. package/src/ruvocal/src/lib/server/mcp/clientPool.spec.ts +175 -175
  273. package/src/ruvocal/src/lib/server/mcp/hf.ts +32 -32
  274. package/src/ruvocal/src/lib/server/mcp/httpClient.ts +122 -122
  275. package/src/ruvocal/src/lib/server/mcp/registry.ts +76 -76
  276. package/src/ruvocal/src/lib/server/mcp/tools.ts +196 -196
  277. package/src/ruvocal/src/lib/server/metrics.ts +255 -255
  278. package/src/ruvocal/src/lib/server/models.ts +518 -518
  279. package/src/ruvocal/src/lib/server/requestContext.ts +55 -55
  280. package/src/ruvocal/src/lib/server/router/arch.ts +230 -230
  281. package/src/ruvocal/src/lib/server/router/endpoint.ts +316 -316
  282. package/src/ruvocal/src/lib/server/router/multimodal.ts +28 -28
  283. package/src/ruvocal/src/lib/server/router/policy.ts +49 -49
  284. package/src/ruvocal/src/lib/server/router/toolsRoute.ts +51 -51
  285. package/src/ruvocal/src/lib/server/router/types.ts +21 -21
  286. package/src/ruvocal/src/lib/server/sendSlack.ts +23 -23
  287. package/src/ruvocal/src/lib/server/textGeneration/generate.ts +258 -258
  288. package/src/ruvocal/src/lib/server/textGeneration/index.ts +96 -96
  289. package/src/ruvocal/src/lib/server/textGeneration/mcp/fileRefs.ts +155 -155
  290. package/src/ruvocal/src/lib/server/textGeneration/mcp/routerResolution.ts +108 -108
  291. package/src/ruvocal/src/lib/server/textGeneration/mcp/runMcpFlow.ts +831 -831
  292. package/src/ruvocal/src/lib/server/textGeneration/mcp/toolInvocation.ts +349 -349
  293. package/src/ruvocal/src/lib/server/textGeneration/mcp/wasmTools.test.ts +633 -633
  294. package/src/ruvocal/src/lib/server/textGeneration/reasoning.ts +23 -23
  295. package/src/ruvocal/src/lib/server/textGeneration/title.ts +83 -83
  296. package/src/ruvocal/src/lib/server/textGeneration/types.ts +28 -28
  297. package/src/ruvocal/src/lib/server/textGeneration/utils/prepareFiles.ts +88 -88
  298. package/src/ruvocal/src/lib/server/textGeneration/utils/routing.ts +21 -21
  299. package/src/ruvocal/src/lib/server/textGeneration/utils/toolPrompt.ts +49 -49
  300. package/src/ruvocal/src/lib/server/urlSafety.ts +77 -77
  301. package/src/ruvocal/src/lib/server/usageLimits.ts +30 -30
  302. package/src/ruvocal/src/lib/stores/autopilotStore.svelte.ts +175 -175
  303. package/src/ruvocal/src/lib/stores/backgroundGenerations.svelte.ts +32 -32
  304. package/src/ruvocal/src/lib/stores/backgroundGenerations.ts +1 -1
  305. package/src/ruvocal/src/lib/stores/errors.ts +9 -9
  306. package/src/ruvocal/src/lib/stores/isAborted.ts +3 -3
  307. package/src/ruvocal/src/lib/stores/isPro.ts +4 -4
  308. package/src/ruvocal/src/lib/stores/loading.ts +3 -3
  309. package/src/ruvocal/src/lib/stores/mcpServers.ts +534 -534
  310. package/src/ruvocal/src/lib/stores/pendingChatInput.ts +3 -3
  311. package/src/ruvocal/src/lib/stores/pendingMessage.ts +9 -9
  312. package/src/ruvocal/src/lib/stores/settings.ts +182 -182
  313. package/src/ruvocal/src/lib/stores/shareModal.ts +13 -13
  314. package/src/ruvocal/src/lib/stores/titleUpdate.ts +8 -8
  315. package/src/ruvocal/src/lib/stores/wasmMcp.ts +472 -472
  316. package/src/ruvocal/src/lib/switchTheme.ts +124 -124
  317. package/src/ruvocal/src/lib/types/AbortedGeneration.ts +8 -8
  318. package/src/ruvocal/src/lib/types/Assistant.ts +31 -31
  319. package/src/ruvocal/src/lib/types/AssistantStats.ts +11 -11
  320. package/src/ruvocal/src/lib/types/ConfigKey.ts +4 -4
  321. package/src/ruvocal/src/lib/types/ConvSidebar.ts +9 -9
  322. package/src/ruvocal/src/lib/types/Conversation.ts +27 -27
  323. package/src/ruvocal/src/lib/types/ConversationStats.ts +13 -13
  324. package/src/ruvocal/src/lib/types/Message.ts +41 -41
  325. package/src/ruvocal/src/lib/types/MessageEvent.ts +10 -10
  326. package/src/ruvocal/src/lib/types/MessageUpdate.ts +139 -139
  327. package/src/ruvocal/src/lib/types/MigrationResult.ts +7 -7
  328. package/src/ruvocal/src/lib/types/Model.ts +23 -23
  329. package/src/ruvocal/src/lib/types/Report.ts +12 -12
  330. package/src/ruvocal/src/lib/types/Review.ts +6 -6
  331. package/src/ruvocal/src/lib/types/Semaphore.ts +19 -19
  332. package/src/ruvocal/src/lib/types/Session.ts +22 -22
  333. package/src/ruvocal/src/lib/types/Settings.ts +93 -93
  334. package/src/ruvocal/src/lib/types/SharedConversation.ts +9 -9
  335. package/src/ruvocal/src/lib/types/Template.ts +6 -6
  336. package/src/ruvocal/src/lib/types/Timestamps.ts +4 -4
  337. package/src/ruvocal/src/lib/types/TokenCache.ts +6 -6
  338. package/src/ruvocal/src/lib/types/Tool.ts +77 -77
  339. package/src/ruvocal/src/lib/types/UrlDependency.ts +5 -5
  340. package/src/ruvocal/src/lib/types/User.ts +14 -14
  341. package/src/ruvocal/src/lib/utils/PublicConfig.svelte.ts +75 -75
  342. package/src/ruvocal/src/lib/utils/auth.ts +17 -17
  343. package/src/ruvocal/src/lib/utils/chunk.ts +33 -33
  344. package/src/ruvocal/src/lib/utils/cookiesAreEnabled.ts +13 -13
  345. package/src/ruvocal/src/lib/utils/debounce.ts +17 -17
  346. package/src/ruvocal/src/lib/utils/deepestChild.ts +6 -6
  347. package/src/ruvocal/src/lib/utils/favicon.ts +21 -21
  348. package/src/ruvocal/src/lib/utils/fetchJSON.ts +23 -23
  349. package/src/ruvocal/src/lib/utils/file2base64.ts +14 -14
  350. package/src/ruvocal/src/lib/utils/formatUserCount.ts +37 -37
  351. package/src/ruvocal/src/lib/utils/generationState.spec.ts +75 -75
  352. package/src/ruvocal/src/lib/utils/generationState.ts +26 -26
  353. package/src/ruvocal/src/lib/utils/getHref.ts +41 -41
  354. package/src/ruvocal/src/lib/utils/getReturnFromGenerator.ts +7 -7
  355. package/src/ruvocal/src/lib/utils/haptics.ts +64 -64
  356. package/src/ruvocal/src/lib/utils/hashConv.ts +12 -12
  357. package/src/ruvocal/src/lib/utils/hf.ts +17 -17
  358. package/src/ruvocal/src/lib/utils/isDesktop.ts +7 -7
  359. package/src/ruvocal/src/lib/utils/isUrl.ts +8 -8
  360. package/src/ruvocal/src/lib/utils/isVirtualKeyboard.ts +16 -16
  361. package/src/ruvocal/src/lib/utils/loadAttachmentsFromUrls.ts +115 -115
  362. package/src/ruvocal/src/lib/utils/marked.spec.ts +96 -96
  363. package/src/ruvocal/src/lib/utils/marked.ts +531 -531
  364. package/src/ruvocal/src/lib/utils/mcpValidation.ts +147 -147
  365. package/src/ruvocal/src/lib/utils/mergeAsyncGenerators.ts +38 -38
  366. package/src/ruvocal/src/lib/utils/messageUpdates.spec.ts +262 -262
  367. package/src/ruvocal/src/lib/utils/messageUpdates.ts +324 -324
  368. package/src/ruvocal/src/lib/utils/mime.ts +56 -56
  369. package/src/ruvocal/src/lib/utils/models.ts +14 -14
  370. package/src/ruvocal/src/lib/utils/parseBlocks.ts +120 -120
  371. package/src/ruvocal/src/lib/utils/parseIncompleteMarkdown.ts +644 -644
  372. package/src/ruvocal/src/lib/utils/parseStringToList.ts +10 -10
  373. package/src/ruvocal/src/lib/utils/randomUuid.ts +14 -14
  374. package/src/ruvocal/src/lib/utils/searchTokens.ts +33 -33
  375. package/src/ruvocal/src/lib/utils/sha256.ts +7 -7
  376. package/src/ruvocal/src/lib/utils/stringifyError.ts +12 -12
  377. package/src/ruvocal/src/lib/utils/sum.ts +3 -3
  378. package/src/ruvocal/src/lib/utils/template.spec.ts +59 -59
  379. package/src/ruvocal/src/lib/utils/template.ts +53 -53
  380. package/src/ruvocal/src/lib/utils/timeout.ts +9 -9
  381. package/src/ruvocal/src/lib/utils/toolProgress.spec.ts +46 -46
  382. package/src/ruvocal/src/lib/utils/toolProgress.ts +11 -11
  383. package/src/ruvocal/src/lib/utils/tree/addChildren.spec.ts +102 -102
  384. package/src/ruvocal/src/lib/utils/tree/addChildren.ts +48 -48
  385. package/src/ruvocal/src/lib/utils/tree/addSibling.spec.ts +81 -81
  386. package/src/ruvocal/src/lib/utils/tree/addSibling.ts +41 -41
  387. package/src/ruvocal/src/lib/utils/tree/buildSubtree.spec.ts +110 -110
  388. package/src/ruvocal/src/lib/utils/tree/buildSubtree.ts +24 -24
  389. package/src/ruvocal/src/lib/utils/tree/convertLegacyConversation.spec.ts +31 -31
  390. package/src/ruvocal/src/lib/utils/tree/convertLegacyConversation.ts +36 -36
  391. package/src/ruvocal/src/lib/utils/tree/isMessageId.spec.ts +15 -15
  392. package/src/ruvocal/src/lib/utils/tree/isMessageId.ts +5 -5
  393. package/src/ruvocal/src/lib/utils/tree/tree.d.ts +14 -14
  394. package/src/ruvocal/src/lib/utils/tree/treeHelpers.spec.ts +167 -167
  395. package/src/ruvocal/src/lib/utils/updates.ts +39 -39
  396. package/src/ruvocal/src/lib/utils/urlParams.ts +13 -13
  397. package/src/ruvocal/src/lib/wasm/idb.ts +438 -438
  398. package/src/ruvocal/src/lib/wasm/index.ts +1213 -1213
  399. package/src/ruvocal/src/lib/wasm/tests/wasm-capabilities.test.ts +565 -565
  400. package/src/ruvocal/src/lib/wasm/wasm.worker.ts +332 -332
  401. package/src/ruvocal/src/lib/wasm/workerClient.ts +166 -166
  402. package/src/ruvocal/src/lib/workers/autopilotWorker.ts +221 -221
  403. package/src/ruvocal/src/lib/workers/detailFetchWorker.ts +100 -100
  404. package/src/ruvocal/src/lib/workers/markdownWorker.ts +61 -61
  405. package/src/ruvocal/src/routes/+error.svelte +20 -20
  406. package/src/ruvocal/src/routes/+layout.svelte +324 -324
  407. package/src/ruvocal/src/routes/+layout.ts +91 -91
  408. package/src/ruvocal/src/routes/+page.svelte +168 -168
  409. package/src/ruvocal/src/routes/.well-known/oauth-cimd/+server.ts +37 -37
  410. package/src/ruvocal/src/routes/__debug/openai/+server.ts +21 -21
  411. package/src/ruvocal/src/routes/admin/export/+server.ts +159 -159
  412. package/src/ruvocal/src/routes/admin/stats/compute/+server.ts +16 -16
  413. package/src/ruvocal/src/routes/api/conversation/[id]/+server.ts +40 -40
  414. package/src/ruvocal/src/routes/api/conversation/[id]/message/[messageId]/+server.ts +42 -42
  415. package/src/ruvocal/src/routes/api/conversations/+server.ts +48 -48
  416. package/src/ruvocal/src/routes/api/fetch-url/+server.ts +147 -147
  417. package/src/ruvocal/src/routes/api/mcp/health/+server.ts +292 -292
  418. package/src/ruvocal/src/routes/api/mcp/servers/+server.ts +32 -32
  419. package/src/ruvocal/src/routes/api/models/+server.ts +25 -25
  420. package/src/ruvocal/src/routes/api/transcribe/+server.ts +104 -104
  421. package/src/ruvocal/src/routes/api/user/+server.ts +15 -15
  422. package/src/ruvocal/src/routes/api/user/validate-token/+server.ts +20 -20
  423. package/src/ruvocal/src/routes/api/v2/conversations/+server.ts +48 -48
  424. package/src/ruvocal/src/routes/api/v2/conversations/[id]/+server.ts +94 -94
  425. package/src/ruvocal/src/routes/api/v2/conversations/[id]/message/[messageId]/+server.ts +43 -43
  426. package/src/ruvocal/src/routes/api/v2/conversations/import-share/+server.ts +23 -23
  427. package/src/ruvocal/src/routes/api/v2/debug/config/+server.ts +16 -16
  428. package/src/ruvocal/src/routes/api/v2/debug/refresh/+server.ts +30 -30
  429. package/src/ruvocal/src/routes/api/v2/export/+server.ts +196 -196
  430. package/src/ruvocal/src/routes/api/v2/feature-flags/+server.ts +14 -14
  431. package/src/ruvocal/src/routes/api/v2/models/+server.ts +38 -38
  432. package/src/ruvocal/src/routes/api/v2/models/[namespace]/+server.ts +8 -8
  433. package/src/ruvocal/src/routes/api/v2/models/[namespace]/[model]/+server.ts +8 -8
  434. package/src/ruvocal/src/routes/api/v2/models/[namespace]/[model]/subscribe/+server.ts +28 -28
  435. package/src/ruvocal/src/routes/api/v2/models/[namespace]/subscribe/+server.ts +28 -28
  436. package/src/ruvocal/src/routes/api/v2/models/old/+server.ts +7 -7
  437. package/src/ruvocal/src/routes/api/v2/models/refresh/+server.ts +33 -33
  438. package/src/ruvocal/src/routes/api/v2/public-config/+server.ts +7 -7
  439. package/src/ruvocal/src/routes/api/v2/user/+server.ts +17 -17
  440. package/src/ruvocal/src/routes/api/v2/user/billing-orgs/+server.ts +73 -73
  441. package/src/ruvocal/src/routes/api/v2/user/reports/+server.ts +17 -17
  442. package/src/ruvocal/src/routes/api/v2/user/settings/+server.ts +110 -110
  443. package/src/ruvocal/src/routes/conversation/+server.ts +115 -115
  444. package/src/ruvocal/src/routes/conversation/[id]/+page.svelte +586 -586
  445. package/src/ruvocal/src/routes/conversation/[id]/+page.ts +60 -60
  446. package/src/ruvocal/src/routes/conversation/[id]/+server.ts +740 -740
  447. package/src/ruvocal/src/routes/conversation/[id]/message/[messageId]/prompt/+server.ts +66 -66
  448. package/src/ruvocal/src/routes/conversation/[id]/share/+server.ts +69 -69
  449. package/src/ruvocal/src/routes/conversation/[id]/stop-generating/+server.ts +35 -35
  450. package/src/ruvocal/src/routes/healthcheck/+server.ts +3 -3
  451. package/src/ruvocal/src/routes/login/+server.ts +5 -5
  452. package/src/ruvocal/src/routes/login/callback/+server.ts +103 -103
  453. package/src/ruvocal/src/routes/login/callback/updateUser.spec.ts +157 -157
  454. package/src/ruvocal/src/routes/login/callback/updateUser.ts +215 -215
  455. package/src/ruvocal/src/routes/logout/+server.ts +18 -18
  456. package/src/ruvocal/src/routes/metrics/+server.ts +18 -18
  457. package/src/ruvocal/src/routes/models/+page.svelte +233 -233
  458. package/src/ruvocal/src/routes/models/[...model]/+page.svelte +161 -161
  459. package/src/ruvocal/src/routes/models/[...model]/+page.ts +14 -14
  460. package/src/ruvocal/src/routes/models/[...model]/thumbnail.png/+server.ts +64 -64
  461. package/src/ruvocal/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte +28 -28
  462. package/src/ruvocal/src/routes/privacy/+page.svelte +11 -11
  463. package/src/ruvocal/src/routes/r/[id]/+page.ts +34 -34
  464. package/src/ruvocal/src/routes/settings/(nav)/+layout.svelte +282 -282
  465. package/src/ruvocal/src/routes/settings/(nav)/+layout.ts +1 -1
  466. package/src/ruvocal/src/routes/settings/(nav)/+server.ts +59 -59
  467. package/src/ruvocal/src/routes/settings/(nav)/[...model]/+page.svelte +464 -464
  468. package/src/ruvocal/src/routes/settings/(nav)/[...model]/+page.ts +14 -14
  469. package/src/ruvocal/src/routes/settings/(nav)/application/+page.svelte +362 -362
  470. package/src/ruvocal/src/routes/settings/+layout.svelte +40 -40
  471. package/src/ruvocal/src/styles/highlight-js.css +195 -195
  472. package/src/ruvocal/src/styles/main.css +144 -144
  473. package/src/ruvocal/static/chatui/favicon-dark.svg +3 -3
  474. package/src/ruvocal/static/chatui/favicon-dev.svg +3 -3
  475. package/src/ruvocal/static/chatui/favicon.svg +3 -3
  476. package/src/ruvocal/static/chatui/icon.svg +3 -3
  477. package/src/ruvocal/static/chatui/logo.svg +7 -7
  478. package/src/ruvocal/static/chatui/manifest.json +54 -54
  479. package/src/ruvocal/static/chatui/welcome.js +184 -184
  480. package/src/ruvocal/static/huggingchat/favicon-dark.svg +4 -4
  481. package/src/ruvocal/static/huggingchat/favicon-dev.svg +4 -4
  482. package/src/ruvocal/static/huggingchat/favicon.svg +4 -4
  483. package/src/ruvocal/static/huggingchat/fulltext-logo.svg +1 -1
  484. package/src/ruvocal/static/huggingchat/icon.svg +4 -4
  485. package/src/ruvocal/static/huggingchat/logo.svg +4 -4
  486. package/src/ruvocal/static/huggingchat/manifest.json +54 -54
  487. package/src/ruvocal/static/huggingchat/routes.chat.json +226 -226
  488. package/src/ruvocal/static/robots.txt +10 -10
  489. package/src/ruvocal/static/wasm/rvagent_wasm.js +1539 -1539
  490. package/src/ruvocal/stub/@reflink/reflink/package.json +5 -5
  491. package/src/ruvocal/svelte.config.js +53 -53
  492. package/src/ruvocal/tailwind.config.cjs +30 -30
  493. package/src/ruvocal/tsconfig.json +19 -19
  494. package/src/ruvocal/vite.config.ts +87 -87
  495. package/src/scripts/deploy.sh +116 -116
  496. package/src/scripts/generate-config.js +245 -245
  497. package/src/scripts/generate-welcome.js +187 -187
  498. 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
+ }