codesearch-mcp 0.0.6-alpha → 0.0.7-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.
- package/LICENSE +21 -0
- package/dist/app.js +40 -2
- package/dist/src/ast.d.ts +1 -1
- package/dist/src/ast.js +68 -14
- package/dist/src/indexer.js +1 -1
- package/dist/src/mcp.js +22 -4
- package/dist/src/searcher.d.ts +8 -0
- package/dist/src/searcher.js +37 -0
- package/package.json +2 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aleks
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/app.js
CHANGED
|
@@ -60,7 +60,8 @@ async function main() {
|
|
|
60
60
|
const isIndex = argv1 === 'rag-index' || process.argv[2] === 'index';
|
|
61
61
|
const isMcp = argv1 === 'rag-mcp' || process.argv[2] === 'mcp';
|
|
62
62
|
const isSetup = argv1 === 'rag-setup' || process.argv[2] === 'setup';
|
|
63
|
-
const
|
|
63
|
+
const isBackfill = argv1 === 'rag-index' && process.argv[2] === 'backfill';
|
|
64
|
+
const mode = isBackfill ? 'backfill' : isIndex ? 'index' : isMcp ? 'mcp' : isSetup ? 'setup' : (process.argv[2] ?? '');
|
|
64
65
|
if (mode === 'index') {
|
|
65
66
|
const { initDbWithSchema } = await Promise.resolve().then(() => __importStar(require('./src/db')));
|
|
66
67
|
const { discoverProjects, scanAndIndex } = await Promise.resolve().then(() => __importStar(require('./src/indexer')));
|
|
@@ -123,8 +124,45 @@ async function main() {
|
|
|
123
124
|
const { runSetup } = await Promise.resolve().then(() => __importStar(require('./src/setup')));
|
|
124
125
|
await runSetup();
|
|
125
126
|
}
|
|
127
|
+
else if (mode === 'backfill') {
|
|
128
|
+
const { initDbWithSchema, db, CodeDependency } = await Promise.resolve().then(() => __importStar(require('./src/db')));
|
|
129
|
+
const { extractImportEdges } = await Promise.resolve().then(() => __importStar(require('./src/ast')));
|
|
130
|
+
const { config } = await Promise.resolve().then(() => __importStar(require('./src/config')));
|
|
131
|
+
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
132
|
+
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
|
133
|
+
await initDbWithSchema();
|
|
134
|
+
const rows = await db.query(`SELECT DISTINCT "serviceName", "filePath" FROM code_chunk ORDER BY "serviceName", "filePath"`);
|
|
135
|
+
console.log(`Backfilling dependency edges for ${rows.length} indexed files...\n`);
|
|
136
|
+
let processed = 0, skipped = 0, totalEdges = 0;
|
|
137
|
+
for (const { serviceName, filePath } of rows) {
|
|
138
|
+
const absPath = path.join(config.workspaceRoot, filePath);
|
|
139
|
+
const projectRoot = path.join(config.workspaceRoot, serviceName);
|
|
140
|
+
if (!fs.existsSync(absPath)) {
|
|
141
|
+
skipped++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const rawEdges = await extractImportEdges(absPath, serviceName, projectRoot);
|
|
145
|
+
const edges = rawEdges
|
|
146
|
+
.filter(e => e.toFile.startsWith(config.workspaceRoot))
|
|
147
|
+
.map(e => ({ ...e, fromFile: path.relative(config.workspaceRoot, e.fromFile), toFile: path.relative(config.workspaceRoot, e.toFile) }));
|
|
148
|
+
await db.transaction(async (manager) => {
|
|
149
|
+
await manager.delete(CodeDependency, { project: serviceName, fromFile: filePath });
|
|
150
|
+
if (edges.length > 0) {
|
|
151
|
+
await manager.createQueryBuilder().insert().into(CodeDependency).values(edges).execute();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
totalEdges += edges.length;
|
|
155
|
+
processed++;
|
|
156
|
+
if (processed % 50 === 0)
|
|
157
|
+
process.stdout.write(` ${processed}/${rows.length} processed, ${totalEdges} edges...\r`);
|
|
158
|
+
}
|
|
159
|
+
console.log(`\nDone. Processed: ${processed}, Skipped (not on disk): ${skipped}, Total edges: ${totalEdges}`);
|
|
160
|
+
await db.destroy();
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
126
163
|
else {
|
|
127
|
-
console.log('Usage: rag-index
|
|
164
|
+
console.log('Usage: rag-index [path] [--reindex] [--select]');
|
|
165
|
+
console.log(' rag-index backfill');
|
|
128
166
|
console.log(' rag-mcp');
|
|
129
167
|
console.log(' rag-setup');
|
|
130
168
|
process.exit(1);
|
package/dist/src/ast.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { CodeDependency } from './db';
|
|
2
|
-
export declare function extractImportEdges(filePath: string, serviceName: string): Promise<Omit<CodeDependency, 'id'>[]>;
|
|
2
|
+
export declare function extractImportEdges(filePath: string, serviceName: string, projectRoot?: string): Promise<Omit<CodeDependency, 'id'>[]>;
|
package/dist/src/ast.js
CHANGED
|
@@ -1,21 +1,79 @@
|
|
|
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.extractImportEdges = extractImportEdges;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
4
39
|
const ts_morph_1 = require("ts-morph");
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
40
|
+
// Per-project ts-morph instances keyed by tsconfig.json path ('' = no tsconfig fallback).
|
|
41
|
+
// Loading the tsconfig gives ts-morph the compilerOptions.paths needed to resolve aliases like @core/*.
|
|
42
|
+
const morphProjects = new Map();
|
|
43
|
+
// Secondary cache: projectRoot → tsconfig key. Avoids one fs.existsSync per file after first lookup.
|
|
44
|
+
const projectRootKeys = new Map();
|
|
45
|
+
const FALLBACK_OPTS = { skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: false, compilerOptions: { allowJs: true, noEmit: true } };
|
|
46
|
+
function getMorphProject(projectRoot) {
|
|
47
|
+
if (!projectRootKeys.has(projectRoot)) {
|
|
48
|
+
const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
|
|
49
|
+
projectRootKeys.set(projectRoot, fs.existsSync(tsconfigPath) ? tsconfigPath : '');
|
|
50
|
+
}
|
|
51
|
+
const key = projectRootKeys.get(projectRoot);
|
|
52
|
+
if (!morphProjects.has(key)) {
|
|
53
|
+
let project;
|
|
54
|
+
try {
|
|
55
|
+
const opts = key
|
|
56
|
+
? { tsConfigFilePath: key, skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: false }
|
|
57
|
+
: FALLBACK_OPTS;
|
|
58
|
+
project = new ts_morph_1.Project(opts);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// tsconfig is invalid — fall back so other files in the project can still be processed
|
|
62
|
+
project = new ts_morph_1.Project(FALLBACK_OPTS);
|
|
63
|
+
}
|
|
64
|
+
morphProjects.set(key, project);
|
|
65
|
+
}
|
|
66
|
+
return morphProjects.get(key);
|
|
67
|
+
}
|
|
12
68
|
// Extract import edges from a TS/JS file using ts-morph.
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
69
|
+
// projectRoot must be the project root (where tsconfig.json lives) for alias resolution to work.
|
|
70
|
+
// Defaults to the file's directory — callers that need alias resolution must pass the project root explicitly.
|
|
71
|
+
// Removes the source file from the morph project after parsing to prevent memory accumulation.
|
|
72
|
+
async function extractImportEdges(filePath, serviceName, projectRoot = path.dirname(filePath)) {
|
|
16
73
|
// Only process JS/TS files; skip declaration files
|
|
17
74
|
if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath))
|
|
18
75
|
return [];
|
|
76
|
+
const morphProject = getMorphProject(projectRoot);
|
|
19
77
|
// Remove stale cached version before re-adding
|
|
20
78
|
const existing = morphProject.getSourceFile(filePath);
|
|
21
79
|
if (existing)
|
|
@@ -49,10 +107,6 @@ async function extractImportEdges(filePath, serviceName) {
|
|
|
49
107
|
else if (namespaceImport) {
|
|
50
108
|
edges.push({ project: serviceName, fromFile: filePath, fromSymbol: null, toFile, toSymbol: null, depType: 'import-namespace' });
|
|
51
109
|
}
|
|
52
|
-
else {
|
|
53
|
-
// Side-effect import: import './foo'
|
|
54
|
-
edges.push({ project: serviceName, fromFile: filePath, fromSymbol: null, toFile, toSymbol: null, depType: 'import' });
|
|
55
|
-
}
|
|
56
110
|
}
|
|
57
111
|
// Remove from memory — prevents accumulation across large workspaces
|
|
58
112
|
morphProject.removeSourceFile(sourceFile);
|
package/dist/src/indexer.js
CHANGED
|
@@ -90,7 +90,7 @@ async function ingestFile(filePath, serviceName, mtimeMs, projectRoot) {
|
|
|
90
90
|
return { status: 'skipped', chunks: 0 };
|
|
91
91
|
}
|
|
92
92
|
// Extract import edges before the transaction — disk I/O must not hold a DB connection open
|
|
93
|
-
const rawEdges = await (0, ast_1.extractImportEdges)(filePath, serviceName);
|
|
93
|
+
const rawEdges = await (0, ast_1.extractImportEdges)(filePath, serviceName, projectRoot);
|
|
94
94
|
const edges = rawEdges
|
|
95
95
|
.filter(e => e.toFile.startsWith(config_1.config.workspaceRoot))
|
|
96
96
|
.map(e => ({
|
package/dist/src/mcp.js
CHANGED
|
@@ -136,7 +136,7 @@ async function startMCP() {
|
|
|
136
136
|
properties: {
|
|
137
137
|
file_path: { type: 'string', description: 'File path as returned by search_codebase' },
|
|
138
138
|
project: { type: 'string', description: 'Project name (use list_projects to find it)' },
|
|
139
|
-
symbol: { type: 'string', description: 'Filter to a specific exported symbol name (
|
|
139
|
+
symbol: { type: 'string', description: 'Filter to a specific exported symbol name. When the graph returns no results, also triggers a text-search fallback to find runtime usages (e.g. via singleton or container).' },
|
|
140
140
|
transitive: { type: 'boolean', description: 'Include transitive (indirect) dependents. Default: false.' },
|
|
141
141
|
},
|
|
142
142
|
required: ['file_path', 'project'],
|
|
@@ -233,10 +233,28 @@ async function startMCP() {
|
|
|
233
233
|
const symbol = req.params.arguments?.symbol ? String(req.params.arguments.symbol) : null;
|
|
234
234
|
const transitive = Boolean(req.params.arguments?.transitive);
|
|
235
235
|
const rows = await (0, searcher_1.getDependents)(filePath, project, symbol, transitive);
|
|
236
|
-
if (rows.length
|
|
237
|
-
return { content: [{ type: 'text', text:
|
|
236
|
+
if (rows.length > 0) {
|
|
237
|
+
return { content: [{ type: 'text', text: formatDependents(filePath, rows, transitive) }] };
|
|
238
|
+
}
|
|
239
|
+
const header = `**Dependents of** \`${filePath}\`\n\n**Direct:** No direct import dependents found.`;
|
|
240
|
+
if (!symbol) {
|
|
241
|
+
return { content: [{ type: 'text', text: `${header}\n\nTip: pass \`symbol\` to trigger a text-search fallback for runtime usages (e.g. via singleton or container).` }] };
|
|
242
|
+
}
|
|
243
|
+
const { matches, truncated } = (0, searcher_1.grepSymbol)(symbol, project, filePath);
|
|
244
|
+
if (matches.length === 0) {
|
|
245
|
+
return { content: [{ type: 'text', text: `${header}\n\n**Fallback \`[text search]\` for \`${symbol}\`:** No results.` }] };
|
|
246
|
+
}
|
|
247
|
+
const grouped = new Map();
|
|
248
|
+
for (const m of matches) {
|
|
249
|
+
if (!grouped.has(m.filePath))
|
|
250
|
+
grouped.set(m.filePath, []);
|
|
251
|
+
grouped.get(m.filePath).push(m.line);
|
|
252
|
+
}
|
|
253
|
+
const fallbackLines = [`\n**Fallback \`[text search]\` for \`${symbol}\`:**${truncated ? ' *(truncated to 200 results)*' : ''}`];
|
|
254
|
+
for (const [fp, lines] of grouped) {
|
|
255
|
+
fallbackLines.push(` ~ \`${fp}\`:${lines.join(', ')}`);
|
|
238
256
|
}
|
|
239
|
-
return { content: [{ type: 'text', text:
|
|
257
|
+
return { content: [{ type: 'text', text: header + fallbackLines.join('\n') }] };
|
|
240
258
|
}
|
|
241
259
|
throw new Error(`Unknown tool: ${req.params.name}`);
|
|
242
260
|
}
|
package/dist/src/searcher.d.ts
CHANGED
|
@@ -47,4 +47,12 @@ export declare function searchChunks(query: string, limit: number, project: stri
|
|
|
47
47
|
export declare function fetchNeighbors(results: SearchResult[]): Promise<NeighborResult[]>;
|
|
48
48
|
export declare function getDependencies(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
|
|
49
49
|
export declare function getDependents(filePath: string, project: string, symbol?: string | null, transitive?: boolean): Promise<DependencyRow[]>;
|
|
50
|
+
export interface GrepMatch {
|
|
51
|
+
filePath: string;
|
|
52
|
+
line: number;
|
|
53
|
+
}
|
|
54
|
+
export declare function grepSymbol(symbol: string, project: string, excludeFile: string): {
|
|
55
|
+
matches: GrepMatch[];
|
|
56
|
+
truncated: boolean;
|
|
57
|
+
};
|
|
50
58
|
export declare function readFileRange(filePath: string, startLine: number, endLine: number): Promise<FileRangeResult>;
|
package/dist/src/searcher.js
CHANGED
|
@@ -39,9 +39,11 @@ exports.searchChunks = searchChunks;
|
|
|
39
39
|
exports.fetchNeighbors = fetchNeighbors;
|
|
40
40
|
exports.getDependencies = getDependencies;
|
|
41
41
|
exports.getDependents = getDependents;
|
|
42
|
+
exports.grepSymbol = grepSymbol;
|
|
42
43
|
exports.readFileRange = readFileRange;
|
|
43
44
|
const fs = __importStar(require("fs"));
|
|
44
45
|
const path = __importStar(require("path"));
|
|
46
|
+
const child_process_1 = require("child_process");
|
|
45
47
|
const ollama_1 = require("ollama");
|
|
46
48
|
const db_1 = require("./db");
|
|
47
49
|
const config_1 = require("./config");
|
|
@@ -191,6 +193,41 @@ async function getDependents(filePath, project, symbol = null, transitive = fals
|
|
|
191
193
|
ORDER BY MIN(depth), "fromFile"`, [filePath, project]);
|
|
192
194
|
return [...direct, ...indirect];
|
|
193
195
|
}
|
|
196
|
+
const GREP_MAX_RESULTS = 200;
|
|
197
|
+
// Grep stops after this many raw lines; generous multiple of GREP_MAX_RESULTS to account for
|
|
198
|
+
// the declaring file's own matches being excluded from the final count.
|
|
199
|
+
const GREP_RAW_LIMIT = GREP_MAX_RESULTS * 5;
|
|
200
|
+
const GREP_EXCLUDE_DIRS = [...file_utils_1.IGNORE_DIRS].map(d => `--exclude-dir=${d}`);
|
|
201
|
+
// Text search for a symbol across all TS/JS files in the project directory.
|
|
202
|
+
// Uses grep -F -w (literal whole-word match) via spawnSync to avoid shell injection.
|
|
203
|
+
// Excludes the declaring file itself. Uses IGNORE_DIRS for consistency with the indexer.
|
|
204
|
+
function grepSymbol(symbol, project, excludeFile) {
|
|
205
|
+
const projectDir = path.join(config_1.config.workspaceRoot, project);
|
|
206
|
+
const result = (0, child_process_1.spawnSync)('grep', [
|
|
207
|
+
'-rn', '-F', '-w',
|
|
208
|
+
'-m', String(GREP_RAW_LIMIT),
|
|
209
|
+
'--include=*.ts', '--include=*.tsx', '--include=*.js', '--include=*.jsx',
|
|
210
|
+
...GREP_EXCLUDE_DIRS,
|
|
211
|
+
symbol,
|
|
212
|
+
projectDir,
|
|
213
|
+
], { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
|
|
214
|
+
if (result.error || !result.stdout) {
|
|
215
|
+
if (result.error)
|
|
216
|
+
console.error(`[grepSymbol] grep failed: ${result.error.message}`);
|
|
217
|
+
return { matches: [], truncated: false };
|
|
218
|
+
}
|
|
219
|
+
const excludeAbsPath = path.join(config_1.config.workspaceRoot, excludeFile);
|
|
220
|
+
const all = result.stdout.split('\n').flatMap(line => {
|
|
221
|
+
const match = line.match(/^(.+?):(\d+):/);
|
|
222
|
+
if (!match)
|
|
223
|
+
return [];
|
|
224
|
+
const [, absFile, lineNum] = match;
|
|
225
|
+
if (absFile === excludeAbsPath)
|
|
226
|
+
return [];
|
|
227
|
+
return [{ filePath: path.relative(config_1.config.workspaceRoot, absFile), line: parseInt(lineNum, 10) }];
|
|
228
|
+
});
|
|
229
|
+
return { matches: all.slice(0, GREP_MAX_RESULTS), truncated: all.length > GREP_MAX_RESULTS };
|
|
230
|
+
}
|
|
194
231
|
// Read a line range from a file that is present in the index
|
|
195
232
|
async function readFileRange(filePath, startLine, endLine) {
|
|
196
233
|
const [{ exists }] = await db_1.db.query(`SELECT EXISTS (SELECT 1 FROM code_chunk WHERE "filePath" = $1) AS exists`, [filePath]);
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codesearch-mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7-alpha",
|
|
4
4
|
"description": "Local semantic codebase search via MCP — indexes projects into pgvector, exposes search to Claude",
|
|
5
|
+
"license": "MIT",
|
|
5
6
|
"main": "dist/app.js",
|
|
6
7
|
"bin": {
|
|
7
8
|
"rag-index": "dist/app.js",
|