@esankhan3/code-search-mcp 0.0.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +343 -248
  2. package/dist/__tests__/config-resolver.test.js +153 -0
  3. package/dist/__tests__/daemon-rpc.test.js +100 -0
  4. package/dist/__tests__/observability.test.js +69 -0
  5. package/dist/__tests__/projects.test.js +141 -0
  6. package/dist/backends/daemon-client.js +87 -0
  7. package/dist/backends/in-process.js +104 -0
  8. package/dist/backends/index.js +29 -0
  9. package/dist/cli/index.js +231 -0
  10. package/dist/core/config.js +362 -49
  11. package/dist/core/env-config.js +45 -43
  12. package/dist/daemon/index.js +119 -0
  13. package/dist/daemon/rpc-server.js +99 -0
  14. package/dist/daemon/watcher.js +105 -0
  15. package/dist/index.js +7 -0
  16. package/dist/observability/logger.js +66 -0
  17. package/dist/observability/metrics.js +138 -0
  18. package/dist/projects/registry.js +146 -0
  19. package/dist/resources/resources.js +3 -3
  20. package/dist/server.js +6 -2
  21. package/dist/tools/graph.js +1 -1
  22. package/dist/tools/index-tools.js +1 -1
  23. package/dist/tools/profile.js +2 -2
  24. package/dist/tools/search.js +1 -1
  25. package/dist/transports/http-transport.js +38 -0
  26. package/package.json +31 -12
  27. package/scripts/smoke/run-smoke.sh +167 -0
  28. package/dist/core/ast-graph-builder.js +0 -800
  29. package/dist/core/chunker.js +0 -368
  30. package/dist/core/claude-runner.js +0 -173
  31. package/dist/core/cross-repo-detector.js +0 -887
  32. package/dist/core/embedder.js +0 -334
  33. package/dist/core/file-walker.js +0 -186
  34. package/dist/core/git-diff.js +0 -160
  35. package/dist/core/graph-metrics.js +0 -233
  36. package/dist/core/indexer.js +0 -552
  37. package/dist/core/project-graph-builder.js +0 -202
  38. package/dist/core/query-classifier.js +0 -144
  39. package/dist/core/query-router.js +0 -210
  40. package/dist/core/rag-evaluator.js +0 -213
  41. package/dist/core/repo-profiler.js +0 -383
  42. package/dist/core/reranker.js +0 -218
  43. package/dist/core/retriever.js +0 -242
  44. package/dist/core/semantic-edge-detector.js +0 -147
  45. package/dist/core/service-mesh-inferrer.js +0 -241
  46. package/dist/core/structural-hasher.js +0 -316
  47. package/dist/core/tree-sitter-parser.js +0 -539
  48. package/dist/core/vector-store.js +0 -259
  49. package/dist/core/workspace-detector.js +0 -481
  50. /package/dist/{core → backends}/types.js +0 -0
@@ -1,368 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { readFileSync, existsSync } from "node:fs";
3
- import { join, relative, extname, basename, dirname } from "node:path";
4
- import { SOURCE_EXTENSIONS, walkDir, langFromExt, extractImports } from "./file-walker.js";
5
- function tsPatterns() {
6
- return [
7
- {
8
- regex: /^export\s+function\s+/,
9
- entityType: "function",
10
- nameExtractor: (l) => l.match(/^export\s+function\s+(\w+)/)?.[1]
11
- },
12
- {
13
- regex: /^export\s+class\s+/,
14
- entityType: "class",
15
- nameExtractor: (l) => l.match(/^export\s+class\s+(\w+)/)?.[1]
16
- },
17
- {
18
- regex: /^export\s+const\s+/,
19
- entityType: "function",
20
- nameExtractor: (l) => l.match(/^export\s+const\s+(\w+)/)?.[1]
21
- },
22
- {
23
- regex: /^function\s+/,
24
- entityType: "function",
25
- nameExtractor: (l) => l.match(/^function\s+(\w+)/)?.[1]
26
- },
27
- {
28
- regex: /^class\s+/,
29
- entityType: "class",
30
- nameExtractor: (l) => l.match(/^class\s+(\w+)/)?.[1]
31
- },
32
- {
33
- regex: /^interface\s+/,
34
- entityType: "interface",
35
- nameExtractor: (l) => l.match(/^interface\s+(\w+)/)?.[1]
36
- },
37
- {
38
- regex: /^type\s+/,
39
- entityType: "type",
40
- nameExtractor: (l) => l.match(/^type\s+(\w+)/)?.[1]
41
- }
42
- ];
43
- }
44
- function pyPatterns() {
45
- return [
46
- {
47
- regex: /^def\s+/,
48
- entityType: "function",
49
- nameExtractor: (l) => l.match(/^def\s+(\w+)/)?.[1]
50
- },
51
- {
52
- regex: /^class\s+/,
53
- entityType: "class",
54
- nameExtractor: (l) => l.match(/^class\s+(\w+)/)?.[1]
55
- },
56
- {
57
- regex: /^async\s+def\s+/,
58
- entityType: "function",
59
- nameExtractor: (l) => l.match(/^async\s+def\s+(\w+)/)?.[1]
60
- }
61
- ];
62
- }
63
- function goPatterns() {
64
- return [
65
- {
66
- regex: /^func\s+/,
67
- entityType: "function",
68
- nameExtractor: (l) => l.match(/^func\s+(?:\([^)]*\)\s+)?(\w+)/)?.[1]
69
- },
70
- {
71
- regex: /^type\s+\w+\s+struct/,
72
- entityType: "class",
73
- nameExtractor: (l) => l.match(/^type\s+(\w+)\s+struct/)?.[1]
74
- },
75
- {
76
- regex: /^type\s+\w+\s+interface/,
77
- entityType: "interface",
78
- nameExtractor: (l) => l.match(/^type\s+(\w+)\s+interface/)?.[1]
79
- }
80
- ];
81
- }
82
- function rsPatterns() {
83
- return [
84
- {
85
- regex: /^(pub\s+)?fn\s+/,
86
- entityType: "function",
87
- nameExtractor: (l) => l.match(/fn\s+(\w+)/)?.[1]
88
- },
89
- {
90
- regex: /^(pub\s+)?struct\s+/,
91
- entityType: "class",
92
- nameExtractor: (l) => l.match(/struct\s+(\w+)/)?.[1]
93
- },
94
- {
95
- regex: /^impl\s+/,
96
- entityType: "class",
97
- nameExtractor: (l) => l.match(/^impl\s+(?:<[^>]+>\s+)?(\w+)/)?.[1]
98
- },
99
- {
100
- regex: /^(pub\s+)?trait\s+/,
101
- entityType: "interface",
102
- nameExtractor: (l) => l.match(/trait\s+(\w+)/)?.[1]
103
- },
104
- {
105
- regex: /^(pub\s+)?enum\s+/,
106
- entityType: "type",
107
- nameExtractor: (l) => l.match(/enum\s+(\w+)/)?.[1]
108
- }
109
- ];
110
- }
111
- function javaPatterns() {
112
- return [
113
- {
114
- regex: /^public\s+class\s+/,
115
- entityType: "class",
116
- nameExtractor: (l) => l.match(/class\s+(\w+)/)?.[1]
117
- },
118
- {
119
- regex: /^public\s+void\s+/,
120
- entityType: "method",
121
- nameExtractor: (l) => l.match(/void\s+(\w+)/)?.[1]
122
- },
123
- {
124
- regex: /^private\s+void\s+/,
125
- entityType: "method",
126
- nameExtractor: (l) => l.match(/void\s+(\w+)/)?.[1]
127
- },
128
- {
129
- regex: /^public\s+static\s+/,
130
- entityType: "method",
131
- nameExtractor: (l) => l.match(/static\s+\w+\s+(\w+)/)?.[1]
132
- }
133
- ];
134
- }
135
- function patternsForLanguage(lang) {
136
- switch (lang) {
137
- case "typescript":
138
- case "javascript":
139
- return tsPatterns();
140
- case "python":
141
- return pyPatterns();
142
- case "go":
143
- return goPatterns();
144
- case "rust":
145
- return rsPatterns();
146
- case "java":
147
- return javaPatterns();
148
- default:
149
- return tsPatterns();
150
- }
151
- }
152
- function chunkId(filePath, startLine, endLine) {
153
- return createHash("sha256").update(`${filePath}${startLine}${endLine}`).digest("hex").slice(0, 16);
154
- }
155
- function extractExports(content, lang) {
156
- const exports = [];
157
- if (lang === "typescript" || lang === "javascript") {
158
- const re = /export\s+(?:default\s+)?(?:function|class|const|let|var|interface|type|enum)\s+(\w+)/g;
159
- let m;
160
- while ((m = re.exec(content)) !== null) {
161
- exports.push(m[1]);
162
- }
163
- }
164
- return exports;
165
- }
166
- function buildContextPrefix(relPath, lang, imports) {
167
- const commentPrefix = lang === "python" ? "#" : "//";
168
- const moduleName = dirname(relPath).split("/").filter(Boolean).pop() ?? basename(relPath);
169
- const lines = [
170
- `${commentPrefix} File: ${relPath}`,
171
- `${commentPrefix} Module: ${moduleName}`
172
- ];
173
- if (imports.length > 0) {
174
- const maxImportChars = 150;
175
- const importStr = imports.join(", ");
176
- const capped = importStr.length > maxImportChars ? importStr.slice(0, maxImportChars) + "..." : importStr;
177
- lines.push(`${commentPrefix} Imports: ${capped}`);
178
- }
179
- return lines.join("\n");
180
- }
181
- function contextualizedTokens(contextPrefix, content) {
182
- return Math.ceil((contextPrefix.length + 1 + content.length) / 4);
183
- }
184
- function splitOversizedContent(contentLines, maxContentChars) {
185
- const parts = [];
186
- let current = [];
187
- let currentLen = 0;
188
- for (const line of contentLines) {
189
- const lineLen = line.length + 1;
190
- if (lineLen > maxContentChars) {
191
- if (current.length > 0) {
192
- parts.push(current.join("\n"));
193
- current = [];
194
- currentLen = 0;
195
- }
196
- parts.push(line.slice(0, maxContentChars));
197
- continue;
198
- }
199
- if (currentLen + lineLen > maxContentChars && current.length > 0) {
200
- parts.push(current.join("\n"));
201
- current = [];
202
- currentLen = 0;
203
- }
204
- current.push(line);
205
- currentLen += lineLen;
206
- }
207
- if (current.length > 0) {
208
- parts.push(current.join("\n"));
209
- }
210
- return parts;
211
- }
212
- function chunkFile(filePath, repoPath, repoName, project, maxTokens) {
213
- let raw;
214
- try {
215
- raw = readFileSync(filePath, "utf-8");
216
- } catch {
217
- return [];
218
- }
219
- const relPath = relative(repoPath, filePath);
220
- const ext = extname(filePath);
221
- const lang = langFromExt(ext);
222
- const lines = raw.split("\n");
223
- const patterns = patternsForLanguage(lang);
224
- const fileImports = extractImports(lines, lang);
225
- const contextPrefix = buildContextPrefix(relPath, lang, fileImports);
226
- const prefixTokens = Math.ceil(contextPrefix.length / 4) + 1;
227
- const maxContentTokens = maxTokens - prefixTokens;
228
- const maxContentChars = maxContentTokens * 4;
229
- const boundaries = [];
230
- for (let i = 0; i < lines.length; i++) {
231
- const trimmed = lines[i].trimStart();
232
- for (const pat of patterns) {
233
- if (pat.regex.test(trimmed)) {
234
- boundaries.push({
235
- line: i,
236
- entityType: pat.entityType,
237
- entityName: pat.nameExtractor(trimmed)
238
- });
239
- break;
240
- }
241
- }
242
- }
243
- const chunks = [];
244
- const pushChunk = (content, startLine, endLine, entityType, entityName) => {
245
- const ctxContent = `${contextPrefix}
246
- ${content}`;
247
- const tokens = Math.ceil(ctxContent.length / 4);
248
- const id = chunkId(relPath, startLine, endLine);
249
- chunks.push({
250
- id,
251
- filePath: relPath,
252
- repoName,
253
- project,
254
- startLine,
255
- endLine,
256
- content,
257
- contextPrefix,
258
- contextualizedContent: ctxContent,
259
- language: lang,
260
- entityType,
261
- entityName,
262
- tokens,
263
- imports: fileImports,
264
- exports: extractExports(content, lang)
265
- });
266
- };
267
- const pushContentWithSplit = (contentLines, startLine, entityType, entityName) => {
268
- const content = contentLines.join("\n");
269
- if (contextualizedTokens(contextPrefix, content) <= maxTokens) {
270
- pushChunk(content, startLine, startLine + contentLines.length - 1, entityType, entityName);
271
- } else {
272
- const parts = splitOversizedContent(contentLines, maxContentChars);
273
- let lineOffset = startLine;
274
- for (let p = 0; p < parts.length; p++) {
275
- const partLines = parts[p].split("\n").length;
276
- const partName = entityName ? `${entityName}$${p + 1}` : void 0;
277
- pushChunk(parts[p], lineOffset, lineOffset + partLines - 1, entityType, partName);
278
- lineOffset += partLines;
279
- }
280
- }
281
- };
282
- if (boundaries.length === 0) {
283
- pushContentWithSplit(lines, 1, "module", basename(filePath, ext));
284
- return chunks;
285
- }
286
- if (boundaries[0].line > 0) {
287
- const leadLines = lines.slice(0, boundaries[0].line);
288
- const content = leadLines.join("\n").trimEnd();
289
- if (content.length > 0) {
290
- pushContentWithSplit(leadLines, 1, "import", void 0);
291
- }
292
- }
293
- for (let i = 0; i < boundaries.length; i++) {
294
- const startLine = boundaries[i].line;
295
- const endLine = i + 1 < boundaries.length ? boundaries[i + 1].line - 1 : lines.length - 1;
296
- const chunkLines = lines.slice(startLine, endLine + 1);
297
- pushContentWithSplit(chunkLines, startLine + 1, boundaries[i].entityType, boundaries[i].entityName);
298
- }
299
- return chunks;
300
- }
301
- async function chunkRepo(repoPath, repoName, project, config, cachedFiles) {
302
- const files = [];
303
- walkDir(repoPath, files);
304
- const allChunks = [];
305
- const changedFiles = [];
306
- const fileIndex = {};
307
- const currentFilePaths = /* @__PURE__ */ new Set();
308
- for (const file of files) {
309
- const relPath = relative(repoPath, file);
310
- currentFilePaths.add(relPath);
311
- let contents;
312
- try {
313
- contents = readFileSync(file, "utf-8");
314
- } catch {
315
- continue;
316
- }
317
- const contentHash = createHash("sha256").update(contents).digest("hex");
318
- if (cachedFiles?.[relPath]?.contentHash === contentHash) {
319
- fileIndex[relPath] = cachedFiles[relPath];
320
- continue;
321
- }
322
- changedFiles.push(relPath);
323
- const chunks = chunkFile(file, repoPath, repoName, project, config.maxTokens);
324
- allChunks.push(...chunks);
325
- fileIndex[relPath] = { contentHash, chunkCount: chunks.length };
326
- }
327
- const deletedFiles = [];
328
- if (cachedFiles) {
329
- for (const path of Object.keys(cachedFiles)) {
330
- if (!currentFilePaths.has(path)) {
331
- deletedFiles.push(path);
332
- }
333
- }
334
- }
335
- return { chunks: allChunks, changedFiles, deletedFiles, fileIndex };
336
- }
337
- async function chunkChangedFiles(repoPath, repoName, project, config, diff) {
338
- const allChunks = [];
339
- const changedFiles = [];
340
- const fileIndex = {};
341
- for (const relPath of [...diff.added, ...diff.modified]) {
342
- const fullPath = join(repoPath, relPath);
343
- if (!existsSync(fullPath)) continue;
344
- const ext = extname(fullPath);
345
- if (!SOURCE_EXTENSIONS.has(ext)) continue;
346
- let contents;
347
- try {
348
- contents = readFileSync(fullPath, "utf-8");
349
- } catch {
350
- continue;
351
- }
352
- changedFiles.push(relPath);
353
- const contentHash = createHash("sha256").update(contents).digest("hex");
354
- const chunks = chunkFile(fullPath, repoPath, repoName, project, config.maxTokens);
355
- allChunks.push(...chunks);
356
- fileIndex[relPath] = { contentHash, chunkCount: chunks.length };
357
- }
358
- return {
359
- chunks: allChunks,
360
- changedFiles,
361
- deletedFiles: [...diff.deleted],
362
- fileIndex
363
- };
364
- }
365
- export {
366
- chunkChangedFiles,
367
- chunkRepo
368
- };
@@ -1,173 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { loadServerConfig } from "./env-config.js";
3
- async function runClaude(prompt, systemPrompt, opts) {
4
- const config = loadServerConfig();
5
- if (config.llmMode === "none") {
6
- throw new Error("LLM inference disabled (CODE_SEARCH_LLM_MODE=none)");
7
- }
8
- const model = opts?.model ?? config.llmModel;
9
- const timeoutMs = opts?.timeoutMs ?? 6e5;
10
- if (config.llmMode === "api") {
11
- return runViaApi(prompt, systemPrompt, model, timeoutMs, config);
12
- }
13
- return runViaCli(prompt, systemPrompt, model, timeoutMs, config.claudeBin);
14
- }
15
- function isLlmAvailable() {
16
- const config = loadServerConfig();
17
- return config.llmMode !== "none";
18
- }
19
- function runViaCli(prompt, systemPrompt, model, timeoutMs, claudeBin) {
20
- return new Promise((resolve, reject) => {
21
- const args = [
22
- "-p",
23
- prompt,
24
- "--system-prompt",
25
- systemPrompt,
26
- "--output-format",
27
- "stream-json",
28
- "--verbose",
29
- "--model",
30
- model,
31
- "--permission-mode",
32
- "bypassPermissions"
33
- ];
34
- const proc = spawn(claudeBin, args, {
35
- stdio: ["pipe", "pipe", "pipe"],
36
- env: { ...process.env }
37
- });
38
- proc.stdin?.end();
39
- let buffer = "";
40
- let fullText = "";
41
- let resultData = null;
42
- const timer = setTimeout(() => {
43
- proc.kill("SIGTERM");
44
- reject(new Error(`Claude CLI timed out after ${Math.round(timeoutMs / 1e3)}s`));
45
- }, timeoutMs);
46
- proc.stdout?.on("data", (data) => {
47
- buffer += data.toString();
48
- const lines = buffer.split("\n");
49
- buffer = lines.pop() || "";
50
- for (const line of lines) {
51
- const trimmed = line.trim();
52
- if (!trimmed) continue;
53
- try {
54
- const msg = JSON.parse(trimmed);
55
- if (msg.type === "assistant" && msg.message?.content) {
56
- for (const block of msg.message.content) {
57
- if (block.type === "text" && block.text) {
58
- fullText += block.text;
59
- }
60
- }
61
- }
62
- if (msg.type === "result") {
63
- resultData = {
64
- result: msg.result ?? fullText,
65
- costUsd: msg.total_cost_usd ?? 0,
66
- inputTokens: msg.usage?.input_tokens ?? 0,
67
- outputTokens: msg.usage?.output_tokens ?? 0,
68
- durationMs: msg.duration_ms ?? 0
69
- };
70
- }
71
- } catch {
72
- }
73
- }
74
- });
75
- let stderr = "";
76
- proc.stderr?.on("data", (data) => {
77
- stderr += data.toString();
78
- });
79
- proc.on("error", (err) => {
80
- clearTimeout(timer);
81
- reject(err);
82
- });
83
- proc.on("close", (code) => {
84
- clearTimeout(timer);
85
- if (resultData) {
86
- resolve(resultData);
87
- } else if (code === 0 && fullText) {
88
- resolve({ result: fullText, costUsd: 0, inputTokens: 0, outputTokens: 0, durationMs: 0 });
89
- } else {
90
- reject(new Error(`claude exited with code ${code}: ${stderr.slice(0, 500)}`));
91
- }
92
- });
93
- });
94
- }
95
- async function runViaApi(prompt, systemPrompt, model, timeoutMs, config) {
96
- const startTime = Date.now();
97
- const controller = new AbortController();
98
- const timer = setTimeout(() => controller.abort(), timeoutMs);
99
- try {
100
- const isAnthropic = config.llmProvider === "anthropic";
101
- const url = isAnthropic ? "https://api.anthropic.com/v1/messages" : `${config.llmBaseUrl ?? "https://api.openai.com"}/v1/chat/completions`;
102
- if (!config.llmApiKey) {
103
- throw new Error(
104
- `CODE_SEARCH_LLM_API_KEY not set. Required for LLM_MODE=api. Set it to your ${config.llmProvider} API key, or use LLM_MODE=cli for Claude CLI, or LLM_MODE=none to disable.`
105
- );
106
- }
107
- let body;
108
- let headers;
109
- if (isAnthropic) {
110
- headers = {
111
- "Content-Type": "application/json",
112
- "x-api-key": config.llmApiKey,
113
- "anthropic-version": "2023-06-01"
114
- };
115
- body = JSON.stringify({
116
- model,
117
- max_tokens: 8192,
118
- system: [{ type: "text", text: systemPrompt }],
119
- messages: [{ role: "user", content: prompt }]
120
- });
121
- } else {
122
- headers = {
123
- "Content-Type": "application/json",
124
- "Authorization": `Bearer ${config.llmApiKey}`
125
- };
126
- body = JSON.stringify({
127
- model,
128
- max_tokens: 8192,
129
- messages: [
130
- { role: "system", content: systemPrompt },
131
- { role: "user", content: prompt }
132
- ]
133
- });
134
- }
135
- const response = await fetch(url, {
136
- method: "POST",
137
- headers,
138
- body,
139
- signal: controller.signal
140
- });
141
- if (!response.ok) {
142
- const errBody = await response.text().catch(() => "");
143
- throw new Error(`LLM API error ${response.status}: ${errBody.slice(0, 500)}`);
144
- }
145
- const json = await response.json();
146
- const durationMs = Date.now() - startTime;
147
- if (isAnthropic) {
148
- const text = (json.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("");
149
- return {
150
- result: text,
151
- costUsd: 0,
152
- inputTokens: json.usage?.input_tokens ?? 0,
153
- outputTokens: json.usage?.output_tokens ?? 0,
154
- durationMs
155
- };
156
- } else {
157
- const text = json.choices?.[0]?.message?.content ?? "";
158
- return {
159
- result: text,
160
- costUsd: 0,
161
- inputTokens: json.usage?.prompt_tokens ?? 0,
162
- outputTokens: json.usage?.completion_tokens ?? 0,
163
- durationMs
164
- };
165
- }
166
- } finally {
167
- clearTimeout(timer);
168
- }
169
- }
170
- export {
171
- isLlmAvailable,
172
- runClaude
173
- };