ocean-brain 0.8.2 → 0.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 (244) hide show
  1. package/README.md +2 -0
  2. package/dist/mcp.js +103 -105
  3. package/package.json +3 -2
  4. package/server/client/dist/assets/{ArrowRight.es-g2YZTklZ.js → ArrowRight.es-BEhnlwfe.js} +1 -1
  5. package/server/client/dist/assets/AuxiliaryPanelHeader-B1k7pZpn.js +1 -0
  6. package/server/client/dist/assets/Calendar-D7QJsAlg.js +25 -0
  7. package/server/client/dist/assets/{Callout-B_IPSF_5.js → Callout-BTd1BYtV.js} +1 -1
  8. package/server/client/dist/assets/{Copy.es-CNxe4ujr.js → Copy.es-BoLjilxv.js} +1 -1
  9. package/server/client/dist/assets/File.es-Ct7fscBg.js +1 -0
  10. package/server/client/dist/assets/Graph-DYUb7gQb.js +46 -0
  11. package/server/client/dist/assets/{Graph-D9GeO71k.js → Graph-LY_IgMYy.js} +1 -1
  12. package/server/client/dist/assets/{Image.es-DnON8cv2.js → Image.es-OYyN3BYt.js} +1 -1
  13. package/server/client/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  14. package/server/client/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  15. package/server/client/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  16. package/server/client/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  17. package/server/client/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  18. package/server/client/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  19. package/server/client/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  20. package/server/client/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  21. package/server/client/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  22. package/server/client/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  23. package/server/client/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  24. package/server/client/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  25. package/server/client/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  26. package/server/client/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  27. package/server/client/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  28. package/server/client/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  29. package/server/client/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  30. package/server/client/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  31. package/server/client/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  32. package/server/client/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  33. package/server/client/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  34. package/server/client/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  35. package/server/client/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  36. package/server/client/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  37. package/server/client/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  38. package/server/client/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  39. package/server/client/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  40. package/server/client/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  41. package/server/client/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  42. package/server/client/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  43. package/server/client/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  44. package/server/client/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  45. package/server/client/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  46. package/server/client/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  47. package/server/client/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  48. package/server/client/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  49. package/server/client/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  50. package/server/client/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  51. package/server/client/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  52. package/server/client/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  53. package/server/client/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  54. package/server/client/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  55. package/server/client/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  56. package/server/client/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  57. package/server/client/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  58. package/server/client/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  59. package/server/client/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  60. package/server/client/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  61. package/server/client/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  62. package/server/client/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  63. package/server/client/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  64. package/server/client/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  65. package/server/client/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  66. package/server/client/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  67. package/server/client/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  68. package/server/client/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  69. package/server/client/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  70. package/server/client/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  71. package/server/client/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  72. package/server/client/dist/assets/Note-DsMhwrHw.js +25 -0
  73. package/server/client/dist/assets/NotePropertiesPanel-aCKXAe5F.js +2 -0
  74. package/server/client/dist/assets/PencilLine.es-0JpEgxFY.js +1 -0
  75. package/server/client/dist/assets/{Plus.es-CQ8wn2is.js → Plus.es-DRfLW_bI.js} +1 -1
  76. package/server/client/dist/assets/Reminders-COr1OHvF.js +1 -0
  77. package/server/client/dist/assets/Search-Ctkx0BQk.js +31 -0
  78. package/server/client/dist/assets/SurfaceCard-t7pdfhEe.js +1 -0
  79. package/server/client/dist/assets/Tag-BjxtrIQW.js +1 -0
  80. package/server/client/dist/assets/TagNotes-DV_fdRbg.js +1 -0
  81. package/server/client/dist/assets/{Trash.es-0aPpA-3S.js → Trash.es-Bo1VyK9Q.js} +1 -1
  82. package/server/client/dist/assets/ViewChip-DlCkL4z4.js +1 -0
  83. package/server/client/dist/assets/ViewNotes-wWWeFJyU.js +1 -0
  84. package/server/client/dist/assets/ViewSectionDialog-DhH8harC.js +102 -0
  85. package/server/client/dist/assets/Views-CWE99SnI.js +1 -0
  86. package/server/client/dist/assets/abnfDiagram-VRR7QNED-CWlV5dXw.js +1 -0
  87. package/server/client/dist/assets/app-B9WZRrAH.css +1 -0
  88. package/server/client/dist/assets/app-BpSMjzET.js +26 -0
  89. package/server/client/dist/assets/arc-BV-n6RUX.js +1 -0
  90. package/server/client/dist/assets/architectureDiagram-ZJ3FMSHR-CekzMXdu.js +36 -0
  91. package/server/client/dist/assets/blockDiagram-677ZJIJ3-B49yHoxq.js +132 -0
  92. package/server/client/dist/assets/c4Diagram-LMCZKHZV-0AfB9NBC.js +10 -0
  93. package/server/client/dist/assets/channel-CigOulR-.js +1 -0
  94. package/server/client/dist/assets/chunk-2Q5K7J3B-DaPQAU1q.js +1 -0
  95. package/server/client/dist/assets/chunk-32BRIVSS-CfL3kZAg.js +1 -0
  96. package/server/client/dist/assets/chunk-5VM5RSS4-DeOwWPE_.js +15 -0
  97. package/server/client/dist/assets/chunk-EX3LRPZG-C3W1oc9y.js +231 -0
  98. package/server/client/dist/assets/chunk-JWPE2WC7-_Oqh79OY.js +1 -0
  99. package/server/client/dist/assets/chunk-MOJQB5TN-DDKNNzI6.js +88 -0
  100. package/server/client/dist/assets/chunk-RYQCIY6F-DCHah7CC.js +1 -0
  101. package/server/client/dist/assets/chunk-V7JOEXUC-uUOIzgxj.js +206 -0
  102. package/server/client/dist/assets/chunk-VR4S4FIN-DDVw2WPs.js +1 -0
  103. package/server/client/dist/assets/chunk-XXDRQBXY-Coo4UFm2.js +1 -0
  104. package/server/client/dist/assets/classDiagram-OUVF2IWQ-D13ISIOW.js +1 -0
  105. package/server/client/dist/assets/classDiagram-v2-EOCWNBFH-D13ISIOW.js +1 -0
  106. package/server/client/dist/assets/colors-Cc3OSVma.js +1 -0
  107. package/server/client/dist/assets/cose-bilkent-JH36ORCC-C3pHmHpD.js +1 -0
  108. package/server/client/dist/assets/cynefin-VYW2F7L2-DU_G4Atc.js +166 -0
  109. package/server/client/dist/assets/cynefinDiagram-TSTJHNR4-DZ6iiJFo.js +62 -0
  110. package/server/client/dist/assets/cytoscape.esm-D3_iZ_3b.js +321 -0
  111. package/server/client/dist/assets/dagre-VKFMJZFB-DNELbhwC.js +4 -0
  112. package/server/client/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  113. package/server/client/dist/assets/diagram-FQU43EPY-CMEvZJ61.js +3 -0
  114. package/server/client/dist/assets/diagram-G47NLZAW-BYDtf6j9.js +24 -0
  115. package/server/client/dist/assets/diagram-NH7WQ7WH-CmXfcCQA.js +24 -0
  116. package/server/client/dist/assets/diagram-OA4YK3LP-BflTKJxU.js +30 -0
  117. package/server/client/dist/assets/diagram-WEI45ONY-KW3fKqna.js +41 -0
  118. package/server/client/dist/assets/ebnfDiagram-CCIWWBDH-CbyhY5Nj.js +1 -0
  119. package/server/client/dist/assets/erDiagram-Q63AITRT-CC2eZV8g.js +85 -0
  120. package/server/client/dist/assets/{floating-ui.react-dom-wWLZ1CdM.js → floating-ui.react-dom-BWTnA00G.js} +1 -1
  121. package/server/client/dist/assets/flowDiagram-23GEKE2U-unplo7Qm.js +156 -0
  122. package/server/client/dist/assets/ganttDiagram-NO4QXBWP-BcBK5HcZ.js +292 -0
  123. package/server/client/dist/assets/gitGraphDiagram-IHSO6WYX-DgVbbb5k.js +106 -0
  124. package/server/client/dist/assets/graph-C5DFNLrC.js +1 -0
  125. package/server/client/dist/assets/image-delete-confirmation-D7xrODQV.js +1 -0
  126. package/server/client/dist/assets/image.api-CyQisInP.js +17 -0
  127. package/server/client/dist/assets/index-B-E1Ilh9.js +1 -0
  128. package/server/client/dist/assets/infoDiagram-FWYZ7A6U-N_D9RdfG.js +2 -0
  129. package/server/client/dist/assets/init-Gi6I4Gst.js +1 -0
  130. package/server/client/dist/assets/isObject-U9U2HXiq.js +1 -0
  131. package/server/client/dist/assets/ishikawaDiagram-FXEZZL3T-BBKrzmB2.js +70 -0
  132. package/server/client/dist/assets/journeyDiagram-5HDEW3XC-NdCxLmQB.js +139 -0
  133. package/server/client/dist/assets/kanban-definition-HUTT4EX6-Ck7t4-tt.js +89 -0
  134. package/server/client/dist/assets/katex-DywYRkvg.css +1 -0
  135. package/server/client/dist/assets/katex-HP8lGamR.js +257 -0
  136. package/server/client/dist/assets/katex-x288kqHd.js +257 -0
  137. package/server/client/dist/assets/layout-Cz0MDlGi.js +1 -0
  138. package/server/client/dist/assets/linear-D6yRgDLq.js +1 -0
  139. package/server/client/dist/assets/manage-image-azmo5iVw.js +1 -0
  140. package/server/client/dist/assets/manage-image-detail-BF-uSqb_.js +1 -0
  141. package/server/client/dist/assets/map-Mm1V-_Na.js +1 -0
  142. package/server/client/dist/assets/{mcp-CT_9IV2c.js → mcp-CCaFd0zm.js} +4 -4
  143. package/server/client/dist/assets/{mcp-admin-adapter-xAuyZc_M.js → mcp-admin-adapter-PMmgPupc.js} +1 -1
  144. package/server/client/dist/assets/mermaid.core-B14ZAmWA.js +308 -0
  145. package/server/client/dist/assets/min-tv6RD-6E.js +1 -0
  146. package/server/client/dist/assets/mindmap-definition-LN4V7U3C-BTUqSdcP.js +96 -0
  147. package/server/client/dist/assets/{note-core-Buwzyxi7.js → note-core-DUTqUh5N.js} +2 -2
  148. package/server/client/dist/assets/{note-runtime-CPDVOFEo.js → note-runtime-zN5ddTba.js} +1 -1
  149. package/server/client/dist/assets/{note-ui-CHI-tzj-.js → note-ui-CtijppHK.js} +1 -1
  150. package/server/client/dist/assets/{note.api-D_00ZzJC.js → note.api-D3oJmG3q.js} +6 -6
  151. package/server/client/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  152. package/server/client/dist/assets/pegDiagram-2B236MQR-Cjcu3mVz.js +1 -0
  153. package/server/client/dist/assets/pieDiagram-ENE6RG2P-BxI_c22Z.js +39 -0
  154. package/server/client/dist/assets/{placeholder-DbdGiYGL.js → placeholder-AusSo8p9.js} +4 -4
  155. package/server/client/dist/assets/properties-BrNJLkxR.js +1 -0
  156. package/server/client/dist/assets/quadrantDiagram-ABIIQ3AL-DGm0a-yR.js +7 -0
  157. package/server/client/dist/assets/railroadDiagram-RFXS5EU6-DmCDu89O.js +1 -0
  158. package/server/client/dist/assets/requirementDiagram-TGXJPOKE-CBpUt7Ig.js +84 -0
  159. package/server/client/dist/assets/route-preload.js +2 -2
  160. package/server/client/dist/assets/sankeyDiagram-HTMAVEWB-BjGnDZlp.js +40 -0
  161. package/server/client/dist/assets/search-D-xKTPdO.js +1 -0
  162. package/server/client/dist/assets/search-admin-adapter-CJnJmL6w.js +1 -0
  163. package/server/client/dist/assets/sequenceDiagram-DBY2YBRQ-B7rpCfS6.js +162 -0
  164. package/server/client/dist/assets/sizeCapture-X5ZJPWSS-CUJhD0uW.js +1 -0
  165. package/server/client/dist/assets/stateDiagram-2N3HPSRC-Y9-3XqRX.js +1 -0
  166. package/server/client/dist/assets/stateDiagram-v2-6OUMAXLB-DbHXhT02.js +1 -0
  167. package/server/client/dist/assets/swimlanes-5IMT3BWC-Cs1NO9ng.js +2 -0
  168. package/server/client/dist/assets/swimlanesDiagram-G3AALYLV-DQnrq2ZC.js +8 -0
  169. package/server/client/dist/assets/{tag.api-CX0SE06J.js → tag.api-DNSQpqwk.js} +3 -3
  170. package/server/client/dist/assets/time-D5LRMumI.js +9 -0
  171. package/server/client/dist/assets/timeline-definition-FHXFAJF6-8fqD4WNL.js +120 -0
  172. package/server/client/dist/assets/toNumber-DE3J3kMD.js +1 -0
  173. package/server/client/dist/assets/transform-BUB79_za.js +1 -0
  174. package/server/client/dist/assets/trash-D4-9sv8q.js +1 -0
  175. package/server/client/dist/assets/url-9nrbQs7Z.js +1 -0
  176. package/server/client/dist/assets/{useReminderMutate-CLK1tHvT.js → useReminderMutate-BrGO9M0o.js} +4 -4
  177. package/server/client/dist/assets/vennDiagram-L72KCM5P-BPqKkd5a.js +34 -0
  178. package/server/client/dist/assets/wardleyDiagram-EHGQE667-Cq11kU2M.js +78 -0
  179. package/server/client/dist/assets/xychartDiagram-FW5EYKEG-DJ6UzFj-.js +7 -0
  180. package/server/client/dist/index.html +7 -7
  181. package/server/dist/features/image/services/delete.js +25 -1
  182. package/server/dist/features/note/graphql/note.field.resolver.js +2 -0
  183. package/server/dist/features/note/graphql/note.mutation.resolver.js +54 -23
  184. package/server/dist/features/note/graphql/note.query.resolver.js +15 -5
  185. package/server/dist/features/note/graphql/note.type-defs.js +2 -1
  186. package/server/dist/features/note/http/mcp.js +1 -60
  187. package/server/dist/features/note/services/authoring.js +17 -80
  188. package/server/dist/features/note/services/markdown-patch.js +80 -8
  189. package/server/dist/features/note/services/note-reference-index.js +98 -0
  190. package/server/dist/features/note/services/properties.js +21 -1
  191. package/server/dist/features/note/services/property-restore.js +26 -0
  192. package/server/dist/features/note/services/search.js +5 -0
  193. package/server/dist/features/note/services/snapshot.js +82 -58
  194. package/server/dist/features/note/services/trash.js +18 -31
  195. package/server/dist/features/note/services/write.js +10 -3
  196. package/server/dist/features/search/embedding-api-key-store.js +57 -0
  197. package/server/dist/features/search/embedding-client.js +179 -0
  198. package/server/dist/features/search/graphql/index.js +6 -0
  199. package/server/dist/features/search/graphql/resolver.js +32 -0
  200. package/server/dist/features/search/graphql/type-defs.js +37 -0
  201. package/server/dist/features/search/http/handlers.js +108 -0
  202. package/server/dist/features/search/hybrid-ranking.js +57 -0
  203. package/server/dist/features/search/hybrid-search.js +176 -0
  204. package/server/dist/features/search/note-change.js +16 -0
  205. package/server/dist/features/search/note-chunking.js +75 -0
  206. package/server/dist/features/search/related-notes.js +186 -0
  207. package/server/dist/features/search/search-config.js +150 -0
  208. package/server/dist/features/search/search-manager.js +431 -0
  209. package/server/dist/features/search/semantic-indexer.js +138 -0
  210. package/server/dist/features/search/sqlite-vector-index.js +636 -0
  211. package/server/dist/features/search/url-normalization.js +10 -0
  212. package/server/dist/features/tag/services/organization.js +3 -0
  213. package/server/dist/features/view/services/workspace.js +1 -1
  214. package/server/dist/main.js +12 -3
  215. package/server/dist/modules/blocknote.js +4 -5
  216. package/server/dist/paths.js +7 -2
  217. package/server/dist/routes/api.js +37 -0
  218. package/server/dist/routes/mcp.js +0 -5
  219. package/server/dist/schema/index.js +3 -0
  220. package/server/prisma/migrations/20260728090000_0019_note_reference_index/migration.sql +15 -0
  221. package/server/prisma/schema.prisma +16 -0
  222. package/server/client/dist/assets/Calendar-BP6HTuSn.js +0 -25
  223. package/server/client/dist/assets/Graph-Xha3fwhp.js +0 -46
  224. package/server/client/dist/assets/Image-BLEjLUqd.js +0 -1
  225. package/server/client/dist/assets/Note-DjrCYtat.js +0 -24
  226. package/server/client/dist/assets/NotePropertiesPanel-2DgXEM4B.js +0 -2
  227. package/server/client/dist/assets/Reminders-B4l3KdlK.js +0 -1
  228. package/server/client/dist/assets/Search-BN5sf7bE.js +0 -1
  229. package/server/client/dist/assets/SurfaceCard-BZm73nyS.js +0 -1
  230. package/server/client/dist/assets/Tag-oSnmViVp.js +0 -1
  231. package/server/client/dist/assets/TagNotes-BDTCcJcL.js +0 -1
  232. package/server/client/dist/assets/ViewNotes-CiZtGJNE.js +0 -1
  233. package/server/client/dist/assets/ViewSectionDialog-Ctl_694Y.js +0 -102
  234. package/server/client/dist/assets/Views-D-zlatYD.js +0 -1
  235. package/server/client/dist/assets/app-DmqIOMWF.css +0 -1
  236. package/server/client/dist/assets/app-cTRcKMGc.js +0 -26
  237. package/server/client/dist/assets/image.api-CVuuvTBk.js +0 -17
  238. package/server/client/dist/assets/index-DKJChPr4.js +0 -1
  239. package/server/client/dist/assets/manage-image-BZjaxp-f.js +0 -1
  240. package/server/client/dist/assets/manage-image-detail-DPv0FQ2m.js +0 -1
  241. package/server/client/dist/assets/properties-D447wsWk.js +0 -1
  242. package/server/client/dist/assets/time-B3pt8vt4.js +0 -9
  243. package/server/client/dist/assets/trash-8sH7EZgc.js +0 -1
  244. package/server/client/dist/assets/url-BSnWpmJx.js +0 -1
@@ -0,0 +1,176 @@
1
+ import {
2
+ buildNoteSearchText,
3
+ NOTE_SEARCH_TEXT_SCHEMA_VERSION,
4
+ parseNoteSearchQuery
5
+ } from "../../features/note/services/search.js";
6
+ import models from "../../models.js";
7
+ import { runDataMaintenanceInBackground } from "../../modules/data-maintenance.js";
8
+ import { fuseHybridSearchRanks } from "./hybrid-ranking.js";
9
+ import { getDefaultSemanticSearchManager } from "./search-manager.js";
10
+ const MAX_HYBRID_CANDIDATES = 80;
11
+ const MAX_HYBRID_SEMANTIC_CANDIDATES = 30;
12
+ const unavailableSemanticAttempt = (error) => ({
13
+ available: false,
14
+ used: false,
15
+ matches: [],
16
+ error: error instanceof Error ? error.message : "Semantic search is unavailable."
17
+ });
18
+ const buildLexicalWhere = (query) => {
19
+ const parsedQuery = parseNoteSearchQuery(query);
20
+ if (!parsedQuery.hasFilters) {
21
+ return void 0;
22
+ }
23
+ return {
24
+ AND: [
25
+ { searchableTextVersion: NOTE_SEARCH_TEXT_SCHEMA_VERSION },
26
+ ...parsedQuery.included.length > 0 ? [
27
+ {
28
+ OR: parsedQuery.included.map((term) => ({ searchableText: { contains: term } }))
29
+ }
30
+ ] : [],
31
+ ...parsedQuery.excluded.map((term) => ({ NOT: { searchableText: { contains: term } } }))
32
+ ]
33
+ };
34
+ };
35
+ const matchesHybridLexicalQuery = (searchableText, query) => {
36
+ const parsedQuery = parseNoteSearchQuery(query);
37
+ const normalizedSearchableText = searchableText.toLowerCase();
38
+ const includesAnyTerm = parsedQuery.included.length === 0 || parsedQuery.included.some((term) => normalizedSearchableText.includes(term));
39
+ return includesAnyTerm && parsedQuery.excluded.every((term) => !normalizedSearchableText.includes(term));
40
+ };
41
+ const scoreLexicalCandidate = (candidate, query) => {
42
+ const parsedQuery = parseNoteSearchQuery(query);
43
+ const phrase = parsedQuery.included.join(" ");
44
+ const title = candidate.title.toLowerCase();
45
+ const searchableText = candidate.searchableText.toLowerCase();
46
+ const matchingTermCount = parsedQuery.included.filter((term) => searchableText.includes(term)).length;
47
+ let score = 0;
48
+ if (phrase && title === phrase) {
49
+ score += 100;
50
+ } else if (phrase && title.includes(phrase)) {
51
+ score += 40;
52
+ }
53
+ if (phrase && searchableText.includes(phrase)) {
54
+ score += 15;
55
+ }
56
+ if (parsedQuery.included.length > 1 && matchingTermCount === parsedQuery.included.length) {
57
+ score += 20;
58
+ }
59
+ for (const term of parsedQuery.included) {
60
+ if (title.includes(term)) {
61
+ score += 8;
62
+ }
63
+ if (searchableText.includes(term)) {
64
+ score += 1;
65
+ }
66
+ }
67
+ return score;
68
+ };
69
+ const rankLexicalCandidates = (candidates, query) => {
70
+ return [...candidates].sort((left, right) => {
71
+ const scoreDifference = scoreLexicalCandidate(right, query) - scoreLexicalCandidate(left, query);
72
+ if (scoreDifference !== 0) {
73
+ return scoreDifference;
74
+ }
75
+ const updatedAtDifference = right.updatedAt.getTime() - left.updatedAt.getTime();
76
+ return updatedAtDifference !== 0 ? updatedAtDifference : left.id - right.id;
77
+ });
78
+ };
79
+ const listDefaultLexicalNoteIds = async (query, limit) => {
80
+ const where = buildLexicalWhere(query);
81
+ if (!where) {
82
+ return [];
83
+ }
84
+ const [freshCandidates, staleCandidates] = await Promise.all([
85
+ models.note.findMany({
86
+ where,
87
+ select: {
88
+ id: true,
89
+ title: true,
90
+ searchableText: true,
91
+ updatedAt: true
92
+ }
93
+ }),
94
+ models.note.findMany({
95
+ where: { searchableTextVersion: { not: NOTE_SEARCH_TEXT_SCHEMA_VERSION } },
96
+ select: {
97
+ id: true,
98
+ title: true,
99
+ content: true,
100
+ searchableText: true,
101
+ updatedAt: true
102
+ }
103
+ })
104
+ ]);
105
+ if (staleCandidates.length > 0) {
106
+ void runDataMaintenanceInBackground();
107
+ }
108
+ const matchingStaleCandidates = staleCandidates.map((candidate) => ({
109
+ ...candidate,
110
+ searchableText: buildNoteSearchText(candidate)
111
+ })).filter((candidate) => matchesHybridLexicalQuery(candidate.searchableText, query));
112
+ return rankLexicalCandidates([...freshCandidates, ...matchingStaleCandidates], query).slice(0, limit).map((candidate) => candidate.id);
113
+ };
114
+ const defaultDependencies = {
115
+ listLexicalNoteIds: listDefaultLexicalNoteIds,
116
+ trySemanticSearch: (query, limit) => getDefaultSemanticSearchManager().trySearch(query, limit),
117
+ findNotesByIds: (ids) => models.note.findMany({ where: { id: { in: ids } } })
118
+ };
119
+ const createHybridNoteSearch = (dependencies = defaultDependencies) => {
120
+ return async ({
121
+ query,
122
+ limit,
123
+ offset,
124
+ mode = "hybrid"
125
+ }) => {
126
+ const normalizedQuery = query.trim();
127
+ if (!normalizedQuery || limit <= 0 || offset < 0) {
128
+ return {
129
+ totalCount: 0,
130
+ notes: [],
131
+ matches: [],
132
+ semanticAvailable: false,
133
+ semanticUsed: false,
134
+ semanticError: null
135
+ };
136
+ }
137
+ const candidateLimit = MAX_HYBRID_CANDIDATES;
138
+ const semanticCandidateLimit = mode === "hybrid" ? MAX_HYBRID_SEMANTIC_CANDIDATES : MAX_HYBRID_CANDIDATES;
139
+ const [lexicalNoteIds, semanticAttempt] = await Promise.all([
140
+ mode === "semantic" ? Promise.resolve([]) : dependencies.listLexicalNoteIds(normalizedQuery, candidateLimit),
141
+ mode === "lexical" ? Promise.resolve({
142
+ available: false,
143
+ used: false,
144
+ matches: [],
145
+ error: null
146
+ }) : dependencies.trySemanticSearch(normalizedQuery, semanticCandidateLimit).catch((error) => unavailableSemanticAttempt(error))
147
+ ]);
148
+ const rankedCandidates = fuseHybridSearchRanks({
149
+ lexicalNoteIds,
150
+ semanticNoteIds: semanticAttempt.matches.slice(0, semanticCandidateLimit).map((match) => match.noteId)
151
+ });
152
+ const pageCandidates = rankedCandidates.slice(offset, offset + limit);
153
+ const notes = await dependencies.findNotesByIds(pageCandidates.map((candidate) => candidate.noteId));
154
+ const notesById = new Map(notes.map((note) => [note.id, note]));
155
+ const existingCandidates = pageCandidates.filter((candidate) => notesById.has(candidate.noteId));
156
+ return {
157
+ totalCount: rankedCandidates.length,
158
+ notes: existingCandidates.map((candidate) => notesById.get(candidate.noteId)),
159
+ matches: existingCandidates.map((candidate) => ({
160
+ noteId: candidate.noteId,
161
+ lexical: candidate.lexicalRank !== null,
162
+ semantic: candidate.semanticRank !== null
163
+ })),
164
+ semanticAvailable: semanticAttempt.available,
165
+ semanticUsed: semanticAttempt.used,
166
+ semanticError: semanticAttempt.error
167
+ };
168
+ };
169
+ };
170
+ const searchNotesHybrid = createHybridNoteSearch();
171
+ export {
172
+ createHybridNoteSearch,
173
+ matchesHybridLexicalQuery,
174
+ rankLexicalCandidates,
175
+ searchNotesHybrid
176
+ };
@@ -0,0 +1,16 @@
1
+ import { EventEmitter } from "node:events";
2
+ const NOTE_CHANGE_CHANNEL = "semantic-search-note-change";
3
+ const noteChangeEmitter = new EventEmitter();
4
+ const notifySemanticSearchNoteChanged = (noteId) => {
5
+ if (Number.isInteger(noteId) && noteId > 0) {
6
+ noteChangeEmitter.emit(NOTE_CHANGE_CHANNEL, noteId);
7
+ }
8
+ };
9
+ const subscribeSemanticSearchNoteChanges = (listener) => {
10
+ noteChangeEmitter.on(NOTE_CHANGE_CHANNEL, listener);
11
+ return () => noteChangeEmitter.off(NOTE_CHANGE_CHANNEL, listener);
12
+ };
13
+ export {
14
+ notifySemanticSearchNoteChanged,
15
+ subscribeSemanticSearchNoteChanges
16
+ };
@@ -0,0 +1,75 @@
1
+ import { createHash } from "node:crypto";
2
+ import { extractVisibleSearchTextFromContent } from "../../features/note/services/search.js";
3
+ const NOTE_EMBEDDING_TEXT_SCHEMA_VERSION = 1;
4
+ const DEFAULT_NOTE_CHUNK_MAX_LENGTH = 900;
5
+ const DEFAULT_NOTE_CHUNK_OVERLAP = 120;
6
+ const normalizeText = (value) => value.replace(/\s+/g, " ").trim();
7
+ const findWindowEnd = (text, start, maxLength) => {
8
+ const desiredEnd = Math.min(text.length, start + maxLength);
9
+ if (desiredEnd === text.length) {
10
+ return desiredEnd;
11
+ }
12
+ const minimumEnd = start + Math.floor(maxLength * 0.7);
13
+ const breakAt = text.lastIndexOf(" ", desiredEnd);
14
+ return breakAt >= minimumEnd ? breakAt : desiredEnd;
15
+ };
16
+ const findNextWindowStart = (text, start, end, overlap) => {
17
+ const desiredStart = Math.max(start + 1, end - overlap);
18
+ const nextSpace = text.indexOf(" ", desiredStart);
19
+ if (nextSpace === -1 || nextSpace >= end) {
20
+ return desiredStart;
21
+ }
22
+ return nextSpace + 1;
23
+ };
24
+ const splitSearchText = (text, options = {}) => {
25
+ const normalizedText = normalizeText(text);
26
+ if (!normalizedText) {
27
+ return [];
28
+ }
29
+ const maxLength = options.maxLength ?? DEFAULT_NOTE_CHUNK_MAX_LENGTH;
30
+ const overlap = options.overlap ?? DEFAULT_NOTE_CHUNK_OVERLAP;
31
+ if (!Number.isInteger(maxLength) || maxLength < 100) {
32
+ throw new Error("Note chunk maxLength must be an integer of at least 100.");
33
+ }
34
+ if (!Number.isInteger(overlap) || overlap < 0 || overlap >= maxLength) {
35
+ throw new Error("Note chunk overlap must be a non-negative integer smaller than maxLength.");
36
+ }
37
+ const chunks = [];
38
+ let start = 0;
39
+ while (start < normalizedText.length) {
40
+ const end = findWindowEnd(normalizedText, start, maxLength);
41
+ const chunk = normalizedText.slice(start, end).trim();
42
+ if (chunk) {
43
+ chunks.push(chunk);
44
+ }
45
+ if (end >= normalizedText.length) {
46
+ break;
47
+ }
48
+ start = findNextWindowStart(normalizedText, start, end, overlap);
49
+ }
50
+ return chunks;
51
+ };
52
+ const buildSourceHash = (title, body) => {
53
+ return createHash("sha256").update(`${NOTE_EMBEDDING_TEXT_SCHEMA_VERSION}\0${title}\0${body}`).digest("hex");
54
+ };
55
+ const buildNoteEmbeddingChunks = (note, options = {}) => {
56
+ const title = normalizeText(note.title);
57
+ const body = normalizeText(extractVisibleSearchTextFromContent(note.content));
58
+ const bodyChunks = splitSearchText(body, options);
59
+ const searchableChunks = bodyChunks.length > 0 ? bodyChunks : title ? [""] : [];
60
+ const sourceHash = buildSourceHash(title, body);
61
+ return searchableChunks.map((chunk, chunkIndex) => ({
62
+ noteId: note.id,
63
+ chunkIndex,
64
+ sourceHash,
65
+ text: title ? `Title: ${title}
66
+ Content: ${chunk}`.trimEnd() : chunk
67
+ }));
68
+ };
69
+ export {
70
+ DEFAULT_NOTE_CHUNK_MAX_LENGTH,
71
+ DEFAULT_NOTE_CHUNK_OVERLAP,
72
+ NOTE_EMBEDDING_TEXT_SCHEMA_VERSION,
73
+ buildNoteEmbeddingChunks,
74
+ splitSearchText
75
+ };
@@ -0,0 +1,186 @@
1
+ import models, { Prisma } from "../../models.js";
2
+ const MAX_RELATED_NOTES = 5;
3
+ const normalizeLimit = (limit) => Math.min(MAX_RELATED_NOTES, Math.max(0, Math.trunc(limit)));
4
+ const toDate = (value) => value instanceof Date ? value : new Date(value);
5
+ const buildReasons = (candidate) => {
6
+ const reasons = [];
7
+ if (candidate.linked) {
8
+ reasons.push("Linked from this note");
9
+ }
10
+ if (candidate.backlink) {
11
+ reasons.push("Backlink to this note");
12
+ }
13
+ for (const tagName of candidate.sharedTagNames) {
14
+ reasons.push(`Shares ${tagName}`);
15
+ }
16
+ return reasons;
17
+ };
18
+ const compareCandidates = (left, right) => {
19
+ const leftScore = (left.linked ? 100 : 0) + (left.backlink ? 100 : 0) + left.sharedTagNames.length * 20;
20
+ const rightScore = (right.linked ? 100 : 0) + (right.backlink ? 100 : 0) + right.sharedTagNames.length * 20;
21
+ if (leftScore !== rightScore) {
22
+ return rightScore - leftScore;
23
+ }
24
+ const updatedAtDifference = right.updatedAt.getTime() - left.updatedAt.getTime();
25
+ return updatedAtDifference !== 0 ? updatedAtDifference : left.id - right.id;
26
+ };
27
+ const findCandidatesFromIndex = async ({
28
+ noteId,
29
+ tagIds,
30
+ limit
31
+ }) => {
32
+ const rows = tagIds.length === 0 ? await models.$queryRaw(Prisma.sql`
33
+ WITH candidate_scores AS (
34
+ SELECT
35
+ note."id" AS "id",
36
+ note."title" AS "title",
37
+ note."updatedAt" AS "updatedAt",
38
+ CASE WHEN EXISTS (
39
+ SELECT 1
40
+ FROM "NoteReference" AS outgoing
41
+ WHERE outgoing."sourceNoteId" = ${noteId}
42
+ AND outgoing."targetNoteId" = note."id"
43
+ ) THEN 1 ELSE 0 END AS "linked",
44
+ CASE WHEN EXISTS (
45
+ SELECT 1
46
+ FROM "NoteReference" AS incoming
47
+ WHERE incoming."targetNoteId" = ${noteId}
48
+ AND incoming."sourceNoteId" = note."id"
49
+ ) THEN 1 ELSE 0 END AS "backlink",
50
+ 0 AS "sharedTagCount"
51
+ FROM "Note" AS note
52
+ WHERE note."id" <> ${noteId}
53
+ AND (
54
+ EXISTS (
55
+ SELECT 1
56
+ FROM "NoteReference" AS outgoing
57
+ WHERE outgoing."sourceNoteId" = ${noteId}
58
+ AND outgoing."targetNoteId" = note."id"
59
+ )
60
+ OR EXISTS (
61
+ SELECT 1
62
+ FROM "NoteReference" AS incoming
63
+ WHERE incoming."targetNoteId" = ${noteId}
64
+ AND incoming."sourceNoteId" = note."id"
65
+ )
66
+ )
67
+ )
68
+ SELECT *, ("linked" * 100 + "backlink" * 100 + "sharedTagCount" * 20) AS "score"
69
+ FROM candidate_scores
70
+ ORDER BY "score" DESC, "updatedAt" DESC, "id" ASC
71
+ LIMIT ${limit}
72
+ `) : await models.$queryRaw(Prisma.sql`
73
+ WITH candidate_scores AS (
74
+ SELECT
75
+ note."id" AS "id",
76
+ note."title" AS "title",
77
+ note."updatedAt" AS "updatedAt",
78
+ CASE WHEN EXISTS (
79
+ SELECT 1
80
+ FROM "NoteReference" AS outgoing
81
+ WHERE outgoing."sourceNoteId" = ${noteId}
82
+ AND outgoing."targetNoteId" = note."id"
83
+ ) THEN 1 ELSE 0 END AS "linked",
84
+ CASE WHEN EXISTS (
85
+ SELECT 1
86
+ FROM "NoteReference" AS incoming
87
+ WHERE incoming."targetNoteId" = ${noteId}
88
+ AND incoming."sourceNoteId" = note."id"
89
+ ) THEN 1 ELSE 0 END AS "backlink",
90
+ COUNT(DISTINCT noteTags."B") AS "sharedTagCount"
91
+ FROM "Note" AS note
92
+ LEFT JOIN "_NoteToTag" AS noteTags
93
+ ON noteTags."A" = note."id"
94
+ AND noteTags."B" IN (${Prisma.join(tagIds)})
95
+ WHERE note."id" <> ${noteId}
96
+ AND (
97
+ EXISTS (
98
+ SELECT 1
99
+ FROM "NoteReference" AS outgoing
100
+ WHERE outgoing."sourceNoteId" = ${noteId}
101
+ AND outgoing."targetNoteId" = note."id"
102
+ )
103
+ OR EXISTS (
104
+ SELECT 1
105
+ FROM "NoteReference" AS incoming
106
+ WHERE incoming."targetNoteId" = ${noteId}
107
+ AND incoming."sourceNoteId" = note."id"
108
+ )
109
+ OR noteTags."B" IS NOT NULL
110
+ )
111
+ GROUP BY note."id", note."title", note."updatedAt"
112
+ )
113
+ SELECT *, ("linked" * 100 + "backlink" * 100 + "sharedTagCount" * 20) AS "score"
114
+ FROM candidate_scores
115
+ ORDER BY "score" DESC, "updatedAt" DESC, "id" ASC
116
+ LIMIT ${limit}
117
+ `);
118
+ if (rows.length === 0) {
119
+ return [];
120
+ }
121
+ const candidateIds = rows.map((row) => row.id);
122
+ const candidateNotes = await models.note.findMany({
123
+ where: { id: { in: candidateIds } },
124
+ select: {
125
+ id: true,
126
+ tags: {
127
+ select: {
128
+ id: true,
129
+ name: true
130
+ }
131
+ }
132
+ }
133
+ });
134
+ const tagsByNoteId = new Map(candidateNotes.map((note) => [note.id, note.tags]));
135
+ const sourceTagIds = new Set(tagIds);
136
+ return rows.map((row) => ({
137
+ id: row.id,
138
+ title: row.title || "Untitled",
139
+ updatedAt: toDate(row.updatedAt),
140
+ linked: Number(row.linked) === 1,
141
+ backlink: Number(row.backlink) === 1,
142
+ sharedTagNames: (tagsByNoteId.get(row.id) ?? []).filter((tag) => sourceTagIds.has(tag.id)).map((tag) => tag.name).sort((left, right) => left.localeCompare(right))
143
+ }));
144
+ };
145
+ const defaultDependencies = {
146
+ findSourceNote: (noteId) => models.note.findUnique({
147
+ where: { id: noteId },
148
+ select: {
149
+ id: true,
150
+ tags: {
151
+ select: {
152
+ id: true,
153
+ name: true
154
+ }
155
+ }
156
+ }
157
+ }),
158
+ findCandidates: findCandidatesFromIndex
159
+ };
160
+ const createSearchRelatedNotes = (dependencies = defaultDependencies) => {
161
+ return async (noteId, limit = MAX_RELATED_NOTES) => {
162
+ const normalizedLimit = normalizeLimit(limit);
163
+ if (!Number.isSafeInteger(noteId) || noteId <= 0 || normalizedLimit === 0) {
164
+ return [];
165
+ }
166
+ const source = await dependencies.findSourceNote(noteId);
167
+ if (!source) {
168
+ return [];
169
+ }
170
+ const candidates = await dependencies.findCandidates({
171
+ noteId,
172
+ tagIds: source.tags.map((tag) => tag.id),
173
+ limit: normalizedLimit
174
+ });
175
+ return candidates.filter((candidate) => candidate.linked || candidate.backlink || candidate.sharedTagNames.length > 0).sort(compareCandidates).slice(0, normalizedLimit).map((candidate) => ({
176
+ id: candidate.id,
177
+ title: candidate.title || "Untitled",
178
+ reasons: buildReasons(candidate)
179
+ }));
180
+ };
181
+ };
182
+ const searchRelatedNotes = createSearchRelatedNotes();
183
+ export {
184
+ createSearchRelatedNotes,
185
+ searchRelatedNotes
186
+ };
@@ -0,0 +1,150 @@
1
+ import { normalizeEmbeddingApiUrl } from "./embedding-client.js";
2
+ import { stripTrailingSlashes } from "./url-normalization.js";
3
+ const SEMANTIC_SEARCH_CONFIG_CACHE_KEY = "SEMANTIC_SEARCH_CONFIG_V1";
4
+ const SEMANTIC_SEARCH_VALIDATED_CONNECTION_CACHE_KEY = "SEMANTIC_SEARCH_VALIDATED_CONNECTION_V1";
5
+ const LEGACY_KOREAN_QUERY_INSTRUCTION = "Given a vague Korean memory query, retrieve relevant passages from personal notes.";
6
+ const DEFAULT_SEMANTIC_SEARCH_CONFIG = {
7
+ enabled: false,
8
+ baseUrl: "",
9
+ model: "",
10
+ queryInstruction: ""
11
+ };
12
+ const parseStoredConfig = (value) => {
13
+ try {
14
+ const parsed = JSON.parse(value);
15
+ if (typeof parsed.enabled !== "boolean" || typeof parsed.baseUrl !== "string" || typeof parsed.model !== "string" || typeof parsed.queryInstruction !== "string") {
16
+ return null;
17
+ }
18
+ return {
19
+ enabled: parsed.enabled,
20
+ baseUrl: parsed.baseUrl,
21
+ model: parsed.model,
22
+ queryInstruction: parsed.queryInstruction
23
+ };
24
+ } catch {
25
+ return null;
26
+ }
27
+ };
28
+ const readStoredAuthFingerprint = ({
29
+ authFingerprint,
30
+ usesBearerAuth
31
+ }) => {
32
+ if (typeof authFingerprint === "string") {
33
+ return authFingerprint;
34
+ }
35
+ if (usesBearerAuth === false) {
36
+ return "";
37
+ }
38
+ return "legacy-bearer-auth";
39
+ };
40
+ const parseValidatedConnection = (value) => {
41
+ try {
42
+ const parsed = JSON.parse(value);
43
+ if (typeof parsed.baseUrl !== "string" || typeof parsed.model !== "string" || typeof parsed.validatedAt !== "string") {
44
+ return null;
45
+ }
46
+ return {
47
+ baseUrl: parsed.baseUrl,
48
+ model: parsed.model,
49
+ validatedAt: parsed.validatedAt,
50
+ authFingerprint: readStoredAuthFingerprint(parsed)
51
+ };
52
+ } catch {
53
+ return null;
54
+ }
55
+ };
56
+ const normalizeSemanticSearchConfig = (input) => {
57
+ if (input.baseUrl.length > 2048) {
58
+ throw new Error("Embedding API URL is too long.");
59
+ }
60
+ const config = {
61
+ enabled: input.enabled,
62
+ baseUrl: stripTrailingSlashes(input.baseUrl.trim()),
63
+ model: input.model.trim(),
64
+ queryInstruction: input.queryInstruction.trim()
65
+ };
66
+ if (config.baseUrl) {
67
+ normalizeEmbeddingApiUrl(config.baseUrl);
68
+ }
69
+ if (config.enabled && (!config.baseUrl || !config.model)) {
70
+ throw new Error("Embedding API URL and model are required when semantic search is enabled.");
71
+ }
72
+ if (config.model.length > 200) {
73
+ throw new Error("Embedding model name is too long.");
74
+ }
75
+ if (config.queryInstruction.length > 1e3) {
76
+ throw new Error("Embedding query instruction is too long.");
77
+ }
78
+ return config;
79
+ };
80
+ class SemanticSearchConfigStore {
81
+ constructor(cache) {
82
+ this.cache = cache;
83
+ }
84
+ cache;
85
+ async get() {
86
+ const row = await this.cache.findUnique({ where: { key: SEMANTIC_SEARCH_CONFIG_CACHE_KEY } });
87
+ const config = row ? parseStoredConfig(row.value) : null;
88
+ if (!config) {
89
+ return DEFAULT_SEMANTIC_SEARCH_CONFIG;
90
+ }
91
+ if (config.queryInstruction.trim() === LEGACY_KOREAN_QUERY_INSTRUCTION) {
92
+ return this.set({ ...config, queryInstruction: "" });
93
+ }
94
+ return config;
95
+ }
96
+ async set(input) {
97
+ const config = normalizeSemanticSearchConfig(input);
98
+ const value = JSON.stringify(config);
99
+ await this.cache.upsert({
100
+ where: { key: SEMANTIC_SEARCH_CONFIG_CACHE_KEY },
101
+ create: { key: SEMANTIC_SEARCH_CONFIG_CACHE_KEY, value },
102
+ update: { value }
103
+ });
104
+ return config;
105
+ }
106
+ async getValidatedConnection() {
107
+ const row = await this.cache.findUnique({
108
+ where: { key: SEMANTIC_SEARCH_VALIDATED_CONNECTION_CACHE_KEY }
109
+ });
110
+ return row ? parseValidatedConnection(row.value) : null;
111
+ }
112
+ async isConnectionValidated(input, authFingerprint = "") {
113
+ const config = normalizeSemanticSearchConfig({
114
+ ...DEFAULT_SEMANTIC_SEARCH_CONFIG,
115
+ baseUrl: input.baseUrl,
116
+ model: input.model
117
+ });
118
+ const validated = await this.getValidatedConnection();
119
+ return Boolean(
120
+ validated && validated.baseUrl === config.baseUrl && validated.model === config.model && validated.authFingerprint === authFingerprint
121
+ );
122
+ }
123
+ async markConnectionValidated(input, authFingerprint = "") {
124
+ const config = normalizeSemanticSearchConfig({
125
+ ...DEFAULT_SEMANTIC_SEARCH_CONFIG,
126
+ baseUrl: input.baseUrl,
127
+ model: input.model
128
+ });
129
+ const validated = {
130
+ baseUrl: config.baseUrl,
131
+ model: config.model,
132
+ validatedAt: (/* @__PURE__ */ new Date()).toISOString(),
133
+ authFingerprint
134
+ };
135
+ const value = JSON.stringify(validated);
136
+ await this.cache.upsert({
137
+ where: { key: SEMANTIC_SEARCH_VALIDATED_CONNECTION_CACHE_KEY },
138
+ create: { key: SEMANTIC_SEARCH_VALIDATED_CONNECTION_CACHE_KEY, value },
139
+ update: { value }
140
+ });
141
+ return validated;
142
+ }
143
+ }
144
+ export {
145
+ DEFAULT_SEMANTIC_SEARCH_CONFIG,
146
+ SEMANTIC_SEARCH_CONFIG_CACHE_KEY,
147
+ SEMANTIC_SEARCH_VALIDATED_CONNECTION_CACHE_KEY,
148
+ SemanticSearchConfigStore,
149
+ normalizeSemanticSearchConfig
150
+ };