bluera-knowledge 0.9.21
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/.claude/commands/commit.md +37 -0
- package/.claude/hooks/post-edit-check.sh +41 -0
- package/.claude/settings.local.json.example +40 -0
- package/.claude/skills/atomic-commits/SKILL.md +53 -0
- package/.claude-plugin/plugin.json +13 -0
- package/.editorconfig +15 -0
- package/.github/workflows/auto-release.yml +59 -0
- package/.github/workflows/ci.yml +142 -0
- package/.github/workflows/release.yml +66 -0
- package/.github/workflows/update-marketplace.yml +96 -0
- package/.husky/pre-commit +47 -0
- package/.husky/pre-push +29 -0
- package/.versionrc.json +28 -0
- package/CHANGELOG.md +410 -0
- package/CLAUDE.md +109 -0
- package/LICENSE +21 -0
- package/NOTICE +47 -0
- package/README.md +1546 -0
- package/SECURITY.md +65 -0
- package/bun.lock +1758 -0
- package/commands/add-folder.md +48 -0
- package/commands/add-repo.md +50 -0
- package/commands/cancel.md +63 -0
- package/commands/check-status.md +78 -0
- package/commands/crawl.md +51 -0
- package/commands/index.md +48 -0
- package/commands/remove-store.md +52 -0
- package/commands/search.md +79 -0
- package/commands/search.sh +63 -0
- package/commands/stores.md +54 -0
- package/commands/suggest.md +82 -0
- package/dist/chunk-5QMHZUC4.js +3617 -0
- package/dist/chunk-5QMHZUC4.js.map +1 -0
- package/dist/chunk-BICFAWMN.js +656 -0
- package/dist/chunk-BICFAWMN.js.map +1 -0
- package/dist/chunk-J7J6LXOJ.js +958 -0
- package/dist/chunk-J7J6LXOJ.js.map +1 -0
- package/dist/chunk-L2YVNC63.js +59 -0
- package/dist/chunk-L2YVNC63.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1429 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/server.d.ts +15 -0
- package/dist/mcp/server.js +11 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/watch.service-YAIKKDCF.js +7 -0
- package/dist/watch.service-YAIKKDCF.js.map +1 -0
- package/dist/workers/background-worker-cli.d.ts +1 -0
- package/dist/workers/background-worker-cli.js +310 -0
- package/dist/workers/background-worker-cli.js.map +1 -0
- package/docs/plans/2024-12-17-ai-search-quality-implementation.md +752 -0
- package/docs/plans/2024-12-17-ai-search-quality-testing-design.md +201 -0
- package/docs/plans/2025-12-16-bluera-knowledge-cli.md +2951 -0
- package/docs/plans/2025-12-16-phase2-features.md +1518 -0
- package/docs/plans/2025-12-17-hil-implementation.md +926 -0
- package/docs/plans/2025-12-17-hil-quality-testing.md +224 -0
- package/docs/plans/2025-12-17-search-quality-phase1-implementation.md +1416 -0
- package/docs/plans/2025-12-17-search-quality-testing-v2-design.md +212 -0
- package/docs/plans/2025-12-28-ai-agent-optimization.md +1630 -0
- package/eslint-rules/require-skip-comment.js +81 -0
- package/eslint.config.js +61 -0
- package/hooks/check-dependencies.sh +110 -0
- package/hooks/format-search-results.py +132 -0
- package/hooks/hooks.json +27 -0
- package/hooks/job-status-hook.sh +51 -0
- package/knip.json +43 -0
- package/mcp.plugin.json +12 -0
- package/package.json +103 -0
- package/python/crawl_worker.py +275 -0
- package/python/requirements.txt +2 -0
- package/scripts/readme-version-updater.cjs +18 -0
- package/skills/advanced-workflows/SKILL.md +273 -0
- package/skills/atomic-commits/SKILL.md +77 -0
- package/skills/knowledge-search/SKILL.md +54 -0
- package/skills/search-optimization/SKILL.md +396 -0
- package/skills/store-lifecycle/SKILL.md +470 -0
- package/skills/when-to-query/SKILL.md +66 -0
- package/src/analysis/ast-parser.test.ts +423 -0
- package/src/analysis/ast-parser.ts +192 -0
- package/src/analysis/code-graph.test.ts +698 -0
- package/src/analysis/code-graph.ts +245 -0
- package/src/analysis/dependency-usage-analyzer.test.ts +799 -0
- package/src/analysis/dependency-usage-analyzer.ts +405 -0
- package/src/analysis/go-ast-parser.test.ts +531 -0
- package/src/analysis/go-ast-parser.ts +478 -0
- package/src/analysis/parser-factory.test.ts +132 -0
- package/src/analysis/parser-factory.ts +44 -0
- package/src/analysis/python-ast-parser.test.ts +210 -0
- package/src/analysis/python-ast-parser.ts +34 -0
- package/src/analysis/repo-url-resolver.test.ts +533 -0
- package/src/analysis/repo-url-resolver.ts +233 -0
- package/src/analysis/rust-ast-parser.test.ts +568 -0
- package/src/analysis/rust-ast-parser.ts +477 -0
- package/src/analysis/tree-sitter-parser.test.ts +297 -0
- package/src/analysis/tree-sitter-parser.ts +223 -0
- package/src/cli/commands/crawl.test.ts +942 -0
- package/src/cli/commands/crawl.ts +141 -0
- package/src/cli/commands/index-cmd.test.ts +722 -0
- package/src/cli/commands/index-cmd.ts +105 -0
- package/src/cli/commands/mcp.test.ts +218 -0
- package/src/cli/commands/mcp.ts +18 -0
- package/src/cli/commands/plugin-api.test.ts +313 -0
- package/src/cli/commands/plugin-api.ts +45 -0
- package/src/cli/commands/search.test.ts +911 -0
- package/src/cli/commands/search.ts +113 -0
- package/src/cli/commands/serve.test.ts +329 -0
- package/src/cli/commands/serve.ts +28 -0
- package/src/cli/commands/setup.test.ts +820 -0
- package/src/cli/commands/setup.ts +153 -0
- package/src/cli/commands/store.test.ts +1003 -0
- package/src/cli/commands/store.ts +167 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/program.ts +59 -0
- package/src/crawl/article-converter.test.ts +604 -0
- package/src/crawl/article-converter.ts +98 -0
- package/src/crawl/bridge.test.ts +674 -0
- package/src/crawl/bridge.ts +236 -0
- package/src/crawl/claude-client.test.ts +663 -0
- package/src/crawl/claude-client.ts +234 -0
- package/src/crawl/intelligent-crawler.test.ts +931 -0
- package/src/crawl/intelligent-crawler.ts +428 -0
- package/src/crawl/markdown-utils.test.ts +703 -0
- package/src/crawl/markdown-utils.ts +228 -0
- package/src/crawl/schemas.ts +114 -0
- package/src/db/embeddings.test.ts +63 -0
- package/src/db/embeddings.ts +69 -0
- package/src/db/index.ts +2 -0
- package/src/db/lance.test.ts +390 -0
- package/src/db/lance.ts +164 -0
- package/src/defaults/repos.ts +67 -0
- package/src/index.ts +107 -0
- package/src/mcp/cache.test.ts +202 -0
- package/src/mcp/cache.ts +103 -0
- package/src/mcp/commands/index.ts +20 -0
- package/src/mcp/commands/job.commands.ts +54 -0
- package/src/mcp/commands/meta.commands.ts +54 -0
- package/src/mcp/commands/registry.ts +183 -0
- package/src/mcp/commands/store.commands.ts +75 -0
- package/src/mcp/handlers/execute.handler.test.ts +179 -0
- package/src/mcp/handlers/execute.handler.ts +24 -0
- package/src/mcp/handlers/index.ts +43 -0
- package/src/mcp/handlers/job.handler.test.ts +189 -0
- package/src/mcp/handlers/job.handler.ts +116 -0
- package/src/mcp/handlers/search.handler.test.ts +334 -0
- package/src/mcp/handlers/search.handler.ts +209 -0
- package/src/mcp/handlers/store.handler.test.ts +415 -0
- package/src/mcp/handlers/store.handler.ts +295 -0
- package/src/mcp/schemas/index.test.ts +315 -0
- package/src/mcp/schemas/index.ts +138 -0
- package/src/mcp/server.test.ts +36 -0
- package/src/mcp/server.ts +162 -0
- package/src/mcp/types.ts +41 -0
- package/src/plugin/commands.test.ts +789 -0
- package/src/plugin/commands.ts +257 -0
- package/src/plugin/dependency-analyzer.test.ts +380 -0
- package/src/plugin/dependency-analyzer.ts +147 -0
- package/src/plugin/git-clone.test.ts +332 -0
- package/src/plugin/git-clone.ts +57 -0
- package/src/server/app.test.ts +752 -0
- package/src/server/app.ts +119 -0
- package/src/server/index.test.ts +477 -0
- package/src/server/index.ts +1 -0
- package/src/services/chunking.service.test.ts +363 -0
- package/src/services/chunking.service.ts +350 -0
- package/src/services/code-graph.service.test.ts +304 -0
- package/src/services/code-graph.service.ts +302 -0
- package/src/services/code-unit.service.test.ts +596 -0
- package/src/services/code-unit.service.ts +115 -0
- package/src/services/config.service.test.ts +127 -0
- package/src/services/config.service.ts +69 -0
- package/src/services/index.service.test.ts +1002 -0
- package/src/services/index.service.ts +266 -0
- package/src/services/index.ts +75 -0
- package/src/services/job.service.test.ts +418 -0
- package/src/services/job.service.ts +246 -0
- package/src/services/project-root.service.test.ts +506 -0
- package/src/services/project-root.service.ts +112 -0
- package/src/services/search.service.test.ts +1105 -0
- package/src/services/search.service.ts +892 -0
- package/src/services/services.test.ts +38 -0
- package/src/services/snippet.service.test.ts +205 -0
- package/src/services/snippet.service.ts +166 -0
- package/src/services/store.service.test.ts +474 -0
- package/src/services/store.service.ts +225 -0
- package/src/services/watch.service.test.ts +553 -0
- package/src/services/watch.service.ts +71 -0
- package/src/types/brands.test.ts +45 -0
- package/src/types/brands.ts +32 -0
- package/src/types/config.ts +79 -0
- package/src/types/document.ts +30 -0
- package/src/types/index.ts +66 -0
- package/src/types/job.ts +46 -0
- package/src/types/progress.ts +9 -0
- package/src/types/result.test.ts +44 -0
- package/src/types/result.ts +41 -0
- package/src/types/search.ts +95 -0
- package/src/types/store.test.ts +69 -0
- package/src/types/store.ts +47 -0
- package/src/utils/type-guards.test.ts +346 -0
- package/src/utils/type-guards.ts +61 -0
- package/src/workers/background-worker-cli.ts +105 -0
- package/src/workers/background-worker.test.ts +178 -0
- package/src/workers/background-worker.ts +294 -0
- package/src/workers/spawn-worker.test.ts +128 -0
- package/src/workers/spawn-worker.ts +49 -0
- package/tests/analysis/ast-parser.test.ts +98 -0
- package/tests/analysis/code-graph.test.ts +60 -0
- package/tests/fixtures/README.md +114 -0
- package/tests/fixtures/code-snippets/api/error-handling.ts +267 -0
- package/tests/fixtures/code-snippets/api/rest-controller.ts +303 -0
- package/tests/fixtures/code-snippets/auth/jwt-auth.ts +213 -0
- package/tests/fixtures/code-snippets/auth/oauth-flow.ts +245 -0
- package/tests/fixtures/code-snippets/database/repository-pattern.ts +272 -0
- package/tests/fixtures/corpus/VERSION.md +25 -0
- package/tests/fixtures/corpus/articles/jwt-authentication.md +97 -0
- package/tests/fixtures/corpus/articles/react-hooks-patterns.md +127 -0
- package/tests/fixtures/corpus/articles/typescript-generics.md +111 -0
- package/tests/fixtures/corpus/documentation/express-middleware.md +71 -0
- package/tests/fixtures/corpus/documentation/express-routing.md +83 -0
- package/tests/fixtures/corpus/documentation/node-streams.md +78 -0
- package/tests/fixtures/corpus/oss-repos/express/History.md +3871 -0
- package/tests/fixtures/corpus/oss-repos/express/LICENSE +24 -0
- package/tests/fixtures/corpus/oss-repos/express/Readme.md +276 -0
- package/tests/fixtures/corpus/oss-repos/express/SECURITY.md +56 -0
- package/tests/fixtures/corpus/oss-repos/express/benchmarks/Makefile +17 -0
- package/tests/fixtures/corpus/oss-repos/express/benchmarks/README.md +34 -0
- package/tests/fixtures/corpus/oss-repos/express/benchmarks/middleware.js +20 -0
- package/tests/fixtures/corpus/oss-repos/express/benchmarks/run +18 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/README.md +29 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/auth/index.js +134 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/auth/views/foot.ejs +2 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/auth/views/head.ejs +20 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/auth/views/login.ejs +21 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/content-negotiation/db.js +9 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/content-negotiation/index.js +46 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/content-negotiation/users.js +19 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/cookie-sessions/index.js +25 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/cookies/index.js +53 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/downloads/files/CCTV/345/244/247/350/265/233/344/270/212/346/265/267/345/210/206/350/265/233/345/214/272.txt +2 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/downloads/files/amazing.txt +1 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/downloads/files/notes/groceries.txt +3 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/downloads/index.js +40 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/ejs/index.js +57 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/ejs/public/stylesheets/style.css +4 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/ejs/views/footer.html +2 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/ejs/views/header.html +9 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/ejs/views/users.html +10 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/error/index.js +53 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/error-pages/index.js +103 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/error-pages/views/404.ejs +3 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/error-pages/views/500.ejs +8 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/error-pages/views/error_header.ejs +10 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/error-pages/views/footer.ejs +2 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/error-pages/views/index.ejs +20 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/hello-world/index.js +15 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/markdown/index.js +44 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/markdown/views/index.md +4 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/multi-router/controllers/api_v1.js +15 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/multi-router/controllers/api_v2.js +15 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/multi-router/index.js +18 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/mvc/controllers/main/index.js +5 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/mvc/controllers/pet/index.js +31 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/mvc/controllers/pet/views/edit.ejs +17 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/mvc/controllers/pet/views/show.ejs +15 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/mvc/controllers/user/index.js +41 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/mvc/controllers/user/views/edit.hbs +27 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/mvc/controllers/user/views/list.hbs +18 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/mvc/controllers/user/views/show.hbs +31 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/mvc/controllers/user-pet/index.js +22 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/mvc/db.js +16 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/mvc/index.js +95 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/mvc/lib/boot.js +83 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/mvc/public/style.css +14 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/mvc/views/404.ejs +13 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/mvc/views/5xx.ejs +13 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/online/index.js +61 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/params/index.js +74 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/resource/index.js +95 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/route-map/index.js +75 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/route-middleware/index.js +90 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/route-separation/index.js +55 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/route-separation/post.js +13 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/route-separation/public/style.css +24 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/route-separation/site.js +5 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/route-separation/user.js +47 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/route-separation/views/footer.ejs +2 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/route-separation/views/header.ejs +9 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/route-separation/views/index.ejs +10 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/route-separation/views/posts/index.ejs +12 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/route-separation/views/users/edit.ejs +23 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/route-separation/views/users/index.ejs +14 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/route-separation/views/users/view.ejs +9 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/search/index.js +61 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/search/public/client.js +15 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/search/public/index.html +21 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/session/index.js +37 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/session/redis.js +39 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/static-files/index.js +43 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/static-files/public/css/style.css +3 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/static-files/public/hello.txt +1 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/static-files/public/js/app.js +1 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/vhost/index.js +53 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/view-constructor/github-view.js +53 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/view-constructor/index.js +48 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/view-locals/index.js +155 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/view-locals/user.js +36 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/view-locals/views/index.ejs +20 -0
- package/tests/fixtures/corpus/oss-repos/express/examples/web-service/index.js +117 -0
- package/tests/fixtures/corpus/oss-repos/express/index.js +11 -0
- package/tests/fixtures/corpus/oss-repos/express/lib/application.js +631 -0
- package/tests/fixtures/corpus/oss-repos/express/lib/express.js +81 -0
- package/tests/fixtures/corpus/oss-repos/express/lib/request.js +514 -0
- package/tests/fixtures/corpus/oss-repos/express/lib/response.js +1053 -0
- package/tests/fixtures/corpus/oss-repos/express/lib/utils.js +271 -0
- package/tests/fixtures/corpus/oss-repos/express/lib/view.js +205 -0
- package/tests/fixtures/corpus/oss-repos/express/package.json +99 -0
- package/tests/fixtures/corpus/oss-repos/express/test/Route.js +274 -0
- package/tests/fixtures/corpus/oss-repos/express/test/Router.js +636 -0
- package/tests/fixtures/corpus/oss-repos/express/test/acceptance/auth.js +117 -0
- package/tests/fixtures/corpus/oss-repos/express/test/acceptance/content-negotiation.js +49 -0
- package/tests/fixtures/corpus/oss-repos/express/test/acceptance/cookie-sessions.js +38 -0
- package/tests/fixtures/corpus/oss-repos/express/test/acceptance/cookies.js +71 -0
- package/tests/fixtures/corpus/oss-repos/express/test/acceptance/downloads.js +47 -0
- package/tests/fixtures/corpus/oss-repos/express/test/acceptance/ejs.js +17 -0
- package/tests/fixtures/corpus/oss-repos/express/test/acceptance/error-pages.js +99 -0
- package/tests/fixtures/corpus/oss-repos/express/test/acceptance/error.js +29 -0
- package/tests/fixtures/corpus/oss-repos/express/test/acceptance/hello-world.js +21 -0
- package/tests/fixtures/corpus/oss-repos/express/test/acceptance/markdown.js +21 -0
- package/tests/fixtures/corpus/oss-repos/express/test/acceptance/multi-router.js +44 -0
- package/tests/fixtures/corpus/oss-repos/express/test/acceptance/mvc.js +132 -0
- package/tests/fixtures/corpus/oss-repos/express/test/acceptance/params.js +44 -0
- package/tests/fixtures/corpus/oss-repos/express/test/acceptance/resource.js +68 -0
- package/tests/fixtures/corpus/oss-repos/express/test/acceptance/route-map.js +45 -0
- package/tests/fixtures/corpus/oss-repos/express/test/acceptance/route-separation.js +97 -0
- package/tests/fixtures/corpus/oss-repos/express/test/acceptance/vhost.js +46 -0
- package/tests/fixtures/corpus/oss-repos/express/test/acceptance/web-service.js +105 -0
- package/tests/fixtures/corpus/oss-repos/express/test/app.all.js +38 -0
- package/tests/fixtures/corpus/oss-repos/express/test/app.engine.js +83 -0
- package/tests/fixtures/corpus/oss-repos/express/test/app.head.js +66 -0
- package/tests/fixtures/corpus/oss-repos/express/test/app.js +120 -0
- package/tests/fixtures/corpus/oss-repos/express/test/app.listen.js +55 -0
- package/tests/fixtures/corpus/oss-repos/express/test/app.locals.js +26 -0
- package/tests/fixtures/corpus/oss-repos/express/test/app.options.js +116 -0
- package/tests/fixtures/corpus/oss-repos/express/test/app.param.js +323 -0
- package/tests/fixtures/corpus/oss-repos/express/test/app.render.js +374 -0
- package/tests/fixtures/corpus/oss-repos/express/test/app.request.js +143 -0
- package/tests/fixtures/corpus/oss-repos/express/test/app.response.js +143 -0
- package/tests/fixtures/corpus/oss-repos/express/test/app.route.js +197 -0
- package/tests/fixtures/corpus/oss-repos/express/test/app.router.js +1217 -0
- package/tests/fixtures/corpus/oss-repos/express/test/app.routes.error.js +62 -0
- package/tests/fixtures/corpus/oss-repos/express/test/app.use.js +542 -0
- package/tests/fixtures/corpus/oss-repos/express/test/config.js +207 -0
- package/tests/fixtures/corpus/oss-repos/express/test/exports.js +82 -0
- package/tests/fixtures/corpus/oss-repos/express/test/express.json.js +755 -0
- package/tests/fixtures/corpus/oss-repos/express/test/express.raw.js +513 -0
- package/tests/fixtures/corpus/oss-repos/express/test/express.static.js +815 -0
- package/tests/fixtures/corpus/oss-repos/express/test/express.text.js +566 -0
- package/tests/fixtures/corpus/oss-repos/express/test/express.urlencoded.js +828 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/% of dogs.txt +1 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/.name +1 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/blog/index.html +1 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/blog/post/index.tmpl +1 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/broken.send +0 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/default_layout/name.tmpl +1 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/default_layout/user.tmpl +1 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/email.tmpl +1 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/empty.txt +0 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/local_layout/user.tmpl +1 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/name.tmpl +1 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/name.txt +1 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/nums.txt +1 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/pets/names.txt +1 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/snow /342/230/203/.gitkeep +0 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/todo.html +1 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/todo.txt +1 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/user.html +1 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/user.tmpl +1 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/users/index.html +1 -0
- package/tests/fixtures/corpus/oss-repos/express/test/fixtures/users/tobi.txt +1 -0
- package/tests/fixtures/corpus/oss-repos/express/test/middleware.basic.js +42 -0
- package/tests/fixtures/corpus/oss-repos/express/test/regression.js +20 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.accepts.js +125 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.acceptsCharsets.js +50 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.acceptsEncodings.js +39 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.acceptsLanguages.js +57 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.baseUrl.js +88 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.fresh.js +70 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.get.js +60 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.host.js +156 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.hostname.js +188 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.ip.js +113 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.ips.js +71 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.is.js +169 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.path.js +20 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.protocol.js +113 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.query.js +106 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.range.js +104 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.route.js +28 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.secure.js +101 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.signedCookies.js +37 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.stale.js +50 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.subdomains.js +173 -0
- package/tests/fixtures/corpus/oss-repos/express/test/req.xhr.js +42 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.append.js +116 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.attachment.js +79 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.clearCookie.js +62 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.cookie.js +295 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.download.js +487 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.format.js +248 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.get.js +21 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.json.js +186 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.jsonp.js +344 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.links.js +65 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.locals.js +40 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.location.js +316 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.redirect.js +214 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.render.js +367 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.send.js +569 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.sendFile.js +913 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.sendStatus.js +44 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.set.js +124 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.status.js +206 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.type.js +46 -0
- package/tests/fixtures/corpus/oss-repos/express/test/res.vary.js +90 -0
- package/tests/fixtures/corpus/oss-repos/express/test/support/env.js +3 -0
- package/tests/fixtures/corpus/oss-repos/express/test/support/tmpl.js +36 -0
- package/tests/fixtures/corpus/oss-repos/express/test/support/utils.js +86 -0
- package/tests/fixtures/corpus/oss-repos/express/test/utils.js +83 -0
- package/tests/fixtures/corpus/oss-repos/hono/.devcontainer/Dockerfile +11 -0
- package/tests/fixtures/corpus/oss-repos/hono/.devcontainer/devcontainer.json +21 -0
- package/tests/fixtures/corpus/oss-repos/hono/.devcontainer/docker-compose.yml +18 -0
- package/tests/fixtures/corpus/oss-repos/hono/.eslintignore +1 -0
- package/tests/fixtures/corpus/oss-repos/hono/.eslintrc.cjs +9 -0
- package/tests/fixtures/corpus/oss-repos/hono/.gitpod.yml +9 -0
- package/tests/fixtures/corpus/oss-repos/hono/.prettierrc +9 -0
- package/tests/fixtures/corpus/oss-repos/hono/.vitest.config/jsx-runtime-default.ts +15 -0
- package/tests/fixtures/corpus/oss-repos/hono/.vitest.config/jsx-runtime-dom.ts +15 -0
- package/tests/fixtures/corpus/oss-repos/hono/.vitest.config/setup-vitest.ts +47 -0
- package/tests/fixtures/corpus/oss-repos/hono/LICENSE +21 -0
- package/tests/fixtures/corpus/oss-repos/hono/README.md +91 -0
- package/tests/fixtures/corpus/oss-repos/hono/build.ts +80 -0
- package/tests/fixtures/corpus/oss-repos/hono/bun.lockb +0 -0
- package/tests/fixtures/corpus/oss-repos/hono/bunfig.toml +7 -0
- package/tests/fixtures/corpus/oss-repos/hono/codecov.yml +13 -0
- package/tests/fixtures/corpus/oss-repos/hono/docs/CODE_OF_CONDUCT.md +128 -0
- package/tests/fixtures/corpus/oss-repos/hono/docs/CONTRIBUTING.md +62 -0
- package/tests/fixtures/corpus/oss-repos/hono/docs/MIGRATION.md +295 -0
- package/tests/fixtures/corpus/oss-repos/hono/docs/images/hono-logo.png +0 -0
- package/tests/fixtures/corpus/oss-repos/hono/docs/images/hono-logo.pxm +0 -0
- package/tests/fixtures/corpus/oss-repos/hono/docs/images/hono-logo.svg +6 -0
- package/tests/fixtures/corpus/oss-repos/hono/docs/images/hono-title.png +0 -0
- package/tests/fixtures/corpus/oss-repos/hono/docs/images/hono-title.pxm +0 -0
- package/tests/fixtures/corpus/oss-repos/hono/jsr.json +119 -0
- package/tests/fixtures/corpus/oss-repos/hono/package.cjs.json +3 -0
- package/tests/fixtures/corpus/oss-repos/hono/package.json +650 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/aws-lambda/handler.ts +492 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/aws-lambda/index.ts +13 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/aws-lambda/types.ts +144 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/bun/conninfo.ts +28 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/bun/index.ts +9 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/bun/serve-static.ts +35 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/bun/server.ts +30 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/bun/ssg.ts +27 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/bun/websocket.ts +110 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-pages/handler.ts +120 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-pages/index.ts +7 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-workers/conninfo.ts +7 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-workers/index.ts +8 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-workers/serve-static-module.ts +12 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-workers/serve-static.ts +39 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-workers/utils.ts +50 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-workers/websocket.ts +50 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/deno/conninfo.ts +17 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/deno/deno.d.ts +28 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/deno/index.ts +9 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/deno/serve-static.ts +40 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/deno/ssg.ts +27 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/deno/websocket.ts +51 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/lambda-edge/conninfo.ts +15 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/lambda-edge/handler.ts +189 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/lambda-edge/index.ts +14 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/netlify/handler.ts +10 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/netlify/index.ts +6 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/netlify/mod.ts +1 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/service-worker/handler.ts +34 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/service-worker/index.ts +5 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/service-worker/types.ts +14 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/vercel/conninfo.ts +8 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/vercel/handler.ts +9 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/vercel/index.ts +7 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/client/client.ts +214 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/client/index.ts +14 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/client/types.ts +180 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/client/utils.ts +54 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/compose.ts +94 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/context.ts +914 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/accepts/accepts.ts +81 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/accepts/index.ts +6 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/adapter/index.ts +85 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/conninfo/index.ts +6 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/conninfo/types.ts +45 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/cookie/index.ts +130 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/css/common.ts +243 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/css/index.ts +220 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/dev/index.ts +79 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/factory/index.ts +246 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/html/index.ts +56 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/ssg/index.ts +13 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/ssg/middleware.ts +79 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/ssg/ssg.ts +388 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/ssg/utils.ts +71 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/streaming/index.ts +9 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/streaming/sse.ts +89 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/streaming/stream.ts +36 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/streaming/text.ts +15 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/testing/index.ts +26 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/websocket/index.ts +57 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/hono-base.ts +523 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/hono.ts +34 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/http-exception.ts +78 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/index.ts +51 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/base.ts +419 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/children.ts +20 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/components.ts +195 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/constants.ts +5 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/context.ts +50 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/client.ts +89 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/components.ts +39 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/context.ts +52 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/css.ts +246 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/hooks/index.ts +91 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/index.ts +159 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/intrinsic-element/components.ts +398 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/jsx-dev-runtime.ts +22 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/jsx-runtime.ts +7 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/render.ts +772 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/server.ts +70 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/utils.ts +7 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/hooks/index.ts +426 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/index.ts +114 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/intrinsic-element/common.ts +11 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/intrinsic-element/components.ts +196 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/intrinsic-elements.ts +924 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/jsx-dev-runtime.ts +26 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/jsx-runtime.ts +18 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/streaming.ts +184 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/types.ts +41 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/utils.ts +36 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/basic-auth/index.ts +128 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/bearer-auth/index.ts +159 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/body-limit/index.ts +115 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/cache/index.ts +127 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/combine/index.ts +153 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/compress/index.ts +79 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/context-storage/index.ts +55 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/cors/index.ts +141 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/csrf/index.ts +90 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/etag/index.ts +88 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/ip-restriction/index.ts +178 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/jsx-renderer/index.ts +158 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/jwt/index.ts +8 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/jwt/jwt.ts +159 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/logger/index.ts +93 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/method-override/index.ts +146 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/powered-by/index.ts +13 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/pretty-json/index.ts +50 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/request-id/index.ts +8 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/request-id/request-id.ts +59 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/secure-headers/index.ts +8 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/secure-headers/permissions-policy.ts +86 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/secure-headers/secure-headers.ts +319 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/serve-static/index.ts +140 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/timeout/index.ts +58 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/timing/index.ts +7 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/timing/timing.ts +225 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/trailing-slash/index.ts +71 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/preset/quick.ts +24 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/preset/tiny.ts +20 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/request.ts +403 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/router/linear-router/index.ts +6 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/router/linear-router/router.ts +132 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/router/pattern-router/index.ts +6 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/router/pattern-router/router.ts +54 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/router/reg-exp-router/index.ts +6 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/router/reg-exp-router/node.ts +159 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/router/reg-exp-router/router.ts +274 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/router/reg-exp-router/trie.ts +74 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/router/smart-router/index.ts +6 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/router/smart-router/router.ts +69 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/router/trie-router/index.ts +6 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/router/trie-router/node.ts +205 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/router/trie-router/router.ts +28 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/router.ts +103 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/types.ts +2009 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/basic-auth.ts +26 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/body.ts +225 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/buffer.ts +65 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/color.ts +26 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/concurrent.ts +55 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/cookie.ts +230 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/crypto.ts +65 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/encode.ts +34 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/filepath.ts +56 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/handler.ts +15 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/html.ts +182 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/http-status.ts +69 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/ipaddr.ts +113 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/jwt/index.ts +7 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/jwt/jwa.ts +23 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/jwt/jws.ts +226 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/jwt/jwt.ts +114 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/jwt/types.ts +83 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/jwt/utf8.ts +7 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/mime.ts +142 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/stream.ts +96 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/types.ts +105 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/url.ts +310 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/validator/index.ts +7 -0
- package/tests/fixtures/corpus/oss-repos/hono/src/validator/validator.ts +151 -0
- package/tests/fixtures/corpus/oss-repos/hono/tsconfig.build.json +23 -0
- package/tests/fixtures/corpus/oss-repos/hono/tsconfig.json +28 -0
- package/tests/fixtures/corpus/oss-repos/hono/vitest.config.ts +34 -0
- package/tests/fixtures/corpus/oss-repos/hono/yarn.lock +6232 -0
- package/tests/fixtures/documentation/api-reference.md +412 -0
- package/tests/fixtures/documentation/architecture.md +214 -0
- package/tests/fixtures/documentation/deployment-guide.md +420 -0
- package/tests/fixtures/github-readmes/express.md +133 -0
- package/tests/fixtures/github-readmes/nextjs.md +106 -0
- package/tests/fixtures/github-readmes/react.md +74 -0
- package/tests/fixtures/github-readmes/typescript.md +93 -0
- package/tests/fixtures/github-readmes/vite.md +79 -0
- package/tests/fixtures/queries/core.json +125 -0
- package/tests/fixtures/queries/extended.json +427 -0
- package/tests/fixtures/queries/generated/.gitkeep +0 -0
- package/tests/fixtures/test-server.ts +267 -0
- package/tests/helpers/performance-metrics.ts +387 -0
- package/tests/helpers/search-relevance.ts +381 -0
- package/tests/integration/cli-consistency.test.ts +299 -0
- package/tests/integration/cli.test.ts +69 -0
- package/tests/integration/e2e-workflow.test.ts +612 -0
- package/tests/integration/python-bridge.test.ts +183 -0
- package/tests/integration/search-quality.test.ts +718 -0
- package/tests/integration/stress.test.ts +326 -0
- package/tests/mcp/server.test.ts +15 -0
- package/tests/scripts/schemas/evaluation.json +44 -0
- package/tests/scripts/schemas/query-generation.json +21 -0
- package/tests/services/code-unit.service.test.ts +47 -0
- package/tests/services/search.progressive-context.test.ts +35 -0
- package/tsconfig.json +34 -0
- package/tsup.config.ts +15 -0
- package/turndown-plugin-gfm.d.ts +29 -0
- package/vitest.config.ts +79 -0
|
@@ -0,0 +1,2951 @@
|
|
|
1
|
+
# Bluera Knowledge CLI Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Build a CLI tool for managing knowledge stores with semantic search capabilities, providing full feature parity with the existing VS Code extension.
|
|
6
|
+
|
|
7
|
+
**Architecture:** TypeScript primary with Python subprocess bridge for web crawling. Unified service layer shared between CLI and HTTP API. LanceDB for vector storage, Transformers.js for embeddings.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Node.js, Commander.js, LanceDB, Transformers.js, Hono, Zod, crawl4ai (Python)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Phase 1: Project Scaffolding
|
|
14
|
+
|
|
15
|
+
### Task 1.1: Initialize package.json
|
|
16
|
+
|
|
17
|
+
**Files:**
|
|
18
|
+
- Create: `package.json`
|
|
19
|
+
|
|
20
|
+
**Step 1: Create package.json**
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"name": "bluera-knowledge",
|
|
25
|
+
"version": "0.1.0",
|
|
26
|
+
"description": "CLI tool for managing knowledge stores with semantic search",
|
|
27
|
+
"type": "module",
|
|
28
|
+
"bin": {
|
|
29
|
+
"bluera-knowledge": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"dev": "tsup --watch",
|
|
36
|
+
"start": "node dist/index.js",
|
|
37
|
+
"test": "vitest",
|
|
38
|
+
"test:run": "vitest run",
|
|
39
|
+
"lint": "eslint src/",
|
|
40
|
+
"typecheck": "tsc --noEmit"
|
|
41
|
+
},
|
|
42
|
+
"keywords": ["knowledge", "semantic-search", "vector-database", "cli"],
|
|
43
|
+
"author": "Bluera",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=20.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Step 2: Commit**
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git add package.json
|
|
55
|
+
git commit -m "chore: initialize package.json"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### Task 1.2: Configure TypeScript
|
|
61
|
+
|
|
62
|
+
**Files:**
|
|
63
|
+
- Create: `tsconfig.json`
|
|
64
|
+
|
|
65
|
+
**Step 1: Create tsconfig.json with strict settings**
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"compilerOptions": {
|
|
70
|
+
"target": "ES2022",
|
|
71
|
+
"module": "NodeNext",
|
|
72
|
+
"moduleResolution": "NodeNext",
|
|
73
|
+
"lib": ["ES2022"],
|
|
74
|
+
"outDir": "dist",
|
|
75
|
+
"rootDir": "src",
|
|
76
|
+
"declaration": true,
|
|
77
|
+
"declarationMap": true,
|
|
78
|
+
"sourceMap": true,
|
|
79
|
+
"strict": true,
|
|
80
|
+
"noImplicitAny": true,
|
|
81
|
+
"strictNullChecks": true,
|
|
82
|
+
"strictFunctionTypes": true,
|
|
83
|
+
"strictBindCallApply": true,
|
|
84
|
+
"strictPropertyInitialization": true,
|
|
85
|
+
"noImplicitThis": true,
|
|
86
|
+
"useUnknownInCatchVariables": true,
|
|
87
|
+
"alwaysStrict": true,
|
|
88
|
+
"noUnusedLocals": true,
|
|
89
|
+
"noUnusedParameters": true,
|
|
90
|
+
"noImplicitReturns": true,
|
|
91
|
+
"noFallthroughCasesInSwitch": true,
|
|
92
|
+
"noUncheckedIndexedAccess": true,
|
|
93
|
+
"noImplicitOverride": true,
|
|
94
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
95
|
+
"exactOptionalPropertyTypes": true,
|
|
96
|
+
"esModuleInterop": true,
|
|
97
|
+
"forceConsistentCasingInFileNames": true,
|
|
98
|
+
"skipLibCheck": true
|
|
99
|
+
},
|
|
100
|
+
"include": ["src/**/*"],
|
|
101
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Step 2: Commit**
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
git add tsconfig.json
|
|
109
|
+
git commit -m "chore: configure TypeScript with strict settings"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
### Task 1.3: Configure ESLint
|
|
115
|
+
|
|
116
|
+
**Files:**
|
|
117
|
+
- Create: `eslint.config.js`
|
|
118
|
+
|
|
119
|
+
**Step 1: Create eslint.config.js**
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
import eslint from '@eslint/js';
|
|
123
|
+
import tseslint from 'typescript-eslint';
|
|
124
|
+
|
|
125
|
+
export default tseslint.config(
|
|
126
|
+
eslint.configs.recommended,
|
|
127
|
+
...tseslint.configs.strictTypeChecked,
|
|
128
|
+
{
|
|
129
|
+
languageOptions: {
|
|
130
|
+
parserOptions: {
|
|
131
|
+
projectService: true,
|
|
132
|
+
tsconfigRootDir: import.meta.dirname,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
rules: {
|
|
138
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
139
|
+
'@typescript-eslint/no-unsafe-assignment': 'error',
|
|
140
|
+
'@typescript-eslint/no-unsafe-member-access': 'error',
|
|
141
|
+
'@typescript-eslint/no-unsafe-call': 'error',
|
|
142
|
+
'@typescript-eslint/no-unsafe-return': 'error',
|
|
143
|
+
'@typescript-eslint/explicit-function-return-type': 'error',
|
|
144
|
+
'@typescript-eslint/explicit-module-boundary-types': 'error',
|
|
145
|
+
'@typescript-eslint/prefer-readonly': 'error',
|
|
146
|
+
'@typescript-eslint/strict-boolean-expressions': 'error',
|
|
147
|
+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
ignores: ['dist/**', 'node_modules/**', '*.config.js'],
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Step 2: Commit**
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
git add eslint.config.js
|
|
160
|
+
git commit -m "chore: configure ESLint with strict TypeScript rules"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
### Task 1.4: Configure tsup build
|
|
166
|
+
|
|
167
|
+
**Files:**
|
|
168
|
+
- Create: `tsup.config.ts`
|
|
169
|
+
|
|
170
|
+
**Step 1: Create tsup.config.ts**
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { defineConfig } from 'tsup';
|
|
174
|
+
|
|
175
|
+
export default defineConfig({
|
|
176
|
+
entry: ['src/index.ts'],
|
|
177
|
+
format: ['esm'],
|
|
178
|
+
dts: true,
|
|
179
|
+
sourcemap: true,
|
|
180
|
+
clean: true,
|
|
181
|
+
target: 'node20',
|
|
182
|
+
shims: true,
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Step 2: Commit**
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
git add tsup.config.ts
|
|
190
|
+
git commit -m "chore: configure tsup build"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
### Task 1.5: Configure Vitest
|
|
196
|
+
|
|
197
|
+
**Files:**
|
|
198
|
+
- Create: `vitest.config.ts`
|
|
199
|
+
|
|
200
|
+
**Step 1: Create vitest.config.ts**
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { defineConfig } from 'vitest/config';
|
|
204
|
+
|
|
205
|
+
export default defineConfig({
|
|
206
|
+
test: {
|
|
207
|
+
globals: true,
|
|
208
|
+
environment: 'node',
|
|
209
|
+
include: ['src/**/*.test.ts', 'tests/**/*.test.ts'],
|
|
210
|
+
coverage: {
|
|
211
|
+
provider: 'v8',
|
|
212
|
+
reporter: ['text', 'json', 'html'],
|
|
213
|
+
include: ['src/**/*.ts'],
|
|
214
|
+
exclude: ['src/**/*.test.ts', 'src/index.ts'],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Step 2: Commit**
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
git add vitest.config.ts
|
|
224
|
+
git commit -m "chore: configure Vitest"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
### Task 1.6: Update .gitignore
|
|
230
|
+
|
|
231
|
+
**Files:**
|
|
232
|
+
- Modify: `.gitignore`
|
|
233
|
+
|
|
234
|
+
**Step 1: Update .gitignore**
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
node_modules/
|
|
238
|
+
dist/
|
|
239
|
+
.env
|
|
240
|
+
.env.local
|
|
241
|
+
*.log
|
|
242
|
+
.DS_Store
|
|
243
|
+
coverage/
|
|
244
|
+
.vscode/
|
|
245
|
+
*.local.md
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Step 2: Commit**
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
git add .gitignore
|
|
252
|
+
git commit -m "chore: update .gitignore"
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
### Task 1.7: Install dependencies
|
|
258
|
+
|
|
259
|
+
**Files:**
|
|
260
|
+
- Modify: `package.json` (lockfile created)
|
|
261
|
+
|
|
262
|
+
**Step 1: Install production dependencies**
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
npm install commander @lancedb/lancedb @huggingface/transformers chokidar hono @hono/node-server cli-table3 ora chalk zod
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Step 2: Install dev dependencies**
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
npm install -D typescript tsup vitest @types/node eslint @eslint/js typescript-eslint
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Step 3: Commit**
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
git add package.json package-lock.json
|
|
278
|
+
git commit -m "chore: install dependencies"
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
### Task 1.8: Create directory structure
|
|
284
|
+
|
|
285
|
+
**Files:**
|
|
286
|
+
- Create: `src/index.ts`
|
|
287
|
+
- Create: `src/types/index.ts`
|
|
288
|
+
- Create: `src/services/index.ts`
|
|
289
|
+
- Create: `src/db/index.ts`
|
|
290
|
+
- Create: `src/cli/index.ts`
|
|
291
|
+
- Create: `src/server/index.ts`
|
|
292
|
+
|
|
293
|
+
**Step 1: Create src/index.ts (entry point)**
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
#!/usr/bin/env node
|
|
297
|
+
|
|
298
|
+
console.log('bkb CLI');
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Step 2: Create empty module index files**
|
|
302
|
+
|
|
303
|
+
Create `src/types/index.ts`:
|
|
304
|
+
```typescript
|
|
305
|
+
// Types module - no external dependencies
|
|
306
|
+
export {};
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Create `src/services/index.ts`:
|
|
310
|
+
```typescript
|
|
311
|
+
// Services module
|
|
312
|
+
export {};
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Create `src/db/index.ts`:
|
|
316
|
+
```typescript
|
|
317
|
+
// Database module
|
|
318
|
+
export {};
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Create `src/cli/index.ts`:
|
|
322
|
+
```typescript
|
|
323
|
+
// CLI module
|
|
324
|
+
export {};
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Create `src/server/index.ts`:
|
|
328
|
+
```typescript
|
|
329
|
+
// Server module
|
|
330
|
+
export {};
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Step 3: Verify build works**
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
npm run build
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Expected: Build succeeds, creates `dist/index.js`
|
|
340
|
+
|
|
341
|
+
**Step 4: Verify CLI runs**
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
node dist/index.js
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Expected: Output `bkb CLI`
|
|
348
|
+
|
|
349
|
+
**Step 5: Commit**
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
git add src/
|
|
353
|
+
git commit -m "chore: create directory structure"
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Phase 2: Types Layer
|
|
359
|
+
|
|
360
|
+
### Task 2.1: Create branded types
|
|
361
|
+
|
|
362
|
+
**Files:**
|
|
363
|
+
- Create: `src/types/brands.ts`
|
|
364
|
+
- Create: `src/types/brands.test.ts`
|
|
365
|
+
|
|
366
|
+
**Step 1: Write the failing test**
|
|
367
|
+
|
|
368
|
+
Create `src/types/brands.test.ts`:
|
|
369
|
+
```typescript
|
|
370
|
+
import { describe, it, expect } from 'vitest';
|
|
371
|
+
import { createStoreId, createDocumentId, isStoreId, isDocumentId } from './brands.js';
|
|
372
|
+
|
|
373
|
+
describe('branded types', () => {
|
|
374
|
+
describe('StoreId', () => {
|
|
375
|
+
it('creates a valid store ID', () => {
|
|
376
|
+
const id = createStoreId('store-123');
|
|
377
|
+
expect(id).toBe('store-123');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('validates store ID format', () => {
|
|
381
|
+
expect(isStoreId('valid-store-id')).toBe(true);
|
|
382
|
+
expect(isStoreId('')).toBe(false);
|
|
383
|
+
expect(isStoreId('has spaces')).toBe(false);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe('DocumentId', () => {
|
|
388
|
+
it('creates a valid document ID', () => {
|
|
389
|
+
const id = createDocumentId('doc-456');
|
|
390
|
+
expect(id).toBe('doc-456');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('validates document ID format', () => {
|
|
394
|
+
expect(isDocumentId('valid-doc-id')).toBe(true);
|
|
395
|
+
expect(isDocumentId('')).toBe(false);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Step 2: Run test to verify it fails**
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
npm run test:run -- src/types/brands.test.ts
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Expected: FAIL - module not found
|
|
408
|
+
|
|
409
|
+
**Step 3: Write minimal implementation**
|
|
410
|
+
|
|
411
|
+
Create `src/types/brands.ts`:
|
|
412
|
+
```typescript
|
|
413
|
+
// Branded type symbols
|
|
414
|
+
declare const StoreIdBrand: unique symbol;
|
|
415
|
+
declare const DocumentIdBrand: unique symbol;
|
|
416
|
+
|
|
417
|
+
// Branded types
|
|
418
|
+
export type StoreId = string & { readonly [StoreIdBrand]: typeof StoreIdBrand };
|
|
419
|
+
export type DocumentId = string & { readonly [DocumentIdBrand]: typeof DocumentIdBrand };
|
|
420
|
+
|
|
421
|
+
// Valid ID pattern: alphanumeric, hyphens, underscores
|
|
422
|
+
const ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
423
|
+
|
|
424
|
+
export function isStoreId(value: string): value is StoreId {
|
|
425
|
+
return value.length > 0 && ID_PATTERN.test(value);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function isDocumentId(value: string): value is DocumentId {
|
|
429
|
+
return value.length > 0 && ID_PATTERN.test(value);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function createStoreId(value: string): StoreId {
|
|
433
|
+
if (!isStoreId(value)) {
|
|
434
|
+
throw new Error(`Invalid store ID: ${value}`);
|
|
435
|
+
}
|
|
436
|
+
return value;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function createDocumentId(value: string): DocumentId {
|
|
440
|
+
if (!isDocumentId(value)) {
|
|
441
|
+
throw new Error(`Invalid document ID: ${value}`);
|
|
442
|
+
}
|
|
443
|
+
return value;
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**Step 4: Run test to verify it passes**
|
|
448
|
+
|
|
449
|
+
```bash
|
|
450
|
+
npm run test:run -- src/types/brands.test.ts
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Expected: PASS
|
|
454
|
+
|
|
455
|
+
**Step 5: Commit**
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
git add src/types/brands.ts src/types/brands.test.ts
|
|
459
|
+
git commit -m "feat: add branded types for StoreId and DocumentId"
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
### Task 2.2: Create Result type
|
|
465
|
+
|
|
466
|
+
**Files:**
|
|
467
|
+
- Create: `src/types/result.ts`
|
|
468
|
+
- Create: `src/types/result.test.ts`
|
|
469
|
+
|
|
470
|
+
**Step 1: Write the failing test**
|
|
471
|
+
|
|
472
|
+
Create `src/types/result.test.ts`:
|
|
473
|
+
```typescript
|
|
474
|
+
import { describe, it, expect } from 'vitest';
|
|
475
|
+
import { ok, err, isOk, isErr, unwrap, unwrapOr } from './result.js';
|
|
476
|
+
|
|
477
|
+
describe('Result type', () => {
|
|
478
|
+
describe('ok', () => {
|
|
479
|
+
it('creates a success result', () => {
|
|
480
|
+
const result = ok(42);
|
|
481
|
+
expect(isOk(result)).toBe(true);
|
|
482
|
+
expect(isErr(result)).toBe(false);
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
describe('err', () => {
|
|
487
|
+
it('creates an error result', () => {
|
|
488
|
+
const result = err(new Error('failed'));
|
|
489
|
+
expect(isErr(result)).toBe(true);
|
|
490
|
+
expect(isOk(result)).toBe(false);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
describe('unwrap', () => {
|
|
495
|
+
it('returns value for success', () => {
|
|
496
|
+
const result = ok(42);
|
|
497
|
+
expect(unwrap(result)).toBe(42);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('throws for error', () => {
|
|
501
|
+
const result = err(new Error('failed'));
|
|
502
|
+
expect(() => unwrap(result)).toThrow('failed');
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
describe('unwrapOr', () => {
|
|
507
|
+
it('returns value for success', () => {
|
|
508
|
+
const result = ok(42);
|
|
509
|
+
expect(unwrapOr(result, 0)).toBe(42);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('returns default for error', () => {
|
|
513
|
+
const result = err(new Error('failed'));
|
|
514
|
+
expect(unwrapOr(result, 0)).toBe(0);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
**Step 2: Run test to verify it fails**
|
|
521
|
+
|
|
522
|
+
```bash
|
|
523
|
+
npm run test:run -- src/types/result.test.ts
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
Expected: FAIL - module not found
|
|
527
|
+
|
|
528
|
+
**Step 3: Write minimal implementation**
|
|
529
|
+
|
|
530
|
+
Create `src/types/result.ts`:
|
|
531
|
+
```typescript
|
|
532
|
+
export type Result<T, E = Error> =
|
|
533
|
+
| { readonly success: true; readonly data: T }
|
|
534
|
+
| { readonly success: false; readonly error: E };
|
|
535
|
+
|
|
536
|
+
export function ok<T>(data: T): Result<T, never> {
|
|
537
|
+
return { success: true, data };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export function err<E>(error: E): Result<never, E> {
|
|
541
|
+
return { success: false, error };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export function isOk<T, E>(result: Result<T, E>): result is { success: true; data: T } {
|
|
545
|
+
return result.success;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export function isErr<T, E>(result: Result<T, E>): result is { success: false; error: E } {
|
|
549
|
+
return !result.success;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function unwrap<T, E>(result: Result<T, E>): T {
|
|
553
|
+
if (isOk(result)) {
|
|
554
|
+
return result.data;
|
|
555
|
+
}
|
|
556
|
+
if (result.error instanceof Error) {
|
|
557
|
+
throw result.error;
|
|
558
|
+
}
|
|
559
|
+
throw new Error(String(result.error));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export function unwrapOr<T, E>(result: Result<T, E>, defaultValue: T): T {
|
|
563
|
+
if (isOk(result)) {
|
|
564
|
+
return result.data;
|
|
565
|
+
}
|
|
566
|
+
return defaultValue;
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
**Step 4: Run test to verify it passes**
|
|
571
|
+
|
|
572
|
+
```bash
|
|
573
|
+
npm run test:run -- src/types/result.test.ts
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
Expected: PASS
|
|
577
|
+
|
|
578
|
+
**Step 5: Commit**
|
|
579
|
+
|
|
580
|
+
```bash
|
|
581
|
+
git add src/types/result.ts src/types/result.test.ts
|
|
582
|
+
git commit -m "feat: add Result type for error handling"
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
### Task 2.3: Create Store types
|
|
588
|
+
|
|
589
|
+
**Files:**
|
|
590
|
+
- Create: `src/types/store.ts`
|
|
591
|
+
- Create: `src/types/store.test.ts`
|
|
592
|
+
|
|
593
|
+
**Step 1: Write the failing test**
|
|
594
|
+
|
|
595
|
+
Create `src/types/store.test.ts`:
|
|
596
|
+
```typescript
|
|
597
|
+
import { describe, it, expect } from 'vitest';
|
|
598
|
+
import { isFileStore, isRepoStore, isWebStore } from './store.js';
|
|
599
|
+
import type { Store, FileStore, RepoStore, WebStore } from './store.js';
|
|
600
|
+
import { createStoreId } from './brands.js';
|
|
601
|
+
|
|
602
|
+
describe('Store types', () => {
|
|
603
|
+
const fileStore: FileStore = {
|
|
604
|
+
type: 'file',
|
|
605
|
+
id: createStoreId('file-store'),
|
|
606
|
+
name: 'My Files',
|
|
607
|
+
path: '/path/to/files',
|
|
608
|
+
description: 'Test file store',
|
|
609
|
+
createdAt: new Date(),
|
|
610
|
+
updatedAt: new Date(),
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const repoStore: RepoStore = {
|
|
614
|
+
type: 'repo',
|
|
615
|
+
id: createStoreId('repo-store'),
|
|
616
|
+
name: 'My Repo',
|
|
617
|
+
path: '/path/to/repo',
|
|
618
|
+
branch: 'main',
|
|
619
|
+
createdAt: new Date(),
|
|
620
|
+
updatedAt: new Date(),
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const webStore: WebStore = {
|
|
624
|
+
type: 'web',
|
|
625
|
+
id: createStoreId('web-store'),
|
|
626
|
+
name: 'Docs Site',
|
|
627
|
+
url: 'https://docs.example.com',
|
|
628
|
+
depth: 2,
|
|
629
|
+
createdAt: new Date(),
|
|
630
|
+
updatedAt: new Date(),
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
describe('isFileStore', () => {
|
|
634
|
+
it('returns true for file stores', () => {
|
|
635
|
+
expect(isFileStore(fileStore)).toBe(true);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it('returns false for other store types', () => {
|
|
639
|
+
expect(isFileStore(repoStore)).toBe(false);
|
|
640
|
+
expect(isFileStore(webStore)).toBe(false);
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
describe('isRepoStore', () => {
|
|
645
|
+
it('returns true for repo stores', () => {
|
|
646
|
+
expect(isRepoStore(repoStore)).toBe(true);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it('returns false for other store types', () => {
|
|
650
|
+
expect(isRepoStore(fileStore)).toBe(false);
|
|
651
|
+
expect(isRepoStore(webStore)).toBe(false);
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
describe('isWebStore', () => {
|
|
656
|
+
it('returns true for web stores', () => {
|
|
657
|
+
expect(isWebStore(webStore)).toBe(true);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('returns false for other store types', () => {
|
|
661
|
+
expect(isWebStore(fileStore)).toBe(false);
|
|
662
|
+
expect(isWebStore(repoStore)).toBe(false);
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
**Step 2: Run test to verify it fails**
|
|
669
|
+
|
|
670
|
+
```bash
|
|
671
|
+
npm run test:run -- src/types/store.test.ts
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
Expected: FAIL - module not found
|
|
675
|
+
|
|
676
|
+
**Step 3: Write minimal implementation**
|
|
677
|
+
|
|
678
|
+
Create `src/types/store.ts`:
|
|
679
|
+
```typescript
|
|
680
|
+
import type { StoreId } from './brands.js';
|
|
681
|
+
|
|
682
|
+
export type StoreType = 'file' | 'repo' | 'web';
|
|
683
|
+
export type StoreStatus = 'ready' | 'indexing' | 'error';
|
|
684
|
+
|
|
685
|
+
interface BaseStore {
|
|
686
|
+
readonly id: StoreId;
|
|
687
|
+
readonly name: string;
|
|
688
|
+
readonly description?: string;
|
|
689
|
+
readonly tags?: readonly string[];
|
|
690
|
+
readonly status?: StoreStatus;
|
|
691
|
+
readonly createdAt: Date;
|
|
692
|
+
readonly updatedAt: Date;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export interface FileStore extends BaseStore {
|
|
696
|
+
readonly type: 'file';
|
|
697
|
+
readonly path: string;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export interface RepoStore extends BaseStore {
|
|
701
|
+
readonly type: 'repo';
|
|
702
|
+
readonly path: string;
|
|
703
|
+
readonly branch?: string;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
export interface WebStore extends BaseStore {
|
|
707
|
+
readonly type: 'web';
|
|
708
|
+
readonly url: string;
|
|
709
|
+
readonly depth: number;
|
|
710
|
+
readonly maxPages?: number;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export type Store = FileStore | RepoStore | WebStore;
|
|
714
|
+
|
|
715
|
+
export function isFileStore(store: Store): store is FileStore {
|
|
716
|
+
return store.type === 'file';
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export function isRepoStore(store: Store): store is RepoStore {
|
|
720
|
+
return store.type === 'repo';
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export function isWebStore(store: Store): store is WebStore {
|
|
724
|
+
return store.type === 'web';
|
|
725
|
+
}
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
**Step 4: Run test to verify it passes**
|
|
729
|
+
|
|
730
|
+
```bash
|
|
731
|
+
npm run test:run -- src/types/store.test.ts
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
Expected: PASS
|
|
735
|
+
|
|
736
|
+
**Step 5: Commit**
|
|
737
|
+
|
|
738
|
+
```bash
|
|
739
|
+
git add src/types/store.ts src/types/store.test.ts
|
|
740
|
+
git commit -m "feat: add Store discriminated union types"
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
---
|
|
744
|
+
|
|
745
|
+
### Task 2.4: Create Document types
|
|
746
|
+
|
|
747
|
+
**Files:**
|
|
748
|
+
- Create: `src/types/document.ts`
|
|
749
|
+
|
|
750
|
+
**Step 1: Create document types**
|
|
751
|
+
|
|
752
|
+
Create `src/types/document.ts`:
|
|
753
|
+
```typescript
|
|
754
|
+
import type { DocumentId, StoreId } from './brands.js';
|
|
755
|
+
|
|
756
|
+
export type DocumentType = 'file' | 'chunk' | 'web';
|
|
757
|
+
|
|
758
|
+
export interface DocumentMetadata {
|
|
759
|
+
readonly path?: string;
|
|
760
|
+
readonly url?: string;
|
|
761
|
+
readonly type: DocumentType;
|
|
762
|
+
readonly storeId: StoreId;
|
|
763
|
+
readonly indexedAt: Date;
|
|
764
|
+
readonly fileHash?: string;
|
|
765
|
+
readonly chunkIndex?: number;
|
|
766
|
+
readonly totalChunks?: number;
|
|
767
|
+
readonly [key: string]: unknown;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export interface Document {
|
|
771
|
+
readonly id: DocumentId;
|
|
772
|
+
readonly content: string;
|
|
773
|
+
readonly vector: readonly number[];
|
|
774
|
+
readonly metadata: DocumentMetadata;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
export interface DocumentChunk {
|
|
778
|
+
readonly id: DocumentId;
|
|
779
|
+
readonly content: string;
|
|
780
|
+
readonly startLine?: number;
|
|
781
|
+
readonly endLine?: number;
|
|
782
|
+
readonly metadata: DocumentMetadata;
|
|
783
|
+
}
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
**Step 2: Commit**
|
|
787
|
+
|
|
788
|
+
```bash
|
|
789
|
+
git add src/types/document.ts
|
|
790
|
+
git commit -m "feat: add Document types"
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
---
|
|
794
|
+
|
|
795
|
+
### Task 2.5: Create Search types
|
|
796
|
+
|
|
797
|
+
**Files:**
|
|
798
|
+
- Create: `src/types/search.ts`
|
|
799
|
+
|
|
800
|
+
**Step 1: Create search types**
|
|
801
|
+
|
|
802
|
+
Create `src/types/search.ts`:
|
|
803
|
+
```typescript
|
|
804
|
+
import type { StoreId, DocumentId } from './brands.js';
|
|
805
|
+
import type { DocumentMetadata } from './document.js';
|
|
806
|
+
|
|
807
|
+
export type SearchMode = 'vector' | 'fts' | 'hybrid';
|
|
808
|
+
|
|
809
|
+
export interface SearchQuery {
|
|
810
|
+
readonly query: string;
|
|
811
|
+
readonly stores?: readonly StoreId[];
|
|
812
|
+
readonly mode?: SearchMode;
|
|
813
|
+
readonly limit?: number;
|
|
814
|
+
readonly threshold?: number;
|
|
815
|
+
readonly filter?: Record<string, unknown>;
|
|
816
|
+
readonly includeContent?: boolean;
|
|
817
|
+
readonly contextLines?: number;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
export interface SearchResult {
|
|
821
|
+
readonly id: DocumentId;
|
|
822
|
+
readonly score: number;
|
|
823
|
+
readonly content: string;
|
|
824
|
+
readonly highlight?: string;
|
|
825
|
+
readonly metadata: DocumentMetadata;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
export interface SearchResponse {
|
|
829
|
+
readonly query: string;
|
|
830
|
+
readonly mode: SearchMode;
|
|
831
|
+
readonly stores: readonly StoreId[];
|
|
832
|
+
readonly results: readonly SearchResult[];
|
|
833
|
+
readonly totalResults: number;
|
|
834
|
+
readonly timeMs: number;
|
|
835
|
+
}
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
**Step 2: Commit**
|
|
839
|
+
|
|
840
|
+
```bash
|
|
841
|
+
git add src/types/search.ts
|
|
842
|
+
git commit -m "feat: add Search types"
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
---
|
|
846
|
+
|
|
847
|
+
### Task 2.6: Create Config types
|
|
848
|
+
|
|
849
|
+
**Files:**
|
|
850
|
+
- Create: `src/types/config.ts`
|
|
851
|
+
|
|
852
|
+
**Step 1: Create config types**
|
|
853
|
+
|
|
854
|
+
Create `src/types/config.ts`:
|
|
855
|
+
```typescript
|
|
856
|
+
export interface EmbeddingConfig {
|
|
857
|
+
readonly model: string;
|
|
858
|
+
readonly batchSize: number;
|
|
859
|
+
readonly dimensions: number;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
export interface IndexingConfig {
|
|
863
|
+
readonly concurrency: number;
|
|
864
|
+
readonly chunkSize: number;
|
|
865
|
+
readonly chunkOverlap: number;
|
|
866
|
+
readonly ignorePatterns: readonly string[];
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
export interface SearchConfig {
|
|
870
|
+
readonly defaultMode: 'vector' | 'fts' | 'hybrid';
|
|
871
|
+
readonly defaultLimit: number;
|
|
872
|
+
readonly minScore: number;
|
|
873
|
+
readonly rrf: {
|
|
874
|
+
readonly k: number;
|
|
875
|
+
readonly vectorWeight: number;
|
|
876
|
+
readonly ftsWeight: number;
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
export interface CrawlConfig {
|
|
881
|
+
readonly userAgent: string;
|
|
882
|
+
readonly timeout: number;
|
|
883
|
+
readonly maxConcurrency: number;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
export interface ServerConfig {
|
|
887
|
+
readonly port: number;
|
|
888
|
+
readonly host: string;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
export interface AppConfig {
|
|
892
|
+
readonly version: number;
|
|
893
|
+
readonly dataDir: string;
|
|
894
|
+
readonly embedding: EmbeddingConfig;
|
|
895
|
+
readonly indexing: IndexingConfig;
|
|
896
|
+
readonly search: SearchConfig;
|
|
897
|
+
readonly crawl: CrawlConfig;
|
|
898
|
+
readonly server: ServerConfig;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
export const DEFAULT_CONFIG: AppConfig = {
|
|
902
|
+
version: 1,
|
|
903
|
+
dataDir: '~/.bluera/knowledge-data',
|
|
904
|
+
embedding: {
|
|
905
|
+
model: 'Xenova/all-MiniLM-L6-v2',
|
|
906
|
+
batchSize: 32,
|
|
907
|
+
dimensions: 384,
|
|
908
|
+
},
|
|
909
|
+
indexing: {
|
|
910
|
+
concurrency: 4,
|
|
911
|
+
chunkSize: 512,
|
|
912
|
+
chunkOverlap: 50,
|
|
913
|
+
ignorePatterns: ['node_modules/**', '.git/**', '*.min.js', '*.map'],
|
|
914
|
+
},
|
|
915
|
+
search: {
|
|
916
|
+
defaultMode: 'hybrid',
|
|
917
|
+
defaultLimit: 10,
|
|
918
|
+
minScore: 0.5,
|
|
919
|
+
rrf: {
|
|
920
|
+
k: 60,
|
|
921
|
+
vectorWeight: 0.7,
|
|
922
|
+
ftsWeight: 0.3,
|
|
923
|
+
},
|
|
924
|
+
},
|
|
925
|
+
crawl: {
|
|
926
|
+
userAgent: 'BlueraKnowledge/1.0',
|
|
927
|
+
timeout: 30000,
|
|
928
|
+
maxConcurrency: 3,
|
|
929
|
+
},
|
|
930
|
+
server: {
|
|
931
|
+
port: 3847,
|
|
932
|
+
host: '127.0.0.1',
|
|
933
|
+
},
|
|
934
|
+
};
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
**Step 2: Commit**
|
|
938
|
+
|
|
939
|
+
```bash
|
|
940
|
+
git add src/types/config.ts
|
|
941
|
+
git commit -m "feat: add Config types with defaults"
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
---
|
|
945
|
+
|
|
946
|
+
### Task 2.7: Create types barrel export
|
|
947
|
+
|
|
948
|
+
**Files:**
|
|
949
|
+
- Modify: `src/types/index.ts`
|
|
950
|
+
|
|
951
|
+
**Step 1: Update types index**
|
|
952
|
+
|
|
953
|
+
Update `src/types/index.ts`:
|
|
954
|
+
```typescript
|
|
955
|
+
// Branded types
|
|
956
|
+
export {
|
|
957
|
+
type StoreId,
|
|
958
|
+
type DocumentId,
|
|
959
|
+
createStoreId,
|
|
960
|
+
createDocumentId,
|
|
961
|
+
isStoreId,
|
|
962
|
+
isDocumentId,
|
|
963
|
+
} from './brands.js';
|
|
964
|
+
|
|
965
|
+
// Result type
|
|
966
|
+
export {
|
|
967
|
+
type Result,
|
|
968
|
+
ok,
|
|
969
|
+
err,
|
|
970
|
+
isOk,
|
|
971
|
+
isErr,
|
|
972
|
+
unwrap,
|
|
973
|
+
unwrapOr,
|
|
974
|
+
} from './result.js';
|
|
975
|
+
|
|
976
|
+
// Store types
|
|
977
|
+
export {
|
|
978
|
+
type Store,
|
|
979
|
+
type FileStore,
|
|
980
|
+
type RepoStore,
|
|
981
|
+
type WebStore,
|
|
982
|
+
type StoreType,
|
|
983
|
+
type StoreStatus,
|
|
984
|
+
isFileStore,
|
|
985
|
+
isRepoStore,
|
|
986
|
+
isWebStore,
|
|
987
|
+
} from './store.js';
|
|
988
|
+
|
|
989
|
+
// Document types
|
|
990
|
+
export {
|
|
991
|
+
type Document,
|
|
992
|
+
type DocumentChunk,
|
|
993
|
+
type DocumentMetadata,
|
|
994
|
+
type DocumentType,
|
|
995
|
+
} from './document.js';
|
|
996
|
+
|
|
997
|
+
// Search types
|
|
998
|
+
export {
|
|
999
|
+
type SearchQuery,
|
|
1000
|
+
type SearchResult,
|
|
1001
|
+
type SearchResponse,
|
|
1002
|
+
type SearchMode,
|
|
1003
|
+
} from './search.js';
|
|
1004
|
+
|
|
1005
|
+
// Config types
|
|
1006
|
+
export {
|
|
1007
|
+
type AppConfig,
|
|
1008
|
+
type EmbeddingConfig,
|
|
1009
|
+
type IndexingConfig,
|
|
1010
|
+
type SearchConfig,
|
|
1011
|
+
type CrawlConfig,
|
|
1012
|
+
type ServerConfig,
|
|
1013
|
+
DEFAULT_CONFIG,
|
|
1014
|
+
} from './config.js';
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
**Step 2: Run all type tests**
|
|
1018
|
+
|
|
1019
|
+
```bash
|
|
1020
|
+
npm run test:run -- src/types/
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
Expected: All tests pass
|
|
1024
|
+
|
|
1025
|
+
**Step 3: Commit**
|
|
1026
|
+
|
|
1027
|
+
```bash
|
|
1028
|
+
git add src/types/index.ts
|
|
1029
|
+
git commit -m "feat: add types barrel export"
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
---
|
|
1033
|
+
|
|
1034
|
+
## Phase 3: Data Layer
|
|
1035
|
+
|
|
1036
|
+
### Task 3.1: Create embedding engine
|
|
1037
|
+
|
|
1038
|
+
**Files:**
|
|
1039
|
+
- Create: `src/db/embeddings.ts`
|
|
1040
|
+
- Create: `src/db/embeddings.test.ts`
|
|
1041
|
+
|
|
1042
|
+
**Step 1: Write the failing test**
|
|
1043
|
+
|
|
1044
|
+
Create `src/db/embeddings.test.ts`:
|
|
1045
|
+
```typescript
|
|
1046
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
1047
|
+
import { EmbeddingEngine } from './embeddings.js';
|
|
1048
|
+
|
|
1049
|
+
describe('EmbeddingEngine', () => {
|
|
1050
|
+
let engine: EmbeddingEngine;
|
|
1051
|
+
|
|
1052
|
+
beforeAll(async () => {
|
|
1053
|
+
engine = new EmbeddingEngine();
|
|
1054
|
+
await engine.initialize();
|
|
1055
|
+
}, 60000); // Allow time for model download
|
|
1056
|
+
|
|
1057
|
+
it('generates embeddings for text', async () => {
|
|
1058
|
+
const embedding = await engine.embed('Hello world');
|
|
1059
|
+
expect(embedding).toHaveLength(384);
|
|
1060
|
+
expect(embedding.every((n) => typeof n === 'number')).toBe(true);
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
it('generates batch embeddings', async () => {
|
|
1064
|
+
const texts = ['Hello', 'World', 'Test'];
|
|
1065
|
+
const embeddings = await engine.embedBatch(texts);
|
|
1066
|
+
expect(embeddings).toHaveLength(3);
|
|
1067
|
+
expect(embeddings.every((e) => e.length === 384)).toBe(true);
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
it('produces similar embeddings for similar text', async () => {
|
|
1071
|
+
const emb1 = await engine.embed('The cat sat on the mat');
|
|
1072
|
+
const emb2 = await engine.embed('A cat was sitting on a rug');
|
|
1073
|
+
const emb3 = await engine.embed('Quantum physics is complex');
|
|
1074
|
+
|
|
1075
|
+
const sim12 = cosineSimilarity(emb1, emb2);
|
|
1076
|
+
const sim13 = cosineSimilarity(emb1, emb3);
|
|
1077
|
+
|
|
1078
|
+
expect(sim12).toBeGreaterThan(sim13);
|
|
1079
|
+
});
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
function cosineSimilarity(a: number[], b: number[]): number {
|
|
1083
|
+
let dotProduct = 0;
|
|
1084
|
+
let normA = 0;
|
|
1085
|
+
let normB = 0;
|
|
1086
|
+
for (let i = 0; i < a.length; i++) {
|
|
1087
|
+
dotProduct += a[i]! * b[i]!;
|
|
1088
|
+
normA += a[i]! * a[i]!;
|
|
1089
|
+
normB += b[i]! * b[i]!;
|
|
1090
|
+
}
|
|
1091
|
+
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
1092
|
+
}
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
**Step 2: Run test to verify it fails**
|
|
1096
|
+
|
|
1097
|
+
```bash
|
|
1098
|
+
npm run test:run -- src/db/embeddings.test.ts
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
Expected: FAIL - module not found
|
|
1102
|
+
|
|
1103
|
+
**Step 3: Write minimal implementation**
|
|
1104
|
+
|
|
1105
|
+
Create `src/db/embeddings.ts`:
|
|
1106
|
+
```typescript
|
|
1107
|
+
import { pipeline, type FeatureExtractionPipeline } from '@huggingface/transformers';
|
|
1108
|
+
|
|
1109
|
+
export class EmbeddingEngine {
|
|
1110
|
+
private extractor: FeatureExtractionPipeline | null = null;
|
|
1111
|
+
private readonly modelName: string;
|
|
1112
|
+
private readonly dimensions: number;
|
|
1113
|
+
|
|
1114
|
+
constructor(modelName = 'Xenova/all-MiniLM-L6-v2', dimensions = 384) {
|
|
1115
|
+
this.modelName = modelName;
|
|
1116
|
+
this.dimensions = dimensions;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
async initialize(): Promise<void> {
|
|
1120
|
+
if (this.extractor !== null) return;
|
|
1121
|
+
this.extractor = await pipeline('feature-extraction', this.modelName, {
|
|
1122
|
+
dtype: 'fp32',
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
async embed(text: string): Promise<number[]> {
|
|
1127
|
+
if (this.extractor === null) {
|
|
1128
|
+
await this.initialize();
|
|
1129
|
+
}
|
|
1130
|
+
const output = await this.extractor!(text, {
|
|
1131
|
+
pooling: 'mean',
|
|
1132
|
+
normalize: true,
|
|
1133
|
+
});
|
|
1134
|
+
return Array.from(output.data as Float32Array);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
async embedBatch(texts: string[]): Promise<number[][]> {
|
|
1138
|
+
const results: number[][] = [];
|
|
1139
|
+
for (const text of texts) {
|
|
1140
|
+
results.push(await this.embed(text));
|
|
1141
|
+
}
|
|
1142
|
+
return results;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
getDimensions(): number {
|
|
1146
|
+
return this.dimensions;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
**Step 4: Run test to verify it passes**
|
|
1152
|
+
|
|
1153
|
+
```bash
|
|
1154
|
+
npm run test:run -- src/db/embeddings.test.ts
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
Expected: PASS (may take time for model download on first run)
|
|
1158
|
+
|
|
1159
|
+
**Step 5: Commit**
|
|
1160
|
+
|
|
1161
|
+
```bash
|
|
1162
|
+
git add src/db/embeddings.ts src/db/embeddings.test.ts
|
|
1163
|
+
git commit -m "feat: add EmbeddingEngine with Transformers.js"
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
---
|
|
1167
|
+
|
|
1168
|
+
### Task 3.2: Create LanceDB wrapper
|
|
1169
|
+
|
|
1170
|
+
**Files:**
|
|
1171
|
+
- Create: `src/db/lance.ts`
|
|
1172
|
+
- Create: `src/db/lance.test.ts`
|
|
1173
|
+
|
|
1174
|
+
**Step 1: Write the failing test**
|
|
1175
|
+
|
|
1176
|
+
Create `src/db/lance.test.ts`:
|
|
1177
|
+
```typescript
|
|
1178
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
1179
|
+
import { LanceStore } from './lance.js';
|
|
1180
|
+
import { createStoreId, createDocumentId } from '../types/brands.js';
|
|
1181
|
+
import { rm, mkdtemp } from 'node:fs/promises';
|
|
1182
|
+
import { tmpdir } from 'node:os';
|
|
1183
|
+
import { join } from 'node:path';
|
|
1184
|
+
|
|
1185
|
+
describe('LanceStore', () => {
|
|
1186
|
+
let store: LanceStore;
|
|
1187
|
+
let tempDir: string;
|
|
1188
|
+
const storeId = createStoreId('test-store');
|
|
1189
|
+
|
|
1190
|
+
beforeAll(async () => {
|
|
1191
|
+
tempDir = await mkdtemp(join(tmpdir(), 'lance-test-'));
|
|
1192
|
+
store = new LanceStore(tempDir);
|
|
1193
|
+
await store.initialize(storeId);
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
afterAll(async () => {
|
|
1197
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
it('adds and retrieves documents', async () => {
|
|
1201
|
+
const doc = {
|
|
1202
|
+
id: createDocumentId('doc-1'),
|
|
1203
|
+
content: 'Test content',
|
|
1204
|
+
vector: new Array(384).fill(0.1),
|
|
1205
|
+
metadata: {
|
|
1206
|
+
type: 'file' as const,
|
|
1207
|
+
storeId,
|
|
1208
|
+
indexedAt: new Date(),
|
|
1209
|
+
path: '/test/file.txt',
|
|
1210
|
+
},
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
await store.addDocuments(storeId, [doc]);
|
|
1214
|
+
|
|
1215
|
+
const results = await store.search(storeId, new Array(384).fill(0.1), 10);
|
|
1216
|
+
expect(results.length).toBeGreaterThan(0);
|
|
1217
|
+
expect(results[0]?.id).toBe('doc-1');
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
it('deletes documents', async () => {
|
|
1221
|
+
const docId = createDocumentId('doc-to-delete');
|
|
1222
|
+
const doc = {
|
|
1223
|
+
id: docId,
|
|
1224
|
+
content: 'Delete me',
|
|
1225
|
+
vector: new Array(384).fill(0.2),
|
|
1226
|
+
metadata: {
|
|
1227
|
+
type: 'file' as const,
|
|
1228
|
+
storeId,
|
|
1229
|
+
indexedAt: new Date(),
|
|
1230
|
+
},
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
await store.addDocuments(storeId, [doc]);
|
|
1234
|
+
await store.deleteDocuments(storeId, [docId]);
|
|
1235
|
+
|
|
1236
|
+
const results = await store.search(storeId, new Array(384).fill(0.2), 10);
|
|
1237
|
+
const found = results.find((r) => r.id === 'doc-to-delete');
|
|
1238
|
+
expect(found).toBeUndefined();
|
|
1239
|
+
});
|
|
1240
|
+
});
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
**Step 2: Run test to verify it fails**
|
|
1244
|
+
|
|
1245
|
+
```bash
|
|
1246
|
+
npm run test:run -- src/db/lance.test.ts
|
|
1247
|
+
```
|
|
1248
|
+
|
|
1249
|
+
Expected: FAIL - module not found
|
|
1250
|
+
|
|
1251
|
+
**Step 3: Write minimal implementation**
|
|
1252
|
+
|
|
1253
|
+
Create `src/db/lance.ts`:
|
|
1254
|
+
```typescript
|
|
1255
|
+
import * as lancedb from '@lancedb/lancedb';
|
|
1256
|
+
import type { Table, Connection } from '@lancedb/lancedb';
|
|
1257
|
+
import type { Document, DocumentMetadata } from '../types/document.js';
|
|
1258
|
+
import type { StoreId, DocumentId } from '../types/brands.js';
|
|
1259
|
+
import { createDocumentId } from '../types/brands.js';
|
|
1260
|
+
|
|
1261
|
+
interface LanceDocument {
|
|
1262
|
+
id: string;
|
|
1263
|
+
content: string;
|
|
1264
|
+
vector: number[];
|
|
1265
|
+
metadata: string; // JSON serialized
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
interface SearchHit {
|
|
1269
|
+
id: string;
|
|
1270
|
+
content: string;
|
|
1271
|
+
metadata: string;
|
|
1272
|
+
_distance: number;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
export class LanceStore {
|
|
1276
|
+
private connection: Connection | null = null;
|
|
1277
|
+
private tables: Map<string, Table> = new Map();
|
|
1278
|
+
private readonly dataDir: string;
|
|
1279
|
+
|
|
1280
|
+
constructor(dataDir: string) {
|
|
1281
|
+
this.dataDir = dataDir;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
async initialize(storeId: StoreId): Promise<void> {
|
|
1285
|
+
if (this.connection === null) {
|
|
1286
|
+
this.connection = await lancedb.connect(this.dataDir);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
const tableName = this.getTableName(storeId);
|
|
1290
|
+
const tableNames = await this.connection.tableNames();
|
|
1291
|
+
|
|
1292
|
+
if (!tableNames.includes(tableName)) {
|
|
1293
|
+
// Create table with initial schema
|
|
1294
|
+
const table = await this.connection.createTable(tableName, [
|
|
1295
|
+
{
|
|
1296
|
+
id: '__init__',
|
|
1297
|
+
content: '',
|
|
1298
|
+
vector: new Array(384).fill(0),
|
|
1299
|
+
metadata: '{}',
|
|
1300
|
+
},
|
|
1301
|
+
]);
|
|
1302
|
+
// Delete the init row
|
|
1303
|
+
await table.delete('id = "__init__"');
|
|
1304
|
+
this.tables.set(tableName, table);
|
|
1305
|
+
} else {
|
|
1306
|
+
const table = await this.connection.openTable(tableName);
|
|
1307
|
+
this.tables.set(tableName, table);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
async addDocuments(storeId: StoreId, documents: Document[]): Promise<void> {
|
|
1312
|
+
const table = await this.getTable(storeId);
|
|
1313
|
+
const lanceDocuments: LanceDocument[] = documents.map((doc) => ({
|
|
1314
|
+
id: doc.id,
|
|
1315
|
+
content: doc.content,
|
|
1316
|
+
vector: [...doc.vector],
|
|
1317
|
+
metadata: JSON.stringify(doc.metadata),
|
|
1318
|
+
}));
|
|
1319
|
+
await table.add(lanceDocuments);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
async deleteDocuments(storeId: StoreId, documentIds: DocumentId[]): Promise<void> {
|
|
1323
|
+
const table = await this.getTable(storeId);
|
|
1324
|
+
const idList = documentIds.map((id) => `"${id}"`).join(', ');
|
|
1325
|
+
await table.delete(`id IN (${idList})`);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
async search(
|
|
1329
|
+
storeId: StoreId,
|
|
1330
|
+
vector: number[],
|
|
1331
|
+
limit: number,
|
|
1332
|
+
threshold?: number
|
|
1333
|
+
): Promise<Array<{ id: DocumentId; content: string; score: number; metadata: DocumentMetadata }>> {
|
|
1334
|
+
const table = await this.getTable(storeId);
|
|
1335
|
+
let query = table.vectorSearch(vector).limit(limit);
|
|
1336
|
+
|
|
1337
|
+
if (threshold !== undefined) {
|
|
1338
|
+
query = query.distanceType('cosine');
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
const results = (await query.toArray()) as SearchHit[];
|
|
1342
|
+
|
|
1343
|
+
return results
|
|
1344
|
+
.filter((r) => {
|
|
1345
|
+
if (threshold === undefined) return true;
|
|
1346
|
+
const score = 1 - r._distance;
|
|
1347
|
+
return score >= threshold;
|
|
1348
|
+
})
|
|
1349
|
+
.map((r) => ({
|
|
1350
|
+
id: createDocumentId(r.id),
|
|
1351
|
+
content: r.content,
|
|
1352
|
+
score: 1 - r._distance,
|
|
1353
|
+
metadata: JSON.parse(r.metadata) as DocumentMetadata,
|
|
1354
|
+
}));
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
async deleteStore(storeId: StoreId): Promise<void> {
|
|
1358
|
+
const tableName = this.getTableName(storeId);
|
|
1359
|
+
if (this.connection !== null) {
|
|
1360
|
+
await this.connection.dropTable(tableName);
|
|
1361
|
+
this.tables.delete(tableName);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
private getTableName(storeId: StoreId): string {
|
|
1366
|
+
return `documents_${storeId}`;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
private async getTable(storeId: StoreId): Promise<Table> {
|
|
1370
|
+
const tableName = this.getTableName(storeId);
|
|
1371
|
+
let table = this.tables.get(tableName);
|
|
1372
|
+
if (table === undefined) {
|
|
1373
|
+
await this.initialize(storeId);
|
|
1374
|
+
table = this.tables.get(tableName);
|
|
1375
|
+
}
|
|
1376
|
+
if (table === undefined) {
|
|
1377
|
+
throw new Error(`Table not found for store: ${storeId}`);
|
|
1378
|
+
}
|
|
1379
|
+
return table;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
```
|
|
1383
|
+
|
|
1384
|
+
**Step 4: Run test to verify it passes**
|
|
1385
|
+
|
|
1386
|
+
```bash
|
|
1387
|
+
npm run test:run -- src/db/lance.test.ts
|
|
1388
|
+
```
|
|
1389
|
+
|
|
1390
|
+
Expected: PASS
|
|
1391
|
+
|
|
1392
|
+
**Step 5: Commit**
|
|
1393
|
+
|
|
1394
|
+
```bash
|
|
1395
|
+
git add src/db/lance.ts src/db/lance.test.ts
|
|
1396
|
+
git commit -m "feat: add LanceDB wrapper for vector storage"
|
|
1397
|
+
```
|
|
1398
|
+
|
|
1399
|
+
---
|
|
1400
|
+
|
|
1401
|
+
### Task 3.3: Update db barrel export
|
|
1402
|
+
|
|
1403
|
+
**Files:**
|
|
1404
|
+
- Modify: `src/db/index.ts`
|
|
1405
|
+
|
|
1406
|
+
**Step 1: Update db index**
|
|
1407
|
+
|
|
1408
|
+
Update `src/db/index.ts`:
|
|
1409
|
+
```typescript
|
|
1410
|
+
export { EmbeddingEngine } from './embeddings.js';
|
|
1411
|
+
export { LanceStore } from './lance.js';
|
|
1412
|
+
```
|
|
1413
|
+
|
|
1414
|
+
**Step 2: Run all db tests**
|
|
1415
|
+
|
|
1416
|
+
```bash
|
|
1417
|
+
npm run test:run -- src/db/
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
Expected: All tests pass
|
|
1421
|
+
|
|
1422
|
+
**Step 3: Commit**
|
|
1423
|
+
|
|
1424
|
+
```bash
|
|
1425
|
+
git add src/db/index.ts
|
|
1426
|
+
git commit -m "feat: add db barrel export"
|
|
1427
|
+
```
|
|
1428
|
+
|
|
1429
|
+
---
|
|
1430
|
+
|
|
1431
|
+
## Phase 4: Services Layer
|
|
1432
|
+
|
|
1433
|
+
### Task 4.1: Create ConfigService
|
|
1434
|
+
|
|
1435
|
+
**Files:**
|
|
1436
|
+
- Create: `src/services/config.service.ts`
|
|
1437
|
+
- Create: `src/services/config.service.test.ts`
|
|
1438
|
+
|
|
1439
|
+
**Step 1: Write the failing test**
|
|
1440
|
+
|
|
1441
|
+
Create `src/services/config.service.test.ts`:
|
|
1442
|
+
```typescript
|
|
1443
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
1444
|
+
import { ConfigService } from './config.service.js';
|
|
1445
|
+
import { rm, mkdtemp } from 'node:fs/promises';
|
|
1446
|
+
import { tmpdir } from 'node:os';
|
|
1447
|
+
import { join } from 'node:path';
|
|
1448
|
+
|
|
1449
|
+
describe('ConfigService', () => {
|
|
1450
|
+
let configService: ConfigService;
|
|
1451
|
+
let tempDir: string;
|
|
1452
|
+
let configPath: string;
|
|
1453
|
+
|
|
1454
|
+
beforeEach(async () => {
|
|
1455
|
+
tempDir = await mkdtemp(join(tmpdir(), 'config-test-'));
|
|
1456
|
+
configPath = join(tempDir, 'config.json');
|
|
1457
|
+
configService = new ConfigService(configPath, tempDir);
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
afterEach(async () => {
|
|
1461
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
it('loads default config when no file exists', async () => {
|
|
1465
|
+
const config = await configService.load();
|
|
1466
|
+
expect(config.version).toBe(1);
|
|
1467
|
+
expect(config.embedding.model).toBe('Xenova/all-MiniLM-L6-v2');
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
it('saves and loads config', async () => {
|
|
1471
|
+
const config = await configService.load();
|
|
1472
|
+
config.search.defaultLimit = 20;
|
|
1473
|
+
await configService.save(config);
|
|
1474
|
+
|
|
1475
|
+
const loaded = await configService.load();
|
|
1476
|
+
expect(loaded.search.defaultLimit).toBe(20);
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
it('resolves data directory path', async () => {
|
|
1480
|
+
const config = await configService.load();
|
|
1481
|
+
const dataDir = configService.resolveDataDir();
|
|
1482
|
+
expect(dataDir).toBe(tempDir);
|
|
1483
|
+
});
|
|
1484
|
+
});
|
|
1485
|
+
```
|
|
1486
|
+
|
|
1487
|
+
**Step 2: Run test to verify it fails**
|
|
1488
|
+
|
|
1489
|
+
```bash
|
|
1490
|
+
npm run test:run -- src/services/config.service.test.ts
|
|
1491
|
+
```
|
|
1492
|
+
|
|
1493
|
+
Expected: FAIL - module not found
|
|
1494
|
+
|
|
1495
|
+
**Step 3: Write minimal implementation**
|
|
1496
|
+
|
|
1497
|
+
Create `src/services/config.service.ts`:
|
|
1498
|
+
```typescript
|
|
1499
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
1500
|
+
import { dirname } from 'node:path';
|
|
1501
|
+
import { homedir } from 'node:os';
|
|
1502
|
+
import type { AppConfig } from '../types/config.js';
|
|
1503
|
+
import { DEFAULT_CONFIG } from '../types/config.js';
|
|
1504
|
+
|
|
1505
|
+
export class ConfigService {
|
|
1506
|
+
private readonly configPath: string;
|
|
1507
|
+
private readonly dataDir: string;
|
|
1508
|
+
private config: AppConfig | null = null;
|
|
1509
|
+
|
|
1510
|
+
constructor(
|
|
1511
|
+
configPath = `${homedir()}/.bluera/knowledge.json`,
|
|
1512
|
+
dataDir?: string
|
|
1513
|
+
) {
|
|
1514
|
+
this.configPath = configPath;
|
|
1515
|
+
this.dataDir = dataDir ?? this.expandPath(DEFAULT_CONFIG.dataDir);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
async load(): Promise<AppConfig> {
|
|
1519
|
+
if (this.config !== null) {
|
|
1520
|
+
return this.config;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
try {
|
|
1524
|
+
const content = await readFile(this.configPath, 'utf-8');
|
|
1525
|
+
this.config = { ...DEFAULT_CONFIG, ...JSON.parse(content) } as AppConfig;
|
|
1526
|
+
} catch {
|
|
1527
|
+
this.config = { ...DEFAULT_CONFIG };
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
return this.config;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
async save(config: AppConfig): Promise<void> {
|
|
1534
|
+
await mkdir(dirname(this.configPath), { recursive: true });
|
|
1535
|
+
await writeFile(this.configPath, JSON.stringify(config, null, 2));
|
|
1536
|
+
this.config = config;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
resolveDataDir(): string {
|
|
1540
|
+
return this.dataDir;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
private expandPath(path: string): string {
|
|
1544
|
+
if (path.startsWith('~')) {
|
|
1545
|
+
return path.replace('~', homedir());
|
|
1546
|
+
}
|
|
1547
|
+
return path;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
```
|
|
1551
|
+
|
|
1552
|
+
**Step 4: Run test to verify it passes**
|
|
1553
|
+
|
|
1554
|
+
```bash
|
|
1555
|
+
npm run test:run -- src/services/config.service.test.ts
|
|
1556
|
+
```
|
|
1557
|
+
|
|
1558
|
+
Expected: PASS
|
|
1559
|
+
|
|
1560
|
+
**Step 5: Commit**
|
|
1561
|
+
|
|
1562
|
+
```bash
|
|
1563
|
+
git add src/services/config.service.ts src/services/config.service.test.ts
|
|
1564
|
+
git commit -m "feat: add ConfigService"
|
|
1565
|
+
```
|
|
1566
|
+
|
|
1567
|
+
---
|
|
1568
|
+
|
|
1569
|
+
### Task 4.2: Create StoreService
|
|
1570
|
+
|
|
1571
|
+
**Files:**
|
|
1572
|
+
- Create: `src/services/store.service.ts`
|
|
1573
|
+
- Create: `src/services/store.service.test.ts`
|
|
1574
|
+
|
|
1575
|
+
**Step 1: Write the failing test**
|
|
1576
|
+
|
|
1577
|
+
Create `src/services/store.service.test.ts`:
|
|
1578
|
+
```typescript
|
|
1579
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
1580
|
+
import { StoreService } from './store.service.js';
|
|
1581
|
+
import { rm, mkdtemp } from 'node:fs/promises';
|
|
1582
|
+
import { tmpdir } from 'node:os';
|
|
1583
|
+
import { join } from 'node:path';
|
|
1584
|
+
|
|
1585
|
+
describe('StoreService', () => {
|
|
1586
|
+
let storeService: StoreService;
|
|
1587
|
+
let tempDir: string;
|
|
1588
|
+
|
|
1589
|
+
beforeEach(async () => {
|
|
1590
|
+
tempDir = await mkdtemp(join(tmpdir(), 'store-test-'));
|
|
1591
|
+
storeService = new StoreService(tempDir);
|
|
1592
|
+
await storeService.initialize();
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
afterEach(async () => {
|
|
1596
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
it('creates a file store', async () => {
|
|
1600
|
+
const result = await storeService.create({
|
|
1601
|
+
name: 'My Files',
|
|
1602
|
+
type: 'file',
|
|
1603
|
+
path: '/path/to/files',
|
|
1604
|
+
});
|
|
1605
|
+
|
|
1606
|
+
expect(result.success).toBe(true);
|
|
1607
|
+
if (result.success) {
|
|
1608
|
+
expect(result.data.name).toBe('My Files');
|
|
1609
|
+
expect(result.data.type).toBe('file');
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
it('lists all stores', async () => {
|
|
1614
|
+
await storeService.create({ name: 'Store 1', type: 'file', path: '/path/1' });
|
|
1615
|
+
await storeService.create({ name: 'Store 2', type: 'repo', path: '/path/2' });
|
|
1616
|
+
|
|
1617
|
+
const stores = await storeService.list();
|
|
1618
|
+
expect(stores).toHaveLength(2);
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
it('gets store by ID', async () => {
|
|
1622
|
+
const createResult = await storeService.create({
|
|
1623
|
+
name: 'Test Store',
|
|
1624
|
+
type: 'file',
|
|
1625
|
+
path: '/path/test',
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
if (!createResult.success) throw new Error('Create failed');
|
|
1629
|
+
|
|
1630
|
+
const store = await storeService.get(createResult.data.id);
|
|
1631
|
+
expect(store?.name).toBe('Test Store');
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
it('gets store by name', async () => {
|
|
1635
|
+
await storeService.create({
|
|
1636
|
+
name: 'Named Store',
|
|
1637
|
+
type: 'file',
|
|
1638
|
+
path: '/path/named',
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
const store = await storeService.getByName('Named Store');
|
|
1642
|
+
expect(store?.name).toBe('Named Store');
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
it('deletes a store', async () => {
|
|
1646
|
+
const createResult = await storeService.create({
|
|
1647
|
+
name: 'To Delete',
|
|
1648
|
+
type: 'file',
|
|
1649
|
+
path: '/path/delete',
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
if (!createResult.success) throw new Error('Create failed');
|
|
1653
|
+
|
|
1654
|
+
const deleteResult = await storeService.delete(createResult.data.id);
|
|
1655
|
+
expect(deleteResult.success).toBe(true);
|
|
1656
|
+
|
|
1657
|
+
const stores = await storeService.list();
|
|
1658
|
+
expect(stores).toHaveLength(0);
|
|
1659
|
+
});
|
|
1660
|
+
});
|
|
1661
|
+
```
|
|
1662
|
+
|
|
1663
|
+
**Step 2: Run test to verify it fails**
|
|
1664
|
+
|
|
1665
|
+
```bash
|
|
1666
|
+
npm run test:run -- src/services/store.service.test.ts
|
|
1667
|
+
```
|
|
1668
|
+
|
|
1669
|
+
Expected: FAIL - module not found
|
|
1670
|
+
|
|
1671
|
+
**Step 3: Write minimal implementation**
|
|
1672
|
+
|
|
1673
|
+
Create `src/services/store.service.ts`:
|
|
1674
|
+
```typescript
|
|
1675
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
1676
|
+
import { join } from 'node:path';
|
|
1677
|
+
import { randomUUID } from 'node:crypto';
|
|
1678
|
+
import type { Store, FileStore, RepoStore, WebStore, StoreType } from '../types/store.js';
|
|
1679
|
+
import type { StoreId } from '../types/brands.js';
|
|
1680
|
+
import { createStoreId } from '../types/brands.js';
|
|
1681
|
+
import type { Result } from '../types/result.js';
|
|
1682
|
+
import { ok, err } from '../types/result.js';
|
|
1683
|
+
|
|
1684
|
+
interface CreateStoreInput {
|
|
1685
|
+
name: string;
|
|
1686
|
+
type: StoreType;
|
|
1687
|
+
path?: string;
|
|
1688
|
+
url?: string;
|
|
1689
|
+
description?: string;
|
|
1690
|
+
tags?: string[];
|
|
1691
|
+
branch?: string;
|
|
1692
|
+
depth?: number;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
interface StoreRegistry {
|
|
1696
|
+
stores: Store[];
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
export class StoreService {
|
|
1700
|
+
private readonly dataDir: string;
|
|
1701
|
+
private registry: StoreRegistry = { stores: [] };
|
|
1702
|
+
|
|
1703
|
+
constructor(dataDir: string) {
|
|
1704
|
+
this.dataDir = dataDir;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
async initialize(): Promise<void> {
|
|
1708
|
+
await mkdir(this.dataDir, { recursive: true });
|
|
1709
|
+
await this.loadRegistry();
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
async create(input: CreateStoreInput): Promise<Result<Store>> {
|
|
1713
|
+
const existing = await this.getByName(input.name);
|
|
1714
|
+
if (existing !== undefined) {
|
|
1715
|
+
return err(new Error(`Store with name "${input.name}" already exists`));
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
const id = createStoreId(randomUUID());
|
|
1719
|
+
const now = new Date();
|
|
1720
|
+
|
|
1721
|
+
let store: Store;
|
|
1722
|
+
|
|
1723
|
+
switch (input.type) {
|
|
1724
|
+
case 'file':
|
|
1725
|
+
if (input.path === undefined) {
|
|
1726
|
+
return err(new Error('Path is required for file stores'));
|
|
1727
|
+
}
|
|
1728
|
+
store = {
|
|
1729
|
+
type: 'file',
|
|
1730
|
+
id,
|
|
1731
|
+
name: input.name,
|
|
1732
|
+
path: input.path,
|
|
1733
|
+
description: input.description,
|
|
1734
|
+
tags: input.tags,
|
|
1735
|
+
status: 'ready',
|
|
1736
|
+
createdAt: now,
|
|
1737
|
+
updatedAt: now,
|
|
1738
|
+
} satisfies FileStore;
|
|
1739
|
+
break;
|
|
1740
|
+
|
|
1741
|
+
case 'repo':
|
|
1742
|
+
if (input.path === undefined) {
|
|
1743
|
+
return err(new Error('Path is required for repo stores'));
|
|
1744
|
+
}
|
|
1745
|
+
store = {
|
|
1746
|
+
type: 'repo',
|
|
1747
|
+
id,
|
|
1748
|
+
name: input.name,
|
|
1749
|
+
path: input.path,
|
|
1750
|
+
branch: input.branch,
|
|
1751
|
+
description: input.description,
|
|
1752
|
+
tags: input.tags,
|
|
1753
|
+
status: 'ready',
|
|
1754
|
+
createdAt: now,
|
|
1755
|
+
updatedAt: now,
|
|
1756
|
+
} satisfies RepoStore;
|
|
1757
|
+
break;
|
|
1758
|
+
|
|
1759
|
+
case 'web':
|
|
1760
|
+
if (input.url === undefined) {
|
|
1761
|
+
return err(new Error('URL is required for web stores'));
|
|
1762
|
+
}
|
|
1763
|
+
store = {
|
|
1764
|
+
type: 'web',
|
|
1765
|
+
id,
|
|
1766
|
+
name: input.name,
|
|
1767
|
+
url: input.url,
|
|
1768
|
+
depth: input.depth ?? 1,
|
|
1769
|
+
description: input.description,
|
|
1770
|
+
tags: input.tags,
|
|
1771
|
+
status: 'ready',
|
|
1772
|
+
createdAt: now,
|
|
1773
|
+
updatedAt: now,
|
|
1774
|
+
} satisfies WebStore;
|
|
1775
|
+
break;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
this.registry.stores.push(store);
|
|
1779
|
+
await this.saveRegistry();
|
|
1780
|
+
|
|
1781
|
+
return ok(store);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
async list(type?: StoreType): Promise<Store[]> {
|
|
1785
|
+
if (type !== undefined) {
|
|
1786
|
+
return this.registry.stores.filter((s) => s.type === type);
|
|
1787
|
+
}
|
|
1788
|
+
return [...this.registry.stores];
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
async get(id: StoreId): Promise<Store | undefined> {
|
|
1792
|
+
return this.registry.stores.find((s) => s.id === id);
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
async getByName(name: string): Promise<Store | undefined> {
|
|
1796
|
+
return this.registry.stores.find((s) => s.name === name);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
async getByIdOrName(idOrName: string): Promise<Store | undefined> {
|
|
1800
|
+
return this.registry.stores.find((s) => s.id === idOrName || s.name === idOrName);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
async update(id: StoreId, updates: Partial<Pick<Store, 'name' | 'description' | 'tags'>>): Promise<Result<Store>> {
|
|
1804
|
+
const index = this.registry.stores.findIndex((s) => s.id === id);
|
|
1805
|
+
if (index === -1) {
|
|
1806
|
+
return err(new Error(`Store not found: ${id}`));
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
const store = this.registry.stores[index]!;
|
|
1810
|
+
const updated = {
|
|
1811
|
+
...store,
|
|
1812
|
+
...updates,
|
|
1813
|
+
updatedAt: new Date(),
|
|
1814
|
+
} as Store;
|
|
1815
|
+
|
|
1816
|
+
this.registry.stores[index] = updated;
|
|
1817
|
+
await this.saveRegistry();
|
|
1818
|
+
|
|
1819
|
+
return ok(updated);
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
async delete(id: StoreId): Promise<Result<void>> {
|
|
1823
|
+
const index = this.registry.stores.findIndex((s) => s.id === id);
|
|
1824
|
+
if (index === -1) {
|
|
1825
|
+
return err(new Error(`Store not found: ${id}`));
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
this.registry.stores.splice(index, 1);
|
|
1829
|
+
await this.saveRegistry();
|
|
1830
|
+
|
|
1831
|
+
return ok(undefined);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
private async loadRegistry(): Promise<void> {
|
|
1835
|
+
const registryPath = join(this.dataDir, 'stores.json');
|
|
1836
|
+
try {
|
|
1837
|
+
const content = await readFile(registryPath, 'utf-8');
|
|
1838
|
+
const data = JSON.parse(content) as { stores: Store[] };
|
|
1839
|
+
this.registry = {
|
|
1840
|
+
stores: data.stores.map((s) => ({
|
|
1841
|
+
...s,
|
|
1842
|
+
id: createStoreId(s.id),
|
|
1843
|
+
createdAt: new Date(s.createdAt),
|
|
1844
|
+
updatedAt: new Date(s.updatedAt),
|
|
1845
|
+
})),
|
|
1846
|
+
};
|
|
1847
|
+
} catch {
|
|
1848
|
+
this.registry = { stores: [] };
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
private async saveRegistry(): Promise<void> {
|
|
1853
|
+
const registryPath = join(this.dataDir, 'stores.json');
|
|
1854
|
+
await writeFile(registryPath, JSON.stringify(this.registry, null, 2));
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
```
|
|
1858
|
+
|
|
1859
|
+
**Step 4: Run test to verify it passes**
|
|
1860
|
+
|
|
1861
|
+
```bash
|
|
1862
|
+
npm run test:run -- src/services/store.service.test.ts
|
|
1863
|
+
```
|
|
1864
|
+
|
|
1865
|
+
Expected: PASS
|
|
1866
|
+
|
|
1867
|
+
**Step 5: Commit**
|
|
1868
|
+
|
|
1869
|
+
```bash
|
|
1870
|
+
git add src/services/store.service.ts src/services/store.service.test.ts
|
|
1871
|
+
git commit -m "feat: add StoreService for store CRUD operations"
|
|
1872
|
+
```
|
|
1873
|
+
|
|
1874
|
+
---
|
|
1875
|
+
|
|
1876
|
+
### Task 4.3: Create SearchService
|
|
1877
|
+
|
|
1878
|
+
**Files:**
|
|
1879
|
+
- Create: `src/services/search.service.ts`
|
|
1880
|
+
- Create: `src/services/search.service.test.ts`
|
|
1881
|
+
|
|
1882
|
+
**Step 1: Write the failing test**
|
|
1883
|
+
|
|
1884
|
+
Create `src/services/search.service.test.ts`:
|
|
1885
|
+
```typescript
|
|
1886
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
1887
|
+
import { SearchService } from './search.service.js';
|
|
1888
|
+
import { LanceStore } from '../db/lance.js';
|
|
1889
|
+
import { EmbeddingEngine } from '../db/embeddings.js';
|
|
1890
|
+
import { createStoreId, createDocumentId } from '../types/brands.js';
|
|
1891
|
+
import { rm, mkdtemp } from 'node:fs/promises';
|
|
1892
|
+
import { tmpdir } from 'node:os';
|
|
1893
|
+
import { join } from 'node:path';
|
|
1894
|
+
|
|
1895
|
+
describe('SearchService', () => {
|
|
1896
|
+
let searchService: SearchService;
|
|
1897
|
+
let lanceStore: LanceStore;
|
|
1898
|
+
let embeddingEngine: EmbeddingEngine;
|
|
1899
|
+
let tempDir: string;
|
|
1900
|
+
const storeId = createStoreId('test-store');
|
|
1901
|
+
|
|
1902
|
+
beforeAll(async () => {
|
|
1903
|
+
tempDir = await mkdtemp(join(tmpdir(), 'search-test-'));
|
|
1904
|
+
lanceStore = new LanceStore(tempDir);
|
|
1905
|
+
embeddingEngine = new EmbeddingEngine();
|
|
1906
|
+
|
|
1907
|
+
await embeddingEngine.initialize();
|
|
1908
|
+
await lanceStore.initialize(storeId);
|
|
1909
|
+
|
|
1910
|
+
// Add test documents
|
|
1911
|
+
const texts = [
|
|
1912
|
+
'TypeScript is a typed superset of JavaScript',
|
|
1913
|
+
'Python is great for machine learning',
|
|
1914
|
+
'React is a JavaScript library for building user interfaces',
|
|
1915
|
+
];
|
|
1916
|
+
|
|
1917
|
+
for (let i = 0; i < texts.length; i++) {
|
|
1918
|
+
const text = texts[i]!;
|
|
1919
|
+
const vector = await embeddingEngine.embed(text);
|
|
1920
|
+
await lanceStore.addDocuments(storeId, [
|
|
1921
|
+
{
|
|
1922
|
+
id: createDocumentId(`doc-${i}`),
|
|
1923
|
+
content: text,
|
|
1924
|
+
vector,
|
|
1925
|
+
metadata: {
|
|
1926
|
+
type: 'file',
|
|
1927
|
+
storeId,
|
|
1928
|
+
indexedAt: new Date(),
|
|
1929
|
+
},
|
|
1930
|
+
},
|
|
1931
|
+
]);
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
searchService = new SearchService(lanceStore, embeddingEngine);
|
|
1935
|
+
}, 120000);
|
|
1936
|
+
|
|
1937
|
+
afterAll(async () => {
|
|
1938
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1939
|
+
});
|
|
1940
|
+
|
|
1941
|
+
it('searches with vector mode', async () => {
|
|
1942
|
+
const results = await searchService.search({
|
|
1943
|
+
query: 'JavaScript programming',
|
|
1944
|
+
stores: [storeId],
|
|
1945
|
+
mode: 'vector',
|
|
1946
|
+
limit: 10,
|
|
1947
|
+
});
|
|
1948
|
+
|
|
1949
|
+
expect(results.results.length).toBeGreaterThan(0);
|
|
1950
|
+
expect(results.mode).toBe('vector');
|
|
1951
|
+
});
|
|
1952
|
+
|
|
1953
|
+
it('returns results with scores', async () => {
|
|
1954
|
+
const results = await searchService.search({
|
|
1955
|
+
query: 'machine learning Python',
|
|
1956
|
+
stores: [storeId],
|
|
1957
|
+
mode: 'vector',
|
|
1958
|
+
limit: 10,
|
|
1959
|
+
});
|
|
1960
|
+
|
|
1961
|
+
expect(results.results[0]?.score).toBeGreaterThan(0);
|
|
1962
|
+
expect(results.results[0]?.content).toContain('Python');
|
|
1963
|
+
});
|
|
1964
|
+
});
|
|
1965
|
+
```
|
|
1966
|
+
|
|
1967
|
+
**Step 2: Run test to verify it fails**
|
|
1968
|
+
|
|
1969
|
+
```bash
|
|
1970
|
+
npm run test:run -- src/services/search.service.test.ts
|
|
1971
|
+
```
|
|
1972
|
+
|
|
1973
|
+
Expected: FAIL - module not found
|
|
1974
|
+
|
|
1975
|
+
**Step 3: Write minimal implementation**
|
|
1976
|
+
|
|
1977
|
+
Create `src/services/search.service.ts`:
|
|
1978
|
+
```typescript
|
|
1979
|
+
import type { LanceStore } from '../db/lance.js';
|
|
1980
|
+
import type { EmbeddingEngine } from '../db/embeddings.js';
|
|
1981
|
+
import type { SearchQuery, SearchResponse, SearchResult } from '../types/search.js';
|
|
1982
|
+
import type { StoreId } from '../types/brands.js';
|
|
1983
|
+
|
|
1984
|
+
export class SearchService {
|
|
1985
|
+
private readonly lanceStore: LanceStore;
|
|
1986
|
+
private readonly embeddingEngine: EmbeddingEngine;
|
|
1987
|
+
|
|
1988
|
+
constructor(lanceStore: LanceStore, embeddingEngine: EmbeddingEngine) {
|
|
1989
|
+
this.lanceStore = lanceStore;
|
|
1990
|
+
this.embeddingEngine = embeddingEngine;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
async search(query: SearchQuery): Promise<SearchResponse> {
|
|
1994
|
+
const startTime = Date.now();
|
|
1995
|
+
const mode = query.mode ?? 'hybrid';
|
|
1996
|
+
const limit = query.limit ?? 10;
|
|
1997
|
+
const stores = query.stores ?? [];
|
|
1998
|
+
|
|
1999
|
+
let allResults: SearchResult[] = [];
|
|
2000
|
+
|
|
2001
|
+
if (mode === 'vector' || mode === 'hybrid') {
|
|
2002
|
+
const queryVector = await this.embeddingEngine.embed(query.query);
|
|
2003
|
+
|
|
2004
|
+
for (const storeId of stores) {
|
|
2005
|
+
const results = await this.lanceStore.search(
|
|
2006
|
+
storeId,
|
|
2007
|
+
queryVector,
|
|
2008
|
+
limit,
|
|
2009
|
+
query.threshold
|
|
2010
|
+
);
|
|
2011
|
+
|
|
2012
|
+
allResults.push(
|
|
2013
|
+
...results.map((r) => ({
|
|
2014
|
+
id: r.id,
|
|
2015
|
+
score: r.score,
|
|
2016
|
+
content: r.content,
|
|
2017
|
+
metadata: r.metadata,
|
|
2018
|
+
}))
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// Sort by score descending
|
|
2024
|
+
allResults.sort((a, b) => b.score - a.score);
|
|
2025
|
+
|
|
2026
|
+
// Limit results
|
|
2027
|
+
allResults = allResults.slice(0, limit);
|
|
2028
|
+
|
|
2029
|
+
return {
|
|
2030
|
+
query: query.query,
|
|
2031
|
+
mode,
|
|
2032
|
+
stores,
|
|
2033
|
+
results: allResults,
|
|
2034
|
+
totalResults: allResults.length,
|
|
2035
|
+
timeMs: Date.now() - startTime,
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
async searchAllStores(
|
|
2040
|
+
query: SearchQuery,
|
|
2041
|
+
storeIds: StoreId[]
|
|
2042
|
+
): Promise<SearchResponse> {
|
|
2043
|
+
return this.search({
|
|
2044
|
+
...query,
|
|
2045
|
+
stores: storeIds,
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
```
|
|
2050
|
+
|
|
2051
|
+
**Step 4: Run test to verify it passes**
|
|
2052
|
+
|
|
2053
|
+
```bash
|
|
2054
|
+
npm run test:run -- src/services/search.service.test.ts
|
|
2055
|
+
```
|
|
2056
|
+
|
|
2057
|
+
Expected: PASS
|
|
2058
|
+
|
|
2059
|
+
**Step 5: Commit**
|
|
2060
|
+
|
|
2061
|
+
```bash
|
|
2062
|
+
git add src/services/search.service.ts src/services/search.service.test.ts
|
|
2063
|
+
git commit -m "feat: add SearchService for vector search"
|
|
2064
|
+
```
|
|
2065
|
+
|
|
2066
|
+
---
|
|
2067
|
+
|
|
2068
|
+
### Task 4.4: Create IndexService (simplified)
|
|
2069
|
+
|
|
2070
|
+
**Files:**
|
|
2071
|
+
- Create: `src/services/index.service.ts`
|
|
2072
|
+
- Create: `src/services/index.service.test.ts`
|
|
2073
|
+
|
|
2074
|
+
**Step 1: Write the failing test**
|
|
2075
|
+
|
|
2076
|
+
Create `src/services/index.service.test.ts`:
|
|
2077
|
+
```typescript
|
|
2078
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2079
|
+
import { IndexService } from './index.service.js';
|
|
2080
|
+
import { LanceStore } from '../db/lance.js';
|
|
2081
|
+
import { EmbeddingEngine } from '../db/embeddings.js';
|
|
2082
|
+
import { createStoreId } from '../types/brands.js';
|
|
2083
|
+
import { rm, mkdtemp, writeFile, mkdir } from 'node:fs/promises';
|
|
2084
|
+
import { tmpdir } from 'node:os';
|
|
2085
|
+
import { join } from 'node:path';
|
|
2086
|
+
import type { FileStore } from '../types/store.js';
|
|
2087
|
+
|
|
2088
|
+
describe('IndexService', () => {
|
|
2089
|
+
let indexService: IndexService;
|
|
2090
|
+
let lanceStore: LanceStore;
|
|
2091
|
+
let embeddingEngine: EmbeddingEngine;
|
|
2092
|
+
let tempDir: string;
|
|
2093
|
+
let testFilesDir: string;
|
|
2094
|
+
const storeId = createStoreId('test-store');
|
|
2095
|
+
|
|
2096
|
+
beforeAll(async () => {
|
|
2097
|
+
tempDir = await mkdtemp(join(tmpdir(), 'index-test-'));
|
|
2098
|
+
testFilesDir = join(tempDir, 'files');
|
|
2099
|
+
await mkdir(testFilesDir, { recursive: true });
|
|
2100
|
+
|
|
2101
|
+
// Create test files
|
|
2102
|
+
await writeFile(join(testFilesDir, 'test1.txt'), 'Hello world, this is a test file.');
|
|
2103
|
+
await writeFile(join(testFilesDir, 'test2.md'), '# Heading\n\nSome markdown content here.');
|
|
2104
|
+
|
|
2105
|
+
lanceStore = new LanceStore(tempDir);
|
|
2106
|
+
embeddingEngine = new EmbeddingEngine();
|
|
2107
|
+
|
|
2108
|
+
await embeddingEngine.initialize();
|
|
2109
|
+
await lanceStore.initialize(storeId);
|
|
2110
|
+
|
|
2111
|
+
indexService = new IndexService(lanceStore, embeddingEngine);
|
|
2112
|
+
}, 120000);
|
|
2113
|
+
|
|
2114
|
+
afterAll(async () => {
|
|
2115
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
2116
|
+
});
|
|
2117
|
+
|
|
2118
|
+
it('indexes a file store', async () => {
|
|
2119
|
+
const store: FileStore = {
|
|
2120
|
+
type: 'file',
|
|
2121
|
+
id: storeId,
|
|
2122
|
+
name: 'Test Store',
|
|
2123
|
+
path: testFilesDir,
|
|
2124
|
+
createdAt: new Date(),
|
|
2125
|
+
updatedAt: new Date(),
|
|
2126
|
+
};
|
|
2127
|
+
|
|
2128
|
+
const result = await indexService.indexStore(store);
|
|
2129
|
+
|
|
2130
|
+
expect(result.success).toBe(true);
|
|
2131
|
+
if (result.success) {
|
|
2132
|
+
expect(result.data.documentsIndexed).toBeGreaterThan(0);
|
|
2133
|
+
}
|
|
2134
|
+
});
|
|
2135
|
+
});
|
|
2136
|
+
```
|
|
2137
|
+
|
|
2138
|
+
**Step 2: Run test to verify it fails**
|
|
2139
|
+
|
|
2140
|
+
```bash
|
|
2141
|
+
npm run test:run -- src/services/index.service.test.ts
|
|
2142
|
+
```
|
|
2143
|
+
|
|
2144
|
+
Expected: FAIL - module not found
|
|
2145
|
+
|
|
2146
|
+
**Step 3: Write minimal implementation**
|
|
2147
|
+
|
|
2148
|
+
Create `src/services/index.service.ts`:
|
|
2149
|
+
```typescript
|
|
2150
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2151
|
+
import { join, extname } from 'node:path';
|
|
2152
|
+
import { createHash } from 'node:crypto';
|
|
2153
|
+
import type { LanceStore } from '../db/lance.js';
|
|
2154
|
+
import type { EmbeddingEngine } from '../db/embeddings.js';
|
|
2155
|
+
import type { Store, FileStore, RepoStore } from '../types/store.js';
|
|
2156
|
+
import type { Document } from '../types/document.js';
|
|
2157
|
+
import { createDocumentId } from '../types/brands.js';
|
|
2158
|
+
import type { Result } from '../types/result.js';
|
|
2159
|
+
import { ok, err } from '../types/result.js';
|
|
2160
|
+
|
|
2161
|
+
interface IndexResult {
|
|
2162
|
+
documentsIndexed: number;
|
|
2163
|
+
chunksCreated: number;
|
|
2164
|
+
timeMs: number;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
const TEXT_EXTENSIONS = new Set([
|
|
2168
|
+
'.txt', '.md', '.js', '.ts', '.jsx', '.tsx', '.json', '.yaml', '.yml',
|
|
2169
|
+
'.html', '.css', '.scss', '.less', '.py', '.rb', '.go', '.rs', '.java',
|
|
2170
|
+
'.c', '.cpp', '.h', '.hpp', '.sh', '.bash', '.zsh', '.sql', '.xml',
|
|
2171
|
+
]);
|
|
2172
|
+
|
|
2173
|
+
export class IndexService {
|
|
2174
|
+
private readonly lanceStore: LanceStore;
|
|
2175
|
+
private readonly embeddingEngine: EmbeddingEngine;
|
|
2176
|
+
|
|
2177
|
+
constructor(lanceStore: LanceStore, embeddingEngine: EmbeddingEngine) {
|
|
2178
|
+
this.lanceStore = lanceStore;
|
|
2179
|
+
this.embeddingEngine = embeddingEngine;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
async indexStore(store: Store): Promise<Result<IndexResult>> {
|
|
2183
|
+
const startTime = Date.now();
|
|
2184
|
+
|
|
2185
|
+
try {
|
|
2186
|
+
if (store.type === 'file' || store.type === 'repo') {
|
|
2187
|
+
return await this.indexFileStore(store);
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
return err(new Error(`Indexing not supported for store type: ${store.type}`));
|
|
2191
|
+
} catch (error) {
|
|
2192
|
+
return err(error instanceof Error ? error : new Error(String(error)));
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
private async indexFileStore(store: FileStore | RepoStore): Promise<Result<IndexResult>> {
|
|
2197
|
+
const startTime = Date.now();
|
|
2198
|
+
const files = await this.scanDirectory(store.path);
|
|
2199
|
+
const documents: Document[] = [];
|
|
2200
|
+
|
|
2201
|
+
for (const filePath of files) {
|
|
2202
|
+
const content = await readFile(filePath, 'utf-8');
|
|
2203
|
+
const vector = await this.embeddingEngine.embed(content);
|
|
2204
|
+
const fileHash = createHash('md5').update(content).digest('hex');
|
|
2205
|
+
|
|
2206
|
+
const doc: Document = {
|
|
2207
|
+
id: createDocumentId(`${store.id}-${fileHash}`),
|
|
2208
|
+
content,
|
|
2209
|
+
vector,
|
|
2210
|
+
metadata: {
|
|
2211
|
+
type: 'file',
|
|
2212
|
+
storeId: store.id,
|
|
2213
|
+
path: filePath,
|
|
2214
|
+
indexedAt: new Date(),
|
|
2215
|
+
fileHash,
|
|
2216
|
+
},
|
|
2217
|
+
};
|
|
2218
|
+
|
|
2219
|
+
documents.push(doc);
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
if (documents.length > 0) {
|
|
2223
|
+
await this.lanceStore.addDocuments(store.id, documents);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
return ok({
|
|
2227
|
+
documentsIndexed: documents.length,
|
|
2228
|
+
chunksCreated: documents.length, // Simplified - no chunking yet
|
|
2229
|
+
timeMs: Date.now() - startTime,
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
private async scanDirectory(dir: string): Promise<string[]> {
|
|
2234
|
+
const files: string[] = [];
|
|
2235
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
2236
|
+
|
|
2237
|
+
for (const entry of entries) {
|
|
2238
|
+
const fullPath = join(dir, entry.name);
|
|
2239
|
+
|
|
2240
|
+
if (entry.isDirectory()) {
|
|
2241
|
+
// Skip common ignored directories
|
|
2242
|
+
if (!['node_modules', '.git', 'dist', 'build'].includes(entry.name)) {
|
|
2243
|
+
files.push(...(await this.scanDirectory(fullPath)));
|
|
2244
|
+
}
|
|
2245
|
+
} else if (entry.isFile()) {
|
|
2246
|
+
const ext = extname(entry.name).toLowerCase();
|
|
2247
|
+
if (TEXT_EXTENSIONS.has(ext)) {
|
|
2248
|
+
files.push(fullPath);
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
return files;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
```
|
|
2257
|
+
|
|
2258
|
+
**Step 4: Run test to verify it passes**
|
|
2259
|
+
|
|
2260
|
+
```bash
|
|
2261
|
+
npm run test:run -- src/services/index.service.test.ts
|
|
2262
|
+
```
|
|
2263
|
+
|
|
2264
|
+
Expected: PASS
|
|
2265
|
+
|
|
2266
|
+
**Step 5: Commit**
|
|
2267
|
+
|
|
2268
|
+
```bash
|
|
2269
|
+
git add src/services/index.service.ts src/services/index.service.test.ts
|
|
2270
|
+
git commit -m "feat: add IndexService for file indexing"
|
|
2271
|
+
```
|
|
2272
|
+
|
|
2273
|
+
---
|
|
2274
|
+
|
|
2275
|
+
### Task 4.5: Create ServiceContainer
|
|
2276
|
+
|
|
2277
|
+
**Files:**
|
|
2278
|
+
- Modify: `src/services/index.ts`
|
|
2279
|
+
|
|
2280
|
+
**Step 1: Create ServiceContainer**
|
|
2281
|
+
|
|
2282
|
+
Update `src/services/index.ts`:
|
|
2283
|
+
```typescript
|
|
2284
|
+
import { ConfigService } from './config.service.js';
|
|
2285
|
+
import { StoreService } from './store.service.js';
|
|
2286
|
+
import { SearchService } from './search.service.js';
|
|
2287
|
+
import { IndexService } from './index.service.js';
|
|
2288
|
+
import { LanceStore } from '../db/lance.js';
|
|
2289
|
+
import { EmbeddingEngine } from '../db/embeddings.js';
|
|
2290
|
+
|
|
2291
|
+
export { ConfigService } from './config.service.js';
|
|
2292
|
+
export { StoreService } from './store.service.js';
|
|
2293
|
+
export { SearchService } from './search.service.js';
|
|
2294
|
+
export { IndexService } from './index.service.js';
|
|
2295
|
+
|
|
2296
|
+
export interface ServiceContainer {
|
|
2297
|
+
config: ConfigService;
|
|
2298
|
+
store: StoreService;
|
|
2299
|
+
search: SearchService;
|
|
2300
|
+
index: IndexService;
|
|
2301
|
+
lance: LanceStore;
|
|
2302
|
+
embeddings: EmbeddingEngine;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
export async function createServices(
|
|
2306
|
+
configPath?: string,
|
|
2307
|
+
dataDir?: string
|
|
2308
|
+
): Promise<ServiceContainer> {
|
|
2309
|
+
const config = new ConfigService(configPath, dataDir);
|
|
2310
|
+
const appConfig = await config.load();
|
|
2311
|
+
const resolvedDataDir = config.resolveDataDir();
|
|
2312
|
+
|
|
2313
|
+
const lance = new LanceStore(resolvedDataDir);
|
|
2314
|
+
const embeddings = new EmbeddingEngine(
|
|
2315
|
+
appConfig.embedding.model,
|
|
2316
|
+
appConfig.embedding.dimensions
|
|
2317
|
+
);
|
|
2318
|
+
|
|
2319
|
+
await embeddings.initialize();
|
|
2320
|
+
|
|
2321
|
+
const store = new StoreService(resolvedDataDir);
|
|
2322
|
+
await store.initialize();
|
|
2323
|
+
|
|
2324
|
+
const search = new SearchService(lance, embeddings);
|
|
2325
|
+
const index = new IndexService(lance, embeddings);
|
|
2326
|
+
|
|
2327
|
+
return {
|
|
2328
|
+
config,
|
|
2329
|
+
store,
|
|
2330
|
+
search,
|
|
2331
|
+
index,
|
|
2332
|
+
lance,
|
|
2333
|
+
embeddings,
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
```
|
|
2337
|
+
|
|
2338
|
+
**Step 2: Commit**
|
|
2339
|
+
|
|
2340
|
+
```bash
|
|
2341
|
+
git add src/services/index.ts
|
|
2342
|
+
git commit -m "feat: add ServiceContainer for dependency injection"
|
|
2343
|
+
```
|
|
2344
|
+
|
|
2345
|
+
---
|
|
2346
|
+
|
|
2347
|
+
## Phase 5: CLI Layer
|
|
2348
|
+
|
|
2349
|
+
### Task 5.1: Create CLI framework
|
|
2350
|
+
|
|
2351
|
+
**Files:**
|
|
2352
|
+
- Create: `src/cli/program.ts`
|
|
2353
|
+
- Modify: `src/index.ts`
|
|
2354
|
+
|
|
2355
|
+
**Step 1: Create CLI program**
|
|
2356
|
+
|
|
2357
|
+
Create `src/cli/program.ts`:
|
|
2358
|
+
```typescript
|
|
2359
|
+
import { Command } from 'commander';
|
|
2360
|
+
|
|
2361
|
+
export function createProgram(): Command {
|
|
2362
|
+
const program = new Command();
|
|
2363
|
+
|
|
2364
|
+
program
|
|
2365
|
+
.name('bluera-knowledge')
|
|
2366
|
+
.description('CLI tool for managing knowledge stores with semantic search')
|
|
2367
|
+
.version('0.1.0');
|
|
2368
|
+
|
|
2369
|
+
program
|
|
2370
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
2371
|
+
.option('-d, --data-dir <path>', 'Data directory')
|
|
2372
|
+
.option('-f, --format <format>', 'Output format: json | table | plain', 'table')
|
|
2373
|
+
.option('-q, --quiet', 'Suppress non-essential output')
|
|
2374
|
+
.option('-v, --verbose', 'Enable verbose logging');
|
|
2375
|
+
|
|
2376
|
+
return program;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
export interface GlobalOptions {
|
|
2380
|
+
config?: string;
|
|
2381
|
+
dataDir?: string;
|
|
2382
|
+
format: 'json' | 'table' | 'plain';
|
|
2383
|
+
quiet?: boolean;
|
|
2384
|
+
verbose?: boolean;
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
export function getGlobalOptions(program: Command): GlobalOptions {
|
|
2388
|
+
const opts = program.opts<GlobalOptions>();
|
|
2389
|
+
return {
|
|
2390
|
+
config: opts.config,
|
|
2391
|
+
dataDir: opts.dataDir,
|
|
2392
|
+
format: opts.format ?? 'table',
|
|
2393
|
+
quiet: opts.quiet,
|
|
2394
|
+
verbose: opts.verbose,
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
```
|
|
2398
|
+
|
|
2399
|
+
**Step 2: Update entry point**
|
|
2400
|
+
|
|
2401
|
+
Update `src/index.ts`:
|
|
2402
|
+
```typescript
|
|
2403
|
+
#!/usr/bin/env node
|
|
2404
|
+
|
|
2405
|
+
import { createProgram } from './cli/program.js';
|
|
2406
|
+
|
|
2407
|
+
const program = createProgram();
|
|
2408
|
+
|
|
2409
|
+
program.parse();
|
|
2410
|
+
```
|
|
2411
|
+
|
|
2412
|
+
**Step 3: Verify CLI works**
|
|
2413
|
+
|
|
2414
|
+
```bash
|
|
2415
|
+
npm run build && node dist/index.js --help
|
|
2416
|
+
```
|
|
2417
|
+
|
|
2418
|
+
Expected: Help output with description and options
|
|
2419
|
+
|
|
2420
|
+
**Step 4: Commit**
|
|
2421
|
+
|
|
2422
|
+
```bash
|
|
2423
|
+
git add src/cli/program.ts src/index.ts
|
|
2424
|
+
git commit -m "feat: add CLI framework with Commander.js"
|
|
2425
|
+
```
|
|
2426
|
+
|
|
2427
|
+
---
|
|
2428
|
+
|
|
2429
|
+
### Task 5.2: Add store commands
|
|
2430
|
+
|
|
2431
|
+
**Files:**
|
|
2432
|
+
- Create: `src/cli/commands/store.ts`
|
|
2433
|
+
- Modify: `src/index.ts`
|
|
2434
|
+
|
|
2435
|
+
**Step 1: Create store commands**
|
|
2436
|
+
|
|
2437
|
+
Create `src/cli/commands/store.ts`:
|
|
2438
|
+
```typescript
|
|
2439
|
+
import { Command } from 'commander';
|
|
2440
|
+
import { createServices } from '../../services/index.js';
|
|
2441
|
+
import type { GlobalOptions } from '../program.js';
|
|
2442
|
+
import type { StoreType } from '../../types/store.js';
|
|
2443
|
+
|
|
2444
|
+
export function createStoreCommand(getOptions: () => GlobalOptions): Command {
|
|
2445
|
+
const store = new Command('store').description('Manage knowledge stores');
|
|
2446
|
+
|
|
2447
|
+
store
|
|
2448
|
+
.command('list')
|
|
2449
|
+
.description('List all stores')
|
|
2450
|
+
.option('-t, --type <type>', 'Filter by store type (file, repo, web)')
|
|
2451
|
+
.action(async (options: { type?: StoreType }) => {
|
|
2452
|
+
const globalOpts = getOptions();
|
|
2453
|
+
const services = await createServices(globalOpts.config, globalOpts.dataDir);
|
|
2454
|
+
const stores = await services.store.list(options.type);
|
|
2455
|
+
|
|
2456
|
+
if (globalOpts.format === 'json') {
|
|
2457
|
+
console.log(JSON.stringify(stores, null, 2));
|
|
2458
|
+
} else {
|
|
2459
|
+
if (stores.length === 0) {
|
|
2460
|
+
console.log('No stores found.');
|
|
2461
|
+
} else {
|
|
2462
|
+
console.log('\nStores:\n');
|
|
2463
|
+
for (const s of stores) {
|
|
2464
|
+
console.log(` ${s.name} (${s.type}) - ${s.id}`);
|
|
2465
|
+
}
|
|
2466
|
+
console.log('');
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
});
|
|
2470
|
+
|
|
2471
|
+
store
|
|
2472
|
+
.command('create <name>')
|
|
2473
|
+
.description('Create a new store')
|
|
2474
|
+
.requiredOption('-t, --type <type>', 'Store type (file, repo, web)')
|
|
2475
|
+
.requiredOption('-s, --source <path>', 'Source path or URL')
|
|
2476
|
+
.option('-d, --description <desc>', 'Store description')
|
|
2477
|
+
.option('--tags <tags>', 'Comma-separated tags')
|
|
2478
|
+
.action(async (name: string, options: {
|
|
2479
|
+
type: StoreType;
|
|
2480
|
+
source: string;
|
|
2481
|
+
description?: string;
|
|
2482
|
+
tags?: string;
|
|
2483
|
+
}) => {
|
|
2484
|
+
const globalOpts = getOptions();
|
|
2485
|
+
const services = await createServices(globalOpts.config, globalOpts.dataDir);
|
|
2486
|
+
|
|
2487
|
+
const result = await services.store.create({
|
|
2488
|
+
name,
|
|
2489
|
+
type: options.type,
|
|
2490
|
+
path: options.type !== 'web' ? options.source : undefined,
|
|
2491
|
+
url: options.type === 'web' ? options.source : undefined,
|
|
2492
|
+
description: options.description,
|
|
2493
|
+
tags: options.tags?.split(',').map((t) => t.trim()),
|
|
2494
|
+
});
|
|
2495
|
+
|
|
2496
|
+
if (result.success) {
|
|
2497
|
+
if (globalOpts.format === 'json') {
|
|
2498
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
2499
|
+
} else {
|
|
2500
|
+
console.log(`\nCreated store: ${result.data.name} (${result.data.id})\n`);
|
|
2501
|
+
}
|
|
2502
|
+
} else {
|
|
2503
|
+
console.error(`Error: ${result.error.message}`);
|
|
2504
|
+
process.exit(1);
|
|
2505
|
+
}
|
|
2506
|
+
});
|
|
2507
|
+
|
|
2508
|
+
store
|
|
2509
|
+
.command('info <store>')
|
|
2510
|
+
.description('Show store details')
|
|
2511
|
+
.action(async (storeIdOrName: string) => {
|
|
2512
|
+
const globalOpts = getOptions();
|
|
2513
|
+
const services = await createServices(globalOpts.config, globalOpts.dataDir);
|
|
2514
|
+
const s = await services.store.getByIdOrName(storeIdOrName);
|
|
2515
|
+
|
|
2516
|
+
if (s === undefined) {
|
|
2517
|
+
console.error(`Store not found: ${storeIdOrName}`);
|
|
2518
|
+
process.exit(3);
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
if (globalOpts.format === 'json') {
|
|
2522
|
+
console.log(JSON.stringify(s, null, 2));
|
|
2523
|
+
} else {
|
|
2524
|
+
console.log(`\nStore: ${s.name}`);
|
|
2525
|
+
console.log(` ID: ${s.id}`);
|
|
2526
|
+
console.log(` Type: ${s.type}`);
|
|
2527
|
+
if ('path' in s) console.log(` Path: ${s.path}`);
|
|
2528
|
+
if ('url' in s) console.log(` URL: ${s.url}`);
|
|
2529
|
+
if (s.description !== undefined) console.log(` Description: ${s.description}`);
|
|
2530
|
+
console.log(` Created: ${s.createdAt.toISOString()}`);
|
|
2531
|
+
console.log(` Updated: ${s.updatedAt.toISOString()}`);
|
|
2532
|
+
console.log('');
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
|
|
2536
|
+
store
|
|
2537
|
+
.command('delete <store>')
|
|
2538
|
+
.description('Delete a store')
|
|
2539
|
+
.option('-f, --force', 'Skip confirmation')
|
|
2540
|
+
.action(async (storeIdOrName: string, options: { force?: boolean }) => {
|
|
2541
|
+
const globalOpts = getOptions();
|
|
2542
|
+
const services = await createServices(globalOpts.config, globalOpts.dataDir);
|
|
2543
|
+
const s = await services.store.getByIdOrName(storeIdOrName);
|
|
2544
|
+
|
|
2545
|
+
if (s === undefined) {
|
|
2546
|
+
console.error(`Store not found: ${storeIdOrName}`);
|
|
2547
|
+
process.exit(3);
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
const result = await services.store.delete(s.id);
|
|
2551
|
+
|
|
2552
|
+
if (result.success) {
|
|
2553
|
+
console.log(`Deleted store: ${s.name}`);
|
|
2554
|
+
} else {
|
|
2555
|
+
console.error(`Error: ${result.error.message}`);
|
|
2556
|
+
process.exit(1);
|
|
2557
|
+
}
|
|
2558
|
+
});
|
|
2559
|
+
|
|
2560
|
+
return store;
|
|
2561
|
+
}
|
|
2562
|
+
```
|
|
2563
|
+
|
|
2564
|
+
**Step 2: Register store command**
|
|
2565
|
+
|
|
2566
|
+
Update `src/index.ts`:
|
|
2567
|
+
```typescript
|
|
2568
|
+
#!/usr/bin/env node
|
|
2569
|
+
|
|
2570
|
+
import { createProgram, getGlobalOptions } from './cli/program.js';
|
|
2571
|
+
import { createStoreCommand } from './cli/commands/store.js';
|
|
2572
|
+
|
|
2573
|
+
const program = createProgram();
|
|
2574
|
+
|
|
2575
|
+
program.addCommand(createStoreCommand(() => getGlobalOptions(program)));
|
|
2576
|
+
|
|
2577
|
+
program.parse();
|
|
2578
|
+
```
|
|
2579
|
+
|
|
2580
|
+
**Step 3: Verify store commands work**
|
|
2581
|
+
|
|
2582
|
+
```bash
|
|
2583
|
+
npm run build && node dist/index.js store --help
|
|
2584
|
+
```
|
|
2585
|
+
|
|
2586
|
+
Expected: Help output for store commands
|
|
2587
|
+
|
|
2588
|
+
**Step 4: Commit**
|
|
2589
|
+
|
|
2590
|
+
```bash
|
|
2591
|
+
git add src/cli/commands/store.ts src/index.ts
|
|
2592
|
+
git commit -m "feat: add store CLI commands"
|
|
2593
|
+
```
|
|
2594
|
+
|
|
2595
|
+
---
|
|
2596
|
+
|
|
2597
|
+
### Task 5.3: Add search command
|
|
2598
|
+
|
|
2599
|
+
**Files:**
|
|
2600
|
+
- Create: `src/cli/commands/search.ts`
|
|
2601
|
+
- Modify: `src/index.ts`
|
|
2602
|
+
|
|
2603
|
+
**Step 1: Create search command**
|
|
2604
|
+
|
|
2605
|
+
Create `src/cli/commands/search.ts`:
|
|
2606
|
+
```typescript
|
|
2607
|
+
import { Command } from 'commander';
|
|
2608
|
+
import { createServices } from '../../services/index.js';
|
|
2609
|
+
import type { GlobalOptions } from '../program.js';
|
|
2610
|
+
import type { SearchMode } from '../../types/search.js';
|
|
2611
|
+
import { createStoreId } from '../../types/brands.js';
|
|
2612
|
+
|
|
2613
|
+
export function createSearchCommand(getOptions: () => GlobalOptions): Command {
|
|
2614
|
+
const search = new Command('search')
|
|
2615
|
+
.description('Search across knowledge stores')
|
|
2616
|
+
.argument('<query>', 'Search query')
|
|
2617
|
+
.option('-s, --stores <stores>', 'Comma-separated store IDs/names')
|
|
2618
|
+
.option('-m, --mode <mode>', 'Search mode: vector | fts | hybrid', 'hybrid')
|
|
2619
|
+
.option('-n, --limit <number>', 'Max results', '10')
|
|
2620
|
+
.option('-t, --threshold <number>', 'Minimum relevance score 0-1')
|
|
2621
|
+
.option('--include-content', 'Include full content in results')
|
|
2622
|
+
.action(async (query: string, options: {
|
|
2623
|
+
stores?: string;
|
|
2624
|
+
mode?: SearchMode;
|
|
2625
|
+
limit?: string;
|
|
2626
|
+
threshold?: string;
|
|
2627
|
+
includeContent?: boolean;
|
|
2628
|
+
}) => {
|
|
2629
|
+
const globalOpts = getOptions();
|
|
2630
|
+
const services = await createServices(globalOpts.config, globalOpts.dataDir);
|
|
2631
|
+
|
|
2632
|
+
// Get store IDs
|
|
2633
|
+
let storeIds = (await services.store.list()).map((s) => s.id);
|
|
2634
|
+
|
|
2635
|
+
if (options.stores !== undefined) {
|
|
2636
|
+
const requestedStores = options.stores.split(',').map((s) => s.trim());
|
|
2637
|
+
const resolvedStores = [];
|
|
2638
|
+
|
|
2639
|
+
for (const requested of requestedStores) {
|
|
2640
|
+
const store = await services.store.getByIdOrName(requested);
|
|
2641
|
+
if (store !== undefined) {
|
|
2642
|
+
resolvedStores.push(store.id);
|
|
2643
|
+
} else {
|
|
2644
|
+
console.error(`Store not found: ${requested}`);
|
|
2645
|
+
process.exit(3);
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
storeIds = resolvedStores;
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
if (storeIds.length === 0) {
|
|
2653
|
+
console.error('No stores to search. Create a store first.');
|
|
2654
|
+
process.exit(1);
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
// Initialize LanceDB for each store
|
|
2658
|
+
for (const storeId of storeIds) {
|
|
2659
|
+
await services.lance.initialize(storeId);
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
const results = await services.search.search({
|
|
2663
|
+
query,
|
|
2664
|
+
stores: storeIds,
|
|
2665
|
+
mode: options.mode ?? 'hybrid',
|
|
2666
|
+
limit: parseInt(options.limit ?? '10', 10),
|
|
2667
|
+
threshold: options.threshold !== undefined ? parseFloat(options.threshold) : undefined,
|
|
2668
|
+
includeContent: options.includeContent,
|
|
2669
|
+
});
|
|
2670
|
+
|
|
2671
|
+
if (globalOpts.format === 'json') {
|
|
2672
|
+
console.log(JSON.stringify(results, null, 2));
|
|
2673
|
+
} else {
|
|
2674
|
+
console.log(`\nSearch: "${query}"`);
|
|
2675
|
+
console.log(`Mode: ${results.mode} | Stores: ${results.stores.length} | Results: ${results.totalResults} | Time: ${results.timeMs}ms\n`);
|
|
2676
|
+
|
|
2677
|
+
if (results.results.length === 0) {
|
|
2678
|
+
console.log('No results found.\n');
|
|
2679
|
+
} else {
|
|
2680
|
+
for (let i = 0; i < results.results.length; i++) {
|
|
2681
|
+
const r = results.results[i]!;
|
|
2682
|
+
const path = r.metadata.path ?? r.metadata.url ?? 'unknown';
|
|
2683
|
+
console.log(`${i + 1}. [${r.score.toFixed(2)}] ${path}`);
|
|
2684
|
+
const preview = r.content.slice(0, 150).replace(/\n/g, ' ');
|
|
2685
|
+
console.log(` ${preview}${r.content.length > 150 ? '...' : ''}\n`);
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
});
|
|
2690
|
+
|
|
2691
|
+
return search;
|
|
2692
|
+
}
|
|
2693
|
+
```
|
|
2694
|
+
|
|
2695
|
+
**Step 2: Register search command**
|
|
2696
|
+
|
|
2697
|
+
Update `src/index.ts`:
|
|
2698
|
+
```typescript
|
|
2699
|
+
#!/usr/bin/env node
|
|
2700
|
+
|
|
2701
|
+
import { createProgram, getGlobalOptions } from './cli/program.js';
|
|
2702
|
+
import { createStoreCommand } from './cli/commands/store.js';
|
|
2703
|
+
import { createSearchCommand } from './cli/commands/search.js';
|
|
2704
|
+
|
|
2705
|
+
const program = createProgram();
|
|
2706
|
+
|
|
2707
|
+
program.addCommand(createStoreCommand(() => getGlobalOptions(program)));
|
|
2708
|
+
program.addCommand(createSearchCommand(() => getGlobalOptions(program)));
|
|
2709
|
+
|
|
2710
|
+
program.parse();
|
|
2711
|
+
```
|
|
2712
|
+
|
|
2713
|
+
**Step 3: Commit**
|
|
2714
|
+
|
|
2715
|
+
```bash
|
|
2716
|
+
git add src/cli/commands/search.ts src/index.ts
|
|
2717
|
+
git commit -m "feat: add search CLI command"
|
|
2718
|
+
```
|
|
2719
|
+
|
|
2720
|
+
---
|
|
2721
|
+
|
|
2722
|
+
### Task 5.4: Add index command
|
|
2723
|
+
|
|
2724
|
+
**Files:**
|
|
2725
|
+
- Create: `src/cli/commands/index-cmd.ts`
|
|
2726
|
+
- Modify: `src/index.ts`
|
|
2727
|
+
|
|
2728
|
+
**Step 1: Create index command**
|
|
2729
|
+
|
|
2730
|
+
Create `src/cli/commands/index-cmd.ts`:
|
|
2731
|
+
```typescript
|
|
2732
|
+
import { Command } from 'commander';
|
|
2733
|
+
import { createServices } from '../../services/index.js';
|
|
2734
|
+
import type { GlobalOptions } from '../program.js';
|
|
2735
|
+
|
|
2736
|
+
export function createIndexCommand(getOptions: () => GlobalOptions): Command {
|
|
2737
|
+
const index = new Command('index')
|
|
2738
|
+
.description('Index a knowledge store')
|
|
2739
|
+
.argument('<store>', 'Store ID or name')
|
|
2740
|
+
.option('--force', 'Force reindex all files')
|
|
2741
|
+
.action(async (storeIdOrName: string, options: { force?: boolean }) => {
|
|
2742
|
+
const globalOpts = getOptions();
|
|
2743
|
+
const services = await createServices(globalOpts.config, globalOpts.dataDir);
|
|
2744
|
+
|
|
2745
|
+
const store = await services.store.getByIdOrName(storeIdOrName);
|
|
2746
|
+
|
|
2747
|
+
if (store === undefined) {
|
|
2748
|
+
console.error(`Store not found: ${storeIdOrName}`);
|
|
2749
|
+
process.exit(3);
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
console.log(`\nIndexing store: ${store.name}...\n`);
|
|
2753
|
+
|
|
2754
|
+
await services.lance.initialize(store.id);
|
|
2755
|
+
|
|
2756
|
+
const result = await services.index.indexStore(store);
|
|
2757
|
+
|
|
2758
|
+
if (result.success) {
|
|
2759
|
+
if (globalOpts.format === 'json') {
|
|
2760
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
2761
|
+
} else {
|
|
2762
|
+
console.log(`Indexed ${result.data.documentsIndexed} documents`);
|
|
2763
|
+
console.log(`Created ${result.data.chunksCreated} chunks`);
|
|
2764
|
+
console.log(`Time: ${result.data.timeMs}ms\n`);
|
|
2765
|
+
}
|
|
2766
|
+
} else {
|
|
2767
|
+
console.error(`Error: ${result.error.message}`);
|
|
2768
|
+
process.exit(4);
|
|
2769
|
+
}
|
|
2770
|
+
});
|
|
2771
|
+
|
|
2772
|
+
return index;
|
|
2773
|
+
}
|
|
2774
|
+
```
|
|
2775
|
+
|
|
2776
|
+
**Step 2: Register index command**
|
|
2777
|
+
|
|
2778
|
+
Update `src/index.ts`:
|
|
2779
|
+
```typescript
|
|
2780
|
+
#!/usr/bin/env node
|
|
2781
|
+
|
|
2782
|
+
import { createProgram, getGlobalOptions } from './cli/program.js';
|
|
2783
|
+
import { createStoreCommand } from './cli/commands/store.js';
|
|
2784
|
+
import { createSearchCommand } from './cli/commands/search.js';
|
|
2785
|
+
import { createIndexCommand } from './cli/commands/index-cmd.js';
|
|
2786
|
+
|
|
2787
|
+
const program = createProgram();
|
|
2788
|
+
|
|
2789
|
+
program.addCommand(createStoreCommand(() => getGlobalOptions(program)));
|
|
2790
|
+
program.addCommand(createSearchCommand(() => getGlobalOptions(program)));
|
|
2791
|
+
program.addCommand(createIndexCommand(() => getGlobalOptions(program)));
|
|
2792
|
+
|
|
2793
|
+
program.parse();
|
|
2794
|
+
```
|
|
2795
|
+
|
|
2796
|
+
**Step 3: Commit**
|
|
2797
|
+
|
|
2798
|
+
```bash
|
|
2799
|
+
git add src/cli/commands/index-cmd.ts src/index.ts
|
|
2800
|
+
git commit -m "feat: add index CLI command"
|
|
2801
|
+
```
|
|
2802
|
+
|
|
2803
|
+
---
|
|
2804
|
+
|
|
2805
|
+
### Task 5.5: Update CLI barrel export
|
|
2806
|
+
|
|
2807
|
+
**Files:**
|
|
2808
|
+
- Modify: `src/cli/index.ts`
|
|
2809
|
+
|
|
2810
|
+
**Step 1: Update CLI index**
|
|
2811
|
+
|
|
2812
|
+
Update `src/cli/index.ts`:
|
|
2813
|
+
```typescript
|
|
2814
|
+
export { createProgram, getGlobalOptions, type GlobalOptions } from './program.js';
|
|
2815
|
+
export { createStoreCommand } from './commands/store.js';
|
|
2816
|
+
export { createSearchCommand } from './commands/search.js';
|
|
2817
|
+
export { createIndexCommand } from './commands/index-cmd.js';
|
|
2818
|
+
```
|
|
2819
|
+
|
|
2820
|
+
**Step 2: Run full build and test**
|
|
2821
|
+
|
|
2822
|
+
```bash
|
|
2823
|
+
npm run build && npm run test:run
|
|
2824
|
+
```
|
|
2825
|
+
|
|
2826
|
+
Expected: All tests pass, build succeeds
|
|
2827
|
+
|
|
2828
|
+
**Step 3: Commit**
|
|
2829
|
+
|
|
2830
|
+
```bash
|
|
2831
|
+
git add src/cli/index.ts
|
|
2832
|
+
git commit -m "feat: add CLI barrel export"
|
|
2833
|
+
```
|
|
2834
|
+
|
|
2835
|
+
---
|
|
2836
|
+
|
|
2837
|
+
## Phase 6: Integration Testing
|
|
2838
|
+
|
|
2839
|
+
### Task 6.1: Create end-to-end test
|
|
2840
|
+
|
|
2841
|
+
**Files:**
|
|
2842
|
+
- Create: `tests/integration/cli.test.ts`
|
|
2843
|
+
|
|
2844
|
+
**Step 1: Write integration test**
|
|
2845
|
+
|
|
2846
|
+
Create `tests/integration/cli.test.ts`:
|
|
2847
|
+
```typescript
|
|
2848
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2849
|
+
import { execSync } from 'node:child_process';
|
|
2850
|
+
import { rm, mkdtemp, writeFile, mkdir } from 'node:fs/promises';
|
|
2851
|
+
import { tmpdir } from 'node:os';
|
|
2852
|
+
import { join } from 'node:path';
|
|
2853
|
+
|
|
2854
|
+
describe('CLI Integration', () => {
|
|
2855
|
+
let tempDir: string;
|
|
2856
|
+
let testFilesDir: string;
|
|
2857
|
+
|
|
2858
|
+
beforeAll(async () => {
|
|
2859
|
+
tempDir = await mkdtemp(join(tmpdir(), 'cli-test-'));
|
|
2860
|
+
testFilesDir = join(tempDir, 'files');
|
|
2861
|
+
await mkdir(testFilesDir, { recursive: true });
|
|
2862
|
+
await writeFile(
|
|
2863
|
+
join(testFilesDir, 'test.md'),
|
|
2864
|
+
'# Test Document\n\nThis is content about TypeScript and JavaScript programming.'
|
|
2865
|
+
);
|
|
2866
|
+
}, 30000);
|
|
2867
|
+
|
|
2868
|
+
afterAll(async () => {
|
|
2869
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
2870
|
+
});
|
|
2871
|
+
|
|
2872
|
+
const cli = (args: string): string => {
|
|
2873
|
+
return execSync(`node dist/index.js ${args} --data-dir "${tempDir}"`, {
|
|
2874
|
+
encoding: 'utf-8',
|
|
2875
|
+
timeout: 120000,
|
|
2876
|
+
});
|
|
2877
|
+
};
|
|
2878
|
+
|
|
2879
|
+
it('shows help', () => {
|
|
2880
|
+
const output = cli('--help');
|
|
2881
|
+
expect(output).toContain('bkb');
|
|
2882
|
+
expect(output).toContain('CLI tool for managing knowledge stores');
|
|
2883
|
+
});
|
|
2884
|
+
|
|
2885
|
+
it('creates and lists stores', () => {
|
|
2886
|
+
cli(`store create test-store --type file --source "${testFilesDir}"`);
|
|
2887
|
+
const listOutput = cli('store list');
|
|
2888
|
+
expect(listOutput).toContain('test-store');
|
|
2889
|
+
});
|
|
2890
|
+
|
|
2891
|
+
it('shows store info', () => {
|
|
2892
|
+
const output = cli('store info test-store');
|
|
2893
|
+
expect(output).toContain('test-store');
|
|
2894
|
+
expect(output).toContain('file');
|
|
2895
|
+
});
|
|
2896
|
+
|
|
2897
|
+
it('indexes a store', () => {
|
|
2898
|
+
const output = cli('index test-store');
|
|
2899
|
+
expect(output).toContain('Indexed');
|
|
2900
|
+
expect(output).toContain('documents');
|
|
2901
|
+
}, 120000);
|
|
2902
|
+
|
|
2903
|
+
it('searches indexed content', () => {
|
|
2904
|
+
const output = cli('search "TypeScript programming"');
|
|
2905
|
+
expect(output).toContain('Search:');
|
|
2906
|
+
expect(output).toContain('Results:');
|
|
2907
|
+
}, 60000);
|
|
2908
|
+
});
|
|
2909
|
+
```
|
|
2910
|
+
|
|
2911
|
+
**Step 2: Run integration tests**
|
|
2912
|
+
|
|
2913
|
+
```bash
|
|
2914
|
+
npm run build && npm run test:run -- tests/integration/
|
|
2915
|
+
```
|
|
2916
|
+
|
|
2917
|
+
Expected: All integration tests pass
|
|
2918
|
+
|
|
2919
|
+
**Step 3: Commit**
|
|
2920
|
+
|
|
2921
|
+
```bash
|
|
2922
|
+
git add tests/integration/cli.test.ts
|
|
2923
|
+
git commit -m "test: add CLI integration tests"
|
|
2924
|
+
```
|
|
2925
|
+
|
|
2926
|
+
---
|
|
2927
|
+
|
|
2928
|
+
## Summary
|
|
2929
|
+
|
|
2930
|
+
This plan covers the core implementation of the Bluera Knowledge CLI:
|
|
2931
|
+
|
|
2932
|
+
**Phase 1:** Project scaffolding (8 tasks)
|
|
2933
|
+
**Phase 2:** Types layer (7 tasks)
|
|
2934
|
+
**Phase 3:** Data layer (3 tasks)
|
|
2935
|
+
**Phase 4:** Services layer (5 tasks)
|
|
2936
|
+
**Phase 5:** CLI layer (5 tasks)
|
|
2937
|
+
**Phase 6:** Integration testing (1 task)
|
|
2938
|
+
|
|
2939
|
+
**Total: 29 bite-sized tasks**
|
|
2940
|
+
|
|
2941
|
+
**Not included in this initial plan (future phases):**
|
|
2942
|
+
- HTTP server mode (Hono)
|
|
2943
|
+
- Python bridge for web crawling
|
|
2944
|
+
- Document chunking
|
|
2945
|
+
- Full-text search
|
|
2946
|
+
- Hybrid search with RRF
|
|
2947
|
+
- Progress bars and interactive mode
|
|
2948
|
+
- File watching
|
|
2949
|
+
- Export/import functionality
|
|
2950
|
+
|
|
2951
|
+
These can be added in subsequent plans once the core is working.
|