synabun 1.3.0 → 2.0.0-beta

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 (1153) hide show
  1. package/CHANGELOG.md +197 -662
  2. package/LICENSE-COMMERCIAL.md +33 -0
  3. package/NOTICE +17 -0
  4. package/README.md +175 -853
  5. package/THIRD-PARTY-LICENSES.md +131 -0
  6. package/benchmarks/browser-live-results.json +79 -0
  7. package/benchmarks/browser-v2-results.json +258 -0
  8. package/benchmarks/fixtures/browser/README.md +57 -0
  9. package/benchmarks/fixtures/browser/page-state.json +132 -0
  10. package/benchmarks/fixtures/browser/social.json +92 -0
  11. package/benchmarks/fixtures/browser/target.json +319 -0
  12. package/benchmarks/fixtures/desktop/README.md +58 -0
  13. package/benchmarks/fixtures/desktop/abstention.json +59 -0
  14. package/benchmarks/fixtures/desktop/adversarial.json +22 -0
  15. package/benchmarks/fixtures/desktop/forbidden.json +93 -0
  16. package/benchmarks/fixtures/desktop/generate.mjs +1023 -0
  17. package/benchmarks/fixtures/desktop/target.json +136 -0
  18. package/bin/synabun.js +30 -0
  19. package/docs/browser-v2.md +109 -0
  20. package/docs/hook-integrity.md +89 -0
  21. package/docs/judgments.md +182 -0
  22. package/docs/style-guide.md +246 -0
  23. package/docs/whatsapp.md +325 -0
  24. package/hooks/claude-code/post-plan.mjs +613 -391
  25. package/hooks/claude-code/post-remember.mjs +246 -122
  26. package/hooks/claude-code/pre-compact.mjs +171 -110
  27. package/hooks/claude-code/pre-task.mjs +133 -0
  28. package/hooks/claude-code/pre-websearch.mjs +15 -4
  29. package/hooks/claude-code/prompt-origin.mjs +94 -0
  30. package/hooks/claude-code/prompt-submit.mjs +1244 -861
  31. package/hooks/claude-code/session-start.mjs +114 -156
  32. package/hooks/claude-code/shared.mjs +1228 -368
  33. package/hooks/claude-code/state.mjs +167 -0
  34. package/hooks/claude-code/stop.mjs +515 -140
  35. package/hooks/claude-code/subagent-start.mjs +154 -0
  36. package/hooks/claude-code/subagent-stop.mjs +102 -0
  37. package/hooks/claude-code/task-gate.mjs +150 -0
  38. package/lib/claude-hooks.js +84 -0
  39. package/lib/claude-usage-audit.js +141 -0
  40. package/lib/client-config-repair.js +144 -0
  41. package/lib/data-home-migration.js +498 -0
  42. package/lib/first-launch-protection.js +240 -0
  43. package/lib/paths.d.ts +21 -0
  44. package/lib/paths.js +103 -0
  45. package/mcp-server/dist/config.d.ts +22 -0
  46. package/mcp-server/dist/config.d.ts.map +1 -0
  47. package/mcp-server/dist/config.js +109 -0
  48. package/mcp-server/dist/config.js.map +1 -0
  49. package/mcp-server/dist/http.d.ts +30 -0
  50. package/mcp-server/dist/http.d.ts.map +1 -0
  51. package/mcp-server/dist/http.js +276 -0
  52. package/mcp-server/dist/http.js.map +1 -0
  53. package/mcp-server/dist/index.d.ts +48 -0
  54. package/mcp-server/dist/index.d.ts.map +1 -0
  55. package/mcp-server/dist/index.js +538 -0
  56. package/mcp-server/dist/index.js.map +1 -0
  57. package/mcp-server/dist/preload-http.d.ts +3 -0
  58. package/mcp-server/dist/preload-http.d.ts.map +1 -0
  59. package/mcp-server/dist/preload-http.js +28 -0
  60. package/mcp-server/dist/preload-http.js.map +1 -0
  61. package/mcp-server/dist/preload.d.ts +3 -0
  62. package/mcp-server/dist/preload.d.ts.map +1 -0
  63. package/mcp-server/dist/preload.js +32 -0
  64. package/mcp-server/dist/preload.js.map +1 -0
  65. package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.d.ts +19 -0
  66. package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.d.ts.map +1 -0
  67. package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.js +403 -0
  68. package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.js.map +1 -0
  69. package/mcp-server/dist/scripts/setup-embeddings.d.ts +3 -0
  70. package/mcp-server/dist/scripts/setup-embeddings.d.ts.map +1 -0
  71. package/mcp-server/dist/scripts/setup-embeddings.js +15 -0
  72. package/mcp-server/dist/scripts/setup-embeddings.js.map +1 -0
  73. package/mcp-server/dist/services/assistant-judgments.d.ts +79 -0
  74. package/mcp-server/dist/services/assistant-judgments.d.ts.map +1 -0
  75. package/mcp-server/dist/services/assistant-judgments.js +128 -0
  76. package/mcp-server/dist/services/assistant-judgments.js.map +1 -0
  77. package/mcp-server/dist/services/browser-assist-gate.d.ts +314 -0
  78. package/mcp-server/dist/services/browser-assist-gate.d.ts.map +1 -0
  79. package/mcp-server/dist/services/browser-assist-gate.js +344 -0
  80. package/mcp-server/dist/services/browser-assist-gate.js.map +1 -0
  81. package/mcp-server/dist/services/browser-assist.d.ts +104 -0
  82. package/mcp-server/dist/services/browser-assist.d.ts.map +1 -0
  83. package/mcp-server/dist/services/browser-assist.js +282 -0
  84. package/mcp-server/dist/services/browser-assist.js.map +1 -0
  85. package/mcp-server/dist/services/browser-bench.d.ts +772 -0
  86. package/mcp-server/dist/services/browser-bench.d.ts.map +1 -0
  87. package/mcp-server/dist/services/browser-bench.js +424 -0
  88. package/mcp-server/dist/services/browser-bench.js.map +1 -0
  89. package/mcp-server/dist/services/browser-judgments.d.ts +196 -0
  90. package/mcp-server/dist/services/browser-judgments.d.ts.map +1 -0
  91. package/mcp-server/dist/services/browser-judgments.js +363 -0
  92. package/mcp-server/dist/services/browser-judgments.js.map +1 -0
  93. package/mcp-server/dist/services/browser-risk.d.ts +166 -0
  94. package/mcp-server/dist/services/browser-risk.d.ts.map +1 -0
  95. package/mcp-server/dist/services/browser-risk.js +458 -0
  96. package/mcp-server/dist/services/browser-risk.js.map +1 -0
  97. package/mcp-server/dist/services/categories.d.ts +61 -0
  98. package/mcp-server/dist/services/categories.d.ts.map +1 -0
  99. package/mcp-server/dist/services/categories.js +377 -0
  100. package/mcp-server/dist/services/categories.js.map +1 -0
  101. package/mcp-server/dist/services/codex-config-heal.d.ts +49 -0
  102. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -0
  103. package/mcp-server/dist/services/codex-config-heal.js +346 -0
  104. package/mcp-server/dist/services/codex-config-heal.js.map +1 -0
  105. package/mcp-server/dist/services/desktop-assist-gate.d.ts +198 -0
  106. package/mcp-server/dist/services/desktop-assist-gate.d.ts.map +1 -0
  107. package/mcp-server/dist/services/desktop-assist-gate.js +202 -0
  108. package/mcp-server/dist/services/desktop-assist-gate.js.map +1 -0
  109. package/mcp-server/dist/services/desktop-assist.d.ts +104 -0
  110. package/mcp-server/dist/services/desktop-assist.d.ts.map +1 -0
  111. package/mcp-server/dist/services/desktop-assist.js +266 -0
  112. package/mcp-server/dist/services/desktop-assist.js.map +1 -0
  113. package/mcp-server/dist/services/desktop-bench.d.ts +553 -0
  114. package/mcp-server/dist/services/desktop-bench.d.ts.map +1 -0
  115. package/mcp-server/dist/services/desktop-bench.js +311 -0
  116. package/mcp-server/dist/services/desktop-bench.js.map +1 -0
  117. package/mcp-server/dist/services/desktop-client.d.ts +23 -0
  118. package/mcp-server/dist/services/desktop-client.d.ts.map +1 -0
  119. package/mcp-server/dist/services/desktop-client.js +79 -0
  120. package/mcp-server/dist/services/desktop-client.js.map +1 -0
  121. package/mcp-server/dist/services/desktop-judgments.d.ts +125 -0
  122. package/mcp-server/dist/services/desktop-judgments.d.ts.map +1 -0
  123. package/mcp-server/dist/services/desktop-judgments.js +177 -0
  124. package/mcp-server/dist/services/desktop-judgments.js.map +1 -0
  125. package/mcp-server/dist/services/desktop-risk.d.ts +283 -0
  126. package/mcp-server/dist/services/desktop-risk.d.ts.map +1 -0
  127. package/mcp-server/dist/services/desktop-risk.js +605 -0
  128. package/mcp-server/dist/services/desktop-risk.js.map +1 -0
  129. package/mcp-server/dist/services/discord.d.ts +112 -0
  130. package/mcp-server/dist/services/discord.d.ts.map +1 -0
  131. package/mcp-server/dist/services/discord.js +477 -0
  132. package/mcp-server/dist/services/discord.js.map +1 -0
  133. package/mcp-server/dist/services/edit-stale.d.ts +81 -0
  134. package/mcp-server/dist/services/edit-stale.d.ts.map +1 -0
  135. package/mcp-server/dist/services/edit-stale.js +358 -0
  136. package/mcp-server/dist/services/edit-stale.js.map +1 -0
  137. package/mcp-server/dist/services/embedding-service.d.ts +16 -0
  138. package/mcp-server/dist/services/embedding-service.d.ts.map +1 -0
  139. package/mcp-server/dist/services/embedding-service.js +126 -0
  140. package/mcp-server/dist/services/embedding-service.js.map +1 -0
  141. package/mcp-server/dist/services/embedding-worker.d.ts +2 -0
  142. package/mcp-server/dist/services/embedding-worker.d.ts.map +1 -0
  143. package/mcp-server/dist/services/embedding-worker.js +52 -0
  144. package/mcp-server/dist/services/embedding-worker.js.map +1 -0
  145. package/mcp-server/dist/services/fb-regions.d.ts +71 -0
  146. package/mcp-server/dist/services/fb-regions.d.ts.map +1 -0
  147. package/mcp-server/dist/services/fb-regions.js +211 -0
  148. package/mcp-server/dist/services/fb-regions.js.map +1 -0
  149. package/mcp-server/dist/services/file-checksums.d.ts +22 -0
  150. package/mcp-server/dist/services/file-checksums.d.ts.map +1 -0
  151. package/mcp-server/dist/services/file-checksums.js +55 -0
  152. package/mcp-server/dist/services/file-checksums.js.map +1 -0
  153. package/mcp-server/dist/services/file-excerpt.d.ts +24 -0
  154. package/mcp-server/dist/services/file-excerpt.d.ts.map +1 -0
  155. package/mcp-server/dist/services/file-excerpt.js +65 -0
  156. package/mcp-server/dist/services/file-excerpt.js.map +1 -0
  157. package/mcp-server/dist/services/identity.d.ts +124 -0
  158. package/mcp-server/dist/services/identity.d.ts.map +1 -0
  159. package/mcp-server/dist/services/identity.js +241 -0
  160. package/mcp-server/dist/services/identity.js.map +1 -0
  161. package/mcp-server/dist/services/local-embeddings.d.ts +2 -0
  162. package/mcp-server/dist/services/local-embeddings.d.ts.map +1 -0
  163. package/mcp-server/dist/services/local-embeddings.js +3 -0
  164. package/mcp-server/dist/services/local-embeddings.js.map +1 -0
  165. package/mcp-server/dist/services/memory-client.d.ts +4 -0
  166. package/mcp-server/dist/services/memory-client.d.ts.map +1 -0
  167. package/mcp-server/dist/services/memory-client.js +27 -0
  168. package/mcp-server/dist/services/memory-client.js.map +1 -0
  169. package/mcp-server/dist/services/memory-judge-backfill.d.ts +135 -0
  170. package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -0
  171. package/mcp-server/dist/services/memory-judge-backfill.js +510 -0
  172. package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -0
  173. package/mcp-server/dist/services/memory-judgments.d.ts +362 -0
  174. package/mcp-server/dist/services/memory-judgments.d.ts.map +1 -0
  175. package/mcp-server/dist/services/memory-judgments.js +674 -0
  176. package/mcp-server/dist/services/memory-judgments.js.map +1 -0
  177. package/mcp-server/dist/services/memory-maintenance.d.ts +108 -0
  178. package/mcp-server/dist/services/memory-maintenance.d.ts.map +1 -0
  179. package/mcp-server/dist/services/memory-maintenance.js +403 -0
  180. package/mcp-server/dist/services/memory-maintenance.js.map +1 -0
  181. package/mcp-server/dist/services/memory-retrieval.d.ts +66 -0
  182. package/mcp-server/dist/services/memory-retrieval.d.ts.map +1 -0
  183. package/mcp-server/dist/services/memory-retrieval.js +412 -0
  184. package/mcp-server/dist/services/memory-retrieval.js.map +1 -0
  185. package/mcp-server/dist/services/memory-schema.d.ts +16 -0
  186. package/mcp-server/dist/services/memory-schema.d.ts.map +1 -0
  187. package/mcp-server/dist/services/memory-schema.js +282 -0
  188. package/mcp-server/dist/services/memory-schema.js.map +1 -0
  189. package/mcp-server/dist/services/memory-sessions.d.ts +24 -0
  190. package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -0
  191. package/mcp-server/dist/services/memory-sessions.js +110 -0
  192. package/mcp-server/dist/services/memory-sessions.js.map +1 -0
  193. package/mcp-server/dist/services/memory-triage.d.ts +60 -0
  194. package/mcp-server/dist/services/memory-triage.d.ts.map +1 -0
  195. package/mcp-server/dist/services/memory-triage.js +189 -0
  196. package/mcp-server/dist/services/memory-triage.js.map +1 -0
  197. package/mcp-server/dist/services/memory-verification.d.ts +24 -0
  198. package/mcp-server/dist/services/memory-verification.d.ts.map +1 -0
  199. package/mcp-server/dist/services/memory-verification.js +92 -0
  200. package/mcp-server/dist/services/memory-verification.js.map +1 -0
  201. package/mcp-server/dist/services/memory-writer.d.ts +26 -0
  202. package/mcp-server/dist/services/memory-writer.d.ts.map +1 -0
  203. package/mcp-server/dist/services/memory-writer.js +126 -0
  204. package/mcp-server/dist/services/memory-writer.js.map +1 -0
  205. package/mcp-server/dist/services/neural-interface.d.ts +439 -0
  206. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -0
  207. package/mcp-server/dist/services/neural-interface.js +1135 -0
  208. package/mcp-server/dist/services/neural-interface.js.map +1 -0
  209. package/mcp-server/dist/services/profiles.d.ts +120 -0
  210. package/mcp-server/dist/services/profiles.d.ts.map +1 -0
  211. package/mcp-server/dist/services/profiles.js +387 -0
  212. package/mcp-server/dist/services/profiles.js.map +1 -0
  213. package/mcp-server/dist/services/secret-gate.d.ts +29 -0
  214. package/mcp-server/dist/services/secret-gate.d.ts.map +1 -0
  215. package/mcp-server/dist/services/secret-gate.js +43 -0
  216. package/mcp-server/dist/services/secret-gate.js.map +1 -0
  217. package/mcp-server/dist/services/server-instructions.d.ts +10 -0
  218. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -0
  219. package/mcp-server/dist/services/server-instructions.js +101 -0
  220. package/mcp-server/dist/services/server-instructions.js.map +1 -0
  221. package/mcp-server/dist/services/sqlite.d.ts +235 -0
  222. package/mcp-server/dist/services/sqlite.d.ts.map +1 -0
  223. package/mcp-server/dist/services/sqlite.js +1185 -0
  224. package/mcp-server/dist/services/sqlite.js.map +1 -0
  225. package/mcp-server/dist/services/typesafe-bench.d.ts +99 -0
  226. package/mcp-server/dist/services/typesafe-bench.d.ts.map +1 -0
  227. package/mcp-server/dist/services/typesafe-bench.js +218 -0
  228. package/mcp-server/dist/services/typesafe-bench.js.map +1 -0
  229. package/mcp-server/dist/services/typesafe-config.d.ts +342 -0
  230. package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -0
  231. package/mcp-server/dist/services/typesafe-config.js +746 -0
  232. package/mcp-server/dist/services/typesafe-config.js.map +1 -0
  233. package/mcp-server/dist/services/typesafe.d.ts +213 -0
  234. package/mcp-server/dist/services/typesafe.d.ts.map +1 -0
  235. package/mcp-server/dist/services/typesafe.js +330 -0
  236. package/mcp-server/dist/services/typesafe.js.map +1 -0
  237. package/mcp-server/dist/services/vector-cache.d.ts +68 -0
  238. package/mcp-server/dist/services/vector-cache.d.ts.map +1 -0
  239. package/mcp-server/dist/services/vector-cache.js +271 -0
  240. package/mcp-server/dist/services/vector-cache.js.map +1 -0
  241. package/mcp-server/dist/services/whiteboard-geometry.d.ts +305 -0
  242. package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -0
  243. package/mcp-server/dist/services/whiteboard-geometry.js +666 -0
  244. package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -0
  245. package/mcp-server/dist/tools/agents.d.ts +212 -0
  246. package/mcp-server/dist/tools/agents.d.ts.map +1 -0
  247. package/mcp-server/dist/tools/agents.js +407 -0
  248. package/mcp-server/dist/tools/agents.js.map +1 -0
  249. package/mcp-server/dist/tools/bluesky.d.ts +39 -0
  250. package/mcp-server/dist/tools/bluesky.d.ts.map +1 -0
  251. package/mcp-server/dist/tools/bluesky.js +1144 -0
  252. package/mcp-server/dist/tools/bluesky.js.map +1 -0
  253. package/mcp-server/dist/tools/browser-advanced.d.ts +58 -0
  254. package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -0
  255. package/mcp-server/dist/tools/browser-advanced.js +106 -0
  256. package/mcp-server/dist/tools/browser-advanced.js.map +1 -0
  257. package/mcp-server/dist/tools/browser-batch.d.ts +99 -0
  258. package/mcp-server/dist/tools/browser-batch.d.ts.map +1 -0
  259. package/mcp-server/dist/tools/browser-batch.js +187 -0
  260. package/mcp-server/dist/tools/browser-batch.js.map +1 -0
  261. package/mcp-server/dist/tools/browser-cheatsheet.d.ts +21 -0
  262. package/mcp-server/dist/tools/browser-cheatsheet.d.ts.map +1 -0
  263. package/mcp-server/dist/tools/browser-cheatsheet.js +144 -0
  264. package/mcp-server/dist/tools/browser-cheatsheet.js.map +1 -0
  265. package/mcp-server/dist/tools/browser-interact.d.ts +239 -0
  266. package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -0
  267. package/mcp-server/dist/tools/browser-interact.js +300 -0
  268. package/mcp-server/dist/tools/browser-interact.js.map +1 -0
  269. package/mcp-server/dist/tools/browser-navigate.d.ts +90 -0
  270. package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -0
  271. package/mcp-server/dist/tools/browser-navigate.js +142 -0
  272. package/mcp-server/dist/tools/browser-navigate.js.map +1 -0
  273. package/mcp-server/dist/tools/browser-observe.d.ts +561 -0
  274. package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -0
  275. package/mcp-server/dist/tools/browser-observe.js +2009 -0
  276. package/mcp-server/dist/tools/browser-observe.js.map +1 -0
  277. package/mcp-server/dist/tools/browser.d.ts +34 -0
  278. package/mcp-server/dist/tools/browser.d.ts.map +1 -0
  279. package/mcp-server/dist/tools/browser.js +112 -0
  280. package/mcp-server/dist/tools/browser.js.map +1 -0
  281. package/mcp-server/dist/tools/card-tools.d.ts +82 -0
  282. package/mcp-server/dist/tools/card-tools.d.ts.map +1 -0
  283. package/mcp-server/dist/tools/card-tools.js +137 -0
  284. package/mcp-server/dist/tools/card-tools.js.map +1 -0
  285. package/mcp-server/dist/tools/card.d.ts +8 -0
  286. package/mcp-server/dist/tools/card.d.ts.map +1 -0
  287. package/mcp-server/dist/tools/card.js +16 -0
  288. package/mcp-server/dist/tools/card.js.map +1 -0
  289. package/mcp-server/dist/tools/category.d.ts +32 -0
  290. package/mcp-server/dist/tools/category.d.ts.map +1 -0
  291. package/mcp-server/dist/tools/category.js +290 -0
  292. package/mcp-server/dist/tools/category.js.map +1 -0
  293. package/mcp-server/dist/tools/choice.d.ts +160 -0
  294. package/mcp-server/dist/tools/choice.d.ts.map +1 -0
  295. package/mcp-server/dist/tools/choice.js +80 -0
  296. package/mcp-server/dist/tools/choice.js.map +1 -0
  297. package/mcp-server/dist/tools/computer.d.ts +71 -0
  298. package/mcp-server/dist/tools/computer.d.ts.map +1 -0
  299. package/mcp-server/dist/tools/computer.js +174 -0
  300. package/mcp-server/dist/tools/computer.js.map +1 -0
  301. package/mcp-server/dist/tools/discord-channel.d.ts +44 -0
  302. package/mcp-server/dist/tools/discord-channel.d.ts.map +1 -0
  303. package/mcp-server/dist/tools/discord-channel.js +236 -0
  304. package/mcp-server/dist/tools/discord-channel.js.map +1 -0
  305. package/mcp-server/dist/tools/discord-guild.d.ts +20 -0
  306. package/mcp-server/dist/tools/discord-guild.d.ts.map +1 -0
  307. package/mcp-server/dist/tools/discord-guild.js +138 -0
  308. package/mcp-server/dist/tools/discord-guild.js.map +1 -0
  309. package/mcp-server/dist/tools/discord-member.d.ts +26 -0
  310. package/mcp-server/dist/tools/discord-member.d.ts.map +1 -0
  311. package/mcp-server/dist/tools/discord-member.js +122 -0
  312. package/mcp-server/dist/tools/discord-member.js.map +1 -0
  313. package/mcp-server/dist/tools/discord-message.d.ts +36 -0
  314. package/mcp-server/dist/tools/discord-message.d.ts.map +1 -0
  315. package/mcp-server/dist/tools/discord-message.js +184 -0
  316. package/mcp-server/dist/tools/discord-message.js.map +1 -0
  317. package/mcp-server/dist/tools/discord-onboarding.d.ts +32 -0
  318. package/mcp-server/dist/tools/discord-onboarding.d.ts.map +1 -0
  319. package/mcp-server/dist/tools/discord-onboarding.js +179 -0
  320. package/mcp-server/dist/tools/discord-onboarding.js.map +1 -0
  321. package/mcp-server/dist/tools/discord-role.d.ts +34 -0
  322. package/mcp-server/dist/tools/discord-role.d.ts.map +1 -0
  323. package/mcp-server/dist/tools/discord-role.js +178 -0
  324. package/mcp-server/dist/tools/discord-role.js.map +1 -0
  325. package/mcp-server/dist/tools/discord-thread.d.ts +28 -0
  326. package/mcp-server/dist/tools/discord-thread.d.ts.map +1 -0
  327. package/mcp-server/dist/tools/discord-thread.js +88 -0
  328. package/mcp-server/dist/tools/discord-thread.js.map +1 -0
  329. package/mcp-server/dist/tools/discord-webhook.d.ts +32 -0
  330. package/mcp-server/dist/tools/discord-webhook.d.ts.map +1 -0
  331. package/mcp-server/dist/tools/discord-webhook.js +154 -0
  332. package/mcp-server/dist/tools/discord-webhook.js.map +1 -0
  333. package/mcp-server/dist/tools/discord.d.ts +7 -0
  334. package/mcp-server/dist/tools/discord.d.ts.map +1 -0
  335. package/mcp-server/dist/tools/discord.js +25 -0
  336. package/mcp-server/dist/tools/discord.js.map +1 -0
  337. package/mcp-server/dist/tools/fb-groups.d.ts +98 -0
  338. package/mcp-server/dist/tools/fb-groups.d.ts.map +1 -0
  339. package/mcp-server/dist/tools/fb-groups.js +369 -0
  340. package/mcp-server/dist/tools/fb-groups.js.map +1 -0
  341. package/mcp-server/dist/tools/forget.d.ts +14 -0
  342. package/mcp-server/dist/tools/forget.d.ts.map +1 -0
  343. package/mcp-server/dist/tools/forget.js +26 -0
  344. package/mcp-server/dist/tools/forget.js.map +1 -0
  345. package/mcp-server/dist/tools/git-tools.d.ts +24 -0
  346. package/mcp-server/dist/tools/git-tools.d.ts.map +1 -0
  347. package/mcp-server/dist/tools/git-tools.js +107 -0
  348. package/mcp-server/dist/tools/git-tools.js.map +1 -0
  349. package/mcp-server/dist/tools/git.d.ts +7 -0
  350. package/mcp-server/dist/tools/git.d.ts.map +1 -0
  351. package/mcp-server/dist/tools/git.js +11 -0
  352. package/mcp-server/dist/tools/git.js.map +1 -0
  353. package/mcp-server/dist/tools/gsc/experience.d.ts +135 -0
  354. package/mcp-server/dist/tools/gsc/experience.d.ts.map +1 -0
  355. package/mcp-server/dist/tools/gsc/experience.js +351 -0
  356. package/mcp-server/dist/tools/gsc/experience.js.map +1 -0
  357. package/mcp-server/dist/tools/gsc/helpers.d.ts +78 -0
  358. package/mcp-server/dist/tools/gsc/helpers.d.ts.map +1 -0
  359. package/mcp-server/dist/tools/gsc/helpers.js +258 -0
  360. package/mcp-server/dist/tools/gsc/helpers.js.map +1 -0
  361. package/mcp-server/dist/tools/gsc/indexing.d.ts +121 -0
  362. package/mcp-server/dist/tools/gsc/indexing.d.ts.map +1 -0
  363. package/mcp-server/dist/tools/gsc/indexing.js +357 -0
  364. package/mcp-server/dist/tools/gsc/indexing.js.map +1 -0
  365. package/mcp-server/dist/tools/gsc/inspect.d.ts +78 -0
  366. package/mcp-server/dist/tools/gsc/inspect.d.ts.map +1 -0
  367. package/mcp-server/dist/tools/gsc/inspect.js +321 -0
  368. package/mcp-server/dist/tools/gsc/inspect.js.map +1 -0
  369. package/mcp-server/dist/tools/gsc/navigate.d.ts +44 -0
  370. package/mcp-server/dist/tools/gsc/navigate.d.ts.map +1 -0
  371. package/mcp-server/dist/tools/gsc/navigate.js +197 -0
  372. package/mcp-server/dist/tools/gsc/navigate.js.map +1 -0
  373. package/mcp-server/dist/tools/gsc/performance.d.ts +84 -0
  374. package/mcp-server/dist/tools/gsc/performance.d.ts.map +1 -0
  375. package/mcp-server/dist/tools/gsc/performance.js +274 -0
  376. package/mcp-server/dist/tools/gsc/performance.js.map +1 -0
  377. package/mcp-server/dist/tools/gsc/settings.d.ts +153 -0
  378. package/mcp-server/dist/tools/gsc/settings.d.ts.map +1 -0
  379. package/mcp-server/dist/tools/gsc/settings.js +425 -0
  380. package/mcp-server/dist/tools/gsc/settings.js.map +1 -0
  381. package/mcp-server/dist/tools/gsc.d.ts +7 -0
  382. package/mcp-server/dist/tools/gsc.d.ts.map +1 -0
  383. package/mcp-server/dist/tools/gsc.js +51 -0
  384. package/mcp-server/dist/tools/gsc.js.map +1 -0
  385. package/mcp-server/dist/tools/image-tools.d.ts +24 -0
  386. package/mcp-server/dist/tools/image-tools.d.ts.map +1 -0
  387. package/mcp-server/dist/tools/image-tools.js +80 -0
  388. package/mcp-server/dist/tools/image-tools.js.map +1 -0
  389. package/mcp-server/dist/tools/image.d.ts +10 -0
  390. package/mcp-server/dist/tools/image.d.ts.map +1 -0
  391. package/mcp-server/dist/tools/image.js +14 -0
  392. package/mcp-server/dist/tools/image.js.map +1 -0
  393. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +97 -0
  394. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -0
  395. package/mcp-server/dist/tools/leonardo-browser-tools.js +228 -0
  396. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -0
  397. package/mcp-server/dist/tools/leonardo.d.ts +13 -0
  398. package/mcp-server/dist/tools/leonardo.d.ts.map +1 -0
  399. package/mcp-server/dist/tools/leonardo.js +21 -0
  400. package/mcp-server/dist/tools/leonardo.js.map +1 -0
  401. package/mcp-server/dist/tools/loop.d.ts +30 -0
  402. package/mcp-server/dist/tools/loop.d.ts.map +1 -0
  403. package/mcp-server/dist/tools/loop.js +401 -0
  404. package/mcp-server/dist/tools/loop.js.map +1 -0
  405. package/mcp-server/dist/tools/memories.d.ts +56 -0
  406. package/mcp-server/dist/tools/memories.d.ts.map +1 -0
  407. package/mcp-server/dist/tools/memories.js +171 -0
  408. package/mcp-server/dist/tools/memories.js.map +1 -0
  409. package/mcp-server/dist/tools/morelogin.d.ts +24 -0
  410. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -0
  411. package/mcp-server/dist/tools/morelogin.js +79 -0
  412. package/mcp-server/dist/tools/morelogin.js.map +1 -0
  413. package/mcp-server/dist/tools/profile.d.ts +17 -0
  414. package/mcp-server/dist/tools/profile.d.ts.map +1 -0
  415. package/mcp-server/dist/tools/profile.js +114 -0
  416. package/mcp-server/dist/tools/profile.js.map +1 -0
  417. package/mcp-server/dist/tools/recall.d.ts +71 -0
  418. package/mcp-server/dist/tools/recall.d.ts.map +1 -0
  419. package/mcp-server/dist/tools/recall.js +147 -0
  420. package/mcp-server/dist/tools/recall.js.map +1 -0
  421. package/mcp-server/dist/tools/reflect.d.ts +56 -0
  422. package/mcp-server/dist/tools/reflect.d.ts.map +1 -0
  423. package/mcp-server/dist/tools/reflect.js +157 -0
  424. package/mcp-server/dist/tools/reflect.js.map +1 -0
  425. package/mcp-server/dist/tools/remember.d.ts +62 -0
  426. package/mcp-server/dist/tools/remember.d.ts.map +1 -0
  427. package/mcp-server/dist/tools/remember.js +201 -0
  428. package/mcp-server/dist/tools/remember.js.map +1 -0
  429. package/mcp-server/dist/tools/response.d.ts +23 -0
  430. package/mcp-server/dist/tools/response.d.ts.map +1 -0
  431. package/mcp-server/dist/tools/response.js +24 -0
  432. package/mcp-server/dist/tools/response.js.map +1 -0
  433. package/mcp-server/dist/tools/restore.d.ts +14 -0
  434. package/mcp-server/dist/tools/restore.d.ts.map +1 -0
  435. package/mcp-server/dist/tools/restore.js +23 -0
  436. package/mcp-server/dist/tools/restore.js.map +1 -0
  437. package/mcp-server/dist/tools/style-guide-tools.d.ts +51 -0
  438. package/mcp-server/dist/tools/style-guide-tools.d.ts.map +1 -0
  439. package/mcp-server/dist/tools/style-guide-tools.js +231 -0
  440. package/mcp-server/dist/tools/style-guide-tools.js.map +1 -0
  441. package/mcp-server/dist/tools/style-guide.d.ts +8 -0
  442. package/mcp-server/dist/tools/style-guide.d.ts.map +1 -0
  443. package/mcp-server/dist/tools/style-guide.js +12 -0
  444. package/mcp-server/dist/tools/style-guide.js.map +1 -0
  445. package/mcp-server/dist/tools/sync.d.ts +20 -0
  446. package/mcp-server/dist/tools/sync.d.ts.map +1 -0
  447. package/mcp-server/dist/tools/sync.js +156 -0
  448. package/mcp-server/dist/tools/sync.js.map +1 -0
  449. package/mcp-server/dist/tools/tictactoe-tools.d.ts +18 -0
  450. package/mcp-server/dist/tools/tictactoe-tools.d.ts.map +1 -0
  451. package/mcp-server/dist/tools/tictactoe-tools.js +78 -0
  452. package/mcp-server/dist/tools/tictactoe-tools.js.map +1 -0
  453. package/mcp-server/dist/tools/tictactoe.d.ts +7 -0
  454. package/mcp-server/dist/tools/tictactoe.d.ts.map +1 -0
  455. package/mcp-server/dist/tools/tictactoe.js +11 -0
  456. package/mcp-server/dist/tools/tictactoe.js.map +1 -0
  457. package/mcp-server/dist/tools/utils.d.ts +19 -0
  458. package/mcp-server/dist/tools/utils.d.ts.map +1 -0
  459. package/mcp-server/dist/tools/utils.js +54 -0
  460. package/mcp-server/dist/tools/utils.js.map +1 -0
  461. package/mcp-server/dist/tools/whiteboard-format.d.ts +88 -0
  462. package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -0
  463. package/mcp-server/dist/tools/whiteboard-format.js +337 -0
  464. package/mcp-server/dist/tools/whiteboard-format.js.map +1 -0
  465. package/mcp-server/dist/tools/whiteboard-tools.d.ts +326 -0
  466. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -0
  467. package/mcp-server/dist/tools/whiteboard-tools.js +222 -0
  468. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -0
  469. package/mcp-server/dist/tools/whiteboard.d.ts +8 -0
  470. package/mcp-server/dist/tools/whiteboard.d.ts.map +1 -0
  471. package/mcp-server/dist/tools/whiteboard.js +16 -0
  472. package/mcp-server/dist/tools/whiteboard.js.map +1 -0
  473. package/mcp-server/dist/tools/youtube/analytics.d.ts +46 -0
  474. package/mcp-server/dist/tools/youtube/analytics.d.ts.map +1 -0
  475. package/mcp-server/dist/tools/youtube/analytics.js +83 -0
  476. package/mcp-server/dist/tools/youtube/analytics.js.map +1 -0
  477. package/mcp-server/dist/tools/youtube/dedup.d.ts +35 -0
  478. package/mcp-server/dist/tools/youtube/dedup.d.ts.map +1 -0
  479. package/mcp-server/dist/tools/youtube/dedup.js +56 -0
  480. package/mcp-server/dist/tools/youtube/dedup.js.map +1 -0
  481. package/mcp-server/dist/tools/youtube/download.d.ts +31 -0
  482. package/mcp-server/dist/tools/youtube/download.d.ts.map +1 -0
  483. package/mcp-server/dist/tools/youtube/download.js +30 -0
  484. package/mcp-server/dist/tools/youtube/download.js.map +1 -0
  485. package/mcp-server/dist/tools/youtube/helpers.d.ts +66 -0
  486. package/mcp-server/dist/tools/youtube/helpers.d.ts.map +1 -0
  487. package/mcp-server/dist/tools/youtube/helpers.js +143 -0
  488. package/mcp-server/dist/tools/youtube/helpers.js.map +1 -0
  489. package/mcp-server/dist/tools/youtube/media.d.ts +43 -0
  490. package/mcp-server/dist/tools/youtube/media.d.ts.map +1 -0
  491. package/mcp-server/dist/tools/youtube/media.js +97 -0
  492. package/mcp-server/dist/tools/youtube/media.js.map +1 -0
  493. package/mcp-server/dist/tools/youtube/metadata.d.ts +33 -0
  494. package/mcp-server/dist/tools/youtube/metadata.d.ts.map +1 -0
  495. package/mcp-server/dist/tools/youtube/metadata.js +106 -0
  496. package/mcp-server/dist/tools/youtube/metadata.js.map +1 -0
  497. package/mcp-server/dist/tools/youtube/navigate.d.ts +30 -0
  498. package/mcp-server/dist/tools/youtube/navigate.d.ts.map +1 -0
  499. package/mcp-server/dist/tools/youtube/navigate.js +71 -0
  500. package/mcp-server/dist/tools/youtube/navigate.js.map +1 -0
  501. package/mcp-server/dist/tools/youtube/playlist.d.ts +29 -0
  502. package/mcp-server/dist/tools/youtube/playlist.d.ts.map +1 -0
  503. package/mcp-server/dist/tools/youtube/playlist.js +128 -0
  504. package/mcp-server/dist/tools/youtube/playlist.js.map +1 -0
  505. package/mcp-server/dist/tools/youtube/schedule.d.ts +31 -0
  506. package/mcp-server/dist/tools/youtube/schedule.d.ts.map +1 -0
  507. package/mcp-server/dist/tools/youtube/schedule.js +147 -0
  508. package/mcp-server/dist/tools/youtube/schedule.js.map +1 -0
  509. package/mcp-server/dist/tools/youtube/source.d.ts +36 -0
  510. package/mcp-server/dist/tools/youtube/source.d.ts.map +1 -0
  511. package/mcp-server/dist/tools/youtube/source.js +85 -0
  512. package/mcp-server/dist/tools/youtube/source.js.map +1 -0
  513. package/mcp-server/dist/tools/youtube/upload.d.ts +53 -0
  514. package/mcp-server/dist/tools/youtube/upload.d.ts.map +1 -0
  515. package/mcp-server/dist/tools/youtube/upload.js +255 -0
  516. package/mcp-server/dist/tools/youtube/upload.js.map +1 -0
  517. package/mcp-server/dist/tools/youtube.d.ts +12 -0
  518. package/mcp-server/dist/tools/youtube.d.ts.map +1 -0
  519. package/mcp-server/dist/tools/youtube.js +38 -0
  520. package/mcp-server/dist/tools/youtube.js.map +1 -0
  521. package/mcp-server/dist/tui.d.ts +3 -0
  522. package/mcp-server/dist/tui.d.ts.map +1 -0
  523. package/mcp-server/dist/tui.js +806 -0
  524. package/mcp-server/dist/tui.js.map +1 -0
  525. package/mcp-server/dist/types.d.ts +77 -0
  526. package/mcp-server/dist/types.d.ts.map +1 -0
  527. package/mcp-server/dist/types.js +2 -0
  528. package/mcp-server/dist/types.js.map +1 -0
  529. package/mcp-server/package-lock.json +4798 -0
  530. package/mcp-server/package.json +55 -38
  531. package/mcp-server/run.mjs +12 -2
  532. package/mcp-server/src/config.ts +48 -3
  533. package/mcp-server/src/http.ts +296 -93
  534. package/mcp-server/src/index.ts +546 -203
  535. package/mcp-server/src/preload-http.ts +19 -18
  536. package/mcp-server/src/preload.ts +19 -18
  537. package/mcp-server/src/scripts/setup-embeddings.ts +9 -0
  538. package/mcp-server/src/services/assistant-judgments.ts +157 -0
  539. package/mcp-server/src/services/browser-assist-gate.ts +383 -0
  540. package/mcp-server/src/services/browser-assist.ts +267 -0
  541. package/mcp-server/src/services/browser-bench.ts +477 -0
  542. package/mcp-server/src/services/browser-judgments.ts +414 -0
  543. package/mcp-server/src/services/browser-risk.ts +484 -0
  544. package/mcp-server/src/services/categories.ts +36 -19
  545. package/mcp-server/src/services/codex-config-heal.ts +403 -0
  546. package/mcp-server/src/services/desktop-assist-gate.ts +251 -0
  547. package/mcp-server/src/services/desktop-assist.ts +308 -0
  548. package/mcp-server/src/services/desktop-bench.ts +372 -0
  549. package/mcp-server/src/services/desktop-client.ts +76 -0
  550. package/mcp-server/src/services/desktop-judgments.ts +218 -0
  551. package/mcp-server/src/services/desktop-risk.ts +743 -0
  552. package/mcp-server/src/services/edit-stale.ts +299 -0
  553. package/mcp-server/src/services/embedding-service.ts +108 -0
  554. package/mcp-server/src/services/embedding-worker.ts +46 -0
  555. package/mcp-server/src/services/fb-regions.ts +221 -0
  556. package/mcp-server/src/services/file-checksums.ts +26 -9
  557. package/mcp-server/src/services/file-excerpt.ts +59 -0
  558. package/mcp-server/src/services/identity.ts +291 -0
  559. package/mcp-server/src/services/local-embeddings.ts +2 -77
  560. package/mcp-server/src/services/memory-client.ts +23 -0
  561. package/mcp-server/src/services/memory-judge-backfill.ts +462 -0
  562. package/mcp-server/src/services/memory-judgments.ts +901 -0
  563. package/mcp-server/src/services/memory-maintenance.ts +360 -0
  564. package/mcp-server/src/services/memory-retrieval.ts +349 -0
  565. package/mcp-server/src/services/memory-schema.ts +280 -0
  566. package/mcp-server/src/services/memory-sessions.ts +87 -0
  567. package/mcp-server/src/services/memory-triage.ts +182 -0
  568. package/mcp-server/src/services/memory-verification.ts +68 -0
  569. package/mcp-server/src/services/memory-writer.ts +125 -0
  570. package/mcp-server/src/services/neural-interface.ts +1217 -73
  571. package/mcp-server/src/services/profiles.ts +428 -0
  572. package/mcp-server/src/services/secret-gate.ts +52 -0
  573. package/mcp-server/src/services/server-instructions.ts +122 -0
  574. package/mcp-server/src/services/sqlite.ts +1456 -823
  575. package/mcp-server/src/services/typesafe-bench.ts +247 -0
  576. package/mcp-server/src/services/typesafe-config.ts +790 -0
  577. package/mcp-server/src/services/typesafe.ts +390 -0
  578. package/mcp-server/src/services/vector-cache.ts +255 -0
  579. package/mcp-server/src/services/whiteboard-geometry.ts +711 -0
  580. package/mcp-server/src/tools/agents.ts +539 -0
  581. package/mcp-server/src/tools/bluesky.ts +1186 -0
  582. package/mcp-server/src/tools/browser-advanced.ts +15 -6
  583. package/mcp-server/src/tools/browser-batch.ts +178 -0
  584. package/mcp-server/src/tools/browser-cheatsheet.ts +155 -0
  585. package/mcp-server/src/tools/browser-interact.ts +362 -255
  586. package/mcp-server/src/tools/browser-navigate.ts +164 -77
  587. package/mcp-server/src/tools/browser-observe.ts +2222 -1330
  588. package/mcp-server/src/tools/browser.ts +181 -107
  589. package/mcp-server/src/tools/card.ts +7 -5
  590. package/mcp-server/src/tools/choice.ts +103 -0
  591. package/mcp-server/src/tools/computer.ts +190 -0
  592. package/mcp-server/src/tools/discord.ts +10 -23
  593. package/mcp-server/src/tools/fb-groups.ts +387 -0
  594. package/mcp-server/src/tools/forget.ts +1 -1
  595. package/mcp-server/src/tools/git.ts +3 -1
  596. package/mcp-server/src/tools/gsc/experience.ts +440 -0
  597. package/mcp-server/src/tools/gsc/helpers.ts +288 -0
  598. package/mcp-server/src/tools/gsc/indexing.ts +495 -0
  599. package/mcp-server/src/tools/gsc/inspect.ts +391 -0
  600. package/mcp-server/src/tools/gsc/navigate.ts +236 -0
  601. package/mcp-server/src/tools/gsc/performance.ts +352 -0
  602. package/mcp-server/src/tools/gsc/settings.ts +564 -0
  603. package/mcp-server/src/tools/gsc.ts +95 -0
  604. package/mcp-server/src/tools/image.ts +3 -1
  605. package/mcp-server/src/tools/leonardo-browser-tools.ts +47 -37
  606. package/mcp-server/src/tools/leonardo.ts +7 -5
  607. package/mcp-server/src/tools/loop.ts +118 -25
  608. package/mcp-server/src/tools/memories.ts +65 -5
  609. package/mcp-server/src/tools/morelogin.ts +81 -0
  610. package/mcp-server/src/tools/profile.ts +122 -0
  611. package/mcp-server/src/tools/recall.ts +105 -261
  612. package/mcp-server/src/tools/reflect.ts +171 -145
  613. package/mcp-server/src/tools/remember.ts +223 -115
  614. package/mcp-server/src/tools/style-guide-tools.ts +250 -0
  615. package/mcp-server/src/tools/style-guide.ts +16 -0
  616. package/mcp-server/src/tools/sync.ts +66 -9
  617. package/mcp-server/src/tools/tictactoe.ts +3 -1
  618. package/mcp-server/src/tools/utils.ts +22 -0
  619. package/mcp-server/src/tools/whiteboard-format.ts +394 -0
  620. package/mcp-server/src/tools/whiteboard-tools.ts +191 -183
  621. package/mcp-server/src/tools/whiteboard.ts +7 -5
  622. package/mcp-server/src/tools/youtube/analytics.ts +106 -0
  623. package/mcp-server/src/tools/youtube/dedup.ts +57 -0
  624. package/mcp-server/src/tools/youtube/download.ts +41 -0
  625. package/mcp-server/src/tools/youtube/helpers.ts +183 -0
  626. package/mcp-server/src/tools/youtube/media.ts +109 -0
  627. package/mcp-server/src/tools/youtube/metadata.ts +112 -0
  628. package/mcp-server/src/tools/youtube/navigate.ts +81 -0
  629. package/mcp-server/src/tools/youtube/playlist.ts +134 -0
  630. package/mcp-server/src/tools/youtube/schedule.ts +158 -0
  631. package/mcp-server/src/tools/youtube/source.ts +101 -0
  632. package/mcp-server/src/tools/youtube/upload.ts +296 -0
  633. package/mcp-server/src/tools/youtube.ts +47 -0
  634. package/mcp-server/src/tui.ts +8 -9
  635. package/neural-interface/games/TicTacToe/1.svg +1 -0
  636. package/neural-interface/games/TicTacToe/2.svg +1 -0
  637. package/neural-interface/games/TicTacToe/3.svg +1 -0
  638. package/neural-interface/games/TicTacToe/4.svg +1 -0
  639. package/neural-interface/games/TicTacToe/5.svg +1 -0
  640. package/neural-interface/games/TicTacToe/6.svg +1 -0
  641. package/neural-interface/games/TicTacToe/7.svg +1 -0
  642. package/neural-interface/games/TicTacToe/8.svg +1 -0
  643. package/neural-interface/games/TicTacToe/9.svg +1 -0
  644. package/neural-interface/games/TicTacToe/Board.svg +1 -0
  645. package/neural-interface/games/TicTacToe/Circle.svg +1 -0
  646. package/neural-interface/games/TicTacToe/Cross.svg +1 -0
  647. package/neural-interface/i18n/en.json +6983 -0
  648. package/neural-interface/i18n/pt-BR.json +6987 -0
  649. package/neural-interface/lib/assistant-api.js +1005 -0
  650. package/neural-interface/lib/assistant-attachments.js +121 -0
  651. package/neural-interface/lib/assistant-brains/claude.js +207 -0
  652. package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +43 -0
  653. package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +56 -0
  654. package/neural-interface/lib/assistant-brains/codex.js +213 -0
  655. package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +47 -0
  656. package/neural-interface/lib/assistant-brains/opencode-route-gate.js +62 -0
  657. package/neural-interface/lib/assistant-brains/opencode.js +320 -0
  658. package/neural-interface/lib/assistant-budget.js +318 -0
  659. package/neural-interface/lib/assistant-catalog.js +731 -0
  660. package/neural-interface/lib/assistant-clarify.js +664 -0
  661. package/neural-interface/lib/assistant-config.js +232 -0
  662. package/neural-interface/lib/assistant-dispatch.js +2744 -0
  663. package/neural-interface/lib/assistant-envelope.js +579 -0
  664. package/neural-interface/lib/assistant-evidence.js +236 -0
  665. package/neural-interface/lib/assistant-hidden-models.js +272 -0
  666. package/neural-interface/lib/assistant-jev.js +117 -0
  667. package/neural-interface/lib/assistant-judgments.js +111 -0
  668. package/neural-interface/lib/assistant-media.js +335 -0
  669. package/neural-interface/lib/assistant-memory.js +398 -0
  670. package/neural-interface/lib/assistant-persona.js +444 -0
  671. package/neural-interface/lib/assistant-plan-permissions.js +448 -0
  672. package/neural-interface/lib/assistant-playbooks/design.md +87 -0
  673. package/neural-interface/lib/assistant-playbooks.js +72 -0
  674. package/neural-interface/lib/assistant-pricing.js +159 -0
  675. package/neural-interface/lib/assistant-route-gate-codex.js +163 -0
  676. package/neural-interface/lib/assistant-route-gate.js +212 -0
  677. package/neural-interface/lib/assistant-router.js +1324 -0
  678. package/neural-interface/lib/assistant-runtime.js +3337 -0
  679. package/neural-interface/lib/assistant-task-prompt.js +472 -0
  680. package/neural-interface/lib/assistant-usage.js +1688 -0
  681. package/neural-interface/lib/augmented-path.js +50 -0
  682. package/neural-interface/lib/automation-storage-migration.js +54 -0
  683. package/neural-interface/lib/backup-service.js +851 -0
  684. package/neural-interface/lib/backup-zip-reader.js +262 -0
  685. package/neural-interface/lib/browser-assist-api.js +41 -0
  686. package/neural-interface/lib/browser-console.js +176 -0
  687. package/neural-interface/lib/browser-execution.js +241 -0
  688. package/neural-interface/lib/browser-extraction.js +191 -0
  689. package/neural-interface/lib/browser-screenshot.js +399 -0
  690. package/neural-interface/lib/browser-semantic-context.js +639 -0
  691. package/neural-interface/lib/browser-snapshots.js +192 -0
  692. package/neural-interface/lib/browser-tab-recovery.js +119 -0
  693. package/neural-interface/lib/browser-targets.js +70 -0
  694. package/neural-interface/lib/browser-tool-policy.js +926 -0
  695. package/neural-interface/lib/claude-accounts.js +230 -0
  696. package/neural-interface/lib/claude-agent-bridge.js +3296 -0
  697. package/neural-interface/lib/claude-bypass-policy.js +99 -0
  698. package/neural-interface/lib/claude-history.js +429 -0
  699. package/neural-interface/lib/claude-model-catalog.js +483 -0
  700. package/neural-interface/lib/claude-panel-requests.js +165 -0
  701. package/neural-interface/lib/claude-panel-session.js +272 -0
  702. package/neural-interface/lib/claude-permission-rules.js +153 -0
  703. package/neural-interface/lib/claude-session-cleanup.js +71 -0
  704. package/neural-interface/lib/claude-session-list.js +82 -0
  705. package/neural-interface/lib/claude-session-ops.js +140 -0
  706. package/neural-interface/lib/claude-settings-view.js +51 -0
  707. package/neural-interface/lib/claude-skin-connect.js +79 -0
  708. package/neural-interface/lib/claude-stall-policy.js +7 -0
  709. package/neural-interface/lib/claude-temporary.js +435 -0
  710. package/neural-interface/lib/cli-update-planner.js +312 -0
  711. package/neural-interface/lib/codex-capabilities.d.ts +41 -0
  712. package/neural-interface/lib/codex-capabilities.js +148 -0
  713. package/neural-interface/lib/codex-collaboration-mode.js +17 -0
  714. package/neural-interface/lib/codex-compat-rpc.js +241 -0
  715. package/neural-interface/lib/codex-mcp-approvals.js +56 -0
  716. package/neural-interface/lib/codex-mcp-apps.js +47 -0
  717. package/neural-interface/lib/codex-model-context.js +202 -0
  718. package/neural-interface/lib/codex-plan-store.js +71 -0
  719. package/neural-interface/lib/codex-request-journal.js +242 -0
  720. package/neural-interface/lib/codex-runtime-path.js +213 -0
  721. package/neural-interface/lib/codex-session-catalog.js +231 -0
  722. package/neural-interface/lib/codex-session-settings.js +46 -0
  723. package/neural-interface/lib/codex-sidepanel-permissions.js +110 -0
  724. package/neural-interface/lib/codex-thread-history.js +165 -0
  725. package/neural-interface/lib/codex-user-verification.js +34 -0
  726. package/neural-interface/lib/codex-writer-lifecycle.js +152 -0
  727. package/neural-interface/lib/confined-fs.js +256 -0
  728. package/neural-interface/lib/db.js +922 -0
  729. package/neural-interface/lib/design-md.js +9 -0
  730. package/neural-interface/lib/desktop/api.js +92 -0
  731. package/neural-interface/lib/desktop/audit.js +62 -0
  732. package/neural-interface/lib/desktop/build.js +423 -0
  733. package/neural-interface/lib/desktop/config.js +606 -0
  734. package/neural-interface/lib/desktop/coords.js +76 -0
  735. package/neural-interface/lib/desktop/fake-helper.js +1192 -0
  736. package/neural-interface/lib/desktop/frames.js +77 -0
  737. package/neural-interface/lib/desktop/grants.js +84 -0
  738. package/neural-interface/lib/desktop/guards.js +170 -0
  739. package/neural-interface/lib/desktop/helper/SynabunDesktop.swift +3056 -0
  740. package/neural-interface/lib/desktop/lease.js +51 -0
  741. package/neural-interface/lib/desktop/manager.js +545 -0
  742. package/neural-interface/lib/desktop/press-contexts.js +103 -0
  743. package/neural-interface/lib/desktop/protocol.js +495 -0
  744. package/neural-interface/lib/desktop/service.js +1202 -0
  745. package/neural-interface/lib/effort-levels.js +148 -0
  746. package/neural-interface/lib/event-loop-monitor.js +67 -0
  747. package/neural-interface/lib/exec-loop-command.js +209 -0
  748. package/neural-interface/lib/folder-picker.ps1 +77 -0
  749. package/neural-interface/lib/http-guards.js +287 -0
  750. package/neural-interface/lib/jsonl-lines.js +94 -0
  751. package/neural-interface/lib/loop-logger.js +127 -0
  752. package/neural-interface/lib/mcp-client-config.js +133 -0
  753. package/neural-interface/lib/mcp-installer.js +487 -0
  754. package/neural-interface/lib/mcp-profile-refresh-coordinator.js +78 -0
  755. package/neural-interface/lib/mcp-profile-servers.js +120 -0
  756. package/neural-interface/lib/memo-cache.js +84 -0
  757. package/neural-interface/lib/memory-api.js +758 -0
  758. package/neural-interface/lib/memory-map-api.js +1150 -0
  759. package/neural-interface/lib/memory-map-layout.js +872 -0
  760. package/neural-interface/lib/memory-map-worker.js +165 -0
  761. package/neural-interface/lib/memory-session-capture.js +81 -0
  762. package/neural-interface/lib/memory-timers.js +62 -0
  763. package/neural-interface/lib/morelogin.js +301 -0
  764. package/neural-interface/lib/native-binary-runtime.js +650 -0
  765. package/neural-interface/lib/native-loop-goal.js +33 -0
  766. package/neural-interface/lib/native-loop-providers.js +1016 -0
  767. package/neural-interface/lib/native-loop-runtime.js +747 -0
  768. package/neural-interface/lib/opencode-hidden-models.js +113 -0
  769. package/neural-interface/lib/opencode-history.js +253 -0
  770. package/neural-interface/lib/opencode-runtime-config.js +58 -0
  771. package/neural-interface/lib/opencode-v2-client.js +486 -0
  772. package/neural-interface/lib/opencode-v2-ws-requests.js +955 -0
  773. package/neural-interface/lib/path-confine.js +213 -0
  774. package/neural-interface/lib/plugin-installer.js +264 -0
  775. package/neural-interface/lib/project-storage.js +766 -0
  776. package/neural-interface/lib/pty-host/core.js +603 -0
  777. package/neural-interface/lib/pty-host/flow.js +43 -0
  778. package/neural-interface/lib/pty-host/host.js +47 -0
  779. package/neural-interface/lib/pty-host/manager.js +360 -0
  780. package/neural-interface/lib/pty-host/protocol.js +81 -0
  781. package/neural-interface/lib/pty-host/snapshot.js +133 -0
  782. package/neural-interface/lib/remote-policy.js +582 -0
  783. package/neural-interface/lib/rulesets/api.js +175 -0
  784. package/neural-interface/lib/rulesets/fs-safe.js +245 -0
  785. package/neural-interface/lib/rulesets/installer.js +1453 -0
  786. package/neural-interface/lib/rulesets/json-edit.js +186 -0
  787. package/neural-interface/lib/rulesets/lock.js +170 -0
  788. package/neural-interface/lib/rulesets/managed-text.js +314 -0
  789. package/neural-interface/lib/rulesets/normalise.js +24 -0
  790. package/neural-interface/lib/rulesets/render.js +84 -0
  791. package/neural-interface/lib/schedule-launch-intent.js +40 -0
  792. package/neural-interface/lib/session-cache-builder.js +524 -0
  793. package/neural-interface/lib/session-chunker.js +344 -0
  794. package/neural-interface/lib/session-indexer.js +327 -0
  795. package/neural-interface/lib/session-title-generator.js +227 -0
  796. package/neural-interface/lib/skill-prompt-bridge.js +11 -0
  797. package/neural-interface/lib/style-guide/api.js +375 -0
  798. package/neural-interface/lib/style-guide/color.js +352 -0
  799. package/neural-interface/lib/style-guide/export-css.js +99 -0
  800. package/neural-interface/lib/style-guide/export-dtcg.js +209 -0
  801. package/neural-interface/lib/style-guide/export-tailwind.js +102 -0
  802. package/neural-interface/lib/style-guide/fonts.json +227 -0
  803. package/neural-interface/lib/style-guide/import.js +1268 -0
  804. package/neural-interface/lib/style-guide/pointers.js +103 -0
  805. package/neural-interface/lib/style-guide/presets.js +232 -0
  806. package/neural-interface/lib/style-guide/render-design-md.js +319 -0
  807. package/neural-interface/lib/style-guide/schema.js +848 -0
  808. package/neural-interface/lib/style-guide/security.js +43 -0
  809. package/neural-interface/lib/style-guide/store.js +491 -0
  810. package/neural-interface/lib/style-guide/summary.js +106 -0
  811. package/neural-interface/lib/style-guide/tokens.js +170 -0
  812. package/neural-interface/lib/synabun-update-plan.js +305 -0
  813. package/neural-interface/lib/synabun-version.js +179 -0
  814. package/neural-interface/lib/system-restore.js +99 -0
  815. package/neural-interface/lib/terminal-env.js +86 -0
  816. package/neural-interface/lib/ui-blob-store.js +106 -0
  817. package/neural-interface/lib/vector-cache.js +2 -0
  818. package/neural-interface/lib/virtual-socket.js +60 -0
  819. package/neural-interface/lib/whatsapp/api.js +146 -0
  820. package/neural-interface/lib/whatsapp/auth-store.js +389 -0
  821. package/neural-interface/lib/whatsapp/baileys-adapter.js +457 -0
  822. package/neural-interface/lib/whatsapp/brain.js +128 -0
  823. package/neural-interface/lib/whatsapp/bridge.js +1499 -0
  824. package/neural-interface/lib/whatsapp/cards.js +519 -0
  825. package/neural-interface/lib/whatsapp/commands.js +92 -0
  826. package/neural-interface/lib/whatsapp/config.js +291 -0
  827. package/neural-interface/lib/whatsapp/connector/entry.mjs +25 -0
  828. package/neural-interface/lib/whatsapp/connector/manifest.json +6 -0
  829. package/neural-interface/lib/whatsapp/connector/npm-shrinkwrap.json +717 -0
  830. package/neural-interface/lib/whatsapp/connector/package.json +8 -0
  831. package/neural-interface/lib/whatsapp/fake-baileys.js +444 -0
  832. package/neural-interface/lib/whatsapp/format.js +552 -0
  833. package/neural-interface/lib/whatsapp/host-core.js +1225 -0
  834. package/neural-interface/lib/whatsapp/host.js +119 -0
  835. package/neural-interface/lib/whatsapp/identity.js +288 -0
  836. package/neural-interface/lib/whatsapp/inbound.js +149 -0
  837. package/neural-interface/lib/whatsapp/installer.js +562 -0
  838. package/neural-interface/lib/whatsapp/manager.js +638 -0
  839. package/neural-interface/lib/whatsapp/paths.js +81 -0
  840. package/neural-interface/lib/whatsapp/protocol.js +327 -0
  841. package/neural-interface/lib/whatsapp/qr-svg.js +57 -0
  842. package/neural-interface/lib/whatsapp/redact.js +130 -0
  843. package/neural-interface/lib/whatsapp/service.js +1305 -0
  844. package/neural-interface/lib/whatsapp/vendor/qrcodegen.js +848 -0
  845. package/neural-interface/package-lock.json +4682 -0
  846. package/neural-interface/package.json +58 -27
  847. package/neural-interface/public/apple-touch-icon.png +0 -0
  848. package/neural-interface/public/category-logos/synabun.png +0 -0
  849. package/neural-interface/public/claude.svg +1 -0
  850. package/neural-interface/public/favicon-16x16.png +0 -0
  851. package/neural-interface/public/favicon-32x32.png +0 -0
  852. package/neural-interface/public/icon-192.png +0 -0
  853. package/neural-interface/public/icon-512.png +0 -0
  854. package/neural-interface/public/index.html +32 -182
  855. package/neural-interface/public/index2d.html +2 -2
  856. package/neural-interface/public/logo-synabun.png +0 -0
  857. package/neural-interface/public/logoHD.png +0 -0
  858. package/neural-interface/public/mcp.svg +4 -0
  859. package/neural-interface/public/offline.html +335 -299
  860. package/neural-interface/public/onboarding.html +692 -155
  861. package/neural-interface/public/opencode-logo-dark-square.svg +1 -0
  862. package/neural-interface/public/shared/agent-runtime-options.js +596 -0
  863. package/neural-interface/public/shared/api.js +339 -28
  864. package/neural-interface/public/shared/assistant/asst-accounts.js +318 -0
  865. package/neural-interface/public/shared/assistant/asst-agents-dock.js +296 -0
  866. package/neural-interface/public/shared/assistant/asst-api.js +270 -0
  867. package/neural-interface/public/shared/assistant/asst-brain-picker.js +744 -0
  868. package/neural-interface/public/shared/assistant/asst-budget.js +333 -0
  869. package/neural-interface/public/shared/assistant/asst-composer.js +656 -0
  870. package/neural-interface/public/shared/assistant/asst-computer.js +803 -0
  871. package/neural-interface/public/shared/assistant/asst-control.js +1018 -0
  872. package/neural-interface/public/shared/assistant/asst-hosts.js +262 -0
  873. package/neural-interface/public/shared/assistant/asst-mascot.js +311 -0
  874. package/neural-interface/public/shared/assistant/asst-menu.js +375 -0
  875. package/neural-interface/public/shared/assistant/asst-panel.js +1845 -0
  876. package/neural-interface/public/shared/assistant/asst-rack-styles.js +589 -0
  877. package/neural-interface/public/shared/assistant/asst-render.js +3296 -0
  878. package/neural-interface/public/shared/assistant/asst-route.js +1653 -0
  879. package/neural-interface/public/shared/assistant/asst-roving.js +75 -0
  880. package/neural-interface/public/shared/assistant/asst-sanitize.js +117 -0
  881. package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +488 -0
  882. package/neural-interface/public/shared/assistant/asst-sidepanel.js +675 -0
  883. package/neural-interface/public/shared/assistant/asst-slash.js +258 -0
  884. package/neural-interface/public/shared/assistant/asst-state.js +727 -0
  885. package/neural-interface/public/shared/assistant/asst-styles.js +1684 -0
  886. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +1017 -0
  887. package/neural-interface/public/shared/assistant/asst-usage.js +444 -0
  888. package/neural-interface/public/shared/assistant/asst-ws.js +175 -0
  889. package/neural-interface/public/shared/automation-context.js +75 -0
  890. package/neural-interface/public/shared/blob-store.js +99 -0
  891. package/neural-interface/public/shared/cdx/cdx-capabilities.js +118 -0
  892. package/neural-interface/public/shared/cdx/cdx-cards.js +37 -0
  893. package/neural-interface/public/shared/cdx/cdx-commands.js +171 -0
  894. package/neural-interface/public/shared/cdx/cdx-context-menu.js +402 -0
  895. package/neural-interface/public/shared/cdx/cdx-context-model.js +220 -0
  896. package/neural-interface/public/shared/cdx/cdx-features.js +408 -0
  897. package/neural-interface/public/shared/cdx/cdx-icons.js +89 -0
  898. package/neural-interface/public/shared/cdx/cdx-mcp-app-host.js +414 -0
  899. package/neural-interface/public/shared/cdx/cdx-mcp-app-proxy.html +6 -0
  900. package/neural-interface/public/shared/cdx/cdx-mcp-app-proxy.js +56 -0
  901. package/neural-interface/public/shared/cdx/cdx-mcp-app.js +178 -0
  902. package/neural-interface/public/shared/cdx/cdx-model-catalog.js +120 -0
  903. package/neural-interface/public/shared/cdx/cdx-output-model.js +261 -0
  904. package/neural-interface/public/shared/cdx/cdx-output.js +382 -0
  905. package/neural-interface/public/shared/cdx/cdx-panel.js +1372 -0
  906. package/neural-interface/public/shared/cdx/cdx-plan.js +266 -0
  907. package/neural-interface/public/shared/cdx/cdx-protocol.js +682 -0
  908. package/neural-interface/public/shared/cdx/cdx-render.js +3381 -0
  909. package/neural-interface/public/shared/cdx/cdx-requests.js +1136 -0
  910. package/neural-interface/public/shared/cdx/cdx-styles.js +3251 -0
  911. package/neural-interface/public/shared/cdx/cdx-tabs.js +7992 -0
  912. package/neural-interface/public/shared/cdx/cdx-ws.js +363 -0
  913. package/neural-interface/public/shared/cli-status.js +169 -0
  914. package/neural-interface/public/shared/constants.js +22 -5
  915. package/neural-interface/public/shared/cp/cp-agents.js +186 -0
  916. package/neural-interface/public/shared/cp/cp-bash.js +252 -0
  917. package/neural-interface/public/shared/cp/cp-compose.js +159 -0
  918. package/neural-interface/public/shared/cp/cp-context-menu.js +309 -0
  919. package/neural-interface/public/shared/cp/cp-context-model.js +157 -0
  920. package/neural-interface/public/shared/cp/cp-context-window.js +80 -0
  921. package/neural-interface/public/shared/cp/cp-ctx.js +11 -0
  922. package/neural-interface/public/shared/cp/cp-diff.js +255 -0
  923. package/neural-interface/public/shared/cp/cp-event-rows.js +637 -0
  924. package/neural-interface/public/shared/cp/cp-events.js +846 -0
  925. package/neural-interface/public/shared/cp/cp-markdown.js +104 -0
  926. package/neural-interface/public/shared/cp/cp-permission-model.js +905 -0
  927. package/neural-interface/public/shared/cp/cp-permissions.js +626 -0
  928. package/neural-interface/public/shared/cp/cp-rehydrate.js +113 -0
  929. package/neural-interface/public/shared/cp/cp-restore.js +224 -0
  930. package/neural-interface/public/shared/cp/cp-session-model.js +338 -0
  931. package/neural-interface/public/shared/cp/cp-session-settings.js +254 -0
  932. package/neural-interface/public/shared/cp/cp-sessions.js +227 -0
  933. package/neural-interface/public/shared/cp/cp-statusline.js +153 -0
  934. package/neural-interface/public/shared/cp/cp-styles.js +349 -0
  935. package/neural-interface/public/shared/cp/cp-tasks-model.js +282 -0
  936. package/neural-interface/public/shared/cp/cp-tasks.js +107 -0
  937. package/neural-interface/public/shared/cp/cp-temporary-view.js +56 -0
  938. package/neural-interface/public/shared/cp/cp-temporary.js +231 -0
  939. package/neural-interface/public/shared/cp/cp-tool-cards.js +326 -0
  940. package/neural-interface/public/shared/cp/cp-tool-results.js +618 -0
  941. package/neural-interface/public/shared/cp/cp-usage-model.js +124 -0
  942. package/neural-interface/public/shared/graph-activity.js +68 -0
  943. package/neural-interface/public/shared/html-shell.js +107 -12
  944. package/neural-interface/public/shared/i18n.js +132 -34
  945. package/neural-interface/public/shared/node-positions-store.js +44 -0
  946. package/neural-interface/public/shared/ocp-hidden-models.js +116 -0
  947. package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +355 -0
  948. package/neural-interface/public/shared/ocp-v2/ocp-v2-autoaccept-button.js +35 -0
  949. package/neural-interface/public/shared/ocp-v2/ocp-v2-automation-ownership.js +72 -0
  950. package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +280 -0
  951. package/neural-interface/public/shared/ocp-v2/ocp-v2-caps.js +67 -0
  952. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +179 -0
  953. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes.js +164 -0
  954. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +725 -0
  955. package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +75 -0
  956. package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +1204 -0
  957. package/neural-interface/public/shared/ocp-v2/ocp-v2-confirm-logic.js +117 -0
  958. package/neural-interface/public/shared/ocp-v2/ocp-v2-confirm.js +59 -0
  959. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-gauge.js +252 -0
  960. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +478 -0
  961. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +179 -0
  962. package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +233 -0
  963. package/neural-interface/public/shared/ocp-v2/ocp-v2-icons.js +30 -0
  964. package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +1065 -0
  965. package/neural-interface/public/shared/ocp-v2/ocp-v2-mention-hints.js +182 -0
  966. package/neural-interface/public/shared/ocp-v2/ocp-v2-modelpicker.js +655 -0
  967. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +2636 -0
  968. package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +601 -0
  969. package/neural-interface/public/shared/ocp-v2/ocp-v2-profile-inheritance.js +78 -0
  970. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +649 -0
  971. package/neural-interface/public/shared/ocp-v2/ocp-v2-providers.js +35 -0
  972. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +357 -0
  973. package/neural-interface/public/shared/ocp-v2/ocp-v2-render-logic.js +189 -0
  974. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +1869 -0
  975. package/neural-interface/public/shared/ocp-v2/ocp-v2-select.js +42 -0
  976. package/neural-interface/public/shared/ocp-v2/ocp-v2-send-logic.js +117 -0
  977. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +1714 -0
  978. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +468 -0
  979. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-widgets.js +63 -0
  980. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +366 -0
  981. package/neural-interface/public/shared/ocp-v2/ocp-v2-settings-link.js +19 -0
  982. package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +343 -0
  983. package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +746 -0
  984. package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +347 -0
  985. package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +338 -0
  986. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +3543 -0
  987. package/neural-interface/public/shared/ocp-v2/ocp-v2-subagents-logic.js +482 -0
  988. package/neural-interface/public/shared/ocp-v2/ocp-v2-tools-logic.js +351 -0
  989. package/neural-interface/public/shared/ocp-v2/ocp-v2-tools.js +168 -0
  990. package/neural-interface/public/shared/ocp-v2/ocp-v2-variantpicker.js +315 -0
  991. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +545 -0
  992. package/neural-interface/public/shared/provider-icons.js +30 -0
  993. package/neural-interface/public/shared/session-title.js +100 -0
  994. package/neural-interface/public/shared/settings/settings-ia.js +389 -0
  995. package/neural-interface/public/shared/settings/settings-kit.js +234 -0
  996. package/neural-interface/public/shared/settings/settings-search.js +463 -0
  997. package/neural-interface/public/shared/settings/settings-shell.js +570 -0
  998. package/neural-interface/public/shared/state.js +5 -0
  999. package/neural-interface/public/shared/storage.js +50 -7
  1000. package/neural-interface/public/shared/styleguide/sg-agents.js +42 -0
  1001. package/neural-interface/public/shared/styleguide/sg-brand.js +4 -0
  1002. package/neural-interface/public/shared/styleguide/sg-color-client.js +388 -0
  1003. package/neural-interface/public/shared/styleguide/sg-colors.js +148 -0
  1004. package/neural-interface/public/shared/styleguide/sg-components.js +35 -0
  1005. package/neural-interface/public/shared/styleguide/sg-history.js +41 -0
  1006. package/neural-interface/public/shared/styleguide/sg-icons-logo.js +38 -0
  1007. package/neural-interface/public/shared/styleguide/sg-imagery.js +4 -0
  1008. package/neural-interface/public/shared/styleguide/sg-import-export.js +47 -0
  1009. package/neural-interface/public/shared/styleguide/sg-kit.js +281 -0
  1010. package/neural-interface/public/shared/styleguide/sg-labels.js +318 -0
  1011. package/neural-interface/public/shared/styleguide/sg-layout.js +4 -0
  1012. package/neural-interface/public/shared/styleguide/sg-motion.js +5 -0
  1013. package/neural-interface/public/shared/styleguide/sg-panel.js +229 -0
  1014. package/neural-interface/public/shared/styleguide/sg-preview.js +138 -0
  1015. package/neural-interface/public/shared/styleguide/sg-rules.js +9 -0
  1016. package/neural-interface/public/shared/styleguide/sg-shape.js +24 -0
  1017. package/neural-interface/public/shared/styleguide/sg-state.js +132 -0
  1018. package/neural-interface/public/shared/styleguide/sg-typography.js +124 -0
  1019. package/neural-interface/public/shared/styles.css +7883 -1155
  1020. package/neural-interface/public/shared/synabun-mascot.js +1625 -0
  1021. package/neural-interface/public/shared/synabun-ticker.js +376 -0
  1022. package/neural-interface/public/shared/term-renderer.js +343 -0
  1023. package/neural-interface/public/shared/ui-assistant-panel.js +10 -0
  1024. package/neural-interface/public/shared/ui-assistant.js +77 -0
  1025. package/neural-interface/public/shared/ui-automation-studio.js +5105 -3573
  1026. package/neural-interface/public/shared/ui-claude-panel.js +11685 -6333
  1027. package/neural-interface/public/shared/ui-codex-panel.js +12 -0
  1028. package/neural-interface/public/shared/ui-cost-widget.js +55 -34
  1029. package/neural-interface/public/shared/ui-detail.js +77 -14
  1030. package/neural-interface/public/shared/ui-explorer.js +360 -181
  1031. package/neural-interface/public/shared/ui-file-explorer.js +485 -107
  1032. package/neural-interface/public/shared/ui-help.js +2 -2
  1033. package/neural-interface/public/shared/ui-image-gallery.js +45 -21
  1034. package/neural-interface/public/shared/ui-judgments.js +1346 -0
  1035. package/neural-interface/public/shared/ui-keybinds.js +3 -2
  1036. package/neural-interface/public/shared/ui-link.js +105 -51
  1037. package/neural-interface/public/shared/ui-loading.js +4 -4
  1038. package/neural-interface/public/shared/ui-memory-maintenance.js +111 -0
  1039. package/neural-interface/public/shared/ui-menubar.js +89 -4
  1040. package/neural-interface/public/shared/ui-native-loop-router-state.js +94 -0
  1041. package/neural-interface/public/shared/ui-native-loop-router.js +493 -0
  1042. package/neural-interface/public/shared/ui-native-window-id.js +7 -0
  1043. package/neural-interface/public/shared/ui-navbar.js +357 -222
  1044. package/neural-interface/public/shared/ui-notifications.js +58 -12
  1045. package/neural-interface/public/shared/ui-opencode-panel-v2.js +17 -0
  1046. package/neural-interface/public/shared/ui-panels.js +15 -7
  1047. package/neural-interface/public/shared/ui-resume.js +823 -653
  1048. package/neural-interface/public/shared/ui-schedule-queue.js +172 -0
  1049. package/neural-interface/public/shared/ui-schedules-studio.js +1882 -0
  1050. package/neural-interface/public/shared/ui-scroll-follow.js +194 -0
  1051. package/neural-interface/public/shared/ui-sessions.js +789 -34
  1052. package/neural-interface/public/shared/ui-settings.js +5793 -2190
  1053. package/neural-interface/public/shared/ui-sidepanel-layout.js +109 -0
  1054. package/neural-interface/public/shared/ui-sidepanel-runtime.js +19 -0
  1055. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +40 -0
  1056. package/neural-interface/public/shared/ui-sidepanel-sessions.js +221 -0
  1057. package/neural-interface/public/shared/ui-sidepanel-state.js +131 -0
  1058. package/neural-interface/public/shared/ui-sidepanel-tray.js +547 -0
  1059. package/neural-interface/public/shared/ui-sidepanel-windows.js +387 -0
  1060. package/neural-interface/public/shared/ui-skills.js +18 -32
  1061. package/neural-interface/public/shared/ui-styleguide.js +35 -0
  1062. package/neural-interface/public/shared/ui-sync.js +87 -0
  1063. package/neural-interface/public/shared/ui-terminal.js +7877 -6385
  1064. package/neural-interface/public/shared/ui-tool-updates.js +292 -0
  1065. package/neural-interface/public/shared/ui-tooltip.js +156 -15
  1066. package/neural-interface/public/shared/ui-tutorial-draw.js +119 -683
  1067. package/neural-interface/public/shared/ui-tutorial-layout.js +135 -0
  1068. package/neural-interface/public/shared/ui-tutorial-steps.js +59 -2209
  1069. package/neural-interface/public/shared/ui-tutorial.css +102 -0
  1070. package/neural-interface/public/shared/ui-tutorial.js +557 -1076
  1071. package/neural-interface/public/shared/ui-update.js +420 -68
  1072. package/neural-interface/public/shared/ui-whatsapp.js +941 -0
  1073. package/neural-interface/public/shared/ui-whiteboard.js +1076 -233
  1074. package/neural-interface/public/shared/ui-workspaces.js +2 -1
  1075. package/neural-interface/public/shared/utils.js +58 -0
  1076. package/neural-interface/public/shared/vterm-buffer.js +15 -5
  1077. package/neural-interface/public/shared/whatsapp/wa-phone.js +65 -0
  1078. package/neural-interface/public/shared/whatsapp/wa-view.js +411 -0
  1079. package/neural-interface/public/sidepanel-session.html +18 -0
  1080. package/neural-interface/public/sw.js +8 -2
  1081. package/neural-interface/public/synabun.png +0 -0
  1082. package/neural-interface/public/variant/2d/graph.js +1707 -1635
  1083. package/neural-interface/public/variant/2d/lasso.js +2 -1
  1084. package/neural-interface/public/variant/2d/layout.js +595 -594
  1085. package/neural-interface/public/variant/2d/main.js +556 -521
  1086. package/neural-interface/public/variant/2d/settings-gfx.js +18 -24
  1087. package/neural-interface/public/variant/3d/main.js +516 -500
  1088. package/neural-interface/public/variant/3d/map-controls.js +144 -0
  1089. package/neural-interface/public/variant/3d/map-data.js +251 -0
  1090. package/neural-interface/public/variant/3d/map-labels.js +241 -0
  1091. package/neural-interface/public/variant/3d/map-pick.js +90 -0
  1092. package/neural-interface/public/variant/3d/map-renderer.js +521 -0
  1093. package/neural-interface/public/variant/3d/map-shaders.js +158 -0
  1094. package/neural-interface/public/variant/3d/map-view.js +618 -0
  1095. package/neural-interface/public/vendor/three/manifest.json +3 -0
  1096. package/neural-interface/public/vendor/three/three-0.169.0.min.js +3844 -0
  1097. package/neural-interface/public/vendor/xterm/manifest.json +3 -0
  1098. package/neural-interface/public/vendor/xterm/xterm-6.0.0.css +285 -0
  1099. package/neural-interface/public/vendor/xterm/xterm-6.0.0.min.js +192 -0
  1100. package/neural-interface/scripts/build-vendor.mjs +64 -0
  1101. package/neural-interface/scripts/codex-protocol-fixture.mjs +83 -0
  1102. package/neural-interface/scripts/rebuild-pty.js +37 -23
  1103. package/neural-interface/scripts/vendor-three-entry.js +24 -0
  1104. package/neural-interface/scripts/vendor-xterm-entry.js +8 -0
  1105. package/neural-interface/server.js +33948 -15000
  1106. package/neural-interface/skins/default/skin.css +90 -0
  1107. package/neural-interface/skins/default/skin.json +8 -0
  1108. package/neural-interface/templates/rulesets/README.md +9 -0
  1109. package/neural-interface/templates/rulesets/claude.md +3 -0
  1110. package/neural-interface/templates/rulesets/codex.md +2 -0
  1111. package/neural-interface/templates/rulesets/coexistence.md +6 -0
  1112. package/neural-interface/templates/rulesets/core.md +26 -0
  1113. package/neural-interface/templates/rulesets/cursor.md +2 -0
  1114. package/neural-interface/templates/rulesets/gemini.md +2 -0
  1115. package/neural-interface/templates/rulesets/legacy-hashes.json +227 -0
  1116. package/neural-interface/templates/rulesets/manifest.json +12 -0
  1117. package/neural-interface/templates/rulesets/opencode.md +2 -0
  1118. package/package.json +119 -53
  1119. package/postinstall.js +125 -26
  1120. package/preuninstall.js +187 -0
  1121. package/scripts/audit-claude-hook-usage.mjs +12 -0
  1122. package/scripts/audit-memory-duplicates.mjs +44 -0
  1123. package/scripts/measure-claude-hook-overhead.mjs +77 -0
  1124. package/scripts/recover-user-config.mjs +254 -0
  1125. package/scripts/restore-data-home-from-backup.mjs +239 -0
  1126. package/setup.js +804 -260
  1127. package/skills/leonardo/SKILL.md +19 -10
  1128. package/skills/leonardo/modules/image-prompter.md +18 -12
  1129. package/skills/leonardo/modules/video-prompter.md +19 -13
  1130. package/skills/synabun/SKILL.md +209 -185
  1131. package/skills/synabun/icon.png +0 -0
  1132. package/skills/synabun/modules/audit.md +380 -374
  1133. package/skills/synabun/modules/changelog.md +15 -6
  1134. package/skills/synabun/modules/idea.md +148 -144
  1135. package/skills/synabun/modules/memorize.md +13 -4
  1136. package/skills/synabun/modules/schedule.md +198 -0
  1137. package/skills/synabun/synabunicon.png +0 -0
  1138. package/updater.cmd +31 -0
  1139. package/updater.mjs +382 -0
  1140. package/updater.sh +29 -0
  1141. package/hooks/claude-code/test-user-learning.mjs +0 -531
  1142. package/neural-interface/public/claude-chat.css +0 -878
  1143. package/neural-interface/public/claude-chat.html +0 -92
  1144. package/neural-interface/public/claude-chat.js +0 -1168
  1145. package/neural-interface/public/index-legacy.html +0 -14677
  1146. package/neural-interface/public/index2d-legacy.html +0 -8193
  1147. package/neural-interface/public/shared/html-term-renderer.js +0 -1022
  1148. package/neural-interface/public/variant/3d/background.js +0 -884
  1149. package/neural-interface/public/variant/3d/camera.js +0 -661
  1150. package/neural-interface/public/variant/3d/gfx.js +0 -116
  1151. package/neural-interface/public/variant/3d/graph.js +0 -2399
  1152. package/neural-interface/public/variant/3d/settings-gfx.js +0 -228
  1153. package/neural-interface/templates/CLAUDE-template.md +0 -211
@@ -1,823 +1,1456 @@
1
- /**
2
- * SQLite storage layer.
3
- * Uses Node.js built-in node:sqlite (available since Node 22.5.0).
4
- * Vectors stored as Float32Array BLOBs, cosine similarity computed in JS.
5
- */
6
-
7
- import { DatabaseSync } from 'node:sqlite';
8
- import path from 'path';
9
- import fs from 'fs';
10
- import { config } from '../config.js';
11
- import { getAllCategories } from './categories.js';
12
- import type { MemoryPayload, MemoryStats, SessionChunkPayload } from '../types.js';
13
-
14
- type SQLValue = null | number | bigint | string | Uint8Array;
15
-
16
- // Re-export for backward compat (was used by categories.ts)
17
- export const CATEGORIES_POINT_ID = '00000000-0000-0000-0000-000000000000';
18
-
19
- let db: DatabaseSync | null = null;
20
-
21
- function getDbPath(): string {
22
- const envPath = process.env.SQLITE_DB_PATH;
23
- const defaultPath = path.join(config.dataDir, 'memory.db');
24
- if (!envPath) return defaultPath;
25
- // If the env path's parent directory doesn't exist (e.g. Windows path on Mac),
26
- // fall back to the local default so cross-OS restores work instantly
27
- if (!fs.existsSync(path.dirname(envPath))) return defaultPath;
28
- return envPath;
29
- }
30
-
31
- function getDb(): DatabaseSync {
32
- if (!db) {
33
- db = new DatabaseSync(getDbPath());
34
- db.exec('PRAGMA journal_mode = WAL');
35
- db.exec('PRAGMA foreign_keys = ON');
36
- db.exec('PRAGMA busy_timeout = 5000');
37
- }
38
- return db;
39
- }
40
-
41
- // --- Vector encoding/decoding ---
42
-
43
- function encodeVector(vector: number[]): Uint8Array {
44
- const f32 = new Float32Array(vector);
45
- return new Uint8Array(f32.buffer);
46
- }
47
-
48
- function decodeVector(blob: Uint8Array): number[] {
49
- const f32 = new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4);
50
- return Array.from(f32);
51
- }
52
-
53
- /**
54
- * Cosine similarity for normalized vectors (equals dot product).
55
- * Vectors from Transformers.js with normalize:true are unit-length.
56
- */
57
- function cosineSimilarity(a: number[], b: number[]): number {
58
- let dot = 0;
59
- for (let i = 0; i < a.length; i++) {
60
- dot += a[i] * b[i];
61
- }
62
- return dot;
63
- }
64
-
65
- // --- Filter translation ---
66
-
67
- interface FilterCondition {
68
- key: string;
69
- match?: { value: string | number };
70
- range?: { gte?: number; lte?: number; gt?: number; lt?: number };
71
- is_empty?: { key: string };
72
- }
73
-
74
- interface MemoryFilter {
75
- must?: FilterCondition[];
76
- must_not?: FilterCondition[];
77
- should?: FilterCondition[];
78
- }
79
-
80
- /**
81
- * Translates JSON filters to SQL WHERE clauses.
82
- * Supports: exact match, range, tags (JSON array contains), is_empty.
83
- */
84
- function translateFilter(filter?: Record<string, unknown>): { where: string; params: SQLValue[] } {
85
- if (!filter) return { where: '', params: [] };
86
-
87
- const f = filter as MemoryFilter;
88
- const clauses: string[] = [];
89
- const params: SQLValue[] = [];
90
-
91
- for (const cond of f.must || []) {
92
- if (cond.match) {
93
- if (cond.key === 'tags') {
94
- clauses.push(`EXISTS (SELECT 1 FROM json_each(tags) WHERE json_each.value = ?)`);
95
- params.push(cond.match.value);
96
- } else {
97
- clauses.push(`${sanitizeColumn(cond.key)} = ?`);
98
- params.push(cond.match.value);
99
- }
100
- } else if (cond.range) {
101
- const col = sanitizeColumn(cond.key);
102
- if (cond.range.gte !== undefined) { clauses.push(`${col} >= ?`); params.push(cond.range.gte); }
103
- if (cond.range.lte !== undefined) { clauses.push(`${col} <= ?`); params.push(cond.range.lte); }
104
- if (cond.range.gt !== undefined) { clauses.push(`${col} > ?`); params.push(cond.range.gt); }
105
- if (cond.range.lt !== undefined) { clauses.push(`${col} < ?`); params.push(cond.range.lt); }
106
- } else if (cond.is_empty) {
107
- clauses.push(`(${sanitizeColumn(cond.is_empty.key)} IS NULL OR ${sanitizeColumn(cond.is_empty.key)} = '')`);
108
- }
109
- }
110
-
111
- for (const cond of f.must_not || []) {
112
- if (cond.match) {
113
- clauses.push(`(${sanitizeColumn(cond.key)} IS NULL OR ${sanitizeColumn(cond.key)} != ?)`);
114
- params.push(cond.match.value);
115
- }
116
- }
117
-
118
- return {
119
- where: clauses.length > 0 ? ' AND ' + clauses.join(' AND ') : '',
120
- params,
121
- };
122
- }
123
-
124
- /** Allowlist of valid column names to prevent SQL injection */
125
- const VALID_COLUMNS = new Set([
126
- 'id', 'content', 'category', 'subcategory', 'project', 'tags', 'importance',
127
- 'source', 'created_at', 'updated_at', 'accessed_at', 'access_count',
128
- 'related_files', 'related_memory_ids', 'file_checksums', 'trashed_at',
129
- 'source_session_chunks', '_type',
130
- // session_chunks columns
131
- 'session_id', 'git_branch', 'cwd', 'chunk_index', 'start_timestamp',
132
- 'end_timestamp', 'tools_used', 'files_modified', 'files_read',
133
- 'user_messages', 'turn_count', 'dedup_memory_id', 'indexed_at', 'summary',
134
- ]);
135
-
136
- function sanitizeColumn(name: string): string {
137
- if (!VALID_COLUMNS.has(name)) {
138
- throw new Error(`Invalid column name: ${name}`);
139
- }
140
- return name;
141
- }
142
-
143
- // --- Schema initialization ---
144
-
145
- const SCHEMA_SQL = `
146
- CREATE TABLE IF NOT EXISTS memories (
147
- id TEXT PRIMARY KEY,
148
- vector BLOB NOT NULL,
149
- content TEXT NOT NULL,
150
- category TEXT NOT NULL,
151
- subcategory TEXT,
152
- project TEXT NOT NULL,
153
- tags TEXT NOT NULL DEFAULT '[]',
154
- importance INTEGER NOT NULL DEFAULT 5,
155
- source TEXT NOT NULL DEFAULT 'self-discovered',
156
- created_at TEXT NOT NULL,
157
- updated_at TEXT NOT NULL,
158
- accessed_at TEXT NOT NULL,
159
- access_count INTEGER NOT NULL DEFAULT 0,
160
- related_files TEXT,
161
- related_memory_ids TEXT,
162
- file_checksums TEXT,
163
- trashed_at TEXT,
164
- source_session_chunks TEXT
165
- );
166
-
167
- CREATE TABLE IF NOT EXISTS session_chunks (
168
- id TEXT PRIMARY KEY,
169
- vector BLOB NOT NULL,
170
- content TEXT NOT NULL,
171
- summary TEXT,
172
- session_id TEXT,
173
- project TEXT,
174
- git_branch TEXT,
175
- cwd TEXT,
176
- chunk_index INTEGER DEFAULT 0,
177
- start_timestamp TEXT,
178
- end_timestamp TEXT,
179
- tools_used TEXT DEFAULT '[]',
180
- files_modified TEXT DEFAULT '[]',
181
- files_read TEXT DEFAULT '[]',
182
- user_messages TEXT DEFAULT '[]',
183
- turn_count INTEGER DEFAULT 0,
184
- related_memory_ids TEXT DEFAULT '[]',
185
- dedup_memory_id TEXT,
186
- indexed_at TEXT
187
- );
188
-
189
- CREATE TABLE IF NOT EXISTS categories (
190
- name TEXT PRIMARY KEY,
191
- description TEXT NOT NULL,
192
- created_at TEXT NOT NULL,
193
- parent TEXT,
194
- color TEXT,
195
- is_parent INTEGER DEFAULT 0
196
- );
197
-
198
- CREATE INDEX IF NOT EXISTS idx_mem_category ON memories(category);
199
- CREATE INDEX IF NOT EXISTS idx_mem_project ON memories(project);
200
- CREATE INDEX IF NOT EXISTS idx_mem_importance ON memories(importance);
201
- CREATE INDEX IF NOT EXISTS idx_mem_trashed ON memories(trashed_at);
202
- CREATE INDEX IF NOT EXISTS idx_mem_created ON memories(created_at);
203
- CREATE INDEX IF NOT EXISTS idx_mem_source ON memories(source);
204
- CREATE INDEX IF NOT EXISTS idx_sc_session ON session_chunks(session_id);
205
- CREATE INDEX IF NOT EXISTS idx_sc_project ON session_chunks(project);
206
- CREATE INDEX IF NOT EXISTS idx_sc_branch ON session_chunks(git_branch);
207
-
208
- CREATE TABLE IF NOT EXISTS kv_config (
209
- key TEXT PRIMARY KEY,
210
- value TEXT NOT NULL
211
- );
212
- `;
213
-
214
- // FTS5 created separately since CREATE VIRTUAL TABLE IF NOT EXISTS
215
- // can fail silently on some SQLite versions
216
- const FTS_SQL = `
217
- CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
218
- content, category, project, tags,
219
- content=memories, content_rowid=rowid,
220
- tokenize='porter unicode61'
221
- );
222
- `;
223
-
224
- // --- Public API: Collection initialization ---
225
-
226
- export async function ensureCollection(): Promise<void> {
227
- const d = getDb();
228
- d.exec(SCHEMA_SQL);
229
- try {
230
- d.exec(FTS_SQL);
231
- } catch {
232
- // FTS5 may already exist or not be available
233
- }
234
- }
235
-
236
- export async function ensureSessionCollection(): Promise<void> {
237
- // No-op: session_chunks table is created in ensureCollection()
238
- }
239
-
240
- /** Combined init function for cleaner startup */
241
- export async function ensureDatabase(): Promise<void> {
242
- await ensureCollection();
243
- }
244
-
245
- // --- Public API: Memory operations ---
246
-
247
- export async function upsertMemory(
248
- id: string,
249
- vector: number[],
250
- payload: MemoryPayload
251
- ): Promise<void> {
252
- const d = getDb();
253
- const stmt = d.prepare(`
254
- INSERT OR REPLACE INTO memories
255
- (id, vector, content, category, subcategory, project, tags, importance, source,
256
- created_at, updated_at, accessed_at, access_count, related_files,
257
- related_memory_ids, file_checksums, trashed_at, source_session_chunks)
258
- VALUES
259
- (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
260
- `);
261
- stmt.run(
262
- id,
263
- encodeVector(vector),
264
- payload.content,
265
- payload.category,
266
- payload.subcategory ?? null,
267
- payload.project,
268
- JSON.stringify(payload.tags || []),
269
- payload.importance ?? 5,
270
- payload.source || 'self-discovered',
271
- payload.created_at,
272
- payload.updated_at,
273
- payload.accessed_at,
274
- payload.access_count ?? 0,
275
- payload.related_files ? JSON.stringify(payload.related_files) : null,
276
- payload.related_memory_ids ? JSON.stringify(payload.related_memory_ids) : null,
277
- payload.file_checksums ? JSON.stringify(payload.file_checksums) : null,
278
- payload.trashed_at ?? null,
279
- payload.source_session_chunks ? JSON.stringify(payload.source_session_chunks) : null,
280
- );
281
-
282
- // Update FTS index
283
- try {
284
- d.prepare(`INSERT OR REPLACE INTO memories_fts(rowid, content, category, project, tags)
285
- SELECT rowid, content, category, project, tags FROM memories WHERE id = ?`).run(id);
286
- } catch {
287
- // FTS update failure is non-fatal
288
- }
289
- }
290
-
291
- export async function searchMemories(
292
- vector: number[],
293
- limit: number,
294
- filter?: Record<string, unknown>,
295
- scoreThreshold?: number
296
- ) {
297
- const d = getDb();
298
- const threshold = scoreThreshold ?? 0.3;
299
-
300
- // Build WHERE clause from filter
301
- const { where, params } = translateFilter(filter);
302
-
303
- // Fetch all candidate rows (non-trashed) with their vectors
304
- const rows = d.prepare(`
305
- SELECT id, vector, content, category, subcategory, project, tags, importance, source,
306
- created_at, updated_at, accessed_at, access_count, related_files,
307
- related_memory_ids, file_checksums, trashed_at, source_session_chunks
308
- FROM memories
309
- WHERE trashed_at IS NULL${where}
310
- `).all(...params) as Array<Record<string, unknown>>;
311
-
312
- // Compute cosine similarity and rank
313
- const scored = rows.map((row) => {
314
- const rowVector = decodeVector(row.vector as Uint8Array);
315
- const score = cosineSimilarity(vector, rowVector);
316
- return { row, score };
317
- }).filter((r) => r.score >= threshold);
318
-
319
- scored.sort((a, b) => b.score - a.score);
320
-
321
- return scored.slice(0, limit).map((r) => ({
322
- id: r.row.id as string,
323
- score: r.score,
324
- payload: rowToPayload(r.row),
325
- }));
326
- }
327
-
328
- export async function getMemory(id: string) {
329
- const d = getDb();
330
- const row = d.prepare(`
331
- SELECT id, content, category, subcategory, project, tags, importance, source,
332
- created_at, updated_at, accessed_at, access_count, related_files,
333
- related_memory_ids, file_checksums, trashed_at, source_session_chunks
334
- FROM memories WHERE id = ?
335
- `).get(id) as Record<string, unknown> | undefined;
336
-
337
- if (!row) return null;
338
- return { id: row.id as string, payload: rowToPayload(row) };
339
- }
340
-
341
- export async function updatePayload(
342
- id: string,
343
- payload: Partial<MemoryPayload>
344
- ): Promise<void> {
345
- const d = getDb();
346
- const sets: string[] = [];
347
- const params: SQLValue[] = [];
348
-
349
- for (const [key, value] of Object.entries(payload)) {
350
- if (key === 'id' || key === 'vector') continue;
351
- const col = sanitizeColumn(key);
352
- if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
353
- sets.push(`${col} = ?`);
354
- params.push(JSON.stringify(value));
355
- } else {
356
- sets.push(`${col} = ?`);
357
- params.push((value ?? null) as SQLValue);
358
- }
359
- }
360
-
361
- if (sets.length === 0) return;
362
- params.push(id);
363
- d.prepare(`UPDATE memories SET ${sets.join(', ')} WHERE id = ?`).run(...params);
364
-
365
- // Update FTS if content changed
366
- if (payload.content) {
367
- try {
368
- d.prepare(`INSERT OR REPLACE INTO memories_fts(rowid, content, category, project, tags)
369
- SELECT rowid, content, category, project, tags FROM memories WHERE id = ?`).run(id);
370
- } catch { /* non-fatal */ }
371
- }
372
- }
373
-
374
- export async function updateVector(
375
- id: string,
376
- vector: number[],
377
- payload: MemoryPayload
378
- ): Promise<void> {
379
- // Full upsert — replaces both vector and payload
380
- await upsertMemory(id, vector, payload);
381
- }
382
-
383
- export async function updatePayloadByFilter(
384
- filter: Record<string, unknown>,
385
- payload: Partial<MemoryPayload>
386
- ): Promise<void> {
387
- const d = getDb();
388
- const { where, params: filterParams } = translateFilter(filter);
389
-
390
- const sets: string[] = [];
391
- const setParams: SQLValue[] = [];
392
-
393
- for (const [key, value] of Object.entries(payload)) {
394
- if (key === 'id' || key === 'vector') continue;
395
- const col = sanitizeColumn(key);
396
- if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
397
- sets.push(`${col} = ?`);
398
- setParams.push(JSON.stringify(value));
399
- } else {
400
- sets.push(`${col} = ?`);
401
- setParams.push((value ?? null) as SQLValue);
402
- }
403
- }
404
-
405
- if (sets.length === 0) return;
406
- d.prepare(`UPDATE memories SET ${sets.join(', ')} WHERE trashed_at IS NULL${where}`)
407
- .run(...setParams, ...filterParams);
408
- }
409
-
410
- export async function deleteMemory(id: string): Promise<void> {
411
- const d = getDb();
412
- d.prepare('DELETE FROM memories WHERE id = ?').run(id);
413
- try {
414
- d.prepare('DELETE FROM memories_fts WHERE rowid = (SELECT rowid FROM memories WHERE id = ?)').run(id);
415
- } catch { /* non-fatal */ }
416
- }
417
-
418
- export async function softDeleteMemory(id: string): Promise<void> {
419
- await updatePayload(id, { trashed_at: new Date().toISOString() } as Partial<MemoryPayload>);
420
- }
421
-
422
- export async function restoreMemory(id: string): Promise<void> {
423
- const d = getDb();
424
- d.prepare('UPDATE memories SET trashed_at = NULL WHERE id = ?').run(id);
425
- }
426
-
427
- export async function scrollMemories(
428
- filter?: Record<string, unknown>,
429
- limit: number = 20,
430
- offset?: string
431
- ) {
432
- const d = getDb();
433
- const { where, params } = translateFilter(filter);
434
- const numericOffset = offset ? parseInt(offset, 10) : 0;
435
-
436
- const rows = d.prepare(`
437
- SELECT id, content, category, subcategory, project, tags, importance, source,
438
- created_at, updated_at, accessed_at, access_count, related_files,
439
- related_memory_ids, file_checksums, trashed_at, source_session_chunks
440
- FROM memories
441
- WHERE trashed_at IS NULL${where}
442
- ORDER BY created_at DESC
443
- LIMIT ? OFFSET ?
444
- `).all(...params, limit, numericOffset) as Array<Record<string, unknown>>;
445
-
446
- const points = rows.map((row) => ({
447
- id: row.id as string,
448
- payload: rowToPayload(row),
449
- }));
450
-
451
- // Determine next offset
452
- const nextOffset = rows.length === limit ? String(numericOffset + limit) : null;
453
-
454
- return { points, next_page_offset: nextOffset };
455
- }
456
-
457
- export async function countMemories(filter?: Record<string, unknown>): Promise<number> {
458
- const d = getDb();
459
- const { where, params } = translateFilter(filter);
460
- const result = d.prepare(`
461
- SELECT COUNT(*) as cnt FROM memories WHERE trashed_at IS NULL${where}
462
- `).get(...params) as { cnt: number };
463
- return result.cnt;
464
- }
465
-
466
- export async function getMemoryStats(): Promise<MemoryStats> {
467
- const d = getDb();
468
-
469
- const totalRow = d.prepare('SELECT COUNT(*) as cnt FROM memories WHERE trashed_at IS NULL').get() as { cnt: number };
470
- const total = totalRow.cnt;
471
-
472
- // Per-category counts using SQL aggregate
473
- const catRows = d.prepare(`
474
- SELECT category, COUNT(*) as cnt FROM memories WHERE trashed_at IS NULL GROUP BY category
475
- `).all() as Array<{ category: string; cnt: number }>;
476
- const by_category: Record<string, number> = {};
477
- // Initialize with all known categories at 0
478
- for (const cat of getAllCategories()) {
479
- by_category[cat] = 0;
480
- }
481
- for (const row of catRows) {
482
- by_category[row.category] = row.cnt;
483
- }
484
-
485
- // Per-project counts
486
- const projRows = d.prepare(`
487
- SELECT project, COUNT(*) as cnt FROM memories WHERE trashed_at IS NULL GROUP BY project
488
- `).all() as Array<{ project: string; cnt: number }>;
489
- const by_project: Record<string, number> = {};
490
- for (const row of projRows) {
491
- by_project[row.project || 'global'] = row.cnt;
492
- }
493
-
494
- // Oldest/newest
495
- const dateRow = d.prepare(`
496
- SELECT MIN(created_at) as oldest, MAX(created_at) as newest FROM memories WHERE trashed_at IS NULL
497
- `).get() as { oldest: string | null; newest: string | null };
498
-
499
- return {
500
- total,
501
- by_category,
502
- by_project,
503
- oldest: dateRow.oldest ?? undefined,
504
- newest: dateRow.newest ?? undefined,
505
- };
506
- }
507
-
508
- // --- FTS5 full-text search ---
509
-
510
- /**
511
- * Search memories using FTS5 full-text search (keyword fallback).
512
- * Returns results scored by BM25 relevance. Used when vector similarity
513
- * scores are low (exact identifiers, error codes, proper nouns).
514
- */
515
- export async function searchMemoriesFTS(
516
- query: string,
517
- limit: number,
518
- filter?: Record<string, unknown>,
519
- excludeIds?: Set<string>
520
- ): Promise<Array<{ id: string; score: number; payload: MemoryPayload }>> {
521
- const d = getDb();
522
-
523
- // Sanitize query for FTS5: escape double quotes, wrap terms
524
- const ftsQuery = query
525
- .replace(/"/g, '""')
526
- .split(/\s+/)
527
- .filter(t => t.length > 1)
528
- .map(t => `"${t}"`)
529
- .join(' OR ');
530
-
531
- if (!ftsQuery) return [];
532
-
533
- const { where, params } = translateFilter(filter);
534
-
535
- try {
536
- const rows = d.prepare(`
537
- SELECT m.id, m.content, m.category, m.subcategory, m.project, m.tags,
538
- m.importance, m.source, m.created_at, m.updated_at, m.accessed_at,
539
- m.access_count, m.related_files, m.related_memory_ids,
540
- m.file_checksums, m.trashed_at, m.source_session_chunks,
541
- rank
542
- FROM memories_fts fts
543
- JOIN memories m ON m.rowid = fts.rowid
544
- WHERE memories_fts MATCH ? AND m.trashed_at IS NULL${where}
545
- ORDER BY rank
546
- LIMIT ?
547
- `).all(ftsQuery, ...params, limit) as Array<Record<string, unknown>>;
548
-
549
- return rows
550
- .filter(row => !excludeIds || !excludeIds.has(row.id as string))
551
- .map(row => {
552
- // FTS5 rank is negative (lower = better match). Normalize to 0-1 range.
553
- const rawRank = Math.abs(row.rank as number);
554
- const normalizedScore = Math.min(1, rawRank / 10);
555
- return {
556
- id: row.id as string,
557
- score: normalizedScore,
558
- payload: rowToPayload(row),
559
- };
560
- });
561
- } catch {
562
- // FTS5 not available or query error — return empty
563
- return [];
564
- }
565
- }
566
-
567
- // --- Session Chunks ---
568
-
569
- export async function searchSessionChunks(
570
- vector: number[],
571
- limit: number,
572
- filter?: Record<string, unknown>,
573
- scoreThreshold = 0.3
574
- ) {
575
- const d = getDb();
576
- const { where, params } = translateFilter(filter);
577
-
578
- const rows = d.prepare(`
579
- SELECT id, vector, content, summary, session_id, project, git_branch, cwd,
580
- chunk_index, start_timestamp, end_timestamp, tools_used, files_modified,
581
- files_read, user_messages, turn_count, related_memory_ids, dedup_memory_id, indexed_at
582
- FROM session_chunks
583
- WHERE 1=1${where}
584
- `).all(...params) as Array<Record<string, unknown>>;
585
-
586
- const scored = rows.map((row) => {
587
- const rowVector = decodeVector(row.vector as Uint8Array);
588
- const score = cosineSimilarity(vector, rowVector);
589
- return { row, score };
590
- }).filter((r) => r.score >= scoreThreshold);
591
-
592
- scored.sort((a, b) => b.score - a.score);
593
-
594
- return scored.slice(0, limit).map((r) => ({
595
- id: r.row.id as string,
596
- score: r.score,
597
- payload: rowToSessionChunkPayload(r.row),
598
- }));
599
- }
600
-
601
- export async function upsertSessionChunks(
602
- points: Array<{ id: string; vector: number[]; payload: SessionChunkPayload }>
603
- ): Promise<void> {
604
- const d = getDb();
605
- const stmt = d.prepare(`
606
- INSERT OR REPLACE INTO session_chunks
607
- (id, vector, content, summary, session_id, project, git_branch, cwd,
608
- chunk_index, start_timestamp, end_timestamp, tools_used, files_modified,
609
- files_read, user_messages, turn_count, related_memory_ids, dedup_memory_id, indexed_at)
610
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
611
- `);
612
-
613
- // Manual transaction since node:sqlite doesn't have db.transaction()
614
- d.exec('BEGIN');
615
- try {
616
- for (const p of points) {
617
- stmt.run(
618
- p.id,
619
- encodeVector(p.vector),
620
- p.payload.content,
621
- p.payload.summary || null,
622
- p.payload.session_id || null,
623
- p.payload.project || null,
624
- p.payload.git_branch ?? null,
625
- p.payload.cwd ?? null,
626
- p.payload.chunk_index ?? 0,
627
- p.payload.start_timestamp || null,
628
- p.payload.end_timestamp || null,
629
- JSON.stringify(p.payload.tools_used || []),
630
- JSON.stringify(p.payload.files_modified || []),
631
- JSON.stringify(p.payload.files_read || []),
632
- JSON.stringify(p.payload.user_messages || []),
633
- p.payload.turn_count ?? 0,
634
- JSON.stringify(p.payload.related_memory_ids || []),
635
- p.payload.dedup_memory_id ?? null,
636
- p.payload.indexed_at || new Date().toISOString(),
637
- );
638
- }
639
- d.exec('COMMIT');
640
- } catch (err) {
641
- d.exec('ROLLBACK');
642
- throw err;
643
- }
644
- }
645
-
646
- export async function scrollSessionChunks(
647
- filter?: Record<string, unknown>,
648
- limit: number = 20,
649
- offset?: string
650
- ) {
651
- const d = getDb();
652
- const { where, params } = translateFilter(filter);
653
- const numericOffset = offset ? parseInt(offset, 10) : 0;
654
-
655
- const rows = d.prepare(`
656
- SELECT id, content, summary, session_id, project, git_branch, cwd,
657
- chunk_index, start_timestamp, end_timestamp, tools_used, files_modified,
658
- files_read, user_messages, turn_count, related_memory_ids, dedup_memory_id, indexed_at
659
- FROM session_chunks
660
- WHERE 1=1${where}
661
- ORDER BY indexed_at DESC
662
- LIMIT ? OFFSET ?
663
- `).all(...params, limit, numericOffset) as Array<Record<string, unknown>>;
664
-
665
- const points = rows.map((row) => ({
666
- id: row.id as string,
667
- payload: rowToSessionChunkPayload(row),
668
- }));
669
-
670
- const nextOffset = rows.length === limit ? String(numericOffset + limit) : null;
671
- return { points, next_page_offset: nextOffset };
672
- }
673
-
674
- export async function countSessionChunks(filter?: Record<string, unknown>): Promise<number> {
675
- const d = getDb();
676
- const { where, params } = translateFilter(filter);
677
- const result = d.prepare(`SELECT COUNT(*) as cnt FROM session_chunks WHERE 1=1${where}`)
678
- .get(...params) as { cnt: number };
679
- return result.cnt;
680
- }
681
-
682
- // --- Categories stored in SQLite ---
683
-
684
- interface StoredCategory {
685
- name: string;
686
- description: string;
687
- created_at: string;
688
- parent?: string;
689
- color?: string;
690
- is_parent?: boolean;
691
- }
692
-
693
- export async function getCategories(): Promise<StoredCategory[] | null> {
694
- try {
695
- const d = getDb();
696
- const rows = d.prepare('SELECT name, description, created_at, parent, color, is_parent FROM categories ORDER BY name')
697
- .all() as Array<Record<string, unknown>>;
698
- if (rows.length === 0) return null;
699
- return rows.map((row) => ({
700
- name: row.name as string,
701
- description: row.description as string,
702
- created_at: row.created_at as string,
703
- parent: row.parent as string | undefined,
704
- color: row.color as string | undefined,
705
- is_parent: row.is_parent === 1 ? true : undefined,
706
- }));
707
- } catch {
708
- return null;
709
- }
710
- }
711
-
712
- export async function saveCategories(categories: StoredCategory[]): Promise<void> {
713
- const d = getDb();
714
- const insertStmt = d.prepare(`
715
- INSERT OR REPLACE INTO categories (name, description, created_at, parent, color, is_parent)
716
- VALUES (?, ?, ?, ?, ?, ?)
717
- `);
718
-
719
- // Manual transaction
720
- d.exec('BEGIN');
721
- try {
722
- // Get existing names to detect deletions
723
- const existing = new Set(
724
- (d.prepare('SELECT name FROM categories').all() as Array<{ name: string }>).map((r) => r.name)
725
- );
726
- const incoming = new Set(categories.map((c) => c.name));
727
-
728
- // Delete removed categories
729
- for (const name of existing) {
730
- if (!incoming.has(name)) {
731
- d.prepare('DELETE FROM categories WHERE name = ?').run(name);
732
- }
733
- }
734
-
735
- // Upsert all incoming
736
- for (const cat of categories) {
737
- insertStmt.run(
738
- cat.name,
739
- cat.description,
740
- cat.created_at,
741
- cat.parent ?? null,
742
- cat.color ?? null,
743
- cat.is_parent ? 1 : 0,
744
- );
745
- }
746
- d.exec('COMMIT');
747
- } catch (err) {
748
- d.exec('ROLLBACK');
749
- throw err;
750
- }
751
- }
752
-
753
- // --- Helpers ---
754
-
755
- function parseJsonOrDefault<T>(value: unknown, defaultValue: T): T {
756
- if (value == null || value === '') return defaultValue;
757
- if (typeof value === 'string') {
758
- try { return JSON.parse(value) as T; } catch { return defaultValue; }
759
- }
760
- return value as T;
761
- }
762
-
763
- function rowToPayload(row: Record<string, unknown>): MemoryPayload {
764
- return {
765
- content: row.content as string,
766
- category: row.category as string,
767
- subcategory: row.subcategory as string | undefined,
768
- project: row.project as string,
769
- tags: parseJsonOrDefault<string[]>(row.tags, []),
770
- importance: (row.importance as number) ?? 5,
771
- source: (row.source as MemoryPayload['source']) || 'self-discovered',
772
- created_at: row.created_at as string,
773
- updated_at: row.updated_at as string,
774
- accessed_at: row.accessed_at as string,
775
- access_count: (row.access_count as number) ?? 0,
776
- related_files: parseJsonOrDefault<string[] | undefined>(row.related_files, undefined),
777
- related_memory_ids: parseJsonOrDefault<string[] | undefined>(row.related_memory_ids, undefined),
778
- file_checksums: parseJsonOrDefault<Record<string, string> | undefined>(row.file_checksums, undefined),
779
- trashed_at: row.trashed_at as string | null | undefined,
780
- source_session_chunks: parseJsonOrDefault(row.source_session_chunks, undefined),
781
- };
782
- }
783
-
784
- function rowToSessionChunkPayload(row: Record<string, unknown>): SessionChunkPayload {
785
- return {
786
- content: row.content as string,
787
- summary: (row.summary as string) || '',
788
- session_id: (row.session_id as string) || '',
789
- project: (row.project as string) || '',
790
- git_branch: row.git_branch as string | null,
791
- cwd: row.cwd as string | null,
792
- chunk_index: (row.chunk_index as number) ?? 0,
793
- start_timestamp: (row.start_timestamp as string) || '',
794
- end_timestamp: (row.end_timestamp as string) || '',
795
- tools_used: parseJsonOrDefault<string[]>(row.tools_used, []),
796
- files_modified: parseJsonOrDefault<string[]>(row.files_modified, []),
797
- files_read: parseJsonOrDefault<string[]>(row.files_read, []),
798
- user_messages: parseJsonOrDefault<string[]>(row.user_messages, []),
799
- turn_count: (row.turn_count as number) ?? 0,
800
- related_memory_ids: parseJsonOrDefault<string[]>(row.related_memory_ids, []),
801
- dedup_memory_id: row.dedup_memory_id as string | null,
802
- indexed_at: (row.indexed_at as string) || '',
803
- };
804
- }
805
-
806
- /**
807
- * Close the database connection. Call on process exit.
808
- */
809
- export function closeDatabase(): void {
810
- if (db) {
811
- db.close();
812
- db = null;
813
- }
814
- }
815
-
816
- /**
817
- * Reopen the database at the current SQLITE_DB_PATH.
818
- * Call after updating process.env.SQLITE_DB_PATH to switch databases.
819
- */
820
- export async function reopenDatabase(): Promise<void> {
821
- closeDatabase();
822
- await ensureDatabase();
823
- }
1
+ /**
2
+ * SQLite storage layer.
3
+ * Uses Node.js built-in node:sqlite (available since Node 22.5.0).
4
+ * Vectors stored as Float32Array BLOBs, cosine similarity computed in JS.
5
+ */
6
+
7
+ import { DatabaseSync, type StatementSync } from 'node:sqlite';
8
+ import { randomUUID } from 'node:crypto';
9
+ import path from 'path';
10
+ import fs from 'fs';
11
+ import os from 'os';
12
+ import { config } from '../config.js';
13
+ import { getAllCategories } from './categories.js';
14
+ import { VectorCache } from './vector-cache.js';
15
+ import { migrateMemory } from './memory-schema.js';
16
+ import { REGION_BY_CURRENCY, resolveClassification } from './fb-regions.js';
17
+ import type { MemoryPayload, MemoryStats, SessionChunkPayload } from '../types.js';
18
+
19
+ type SQLValue = null | number | bigint | string | Uint8Array;
20
+
21
+ // Re-export for backward compat (was used by categories.ts)
22
+ export const CATEGORIES_POINT_ID = '00000000-0000-0000-0000-000000000000';
23
+
24
+ let db: DatabaseSync | null = null;
25
+
26
+ export function getDbPath(): string {
27
+ const envPath = process.env.SQLITE_DB_PATH;
28
+ const defaultPath = path.join(config.dataDir, 'memory.db');
29
+ if (!envPath) return defaultPath;
30
+ // If the env path's parent directory doesn't exist (e.g. Windows path on Mac),
31
+ // fall back to the local default so cross-OS restores work instantly
32
+ if (!fs.existsSync(path.dirname(envPath))) return defaultPath;
33
+ return envPath;
34
+ }
35
+
36
+ /**
37
+ * Refuse to open a store outside the temp directory while tests are running.
38
+ *
39
+ * On 2026-09-18 `npm test` deleted all 24,346 live memories: `tests/setup.ts`
40
+ * redirects SQLITE_DB_PATH at a temp dir, but it only runs if a vitest config
41
+ * lists it under `setupFiles`, and it wasn't wired in. `memory-v2.test.ts`
42
+ * opens with `DELETE FROM memories`, so the damage was silent and total.
43
+ *
44
+ * This lives here rather than in setup.ts deliberately: a guard inside the
45
+ * setup file cannot fire when the setup file is the thing that didn't run.
46
+ * It also catches the quieter path — getDbPath() falls back to the default
47
+ * live store whenever the env path's parent directory is missing, so a typo'd
48
+ * SQLITE_DB_PATH silently aims at production.
49
+ *
50
+ * `node --test` sets NODE_TEST_CONTEXT in every test-file process and the
51
+ * processes it spawns; the Neural Interface suites import this module through
52
+ * dist and would otherwise migrate the live store on their first getDb().
53
+ */
54
+ function assertTestDbIsDisposable(dbPath: string): void {
55
+ if ((!process.env.VITEST && !process.env.NODE_TEST_CONTEXT) || dbPath === ':memory:') return;
56
+ // os.tmpdir() is /var/folders/... on macOS, so an explicit /tmp path is
57
+ // legitimate but would not match it. Resolve symlinks on both sides too:
58
+ // /tmp is a link to /private/tmp, and os.tmpdir() to /private/var/folders/...
59
+ const real = (p: string) => { try { return fs.realpathSync(p); } catch { return p; } };
60
+ const roots = new Set([os.tmpdir(), '/tmp'].flatMap(r => [r, real(r)]));
61
+ const under = (child: string, root: string) => child === root || child.startsWith(root.endsWith(path.sep) ? root : root + path.sep);
62
+ const resolved = path.resolve(dbPath);
63
+ const resolvedReal = real(path.dirname(resolved)) + path.sep + path.basename(resolved);
64
+ for (const root of roots) if (under(resolved, root) || under(resolvedReal, root)) return;
65
+ throw new Error(
66
+ `Refusing to open ${dbPath} under a test runner — a test database must live under ${os.tmpdir()} or /tmp. ` +
67
+ 'Check that vitest.config.ts still lists setupFiles: [\'tests/setup.ts\'], and that ' +
68
+ 'SQLITE_DB_PATH points at a directory that exists (getDbPath falls back to the live ' +
69
+ 'store when the parent is missing).',
70
+ );
71
+ }
72
+
73
+ export function getDb(): DatabaseSync {
74
+ if (!db) {
75
+ const dbPath = getDbPath();
76
+ assertTestDbIsDisposable(dbPath);
77
+ if (dbPath !== ':memory:') fs.mkdirSync(path.dirname(dbPath), { recursive: true });
78
+ db = new DatabaseSync(dbPath);
79
+ db.exec('PRAGMA busy_timeout = 5000');
80
+ db.exec('PRAGMA journal_mode = WAL');
81
+ db.exec('PRAGMA foreign_keys = ON');
82
+ db.exec('PRAGMA recursive_triggers = ON');
83
+ try {
84
+ db.exec(SCHEMA_SQL);
85
+ db.exec(FB_SCHEMA_SQL);
86
+ db.exec(FTS_SQL);
87
+ migrateMemory(db, dbPath);
88
+ } catch (error) { db.close(); db = null; throw error; }
89
+ }
90
+ return db;
91
+ }
92
+
93
+ // --- In-memory vector caches (see vector-cache.ts for coherence model) ---
94
+
95
+ export const memoryVectors = new VectorCache(
96
+ getDb,
97
+ 'SELECT id, vector FROM memories WHERE trashed_at IS NULL',
98
+ 'SELECT vector FROM memories WHERE id = ? AND trashed_at IS NULL',
99
+ 'memories',
100
+ );
101
+
102
+ export const chunkVectors = new VectorCache(
103
+ getDb,
104
+ 'SELECT id, vector FROM session_chunks',
105
+ 'SELECT vector FROM session_chunks WHERE id = ?',
106
+ 'session_chunks',
107
+ );
108
+
109
+ // --- Vector encoding/decoding ---
110
+
111
+ function encodeVector(vector: number[]): Uint8Array {
112
+ const f32 = new Float32Array(vector);
113
+ return new Uint8Array(f32.buffer);
114
+ }
115
+
116
+ // --- Filter translation ---
117
+
118
+ interface FilterCondition {
119
+ key: string;
120
+ match?: { value: string | number; aliases?: string[] };
121
+ range?: { gte?: number; lte?: number; gt?: number; lt?: number };
122
+ is_empty?: { key: string };
123
+ }
124
+
125
+ interface MemoryFilter {
126
+ must?: FilterCondition[];
127
+ must_not?: FilterCondition[];
128
+ should?: FilterCondition[];
129
+ }
130
+
131
+ /**
132
+ * Translates JSON filters to SQL WHERE clauses.
133
+ * Supports: exact match, range, tags (JSON array contains), is_empty.
134
+ */
135
+ function translateFilter(filter?: Record<string, unknown>): { where: string; params: SQLValue[] } {
136
+ if (!filter) return { where: '', params: [] };
137
+
138
+ const f = filter as MemoryFilter;
139
+ const clauses: string[] = [];
140
+ const params: SQLValue[] = [];
141
+
142
+ for (const cond of f.must || []) {
143
+ if (cond.match) {
144
+ if (cond.key === 'tags') {
145
+ clauses.push(`EXISTS (SELECT 1 FROM json_each(tags) WHERE json_each.value = ?)`);
146
+ params.push(cond.match.value);
147
+ } else {
148
+ clauses.push(`${sanitizeColumn(cond.key)} = ?`);
149
+ params.push(cond.match.value);
150
+ }
151
+ } else if (cond.range) {
152
+ const col = sanitizeColumn(cond.key);
153
+ if (cond.range.gte !== undefined) { clauses.push(`${col} >= ?`); params.push(cond.range.gte); }
154
+ if (cond.range.lte !== undefined) { clauses.push(`${col} <= ?`); params.push(cond.range.lte); }
155
+ if (cond.range.gt !== undefined) { clauses.push(`${col} > ?`); params.push(cond.range.gt); }
156
+ if (cond.range.lt !== undefined) { clauses.push(`${col} < ?`); params.push(cond.range.lt); }
157
+ } else if (cond.is_empty) {
158
+ clauses.push(`(${sanitizeColumn(cond.is_empty.key)} IS NULL OR ${sanitizeColumn(cond.is_empty.key)} = '')`);
159
+ }
160
+ }
161
+
162
+ const alternatives = (f.should || []).filter(c => c.key === 'tags' && c.match).map(c => {
163
+ params.push(c.match!.value);
164
+ return 'EXISTS (SELECT 1 FROM json_each(tags) WHERE json_each.value = ?)';
165
+ });
166
+ if (alternatives.length) clauses.push('(' + alternatives.join(' OR ') + ')');
167
+
168
+ for (const cond of f.must_not || []) {
169
+ if (cond.match) {
170
+ clauses.push(`(${sanitizeColumn(cond.key)} IS NULL OR ${sanitizeColumn(cond.key)} != ?)`);
171
+ params.push(cond.match.value);
172
+ }
173
+ }
174
+
175
+ return {
176
+ where: clauses.length > 0 ? ' AND ' + clauses.join(' AND ') : '',
177
+ params,
178
+ };
179
+ }
180
+
181
+ /** Allowlist of valid column names to prevent SQL injection */
182
+ const VALID_COLUMNS = new Set([
183
+ 'id', 'content', 'category', 'subcategory', 'project', 'tags', 'importance',
184
+ 'source', 'created_at', 'updated_at', 'accessed_at', 'access_count',
185
+ 'related_files', 'related_memory_ids', 'file_checksums', 'trashed_at',
186
+ 'source_session_chunks', '_type',
187
+ // session_chunks columns
188
+ 'session_id', 'git_branch', 'cwd', 'chunk_index', 'start_timestamp',
189
+ 'end_timestamp', 'tools_used', 'files_modified', 'files_read',
190
+ 'user_messages', 'turn_count', 'dedup_memory_id', 'indexed_at', 'summary',
191
+ ]);
192
+
193
+ function sanitizeColumn(name: string): string {
194
+ if (!VALID_COLUMNS.has(name)) {
195
+ throw new Error(`Invalid column name: ${name}`);
196
+ }
197
+ return name;
198
+ }
199
+
200
+ // --- Schema initialization ---
201
+
202
+ const SCHEMA_SQL = `
203
+ CREATE TABLE IF NOT EXISTS memories (
204
+ id TEXT PRIMARY KEY,
205
+ vector BLOB NOT NULL,
206
+ content TEXT NOT NULL,
207
+ category TEXT NOT NULL,
208
+ subcategory TEXT,
209
+ project TEXT NOT NULL,
210
+ tags TEXT NOT NULL DEFAULT '[]',
211
+ importance INTEGER NOT NULL DEFAULT 5,
212
+ source TEXT NOT NULL DEFAULT 'self-discovered',
213
+ created_at TEXT NOT NULL,
214
+ updated_at TEXT NOT NULL,
215
+ accessed_at TEXT NOT NULL,
216
+ access_count INTEGER NOT NULL DEFAULT 0,
217
+ related_files TEXT,
218
+ related_memory_ids TEXT,
219
+ file_checksums TEXT,
220
+ trashed_at TEXT,
221
+ source_session_chunks TEXT
222
+ );
223
+
224
+ CREATE TABLE IF NOT EXISTS session_chunks (
225
+ id TEXT PRIMARY KEY,
226
+ vector BLOB NOT NULL,
227
+ content TEXT NOT NULL,
228
+ summary TEXT,
229
+ session_id TEXT,
230
+ project TEXT,
231
+ git_branch TEXT,
232
+ cwd TEXT,
233
+ chunk_index INTEGER DEFAULT 0,
234
+ start_timestamp TEXT,
235
+ end_timestamp TEXT,
236
+ tools_used TEXT DEFAULT '[]',
237
+ files_modified TEXT DEFAULT '[]',
238
+ files_read TEXT DEFAULT '[]',
239
+ user_messages TEXT DEFAULT '[]',
240
+ turn_count INTEGER DEFAULT 0,
241
+ related_memory_ids TEXT DEFAULT '[]',
242
+ dedup_memory_id TEXT,
243
+ indexed_at TEXT
244
+ );
245
+
246
+ CREATE TABLE IF NOT EXISTS categories (
247
+ name TEXT PRIMARY KEY,
248
+ description TEXT NOT NULL,
249
+ created_at TEXT NOT NULL,
250
+ parent TEXT,
251
+ color TEXT,
252
+ is_parent INTEGER DEFAULT 0
253
+ );
254
+
255
+ CREATE INDEX IF NOT EXISTS idx_mem_category ON memories(category);
256
+ CREATE INDEX IF NOT EXISTS idx_mem_project ON memories(project);
257
+ CREATE INDEX IF NOT EXISTS idx_mem_importance ON memories(importance);
258
+ CREATE INDEX IF NOT EXISTS idx_mem_trashed ON memories(trashed_at);
259
+ CREATE INDEX IF NOT EXISTS idx_mem_created ON memories(created_at);
260
+ CREATE INDEX IF NOT EXISTS idx_mem_source ON memories(source);
261
+ CREATE INDEX IF NOT EXISTS idx_sc_session ON session_chunks(session_id);
262
+ CREATE INDEX IF NOT EXISTS idx_sc_project ON session_chunks(project);
263
+ CREATE INDEX IF NOT EXISTS idx_sc_branch ON session_chunks(git_branch);
264
+
265
+ CREATE TABLE IF NOT EXISTS kv_config (
266
+ key TEXT PRIMARY KEY,
267
+ value TEXT NOT NULL
268
+ );
269
+ `;
270
+
271
+ // FTS5 created separately since CREATE VIRTUAL TABLE IF NOT EXISTS
272
+ // can fail silently on some SQLite versions
273
+ const FTS_SQL = `
274
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
275
+ content, category, project, tags,
276
+ content=memories, content_rowid=rowid,
277
+ tokenize='porter unicode61'
278
+ );
279
+ `;
280
+
281
+ // Facebook group directory — structured single source of truth for the group
282
+ // collection. One row per group (fb_groups), one append-only row per posting event
283
+ // (fb_post_log). Replaces a single-text seed-queue memory, which one whole-body
284
+ // reflect() can overwrite; atomic per-row writes make that data-loss class impossible.
285
+ const FB_SCHEMA_SQL = `
286
+ CREATE TABLE IF NOT EXISTS fb_groups (
287
+ url TEXT PRIMARY KEY,
288
+ name TEXT NOT NULL DEFAULT '',
289
+ region TEXT,
290
+ country TEXT,
291
+ lang TEXT,
292
+ currency TEXT,
293
+ joined INTEGER NOT NULL DEFAULT 1,
294
+ allows_promo TEXT NOT NULL DEFAULT 'unknown',
295
+ member_count INTEGER,
296
+ pending_since TEXT,
297
+ cooldown_until TEXT,
298
+ last_status TEXT,
299
+ last_error TEXT,
300
+ notes TEXT,
301
+ source TEXT NOT NULL DEFAULT 'import',
302
+ added_at TEXT NOT NULL,
303
+ updated_at TEXT NOT NULL
304
+ );
305
+
306
+ CREATE TABLE IF NOT EXISTS fb_post_log (
307
+ id TEXT PRIMARY KEY,
308
+ group_url TEXT NOT NULL,
309
+ offer_slug TEXT NOT NULL,
310
+ currency TEXT,
311
+ status TEXT NOT NULL,
312
+ post_url TEXT,
313
+ session_id TEXT,
314
+ posted_at TEXT NOT NULL,
315
+ note TEXT
316
+ );
317
+
318
+ CREATE INDEX IF NOT EXISTS idx_fbg_region ON fb_groups(region);
319
+ CREATE INDEX IF NOT EXISTS idx_fbg_joined ON fb_groups(joined);
320
+ CREATE INDEX IF NOT EXISTS idx_fbg_promo ON fb_groups(allows_promo);
321
+ CREATE INDEX IF NOT EXISTS idx_fbpl_url ON fb_post_log(group_url);
322
+ CREATE INDEX IF NOT EXISTS idx_fbpl_offer ON fb_post_log(offer_slug);
323
+ CREATE INDEX IF NOT EXISTS idx_fbpl_posted ON fb_post_log(posted_at);
324
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_fbpl_url_offer_day
325
+ ON fb_post_log(group_url, offer_slug, substr(posted_at,1,10));
326
+ `;
327
+
328
+ // --- Public API: Collection initialization ---
329
+
330
+ export async function ensureCollection(): Promise<void> {
331
+ getDb();
332
+ }
333
+
334
+ export async function ensureSessionCollection(): Promise<void> {
335
+ // No-op: session_chunks table is created in ensureCollection()
336
+ }
337
+
338
+ /** Combined init function for cleaner startup */
339
+ export async function ensureDatabase(): Promise<void> {
340
+ await ensureCollection();
341
+ }
342
+
343
+ // --- Public API: Memory operations ---
344
+
345
+ export function upsertMemory(
346
+ id: string,
347
+ vector: number[],
348
+ payload: MemoryPayload
349
+ ): void {
350
+ const d = getDb();
351
+ const stmt = d.prepare(`
352
+ INSERT INTO memories
353
+ (id, vector, content, category, subcategory, project, tags, importance, source,
354
+ created_at, updated_at, accessed_at, access_count, related_files,
355
+ related_memory_ids, file_checksums, trashed_at, source_session_chunks)
356
+ VALUES
357
+ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
358
+ ON CONFLICT(id) DO UPDATE SET
359
+ vector=excluded.vector,content=excluded.content,category=excluded.category,subcategory=excluded.subcategory,
360
+ project=excluded.project,tags=excluded.tags,importance=excluded.importance,source=excluded.source,
361
+ updated_at=excluded.updated_at,related_files=excluded.related_files,related_memory_ids=excluded.related_memory_ids,
362
+ file_checksums=excluded.file_checksums,trashed_at=excluded.trashed_at,source_session_chunks=excluded.source_session_chunks
363
+ `);
364
+ stmt.run(
365
+ id,
366
+ encodeVector(vector),
367
+ payload.content,
368
+ payload.category,
369
+ payload.subcategory ?? null,
370
+ payload.project,
371
+ JSON.stringify(payload.tags || []),
372
+ payload.importance ?? 5,
373
+ payload.source || 'self-discovered',
374
+ payload.created_at,
375
+ payload.updated_at,
376
+ payload.accessed_at,
377
+ payload.access_count ?? 0,
378
+ payload.related_files ? JSON.stringify(payload.related_files) : null,
379
+ payload.related_memory_ids ? JSON.stringify(payload.related_memory_ids) : null,
380
+ payload.file_checksums ? JSON.stringify(payload.file_checksums) : null,
381
+ payload.trashed_at ?? null,
382
+ payload.source_session_chunks ? JSON.stringify(payload.source_session_chunks) : null,
383
+ );
384
+
385
+ if (payload.trashed_at) memoryVectors.remove(id);
386
+ else memoryVectors.set(id, vector);
387
+
388
+
389
+ }
390
+
391
+ export function searchMemories(
392
+ vector: number[],
393
+ limit: number,
394
+ filter?: Record<string, unknown>,
395
+ scoreThreshold?: number
396
+ ) {
397
+ const d = getDb();
398
+ const threshold = scoreThreshold ?? 0.3;
399
+
400
+ // When a filter is present, resolve the matching id set via the indexed
401
+ // SQL WHERE first, then score only those rows in the vector cache.
402
+ let allowedIds: Set<string> | undefined;
403
+ const { where, params } = translateFilter(filter);
404
+ if (where) {
405
+ const idRows = d.prepare(`
406
+ SELECT id FROM memories WHERE trashed_at IS NULL${where}
407
+ `).all(...params) as Array<{ id: string }>;
408
+ allowedIds = new Set(idRows.map((r) => r.id));
409
+ if (allowedIds.size === 0) return [];
410
+ }
411
+
412
+ const top = memoryVectors.topK(vector, limit, threshold, allowedIds);
413
+ if (top.length === 0) return [];
414
+
415
+ // Hydrate payloads for just the top K rows
416
+ const placeholders = top.map(() => '?').join(', ');
417
+ const rows = d.prepare(`
418
+ SELECT id, content, category, subcategory, project, tags, importance, source,
419
+ created_at, updated_at, accessed_at, access_count, related_files,
420
+ related_memory_ids, file_checksums, trashed_at, source_session_chunks
421
+ FROM memories
422
+ WHERE id IN (${placeholders})
423
+ `).all(...top.map((t) => t.id)) as Array<Record<string, unknown>>;
424
+
425
+ const rowById = new Map(rows.map((r) => [r.id as string, r]));
426
+ return top
427
+ .filter((t) => rowById.has(t.id))
428
+ .map((t) => ({
429
+ id: t.id,
430
+ score: t.score,
431
+ payload: rowToPayload(rowById.get(t.id)!),
432
+ }));
433
+ }
434
+
435
+ export async function getMemory(id: string) {
436
+ const d = getDb();
437
+ const row = d.prepare(`
438
+ SELECT id, content, category, subcategory, project, tags, importance, source,
439
+ created_at, updated_at, accessed_at, access_count, related_files,
440
+ related_memory_ids, file_checksums, trashed_at, source_session_chunks
441
+ FROM memories WHERE id = ?
442
+ `).get(id) as Record<string, unknown> | undefined;
443
+
444
+ if (!row) return null;
445
+ return { id: row.id as string, payload: rowToPayload(row) };
446
+ }
447
+
448
+ export function updatePayload(
449
+ id: string,
450
+ payload: Partial<MemoryPayload>
451
+ ): void {
452
+ const d = getDb();
453
+ const sets: string[] = [];
454
+ const params: SQLValue[] = [];
455
+
456
+ for (const [key, value] of Object.entries(payload)) {
457
+ if (key === 'id' || key === 'vector') continue;
458
+ const col = sanitizeColumn(key);
459
+ if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
460
+ sets.push(`${col} = ?`);
461
+ params.push(JSON.stringify(value));
462
+ } else {
463
+ sets.push(`${col} = ?`);
464
+ params.push((value ?? null) as SQLValue);
465
+ }
466
+ }
467
+
468
+ if (sets.length === 0) return;
469
+ params.push(id);
470
+ d.prepare(`UPDATE memories SET ${sets.join(', ')} WHERE id = ?`).run(...params);
471
+
472
+ // Keep the vector cache in sync with trash-state transitions
473
+ if ('trashed_at' in payload) {
474
+ if (payload.trashed_at) memoryVectors.remove(id);
475
+ else memoryVectors.setFromDb(id);
476
+ }
477
+
478
+
479
+ }
480
+
481
+ export async function updateVector(
482
+ id: string,
483
+ vector: number[],
484
+ payload: MemoryPayload
485
+ ): Promise<void> {
486
+ // Full upsert — replaces both vector and payload
487
+ await upsertMemory(id, vector, payload);
488
+ }
489
+
490
+ export async function updatePayloadByFilter(
491
+ filter: Record<string, unknown>,
492
+ payload: Partial<MemoryPayload>
493
+ ): Promise<void> {
494
+ const d = getDb();
495
+ const { where, params: filterParams } = translateFilter(filter);
496
+
497
+ const sets: string[] = [];
498
+ const setParams: SQLValue[] = [];
499
+
500
+ for (const [key, value] of Object.entries(payload)) {
501
+ if (key === 'id' || key === 'vector') continue;
502
+ const col = sanitizeColumn(key);
503
+ if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
504
+ sets.push(`${col} = ?`);
505
+ setParams.push(JSON.stringify(value));
506
+ } else {
507
+ sets.push(`${col} = ?`);
508
+ setParams.push((value ?? null) as SQLValue);
509
+ }
510
+ }
511
+
512
+ if (sets.length === 0) return;
513
+ d.prepare(`UPDATE memories SET ${sets.join(', ')} WHERE trashed_at IS NULL${where}`)
514
+ .run(...setParams, ...filterParams);
515
+
516
+ // Bulk path — affected ids unknown here; reload lazily on next search
517
+ if ('trashed_at' in payload) memoryVectors.invalidate();
518
+ }
519
+
520
+ export function deleteMemory(id: string): void {
521
+ const d = getDb();
522
+ d.exec('SAVEPOINT memory_purge');
523
+ try {
524
+ d.prepare('DELETE FROM memories WHERE id = ?').run(id);
525
+ memoryVectors.remove(id);
526
+ d.prepare('DELETE FROM memory_revisions WHERE memory_id = ?').run(id);
527
+ d.prepare('DELETE FROM memory_metadata WHERE memory_id = ?').run(id);
528
+ d.prepare('DELETE FROM memory_relations WHERE from_id = ? OR to_id = ?').run(id, id);
529
+ d.prepare('DELETE FROM memory_feedback WHERE memory_id = ?').run(id);
530
+ d.exec('RELEASE memory_purge');
531
+ } catch (error) {
532
+ d.exec('ROLLBACK TO memory_purge'); d.exec('RELEASE memory_purge');
533
+ memoryVectors.invalidate(); throw error;
534
+ }
535
+ }
536
+
537
+ export async function softDeleteMemory(id: string): Promise<void> {
538
+ await updatePayload(id, { trashed_at: new Date().toISOString() } as Partial<MemoryPayload>);
539
+ }
540
+
541
+ /**
542
+ * Batch access tracking for recall results: one UPDATE instead of N.
543
+ * Leaves vectors untouched, so no cache maintenance is needed.
544
+ */
545
+ export async function touchMemories(ids: string[], accessedAt: string): Promise<void> {
546
+ if (ids.length === 0) return;
547
+ const d = getDb();
548
+ const placeholders = ids.map(() => '?').join(', ');
549
+ d.prepare(`
550
+ UPDATE memories SET accessed_at = ?, access_count = access_count + 1
551
+ WHERE id IN (${placeholders})
552
+ `).run(accessedAt, ...ids);
553
+ }
554
+
555
+ export async function restoreMemory(id: string): Promise<void> {
556
+ const d = getDb();
557
+ d.prepare('UPDATE memories SET trashed_at = NULL WHERE id = ?').run(id);
558
+ memoryVectors.setFromDb(id);
559
+ }
560
+
561
+ export async function scrollMemories(
562
+ filter?: Record<string, unknown>,
563
+ limit: number = 20,
564
+ offset?: string
565
+ ) {
566
+ const d = getDb();
567
+ const { where, params } = translateFilter(filter);
568
+ const numericOffset = offset ? parseInt(offset, 10) : 0;
569
+
570
+ const rows = d.prepare(`
571
+ SELECT id, content, category, subcategory, project, tags, importance, source,
572
+ created_at, updated_at, accessed_at, access_count, related_files,
573
+ related_memory_ids, file_checksums, trashed_at, source_session_chunks
574
+ FROM memories
575
+ WHERE trashed_at IS NULL${where}
576
+ ORDER BY created_at DESC
577
+ LIMIT ? OFFSET ?
578
+ `).all(...params, limit, numericOffset) as Array<Record<string, unknown>>;
579
+
580
+ const points = rows.map((row) => ({
581
+ id: row.id as string,
582
+ payload: rowToPayload(row),
583
+ }));
584
+
585
+ // Determine next offset
586
+ const nextOffset = rows.length === limit ? String(numericOffset + limit) : null;
587
+
588
+ return { points, next_page_offset: nextOffset };
589
+ }
590
+
591
+ export async function countMemories(filter?: Record<string, unknown>): Promise<number> {
592
+ const d = getDb();
593
+ const { where, params } = translateFilter(filter);
594
+ const result = d.prepare(`
595
+ SELECT COUNT(*) as cnt FROM memories WHERE trashed_at IS NULL${where}
596
+ `).get(...params) as { cnt: number };
597
+ return result.cnt;
598
+ }
599
+
600
+ /**
601
+ * YouTube trailer dedup ledger — return the set of canonical keys
602
+ * (igdb:/appid:/ytid:/name:) for every trailer already posted. youtube_source
603
+ * uses this to drop already-posted candidates before the model sees them.
604
+ *
605
+ * Reads structured tags first (new `youtube-videos` rows written by
606
+ * youtube_upload) and falls back to scanning free-text content for igdbId/appid
607
+ * (legacy `youtube-trailers` rows from before the structured ledger existed).
608
+ * Exact-match by design — never re-post a trailer regardless of how old the
609
+ * ledger gets. Synchronous: this is a tiny indexed read, not a vector search.
610
+ */
611
+ const TRAILER_LEDGER_CATEGORIES = ['youtube-videos', 'youtube-trailers'];
612
+ const TRAILER_KEY_RE = /^(igdb|appid|ytid|name):.+/;
613
+ export function getPostedTrailerKeys(): Set<string> {
614
+ const keys = new Set<string>();
615
+ let rows: Array<{ tags: string | null; content: string | null }> = [];
616
+ try {
617
+ const d = getDb();
618
+ const placeholders = TRAILER_LEDGER_CATEGORIES.map(() => '?').join(', ');
619
+ rows = d.prepare(
620
+ `SELECT tags, content FROM memories WHERE category IN (${placeholders}) AND trashed_at IS NULL`,
621
+ ).all(...TRAILER_LEDGER_CATEGORIES) as Array<{ tags: string | null; content: string | null }>;
622
+ } catch {
623
+ return keys; // dedup is best-effort: a read failure must not break sourcing
624
+ }
625
+ for (const r of rows) {
626
+ // 1) structured dedup tags (preserve case — ytid is case-sensitive).
627
+ try {
628
+ for (const t of JSON.parse(r.tags || '[]')) {
629
+ const s = String(t).trim();
630
+ if (TRAILER_KEY_RE.test(s)) keys.add(s);
631
+ }
632
+ } catch { /* tags not JSON — fall through to content scan */ }
633
+ // 2) content fallback for legacy free-text rows (igdbId / appid only).
634
+ const content = r.content || '';
635
+ const ig = content.match(/igdb\s*id[:=\s#]*([0-9]+)/i);
636
+ if (ig) keys.add(`igdb:${ig[1]}`);
637
+ const ap = content.match(/\bapp\s*id[:=\s#]*([0-9]+)/i);
638
+ if (ap) keys.add(`appid:${ap[1]}`);
639
+ }
640
+ return keys;
641
+ }
642
+
643
+ export async function getMemoryStats(): Promise<MemoryStats> {
644
+ const d = getDb();
645
+
646
+ const totalRow = d.prepare('SELECT COUNT(*) as cnt FROM memories WHERE trashed_at IS NULL').get() as { cnt: number };
647
+ const total = totalRow.cnt;
648
+
649
+ // Per-category counts using SQL aggregate
650
+ const catRows = d.prepare(`
651
+ SELECT category, COUNT(*) as cnt FROM memories WHERE trashed_at IS NULL GROUP BY category
652
+ `).all() as Array<{ category: string; cnt: number }>;
653
+ const by_category: Record<string, number> = {};
654
+ // Initialize with all known categories at 0
655
+ for (const cat of getAllCategories()) {
656
+ by_category[cat] = 0;
657
+ }
658
+ for (const row of catRows) {
659
+ by_category[row.category] = row.cnt;
660
+ }
661
+
662
+ // Per-project counts
663
+ const projRows = d.prepare(`
664
+ SELECT project, COUNT(*) as cnt FROM memories WHERE trashed_at IS NULL GROUP BY project
665
+ `).all() as Array<{ project: string; cnt: number }>;
666
+ const by_project: Record<string, number> = {};
667
+ for (const row of projRows) {
668
+ by_project[row.project || 'global'] = row.cnt;
669
+ }
670
+
671
+ // Oldest/newest
672
+ const dateRow = d.prepare(`
673
+ SELECT MIN(created_at) as oldest, MAX(created_at) as newest FROM memories WHERE trashed_at IS NULL
674
+ `).get() as { oldest: string | null; newest: string | null };
675
+
676
+ return {
677
+ total,
678
+ by_category,
679
+ by_project,
680
+ oldest: dateRow.oldest ?? undefined,
681
+ newest: dateRow.newest ?? undefined,
682
+ };
683
+ }
684
+
685
+ // --- FTS5 full-text search ---
686
+
687
+ /**
688
+ * Search memories using FTS5 full-text search (keyword fallback).
689
+ * Returns results scored by BM25 relevance. Used when vector similarity
690
+ * scores are low (exact identifiers, error codes, proper nouns).
691
+ */
692
+ export async function searchMemoriesFTS(
693
+ query: string,
694
+ limit: number,
695
+ filter?: Record<string, unknown>,
696
+ excludeIds?: Set<string>
697
+ ): Promise<Array<{ id: string; score: number; payload: MemoryPayload }>> {
698
+ const d = getDb();
699
+
700
+ // Sanitize query for FTS5: escape double quotes, wrap terms
701
+ const ftsQuery = query
702
+ .replace(/"/g, '""')
703
+ .split(/\s+/)
704
+ .filter(t => t.length > 1)
705
+ .map(t => `"${t}"`)
706
+ .join(' OR ');
707
+
708
+ if (!ftsQuery) return [];
709
+
710
+ const { where, params } = translateFilter(filter);
711
+
712
+ try {
713
+ const rows = d.prepare(`
714
+ SELECT m.id, m.content, m.category, m.subcategory, m.project, m.tags,
715
+ m.importance, m.source, m.created_at, m.updated_at, m.accessed_at,
716
+ m.access_count, m.related_files, m.related_memory_ids,
717
+ m.file_checksums, m.trashed_at, m.source_session_chunks,
718
+ rank
719
+ FROM memories_fts fts
720
+ JOIN memories m ON m.rowid = fts.rowid
721
+ WHERE memories_fts MATCH ? AND m.id IN (SELECT id FROM memories WHERE trashed_at IS NULL${where})
722
+ ORDER BY rank
723
+ LIMIT ?
724
+ `).all(ftsQuery, ...params, limit) as Array<Record<string, unknown>>;
725
+
726
+ return rows
727
+ .filter(row => !excludeIds || !excludeIds.has(row.id as string))
728
+ .map(row => {
729
+ // FTS5 rank is negative (lower = better match). Normalize to 0-1 range.
730
+ const rawRank = Math.abs(row.rank as number);
731
+ const normalizedScore = Math.min(1, rawRank / 10);
732
+ return {
733
+ id: row.id as string,
734
+ score: normalizedScore,
735
+ payload: rowToPayload(row),
736
+ };
737
+ });
738
+ } catch {
739
+ // FTS5 not available or query error — return empty
740
+ return [];
741
+ }
742
+ }
743
+
744
+ // --- Session Chunks ---
745
+
746
+ export function searchSessionChunks(
747
+ vector: number[],
748
+ limit: number,
749
+ filter?: Record<string, unknown>,
750
+ scoreThreshold = 0.3
751
+ ) {
752
+ const d = getDb();
753
+
754
+ let allowedIds: Set<string> | undefined;
755
+ const { where, params } = translateFilter(filter);
756
+ if (where) {
757
+ const idRows = d.prepare(`
758
+ SELECT id FROM session_chunks WHERE 1=1${where}
759
+ `).all(...params) as Array<{ id: string }>;
760
+ allowedIds = new Set(idRows.map((r) => r.id));
761
+ if (allowedIds.size === 0) return [];
762
+ }
763
+
764
+ const top = chunkVectors.topK(vector, limit, scoreThreshold, allowedIds);
765
+ if (top.length === 0) return [];
766
+
767
+ const placeholders = top.map(() => '?').join(', ');
768
+ const rows = d.prepare(`
769
+ SELECT id, content, summary, session_id, project, git_branch, cwd,
770
+ chunk_index, start_timestamp, end_timestamp, tools_used, files_modified,
771
+ files_read, user_messages, turn_count, related_memory_ids, dedup_memory_id, indexed_at
772
+ FROM session_chunks
773
+ WHERE id IN (${placeholders})
774
+ `).all(...top.map((t) => t.id)) as Array<Record<string, unknown>>;
775
+
776
+ const rowById = new Map(rows.map((r) => [r.id as string, r]));
777
+ return top
778
+ .filter((t) => rowById.has(t.id))
779
+ .map((t) => ({
780
+ id: t.id,
781
+ score: t.score,
782
+ payload: rowToSessionChunkPayload(rowById.get(t.id)!),
783
+ }));
784
+ }
785
+
786
+ export async function upsertSessionChunks(
787
+ points: Array<{ id: string; vector: number[]; payload: SessionChunkPayload }>
788
+ ): Promise<void> {
789
+ const d = getDb();
790
+ const stmt = d.prepare(`
791
+ INSERT OR REPLACE INTO session_chunks
792
+ (id, vector, content, summary, session_id, project, git_branch, cwd,
793
+ chunk_index, start_timestamp, end_timestamp, tools_used, files_modified,
794
+ files_read, user_messages, turn_count, related_memory_ids, dedup_memory_id, indexed_at)
795
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
796
+ `);
797
+
798
+ // Manual transaction since node:sqlite doesn't have db.transaction()
799
+ d.exec('BEGIN');
800
+ try {
801
+ for (const p of points) {
802
+ stmt.run(
803
+ p.id,
804
+ encodeVector(p.vector),
805
+ p.payload.content,
806
+ p.payload.summary || null,
807
+ p.payload.session_id || null,
808
+ p.payload.project || null,
809
+ p.payload.git_branch ?? null,
810
+ p.payload.cwd ?? null,
811
+ p.payload.chunk_index ?? 0,
812
+ p.payload.start_timestamp || null,
813
+ p.payload.end_timestamp || null,
814
+ JSON.stringify(p.payload.tools_used || []),
815
+ JSON.stringify(p.payload.files_modified || []),
816
+ JSON.stringify(p.payload.files_read || []),
817
+ JSON.stringify(p.payload.user_messages || []),
818
+ p.payload.turn_count ?? 0,
819
+ JSON.stringify(p.payload.related_memory_ids || []),
820
+ p.payload.dedup_memory_id ?? null,
821
+ p.payload.indexed_at || new Date().toISOString(),
822
+ );
823
+ }
824
+ d.exec('COMMIT');
825
+ } catch (err) {
826
+ d.exec('ROLLBACK');
827
+ throw err;
828
+ }
829
+
830
+ for (const p of points) chunkVectors.set(p.id, p.vector);
831
+ }
832
+
833
+ export async function scrollSessionChunks(
834
+ filter?: Record<string, unknown>,
835
+ limit: number = 20,
836
+ offset?: string
837
+ ) {
838
+ const d = getDb();
839
+ const { where, params } = translateFilter(filter);
840
+ const numericOffset = offset ? parseInt(offset, 10) : 0;
841
+
842
+ const rows = d.prepare(`
843
+ SELECT id, content, summary, session_id, project, git_branch, cwd,
844
+ chunk_index, start_timestamp, end_timestamp, tools_used, files_modified,
845
+ files_read, user_messages, turn_count, related_memory_ids, dedup_memory_id, indexed_at
846
+ FROM session_chunks
847
+ WHERE 1=1${where}
848
+ ORDER BY indexed_at DESC
849
+ LIMIT ? OFFSET ?
850
+ `).all(...params, limit, numericOffset) as Array<Record<string, unknown>>;
851
+
852
+ const points = rows.map((row) => ({
853
+ id: row.id as string,
854
+ payload: rowToSessionChunkPayload(row),
855
+ }));
856
+
857
+ const nextOffset = rows.length === limit ? String(numericOffset + limit) : null;
858
+ return { points, next_page_offset: nextOffset };
859
+ }
860
+
861
+ export async function countSessionChunks(filter?: Record<string, unknown>): Promise<number> {
862
+ const d = getDb();
863
+ const { where, params } = translateFilter(filter);
864
+ const result = d.prepare(`SELECT COUNT(*) as cnt FROM session_chunks WHERE 1=1${where}`)
865
+ .get(...params) as { cnt: number };
866
+ return result.cnt;
867
+ }
868
+
869
+ // --- Categories stored in SQLite ---
870
+
871
+ interface StoredCategory {
872
+ name: string;
873
+ description: string;
874
+ created_at: string;
875
+ parent?: string;
876
+ color?: string;
877
+ is_parent?: boolean;
878
+ }
879
+
880
+ export async function getCategories(): Promise<StoredCategory[] | null> {
881
+ try {
882
+ const d = getDb();
883
+ const rows = d.prepare('SELECT name, description, created_at, parent, color, is_parent FROM categories ORDER BY name')
884
+ .all() as Array<Record<string, unknown>>;
885
+ if (rows.length === 0) return null;
886
+ return rows.map((row) => ({
887
+ name: row.name as string,
888
+ description: row.description as string,
889
+ created_at: row.created_at as string,
890
+ parent: row.parent as string | undefined,
891
+ color: row.color as string | undefined,
892
+ is_parent: row.is_parent === 1 ? true : undefined,
893
+ }));
894
+ } catch {
895
+ return null;
896
+ }
897
+ }
898
+
899
+ export async function saveCategories(categories: StoredCategory[]): Promise<void> {
900
+ const d = getDb();
901
+ const insertStmt = d.prepare(`
902
+ INSERT OR REPLACE INTO categories (name, description, created_at, parent, color, is_parent)
903
+ VALUES (?, ?, ?, ?, ?, ?)
904
+ `);
905
+
906
+ // Manual transaction
907
+ d.exec('BEGIN');
908
+ try {
909
+ // Get existing names to detect deletions
910
+ const existing = new Set(
911
+ (d.prepare('SELECT name FROM categories').all() as Array<{ name: string }>).map((r) => r.name)
912
+ );
913
+ const incoming = new Set(categories.map((c) => c.name));
914
+
915
+ // Delete removed categories
916
+ for (const name of existing) {
917
+ if (!incoming.has(name)) {
918
+ d.prepare('DELETE FROM categories WHERE name = ?').run(name);
919
+ }
920
+ }
921
+
922
+ // Upsert all incoming
923
+ for (const cat of categories) {
924
+ insertStmt.run(
925
+ cat.name,
926
+ cat.description,
927
+ cat.created_at,
928
+ cat.parent ?? null,
929
+ cat.color ?? null,
930
+ cat.is_parent ? 1 : 0,
931
+ );
932
+ }
933
+ d.exec('COMMIT');
934
+ } catch (err) {
935
+ d.exec('ROLLBACK');
936
+ throw err;
937
+ }
938
+ }
939
+
940
+ // --- Facebook group directory (fb_groups + fb_post_log) ---
941
+
942
+ const FB_COOLDOWN_HOURS_DEFAULT = 48;
943
+
944
+ export interface FbGroupUpsert {
945
+ url: string;
946
+ name?: string;
947
+ region?: string | null;
948
+ country?: string | null;
949
+ lang?: string | null;
950
+ currency?: string | null;
951
+ joined?: number;
952
+ allows_promo?: 'true' | 'false' | 'unknown';
953
+ member_count?: number | null;
954
+ notes?: string | null;
955
+ source?: string;
956
+ }
957
+
958
+ export interface FbPostLogEntry {
959
+ id?: string;
960
+ group_url: string;
961
+ offer_slug: string;
962
+ currency?: string | null;
963
+ status: string;
964
+ post_url?: string | null;
965
+ session_id?: string | null;
966
+ posted_at?: string;
967
+ note?: string | null;
968
+ }
969
+
970
+ export interface FbWorklistOpts {
971
+ region?: string | null;
972
+ currency?: string | null;
973
+ lang?: string | null;
974
+ country?: string | null;
975
+ offerSlug: string;
976
+ limit?: number;
977
+ includePending?: boolean;
978
+ freshDays?: number;
979
+ reset?: boolean;
980
+ }
981
+
982
+ export interface FbWorklistItem {
983
+ url: string;
984
+ name: string;
985
+ region: string | null;
986
+ currency: string | null;
987
+ lang: string | null;
988
+ member_count: number | null;
989
+ allows_promo: string;
990
+ lastPosted: string | null;
991
+ }
992
+
993
+ // COALESCE merge so a partial update never nulls richer prior data; name only overwrites when
994
+ // non-empty; 'false' promo is sticky (an excluded group can't be silently re-opened by an import).
995
+ const FB_GROUP_UPSERT_SQL = `
996
+ INSERT INTO fb_groups (url, name, region, country, lang, currency, joined, allows_promo,
997
+ member_count, notes, source, added_at, updated_at)
998
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
999
+ ON CONFLICT(url) DO UPDATE SET
1000
+ name = CASE WHEN excluded.name != '' THEN excluded.name ELSE fb_groups.name END,
1001
+ region = COALESCE(excluded.region, fb_groups.region),
1002
+ country = COALESCE(excluded.country, fb_groups.country),
1003
+ lang = COALESCE(excluded.lang, fb_groups.lang),
1004
+ currency = COALESCE(excluded.currency, fb_groups.currency),
1005
+ joined = excluded.joined,
1006
+ allows_promo = CASE
1007
+ WHEN fb_groups.allows_promo = 'false' THEN 'false'
1008
+ WHEN excluded.allows_promo = 'unknown' THEN fb_groups.allows_promo
1009
+ ELSE excluded.allows_promo END,
1010
+ member_count = COALESCE(excluded.member_count, fb_groups.member_count),
1011
+ notes = COALESCE(excluded.notes, fb_groups.notes),
1012
+ source = COALESCE(excluded.source, fb_groups.source),
1013
+ updated_at = excluded.updated_at
1014
+ `;
1015
+
1016
+ const FB_POSTLOG_SQL = `
1017
+ INSERT OR REPLACE INTO fb_post_log
1018
+ (id, group_url, offer_slug, currency, status, post_url, session_id, posted_at, note)
1019
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1020
+ `;
1021
+
1022
+ function runFbGroupUpsert(stmt: StatementSync, row: FbGroupUpsert, now: string): void {
1023
+ stmt.run(
1024
+ row.url,
1025
+ row.name ?? '',
1026
+ row.region ?? null,
1027
+ row.country ?? null,
1028
+ row.lang ?? null,
1029
+ row.currency ?? null,
1030
+ row.joined ?? 1,
1031
+ row.allows_promo ?? 'unknown',
1032
+ row.member_count ?? null,
1033
+ row.notes ?? null,
1034
+ row.source ?? 'import',
1035
+ now, // added_at (ignored on conflict)
1036
+ now, // updated_at
1037
+ );
1038
+ }
1039
+
1040
+ function runFbPostLog(stmt: StatementSync, e: FbPostLogEntry, now: string): void {
1041
+ stmt.run(
1042
+ e.id ?? randomUUID(),
1043
+ e.group_url,
1044
+ e.offer_slug,
1045
+ e.currency ?? null,
1046
+ e.status,
1047
+ e.post_url ?? null,
1048
+ e.session_id ?? null,
1049
+ e.posted_at ?? now,
1050
+ e.note ?? null,
1051
+ );
1052
+ }
1053
+
1054
+ /** Upsert one group row (idempotent, COALESCE merge). */
1055
+ export function upsertFbGroup(row: FbGroupUpsert): void {
1056
+ const d = getDb();
1057
+ runFbGroupUpsert(d.prepare(FB_GROUP_UPSERT_SQL), row, new Date().toISOString());
1058
+ }
1059
+
1060
+ /** Bulk migration write — many group upserts + post-log rows in one transaction. */
1061
+ export function importFbData(groups: FbGroupUpsert[], logs: FbPostLogEntry[]): { groupsUpserted: number; postLogRows: number } {
1062
+ const d = getDb();
1063
+ const now = new Date().toISOString();
1064
+ const gStmt = d.prepare(FB_GROUP_UPSERT_SQL);
1065
+ const lStmt = d.prepare(FB_POSTLOG_SQL);
1066
+ d.exec('BEGIN');
1067
+ try {
1068
+ for (const g of groups) runFbGroupUpsert(gStmt, g, now);
1069
+ for (const l of logs) runFbPostLog(lStmt, l, now);
1070
+ d.exec('COMMIT');
1071
+ } catch (err) {
1072
+ d.exec('ROLLBACK');
1073
+ throw err;
1074
+ }
1075
+ return { groupsUpserted: groups.length, postLogRows: logs.length };
1076
+ }
1077
+
1078
+ /**
1079
+ * The per-group checklist tick. One atomic transaction: ensure/refresh the group row, set its
1080
+ * state from the status (pending-approval engages a cooldown; visible-post clears it; failed/
1081
+ * skipped leave state untouched), and append the durable fb_post_log row. Replaces the
1082
+ * corruption-prone reflect-into-ledger entirely.
1083
+ */
1084
+ export function markFbGroup(opts: {
1085
+ url: string;
1086
+ offerSlug: string;
1087
+ currency?: string | null;
1088
+ status: string;
1089
+ postUrl?: string | null;
1090
+ note?: string | null;
1091
+ sessionId?: string | null;
1092
+ name?: string;
1093
+ region?: string | null;
1094
+ lang?: string | null;
1095
+ cooldownHours?: number;
1096
+ }): { url: string; status: string; pending_since: string | null; cooldown_until: string | null } {
1097
+ const d = getDb();
1098
+ const now = new Date().toISOString();
1099
+ const status = opts.status;
1100
+ const isPending = status === 'pending-approval';
1101
+ const isPosted = status === 'visible-post';
1102
+ const cooldownH = opts.cooldownHours ?? FB_COOLDOWN_HOURS_DEFAULT;
1103
+ const cooldownUntil = isPending ? new Date(Date.now() + cooldownH * 3600_000).toISOString() : null;
1104
+
1105
+ d.exec('BEGIN');
1106
+ try {
1107
+ // Ensure the group row exists / refresh descriptive fields (no state touch here).
1108
+ runFbGroupUpsert(d.prepare(FB_GROUP_UPSERT_SQL), {
1109
+ url: opts.url, name: opts.name, region: opts.region, lang: opts.lang,
1110
+ currency: opts.currency ?? undefined, joined: 1, source: 'manual',
1111
+ }, now);
1112
+ // last_status / last_error / currency.
1113
+ d.prepare(`UPDATE fb_groups SET last_status = ?, last_error = ?, currency = COALESCE(?, currency), updated_at = ? WHERE url = ?`)
1114
+ .run(status, status === 'posting-failed' ? (opts.note ?? 'posting-failed') : null, opts.currency ?? null, now, opts.url);
1115
+ // Pending/cooldown state — explicit so visible-post can clear a prior wall.
1116
+ if (isPending) {
1117
+ d.prepare(`UPDATE fb_groups SET pending_since = ?, cooldown_until = ? WHERE url = ?`).run(now, cooldownUntil, opts.url);
1118
+ } else if (isPosted) {
1119
+ d.prepare(`UPDATE fb_groups SET pending_since = NULL, cooldown_until = NULL WHERE url = ?`).run(opts.url);
1120
+ }
1121
+ // Durable append-only log row (one per group/offer/day; latest status wins).
1122
+ runFbPostLog(d.prepare(FB_POSTLOG_SQL), {
1123
+ group_url: opts.url, offer_slug: opts.offerSlug, currency: opts.currency ?? null,
1124
+ status, post_url: opts.postUrl ?? null, session_id: opts.sessionId ?? null, posted_at: now, note: opts.note ?? null,
1125
+ }, now);
1126
+ d.exec('COMMIT');
1127
+ } catch (err) {
1128
+ d.exec('ROLLBACK');
1129
+ throw err;
1130
+ }
1131
+
1132
+ const row = d.prepare(`SELECT pending_since, cooldown_until FROM fb_groups WHERE url = ?`).get(opts.url) as Record<string, unknown> | undefined;
1133
+ return {
1134
+ url: opts.url, status,
1135
+ pending_since: (row?.pending_since as string) ?? null,
1136
+ cooldown_until: (row?.cooldown_until as string) ?? null,
1137
+ };
1138
+ }
1139
+
1140
+ /**
1141
+ * The efficient region fetch + resume engine. Returns the groups still to post to for this offer:
1142
+ * joined, promo not forbidden, region/currency match, not pending/cooldown (unless includePending),
1143
+ * and not already posted for this offer within freshDays (unless reset). Never-posted first, then
1144
+ * least-recently-posted, then largest membership.
1145
+ */
1146
+ export function selectFbWorklist(opts: FbWorklistOpts): FbWorklistItem[] {
1147
+ const d = getDb();
1148
+ const { region, currency, offerSlug } = opts;
1149
+ const limit = opts.limit ?? 20;
1150
+ const freshDays = opts.freshDays ?? 14;
1151
+ const includePending = opts.includePending ?? false;
1152
+ const reset = opts.reset ?? false;
1153
+ const now = new Date();
1154
+ const nowIso = now.toISOString();
1155
+
1156
+ const where: string[] = ['g.joined = 1', "g.allows_promo != 'false'"];
1157
+ const params: SQLValue[] = [];
1158
+
1159
+ if (currency) {
1160
+ const ccy = currency.toUpperCase();
1161
+ const regionForCcy = REGION_BY_CURRENCY[ccy] || null;
1162
+ if (regionForCcy) {
1163
+ // Authoritative seed-queue currency, plus currency-less groups in that region.
1164
+ where.push('(upper(g.currency) = ? OR (g.currency IS NULL AND g.region = ?))');
1165
+ params.push(ccy, regionForCcy);
1166
+ } else {
1167
+ where.push('upper(g.currency) = ?');
1168
+ params.push(ccy);
1169
+ }
1170
+ } else if (region) {
1171
+ where.push('lower(g.region) = lower(?)');
1172
+ params.push(region);
1173
+ }
1174
+
1175
+ // Optional finer targeting (e.g. split the EU currency bucket by language for native-copy seeding).
1176
+ if (opts.lang) {
1177
+ where.push('lower(g.lang) = lower(?)');
1178
+ params.push(opts.lang);
1179
+ }
1180
+ if (opts.country) {
1181
+ where.push('lower(g.country) = lower(?)');
1182
+ params.push(opts.country);
1183
+ }
1184
+
1185
+ if (!includePending) {
1186
+ where.push('g.pending_since IS NULL');
1187
+ where.push('(g.cooldown_until IS NULL OR g.cooldown_until <= ?)');
1188
+ params.push(nowIso);
1189
+ }
1190
+
1191
+ if (!reset && offerSlug) {
1192
+ const cutoff = new Date(now.getTime() - freshDays * 86400_000).toISOString();
1193
+ where.push(
1194
+ `NOT EXISTS (SELECT 1 FROM fb_post_log p WHERE p.group_url = g.url AND p.offer_slug = ? ` +
1195
+ `AND p.status IN ('visible-post','pending-approval') AND p.posted_at >= ?)`
1196
+ );
1197
+ params.push(offerSlug, cutoff);
1198
+ }
1199
+
1200
+ const sql = `
1201
+ SELECT g.url, g.name, g.region, g.currency, g.lang, g.member_count, g.allows_promo,
1202
+ (SELECT MAX(posted_at) FROM fb_post_log p WHERE p.group_url = g.url) AS last_any_posted
1203
+ FROM fb_groups g
1204
+ WHERE ${where.join(' AND ')}
1205
+ ORDER BY (last_any_posted IS NULL) DESC, last_any_posted ASC, g.member_count DESC
1206
+ LIMIT ?`;
1207
+ params.push(limit);
1208
+
1209
+ const rows = d.prepare(sql).all(...params) as Array<Record<string, unknown>>;
1210
+ return rows.map((r) => ({
1211
+ url: r.url as string,
1212
+ name: r.name as string,
1213
+ region: (r.region as string) ?? null,
1214
+ currency: (r.currency as string) ?? null,
1215
+ lang: (r.lang as string) ?? null,
1216
+ member_count: (r.member_count as number) ?? null,
1217
+ allows_promo: r.allows_promo as string,
1218
+ lastPosted: (r.last_any_posted as string) ?? null,
1219
+ }));
1220
+ }
1221
+
1222
+ /** Set/clear a group's promo permission (the exclude action). Overrides the sticky-'false' rule. */
1223
+ export function setFbAllowsPromo(url: string, value: 'true' | 'false' | 'unknown', note?: string): void {
1224
+ const d = getDb();
1225
+ const now = new Date().toISOString();
1226
+ runFbGroupUpsert(d.prepare(FB_GROUP_UPSERT_SQL), { url, joined: 1, allows_promo: value, notes: note, source: 'manual' }, now);
1227
+ d.prepare(`UPDATE fb_groups SET allows_promo = ?, notes = COALESCE(?, notes), updated_at = ? WHERE url = ?`)
1228
+ .run(value, note ?? null, now, url);
1229
+ }
1230
+
1231
+ /**
1232
+ * Curation write: authoritatively set specific columns on a group. Ensures the row exists, then
1233
+ * UPDATEs ONLY the provided fields (undefined = leave untouched). Unlike upsertFbGroup's COALESCE
1234
+ * merge, this overrides — so it can correct a wrong region or re-open promo. Column names come from
1235
+ * a fixed allowlist (no injection).
1236
+ */
1237
+ export function setFbGroupFields(url: string, fields: {
1238
+ region?: string; lang?: string | null; currency?: string | null; country?: string | null;
1239
+ name?: string; member_count?: number | null; allows_promo?: 'true' | 'false' | 'unknown'; notes?: string | null;
1240
+ }): void {
1241
+ const d = getDb();
1242
+ const now = new Date().toISOString();
1243
+ runFbGroupUpsert(d.prepare(FB_GROUP_UPSERT_SQL), { url, joined: 1, source: 'curated' }, now);
1244
+ const allow: Array<[string, unknown]> = [
1245
+ ['region', fields.region], ['lang', fields.lang], ['currency', fields.currency],
1246
+ ['country', fields.country], ['name', fields.name], ['member_count', fields.member_count],
1247
+ ['allows_promo', fields.allows_promo], ['notes', fields.notes],
1248
+ ];
1249
+ const cols: string[] = [];
1250
+ const params: SQLValue[] = [];
1251
+ for (const [col, val] of allow) {
1252
+ if (val !== undefined) { cols.push(`${col} = ?`); params.push((val as SQLValue) ?? null); }
1253
+ }
1254
+ if (cols.length === 0) return;
1255
+ cols.push('updated_at = ?');
1256
+ params.push(now, url);
1257
+ d.prepare(`UPDATE fb_groups SET ${cols.join(', ')} WHERE url = ?`).run(...params);
1258
+ }
1259
+
1260
+ /** List group rows, optionally filtered to one region. */
1261
+ export function selectFbGroups(region?: string): Array<Record<string, unknown>> {
1262
+ const d = getDb();
1263
+ const cols = `url, name, region, country, lang, currency, joined, allows_promo, member_count, pending_since, cooldown_until, last_status, source, updated_at`;
1264
+ if (region) {
1265
+ return d.prepare(`SELECT ${cols} FROM fb_groups WHERE lower(region) = lower(?) ORDER BY member_count DESC, name`).all(region) as Array<Record<string, unknown>>;
1266
+ }
1267
+ return d.prepare(`SELECT ${cols} FROM fb_groups ORDER BY region, member_count DESC, name`).all() as Array<Record<string, unknown>>;
1268
+ }
1269
+
1270
+ /** Coverage + health aggregates for the list/stats action. */
1271
+ export function fbStats(region?: string): {
1272
+ byRegion: Record<string, { total: number; joined: number; promoOk: number; promoNo: number; pending: number; cooldown: number }>;
1273
+ totals: { groups: number; joined: number; pending: number; logRows: number };
1274
+ lastPostedPerOffer: Record<string, string>;
1275
+ } {
1276
+ const d = getDb();
1277
+ const now = new Date().toISOString();
1278
+ const regWhere = region ? ' WHERE lower(region) = lower(?)' : '';
1279
+ const regParams: SQLValue[] = region ? [region] : [];
1280
+
1281
+ const rows = d.prepare(`
1282
+ SELECT COALESCE(region,'Unknown') AS region,
1283
+ COUNT(*) AS total,
1284
+ SUM(CASE WHEN joined=1 THEN 1 ELSE 0 END) AS joined,
1285
+ SUM(CASE WHEN allows_promo!='false' THEN 1 ELSE 0 END) AS promoOk,
1286
+ SUM(CASE WHEN allows_promo='false' THEN 1 ELSE 0 END) AS promoNo,
1287
+ SUM(CASE WHEN pending_since IS NOT NULL THEN 1 ELSE 0 END) AS pending,
1288
+ SUM(CASE WHEN cooldown_until IS NOT NULL AND cooldown_until > ? THEN 1 ELSE 0 END) AS cooldown
1289
+ FROM fb_groups${regWhere}
1290
+ GROUP BY COALESCE(region,'Unknown')
1291
+ ORDER BY total DESC
1292
+ `).all(now, ...regParams) as Array<Record<string, unknown>>;
1293
+
1294
+ const byRegion: Record<string, { total: number; joined: number; promoOk: number; promoNo: number; pending: number; cooldown: number }> = {};
1295
+ for (const r of rows) {
1296
+ byRegion[r.region as string] = {
1297
+ total: Number(r.total), joined: Number(r.joined), promoOk: Number(r.promoOk),
1298
+ promoNo: Number(r.promoNo), pending: Number(r.pending), cooldown: Number(r.cooldown),
1299
+ };
1300
+ }
1301
+
1302
+ const tot = d.prepare(`SELECT COUNT(*) AS groups, SUM(CASE WHEN joined=1 THEN 1 ELSE 0 END) AS joined, SUM(CASE WHEN pending_since IS NOT NULL THEN 1 ELSE 0 END) AS pending FROM fb_groups`).get() as Record<string, unknown> | undefined;
1303
+ const logCount = d.prepare(`SELECT COUNT(*) AS c FROM fb_post_log`).get() as Record<string, unknown> | undefined;
1304
+ const offers = d.prepare(`SELECT offer_slug, MAX(posted_at) AS last FROM fb_post_log GROUP BY offer_slug ORDER BY last DESC LIMIT 25`).all() as Array<{ offer_slug: string; last: string }>;
1305
+ const lastPostedPerOffer: Record<string, string> = {};
1306
+ for (const o of offers) lastPostedPerOffer[o.offer_slug] = o.last;
1307
+
1308
+ return {
1309
+ byRegion,
1310
+ totals: {
1311
+ groups: Number(tot?.groups ?? 0), joined: Number(tot?.joined ?? 0),
1312
+ pending: Number(tot?.pending ?? 0), logRows: Number(logCount?.c ?? 0),
1313
+ },
1314
+ lastPostedPerOffer,
1315
+ };
1316
+ }
1317
+
1318
+ /**
1319
+ * One-shot directory cleanup / re-categorization. Re-derives region/lang/currency/country for
1320
+ * EVERY group from all available signals (group name + stored fields + the dominant currency seen
1321
+ * in fb_post_log), and backfills allows_promo from history: a confirmed visible-post proves promo
1322
+ * is allowed. Fixes the Unknown-region tail and the Spanish-as-EUR mis-bucket. Idempotent — safe to
1323
+ * re-run. Returns before/after counters for verification.
1324
+ */
1325
+ export function recategorizeFbGroups(): {
1326
+ scanned: number; regionChanged: number; currencyChanged: number; promoSet: number;
1327
+ stillUnknown: number; byRegion: Record<string, number>;
1328
+ } {
1329
+ const d = getDb();
1330
+ const now = new Date().toISOString();
1331
+ const rows = d.prepare(`SELECT url, name, region, lang, currency, allows_promo FROM fb_groups`).all() as Array<Record<string, unknown>>;
1332
+
1333
+ // Dominant non-empty currency + any-visible-post flag per group, from the durable post log.
1334
+ const sigRows = d.prepare(`
1335
+ SELECT p.group_url AS url,
1336
+ (SELECT p2.currency FROM fb_post_log p2
1337
+ WHERE p2.group_url = p.group_url AND p2.currency IS NOT NULL AND p2.currency != ''
1338
+ GROUP BY p2.currency ORDER BY COUNT(*) DESC, MAX(p2.posted_at) DESC LIMIT 1) AS topCurrency,
1339
+ MAX(CASE WHEN p.status = 'visible-post' THEN 1 ELSE 0 END) AS hadVisible
1340
+ FROM fb_post_log p GROUP BY p.group_url
1341
+ `).all() as Array<Record<string, unknown>>;
1342
+ const sig = new Map<string, { topCurrency: string | null; hadVisible: boolean }>();
1343
+ for (const s of sigRows) sig.set(s.url as string, { topCurrency: (s.topCurrency as string) || null, hadVisible: Number(s.hadVisible) === 1 });
1344
+
1345
+ const upd = d.prepare(
1346
+ `UPDATE fb_groups SET region = ?, lang = COALESCE(?, lang), currency = COALESCE(?, currency), ` +
1347
+ `country = COALESCE(?, country), allows_promo = ?, updated_at = ? WHERE url = ?`
1348
+ );
1349
+ let regionChanged = 0, currencyChanged = 0, promoSet = 0, stillUnknown = 0;
1350
+ const byRegion: Record<string, number> = {};
1351
+
1352
+ d.exec('BEGIN');
1353
+ try {
1354
+ for (const r of rows) {
1355
+ const url = r.url as string;
1356
+ const s = sig.get(url);
1357
+ const resolved = resolveClassification({
1358
+ name: (r.name as string) || '',
1359
+ url,
1360
+ region: (r.region as string) || null,
1361
+ lang: (r.lang as string) || null,
1362
+ currency: ((r.currency as string) || s?.topCurrency) || null,
1363
+ });
1364
+ const priorPromo = (r.allows_promo as string) || 'unknown';
1365
+ // A confirmed visible-post proves promo is allowed; an explicit 'false' exclude stays sticky.
1366
+ const promo = priorPromo === 'false' ? 'false' : (s?.hadVisible ? 'true' : priorPromo);
1367
+
1368
+ if (resolved.region !== ((r.region as string) || 'Unknown')) regionChanged++;
1369
+ if (resolved.currency && resolved.currency !== ((r.currency as string) || null)) currencyChanged++;
1370
+ if (promo !== priorPromo) promoSet++;
1371
+ if (resolved.region === 'Unknown') stillUnknown++;
1372
+ byRegion[resolved.region] = (byRegion[resolved.region] || 0) + 1;
1373
+
1374
+ upd.run(resolved.region, resolved.lang, resolved.currency, resolved.country, promo, now, url);
1375
+ }
1376
+ d.exec('COMMIT');
1377
+ } catch (err) {
1378
+ d.exec('ROLLBACK');
1379
+ throw err;
1380
+ }
1381
+ return { scanned: rows.length, regionChanged, currencyChanged, promoSet, stillUnknown, byRegion };
1382
+ }
1383
+
1384
+ // --- Helpers ---
1385
+
1386
+ function parseJsonOrDefault<T>(value: unknown, defaultValue: T): T {
1387
+ if (value == null || value === '') return defaultValue;
1388
+ if (typeof value === 'string') {
1389
+ try { return JSON.parse(value) as T; } catch { return defaultValue; }
1390
+ }
1391
+ return value as T;
1392
+ }
1393
+
1394
+ export function rowToPayload(row: Record<string, unknown>): MemoryPayload {
1395
+ return {
1396
+ content: row.content as string,
1397
+ category: row.category as string,
1398
+ subcategory: row.subcategory as string | undefined,
1399
+ project: row.project as string,
1400
+ tags: parseJsonOrDefault<string[]>(row.tags, []),
1401
+ importance: (row.importance as number) ?? 5,
1402
+ source: (row.source as MemoryPayload['source']) || 'self-discovered',
1403
+ created_at: row.created_at as string,
1404
+ updated_at: row.updated_at as string,
1405
+ accessed_at: row.accessed_at as string,
1406
+ access_count: (row.access_count as number) ?? 0,
1407
+ related_files: parseJsonOrDefault<string[] | undefined>(row.related_files, undefined),
1408
+ related_memory_ids: parseJsonOrDefault<string[] | undefined>(row.related_memory_ids, undefined),
1409
+ file_checksums: parseJsonOrDefault<Record<string, string> | undefined>(row.file_checksums, undefined),
1410
+ trashed_at: row.trashed_at as string | null | undefined,
1411
+ source_session_chunks: parseJsonOrDefault(row.source_session_chunks, undefined),
1412
+ };
1413
+ }
1414
+
1415
+ export function rowToSessionChunkPayload(row: Record<string, unknown>): SessionChunkPayload {
1416
+ return {
1417
+ content: row.content as string,
1418
+ summary: (row.summary as string) || '',
1419
+ session_id: (row.session_id as string) || '',
1420
+ project: (row.project as string) || '',
1421
+ git_branch: row.git_branch as string | null,
1422
+ cwd: row.cwd as string | null,
1423
+ chunk_index: (row.chunk_index as number) ?? 0,
1424
+ start_timestamp: (row.start_timestamp as string) || '',
1425
+ end_timestamp: (row.end_timestamp as string) || '',
1426
+ tools_used: parseJsonOrDefault<string[]>(row.tools_used, []),
1427
+ files_modified: parseJsonOrDefault<string[]>(row.files_modified, []),
1428
+ files_read: parseJsonOrDefault<string[]>(row.files_read, []),
1429
+ user_messages: parseJsonOrDefault<string[]>(row.user_messages, []),
1430
+ turn_count: (row.turn_count as number) ?? 0,
1431
+ related_memory_ids: parseJsonOrDefault<string[]>(row.related_memory_ids, []),
1432
+ dedup_memory_id: row.dedup_memory_id as string | null,
1433
+ indexed_at: (row.indexed_at as string) || '',
1434
+ };
1435
+ }
1436
+
1437
+ /**
1438
+ * Close the database connection. Call on process exit.
1439
+ */
1440
+ export function closeDatabase(): void {
1441
+ if (db) {
1442
+ db.close();
1443
+ db = null;
1444
+ }
1445
+ memoryVectors.invalidate();
1446
+ chunkVectors.invalidate();
1447
+ }
1448
+
1449
+ /**
1450
+ * Reopen the database at the current SQLITE_DB_PATH.
1451
+ * Call after updating process.env.SQLITE_DB_PATH to switch databases.
1452
+ */
1453
+ export async function reopenDatabase(): Promise<void> {
1454
+ closeDatabase();
1455
+ await ensureDatabase();
1456
+ }