@esankhan3/code-search-mcp 0.0.2 → 0.1.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.
@@ -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
- };
@@ -1,59 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { homedir } from "node:os";
4
- const DEFAULT_CONFIG = {
5
- embedding: { provider: "auto", dimensions: 1024 },
6
- chunking: { maxTokens: 500, contextEnrichment: "structural" },
7
- retrieval: {
8
- maxChunks: 20,
9
- maxTokens: 12e3,
10
- hybridWeights: { vector: 0.5, bm25: 0.3, graph: 0.2 },
11
- reranker: "ollama"
12
- },
13
- autoIndex: true
14
- };
15
- function loadKnowledgeConfig(project) {
16
- const anvilHome = process.env.ANVIL_HOME || join(homedir(), ".anvil");
17
- const paths = [
18
- join(anvilHome, "projects", project, "factory.yaml"),
19
- join(anvilHome, "projects", project, "project.yaml")
20
- ];
21
- for (const p of paths) {
22
- if (!existsSync(p)) continue;
23
- try {
24
- const raw = readFileSync(p, "utf-8");
25
- return parseKnowledgeSection(raw);
26
- } catch {
27
- }
28
- }
29
- return { ...DEFAULT_CONFIG };
30
- }
31
- function parseKnowledgeSection(yaml) {
32
- const config = { ...DEFAULT_CONFIG };
33
- const providerMatch = yaml.match(/^\s{4}provider:\s+(\S+)/m);
34
- if (providerMatch) {
35
- config.embedding = { ...config.embedding, provider: providerMatch[1] };
36
- }
37
- const modelMatch = yaml.match(/^\s{4}model:\s+(\S+)/m);
38
- if (modelMatch) config.embedding.model = modelMatch[1];
39
- const dimMatch = yaml.match(/^\s{4}dimensions:\s+(\d+)/m);
40
- if (dimMatch) config.embedding.dimensions = parseInt(dimMatch[1], 10);
41
- const chunkMatch = yaml.match(/^\s{4}max_tokens:\s+(\d+)/m);
42
- if (chunkMatch) config.chunking.maxTokens = parseInt(chunkMatch[1], 10);
43
- const enrichMatch = yaml.match(/^\s{4}context_enrichment:\s+(\S+)/m);
44
- if (enrichMatch) config.chunking.contextEnrichment = enrichMatch[1];
45
- const autoMatch = yaml.match(/^\s{2}auto_index:\s+(true|false)/m);
46
- if (autoMatch) config.autoIndex = autoMatch[1] === "true";
47
- return config;
48
- }
49
- function getKnowledgeBasePath(project) {
50
- const dataDir = process.env.CODE_SEARCH_DATA_DIR;
51
- if (dataDir) return join(dataDir, project);
52
- const anvilHome = process.env.ANVIL_HOME || join(homedir(), ".anvil");
53
- return join(anvilHome, "knowledge-base", project);
54
- }
55
- export {
56
- DEFAULT_CONFIG,
57
- getKnowledgeBasePath,
58
- loadKnowledgeConfig
59
- };