codesearch-mcp 0.0.16-alpha → 0.0.17-alpha.1
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/CHANGELOG.md +37 -1
- package/README.md +59 -36
- package/dist/app.d.ts +1 -1
- package/dist/app.js +67 -16
- package/dist/src/commands/BackfillCommand.js +15 -5
- package/dist/src/commands/ConfigCommand.d.ts +12 -0
- package/dist/src/commands/ConfigCommand.js +98 -0
- package/dist/src/commands/IndexCommand.js +1 -1
- package/dist/src/config-store.d.ts +53 -0
- package/dist/src/config-store.js +197 -0
- package/dist/src/config.d.ts +22 -8
- package/dist/src/config.js +103 -58
- package/dist/src/container.d.ts +1 -1
- package/dist/src/container.js +8 -7
- package/dist/src/db/index.d.ts +1 -1
- package/dist/src/db/index.js +2 -1
- package/dist/src/db/schema.d.ts +160 -0
- package/dist/src/db/schema.js +12 -1
- package/dist/src/help.js +28 -12
- package/dist/src/interfaces/IAstParser.d.ts +14 -1
- package/dist/src/interfaces/IEmbedder.d.ts +1 -0
- package/dist/src/project-selector.d.ts +14 -5
- package/dist/src/project-selector.js +74 -60
- package/dist/src/services/AstParserService.d.ts +5 -1
- package/dist/src/services/AstParserService.js +107 -16
- package/dist/src/services/DatabaseService.js +18 -0
- package/dist/src/services/IndexerService.d.ts +7 -0
- package/dist/src/services/IndexerService.js +160 -40
- package/dist/src/services/LoggerService.d.ts +5 -1
- package/dist/src/services/LoggerService.js +8 -5
- package/dist/src/services/McpServer.d.ts +1 -0
- package/dist/src/services/McpServer.js +72 -4
- package/dist/src/services/OllamaEmbedder.d.ts +2 -0
- package/dist/src/services/OllamaEmbedder.js +29 -0
- package/dist/src/services/SearcherService.d.ts +13 -0
- package/dist/src/services/SearcherService.js +118 -22
- package/dist/src/setup.js +57 -62
- package/package.json +3 -3
package/dist/src/help.js
CHANGED
|
@@ -5,28 +5,22 @@ function printHelp(command) {
|
|
|
5
5
|
switch (command) {
|
|
6
6
|
case 'rag-index':
|
|
7
7
|
case 'index':
|
|
8
|
-
console.log('Usage: rag-index [
|
|
9
|
-
console.log(' rag-index backfill');
|
|
8
|
+
console.log('Usage: rag-index [options]');
|
|
10
9
|
console.log('');
|
|
11
10
|
console.log('Index a workspace of JS/TS projects into the vector database.');
|
|
12
|
-
console.log('');
|
|
13
|
-
console.log('Arguments:');
|
|
14
|
-
console.log(' path Path to workspace (defaults to WORKSPACE_ROOT env var)');
|
|
11
|
+
console.log('Workspace is read from workspaceRoot (set via rag-setup or rag-config).');
|
|
15
12
|
console.log('');
|
|
16
13
|
console.log('Options:');
|
|
17
14
|
console.log(' --reindex Force re-index all files (ignores cached mtimes)');
|
|
18
15
|
console.log(' --select Interactively select which projects to index');
|
|
19
16
|
console.log(' -h, --help Show this help message');
|
|
20
|
-
console.log('');
|
|
21
|
-
console.log('Subcommands:');
|
|
22
|
-
console.log(' backfill Populate symbol index (code_symbol) for already-indexed files');
|
|
23
17
|
break;
|
|
24
18
|
case 'rag-mcp':
|
|
25
19
|
case 'mcp':
|
|
26
20
|
console.log('Usage: rag-mcp');
|
|
27
21
|
console.log('');
|
|
28
22
|
console.log('Start the MCP server for semantic code search.');
|
|
29
|
-
console.log('Requires
|
|
23
|
+
console.log('Requires workspaceRoot to be set (run rag-setup first).');
|
|
30
24
|
console.log('');
|
|
31
25
|
console.log('Options:');
|
|
32
26
|
console.log(' -h, --help Show this help message');
|
|
@@ -35,17 +29,39 @@ function printHelp(command) {
|
|
|
35
29
|
case 'setup':
|
|
36
30
|
console.log('Usage: rag-setup');
|
|
37
31
|
console.log('');
|
|
38
|
-
console.log('Interactive setup: configure DB, Ollama, and
|
|
32
|
+
console.log('Interactive setup: configure DB, Ollama, and workspaceRoot.');
|
|
39
33
|
console.log('Run once before using rag-index or rag-mcp.');
|
|
40
34
|
console.log('');
|
|
41
35
|
console.log('Options:');
|
|
42
36
|
console.log(' -h, --help Show this help message');
|
|
43
37
|
break;
|
|
38
|
+
case 'rag-config':
|
|
39
|
+
case 'config':
|
|
40
|
+
console.log('Usage: rag-config <subcommand>');
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log('Manage settings stored in ~/.config/codesearch-mcp/config.json');
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log('Subcommands:');
|
|
45
|
+
console.log(' show Print current config (password redacted)');
|
|
46
|
+
console.log(' get <key> Print a single value');
|
|
47
|
+
console.log(' set <key> <value> Set a value');
|
|
48
|
+
console.log(' reset <key> Remove key (falls back to built-in default)');
|
|
49
|
+
console.log('');
|
|
50
|
+
console.log('Keys:');
|
|
51
|
+
console.log(' db.host, db.port, db.user, db.password, db.name');
|
|
52
|
+
console.log(' ollama.host, ollama.model');
|
|
53
|
+
console.log(' indexing.chunkSize, indexing.chunkOverlap, indexing.embedBatchSize');
|
|
54
|
+
console.log(' search.similarityThreshold, search.defaultLimit, search.maxLimit');
|
|
55
|
+
console.log(' search.autoTranslate, search.translateModel');
|
|
56
|
+
console.log(' reranker.enabled, reranker.model, reranker.url, reranker.overfetch');
|
|
57
|
+
console.log(' logging.level, logging.dir');
|
|
58
|
+
console.log(' workspaceRoot');
|
|
59
|
+
break;
|
|
44
60
|
default:
|
|
45
|
-
console.log('Usage: rag-index [
|
|
46
|
-
console.log(' rag-index backfill');
|
|
61
|
+
console.log('Usage: rag-index [--reindex] [--select]');
|
|
47
62
|
console.log(' rag-mcp');
|
|
48
63
|
console.log(' rag-setup');
|
|
64
|
+
console.log(' rag-config <show|get|set|reset>');
|
|
49
65
|
console.log('');
|
|
50
66
|
console.log('Run any command with -h or --help for details.');
|
|
51
67
|
}
|
|
@@ -16,15 +16,28 @@ export interface SymbolEntry {
|
|
|
16
16
|
endLine: number | null;
|
|
17
17
|
col: number | null;
|
|
18
18
|
}
|
|
19
|
+
export interface CallEdge {
|
|
20
|
+
project: string;
|
|
21
|
+
fromFile: string;
|
|
22
|
+
fromSymbol: string | null;
|
|
23
|
+
calleeName: string;
|
|
24
|
+
calleeFile: string;
|
|
25
|
+
calleeSymbol: string | null;
|
|
26
|
+
line: number | null;
|
|
27
|
+
col: number | null;
|
|
28
|
+
}
|
|
19
29
|
export interface FileAnalysis {
|
|
20
30
|
edges: DepEdge[];
|
|
21
31
|
symbols: SymbolEntry[];
|
|
32
|
+
calls: CallEdge[];
|
|
22
33
|
}
|
|
23
34
|
export interface IAstParser {
|
|
24
35
|
/**
|
|
25
36
|
* @deprecated Use analyzeProject instead for project-wide context.
|
|
26
37
|
*/
|
|
27
38
|
analyzeFile(filePath: string, serviceName: string, projectRoot?: string): Promise<FileAnalysis>;
|
|
28
|
-
analyzeProject(filePaths: string[], serviceName: string, projectRoot?: string
|
|
39
|
+
analyzeProject(filePaths: string[], serviceName: string, projectRoot?: string, opts?: {
|
|
40
|
+
skipDependencyResolution?: boolean;
|
|
41
|
+
}): Promise<Map<string, FileAnalysis>>;
|
|
29
42
|
clearCache(projectRoot?: string): void;
|
|
30
43
|
}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface Project {
|
|
2
2
|
name: string;
|
|
3
3
|
path: string;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
}
|
|
5
|
+
export declare class ProjectSelector {
|
|
6
|
+
private readonly workspaceDir;
|
|
7
|
+
private included;
|
|
8
|
+
private skipped;
|
|
9
|
+
constructor(workspaceDir: string);
|
|
10
|
+
select(allProjects: Project[], forceSelect: boolean): Promise<Project[]>;
|
|
11
|
+
private printAlreadySelected;
|
|
12
|
+
private promptForSelection;
|
|
13
|
+
private promptForSkip;
|
|
14
|
+
private load;
|
|
15
|
+
private save;
|
|
16
|
+
}
|
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
36
|
+
exports.ProjectSelector = void 0;
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const prompts_1 = require("@inquirer/prompts");
|
|
@@ -65,76 +65,90 @@ async function saveLines(filePath, names) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
async
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const config = await loadConfig(workspaceDir);
|
|
83
|
-
const newProjects = allProjects.filter(p => !config.included.has(p.name) && !config.skipped.has(p.name));
|
|
84
|
-
// Nothing to ask — return previously saved selection silently
|
|
85
|
-
if (newProjects.length === 0 && !forceSelect) {
|
|
86
|
-
const selected = allProjects.filter(p => config.included.has(p.name));
|
|
87
|
-
if (selected.length === 0) {
|
|
88
|
-
console.log('No projects selected. Run with --select to choose projects.');
|
|
68
|
+
class ProjectSelector {
|
|
69
|
+
workspaceDir;
|
|
70
|
+
included = new Set();
|
|
71
|
+
skipped = new Set();
|
|
72
|
+
constructor(workspaceDir) {
|
|
73
|
+
this.workspaceDir = workspaceDir;
|
|
74
|
+
}
|
|
75
|
+
async select(allProjects, forceSelect) {
|
|
76
|
+
await this.load();
|
|
77
|
+
const newProjects = allProjects.filter(p => !this.included.has(p.name) && !this.skipped.has(p.name));
|
|
78
|
+
const previouslySelected = allProjects.filter(p => this.included.has(p.name));
|
|
79
|
+
// Nothing new and something already selected — return silently
|
|
80
|
+
if (newProjects.length === 0 && !forceSelect && previouslySelected.length > 0) {
|
|
81
|
+
return previouslySelected;
|
|
89
82
|
}
|
|
83
|
+
// No previous selection — treat as implicit --select so the user picks projects now
|
|
84
|
+
const autoSelect = !forceSelect && previouslySelected.length === 0;
|
|
85
|
+
// In --select / auto-select mode show all non-skipped projects; otherwise only new ones
|
|
86
|
+
const toShow = (forceSelect || autoSelect)
|
|
87
|
+
? allProjects.filter(p => !this.skipped.has(p.name))
|
|
88
|
+
: newProjects;
|
|
89
|
+
if (toShow.length === 0) {
|
|
90
|
+
console.log('All discovered projects are marked as skipped. Run with --select to reconfigure.');
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
if (!forceSelect && !autoSelect)
|
|
94
|
+
this.printAlreadySelected(allProjects);
|
|
95
|
+
await this.promptForSelection(toShow, forceSelect || autoSelect, newProjects.length);
|
|
96
|
+
await this.promptForSkip(toShow);
|
|
97
|
+
await this.save();
|
|
98
|
+
const selected = allProjects.filter(p => this.included.has(p.name));
|
|
99
|
+
console.log(`\nSelected: ${selected.map(p => p.name).join(', ') || '(none)'}`);
|
|
90
100
|
return selected;
|
|
91
101
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (!forceSelect && alreadyIncluded.length > 0) {
|
|
98
|
-
console.log(`Already selected (will be re-indexed): ${alreadyIncluded.map(p => p.name).join(', ')}`);
|
|
99
|
-
console.log('Run with --select to change the full selection.\n');
|
|
100
|
-
}
|
|
101
|
-
const message = forceSelect
|
|
102
|
-
? 'Select projects to index'
|
|
103
|
-
: `Found ${newProjects.length} new project(s) — select which to add`;
|
|
104
|
-
// Checkbox: pre-check previously included projects
|
|
105
|
-
const chosen = await (0, prompts_1.checkbox)({
|
|
106
|
-
message,
|
|
107
|
-
choices: toShow.map(p => ({
|
|
108
|
-
name: p.name,
|
|
109
|
-
value: p.name,
|
|
110
|
-
checked: config.included.has(p.name),
|
|
111
|
-
})),
|
|
112
|
-
pageSize: 15,
|
|
113
|
-
});
|
|
114
|
-
const chosenSet = new Set(chosen);
|
|
115
|
-
// Update included
|
|
116
|
-
for (const p of toShow) {
|
|
117
|
-
if (chosenSet.has(p.name)) {
|
|
118
|
-
config.included.add(p.name);
|
|
119
|
-
config.skipped.delete(p.name);
|
|
102
|
+
printAlreadySelected(allProjects) {
|
|
103
|
+
const alreadyIncluded = allProjects.filter(p => this.included.has(p.name));
|
|
104
|
+
if (alreadyIncluded.length > 0) {
|
|
105
|
+
console.log(`Already selected (will be re-indexed): ${alreadyIncluded.map(p => p.name).join(', ')}`);
|
|
106
|
+
console.log('Run with --select to change the full selection.\n');
|
|
120
107
|
}
|
|
121
|
-
|
|
122
|
-
|
|
108
|
+
}
|
|
109
|
+
async promptForSelection(toShow, showAll, newCount) {
|
|
110
|
+
const message = showAll
|
|
111
|
+
? 'Select projects to index'
|
|
112
|
+
: `Found ${newCount} new project(s) — select which to add`;
|
|
113
|
+
const chosen = await (0, prompts_1.checkbox)({
|
|
114
|
+
message,
|
|
115
|
+
choices: toShow.map(p => ({ name: p.name, value: p.name, checked: this.included.has(p.name) })),
|
|
116
|
+
pageSize: 15,
|
|
117
|
+
});
|
|
118
|
+
const chosenSet = new Set(chosen);
|
|
119
|
+
for (const p of toShow) {
|
|
120
|
+
if (chosenSet.has(p.name)) {
|
|
121
|
+
this.included.add(p.name);
|
|
122
|
+
this.skipped.delete(p.name);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
this.included.delete(p.name);
|
|
126
|
+
}
|
|
123
127
|
}
|
|
124
128
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
async promptForSkip(toShow) {
|
|
130
|
+
const unchecked = toShow.filter(p => !this.included.has(p.name) && !this.skipped.has(p.name));
|
|
131
|
+
if (unchecked.length === 0)
|
|
132
|
+
return;
|
|
128
133
|
const toSkip = await (0, prompts_1.checkbox)({
|
|
129
134
|
message: 'Always skip (never ask again)?',
|
|
130
135
|
choices: unchecked.map(p => ({ name: p.name, value: p.name, checked: false })),
|
|
131
136
|
pageSize: 15,
|
|
132
137
|
});
|
|
133
138
|
for (const name of toSkip)
|
|
134
|
-
|
|
139
|
+
this.skipped.add(name);
|
|
140
|
+
}
|
|
141
|
+
async load() {
|
|
142
|
+
[this.included, this.skipped] = await Promise.all([
|
|
143
|
+
loadLines(path.join(this.workspaceDir, '.ragprojects')),
|
|
144
|
+
loadLines(path.join(this.workspaceDir, '.ragskip')),
|
|
145
|
+
]);
|
|
146
|
+
}
|
|
147
|
+
async save() {
|
|
148
|
+
await Promise.all([
|
|
149
|
+
saveLines(path.join(this.workspaceDir, '.ragprojects'), this.included),
|
|
150
|
+
saveLines(path.join(this.workspaceDir, '.ragskip'), this.skipped),
|
|
151
|
+
]);
|
|
135
152
|
}
|
|
136
|
-
await saveConfig(workspaceDir, config);
|
|
137
|
-
const selected = allProjects.filter(p => config.included.has(p.name));
|
|
138
|
-
console.log(`\nSelected: ${selected.map(p => p.name).join(', ') || '(none)'}`);
|
|
139
|
-
return selected;
|
|
140
153
|
}
|
|
154
|
+
exports.ProjectSelector = ProjectSelector;
|
|
@@ -4,7 +4,9 @@ export declare class AstParserService implements IAstParser {
|
|
|
4
4
|
private logger;
|
|
5
5
|
private morphProjects;
|
|
6
6
|
constructor(logger?: LoggerService);
|
|
7
|
-
analyzeProject(filePaths: string[], serviceName: string, projectRoot?: string
|
|
7
|
+
analyzeProject(filePaths: string[], serviceName: string, projectRoot?: string, opts?: {
|
|
8
|
+
skipDependencyResolution?: boolean;
|
|
9
|
+
}): Promise<Map<string, FileAnalysis>>;
|
|
8
10
|
clearCache(projectRoot?: string): void;
|
|
9
11
|
analyzeFile(filePath: string, serviceName: string, projectRoot?: string): Promise<FileAnalysis>;
|
|
10
12
|
extractImportEdges(filePath: string, serviceName: string, projectRoot?: string): Promise<DepEdge[]>;
|
|
@@ -14,4 +16,6 @@ export declare class AstParserService implements IAstParser {
|
|
|
14
16
|
private resolveFromSymbol;
|
|
15
17
|
private buildUsageMap;
|
|
16
18
|
private extractSymbols;
|
|
19
|
+
private extractCalls;
|
|
20
|
+
private getContainerName;
|
|
17
21
|
}
|
|
@@ -51,13 +51,15 @@ class AstParserService {
|
|
|
51
51
|
this.logger = logger;
|
|
52
52
|
}
|
|
53
53
|
// ── public interface ────────────────────────────────────────────────────────
|
|
54
|
-
async analyzeProject(filePaths, serviceName, projectRoot = process.cwd()) {
|
|
54
|
+
async analyzeProject(filePaths, serviceName, projectRoot = process.cwd(), opts = {}) {
|
|
55
55
|
const t0 = Date.now();
|
|
56
|
+
const skipDependencyResolution = opts.skipDependencyResolution ?? false;
|
|
57
|
+
this.logger.debug('AstParser: analyzeProject start', { serviceName, projectRoot, files: filePaths.length, skipDependencyResolution });
|
|
56
58
|
const analysisMap = new Map();
|
|
57
|
-
const morphProject = this.createMorphProject(projectRoot);
|
|
59
|
+
const morphProject = this.createMorphProject(projectRoot, skipDependencyResolution);
|
|
58
60
|
for (const filePath of filePaths) {
|
|
59
61
|
if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
|
|
60
|
-
analysisMap.set(filePath, { edges: [], symbols: [] });
|
|
62
|
+
analysisMap.set(filePath, { edges: [], symbols: [], calls: [] });
|
|
61
63
|
continue;
|
|
62
64
|
}
|
|
63
65
|
try {
|
|
@@ -65,7 +67,7 @@ class AstParserService {
|
|
|
65
67
|
}
|
|
66
68
|
catch (err) {
|
|
67
69
|
this.logger.warn('AstParser: failed to add source file', { filePath, reason: err.message });
|
|
68
|
-
analysisMap.set(filePath, { edges: [], symbols: [] });
|
|
70
|
+
analysisMap.set(filePath, { edges: [], symbols: [], calls: [] });
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
73
|
for (const sourceFile of morphProject.getSourceFiles()) {
|
|
@@ -73,11 +75,12 @@ class AstParserService {
|
|
|
73
75
|
try {
|
|
74
76
|
const edges = this.extractEdges(sourceFile, filePath, serviceName);
|
|
75
77
|
const symbols = this.extractSymbols(sourceFile, filePath, serviceName);
|
|
76
|
-
|
|
78
|
+
const calls = this.extractCalls(sourceFile, filePath, serviceName);
|
|
79
|
+
analysisMap.set(filePath, { edges, symbols, calls });
|
|
77
80
|
}
|
|
78
81
|
catch (err) {
|
|
79
82
|
this.logger.warn('AstParser: failed to analyze file', { filePath, reason: err.message });
|
|
80
|
-
analysisMap.set(filePath, { edges: [], symbols: [] });
|
|
83
|
+
analysisMap.set(filePath, { edges: [], symbols: [], calls: [] });
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
// Release source file memory before returning
|
|
@@ -85,7 +88,7 @@ class AstParserService {
|
|
|
85
88
|
morphProject.removeSourceFile(sourceFile);
|
|
86
89
|
}
|
|
87
90
|
this.logger.debug('AstParser: analyzeProject done', {
|
|
88
|
-
projectRoot, total: filePaths.length, parsed: analysisMap.size, ms: Date.now() - t0,
|
|
91
|
+
serviceName, projectRoot, total: filePaths.length, parsed: analysisMap.size, skipDependencyResolution, ms: Date.now() - t0,
|
|
89
92
|
});
|
|
90
93
|
return analysisMap;
|
|
91
94
|
}
|
|
@@ -101,8 +104,11 @@ class AstParserService {
|
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
async analyzeFile(filePath, serviceName, projectRoot = path.dirname(filePath)) {
|
|
107
|
+
const t0 = Date.now();
|
|
108
|
+
this.logger.debug('AstParser: analyzeFile start', { filePath, serviceName, projectRoot });
|
|
104
109
|
if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
|
|
105
|
-
|
|
110
|
+
this.logger.debug('AstParser: analyzeFile skip (non-TS)', { filePath, ms: Date.now() - t0 });
|
|
111
|
+
return { edges: [], symbols: [], calls: [] };
|
|
106
112
|
}
|
|
107
113
|
const morphProject = this.getMorphProject(projectRoot);
|
|
108
114
|
const existing = morphProject.getSourceFile(filePath);
|
|
@@ -112,13 +118,16 @@ class AstParserService {
|
|
|
112
118
|
try {
|
|
113
119
|
sourceFile = morphProject.addSourceFileAtPath(filePath);
|
|
114
120
|
}
|
|
115
|
-
catch {
|
|
116
|
-
|
|
121
|
+
catch (err) {
|
|
122
|
+
this.logger.warn('AstParser: analyzeFile failed to add source file', { filePath, reason: err.message, ms: Date.now() - t0 });
|
|
123
|
+
return { edges: [], symbols: [], calls: [] };
|
|
117
124
|
}
|
|
118
125
|
const edges = this.extractEdges(sourceFile, filePath, serviceName);
|
|
119
126
|
const symbols = this.extractSymbols(sourceFile, filePath, serviceName);
|
|
127
|
+
const calls = this.extractCalls(sourceFile, filePath, serviceName);
|
|
120
128
|
morphProject.removeSourceFile(sourceFile);
|
|
121
|
-
|
|
129
|
+
this.logger.debug('AstParser: analyzeFile done', { filePath, edges: edges.length, symbols: symbols.length, calls: calls.length, ms: Date.now() - t0 });
|
|
130
|
+
return { edges, symbols, calls };
|
|
122
131
|
}
|
|
123
132
|
// Kept for test compatibility (extract-import-edges.test.ts).
|
|
124
133
|
async extractImportEdges(filePath, serviceName, projectRoot) {
|
|
@@ -126,30 +135,35 @@ class AstParserService {
|
|
|
126
135
|
return edges;
|
|
127
136
|
}
|
|
128
137
|
// ── private: MorphProject cache (LRU, max MORPH_CACHE_MAX entries) ──────────
|
|
129
|
-
createMorphProject(projectRoot) {
|
|
138
|
+
createMorphProject(projectRoot, skipFileDependencyResolution = false) {
|
|
139
|
+
const t0 = Date.now();
|
|
130
140
|
const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
|
|
131
141
|
const hasTsconfig = fs.existsSync(tsconfigPath);
|
|
132
|
-
this.logger.debug('AstParser: tsconfig lookup', { projectRoot, found: hasTsconfig });
|
|
142
|
+
this.logger.debug('AstParser: tsconfig lookup', { projectRoot, found: hasTsconfig, skipFileDependencyResolution });
|
|
133
143
|
let project;
|
|
134
144
|
try {
|
|
135
145
|
const opts = hasTsconfig
|
|
136
|
-
? { tsConfigFilePath: tsconfigPath, skipAddingFilesFromTsConfig: true, skipFileDependencyResolution
|
|
137
|
-
: FALLBACK_OPTS;
|
|
146
|
+
? { tsConfigFilePath: tsconfigPath, skipAddingFilesFromTsConfig: true, skipFileDependencyResolution }
|
|
147
|
+
: { ...FALLBACK_OPTS, skipFileDependencyResolution };
|
|
138
148
|
project = new ts_morph_1.Project(opts);
|
|
139
149
|
if (hasTsconfig) {
|
|
140
150
|
const co = project.getCompilerOptions();
|
|
141
151
|
const pathsMap = co.paths ?? {};
|
|
142
152
|
const pathCount = Object.keys(pathsMap).length;
|
|
143
|
-
this.logger.debug('AstParser: tsconfig loaded', { pathCount });
|
|
153
|
+
this.logger.debug('AstParser: tsconfig loaded', { pathCount, ms: Date.now() - t0 });
|
|
144
154
|
if (pathCount > 0) {
|
|
145
155
|
this.logger.debug('AstParser: path aliases', { aliases: Object.keys(pathsMap) });
|
|
146
156
|
}
|
|
147
157
|
}
|
|
158
|
+
else {
|
|
159
|
+
this.logger.debug('AstParser: MorphProject created (fallback opts)', { ms: Date.now() - t0 });
|
|
160
|
+
}
|
|
148
161
|
}
|
|
149
162
|
catch (err) {
|
|
150
163
|
this.logger.warn('AstParser: tsconfig load failed, using fallback', {
|
|
151
164
|
tsconfigPath,
|
|
152
165
|
reason: err.message,
|
|
166
|
+
ms: Date.now() - t0,
|
|
153
167
|
});
|
|
154
168
|
project = new ts_morph_1.Project(FALLBACK_OPTS);
|
|
155
169
|
}
|
|
@@ -161,6 +175,7 @@ class AstParserService {
|
|
|
161
175
|
const cached = this.morphProjects.get(projectRoot);
|
|
162
176
|
this.morphProjects.delete(projectRoot);
|
|
163
177
|
this.morphProjects.set(projectRoot, cached);
|
|
178
|
+
this.logger.debug('AstParser: getMorphProject cache hit', { projectRoot, cacheSize: this.morphProjects.size });
|
|
164
179
|
return cached;
|
|
165
180
|
}
|
|
166
181
|
if (this.morphProjects.size >= MORPH_CACHE_MAX) {
|
|
@@ -169,6 +184,7 @@ class AstParserService {
|
|
|
169
184
|
this.morphProjects.delete(lruKey);
|
|
170
185
|
this.logger.debug('AstParser: evicted LRU project', { lruKey });
|
|
171
186
|
}
|
|
187
|
+
this.logger.debug('AstParser: getMorphProject cache miss', { projectRoot, cacheSize: this.morphProjects.size });
|
|
172
188
|
const project = this.createMorphProject(projectRoot);
|
|
173
189
|
this.morphProjects.set(projectRoot, project);
|
|
174
190
|
return project;
|
|
@@ -337,5 +353,80 @@ class AstParserService {
|
|
|
337
353
|
}
|
|
338
354
|
return symbols;
|
|
339
355
|
}
|
|
356
|
+
// ── private: call graph extraction ────────────────────────────────────────
|
|
357
|
+
extractCalls(sourceFile, filePath, serviceName) {
|
|
358
|
+
const calls = [];
|
|
359
|
+
for (const callExpr of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)) {
|
|
360
|
+
const expr = callExpr.getExpression();
|
|
361
|
+
let calleeName = null;
|
|
362
|
+
let calleeFile = null;
|
|
363
|
+
let calleeSymbol = null;
|
|
364
|
+
try {
|
|
365
|
+
if (ts_morph_1.Node.isPropertyAccessExpression(expr)) {
|
|
366
|
+
calleeName = expr.getName();
|
|
367
|
+
const sym = expr.getNameNode().getSymbol();
|
|
368
|
+
const decl = sym?.getDeclarations()?.[0];
|
|
369
|
+
const declPath = decl?.getSourceFile().getFilePath();
|
|
370
|
+
if (declPath && !declPath.includes('/node_modules/') && declPath !== filePath) {
|
|
371
|
+
calleeFile = declPath;
|
|
372
|
+
calleeSymbol = calleeName;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
else if (ts_morph_1.Node.isIdentifier(expr)) {
|
|
376
|
+
calleeName = expr.getText();
|
|
377
|
+
const sym = expr.getSymbol();
|
|
378
|
+
if (sym) {
|
|
379
|
+
// Imported identifiers resolve to ImportSpecifier (same file); follow the alias.
|
|
380
|
+
const firstDecl = sym.getDeclarations()?.[0];
|
|
381
|
+
const resolved = firstDecl && ts_morph_1.Node.isImportSpecifier(firstDecl)
|
|
382
|
+
? (sym.getAliasedSymbol() ?? sym)
|
|
383
|
+
: sym;
|
|
384
|
+
const decl = resolved.getDeclarations()?.[0];
|
|
385
|
+
const declPath = decl?.getSourceFile().getFilePath();
|
|
386
|
+
if (declPath && !declPath.includes('/node_modules/') && declPath !== filePath) {
|
|
387
|
+
calleeFile = declPath;
|
|
388
|
+
calleeSymbol = calleeName;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
// type resolution failures are normal — skip this call
|
|
395
|
+
}
|
|
396
|
+
if (!calleeName || !calleeFile)
|
|
397
|
+
continue;
|
|
398
|
+
const fromSymbol = this.getContainerName(callExpr);
|
|
399
|
+
const pos = sourceFile.compilerNode
|
|
400
|
+
.getLineAndCharacterOfPosition(callExpr.getStart());
|
|
401
|
+
calls.push({
|
|
402
|
+
project: serviceName,
|
|
403
|
+
fromFile: filePath,
|
|
404
|
+
fromSymbol,
|
|
405
|
+
calleeName,
|
|
406
|
+
calleeFile,
|
|
407
|
+
calleeSymbol,
|
|
408
|
+
line: pos.line + 1,
|
|
409
|
+
col: pos.character + 1,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return calls;
|
|
413
|
+
}
|
|
414
|
+
getContainerName(node) {
|
|
415
|
+
let current = node.getParent();
|
|
416
|
+
while (current) {
|
|
417
|
+
if (ts_morph_1.Node.isMethodDeclaration(current) || ts_morph_1.Node.isFunctionDeclaration(current)) {
|
|
418
|
+
return current.getName() ?? null;
|
|
419
|
+
}
|
|
420
|
+
if (ts_morph_1.Node.isArrowFunction(current) || ts_morph_1.Node.isFunctionExpression(current)) {
|
|
421
|
+
const parent = current.getParent();
|
|
422
|
+
if (ts_morph_1.Node.isVariableDeclaration(parent))
|
|
423
|
+
return parent.getName();
|
|
424
|
+
if (ts_morph_1.Node.isPropertyDeclaration(parent))
|
|
425
|
+
return parent.getName();
|
|
426
|
+
}
|
|
427
|
+
current = current.getParent();
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
340
431
|
}
|
|
341
432
|
exports.AstParserService = AstParserService;
|
|
@@ -61,6 +61,7 @@ class DatabaseService {
|
|
|
61
61
|
const { host, port, username, password, database } = this.config.db;
|
|
62
62
|
this.pool = new pg_1.Pool({ host, port, user: username, password, database });
|
|
63
63
|
this._db = (0, node_postgres_1.drizzle)(this.pool, { schema });
|
|
64
|
+
await this.pool.query('SELECT 1');
|
|
64
65
|
}
|
|
65
66
|
async initDbWithSchema() {
|
|
66
67
|
await this.ensureDatabaseExists();
|
|
@@ -141,6 +142,23 @@ class DatabaseService {
|
|
|
141
142
|
await this.query(`CREATE INDEX IF NOT EXISTS idx_symbol_name ON code_symbol (name, project)`);
|
|
142
143
|
await this.query(`CREATE INDEX IF NOT EXISTS idx_symbol_file ON code_symbol (project, "filePath")`);
|
|
143
144
|
await this.query(`CREATE UNIQUE INDEX IF NOT EXISTS idx_symbol_unique ON code_symbol (project, "filePath", name, kind)`);
|
|
145
|
+
// code_call table
|
|
146
|
+
await this.query(`
|
|
147
|
+
CREATE TABLE IF NOT EXISTS code_call (
|
|
148
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
149
|
+
project TEXT NOT NULL,
|
|
150
|
+
"fromFile" TEXT NOT NULL,
|
|
151
|
+
"fromSymbol" TEXT,
|
|
152
|
+
"calleeName" TEXT NOT NULL,
|
|
153
|
+
"calleeFile" TEXT NOT NULL,
|
|
154
|
+
"calleeSymbol" TEXT,
|
|
155
|
+
line INT,
|
|
156
|
+
col INT
|
|
157
|
+
)
|
|
158
|
+
`);
|
|
159
|
+
await this.query(`CREATE INDEX IF NOT EXISTS idx_call_callee ON code_call (project, "calleeName")`);
|
|
160
|
+
await this.query(`CREATE INDEX IF NOT EXISTS idx_call_callee_file ON code_call (project, "calleeFile")`);
|
|
161
|
+
await this.query(`CREATE INDEX IF NOT EXISTS idx_call_from ON code_call (project, "fromFile")`);
|
|
144
162
|
}
|
|
145
163
|
async ensureDatabaseExists() {
|
|
146
164
|
const { host, port, username, password, database } = this.config.db;
|
|
@@ -27,6 +27,12 @@ export declare class IndexerService {
|
|
|
27
27
|
skipped: number;
|
|
28
28
|
failed: number;
|
|
29
29
|
}>;
|
|
30
|
+
backfillCalls(signal?: AbortSignal): Promise<{
|
|
31
|
+
total: number;
|
|
32
|
+
processed: number;
|
|
33
|
+
skipped: number;
|
|
34
|
+
failed: number;
|
|
35
|
+
}>;
|
|
30
36
|
discoverProjects(rootDir: string): {
|
|
31
37
|
name: string;
|
|
32
38
|
path: string;
|
|
@@ -34,6 +40,7 @@ export declare class IndexerService {
|
|
|
34
40
|
private writeDepEdges;
|
|
35
41
|
scanAndIndex(dir: string, serviceName: string, force?: boolean, signal?: AbortSignal, preCollectedFiles?: string[]): Promise<IndexStats>;
|
|
36
42
|
private getSplitter;
|
|
43
|
+
private static inferRole;
|
|
37
44
|
private getEnrichedPrompt;
|
|
38
45
|
private embedText;
|
|
39
46
|
private static hashContent;
|