codesearch-mcp 0.0.18-alpha → 0.2.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 (96) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/README.md +194 -23
  3. package/dist/app.js +38 -2
  4. package/dist/src/commands/IndexCommand.js +55 -1
  5. package/dist/src/commands/McpCommand.js +6 -2
  6. package/dist/src/config-error.d.ts +7 -0
  7. package/dist/src/config-error.js +14 -0
  8. package/dist/src/config-store.d.ts +73 -17
  9. package/dist/src/config-store.js +142 -27
  10. package/dist/src/config.d.ts +36 -11
  11. package/dist/src/config.js +89 -27
  12. package/dist/src/constants.d.ts +42 -2
  13. package/dist/src/constants.js +61 -7
  14. package/dist/src/container.js +5 -3
  15. package/dist/src/db/index.d.ts +1 -1
  16. package/dist/src/db/index.js +2 -1
  17. package/dist/src/db/schema.d.ts +111 -0
  18. package/dist/src/db/schema.js +15 -1
  19. package/dist/src/help.js +6 -5
  20. package/dist/src/interfaces/IAstParser.d.ts +14 -0
  21. package/dist/src/interfaces/IChunker.d.ts +24 -0
  22. package/dist/src/interfaces/IChunker.js +2 -0
  23. package/dist/src/interfaces/IDatabase.d.ts +31 -0
  24. package/dist/src/interfaces/IEmbedder.d.ts +14 -2
  25. package/dist/src/interfaces/ILogger.d.ts +7 -0
  26. package/dist/src/interfaces/ISearcher.d.ts +1 -0
  27. package/dist/src/interfaces/index.d.ts +2 -1
  28. package/dist/src/owner-path.d.ts +10 -0
  29. package/dist/src/owner-path.js +27 -0
  30. package/dist/src/services/AstChunker.d.ts +42 -0
  31. package/dist/src/services/AstChunker.js +158 -0
  32. package/dist/src/services/AstParserService.d.ts +6 -0
  33. package/dist/src/services/AstParserService.js +31 -2
  34. package/dist/src/services/DatabaseService.d.ts +17 -1
  35. package/dist/src/services/DatabaseService.js +116 -25
  36. package/dist/src/services/EmbedderFactory.d.ts +9 -0
  37. package/dist/src/services/EmbedderFactory.js +26 -0
  38. package/dist/src/services/IndexerService.d.ts +21 -5
  39. package/dist/src/services/IndexerService.js +127 -44
  40. package/dist/src/services/InfinityEmbedder.d.ts +38 -0
  41. package/dist/src/services/InfinityEmbedder.js +88 -0
  42. package/dist/src/services/LoggerService.d.ts +3 -1
  43. package/dist/src/services/LoggerService.js +17 -3
  44. package/dist/src/services/OpenAICompatibleEmbedder.d.ts +25 -0
  45. package/dist/src/services/OpenAICompatibleEmbedder.js +75 -0
  46. package/dist/src/services/SearcherService.d.ts +21 -1
  47. package/dist/src/services/SearcherService.js +129 -28
  48. package/dist/src/services/VoyageEmbedder.d.ts +22 -0
  49. package/dist/src/services/VoyageEmbedder.js +72 -0
  50. package/dist/src/services/WebServerService.d.ts +57 -14
  51. package/dist/src/services/WebServerService.js +134 -26
  52. package/dist/src/services/http-retry.d.ts +19 -0
  53. package/dist/src/services/http-retry.js +54 -0
  54. package/dist/src/services/infinity-provisioning.d.ts +115 -0
  55. package/dist/src/services/infinity-provisioning.js +516 -0
  56. package/dist/src/services/legacy-ollama-index.d.ts +24 -0
  57. package/dist/src/services/legacy-ollama-index.js +61 -0
  58. package/dist/src/services/reranker.d.ts +15 -0
  59. package/dist/src/services/reranker.js +44 -0
  60. package/dist/src/setup.d.ts +43 -0
  61. package/dist/src/setup.js +227 -55
  62. package/dist/src/tools/SearchCodebaseTool.js +1 -1
  63. package/dist/src/tools/format-helpers.js +2 -1
  64. package/dist/src/web/dashboard.js +5 -0
  65. package/docker/docker-compose.infinity-gpu.yml +19 -0
  66. package/docker/docker-compose.yml +20 -5
  67. package/docs/web-dashboard.md +36 -17
  68. package/package.json +18 -3
  69. package/dist/src/ast.d.ts +0 -2
  70. package/dist/src/ast.js +0 -110
  71. package/dist/src/commands/SetupCommand.d.ts +0 -7
  72. package/dist/src/commands/SetupCommand.js +0 -14
  73. package/dist/src/db/data-source.d.ts +0 -2
  74. package/dist/src/db/data-source.js +0 -13
  75. package/dist/src/db/entities/code-chunk.entity.d.ts +0 -9
  76. package/dist/src/db/entities/code-chunk.entity.js +0 -54
  77. package/dist/src/db/entities/code-dependency.entity.d.ts +0 -9
  78. package/dist/src/db/entities/code-dependency.entity.js +0 -54
  79. package/dist/src/db/entities/code-symbol.entity.d.ts +0 -10
  80. package/dist/src/db/entities/code-symbol.entity.js +0 -59
  81. package/dist/src/db/init.d.ts +0 -2
  82. package/dist/src/db/init.js +0 -62
  83. package/dist/src/env-loader.d.ts +0 -1
  84. package/dist/src/env-loader.js +0 -41
  85. package/dist/src/indexer.d.ts +0 -17
  86. package/dist/src/indexer.js +0 -242
  87. package/dist/src/mcp.d.ts +0 -1
  88. package/dist/src/mcp.js +0 -403
  89. package/dist/src/ollama.d.ts +0 -2
  90. package/dist/src/ollama.js +0 -6
  91. package/dist/src/searcher.d.ts +0 -74
  92. package/dist/src/searcher.js +0 -446
  93. package/dist/src/services/OllamaEmbedder.d.ts +0 -14
  94. package/dist/src/services/OllamaEmbedder.js +0 -63
  95. package/dist/vitest.config.d.ts +0 -2
  96. package/dist/vitest.config.js +0 -9
@@ -34,16 +34,46 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.Config = exports.ConfigError = void 0;
37
+ exports.resolveEnvRef = resolveEnvRef;
37
38
  const os = __importStar(require("os"));
38
39
  const zod_1 = require("zod");
39
40
  const config_store_1 = require("./config-store");
40
- class ConfigError extends Error {
41
- constructor(message) {
42
- super(message);
43
- this.name = 'ConfigError';
41
+ const config_error_1 = require("./config-error");
42
+ Object.defineProperty(exports, "ConfigError", { enumerable: true, get: function () { return config_error_1.ConfigError; } });
43
+ /**
44
+ * Resolve a config value that may reference an environment variable. A value of
45
+ * the form `env:VAR_NAME` resolves to `process.env.VAR_NAME`; any other string
46
+ * (including empty) is returned verbatim. A referenced-but-unset variable throws
47
+ * a clear error naming the variable and the `.env` file. Pure aside from the
48
+ * injected env map. See `src/app.ts` for how `.env` is loaded into the process.
49
+ */
50
+ function resolveEnvRef(value, env = process.env) {
51
+ if (!value.startsWith('env:'))
52
+ return value;
53
+ const name = value.slice(4).trim();
54
+ const resolved = name ? env[name] : undefined;
55
+ if (resolved == null || resolved === '') {
56
+ throw new config_error_1.ConfigError(`Config references environment variable "${name}" (as "env:${name}") but it is not set. ` +
57
+ `Define ${name} in ${config_store_1.ENV_FILE} or export it in the environment.`);
44
58
  }
59
+ return resolved;
60
+ }
61
+ const DEFAULT_BASE_URLS = {
62
+ 'openai-compatible': 'https://api.openai.com/v1',
63
+ 'voyage': 'https://api.voyageai.com/v1',
64
+ 'infinity': 'http://localhost:7997',
65
+ };
66
+ function defaultBaseURL(provider) {
67
+ return DEFAULT_BASE_URLS[provider] ?? DEFAULT_BASE_URLS.infinity;
68
+ }
69
+ // Reranking is served only by OpenAI-compatible `/rerank` endpoints (Infinity,
70
+ // TEI, vLLM, Jina/Cohere-shape) — a single endpoint shape. Normalize a trailing
71
+ // `/embeddings` or `/v1` to the server root so a baseURL written with an
72
+ // OpenAI-style suffix still yields `<root>/rerank`.
73
+ function deriveRerankUrl(baseURL) {
74
+ const root = baseURL.replace(/\/+$/, '').replace(/\/(embeddings|v1)$/, '');
75
+ return `${root}/rerank`;
45
76
  }
46
- exports.ConfigError = ConfigError;
47
77
  const resolvedSchema = zod_1.z.object({
48
78
  db: zod_1.z.object({
49
79
  host: zod_1.z.string().min(1, 'required — run rag-setup'),
@@ -52,37 +82,56 @@ const resolvedSchema = zod_1.z.object({
52
82
  password: zod_1.z.string().min(1, 'required — run rag-setup'),
53
83
  database: zod_1.z.string().min(1),
54
84
  }),
55
- ollama: zod_1.z.object({
56
- host: zod_1.z.string().min(1).default(config_store_1.CONFIG_DEFAULTS['ollama.host']),
57
- model: zod_1.z.string().min(1).default(config_store_1.CONFIG_DEFAULTS['ollama.model']),
58
- rerankerModel: zod_1.z.string().default(config_store_1.CONFIG_DEFAULTS['reranker.model']),
59
- rerankerUrl: zod_1.z.string().min(1),
85
+ embedding: zod_1.z.object({
86
+ provider: zod_1.z.enum(['openai-compatible', 'voyage', 'infinity']).default(config_store_1.CONFIG_DEFAULTS['embedding.provider']),
87
+ baseURL: zod_1.z.string().min(1),
88
+ apiKey: zod_1.z.string().default(config_store_1.CONFIG_DEFAULTS['embedding.apiKey']),
89
+ model: zod_1.z.string().min(1).default(config_store_1.CONFIG_DEFAULTS['embedding.model']),
90
+ dimensions: zod_1.z.number().int().positive().nullable().default(null),
91
+ batchSize: zod_1.z.number().int().positive().default(config_store_1.CONFIG_DEFAULTS['embedding.batchSize']),
92
+ }),
93
+ reranker: zod_1.z.object({
94
+ enabled: zod_1.z.boolean().default(config_store_1.CONFIG_DEFAULTS['reranker.enabled']),
95
+ provider: zod_1.z.enum(['openai-compatible']).default(config_store_1.CONFIG_DEFAULTS['reranker.provider']),
96
+ baseURL: zod_1.z.string().min(1),
97
+ apiKey: zod_1.z.string().default(config_store_1.CONFIG_DEFAULTS['reranker.apiKey']),
98
+ model: zod_1.z.string().default(config_store_1.CONFIG_DEFAULTS['reranker.model']),
99
+ url: zod_1.z.string().min(1),
100
+ overfetch: zod_1.z.number().int().min(1).max(20).default(config_store_1.CONFIG_DEFAULTS['reranker.overfetch']),
101
+ candidateFloor: zod_1.z.number().int().min(1).default(config_store_1.CONFIG_DEFAULTS['reranker.candidateFloor']),
60
102
  }),
61
103
  indexer: zod_1.z.object({
62
104
  chunkSize: zod_1.z.number().int().positive().default(config_store_1.CONFIG_DEFAULTS['indexing.chunkSize']),
63
105
  chunkOverlap: zod_1.z.number().int().min(0).default(config_store_1.CONFIG_DEFAULTS['indexing.chunkOverlap']),
64
106
  batchSize: zod_1.z.number().int().positive().default(config_store_1.CONFIG_DEFAULTS['indexing.embedBatchSize']),
65
107
  astBatchSize: zod_1.z.number().int().positive().default(config_store_1.CONFIG_DEFAULTS['indexing.astBatchSize']),
108
+ chunkStrategy: zod_1.z.enum(['ast', 'character']).default(config_store_1.CONFIG_DEFAULTS['indexing.chunkStrategy']),
66
109
  }),
67
110
  search: zod_1.z.object({
68
111
  similarityThreshold: zod_1.z.number().min(0).max(1).default(config_store_1.CONFIG_DEFAULTS['search.similarityThreshold']),
69
112
  defaultLimit: zod_1.z.number().int().positive().default(config_store_1.CONFIG_DEFAULTS['search.defaultLimit']),
70
113
  maxLimit: zod_1.z.number().int().positive().default(config_store_1.CONFIG_DEFAULTS['search.maxLimit']),
71
- autoTranslate: zod_1.z.boolean().default(config_store_1.CONFIG_DEFAULTS['search.autoTranslate']),
72
- translateModel: zod_1.z.string().default(config_store_1.CONFIG_DEFAULTS['search.translateModel']),
73
- rerankerEnabled: zod_1.z.boolean().default(config_store_1.CONFIG_DEFAULTS['reranker.enabled']),
74
- rerankerOverfetch: zod_1.z.number().int().min(1).max(20).default(config_store_1.CONFIG_DEFAULTS['reranker.overfetch']),
75
114
  }),
76
115
  logging: zod_1.z.object({
77
116
  level: zod_1.z.enum(['debug', 'info', 'warn', 'error']).default(config_store_1.CONFIG_DEFAULTS['logging.level']),
78
117
  dir: zod_1.z.string().optional(),
118
+ searchMetrics: zod_1.z.boolean().default(config_store_1.CONFIG_DEFAULTS['logging.searchMetrics']),
79
119
  }),
80
120
  workspaceRoot: zod_1.z.string().default(''),
81
121
  });
82
122
  class Config {
83
123
  data;
84
124
  get db() { return this.data.db; }
85
- get ollama() { return this.data.ollama; }
125
+ /**
126
+ * Embedding config. `apiKey` is returned **raw** (possibly an `env:VAR` ref) —
127
+ * callers resolve it lazily via `resolveEnvRef` at the moment they build a
128
+ * request, so an unset referenced variable fails only when the key is actually
129
+ * needed (during embed/search) rather than crashing container construction and
130
+ * locking the user out of `rag-web`/`rag-mcp`.
131
+ */
132
+ get embedding() { return this.data.embedding; }
133
+ /** Reranker config. `apiKey` is raw — resolve lazily with `resolveEnvRef`. */
134
+ get reranker() { return this.data.reranker; }
86
135
  get indexer() { return this.data.indexer; }
87
136
  get search() { return this.data.search; }
88
137
  get logging() { return this.data.logging; }
@@ -90,8 +139,11 @@ class Config {
90
139
  constructor() {
91
140
  const raw = (0, config_store_1.readConfigJson)();
92
141
  if (!raw)
93
- throw new ConfigError('Config not found. Run rag-setup.');
94
- const ollamaHost = raw.ollama?.host ?? 'http://localhost:11434';
142
+ throw new config_error_1.ConfigError('Config not found. Run rag-setup.');
143
+ const embProvider = (raw.embedding?.provider ?? config_store_1.CONFIG_DEFAULTS['embedding.provider']);
144
+ const embBaseURL = raw.embedding?.baseURL ?? defaultBaseURL(embProvider);
145
+ const rerankBaseURL = raw.reranker?.baseURL ?? embBaseURL;
146
+ const rerankUrl = raw.reranker?.url ?? deriveRerankUrl(rerankBaseURL);
95
147
  const result = resolvedSchema.safeParse({
96
148
  db: {
97
149
  host: raw.db?.host,
@@ -100,30 +152,40 @@ class Config {
100
152
  password: raw.db?.password,
101
153
  database: raw.db?.name ?? this.deriveDbName(),
102
154
  },
103
- ollama: {
104
- host: raw.ollama?.host,
105
- model: raw.ollama?.model,
106
- rerankerModel: raw.reranker?.model,
107
- rerankerUrl: raw.reranker?.url ?? `${ollamaHost}/api/rerank`,
155
+ embedding: {
156
+ provider: raw.embedding?.provider,
157
+ baseURL: embBaseURL,
158
+ apiKey: raw.embedding?.apiKey,
159
+ model: raw.embedding?.model,
160
+ dimensions: raw.embedding?.dimensions,
161
+ batchSize: raw.embedding?.batchSize,
162
+ },
163
+ reranker: {
164
+ enabled: raw.reranker?.enabled,
165
+ provider: raw.reranker?.provider,
166
+ baseURL: rerankBaseURL,
167
+ apiKey: raw.reranker?.apiKey,
168
+ model: raw.reranker?.model,
169
+ url: rerankUrl,
170
+ overfetch: raw.reranker?.overfetch,
171
+ candidateFloor: raw.reranker?.candidateFloor,
108
172
  },
109
173
  indexer: {
110
174
  chunkSize: raw.indexing?.chunkSize,
111
175
  chunkOverlap: raw.indexing?.chunkOverlap,
112
176
  batchSize: raw.indexing?.embedBatchSize,
113
177
  astBatchSize: raw.indexing?.astBatchSize,
178
+ chunkStrategy: raw.indexing?.chunkStrategy,
114
179
  },
115
180
  search: {
116
181
  similarityThreshold: raw.search?.similarityThreshold,
117
182
  defaultLimit: raw.search?.defaultLimit,
118
183
  maxLimit: raw.search?.maxLimit,
119
- autoTranslate: raw.search?.autoTranslate,
120
- translateModel: raw.search?.translateModel,
121
- rerankerEnabled: raw.reranker?.enabled,
122
- rerankerOverfetch: raw.reranker?.overfetch,
123
184
  },
124
185
  logging: {
125
186
  level: raw.logging?.level,
126
187
  dir: raw.logging?.dir,
188
+ searchMetrics: raw.logging?.searchMetrics,
127
189
  },
128
190
  workspaceRoot: raw.workspaceRoot,
129
191
  });
@@ -131,7 +193,7 @@ class Config {
131
193
  const issues = result.error.issues
132
194
  .map(i => ` ${i.path.join('.')}: ${i.message}`)
133
195
  .join('\n');
134
- throw new ConfigError(`Invalid config:\n${issues}\nRun rag-setup or: rag-config set <key> <value>`);
196
+ throw new config_error_1.ConfigError(`Invalid config:\n${issues}\nRun rag-setup or: rag-config set <key> <value>`);
135
197
  }
136
198
  this.data = result.data;
137
199
  }
@@ -1,2 +1,42 @@
1
- export declare const SUPPORTED_EMBED_MODELS: readonly ["bge-m3"];
2
- export declare const DEFAULT_EMBED_MODEL: string;
1
+ export type EmbeddingProvider = 'openai-compatible' | 'voyage' | 'infinity';
2
+ /**
3
+ * pgvector's HNSW / IVFFlat index hard limit. The effective embedding dimension
4
+ * must not exceed this; larger cloud models must reduce via the Matryoshka
5
+ * `dimensions` request parameter (`embedding.dimensions`).
6
+ */
7
+ export declare const MAX_EMBED_DIM = 2000;
8
+ export interface ModelInfo {
9
+ provider: EmbeddingProvider;
10
+ /** Native / default output width. */
11
+ dimensions: number;
12
+ /** Model supports Matryoshka dimension reduction via a request parameter. */
13
+ dimConfigurable?: boolean;
14
+ /** Max width that can be indexed under the pgvector cap (advisory). */
15
+ maxIndexable?: number;
16
+ /** Max width the model itself can emit (advisory). */
17
+ maxDim?: number;
18
+ }
19
+ /**
20
+ * Advisory (NOT exhaustive) registry of known embedding models. Used to resolve
21
+ * a model's dimension without a runtime probe and to suggest models in the
22
+ * dashboard. Models absent here still work — their dimension is probed at index
23
+ * time (see `resolveDimension`).
24
+ */
25
+ export declare const MODEL_REGISTRY: Record<string, ModelInfo>;
26
+ export declare const DEFAULT_EMBED_MODEL = "jinaai/jina-embeddings-v2-base-code";
27
+ /**
28
+ * Former Ollama registry model names, kept only so config validation can
29
+ * recognise an implicit (provider-less) config that still targets Ollama —
30
+ * see `detectFormerOllamaConfig` in `config-store.ts`.
31
+ */
32
+ export declare const FORMER_OLLAMA_MODELS: readonly ["bge-m3", "nomic-embed-text", "mxbai-embed-large"];
33
+ /** Advisory dimension for a model, or `undefined` when the model is unknown. */
34
+ export declare function registryDimension(model: string): number | undefined;
35
+ /**
36
+ * Resolve the effective vector dimension: explicit override → registry → probe.
37
+ * Validates the result is ≤ MAX_EMBED_DIM, throwing an error that names
38
+ * `embedding.dimensions` as the fix. `probeFn` embeds a short string once and
39
+ * returns the observed length; only called when neither override nor registry
40
+ * yields a value. Pure aside from the injected probe.
41
+ */
42
+ export declare function resolveDimension(override: number | null | undefined, model: string, probeFn: () => Promise<number>, registry?: Record<string, ModelInfo>): Promise<number>;
@@ -1,9 +1,63 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_EMBED_MODEL = exports.SUPPORTED_EMBED_MODELS = void 0;
4
- // Embedding model is tied to the vector column dimension (bge-m3 → 1024).
5
- // Only 1024-dim models belong here — switching requires dropping/recreating the
6
- // `embedding` column and re-indexing. The dashboard offers this list as a strict
7
- // dropdown for ollama.model (no free text) so a mismatched model can't be set.
8
- exports.SUPPORTED_EMBED_MODELS = ['bge-m3'];
9
- exports.DEFAULT_EMBED_MODEL = exports.SUPPORTED_EMBED_MODELS[0];
3
+ exports.FORMER_OLLAMA_MODELS = exports.DEFAULT_EMBED_MODEL = exports.MODEL_REGISTRY = exports.MAX_EMBED_DIM = void 0;
4
+ exports.registryDimension = registryDimension;
5
+ exports.resolveDimension = resolveDimension;
6
+ /**
7
+ * pgvector's HNSW / IVFFlat index hard limit. The effective embedding dimension
8
+ * must not exceed this; larger cloud models must reduce via the Matryoshka
9
+ * `dimensions` request parameter (`embedding.dimensions`).
10
+ */
11
+ exports.MAX_EMBED_DIM = 2000;
12
+ /**
13
+ * Advisory (NOT exhaustive) registry of known embedding models. Used to resolve
14
+ * a model's dimension without a runtime probe and to suggest models in the
15
+ * dashboard. Models absent here still work — their dimension is probed at index
16
+ * time (see `resolveDimension`).
17
+ */
18
+ exports.MODEL_REGISTRY = {
19
+ 'text-embedding-3-small': { provider: 'openai-compatible', dimensions: 1536, dimConfigurable: true },
20
+ 'text-embedding-3-large': { provider: 'openai-compatible', dimensions: 3072, dimConfigurable: true, maxIndexable: 2000 },
21
+ 'text-embedding-ada-002': { provider: 'openai-compatible', dimensions: 1536 },
22
+ 'voyage-code-3': { provider: 'voyage', dimensions: 1024, dimConfigurable: true, maxDim: 2048 },
23
+ 'voyage-3': { provider: 'voyage', dimensions: 1024, dimConfigurable: true },
24
+ 'voyage-3-lite': { provider: 'voyage', dimensions: 512 },
25
+ 'BAAI/bge-m3': { provider: 'infinity', dimensions: 1024 },
26
+ 'jinaai/jina-embeddings-v2-base-code': { provider: 'infinity', dimensions: 768 },
27
+ };
28
+ exports.DEFAULT_EMBED_MODEL = 'jinaai/jina-embeddings-v2-base-code';
29
+ /**
30
+ * Former Ollama registry model names, kept only so config validation can
31
+ * recognise an implicit (provider-less) config that still targets Ollama —
32
+ * see `detectFormerOllamaConfig` in `config-store.ts`.
33
+ */
34
+ exports.FORMER_OLLAMA_MODELS = ['bge-m3', 'nomic-embed-text', 'mxbai-embed-large'];
35
+ /** Advisory dimension for a model, or `undefined` when the model is unknown. */
36
+ function registryDimension(model) {
37
+ return exports.MODEL_REGISTRY[model]?.dimensions;
38
+ }
39
+ /**
40
+ * Resolve the effective vector dimension: explicit override → registry → probe.
41
+ * Validates the result is ≤ MAX_EMBED_DIM, throwing an error that names
42
+ * `embedding.dimensions` as the fix. `probeFn` embeds a short string once and
43
+ * returns the observed length; only called when neither override nor registry
44
+ * yields a value. Pure aside from the injected probe.
45
+ */
46
+ async function resolveDimension(override, model, probeFn, registry = exports.MODEL_REGISTRY) {
47
+ let dim;
48
+ if (override != null) {
49
+ dim = override;
50
+ }
51
+ else {
52
+ const known = registry[model]?.dimensions;
53
+ dim = known != null ? known : await probeFn();
54
+ }
55
+ if (!Number.isInteger(dim) || dim <= 0) {
56
+ throw new Error(`Could not resolve a valid embedding dimension for model "${model}" (got ${dim}). Set embedding.dimensions explicitly.`);
57
+ }
58
+ if (dim > exports.MAX_EMBED_DIM) {
59
+ throw new Error(`Embedding dimension ${dim} for model "${model}" exceeds the pgvector limit of ${exports.MAX_EMBED_DIM}. ` +
60
+ `Set embedding.dimensions to a value ≤ ${exports.MAX_EMBED_DIM} to request dimension-reduced (Matryoshka) vectors.`);
61
+ }
62
+ return dim;
63
+ }
@@ -3,8 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createContainer = createContainer;
4
4
  const config_1 = require("./config");
5
5
  const DatabaseService_1 = require("./services/DatabaseService");
6
- const OllamaEmbedder_1 = require("./services/OllamaEmbedder");
6
+ const EmbedderFactory_1 = require("./services/EmbedderFactory");
7
7
  const AstParserService_1 = require("./services/AstParserService");
8
+ const AstChunker_1 = require("./services/AstChunker");
8
9
  const IndexerService_1 = require("./services/IndexerService");
9
10
  const SearcherService_1 = require("./services/SearcherService");
10
11
  const McpServer_1 = require("./services/McpServer");
@@ -15,10 +16,11 @@ function createContainer(loggerName) {
15
16
  const config = new config_1.Config();
16
17
  const logger = LoggerService_1.LoggerService.fromConfig(config.logging, loggerName);
17
18
  const db = new DatabaseService_1.DatabaseService(config);
18
- const embedder = new OllamaEmbedder_1.OllamaEmbedder(config, logger);
19
+ const embedder = (0, EmbedderFactory_1.createEmbedder)(config, logger);
19
20
  const ast = new AstParserService_1.AstParserService(logger);
21
+ const chunker = new AstChunker_1.AstChunker(config);
20
22
  const discovery = new MonorepoAwareDiscoveryService_1.MonorepoAwareDiscoveryService([new NxMonorepoResolver_1.NxMonorepoResolver(logger)], logger);
21
- const indexer = new IndexerService_1.IndexerService(db, embedder, ast, config, logger);
23
+ const indexer = new IndexerService_1.IndexerService(db, embedder, ast, chunker, config, logger);
22
24
  const searcher = new SearcherService_1.SearcherService(db, embedder, discovery, config, logger);
23
25
  const mcp = new McpServer_1.McpServer(searcher, config, logger);
24
26
  return { config, db, embedder, indexer, searcher, mcp, logger, discovery };
@@ -1,2 +1,2 @@
1
- export { codeChunk, codeDependency, codeSymbol, codeCall } from './schema';
1
+ export { codeChunk, codeDependency, codeSymbol, codeCall, indexMeta } from './schema';
2
2
  export type { CodeChunkInsert } from './schema';
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.codeCall = exports.codeSymbol = exports.codeDependency = exports.codeChunk = void 0;
3
+ exports.indexMeta = exports.codeCall = exports.codeSymbol = exports.codeDependency = exports.codeChunk = void 0;
4
4
  var schema_1 = require("./schema");
5
5
  Object.defineProperty(exports, "codeChunk", { enumerable: true, get: function () { return schema_1.codeChunk; } });
6
6
  Object.defineProperty(exports, "codeDependency", { enumerable: true, get: function () { return schema_1.codeDependency; } });
7
7
  Object.defineProperty(exports, "codeSymbol", { enumerable: true, get: function () { return schema_1.codeSymbol; } });
8
8
  Object.defineProperty(exports, "codeCall", { enumerable: true, get: function () { return schema_1.codeCall; } });
9
+ Object.defineProperty(exports, "indexMeta", { enumerable: true, get: function () { return schema_1.indexMeta; } });
@@ -142,6 +142,25 @@ export declare const codeChunk: import("drizzle-orm/pg-core").PgTableWithColumns
142
142
  }, {}, {
143
143
  length: 64;
144
144
  }>;
145
+ ownerPath: import("drizzle-orm/pg-core").PgColumn<{
146
+ name: "ownerPath";
147
+ tableName: "code_chunk";
148
+ dataType: "string";
149
+ columnType: "PgVarchar";
150
+ data: string;
151
+ driverParam: string;
152
+ notNull: false;
153
+ hasDefault: false;
154
+ isPrimaryKey: false;
155
+ isAutoincrement: false;
156
+ hasRuntimeDefault: false;
157
+ enumValues: [string, ...string[]];
158
+ baseColumn: never;
159
+ identity: undefined;
160
+ generated: undefined;
161
+ }, {}, {
162
+ length: number | undefined;
163
+ }>;
145
164
  };
146
165
  dialect: "pg";
147
166
  }>;
@@ -574,4 +593,96 @@ export declare const codeCall: import("drizzle-orm/pg-core").PgTableWithColumns<
574
593
  };
575
594
  dialect: "pg";
576
595
  }>;
596
+ export declare const indexMeta: import("drizzle-orm/pg-core").PgTableWithColumns<{
597
+ name: "index_meta";
598
+ schema: undefined;
599
+ columns: {
600
+ id: import("drizzle-orm/pg-core").PgColumn<{
601
+ name: "id";
602
+ tableName: "index_meta";
603
+ dataType: "boolean";
604
+ columnType: "PgBoolean";
605
+ data: boolean;
606
+ driverParam: boolean;
607
+ notNull: true;
608
+ hasDefault: true;
609
+ isPrimaryKey: true;
610
+ isAutoincrement: false;
611
+ hasRuntimeDefault: false;
612
+ enumValues: undefined;
613
+ baseColumn: never;
614
+ identity: undefined;
615
+ generated: undefined;
616
+ }, {}, {}>;
617
+ provider: import("drizzle-orm/pg-core").PgColumn<{
618
+ name: "provider";
619
+ tableName: "index_meta";
620
+ dataType: "string";
621
+ columnType: "PgText";
622
+ data: string;
623
+ driverParam: string;
624
+ notNull: true;
625
+ hasDefault: false;
626
+ isPrimaryKey: false;
627
+ isAutoincrement: false;
628
+ hasRuntimeDefault: false;
629
+ enumValues: [string, ...string[]];
630
+ baseColumn: never;
631
+ identity: undefined;
632
+ generated: undefined;
633
+ }, {}, {}>;
634
+ model: import("drizzle-orm/pg-core").PgColumn<{
635
+ name: "model";
636
+ tableName: "index_meta";
637
+ dataType: "string";
638
+ columnType: "PgText";
639
+ data: string;
640
+ driverParam: string;
641
+ notNull: true;
642
+ hasDefault: false;
643
+ isPrimaryKey: false;
644
+ isAutoincrement: false;
645
+ hasRuntimeDefault: false;
646
+ enumValues: [string, ...string[]];
647
+ baseColumn: never;
648
+ identity: undefined;
649
+ generated: undefined;
650
+ }, {}, {}>;
651
+ dimensions: import("drizzle-orm/pg-core").PgColumn<{
652
+ name: "dimensions";
653
+ tableName: "index_meta";
654
+ dataType: "number";
655
+ columnType: "PgInteger";
656
+ data: number;
657
+ driverParam: string | number;
658
+ notNull: true;
659
+ hasDefault: false;
660
+ isPrimaryKey: false;
661
+ isAutoincrement: false;
662
+ hasRuntimeDefault: false;
663
+ enumValues: undefined;
664
+ baseColumn: never;
665
+ identity: undefined;
666
+ generated: undefined;
667
+ }, {}, {}>;
668
+ indexedAt: import("drizzle-orm/pg-core").PgColumn<{
669
+ name: "indexedAt";
670
+ tableName: "index_meta";
671
+ dataType: "date";
672
+ columnType: "PgTimestamp";
673
+ data: Date;
674
+ driverParam: string;
675
+ notNull: true;
676
+ hasDefault: true;
677
+ isPrimaryKey: false;
678
+ isAutoincrement: false;
679
+ hasRuntimeDefault: false;
680
+ enumValues: undefined;
681
+ baseColumn: never;
682
+ identity: undefined;
683
+ generated: undefined;
684
+ }, {}, {}>;
685
+ };
686
+ dialect: "pg";
687
+ }>;
577
688
  export type CodeChunkInsert = typeof codeChunk.$inferInsert;
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.codeCall = exports.codeSymbol = exports.codeDependency = exports.codeChunk = void 0;
3
+ exports.indexMeta = exports.codeCall = exports.codeSymbol = exports.codeDependency = exports.codeChunk = void 0;
4
4
  const pg_core_1 = require("drizzle-orm/pg-core");
5
+ // NOTE: the `dimensions` literal below is nominal only. `dataType()` is invoked
6
+ // solely by drizzle-kit DDL generation, which this project does not use — the
7
+ // `code_chunk` table is created by raw SQL in DatabaseService at the dimension
8
+ // resolved from the active model, and the real width is recorded in index_meta.
5
9
  const vector = (0, pg_core_1.customType)({
6
10
  dataType(config) { return `vector(${config?.dimensions ?? 1024})`; },
7
11
  });
@@ -14,6 +18,7 @@ exports.codeChunk = (0, pg_core_1.pgTable)('code_chunk', {
14
18
  embedding: vector('embedding', { dimensions: 1024 }).notNull(),
15
19
  lastModified: (0, pg_core_1.doublePrecision)('lastModified'),
16
20
  contentHash: (0, pg_core_1.varchar)('contentHash', { length: 64 }),
21
+ ownerPath: (0, pg_core_1.varchar)('ownerPath'),
17
22
  });
18
23
  exports.codeDependency = (0, pg_core_1.pgTable)('code_dependency', {
19
24
  id: (0, pg_core_1.uuid)('id').primaryKey().defaultRandom(),
@@ -45,3 +50,12 @@ exports.codeCall = (0, pg_core_1.pgTable)('code_call', {
45
50
  line: (0, pg_core_1.integer)('line'),
46
51
  col: (0, pg_core_1.integer)('col'),
47
52
  });
53
+ // Single-row source of truth for which model built the current index. The
54
+ // `id` column is a boolean pinned to `true` so at most one row can exist.
55
+ exports.indexMeta = (0, pg_core_1.pgTable)('index_meta', {
56
+ id: (0, pg_core_1.boolean)('id').primaryKey().default(true),
57
+ provider: (0, pg_core_1.text)('provider').notNull(),
58
+ model: (0, pg_core_1.text)('model').notNull(),
59
+ dimensions: (0, pg_core_1.integer)('dimensions').notNull(),
60
+ indexedAt: (0, pg_core_1.timestamp)('indexedAt', { withTimezone: true }).defaultNow().notNull(),
61
+ });
package/dist/src/help.js CHANGED
@@ -29,7 +29,7 @@ function printHelp(command) {
29
29
  case 'setup':
30
30
  console.log('Usage: rag-setup');
31
31
  console.log('');
32
- console.log('Interactive setup: configure DB, Ollama, and workspaceRoot.');
32
+ console.log('Interactive setup: configure DB, embedding backend, and workspaceRoot.');
33
33
  console.log('Run once before using rag-index or rag-mcp.');
34
34
  console.log('');
35
35
  console.log('Options:');
@@ -50,13 +50,14 @@ function printHelp(command) {
50
50
  console.log('');
51
51
  console.log('Keys:');
52
52
  console.log(' db.host, db.port, db.user, db.password, db.name');
53
- console.log(' ollama.host, ollama.model');
53
+ console.log(' embedding.provider, embedding.baseURL, embedding.apiKey, embedding.model,');
54
+ console.log(' embedding.dimensions, embedding.batchSize');
54
55
  console.log(' indexing.chunkSize, indexing.chunkOverlap, indexing.embedBatchSize');
55
56
  console.log(' search.similarityThreshold, search.defaultLimit, search.maxLimit');
56
- console.log(' search.autoTranslate, search.translateModel');
57
- console.log(' reranker.enabled, reranker.model, reranker.url, reranker.overfetch');
57
+ console.log(' reranker.enabled, reranker.provider, reranker.baseURL, reranker.apiKey,');
58
+ console.log(' reranker.model, reranker.url, reranker.overfetch');
58
59
  console.log(' logging.level, logging.dir');
59
- console.log(' workspaceRoot');
60
+ console.log(' workspaceRoot (API keys: use env:VAR_NAME → ~/.config/codesearch-mcp/.env)');
60
61
  break;
61
62
  case 'rag-web':
62
63
  case 'web':
@@ -26,10 +26,24 @@ export interface CallEdge {
26
26
  line: number | null;
27
27
  col: number | null;
28
28
  }
29
+ /**
30
+ * Line range of a named scope used only to compute a chunk's owning declaration
31
+ * (`ownerPath`). Method/constructor/accessor ranges carry their enclosing class
32
+ * as `container`; top-level declarations use `container: null`. These are NOT
33
+ * persisted to `code_symbol` — they exist only for chunk ownership.
34
+ */
35
+ export interface OwnerRange {
36
+ name: string;
37
+ container: string | null;
38
+ line: number;
39
+ endLine: number;
40
+ }
29
41
  export interface FileAnalysis {
30
42
  edges: DepEdge[];
31
43
  symbols: SymbolEntry[];
32
44
  calls: CallEdge[];
45
+ /** Method/accessor/constructor ranges for owner-path resolution (Phase 2). */
46
+ ownerRanges?: OwnerRange[];
33
47
  }
34
48
  export interface AnalyzeProjectOpts {
35
49
  skipDependencyResolution?: boolean;
@@ -0,0 +1,24 @@
1
+ import type { SymbolEntry } from './IAstParser';
2
+ /** One embeddable unit of source with its 1-based inclusive line range. */
3
+ export interface ChunkPiece {
4
+ content: string;
5
+ startLine: number;
6
+ endLine: number;
7
+ }
8
+ export interface ChunkOpts {
9
+ /** 'ast' = symbol-aligned boundaries; 'character' = fixed-size recursive splitter. */
10
+ strategy: 'ast' | 'character';
11
+ /** Schema files (.prisma/.graphql/.gql) always use the schema separator splitter. */
12
+ isSchemaFile: boolean;
13
+ /** Top-level symbol ranges for the file; may be empty (no analysis available). */
14
+ symbols: SymbolEntry[];
15
+ }
16
+ export interface IChunker {
17
+ /**
18
+ * Split source into embeddable pieces. AST strategy aligns boundaries to
19
+ * top-level declaration ranges and never emits a piece longer than the
20
+ * configured chunk size; falls back to the character splitter for schema
21
+ * files, empty-symbol files, or when strategy is 'character'.
22
+ */
23
+ chunk(code: string, opts: ChunkOpts): Promise<ChunkPiece[]>;
24
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,4 +1,10 @@
1
1
  import type { DrizzleDb, DrizzleTx } from '../services/DatabaseService';
2
+ /** Single-row record of which embedding model built the current index. */
3
+ export interface IndexMeta {
4
+ provider: string;
5
+ model: string;
6
+ dimensions: number;
7
+ }
2
8
  /**
3
9
  * Database abstraction — services depend on this interface
4
10
  * rather than the concrete DatabaseService.
@@ -10,4 +16,29 @@ export interface IDatabase {
10
16
  initDb(): Promise<void>;
11
17
  initDbWithSchema(): Promise<void>;
12
18
  destroy(): Promise<void>;
19
+ /** Read the single `index_meta` row, or null when absent (table missing → null). */
20
+ readIndexMeta(): Promise<IndexMeta | null>;
21
+ /** Upsert the single `index_meta` row. */
22
+ writeIndexMeta(meta: IndexMeta): Promise<void>;
23
+ /** Create `code_chunk` (+ HNSW/service indexes) at `dimensions` if absent. */
24
+ ensureChunkTable(dimensions: number): Promise<void>;
25
+ /** Drop and recreate `code_chunk` at `dimensions` (dimension change). */
26
+ recreateChunkTable(dimensions: number): Promise<void>;
27
+ /**
28
+ * Return all file paths referenced in any of the four index tables for `project`.
29
+ * Union of code_chunk.filePath, code_symbol.filePath, code_call.fromFile,
30
+ * code_dependency.fromFile. calleeFile and toFile are excluded (may belong to other projects).
31
+ */
32
+ listKnownFiles(project: string): Promise<string[]>;
33
+ /**
34
+ * Delete rows for `filePaths` from all four index tables in one transaction.
35
+ * code_call rows where the file is either fromFile OR calleeFile are removed.
36
+ * code_dependency rows where the file is either fromFile OR toFile are removed.
37
+ * No-op when `filePaths` is empty.
38
+ */
39
+ deleteFiles(project: string, filePaths: string[]): Promise<void>;
40
+ /** Return all project names referenced in any of the four index tables. */
41
+ listIndexedProjects(): Promise<string[]>;
42
+ /** Delete all rows for `name` from the four index tables in one transaction. */
43
+ deleteProject(name: string): Promise<void>;
13
44
  }
@@ -1,6 +1,18 @@
1
+ export type InputType = 'query' | 'document';
1
2
  export interface IEmbedder {
2
- embed(text: string): Promise<number[]>;
3
- translate?(text: string, model: string): Promise<string>;
3
+ /**
4
+ * The vector width this embedder produces, when known without a probe:
5
+ * `embedding.dimensions` override → model registry, else `0` for models whose
6
+ * width is only knowable by embedding once (see `resolveDimension`). Read by
7
+ * `SearcherService.assertIndexMatches` to catch a dimension change on the
8
+ * search side (skipped when `0`). Indexing resolves the true width via
9
+ * `resolveDimension` (which probes when needed), not this field.
10
+ */
11
+ readonly dimensions: number;
12
+ embed(text: string, inputType?: InputType): Promise<number[]>;
13
+ /** One vector per input, in input order. The indexing path. */
14
+ embedBatch(texts: string[], inputType?: InputType): Promise<number[][]>;
4
15
  rerank?(query: string, documents: string[], topN: number): Promise<number[]>;
5
16
  listModels?(): Promise<string[]>;
17
+ ensureModelAvailable?(): Promise<void>;
6
18
  }