ocean-brain 0.8.2 → 0.10.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 (232) hide show
  1. package/README.md +2 -0
  2. package/dist/mcp.js +47 -75
  3. package/package.json +3 -2
  4. package/server/client/dist/assets/{ArrowRight.es-g2YZTklZ.js → ArrowRight.es-Bm-ZXpEC.js} +1 -1
  5. package/server/client/dist/assets/Calendar-BsNs3NzO.js +25 -0
  6. package/server/client/dist/assets/{Callout-B_IPSF_5.js → Callout-CviyPHKO.js} +1 -1
  7. package/server/client/dist/assets/{Copy.es-CNxe4ujr.js → Copy.es-BPreRnP9.js} +1 -1
  8. package/server/client/dist/assets/{Graph-D9GeO71k.js → Graph-vXQQ0a-Q.js} +1 -1
  9. package/server/client/dist/assets/Graph-yPE3ljW0.js +46 -0
  10. package/server/client/dist/assets/{Image.es-DnON8cv2.js → Image.es-DnR3qc_c.js} +1 -1
  11. package/server/client/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  12. package/server/client/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  13. package/server/client/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  14. package/server/client/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  15. package/server/client/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  16. package/server/client/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  17. package/server/client/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  18. package/server/client/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  19. package/server/client/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  20. package/server/client/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  21. package/server/client/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  22. package/server/client/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  23. package/server/client/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  24. package/server/client/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  25. package/server/client/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  26. package/server/client/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  27. package/server/client/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  28. package/server/client/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  29. package/server/client/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  30. package/server/client/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  31. package/server/client/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  32. package/server/client/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  33. package/server/client/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  34. package/server/client/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  35. package/server/client/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  36. package/server/client/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  37. package/server/client/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  38. package/server/client/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  39. package/server/client/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  40. package/server/client/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  41. package/server/client/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  42. package/server/client/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  43. package/server/client/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  44. package/server/client/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  45. package/server/client/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  46. package/server/client/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  47. package/server/client/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  48. package/server/client/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  49. package/server/client/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  50. package/server/client/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  51. package/server/client/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  52. package/server/client/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  53. package/server/client/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  54. package/server/client/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  55. package/server/client/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  56. package/server/client/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  57. package/server/client/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  58. package/server/client/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  59. package/server/client/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  60. package/server/client/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  61. package/server/client/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  62. package/server/client/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  63. package/server/client/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  64. package/server/client/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  65. package/server/client/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  66. package/server/client/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  67. package/server/client/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  68. package/server/client/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  69. package/server/client/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  70. package/server/client/dist/assets/Note-CmreVzvQ.js +25 -0
  71. package/server/client/dist/assets/{NotePropertiesPanel-2DgXEM4B.js → NotePropertiesPanel-DXYgE8h2.js} +2 -2
  72. package/server/client/dist/assets/PencilLine.es-B9V6u64t.js +1 -0
  73. package/server/client/dist/assets/{Plus.es-CQ8wn2is.js → Plus.es-CRRp2u5J.js} +1 -1
  74. package/server/client/dist/assets/Reminders-BOtfNeYD.js +1 -0
  75. package/server/client/dist/assets/Search-C-FB3pKf.js +1 -0
  76. package/server/client/dist/assets/SurfaceCard-GMt5xM7v.js +1 -0
  77. package/server/client/dist/assets/Tag-lBxDRE77.js +1 -0
  78. package/server/client/dist/assets/TagNotes-Bgorp_rC.js +1 -0
  79. package/server/client/dist/assets/{Trash.es-0aPpA-3S.js → Trash.es-svHOb5LN.js} +1 -1
  80. package/server/client/dist/assets/ViewNotes-C7d10VFf.js +1 -0
  81. package/server/client/dist/assets/ViewSectionDialog-Bhw-ILop.js +102 -0
  82. package/server/client/dist/assets/Views-C7V3saUo.js +1 -0
  83. package/server/client/dist/assets/abnfDiagram-VRR7QNED-CWlV5dXw.js +1 -0
  84. package/server/client/dist/assets/app-DQBV36r_.js +50 -0
  85. package/server/client/dist/assets/app-ZgwR5t_I.css +1 -0
  86. package/server/client/dist/assets/arc-BV-n6RUX.js +1 -0
  87. package/server/client/dist/assets/architectureDiagram-ZJ3FMSHR-CekzMXdu.js +36 -0
  88. package/server/client/dist/assets/blockDiagram-677ZJIJ3-B49yHoxq.js +132 -0
  89. package/server/client/dist/assets/c4Diagram-LMCZKHZV-0AfB9NBC.js +10 -0
  90. package/server/client/dist/assets/channel-CigOulR-.js +1 -0
  91. package/server/client/dist/assets/chunk-2Q5K7J3B-DaPQAU1q.js +1 -0
  92. package/server/client/dist/assets/chunk-32BRIVSS-CfL3kZAg.js +1 -0
  93. package/server/client/dist/assets/chunk-5VM5RSS4-DeOwWPE_.js +15 -0
  94. package/server/client/dist/assets/chunk-EX3LRPZG-C3W1oc9y.js +231 -0
  95. package/server/client/dist/assets/chunk-JWPE2WC7-_Oqh79OY.js +1 -0
  96. package/server/client/dist/assets/chunk-MOJQB5TN-DDKNNzI6.js +88 -0
  97. package/server/client/dist/assets/chunk-RYQCIY6F-DCHah7CC.js +1 -0
  98. package/server/client/dist/assets/chunk-V7JOEXUC-uUOIzgxj.js +206 -0
  99. package/server/client/dist/assets/chunk-VR4S4FIN-DDVw2WPs.js +1 -0
  100. package/server/client/dist/assets/chunk-XXDRQBXY-Coo4UFm2.js +1 -0
  101. package/server/client/dist/assets/classDiagram-OUVF2IWQ-D13ISIOW.js +1 -0
  102. package/server/client/dist/assets/classDiagram-v2-EOCWNBFH-D13ISIOW.js +1 -0
  103. package/server/client/dist/assets/colors-Cc3OSVma.js +1 -0
  104. package/server/client/dist/assets/cose-bilkent-JH36ORCC-C3pHmHpD.js +1 -0
  105. package/server/client/dist/assets/cynefin-VYW2F7L2-DU_G4Atc.js +166 -0
  106. package/server/client/dist/assets/cynefinDiagram-TSTJHNR4-DZ6iiJFo.js +62 -0
  107. package/server/client/dist/assets/cytoscape.esm-D3_iZ_3b.js +321 -0
  108. package/server/client/dist/assets/dagre-VKFMJZFB-DNELbhwC.js +4 -0
  109. package/server/client/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  110. package/server/client/dist/assets/diagram-FQU43EPY-CMEvZJ61.js +3 -0
  111. package/server/client/dist/assets/diagram-G47NLZAW-BYDtf6j9.js +24 -0
  112. package/server/client/dist/assets/diagram-NH7WQ7WH-CmXfcCQA.js +24 -0
  113. package/server/client/dist/assets/diagram-OA4YK3LP-BflTKJxU.js +30 -0
  114. package/server/client/dist/assets/diagram-WEI45ONY-KW3fKqna.js +41 -0
  115. package/server/client/dist/assets/ebnfDiagram-CCIWWBDH-CbyhY5Nj.js +1 -0
  116. package/server/client/dist/assets/erDiagram-Q63AITRT-CC2eZV8g.js +85 -0
  117. package/server/client/dist/assets/{floating-ui.react-dom-wWLZ1CdM.js → floating-ui.react-dom-BWTnA00G.js} +1 -1
  118. package/server/client/dist/assets/flowDiagram-23GEKE2U-unplo7Qm.js +156 -0
  119. package/server/client/dist/assets/ganttDiagram-NO4QXBWP-BcBK5HcZ.js +292 -0
  120. package/server/client/dist/assets/gitGraphDiagram-IHSO6WYX-DgVbbb5k.js +106 -0
  121. package/server/client/dist/assets/graph-C5DFNLrC.js +1 -0
  122. package/server/client/dist/assets/image-delete-confirmation-D7xrODQV.js +1 -0
  123. package/server/client/dist/assets/image.api-QmHYjp3z.js +17 -0
  124. package/server/client/dist/assets/index-CaF9d-zG.js +1 -0
  125. package/server/client/dist/assets/infoDiagram-FWYZ7A6U-N_D9RdfG.js +2 -0
  126. package/server/client/dist/assets/init-Gi6I4Gst.js +1 -0
  127. package/server/client/dist/assets/isObject-U9U2HXiq.js +1 -0
  128. package/server/client/dist/assets/ishikawaDiagram-FXEZZL3T-BBKrzmB2.js +70 -0
  129. package/server/client/dist/assets/journeyDiagram-5HDEW3XC-NdCxLmQB.js +139 -0
  130. package/server/client/dist/assets/kanban-definition-HUTT4EX6-Ck7t4-tt.js +89 -0
  131. package/server/client/dist/assets/katex-DywYRkvg.css +1 -0
  132. package/server/client/dist/assets/katex-HP8lGamR.js +257 -0
  133. package/server/client/dist/assets/katex-x288kqHd.js +257 -0
  134. package/server/client/dist/assets/layout-Cz0MDlGi.js +1 -0
  135. package/server/client/dist/assets/linear-D6yRgDLq.js +1 -0
  136. package/server/client/dist/assets/manage-image-DCDF8AM9.js +1 -0
  137. package/server/client/dist/assets/manage-image-detail-e0ViRAj-.js +1 -0
  138. package/server/client/dist/assets/map-Mm1V-_Na.js +1 -0
  139. package/server/client/dist/assets/mcp-admin-adapter-D39G3udJ.js +1 -0
  140. package/server/client/dist/assets/{mcp-CT_9IV2c.js → mcp-uFZkdhCi.js} +4 -4
  141. package/server/client/dist/assets/mermaid.core-B14ZAmWA.js +308 -0
  142. package/server/client/dist/assets/min-tv6RD-6E.js +1 -0
  143. package/server/client/dist/assets/mindmap-definition-LN4V7U3C-BTUqSdcP.js +96 -0
  144. package/server/client/dist/assets/{note-core-Buwzyxi7.js → note-core-DUTqUh5N.js} +2 -2
  145. package/server/client/dist/assets/{note-runtime-CPDVOFEo.js → note-runtime-zN5ddTba.js} +1 -1
  146. package/server/client/dist/assets/{note-ui-CHI-tzj-.js → note-ui-CxK8ymUc.js} +1 -1
  147. package/server/client/dist/assets/{note.api-D_00ZzJC.js → note.api-BQovzEzd.js} +6 -6
  148. package/server/client/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  149. package/server/client/dist/assets/pegDiagram-2B236MQR-Cjcu3mVz.js +1 -0
  150. package/server/client/dist/assets/pieDiagram-ENE6RG2P-BxI_c22Z.js +39 -0
  151. package/server/client/dist/assets/{placeholder-DbdGiYGL.js → placeholder-D3b8lZGE.js} +4 -4
  152. package/server/client/dist/assets/properties-M-yWq1D7.js +1 -0
  153. package/server/client/dist/assets/quadrantDiagram-ABIIQ3AL-DGm0a-yR.js +7 -0
  154. package/server/client/dist/assets/railroadDiagram-RFXS5EU6-DmCDu89O.js +1 -0
  155. package/server/client/dist/assets/requirementDiagram-TGXJPOKE-CBpUt7Ig.js +84 -0
  156. package/server/client/dist/assets/route-preload.js +2 -2
  157. package/server/client/dist/assets/sankeyDiagram-HTMAVEWB-BjGnDZlp.js +40 -0
  158. package/server/client/dist/assets/search-DFnzZKLd.js +1 -0
  159. package/server/client/dist/assets/sequenceDiagram-DBY2YBRQ-B7rpCfS6.js +162 -0
  160. package/server/client/dist/assets/sizeCapture-X5ZJPWSS-CUJhD0uW.js +1 -0
  161. package/server/client/dist/assets/stateDiagram-2N3HPSRC-Y9-3XqRX.js +1 -0
  162. package/server/client/dist/assets/stateDiagram-v2-6OUMAXLB-DbHXhT02.js +1 -0
  163. package/server/client/dist/assets/swimlanes-5IMT3BWC-Cs1NO9ng.js +2 -0
  164. package/server/client/dist/assets/swimlanesDiagram-G3AALYLV-DQnrq2ZC.js +8 -0
  165. package/server/client/dist/assets/{tag.api-CX0SE06J.js → tag.api-DUfjr8bg.js} +3 -3
  166. package/server/client/dist/assets/time-CfAYZipR.js +9 -0
  167. package/server/client/dist/assets/timeline-definition-FHXFAJF6-8fqD4WNL.js +120 -0
  168. package/server/client/dist/assets/toNumber-DE3J3kMD.js +1 -0
  169. package/server/client/dist/assets/transform-BUB79_za.js +1 -0
  170. package/server/client/dist/assets/trash-CClKfP1x.js +1 -0
  171. package/server/client/dist/assets/url-9nrbQs7Z.js +1 -0
  172. package/server/client/dist/assets/{useReminderMutate-CLK1tHvT.js → useReminderMutate--omDLmBV.js} +4 -4
  173. package/server/client/dist/assets/vennDiagram-L72KCM5P-BPqKkd5a.js +34 -0
  174. package/server/client/dist/assets/wardleyDiagram-EHGQE667-Cq11kU2M.js +78 -0
  175. package/server/client/dist/assets/xychartDiagram-FW5EYKEG-DJ6UzFj-.js +7 -0
  176. package/server/client/dist/index.html +7 -7
  177. package/server/dist/features/image/services/delete.js +25 -1
  178. package/server/dist/features/note/graphql/note.mutation.resolver.js +38 -12
  179. package/server/dist/features/note/graphql/note.type-defs.js +1 -1
  180. package/server/dist/features/note/http/mcp.js +1 -60
  181. package/server/dist/features/note/services/authoring.js +2 -70
  182. package/server/dist/features/note/services/markdown-patch.js +80 -8
  183. package/server/dist/features/note/services/properties.js +21 -1
  184. package/server/dist/features/note/services/property-restore.js +26 -0
  185. package/server/dist/features/note/services/snapshot.js +79 -59
  186. package/server/dist/features/note/services/trash.js +16 -31
  187. package/server/dist/features/search/embedding-api-key-store.js +57 -0
  188. package/server/dist/features/search/embedding-client.js +179 -0
  189. package/server/dist/features/search/graphql/index.js +6 -0
  190. package/server/dist/features/search/graphql/resolver.js +20 -0
  191. package/server/dist/features/search/graphql/type-defs.js +30 -0
  192. package/server/dist/features/search/http/handlers.js +108 -0
  193. package/server/dist/features/search/hybrid-ranking.js +55 -0
  194. package/server/dist/features/search/hybrid-search.js +174 -0
  195. package/server/dist/features/search/note-change.js +16 -0
  196. package/server/dist/features/search/note-chunking.js +75 -0
  197. package/server/dist/features/search/search-config.js +150 -0
  198. package/server/dist/features/search/search-manager.js +431 -0
  199. package/server/dist/features/search/semantic-indexer.js +138 -0
  200. package/server/dist/features/search/sqlite-vector-index.js +636 -0
  201. package/server/dist/features/search/url-normalization.js +10 -0
  202. package/server/dist/features/tag/services/organization.js +3 -0
  203. package/server/dist/features/view/services/workspace.js +1 -1
  204. package/server/dist/main.js +2 -0
  205. package/server/dist/modules/blocknote.js +4 -5
  206. package/server/dist/paths.js +7 -2
  207. package/server/dist/routes/api.js +37 -0
  208. package/server/dist/routes/mcp.js +0 -5
  209. package/server/dist/schema/index.js +3 -0
  210. package/server/client/dist/assets/Calendar-BP6HTuSn.js +0 -25
  211. package/server/client/dist/assets/Graph-Xha3fwhp.js +0 -46
  212. package/server/client/dist/assets/Image-BLEjLUqd.js +0 -1
  213. package/server/client/dist/assets/Note-DjrCYtat.js +0 -24
  214. package/server/client/dist/assets/Reminders-B4l3KdlK.js +0 -1
  215. package/server/client/dist/assets/Search-BN5sf7bE.js +0 -1
  216. package/server/client/dist/assets/SurfaceCard-BZm73nyS.js +0 -1
  217. package/server/client/dist/assets/Tag-oSnmViVp.js +0 -1
  218. package/server/client/dist/assets/TagNotes-BDTCcJcL.js +0 -1
  219. package/server/client/dist/assets/ViewNotes-CiZtGJNE.js +0 -1
  220. package/server/client/dist/assets/ViewSectionDialog-Ctl_694Y.js +0 -102
  221. package/server/client/dist/assets/Views-D-zlatYD.js +0 -1
  222. package/server/client/dist/assets/app-DmqIOMWF.css +0 -1
  223. package/server/client/dist/assets/app-cTRcKMGc.js +0 -26
  224. package/server/client/dist/assets/image.api-CVuuvTBk.js +0 -17
  225. package/server/client/dist/assets/index-DKJChPr4.js +0 -1
  226. package/server/client/dist/assets/manage-image-BZjaxp-f.js +0 -1
  227. package/server/client/dist/assets/manage-image-detail-DPv0FQ2m.js +0 -1
  228. package/server/client/dist/assets/mcp-admin-adapter-xAuyZc_M.js +0 -1
  229. package/server/client/dist/assets/properties-D447wsWk.js +0 -1
  230. package/server/client/dist/assets/time-B3pt8vt4.js +0 -9
  231. package/server/client/dist/assets/trash-8sH7EZgc.js +0 -1
  232. package/server/client/dist/assets/url-BSnWpmJx.js +0 -1
@@ -0,0 +1,636 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { getLoadablePath } from "sqlite-vec";
4
+ import sqlite3 from "sqlite3";
5
+ const SEMANTIC_INDEX_SCHEMA_VERSION = 2;
6
+ const SEARCH_DATABASE_SCHEMA_VERSION = 1;
7
+ const SEARCH_DATABASE_SCHEMA_SQL = `
8
+ CREATE TABLE IF NOT EXISTS search_index_state (
9
+ id INTEGER NOT NULL PRIMARY KEY CHECK (id = 1),
10
+ schema_version INTEGER NOT NULL,
11
+ profile_json TEXT NOT NULL,
12
+ note_count INTEGER NOT NULL,
13
+ chunk_count INTEGER NOT NULL,
14
+ indexed_at TEXT NOT NULL
15
+ );
16
+ CREATE TABLE IF NOT EXISTS search_chunks (
17
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
18
+ note_id INTEGER NOT NULL,
19
+ chunk_index INTEGER NOT NULL,
20
+ source_hash TEXT NOT NULL,
21
+ text TEXT NOT NULL,
22
+ embedding BLOB NOT NULL,
23
+ UNIQUE (note_id, chunk_index)
24
+ );
25
+ CREATE INDEX IF NOT EXISTS search_chunks_note_id_idx ON search_chunks(note_id);
26
+ CREATE TABLE IF NOT EXISTS search_note_sync_queue (
27
+ note_id INTEGER NOT NULL PRIMARY KEY,
28
+ version INTEGER NOT NULL,
29
+ first_queued_at INTEGER NOT NULL,
30
+ last_queued_at INTEGER NOT NULL,
31
+ attempt_count INTEGER NOT NULL,
32
+ next_attempt_at INTEGER NOT NULL,
33
+ last_error TEXT
34
+ );
35
+ CREATE INDEX IF NOT EXISTS search_note_sync_queue_ready_idx
36
+ ON search_note_sync_queue(next_attempt_at, first_queued_at);
37
+ CREATE TABLE IF NOT EXISTS search_note_sync_state (
38
+ id INTEGER NOT NULL PRIMARY KEY CHECK (id = 1),
39
+ last_synced_at TEXT,
40
+ last_reconciled_at TEXT
41
+ );
42
+ `;
43
+ const RESET_SEARCH_DATABASE_SCHEMA_SQL = `
44
+ DROP TABLE IF EXISTS search_note_sync_queue;
45
+ DROP TABLE IF EXISTS search_note_sync_state;
46
+ DROP TABLE IF EXISTS search_chunks;
47
+ DROP TABLE IF EXISTS search_index_state;
48
+ `;
49
+ const openDatabase = async (filePath) => {
50
+ if (filePath !== ":memory:") {
51
+ await mkdir(path.dirname(filePath), { recursive: true });
52
+ }
53
+ const database = await new Promise((resolve, reject) => {
54
+ const db = new sqlite3.Database(filePath, (error) => {
55
+ if (error) {
56
+ reject(error);
57
+ return;
58
+ }
59
+ resolve(db);
60
+ });
61
+ });
62
+ await new Promise((resolve, reject) => {
63
+ database.loadExtension(getLoadablePath(), (error) => {
64
+ if (error) {
65
+ reject(error);
66
+ return;
67
+ }
68
+ resolve();
69
+ });
70
+ });
71
+ return database;
72
+ };
73
+ const run = (database, sql, parameters = []) => {
74
+ return new Promise((resolve, reject) => {
75
+ database.run(sql, parameters, function onRun(error) {
76
+ if (error) {
77
+ reject(error);
78
+ return;
79
+ }
80
+ resolve(this);
81
+ });
82
+ });
83
+ };
84
+ const get = (database, sql, parameters = []) => {
85
+ return new Promise((resolve, reject) => {
86
+ database.get(sql, parameters, (error, row) => {
87
+ if (error) {
88
+ reject(error);
89
+ return;
90
+ }
91
+ resolve(row);
92
+ });
93
+ });
94
+ };
95
+ const all = (database, sql, parameters = []) => {
96
+ return new Promise((resolve, reject) => {
97
+ database.all(sql, parameters, (error, rows) => {
98
+ if (error) {
99
+ reject(error);
100
+ return;
101
+ }
102
+ resolve(rows);
103
+ });
104
+ });
105
+ };
106
+ const exec = (database, sql) => {
107
+ return new Promise((resolve, reject) => {
108
+ database.exec(sql, (error) => {
109
+ if (error) {
110
+ reject(error);
111
+ return;
112
+ }
113
+ resolve();
114
+ });
115
+ });
116
+ };
117
+ const initializeSearchDatabase = async (database) => {
118
+ await exec(
119
+ database,
120
+ `
121
+ PRAGMA journal_mode = WAL;
122
+ PRAGMA synchronous = NORMAL;
123
+ `
124
+ );
125
+ const schemaVersion = await get(
126
+ database,
127
+ "SELECT user_version AS userVersion FROM pragma_user_version"
128
+ );
129
+ const storedVersion = schemaVersion?.userVersion ?? 0;
130
+ if (storedVersion !== 0 && storedVersion !== SEARCH_DATABASE_SCHEMA_VERSION) {
131
+ await exec(database, RESET_SEARCH_DATABASE_SCHEMA_SQL);
132
+ }
133
+ await exec(database, SEARCH_DATABASE_SCHEMA_SQL);
134
+ await exec(database, `PRAGMA user_version = ${SEARCH_DATABASE_SCHEMA_VERSION};`);
135
+ };
136
+ const close = (database) => {
137
+ return new Promise((resolve, reject) => {
138
+ database.close((error) => {
139
+ if (error) {
140
+ reject(error);
141
+ return;
142
+ }
143
+ resolve();
144
+ });
145
+ });
146
+ };
147
+ const serializeEmbedding = (embedding) => {
148
+ return Buffer.from(Float32Array.from(embedding).buffer);
149
+ };
150
+ const isSemanticIndexProfile = (value) => {
151
+ if (!value || typeof value !== "object") {
152
+ return false;
153
+ }
154
+ const profile = value;
155
+ return typeof profile.model === "string" && typeof profile.baseUrl === "string" && Number.isInteger(profile.dimensions) && Number(profile.dimensions) > 0 && typeof profile.queryInstruction === "string" && Number.isInteger(profile.textSchemaVersion);
156
+ };
157
+ const parseProfile = (profileJson) => {
158
+ try {
159
+ const profile = JSON.parse(profileJson);
160
+ return isSemanticIndexProfile(profile) ? profile : null;
161
+ } catch {
162
+ return null;
163
+ }
164
+ };
165
+ const validateIndexInput = (profile, chunks) => {
166
+ if (!profile.model.trim()) {
167
+ throw new Error("Semantic index profile model is required.");
168
+ }
169
+ if (!profile.baseUrl.trim()) {
170
+ throw new Error("Semantic index profile API URL is required.");
171
+ }
172
+ if (!Number.isInteger(profile.dimensions) || profile.dimensions <= 0) {
173
+ throw new Error("Semantic index dimensions must be a positive integer.");
174
+ }
175
+ for (const chunk of chunks) {
176
+ if (chunk.embedding.length !== profile.dimensions) {
177
+ throw new Error(
178
+ `Embedding dimensions for note ${chunk.noteId} chunk ${chunk.chunkIndex} do not match the index profile.`
179
+ );
180
+ }
181
+ if (chunk.embedding.some((value) => !Number.isFinite(value))) {
182
+ throw new Error(`Embedding for note ${chunk.noteId} chunk ${chunk.chunkIndex} contains an invalid value.`);
183
+ }
184
+ }
185
+ };
186
+ class SqliteSemanticVectorIndex {
187
+ constructor(filePath) {
188
+ this.filePath = filePath;
189
+ }
190
+ filePath;
191
+ databasePromise = null;
192
+ operationTail = Promise.resolve();
193
+ getDatabase() {
194
+ if (!this.databasePromise) {
195
+ this.databasePromise = openDatabase(this.filePath).then(async (database) => {
196
+ await initializeSearchDatabase(database);
197
+ return database;
198
+ });
199
+ }
200
+ return this.databasePromise;
201
+ }
202
+ runExclusive(operation) {
203
+ const result = this.operationTail.then(operation, operation);
204
+ this.operationTail = result.then(
205
+ () => void 0,
206
+ () => void 0
207
+ );
208
+ return result;
209
+ }
210
+ async replaceAll(profile, chunks) {
211
+ validateIndexInput(profile, chunks);
212
+ return this.runExclusive(async () => {
213
+ const database = await this.getDatabase();
214
+ const indexedAt = (/* @__PURE__ */ new Date()).toISOString();
215
+ const noteCount = new Set(chunks.map((chunk) => chunk.noteId)).size;
216
+ await exec(database, "BEGIN IMMEDIATE;");
217
+ try {
218
+ await run(database, "DELETE FROM search_chunks");
219
+ await run(database, "DELETE FROM search_index_state");
220
+ for (const chunk of chunks) {
221
+ await run(
222
+ database,
223
+ `
224
+ INSERT INTO search_chunks (
225
+ note_id,
226
+ chunk_index,
227
+ source_hash,
228
+ text,
229
+ embedding
230
+ ) VALUES (?, ?, ?, ?, ?)
231
+ `,
232
+ [
233
+ chunk.noteId,
234
+ chunk.chunkIndex,
235
+ chunk.sourceHash,
236
+ chunk.text,
237
+ serializeEmbedding(chunk.embedding)
238
+ ]
239
+ );
240
+ }
241
+ await run(
242
+ database,
243
+ `
244
+ INSERT INTO search_index_state (
245
+ id,
246
+ schema_version,
247
+ profile_json,
248
+ note_count,
249
+ chunk_count,
250
+ indexed_at
251
+ ) VALUES (1, ?, ?, ?, ?, ?)
252
+ `,
253
+ [SEMANTIC_INDEX_SCHEMA_VERSION, JSON.stringify(profile), noteCount, chunks.length, indexedAt]
254
+ );
255
+ await exec(database, "COMMIT;");
256
+ } catch (error) {
257
+ await exec(database, "ROLLBACK;");
258
+ throw error;
259
+ }
260
+ return this.getStatusFromDatabase(database);
261
+ });
262
+ }
263
+ async getStatusFromDatabase(database) {
264
+ const state = await get(
265
+ database,
266
+ `
267
+ SELECT
268
+ profile_json AS profileJson,
269
+ note_count AS noteCount,
270
+ chunk_count AS chunkCount,
271
+ indexed_at AS indexedAt
272
+ FROM search_index_state
273
+ WHERE id = 1 AND schema_version = ?
274
+ `,
275
+ [SEMANTIC_INDEX_SCHEMA_VERSION]
276
+ );
277
+ const profile = state ? parseProfile(state.profileJson) : null;
278
+ return {
279
+ ready: Boolean(state && profile && state.chunkCount > 0),
280
+ profile,
281
+ noteCount: state?.noteCount ?? 0,
282
+ chunkCount: state?.chunkCount ?? 0,
283
+ indexedAt: state?.indexedAt ?? null
284
+ };
285
+ }
286
+ async refreshIndexCounts(database) {
287
+ const counts = await get(
288
+ database,
289
+ `
290
+ SELECT
291
+ COUNT(DISTINCT note_id) AS noteCount,
292
+ COUNT(*) AS chunkCount
293
+ FROM search_chunks
294
+ `
295
+ );
296
+ await run(
297
+ database,
298
+ `
299
+ UPDATE search_index_state
300
+ SET note_count = ?, chunk_count = ?, indexed_at = ?
301
+ WHERE id = 1 AND schema_version = ?
302
+ `,
303
+ [counts?.noteCount ?? 0, counts?.chunkCount ?? 0, (/* @__PURE__ */ new Date()).toISOString(), SEMANTIC_INDEX_SCHEMA_VERSION]
304
+ );
305
+ }
306
+ async getStatus() {
307
+ return this.runExclusive(async () => this.getStatusFromDatabase(await this.getDatabase()));
308
+ }
309
+ async getNoteSourceHash(noteId) {
310
+ return this.runExclusive(async () => {
311
+ const row = await get(
312
+ await this.getDatabase(),
313
+ "SELECT source_hash AS sourceHash FROM search_chunks WHERE note_id = ? LIMIT 1",
314
+ [noteId]
315
+ );
316
+ return row?.sourceHash ?? null;
317
+ });
318
+ }
319
+ async getAllNoteSourceHashes() {
320
+ return this.runExclusive(async () => {
321
+ const rows = await all(
322
+ await this.getDatabase(),
323
+ `
324
+ SELECT note_id AS noteId, source_hash AS sourceHash
325
+ FROM search_chunks
326
+ GROUP BY note_id, source_hash
327
+ ORDER BY note_id ASC
328
+ `
329
+ );
330
+ return new Map(rows.map((row) => [row.noteId, row.sourceHash]));
331
+ });
332
+ }
333
+ async enqueueNoteSync(noteId, queuedAt) {
334
+ if (!Number.isInteger(noteId) || noteId <= 0) {
335
+ throw new Error("A positive note ID is required to queue semantic search synchronization.");
336
+ }
337
+ if (!Number.isFinite(queuedAt) || queuedAt < 0) {
338
+ throw new Error("A valid queue timestamp is required for semantic search synchronization.");
339
+ }
340
+ return this.runExclusive(async () => {
341
+ await run(
342
+ await this.getDatabase(),
343
+ `
344
+ INSERT INTO search_note_sync_queue (
345
+ note_id,
346
+ version,
347
+ first_queued_at,
348
+ last_queued_at,
349
+ attempt_count,
350
+ next_attempt_at,
351
+ last_error
352
+ ) VALUES (?, 1, ?, ?, 0, ?, NULL)
353
+ ON CONFLICT(note_id) DO UPDATE SET
354
+ version = search_note_sync_queue.version + 1,
355
+ last_queued_at = excluded.last_queued_at,
356
+ attempt_count = 0,
357
+ next_attempt_at = excluded.next_attempt_at,
358
+ last_error = NULL
359
+ `,
360
+ [noteId, queuedAt, queuedAt, queuedAt]
361
+ );
362
+ });
363
+ }
364
+ async listPendingNoteSyncs({
365
+ now,
366
+ quietPeriodMs,
367
+ maxWaitMs,
368
+ limit,
369
+ force = false
370
+ }) {
371
+ if (![now, quietPeriodMs, maxWaitMs].every((value) => Number.isFinite(value) && value >= 0)) {
372
+ throw new Error("Semantic search synchronization timing values must be non-negative numbers.");
373
+ }
374
+ if (!Number.isInteger(limit) || limit <= 0) {
375
+ throw new Error("Semantic search synchronization batch limit must be a positive integer.");
376
+ }
377
+ return this.runExclusive(async () => {
378
+ return all(
379
+ await this.getDatabase(),
380
+ `
381
+ SELECT
382
+ note_id AS noteId,
383
+ version,
384
+ first_queued_at AS firstQueuedAt,
385
+ last_queued_at AS lastQueuedAt,
386
+ attempt_count AS attemptCount
387
+ FROM search_note_sync_queue
388
+ WHERE
389
+ ? = 1
390
+ OR (
391
+ next_attempt_at <= ?
392
+ AND (
393
+ last_queued_at <= ?
394
+ OR first_queued_at <= ?
395
+ )
396
+ )
397
+ ORDER BY first_queued_at ASC, note_id ASC
398
+ LIMIT ?
399
+ `,
400
+ [force ? 1 : 0, now, now - quietPeriodMs, now - maxWaitMs, limit]
401
+ );
402
+ });
403
+ }
404
+ async completeNoteSyncs(entries, completedAt) {
405
+ return this.runExclusive(async () => {
406
+ const database = await this.getDatabase();
407
+ await exec(database, "BEGIN IMMEDIATE;");
408
+ try {
409
+ for (const entry of entries) {
410
+ await run(database, "DELETE FROM search_note_sync_queue WHERE note_id = ? AND version = ?", [
411
+ entry.noteId,
412
+ entry.version
413
+ ]);
414
+ }
415
+ await run(
416
+ database,
417
+ `
418
+ INSERT INTO search_note_sync_state (id, last_synced_at, last_reconciled_at)
419
+ VALUES (1, ?, NULL)
420
+ ON CONFLICT(id) DO UPDATE SET last_synced_at = excluded.last_synced_at
421
+ `,
422
+ [new Date(completedAt).toISOString()]
423
+ );
424
+ await exec(database, "COMMIT;");
425
+ } catch (error) {
426
+ await exec(database, "ROLLBACK;");
427
+ throw error;
428
+ }
429
+ });
430
+ }
431
+ async failNoteSyncs(entries, error, retryAt) {
432
+ return this.runExclusive(async () => {
433
+ const database = await this.getDatabase();
434
+ await exec(database, "BEGIN IMMEDIATE;");
435
+ try {
436
+ for (const entry of entries) {
437
+ await run(
438
+ database,
439
+ `
440
+ UPDATE search_note_sync_queue
441
+ SET attempt_count = attempt_count + 1,
442
+ next_attempt_at = ?,
443
+ last_error = ?
444
+ WHERE note_id = ? AND version = ?
445
+ `,
446
+ [retryAt, error, entry.noteId, entry.version]
447
+ );
448
+ }
449
+ await exec(database, "COMMIT;");
450
+ } catch (transactionError) {
451
+ await exec(database, "ROLLBACK;");
452
+ throw transactionError;
453
+ }
454
+ });
455
+ }
456
+ async recordNoteSyncSuccess(completedAt) {
457
+ return this.completeNoteSyncs([], completedAt);
458
+ }
459
+ async recordNoteSyncReconciliation(reconciledAt) {
460
+ return this.runExclusive(async () => {
461
+ await run(
462
+ await this.getDatabase(),
463
+ `
464
+ INSERT INTO search_note_sync_state (id, last_synced_at, last_reconciled_at)
465
+ VALUES (1, NULL, ?)
466
+ ON CONFLICT(id) DO UPDATE SET last_reconciled_at = excluded.last_reconciled_at
467
+ `,
468
+ [new Date(reconciledAt).toISOString()]
469
+ );
470
+ });
471
+ }
472
+ async getNoteSyncQueueStatus() {
473
+ return this.runExclusive(async () => {
474
+ const database = await this.getDatabase();
475
+ const [queue, state, latestError] = await Promise.all([
476
+ get(
477
+ database,
478
+ `
479
+ SELECT
480
+ COUNT(*) AS pendingNoteCount,
481
+ MIN(first_queued_at) AS oldestQueuedAt
482
+ FROM search_note_sync_queue
483
+ `
484
+ ),
485
+ get(
486
+ database,
487
+ `
488
+ SELECT
489
+ last_synced_at AS lastSyncedAt,
490
+ last_reconciled_at AS lastReconciledAt
491
+ FROM search_note_sync_state
492
+ WHERE id = 1
493
+ `
494
+ ),
495
+ get(
496
+ database,
497
+ `
498
+ SELECT last_error AS error
499
+ FROM search_note_sync_queue
500
+ WHERE last_error IS NOT NULL
501
+ ORDER BY next_attempt_at DESC, note_id ASC
502
+ LIMIT 1
503
+ `
504
+ )
505
+ ]);
506
+ return {
507
+ pendingNoteCount: queue?.pendingNoteCount ?? 0,
508
+ oldestQueuedAt: queue?.oldestQueuedAt === null || queue?.oldestQueuedAt === void 0 ? null : new Date(queue.oldestQueuedAt).toISOString(),
509
+ lastSyncedAt: state?.lastSyncedAt ?? null,
510
+ lastReconciledAt: state?.lastReconciledAt ?? null,
511
+ error: latestError?.error ?? null
512
+ };
513
+ });
514
+ }
515
+ async replaceNote(noteId, chunks) {
516
+ return this.runExclusive(async () => {
517
+ const database = await this.getDatabase();
518
+ const status = await this.getStatusFromDatabase(database);
519
+ if (!status.ready || !status.profile) {
520
+ throw new Error("Build the semantic index before updating an individual note.");
521
+ }
522
+ if (chunks.some((chunk) => chunk.noteId !== noteId)) {
523
+ throw new Error("Every replacement chunk must belong to the requested note.");
524
+ }
525
+ validateIndexInput(status.profile, chunks);
526
+ await exec(database, "BEGIN IMMEDIATE;");
527
+ try {
528
+ await run(database, "DELETE FROM search_chunks WHERE note_id = ?", [noteId]);
529
+ for (const chunk of chunks) {
530
+ await run(
531
+ database,
532
+ `
533
+ INSERT INTO search_chunks (
534
+ note_id,
535
+ chunk_index,
536
+ source_hash,
537
+ text,
538
+ embedding
539
+ ) VALUES (?, ?, ?, ?, ?)
540
+ `,
541
+ [
542
+ chunk.noteId,
543
+ chunk.chunkIndex,
544
+ chunk.sourceHash,
545
+ chunk.text,
546
+ serializeEmbedding(chunk.embedding)
547
+ ]
548
+ );
549
+ }
550
+ await this.refreshIndexCounts(database);
551
+ await exec(database, "COMMIT;");
552
+ } catch (error) {
553
+ await exec(database, "ROLLBACK;");
554
+ throw error;
555
+ }
556
+ return this.getStatusFromDatabase(database);
557
+ });
558
+ }
559
+ async removeNote(noteId) {
560
+ return this.runExclusive(async () => {
561
+ const database = await this.getDatabase();
562
+ await exec(database, "BEGIN IMMEDIATE;");
563
+ try {
564
+ await run(database, "DELETE FROM search_chunks WHERE note_id = ?", [noteId]);
565
+ await this.refreshIndexCounts(database);
566
+ await exec(database, "COMMIT;");
567
+ } catch (error) {
568
+ await exec(database, "ROLLBACK;");
569
+ throw error;
570
+ }
571
+ return this.getStatusFromDatabase(database);
572
+ });
573
+ }
574
+ async search(queryEmbedding, limit) {
575
+ if (!Number.isInteger(limit) || limit <= 0) {
576
+ return [];
577
+ }
578
+ return this.runExclusive(async () => {
579
+ const database = await this.getDatabase();
580
+ const status = await this.getStatusFromDatabase(database);
581
+ if (!status.ready || !status.profile) {
582
+ return [];
583
+ }
584
+ if (queryEmbedding.length !== status.profile.dimensions) {
585
+ throw new Error(
586
+ `Query embedding has ${queryEmbedding.length} dimensions but the index requires ${status.profile.dimensions}.`
587
+ );
588
+ }
589
+ if (queryEmbedding.some((value) => !Number.isFinite(value))) {
590
+ throw new Error("Query embedding contains an invalid value.");
591
+ }
592
+ const rows = await all(
593
+ database,
594
+ `
595
+ SELECT
596
+ note_id AS noteId,
597
+ MIN(vec_distance_cosine(embedding, ?)) AS distance
598
+ FROM search_chunks
599
+ GROUP BY note_id
600
+ ORDER BY distance ASC, note_id ASC
601
+ LIMIT ?
602
+ `,
603
+ [serializeEmbedding(queryEmbedding), limit]
604
+ );
605
+ return rows.map((row) => ({ noteId: row.noteId, distance: row.distance }));
606
+ });
607
+ }
608
+ async clear() {
609
+ return this.runExclusive(async () => {
610
+ const database = await this.getDatabase();
611
+ await exec(database, "BEGIN IMMEDIATE;");
612
+ try {
613
+ await run(database, "DELETE FROM search_chunks");
614
+ await run(database, "DELETE FROM search_index_state");
615
+ await exec(database, "COMMIT;");
616
+ } catch (error) {
617
+ await exec(database, "ROLLBACK;");
618
+ throw error;
619
+ }
620
+ });
621
+ }
622
+ async close() {
623
+ await this.operationTail;
624
+ if (!this.databasePromise) {
625
+ return;
626
+ }
627
+ const database = await this.databasePromise;
628
+ this.databasePromise = null;
629
+ await close(database);
630
+ }
631
+ }
632
+ export {
633
+ SEARCH_DATABASE_SCHEMA_VERSION,
634
+ SEMANTIC_INDEX_SCHEMA_VERSION,
635
+ SqliteSemanticVectorIndex
636
+ };
@@ -0,0 +1,10 @@
1
+ const stripTrailingSlashes = (value) => {
2
+ let end = value.length;
3
+ while (end > 0 && value.charCodeAt(end - 1) === 47) {
4
+ end -= 1;
5
+ }
6
+ return end === value.length ? value : value.slice(0, end);
7
+ };
8
+ export {
9
+ stripTrailingSlashes
10
+ };
@@ -10,6 +10,9 @@ const normalizeTagName = (name) => {
10
10
  if (!trimmedName) {
11
11
  throw new InvalidTagNameError("A tag name is required.");
12
12
  }
13
+ if (trimmedName.startsWith("#")) {
14
+ throw new InvalidTagNameError("Tag names must use @, not #. Example: @project.");
15
+ }
13
16
  const normalizedName = trimmedName.startsWith("@") ? trimmedName : `@${trimmedName}`;
14
17
  if (normalizedName === "@" || /\s/.test(normalizedName.slice(1))) {
15
18
  throw new InvalidTagNameError("Tag names must be a single token like @project.");
@@ -36,7 +36,7 @@ const normalizeTagName = (value) => {
36
36
  return trimmedValue;
37
37
  }
38
38
  if (trimmedValue.startsWith("#")) {
39
- return `@${trimmedValue.slice(1)}`;
39
+ throw new Error("View tag names must use @, not #.");
40
40
  }
41
41
  return `@${trimmedValue}`;
42
42
  };
@@ -1,4 +1,5 @@
1
1
  import { createApp } from "./app.js";
2
+ import { getDefaultSemanticSearchManager } from "./features/search/search-manager.js";
2
3
  import { logAuthConfig, resolveAuthConfig } from "./modules/auth-mode.js";
3
4
  import { startDataMaintenanceScheduler } from "./modules/data-maintenance.js";
4
5
  const PORT = Number(process.env.PORT || 6683);
@@ -7,6 +8,7 @@ try {
7
8
  const authConfig = resolveAuthConfig(process.env);
8
9
  logAuthConfig(authConfig);
9
10
  const app = createApp(authConfig);
11
+ getDefaultSemanticSearchManager();
10
12
  app.listen(PORT, HOST, () => {
11
13
  process.stdout.write(`http server listen on ${HOST}:${PORT} (auth: ${authConfig.mode})
12
14
  `);