luca 1.1.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (754) hide show
  1. package/.github/workflows/release.yaml +169 -0
  2. package/AGENTS.md +99 -0
  3. package/CLAUDE.md +115 -0
  4. package/CNAME +1 -0
  5. package/README.md +257 -8
  6. package/RUNME.md +56 -0
  7. package/assistants/codingAssistant/ABOUT.md +5 -0
  8. package/assistants/codingAssistant/CORE.md +28 -0
  9. package/assistants/codingAssistant/hooks.ts +21 -0
  10. package/assistants/codingAssistant/tools.ts +12 -0
  11. package/assistants/inkbot/ABOUT.md +16 -0
  12. package/assistants/inkbot/CORE.md +330 -0
  13. package/assistants/inkbot/hooks.ts +6 -0
  14. package/assistants/inkbot/tools.ts +53 -0
  15. package/assistants/researcher/ABOUT.md +5 -0
  16. package/assistants/researcher/CORE.md +46 -0
  17. package/assistants/researcher/hooks.ts +16 -0
  18. package/assistants/researcher/tools.ts +237 -0
  19. package/bun.lock +2769 -0
  20. package/bunfig.toml +3 -0
  21. package/commands/audit-docs.ts +740 -0
  22. package/commands/build-bootstrap.ts +118 -0
  23. package/commands/build-python-bridge.ts +43 -0
  24. package/commands/build-scaffolds.ts +176 -0
  25. package/commands/generate-api-docs.ts +114 -0
  26. package/commands/inkbot.ts +874 -0
  27. package/commands/release.ts +80 -0
  28. package/commands/try-all-challenges.ts +543 -0
  29. package/commands/try-challenge.ts +100 -0
  30. package/dist/agi/container.server.d.ts +63 -0
  31. package/dist/agi/container.server.d.ts.map +1 -0
  32. package/dist/agi/endpoints/ask.d.ts +20 -0
  33. package/dist/agi/endpoints/ask.d.ts.map +1 -0
  34. package/dist/agi/endpoints/conversations/[id].d.ts +27 -0
  35. package/dist/agi/endpoints/conversations/[id].d.ts.map +1 -0
  36. package/dist/agi/endpoints/conversations.d.ts +18 -0
  37. package/dist/agi/endpoints/conversations.d.ts.map +1 -0
  38. package/dist/agi/endpoints/experts.d.ts +8 -0
  39. package/dist/agi/endpoints/experts.d.ts.map +1 -0
  40. package/dist/agi/feature.d.ts +9 -0
  41. package/dist/agi/feature.d.ts.map +1 -0
  42. package/dist/agi/features/assistant.d.ts +509 -0
  43. package/dist/agi/features/assistant.d.ts.map +1 -0
  44. package/dist/agi/features/assistants-manager.d.ts +236 -0
  45. package/dist/agi/features/assistants-manager.d.ts.map +1 -0
  46. package/dist/agi/features/autonomous-assistant.d.ts +281 -0
  47. package/dist/agi/features/autonomous-assistant.d.ts.map +1 -0
  48. package/dist/agi/features/browser-use.d.ts +479 -0
  49. package/dist/agi/features/browser-use.d.ts.map +1 -0
  50. package/dist/agi/features/claude-code.d.ts +824 -0
  51. package/dist/agi/features/claude-code.d.ts.map +1 -0
  52. package/dist/agi/features/conversation-history.d.ts +245 -0
  53. package/dist/agi/features/conversation-history.d.ts.map +1 -0
  54. package/dist/agi/features/conversation.d.ts +464 -0
  55. package/dist/agi/features/conversation.d.ts.map +1 -0
  56. package/dist/agi/features/docs-reader.d.ts +72 -0
  57. package/dist/agi/features/docs-reader.d.ts.map +1 -0
  58. package/dist/agi/features/file-tools.d.ts +110 -0
  59. package/dist/agi/features/file-tools.d.ts.map +1 -0
  60. package/dist/agi/features/luca-coder.d.ts +323 -0
  61. package/dist/agi/features/luca-coder.d.ts.map +1 -0
  62. package/dist/agi/features/openai-codex.d.ts +381 -0
  63. package/dist/agi/features/openai-codex.d.ts.map +1 -0
  64. package/dist/agi/features/openapi.d.ts +200 -0
  65. package/dist/agi/features/openapi.d.ts.map +1 -0
  66. package/dist/agi/features/skills-library.d.ts +167 -0
  67. package/dist/agi/features/skills-library.d.ts.map +1 -0
  68. package/dist/agi/index.d.ts +5 -0
  69. package/dist/agi/index.d.ts.map +1 -0
  70. package/dist/agi/lib/interceptor-chain.d.ts +44 -0
  71. package/dist/agi/lib/interceptor-chain.d.ts.map +1 -0
  72. package/dist/agi/lib/token-counter.d.ts +13 -0
  73. package/dist/agi/lib/token-counter.d.ts.map +1 -0
  74. package/dist/bootstrap/generated.d.ts +5 -0
  75. package/dist/bootstrap/generated.d.ts.map +1 -0
  76. package/dist/browser.d.ts +12 -0
  77. package/dist/browser.d.ts.map +1 -0
  78. package/dist/bus.d.ts +29 -0
  79. package/dist/bus.d.ts.map +1 -0
  80. package/dist/cli/build-info.d.ts +4 -0
  81. package/dist/cli/build-info.d.ts.map +1 -0
  82. package/dist/cli/cli.d.ts +3 -0
  83. package/dist/cli/cli.d.ts.map +1 -0
  84. package/dist/client.d.ts +60 -0
  85. package/dist/client.d.ts.map +1 -0
  86. package/dist/clients/civitai/index.d.ts +472 -0
  87. package/dist/clients/civitai/index.d.ts.map +1 -0
  88. package/dist/clients/client-template.d.ts +30 -0
  89. package/dist/clients/client-template.d.ts.map +1 -0
  90. package/dist/clients/comfyui/index.d.ts +281 -0
  91. package/dist/clients/comfyui/index.d.ts.map +1 -0
  92. package/dist/clients/elevenlabs/index.d.ts +197 -0
  93. package/dist/clients/elevenlabs/index.d.ts.map +1 -0
  94. package/dist/clients/graph.d.ts +64 -0
  95. package/dist/clients/graph.d.ts.map +1 -0
  96. package/dist/clients/openai/index.d.ts +247 -0
  97. package/dist/clients/openai/index.d.ts.map +1 -0
  98. package/dist/clients/rest.d.ts +92 -0
  99. package/dist/clients/rest.d.ts.map +1 -0
  100. package/dist/clients/supabase/index.d.ts +176 -0
  101. package/dist/clients/supabase/index.d.ts.map +1 -0
  102. package/dist/clients/websocket.d.ts +127 -0
  103. package/dist/clients/websocket.d.ts.map +1 -0
  104. package/dist/command.d.ts +163 -0
  105. package/dist/command.d.ts.map +1 -0
  106. package/dist/commands/bootstrap.d.ts +20 -0
  107. package/dist/commands/bootstrap.d.ts.map +1 -0
  108. package/dist/commands/chat.d.ts +37 -0
  109. package/dist/commands/chat.d.ts.map +1 -0
  110. package/dist/commands/code.d.ts +28 -0
  111. package/dist/commands/code.d.ts.map +1 -0
  112. package/dist/commands/console.d.ts +22 -0
  113. package/dist/commands/console.d.ts.map +1 -0
  114. package/dist/commands/describe.d.ts +50 -0
  115. package/dist/commands/describe.d.ts.map +1 -0
  116. package/dist/commands/eval.d.ts +23 -0
  117. package/dist/commands/eval.d.ts.map +1 -0
  118. package/dist/commands/help.d.ts +25 -0
  119. package/dist/commands/help.d.ts.map +1 -0
  120. package/dist/commands/index.d.ts +18 -0
  121. package/dist/commands/index.d.ts.map +1 -0
  122. package/dist/commands/introspect.d.ts +24 -0
  123. package/dist/commands/introspect.d.ts.map +1 -0
  124. package/dist/commands/mcp.d.ts +35 -0
  125. package/dist/commands/mcp.d.ts.map +1 -0
  126. package/dist/commands/prompt.d.ts +38 -0
  127. package/dist/commands/prompt.d.ts.map +1 -0
  128. package/dist/commands/run.d.ts +24 -0
  129. package/dist/commands/run.d.ts.map +1 -0
  130. package/dist/commands/sandbox-mcp.d.ts +34 -0
  131. package/dist/commands/sandbox-mcp.d.ts.map +1 -0
  132. package/dist/commands/save-api-docs.d.ts +21 -0
  133. package/dist/commands/save-api-docs.d.ts.map +1 -0
  134. package/dist/commands/scaffold.d.ts +24 -0
  135. package/dist/commands/scaffold.d.ts.map +1 -0
  136. package/dist/commands/select.d.ts +22 -0
  137. package/dist/commands/select.d.ts.map +1 -0
  138. package/dist/commands/serve.d.ts +29 -0
  139. package/dist/commands/serve.d.ts.map +1 -0
  140. package/dist/container-describer.d.ts +144 -0
  141. package/dist/container-describer.d.ts.map +1 -0
  142. package/dist/container.d.ts +451 -0
  143. package/dist/container.d.ts.map +1 -0
  144. package/dist/endpoint.d.ts +113 -0
  145. package/dist/endpoint.d.ts.map +1 -0
  146. package/dist/feature.d.ts +47 -0
  147. package/dist/feature.d.ts.map +1 -0
  148. package/dist/graft.d.ts +29 -0
  149. package/dist/graft.d.ts.map +1 -0
  150. package/dist/hash-object.d.ts +8 -0
  151. package/dist/hash-object.d.ts.map +1 -0
  152. package/dist/helper.d.ts +209 -0
  153. package/dist/helper.d.ts.map +1 -0
  154. package/dist/introspection/generated.node.d.ts +44623 -0
  155. package/dist/introspection/generated.node.d.ts.map +1 -0
  156. package/dist/introspection/generated.web.d.ts +1412 -0
  157. package/dist/introspection/generated.web.d.ts.map +1 -0
  158. package/dist/introspection/index.d.ts +156 -0
  159. package/dist/introspection/index.d.ts.map +1 -0
  160. package/dist/introspection/scan.d.ts +147 -0
  161. package/dist/introspection/scan.d.ts.map +1 -0
  162. package/dist/node/container.d.ts +256 -0
  163. package/dist/node/container.d.ts.map +1 -0
  164. package/dist/node/feature.d.ts +9 -0
  165. package/dist/node/feature.d.ts.map +1 -0
  166. package/dist/node/features/container-link.d.ts +213 -0
  167. package/dist/node/features/container-link.d.ts.map +1 -0
  168. package/dist/node/features/content-db.d.ts +354 -0
  169. package/dist/node/features/content-db.d.ts.map +1 -0
  170. package/dist/node/features/disk-cache.d.ts +236 -0
  171. package/dist/node/features/disk-cache.d.ts.map +1 -0
  172. package/dist/node/features/dns.d.ts +511 -0
  173. package/dist/node/features/dns.d.ts.map +1 -0
  174. package/dist/node/features/docker.d.ts +485 -0
  175. package/dist/node/features/docker.d.ts.map +1 -0
  176. package/dist/node/features/downloader.d.ts +73 -0
  177. package/dist/node/features/downloader.d.ts.map +1 -0
  178. package/dist/node/features/figlet-fonts.d.ts +4 -0
  179. package/dist/node/features/figlet-fonts.d.ts.map +1 -0
  180. package/dist/node/features/file-manager.d.ts +177 -0
  181. package/dist/node/features/file-manager.d.ts.map +1 -0
  182. package/dist/node/features/fs.d.ts +635 -0
  183. package/dist/node/features/fs.d.ts.map +1 -0
  184. package/dist/node/features/git.d.ts +329 -0
  185. package/dist/node/features/git.d.ts.map +1 -0
  186. package/dist/node/features/google-auth.d.ts +200 -0
  187. package/dist/node/features/google-auth.d.ts.map +1 -0
  188. package/dist/node/features/google-calendar.d.ts +194 -0
  189. package/dist/node/features/google-calendar.d.ts.map +1 -0
  190. package/dist/node/features/google-docs.d.ts +138 -0
  191. package/dist/node/features/google-docs.d.ts.map +1 -0
  192. package/dist/node/features/google-drive.d.ts +202 -0
  193. package/dist/node/features/google-drive.d.ts.map +1 -0
  194. package/dist/node/features/google-mail.d.ts +221 -0
  195. package/dist/node/features/google-mail.d.ts.map +1 -0
  196. package/dist/node/features/google-sheets.d.ts +157 -0
  197. package/dist/node/features/google-sheets.d.ts.map +1 -0
  198. package/dist/node/features/grep.d.ts +207 -0
  199. package/dist/node/features/grep.d.ts.map +1 -0
  200. package/dist/node/features/helpers.d.ts +236 -0
  201. package/dist/node/features/helpers.d.ts.map +1 -0
  202. package/dist/node/features/ink.d.ts +332 -0
  203. package/dist/node/features/ink.d.ts.map +1 -0
  204. package/dist/node/features/ipc-socket.d.ts +298 -0
  205. package/dist/node/features/ipc-socket.d.ts.map +1 -0
  206. package/dist/node/features/json-tree.d.ts +140 -0
  207. package/dist/node/features/json-tree.d.ts.map +1 -0
  208. package/dist/node/features/networking.d.ts +373 -0
  209. package/dist/node/features/networking.d.ts.map +1 -0
  210. package/dist/node/features/nlp.d.ts +125 -0
  211. package/dist/node/features/nlp.d.ts.map +1 -0
  212. package/dist/node/features/opener.d.ts +93 -0
  213. package/dist/node/features/opener.d.ts.map +1 -0
  214. package/dist/node/features/os.d.ts +168 -0
  215. package/dist/node/features/os.d.ts.map +1 -0
  216. package/dist/node/features/package-finder.d.ts +419 -0
  217. package/dist/node/features/package-finder.d.ts.map +1 -0
  218. package/dist/node/features/postgres.d.ts +173 -0
  219. package/dist/node/features/postgres.d.ts.map +1 -0
  220. package/dist/node/features/proc.d.ts +285 -0
  221. package/dist/node/features/proc.d.ts.map +1 -0
  222. package/dist/node/features/process-manager.d.ts +427 -0
  223. package/dist/node/features/process-manager.d.ts.map +1 -0
  224. package/dist/node/features/python.d.ts +477 -0
  225. package/dist/node/features/python.d.ts.map +1 -0
  226. package/dist/node/features/redis.d.ts +247 -0
  227. package/dist/node/features/redis.d.ts.map +1 -0
  228. package/dist/node/features/repl.d.ts +84 -0
  229. package/dist/node/features/repl.d.ts.map +1 -0
  230. package/dist/node/features/runpod.d.ts +527 -0
  231. package/dist/node/features/runpod.d.ts.map +1 -0
  232. package/dist/node/features/secure-shell.d.ts +145 -0
  233. package/dist/node/features/secure-shell.d.ts.map +1 -0
  234. package/dist/node/features/semantic-search.d.ts +207 -0
  235. package/dist/node/features/semantic-search.d.ts.map +1 -0
  236. package/dist/node/features/sqlite.d.ts +180 -0
  237. package/dist/node/features/sqlite.d.ts.map +1 -0
  238. package/dist/node/features/telegram.d.ts +173 -0
  239. package/dist/node/features/telegram.d.ts.map +1 -0
  240. package/dist/node/features/transpiler.d.ts +51 -0
  241. package/dist/node/features/transpiler.d.ts.map +1 -0
  242. package/dist/node/features/tts.d.ts +108 -0
  243. package/dist/node/features/tts.d.ts.map +1 -0
  244. package/dist/node/features/ui.d.ts +562 -0
  245. package/dist/node/features/ui.d.ts.map +1 -0
  246. package/dist/node/features/vault.d.ts +90 -0
  247. package/dist/node/features/vault.d.ts.map +1 -0
  248. package/dist/node/features/vm.d.ts +285 -0
  249. package/dist/node/features/vm.d.ts.map +1 -0
  250. package/dist/node/features/yaml-tree.d.ts +118 -0
  251. package/dist/node/features/yaml-tree.d.ts.map +1 -0
  252. package/dist/node/features/yaml.d.ts +127 -0
  253. package/dist/node/features/yaml.d.ts.map +1 -0
  254. package/dist/node.d.ts +67 -0
  255. package/dist/node.d.ts.map +1 -0
  256. package/dist/python/generated.d.ts +2 -0
  257. package/dist/python/generated.d.ts.map +1 -0
  258. package/dist/react/index.d.ts +36 -0
  259. package/dist/react/index.d.ts.map +1 -0
  260. package/dist/registry.d.ts +97 -0
  261. package/dist/registry.d.ts.map +1 -0
  262. package/dist/scaffolds/generated.d.ts +13 -0
  263. package/dist/scaffolds/generated.d.ts.map +1 -0
  264. package/dist/scaffolds/template.d.ts +11 -0
  265. package/dist/scaffolds/template.d.ts.map +1 -0
  266. package/dist/schemas/base.d.ts +254 -0
  267. package/dist/schemas/base.d.ts.map +1 -0
  268. package/dist/selector.d.ts +130 -0
  269. package/dist/selector.d.ts.map +1 -0
  270. package/dist/server.d.ts +89 -0
  271. package/dist/server.d.ts.map +1 -0
  272. package/dist/servers/express.d.ts +104 -0
  273. package/dist/servers/express.d.ts.map +1 -0
  274. package/dist/servers/mcp.d.ts +201 -0
  275. package/dist/servers/mcp.d.ts.map +1 -0
  276. package/dist/servers/socket.d.ts +121 -0
  277. package/dist/servers/socket.d.ts.map +1 -0
  278. package/dist/state.d.ts +24 -0
  279. package/dist/state.d.ts.map +1 -0
  280. package/dist/web/clients/socket.d.ts +37 -0
  281. package/dist/web/clients/socket.d.ts.map +1 -0
  282. package/dist/web/container.d.ts +55 -0
  283. package/dist/web/container.d.ts.map +1 -0
  284. package/dist/web/extension.d.ts +4 -0
  285. package/dist/web/extension.d.ts.map +1 -0
  286. package/dist/web/feature.d.ts +8 -0
  287. package/dist/web/feature.d.ts.map +1 -0
  288. package/dist/web/features/asset-loader.d.ts +35 -0
  289. package/dist/web/features/asset-loader.d.ts.map +1 -0
  290. package/dist/web/features/container-link.d.ts +167 -0
  291. package/dist/web/features/container-link.d.ts.map +1 -0
  292. package/dist/web/features/esbuild.d.ts +51 -0
  293. package/dist/web/features/esbuild.d.ts.map +1 -0
  294. package/dist/web/features/helpers.d.ts +140 -0
  295. package/dist/web/features/helpers.d.ts.map +1 -0
  296. package/dist/web/features/network.d.ts +69 -0
  297. package/dist/web/features/network.d.ts.map +1 -0
  298. package/dist/web/features/speech.d.ts +71 -0
  299. package/dist/web/features/speech.d.ts.map +1 -0
  300. package/dist/web/features/vault.d.ts +62 -0
  301. package/dist/web/features/vault.d.ts.map +1 -0
  302. package/dist/web/features/vm.d.ts +48 -0
  303. package/dist/web/features/vm.d.ts.map +1 -0
  304. package/dist/web/features/voice-recognition.d.ts +96 -0
  305. package/dist/web/features/voice-recognition.d.ts.map +1 -0
  306. package/dist/web/shims/isomorphic-vm.d.ts +22 -0
  307. package/dist/web/shims/isomorphic-vm.d.ts.map +1 -0
  308. package/docs/CLI.md +335 -0
  309. package/docs/CNAME +1 -0
  310. package/docs/README.md +60 -0
  311. package/docs/TABLE-OF-CONTENTS.md +183 -0
  312. package/docs/apis/clients/elevenlabs.md +308 -0
  313. package/docs/apis/clients/graph.md +107 -0
  314. package/docs/apis/clients/openai.md +429 -0
  315. package/docs/apis/clients/rest.md +161 -0
  316. package/docs/apis/clients/websocket.md +174 -0
  317. package/docs/apis/features/agi/assistant.md +625 -0
  318. package/docs/apis/features/agi/assistants-manager.md +282 -0
  319. package/docs/apis/features/agi/auto-assistant.md +279 -0
  320. package/docs/apis/features/agi/browser-use.md +802 -0
  321. package/docs/apis/features/agi/claude-code.md +884 -0
  322. package/docs/apis/features/agi/conversation-history.md +364 -0
  323. package/docs/apis/features/agi/conversation.md +548 -0
  324. package/docs/apis/features/agi/docs-reader.md +99 -0
  325. package/docs/apis/features/agi/file-tools.md +163 -0
  326. package/docs/apis/features/agi/luca-coder.md +407 -0
  327. package/docs/apis/features/agi/openai-codex.md +396 -0
  328. package/docs/apis/features/agi/openapi.md +138 -0
  329. package/docs/apis/features/agi/semantic-search.md +387 -0
  330. package/docs/apis/features/agi/skills-library.md +239 -0
  331. package/docs/apis/features/node/container-link.md +192 -0
  332. package/docs/apis/features/node/content-db.md +450 -0
  333. package/docs/apis/features/node/disk-cache.md +379 -0
  334. package/docs/apis/features/node/dns.md +652 -0
  335. package/docs/apis/features/node/docker.md +706 -0
  336. package/docs/apis/features/node/downloader.md +81 -0
  337. package/docs/apis/features/node/esbuild.md +60 -0
  338. package/docs/apis/features/node/file-manager.md +191 -0
  339. package/docs/apis/features/node/fs.md +1217 -0
  340. package/docs/apis/features/node/git.md +371 -0
  341. package/docs/apis/features/node/google-auth.md +193 -0
  342. package/docs/apis/features/node/google-calendar.md +202 -0
  343. package/docs/apis/features/node/google-docs.md +173 -0
  344. package/docs/apis/features/node/google-drive.md +246 -0
  345. package/docs/apis/features/node/google-mail.md +214 -0
  346. package/docs/apis/features/node/google-sheets.md +194 -0
  347. package/docs/apis/features/node/grep.md +292 -0
  348. package/docs/apis/features/node/helpers.md +164 -0
  349. package/docs/apis/features/node/ink.md +334 -0
  350. package/docs/apis/features/node/ipc-socket.md +249 -0
  351. package/docs/apis/features/node/json-tree.md +86 -0
  352. package/docs/apis/features/node/networking.md +316 -0
  353. package/docs/apis/features/node/nlp.md +133 -0
  354. package/docs/apis/features/node/opener.md +97 -0
  355. package/docs/apis/features/node/os.md +146 -0
  356. package/docs/apis/features/node/package-finder.md +392 -0
  357. package/docs/apis/features/node/postgres.md +234 -0
  358. package/docs/apis/features/node/proc.md +399 -0
  359. package/docs/apis/features/node/process-manager.md +305 -0
  360. package/docs/apis/features/node/python.md +604 -0
  361. package/docs/apis/features/node/redis.md +380 -0
  362. package/docs/apis/features/node/repl.md +88 -0
  363. package/docs/apis/features/node/runpod.md +674 -0
  364. package/docs/apis/features/node/secure-shell.md +176 -0
  365. package/docs/apis/features/node/semantic-search.md +408 -0
  366. package/docs/apis/features/node/sqlite.md +233 -0
  367. package/docs/apis/features/node/telegram.md +279 -0
  368. package/docs/apis/features/node/transpiler.md +74 -0
  369. package/docs/apis/features/node/tts.md +133 -0
  370. package/docs/apis/features/node/ui.md +701 -0
  371. package/docs/apis/features/node/vault.md +59 -0
  372. package/docs/apis/features/node/vm.md +75 -0
  373. package/docs/apis/features/node/yaml-tree.md +85 -0
  374. package/docs/apis/features/node/yaml.md +176 -0
  375. package/docs/apis/features/web/asset-loader.md +59 -0
  376. package/docs/apis/features/web/container-link.md +192 -0
  377. package/docs/apis/features/web/esbuild.md +54 -0
  378. package/docs/apis/features/web/helpers.md +164 -0
  379. package/docs/apis/features/web/network.md +44 -0
  380. package/docs/apis/features/web/speech.md +69 -0
  381. package/docs/apis/features/web/vault.md +59 -0
  382. package/docs/apis/features/web/vm.md +75 -0
  383. package/docs/apis/features/web/voice.md +84 -0
  384. package/docs/apis/servers/express.md +171 -0
  385. package/docs/apis/servers/mcp.md +238 -0
  386. package/docs/apis/servers/websocket.md +170 -0
  387. package/docs/bootstrap/CLAUDE.md +101 -0
  388. package/docs/bootstrap/SKILL.md +341 -0
  389. package/docs/bootstrap/templates/about-command.ts +41 -0
  390. package/docs/bootstrap/templates/docs-models.ts +22 -0
  391. package/docs/bootstrap/templates/docs-readme.md +43 -0
  392. package/docs/bootstrap/templates/example-feature.ts +53 -0
  393. package/docs/bootstrap/templates/health-endpoint.ts +15 -0
  394. package/docs/bootstrap/templates/luca-cli.ts +30 -0
  395. package/docs/bootstrap/templates/runme.md +54 -0
  396. package/docs/challenges/caching-proxy.md +16 -0
  397. package/docs/challenges/content-db-round-trip.md +14 -0
  398. package/docs/challenges/custom-command.md +9 -0
  399. package/docs/challenges/file-watcher-pipeline.md +11 -0
  400. package/docs/challenges/grep-audit-report.md +15 -0
  401. package/docs/challenges/multi-feature-dashboard.md +14 -0
  402. package/docs/challenges/process-orchestrator.md +17 -0
  403. package/docs/challenges/rest-api-server-with-client.md +12 -0
  404. package/docs/challenges/script-runner-with-vm.md +11 -0
  405. package/docs/challenges/simple-rest-api.md +15 -0
  406. package/docs/challenges/websocket-serve-and-client.md +11 -0
  407. package/docs/challenges/yaml-config-system.md +14 -0
  408. package/docs/command-system-overhaul.md +94 -0
  409. package/docs/documentation-audit.md +134 -0
  410. package/docs/examples/assistant/CORE.md +18 -0
  411. package/docs/examples/assistant/hooks.ts +3 -0
  412. package/docs/examples/assistant/tools.ts +10 -0
  413. package/docs/examples/assistant-hooks-reference.ts +171 -0
  414. package/docs/examples/assistant-with-process-manager.md +84 -0
  415. package/docs/examples/content-db.md +77 -0
  416. package/docs/examples/disk-cache.md +83 -0
  417. package/docs/examples/docker.md +101 -0
  418. package/docs/examples/downloader.md +70 -0
  419. package/docs/examples/entity.md +124 -0
  420. package/docs/examples/esbuild.md +80 -0
  421. package/docs/examples/feature-as-tool-provider.md +143 -0
  422. package/docs/examples/file-manager.md +82 -0
  423. package/docs/examples/fs.md +83 -0
  424. package/docs/examples/git.md +85 -0
  425. package/docs/examples/google-auth.md +88 -0
  426. package/docs/examples/google-calendar.md +94 -0
  427. package/docs/examples/google-docs.md +82 -0
  428. package/docs/examples/google-drive.md +96 -0
  429. package/docs/examples/google-sheets.md +95 -0
  430. package/docs/examples/grep.md +85 -0
  431. package/docs/examples/ink-blocks.md +75 -0
  432. package/docs/examples/ink-renderer.md +41 -0
  433. package/docs/examples/ink.md +103 -0
  434. package/docs/examples/ipc-socket.md +103 -0
  435. package/docs/examples/json-tree.md +91 -0
  436. package/docs/examples/networking.md +58 -0
  437. package/docs/examples/nlp.md +91 -0
  438. package/docs/examples/opener.md +78 -0
  439. package/docs/examples/os.md +72 -0
  440. package/docs/examples/package-finder.md +89 -0
  441. package/docs/examples/postgres.md +91 -0
  442. package/docs/examples/proc.md +81 -0
  443. package/docs/examples/process-manager.md +79 -0
  444. package/docs/examples/python.md +132 -0
  445. package/docs/examples/repl.md +93 -0
  446. package/docs/examples/runpod.md +119 -0
  447. package/docs/examples/secure-shell.md +92 -0
  448. package/docs/examples/sqlite.md +86 -0
  449. package/docs/examples/structured-output-with-assistants.md +144 -0
  450. package/docs/examples/telegram.md +77 -0
  451. package/docs/examples/tts.md +86 -0
  452. package/docs/examples/ui.md +80 -0
  453. package/docs/examples/vault.md +70 -0
  454. package/docs/examples/vm.md +86 -0
  455. package/docs/examples/websocket-ask-and-reply-example.md +128 -0
  456. package/docs/examples/yaml-tree.md +93 -0
  457. package/docs/examples/yaml.md +104 -0
  458. package/docs/ideas/assistant-factory-pattern.md +142 -0
  459. package/docs/in-memory-fs.md +4 -0
  460. package/docs/introspection-audit.md +49 -0
  461. package/docs/introspection.md +164 -0
  462. package/docs/mcp/readme.md +162 -0
  463. package/docs/models.ts +41 -0
  464. package/docs/philosophy.md +86 -0
  465. package/docs/principles.md +7 -0
  466. package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
  467. package/docs/prompts/check-for-undocumented-features.md +27 -0
  468. package/docs/prompts/mcp-test-easy-command.md +27 -0
  469. package/docs/scaffolds/client.md +149 -0
  470. package/docs/scaffolds/command.md +120 -0
  471. package/docs/scaffolds/endpoint.md +171 -0
  472. package/docs/scaffolds/feature.md +158 -0
  473. package/docs/scaffolds/selector.md +91 -0
  474. package/docs/scaffolds/server.md +196 -0
  475. package/docs/selectors.md +115 -0
  476. package/docs/sessions/custom-command/attempt-log-2.md +195 -0
  477. package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +728 -0
  478. package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +555 -0
  479. package/docs/sessions/grep-audit-report/attempt-log-1.md +289 -0
  480. package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +679 -0
  481. package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +1 -0
  482. package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +920 -0
  483. package/docs/sessions/simple-rest-api/attempt-log-1.md +593 -0
  484. package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +995 -0
  485. package/docs/tutorials/00-bootstrap.md +166 -0
  486. package/docs/tutorials/01-getting-started.md +106 -0
  487. package/docs/tutorials/02-container.md +210 -0
  488. package/docs/tutorials/03-scripts.md +194 -0
  489. package/docs/tutorials/04-features-overview.md +196 -0
  490. package/docs/tutorials/05-state-and-events.md +171 -0
  491. package/docs/tutorials/06-servers.md +157 -0
  492. package/docs/tutorials/07-endpoints.md +198 -0
  493. package/docs/tutorials/08-commands.md +252 -0
  494. package/docs/tutorials/09-clients.md +162 -0
  495. package/docs/tutorials/10-creating-features.md +203 -0
  496. package/docs/tutorials/11-contentbase.md +191 -0
  497. package/docs/tutorials/12-assistants.md +215 -0
  498. package/docs/tutorials/13-introspection.md +157 -0
  499. package/docs/tutorials/14-type-system.md +174 -0
  500. package/docs/tutorials/15-project-patterns.md +222 -0
  501. package/docs/tutorials/16-google-features.md +534 -0
  502. package/docs/tutorials/17-tui-blocks.md +530 -0
  503. package/docs/tutorials/18-semantic-search.md +334 -0
  504. package/docs/tutorials/19-python-sessions.md +401 -0
  505. package/docs/tutorials/20-browser-esm.md +234 -0
  506. package/index.html +1430 -0
  507. package/index.ts +1 -0
  508. package/install.sh +84 -0
  509. package/luca.cli.ts +16 -0
  510. package/luca.console.ts +9 -0
  511. package/main.py +6 -0
  512. package/package.json +219 -66
  513. package/public/index.html +1430 -0
  514. package/public/slides-ai-native.html +902 -0
  515. package/public/slides-intro.html +974 -0
  516. package/pyproject.toml +7 -0
  517. package/scripts/build-web.ts +28 -0
  518. package/scripts/examples/ask-luca-expert.ts +42 -0
  519. package/scripts/examples/assistant-questions.ts +12 -0
  520. package/scripts/examples/excalidraw-expert.ts +75 -0
  521. package/scripts/examples/expert-chat.ts +0 -0
  522. package/scripts/examples/file-manager.ts +14 -0
  523. package/scripts/examples/ideas.ts +12 -0
  524. package/scripts/examples/interactive-chat.ts +20 -0
  525. package/scripts/examples/openai-tool-calls.ts +113 -0
  526. package/scripts/examples/opening-a-web-browser.ts +5 -0
  527. package/scripts/examples/telegram-bot.ts +79 -0
  528. package/scripts/examples/using-assistant-with-mcp.ts +555 -0
  529. package/scripts/examples/using-claude-code.ts +10 -0
  530. package/scripts/examples/using-contentdb.ts +35 -0
  531. package/scripts/examples/using-conversations.ts +35 -0
  532. package/scripts/examples/using-disk-cache.ts +10 -0
  533. package/scripts/examples/using-docker-shell.ts +75 -0
  534. package/scripts/examples/using-elevenlabs.ts +25 -0
  535. package/scripts/examples/using-google-calendar.ts +57 -0
  536. package/scripts/examples/using-google-docs.ts +74 -0
  537. package/scripts/examples/using-google-drive.ts +74 -0
  538. package/scripts/examples/using-google-sheets.ts +89 -0
  539. package/scripts/examples/using-nlp.ts +55 -0
  540. package/scripts/examples/using-ollama.ts +11 -0
  541. package/scripts/examples/using-postgres.ts +55 -0
  542. package/scripts/examples/using-runpod.ts +32 -0
  543. package/scripts/examples/using-tts.ts +40 -0
  544. package/scripts/scaffold.ts +391 -0
  545. package/scripts/scratch.ts +15 -0
  546. package/scripts/stamp-build.sh +12 -0
  547. package/scripts/test-assistant-hooks.ts +13 -0
  548. package/scripts/test-docs-reader.ts +10 -0
  549. package/scripts/test-linux-binary.sh +80 -0
  550. package/scripts/update-introspection-data.ts +58 -0
  551. package/src/agi/README.md +14 -0
  552. package/src/agi/container.server.ts +152 -0
  553. package/src/agi/endpoints/ask.ts +60 -0
  554. package/src/agi/endpoints/conversations/[id].ts +45 -0
  555. package/src/agi/endpoints/conversations.ts +31 -0
  556. package/src/agi/endpoints/experts.ts +37 -0
  557. package/src/agi/feature.ts +13 -0
  558. package/src/agi/features/agent-memory.ts +694 -0
  559. package/src/agi/features/assistant.ts +1624 -0
  560. package/src/agi/features/assistants-manager.ts +418 -0
  561. package/src/agi/features/autonomous-assistant.ts +431 -0
  562. package/src/agi/features/browser-use.ts +653 -0
  563. package/src/agi/features/claude-code.ts +1538 -0
  564. package/src/agi/features/coding-tools.ts +175 -0
  565. package/src/agi/features/conversation-history.ts +495 -0
  566. package/src/agi/features/conversation.ts +1323 -0
  567. package/src/agi/features/docs-reader.ts +167 -0
  568. package/src/agi/features/file-tools.ts +293 -0
  569. package/src/agi/features/luca-coder.ts +639 -0
  570. package/src/agi/features/openai-codex.ts +651 -0
  571. package/src/agi/features/openapi.ts +445 -0
  572. package/src/agi/features/skills-library.ts +478 -0
  573. package/src/agi/index.ts +6 -0
  574. package/src/agi/lib/interceptor-chain.ts +89 -0
  575. package/src/agi/lib/token-counter.ts +122 -0
  576. package/src/bootstrap/generated.ts +9792 -0
  577. package/src/browser.ts +25 -0
  578. package/src/bus.ts +122 -0
  579. package/src/cli/build-info.ts +4 -0
  580. package/src/cli/cli.ts +355 -0
  581. package/src/client.ts +170 -0
  582. package/src/clients/civitai/index.ts +537 -0
  583. package/src/clients/client-template.ts +41 -0
  584. package/src/clients/comfyui/index.ts +604 -0
  585. package/src/clients/elevenlabs/index.ts +317 -0
  586. package/src/clients/graph.ts +87 -0
  587. package/src/clients/openai/index.ts +456 -0
  588. package/src/clients/rest.ts +207 -0
  589. package/src/clients/supabase/index.ts +357 -0
  590. package/src/clients/voicebox/index.ts +300 -0
  591. package/src/clients/websocket.ts +251 -0
  592. package/src/command.ts +505 -0
  593. package/src/commands/bootstrap.ts +244 -0
  594. package/src/commands/chat.ts +308 -0
  595. package/src/commands/code.ts +371 -0
  596. package/src/commands/console.ts +189 -0
  597. package/src/commands/describe.ts +243 -0
  598. package/src/commands/eval.ts +121 -0
  599. package/src/commands/help.ts +240 -0
  600. package/src/commands/index.ts +19 -0
  601. package/src/commands/introspect.ts +218 -0
  602. package/src/commands/mcp.ts +64 -0
  603. package/src/commands/prompt.ts +982 -0
  604. package/src/commands/run.ts +278 -0
  605. package/src/commands/sandbox-mcp.ts +343 -0
  606. package/src/commands/save-api-docs.ts +51 -0
  607. package/src/commands/scaffold.ts +225 -0
  608. package/src/commands/select.ts +99 -0
  609. package/src/commands/serve.ts +208 -0
  610. package/src/container-describer.ts +1084 -0
  611. package/src/container.ts +1186 -0
  612. package/src/endpoint.ts +365 -0
  613. package/src/entity.ts +173 -0
  614. package/src/feature.ts +118 -0
  615. package/src/graft.ts +181 -0
  616. package/src/hash-object.ts +97 -0
  617. package/src/helper.ts +849 -0
  618. package/src/introspection/generated.agi.ts +40208 -0
  619. package/src/introspection/generated.node.ts +28686 -0
  620. package/src/introspection/generated.web.ts +2251 -0
  621. package/src/introspection/index.ts +296 -0
  622. package/src/introspection/scan.ts +1131 -0
  623. package/src/node/container.ts +409 -0
  624. package/src/node/feature.ts +13 -0
  625. package/src/node/features/container-link.ts +559 -0
  626. package/src/node/features/content-db.ts +812 -0
  627. package/src/node/features/disk-cache.ts +388 -0
  628. package/src/node/features/dns.ts +669 -0
  629. package/src/node/features/docker.ts +921 -0
  630. package/src/node/features/downloader.ts +79 -0
  631. package/src/node/features/figlet-fonts.ts +600 -0
  632. package/src/node/features/file-manager.ts +535 -0
  633. package/src/node/features/fs.ts +1050 -0
  634. package/src/node/features/git.ts +592 -0
  635. package/src/node/features/google-auth.ts +504 -0
  636. package/src/node/features/google-calendar.ts +306 -0
  637. package/src/node/features/google-docs.ts +412 -0
  638. package/src/node/features/google-drive.ts +346 -0
  639. package/src/node/features/google-mail.ts +540 -0
  640. package/src/node/features/google-sheets.ts +286 -0
  641. package/src/node/features/grep.ts +427 -0
  642. package/src/node/features/helpers.ts +735 -0
  643. package/src/node/features/ink.ts +490 -0
  644. package/src/node/features/ipc-socket.ts +649 -0
  645. package/src/node/features/json-tree.ts +170 -0
  646. package/src/node/features/networking.ts +961 -0
  647. package/src/node/features/nlp.ts +212 -0
  648. package/src/node/features/opener.ts +180 -0
  649. package/src/node/features/os.ts +403 -0
  650. package/src/node/features/package-finder.ts +540 -0
  651. package/src/node/features/postgres.ts +289 -0
  652. package/src/node/features/proc.ts +503 -0
  653. package/src/node/features/process-manager.ts +844 -0
  654. package/src/node/features/python.ts +906 -0
  655. package/src/node/features/redis.ts +446 -0
  656. package/src/node/features/repl.ts +212 -0
  657. package/src/node/features/runpod.ts +811 -0
  658. package/src/node/features/secure-shell.ts +267 -0
  659. package/src/node/features/semantic-search.ts +935 -0
  660. package/src/node/features/sqlite.ts +289 -0
  661. package/src/node/features/telegram.ts +343 -0
  662. package/src/node/features/transpiler.ts +161 -0
  663. package/src/node/features/tts.ts +185 -0
  664. package/src/node/features/ui.ts +786 -0
  665. package/src/node/features/vault.ts +153 -0
  666. package/src/node/features/vm.ts +462 -0
  667. package/src/node/features/yaml-tree.ts +148 -0
  668. package/src/node/features/yaml.ts +133 -0
  669. package/src/node.ts +76 -0
  670. package/src/python/bridge.py +220 -0
  671. package/src/python/generated.ts +227 -0
  672. package/src/react/index.ts +175 -0
  673. package/src/registry.ts +210 -0
  674. package/src/scaffolds/generated.ts +1815 -0
  675. package/src/scaffolds/template.ts +46 -0
  676. package/src/schemas/base.ts +296 -0
  677. package/src/selector.ts +352 -0
  678. package/src/server.ts +229 -0
  679. package/src/servers/express.ts +283 -0
  680. package/src/servers/mcp.ts +802 -0
  681. package/src/servers/socket.ts +258 -0
  682. package/src/state.ts +101 -0
  683. package/src/web/clients/socket.ts +99 -0
  684. package/src/web/container.ts +75 -0
  685. package/src/web/extension.ts +30 -0
  686. package/src/web/feature.ts +12 -0
  687. package/src/web/features/asset-loader.ts +72 -0
  688. package/src/web/features/container-link.ts +382 -0
  689. package/src/web/features/esbuild.ts +93 -0
  690. package/src/web/features/helpers.ts +269 -0
  691. package/src/web/features/network.ts +85 -0
  692. package/src/web/features/speech.ts +104 -0
  693. package/src/web/features/vault.ts +207 -0
  694. package/src/web/features/vm.ts +85 -0
  695. package/src/web/features/voice-recognition.ts +161 -0
  696. package/src/web/shims/isomorphic-vm.ts +149 -0
  697. package/test/assistant-hooks.test.ts +306 -0
  698. package/test/assistant.test.ts +81 -0
  699. package/test/bus.test.ts +134 -0
  700. package/test/clients-servers.test.ts +217 -0
  701. package/test/command.test.ts +267 -0
  702. package/test/container-link.test.ts +274 -0
  703. package/test/conversation.test.ts +220 -0
  704. package/test/features.test.ts +160 -0
  705. package/test/fork-and-research.test.ts +450 -0
  706. package/test/integration.test.ts +787 -0
  707. package/test/interceptor-chain.test.ts +61 -0
  708. package/test/node-container.test.ts +121 -0
  709. package/test/python-session.test.ts +105 -0
  710. package/test/rate-limit.test.ts +272 -0
  711. package/test/semantic-search.test.ts +550 -0
  712. package/test/state.test.ts +121 -0
  713. package/test/vm-context.test.ts +146 -0
  714. package/test/vm-loadmodule.test.ts +213 -0
  715. package/test/websocket-ask.test.ts +101 -0
  716. package/test-integration/assistant.test.ts +138 -0
  717. package/test-integration/assistants-manager.test.ts +113 -0
  718. package/test-integration/claude-code.test.ts +98 -0
  719. package/test-integration/conversation-history.test.ts +205 -0
  720. package/test-integration/conversation.test.ts +137 -0
  721. package/test-integration/elevenlabs.test.ts +55 -0
  722. package/test-integration/google-services.test.ts +80 -0
  723. package/test-integration/helpers.ts +89 -0
  724. package/test-integration/memory.test.ts +204 -0
  725. package/test-integration/openai-codex.test.ts +93 -0
  726. package/test-integration/runpod.test.ts +58 -0
  727. package/test-integration/server-endpoints.test.ts +97 -0
  728. package/test-integration/telegram.test.ts +46 -0
  729. package/tsconfig.build.json +12 -0
  730. package/tsconfig.json +58 -0
  731. package/uv.lock +8 -0
  732. package/LICENSE +0 -21
  733. package/dist/cli/index.d.ts +0 -2
  734. package/dist/cli/index.js +0 -5
  735. package/dist/cli/run.d.ts +0 -12
  736. package/dist/cli/run.js +0 -42
  737. package/dist/config/consts.d.ts +0 -2
  738. package/dist/config/consts.js +0 -29
  739. package/dist/config/default.d.ts +0 -8
  740. package/dist/config/default.js +0 -15
  741. package/dist/config/initConfig.d.ts +0 -1
  742. package/dist/config/initConfig.js +0 -52
  743. package/dist/config/openConfig.d.ts +0 -2
  744. package/dist/config/openConfig.js +0 -24
  745. package/dist/config/runConfig.d.ts +0 -3
  746. package/dist/config/runConfig.js +0 -117
  747. package/dist/config/types.d.ts +0 -13
  748. package/dist/config/types.js +0 -2
  749. package/dist/index.d.ts +0 -1
  750. package/dist/index.js +0 -5
  751. package/dist/utils/common.d.ts +0 -2
  752. package/dist/utils/common.js +0 -52
  753. package/dist/utils/index.d.ts +0 -1
  754. package/dist/utils/index.js +0 -17
@@ -0,0 +1,935 @@
1
+ import { z } from 'zod'
2
+ import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
3
+ import { type AvailableFeatures } from '@soederpop/luca/feature'
4
+ import { Feature } from '../feature.js'
5
+ import { Database } from 'bun:sqlite'
6
+ import { createHash } from 'node:crypto'
7
+ import { mkdirSync, existsSync, statSync } from 'node:fs'
8
+ import { dirname, join } from 'node:path'
9
+ import { homedir } from 'node:os'
10
+
11
+ declare module '@soederpop/luca/feature' {
12
+ interface AvailableFeatures {
13
+ semanticSearch: typeof SemanticSearch
14
+ }
15
+ }
16
+
17
+ // ── Schemas ─────────────────────────────────────────────────────────
18
+
19
+ export const SemanticSearchOptionsSchema = FeatureOptionsSchema.extend({
20
+ dbPath: z.string().default('.contentbase/search.sqlite').describe('Path to the SQLite database file'),
21
+ embeddingModel: z.string().default('text-embedding-3-small').describe('Embedding model name'),
22
+ embeddingProvider: z.enum(['local', 'openai']).default('openai').describe('Where to generate embeddings'),
23
+ chunkStrategy: z.enum(['section', 'fixed', 'document']).default('section').describe('How to split documents'),
24
+ chunkSize: z.number().default(900).describe('Token limit per chunk for fixed strategy'),
25
+ chunkOverlap: z.number().default(0.15).describe('Overlap ratio for fixed strategy'),
26
+ })
27
+
28
+ export const SemanticSearchStateSchema = FeatureStateSchema.extend({
29
+ indexed: z.number().default(0).describe('Count of indexed documents'),
30
+ embedded: z.number().default(0).describe('Count of documents with embeddings'),
31
+ lastIndexedAt: z.string().nullable().default(null).describe('ISO timestamp of last indexing'),
32
+ dbReady: z.boolean().default(false).describe('Whether SQLite is initialized'),
33
+ })
34
+
35
+ export type SemanticSearchOptions = z.infer<typeof SemanticSearchOptionsSchema>
36
+ export type SemanticSearchState = z.infer<typeof SemanticSearchStateSchema>
37
+
38
+ export const SemanticSearchEventsSchema = FeatureEventsSchema.extend({
39
+ modelLoaded: z.tuple([]).describe('When the local embedding model is loaded into memory'),
40
+ dbReady: z.tuple([]).describe('When the SQLite database is initialized and ready'),
41
+ indexed: z.tuple([z.object({
42
+ documents: z.number().describe('Number of documents indexed'),
43
+ chunks: z.number().describe('Number of chunks created'),
44
+ }).describe('Indexing result')]).describe('When documents are indexed with embeddings'),
45
+ modelDisposed: z.tuple([]).describe('When the local embedding model is disposed from memory'),
46
+ }).describe('Semantic Search events')
47
+
48
+ // ── Types ───────────────────────────────────────────────────────────
49
+
50
+ export interface Chunk {
51
+ pathId: string
52
+ section?: string
53
+ headingPath?: string
54
+ seq: number
55
+ content: string
56
+ contentHash: string
57
+ }
58
+
59
+ export interface SearchResult {
60
+ pathId: string
61
+ model: string
62
+ title: string
63
+ meta: Record<string, any>
64
+ score: number
65
+ snippet: string
66
+ matchedSection?: string
67
+ headingPath?: string
68
+ }
69
+
70
+ export interface SearchOptions {
71
+ limit?: number
72
+ model?: string
73
+ where?: Record<string, any>
74
+ }
75
+
76
+ export interface HybridSearchOptions extends SearchOptions {
77
+ ftsWeight?: number
78
+ vecWeight?: number
79
+ }
80
+
81
+ export interface IndexStatus {
82
+ documentCount: number
83
+ chunkCount: number
84
+ embeddingCount: number
85
+ lastIndexedAt: string | null
86
+ provider: string
87
+ model: string
88
+ dimensions: number
89
+ dbSizeBytes: number
90
+ }
91
+
92
+ export interface DocumentInput {
93
+ pathId: string
94
+ model?: string
95
+ title?: string
96
+ slug?: string
97
+ meta?: Record<string, any>
98
+ content: string
99
+ sections?: Array<{ heading: string; headingPath: string; content: string; level: number }>
100
+ }
101
+
102
+ // ── Dimension map ───────────────────────────────────────────────────
103
+
104
+ const DIMENSION_MAP: Record<string, number> = {
105
+ 'embedding-gemma-300M-Q8_0': 768,
106
+ 'text-embedding-3-small': 1536,
107
+ 'text-embedding-3-large': 3072,
108
+ }
109
+
110
+ function getDimensions(provider: string, model: string): number {
111
+ if (provider === 'openai') {
112
+ return DIMENSION_MAP[model] ?? 1536
113
+ }
114
+ return DIMENSION_MAP[model] ?? 768
115
+ }
116
+
117
+ // ── Model path resolution ───────────────────────────────────────────
118
+
119
+ const MODEL_FILENAMES: Record<string, string> = {
120
+ 'embedding-gemma-300M-Q8_0': 'hf_ggml-org_embeddinggemma-300M-Q8_0.gguf',
121
+ }
122
+
123
+ function resolveModelPath(modelName: string): string {
124
+ const filename = MODEL_FILENAMES[modelName] ?? `${modelName}.gguf`
125
+ const cacheBase = process.env.XDG_CACHE_HOME || join(homedir(), '.cache')
126
+ const lucaCache = join(cacheBase, 'luca', 'models', filename)
127
+ if (existsSync(lucaCache)) return lucaCache
128
+ const qmdCache = join(cacheBase, 'qmd', 'models', filename)
129
+ if (existsSync(qmdCache)) return qmdCache
130
+ return lucaCache
131
+ }
132
+
133
+ // ── Content hashing ─────────────────────────────────────────────────
134
+
135
+ function contentHash(text: string): string {
136
+ return createHash('sha256').update(text).digest('hex').slice(0, 16)
137
+ }
138
+
139
+ // ── Vector math ─────────────────────────────────────────────────────
140
+
141
+ function cosineSimilarity(a: Float32Array, b: Float32Array): number {
142
+ let dot = 0, normA = 0, normB = 0
143
+ for (let i = 0; i < a.length; i++) {
144
+ dot += a[i]! * b[i]!
145
+ normA += a[i]! * a[i]!
146
+ normB += b[i]! * b[i]!
147
+ }
148
+ const denom = Math.sqrt(normA) * Math.sqrt(normB)
149
+ return denom === 0 ? 0 : dot / denom
150
+ }
151
+
152
+ function vecToBlob(vec: Float32Array): Buffer {
153
+ return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength)
154
+ }
155
+
156
+ function blobToVec(blob: Buffer | Uint8Array): Float32Array {
157
+ const bytes = blob instanceof Uint8Array ? blob : new Uint8Array(blob)
158
+ return new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 4)
159
+ }
160
+
161
+ // ── Chunking functions ──────────────────────────────────────────────
162
+
163
+ function chunkBySection(doc: DocumentInput, maxTokens: number): Chunk[] {
164
+ const chunks: Chunk[] = []
165
+ const sections = doc.sections || []
166
+
167
+ if (sections.length === 0) {
168
+ return chunkByFixed(doc, maxTokens, 0.15)
169
+ }
170
+
171
+ for (let i = 0; i < sections.length; i++) {
172
+ const section = sections[i]!
173
+ const prefix = doc.title ? `${doc.title} > ${section.headingPath}` : section.headingPath
174
+ const text = `${prefix}\n\n${section.content}`
175
+
176
+ const words = text.split(/\s+/)
177
+ const wordLimit = Math.floor(maxTokens * 0.75)
178
+
179
+ if (words.length <= wordLimit) {
180
+ chunks.push({
181
+ pathId: doc.pathId,
182
+ section: section.heading,
183
+ headingPath: section.headingPath,
184
+ seq: chunks.length,
185
+ content: text,
186
+ contentHash: contentHash(text),
187
+ })
188
+ } else {
189
+ const subChunks = splitAtParagraphs(text, wordLimit)
190
+ for (const sub of subChunks) {
191
+ chunks.push({
192
+ pathId: doc.pathId,
193
+ section: section.heading,
194
+ headingPath: section.headingPath,
195
+ seq: chunks.length,
196
+ content: sub,
197
+ contentHash: contentHash(sub),
198
+ })
199
+ }
200
+ }
201
+ }
202
+
203
+ return chunks
204
+ }
205
+
206
+ function splitAtParagraphs(text: string, wordLimit: number): string[] {
207
+ const paragraphs = text.split(/\n\n+/)
208
+ const results: string[] = []
209
+ let current: string[] = []
210
+ let currentWords = 0
211
+
212
+ for (const para of paragraphs) {
213
+ const paraWords = para.split(/\s+/).length
214
+ if (currentWords + paraWords > wordLimit && current.length > 0) {
215
+ results.push(current.join('\n\n'))
216
+ current = [para]
217
+ currentWords = paraWords
218
+ } else {
219
+ current.push(para)
220
+ currentWords += paraWords
221
+ }
222
+ }
223
+
224
+ if (current.length > 0) {
225
+ results.push(current.join('\n\n'))
226
+ }
227
+
228
+ return results
229
+ }
230
+
231
+ function chunkByFixed(doc: DocumentInput, maxTokens: number, overlapPct: number): Chunk[] {
232
+ const words = doc.content.split(/\s+/)
233
+ const chunkSize = Math.floor(maxTokens * 0.75)
234
+ const overlap = Math.floor(chunkSize * overlapPct)
235
+ const chunks: Chunk[] = []
236
+ let start = 0
237
+
238
+ while (start < words.length) {
239
+ const end = Math.min(start + chunkSize, words.length)
240
+ const text = words.slice(start, end).join(' ')
241
+ chunks.push({
242
+ pathId: doc.pathId,
243
+ seq: chunks.length,
244
+ content: text,
245
+ contentHash: contentHash(text),
246
+ })
247
+ if (end >= words.length) break
248
+ start = end - overlap
249
+ }
250
+
251
+ return chunks
252
+ }
253
+
254
+ function chunkByDocument(doc: DocumentInput): Chunk[] {
255
+ return [{
256
+ pathId: doc.pathId,
257
+ seq: 0,
258
+ content: doc.content,
259
+ contentHash: contentHash(doc.content),
260
+ }]
261
+ }
262
+
263
+ // ── Feature class ───────────────────────────────────────────────────
264
+
265
+ /**
266
+ * Semantic search feature providing BM25 keyword search, vector similarity search,
267
+ * and hybrid search with Reciprocal Rank Fusion over a SQLite-backed index.
268
+ *
269
+ * Uses bun:sqlite for FTS5 keyword search and BLOB-stored embeddings with
270
+ * JavaScript cosine similarity for vector search.
271
+ *
272
+ * @extends Feature
273
+ *
274
+ * @example
275
+ * ```typescript
276
+ * const search = container.feature('semanticSearch', {
277
+ * dbPath: '.contentbase/search.sqlite',
278
+ * embeddingProvider: 'local',
279
+ * })
280
+ * await search.initDb()
281
+ * await search.indexDocuments(docs)
282
+ * const results = await search.hybridSearch('how does authentication work')
283
+ * ```
284
+ */
285
+ export class SemanticSearch extends Feature<SemanticSearchState, SemanticSearchOptions> {
286
+ static override stateSchema = SemanticSearchStateSchema
287
+ static override optionsSchema = SemanticSearchOptionsSchema
288
+ static override eventsSchema = SemanticSearchEventsSchema
289
+ static override shortcut = 'features.semanticSearch' as const
290
+ static { Feature.register(this, 'semanticSearch') }
291
+
292
+ private _db: Database | null = null
293
+ private _llamaContext: any = null
294
+ private _llamaModel: any = null
295
+ private _llamaInstance: any = null
296
+ private _idleTimer: ReturnType<typeof setTimeout> | null = null
297
+ private _dimensions: number
298
+
299
+
300
+ override get initialState(): SemanticSearchState {
301
+ return {
302
+ ...super.initialState,
303
+ indexed: 0,
304
+ embedded: 0,
305
+ lastIndexedAt: null,
306
+ dbReady: false,
307
+ }
308
+ }
309
+
310
+ constructor(options: SemanticSearchOptions, context: any) {
311
+ super(options, context)
312
+ this._dimensions = getDimensions(this.options.embeddingProvider, this.options.embeddingModel)
313
+ }
314
+
315
+ // ── Database path ───────────────────────────────────────────────
316
+
317
+ private get resolvedDbPath(): string {
318
+ const base = this.options.dbPath.replace(/\.sqlite$/, '')
319
+ const suffix = `${this.options.embeddingProvider}-${this.options.embeddingModel}`
320
+ return `${base}.${suffix}.sqlite`
321
+ }
322
+
323
+ get db(): Database {
324
+ if (!this._db) throw new Error('Database not initialized. Call initDb() first.')
325
+ return this._db
326
+ }
327
+
328
+ get dimensions(): number {
329
+ return this._dimensions
330
+ }
331
+
332
+ // ── 2.2 Database Layer ──────────────────────────────────────────
333
+
334
+ async initDb(): Promise<void> {
335
+ const dbPath = this.resolvedDbPath
336
+ const dir = dirname(dbPath)
337
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
338
+
339
+ const isNew = !existsSync(dbPath)
340
+ this._db = new Database(dbPath)
341
+
342
+ this._db.exec('PRAGMA journal_mode = WAL')
343
+ this._db.exec('PRAGMA foreign_keys = ON')
344
+
345
+ if (isNew) {
346
+ this._createTables()
347
+ this._writeMeta()
348
+ } else {
349
+ this._verifyMeta()
350
+ }
351
+
352
+ this.state.set('dbReady', true)
353
+ this.emit('dbReady')
354
+ }
355
+
356
+ private _createTables(): void {
357
+ this.db.exec(`
358
+ CREATE TABLE search_meta (
359
+ key TEXT PRIMARY KEY,
360
+ value TEXT NOT NULL
361
+ )
362
+ `)
363
+
364
+ this.db.exec(`
365
+ CREATE TABLE documents (
366
+ path_id TEXT PRIMARY KEY,
367
+ model TEXT,
368
+ title TEXT,
369
+ slug TEXT,
370
+ meta_json TEXT,
371
+ content TEXT,
372
+ sections_json TEXT,
373
+ content_hash TEXT,
374
+ indexed_at TEXT
375
+ )
376
+ `)
377
+
378
+ this.db.exec(`
379
+ CREATE VIRTUAL TABLE documents_fts USING fts5(
380
+ path_id, title, content, sections_text,
381
+ tokenize='porter unicode61'
382
+ )
383
+ `)
384
+
385
+ this.db.exec(`
386
+ CREATE TABLE chunks (
387
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
388
+ path_id TEXT NOT NULL,
389
+ section TEXT,
390
+ heading_path TEXT,
391
+ seq INTEGER NOT NULL,
392
+ content TEXT NOT NULL,
393
+ content_hash TEXT NOT NULL,
394
+ embedding BLOB,
395
+ FOREIGN KEY (path_id) REFERENCES documents(path_id) ON DELETE CASCADE
396
+ )
397
+ `)
398
+ }
399
+
400
+ private _writeMeta(): void {
401
+ const insert = this.db.prepare('INSERT INTO search_meta (key, value) VALUES (?, ?)')
402
+ const tx = this.db.transaction(() => {
403
+ insert.run('provider', this.options.embeddingProvider)
404
+ insert.run('model', this.options.embeddingModel)
405
+ insert.run('dims', String(this._dimensions))
406
+ insert.run('createdAt', new Date().toISOString())
407
+ })
408
+ tx()
409
+ }
410
+
411
+ private _verifyMeta(): void {
412
+ const getMeta = this.db.query('SELECT value FROM search_meta WHERE key = ?')
413
+ const stored = {
414
+ provider: (getMeta.get('provider') as any)?.value,
415
+ model: (getMeta.get('model') as any)?.value,
416
+ dims: (getMeta.get('dims') as any)?.value,
417
+ }
418
+
419
+ const expected = {
420
+ provider: this.options.embeddingProvider,
421
+ model: this.options.embeddingModel,
422
+ dims: String(this._dimensions),
423
+ }
424
+
425
+ if (stored.provider !== expected.provider || stored.model !== expected.model || stored.dims !== expected.dims) {
426
+ this._db?.close()
427
+ this._db = null
428
+ throw new Error(
429
+ `Database provider/model mismatch. ` +
430
+ `Stored: ${stored.provider}/${stored.model} (${stored.dims}d). ` +
431
+ `Expected: ${expected.provider}/${expected.model} (${expected.dims}d). ` +
432
+ `Delete the database and re-index to switch providers.`
433
+ )
434
+ }
435
+ }
436
+
437
+ insertDocument(doc: DocumentInput): void {
438
+ const sectionsText = doc.sections?.map(s => `${s.heading}\n${s.content}`).join('\n\n') ?? ''
439
+
440
+ const hash = contentHash(doc.content)
441
+ const now = new Date().toISOString()
442
+
443
+ this.db.prepare(
444
+ `INSERT OR REPLACE INTO documents (path_id, model, title, slug, meta_json, content, sections_json, content_hash, indexed_at)
445
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
446
+ ).run(
447
+ doc.pathId,
448
+ doc.model ?? null,
449
+ doc.title ?? null,
450
+ doc.slug ?? null,
451
+ doc.meta ? JSON.stringify(doc.meta) : null,
452
+ doc.content,
453
+ doc.sections ? JSON.stringify(doc.sections) : null,
454
+ hash,
455
+ now,
456
+ )
457
+
458
+ // Sync FTS5
459
+ this.db.prepare('DELETE FROM documents_fts WHERE path_id = ?').run(doc.pathId)
460
+ this.db.prepare(
461
+ 'INSERT INTO documents_fts(path_id, title, content, sections_text) VALUES(?, ?, ?, ?)'
462
+ ).run(doc.pathId, doc.title ?? '', doc.content, sectionsText)
463
+ }
464
+
465
+ insertChunk(chunk: Chunk, embedding: Float32Array): void {
466
+ this.db.prepare(
467
+ `INSERT INTO chunks (path_id, section, heading_path, seq, content, content_hash, embedding)
468
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
469
+ ).run(
470
+ chunk.pathId,
471
+ chunk.section ?? null,
472
+ chunk.headingPath ?? null,
473
+ chunk.seq,
474
+ chunk.content,
475
+ chunk.contentHash,
476
+ vecToBlob(embedding),
477
+ )
478
+ }
479
+
480
+ removeDocument(pathId: string): void {
481
+ this.db.prepare('DELETE FROM documents_fts WHERE path_id = ?').run(pathId)
482
+ this.db.prepare('DELETE FROM chunks WHERE path_id = ?').run(pathId)
483
+ this.db.prepare('DELETE FROM documents WHERE path_id = ?').run(pathId)
484
+ }
485
+
486
+ getStats(): IndexStatus {
487
+ const docCount = (this.db.query('SELECT COUNT(*) as c FROM documents').get() as any).c
488
+ const chunkCount = (this.db.query('SELECT COUNT(*) as c FROM chunks').get() as any).c
489
+ const embCount = (this.db.query('SELECT COUNT(*) as c FROM chunks WHERE embedding IS NOT NULL').get() as any).c
490
+ const lastDoc = this.db.query('SELECT indexed_at FROM documents ORDER BY indexed_at DESC LIMIT 1').get() as any
491
+
492
+ let dbSize = 0
493
+ try { dbSize = statSync(this.resolvedDbPath).size } catch {}
494
+
495
+ return {
496
+ documentCount: docCount,
497
+ chunkCount: chunkCount,
498
+ embeddingCount: embCount,
499
+ lastIndexedAt: lastDoc?.indexed_at ?? null,
500
+ provider: this.options.embeddingProvider,
501
+ model: this.options.embeddingModel,
502
+ dimensions: this._dimensions,
503
+ dbSizeBytes: dbSize,
504
+ }
505
+ }
506
+
507
+ // ── 2.3 Embedding Engine ────────────────────────────────────────
508
+
509
+ async embed(texts: string[]): Promise<number[][]> {
510
+ if (this.options.embeddingProvider === 'openai') {
511
+ return this._embedOpenAI(texts)
512
+ }
513
+ return this._embedLocal(texts)
514
+ }
515
+
516
+ private async _embedLocal(texts: string[]): Promise<number[][]> {
517
+ const ctx = await this._ensureLocalModel()
518
+ const results: number[][] = []
519
+
520
+ for (const text of texts) {
521
+ try {
522
+ const embedding = await ctx.getEmbeddingFor(text)
523
+ results.push(Array.from(new Float32Array(embedding.vector)))
524
+ } catch {
525
+ const truncated = text.split(/\s+/).slice(0, 300).join(' ')
526
+ try {
527
+ const embedding = await ctx.getEmbeddingFor(truncated)
528
+ results.push(Array.from(new Float32Array(embedding.vector)))
529
+ } catch {
530
+ results.push(new Array(this._dimensions).fill(0))
531
+ }
532
+ }
533
+ }
534
+
535
+ this._resetIdleTimer()
536
+ return results
537
+ }
538
+
539
+ private async _embedOpenAI(texts: string[]): Promise<number[][]> {
540
+ const openai = (this.container as any).client('openai') as any
541
+ const results: number[][] = []
542
+
543
+ for (let i = 0; i < texts.length; i += 2048) {
544
+ const batch = texts.slice(i, i + 2048)
545
+ const response = await openai.openai.embeddings.create({
546
+ model: this.options.embeddingModel === 'embedding-gemma-300M-Q8_0'
547
+ ? 'text-embedding-3-small'
548
+ : this.options.embeddingModel,
549
+ input: batch,
550
+ })
551
+ for (const item of response.data) {
552
+ results.push(item.embedding)
553
+ }
554
+ }
555
+
556
+ return results
557
+ }
558
+
559
+ async ensureModel(): Promise<void> {
560
+ if (this.options.embeddingProvider === 'local') {
561
+ await this._ensureLocalModel()
562
+ }
563
+ }
564
+
565
+ private async _ensureLocalModel(): Promise<any> {
566
+ if (this._llamaContext) return this._llamaContext
567
+
568
+ let getLlama: any
569
+ try {
570
+ // Try resolving from the project's node_modules first (for compiled binary),
571
+ // then fall back to regular dynamic import
572
+ const cwdModulePath = join(process.cwd(), 'node_modules', 'node-llama-cpp')
573
+ try {
574
+ ;({ getLlama } = await import(cwdModulePath))
575
+ } catch {
576
+ ;({ getLlama } = await import('node-llama-cpp'))
577
+ }
578
+ } catch {
579
+ throw new Error(
580
+ 'Local embeddings require node-llama-cpp which is not installed.\n' +
581
+ 'Either:\n' +
582
+ ' 1. Use OpenAI embeddings (default): set embeddingProvider to "openai"\n' +
583
+ ' 2. Install node-llama-cpp: bun add --optional node-llama-cpp@3.17.1\n' +
584
+ ' 3. Use the helper: await semanticSearch.installLocalEmbeddings(process.cwd())'
585
+ )
586
+ }
587
+ this._llamaInstance = await getLlama()
588
+
589
+ const modelPath = resolveModelPath(this.options.embeddingModel)
590
+ if (!existsSync(modelPath)) {
591
+ throw new Error(
592
+ `Embedding model not found at ${modelPath}. ` +
593
+ `Download it to ~/.cache/luca/models/ or ~/.cache/qmd/models/`
594
+ )
595
+ }
596
+
597
+ this._llamaModel = await this._llamaInstance.loadModel({ modelPath })
598
+ this._llamaContext = await this._llamaModel.createEmbeddingContext({ contextSize: 2048 })
599
+
600
+ this.emit('modelLoaded')
601
+ return this._llamaContext
602
+ }
603
+
604
+ private _resetIdleTimer(): void {
605
+ if (this._idleTimer) clearTimeout(this._idleTimer)
606
+ this._idleTimer = setTimeout(() => this.disposeModel(), 5 * 60 * 1000)
607
+ }
608
+
609
+ async disposeModel(): Promise<void> {
610
+ if (this._idleTimer) {
611
+ clearTimeout(this._idleTimer)
612
+ this._idleTimer = null
613
+ }
614
+ if (this._llamaContext) {
615
+ await this._llamaContext.dispose()
616
+ this._llamaContext = null
617
+ }
618
+ if (this._llamaModel) {
619
+ await this._llamaModel.dispose()
620
+ this._llamaModel = null
621
+ }
622
+ if (this._llamaInstance) {
623
+ await this._llamaInstance.dispose()
624
+ this._llamaInstance = null
625
+ }
626
+ this.emit('modelDisposed')
627
+ }
628
+
629
+ getDimensions(): number {
630
+ return this._dimensions
631
+ }
632
+
633
+ // ── 2.4 Document Chunking ───────────────────────────────────────
634
+
635
+ chunkDocument(doc: DocumentInput, strategy?: 'section' | 'fixed' | 'document'): Chunk[] {
636
+ const strat = strategy ?? this.options.chunkStrategy
637
+
638
+ switch (strat) {
639
+ case 'section':
640
+ return chunkBySection(doc, this.options.chunkSize)
641
+ case 'fixed':
642
+ return chunkByFixed(doc, this.options.chunkSize, this.options.chunkOverlap)
643
+ case 'document':
644
+ return chunkByDocument(doc)
645
+ default:
646
+ return chunkByFixed(doc, this.options.chunkSize, this.options.chunkOverlap)
647
+ }
648
+ }
649
+
650
+ // ── 2.5 Search Engine ───────────────────────────────────────────
651
+
652
+ async search(query: string, options: SearchOptions = {}): Promise<SearchResult[]> {
653
+ const limit = options.limit ?? 10
654
+ const whereClause = this._buildWhereClause(options)
655
+
656
+ const stmt = this.db.query(`
657
+ SELECT f.path_id, f.title, f.content,
658
+ d.model, d.meta_json,
659
+ bm25(documents_fts, 0, 1, 2, 1) as score,
660
+ snippet(documents_fts, 2, '>>>', '<<<', '...', 40) as snippet
661
+ FROM documents_fts f
662
+ JOIN documents d ON d.path_id = f.path_id
663
+ WHERE documents_fts MATCH ?
664
+ ${whereClause}
665
+ ORDER BY score
666
+ LIMIT ?
667
+ `)
668
+
669
+ const rows = stmt.all(query, limit) as any[]
670
+
671
+ return rows.map(r => ({
672
+ pathId: r.path_id,
673
+ model: r.model ?? '',
674
+ title: r.title ?? '',
675
+ meta: r.meta_json ? JSON.parse(r.meta_json) : {},
676
+ score: -r.score, // FTS5 bm25 returns negative (lower = better)
677
+ snippet: r.snippet ?? '',
678
+ matchedSection: undefined,
679
+ headingPath: undefined,
680
+ }))
681
+ }
682
+
683
+ async vectorSearch(query: string, options: SearchOptions = {}): Promise<SearchResult[]> {
684
+ const limit = options.limit ?? 10
685
+ const embeddings = await this.embed([query])
686
+ const queryVec = new Float32Array(embeddings[0]!)
687
+
688
+ // Fetch all chunks with embeddings
689
+ const rows = this.db.query(`
690
+ SELECT c.id, c.path_id, c.section, c.heading_path, c.content, c.embedding,
691
+ d.model, d.title, d.meta_json
692
+ FROM chunks c
693
+ JOIN documents d ON d.path_id = c.path_id
694
+ WHERE c.embedding IS NOT NULL
695
+ `).all() as any[]
696
+
697
+ // Compute cosine similarity and rank
698
+ const scored: Array<{ row: any; similarity: number }> = []
699
+ for (const r of rows) {
700
+ if (!this._matchesFilters(r, options)) continue
701
+ const chunkVec = blobToVec(r.embedding)
702
+ const similarity = cosineSimilarity(queryVec, chunkVec)
703
+ scored.push({ row: r, similarity })
704
+ }
705
+
706
+ scored.sort((a, b) => b.similarity - a.similarity)
707
+
708
+ // Deduplicate by pathId, keep best score
709
+ const seen = new Map<string, (typeof scored)[0]>()
710
+ for (const s of scored) {
711
+ if (!seen.has(s.row.path_id)) {
712
+ seen.set(s.row.path_id, s)
713
+ }
714
+ }
715
+
716
+ return Array.from(seen.values())
717
+ .slice(0, limit)
718
+ .map(s => ({
719
+ pathId: s.row.path_id,
720
+ model: s.row.model ?? '',
721
+ title: s.row.title ?? '',
722
+ meta: s.row.meta_json ? JSON.parse(s.row.meta_json) : {},
723
+ score: s.similarity,
724
+ snippet: (s.row.content ?? '').substring(0, 200),
725
+ matchedSection: s.row.section ?? undefined,
726
+ headingPath: s.row.heading_path ?? undefined,
727
+ }))
728
+ }
729
+
730
+ async hybridSearch(query: string, options: HybridSearchOptions = {}): Promise<SearchResult[]> {
731
+ const limit = options.limit ?? 10
732
+ const fetchLimit = Math.max(limit * 2, 20)
733
+
734
+ const [bm25Results, vecResults] = await Promise.all([
735
+ this.search(query, { ...options, limit: fetchLimit }).catch(() => [] as SearchResult[]),
736
+ this.vectorSearch(query, { ...options, limit: fetchLimit }),
737
+ ])
738
+
739
+ return this._fuseRRF(bm25Results, vecResults, limit)
740
+ }
741
+
742
+ async deepSearch(_query: string, _options: SearchOptions = {}): Promise<SearchResult[]> {
743
+ throw new Error('deepSearch is not yet implemented (planned for v2). Use hybridSearch() instead.')
744
+ }
745
+
746
+ private _fuseRRF(bm25Results: SearchResult[], vecResults: SearchResult[], limit: number): SearchResult[] {
747
+ const RRF_K = 60
748
+ const scores = new Map<string, { score: number; result: SearchResult }>()
749
+
750
+ for (let i = 0; i < bm25Results.length; i++) {
751
+ const r = bm25Results[i]!
752
+ const rrfScore = 1 / (RRF_K + i + 1)
753
+ scores.set(r.pathId, { score: rrfScore, result: r })
754
+ }
755
+
756
+ for (let i = 0; i < vecResults.length; i++) {
757
+ const r = vecResults[i]!
758
+ const rrfScore = 1 / (RRF_K + i + 1)
759
+ const existing = scores.get(r.pathId)
760
+ if (existing) {
761
+ existing.score += rrfScore
762
+ if (r.matchedSection) existing.result.matchedSection = r.matchedSection
763
+ if (r.headingPath) existing.result.headingPath = r.headingPath
764
+ } else {
765
+ scores.set(r.pathId, { score: rrfScore, result: r })
766
+ }
767
+ }
768
+
769
+ return Array.from(scores.values())
770
+ .sort((a, b) => b.score - a.score)
771
+ .slice(0, limit)
772
+ .map(s => ({ ...s.result, score: s.score }))
773
+ }
774
+
775
+ private _buildWhereClause(options: SearchOptions): string {
776
+ const conditions: string[] = []
777
+
778
+ if (options.model) {
779
+ conditions.push(`d.model = '${options.model.replace(/'/g, "''")}'`)
780
+ }
781
+
782
+ if (options.where) {
783
+ for (const [key, value] of Object.entries(options.where)) {
784
+ const escaped = String(value).replace(/'/g, "''")
785
+ conditions.push(`json_extract(d.meta_json, '$.${key}') = '${escaped}'`)
786
+ }
787
+ }
788
+
789
+ return conditions.length > 0 ? `AND ${conditions.join(' AND ')}` : ''
790
+ }
791
+
792
+ private _matchesFilters(row: any, options: SearchOptions): boolean {
793
+ if (options.model && row.model !== options.model) return false
794
+ if (options.where && row.meta_json) {
795
+ const meta = JSON.parse(row.meta_json)
796
+ for (const [key, value] of Object.entries(options.where)) {
797
+ if (meta[key] !== value) return false
798
+ }
799
+ }
800
+ return true
801
+ }
802
+
803
+ // ── 2.6 Index Management ────────────────────────────────────────
804
+
805
+ async indexDocuments(docs: DocumentInput[]): Promise<void> {
806
+ const tx = this.db.transaction(() => {
807
+ for (const doc of docs) {
808
+ this.insertDocument(doc)
809
+ }
810
+ })
811
+ tx()
812
+
813
+ // Chunk and embed
814
+ const allChunks: Chunk[] = []
815
+ for (const doc of docs) {
816
+ const chunks = this.chunkDocument(doc)
817
+ for (const chunk of chunks) {
818
+ allChunks.push(chunk)
819
+ }
820
+ }
821
+
822
+ // Batch embed all chunks
823
+ const texts = allChunks.map(c => c.content)
824
+ const embeddings = await this.embed(texts)
825
+
826
+ // Insert chunks + vectors in a transaction
827
+ const insertTx = this.db.transaction(() => {
828
+ for (let i = 0; i < allChunks.length; i++) {
829
+ this.insertChunk(allChunks[i]!, new Float32Array(embeddings[i]!))
830
+ }
831
+ })
832
+ insertTx()
833
+
834
+ const now = new Date().toISOString()
835
+ this.state.set('indexed', (this.state.get('indexed') ?? 0) + docs.length)
836
+ this.state.set('embedded', (this.state.get('embedded') ?? 0) + allChunks.length)
837
+ this.state.set('lastIndexedAt', now)
838
+
839
+ this.emit('indexed', { documents: docs.length, chunks: allChunks.length })
840
+ }
841
+
842
+ async reindex(pathIds?: string[]): Promise<void> {
843
+ if (pathIds) {
844
+ for (const pathId of pathIds) {
845
+ this.removeDocument(pathId)
846
+ }
847
+ } else {
848
+ this.db.exec('DELETE FROM chunks')
849
+ this.db.exec('DELETE FROM documents_fts')
850
+ this.db.exec('DELETE FROM documents')
851
+ }
852
+ }
853
+
854
+ removeStale(currentPathIds: string[]): void {
855
+ const existing = (this.db.query('SELECT path_id FROM documents').all() as any[])
856
+ .map(r => r.path_id)
857
+
858
+ const currentSet = new Set(currentPathIds)
859
+ const stale = existing.filter((id: string) => !currentSet.has(id))
860
+
861
+ for (const pathId of stale) {
862
+ this.removeDocument(pathId)
863
+ }
864
+ }
865
+
866
+ needsReindex(doc: DocumentInput): boolean {
867
+ const row = this.db.query('SELECT content_hash FROM documents WHERE path_id = ?').get(doc.pathId) as any
868
+ if (!row) return true
869
+ return row.content_hash !== contentHash(doc.content)
870
+ }
871
+
872
+ status(): IndexStatus {
873
+ return this.getStats()
874
+ }
875
+
876
+ // ── Local Embeddings Install ────────────────────────────────────
877
+
878
+ static readonly PINNED_LLAMA_VERSION = '3.17.1'
879
+
880
+ /**
881
+ * Install node-llama-cpp into the user's project for local embedding support.
882
+ * Detects package manager from lockfile presence and verifies the native addon loads.
883
+ */
884
+ async installLocalEmbeddings(cwd: string): Promise<void> {
885
+ const { execSync } = await import('node:child_process')
886
+ const pkg = `node-llama-cpp@${SemanticSearch.PINNED_LLAMA_VERSION}`
887
+
888
+ let cmd: string
889
+ if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) {
890
+ cmd = `bun add --optional ${pkg}`
891
+ } else if (existsSync(join(cwd, 'pnpm-lock.yaml'))) {
892
+ cmd = `pnpm add --save-optional ${pkg}`
893
+ } else if (existsSync(join(cwd, 'yarn.lock'))) {
894
+ cmd = `yarn add --optional ${pkg}`
895
+ } else {
896
+ cmd = `npm install --save-optional ${pkg}`
897
+ }
898
+
899
+ try {
900
+ execSync(cmd, { cwd, stdio: 'pipe', timeout: 120_000 })
901
+ } catch (err: any) {
902
+ const stderr = err?.stderr?.toString() ?? ''
903
+ throw new Error(
904
+ `Failed to install ${pkg} via: ${cmd}\n` +
905
+ (stderr ? `stderr: ${stderr}\n` : '') +
906
+ `If this is an ABI mismatch, ensure your Node/Bun version matches the prebuilt binary.`
907
+ )
908
+ }
909
+
910
+ // Verify the native addon actually loads
911
+ const modulePath = join(cwd, 'node_modules', 'node-llama-cpp')
912
+ try {
913
+ await import(modulePath)
914
+ } catch (err: any) {
915
+ throw new Error(
916
+ `node-llama-cpp was installed but failed to load from ${modulePath}.\n` +
917
+ `This usually means a native addon ABI mismatch.\n` +
918
+ `Error: ${err?.message ?? err}`
919
+ )
920
+ }
921
+ }
922
+
923
+ // ── Lifecycle ───────────────────────────────────────────────────
924
+
925
+ async close(): Promise<void> {
926
+ await this.disposeModel()
927
+ if (this._db) {
928
+ this._db.close()
929
+ this._db = null
930
+ }
931
+ this.state.set('dbReady', false)
932
+ }
933
+ }
934
+
935
+ export default SemanticSearch