codesearch-mcp 0.0.5-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 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 mode = isIndex ? 'index' : isMcp ? 'mcp' : isSetup ? 'setup' : (process.argv[2] ?? '');
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 <path> [--reindex] [--select]');
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
- // Single ts-morph Project instance for import graph extraction.
6
- // skipFileDependencyResolution: false is required so getModuleSpecifierSourceFile() resolves relative imports.
7
- const morphProject = new ts_morph_1.Project({
8
- skipAddingFilesFromTsConfig: true,
9
- skipFileDependencyResolution: false,
10
- compilerOptions: { allowJs: true, noEmit: true },
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
- // Only resolves relative imports (external packages are skipped).
14
- // Removes the source file from morphProject after parsing to prevent memory accumulation.
15
- async function extractImportEdges(filePath, serviceName) {
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);
@@ -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 (optional)' },
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 === 0) {
237
- return { content: [{ type: 'text', text: `No dependents found for: ${filePath}` }] };
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: formatDependents(filePath, rows, transitive) }] };
257
+ return { content: [{ type: 'text', text: header + fallbackLines.join('\n') }] };
240
258
  }
241
259
  throw new Error(`Unknown tool: ${req.params.name}`);
242
260
  }
@@ -74,9 +74,14 @@ async function selectProjects(allProjects, workspaceDir, forceSelect) {
74
74
  const toShow = forceSelect
75
75
  ? allProjects.filter(p => !config.skipped.has(p.name))
76
76
  : newProjects;
77
+ const alreadyIncluded = allProjects.filter(p => config.included.has(p.name));
78
+ if (!forceSelect && alreadyIncluded.length > 0) {
79
+ console.log(`Already selected (will be re-indexed): ${alreadyIncluded.map(p => p.name).join(', ')}`);
80
+ console.log('Run with --select to change the full selection.\n');
81
+ }
77
82
  const message = forceSelect
78
83
  ? 'Select projects to index'
79
- : `Found ${newProjects.length} new project(s) — select which to index`;
84
+ : `Found ${newProjects.length} new project(s) — select which to add`;
80
85
  // Checkbox: pre-check previously included projects
81
86
  const chosen = await (0, prompts_1.checkbox)({
82
87
  message,
@@ -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>;
@@ -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.5-alpha",
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",