codesearch-mcp 0.0.17-alpha.1 → 0.1.0-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +95 -6
  3. package/dist/app.js +70 -10
  4. package/dist/src/commands/ConfigCommand.d.ts +1 -0
  5. package/dist/src/commands/ConfigCommand.js +84 -4
  6. package/dist/src/commands/IndexCommand.js +35 -7
  7. package/dist/src/commands/McpCommand.js +9 -4
  8. package/dist/src/commands/{SetupCommand.d.ts → WebCommand.d.ts} +3 -2
  9. package/dist/src/commands/WebCommand.js +56 -0
  10. package/dist/src/config-store.d.ts +85 -4
  11. package/dist/src/config-store.js +157 -32
  12. package/dist/src/config.d.ts +32 -8
  13. package/dist/src/config.js +93 -30
  14. package/dist/src/constants.d.ts +35 -0
  15. package/dist/src/constants.js +57 -4
  16. package/dist/src/container.d.ts +10 -6
  17. package/dist/src/container.js +10 -5
  18. package/dist/src/db/index.d.ts +1 -1
  19. package/dist/src/db/index.js +2 -1
  20. package/dist/src/db/schema.d.ts +111 -0
  21. package/dist/src/db/schema.js +15 -1
  22. package/dist/src/graph.d.ts +60 -0
  23. package/dist/src/graph.js +239 -120
  24. package/dist/src/help.js +20 -5
  25. package/dist/src/interfaces/IAstParser.d.ts +20 -3
  26. package/dist/src/interfaces/IChunker.d.ts +24 -0
  27. package/dist/src/interfaces/IChunker.js +2 -0
  28. package/dist/src/interfaces/IDatabase.d.ts +27 -0
  29. package/dist/src/interfaces/IDatabase.js +2 -0
  30. package/dist/src/interfaces/IEmbedder.d.ts +15 -2
  31. package/dist/src/interfaces/ILogger.d.ts +10 -0
  32. package/dist/src/interfaces/ILogger.js +2 -0
  33. package/dist/src/interfaces/IMcpToolHandler.d.ts +22 -0
  34. package/dist/src/interfaces/IMcpToolHandler.js +2 -0
  35. package/dist/src/interfaces/IMonorepoResolver.d.ts +33 -0
  36. package/dist/src/interfaces/IMonorepoResolver.js +2 -0
  37. package/dist/src/interfaces/IProjectDiscovery.d.ts +28 -0
  38. package/dist/src/interfaces/IProjectDiscovery.js +2 -0
  39. package/dist/src/interfaces/ISearcher.d.ts +37 -0
  40. package/dist/src/interfaces/ISearcher.js +2 -0
  41. package/dist/src/interfaces/index.d.ts +8 -0
  42. package/dist/src/interfaces/index.js +2 -0
  43. package/dist/src/owner-path.d.ts +10 -0
  44. package/dist/src/owner-path.js +27 -0
  45. package/dist/src/project-selector.d.ts +1 -1
  46. package/dist/src/services/AstChunker.d.ts +31 -0
  47. package/dist/src/services/AstChunker.js +114 -0
  48. package/dist/src/services/AstParserService.d.ts +10 -6
  49. package/dist/src/services/AstParserService.js +60 -15
  50. package/dist/src/services/DatabaseService.d.ts +14 -1
  51. package/dist/src/services/DatabaseService.js +70 -25
  52. package/dist/src/services/EmbedderFactory.d.ts +9 -0
  53. package/dist/src/services/EmbedderFactory.js +29 -0
  54. package/dist/src/services/IndexerService.d.ts +22 -12
  55. package/dist/src/services/IndexerService.js +106 -46
  56. package/dist/src/services/InfinityEmbedder.d.ts +79 -0
  57. package/dist/src/services/InfinityEmbedder.js +199 -0
  58. package/dist/src/services/LoggerService.d.ts +2 -1
  59. package/dist/src/services/McpServer.d.ts +16 -7
  60. package/dist/src/services/McpServer.js +54 -367
  61. package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +62 -0
  62. package/dist/src/services/MonorepoAwareDiscoveryService.js +125 -0
  63. package/dist/src/services/NxMonorepoResolver.d.ts +45 -0
  64. package/dist/src/services/NxMonorepoResolver.js +308 -0
  65. package/dist/src/services/OllamaEmbedder.d.ts +19 -5
  66. package/dist/src/services/OllamaEmbedder.js +91 -36
  67. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +25 -0
  68. package/dist/src/services/OpenAICompatibleEmbedder.js +75 -0
  69. package/dist/src/services/ProjectDiscoveryService.d.ts +8 -0
  70. package/dist/src/{env-loader.js → services/ProjectDiscoveryService.js} +16 -5
  71. package/dist/src/services/SearcherService.d.ts +46 -7
  72. package/dist/src/services/SearcherService.js +183 -53
  73. package/dist/src/services/VoyageEmbedder.d.ts +22 -0
  74. package/dist/src/services/VoyageEmbedder.js +72 -0
  75. package/dist/src/services/WebServerService.d.ts +77 -0
  76. package/dist/src/services/WebServerService.js +267 -0
  77. package/dist/src/services/http-retry.d.ts +19 -0
  78. package/dist/src/services/http-retry.js +54 -0
  79. package/dist/src/services/reranker.d.ts +15 -0
  80. package/dist/src/services/reranker.js +44 -0
  81. package/dist/src/setup.d.ts +6 -0
  82. package/dist/src/setup.js +249 -31
  83. package/dist/src/tools/FindSymbolTool.d.ts +8 -0
  84. package/dist/src/tools/FindSymbolTool.js +45 -0
  85. package/dist/src/tools/GetCallersTool.d.ts +8 -0
  86. package/dist/src/tools/GetCallersTool.js +42 -0
  87. package/dist/src/tools/GetDependenciesTool.d.ts +8 -0
  88. package/dist/src/tools/GetDependenciesTool.js +38 -0
  89. package/dist/src/tools/GetDependentsTool.d.ts +8 -0
  90. package/dist/src/tools/GetDependentsTool.js +57 -0
  91. package/dist/src/tools/ListFilesTool.d.ts +8 -0
  92. package/dist/src/tools/ListFilesTool.js +39 -0
  93. package/dist/src/tools/ListProjectsTool.d.ts +8 -0
  94. package/dist/src/tools/ListProjectsTool.js +44 -0
  95. package/dist/src/tools/ReadFileRangeTool.d.ts +8 -0
  96. package/dist/src/tools/ReadFileRangeTool.js +44 -0
  97. package/dist/src/tools/ReadFileTool.d.ts +8 -0
  98. package/dist/src/tools/ReadFileTool.js +42 -0
  99. package/dist/src/tools/SearchCodebaseTool.d.ts +13 -0
  100. package/dist/src/tools/SearchCodebaseTool.js +81 -0
  101. package/dist/src/tools/format-helpers.d.ts +8 -0
  102. package/dist/src/tools/format-helpers.js +58 -0
  103. package/dist/src/tools/index.d.ts +9 -0
  104. package/dist/src/tools/index.js +21 -0
  105. package/dist/src/web/dashboard.d.ts +9 -0
  106. package/dist/src/web/dashboard.js +342 -0
  107. package/docker/docker-compose.infinity-gpu.yml +19 -0
  108. package/docker/docker-compose.yml +25 -0
  109. package/docs/web-dashboard.md +187 -0
  110. package/package.json +25 -3
  111. package/dist/src/ast.d.ts +0 -2
  112. package/dist/src/ast.js +0 -110
  113. package/dist/src/commands/SetupCommand.js +0 -14
  114. package/dist/src/db/data-source.d.ts +0 -2
  115. package/dist/src/db/data-source.js +0 -13
  116. package/dist/src/db/entities/code-chunk.entity.d.ts +0 -9
  117. package/dist/src/db/entities/code-chunk.entity.js +0 -54
  118. package/dist/src/db/entities/code-dependency.entity.d.ts +0 -9
  119. package/dist/src/db/entities/code-dependency.entity.js +0 -54
  120. package/dist/src/db/entities/code-symbol.entity.d.ts +0 -10
  121. package/dist/src/db/entities/code-symbol.entity.js +0 -59
  122. package/dist/src/db/init.d.ts +0 -2
  123. package/dist/src/db/init.js +0 -62
  124. package/dist/src/env-loader.d.ts +0 -1
  125. package/dist/src/indexer.d.ts +0 -17
  126. package/dist/src/indexer.js +0 -242
  127. package/dist/src/mcp.d.ts +0 -1
  128. package/dist/src/mcp.js +0 -403
  129. package/dist/src/ollama.d.ts +0 -2
  130. package/dist/src/ollama.js +0 -6
  131. package/dist/src/searcher.d.ts +0 -74
  132. package/dist/src/searcher.js +0 -446
  133. package/dist/vitest.config.d.ts +0 -2
  134. package/dist/vitest.config.js +0 -9
package/CHANGELOG.md CHANGED
@@ -3,6 +3,54 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
5
 
6
+ ## [Unreleased]
7
+
8
+ ## [0.1.0-alpha] - 2026-07-12
9
+
10
+ ### Added
11
+
12
+ - **`infinity` embedding provider** — a first-class local backend (`embedding.provider = infinity`) that serves both `/embeddings` and `/rerank` from a single [Infinity](https://github.com/michaelfeil/infinity) container (e.g. `jinaai/jina-embeddings-v2-base-code` + `Alibaba-NLP/gte-reranker-modernbert-base`), so local reranking works out of the box — something Ollama does not provide. Reuses the OpenAI-compatible HTTP path and adds only container-readiness behavior. Selecting `infinity` in `rag-setup` optionally provisions a reranker model and auto-wires `reranker.*` to the same server. Managed provisioning vs. an external/already-running server is decided by a `/health` probe (no config flag) — the same "already running?" probe now also defaults `rag-setup`'s "start Docker?" prompt to no for a reachable Ollama. The managed container prefers an NVIDIA GPU and falls back to CPU, and is recreated when the configured model set changes (Infinity binds models at startup). The managed container also gets `restart: unless-stopped` (survives host reboot) and disables Infinity's model warmup on CPU (a multi-minute benchmark at every boot; kept on for GPU where it is cheap). `jinaai/jina-embeddings-v2-base-code` (768 dims, the Infinity default) and `BAAI/bge-m3` (1024) are added to the model registry so their dimension resolves without a probe.
13
+ - **Reranking is `openai-compatible`-only** — `reranker.provider` no longer accepts `ollama` (Ollama has no rerank API — the previous `/api/rerank` path always 404'd and silently fell back to vector order); the only valid backend is an OpenAI-compatible `/rerank` server (Infinity, TEI, vLLM). The default provider is now `openai-compatible`, and an existing `reranker.provider: 'ollama'` is migrated automatically on read. The embedding provider stays independent of the rerank server, so Ollama embeddings + an Infinity reranker is a valid combination.
14
+ - **Pluggable embedding backends** — the embedder is now selected by `embedding.provider` via a factory, with three implementations: `ollama` (native `/api/embed`, the **backward-compatible default**), `openai-compatible` (one code path covering **OpenAI cloud, Infinity, TEI, vLLM, LM Studio, LocalAI** via a configurable `embedding.baseURL`), and `voyage` (native schema with mandatory `input_type`). To run a faster local backend or a cloud embedder you now edit config, not code.
15
+ - **Batch embedding with retry** — `IEmbedder` gains `embedBatch(texts, inputType?)`; indexing sends one request per batch (`embedding.batchSize`, default 32) instead of one call per chunk, with bounded exponential backoff on HTTP 429/5xx. Makes cloud/Infinity indexing of a whole monorepo practical.
16
+ - **Query/document input-type asymmetry** — `embed`/`embedBatch` take an optional `inputType` (`query` | `document`); search embeds as `query`, indexing as `document`. Voyage forwards it as `input_type`; Ollama and the OpenAI-compatible `/embeddings` endpoint ignore it (OpenAI has no such field).
17
+ - **Flexible vector dimension** — the `code_chunk.embedding` column width now derives from the active model (`embedding.dimensions` override → model registry → one-shot runtime probe) instead of a hardcoded 1024, capped at ≤ 2000 (pgvector HNSW limit); larger cloud models reduce via the Matryoshka `dimensions`/`output_dimension` request parameter.
18
+ - **`index_meta` source-of-truth table + mismatch guard** — a single-row table records `{ provider, model, dimensions }`. `search_codebase` (before embedding a query) and `rag-mcp` (at start) refuse with a clear "run `rag-index --reindex`" error when the configured model disagrees with the indexed one, instead of returning noise from a foreign vector space. A dimension change drops+recreates `code_chunk` at the new width on re-index; a same-dimension model change clears its rows.
19
+ - **API-key secrets via `.env`** — keys live in `~/.config/codesearch-mcp/.env` (chmod 600), loaded into the environment at startup before config is read (a real env var wins over the file), so the client-spawned `rag-mcp` sees them too. Config values support an `env:VAR_NAME` reference; a missing referenced variable fails with a clear error. The dashboard shows `✔ set via <VAR>` for `env:` refs and masks raw keys.
20
+ - **Auto-pull of local Ollama models with live progress** — `rag-index` now pulls the configured Ollama model if it is not present locally (only for `provider: ollama`; a no-op for cloud providers), so switching to (or back to) a local model that was never pulled just works without re-running `rag-setup`. The pull streams progress: a live single-line bar on a TTY (`downloading [██████░░░░] 62% (245.7/393.2 MB)`), de-duplicated status lines otherwise. `rag-setup`'s in-container pull now renders Ollama's native progress bar (allocates a TTY via `docker exec -t` when interactive) and pulls the model you actually chose (previously hardcoded to `bge-m3`).
21
+ - **Cloud providers skip the Ollama container** — when a non-Ollama provider is selected, `rag-setup`'s Docker helper starts only the database service (`docker compose up -d db`) and skips the Ollama wait + model pull, instead of bringing up an unused Ollama container.
22
+ - **Owner-symbol breadcrumb in search results** — each `search_codebase` result now shows a `Symbol:` line naming the declaration the chunk belongs to (`IndexerService.ingestFile`, `collectFiles`, …), so the enclosing class/method is visible without opening the file. Computed at index time into a new nullable `code_chunk.ownerPath` column (added via `ADD COLUMN IF NOT EXISTS`): the deepest declaration containing the chunk's start line wins, so a method beats its enclosing class. Method/constructor/accessor ranges are extracted purely for this breadcrumb and are **not** written to `code_symbol`, so `find_symbol` is unchanged. Module-scope chunks (imports) and rows indexed before this release show no breadcrumb; re-index to populate. No embedding-dimension change.
23
+ - **AST-aware chunking** — TS/JS files are now chunked along top-level declaration boundaries (functions, classes, interfaces, types, exported variables) instead of at fixed character offsets, so a declaration is no longer split across chunks and search-hit line ranges map to coherent units of code. Declarations larger than `indexing.chunkSize` and the gaps between them (imports, top-level code) are sub-split by the recursive splitter, so no chunk exceeds the size limit and no source line is dropped. Reuses the symbol ranges already produced during indexing — no extra parse. New config key `indexing.chunkStrategy` (`ast` \| `character`, default `ast`); set it to `character` to restore the previous fixed-size behavior. Schema files (`.prisma`/`.graphql`/`.gql`) and files without symbol analysis keep the character splitter. Takes effect on the next re-index; no embedding-dimension or DB-schema change.
24
+
25
+ ### Changed (breaking)
26
+
27
+ - **Config restructure** — the `ollama.*` section is replaced by `embedding.*` (`provider`, `baseURL`, `apiKey`, `model`, `dimensions`, `batchSize`); `reranker.*` gains `provider`/`baseURL`/`apiKey` mirroring the embedding taxonomy (Infinity can serve both from one server). **Legacy `ollama.host`/`ollama.model` configs are migrated to `embedding.*` automatically on read**, so existing installs keep working without manual edits.
28
+ - **Query translation removed** — the `IEmbedder.translate` method and the `search.autoTranslate` / `search.translateModel` config keys are gone; queries are embedded as-is. The keys are silently ignored if still present.
29
+ - **Re-index required on model/dimension change** — switching the embedding model, provider, or dimension now requires `rag-index --reindex` (a dimension change recreates `code_chunk`). Staying on Ollama + `bge-m3` keeps dimension 1024, backfills `index_meta`, and forces no re-index. The mismatch guard makes any disagreement loud rather than silently corrupting results.
30
+
31
+ ## [0.0.18-alpha] - 2026-07-04
32
+
33
+ ### Added
34
+
35
+ - **Web dashboard (`rag-web`)** — a local browser UI at `http://127.0.0.1:4319` (`--port` to change), bound to `127.0.0.1` only, no auth (single-user local tool). Three tabs: a **schema-driven config editor** (form generated from the config JSON Schema; shows the applied default for any unset key, a strict dropdown for the embedding model, and comboboxes suggesting the user's installed Ollama models for the translate/reranker models), a **project overview**, and the **interactive dependency graph** (the same page as `project-graph.html`, embedded via iframe — filters, legend, and project toggles included). The DB password never reaches the browser (masked as `********`, preserved on save). Requires a valid `config.json`; a failed DB/Ollama connection is non-fatal (config editing still works). Full reference: `docs/web-dashboard.md`.
36
+ - **Config JSON Schema + `rag-config edit`** — `config.json` now gets a generated `config.schema.json` sibling, referenced via `"$schema"`, so schema-aware editors (VS Code, JetBrains) provide key autocomplete, type hints, and inline validation. `rag-config edit` opens the config in `$VISUAL` / `$EDITOR` (falls back to `nano` / `notepad`) and re-validates on save.
37
+ - **Nx monorepo support** — when `workspaceRoot` is an Nx workspace (detected by `nx.json`), projects under `apps/`, `libs/`, and `packages/` are discovered as separate indexed projects instead of a single flat scan. Cross-project imports (`@scope/lib`) resolve through the workspace-wide `tsconfig.base.json` path aliases, so `get_dependencies` / `get_dependents` see edges across projects. Non-Nx (flat) workspaces are unaffected.
38
+ - **`IMonorepoResolver` interface + `NxMonorepoResolver`** — resolver abstraction for monorepo tooling. `NxMonorepoResolver` detects `nx.json`, discovers projects via `workspace.json` / `angular.json` or an `apps/`/`libs/`/`packages/` scan for `project.json` / `package.json`, and resolves the root tsconfig (`tsconfig.base.json`, falling back to `tsconfig.json`).
39
+ - **`MonorepoAwareDiscoveryService`** — implements `IProjectDiscovery` and is wired as the default discovery in the container. Uses the resolver when a monorepo is detected; otherwise delegates to `ProjectDiscoveryService` (flat `package.json` scan). Exposes `analyzeWorkspace()` for full workspace metadata.
40
+ - **`DiscoveredProject` metadata** — discovery now carries optional `packageName` (npm scope + name), `type` (`app` | `lib`), and `rootTsConfigPath` per project.
41
+ - **Nested Nx monorepo under a flat workspace** — when `workspaceRoot` is a flat directory but one of its top-level subdirectories is itself an Nx repo, that repo is discovered as a single selectable unit whose sub-projects (`<repo>/<project>`, e.g. `micros-learning/account`) are expanded and indexed individually. `DiscoveredProject` gains an optional `children` field; the exported `toLeafProjects()` helper expands units into their leaf projects and is shared by `IndexCommand` (indexing loop) and `SearcherService` (name → path resolution). Each sub-project carries the repo's `tsconfig.base.json` as `rootTsConfigPath` for cross-project alias resolution.
42
+ - **Root-level Nx libs via `tsconfig.base.json` paths** — `NxMonorepoResolver` now also discovers projects referenced only through `compilerOptions.paths` aliases (e.g. `contracts/`, `interfaces/` living at the repo root), which the `apps/`/`libs/`/`packages/` scan misses. Only targets that resolve to a real project (with `project.json` / `package.json`) are added; the `tsconfig` reader tolerates JSONC (comments, trailing commas).
43
+ - **Tabbed project graph** — `project-graph.html` and the dashboard's `/graph` now have inner tabs: a **Workspace** tab (flat projects + each nested Nx repo collapsed to one node, double-click to open) and **one tab per Nx repo** showing its sub-projects with real import edges between them. Intra-repo edges come from `SearcherService.getProjectEdges()`, which aggregates `code_dependency` (file → file) into project → project links (most-specific owner wins). `getProjectEdges` is exposed on `ISearcher`.
44
+
45
+ ### Changed
46
+
47
+ - **Single source of truth for config defaults** — built-in defaults now live once in `CONFIG_DEFAULTS` (`config-store.ts`); `config.ts` references them in its schema `.default(...)` calls, so the runtime default and the value the dashboard shows for an unset key cannot diverge. No behavior change.
48
+ - **`analyzeProject` / `scanAndIndex` accept `rootTsConfigPath`** — for monorepo projects the AST parser loads the workspace-wide tsconfig (`tsconfig.base.json`) so ts-morph resolves cross-project path aliases; flat-workspace projects continue to use their local `tsconfig.json`.
49
+ - **`workspace.json` parsing hardened** — a project entry missing a valid `root` is skipped with a warning instead of aborting discovery of the remaining projects in that file.
50
+ - **Nx project discovery deduplicated across strategies** — `workspace.json` / `angular.json`, the directory scan, `tsconfig` paths, and the root-level fallback are now merged and deduped by absolute project path, so a project surfaced by more than one strategy is reported once.
51
+ - **`SearcherService` resolves project names to real roots** — takes the discovery service and builds a lazily-cached `name → filesystem root` map, so nested-monorepo names (`repo/app`) map to their true path (`repo/apps/app`) instead of a naïve `workspaceRoot/name` join. The cache rebuilds on a miss, so projects added or renamed while a long-running `rag-mcp` / `rag-web` server is up are picked up without a restart. `listProjects`, `grepSymbol`, `find_symbol` grep fallback, and `list_files` use it.
52
+ - **Project graph build is non-fatal in `rag-index`** — a failure while computing import edges or writing `project-graph.html` after a successful index is now caught and logged instead of failing the whole run.
53
+
6
54
  ## [0.0.17-alpha] - 2026-05-13
7
55
 
8
56
  ### Added
package/README.md CHANGED
@@ -35,7 +35,7 @@ rag-setup
35
35
 
36
36
  Walks you through:
37
37
  - **Database** — enter connection details; optionally starts Docker containers automatically
38
- - **Ollama** — configure API URL
38
+ - **Embedding backend** — pick a provider (`ollama` / `infinity` / `openai-compatible` / `voyage`), its model, and an API key if the provider needs one (see [Embedding providers](#embedding-providers))
39
39
  - **Logging** — choose log level and directory (defaults: `error` level, `~/.config/codesearch-mcp/logs/`)
40
40
 
41
41
  Config is saved to `~/.config/codesearch-mcp/config.json`.
@@ -54,6 +54,14 @@ rag-index --reindex # force re-embed all files (use after model change)
54
54
  rag-index --select --reindex
55
55
  ```
56
56
 
57
+ ## Monorepo support
58
+
59
+ If `workspaceRoot` is an [Nx](https://nx.dev) workspace (detected by `nx.json`), `rag-index` discovers each project under `apps/`, `libs/`, and `packages/` as a **separate indexed project** rather than scanning the workspace as one blob. Cross-project imports like `@scope/shared` resolve through the workspace `tsconfig.base.json` path aliases, so `get_dependencies` / `get_dependents` report edges **across** projects.
60
+
61
+ **Nested Nx repos.** `workspaceRoot` may also be a plain directory that *contains* an Nx repo in one of its immediate subdirectories (alongside ordinary projects). That nested repo is detected and expanded into its own projects, named `<repo>/<project>` (e.g. `micros-learning/account`), each resolved with the repo's own `tsconfig.base.json`. In the project selector the whole Nx repo appears as **one** selectable entry (so `.ragprojects` stores just `micros-learning`); selecting it indexes all its sub-projects. Ordinary sibling directories are unaffected.
62
+
63
+ No configuration needed — detection is automatic. Plain (non-Nx) workspaces keep the original behavior: each immediate subdirectory with a `package.json` is one project.
64
+
57
65
  ## Register with Claude Code
58
66
 
59
67
  ```bash
@@ -62,6 +70,18 @@ claude mcp add codesearch-mcp -s user -- rag-mcp
62
70
 
63
71
  Then ask Claude anything about your code — it will use the tools below automatically.
64
72
 
73
+ ## Web dashboard
74
+
75
+ ```bash
76
+ rag-web # http://127.0.0.1:4319 (--port to change)
77
+ ```
78
+
79
+ A local web UI (bound to `127.0.0.1` only) with a schema-driven config editor,
80
+ an overview of indexed projects, and the interactive dependency graph. The
81
+ config form shows the applied default for any unset key and offers dropdowns for
82
+ model fields (the embedding model is restricted to dimension-compatible models).
83
+ See [docs/web-dashboard.md](docs/web-dashboard.md) for the full reference.
84
+
65
85
  ## MCP Tools
66
86
 
67
87
  | Tool | Description |
@@ -98,7 +118,9 @@ After each indexing run an interactive `project-graph.html` is written to the wo
98
118
  Pass `include_graph_context: true` to `search_codebase` to see direct imports and dependents inline with each result — without extra `get_dependencies` / `get_dependents` calls:
99
119
 
100
120
  ```
101
- **File:** src/services/InvoiceService.ts **Line:** 45
121
+ **File:** src/services/InvoiceService.ts
122
+ **Symbol:** InvoiceService.create
123
+ **Line:** 45
102
124
  ...code...
103
125
  **Relevance:** 0.32
104
126
  **Imports:** src/repositories/InvoiceRepo.ts · src/dto/CreateInvoiceDto.ts
@@ -107,6 +129,8 @@ Pass `include_graph_context: true` to `search_codebase` to see direct imports an
107
129
 
108
130
  Useful for immediately understanding a file's role in the dependency graph.
109
131
 
132
+ Each result also shows a **Symbol** breadcrumb — the declaration the chunk belongs to (`InvoiceService.create`, `collectFiles`, …) — so you can see the enclosing class/method without opening the file. It is omitted for module-scope chunks and for projects indexed before this feature (re-index to populate).
133
+
110
134
  ## Excluding files
111
135
 
112
136
  Create `.ragignore` in a project root — same syntax as `.gitignore`:
@@ -152,15 +176,80 @@ rag-config reset <key> # remove key (reverts to default)
152
176
  | `db.password` | — | PostgreSQL password |
153
177
  | `db.name` | `rag_<os-username>` | Database name — auto-created on first run |
154
178
  | `workspaceRoot` | — | Root of your projects directory |
155
- | `ollama.host` | `http://localhost:11434` | Ollama API URL |
156
- | `ollama.model` | `bge-m3` | Embedding model |
179
+ | `embedding.provider` | `ollama` | Embedding backend: `ollama` \| `infinity` \| `openai-compatible` \| `voyage` |
180
+ | `embedding.baseURL` | per-provider | Backend base URL (ollama `http://localhost:11434`, OpenAI `https://api.openai.com/v1`, Infinity/TEI/vLLM `http://localhost:7997`, Voyage `https://api.voyageai.com/v1`) |
181
+ | `embedding.apiKey` | `''` | API key, or `env:VAR_NAME` to read from `.env`/environment. Empty for local servers |
182
+ | `embedding.model` | `bge-m3` | Embedding model |
183
+ | `embedding.dimensions` | (derived) | Vector width override; required for models whose native width exceeds 2000 (Matryoshka). Otherwise derived from model → registry → runtime probe |
184
+ | `embedding.batchSize` | `32` | Texts per batch embedding request |
157
185
  | `search.similarityThreshold` | `0.6` | Max cosine distance — raise if search returns nothing |
158
- | `search.autoTranslate` | `false` | Translate non-English queries to English before embedding |
159
- | `search.translateModel` | `llama3.2` | Ollama model for translation (requires `ollama pull llama3.2`) |
186
+ | `reranker.enabled` | `false` | Enable cross-encoder reranking; requires `reranker.model` |
187
+ | `reranker.provider` | `openai-compatible` | Rerank backend — `openai-compatible` `/rerank` only (Infinity, TEI, vLLM). Ollama has no rerank API |
188
+ | `reranker.baseURL` | `embedding.baseURL` | Rerank server base URL (Infinity serves embed + rerank from one server) |
189
+ | `reranker.model` | `''` | Reranker model name (e.g. `qllama/bge-reranker-v2-m3`) |
160
190
  | `indexing.astBatchSize` | `3000` | Max files per ts-morph batch — lower if indexing large projects causes OOM |
191
+ | `indexing.chunkStrategy` | `ast` | How TS/JS files are chunked: `ast` = boundaries aligned to functions/classes, `character` = fixed-size splitter. Re-index to apply |
161
192
  | `logging.level` | `error` | Log level written to file: `error` \| `warn` \| `info` \| `debug` |
162
193
  | `logging.dir` | `~/.config/codesearch-mcp/logs` | Base log directory, or `"false"` to disable |
163
194
 
195
+ ## Embedding providers
196
+
197
+ `embedding.provider` selects the backend. All four produce vectors the same way; they differ in where they run and which models they offer.
198
+
199
+ ### Choosing a provider
200
+
201
+ | Provider | When to use | Needs |
202
+ |----------|-------------|-------|
203
+ | `ollama` | Default. Local, zero-setup, general-purpose (`bge-m3`); auto-pulls the model. | Ollama running (Docker or native) |
204
+ | `infinity` | Local, best quality **plus a real reranker** in one container; GPU or CPU. | Docker (managed) or a running Infinity server |
205
+ | `openai-compatible` | OpenAI cloud, or any OpenAI-style server (TEI, vLLM, LM Studio, LocalAI). | `baseURL` (+ `apiKey` for cloud) |
206
+ | `voyage` | Voyage AI cloud, code-specialized embeddings. | Voyage `apiKey` |
207
+
208
+ Easiest path: `rag-setup` asks for the provider, model, and any API key. Haven't run setup? It's the place to start. To change things later, edit config directly (below).
209
+
210
+ ### Switching provider or changing the model
211
+
212
+ Any change to the provider, model, or dimension requires a **re-index** — the mismatch guard blocks search until you do it. The index is a rebuildable cache, so there's no data migration.
213
+
214
+ ```bash
215
+ # Switch provider (example: OpenAI cloud)
216
+ rag-config set embedding.provider openai-compatible
217
+ rag-config set embedding.baseURL https://api.openai.com/v1
218
+ rag-config set embedding.model text-embedding-3-small
219
+ rag-config set embedding.apiKey env:OPENAI_API_KEY # key lives in .env, not config.json
220
+ rag-index --reindex
221
+
222
+ # Change only the model on the current provider
223
+ rag-config set embedding.model <model-name>
224
+ rag-index --reindex
225
+ ```
226
+
227
+ Per-provider notes:
228
+
229
+ - **ollama** — `rag-index` auto-pulls the model if it isn't present locally (`bge-m3`, `nomic-embed-text`, `mxbai-embed-large`, …). No API key. Change model: `rag-config set embedding.model nomic-embed-text`.
230
+ - **infinity** — the model set is bound at container startup, so changing `embedding.model` (or the reranker model) **recreates** the managed container on the next `rag-index`. Defaults: `jinaai/jina-embeddings-v2-base-code` + reranker `Alibaba-NLP/gte-reranker-modernbert-base`.
231
+ - **openai-compatible** — point `embedding.baseURL` at the server; set `embedding.apiKey` (as `env:VAR`) for cloud, leave empty for local servers (TEI/vLLM/LM Studio). E.g. `text-embedding-3-small`, `text-embedding-3-large`.
232
+ - **voyage** — set `embedding.apiKey`; e.g. `voyage-code-3`, `voyage-3`.
233
+
234
+ The vector **dimension** is derived automatically (model registry → one-shot probe); set `embedding.dimensions` only to force a Matryoshka-reduced width for models whose native size exceeds 2000.
235
+
236
+ ### Infinity — local embeddings + reranking in one container
237
+
238
+ Select `infinity` in `rag-setup` (or set `embedding.provider = infinity`) to run [Infinity](https://github.com/michaelfeil/infinity), which serves both `/embeddings` and `/rerank` from a single container — so you get a real cross-encoder reranker locally, which Ollama does not offer. During setup you're asked for an embedding model (default `jinaai/jina-embeddings-v2-base-code`, 768 dims) and an optional reranker model (default `Alibaba-NLP/gte-reranker-modernbert-base`); when a reranker is given, `reranker.*` is auto-wired to the same server.
239
+
240
+ Managed vs. external is decided by a `/health` probe at `embedding.baseURL` (default `http://localhost:7997`) — no config flag:
241
+
242
+ - **Managed** — if nothing answers the probe, `rag-setup` (and `rag-index`) bring up the bundled `infinity` Docker service with your model set. It **prefers an NVIDIA GPU and falls back to CPU** automatically (GPU layered via `docker-compose.infinity-gpu.yml` only when `nvidia-smi` is present). Because Infinity binds its models at startup, changing `embedding.model`/reranker model **recreates** the container.
243
+ - **External** — if a server already answers (your own Infinity, remote host, CI), it is used as-is and no Docker command runs. Point `embedding.baseURL` at it.
244
+
245
+ The same "already running?" probe now also applies to Ollama: a reachable backend defaults `rag-setup`'s "start Docker?" prompt to no.
246
+
247
+ ### Secrets & external embedders
248
+
249
+ API keys live in `~/.config/codesearch-mcp/.env` (chmod 600), **not** in `config.json`. Reference them with `env:VAR_NAME`, e.g. `embedding.apiKey = env:OPENAI_API_KEY` with `OPENAI_API_KEY=sk-…` in `.env`. The file is loaded into the environment at startup (a real environment variable wins over the file), so all binaries — including the client-spawned `rag-mcp` — see it identically.
250
+
251
+ Switching the embedding model, provider, or dimension is applied by **re-indexing** (`rag-index --reindex`) — the index is a rebuildable cache, so there is no data migration. A single-row `index_meta` table records which model built the index; `search_codebase` and `rag-mcp` refuse with a clear "re-index required" error when the configured model disagrees, rather than returning noise from a foreign vector space. Staying on Ollama + `bge-m3` keeps dimension 1024 and forces no re-index.
252
+
164
253
  ## License
165
254
 
166
255
  MIT
package/dist/app.js CHANGED
@@ -34,25 +34,63 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  };
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
+ const fs = __importStar(require("fs"));
37
38
  const path = __importStar(require("path"));
38
39
  const help_1 = require("./src/help");
39
40
  const container_1 = require("./src/container");
40
41
  const config_1 = require("./src/config");
42
+ const config_store_1 = require("./src/config-store");
41
43
  const IndexCommand_1 = require("./src/commands/IndexCommand");
42
44
  const McpCommand_1 = require("./src/commands/McpCommand");
43
45
  const BackfillCommand_1 = require("./src/commands/BackfillCommand");
44
46
  const ConfigCommand_1 = require("./src/commands/ConfigCommand");
47
+ const WebCommand_1 = require("./src/commands/WebCommand");
45
48
  const setup_1 = require("./src/setup");
49
+ /**
50
+ * Load `~/.config/codesearch-mcp/.env` into `process.env` before config is read.
51
+ * All five binaries share this file — `rag-mcp` in particular is spawned by the
52
+ * MCP client and does not inherit shell exports. A real `process.env` value
53
+ * always wins over the file. Minimal `KEY=VALUE` parser (no `dotenv` dep):
54
+ * skips blanks and `#` comments, strips one layer of surrounding quotes.
55
+ */
56
+ function loadDotEnv(file) {
57
+ let text;
58
+ try {
59
+ text = fs.readFileSync(file, 'utf-8');
60
+ }
61
+ catch {
62
+ return; // absent / unreadable → no-op
63
+ }
64
+ for (const rawLine of text.split(/\r?\n/)) {
65
+ const line = rawLine.trim();
66
+ if (!line || line.startsWith('#'))
67
+ continue;
68
+ const eq = line.indexOf('=');
69
+ if (eq === -1)
70
+ continue;
71
+ const key = line.slice(0, eq).trim();
72
+ if (!key)
73
+ continue;
74
+ let val = line.slice(eq + 1).trim();
75
+ if (val.length >= 2 && ((val[0] === '"' && val.at(-1) === '"') || (val[0] === "'" && val.at(-1) === "'"))) {
76
+ val = val.slice(1, -1);
77
+ }
78
+ if (!(key in process.env))
79
+ process.env[key] = val; // real env wins
80
+ }
81
+ }
46
82
  async function main() {
83
+ loadDotEnv(config_store_1.ENV_FILE);
47
84
  const argv1 = path.basename(process.argv[1] ?? '');
48
85
  const isIndex = argv1 === 'rag-index' || process.argv[2] === 'index' || process.argv[2] === 'rag-index';
49
86
  const isMcp = argv1 === 'rag-mcp' || process.argv[2] === 'mcp' || process.argv[2] === 'rag-mcp';
50
87
  const isSetup = argv1 === 'rag-setup' || process.argv[2] === 'setup' || process.argv[2] === 'rag-setup';
51
88
  const isConfig = argv1 === 'rag-config' || process.argv[2] === 'config' || process.argv[2] === 'rag-config';
89
+ const isWeb = argv1 === 'rag-web' || process.argv[2] === 'web' || process.argv[2] === 'rag-web';
52
90
  const isBackfill = process.argv[2] === 'backfill' ||
53
91
  (process.argv[2] === 'rag-index' && process.argv[3] === 'backfill');
54
- const mode = isBackfill ? 'backfill' : isIndex ? 'index' : isMcp ? 'mcp' : isSetup ? 'setup' : isConfig ? 'config' : (process.argv[2] ?? '');
55
- const isBinary = ['rag-index', 'rag-mcp', 'rag-setup', 'rag-config'].includes(argv1);
92
+ const mode = isBackfill ? 'backfill' : isIndex ? 'index' : isMcp ? 'mcp' : isSetup ? 'setup' : isConfig ? 'config' : isWeb ? 'web' : (process.argv[2] ?? '');
93
+ const isBinary = ['rag-index', 'rag-mcp', 'rag-setup', 'rag-config', 'rag-web'].includes(argv1);
56
94
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
57
95
  (0, help_1.printHelp)(isBinary ? argv1 : mode);
58
96
  process.exit(0);
@@ -75,6 +113,24 @@ async function main() {
75
113
  process.exit(1);
76
114
  }
77
115
  }
116
+ else if (mode === 'web') {
117
+ for (let i = 0; i < effectiveArgs.length; i++) {
118
+ if (effectiveArgs[i] === '--port') {
119
+ const raw = effectiveArgs[i + 1];
120
+ const n = Number(raw);
121
+ if (!raw || !Number.isInteger(n) || n < 1 || n > 65535) {
122
+ console.error(`Invalid --port value: ${raw ?? '(missing)'}\n`);
123
+ (0, help_1.printHelp)(isBinary ? argv1 : 'rag-web');
124
+ process.exit(1);
125
+ }
126
+ i++;
127
+ continue; // skip its (validated) value
128
+ }
129
+ console.error(`Unexpected argument: ${effectiveArgs[i]}\n`);
130
+ (0, help_1.printHelp)(isBinary ? argv1 : 'rag-web');
131
+ process.exit(1);
132
+ }
133
+ }
78
134
  else if (mode === 'backfill') {
79
135
  const knownBackfillFlags = new Set(['--calls']);
80
136
  const backfillIdx = process.argv.indexOf('backfill');
@@ -95,7 +151,7 @@ async function main() {
95
151
  process.exit(0);
96
152
  }
97
153
  // All other modes require a valid config.json
98
- const loggerName = mode === 'index' || mode === 'backfill' ? 'indexer' : mode === 'mcp' ? 'mcp' : undefined;
154
+ const loggerName = mode === 'index' || mode === 'backfill' ? 'indexer' : mode === 'mcp' ? 'mcp' : mode === 'web' ? 'web' : undefined;
99
155
  let container;
100
156
  try {
101
157
  container = (0, container_1.createContainer)(loggerName);
@@ -114,20 +170,24 @@ async function main() {
114
170
  console.error(`workspaceRoot is not configured. ${hint}`);
115
171
  process.exit(1);
116
172
  }
117
- const commands = {
118
- index: new IndexCommand_1.IndexCommand(container, argv1),
119
- mcp: new McpCommand_1.McpCommand(container),
120
- backfill: new BackfillCommand_1.BackfillCommand(container),
173
+ // Construct only the selected command — building every command eagerly would
174
+ // run each constructor's arg parsing (e.g. WebCommand's --port) for modes that
175
+ // never use it.
176
+ const factories = {
177
+ index: () => new IndexCommand_1.IndexCommand(container, argv1),
178
+ mcp: () => new McpCommand_1.McpCommand(container),
179
+ backfill: () => new BackfillCommand_1.BackfillCommand(container),
180
+ web: () => new WebCommand_1.WebCommand(container, effectiveArgs),
121
181
  };
122
- const cmd = commands[mode];
123
- if (!cmd) {
182
+ const factory = factories[mode];
183
+ if (!factory) {
124
184
  const hasUnknownArg = mode !== '';
125
185
  if (hasUnknownArg)
126
186
  console.error(`Unknown command: ${mode}\n`);
127
187
  (0, help_1.printHelp)('default');
128
188
  process.exit(hasUnknownArg ? 1 : 0);
129
189
  }
130
- await cmd.execute();
190
+ await factory().execute();
131
191
  await container.db.destroy();
132
192
  process.exit(0);
133
193
  }
@@ -8,5 +8,6 @@ export declare class ConfigCommand implements ICommand {
8
8
  private get;
9
9
  private set;
10
10
  private reset;
11
+ private edit;
11
12
  }
12
13
  export { ALL_KEYS };
@@ -1,6 +1,41 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.ALL_KEYS = exports.ConfigCommand = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const child_process_1 = require("child_process");
4
39
  const config_store_1 = require("../config-store");
5
40
  Object.defineProperty(exports, "ALL_KEYS", { enumerable: true, get: function () { return config_store_1.ALL_KEYS; } });
6
41
  const help_1 = require("../help");
@@ -16,6 +51,7 @@ class ConfigCommand {
16
51
  case 'get': return this.get(key);
17
52
  case 'set': return this.set(key, value);
18
53
  case 'reset': return this.reset(key);
54
+ case 'edit': return this.edit();
19
55
  default:
20
56
  if (sub)
21
57
  console.error(`Unknown subcommand: ${sub}\n`);
@@ -29,7 +65,7 @@ class ConfigCommand {
29
65
  cfg = (0, config_store_1.readConfigJson)();
30
66
  }
31
67
  catch (e) {
32
- console.error(e.message);
68
+ console.error(e instanceof Error ? e.message : String(e));
33
69
  process.exit(1);
34
70
  }
35
71
  if (!cfg) {
@@ -52,7 +88,7 @@ class ConfigCommand {
52
88
  cfg = (0, config_store_1.readConfigJson)();
53
89
  }
54
90
  catch (e) {
55
- console.error(e.message);
91
+ console.error(e instanceof Error ? e.message : String(e));
56
92
  process.exit(1);
57
93
  }
58
94
  if (!cfg) {
@@ -74,7 +110,7 @@ class ConfigCommand {
74
110
  console.log(`${key} = ${value}`);
75
111
  }
76
112
  catch (e) {
77
- console.error(e.message);
113
+ console.error(e instanceof Error ? e.message : String(e));
78
114
  process.exit(1);
79
115
  }
80
116
  }
@@ -90,7 +126,51 @@ class ConfigCommand {
90
126
  console.log(`${key} reset to default`);
91
127
  }
92
128
  catch (e) {
93
- console.error(e.message);
129
+ console.error(e instanceof Error ? e.message : String(e));
130
+ process.exit(1);
131
+ }
132
+ }
133
+ edit() {
134
+ // Ensure config + schema exist so the editor has something to open and
135
+ // resolve the schema against.
136
+ try {
137
+ if (!fs.existsSync(config_store_1.CONFIG_FILE)) {
138
+ (0, config_store_1.writeConfigJson)({});
139
+ }
140
+ else {
141
+ (0, config_store_1.writeSchemaFile)();
142
+ // Migrate existing configs: re-add the "$schema" reference if absent
143
+ // (rewrite through writeConfigJson) so editors bind to the schema.
144
+ // Skip silently if the file is currently invalid — the user will fix
145
+ // it in the editor and validation runs on save.
146
+ try {
147
+ const cfg = (0, config_store_1.readConfigJson)();
148
+ if (cfg)
149
+ (0, config_store_1.writeConfigJson)(cfg);
150
+ }
151
+ catch { /* invalid file — leave as-is for the user to edit */ }
152
+ }
153
+ }
154
+ catch (e) {
155
+ console.error(e instanceof Error ? e.message : String(e));
156
+ process.exit(1);
157
+ }
158
+ const editor = process.env.VISUAL ||
159
+ process.env.EDITOR ||
160
+ (process.platform === 'win32' ? 'notepad' : 'nano');
161
+ const res = (0, child_process_1.spawnSync)(editor, [config_store_1.CONFIG_FILE], { stdio: 'inherit' });
162
+ if (res.error) {
163
+ console.error(`Cannot launch editor "${editor}": ${res.error.message}`);
164
+ console.error('Set $EDITOR or edit the file directly: ' + config_store_1.CONFIG_FILE);
165
+ process.exit(1);
166
+ }
167
+ // Validate what the user saved; readConfigJson throws with a helpful message.
168
+ try {
169
+ (0, config_store_1.readConfigJson)();
170
+ console.log('Config OK');
171
+ }
172
+ catch (e) {
173
+ console.error(e instanceof Error ? e.message : String(e));
94
174
  process.exit(1);
95
175
  }
96
176
  }
@@ -4,6 +4,7 @@ exports.IndexCommand = void 0;
4
4
  const interrupt_1 = require("../interrupt");
5
5
  const project_selector_1 = require("../project-selector");
6
6
  const graph_1 = require("../graph");
7
+ const MonorepoAwareDiscoveryService_1 = require("../services/MonorepoAwareDiscoveryService");
7
8
  const file_utils_1 = require("../file-utils");
8
9
  class IndexCommand {
9
10
  container;
@@ -13,18 +14,34 @@ class IndexCommand {
13
14
  this.argv1 = argv1;
14
15
  }
15
16
  async execute() {
16
- const { db, indexer, config } = this.container;
17
+ const { db, indexer, searcher, config, discovery, embedder } = this.container;
17
18
  const args = process.argv.slice(this.argv1 === 'rag-index' ? 2 : 3);
18
19
  const force = args.includes('--reindex');
19
20
  const forceSelect = args.includes('--select');
20
21
  await db.initDbWithSchema();
22
+ // Ensure the local model is pulled (Ollama only — no-op for cloud), then
23
+ // resolve the active embedding dimension and reconcile it with index_meta
24
+ // (recreate code_chunk on a dimension change, clear rows on a model change)
25
+ // before scanning. A clear error here beats a mid-run vector width mismatch.
26
+ try {
27
+ await embedder.ensureModelAvailable?.();
28
+ const meta = await indexer.prepareEmbeddingSchema();
29
+ console.log(`Embedding: ${meta.provider} / ${meta.model} (${meta.dimensions} dims)`);
30
+ }
31
+ catch (err) {
32
+ console.error(`\nEmbedding setup failed: ${err.message}`);
33
+ return;
34
+ }
21
35
  const controller = new AbortController();
22
36
  const teardown = (0, interrupt_1.setupInterrupt)(controller);
23
37
  try {
24
- const discovered = indexer.discoverProjects(config.workspaceRoot);
25
- const projects = await new project_selector_1.ProjectSelector(config.workspaceRoot).select(discovered, forceSelect);
26
- if (projects.length === 0)
38
+ const discovered = discovery.discover(config.workspaceRoot);
39
+ const selected = await new project_selector_1.ProjectSelector(config.workspaceRoot).select(discovered, forceSelect);
40
+ if (selected.length === 0)
27
41
  return;
42
+ // Expand any selected nested-monorepo unit (e.g. an Nx repo) into its
43
+ // sub-projects; ordinary projects pass through unchanged.
44
+ const projects = (0, MonorepoAwareDiscoveryService_1.toLeafProjects)(selected);
28
45
  const filesPerProject = projects.map(p => (0, file_utils_1.collectFiles)(p.path, (0, file_utils_1.loadIgnore)(p.path), p.path));
29
46
  const totalFiles = filesPerProject.reduce((sum, f) => sum + f.length, 0);
30
47
  console.log(`\n${projects.length} projects — ${totalFiles} files total`);
@@ -34,7 +51,7 @@ class IndexCommand {
34
51
  for (let i = 0; i < projects.length; i++) {
35
52
  if (controller.signal.aborted)
36
53
  break;
37
- const stats = await indexer.scanAndIndex(projects[i].path, projects[i].name, force, controller.signal, filesPerProject[i]);
54
+ const stats = await indexer.scanAndIndex(projects[i].path, projects[i].name, force, controller.signal, filesPerProject[i], { rootTsConfigPath: projects[i].rootTsConfigPath ?? undefined });
38
55
  allStats.push(stats);
39
56
  const remaining = projects.length - allStats.length;
40
57
  if (remaining > 0 && !controller.signal.aborted) {
@@ -45,8 +62,19 @@ class IndexCommand {
45
62
  if (controller.signal.aborted)
46
63
  console.log('\nIndexing interrupted.');
47
64
  this.printSummary(allStats);
48
- if (!controller.signal.aborted)
49
- (0, graph_1.generateProjectGraph)(projects, config.workspaceRoot);
65
+ if (!controller.signal.aborted) {
66
+ // The graph is a post-index nicety — a failure here (DB dropped after a
67
+ // long run, unreadable dir) must not fail the whole run: indexing already
68
+ // succeeded and the summary is printed.
69
+ try {
70
+ // Real import edges (code_dependency, just written) → intra-repo graph links.
71
+ const projectEdges = await searcher.getProjectEdges();
72
+ (0, graph_1.generateProjectGraph)(projects, projectEdges, config.workspaceRoot);
73
+ }
74
+ catch (err) {
75
+ console.error(` ⚠ Could not build project graph: ${err.message}`);
76
+ }
77
+ }
50
78
  }
51
79
  finally {
52
80
  teardown();
@@ -7,20 +7,25 @@ class McpCommand {
7
7
  this.container = container;
8
8
  }
9
9
  async execute() {
10
- const { db, mcp, logger } = this.container;
10
+ const { db, mcp, searcher, logger } = this.container;
11
11
  let startupError;
12
12
  try {
13
13
  await db.initDb();
14
+ // Refuse to serve searches embedded into a foreign vector space — surface
15
+ // the mismatch through the existing startup-error plumbing so every tool
16
+ // call reports it clearly.
17
+ await searcher.assertIndexMatches();
14
18
  }
15
19
  catch (err) {
16
- startupError = err;
17
- logger.error('McpCommand: database connection failed', { reason: err.message });
20
+ startupError = err instanceof Error ? err : new Error(String(err));
21
+ logger.error('McpCommand: startup check failed', { reason: startupError.message });
18
22
  }
19
23
  try {
20
24
  await mcp.start(startupError);
21
25
  }
22
26
  catch (err) {
23
- logger.error('McpCommand: MCP server failed to start', { reason: err.message });
27
+ const reason = err instanceof Error ? err.message : String(err);
28
+ logger.error('McpCommand: MCP server failed to start', { reason });
24
29
  return;
25
30
  }
26
31
  // server.connect() resolves immediately after setup; keep the process alive
@@ -1,7 +1,8 @@
1
1
  import type { Container } from '../container';
2
2
  import type { ICommand } from './ICommand';
3
- export declare class SetupCommand implements ICommand {
3
+ export declare class WebCommand implements ICommand {
4
4
  private container;
5
- constructor(container: Container);
5
+ private readonly port;
6
+ constructor(container: Container, args?: string[]);
6
7
  execute(): Promise<void>;
7
8
  }