luca 2.0.0 → 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.
- package/.github/workflows/release.yaml +169 -0
- package/AGENTS.md +99 -0
- package/CLAUDE.md +115 -0
- package/CNAME +1 -0
- package/README.md +257 -9
- package/RUNME.md +56 -0
- package/assistants/codingAssistant/ABOUT.md +5 -0
- package/assistants/codingAssistant/CORE.md +28 -0
- package/assistants/codingAssistant/hooks.ts +21 -0
- package/assistants/codingAssistant/tools.ts +12 -0
- package/assistants/inkbot/ABOUT.md +16 -0
- package/assistants/inkbot/CORE.md +330 -0
- package/assistants/inkbot/hooks.ts +6 -0
- package/assistants/inkbot/tools.ts +53 -0
- package/assistants/researcher/ABOUT.md +5 -0
- package/assistants/researcher/CORE.md +46 -0
- package/assistants/researcher/hooks.ts +16 -0
- package/assistants/researcher/tools.ts +237 -0
- package/bun.lock +2769 -0
- package/bunfig.toml +3 -0
- package/commands/audit-docs.ts +740 -0
- package/commands/build-bootstrap.ts +118 -0
- package/commands/build-python-bridge.ts +43 -0
- package/commands/build-scaffolds.ts +176 -0
- package/commands/generate-api-docs.ts +114 -0
- package/commands/inkbot.ts +874 -0
- package/commands/release.ts +80 -0
- package/commands/try-all-challenges.ts +543 -0
- package/commands/try-challenge.ts +100 -0
- package/dist/agi/container.server.d.ts +63 -0
- package/dist/agi/container.server.d.ts.map +1 -0
- package/dist/agi/endpoints/ask.d.ts +20 -0
- package/dist/agi/endpoints/ask.d.ts.map +1 -0
- package/dist/agi/endpoints/conversations/[id].d.ts +27 -0
- package/dist/agi/endpoints/conversations/[id].d.ts.map +1 -0
- package/dist/agi/endpoints/conversations.d.ts +18 -0
- package/dist/agi/endpoints/conversations.d.ts.map +1 -0
- package/dist/agi/endpoints/experts.d.ts +8 -0
- package/dist/agi/endpoints/experts.d.ts.map +1 -0
- package/dist/agi/feature.d.ts +9 -0
- package/dist/agi/feature.d.ts.map +1 -0
- package/dist/agi/features/assistant.d.ts +509 -0
- package/dist/agi/features/assistant.d.ts.map +1 -0
- package/dist/agi/features/assistants-manager.d.ts +236 -0
- package/dist/agi/features/assistants-manager.d.ts.map +1 -0
- package/dist/agi/features/autonomous-assistant.d.ts +281 -0
- package/dist/agi/features/autonomous-assistant.d.ts.map +1 -0
- package/dist/agi/features/browser-use.d.ts +479 -0
- package/dist/agi/features/browser-use.d.ts.map +1 -0
- package/dist/agi/features/claude-code.d.ts +824 -0
- package/dist/agi/features/claude-code.d.ts.map +1 -0
- package/dist/agi/features/conversation-history.d.ts +245 -0
- package/dist/agi/features/conversation-history.d.ts.map +1 -0
- package/dist/agi/features/conversation.d.ts +464 -0
- package/dist/agi/features/conversation.d.ts.map +1 -0
- package/dist/agi/features/docs-reader.d.ts +72 -0
- package/dist/agi/features/docs-reader.d.ts.map +1 -0
- package/dist/agi/features/file-tools.d.ts +110 -0
- package/dist/agi/features/file-tools.d.ts.map +1 -0
- package/dist/agi/features/luca-coder.d.ts +323 -0
- package/dist/agi/features/luca-coder.d.ts.map +1 -0
- package/dist/agi/features/openai-codex.d.ts +381 -0
- package/dist/agi/features/openai-codex.d.ts.map +1 -0
- package/dist/agi/features/openapi.d.ts +200 -0
- package/dist/agi/features/openapi.d.ts.map +1 -0
- package/dist/agi/features/skills-library.d.ts +167 -0
- package/dist/agi/features/skills-library.d.ts.map +1 -0
- package/dist/agi/index.d.ts +5 -0
- package/dist/agi/index.d.ts.map +1 -0
- package/dist/agi/lib/interceptor-chain.d.ts +44 -0
- package/dist/agi/lib/interceptor-chain.d.ts.map +1 -0
- package/dist/agi/lib/token-counter.d.ts +13 -0
- package/dist/agi/lib/token-counter.d.ts.map +1 -0
- package/dist/bootstrap/generated.d.ts +5 -0
- package/dist/bootstrap/generated.d.ts.map +1 -0
- package/dist/browser.d.ts +12 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/bus.d.ts +29 -0
- package/dist/bus.d.ts.map +1 -0
- package/dist/cli/build-info.d.ts +4 -0
- package/dist/cli/build-info.d.ts.map +1 -0
- package/dist/cli/cli.d.ts +3 -12
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/client.d.ts +60 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/clients/civitai/index.d.ts +472 -0
- package/dist/clients/civitai/index.d.ts.map +1 -0
- package/dist/clients/client-template.d.ts +30 -0
- package/dist/clients/client-template.d.ts.map +1 -0
- package/dist/clients/comfyui/index.d.ts +281 -0
- package/dist/clients/comfyui/index.d.ts.map +1 -0
- package/dist/clients/elevenlabs/index.d.ts +197 -0
- package/dist/clients/elevenlabs/index.d.ts.map +1 -0
- package/dist/clients/graph.d.ts +64 -0
- package/dist/clients/graph.d.ts.map +1 -0
- package/dist/clients/openai/index.d.ts +247 -0
- package/dist/clients/openai/index.d.ts.map +1 -0
- package/dist/clients/rest.d.ts +92 -0
- package/dist/clients/rest.d.ts.map +1 -0
- package/dist/clients/supabase/index.d.ts +176 -0
- package/dist/clients/supabase/index.d.ts.map +1 -0
- package/dist/clients/websocket.d.ts +127 -0
- package/dist/clients/websocket.d.ts.map +1 -0
- package/dist/command.d.ts +163 -0
- package/dist/command.d.ts.map +1 -0
- package/dist/commands/bootstrap.d.ts +20 -0
- package/dist/commands/bootstrap.d.ts.map +1 -0
- package/dist/commands/chat.d.ts +37 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/code.d.ts +28 -0
- package/dist/commands/code.d.ts.map +1 -0
- package/dist/commands/console.d.ts +22 -0
- package/dist/commands/console.d.ts.map +1 -0
- package/dist/commands/describe.d.ts +50 -0
- package/dist/commands/describe.d.ts.map +1 -0
- package/dist/commands/eval.d.ts +23 -0
- package/dist/commands/eval.d.ts.map +1 -0
- package/dist/commands/help.d.ts +25 -0
- package/dist/commands/help.d.ts.map +1 -0
- package/dist/commands/index.d.ts +18 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/introspect.d.ts +24 -0
- package/dist/commands/introspect.d.ts.map +1 -0
- package/dist/commands/mcp.d.ts +35 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/prompt.d.ts +38 -0
- package/dist/commands/prompt.d.ts.map +1 -0
- package/dist/commands/run.d.ts +24 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/sandbox-mcp.d.ts +34 -0
- package/dist/commands/sandbox-mcp.d.ts.map +1 -0
- package/dist/commands/save-api-docs.d.ts +21 -0
- package/dist/commands/save-api-docs.d.ts.map +1 -0
- package/dist/commands/scaffold.d.ts +24 -0
- package/dist/commands/scaffold.d.ts.map +1 -0
- package/dist/commands/select.d.ts +22 -0
- package/dist/commands/select.d.ts.map +1 -0
- package/dist/commands/serve.d.ts +29 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/container-describer.d.ts +144 -0
- package/dist/container-describer.d.ts.map +1 -0
- package/dist/container.d.ts +451 -0
- package/dist/container.d.ts.map +1 -0
- package/dist/endpoint.d.ts +113 -0
- package/dist/endpoint.d.ts.map +1 -0
- package/dist/feature.d.ts +47 -0
- package/dist/feature.d.ts.map +1 -0
- package/dist/graft.d.ts +29 -0
- package/dist/graft.d.ts.map +1 -0
- package/dist/hash-object.d.ts +8 -0
- package/dist/hash-object.d.ts.map +1 -0
- package/dist/helper.d.ts +209 -0
- package/dist/helper.d.ts.map +1 -0
- package/dist/introspection/generated.node.d.ts +44623 -0
- package/dist/introspection/generated.node.d.ts.map +1 -0
- package/dist/introspection/generated.web.d.ts +1412 -0
- package/dist/introspection/generated.web.d.ts.map +1 -0
- package/dist/introspection/index.d.ts +156 -0
- package/dist/introspection/index.d.ts.map +1 -0
- package/dist/introspection/scan.d.ts +147 -0
- package/dist/introspection/scan.d.ts.map +1 -0
- package/dist/node/container.d.ts +256 -0
- package/dist/node/container.d.ts.map +1 -0
- package/dist/node/feature.d.ts +9 -0
- package/dist/node/feature.d.ts.map +1 -0
- package/dist/node/features/container-link.d.ts +213 -0
- package/dist/node/features/container-link.d.ts.map +1 -0
- package/dist/node/features/content-db.d.ts +354 -0
- package/dist/node/features/content-db.d.ts.map +1 -0
- package/dist/node/features/disk-cache.d.ts +236 -0
- package/dist/node/features/disk-cache.d.ts.map +1 -0
- package/dist/node/features/dns.d.ts +511 -0
- package/dist/node/features/dns.d.ts.map +1 -0
- package/dist/node/features/docker.d.ts +485 -0
- package/dist/node/features/docker.d.ts.map +1 -0
- package/dist/node/features/downloader.d.ts +73 -0
- package/dist/node/features/downloader.d.ts.map +1 -0
- package/dist/node/features/figlet-fonts.d.ts +4 -0
- package/dist/node/features/figlet-fonts.d.ts.map +1 -0
- package/dist/node/features/file-manager.d.ts +177 -0
- package/dist/node/features/file-manager.d.ts.map +1 -0
- package/dist/node/features/fs.d.ts +635 -0
- package/dist/node/features/fs.d.ts.map +1 -0
- package/dist/node/features/git.d.ts +329 -0
- package/dist/node/features/git.d.ts.map +1 -0
- package/dist/node/features/google-auth.d.ts +200 -0
- package/dist/node/features/google-auth.d.ts.map +1 -0
- package/dist/node/features/google-calendar.d.ts +194 -0
- package/dist/node/features/google-calendar.d.ts.map +1 -0
- package/dist/node/features/google-docs.d.ts +138 -0
- package/dist/node/features/google-docs.d.ts.map +1 -0
- package/dist/node/features/google-drive.d.ts +202 -0
- package/dist/node/features/google-drive.d.ts.map +1 -0
- package/dist/node/features/google-mail.d.ts +221 -0
- package/dist/node/features/google-mail.d.ts.map +1 -0
- package/dist/node/features/google-sheets.d.ts +157 -0
- package/dist/node/features/google-sheets.d.ts.map +1 -0
- package/dist/node/features/grep.d.ts +207 -0
- package/dist/node/features/grep.d.ts.map +1 -0
- package/dist/node/features/helpers.d.ts +236 -0
- package/dist/node/features/helpers.d.ts.map +1 -0
- package/dist/node/features/ink.d.ts +332 -0
- package/dist/node/features/ink.d.ts.map +1 -0
- package/dist/node/features/ipc-socket.d.ts +298 -0
- package/dist/node/features/ipc-socket.d.ts.map +1 -0
- package/dist/node/features/json-tree.d.ts +140 -0
- package/dist/node/features/json-tree.d.ts.map +1 -0
- package/dist/node/features/networking.d.ts +373 -0
- package/dist/node/features/networking.d.ts.map +1 -0
- package/dist/node/features/nlp.d.ts +125 -0
- package/dist/node/features/nlp.d.ts.map +1 -0
- package/dist/node/features/opener.d.ts +93 -0
- package/dist/node/features/opener.d.ts.map +1 -0
- package/dist/node/features/os.d.ts +168 -0
- package/dist/node/features/os.d.ts.map +1 -0
- package/dist/node/features/package-finder.d.ts +419 -0
- package/dist/node/features/package-finder.d.ts.map +1 -0
- package/dist/node/features/postgres.d.ts +173 -0
- package/dist/node/features/postgres.d.ts.map +1 -0
- package/dist/node/features/proc.d.ts +285 -0
- package/dist/node/features/proc.d.ts.map +1 -0
- package/dist/node/features/process-manager.d.ts +427 -0
- package/dist/node/features/process-manager.d.ts.map +1 -0
- package/dist/node/features/python.d.ts +477 -0
- package/dist/node/features/python.d.ts.map +1 -0
- package/dist/node/features/redis.d.ts +247 -0
- package/dist/node/features/redis.d.ts.map +1 -0
- package/dist/node/features/repl.d.ts +84 -0
- package/dist/node/features/repl.d.ts.map +1 -0
- package/dist/node/features/runpod.d.ts +527 -0
- package/dist/node/features/runpod.d.ts.map +1 -0
- package/dist/node/features/secure-shell.d.ts +145 -0
- package/dist/node/features/secure-shell.d.ts.map +1 -0
- package/dist/node/features/semantic-search.d.ts +207 -0
- package/dist/node/features/semantic-search.d.ts.map +1 -0
- package/dist/node/features/sqlite.d.ts +180 -0
- package/dist/node/features/sqlite.d.ts.map +1 -0
- package/dist/node/features/telegram.d.ts +173 -0
- package/dist/node/features/telegram.d.ts.map +1 -0
- package/dist/node/features/transpiler.d.ts +51 -0
- package/dist/node/features/transpiler.d.ts.map +1 -0
- package/dist/node/features/tts.d.ts +108 -0
- package/dist/node/features/tts.d.ts.map +1 -0
- package/dist/node/features/ui.d.ts +562 -0
- package/dist/node/features/ui.d.ts.map +1 -0
- package/dist/node/features/vault.d.ts +90 -0
- package/dist/node/features/vault.d.ts.map +1 -0
- package/dist/node/features/vm.d.ts +285 -0
- package/dist/node/features/vm.d.ts.map +1 -0
- package/dist/node/features/yaml-tree.d.ts +118 -0
- package/dist/node/features/yaml-tree.d.ts.map +1 -0
- package/dist/node/features/yaml.d.ts +127 -0
- package/dist/node/features/yaml.d.ts.map +1 -0
- package/dist/node.d.ts +67 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/python/generated.d.ts +2 -0
- package/dist/python/generated.d.ts.map +1 -0
- package/dist/react/index.d.ts +36 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/registry.d.ts +97 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/scaffolds/generated.d.ts +13 -0
- package/dist/scaffolds/generated.d.ts.map +1 -0
- package/dist/scaffolds/template.d.ts +11 -0
- package/dist/scaffolds/template.d.ts.map +1 -0
- package/dist/schemas/base.d.ts +254 -0
- package/dist/schemas/base.d.ts.map +1 -0
- package/dist/selector.d.ts +130 -0
- package/dist/selector.d.ts.map +1 -0
- package/dist/server.d.ts +89 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/servers/express.d.ts +104 -0
- package/dist/servers/express.d.ts.map +1 -0
- package/dist/servers/mcp.d.ts +201 -0
- package/dist/servers/mcp.d.ts.map +1 -0
- package/dist/servers/socket.d.ts +121 -0
- package/dist/servers/socket.d.ts.map +1 -0
- package/dist/state.d.ts +24 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/web/clients/socket.d.ts +37 -0
- package/dist/web/clients/socket.d.ts.map +1 -0
- package/dist/web/container.d.ts +55 -0
- package/dist/web/container.d.ts.map +1 -0
- package/dist/web/extension.d.ts +4 -0
- package/dist/web/extension.d.ts.map +1 -0
- package/dist/web/feature.d.ts +8 -0
- package/dist/web/feature.d.ts.map +1 -0
- package/dist/web/features/asset-loader.d.ts +35 -0
- package/dist/web/features/asset-loader.d.ts.map +1 -0
- package/dist/web/features/container-link.d.ts +167 -0
- package/dist/web/features/container-link.d.ts.map +1 -0
- package/dist/web/features/esbuild.d.ts +51 -0
- package/dist/web/features/esbuild.d.ts.map +1 -0
- package/dist/web/features/helpers.d.ts +140 -0
- package/dist/web/features/helpers.d.ts.map +1 -0
- package/dist/web/features/network.d.ts +69 -0
- package/dist/web/features/network.d.ts.map +1 -0
- package/dist/web/features/speech.d.ts +71 -0
- package/dist/web/features/speech.d.ts.map +1 -0
- package/dist/web/features/vault.d.ts +62 -0
- package/dist/web/features/vault.d.ts.map +1 -0
- package/dist/web/features/vm.d.ts +48 -0
- package/dist/web/features/vm.d.ts.map +1 -0
- package/dist/web/features/voice-recognition.d.ts +96 -0
- package/dist/web/features/voice-recognition.d.ts.map +1 -0
- package/dist/web/shims/isomorphic-vm.d.ts +22 -0
- package/dist/web/shims/isomorphic-vm.d.ts.map +1 -0
- package/docs/CLI.md +335 -0
- package/docs/CNAME +1 -0
- package/docs/README.md +60 -0
- package/docs/TABLE-OF-CONTENTS.md +183 -0
- package/docs/apis/clients/elevenlabs.md +308 -0
- package/docs/apis/clients/graph.md +107 -0
- package/docs/apis/clients/openai.md +429 -0
- package/docs/apis/clients/rest.md +161 -0
- package/docs/apis/clients/websocket.md +174 -0
- package/docs/apis/features/agi/assistant.md +625 -0
- package/docs/apis/features/agi/assistants-manager.md +282 -0
- package/docs/apis/features/agi/auto-assistant.md +279 -0
- package/docs/apis/features/agi/browser-use.md +802 -0
- package/docs/apis/features/agi/claude-code.md +884 -0
- package/docs/apis/features/agi/conversation-history.md +364 -0
- package/docs/apis/features/agi/conversation.md +548 -0
- package/docs/apis/features/agi/docs-reader.md +99 -0
- package/docs/apis/features/agi/file-tools.md +163 -0
- package/docs/apis/features/agi/luca-coder.md +407 -0
- package/docs/apis/features/agi/openai-codex.md +396 -0
- package/docs/apis/features/agi/openapi.md +138 -0
- package/docs/apis/features/agi/semantic-search.md +387 -0
- package/docs/apis/features/agi/skills-library.md +239 -0
- package/docs/apis/features/node/container-link.md +192 -0
- package/docs/apis/features/node/content-db.md +450 -0
- package/docs/apis/features/node/disk-cache.md +379 -0
- package/docs/apis/features/node/dns.md +652 -0
- package/docs/apis/features/node/docker.md +706 -0
- package/docs/apis/features/node/downloader.md +81 -0
- package/docs/apis/features/node/esbuild.md +60 -0
- package/docs/apis/features/node/file-manager.md +191 -0
- package/docs/apis/features/node/fs.md +1217 -0
- package/docs/apis/features/node/git.md +371 -0
- package/docs/apis/features/node/google-auth.md +193 -0
- package/docs/apis/features/node/google-calendar.md +202 -0
- package/docs/apis/features/node/google-docs.md +173 -0
- package/docs/apis/features/node/google-drive.md +246 -0
- package/docs/apis/features/node/google-mail.md +214 -0
- package/docs/apis/features/node/google-sheets.md +194 -0
- package/docs/apis/features/node/grep.md +292 -0
- package/docs/apis/features/node/helpers.md +164 -0
- package/docs/apis/features/node/ink.md +334 -0
- package/docs/apis/features/node/ipc-socket.md +249 -0
- package/docs/apis/features/node/json-tree.md +86 -0
- package/docs/apis/features/node/networking.md +316 -0
- package/docs/apis/features/node/nlp.md +133 -0
- package/docs/apis/features/node/opener.md +97 -0
- package/docs/apis/features/node/os.md +146 -0
- package/docs/apis/features/node/package-finder.md +392 -0
- package/docs/apis/features/node/postgres.md +234 -0
- package/docs/apis/features/node/proc.md +399 -0
- package/docs/apis/features/node/process-manager.md +305 -0
- package/docs/apis/features/node/python.md +604 -0
- package/docs/apis/features/node/redis.md +380 -0
- package/docs/apis/features/node/repl.md +88 -0
- package/docs/apis/features/node/runpod.md +674 -0
- package/docs/apis/features/node/secure-shell.md +176 -0
- package/docs/apis/features/node/semantic-search.md +408 -0
- package/docs/apis/features/node/sqlite.md +233 -0
- package/docs/apis/features/node/telegram.md +279 -0
- package/docs/apis/features/node/transpiler.md +74 -0
- package/docs/apis/features/node/tts.md +133 -0
- package/docs/apis/features/node/ui.md +701 -0
- package/docs/apis/features/node/vault.md +59 -0
- package/docs/apis/features/node/vm.md +75 -0
- package/docs/apis/features/node/yaml-tree.md +85 -0
- package/docs/apis/features/node/yaml.md +176 -0
- package/docs/apis/features/web/asset-loader.md +59 -0
- package/docs/apis/features/web/container-link.md +192 -0
- package/docs/apis/features/web/esbuild.md +54 -0
- package/docs/apis/features/web/helpers.md +164 -0
- package/docs/apis/features/web/network.md +44 -0
- package/docs/apis/features/web/speech.md +69 -0
- package/docs/apis/features/web/vault.md +59 -0
- package/docs/apis/features/web/vm.md +75 -0
- package/docs/apis/features/web/voice.md +84 -0
- package/docs/apis/servers/express.md +171 -0
- package/docs/apis/servers/mcp.md +238 -0
- package/docs/apis/servers/websocket.md +170 -0
- package/docs/bootstrap/CLAUDE.md +101 -0
- package/docs/bootstrap/SKILL.md +341 -0
- package/docs/bootstrap/templates/about-command.ts +41 -0
- package/docs/bootstrap/templates/docs-models.ts +22 -0
- package/docs/bootstrap/templates/docs-readme.md +43 -0
- package/docs/bootstrap/templates/example-feature.ts +53 -0
- package/docs/bootstrap/templates/health-endpoint.ts +15 -0
- package/docs/bootstrap/templates/luca-cli.ts +30 -0
- package/docs/bootstrap/templates/runme.md +54 -0
- package/docs/challenges/caching-proxy.md +16 -0
- package/docs/challenges/content-db-round-trip.md +14 -0
- package/docs/challenges/custom-command.md +9 -0
- package/docs/challenges/file-watcher-pipeline.md +11 -0
- package/docs/challenges/grep-audit-report.md +15 -0
- package/docs/challenges/multi-feature-dashboard.md +14 -0
- package/docs/challenges/process-orchestrator.md +17 -0
- package/docs/challenges/rest-api-server-with-client.md +12 -0
- package/docs/challenges/script-runner-with-vm.md +11 -0
- package/docs/challenges/simple-rest-api.md +15 -0
- package/docs/challenges/websocket-serve-and-client.md +11 -0
- package/docs/challenges/yaml-config-system.md +14 -0
- package/docs/command-system-overhaul.md +94 -0
- package/docs/documentation-audit.md +134 -0
- package/docs/examples/assistant/CORE.md +18 -0
- package/docs/examples/assistant/hooks.ts +3 -0
- package/docs/examples/assistant/tools.ts +10 -0
- package/docs/examples/assistant-hooks-reference.ts +171 -0
- package/docs/examples/assistant-with-process-manager.md +84 -0
- package/docs/examples/content-db.md +77 -0
- package/docs/examples/disk-cache.md +83 -0
- package/docs/examples/docker.md +101 -0
- package/docs/examples/downloader.md +70 -0
- package/docs/examples/entity.md +124 -0
- package/docs/examples/esbuild.md +80 -0
- package/docs/examples/feature-as-tool-provider.md +143 -0
- package/docs/examples/file-manager.md +82 -0
- package/docs/examples/fs.md +83 -0
- package/docs/examples/git.md +85 -0
- package/docs/examples/google-auth.md +88 -0
- package/docs/examples/google-calendar.md +94 -0
- package/docs/examples/google-docs.md +82 -0
- package/docs/examples/google-drive.md +96 -0
- package/docs/examples/google-sheets.md +95 -0
- package/docs/examples/grep.md +85 -0
- package/docs/examples/ink-blocks.md +75 -0
- package/docs/examples/ink-renderer.md +41 -0
- package/docs/examples/ink.md +103 -0
- package/docs/examples/ipc-socket.md +103 -0
- package/docs/examples/json-tree.md +91 -0
- package/docs/examples/networking.md +58 -0
- package/docs/examples/nlp.md +91 -0
- package/docs/examples/opener.md +78 -0
- package/docs/examples/os.md +72 -0
- package/docs/examples/package-finder.md +89 -0
- package/docs/examples/postgres.md +91 -0
- package/docs/examples/proc.md +81 -0
- package/docs/examples/process-manager.md +79 -0
- package/docs/examples/python.md +132 -0
- package/docs/examples/repl.md +93 -0
- package/docs/examples/runpod.md +119 -0
- package/docs/examples/secure-shell.md +92 -0
- package/docs/examples/sqlite.md +86 -0
- package/docs/examples/structured-output-with-assistants.md +144 -0
- package/docs/examples/telegram.md +77 -0
- package/docs/examples/tts.md +86 -0
- package/docs/examples/ui.md +80 -0
- package/docs/examples/vault.md +70 -0
- package/docs/examples/vm.md +86 -0
- package/docs/examples/websocket-ask-and-reply-example.md +128 -0
- package/docs/examples/yaml-tree.md +93 -0
- package/docs/examples/yaml.md +104 -0
- package/docs/ideas/assistant-factory-pattern.md +142 -0
- package/docs/in-memory-fs.md +4 -0
- package/docs/introspection-audit.md +49 -0
- package/docs/introspection.md +164 -0
- package/docs/mcp/readme.md +162 -0
- package/docs/models.ts +41 -0
- package/docs/philosophy.md +86 -0
- package/docs/principles.md +7 -0
- package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
- package/docs/prompts/check-for-undocumented-features.md +27 -0
- package/docs/prompts/mcp-test-easy-command.md +27 -0
- package/docs/scaffolds/client.md +149 -0
- package/docs/scaffolds/command.md +120 -0
- package/docs/scaffolds/endpoint.md +171 -0
- package/docs/scaffolds/feature.md +158 -0
- package/docs/scaffolds/selector.md +91 -0
- package/docs/scaffolds/server.md +196 -0
- package/docs/selectors.md +115 -0
- package/docs/sessions/custom-command/attempt-log-2.md +195 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +728 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +555 -0
- package/docs/sessions/grep-audit-report/attempt-log-1.md +289 -0
- package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +679 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +1 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +920 -0
- package/docs/sessions/simple-rest-api/attempt-log-1.md +593 -0
- package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +995 -0
- package/docs/tutorials/00-bootstrap.md +166 -0
- package/docs/tutorials/01-getting-started.md +106 -0
- package/docs/tutorials/02-container.md +210 -0
- package/docs/tutorials/03-scripts.md +194 -0
- package/docs/tutorials/04-features-overview.md +196 -0
- package/docs/tutorials/05-state-and-events.md +171 -0
- package/docs/tutorials/06-servers.md +157 -0
- package/docs/tutorials/07-endpoints.md +198 -0
- package/docs/tutorials/08-commands.md +252 -0
- package/docs/tutorials/09-clients.md +162 -0
- package/docs/tutorials/10-creating-features.md +203 -0
- package/docs/tutorials/11-contentbase.md +191 -0
- package/docs/tutorials/12-assistants.md +215 -0
- package/docs/tutorials/13-introspection.md +157 -0
- package/docs/tutorials/14-type-system.md +174 -0
- package/docs/tutorials/15-project-patterns.md +222 -0
- package/docs/tutorials/16-google-features.md +534 -0
- package/docs/tutorials/17-tui-blocks.md +530 -0
- package/docs/tutorials/18-semantic-search.md +334 -0
- package/docs/tutorials/19-python-sessions.md +401 -0
- package/docs/tutorials/20-browser-esm.md +234 -0
- package/index.html +1430 -0
- package/index.ts +1 -0
- package/install.sh +84 -0
- package/luca.cli.ts +16 -0
- package/luca.console.ts +9 -0
- package/main.py +6 -0
- package/package.json +219 -58
- package/public/index.html +1430 -0
- package/public/slides-ai-native.html +902 -0
- package/public/slides-intro.html +974 -0
- package/pyproject.toml +7 -0
- package/scripts/build-web.ts +28 -0
- package/scripts/examples/ask-luca-expert.ts +42 -0
- package/scripts/examples/assistant-questions.ts +12 -0
- package/scripts/examples/excalidraw-expert.ts +75 -0
- package/scripts/examples/expert-chat.ts +0 -0
- package/scripts/examples/file-manager.ts +14 -0
- package/scripts/examples/ideas.ts +12 -0
- package/scripts/examples/interactive-chat.ts +20 -0
- package/scripts/examples/openai-tool-calls.ts +113 -0
- package/scripts/examples/opening-a-web-browser.ts +5 -0
- package/scripts/examples/telegram-bot.ts +79 -0
- package/scripts/examples/using-assistant-with-mcp.ts +555 -0
- package/scripts/examples/using-claude-code.ts +10 -0
- package/scripts/examples/using-contentdb.ts +35 -0
- package/scripts/examples/using-conversations.ts +35 -0
- package/scripts/examples/using-disk-cache.ts +10 -0
- package/scripts/examples/using-docker-shell.ts +75 -0
- package/scripts/examples/using-elevenlabs.ts +25 -0
- package/scripts/examples/using-google-calendar.ts +57 -0
- package/scripts/examples/using-google-docs.ts +74 -0
- package/scripts/examples/using-google-drive.ts +74 -0
- package/scripts/examples/using-google-sheets.ts +89 -0
- package/scripts/examples/using-nlp.ts +55 -0
- package/scripts/examples/using-ollama.ts +11 -0
- package/scripts/examples/using-postgres.ts +55 -0
- package/scripts/examples/using-runpod.ts +32 -0
- package/scripts/examples/using-tts.ts +40 -0
- package/scripts/scaffold.ts +391 -0
- package/scripts/scratch.ts +15 -0
- package/scripts/stamp-build.sh +12 -0
- package/scripts/test-assistant-hooks.ts +13 -0
- package/scripts/test-docs-reader.ts +10 -0
- package/scripts/test-linux-binary.sh +80 -0
- package/scripts/update-introspection-data.ts +58 -0
- package/src/agi/README.md +14 -0
- package/src/agi/container.server.ts +152 -0
- package/src/agi/endpoints/ask.ts +60 -0
- package/src/agi/endpoints/conversations/[id].ts +45 -0
- package/src/agi/endpoints/conversations.ts +31 -0
- package/src/agi/endpoints/experts.ts +37 -0
- package/src/agi/feature.ts +13 -0
- package/src/agi/features/agent-memory.ts +694 -0
- package/src/agi/features/assistant.ts +1624 -0
- package/src/agi/features/assistants-manager.ts +418 -0
- package/src/agi/features/autonomous-assistant.ts +431 -0
- package/src/agi/features/browser-use.ts +653 -0
- package/src/agi/features/claude-code.ts +1538 -0
- package/src/agi/features/coding-tools.ts +175 -0
- package/src/agi/features/conversation-history.ts +495 -0
- package/src/agi/features/conversation.ts +1323 -0
- package/src/agi/features/docs-reader.ts +167 -0
- package/src/agi/features/file-tools.ts +293 -0
- package/src/agi/features/luca-coder.ts +639 -0
- package/src/agi/features/openai-codex.ts +651 -0
- package/src/agi/features/openapi.ts +445 -0
- package/src/agi/features/skills-library.ts +478 -0
- package/src/agi/index.ts +6 -0
- package/src/agi/lib/interceptor-chain.ts +89 -0
- package/src/agi/lib/token-counter.ts +122 -0
- package/src/bootstrap/generated.ts +9792 -0
- package/src/browser.ts +25 -0
- package/src/bus.ts +122 -0
- package/src/cli/build-info.ts +4 -0
- package/src/cli/cli.ts +355 -0
- package/src/client.ts +170 -0
- package/src/clients/civitai/index.ts +537 -0
- package/src/clients/client-template.ts +41 -0
- package/src/clients/comfyui/index.ts +604 -0
- package/src/clients/elevenlabs/index.ts +317 -0
- package/src/clients/graph.ts +87 -0
- package/src/clients/openai/index.ts +456 -0
- package/src/clients/rest.ts +207 -0
- package/src/clients/supabase/index.ts +357 -0
- package/src/clients/voicebox/index.ts +300 -0
- package/src/clients/websocket.ts +251 -0
- package/src/command.ts +505 -0
- package/src/commands/bootstrap.ts +244 -0
- package/src/commands/chat.ts +308 -0
- package/src/commands/code.ts +371 -0
- package/src/commands/console.ts +189 -0
- package/src/commands/describe.ts +243 -0
- package/src/commands/eval.ts +121 -0
- package/src/commands/help.ts +240 -0
- package/src/commands/index.ts +19 -0
- package/src/commands/introspect.ts +218 -0
- package/src/commands/mcp.ts +64 -0
- package/src/commands/prompt.ts +982 -0
- package/src/commands/run.ts +278 -0
- package/src/commands/sandbox-mcp.ts +343 -0
- package/src/commands/save-api-docs.ts +51 -0
- package/src/commands/scaffold.ts +225 -0
- package/src/commands/select.ts +99 -0
- package/src/commands/serve.ts +208 -0
- package/src/container-describer.ts +1084 -0
- package/src/container.ts +1186 -0
- package/src/endpoint.ts +365 -0
- package/src/entity.ts +173 -0
- package/src/feature.ts +118 -0
- package/src/graft.ts +181 -0
- package/src/hash-object.ts +97 -0
- package/src/helper.ts +849 -0
- package/src/introspection/generated.agi.ts +40208 -0
- package/src/introspection/generated.node.ts +28686 -0
- package/src/introspection/generated.web.ts +2251 -0
- package/src/introspection/index.ts +296 -0
- package/src/introspection/scan.ts +1131 -0
- package/src/node/container.ts +409 -0
- package/src/node/feature.ts +13 -0
- package/src/node/features/container-link.ts +559 -0
- package/src/node/features/content-db.ts +812 -0
- package/src/node/features/disk-cache.ts +388 -0
- package/src/node/features/dns.ts +669 -0
- package/src/node/features/docker.ts +921 -0
- package/src/node/features/downloader.ts +79 -0
- package/src/node/features/figlet-fonts.ts +600 -0
- package/src/node/features/file-manager.ts +535 -0
- package/src/node/features/fs.ts +1050 -0
- package/src/node/features/git.ts +592 -0
- package/src/node/features/google-auth.ts +504 -0
- package/src/node/features/google-calendar.ts +306 -0
- package/src/node/features/google-docs.ts +412 -0
- package/src/node/features/google-drive.ts +346 -0
- package/src/node/features/google-mail.ts +540 -0
- package/src/node/features/google-sheets.ts +286 -0
- package/src/node/features/grep.ts +427 -0
- package/src/node/features/helpers.ts +735 -0
- package/src/node/features/ink.ts +490 -0
- package/src/node/features/ipc-socket.ts +649 -0
- package/src/node/features/json-tree.ts +170 -0
- package/src/node/features/networking.ts +961 -0
- package/src/node/features/nlp.ts +212 -0
- package/src/node/features/opener.ts +180 -0
- package/src/node/features/os.ts +403 -0
- package/src/node/features/package-finder.ts +540 -0
- package/src/node/features/postgres.ts +289 -0
- package/src/node/features/proc.ts +503 -0
- package/src/node/features/process-manager.ts +844 -0
- package/src/node/features/python.ts +906 -0
- package/src/node/features/redis.ts +446 -0
- package/src/node/features/repl.ts +212 -0
- package/src/node/features/runpod.ts +811 -0
- package/src/node/features/secure-shell.ts +267 -0
- package/src/node/features/semantic-search.ts +935 -0
- package/src/node/features/sqlite.ts +289 -0
- package/src/node/features/telegram.ts +343 -0
- package/src/node/features/transpiler.ts +161 -0
- package/src/node/features/tts.ts +185 -0
- package/src/node/features/ui.ts +786 -0
- package/src/node/features/vault.ts +153 -0
- package/src/node/features/vm.ts +462 -0
- package/src/node/features/yaml-tree.ts +148 -0
- package/src/node/features/yaml.ts +133 -0
- package/src/node.ts +76 -0
- package/src/python/bridge.py +220 -0
- package/src/python/generated.ts +227 -0
- package/src/react/index.ts +175 -0
- package/src/registry.ts +210 -0
- package/src/scaffolds/generated.ts +1815 -0
- package/src/scaffolds/template.ts +46 -0
- package/src/schemas/base.ts +296 -0
- package/src/selector.ts +352 -0
- package/src/server.ts +229 -0
- package/src/servers/express.ts +283 -0
- package/src/servers/mcp.ts +802 -0
- package/src/servers/socket.ts +258 -0
- package/src/state.ts +101 -0
- package/src/web/clients/socket.ts +99 -0
- package/src/web/container.ts +75 -0
- package/src/web/extension.ts +30 -0
- package/src/web/feature.ts +12 -0
- package/src/web/features/asset-loader.ts +72 -0
- package/src/web/features/container-link.ts +382 -0
- package/src/web/features/esbuild.ts +93 -0
- package/src/web/features/helpers.ts +269 -0
- package/src/web/features/network.ts +85 -0
- package/src/web/features/speech.ts +104 -0
- package/src/web/features/vault.ts +207 -0
- package/src/web/features/vm.ts +85 -0
- package/src/web/features/voice-recognition.ts +161 -0
- package/src/web/shims/isomorphic-vm.ts +149 -0
- package/test/assistant-hooks.test.ts +306 -0
- package/test/assistant.test.ts +81 -0
- package/test/bus.test.ts +134 -0
- package/test/clients-servers.test.ts +217 -0
- package/test/command.test.ts +267 -0
- package/test/container-link.test.ts +274 -0
- package/test/conversation.test.ts +220 -0
- package/test/features.test.ts +160 -0
- package/test/fork-and-research.test.ts +450 -0
- package/test/integration.test.ts +787 -0
- package/test/interceptor-chain.test.ts +61 -0
- package/test/node-container.test.ts +121 -0
- package/test/python-session.test.ts +105 -0
- package/test/rate-limit.test.ts +272 -0
- package/test/semantic-search.test.ts +550 -0
- package/test/state.test.ts +121 -0
- package/test/vm-context.test.ts +146 -0
- package/test/vm-loadmodule.test.ts +213 -0
- package/test/websocket-ask.test.ts +101 -0
- package/test-integration/assistant.test.ts +138 -0
- package/test-integration/assistants-manager.test.ts +113 -0
- package/test-integration/claude-code.test.ts +98 -0
- package/test-integration/conversation-history.test.ts +205 -0
- package/test-integration/conversation.test.ts +137 -0
- package/test-integration/elevenlabs.test.ts +55 -0
- package/test-integration/google-services.test.ts +80 -0
- package/test-integration/helpers.ts +89 -0
- package/test-integration/memory.test.ts +204 -0
- package/test-integration/openai-codex.test.ts +93 -0
- package/test-integration/runpod.test.ts +58 -0
- package/test-integration/server-endpoints.test.ts +97 -0
- package/test-integration/telegram.test.ts +46 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +58 -0
- package/uv.lock +8 -0
- package/LICENSE +0 -21
- package/dist/cli/cli.js +0 -48
- package/dist/cli/common.d.ts +0 -2
- package/dist/cli/common.js +0 -6
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.js +0 -5
- package/dist/cli/run.d.ts +0 -1
- package/dist/cli/run.js +0 -38
- package/dist/core/index.d.ts +0 -4
- package/dist/core/index.js +0 -32
- package/dist/core/read.d.ts +0 -2
- package/dist/core/read.js +0 -29
- package/dist/core/request.d.ts +0 -1
- package/dist/core/request.js +0 -2
- package/dist/core/write.d.ts +0 -2
- package/dist/core/write.js +0 -21
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -5
- package/dist/utils/common.d.ts +0 -9
- package/dist/utils/common.js +0 -57
- package/dist/utils/consts.d.ts +0 -3
- package/dist/utils/consts.js +0 -11
- package/dist/utils/dict.d.ts +0 -1
- package/dist/utils/dict.js +0 -7
- package/dist/utils/index.d.ts +0 -5
- package/dist/utils/index.js +0 -21
- package/dist/utils/log.d.ts +0 -1
- package/dist/utils/log.js +0 -5
- package/dist/utils/types.d.ts +0 -1
- package/dist/utils/types.js +0 -2
- package/dist/utils/utils.test.d.ts +0 -1
- package/dist/utils/utils.test.js +0 -7
|
@@ -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
|